/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2014 - 2016, 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.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.xml.transform.TransformerException;
import org.apache.commons.io.FileUtils;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.feature.NameImpl;
import org.geotools.filter.visitor.SimplifyingFilterVisitor;
import org.geotools.styling.ColorMap;
import org.geotools.styling.FeatureTypeStyle;
import org.geotools.styling.NamedLayer;
import org.geotools.styling.Rule;
import org.geotools.styling.SLDTransformer;
import org.geotools.styling.StyleFactory;
import org.geotools.styling.StyledLayerDescriptor;
import org.geotools.styling.builder.ChannelSelectionBuilder;
import org.geotools.styling.builder.ColorMapBuilder;
import org.geotools.styling.builder.ColorMapEntryBuilder;
import org.geotools.styling.builder.ContrastEnhancementBuilder;
import org.geotools.styling.builder.FeatureTypeStyleBuilder;
import org.geotools.styling.builder.FillBuilder;
import org.geotools.styling.builder.FontBuilder;
import org.geotools.styling.builder.GraphicBuilder;
import org.geotools.styling.builder.HaloBuilder;
import org.geotools.styling.builder.LineSymbolizerBuilder;
import org.geotools.styling.builder.MarkBuilder;
import org.geotools.styling.builder.PointPlacementBuilder;
import org.geotools.styling.builder.PointSymbolizerBuilder;
import org.geotools.styling.builder.PolygonSymbolizerBuilder;
import org.geotools.styling.builder.RasterSymbolizerBuilder;
import org.geotools.styling.builder.RuleBuilder;
import org.geotools.styling.builder.StrokeBuilder;
import org.geotools.styling.builder.StyleBuilder;
import org.geotools.styling.builder.SymbolizerBuilder;
import org.geotools.styling.builder.TextSymbolizerBuilder;
import org.geotools.styling.css.Value.Function;
import org.geotools.styling.css.Value.Literal;
import org.geotools.styling.css.Value.MultiValue;
import org.geotools.styling.css.selector.AbstractSelectorVisitor;
import org.geotools.styling.css.selector.Data;
import org.geotools.styling.css.selector.Or;
import org.geotools.styling.css.selector.PseudoClass;
import org.geotools.styling.css.selector.Selector;
import org.geotools.styling.css.selector.TypeName;
import org.geotools.styling.css.util.FeatureTypeGuesser;
import org.geotools.styling.css.util.OgcFilterBuilder;
import org.geotools.styling.css.util.PseudoClassRemover;
import org.geotools.styling.css.util.ScaleRangeExtractor;
import org.geotools.styling.css.util.TypeNameExtractor;
import org.geotools.styling.css.util.TypeNameSimplifier;
import org.geotools.styling.css.util.UnboundSimplifyingFilterVisitor;
import org.geotools.util.Converters;
import org.geotools.util.Range;
import org.geotools.util.logging.Logging;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.Name;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.expression.Expression;
import org.opengis.style.Style;
import org.w3c.dom.css.CSSRule;
/**
* Transforms a GeoCSS into an equivalent GeoTools {@link Style} object
*
* @author Andrea Aime - GeoSolutions
*/
public class CssTranslator {
/**
* The ways the CSS -> SLD transformation can be performed
*
* @author Andrea Aime - GeoSolutions
*
*/
static enum TranslationMode {
/**
* Generates fully exclusive rules, extra rules are removed
*/
Exclusive, /**
* Sets the "exclusive" evaluation mode in the FeatureTypeStyle and delegates finding the first matching rules to the renderer,
* will generate more rules, but work a lot less to do so by avoiding to compute the domain coverage
*/
Simple, /**
* The translator will pick Exclusive by default, but if the rules to be turned into SLD go beyond
*/
Flat, /**
* All rules are merged straight forward if filters are exactly matching only with the
* direct following pseudo rules. There is no cascading going on, no creation of additional
* rules. After merging the rules are sorted by z-index.
*/
Auto;
};
static final Logger LOGGER = Logging.getLogger(CssTranslator.class);
static final String DIRECTIVE_MAX_OUTPUT_RULES = "maxOutputRules";
static final String DIRECTIVE_AUTO_THRESHOLD = "autoThreshold";
static final String DIRECTIVE_TRANSLATION_MODE = "mode";
static final String DIRECTIVE_STYLE_TITLE = "styleTitle";
static final String DIRECTIVE_STYLE_ABSTRACT = "styleAbstract";
static final int MAX_OUTPUT_RULES_DEFAULT = Integer
.valueOf(System.getProperty("org.geotools.css." + DIRECTIVE_MAX_OUTPUT_RULES, "10000"));
static final int AUTO_THRESHOLD_DEFAULT = Integer
.valueOf(System.getProperty("org.geotools.css." + DIRECTIVE_AUTO_THRESHOLD, "100"));
static final FilterFactory2 FF = CommonFactoryFinder.getFilterFactory2();
/**
* Matches the title tag inside a rule comment
*/
static final Pattern TITLE_PATTERN = Pattern.compile("^.*@title\\s*(?:\\:\\s*)?(.+)\\s*$");
/**
* Matches the abstract tag inside a rule comment
*/
static final Pattern ABSTRACT_PATTERN = Pattern
.compile("^.*@abstract\\s*(?:\\:\\s*)?(.+)\\s*$");
/**
* The global composite property
*/
static final String COMPOSITE = "composite";
/**
* The global composite-base property
*/
static final String COMPOSITE_BASE = "composite-base";
/**
* The attribute sorting property
*/
static final String SORT_BY = "sort-by";
/**
* The sort group for z-ordering
*/
static final String SORT_BY_GROUP = "sort-by-group";
/**
* The transformation
*/
static final String TRANSFORM = "transform";
@SuppressWarnings("serial")
static final Map<String, String> POLYGON_VENDOR_OPTIONS = new HashMap<String, String>() {
{
put("-gt-graphic-margin", "graphic-margin");
put("-gt-fill-label-obstacle", "labelObstacle");
put("-gt-fill-random", "random");
put("-gt-fill-random-seed", "random-seed");
put("-gt-fill-random-tile-size", "random-tile-size");
put("-gt-fill-random-symbol-count", "random-symbol-count");
put("-gt-fill-random-space-around", "random-space-around");
put("-gt-fill-random-rotation", "random-rotation");
put("fill-composite", "composite");
}
};
@SuppressWarnings("serial")
static final Map<String, String> TEXT_VENDOR_OPTIONS = new HashMap<String, String>() {
{
put("-gt-label-padding", "spaceAround");
put("-gt-label-group", "group");
put("-gt-label-max-displacement", "maxDisplacement");
put("-gt-label-min-group-distance", "minGroupDistance");
put("-gt-label-repeat", "repeat");
put("-gt-label-all-group", "allGroup");
put("-gt-label-remove-overlaps", "removeOverlaps");
put("-gt-label-allow-overruns", "allowOverrun");
put("-gt-label-follow-line", "followLine");
put("-gt-label-underline-text", "underlineText");
put("-gt-label-max-angle-delta", "maxAngleDelta");
put("-gt-label-auto-wrap", "autoWrap");
put("-gt-label-force-ltr", "forceLeftToRight");
put("-gt-label-conflict-resolution", "conflictResolution");
put("-gt-label-fit-goodness", "goodnessOfFit");
put("-gt-label-kerning", "kerning");
put("-gt-shield-resize", "graphic-resize");
put("-gt-shield-margin", "graphic-margin");
}
};
@SuppressWarnings("serial")
static final Map<String, String> LINE_VENDOR_OPTIONS = new HashMap<String, String>() {
{
put("-gt-stroke-label-obstacle", "labelObstacle");
put("stroke-composite", "composite");
}
};
@SuppressWarnings("serial")
static final Map<String, String> POINT_VENDOR_OPTIONS = new HashMap<String, String>() {
{
put("-gt-mark-label-obstacle", "labelObstacle");
put("mark-composite", "composite");
}
};
@SuppressWarnings("serial")
static final Map<String, String> RASTER_VENDOR_OPTIONS = new HashMap<String, String>() {
{
put("raster-composite", "composite");
}
};
@SuppressWarnings("serial")
static final Map<String, String> CONTRASTENHANCMENT_VENDOR_OPTIONS = new HashMap<String, String>() {
{
put("-gt-raster-contrast-enhancement-algorithm", "algorithm");
put("-gt-raster-contrast-enhancement-min", "minValue");
put("-gt-raster-contrast-enhancement-max", "maxValue");
put("-gt-raster-contrast-enhancement-normalizationfactor", "normalizationFactor");
put("-gt-raster-contrast-enhancement-correctionfactor", "correctionFactor");
//short forms for lazy people
put("-gt-rce-algorithm", "algorithm");
put("-gt-rce-min", "minValue");
put("-gt-rce-max", "maxValue");
put("-gt-rce-normalizationfactor", "normalizationFactor");
put("-gt-rce-correctionfactor", "correctionFactor");
}
};
/**
* Limits how many output rules we are going to generate
*/
int maxCombinations = MAX_OUTPUT_RULES_DEFAULT;
public int getMaxCombinations() {
return maxCombinations;
}
/**
* Maximum number of rule combinations before bailing out of the power set generation
*
* @param maxCombinations
*/
public void setMaxCombinations(int maxCombinations) {
this.maxCombinations = maxCombinations;
}
/**
* Translates a CSS stylesheet into an equivalent GeoTools {@link Style} object
*
* @param stylesheet
* @return
*/
public Style translate(Stylesheet stylesheet) {
// get the directives influencing translation
int maxCombinations = getMaxCombinations(stylesheet);
final TranslationMode mode = getTranslationMode(stylesheet);
int autoThreshold = getAutoThreshold(stylesheet);
List<CssRule> topRules = stylesheet.getRules();
List<CssRule> allRules = expandNested(topRules);
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Starting with " + allRules.size() + " rules in the stylesheet");
}
// prepare the full SLD builder
StyleBuilder styleBuilder = new StyleBuilder();
styleBuilder.name("Default Styler");
styleBuilder.title(stylesheet.getDirectiveValue(DIRECTIVE_STYLE_TITLE));
styleBuilder.styleAbstract(stylesheet.getDirectiveValue(DIRECTIVE_STYLE_ABSTRACT));
int translatedRuleCount = 0;
if (mode == TranslationMode.Flat) {
translatedRuleCount = translateFlat(allRules, styleBuilder);
} else {
translatedRuleCount = translateCss(mode, allRules, styleBuilder, maxCombinations, autoThreshold);
}
// check that we have generated at least one rule in output
if (translatedRuleCount == 0) {
throw new IllegalArgumentException("Invalid CSS style, no rule seems to activate "
+ "any symbolization. The properties activating the symbolizers are fill, "
+ "stroke, mark, label, raster-channels, have any been used in a rule matching any feature?");
}
return styleBuilder.build();
}
private List<CssRule> expandNested(List<CssRule> topRules) {
RulesCombiner combiner = new RulesCombiner(new UnboundSimplifyingFilterVisitor());
List<CssRule> expanded = topRules.stream().flatMap(r -> r.expandNested(combiner).stream()).collect(Collectors.toList());
return expanded;
}
private int translateCss(final TranslationMode mode, List<CssRule> allRules, StyleBuilder styleBuilder, int maxCombinations, int autoThreshold) {
// split rules by index and typename, then build the power set for each group and
// generate the rules and symbolizers
Map<Integer, List<CssRule>> zIndexRules = organizeByZIndex(allRules);
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Split the rules into " + zIndexRules + " sets after z-index separation");
}
int translatedRuleCount = 0;
for (Map.Entry<Integer, List<CssRule>> zEntry : zIndexRules.entrySet()) {
final Integer zIndex = zEntry.getKey();
List<CssRule> rules = zEntry.getValue();
Collections.sort(rules, CssRuleComparator.DESCENDING);
Map<String, List<CssRule>> typenameRules = organizeByTypeName(rules);
// build the SLD
for (Map.Entry<String, List<CssRule>> entry : typenameRules.entrySet()) {
String featureTypeName = entry.getKey();
List<CssRule> localRules = entry.getValue();
final FeatureType targetFeatureType = getTargetFeatureType(featureTypeName,
localRules);
if (targetFeatureType != null) {
// attach the target feature type to all Data selectors to allow range based
// simplification
for (CssRule rule : localRules) {
rule.getSelector().accept(new AbstractSelectorVisitor() {
@Override
public Object visit(Data data) {
data.featureType = targetFeatureType;
return super.visit(data);
}
});
}
}
// at this point we can have rules with selectors having two scale ranges
// in or, we should split them, as we cannot represent them in SLD
// (and yes, this changes their selectivity a bit, could not find a reasonable
// solution out of this so far, past the power set we might end up with
// and and of two selectors, that internally have ORs of scales, which could
// be quite complicated to un-tangle)
List<CssRule> flattenedRules = flattenScaleRanges(localRules);
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Preparing power set expansion with " + flattenedRules.size()
+ " rules for feature type: " + featureTypeName);
}
// The simplifying visitor that will cache the results to avoid re-computing
// over and over the same simplifications
CachedSimplifyingFilterVisitor cachedSimplifier = new CachedSimplifyingFilterVisitor(
targetFeatureType);
RulePowerSetBuilder builder = new RulePowerSetBuilder(flattenedRules, cachedSimplifier, maxCombinations) {
@Override
protected java.util.List<CssRule> buildResult(java.util.List<CssRule> rules) {
if (zIndex != null && zIndex > 0) {
TreeSet<Integer> zIndexes = getZIndexesForRules(rules);
if (!zIndexes.contains(zIndex)) {
return null;
}
}
return super.buildResult(rules);
}
};
List<CssRule> combinedRules = builder.buildPowerSet();
if (combinedRules.isEmpty()) {
continue;
}
// create the feature type style for this typename
FeatureTypeStyleBuilder ftsBuilder = styleBuilder.featureTypeStyle();
// regardless of the translation mode, the first rule matching is
// the only one that we want to be applied (in exclusive mode it will be
// the only one matching, the simple mode we want the evaluation to stop there)
ftsBuilder.option(FeatureTypeStyle.KEY_EVALUATION_MODE,
FeatureTypeStyle.VALUE_EVALUATION_MODE_FIRST);
if (featureTypeName != null) {
ftsBuilder.setFeatureTypeNames(
Arrays.asList((Name) new NameImpl(featureTypeName)));
}
Collections.sort(combinedRules, CssRuleComparator.DESCENDING);
int rulesCount = combinedRules.size();
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Generated " + rulesCount
+ " combined rules after filtered power set expansion");
}
String composite = null;
Boolean compositeBase = null;
String sortBy = null;
String sortByGroup = null;
Expression transform = null;
// setup the tool that will eliminate redundant rules (if necessary)
DomainCoverage coverage = new DomainCoverage(targetFeatureType, cachedSimplifier);
if (mode == TranslationMode.Exclusive) {
// create a SLD rule for each css one, making them exclusive, that is,
// remove from each rule the union of the zoom/data domain matched by previous
// rules
coverage.exclusiveRulesEnabled = true;
} else if (mode == TranslationMode.Auto) {
if (rulesCount < autoThreshold) {
LOGGER.fine("Sticking to Exclusive translation mode, rules number is "
+ rulesCount + " with a threshold of " + autoThreshold);
coverage.exclusiveRulesEnabled = true;
} else {
LOGGER.info("Switching to Simple translation mode, rules number is "
+ rulesCount + " with a threshold of " + autoThreshold);
coverage.exclusiveRulesEnabled = false;
}
} else {
// just skip rules with the same selector
coverage.exclusiveRulesEnabled = false;
}
// generate the SLD rules
for (int i = 0; i < rulesCount; i++) {
// skip eventual combinations that are not sporting any
// root pseudo class
CssRule cssRule = combinedRules.get(i);
if (!cssRule.hasSymbolizerProperty()) {
continue;
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Current domain coverage: " + coverage);
LOGGER.fine("Adding rule to domain coverage: " + cssRule);
LOGGER.fine("Rules left to process: " + (rulesCount - i));
}
List<CssRule> derivedRules = coverage.addRule(cssRule);
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Derived rules not yet covered in domain coverage: "
+ derivedRules.size() + "\n" + derivedRules);
}
for (CssRule derived : derivedRules) {
buildSldRule(derived, ftsBuilder, targetFeatureType);
translatedRuleCount++;
// Reminder about why this is done the way it's done. These are all rule properties
// in CSS and are subject to override. In SLD they contribute to containing
// FeatureTypeStyle, so the first one found wins and controls this z-level
// check if we have global composition going, and use the value of
// the first rule providing the information (the one with the highest
// priority)
if (composite == null) {
List<Value> values = derived
.getPropertyValues(PseudoClass.ROOT, COMPOSITE).get(COMPOSITE);
if (values != null && !values.isEmpty()) {
composite = values.get(0).toLiteral();
}
}
if (compositeBase == null) {
List<Value> values = derived
.getPropertyValues(PseudoClass.ROOT, COMPOSITE_BASE)
.get(COMPOSITE_BASE);
if (values != null && !values.isEmpty()) {
compositeBase = Boolean.valueOf(values.get(0).toLiteral());
}
}
// check if we have any sort-by
if (sortBy == null) {
List<Value> values = derived
.getPropertyValues(PseudoClass.ROOT, SORT_BY).get(SORT_BY);
if (values != null && !values.isEmpty()) {
sortBy = values.get(0).toLiteral();
}
}
// check if we have any sort-by-group
if (sortByGroup == null) {
List<Value> values = derived
.getPropertyValues(PseudoClass.ROOT, SORT_BY_GROUP)
.get(SORT_BY_GROUP);
if (values != null && !values.isEmpty()) {
sortByGroup = values.get(0).toLiteral();
}
}
// check if we have a transform, apply it
if(transform == null) {
List<Value> values = derived
.getPropertyValues(PseudoClass.ROOT, TRANSFORM)
.get(TRANSFORM);
if (values != null && !values.isEmpty()) {
transform = values.get(0).toExpression();
}
}
}
if (composite != null) {
ftsBuilder.option(COMPOSITE, composite);
}
if (Boolean.TRUE.equals(compositeBase)) {
ftsBuilder.option(COMPOSITE_BASE, "true");
}
if (sortBy != null) {
ftsBuilder.option(FeatureTypeStyle.SORT_BY, sortBy);
}
if (sortByGroup != null) {
ftsBuilder.option(FeatureTypeStyle.SORT_BY_GROUP, sortByGroup);
}
if (transform != null) {
ftsBuilder.transformation(transform);
}
}
}
}
return translatedRuleCount;
}
private int translateFlat(List<CssRule> allRules, StyleBuilder styleBuilder) {
List<CssRule> finalRules = new ArrayList<>();
CssRule actualRule = null;
Map<PseudoClass, List<Property>> properties = null;
Set<PseudoClass> mixablePseudoClasses = null;
int translatedRuleCount = 0;
for (CssRule rule : allRules) {
if (rule.getProperties().get(PseudoClass.ROOT) == null) {
Selector simplified = (Selector) rule.selector.accept(new PseudoClassRemover());
if (actualRule != null && actualRule.getSelector().equals(simplified)) {
boolean changed = false;
for (Map.Entry<PseudoClass, List<Property>> item : rule.properties.entrySet()) {
if (mixablePseudoClasses.contains(item.getKey())) {
properties.put(item.getKey(), item.getValue());
changed = true;
}
}
if (changed) {
actualRule = new CssRule(actualRule.selector, properties, actualRule.comment);
}
}
} else {
if (actualRule != null) {
finalRules.add(actualRule);
}
actualRule = rule;
mixablePseudoClasses = actualRule.getMixablePseudoClasses();
properties = new LinkedHashMap<>(actualRule.properties);
}
}
if (actualRule != null) {
finalRules.add(actualRule);
}
if (finalRules.isEmpty()) {
return 0;
}
Map<Integer, List<CssRule>> zIndexRules = organizeByZIndex(finalRules);
for (Map.Entry<Integer, List<CssRule>> zEntry : zIndexRules.entrySet()) {
List<CssRule> rules = zEntry.getValue();
Map<String, List<CssRule>> typenameRules = organizeByTypeName(rules);
// build the SLD
for (Map.Entry<String, List<CssRule>> entry : typenameRules.entrySet()) {
String featureTypeName = entry.getKey();
List<CssRule> localRules = entry.getValue();
final FeatureType targetFeatureType = getTargetFeatureType(featureTypeName, localRules);
List<CssRule> flattenedRules = flattenScaleRanges(localRules);
FeatureTypeStyleBuilder ftsBuilder = styleBuilder.featureTypeStyle();
if (featureTypeName != null) {
ftsBuilder.setFeatureTypeNames(
Arrays.asList((Name) new NameImpl(featureTypeName)));
}
String composite = null;
Boolean compositeBase = null;
String sortBy = null;
String sortByGroup = null;
// generate the SLD rules
for (CssRule cssRule : flattenedRules) {
if (!cssRule.hasSymbolizerProperty()) {
continue;
}
buildSldRule(cssRule, ftsBuilder, targetFeatureType);
translatedRuleCount++;
// check if we have global composition going, and use the value of
// the first rule providing the information (the one with the highest
// priority)
if (composite == null) {
List<Value> values = cssRule
.getPropertyValues(PseudoClass.ROOT, COMPOSITE).get(COMPOSITE);
if (values != null && !values.isEmpty()) {
composite = values.get(0).toLiteral();
}
}
if (compositeBase == null) {
List<Value> values = cssRule
.getPropertyValues(PseudoClass.ROOT, COMPOSITE_BASE)
.get(COMPOSITE_BASE);
if (values != null && !values.isEmpty()) {
compositeBase = Boolean.valueOf(values.get(0).toLiteral());
}
}
// check if we have any sort-by
if (sortBy == null) {
List<Value> values = cssRule
.getPropertyValues(PseudoClass.ROOT, SORT_BY).get(SORT_BY);
if (values != null && !values.isEmpty()) {
sortBy = values.get(0).toLiteral();
}
}
// check if we have any sort-by-group
if (sortByGroup == null) {
List<Value> values = cssRule
.getPropertyValues(PseudoClass.ROOT, SORT_BY_GROUP)
.get(SORT_BY_GROUP);
if (values != null && !values.isEmpty()) {
sortByGroup = values.get(0).toLiteral();
}
}
}
if (composite != null) {
ftsBuilder.option(COMPOSITE, composite);
}
if (Boolean.TRUE.equals(compositeBase)) {
ftsBuilder.option(COMPOSITE_BASE, "true");
}
if (sortBy != null) {
ftsBuilder.option(FeatureTypeStyle.SORT_BY, sortBy);
}
if (sortByGroup != null) {
ftsBuilder.option(FeatureTypeStyle.SORT_BY_GROUP, sortByGroup);
}
}
}
return translatedRuleCount;
}
private TranslationMode getTranslationMode(Stylesheet stylesheet) {
String value = stylesheet.getDirectiveValue(DIRECTIVE_TRANSLATION_MODE);
if (value != null) {
try {
return TranslationMode.valueOf(value);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid translation mode '" + value
+ "', supported values are: " + TranslationMode.values());
}
}
return TranslationMode.Auto;
}
private int getMaxCombinations(Stylesheet stylesheet) {
int maxCombinations = this.maxCombinations;
String maxOutputRulesDirective = stylesheet.getDirectiveValue(DIRECTIVE_MAX_OUTPUT_RULES);
if (maxOutputRulesDirective != null) {
Integer converted = Converters.convert(maxOutputRulesDirective, Integer.class);
if (converted == null) {
throw new IllegalArgumentException("Invalid value for " + DIRECTIVE_MAX_OUTPUT_RULES
+ ", it should be a positive integer value, it was "
+ maxOutputRulesDirective);
}
maxCombinations = converted;
}
return maxCombinations;
}
private int getAutoThreshold(Stylesheet stylesheet) {
int result = AUTO_THRESHOLD_DEFAULT;
String autoThreshold = stylesheet.getDirectiveValue(DIRECTIVE_AUTO_THRESHOLD);
if (autoThreshold != null) {
Integer converted = Converters.convert(autoThreshold, Integer.class);
if (converted == null) {
throw new IllegalArgumentException("Invalid value for " + DIRECTIVE_AUTO_THRESHOLD
+ ", it should be a positive integer value, it was " + autoThreshold);
}
result = converted;
}
return result;
}
/**
* SLD rules can have two or more selectors in OR using different scale ranges, however the SLD model does not allow for that. Flatten them into N
* different rules, with the same properties, but different selectors
*
* @param rules
* @return
*/
private List<CssRule> flattenScaleRanges(List<CssRule> rules) {
List<CssRule> result = new ArrayList<>();
for (CssRule rule : rules) {
if (rule.getSelector() instanceof Or) {
Or or = (Or) rule.getSelector();
List<Selector> others = new ArrayList<>();
for (Selector child : or.getChildren()) {
ScaleRangeExtractor extractor = new ScaleRangeExtractor();
Range<Double> range = extractor.getScaleRange(child);
if (range == null) {
others.add(child);
} else {
result.add(new CssRule(child, rule.getProperties(), rule.getComment()));
}
}
if (others.size() == 1) {
result.add(new CssRule(others.get(0), rule.getProperties(), rule.getComment()));
} else if (others.size() > 0) {
result.add(
new CssRule(new Or(others), rule.getProperties(), rule.getComment()));
}
} else {
result.add(rule);
}
}
return result;
}
/**
* This method builds a target feature type based on the provided rules, subclasses can override and maybe pick the feature type from a well known
* source
*/
protected FeatureType getTargetFeatureType(String featureTypeName, List<CssRule> rules) {
FeatureTypeGuesser guesser = new FeatureTypeGuesser();
for (CssRule rule : rules) {
guesser.addRule(rule);
}
return guesser.getFeatureType();
}
/**
* Splits the rules into different sets by feature type name
*
* @param rules
* @return
*/
private Map<String, List<CssRule>> organizeByTypeName(List<CssRule> rules) {
TypeNameExtractor extractor = new TypeNameExtractor();
for (CssRule rule : rules) {
rule.getSelector().accept(extractor);
}
// extract all typename specific rules
Map<String, List<CssRule>> result = new LinkedHashMap<>();
Set<TypeName> typeNames = extractor.getTypeNames();
if (typeNames.size() == 1 && typeNames.contains(TypeName.DEFAULT)) {
// no layer specific stuff
result.put(TypeName.DEFAULT.name, rules);
}
for (TypeName tn : typeNames) {
List<CssRule> typeNameRules = new ArrayList<>();
for (CssRule rule : rules) {
TypeNameSimplifier simplifier = new TypeNameSimplifier(tn);
Selector simplified = (Selector) rule.getSelector().accept(simplifier);
if (simplified != Selector.REJECT) {
typeNameRules
.add(new CssRule(simplified, rule.getProperties(), rule.getComment()));
}
}
result.put(tn.name, typeNameRules);
}
return result;
}
/**
* Organizes them rules by ascending z-index
*
* @param rules
* @return
*/
private Map<Integer, List<CssRule>> organizeByZIndex(List<CssRule> rules) {
TreeSet<Integer> indexes = getZIndexesForRules(rules);
Map<Integer, List<CssRule>> result = new TreeMap<>();
if (indexes.size() == 1) {
result.put(indexes.first(), rules);
} else {
// now for each level extract the sub-rules attached to that level,
// considering that properties not associated to a level, bind to all levels
int symbolizerPropertyCount = 0;
for (Integer index : indexes) {
List<CssRule> rulesByIndex = new ArrayList<>();
for (CssRule rule : rules) {
CssRule subRule = rule.getSubRuleByZIndex(index);
if (subRule != null) {
if (subRule.hasSymbolizerProperty()) {
symbolizerPropertyCount++;
}
rulesByIndex.add(subRule);
}
}
// do we have at least one property that will trigger the generation
// of a symbolizer in here?
if (symbolizerPropertyCount > 0) {
result.put(index, rulesByIndex);
}
}
}
return result;
}
private TreeSet<Integer> getZIndexesForRules(List<CssRule> rules) {
// collect and sort all the indexes first
TreeSet<Integer> indexes = new TreeSet<>(new ZIndexComparator());
for (CssRule rule : rules) {
Set<Integer> ruleIndexes = rule.getZIndexes();
if (ruleIndexes.contains(null)) {
ruleIndexes.remove(null);
ruleIndexes.add(0);
}
indexes.addAll(ruleIndexes);
}
return indexes;
}
/**
* Turns an SLD compatible {@link CSSRule} into a {@link Rule}, appending it to the {@link FeatureTypeStyleBuilder}
*
* @param cssRule
* @param fts
* @param targetFeatureType
*/
void buildSldRule(CssRule cssRule, FeatureTypeStyleBuilder fts, FeatureType targetFeatureType) {
// check we have a valid scale range
Range<Double> scaleRange = ScaleRangeExtractor.getScaleRange(cssRule);
if (scaleRange != null && scaleRange.isEmpty()) {
return;
}
// check we have a valid filter
Filter filter = OgcFilterBuilder.buildFilter(cssRule.getSelector(), targetFeatureType);
if (filter == Filter.EXCLUDE) {
return;
}
// ok, build the rule
RuleBuilder ruleBuilder;
ruleBuilder = fts.rule();
ruleBuilder.filter(filter);
String title = getCombinedTag(cssRule.getComment(), TITLE_PATTERN, ", ");
if (title != null) {
ruleBuilder.title(title);
}
String ruleAbstract = getCombinedTag(cssRule.getComment(), ABSTRACT_PATTERN, "\n");
if (ruleAbstract != null) {
ruleBuilder.ruleAbstract(ruleAbstract);
}
if (scaleRange != null) {
Double minValue = scaleRange.getMinValue();
if (minValue != null && minValue > 0) {
ruleBuilder.min(minValue);
}
Double maxValue = scaleRange.getMaxValue();
if (maxValue != null && maxValue < Double.POSITIVE_INFINITY) {
ruleBuilder.max(maxValue);
}
}
// see if we can fold the stroke into a polygon symbolizer
boolean generateStroke = cssRule.hasProperty(PseudoClass.ROOT, "stroke");
boolean lineSymbolizerSpecificProperties = cssRule.hasAnyProperty(PseudoClass.ROOT,
LINE_VENDOR_OPTIONS.keySet()) || !sameGeometry(cssRule, "stroke-geometry", "fill-geometry");
boolean includeStrokeInPolygonSymbolizer = generateStroke
&& !lineSymbolizerSpecificProperties;
boolean generatePolygonSymbolizer = cssRule.hasProperty(PseudoClass.ROOT, "fill");
if (generatePolygonSymbolizer) {
addPolygonSymbolizer(cssRule, ruleBuilder, includeStrokeInPolygonSymbolizer);
}
if (generateStroke && !(generatePolygonSymbolizer && includeStrokeInPolygonSymbolizer)) {
addLineSymbolizer(cssRule, ruleBuilder);
}
if (cssRule.hasProperty(PseudoClass.ROOT, "mark")) {
addPointSymbolizer(cssRule, ruleBuilder);
}
if (cssRule.hasProperty(PseudoClass.ROOT, "label")) {
addTextSymbolizer(cssRule, ruleBuilder);
}
if (cssRule.hasProperty(PseudoClass.ROOT, "raster-channels")) {
addRasterSymbolizer(cssRule, ruleBuilder);
}
}
private boolean sameGeometry(CssRule cssRule, String geomProperty1, String geomProperty2) {
Property p1 = cssRule.getProperty(PseudoClass.ROOT, geomProperty1);
Property p2 = cssRule.getProperty(PseudoClass.ROOT, geomProperty2);
return Objects.equals(p1, p2);
}
private String getCombinedTag(String comment, Pattern p, String separator) {
if (comment == null || comment.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder();
for (String line : comment.split("\n")) {
Matcher matcher = p.matcher(line);
if (matcher.matches()) {
String text = matcher.group(1).trim();
if (!text.isEmpty()) {
if (sb.length() > 0) {
sb.append(separator);
}
sb.append(text);
}
}
}
if (sb.length() > 0) {
return sb.toString();
} else {
return null;
}
}
/**
* Builds a polygon symbolizer into the current rule, if a <code>fill</code> property is found
*
* @param cssRule
* @param ruleBuilder
* @param includeStrokeInPolygonSymbolizer
*/
private void addPolygonSymbolizer(CssRule cssRule, RuleBuilder ruleBuilder,
boolean includeStrokeInPolygonSymbolizer) {
Map<String, List<Value>> values;
if (includeStrokeInPolygonSymbolizer) {
values = cssRule.getPropertyValues(PseudoClass.ROOT, "fill", "-gt-graphic-margin",
"stroke");
} else {
values = cssRule.getPropertyValues(PseudoClass.ROOT, "fill", "-gt-graphic-margin");
}
if (values == null || values.isEmpty()) {
return;
}
int repeatCount = getMaxRepeatCount(values);
for (int i = 0; i < repeatCount; i++) {
PolygonSymbolizerBuilder pb = ruleBuilder.polygon();
Expression fillGeometry = getExpression(values, "fill-geometry", i);
if (fillGeometry != null) {
pb.geometry(fillGeometry);
}
FillBuilder fb = pb.fill();
buildFill(cssRule, fb, values, i);
if (includeStrokeInPolygonSymbolizer) {
StrokeBuilder sb = pb.stroke();
buildStroke(cssRule, sb, values, i);
}
addVendorOptions(pb, POLYGON_VENDOR_OPTIONS, values, i);
}
}
/**
* Builds a point symbolizer into the current rule, if a <code>mark</code> property is found
*
* @param cssRule
* @param ruleBuilder
*/
private void addPointSymbolizer(CssRule cssRule, RuleBuilder ruleBuilder) {
Map<String, List<Value>> values = cssRule.getPropertyValues(PseudoClass.ROOT, "mark");
if (values == null || values.isEmpty()) {
return;
}
int repeatCount = getMaxRepeatCount(values);
for (int i = 0; i < repeatCount; i++) {
final PointSymbolizerBuilder pb = ruleBuilder.point();
Expression markGeometry = getExpression(values, "mark-geometry", i);
if (markGeometry != null) {
pb.geometry(markGeometry);
}
for (Value markValue : getMultiValue(values, "mark", i)) {
new SubgraphicBuilder("mark", markValue, values, cssRule, i) {
@Override
protected GraphicBuilder getGraphicBuilder() {
return pb.graphic();
}
};
}
addVendorOptions(pb, POINT_VENDOR_OPTIONS, values, i);
}
}
/**
* Builds a text symbolizer into the current rule, if a <code>label</code> property is found
*
* @param cssRule
* @param ruleBuilder
*/
private void addTextSymbolizer(CssRule cssRule, RuleBuilder ruleBuilder) {
Map<String, List<Value>> values = cssRule.getPropertyValues(PseudoClass.ROOT, "label",
"font", "shield", "halo");
if (values == null || values.isEmpty()) {
return;
}
int repeatCount = getMaxRepeatCount(values);
for (int i = 0; i < repeatCount; i++) {
final TextSymbolizerBuilder tb = ruleBuilder.text();
Expression labelGeometry = getExpression(values, "label-geometry", i);
if (labelGeometry != null) {
tb.geometry(labelGeometry);
}
// special handling for label, we allow multi-valued and treat as concatenation
Value labelValue = getValue(values, "label", i);
Expression labelExpression;
if (labelValue instanceof MultiValue) {
MultiValue m = (MultiValue) labelValue;
List<Expression> parts = new ArrayList<>();
for (Value mv : m.values) {
parts.add(mv.toExpression());
}
labelExpression = FF.function("Concatenate",
parts.toArray(new Expression[parts.size()]));
} else {
labelExpression = labelValue.toExpression();
}
tb.label(labelExpression);
Expression[] anchor = getExpressionArray(values, "label-anchor", i);
Expression[] offsets = getExpressionArray(values, "label-offset", i);
if (offsets != null && offsets.length == 1) {
tb.linePlacement().offset(offsets[0]);
} else if (offsets != null || anchor != null) {
PointPlacementBuilder ppb = tb.pointPlacement();
if (anchor != null) {
if (anchor.length == 2) {
ppb.anchor().x(anchor[0]);
ppb.anchor().y(anchor[1]);
} else if (anchor.length == 1) {
ppb.anchor().x(anchor[0]);
ppb.anchor().y(anchor[0]);
} else {
throw new IllegalArgumentException(
"Invalid anchor specification, should be two "
+ "floats between 0 and 1 with a space in between, instead it is "
+ getValue(values, "label-anchor", i));
}
}
if (offsets != null) {
if (offsets.length == 2) {
ppb.displacement().x(offsets[0]);
ppb.displacement().y(offsets[1]);
} else if (offsets.length == 1) {
ppb.displacement().x(offsets[0]);
ppb.displacement().y(offsets[0]);
} else {
throw new IllegalArgumentException(
"Invalid anchor specification, should be two "
+ "floats (or 1 for line placement with a certain offset) instead it is "
+ getValue(values, "label-anchor", i));
}
}
}
Expression rotation = getMeasureExpression(values, "label-rotation", i, "deg");
if (rotation != null) {
tb.pointPlacement().rotation(rotation);
}
for (Value shieldValue : getMultiValue(values, "shield", i)) {
new SubgraphicBuilder("shield", shieldValue, values, cssRule, i) {
@Override
protected GraphicBuilder getGraphicBuilder() {
return tb.shield();
}
};
}
// the color
Expression fill = getExpression(values, "font-fill", i);
if (fill != null) {
tb.fill().color(fill);
}
Expression opacity = getExpression(values, "font-opacity", i);
if (opacity != null) {
tb.fill().opacity(opacity);
}
// the fontdi
Map<String, List<Value>> fontLikeProperties = cssRule
.getPropertyValues(PseudoClass.ROOT, "font");
if (!fontLikeProperties.isEmpty() && (fontLikeProperties.size() > 1
|| fontLikeProperties.get("font-fill") == null)) {
int maxSize = getMaxMultiValueSize(values, i, "font-family", "font-style",
"font-weight", "font-family");
for (int j = 0; j < maxSize; j++) {
FontBuilder fb = tb.newFont();
Expression fontFamily = getExpression(
getValueInMulti(values, "font-family", i, j));
if (fontFamily != null) {
fb.family(fontFamily);
}
Expression fontStyle = getExpression(
getValueInMulti(values, "font-style", i, j));
if (fontStyle != null) {
fb.style(fontStyle);
}
Expression fontWeight = getExpression(
getValueInMulti(values, "font-weight", i, j));
if (fontWeight != null) {
fb.weight(fontWeight);
}
Expression fontSize = getMeasureExpression(
getValueInMulti(values, "font-size", i, j), "px");
if (fontSize != null) {
fb.size(fontSize);
}
}
}
// the halo
if (!cssRule.getPropertyValues(PseudoClass.ROOT, "halo").isEmpty()) {
HaloBuilder hb = tb.halo();
Expression haloRadius = getMeasureExpression(values, "halo-radius", i, "px");
if (haloRadius != null) {
hb.radius(haloRadius);
}
Expression haloColor = getExpression(values, "halo-color", i);
if (haloColor != null) {
hb.fill().color(haloColor);
}
Expression haloOpacity = getExpression(values, "halo-opacity", i);
if (haloOpacity != null) {
hb.fill().opacity(haloOpacity);
}
}
Expression priority = getExpression(values, "-gt-label-priority", i);
if (priority != null) {
tb.priority(priority);
}
addVendorOptions(tb, TEXT_VENDOR_OPTIONS, values, i);
}
}
/**
* Builds a raster symbolizer into the current rule, if a <code>raster-channels</code> property is found
*
* @param cssRule
* @param ruleBuilder
*/
private void addRasterSymbolizer(CssRule cssRule, RuleBuilder ruleBuilder) {
Map<String, List<Value>> values = cssRule.getPropertyValues(PseudoClass.ROOT, "raster","rce");
if (values == null || values.isEmpty()) {
return;
}
int repeatCount = getMaxRepeatCount(values);
for (int i = 0; i < repeatCount; i++) {
RasterSymbolizerBuilder rb = ruleBuilder.raster();
String[] channelNames = getStringArray(values, "raster-channels", i);
String[] constrastEnhancements = getStringArray(values, "raster-contrast-enhancement",
i);
HashMap<String, Expression> constrastParameters = new HashMap<>();
for (String cssKey : values.keySet()) {
String sldKey = CONTRASTENHANCMENT_VENDOR_OPTIONS.get(cssKey);
if (sldKey != null) {
constrastParameters.put(sldKey, getExpression(values, cssKey, i));
}
}
double[] gammas = getDoubleArray(values, "raster-gamma", i);
if (!"auto".equals(channelNames[0])) {
ChannelSelectionBuilder cs = rb.channelSelection();
if (channelNames.length == 1) {
applyContrastEnhancement(
cs.gray().channelName(channelNames[0]).contrastEnhancement(),
constrastEnhancements, constrastParameters, gammas, 0);
} else if (channelNames.length == 2 || channelNames.length > 3) {
throw new IllegalArgumentException(
"raster-channels can accept the name of one or three bands, not "
+ channelNames.length);
} else {
applyContrastEnhancement(
cs.red().channelName(channelNames[0]).contrastEnhancement(),
constrastEnhancements, constrastParameters, gammas,
0);
applyContrastEnhancement(
cs.green().channelName(channelNames[1]).contrastEnhancement(),
constrastEnhancements, constrastParameters, gammas,
1);
applyContrastEnhancement(
cs.blue().channelName(channelNames[2]).contrastEnhancement(),
constrastEnhancements, constrastParameters, gammas,
2);
}
} else {
applyContrastEnhancement(rb.contrastEnhancement(), constrastEnhancements,
constrastParameters, gammas, 0);
}
Expression opacity = getExpression(values, "raster-opacity", i);
if (opacity != null) {
rb.opacity(opacity);
}
Expression geom = getExpression(values, "raster-geometry", i);
if (geom != null) {
rb.geometry(geom);
}
Value v = getValue(values, "raster-color-map", i);
if (v != null) {
if (v instanceof Function) {
v = new MultiValue(v);
}
if (!(v instanceof MultiValue)) {
throw new IllegalArgumentException(
"Invalid color map, it must be comprised of one or more color-map-entry function: "
+ v);
} else {
MultiValue cm = (MultiValue) v;
ColorMapBuilder cmb = rb.colorMap();
for (Value entry : cm.values) {
if (!(entry instanceof Function)) {
throw new IllegalArgumentException(
"Invalid color map content, it must be a color-map-entry function"
+ entry);
}
Function f = (Function) entry;
if (!"color-map-entry".equals(f.name)) {
throw new IllegalArgumentException(
"Invalid color map content, it must be a color-map-entry function"
+ entry);
} else if (f.parameters.size() < 2 || f.parameters.size() > 3) {
throw new IllegalArgumentException(
"Invalid color map content, it must be a color-map-entry function "
+ "with either 2 parameters (color and value) or 3 parameters "
+ "(color, value and opacity)" + entry);
}
ColorMapEntryBuilder eb = cmb.entry();
eb.color(f.parameters.get(0).toExpression());
eb.quantity(f.parameters.get(1).toExpression());
if (f.parameters.size() == 3) {
eb.opacity(f.parameters.get(2).toExpression());
}
}
String type = getLiteral(values, "raster-color-map-type", i, null);
if (type != null) {
if ("intervals".equals(type)) {
cmb.type(ColorMap.TYPE_INTERVALS);
} else if ("ramp".equals(type)) {
cmb.type(ColorMap.TYPE_RAMP);
} else if ("values".equals(type)) {
cmb.type(ColorMap.TYPE_VALUES);
} else {
throw new IllegalArgumentException("Invalid color map type " + type);
}
}
}
}
addVendorOptions(rb, RASTER_VENDOR_OPTIONS, values, i);
}
}
/**
* Applies contrast enhancement for the i-th band
*
* @param ceb
* @param constrastEnhancements
* @param constrastAlgorithms
* @param constrastParameters
* @param gammas
* @param i
*/
private void applyContrastEnhancement(ContrastEnhancementBuilder ceb,
String[] constrastEnhancements, Map<String, Expression> constrastParameters,
double[] gammas, int i) {
if (constrastEnhancements != null && constrastEnhancements.length > 0) {
String contrastEnhancementName;
if (constrastEnhancements.length > i) {
contrastEnhancementName = constrastEnhancements[0];
} else {
contrastEnhancementName = constrastEnhancements[i];
}
//
if ("histogram".equals(contrastEnhancementName)) {
ceb.histogram(constrastParameters);
} else if ("normalize".equals(contrastEnhancementName)) {
ceb.normalize(constrastParameters);
} else if ("exponential".equals(contrastEnhancementName)) {
ceb.exponential(constrastParameters);
} else if ("logarithmic".equals(contrastEnhancementName)) {
ceb.logarithmic(constrastParameters);
} else if (!"none".equals(contrastEnhancementName)) {
//
throw new IllegalArgumentException("Invalid contrast enhancement name "
+ contrastEnhancementName
+ ", valid values are 'none', 'histogram', 'normalize', 'exponential' or 'logarithmic'");
}
} else {
ceb.unset();
}
if (gammas != null && gammas.length > 0) {
double gamma;
if (gammas.length > i) {
gamma = gammas[0];
} else {
gamma = gammas[i];
}
ceb.gamma(gamma);
}
}
/**
* Builds a graphic object into the current style build parent
*/
abstract class SubgraphicBuilder {
public SubgraphicBuilder(String propertyName, Value v, Map<String, List<Value>> values,
CssRule cssRule, int i) {
if (v != null) {
if (!(v instanceof Function)) {
throw new IllegalArgumentException(
"The value of '" + propertyName + "' must be a symbol or a url");
}
Function f = (Function) v;
GraphicBuilder gb = getGraphicBuilder();
if (Function.SYMBOL.equals(f.name)) {
buildMark(f.parameters.get(0), cssRule, propertyName, i, gb);
} else if (Function.URL.equals(f.name)) {
Value graphicLocation = f.parameters.get(0);
String location = graphicLocation.toLiteral();
// to turn stuff into SLD we need to make sure the URL is a valid one
// try {
// new URL(location);
// } catch (MalformedURLException e) {
// location = "file://" + location;
// }
String mime = getLiteral(values, propertyName + "-mime", i, "image/jpeg");
gb.externalGraphic(location, mime);
} else {
throw new IllegalArgumentException("'" + propertyName
+ "' accepts either a 'symbol' or a 'url' function, the following function is unrecognized: "
+ f);
}
Expression rotation = getMeasureExpression(values, propertyName + "-rotation", i,
"deg");
if (rotation != null) {
gb.rotation(rotation);
}
Expression size = getMeasureExpression(values, propertyName + "-size", i, "px");
if (size != null) {
gb.size(size);
}
double[] anchor = getDoubleArray(values, propertyName + "-anchor", i);
double[] offsets = getDoubleArray(values, propertyName + "-offset", i);
if (anchor != null) {
if (anchor.length == 2) {
gb.anchor().x(anchor[0]);
gb.anchor().y(anchor[1]);
} else if (anchor.length == 1) {
gb.anchor().x(anchor[0]);
gb.anchor().y(anchor[0]);
} else {
throw new IllegalArgumentException(
"Invalid anchor specification, should be two "
+ "floats between 0 and 1 with a space in between, instead it is "
+ getValue(values, propertyName + "-anchor", i));
}
}
if (offsets != null) {
if (offsets.length == 2) {
gb.displacement().x(offsets[0]);
gb.displacement().y(offsets[1]);
} else if (offsets.length == 1) {
gb.displacement().x(offsets[0]);
gb.displacement().y(offsets[0]);
} else {
throw new IllegalArgumentException(
"Invalid anchor specification, should be two "
+ "floats (or 1 for line placement with a certain offset) instead it is "
+ getValue(values, propertyName + "-anchor", i));
}
}
if ("mark".equals(propertyName)) {
Expression opacity = getExpression(values, "mark-opacity", i);
if (opacity != null) {
gb.opacity(opacity);
}
}
}
}
protected abstract GraphicBuilder getGraphicBuilder();
}
/**
* Builds the fill using a FillBuilder
*
* @param cssRule
* @param fb
* @param values
* @param i
*/
private void buildFill(CssRule cssRule, final FillBuilder fb, Map<String, List<Value>> values,
int i) {
for (Value fillValue : getMultiValue(values, "fill", i)) {
if (Function.isGraphicsFunction(fillValue)) {
new SubgraphicBuilder("fill", fillValue, values, cssRule, i) {
@Override
protected GraphicBuilder getGraphicBuilder() {
return fb.graphicFill();
}
};
} else if (fillValue != null) {
fb.color(getExpression(fillValue));
}
}
Expression opacity = getExpression(values, "fill-opacity", i);
if (opacity != null) {
fb.opacity(opacity);
}
}
/**
* Adds a line symbolizer, assuming the <code>stroke<code> property is found
*
* @param cssRule
* @param ruleBuilder
*/
private void addLineSymbolizer(CssRule cssRule, RuleBuilder ruleBuilder) {
Map<String, List<Value>> values = cssRule.getPropertyValues(PseudoClass.ROOT, "stroke");
if (values == null || values.isEmpty()) {
return;
}
int repeatCount = getMaxRepeatCount(values);
for (int i = 0; i < repeatCount; i++) {
if (getValue(values, "stroke", i) == null) {
continue;
}
LineSymbolizerBuilder lb = ruleBuilder.line();
Expression strokeGeometry = getExpression(values, "stroke-geometry", i);
if (strokeGeometry != null) {
lb.geometry(strokeGeometry);
}
Expression strokeOffset = getExpression(values, "stroke-offset", i);
if (strokeOffset != null && !isZero(strokeOffset)) {
lb.perpendicularOffset(strokeOffset);
}
StrokeBuilder strokeBuilder = lb.stroke();
buildStroke(cssRule, strokeBuilder, values, i);
addVendorOptions(lb, LINE_VENDOR_OPTIONS, values, i);
}
}
/**
* Returns true if the expression is a constant value zero
* @param expression
* @return
*/
private boolean isZero(Expression expression) {
if(!(expression instanceof org.opengis.filter.expression.Literal)) {
return false;
}
org.opengis.filter.expression.Literal l = (org.opengis.filter.expression.Literal) expression;
return l.evaluate(null, Double.class) == 0;
}
/**
* Builds a stroke using the stroke buidler for the i-th set of property values
*
* @param cssRule
* @param strokeBuilder
* @param values
* @param i
*/
private void buildStroke(CssRule cssRule, final StrokeBuilder strokeBuilder,
final Map<String, List<Value>> values, final int i) {
boolean simpleStroke = false;
for (Value strokeValue : getMultiValue(values, "stroke", i)) {
if (Function.isGraphicsFunction(strokeValue)) {
new SubgraphicBuilder("stroke", strokeValue, values, cssRule, i) {
@Override
protected GraphicBuilder getGraphicBuilder() {
String repeat = getLiteral(values, "stroke-repeat", i, "repeat");
if ("repeat".equals(repeat)) {
return strokeBuilder.graphicStroke();
} else {
return strokeBuilder.fillBuilder();
}
}
};
} else if (strokeValue != null) {
simpleStroke = true;
strokeBuilder.color(strokeValue.toExpression());
}
}
if (simpleStroke) {
Expression opacity = getExpression(values, "stroke-opacity", i);
if (opacity != null) {
strokeBuilder.opacity(opacity);
}
Expression width = getMeasureExpression(values, "stroke-width", i, "px");
if (width != null) {
strokeBuilder.width(width);
}
Expression lineCap = getExpression(values, "stroke-linecap", i);
if (lineCap != null) {
strokeBuilder.lineCap(lineCap);
}
Expression lineJoin = getExpression(values, "stroke-linejoin", i);
if (lineJoin != null) {
strokeBuilder.lineJoin(lineJoin);
}
}
float[] dasharray = getFloatArray(values, "stroke-dasharray", i);
if (dasharray != null) {
strokeBuilder.dashArray(dasharray);
}
Expression dashOffset = getMeasureExpression(values, "stroke-dashoffset", i, "px");
if (dashOffset != null) {
strokeBuilder.dashOffset(dashOffset);
}
}
/**
* Adds the vendor options available
*
* @param sb
* @param vendorOptions
* @param values
* @param idx
*/
private void addVendorOptions(SymbolizerBuilder<?> sb, Map<String, String> vendorOptions,
Map<String, List<Value>> values, int idx) {
// for (Map.Entry<String, String> entry : vendorOptions.entrySet()) {
// String cssKey = entry.getKey();
// String sldKey = entry.getValue();
// String value = getLiteral(values, cssKey, idx, null);
// if (value != null) {
// sb.option(sldKey, value);
// }
// }
for (String cssKey : values.keySet()) {
String sldKey = vendorOptions.get(cssKey);
if (sldKey != null) {
String value = getLiteral(values, cssKey, idx, null);
if (value != null) {
sb.option(sldKey, value);
}
}
}
}
/**
* Builds a mark into the graphic builder from the idx-th set of property alues
*
* @param markName
* @param cssRule
* @param indexedPseudoClass
* @param idx
* @param gb
*/
private void buildMark(Value markName, CssRule cssRule, String indexedPseudoClass, int idx,
GraphicBuilder gb) {
MarkBuilder mark = gb.mark();
mark.name(markName.toExpression());
// see if we have a pseudo-selector for this idx
Map<String, List<Value>> values = getValuesForIndexedPseudoClass(cssRule,
indexedPseudoClass, idx);
if (values == null || values.isEmpty()) {
mark.fill().reset();
mark.stroke().reset();
} else {
// unless specified and empty, a mark always has a fill and a stroke
if (values.containsKey("fill") && values.get("fill") != null) {
FillBuilder fb = mark.fill();
buildFill(cssRule, fb, values, idx);
} else if (!values.containsKey("fill")) {
mark.fill();
}
if (values.containsKey("stroke") && values.get("stroke") != null) {
StrokeBuilder sb = mark.stroke();
buildStroke(cssRule, sb, values, idx);
} else if (!values.containsKey("stroke")) {
mark.stroke();
}
}
Expression size = getMeasureExpression(values, "size", idx, "px");
if (size != null) {
gb.size(size);
}
Expression rotation = getMeasureExpression(values, "rotation", idx, "deg");
if (rotation != null) {
gb.rotation(rotation);
}
}
/**
* Returns the set of values for the idx-th pseudo-class taking into account both generic and non indexed pseudo class names
*
* @param cssRule
* @param pseudoClassName
* @param idx
* @return
*/
private Map<String, List<Value>> getValuesForIndexedPseudoClass(CssRule cssRule,
String pseudoClassName, int idx) {
Map<String, List<Value>> combined = new LinkedHashMap<>();
// catch all ones
combined.putAll(cssRule.getPropertyValues(PseudoClass.newPseudoClass("symbol")));
// catch all index specific
combined.putAll(cssRule.getPropertyValues(PseudoClass.newPseudoClass("symbol", idx + 1)));
// symbol specific ones
combined.putAll(cssRule.getPropertyValues(PseudoClass.newPseudoClass(pseudoClassName)));
// symbol and index specific ones
combined.putAll(
cssRule.getPropertyValues(PseudoClass.newPseudoClass(pseudoClassName, idx + 1)));
return combined;
}
/**
* Builds an expression out of the i-th value
*
* @param valueMap
* @param name
* @param i
* @return
*/
private Expression getExpression(Map<String, List<Value>> valueMap, String name, int i) {
Value v = getValue(valueMap, name, i);
return getExpression(v);
}
/**
* Builds/grabs an expression from the specified value, if a multi value is passed the first value will be used
*
* @param v
* @return
*/
private Expression getExpression(Value v) {
if (v == null) {
return null;
} else {
if (v instanceof MultiValue) {
return ((MultiValue) v).values.get(0).toExpression();
} else {
return v.toExpression();
}
}
}
/**
* Returns an expression for the i-th value of the specified property, taking into account units of measure
*
* @param valueMap
* @param name
* @param i
* @param defaultUnit
* @return
*/
private Expression getMeasureExpression(Map<String, List<Value>> valueMap, String name, int i,
String defaultUnit) {
Value v = getValue(valueMap, name, i);
return getMeasureExpression(v, defaultUnit);
}
private Expression getMeasureExpression(Value v, String defaultUnit) {
if (v == null) {
return null;
} else if (v instanceof Literal) {
String literal = v.toLiteral();
if (literal.endsWith(defaultUnit)) {
String simplified = literal.substring(0, literal.length() - defaultUnit.length());
return FF.literal(simplified);
} else {
return FF.literal(literal);
}
} else {
return v.toExpression();
}
}
/**
* Returns the i-th value of the specified property
*
* @param valueMap
* @param name
* @param i
* @return
*/
private Value getValue(Map<String, List<Value>> valueMap, String name, int i) {
List<Value> values = valueMap.get(name);
if (values == null || values.isEmpty()) {
return null;
}
if (values.size() == 1) {
return values.get(0);
} else if (i > values.size()) {
return null;
} else {
return values.get(i);
}
}
private List<Value> getMultiValue(Map<String, List<Value>> valueMap, String name, int i) {
Value value = getValue(valueMap, name, i);
if (value instanceof MultiValue) {
return ((MultiValue) value).values;
} else if (value == null) {
return Collections.emptyList();
} else {
return Collections.singletonList(value);
}
}
public int getMaxMultiValueSize(Map<String, List<Value>> valueMap, int i, String... names) {
int max = 0;
for (String name : names) {
List<Value> values = getMultiValue(valueMap, name, i);
int size = values.size();
if (size > max) {
max = size;
}
}
return max;
}
public Value getValueInMulti(Map<String, List<Value>> valueMap, String name, int i,
int valueIdx) {
List<Value> values = getMultiValue(valueMap, name, i);
if (values.isEmpty()) {
return null;
} else if (values.size() <= valueIdx) {
return values.get(values.size() - 1);
} else {
return values.get(valueIdx);
}
}
/**
* Returns the i-th value of the specified property, as a literal
*
* @param valueMap
* @param name
* @param i
* @param defaultValue
* @return
*/
private String getLiteral(Map<String, List<Value>> valueMap, String name, int i,
String defaultValue) {
Value v = getValue(valueMap, name, i);
if (v == null) {
return defaultValue;
} else {
return v.toLiteral();
}
}
/**
* Returns the i-th value of the specified property, as a array of floats
*
* @param valueMap
* @param name
* @param i
* @return
*/
private float[] getFloatArray(Map<String, List<Value>> valueMap, String name, int i) {
double[] doubles = getDoubleArray(valueMap, name, i);
if (doubles == null) {
return null;
} else {
float[] floats = new float[doubles.length];
for (int j = 0; j < doubles.length; j++) {
floats[j] = (float) doubles[j];
}
return floats;
}
}
/**
* Returns the i-th value of the specified property, as a array of doubles
*
* @param valueMap
* @param name
* @param i
* @return
*/
private double[] getDoubleArray(Map<String, List<Value>> valueMap, String name, int i) {
Value v = getValue(valueMap, name, i);
if (v == null) {
return null;
}
if (v instanceof MultiValue) {
MultiValue m = (MultiValue) v;
if (m.values.size() == 0) {
return null;
}
double[] result = new double[m.values.size()];
for (int j = 0; j < m.values.size(); j++) {
String literal = m.values.get(j).toLiteral();
if (literal.endsWith("%")) {
literal = literal.substring(0, literal.length() - 1);
double d = Double.parseDouble(literal);
result[j] = d / 100d;
} else {
result[j] = Double.parseDouble(literal);
}
}
return result;
} else {
return new double[] { Double.parseDouble(v.toLiteral()) };
}
}
/**
* Returns the i-th value of the specified property, as a array of strings
*
* @param valueMap
* @param name
* @param i
* @return
*/
private String[] getStringArray(Map<String, List<Value>> valueMap, String name, int i) {
Value v = getValue(valueMap, name, i);
if (v == null) {
return null;
}
if (v instanceof MultiValue) {
MultiValue m = (MultiValue) v;
if (m.values.size() == 0) {
return null;
}
String[] result = new String[m.values.size()];
for (int j = 0; j < m.values.size(); j++) {
result[j] = m.values.get(j).toLiteral();
}
return result;
} else {
return new String[] { v.toLiteral() };
}
}
/**
* Returns the i-th value of the specified property, as a array of expressions
*
* @param valueMap
* @param name
* @param i
* @return
*/
private Expression[] getExpressionArray(Map<String, List<Value>> valueMap, String name, int i) {
Value v = getValue(valueMap, name, i);
if (v == null) {
return null;
}
if (v instanceof MultiValue) {
MultiValue m = (MultiValue) v;
if (m.values.size() == 0) {
return null;
}
Expression[] result = new Expression[m.values.size()];
for (int j = 0; j < m.values.size(); j++) {
result[j] = m.values.get(j).toExpression();
}
return result;
} else {
return new Expression[] { v.toExpression() };
}
}
/**
* 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;
}
public static void main(String[] args) throws IOException, TransformerException {
if (args.length != 2) {
System.err.println("Usage: CssTranslator <input.css> <output.sld>");
System.exit(-1);
}
File input = new File(args[0]);
if (!input.exists()) {
System.err.println("Could not locate input file " + input.getPath());
System.exit(-2);
}
File output = new File(args[1]);
File outputParent = output.getParentFile();
if (!outputParent.exists() && !outputParent.mkdirs()) {
System.err
.println("Output file parent directory does not exist, and cannot be created: "
+ outputParent.getPath());
System.exit(-2);
}
long start = System.currentTimeMillis();
String css = FileUtils.readFileToString(input);
Stylesheet styleSheet = CssParser.parse(css);
java.util.logging.ConsoleHandler handler = new java.util.logging.ConsoleHandler();
handler.setLevel(java.util.logging.Level.FINE);
org.geotools.util.logging.Logging.getLogger("org.geotools.styling.css")
.setLevel(java.util.logging.Level.FINE);
org.geotools.util.logging.Logging.getLogger("org.geotools.styling.css").addHandler(handler);
CssTranslator translator = new CssTranslator();
Style style = translator.translate(styleSheet);
StyleFactory styleFactory = CommonFactoryFinder.getStyleFactory();
StyledLayerDescriptor sld = styleFactory.createStyledLayerDescriptor();
NamedLayer layer = styleFactory.createNamedLayer();
layer.addStyle((org.geotools.styling.Style) style);
sld.layers().add(layer);
SLDTransformer tx = new SLDTransformer();
tx.setIndentation(2);
try (FileOutputStream fos = new FileOutputStream(output)) {
tx.transform(sld, fos);
}
long end = System.currentTimeMillis();
System.out.println("Translation performed in " + (end - start) / 1000d + " seconds");
}
}