/* 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 com.rgi.common.BoundingBox; import com.rgi.common.Dimensions; import com.rgi.common.Range; 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.geopackage.GeoPackage; import com.rgi.geopackage.core.SpatialReferenceSystem; import com.rgi.geopackage.tiles.GeoPackageTiles; import com.rgi.geopackage.tiles.Tile; import com.rgi.geopackage.tiles.TileCoordinate; import com.rgi.geopackage.tiles.TileMatrix; import com.rgi.geopackage.tiles.TileMatrixSet; import com.rgi.geopackage.tiles.TileSet; import com.rgi.geopackage.verification.VerificationLevel; import com.rgi.store.tiles.TileHandle; import com.rgi.store.tiles.TileStoreException; import com.rgi.store.tiles.TileStoreReader; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.sql.SQLException; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; /** * @author Luke Lambert * */ public class GeoPackageReader implements TileStoreReader { /** * @param geoPackageFile * Handle to a new or existing GeoPackage file * @param tileSetTableName * Name for the new tile set's table in the GeoPackage database * @throws TileStoreException * if there's an error in constructing the underlying tile store implementation */ public GeoPackageReader(final File geoPackageFile, final String tileSetTableName) throws TileStoreException { this(geoPackageFile, tileSetTableName, VerificationLevel.Fast); } /** * @param geoPackageFile * Handle to a new or existing GeoPackage file * @param tileSetTableName * Name for the new tile set's table in the GeoPackage database * @param verificationLevel * Controls the level of verification testing performed on this * GeoPackage. If verificationLevel is not None * {@link GeoPackage#verify()} is called automatically and will throw if * there are any conformance violations with the severity * {@link com.rgi.geopackage.verification.Severity#Error}. Throwing from this method means * that it won't be possible to instantiate a GeoPackage object * based on an SQLite "GeoPackage" file with severe errors. * @throws TileStoreException * if there's an error in constructing the underlying tile store implementation */ public GeoPackageReader(final File geoPackageFile, final String tileSetTableName, final VerificationLevel verificationLevel) throws TileStoreException { if(geoPackageFile == null) { throw new IllegalArgumentException("GeoPackage file may not be null"); } if(tileSetTableName == null) { throw new IllegalArgumentException("Tile set may not be null or empty"); } try { this.geoPackage = new GeoPackage(geoPackageFile, verificationLevel, GeoPackage.OpenMode.Open); } catch(final Exception ex) { throw new TileStoreException(ex); } try { this.tileSet = this.geoPackage.tiles().getTileSet(tileSetTableName); if(this.tileSet == null) { throw new IllegalArgumentException("Table name does not specify a valid GeoPackage tile set"); } final SpatialReferenceSystem srs = this.geoPackage.core().getSpatialReferenceSystem(this.tileSet.getSpatialReferenceSystemIdentifier()); if(srs == null) { throw new IllegalArgumentException("SRS may not be null"); } this.crsProfile = CrsProfileFactory.create(srs.getOrganization(), srs.getOrganizationSrsId()); this.zoomLevels = this.geoPackage.tiles().getTileZoomLevels(this.tileSet); this.tileMatrixSet = this.geoPackage.tiles().getTileMatrixSet(this.tileSet); this.tileMatrices = this.geoPackage.tiles() .getTileMatrices(this.tileSet) .stream() .collect(Collectors.toMap(TileMatrix::getZoomLevel, tileMatrix -> tileMatrix)); this.tileScheme = new TileScheme() { @Override public TileMatrixDimensions dimensions(final int zoomLevel) { if(GeoPackageReader.this.tileMatrices.containsKey(zoomLevel)) { final TileMatrix tileMatrix = GeoPackageReader.this.tileMatrices.get(zoomLevel); return new TileMatrixDimensions(tileMatrix.getMatrixWidth(), tileMatrix.getMatrixHeight()); } throw new IllegalArgumentException(String.format("Zoom level must be in the range %s", new Range<>(GeoPackageReader.this.tileMatrices.keySet(), Integer::compare))); } @Override public Collection<Integer> getZoomLevels() { try { return GeoPackageReader.this.geoPackage.tiles().getTileZoomLevels(GeoPackageReader.this.tileSet); } catch(final SQLException ex) { throw new RuntimeException(ex); } } }; } catch(final Exception ex) { try { this.geoPackage.close(); } catch(final SQLException ex1) { throw new TileStoreException(ex1); } throw new TileStoreException(ex); } } @Override public void close() throws SQLException { this.geoPackage.close(); } @Override public BoundingBox getBounds() throws TileStoreException { return this.tileMatrixSet.getBoundingBox(); } @Override public long countTiles() throws TileStoreException { // TODO lazy precalculation ? try { return this.geoPackage.core().getRowCount(this.tileSet); } catch(final SQLException ex) { throw new TileStoreException(ex); } } @Override public long getByteSize() throws TileStoreException { // TODO lazy precalculation ? return this.geoPackage.getFile().getTotalSpace(); } @Override public BufferedImage getTile(final int column, final int row, final int zoomLevel) throws TileStoreException { try { return getImage(this.geoPackage .tiles() .getTile(this.tileSet, column, row, zoomLevel)); } catch(final SQLException ex) { throw new TileStoreException(ex); } } @Override public BufferedImage getTile(final CrsCoordinate coordinate, final int zoomLevel) throws TileStoreException { if(coordinate == null) { throw new IllegalArgumentException("Coordinate may not be null"); } if(!coordinate.getCoordinateReferenceSystem().equals(this.getCoordinateReferenceSystem())) { throw new IllegalArgumentException("Coordinate's coordinate reference system does not match the tile store's coordinate reference system"); } try { return getImage(this.geoPackage .tiles() .getTile(this.tileSet, coordinate, this.crsProfile.getPrecision(), zoomLevel)); } catch(final IllegalArgumentException ignored) // This is to catch an IAE if the crsCoordinate requested is outside the bounds of the GeoPackage tiles BoundingBox { return null; } catch(final SQLException ex) { throw new TileStoreException(ex); } } @Override public CoordinateReferenceSystem getCoordinateReferenceSystem() { return this.crsProfile.getCoordinateReferenceSystem(); } @Override public Set<Integer> getZoomLevels() throws TileStoreException { return Collections.unmodifiableSet(this.zoomLevels); } @Override public Stream<TileHandle> stream() throws TileStoreException { try { return this.geoPackage .tiles() .getTiles(this.tileSet) .map(tileCoordinate -> this.getTileHandle(tileCoordinate.getZoomLevel(), tileCoordinate.getColumn(), tileCoordinate.getRow())); } catch(final SQLException ex) { throw new TileStoreException(ex); } } @Override public Stream<TileHandle> stream(final int zoomLevel) throws TileStoreException { try { return this.geoPackage .tiles() .getTiles(this.tileSet, zoomLevel) .map(tileCoordinate -> this.getTileHandle(zoomLevel, tileCoordinate.getX(), tileCoordinate.getY())); } catch(final SQLException ex) { throw new TileStoreException(ex); } } @Override public String getImageType() throws TileStoreException { try { final TileCoordinate coordinate = this.geoPackage .tiles() .getTiles(this.tileSet) .findFirst() .orElse(null); if(coordinate != null) { final Tile tile = this.geoPackage .tiles() .getTile(this.tileSet, coordinate.getColumn(), coordinate.getRow(), coordinate.getZoomLevel()); if(tile != null) { final byte[] imageData = tile.getImageData(); try(final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(imageData)) { try(final ImageInputStream imageInputStream = ImageIO.createImageInputStream(byteArrayInputStream)) { final Iterator<ImageReader> imageReaders = ImageIO.getImageReaders(imageInputStream); while(imageReaders.hasNext()) { final ImageReader imageReader = imageReaders.next(); final String[] names = imageReader.getOriginatingProvider().getFormatNames(); if(names != null && names.length > 0) { return names[0]; } } } } } } return null; } catch(final IOException | SQLException ex) { throw new TileStoreException(ex); } } @Override public Dimensions<Integer> getImageDimensions() throws TileStoreException { final TileHandle tile = this.stream().findFirst().orElse(null); if(tile != null) { final BufferedImage image = tile.getImage(); return new Dimensions<>(image.getWidth(), image.getHeight()); } return null; } @Override public String getName() { return String.format("%s-%s", this.geoPackage.getFile().getName(), this.tileSet.getIdentifier()); } @Override public TileScheme getTileScheme() { return this.tileScheme; } @Override public TileOrigin getTileOrigin() { return GeoPackageTiles.Origin; } private static BufferedImage getImage(final Tile tile) throws TileStoreException { if(tile == null) { return null; } try { return ImageUtility.bytesToBufferedImage(tile.getImageData()); } catch(final IOException ex) { throw new TileStoreException(ex); } } private TileHandle getTileHandle(final int zoomLevel, final int column, final int row) { final TileMatrix tileMatrix = GeoPackageReader.this.tileMatrices.get(zoomLevel); final TileMatrixDimensions matrix = new TileMatrixDimensions(tileMatrix.getMatrixWidth(), tileMatrix.getMatrixHeight()); return new TileHandle() { @Override public int getZoomLevel() { return zoomLevel; } @Override public int getColumn() { return column; } @Override public int getRow() { return row; } @Override public TileMatrixDimensions getMatrix() { return matrix; } @Override public CrsCoordinate getCrsCoordinate() throws TileStoreException { return GeoPackageReader.this .crsProfile .tileToCrsCoordinate(column, row, GeoPackageReader.this.getBounds(), matrix, GeoPackageTiles.Origin); } @Override public CrsCoordinate getCrsCoordinate(final TileOrigin corner) throws TileStoreException { return GeoPackageReader.this .crsProfile .tileToCrsCoordinate(column + corner.getHorizontal(), // same as: column - (GeoPackageTiles.Origin.getVertical() - corner.getHorizontal()) because GeoPackageTiles.Origin.getVertical() is always 0 row + (1 - corner.getVertical()), GeoPackageReader.this.getBounds(), matrix, GeoPackageTiles.Origin); } @Override public BoundingBox getBounds() throws TileStoreException { final Coordinate<Double> upperLeft = this.getCrsCoordinate(TileOrigin.UpperLeft); final Coordinate<Double> lowerRight = this.getCrsCoordinate(TileOrigin.LowerRight); return new BoundingBox(upperLeft.getX(), lowerRight.getY(), lowerRight.getX(), upperLeft.getY()); } @Override public BufferedImage getImage() throws TileStoreException { return GeoPackageReader.this.getTile(column, row, zoomLevel); } }; } private final GeoPackage geoPackage; private final TileSet tileSet; private final CrsProfile crsProfile; private final TileScheme tileScheme; private final Set<Integer> zoomLevels; private final Map<Integer, TileMatrix> tileMatrices; private final TileMatrixSet tileMatrixSet; }