/* 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.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.media.jai.ImageLayout;
import javax.media.jai.Interpolation;
import javax.media.jai.InterpolationBicubic2;
import javax.media.jai.InterpolationBilinear;
import javax.media.jai.InterpolationNearest;
import javax.media.jai.JAI;
import javax.media.jai.LookupTableJAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.ROI;
import javax.media.jai.ROIShape;
import javax.media.jai.operator.BandMergeDescriptor;
import javax.media.jai.operator.ConstantDescriptor;
import javax.media.jai.operator.CropDescriptor;
import javax.media.jai.operator.LookupDescriptor;
import javax.media.jai.operator.MosaicDescriptor;
import org.geoserver.platform.ServiceException;
import org.geoserver.wms.DefaultWebMapService;
import org.geoserver.wms.GetMapOutputFormat;
import org.geoserver.wms.GetMapRequest;
import org.geoserver.wms.WMS;
import org.geoserver.wms.WMSInfo;
import org.geoserver.wms.WMSInfo.WMSInterpolation;
import org.geoserver.wms.WMSMapContext;
import org.geoserver.wms.WatermarkInfo;
import org.geoserver.wms.decoration.MapDecoration;
import org.geoserver.wms.decoration.MapDecorationLayout;
import org.geoserver.wms.decoration.MetatiledMapDecorationLayout;
import org.geoserver.wms.decoration.WatermarkDecoration;
import org.geotools.coverage.grid.GridCoverage2D;
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.gce.imagemosaic.ImageMosaicFormat;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.image.ImageWorker;
import org.geotools.image.palette.InverseColorMapOp;
import org.geotools.map.MapLayer;
import org.geotools.parameter.Parameter;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.referencing.operation.builder.GridToEnvelopeMapper;
import org.geotools.renderer.GTRenderer;
import org.geotools.renderer.lite.RendererUtilities;
import org.geotools.renderer.lite.StreamingRenderer;
import org.geotools.renderer.lite.gridcoverage2d.GridCoverageRenderer;
import org.geotools.renderer.shape.ShapefileRenderer;
import org.geotools.resources.image.ColorUtilities;
import org.geotools.styling.RasterSymbolizer;
import org.geotools.styling.Style;
import org.geotools.util.logging.Logging;
import org.opengis.feature.Feature;
import org.opengis.feature.type.FeatureType;
import org.opengis.geometry.BoundingBox;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.vfny.geoserver.global.GeoserverDataDirectory;
/**
* A {@link GetMapOutputFormat} that produces {@link RenderedImageMap} instances to be encoded in
* the constructor supplied MIME-Type.
* <p>
* Instances of this class are expected to be declared in the application context supplying the
* prescribed MIME-Type to create maps for, and the list of output format names to be declared in
* the GetCapabilities document. Note that the prescribed MIME-Type (the MIME Type the produced
* images are to be encoded as and that is to be set in the response HTTP Content-Type header) may
* differ from what's declared in the capabilities document, hence the separation of concerns and
* the two different arguments in the constructor (for example, a declared output format of
* {@code image/geotiff8} may indicate to create an indexed geotiff image with 8-bit pixel depth,
* but the resulting MIME-Type be {@code image/tiff}.
* </p>
* <p>
* Whether or not the output format instance permits images with transparency and/or indexed 8-bit
* color model is described by the {@link #isTransparencySupported() transparencySupported} and
* {@link #isPaletteSupported() paletteSupported} properties respectively.
* </p>
*
* @author Chris Holmes, TOPP
* @author Simone Giannecchini, GeoSolutions
* @author Andrea Aime
* @author Gabriel Roldan
* @version $Id: RenderedImageMapOutputFormat.java 15870 2011-05-20 13:12:57Z aaime $
* @see RenderedImageMapOutputFormat
* @see PNGMapResponse
* @see GIFMapResponse
* @see TIFFMapResponse
* @see GeoTIFFMapResponse
* @see JPEGMapResponse
*/
public class RenderedImageMapOutputFormat extends AbstractMapOutputFormat {
private final static Interpolation NN_INTERPOLATION = new InterpolationNearest();
private final static Interpolation BIL_INTERPOLATION = new InterpolationBilinear();
private final static Interpolation BIC_INTERPOLATION = new InterpolationBicubic2(0);
// antialiasing settings, no antialias, only text, full antialias
private final static String AA_NONE = "NONE";
private final static String AA_TEXT = "TEXT";
private final static String AA_FULL = "FULL";
private final static List<String> AA_SETTINGS = Arrays.asList(new String[] { AA_NONE, AA_TEXT,
AA_FULL });
/**
* The size of a megabyte
*/
private static final int KB = 1024;
/**
* The lookup table used for data type transformation (it's really the identity one)
*/
private static LookupTableJAI IDENTITY_TABLE = new LookupTableJAI(getTable());
private static byte[] getTable() {
byte[] arr = new byte[256];
for (int i = 0; i < arr.length; i++) {
arr[i] = (byte) i;
}
return arr;
}
/** A logger for this class. */
private static final Logger LOGGER = Logging.getLogger(RenderedImageMapOutputFormat.class);
/** Which format to encode the image in if one is not supplied */
private static final String DEFAULT_MAP_FORMAT = "image/png";
/** WMS Service configuration * */
protected final WMS wms;
private boolean palleteSupported = true;
private boolean transparencySupported = true;
/**
*
*/
public RenderedImageMapOutputFormat(WMS wms) {
this(DEFAULT_MAP_FORMAT, wms);
}
/**
* @param the
* mime type to be written down as an HTTP header when a map of this format is
* generated
*/
public RenderedImageMapOutputFormat(String mime, WMS wms) {
super(mime);
this.wms = wms;
}
/**
*
* @param mime
* the actual MIME Type resulting for the image created using this output format
* @param outputFormats
* the list of output format names to declare in the GetCapabilities document, does
* not need to match {@code mime} (e.g., an output format of {@code image/geotiff8}
* may result in a map returned with MIME Type {@code image/tiff})
* @param wms
*/
public RenderedImageMapOutputFormat(String mime, String[] outputFormats, WMS wms) {
super(mime, outputFormats);
this.wms = wms;
}
/**
* @see org.geoserver.wms.GetMapOutputFormat#produceMap(org.geoserver.wms.WMSMapContext)
*/
public final RenderedImageMap produceMap(WMSMapContext mapContext) throws ServiceException {
return produceMap(mapContext, false);
}
/**
* Actually produces the map image, careing about meta tiling if {@code tiled == true}.
*
* @param mapContext
* @param tiled
* Indicates whether metatiling is activated for this map producer.
*/
public RenderedImageMap produceMap(final WMSMapContext mapContext, final boolean tiled)
throws ServiceException {
System.setProperty("tolerance", "0.333");
final MapDecorationLayout layout = findDecorationLayout(mapContext, tiled);
Rectangle paintArea = new Rectangle(0, 0, mapContext.getMapWidth(),
mapContext.getMapHeight());
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("setting up " + paintArea.width + "x" + paintArea.height + " image");
}
// extra antialias setting
final GetMapRequest request = mapContext.getRequest();
String antialias = (String) request.getFormatOptions().get("antialias");
if (antialias != null)
antialias = antialias.toUpperCase();
// figure out a palette for buffered image creation
IndexColorModel palette = null;
final InverseColorMapOp paletteInverter = mapContext.getPaletteInverter();
final boolean transparent = mapContext.isTransparent() && isTransparencySupported();
final Color bgColor = mapContext.getBgColor();
if (paletteInverter != null && AA_NONE.equals(antialias)) {
palette = paletteInverter.getIcm();
} else if (AA_NONE.equals(antialias)) {
PaletteExtractor pe = new PaletteExtractor(transparent ? null : bgColor);
MapLayer[] layers = mapContext.getLayers();
for (int i = 0; i < layers.length; i++) {
pe.visit(layers[i].getStyle());
if (!pe.canComputePalette())
break;
}
if (pe.canComputePalette())
palette = pe.getPalette();
}
// before even preparing the rendering surface, check it's not too big,
// if so, throw a service exception
long maxMemory = wms.getMaxRequestMemory() * KB;
// ... base image memory
long memory = getDrawingSurfaceMemoryUse(paintArea.width, paintArea.height, palette,
transparent);
// .. use a fake streaming renderer to evaluate the extra back buffers used when rendering
// multiple featureTypeStyles against the same layer
StreamingRenderer testRenderer = new StreamingRenderer();
testRenderer.setContext(mapContext);
memory += testRenderer.getMaxBackBufferMemory(paintArea.width, paintArea.height);
if (maxMemory > 0 && memory > maxMemory) {
long kbUsed = memory / KB;
long kbMax = maxMemory / KB;
throw new ServiceException("Rendering request would use " + kbUsed + "KB, whilst the "
+ "maximum memory allowed is " + kbMax + "KB");
}
// TODO: allow rendering to continue with vector layers
// TODO: allow rendering to continue with layout
// TODO: handle rotated rasters
// TODO: handle color conversions
// TODO: handle meta-tiling
// TODO: how to handle timeout here? I guess we need to move it into the dispatcher?
RenderedImage image = null;
// fast path for pure coverage rendering
if (DefaultWebMapService.isDirectRasterPathEnabled() &&
mapContext.getLayerCount() == 1
&& mapContext.getAngle() == 0.0
&& (layout == null || layout.isEmpty())) {
List<GridCoverage2D> renderedCoverages = new ArrayList<GridCoverage2D>(2);
try {
image = directRasterRender(mapContext, 0, renderedCoverages);
} catch (Exception e) {
throw new ServiceException("Error rendering coverage on the fast path", e);
}
if (image != null) {
RenderedImageMap result = new RenderedImageMap(mapContext, image, getMimeType());
result.setRenderedCoverages(renderedCoverages);
return result;
}
}
// we use the alpha channel if the image is transparent or if the meta tiler
// is enabled, since apparently the Crop operation inside the meta-tiler
// generates striped images in that case (see GEOS-
boolean useAlpha = transparent || MetatileMapOutputFormat.isRequestTiled(request, this);
final RenderedImage preparedImage = prepareImage(paintArea.width, paintArea.height,
palette, useAlpha);
final Map<RenderingHints.Key, Object> hintsMap = new HashMap<RenderingHints.Key, Object>();
final Graphics2D graphic = ImageUtils.prepareTransparency(transparent, bgColor,
preparedImage, hintsMap);
// set up the antialias hints
if (AA_NONE.equals(antialias)) {
hintsMap.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
if (preparedImage.getColorModel() instanceof IndexColorModel) {
// otherwise we end up with dithered colors where the match is
// not 100%
hintsMap.put(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
}
} else if (AA_TEXT.equals(antialias)) {
hintsMap.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
hintsMap.put(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
} else {
if (antialias != null && !AA_FULL.equals(antialias)) {
LOGGER.warning("Unrecognized antialias setting '" + antialias
+ "', valid values are " + AA_SETTINGS);
}
hintsMap.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
// these two hints improve text layout in diagonal labels and reduce artifacts
// in line rendering (without hampering performance)
hintsMap.put(RenderingHints.KEY_FRACTIONALMETRICS,
RenderingHints.VALUE_FRACTIONALMETRICS_ON);
hintsMap.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
// turn off/on interpolation rendering hint
if (wms != null) {
if (WMSInterpolation.Nearest.equals(wms.getInterpolation())) {
hintsMap.put(JAI.KEY_INTERPOLATION, NN_INTERPOLATION);
hintsMap.put(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
} else if (WMSInterpolation.Bilinear.equals(wms.getInterpolation())) {
hintsMap.put(JAI.KEY_INTERPOLATION, BIL_INTERPOLATION);
hintsMap.put(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
} else if (WMSInterpolation.Bicubic.equals(wms.getInterpolation())) {
hintsMap.put(JAI.KEY_INTERPOLATION, BIC_INTERPOLATION);
hintsMap.put(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
}
}
// make sure the hints are set before we start rendering the map
graphic.setRenderingHints(hintsMap);
RenderingHints hints = new RenderingHints(hintsMap);
GTRenderer renderer;
if (DefaultWebMapService.useShapefileRenderer()) {
renderer = new ShapefileRenderer();
} else {
StreamingRenderer sr = new StreamingRenderer();
sr.setThreadPool(DefaultWebMapService.getRenderingPool());
renderer = sr;
}
renderer.setContext(mapContext);
renderer.setJava2DHints(hints);
// setup the renderer hints
Map<Object, Object> rendererParams = new HashMap<Object, Object>();
rendererParams.put("optimizedDataLoadingEnabled", new Boolean(true));
rendererParams.put("renderingBuffer", new Integer(mapContext.getBuffer()));
rendererParams.put("maxFiltersToSendToDatastore", DefaultWebMapService.getMaxFilterRules());
rendererParams.put(ShapefileRenderer.SCALE_COMPUTATION_METHOD_KEY,
ShapefileRenderer.SCALE_OGC);
if (AA_NONE.equals(antialias)) {
rendererParams.put(ShapefileRenderer.TEXT_RENDERING_KEY,
StreamingRenderer.TEXT_RENDERING_STRING);
} else {
rendererParams.put(ShapefileRenderer.TEXT_RENDERING_KEY,
StreamingRenderer.TEXT_RENDERING_ADAPTIVE);
}
if (DefaultWebMapService.isLineWidthOptimizationEnabled()) {
rendererParams.put(StreamingRenderer.LINE_WIDTH_OPTIMIZATION_KEY, true);
}
// turn on advanced projection handling
rendererParams.put(StreamingRenderer.ADVANCED_PROJECTION_HANDLING_KEY, true);
if(DefaultWebMapService.isContinuousMapWrappingEnabled()) {
rendererParams.put(StreamingRenderer.CONTINUOUS_MAP_WRAPPING, true);
}
// see if the user specified a dpi
if (mapContext.getRequest().getFormatOptions().get("dpi") != null) {
rendererParams.put(StreamingRenderer.DPI_KEY, ((Integer) mapContext.getRequest()
.getFormatOptions().get("dpi")));
}
boolean kmplacemark = false;
if (mapContext.getRequest().getFormatOptions().get("kmplacemark") != null)
kmplacemark = ((Boolean) mapContext.getRequest().getFormatOptions().get("kmplacemark"))
.booleanValue();
if (kmplacemark) {
// create a StyleVisitor that copies a style, but removes the
// PointSymbolizers and TextSymbolizers
KMLStyleFilteringVisitor dupVisitor = new KMLStyleFilteringVisitor();
// Remove PointSymbolizers and TextSymbolizers from the
// layers' Styles to prevent their rendering on the
// raster image. Both are better served with the
// placemarks.
MapLayer[] layers = mapContext.getLayers();
for (int i = 0; i < layers.length; i++) {
Style style = layers[i].getStyle();
style.accept(dupVisitor);
Style copy = (Style) dupVisitor.getCopy();
layers[i].setStyle(copy);
}
}
renderer.setRendererHints(rendererParams);
// if abort already requested bail out
// if (this.abortRequested) {
// graphic.dispose();
// return null;
// }
// enforce no more than x rendering errors
int maxErrors = wms.getMaxRenderingErrors();
MaxErrorEnforcer errorChecker = new MaxErrorEnforcer(renderer, maxErrors);
// Add a render listener that ignores well known rendering exceptions and reports back non
// ignorable ones
final RenderExceptionStrategy nonIgnorableExceptionListener;
nonIgnorableExceptionListener = new RenderExceptionStrategy(renderer);
renderer.addRenderListener(nonIgnorableExceptionListener);
// setup the timeout enforcer (the enforcer is neutral when the timeout is 0)
int maxRenderingTime = wms.getMaxRenderingTime() * 1000;
RenderingTimeoutEnforcer timeout = new RenderingTimeoutEnforcer(maxRenderingTime, renderer,
graphic);
timeout.start();
try {
// finally render the image;
renderer.paint(graphic, paintArea, mapContext.getRenderingArea(),
mapContext.getRenderingTransform());
// apply watermarking
if (layout != null) {
try {
layout.paint(graphic, paintArea, mapContext);
} catch (Exception e) {
throw new ServiceException("Problem occurred while trying to watermark data", e);
}
}
} finally {
timeout.stop();
graphic.dispose();
}
// check if the request did timeout
if (timeout.isTimedOut()) {
throw new ServiceException(
"This requested used more time than allowed and has been forcefully stopped. "
+ "Max rendering time is " + (maxRenderingTime / 1000.0) + "s");
}
// check if a non ignorable error occurred
if (nonIgnorableExceptionListener.exceptionOccurred()) {
Exception renderError = nonIgnorableExceptionListener.getException();
throw new ServiceException("Rendering process failed", renderError, "internalError");
}
// check if too many errors occurred
if (errorChecker.exceedsMaxErrors()) {
throw new ServiceException("More than " + maxErrors
+ " rendering errors occurred, bailing out.", errorChecker.getLastException(),
"internalError");
}
// if (!this.abortRequested) {
if (palette != null && palette.getMapSize() < 256)
image = optimizeSampleModel(preparedImage);
else
image = preparedImage;
// }
RenderedImageMap map = new RenderedImageMap(mapContext, image, getMimeType());
return map;
}
protected MapDecorationLayout findDecorationLayout(WMSMapContext mapContext, final boolean tiled) {
String layoutName = null;
if (mapContext.getRequest().getFormatOptions() != null) {
layoutName = (String) mapContext.getRequest().getFormatOptions().get("layout");
}
MapDecorationLayout layout = null;
if (layoutName != null) {
try {
File layoutDir = GeoserverDataDirectory.findConfigDir(
GeoserverDataDirectory.getGeoserverDataDirectory(), "layouts");
if (layoutDir != null) {
File layoutConfig = new File(layoutDir, layoutName + ".xml");
if (layoutConfig.exists() && layoutConfig.canRead()) {
layout = MapDecorationLayout.fromFile(layoutConfig, tiled);
} else {
LOGGER.log(Level.WARNING, "Unknown layout requested: " + layoutName);
}
} else {
LOGGER.log(Level.WARNING, "No layout directory defined");
}
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Failed to load layout: " + layoutName, e);
}
}
if (layout == null) {
layout = tiled ? new MetatiledMapDecorationLayout() : new MapDecorationLayout();
}
MapDecorationLayout.Block watermark = getWatermark(wms.getServiceInfo());
if (watermark != null) {
layout.addBlock(watermark);
}
return layout;
}
public static MapDecorationLayout.Block getWatermark(WMSInfo wms) {
WatermarkInfo watermark = (wms == null ? null : wms.getWatermark());
if (watermark != null && watermark.isEnabled()) {
Map<String, String> options = new HashMap<String, String>();
options.put("url", watermark.getURL());
options.put("opacity", Float.toString((255f - watermark.getTransparency()) / 2.55f));
MapDecoration d = new WatermarkDecoration();
try {
d.loadOptions(options);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Couldn't construct watermark from configuration", e);
throw new ServiceException(e);
}
MapDecorationLayout.Block.Position p = null;
switch (watermark.getPosition()) {
case TOP_LEFT:
p = MapDecorationLayout.Block.Position.UL;
break;
case TOP_CENTER:
p = MapDecorationLayout.Block.Position.UC;
break;
case TOP_RIGHT:
p = MapDecorationLayout.Block.Position.UR;
break;
case MID_LEFT:
p = MapDecorationLayout.Block.Position.CL;
break;
case MID_CENTER:
p = MapDecorationLayout.Block.Position.CC;
break;
case MID_RIGHT:
p = MapDecorationLayout.Block.Position.CR;
break;
case BOT_LEFT:
p = MapDecorationLayout.Block.Position.LL;
break;
case BOT_CENTER:
p = MapDecorationLayout.Block.Position.LC;
break;
case BOT_RIGHT:
p = MapDecorationLayout.Block.Position.LR;
break;
default:
throw new ServiceException(
"Unknown WatermarkInfo.Position value. Something is seriously wrong.");
}
return new MapDecorationLayout.Block(d, p, null, new Point(0, 0));
}
return null;
}
/**
* Sets up a {@link BufferedImage#TYPE_4BYTE_ABGR} if the paletteInverter is not provided, or a
* indexed image otherwise. Subclasses may override this method should they need a special kind
* of image
*
* @param width
* @param height
* @param paletteInverter
* @return
*/
final protected RenderedImage prepareImage(int width, int height, IndexColorModel palette,
boolean transparent) {
return ImageUtils.createImage(width, height, isPaletteSupported() ? palette : null,
transparent && isTransparencySupported());
}
/**
* Returns true if the format supports image transparency, false otherwise (defaults to
* {@code true})
*
* @return true if the format supports image transparency, false otherwise
*/
public boolean isTransparencySupported() {
return transparencySupported;
}
public void setTransparencySupported(boolean supportsTransparency) {
this.transparencySupported = supportsTransparency;
}
/**
* Returns true if the format supports palette encoding, false otherwise (defaults to
* {@code true}).
*
* @return true if the format supports palette encoding, false otherwise
*/
public boolean isPaletteSupported() {
return palleteSupported;
}
public void setPaletteSupported(boolean supportsPalette) {
this.palleteSupported = supportsPalette;
}
/**
* When you override {@link #prepareImage(int, int, IndexColorModel, boolean)} remember to
* override this one as well
*
* @param width
* @param height
* @param palette
* @param transparent
* @return
*/
protected long getDrawingSurfaceMemoryUse(int width, int height, IndexColorModel palette,
boolean transparent) {
return ImageUtils.getDrawingSurfaceMemoryUse(width, height, isPaletteSupported() ? palette
: null, transparent && isTransparencySupported());
}
/**
* This takes an image with an indexed color model that uses less than 256 colors and has a 8bit
* sample model, and transforms it to one that has the optimal sample model (for example, 1bit
* if the palette only has 2 colors)
*
* @param source
* @return
*/
private static RenderedImage optimizeSampleModel(RenderedImage source) {
int w = source.getWidth();
int h = source.getHeight();
ImageLayout layout = new ImageLayout();
layout.setColorModel(source.getColorModel());
layout.setSampleModel(source.getColorModel().createCompatibleSampleModel(w, h));
// if I don't force tiling off with this setting an exception is thrown
// when writing the image out...
layout.setTileWidth(w);
layout.setTileHeight(h);
RenderingHints hints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout);
// TODO SIMONE why not format?
return LookupDescriptor.create(source, IDENTITY_TABLE, hints);
}
/**
* Renders a single coverage as the final RenderedImage to be encoded, skipping all of the
* Java2D machinery and using a pure JAI chain of transformations instead. This considerably
* improves both scalability and performance
*
* @param mapContext
* The map definition (used for map size and transparency/color management)
* @param layerIndex
* the layer that is supposed to contain a coverage
* @param renderedCoverages
* placeholder where to deposit rendered coverages, if any, so that they can be
* disposed later
* @return the result of rendering the coverage, or null if there was no coverage, or the
* coverage could not be renderer for some reason
*/
private RenderedImage directRasterRender(WMSMapContext mapContext, int layerIndex,
List<GridCoverage2D> renderedCoverages) throws IOException {
//
// extract the raster symbolizers
//
List<RasterSymbolizer> symbolizers = getRasterSymbolizers(mapContext, 0);
if (symbolizers.size() != 1)
return null;
RasterSymbolizer symbolizer = symbolizers.get(0);
//
// Get a reader
//
final Feature feature = mapContext.getLayer(0).getFeatureSource().getFeatures().features().next();
final AbstractGridCoverage2DReader reader = (AbstractGridCoverage2DReader) feature.getProperty("grid").getValue();
final Object params = feature.getProperty("params").getValue();
// if there is a output tile size hint, use it, otherwise use the output size itself
final int tileSizeX;
final int tileSizeY;
if (mapContext.getTileSize() != -1) {
tileSizeX = tileSizeY = mapContext.getTileSize();
} else {
tileSizeX = mapContext.getMapWidth();
tileSizeY = mapContext.getMapHeight();
}
//
// dimensions
//
final int mapWidth = mapContext.getMapWidth();
final int mapHeight= mapContext.getMapHeight();
final ReferencedEnvelope mapEnvelope = mapContext.getAreaOfInterest();
final CoordinateReferenceSystem mapCRS=mapContext.getCoordinateReferenceSystem();
final Rectangle mapRasterArea = new Rectangle(0, 0, mapWidth,mapHeight);
final AffineTransform worldToScreen = RendererUtilities.worldToScreenTransform(mapEnvelope, mapRasterArea);
//
// Check transparency and bg color
//
final boolean transparent = mapContext.isTransparent() && isTransparencySupported();
Color bgColor = mapContext.getBgColor();
// set transparency
if (transparent) {
bgColor = new Color(bgColor.getRed(), bgColor.getGreen(), bgColor.getBlue(), 0);
} else {
bgColor = new Color(bgColor.getRed(), bgColor.getGreen(), bgColor.getBlue(), 255);
}
//
// grab the interpolation
//
Interpolation interpolation = Interpolation.getInstance(Interpolation.INTERP_NEAREST);
if (wms != null) {
if (WMSInterpolation.Nearest.equals(wms.getInterpolation())) {
interpolation = Interpolation.getInstance(Interpolation.INTERP_NEAREST);
} else if (WMSInterpolation.Bilinear.equals(wms.getInterpolation())) {
interpolation = Interpolation.getInstance(Interpolation.INTERP_BILINEAR);
} else if (WMSInterpolation.Bicubic.equals(wms.getInterpolation())) {
interpolation = Interpolation.getInstance(Interpolation.INTERP_BICUBIC);
}
}
//
// read best available coverage and render it
//
final CoordinateReferenceSystem coverageCRS= reader.getCrs();
final GridGeometry2D readGG;
final ReferencedEnvelope bufferedEnvelope;
final Rectangle bufferedTargetArea;
final boolean equalsMetadata=CRS.equalsIgnoreMetadata(mapCRS, coverageCRS);
boolean sameCRS;
try {
sameCRS = equalsMetadata?true:CRS.findMathTransform(mapCRS, coverageCRS,true).isIdentity();
} catch (FactoryException e1) {
final IOException ioe= new IOException();
ioe.initCause(e1);
throw ioe;
}
if(sameCRS){
readGG = new GridGeometry2D(
new GridEnvelope2D(mapRasterArea),
mapEnvelope );
bufferedTargetArea=null;
bufferedEnvelope=null;
}else{
//
// SG added gutter to the drawing. We need to investigate much more and also we need to do this only when needed
//
// final int bPadding=interpolation.getBottomPadding();
// final int lPadding=interpolation.getLeftPadding();
// final int rPadding=interpolation.getRightPadding();
// final int uPadding=interpolation.getTopPadding();
// final Rectangle bufferedTargetArea = (Rectangle) rasterArea.clone();
// bufferedTargetArea.add(rasterArea.x+rasterArea.width+-rPadding*2, rasterArea.y+rasterArea.height+-uPadding*2);
// bufferedTargetArea.add(rasterArea.x-lPadding*2, rasterArea.y-bPadding*2);
bufferedTargetArea = (Rectangle) mapRasterArea.clone();
bufferedTargetArea.add(mapRasterArea.x+mapRasterArea.width+10, mapRasterArea.y+mapRasterArea.height+10);
bufferedTargetArea.add(mapRasterArea.x-10, mapRasterArea.y-10);
final GridToEnvelopeMapper ge = new GridToEnvelopeMapper(new GridEnvelope2D(mapRasterArea), mapEnvelope);
readGG = new GridGeometry2D(
new GridEnvelope2D(bufferedTargetArea),
PixelInCell.CELL_CORNER,
ge.createTransform(),
mapCRS,
null );
bufferedEnvelope=new ReferencedEnvelope(readGG.getEnvelope2D());
}
// actual read
RenderedImage image = null;
try {
final GridCoverage2D coverage;
final Color readerBgColor = transparent ? null : bgColor;
// SG take into account gutter for reprojection
if(sameCRS) {
coverage = readBestCoverage(
reader,
params,
mapEnvelope,
mapRasterArea,
interpolation,
readerBgColor);
}else{
coverage = readBestCoverage(
reader,
params,
bufferedEnvelope,
bufferedTargetArea,
interpolation,
readerBgColor);
}
//
// now, render the coverage using the gridcoverage renderer
//
try {
if (coverage == null) {
// we're outside of the coverage definition area, return an empty space
image = createBkgImage((float) mapWidth,(float) mapHeight, bgColor,null);
} else {
final GridCoverageRenderer gcr;
if(sameCRS){
gcr = new GridCoverageRenderer(
mapCRS,
mapEnvelope, mapRasterArea, worldToScreen,
new RenderingHints(JAI.KEY_INTERPOLATION,interpolation));
} else {
//
// SG added gutter to the drawing. We need to investigate much more and also we need to do this only when needed
//
gcr = new GridCoverageRenderer(
mapCRS,
bufferedEnvelope,
bufferedTargetArea,
worldToScreen,
new RenderingHints(JAI.KEY_INTERPOLATION,interpolation));
}
// create a solid color empty image
image = gcr.renderImage(coverage, symbolizer, interpolation,
mapContext.getBgColor(), tileSizeX, tileSizeY);
}
} finally {
// once the final image is rendered we need to clean up the planar image chain
// that the coverage references to
if (coverage != null)
renderedCoverages.add(coverage);
}
} catch (Throwable e) {
throw new ServiceException(e);
}
// check if we managed to process the coverage into an image
if (image == null) {
return null;
}
////
//
// Final Touch
////
//
// We need to prepare the background values for the finalcut on the image we have prepared. If
// we need to enlarge the image we go with Mosaic if we need to crop we use Crop. Notice that
// if we need to mess up with the background color we need to go by Mosaic and we cannot use Crop
// since it does not support changing the bkg color.
//
////
final Rectangle imageBounds = PlanarImage.wrapRenderedImage(image).getBounds();
// we need to do a mosaic, let's prepare a layout
// prepare a final image layout should we need to perform a mosaic or a crop
final ImageLayout layout = new ImageLayout();
layout.setMinX(0);
layout.setMinY(0);
layout.setWidth(mapWidth);
layout.setHeight(mapHeight);
layout.setTileGridXOffset(0);
layout.setTileGridYOffset(0);
layout.setTileWidth(tileSizeX);
layout.setTileHeight(tileSizeY);
// We need to find the background color expressed in terms of image color components
// (which depends on the color model nature, the input and output transparency)
// TODO: there must be a more general way to turn a color into the
// required components for a certain color model... right???
ColorModel cm = image.getColorModel();
double[] bgValues = null;
// collecting alpha channels as needed
PlanarImage[] alphaChannels = null;
//
// IndexColorModel
//
final ImageWorker worker = new ImageWorker(image);
final int transparencyType=cm.getTransparency();
// in case of index color model we try to preserve it, so that output
// formats that can work with it can enjoy its extra compactness
if (cm instanceof IndexColorModel) {
final IndexColorModel icm = (IndexColorModel) cm;
// try to find the index that matches the requested background color
final int bgColorIndex;
if(transparent) {
bgColorIndex = icm.getTransparentPixel();
} else {
bgColorIndex = ColorUtilities.findColorIndex(bgColor, icm);
}
//we did not find the background color, well we have to expand to RGB and then tell Mosaic to use the RGB(A) color as the
// background
if (bgColorIndex == -1) {
// we need to expand the image to RGB
image = worker.forceComponentColorModel().getRenderedImage();
if(transparent) {
image = addAlphaChannel(image);
worker.setImage(image);
}
bgValues = new double[] { bgColor.getRed(), bgColor.getGreen(), bgColor.getBlue(),
transparent ? 0 : 255 };
cm = image.getColorModel();
} else {
// we found the background color in the original image palette therefore we set its index as the bkg value.
// The final Mosaic will use the IndexColorModel of this image anywa, therefore all we need to do is to force
// the background to point to the right color in the palettte
bgValues = new double[] { bgColorIndex };
}
//collect alpha channels if we have them in order to reuse them later on for mosaic operation
if (cm.hasAlpha()) {
worker.forceComponentColorModel();
final RenderedImage alpha =worker.retainLastBand().getRenderedImage();
alphaChannels = new PlanarImage[] { PlanarImage.wrapRenderedImage(alpha) };
}
}
//
// ComponentColorModel
//
// in case of component color model
if (cm instanceof ComponentColorModel) {
// convert to RGB if necessary
ComponentColorModel ccm = (ComponentColorModel) cm;
boolean hasAlpha = cm.hasAlpha();
// if we have a grayscale image see if we have to expand to RGB
if (ccm.getNumColorComponents() == 1) {
if((!isLevelOfGray(bgColor) && !transparent) || (ccm.getTransferType() == DataBuffer.TYPE_DOUBLE ||
ccm.getTransferType() == DataBuffer.TYPE_FLOAT
|| ccm.getTransferType() == DataBuffer.TYPE_UNDEFINED)) {
// expand to RGB, this is not a case we can optimize
final ImageWorker iw = new ImageWorker(image);
if (hasAlpha) {
final RenderedImage alpha = iw.retainLastBand().getRenderedImage();
// get first band
final RenderedImage gray = new ImageWorker(image).retainFirstBand()
.getRenderedImage();
image = new ImageWorker(gray).bandMerge(3).addBand(alpha, false)
.forceComponentColorModel().forceColorSpaceRGB().getRenderedImage();
} else {
image = iw.bandMerge(3).forceComponentColorModel().forceColorSpaceRGB()
.getRenderedImage();
}
} else if(!hasAlpha) {
// no transparency in the original data, so no need to expand to RGB
if(transparent) {
// we need to expand the image with an alpha channel
image = addAlphaChannel(image);
bgValues = new double[] { mapToGrayColor(bgColor, ccm), 0 };
} else {
bgValues = new double[] { mapToGrayColor(bgColor, ccm) };
}
} else {
// extract the alpha channel
final ImageWorker iw = new ImageWorker(image);
final RenderedImage alpha = iw.retainLastBand().getRenderedImage();
alphaChannels = new PlanarImage[] { PlanarImage.wrapRenderedImage(alpha) };
if (transparent) {
bgValues = new double[] { mapToGrayColor(bgColor, ccm), 0 };
} else {
bgValues = new double[] { mapToGrayColor(bgColor, ccm), 255 };
}
}
// get back the ColorModel
cm = image.getColorModel();
ccm = (ComponentColorModel) cm;
hasAlpha = cm.hasAlpha();
}
if(bgValues == null) {
if (hasAlpha) {
// get alpha
final ImageWorker iw = new ImageWorker(image);
final RenderedImage alpha = iw.retainLastBand().getRenderedImage();
alphaChannels = new PlanarImage[] { PlanarImage.wrapRenderedImage(alpha) };
if (transparent) {
bgValues = new double[] { bgColor.getRed(), bgColor.getGreen(),
bgColor.getBlue(), 0 };
} else {
bgValues = new double[] { bgColor.getRed(), bgColor.getGreen(),
bgColor.getBlue(), 255 };
}
} else {
if (transparent) {
image = addAlphaChannel(image);
// this will work fine for all situation where the color components are <= 3
// e.g., one band rasters with no colormap will have only one usually
bgValues = new double[] { 0, 0, 0, 0 };
} else {
// TODO: handle the case where the component color model is not RGB
// We cannot use ImageWorker as is because it basically seems to assume
// component -> 3 band in forceComponentColorModel()
// but I guess we'll need to turn the image into a 3 band RGB one.
bgValues = new double[] { bgColor.getRed(), bgColor.getGreen(),
bgColor.getBlue() };
}
}
}
}
//
// If we need to add a collar use mosaic or if we need to blend/apply a bkg color
if(!(imageBounds.contains(mapRasterArea) || imageBounds.equals(mapRasterArea))||transparencyType!=Transparency.OPAQUE) {
ROI[] rois = new ROI[] { new ROIShape(imageBounds) };
// build the transparency thresholds
double[][] thresholds = new double[][] { { ColorUtilities.getThreshold(image
.getSampleModel().getDataType()) } };
// apply the mosaic
image = MosaicDescriptor.create(new RenderedImage[] { image },
alphaChannels != null && transparencyType==Transparency.TRANSLUCENT ? MosaicDescriptor.MOSAIC_TYPE_BLEND: MosaicDescriptor.MOSAIC_TYPE_OVERLAY,
alphaChannels, rois, thresholds, bgValues, new RenderingHints(
JAI.KEY_IMAGE_LAYOUT, layout));
} else {
// Check if we need to crop a subset of the produced image, else return it right away
if (imageBounds.contains(mapRasterArea) && !imageBounds.equals(mapRasterArea)) { // the produced image does not need a final mosaicking operation but a crop!
// reduce the image to the actually required size
image= CropDescriptor.create(
image,
(float)0,
(float)0,
(float)mapWidth,
(float)mapHeight,
null);
}
}
return image;
}
private RenderedImage addAlphaChannel(RenderedImage image) {
final ImageLayout tempLayout= new ImageLayout(image);
tempLayout.unsetValid(ImageLayout.COLOR_MODEL_MASK).unsetValid(ImageLayout.SAMPLE_MODEL_MASK);
RenderedImage alpha = ConstantDescriptor.create(
Float.valueOf( image.getWidth()),
Float.valueOf(image.getHeight()),
new Byte[] { Byte.valueOf((byte) 255) },
new RenderingHints(JAI.KEY_IMAGE_LAYOUT,tempLayout));
image = BandMergeDescriptor.create(image, alpha, null);
return image;
}
/**
* Given a one band (plus eventual alpha) color model and the red part of a gray
* color returns the appropriate background color to be used in the mosaic operation
* @param red
* @param cm
* @return
*/
double mapToGrayColor(Color gray, ComponentColorModel cm) {
double[] rescaleFactors = new double[DataBuffer.TYPE_UNDEFINED + 1];
rescaleFactors[DataBuffer.TYPE_BYTE] = 1;
rescaleFactors[DataBuffer.TYPE_SHORT] = 255;
rescaleFactors[DataBuffer.TYPE_INT] = Integer.MAX_VALUE / 255;
rescaleFactors[DataBuffer.TYPE_USHORT] = 512;
rescaleFactors[DataBuffer.TYPE_DOUBLE] = 1 / 255.0;
rescaleFactors[DataBuffer.TYPE_FLOAT] = 1 / 255.0;
rescaleFactors[DataBuffer.TYPE_UNDEFINED] = 1;
return gray.getRed() / rescaleFactors[cm.getTransferType()];
}
/**
* Returns true if the color is a level of gray
* @param color
* @return
*/
private static boolean isLevelOfGray(Color color) {
return color.getRed() == color.getBlue() && color.getRed() == color.getGreen();
}
/**
* Creates a bkg image using the supplied parameters.
* @param width the width of the timage to create
* @param height the height of the image to create
* @param bgColor the background color of the image to create
* @param renderingHints the hints to apply
* @return a {@link RenderedImage} with constant values as fill
*/
private final static RenderedImage createBkgImage(float width, float height, Color bgColor,
RenderingHints renderingHints) {
// prepare bands for constant image if needed
final Number[] bands = new Byte[] { (byte) bgColor.getRed(), (byte) bgColor.getGreen(),
(byte) bgColor.getBlue(), (byte) bgColor.getAlpha() };
return ConstantDescriptor.create(width,height, bands, renderingHints);
}
/**
* Reads the best matching grid out of a grid coverage applying sub-sampling and using overviews
* as necessary
*
* @param mapContext
* @param reader
* @param params
* @param requestedRasterArea
* @param interpolation
* @return
* @throws IOException
*/
private static GridCoverage2D readBestCoverage(
final AbstractGridCoverage2DReader reader,
final Object params,
final ReferencedEnvelope requestedModelArea,
final Rectangle requestedRasterArea,
final Interpolation interpolation,
final Color bgColor) throws IOException {
////
//
// Intersect the present envelope with the request envelope, also in WGS 84 to make sure
// there is an actual intersection
//
////
try {
final CoordinateReferenceSystem coverageCRS=reader.getCrs();
final CoordinateReferenceSystem requestCRS= requestedModelArea.getCoordinateReferenceSystem();
final ReferencedEnvelope coverageEnvelope=new ReferencedEnvelope(reader.getOriginalEnvelope());
if(CRS.equalsIgnoreMetadata(coverageCRS, requestCRS)){
if(!coverageEnvelope.intersects((BoundingBox)requestedModelArea))
return null;
}else{
ReferencedEnvelope dataEnvelopeWGS84 = coverageEnvelope.transform(DefaultGeographicCRS.WGS84, true);
ReferencedEnvelope requestEnvelopeWGS84 = requestedModelArea.transform(
DefaultGeographicCRS.WGS84, true);
if (!dataEnvelopeWGS84.intersects((BoundingBox) requestEnvelopeWGS84))
return null;
}
} catch (Exception e) {
LOGGER.log(
Level.WARNING,
"Failed to compare data and request envelopes, proceeding with rendering anyways",
e);
}
// //
// It is an AbstractGridCoverage2DReader, let's use parameters
// if we have any supplied by a user.
// //
// first I created the correct ReadGeometry
final Parameter<GridGeometry2D> readGG = (Parameter<GridGeometry2D>) AbstractGridFormat.READ_GRIDGEOMETRY2D.createValue();
readGG.setValue(new GridGeometry2D(new GridEnvelope2D(requestedRasterArea), requestedModelArea));
final Parameter<Interpolation> readInterpolation=(Parameter<Interpolation>) ImageMosaicFormat.INTERPOLATION.createValue();
readInterpolation.setValue(interpolation);
final Parameter<Color> bgColorParam;
if(bgColor != null) {
bgColorParam = (Parameter<Color>) AbstractGridFormat.BACKGROUND_COLOR.createValue();
bgColorParam.setValue(bgColor);
} else {
bgColorParam = null;
}
// then I try to get read parameters associated with this
// coverage if there are any.
GridCoverage2D coverage = null;
GeneralParameterValue[] readParams = (GeneralParameterValue[]) params;
final int length = readParams == null ? 0 :readParams.length;
if (length > 0) {
// //
//
// Getting parameters to control how to read this coverage.
// Remember to check to actually have them before forwarding
// them to the reader.
//
// //
// we have a valid number of parameters, let's check if
// also have a READ_GRIDGEOMETRY2D. In such case we just
// override it with the one we just build for this
// request.
final String readGGName = AbstractGridFormat.READ_GRIDGEOMETRY2D.getName().toString();
final String readInterpolationName = ImageMosaicFormat.INTERPOLATION.getName().toString();
final String bgColorName = AbstractGridFormat.BACKGROUND_COLOR.getName().toString();
int i = 0;
boolean foundInterpolation = false;
boolean foundGG = false;
boolean foundBgColor = false;
for (; i < length; i++) {
final String paramName = readParams[i].getDescriptor().getName().toString();
if (paramName.equalsIgnoreCase(readGGName)){
((Parameter) readParams[i]).setValue(readGG);
foundGG = true;
} else if(paramName.equalsIgnoreCase(readInterpolationName)){
((Parameter) readParams[i]).setValue(interpolation);
foundInterpolation = true;
} else if(paramName.equalsIgnoreCase(bgColorName) && bgColor != null) {
((Parameter) readParams[i]).setValue(bgColor);
foundBgColor = true;
}
}
// did we find anything?
if (!foundGG || !foundInterpolation || !(foundBgColor && bgColor != null)) {
// add the correct read geometry to the supplied
// params since we did not find anything
List<GeneralParameterValue> paramList = new ArrayList<GeneralParameterValue>();
paramList.addAll(Arrays.asList(readParams));
if(!foundGG) {
paramList.add(readGG);
}
if(!foundInterpolation) {
paramList.add(readInterpolation);
}
if(!foundBgColor && bgColor != null) {
paramList.add(bgColorParam);
}
readParams = (GeneralParameterValue[]) paramList.toArray(new GeneralParameterValue[paramList
.size()]);
}
coverage = (GridCoverage2D) reader.read(readParams);
} else {
// if for any reason the previous block did not produce a coverage (no params, empty params)
if(bgColorParam != null) {
coverage = (GridCoverage2D) reader.read(new GeneralParameterValue[] {readGG ,readInterpolation, bgColorParam});
} else {
coverage = (GridCoverage2D) reader.read(new GeneralParameterValue[] {readGG ,readInterpolation});
}
}
return coverage;
}
/**
* Returns the list of raster symbolizers contained in a specific layer of the map context (the
* full map context is provided in order to compute the current scale and thus determine the
* active rules)
*
* @param mc
* @param layerIndex
* @return
*/
static List<RasterSymbolizer> getRasterSymbolizers(WMSMapContext mc, int layerIndex) {
double scaleDenominator = RendererUtilities.calculateOGCScale(mc.getAreaOfInterest(),
mc.getMapWidth(), null);
MapLayer layer = mc.getLayer(layerIndex);
FeatureType featureType = layer.getFeatureSource().getSchema();
Style style = layer.getStyle();
RasterSymbolizerVisitor visitor = new RasterSymbolizerVisitor(scaleDenominator, featureType);
style.accept(visitor);
return visitor.getRasterSymbolizers();
}
}