package mil.nga.geopackage.tiles.overlay;

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

/**
 * Represents a collection of rows from a feature table
 *
 * @author osbornb
 * @since 1.2.7
 */
public class FeatureTableData {

    /**
     * Table name
     */
    private String name;

    /**
     * Row count
     */
    private long count;

    /**
     * List of rows
     */
    private List<FeatureRowData> rows;

    /**
     * Constructor
     *
     * @param name  table name
     * @param count row count
     */
    public FeatureTableData(String name, long count) {
        this(name, count, null);
    }

    /**
     * Constructor
     *
     * @param name  table name
     * @param count row count
     * @param rows  feature rows
     */
    public FeatureTableData(String name, long count, List<FeatureRowData> rows) {
        this.name = name;
        this.count = count;
        this.rows = rows;
    }

    /**
     * Get the table name
     *
     * @return table name
     */
    public String getName() {
        return name;
    }

    /**
     * Get the row count
     *
     * @return row count
     */
    public long getCount() {
        return count;
    }

    /**
     * Get the feature rows
     *
     * @return feature rows
     */
    public List<FeatureRowData> getRows() {
        return rows;
    }

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

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

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

    /**
     * Build a JSON compatible object
     *
     * @param includePoints     true to include point geometries, ignored if includeGeometries is true
     * @param includeGeometries true to include all geometry types
     * @return JSON compatible object
     */
    public Object jsonCompatible(boolean includePoints, boolean includeGeometries) {
        Object jsonObject = null;
        if (rows == null || rows.isEmpty()) {
            jsonObject = count;
        } else {
            List<Object> jsonRows = new ArrayList<>();
            for (FeatureRowData row : rows) {
                jsonRows.add(row.jsonCompatible(includePoints, includeGeometries));
            }
            jsonObject = jsonRows;
        }

        return jsonObject;
    }

}


package mil.nga.geopackage.extension.style;

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

import mil.nga.geopackage.extension.related.UserMappingDao;
import mil.nga.geopackage.user.custom.UserCustomCursor;
import mil.nga.geopackage.user.custom.UserCustomDao;
import mil.nga.geopackage.user.custom.UserCustomRow;
import mil.nga.sf.GeometryType;

/**
 * Style Mapping DAO for reading style mapping data tables
 *
 * @author osbornb
 * @since 3.2.0
 */
public class StyleMappingDao extends UserMappingDao {

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

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

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

    /**
     * Get the style mapping row from the current result set location
     *
     * @param cursor cursor
     * @return style mapping row
     */
    public StyleMappingRow getRow(UserCustomCursor cursor) {
        return getRow(cursor.getRow());
    }

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

    /**
     * Query for style mappings by base id
     *
     * @param id base id, feature contents id or feature geometry id
     * @return style mappings rows
     */
    public List<StyleMappingRow> queryByBaseFeatureId(long id) {
        List<StyleMappingRow> rows = new ArrayList<>();
        UserCustomCursor cursor = queryByBaseId(id);
        try {
            while (cursor.moveToNext()) {
                rows.add(getRow(cursor));
            }
        } finally {
            cursor.close();
        }
        return rows;
    }

    /**
     * Delete by base is and geometry type
     *
     * @param id           base id
     * @param geometryType geometry type
     * @return rows deleted
     */
    public int deleteByBaseId(long id, GeometryType geometryType) {

        String geometryTypeName = null;
        if (geometryType != null) {
            geometryTypeName = geometryType.getName();
        }

        StringBuilder where = new StringBuilder();
        where.append(buildWhere(StyleMappingTable.COLUMN_BASE_ID, id));
        where.append(" AND ");
        where.append(buildWhere(StyleMappingTable.COLUMN_GEOMETRY_TYPE_NAME,
                geometryTypeName));

        List<Object> whereArguments = new ArrayList<>();
        whereArguments.add(id);
        if (geometryTypeName != null) {
            whereArguments.add(geometryTypeName);
        }

        String[] whereArgs = buildWhereArgs(whereArguments);

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

        return deleted;
    }

}


package mil.nga.geopackage.tiles;

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

import mil.nga.geopackage.BoundingBox;

/**
 * Tile Bounding Box utility methods relying on Android libraries
 *
 * @author osbornb
 */
public class TileBoundingBoxAndroidUtils {

    /**
     * Get a rectangle using the tile width, height, bounding box, and the
     * bounding box section within the outer box to build the rectangle from
     *
     * @param width              width
     * @param height             height
     * @param boundingBox        full bounding box
     * @param boundingBoxSection rectangle bounding box section
     * @return rectangle
     */
    public static Rect getRectangle(long width, long height,
                                    BoundingBox boundingBox, BoundingBox boundingBoxSection) {

        RectF rectF = getFloatRectangle(width, height, boundingBox,
                boundingBoxSection);

        Rect rect = new Rect(Math.round(rectF.left), Math.round(rectF.top),
                Math.round(rectF.right), Math.round(rectF.bottom));

        return rect;
    }

    /**
     * Get a rectangle with rounded floating point boundaries using the tile width,
     * height, bounding box, and the bounding box section within the outer box
     * to build the rectangle from
     *
     * @param width              width
     * @param height             height
     * @param boundingBox        full bounding box
     * @param boundingBoxSection rectangle bounding box section
     * @return floating point rectangle
     * @since 1.3.0
     */
    public static RectF getRoundedFloatRectangle(long width, long height,
                                                 BoundingBox boundingBox, BoundingBox boundingBoxSection) {

        Rect rect = getRectangle(width, height, boundingBox,
                boundingBoxSection);

        RectF rectF = new RectF(rect);

        return rectF;
    }

    /**
     * Get a rectangle with floating point boundaries using the tile width,
     * height, bounding box, and the bounding box section within the outer box
     * to build the rectangle from
     *
     * @param width              width
     * @param height             height
     * @param boundingBox        full bounding box
     * @param boundingBoxSection rectangle bounding box section
     * @return floating point rectangle
     */
    public static RectF getFloatRectangle(long width, long height,
                                          BoundingBox boundingBox, BoundingBox boundingBoxSection) {

        float left = TileBoundingBoxUtils.getXPixel(width, boundingBox,
                boundingBoxSection.getMinLongitude());
        float right = TileBoundingBoxUtils.getXPixel(width, boundingBox,
                boundingBoxSection.getMaxLongitude());
        float top = TileBoundingBoxUtils.getYPixel(height, boundingBox,
                boundingBoxSection.getMaxLatitude());
        float bottom = TileBoundingBoxUtils.getYPixel(height, boundingBox,
                boundingBoxSection.getMinLatitude());

        RectF rect = new RectF(left, top, right, bottom);

        return rect;
    }

    /**
     * Check if the rectangle is valid
     *
     * @param rect rectangle
     * @return true if valid
     * @since 1.3.1
     */
    public static boolean isValid(Rect rect) {
        return rect.left < rect.right && rect.top < rect.bottom;
    }

    /**
     * Check if the rectangle is valid allowing empty ranges
     *
     * @param rect rectangle
     * @return true if valid
     * @since 1.3.1
     */
    public static boolean isValidAllowEmpty(Rect rect) {
        return rect.left <= rect.right && rect.top <= rect.bottom;
    }

    /**
     * Check if the rectangle is valid
     *
     * @param rectF rectangle
     * @return true if valid
     * @since 1.3.1
     */
    public static boolean isValid(RectF rectF) {
        return rectF.left < rectF.right && rectF.top < rectF.bottom;
    }

    /**
     * Check if the rectangle is valid allowing empty ranges
     *
     * @param rectF rectangle
     * @return true if valid
     * @since 1.3.1
     */
    public static boolean isValidAllowEmpty(RectF rectF) {
        return rectF.left <= rectF.right && rectF.top <= rectF.bottom;
    }

}


package mil.nga.geopackage.factory;

import android.content.Context;
import android.content.res.Resources;
import android.database.DatabaseErrorHandler;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.util.Log;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import mil.nga.geopackage.GeoPackage;
import mil.nga.geopackage.GeoPackageConstants;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.GeoPackageManager;
import mil.nga.geopackage.R;
import mil.nga.geopackage.core.contents.Contents;
import mil.nga.geopackage.core.srs.SpatialReferenceSystem;
import mil.nga.geopackage.db.GeoPackageConnection;
import mil.nga.geopackage.db.GeoPackageDatabase;
import mil.nga.geopackage.db.GeoPackageTableCreator;
import mil.nga.geopackage.db.metadata.GeoPackageMetadata;
import mil.nga.geopackage.db.metadata.GeoPackageMetadataDataSource;
import mil.nga.geopackage.db.metadata.GeoPackageMetadataDb;
import mil.nga.geopackage.io.GeoPackageIOUtils;
import mil.nga.geopackage.io.GeoPackageProgress;
import mil.nga.geopackage.validate.GeoPackageValidate;
import mil.nga.sf.util.ByteReader;

/**
 * GeoPackage Database management implementation
 *
 * @author osbornb
 */
class GeoPackageManagerImpl implements GeoPackageManager {

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

    /**
     * Validate the database header of an imported database
     */
    private boolean importHeaderValidation;

    /**
     * Validate the database integrity of a imported database
     */
    private boolean importIntegrityValidation;

    /**
     * Validate the database header when opening a database
     */
    private boolean openHeaderValidation;

    /**
     * Validate the database integrity when opening a database
     */
    private boolean openIntegrityValidation;

    /**
     * Write ahead logging state for SQLite connections
     */
    private boolean sqliteWriteAheadLogging;

    /**
     * Constructor
     *
     * @param context
     */
    GeoPackageManagerImpl(Context context) {
        this.context = context;

        Resources resources = context.getResources();
        importHeaderValidation = resources.getBoolean(R.bool.manager_validation_import_header);
        importIntegrityValidation = resources.getBoolean(R.bool.manager_validation_import_integrity);
        openHeaderValidation = resources.getBoolean(R.bool.manager_validation_open_header);
        openIntegrityValidation = resources.getBoolean(R.bool.manager_validation_open_integrity);
        sqliteWriteAheadLogging = resources.getBoolean(R.bool.sqlite_write_ahead_logging);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<String> databases() {
        Set<String> sortedDatabases = new TreeSet<String>();
        addDatabases(sortedDatabases);
        List<String> databases = new ArrayList<String>();
        databases.addAll(sortedDatabases);
        return databases;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<String> databasesLike(String like) {
        List<String> databases = null;
        GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb(
                context);
        metadataDb.open();
        try {
            GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb);
            databases = dataSource.getMetadataWhereNameLike(like, GeoPackageMetadata.COLUMN_NAME);
        } finally {
            metadataDb.close();
        }

        databases = deleteMissingDatabases(databases);

        return databases;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<String> databasesNotLike(String notLike) {
        List<String> databases = null;
        GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb(
                context);
        metadataDb.open();
        try {
            GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb);
            databases = dataSource.getMetadataWhereNameNotLike(notLike, GeoPackageMetadata.COLUMN_NAME);
        } finally {
            metadataDb.close();
        }

        databases = deleteMissingDatabases(databases);

        return databases;
    }

    /**
     * Delete all databases that do not exist or the database file does not exist
     *
     * @param databases list of databases
     * @return databases that exist
     */
    private List<String> deleteMissingDatabases(List<String> databases) {
        List<String> filesExist = new ArrayList<>();
        for (String database : databases) {
            if (exists(database)) {
                filesExist.add(database);
            }
        }
        return filesExist;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<String> internalDatabases() {
        Set<String> sortedDatabases = new TreeSet<String>();
        addInternalDatabases(sortedDatabases);
        List<String> databases = new ArrayList<String>();
        databases.addAll(sortedDatabases);
        return databases;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<String> externalDatabases() {
        Set<String> sortedDatabases = new TreeSet<String>();
        addExternalDatabases(sortedDatabases);
        List<String> databases = new ArrayList<String>();
        databases.addAll(sortedDatabases);
        return databases;
    }

    /**
     * {@inheritDoc}
     */
    public int count() {
        return databaseSet().size();
    }

    /**
     * {@inheritDoc}
     */
    public int internalCount() {
        return internalDatabaseSet().size();
    }

    /**
     * {@inheritDoc}
     */
    public int externalCount() {
        return externalDatabaseSet().size();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Set<String> databaseSet() {
        Set<String> databases = new HashSet<String>();
        addDatabases(databases);
        return databases;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Set<String> internalDatabaseSet() {
        Set<String> databases = new HashSet<String>();
        addInternalDatabases(databases);
        return databases;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Set<String> externalDatabaseSet() {
        Set<String> databases = new HashSet<String>();
        addExternalDatabases(databases);
        return databases;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean exists(String database) {
        boolean exists = internalDatabaseSet().contains(database);

        if (!exists) {
            GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb(
                    context);
            metadataDb.open();
            try {
                GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb);
                GeoPackageMetadata metadata = dataSource.get(database);
                if (metadata != null) {
                    if (metadata.getExternalPath() != null && !new File(metadata.getExternalPath()).exists()) {
                        delete(database);
                    } else {
                        exists = true;
                    }
                }
            } finally {
                metadataDb.close();
            }
        }
        return exists;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public long size(String database) {
        File dbFile = getFile(database);
        long size = dbFile.length();
        return size;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isExternal(String database) {
        boolean external = false;
        GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb(
                context);
        metadataDb.open();
        try {
            GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb);
            external = dataSource.isExternal(database);
        } finally {
            metadataDb.close();
        }
        return external;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean existsAtExternalFile(File file) {
        return existsAtExternalPath(file.getAbsolutePath());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean existsAtExternalPath(String path) {
        GeoPackageMetadata metadata = getGeoPackageMetadataAtExternalPath(path);
        return metadata != null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getPath(String database) {
        File dbFile = getFile(database);
        String path = dbFile.getAbsolutePath();
        return path;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public File getFile(String database) {
        File dbFile = null;
        GeoPackageMetadata metadata = getGeoPackageMetadata(database);
        if (metadata != null && metadata.isExternal()) {
            dbFile = new File(metadata.getExternalPath());
        } else {
            dbFile = context.getDatabasePath(database);
        }

        if (dbFile == null || !dbFile.exists()) {
            throw new GeoPackageException("GeoPackage does not exist: "
                    + database);
        }

        return dbFile;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getDatabaseAtExternalFile(File file) {
        return getDatabaseAtExternalPath(file.getAbsolutePath());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getDatabaseAtExternalPath(String path) {
        String database = null;
        GeoPackageMetadata metadata = getGeoPackageMetadataAtExternalPath(path);
        if (metadata != null) {
            database = metadata.getName();
        }
        return database;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String readableSize(String database) {
        long size = size(database);
        return GeoPackageIOUtils.formatBytes(size);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean delete(String database) {
        boolean deleted = false;
        boolean external = isExternal(database);

        GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb(
                context);
        metadataDb.open();
        try {
            GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb);
            deleted = dataSource.delete(database);
        } finally {
            metadataDb.close();
        }

        if (!external) {
            deleted = context.deleteDatabase(database);
        }
        return deleted;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean deleteAll() {

        boolean deleted = true;

        for (String database : databaseSet()) {
            deleted = delete(database) && deleted;
        }

        return deleted;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean deleteAllExternal() {

        boolean deleted = true;

        for (String database : externalDatabaseSet()) {
            deleted = delete(database) && deleted;
        }

        return deleted;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean deleteAllMissingExternal() {

        boolean deleted = false;

        List<GeoPackageMetadata> externalGeoPackages = getExternalGeoPackages();
        for (GeoPackageMetadata external : externalGeoPackages) {
            if (!new File(external.getExternalPath()).exists()) {
                deleted = delete(external.getName()) || deleted;
            }
        }

        return deleted;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean create(String database) {

        boolean created = false;

        if (exists(database)) {
            throw new GeoPackageException("GeoPackage already exists: "
                    + database);
        } else {
            GeoPackageDatabase db = new GeoPackageDatabase(context.openOrCreateDatabase(database,
                    Context.MODE_PRIVATE, null));
            createAndCloseGeoPackage(db);
            GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb(
                    context);
            metadataDb.open();
            try {
                GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb);
                // Save in metadata
                GeoPackageMetadata metadata = new GeoPackageMetadata();
                metadata.setName(database);
                dataSource.create(metadata);
            } finally {
                metadataDb.close();
            }
            created = true;
        }

        return created;
    }

    /**
     * Create the required GeoPackage application id and tables in the newly created and open database connection.  Then close the connection.
     *
     * @param db
     */
    private void createAndCloseGeoPackage(GeoPackageDatabase db) {

        GeoPackageConnection connection = new GeoPackageConnection(db);

        // Set the GeoPackage application id and user version
        connection.setApplicationId();
        connection.setUserVersion();

        // Create the minimum required tables
        GeoPackageTableCreator tableCreator = new GeoPackageTableCreator(connection);
        tableCreator.createRequired();

        connection.close();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean createAtPath(String database, File path) {

        // Create the absolute file path
        File file = new File(path, database + "." + GeoPackageConstants.GEOPACKAGE_EXTENSION);

        // Create the GeoPackage
        boolean created = createFile(database, file);

        return created;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean createFile(File file) {

        // Get the database name
        String database = GeoPackageIOUtils.getFileNameWithoutExtension(file);

        // Create the GeoPackage
        boolean created = createFile(database, file);

        return created;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean createFile(String database, File file) {

        boolean created = false;

        if (exists(database)) {
            throw new GeoPackageException("GeoPackage already exists: "
                    + database);
        } else {

            // Check if the path is an absolute path to the GeoPackage file to create
            if (!GeoPackageValidate.hasGeoPackageExtension(file)) {

                // Make sure this isn't a path to another file extension
                if (GeoPackageIOUtils.getFileExtension(file) != null) {
                    throw new GeoPackageException("File can not have a non GeoPackage extension. Invalid File: "
                            + file.getAbsolutePath());
                }

                // Add the extension
                file = new File(file.getParentFile(), file.getName() + "." + GeoPackageConstants.GEOPACKAGE_EXTENSION);
            }

            // Make sure the file does not already exist
            if (file.exists()) {
                throw new GeoPackageException("GeoPackage file already exists: "
                        + file.getAbsolutePath());
            }

            // Create the new GeoPackage file
            GeoPackageDatabase db = new GeoPackageDatabase(SQLiteDatabase.openOrCreateDatabase(file, null));
            createAndCloseGeoPackage(db);

            // Import the GeoPackage
            created = importGeoPackageAsExternalLink(file, database);
        }
        return created;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean importGeoPackage(File file) {
        return importGeoPackage(null, file, false);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean importGeoPackage(File file, boolean override) {
        return importGeoPackage(null, file, override);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean importGeoPackage(String database, InputStream stream) {
        return importGeoPackage(database, stream, false, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean importGeoPackage(String database, InputStream stream, GeoPackageProgress progress) {
        return importGeoPackage(database, stream, false, progress);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean importGeoPackage(String database, InputStream stream,
                                    boolean override) {
        return importGeoPackage(database, stream, override, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean importGeoPackage(String database, InputStream stream,
                                    boolean override, GeoPackageProgress progress) {

        if (progress != null) {
            try {
                int streamLength = stream.available();
                if (streamLength > 0) {
                    progress.setMax(streamLength);
                }
            } catch (IOException e) {
                Log.w(GeoPackageManagerImpl.class.getSimpleName(), "Could not determine stream available size. Database: " + database, e);
            }
        }

        boolean success = importGeoPackage(database, override, stream, progress);
        return success;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean importGeoPackage(String name, File file) {
        return importGeoPackage(name, file, false);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean importGeoPackage(String name, File file, boolean override) {

        // Verify the file has the right extension
        GeoPackageValidate.validateGeoPackageExtension(file);

        // Use the provided name or the base file name as the database name
        String database;
        if (name != null) {
            database = name;
        } else {
            database = GeoPackageIOUtils.getFileNameWithoutExtension(file);
        }

        boolean success = false;
        try {
            FileInputStream geoPackageStream = new FileInputStream(file);
            success = importGeoPackage(database, override, geoPackageStream,
                    null);
        } catch (FileNotFoundException e) {
            throw new GeoPackageException(
                    "Failed read or write GeoPackage file '" + file
                            + "' to database: '" + database + "'", e);
        }

        return success;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean importGeoPackage(String name, URL url) {
        return importGeoPackage(name, url, false, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean importGeoPackage(String name, URL url,
                                    GeoPackageProgress progress) {
        return importGeoPackage(name, url, false, progress);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean importGeoPackage(String name, URL url, boolean override) {
        return importGeoPackage(name, url, override, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean importGeoPackage(String name, URL url, boolean override,
                                    GeoPackageProgress progress) {

        boolean success = false;

        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 import GeoPackage "
                        + name + " from URL: '" + url.toString() + "'. HTTP "
                        + connection.getResponseCode() + " "
                        + connection.getResponseMessage());
            }

            int fileLength = connection.getContentLength();
            if (fileLength != -1 && progress != null) {
                progress.setMax(fileLength);
            }

            InputStream geoPackageStream = connection.getInputStream();
            success = importGeoPackage(name, override, geoPackageStream,
                    progress);
        } catch (IOException e) {
            throw new GeoPackageException("Failed to import GeoPackage " + name
                    + " from URL: '" + url.toString() + "'", e);
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }

        return success;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void exportGeoPackage(String database, File directory) {
        exportGeoPackage(database, database, directory);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void exportGeoPackage(String database, String name, File directory) {

        File file = new File(directory, name);

        // Add the extension if not on the name
        if (!GeoPackageValidate.hasGeoPackageExtension(file)) {
            name += "." + GeoPackageConstants.GEOPACKAGE_EXTENSION;
            file = new File(directory, name);
        }

        // Copy the geopackage database to the new file location
        File dbFile = getFile(database);
        try {
            GeoPackageIOUtils.copyFile(dbFile, file);
        } catch (IOException e) {
            throw new GeoPackageException(
                    "Failed read or write GeoPackage database '" + database
                            + "' to file: '" + file, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public GeoPackage open(String database) {
        return open(database, true);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public GeoPackage open(String database, boolean writable) {

        GeoPackage db = null;

        if (exists(database)) {
            GeoPackageCursorFactory cursorFactory = new GeoPackageCursorFactory();
            String path = null;
            SQLiteDatabase sqlite = null;
            GeoPackageMetadata metadata = getGeoPackageMetadata(database);
            if (metadata != null && metadata.isExternal()) {
                path = metadata.getExternalPath();
                if (writable) {
                    try {
                        sqlite = SQLiteDatabase.openDatabase(path,
                                cursorFactory, SQLiteDatabase.OPEN_READWRITE
                                        | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
                    } catch (Exception e) {
                        Log.e(GeoPackageManagerImpl.class.getSimpleName(), "Failed to open database as writable: " + database, e);
                    }
                }
                if (sqlite == null) {
                    sqlite = SQLiteDatabase.openDatabase(path,
                            cursorFactory, SQLiteDatabase.OPEN_READONLY
                                    | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
                    writable = false;
                }
            } else {
                sqlite = context.openOrCreateDatabase(database,
                        Context.MODE_PRIVATE, cursorFactory);
            }
            if(sqliteWriteAheadLogging){
                sqlite.enableWriteAheadLogging();
            }else {
                sqlite.disableWriteAheadLogging();
            }

            // Validate the database if validation is enabled
            validateDatabaseAndCloseOnError(sqlite, openHeaderValidation, openIntegrityValidation);

            GeoPackageConnection connection = new GeoPackageConnection(new GeoPackageDatabase(sqlite));
            GeoPackageTableCreator tableCreator = new GeoPackageTableCreator(connection);
            db = new GeoPackageImpl(context, database, path, connection, cursorFactory, tableCreator, writable);

            // Validate the GeoPackage has the minimum required tables
            try {
                GeoPackageValidate.validateMinimumTables(db);
            } catch (RuntimeException e) {
                db.close();
                throw e;
            }
        }

        return db;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isImportHeaderValidation() {
        return importHeaderValidation;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setImportHeaderValidation(boolean enabled) {
        this.importHeaderValidation = enabled;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isImportIntegrityValidation() {
        return importIntegrityValidation;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setImportIntegrityValidation(boolean enabled) {
        this.importIntegrityValidation = enabled;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isOpenHeaderValidation() {
        return openHeaderValidation;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setOpenHeaderValidation(boolean enabled) {
        this.openHeaderValidation = enabled;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isOpenIntegrityValidation() {
        return openIntegrityValidation;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setOpenIntegrityValidation(boolean enabled) {
        this.openIntegrityValidation = enabled;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isSqliteWriteAheadLogging(){
        return sqliteWriteAheadLogging;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setSqliteWriteAheadLogging(boolean enabled){
        this.sqliteWriteAheadLogging = enabled;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean validate(String database) {
        boolean valid = isValid(database, true, true);
        return valid;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean validateHeader(String database) {
        boolean valid = isValid(database, true, false);
        return valid;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean validateIntegrity(String database) {
        boolean valid = isValid(database, false, true);
        return valid;
    }

    /**
     * Validate the GeoPackage database
     *
     * @param database
     * @param validateHeader    true to validate the header of the database
     * @param validateIntegrity true to validate the integrity of the database
     * @return true if valid
     */
    private boolean isValid(String database, boolean validateHeader, boolean validateIntegrity) {

        boolean valid = false;

        if (exists(database)) {
            GeoPackageCursorFactory cursorFactory = new GeoPackageCursorFactory();
            String path = null;
            SQLiteDatabase sqlite;
            GeoPackageMetadata metadata = getGeoPackageMetadata(database);
            if (metadata != null && metadata.isExternal()) {
                path = metadata.getExternalPath();
                try {
                    sqlite = SQLiteDatabase.openDatabase(path,
                            cursorFactory, SQLiteDatabase.OPEN_READWRITE
                                    | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
                } catch (Exception e) {
                    sqlite = SQLiteDatabase.openDatabase(path,
                            cursorFactory, SQLiteDatabase.OPEN_READONLY
                                    | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
                }
            } else {
                path = context.getDatabasePath(database).getAbsolutePath();
                sqlite = context.openOrCreateDatabase(database,
                        Context.MODE_PRIVATE, cursorFactory);
            }

            try {
                valid = (!validateHeader || isDatabaseHeaderValid(sqlite))
                        && (!validateIntegrity || sqlite.isDatabaseIntegrityOk());
            } catch (Exception e) {
                Log.e(GeoPackageManagerImpl.class.getSimpleName(), "Failed to validate database", e);
            } finally {
                sqlite.close();
            }
        }

        return valid;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean copy(String database, String databaseCopy) {
        // Copy the database as a new file
        File dbFile = getFile(database);
        File dbCopyFile = context.getDatabasePath(databaseCopy);
        try {
            GeoPackageIOUtils.copyFile(dbFile, dbCopyFile);
        } catch (IOException e) {
            throw new GeoPackageException(
                    "Failed to copy GeoPackage database '" + database
                            + "' to '" + databaseCopy + "'", e);
        }

        return exists(databaseCopy);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean rename(String database, String newDatabase) {
        GeoPackageMetadata metadata = getGeoPackageMetadata(database);
        if (metadata != null) {

            GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb(
                    context);
            metadataDb.open();
            try {
                GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb);
                dataSource.rename(metadata, newDatabase);
            } finally {
                metadataDb.close();
            }
        }

        if ((metadata == null || !metadata.isExternal()) && copy(database, newDatabase)) {
            delete(database);
        }
        return exists(newDatabase);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean importGeoPackageAsExternalLink(File path, String database) {
        return importGeoPackageAsExternalLink(path, database, false);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean importGeoPackageAsExternalLink(File path, String database, boolean override) {
        return importGeoPackageAsExternalLink(path.getAbsolutePath(), database, override);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean importGeoPackageAsExternalLink(String path, String database) {
        return importGeoPackageAsExternalLink(path, database, false);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean importGeoPackageAsExternalLink(String path, String database, boolean override) {

        if (exists(database)) {
            if (override) {
                if (!delete(database)) {
                    throw new GeoPackageException(
                            "Failed to delete existing database: " + database);
                }
            } else {
                throw new GeoPackageException(
                        "GeoPackage database already exists: " + database);
            }
        }

        // Verify the file is a database and can be opened
        try {
            SQLiteDatabase sqlite = SQLiteDatabase.openDatabase(path, null,
                    SQLiteDatabase.OPEN_READONLY
                            | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
            validateDatabaseAndClose(sqlite, importHeaderValidation, importIntegrityValidation);
        } catch (SQLiteException e) {
            throw new GeoPackageException(
                    "Failed to import GeoPackage database as external link: "
                            + database + ", Path: " + path, e);
        }

        GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb(
                context);
        metadataDb.open();
        try {
            GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb);
            // Save the external link in metadata
            GeoPackageMetadata metadata = new GeoPackageMetadata();
            metadata.setName(database);
            metadata.setExternalPath(path);
            dataSource.create(metadata);

            GeoPackage geoPackage = open(database, false);
            if (geoPackage != null) {
                try {
                    GeoPackageValidate.validateMinimumTables(geoPackage);
                } catch (RuntimeException e) {
                    dataSource.delete(database);
                    throw e;
                } finally {
                    geoPackage.close();
                }
            } else {
                dataSource.delete(database);
                throw new GeoPackageException(
                        "Unable to open GeoPackage database. Database: "
                                + database);
            }
        } finally {
            metadataDb.close();
        }

        return exists(database);
    }

    /**
     * Validate the database and close when validation fails. Throw an error when not valid.
     *
     * @param sqliteDatabase    database
     * @param validateHeader    validate the header
     * @param validateIntegrity validate the integrity
     */
    private void validateDatabaseAndCloseOnError(SQLiteDatabase sqliteDatabase, boolean validateHeader, boolean validateIntegrity) {
        validateDatabase(sqliteDatabase, validateHeader, validateIntegrity, false, true);
    }

    /**
     * Validate the database and close it. Throw an error when not valid.
     *
     * @param sqliteDatabase    database
     * @param validateHeader    validate the header
     * @param validateIntegrity validate the integrity
     */
    private void validateDatabaseAndClose(SQLiteDatabase sqliteDatabase, boolean validateHeader, boolean validateIntegrity) {
        validateDatabase(sqliteDatabase, validateHeader, validateIntegrity, true, true);
    }

    /**
     * Validate the database header and integrity.  Throw an error when not valid.
     *
     * @param sqliteDatabase    database
     * @param validateHeader    validate the header
     * @param validateIntegrity validate the integrity
     * @param close             close the database after validation
     * @param closeOnError      close the database if validation fails
     */
    private void validateDatabase(SQLiteDatabase sqliteDatabase, boolean validateHeader, boolean validateIntegrity, boolean close, boolean closeOnError) {
        try {
            if (validateHeader) {
                validateDatabaseHeader(sqliteDatabase);
            }
            if (validateIntegrity) {
                validateDatabaseIntegrity(sqliteDatabase);
            }
        } catch (Exception e) {
            if (closeOnError) {
                sqliteDatabase.close();
            }
            throw e;
        }

        if (close) {
            sqliteDatabase.close();
        }
    }

    /**
     * Validate the header of the database file to verify it is a sqlite database
     *
     * @param sqliteDatabase database
     */
    private void validateDatabaseHeader(SQLiteDatabase sqliteDatabase) {

        boolean validHeader = isDatabaseHeaderValid(sqliteDatabase);
        if (!validHeader) {
            throw new GeoPackageException(
                    "GeoPackage SQLite header is not valid: " + sqliteDatabase.getPath());
        }
    }

    /**
     * Determine if the header of the database file is valid
     *
     * @param sqliteDatabase database
     * @return true if valid
     */
    private boolean isDatabaseHeaderValid(SQLiteDatabase sqliteDatabase) {

        boolean validHeader = false;
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(sqliteDatabase.getPath());
            byte[] headerBytes = new byte[16];
            if (fis.read(headerBytes) == 16) {
                ByteReader byteReader = new ByteReader(headerBytes);
                String header = byteReader.readString(headerBytes.length);
                String headerPrefix = header.substring(0, GeoPackageConstants.SQLITE_HEADER_PREFIX.length());
                validHeader = headerPrefix.equalsIgnoreCase(GeoPackageConstants.SQLITE_HEADER_PREFIX);
            }
        } catch (Exception e) {
            Log.e(GeoPackageManagerImpl.class.getSimpleName(), "Failed to retrieve database header", e);
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    // eat
                }
            }
        }

        return validHeader;
    }

    /**
     * Validate the integrity of the database
     *
     * @param sqliteDatabase database
     */
    private void validateDatabaseIntegrity(SQLiteDatabase sqliteDatabase) {

        if (!sqliteDatabase.isDatabaseIntegrityOk()) {
            throw new GeoPackageException(
                    "GeoPackage SQLite file integrity failed: " + sqliteDatabase.getPath());
        }
    }

    /**
     * Add all databases to the collection
     *
     * @param databases
     */
    private void addDatabases(Collection<String> databases) {

        // Add the internal databases
        addInternalDatabases(databases);

        // Add the external databases
        addExternalDatabases(databases);
    }

    /**
     * Add all internal databases to the collection
     *
     * @param databases
     */
    private void addInternalDatabases(Collection<String> databases) {
        String[] databaseArray = context.databaseList();
        for (String database : databaseArray) {
            if (!isTemporary(database)
                    && !database
                    .equalsIgnoreCase(GeoPackageMetadataDb.DATABASE_NAME)) {
                databases.add(database);
            }
        }
    }

    /**
     * Add all external databases to the collection
     *
     * @param databases
     */
    private void addExternalDatabases(Collection<String> databases) {
        // Get the external GeoPackages, adding those where the file exists and
        // deleting those with missing files
        List<GeoPackageMetadata> externalGeoPackages = getExternalGeoPackages();
        for (GeoPackageMetadata external : externalGeoPackages) {
            if (new File(external.getExternalPath()).exists()) {
                databases.add(external.getName());
            } else {
                delete(external.getName());
            }
        }
    }

    /**
     * Import the GeoPackage stream
     *
     * @param database
     * @param override
     * @param geoPackageStream
     * @param progress
     * @return true if imported successfully
     */
    private boolean importGeoPackage(String database, boolean override,
                                     InputStream geoPackageStream, GeoPackageProgress progress) {

        try {

            if (exists(database)) {
                if (override) {
                    if (!delete(database)) {
                        throw new GeoPackageException(
                                "Failed to delete existing database: " + database);
                    }
                } else {
                    throw new GeoPackageException(
                            "GeoPackage database already exists: " + database);
                }
            }

            // Copy the geopackage over as a database
            File newDbFile = context.getDatabasePath(database);
            try {
                SQLiteDatabase db = context.openOrCreateDatabase(database,
                        Context.MODE_PRIVATE, null);
                db.close();
                GeoPackageIOUtils.copyStream(geoPackageStream, newDbFile, progress);
            } catch (IOException e) {
                throw new GeoPackageException(
                        "Failed to import GeoPackage database: " + database, e);
            }

        } finally {
            GeoPackageIOUtils.closeQuietly(geoPackageStream);
        }

        if (progress == null || progress.isActive()) {

            // Verify that the database is valid
            try {
                SQLiteDatabase sqlite = context.openOrCreateDatabase(database,
                        Context.MODE_PRIVATE, null, new DatabaseErrorHandler() {
                            @Override
                            public void onCorruption(SQLiteDatabase dbObj) {
                            }
                        });
                validateDatabaseAndClose(sqlite, importHeaderValidation, importIntegrityValidation);

                GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb(
                        context);
                metadataDb.open();
                try {
                    GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb);
                    // Save in metadata
                    GeoPackageMetadata metadata = new GeoPackageMetadata();
                    metadata.setName(database);
                    dataSource.create(metadata);
                } finally {
                    metadataDb.close();
                }
            } catch (Exception e) {
                delete(database);
                throw new GeoPackageException(
                        "Invalid GeoPackage database file", e);
            }

            GeoPackage geoPackage = open(database, false);
            if (geoPackage != null) {
                try {
                    if (!geoPackage.getSpatialReferenceSystemDao()
                            .isTableExists()
                            || !geoPackage.getContentsDao().isTableExists()) {
                        delete(database);
                        throw new GeoPackageException(
                                "Invalid GeoPackage database file. Does not contain required tables: "
                                        + SpatialReferenceSystem.TABLE_NAME
                                        + " & " + Contents.TABLE_NAME
                                        + ", Database: " + database);
                    }
                } catch (SQLException e) {
                    delete(database);
                    throw new GeoPackageException(
                            "Invalid GeoPackage database file. Could not verify existence of required tables: "
                                    + SpatialReferenceSystem.TABLE_NAME
                                    + " & "
                                    + Contents.TABLE_NAME
                                    + ", Database: "
                                    + database);
                } finally {
                    geoPackage.close();
                }
            } else {
                delete(database);
                throw new GeoPackageException(
                        "Unable to open GeoPackage database. Database: "
                                + database);
            }
        }

        return exists(database);
    }

    /**
     * Get all external GeoPackage metadata
     *
     * @return
     */
    private List<GeoPackageMetadata> getExternalGeoPackages() {
        List<GeoPackageMetadata> metadata = null;

        GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb(
                context);
        metadataDb.open();
        try {
            GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb);
            metadata = dataSource.getAllExternal();
        } finally {
            metadataDb.close();
        }

        return metadata;
    }

    /**
     * Get the GeoPackage metadata
     *
     * @param database
     * @return
     */
    private GeoPackageMetadata getGeoPackageMetadata(String database) {
        GeoPackageMetadata metadata = null;

        GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb(
                context);
        metadataDb.open();
        try {
            GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb);
            metadata = dataSource.get(database);
        } finally {
            metadataDb.close();
        }

        return metadata;
    }

    /**
     * Get the GeoPackage metadata of the database at the external path
     *
     * @param path external database path
     * @return metadata or null
     */
    private GeoPackageMetadata getGeoPackageMetadataAtExternalPath(String path) {
        GeoPackageMetadata metadata = null;

        GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb(
                context);
        metadataDb.open();
        try {
            GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb);
            metadata = dataSource.getExternalAtPath(path);
        } finally {
            metadataDb.close();
        }

        return metadata;
    }

    /**
     * Check if the database is temporary (rollback journal)
     *
     * @param database
     * @return
     */
    private boolean isTemporary(String database) {
        return database.endsWith(context.getString(R.string.geopackage_db_rollback_journal_suffix))
                || database.endsWith(context.getString(R.string.geopackage_db_write_ahead_log_suffix))
                || database.endsWith(context.getString(R.string.geopackage_db_shared_memory_suffix));
    }

}


package mil.nga.geopackage.db.metadata;

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

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.db.GeoPackageDatabase;
import mil.nga.sf.GeometryEnvelope;

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

    /**
     * Database
     */
    private GeoPackageDatabase db;

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

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

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

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

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

    /**
     * Create a new geometry metadata
     *
     * @param metadata geometry metadata
     * @return id
     */
    public long create(GeometryMetadata metadata) {
        ContentValues values = new ContentValues();
        values.put(GeometryMetadata.COLUMN_GEOPACKAGE_ID, metadata.getGeoPackageId());
        values.put(GeometryMetadata.COLUMN_TABLE_NAME, metadata.getTableName());
        values.put(GeometryMetadata.COLUMN_ID, metadata.getId());
        values.put(GeometryMetadata.COLUMN_MIN_X, metadata.getMinX());
        values.put(GeometryMetadata.COLUMN_MAX_X, metadata.getMaxX());
        values.put(GeometryMetadata.COLUMN_MIN_Y, metadata.getMinY());
        values.put(GeometryMetadata.COLUMN_MAX_Y, metadata.getMaxY());
        values.put(GeometryMetadata.COLUMN_MIN_Z, metadata.getMinZ());
        values.put(GeometryMetadata.COLUMN_MAX_Z, metadata.getMaxZ());
        values.put(GeometryMetadata.COLUMN_MIN_M, metadata.getMinM());
        values.put(GeometryMetadata.COLUMN_MAX_M, metadata.getMaxM());
        long insertId = db.insert(
                GeometryMetadata.TABLE_NAME, null,
                values);
        if (insertId == -1) {
            throw new GeoPackageException(
                    "Failed to insert geometry metadata. GeoPackage Id: "
                            + metadata.getGeoPackageId() + ", Table Name: "
                            + metadata.getTableName() + ", Geometry Id: "
                            + metadata.getId());
        }
        metadata.setId(insertId);
        return insertId;
    }

    /**
     * Create a new geometry metadata from an envelope
     *
     * @param geoPackage GeoPackage name
     * @param tableName  table name
     * @param geomId     geometry id
     * @param envelope   geometry envelope
     * @return geometry metadata
     */
    public GeometryMetadata create(String geoPackage, String tableName, long geomId, GeometryEnvelope envelope) {
        return create(getGeoPackageId(geoPackage), tableName, geomId, envelope);
    }

    /**
     * Create a new geometry metadata from an envelope
     *
     * @param geoPackageId GeoPackage id
     * @param tableName    table name
     * @param geomId       geometry id
     * @param envelope     geometry envelope
     * @return geometry metadata
     */
    public GeometryMetadata create(long geoPackageId, String tableName, long geomId, GeometryEnvelope envelope) {

        GeometryMetadata metadata = populate(geoPackageId, tableName, geomId, envelope);
        create(metadata);
        return metadata;
    }

    /**
     * Populate a new geometry metadata from an envelope
     *
     * @param geoPackageId GeoPackage id
     * @param tableName    table name
     * @param geomId       geometry id
     * @param envelope     geometry envelope
     * @return geometry metadata
     */
    public GeometryMetadata populate(long geoPackageId, String tableName, long geomId, GeometryEnvelope envelope) {

        GeometryMetadata metadata = new GeometryMetadata();
        metadata.setGeoPackageId(geoPackageId);
        metadata.setTableName(tableName);
        metadata.setId(geomId);
        metadata.setMinX(envelope.getMinX());
        metadata.setMaxX(envelope.getMaxX());
        metadata.setMinY(envelope.getMinY());
        metadata.setMaxY(envelope.getMaxY());
        if (envelope.hasZ()) {
            metadata.setMinZ(envelope.getMinZ());
            metadata.setMaxZ(envelope.getMaxZ());
        }
        if (envelope.hasM()) {
            metadata.setMinM(envelope.getMinM());
            metadata.setMaxM(envelope.getMaxM());
        }
        return metadata;
    }

    /**
     * Delete the geometry metadata
     *
     * @param metadata geometry metadata
     * @return deleted count
     */
    public boolean delete(GeometryMetadata metadata) {
        return delete(metadata.getGeoPackageId(), metadata.getTableName(), metadata.getId());
    }

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

    /**
     * Delete geometry metadata by database
     *
     * @param geoPackageId GeoPackage id
     * @return deleted count
     */
    public int delete(long geoPackageId) {
        String whereClause = GeometryMetadata.COLUMN_GEOPACKAGE_ID + " = ?";
        String[] whereArgs = new String[]{String.valueOf(geoPackageId)};
        int deleteCount = db.delete(
                GeometryMetadata.TABLE_NAME,
                whereClause, whereArgs);
        return deleteCount;
    }

    /**
     * Delete geometry metadata by table name
     *
     * @param geoPackage GeoPackage name
     * @param tableName  table name
     * @return deleted count
     */
    public int delete(String geoPackage, String tableName) {
        return delete(getGeoPackageId(geoPackage), tableName);
    }

    /**
     * Delete geometry metadata by table name
     *
     * @param geoPackageId GeoPackage id
     * @param tableName    table name
     * @return deleted count
     */
    public int delete(long geoPackageId, String tableName) {
        String whereClause = GeometryMetadata.COLUMN_GEOPACKAGE_ID
                + " = ? AND " + GeometryMetadata.COLUMN_TABLE_NAME + " = ?";
        String[] whereArgs = new String[]{String.valueOf(geoPackageId), tableName};
        int deleteCount = db.delete(
                GeometryMetadata.TABLE_NAME,
                whereClause, whereArgs);
        return deleteCount;
    }

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

    /**
     * Delete the geometry metadata
     *
     * @param geoPackageId GeoPackage id
     * @param tableName    table name
     * @param id           id
     * @return deleted flag
     */
    public boolean delete(long geoPackageId, String tableName, long id) {
        String whereClause = GeometryMetadata.COLUMN_GEOPACKAGE_ID
                + " = ? AND " + GeometryMetadata.COLUMN_TABLE_NAME + " = ? AND "
                + GeometryMetadata.COLUMN_ID + " = ?";
        String[] whereArgs = new String[]{String.valueOf(geoPackageId), tableName, String.valueOf(id)};
        int deleteCount = db.delete(
                GeometryMetadata.TABLE_NAME,
                whereClause, whereArgs);
        return deleteCount > 0;
    }

    /**
     * Create the geometry metadata or update if it already exists
     *
     * @param metadata geometry metadata
     * @return created flag
     */
    public boolean createOrUpdate(GeometryMetadata metadata) {

        boolean success = false;

        if (exists(metadata)) {
            success = update(metadata);
        } else {
            create(metadata);
            success = true;
        }

        return success;
    }

    /**
     * Update the geometry metadata
     *
     * @param metadata geometry metadata
     * @return updated flag
     */
    public boolean update(GeometryMetadata metadata) {
        String whereClause = GeometryMetadata.COLUMN_GEOPACKAGE_ID
                + " = ? AND " + GeometryMetadata.COLUMN_TABLE_NAME + " = ? AND "
                + GeometryMetadata.COLUMN_ID + " = ?";
        String[] whereArgs = new String[]{String.valueOf(metadata.getGeoPackageId()), metadata.getTableName(), String.valueOf(metadata.getId())};
        ContentValues values = new ContentValues();
        values.put(GeometryMetadata.COLUMN_MIN_X, metadata.getMinX());
        values.put(GeometryMetadata.COLUMN_MAX_X, metadata.getMaxX());
        values.put(GeometryMetadata.COLUMN_MIN_Y, metadata.getMinY());
        values.put(GeometryMetadata.COLUMN_MAX_Y, metadata.getMaxY());
        values.put(GeometryMetadata.COLUMN_MIN_Z, metadata.getMinZ());
        values.put(GeometryMetadata.COLUMN_MAX_Z, metadata.getMaxZ());
        values.put(GeometryMetadata.COLUMN_MIN_M, metadata.getMinM());
        values.put(GeometryMetadata.COLUMN_MAX_M, metadata.getMaxM());
        int updateCount = db.update(
                GeometryMetadata.TABLE_NAME, values,
                whereClause, whereArgs);
        return updateCount > 0;
    }

    /**
     * Check if a table metadata exists
     *
     * @param metadata geometry metadata
     * @return exists flag
     */
    public boolean exists(GeometryMetadata metadata) {
        return get(metadata) != null;
    }

    /**
     * Get a table metadata
     *
     * @param metadata geometry metadata
     * @return geometry metadata
     */
    public GeometryMetadata get(GeometryMetadata metadata) {
        return get(metadata.getGeoPackageId(), metadata.getTableName(), metadata.getId());
    }

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

    /**
     * Get a table metadata
     *
     * @param geoPackageId GeoPackage id
     * @param tableName    table name
     * @param id           id
     * @return geometry metadata
     */
    public GeometryMetadata get(long geoPackageId, String tableName, long id) {
        String selection = GeometryMetadata.COLUMN_GEOPACKAGE_ID
                + " = ? AND " + GeometryMetadata.COLUMN_TABLE_NAME + " = ? AND "
                + GeometryMetadata.COLUMN_ID + " = ?";
        String[] selectionArgs = new String[]{String.valueOf(geoPackageId), tableName, String.valueOf(id)};
        Cursor cursor = db.query(
                GeometryMetadata.TABLE_NAME,
                GeometryMetadata.COLUMNS, selection, selectionArgs, null, null, null);
        GeometryMetadata metadata = null;
        try {
            if (cursor.moveToNext()) {
                metadata = createGeometryMetadata(cursor);
            }
        } finally {
            cursor.close();
        }
        return metadata;
    }

    /**
     * Query for all table geometry metadata
     *
     * @param geoPackage GeoPackage name
     * @param tableName  table name
     * @return cursor that must be closed
     */
    public Cursor query(String geoPackage, String tableName) {
        return query(getGeoPackageId(geoPackage), tableName);
    }

    /**
     * Query for all table geometry metadata count
     *
     * @param geoPackage GeoPackage name
     * @param tableName  table name
     * @return count
     * @since 1.1.0
     */
    public int count(String geoPackage, String tableName) {
        return count(getGeoPackageId(geoPackage), tableName);
    }

    /**
     * Query for the bounds of the feature table index
     *
     * @param geoPackage GeoPackage name
     * @param tableName  table name
     * @return bounding box
     * @since 3.1.0
     */
    public BoundingBox getBoundingBox(String geoPackage, String tableName) {
        return getBoundingBox(getGeoPackageId(geoPackage), tableName);
    }

    /**
     * Query for the bounds of the feature table index
     *
     * @param geoPackageId GeoPackage id
     * @param tableName    table name
     * @return bounding box
     * @since 3.1.0
     */
    public BoundingBox getBoundingBox(long geoPackageId, String tableName) {

        BoundingBox boundingBox = null;

        Cursor result = db.rawQuery("SELECT MIN(" + GeometryMetadata.COLUMN_MIN_X + "), MIN("
                        + GeometryMetadata.COLUMN_MIN_Y + "), MAX("
                        + GeometryMetadata.COLUMN_MAX_X + "), MAX("
                        + GeometryMetadata.COLUMN_MAX_Y + ") FROM "
                        + GeometryMetadata.TABLE_NAME
                        + " WHERE " + GeometryMetadata.COLUMN_GEOPACKAGE_ID + " = ? AND "
                        + GeometryMetadata.COLUMN_TABLE_NAME + " = ?",
                new String[]{String.valueOf(geoPackageId), tableName});
        try {
            if (result.moveToNext()) {
                boundingBox = new BoundingBox(result.getDouble(0),
                        result.getDouble(1), result.getDouble(2),
                        result.getDouble(3));
            }
        } finally {
            result.close();
        }

        return boundingBox;
    }

    /**
     * Query for all table geometry metadata
     *
     * @param geoPackageId GeoPackage id
     * @param tableName    table name
     * @return cursor that must be closed
     */
    public Cursor query(long geoPackageId, String tableName) {
        String selection = GeometryMetadata.COLUMN_GEOPACKAGE_ID
                + " = ? AND " + GeometryMetadata.COLUMN_TABLE_NAME + " = ?";
        String[] selectionArgs = new String[]{String.valueOf(geoPackageId), tableName};
        Cursor cursor = db.query(
                GeometryMetadata.TABLE_NAME,
                GeometryMetadata.COLUMNS, selection, selectionArgs, null, null, null);
        return cursor;
    }

    /**
     * Query for all table geometry metadata count
     *
     * @param geoPackageId GeoPackage id
     * @param tableName    table name
     * @return count
     * @since 1.1.0
     */
    public int count(long geoPackageId, String tableName) {
        Cursor cursor = query(geoPackageId, tableName);
        int count = cursor.getCount();
        cursor.close();
        return count;
    }

    /**
     * Query for all table geometry metadata matching the bounding box in the same projection
     *
     * @param geoPackage  GeoPackage name
     * @param tableName   table name
     * @param boundingBox bounding box
     * @return cursor that must be closed
     */
    public Cursor query(String geoPackage, String tableName, BoundingBox boundingBox) {
        return query(getGeoPackageId(geoPackage), tableName, boundingBox);
    }

    /**
     * Query for all table geometry metadata count matching the bounding box in the same projection
     *
     * @param geoPackage  GeoPackage name
     * @param tableName   table name
     * @param boundingBox bounding box
     * @return count
     * @since 1.1.0
     */
    public int count(String geoPackage, String tableName, BoundingBox boundingBox) {
        return count(getGeoPackageId(geoPackage), tableName, boundingBox);
    }

    /**
     * Query for all table geometry metadata matching the bounding box in the same projection
     *
     * @param geoPackageId GeoPackage id
     * @param tableName    table name
     * @param boundingBox  bounding box
     * @return cursor that must be closed
     */
    public Cursor query(long geoPackageId, String tableName, BoundingBox boundingBox) {
        GeometryEnvelope envelope = new GeometryEnvelope();
        envelope.setMinX(boundingBox.getMinLongitude());
        envelope.setMaxX(boundingBox.getMaxLongitude());
        envelope.setMinY(boundingBox.getMinLatitude());
        envelope.setMaxY(boundingBox.getMaxLatitude());
        return query(geoPackageId, tableName, envelope);
    }

    /**
     * Query for all table geometry metadata count matching the bounding box in the same projection
     *
     * @param geoPackageId GeoPackage id
     * @param tableName    table name
     * @param boundingBox  bounding box
     * @return count
     * @since 1.1.0
     */
    public int count(long geoPackageId, String tableName, BoundingBox boundingBox) {
        Cursor cursor = query(geoPackageId, tableName, boundingBox);
        int count = cursor.getCount();
        cursor.close();
        return count;
    }

    /**
     * Query for all table geometry metadata matching the envelope
     *
     * @param geoPackage GeoPackage name
     * @param tableName  table name
     * @param envelope   geometry envelope
     * @return cursor that must be closed
     */
    public Cursor query(String geoPackage, String tableName, GeometryEnvelope envelope) {
        return query(getGeoPackageId(geoPackage), tableName, envelope);
    }

    /**
     * Query for all table geometry metadata count matching the envelope
     *
     * @param geoPackage GeoPackage name
     * @param tableName  table name
     * @param envelope   geometry envelope
     * @return count
     * @since 1.1.0
     */
    public int count(String geoPackage, String tableName, GeometryEnvelope envelope) {
        return count(getGeoPackageId(geoPackage), tableName, envelope);
    }

    /**
     * Query for all table geometry metadata matching the envelope
     *
     * @param geoPackageId GeoPackage id
     * @param tableName    table name
     * @param envelope     geometry envelope
     * @return cursor that must be closed
     */
    public Cursor query(long geoPackageId, String tableName, GeometryEnvelope envelope) {
        StringBuilder selection = new StringBuilder();
        selection.append(GeometryMetadata.COLUMN_GEOPACKAGE_ID).append(" = ? AND ")
                .append(GeometryMetadata.COLUMN_TABLE_NAME).append(" = ?");
        selection.append(" AND ").append(GeometryMetadata.COLUMN_MIN_X).append(" <= ?");
        selection.append(" AND ").append(GeometryMetadata.COLUMN_MAX_X).append(" >= ?");
        selection.append(" AND ").append(GeometryMetadata.COLUMN_MIN_Y).append(" <= ?");
        selection.append(" AND ").append(GeometryMetadata.COLUMN_MAX_Y).append(" >= ?");
        int args = 6;
        if (envelope.hasZ()) {
            args += 2;
            selection.append(" AND ").append(GeometryMetadata.COLUMN_MIN_Z).append(" <= ?");
            selection.append(" AND ").append(GeometryMetadata.COLUMN_MAX_Z).append(" >= ?");
        }
        if (envelope.hasM()) {
            args += 2;
            selection.append(" AND ").append(GeometryMetadata.COLUMN_MIN_M).append(" <= ?");
            selection.append(" AND ").append(GeometryMetadata.COLUMN_MAX_M).append(" >= ?");
        }

        double minX = envelope.getMinX() - tolerance;
        double maxX = envelope.getMaxX() + tolerance;
        double minY = envelope.getMinY() - tolerance;
        double maxY = envelope.getMaxY() + tolerance;

        String[] selectionArgs = new String[args];
        int argCount = 0;
        selectionArgs[argCount++] = String.valueOf(geoPackageId);
        selectionArgs[argCount++] = tableName;
        selectionArgs[argCount++] = String.valueOf(maxX);
        selectionArgs[argCount++] = String.valueOf(minX);
        selectionArgs[argCount++] = String.valueOf(maxY);
        selectionArgs[argCount++] = String.valueOf(minY);
        if (envelope.hasZ()) {
            double minZ = envelope.getMinZ() - tolerance;
            double maxZ = envelope.getMaxZ() + tolerance;
            selectionArgs[argCount++] = String.valueOf(maxZ);
            selectionArgs[argCount++] = String.valueOf(minZ);
        }
        if (envelope.hasM()) {
            double minM = envelope.getMinM() - tolerance;
            double maxM = envelope.getMaxM() + tolerance;
            selectionArgs[argCount++] = String.valueOf(maxM);
            selectionArgs[argCount++] = String.valueOf(minM);
        }
        Cursor cursor = db.query(
                GeometryMetadata.TABLE_NAME,
                GeometryMetadata.COLUMNS, selection.toString(), selectionArgs, null, null, null);
        return cursor;
    }

    /**
     * Query for all table geometry metadata count matching the envelope
     *
     * @param geoPackageId GeoPackage id
     * @param tableName    table name
     * @param envelope     geometry envelope
     * @return count
     * @since 1.1.0
     */
    public int count(long geoPackageId, String tableName, GeometryEnvelope envelope) {
        Cursor cursor = query(geoPackageId, tableName, envelope);
        int count = cursor.getCount();
        cursor.close();
        return count;
    }

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

    /**
     * Create a geometry metadata from the current cursor location
     *
     * @param cursor cursor
     * @return geometry metadata
     */
    public static GeometryMetadata createGeometryMetadata(Cursor cursor) {
        GeometryMetadata metadata = new GeometryMetadata();
        metadata.setGeoPackageId(cursor.getLong(0));
        metadata.setTableName(cursor.getString(1));
        metadata.setId(cursor.getLong(2));
        metadata.setMinX(cursor.getDouble(3));
        metadata.setMaxX(cursor.getDouble(4));
        metadata.setMinY(cursor.getDouble(5));
        metadata.setMaxY(cursor.getDouble(6));
        if (!cursor.isNull(7)) {
            metadata.setMinZ(cursor.getDouble(7));
        }
        if (!cursor.isNull(8)) {
            metadata.setMaxZ(cursor.getDouble(8));
        }
        if (!cursor.isNull(9)) {
            metadata.setMinM(cursor.getDouble(9));
        }
        if (!cursor.isNull(10)) {
            metadata.setMaxM(cursor.getDouble(10));
        }
        return metadata;
    }

}


package mil.nga.geopackage.user;

import android.content.ContentValues;

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

/**
 * Abstract User DAO for reading user tables
 *
 * @param <TColumn> column type
 * @param <TTable>  table type
 * @param <TRow>    row type
 * @param <TResult> result type
 * @author osbornb
 */
public abstract class UserDao<TColumn extends UserColumn, TTable extends UserTable<TColumn>, TRow extends UserRow<TColumn, TTable>, TResult extends UserCursor<TColumn, TTable, TRow>>
        extends UserCoreDao<TColumn, TTable, TRow, TResult> {

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

    /**
     * User database
     */
    private UserConnection userDb;

    /**
     * Invalid requery flag to requery to handle invalid large user rows
     */
    private boolean invalidRequery = true;

    /**
     * Constructor
     *
     * @param database database name
     * @param db       GeoPackage connection
     * @param userDb   user connection
     * @param table    table
     */
    protected UserDao(String database, GeoPackageConnection db,
                      UserConnection<TColumn, TTable, TRow, TResult> userDb,
                      TTable table) {
        super(database, db, userDb, table);
        this.db = db.getDb();
        this.userDb = userDb;
        userDb.setTable(table);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public GeoPackageConnection getDb() {
        return (GeoPackageConnection) super.getDb();
    }

    /**
     * Get the database connection
     *
     * @return database
     * @since 1.3.1
     */
    public GeoPackageDatabase getDatabaseConnection() {
        return db;
    }

    /**
     * Is the invalid requery flag enabled?
     * When enabled (default is true) large invalid user rows are requeried and handled.
     *
     * @return invalid requery flag
     * @since 2.0.0
     */
    public boolean isInvalidRequery() {
        return invalidRequery;
    }

    /**
     * Set the invalid requery flag.
     * When enabled (default is true) large invalid user rows are requeried and handled.
     *
     * @param invalidRequery invalid requery flag
     * @since 2.0.0
     */
    public void setInvalidRequery(boolean invalidRequery) {
        this.invalidRequery = invalidRequery;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected TResult prepareResult(TResult result) {
        if (invalidRequery) {
            result.enableInvalidRequery(this);
        }
        return result;
    }

    /**
     * {@inheritDoc}
     * Handles requery of invalid id row
     */
    @Override
    public TRow queryForIdRow(long id) {
        TRow row = null;
        TResult readCursor = queryForId(id);
        if (readCursor.moveToNext()) {
            row = readCursor.getRow();
            if (!row.isValid() && readCursor.moveToNext()) {
                row = readCursor.getRow();
            }
        }
        readCursor.close();
        return row;
    }

    /**
     * Query using the previous result query arguments
     *
     * @param previousResult previous result
     * @return result
     * @since 2.0.0
     */
    public TResult query(TResult previousResult) {
        return (TResult) userDb.query(previousResult);
    }

    /**
     * Query using the user query arguments
     *
     * @param query user query
     * @return result
     * @since 2.0.0
     */
    public TResult query(UserQuery query) {
        return (TResult) userDb.query(query);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int update(TRow row) {
        ContentValues contentValues = row.toContentValues();
        int updated = 0;
        if (contentValues.size() > 0) {
            updated = db.update(getTableName(), contentValues,
                    getPkWhere(row.getId()), getPkWhereArgs(row.getId()));
        }
        return updated;
    }

    /**
     * Update all rows matching the where clause with the provided values
     *
     * @param values      content values
     * @param whereClause where clause
     * @param whereArgs   where arguments
     * @return updated rows
     */
    public int update(ContentValues values, String whereClause,
                      String[] whereArgs) {
        return db.update(getTableName(), values, whereClause, whereArgs);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public long insert(TRow row) {
        long id = db.insertOrThrow(getTableName(), null, row.toContentValues());
        if (row.hasIdColumn()) {
            row.setId(id);
        }
        return id;
    }

    /**
     * Inserts a new row
     *
     * @param values content values
     * @return row id, -1 on error
     */
    public long insert(ContentValues values) {
        return db.insert(getTableName(), null, values);
    }

    /**
     * Inserts a new row
     *
     * @param values content values
     * @return row id
     */
    public long insertOrThrow(ContentValues values) {
        return db.insertOrThrow(getTableName(), null, values);
    }

}


package mil.nga.geopackage.tiles.features.custom;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import androidx.core.content.ContextCompat;
import android.util.TypedValue;

import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.R;
import mil.nga.geopackage.features.index.FeatureIndexResults;
import mil.nga.geopackage.features.user.FeatureCursor;
import mil.nga.geopackage.tiles.features.CustomFeaturesTile;

/**
 * Draws a tile indicating the number of features that exist within the tile, visible when zoomed
 * in closer. The number is drawn in the center of the tile and by default is surrounded by a colored
 * circle with border.  By default a tile border is drawn and the tile is colored (transparently
 * most likely). The paint objects for each draw type can be modified to or set to null (except for
 * the text paint object).
 *
 * @author osbornb
 * @since 1.1.0
 */
public class NumberFeaturesTile implements CustomFeaturesTile {

    /**
     * Text paint object
     */
    private Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    /**
     * Circle paint object
     */
    private Paint circlePaint = null;

    /**
     * Circle fill paint object
     */
    private Paint circleFillPaint = null;

    /**
     * Tile border paint object
     */
    private Paint tileBorderPaint = null;

    /**
     * Tile fill paint object
     */
    private Paint tileFillPaint = null;

    /**
     * The percentage of border to include around the edges of the text in the circle
     */
    private float circlePaddingPercentage;

    /**
     * Flag indicating whether tiles should be drawn for feature tables that are not indexed
     */
    private boolean drawUnindexedTiles;

    /**
     * Constructor
     *
     * @param context context
     */
    public NumberFeaturesTile(Context context) {

        Resources resources = context.getResources();

        // Set the default text paint values
        textPaint.setColor(ContextCompat.getColor(context, R.color.number_features_tile_text_color));
        textPaint.setTextSize(resources.getDimensionPixelSize(R.dimen.number_features_tile_text_size));

        // Set the default circle paint values
        if (resources.getBoolean(R.bool.number_features_tile_circle_draw)) {
            circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            circlePaint.setStyle(Paint.Style.STROKE);
            circlePaint.setColor(ContextCompat.getColor(context, R.color.number_features_tile_circle_color));
            TypedValue circleStrokeWidth = new TypedValue();
            resources.getValue(R.dimen.number_features_tile_circle_stroke_width,
                    circleStrokeWidth, true);
            circlePaint.setStrokeWidth(circleStrokeWidth.getFloat());
        }

        // Set the default circle fill paint values
        if (resources.getBoolean(R.bool.number_features_tile_circle_fill_draw)) {
            circleFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            circleFillPaint.setStyle(Paint.Style.FILL_AND_STROKE);
            circleFillPaint.setColor(ContextCompat.getColor(context, R.color.number_features_tile_circle_fill_color));
        }

        // Set the default tile border paint values
        if (resources.getBoolean(R.bool.number_features_tile_border_draw)) {
            tileBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            tileBorderPaint.setStyle(Paint.Style.STROKE);
            tileBorderPaint.setColor(ContextCompat.getColor(context, R.color.number_features_tile_border_color));
            TypedValue tileBorderStrokeWidth = new TypedValue();
            resources.getValue(R.dimen.number_features_tile_border_stroke_width,
                    tileBorderStrokeWidth, true);
            tileBorderPaint.setStrokeWidth(tileBorderStrokeWidth.getFloat());
        }

        // Set the default tile fill paint values
        if (resources.getBoolean(R.bool.number_features_tile_fill_draw)) {
            tileFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            tileFillPaint.setStyle(Paint.Style.FILL_AND_STROKE);
            tileFillPaint.setColor(ContextCompat.getColor(context, R.color.number_features_tile_fill_color));
        }

        // Set the default circle padding percentage
        TypedValue circlePadding = new TypedValue();
        resources.getValue(R.dimen.number_features_tile_circle_padding_percentage,
                circlePadding, true);
        circlePaddingPercentage = circlePadding.getFloat();

        // Set the default draw unindexed tiles value
        drawUnindexedTiles = resources.getBoolean(R.bool.number_features_tile_unindexed_draw);
    }

    /**
     * Get the paint object used to draw the text
     *
     * @return text paint object
     */
    public Paint getTextPaint() {
        return textPaint;
    }

    /**
     * Set the paint object used to draw the text
     *
     * @param textPaint text paint
     */
    public void setTextPaint(Paint textPaint) {
        if (textPaint == null) {
            throw new GeoPackageException("Text Paint can not be null");
        }
        this.textPaint = textPaint;
    }

    /**
     * Get the paint object used to draw the circle
     *
     * @return circle paint object
     */
    public Paint getCirclePaint() {
        return circlePaint;
    }

    /**
     * Set the paint object used to draw the circle
     *
     * @param circlePaint circle paint
     */
    public void setCirclePaint(Paint circlePaint) {
        this.circlePaint = circlePaint;
    }

    /**
     * Get the paint object used to draw the filled circle
     *
     * @return circle fill paint object
     */
    public Paint getCircleFillPaint() {
        return circleFillPaint;
    }

    /**
     * Set the paint object used to draw the filled circle
     *
     * @param circleFillPaint circle fill paint
     */
    public void setCircleFillPaint(Paint circleFillPaint) {
        this.circleFillPaint = circleFillPaint;
    }

    /**
     * Get the circle padding percentage around the text
     *
     * @return circle padding percentage, 0.0 to 1.0
     */
    public float getCirclePaddingPercentage() {
        return circlePaddingPercentage;
    }

    /**
     * Set the circle padding percentage to pad around the text, value between 0.0 and 1.0
     *
     * @param circlePaddingPercentage circle padding percentage
     */
    public void setCirclePaddingPercentage(float circlePaddingPercentage) {
        if (circlePaddingPercentage < 0.0 || circlePaddingPercentage > 1.0) {
            throw new GeoPackageException("Circle padding percentage must be between 0.0 and 1.0: "
                    + circlePaddingPercentage);
        }
        this.circlePaddingPercentage = circlePaddingPercentage;
    }

    /**
     * Get the tile border paint object used to draw a border around the tile
     *
     * @return tile border paint
     */
    public Paint getTileBorderPaint() {
        return tileBorderPaint;
    }

    /**
     * Set the tile border paint object used to draw a border around the tile
     *
     * @param tileBorderPaint tile border paint
     */
    public void setTileBorderPaint(Paint tileBorderPaint) {
        this.tileBorderPaint = tileBorderPaint;
    }

    /**
     * Get the tile fill paint object used to color the entire tile
     *
     * @return tile fill paint
     */
    public Paint getTileFillPaint() {
        return tileFillPaint;
    }

    /**
     * Set the tile fill paint object used to color the entire tile
     *
     * @param tileFillPaint tile fill paint
     */
    public void setTileFillPaint(Paint tileFillPaint) {
        this.tileFillPaint = tileFillPaint;
    }

    /**
     * Is the draw unindexed tiles option enabled
     *
     * @return true if drawing unindexed tiles
     */
    public boolean isDrawUnindexedTiles() {
        return drawUnindexedTiles;
    }

    /**
     * Set the draw unindexed tiles option
     *
     * @param drawUnindexedTiles draw unindexed tiles flag
     */
    public void setDrawUnindexedTiles(boolean drawUnindexedTiles) {
        this.drawUnindexedTiles = drawUnindexedTiles;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Bitmap drawTile(int tileWidth, int tileHeight, long tileFeatureCount, FeatureIndexResults featureIndexResults) {

        String featureText = String.valueOf(tileFeatureCount);
        Bitmap bitmap = drawTile(tileWidth, tileHeight, featureText);

        return bitmap;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Bitmap drawUnindexedTile(int tileWidth, int tileHeight, long totalFeatureCount, FeatureCursor allFeatureResults) {

        Bitmap bitmap = null;

        if (drawUnindexedTiles) {
            // Draw a tile indicating we have no idea if there are features inside.
            // The table is not indexed and more features exist than the max feature count set.
            bitmap = drawTile(tileWidth, tileHeight, "?");
        }

        return bitmap;
    }

    /**
     * Draw a tile with the provided text label in the middle
     *
     * @param tileWidth  tile width
     * @param tileHeight tile height
     * @param text       text
     * @return tile bitmap
     */
    private Bitmap drawTile(int tileWidth, int tileHeight, String text) {

        // Create bitmap and canvas
        Bitmap bitmap = Bitmap.createBitmap(tileWidth,
                tileHeight, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);

        // Draw the tile fill paint
        if (tileFillPaint != null) {
            canvas.drawRect(0, 0, tileWidth, tileHeight, tileFillPaint);
        }

        // Draw the tile border
        if (tileBorderPaint != null) {
            canvas.drawRect(0, 0, tileWidth, tileHeight, tileBorderPaint);
        }

        // Determine the text bounds
        Rect textBounds = new Rect();
        textPaint.getTextBounds(text, 0, text.length(), textBounds);

        // Determine the center of the tile
        int centerX = (int) (bitmap.getWidth() / 2.0f);
        int centerY = (int) (bitmap.getHeight() / 2.0f);

        // Draw the circle
        if (circlePaint != null || circleFillPaint != null) {
            int diameter = Math.max(textBounds.width(), textBounds.height());
            float radius = diameter / 2.0f;
            radius = radius + (diameter * circlePaddingPercentage);

            // Draw the filled circle
            if (circleFillPaint != null) {
                canvas.drawCircle(centerX, centerY, radius, circleFillPaint);
            }

            // Draw the circle
            if (circlePaint != null) {
                canvas.drawCircle(centerX, centerY, radius, circlePaint);
            }

        }

        // Draw the text
        canvas.drawText(text, centerX - textBounds.exactCenterX(), centerY - textBounds.exactCenterY(), textPaint);

        return bitmap;
    }

}


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

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

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

/**
 * User Simple Attributes DAO for reading user simple attributes data tables
 *
 * @author osbornb
 * @since 3.0.1
 */
public class SimpleAttributesDao extends UserCustomDao {

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

    /**
     * Constructor
     *
     * @param dao                   user custom data access object
     * @param simpleAttributesTable simple attributes table
     */
    protected SimpleAttributesDao(UserCustomDao dao,
                                  SimpleAttributesTable simpleAttributesTable) {
        super(dao, simpleAttributesTable);
    }

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

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

    /**
     * Get the simple attributes row from the current cursor location
     *
     * @param cursor cursor
     * @return simple attributes row
     */
    public SimpleAttributesRow getRow(UserCustomCursor cursor) {
        return getRow(cursor.getRow());
    }

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

    /**
     * Get the simple attributes rows that exist with the provided ids
     *
     * @param ids list of ids
     * @return simple attributes rows
     */
    public List<SimpleAttributesRow> getRows(List<Long> ids) {
        List<SimpleAttributesRow> simpleAttributesRows = new ArrayList<>();
        for (long id : ids) {
            UserCustomRow userCustomRow = queryForIdRow(id);
            if (userCustomRow != null) {
                simpleAttributesRows.add(getRow(userCustomRow));
            }
        }
        return simpleAttributesRows;
    }

}