package mil.nga.geopackage.features.index;

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

import com.j256.ormlite.dao.CloseableIterator;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.GeoPackage;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.db.FeatureIndexer;
import mil.nga.geopackage.extension.FeatureIndexRTreeResults;
import mil.nga.geopackage.extension.RTreeIndexExtension;
import mil.nga.geopackage.extension.RTreeIndexTableDao;
import mil.nga.geopackage.extension.index.FeatureTableIndex;
import mil.nga.geopackage.extension.index.GeometryIndex;
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.ManualFeatureQuery;
import mil.nga.geopackage.io.GeoPackageProgress;
import mil.nga.geopackage.user.custom.UserCustomCursor;
import mil.nga.sf.GeometryEnvelope;
import mil.nga.sf.proj.Projection;

/**
 * Feature Index Manager to manage indexing of feature geometries in both Android metadata and
 * within a GeoPackage using the Geometry Index Extension
 *
 * @author osbornb
 * @see mil.nga.geopackage.db.FeatureIndexer
 * @see mil.nga.geopackage.extension.index.FeatureTableIndex
 * @since 1.1.0
 */
public class FeatureIndexManager {

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

    /**
     * Feature Table Index, for indexing within a GeoPackage extension
     */
    private final FeatureTableIndex featureTableIndex;

    /**
     * Feature Indexer, for indexing within Android metadata
     */
    private final FeatureIndexer featureIndexer;

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

    /**
     * Manual Feature Queries
     */
    private final ManualFeatureQuery manualFeatureQuery;

    /**
     * Ordered set of index locations to check in order when checking if features are indexed
     * and when querying for features
     */
    private Set<FeatureIndexType> indexLocationQueryOrder = new LinkedHashSet<>();

    /**
     * Index location, when set index calls without specifying a location go to this location
     */
    private FeatureIndexType indexLocation;

    /**
     * Constructor
     *
     * @param context      context
     * @param geoPackage   GeoPackage
     * @param featureTable feature table
     * @since 3.1.0
     */
    public FeatureIndexManager(Context context, GeoPackage geoPackage, String featureTable) {
        this(context, geoPackage, geoPackage.getFeatureDao(featureTable));
    }

    /**
     * Constructor
     *
     * @param context    context
     * @param geoPackage GeoPackage
     * @param featureDao feature DAO
     */
    public FeatureIndexManager(Context context, GeoPackage geoPackage, FeatureDao featureDao) {
        this.featureDao = featureDao;
        featureTableIndex = new FeatureTableIndex(geoPackage, featureDao);
        featureIndexer = new FeatureIndexer(context, featureDao);
        RTreeIndexExtension rTreeExtension = new RTreeIndexExtension(geoPackage);
        rTreeIndexTableDao = rTreeExtension.getTableDao(featureDao);
        manualFeatureQuery = new ManualFeatureQuery(featureDao);

        // Set the default indexed check and query order
        indexLocationQueryOrder.add(FeatureIndexType.RTREE);
        indexLocationQueryOrder.add(FeatureIndexType.GEOPACKAGE);
        indexLocationQueryOrder.add(FeatureIndexType.METADATA);
    }

    /**
     * Close the index connections
     */
    public void close() {
        featureTableIndex.close();
        featureIndexer.close();
        // rTreeIndexTableDao.close();
    }

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

    /**
     * Get the feature table index, used to index inside the GeoPackage as an extension
     *
     * @return feature table index
     */
    public FeatureTableIndex getFeatureTableIndex() {
        return featureTableIndex;
    }

    /**
     * Get the feature indexer, used to index in metadata tables
     *
     * @return feature indexer
     */
    public FeatureIndexer getFeatureIndexer() {
        return featureIndexer;
    }

    /**
     * Get the RTree Index Table DAO
     *
     * @return RTree index table DAO
     * @since 3.1.0
     */
    public RTreeIndexTableDao getRTreeIndexTableDao() {
        return rTreeIndexTableDao;
    }

    /**
     * Get the ordered set of ordered index query locations
     *
     * @return set of ordered index types
     * @since 3.1.0
     */
    public Set<FeatureIndexType> getIndexLocationQueryOrder() {
        return Collections.unmodifiableSet(indexLocationQueryOrder);
    }

    /**
     * Get the index location
     *
     * @return index location or null if not set
     */
    public FeatureIndexType getIndexLocation() {
        return indexLocation;
    }

    /**
     * Prioritize the query location order.  All types are placed at the front of the query order
     * in the order they are given. Omitting a location leaves it at it's current priority location.
     *
     * @param types feature index types
     * @since 3.1.0
     */
    public void prioritizeQueryLocation(Collection<FeatureIndexType> types) {
        prioritizeQueryLocation(types.toArray(new FeatureIndexType[types.size()]));
    }

    /**
     * Prioritize the query location order.  All types are placed at the front of the query order
     * in the order they are given. Omitting a location leaves it at it's current priority location.
     *
     * @param types feature index types
     */
    public void prioritizeQueryLocation(FeatureIndexType... types) {
        // Create a new query order set
        Set<FeatureIndexType> queryOrder = new LinkedHashSet<>();
        for (FeatureIndexType type : types) {
            if (type != FeatureIndexType.NONE) {
                queryOrder.add(type);
            }
        }
        // Add any locations not provided to this method
        queryOrder.addAll(indexLocationQueryOrder);
        // Update the query order set
        indexLocationQueryOrder = queryOrder;
    }

    /**
     * Set the index location order, overriding all previously set types
     *
     * @param types feature index types
     * @since 3.1.0
     */
    public void setIndexLocationOrder(Collection<FeatureIndexType> types) {
        setIndexLocationOrder(types.toArray(new FeatureIndexType[types.size()]));
    }

    /**
     * Set the index location order, overriding all previously set types
     *
     * @param types feature index types
     * @since 3.1.0
     */
    public void setIndexLocationOrder(FeatureIndexType... types) {
        // Create a new query order set
        Set<FeatureIndexType> queryOrder = new LinkedHashSet<>();
        for (FeatureIndexType type : types) {
            if (type != FeatureIndexType.NONE) {
                queryOrder.add(type);
            }
        }
        // Update the query order set
        indexLocationQueryOrder = queryOrder;
    }

    /**
     * Set the index location
     *
     * @param indexLocation feature index type
     */
    public void setIndexLocation(FeatureIndexType indexLocation) {
        this.indexLocation = indexLocation;
    }

    /**
     * Set the GeoPackage Progress
     *
     * @param progress GeoPackage progress
     */
    public void setProgress(GeoPackageProgress progress) {
        featureTableIndex.setProgress(progress);
        featureIndexer.setProgress(progress);
        rTreeIndexTableDao.setProgress(progress);
    }

    /**
     * Index the feature table if needed, using the set index location
     *
     * @return count
     */
    public int index() {
        return index(verifyIndexLocation(), false);
    }

    /**
     * Index the feature tables if needed for the index types
     *
     * @param types feature index types
     * @return largest count of indexed features
     * @since 2.0.0
     */
    public int index(List<FeatureIndexType> types) {
        int count = 0;
        for (FeatureIndexType type : types) {
            int typeCount = index(type);
            count = Math.max(count, typeCount);
        }
        return count;
    }

    /**
     * Index the feature table if needed
     *
     * @param type index location type
     * @return count
     */
    public int index(FeatureIndexType type) {
        return index(type, false);
    }

    /**
     * Index the feature table, using the set index location
     *
     * @param force true to force re-indexing
     * @return count
     */
    public int index(boolean force) {
        return index(verifyIndexLocation(), force);
    }

    /**
     * Index the feature tables for the index types
     *
     * @param force true to force re-indexing
     * @param types feature index types
     * @return largest count of indexed features
     * @since 2.0.0
     */
    public int index(boolean force, List<FeatureIndexType> types) {
        int count = 0;
        for (FeatureIndexType type : types) {
            int typeCount = index(type, force);
            count = Math.max(count, typeCount);
        }
        return count;
    }

    /**
     * Index the feature table
     *
     * @param type  index location type
     * @param force true to force re-indexing
     * @return count
     */
    public int index(FeatureIndexType type, boolean force) {
        if (type == null) {
            throw new GeoPackageException("FeatureIndexType is required to index");
        }
        int count = 0;
        switch (type) {
            case GEOPACKAGE:
                count = featureTableIndex.index(force);
                break;
            case METADATA:
                count = featureIndexer.index(force);
                break;
            case RTREE:
                boolean rTreeIndexed = rTreeIndexTableDao.has();
                if (!rTreeIndexed || force) {
                    if (rTreeIndexed) {
                        rTreeIndexTableDao.delete();
                    }
                    rTreeIndexTableDao.create();
                    count = rTreeIndexTableDao.count();
                }
                break;
            default:
                throw new GeoPackageException("Unsupported FeatureIndexType: "
                        + type);
        }
        return count;
    }

    /**
     * Index the feature row, using the set index location.
     * This method assumes that indexing has been completed and
     * maintained as the last indexed time is updated.
     *
     * @param row feature row to index
     * @return true if indexed
     */
    public boolean index(FeatureRow row) {
        return index(verifyIndexLocation(), row);
    }

    /**
     * Index the feature row for the index types.
     * This method assumes that indexing has been completed and
     * maintained as the last indexed time is updated.
     *
     * @param row   feature row to index
     * @param types feature index types
     * @return true if indexed from any type
     * @since 2.0.0
     */
    public boolean index(FeatureRow row, List<FeatureIndexType> types) {
        boolean indexed = false;
        for (FeatureIndexType type : types) {
            if (index(type, row)) {
                indexed = true;
            }
        }
        return indexed;
    }

    /**
     * Index the feature row. This method assumes that indexing has been completed and
     * maintained as the last indexed time is updated.
     *
     * @param type index location type
     * @param row  feature row to index
     * @return true if indexed
     */
    public boolean index(FeatureIndexType type, FeatureRow row) {
        boolean indexed = false;
        if (type == null) {
            throw new GeoPackageException("FeatureIndexType is required to index");
        }
        switch (type) {
            case GEOPACKAGE:
                indexed = featureTableIndex.index(row);
                break;
            case METADATA:
                indexed = featureIndexer.index(row);
                break;
            case RTREE:
                // Updated by triggers, ignore for RTree
                indexed = true;
                break;
            default:
                throw new GeoPackageException("Unsupported FeatureIndexType: " + type);
        }
        return indexed;
    }

    /**
     * Delete the feature index
     *
     * @return true if deleted
     */
    public boolean deleteIndex() {
        return deleteIndex(verifyIndexLocation());
    }

    /**
     * Delete the feature index from all query order locations
     *
     * @return true if deleted
     * @since 3.1.0
     */
    public boolean deleteAllIndexes() {
        return deleteIndex(indexLocationQueryOrder);
    }

    /**
     * Delete the feature index from the index types
     *
     * @param types feature index types
     * @return true if deleted from any type
     * @since 2.0.0
     */
    public boolean deleteIndex(Collection<FeatureIndexType> types) {
        boolean deleted = false;
        for (FeatureIndexType type : types) {
            if (deleteIndex(type)) {
                deleted = true;
            }
        }
        return deleted;
    }

    /**
     * Delete the feature index
     *
     * @param type feature index type
     * @return true if deleted
     */
    public boolean deleteIndex(FeatureIndexType type) {
        if (type == null) {
            throw new GeoPackageException("FeatureIndexType is required to delete index");
        }
        boolean deleted = false;
        switch (type) {
            case GEOPACKAGE:
                deleted = featureTableIndex.deleteIndex();
                break;
            case METADATA:
                deleted = featureIndexer.deleteIndex();
                break;
            case RTREE:
                rTreeIndexTableDao.delete();
                deleted = true;
                break;
            default:
                throw new GeoPackageException("Unsupported FeatureIndexType: " + type);
        }
        return deleted;
    }

    /**
     * Delete the feature index for the feature row
     *
     * @param row feature row
     * @return true if deleted
     */
    public boolean deleteIndex(FeatureRow row) {
        return deleteIndex(verifyIndexLocation(), row);
    }

    /**
     * Delete the feature index for the feature row from the index types
     *
     * @param row   feature row
     * @param types feature index types
     * @return true if deleted from any type
     * @since 2.0.0
     */
    public boolean deleteIndex(FeatureRow row, List<FeatureIndexType> types) {
        boolean deleted = false;
        for (FeatureIndexType type : types) {
            if (deleteIndex(type, row)) {
                deleted = true;
            }
        }
        return deleted;
    }

    /**
     * Delete the feature index for the feature row
     *
     * @param type feature index type
     * @param row  feature row
     * @return true if deleted
     */
    public boolean deleteIndex(FeatureIndexType type, FeatureRow row) {
        return deleteIndex(type, row.getId());
    }

    /**
     * Delete the feature index for the geometry id
     *
     * @param geomId geometry id
     * @return true if deleted
     */
    public boolean deleteIndex(long geomId) {
        return deleteIndex(verifyIndexLocation(), geomId);
    }

    /**
     * Delete the feature index for the geometry id from the index types
     *
     * @param geomId geometry id
     * @param types  feature index types
     * @return true if deleted from any type
     * @since 2.0.0
     */
    public boolean deleteIndex(long geomId, List<FeatureIndexType> types) {
        boolean deleted = false;
        for (FeatureIndexType type : types) {
            if (deleteIndex(type, geomId)) {
                deleted = true;
            }
        }
        return deleted;
    }

    /**
     * Delete the feature index for the geometry id
     *
     * @param type   feature index type
     * @param geomId geometry id
     * @return true if deleted
     */
    public boolean deleteIndex(FeatureIndexType type, long geomId) {
        if (type == null) {
            throw new GeoPackageException("FeatureIndexType is required to delete index");
        }
        boolean deleted = false;
        switch (type) {
            case GEOPACKAGE:
                deleted = featureTableIndex.deleteIndex(geomId) > 0;
                break;
            case METADATA:
                deleted = featureIndexer.deleteIndex(geomId);
                break;
            case RTREE:
                // Updated by triggers, ignore for RTree
                deleted = true;
                break;
            default:
                throw new GeoPackageException("Unsupported FeatureIndexType: " + type);
        }
        return deleted;
    }

    /**
     * Retain the feature index from the index types and delete the others
     *
     * @param type feature index type to retain
     * @return true if deleted from any type
     * @since 3.1.0
     */
    public boolean retainIndex(FeatureIndexType type) {
        List<FeatureIndexType> retain = new ArrayList<FeatureIndexType>();
        retain.add(type);
        return retainIndex(retain);
    }

    /**
     * Retain the feature index from the index types and delete the others
     *
     * @param types feature index types to retain
     * @return true if deleted from any type
     * @since 3.1.0
     */
    public boolean retainIndex(Collection<FeatureIndexType> types) {
        Set<FeatureIndexType> delete = new HashSet<>(indexLocationQueryOrder);
        delete.removeAll(types);
        return deleteIndex(delete);
    }

    /**
     * Determine if the feature table is indexed
     *
     * @return true if indexed
     */
    public boolean isIndexed() {
        boolean indexed = false;
        for (FeatureIndexType type : indexLocationQueryOrder) {
            indexed = isIndexed(type);
            if (indexed) {
                break;
            }
        }
        return indexed;
    }

    /**
     * Is the feature table indexed in the provided type location
     *
     * @param type index location type
     * @return true if indexed
     */
    public boolean isIndexed(FeatureIndexType type) {
        boolean indexed = false;
        if (type == null) {
            indexed = isIndexed();
        } else {
            switch (type) {
                case GEOPACKAGE:
                    indexed = featureTableIndex.isIndexed();
                    break;
                case METADATA:
                    indexed = featureIndexer.isIndexed();
                    break;
                case RTREE:
                    indexed = rTreeIndexTableDao.has();
                    break;
                default:
                    throw new GeoPackageException("Unsupported FeatureIndexType: " + type);
            }
        }
        return indexed;
    }

    /**
     * Get the indexed types that are currently indexed
     *
     * @return indexed types
     * @since 2.0.0
     */
    public List<FeatureIndexType> getIndexedTypes() {
        List<FeatureIndexType> indexed = new ArrayList<>();
        for (FeatureIndexType type : indexLocationQueryOrder) {
            if (isIndexed(type)) {
                indexed.add(type);
            }
        }
        return indexed;
    }

    /**
     * Get the date last indexed
     *
     * @return last indexed date or null
     */
    public Date getLastIndexed() {
        Date lastIndexed = null;
        for (FeatureIndexType type : indexLocationQueryOrder) {
            lastIndexed = getLastIndexed(type);
            if (lastIndexed != null) {
                break;
            }
        }
        return lastIndexed;
    }

    /**
     * Get the date last indexed
     *
     * @param type feature index type
     * @return last indexed date or null
     */
    public Date getLastIndexed(FeatureIndexType type) {
        Date lastIndexed = null;
        if (type == null) {
            lastIndexed = getLastIndexed();
        } else {
            switch (type) {
                case GEOPACKAGE:
                    lastIndexed = featureTableIndex.getLastIndexed();
                    break;
                case METADATA:
                    lastIndexed = featureIndexer.getLastIndexed();
                    break;
                case RTREE:
                    if (rTreeIndexTableDao.has()) {
                        // Updated by triggers, assume up to date
                        lastIndexed = new Date();
                    }
                    break;
                default:
                    throw new GeoPackageException("Unsupported FeatureIndexType: " + type);
            }
        }
        return lastIndexed;
    }

    /**
     * Query for all feature index results
     *
     * @return feature index results, close when done
     */
    public FeatureIndexResults query() {
        FeatureIndexResults results = null;
        switch (getIndexedType()) {
            case GEOPACKAGE:
                long count = featureTableIndex.count();
                CloseableIterator<GeometryIndex> geometryIndices = featureTableIndex.query();
                results = new FeatureIndexGeoPackageResults(featureTableIndex, count, geometryIndices);
                break;
            case METADATA:
                Cursor geometryMetadata = featureIndexer.query();
                results = new FeatureIndexMetadataResults(featureIndexer, geometryMetadata);
                break;
            case RTREE:
                UserCustomCursor cursor = rTreeIndexTableDao.queryForAll();
                results = new FeatureIndexRTreeResults(rTreeIndexTableDao,
                        cursor);
                break;
            default:
                FeatureCursor featureCursor = featureDao.queryForAll();
                results = new FeatureIndexFeatureResults(featureCursor);
        }
        return results;
    }

    /**
     * Query for all feature index count
     *
     * @return count
     */
    public long count() {
        long count = 0;
        switch (getIndexedType()) {
            case GEOPACKAGE:
                count = featureTableIndex.count();
                break;
            case METADATA:
                count = featureIndexer.count();
                break;
            case RTREE:
                count = rTreeIndexTableDao.count();
                break;
            default:
                count = manualFeatureQuery.countWithGeometries();
        }
        return count;
    }

    /**
     * Query for the feature index bounds
     *
     * @return bounding box
     */
    public BoundingBox getBoundingBox() {
        BoundingBox bounds = null;
        switch (getIndexedType()) {
            case GEOPACKAGE:
                bounds = featureTableIndex.getBoundingBox();
                break;
            case METADATA:
                bounds = featureIndexer.getBoundingBox();
                break;
            case RTREE:
                bounds = rTreeIndexTableDao.getBoundingBox();
                break;
            default:
                bounds = manualFeatureQuery.getBoundingBox();
        }
        return bounds;
    }

    /**
     * Query for the feature index bounds and return in the provided projection
     *
     * @param projection desired projection
     * @return bounding box
     */
    public BoundingBox getBoundingBox(Projection projection) {
        BoundingBox bounds = null;
        switch (getIndexedType()) {
            case GEOPACKAGE:
                bounds = featureTableIndex.getBoundingBox(projection);
                break;
            case METADATA:
                bounds = featureIndexer.getBoundingBox(projection);
                break;
            case RTREE:
                bounds = rTreeIndexTableDao.getBoundingBox(projection);
                break;
            default:
                bounds = manualFeatureQuery.getBoundingBox(projection);
        }
        return bounds;
    }

    /**
     * Query for feature index results within the bounding box, projected
     * correctly
     *
     * @param boundingBox bounding box
     * @return feature index results, close when done
     */
    public FeatureIndexResults query(BoundingBox boundingBox) {
        FeatureIndexResults results = null;
        switch (getIndexedType()) {
            case GEOPACKAGE:
                long count = featureTableIndex.count(boundingBox);
                CloseableIterator<GeometryIndex> geometryIndices = featureTableIndex.query(boundingBox);
                results = new FeatureIndexGeoPackageResults(featureTableIndex, count, geometryIndices);
                break;
            case METADATA:
                Cursor geometryMetadata = featureIndexer.query(boundingBox);
                results = new FeatureIndexMetadataResults(featureIndexer, geometryMetadata);
                break;
            case RTREE:
                UserCustomCursor cursor = rTreeIndexTableDao
                        .query(boundingBox);
                results = new FeatureIndexRTreeResults(rTreeIndexTableDao,
                        cursor);
                break;
            default:
                results = manualFeatureQuery.query(boundingBox);
        }
        return results;
    }

    /**
     * Query for feature index count within the bounding box, projected
     * correctly
     *
     * @param boundingBox bounding box
     * @return count
     */
    public long count(BoundingBox boundingBox) {
        long count = 0;
        switch (getIndexedType()) {
            case GEOPACKAGE:
                count = featureTableIndex.count(boundingBox);
                break;
            case METADATA:
                count = featureIndexer.count(boundingBox);
                break;
            case RTREE:
                count = rTreeIndexTableDao.count(boundingBox);
                break;
            default:
                count = manualFeatureQuery.count(boundingBox);
        }
        return count;
    }

    /**
     * Query for feature index results within the Geometry Envelope
     *
     * @param envelope geometry envelope
     * @return feature index results, close when done
     */
    public FeatureIndexResults query(GeometryEnvelope envelope) {
        FeatureIndexResults results = null;
        switch (getIndexedType()) {
            case GEOPACKAGE:
                long count = featureTableIndex.count(envelope);
                CloseableIterator<GeometryIndex> geometryIndices = featureTableIndex.query(envelope);
                results = new FeatureIndexGeoPackageResults(featureTableIndex, count, geometryIndices);
                break;
            case METADATA:
                Cursor geometryMetadata = featureIndexer.query(envelope);
                results = new FeatureIndexMetadataResults(featureIndexer, geometryMetadata);
                break;
            case RTREE:
                UserCustomCursor cursor = rTreeIndexTableDao.query(envelope);
                results = new FeatureIndexRTreeResults(rTreeIndexTableDao,
                        cursor);
                break;
            default:
                results = manualFeatureQuery.query(envelope);
        }
        return results;
    }

    /**
     * Query for feature index count within the Geometry Envelope
     *
     * @param envelope geometry envelope
     * @return count
     */
    public long count(GeometryEnvelope envelope) {
        long count = 0;
        switch (getIndexedType()) {
            case GEOPACKAGE:
                count = featureTableIndex.count(envelope);
                break;
            case METADATA:
                count = featureIndexer.count(envelope);
                break;
            case RTREE:
                count = rTreeIndexTableDao.count(envelope);
                break;
            default:
                count = manualFeatureQuery.count(envelope);
        }
        return count;
    }

    /**
     * Query for feature index results within the bounding box in
     * the provided projection
     *
     * @param boundingBox bounding box
     * @param projection  projection
     * @return feature index results, close when done
     */
    public FeatureIndexResults query(BoundingBox boundingBox, Projection projection) {
        FeatureIndexResults results = null;
        switch (getIndexedType()) {
            case GEOPACKAGE:
                long count = featureTableIndex.count(boundingBox, projection);
                CloseableIterator<GeometryIndex> geometryIndices = featureTableIndex.query(boundingBox, projection);
                results = new FeatureIndexGeoPackageResults(featureTableIndex, count, geometryIndices);
                break;
            case METADATA:
                Cursor geometryMetadata = featureIndexer.query(boundingBox, projection);
                results = new FeatureIndexMetadataResults(featureIndexer, geometryMetadata);
                break;
            case RTREE:
                UserCustomCursor cursor = rTreeIndexTableDao.query(
                        boundingBox, projection);
                results = new FeatureIndexRTreeResults(rTreeIndexTableDao,
                        cursor);
                break;
            default:
                results = manualFeatureQuery.query(boundingBox, projection);
        }
        return results;
    }

    /**
     * Query for feature index count within the bounding box in
     * the provided projection
     *
     * @param boundingBox bounding box
     * @param projection  projection
     * @return count
     */
    public long count(BoundingBox boundingBox, Projection projection) {
        long count = 0;
        switch (getIndexedType()) {
            case GEOPACKAGE:
                count = featureTableIndex.count(boundingBox, projection);
                break;
            case METADATA:
                count = featureIndexer.count(boundingBox, projection);
                break;
            case RTREE:
                count = rTreeIndexTableDao.count(boundingBox, projection);
                break;
            default:
                count = manualFeatureQuery.count(boundingBox, projection);
        }
        return count;
    }

    /**
     * Verify the index location is set
     *
     * @return feature index type
     */
    private FeatureIndexType verifyIndexLocation() {
        if (indexLocation == null) {
            throw new GeoPackageException("Index Location is not set, set the location or call an index method specifying the location");
        }
        return indexLocation;
    }

    /**
     * Get the indexed type or throw an error if not indexed
     */
    private FeatureIndexType getIndexedType() {

        FeatureIndexType indexType = FeatureIndexType.NONE;

        // Check for an indexed type
        for (FeatureIndexType type : indexLocationQueryOrder) {
            if (isIndexed(type)) {
                indexType = type;
                break;
            }
        }

        return indexType;
    }

}


package mil.nga.geopackage.tiles.retriever;

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.extension.scale.TileScaling;
import mil.nga.geopackage.tiles.TileBoundingBoxUtils;
import mil.nga.geopackage.tiles.user.TileDao;
import mil.nga.sf.proj.Projection;
import mil.nga.sf.proj.ProjectionConstants;
import mil.nga.sf.proj.ProjectionFactory;

/**
 * GeoPackage Tile Retriever, retrieves a tile from a GeoPackage from XYZ coordinates
 *
 * @author osbornb
 * @since 1.2.0
 */
public class GeoPackageTileRetriever implements TileRetriever {

    /**
     * Tile Creator
     */
    private final TileCreator tileCreator;

    /**
     * Constructor using GeoPackage tile sizes
     *
     * @param tileDao tile dao
     */
    public GeoPackageTileRetriever(TileDao tileDao) {
        this(tileDao, null, null);
    }

    /**
     * Constructor with specified tile size
     *
     * @param tileDao tile dao
     * @param width   width
     * @param height  height
     */
    public GeoPackageTileRetriever(TileDao tileDao, Integer width, Integer height) {

        tileDao.adjustTileMatrixLengths();

        Projection webMercator = ProjectionFactory
                .getProjection(ProjectionConstants.EPSG_WEB_MERCATOR);

        tileCreator = new TileCreator(tileDao, width, height, webMercator);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasTile(int x, int y, int zoom) {

        // Get the bounding box of the requested tile
        BoundingBox webMercatorBoundingBox = TileBoundingBoxUtils
                .getWebMercatorBoundingBox(x, y, zoom);

        boolean hasTile = tileCreator.hasTile(webMercatorBoundingBox);

        return hasTile;
    }

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

        // Get the bounding box of the requested tile
        BoundingBox webMercatorBoundingBox = TileBoundingBoxUtils
                .getWebMercatorBoundingBox(x, y, zoom);

        GeoPackageTile tile = tileCreator.getTile(webMercatorBoundingBox);

        return tile;
    }

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

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

}


package mil.nga.geopackage.io;

import android.content.Context;

import java.io.File;

/**
 * Input / Output Context utility methods
 *
 * @author osbornb
 */
public class ContextIOUtils {

    /**
     * Get the internal storage file for the file path
     *
     * @param context  context
     * @param filePath file path
     * @return file
     */
    public static File getInternalFile(Context context, String filePath) {
        File internalFile = null;
        if (filePath != null) {
            internalFile = new File(context.getFilesDir(), filePath);
        } else {
            internalFile = context.getFilesDir();
        }
        return internalFile;
    }

    /**
     * Get the internal storage patch for the file path
     *
     * @param context  context
     * @param filePath file path
     * @return file path
     */
    public static String getInternalFilePath(Context context, String filePath) {
        return getInternalFile(context, filePath).getAbsolutePath();
    }

}


package mil.nga.geopackage.extension.style;

import mil.nga.geopackage.extension.related.UserMappingRow;
import mil.nga.geopackage.user.custom.UserCustomColumn;
import mil.nga.geopackage.user.custom.UserCustomRow;
import mil.nga.sf.GeometryType;

/**
 * Style Mapping Row containing the values from a single result set row
 *
 * @author osbornb
 * @since 3.2.0
 */
public class StyleMappingRow extends UserMappingRow {

    /**
     * Constructor to create an empty row
     *
     * @param table style mapping table
     */
    protected StyleMappingRow(StyleMappingTable table) {
        super(table);
    }

    /**
     * Constructor
     *
     * @param userCustomRow user custom row
     */
    public StyleMappingRow(UserCustomRow userCustomRow) {
        super(userCustomRow);
    }

    /**
     * Copy Constructor
     *
     * @param styleMappingRow style mapping row to copy
     */
    public StyleMappingRow(StyleMappingRow styleMappingRow) {
        super(styleMappingRow);
    }

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

    /**
     * Get the geometry type name column index
     *
     * @return geometry type name column index
     */
    public int getGeometryTypeNameColumnIndex() {
        return getTable().getGeometryTypeNameColumnIndex();
    }

    /**
     * Get the geometry type name column
     *
     * @return geometry type name column
     */
    public UserCustomColumn getGeometryTypeNameColumn() {
        return getTable().getGeometryTypeNameColumn();
    }

    /**
     * Get the geometry type name
     *
     * @return geometry type name
     */
    public String getGeometryTypeName() {
        return getValueString(getGeometryTypeNameColumnIndex());
    }

    /**
     * Get the geometry type
     *
     * @return geometry type
     */
    public GeometryType getGeometryType() {
        GeometryType geometryType = null;
        String geometryTypeName = getGeometryTypeName();
        if (geometryTypeName != null) {
            geometryType = GeometryType.fromName(geometryTypeName);
        }
        return geometryType;
    }

    /**
     * Set the geometry type
     *
     * @param geometryType geometry type
     */
    public void setGeometryType(GeometryType geometryType) {
        String geometryTypeName = null;
        if (geometryType != null) {
            geometryTypeName = geometryType.getName();
        }
        setValue(getGeometryTypeNameColumnIndex(), geometryTypeName);
    }

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

}


package mil.nga.geopackage.tiles;

import android.content.Context;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.GeoPackage;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.R;
import mil.nga.geopackage.io.GeoPackageIOUtils;
import mil.nga.sf.proj.Projection;

/**
 * Creates a set of tiles within a GeoPackage by downloading the tiles from a
 * URL
 *
 * @author osbornb
 */
public class UrlTileGenerator extends TileGenerator {

    /**
     * Tile URL
     */
    private final String tileUrl;

    /**
     * True if the URL has x, y, or z variables
     */
    private final boolean urlHasXYZ;

    /**
     * True if the URL has bounding box variables
     */
    private final boolean urlHasBoundingBox;

    /**
     * TMS URL flag, when true x,y,z converted to TMS when requesting the tile
     */
    private boolean tms = false;

    /**
     * Constructor
     *
     * @param context     app context
     * @param geoPackage  GeoPackage
     * @param tableName   table name
     * @param tileUrl     tile url
     * @param minZoom     min zoom
     * @param maxZoom     max zoom
     * @param boundingBox tiles bounding box
     * @param projection  tiles projection
     * @since 1.3.0
     */
    public UrlTileGenerator(Context context, GeoPackage geoPackage,
                            String tableName, String tileUrl, int minZoom, int maxZoom, BoundingBox boundingBox, Projection projection) {
        super(context, geoPackage, tableName, minZoom, maxZoom, boundingBox, projection);

        try {
            this.tileUrl = URLDecoder.decode(tileUrl, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new GeoPackageException("Failed to decode tile url: "
                    + tileUrl, e);
        }

        this.urlHasXYZ = hasXYZ(tileUrl);
        this.urlHasBoundingBox = hasBoundingBox(tileUrl);

        if (!this.urlHasXYZ && !this.urlHasBoundingBox) {
            throw new GeoPackageException(
                    "URL does not contain x,y,z or bounding box variables: "
                            + tileUrl);
        }
    }

    /**
     * Is TMS URL
     *
     * @return true if TMS URL
     * @since 1.2.1
     */
    public boolean isTms() {
        return tms;
    }

    /**
     * Set the TMS URL flag
     *
     * @param tms true if a TMS URL
     * @since 1.2.1
     */
    public void setTms(boolean tms) {
        this.tms = tms;
    }

    /**
     * Determine if the url has bounding box variables
     *
     * @param url
     * @return
     */
    private boolean hasBoundingBox(String url) {

        String replacedUrl = replaceBoundingBox(url, boundingBox);
        boolean hasBoundingBox = !replacedUrl.equals(url);

        return hasBoundingBox;
    }

    /**
     * Replace x, y, and z in the url
     *
     * @param url
     * @param z
     * @param x
     * @param y
     * @return
     */
    private String replaceXYZ(String url, int z, long x, long y) {

        url = url.replaceAll(
                context.getString(R.string.tile_generator_variable_z),
                String.valueOf(z));
        url = url.replaceAll(
                context.getString(R.string.tile_generator_variable_x),
                String.valueOf(x));
        url = url.replaceAll(
                context.getString(R.string.tile_generator_variable_y),
                String.valueOf(y));
        return url;
    }

    /**
     * Determine if the url has x, y, or z variables
     *
     * @param url
     * @return
     */
    private boolean hasXYZ(String url) {

        String replacedUrl = replaceXYZ(url, 0, 0, 0);
        boolean hasXYZ = !replacedUrl.equals(url);

        return hasXYZ;
    }

    /**
     * Replace the bounding box coordinates in the url
     *
     * @param url
     * @param z
     * @param x
     * @param y
     * @return
     */
    private String replaceBoundingBox(String url, int z, long x, long y) {

        BoundingBox boundingBox = TileBoundingBoxUtils.getProjectedBoundingBox(
                projection, x, y, z);

        url = replaceBoundingBox(url, boundingBox);

        return url;
    }

    /**
     * Replace the url parts with the bounding box
     *
     * @param url
     * @param boundingBox
     * @return
     */
    private String replaceBoundingBox(String url, BoundingBox boundingBox) {

        url = url.replaceAll(
                context.getString(R.string.tile_generator_variable_min_lat),
                String.valueOf(boundingBox.getMinLatitude()));
        url = url.replaceAll(
                context.getString(R.string.tile_generator_variable_max_lat),
                String.valueOf(boundingBox.getMaxLatitude()));
        url = url.replaceAll(
                context.getString(R.string.tile_generator_variable_min_lon),
                String.valueOf(boundingBox.getMinLongitude()));
        url = url.replaceAll(
                context.getString(R.string.tile_generator_variable_max_lon),
                String.valueOf(boundingBox.getMaxLongitude()));

        return url;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void preTileGeneration() {

    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected byte[] createTile(int z, long x, long y) {

        byte[] bytes = null;

        String zoomUrl = tileUrl;

        // Replace x, y, and z
        if (urlHasXYZ) {
            long yRequest = y;

            // If TMS, flip the y value
            if (tms) {
                yRequest = TileBoundingBoxUtils.getYAsOppositeTileFormat(z,
                        (int) y);
            }

            zoomUrl = replaceXYZ(zoomUrl, z, x, yRequest);
        }

        // Replace bounding box
        if (urlHasBoundingBox) {
            zoomUrl = replaceBoundingBox(zoomUrl, z, x, y);
        }

        URL url;
        try {
            url = new URL(zoomUrl);
        } catch (MalformedURLException e) {
            throw new GeoPackageException("Failed to download tile. URL: "
                    + zoomUrl + ", z=" + z + ", x=" + x + ", y=" + y, e);
        }

        HttpURLConnection connection = null;
        try {
            connection = (HttpURLConnection) url.openConnection();
            connection.connect();

            int responseCode = connection.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_MOVED_PERM
                    || responseCode == HttpURLConnection.HTTP_MOVED_TEMP
                    || responseCode == HttpURLConnection.HTTP_SEE_OTHER) {
                String redirect = connection.getHeaderField("Location");
                connection.disconnect();
                url = new URL(redirect);
                connection = (HttpURLConnection) url.openConnection();
                connection.connect();
            }

            if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
                throw new GeoPackageException("Failed to download tile. URL: "
                        + zoomUrl + ", z=" + z + ", x=" + x + ", y=" + y);
            }

            InputStream geoPackageStream = connection.getInputStream();
            bytes = GeoPackageIOUtils.streamBytes(geoPackageStream);

        } catch (IOException e) {
            throw new GeoPackageException("Failed to download tile. URL: "
                    + zoomUrl + ", z=" + z + ", x=" + x + ", y=" + y, e);
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }

        return bytes;
    }

}


package mil.nga.geopackage.features.user;

import android.database.Cursor;

import java.util.List;

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

/**
 * Feature Cursor to wrap a database cursor for feature queries
 *
 * @author osbornb
 */
public class FeatureCursor extends
        UserCursor<FeatureColumn, FeatureTable, FeatureRow> {

    /**
     * Constructor
     *
     * @param table  feature table
     * @param cursor cursor
     */
    public FeatureCursor(FeatureTable table, Cursor cursor) {
        super(table, cursor);
    }

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

    /**
     * {@inheritDoc}
     * Handles geometries
     */
    @Override
    public Object getValue(FeatureColumn column) {
        Object value;
        if (column.isGeometry()) {
            value = getGeometry();
        } else {
            value = super.getValue(column);
        }
        return value;
    }

    /**
     * Get the geometry
     *
     * @return geometry data
     */
    public GeoPackageGeometryData getGeometry() {

        GeoPackageGeometryData geometry = null;

        int columnIndex = getTable().getGeometryColumnIndex();
        int type = getType(columnIndex);

        if (type != FIELD_TYPE_NULL) {
            byte[] geometryBytes = getBlob(columnIndex);

            if (geometryBytes != null) {
                geometry = new GeoPackageGeometryData(geometryBytes);
            }
        }

        return geometry;
    }

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

    /**
     * {@inheritDoc}
     */
    @Override
    protected UserInvalidCursor<FeatureColumn, FeatureTable, FeatureRow, ? extends UserCursor<FeatureColumn, FeatureTable, FeatureRow>, ? extends UserDao<FeatureColumn, FeatureTable, FeatureRow, ? extends UserCursor<FeatureColumn, FeatureTable, FeatureRow>>> createInvalidCursor(UserDao dao, UserCursor cursor, List<Integer> invalidPositions, List<FeatureColumn> blobColumns) {
        return new FeatureInvalidCursor((FeatureDao) dao, (FeatureCursor) cursor, invalidPositions, blobColumns);
    }

}


package mil.nga.geopackage.db;

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

import java.util.Date;

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.core.contents.Contents;
import mil.nga.geopackage.db.metadata.GeoPackageMetadataDb;
import mil.nga.geopackage.db.metadata.GeometryMetadata;
import mil.nga.geopackage.db.metadata.GeometryMetadataDataSource;
import mil.nga.geopackage.db.metadata.TableMetadata;
import mil.nga.geopackage.db.metadata.TableMetadataDataSource;
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.geopackage.geom.GeoPackageGeometryData;
import mil.nga.geopackage.io.GeoPackageProgress;
import mil.nga.sf.Geometry;
import mil.nga.sf.GeometryEnvelope;
import mil.nga.sf.proj.Projection;
import mil.nga.sf.proj.ProjectionTransform;
import mil.nga.sf.util.GeometryEnvelopeBuilder;

/**
 * Feature Indexer, indexes feature geometries into a table for searching
 */
public class FeatureIndexer {

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

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

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

    /**
     * Database connection to the metadata
     */
    private final GeoPackageMetadataDb db;

    /**
     * Geometry Metadata Data Source
     */
    private final GeometryMetadataDataSource geometryMetadataDataSource;

    /**
     * Progress
     */
    private GeoPackageProgress progress;

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

    /**
     * Constructor
     *
     * @param context    context
     * @param featureDao feature dao
     */
    public FeatureIndexer(Context context, FeatureDao featureDao) {
        this.context = context;
        this.featureDao = featureDao;
        db = new GeoPackageMetadataDb(context);
        db.open();
        geometryMetadataDataSource = new GeometryMetadataDataSource(db);
    }

    /**
     * Close the database connection in the feature indexer
     *
     * @since 1.1.0
     */
    public void close() {
        db.close();
    }

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

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

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

    /**
     * Index the feature table if needed
     *
     * @return count
     */
    public int index() {
        return index(false);
    }

    /**
     * Index the feature table
     *
     * @param force true to force re-indexing
     * @return count
     */
    public int index(boolean force) {
        int count = 0;
        if (force || !isIndexed()) {
            count = indexTable();
        }
        return count;
    }

    /**
     * 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) {

        long geoPackageId = geometryMetadataDataSource.getGeoPackageId(featureDao.getDatabase());
        boolean indexed = index(geoPackageId, row, true);

        // Update the last indexed time
        updateLastIndexed(db, geoPackageId);

        return indexed;
    }

    /**
     * Index the feature table
     *
     * @return count
     */
    private int indexTable() {

        int count = 0;

        // Get or create the table metadata
        TableMetadataDataSource tableDs = new TableMetadataDataSource(db);
        TableMetadata metadata = tableDs.getOrCreate(featureDao.getDatabase(), featureDao.getTableName());

        // Delete existing index rows
        geometryMetadataDataSource.delete(featureDao.getDatabase(), featureDao.getTableName());

        long offset = 0;
        int chunkCount = 0;

        // Index all features
        while (chunkCount >= 0) {

            FeatureCursor cursor = featureDao.queryForChunk(chunkLimit, offset);
            chunkCount = indexRows(metadata.getGeoPackageId(), cursor);

            if (chunkCount > 0) {
                count += chunkCount;
            }

            offset += chunkLimit;
        }

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

        return count;
    }

    /**
     * Index the feature rows in the cursor
     *
     * @param geoPackageId GeoPackage id
     * @param cursor       feature cursor
     * @return count, -1 if no results or canceled
     */
    private int indexRows(long geoPackageId, 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(geoPackageId, row, false);
                        if (indexed) {
                            count++;
                        }
                        if (progress != null) {
                            progress.addProgress(1);
                        }
                    }
                } catch (Exception e) {
                    Log.e(FeatureIndexer.class.getSimpleName(), "Failed to index feature. Table: "
                            + featureDao.getTableName() + ", Position: " + cursor.getPosition(), e);
                }
            }
        } finally {
            cursor.close();
        }

        return count;
    }

    /**
     * Index the feature row
     *
     * @param geoPackageId   GeoPackage id
     * @param row            feature row
     * @param possibleUpdate possible update flag
     * @return true if indexed
     */
    private boolean index(long geoPackageId, FeatureRow row, boolean possibleUpdate) {

        boolean indexed = false;

        GeoPackageGeometryData geomData = row.getGeometry();
        if (geomData != null) {

            // Get the envelope
            GeometryEnvelope envelope = geomData.getEnvelope();

            // If no envelope, build one from the geometry
            if (envelope == null) {
                Geometry geometry = geomData.getGeometry();
                if (geometry != null) {
                    envelope = GeometryEnvelopeBuilder.buildEnvelope(geometry);
                }
            }

            // Create the new index row
            if (envelope != null) {
                GeometryMetadata metadata = geometryMetadataDataSource.populate(geoPackageId, featureDao.getTableName(), row.getId(), envelope);
                if (possibleUpdate) {
                    geometryMetadataDataSource.createOrUpdate(metadata);
                } else {
                    geometryMetadataDataSource.create(metadata);
                }
                indexed = true;
            }
        }

        return indexed;
    }

    /**
     * Update the least indexed time
     *
     * @param db           metadata db
     * @param geoPackageId GeoPackage id
     */
    private void updateLastIndexed(GeoPackageMetadataDb db, long geoPackageId) {

        long indexedTime = (new Date()).getTime();

        TableMetadataDataSource ds = new TableMetadataDataSource(db);
        if (!ds.updateLastIndexed(geoPackageId, featureDao.getTableName(), indexedTime)) {
            throw new GeoPackageException("Failed to update last indexed time. Table: GeoPackage Id: "
                    + geoPackageId + ", Table: " + featureDao.getTableName() + ", Last Indexed: " + indexedTime);
        }

    }

    /**
     * Delete the feature table index
     *
     * @return true if index deleted
     * @since 1.1.0
     */
    public boolean deleteIndex() {
        TableMetadataDataSource tableMetadataDataSource = new TableMetadataDataSource(db);
        boolean deleted = tableMetadataDataSource.delete(featureDao.getDatabase(), featureDao.getTableName());
        return deleted;
    }

    /**
     * Delete the index for the feature row
     *
     * @param row feature row
     * @return true if deleted
     * @since 1.1.0
     */
    public boolean deleteIndex(FeatureRow row) {
        return deleteIndex(row.getId());
    }

    /**
     * Delete the index for the geometry id
     *
     * @param geomId geometry id
     * @return true if deleted
     */
    public boolean deleteIndex(long geomId) {
        boolean deleted = geometryMetadataDataSource.delete(
                featureDao.getDatabase(), featureDao.getTableName(), geomId);
        return deleted;
    }

    /**
     * Determine if the database table is indexed after database modifications
     *
     * @return true if indexed
     */
    public boolean isIndexed() {

        boolean indexed = false;

        Date lastIndexed = getLastIndexed();
        if (lastIndexed != null) {
            Contents contents = featureDao.getGeometryColumns().getContents();
            Date lastChange = contents.getLastChange();
            indexed = lastIndexed.equals(lastChange) || lastIndexed.after(lastChange);
        }

        return indexed;
    }

    /**
     * Get the date last indexed
     *
     * @return last indexed date or null
     * @since 1.1.0
     */
    public Date getLastIndexed() {
        Date date = null;
        TableMetadataDataSource ds = new TableMetadataDataSource(db);
        TableMetadata metadata = ds.get(featureDao.getDatabase(), featureDao.getTableName());
        if (metadata != null) {
            Long lastIndexed = metadata.getLastIndexed();
            if (lastIndexed != null) {
                date = new Date(lastIndexed);
            }
        }
        return date;
    }

    /**
     * Query for all Geometry Metadata
     *
     * @return geometry metadata cursor
     * @since 1.1.0
     */
    public Cursor query() {
        Cursor cursor = geometryMetadataDataSource.query(featureDao.getDatabase(), featureDao.getTableName());
        return cursor;
    }

    /**
     * Query for all Geometry Metadata count
     *
     * @return count
     * @since 1.1.0
     */
    public int count() {
        int count = geometryMetadataDataSource.count(featureDao.getDatabase(), featureDao.getTableName());
        return count;
    }

    /**
     * Query for the bounds of the feature table index
     *
     * @return bounding box
     * @since 3.1.0
     */
    public BoundingBox getBoundingBox() {
        return geometryMetadataDataSource.getBoundingBox(featureDao.getDatabase(), featureDao.getTableName());
    }

    /**
     * Query for the feature index bounds and return in the provided projection
     *
     * @param projection desired projection
     * @return bounding box
     * @since 3.1.0
     */
    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 Geometry Metadata within the bounding box, projected
     * correctly
     *
     * @param boundingBox bounding box
     * @return geometry metadata cursor
     * @since 1.1.0
     */
    public Cursor query(BoundingBox boundingBox) {
        Cursor cursor = geometryMetadataDataSource.query(featureDao.getDatabase(), featureDao.getTableName(), boundingBox);
        return cursor;
    }

    /**
     * Query for Geometry Metadata count within the bounding box, projected
     * correctly
     *
     * @param boundingBox bounding box
     * @return count
     * @since 1.1.0
     */
    public int count(BoundingBox boundingBox) {
        int count = geometryMetadataDataSource.count(featureDao.getDatabase(), featureDao.getTableName(), boundingBox);
        return count;
    }

    /**
     * Query for Geometry Metadata within the Geometry Envelope
     *
     * @param envelope geometry envelope
     * @return geometry metadata cursor
     * @since 1.1.0
     */
    public Cursor query(GeometryEnvelope envelope) {
        Cursor cursor = geometryMetadataDataSource.query(featureDao.getDatabase(), featureDao.getTableName(), envelope);
        return cursor;
    }

    /**
     * Query for Geometry Metadata count within the Geometry Envelope
     *
     * @param envelope geometry envelope
     * @return count
     * @since 1.1.0
     */
    public int count(GeometryEnvelope envelope) {
        int count = geometryMetadataDataSource.count(featureDao.getDatabase(), featureDao.getTableName(), envelope);
        return count;
    }

    /**
     * Query for Geometry Metadata within the bounding box in
     * the provided projection
     *
     * @param boundingBox bounding box
     * @param projection  projection of the provided bounding box
     * @return geometry metadata cursor
     * @since 1.1.0
     */
    public Cursor query(BoundingBox boundingBox,
                        Projection projection) {

        BoundingBox featureBoundingBox = getFeatureBoundingBox(boundingBox,
                projection);

        Cursor cursor = query(featureBoundingBox);

        return cursor;
    }

    /**
     * Query for Geometry Metadata count within the bounding box in
     * the provided projection
     *
     * @param boundingBox bounding box
     * @param projection  projection of the provided bounding box
     * @return count
     * @since 1.1.0
     */
    public long count(BoundingBox boundingBox, Projection projection) {

        BoundingBox featureBoundingBox = getFeatureBoundingBox(boundingBox,
                projection);

        long count = count(featureBoundingBox);

        return count;
    }

    /**
     * Get the bounding box in the feature projection from the bounding box in
     * the provided projection
     *
     * @param boundingBox bounding box
     * @param projection  projection
     * @return feature projected bounding box
     */
    private BoundingBox getFeatureBoundingBox(BoundingBox boundingBox,
                                              Projection projection) {
        ProjectionTransform projectionTransform = projection
                .getTransformation(featureDao.getProjection());
        BoundingBox featureBoundingBox = boundingBox
                .transform(projectionTransform);
        return featureBoundingBox;
    }

    /**
     * Get the Geometry Metadata for the current place in the cursor
     *
     * @param cursor cursor
     * @return geometry metadata
     * @since 1.1.0
     */
    public GeometryMetadata getGeometryMetadata(Cursor cursor) {
        GeometryMetadata geometryMetadata = GeometryMetadataDataSource.createGeometryMetadata(cursor);
        return geometryMetadata;
    }

    /**
     * Get the feature row for the current place in the cursor
     *
     * @param cursor cursor
     * @return feature row
     * @since 1.1.0
     */
    public FeatureRow getFeatureRow(Cursor cursor) {
        GeometryMetadata geometryMetadata = getGeometryMetadata(cursor);
        FeatureRow featureRow = getFeatureRow(geometryMetadata);
        return featureRow;
    }

    /**
     * Get the feature row for the Geometry Metadata
     *
     * @param geometryMetadata geometry metadata
     * @return feature row
     * @since 1.1.0
     */
    public FeatureRow getFeatureRow(GeometryMetadata geometryMetadata) {

        long geomId = geometryMetadata.getId();

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

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

    /**
     * Set the query range tolerance
     *
     * @param tolerance query range tolerance
     * @since 3.1.0
     */
    public void setTolerance(double tolerance) {
        geometryMetadataDataSource.setTolerance(tolerance);
    }

}


package mil.nga.geopackage.extension.link;

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

import mil.nga.geopackage.GeoPackage;
import mil.nga.geopackage.features.user.FeatureDao;
import mil.nga.geopackage.tiles.user.TileDao;

/**
 * Feature Tile Table linker, used to link feature and tile tables together when
 * the tiles represent the feature data
 *
 * @author osbornb
 * @since 1.2.6
 */
public class FeatureTileTableLinker extends FeatureTileTableCoreLinker {

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

    /**
     * Constructor
     *
     * @param geoPackage GeoPackage
     */
    public FeatureTileTableLinker(GeoPackage geoPackage) {
        super(geoPackage);
        this.geoPackage = geoPackage;
    }

    /**
     * Query for the tile tables linked to a feature table and return tile DAOs
     * to those tables
     *
     * @param featureTable feature table
     * @return tiles DAOs
     */
    public List<TileDao> getTileDaosForFeatureTable(String featureTable) {

        List<TileDao> tileDaos = new ArrayList<TileDao>();

        List<String> tileTables = getTileTablesForFeatureTable(featureTable);
        for (String tileTable : tileTables) {
            if (geoPackage.isTileTable(tileTable)) {
                TileDao tileDao = geoPackage.getTileDao(tileTable);
                tileDaos.add(tileDao);
            }
        }

        return tileDaos;
    }

    /**
     * Query for the feature tables linked to a tile table and return feature
     * DAOs to those tables
     *
     * @param tileTable tile table
     * @return feature DAOs
     */
    public List<FeatureDao> getFeatureDaosForTileTable(String tileTable) {

        List<FeatureDao> featureDaos = new ArrayList<FeatureDao>();

        List<String> featureTables = getFeatureTablesForTileTable(tileTable);
        for (String featureTable : featureTables) {
            if (geoPackage.isFeatureTable(featureTable)) {
                FeatureDao featureDao = geoPackage.getFeatureDao(featureTable);
                featureDaos.add(featureDao);
            }
        }

        return featureDaos;
    }

}


package mil.nga.geopackage.tiles.features;

import android.content.Context;

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.GeoPackage;
import mil.nga.geopackage.extension.link.FeatureTileTableLinker;
import mil.nga.geopackage.tiles.TileBoundingBoxUtils;
import mil.nga.geopackage.tiles.TileGenerator;
import mil.nga.geopackage.tiles.TileGrid;
import mil.nga.sf.proj.Projection;
import mil.nga.sf.proj.ProjectionConstants;
import mil.nga.sf.proj.ProjectionTransform;

/**
 * Creates a set of tiles within a GeoPackage by generating tiles from features
 *
 * @author osbornb
 */
public class FeatureTileGenerator extends TileGenerator {

    /**
     * Feature tiles
     */
    private final FeatureTiles featureTiles;

    /**
     * Flag indicating whether the feature and tile tables should be linked
     */
    private boolean linkTables = true;

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

    /**
     * Constructor
     *
     * @param context           app context
     * @param geoPackage        GeoPackage
     * @param tableName         table name
     * @param featureTiles      feature tiles
     * @param featureGeoPackage feature GeoPackage if different from the destination
     * @param minZoom           min zoom
     * @param maxZoom           max zoom
     * @param boundingBox       tiles bounding box
     * @param projection        tiles projection
     * @since 3.2.0
     */
    public FeatureTileGenerator(Context context, GeoPackage geoPackage, String tableName,
                                FeatureTiles featureTiles, GeoPackage featureGeoPackage,
                                int minZoom, int maxZoom, BoundingBox boundingBox,
                                Projection projection) {
        super(context, geoPackage, tableName, minZoom, maxZoom, getBoundingBox(
                featureGeoPackage, featureTiles, boundingBox, projection),
                projection);
        this.featureTiles = featureTiles;
    }

    /**
     * Constructor, find the the bounding box from the feature table
     *
     * @param context      app context
     * @param geoPackage   GeoPackage
     * @param tableName    table name
     * @param featureTiles feature tiles
     * @param minZoom      min zoom
     * @param maxZoom      max zoom
     * @param projection   tiles projection
     * @since 3.2.0
     */
    public FeatureTileGenerator(Context context, GeoPackage geoPackage, String tableName,
                                FeatureTiles featureTiles, int minZoom, int maxZoom,
                                Projection projection) {
        this(context, geoPackage, tableName, featureTiles, minZoom, maxZoom, null,
                projection);
    }

    /**
     * Constructor, find the the bounding box from the feature table
     *
     * @param context           app context
     * @param geoPackage        GeoPackage
     * @param tableName         table name
     * @param featureTiles      feature tiles
     * @param featureGeoPackage feature GeoPackage if different from the destination
     * @param minZoom           min zoom
     * @param maxZoom           max zoom
     * @param projection        tiles projection
     * @since 3.2.0
     */
    public FeatureTileGenerator(Context context, GeoPackage geoPackage, String tableName,
                                FeatureTiles featureTiles, GeoPackage featureGeoPackage,
                                int minZoom, int maxZoom, Projection projection) {
        this(context, geoPackage, tableName, featureTiles, featureGeoPackage, minZoom,
                maxZoom, null, projection);
    }

    /**
     * Get the bounding box for the feature tile generator, from the provided
     * and from the feature table
     *
     * @param geoPackage   GeoPackage
     * @param featureTiles feature tiles
     * @param boundingBox  bounding box
     * @param projection   projection
     * @return bounding box
     */
    private static BoundingBox getBoundingBox(GeoPackage geoPackage,
                                              FeatureTiles featureTiles, BoundingBox boundingBox,
                                              Projection projection) {

        String tableName = featureTiles.getFeatureDao().getTableName();
        boolean manualQuery = boundingBox == null;
        BoundingBox featureBoundingBox = geoPackage.getBoundingBox(projection,
                tableName, manualQuery);
        if (featureBoundingBox != null) {
            if (boundingBox == null) {
                boundingBox = featureBoundingBox;
            } else {
                boundingBox = boundingBox.overlap(featureBoundingBox);
            }
        }

        if (boundingBox != null) {
            boundingBox = featureTiles.expandBoundingBox(boundingBox,
                    projection);
        }

        return boundingBox;
    }

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

        ProjectionTransform projectionToWebMercator = projection
                .getTransformation(ProjectionConstants.EPSG_WEB_MERCATOR);
        BoundingBox webMercatorBoundingBox = boundingBox
                .transform(projectionToWebMercator);

        TileGrid tileGrid = TileBoundingBoxUtils.getTileGrid(webMercatorBoundingBox, zoom);
        BoundingBox tileBoundingBox = TileBoundingBoxUtils.getWebMercatorBoundingBox(
                tileGrid.getMinX(), tileGrid.getMinY(), zoom);

        BoundingBox expandedBoundingBox = featureTiles.expandBoundingBox(webMercatorBoundingBox, tileBoundingBox);

        BoundingBox zoomBoundingBox = expandedBoundingBox.transform(projectionToWebMercator.getInverseTransformation());

        return zoomBoundingBox;
    }

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

    /**
     * Is the feature table going to be linked with the tile table? Defaults to
     * true.
     *
     * @return true if tables will be linked upon generation
     * @since 1.2.5
     */
    public boolean isLinkTables() {
        return linkTables;
    }

    /**
     * Set the link tables flag
     *
     * @param linkTables link tables flag
     * @since 1.2.5
     */
    public void setLinkTables(boolean linkTables) {
        this.linkTables = linkTables;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void preTileGeneration() {

        // Link the feature and tile table if they are in the same GeoPackage
        GeoPackage geoPackage = getGeoPackage();
        String featureTable = featureTiles.getFeatureDao().getTableName();
        String tileTable = getTableName();
        if (linkTables && geoPackage.isFeatureTable(featureTable)
                && geoPackage.isTileTable(tileTable)) {
            FeatureTileTableLinker linker = new FeatureTileTableLinker(
                    geoPackage);
            linker.link(featureTable, tileTable);
        }

    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected byte[] createTile(int z, long x, long y) {

        byte[] tileData = featureTiles.drawTileBytes((int) x, (int) y, z);

        return tileData;
    }

}


package mil.nga.geopackage.extension.related;

import java.util.List;

import mil.nga.geopackage.db.CoreSQLUtils;
import mil.nga.geopackage.user.custom.UserCustomCursor;
import mil.nga.geopackage.user.custom.UserCustomDao;
import mil.nga.geopackage.user.custom.UserCustomRow;

/**
 * User Mapping DAO for reading user mapping data tables
 *
 * @author osbornb
 * @since 3.0.1
 */
public class UserMappingDao extends UserCustomDao {

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

    /**
     * Constructor
     *
     * @param dao              user custom data access object
     * @param userMappingTable user mapping table
     */
    protected UserMappingDao(UserCustomDao dao,
                             UserMappingTable userMappingTable) {
        super(dao, userMappingTable);
    }

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

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

    /**
     * Get the user mapping row from the current cursor location
     *
     * @param cursor cursor
     * @return user mapping row
     */
    public UserMappingRow getRow(UserCustomCursor cursor) {
        return getRow(cursor.getRow());
    }

    /**
     * Get a user mapping row from the user custom row
     *
     * @param row custom row
     * @return user mapping row
     */
    public UserMappingRow getRow(UserCustomRow row) {
        return new UserMappingRow(row);
    }

    /**
     * Query by base id
     *
     * @param userMappingRow user mapping row
     * @return cursor
     */
    public UserCustomCursor queryByBaseId(UserMappingRow userMappingRow) {
        return queryByBaseId(userMappingRow.getBaseId());
    }

    /**
     * Query by base id
     *
     * @param baseId base id
     * @return cursor
     */
    public UserCustomCursor queryByBaseId(long baseId) {
        return queryForEq(UserMappingTable.COLUMN_BASE_ID, baseId);
    }

    /**
     * Count by base id
     *
     * @param userMappingRow user mapping row
     * @return count
     */
    public int countByBaseId(UserMappingRow userMappingRow) {
        return countByBaseId(userMappingRow.getBaseId());
    }

    /**
     * Count by base id
     *
     * @param baseId base id
     * @return count
     */
    public int countByBaseId(long baseId) {
        return count(queryByBaseId(baseId));
    }

    /**
     * Query by related id
     *
     * @param userMappingRow user mapping row
     * @return cursor
     */
    public UserCustomCursor queryByRelatedId(UserMappingRow userMappingRow) {
        return queryByRelatedId(userMappingRow.getRelatedId());
    }

    /**
     * Query by related id
     *
     * @param relatedId related id
     * @return cursor
     */
    public UserCustomCursor queryByRelatedId(long relatedId) {
        return queryForEq(UserMappingTable.COLUMN_RELATED_ID, relatedId);
    }

    /**
     * Count by related id
     *
     * @param userMappingRow user mapping row
     * @return count
     */
    public int countByRelatedId(UserMappingRow userMappingRow) {
        return countByRelatedId(userMappingRow.getRelatedId());
    }

    /**
     * Count by related id
     *
     * @param relatedId related id
     * @return count
     */
    public int countByRelatedId(long relatedId) {
        return count(queryByRelatedId(relatedId));
    }

    /**
     * Query by both base id and related id
     *
     * @param userMappingRow user mapping row
     * @return cursor
     */
    public UserCustomCursor queryByIds(UserMappingRow userMappingRow) {
        return queryByIds(userMappingRow.getBaseId(),
                userMappingRow.getRelatedId());
    }

    /**
     * Query by both base id and related id
     *
     * @param baseId    base id
     * @param relatedId related id
     * @return cursor
     */
    public UserCustomCursor queryByIds(long baseId, long relatedId) {
        return query(buildWhereIds(baseId, relatedId),
                buildWhereIdsArgs(baseId, relatedId));
    }

    /**
     * Get the unique base ids
     *
     * @return list of unique base ids
     * @since 3.2.0
     */
    public List<Long> uniqueBaseIds() {
        return querySingleColumnTypedResults(
                "SELECT DISTINCT " + CoreSQLUtils.quoteWrap(UserMappingTable.COLUMN_BASE_ID) + " FROM "
                        + CoreSQLUtils.quoteWrap(getTableName()), null);
    }

    /**
     * Get the unique related ids
     *
     * @return list of unique related ids
     * @since 3.2.0
     */
    public List<Long> uniqueRelatedIds() {
        return querySingleColumnTypedResults(
                "SELECT DISTINCT " + CoreSQLUtils.quoteWrap(UserMappingTable.COLUMN_RELATED_ID) + " FROM "
                        + CoreSQLUtils.quoteWrap(getTableName()), null);
    }

    /**
     * Count by both base id and related id
     *
     * @param userMappingRow user mapping row
     * @return count
     */
    public int countByIds(UserMappingRow userMappingRow) {
        return countByIds(userMappingRow.getBaseId(),
                userMappingRow.getRelatedId());
    }

    /**
     * Count by both base id and related id
     *
     * @param baseId    base id
     * @param relatedId related id
     * @return count
     */
    public int countByIds(long baseId, long relatedId) {
        return count(queryByIds(baseId, relatedId));
    }

    /**
     * Delete user mappings by base id
     *
     * @param userMappingRow user mapping row
     * @return rows deleted
     */
    public int deleteByBaseId(UserMappingRow userMappingRow) {
        return deleteByBaseId(userMappingRow.getBaseId());
    }

    /**
     * Delete user mappings by base id
     *
     * @param baseId base id
     * @return rows deleted
     */
    public int deleteByBaseId(long baseId) {

        StringBuilder where = new StringBuilder();
        where.append(buildWhere(UserMappingTable.COLUMN_BASE_ID, baseId));

        String[] whereArgs = buildWhereArgs(new Object[]{baseId});

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

        return deleted;
    }

    /**
     * Delete user mappings by related id
     *
     * @param userMappingRow user mapping row
     * @return rows deleted
     */
    public int deleteByRelatedId(UserMappingRow userMappingRow) {
        return deleteByRelatedId(userMappingRow.getRelatedId());
    }

    /**
     * Delete user mappings by related id
     *
     * @param relatedId related id
     * @return rows deleted
     */
    public int deleteByRelatedId(long relatedId) {

        StringBuilder where = new StringBuilder();
        where.append(buildWhere(UserMappingTable.COLUMN_RELATED_ID, relatedId));

        String[] whereArgs = buildWhereArgs(new Object[]{relatedId});

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

        return deleted;
    }

    /**
     * Delete user mappings by both base id and related id
     *
     * @param userMappingRow user mapping row
     * @return rows deleted
     */
    public int deleteByIds(UserMappingRow userMappingRow) {
        return deleteByIds(userMappingRow.getBaseId(),
                userMappingRow.getRelatedId());
    }

    /**
     * Delete user mappings by both base id and related id
     *
     * @param baseId    base id
     * @param relatedId related id
     * @return rows deleted
     */
    public int deleteByIds(long baseId, long relatedId) {
        return delete(buildWhereIds(baseId, relatedId),
                buildWhereIdsArgs(baseId, relatedId));
    }

    /**
     * Build the where ids clause
     *
     * @param baseId    base id
     * @param relatedId related id
     * @return where clause
     */
    private String buildWhereIds(long baseId, long relatedId) {

        StringBuilder where = new StringBuilder();
        where.append(buildWhere(UserMappingTable.COLUMN_BASE_ID, baseId));
        where.append(" AND ");
        where.append(buildWhere(UserMappingTable.COLUMN_RELATED_ID, relatedId));

        return where.toString();
    }

    /**
     * Build the where ids clause arguments
     *
     * @param baseId    base id
     * @param relatedId related id
     * @return where args
     */
    private String[] buildWhereIdsArgs(long baseId, long relatedId) {
        return buildWhereArgs(new Object[]{baseId, relatedId});
    }

}


package mil.nga.geopackage.db.metadata;

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

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

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

/**
 * GeoPackage metadata Data Source
 *
 * @author osbornb
 */
public class GeoPackageMetadataDataSource {

    /**
     * Database
     */
    private GeoPackageDatabase db;

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

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

    /**
     * Create a new GeoPackage metadata
     *
     * @param metadata GeoPackage metadata
     */
    public void create(GeoPackageMetadata metadata) {
        ContentValues values = new ContentValues();
        values.put(GeoPackageMetadata.COLUMN_NAME, metadata.getName());
        values.put(GeoPackageMetadata.COLUMN_EXTERNAL_PATH, metadata.getExternalPath());
        long insertId = db.insert(
                GeoPackageMetadata.TABLE_NAME, null,
                values);
        if (insertId == -1) {
            throw new GeoPackageException(
                    "Failed to insert GeoPackage metadata. Name: "
                            + metadata.getName() + ", External Path: "
                            + metadata.getExternalPath());
        }
        metadata.setId(insertId);
    }

    /**
     * Delete the GeoPackage metadata
     *
     * @param metadata GeoPackage metadata
     * @return deleted flag
     */
    public boolean delete(GeoPackageMetadata metadata) {
        return delete(metadata.getName());
    }

    /**
     * Delete the database
     *
     * @param database database name
     * @return deleted flag
     */
    public boolean delete(String database) {

        GeoPackageMetadata metadata = get(database);
        if (metadata != null) {
            TableMetadataDataSource tableDs = new TableMetadataDataSource(db);
            tableDs.delete(metadata.getId());
        }

        String whereClause = GeoPackageMetadata.COLUMN_NAME + " = ?";
        String[] whereArgs = new String[]{database};
        int deleteCount = db.delete(
                GeoPackageMetadata.TABLE_NAME,
                whereClause, whereArgs);
        return deleteCount > 0;
    }

    /**
     * Rename the GeoPackage metadata to the new name
     *
     * @param metadata GeoPackage metadata
     * @param newName  new name
     * @return renamed flag
     */
    public boolean rename(GeoPackageMetadata metadata, String newName) {
        boolean renamed = rename(metadata.getName(), newName);
        if (renamed) {
            metadata.setName(newName);
        }
        return renamed;
    }

    /**
     * Rename the GeoPackage name to the new name
     *
     * @param name    GeoPackage name
     * @param newName new name
     * @return renamed flag
     */
    public boolean rename(String name, String newName) {
        String whereClause = GeoPackageMetadata.COLUMN_NAME + " = ?";
        String[] whereArgs = new String[]{name};
        ContentValues values = new ContentValues();
        values.put(GeoPackageMetadata.COLUMN_NAME, newName);
        int updateCount = db.update(
                GeoPackageMetadata.TABLE_NAME, values,
                whereClause, whereArgs);
        return updateCount > 0;
    }

    /**
     * Get all GeoPackage metadata
     *
     * @return all GeoPackage metadata
     */
    public List<GeoPackageMetadata> getAll() {
        List<GeoPackageMetadata> allMetadata = new ArrayList<GeoPackageMetadata>();

        Cursor cursor = db.query(
                GeoPackageMetadata.TABLE_NAME,
                GeoPackageMetadata.COLUMNS, null, null, null, null, null);
        try {
            while (cursor.moveToNext()) {
                GeoPackageMetadata metadata = createGeoPackageMetadata(cursor);
                allMetadata.add(metadata);
            }
        } finally {
            cursor.close();
        }
        return allMetadata;
    }

    /**
     * Get all external GeoPackage metadata
     *
     * @return external GeoPackage metadata
     */
    public List<GeoPackageMetadata> getAllExternal() {
        List<GeoPackageMetadata> allMetadata = new ArrayList<GeoPackageMetadata>();

        String selection = GeoPackageMetadata.COLUMN_EXTERNAL_PATH + " IS NOT NULL";
        Cursor cursor = db.query(
                GeoPackageMetadata.TABLE_NAME,
                GeoPackageMetadata.COLUMNS, selection, null, null, null, null);

        try {
            while (cursor.moveToNext()) {
                GeoPackageMetadata metadata = createGeoPackageMetadata(cursor);
                allMetadata.add(metadata);
            }
        } finally {
            cursor.close();
        }
        return allMetadata;
    }

    /**
     * Get GeoPackage metadata by name
     *
     * @param database database name
     * @return GeoPackage metadata
     */
    public GeoPackageMetadata get(String database) {
        GeoPackageMetadata metadata = null;
        String selection = GeoPackageMetadata.COLUMN_NAME + " = ?";
        String[] selectionArgs = new String[]{database};
        Cursor cursor = db.query(
                GeoPackageMetadata.TABLE_NAME,
                GeoPackageMetadata.COLUMNS, selection, selectionArgs, null, null, null);
        try {
            if (cursor.moveToNext()) {
                metadata = createGeoPackageMetadata(cursor);
            }
        } finally {
            cursor.close();
        }
        return metadata;
    }

    /**
     * Get GeoPackage metadata by id
     *
     * @param id id
     * @return GeoPackage metadata
     */
    public GeoPackageMetadata get(long id) {
        GeoPackageMetadata metadata = null;
        String selection = GeoPackageMetadata.COLUMN_ID + " = ?";
        String[] selectionArgs = new String[]{String.valueOf(id)};
        Cursor cursor = db.query(
                GeoPackageMetadata.TABLE_NAME,
                GeoPackageMetadata.COLUMNS, selection, selectionArgs, null, null, null);
        try {
            if (cursor.moveToNext()) {
                metadata = createGeoPackageMetadata(cursor);
            }
        } finally {
            cursor.close();
        }
        return metadata;
    }

    /**
     * Get GeoPackage metadata or create it if it does not exist
     *
     * @param geoPackage GeoPackage name
     * @return GeoPackage metadata
     */
    public GeoPackageMetadata getOrCreate(String geoPackage) {
        GeoPackageMetadata metadata = get(geoPackage);

        if (metadata == null) {
            metadata = new GeoPackageMetadata();
            metadata.setName(geoPackage);
            create(metadata);
        }

        return metadata;
    }

    /**
     * Determine if the metadata exists
     *
     * @param database database name
     * @return exists flag
     */
    public boolean exists(String database) {
        return get(database) != null;
    }

    /**
     * Determine if the GeoPackage is external
     *
     * @param database database name
     * @return external flag
     */
    public boolean isExternal(String database) {
        GeoPackageMetadata metadata = get(database);
        return get(database) != null && metadata.getExternalPath() != null;
    }

    /**
     * Get external GeoPackage metadata by external path
     *
     * @param path path
     * @return GeoPackage metadata
     */
    public GeoPackageMetadata getExternalAtPath(String path) {
        GeoPackageMetadata metadata = null;
        String selection = GeoPackageMetadata.COLUMN_EXTERNAL_PATH + " = ?";
        String[] selectionArgs = new String[]{path};
        Cursor cursor = db.query(
                GeoPackageMetadata.TABLE_NAME,
                GeoPackageMetadata.COLUMNS, selection, selectionArgs, null, null, null);
        try {
            if (cursor.moveToNext()) {
                metadata = createGeoPackageMetadata(cursor);
            }
        } finally {
            cursor.close();
        }
        return metadata;
    }

    /**
     * Get metadata where the name is like
     *
     * @param like       like argument
     * @param sortColumn sort by column
     * @return metadata names
     * @since 1.2.7
     */
    public List<String> getMetadataWhereNameLike(String like, String sortColumn) {
        return getMetadataWhereNameLike(like, sortColumn, false);
    }

    /**
     * Get metadata where the name is not like
     *
     * @param notLike    not like argument
     * @param sortColumn sort by column
     * @return metadata names
     * @since 1.2.7
     */
    public List<String> getMetadataWhereNameNotLike(String notLike, String sortColumn) {
        return getMetadataWhereNameLike(notLike, sortColumn, true);
    }

    /**
     * Get metadata where the name is like or not like
     *
     * @param like       like or not like argument
     * @param sortColumn sort by column
     * @param notLike    true if a not like query
     * @return metadata names
     */
    private List<String> getMetadataWhereNameLike(String like, String sortColumn, boolean notLike) {
        List<String> names = new ArrayList<>();
        StringBuilder where = new StringBuilder(GeoPackageMetadata.COLUMN_NAME);
        if (notLike) {
            where.append(" not");
        }
        where.append(" like ?");
        String[] whereArgs = new String[]{like};
        Cursor cursor = db.query(GeoPackageMetadata.TABLE_NAME, new String[]{GeoPackageMetadata.COLUMN_NAME}, where.toString(), whereArgs, null, null, sortColumn);
        try {
            while (cursor.moveToNext()) {
                names.add(cursor.getString(0));
            }
        } finally {
            cursor.close();
        }
        return names;
    }

    /**
     * Create a GeoPackage metadata from the current cursor location
     *
     * @param cursor cursor
     * @return GeoPackage metadata
     */
    private GeoPackageMetadata createGeoPackageMetadata(Cursor cursor) {
        GeoPackageMetadata metadata = new GeoPackageMetadata();
        metadata.setId(cursor.getLong(0));
        metadata.setName(cursor.getString(1));
        metadata.setExternalPath(cursor.getString(2));
        return metadata;
    }

}


package mil.nga.geopackage.attributes;

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;

/**
 * Attributes Cursor to wrap a database cursor for attributes queries
 *
 * @author osbornb
 * @since 1.3.1
 */
public class AttributesCursor extends
        UserCursor<AttributesColumn, AttributesTable, AttributesRow> {

    /**
     * Constructor
     *
     * @param table  attributes table
     * @param cursor cursor
     */
    public AttributesCursor(AttributesTable table, Cursor cursor) {
        super(table, cursor);
    }

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

    /**
     * {@inheritDoc}
     */
    @Override
    protected UserInvalidCursor<AttributesColumn, AttributesTable, AttributesRow, ? extends UserCursor<AttributesColumn, AttributesTable, AttributesRow>, ? extends UserDao<AttributesColumn, AttributesTable, AttributesRow, ? extends UserCursor<AttributesColumn, AttributesTable, AttributesRow>>> createInvalidCursor(UserDao dao, UserCursor cursor, List<Integer> invalidPositions, List<AttributesColumn> blobColumns) {
        return new AttributesInvalidCursor((AttributesDao) dao, (AttributesCursor) cursor, invalidPositions, blobColumns);
    }

}


package mil.nga.geopackage.features.index;

import com.j256.ormlite.dao.CloseableIterator;

import java.util.Iterator;

import mil.nga.geopackage.extension.index.FeatureTableIndex;
import mil.nga.geopackage.extension.index.GeometryIndex;
import mil.nga.geopackage.features.user.FeatureRow;

/**
 * Iterable Feature Index Results to iterate on feature rows
 * retrieved from GeoPackage index extension results
 *
 * @author osbornb
 * @since 1.1.0
 */
class FeatureIndexGeoPackageResults implements FeatureIndexResults {

    /**
     * Feature Table Index, for indexing within a GeoPackage extension
     */
    private final FeatureTableIndex featureTableIndex;

    /**
     * Total count of the results
     */
    private final long count;

    /**
     * Iterator of Geometry Index results
     */
    private final CloseableIterator<GeometryIndex> geometryIndices;

    /**
     * Constructor
     *
     * @param featureTableIndex
     * @param count
     * @param geometryIndices
     */
    public FeatureIndexGeoPackageResults(FeatureTableIndex featureTableIndex, long count, CloseableIterator<GeometryIndex> geometryIndices) {
        this.featureTableIndex = featureTableIndex;
        this.count = count;
        this.geometryIndices = geometryIndices;
    }

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

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

            /**
             * {@inheritDoc}
             */
            @Override
            public FeatureRow next() {
                GeometryIndex geometryIndex = geometryIndices.next();
                FeatureRow featureRow = featureTableIndex.getFeatureRow(geometryIndex);
                return featureRow;
            }
        };
    }

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

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

    /**
     * {@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 geometryIndices.hasNext();
                    }

                    /**
                     * {@inheritDoc}
                     */
                    @Override
                    public Long next() {
                        return geometryIndices.next().getGeomId();
                    }

                };
            }
        };
    }

}