package org.geotools.mbtiles.mosaic; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.Raster; import java.awt.image.SampleModel; import java.awt.image.WritableRaster; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.sql.SQLException; import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import javax.imageio.ImageIO; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import org.geotools.coverage.CoverageFactoryFinder; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridCoverageFactory; import org.geotools.coverage.grid.GridEnvelope2D; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader; import org.geotools.coverage.grid.io.AbstractGridFormat; import org.geotools.factory.Hints; import org.geotools.geometry.GeneralEnvelope; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.mbtiles.MBTilesFile; import org.geotools.mbtiles.MBTilesMetadata; import org.geotools.mbtiles.MBTilesTile; import org.geotools.referencing.CRS; import org.opengis.coverage.grid.Format; import org.opengis.parameter.GeneralParameterValue; import org.opengis.parameter.ParameterValue; import org.opengis.referencing.ReferenceIdentifier; import org.opengis.referencing.crs.CoordinateReferenceSystem; public class MBTilesReader extends AbstractGridCoverage2DReader { final static CoordinateReferenceSystem SPHERICAL_MERCATOR; final static CoordinateReferenceSystem WGS_84; static { try { SPHERICAL_MERCATOR = CRS.decode("EPSG:3857", true); WGS_84 = CRS.decode("EPSG:4326", true); } catch (Exception e) { throw new RuntimeException(e); } } protected final static ReferencedEnvelope WORLD_ENVELOPE = new ReferencedEnvelope(-20037508.34,20037508.34,-20037508.34,20037508.34, SPHERICAL_MERCATOR); protected final static int DEFAULT_TILE_SIZE = 256; protected final static int ZOOM_LEVEL_BASE = 2; protected GridCoverageFactory coverageFactory; protected MBTilesMetadata metadata; protected ReferencedEnvelope bounds; protected File sourceFile; public MBTilesReader(Object source, Hints hints) throws IOException { sourceFile = MBTilesFormat.getFileFromSource(source); MBTilesFile file = new MBTilesFile(sourceFile); metadata = file.loadMetaData(); try { bounds = ReferencedEnvelope.create(metadata.getBounds(), WGS_84).transform(SPHERICAL_MERCATOR, true); } catch (Exception e) { bounds = null; } originalEnvelope = new GeneralEnvelope(bounds == null ? WORLD_ENVELOPE : bounds); long maxZoom; try { maxZoom = file.maxZoom(); } catch (SQLException e) { throw new IOException(e); } long size = Math.round(Math.pow(ZOOM_LEVEL_BASE, maxZoom)) * DEFAULT_TILE_SIZE; highestRes = new double[] { WORLD_ENVELOPE.getSpan(0) / size, WORLD_ENVELOPE.getSpan(1) / size }; originalGridRange = new GridEnvelope2D(new Rectangle((int) size, (int) size)); coverageFactory = CoverageFactoryFinder.getGridCoverageFactory(this.hints); crs = SPHERICAL_MERCATOR; } @Override public Format getFormat() { return new MBTilesFormat(); } @Override public GridCoverage2D read(GeneralParameterValue[] parameters) throws IllegalArgumentException, IOException { MBTilesFile file = new MBTilesFile(sourceFile); ReferencedEnvelope requestedEnvelope = null; Rectangle dim = null; if (parameters != null) { for (int i = 0; i < parameters.length; i++) { final ParameterValue param = (ParameterValue) parameters[i]; final ReferenceIdentifier name = param.getDescriptor().getName(); if (name.equals(AbstractGridFormat.READ_GRIDGEOMETRY2D.getName())) { final GridGeometry2D gg = (GridGeometry2D) param.getValue(); try { requestedEnvelope = ReferencedEnvelope.create(gg.getEnvelope(), gg.getCoordinateReferenceSystem()).transform(SPHERICAL_MERCATOR, true);; } catch (Exception e) { requestedEnvelope = null; } dim = gg.getGridRange2D().getBounds(); continue; } } } if (requestedEnvelope == null) { requestedEnvelope = bounds; } long zoomLevel = 0; long leftTile, topTile, rightTile, bottomTile; if (requestedEnvelope != null && dim != null) { //find the closest zoom based on horizontal resolution double ratioWidth = requestedEnvelope.getSpan(0) / WORLD_ENVELOPE.getSpan(0); //proportion of total width that is being requested double propWidth = dim.getWidth() / ratioWidth; //this is the width in pixels that the whole world would have in the requested resolution zoomLevel = Math.round(Math.log(propWidth / DEFAULT_TILE_SIZE) / Math.log(ZOOM_LEVEL_BASE)); //the closest zoom level to the resolution, based on the formula width = zoom_base^zoom_level * tile_size -> zoom_level = log(width / tile_size)/log(zoom_base) } try { //now take a zoom level that is available in the database zoomLevel = file.closestZoom(zoomLevel); } catch (SQLException e1) { throw new IOException(e1); } long numberOfTiles = Math.round(Math.pow(ZOOM_LEVEL_BASE, zoomLevel)); //number of tile columns/rows for chosen zoom level double resX = WORLD_ENVELOPE.getSpan(0) / numberOfTiles; //points per tile double resY = WORLD_ENVELOPE.getSpan(1) / numberOfTiles; //points per tile double offsetX = WORLD_ENVELOPE.getMinimum(0); double offsetY = WORLD_ENVELOPE.getMinimum(1); try { //take available tiles from database leftTile = file.minColumn(zoomLevel); rightTile = file.maxColumn(zoomLevel); bottomTile = file.minRow(zoomLevel); topTile = file.maxRow(zoomLevel); } catch (SQLException e) { throw new IOException(e); } if (requestedEnvelope != null) { //crop tiles to requested envelope leftTile = Math.max(leftTile, Math.round(Math.floor((requestedEnvelope.getMinimum(0) - offsetX) / resX ))); bottomTile = Math.max(bottomTile, Math.round(Math.floor((requestedEnvelope.getMinimum(1) - offsetY) / resY ))); rightTile = Math.max(leftTile, Math.min(rightTile, Math.round(Math.floor((requestedEnvelope.getMaximum(0) - offsetX) / resX )))); topTile = Math.max(bottomTile, Math.min(topTile, Math.round(Math.floor((requestedEnvelope.getMaximum(1) - offsetY) / resY )))); } int width = (int) (rightTile - leftTile + 1) * DEFAULT_TILE_SIZE; int height = (int) (topTile - bottomTile + 1) * DEFAULT_TILE_SIZE; //recalculate the envelope we are actually returning ReferencedEnvelope resultEnvelope = new ReferencedEnvelope(offsetX + leftTile * resX, offsetX + (rightTile+1) * resX, offsetY + bottomTile * resY, offsetY + (topTile+1) * resY, SPHERICAL_MERCATOR); BufferedImage image = null; MBTilesFile.TileIterator it; try { it = file.tiles(zoomLevel, leftTile, bottomTile, rightTile, topTile); } catch (SQLException e) { throw new IOException(e); } while (it.hasNext()) { MBTilesTile tile = it.next(); BufferedImage tileImage = readImage(tile.getData(), metadata.getFormatStr()==null? "png" : metadata.getFormatStr()); if (image == null) { image = getStartImage(tileImage, width, height); } //coordinates int posx = (int) (tile.getTileColumn() - leftTile) * DEFAULT_TILE_SIZE; int posy = (int) (topTile - tile.getTileRow()) * DEFAULT_TILE_SIZE; image.getRaster().setRect(posx, posy, tileImage.getData() ); } it.close(); if (image == null){ // no tiles ?? image = getStartImage(width, height); } return coverageFactory.create(metadata.getName()==null? "nameless mbtiles" : metadata.getName(), image, resultEnvelope); } protected static BufferedImage readImage(byte[] data, String format) throws IOException { ByteArrayInputStream bis = new ByteArrayInputStream(data); Iterator<?> readers = ImageIO.getImageReadersByFormatName(format); ImageReader reader = (ImageReader) readers.next(); Object source = bis; ImageInputStream iis = ImageIO.createImageInputStream(source); reader.setInput(iis, true); ImageReadParam param = reader.getDefaultReadParam(); return reader.read(0, param); } protected BufferedImage getStartImage(BufferedImage copyFrom, int width, int height) { Map<String, Object> properties = null; if (copyFrom.getPropertyNames() != null) { properties = new HashMap<String, Object>(); for (String name : copyFrom.getPropertyNames()) { properties.put(name, copyFrom.getProperty(name)); } } SampleModel sm = copyFrom.getSampleModel().createCompatibleSampleModel(width, height); WritableRaster raster = Raster.createWritableRaster(sm, null); BufferedImage image = new BufferedImage(copyFrom.getColorModel(), raster, copyFrom.isAlphaPremultiplied(), (Hashtable<?, ?>) properties); //white background Graphics2D g2D = (Graphics2D) image.getGraphics(); Color save = g2D.getColor(); g2D.setColor(Color.WHITE); g2D.fillRect(0, 0, image.getWidth(), image.getHeight()); g2D.setColor(save); return image; } protected BufferedImage getStartImage(int imageType, int width, int height) { if (imageType == BufferedImage.TYPE_CUSTOM) imageType = BufferedImage.TYPE_3BYTE_BGR; BufferedImage image = new BufferedImage(width, height, imageType); //white background Graphics2D g2D = (Graphics2D) image.getGraphics(); Color save = g2D.getColor(); g2D.setColor(Color.WHITE); g2D.fillRect(0, 0, image.getWidth(), image.getHeight()); g2D.setColor(save); return image; } protected BufferedImage getStartImage(int width, int height) { return getStartImage(BufferedImage.TYPE_CUSTOM, width, height); } }