package mil.nga.geopackage.factory;

import android.content.Context;
import android.database.Cursor;

import com.j256.ormlite.stmt.PreparedQuery;
import com.j256.ormlite.stmt.QueryBuilder;

import java.sql.SQLException;
import java.util.List;

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.GeoPackage;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.attributes.AttributesConnection;
import mil.nga.geopackage.attributes.AttributesCursor;
import mil.nga.geopackage.attributes.AttributesDao;
import mil.nga.geopackage.attributes.AttributesTable;
import mil.nga.geopackage.attributes.AttributesTableReader;
import mil.nga.geopackage.attributes.AttributesWrapperConnection;
import mil.nga.geopackage.core.contents.Contents;
import mil.nga.geopackage.core.contents.ContentsDao;
import mil.nga.geopackage.core.contents.ContentsDataType;
import mil.nga.geopackage.db.GeoPackageConnection;
import mil.nga.geopackage.db.GeoPackageTableCreator;
import mil.nga.geopackage.extension.RTreeIndexExtension;
import mil.nga.geopackage.features.columns.GeometryColumns;
import mil.nga.geopackage.features.columns.GeometryColumnsDao;
import mil.nga.geopackage.features.index.FeatureIndexManager;
import mil.nga.geopackage.features.user.FeatureConnection;
import mil.nga.geopackage.features.user.FeatureCursor;
import mil.nga.geopackage.features.user.FeatureDao;
import mil.nga.geopackage.features.user.FeatureTable;
import mil.nga.geopackage.features.user.FeatureTableReader;
import mil.nga.geopackage.features.user.FeatureWrapperConnection;
import mil.nga.geopackage.tiles.matrix.TileMatrix;
import mil.nga.geopackage.tiles.matrix.TileMatrixDao;
import mil.nga.geopackage.tiles.matrix.TileMatrixKey;
import mil.nga.geopackage.tiles.matrixset.TileMatrixSet;
import mil.nga.geopackage.tiles.matrixset.TileMatrixSetDao;
import mil.nga.geopackage.tiles.user.TileConnection;
import mil.nga.geopackage.tiles.user.TileCursor;
import mil.nga.geopackage.tiles.user.TileDao;
import mil.nga.geopackage.tiles.user.TileTable;
import mil.nga.geopackage.tiles.user.TileTableReader;
import mil.nga.geopackage.tiles.user.TileWrapperConnection;
import mil.nga.sf.proj.Projection;

/**
 * A single GeoPackage database connection implementation
 *
 * @author osbornb
 */
class GeoPackageImpl extends GeoPackageCoreImpl implements GeoPackage {

    /**
     * Context
     */
    private final Context context;

    /**
     * Database connection
     */
    private final GeoPackageConnection database;

    /**
     * Cursor factory
     */
    private final GeoPackageCursorFactory cursorFactory;

    /**
     * Constructor
     *
     * @param context       context
     * @param name          GeoPackage name
     * @param path          database path
     * @param database      database connection
     * @param cursorFactory cursor factory
     * @param tableCreator  table creator
     * @param writable      writable flag
     */
    GeoPackageImpl(Context context, String name, String path, GeoPackageConnection database,
                   GeoPackageCursorFactory cursorFactory,
                   GeoPackageTableCreator tableCreator, boolean writable) {
        super(name, path, database, tableCreator, writable);
        this.context = context;
        this.database = database;
        this.cursorFactory = cursorFactory;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected BoundingBox getFeatureBoundingBox(Projection projection,
                                                String table, boolean manual) {

        BoundingBox boundingBox = null;

        FeatureIndexManager indexManager = new FeatureIndexManager(context, this, table);
        try {
            if (manual || indexManager.isIndexed()) {
                boundingBox = indexManager.getBoundingBox(projection);
            }
        } finally {
            indexManager.close();
        }

        return boundingBox;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void registerCursorWrapper(String table, GeoPackageCursorWrapper cursorWrapper) {
        cursorFactory.registerTable(table,
                cursorWrapper);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FeatureDao getFeatureDao(GeometryColumns geometryColumns) {

        if (geometryColumns == null) {
            throw new GeoPackageException("Non null "
                    + GeometryColumns.class.getSimpleName()
                    + " is required to create "
                    + FeatureDao.class.getSimpleName());
        }

        // Read the existing table and create the dao
        FeatureTableReader tableReader = new FeatureTableReader(geometryColumns);
        final FeatureTable featureTable = tableReader.readTable(new FeatureWrapperConnection(database));
        featureTable.setContents(geometryColumns.getContents());
        FeatureConnection userDb = new FeatureConnection(database);
        FeatureDao dao = new FeatureDao(getName(), database, userDb, geometryColumns, featureTable);

        // Register the table name (with and without quotes) to wrap cursors with the feature cursor
        registerCursorWrapper(geometryColumns.getTableName(),
                new GeoPackageCursorWrapper() {

                    @Override
                    public Cursor wrapCursor(Cursor cursor) {
                        return new FeatureCursor(featureTable, cursor);
                    }
                });

        // If the GeoPackage is writable and the feature table has a RTree Index
        // extension, drop the RTree triggers.  User defined functions are currently not supported.
        if (writable) {
            RTreeIndexExtension rtree = new RTreeIndexExtension(this);
            rtree.dropTriggers(featureTable);
        }

        return dao;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FeatureDao getFeatureDao(Contents contents) {

        if (contents == null) {
            throw new GeoPackageException("Non null "
                    + Contents.class.getSimpleName()
                    + " is required to create "
                    + FeatureDao.class.getSimpleName());
        }

        GeometryColumns geometryColumns = contents.getGeometryColumns();
        if (geometryColumns == null) {
            throw new GeoPackageException("No "
                    + GeometryColumns.class.getSimpleName() + " exists for "
                    + Contents.class.getSimpleName() + " " + contents.getId());
        }

        return getFeatureDao(geometryColumns);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FeatureDao getFeatureDao(String tableName) {
        GeometryColumnsDao dao = getGeometryColumnsDao();
        List<GeometryColumns> geometryColumnsList;
        try {
            geometryColumnsList = dao.queryForEq(
                    GeometryColumns.COLUMN_TABLE_NAME, tableName);
        } catch (SQLException e) {
            throw new GeoPackageException("Failed to retrieve "
                    + FeatureDao.class.getSimpleName() + " for table name: "
                    + tableName + ". Exception retrieving "
                    + GeometryColumns.class.getSimpleName() + ".", e);
        }
        if (geometryColumnsList.isEmpty()) {
            throw new GeoPackageException(
                    "No Feature Table exists for table name: " + tableName);
        } else if (geometryColumnsList.size() > 1) {
            // This shouldn't happen with the table name unique constraint on
            // geometry columns
            throw new GeoPackageException("Unexpected state. More than one "
                    + GeometryColumns.class.getSimpleName()
                    + " matched for table name: " + tableName + ", count: "
                    + geometryColumnsList.size());
        }
        return getFeatureDao(geometryColumnsList.get(0));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TileDao getTileDao(TileMatrixSet tileMatrixSet) {

        if (tileMatrixSet == null) {
            throw new GeoPackageException("Non null "
                    + TileMatrixSet.class.getSimpleName()
                    + " is required to create " + TileDao.class.getSimpleName());
        }

        // Get the Tile Matrix collection, order by zoom level ascending & pixel
        // size descending per requirement 51
        List<TileMatrix> tileMatrices;
        try {
            TileMatrixDao tileMatrixDao = getTileMatrixDao();
            QueryBuilder<TileMatrix, TileMatrixKey> qb = tileMatrixDao
                    .queryBuilder();
            qb.where().eq(TileMatrix.COLUMN_TABLE_NAME,
                    tileMatrixSet.getTableName());
            qb.orderBy(TileMatrix.COLUMN_ZOOM_LEVEL, true);
            qb.orderBy(TileMatrix.COLUMN_PIXEL_X_SIZE, false);
            qb.orderBy(TileMatrix.COLUMN_PIXEL_Y_SIZE, false);
            PreparedQuery<TileMatrix> query = qb.prepare();
            tileMatrices = tileMatrixDao.query(query);
        } catch (SQLException e) {
            throw new GeoPackageException("Failed to retrieve "
                    + TileDao.class.getSimpleName() + " for table name: "
                    + tileMatrixSet.getTableName() + ". Exception retrieving "
                    + TileMatrix.class.getSimpleName() + " collection.", e);
        }

        // Read the existing table and create the dao
        TileTableReader tableReader = new TileTableReader(
                tileMatrixSet.getTableName());
        final TileTable tileTable = tableReader.readTable(new TileWrapperConnection(database));
        tileTable.setContents(tileMatrixSet.getContents());
        TileConnection userDb = new TileConnection(database);
        TileDao dao = new TileDao(getName(), database, userDb, tileMatrixSet, tileMatrices,
                tileTable);

        // Register the table name (with and without quotes) to wrap cursors with the tile cursor
        registerCursorWrapper(tileMatrixSet.getTableName(),
                new GeoPackageCursorWrapper() {

                    @Override
                    public Cursor wrapCursor(Cursor cursor) {
                        return new TileCursor(tileTable, cursor);
                    }
                });

        return dao;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TileDao getTileDao(Contents contents) {

        if (contents == null) {
            throw new GeoPackageException("Non null "
                    + Contents.class.getSimpleName()
                    + " is required to create " + TileDao.class.getSimpleName());
        }

        TileMatrixSet tileMatrixSet = contents.getTileMatrixSet();
        if (tileMatrixSet == null) {
            throw new GeoPackageException("No "
                    + TileMatrixSet.class.getSimpleName() + " exists for "
                    + Contents.class.getSimpleName() + " " + contents.getId());
        }

        return getTileDao(tileMatrixSet);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TileDao getTileDao(String tableName) {

        TileMatrixSetDao dao = getTileMatrixSetDao();
        List<TileMatrixSet> tileMatrixSetList;
        try {
            tileMatrixSetList = dao.queryForEq(TileMatrixSet.COLUMN_TABLE_NAME,
                    tableName);
        } catch (SQLException e) {
            throw new GeoPackageException("Failed to retrieve "
                    + TileDao.class.getSimpleName() + " for table name: "
                    + tableName + ". Exception retrieving "
                    + TileMatrixSet.class.getSimpleName() + ".", e);
        }
        if (tileMatrixSetList.isEmpty()) {
            throw new GeoPackageException(
                    "No Tile Table exists for table name: " + tableName);
        } else if (tileMatrixSetList.size() > 1) {
            // This shouldn't happen with the table name primary key on tile
            // matrix set table
            throw new GeoPackageException("Unexpected state. More than one "
                    + TileMatrixSet.class.getSimpleName()
                    + " matched for table name: " + tableName + ", count: "
                    + tileMatrixSetList.size());
        }
        return getTileDao(tileMatrixSetList.get(0));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public AttributesDao getAttributesDao(Contents contents) {

        if (contents == null) {
            throw new GeoPackageException("Non null "
                    + Contents.class.getSimpleName()
                    + " is required to create "
                    + AttributesDao.class.getSimpleName());
        }
        if (contents.getDataType() != ContentsDataType.ATTRIBUTES) {
            throw new GeoPackageException(Contents.class.getSimpleName()
                    + " is required to be of type "
                    + ContentsDataType.ATTRIBUTES + ". Actual: "
                    + contents.getDataTypeString());
        }

        // Read the existing table and create the dao
        AttributesTableReader tableReader = new AttributesTableReader(
                contents.getTableName());
        final AttributesTable attributesTable = tableReader.readTable(new AttributesWrapperConnection(database));
        attributesTable.setContents(contents);
        AttributesConnection userDb = new AttributesConnection(database);
        AttributesDao dao = new AttributesDao(getName(), database, userDb,
                attributesTable);

        // Register the table name (with and without quotes) to wrap cursors with the attributes cursor
        registerCursorWrapper(attributesTable.getTableName(),
                new GeoPackageCursorWrapper() {

                    @Override
                    public Cursor wrapCursor(Cursor cursor) {
                        return new AttributesCursor(attributesTable, cursor);
                    }
                });

        return dao;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public AttributesDao getAttributesDao(String tableName) {

        ContentsDao dao = getContentsDao();
        Contents contents = null;
        try {
            contents = dao.queryForId(tableName);
        } catch (SQLException e) {
            throw new GeoPackageException("Failed to retrieve "
                    + Contents.class.getSimpleName() + " for table name: "
                    + tableName, e);
        }
        if (contents == null) {
            throw new GeoPackageException(
                    "No Contents Table exists for table name: " + tableName);
        }
        return getAttributesDao(contents);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void execSQL(String sql) {
        database.execSQL(sql);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Cursor rawQuery(String sql, String[] args) {
        return database.rawQuery(sql, args);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public GeoPackageConnection getConnection() {
        return database;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Context getContext() {
        return context;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Cursor foreignKeyCheck() {
        Cursor cursor = rawQuery("PRAGMA foreign_key_check", null);
        if (!cursor.moveToNext()) {
            cursor.close();
            cursor = null;
        }
        return cursor;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Cursor integrityCheck() {
        return integrityCheck(rawQuery("PRAGMA integrity_check", null));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Cursor quickCheck() {
        return integrityCheck(rawQuery("PRAGMA quick_check", null));
    }

    /**
     * Check the cursor returned from the integrity check to see if things are "ok"
     *
     * @param cursor
     * @return null if ok, else the open cursor
     */
    private Cursor integrityCheck(Cursor cursor) {
        if (cursor.moveToNext()) {
            String value = cursor.getString(0);
            if (value.equals("ok")) {
                cursor.close();
                cursor = null;
            }
        }
        return cursor;
    }

}


package mil.nga.geopackage.extension.related;

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

import mil.nga.geopackage.GeoPackage;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.db.GeoPackageConnection;
import mil.nga.geopackage.extension.related.media.MediaDao;
import mil.nga.geopackage.extension.related.media.MediaTable;
import mil.nga.geopackage.extension.related.simple.SimpleAttributesDao;
import mil.nga.geopackage.extension.related.simple.SimpleAttributesTable;
import mil.nga.geopackage.user.custom.UserCustomColumn;
import mil.nga.geopackage.user.custom.UserCustomCursor;
import mil.nga.geopackage.user.custom.UserCustomDao;
import mil.nga.geopackage.user.custom.UserCustomTable;
import mil.nga.geopackage.user.custom.UserCustomTableReader;

/**
 * Related Tables extension
 *
 * @author osbornb
 * @since 3.0.1
 */
public class RelatedTablesExtension extends RelatedTablesCoreExtension {

    /**
     * GeoPackage connection
     */
    private GeoPackageConnection connection;

    /**
     * Constructor
     *
     * @param geoPackage GeoPackage
     */
    public RelatedTablesExtension(GeoPackage geoPackage) {
        super(geoPackage);
        connection = geoPackage.getConnection();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public GeoPackage getGeoPackage() {
        return (GeoPackage) super.getGeoPackage();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getPrimaryKeyColumnName(String tableName) {
        UserCustomTable table = UserCustomTableReader.readTable(connection,
                tableName);
        UserCustomColumn pkColumn = table.getPkColumn();
        if (pkColumn == null) {
            throw new GeoPackageException("Found no primary key for table "
                    + tableName);
        }
        return pkColumn.getName();
    }

    /**
     * Get a User Custom DAO from a table name
     *
     * @param tableName table name
     * @return user custom dao
     */
    public UserCustomDao getUserDao(String tableName) {
        return UserCustomDao.readTable(getGeoPackage(),
                tableName);
    }

    /**
     * Get a User Mapping DAO from an extended relation
     *
     * @param extendedRelation extended relation
     * @return user mapping dao
     */
    public UserMappingDao getMappingDao(ExtendedRelation extendedRelation) {
        return getMappingDao(extendedRelation.getMappingTableName());
    }

    /**
     * Get a User Mapping DAO from a table name
     *
     * @param tableName mapping table name
     * @return user mapping dao
     */
    public UserMappingDao getMappingDao(String tableName) {
        UserMappingDao userMappingDao = new UserMappingDao(getUserDao(tableName));
        userMappingDao.registerCursorWrapper(getGeoPackage());
        return userMappingDao;
    }

    /**
     * Get a related media table DAO
     *
     * @param mediaTable media table
     * @return media DAO
     */
    public MediaDao getMediaDao(MediaTable mediaTable) {
        return getMediaDao(mediaTable.getTableName());
    }

    /**
     * Get a related media table DAO
     *
     * @param extendedRelation extended relation
     * @return media DAO
     */
    public MediaDao getMediaDao(ExtendedRelation extendedRelation) {
        return getMediaDao(extendedRelation.getRelatedTableName());
    }

    /**
     * Get a related media table DAO
     *
     * @param tableName media table name
     * @return media DAO
     */
    public MediaDao getMediaDao(String tableName) {
        MediaDao mediaDao = new MediaDao(getUserDao(tableName));
        mediaDao.registerCursorWrapper(getGeoPackage());
        setContents(mediaDao.getTable());
        return mediaDao;
    }

    /**
     * Get a related simple attributes table DAO
     *
     * @param simpleAttributesTable simple attributes table
     * @return simple attributes DAO
     */
    public SimpleAttributesDao getSimpleAttributesDao(
            SimpleAttributesTable simpleAttributesTable) {
        return getSimpleAttributesDao(simpleAttributesTable.getTableName());
    }

    /**
     * Get a related simple attributes table DAO
     *
     * @param extendedRelation extended relation
     * @return simple attributes DAO
     */
    public SimpleAttributesDao getSimpleAttributesDao(
            ExtendedRelation extendedRelation) {
        return getSimpleAttributesDao(extendedRelation.getRelatedTableName());
    }

    /**
     * Get a related simple attributes table DAO
     *
     * @param tableName simple attributes table name
     * @return simple attributes DAO
     */
    public SimpleAttributesDao getSimpleAttributesDao(String tableName) {
        SimpleAttributesDao simpleAttributesDao = new SimpleAttributesDao(
                getUserDao(tableName));
        simpleAttributesDao.registerCursorWrapper(getGeoPackage());
        setContents(simpleAttributesDao.getTable());
        return simpleAttributesDao;
    }

    /**
     * Get the related id mappings for the base id
     *
     * @param extendedRelation extended relation
     * @param baseId           base id
     * @return IDs representing the matching related IDs
     */
    public List<Long> getMappingsForBase(ExtendedRelation extendedRelation,
                                         long baseId) {
        return getMappingsForBase(extendedRelation.getMappingTableName(),
                baseId);
    }

    /**
     * Get the related id mappings for the base id
     *
     * @param tableName mapping table name
     * @param baseId    base id
     * @return IDs representing the matching related IDs
     */
    public List<Long> getMappingsForBase(String tableName, long baseId) {

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

        UserMappingDao userMappingDao = getMappingDao(tableName);
        UserCustomCursor cursor = userMappingDao.queryByBaseId(baseId);
        try {
            while (cursor.moveToNext()) {
                UserMappingRow row = userMappingDao.getRow(cursor);
                relatedIds.add(row.getRelatedId());
            }
        } finally {
            cursor.close();
        }

        return relatedIds;
    }

    /**
     * Get the base id mappings for the related id
     *
     * @param extendedRelation extended relation
     * @param relatedId        related id
     * @return IDs representing the matching base IDs
     */
    public List<Long> getMappingsForRelated(ExtendedRelation extendedRelation,
                                            long relatedId) {
        return getMappingsForRelated(extendedRelation.getMappingTableName(),
                relatedId);
    }

    /**
     * Get the base id mappings for the related id
     *
     * @param tableName mapping table name
     * @param relatedId related id
     * @return IDs representing the matching base IDs
     */
    public List<Long> getMappingsForRelated(String tableName, long relatedId) {

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

        UserMappingDao userMappingDao = getMappingDao(tableName);
        UserCustomCursor cursor = userMappingDao
                .queryByRelatedId(relatedId);
        try {
            while (cursor.moveToNext()) {
                UserMappingRow row = userMappingDao.getRow(cursor);
                baseIds.add(row.getBaseId());
            }
        } finally {
            cursor.close();
        }

        return baseIds;
    }

    /**
     * Determine if the base id and related id mapping exists
     *
     * @param tableName mapping table name
     * @param baseId    base id
     * @param relatedId related id
     * @return true if mapping exists
     * @since 3.2.0
     */
    public boolean hasMapping(String tableName, long baseId, long relatedId) {
        boolean has = false;
        UserMappingDao userMappingDao = getMappingDao(tableName);
        UserCustomCursor cursor = userMappingDao.queryByIds(baseId,
                relatedId);
        try {
            has = cursor.getCount() > 0;
        } finally {
            cursor.close();
        }
        return has;
    }

}


package mil.nga.geopackage.tiles.user;

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;

/**
 * Tile Cursor to wrap a database cursor for tile queries
 *
 * @author osbornb
 */
public class TileCursor extends UserCursor<TileColumn, TileTable, TileRow> {

    /**
     * Constructor
     *
     * @param table  tile table
     * @param cursor cursor
     */
    public TileCursor(TileTable table, Cursor cursor) {
        super(table, cursor);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TileRow getRow(int[] columnTypes, Object[] values) {
        return new TileRow(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
     * @since 2.0.0
     */
    public void enableInvalidRequery(TileDao dao) {
        super.enableInvalidRequery(dao);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected UserInvalidCursor<TileColumn, TileTable, TileRow, ? extends UserCursor<TileColumn, TileTable, TileRow>, ? extends UserDao<TileColumn, TileTable, TileRow, ? extends UserCursor<TileColumn, TileTable, TileRow>>> createInvalidCursor(UserDao dao, UserCursor cursor, List<Integer> invalidPositions, List<TileColumn> blobColumns) {
        return new TileInvalidCursor((TileDao) dao, (TileCursor) cursor, invalidPositions, blobColumns);
    }

}


package mil.nga.geopackage.user.custom;

import android.database.Cursor;

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.GeoPackage;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.db.GeoPackageConnection;
import mil.nga.geopackage.factory.GeoPackageCursorWrapper;
import mil.nga.geopackage.user.UserDao;
import mil.nga.sf.proj.Projection;

/**
 * User Custom DAO for reading user custom data tables
 *
 * @author osbornb
 * @since 3.0.1
 */
public class UserCustomDao
        extends
        UserDao<UserCustomColumn, UserCustomTable, UserCustomRow, UserCustomCursor> {

    /**
     * User Custom connection
     */
    protected final UserCustomConnection userDb;

    /**
     * Constructor
     *
     * @param database database name
     * @param db       database connection
     * @param userDb   user connection
     * @param table    user custom table
     */
    public UserCustomDao(String database, GeoPackageConnection db,
                         UserCustomConnection userDb, UserCustomTable table) {
        super(database, db, userDb, table);

        this.userDb = userDb;
    }

    /**
     * Constructor
     *
     * @param dao user custom data access object
     */
    public UserCustomDao(UserCustomDao dao) {
        this(dao, dao.getTable());
    }

    /**
     * Constructor
     *
     * @param dao             user custom data access object
     * @param userCustomTable user custom table
     */
    public UserCustomDao(UserCustomDao dao, UserCustomTable userCustomTable) {
        this(dao.getDatabase(), dao.getDb(), dao.getUserDb(), userCustomTable);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BoundingBox getBoundingBox() {
        throw new GeoPackageException(
                "Bounding Box not supported for User Custom");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BoundingBox getBoundingBox(Projection projection) {
        throw new GeoPackageException(
                "Bounding Box not supported for User Custom");
    }

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

    /**
     * Get the User Custom connection
     *
     * @return user custom connection
     */
    public UserCustomConnection getUserDb() {
        return userDb;
    }

    /**
     * Get the count of the cursor and close it
     *
     * @param cursor cursor
     * @return count
     */
    protected int count(UserCustomCursor cursor) {
        int count = 0;
        try {
            count = cursor.getCount();
        } finally {
            cursor.close();
        }
        return count;
    }

    /**
     * Register the cursor wrapper into the GeoPackage
     *
     * @param geoPackage GeoPackage
     */
    public void registerCursorWrapper(GeoPackage geoPackage) {
        geoPackage.registerCursorWrapper(getTableName(), new GeoPackageCursorWrapper() {
            @Override
            public Cursor wrapCursor(Cursor cursor) {
                return new UserCustomCursor(getTable(), cursor);
            }
        });
    }

    /**
     * Read the database table and create a DAO
     *
     * @param geoPackage GeoPackage
     * @param tableName  table name
     * @return user custom DAO
     */
    public static UserCustomDao readTable(GeoPackage geoPackage, String tableName) {

        UserCustomConnection userDb = new UserCustomConnection(geoPackage.getConnection());
        final UserCustomTable userCustomTable = UserCustomTableReader.readTable(
                geoPackage.getConnection(), tableName);
        UserCustomDao dao = new UserCustomDao(geoPackage.getName(), geoPackage.getConnection(), userDb,
                userCustomTable);

        dao.registerCursorWrapper(geoPackage);

        return dao;
    }

}


package mil.nga.geopackage.tiles.retriever;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.Log;

import org.locationtech.proj4j.ProjCoordinate;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.extension.scale.TileScaling;
import mil.nga.geopackage.extension.scale.TileScalingType;
import mil.nga.geopackage.io.BitmapConverter;
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.sf.proj.Projection;
import mil.nga.sf.proj.ProjectionTransform;

/**
 * Tile Creator, creates a tile from a tile matrix to the desired projection
 *
 * @author osbornb
 * @since 1.3.0
 */
public class TileCreator {

    /**
     * Compress format
     */
    private static final Bitmap.CompressFormat COMPRESS_FORMAT = Bitmap.CompressFormat.PNG;

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

    /**
     * Tile width
     */
    private final Integer width;

    /**
     * Tile height
     */
    private final Integer height;

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

    /**
     * Projection of the requests
     */
    private final Projection requestProjection;

    /**
     * Projection of the tiles
     */
    private final Projection tilesProjection;

    /**
     * Tile Set bounding box
     */
    private final BoundingBox tileSetBoundingBox;

    /**
     * Flag indicating the the tile and request projections are the same
     */
    private final boolean sameProjection;

    /**
     * Tile Scaling options
     */
    private TileScaling scaling;

    /**
     * Constructor, specified tile size and projection
     *
     * @param tileDao           tile dao
     * @param width             requested width
     * @param height            requested height
     * @param requestProjection requested projection
     */
    public TileCreator(TileDao tileDao, Integer width, Integer height, Projection requestProjection) {
        this.tileDao = tileDao;
        this.width = width;
        this.height = height;
        this.requestProjection = requestProjection;

        tileMatrixSet = tileDao.getTileMatrixSet();
        tilesProjection = tileDao.getTileMatrixSet().getProjection();
        tileSetBoundingBox = tileMatrixSet.getBoundingBox();

        // Check if the projections have the same units
        sameProjection = (requestProjection.getUnit().name.equals(tilesProjection.getUnit().name));
    }

    /**
     * Constructor, tile tables tile size and projection
     *
     * @param tileDao tile dao
     */
    public TileCreator(TileDao tileDao) {
        this(tileDao, null, null, tileDao.getProjection());
    }

    /**
     * Constructor, tile tables projection with specified tile size
     *
     * @param tileDao tile dao
     * @param width   requested width
     * @param height  requested height
     */
    public TileCreator(TileDao tileDao, Integer width, Integer height) {
        this(tileDao, width, height, tileDao.getProjection());
    }

    /**
     * Constructor, tile tables tile size and requested projection
     *
     * @param tileDao           tile dao
     * @param requestProjection requested projection
     */
    public TileCreator(TileDao tileDao, Projection requestProjection) {
        this(tileDao, null, null, requestProjection);
    }

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

    /**
     * Get the requested tile width
     *
     * @return width
     */
    public Integer getWidth() {
        return width;
    }

    /**
     * Get the requested tile height
     *
     * @return height
     */
    public Integer getHeight() {
        return height;
    }

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

    /**
     * Get the request projection
     *
     * @return request projection
     */
    public Projection getRequestProjection() {
        return requestProjection;
    }

    /**
     * Get the tiles projection
     *
     * @return tiles projection
     */
    public Projection getTilesProjection() {
        return tilesProjection;
    }

    /**
     * Get the tile set bounding box
     *
     * @return tile set bounding box
     */
    public BoundingBox getTileSetBoundingBox() {
        return tileSetBoundingBox;
    }

    /**
     * Is the request and tile projection the same
     *
     * @return true if the same
     */
    public boolean isSameProjection() {
        return sameProjection;
    }

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

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

    /**
     * Check if the tile table contains a tile for the request bounding box
     *
     * @param requestBoundingBox request bounding box in the request projection
     * @return true if a tile exists
     */
    public boolean hasTile(BoundingBox requestBoundingBox) {

        boolean hasTile = false;

        // Transform to the projection of the tiles
        ProjectionTransform transformRequestToTiles = requestProjection.getTransformation(tilesProjection);
        BoundingBox tilesBoundingBox = requestBoundingBox.transform(transformRequestToTiles);

        List<TileMatrix> tileMatrices = getTileMatrices(tilesBoundingBox);

        for (int i = 0; !hasTile && i < tileMatrices.size(); i++) {

            TileMatrix tileMatrix = tileMatrices.get(i);

            TileCursor tileResults = retrieveTileResults(tilesBoundingBox, tileMatrix);
            if (tileResults != null) {

                try {
                    hasTile = tileResults.getCount() > 0;
                } finally {
                    tileResults.close();
                }
            }
        }

        return hasTile;
    }

    /**
     * Get the tile from the request bounding box in the request projection
     *
     * @param requestBoundingBox request bounding box in the request projection
     * @return tile
     */
    public GeoPackageTile getTile(BoundingBox requestBoundingBox) {

        GeoPackageTile tile = null;

        // Transform to the projection of the tiles
        ProjectionTransform transformRequestToTiles = requestProjection.getTransformation(tilesProjection);
        BoundingBox tilesBoundingBox = requestBoundingBox.transform(transformRequestToTiles);

        List<TileMatrix> tileMatrices = getTileMatrices(tilesBoundingBox);

        for (int i = 0; tile == null && i < tileMatrices.size(); i++) {

            TileMatrix tileMatrix = tileMatrices.get(i);

            TileCursor tileResults = retrieveTileResults(tilesBoundingBox, tileMatrix);
            if (tileResults != null) {

                try {

                    if (tileResults.getCount() > 0) {

                        BoundingBox requestProjectedBoundingBox = requestBoundingBox.transform(transformRequestToTiles);

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

                        // Determine the size of the tile to initially draw
                        int tileWidth = requestedTileWidth;
                        int tileHeight = requestedTileHeight;
                        if (!sameProjection) {
                            tileWidth = (int) Math.round(
                                    (requestProjectedBoundingBox.getMaxLongitude() - requestProjectedBoundingBox.getMinLongitude())
                                            / tileMatrix.getPixelXSize());
                            tileHeight = (int) Math.round(
                                    (requestProjectedBoundingBox.getMaxLatitude() - requestProjectedBoundingBox.getMinLatitude())
                                            / tileMatrix.getPixelYSize());
                        }

                        // Draw the resulting bitmap with the matching tiles
                        Bitmap tileBitmap = drawTile(tileMatrix, tileResults, requestProjectedBoundingBox, tileWidth, tileHeight);

                        // Create the tile
                        if (tileBitmap != null) {

                            // Project the tile if needed
                            if (!sameProjection) {
                                Bitmap reprojectTile = reprojectTile(tileBitmap, requestedTileWidth, requestedTileHeight, requestBoundingBox, transformRequestToTiles, tilesBoundingBox);
                                tileBitmap.recycle();
                                tileBitmap = reprojectTile;
                            }

                            try {
                                byte[] tileData = BitmapConverter.toBytes(
                                        tileBitmap, COMPRESS_FORMAT);
                                tileBitmap.recycle();
                                tile = new GeoPackageTile(requestedTileWidth, requestedTileHeight, tileData);
                            } catch (IOException e) {
                                Log.e(TileCreator.class.getSimpleName(), "Failed to create tile. min lat: "
                                        + requestBoundingBox.getMinLatitude()
                                        + ", max lat: " + requestBoundingBox.getMaxLatitude()
                                        + ", min lon: " + requestBoundingBox.getMinLongitude() +
                                        ", max lon: " + requestBoundingBox.getMaxLongitude(), e);
                            }
                        }

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

        return tile;
    }

    /**
     * Draw the tile from the tile results
     *
     * @param tileMatrix
     * @param tileResults
     * @param requestProjectedBoundingBox
     * @param tileWidth
     * @param tileHeight
     * @return tile bitmap
     */
    private Bitmap drawTile(TileMatrix tileMatrix, TileCursor tileResults, BoundingBox requestProjectedBoundingBox, int tileWidth, int tileHeight) {

        // Draw the resulting bitmap with the matching tiles
        Bitmap tileBitmap = null;
        Canvas canvas = null;
        Paint paint = null;
        while (tileResults.moveToNext()) {

            // Get the next tile
            TileRow tileRow = tileResults.getRow();
            Bitmap tileDataBitmap = tileRow.getTileDataBitmap();

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

            // Get the bounding box where the requested image and
            // tile overlap
            BoundingBox overlap = requestProjectedBoundingBox.overlap(
                    tileBoundingBox);

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

                // Get the rectangle of the tile image to draw
                Rect src = TileBoundingBoxAndroidUtils
                        .getRectangle(tileMatrix.getTileWidth(),
                                tileMatrix.getTileHeight(),
                                tileBoundingBox, overlap);

                // Get the rectangle of where to draw the tile in
                // the resulting image
                RectF dest = TileBoundingBoxAndroidUtils
                        .getRoundedFloatRectangle(tileWidth, tileHeight,
                                requestProjectedBoundingBox, overlap);

                // Create the bitmap first time through
                if (tileBitmap == null) {
                    tileBitmap = Bitmap.createBitmap(tileWidth,
                            tileHeight, Bitmap.Config.ARGB_8888);
                    canvas = new Canvas(tileBitmap);
                    paint = new Paint(Paint.ANTI_ALIAS_FLAG);
                }

                // Draw the tile to the bitmap
                canvas.drawBitmap(tileDataBitmap, src, dest, paint);
            }
        }

        return tileBitmap;
    }

    /**
     * Reproject the tile to the requested projection
     *
     * @param tile                    tile in the tile matrix projection
     * @param requestedTileWidth      requested tile width
     * @param requestedTileHeight     requested tile height
     * @param requestBoundingBox      request bounding box in the request projection
     * @param transformRequestToTiles transformation from request to tiles
     * @param tilesBoundingBox        request bounding box in the tile matrix projection
     * @return projected tile
     */
    private Bitmap reprojectTile(Bitmap tile, int requestedTileWidth, int requestedTileHeight, BoundingBox requestBoundingBox, ProjectionTransform transformRequestToTiles, BoundingBox tilesBoundingBox) {

        final double requestedWidthUnitsPerPixel = (requestBoundingBox.getMaxLongitude() - requestBoundingBox.getMinLongitude()) / requestedTileWidth;
        final double requestedHeightUnitsPerPixel = (requestBoundingBox.getMaxLatitude() - requestBoundingBox.getMinLatitude()) / requestedTileHeight;

        final double tilesDistanceWidth = tilesBoundingBox.getMaxLongitude() - tilesBoundingBox.getMinLongitude();
        final double tilesDistanceHeight = tilesBoundingBox.getMaxLatitude() - tilesBoundingBox.getMinLatitude();

        final int width = tile.getWidth();
        final int height = tile.getHeight();

        // Tile pixels of the tile matrix tiles
        int[] pixels = new int[width * height];
        tile.getPixels(pixels, 0, width, 0, 0, width, height);

        // Projected tile pixels to draw the reprojected tile
        int[] projectedPixels = new int[requestedTileWidth * requestedTileHeight];

        // Retrieve each pixel in the new tile from the unprojected tile
        for (int y = 0; y < requestedTileHeight; y++) {
            for (int x = 0; x < requestedTileWidth; x++) {

                double longitude = requestBoundingBox.getMinLongitude() + (x * requestedWidthUnitsPerPixel);
                double latitude = requestBoundingBox.getMaxLatitude() - (y * requestedHeightUnitsPerPixel);
                ProjCoordinate fromCoord = new ProjCoordinate(longitude, latitude);
                ProjCoordinate toCoord = transformRequestToTiles.transform(fromCoord);
                double projectedLongitude = toCoord.x;
                double projectedLatitude = toCoord.y;

                int xPixel = (int) Math.round(((projectedLongitude - tilesBoundingBox.getMinLongitude()) / tilesDistanceWidth) * width);
                int yPixel = (int) Math.round(((tilesBoundingBox.getMaxLatitude() - projectedLatitude) / tilesDistanceHeight) * height);

                xPixel = Math.max(0, xPixel);
                xPixel = Math.min(width - 1, xPixel);

                yPixel = Math.max(0, yPixel);
                yPixel = Math.min(height - 1, yPixel);

                int color = pixels[(yPixel * width) + xPixel];
                projectedPixels[(y * requestedTileWidth) + x] = color;
            }
        }

        // Draw the new tile bitmap
        Bitmap projectedTileBitmap = Bitmap.createBitmap(requestedTileWidth,
                requestedTileHeight, tile.getConfig());
        projectedTileBitmap.setPixels(projectedPixels, 0, requestedTileWidth, 0, 0, requestedTileWidth, requestedTileHeight);

        return projectedTileBitmap;
    }

    /**
     * Get the tile matrices that may contain the tiles for the bounding box, matches against the bounding box and zoom level options
     *
     * @param projectedRequestBoundingBox bounding box projected to the tiles
     * @return tile matrices
     */
    private List<TileMatrix> getTileMatrices(BoundingBox projectedRequestBoundingBox) {

        List<TileMatrix> tileMatrices = new ArrayList<>();

        // Check if the request overlaps the tile matrix set
        if (!tileDao.getTileMatrices().isEmpty()
                && projectedRequestBoundingBox.intersects(
                tileSetBoundingBox)) {

            // Get the tile distance
            double distanceWidth = projectedRequestBoundingBox
                    .getMaxLongitude()
                    - projectedRequestBoundingBox.getMinLongitude();
            double distanceHeight = projectedRequestBoundingBox
                    .getMaxLatitude()
                    - projectedRequestBoundingBox.getMinLatitude();

            // Get the zoom level to request based upon the tile size
            Long requestZoomLevel = null;
            if (scaling != null) {
                // When options are provided, get the approximate zoom level regardless of whether a tile level exists
                requestZoomLevel = tileDao.getApproximateZoomLevel(distanceWidth, distanceHeight);
            } else {
                // Get the closest existing zoom level
                requestZoomLevel = tileDao.getZoomLevel(distanceWidth, distanceHeight);
            }

            // If there is a matching zoom level
            if (requestZoomLevel != null) {

                List<Long> zoomLevels = null;

                // If options are configured, build the possible zoom levels in order to request
                if (scaling != null && scaling.getScalingType() != null) {

                    // Find zoom in levels
                    List<Long> zoomInLevels = new ArrayList<>();
                    if (scaling.isZoomIn()) {
                        long zoomIn = scaling.getZoomIn() != null ? requestZoomLevel + scaling.getZoomIn() : tileDao.getMaxZoom();
                        for (long zoomLevel = requestZoomLevel + 1; zoomLevel <= zoomIn; zoomLevel++) {
                            zoomInLevels.add(zoomLevel);
                        }
                    }

                    // Find zoom out levels
                    List<Long> zoomOutLevels = new ArrayList<>();
                    if (scaling.isZoomOut()) {
                        long zoomOut = scaling.getZoomOut() != null ? requestZoomLevel - scaling.getZoomOut() : tileDao.getMinZoom();
                        for (long zoomLevel = requestZoomLevel - 1; zoomLevel >= zoomOut; zoomLevel--) {
                            zoomOutLevels.add(zoomLevel);
                        }
                    }

                    if (zoomInLevels.isEmpty()) {
                        // Only zooming out
                        zoomLevels = zoomOutLevels;
                    } else if (zoomOutLevels.isEmpty()) {
                        // Only zooming in
                        zoomLevels = zoomInLevels;
                    } else {
                        // Determine how to order the zoom in and zoom out levels
                        TileScalingType type = scaling.getScalingType();
                        switch (type) {
                            case IN:
                            case IN_OUT:
                                // Order zoom in levels before zoom out levels
                                zoomLevels = zoomInLevels;
                                zoomLevels.addAll(zoomOutLevels);
                                break;
                            case OUT:
                            case OUT_IN:
                                // Order zoom out levels before zoom in levels
                                zoomLevels = zoomOutLevels;
                                zoomLevels.addAll(zoomInLevels);
                                break;
                            case CLOSEST_IN_OUT:
                            case CLOSEST_OUT_IN:
                                // Alternate the zoom in and out levels

                                List<Long> firstLevels;
                                List<Long> secondLevels;
                                if (type == TileScalingType.CLOSEST_IN_OUT) {
                                    // Alternate starting with zoom in
                                    firstLevels = zoomInLevels;
                                    secondLevels = zoomOutLevels;
                                } else {
                                    // Alternate starting with zoom out
                                    firstLevels = zoomOutLevels;
                                    secondLevels = zoomInLevels;
                                }

                                zoomLevels = new ArrayList<>();
                                int maxLevels = Math.max(firstLevels.size(), secondLevels.size());
                                for (int i = 0; i < maxLevels; i++) {
                                    if (i < firstLevels.size()) {
                                        zoomLevels.add(firstLevels.get(i));
                                    }
                                    if (i < secondLevels.size()) {
                                        zoomLevels.add(secondLevels.get(i));
                                    }
                                }

                                break;
                            default:
                                throw new GeoPackageException("Unsupported " + TileScalingType.class.getSimpleName()
                                        + ": " + type);
                        }
                    }
                } else {
                    zoomLevels = new ArrayList<>();
                }

                // Always check the request zoom level first
                zoomLevels.add(0, requestZoomLevel);

                // Build a list of tile matrices that exist for the zoom levels
                for (long zoomLevel : zoomLevels) {
                    TileMatrix tileMatrix = tileDao.getTileMatrix(zoomLevel);
                    if (tileMatrix != null) {
                        tileMatrices.add(tileMatrix);
                    }
                }

            }
        }

        return tileMatrices;
    }

    /**
     * Get the tile row results of tiles needed to draw the requested bounding box tile
     *
     * @param projectedRequestBoundingBox bounding box projected to the tiles
     * @param tileMatrix
     * @return tile cursor results or null
     */
    private TileCursor retrieveTileResults(BoundingBox projectedRequestBoundingBox, TileMatrix tileMatrix) {

        TileCursor tileResults = null;

        if (tileMatrix != null) {

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

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

        }

        return tileResults;
    }

}