/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2016 Open Source Geospatial Foundation (OSGeo)
* (C) 2014-2016 Boundless Spatial
*
* 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.ysld.parse;
import org.geotools.filter.function.FilterFunction_strConcat;
import org.geotools.filter.function.string.ConcatenateFunction;
import org.geotools.renderer.style.ExpressionExtractor;
import org.geotools.styling.AnchorPoint;
import org.geotools.styling.Displacement;
import org.geotools.ysld.Colors;
import org.geotools.ysld.Tuple;
import org.geotools.ysld.YamlMap;
import org.geotools.ysld.Ysld;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Function;
import org.opengis.filter.expression.Literal;
import java.awt.Color;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/**
* Parsing utilities
*/
public class Util {
/**
* Pattern to catch attribute expressions.
*/
static final Pattern ATTRIBUTE_PATTERN = Pattern.compile("\\[.+\\]");
/**
* Pattern to catch url style well known names
*/
static final Pattern WELLKNOWNNAME_PATTERN = Pattern.compile("\\w+://.+");
/**
* Pattern to catch 6 digit hex colors.
*/
static final Pattern COLOR_PATTERN = Pattern.compile("#\\p{XDigit}{6}");
/**
* Parses an expression from its string representation.
*/
public static Expression expression(String value, Factory factory) {
return expression(value, false, factory);
}
private static void collectExpressions(List<Expression> list, Expression expr) {
if (expr == null)
return;
if (expr instanceof ConcatenateFunction || expr instanceof FilterFunction_strConcat) {
for (Expression param : ((Function) expr).getParameters()) {
collectExpressions(list, param);
}
return;
} else {
if (expr instanceof Literal) {
Object value = ((Literal) expr).getValue();
if (value == null)
return;
if (value instanceof String && ((String) value).isEmpty())
return;
}
list.add(expr);
}
}
/**
* Simplifies an {@link Expression} which may contain multiple {@link ConcatenateFunction} into a single top-level
* {@link ConcatenateFunction} with a flat list of parameters.
*
* If the passed {@link Expression} performs no concatenation, it is returned as-is.
* If the passed {@link Expression} represents an empty value, a {@link Literal} expression with value null is returned.
*
* @param expr
* @param factory Function factory
* @return Simplified expression
*/
public static Expression unwrapConcatenates(Expression expr, Factory factory) {
List<Expression> list = splitConcatenates(expr);
if (list.isEmpty()) {
return factory.filter.literal(null);
} else if (list.size() == 1) {
return list.get(0);
} else {
return factory.filter.function("Concatenate", list.toArray(new Expression[] {}));
}
}
/**
* Splits an {@link Expression} into a list of expressions by removing {@link ConcatenateFunction} and
* {@link FilterFunction_strConcat} Functions, and listing the children of those functions.
* This is applied recursively, so nested Expressions are also handled.
* Null-valued or empty {@link Literal} Expressions are removed.
*
* @param expr
* @return List of expressions, containing no concatenate functions
*/
public static List<Expression> splitConcatenates(Expression expr) {
List<Expression> list = new ArrayList<>();
collectExpressions(list, expr);
return list;
}
/**
* Parses an expression from its string representation.
* <p>
* The <tt>safe</tt> parameter when set to true will cause null to be returned when the string can not be parsed as a ECQL expression. When false
* it will result in an exception thrown back.
* </p>
* @return The parsed expression, or null if the string value was empty.
*/
public static Expression expression(String value, boolean safe, Factory factory) {
if (value.isEmpty()) {
return null;
}
Expression expr = ExpressionExtractor.extractCqlExpressions(value);
expr = unwrapConcatenates(expr, factory);
// Expression expr = ECQL.toExpression(value, factory.filter);
/*
* if (expr instanceof PropertyName && !ATTRIBUTE_PATTERN.matcher(value).matches()) { // treat as literal return
* factory.filter.literal(((PropertyName) expr).getPropertyName()); }
*/
return expr;
}
/**
* Parses an anchor tuple.
*/
public static AnchorPoint anchor(Object value, Factory factory) {
Tuple t = null;
try {
t = Tuple.of(2).parse(value);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
String.format("Bad anchor: '%s', must be of form (<x>,<y>)", value), e);
}
Expression x = t.at(0) != null ? expression(t.strAt(0), factory)
: factory.filter.literal(0);
Expression y = t.at(1) != null ? expression(t.strAt(1), factory)
: factory.filter.literal(0);
return factory.style.createAnchorPoint(x, y);
}
/**
* Parses an displacement tuple.
*/
public static Displacement displacement(Object value, Factory factory) {
Tuple t = null;
try {
t = Tuple.of(2).parse(value);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
String.format("Bad displacement: '%s', must be of form (<x>,<y>)", value), e);
}
Expression x = t.at(0) != null ? expression(t.strAt(0), factory)
: factory.filter.literal(0);
Expression y = t.at(1) != null ? expression(t.strAt(1), factory)
: factory.filter.literal(0);
return factory.style.createDisplacement(x, y);
}
static final Pattern HEX_PATTERN = Pattern
.compile("\\s*(?:(?:0x)|#)?([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})\\s*");
static final Pattern RGB_PATTERN = Pattern.compile(
"\\s*rgb\\s*\\(\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*\\)\\s*",
Pattern.CASE_INSENSITIVE);
/**
* Parses a color from string representation.
*/
public static Expression color(Object value, Factory factory) {
Color color = null;
if (value instanceof String) {
Matcher m = HEX_PATTERN.matcher((String) value);
if (m.matches()) {
color = parseColorAsHex(m);
}
if (color == null) {
m = RGB_PATTERN.matcher((String) value);
if (m.matches()) {
color = parseColorAsRGB(m);
}
}
if (color == null) {
color = Colors.get((String) value);
}
} else if (value instanceof Integer) {
color = new Color((int) value);
}
if (value != null)
value = value.toString();
return color != null ? factory.filter.literal(color) : expression((String) value, factory);
}
static Color parseColorAsHex(Matcher m) {
String hex = m.group(1);
if (hex.length() == 3) {
return new Color(0x11 * Integer.parseInt(hex.substring(0, 1), 16),
0x11 * Integer.parseInt(hex.substring(1, 2), 16),
0x11 * Integer.parseInt(hex.substring(2, 3), 16));
}
return new Color(Integer.parseInt(hex.substring(0, 2), 16),
Integer.parseInt(hex.substring(2, 4), 16),
Integer.parseInt(hex.substring(4, 6), 16));
}
static Color parseColorAsRGB(Matcher m) {
return new Color(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)),
Integer.parseInt(m.group(3)));
}
/**
* Parses a float array from a space delimited list.
*/
public static float[] floatArray(String value) {
List<Float> list = new ArrayList<Float>();
for (String str : value.split(" ")) {
list.add(Float.parseFloat(str));
}
float[] array = new float[list.size()];
for (int i = 0; i < list.size(); i++) {
array[i] = list.get(i).floatValue();
}
return array;
}
/**
* Returns the first non-null parameter or null.
*/
@SafeVarargs
@Nullable
public static <T> T defaultForNull(@Nullable T... options) {
for (T o : options) {
if (o != null)
return o;
}
return null;
}
/**
* Returns the first non-null parameter or throws NullPointerException.
*/
@SafeVarargs
public static <T> T forceDefaultForNull(@Nullable T... options) {
for (T o : options) {
if (o != null)
return o;
}
throw new NullPointerException();
}
/**
* Finds an applicable {@link ZoomContext} based on a name
* @param name Name of the ZoomContext.
* @param zCtxtFinders List of finders for the {@link ZoomContext}
* @throws IllegalArgumentException If name is "EPSG:4326", "EPSG:3857", or "EPSG:900913" (These names cause ambiguities).
* @return {@link ZoomContext} matching the name.
*/
public static @Nullable ZoomContext getNamedZoomContext(String name,
List<ZoomContextFinder> zCtxtFinders) {
if (name.equalsIgnoreCase("EPSG:4326")) {
throw new IllegalArgumentException(
"Should not use EPSG code to refer to WGS84 zoom levels as it causes ambiguities");
}
if (name.equalsIgnoreCase("EPSG:3857") || name.equalsIgnoreCase("EPSG:900913")) {
throw new IllegalArgumentException(
"Should not use EPSG code to refer to WebMercator zoom levels");
}
for (ZoomContextFinder finder : zCtxtFinders) {
ZoomContext found = finder.get(name);
if (found != null) {
return found;
}
}
return WellKnownZoomContextFinder.getInstance().get(name);
}
static final Pattern EMBEDED_EXPRESSION_ESCAPED = Pattern.compile("\\\\([$}\\\\])");
static final Pattern EMBEDED_FILTER = Pattern.compile("^\\s*\\$\\{(.*?)\\}\\s*$");
/**
* Removes up to one set of ${ } expression brackets from a YSLD string.
* Escape sequences for the characters $}\ within the brackets are unescaped.
*
* @param s
* @return s with brackets and escape sequences removed.
*/
public static String removeExpressionBrackets(String s) {
Matcher m1 = EMBEDED_FILTER.matcher(s);
if (m1.matches()) {
return EMBEDED_EXPRESSION_ESCAPED.matcher(m1.group(1)).replaceAll("$1");
}
return s;
}
/**
*
* @return A number parsed from the provided string, or the string itself if parsing failed. Also returns null if the string was null.
*/
public static Object makeNumberIfPossible(String str) {
if (str == null)
return null;
try {
return Long.parseLong(str);
} catch (NumberFormatException e1) {
try {
return Double.parseDouble(str);
} catch (NumberFormatException e2) {
if ("true".equalsIgnoreCase(str) || "false".equalsIgnoreCase(str)) {
return Boolean.parseBoolean(str);
}
}
}
return str;
}
/**
* Serializes a Java {@link Color} to a String representation of the format "#RRGGBB"
*
* @param c Color
* @return String representation of c
*/
public static String serializeColor(Color c) {
return String.format("#%06X", c.getRGB() & 0x00_FF_FF_FF);
}
/**
*
* @return The string with quotes (') stripped from the beginning and end, or null if the string was null.
*/
public static String stripQuotes(String str) {
if (str == null) {
return str;
}
// strip quotes
if (str.charAt(0) == '\'') {
str = str.substring(1);
}
if (str.charAt(str.length() - 1) == '\'') {
str = str.substring(0, str.length() - 1);
}
return str;
}
/**
* Parse all vendor options (keys starting with 'x-')
*
* @param sourceMap YamlMap to parse
* @return A map of the vendor options
*/
static public Map<String, String> vendorOptions(YamlMap sourceMap) {
Map<String, String> optionMap = new HashMap<>();
for (String key : sourceMap) {
if (key.startsWith(Ysld.OPTION_PREFIX)) {
String option = key.substring(Ysld.OPTION_PREFIX.length());
optionMap.put(option, sourceMap.str(key));
}
}
return optionMap;
}
}