/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2014, 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.styling.css; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import org.geotools.factory.CommonFactoryFinder; import org.geotools.filter.text.ecql.ECQL; import org.geotools.filter.visitor.SimplifyingFilterVisitor; import org.geotools.styling.css.selector.Data; import org.geotools.styling.css.selector.Or; import org.geotools.styling.css.selector.ScaleRange; import org.geotools.styling.css.selector.Selector; import org.geotools.styling.css.util.OgcFilterBuilder; import org.geotools.styling.css.util.ScaleRangeExtractor; import org.geotools.styling.css.util.UnboundSimplifyingFilterVisitor; import org.geotools.util.NumberRange; import org.geotools.util.Range; import org.opengis.feature.type.FeatureType; import org.opengis.filter.And; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory2; /** * Represents the current coverage of the scale/filter domain, and has helper methods to add * {@link CssRule} to the covereage, split them into subrules by scale ranges, compare them with the * existing coverage, and genenerate rules matching only what's left to be covered. * * @author Andrea Aime - GeoSolutions * */ class DomainCoverage { /** * The full range of scales possible. Once this is covered, the whole domain is */ static final NumberRange<Double> FULL_SCALE_RANGE = new NumberRange<Double>(Double.class, 0d, Double.POSITIVE_INFINITY); /** * A simplified representation of a Selector that takes apart the three main components, scale * range, filter and pseudoClass, to make it compatible with the SLD filtering model. A Selector * expressed in CSS language can be converted into a list of these. * * @author Andrea Aime - GeoSolutions * */ class SLDSelector { NumberRange<Double> scaleRange; Filter filter; public SLDSelector(NumberRange<?> scaleRange, Filter filter) { this.scaleRange = new NumberRange(Double.class, scaleRange.getMinimum(), scaleRange.isMinIncluded(), scaleRange.getMaximum(), scaleRange.isMaxIncluded()); this.filter = filter; } /** * Returns a list of scale dependent filters that represent the difference (the uncovered * area) between this {@link SLDSelector} and then specified rule * * @param rule * @return */ public List<SLDSelector> difference(SLDSelector other) { List<SLDSelector> result = new ArrayList<>(); // fast interaction tests if (!this.scaleRange.intersects(other.scaleRange)) { return Collections.singletonList(this); } // first case, portions of scale range not overlapping NumberRange<?>[] scaleRangeDifferences = this.scaleRange.subtract(other.scaleRange); for (NumberRange<?> scaleRangeDifference : scaleRangeDifferences) { result.add(new SLDSelector(scaleRangeDifference, this.filter)); } // second case, scale ranges overlapping, but filter/pseudoclass not NumberRange<?> scaleRangeIntersection = this.scaleRange.intersect(other.scaleRange); if (scaleRangeIntersection != null && !scaleRangeIntersection.isEmpty()) { And difference = FF.and(this.filter, FF.not(other.filter)); Filter simplifiedDifference = simplify(difference); if (simplifiedDifference != Filter.EXCLUDE) { result.add(new SLDSelector(scaleRangeIntersection, simplifiedDifference)); } } return result; } @Override public String toString() { return "SLDSelector [scaleRange=" + scaleRange + ", filter=" + ECQL.toCQL(filter) + "]"; } public Selector toSelector(SimplifyingFilterVisitor visitor) { Selector selector = Selector.and(new ScaleRange(scaleRange), new Data(filter), visitor); return selector; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + getOuterType().hashCode(); result = prime * result + ((filter == null) ? 0 : filter.hashCode()); result = prime * result + ((scaleRange == null) ? 0 : scaleRange.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; SLDSelector other = (SLDSelector) obj; if (!getOuterType().equals(other.getOuterType())) return false; if (filter == null) { if (other.filter != null) return false; } else if (!filter.equals(other.filter)) return false; if (scaleRange == null) { if (other.scaleRange != null) return false; } else if (!scaleRange.equals(other.scaleRange)) return false; return true; } private DomainCoverage getOuterType() { return DomainCoverage.this; } } /** * Orders SLDSelector by the scale range (using the minimum value) * * @author Andrea Aime - GeoSolutions * */ private class SLDSelectorComparator implements Comparator<SLDSelector> { @Override public int compare(SLDSelector o1, SLDSelector o2) { NumberRange<Double> sr1 = o1.scaleRange; NumberRange<Double> sr2 = o2.scaleRange; if (sr1.getMinimum() == sr2.getMinimum()) { if (sr1.isMinIncluded()) { return sr2.isMinIncluded() ? 0 : -1; } else { return sr2.isMinIncluded() ? 1 : 0; } } else { return sr1.getMinimum() > sr2.getMinimum() ? 1 : -1; } } } static final FilterFactory2 FF = CommonFactoryFinder.getFilterFactory2(); /** * The current domain coverage */ private List<SLDSelector> elements; /** * The target feature type for this domain coverage computation */ private FeatureType targetFeatureType; /** * A simplifier visitor that will cache results that have been simplified already, since this * class unites/intersects filters a lot in order to compute the coverage */ private UnboundSimplifyingFilterVisitor simplifier; /** * The set of selectors generated so far. We can get several repeated selectors due to * conditional pseudo-classes, but only the first one will be not covered */ Set<List<SLDSelector>> generatedSelectors = new HashSet<>(); /** * When true, the detailed (expensive) coverage computation will generate exclusive rules */ boolean exclusiveRulesEnabled = true; /** * Create a new domain coverage for the given feature type * * @param targetFeatureType */ public DomainCoverage(FeatureType targetFeatureType, UnboundSimplifyingFilterVisitor simplifier) { this.elements = new ArrayList<>(); this.targetFeatureType = targetFeatureType; this.simplifier = simplifier; } /** * Adds a rule to the domain, and returns a list of rules representing bits of the domain that * were still not covered by the previous rules * * @param rule * @return */ public List<CssRule> addRule(CssRule rule) { Selector selector = rule.getSelector(); // turns the rule in a set of domain coverage expressions (simplified selectors) List<SLDSelector> ruleCoverage = toSLDSelectors(selector, targetFeatureType); if (generatedSelectors.contains(ruleCoverage)) { return Collections.emptyList(); } else { generatedSelectors.add(ruleCoverage); } // if we are just checking for straight duplicates, let it go if (!exclusiveRulesEnabled) { return coverageToRules(rule, ruleCoverage); } // for each rule we have in the domain, get the differences, if any, with this rule, // emit them as derived rules, and increase the coverage if (elements.isEmpty()) { elements.addAll(ruleCoverage); return coverageToRules(rule, ruleCoverage); } else { List<SLDSelector> reducedCoverage = new ArrayList<>(ruleCoverage); for (SLDSelector element : elements) { List<SLDSelector> difference = new ArrayList<>(); for (SLDSelector rc : reducedCoverage) { List<SLDSelector> ruleDifference = rc.difference(element); difference.addAll(ruleDifference); } reducedCoverage = difference; if (reducedCoverage.isEmpty()) { break; } } if (!reducedCoverage.isEmpty()) { List<CssRule> derivedRules = new ArrayList<>(); for (SLDSelector rc : reducedCoverage) { derivedRules.add(new CssRule(rc.toSelector(simplifier), rule.getProperties(), rule.getComment())); } elements.addAll(reducedCoverage); // so far, this sorting done just for the sake of readability during debugging Collections.sort(elements, new SLDSelectorComparator()); List<SLDSelector> combined = new ArrayList<>(); SLDSelector prev = null; for (SLDSelector ss : elements) { if (prev == null) { prev = ss; } else if (prev.scaleRange.equals(ss.scaleRange)) { org.opengis.filter.Or or = FF.or(ss.filter, prev.filter); Filter simplified = simplify(or); prev = new SLDSelector(prev.scaleRange, simplified); } else { combined.add(prev); prev = ss; } } if (prev != null) { combined.add(prev); } this.elements = combined; return derivedRules; } else { return Collections.emptyList(); } } } private List<CssRule> coverageToRules(CssRule rule, List<SLDSelector> ruleCoverage) { List<CssRule> result = new ArrayList<>(); for (SLDSelector ss : ruleCoverage) { result.add(new CssRule(ss.toSelector(simplifier), rule.getProperties(), rule .getComment())); } return result; } /** * Turns the specified selector into a list of "standardized" SLDSelector * * @param selector * @param targetFeatureType * @return */ List<SLDSelector> toSLDSelectors(Selector selector, FeatureType targetFeatureType) { List<SLDSelector> result = new ArrayList<>(); if (selector instanceof Or) { Or or = (Or) selector; for (Selector s : or.getChildren()) { if (s instanceof Or) { throw new IllegalArgumentException( "Unexpected or selector nested inside another one, " + "at this point they should have been all flattened: " + selector); } toIndependentSLDSelectors(s, targetFeatureType, result); } } else { toIndependentSLDSelectors(selector, targetFeatureType, result); } return result; } /** * Flattens a single SLD selector into a list of {@link SLDSelector}, adding them into the * scaleDependentFilters list * * @param selector * @param targetFeatureType * @param scaleDependentFilters */ private void toIndependentSLDSelectors(Selector selector, FeatureType targetFeatureType, List<SLDSelector> scaleDependentFilters) { Range<Double> range = ScaleRangeExtractor.getScaleRange(selector); if (range == null) { range = FULL_SCALE_RANGE; } Filter filter = OgcFilterBuilder.buildFilter(selector, targetFeatureType); boolean merged = false; for (SLDSelector existing : scaleDependentFilters) { if (existing.scaleRange.equals(range)) { if (existing.filter instanceof org.opengis.filter.Or) { org.opengis.filter.Or or = (org.opengis.filter.Or) existing.filter; List<Filter> children = new ArrayList<>(or.getChildren()); children.add(filter); existing.filter = simplify(FF.or(children)); } else { existing.filter = simplify(FF.or(existing.filter, filter)); } merged = true; break; } } if (!merged) { scaleDependentFilters.add(new SLDSelector(new NumberRange<Double>(range), filter)); } } /** * Simplifies a filter via the simplifying filter visitor, taking into account the target * feature type * * @param filter * @return */ Filter simplify(Filter filter) { return (Filter) filter.accept(simplifier, null); } @Override public String toString() { StringBuilder sb = new StringBuilder("DomainCoverage[items=").append(elements.size()) .append(",\n"); for (SLDSelector selector : elements) { sb.append(selector).append("\n"); } sb.append("] // DomainCoverage end"); return sb.toString(); } void setExclusiveRulesEnabled(boolean exclusiveRulesEnabled) { this.exclusiveRulesEnabled = exclusiveRulesEnabled; } }