/*
* 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.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.geotools.styling.Style;
import org.geotools.styling.css.Value.Function;
import org.geotools.styling.css.selector.PseudoClass;
import org.geotools.styling.css.selector.Selector;
import org.geotools.styling.css.util.PseudoClassExtractor;
import org.geotools.util.Converters;
/**
* A rule in CSS
*
* @author Andrea Aime - GeoSolutions
*
*/
public class CssRule {
public static final Integer NO_Z_INDEX = null;
Selector selector;
Map<PseudoClass, List<Property>> properties;
String comment;
List<CssRule> ancestry;
List<CssRule> nestedRules;
/**
* Builds a CSS rule
*
* @param selector The rule selector
* @param properties The set of rule properties
*/
public CssRule(Selector selector, List<Property> properties) {
super();
this.setSelector(selector);
PseudoClassExtractor extractor = new PseudoClassExtractor();
selector.accept(extractor);
this.setProperties(new HashMap<PseudoClass, List<Property>>());
Set<PseudoClass> pseudoClasses = extractor.getPseudoClasses();
for (PseudoClass ps : pseudoClasses) {
this.getProperties().put(ps, properties);
}
}
/**
* Builds a CSS rule
*
* @param selector The rule selector
* @param properties The set of rule properties
* @param comment The rule comment (can be used to generate SLD's title and abstract
*/
public CssRule(Selector selector, List<Property> properties, String comment) {
this(selector, properties);
this.setComment(comment);
}
/**
* Builds a CSS rule
*
* @param selector The rule selector
* @param properties The set of rule properties, already organized by pseudo-selector
* @param comment The rule comment (can be used to generate SLD's title and abstract
*/
CssRule(Selector selector, Map<PseudoClass, List<Property>> properties, String comment) {
this.setSelector(selector);
this.setProperties(properties);
this.setComment(comment);
}
@Override
public String toString() {
String base = "Rule [\n selector=" + getSelector() + ",\n properties="
+ getProperties() + "]";
return base;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((getComment() == null) ? 0 : getComment().hashCode());
result = prime * result + ((getProperties() == null) ? 0 : getProperties().hashCode());
result = prime * result + ((getSelector() == null) ? 0 : getSelector().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;
CssRule other = (CssRule) obj;
if (getComment() == null) {
if (other.getComment() != null)
return false;
} else if (!getComment().equals(other.getComment()))
return false;
if (getProperties() == null) {
if (other.getProperties() != null)
return false;
} else if (!getProperties().equals(other.getProperties()))
return false;
if (getSelector() == null) {
if (other.getSelector() != null)
return false;
} else if (!getSelector().equals(other.getSelector()))
return false;
return true;
}
/**
* Returns the property values by pseudo-class, matching those that satisfy the specified name
* prefixes
*
* @param pseudoClass
* @param symbolizerPrefixes
* @return
*/
public Map<String, List<Value>> getPropertyValues(PseudoClass pseudoClass,
String... symbolizerPrefixes) {
List<Property> psProperties = getProperties().get(pseudoClass);
if (psProperties == null) {
return Collections.emptyMap();
}
Map<String, List<Value>> result = new LinkedHashMap<>();
if (symbolizerPrefixes != null && symbolizerPrefixes.length > 0) {
for (Property property : psProperties) {
for (String symbolizerPrefix : symbolizerPrefixes) {
if (symbolizerPrefix == null || property.getName().startsWith(symbolizerPrefix)
|| property.getName().startsWith("-gt-" + symbolizerPrefix)) {
result.put(property.getName(), property.getValues());
}
}
}
} else {
for (Property property : psProperties) {
result.put(property.getName(), property.getValues());
}
}
return result;
}
/**
* Returns true if the rule has any property for the given name, in the give pseudo-class
*
* @param pseudoClass
* @param propertyName
* @return
*/
public boolean hasProperty(PseudoClass pseudoClass, String propertyName) {
List<Property> psProperties = getProperties().get(pseudoClass);
if (psProperties == null) {
return false;
}
for (Property property : psProperties) {
if (propertyName.equals(property.getName())) {
return true;
}
}
return false;
}
/**
* Returns the property with a given name (will look for an exact match)
*
* @param pseudoClass
* @param propertyName
* @return
*/
public Property getProperty(PseudoClass pseudoClass, String propertyName) {
List<Property> psProperties = getProperties().get(pseudoClass);
if (psProperties == null) {
return null;
}
for (Property property : psProperties) {
if (propertyName.equals(property.getName())) {
return property;
}
}
return null;
}
/**
* Returns true if any of the properties specified is found in the given pseudo-class
*
* @param pseudoClass
* @param propertyNames
* @return
*/
public boolean hasAnyProperty(PseudoClass pseudoClass, Collection<String> propertyNames) {
List<Property> psProperties = getProperties().get(pseudoClass);
if (psProperties == null) {
return false;
}
for (Property property : psProperties) {
if (propertyNames.contains(property.getName())) {
return true;
}
}
return false;
}
/**
* This rule covers the other if it has the same selector, and has all the properties of the
* other, plus eventually some more
*
* @param other
* @return
*/
public boolean covers(CssRule other) {
if (!other.getSelector().equals(getSelector())) {
return false;
}
Set<PseudoClass> pseudoClasses = this.getProperties().keySet();
Set<PseudoClass> otherPseudoClasses = other.getProperties().keySet();
if (!pseudoClasses.containsAll(otherPseudoClasses)) {
return false;
}
for (PseudoClass pc : otherPseudoClasses) {
List<Property> properties = this.getProperties().get(pc);
List<Property> otherProperties = other.getProperties().get(pc);
for (Property p : otherProperties) {
if (!properties.contains(p)) {
return false;
}
}
}
return true;
}
/**
* Extracts a sub-rule at the given z-index. Will return null if this rule has nothing at that
* specific z-index
*
* @param zIndex
* @return
*/
public CssRule getSubRuleByZIndex(Integer zIndex) {
Map<PseudoClass, List<Property>> zProperties = new HashMap<>();
List<Integer> zIndexes = new ArrayList<>();
for (Map.Entry<PseudoClass, List<Property>> entry : this.getProperties().entrySet()) {
List<Property> props = entry.getValue();
collectZIndexesInProperties(props, zIndexes);
// the list of z-index values is positional, people will normally set them in
// increasing order, but we don't want to make assumptions... users could
// even repeat the same z-index multiple times, take care of that as well
ListIterator<Integer> it = zIndexes.listIterator();
while (it.hasNext()) {
int zIndexPosition = it.nextIndex();
Integer nextZIndex = it.next();
if (nextZIndex == NO_Z_INDEX) {
// this set of properties is z-index independent
zProperties.put(entry.getKey(), new ArrayList<>(props));
} else if (!nextZIndex.equals(zIndex)) {
continue;
} else {
// extract the property values at that position
List<Property> zIndexProperties = new ArrayList<>();
for (Property property : props) {
if (isZIndex(property)) {
continue;
}
List<Value> values = property.getValues();
if (zIndexPosition < values.size()) {
Property p = new Property(property.getName(), Arrays.asList(values
.get(zIndexPosition)));
zIndexProperties.add(p);
} else if (values.size() == 1) {
// properties that does not have multiple values are bound to all levels
zIndexProperties.add(property);
}
}
// if we collected any, add to the result
if (zIndexProperties.size() > 0) {
zProperties.put(entry.getKey(), zIndexProperties);
}
}
}
}
if (zProperties.size() > 0) {
// if the properties had an original z-index, mark it, we'll need it
// to figure out if a combination of rules can be applied at a z-index > 0, or not
if (zIndex != null && zIndexes.contains(zIndex)) {
List<Property> rootProperties = zProperties.get(PseudoClass.ROOT);
if(rootProperties == null) {
rootProperties = new ArrayList<>();
zProperties.put(PseudoClass.ROOT, rootProperties);
}
rootProperties.add(new Property("z-index",
Arrays.asList((Value) new Value.Literal(String.valueOf(zIndex)))));
}
CssRule zRule = new CssRule(this.getSelector(), zProperties, this.getComment());
zRule.ancestry = Arrays.asList(this);
return zRule;
} else {
return null;
}
}
/**
* Returns all z-index values used by this rule
*
* @return
*/
public Set<Integer> getZIndexes() {
Set<Integer> indexes = new TreeSet<>(new ZIndexComparator());
List<Integer> singleListIndexes = new ArrayList<>();
for (List<Property> list : getProperties().values()) {
collectZIndexesInProperties(list, singleListIndexes);
indexes.addAll(singleListIndexes);
}
return indexes;
}
/**
* Returns the z-index values, in the order they are submitted
*
* @param properties
* @return
*/
void collectZIndexesInProperties(List<Property> properties, List<Integer> zIndexes) {
if (zIndexes.size() > 0) {
zIndexes.clear();
}
for (Property property : properties) {
if (isZIndex(property)) {
if (zIndexes.size() > 0) {
// we have two z-index in the same set of properties? keep the latest
zIndexes.clear();
}
List<Value> values = property.getValues();
for (Value value : values) {
if (value instanceof Value.Literal) {
String body = ((Value.Literal) value).body;
Integer zIndex = Converters.convert(body, Integer.class);
if (zIndex == null) {
throw new IllegalArgumentException(
"Invalid value for z-index, it should be an integer: " + body);
} else {
zIndexes.add(zIndex);
}
} else {
throw new IllegalArgumentException(
"z-index must be integer literals, they cannot be expressions, multi-values or any other type: "
+ value);
}
}
}
}
// if we did not find the z-index property, the only z-index is teh default one (which is
if (zIndexes.isEmpty()) {
zIndexes.add(null);
}
}
private boolean isZIndex(Property property) {
String name = property.getName();
return "z-index".equals(name) || "raster-z-index".equals(name);
}
/**
* Returns the rule selector
*
* @return
*/
public Selector getSelector() {
return selector;
}
public void setSelector(Selector selector) {
this.selector = selector;
}
/**
* Returns the rules properties, organized by pseudo-class
*
* @return
*/
public Map<PseudoClass, List<Property>> getProperties() {
return properties;
}
public void setProperties(Map<PseudoClass, List<Property>> properties) {
this.properties = properties;
}
/**
* Returns the rule comment
*
* @return
*/
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
/**
* Returns the original rules from which this rule originated (rules get re-organized and
* combined a lot during the translation process to Geotools {@link Style}
*/
public List<CssRule> getAncestry() {
return ancestry;
}
public void setAncestry(List<CssRule> ancestry) {
this.ancestry = ancestry;
}
/**
* Returns the rules nested in this one
* @return
*/
public List<CssRule> getNestedRules() {
return this.nestedRules;
}
/**
* Returns true if the style has at least one property activating a symbolizer, e.g., fill,
* stroke, mark, label or raster-channel
*
* @param rootProperties
* @return
*/
boolean hasSymbolizerProperty() {
List<Property> rootProperties = getProperties().get(PseudoClass.ROOT);
if (rootProperties == null) {
return false;
}
for (Property property : rootProperties) {
String name = property.getName();
switch (name) {
case "fill":
case "stroke":
case "mark":
case "label":
case "raster-channels":
return true;
}
}
return false;
}
/**
* Returns the list of pseudo classes that can be mixed into this rule, meaning we have root
* properties in which these pseudo classes can be mixed in.
*
* @param rootProperties
* @return
*/
Set<PseudoClass> getMixablePseudoClasses() {
List<Property> rootProperties = getProperties().get(PseudoClass.ROOT);
if (rootProperties == null) {
return Collections.emptySet();
}
Set<PseudoClass> result = new HashSet<>();
for (Property property : rootProperties) {
String name = property.getName();
switch (name) {
case "fill":
case "stroke":
result.add(PseudoClass.newPseudoClass("symbol"));
addPseudoClassesForConditionallyMixableProperty(result, property);
break;
case "mark":
result.add(PseudoClass.newPseudoClass("symbol"));
result.add(PseudoClass.newPseudoClass("mark"));
addIndexedPseudoClasses(result, "mark");
break;
case "label":
result.add(PseudoClass.newPseudoClass("symbol"));
result.add(PseudoClass.newPseudoClass("shield"));
addIndexedPseudoClasses(result, "shield");
break;
}
}
return result;
}
/**
* Adds pseudo classes for fill and stroke, whose ability to mix-in depends on whether a
* function (symbol) or a straight value was used for the value of the property
*
* @param result
* @param property
*/
private void addPseudoClassesForConditionallyMixableProperty(Set<PseudoClass> result,
Property property) {
String propertyName = property.getName();
List<Value> values = property.getValues();
if (values.size() == 1 && values.get(0) instanceof Function) {
result.add(PseudoClass.newPseudoClass(propertyName));
addIndexedPseudoClasses(result, propertyName);
} else {
for (int i = 0; i < values.size(); i++) {
if (values.get(i) instanceof Function) {
result.add(PseudoClass.newPseudoClass("symbol", i));
result.add(PseudoClass.newPseudoClass(propertyName, i + 1));
}
}
}
}
/**
* Collects all properties starting with the propertyName, and adds pseudo classes up to the max
* index found in said properties
*
* @param result
* @param propertyName
*/
private void addIndexedPseudoClasses(Set<PseudoClass> result, String propertyName) {
Map<String, List<Value>> properties = getPropertyValues(PseudoClass.ROOT, propertyName);
int maxRepeatCount = getMaxRepeatCount(properties);
if (maxRepeatCount >= 1) {
for (int i = 1; i <= maxRepeatCount; i++) {
result.add(PseudoClass.newPseudoClass("symbol", i));
result.add(PseudoClass.newPseudoClass(propertyName, i));
}
}
}
/**
* Returns the max number of property values in the provided property set (for repeated
* symbolizers)
*
* @param valueMap
* @return
*/
private int getMaxRepeatCount(Map<String, List<Value>> valueMap) {
int max = 1;
for (List<Value> values : valueMap.values()) {
max = Math.max(max, values.size());
}
return max;
}
/**
* Turns a rule with nested subrules into a flat list of rules (this rule, plus all nested with
* a properly combined selector and property inheritance)
* @return
*/
public List<CssRule> expandNested(RulesCombiner combiner) {
if (nestedRules.isEmpty()) {
return Collections.singletonList(this);
} else {
Stream<CssRule> nestedRulesStream = nestedRules.stream().flatMap(r -> {
return r.expandNested(combiner).stream().map(sr -> {
CssRule combined = combiner.combineRules(Arrays.asList(this, sr));
combined.setComment(sr.getComment());
combined.setAncestry(null);
return combined;
});
});
return Stream.concat(Stream.of(this), nestedRulesStream).collect(Collectors.toList());
}
}
}