/*
* 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.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.styling.css.selector.PseudoClass;
import org.geotools.styling.css.selector.Selector;
import org.geotools.styling.css.util.FilteredPowerSetBuilder;
import org.geotools.styling.css.util.PseudoClassRemover;
import org.geotools.styling.css.util.UnboundSimplifyingFilterVisitor;
import org.geotools.util.logging.Logging;
/**
* Gives a list of Rules, it builds their power set, making it so that any set of rules extracted as
* at least a chance to match a feature (e.g., the rule selectors are not contractiding each other)
*
* @author Andrea Aime - GeoSolutions
*
*/
class RulePowerSetBuilder extends FilteredPowerSetBuilder<CssRule, CssRule> {
static final Logger LOGGER = Logging.getLogger(RulePowerSetBuilder.class);
RulesCombiner combiner;
int maxCombinations = -1;
int count = 0;
UnboundSimplifyingFilterVisitor simplifier;
/**
* These are pseudo class bits that mix in the main rule set, or not, depending on whether their
* pseudo class is a match. They are treated separately to reduce the number of rules the power
* set generates
*/
List<CssRule> mixins;
private static List[] classifyRules(List<CssRule> domain) {
List<CssRule> main = new ArrayList<>();
List<CssRule> mixins = new ArrayList<>();
for (CssRule rule : domain) {
if (rule.getProperties().get(PseudoClass.ROOT) == null) {
Selector simplified = (Selector) rule.selector.accept(new PseudoClassRemover());
rule = new CssRule(simplified, rule.properties, rule.comment);
mixins.add(rule);
} else {
main.add(rule);
}
}
Collections.sort(main, CssRuleComparator.ASCENDING);
Collections.sort(mixins, CssRuleComparator.ASCENDING);
return new List[] { main, mixins };
}
public RulePowerSetBuilder(List<CssRule> domain, UnboundSimplifyingFilterVisitor simplifier) {
this(classifyRules(domain), simplifier, -1);
}
RulePowerSetBuilder(List<CssRule> domain, UnboundSimplifyingFilterVisitor simplifier,
int maxCombinations) {
this(classifyRules(domain), simplifier, maxCombinations);
}
protected RulePowerSetBuilder(List[] domainMixins, UnboundSimplifyingFilterVisitor simplifier,
int maxCombinations) {
super(domainMixins[0]);
this.mixins = domainMixins[1];
this.maxCombinations = maxCombinations;
this.simplifier = simplifier;
this.combiner = new RulesCombiner(simplifier);
}
@Override
protected List<CssRule> buildResult(List<CssRule> rules) {
boolean foundSymbolizerProperty = false;
for (CssRule rule : rules) {
if (rule.hasSymbolizerProperty()) {
foundSymbolizerProperty = true;
break;
}
}
// if none of the source rules has a symbolizer property, there is nothing to combine,
// the cascade still won't generate any output
if (!foundSymbolizerProperty) {
return null;
}
CssRule combined;
if (rules.size() == 1) {
combined = rules.get(0);
} else {
combined = combiner.combineRules(rules);
}
// do we have mixins to consider now?
List<CssRule> results = new ArrayList<>();
if (mixins == null || mixins.size() == 0) {
results.add(combined);
} else {
List<CssRule> applicableMixins = getApplicableMixins(combined);
if (applicableMixins.size() > 0) {
int idx = 0;
// let's see if all mixins are applying without conditinos
for (; idx < applicableMixins.size(); idx++) {
CssRule mixin = applicableMixins.get(idx);
Selector mixedSelector = Selector.and(mixin.selector, combined.selector,
simplifier);
if (mixedSelector == Selector.REJECT) {
// this mixin is no good
continue;
} else if (mixedSelector.equals(combined.selector)) {
// this mixin always applies
combined = combiner.combineRules(Arrays.asList(combined, mixin));
} else {
break;
}
}
// in this case we stumbled into a mixin that adds a condition to the selector,
// from here on we have to perform another power set expansion with the remaining
// mixins
if (idx < applicableMixins.size()) {
List<CssRule> list = new ArrayList<>();
list.add(combined);
list.addAll(applicableMixins.subList(idx, applicableMixins.size()));
RulePowerSetBuilder builder = new RulePowerSetBuilder(new List[] { list,
Collections.emptyList() }, simplifier, maxCombinations - count);
List<CssRule> conditionalPowerSet = builder.buildPowerSet();
results.addAll(conditionalPowerSet);
} else {
results.add(combined);
}
} else {
results.add(combined);
}
}
// make sure we're not going beyond the max generated rules
count += results.size();
if (maxCombinations > 0 && count > maxCombinations) {
LOGGER.severe("Bailing out, the CSS rule combinations have already generated more than "
+ "maxCombinations SLD rules, giving up. Please simplify your CSS style");
} else if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("New rule (" + count + "):" + combined);
}
return results;
}
/**
* Returns all the mixins that can be combined with the rule at hand, that is, mixins that have
* their pseudo-classes matched by the main rule symbolizers. Two lists will be returned, an
* in-conditional one, where the mixins just blend into the main rule, and a conditional one,
* where the mixin adds its own conditions, and thus require its own power set expansion
*
* @param rule
* @return
*/
private List<CssRule> getApplicableMixins(CssRule rule) {
Set<PseudoClass> mixablePseudoClasses = rule.getMixablePseudoClasses();
List<CssRule> result = new ArrayList<>();
for (CssRule mixin : mixins) {
Set<PseudoClass> pseudoClasses = mixin.properties.keySet();
// scroll to avoid building extra sets
boolean found = false;
for (PseudoClass pseudoClass : pseudoClasses) {
if (mixablePseudoClasses.contains(pseudoClass)) {
found = true;
break;
}
}
if (found && acceptMixinCssRule(rule, mixin)) {
result.add(mixin);
}
}
return result;
}
/**
* Filter applicable mixin rules. Defaults to accepting all rules.
* @param rule
* @param mixinRule
* @return
*/
protected boolean acceptMixinCssRule(CssRule rule, CssRule mixinRule) {
return true;
}
@Override
protected boolean accept(List<CssRule> rules) {
if (count > maxCombinations) {
return false;
}
Selector combined = combiner.combineSelectors(rules);
return combined != Selector.REJECT;
}
@Override
protected List<CssRule> postFilterResult(List<CssRule> result) {
List<CssRule> filtered = new ArrayList<>();
// first remove the rules that have no root pseudo selector, they are contributing nothing
for (CssRule cssRule : result) {
if (cssRule.getProperties().get(PseudoClass.ROOT) != null) {
filtered.add(cssRule);
}
}
if (filtered.size() <= 1) {
return filtered;
}
// sort by selectivity
Collections.sort(filtered, CssRuleComparator.DESCENDING);
return filtered;
}
}