/*
* 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.RenderingHints.Key;
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.Collections;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
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.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.filter.function.GeometryTransformationVisitor;
import org.geotools.filter.visitor.SimplifyingFilterVisitor;
import org.geotools.geometry.jts.Decimator;
import org.geotools.geometry.jts.LiteCoordinateSequence;
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.crs.ProjectionHandler;
import org.geotools.renderer.crs.ProjectionHandlerFinder;
import org.geotools.renderer.label.LabelCacheImpl;
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.PointSymbolizer;
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.FeatureType;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.PropertyName;
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.
* <ul>
* <li>The code is relatively simple to understand, so it can be used as a
* simple example of an SLD compliant rendering code</li>
* <li>Uses as little memory as possible</li>
* </ul>
* Use this class if you need a stateless renderer that provides low memory
* footprint and decent rendering performance on the first call but don't need
* good optimal performance on subsequent calls on the same data.
*
* <p>
* 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. <br>
* 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. <br>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 handler that will be called to process the geometries to deal with projections
* singularities and dateline wrapping
*/
private ProjectionHandler projectionHandler;
/** 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 = 0.8;
/** Factory that will resolve symbolizers into rendered styles */
private SLDStyleFactory styleFactory = new SLDStyleFactory();
protected LabelCache labelCache = new LabelCacheImpl();
/** The painter class we use to depict shapes onto the screen */
private StyledShapePainter painter = new StyledShapePainter(labelCache);
private IndexedFeatureResults indexedFeatureResults;
private List<RenderListener> renderListeners = new CopyOnWriteArrayList<RenderListener>();
private RenderingHints java2dHints;
private boolean optimizedDataLoadingEnabledDEFAULT = true;
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.
* <p>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<p>
* 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.
* <p>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).</p>
* <p>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</p>
*/
public static final String OPTIMIZE_FTS_RENDERING_KEY = "optimizeFTSRendering";
/**
* Enables advanced reprojection handling. Geometries will be sliced to fit into the
* area of definition of the rendering projection, and for projections that can wrap
* the world in a continuous way (e.g., Mercator) a Google Maps like effect will be
* generated (continuous horizontal map).
*/
public static final String ADVANCED_PROJECTION_HANDLING_KEY = "advancedProjectionHandling";
/**
* Boolean flag indicating whether vector rendering should be preferred when
* painting graphic fills. See {@link SLDStyleFactory#isVectorRenderingEnabled()}
* for more details.
*/
public static final String VECTOR_RENDERING_KEY = "vectorRenderingEnabled";
private static boolean VECTOR_RENDERING_ENABLED_DEFAULT = false;
public static final String LABEL_CACHE_KEY = "labelCache";
public static final String DPI_KEY = "dpi";
public static final String DECLARED_SCALE_DENOM_KEY = "declaredScaleDenominator";
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)
* "vectorRenderingEnabled" - Boolean yes/no (see default vectorRenderingEnabledDEFAULT)
* "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;
/**
* Whether the renderer must perform generalization for the current set of features.
* For each layer we will set this flag depending on whether the datastore can do full
* generalization for us, or not
*/
private boolean inMemoryGeneralization = true;
/**
* 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;
}
if (renderListeners.size() > 0) {
RenderListener listener;
for (int i = 0; i < renderListeners.size(); i++) {
listener = renderListeners.get(i);
listener.featureRenderer((SimpleFeature) feature);
}
}
}
private void fireErrorEvent(Exception e) {
if (renderListeners.size() > 0) {
RenderListener listener;
for (int i = 0; i < renderListeners.size(); i++) {
listener = renderListeners.get(i);
listener.errorOccurred(e);
}
}
}
/**
* If you call this method from another thread than the one that called
* <code>paint</code> or <code>render</code> 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 <code>setContext</code>. <p/> 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 <code>setContext</code>. <p/> 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 <code>setContext</code>. <p/> 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 <code>setContext</code>. <p/> This version of
* the method assumes that paint area, envelope and worldToScreen transform
* are already computed. Use this method to avoid recomputation. <b>Note
* however that no check is performed that they are really in sync!<b/>
*
* @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,
AffineTransform worldToScreen, 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);
}
}
if (XAffineTransform.getRotation(worldToScreen) != 0.0) {
return RendererUtilities.calculateOGCScaleAffine(envelope.getCoordinateReferenceSystem(),
worldToScreen, hints);
}
return RendererUtilities.calculateOGCScale(envelope, paintArea.width, hints);
}
/**
* Renders features based on the map layers and their styles as specified in
* the map context using <code>setContext</code>. <p/> This version of
* the method assumes that paint area, envelope and worldToScreen transform
* are already computed. Use this method to avoid recomputation. <b>Note
* however that no check is performed that they are really in sync!<b/>
*
* @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);
}
if(isAdvancedProjectionHandlingEnabled())
projectionHandler = ProjectionHandlerFinder.getHandler(mapExtent);
// compute scale according to the user specified method
scaleDenominator = computeScale(mapArea, paintArea,worldToScreenTransform, rendererHints);
if(LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Computed scale denominator: " + scaleDenominator);
//////////////////////////////////////////////////////////////////////
//
// 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 LabelCacheImpl) {
boolean outlineEnabled = TEXT_RENDERING_OUTLINE.equals(getTextRenderingMethod());
((LabelCacheImpl) 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 <code>buffer</code> 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 <code>Source</code> instance to be rendered.
* <p>
* <em><strong>Note: This is proof-of-concept quality only!</strong> 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.</em>
* </p>
*
* @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.
* <p>
* If <code>optimizedDataLoadingEnabled</code> attribute has been set to
* <code>true</code>, the following optimization will be performed in
* order to limit the number of features returned:
* <ul>
* <li>Just the features whose geometric attributes lies within
* <code>envelope</code> will be queried</li>
* <li>The queried attributes will be limited to just those needed to
* perform the rendering, based on the required geometric and non geometric
* attributes found in the Layer's style rules</li>
* <li>If a <code>Query</code> 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</li>
* <li>At least that the layer's definition query explicitly says to
* retrieve some attribute, no attributes will be requested from it, for
* performance reasons. So it is desirable to not use a Query for filtering
* a layer which includes attributes. Note that including the attributes in
* the result is not necessary for the query's filter to get properly
* processed. </li>
* </ul>
* </p>
* <p>
* <b>NOTE </b>: This is an internal method and should only be called by
* <code>paint(Graphics2D, Rectangle, AffineTransform)</code>. It is
* package protected just to allow unit testing it.
* </p>
*
* @param currLayer
* the actually processing layer for renderition
* @param schema
* @param source
* @param envelope
* the spatial extent which is the target area of the rendering
* process
* @param destinationCrs
* DOCUMENT ME!
* @param sourceCrs
* @param screenSize
* @param geometryAttribute
* @return the set of features resulting from <code>currLayer</code> 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
*/
FeatureCollection<SimpleFeatureType, SimpleFeature> queryLayer(MapLayer currLayer, FeatureSource<SimpleFeatureType, SimpleFeature> source,
SimpleFeatureType schema, LiteFeatureTypeStyle[] styles,
Envelope mapArea, CoordinateReferenceSystem mapCRS,
CoordinateReferenceSystem featCrs, Rectangle screenSize,
GeometryDescriptor geometryAttribute,
AffineTransform worldToScreenTransform)
throws IllegalFilterException, IOException,
IllegalAttributeException {
FeatureCollection<SimpleFeatureType, SimpleFeature> results = null;
DefaultQuery query = new DefaultQuery(DefaultQuery.ALL);
Query definitionQuery;
final int length;
Filter filter = null;
// if map extent are not already expanded by a constant buffer, try to compute a layer
// specific one based on stroke widths
if(getRenderingBuffer() == 0) {
int buffer = findRenderingBuffer(styles);
if (buffer > 0) {
mapArea = expandEnvelope(mapArea, worldToScreenTransform,
buffer);
LOGGER.fine("Expanding rendering area by " + buffer
+ " pixels to consider stroke width");
}
}
// build a list of attributes used in the rendering
String[] attributes;
if (styles == null) {
List<AttributeDescriptor> ats = schema.getAttributeDescriptors();
length = ats.size();
attributes = new String[length];
for (int t = 0; t < length; t++) {
attributes[t] = ats.get(t).getLocalName();
}
} else {
attributes = findStyleAttributes(styles, schema);
}
ReferencedEnvelope envelope = new ReferencedEnvelope(mapArea, mapCRS);
if (isOptimizedDataLoadingEnabled()) {
// see what attributes we really need by exploring the styles
// for testing purposes we have a null case -->
try {
// Then create the geometry filters. We have to create one for
// each geometric attribute used during the rendering as the
// feature may have more than one and the styles could use non
// default geometric ones
List<ReferencedEnvelope> envelopes;
if (projectionHandler != null) {
envelopes = projectionHandler.getQueryEnvelopes(featCrs);
} else {
if (mapCRS != null && featCrs != null && !CRS.equalsIgnoreMetadata(featCrs, mapCRS)) {
envelopes = Collections.singletonList(envelope.transform(featCrs, true, 10));
} else {
envelopes = Collections.singletonList(envelope);
}
}
if(LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Querying layer " + schema.getTypeName() + " with bbox: " + envelope);
filter = createBBoxFilters(schema, attributes, envelopes);
// now build the query using only the attributes and the
// bounding box needed
query = new DefaultQuery(schema.getTypeName());
query.setFilter(filter);
query.setPropertyNames(attributes);
processRuleForQuery(styles, query);
} catch (Exception e) {
fireErrorEvent(new Exception("Error transforming bbox", e));
canTransform = false;
query = new DefaultQuery(schema.getTypeName());
query.setPropertyNames(attributes);
Envelope bounds = source.getBounds();
if (bounds != null && envelope.intersects(bounds)) {
LOGGER.log(Level.WARNING, "Got a tranform exception while trying to de-project the current " +
"envelope, bboxs intersect therefore using envelope)", e);
filter = null;
filter = createBBoxFilters(schema, attributes, Collections.singletonList(envelope));
query.setFilter(filter);
} else {
LOGGER.log(Level.WARNING, "Got a tranform exception while trying to de-project the current " +
"envelope, falling back on full data loading (no bbox query)", e);
query.setFilter(Filter.INCLUDE);
}
processRuleForQuery(styles, query);
}
}
// now, if a definition query has been established for this layer, be
// sure to respect it by combining it with the bounding box one.
// Currently this definition query is being set dynamically in geoserver
// as per the user's filter, maxFeatures and startIndex WMS GetMap custom parameters
definitionQuery = currLayer.getQuery();
if (definitionQuery != Query.ALL) {
if (query == Query.ALL) {
query = new DefaultQuery(definitionQuery);
} else {
query = new DefaultQuery(DataUtilities.mixQueries(
definitionQuery, query, "liteRenderer"));
}
}
query.setCoordinateSystem(featCrs);
// prepare hints
// ... basic one, we want fast and compact coordinate sequences
Hints hints = new Hints(Hints.JTS_COORDINATE_SEQUENCE_FACTORY, new LiteCoordinateSequenceFactory());
// ... if possible we let the datastore do the generalization
Set<RenderingHints.Key> fsHints = source.getSupportedHints();
if(fsHints.contains(Hints.GEOMETRY_DISTANCE) || fsHints.contains(Hints.GEOMETRY_SIMPLIFICATION)) {
CoordinateReferenceSystem crs = getNativeCRS(schema, Arrays.asList(attributes));
if(crs != null) {
try {
MathTransform mt = buildFullTransform(crs, mapCRS, worldToScreenTransform);
double[] spans = Decimator.computeGeneralizationDistances(mt.inverse(), screenSize, generalizationDistance);
double distance = spans[0] < spans[1] ? spans[0] : spans[1];
if(fsHints.contains(Hints.GEOMETRY_SIMPLIFICATION)) {
// good, we don't need to perform in memory generalization, the datastore
// does it all for us
hints.put(Hints.GEOMETRY_SIMPLIFICATION, distance);
inMemoryGeneralization = false;
} else if(fsHints.contains(Hints.GEOMETRY_DISTANCE)) {
// in this case the datastore can get us close, but we can still
// perform some in memory generalization
hints.put(Hints.GEOMETRY_DISTANCE, distance);
}
} catch(Exception e) {
LOGGER.log(Level.INFO, "Error computing the generalization hints", e);
}
}
}
query.setHints(hints);
// simplify the filter
SimplifyingFilterVisitor simplifier = new SimplifyingFilterVisitor();
Filter simplifiedFilter = (Filter) query.getFilter().accept(simplifier, null);
query.setFilter(simplifiedFilter);
return source.getFeatures(query);
}
/**
* Takes care of eventual geometric transformations
* @param styles
* @param envelope
* @return
*/
ReferencedEnvelope expandEnvelopeByTransformations(LiteFeatureTypeStyle[] styles,
ReferencedEnvelope envelope) {
GeometryTransformationVisitor visitor = new GeometryTransformationVisitor();
ReferencedEnvelope result = new ReferencedEnvelope(envelope);
for (LiteFeatureTypeStyle lts : styles) {
List<Rule> rules = new ArrayList<Rule>();
rules.addAll(Arrays.asList(lts.ruleList));
rules.addAll(Arrays.asList(lts.elseRules));
for (Rule r : rules) {
for (Symbolizer s : r.symbolizers()) {
if(s.getGeometry() != null)
result.expandToInclude((ReferencedEnvelope) s.getGeometry().accept(visitor, envelope));
}
}
}
return result;
}
/**
* Builds a full transform going from the source CRS to the denstionan CRS
* and from there to the screen
*/
private MathTransform2D buildFullTransform(CoordinateReferenceSystem sourceCRS,
CoordinateReferenceSystem destCRS, AffineTransform worldToScreenTransform)
throws FactoryException {
MathTransform2D mt = buildTransform(sourceCRS, destCRS);
// concatenate from world to screen
if (mt != null && !mt.isIdentity()) {
mt = (MathTransform2D) ConcatenatedTransform
.create(mt, ProjectiveTransform.create(worldToScreenTransform));
} else {
mt = (MathTransform2D) ProjectiveTransform.create(worldToScreenTransform);
}
return mt;
}
/**
* Builds the transform from sourceCRS to destCRS
* @param sourceCRS
* @param destCRS
* @return the transform, or null if any of the crs is null, or if the the two crs are equal
* @throws FactoryException
*/
private MathTransform2D buildTransform(CoordinateReferenceSystem sourceCRS,
CoordinateReferenceSystem destCRS) throws FactoryException {
// the basic crs transformation, if any
MathTransform2D mt;
if (sourceCRS == null || destCRS == null || CRS.equalsIgnoreMetadata(sourceCRS,
destCRS))
mt = null;
else
mt = (MathTransform2D) CRS.findMathTransform(sourceCRS, destCRS, true);
return mt;
}
/**
* Scans the schema for the specified attributes are returns a single CRS
* if all the geometric attributes in the lot share one CRS, null if
* there are different ones
* @param schema
* @return
*/
private CoordinateReferenceSystem getNativeCRS(SimpleFeatureType schema, List<String> attNames) {
// first off, check how many crs we have, this hint works only
// if we have just one native CRS at hand (and the native CRS is known
CoordinateReferenceSystem crs = null;
for (AttributeDescriptor att : schema.getAttributeDescriptors()) {
if(!attNames.contains(att.getLocalName()))
continue;
if(att instanceof GeometryDescriptor) {
GeometryDescriptor gd = (GeometryDescriptor) att;
CoordinateReferenceSystem gdCrs = gd.getCoordinateReferenceSystem();
if(crs == null) {
crs = gdCrs;
} else if(gdCrs == null) {
crs = null;
break;
} else if(!CRS.equalsIgnoreMetadata(crs, gdCrs)) {
crs = null;
break;
}
}
}
return crs;
}
/**
* JE: If there is a single rule "and" its filter together with the query's
* filter and send it off to datastore. This will allow as more processing
* to be done on the back end... Very useful if DataStore is a database.
* Problem is that worst case each filter is ran twice. Next we will modify
* it to find a "Common" filter between all rules and send that to the
* datastore.
*
* DJB: trying to be smarter. If there are no "elseRules" and no rules w/o a
* filter, then it makes sense to send them off to the Datastore We limit
* the number of Filters sent off to the datastore, just because it could
* get a bit rediculous. In general, for a database, if you can limit 10% of
* the rows being returned you're probably doing quite well. The main
* problem is when your filters really mean you're secretly asking for all
* the data in which case sending the filters to the Datastore actually
* costs you. But, databases are *much* faster at processing the Filters
* than JAVA is and can use statistical analysis to do it.
*
* @param styles
* @param q
*/
private void processRuleForQuery(LiteFeatureTypeStyle[] styles,
DefaultQuery q) {
try {
// first we check to see if there are >
// "getMaxFiltersToSendToDatastore" rules
// if so, then we dont do anything since no matter what there's too
// many to send down.
// next we check for any else rules. If we find any --> dont send
// anything to Datastore
// next we check for rules w/o filters. If we find any --> dont send
// anything to Datastore
//
// otherwise, we're gold and can "or" together all the filters then
// AND it with the original filter.
// ie. SELECT * FROM ... WHERE (the_geom && BBOX) AND (filter1 OR
// filter2 OR filter3);
final int maxFilters = getMaxFiltersToSendToDatastore();
final List<Filter> filtersToDS = new ArrayList<Filter>();
// look at each featuretypestyle
for(LiteFeatureTypeStyle style : styles) {
if (style.elseRules.length > 0) // uh-oh has elseRule
return;
// look at each rule in the featuretypestyle
for(Rule r : style.ruleList) {
if (r.getFilter() == null)
return; // uh-oh has no filter (want all rows)
filtersToDS.add(r.getFilter());
}
}
// if too many bail out
if (filtersToDS.size() > maxFilters)
return;
// or together all the filters
org.opengis.filter.Filter ruleFiltersCombined;
if (filtersToDS.size() == 1) {
ruleFiltersCombined = (Filter) filtersToDS.get(0);
} else {
ruleFiltersCombined = filterFactory.or(filtersToDS);
}
// combine with the pre-existing filter
ruleFiltersCombined = filterFactory.and(
q.getFilter(), ruleFiltersCombined);
q.setFilter(ruleFiltersCombined);
} catch (Exception e) {
if (LOGGER.isLoggable(Level.WARNING))
LOGGER.log(Level.SEVERE,
"Could not send rules to datastore due to: "
+ e.getLocalizedMessage(), e);
}
}
/**
* find out the maximum number of filters we're going to send off to the
* datastore. See processRuleForQuery() for details.
*
*/
private int getMaxFiltersToSendToDatastore() {
try {
Integer result = (Integer) rendererHints
.get("maxFiltersToSendToDatastore");
if (result == null)
return defaultMaxFiltersToSendToDatastore; // default if not
// present in hints
return result.intValue();
} catch (Exception e) {
return defaultMaxFiltersToSendToDatastore;
}
}
/**
* Checks if optimized feature type style rendering is enabled, or not.
* See {@link #OPTIMIZE_FTS_RENDERING_KEY} description for a full explanation.
*/
private boolean isOptimizedFTSRenderingEnabled() {
if (rendererHints == null)
return true;
Object result = rendererHints.get(OPTIMIZE_FTS_RENDERING_KEY);
if (result == null)
return true;
return Boolean.TRUE.equals(result);
}
/**
* Checks if the advanced projection handling is enabled
* @return
*/
private boolean isAdvancedProjectionHandlingEnabled() {
if (rendererHints == null)
return false;
Object result = rendererHints.get(ADVANCED_PROJECTION_HANDLING_KEY);
if (result == null)
return false;
return Boolean.TRUE.equals(result);
}
/**
* Checks if vector rendering is enabled or not.
* See {@link SLDStyleFactory#isVectorRenderingEnabled()} for a full explanation.
*/
private boolean isVectorRenderingEnabled() {
if (rendererHints == null)
return true;
Object result = rendererHints.get(VECTOR_RENDERING_KEY);
if (result == null)
return VECTOR_RENDERING_ENABLED_DEFAULT;
return ((Boolean)result).booleanValue();
}
/**
* Returns an estimate of the rendering buffer needed to properly display this
* layer taking into consideration the constant stroke sizes in the feature type
* styles.
*
* @param styles
* the feature type styles to be applied to the layer
* @return an estimate of the buffer that should be used to properly display a layer
* rendered with the specified styles
*/
private int findRenderingBuffer(LiteFeatureTypeStyle[] styles) {
final MetaBufferEstimator rbe = new MetaBufferEstimator();
for (int t = 0; t < styles.length; t++) {
final LiteFeatureTypeStyle lfts = styles[t];
Rule[] rules = lfts.elseRules;
for (int j = 0; j < rules.length; j++) {
rbe.visit(rules[j]);
}
rules = lfts.ruleList;
for (int j = 0; j < rules.length; j++) {
rbe.visit(rules[j]);
}
}
if(!rbe.isEstimateAccurate())
LOGGER.warning("Assuming rendering buffer = " + rbe.getBuffer()
+ ", but estimation is not accurate, you may want to set a buffer manually");
return rbe.getBuffer();
}
/**
* Inspects the <code>MapLayer</code>'s style and retrieves it's needed
* attribute names, returning at least the default geometry attribute name.
*
* @param layer
* the <code>MapLayer</code> to determine the needed attributes
* from
* @param schema
* the <code>layer</code>'s FeatureSource<SimpleFeatureType, SimpleFeature> schema
* @return the minimum set of attribute names needed to render
* <code>layer</code>
*/
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));
List<AttributeDescriptor> attTypes = schema.getAttributeDescriptors();
String attName;
final int attTypesLength = attTypes.size();
for (int i = 0; i < attTypesLength; i++) {
attName = attTypes.get(i).getLocalName();
// DJB: This geometry check was commented out. I think it should
// actually be back in or
// you get ALL the attributes back, which isn't what you want.
// ALX: For rasters I need even the "grid" attribute.
// DJB:geos-469, we do not grab all the geometry columns.
// for symbolizers, if a geometry is required it is either
// explicitly named
// ("<Geometry><PropertyName>the_geom</PropertyName></Geometry>")
// or the default geometry is assumed (no <Geometry> element).
// I've modified the style attribute extractor so it tracks if the
// default geometry is used. So, we no longer add EVERY geometry
// column to the query!!
if ((attName.equalsIgnoreCase("grid"))
&& !atts.contains(attName)||
(attName.equalsIgnoreCase("params"))
&& !atts.contains(attName)) {
atts.add(attName);
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("added attribute " + attName);
}
}
try {
// DJB:geos-469 if the default geometry was used in the style, we
// need to grab it.
if (sae.getDefaultGeometryUsed()
&& (!atts.contains(schema.getGeometryDescriptor().getLocalName()))) {
atts.add(schema.getGeometryDescriptor().getLocalName());
}
} catch (Exception e) {
// might not be a geometry column. That will cause problems down the
// road (why render a non-geometry layer)
}
ftsAttributes = new String[atts.size()];
atts.toArray(ftsAttributes);
return ftsAttributes;
}
/**
* Creates the bounding box filters (one for each geometric attribute)
* needed to query a <code>MapLayer</code>'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 <code>attributes</code>. If there are just one geometric
* attribute, just returns its corresponding
* <code>GeometryFilter</code>.
* @throws IllegalFilterException
* if something goes wrong creating the filter
*/
private Filter createBBoxFilters(SimpleFeatureType schema, String[] attributes,
List<ReferencedEnvelope> bboxes) throws IllegalFilterException {
Filter filter = Filter.INCLUDE;
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) {
Filter gfilter = new FastBBOX(attType.getLocalName(), bboxes.get(0));
if (filter == Filter.INCLUDE) {
filter = gfilter;
} else {
filter = filterFactory.or( filter, gfilter );
}
if(bboxes.size() > 0) {
for (int k = 1; k < bboxes.size(); k++) {
filter = filterFactory.or( filter, new FastBBOX(attType.getLocalName(), bboxes.get(k)) );
}
}
}
}
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);
}
/**
* <p>Creates a list of <code>LiteFeatureTypeStyle</code>s with:
* <ol type="a">
* <li>out-of-scale rules removed</li>
* <li>incompatible FeatureTypeStyles removed</li>
* </ol>
* </p>
*
* <p><em><strong>Note:</strong> This method has a lot of duplication with
* {@link #createLiteFeatureTypeStyles(FeatureTypeStyle[], SimpleFeatureType, Graphics2D)}.
* </em></p>
*
* @param featureStyles Styles to process
* @param typeDescription The type description that has to be matched
* @return ArrayList<LiteFeatureTypeStyle>
*/
//TODO: Merge the two createLiteFeatureTypeStyles() methods
private ArrayList createLiteFeatureTypeStyles(FeatureTypeStyle[] featureStyles, Object typeDescription, Graphics2D graphics) throws IOException {
ArrayList result = new ArrayList();
Rule[] rules;
ArrayList ruleList = new ArrayList();
ArrayList elseRuleList = new ArrayList();
Rule r;
LiteFeatureTypeStyle lfts;
BufferedImage image;
int numOfRules;
int itemNumber = 0;
final int length = featureStyles.length;
for (int i = 0; i < length; i++) {
FeatureTypeStyle fts = featureStyles[i];
if ( typeDescription == null || typeDescription.toString().indexOf( fts.getFeatureTypeName() ) == -1 ) continue;
// get applicable rules at the current scale
rules = fts.getRules();
ruleList = new ArrayList();
elseRuleList = new ArrayList();
numOfRules = rules.length;
for (int j = 0; j < numOfRules; j++) {
// getting rule
r = rules[j];
if (isWithInScale(r)) {
if (r.hasElseFilter()) {
elseRuleList.add(r);
} else {
ruleList.add(r);
}
}
}
if ((ruleList.size() == 0) && (elseRuleList.size() == 0))
continue; // DJB: optimization - nothing to render, dont
// do anything!!
if (itemNumber == 0 || !isOptimizedFTSRenderingEnabled()) // we can optimize this one!
{
lfts = new LiteFeatureTypeStyle(graphics, ruleList,
elseRuleList);
} else {
image = graphics
.getDeviceConfiguration()
.createCompatibleImage(screenSize.width,
screenSize.height, Transparency.TRANSLUCENT);
lfts = new LiteFeatureTypeStyle(image, graphics
.getTransform(), ruleList, elseRuleList,
java2dHints);
}
result.add(lfts);
itemNumber++;
}
return result;
}
/**
* creates a list of LiteFeatureTypeStyles a) out-of-scale rules removed b)
* incompatible FeatureTypeStyles removed
*
*
* @param featureStylers
* @param features
* @throws Exception
* @return ArrayList<LiteFeatureTypeStyle>
*/
private ArrayList createLiteFeatureTypeStyles(
FeatureTypeStyle[] featureStyles, SimpleFeatureType ftype,
Graphics2D graphics) throws IOException {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("creating rules for scale denominator - "
+ NumberFormat.getNumberInstance().format(scaleDenominator));
ArrayList result = new ArrayList();
int itemNumber = 0;
LiteFeatureTypeStyle lfts;
for (FeatureTypeStyle fts : featureStyles) {
if (isFeatureTypeStyleActive(ftype, fts)) {
// DJB: this FTS is compatible with this FT.
// get applicable rules at the current scale
List[] splittedRules = splitRules(fts);
List ruleList = splittedRules[0];
List elseRuleList = splittedRules[1];
// if none, skip it
if ((ruleList.size() == 0) && (elseRuleList.size() == 0))
continue;
if (itemNumber == 0 || !isOptimizedFTSRenderingEnabled()) // we can optimize this one!
{
lfts = new LiteFeatureTypeStyle(graphics, ruleList,
elseRuleList);
} else {
BufferedImage image = graphics
.getDeviceConfiguration()
.createCompatibleImage(screenSize.width,
screenSize.height, Transparency.TRANSLUCENT);
lfts = new LiteFeatureTypeStyle(image, graphics
.getTransform(), ruleList, elseRuleList,
java2dHints);
}
result.add(lfts);
itemNumber++;
}
}
return result;
}
private boolean isFeatureTypeStyleActive(SimpleFeatureType ftype, FeatureTypeStyle fts) {
return ((ftype.getTypeName() != null)
&& (ftype.getTypeName().equalsIgnoreCase(fts.getFeatureTypeName()) ||
FeatureTypes.isDecendedFrom(ftype, null, fts.getFeatureTypeName())));
}
private List[] splitRules(FeatureTypeStyle fts) {
Rule[] rules;
List<Rule> ruleList = new ArrayList<Rule>();
List<Rule> elseRuleList = new ArrayList<Rule>();
rules = fts.getRules();
ruleList = new ArrayList();
elseRuleList = new ArrayList();
for (int j = 0; j < rules.length; j++) {
// getting rule
Rule r = rules[j];
if (isWithInScale(r)) {
if (r.hasElseFilter()) {
elseRuleList.add(r);
} else {
ruleList.add(r);
}
}
}
return new List[] {ruleList, elseRuleList};
}
/**
* When drawing in optimized mode a 32bit surface is created for each FeatureTypeStyle
* other than the first in order to draw features in parallel while respecting the
* feature draw ordering multiple FTS impose. This method allows to estimate how many
* megabytes will be needed, in terms of back buffers, to draw the current {@link MapContext},
* assuming the feature type style optimizations are turned on (in the case they are off,
* no extra memory will be used).
* @param width the image width
* @param height the image height
*/
public int getMaxBackBufferMemory(int width, int height) {
int maxBuffers = 0;
for (MapLayer layer : context.getLayers()) {
if (!layer.isVisible()) {
// Only render layer when layer is visible
continue;
}
// skip layers that do have only one fts
if(layer.getStyle().getFeatureTypeStyles().length < 2)
continue;
// count how many lite feature type styles are active
int currCount = 0;
SimpleFeatureType ftype = (SimpleFeatureType) layer.getFeatureSource().getSchema();
for (FeatureTypeStyle fts : layer.getStyle().getFeatureTypeStyles()) {
if (isFeatureTypeStyleActive(ftype, fts)) {
// get applicable rules at the current scale
List[] splittedRules = splitRules(fts);
List ruleList = splittedRules[0];
List elseRuleList = splittedRules[1];
// if none, skip this fts
if ((ruleList.size() == 0) && (elseRuleList.size() == 0))
continue;
currCount++;
}
}
// consider the first fts does not allocate a buffer
currCount--;
if(currCount > maxBuffers)
maxBuffers = currCount;
}
return maxBuffers * width * height * 4;
}
/**
* Prepair a FeatureCollection<SimpleFeatureType, SimpleFeature> for display, this method formally ensured that a FeatureReader
* produced the correct CRS and has now been updated to work with FeatureCollection.
* <p>
* What is really going on is the need to set up for reprojection; but *after* decimation has
* occured.
* </p>
*
* @param features
* @param sourceCrs
* @return FeatureCollection<SimpleFeatureType, SimpleFeature> that produces results with the correct CRS
*/
private FeatureCollection<SimpleFeatureType, SimpleFeature> prepFeatureCollection( FeatureCollection<SimpleFeatureType, SimpleFeature> features, CoordinateReferenceSystem sourceCrs ) {
// DJB: dont do reprojection here - do it after decimation
// but we ensure that the reader is producing geometries with
// the correct CRS
// NOTE: it, by default, produces ones that are are tagged with
// the CRS of the datastore, which
// maybe incorrect.
// The correct value is in sourceCrs.
// this is the reader's CRS
CoordinateReferenceSystem rCS = null;
try {
rCS = features.getSchema().getGeometryDescriptor().getType().getCoordinateReferenceSystem();
} catch(NullPointerException e) {
// life sucks sometimes
}
// sourceCrs == source's real SRS
// if we need to recode the incoming geometries
if (rCS != sourceCrs) // not both null or both EXACTLY the
// same CRS object
{
if (sourceCrs != null) // dont re-tag to null, keep the
// DataStore's CRS (this shouldnt
// really happen)
{
// if the datastore is producing null CRS, we recode.
// if the datastore's CRS != real CRS, then we recode
if ((rCS == null) || !CRS.equalsIgnoreMetadata(rCS, sourceCrs)) {
// need to retag the features
try {
return new ForceCoordinateSystemFeatureResults( features, sourceCrs );
} catch (Exception ee) {
LOGGER.log(Level.WARNING, ee.getLocalizedMessage(), ee);
}
}
}
}
return features;
}
/**
* Applies each feature type styler in turn to all of the features. This
* perhaps needs some explanation to make it absolutely clear.
* featureStylers[0] is applied to all features before featureStylers[1] is
* applied. This can have important consequences as regards the painting
* order.
* <p>
* 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.
* </p>
* <p>
* This method is internal and should only be called by render.
* </p>
* <p>
* </p>
*
* @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<SimpleFeatureType, SimpleFeature> featureSource = (FeatureSource<SimpleFeatureType, SimpleFeature>) currLayer.getFeatureSource();
Collection collection = null;
FeatureCollection features = null;
final CoordinateReferenceSystem sourceCrs;
final NumberRange scaleRange = new NumberRange(scaleDenominator,scaleDenominator);
final ArrayList lfts ;
if ( featureSource != null ) {
final SimpleFeatureType schema = featureSource.getSchema();
final GeometryDescriptor geometryAttribute = schema.getGeometryDescriptor();
sourceCrs = geometryAttribute.getType().getCoordinateReferenceSystem();
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine(new StringBuffer("processing ").append(
featureStylers.length).append(" stylers for ").append(
currLayer.getFeatureSource().getSchema().getName())
.toString());
}
// transformMap = new HashMap();
lfts = createLiteFeatureTypeStyles(featureStylers,schema, graphics);
if(lfts.size() == 0)
return;
LiteFeatureTypeStyle[] featureTypeStyleArray = (LiteFeatureTypeStyle[]) lfts.toArray(new LiteFeatureTypeStyle[lfts.size()]);
// /////////////////////////////////////////////////////////////////////
//
// DJB: get a featureresults (so you can get a feature reader) for the
// data
//
// /////////////////////////////////////////////////////////////////////
// ... assume we have to do the generalization, the query layer process will
// turn down the flag if we don't
inMemoryGeneralization = true;
features = queryLayer(currLayer, featureSource, schema,
featureTypeStyleArray,
mapArea, destinationCrs, sourceCrs, screenSize,
geometryAttribute, at);
features = prepFeatureCollection( features, sourceCrs );
} else {
CollectionSource source = currLayer.getSource();
collection = queryLayer( currLayer, currLayer.getSource() );
sourceCrs = null;
lfts = createLiteFeatureTypeStyles( featureStylers, source.describe(), graphics );
}
if (lfts.size() == 0) return; // nothing to do
if(isOptimizedFTSRenderingEnabled())
drawOptimized(graphics, currLayer, at, destinationCrs, layerId, collection, features,
scaleRange, lfts);
else
drawPlain(graphics, currLayer, at, destinationCrs, layerId, collection, features,
scaleRange, lfts);
}
/**
* Performs all rendering on the user provided graphics object by scanning
* the collection multiple times, one for each feature type style provided
*/
private void drawPlain(final Graphics2D graphics, MapLayer currLayer, AffineTransform at,
CoordinateReferenceSystem destinationCrs, String layerId, Collection collection,
FeatureCollection features, final NumberRange scaleRange, final ArrayList lfts) {
final LiteFeatureTypeStyle[] fts_array = (LiteFeatureTypeStyle[]) lfts
.toArray(new LiteFeatureTypeStyle[lfts.size()]);
// for each lite feature type style, scan the whole collection and draw
for (LiteFeatureTypeStyle liteFeatureTypeStyle : fts_array) {
Iterator iterator = null;
if (collection != null)
iterator = collection.iterator();
if (features != null)
iterator = features.iterator();
if (iterator == null)
return; // nothing to do
try {
boolean clone = isCloningRequired(currLayer, fts_array);
RenderableFeature rf = new RenderableFeature(currLayer, clone);
// loop exit condition tested inside try catch
// make sure we test hasNext() outside of the try/cath that follows, as that
// one is there to make sure a single feature error does not ruin the rendering
// (best effort) whilst an exception in hasNext() + ignoring catch results in
// an infinite loop
while (iterator.hasNext() && !renderingStopRequested) {
try {
rf.setFeature(iterator.next());
process(rf, liteFeatureTypeStyle, scaleRange, at, destinationCrs, layerId);
} catch (Throwable tr) {
LOGGER.log(Level.SEVERE, tr.getLocalizedMessage(), tr);
fireErrorEvent(new Exception("Error rendering feature", tr));
}
}
} finally {
if (collection instanceof FeatureCollection) {
FeatureCollection resource = (FeatureCollection) collection;
resource.close(iterator);
} else if (features != null) {
features.close(iterator);
}
}
}
}
/**
* Performs rendering so that the collection is scanned only once even in presence
* of multiple feature type styles, using the in memory buffer for each feature type
* style other than the first one (that uses the graphics provided by the user)s
*/
private void drawOptimized(final Graphics2D graphics, MapLayer currLayer, AffineTransform at,
CoordinateReferenceSystem destinationCrs, String layerId, Collection collection,
FeatureCollection features, final NumberRange scaleRange, final ArrayList lfts) {
Iterator iterator = null;
if( collection != null ) iterator = collection.iterator();
if( features != null ) iterator = features.iterator();
if( iterator == null ) return; // nothing to do
final LiteFeatureTypeStyle[] fts_array = (LiteFeatureTypeStyle[]) lfts
.toArray(new LiteFeatureTypeStyle[lfts.size()]);
try {
boolean clone = isCloningRequired(currLayer, fts_array);
RenderableFeature rf = new RenderableFeature(currLayer, clone);
// loop exit condition tested inside try catch
// make sure we test hasNext() outside of the try/cath that follows, as that
// one is there to make sure a single feature error does not ruin the rendering
// (best effort) whilst an exception in hasNext() + ignoring catch results in
// an infinite loop
while (iterator.hasNext() && !renderingStopRequested) {
try {
rf.setFeature(iterator.next());
// draw the feature on the main graphics and on the eventual extra image buffers
for (LiteFeatureTypeStyle liteFeatureTypeStyle : fts_array) {
process(rf, liteFeatureTypeStyle, scaleRange, at, destinationCrs, layerId);
}
} catch (Throwable tr) {
LOGGER.log(Level.SEVERE, tr.getLocalizedMessage(), tr);
fireErrorEvent(new Exception("Error rendering feature", tr));
}
}
} finally {
if( collection instanceof FeatureCollection ){
FeatureCollection resource = (FeatureCollection ) collection;
resource.close( iterator );
} else if(features != null) {
features.close( iterator );
}
}
// have to re-form the image now.
// graphics.setTransform( new AffineTransform() );
graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
for (int t = 0; t < fts_array.length; t++) {
// first fts won't have an image, it's using the user provided graphics
// straight, so we don't need to compose it back in.
if (fts_array[t].myImage != null)
{
graphics.drawImage(fts_array[t].myImage, 0, 0, null);
fts_array[t].myImage.flush();
fts_array[t].graphics.dispose();
}
}
}
/**
* Tells if geometry cloning is required or not
*/
private boolean isCloningRequired(MapLayer layer, LiteFeatureTypeStyle[] lfts) {
// check if the features are detached, we can thus modify the geometries in place
final Set<Key> hints = layer.getFeatureSource().getSupportedHints();
if(!hints.contains(Hints.FEATURE_DETACHED))
return true;
// check if there is any conflicting geometry transformation.
// No geometry transformations -> we can modify geometries in place
// Just one geometry transformation over an attribute -> we can modify geometries in place
// Two tx over the same attribute, or straight usage and a tx -> we have to preserve the
// original geometry as well, thus we need cloning
StyleAttributeExtractor extractor = new StyleAttributeExtractor();
FeatureType featureType = layer.getFeatureSource().getSchema();
Set<String> plainGeometries = new java.util.HashSet<String>();
Set<String> txGeometries = new java.util.HashSet<String>();
for (LiteFeatureTypeStyle lft : lfts) {
for(Rule r: lft.ruleList) {
for(Symbolizer s: r.symbolizers()) {
if(s.getGeometry() == null) {
String attribute = featureType.getGeometryDescriptor().getName().getLocalPart();
if(txGeometries.contains(attribute))
return true;
plainGeometries.add(attribute);
} else if(s.getGeometry() instanceof PropertyName) {
String attribute = ((PropertyName) s.getGeometry()).getPropertyName();
if(txGeometries.contains(attribute))
return true;
plainGeometries.add(attribute);
} else {
Expression g = s.getGeometry();
extractor.clear();
g.accept(extractor, null);
Set<String> attributes = extractor.getAttributeNameSet();
for (String attribute : attributes) {
if(plainGeometries.contains(attribute))
return true;
if(txGeometries.contains(attribute))
return true;
txGeometries.add(attribute);
}
}
}
}
}
return false;
}
/**
* @param rf
* @param feature
* @param style
* @param layerId
*/
final private void process(RenderableFeature rf, LiteFeatureTypeStyle style,
NumberRange scaleRange, AffineTransform at,
CoordinateReferenceSystem destinationCrs, String layerId)
throws TransformException, FactoryException {
boolean doElse = true;
Rule[] elseRuleList = style.elseRules;
Rule[] ruleList = style.ruleList;
Rule r;
Filter filter;
Graphics2D graphics = style.graphics;
// applicable rules
final int length = ruleList.length;
for (int t = 0; t < length; t++) {
r = ruleList[t];
filter = r.getFilter();
if ((filter == null) || filter.evaluate(rf.content)) {
doElse = false;
processSymbolizers(graphics, rf, r.symbolizers(), scaleRange,
at, destinationCrs, layerId);
}
}
if (doElse) {
final int elseLength = elseRuleList.length;
for (int tt = 0; tt < elseLength; tt++) {
r = elseRuleList[tt];
processSymbolizers(graphics, rf, r.symbolizers(), scaleRange,
at, destinationCrs, layerId);
}
}
}
/**
* Applies each of a set of symbolizers in turn to a given feature.
* <p>
* This is an internal method and should only be called by processStylers.
* </p>
* @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 List<Symbolizer> symbolizers,
NumberRange scaleRange, AffineTransform at,
CoordinateReferenceSystem destinationCrs, String layerId)
throws TransformException, FactoryException {
// clips Graphics to current drawing area before painting
Graphics2D clippedGraphics = (Graphics2D)graphics.create();
clippedGraphics.clip(screenSize);
for (Symbolizer symbolizer : symbolizers) {
// /////////////////////////////////////////////////////////////////
//
// RASTER
//
// /////////////////////////////////////////////////////////////////
if (symbolizer instanceof RasterSymbolizer) {
renderRaster(clippedGraphics, drawMe.content,
(RasterSymbolizer) symbolizer, destinationCrs,
scaleRange,at);
} else {
// /////////////////////////////////////////////////////////////////
//
// FEATURE
//
// /////////////////////////////////////////////////////////////////
LiteShape2 shape = drawMe.getShape(symbolizer, at);
if(shape == null)
continue;
if (symbolizer instanceof TextSymbolizer && drawMe.content instanceof SimpleFeature) {
labelCache.put(layerId, (TextSymbolizer) symbolizer, (SimpleFeature) drawMe.content,
shape, scaleRange);
} else {
Style2D style = styleFactory.createStyle(drawMe.content,
symbolizer, scaleRange);
painter.paint(clippedGraphics, 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 worldToScreen the world to screen transform
* @param world2Grid
* @task make it follow the symbolizer
*/
private void renderRaster(Graphics2D graphics, Object drawMe,
RasterSymbolizer symbolizer,
CoordinateReferenceSystem destinationCRS, Range scaleRange, AffineTransform worldToScreen) {
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());
GridCoverage2D coverage=null;
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.
//
// /////////////////////////////////////////////////////////////////
final GridCoverageRenderer gcr = new GridCoverageRenderer(
destinationCRS, originalMapExtent, screenSize, worldToScreen,java2dHints);
// //
// It is a grid coverage
// //
if (grid instanceof GridCoverage)
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<GridGeometry2D> readGG = new Parameter<GridGeometry2D>(AbstractGridFormat.READ_GRIDGEOMETRY2D);
readGG.setValue(new GridGeometry2D(new GridEnvelope2D(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);
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 });
}
try{
if(coverage!=null)
gcr.paint(graphics, coverage, symbolizer);
}
finally {
//we need to try and dispose this coverage since it was created on purpose for rendering
if(coverage!=null)
coverage.dispose(true);
}
}
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);
}
}
/**
* 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) {
Expression geomExpr = s.getGeometry();
// get the geometry
Geometry geom;
if(geomExpr == null) {
if(drawMe instanceof SimpleFeature)
geom = (Geometry) ((SimpleFeature) drawMe).getDefaultGeometry();
else
geom = (Geometry) defaultGeometryPropertyName.evaluate(drawMe, Geometry.class);
} else {
geom = (Geometry) geomExpr.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;
SimpleFeatureType schema = f.getFeatureType();
Expression geometry = s.getGeometry();
String geomName = null;
if(geometry instanceof PropertyName) {
geomName = ((PropertyName) geometry).getPropertyName();
return getAttributeCRS(geomName, schema);
} else if(geometry == null) {
return getAttributeCRS(null, schema);
} else {
StyleAttributeExtractor attExtractor = new StyleAttributeExtractor();
geometry.accept(attExtractor, null);
for(String name : attExtractor.getAttributeNameSet()) {
if(schema.getDescriptor(name) instanceof GeometryDescriptor) {
return getAttributeCRS(name, schema);
}
}
}
} else if ( currLayer.getSource() != null ) {
return currLayer.getSource().getCRS();
}
return null;
}
/**
* Finds the CRS of the specified attribute (or uses the default geometry instead)
* @param geomName
* @param schema
* @return
*/
org.opengis.referencing.crs.CoordinateReferenceSystem getAttributeCRS(String geomName,
SimpleFeatureType schema) {
if (geomName == null || "".equals(geomName)) {
GeometryDescriptor geom = schema.getGeometryDescriptor();
return geom.getType().getCoordinateReferenceSystem();
} else {
GeometryDescriptor geom = (GeometryDescriptor) schema.getDescriptor( geomName );
return geom.getType().getCoordinateReferenceSystem();
}
}
/**
* 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;
}
/**
* <p>
* Returns true if the optimized data loading is enabled, false otherwise.
* </p>
* <p>
* 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
* </p>
*
*/
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();
}
/**
* <p>
* 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).
* </p>
*
*/
private int getRenderingBuffer() {
if (rendererHints == null)
return renderingBufferDEFAULT;
Number result = (Number) rendererHints.get("renderingBuffer");
if (result == null)
return renderingBufferDEFAULT;
return result.intValue();
}
/**
* <p>
* Returns scale computation algorithm to be used.
* </p>
*
*/
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;
}
/**
* <p>
* Sets the generalizazion distance in the screen space.
* </p>
* <p>
* Default value is 0.8, meaning that two subsequent points are collapsed to
* one if their on screen distance is less than one pixel
* </p>
* <p>
* Set the distance to 0 if you don't want any kind of generalization
* </p>
*
* @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;
// sets whether vector rendering is enabled in the SLDStyleFactory
styleFactory.setVectorRenderingEnabled(isVectorRenderingEnabled());
}
/*
* (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;
}
/**
* A decimator that will just transform coordinates
*/
private static final Decimator NULL_DECIMATOR = new Decimator(-1, -1);
/**
* A class transforming (and caching) feature's geometries to shapes
**/
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, boolean clone) {
this.layer = layer;
this.clone = clone;
}
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 crsTransform = null;
MathTransform2D atTransform = null;
MathTransform2D fullTransform = null;
if (sa == null) {
sa = new SymbolizerAssociation();
sa.crs = (findGeometryCS(layer, content, symbolizer));
try {
crsTransform = buildTransform(sa.crs, destinationCrs);
atTransform = (MathTransform2D) ProjectiveTransform.create(worldToScreenTransform);
fullTransform = buildFullTransform(sa.crs, destinationCrs, at);
} catch (Exception e) {
// fall through
LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e);
}
sa.xform = fullTransform;
sa.crsxform = crsTransform;
sa.axform = atTransform;
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);
return getTransformedShape(RendererUtilities.getCentroid(g), null);
} else {
return getTransformedShape(RendererUtilities.getCentroid(g), sa);
}
} else {
return getTransformedShape(g, sa);
}
} 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 originalGeom, SymbolizerAssociation sa) throws TransformException,
FactoryException {
for (int i = 0; i < geometries.size(); i++) {
if(geometries.get(i) == originalGeom)
return (LiteShape2) shapes.get(i);
}
// we need to clone if the clone flag is high or if the coordinate sequence is not the one we asked for
Geometry geom = originalGeom;
if(clone || !(geom.getFactory().getCoordinateSequenceFactory() instanceof LiteCoordinateSequenceFactory)) {
geom = LiteCoordinateSequence.cloneGeometry(geom);
}
LiteShape2 shape;
if(projectionHandler != null && sa != null) {
// first generalize and transform the geometry into the rendering CRS
geom = projectionHandler.preProcess(sa.crs, geom);
if(geom == null) {
shape = null;
} else {
// first generalize and transform the geometry into the rendering CRS
Decimator d = getDecimator(sa.xform);
d.decimateTransformGeneralize(geom, sa.crsxform);
geom.geometryChanged();
// then post process it
geom = projectionHandler.postProcess(geom);
// apply the affine transform turning the coordinates into pixels
d = new Decimator(-1, -1);
d.decimateTransformGeneralize(geom, sa.axform);
// wrap into a lite shape
if(geom != null)
geom.geometryChanged();
shape = new LiteShape2(geom, null, null, false, false);
}
} else {
MathTransform2D xform = null;
if(sa != null)
xform = sa.xform;
shape = new LiteShape2(geom, xform, getDecimator(xform), false, false);
}
// cache the result
geometries.add(originalGeom);
shapes.add(shape);
return shape;
}
/**
* @throws org.opengis.referencing.operation.NoninvertibleTransformException
*/
private Decimator getDecimator(MathTransform2D mathTransform) {
// returns a decimator that does nothing if the currently set generalization
// distance is zero (no generalization desired) or if the datastore has
// already done full generalization at the desired level
if (generalizationDistance == 0 || !inMemoryGeneralization)
return NULL_DECIMATOR;
Decimator decimator = (Decimator) decimators.get(mathTransform);
if (decimator == null) {
try {
if (mathTransform != null && !mathTransform.isIdentity())
decimator = new Decimator(mathTransform.inverse(), screenSize, generalizationDistance);
else
decimator = new Decimator(null, screenSize, generalizationDistance);
} catch(org.opengis.referencing.operation.NoninvertibleTransformException e) {
decimator = new Decimator(null, screenSize, generalizationDistance);
}
decimators.put(mathTransform, decimator);
}
return decimator;
}
}
}