/* 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.geoserver.wms.map; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; import java.awt.image.WritableRaster; import java.io.IOException; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.media.jai.JAI; import javax.media.jai.PlanarImage; import javax.media.jai.RenderedOp; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.ServiceException; import org.geoserver.wms.GetMapOutputFormat; import org.geoserver.wms.GetMapRequest; import org.geoserver.wms.MapProducerCapabilities; import org.geoserver.wms.RasterCleaner; import org.geoserver.wms.WMSMapContent; import org.geoserver.wms.WebMap; import org.geoserver.wms.map.QuickTileCache.MetaTileKey; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.image.crop.GTCropDescriptor; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Errors; import org.geotools.resources.image.ImageUtilities; import org.geotools.util.logging.Logging; /** * 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 MetatileMapOutputFormat implements GetMapOutputFormat { private static final RenderingHints NO_CACHE_HINTS = new RenderingHints(JAI.KEY_TILE_CACHE, null); /** A logger for this class. */ private static final Logger LOGGER = Logging.getLogger(MetatileMapOutputFormat.class); /** Small number for double equality comparison */ public static final double EPS = 1E-6; private static QuickTileCache tileCache; private GetMapRequest request; private RenderedImageMapOutputFormat delegate; public MetatileMapOutputFormat(GetMapRequest request, RenderedImageMapOutputFormat delegate) { if (tileCache == null) { tileCache = (QuickTileCache) GeoServerExtensions.bean("metaTileCache"); } this.request = request; this.delegate = delegate; } /** * * @see org.geoserver.wms.GetMapOutputFormat#produceMap(org.geoserver.wms.WMSMapContent) */ public WebMap produceMap(WMSMapContent mapContent) throws ServiceException, IOException { // 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) { RenderedImage tile = tileCache.getTile(key, request); List<GridCoverage2D> renderedCoverages = null; 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 = mapContent.getRenderingArea(); mapContent.getViewport().setBounds(new ReferencedEnvelope(key.getMetaTileEnvelope(), origEnv.getCoordinateReferenceSystem())); mapContent.setMapWidth(key.getTileSize() * key.getMetaFactor()); mapContent.setMapHeight(key.getTileSize() * key.getMetaFactor()); mapContent.setTileSize(key.getTileSize()); RenderedImageMap metaTileMap = delegate.produceMap(mapContent); RenderedImage metaTile = metaTileMap.getImage(); RenderedImage[] tiles = split(key, metaTile, mapContent); tileCache.storeTiles(key, tiles); tile = tileCache.getTile(key, request, tiles); renderedCoverages = metaTileMap.getRenderedCoverages(); } RenderedImageMap tileMap = new RenderedImageMap(mapContent, tile, getMimeType()); tileMap.setRenderedCoverages(renderedCoverages); return tileMap; } } /** * * @see org.geoserver.wms.GetMapOutputFormat#getOutputFormatNames() */ public Set<String> getOutputFormatNames() { return delegate.getOutputFormatNames(); } /** * * @see org.geoserver.wms.GetMapOutputFormat#getMimeType() */ public String getMimeType() { return delegate.getMimeType(); } /** * 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, GetMapOutputFormat delegate) { boolean tiled = request.isTiled(); Point2D tilesOrigin = request.getTilesOrigin(); int width = request.getWidth(); int height = request.getHeight(); if (tiled && tilesOrigin != null && width == 256 && height == 256 && delegate instanceof RenderedImageMapOutputFormat) { return true; } return false; } /** * 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 static RenderedImage[] split(MetaTileKey key, RenderedImage metaTile, WMSMapContent map) { final int metaFactor = key.getMetaFactor(); final RenderedImage[] tiles = new RenderedImage[key.getMetaFactor() * key.getMetaFactor()]; final int tileSize = key.getTileSize(); // check image type int type = 0; if (metaTile instanceof PlanarImage) { type = 1; } else if (metaTile instanceof BufferedImage) { type = 2; } // now do the splitting 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); RenderedImage tile; switch (type) { case 0: // do a crop, and then turn it into a buffered image so that we can release // the image chain RenderedOp cropped = GTCropDescriptor.create(metaTile, Float.valueOf(x), Float.valueOf(y), Float.valueOf( tileSize), Float.valueOf(tileSize), NO_CACHE_HINTS); RasterCleaner.addImage(cropped); tile = cropped.getAsBufferedImage(); break; case 1: final PlanarImage pImage = (PlanarImage) metaTile; final WritableRaster wTile = WritableRaster.createWritableRaster( pImage.getSampleModel().createCompatibleSampleModel(tileSize, tileSize), new Point(x, y)); Rectangle sourceArea = new Rectangle(x, y, tileSize, tileSize); sourceArea = sourceArea.intersection(pImage.getBounds()); // copying the data to ensure we don't have side effects when we clean the cache pImage.copyData(wTile); if(wTile.getMinX()!=0||wTile.getMinY()!=0) { tile = new BufferedImage(pImage.getColorModel(), (WritableRaster) wTile.createTranslatedChild(0, 0), pImage.getColorModel().isAlphaPremultiplied(), null); } else { tile = new BufferedImage(pImage.getColorModel(), wTile, pImage.getColorModel().isAlphaPremultiplied(), null); } break; case 2: final BufferedImage image = (BufferedImage) metaTile; tile = image.getSubimage(x, y, tileSize, tileSize); break; default: throw new IllegalStateException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,"metaTile class",metaTile.getClass().toString())); } tiles[(i * key.getMetaFactor()) + j] = tile; } } // dispose input image if necessary/possible RasterCleaner.addImage(metaTile); return tiles; } public MapProducerCapabilities getCapabilities(String format) { throw new RuntimeException("The meta-tile output format should never be invoked directly!"); } }