/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2004-2016, 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 static java.lang.Math.abs; import java.awt.AlphaComposite; import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.RenderingHints.Key; import java.awt.Shape; import java.awt.font.GlyphVector; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; 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.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; import javax.media.jai.Interpolation; import javax.media.jai.PlanarImage; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridEnvelope2D; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.coverage.grid.InvalidGridGeometryException; import org.geotools.coverage.grid.io.GridCoverage2DReader; import org.geotools.data.DataUtilities; import org.geotools.data.FeatureSource; import org.geotools.data.Query; import org.geotools.data.QueryCapabilities; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.factory.CommonFactoryFinder; import org.geotools.factory.Hints; import org.geotools.feature.FeatureCollection; import org.geotools.feature.FeatureIterator; import org.geotools.feature.FeatureTypes; import org.geotools.feature.SchemaException; import org.geotools.filter.IllegalFilterException; import org.geotools.filter.function.GeometryTransformationVisitor; import org.geotools.filter.spatial.DefaultCRSFilterVisitor; import org.geotools.filter.spatial.ReprojectingFilterVisitor; import org.geotools.filter.visitor.SimplifyingFilterVisitor; import org.geotools.filter.visitor.SpatialFilterVisitor; import org.geotools.geometry.jts.Decimator; import org.geotools.geometry.jts.GeometryClipper; import org.geotools.geometry.jts.JTS; import org.geotools.geometry.jts.LiteCoordinateSequence; import org.geotools.geometry.jts.LiteCoordinateSequenceFactory; import org.geotools.geometry.jts.LiteShape2; import org.geotools.geometry.jts.OffsetCurveBuilder; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.map.DirectLayer; import org.geotools.map.Layer; import org.geotools.map.MapContent; import org.geotools.map.MapContext; import org.geotools.map.MapLayer; import org.geotools.map.StyleLayer; import org.geotools.referencing.CRS; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.geotools.referencing.operation.matrix.XAffineTransform; import org.geotools.referencing.operation.transform.AffineTransform2D; 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.ScreenMap; import org.geotools.renderer.crs.ProjectionHandler; import org.geotools.renderer.crs.ProjectionHandlerFinder; import org.geotools.renderer.label.LabelCacheImpl; import org.geotools.renderer.label.LabelCacheImpl.LabelRenderingMode; import org.geotools.renderer.lite.gridcoverage2d.GridCoverageReaderHelper; import org.geotools.renderer.lite.gridcoverage2d.GridCoverageRenderer; import org.geotools.renderer.style.LineStyle2D; import org.geotools.renderer.style.SLDStyleFactory; import org.geotools.renderer.style.Style2D; import org.geotools.resources.coverage.FeatureUtilities; import org.geotools.resources.image.ImageUtilities; import org.geotools.styling.FeatureTypeStyle; import org.geotools.styling.PointSymbolizer; import org.geotools.styling.RasterSymbolizer; import org.geotools.styling.Rule; import org.geotools.styling.RuleImpl; import org.geotools.styling.StyleAttributeExtractor; import org.geotools.styling.Symbolizer; import org.geotools.styling.TextSymbolizer; import org.geotools.styling.visitor.DpiRescaleStyleVisitor; import org.geotools.styling.visitor.DuplicatingStyleVisitor; import org.geotools.styling.visitor.UomRescaleStyleVisitor; import org.opengis.coverage.processing.OperationNotFoundException; import org.opengis.feature.Feature; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.FeatureType; import org.opengis.feature.type.GeometryDescriptor; import org.opengis.feature.type.Name; import org.opengis.feature.type.PropertyDescriptor; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory2; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.PropertyName; import org.opengis.filter.sort.SortBy; import org.opengis.parameter.GeneralParameterValue; import org.opengis.referencing.FactoryException; import org.opengis.referencing.NoSuchAuthorityCodeException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.crs.SingleCRS; import org.opengis.referencing.datum.PixelInCell; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransform2D; import org.opengis.referencing.operation.TransformException; import org.opengis.style.LineSymbolizer; import org.opengis.style.PolygonSymbolizer; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; import com.vividsolutions.jts.simplify.TopologyPreservingSimplifier; /** * A streaming implementation of the GTRenderer interface. * <ul> * <li>Uses as little memory as possible by processing features as they come from the data source, * instead of accumulating them up-front</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$ * @version $Id$ */ public class StreamingRenderer implements GTRenderer { private static final int REPROJECTION_RASTER_GUTTER = 10; 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"; /** * The rendering buffer grows the query area to account for features that are contributing to * the requested area due to their large symbolizer, or long label */ public static final String RENDERING_BUFFER = "renderingBuffer"; /** 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 */ protected final static FilterFactory2 filterFactory = CommonFactoryFinder.getFilterFactory2(null); private final static PropertyName gridPropertyName = filterFactory.property("grid"); private final static PropertyName paramsPropertyName = filterFactory.property("params"); private final static PropertyName defaultGeometryPropertyName = filterFactory.property(""); /** * The MapContent instance which contains the layers and the bounding box which needs to be * rendered. */ private MapContent mapContent; /** * Flag which determines if the renderer is interactive or not. An * interactive renderer will return rather than waiting for time consuming * operations to complete (e.g. Image Loading). A non-interactive renderer * (e.g. a SVG or PDF renderer) will block for these operations. */ private boolean interactive = true; /** * Flag which controls behaviour for applying affine transformation to the * graphics object. If true then the transform will be concatenated to the * existing transform. If false it will be replaced. */ private boolean concatTransforms = false; /** Geographic map extent, eventually expanded to consider buffer area around the map */ private ReferencedEnvelope mapExtent; /** Geographic map extent, as provided by the caller */ private ReferencedEnvelope originalMapExtent; /** The size of the output area in output units. */ private Rectangle screenSize; /** * This flag is set to false when starting rendering, and will be checked * during the rendering loop in order to make it stop forcefully */ boolean renderingStopRequested = false; /** * The ratio required to scale the features to be rendered so that they fit * into the output space. */ protected 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 */ protected StyledShapePainter painter = new StyledShapePainter(labelCache); private BlockingQueue<RenderingRequest> requests; private List<RenderListener> renderListeners = new CopyOnWriteArrayList<RenderListener>(); private RenderingHints java2dHints; 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 = LabelCacheImpl.LabelRenderingMode.STRING.name(); /** * 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 = LabelCacheImpl.LabelRenderingMode.OUTLINE.name(); /** * Will use STRING mode for horizontal labels, OUTLINE mode for all other labels. * Works best when coupled with {@link RenderingHints#VALUE_FRACTIONALMETRICS_ON} */ public static final String TEXT_RENDERING_ADAPTIVE = LabelCacheImpl.LabelRenderingMode.ADAPTIVE.name(); /** * 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. */ public static final String ADVANCED_PROJECTION_HANDLING_KEY = "advancedProjectionHandling"; /** * Enabled continuous cartographic wrapping for projections that can wrap * around their edges (e.g., Mercator): this results in a continous horizontal map much * like Google Maps */ public static final String CONTINUOUS_MAP_WRAPPING = "continuousMapWrapping"; /** * 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 FORCE_EPSG_AXIS_ORDER_KEY = "ForceEPSGAxisOrder"; public static final String DPI_KEY = "dpi"; public static final String DECLARED_SCALE_DENOM_KEY = "declaredScaleDenominator"; public static final String SCALE_COMPUTATION_METHOD_KEY = "scaleComputationMethod"; public static final String BYLAYER_INTERPOLATION = "byLayerInterpolation"; /** * "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. * "forceEPSGAxisOrder" - When doing spatial filter reprojection (from the SLD towards the native CRS) assume the geometries * are expressed with the axis order suggested by the official EPSG database, regardless of how the * CRS system might be configured */ 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; /** * The thread pool used to submit the painter workers. */ private ExecutorService threadPool; private PainterThread painterThread; /** * 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 a thread pool to be used in parallel rendering * @param threadPool */ public void setThreadPool(ExecutorService threadPool) { this.threadPool = threadPool; } /** * 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); if(labelCache instanceof LabelCacheImpl) { ((LabelCacheImpl) labelCache).addRenderListener(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)){ if(feature instanceof Feature) { LOGGER.log(Level.FINE, "Skipping non simple feature rendering notification"); } 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(Throwable t) { LOGGER.log(Level.SEVERE, t.getLocalizedMessage(), t); if (renderListeners.size() > 0) { Exception e; if(t instanceof Exception) { e = (Exception) t; } else { e = new Exception(t); } 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; // un-block the queue in case it was filled with requests and the main // thread got blocked on it requests.clear(); // wake up the painter and put a death pill in the queue painterThread.interrupt(); try { requests.put(new EndRequest()); } catch(InterruptedException e) { throw new RuntimeException("Interrupted while trying to put the end " + "request in the requests queue, this should never happen", e); } 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) { fireErrorEvent(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, mapContent.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) { LOGGER.severe("renderer passed null graphics argument"); throw new NullPointerException("renderer requires graphics"); } else if (paintArea == null) { LOGGER.severe("renderer passed null paintArea argument"); throw new NullPointerException("renderer requires paintArea"); } else if (mapArea == null) { LOGGER.severe("renderer passed null mapArea argument"); throw new NullPointerException("renderer requires mapArea"); } 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); // add the anchor for graphic fills Point2D textureAnchor = new Point2D.Double(worldToScreenTransform.getTranslateX(), worldToScreenTransform.getTranslateY()); graphics.setRenderingHint(StyledShapePainter.TEXTURE_ANCHOR_HINT_KEY, textureAnchor); // reset the abort flag renderingStopRequested = false; // setup the graphic clip graphics.setClip(paintArea); // //////////////////////////////////////////////////////////////////// // // Managing transformations , CRSs and scales // // If we are rendering to a component which has already set up some form // of transformation then we can concatenate our transformation to it. // An example of this is the ZoomPane component of the swinggui module. // //////////////////////////////////////////////////////////////////// if (concatTransforms) { AffineTransform atg = graphics.getTransform(); atg.concatenate(worldToScreenTransform); worldToScreenTransform = atg; graphics.setTransform(worldToScreenTransform); } // compute scale according to the user specified method scaleDenominator = computeScale(mapArea, paintArea,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()); } // Setup the secondary painting thread requests = getRequestsQueue(); painterThread = new PainterThread(requests); ExecutorService localThreadPool = threadPool; boolean localPool = false; if(localThreadPool == null) { localThreadPool = Executors.newSingleThreadExecutor(); localPool = true; } Future painterFuture = localThreadPool.submit(painterThread); try { if(mapContent == null) { throw new IllegalStateException("Cannot call paint, you did not set a MapContent in this renderer"); } // re-organize the map content and generate the z group layers MapContent zGroupedMapContent = ZGroupLayerFactory.filter(mapContent); // split over multiple map contents, one per composition base List<CompositingGroup> compositingGroups = CompositingGroup .splitOnCompositingBase(graphics, paintArea, zGroupedMapContent); int layerCounter = 0; for (CompositingGroup compositingGroup : compositingGroups) { MapContent currentMapContent = compositingGroup.mapContent; Graphics2D compositingGraphic = compositingGroup.graphics; // //////////////////////////////////////////////////////////////////// // // Processing all the map layers in the context using the accompaining // styles // // //////////////////////////////////////////////////////////////////// labelCache.start(); if(labelCache instanceof LabelCacheImpl) { ((LabelCacheImpl) labelCache).setLabelRenderingMode(LabelRenderingMode.valueOf(getTextRenderingMethod())); } for (Layer layer : currentMapContent.layers()) { layerCounter++; String layerId = String.valueOf(layerCounter); if (!layer.isVisible()) { // Only render layer when layer is visible continue; } if (renderingStopRequested) { return; } labelCache.startLayer(layerId); if (layer instanceof DirectLayer) { RenderingRequest request = new RenderDirectLayerRequest(compositingGraphic, (DirectLayer) layer); try { requests.put(request); } catch (InterruptedException e) { fireErrorEvent(e); } } else if (layer instanceof ZGroupLayer) { try { ZGroupLayer zGroup = (ZGroupLayer) layer; zGroup.drawFeatures(compositingGraphic, this, layerId); } catch (Throwable t) { fireErrorEvent(t); } } else { try { // extract the feature type stylers from the style object // and process them processStylers(compositingGraphic, layer, layerId); } catch (Throwable t) { fireErrorEvent(t); } } labelCache.endLayer(layerId, graphics, screenSize); } // have we been painting on a back buffer? If so, merge on the main graphic if (compositingGraphic instanceof DelayedBackbufferGraphic) { RenderingRequest request = new MargeCompositingGroupRequest(graphics, compositingGroup); try { requests.put(request); } catch (InterruptedException e) { fireErrorEvent(e); } } // the compositing group has its own map content, clones of the original one, // they need to be disposed to avoid nagging messages (not that disposing here // does any good...) compositingGroup.mapContent.dispose(); } } finally { try { if(!renderingStopRequested) { requests.put(new EndRequest()); painterFuture.get(); } } catch(Exception e) { painterFuture.cancel(true); fireErrorEvent(e); } finally { if(localPool) { localThreadPool.shutdown(); } } } if(!renderingStopRequested) { labelCache.end(graphics, paintArea); } else { labelCache.clear(); } 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()); } } /** * Builds the blocking queue used to bridge between the data loading thread and * the painting one * @return */ protected BlockingQueue<RenderingRequest> getRequestsQueue() { return new RenderingBlockingQueue(10000); } /** * 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 features to be rendered based on the target * rendering bounding box. * <p> * 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 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 * @see MapLayer#setQuery(org.geotools.data.Query) */ /* * Default visibility for testing purposes */ Query getStyleQuery(Layer layer, List<LiteFeatureTypeStyle> styleList, Envelope mapArea, CoordinateReferenceSystem mapCRS, CoordinateReferenceSystem featCrs, Rectangle screenSize, GeometryDescriptor geometryAttribute, AffineTransform worldToScreenTransform, boolean hasRenderingTransformation) throws IllegalFilterException, IOException, FactoryException { FeatureSource<FeatureType, Feature> source = (FeatureSource<FeatureType, Feature>) layer.getFeatureSource(); FeatureType schema = source.getSchema(); Query query = new Query(Query.ALL); 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 metaBuffer = findRenderingBuffer(styleList); if (metaBuffer > 0) { mapArea = expandEnvelope(mapArea, worldToScreenTransform, metaBuffer); LOGGER.fine("Expanding rendering area by " + metaBuffer + " pixels to consider stroke width"); // expand the screenmaps by the meta buffer, otherwise we'll throw away geomtries // that sit outside of the map, but whose symbolizer may contribute to it for (LiteFeatureTypeStyle lfts : styleList) { if(lfts.screenMap != null) { lfts.screenMap = new ScreenMap(lfts.screenMap, metaBuffer); } } } setMetaBuffer(styleList, metaBuffer); } // take care of rendering transforms mapArea = expandEnvelopeByTransformations(styleList, new ReferencedEnvelope(mapArea, mapCRS)); // build a list of attributes used in the rendering List<PropertyName> attributes; if (styleList == null) { attributes = null; } else { attributes = findStyleAttributes(styleList, schema); } ReferencedEnvelope envelope = new ReferencedEnvelope(mapArea, mapCRS); // 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 = null; // enable advanced projection handling with the updated map extent if (isAdvancedProjectionHandlingEnabled()) { // get the projection handler and set a tentative envelope ProjectionHandler projectionHandler = ProjectionHandlerFinder.getHandler(envelope, featCrs, isMapWrappingEnabled()); if (projectionHandler != null) { setProjectionHandler(styleList, projectionHandler); envelopes = projectionHandler.getQueryEnvelopes(); } } if(envelopes == null) { 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.getName() + " with bbox: " + envelope); filter = createBBoxFilters(schema, attributes, envelopes); // now build the query using only the attributes and the // bounding box needed query = new Query(schema.getName().getLocalPart()); query.setFilter(filter); query.setProperties(attributes); processRuleForQuery(styleList, query); } catch (Exception e) { final Exception txException = new Exception("Error transforming bbox", e); LOGGER.log(Level.SEVERE, "Error querying layer", txException); fireErrorEvent(txException); canTransform = false; query = new Query(schema.getName().getLocalPart()); query.setProperties(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(styleList, query); } // get the eventual sort-by from the styles SortBy[] sortBy = getSortByFromLiteStyles(styleList); if(sortBy != null) { QueryCapabilities qc = source.getQueryCapabilities(); if (qc != null && !qc.supportsSorting(sortBy)) { throw new IllegalArgumentException("The feature source in layer " + layer.getTitle() + " cannot sort on " + Arrays.toString(sortBy)); } query.setSortBy(sortBy); } // prepare hints // ... basic one, we want fast and compact coordinate sequences and geometries optimized // for the collection of one item case (typical in shapefiles) LiteCoordinateSequenceFactory csFactory = new LiteCoordinateSequenceFactory(); GeometryFactory gFactory = new SimpleGeometryFactory(csFactory); Hints hints = new Hints(Hints.JTS_COORDINATE_SEQUENCE_FACTORY, csFactory); hints.put(Hints.JTS_GEOMETRY_FACTORY, gFactory); hints.put(Hints.FEATURE_2D, Boolean.TRUE); // update the screenmaps try { CoordinateReferenceSystem crs = getNativeCRS(schema, attributes); if(crs != null) { Set<RenderingHints.Key> fsHints = source.getSupportedHints(); SingleCRS crs2D = crs == null ? null : CRS.getHorizontalCRS(crs); MathTransform mt = buildFullTransform(crs2D, mapCRS, worldToScreenTransform); double[] spans = Decimator.computeGeneralizationDistances(mt.inverse(), screenSize, generalizationDistance); double distance = spans[0] < spans[1] ? spans[0] : spans[1]; for (LiteFeatureTypeStyle fts : styleList) { if(fts.screenMap != null) { fts.screenMap.setTransform(mt); fts.screenMap.setSpans(spans[0], spans[1]); if(fsHints.contains(Hints.SCREENMAP)) { // replace the renderer screenmap with the hint, and avoid doing // the work twice hints.put(Hints.SCREENMAP, fts.screenMap); fts.screenMap = null; } } } if(hasRenderingTransformation) { // the RT might need valid geometries, we can at most apply a topology // preserving generalization if(fsHints.contains(Hints.GEOMETRY_GENERALIZATION)) { hints.put(Hints.GEOMETRY_GENERALIZATION, distance); disableInMemoryGeneralization(styleList); } } else { // ... if possible we let the datastore do the generalization 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); disableInMemoryGeneralization(styleList); } 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); } if(query.getHints() == null) { query.setHints(hints); } else { query.getHints().putAll(hints); } // simplify the filter SimplifyingFilterVisitor simplifier = new SimplifyingFilterVisitor(); simplifier.setFeatureType(source.getSchema()); Filter simplifiedFilter = (Filter) query.getFilter().accept(simplifier, null); query.setFilter(simplifiedFilter); return query; } private void setMetaBuffer(List<LiteFeatureTypeStyle> styleList, int metaBuffer) { for (LiteFeatureTypeStyle fts : styleList) { fts.metaBuffer = metaBuffer; } } private void setProjectionHandler(List<LiteFeatureTypeStyle> styleList, ProjectionHandler projectionHandler) { for (LiteFeatureTypeStyle fts : styleList) { fts.projectionHandler = projectionHandler; } } private void disableInMemoryGeneralization(List<LiteFeatureTypeStyle> styleList) { for (LiteFeatureTypeStyle fts : styleList) { fts.inMemoryGeneralization = false; } } /** * Returns the sort-by from the list of feature type styles for a given layer. The code assumes * the styles have already been classified and are uniform in sorting clauses * * @param styles * @return */ private SortBy[] getSortByFromLiteStyles(List<LiteFeatureTypeStyle> styles) { for (LiteFeatureTypeStyle fts : styles) { if (fts.sortBy != null) { return fts.sortBy; } } return null; } Query getDefinitionQuery(Layer currLayer, FeatureSource<FeatureType, Feature> source, CoordinateReferenceSystem featCrs) throws FactoryException { // now, if a definition query has been established for this layer, be // sure to respect it by combining it with the bounding box one. Query definitionQuery = reprojectQuery(currLayer.getQuery(), source); definitionQuery.setCoordinateSystem(featCrs); return definitionQuery; } /** * Takes care of eventual geometric transformations * @param styles * @param envelope * @return */ ReferencedEnvelope expandEnvelopeByTransformations(List<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) { ReferencedEnvelope re = (ReferencedEnvelope) s.getGeometry().accept(visitor, envelope); if(re != null) { result.expandToInclude(re); } } } } } return result; } /** * Builds a full transform going from the source CRS to the destination CRS * and from there to the screen. * <p> * Although we ask for 2D content (via {@link Hints#FEATURE_2D} ) not all DataStore implementations * are capable. In this event we will manually stage the information into * {@link DefaultGeographicCRS#WGS84}) and before using this transform. */ private MathTransform buildFullTransform(CoordinateReferenceSystem sourceCRS, CoordinateReferenceSystem destCRS, AffineTransform worldToScreenTransform) throws FactoryException { MathTransform mt = buildTransform(sourceCRS, destCRS); // concatenate from world to screen if (mt != null && !mt.isIdentity()) { mt = ConcatenatedTransform .create(mt, ProjectiveTransform.create(worldToScreenTransform)); } else { mt = ProjectiveTransform.create(worldToScreenTransform); } return mt; } /** * Builds the transform from sourceCRS to destCRS/ * <p> * Although we ask for 2D content (via {@link Hints#FEATURE_2D} ) not all DataStore implementations * are capable. With that in mind if the provided soruceCRS is not 2D we are going to manually * post-process the Geomtries into {@link DefaultGeographicCRS#WGS84} - and the {@link MathTransform2D} * returned here will transition from WGS84 to the requested 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 If no transform is available to the destCRS */ private MathTransform buildTransform(CoordinateReferenceSystem sourceCRS, CoordinateReferenceSystem destCRS) throws FactoryException { MathTransform transform = null; if( sourceCRS != null && sourceCRS.getCoordinateSystem().getDimension() >= 3 ){ // We are going to transform over to DefaultGeographic.WGS84 on the fly // so we will set up our math transform to take it from there MathTransform toWgs84_3d = CRS.findMathTransform( sourceCRS, DefaultGeographicCRS.WGS84_3D ); MathTransform toWgs84_2d = CRS.findMathTransform( DefaultGeographicCRS.WGS84_3D, DefaultGeographicCRS.WGS84); transform = ConcatenatedTransform.create(toWgs84_3d, toWgs84_2d); sourceCRS = DefaultGeographicCRS.WGS84; } // 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); } if(transform != null) { if(mt == null) { return transform; } else { return ConcatenatedTransform.create(transform, mt); } } else { 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(FeatureType schema, List<PropertyName> 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; //NC - property (namespace) support for (PropertyName name : attNames) { Object att = name.evaluate(schema); 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(List<LiteFeatureTypeStyle> styles, Query 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 = 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 { if (rendererHints == null) return defaultMaxFiltersToSendToDatastore; 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 continuous map wrapping is enabled * @return */ private boolean isMapWrappingEnabled() { if (rendererHints == null) return false; Object result = rendererHints.get(CONTINUOUS_MAP_WRAPPING); if (result == null) return false; return Boolean.TRUE.equals(result); } /** * Checks if the geometries in spatial filters in the SLD must be assumed to be expressed * in the official EPSG axis order, regardless of how the referencing subsystem is configured * (this is required to support filter reprojection in WMS 1.3+) * @return */ private boolean isEPSGAxisOrderForced() { if (rendererHints == null) return false; Object result = rendererHints.get(FORCE_EPSG_AXIS_ORDER_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(List<LiteFeatureTypeStyle> styles) { final MetaBufferEstimator rbe = new MetaBufferEstimator(); for (LiteFeatureTypeStyle lfts : styles) { 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.fine("Assuming rendering buffer = " + rbe.getBuffer() + ", but estimation is not accurate, you may want to set a buffer manually"); // the actual amount we have to grow the rendering area by is half of the stroke/symbol sizes // plus one extra pixel for antialiasing effects return (int) Math.round(rbe.getBuffer() / 2.0 + 1); } /** * 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 List<PropertyName> findStyleAttributes(List<LiteFeatureTypeStyle> styles, FeatureType schema) { final StyleAttributeExtractor sae = new StyleAttributeExtractor(); for (LiteFeatureTypeStyle lfts : styles) { for (Rule rule : lfts.elseRules) { sae.visit(rule); } for (Rule rule : lfts.ruleList) { sae.visit(rule); } } if(sae.isUsingDynamincProperties()) { return null; } Set<PropertyName> attributes = sae.getAttributes(); Set<String> attributeNames = sae.getAttributeNameSet(); /* * 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<PropertyName> atts = new ArrayList<PropertyName>(attributes); Collection<PropertyDescriptor> attTypes = schema.getDescriptors(); Name attName; for (PropertyDescriptor pd : attTypes) { //attName = pd.getName().getLocalPart(); attName = pd.getName(); // 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.getLocalPart().equalsIgnoreCase("grid")) && !attributeNames.contains(attName.getLocalPart()) || (attName.getLocalPart().equalsIgnoreCase("params")) && !attributeNames.contains(attName.getLocalPart()) ) { atts.add(filterFactory.property (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. // we substitute the default geometry attribute "" with the proper geometry attribute name (this will help us avoid // situations were the geometry is not read because of the default geometry attribute "" not being taken in account) if (sae.getDefaultGeometryUsed() && !attributeNames.contains(schema.getGeometryDescriptor().getName().toString())) { atts.add(filterFactory.property ( schema.getGeometryDescriptor().getName() )); } // the geometry attribute name was added above, we need to remove the default geometry attribute "" if present for (Iterator<PropertyName> it = atts.iterator(); it.hasNext();){ PropertyName propertyName = it.next(); if (propertyName.getPropertyName().equals("")) { // well the default geometry attribute name "" is present, so let's remove it it.remove(); break; } } } catch (Exception e) { // might not be a geometry column. That will cause problems down the // road (why render a non-geometry layer) } return atts; } /** * 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(FeatureType schema, List<PropertyName> attributes, List<ReferencedEnvelope> bboxes) throws IllegalFilterException { Filter filter = Filter.INCLUDE; final int length = attributes.size(); Object attType; for (int j = 0; j < length; j++) { //NC - support nested attributes -> use evaluation for getting descriptor //result is not necessary a descriptor, is Name in case of @attribute attType = attributes.get(j).evaluate(schema); // the attribute type might be missing because of rendering transformations, skip it if (attType == null) { continue; } if (attType instanceof GeometryDescriptor) { Filter gfilter = new FastBBOX(attributes.get(j), bboxes.get(0), filterFactory); 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(localName, bboxes.get(k), filterFactory) ); filter = filterFactory.or( filter, new FastBBOX(attributes.get(j), bboxes.get(k), filterFactory) ); } } } } return filter; } /** * Checks if a rule can be triggered at the current scale level * * @param r * The rule * @return true if the scale is compatible with the rule settings */ private boolean isWithInScale(Rule r) { return ((r.getMinScaleDenominator() - TOLERANCE) <= scaleDenominator) && ((r.getMaxScaleDenominator() + TOLERANCE) > scaleDenominator); } /** * creates a list of LiteFeatureTypeStyles a) out-of-scale rules removed b) incompatible * FeatureTypeStyles removed * * * @return ArrayList<LiteFeatureTypeStyle> * @throws FactoryException */ ArrayList<LiteFeatureTypeStyle> createLiteFeatureTypeStyles(Layer layer, Graphics2D graphics, boolean optimizedFTSRendering) throws IOException, FactoryException { if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("creating rules for scale denominator - " + NumberFormat.getNumberInstance().format(scaleDenominator)); ArrayList<LiteFeatureTypeStyle> result = new ArrayList<LiteFeatureTypeStyle>(); LiteFeatureTypeStyle lfts; boolean foundComposite = false; for (FeatureTypeStyle fts : layer.getStyle().featureTypeStyles()) { if (isFeatureTypeStyleActive(layer.getFeatureSource().getSchema(), fts)) { // DJB: this FTS is compatible with this FT. // get applicable rules at the current scale List<List<Rule>> splittedRules = splitRules(fts); List<Rule> ruleList = splittedRules.get(0); List<Rule> elseRuleList = splittedRules.get(1); // if none, skip it if ((ruleList.isEmpty()) && (elseRuleList.isEmpty())) continue; // get the fts level composition, if any Composite composite = styleFactory.getComposite(fts.getOptions()); foundComposite |= composite != null; // we can optimize this one and draw directly on the graphics, assuming // there is no composition if (!foundComposite && (result.isEmpty() || !optimizedFTSRendering)) { lfts = new LiteFeatureTypeStyle(layer, graphics, ruleList, elseRuleList, fts.getTransformation()); } else { lfts = new LiteFeatureTypeStyle(layer, new DelayedBackbufferGraphic(graphics, screenSize), ruleList, elseRuleList, fts.getTransformation()); } lfts.composite = composite; if (FeatureTypeStyle.VALUE_EVALUATION_MODE_FIRST.equals(fts.getOptions().get( FeatureTypeStyle.KEY_EVALUATION_MODE))) { lfts.matchFirst = true; } // get the sort by, if any SortBy[] sortBy = styleFactory.getSortBy(fts.getOptions()); lfts.sortBy = sortBy; if (screenMapEnabled(lfts)) { int renderingBuffer = getRenderingBuffer(); lfts.screenMap = new ScreenMap(screenSize.x - renderingBuffer, screenSize.y - renderingBuffer, screenSize.width + renderingBuffer * 2, screenSize.height + renderingBuffer * 2); } result.add(lfts); } } if (!result.isEmpty()) { // make sure all spatial filters in the feature source native SRS reprojectSpatialFilters(result, layer.getFeatureSource().getSchema()); // apply the uom and dpi rescale applyUnitRescale(result); } return result; } /** * Returns true if the ScreenMap optimization can be applied given the current renderer and * configuration and the style to be applied * * @param lfts * @return */ boolean screenMapEnabled(LiteFeatureTypeStyle lfts) { if (generalizationDistance == 0.0) { return false; } OpacityFinder finder = new OpacityFinder(new Class[] { PointSymbolizer.class, LineSymbolizer.class, PolygonSymbolizer.class }); for (Rule r : lfts.ruleList) { r.accept(finder); } for (Rule r : lfts.elseRules) { r.accept(finder); } return !finder.hasOpacity; } private boolean isFeatureTypeStyleActive(FeatureType ftype, FeatureTypeStyle fts) { // TODO: find a complex feature equivalent for this check return fts.featureTypeNames().isEmpty() || ((ftype.getName().getLocalPart() != null) && (ftype.getName().getLocalPart().equalsIgnoreCase(fts.getFeatureTypeName()) || FeatureTypes.isDecendedFrom(ftype, null, fts.getFeatureTypeName()))); } private List<List<Rule>> splitRules(FeatureTypeStyle fts) { List<Rule> ruleList = new ArrayList<Rule>(); List<Rule> elseRuleList = new ArrayList<Rule>(); for (Rule r : fts.rules()) { if (isWithInScale(r)) { if (r.isElseFilter()) { elseRuleList.add(r); } else { ruleList.add(r); } } } return Arrays.asList(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 (Layer layer : mapContent.layers()) { if (!layer.isVisible()) { // Only render layer when layer is visible continue; } // Skip layers that do not have multiple FeatureTypeStyles if (!(layer instanceof StyleLayer)) { continue; } StyleLayer styleLayer = (StyleLayer) layer; if (styleLayer.getStyle().featureTypeStyles().size() < 2) continue; // count how many lite feature type styles are active int currCount = 0; FeatureType ftype = layer.getFeatureSource().getSchema(); for (FeatureTypeStyle fts : styleLayer.getStyle().featureTypeStyles()) { if (isFeatureTypeStyleActive(ftype, fts)) { // get applicable rules at the current scale List<List<Rule>> splittedRules = splitRules(fts); List<Rule> ruleList = splittedRules.get(0); List<Rule> elseRuleList = splittedRules.get(1); // if none, skip this fts if ((ruleList.isEmpty()) && (elseRuleList.isEmpty())) continue; currCount++; } } // consider the first fts does not allocate a buffer currCount--; if(currCount > maxBuffers) maxBuffers = currCount; } return maxBuffers * width * height * 4; } /** * Applies all the styles to the features/coverages contained in the given layer. * * @param graphics Target graphics for rendering * @param layer The layer being styled * @param layerId Handle used to identify the layer in the {@link LabelCache} * @throws Exception */ private void processStylers(final Graphics2D graphics, final Layer layer, String layerId) throws Exception { // ///////////////////////////////////////////////////////////////////// // // Preparing feature information and styles // // ///////////////////////////////////////////////////////////////////// final FeatureSource featureSource = layer.getFeatureSource(); if (featureSource == null) { throw new IllegalArgumentException( "The layer does not contain a feature source: " + layer.getTitle()); } final FeatureType schema = featureSource.getSchema(); final ArrayList<LiteFeatureTypeStyle> lfts = createLiteFeatureTypeStyles(layer, graphics, isOptimizedFTSRenderingEnabled()); if (lfts.isEmpty()) { return; } else { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Processing " + lfts.size() + " stylers for " + schema.getName()); } } // classify by sortby and transformation (aka how we produce the features to // be rendered) List<List<LiteFeatureTypeStyle>> txClassified = classifyByFeatureProduction(lfts); // render groups by uniform transformation for (List<LiteFeatureTypeStyle> uniform : txClassified) { FeatureCollection features = getFeatures(layer, schema, uniform); if(features == null) { continue; } // finally, perform rendering if (isOptimizedFTSRenderingEnabled() && lfts.size() > 1) { drawOptimized(graphics, layerId, features, uniform); } else { drawPlain(graphics, layerId, features, uniform); } } } FeatureCollection getFeatures(final Layer layer, final FeatureType schema, List<LiteFeatureTypeStyle> featureTypeStyles) throws IOException, FactoryException, NoninvertibleTransformException, SchemaException, TransformException { final FeatureSource featureSource = layer.getFeatureSource(); Expression transform = featureTypeStyles.get(0).transformation; // grab the source crs and geometry attribute final CoordinateReferenceSystem sourceCrs; final GeometryDescriptor geometryAttribute = schema.getGeometryDescriptor(); if (geometryAttribute != null && geometryAttribute.getType() != null) { sourceCrs = geometryAttribute.getType().getCoordinateReferenceSystem(); } else { sourceCrs = null; } // ... assume we have to do the generalization, the query layer process will // turn down the flag if we don't boolean hasTransformation = transform != null; Query styleQuery = getStyleQuery(layer, featureTypeStyles, mapExtent, destinationCrs, sourceCrs, screenSize, geometryAttribute, worldToScreenTransform, hasTransformation); Query definitionQuery = getDefinitionQuery(layer, featureSource, sourceCrs); FeatureCollection features = null; if(hasTransformation) { // prepare the stage for the raster transformations GridGeometry2D gridGeometry = getRasterGridGeometry(destinationCrs, sourceCrs); // vector transformation wise, we have to account for two separate queries, // the one attached to the layer and then one coming from SLD. // The first source attributes, the latter talks tx output attributes // so they have to be applied before and after the transformation respectively RenderingTransformationHelper helper = new RenderingTransformationHelper() { @Override protected GridCoverage2D readCoverage(GridCoverage2DReader reader, Object params, GridGeometry2D readGG) throws IOException { GeneralParameterValue[] readParams = (GeneralParameterValue[]) params; Interpolation interpolation = getRenderingInterpolation(layer); GridCoverageReaderHelper helper; try { helper = new GridCoverageReaderHelper(reader, readGG.getGridRange2D(), ReferencedEnvelope.reference(readGG.getEnvelope2D()), interpolation); return helper.readCoverage(readParams); } catch (InvalidGridGeometryException | FactoryException e) { throw new IOException("Failure reading the coverage", e); } } }; Object result = helper.applyRenderingTransformation(transform, featureSource, definitionQuery, styleQuery, gridGeometry, sourceCrs, java2dHints); if(result == null) { return null; } else if (result instanceof FeatureCollection) { features = (FeatureCollection) result; } else if (result instanceof GridCoverage2D) { GridCoverage2D coverage = (GridCoverage2D) result; // we only avoid disposing if the input was a in memory GridCovereage2D if((schema instanceof SimpleFeatureType && !FeatureUtilities.isWrappedCoverage((SimpleFeatureType) schema))) { coverage = new DisposableGridCoverage(coverage); } features = FeatureUtilities.wrapGridCoverage(coverage); } else if (result instanceof GridCoverage2DReader) { features = FeatureUtilities.wrapGridCoverageReader( (GridCoverage2DReader) result, null); } else { throw new IllegalArgumentException( "Don't know how to handle the results of the transformation, " + "the supported result types are FeatureCollection, GridCoverage2D " + "and GridCoverage2DReader, but we got: " + result.getClass()); } } else { Query mixed = DataUtilities.mixQueries(definitionQuery, styleQuery, null); // mix the sort by, the style query takes precedence if (styleQuery.getSortBy() != null) { mixed.setSortBy(styleQuery.getSortBy()); } else { mixed.setSortBy(definitionQuery.getSortBy()); } checkAttributeExistence(featureSource.getSchema(), mixed); features = featureSource.getFeatures(mixed); features = RendererUtilities.fixFeatureCollectionReferencing(features, sourceCrs); } // HACK HACK HACK // For complex features, we need the targetCrs and version in scenario where we have // a top level feature that does not contain a geometry(therefore no crs) and has a // nested feature that contains geometry as its property.Furthermore it is possible // for each nested feature to have different crs hence we need to reproject on each // feature accordingly. // This is a Hack, this information should not be passed through feature type // appschema will need to remove this information from the feature type again if (!(features instanceof SimpleFeatureCollection)) { features.getSchema().getUserData().put("targetCrs", destinationCrs); features.getSchema().getUserData().put("targetVersion", "wms:getmap"); } return features; } /** * Classify a List of LiteFeatureTypeStyle objects by Transformation. * * @param lfts A List of LiteFeatureTypeStyles * @return A List of List of LiteFeatureTypeStyles */ List<List<LiteFeatureTypeStyle>> classifyByFeatureProduction(List<LiteFeatureTypeStyle> lfts) { List<List<LiteFeatureTypeStyle>> txClassified = new ArrayList<List<LiteFeatureTypeStyle>>(); txClassified.add(new ArrayList<LiteFeatureTypeStyle>()); Expression transformation = null; SortBy[] sortBy = null; for (int i = 0; i < lfts.size(); i++) { LiteFeatureTypeStyle curr = lfts.get(i); if(i == 0) { transformation = curr.transformation; sortBy = curr.sortBy; } else { // do they have the same transformation? boolean differentTransformation = (transformation != curr.transformation) || (transformation != null && curr.transformation != null && !curr.transformation.equals(transformation)); // is sorting incompatible, that is, different from the one // we are working against? "null" means not caring about sorting, // it's thus compatible with whatever other sort boolean incompatibleSort = false; if (curr.sortBy != null) { if (sortBy == null) { // we started with "whatever sorting", from here on we have one sortBy = curr.sortBy; } else { incompatibleSort = true; } } if (differentTransformation || incompatibleSort) { // create a new slot (we always add the lfts into the last one) txClassified.add(new ArrayList<LiteFeatureTypeStyle>()); transformation = curr.transformation; sortBy = curr.sortBy; } } txClassified.get(txClassified.size() - 1).add(curr); } return txClassified; } /** * Checks the attributes in the query (which we got from the SLD) match the * schema, throws an {@link IllegalFilterException} otherwise * @param schema * @param attributeNames */ void checkAttributeExistence(FeatureType schema, Query query) { if(query.getProperties() == null) { return; } for (PropertyName attribute : query.getProperties()) { if(attribute.evaluate(schema) == null) { if (schema instanceof SimpleFeatureType) { List<Name> allNames = new ArrayList<Name>(); for (PropertyDescriptor pd : schema.getDescriptors()) { allNames.add(pd.getName()); } throw new IllegalFilterException("Could not find '" + attribute + "' in the FeatureType (" + schema.getName() + "), available attributes are: " + allNames); } else { throw new IllegalFilterException("Could not find '" + attribute + "' in the FeatureType (" + schema.getName() + ")"); } } } } /** * Applies Unit Of Measure rescaling against all symbolizers, the result will be symbolizers * that operate purely in pixels * @param lfts */ void applyUnitRescale(final ArrayList<LiteFeatureTypeStyle> lfts) { // apply dpi rescale double dpi = RendererUtilities.getDpi(getRendererHints()); double standardDpi = RendererUtilities.getDpi(Collections.emptyMap()); if(dpi != standardDpi) { double scaleFactor = dpi / standardDpi; DpiRescaleStyleVisitor dpiVisitor = new GraphicsAwareDpiRescaleStyleVisitor(scaleFactor); for(LiteFeatureTypeStyle fts : lfts) { rescaleFeatureTypeStyle(fts, dpiVisitor); } } // apply UOM rescaling double pixelsPerMeters = RendererUtilities.calculatePixelsPerMeterRatio(scaleDenominator, rendererHints); UomRescaleStyleVisitor rescaleVisitor = new UomRescaleStyleVisitor(pixelsPerMeters); for(LiteFeatureTypeStyle fts : lfts) { rescaleFeatureTypeStyle(fts, rescaleVisitor); } } /** * Reprojects the spatial filters in each {@link LiteFeatureTypeStyle} so that they match * the feature source native coordinate system * @param lfts * @param fs * @throws FactoryException */ void reprojectSpatialFilters(final ArrayList<LiteFeatureTypeStyle> lfts, FeatureType schema) throws FactoryException { CoordinateReferenceSystem declaredCRS = getDeclaredSRS(schema); // reproject spatial filters in each fts for (LiteFeatureTypeStyle fts : lfts) { reprojectSpatialFilters(fts, declaredCRS, schema); } } /** * Computes the declared SRS of a layer based on the layer schema and the EPSG forcing flag * @param schema * @return * @throws FactoryException * @throws NoSuchAuthorityCodeException */ private CoordinateReferenceSystem getDeclaredSRS(FeatureType schema) throws FactoryException { // compute the default SRS of the feature source CoordinateReferenceSystem declaredCRS = schema.getCoordinateReferenceSystem(); if(isEPSGAxisOrderForced()) { Integer code = CRS.lookupEpsgCode(declaredCRS, false); if(code != null) { declaredCRS = CRS.decode("urn:ogc:def:crs:EPSG::" + code); } } return declaredCRS; } /** * Reprojects all spatial filters in the specified Query so that they match the native srs of the * specified feature source * * @param query * @param source * @return * @throws FactoryException */ private Query reprojectQuery(Query query, FeatureSource<FeatureType, Feature> source) throws FactoryException { if(query == null || query.getFilter() == null) { return query; } // compute the declared CRS Filter original = query.getFilter(); CoordinateReferenceSystem declaredCRS = getDeclaredSRS(source.getSchema()); Filter reprojected = reprojectSpatialFilter(declaredCRS, source.getSchema(), original); if(reprojected == original) { return query; } else { Query rq = new Query(query); rq.setFilter(reprojected); return rq; } } /** * Reprojects spatial filters so that they match the feature source native CRS, and assuming all literal * geometries are specified in the specified declaredCRS */ void reprojectSpatialFilters(LiteFeatureTypeStyle fts, CoordinateReferenceSystem declaredCRS, FeatureType schema) { for (int i = 0; i < fts.ruleList.length; i++) { fts.ruleList[i] = reprojectSpatialFilters(fts.ruleList[i], declaredCRS, schema); } if(fts.elseRules != null) { for (int i = 0; i < fts.elseRules.length; i++) { fts.elseRules[i] = reprojectSpatialFilters(fts.elseRules[i], declaredCRS, schema); } } } /** * Reprojects spatial filters so that they match the feature source native CRS, and assuming all literal * geometries are specified in the specified declaredCRS */ private Rule reprojectSpatialFilters(Rule rule, CoordinateReferenceSystem declaredCRS, FeatureType schema) { // NPE avoidance Filter filter = rule.getFilter(); if(filter == null) { return rule; } // try to reproject the filter Filter reprojected = reprojectSpatialFilter(declaredCRS, schema, filter); if(reprojected == filter) { return rule; } // clone the rule (the style can be reused over and over, we cannot alter it) and set the new filter Rule rr = new RuleImpl(rule); rr.setFilter(reprojected); return rr; } /** * Reprojects spatial filters so that they match the feature source native CRS, and assuming all literal * geometries are specified in the specified declaredCRS */ private Filter reprojectSpatialFilter(CoordinateReferenceSystem declaredCRS, FeatureType schema, Filter filter) { // NPE avoidance if(filter == null) { return null; } // do we have any spatial filter? SpatialFilterVisitor sfv = new SpatialFilterVisitor(); filter.accept(sfv, null); if(!sfv.hasSpatialFilter()) { return filter; } // all right, we need to default the literals to the declaredCRS and then reproject to // the native one DefaultCRSFilterVisitor defaulter = new DefaultCRSFilterVisitor(filterFactory, declaredCRS); Filter defaulted = (Filter) filter.accept(defaulter, null); ReprojectingFilterVisitor reprojector = new ReprojectingFilterVisitor(filterFactory, schema); Filter reprojected = (Filter) defaulted.accept(reprojector, null); return reprojected; } /** * Utility method to apply the two rescale visitors without duplicating code * @param fts * @param visitor */ void rescaleFeatureTypeStyle(LiteFeatureTypeStyle fts, DuplicatingStyleVisitor visitor) { for (int i = 0; i < fts.ruleList.length; i++) { visitor.visit(fts.ruleList[i]); fts.ruleList[i] = (Rule) visitor.getCopy(); } if(fts.elseRules != null) { for (int i = 0; i < fts.elseRules.length; i++) { visitor.visit(fts.elseRules[i]); fts.elseRules[i] = (Rule) visitor.getCopy(); } } } /** * 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, String layerId, FeatureCollection<?, ?> features, final List<LiteFeatureTypeStyle> lfts) { // for each lite feature type style, scan the whole collection and draw for (LiteFeatureTypeStyle liteFeatureTypeStyle : lfts) { try (FeatureIterator<?> featureIterator = ((FeatureCollection<?, ?>) features) .features()) { if( featureIterator == null ){ return; // nothing to do } RenderableFeature rf = createRenderableFeature(layerId, isCloningRequired(lfts)); rf.layer = liteFeatureTypeStyle.layer; rf.setScreenMap(liteFeatureTypeStyle.screenMap); // 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 (featureIterator.hasNext() && !renderingStopRequested) { rf.setFeature(featureIterator.next()); processFeature(rf, liteFeatureTypeStyle); } } if (liteFeatureTypeStyle.composite != null) { try { requests.put(new MergeLayersRequest(graphics, Collections.singletonList(liteFeatureTypeStyle))); } catch (InterruptedException e) { fireErrorEvent(e); } } } } /** * Builds a new renderable feature for the given layerId and set of lite feature type styles * * @param layerId * @param cloningRequired TODO * @return */ RenderableFeature createRenderableFeature(String layerId, boolean cloningRequired) { RenderableFeature rf = new RenderableFeature(layerId, cloningRequired); return rf; } /** * 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, String layerId, FeatureCollection features, final List<LiteFeatureTypeStyle> lfts) { try (FeatureIterator<?> iterator = features.features()) { if (iterator == null) return; // nothing to do RenderableFeature rf = createRenderableFeature(layerId, isCloningRequired(lfts)); // 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) { rf.setFeature(iterator.next()); // draw the feature on the main graphics and on the eventual extra image buffers for (LiteFeatureTypeStyle liteFeatureTypeStyle : lfts) { processFeature(rf, liteFeatureTypeStyle); } } // submit the merge request requests.put(new MergeLayersRequest(graphics, lfts)); } catch (InterruptedException e) { fireErrorEvent(e); } } /** * Tells if geometry cloning is required or not */ private boolean isCloningRequired(List<LiteFeatureTypeStyle> lfts) { // check if the features are detached, we can thus modify the geometries in place Layer layer = lfts.get(0).layer; 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 fts * @param layerId */ void processFeature(RenderableFeature rf, LiteFeatureTypeStyle fts) { try { // init the renderable feature for this fts rf.inMemoryGeneralization = fts.inMemoryGeneralization; rf.projectionHandler = fts.projectionHandler; rf.setScreenMap(fts.screenMap); rf.layer = fts.layer; rf.metaBuffer = fts.metaBuffer; // can the rules boolean doElse = true; Rule[] elseRuleList = fts.elseRules; Rule[] ruleList = fts.ruleList; Rule r; Filter filter; Graphics2D graphics = fts.graphics; // applicable rules final int length = ruleList.length; int paintCommands = 0; for (int t = 0; t < length; t++) { r = ruleList[t]; filter = r.getFilter(); if (filter == null || filter.evaluate(rf.feature)) { doElse = false; paintCommands += processSymbolizers(graphics, rf, r.symbolizers()); // bail out if we are in match first mode if (fts.matchFirst) { break; } } } // only emit a feature drawn event if we actually painted something with it, // if it has been clipped out or eliminated by the screenmap we won't emit the event instead if(paintCommands > 0) { requests.put(new FeatureRenderedRequest(rf.feature)); } if (doElse) { final int elseLength = elseRuleList.length; for (int tt = 0; tt < elseLength; tt++) { r = elseRuleList[tt]; processSymbolizers(graphics, rf, r.symbolizers()); } } } catch (Throwable tr) { fireErrorEvent(tr); } } /** * 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. * 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 */ private int processSymbolizers(final Graphics2D graphics, final RenderableFeature drawMe, final List<Symbolizer> symbolizers) throws Exception { int paintCommands = 0; for (Symbolizer symbolizer : symbolizers) { // ///////////////////////////////////////////////////////////////// // // RASTER // // ///////////////////////////////////////////////////////////////// if (symbolizer instanceof RasterSymbolizer) { // grab the grid coverage GridCoverage2D coverage = null; boolean disposeCoverage = false; try { // // // It is a grid coverage // // final Object grid = gridPropertyName.evaluate(drawMe.feature); // resolve color map entry cql expressions before getting into another thread ColorMapEntryResolver simplifier = new ColorMapEntryResolver(); symbolizer.accept(simplifier); final RasterSymbolizer rs = (RasterSymbolizer) simplifier.getCopy(); if (grid instanceof GridCoverage2D) { coverage = (GridCoverage2D) grid; if (coverage != null) { disposeCoverage = grid instanceof DisposableGridCoverage; requests.put(new RenderRasterRequest(graphics, coverage, disposeCoverage, rs, destinationCrs, worldToScreenTransform)); paintCommands++; } } else if (grid instanceof GridCoverage2DReader) { final GeneralParameterValue[] params = (GeneralParameterValue[]) paramsPropertyName .evaluate(drawMe.feature); GridCoverage2DReader reader = (GridCoverage2DReader) grid; requests.put(new RenderCoverageReaderRequest(graphics, reader, params, rs, destinationCrs, worldToScreenTransform, getRenderingInterpolation(drawMe.layer))); } } catch (IllegalArgumentException e) { LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e); fireErrorEvent(e); } } else { // ///////////////////////////////////////////////////////////////// // // FEATURE // // ///////////////////////////////////////////////////////////////// LiteShape2 shape = drawMe.getShape(symbolizer, worldToScreenTransform); if(shape == null) { continue; } if (symbolizer instanceof TextSymbolizer && drawMe.feature instanceof Feature) { labelCache.put(drawMe.layerId, (TextSymbolizer) symbolizer, drawMe.feature, shape, null); paintCommands++; } else { Style2D style = styleFactory.createStyle(drawMe.feature, symbolizer); // clip to the visible area + the size of the symbolizer (with some extra // to make sure we get no artifacts from polygon new borders) double size = RendererUtilities.getStyle2DSize(style); // take into account the meta buffer to try and clip all geometries by the same // amount double clipBuffer = Math.max(size / 2, drawMe.metaBuffer) + 10; Envelope env = new Envelope(screenSize.getMinX(), screenSize.getMaxX(), screenSize.getMinY(), screenSize.getMaxY()); env.expandBy(clipBuffer); final GeometryClipper clipper = new GeometryClipper(env); Geometry source = shape.getGeometry(); // we need to preserve the topology if we end up applying buffer for perp. offset boolean preserveTopology = style instanceof LineStyle2D && ((LineStyle2D) style).getPerpendicularOffset() != 0 && (source instanceof Polygon || source instanceof MultiPolygon); Geometry g = clipper.clipSafe(shape.getGeometry(), preserveTopology, 1); // handle perpendincular offset as needed if(style instanceof LineStyle2D && ((LineStyle2D) style).getPerpendicularOffset() != 0 && g != null && !g.isEmpty()) { LineStyle2D ls = (LineStyle2D) style; double offset = ls.getPerpendicularOffset(); // people applying an offset on a polygon really expect a buffer instead, // do so... however buffering is damn expensive, so let's apply some heuristics // to still run the offset curve builder for the simplest cases if((source instanceof Polygon || source instanceof MultiPolygon) && abs(offset) > 3) { // buffering is expensive, we can be a bit off with the // result, do simplify the geometry first Geometry simplified = TopologyPreservingSimplifier.simplify(source, Math.max(abs(offset) / 10, 1)); try { g = simplified.buffer(offset); } catch(Exception e) { LOGGER.log(Level.FINE, "Failed to apply JTS buffer to the geometry, falling back on the offset curve builder", e); OffsetCurveBuilder offseter = new OffsetCurveBuilder(offset); g = offseter.offset(g); } } else { OffsetCurveBuilder offseter = new OffsetCurveBuilder(offset); g = offseter.offset(g); } } if(g == null) { continue; } else { shape = new LiteShape2(g, null, null, false); } PaintShapeRequest paintShapeRequest = new PaintShapeRequest(graphics, shape, style, scaleDenominator); if (symbolizer.hasOption("labelObstacle")) { paintShapeRequest.setLabelObstacle(true); } requests.put(paintShapeRequest); paintCommands++; } } } return paintCommands; } /** * Builds a raster grid geometry that will be used for reading, taking into account * the original map extent and target paint area, and expanding the target raster area * by {@link #REPROJECTION_RASTER_GUTTER} * @param destinationCrs * @param sourceCRS * @return * @throws NoninvertibleTransformException */ GridGeometry2D getRasterGridGeometry(CoordinateReferenceSystem destinationCrs, CoordinateReferenceSystem sourceCRS) throws NoninvertibleTransformException { GridGeometry2D readGG; if (sourceCRS == null || destinationCrs == null || CRS.equalsIgnoreMetadata(destinationCrs, sourceCRS)) { readGG = new GridGeometry2D(new GridEnvelope2D(screenSize), originalMapExtent); } else { // reprojection involved, read a bit more pixels to account for rotation Rectangle bufferedTargetArea = (Rectangle) screenSize.clone(); bufferedTargetArea.add( // exand top/right screenSize.x + screenSize.width + REPROJECTION_RASTER_GUTTER, screenSize.y + screenSize.height + REPROJECTION_RASTER_GUTTER); bufferedTargetArea.add( // exand bottom/left screenSize.x - REPROJECTION_RASTER_GUTTER, screenSize.y - REPROJECTION_RASTER_GUTTER); // now create the final envelope accordingly readGG = new GridGeometry2D(new GridEnvelope2D(bufferedTargetArea), PixelInCell.CELL_CORNER, new AffineTransform2D( worldToScreenTransform.createInverse()), originalMapExtent.getCoordinateReferenceSystem(), null); } return readGG; } /** * 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 if (drawMe instanceof Feature) { geom = (Geometry) ((Feature) drawMe).getDefaultGeometryProperty().getValue(); } else { geom = defaultGeometryPropertyName.evaluate(drawMe, Geometry.class); } } else { geom = 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(Feature f, Symbolizer s) { FeatureType schema = f.getType(); Expression geometry = s.getGeometry(); if (geometry instanceof PropertyName) { return getAttributeCRS((PropertyName) geometry, schema); } else if (geometry == null) { return getAttributeCRS(null, schema); } else { StyleAttributeExtractor attExtractor = new StyleAttributeExtractor(); geometry.accept(attExtractor, null); for (PropertyName name : attExtractor.getAttributes()) { if (name.evaluate(schema) instanceof GeometryDescriptor) { return getAttributeCRS(name, schema); } } } 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(PropertyName geomName, FeatureType schema) { if (geomName == null || "".equals (geomName.getPropertyName())) { GeometryDescriptor geom = schema.getGeometryDescriptor(); return geom.getType().getCoordinateReferenceSystem(); } else { GeometryDescriptor geom = (GeometryDescriptor) geomName.evaluate(schema); 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 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(RENDERING_BUFFER); 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; } 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; } /** * {@inheritDoc} * * @deprecated The {@code MapContext} class is being phased out. * Please use {@link #setMapContent}. */ public void setContext(MapContext context) { // MapContext isA MapContent mapContent = context; } /** * {@inheritDoc} * * @deprecated The {@code MapContext} class is being phased out. * Please use {@link #setMapContent}. */ public MapContext getContext() { if(mapContent instanceof MapContext) { return (MapContext) mapContent; } else { MapContext context = new MapContext( mapContent ); return context; } } public void setMapContent(MapContent mapContent) { this.mapContent = mapContent; } public MapContent getMapContent() { return mapContent; } 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; } Interpolation getRenderingInterpolation(Layer currLayer) { if (currLayer != null && currLayer.getUserData().containsKey(BYLAYER_INTERPOLATION)) { return (Interpolation) currLayer.getUserData().get(BYLAYER_INTERPOLATION); } if(java2dHints == null) { return Interpolation.getInstance(Interpolation.INTERP_NEAREST); } Object interpolationHint = java2dHints.get(RenderingHints.KEY_INTERPOLATION); if (interpolationHint == null || interpolationHint == RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR) { return Interpolation.getInstance(Interpolation.INTERP_NEAREST); } else if (interpolationHint == RenderingHints.VALUE_INTERPOLATION_BILINEAR) { return Interpolation.getInstance(Interpolation.INTERP_BILINEAR); } else { return Interpolation.getInstance(Interpolation.INTERP_BICUBIC); } } /** * 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 **/ class RenderableFeature { Feature feature; Layer layer; boolean inMemoryGeneralization; ProjectionHandler projectionHandler; int metaBuffer; 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(); private ScreenMap screenMap; private String layerId; public RenderableFeature(String layerId, boolean clone) { this.layerId = layerId; this.clone = clone; } public void setScreenMap(ScreenMap screenMap) { this.screenMap = screenMap; } public void setFeature(Feature feature) { this.feature = feature; geometries.clear(); shapes.clear(); } public LiteShape2 getShape(Symbolizer symbolizer, AffineTransform at) throws FactoryException { Geometry g = findGeometry(feature, symbolizer); // pulls the geometry if (g == null || g.isEmpty()) return null; try { // process screenmap if necessary (only do it once, // the geometry will be transformed simplified in place and the screenmap // really needs to play against the original coordinates, plus, once we start // drawing a geometry we want to apply all symbolizers on it) if (screenMap != null // && !(symbolizer instanceof PointSymbolizer) // && !(g instanceof Point) && getGeometryIndex(g) == -1) { Envelope env = g.getEnvelopeInternal(); if(screenMap.canSimplify(env)) if (screenMap.checkAndSet(env)) { return null; } else { g = screenMap.getSimplifiedShape(env.getMinX(), env.getMinY(), env.getMaxX(), env.getMaxY(), g.getFactory(), g.getClass()); } } SymbolizerAssociation sa = (SymbolizerAssociation) symbolizerAssociationHT .get(symbolizer); MathTransform crsTransform = null; MathTransform atTransform = null; MathTransform fullTransform = null; if (sa == null) { sa = new SymbolizerAssociation(); sa.crs = (findGeometryCS(feature, symbolizer)); try { crsTransform = buildTransform(sa.crs, destinationCrs); atTransform = 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; if(projectionHandler != null) { sa.rxform = projectionHandler.getRenderingTransform(sa.crsxform); } else { sa.rxform = sa.crsxform; } symbolizerAssociationHT.put(symbolizer, sa); } // some shapes may be too close to projection boundaries to // get transformed, try to be lenient 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 LiteShape2 first = getTransformedShape(g, sa); if(first != null) { if(projectionHandler != null) { // at the same time, we cannot keep the geometry in screen space because // that would prevent the advanced projection handling to do its work, // to replicate the geometries across the datelines, so we transform // it back to the original Geometry tx = JTS.transform(first.getGeometry(), sa.xform.inverse()); return getTransformedShape(RendererUtilities.getCentroid(tx), sa); } else { return getTransformedShape(RendererUtilities.getCentroid(g), null); } } else { return 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(ae); return null; } } private int getGeometryIndex(Geometry g) { for (int i = 0; i < geometries.size(); i++) { if(geometries.get(i) == g) { return i; } } return -1; } private LiteShape2 getTransformedShape(Geometry originalGeom, SymbolizerAssociation sa) throws TransformException, FactoryException { int idx = getGeometryIndex(originalGeom); if(idx != -1) { return (LiteShape2) shapes.get(idx); } // 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)) { int dim = sa.crs != null ? sa.crs.getCoordinateSystem().getDimension() : 2; geom = LiteCoordinateSequence.cloneGeometry(geom, dim); } LiteShape2 shape; if(projectionHandler != null && sa != null) { // first generalize and transform the geometry into the rendering CRS geom = projectionHandler.preProcess(geom); if(geom == null) { shape = null; } else { // first generalize and transform the geometry into the rendering CRS Decimator d = getDecimator(sa.xform); geom = d.decimateTransformGeneralize(geom, sa.rxform); geom.geometryChanged(); // then post process it (provide reverse transform if available) MathTransform reverse = null; if (sa.crsxform != null) { if (sa.crsxform instanceof ConcatenatedTransform && ((ConcatenatedTransform) sa.crsxform).transform1 .getTargetDimensions() >= 3 && ((ConcatenatedTransform) sa.crsxform).transform2 .getTargetDimensions() == 2) { reverse = null; // We are downcasting 3D data to 2D data so no inverse is available } else { try { reverse = sa.crsxform.inverse(); } catch (Exception cannotReverse) { reverse = null; // reverse transform not available } } } geom = projectionHandler.postProcess(reverse, geom); if(geom == null) { shape = null; } else { // apply the affine transform turning the coordinates into pixels d = new Decimator(-1, -1); geom = d.decimateTransformGeneralize(geom, sa.axform); // wrap into a lite shape geom.geometryChanged(); shape = new LiteShape2(geom, null, null, false, false); } } } else { MathTransform 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(MathTransform 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; } } /** * A request sent to the painting thread * @author aaime */ protected abstract class RenderingRequest { abstract void execute(); } /** * A request to paint a shape with a specific Style2D * @author aaime * */ protected class PaintShapeRequest extends RenderingRequest { Graphics2D graphic; LiteShape2 shape; Style2D style; double scale; boolean labelObstacle = false; public PaintShapeRequest(Graphics2D graphic, LiteShape2 shape, Style2D style, double scale) { this.graphic = graphic; this.shape = shape; this.style = style; this.scale = scale; } public void setLabelObstacle(boolean labelObstacle) { this.labelObstacle = labelObstacle; } @Override void execute() { if(graphic instanceof DelayedBackbufferGraphic) { ((DelayedBackbufferGraphic) graphic).init(); } try { painter.paint(graphic, shape, style, scale, labelObstacle); } catch(Throwable t) { fireErrorEvent(t); } } } /** * A request to paint a shape with a specific Style2D * @author aaime * */ protected class FeatureRenderedRequest extends RenderingRequest { Object content; public FeatureRenderedRequest(Object content) { this.content = content; } @Override void execute() { fireFeatureRenderedEvent(content); } } /** * A request to merge multiple back buffers to the main graphics * @author aaime * */ protected class MergeLayersRequest extends RenderingRequest { Graphics2D graphics; List<LiteFeatureTypeStyle> lfts; public MergeLayersRequest(Graphics2D graphics, List<LiteFeatureTypeStyle> lfts) { this.graphics = graphics; this.lfts = lfts; } @Override void execute() { if (graphics instanceof DelayedBackbufferGraphic) { ((DelayedBackbufferGraphic) graphics).init(); } for (LiteFeatureTypeStyle currentLayer : lfts) { // 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. final Graphics2D ftsGraphics = currentLayer.graphics; if (ftsGraphics instanceof DelayedBackbufferGraphic && !(ftsGraphics == graphics)) { final BufferedImage image = ((DelayedBackbufferGraphic) ftsGraphics).image; // we may have not found anything to paint, in that case the delegate // has not been initialized if(image != null) { if (currentLayer.composite == null) { graphics.setComposite(AlphaComposite.SrcOver); } else { graphics.setComposite(currentLayer.composite); } graphics.drawImage(image, 0, 0, null); ftsGraphics.dispose(); } } } } } protected class MargeCompositingGroupRequest extends RenderingRequest { Graphics2D graphics; CompositingGroup compositingGroup; public MargeCompositingGroupRequest(Graphics2D graphics, CompositingGroup compositingGroup) { this.graphics = graphics; this.compositingGroup = compositingGroup; } @Override void execute() { if (graphics instanceof DelayedBackbufferGraphic) { ((DelayedBackbufferGraphic) graphics).init(); } final BufferedImage image = ((DelayedBackbufferGraphic) compositingGroup.graphics).image; // we may have not found anything to paint, in that case the delegate // has not been initialized if (image != null) { compositingGroup.graphics.dispose(); Composite composite = compositingGroup.composite; if (composite == null) { graphics.setComposite(AlphaComposite.SrcOver); } else { graphics.setComposite(composite); } graphics.drawImage(image, 0, 0, null); } } } /** * A request to render a raster * @author aaime */ protected class RenderRasterRequest extends RenderingRequest { private Graphics2D graphics; private boolean disposeCoverage; private GridCoverage2D coverage; private RasterSymbolizer symbolizer; private CoordinateReferenceSystem destinationCRS; private AffineTransform worldToScreen; public RenderRasterRequest(Graphics2D graphics, GridCoverage2D coverage, boolean disposeCoverage, RasterSymbolizer symbolizer, CoordinateReferenceSystem destinationCRS, AffineTransform worldToScreen) { this.graphics = graphics; this.coverage = coverage; this.disposeCoverage = disposeCoverage; this.symbolizer = symbolizer; this.destinationCRS = destinationCRS; this.worldToScreen = worldToScreen; } @Override void execute() { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Rendering Raster " + coverage); } if (graphics instanceof DelayedBackbufferGraphic) { ((DelayedBackbufferGraphic) graphics).init(); } 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); try { gcr.paint(graphics, coverage, symbolizer); } finally { // we need to try and dispose this coverage if was created on purpose for // rendering if (coverage != null && disposeCoverage) { coverage.dispose(true); final RenderedImage image = coverage.getRenderedImage(); if(image instanceof PlanarImage) { ImageUtilities.disposePlanarImageChain((PlanarImage) image); } } } if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Raster rendered"); } } catch (Exception e) { LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e); fireErrorEvent(e); } } } /** * A request to render a raster * * @author aaime */ protected class RenderCoverageReaderRequest extends RenderingRequest { private Graphics2D graphics; private GridCoverage2DReader reader; private RasterSymbolizer symbolizer; private CoordinateReferenceSystem destinationCRS; private AffineTransform worldToScreen; private GeneralParameterValue[] readParams; private Interpolation interpolation; public RenderCoverageReaderRequest(Graphics2D graphics, GridCoverage2DReader reader, GeneralParameterValue[] readParams, RasterSymbolizer symbolizer, CoordinateReferenceSystem destinationCRS, AffineTransform worldToScreen, Interpolation interpolation) { this.graphics = graphics; this.reader = reader; this.readParams = readParams; this.symbolizer = symbolizer; this.destinationCRS = destinationCRS; this.worldToScreen = worldToScreen; this.interpolation = interpolation; } @Override void execute() { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Rendering reader " + reader); } if (graphics instanceof DelayedBackbufferGraphic) { ((DelayedBackbufferGraphic) graphics).init(); } 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); // Checks on the Reprojection parameters gcr.setAdvancedProjectionHandlingEnabled(isAdvancedProjectionHandlingEnabled()); gcr.setWrapEnabled(isMapWrappingEnabled()); gcr.paint(graphics, reader, readParams, symbolizer, interpolation, null); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Raster rendered"); } } catch (Exception e) { LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e); fireErrorEvent(e); } } } protected class RenderDirectLayerRequest extends RenderingRequest { private final Graphics2D graphics; private final DirectLayer layer; public RenderDirectLayerRequest(Graphics2D graphics, DirectLayer layer) { this.graphics = graphics; this.layer = layer; } @Override void execute() { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Rendering DirectLayer: " + layer); } if (graphics instanceof DelayedBackbufferGraphic) { ((DelayedBackbufferGraphic) graphics).init(); } try { layer.draw(graphics, mapContent, mapContent.getViewport()); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Layer rendered"); } } catch (Exception e) { LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e); fireErrorEvent(e); } } } /** * Marks the end of the request flow, instructs the painting thread to exit * @author Andrea Aime - OpenGeo */ protected class EndRequest extends RenderingRequest { @Override void execute() { // nothing to do here } } /** * The secondary thread that actually issues the paint requests against the graphic object * @author aaime * */ class PainterThread implements Runnable { BlockingQueue<RenderingRequest> requests; Thread thread; public PainterThread(BlockingQueue<RenderingRequest> requests) { this.requests = requests; } public void interrupt() { if(thread != null) { thread.interrupt(); } } public void run() { thread = Thread.currentThread(); boolean done = false; while(!done) { try { List<RenderingRequest> localRequests = new ArrayList<StreamingRenderer.RenderingRequest>(); RenderingRequest request = requests.take(); requests.drainTo(localRequests); localRequests.add(0, request); for (RenderingRequest r : localRequests) { if(r instanceof EndRequest || renderingStopRequested) { done = true; break; } else { r.execute(); } } } catch(InterruptedException e) { // ok, we might have been interrupted to stop processing if(renderingStopRequested) { done = true; } } catch(Throwable t) { fireErrorEvent(t); } } } } /** * A blocking queue subclass with a special behavior for the occasion when the * rendering stop has been requested: puts are getting ignored, and take always * returns an EndRequest * * @author Andrea Aime - GeoSolutions * */ public class RenderingBlockingQueue extends ArrayBlockingQueue<RenderingRequest> { private static final long serialVersionUID = 4908029658595573833L; public RenderingBlockingQueue(int capacity) { super(capacity); } @Override public void put(RenderingRequest e) throws InterruptedException { if(!renderingStopRequested) { super.put(e); if(renderingStopRequested) { this.clear(); } } } @Override public RenderingRequest take() throws InterruptedException { if(!renderingStopRequested) { return super.take(); } else { return new EndRequest(); } } @Override public int drainTo(Collection<? super RenderingRequest> list) { if(!renderingStopRequested) { return super.drainTo(list); } else { list.clear(); list.add(new EndRequest()); return 1; } } } }