com.rgi.geopackage.tiles.GeoPackageTiles Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of swagd Show documentation
Show all versions of swagd Show documentation
SWAGD: Software to Aggregate Geospatial Data
The newest version!
/* The MIT License (MIT)
*
* Copyright (c) 2015 Reinventing Geospatial, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.rgi.geopackage.tiles;
import com.rgi.common.BoundingBox;
import com.rgi.common.coordinate.Coordinate;
import com.rgi.common.coordinate.CoordinateReferenceSystem;
import com.rgi.common.coordinate.CrsCoordinate;
import com.rgi.common.tile.TileOrigin;
import com.rgi.common.util.BoundsUtility;
import com.rgi.common.util.jdbc.JdbcUtility;
import com.rgi.geopackage.core.GeoPackageCore;
import com.rgi.geopackage.core.SpatialReferenceSystem;
import com.rgi.geopackage.utility.DatabaseUtility;
import com.rgi.geopackage.verification.VerificationIssue;
import com.rgi.geopackage.verification.VerificationLevel;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
/**
* @author Luke Lambert
*
*/
public class GeoPackageTiles
{
/**
* Constructor
*
* @param databaseConnection
* The open connection to the database that contains a GeoPackage
* @param core
* Access to GeoPackage's "core" methods
*/
public GeoPackageTiles(final Connection databaseConnection, final GeoPackageCore core)
{
this.databaseConnection = databaseConnection;
this.core = core;
}
/**
* Requirements this GeoPackage failed to meet
*
* @param verificationLevel
* Controls the level of verification testing performed
* @return The tile GeoPackage requirements this GeoPackage fails to conform to
* @throws SQLException throws if {@link TilesVerifier#TilesVerifier Verifier Constructor} throws
*/
public Collection getVerificationIssues(final VerificationLevel verificationLevel) throws SQLException
{
return new TilesVerifier(this.databaseConnection, verificationLevel).getVerificationIssues();
}
/**
* Creates a user defined tiles table, and adds a corresponding entry to the
* content table
*
* @param tableName
* The name of the tiles table. The table name must begin with a
* letter (A..Z, a..z) or an underscore (_) and may only be
* followed by letters, underscores, or numbers, and may not
* begin with the prefix "gpkg_"
* @param identifier
* A human-readable identifier (e.g. short name) for the
* tableName content
* @param description
* A human-readable description for the tableName content
* @param boundingBox
* Bounding box for all content in tableName
* @param spatialReferenceSystem
* Spatial Reference System (SRS)
* @return Returns a newly created user defined tiles table
* @throws SQLException
* throws if the method {@link #getTileSet(String) getTileSet}
* or the method
* {@link DatabaseUtility#tableOrViewExists(Connection, String)
* tableOrViewExists} or if the database cannot roll back the
* changes after a different exception throws will throw an SQLException
*
*/
public TileSet addTileSet(final String tableName,
final String identifier,
final String description,
final BoundingBox boundingBox,
final SpatialReferenceSystem spatialReferenceSystem) throws SQLException
{
DatabaseUtility.validateTableName(tableName);
if(boundingBox == null)
{
throw new IllegalArgumentException("Bounding box cannot be mull.");
}
if(spatialReferenceSystem == null)
{
throw new IllegalArgumentException("Spatial reference system may not be null");
}
final TileSet existingContent = this.getTileSet(tableName);
if(existingContent != null)
{
if(existingContent.equals(tableName,
TileSet.TileContentType,
identifier,
description,
boundingBox.getMinimumX(),
boundingBox.getMinimumY(),
boundingBox.getMaximumX(),
boundingBox.getMaximumY(),
spatialReferenceSystem.getIdentifier()))
{
return existingContent;
}
throw new IllegalArgumentException("An entry in the content table already exists with this table name, but has different values for its other fields");
}
if(DatabaseUtility.tableOrViewExists(this.databaseConnection, tableName))
{
throw new IllegalArgumentException("A table already exists with this tile set's table name");
}
try
{
this.createTilesTablesNoCommit(); // Create the tile metadata tables
// Create the tile set table
JdbcUtility.update(this.databaseConnection, GeoPackageTiles.getTileSetCreationSql(tableName));
// Add tile set to the content table
this.core.addContent(tableName,
TileSet.TileContentType,
identifier,
description,
boundingBox,
spatialReferenceSystem);
this.addTileMatrixSetNoCommit(tableName,
boundingBox,
spatialReferenceSystem); // Add tile matrix set metadata
this.databaseConnection.commit();
return this.getTileSet(tableName);
}
catch(final Exception ex)
{
this.databaseConnection.rollback();
throw ex;
}
}
/**
* The zoom levels that a tile set has values for
*
* @param tileSet
* A handle to a set of tiles
* @return Returns all of the zoom levels that apply for tileSet
* @throws SQLException
* SQLException thrown by automatic close() invocation on preparedStatement or various other SQLExceptions
*/
public Set getTileZoomLevels(final TileSet tileSet) throws SQLException
{
if(tileSet == null)
{
throw new IllegalArgumentException("Tile set cannot be null");
}
final String zoomLevelQuerySql = String.format("SELECT zoom_level FROM %s WHERE table_name = ?;",
GeoPackageTiles.MatrixTableName);
final List zoomLevels = JdbcUtility.select(this.databaseConnection,
zoomLevelQuerySql,
preparedStatement -> preparedStatement.setString(1, tileSet.getTableName()),
resultSet -> resultSet.getInt(1));
return new HashSet<>(zoomLevels);
}
/**
* Gets all entries in the GeoPackage's contents table with the "tiles"
* data_type
*
* @return Returns a collection of {@link TileSet}s
* @throws SQLException
* throws if the method
* {@link #getTileSets(SpatialReferenceSystem) getTileSets}
* throws
*/
public Collection getTileSets() throws SQLException
{
return this.getTileSets(null);
}
/**
* Gets all entries in the GeoPackage's contents table with the "tiles"
* data_type that also match the supplied spatial reference system
*
* @param matchingSpatialReferenceSystem
* Spatial reference system that returned {@link TileSet}s refer
* to
* @return Returns a collection of {@link TileSet}s
* @throws SQLException
* Throws if there's an SQL error
*/
public Collection getTileSets(final SpatialReferenceSystem matchingSpatialReferenceSystem) throws SQLException
{
return this.core.getContent(TileSet.TileContentType,
(tableName,
dataType,
identifier,
description,
lastChange,
minimumX,
minimumY,
maximumX,
maximumY,
spatialReferenceSystem) -> new TileSet(tableName,
identifier,
description,
lastChange,
minimumX,
minimumY,
maximumX,
maximumY,
spatialReferenceSystem),
matchingSpatialReferenceSystem);
}
/**
* Adds a tile matrix
*
* @param tileMatrixSet
* A handle to a tile matrix set
* @param zoomLevel
* The zoom level of the associated tile set (0 <= zoomLevel <=
* max_level)
* @param matrixWidth
* The number of columns (>= 1) for this tile at this zoom level
* @param matrixHeight
* The number of rows (>= 1) for this tile at this zoom level
* @param tileWidth
* The tile width in pixels (>= 1) at this zoom level
* @param tileHeight
* The tile height in pixels (>= 1) at this zoom level
* @return Returns the newly added tile matrix
* @throws SQLException
* throws when the method {@link #getTileMatrix(TileMatrixSet,
* int)} or the database cannot roll back the changes after a
* different exception is thrown, an SQLException is thrown
*/
public TileMatrix addTileMatrix(final TileMatrixSet tileMatrixSet,
final int zoomLevel,
final int matrixWidth,
final int matrixHeight,
final int tileWidth,
final int tileHeight) throws SQLException
{
if(tileMatrixSet == null)
{
throw new IllegalArgumentException("The tile set may not be null");
}
if(zoomLevel < 0)
{
throw new IllegalArgumentException("Zoom level must be greater than or equal to 0");
}
if(matrixWidth <= 0)
{
throw new IllegalArgumentException("Matrix width must be greater than 0");
}
if(matrixHeight <= 0)
{
throw new IllegalArgumentException("Matrix height must be greater than 0");
}
if(tileWidth <= 0)
{
throw new IllegalArgumentException("Tile width must be greater than 0");
}
if(tileHeight <= 0)
{
throw new IllegalArgumentException("Matrix height must be greater than 0");
}
final BoundingBox bounds = tileMatrixSet.getBoundingBox();
final double pixelXSize = ((bounds.getWidth()) / matrixWidth ) / tileWidth;
final double pixelYSize = ((bounds.getHeight()) / matrixHeight) / tileHeight;
final TileMatrix tileMatrix = this.getTileMatrix(tileMatrixSet, zoomLevel);
if(tileMatrix != null)
{
if(!tileMatrix.equals(tileMatrixSet.getTableName(),
zoomLevel,
matrixWidth,
matrixHeight,
tileWidth,
tileHeight,
pixelXSize,
pixelYSize))
{
throw new IllegalArgumentException("An entry in the tile matrix set table already exists with this table name, but has different values for its other fields");
}
return tileMatrix;
}
final String insertTileMatrix = String.format("INSERT INTO %s (%s, %s, %s, %s, %s, %s, %s, %s) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
GeoPackageTiles.MatrixTableName,
"table_name",
"zoom_level",
"matrix_width",
"matrix_height",
"tile_width",
"tile_height",
"pixel_x_size",
"pixel_y_size");
JdbcUtility.update(this.databaseConnection,
insertTileMatrix,
preparedStatement -> { preparedStatement.setString(1, tileMatrixSet.getTableName());
preparedStatement.setInt (2, zoomLevel);
preparedStatement.setInt (3, matrixWidth);
//noinspection SuspiciousNameCombination
preparedStatement.setInt (4, matrixHeight);
preparedStatement.setInt (5, tileWidth);
//noinspection SuspiciousNameCombination
preparedStatement.setInt (6, tileHeight);
preparedStatement.setDouble(7, pixelXSize);
//noinspection SuspiciousNameCombination
preparedStatement.setDouble(8, pixelYSize);
});
this.databaseConnection.commit();
return new TileMatrix(tileMatrixSet.getTableName(),
zoomLevel,
matrixWidth,
matrixHeight,
tileWidth,
tileHeight,
pixelXSize,
pixelYSize);
}
/**
* Add a tile to the GeoPackage
*
* @param tileSet
* Tile set that which the tiles and tile metadata are associated
* @param tileMatrix
* Tile matrix associated with the tile set at the corresponding
* zoom level
* @param column
* The 'x' portion of the coordinate
* @param row
* The 'y' portion of the coordinate
* @param imageData
* The bytes of the image file
* @return The Tile added to the GeoPackage with the properties of the
* parameters
* @throws SQLException
* SQLException thrown by automatic close() invocation on
* preparedStatement or if the Database is unable to commit the
* changes or if the method
* {@link #getTile(TileSet, int, int, int) getTile}
* throws an SQLException or other various SQLExceptions
*/
public Tile addTile(final TileSet tileSet,
final TileMatrix tileMatrix,
final int column,
final int row,
final byte[] imageData) throws SQLException
{
if(tileSet == null)
{
throw new IllegalArgumentException("Tile set may not be null");
}
if(tileMatrix == null)
{
throw new IllegalArgumentException("Tile matrix may not be null");
}
if(imageData == null || imageData.length == 0) // TODO the standard restricts the image types to image/jpeg, image/png and image/x-webp (by extension only: http://www.geopackage.org/spec/#extension_tiles_webp)
// TODO It'd be desirable to check the height/width of the image against the values described by the tile matrix, but this is difficult to do with a string of bytes. One solution would be to changed to a java BufferedImage rather than raw bytes, but this *might* unnecessarily confine extension writers to to formats that fit into Java.ImageIO
{
throw new IllegalArgumentException("Image data may not be null or empty");
}
// Verify row and column are within the tile metadata's range
if(row < 0 || row >= tileMatrix.getMatrixHeight())
{
throw new IllegalArgumentException(String.format("Tile row %d is outside of the valid row range [0, %d] (0 to tile matrix metadata's matrix height - 1)",
row,
tileMatrix.getMatrixHeight()-1));
}
if(column < 0 || column >= tileMatrix.getMatrixWidth())
{
throw new IllegalArgumentException(String.format("Tile column %d is outside of the valid column range [0, %d] (0 to tile matrix metadata's matrix width - 1)",
column,
tileMatrix.getMatrixWidth()-1));
}
final String insertTileSql = String.format("INSERT INTO %s (%s, %s, %s, %s) VALUES (?, ?, ?, ?)",
tileSet.getTableName(),
"zoom_level",
"tile_column",
"tile_row",
"tile_data");
final int tileIdentifier = JdbcUtility.update(this.databaseConnection,
insertTileSql,
preparedStatement -> { preparedStatement.setInt (1, tileMatrix.getZoomLevel());
preparedStatement.setInt (2, column);
preparedStatement.setInt (3, row);
preparedStatement.setBytes(4, imageData); // .setBlob() didn't work as advertised in the sqlite-jdbc driver. not sure about sqldroid
},
resultSet -> resultSet.getInt(1)); // tile identifier
this.databaseConnection.commit();
return new Tile(tileIdentifier,
tileMatrix.getZoomLevel(),
column,
row,
imageData);
}
/**
* Add a tile to the GeoPackage
*
* @param tileSet
* Tile set that which the tiles and tile metadata are associated
* @param tileMatrix
* Tile matrix associated with the tile set at the corresponding
* zoom level
* @param coordinate
* The coordinate of the tile in units of the tile set's spatial
* reference system
* @param precision
* Specifies a tolerance for coordinate value testings to a number of decimal places
* @param imageData
* The bytes of the image file
* @return returns a Tile added to the GeoPackage with the properties of the
* parameters
* @throws SQLException
* is thrown if the following methods throw
* {@link #crsToTileCoordinate(TileSet, CrsCoordinate, int, int)
* crsToRelativeTileCoordinate} or
* {@link #addTile(TileSet, TileMatrix, int, int, byte[])
* addTile} throws an SQLException
*/
public Tile addTile(final TileSet tileSet,
final TileMatrix tileMatrix,
final CrsCoordinate coordinate,
final int precision,
final byte[] imageData) throws SQLException
{
final Coordinate tileCoordinate = this.crsToTileCoordinate(tileSet,
coordinate,
precision,
tileMatrix.getZoomLevel());
return this.addTile(tileSet,
tileMatrix,
tileCoordinate.getX(),
tileCoordinate.getY(),
imageData);
}
/**
* Gets tile coordinates for every tile in a tile set. A tile set need not
* have an entry for every possible position in its respective tile
* matrices.
*
* @param tileSet
* Handle to the tile set that the requested tiles should belong
* @return Returns a {@link Stream} of {@link TileCoordinate}s
* representing every tile that the specific tile set contains.
* @throws SQLException
* when SQLException thrown by automatic close() invocation on
* preparedStatement or if other SQLExceptions occur
*/
public Stream getTiles(final TileSet tileSet) throws SQLException
{
if(tileSet == null)
{
throw new IllegalArgumentException("Tile set cannot be null");
}
final String tileQuery = String.format("SELECT %s, %s, %s FROM %s;",
"zoom_level",
"tile_column",
"tile_row",
tileSet.getTableName());
return JdbcUtility.select(this.databaseConnection,
tileQuery,
null,
resultSet -> new TileCoordinate(resultSet.getInt(2),
resultSet.getInt(3),
resultSet.getInt(1)))
.stream();
}
/**
* Gets a stream of every tile in the tile store for a given zoom level.
* The zoom level need not have an entry for every possible position in
* its respective tile matrices. If there are no tiles at this zoom level,
* an empty stream will be returned.
*
* @param tileSet
* Handle to the tile set that the requested tiles should belong
* @param zoomLevel
* The zoom level of the requested tiles
* @return Returns a {@link Stream} of relative tile {@link Coordinate}s
* representing every tile that the specific tile set contains.
* @throws SQLException
* when SQLException thrown by automatic close() invocation on
* preparedStatement or if other SQLExceptions occur
*/
public Stream> getTiles(final TileSet tileSet, final int zoomLevel) throws SQLException
{
if(tileSet == null)
{
throw new IllegalArgumentException("Tile set cannot be null");
}
final String tileQuery = String.format("SELECT %s, %s FROM %s WHERE zoom_level = ?;",
"tile_column",
"tile_row",
tileSet.getTableName());
return JdbcUtility.select(this.databaseConnection,
tileQuery,
preparedStatement -> preparedStatement.setInt(1, zoomLevel),
resultSet -> new Coordinate<>(resultSet.getInt(1), resultSet.getInt(2)))
.stream();
}
/**
* Gets a tile
*
* @param tileSet
* Handle to the tile set that the requested tile should belong
* @param column
* The 'x' portion of the coordinate
* @param row
* The 'y' portion of the coordinate
* @param zoomLevel
* The zoom level associated with the coordinate
* @return Returns the requested tile, or null if it's not found
* @throws SQLException
* SQLException thrown by automatic close() invocation on
* preparedStatement or if other SQLExceptions occur when adding
* the Tile data to the database
*/
public Tile getTile(final TileSet tileSet,
final int column,
final int row,
final int zoomLevel) throws SQLException
{
if(tileSet == null)
{
throw new IllegalArgumentException("Tile set cannot be null");
}
final String tileQuery = String.format("SELECT %s, %s, %s, %s, %s FROM %s WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?;",
"id",
"zoom_level",
"tile_column",
"tile_row",
"tile_data",
tileSet.getTableName());
return JdbcUtility.selectOne(this.databaseConnection,
tileQuery,
preparedStatement -> { preparedStatement.setInt(1, zoomLevel);
preparedStatement.setInt(2, column);
preparedStatement.setInt(3, row);
},
resultSet -> new Tile(resultSet.getInt(1), // id
resultSet.getInt(2), // zoom level
resultSet.getInt(3), // column
resultSet.getInt(4), // row
resultSet.getBytes(5))); // data
}
/**
* Gets a tile
*
* @param tileSet
* Handle to the tile set that the requested tile should belong
* @param coordinate
* Coordinate, in the units of the tile set's spatial reference
* system, of the requested tile
* @param precision
* Specifies a tolerance for coordinate value testings to a number of decimal places
* @param zoomLevel
* Zoom level
* @return Returns the requested tile, or null if it's not found
* @throws SQLException
* throws when the method
* {@link #crsToTileCoordinate(TileSet, CrsCoordinate, int, int)
* crsToRelativeTileCoordinate} or the method
* {@link #getTile(TileSet, int, int, int)} throws an
* SQLException
*/
public Tile getTile(final TileSet tileSet,
final CrsCoordinate coordinate,
final int precision,
final int zoomLevel) throws SQLException
{
final Coordinate tileCoordinate = this.crsToTileCoordinate(tileSet,
coordinate,
precision,
zoomLevel);
return this.getTile(tileSet,
tileCoordinate.getX(),
tileCoordinate.getY(),
zoomLevel);
}
/**
* Gets a tile set's tile matrix
*
* @param tileSet
* A handle to a set of tiles
* @return Returns a tile set's tile matrix set
* @throws SQLException
* SQLException thrown by automatic close() invocation on
* preparedStatement or if the method
* {@link DatabaseUtility#tableOrViewExists(Connection, String)}
* throws or other SQLExceptions occur when retrieving
* information from the database
*/
public TileMatrixSet getTileMatrixSet(final TileSet tileSet) throws SQLException
{
if(tileSet == null)
{
throw new IllegalArgumentException("Tile set cannot be null");
}
final String querySql = String.format("SELECT %s, %s, %s, %s, %s, %s FROM %s WHERE table_name = ?;",
"table_name",
"srs_id",
"min_x",
"min_y",
"max_x",
"max_y",
GeoPackageTiles.MatrixSetTableName);
return JdbcUtility.selectOne(this.databaseConnection,
querySql,
preparedStatement -> preparedStatement.setString(1, tileSet.getTableName()),
resultSet -> new TileMatrixSet(resultSet.getString(1), // table name
this.core.getSpatialReferenceSystem(resultSet.getInt(2)), // srs id
new BoundingBox(resultSet.getDouble(3), // min x
resultSet.getDouble(4), // min y
resultSet.getDouble(5), // max x
resultSet.getDouble(6)))); // max y
}
/**
* Gets a tile set object based on its table name
*
* @param tileSetTableName
* Name of a tile set table
* @return Returns a {@link TileSet} or null if there isn't with the supplied table
* name
* @throws SQLException
* throws if the method
* {@link GeoPackageCore#getContent(String, com.rgi.geopackage.core.ContentFactory, SpatialReferenceSystem)}
* throws an SQLException
*/
public TileSet getTileSet(final String tileSetTableName) throws SQLException
{
return this.core.getContent(tileSetTableName,
(tableName,
dataType,
identifier,
description,
lastChange,
minimumX,
minimumY,
maximumX,
maximumY,
spatialReferenceSystem) -> new TileSet(tableName,
identifier,
description,
lastChange,
minimumX,
minimumY,
maximumX,
maximumY,
spatialReferenceSystem));
}
/**
* Adds a tile matrix associated with a tile set
*
* **WARNING** this does not do a database commit. It is expected
* that this transaction will always be paired with others that need to be
* committed or roll back as a single transaction.
*
* @param tableName
* Name of the tile set
* @param boundingBox
* Bounding box of the tile matrix set
* @param spatialReferenceSystem
* Spatial reference system of the tile matrix set
* @throws SQLException
* thrown by automatic close() invocation on preparedStatement
*/
private void addTileMatrixSetNoCommit(final String tableName,
final BoundingBox boundingBox,
final SpatialReferenceSystem spatialReferenceSystem) throws SQLException
{
if(tableName == null || tableName.isEmpty())
{
throw new IllegalArgumentException("Table name cannot null or empty");
}
if(spatialReferenceSystem == null)
{
throw new IllegalArgumentException("Spatial reference system cannot be null");
}
if(boundingBox == null)
{
throw new IllegalArgumentException("Bounding box cannot be null");
}
final String insertTileMatrixSetSql = String.format("INSERT INTO %s (%s, %s, %s, %s, %s, %s) VALUES (?, ?, ?, ?, ?, ?)",
GeoPackageTiles.MatrixSetTableName,
"table_name",
"srs_id",
"min_x",
"min_y",
"max_x",
"max_y");
JdbcUtility.update(this.databaseConnection,
insertTileMatrixSetSql,
preparedStatement -> { preparedStatement.setString(1, tableName);
preparedStatement.setInt (2, spatialReferenceSystem.getIdentifier());
preparedStatement.setDouble(3, boundingBox.getMinimumX());
preparedStatement.setDouble(4, boundingBox.getMinimumY());
preparedStatement.setDouble(5, boundingBox.getMaximumX());
preparedStatement.setDouble(6, boundingBox.getMaximumY());
});
}
/**
* Get a tile set's tile matrix
*
* @param tileMatrixSet
* Handle to a tile matrix set
* @param zoomLevel
* Zoom level of the tile matrix
* @return Returns a tile set's tile matrix that corresponds to the input
* level, or null if one doesn't exist
* @throws SQLException
* SQLException thrown by automatic close() invocation on
* preparedStatement or when an SQLException occurs retrieving
* information from the database
*/
public TileMatrix getTileMatrix(final TileMatrixSet tileMatrixSet, final int zoomLevel) throws SQLException
{
if(tileMatrixSet == null)
{
throw new IllegalArgumentException("Tile matrix set may not be null");
}
return this.getTileMatrix(tileMatrixSet.getTableName(), zoomLevel);
}
/**
* Gets the tile matrices associated with a tile set
*
* @param tileSet
* A handle to a set of tiles
* @return Returns every tile matrix associated with a tile set in ascending
* order by zoom level
* @throws SQLException
* SQLException thrown by automatic close() invocation on
* preparedStatement or when an SQLException occurs retrieving
* information from the database
*/
public List getTileMatrices(final TileSet tileSet) throws SQLException
{
if(tileSet == null)
{
throw new IllegalArgumentException("Tile set cannot be null");
}
final String tileQuery = String.format("SELECT %s, %s, %s, %s, %s, %s, %s FROM %s WHERE table_name = ? ORDER BY %1$s ASC;",
"zoom_level",
"matrix_width",
"matrix_height",
"tile_width",
"tile_height",
"pixel_x_size",
"pixel_y_size",
GeoPackageTiles.MatrixTableName);
return JdbcUtility.select(this.databaseConnection,
tileQuery,
preparedStatement -> preparedStatement.setString(1, tileSet.getTableName()),
resultSet -> new TileMatrix(tileSet.getTableName(),
resultSet.getInt (1), // zoom level
resultSet.getInt (2), // matrix width
resultSet.getInt (3), // matrix height
resultSet.getInt (4), // tile width
resultSet.getInt (5), // tile height
resultSet.getDouble(6), // pixel x size
resultSet.getDouble(7))); // pixel y size
}
/**
* Convert a CRS coordinate to a tile coordinate relative to a tile set
*
* @param tileSet
* A handle to a set of tiles
* @param crsCoordinate
* A coordinate with a specified coordinate reference system
* @param precision
* Specifies a tolerance for coordinate value testings to a number of decimal places
* @param zoomLevel
* Zoom level
* @return Returns a tile coordinate relative and specific to the input tile
* set. The input CRS coordinate would be contained in the the
* associated tile bounds.
* @throws SQLException
* throws if the method {@link #getTileMatrix(String, int)
* getTileMatrix} or the method
* {@link GeoPackageCore#getSpatialReferenceSystem(int)
* getSpatialReferenceSystem} or the method
* {@link #getTileMatrixSet(TileSet) getTileMatrixSet} throws
*/
public Coordinate crsToTileCoordinate(final TileSet tileSet,
final CrsCoordinate crsCoordinate,
final int precision,
final int zoomLevel) throws SQLException
{
if(tileSet == null)
{
throw new IllegalArgumentException("Tile set may not be null");
}
if(crsCoordinate == null)
{
throw new IllegalArgumentException("CRS coordinate may not be null");
}
final CoordinateReferenceSystem crs = crsCoordinate.getCoordinateReferenceSystem();
final SpatialReferenceSystem srs = this.core.getSpatialReferenceSystem(tileSet.getSpatialReferenceSystemIdentifier());
if(srs == null)
{
throw new IllegalArgumentException("Spatial Reference System may not be null."); //added due to coverity scan
}
if(!crs.getAuthority().equalsIgnoreCase(srs.getOrganization()) ||
crs.getIdentifier() != srs.getOrganizationSrsId())
{
throw new IllegalArgumentException("Coordinate transformation is not currently supported. The incoming spatial reference system must match that of the tile set's");
}
final TileMatrix tileMatrix = this.getTileMatrix(tileSet.getTableName(), zoomLevel);
if(tileMatrix == null)
{
throw new IllegalArgumentException("Invalid zoom level for this tile set");
}
final TileMatrixSet tileMatrixSet = this.getTileMatrixSet(tileSet);
final BoundingBox tileSetBounds = tileMatrixSet.getBoundingBox();
if(!BoundsUtility.contains(roundBounds(tileSetBounds, precision), crsCoordinate, GeoPackageTiles.Origin))
{
throw new IllegalArgumentException("The requested geographic coordinate is outside the bounds of the tile set");
}
final Coordinate boundsCorner = BoundsUtility.boundsCorner(roundBounds(tileSetBounds, precision), GeoPackageTiles.Origin);
final double tileWidthInSrs = tileMatrix.getPixelXSize() * tileMatrix.getTileWidth();
final double tileHeightInSrs = tileMatrix.getPixelYSize() * tileMatrix.getTileHeight();
final double normalizedSrsTileCoordinateX = Math.abs(crsCoordinate.getX() - boundsCorner.getX());
final double normalizedSrsTileCoordinateY = Math.abs(crsCoordinate.getY() - boundsCorner.getY());
final double divisor = 1000000000.0; // Round to integer extent
@SuppressWarnings("NumericCastThatLosesPrecision")
final int tileX = (int)Math.floor(Math.round((normalizedSrsTileCoordinateX / tileWidthInSrs) * divisor) / divisor);
@SuppressWarnings("NumericCastThatLosesPrecision")
final int tileY = (int)Math.floor(Math.round((normalizedSrsTileCoordinateY / tileHeightInSrs) * divisor) / divisor);
return new Coordinate<>(tileX, tileY);
}
/**
* Converts a tile coordinate, relative to the input tile set, to a
* geographic point. {@link GeoPackageTiles#Origin} is used as the
* representative point of the tile.
*
* @param tileSet
* Handle to the tile set that the requested tile should belong
* @param column
* The 'x' portion of the coordinate
* @param row
* The 'y' portion of the coordinate
* @param zoomLevel
* The zoom level associated with the coordinate
* @return A {@link CrsCoordinate} point, using
* {@link GeoPackageTiles#Origin} as the representative corner
* of the tile.
* @throws SQLException
* When there is an SQL failure in getting the tile matrix, the
* spatial reference system, or the tile matrix set of the
* input tile set.
*/
public CrsCoordinate tileToCrsCoordinate(final TileSet tileSet,
final int column,
final int row,
final int zoomLevel) throws SQLException
{
if(tileSet == null)
{
throw new IllegalArgumentException("Tile set may not be null");
}
if(column < 0)
{
throw new IllegalArgumentException("Column must be 0 or greater;");
}
if(row < 0)
{
throw new IllegalArgumentException("Row must be 0 or greater;");
}
final TileMatrix tileMatrix = this.getTileMatrix(tileSet.getTableName(), zoomLevel);
if(tileMatrix == null)
{
throw new IllegalArgumentException("Invalid zoom level for this tile set");
}
final double tileWidthInSrs = tileMatrix.getPixelXSize() * tileMatrix.getTileWidth(); // We could also divide the tile set bounds by the tile matrix width/height
final double tileHeightInSrs = tileMatrix.getPixelYSize() * tileMatrix.getTileHeight();
final SpatialReferenceSystem srs = this.core.getSpatialReferenceSystem(tileSet.getSpatialReferenceSystemIdentifier());
if(srs == null)
{
throw new IllegalArgumentException("Spatial Reference System may not be null");//added due to coverity scan
}
final TileMatrixSet tileMatrixSet = this.getTileMatrixSet(tileSet);
final BoundingBox tileSetBounds = tileMatrixSet.getBoundingBox();
final Coordinate boundsCorner = tileSetBounds.getTopLeft();
return new CrsCoordinate(boundsCorner.getX() + (column * tileWidthInSrs),
boundsCorner.getY() - (row * tileHeightInSrs),
new CoordinateReferenceSystem(srs.getOrganization(), srs.getOrganizationSrsId()));
}
/**
* Creates the tables required for storing tiles
*
*
* **WARNING** this does not do a database commit. It is expected
* that this transaction will always be paired with others that need to be
* committed or roll back as a single transaction.
*
* @throws SQLException
* if there is a database error
*/
private void createTilesTablesNoCommit() throws SQLException
{
// Create the tile matrix set table or view
if(!DatabaseUtility.tableOrViewExists(this.databaseConnection, GeoPackageTiles.MatrixSetTableName))
{
JdbcUtility.update(this.databaseConnection, GeoPackageTiles.getTileMatrixSetCreationSql());
}
// Create the tile matrix table or view
if(!DatabaseUtility.tableOrViewExists(this.databaseConnection, GeoPackageTiles.MatrixTableName))
{
JdbcUtility.update(this.databaseConnection, GeoPackageTiles.getTileMatrixCreationSql());
}
}
private static String getTileMatrixSetCreationSql()
{
// http://www.geopackage.org/spec/#gpkg_tile_matrix_set_sql
// http://www.geopackage.org/spec/#_tile_matrix_set
return "CREATE TABLE " + GeoPackageTiles.MatrixSetTableName + '\n' +
"(table_name TEXT NOT NULL PRIMARY KEY, -- Tile Pyramid User Data Table Name\n" +
" srs_id INTEGER NOT NULL, -- Spatial Reference System ID: gpkg_spatial_ref_sys.srs_id\n" +
" min_x DOUBLE NOT NULL, -- Bounding box minimum easting or longitude for all content in table_name\n" +
" min_y DOUBLE NOT NULL, -- Bounding box minimum northing or latitude for all content in table_name\n" +
" max_x DOUBLE NOT NULL, -- Bounding box maximum easting or longitude for all content in table_name\n" +
" max_y DOUBLE NOT NULL, -- Bounding box maximum northing or latitude for all content in table_name\n" +
" CONSTRAINT fk_gtms_table_name FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name)," +
" CONSTRAINT fk_gtms_srs FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys (srs_id));";
}
private static String getTileMatrixCreationSql()
{
// http://www.geopackage.org/spec/#tile_matrix
// http://www.geopackage.org/spec/#gpkg_tile_matrix_sql
return "CREATE TABLE " + GeoPackageTiles.MatrixTableName + '\n' +
"(table_name TEXT NOT NULL, -- Tile Pyramid User Data Table Name\n" +
" zoom_level INTEGER NOT NULL, -- 0 <= zoom_level <= max_level for table_name\n" +
" matrix_width INTEGER NOT NULL, -- Number of columns (>= 1) in tile matrix at this zoom level\n" +
" matrix_height INTEGER NOT NULL, -- Number of rows (>= 1) in tile matrix at this zoom level\n" +
" tile_width INTEGER NOT NULL, -- Tile width in pixels (>= 1) for this zoom level\n" +
" tile_height INTEGER NOT NULL, -- Tile height in pixels (>= 1) for this zoom level\n" +
" pixel_x_size DOUBLE NOT NULL, -- In t_table_name srid units or default meters for srid 0 (>0)\n" +
" pixel_y_size DOUBLE NOT NULL, -- In t_table_name srid units or default meters for srid 0 (>0)\n" +
" CONSTRAINT pk_ttm PRIMARY KEY (table_name, zoom_level)," +
" CONSTRAINT fk_tmm_table_name FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name));";
}
private static String getTileSetCreationSql(final String tileTableName)
{
// http://www.geopackage.org/spec/#tiles_user_tables
// http://www.geopackage.org/spec/#_sample_tile_pyramid_informative
return "CREATE TABLE " + tileTableName + '\n' +
"(id INTEGER PRIMARY KEY AUTOINCREMENT, -- Autoincrement primary key\n" +
" zoom_level INTEGER NOT NULL, -- min(zoom_level) <= zoom_level <= max(zoom_level) for t_table_name\n" +
" tile_column INTEGER NOT NULL, -- 0 to tile_matrix matrix_width - 1\n" +
" tile_row INTEGER NOT NULL, -- 0 to tile_matrix matrix_height - 1\n" +
" tile_data BLOB NOT NULL, -- Of an image MIME type specified in clauses Tile Encoding PNG, Tile Encoding JPEG, Tile Encoding WEBP\n" +
" UNIQUE (zoom_level, tile_column, tile_row));";
}
private TileMatrix getTileMatrix(final String tileTableName, final int zoomLevel) throws SQLException
{
if(tileTableName == null || tileTableName.isEmpty())
{
throw new IllegalArgumentException("Tile table name cannot be null or empty");
}
final String tileQuery = String.format("SELECT %s, %s, %s, %s, %s, %s FROM %s WHERE table_name = ? AND zoom_level = ?;",
"matrix_width",
"matrix_height",
"tile_width",
"tile_height",
"pixel_x_size",
"pixel_y_size",
GeoPackageTiles.MatrixTableName);
return JdbcUtility.selectOne(this.databaseConnection,
tileQuery,
preparedStatement -> { preparedStatement.setString(1, tileTableName);
preparedStatement.setInt (2, zoomLevel);
},
resultSet -> new TileMatrix(tileTableName,
zoomLevel,
resultSet.getInt (1), // matrix width
resultSet.getInt (2), // matrix height
resultSet.getInt (3), // tile width
resultSet.getInt (4), // tile height
resultSet.getDouble(5), // pixel x size
resultSet.getDouble(6))); // pixel y size
}
/**
* Rounds the bounds to the appropriate level of accuracy
* (2 decimal places for meters, 7 decimal places for degrees)
* @param bounds
* A {@link BoundingBox} that needs to be rounded
* @param precision
* The Coordinate Reference System of the bounds (to determine level of precision)
* @return A {@link BoundingBox} with the minimum values rounded down, and the maximum values rounded up to the specified level of precision
*/
private static BoundingBox roundBounds(final BoundingBox bounds, final int precision)
{
final double divisor = StrictMath.pow(10, precision);
return new BoundingBox(Math.floor(bounds.getMinimumX()*divisor) / divisor,
Math.floor(bounds.getMinimumY()*divisor) / divisor,
Math.ceil (bounds.getMaximumX()*divisor) / divisor,
Math.ceil (bounds.getMaximumY()*divisor) / divisor);
}
private final GeoPackageCore core;
private final Connection databaseConnection;
/**
* The TileOrigin for GeoPackage's is UpperLeft
* http://www.geopackage.org/spec/#clause_tile_matrix_table_data_values
*/
public static final TileOrigin Origin = TileOrigin.UpperLeft; // http://www.geopackage.org/spec/#clause_tile_matrix_table_data_values
/**
* The String name "gpkg_tile_matrix_set" of the database Tiles table
* containing the Bounding Box information
* http://www.geopackage.org/spec/#_tile_matrix_set
*/
static final String MatrixSetTableName = "gpkg_tile_matrix_set";
/**
* The String name "gpkg_tile_matrix" of the database Tiles table containing
* the pixel x and y values, TileMatrixDimensions at a particular zoom level
* http://www.geopackage.org/spec/#tile_matrix
*/
static final String MatrixTableName = "gpkg_tile_matrix";
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy