/* * 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; } } }