package mil.nga.geopackage.tiles; import java.awt.Graphics; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.ArrayList; import java.util.List; import mil.nga.geopackage.BoundingBox; import mil.nga.geopackage.GeoPackageException; import mil.nga.geopackage.extension.scale.TileScaling; import mil.nga.geopackage.extension.scale.TileScalingType; import mil.nga.geopackage.tiles.matrix.TileMatrix; import mil.nga.geopackage.tiles.matrixset.TileMatrixSet; import mil.nga.geopackage.tiles.user.TileDao; import mil.nga.geopackage.tiles.user.TileResultSet; import mil.nga.geopackage.tiles.user.TileRow; import mil.nga.sf.proj.Projection; import mil.nga.sf.proj.ProjectionTransform; import org.locationtech.proj4j.ProjCoordinate; /** * Tile Creator, creates a tile from a tile matrix to the desired projection * * @author osbornb * @since 1.2.0 */ public class TileCreator { /** * Tile DAO */ private final TileDao tileDao; /** * Tile width */ private final Integer width; /** * Tile height */ private final Integer height; /** * Tile Matrix Set */ private final TileMatrixSet tileMatrixSet; /** * Projection of the requests */ private final Projection requestProjection; /** * Projection of the tiles */ private final Projection tilesProjection; /** * Tile Set bounding box */ private final BoundingBox tileSetBoundingBox; /** * Flag indicating the the tile and request projections are the same */ private final boolean sameProjection; /** * Tile Scaling options */ private TileScaling scaling; /** * Image format */ private final String imageFormat; /** * Constructor * * @param tileDao * tile dao * @param width * request width * @param height * request height * @param requestProjection * request projection * @param imageFormat * image format */ public TileCreator(TileDao tileDao, Integer width, Integer height, Projection requestProjection, String imageFormat) { this.tileDao = tileDao; this.width = width; this.height = height; this.requestProjection = requestProjection; this.imageFormat = imageFormat; if (imageFormat == null && (width != null || height != null)) { throw new GeoPackageException( "The width and height request size can not be specified when requesting raw tiles (no image format specified)"); } tileMatrixSet = tileDao.getTileMatrixSet(); tilesProjection = tileDao.getTileMatrixSet().getProjection(); tileSetBoundingBox = tileMatrixSet.getBoundingBox(); // Check if the projections have the same units sameProjection = (requestProjection.getUnit().name .equals(tilesProjection.getUnit().name)); if (imageFormat == null && !sameProjection) { throw new GeoPackageException( "The requested projection must be the same as the stored tiles when requesting raw tiles (no image format specified)"); } } /** * Constructor, use the tile tables image width and height as request size * * @param tileDao * tile dao * @param imageFormat * image format */ public TileCreator(TileDao tileDao, String imageFormat) { this(tileDao, null, null, tileDao.getProjection(), imageFormat); } /** * Constructor, use the tile tables image width and height as request size * and request as the specified projection * * @param tileDao * tile dao * @param requestProjection * request projection * @param imageFormat * image format */ public TileCreator(TileDao tileDao, Projection requestProjection, String imageFormat) { this(tileDao, null, null, requestProjection, imageFormat); } /** * Constructor, request raw tile images directly from the tile table in * their original size * * @param tileDao * tile dao */ public TileCreator(TileDao tileDao) { this(tileDao, null, null, tileDao.getProjection(), null); } /** * Get the tile dao * * @return tile dao */ public TileDao getTileDao() { return tileDao; } /** * Get the requested tile width * * @return width */ public Integer getWidth() { return width; } /** * Get the requested tile height * * @return height */ public Integer getHeight() { return height; } /** * Get the tile matrix set * * @return tile matrix set */ public TileMatrixSet getTileMatrixSet() { return tileMatrixSet; } /** * Get the request projection * * @return request projection */ public Projection getRequestProjection() { return requestProjection; } /** * Get the tiles projection * * @return tiles projection */ public Projection getTilesProjection() { return tilesProjection; } /** * Get the tile set bounding box * * @return tile set bounding box */ public BoundingBox getTileSetBoundingBox() { return tileSetBoundingBox; } /** * Is the request and tile projection the same * * @return true if the same */ public boolean isSameProjection() { return sameProjection; } /** * Get the tile scaling options * * @return tile scaling options * @since 2.0.2 */ public TileScaling getScaling() { return scaling; } /** * Set the tile scaling options * * @param scaling * tile scaling options * @since 2.0.2 */ public void setScaling(TileScaling scaling) { this.scaling = scaling; } /** * Get the requested image format * * @return image format */ public String getImageFormat() { return imageFormat; } /** * Check if the tile table contains a tile for the request bounding box * * @param requestBoundingBox * request bounding box in the request projection * @return true if a tile exists */ public boolean hasTile(BoundingBox requestBoundingBox) { boolean hasTile = false; // Transform to the projection of the tiles ProjectionTransform transformRequestToTiles = requestProjection .getTransformation(tilesProjection); BoundingBox tilesBoundingBox = requestBoundingBox .transform(transformRequestToTiles); List<TileMatrix> tileMatrices = getTileMatrices(tilesBoundingBox); for (int i = 0; !hasTile && i < tileMatrices.size(); i++) { TileMatrix tileMatrix = tileMatrices.get(i); TileResultSet tileResults = retrieveTileResults(tilesBoundingBox, tileMatrix); if (tileResults != null) { try { hasTile = tileResults.getCount() > 0; } finally { tileResults.close(); } } } return hasTile; } /** * Get the tile from the request bounding box in the request projection * * @param requestBoundingBox * request bounding box in the request projection * @return image */ public GeoPackageTile getTile(BoundingBox requestBoundingBox) { GeoPackageTile tile = null; // Transform to the projection of the tiles ProjectionTransform transformRequestToTiles = requestProjection .getTransformation(tilesProjection); BoundingBox tilesBoundingBox = requestBoundingBox .transform(transformRequestToTiles); List<TileMatrix> tileMatrices = getTileMatrices(tilesBoundingBox); for (int i = 0; tile == null && i < tileMatrices.size(); i++) { TileMatrix tileMatrix = tileMatrices.get(i); TileResultSet tileResults = retrieveTileResults(tilesBoundingBox, tileMatrix); if (tileResults != null) { try { if (tileResults.getCount() > 0) { BoundingBox requestProjectedBoundingBox = requestBoundingBox .transform(transformRequestToTiles); // Determine the requested tile dimensions, or use the // dimensions of a single tile matrix tile int requestedTileWidth = width != null ? width : (int) tileMatrix.getTileWidth(); int requestedTileHeight = height != null ? height : (int) tileMatrix.getTileHeight(); // Determine the size of the tile to initially draw int tileWidth = requestedTileWidth; int tileHeight = requestedTileHeight; if (!sameProjection) { tileWidth = (int) Math .round((requestProjectedBoundingBox .getMaxLongitude() - requestProjectedBoundingBox .getMinLongitude()) / tileMatrix.getPixelXSize()); tileHeight = (int) Math .round((requestProjectedBoundingBox .getMaxLatitude() - requestProjectedBoundingBox .getMinLatitude()) / tileMatrix.getPixelYSize()); } // Draw the resulting bitmap with the matching tiles GeoPackageTile geoPackageTile = drawTile(tileMatrix, tileResults, requestProjectedBoundingBox, tileWidth, tileHeight); // Create the tile if (geoPackageTile != null) { // Project the tile if needed if (!sameProjection && geoPackageTile.getImage() != null) { BufferedImage reprojectTile = reprojectTile( geoPackageTile.getImage(), requestedTileWidth, requestedTileHeight, requestBoundingBox, transformRequestToTiles, tilesBoundingBox); geoPackageTile = new GeoPackageTile( requestedTileWidth, requestedTileHeight, reprojectTile); } tile = geoPackageTile; } } } finally { tileResults.close(); } } } return tile; } /** * Draw the tile from the tile results * * @param tileMatrix * @param tileResults * @param requestProjectedBoundingBox * @param tileWidth * @param tileHeight * @return tile bitmap */ private GeoPackageTile drawTile(TileMatrix tileMatrix, TileResultSet tileResults, BoundingBox requestProjectedBoundingBox, int tileWidth, int tileHeight) { // Draw the resulting bitmap with the matching tiles GeoPackageTile geoPackageTile = null; Graphics graphics = null; while (tileResults.moveToNext()) { // Get the next tile TileRow tileRow = tileResults.getRow(); BufferedImage tileDataImage; try { tileDataImage = tileRow.getTileDataImage(); } catch (IOException e) { throw new GeoPackageException( "Failed to read the tile row image data", e); } // Get the bounding box of the tile BoundingBox tileBoundingBox = TileBoundingBoxUtils.getBoundingBox( tileSetBoundingBox, tileMatrix, tileRow.getTileColumn(), tileRow.getTileRow()); // Get the bounding box where the requested image and // tile overlap BoundingBox overlap = requestProjectedBoundingBox .overlap(tileBoundingBox); // If the tile overlaps with the requested box if (overlap != null) { // Get the rectangle of the tile image to draw ImageRectangle src = TileBoundingBoxJavaUtils.getRectangle( tileMatrix.getTileWidth(), tileMatrix.getTileHeight(), tileBoundingBox, overlap); // Get the rectangle of where to draw the tile in // the resulting image ImageRectangle dest = TileBoundingBoxJavaUtils.getRectangle( tileWidth, tileHeight, requestProjectedBoundingBox, overlap); if (src.isValid() && dest.isValid()) { if (imageFormat != null) { // Create the bitmap first time through if (geoPackageTile == null) { BufferedImage bufferedImage = ImageUtils .createBufferedImage(tileWidth, tileHeight, imageFormat); graphics = bufferedImage.getGraphics(); geoPackageTile = new GeoPackageTile(tileWidth, tileHeight, bufferedImage); } // Draw the tile to the image graphics.drawImage(tileDataImage, dest.getLeft(), dest.getTop(), dest.getRight(), dest.getBottom(), src.getLeft(), src.getTop(), src.getRight(), src.getBottom(), null); } else { // Verify only one image was found and // it lines up perfectly if (geoPackageTile != null || !src.equals(dest)) { throw new GeoPackageException( "Raw image only supported when the images are aligned with the tile format requiring no combining and cropping"); } geoPackageTile = new GeoPackageTile(tileWidth, tileHeight, tileRow.getTileData()); } } } } // Check if the entire image is transparent if (geoPackageTile != null && geoPackageTile.getImage() != null && ImageUtils.isFullyTransparent(geoPackageTile.getImage())) { geoPackageTile = null; } return geoPackageTile; } /** * Reproject the tile to the requested projection * * @param tile * tile in the tile matrix projection * @param requestedTileWidth * requested tile width * @param requestedTileHeight * requested tile height * @param requestBoundingBox * request bounding box in the request projection * @param transformRequestToTiles * transformation from request to tiles * @param tilesBoundingBox * request bounding box in the tile matrix projection * @return projected tile */ private BufferedImage reprojectTile(BufferedImage tile, int requestedTileWidth, int requestedTileHeight, BoundingBox requestBoundingBox, ProjectionTransform transformRequestToTiles, BoundingBox tilesBoundingBox) { final double requestedWidthUnitsPerPixel = (requestBoundingBox .getMaxLongitude() - requestBoundingBox.getMinLongitude()) / requestedTileWidth; final double requestedHeightUnitsPerPixel = (requestBoundingBox .getMaxLatitude() - requestBoundingBox.getMinLatitude()) / requestedTileHeight; final double tilesDistanceWidth = tilesBoundingBox.getMaxLongitude() - tilesBoundingBox.getMinLongitude(); final double tilesDistanceHeight = tilesBoundingBox.getMaxLatitude() - tilesBoundingBox.getMinLatitude(); final int width = tile.getWidth(); final int height = tile.getHeight(); // Tile pixels of the tile matrix tiles int[] pixels = new int[width * height]; tile.getRGB(0, 0, width, height, pixels, 0, width); // Projected tile pixels to draw the reprojected tile int[] projectedPixels = new int[requestedTileWidth * requestedTileHeight]; // Retrieve each pixel in the new tile from the unprojected tile for (int y = 0; y < requestedTileHeight; y++) { for (int x = 0; x < requestedTileWidth; x++) { double longitude = requestBoundingBox.getMinLongitude() + (x * requestedWidthUnitsPerPixel); double latitude = requestBoundingBox.getMaxLatitude() - (y * requestedHeightUnitsPerPixel); ProjCoordinate fromCoord = new ProjCoordinate(longitude, latitude); ProjCoordinate toCoord = transformRequestToTiles .transform(fromCoord); double projectedLongitude = toCoord.x; double projectedLatitude = toCoord.y; int xPixel = (int) Math .round(((projectedLongitude - tilesBoundingBox .getMinLongitude()) / tilesDistanceWidth) * width); int yPixel = (int) Math .round(((tilesBoundingBox.getMaxLatitude() - projectedLatitude) / tilesDistanceHeight) * height); xPixel = Math.max(0, xPixel); xPixel = Math.min(width - 1, xPixel); yPixel = Math.max(0, yPixel); yPixel = Math.min(height - 1, yPixel); int color = pixels[(yPixel * width) + xPixel]; projectedPixels[(y * requestedTileWidth) + x] = color; } } // Draw the new image BufferedImage projectedTileImage = new BufferedImage( requestedTileWidth, requestedTileHeight, tile.getType()); projectedTileImage.setRGB(0, 0, requestedTileWidth, requestedTileHeight, projectedPixels, 0, requestedTileWidth); return projectedTileImage; } /** * Get the tile matrices that may contain the tiles for the bounding box, * matches against the bounding box and zoom level options * * @param projectedRequestBoundingBox * bounding box projected to the tiles * @return tile matrices */ private List<TileMatrix> getTileMatrices( BoundingBox projectedRequestBoundingBox) { List<TileMatrix> tileMatrices = new ArrayList<>(); // Check if the request overlaps the tile matrix set if (!tileDao.getTileMatrices().isEmpty() && projectedRequestBoundingBox.intersects(tileSetBoundingBox)) { // Get the tile distance double distanceWidth = projectedRequestBoundingBox .getMaxLongitude() - projectedRequestBoundingBox.getMinLongitude(); double distanceHeight = projectedRequestBoundingBox .getMaxLatitude() - projectedRequestBoundingBox.getMinLatitude(); // Get the zoom level to request based upon the tile size Long requestZoomLevel = null; if (scaling != null) { // When options are provided, get the approximate zoom level // regardless of whether a tile level exists requestZoomLevel = tileDao.getApproximateZoomLevel( distanceWidth, distanceHeight); } else { // Get the closest existing zoom level requestZoomLevel = tileDao.getZoomLevel(distanceWidth, distanceHeight); } // If there is a matching zoom level if (requestZoomLevel != null) { List<Long> zoomLevels = null; // If options are configured, build the possible zoom levels in // order to request if (scaling != null && scaling.getScalingType() != null) { // Find zoom in levels List<Long> zoomInLevels = new ArrayList<>(); if (scaling.isZoomIn()) { long zoomIn = scaling.getZoomIn() != null ? requestZoomLevel + scaling.getZoomIn() : tileDao.getMaxZoom(); for (long zoomLevel = requestZoomLevel + 1; zoomLevel <= zoomIn; zoomLevel++) { zoomInLevels.add(zoomLevel); } } // Find zoom out levels List<Long> zoomOutLevels = new ArrayList<>(); if (scaling.isZoomOut()) { long zoomOut = scaling.getZoomOut() != null ? requestZoomLevel - scaling.getZoomOut() : tileDao.getMinZoom(); for (long zoomLevel = requestZoomLevel - 1; zoomLevel >= zoomOut; zoomLevel--) { zoomOutLevels.add(zoomLevel); } } if (zoomInLevels.isEmpty()) { // Only zooming out zoomLevels = zoomOutLevels; } else if (zoomOutLevels.isEmpty()) { // Only zooming in zoomLevels = zoomInLevels; } else { // Determine how to order the zoom in and zoom out // levels TileScalingType type = scaling.getScalingType(); switch (type) { case IN: case IN_OUT: // Order zoom in levels before zoom out levels zoomLevels = zoomInLevels; zoomLevels.addAll(zoomOutLevels); break; case OUT: case OUT_IN: // Order zoom out levels before zoom in levels zoomLevels = zoomOutLevels; zoomLevels.addAll(zoomInLevels); break; case CLOSEST_IN_OUT: case CLOSEST_OUT_IN: // Alternate the zoom in and out levels List<Long> firstLevels; List<Long> secondLevels; if (type == TileScalingType.CLOSEST_IN_OUT) { // Alternate starting with zoom in firstLevels = zoomInLevels; secondLevels = zoomOutLevels; } else { // Alternate starting with zoom out firstLevels = zoomOutLevels; secondLevels = zoomInLevels; } zoomLevels = new ArrayList<>(); int maxLevels = Math.max(firstLevels.size(), secondLevels.size()); for (int i = 0; i < maxLevels; i++) { if (i < firstLevels.size()) { zoomLevels.add(firstLevels.get(i)); } if (i < secondLevels.size()) { zoomLevels.add(secondLevels.get(i)); } } break; default: throw new GeoPackageException("Unsupported " + TileScalingType.class.getSimpleName() + ": " + type); } } } else { zoomLevels = new ArrayList<>(); } // Always check the request zoom level first zoomLevels.add(0, requestZoomLevel); // Build a list of tile matrices that exist for the zoom levels for (long zoomLevel : zoomLevels) { TileMatrix tileMatrix = tileDao.getTileMatrix(zoomLevel); if (tileMatrix != null) { tileMatrices.add(tileMatrix); } } } } return tileMatrices; } /** * Get the tile row results of tiles needed to draw the requested bounding * box tile * * @param projectedRequestBoundingBox * bounding box projected to the tiles * @param tileMatrix * @return tile cursor results or null */ private TileResultSet retrieveTileResults( BoundingBox projectedRequestBoundingBox, TileMatrix tileMatrix) { TileResultSet tileResults = null; if (tileMatrix != null) { // Get the tile grid TileGrid tileGrid = TileBoundingBoxUtils.getTileGrid( tileSetBoundingBox, tileMatrix.getMatrixWidth(), tileMatrix.getMatrixHeight(), projectedRequestBoundingBox); // Query for matching tiles in the tile grid tileResults = tileDao.queryByTileGrid(tileGrid, tileMatrix.getZoomLevel()); } return tileResults; } }