/* 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.store.tiles.geopackage; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.sql.SQLException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.stream.Collectors; import javax.activation.MimeType; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; 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.coordinate.referencesystem.profile.CrsProfile; import com.rgi.common.coordinate.referencesystem.profile.CrsProfileFactory; import com.rgi.common.tile.TileOrigin; import com.rgi.common.tile.scheme.TileMatrixDimensions; import com.rgi.common.tile.scheme.TileScheme; import com.rgi.common.util.ImageUtility; import com.rgi.common.util.MimeTypeUtility; import com.rgi.geopackage.GeoPackage; import com.rgi.geopackage.core.SpatialReferenceSystem; import com.rgi.geopackage.tiles.GeoPackageTiles; import com.rgi.geopackage.tiles.TileMatrix; import com.rgi.geopackage.tiles.TileSet; import com.rgi.geopackage.verification.ConformanceException; import com.rgi.store.tiles.TileStoreException; import com.rgi.store.tiles.TileStoreWriter; /** * @author Luke Lambert * */ public class GeoPackageWriter implements TileStoreWriter { /** * @param geoPackageFile * Handle to a new or existing GeoPackage file * @param coordinateReferenceSystem * Coordinate reference system * @param tileSetTableName * Name for the new tile set's table in the GeoPackage database * @param tileSetIdentifier * A human-readable identifier (e.g. short name) for the tile set * @param tileSetDescription * A human-readable description of the tile set * @param tileSetBounds * Minimum bounds of the tile set, in spatial reference system * units * @param tileScheme * Contains the mechanism to calculate the relationship between * the tile matrix dimensions at valid zoom levels * @param imageOutputFormat * Image format for used for output * @param imageWriteOptions * Controls details of the image writing process. If null, a * default ImageWriteParam used instead * @throws TileStoreException * if there's an error in constructing the underlying tile store implementation * */ public GeoPackageWriter(final File geoPackageFile, final CoordinateReferenceSystem coordinateReferenceSystem, final String tileSetTableName, final String tileSetIdentifier, final String tileSetDescription, final BoundingBox tileSetBounds, final TileScheme tileScheme, final MimeType imageOutputFormat, final ImageWriteParam imageWriteOptions) throws TileStoreException { if(geoPackageFile == null) { throw new IllegalArgumentException("GeoPackageFile cannot be null."); } if(coordinateReferenceSystem == null) { throw new IllegalArgumentException("Coordinate reference system cannot be null"); } if(imageOutputFormat == null) { throw new IllegalArgumentException("Image output format may not be null"); } if(!MimeTypeUtility.contains(GeoPackageWriter.SupportedImageFormats, imageOutputFormat)) { throw new IllegalArgumentException(String.format("Image output type '%s' is inappropriate for this tile store. Valid formats are: %s", imageOutputFormat.toString(), GeoPackageWriter.SupportedImageFormats .stream() .map(MimeType::toString) .collect(Collectors.joining(", ", "'", "'")))); } if(geoPackageFile.getParentFile() != null && !geoPackageFile.getParentFile().isDirectory()) { if(!geoPackageFile.getParentFile().mkdirs()) { throw new RuntimeException("Unable to create file: " + geoPackageFile.getPath()); } } try { this.imageWriter = ImageIO.getImageWritersByMIMEType(imageOutputFormat.toString()).next(); } catch(final NoSuchElementException ignored) { throw new IllegalArgumentException(String.format("Mime type '%s' is not a supported for image writing by your Java environment", imageOutputFormat.toString())); } try { this.geoPackage = new GeoPackage(geoPackageFile, GeoPackage.OpenMode.OpenOrCreate); } catch(final ClassNotFoundException | ConformanceException | IOException | SQLException ex) { throw new TileStoreException(ex); } try { if(this.geoPackage.tiles().getTileSet(tileSetTableName) != null) { throw new IllegalArgumentException("Tile set table name must be unique in this GeoPackage"); } this.crsProfile = CrsProfileFactory.create(coordinateReferenceSystem); final SpatialReferenceSystem spatialReferenceSystem = this.geoPackage.core() .addSpatialReferenceSystem(this.crsProfile.getName(), this.crsProfile.getCoordinateReferenceSystem().getAuthority(), this.crsProfile.getCoordinateReferenceSystem().getIdentifier(), this.crsProfile.getWellKnownText(), this.crsProfile.getDescription()); this.tileSet = this.geoPackage.tiles() .addTileSet(tileSetTableName, tileSetIdentifier, tileSetDescription, tileSetBounds, spatialReferenceSystem); this.imageWriteOptions = imageWriteOptions; // May be null this.tileScheme = tileScheme; } catch(final Exception ex) { try { this.geoPackage.close(); } catch(final SQLException ex1) { ex1.printStackTrace(); } throw new TileStoreException(ex); } } @Override public void close() throws SQLException { this.geoPackage.close(); } @Override public Coordinate<Integer> crsToTileCoordinate(final CrsCoordinate coordinate, final int zoomLevel) throws TileStoreException { try { return this.geoPackage .tiles() .crsToTileCoordinate(this.tileSet, coordinate, this.crsProfile.getPrecision(), zoomLevel); } catch(final SQLException ex) { throw new TileStoreException(ex); } } @Override public CrsCoordinate tileToCrsCoordinate(final int column, final int row, final int zoomLevel, final TileOrigin corner) throws TileStoreException { if(corner == null) { throw new IllegalArgumentException("Corner may not be null"); } try { final TileMatrixDimensions dimensions = this.tileScheme.dimensions(zoomLevel); final Coordinate<Integer> tileCoordinate = corner.transform(GeoPackageTiles.Origin, column, row, dimensions); return this.geoPackage .tiles() .tileToCrsCoordinate(this.tileSet, tileCoordinate.getX() + corner.getHorizontal(), tileCoordinate.getY() + GeoPackageTiles.Origin.getVertical() - corner.getVertical(), zoomLevel); } catch(final SQLException ex) { throw new TileStoreException(ex); } } @Override public BoundingBox getTileBoundingBox(final int column, final int row, final int zoomLevel) throws TileStoreException { final Coordinate<Double> lowerLeft = this.tileToCrsCoordinate(column, row, zoomLevel, TileOrigin.LowerLeft); final Coordinate<Double> upperRight = this.tileToCrsCoordinate(column, row, zoomLevel, TileOrigin.UpperRight); return new BoundingBox(lowerLeft.getX(), lowerLeft.getY(), upperRight.getX(), upperRight.getY()); } @Override public void addTile(final CrsCoordinate coordinate, final int zoomLevel, final BufferedImage image) throws TileStoreException { if(coordinate == null) { throw new IllegalArgumentException("Coordinate may not be null"); } if(image == null) { throw new IllegalArgumentException("Image may not be null"); } if(!coordinate.getCoordinateReferenceSystem().equals(this.crsProfile.getCoordinateReferenceSystem())) { throw new IllegalArgumentException("Coordinate's coordinate reference system does not match the tile store's coordinate reference system"); } try { this.geoPackage .tiles() .addTile(this.tileSet, this.getTileMatrix(zoomLevel, image.getWidth(), image.getHeight()), coordinate, this.crsProfile.getPrecision(), ImageUtility.bufferedImageToBytes(image, this.imageWriter, this.imageWriteOptions)); } catch(final SQLException | IOException ex) { throw new TileStoreException(ex); } } @Override public void addTile(final int column, final int row, final int zoomLevel, final BufferedImage image) throws TileStoreException { if(image == null) { throw new IllegalArgumentException("Image may not be null"); } try { this.geoPackage .tiles() .addTile(this.tileSet, this.getTileMatrix(zoomLevel, image.getWidth(), image.getHeight()), column, row, ImageUtility.bufferedImageToBytes(image, this.imageWriter, this.imageWriteOptions)); } catch(final SQLException | IOException ex) { throw new TileStoreException(ex); } } @Override public Set<MimeType> getSupportedImageFormats() { return Collections.unmodifiableSet(GeoPackageWriter.SupportedImageFormats); } @Override public CoordinateReferenceSystem getCoordinateReferenceSystem() { return this.crsProfile.getCoordinateReferenceSystem(); } @Override public TileScheme getTileScheme() { return this.tileScheme; } @Override public TileOrigin getTileOrigin() { return GeoPackageTiles.Origin; } private TileMatrix getTileMatrix(final int zoomLevel, final int imageWidth, final int imageHeight) throws SQLException { if(this.tileMatrices.containsKey(zoomLevel)) { return this.tileMatrices.get(zoomLevel); } final TileMatrix tileMatrix = this.addTileMatrix(zoomLevel, imageHeight, imageWidth); this.tileMatrices.put(zoomLevel, tileMatrix); return tileMatrix; } private TileMatrix addTileMatrix(final int zoomLevel, final int tilePixelHeight, final int tilePixelWidth) throws SQLException { final TileMatrixDimensions tileMatrixDimensions = this.tileScheme.dimensions(zoomLevel); return this.geoPackage.tiles() .addTileMatrix(this.geoPackage .tiles() .getTileMatrixSet(this.tileSet), zoomLevel, tileMatrixDimensions.getWidth(), tileMatrixDimensions.getHeight(), tilePixelWidth, tilePixelHeight ); } private final GeoPackage geoPackage; private final TileSet tileSet; private final CrsProfile crsProfile; private final ImageWriter imageWriter; private final ImageWriteParam imageWriteOptions; private final TileScheme tileScheme; private final Map<Integer, TileMatrix> tileMatrices = new HashMap<>(); /** * Image formats supported by an unextended GeoPackage */ public static final Set<MimeType> SupportedImageFormats = MimeTypeUtility.createMimeTypeSet("image/jpeg", "image/png"); }