/* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved. * This code is licensed under the GPL 2.0 license, availible at the root * application directory. */ package org.vfny.geoserver.wms.responses.map.metatile; import java.awt.RenderingHints; import java.awt.image.RenderedImage; import java.io.IOException; import java.io.OutputStream; import java.util.logging.Level; import java.util.logging.Logger; import javax.media.jai.JAI; import javax.media.jai.operator.CropDescriptor; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.ServiceException; import org.geotools.geometry.jts.ReferencedEnvelope; import org.vfny.geoserver.wms.GetMapProducer; import org.vfny.geoserver.wms.RasterMapProducer; import org.vfny.geoserver.wms.WMSMapContext; import org.vfny.geoserver.wms.WmsException; import org.vfny.geoserver.wms.requests.GetMapRequest; import org.vfny.geoserver.wms.responses.AbstractGetMapProducer; import org.vfny.geoserver.wms.responses.DefaultRasterMapProducer; import org.vfny.geoserver.wms.responses.map.metatile.QuickTileCache.MetaTileKey; /** * Wrapping map producer that performs on the fly meta tiling wrapping another * map producer. It will first peek inside a tile cache to see if the requested * tile has already been computed, if so, it'll encode and return that one, * otherwise it'll build a meta tile, split it, and finally encode just the * requested tile, putting the others in the tile cache. * * @author Andrea Aime - TOPP * @author Simone Giannecchini - GeoSolutions */ public final class MetatileMapProducer extends AbstractGetMapProducer implements GetMapProducer { /** A logger for this class. */ private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.vfny.geoserver.responses.wms.map.metatile"); /** Small number for double equality comparison */ public static final double EPS = 1E-6; private GetMapRequest request; private RasterMapProducer delegate; private RenderedImage tile; private static QuickTileCache tileCache; /** * True if the request has the tiled hint, is 256x256 image, and the raw * delegate is a raster one * * @param request * @param delegate * @return */ public static boolean isRequestTiled(GetMapRequest request, GetMapProducer delegate) { if (!(request.isTiled() && (request.getTilesOrigin() != null) && (request.getWidth() == 256) && (request.getHeight() == 256) && delegate instanceof RasterMapProducer)) { return false; } return true; } public MetatileMapProducer(GetMapRequest request, RasterMapProducer delegate) { if(tileCache == null) { tileCache = (QuickTileCache) GeoServerExtensions.bean("metaTileCache"); } this.request = request; this.delegate = delegate; } public void produceMap() throws WmsException { // get the key that identifies the meta tile. The cache will make sure // two threads asking // for the same tile will get the same key, and thus will synchronize // with each other // (the first eventually builds the meta-tile, the second finds it ready // to be used) QuickTileCache.MetaTileKey key = tileCache.getMetaTileKey(request); synchronized (key) { tile = tileCache.getTile(key, request); if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("Looked for meta tile " + key.metaTileCoords.x + ", " + key.metaTileCoords.y + "in cache: " + ((tile != null) ? "hit!" : "miss")); } if (tile == null) { // compute the meta-tile if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("Building meta tile " + key.metaTileCoords.x + ", " + key.metaTileCoords.y); } // alter the map definition so that we build a meta-tile instead // of just the tile ReferencedEnvelope origEnv = mapContext.getAreaOfInterest(); mapContext.setAreaOfInterest(new ReferencedEnvelope(key .getMetaTileEnvelope(), origEnv .getCoordinateReferenceSystem())); mapContext.setMapWidth(key.getTileSize() * key.getMetaFactor()); mapContext .setMapHeight(key.getTileSize() * key.getMetaFactor()); // generate, split and cache delegate.setMapContext(mapContext); if (this.delegate instanceof DefaultRasterMapProducer) { ((DefaultRasterMapProducer)this.delegate).setMetatiled(true); } delegate.produceMap(); RenderedImage metaTile = delegate.getImage(); RenderedImage[] tiles = split(key, metaTile, mapContext); tileCache.storeTiles(key, tiles); tile = tileCache.getTile(key, request, tiles); } } } // /** // * Splits the tile into a set of tiles, numbered from lower right and // going up so // * that first row is 0,1,2,...,metaTileFactor, and so on. // * In the case of a 3x3 meta-tile, the layout is as follows: // * <pre> // * 6 7 8 // * 3 4 5 // * 0 1 2 // * </pre> // * @param key // * @param metaTile // * @param map // * @return // */ // private BufferedImage[] split(MetaTileKey key, BufferedImage metaTile, // WMSMapContext map) { // int metaFactor = key.getMetaFactor(); // BufferedImage[] tiles = new BufferedImage[key.getMetaFactor() * // key.getMetaFactor()]; // int tileSize = key.getTileSize(); // // for (int i = 0; i < metaFactor; i++) { // for (int j = 0; j < metaFactor; j++) { // // TODO: create child writable rasters instead of cloning the images // using // // graphics2d. Should be quite a bit faster and save some memory. Or // else, // // store meta-tiles in the cache directly, and extract children tiles // // on demand (even simpler) // BufferedImage tile; // // // keep the palette if necessary // if (metaTile.getType() == BufferedImage.TYPE_BYTE_INDEXED) { // tile = new BufferedImage(tileSize, tileSize, // BufferedImage.TYPE_BYTE_INDEXED, // (IndexColorModel) metaTile.getColorModel()); // } else if (metaTile.getType() == BufferedImage.TYPE_CUSTOM) { // throw new RuntimeException("We don't support custom buffered image // tiling"); // } else { // tile = new BufferedImage(tileSize, tileSize, metaTile.getType()); // } // // Graphics2D g2d = (Graphics2D) tile.getGraphics(); // AffineTransform at = AffineTransform.getTranslateInstance(-j * tileSize, // (-tileSize * (metaFactor - 1)) + (i * tileSize)); // setupBackground(g2d, map); // g2d.drawRenderedImage(metaTile, at); // g2d.dispose(); // tiles[(i * key.getMetaFactor()) + j] = tile; // } // } // // return tiles; // } /** * Splits the tile into a set of tiles, numbered from lower right and going * up so that first row is 0,1,2,...,metaTileFactor, and so on. In the case * of a 3x3 meta-tile, the layout is as follows: * * <pre> * 6 7 8 * 3 4 5 * 0 1 2 * </pre> * * @param key * @param metaTile * @param map * @return */ private RenderedImage[] split(MetaTileKey key, RenderedImage metaTile, WMSMapContext map) { final int metaFactor = key.getMetaFactor(); final RenderedImage[] tiles = new RenderedImage[key.getMetaFactor() * key.getMetaFactor()]; final int tileSize = key.getTileSize(); final RenderingHints no_cache = new RenderingHints(JAI.KEY_TILE_CACHE, null); for (int i = 0; i < metaFactor; i++) { for (int j = 0; j < metaFactor; j++) { int x = j * tileSize; int y = (tileSize * (metaFactor - 1)) - (i * tileSize); tile = CropDescriptor.create(metaTile, new Float(x), new Float( y), new Float(tileSize), new Float(tileSize), no_cache); tiles[(i * key.getMetaFactor()) + j] = tile; } } return tiles; } /** * Have the delegate encode the tile */ public void writeTo(OutputStream out) throws ServiceException, IOException { delegate.formatImageOutputStream(tile, out); } public void abort() { delegate.abort(); } public String getContentDisposition() { return delegate.getContentDisposition(); } public String getContentType() throws IllegalStateException { return delegate.getContentType(); } }