/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2011, Geomatys * * 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.geotoolkit.display2d.container.stateless; import java.awt.Graphics2D; import java.awt.Image; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.SampleModel; import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.logging.Level; import org.geotoolkit.display.canvas.RenderingContext; import org.geotoolkit.display.VisitFilter; import org.geotoolkit.display.canvas.control.CanvasMonitor; import org.geotoolkit.display.PortrayalException; import org.geotoolkit.display.SearchArea; import org.geotoolkit.display2d.GO2Hints; import org.geotoolkit.display2d.GO2Utilities; import static org.geotoolkit.display2d.GO2Utilities.*; import org.geotoolkit.display2d.canvas.J2DCanvas; import org.geotoolkit.display2d.canvas.RenderingContext2D; import org.geotoolkit.display2d.container.ContextContainer2D; import org.geotoolkit.display2d.primitive.DefaultProjectedObject; import org.geotoolkit.display2d.primitive.GraphicJ2D; import org.geotoolkit.display2d.primitive.ProjectedObject; import org.geotoolkit.display2d.style.CachedRule; import org.geotoolkit.display2d.style.CachedSymbolizer; import org.geotoolkit.display2d.style.renderer.SymbolizerRenderer; import org.geotoolkit.factory.FactoryFinder; import org.geotoolkit.filter.identity.DefaultFeatureId; import org.geotoolkit.map.CollectionMapLayer; import org.geotoolkit.map.GraphicBuilder; import org.geotoolkit.style.MutableRule; import org.geotoolkit.style.MutableStyle; import org.opengis.display.primitive.Graphic; import org.opengis.filter.Filter; import org.opengis.filter.expression.Literal; import org.opengis.filter.identity.FeatureId; import org.opengis.style.Rule; import org.opengis.style.Style; import org.opengis.style.Symbolizer; import org.opengis.style.TextSymbolizer; import org.apache.sis.internal.feature.AttributeConvention; import org.opengis.feature.FeatureType; /** * Single object to represent a collection map layer * This is a Stateless graphic object. * * @author johann sorel (Geomatys) * @module */ public class StatelessCollectionLayerJ2D<T extends CollectionMapLayer> extends StatelessMapLayerJ2D<T>{ private static final Literal ID_EXPRESSION = FactoryFinder.getFilterFactory(null).literal(AttributeConvention.IDENTIFIER_PROPERTY.toString()); protected final StatelessContextParams params; public StatelessCollectionLayerJ2D(final J2DCanvas canvas, final T layer){ super(canvas, layer, false); params = new StatelessContextParams(canvas,layer); } /** * {@inheritDoc } */ @Override public void paintLayer(final RenderingContext2D renderingContext) { //search for a special graphic renderer final GraphicBuilder<GraphicJ2D> builder = (GraphicBuilder<GraphicJ2D>) item.getGraphicBuilder(GraphicJ2D.class); if(builder != null){ //let the parent class handle it super.paintLayer(renderingContext); return; } //first extract the valid rules at this scale final List<Rule> validRules = getValidRules(renderingContext,item,null); //we perform a first check on the style to see if there is at least //one valid rule at this scale, if not we just continue. if(validRules.isEmpty()){ return; } final CachedRule[] rules = toCachedRules(validRules, null); final Set<String> attributs = GO2Utilities.propertiesCachedNames(rules); final Collection<?> candidates; try { candidates = optimizeCollection(renderingContext, attributs, validRules); } catch (Exception ex) { renderingContext.getMonitor().exceptionOccured(ex, Level.WARNING); //can not continue this layer with this error return; } paintVectorLayer(rules, candidates,renderingContext); } /** * @return the valid rules at this scale, selection rules will be mixed in. */ protected static List<Rule> getValidRules(final RenderingContext2D renderingContext, final CollectionMapLayer item, final FeatureType type){ final List<Rule> normalRules = GO2Utilities.getValidRules( item.getStyle(), renderingContext.getSEScale(), type); final Filter selectionFilter = item.getSelectionFilter(); if(selectionFilter != null && !Filter.EXCLUDE.equals(selectionFilter)){ //merge the style and filter with the selection final List<Rule> selectionRules; final List<Rule> mixedRules = new ArrayList<Rule>(); final MutableStyle selectionStyle = item.getSelectionStyle(); if(selectionStyle == null){ selectionRules = GO2Utilities.getValidRules( ContextContainer2D.DEFAULT_SELECTION_STYLE, renderingContext.getSEScale(), type); }else{ selectionRules = GO2Utilities.getValidRules( selectionStyle, renderingContext.getSEScale(), type); } //update the rules filters for(final Rule rule : selectionRules){ final List<? extends Symbolizer> symbols = rule.symbolizers(); final MutableRule mixedRule = STYLE_FACTORY.rule(symbols.toArray(new Symbolizer[symbols.size()])); Filter f = rule.getFilter(); if(f == null){ f = selectionFilter; }else{ f = FILTER_FACTORY.and(f,selectionFilter); } mixedRule.setFilter(f); mixedRules.add(mixedRule); } final Filter notSelectionFilter = FILTER_FACTORY.not(selectionFilter); for(final Rule rule : normalRules){ final MutableRule mixedRule = STYLE_FACTORY.rule(rule.symbolizers().toArray(new Symbolizer[0])); Filter f = rule.getFilter(); if(f == null){ f = notSelectionFilter; }else{ f = FILTER_FACTORY.and(f,notSelectionFilter); } mixedRule.setFilter(f); mixedRules.add(mixedRule); } return mixedRules; } return normalRules; } protected static CachedRule[] toCachedRules(Collection<? extends Rule> rules, final FeatureType expected){ final CachedRule[] cached = new CachedRule[rules.size()]; int i=0; for(Rule r : rules){ cached[i] = getCached(r, expected); i++; } return cached; } protected CachedRule[] prepareStyleRules(final RenderingContext2D renderingContext, final CollectionMapLayer layer, final FeatureType type){ final CachedRule[] rules; final Style style = item.getStyle(); final Filter selectionFilter = item.getSelectionFilter(); if(selectionFilter != null && !Filter.EXCLUDE.equals(selectionFilter)){ //merge the style and filter with the selection final List<Rule> selectionRules; final List<Rule> normalRules = GO2Utilities.getValidRules( style, renderingContext.getSEScale(), type); final List<CachedRule> mixedRules = new ArrayList<CachedRule>(); final MutableStyle selectionStyle = item.getSelectionStyle(); if(selectionStyle == null){ selectionRules = GO2Utilities.getValidRules( ContextContainer2D.DEFAULT_SELECTION_STYLE, renderingContext.getSEScale(), type); }else{ selectionRules = GO2Utilities.getValidRules( selectionStyle, renderingContext.getSEScale(), type); } //update the rules filters for(final Rule rule : selectionRules){ final List<? extends Symbolizer> symbols = rule.symbolizers(); final MutableRule mixedRule = STYLE_FACTORY.rule(symbols.toArray(new Symbolizer[symbols.size()])); Filter f = rule.getFilter(); if(f == null){ f = selectionFilter; }else{ f = FILTER_FACTORY.and(f,selectionFilter); } mixedRule.setFilter(f); mixedRules.add(GO2Utilities.getCached(mixedRule,type)); } final Filter notSelectionFilter = FILTER_FACTORY.not(selectionFilter); for(final Rule rule : normalRules){ final MutableRule mixedRule = STYLE_FACTORY.rule(rule.symbolizers().toArray(new Symbolizer[0])); Filter f = rule.getFilter(); if(f == null){ f = notSelectionFilter; }else{ f = FILTER_FACTORY.and(f,notSelectionFilter); } mixedRule.setFilter(f); mixedRules.add(GO2Utilities.getCached(mixedRule,type)); } rules = mixedRules.toArray(new CachedRule[mixedRules.size()]); }else{ rules = GO2Utilities.getValidCachedRules( style, renderingContext.getSEScale(), type); } return rules; } protected StatelessContextParams getStatefullParameters(final RenderingContext2D context){ params.update(context); return params; } protected Collection<?> optimizeCollection(final RenderingContext2D context, final Set<String> requieredAtts, final List<Rule> rules) throws Exception { return item.getCollection(); } protected void paintVectorLayer(final CachedRule[] rules, final Collection<?> candidates, final RenderingContext2D context) { final CanvasMonitor monitor = context.getMonitor(); //we do not check if the collection is empty or not since //it can be a very expensive operation //prepare the rendering parameters final StatelessContextParams params = getStatefullParameters(context); if(monitor.stopRequested()) return; //check if we have group symbolizers, if it's the case we must render by symbol order. boolean symbolOrder = false; for(CachedRule rule : rules){ for(CachedSymbolizer symbolizer : rule.symbolizers()){ if(symbolizer.getRenderer().isGroupSymbolizer()){ symbolOrder = true; break; } } } symbolOrder = symbolOrder || Boolean.TRUE.equals(canvas.getRenderingHint(GO2Hints.KEY_SYMBOL_RENDERING_ORDER)); if(symbolOrder){ try{ renderBySymbolOrder(candidates, context, rules, params); }catch(PortrayalException ex){ monitor.exceptionOccured(ex, Level.WARNING); } }else{ try{ renderByObjectOrder(candidates, context, rules, params); }catch(PortrayalException ex){ monitor.exceptionOccured(ex, Level.WARNING); } } } /** * Render by object order. * @param candidates * @param renderers * @param context * @param params * @throws PortrayalException */ protected final void renderByObjectOrder(final Collection<?> candidates, final RenderingContext2D context, final CachedRule[] rules, final StatelessContextParams params) throws PortrayalException{ final RenderingIterator statefullIterator = getIterator(candidates, context, params); renderByObjectOrder(statefullIterator, context, rules); } protected final void renderByObjectOrder(final RenderingIterator statefullIterator, final RenderingContext2D context, final CachedRule[] rules) throws PortrayalException{ final CanvasMonitor monitor = context.getMonitor(); //prepare the renderers final DefaultCachedRule renderers = new DefaultCachedRule(rules, context); try{ //performance routine, only one symbol to render if(renderers.rules.length == 1 && (renderers.rules[0].getFilter() == null || renderers.rules[0].getFilter() == Filter.INCLUDE) && renderers.rules[0].symbolizers().length == 1){ renderers.renderers[0][0].portray(statefullIterator); return; } while(statefullIterator.hasNext()){ if(monitor.stopRequested()) return; final ProjectedObject projectedCandidate = statefullIterator.next(); boolean painted = false; for(int i=0; i<renderers.elseRuleIndex; i++){ final CachedRule rule = renderers.rules[i]; final Filter ruleFilter = rule.getFilter(); //test if the rule is valid for this feature if (ruleFilter == null || ruleFilter.evaluate(projectedCandidate.getCandidate())) { painted = true; for (final SymbolizerRenderer renderer : renderers.renderers[i]) { renderer.portray(projectedCandidate); } } } //the feature hasn't been painted, paint it with the 'else' rules if(!painted){ for(int i=renderers.elseRuleIndex; i<renderers.rules.length; i++){ final CachedRule rule = renderers.rules[i]; final Filter ruleFilter = rule.getFilter(); //test if the rule is valid for this feature if (ruleFilter == null || ruleFilter.evaluate(projectedCandidate.getCandidate())) { for (final SymbolizerRenderer renderer : renderers.renderers[i]) { renderer.portray(projectedCandidate); } } } } } }finally{ try { statefullIterator.close(); } catch (IOException ex) { getLogger().log(Level.WARNING, null, ex); } } } /** * render by symbol order. */ protected final void renderBySymbolOrder(final Collection<?> candidates, final RenderingContext2D context, final CachedRule[] rules, final StatelessContextParams params) throws PortrayalException { //performance routine, only one symbol to render if(rules.length == 1 && (rules[0].getFilter() == null || rules[0].getFilter() == Filter.INCLUDE) && rules[0].symbolizers().length == 1){ final RenderingIterator statefullIterator = getIterator(candidates, context, params); final CachedSymbolizer s = rules[0].symbolizers()[0]; final SymbolizerRenderer renderer = s.getRenderer().createRenderer(s, context); renderer.portray(statefullIterator); try { statefullIterator.close(); } catch (IOException ex) { getLogger().log(Level.WARNING, null, ex); } return; } renderBySymbolIndexInRule(candidates,context,rules,params); } /** * Render by symbol index order in a single pass, this results in creating a buffered image * for each symbolizer depth, the maximum number of buffer is the maximum number of symbolizer a rule contain. */ private void renderBySymbolIndexInRule(final Collection<?> candidates, final RenderingContext2D context, final CachedRule[] rules, final StatelessContextParams params) throws PortrayalException { final RenderingIterator statefullIterator = getIterator(candidates, context, params); renderBySymbolIndexInRule(candidates,statefullIterator, context, rules); } /** * Render by symbol index order in a single pass, this results in creating a buffered image * for each symbolizer depth, the maximum number of buffer is the maximum number of symbolizer a rule contain. */ private void renderBySymbolIndexInRule(final Collection<?> candidates,final RenderingIterator statefullIterator, final RenderingContext2D context, final CachedRule[] rules) throws PortrayalException { final CanvasMonitor monitor = context.getMonitor(); final int elseRuleIndex = DefaultCachedRule.sortByElseRule(rules); //store the ids of the features painted during the first round ----------------------------- final BufferedImage originalBuffer = (BufferedImage) context.getCanvas().getSnapShot(); final ColorModel cm = ColorModel.getRGBdefault(); final SampleModel sm = cm.createCompatibleSampleModel(originalBuffer.getWidth(), originalBuffer.getHeight()); final RenderingContext2D originalContext = context; final List<BufferedImage> images = new ArrayList<BufferedImage>(); final List<RenderingContext2D> ctxs = new ArrayList<RenderingContext2D>(); images.add(originalBuffer); ctxs.add(context); final SymbolizerRenderer[][] renderers = new SymbolizerRenderer[rules.length][0]; for(int i=0;i<rules.length;i++){ final CachedRule cr = rules[i]; final CachedSymbolizer[] css = cr.symbolizers(); //do not count text symbolizers at the end int len = css.length; for(int k=css.length-1;k>=0;k--){ if(css[k].getSource() instanceof TextSymbolizer){ len--; }else{ break; } } if(len > images.size()){ for(int k=images.size();k<len;k++){ final BufferedImage layer = createBufferedImage(cm, sm); images.add(k, layer); ctxs.add(k, context.create( ((Graphics2D)layer.getGraphics()) )); } } renderers[i] = new SymbolizerRenderer[css.length]; for(int k=0;k<css.length;k++){ if(css[k].getSource() instanceof TextSymbolizer){ //use the original context renderers[i][k] = css[k].getRenderer().createRenderer(css[k],context); }else{ renderers[i][k] = css[k].getRenderer().createRenderer(css[k],ctxs.get(k)); } } } try{ while(statefullIterator.hasNext()){ if(monitor.stopRequested()) return; final ProjectedObject projectedCandidate = statefullIterator.next(); boolean painted = false; for(int i=0; i<elseRuleIndex; i++){ final CachedRule rule = rules[i]; final Filter ruleFilter = rule.getFilter(); //test if the rule is valid for this feature if (ruleFilter == null || ruleFilter.evaluate(projectedCandidate.getCandidate())) { painted = true; final CachedSymbolizer[] css = rule.symbolizers(); for(int k=0; k<css.length; k++){ renderers[i][k].portray(projectedCandidate); } } } //paint with else rules if(!painted){ for(int i=elseRuleIndex; i<rules.length; i++){ final CachedRule rule = rules[i]; final Filter ruleFilter = rule.getFilter(); //test if the rule is valid for this feature if (ruleFilter == null || ruleFilter.evaluate(projectedCandidate.getCandidate())) { final CachedSymbolizer[] css = rule.symbolizers(); for(int k=0; k<css.length; k++){ renderers[i][k].portray(projectedCandidate); } } } } } //paint group symbolizers for(int i=0; i<elseRuleIndex; i++){ final CachedRule rule = rules[i]; final CachedSymbolizer[] css = rule.symbolizers(); for(int k=0; k<css.length; k++){ if(renderers[i][k].getService().isGroupSymbolizer()){ final RenderingIterator ite = getIterator(candidates, context, params); renderers[i][k].portray(ite); } } } }finally{ try { statefullIterator.close(); } catch (IOException ex) { getLogger().log(Level.WARNING, null, ex); } } //merge images -------------------------- originalContext.switchToDisplayCRS(); final Graphics2D g = originalContext.getGraphics(); g.setComposite(ALPHA_COMPOSITE_1F); for(int i=1;i<images.size();i++){ final Image img = images.get(i); g.drawImage(img, 0, 0, null); recycleBufferedImage((BufferedImage)img); } } protected boolean contain(final Set<FeatureId> ids, final Object candidate){ return ids.contains(id(candidate)); } protected FeatureId id(final Object candidate){ final Object obj = ID_EXPRESSION.evaluate(candidate); if(obj != null){ return new DefaultFeatureId(obj.toString()); }else{ return null; } } /** * {@inheritDoc } */ @Override public List<Graphic> getGraphicAt(final RenderingContext rdcontext, final SearchArea mask, final VisitFilter filter, final List<Graphic> graphics) { return graphics; } protected RenderingIterator getIterator(final Collection<?> features, final RenderingContext2D renderingContext, final StatelessContextParams params){ final Iterator<?> iterator = features.iterator(); final DefaultProjectedObject projectedFeature = new DefaultProjectedObject(params); return new GraphicIterator(iterator, projectedFeature); } protected static interface RenderingIterator extends Iterator<ProjectedObject>,Closeable{} protected class GraphicIterator implements RenderingIterator{ private final Iterator<?> ite; private final DefaultProjectedObject projected; public GraphicIterator(final Iterator<?> ite, final DefaultProjectedObject projected) { this.ite = ite; this.projected = projected; } @Override public boolean hasNext() { return ite.hasNext(); } @Override public ProjectedObject next() { projected.setCandidate(ite.next()); return projected; } @Override public void remove() { throw new UnsupportedOperationException("Not supported."); } @Override public void close() throws IOException { if(ite instanceof Closeable){ ((Closeable)ite).close(); } } } }