package mil.nga.geopackage.user;

import android.database.Cursor;

import mil.nga.geopackage.db.GeoPackageConnection;
import mil.nga.geopackage.db.GeoPackageDatabase;

/**
 * GeoPackage Connection used to define common functionality within different
 * connection types
 *
 * @param <TColumn> column type
 * @param <TTable>  table type
 * @param <TRow>    row type
 * @param <TResult> result type
 * @author osbornb
 */
public abstract class UserConnection<TColumn extends UserColumn, TTable extends UserTable<TColumn>, TRow extends UserRow<TColumn, TTable>, TResult extends UserCursor<TColumn, TTable, TRow>>
        extends UserCoreConnection<TColumn, TTable, TRow, TResult> {

    /**
     * Database connection
     */
    protected final GeoPackageDatabase database;

    /**
     * Table
     */
    protected TTable table;

    /**
     * Constructor
     *
     * @param database GeoPackage connection
     */
    protected UserConnection(GeoPackageConnection database) {
        this.database = database.getDb();
    }

    /**
     * Get the table
     *
     * @return table
     * @since 3.2.0
     */
    public TTable getTable() {
        return table;
    }

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

    /**
     * {@inheritDoc}
     */
    @Override
    public TResult rawQuery(String sql, String[] selectionArgs) {
        UserQuery query = new UserQuery(sql, selectionArgs);
        TResult result = query(query);
        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TResult query(String table, String[] columns, String selection,
                         String[] selectionArgs, String groupBy, String having,
                         String orderBy) {
        UserQuery query = new UserQuery(table, columns, selection, selectionArgs, groupBy, having, orderBy);
        TResult result = query(query);
        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TResult query(String table, String[] columns, String[] columnsAs, String selection,
                         String[] selectionArgs, String groupBy, String having,
                         String orderBy) {
        UserQuery query = new UserQuery(table, columns, columnsAs, selection, selectionArgs, groupBy, having, orderBy);
        TResult result = query(query);
        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TResult query(String table, String[] columns, String selection,
                         String[] selectionArgs, String groupBy, String having,
                         String orderBy, String limit) {
        UserQuery query = new UserQuery(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit);
        TResult result = query(query);
        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TResult query(String table, String[] columns, String[] columnsAs, String selection,
                         String[] selectionArgs, String groupBy, String having,
                         String orderBy, String limit) {
        UserQuery query = new UserQuery(table, columns, columnsAs, selection, selectionArgs, groupBy, having, orderBy, limit);
        TResult result = query(query);
        return result;
    }

    /**
     * Query using the query from a previous query result
     *
     * @param previousResult previous result
     * @return result
     * @since 2.0.0
     */
    public TResult query(TResult previousResult) {
        UserQuery query = previousResult.getQuery();
        TResult result = query(query);
        return result;
    }

    /**
     * Query using the user query arguments
     *
     * @param query user query
     * @return result
     * @since 2.0.0
     */
    public TResult query(UserQuery query) {
        Cursor cursor = null;

        String[] selectionArgs = query.getSelectionArgs();

        String sql = query.getSql();
        if (sql != null) {
            cursor = database.rawQuery(sql, selectionArgs);
        } else {

            String table = query.getTable();
            String[] columns = query.getColumns();
            String selection = query.getSelection();
            String groupBy = query.getGroupBy();
            String having = query.getHaving();
            String orderBy = query.getOrderBy();

            String[] columnsAs = query.getColumnsAs();
            String limit = query.getLimit();

            if (columnsAs != null && limit != null) {
                cursor = database.query(table, columns, columnsAs, selection, selectionArgs, groupBy, having, orderBy, limit);
            } else if (columnsAs != null) {
                cursor = database.query(table, columns, columnsAs, selection, selectionArgs, groupBy, having, orderBy);
            } else if (limit != null) {
                cursor = database.query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit);
            } else {
                cursor = database.query(table, columns, selection, selectionArgs, groupBy, having, orderBy);
            }

        }

        TResult result = handleCursor(cursor, query);

        return result;
    }

    /**
     * Convert the cursor to the result type cursor
     *
     * @param cursor cursor
     * @param query  user query
     * @return result cursor
     */
    private TResult handleCursor(Cursor cursor, UserQuery query) {
        TResult result = convertCursor(cursor);
        result.setQuery(query);
        if (table != null) {
            result.setTable(table);
        }
        return result;
    }

    /**
     * Convert the cursor to the result type cursor
     *
     * @param cursor cursor
     * @return result cursor
     * @since 2.0.0
     */
    protected TResult convertCursor(Cursor cursor) {
        return (TResult) cursor;
    }

}


package mil.nga.geopackage.extension.index;

import android.util.Log;

import com.j256.ormlite.misc.TransactionManager;
import com.j256.ormlite.support.ConnectionSource;

import java.sql.SQLException;
import java.util.concurrent.Callable;

import mil.nga.geopackage.GeoPackage;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.features.user.FeatureCursor;
import mil.nga.geopackage.features.user.FeatureDao;
import mil.nga.geopackage.features.user.FeatureRow;
import mil.nga.geopackage.features.user.FeatureRowSync;
import mil.nga.sf.proj.Projection;

/**
 * Feature Table Index NGA Extension implementation. This extension is used to
 * index Geometries within a feature table by their minimum bounding box for
 * bounding box queries. This extension is required to provide an index
 * implementation when a SQLite version is used before SpatialLite support
 * (Android).
 *
 * @author osbornb
 * @since 1.1.0
 */
public class FeatureTableIndex extends FeatureTableCoreIndex {

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

    /**
     * Feature Row Sync for simultaneous same row queries
     */
    private final FeatureRowSync featureRowSync = new FeatureRowSync();

    /**
     * Constructor
     *
     * @param geoPackage GeoPackage
     * @param featureDao feature dao
     */
    public FeatureTableIndex(GeoPackage geoPackage, FeatureDao featureDao) {
        super(geoPackage, featureDao.getTableName(), featureDao
                .getGeometryColumnName());
        this.featureDao = featureDao;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Projection getProjection() {
        return featureDao.getProjection();
    }

    /**
     * Close the table index
     */
    public void close() {
        // Don't close anything, leave the GeoPackage connection open
    }

    /**
     * Index the feature row. This method assumes that indexing has been
     * completed and maintained as the last indexed time is updated.
     *
     * @param row feature row
     * @return true if indexed
     */
    public boolean index(FeatureRow row) {
        TableIndex tableIndex = getTableIndex();
        if (tableIndex == null) {
            throw new GeoPackageException(
                    "GeoPackage table is not indexed. GeoPackage: "
                            + getGeoPackage().getName() + ", Table: "
                            + getTableName());
        }
        boolean indexed = index(tableIndex, row.getId(), row.getGeometry());

        // Update the last indexed time
        updateLastIndexed();

        return indexed;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected int indexTable(final TableIndex tableIndex) {

        int count = 0;

        long offset = 0;
        int chunkCount = 0;

        while (chunkCount >= 0) {

            final long chunkOffset = offset;

            try {
                // Iterate through each row and index as a single transaction
                ConnectionSource connectionSource = getGeoPackage().getDatabase()
                        .getConnectionSource();
                chunkCount = TransactionManager.callInTransaction(connectionSource,
                        new Callable<Integer>() {
                            public Integer call() throws Exception {

                                FeatureCursor cursor = featureDao.queryForChunk(chunkLimit, chunkOffset);
                                int count = indexRows(tableIndex, cursor);

                                return count;
                            }
                        });
                if (chunkCount > 0) {
                    count += chunkCount;
                }
            } catch (SQLException e) {
                throw new GeoPackageException("Failed to Index Table. GeoPackage: "
                        + getGeoPackage().getName() + ", Table: " + getTableName(),
                        e);
            }

            offset += chunkLimit;
        }

        // Update the last indexed time
        if (progress == null || progress.isActive()) {
            updateLastIndexed();
        }

        return count;
    }

    /**
     * Index the feature rows in the cursor
     *
     * @param tableIndex table index
     * @param cursor     feature cursor
     * @return count, -1 if no results or canceled
     */
    private int indexRows(TableIndex tableIndex, FeatureCursor cursor) {

        int count = -1;

        try {
            while ((progress == null || progress.isActive())
                    && cursor.moveToNext()) {
                if (count < 0) {
                    count++;
                }
                try {
                    FeatureRow row = cursor.getRow();
                    if (row.isValid()) {
                        boolean indexed = index(tableIndex,
                                row.getId(), row.getGeometry());
                        if (indexed) {
                            count++;
                        }
                        if (progress != null) {
                            progress.addProgress(1);
                        }
                    }
                } catch (Exception e) {
                    Log.e(FeatureTableIndex.class.getSimpleName(), "Failed to index feature. Table: "
                            + tableIndex.getTableName() + ", Position: " + cursor.getPosition(), e);
                }
            }
        } finally {
            cursor.close();
        }

        return count;
    }

    /**
     * Delete the index for the feature row
     *
     * @param row feature row
     * @return deleted rows, should be 0 or 1
     */
    public int deleteIndex(FeatureRow row) {
        return deleteIndex(row.getId());
    }

    /**
     * Get the feature row for the Geometry Index
     *
     * @param geometryIndex geometry index
     * @return feature row
     */
    public FeatureRow getFeatureRow(GeometryIndex geometryIndex) {

        long geomId = geometryIndex.getGeomId();

        // Get the row or lock for reading
        FeatureRow row = featureRowSync.getRowOrLock(geomId);
        if (row == null) {
            // Query for the row and set in the sync
            try {
                row = featureDao.queryForIdRow(geomId);
            } finally {
                featureRowSync.setRow(geomId, row);
            }
        }

        return row;
    }

}


package mil.nga.geopackage.extension;

import java.util.Iterator;

import mil.nga.geopackage.features.index.FeatureIndexResults;
import mil.nga.geopackage.features.user.FeatureRow;
import mil.nga.geopackage.user.custom.UserCustomCursor;

/**
 * Iterable Feature Index Results to iterate on feature rows retrieved from
 * RTree results
 *
 * @author osbornb
 * @since 3.1.0
 */
public class FeatureIndexRTreeResults implements FeatureIndexResults {

    /**
     * RTree Index Table DAO
     */
    private final RTreeIndexTableDao dao;

    /**
     * Result Cursor
     */
    private final UserCustomCursor cursor;

    /**
     * Constructor
     *
     * @param dao    RTree Index Table DAO
     * @param cursor result cursor
     */
    public FeatureIndexRTreeResults(RTreeIndexTableDao dao,
                                    UserCustomCursor cursor) {
        this.dao = dao;
        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 dao.getFeatureRow(cursor);
            }
        };
    }

    /**
     * {@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 dao.getRow(cursor).getId();
                    }

                };
            }
        };
    }

}


package mil.nga.geopackage.factory;

import android.database.Cursor;
import android.database.sqlite.SQLiteCursor;
import android.database.sqlite.SQLiteCursorDriver;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteQuery;

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

import mil.nga.geopackage.db.CoreSQLUtils;

/**
 * GeoPackage Cursor Factory. Used when connecting to a GeoPackage database.
 * Registers cursor wrappers for GeoPackage data tables to wrap the cursors
 * returned from queries.
 *
 * @author osbornb
 */
class GeoPackageCursorFactory implements CursorFactory {

    /**
     * Mapping between table names and their cursor wrapper
     */
    private final Map<String, GeoPackageCursorWrapper> tableCursors = Collections
            .synchronizedMap(new HashMap<String, GeoPackageCursorWrapper>());

    /**
     * Constructor
     */
    GeoPackageCursorFactory() {
    }

    /**
     * Register a cursor wrapper for the provided table name. Database queries
     * will wrap the returned cursor
     *
     * @param tableName
     * @param cursorWrapper
     */
    public void registerTable(String tableName,
                              GeoPackageCursorWrapper cursorWrapper) {

        // Remove an existing cursor wrapper
        tableCursors.remove(tableName);

        // Add the wrapper
        tableCursors.put(tableName, cursorWrapper);
        String quotedTableName = CoreSQLUtils.quoteWrap(tableName);
        tableCursors.put(quotedTableName, cursorWrapper);

        // The Android android.database.sqlite.SQLiteDatabase findEditTable method
        // finds the new cursor edit table name based upon the first space or comma.
        // Fix (hopefully temporary) to wrap with the expected cursor type
        int spacePosition = tableName.indexOf(' ');
        if (spacePosition > 0) {
            tableCursors.put(tableName.substring(0, spacePosition), cursorWrapper);
            tableCursors.put(quotedTableName.substring(0, quotedTableName.indexOf(' ')), cursorWrapper);
        }

    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver driver,
                            String editTable, SQLiteQuery query) {

        // Create a standard cursor
        Cursor cursor = new SQLiteCursor(driver, editTable, query);

        // Check if there is an edit table
        if (editTable != null) {
            // Check if the table has a cursor wrapper
            GeoPackageCursorWrapper cursorWrapper = tableCursors.get(editTable);
            if (cursorWrapper != null) {
                cursor = cursorWrapper.wrapCursor(cursor);
            }
        }

        return cursor;
    }

}


package mil.nga.geopackage.tiles.retriever;

import mil.nga.geopackage.tiles.matrix.TileMatrix;
import mil.nga.geopackage.tiles.user.TileDao;
import mil.nga.geopackage.tiles.user.TileRow;

/**
 * GeoPackage Tile Retriever, assumes the Google Maps API zoom level
 * and grid
 *
 * @author osbornb
 * @since 1.2.0
 */
public class GoogleAPIGeoPackageTileRetriever implements TileRetriever {

    /**
     * Tile data access object
     */
    private final TileDao tileDao;

    /**
     * Constructor
     *
     * @param tileDao tile dao
     */
    public GoogleAPIGeoPackageTileRetriever(TileDao tileDao) {
        this.tileDao = tileDao;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasTile(int x, int y, int zoom) {
        return retrieveTileRow(x, y, zoom) != null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public GeoPackageTile getTile(int x, int y, int zoom) {

        GeoPackageTile tile = null;

        TileRow tileRow = retrieveTileRow(x, y, zoom);
        if (tileRow != null) {
            TileMatrix tileMatrix = tileDao.getTileMatrix(zoom);
            int tileWidth = (int) tileMatrix.getTileWidth();
            int tileHeight = (int) tileMatrix.getTileHeight();
            tile = new GeoPackageTile(tileWidth, tileHeight, tileRow.getTileData());
        }

        return tile;
    }

    /**
     * Retrieve the tile row
     *
     * @param x
     * @param y
     * @param zoom
     * @return
     */
    private TileRow retrieveTileRow(int x, int y, int zoom) {
        return tileDao.queryForTile(x, y, zoom);
    }

}


package mil.nga.geopackage.extension.coverage;

import android.graphics.Rect;
import android.graphics.RectF;

import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.GeoPackage;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.tiles.TileBoundingBoxAndroidUtils;
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.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.ProjectionTransform;

/**
 * Tiled Gridded Coverage Data, abstract Common Encoding, Extension
 *
 * @author osbornb
 * @since 2.0.1
 */
public abstract class CoverageData<TImage extends CoverageDataImage> extends CoverageDataCore<TImage> {

    /**
     * Get a Tiled Gridded Coverage Data
     *
     * @param geoPackage        GeoPackage
     * @param tileDao           tile dao
     * @param width             coverage data response width
     * @param height            coverage data response height
     * @param requestProjection request projection
     * @return coverage data
     */
    public static CoverageData<?> getCoverageData(GeoPackage geoPackage,
                                                  TileDao tileDao, Integer width, Integer height,
                                                  Projection requestProjection) {

        TileMatrixSet tileMatrixSet = tileDao.getTileMatrixSet();
        GriddedCoverageDao griddedCoverageDao = geoPackage
                .getGriddedCoverageDao();

        GriddedCoverage griddedCoverage = null;
        try {
            if (griddedCoverageDao.isTableExists()) {
                griddedCoverage = griddedCoverageDao.query(tileMatrixSet);
            }
        } catch (SQLException e) {
            throw new GeoPackageException(
                    "Failed to get Gridded Coverage for table name: "
                            + tileMatrixSet.getTableName(), e);
        }

        CoverageData<?> coverageData = null;

        GriddedCoverageDataType dataType = griddedCoverage.getDataType();
        switch (dataType) {
            case INTEGER:
                coverageData = new CoverageDataPng(geoPackage, tileDao, width,
                        height, requestProjection);
                break;
            case FLOAT:
                coverageData = new CoverageDataTiff(geoPackage, tileDao, width,
                        height, requestProjection);
                break;
            default:
                throw new GeoPackageException(
                        "Unsupported Gridded Coverage Data Type: " + dataType);
        }

        return coverageData;
    }

    /**
     * Get a Tiled Gridded Coverage Data, use the coverage data pixel tile size
     * as the request size width and height
     *
     * @param geoPackage GeoPackage
     * @param tileDao    tile dao
     * @return coverage data
     */
    public static CoverageData<?> getCoverageData(GeoPackage geoPackage,
                                                  TileDao tileDao) {
        return getCoverageData(geoPackage, tileDao, null, null,
                tileDao.getProjection());
    }

    /**
     * Get a Tiled Gridded Coverage Data, use the coverage data 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
     * @return coverage data
     */
    public static CoverageData<?> getCoverageData(GeoPackage geoPackage,
                                                  TileDao tileDao, Projection requestProjection) {
        return getCoverageData(geoPackage, tileDao, null, null,
                requestProjection);
    }

    /**
     * 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
     * @param dataType                 gridded coverage data type
     * @return coverage data
     */
    public static CoverageData<?> createTileTableWithMetadata(
            GeoPackage geoPackage, String tableName,
            BoundingBox contentsBoundingBox, long contentsSrsId,
            BoundingBox tileMatrixSetBoundingBox, long tileMatrixSetSrsId,
            GriddedCoverageDataType dataType) {

        TileMatrixSet tileMatrixSet = CoverageDataCore
                .createTileTableWithMetadata(geoPackage, tableName,
                        contentsBoundingBox, contentsSrsId,
                        tileMatrixSetBoundingBox, tileMatrixSetSrsId);
        TileDao tileDao = geoPackage.getTileDao(tileMatrixSet);

        CoverageData<?> coverageData = null;
        switch (dataType) {
            case INTEGER:
                coverageData = new CoverageDataPng(geoPackage, tileDao);
                break;
            case FLOAT:
                coverageData = new CoverageDataTiff(geoPackage, tileDao);
                break;
            default:
                throw new GeoPackageException(
                        "Unsupported Gridded Coverage Data Type: " + dataType);
        }

        coverageData.getOrCreate();

        return coverageData;
    }

    /**
     * Tile DAO
     */
    protected final TileDao tileDao;

    /**
     * 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 CoverageData(GeoPackage geoPackage, TileDao tileDao, Integer width,
                        Integer height, Projection requestProjection) {
        super(geoPackage, tileDao
                .getTileMatrixSet(), width, height, requestProjection);
        this.tileDao = tileDao;
    }

    /**
     * Create a coverage data image
     *
     * @param tileRow tile row
     * @return image
     */
    public abstract TImage createImage(TileRow tileRow);

    /**
     * Get the coverage data value of the pixel in the tile row image
     *
     * @param griddedTile gridded tile
     * @param tileRow     tile row
     * @param x           x coordinate
     * @param y           y coordinate
     * @return coverage data value
     */
    public abstract double getValue(GriddedTile griddedTile,
                                    TileRow tileRow, int x, int y);

    /**
     * Get the coverage data value
     *
     * @param griddedTile gridded tile
     * @param imageBytes  image bytes
     * @param x           x coordinate
     * @param y           y coordinate
     * @return coverage data value
     */
    public abstract Double getValue(GriddedTile griddedTile,
                                    byte[] imageBytes, int x, int y);

    /**
     * Get the coverage data values
     *
     * @param griddedTile gridded tile
     * @param imageBytes  image bytes
     * @return coverage data values
     */
    public abstract Double[] getValues(GriddedTile griddedTile,
                                       byte[] imageBytes);

    /**
     * Draw a coverage data image tile and format as TIFF bytes 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 bytes
     */
    public abstract byte[] drawTileData(GriddedTile griddedTile, Double[] values,
                                        int tileWidth, int tileHeight);

    /**
     * Draw a coverage data image tile and format as TIFF bytes 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 bytes
     */
    public abstract byte[] drawTileData(GriddedTile griddedTile, Double[][] values);

    /**
     * Get the tile dao
     *
     * @return tile dao
     */
    public TileDao getTileDao() {
        return tileDao;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public CoverageDataResults getValues(CoverageDataRequest request,
                                         Integer width, Integer height) {

        CoverageDataResults coverageDataResults = null;

        // Transform to the projection of the coverage data tiles
        ProjectionTransform transformRequestToCoverage = null;
        BoundingBox requestProjectedBoundingBox = request.getBoundingBox();
        if (!sameProjection) {
            transformRequestToCoverage = requestProjection
                    .getTransformation(coverageProjection);
            requestProjectedBoundingBox = requestProjectedBoundingBox
                    .transform(transformRequestToCoverage);
        }
        request.setProjectedBoundingBox(requestProjectedBoundingBox);

        // Determine how many overlapping pixels to store based upon the
        // algorithm
        int overlappingPixels;
        switch (algorithm) {
            case BICUBIC:
                overlappingPixels = 3;
                break;
            default:
                overlappingPixels = 1;
        }

        // Find the tile matrix and results
        CoverageDataTileMatrixResults results = getResults(request,
                requestProjectedBoundingBox, overlappingPixels);

        if (results != null) {

            TileMatrix tileMatrix = results.getTileMatrix();
            TileCursor tileResults = results.getTileResults();

            try {

                // Determine the requested coverage data dimensions, or use the
                // dimensions of a single tile matrix coverage data tile
                int requestedCoverageDataWidth = width != null ? width
                        : (int) tileMatrix.getTileWidth();
                int requestedCoverageDataHeight = height != null ? height
                        : (int) tileMatrix.getTileHeight();

                // Determine the size of the non projected coverage data results
                int tileWidth = requestedCoverageDataWidth;
                int tileHeight = requestedCoverageDataHeight;
                if (!sameProjection) {
                    int projectedWidth = (int) Math
                            .round((requestProjectedBoundingBox
                                    .getMaxLongitude() - requestProjectedBoundingBox
                                    .getMinLongitude())
                                    / tileMatrix.getPixelXSize());
                    if (projectedWidth > 0) {
                        tileWidth = projectedWidth;
                    }
                    int projectedHeight = (int) Math
                            .round((requestProjectedBoundingBox
                                    .getMaxLatitude() - requestProjectedBoundingBox
                                    .getMinLatitude())
                                    / tileMatrix.getPixelYSize());
                    if (projectedHeight > 0) {
                        tileHeight = projectedHeight;
                    }
                }

                // Retrieve the coverage data from the results
                Double[][] values = getValues(tileMatrix, tileResults,
                        request, tileWidth, tileHeight, overlappingPixels);

                // Project the coverage data if needed
                if (values != null && !sameProjection && !request.isPoint()) {
                    values = reprojectCoverageData(values,
                            requestedCoverageDataWidth,
                            requestedCoverageDataHeight,
                            request.getBoundingBox(),
                            transformRequestToCoverage,
                            requestProjectedBoundingBox);
                }

                // Create the results
                if (values != null) {
                    coverageDataResults = new CoverageDataResults(values,
                            tileMatrix);
                }
            } finally {
                tileResults.close();
            }
        }

        return coverageDataResults;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public CoverageDataResults getValuesUnbounded(CoverageDataRequest request) {

        CoverageDataResults coverageDataResults = null;

        // Transform to the projection of the coverage data tiles
        ProjectionTransform transformRequestToCoverage = null;
        BoundingBox requestProjectedBoundingBox = request.getBoundingBox();
        if (!sameProjection) {
            transformRequestToCoverage = requestProjection
                    .getTransformation(coverageProjection);
            requestProjectedBoundingBox = requestProjectedBoundingBox
                    .transform(transformRequestToCoverage);
        }
        request.setProjectedBoundingBox(requestProjectedBoundingBox);

        // Find the tile matrix and results
        CoverageDataTileMatrixResults results = getResults(request,
                requestProjectedBoundingBox);

        if (results != null) {

            TileMatrix tileMatrix = results.getTileMatrix();
            TileCursor tileResults = results.getTileResults();

            try {

                // Retrieve the coverage data values from the results
                Double[][] values = getValuesUnbounded(tileMatrix,
                        tileResults, request);

                // Project the coverage data if needed
                if (values != null && !sameProjection && !request.isPoint()) {
                    values = reprojectCoverageData(values,
                            values[0].length, values.length,
                            request.getBoundingBox(),
                            transformRequestToCoverage,
                            requestProjectedBoundingBox);
                }

                // Create the results
                if (values != null) {
                    coverageDataResults = new CoverageDataResults(values,
                            tileMatrix);
                }

            } finally {
                tileResults.close();
            }
        }

        return coverageDataResults;
    }

    /**
     * Get the coverage data tile results by finding the tile matrix with values
     *
     * @param request                     coverage data request
     * @param requestProjectedBoundingBox request projected bounding box
     * @return tile matrix results
     */
    private CoverageDataTileMatrixResults getResults(CoverageDataRequest request,
                                                     BoundingBox requestProjectedBoundingBox) {
        return getResults(request, requestProjectedBoundingBox, 0);
    }

    /**
     * Get the coverage data tile results by finding the tile matrix with values
     *
     * @param request                     coverage data request
     * @param requestProjectedBoundingBox request projected bounding box
     * @param overlappingPixels           overlapping request pixels
     * @return tile matrix results
     */
    private CoverageDataTileMatrixResults getResults(CoverageDataRequest request,
                                                     BoundingBox requestProjectedBoundingBox, int overlappingPixels) {
        // Try to get the coverage data from the current zoom level
        TileMatrix tileMatrix = getTileMatrix(request);
        CoverageDataTileMatrixResults results = null;
        if (tileMatrix != null) {
            results = getResults(requestProjectedBoundingBox, tileMatrix,
                    overlappingPixels);

            // Try to zoom in or out to find a matching coverage data
            if (results == null) {
                results = getResultsZoom(requestProjectedBoundingBox,
                        tileMatrix, overlappingPixels);
            }
        }
        return results;
    }

    /**
     * Get the coverage data tile results for a specified tile matrix
     *
     * @param requestProjectedBoundingBox request projected bounding box
     * @param tileMatrix                  tile matrix
     * @param overlappingPixels           number of overlapping pixels used by the algorithm
     * @return tile matrix results
     */
    private CoverageDataTileMatrixResults getResults(
            BoundingBox requestProjectedBoundingBox, TileMatrix tileMatrix,
            int overlappingPixels) {
        CoverageDataTileMatrixResults results = null;
        BoundingBox paddedBoundingBox = padBoundingBox(tileMatrix,
                requestProjectedBoundingBox, overlappingPixels);
        TileCursor tileResults = retrieveSortedTileResults(
                paddedBoundingBox, tileMatrix);
        if (tileResults != null) {
            if (tileResults.getCount() > 0) {
                results = new CoverageDataTileMatrixResults(tileMatrix,
                        tileResults);
            } else {
                tileResults.close();
            }
        }
        return results;
    }

    /**
     * Get the coverage data tile results by zooming in or out as needed from the
     * provided tile matrix to find values
     *
     * @param requestProjectedBoundingBox request projected bounding box
     * @param tileMatrix                  tile matrix
     * @param overlappingPixels           overlapping request pixels
     * @return tile matrix results
     */
    private CoverageDataTileMatrixResults getResultsZoom(
            BoundingBox requestProjectedBoundingBox, TileMatrix tileMatrix,
            int overlappingPixels) {

        CoverageDataTileMatrixResults results = null;

        if (zoomIn && zoomInBeforeOut) {
            results = getResultsZoomIn(requestProjectedBoundingBox, tileMatrix,
                    overlappingPixels);
        }
        if (results == null && zoomOut) {
            results = getResultsZoomOut(requestProjectedBoundingBox,
                    tileMatrix, overlappingPixels);
        }
        if (results == null && zoomIn && !zoomInBeforeOut) {
            results = getResultsZoomIn(requestProjectedBoundingBox, tileMatrix,
                    overlappingPixels);
        }

        return results;
    }

    /**
     * Get the coverage data tile results by zooming in from the provided tile
     * matrix
     *
     * @param requestProjectedBoundingBox request projected bounding box
     * @param tileMatrix                  tile matrix
     * @param overlappingPixels           overlapping request pixels
     * @return tile matrix results
     */
    private CoverageDataTileMatrixResults getResultsZoomIn(
            BoundingBox requestProjectedBoundingBox, TileMatrix tileMatrix,
            int overlappingPixels) {

        CoverageDataTileMatrixResults results = null;
        for (long zoomLevel = tileMatrix.getZoomLevel() + 1; zoomLevel <= tileDao
                .getMaxZoom(); zoomLevel++) {
            TileMatrix zoomTileMatrix = tileDao.getTileMatrix(zoomLevel);
            if (zoomTileMatrix != null) {
                results = getResults(requestProjectedBoundingBox,
                        zoomTileMatrix, overlappingPixels);
                if (results != null) {
                    break;
                }
            }
        }
        return results;
    }

    /**
     * Get the coverage data tile results by zooming out from the provided tile
     * matrix
     *
     * @param requestProjectedBoundingBox request projected bounding box
     * @param tileMatrix                  tile matrix
     * @param overlappingPixels           overlapping request pixels
     * @return tile matrix results
     */
    private CoverageDataTileMatrixResults getResultsZoomOut(
            BoundingBox requestProjectedBoundingBox, TileMatrix tileMatrix,
            int overlappingPixels) {

        CoverageDataTileMatrixResults results = null;
        for (long zoomLevel = tileMatrix.getZoomLevel() - 1; zoomLevel >= tileDao
                .getMinZoom(); zoomLevel--) {
            TileMatrix zoomTileMatrix = tileDao.getTileMatrix(zoomLevel);
            if (zoomTileMatrix != null) {
                results = getResults(requestProjectedBoundingBox,
                        zoomTileMatrix, overlappingPixels);
                if (results != null) {
                    break;
                }
            }
        }
        return results;
    }

    /**
     * Get the coverage data values from the tile results scaled to the provided
     * dimensions
     *
     * @param tileMatrix        tile matrix
     * @param tileResults       tile results
     * @param request           coverage data request
     * @param tileWidth         tile width
     * @param tileHeight        tile height
     * @param overlappingPixels overlapping request pixels
     * @return coverage data values
     */
    private Double[][] getValues(TileMatrix tileMatrix,
                                 TileCursor tileResults, CoverageDataRequest request, int tileWidth,
                                 int tileHeight, int overlappingPixels) {

        Double[][] values = null;

        // Tiles are ordered by rows and then columns. Track the last column
        // coverage data values of the tile to the left and the last rows of the tiles
        // in the row above
        Double[][] leftLastColumns = null;
        Map<Long, Double[][]> lastRowsByColumn = null;
        Map<Long, Double[][]> previousLastRowsByColumn = null;

        long previousRow = -1;
        long previousColumn = Long.MAX_VALUE;

        // Process each coverage data tile
        while (tileResults.moveToNext()) {

            // Get the next coverage data tile
            TileRow tileRow = tileResults.getRow();

            long currentRow = tileRow.getTileRow();
            long currentColumn = tileRow.getTileColumn();

            // If the row has changed, save off the previous last rows and begin
            // tracking this row. Clear the left last columns.
            if (currentRow > previousRow) {
                previousLastRowsByColumn = lastRowsByColumn;
                lastRowsByColumn = new HashMap<Long, Double[][]>();
                leftLastColumns = null;
            }

            // If there was a previous row, retrieve the top left and top
            // overlapping rows
            Double[][] topLeftRows = null;
            Double[][] topRows = null;
            if (previousLastRowsByColumn != null) {
                topLeftRows = previousLastRowsByColumn.get(currentColumn - 1);
                topRows = previousLastRowsByColumn.get(currentColumn);
            }

            // If the current column is not the column after the previous clear
            // the left values
            if (currentColumn < previousColumn
                    || currentColumn != previousColumn + 1) {
                leftLastColumns = null;
            }

            // Get the bounding box of the coverage data
            BoundingBox tileBoundingBox = TileBoundingBoxUtils
                    .getBoundingBox(coverageBoundingBox, tileMatrix,
                            currentColumn, currentRow);

            // Get the bounding box where the request and coverage data tile overlap
            BoundingBox overlap = request.overlap(tileBoundingBox);

            // Get the gridded tile value for the tile
            GriddedTile griddedTile = getGriddedTile(tileRow.getId());

            // Get the coverage data tile image
            TImage image = createImage(tileRow);

            // If the tile overlaps with the requested box
            if (overlap != null) {

                // Get the rectangle of the tile coverage data with matching values
                RectF src = TileBoundingBoxAndroidUtils
                        .getFloatRectangle(tileMatrix.getTileWidth(),
                                tileMatrix.getTileHeight(), tileBoundingBox,
                                overlap);

                // Get the rectangle of where to store the results
                RectF dest = null;
                if (request.getProjectedBoundingBox().equals(overlap)) {
                    if (request.isPoint()) {
                        // For single points request only a single destination
                        // pixel
                        dest = new RectF(0, 0, 0, 0);
                    } else {
                        // The overlap is equal to the request, set as the full
                        // destination size
                        dest = new RectF(0, 0, tileWidth, tileHeight);
                    }
                } else {
                    dest = TileBoundingBoxAndroidUtils.getFloatRectangle(
                            tileWidth, tileHeight,
                            request.getProjectedBoundingBox(), overlap);
                }

                if (TileBoundingBoxAndroidUtils.isValidAllowEmpty(src) && TileBoundingBoxAndroidUtils.isValidAllowEmpty(dest)) {

                    // Create the coverage data array first time through
                    if (values == null) {
                        values = new Double[tileHeight][tileWidth];
                    }

                    // Get the destination widths
                    float destWidth = dest.right - dest.left;
                    float destHeight = dest.bottom - dest.top;

                    // Get the destination heights
                    float srcWidth = src.right - src.left;
                    float srcHeight = src.bottom - src.top;

                    // Determine the source to destination ratio and how many
                    // destination pixels equal half a source pixel
                    float widthRatio;
                    float halfDestWidthPixel;
                    if (destWidth == 0) {
                        widthRatio = 0.0f;
                        halfDestWidthPixel = 0.0f;
                    } else {
                        widthRatio = srcWidth / destWidth;
                        halfDestWidthPixel = 0.5f / widthRatio;
                    }
                    float heightRatio;
                    float halfDestHeightPixel;
                    if (destHeight == 0) {
                        heightRatio = 0.0f;
                        halfDestHeightPixel = 0.0f;
                    } else {
                        heightRatio = srcHeight / destHeight;
                        halfDestHeightPixel = 0.5f / heightRatio;
                    }

                    float algorithmDestWidthPixelOverlap = halfDestWidthPixel
                            * overlappingPixels;
                    float algorithmDestHeightPixelOverlap = halfDestHeightPixel
                            * overlappingPixels;

                    // Determine the range of destination values to set
                    int minDestY = (int) Math.floor(dest.top
                            - algorithmDestHeightPixelOverlap);
                    int maxDestY = (int) Math.ceil(dest.bottom
                            + algorithmDestHeightPixelOverlap);
                    int minDestX = (int) Math.floor(dest.left
                            - algorithmDestWidthPixelOverlap);
                    int maxDestX = (int) Math.ceil(dest.right
                            + algorithmDestWidthPixelOverlap);
                    minDestY = Math.max(minDestY, 0);
                    minDestX = Math.max(minDestX, 0);
                    maxDestY = Math.min(maxDestY, tileHeight - 1);
                    maxDestX = Math.min(maxDestX, tileWidth - 1);

                    // Read and set the coverage data values
                    for (int y = minDestY; y <= maxDestY; y++) {
                        for (int x = minDestX; x <= maxDestX; x++) {

                            if (values[y][x] == null) {

                                // Determine the coverage data based upon the
                                // selected algorithm
                                Double value = null;
                                switch (algorithm) {
                                    case NEAREST_NEIGHBOR:
                                        value = getNearestNeighborValue(
                                                griddedTile, image, leftLastColumns,
                                                topLeftRows, topRows, y, x, widthRatio,
                                                heightRatio, dest.top,
                                                dest.left, src.top,
                                                src.left);
                                        break;
                                    case BILINEAR:
                                        value = getBilinearInterpolationValue(
                                                griddedTile, image, leftLastColumns,
                                                topLeftRows, topRows, y, x, widthRatio,
                                                heightRatio, dest.top,
                                                dest.left, src.top,
                                                src.left);
                                        break;
                                    case BICUBIC:
                                        value = getBicubicInterpolationValue(
                                                griddedTile, image, leftLastColumns,
                                                topLeftRows, topRows, y, x, widthRatio,
                                                heightRatio, dest.top,
                                                dest.left, src.top,
                                                src.left);
                                        break;
                                    default:
                                        throw new UnsupportedOperationException(
                                                "Algorithm is not supported: "
                                                        + algorithm);
                                }

                                if (value != null) {
                                    values[y][x] = value;
                                }

                            }
                        }
                    }

                }
            }

            // Determine and store the coverage data values of the last columns and rows
            leftLastColumns = new Double[overlappingPixels][(int) tileMatrix
                    .getTileHeight()];
            Double[][] lastRows = new Double[overlappingPixels][(int) tileMatrix
                    .getTileWidth()];
            lastRowsByColumn.put(currentColumn, lastRows);

            // For each overlapping pixel
            for (int lastIndex = 0; lastIndex < overlappingPixels; lastIndex++) {

                // Store the last column row coverage data values
                int lastColumnIndex = (int) tileMatrix.getTileWidth()
                        - lastIndex - 1;
                for (int row = 0; row < tileMatrix.getTileHeight(); row++) {
                    Double value = getValue(griddedTile, image,
                            lastColumnIndex, row);
                    leftLastColumns[lastIndex][row] = value;
                }

                // Store the last row column coverage data values
                int lastRowIndex = (int) tileMatrix.getTileHeight() - lastIndex
                        - 1;
                for (int column = 0; column < tileMatrix.getTileWidth(); column++) {
                    Double value = getValue(griddedTile, image,
                            column, lastRowIndex);
                    lastRows[lastIndex][column] = value;
                }

            }

            // Update the previous row and column
            previousRow = currentRow;
            previousColumn = currentColumn;
        }

        return values;
    }

    /**
     * Get the coverage data values from the tile results unbounded in result size
     *
     * @param tileMatrix  tile matrix
     * @param tileResults tile results
     * @param request     coverage data request
     * @return coverage data values
     */
    private Double[][] getValuesUnbounded(TileMatrix tileMatrix,
                                          TileCursor tileResults, CoverageDataRequest request) {

        // Build a map of rows to maps of columns and values
        Map<Long, Map<Long, Double[][]>> rowsMap = new TreeMap<>();

        // Track the min and max row and column
        Long minRow = null;
        Long maxRow = null;
        Long minColumn = null;
        Long maxColumn = null;

        // Track count of tiles involved in the results
        int tileCount = 0;

        // Process each coverage data tile row
        while (tileResults.moveToNext()) {

            // Get the next coverage data tile
            TileRow tileRow = tileResults.getRow();

            // Get the bounding box of the coverage data
            BoundingBox tileBoundingBox = TileBoundingBoxUtils.getBoundingBox(
                    coverageBoundingBox, tileMatrix, tileRow.getTileColumn(),
                    tileRow.getTileRow());

            // Get the bounding box where the request and coverage data tile overlap
            BoundingBox overlap = request.overlap(tileBoundingBox);

            // If the coverage data tile overlaps with the requested box
            if (overlap != null) {

                // Get the rectangle of the tile coverage data with matching values
                Rect src = TileBoundingBoxAndroidUtils.getRectangle(
                        tileMatrix.getTileWidth(), tileMatrix.getTileHeight(),
                        tileBoundingBox, overlap);

                if (TileBoundingBoxAndroidUtils.isValidAllowEmpty(src)) {

                    // Get the source dimensions
                    int srcTop = Math.min(src.top,
                            (int) tileMatrix.getTileHeight() - 1);
                    int srcBottom = Math.min(src.bottom,
                            (int) tileMatrix.getTileHeight() - 1);
                    int srcLeft = Math.min(src.left,
                            (int) tileMatrix.getTileWidth() - 1);
                    int srcRight = Math.min(src.right,
                            (int) tileMatrix.getTileWidth() - 1);

                    // Get the gridded tile value for the tile
                    GriddedTile griddedTile = getGriddedTile(tileRow.getId());

                    // Get the coverage data tile image
                    TImage image = createImage(tileRow);

                    // Create the coverage data results for this tile
                    Double[][] values = new Double[srcBottom - srcTop + 1][srcRight
                            - srcLeft + 1];

                    // Get or add the columns map to the rows map
                    Map<Long, Double[][]> columnsMap = rowsMap.get(tileRow
                            .getTileRow());
                    if (columnsMap == null) {
                        columnsMap = new TreeMap<Long, Double[][]>();
                        rowsMap.put(tileRow.getTileRow(), columnsMap);
                    }

                    // Read and set the coverage data values
                    for (int y = srcTop; y <= srcBottom; y++) {

                        for (int x = srcLeft; x <= srcRight; x++) {

                            // Get the coverage data value from the source pixel
                            Double value = getValue(griddedTile,
                                    image, x, y);

                            values[y - srcTop][x - srcLeft] = value;
                        }
                    }

                    // Set the coverage data values in the results map
                    columnsMap.put(tileRow.getTileColumn(), values);

                    // Increase the contributing tiles count
                    tileCount++;

                    // Track the min and max row and column
                    minRow = minRow == null ? tileRow.getTileRow() : Math.min(
                            minRow, tileRow.getTileRow());
                    maxRow = maxRow == null ? tileRow.getTileRow() : Math.max(
                            maxRow, tileRow.getTileRow());
                    minColumn = minColumn == null ? tileRow.getTileColumn()
                            : Math.min(minColumn, tileRow.getTileColumn());
                    maxColumn = maxColumn == null ? tileRow.getTileColumn()
                            : Math.max(maxColumn, tileRow.getTileColumn());
                }
            }
        }

        // Handle formatting the results
        Double[][] values = formatUnboundedResults(tileMatrix, rowsMap,
                tileCount, minRow, maxRow, minColumn, maxColumn);

        return values;
    }

    /**
     * Get the tile matrix for the zoom level as defined by the area of the
     * request
     *
     * @param request coverage data request
     * @return tile matrix or null
     */
    private TileMatrix getTileMatrix(CoverageDataRequest request) {

        TileMatrix tileMatrix = null;

        // Check if the request overlaps coverage data bounding box
        if (request.overlap(coverageBoundingBox) != null) {

            // Get the tile distance
            BoundingBox projectedBoundingBox = request
                    .getProjectedBoundingBox();
            double distanceWidth = projectedBoundingBox.getMaxLongitude()
                    - projectedBoundingBox.getMinLongitude();
            double distanceHeight = projectedBoundingBox.getMaxLatitude()
                    - projectedBoundingBox.getMinLatitude();

            // Get the zoom level to request based upon the tile size
            Long zoomLevel = tileDao.getClosestZoomLevel(distanceWidth,
                    distanceHeight);

            // If there is a matching zoom level
            if (zoomLevel != null) {
                tileMatrix = tileDao.getTileMatrix(zoomLevel);
            }
        }

        return tileMatrix;
    }

    /**
     * Get the tile row results of coverage data tiles needed to create the
     * requested bounding box coverage data, sorted by row and then column
     *
     * @param projectedRequestBoundingBox bounding box projected to the coverage data
     * @param tileMatrix                  tile matrix
     * @return tile results or null
     */
    private TileCursor retrieveSortedTileResults(
            BoundingBox projectedRequestBoundingBox, TileMatrix tileMatrix) {

        TileCursor tileResults = null;

        if (tileMatrix != null) {

            // Get the tile grid
            TileGrid tileGrid = TileBoundingBoxUtils.getTileGrid(
                    coverageBoundingBox, tileMatrix.getMatrixWidth(),
                    tileMatrix.getMatrixHeight(), projectedRequestBoundingBox);

            // Query for matching tiles in the tile grid
            tileResults = tileDao.queryByTileGrid(tileGrid,
                    tileMatrix.getZoomLevel(), TileTable.COLUMN_TILE_ROW + ","
                            + TileTable.COLUMN_TILE_COLUMN);

        }

        return tileResults;
    }

    /**
     * Get the coverage data value of the pixel in the tile row image
     *
     * @param tileRow tile row
     * @param x       x coordinate
     * @param y       y coordinate
     * @return coverage data value
     */
    public double getValue(TileRow tileRow, int x, int y) {
        GriddedTile griddedTile = getGriddedTile(tileRow.getId());
        double value = getValue(griddedTile, tileRow, x, y);
        return value;
    }

}


package mil.nga.geopackage.extension.style;

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

import mil.nga.sf.GeometryType;
import mil.nga.sf.util.GeometryUtils;

/**
 * Icons for a single feature geometry or feature table default
 *
 * @author osbornb
 * @since 3.2.0
 */
public class Icons {

    /**
     * Default icon
     */
    private IconRow defaultIcon;

    /**
     * Geometry type to icon mapping
     */
    private Map<GeometryType, IconRow> icons = new HashMap<>();

    /**
     * Set the default icon
     *
     * @param iconRow default icon
     */
    public void setDefault(IconRow iconRow) {
        setIcon(iconRow, null);
    }

    /**
     * Set the icon for the geometry type
     *
     * @param iconRow      icon row
     * @param geometryType geometry type
     */
    public void setIcon(IconRow iconRow, GeometryType geometryType) {
        if (geometryType != null) {
            if (iconRow != null) {
                icons.put(geometryType, iconRow);
            } else {
                icons.remove(geometryType);
            }
        } else {
            defaultIcon = iconRow;
        }
    }

    /**
     * Get the default icon
     *
     * @return default icon
     */
    public IconRow getDefault() {
        return defaultIcon;
    }

    /**
     * Get an unmodifiable mapping between specific geometry types and icons
     *
     * @return geometry types to icon mapping
     */
    public Map<GeometryType, IconRow> getIcons() {
        return Collections.unmodifiableMap(icons);
    }

    /**
     * Get the icon, either the default or single geometry type icon
     *
     * @return style
     */
    public IconRow getIcon() {
        return getIcon(null);
    }

    /**
     * Get the icon for the geometry type
     *
     * @param geometryType geometry type
     * @return icon
     */
    public IconRow getIcon(GeometryType geometryType) {

        IconRow iconRow = null;

        if (geometryType != null && !icons.isEmpty()) {
            List<GeometryType> geometryTypes = GeometryUtils
                    .parentHierarchy(geometryType);
            geometryTypes.add(0, geometryType);
            for (GeometryType type : geometryTypes) {
                iconRow = icons.get(type);
                if (iconRow != null) {
                    break;
                }
            }
        }

        if (iconRow == null) {
            iconRow = defaultIcon;
        }

        if (iconRow == null && geometryType == null && icons.size() == 1) {
            iconRow = icons.values().iterator().next();
        }

        return iconRow;
    }

    /**
     * Determine if this icons is empty
     *
     * @return true if empty, false if at least one icon
     */
    public boolean isEmpty() {
        return defaultIcon == null && icons.isEmpty();
    }

    /**
     * Determine if there is a default icon
     *
     * @return true if default icon exists
     */
    public boolean hasDefault() {
        return defaultIcon != null;
    }

}


package mil.nga.geopackage.user.custom;

import android.database.Cursor;

import java.util.List;

import mil.nga.geopackage.user.UserCursor;
import mil.nga.geopackage.user.UserDao;
import mil.nga.geopackage.user.UserInvalidCursor;

/**
 * User Custom Cursor to wrap a database Cursor for tile queries
 *
 * @author osbornb
 * @since 3.0.1
 */
public class UserCustomCursor extends
        UserCursor<UserCustomColumn, UserCustomTable, UserCustomRow> {

    /**
     * Constructor
     *
     * @param table  user custom table
     * @param cursor cursor
     */
    public UserCustomCursor(UserCustomTable table, Cursor cursor) {
        super(table, cursor);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public UserCustomRow getRow(int[] columnTypes, Object[] values) {
        return new UserCustomRow(getTable(), columnTypes, values);
    }

    /**
     * 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
     */
    public void enableInvalidRequery(UserCustomDao dao) {
        super.enableInvalidRequery(dao);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected UserInvalidCursor<UserCustomColumn, UserCustomTable, UserCustomRow, ? extends UserCursor<UserCustomColumn, UserCustomTable, UserCustomRow>, ? extends UserDao<UserCustomColumn, UserCustomTable, UserCustomRow, ? extends UserCursor<UserCustomColumn, UserCustomTable, UserCustomRow>>> createInvalidCursor(UserDao dao, UserCursor cursor, List<Integer> invalidPositions, List<UserCustomColumn> blobColumns) {
        return new UserCustomInvalidCursor((UserCustomDao) dao, (UserCustomCursor) cursor, invalidPositions, blobColumns);
    }

}


package mil.nga.geopackage.db;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;

/**
 * GeoPackage database wrapper around SQLiteDatabase to quote table and column names
 *
 * @author osbornb
 * @since 1.3.1
 */
public class GeoPackageDatabase {

    /**
     * Database connection
     */
    private final SQLiteDatabase db;

    /**
     * SQLite Android Bindings connection
     */
    private org.sqlite.database.sqlite.SQLiteDatabase bindingsDb;

    /**
     * Constructor
     *
     * @param db database
     */
    public GeoPackageDatabase(SQLiteDatabase db) {
        this.db = db;
    }

    /**
     * Get the SQLite database connection
     *
     * @return connection
     */
    public SQLiteDatabase getDb() {
        return db;
    }

    /**
     * Open or get a connection using the SQLite Android Bindings connection
     *
     * @return bindings connection
     */
    public org.sqlite.database.sqlite.SQLiteDatabase openOrGetBindingsDb() {
        if (bindingsDb == null) {
            synchronized (db) {
                if (bindingsDb == null) {
                    System.loadLibrary("sqliteX");
                    bindingsDb = org.sqlite.database.sqlite.SQLiteDatabase.openDatabase(db.getPath(), null, org.sqlite.database.sqlite.SQLiteDatabase.OPEN_READWRITE);
                }
            }
        }
        return bindingsDb;
    }

    /**
     * @param sql sql command
     * @see SQLiteDatabase#execSQL(String)
     */
    public void execSQL(String sql) throws SQLException {
        db.execSQL(sql);
    }

    /**
     * @param table       table name
     * @param whereClause where clause
     * @param whereArgs   where arguments
     * @return deleted rows
     * @see SQLiteDatabase#delete(String, String, String[])
     */
    public int delete(String table, String whereClause, String[] whereArgs) {
        return db.delete(CoreSQLUtils.quoteWrap(table), whereClause, whereArgs);
    }

    /**
     * @param sql           sql command
     * @param selectionArgs selection arguments
     * @return cursor
     * @see SQLiteDatabase#rawQuery(String, String[])
     */
    public Cursor rawQuery(String sql, String[] selectionArgs) {
        return db.rawQuery(sql, selectionArgs);
    }

    /**
     * @see SQLiteDatabase#close()
     */
    public void close() {
        db.close();
        if (bindingsDb != null) {
            bindingsDb.close();
        }
    }

    /**
     * @param table         table name
     * @param columns       columns
     * @param selection     selection
     * @param selectionArgs selection arguments
     * @param groupBy       group by
     * @param having        having
     * @param orderBy       order by
     * @return cursor
     * @see SQLiteDatabase#query(String, String[], String, String[], String, String, String)
     */
    public Cursor query(String table, String[] columns, String selection,
                        String[] selectionArgs, String groupBy, String having,
                        String orderBy) {
        return db.query(CoreSQLUtils.quoteWrap(table), CoreSQLUtils.quoteWrap(columns), selection, selectionArgs, groupBy, having, orderBy);
    }

    /**
     * @param table         table name
     * @param columns       columns
     * @param columnsAs     columns as
     * @param selection     selection
     * @param selectionArgs selection arguments
     * @param groupBy       group by
     * @param having        having
     * @param orderBy       order by
     * @return cursor
     * @see SQLiteDatabase#query(String, String[], String, String[], String, String, String)
     * @since 2.0.0
     */
    public Cursor query(String table, String[] columns, String[] columnsAs, String selection,
                        String[] selectionArgs, String groupBy, String having,
                        String orderBy) {
        String[] wrappedColumns = CoreSQLUtils.quoteWrap(columns);
        String[] wrappedColumnsAs = CoreSQLUtils.buildColumnsAs(wrappedColumns, columnsAs);
        return db.query(CoreSQLUtils.quoteWrap(table), wrappedColumnsAs, selection, selectionArgs, groupBy, having, orderBy);
    }

    /**
     * @param table         table name
     * @param columns       columns
     * @param selection     selection
     * @param selectionArgs selection arguments
     * @param groupBy       group by
     * @param having        having
     * @param orderBy       order by
     * @param limit         limit
     * @return cursor
     * @see SQLiteDatabase#query(String, String[], String, String[], String, String, String, String)
     */
    public Cursor query(String table, String[] columns, String selection,
                        String[] selectionArgs, String groupBy, String having,
                        String orderBy, String limit) {
        return db.query(CoreSQLUtils.quoteWrap(table), CoreSQLUtils.quoteWrap(columns), selection, selectionArgs, groupBy, having, orderBy, limit);
    }

    /**
     * @param table         table name
     * @param columns       columns
     * @param columnsAs     columns as
     * @param selection     selection
     * @param selectionArgs selection arguments
     * @param groupBy       group by
     * @param having        having
     * @param orderBy       order by
     * @param limit         limit
     * @return cursor
     * @see SQLiteDatabase#query(String, String[], String, String[], String, String, String, String)
     * @since 2.0.0
     */
    public Cursor query(String table, String[] columns, String[] columnsAs, String selection,
                        String[] selectionArgs, String groupBy, String having,
                        String orderBy, String limit) {
        String[] wrappedColumns = CoreSQLUtils.quoteWrap(columns);
        String[] wrappedColumnsAs = CoreSQLUtils.buildColumnsAs(wrappedColumns, columnsAs);
        return db.query(CoreSQLUtils.quoteWrap(table), wrappedColumnsAs, selection, selectionArgs, groupBy, having, orderBy, limit);
    }

    /**
     * @param table       table name
     * @param values      content values
     * @param whereClause where clause
     * @param whereArgs   where arguments
     * @return updated rows
     * @see SQLiteDatabase#update(String, ContentValues, String, String[])
     */
    public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
        return db.update(CoreSQLUtils.quoteWrap(table), SQLUtils.quoteWrap(values), whereClause, whereArgs);
    }

    /**
     * @param table          table name
     * @param nullColumnHack null column hack
     * @param values         content values
     * @return row id
     * @see SQLiteDatabase#insertOrThrow(String, String, ContentValues)
     */
    public long insertOrThrow(String table, String nullColumnHack, ContentValues values) throws SQLException {
        return db.insertOrThrow(CoreSQLUtils.quoteWrap(table), nullColumnHack, SQLUtils.quoteWrap(values));
    }

    /**
     * @param table          table name
     * @param nullColumnHack null column hack
     * @param values         content values
     * @return row id
     * @see SQLiteDatabase#insert(String, String, ContentValues)
     */
    public long insert(String table, String nullColumnHack, ContentValues values) {
        return db.insert(CoreSQLUtils.quoteWrap(table), nullColumnHack, SQLUtils.quoteWrap(values));
    }

}


package mil.nga.geopackage.io;

import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * Conversions between Bitmaps and image bytes
 *
 * @author osbornb
 */
public class BitmapConverter {

    /**
     * Decode the bytes to a bitmap
     *
     * @param bytes image bytes
     * @return image bitmap
     */
    public static Bitmap toBitmap(byte[] bytes) {
        return toBitmap(bytes, null);
    }

    /**
     * Decode the bytes to a bitmap, with options
     *
     * @param bytes   image bytes
     * @param options decode options
     * @return image bitmap
     */
    public static Bitmap toBitmap(byte[] bytes, Options options) {
        Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length,
                options);
        return bitmap;
    }

    /**
     * Compress the bitmap to a byte array at full quality
     *
     * @param bitmap image bitmap
     * @param format compress format
     * @return image bytes
     * @throws IOException upon failure
     */
    public static byte[] toBytes(Bitmap bitmap, CompressFormat format)
            throws IOException {
        return toBytes(bitmap, format, 100);
    }

    /**
     * Compress the bitmap to a byte array
     *
     * @param bitmap  bitmap image
     * @param format  compress format
     * @param quality quality
     * @return image bytes
     * @throws IOException upon failure
     */
    public static byte[] toBytes(Bitmap bitmap, CompressFormat format,
                                 int quality) throws IOException {
        byte[] bytes = null;
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        try {
            bitmap.compress(format, quality, byteStream);
            bytes = byteStream.toByteArray();
        } finally {
            byteStream.close();
        }
        return bytes;
    }

}


package mil.nga.geopackage.db.metadata;

import android.content.ContentValues;
import android.database.Cursor;

import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.db.GeoPackageDatabase;

/**
 * Table metadata Data Source
 *
 * @author osbornb
 */
public class TableMetadataDataSource {

    /**
     * Database
     */
    private GeoPackageDatabase db;

    /**
     * Constructor
     *
     * @param db GeoPackage metadata db
     */
    public TableMetadataDataSource(GeoPackageMetadataDb db) {
        this.db = db.getDb();
    }

    /**
     * Constructor
     *
     * @param db
     */
    TableMetadataDataSource(GeoPackageDatabase db) {
        this.db = db;
    }

    /**
     * Create a new table metadata
     *
     * @param metadata table metadata
     */
    public void create(TableMetadata metadata) {
        ContentValues values = new ContentValues();
        values.put(TableMetadata.COLUMN_GEOPACKAGE_ID, metadata.getGeoPackageId());
        values.put(TableMetadata.COLUMN_TABLE_NAME, metadata.getTableName());
        values.put(TableMetadata.COLUMN_LAST_INDEXED, metadata.getLastIndexed());
        long insertId = db.insert(
                TableMetadata.TABLE_NAME, null,
                values);
        if (insertId == -1) {
            throw new GeoPackageException(
                    "Failed to insert table metadata. GeoPackage Id: "
                            + metadata.getGeoPackageId() + ", Table Name: "
                            + metadata.getTableName() + ", Last Indexed: "
                            + metadata.getLastIndexed());
        }
    }

    /**
     * Delete the table metadata
     *
     * @param metadata table metadata
     * @return deleted flag
     */
    public boolean delete(TableMetadata metadata) {
        return delete(metadata.getGeoPackageId(), metadata.getTableName());
    }

    /**
     * Delete the database
     *
     * @param geoPackage GeoPackage name
     * @return deleted count
     */
    public int delete(String geoPackage) {
        return delete(getGeoPackageId(geoPackage));
    }

    /**
     * Delete the database
     *
     * @param geoPackageId GeoPackage id
     * @return deleted count
     */
    public int delete(long geoPackageId) {

        GeometryMetadataDataSource geomDs = new GeometryMetadataDataSource(db);
        geomDs.delete(geoPackageId);

        String whereClause = TableMetadata.COLUMN_GEOPACKAGE_ID + " = ?";
        String[] whereArgs = new String[]{String.valueOf(geoPackageId)};
        int deleteCount = db.delete(
                TableMetadata.TABLE_NAME,
                whereClause, whereArgs);
        return deleteCount;
    }

    /**
     * Delete the database table name
     *
     * @param geoPackage GeoPackage name
     * @param tableName  table name
     * @return deleted flag
     */
    public boolean delete(String geoPackage, String tableName) {
        return delete(getGeoPackageId(geoPackage), tableName);
    }

    /**
     * Delete the database table name
     *
     * @param geoPackageId GeoPackage id
     * @param tableName    table name
     * @return deleted flag
     */
    public boolean delete(long geoPackageId, String tableName) {

        GeometryMetadataDataSource geomDs = new GeometryMetadataDataSource(db);
        geomDs.delete(geoPackageId, tableName);

        String whereClause = TableMetadata.COLUMN_GEOPACKAGE_ID
                + " = ? AND " + TableMetadata.COLUMN_TABLE_NAME + " = ?";
        String[] whereArgs = new String[]{String.valueOf(geoPackageId), tableName};
        int deleteCount = db.delete(
                TableMetadata.TABLE_NAME,
                whereClause, whereArgs);
        return deleteCount > 0;
    }

    /**
     * Update the last indexed time
     *
     * @param metadata    table metadata
     * @param lastIndexed last indexed
     * @return updated flag
     */
    public boolean updateLastIndexed(TableMetadata metadata, long lastIndexed) {
        boolean updated = updateLastIndexed(metadata.getGeoPackageId(), metadata.getTableName(), lastIndexed);
        if (updated) {
            metadata.setLastIndexed(lastIndexed);
        }
        return updated;
    }

    /**
     * Update the last indexed time
     *
     * @param geoPackage  GeoPackage name
     * @param tableName   table name
     * @param lastIndexed last indexed
     * @return updated flag
     */
    public boolean updateLastIndexed(String geoPackage, String tableName, long lastIndexed) {
        return updateLastIndexed(getGeoPackageId(geoPackage), tableName, lastIndexed);
    }

    /**
     * Update the last indexed time
     *
     * @param geoPackageId GeoPackage id
     * @param tableName    table name
     * @param lastIndexed  last indexed
     * @return updated flag
     */
    public boolean updateLastIndexed(long geoPackageId, String tableName, long lastIndexed) {
        String whereClause = TableMetadata.COLUMN_GEOPACKAGE_ID
                + " = ? AND " + TableMetadata.COLUMN_TABLE_NAME + " = ?";
        String[] whereArgs = new String[]{String.valueOf(geoPackageId), tableName};
        ContentValues values = new ContentValues();
        values.put(TableMetadata.COLUMN_LAST_INDEXED, lastIndexed);
        int updateCount = db.update(
                TableMetadata.TABLE_NAME, values,
                whereClause, whereArgs);
        return updateCount > 0;
    }

    /**
     * Get a table metadata
     *
     * @param geoPackage GeoPackage name
     * @param tableName  table name
     * @return table metadata
     */
    public TableMetadata get(String geoPackage, String tableName) {
        return get(getGeoPackageId(geoPackage), tableName);
    }

    /**
     * Get a table metadata
     *
     * @param geoPackageId GeoPackage id
     * @param tableName    table name
     * @return table metadata
     */
    public TableMetadata get(long geoPackageId, String tableName) {

        String selection = TableMetadata.COLUMN_GEOPACKAGE_ID
                + " = ? AND " + TableMetadata.COLUMN_TABLE_NAME + " = ?";
        String[] selectionArgs = new String[]{String.valueOf(geoPackageId), tableName};
        Cursor cursor = db.query(
                TableMetadata.TABLE_NAME,
                TableMetadata.COLUMNS, selection, selectionArgs, null, null, null);
        TableMetadata metadata = null;
        try {
            if (cursor.moveToNext()) {
                metadata = createTableMetadata(cursor);
            }
        } finally {
            cursor.close();
        }
        return metadata;
    }

    /**
     * Get a table metadata or create if it does not exist
     *
     * @param geoPackage GeoPackage name
     * @param tableName  table name
     * @return table metadata
     */
    public TableMetadata getOrCreate(String geoPackage, String tableName) {

        GeoPackageMetadataDataSource ds = new GeoPackageMetadataDataSource(db);
        GeoPackageMetadata geoPackageMetadata = ds.getOrCreate(geoPackage);

        TableMetadata metadata = get(geoPackageMetadata.getId(), tableName);

        if (metadata == null) {
            metadata = new TableMetadata();
            metadata.setGeoPackageId(geoPackageMetadata.getId());
            metadata.setTableName(tableName);
            create(metadata);
        }
        return metadata;
    }

    /**
     * Get a GeoPackage id from the name
     *
     * @param geoPackage GeoPackage name
     * @return id
     */
    public long getGeoPackageId(String geoPackage) {
        long id = -1;
        GeoPackageMetadataDataSource ds = new GeoPackageMetadataDataSource(db);
        GeoPackageMetadata metadata = ds.get(geoPackage);
        if (metadata != null) {
            id = metadata.getId();
        }
        return id;
    }

    /**
     * Create a table metadata from the current cursor location
     *
     * @param cursor cursor
     * @return table metadata
     */
    private TableMetadata createTableMetadata(Cursor cursor) {
        TableMetadata metadata = new TableMetadata();
        metadata.setGeoPackageId(cursor.getLong(0));
        metadata.setTableName(cursor.getString(1));
        if (!cursor.isNull(2)) {
            metadata.setLastIndexed(cursor.getLong(2));
        }
        return metadata;
    }

}


package mil.nga.geopackage.extension;

import android.database.Cursor;

import org.sqlite.database.sqlite.SQLiteDatabase;

import java.util.List;

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.db.CoreSQLUtils;
import mil.nga.geopackage.db.GeoPackageDataType;
import mil.nga.geopackage.db.ResultUtils;
import mil.nga.geopackage.features.user.FeatureDao;
import mil.nga.geopackage.features.user.FeatureRow;
import mil.nga.geopackage.io.GeoPackageProgress;
import mil.nga.geopackage.user.custom.UserCustomCursor;
import mil.nga.geopackage.user.custom.UserCustomDao;
import mil.nga.geopackage.user.custom.UserCustomRow;
import mil.nga.sf.GeometryEnvelope;
import mil.nga.sf.proj.Projection;
import mil.nga.sf.proj.ProjectionTransform;

/**
 * RTree Index Table DAO for reading geometry index ranges
 *
 * @author osbornb
 * @since 3.1.0
 */
public class RTreeIndexTableDao extends UserCustomDao {

    /**
     * RTree index extension
     */
    private final RTreeIndexExtension rTree;

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

    /**
     * Progress
     */
    protected GeoPackageProgress progress;

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

    /**
     * SQLite Android Bindings connection
     */
    private final SQLiteDatabase database;

    /**
     * Constructor
     *
     * @param rTree      RTree extension
     * @param dao        user custom data access object
     * @param featureDao feature DAO
     */
    RTreeIndexTableDao(RTreeIndexExtension rTree, UserCustomDao dao,
                       FeatureDao featureDao) {
        super(dao, dao.getTable());
        this.rTree = rTree;
        this.featureDao = featureDao;
        this.projection = featureDao.getProjection();

        // Connect to the GeoPackage using the SQLite Android Bindings
        database = dao.getDatabaseConnection().openOrGetBindingsDb();
    }

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

    /**
     * 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;
    }

    /**
     * Determine if this feature table has the RTree extension
     *
     * @return true if has extension
     */
    public boolean has() {
        return rTree.has(featureDao.getTable());
    }

    /**
     * Create the RTree extension for the feature table
     *
     * @return extension
     */
    public Extensions create() {
        Extensions extension = null;
        if (!has()) {
            extension = rTree.create(featureDao.getTable());
            if (progress != null) {
                progress.addProgress(count());
            }
        }
        return extension;
    }

    /**
     * Delete the RTree extension for the feature table
     */
    public void delete() {
        rTree.delete(featureDao.getTable());
    }

    /**
     * Get the RTree index extension
     *
     * @return RTree index extension
     */
    public RTreeIndexExtension getRTreeIndexExtension() {
        return rTree;
    }

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

    /**
     * Get the RTree Index Table row from the current result set location
     *
     * @param cursor result cursor
     * @return RTree Index Table row
     */
    public RTreeIndexTableRow getRow(UserCustomCursor cursor) {
        return getRow(cursor.getRow());
    }

    /**
     * Get the RTree Index Table row from the user custom row
     *
     * @param row custom row
     * @return RTree Index Table row
     */
    public RTreeIndexTableRow getRow(UserCustomRow row) {
        return new RTreeIndexTableRow(row);
    }

    /**
     * Get the feature row from the RTree Index Table row
     *
     * @param row RTree Index Table row
     * @return feature row
     */
    public FeatureRow getFeatureRow(RTreeIndexTableRow row) {
        return featureDao.queryForIdRow(row.getId());
    }

    /**
     * Get the feature row from the RTree Index Table row
     *
     * @param cursor result cursor
     * @return feature row
     */
    public FeatureRow getFeatureRow(UserCustomCursor cursor) {
        RTreeIndexTableRow row = getRow(cursor);
        return getFeatureRow(row);
    }

    /**
     * Get the feature row from the user custom row
     *
     * @param row custom row
     * @return feature row
     */
    public FeatureRow getFeatureRow(UserCustomRow row) {
        return getFeatureRow(getRow(row));
    }

    /**
     * Perform a raw query
     *
     * @param sql           sql statement
     * @param selectionArgs selection arguments
     * @return result cursor
     */
    public UserCustomCursor rawQuery(String sql, String[] selectionArgs) {
        validateRTree();

        Cursor cursor = database.rawQuery(sql, selectionArgs);
        UserCustomCursor customCursor = new UserCustomCursor(getTable(), cursor);
        return customCursor;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public UserCustomCursor queryForAll() {
        return query(null, new String[]{});
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public UserCustomCursor query(String where, String[] whereArgs) {

        StringBuilder query = new StringBuilder();
        query.append("select * from ").append(CoreSQLUtils.quoteWrap(getTableName()));
        if (where != null) {
            query.append(" where ").append(where);
        }
        String sql = query.toString();

        UserCustomCursor customCursor = rawQuery(sql, whereArgs);

        return customCursor;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int count() {
        return count(null, new String[]{});
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int count(String where, String[] args) {

        int count = 0;

        StringBuilder countQuery = new StringBuilder();
        countQuery.append("select count(*) from ").append(CoreSQLUtils.quoteWrap(getTableName()));
        if (where != null) {
            countQuery.append(" where ").append(where);
        }
        String sql = countQuery.toString();

        UserCustomCursor customCursor = rawQuery(sql, args);

        Object value = ResultUtils.buildSingleResult(customCursor, 0, GeoPackageDataType.MEDIUMINT);
        if (value != null) {
            count = ((Number) value).intValue();
        }

        return count;
    }

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

        BoundingBox boundingBox = null;

        String sql = "SELECT MIN(" + RTreeIndexExtension.COLUMN_MIN_X + "), MIN("
                + RTreeIndexExtension.COLUMN_MIN_Y + "), MAX("
                + RTreeIndexExtension.COLUMN_MAX_X + "), MAX("
                + RTreeIndexExtension.COLUMN_MAX_Y + ") FROM "
                + CoreSQLUtils.quoteWrap(getTableName());
        UserCustomCursor customCursor = rawQuery(sql, null);

        List<List<Object>> results = ResultUtils.buildResults(customCursor,
                new GeoPackageDataType[]{
                        GeoPackageDataType.DOUBLE, GeoPackageDataType.DOUBLE, GeoPackageDataType.DOUBLE, GeoPackageDataType.DOUBLE},
                1);

        if (!results.isEmpty()) {
            List<Object> resultRow = results.get(0);
            boundingBox = new BoundingBox((Double) resultRow.get(0), (Double) resultRow.get(1),
                    (Double) resultRow.get(2), (Double) resultRow.get(3));
        }

        return boundingBox;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    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;
    }

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

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

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

    /**
     * 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 = projectBoundingBox(boundingBox,
                projection);
        return count(featureBoundingBox);
    }

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

    /**
     * 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());
    }

    /**
     * Query for rows within the bounds
     *
     * @param minX min x
     * @param minY min y
     * @param maxX max x
     * @param maxY max y
     * @return cursor results
     */
    public UserCustomCursor query(double minX, double minY, double maxX,
                                  double maxY) {
        String where = buildWhere(minX, minY, maxX, maxY);
        String[] whereArgs = buildWhereArgs(minX, minY, maxX, maxY);
        return query(where, whereArgs);
    }

    /**
     * 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) {
        String where = buildWhere(minX, minY, maxX, maxY);
        String[] whereArgs = buildWhereArgs(minX, minY, maxX, maxY);
        return count(where, whereArgs);
    }

    /**
     * Validate that the RTree extension exists for the table and column
     */
    private void validateRTree() {
        if (!has()) {
            throw new GeoPackageException(
                    "RTree Extension not found for feature table: "
                            + featureDao.getTableName());
        }
    }

    /**
     * Build a where clause from the bounds for overlapping ranges
     *
     * @param minX min x
     * @param minY min y
     * @param maxX max x
     * @param maxY max y
     * @return where clause
     */
    private String buildWhere(double minX, double minY, double maxX, double maxY) {

        StringBuilder where = new StringBuilder();
        where.append(buildWhere(RTreeIndexExtension.COLUMN_MIN_X, maxX, "<="));
        where.append(" AND ");
        where.append(buildWhere(RTreeIndexExtension.COLUMN_MIN_Y, maxY, "<="));
        where.append(" AND ");
        where.append(buildWhere(RTreeIndexExtension.COLUMN_MAX_X, minX, ">="));
        where.append(" AND ");
        where.append(buildWhere(RTreeIndexExtension.COLUMN_MAX_Y, minY, ">="));

        return where.toString();
    }

    /**
     * Build where arguments from the bounds to match the order in
     * {@link #buildWhereArgs(double, double, double, double)}
     *
     * @param minX min x
     * @param minY min y
     * @param maxX max x
     * @param maxY max y
     * @return where clause args
     */
    private String[] buildWhereArgs(double minX, double minY, double maxX,
                                    double maxY) {

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

        return buildWhereArgs(new Object[]{maxX, maxY, minX, minY});
    }

}