Java tutorial
/* Copyright (c) 2001 - 2013 OpenPlans - www.openplans.org. All rights reserved. * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.geopkg; import static java.lang.String.format; import static org.geoserver.geopkg.GeoPkg.*; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.IOUtils; import org.geoserver.catalog.ResourceInfo; import org.geoserver.gwc.GWC; import org.geoserver.ows.util.OwsUtils; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.ServiceException; import org.geoserver.wms.GetMapRequest; import org.geoserver.wms.MapLayerInfo; import org.geoserver.wms.MapProducerCapabilities; import org.geoserver.wms.RasterCleaner; import org.geoserver.wms.WMS; import org.geoserver.wms.WMSMapContent; import org.geoserver.wms.WebMap; import org.geoserver.wms.WebMapService; import org.geoserver.wms.map.AbstractMapOutputFormat; import org.geoserver.wms.map.JPEGMapResponse; import org.geoserver.wms.map.PNGMapResponse; import org.geoserver.wms.map.RawMap; import org.geoserver.wms.map.RenderedImageMap; import org.geoserver.wms.map.RenderedImageMapResponse; import org.geotools.factory.CommonFactoryFinder; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.geopkg.Entry; import org.geotools.geopkg.GeoPackage; import org.geotools.geopkg.Tile; import org.geotools.geopkg.TileEntry; import org.geotools.geopkg.TileMatrix; import org.geotools.map.Layer; import org.geotools.referencing.CRS; import org.geotools.renderer.lite.RendererUtilities; import org.geotools.util.logging.Logging; import org.geowebcache.grid.BoundingBox; import org.geowebcache.grid.Grid; import org.geowebcache.grid.GridSet; import org.geowebcache.grid.GridSetBroker; import org.geowebcache.grid.GridSubset; import org.geowebcache.grid.GridSubsetFactory; import org.geowebcache.grid.SRS; import org.geowebcache.layer.TileLayer; import org.opengis.filter.FilterFactory; import org.opengis.referencing.crs.CoordinateReferenceSystem; import com.google.common.base.Preconditions; import com.google.common.collect.Sets; import com.vividsolutions.jts.geom.Envelope; /** * * WMS GetMap Output Format for GeoPackage * * @author Justin Deoliveira, Boundless * */ public class GeoPackageGetMapOutputFormat extends AbstractMapOutputFormat { static Logger LOGGER = Logging.getLogger("org.geoserver.geopkg"); static final String PNG_MIME_TYPE = "image/png"; static final String JPEG_MIME_TYPE = "image/jpeg"; static final int TILE_CLEANUP_INTERVAL; static { //calculate the number of tiles we can generate before having to cleanup, value is // 25% of total memory / approximte size of single tile TILE_CLEANUP_INTERVAL = (int) (Runtime.getRuntime().maxMemory() * 0.05 / (256.0 * 256 * 4)); } static FilterFactory filterFactory = CommonFactoryFinder.getFilterFactory(); WebMapService webMapService; WMS wms; GWC gwc; public GeoPackageGetMapOutputFormat(WebMapService webMapService, WMS wms, GWC gwc) { super(MIME_TYPE, Sets.newHashSet(NAMES)); this.webMapService = webMapService; this.wms = wms; this.gwc = gwc; } @Override public MapProducerCapabilities getCapabilities(String format) { return new MapProducerCapabilities(false, false, false, true, null); } @Override public WebMap produceMap(WMSMapContent map) throws ServiceException, IOException { GeoPackage geopkg = new GeoPackage(); geopkg.init(); GetMapRequest req = map.getRequest(); List<Layer> layers = map.layers(); List<MapLayerInfo> mapLayers = req.getLayers(); Preconditions.checkState(layers.size() == mapLayers.size(), "Number of map layers not same as number of rendered layers"); //list of layers to render directly and include as tiles List<MapLayerInfo> tileLayers = new ArrayList(); //tiled mode means render all as map tile layer tileLayers.addAll(mapLayers); addTileLayers(geopkg, tileLayers, map); geopkg.close(); final File dbFile = geopkg.getFile(); final BufferedInputStream bin = new BufferedInputStream(new FileInputStream(dbFile)); RawMap result = new RawMap(map, bin, MIME_TYPE) { @Override public void writeTo(OutputStream out) throws IOException { String dbFilename = getAttachmentFileName(); if (dbFilename != null) { dbFilename = dbFilename.substring(0, dbFilename.length() - 4) + ".gpkg"; } else { //this shouldn't really ever happen, but fallback anyways dbFilename = "geoserver.gpkg"; } IOUtils.copy(bin, out); out.flush(); // JD: disabling zip compression for now // ZipOutputStream zout = new ZipOutputStream(out); // zout.putNextEntry(new ZipEntry(dbFilename)); // // super.writeTo(zout); // zout.closeEntry(); // zout.close(); bin.close(); try { dbFile.delete(); } catch (Exception e) { LOGGER.log(Level.WARNING, "Error deleting file: " + dbFile.getAbsolutePath(), e); } } }; result.setContentDispositionHeader(map, ".gpkg", true); return result; } void addTileLayers(GeoPackage geopkg, List<MapLayerInfo> mapLayers, WMSMapContent map) throws IOException { if (mapLayers.isEmpty()) { return; } //figure out a name for the file entry String tileEntryName = null; Map formatOpts = map.getRequest().getFormatOptions(); if (formatOpts.containsKey("tileset_name")) { tileEntryName = (String) formatOpts.get("tileset_name"); } if (tileEntryName == null) { tileEntryName = map.getTitle(); } if (tileEntryName == null && mapLayers.size() == 1) { Iterator<MapLayerInfo> it = mapLayers.iterator(); tileEntryName = it.next().getLayerInfo().getName(); } GridSubset gridSubset = findBestGridSubset(map); int[] minmax = findMinMaxZoom(gridSubset, map); BoundingBox bbox = bbox(map); TileEntry e = new TileEntry(); e.setTableName(tileEntryName); if (mapLayers.size() == 1) { ResourceInfo r = mapLayers.get(0).getResource(); e.setIdentifier(r.getTitle()); e.setDescription(r.getAbstract()); } e.setBounds(new ReferencedEnvelope(findTileBounds(gridSubset, bbox, minmax[0]), map.getCoordinateReferenceSystem())); e.setSrid(srid(map)); GridSet gridSet = gridSubset.getGridSet(); for (int z = minmax[0]; z < minmax[1]; z++) { Grid g = gridSet.getGrid(z); TileMatrix m = new TileMatrix(); m.setZoomLevel(z); m.setMatrixWidth((int) g.getNumTilesWide()); m.setMatrixHeight((int) g.getNumTilesHigh()); m.setTileWidth(gridSubset.getTileWidth()); m.setTileHeight(gridSubset.getTileHeight()); //TODO: not sure about this m.setXPixelSize(g.getResolution()); m.setYPixelSize(g.getResolution()); //m.setXPixelSize(gridSet.getPixelSize()); //m.setYPixelSize(gridSet.getPixelSize()); e.getTileMatricies().add(m); } //figure out the actual bounds of the tiles to be renderered LOGGER.fine("Creating tile entry" + e.getTableName()); geopkg.create(e); //create a prototype getmap request GetMapRequest req = new GetMapRequest(); OwsUtils.copy(map.getRequest(), req, GetMapRequest.class); req.setLayers(mapLayers); String imageFormat = formatOpts.containsKey("format") ? parseFormatFromOpts(formatOpts) : findBestFormat(map); req.setFormat(imageFormat); req.setWidth(gridSubset.getTileWidth()); req.setHeight(gridSubset.getTileHeight()); //count tiles as we generate them int ntiles = 0; //flag determining if tile row indexes we store in database should be inverted boolean flipy = Boolean.valueOf((String) formatOpts.get("flipy")); for (int z = minmax[0]; z < minmax[1]; z++) { long[] intersect = gridSubset.getCoverageIntersection(z, bbox); for (long x = intersect[0]; x <= intersect[2]; x++) { for (long y = intersect[1]; y <= intersect[3]; y++) { BoundingBox box = gridSubset.boundsFromIndex(new long[] { x, y, z }); req.setBbox(new Envelope(box.getMinX(), box.getMaxX(), box.getMinY(), box.getMaxY())); Tile t = new Tile(); t.setZoom(z); t.setColumn((int) x); t.setRow((int) (flipy ? gridSubset.getNumTilesHigh(z) - (y + 1) : y)); WebMap result = webMapService.getMap(req); t.setData(toBytes(result)); geopkg.add(e, t); //images we encode are actually kept around, we need to clean them up if (ntiles++ == TILE_CLEANUP_INTERVAL) { cleanUpImages(); ntiles = 0; } } } } } Envelope findTileBounds(GridSubset gridSubset, BoundingBox bbox, int z) { long[] i = gridSubset.getCoverageIntersection(z, bbox); BoundingBox b1 = gridSubset.boundsFromIndex(new long[] { i[0], i[1], i[4] }); BoundingBox b2 = gridSubset.boundsFromIndex(new long[] { i[2], i[3], i[4] }); return new Envelope(Math.min(b1.getMinX(), b2.getMinX()), Math.max(b1.getMaxX(), b2.getMaxX()), Math.min(b1.getMinY(), b2.getMinY()), Math.max(b1.getMaxY(), b2.getMaxY())); } void initEntry(Entry e, Layer layer, MapLayerInfo mapLayer, WMSMapContent map) throws IOException { ResourceInfo r = mapLayer.getResource(); e.setTableName(r.getName()); e.setIdentifier(r.getTitle()); e.setDescription(r.getAbstract()); e.setBounds(bounds(map)); e.setSrid(srid(map)); } Integer srid(WMSMapContent map) { Integer srid = null; try { srid = CRS.lookupEpsgCode(map.getCoordinateReferenceSystem(), false); if (srid == null) { srid = Integer.parseInt(map.getRequest().getSRS().split(":")[1]); } } catch (Exception ex) { LOGGER.log(Level.WARNING, "Error determining srid", ex); } return srid; } ReferencedEnvelope bounds(Layer layer, WMSMapContent map) { ReferencedEnvelope e = layer.getBounds(); if (e == null) { e = bounds(map); } return e; } ReferencedEnvelope bounds(WMSMapContent map) { return new ReferencedEnvelope(map.getRequest().getBbox(), map.getCoordinateReferenceSystem()); } BoundingBox bbox(WMSMapContent map) { Envelope bnds = bounds(map); return new BoundingBox(bnds.getMinX(), bnds.getMinY(), bnds.getMaxX(), bnds.getMaxY()); } GridSubset findBestGridSubset(WMSMapContent map) { GetMapRequest req = map.getRequest(); Map formatOpts = req.getFormatOptions(); GridSetBroker gridSetBroker = gwc.getGridSetBroker(); GridSet gridSet = null; //first check format options to see if explicitly specified if (formatOpts.containsKey("gridset")) { gridSet = gridSetBroker.get(formatOpts.get("gridset").toString()); } //next check srs if (gridSet == null) { gridSet = gridSetBroker.get(req.getSRS().toUpperCase()); } if (gridSet != null) { return GridSubsetFactory.createGridSubSet(gridSet); } CoordinateReferenceSystem crs = map.getCoordinateReferenceSystem(); //look up epsg code Integer epsgCode = null; try { epsgCode = CRS.lookupEpsgCode(crs, false); } catch (Exception e) { throw new ServiceException("Unable to determine epsg code for " + crs, e); } if (epsgCode == null) { throw new ServiceException("Unable to determine epsg code for " + crs); } SRS srs = SRS.getSRS(epsgCode); //figure out the appropriate grid sub set Set<GridSubset> gridSubsets = new LinkedHashSet<GridSubset>(); for (MapLayerInfo l : req.getLayers()) { TileLayer tl = gwc.getTileLayerByName(l.getName()); if (tl == null) { throw new ServiceException("No tile layer for " + l.getName()); } List<GridSubset> theseGridSubsets = tl.getGridSubsetsForSRS(srs); if (gridSubsets.isEmpty()) { gridSubsets.addAll(theseGridSubsets); } else { gridSubsets.retainAll(theseGridSubsets); } if (gridSubsets.isEmpty()) { throw new ServiceException("No suitable " + epsgCode + " grid subset for " + req.getLayers()); } } if (gridSubsets.size() > 1) { if (LOGGER.isLoggable(Level.WARNING)) { StringBuilder msg = new StringBuilder("Found multiple grid subsets: "); for (GridSubset gs : gridSubsets) { msg.append(gs.getName()).append(", "); } msg.setLength(msg.length() - 2); msg.append(". Choosing first."); LOGGER.warning(msg.toString()); } } return gridSubsets.iterator().next(); } int[] findMinMaxZoom(GridSubset gridSubset, WMSMapContent map) { GridSet gridSet = gridSubset.getGridSet(); Map formatOpts = map.getRequest().getFormatOptions(); Integer minZoom = null; if (formatOpts.containsKey("min_zoom")) { minZoom = Integer.parseInt(formatOpts.get("min_zoom").toString()); } if (minZoom == null) { minZoom = findClosestZoom(gridSet, map); } Integer maxZoom = null; if (formatOpts.containsKey("max_zoom")) { maxZoom = Integer.parseInt(formatOpts.get("max_zoom").toString()); } else if (formatOpts.containsKey("num_zooms")) { maxZoom = minZoom + Integer.parseInt(formatOpts.get("num_zooms").toString()); } if (maxZoom == null) { //walk down until we hit too many tiles maxZoom = findMaxZoomAuto(gridSubset, minZoom, map); } if (maxZoom < minZoom) { throw new ServiceException(format("maxZoom (%d) can not be less than minZoom (%d)", maxZoom, minZoom)); } //end index if (maxZoom > gridSet.getNumLevels()) { LOGGER.warning(format("Max zoom (%d) can't be greater than number of zoom levels (%d)", maxZoom, gridSet.getNumLevels())); maxZoom = gridSet.getNumLevels(); } return new int[] { minZoom, maxZoom }; } Integer findClosestZoom(GridSet gridSet, WMSMapContent map) { double reqScale = RendererUtilities.calculateOGCScale(bounds(map), gridSet.getTileWidth(), null); int i = 0; double error = Math.abs(gridSet.getGrid(i).getScaleDenominator() - reqScale); while (i < gridSet.getNumLevels() - 1) { Grid g = gridSet.getGrid(i + 1); double e = Math.abs(g.getScaleDenominator() - reqScale); if (e > error) { break; } else { error = e; } i++; } return Math.max(i, 0); } Integer findMaxZoomAuto(GridSubset gridSubset, Integer minZoom, WMSMapContent map) { BoundingBox bbox = bbox(map); int zoom = minZoom; int ntiles = 0; while (ntiles < 256 && zoom < gridSubset.getGridSet().getNumLevels()) { long[] intersect = gridSubset.getCoverageIntersection(zoom, bbox); ntiles += (intersect[2] - intersect[0] + 1) * (intersect[3] - intersect[1] + 1); zoom++; } return zoom; } String parseFormatFromOpts(Map formatOpts) { String format = (String) formatOpts.get("format"); return format.contains("/") ? format : "image/" + format; } String findBestFormat(WMSMapContent map) { //if request is a single coverage layer return jpeg, otherwise use just png List<MapLayerInfo> layers = map.getRequest().getLayers(); if (layers.size() == 1 && layers.get(0).getType() == MapLayerInfo.TYPE_RASTER) { return JPEG_MIME_TYPE; } return PNG_MIME_TYPE; } byte[] toBytes(WebMap map) throws IOException { ByteArrayOutputStream bout = new ByteArrayOutputStream(); if (map instanceof RenderedImageMap) { RenderedImageMapResponse response = JPEG_MIME_TYPE.equals(map.getMimeType()) ? new JPEGMapResponse(wms) : new PNGMapResponse(wms); response.write(map, bout, null); } else if (map instanceof RawMap) { ((RawMap) map).writeTo(bout); } bout.flush(); return bout.toByteArray(); } void cleanUpImages() { RasterCleaner cleaner = GeoServerExtensions.bean(RasterCleaner.class); cleaner.finished(null); } }