/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wms.map;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.media.jai.PlanarImage;
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.filter.function.EnvFunction;
import org.geotools.renderer.lite.gridcoverage2d.GridCoverageRenderer;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.util.logging.Logging;
import it.geosolutions.jaiext.BufferedImageAdapter;
/**
* 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 {
/** 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;
/**
* This variable is use for testing purposes in order to force this
* {@link GridCoverageRenderer} to dump images at various steps on the disk.
*/
private static boolean DEBUG = Boolean.valueOf(GeoServerExtensions.getProperty("org.geoserver.wms.map.MetatileMapOutputFormat.debug"));
private static String DEBUG_DIR;
static {
if (DEBUG) {
final File tempDir = new File(GeoServerExtensions.getProperty("user.home"),".geoserver");
if (!tempDir.exists() ) {
if(!tempDir.mkdir())
LOGGER.severe("Unable to create debug dir, exiting application!!!");
DEBUG=false;
DEBUG_DIR = null;
} else
{
DEBUG_DIR = tempDir.getAbsolutePath();
LOGGER.fine("MetatileMapOutputFormat debug dir "+DEBUG_DIR);
}
}
}
/**
* Write the provided {@link RenderedImage} in the debug directory with the provided file name.
*
* @param raster
* the {@link RenderedImage} that we have to write.
* @param fileName
* a {@link String} indicating where we should write it.
*/
static void writeRenderedImage(final RenderedImage raster, final String fileName) {
if (DEBUG_DIR == null)
throw new NullPointerException(
"Unable to write the provided coverage in the debug directory");
if (DEBUG == false)
throw new IllegalStateException(
"Unable to write the provided coverage since we are not in debug mode");
try {
ImageIO.write(raster, "tiff", new File(DEBUG_DIR, fileName + ".tiff"));
} catch (IOException e) {
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
}
}
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+" of size w="+
key.getTileSize() * key.getMetaFactor()+", h="+
key.getTileSize() * key.getMetaFactor()+ " with metatilign factor "+key.getMetaFactor());
}
// alter the map definition so that we build a meta-tile instead
// of just the tile
mapContent.getViewport().setBounds(key.getMetaTileEnvelope());
mapContent.setMapWidth(key.getTileSize() * key.getMetaFactor());
mapContent.setMapHeight(key.getTileSize() * key.getMetaFactor());
mapContent.setTileSize(key.getTileSize());
// adjust the bbox/width/height env vars that GetMap setup, since we
// are changing them under its feet
EnvFunction.setLocalValue("wms_bbox", mapContent.getViewport().getBounds());
EnvFunction.setLocalValue("wms_width", mapContent.getMapWidth());
EnvFunction.setLocalValue("wms_height", mapContent.getMapHeight());
RenderedImageMap metaTileMap = delegate.produceMap(mapContent);
RenderedImage metaTile = metaTileMap.getImage();
RenderedImage[] tiles = split(key, metaTile);
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
*
*/
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
*
*/
static RenderedImage[] split(MetaTileKey key, RenderedImage metaTile) {
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;
}
if(LOGGER.isLoggable(Level.FINER)){
LOGGER.finer("Metatile type " + type);
}
// now do the splitting
try {
if (DEBUG) {
writeRenderedImage(metaTile, "metaTile");
}
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:
// RENDERED IMAGE
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("Metatile split on RenderedImage");
}
metaTile = PlanarImage.wrapRenderedImage(metaTile);
case 1:
// PLANAR IMAGE
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("Metatile split on PlanarImage");
}
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.createWritableTranslatedChild(0, 0),
pImage.getColorModel().isAlphaPremultiplied(), null);
} else {
tile = new BufferedImage(pImage.getColorModel(), wTile,
pImage.getColorModel().isAlphaPremultiplied(), null);
}
break;
case 2:
// BUFFERED IMAGE
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("Metatile split on BufferedImage");
}
final BufferedImage image = (BufferedImage) metaTile;
final BufferedImage subimage = image.getSubimage(x, y, tileSize, tileSize);
tile = new BufferedImageAdapter(subimage);
break;
default:
throw new IllegalStateException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,
"metaTile class", metaTile.getClass().toString()));
}
tiles[(i * key.getMetaFactor()) + j] = tile;
if (DEBUG) {
writeRenderedImage(tile, "tile" + i + "-" + j);
}
}
}
} finally {
// 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!");
}
}