/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2004-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.renderer.lite; import java.awt.AlphaComposite; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.Transparency; import java.awt.font.GlyphVector; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.image.BufferedImage; import java.io.IOException; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.media.jai.Interpolation; import javax.media.jai.InterpolationBicubic; import javax.media.jai.InterpolationBilinear; import javax.media.jai.InterpolationNearest; import javax.media.jai.JAI; import org.geotools.coverage.grid.GeneralGridRange; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader; import org.geotools.coverage.grid.io.AbstractGridFormat; import org.geotools.data.DataUtilities; import org.geotools.data.DefaultQuery; import org.geotools.data.FeatureSource; import org.geotools.data.Query; import org.geotools.data.crs.ForceCoordinateSystemFeatureResults; import org.geotools.data.memory.CollectionSource; import org.geotools.factory.CommonFactoryFinder; import org.geotools.factory.Hints; import org.geotools.feature.FeatureCollection; import org.geotools.feature.FeatureTypes; import org.geotools.feature.IllegalAttributeException; import org.geotools.filter.IllegalFilterException; import org.geotools.geometry.GeneralEnvelope; import org.geotools.geometry.jts.Decimator; import org.geotools.geometry.jts.LiteCoordinateSequenceFactory; import org.geotools.geometry.jts.LiteShape2; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.map.MapContext; import org.geotools.map.MapLayer; import org.geotools.parameter.Parameter; import org.geotools.referencing.CRS; import org.geotools.referencing.operation.matrix.XAffineTransform; import org.geotools.referencing.operation.transform.ConcatenatedTransform; import org.geotools.referencing.operation.transform.ProjectiveTransform; import org.geotools.renderer.GTRenderer; import org.geotools.renderer.RenderListener; import org.geotools.renderer.lite.gridcoverage2d.GridCoverageRenderer; import org.geotools.renderer.style.SLDStyleFactory; import org.geotools.renderer.style.Style2D; import org.geotools.styling.FeatureTypeStyle; import org.geotools.styling.LineSymbolizer; import org.geotools.styling.PointSymbolizer; import org.geotools.styling.PolygonSymbolizer; import org.geotools.styling.RasterSymbolizer; import org.geotools.styling.Rule; import org.geotools.styling.StyleAttributeExtractor; import org.geotools.styling.Symbolizer; import org.geotools.styling.TextSymbolizer; import org.geotools.util.NumberRange; import org.geotools.util.Range; import org.opengis.coverage.grid.GridCoverage; import org.opengis.coverage.processing.OperationNotFoundException; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.feature.type.GeometryDescriptor; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory; import org.opengis.filter.expression.PropertyName; import org.opengis.filter.spatial.BBOX; import org.opengis.parameter.GeneralParameterValue; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransform2D; import org.opengis.referencing.operation.TransformException; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; /** * A streaming implementation of the GTRenderer interface. *
* At the moment the streaming renderer is not thread safe
*
* @author James Macgill
* @author dblasby
* @author jessie eichar
* @author Simone Giannecchini
* @author Andrea Aime
* @author Alessio Fabiani
*
* @source $URL:
* http://svn.geotools.org/geotools/trunk/gt/module/render/src/org/geotools/renderer/lite/StreamingRenderer.java $
* @version $Id$
*/
public final class StreamingRenderer implements GTRenderer {
private final static int defaultMaxFiltersToSendToDatastore = 5; // default
/**
* Computes the scale as the ratio between map distances and real world distances,
* assuming 90dpi and taking into consideration projection deformations and actual
* earth shape.
* Use this method only when in need of accurate computation. Will break if the
* data extent is outside of the currenct projection definition area.
*/
public static final String SCALE_ACCURATE = "ACCURATE";
/**
* Very simple and lenient scale computation method that conforms to the OGC SLD
* specification 1.0, page 26.
This method is quite approximative, but should
* never break and ensure constant scale even on lat/lon unprojected maps (because
* in that case scale is computed as if the area was along the equator no matter
* what the real position is).
*/
public static final String SCALE_OGC = "OGC";
/** Tolerance used to compare doubles for equality */
private static final double TOLERANCE = 1e-6;
/** The logger for the rendering module. */
private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.rendering");
int error = 0;
/** Filter factory for creating bounding box filters */
private final static FilterFactory filterFactory = CommonFactoryFinder.getFilterFactory(null);
private final static PropertyName gridPropertyName = filterFactory.property("grid");
private final static PropertyName paramsPropertyName = filterFactory.property("params");
private final static PropertyName defaultGeometryPropertyName = filterFactory.property("");
/**
* Context which contains the layers and the bounding box which needs to be
* rendered.
*/
private MapContext context;
/**
* Flag which determines if the renderer is interactive or not. An
* interactive renderer will return rather than waiting for time consuming
* operations to complete (e.g. Image Loading). A non-interactive renderer
* (e.g. a SVG or PDF renderer) will block for these operations.
*/
private boolean interactive = true;
/**
* Flag which controls behaviour for applying affine transformation to the
* graphics object. If true then the transform will be concatenated to the
* existing transform. If false it will be replaced.
*/
private boolean concatTransforms = false;
/** Geographic map extent, eventually expanded to consider buffer area around the map */
private ReferencedEnvelope mapExtent;
/** Geographic map extent, as provided by the caller */
private ReferencedEnvelope originalMapExtent;
/** The size of the output area in output units. */
private Rectangle screenSize;
/**
* This flag is set to false when starting rendering, and will be checked
* during the rendering loop in order to make it stop forcefully
*/
private boolean renderingStopRequested = false;
/**
* The ratio required to scale the features to be rendered so that they fit
* into the output space.
*/
private double scaleDenominator;
/** Maximum displacement for generalization during rendering */
private double generalizationDistance = 1.0;
/** Factory that will resolve symbolizers into rendered styles */
private SLDStyleFactory styleFactory = new SLDStyleFactory();
protected LabelCache labelCache = new LabelCacheDefault();
/** The painter class we use to depict shapes onto the screen */
private StyledShapePainter painter = new StyledShapePainter(labelCache);
private IndexedFeatureResults indexedFeatureResults;
private ListenerList renderListeners = new ListenerList();
private RenderingHints java2dHints;
private boolean optimizedDataLoadingEnabledDEFAULT = false;
private boolean memoryPreloadingEnabledDEFAULT = false;
private int renderingBufferDEFAULT = 0;
private String scaleComputationMethodDEFAULT = SCALE_OGC;
/**
* Text will be rendered using the usual calls gc.drawString/drawGlyphVector.
* This is a little faster, and more consistent with how the platform renders
* the text in other applications. The downside is that on most platform the label
* and its eventual halo are not properly centered.
*/
public static final String TEXT_RENDERING_STRING = "STRING";
/**
* Text will be rendered using the associated {@link GlyphVector} outline, that is, a {@link Shape}.
* This ensures perfect centering between the text and the halo, but introduces more text aliasing.
*/
public static final String TEXT_RENDERING_OUTLINE = "OUTLINE";
/**
* The text rendering method, either TEXT_RENDERING_OUTLINE or TEXT_RENDERING_STRING
*/
public static final String TEXT_RENDERING_KEY = "textRenderingMethod";
private String textRenderingModeDEFAULT = TEXT_RENDERING_STRING;
/**
* Whether the thin line width optimization should be used, or not.
*
When rendering non antialiased lines adopting a width of 0 makes the * java2d renderer get into a fast path that generates the same output * as a 1 pixel wide line
* Unfortunately for antialiased rendering that optimization does not help, * and disallows controlling the width of thin lines. It is provided as * an explicit option as the optimization has been hard coded for years, * removing it when antialiasing is on by default will invalidate lots * of existing styles (making lines appear thicker). */ public static final String LINE_WIDTH_OPTIMIZATION_KEY = "lineWidthOptimization"; /** * Boolean flag controlling a memory/speed trade off related to how * multiple feature type styles are rendered. *
When enabled (by default) multiple feature type styles against the * same data source will be rendered in separate memory back buffers * in a way that allows the source to be scanned only once (each back buffer * is as big as the image being rendered).
*When disabled no memory back buffers will be used but the * feature source will be scanned once for every feature type style * declared against it
*/ public static final String OPTIMIZE_FTS_RENDERING_KEY = "optimizeFTSRendering"; public static final String LABEL_CACHE_KEY = "labelCache"; public static final String FORCE_CRS_KEY = "forceCRS"; public static final String DPI_KEY = "dpi"; public static final String DECLARED_SCALE_DENOM_KEY = "declaredScaleDenominator"; public static final String MEMORY_PRE_LOADING_KEY = "memoryPreloadingEnabled"; public static final String OPTIMIZED_DATA_LOADING_KEY = "optimizedDataLoadingEnabled"; public static final String SCALE_COMPUTATION_METHOD_KEY = "scaleComputationMethod"; /** * "optimizedDataLoadingEnabled" - Boolean yes/no (see default optimizedDataLoadingEnabledDEFAULT) * "memoryPreloadingEnabled" - Boolean yes/no (see default memoryPreloadingEnabledDEFAULT) * "declaredScaleDenominator" - Double the value of the scale denominator to use by the renderer. * by default the value is calculated based on the screen size * and the displayed area of the map. * "dpi" - Integer number of dots per inch of the display 90 DPI is the default (as declared by OGC) * "forceCRS" - CoordinateReferenceSystem declares to the renderer that all layers are of the CRS declared in this hint * "labelCache" - Declares the label cache that will be used by the renderer. */ private Map rendererHints = null; private AffineTransform worldToScreenTransform = null; private CoordinateReferenceSystem destinationCrs; private boolean canTransform; /** * Creates a new instance of LiteRenderer without a context. Use it only to * gain access to utility methods of this class or if you want to render * random feature collections instead of using the map context interface */ public StreamingRenderer() { } /** * Sets the flag which controls behaviour for applying affine transformation * to the graphics object. * * @param flag * If true then the transform will be concatenated to the * existing transform. If false it will be replaced. */ public void setConcatTransforms(boolean flag) { concatTransforms = flag; } /** * Flag which controls behaviour for applying affine transformation to the * graphics object. * * @return a boolean flag. If true then the transform will be concatenated * to the existing transform. If false it will be replaced. */ public boolean getConcatTransforms() { return concatTransforms; } /** * adds a listener that responds to error events of feature rendered events. * * @see RenderListener * * @param listener * the listener to add. */ public void addRenderListener(RenderListener listener) { renderListeners.add(listener); } /** * Removes a render listener. * * @see RenderListener * * @param listener * the listener to remove. */ public void removeRenderListener(RenderListener listener) { renderListeners.remove(listener); } private void fireFeatureRenderedEvent(Object feature) { if( !(feature instanceof SimpleFeature)){ return; } final Object[] objects = renderListeners.getListeners(); final int length = objects.length; RenderListener listener; for (int i = 0; i < length; i++) { listener = (RenderListener) objects[i]; listener.featureRenderer((SimpleFeature) feature); } } private void fireErrorEvent(Exception e) { Object[] objects = renderListeners.getListeners(); final int length = objects.length; RenderListener listener; for (int i = 0; i < length; i++) { listener = (RenderListener) objects[i]; listener.errorOccurred(e); } } /** * If you call this method from another thread than the one that called *paint
or render
the rendering will be
* forcefully stopped before termination
*/
public void stopRendering() {
renderingStopRequested = true;
labelCache.stop();
}
/**
* Renders features based on the map layers and their styles as specified in
* the map context using setContext
. This version of
* the method assumes that the size of the output area and the
* transformation from coordinates to pixels are known. The latter
* determines the map scale. The viewport (the visible part of the map) will
* be calculated internally.
*
* @param graphics
* The graphics object to draw to.
* @param paintArea
* The size of the output area in output units (eg: pixels).
* @param worldToScreen
* A transform which converts World coordinates to Screen
* coordinates.
* @task Need to check if the Layer CoordinateSystem is different to the
* BoundingBox rendering CoordinateSystem and if so, then transform
* the coordinates.
* @deprecated Use paint(Graphics2D graphics, Rectangle paintArea,
* ReferencedEnvelope mapArea) or paint(Graphics2D graphics,
* Rectangle paintArea, ReferencedEnvelope mapArea,
* AffineTransform worldToScreen) instead.
*/
public void paint(Graphics2D graphics, Rectangle paintArea,
AffineTransform worldToScreen) {
if (worldToScreen == null || paintArea == null) {
LOGGER.info("renderer passed null arguments");
return;
} // Other arguments get checked later
// First, create the bbox in real world coordinates
Envelope mapArea;
try {
mapArea = RendererUtilities.createMapEnvelope(paintArea,
worldToScreen);
paint(graphics, paintArea, mapArea, worldToScreen);
} catch (NoninvertibleTransformException e) {
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
fireErrorEvent(new Exception(
"Can't create pixel to world transform", e));
}
}
/**
* Renders features based on the map layers and their styles as specified in
* the map context using setContext
. This version of
* the method assumes that the area of the visible part of the map and the
* size of the output area are known. The transform between the two is
* calculated internally.
*
* @param graphics
* The graphics object to draw to.
* @param paintArea
* The size of the output area in output units (eg: pixels).
* @param mapArea
* the map's visible area (viewport) in map coordinates.
* @deprecated Use paint(Graphics2D graphics, Rectangle paintArea,
* ReferencedEnvelope mapArea) or paint(Graphics2D graphics,
* Rectangle paintArea, ReferencedEnvelope mapArea,
* AffineTransform worldToScreen) instead.
*/
public void paint(Graphics2D graphics, Rectangle paintArea, Envelope mapArea) {
if (mapArea == null || paintArea == null) {
LOGGER.info("renderer passed null arguments");
return;
} // Other arguments get checked later
paint(graphics, paintArea, mapArea, RendererUtilities
.worldToScreenTransform(mapArea, paintArea));
}
/**
* Renders features based on the map layers and their styles as specified in
* the map context using setContext
. This version of
* the method assumes that the area of the visible part of the map and the
* size of the output area are known. The transform between the two is
* calculated internally.
*
* @param graphics
* The graphics object to draw to.
* @param paintArea
* The size of the output area in output units (eg: pixels).
* @param mapArea
* the map's visible area (viewport) in map coordinates.
*/
public void paint(Graphics2D graphics, Rectangle paintArea,
ReferencedEnvelope mapArea) {
if (mapArea == null || paintArea == null) {
LOGGER.info("renderer passed null arguments");
return;
} // Other arguments get checked later
paint(graphics, paintArea, mapArea, RendererUtilities
.worldToScreenTransform(mapArea, paintArea));
}
/**
* Renders features based on the map layers and their styles as specified in
* the map context using setContext
. This version of
* the method assumes that paint area, envelope and worldToScreen transform
* are already computed. Use this method to avoid recomputation. Note
* however that no check is performed that they are really in sync!
*
* @param graphics
* The graphics object to draw to.
* @param paintArea
* The size of the output area in output units (eg: pixels).
* @param mapArea
* the map's visible area (viewport) in map coordinates.
* @param worldToScreen
* A transform which converts World coordinates to Screen
* coordinates.
* @deprecated Use paint(Graphics2D graphics, Rectangle paintArea,
* ReferencedEnvelope mapArea) or paint(Graphics2D graphics,
* Rectangle paintArea, ReferencedEnvelope mapArea,
* AffineTransform worldToScreen) instead.
*/
public void paint(Graphics2D graphics, Rectangle paintArea,
Envelope mapArea, AffineTransform worldToScreen) {
paint( graphics, paintArea, new ReferencedEnvelope(mapArea, context.getCoordinateReferenceSystem()),
worldToScreen);
}
private double computeScale(ReferencedEnvelope envelope, Rectangle paintArea, Map hints) {
if(getScaleComputationMethod().equals(SCALE_ACCURATE)) {
try {
return RendererUtilities.calculateScale(envelope,
paintArea.width, paintArea.height, hints);
} catch (Exception e) // probably either (1) no CRS (2) error xforming
{
LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e);
}
}
return RendererUtilities.calculateOGCScale(envelope, paintArea.width, hints);
}
/**
* Renders features based on the map layers and their styles as specified in
* the map context using setContext
. This version of
* the method assumes that paint area, envelope and worldToScreen transform
* are already computed. Use this method to avoid recomputation. Note
* however that no check is performed that they are really in sync!
*
* @param graphics
* The graphics object to draw to.
* @param paintArea
* The size of the output area in output units (eg: pixels).
* @param mapArea
* the map's visible area (viewport) in map coordinates. Its
* associate CRS is ALWAYS 2D
* @param worldToScreen
* A transform which converts World coordinates to Screen
* coordinates.
*/
public void paint(Graphics2D graphics, Rectangle paintArea,
ReferencedEnvelope mapArea, AffineTransform worldToScreen) {
// ////////////////////////////////////////////////////////////////////
//
// Check for null arguments, recompute missing ones if possible
//
// ////////////////////////////////////////////////////////////////////
if (graphics == null || paintArea == null) {
LOGGER.severe("renderer passed null arguments");
throw new NullPointerException("renderer passed null arguments");
} else if (mapArea == null && paintArea == null) {
LOGGER.severe("renderer passed null arguments");
throw new NullPointerException("renderer passed null arguments");
} else if (mapArea == null) {
LOGGER.severe("renderer passed null arguments");
throw new NullPointerException("renderer passed null arguments");
} else if (worldToScreen == null) {
worldToScreen = RendererUtilities.worldToScreenTransform(mapArea,
paintArea);
if (worldToScreen == null)
return;
}
// ////////////////////////////////////////////////////////////////////
//
// Setting base information
//
// TODO the way this thing is built is a mess if you try to use it in a
// multithreaded environment. I will fix this at the end.
//
// ////////////////////////////////////////////////////////////////////
destinationCrs = mapArea.getCoordinateReferenceSystem();
mapExtent = new ReferencedEnvelope(mapArea);
this.screenSize = paintArea;
this.worldToScreenTransform = worldToScreen;
error = 0;
if (java2dHints != null)
graphics.setRenderingHints(java2dHints);
// reset the abort flag
renderingStopRequested = false;
// ////////////////////////////////////////////////////////////////////
//
// Managing transformations , CRSs and scales
//
// If we are rendering to a component which has already set up some form
// of transformation then we can concatenate our transformation to it.
// An example of this is the ZoomPane component of the swinggui module.
// ////////////////////////////////////////////////////////////////////
if (concatTransforms) {
AffineTransform atg = graphics.getTransform();
atg.concatenate(worldToScreenTransform);
worldToScreenTransform = atg;
graphics.setTransform(worldToScreenTransform);
}
// compute scale according to the user specified method
scaleDenominator = computeScale(mapArea, paintArea, rendererHints);
//////////////////////////////////////////////////////////////////////
//
// Consider expanding the map extent so that a few more geometries
// will be considered, in order to catch those outside of the rendering
// bounds whose stroke is so thick that it countributes rendered area
//
//////////////////////////////////////////////////////////////////////
int buffer = getRenderingBuffer();
originalMapExtent = mapExtent;
if(buffer > 0) {
mapExtent = new ReferencedEnvelope(expandEnvelope(mapExtent, worldToScreen, buffer),
mapExtent.getCoordinateReferenceSystem());
}
// ////////////////////////////////////////////////////////////////////
//
// Processing all the map layers in the context using the accompaining
// styles
//
// ////////////////////////////////////////////////////////////////////
final MapLayer[] layers = context.getLayers();
labelCache.start();
if(labelCache instanceof LabelCacheDefault) {
boolean outlineEnabled = TEXT_RENDERING_OUTLINE.equals(getTextRenderingMethod());
((LabelCacheDefault) labelCache).setOutlineRenderingEnabled(outlineEnabled);
}
final int layersNumber = layers.length;
MapLayer currLayer;
for (int i = 0; i < layersNumber; i++) // DJB: for each layer (ie. one
{
currLayer = layers[i];
if (!currLayer.isVisible()) {
// Only render layer when layer is visible
continue;
}
if (renderingStopRequested) {
return;
}
labelCache.startLayer(i+"");
try {
// extract the feature type stylers from the style object
// and process them
processStylers(graphics, currLayer, worldToScreenTransform,
destinationCrs, mapExtent, screenSize, i+"");
} catch (Throwable t) {
LOGGER.log(Level.SEVERE, t.getLocalizedMessage(), t);
fireErrorEvent(new Exception(new StringBuffer(
"Exception rendering layer ").append(currLayer)
.toString(), t));
}
labelCache.endLayer(i+"", graphics, screenSize);
}
labelCache.end(graphics, paintArea);
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine(new StringBuffer("Style cache hit ratio: ").append(
styleFactory.getHitRatio()).append(" , hits ").append(
styleFactory.getHits()).append(", requests ").append(
styleFactory.getRequests()).toString());
if (error > 0) {
LOGGER
.warning(new StringBuffer(
"Number of Errors during paint(Graphics2D, AffineTransform) = ")
.append(error).toString());
}
}
/**
* Extends the provided {@link Envelope} in order to add the number of pixels
* specified by buffer
in every direction.
*
* @param envelope to extend.
* @param worldToScreen by means of which doing the extension.
* @param buffer to use for the extension.
* @return an extended version of the provided {@link Envelope}.
*/
private Envelope expandEnvelope(Envelope envelope, AffineTransform worldToScreen, int buffer) {
assert buffer>0;
double bufferX = Math.abs(buffer * 1.0 / XAffineTransform.getScaleX0(worldToScreen));
double bufferY = Math.abs(buffer * 1.0 / XAffineTransform.getScaleY0(worldToScreen));
return new Envelope(envelope.getMinX() - bufferX,
envelope.getMaxX() + bufferX, envelope.getMinY() - bufferY,
envelope.getMaxY() + bufferY);
}
/**
* Queries a given layer's Source
instance to be rendered.
* * Note: This is proof-of-concept quality only! At * the moment the query is not filtered, that means all objects with all * fields are read from the datastore for every call to this method. This * method should work like * {@link #queryLayer(MapLayer, FeatureSource, SimpleFeatureType, LiteFeatureTypeStyle[], Envelope, CoordinateReferenceSystem, CoordinateReferenceSystem, Rectangle, GeometryAttributeType)} * and eventually replace it. *
* * @param currLayer The actually processed layer for rendering * @param source Source to read data from */ //TODO: Implement filtering for bbox and read in only the need attributes Collection queryLayer(MapLayer currLayer, CollectionSource source) { //REVISIT: this method does not make sense. Always compares //new DefaultQuery(DefaultQuery.ALL) for reference equality with Query.All. GR. Collection results = null; DefaultQuery query = new DefaultQuery(DefaultQuery.ALL); Query definitionQuery; definitionQuery = currLayer.getQuery(); if (definitionQuery != Query.ALL) { if (query == Query.ALL) { query = new DefaultQuery(definitionQuery); } else { query = new DefaultQuery(DataUtilities.mixQueries(definitionQuery, query, "liteRenderer")); } } results = source.content(query.getFilter()); return results; } /** * Queries a given layer's features to be rendered based on the target * rendering bounding box. *
* If optimizedDataLoadingEnabled
attribute has been set to
* true
, the following optimization will be performed in
* order to limit the number of features returned:
*
envelope
will be queriedQuery
has been set to limit the resulting
* layer's features, the final filter to obtain them will respect it. This
* means that the bounding box filter and the Query filter will be combined,
* also including maxFeatures from Query
* NOTE : This is an internal method and should only be called by
* paint(Graphics2D, Rectangle, AffineTransform)
. It is
* package protected just to allow unit testing it.
*
currLayer
after
* querying its feature source
* @throws IllegalFilterException
* if something goes wrong constructing the bbox filter
* @throws IOException
* @throws IllegalAttributeException
* @see MapLayer#setQuery(org.geotools.data.Query)
*/
/*
* Default visibility for testing purposes
*/
FeatureCollectionMapLayer
's style and retrieves it's needed
* attribute names, returning at least the default geometry attribute name.
*
* @param layer
* the MapLayer
to determine the needed attributes
* from
* @param schema
* the layer
's FeatureSourcelayer
*/
private String[] findStyleAttributes(LiteFeatureTypeStyle[] styles,
SimpleFeatureType schema) {
final StyleAttributeExtractor sae = new StyleAttributeExtractor();
LiteFeatureTypeStyle lfts;
Rule[] rules;
int rulesLength;
final int length = styles.length;
for (int t = 0; t < length; t++) {
lfts = styles[t];
rules = lfts.elseRules;
rulesLength = rules.length;
for (int j = 0; j < rulesLength; j++) {
sae.visit(rules[j]);
}
rules = lfts.ruleList;
rulesLength = rules.length;
for (int j = 0; j < rulesLength; j++) {
sae.visit(rules[j]);
}
}
String[] ftsAttributes = sae.getAttributeNames();
/*
* DJB: this is an old comment - erase it soon (see geos-469 and below) -
* we only add the default geometry if it was used.
*
* GR: if as result of sae.getAttributeNames() ftsAttributes already
* contains geometry attribute names, they gets duplicated, which produces
* an error in AbstracDatastore when trying to create a derivate
* SimpleFeatureType. So I'll add the default geometry only if it is not
* already present, but: should all the geometric attributes be added by
* default? I will add them, but don't really know what's the expected
* behavior
*/
List atts = new LinkedList(Arrays.asList(ftsAttributes));
ListMapLayer
's feature source to return
* just the features for the target rendering extent
*
* @param schema
* the layer's feature source schema
* @param attributes
* set of needed attributes
* @param bbox
* the expression holding the target rendering bounding box
* @return an or'ed list of bbox filters, one for each geometric attribute
* in attributes
. If there are just one geometric
* attribute, just returns its corresponding
* GeometryFilter
.
* @throws IllegalFilterException
* if something goes wrong creating the filter
*/
private Filter createBBoxFilters(SimpleFeatureType schema, String[] attributes,
Envelope bbox) throws IllegalFilterException {
Filter filter = null;
final int length = attributes.length;
AttributeDescriptor attType;
for (int j = 0; j < length; j++) {
attType = schema.getDescriptor(attributes[j]);
// DJB: added this for better error messages!
if (attType == null) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine(new StringBuffer("Could not find '").append(
attributes[j]).append("' in the FeatureType (")
.append(schema.getTypeName()).append(")")
.toString());
throw new IllegalFilterException(new StringBuffer(
"Could not find '").append(
attributes[j] + "' in the FeatureType (").append(
schema.getTypeName()).append(")").toString());
}
if (attType instanceof GeometryDescriptor) {
BBOX gfilter = filterFactory.bbox( attType.getLocalName(), bbox.getMinX(), bbox.getMinY(), bbox.getMaxX(), bbox.getMaxY(), null );
if (filter == null) {
filter = gfilter;
} else {
filter = filterFactory.or( filter, gfilter );
}
}
}
return filter;
}
/**
* Checks if a rule can be triggered at the current scale level
*
* @param r
* The rule
* @return true if the scale is compatible with the rule settings
*/
private boolean isWithInScale(Rule r) {
return ((r.getMinScaleDenominator() - TOLERANCE) <= scaleDenominator)
&& ((r.getMaxScaleDenominator() + TOLERANCE) > scaleDenominator);
}
/**
* Creates a list of LiteFeatureTypeStyle
s with:
*
Note: This method has a lot of duplication with * {@link #createLiteFeatureTypeStyles(FeatureTypeStyle[], SimpleFeatureType, Graphics2D)}. *
* * @param featureStyles Styles to process * @param typeDescription The type description that has to be matched * @return ArrayList* What is really going on is the need to set up for reprojection; but *after* decimation has * occured. *
* * @param features * @param sourceCrs * @return FeatureCollection* In most cases, this is the desired effect. For example, all line features * may be rendered with a fat line and then a thin line. This produces a * 'cased' effect without any strange overlaps. *
** This method is internal and should only be called by render. *
**
* * @param graphics * DOCUMENT ME! * @param features * An array of features to be rendered * @param featureStylers * An array of feature stylers to be applied * @param at * DOCUMENT ME! * @param destinationCrs - * The destination CRS, or null if no reprojection is required * @param screenSize * @param layerId * @throws IOException * @throws IllegalAttributeException * @throws IllegalFilterException */ final private void processStylers(final Graphics2D graphics, MapLayer currLayer, AffineTransform at, CoordinateReferenceSystem destinationCrs, Envelope mapArea, Rectangle screenSize, String layerId) throws IllegalFilterException, IOException, IllegalAttributeException { /* * DJB: changed this a wee bit so that it now does the layer query AFTER * it has evaluated the rules for scale inclusion. This makes it so that * geometry columns (and other columns) will not be queried unless they * are actually going to be required. see geos-469 */ // ///////////////////////////////////////////////////////////////////// // // Preparing feature information and styles // // ///////////////////////////////////////////////////////////////////// final FeatureTypeStyle[] featureStylers = currLayer.getStyle().getFeatureTypeStyles(); final FeatureSource* This is an internal method and should only be called by processStylers. *
* @param currLayer * * @param graphics * @param drawMe * The feature to be rendered * @param symbolizers * An array of symbolizers which actually perform the rendering. * @param scaleRange * The scale range we are working on... provided in order to make * the style factory happy * @param shape * @param destinationCrs * @param layerId * @throws TransformException * @throws FactoryException */ final private void processSymbolizers(final Graphics2D graphics, final RenderableFeature drawMe, final Symbolizer[] symbolizers, NumberRange scaleRange, AffineTransform at, CoordinateReferenceSystem destinationCrs, String layerId) throws TransformException, FactoryException { final int length = symbolizers.length; for (int m = 0; m < length; m++) { // ///////////////////////////////////////////////////////////////// // // RASTER // // ///////////////////////////////////////////////////////////////// final Symbolizer symbolizer = symbolizers[m]; if (symbolizer instanceof RasterSymbolizer) { renderRaster(graphics, drawMe.content, (RasterSymbolizer) symbolizer, destinationCrs, scaleRange); } else { // ///////////////////////////////////////////////////////////////// // // FEATURE // // ///////////////////////////////////////////////////////////////// LiteShape2 shape = drawMe.getShape(symbolizer, at); if(shape == null) continue; if (symbolizer instanceof TextSymbolizer && drawMe.content instanceof SimpleFeature) { labelCache.put(layerId, (TextSymbolizer) symbolizers[m], (SimpleFeature) drawMe.content, shape, scaleRange); } else { Style2D style = styleFactory.createStyle(drawMe.content, symbolizer, scaleRange); painter.paint(graphics, shape, style, scaleDenominator); } } } fireFeatureRenderedEvent(drawMe.content); } /** * Renders a grid coverage on the device. * * @param graphics * DOCUMENT ME! * @param drawMe * the feature that contains the GridCoverage. The grid coverage * must be contained in the "grid" attribute * @param symbolizer * The raster symbolizer * @param scaleRange * @param world2Grid * @task make it follow the symbolizer */ private void renderRaster(Graphics2D graphics, Object drawMe, RasterSymbolizer symbolizer, CoordinateReferenceSystem destinationCRS, Range scaleRange) { final Object grid = gridPropertyName.evaluate( drawMe); if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine(new StringBuffer("rendering Raster for feature ") .append(drawMe.toString()).append(" - ").append( grid).toString()); try { // ///////////////////////////////////////////////////////////////// // // If the grid object is a reader we ask him to do its best for the // requested resolution, if it is a gridcoverage instead we have to // rely on the gridocerage renderer itself. // // ///////////////////////////////////////////////////////////////// // METABUFFER SUPPORT // final GeneralEnvelope metaBufferedEnvelope=handleTileBordersArtifacts(mapExtent,java2dHints,this.worldToScreenTransform); final GridCoverageRenderer gcr = new GridCoverageRenderer( destinationCRS, originalMapExtent, screenSize, java2dHints); // // // It is a grid coverage // // if (grid instanceof GridCoverage) // METABUFFER SUPPORT // gcr.paint(graphics, (GridCoverage2D) grid, symbolizer, metaBufferedEnvelope); gcr.paint(graphics, (GridCoverage2D) grid, symbolizer); else if (grid instanceof AbstractGridCoverage2DReader) { // // // It is an AbstractGridCoverage2DReader, let's use parameters // if we have any supplied by a user. // // // first I created the correct ReadGeometry final Parameter readGG = new Parameter( AbstractGridFormat.READ_GRIDGEOMETRY2D); // METABUFFER SUPPORT // readGG.setValue(new GridGeometry2D(new GeneralGridRange( // screenSize), metaBufferedEnvelope)); readGG.setValue(new GridGeometry2D(new GeneralGridRange( screenSize), mapExtent)); final AbstractGridCoverage2DReader reader = (AbstractGridCoverage2DReader) grid; // then I try to get read parameters associated with this // coverage if there are any. final Object params = paramsPropertyName.evaluate(drawMe); final GridCoverage2D coverage; if (params != null) { // // // // Getting parameters to control how to read this coverage. // Remember to check to actually have them before forwarding // them to the reader. // // // GeneralParameterValue[] readParams = (GeneralParameterValue[]) params; final int length = readParams.length; if (length > 0) { // 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 name = AbstractGridFormat.READ_GRIDGEOMETRY2D .getName().toString(); int i = 0; for (; i < length; i++) if (readParams[i].getDescriptor().getName() .toString().equalsIgnoreCase(name)) break; // did we find anything? if (i < length) { //we found another READ_GRIDGEOMETRY2D, let's override it. ((Parameter) readParams[i]).setValue(readGG); coverage = (GridCoverage2D) reader .read(readParams); } else { // add the correct read geometry to the supplied // params since we did not find anything GeneralParameterValue[] readParams2 = new GeneralParameterValue[length + 1]; System.arraycopy(readParams, 0, readParams2, 0, length); readParams2[length] = readGG; coverage = (GridCoverage2D) reader .read(readParams2); } } else // we have no parameters hence we just use the read grid // geometry to get a coverage coverage = (GridCoverage2D) reader .read(new GeneralParameterValue[] { readGG }); } else { coverage = (GridCoverage2D) reader .read(new GeneralParameterValue[] { readGG }); } // METABUFFER SUPPORT // gcr.paint(graphics, coverage, symbolizer,metaBufferedEnvelope); gcr.paint(graphics, coverage, symbolizer); } if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("Raster rendered"); } catch (FactoryException e) { LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e); fireErrorEvent(e); } catch (TransformException e) { LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e); fireErrorEvent(e); } catch (NoninvertibleTransformException e) { LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e); fireErrorEvent(e); } catch (IllegalArgumentException e) { LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e); fireErrorEvent(e); } catch (IOException e) { LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e); fireErrorEvent(e); } } /** * This method captures a first attempt to handle in a coherent way the problem of having artifacts at the * borders of drown tiles when using Tiling Clients like OpenLayers with higher order interpolation. * * @param mapExtent to draw on. * @param java2dHints to control the drawing process. * @param worldToScreen transformation that maps onto the screen. * @return a {@link GeneralEnvelope} that might be expanded in order to avoid * artifacts at the borders */ private GeneralEnvelope handleTileBordersArtifacts( ReferencedEnvelope mapExtent, RenderingHints java2dHints, AffineTransform worldToScreen) { // ///////////////////////////////////////////////////////////////////// // // Check if interpolation is required. // // ///////////////////////////////////////////////////////////////////// if (!java2dHints.containsKey(JAI.KEY_INTERPOLATION)) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("Unable to find interpolation for this request."); return new GeneralEnvelope(mapExtent); } final Interpolation interp = (Interpolation) java2dHints .get(JAI.KEY_INTERPOLATION); // ///////////////////////////////////////////////////////////////////// // // Ok, we have an interpolation, let's decide if we have to extend the // requested area accordingly. // // ///////////////////////////////////////////////////////////////////// int buffer = 0; if (interp instanceof InterpolationNearest) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("Interpolation Nearest no need for extending."); return new GeneralEnvelope(mapExtent); } if (interp instanceof InterpolationBilinear) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("Interpolation Bilinear extending.by 1 pixel at least"); buffer = 10; } else if (interp instanceof InterpolationBicubic) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("Interpolation Bicubic extending.by 2 pixel at least"); buffer = 30; } if (buffer <= 0) { return new GeneralEnvelope(mapExtent); } // ///////////////////////////////////////////////////////////////////// // // Doing the extension // // ///////////////////////////////////////////////////////////////////// final Envelope tempEnv =expandEnvelope(mapExtent, worldToScreen, buffer); final GeneralEnvelope newEnv = new GeneralEnvelope( new double[] { tempEnv.getMinX(), tempEnv.getMinY() , }, new double[] { tempEnv.getMaxX() , tempEnv.getMaxY() }); newEnv.setCoordinateReferenceSystem(mapExtent .getCoordinateReferenceSystem()); return newEnv; } /** * Finds the geometric attribute requested by the symbolizer * * @param drawMe * The feature * @param s * * /** Finds the geometric attribute requested by the symbolizer * * @param drawMe * The feature * @param s * The symbolizer * @return The geometry requested in the symbolizer, or the default geometry * if none is specified */ private com.vividsolutions.jts.geom.Geometry findGeometry(Object drawMe, Symbolizer s) { PropertyName geomName = getGeometryPropertyName(s); // get the geometry Geometry geom; if(geomName == null) { if(drawMe instanceof SimpleFeature) geom = (Geometry) ((SimpleFeature) drawMe).getDefaultGeometry(); else geom = (Geometry) defaultGeometryPropertyName.evaluate(drawMe, Geometry.class); } else { geom = (Geometry) geomName.evaluate(drawMe, Geometry.class); } return geom; } /** * Finds the geometric attribute coordinate reference system. * @param drawMe2 * * @param f The feature * @param s The symbolizer * @return The geometry requested in the symbolizer, or the default geometry if none is specified */ private org.opengis.referencing.crs.CoordinateReferenceSystem findGeometryCS( MapLayer currLayer, Object drawMe, Symbolizer s) { if( drawMe instanceof SimpleFeature ){ SimpleFeature f = (SimpleFeature) drawMe; PropertyName propertyName = getGeometryPropertyName(s); String geomName = propertyName != null ? propertyName.getPropertyName() : null; if (geomName == null || "".equals(geomName)) { SimpleFeatureType schema = f.getFeatureType(); GeometryDescriptor geom = schema.getGeometryDescriptor(); return geom.getType().getCoordinateReferenceSystem(); } else { SimpleFeatureType schema = f.getFeatureType(); GeometryDescriptor geom = (GeometryDescriptor) schema.getDescriptor( geomName ); return geom.getType().getCoordinateReferenceSystem(); } } else if ( currLayer.getSource() != null ) { return currLayer.getSource().getCRS(); } return null; } private PropertyName getGeometryPropertyName(Symbolizer s) { String geomName = null; // TODO: fix the styles, the getGeometryPropertyName should probably be // moved into an // interface... if (s instanceof PolygonSymbolizer) { geomName = ((PolygonSymbolizer) s).getGeometryPropertyName(); } else if (s instanceof PointSymbolizer) { geomName = ((PointSymbolizer) s).getGeometryPropertyName(); } else if (s instanceof LineSymbolizer) { geomName = ((LineSymbolizer) s).getGeometryPropertyName(); } else if (s instanceof TextSymbolizer) { geomName = ((TextSymbolizer) s).getGeometryPropertyName(); } if( geomName == null ){ return null; } return filterFactory.property(geomName); } /** * Getter for property interactive. * * @return Value of property interactive. */ public boolean isInteractive() { return interactive; } /** * Sets the interactive status of the renderer. An interactive renderer * won't wait for long image loading, preferring an alternative mark instead * * @param interactive * new value for the interactive property */ public void setInteractive(boolean interactive) { this.interactive = interactive; } /** ** Returns true if the optimized data loading is enabled, false otherwise. *
** When optimized data loading is enabled, lite renderer will try to load * only the needed feature attributes (according to styles) and to load only * the features that are in (or overlaps with)the bounding box requested for * painting *
* */ private boolean isOptimizedDataLoadingEnabled() { if (rendererHints == null) return optimizedDataLoadingEnabledDEFAULT; Object result = null; try{ result=rendererHints .get("optimizedDataLoadingEnabled"); }catch (ClassCastException e) { } if (result == null) return optimizedDataLoadingEnabledDEFAULT; return ((Boolean)result).booleanValue(); } /** ** Returns the rendering buffer, a measure in pixels used to expand the geometry search area * enough to capture the geometries that do stay outside of the current rendering bounds but * do affect them because of their large strokes (labels and graphic symbols are handled * differently, see the label chache). *
* */ private int getRenderingBuffer() { if (rendererHints == null) return renderingBufferDEFAULT; Number result = (Number) rendererHints.get("renderingBuffer"); if (result == null) return renderingBufferDEFAULT; return result.intValue(); } /** ** Returns scale computation algorithm to be used. *
* */ private String getScaleComputationMethod() { if (rendererHints == null) return scaleComputationMethodDEFAULT; String result = (String) rendererHints.get("scaleComputationMethod"); if (result == null) return scaleComputationMethodDEFAULT; return result; } /** * Returns the text rendering method */ private String getTextRenderingMethod() { if (rendererHints == null) return textRenderingModeDEFAULT; String result = (String) rendererHints.get(TEXT_RENDERING_KEY); if (result == null) return textRenderingModeDEFAULT; return result; } /** * Returns the generalization distance in the screen space. * */ public double getGeneralizationDistance() { return generalizationDistance; } /** ** Sets the generalizazion distance in the screen space. *
** Default value is 1, meaning that two subsequent points are collapsed to * one if their on screen distance is less than one pixel *
** Set the distance to 0 if you don't want any kind of generalization *
* * @param d */ public void setGeneralizationDistance(double d) { generalizationDistance = d; } /* * (non-Javadoc) * * @see org.geotools.renderer.GTRenderer#setJava2DHints(java.awt.RenderingHints) */ public void setJava2DHints(RenderingHints hints) { this.java2dHints = hints; styleFactory.setRenderingHints(hints); } /* * (non-Javadoc) * * @see org.geotools.renderer.GTRenderer#getJava2DHints() */ public RenderingHints getJava2DHints() { return java2dHints; } public void setRendererHints(Map hints) { if( hints!=null && hints.containsKey(LABEL_CACHE_KEY) ){ LabelCache cache=(LabelCache) hints.get(LABEL_CACHE_KEY); if( cache==null ) throw new NullPointerException("Label_Cache_Hint has a null value for the labelcache"); this.labelCache=cache; this.painter=new StyledShapePainter(cache); } if(hints != null && hints.containsKey(LINE_WIDTH_OPTIMIZATION_KEY)) { styleFactory.setLineOptimizationEnabled(Boolean.TRUE.equals(hints.get(LINE_WIDTH_OPTIMIZATION_KEY))); } rendererHints = hints; } /* * (non-Javadoc) * * @see org.geotools.renderer.GTRenderer#getRendererHints() */ public Map getRendererHints() { return rendererHints; } /* * (non-Javadoc) * * @see org.geotools.renderer.GTRenderer#setContext(org.geotools.map.MapContext) */ public void setContext(MapContext context) { this.context = context; } /* * (non-Javadoc) * * @see org.geotools.renderer.GTRenderer#getContext() */ public MapContext getContext() { return context; } public boolean isCanTransform() { return canTransform; } public static MathTransform getMathTransform( CoordinateReferenceSystem sourceCRS, CoordinateReferenceSystem destCRS) { try { return CRS.findMathTransform(sourceCRS, destCRS, true); } catch (OperationNotFoundException e) { LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e); } catch (FactoryException e) { LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e); } return null; } private class RenderableFeature { Object content; private MapLayer layer; private IdentityHashMap symbolizerAssociationHT = new IdentityHashMap(); // associate a value private List geometries = new ArrayList(); private List shapes = new ArrayList(); private boolean clone; private IdentityHashMap decimators = new IdentityHashMap(); public RenderableFeature(MapLayer layer) { this.layer = layer; this.clone = !layer.getFeatureSource().getSupportedHints().contains(Hints.FEATURE_DETACHED); } public void setFeature(Object feature) { this.content = feature; geometries.clear(); shapes.clear(); } public LiteShape2 getShape(Symbolizer symbolizer, AffineTransform at) throws FactoryException { Geometry g = findGeometry(content, symbolizer); // pulls the geometry if ( g == null ) return null; SymbolizerAssociation sa = (SymbolizerAssociation) symbolizerAssociationHT .get(symbolizer); MathTransform2D transform = null; if (sa == null) { sa = new SymbolizerAssociation(); sa.setCRS(findGeometryCS(layer, content, symbolizer)); try { if (sa.crs == null || CRS.equalsIgnoreMetadata(sa.crs, destinationCrs)) transform = null; else transform = (MathTransform2D) CRS.findMathTransform(sa.crs, destinationCrs, true); if (transform != null && !transform.isIdentity()) { transform = (MathTransform2D) ConcatenatedTransform .create(transform, ProjectiveTransform .create(at)); } else { transform = (MathTransform2D) ProjectiveTransform.create(at); } } catch (Exception e) { // fall through LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e); } sa.setXform(transform); symbolizerAssociationHT.put(symbolizer, sa); } // some shapes may be too close to projection boundaries to // get transformed, try to be lenient try { if (symbolizer instanceof PointSymbolizer) { // if the coordinate transformation will occurr in place on the coordinate sequence if(!clone && g.getFactory().getCoordinateSequenceFactory() instanceof LiteCoordinateSequenceFactory) { // if the symbolizer is a point symbolizer we first get the transformed // geometry to make sure the coordinates have been modified once, and then // compute the centroid in the screen space. This is a side effect of the // fact we're modifing the geometry coordinates directly, if we don't get // the reprojected and decimated geometry we risk of transforming it twice // when computing the centroid getTransformedShape(g, sa.getXform()); return getTransformedShape(RendererUtilities.getCentroid(g), null); } else { return getTransformedShape(RendererUtilities.getCentroid(g), sa.getXform()); } } else { return getTransformedShape(g, sa.getXform()); } } catch (TransformException te) { LOGGER.log(Level.FINE, te.getLocalizedMessage(), te); fireErrorEvent(te); return null; } catch (AssertionError ae) { LOGGER.log(Level.FINE, ae.getLocalizedMessage(), ae); fireErrorEvent(new RuntimeException(ae)); return null; } } private final LiteShape2 getTransformedShape(Geometry g, MathTransform2D transform) throws TransformException, FactoryException { for (int i = 0; i < geometries.size(); i++) { if(geometries.get(i) == g) return (LiteShape2) shapes.get(i); } LiteShape2 shape = new LiteShape2(g, transform, getDecimator(transform), false, clone); geometries.add(g); shapes.add(shape); return shape; } /** * @throws org.opengis.referencing.operation.NoninvertibleTransformException */ private Decimator getDecimator(MathTransform2D mathTransform) throws org.opengis.referencing.operation.NoninvertibleTransformException { Decimator decimator = (Decimator) decimators.get(mathTransform); if (decimator == null) { if (mathTransform != null && !mathTransform.isIdentity()) decimator = new Decimator(mathTransform.inverse(), screenSize); else decimator = new Decimator(null, screenSize); decimators.put(mathTransform, decimator); } return decimator; } } }