package mil.nga.geopackage.features.user;

import java.util.ArrayList;
import java.util.List;

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.features.index.FeatureIndexManager;
import mil.nga.sf.GeometryEnvelope;
import mil.nga.sf.proj.Projection;
import mil.nga.sf.proj.ProjectionTransform;

/**
 * Performs manual brute force queries against feature rows. See
 * {@link FeatureIndexManager} for performing indexed queries.
 *
 * @author osbornb
 * @since 3.1.0
 */
public class ManualFeatureQuery {

    /**
     * Feature DAO
     */
    private final FeatureDao featureDao;

    /**
     * Query single chunk limit
     */
    protected int chunkLimit = 1000;

    /**
     * Query range tolerance
     */
    protected double tolerance = .00000000000001;

    /**
     * Constructor
     *
     * @param featureDao feature DAO
     */
    public ManualFeatureQuery(FeatureDao featureDao) {
        this.featureDao = featureDao;
    }

    /**
     * Get the feature DAO
     *
     * @return feature DAO
     */
    public FeatureDao getFeatureDao() {
        return featureDao;
    }

    /**
     * Get the SQL query chunk limit
     *
     * @return chunk limit
     */
    public int getChunkLimit() {
        return chunkLimit;
    }

    /**
     * Set the SQL query chunk limit
     *
     * @param chunkLimit chunk limit
     */
    public void setChunkLimit(int chunkLimit) {
        this.chunkLimit = chunkLimit;
    }

    /**
     * Get the query range tolerance
     *
     * @return query range tolerance
     */
    public double getTolerance() {
        return tolerance;
    }

    /**
     * Set the query range tolerance
     *
     * @param tolerance query range tolerance
     */
    public void setTolerance(double tolerance) {
        this.tolerance = tolerance;
    }

    /**
     * Get the count of features
     *
     * @return count
     */
    public int count() {
        return featureDao.count();
    }

    /**
     * Get the count of features with non null geometries
     *
     * @return count
     */
    public int countWithGeometries() {
        return featureDao.count(featureDao.getGeometryColumnName() + " IS NOT NULL", null);
    }

    /**
     * Manually build the bounds of the feature table
     *
     * @return bounding box
     */
    public BoundingBox getBoundingBox() {

        GeometryEnvelope envelope = null;

        long offset = 0;
        boolean hasResults = true;

        while (hasResults) {

            hasResults = false;

            FeatureCursor featureCursor = featureDao.queryForChunk(chunkLimit,
                    offset);
            try {
                while (featureCursor.moveToNext()) {
                    hasResults = true;

                    FeatureRow featureRow = featureCursor.getRow();
                    GeometryEnvelope featureEnvelope = featureRow
                            .getGeometryEnvelope();
                    if (featureEnvelope != null) {

                        if (envelope == null) {
                            envelope = featureEnvelope;
                        } else {
                            envelope = envelope.union(featureEnvelope);
                        }

                    }
                }
            } finally {
                featureCursor.close();
            }

            offset += chunkLimit;
        }

        BoundingBox boundingBox = null;
        if (envelope != null) {
            boundingBox = new BoundingBox(envelope);
        }

        return boundingBox;
    }

    /**
     * Manually build the bounds of the feature table in the provided projection
     *
     * @param projection desired projection
     * @return bounding box
     */
    public BoundingBox getBoundingBox(Projection projection) {
        BoundingBox boundingBox = getBoundingBox();
        if (boundingBox != null && projection != null) {
            ProjectionTransform projectionTransform = featureDao
                    .getProjection().getTransformation(projection);
            boundingBox = boundingBox.transform(projectionTransform);
        }
        return boundingBox;
    }

    /**
     * Manually query for rows within the bounding box
     *
     * @param boundingBox bounding box
     * @return results
     */
    public ManualFeatureQueryResults query(BoundingBox boundingBox) {
        return query(boundingBox.buildEnvelope());
    }

    /**
     * Manually query for rows within the bounding box in the provided
     * projection
     *
     * @param boundingBox bounding box
     * @param projection  projection
     * @return results
     */
    public ManualFeatureQueryResults query(BoundingBox boundingBox,
                                           Projection projection) {
        BoundingBox featureBoundingBox = featureDao.projectBoundingBox(
                boundingBox, projection);
        return query(featureBoundingBox);
    }

    /**
     * Manually count the rows within the bounding box
     *
     * @param boundingBox bounding box
     * @return count
     */
    public long count(BoundingBox boundingBox) {
        return count(boundingBox.buildEnvelope());
    }

    /**
     * Manually count the rows within the bounding box in the provided
     * projection
     *
     * @param boundingBox bounding box
     * @param projection  projection
     * @return count
     */
    public long count(BoundingBox boundingBox, Projection projection) {
        BoundingBox featureBoundingBox = featureDao.projectBoundingBox(
                boundingBox, projection);
        return count(featureBoundingBox);
    }

    /**
     * Manually query for rows within the geometry envelope
     *
     * @param envelope geometry envelope
     * @return results
     */
    public ManualFeatureQueryResults query(GeometryEnvelope envelope) {
        return query(envelope.getMinX(), envelope.getMinY(),
                envelope.getMaxX(), envelope.getMaxY());
    }

    /**
     * Manually count the rows within the geometry envelope
     *
     * @param envelope geometry envelope
     * @return count
     */
    public long count(GeometryEnvelope envelope) {
        return count(envelope.getMinX(), envelope.getMinY(),
                envelope.getMaxX(), envelope.getMaxY());
    }

    /**
     * Manually query for rows within the bounds
     *
     * @param minX min x
     * @param minY min y
     * @param maxX max x
     * @param maxY max y
     * @return results
     */
    public ManualFeatureQueryResults query(double minX, double minY,
                                           double maxX, double maxY) {

        List<Long> featureIds = new ArrayList<>();

        long offset = 0;
        boolean hasResults = true;

        minX -= tolerance;
        maxX += tolerance;
        minY -= tolerance;
        maxY += tolerance;

        while (hasResults) {

            hasResults = false;

            FeatureCursor featureCursor = featureDao.queryForChunk(chunkLimit,
                    offset);
            try {
                while (featureCursor.moveToNext()) {
                    hasResults = true;

                    FeatureRow featureRow = featureCursor.getRow();
                    GeometryEnvelope envelope = featureRow
                            .getGeometryEnvelope();
                    if (envelope != null) {

                        double minXMax = Math.max(minX, envelope.getMinX());
                        double maxXMin = Math.min(maxX, envelope.getMaxX());
                        double minYMax = Math.max(minY, envelope.getMinY());
                        double maxYMin = Math.min(maxY, envelope.getMaxY());

                        if (minXMax <= maxXMin && minYMax <= maxYMin) {
                            featureIds.add(featureRow.getId());
                        }

                    }
                }
            } finally {
                featureCursor.close();
            }

            offset += chunkLimit;
        }

        ManualFeatureQueryResults results = new ManualFeatureQueryResults(
                featureDao, featureIds);

        return results;
    }

    /**
     * Manually count the rows within the bounds
     *
     * @param minX min x
     * @param minY min y
     * @param maxX max x
     * @param maxY max y
     * @return count
     */
    public long count(double minX, double minY, double maxX, double maxY) {
        return query(minX, minY, maxX, maxY).count();
    }

}


package mil.nga.geopackage.features.index;

import java.util.Iterator;

import mil.nga.geopackage.features.user.FeatureCursor;
import mil.nga.geopackage.features.user.FeatureRow;

/**
 * Iterable Feature Index Results to iterate on feature results from a feature
 * DAO
 *
 * @author osbornb
 * @since 3.1.0
 */
public class FeatureIndexFeatureResults implements FeatureIndexResults {

    /**
     * Cursor
     */
    private final FeatureCursor cursor;

    /**
     * Constructor
     *
     * @param cursor result cursor
     */
    public FeatureIndexFeatureResults(FeatureCursor cursor) {
        this.cursor = cursor;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Iterator<FeatureRow> iterator() {
        return new Iterator<FeatureRow>() {

            /**
             * {@inheritDoc}
             */
            @Override
            public boolean hasNext() {
                return cursor.moveToNext();
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public FeatureRow next() {
                return cursor.getRow();
            }
        };
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public long count() {
        return cursor.getCount();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void close() {
        cursor.close();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Iterable<Long> ids() {
        return new Iterable<Long>() {

            /**
             * {@inheritDoc}
             */
            @Override
            public Iterator<Long> iterator() {
                return new Iterator<Long>() {

                    /**
                     * {@inheritDoc}
                     */
                    @Override
                    public boolean hasNext() {
                        return cursor.moveToNext();
                    }

                    /**
                     * {@inheritDoc}
                     */
                    @Override
                    public Long next() {
                        return cursor.getRow().getId();
                    }

                };
            }
        };
    }

}


package mil.nga.geopackage.extension.related.media;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import java.io.IOException;

import mil.nga.geopackage.io.BitmapConverter;
import mil.nga.geopackage.user.custom.UserCustomColumn;
import mil.nga.geopackage.user.custom.UserCustomRow;

/**
 * User Media Row containing the values from a single cursor row
 *
 * @author osbornb
 * @since 3.0.1
 */
public class MediaRow extends UserCustomRow {

    /**
     * Constructor to create an empty row
     *
     * @param table media table
     */
    protected MediaRow(MediaTable table) {
        super(table);
    }

    /**
     * Constructor
     *
     * @param userCustomRow user custom row
     */
    public MediaRow(UserCustomRow userCustomRow) {
        super(userCustomRow.getTable(), userCustomRow.getRowColumnTypes(),
                userCustomRow.getValues());
    }

    /**
     * Copy Constructor
     *
     * @param mediaRow media row to copy
     */
    public MediaRow(MediaRow mediaRow) {
        super(mediaRow);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public MediaTable getTable() {
        return (MediaTable) super.getTable();
    }

    /**
     * Get the id column index
     *
     * @return id column index
     */
    public int getIdColumnIndex() {
        return getTable().getIdColumnIndex();
    }

    /**
     * Get the id column
     *
     * @return id column
     */
    public UserCustomColumn getIdColumn() {
        return getTable().getIdColumn();
    }

    /**
     * Get the id
     *
     * @return id
     */
    public long getId() {
        return ((Number) getValue(getIdColumnIndex())).longValue();
    }

    /**
     * Get the data column index
     *
     * @return data column index
     */
    public int getDataColumnIndex() {
        return getTable().getDataColumnIndex();
    }

    /**
     * Get the data column
     *
     * @return data column
     */
    public UserCustomColumn getDataColumn() {
        return getTable().getDataColumn();
    }

    /**
     * Get the data
     *
     * @return data
     */
    public byte[] getData() {
        return (byte[]) getValue(getDataColumnIndex());
    }

    /**
     * Set the data
     *
     * @param data data
     */
    public void setData(byte[] data) {
        setValue(getDataColumnIndex(), data);
    }

    /**
     * Read the data bounds without allocating pixel memory.
     * Access values using:
     * {@link BitmapFactory.Options#outWidth},
     * {@link BitmapFactory.Options#outHeight},
     * {@link BitmapFactory.Options#outMimeType},
     * {@link BitmapFactory.Options#outColorSpace},
     * and {@link BitmapFactory.Options#outConfig}
     *
     * @return bounds options
     * @since 3.2.0
     */
    public BitmapFactory.Options getDataBounds() {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        byte[] data = getData();
        BitmapFactory.decodeByteArray(data, 0, data.length, options);
        return options;
    }

    /**
     * Get the data bitmap
     *
     * @return data bitmap
     * @since 3.2.0
     */
    public Bitmap getDataBitmap() {
        return getDataBitmap(null);
    }

    /**
     * Get the data bitmap with decoding options
     *
     * @param options bitmap options
     * @return data bitmap
     * @since 3.2.0
     */
    public Bitmap getDataBitmap(BitmapFactory.Options options) {
        return BitmapConverter.toBitmap(getData(), options);
    }

    /**
     * Set the data from a full quality bitmap
     *
     * @param bitmap bitmap
     * @param format compress format
     * @throws IOException upon failure
     * @since 3.2.0
     */
    public void setData(Bitmap bitmap, Bitmap.CompressFormat format)
            throws IOException {
        setData(bitmap, format, 100);
    }

    /**
     * Set the data from a bitmap
     *
     * @param bitmap  bitmap
     * @param format  compress format
     * @param quality quality
     * @throws IOException upon failure
     * @since 3.2.0
     */
    public void setData(Bitmap bitmap, Bitmap.CompressFormat format, int quality)
            throws IOException {
        setData(BitmapConverter.toBytes(bitmap, format, quality));
    }

    /**
     * Get the content type column index
     *
     * @return content type column index
     */
    public int getContentTypeColumnIndex() {
        return getTable().getContentTypeColumnIndex();
    }

    /**
     * Get the content type column
     *
     * @return content type column
     */
    public UserCustomColumn getContentTypeColumn() {
        return getTable().getContentTypeColumn();
    }

    /**
     * Get the content type
     *
     * @return content type
     */
    public String getContentType() {
        return getValue(getContentTypeColumnIndex()).toString();
    }

    /**
     * Set the content type
     *
     * @param contentType content type
     */
    public void setContentType(String contentType) {
        setValue(getContentTypeColumnIndex(), contentType);
    }

    /**
     * Copy the row
     *
     * @return row copy
     */
    public MediaRow copy() {
        return new MediaRow(this);
    }

}


package mil.nga.geopackage.tiles;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory.Options;
import android.util.SparseArray;

import org.locationtech.proj4j.units.Units;

import java.io.IOException;
import java.sql.SQLException;
import java.util.Date;

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.GeoPackage;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.core.contents.Contents;
import mil.nga.geopackage.core.contents.ContentsDao;
import mil.nga.geopackage.core.srs.SpatialReferenceSystem;
import mil.nga.geopackage.core.srs.SpatialReferenceSystemDao;
import mil.nga.geopackage.extension.scale.TileScaling;
import mil.nga.geopackage.extension.scale.TileTableScaling;
import mil.nga.geopackage.io.BitmapConverter;
import mil.nga.geopackage.io.GeoPackageProgress;
import mil.nga.geopackage.tiles.matrix.TileMatrix;
import mil.nga.geopackage.tiles.matrix.TileMatrixDao;
import mil.nga.geopackage.tiles.matrix.TileMatrixKey;
import mil.nga.geopackage.tiles.matrixset.TileMatrixSet;
import mil.nga.geopackage.tiles.matrixset.TileMatrixSetDao;
import mil.nga.geopackage.tiles.user.TileCursor;
import mil.nga.geopackage.tiles.user.TileDao;
import mil.nga.geopackage.tiles.user.TileRow;
import mil.nga.geopackage.tiles.user.TileTable;
import mil.nga.sf.proj.Projection;
import mil.nga.sf.proj.ProjectionConstants;
import mil.nga.sf.proj.ProjectionFactory;
import mil.nga.sf.proj.ProjectionTransform;

/**
 * Creates a set of tiles within a GeoPackage
 *
 * @author osbornb
 */
public abstract class TileGenerator {

    /**
     * Context
     */
    protected final Context context;

    /**
     * GeoPackage
     */
    private final GeoPackage geoPackage;

    /**
     * Table Name
     */
    private final String tableName;

    /**
     * Min zoom level
     */
    private final int minZoom;

    /**
     * Max zoom level
     */
    private final int maxZoom;

    /**
     * Tiles projection
     */
    protected Projection projection;

    /**
     * Total tile count
     */
    private Integer tileCount;

    /**
     * Tile grids by zoom level
     */
    private final SparseArray<TileGrid> tileGrids = new SparseArray<>();

    /**
     * Tile bounding boxes by zoom level
     */
    private final SparseArray<BoundingBox> tileBounds = new SparseArray<>();

    /**
     * Tile bounding box
     */
    protected BoundingBox boundingBox;

    /**
     * Compress format
     */
    private CompressFormat compressFormat = null;

    /**
     * Compress quality
     */
    private int compressQuality = 100;

    /**
     * GeoPackage progress
     */
    private GeoPackageProgress progress;

    /**
     * Compression options
     */
    private Options options = null;

    /**
     * True when generating tiles in Google tile format, false when generating
     * GeoPackage format where rows and columns do not match the Google row &
     * column coordinates
     */
    private boolean googleTiles = false;

    /**
     * Tile grid bounding box
     */
    private BoundingBox tileGridBoundingBox;

    /**
     * Matrix height when GeoPackage tile format
     */
    private long matrixHeight = 0;

    /**
     * Matrix width when GeoPackage tile format
     */
    private long matrixWidth = 0;

    /**
     * Tile scaling settings
     */
    private TileScaling scaling = null;

    /**
     * Constructor
     *
     * @param context     app context
     * @param geoPackage  GeoPackage
     * @param tableName   table name
     * @param minZoom     min zoom
     * @param maxZoom     max zoom
     * @param boundingBox tiles bounding box
     * @param projection  tiles projection
     * @since 1.3.0
     */
    public TileGenerator(Context context, GeoPackage geoPackage,
                         String tableName, int minZoom, int maxZoom, BoundingBox boundingBox, Projection projection) {
        this.context = context;
        geoPackage.verifyWritable();
        this.geoPackage = geoPackage;
        this.tableName = tableName;

        this.minZoom = minZoom;
        this.maxZoom = maxZoom;
        this.boundingBox = boundingBox;
        this.projection = projection;
    }

    /**
     * Get the GeoPackage
     *
     * @return GeoPackage
     * @since 1.2.5
     */
    public GeoPackage getGeoPackage() {
        return geoPackage;
    }

    /**
     * Get the table name
     *
     * @return table name
     * @since 1.2.5
     */
    public String getTableName() {
        return tableName;
    }

    /**
     * Get the min zoom
     *
     * @return min zoom
     * @since 1.2.5
     */
    public int getMinZoom() {
        return minZoom;
    }

    /**
     * Get the max zoom
     *
     * @return max zoom
     * @since 1.2.5
     */
    public int getMaxZoom() {
        return maxZoom;
    }

    /**
     * Get bounding box
     *
     * @return bounding box
     * @since 3.2.0
     */
    public BoundingBox getBoundingBox() {
        return boundingBox;
    }

    /**
     * Get the bounding box, possibly expanded for the zoom level
     *
     * @param zoom zoom level
     * @return original or expanded bounding box
     * @since 3.2.0
     */
    public BoundingBox getBoundingBox(int zoom) {
        return boundingBox;
    }

    /**
     * Set the compress format
     *
     * @param compressFormat compression format
     */
    public void setCompressFormat(CompressFormat compressFormat) {
        this.compressFormat = compressFormat;
    }

    /**
     * Get the compress format
     *
     * @return compress format
     * @since 1.2.5
     */
    public CompressFormat getCompressFormat() {
        return compressFormat;
    }

    /**
     * Set the compress quality. The Compress format must be set for this to be
     * used.
     *
     * @param compressQuality compression quality
     */
    public void setCompressQuality(Integer compressQuality) {
        if (compressQuality != null) {
            this.compressQuality = compressQuality;
        }
    }

    /**
     * Get the compress quality
     *
     * @return compress quality or null
     * @since 1.2.5
     */
    public Integer getCompressQuality() {
        return compressQuality;
    }

    /**
     * Set the progress tracker
     *
     * @param progress progress tracker
     */
    public void setProgress(GeoPackageProgress progress) {
        this.progress = progress;
    }

    /**
     * Get the progress tracker
     *
     * @return progress
     * @since 1.2.5
     */
    public GeoPackageProgress getProgress() {
        return progress;
    }

    /**
     * Set the Bitmap Compress Config
     *
     * @param config bitmap config
     */
    public void setBitmapCompressionConfig(Config config) {
        if (options == null) {
            options = new Options();
        }
        options.inPreferredConfig = config;
    }

    /**
     * Set the Google Tiles flag to true to generate Google tile format tiles.
     * Default is false
     *
     * @param googleTiles Google Tiles flag
     */
    public void setGoogleTiles(boolean googleTiles) {
        this.googleTiles = googleTiles;
    }

    /**
     * Is the Google Tiles flag set to generate Google tile format tiles.
     *
     * @return true if Google Tiles format, false if GeoPackage
     * @since 1.2.5
     */
    public boolean isGoogleTiles() {
        return googleTiles;
    }

    /**
     * Get the tile scaling settings
     *
     * @return tile scaling
     * @since 2.0.2
     */
    public TileScaling getScaling() {
        return scaling;
    }

    /**
     * Set the tile scaling settings
     *
     * @param scaling tile scaling
     * @since 2.0.2
     */
    public void setScaling(TileScaling scaling) {
        this.scaling = scaling;
    }

    /**
     * Get the tile count of tiles to be generated
     *
     * @return tile count
     */
    public int getTileCount() {
        if (tileCount == null) {
            long count = 0;

            boolean degrees = projection.isUnit(Units.DEGREES);
            ProjectionTransform transformToWebMercator = null;
            if (!degrees) {
                transformToWebMercator = projection.getTransformation(ProjectionConstants.EPSG_WEB_MERCATOR);
            }

            for (int zoom = minZoom; zoom <= maxZoom; zoom++) {

                BoundingBox expandedBoundingBox = getBoundingBox(zoom);

                // Get the tile grid that includes the entire bounding box
                TileGrid tileGrid = null;
                if (degrees) {
                    tileGrid = TileBoundingBoxUtils.getTileGridWGS84(expandedBoundingBox, zoom);
                } else {
                    tileGrid = TileBoundingBoxUtils.getTileGrid(expandedBoundingBox.transform(transformToWebMercator), zoom);
                }

                count += tileGrid.count();
                tileGrids.put(zoom, tileGrid);
                tileBounds.put(zoom, expandedBoundingBox);
            }

            tileCount = (int) Math.min(count, Integer.MAX_VALUE);
        }
        return tileCount;
    }

    /**
     * Generate the tiles
     *
     * @return tiles created
     * @throws java.sql.SQLException upon failure
     * @throws java.io.IOException   upon failure
     */
    public int generateTiles() throws SQLException, IOException {

        int totalCount = getTileCount();

        // Set the max progress count
        if (progress != null) {
            progress.setMax(totalCount);
        }

        int count = 0;
        boolean update = false;

        // Adjust the tile matrix set and bounds
        BoundingBox minZoomBoundingBox = tileBounds.get(minZoom);
        adjustBounds(minZoomBoundingBox, minZoom);

        // Create a new tile matrix or update an existing
        TileMatrixSetDao tileMatrixSetDao = geoPackage.getTileMatrixSetDao();
        TileMatrixSet tileMatrixSet = null;
        if (!tileMatrixSetDao.isTableExists()
                || !tileMatrixSetDao.idExists(tableName)) {
            // Create the srs if needed
            SpatialReferenceSystemDao srsDao = geoPackage.getSpatialReferenceSystemDao();
            SpatialReferenceSystem srs = srsDao.getOrCreateCode(projection.getAuthority(),
                    Long.parseLong(projection.getCode()));
            // Create the tile table
            tileMatrixSet = geoPackage.createTileTableWithMetadata(
                    tableName,
                    boundingBox,
                    srs.getSrsId(),
                    tileGridBoundingBox,
                    srs.getSrsId());
        } else {
            update = true;
            // Query to get the Tile Matrix Set
            tileMatrixSet = tileMatrixSetDao.queryForId(tableName);

            // Update the tile bounds between the existing and this request
            updateTileBounds(tileMatrixSet);
        }

        preTileGeneration();

        // If tile scaling is set, create the tile scaling extension entry
        if (scaling != null) {
            TileTableScaling tileTableScaling = new TileTableScaling(geoPackage, tileMatrixSet);
            tileTableScaling.createOrUpdate(scaling);
        }

        // Create the tiles
        try {
            Contents contents = tileMatrixSet.getContents();
            TileMatrixDao tileMatrixDao = geoPackage.getTileMatrixDao();
            TileDao tileDao = geoPackage.getTileDao(tileMatrixSet);

            // Create the new matrix tiles
            for (int zoom = minZoom; zoom <= maxZoom
                    && (progress == null || progress.isActive()); zoom++) {

                TileGrid localTileGrid = null;

                // Determine the matrix width and height for Google format
                if (googleTiles) {
                    matrixWidth = TileBoundingBoxUtils.tilesPerSide(zoom);
                    matrixHeight = matrixWidth;
                }
                // Get the local tile grid for GeoPackage format of where the
                // tiles belong
                else {
                    BoundingBox zoomBoundingBox = tileBounds.get(zoom);
                    localTileGrid = TileBoundingBoxUtils.getTileGrid(
                            tileGridBoundingBox, matrixWidth, matrixHeight,
                            zoomBoundingBox);
                }

                // Generate the tiles for the zoom level
                TileGrid tileGrid = tileGrids.get(zoom);
                count += generateTiles(tileMatrixDao, tileDao, contents, zoom,
                        tileGrid, localTileGrid, matrixWidth, matrixHeight,
                        update);

                if (!googleTiles) {
                    // Double the matrix width and height for the next level
                    matrixWidth *= 2;
                    matrixHeight *= 2;
                }
            }

            // Delete the table if cancelled
            if (progress != null && !progress.isActive()
                    && progress.cleanupOnCancel()) {
                geoPackage.deleteTableQuietly(tableName);
                count = 0;
            } else {
                // Update the contents last modified date
                contents.setLastChange(new Date());
                ContentsDao contentsDao = geoPackage.getContentsDao();
                contentsDao.update(contents);
            }
        } catch (RuntimeException e) {
            geoPackage.deleteTableQuietly(tableName);
            throw e;
        } catch (SQLException e) {
            geoPackage.deleteTableQuietly(tableName);
            throw e;
        } catch (IOException e) {
            geoPackage.deleteTableQuietly(tableName);
            throw e;
        }

        return count;
    }

    /**
     * Adjust the tile matrix set and bounds
     *
     * @param boundingBox bounding box
     * @param zoom        zoom
     */
    private void adjustBounds(BoundingBox boundingBox,
                              int zoom) {
        // Google Tile Format
        if (googleTiles) {
            adjustGoogleBounds();
        } else if (projection.isUnit(Units.DEGREES)) {
            adjustGeoPackageBoundsWGS84(boundingBox, zoom);
        } else {
            adjustGeoPackageBounds(boundingBox, zoom);
        }
    }

    /**
     * Adjust the tile matrix set and web mercator bounds for Google tile format
     */
    private void adjustGoogleBounds() {
        // Set the tile matrix set bounding box to be the world
        BoundingBox standardWgs84Box = new BoundingBox(-ProjectionConstants.WGS84_HALF_WORLD_LON_WIDTH,
                ProjectionConstants.WEB_MERCATOR_MIN_LAT_RANGE,
                ProjectionConstants.WGS84_HALF_WORLD_LON_WIDTH,
                ProjectionConstants.WEB_MERCATOR_MAX_LAT_RANGE);
        ProjectionTransform wgs84ToWebMercatorTransform = ProjectionFactory.getProjection(ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM)
                .getTransformation(ProjectionConstants.EPSG_WEB_MERCATOR);
        tileGridBoundingBox = standardWgs84Box.transform(wgs84ToWebMercatorTransform);
    }

    /**
     * Adjust the tile matrix set and WGS84 bounds for GeoPackage format.
     * Determine the tile grid width and height
     *
     * @param boundingBox
     * @param zoom
     */
    private void adjustGeoPackageBoundsWGS84(BoundingBox boundingBox, int zoom) {
        // Get the fitting tile grid and determine the bounding box that fits it
        TileGrid tileGrid = TileBoundingBoxUtils.getTileGridWGS84(boundingBox, zoom);
        tileGridBoundingBox = TileBoundingBoxUtils.getWGS84BoundingBox(tileGrid, zoom);
        matrixWidth = tileGrid.getMaxX() + 1 - tileGrid.getMinX();
        matrixHeight = tileGrid.getMaxY() + 1 - tileGrid.getMinY();
    }

    /**
     * Adjust the tile matrix set and web mercator bounds for GeoPackage format.
     * Determine the tile grid width and height
     *
     * @param requestWebMercatorBoundingBox
     * @param zoom
     */
    private void adjustGeoPackageBounds(
            BoundingBox requestWebMercatorBoundingBox, int zoom) {
        // Get the fitting tile grid and determine the bounding box that
        // fits it
        TileGrid tileGrid = TileBoundingBoxUtils.getTileGrid(
                requestWebMercatorBoundingBox, zoom);
        tileGridBoundingBox = TileBoundingBoxUtils.getWebMercatorBoundingBox(tileGrid, zoom);
        matrixWidth = tileGrid.getMaxX() + 1 - tileGrid.getMinX();
        matrixHeight = tileGrid.getMaxY() + 1 - tileGrid.getMinY();
    }

    /**
     * Update the Content and Tile Matrix Set bounds
     *
     * @param tileMatrixSet
     * @throws java.sql.SQLException
     */
    private void updateTileBounds(TileMatrixSet tileMatrixSet)
            throws SQLException {

        TileDao tileDao = geoPackage.getTileDao(tileMatrixSet);

        if (tileDao.isGoogleTiles()) {
            if (!googleTiles) {
                // If adding GeoPackage tiles to a Google Tile format, add them
                // as Google tiles
                googleTiles = true;
                adjustGoogleBounds();
            }
        } else if (googleTiles) {
            // Can't add Google formatted tiles to GeoPackage tiles
            throw new GeoPackageException(
                    "Can not add Google formatted tiles to "
                            + tableName
                            + " which already contains GeoPackage formatted tiles");
        }

        Projection tileMatrixProjection = tileMatrixSet.getProjection();
        if (!tileMatrixProjection.equals(projection)) {
            throw new GeoPackageException("Can not update tiles projected at "
                    + tileMatrixProjection.getCode() + " with tiles projected at " + projection.getCode());
        }

        Contents contents = tileMatrixSet.getContents();

        // Combine the existing content and request bounding boxes
        BoundingBox previousContentsBoundingBox = contents.getBoundingBox();
        if (previousContentsBoundingBox != null) {
            ProjectionTransform transformProjectionToContents = projection.getTransformation(contents.getProjection());
            BoundingBox contentsBoundingBox = boundingBox;
            if (!transformProjectionToContents.isSameProjection()) {
                contentsBoundingBox = contentsBoundingBox.transform(transformProjectionToContents);
            }
            contentsBoundingBox = contentsBoundingBox.union(previousContentsBoundingBox);

            // Update the contents if modified
            if (!contentsBoundingBox.equals(previousContentsBoundingBox)) {
                contents.setBoundingBox(contentsBoundingBox);
                ContentsDao contentsDao = geoPackage.getContentsDao();
                contentsDao.update(contents);
            }
        }

        // If updating GeoPackage format tiles, all existing metadata and tile
        // rows needs to be adjusted
        if (!googleTiles) {

            BoundingBox previousTileMatrixSetBoundingBox = tileMatrixSet.getBoundingBox();

            // Adjust the bounds to include the request and existing bounds
            ProjectionTransform transformProjectionToTileMatrixSet = projection.getTransformation(tileMatrixProjection);
            boolean sameProjection = transformProjectionToTileMatrixSet.isSameProjection();
            BoundingBox updateBoundingBox = tileBounds.get(minZoom);
            if (!sameProjection) {
                updateBoundingBox = updateBoundingBox.transform(transformProjectionToTileMatrixSet);
            }
            int minNewOrUpdateZoom = Math.min(minZoom, (int) tileDao.getMinZoom());
            adjustBounds(updateBoundingBox, minNewOrUpdateZoom);

            // Update the tile matrix set if modified
            BoundingBox updateTileGridBoundingBox = tileGridBoundingBox;
            if (!sameProjection) {
                updateTileGridBoundingBox = updateTileGridBoundingBox.transform(transformProjectionToTileMatrixSet);
            }
            if (!previousTileMatrixSetBoundingBox.equals(updateTileGridBoundingBox)) {
                updateTileGridBoundingBox = updateTileGridBoundingBox.union(previousTileMatrixSetBoundingBox);
                adjustBounds(updateTileGridBoundingBox, minNewOrUpdateZoom);
                updateTileGridBoundingBox = tileGridBoundingBox;
                if (!sameProjection) {
                    updateTileGridBoundingBox = updateTileGridBoundingBox.transform(transformProjectionToTileMatrixSet);
                }
                tileMatrixSet.setBoundingBox(updateTileGridBoundingBox);
                TileMatrixSetDao tileMatrixSetDao = geoPackage.getTileMatrixSetDao();
                tileMatrixSetDao.update(tileMatrixSet);
            }

            TileMatrixDao tileMatrixDao = geoPackage.getTileMatrixDao();

            // Adjust the tile matrix metadata and tile rows at each existing
            // zoom level
            for (long zoom = tileDao.getMinZoom(); zoom <= tileDao.getMaxZoom(); zoom++) {
                TileMatrix tileMatrix = tileDao.getTileMatrix(zoom);
                if (tileMatrix != null) {

                    // Determine the new width and height at this level
                    long adjustment = (long) Math.pow(2, zoom
                            - minNewOrUpdateZoom);
                    long zoomMatrixWidth = matrixWidth * adjustment;
                    long zoomMatrixHeight = matrixHeight * adjustment;

                    // Get the zoom level tile rows, starting with highest rows
                    // and columns so when updating we avoid constraint
                    // violations
                    TileCursor tileCursor = tileDao
                            .queryForTileDescending(zoom);
                    try {
                        // Update each tile row at this zoom level
                        while (tileCursor.moveToNext()) {
                            TileRow tileRow = tileCursor.getRow();

                            // Get the bounding box of the existing tile
                            BoundingBox tileBoundingBox = TileBoundingBoxUtils
                                    .getBoundingBox(
                                            previousTileMatrixSetBoundingBox,
                                            tileMatrix, tileRow.getTileColumn(), tileRow.getTileRow());

                            // Get the mid lat and lon to find the new tile row
                            // and column
                            double midLatitude = tileBoundingBox
                                    .getMinLatitude()
                                    + ((tileBoundingBox.getMaxLatitude() - tileBoundingBox
                                    .getMinLatitude()) / 2.0);
                            double midLongitude = tileBoundingBox
                                    .getMinLongitude()
                                    + ((tileBoundingBox.getMaxLongitude() - tileBoundingBox
                                    .getMinLongitude()) / 2.0);

                            // Get the new tile row and column with regards to
                            // the new bounding box
                            long newTileRow = TileBoundingBoxUtils.getTileRow(
                                    tileGridBoundingBox,
                                    zoomMatrixHeight, midLatitude);
                            long newTileColumn = TileBoundingBoxUtils
                                    .getTileColumn(
                                            tileGridBoundingBox,
                                            zoomMatrixWidth, midLongitude);

                            // Update the tile row
                            if (tileRow.getTileRow() != newTileRow || tileRow.getTileColumn() != newTileColumn) {
                                tileRow.setTileRow(newTileRow);
                                tileRow.setTileColumn(newTileColumn);
                                tileDao.update(tileRow);
                            }
                        }
                    } finally {
                        tileCursor.close();
                    }

                    // Calculate the pixel size
                    double pixelXSize = (tileGridBoundingBox
                            .getMaxLongitude() - tileGridBoundingBox
                            .getMinLongitude())
                            / zoomMatrixWidth / tileMatrix.getTileWidth();
                    double pixelYSize = (tileGridBoundingBox
                            .getMaxLatitude() - tileGridBoundingBox
                            .getMinLatitude())
                            / zoomMatrixHeight / tileMatrix.getTileHeight();

                    // Update the tile matrix
                    tileMatrix.setMatrixWidth(zoomMatrixWidth);
                    tileMatrix.setMatrixHeight(zoomMatrixHeight);
                    tileMatrix.setPixelXSize(pixelXSize);
                    tileMatrix.setPixelYSize(pixelYSize);

                    tileMatrixDao.update(tileMatrix);
                }
            }

            // Adjust the width and height to the min zoom level of the
            // request
            if (minNewOrUpdateZoom < minZoom) {
                long adjustment = (long) Math.pow(2, minZoom
                        - minNewOrUpdateZoom);
                matrixWidth *= adjustment;
                matrixHeight *= adjustment;
            }

        }
    }

    /**
     * Close the GeoPackage
     */
    public void close() {
        if (geoPackage != null) {
            geoPackage.close();
        }
    }

    /**
     * Generate the tiles for the zoom level
     *
     * @param tileMatrixDao
     * @param tileDao
     * @param contents
     * @param zoomLevel
     * @param tileGrid
     * @param localTileGrid
     * @param matrixWidth
     * @param matrixHeight
     * @param update
     * @return tile count
     * @throws java.sql.SQLException
     * @throws java.io.IOException
     */
    private int generateTiles(TileMatrixDao tileMatrixDao, TileDao tileDao,
                              Contents contents, int zoomLevel, TileGrid tileGrid,
                              TileGrid localTileGrid, long matrixWidth, long matrixHeight,
                              boolean update) throws SQLException, IOException {

        int count = 0;

        Integer tileWidth = null;
        Integer tileHeight = null;

        // Download and create the tile and each coordinate
        for (long x = tileGrid.getMinX(); x <= tileGrid.getMaxX(); x++) {

            // Check if the progress has been cancelled
            if (progress != null && !progress.isActive()) {
                break;
            }

            for (long y = tileGrid.getMinY(); y <= tileGrid.getMaxY(); y++) {

                // Check if the progress has been cancelled
                if (progress != null && !progress.isActive()) {
                    break;
                }

                try {

                    // Create the tile
                    byte[] tileBytes = createTile(zoomLevel, x, y);

                    if (tileBytes != null) {

                        Bitmap bitmap = null;

                        // Compress the image
                        if (compressFormat != null) {
                            bitmap = BitmapConverter.toBitmap(tileBytes, options);
                            if (bitmap != null) {
                                tileBytes = BitmapConverter.toBytes(bitmap,
                                        compressFormat, compressQuality);
                            }
                        }

                        // Create a new tile row
                        TileRow newRow = tileDao.newRow();
                        newRow.setZoomLevel(zoomLevel);

                        long tileColumn = x;
                        long tileRow = y;

                        // Update the column and row to the local tile grid location
                        if (localTileGrid != null) {
                            tileColumn = (x - tileGrid.getMinX())
                                    + localTileGrid.getMinX();
                            tileRow = (y - tileGrid.getMinY())
                                    + localTileGrid.getMinY();
                        }

                        // If an update, delete an existing row
                        if (update) {
                            tileDao.deleteTile(tileColumn, tileRow, zoomLevel);
                        }

                        newRow.setTileColumn(tileColumn);
                        newRow.setTileRow(tileRow);
                        newRow.setTileData(tileBytes);
                        tileDao.create(newRow);

                        count++;

                        // Determine the tile width and height
                        if (tileWidth == null) {
                            if (bitmap == null) {
                                bitmap = BitmapConverter.toBitmap(tileBytes,
                                        options);
                            }
                            if (bitmap != null) {
                                tileWidth = bitmap.getWidth();
                                tileHeight = bitmap.getHeight();
                            }
                        }
                    }
                } catch (Exception e) {
                    // Skip this tile, don't increase count
                }

                // Update the progress count, even on failures
                if (progress != null) {
                    progress.addProgress(1);
                }

            }

        }

        // If none of the tiles were translated into a bitmap with dimensions,
        // delete them
        if (tileWidth == null || tileHeight == null) {
            count = 0;

            StringBuilder where = new StringBuilder();

            where.append(tileDao.buildWhere(TileTable.COLUMN_ZOOM_LEVEL,
                    zoomLevel));

            where.append(" AND ");
            where.append(tileDao.buildWhere(TileTable.COLUMN_TILE_COLUMN,
                    tileGrid.getMinX(), ">="));

            where.append(" AND ");
            where.append(tileDao.buildWhere(TileTable.COLUMN_TILE_COLUMN,
                    tileGrid.getMaxX(), "<="));

            where.append(" AND ");
            where.append(tileDao.buildWhere(TileTable.COLUMN_TILE_ROW,
                    tileGrid.getMinY(), ">="));

            where.append(" AND ");
            where.append(tileDao.buildWhere(TileTable.COLUMN_TILE_ROW,
                    tileGrid.getMaxY(), "<="));

            String[] whereArgs = tileDao.buildWhereArgs(new Object[]{
                    zoomLevel, tileGrid.getMinX(), tileGrid.getMaxX(),
                    tileGrid.getMinY(), tileGrid.getMaxY()});

            tileDao.delete(where.toString(), whereArgs);

        } else {

            // Check if the tile matrix already exists
            boolean create = true;
            if (update) {
                create = !tileMatrixDao.idExists(new TileMatrixKey(tableName,
                        zoomLevel));
            }

            // Create the tile matrix
            if (create) {

                // Calculate meters per pixel
                double pixelXSize = (tileGridBoundingBox.getMaxLongitude() - tileGridBoundingBox
                        .getMinLongitude()) / matrixWidth / tileWidth;
                double pixelYSize = (tileGridBoundingBox.getMaxLatitude() - tileGridBoundingBox
                        .getMinLatitude()) / matrixHeight / tileHeight;

                // Create the tile matrix for this zoom level
                TileMatrix tileMatrix = new TileMatrix();
                tileMatrix.setContents(contents);
                tileMatrix.setZoomLevel(zoomLevel);
                tileMatrix.setMatrixWidth(matrixWidth);
                tileMatrix.setMatrixHeight(matrixHeight);
                tileMatrix.setTileWidth(tileWidth);
                tileMatrix.setTileHeight(tileHeight);
                tileMatrix.setPixelXSize(pixelXSize);
                tileMatrix.setPixelYSize(pixelYSize);
                tileMatrixDao.create(tileMatrix);
            }
        }

        return count;
    }

    /**
     * Called after set up and right before tile generation starts for the first
     * zoom level
     */
    protected abstract void preTileGeneration();

    /**
     * Create the tile
     *
     * @param z zoom level
     * @param x x coordinate
     * @param y y coordinate
     * @return tile bytes
     */
    protected abstract byte[] createTile(int z, long x, long y);

}


package mil.nga.geopackage.tiles.user;

import androidx.collection.LongSparseArray;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.core.contents.Contents;
import mil.nga.geopackage.core.srs.SpatialReferenceSystem;
import mil.nga.geopackage.db.GeoPackageConnection;
import mil.nga.geopackage.tiles.TileBoundingBoxUtils;
import mil.nga.geopackage.tiles.TileGrid;
import mil.nga.geopackage.tiles.matrix.TileMatrix;
import mil.nga.geopackage.tiles.matrixset.TileMatrixSet;
import mil.nga.geopackage.user.UserDao;
import mil.nga.sf.proj.Projection;
import mil.nga.sf.proj.ProjectionConstants;

/**
 * Tile DAO for reading tile user tables
 *
 * @author osbornb
 */
public class TileDao extends UserDao<TileColumn, TileTable, TileRow, TileCursor> {

    /**
     * Tile connection
     */
    private final TileConnection tileDb;

    /**
     * Tile Matrix Set
     */
    private final TileMatrixSet tileMatrixSet;

    /**
     * Tile Matrices
     */
    private final List<TileMatrix> tileMatrices;

    /**
     * Mapping between zoom levels and the tile matrix
     */
    private final LongSparseArray<TileMatrix> zoomLevelToTileMatrix = new LongSparseArray<>();

    /**
     * Min zoom
     */
    private final long minZoom;

    /**
     * Max zoom
     */
    private final long maxZoom;

    /**
     * Array of widths of the tiles at each zoom level in default units
     */
    private final double[] widths;

    /**
     * Array of heights of the tiles at each zoom level in default units
     */
    private final double[] heights;

    /**
     * Constructor
     *
     * @param database      database name
     * @param db            GeoPackage connection
     * @param tileDb        tile connection
     * @param tileMatrixSet tile matrix set
     * @param tileMatrices  tile matrices
     * @param table         tile table
     */
    public TileDao(String database, GeoPackageConnection db, TileConnection tileDb, TileMatrixSet tileMatrixSet,
                   List<TileMatrix> tileMatrices, TileTable table) {
        super(database, db, tileDb, table);

        this.tileDb = tileDb;
        this.tileMatrixSet = tileMatrixSet;
        this.tileMatrices = tileMatrices;
        this.widths = new double[tileMatrices.size()];
        this.heights = new double[tileMatrices.size()];

        projection = tileMatrixSet.getProjection();

        // Set the min and max zoom levels
        if (!tileMatrices.isEmpty()) {
            minZoom = tileMatrices.get(0).getZoomLevel();
            maxZoom = tileMatrices.get(tileMatrices.size() - 1).getZoomLevel();
        } else {
            minZoom = 0;
            maxZoom = 0;
        }

        // Populate the zoom level to tile matrix and the sorted tile widths and
        // heights
        for (int i = 0; i < tileMatrices.size(); i++) {
            TileMatrix tileMatrix = tileMatrices.get(i);
            zoomLevelToTileMatrix.put(tileMatrix.getZoomLevel(), tileMatrix);
            widths[tileMatrices.size() - i - 1] = tileMatrix.getPixelXSize()
                    * tileMatrix.getTileWidth();
            heights[tileMatrices.size() - i - 1] = tileMatrix.getPixelYSize()
                    * tileMatrix.getTileHeight();
        }

        if (tileMatrixSet.getContents() == null) {
            throw new GeoPackageException(TileMatrixSet.class.getSimpleName()
                    + " " + tileMatrixSet.getId() + " has null "
                    + Contents.class.getSimpleName());
        }
        if (tileMatrixSet.getSrs() == null) {
            throw new GeoPackageException(TileMatrixSet.class.getSimpleName()
                    + " " + tileMatrixSet.getId() + " has null "
                    + SpatialReferenceSystem.class.getSimpleName());
        }

    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BoundingBox getBoundingBox() {
        return tileMatrixSet.getBoundingBox();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BoundingBox getBoundingBox(Projection projection) {
        return tileMatrixSet.getBoundingBox(projection);
    }

    /**
     * Get the bounding box of tiles
     *
     * @param zoomLevel zoom level
     * @return bounding box of zoom level, or null if no tiles
     * @since 1.1.1
     */
    public BoundingBox getBoundingBox(long zoomLevel) {
        BoundingBox boundingBox = null;
        TileMatrix tileMatrix = getTileMatrix(zoomLevel);
        if (tileMatrix != null) {
            TileGrid tileGrid = queryForTileGrid(zoomLevel);
            if (tileGrid != null) {
                BoundingBox matrixSetBoundingBox = getBoundingBox();
                boundingBox = TileBoundingBoxUtils.getBoundingBox(
                        matrixSetBoundingBox, tileMatrix, tileGrid);
            }

        }
        return boundingBox;
    }

    /**
     * Get the tile grid of the zoom level
     *
     * @param zoomLevel zoom level
     * @return tile grid at zoom level, null if not tile matrix at zoom level
     * @since 1.1.1
     */
    public TileGrid getTileGrid(long zoomLevel) {
        TileGrid tileGrid = null;
        TileMatrix tileMatrix = getTileMatrix(zoomLevel);
        if (tileMatrix != null) {
            tileGrid = new TileGrid(0, 0, tileMatrix.getMatrixWidth() - 1,
                    tileMatrix.getMatrixHeight() - 1);
        }
        return tileGrid;
    }

    /**
     * Adjust the tile matrix lengths if needed. Check if the tile matrix width
     * and height need to expand to account for pixel * number of pixels fitting
     * into the tile matrix lengths
     */
    public void adjustTileMatrixLengths() {
        TileDaoUtils.adjustTileMatrixLengths(tileMatrixSet, tileMatrices);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TileRow newRow() {
        return new TileRow(getTable());
    }

    /**
     * Get the Tile connection
     *
     * @return tile connection
     */
    public TileConnection getTileDb() {
        return tileDb;
    }

    /**
     * Get the tile matrix set
     *
     * @return tile matrix set
     */
    public TileMatrixSet getTileMatrixSet() {
        return tileMatrixSet;
    }

    /**
     * Get the tile matrices
     *
     * @return tile matrices
     */
    public List<TileMatrix> getTileMatrices() {
        return tileMatrices;
    }

    /**
     * Get the tile matrix at the zoom level
     *
     * @param zoomLevel zoom level
     * @return tile matrix
     */
    public TileMatrix getTileMatrix(long zoomLevel) {
        return zoomLevelToTileMatrix.get(zoomLevel);
    }

    /**
     * Get the min zoom
     *
     * @return min zoom
     */
    public long getMinZoom() {
        return minZoom;
    }

    /**
     * Get the max zoom
     *
     * @return max zoom
     */
    public long getMaxZoom() {
        return maxZoom;
    }

    /**
     * Query for a Tile
     *
     * @param column    column
     * @param row       row
     * @param zoomLevel zoom level
     * @return tile row
     */
    public TileRow queryForTile(long column, long row, long zoomLevel) {

        Map<String, Object> fieldValues = new HashMap<String, Object>();
        fieldValues.put(TileTable.COLUMN_TILE_COLUMN, column);
        fieldValues.put(TileTable.COLUMN_TILE_ROW, row);
        fieldValues.put(TileTable.COLUMN_ZOOM_LEVEL, zoomLevel);

        TileCursor cursor = queryForFieldValues(fieldValues);
        TileRow tileRow = null;
        try {
            if (cursor.moveToNext()) {
                tileRow = cursor.getRow();
            }
        } finally {
            cursor.close();
        }

        return tileRow;
    }

    /**
     * Query for Tiles at a zoom level
     *
     * @param zoomLevel zoom level
     * @return tile cursor, should be closed
     */
    public TileCursor queryForTile(long zoomLevel) {
        return queryForEq(TileTable.COLUMN_ZOOM_LEVEL, zoomLevel);
    }

    /**
     * Query for Tiles at a zoom level in descending row and column order
     *
     * @param zoomLevel zoom level
     * @return tile cursor, should be closed
     */
    public TileCursor queryForTileDescending(long zoomLevel) {
        return queryForEq(TileTable.COLUMN_ZOOM_LEVEL, zoomLevel, null, null,
                TileTable.COLUMN_TILE_ROW + " DESC, "
                        + TileTable.COLUMN_TILE_COLUMN + " DESC");
    }

    /**
     * Query for Tiles at a zoom level and column
     *
     * @param column    column
     * @param zoomLevel zoom level
     * @return tile cursor
     */
    public TileCursor queryForTilesInColumn(long column, long zoomLevel) {

        Map<String, Object> fieldValues = new HashMap<String, Object>();
        fieldValues.put(TileTable.COLUMN_TILE_COLUMN, column);
        fieldValues.put(TileTable.COLUMN_ZOOM_LEVEL, zoomLevel);

        return queryForFieldValues(fieldValues);
    }

    /**
     * Query for Tiles at a zoom level and row
     *
     * @param row       row
     * @param zoomLevel zoom level
     * @return tile cursor
     */
    public TileCursor queryForTilesInRow(long row, long zoomLevel) {

        Map<String, Object> fieldValues = new HashMap<String, Object>();
        fieldValues.put(TileTable.COLUMN_TILE_ROW, row);
        fieldValues.put(TileTable.COLUMN_ZOOM_LEVEL, zoomLevel);

        return queryForFieldValues(fieldValues);
    }

    /**
     * Get the zoom level for the provided width and height in the default units
     *
     * @param length in default units
     * @return zoom level
     */
    public Long getZoomLevel(double length) {

        Long zoomLevel = TileDaoUtils.getZoomLevel(widths, heights, tileMatrices, length);
        return zoomLevel;
    }

    /**
     * Get the zoom level for the provided width and height in the default units
     *
     * @param width  in default units
     * @param height in default units
     * @return zoom level
     * @since 1.3.1
     */
    public Long getZoomLevel(double width, double height) {

        Long zoomLevel = TileDaoUtils.getZoomLevel(widths, heights,
                tileMatrices, width, height);
        return zoomLevel;
    }

    /**
     * Get the closest zoom level for the provided width and height in the
     * default units
     *
     * @param length in default units
     * @return zoom level
     * @since 1.3.1
     */
    public Long getClosestZoomLevel(double length) {

        Long zoomLevel = TileDaoUtils.getClosestZoomLevel(widths, heights,
                tileMatrices, length);
        return zoomLevel;
    }

    /**
     * Get the closest zoom level for the provided width and height in the
     * default units
     *
     * @param width  in default units
     * @param height in default units
     * @return zoom level
     * @since 1.3.1
     */
    public Long getClosestZoomLevel(double width, double height) {

        Long zoomLevel = TileDaoUtils.getClosestZoomLevel(widths, heights,
                tileMatrices, width, height);
        return zoomLevel;
    }

    /**
     * Get the approximate zoom level for the provided length in the default
     * units. Tiles may or may not exist for the returned zoom level. The
     * approximate zoom level is determined using a factor of 2 from the zoom
     * levels with tiles.
     *
     * @param length length in default units
     * @return approximate zoom level
     * @since 2.0.2
     */
    public Long getApproximateZoomLevel(double length) {

        Long zoomLevel = TileDaoUtils.getApproximateZoomLevel(widths, heights,
                tileMatrices, length);
        return zoomLevel;
    }

    /**
     * Get the approximate zoom level for the provided width and height in the
     * default units. Tiles may or may not exist for the returned zoom level.
     * The approximate zoom level is determined using a factor of 2 from the
     * zoom levels with tiles.
     *
     * @param width  width in default units
     * @param height height in default units
     * @return approximate zoom level
     * @since 2.0.2
     */
    public Long getApproximateZoomLevel(double width, double height) {

        Long zoomLevel = TileDaoUtils.getApproximateZoomLevel(widths, heights,
                tileMatrices, width, height);
        return zoomLevel;
    }

    /**
     * Query by tile grid and zoom level
     *
     * @param tileGrid  tile grid
     * @param zoomLevel zoom level
     * @return cursor from query or null if the zoom level tile ranges do not
     * overlap the bounding box
     */
    public TileCursor queryByTileGrid(TileGrid tileGrid, long zoomLevel) {
        return queryByTileGrid(tileGrid, zoomLevel, null);
    }

    /**
     * Query by tile grid and zoom level
     *
     * @param tileGrid  tile grid
     * @param zoomLevel zoom level
     * @param orderBy   order by
     * @return cursor from query or null if the zoom level tile ranges do not
     * overlap the bounding box
     * @since 1.3.1
     */
    public TileCursor queryByTileGrid(TileGrid tileGrid, long zoomLevel,
                                      String orderBy) {

        TileCursor tileCursor = null;

        if (tileGrid != null) {

            StringBuilder where = new StringBuilder();

            where.append(buildWhere(TileTable.COLUMN_ZOOM_LEVEL, zoomLevel));

            where.append(" AND ");
            where.append(buildWhere(TileTable.COLUMN_TILE_COLUMN,
                    tileGrid.getMinX(), ">="));

            where.append(" AND ");
            where.append(buildWhere(TileTable.COLUMN_TILE_COLUMN,
                    tileGrid.getMaxX(), "<="));

            where.append(" AND ");
            where.append(buildWhere(TileTable.COLUMN_TILE_ROW,
                    tileGrid.getMinY(), ">="));

            where.append(" AND ");
            where.append(buildWhere(TileTable.COLUMN_TILE_ROW,
                    tileGrid.getMaxY(), "<="));

            String[] whereArgs = buildWhereArgs(new Object[]{zoomLevel,
                    tileGrid.getMinX(), tileGrid.getMaxX(), tileGrid.getMinY(),
                    tileGrid.getMaxY()});

            tileCursor = query(where.toString(), whereArgs, null, null, orderBy);
        }

        return tileCursor;
    }

    /**
     * Query for the bounding
     *
     * @param zoomLevel zoom level
     * @return tile grid of tiles at the zoom level
     * @since 1.1.1
     */
    public TileGrid queryForTileGrid(long zoomLevel) {

        String where = buildWhere(TileTable.COLUMN_ZOOM_LEVEL, zoomLevel);
        String[] whereArgs = buildWhereArgs(new Object[]{zoomLevel});

        Integer minX = min(TileTable.COLUMN_TILE_COLUMN, where, whereArgs);
        Integer maxX = max(TileTable.COLUMN_TILE_COLUMN, where, whereArgs);
        Integer minY = min(TileTable.COLUMN_TILE_ROW, where, whereArgs);
        Integer maxY = max(TileTable.COLUMN_TILE_ROW, where, whereArgs);

        TileGrid tileGrid = null;
        if (minX != null && maxX != null && minY != null && maxY != null) {
            tileGrid = new TileGrid(minX, minY, maxX, maxY);
        }

        return tileGrid;
    }

    /**
     * Delete a Tile
     *
     * @param column    column
     * @param row       row
     * @param zoomLevel zoom level
     * @return number deleted, should be 0 or 1
     */
    public int deleteTile(long column, long row, long zoomLevel) {

        StringBuilder where = new StringBuilder();

        where.append(buildWhere(TileTable.COLUMN_ZOOM_LEVEL, zoomLevel));

        where.append(" AND ");
        where.append(buildWhere(TileTable.COLUMN_TILE_COLUMN, column));

        where.append(" AND ");
        where.append(buildWhere(TileTable.COLUMN_TILE_ROW, row));

        String[] whereArgs = buildWhereArgs(new Object[]{zoomLevel, column,
                row});

        int deleted = delete(where.toString(), whereArgs);

        return deleted;
    }

    /**
     * Count of Tiles at a zoom level
     *
     * @param zoomLevel zoom level
     * @return count
     */
    public int count(long zoomLevel) {
        String where = buildWhere(TileTable.COLUMN_ZOOM_LEVEL, zoomLevel);
        String[] whereArgs = buildWhereArgs(zoomLevel);
        return count(where, whereArgs);
    }

    /**
     * Get the max length in default units that contains tiles
     *
     * @return max distance length with tiles
     * @since 1.3.0
     */
    public double getMaxLength() {
        return TileDaoUtils.getMaxLength(widths, heights);
    }

    /**
     * Get the min length in default units that contains tiles
     *
     * @return min distance length with tiles
     * @since 1.3.0
     */
    public double getMinLength() {
        return TileDaoUtils.getMinLength(widths, heights);
    }

    /**
     * Determine if the tiles are in the Google tile coordinate format
     *
     * @return google tiles flag
     */
    public boolean isGoogleTiles() {

        // Convert the bounding box to wgs84
        BoundingBox boundingBox = tileMatrixSet.getBoundingBox();
        BoundingBox wgs84BoundingBox = boundingBox.transform(
                projection.getTransformation(
                        ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM));

        boolean googleTiles = false;

        // Verify the bounds are the entire world
        if (wgs84BoundingBox.getMinLatitude() <= ProjectionConstants.WEB_MERCATOR_MIN_LAT_RANGE
                && wgs84BoundingBox.getMaxLatitude() >= ProjectionConstants.WEB_MERCATOR_MAX_LAT_RANGE
                && wgs84BoundingBox.getMinLongitude() <= -ProjectionConstants.WGS84_HALF_WORLD_LON_WIDTH
                && wgs84BoundingBox.getMaxLongitude() >= ProjectionConstants.WGS84_HALF_WORLD_LON_WIDTH) {

            googleTiles = true;

            // Verify each tile matrix is the correct width and height
            for (TileMatrix tileMatrix : tileMatrices) {
                long zoomLevel = tileMatrix.getZoomLevel();
                long tilesPerSide = TileBoundingBoxUtils
                        .tilesPerSide((int) zoomLevel);
                if (tileMatrix.getMatrixWidth() != tilesPerSide
                        || tileMatrix.getMatrixHeight() != tilesPerSide) {
                    googleTiles = false;
                    break;
                }
            }
        }

        return googleTiles;
    }

}


package mil.nga.geopackage.extension.coverage;

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.GeoPackage;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.tiles.user.TileDao;
import mil.nga.geopackage.tiles.user.TileRow;
import mil.nga.sf.proj.Projection;
import mil.nga.tiff.FileDirectory;
import mil.nga.tiff.Rasters;
import mil.nga.tiff.TIFFImage;
import mil.nga.tiff.TiffReader;
import mil.nga.tiff.util.TiffConstants;

/**
 * Tiled Gridded Coverage Data, TIFF Encoding, Extension
 *
 * @author osbornb
 * @since 2.0.1
 */
public class CoverageDataTiff extends CoverageData<CoverageDataTiffImage> {

    /**
     * Single sample coverage data
     */
    public static final int SAMPLES_PER_PIXEL = 1;

    /**
     * Bits per value for floating point coverage data
     */
    public static final int BITS_PER_SAMPLE = 32;

    /**
     * Constructor
     *
     * @param geoPackage        GeoPackage
     * @param tileDao           tile dao
     * @param width             coverage data response width
     * @param height            coverage data response height
     * @param requestProjection request projection
     */
    public CoverageDataTiff(GeoPackage geoPackage, TileDao tileDao,
                            Integer width, Integer height, Projection requestProjection) {
        super(geoPackage, tileDao, width,
                height, requestProjection);
    }

    /**
     * Constructor, use the coverage data tables pixel tile size as the request size
     * width and height
     *
     * @param geoPackage GeoPackage
     * @param tileDao    tile dao
     */
    public CoverageDataTiff(GeoPackage geoPackage, TileDao tileDao) {
        this(geoPackage, tileDao, null, null, tileDao.getProjection());
    }

    /**
     * Constructor, use the coverage data tables pixel tile size as the request size
     * width and height, request as the specified projection
     *
     * @param geoPackage        GeoPackage
     * @param tileDao           tile dao
     * @param requestProjection request projection
     */
    public CoverageDataTiff(GeoPackage geoPackage, TileDao tileDao,
                            Projection requestProjection) {
        this(geoPackage, tileDao, null, null, requestProjection);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public CoverageDataTiffImage createImage(TileRow tileRow) {
        return new CoverageDataTiffImage(tileRow);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public double getValue(GriddedTile griddedTile, TileRow tileRow,
                           int x, int y) {
        byte[] imageBytes = tileRow.getTileData();
        double value = getValue(griddedTile, imageBytes, x, y);
        return value;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Double getValue(GriddedTile griddedTile,
                           CoverageDataTiffImage image, int x, int y) {
        Double value = null;
        if (image.getDirectory() != null) {
            float pixelValue = image.getPixel(x, y);
            value = getValue(griddedTile, pixelValue);
        } else {
            value = getValue(griddedTile, image.getImageBytes(), x, y);
        }
        return value;
    }

    /**
     * Get the pixel value as a float from the image and the coordinate
     *
     * @param imageBytes image bytes
     * @param x          x coordinate
     * @param y          y coordinate
     * @return float pixel value
     */
    public float getPixelValue(byte[] imageBytes, int x, int y) {

        TIFFImage tiffImage = TiffReader.readTiff(imageBytes);
        FileDirectory directory = tiffImage.getFileDirectory();
        validateImageType(directory);
        Rasters rasters = directory.readRasters();
        float pixelValue = rasters.getFirstPixelSample(x, y).floatValue();

        return pixelValue;
    }

    /**
     * Get the pixel values of the image as floats
     *
     * @param imageBytes image bytes
     * @return float pixel values
     */
    public float[] getPixelValues(byte[] imageBytes) {
        TIFFImage tiffImage = TiffReader.readTiff(imageBytes);
        FileDirectory directory = tiffImage.getFileDirectory();
        validateImageType(directory);
        Rasters rasters = directory.readRasters();
        float[] pixels = new float[rasters.getWidth() * rasters.getHeight()];
        for (int y = 0; y < rasters.getHeight(); y++) {
            for (int x = 0; x < rasters.getWidth(); x++) {
                int index = rasters.getSampleIndex(x, y);
                pixels[index] = rasters.getPixelSample(0, x, y).floatValue();
            }
        }
        return pixels;
    }

    /**
     * Validate that the image type
     *
     * @param directory file directory
     */
    public static void validateImageType(FileDirectory directory) {
        if (directory == null) {
            throw new GeoPackageException("The image is null");
        }

        int samplesPerPixel = directory.getSamplesPerPixel();
        Integer bitsPerSample = null;
        if (directory.getBitsPerSample() != null && !directory.getBitsPerSample().isEmpty()) {
            bitsPerSample = directory.getBitsPerSample().get(0);
        }
        Integer sampleFormat = null;
        if (directory.getSampleFormat() != null && !directory.getSampleFormat().isEmpty()) {
            sampleFormat = directory.getSampleFormat().get(0);
        }

        if (samplesPerPixel != SAMPLES_PER_PIXEL
                || bitsPerSample == null || bitsPerSample != BITS_PER_SAMPLE
                || sampleFormat == null || sampleFormat != TiffConstants.SAMPLE_FORMAT_FLOAT) {
            throw new GeoPackageException(
                    "The coverage data tile is expected to be a single sample 32 bit float. Samples Per Pixel: "
                            + samplesPerPixel
                            + ", Bits Per Sample: " + bitsPerSample
                            + ", Sample Format: " + sampleFormat);
        }

    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Double getValue(GriddedTile griddedTile,
                           byte[] imageBytes, int x, int y) {
        float pixelValue = getPixelValue(imageBytes, x, y);
        Double value = getValue(griddedTile, pixelValue);
        return value;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Double[] getValues(GriddedTile griddedTile,
                              byte[] imageBytes) {
        float[] pixelValues = getPixelValues(imageBytes);
        Double[] values = getValues(griddedTile, pixelValues);
        return values;
    }

    /**
     * Draw a coverage data image tile from the flat array of float pixel values of
     * length tileWidth * tileHeight where each pixel is at: (y * tileWidth) + x
     *
     * @param pixelValues float pixel values of length tileWidth * tileHeight
     * @param tileWidth   tile width
     * @param tileHeight  tile height
     * @return coverage data image tile
     */
    public CoverageDataTiffImage drawTile(float[] pixelValues, int tileWidth,
                                          int tileHeight) {

        CoverageDataTiffImage image = createImage(tileWidth, tileHeight);
        for (int y = 0; y < tileHeight; y++) {
            for (int x = 0; x < tileWidth; x++) {
                float pixelValue = pixelValues[(y * tileWidth)
                        + x];
                setPixelValue(image, x, y, pixelValue);
            }
        }
        image.writeTiff();

        return image;
    }

    /**
     * Draw a coverage data image tile and format as TIFF bytes from the flat array
     * of float pixel values of length tileWidth * tileHeight where each pixel
     * is at: (y * tileWidth) + x
     *
     * @param pixelValues float pixel values of length tileWidth * tileHeight
     * @param tileWidth   tile width
     * @param tileHeight  tile height
     * @return coverage data image tile bytes
     */
    public byte[] drawTileData(float[] pixelValues, int tileWidth,
                               int tileHeight) {
        CoverageDataTiffImage image = drawTile(pixelValues, tileWidth, tileHeight);
        byte[] bytes = image.getImageBytes();
        return bytes;
    }

    /**
     * Draw a coverage data image tile from the double array of float pixel values
     * formatted as float[row][width]
     *
     * @param pixelValues float pixel values as [row][width]
     * @return coverage data image tile
     */
    public CoverageDataTiffImage drawTile(float[][] pixelValues) {

        int tileWidth = pixelValues[0].length;
        int tileHeight = pixelValues.length;

        CoverageDataTiffImage image = createImage(tileWidth, tileHeight);
        for (int y = 0; y < tileHeight; y++) {
            for (int x = 0; x < tileWidth; x++) {
                float pixelValue = pixelValues[y][x];
                setPixelValue(image, x, y, pixelValue);
            }
        }
        image.writeTiff();

        return image;
    }

    /**
     * Draw a coverage data image tile and format as TIFF bytes from the double
     * array of float pixel values formatted as float[row][width]
     *
     * @param pixelValues float pixel values as [row][width]
     * @return coverage data image tile bytes
     */
    public byte[] drawTileData(float[][] pixelValues) {
        CoverageDataTiffImage image = drawTile(pixelValues);
        byte[] bytes = image.getImageBytes();
        return bytes;
    }

    /**
     * Draw a coverage data image tile from the flat array of coverage data values of length
     * tileWidth * tileHeight where each coverage data value is at: (y * tileWidth) + x
     *
     * @param griddedTile gridded tile
     * @param values      coverage data values of length tileWidth * tileHeight
     * @param tileWidth   tile width
     * @param tileHeight  tile height
     * @return coverage data image tile
     */
    public CoverageDataTiffImage drawTile(GriddedTile griddedTile, Double[] values,
                                          int tileWidth, int tileHeight) {

        CoverageDataTiffImage image = createImage(tileWidth, tileHeight);
        for (int x = 0; x < tileWidth; x++) {
            for (int y = 0; y < tileHeight; y++) {
                Double value = values[(y * tileWidth) + x];
                float pixelValue = getPixelValue(griddedTile, value);
                setPixelValue(image, x, y, pixelValue);
            }
        }
        image.writeTiff();

        return image;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public byte[] drawTileData(GriddedTile griddedTile, Double[] values,
                               int tileWidth, int tileHeight) {
        CoverageDataTiffImage image = drawTile(griddedTile, values, tileWidth,
                tileHeight);
        byte[] bytes = image.getImageBytes();
        return bytes;
    }

    /**
     * Draw a coverage data image tile from the double array of coverage data values
     * formatted as Double[row][width]
     *
     * @param griddedTile gridded tile
     * @param values      coverage data values as [row][width]
     * @return coverage data image tile
     */
    public CoverageDataTiffImage drawTile(GriddedTile griddedTile, Double[][] values) {

        int tileWidth = values[0].length;
        int tileHeight = values.length;

        CoverageDataTiffImage image = createImage(tileWidth, tileHeight);
        for (int x = 0; x < tileWidth; x++) {
            for (int y = 0; y < tileHeight; y++) {
                Double value = values[y][x];
                short pixelValue = getPixelValue(griddedTile, value);
                setPixelValue(image, x, y, pixelValue);
            }
        }
        image.writeTiff();

        return image;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public byte[] drawTileData(GriddedTile griddedTile, Double[][] values) {
        CoverageDataTiffImage image = drawTile(griddedTile, values);
        byte[] bytes = image.getImageBytes();
        return bytes;
    }

    /**
     * Create a new image
     *
     * @param tileWidth  tile width
     * @param tileHeight tile height
     * @return image
     */
    public CoverageDataTiffImage createImage(int tileWidth, int tileHeight) {

        Rasters rasters = new Rasters(tileWidth, tileHeight, 1,
                BITS_PER_SAMPLE, TiffConstants.SAMPLE_FORMAT_FLOAT);

        int rowsPerStrip = rasters.calculateRowsPerStrip(TiffConstants.PLANAR_CONFIGURATION_CHUNKY);

        FileDirectory fileDirectory = new FileDirectory();
        fileDirectory.setImageWidth(tileWidth);
        fileDirectory.setImageHeight(tileHeight);
        fileDirectory.setBitsPerSample(BITS_PER_SAMPLE);
        fileDirectory.setCompression(TiffConstants.COMPRESSION_NO);
        fileDirectory.setPhotometricInterpretation(TiffConstants.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO);
        fileDirectory.setSamplesPerPixel(SAMPLES_PER_PIXEL);
        fileDirectory.setRowsPerStrip(rowsPerStrip);
        fileDirectory.setPlanarConfiguration(TiffConstants.PLANAR_CONFIGURATION_CHUNKY);
        fileDirectory.setSampleFormat(TiffConstants.SAMPLE_FORMAT_FLOAT);
        fileDirectory.setWriteRasters(rasters);

        CoverageDataTiffImage image = new CoverageDataTiffImage(fileDirectory);

        return image;
    }

    /**
     * Set the pixel value into the image
     *
     * @param image      image
     * @param x          x coordinate
     * @param y          y coordinate
     * @param pixelValue pixel value
     */
    public void setPixelValue(CoverageDataTiffImage image, int x, int y,
                              float pixelValue) {
        image.getRasters().setFirstPixelSample(x, y, pixelValue);
    }

    /**
     * Create the coverage data tile table with metadata and extension
     *
     * @param geoPackage               GeoPackage
     * @param tableName                table name
     * @param contentsBoundingBox      contents bounding box
     * @param contentsSrsId            contents srs id
     * @param tileMatrixSetBoundingBox tile matrix set bounding box
     * @param tileMatrixSetSrsId       tile matrix set srs id
     * @return coverage data
     */
    public static CoverageDataTiff createTileTableWithMetadata(
            GeoPackage geoPackage, String tableName,
            BoundingBox contentsBoundingBox, long contentsSrsId,
            BoundingBox tileMatrixSetBoundingBox, long tileMatrixSetSrsId) {

        CoverageDataTiff coverageData = (CoverageDataTiff) CoverageData
                .createTileTableWithMetadata(geoPackage, tableName,
                        contentsBoundingBox, contentsSrsId,
                        tileMatrixSetBoundingBox, tileMatrixSetSrsId,
                        GriddedCoverageDataType.FLOAT);
        return coverageData;
    }

}


package mil.nga.geopackage.tiles.overlay;

import java.util.HashMap;
import java.util.Map;

import mil.nga.geopackage.geom.GeoPackageGeometryData;
import mil.nga.sf.Geometry;
import mil.nga.sf.GeometryType;
import mil.nga.sf.geojson.FeatureConverter;

/**
 * Represents the values of a single feature row
 *
 * @author osbornb
 * @since 1.2.7
 */
public class FeatureRowData {

    /**
     * Column names and values
     */
    private Map<String, Object> values;

    /**
     * Geometry column
     */
    private String geometryColumn;

    /**
     * Constructor
     *
     * @param values         column names and values
     * @param geometryColumn geometry column name
     */
    public FeatureRowData(Map<String, Object> values, String geometryColumn) {
        this.values = values;
        this.geometryColumn = geometryColumn;
    }

    /**
     * Get the values
     *
     * @return column names and values
     */
    public Map<String, Object> getValues() {
        return values;
    }

    /**
     * Get the geometry column name
     *
     * @return geometry column
     */
    public String getGeometryColumn() {
        return geometryColumn;
    }

    /**
     * Get the geometry data
     *
     * @return geometry data
     */
    public GeoPackageGeometryData getGeometryData() {
        return (GeoPackageGeometryData) values.get(geometryColumn);
    }

    /**
     * Get the geometry
     *
     * @return geometry
     */
    public Geometry getGeometry() {
        return getGeometryData().getGeometry();
    }

    /**
     * Build a JSON compatible object
     *
     * @return JSON compatible object
     */
    public Object jsonCompatible() {
        return jsonCompatible(true, true);
    }

    /**
     * Build a JSON compatible object
     *
     * @param includePoints true to include point geometries, but no other geometry types
     * @return JSON compatible object
     */
    public Object jsonCompatibleWithPoints(boolean includePoints) {
        return jsonCompatible(includePoints, false);
    }

    /**
     * Build a JSON compatible object
     *
     * @param includeGeometries true to include all geometries, false for no geometries
     * @return JSON compatible object
     */
    public Object jsonCompatibleWithGeometries(boolean includeGeometries) {
        return jsonCompatible(includeGeometries, includeGeometries);
    }

    /**
     * Build a JSON compatible object
     *
     * @param includePoints     true to include point geometries, ignored if includeGeometries is true
     * @param includeGeometries true to include all geometry types
     * @return JSON compatible object
     */
    public Object jsonCompatible(boolean includePoints, boolean includeGeometries) {

        Map<String, Object> jsonValues = new HashMap<>();

        for (String key : values.keySet()) {
            Object jsonValue = null;
            Object value = values.get(key);
            if (key.equals(geometryColumn)) {
                GeoPackageGeometryData geometryData = (GeoPackageGeometryData) value;
                if (geometryData.getGeometry() != null) {
                    if (includeGeometries || (includePoints && geometryData.getGeometry().getGeometryType() == GeometryType.POINT)) {
                        jsonValue = FeatureConverter.toMap(geometryData.getGeometry());
                    }
                } else {
                    jsonValue = value;
                }
                if (jsonValue != null) {
                    jsonValues.put(key, jsonValue);
                }
            }
        }

        return jsonValues;
    }

}


package mil.nga.geopackage;

/**
 * GeoPackage Cache
 *
 * @author osbornb
 */
public class GeoPackageCache extends GeoPackageCoreCache<GeoPackage> {

    /**
     * GeoPackage manager
     */
    private GeoPackageManager manager;

    /**
     * Constructor
     *
     * @param manager GeoPackage manager
     */
    public GeoPackageCache(GeoPackageManager manager) {
        this.manager = manager;
    }

    /**
     * Get the cached GeoPackage or open and cache the GeoPackage
     *
     * @param name GeoPackage name
     * @return GeoPackage
     */
    public GeoPackage getOrOpen(String name) {
        return getOrOpen(name, true);
    }

    /**
     * Get the cached GeoPackage or open and cache the GeoPackage
     *
     * @param name     GeoPackage name
     * @param writable writable true to open as writable, false as read only
     * @return GeoPackage
     * @since 3.1.0
     */
    public GeoPackage getOrOpen(String name, boolean writable) {
        return getOrOpen(name, writable, true);
    }

    /**
     * Get the cached GeoPackage or open the GeoPackage file without caching it
     *
     * @param name GeoPackage name
     * @return GeoPackage
     * @since 3.1.0
     */
    public GeoPackage getOrNoCacheOpen(String name) {
        return getOrNoCacheOpen(name, true);
    }

    /**
     * Get the cached GeoPackage or open the GeoPackage file without caching it
     *
     * @param name     GeoPackage name
     * @param writable writable true to open as writable, false as read only
     * @return GeoPackage
     * @since 3.1.0
     */
    public GeoPackage getOrNoCacheOpen(String name, boolean writable) {
        return getOrOpen(name, writable, false);
    }

    /**
     * Get the cached GeoPackage or open and cache the GeoPackage
     *
     * @param name     GeoPackage name
     * @param writable writable true to open as writable, false as read only
     * @param cache    true to cache opened GeoPackages
     * @return GeoPackage
     */
    private GeoPackage getOrOpen(String name, boolean writable, boolean cache) {
        GeoPackage geoPackage = get(name);
        if (geoPackage == null) {
            geoPackage = manager.open(name, writable);
            if (cache) {
                add(geoPackage);
            }
        }
        return geoPackage;
    }

}


package mil.nga.geopackage.user;

import android.database.Cursor;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import mil.nga.geopackage.db.CursorResult;
import mil.nga.geopackage.db.GeoPackageDataType;
import mil.nga.geopackage.db.ResultUtils;

/**
 * Abstract User Cursor
 *
 * @param <TColumn> column type
 * @param <TTable>  table type
 * @param <TRow>    row type
 * @author osbornb
 */
public abstract class UserCursor<TColumn extends UserColumn, TTable extends UserTable<TColumn>, TRow extends UserRow<TColumn, TTable>>
        extends CursorResult implements UserCoreResult<TColumn, TTable, TRow> {

    /**
     * Table
     */
    private TTable table;

    /**
     * Invalid cursor positions due to large sized blobs
     */
    private Set<Integer> invalidPositions = new LinkedHashSet<>();

    /**
     * User query arguments
     */
    private UserQuery query;

    /**
     * Data access object used for requeries
     */
    private UserDao<TColumn, TTable, TRow, ? extends UserCursor<TColumn, TTable, TRow>> dao;

    /**
     * User invalid cursor for iterating through the invalid requeried rows
     */
    private UserInvalidCursor<TColumn, TTable, TRow, ? extends UserCursor<TColumn, TTable, TRow>, ? extends UserDao<TColumn, TTable, TRow, ? extends UserCursor<TColumn, TTable, TRow>>> invalidCursor;

    /**
     * Constructor
     *
     * @param table  table
     * @param cursor cursor
     */
    protected UserCursor(TTable table, Cursor cursor) {
        super(cursor);
        this.table = table;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getValue(TColumn column) {
        return getValue(column.getIndex(), column.getDataType());
    }

    /**
     * Set the table
     *
     * @param table table
     * @since 3.2.0
     */
    public void setTable(TTable table) {
        this.table = table;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TTable getTable() {
        return table;
    }

    /**
     * Get the invalid positions found when retrieving rows
     *
     * @return invalid positions
     * @since 2.0.0
     */
    public List<Integer> getInvalidPositions() {
        return new ArrayList<>(invalidPositions);
    }

    /**
     * Determine if invalid positions were found when retrieving rows
     *
     * @return true if invalid positions
     * @since 2.0.0
     */
    public boolean hasInvalidPositions() {
        return !invalidPositions.isEmpty();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean moveToNext() {
        boolean hasNext = super.moveToNext();
        if (!hasNext) {
            hasNext = moveToNextInvalid();
        }
        return hasNext;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TRow getRow() {

        TRow row;
        if (invalidCursor == null) {
            row = getCurrentRow();
        } else {
            row = invalidCursor.getRow();
        }

        return row;
    }

    /**
     * Get the current row
     *
     * @return row
     */
    private TRow getCurrentRow() {

        TRow row = null;

        if (table != null) {

            int[] columnTypes = new int[table.columnCount()];
            Object[] values = new Object[table.columnCount()];

            boolean valid = true;

            for (TColumn column : table.getColumns()) {

                int index = column.getIndex();
                int columnType = getType(index);

                if (column.isPrimaryKey() && columnType == FIELD_TYPE_NULL) {
                    valid = false;
                }

                columnTypes[index] = columnType;
                values[index] = getValue(column);

            }

            row = getRow(columnTypes, values);

            if (!valid) {
                invalidPositions.add(getPosition());
                row.setValid(false);
            }
        }

        return row;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getValue(int index, GeoPackageDataType dataType) {
        return ResultUtils.getValue(invalidCursor == null ? this : invalidCursor, index, dataType);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void close() {
        super.close();
        if (invalidCursor != null) {
            invalidCursor.close();
        }
    }

    /**
     * Set the user query
     *
     * @param query user query
     * @since 2.0.0
     */
    public void setQuery(UserQuery query) {
        this.query = query;
    }

    /**
     * Get the user query
     *
     * @return user query
     * @since 2.0.0
     */
    public UserQuery getQuery() {
        return query;
    }

    /**
     * Enable requery attempt of invalid rows after iterating through original query rows.
     * Only supported for {@link #moveToNext()} and {@link #getRow()} usage.
     *
     * @param dao data access object used to perform requery
     * @since 2.0.0
     */
    protected void enableInvalidRequery(UserDao<TColumn, TTable, TRow, ? extends UserCursor<TColumn, TTable, TRow>> dao) {
        this.dao = dao;
    }

    /**
     * Move to the next position of invalid rows to requery.  Perform the requery the first time.
     *
     * @return true if invalid rows are left
     */
    private boolean moveToNextInvalid() {

        boolean hasNext = false;

        // If requery has not been performed, a requery dao has been set, and there are invalid positions
        if (invalidCursor == null && dao != null && hasInvalidPositions()) {

            // Close the original cursor when performing an invalid cursor query
            super.close();

            // Set the blob columns to return as null
            List<TColumn> blobColumns = dao.getTable().columnsOfType(GeoPackageDataType.BLOB);
            String[] columnsAs = dao.buildColumnsAsNull(blobColumns);
            query.set(UserQueryParamType.COLUMNS_AS, columnsAs);

            // Query without blob columns and create an invalid cursor
            UserCursor<TColumn, TTable, TRow> requeryCursor = dao.query(query);
            invalidCursor = createInvalidCursor(dao, requeryCursor, getInvalidPositions(), blobColumns);
        }

        if (invalidCursor != null) {
            hasNext = invalidCursor.moveToNext();
        }

        return hasNext;
    }

    /**
     * Create an invalid cursor
     *
     * @param dao              data access object
     * @param cursor           user cursor
     * @param invalidPositions invalid positions
     * @param blobColumns      blob columns
     * @return invalid cursor
     */
    protected abstract UserInvalidCursor<TColumn, TTable, TRow, ? extends UserCursor<TColumn, TTable, TRow>, ? extends UserDao<TColumn, TTable, TRow, ? extends UserCursor<TColumn, TTable, TRow>>> createInvalidCursor(
            UserDao dao, UserCursor cursor, List<Integer> invalidPositions, List<TColumn> blobColumns);

}


package mil.nga.geopackage.features.user;

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.core.contents.Contents;
import mil.nga.geopackage.core.srs.SpatialReferenceSystem;
import mil.nga.geopackage.db.GeoPackageConnection;
import mil.nga.geopackage.features.columns.GeometryColumns;
import mil.nga.geopackage.user.UserDao;
import mil.nga.sf.GeometryType;
import mil.nga.sf.proj.Projection;

/**
 * Feature DAO for reading feature user data tables
 *
 * @author osbornb
 */
public class FeatureDao extends
        UserDao<FeatureColumn, FeatureTable, FeatureRow, FeatureCursor> {

    /**
     * Feature connection
     */
    private final FeatureConnection featureDb;

    /**
     * Geometry Columns
     */
    private final GeometryColumns geometryColumns;

    /**
     * Constructor
     *
     * @param database        database name
     * @param db              GeoPackage connection
     * @param featureDb       feature connection
     * @param geometryColumns geometry columns
     * @param table           feature table
     */
    public FeatureDao(String database, GeoPackageConnection db, FeatureConnection featureDb, GeometryColumns geometryColumns,
                      FeatureTable table) {
        super(database, db, featureDb, table);

        this.featureDb = featureDb;
        this.geometryColumns = geometryColumns;
        if (geometryColumns.getContents() == null) {
            throw new GeoPackageException(GeometryColumns.class.getSimpleName()
                    + " " + geometryColumns.getId() + " has null "
                    + Contents.class.getSimpleName());
        }
        if (geometryColumns.getSrs() == null) {
            throw new GeoPackageException(GeometryColumns.class.getSimpleName()
                    + " " + geometryColumns.getId() + " has null "
                    + SpatialReferenceSystem.class.getSimpleName());
        }

        projection = geometryColumns.getProjection();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BoundingBox getBoundingBox() {
        return getBoundingBox(projection);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BoundingBox getBoundingBox(Projection projection) {
        Contents contents = geometryColumns.getContents();
        BoundingBox boundingBox = contents.getBoundingBox(projection);
        return boundingBox;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FeatureRow newRow() {
        return new FeatureRow(getTable());
    }

    /**
     * Get the Feature connection
     *
     * @return feature connection
     */
    public FeatureConnection getFeatureDb() {
        return featureDb;
    }

    /**
     * Get the Geometry Columns
     *
     * @return geometry columns
     */
    public GeometryColumns getGeometryColumns() {
        return geometryColumns;
    }

    /**
     * The the Geometry Column name
     *
     * @return geometry column name
     */
    public String getGeometryColumnName() {
        return geometryColumns.getColumnName();
    }

    /**
     * Get the Geometry Type
     *
     * @return geometry type
     */
    public GeometryType getGeometryType() {
        return geometryColumns.getGeometryType();
    }

}