/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2015, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.renderer.lite; import java.awt.Composite; import java.awt.Graphics2D; import java.awt.geom.NoninvertibleTransformException; import java.io.IOException; import java.util.ArrayList; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.logging.Level; import org.geotools.data.FeatureSource; import org.geotools.data.sort.SortedFeatureReader; import org.geotools.feature.FeatureCollection; import org.geotools.feature.SchemaException; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.map.FeatureLayer; import org.geotools.map.Layer; import org.geotools.renderer.style.SLDStyleFactory; import org.geotools.styling.FeatureTypeStyle; import org.geotools.styling.Style; import org.geotools.styling.visitor.DuplicatingStyleVisitor; import org.geotools.util.DefaultProgressListener; import org.opengis.feature.type.FeatureType; import org.opengis.feature.type.PropertyDescriptor; import org.opengis.filter.expression.PropertyName; import org.opengis.filter.sort.SortBy; import org.opengis.filter.sort.SortOrder; import org.opengis.referencing.FactoryException; import org.opengis.referencing.operation.TransformException; import org.opengis.util.ProgressListener; /** * A special layer owning multiple feature sources and styles, all in the same z-group * * @author Andrea Aime - GeoSolutions * */ class ZGroupLayer extends Layer { private String groupId; private List<Layer> layers = new ArrayList<>(); private boolean compositingBase = false; private Composite composite; public ZGroupLayer(String groupId, FeatureLayer layer) { this.groupId = groupId; addLayer(layer); } public void drawFeatures(Graphics2D graphics, final StreamingRenderer renderer, String layerId) throws IOException, FactoryException, NoninvertibleTransformException, SchemaException, TransformException { // 1) init all the readers and the lfts associated to them (one at a time to avoid deadlock) // and create one RenderableFeature for each // 2) process all the features one z-level at a time, backtracking if there are multiple // fts for a certain layer. // a listener passed around to stop data reading/painting if rendering stop request is // issued ProgressListener cancellationListener = new DefaultProgressListener() { public boolean isCanceled() { return renderer.renderingStopRequested; }; }; List<ZGroupLayerPainter> painters = null; try { painters = buildLayerPainters(graphics, renderer, layerId, cancellationListener); if (painters.isEmpty()) { return; } // get a comparator to find the first key to paint Comparator<SortKey> comparator = SortKey.buildComparator(painters.get(0).sortBy); // paint all the features as we can SortKey previousKey = null; while (!painters.isEmpty()) { SortKey smallestKey = getSmallestKey(painters, comparator); if (previousKey == null) { previousKey = smallestKey; } else if(comparator.compare(previousKey, smallestKey) >= 0) { throw new IllegalStateException("The sorted rendering moved from a set of " + "sort attributes, to one that's equal or greater, this is unexpected, " + "bailing out to avoid an infinite loop"); } else { previousKey = smallestKey; } for (Iterator it = painters.iterator(); it.hasNext();) { ZGroupLayerPainter painter = (ZGroupLayerPainter) it.next(); painter.paintKey(smallestKey); // if the painter is done, close it if (painter.complete()) { painter.close(); it.remove(); } } } } finally { if (painters != null) { for (ZGroupLayerPainter painter : painters) { painter.close(); } } } } private SortKey getSmallestKey(List<ZGroupLayerPainter> painters, Comparator<SortKey> comparator) { SortKey smallest = null; for (ZGroupLayerPainter painter : painters) { SortKey key = painter.getCurrentKey(); if (smallest == null) { smallest = key; } else if (comparator.compare(key, smallest) < 0) { smallest = key; } } return new SortKey(smallest); } private List<ZGroupLayerPainter> buildLayerPainters(Graphics2D graphics, StreamingRenderer renderer, String layerId, ProgressListener cancellationListener) throws IOException, FactoryException, NoninvertibleTransformException, SchemaException, TransformException { List<ZGroupLayerPainter> painters = new ArrayList<>(); boolean closePainters = true; try { for (Layer layer : layers) { // get the LiteFeatureTypeStyle for this layer final FeatureSource featureSource = layer.getFeatureSource(); if (featureSource == null) { throw new IllegalArgumentException( "The layer does not contain a feature source"); } final FeatureType schema = featureSource.getSchema(); final ArrayList<LiteFeatureTypeStyle> lfts = renderer .createLiteFeatureTypeStyles(layer, graphics, false); if (lfts.isEmpty()) { continue; } else { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine( "Processing " + lfts.size() + " stylers for " + schema.getName()); } } // get the feature iterator we need FeatureCollection features = renderer.getFeatures(layer, schema, lfts); // While we could use a non mark feature iterator for single fts layers, // that would cause multiple connections to be open at the same time, // which in turn could cause deadlocks against connection pools, so we // are going to build a MarkFeatureIterator regardless // TODO: we could optimize down to simple streaming painting if we end up // with a single painter with a single fts (due to scale dependencies) // but we'd have to delay opening the MarkFeatureIterator to recognize the // situation int maxFeatures = SortedFeatureReader.getMaxFeaturesInMemory(layer.getQuery()); MarkFeatureIterator fi = MarkFeatureIterator.create(features, maxFeatures, cancellationListener); if (fi.hasNext()) { ZGroupLayerPainter painter = new ZGroupLayerPainter(fi, lfts, renderer, layerId); painters.add(painter); } else { fi.close(); } } validateSortBy(painters); // got to the end cleanly, no need to close the painters accumulated so far closePainters = false; } finally { if (closePainters) { for (ZGroupLayerPainter painter : painters) { try { painter.close(); } catch (Exception e) { LOGGER.log(Level.FINE, "Failed to close cleanly layer painter " + painter, e); } } } } return painters; } /** * Ensures that all SortBy are meaningful for a cross layer z-order. We need the SortKey for all * the layers to have the same structure, be comparable, and be class compatible with each other * (and of course, exist in the first place) * * @param painters */ private void validateSortBy(List<ZGroupLayerPainter> painters) { Class[] referenceClasses = null; SortOrder[] referenceOrders = null; LiteFeatureTypeStyle reference = null; for (ZGroupLayerPainter painter : painters) { for (LiteFeatureTypeStyle style : painter.lfts) { Class[] styleClasses = getSortByAttributeClasses(style); SortOrder[] styleOrders = getSortOrders(style); if (referenceClasses == null) { referenceClasses = styleClasses; referenceOrders = styleOrders; reference = style; for (int i = 0; i < referenceClasses.length; i++) { if (!Comparable.class.isAssignableFrom(referenceClasses[i])) { throw new IllegalArgumentException( "Found non comparable attribute in z group " + groupId + ": " + sortByToString(style, getSortByAttributeClasses(style)) + " at position " + (i + 1)); } } } else { if (styleClasses.length != referenceClasses.length) { throw new IllegalArgumentException( "Found two sortBy clauses with different number " + "of attributes in group " + groupId + ": " + sortByToString(reference, referenceClasses) + " vs " + sortByToString(style, styleClasses)); } else { for (int i = 0; i < styleClasses.length; i++) { Class currClass = styleClasses[i]; Class referenceClass = referenceClasses[i]; if (!currClass.equals(referenceClass) && !currClass.isAssignableFrom(referenceClass) && !referenceClass.isAssignableFrom(currClass)) { throw new IllegalArgumentException( "Found two incompatible classes at position " + (i + 1) + " of the sortBy clauses in group " + groupId + ": " + sortByToString(reference, referenceClasses) + " vs " + sortByToString(style, styleClasses)); } SortOrder currOrder = styleOrders[i]; SortOrder referenceOrder = referenceOrders[i]; if (!currOrder.equals(referenceOrder)) { throw new IllegalArgumentException( "Found two different sort orders at position " + (i + 1) + " of the sortBy clauses in group " + groupId + ": " + sortByToString(reference, referenceClasses) + " vs " + sortByToString(style, styleClasses)); } } } } } } } private Class[] getSortByAttributeClasses(LiteFeatureTypeStyle style) { SortBy[] sb = style.sortBy; FeatureType schema = style.layer.getFeatureSource().getSchema(); Class[] classes = new Class[sb.length]; for (int i = 0; i < classes.length; i++) { PropertyName property = sb[i].getPropertyName(); if (property == null) { // natural sorts classes[i] = String.class; } else { PropertyDescriptor pd = property.evaluate(schema, null); if (pd == null) { throw new IllegalArgumentException( "Property " + property + " could not be found in feature type " + schema.getName() + " in layer " + style.layer.getTitle()); } classes[i] = pd.getType().getBinding(); } } return classes; } private SortOrder[] getSortOrders(LiteFeatureTypeStyle style) { SortBy[] sb = style.sortBy; SortOrder[] orders = new SortOrder[sb.length]; for (int i = 0; i < orders.length; i++) { orders[i] = sb[i].getSortOrder(); } return orders; } private String sortByToString(LiteFeatureTypeStyle style, Class[] classes) { StringBuilder sb = new StringBuilder("Layer ").append(style.layer.getTitle()).append("["); SortBy[] sortBy = style.sortBy; for (int i = 0; i < sortBy.length; i++) { SortBy curr = sortBy[i]; if (curr == SortBy.NATURAL_ORDER) { sb.append("NaturalOrder"); } else if (curr == SortBy.REVERSE_ORDER) { sb.append("ReverseNaturalOrder"); } else { sb.append(curr.getPropertyName().getPropertyName()); sb.append("(").append(classes[i].getSimpleName()).append(")"); if (curr.getSortOrder() == SortOrder.DESCENDING) { sb.append(" D"); } } if (i < sortBy.length) { sb.append(", "); } } sb.append("]"); return sb.toString(); } @Override public ReferencedEnvelope getBounds() { // no bounds to report return null; } public boolean isCompositingBase() { return compositingBase; } public Composite getComposite() { return composite; } public String getGroupId() { return groupId; } public void addLayer(FeatureLayer layer) { List<FeatureTypeStyle> featureTypeStyles = layer.getStyle().featureTypeStyles(); boolean cleanupStyle = false; for (FeatureTypeStyle fts : featureTypeStyles) { Map<String, String> options = fts.getOptions(); String compositingBaseDefinition = options.get(FeatureTypeStyle.COMPOSITE_BASE); if("true".equalsIgnoreCase(compositingBaseDefinition)) { this.compositingBase = true; } // cannot really rely on equals here, we use a simple "last one wins" logic Composite composite = SLDStyleFactory.getComposite(options); if(composite != null) { this.composite = composite; cleanupStyle = true; } } // compositing is now handled at the ZGroupLayer level, remove it from the // inner layer if(cleanupStyle) { DuplicatingStyleVisitor cleaner = new DuplicatingStyleVisitor() { @Override public void visit(FeatureTypeStyle fts) { super.visit(fts); FeatureTypeStyle copy = (FeatureTypeStyle) pages.peek(); copy.getOptions().remove(FeatureTypeStyle.COMPOSITE); copy.getOptions().remove(FeatureTypeStyle.COMPOSITE_BASE); } }; layer.getStyle().accept(cleaner); Style cleaned = (Style) cleaner.getCopy(); layer.setStyle(cleaned); } layers.add(layer); } }