// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.mappaint.mapcss; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.MessageFormat; import java.util.Arrays; import java.util.EnumSet; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.BiFunction; import java.util.function.IntFunction; import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import org.openstreetmap.josm.actions.search.SearchCompiler.InDataSourceArea; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.OsmUtils; import org.openstreetmap.josm.data.osm.Relation; import org.openstreetmap.josm.data.osm.Tag; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; import org.openstreetmap.josm.gui.mappaint.Cascade; import org.openstreetmap.josm.gui.mappaint.ElemStyles; import org.openstreetmap.josm.gui.mappaint.Environment; import org.openstreetmap.josm.gui.mappaint.mapcss.Condition.Context; import org.openstreetmap.josm.gui.mappaint.mapcss.Condition.ToTagConvertable; import org.openstreetmap.josm.tools.CheckParameterUtil; import org.openstreetmap.josm.tools.JosmRuntimeException; import org.openstreetmap.josm.tools.Utils; /** * Factory to generate {@link Condition}s. * @since 10837 (Extracted from Condition) */ public final class ConditionFactory { private ConditionFactory() { // Hide default constructor for utils classes } /** * Create a new condition that checks the key and the value of the object. * @param k The key. * @param v The reference value * @param op The operation to use when comparing the value * @param context The type of context to use. * @param considerValAsKey whether to consider {@code v} as another key and compare the values of key {@code k} and key {@code v}. * @return The new condition. * @throws MapCSSException if the arguments are incorrect */ public static Condition createKeyValueCondition(String k, String v, Op op, Context context, boolean considerValAsKey) { switch (context) { case PRIMITIVE: if (KeyValueRegexpCondition.SUPPORTED_OPS.contains(op) && !considerValAsKey) { try { return new KeyValueRegexpCondition(k, v, op, false); } catch (PatternSyntaxException e) { throw new MapCSSException(e); } } if (!considerValAsKey && op.equals(Op.EQ)) return new SimpleKeyValueCondition(k, v); return new KeyValueCondition(k, v, op, considerValAsKey); case LINK: if (considerValAsKey) throw new MapCSSException("''considerValAsKey'' not supported in LINK context"); if ("role".equalsIgnoreCase(k)) return new RoleCondition(v, op); else if ("index".equalsIgnoreCase(k)) return new IndexCondition(v, op); else throw new MapCSSException( MessageFormat.format("Expected key ''role'' or ''index'' in link context. Got ''{0}''.", k)); default: throw new AssertionError(); } } /** * Create a condition in which the key and the value need to match a given regexp * @param k The key regexp * @param v The value regexp * @param op The operation to use when comparing the key and the value. * @return The new condition. */ public static Condition createRegexpKeyRegexpValueCondition(String k, String v, Op op) { return new RegexpKeyValueRegexpCondition(k, v, op); } /** * Creates a condition that checks the given key. * @param k The key to test for * @param not <code>true</code> to invert the match * @param matchType The match type to check for. * @param context The context this rule is found in. * @return the new condition. */ public static Condition createKeyCondition(String k, boolean not, KeyMatchType matchType, Context context) { switch (context) { case PRIMITIVE: return new KeyCondition(k, not, matchType); case LINK: if (matchType != null) throw new MapCSSException("Question mark operator ''?'' and regexp match not supported in LINK context"); if (not) return new RoleCondition(k, Op.NEQ); else return new RoleCondition(k, Op.EQ); default: throw new AssertionError(); } } /** * Create a new pseudo class condition * @param id The id of the pseudo class * @param not <code>true</code> to invert the condition * @param context The context the class is found in. * @return The new condition */ public static PseudoClassCondition createPseudoClassCondition(String id, boolean not, Context context) { return PseudoClassCondition.createPseudoClassCondition(id, not, context); } /** * Create a new class condition * @param id The id of the class to match * @param not <code>true</code> to invert the condition * @param context Ignored * @return The new condition */ public static ClassCondition createClassCondition(String id, boolean not, Context context) { return new ClassCondition(id, not); } /** * Create a new condition that a expression needs to be fulfilled * @param e the expression to check * @param context Ignored * @return The new condition */ public static ExpressionCondition createExpressionCondition(Expression e, Context context) { return new ExpressionCondition(e); } /** * This is the operation that {@link KeyValueCondition} uses to match. */ public enum Op { /** The value equals the given reference. */ EQ(Objects::equals), /** The value does not equal the reference. */ NEQ(EQ), /** The value is greater than or equal to the given reference value (as float). */ GREATER_OR_EQUAL(comparisonResult -> comparisonResult >= 0), /** The value is greater than the given reference value (as float). */ GREATER(comparisonResult -> comparisonResult > 0), /** The value is less than or equal to the given reference value (as float). */ LESS_OR_EQUAL(comparisonResult -> comparisonResult <= 0), /** The value is less than the given reference value (as float). */ LESS(comparisonResult -> comparisonResult < 0), /** The reference is treated as regular expression and the value needs to match it. */ REGEX((test, prototype) -> Pattern.compile(prototype).matcher(test).find()), /** The reference is treated as regular expression and the value needs to not match it. */ NREGEX(REGEX), /** The reference is treated as a list separated by ';'. Spaces around the ; are ignored. * The value needs to be equal one of the list elements. */ ONE_OF((test, prototype) -> Arrays.asList(test.split("\\s*;\\s*")).contains(prototype)), /** The value needs to begin with the reference string. */ BEGINS_WITH(String::startsWith), /** The value needs to end with the reference string. */ ENDS_WITH(String::endsWith), /** The value needs to contain the reference string. */ CONTAINS(String::contains); static final Set<Op> NEGATED_OPS = EnumSet.of(NEQ, NREGEX); private final BiFunction<String, String, Boolean> function; private final boolean negated; /** * Create a new string operation. * @param func The function to apply during {@link #eval(String, String)}. */ Op(BiFunction<String, String, Boolean> func) { this.function = func; negated = false; } /** * Create a new float operation that compares two float values * @param comparatorResult A function to mapt the result of the comparison */ Op(IntFunction<Boolean> comparatorResult) { this.function = (test, prototype) -> { float testFloat; try { testFloat = Float.parseFloat(test); } catch (NumberFormatException e) { return Boolean.FALSE; } float prototypeFloat = Float.parseFloat(prototype); int res = Float.compare(testFloat, prototypeFloat); return comparatorResult.apply(res); }; negated = false; } /** * Create a new Op by negating an other op. * @param negate inverse operation */ Op(Op negate) { this.function = (a, b) -> !negate.function.apply(a, b); negated = true; } /** * Evaluates a value against a reference string. * @param testString The value. May be <code>null</code> * @param prototypeString The reference string- * @return <code>true</code> if and only if this operation matches for the given value/reference pair. */ public boolean eval(String testString, String prototypeString) { if (testString == null) return negated; else return function.apply(testString, prototypeString); } } /** * Most common case of a KeyValueCondition, this is the basic key=value case. * * Extra class for performance reasons. */ public static class SimpleKeyValueCondition implements Condition, ToTagConvertable { /** * The key to search for. */ public final String k; /** * The value to search for. */ public final String v; /** * Create a new SimpleKeyValueCondition. * @param k The key * @param v The value. */ public SimpleKeyValueCondition(String k, String v) { this.k = k; this.v = v; } @Override public boolean applies(Environment e) { return v.equals(e.osm.get(k)); } @Override public Tag asTag(OsmPrimitive primitive) { return new Tag(k, v); } @Override public String toString() { return '[' + k + '=' + v + ']'; } } /** * <p>Represents a key/value condition which is either applied to a primitive.</p> * */ public static class KeyValueCondition implements Condition, ToTagConvertable { /** * The key to search for. */ public final String k; /** * The value to search for. */ public final String v; /** * The key/value match operation. */ public final Op op; /** * If this flag is set, {@link #v} is treated as a key and the value is the value set for that key. */ public final boolean considerValAsKey; /** * <p>Creates a key/value-condition.</p> * * @param k the key * @param v the value * @param op the operation * @param considerValAsKey whether to consider {@code v} as another key and compare the values of key {@code k} and key {@code v}. */ public KeyValueCondition(String k, String v, Op op, boolean considerValAsKey) { this.k = k; this.v = v; this.op = op; this.considerValAsKey = considerValAsKey; } @Override public boolean applies(Environment env) { return op.eval(env.osm.get(k), considerValAsKey ? env.osm.get(v) : v); } @Override public Tag asTag(OsmPrimitive primitive) { return new Tag(k, v); } @Override public String toString() { return '[' + k + '\'' + op + '\'' + v + ']'; } } /** * This condition requires a fixed key to match a given regexp */ public static class KeyValueRegexpCondition extends KeyValueCondition { protected static final Set<Op> SUPPORTED_OPS = EnumSet.of(Op.REGEX, Op.NREGEX); final Pattern pattern; /** * Constructs a new {@code KeyValueRegexpCondition}. * @param k key * @param v value * @param op operation * @param considerValAsKey must be false * @throws PatternSyntaxException if the value syntax is invalid */ public KeyValueRegexpCondition(String k, String v, Op op, boolean considerValAsKey) { super(k, v, op, considerValAsKey); CheckParameterUtil.ensureThat(!considerValAsKey, "considerValAsKey is not supported"); CheckParameterUtil.ensureThat(SUPPORTED_OPS.contains(op), "Op must be REGEX or NREGEX"); this.pattern = Pattern.compile(v); } protected boolean matches(Environment env) { final String value = env.osm.get(k); return value != null && pattern.matcher(value).find(); } @Override public boolean applies(Environment env) { if (Op.REGEX.equals(op)) { return matches(env); } else if (Op.NREGEX.equals(op)) { return !matches(env); } else { throw new IllegalStateException(); } } } /** * A condition that checks that a key with the matching pattern has a value with the matching pattern. */ public static class RegexpKeyValueRegexpCondition extends KeyValueRegexpCondition { final Pattern keyPattern; /** * Create a condition in which the key and the value need to match a given regexp * @param k The key regexp * @param v The value regexp * @param op The operation to use when comparing the key and the value. */ public RegexpKeyValueRegexpCondition(String k, String v, Op op) { super(k, v, op, false); this.keyPattern = Pattern.compile(k); } @Override protected boolean matches(Environment env) { for (Map.Entry<String, String> kv: env.osm.getKeys().entrySet()) { if (keyPattern.matcher(kv.getKey()).find() && pattern.matcher(kv.getValue()).find()) { return true; } } return false; } } /** * Role condition. */ public static class RoleCondition implements Condition { final String role; final Op op; /** * Constructs a new {@code RoleCondition}. * @param role role * @param op operation */ public RoleCondition(String role, Op op) { this.role = role; this.op = op; } @Override public boolean applies(Environment env) { String testRole = env.getRole(); if (testRole == null) return false; return op.eval(testRole, role); } } /** * Index condition. */ public static class IndexCondition implements Condition { final String index; final Op op; /** * Constructs a new {@code IndexCondition}. * @param index index * @param op operation */ public IndexCondition(String index, Op op) { this.index = index; this.op = op; } @Override public boolean applies(Environment env) { if (env.index == null) return false; if (index.startsWith("-")) { return env.count != null && op.eval(Integer.toString(env.index - env.count), index); } else { return op.eval(Integer.toString(env.index + 1), index); } } } /** * This defines how {@link KeyCondition} matches a given key. */ public enum KeyMatchType { /** * The key needs to be equal to the given label. */ EQ, /** * The key needs to have a true value (yes, ...) * @see OsmUtils#isTrue(String) */ TRUE, /** * The key needs to have a false value (no, ...) * @see OsmUtils#isFalse(String) */ FALSE, /** * The key needs to match the given regular expression. */ REGEX } /** * <p>KeyCondition represent one of the following conditions in either the link or the * primitive context:</p> * <pre> * ["a label"] PRIMITIVE: the primitive has a tag "a label" * LINK: the parent is a relation and it has at least one member with the role * "a label" referring to the child * * [!"a label"] PRIMITIVE: the primitive doesn't have a tag "a label" * LINK: the parent is a relation but doesn't have a member with the role * "a label" referring to the child * * ["a label"?] PRIMITIVE: the primitive has a tag "a label" whose value evaluates to a true-value * LINK: not supported * * ["a label"?!] PRIMITIVE: the primitive has a tag "a label" whose value evaluates to a false-value * LINK: not supported * </pre> */ public static class KeyCondition implements Condition, ToTagConvertable { /** * The key name. */ public final String label; /** * If we should negate the result of the match. */ public final boolean negateResult; /** * Describes how to match the label against the key. * @see KeyMatchType */ public final KeyMatchType matchType; /** * A predicate used to match a the regexp against the key. Only used if the match type is regexp. */ public final Predicate<String> containsPattern; /** * Creates a new KeyCondition * @param label The key name (or regexp) to use. * @param negateResult If we should negate the result., * @param matchType The match type. */ public KeyCondition(String label, boolean negateResult, KeyMatchType matchType) { this.label = label; this.negateResult = negateResult; this.matchType = matchType == null ? KeyMatchType.EQ : matchType; this.containsPattern = KeyMatchType.REGEX.equals(matchType) ? Pattern.compile(label).asPredicate() : null; } @Override public boolean applies(Environment e) { switch(e.getContext()) { case PRIMITIVE: switch (matchType) { case TRUE: return e.osm.isKeyTrue(label) ^ negateResult; case FALSE: return e.osm.isKeyFalse(label) ^ negateResult; case REGEX: return e.osm.keySet().stream().anyMatch(containsPattern) ^ negateResult; default: return e.osm.hasKey(label) ^ negateResult; } case LINK: Utils.ensure(false, "Illegal state: KeyCondition not supported in LINK context"); return false; default: throw new AssertionError(); } } /** * Get the matched key and the corresponding value. * <p> * WARNING: This ignores {@link #negateResult}. * <p> * WARNING: For regexp, the regular expression is returned instead of a key if the match failed. * @param p The primitive to get the value from. * @return The tag. */ @Override public Tag asTag(OsmPrimitive p) { String key = label; if (KeyMatchType.REGEX.equals(matchType)) { key = p.keySet().stream().filter(containsPattern).findAny().orElse(key); } return new Tag(key, p.get(key)); } @Override public String toString() { return '[' + (negateResult ? "!" : "") + label + ']'; } } /** * Class condition. */ public static class ClassCondition implements Condition { /** Class identifier */ public final String id; final boolean not; /** * Constructs a new {@code ClassCondition}. * @param id id * @param not negation or not */ public ClassCondition(String id, boolean not) { this.id = id; this.not = not; } @Override public boolean applies(Environment env) { Cascade cascade = env.getCascade(env.layer); return cascade != null && (not ^ cascade.containsKey(id)); } @Override public String toString() { return (not ? "!" : "") + '.' + id; } } /** * Like <a href="http://www.w3.org/TR/css3-selectors/#pseudo-classes">CSS pseudo classes</a>, MapCSS pseudo classes * are written in lower case with dashes between words. */ public static final class PseudoClasses { private PseudoClasses() { // Hide default constructor for utilities classes } /** * {@code closed} tests whether the way is closed or the relation is a closed multipolygon * @param e MapCSS environment * @return {@code true} if the way is closed or the relation is a closed multipolygon */ static boolean closed(Environment e) { // NO_UCD (unused code) if (e.osm instanceof Way && ((Way) e.osm).isClosed()) return true; if (e.osm instanceof Relation && ((Relation) e.osm).isMultipolygon()) return true; return false; } /** * {@code :modified} tests whether the object has been modified. * @param e MapCSS environment * @return {@code true} if the object has been modified * @see OsmPrimitive#isModified() */ static boolean modified(Environment e) { // NO_UCD (unused code) return e.osm.isModified() || e.osm.isNewOrUndeleted(); } /** * {@code ;new} tests whether the object is new. * @param e MapCSS environment * @return {@code true} if the object is new * @see OsmPrimitive#isNew() */ static boolean _new(Environment e) { // NO_UCD (unused code) return e.osm.isNew(); } /** * {@code :connection} tests whether the object is a connection node. * @param e MapCSS environment * @return {@code true} if the object is a connection node * @see Node#isConnectionNode() */ static boolean connection(Environment e) { // NO_UCD (unused code) return e.osm instanceof Node && e.osm.getDataSet() != null && ((Node) e.osm).isConnectionNode(); } /** * {@code :tagged} tests whether the object is tagged. * @param e MapCSS environment * @return {@code true} if the object is tagged * @see OsmPrimitive#isTagged() */ static boolean tagged(Environment e) { // NO_UCD (unused code) return e.osm.isTagged(); } /** * {@code :same-tags} tests whether the object has the same tags as its child/parent. * @param e MapCSS environment * @return {@code true} if the object has the same tags as its child/parent * @see OsmPrimitive#hasSameInterestingTags(OsmPrimitive) */ static boolean sameTags(Environment e) { // NO_UCD (unused code) return e.osm.hasSameInterestingTags(Utils.firstNonNull(e.child, e.parent)); } /** * {@code :area-style} tests whether the object has an area style. This is useful for validators. * @param e MapCSS environment * @return {@code true} if the object has an area style * @see ElemStyles#hasAreaElemStyle(OsmPrimitive, boolean) */ static boolean areaStyle(Environment e) { // NO_UCD (unused code) // only for validator return ElemStyles.hasAreaElemStyle(e.osm, false); } /** * {@code unconnected}: tests whether the object is a unconnected node. * @param e MapCSS environment * @return {@code true} if the object is a unconnected node */ static boolean unconnected(Environment e) { // NO_UCD (unused code) return e.osm instanceof Node && OsmPrimitive.getFilteredList(e.osm.getReferrers(), Way.class).isEmpty(); } /** * {@code righthandtraffic} checks if there is right-hand traffic at the current location. * @param e MapCSS environment * @return {@code true} if there is right-hand traffic at the current location * @see ExpressionFactory.Functions#is_right_hand_traffic(Environment) */ static boolean righthandtraffic(Environment e) { // NO_UCD (unused code) return ExpressionFactory.Functions.is_right_hand_traffic(e); } /** * {@code clockwise} whether the way is closed and oriented clockwise, * or non-closed and the 1st, 2nd and last node are in clockwise order. * @param e MapCSS environment * @return {@code true} if the way clockwise * @see ExpressionFactory.Functions#is_clockwise(Environment) */ static boolean clockwise(Environment e) { // NO_UCD (unused code) return ExpressionFactory.Functions.is_clockwise(e); } /** * {@code anticlockwise} whether the way is closed and oriented anticlockwise, * or non-closed and the 1st, 2nd and last node are in anticlockwise order. * @param e MapCSS environment * @return {@code true} if the way clockwise * @see ExpressionFactory.Functions#is_anticlockwise(Environment) */ static boolean anticlockwise(Environment e) { // NO_UCD (unused code) return ExpressionFactory.Functions.is_anticlockwise(e); } /** * {@code unclosed-multipolygon} tests whether the object is an unclosed multipolygon. * @param e MapCSS environment * @return {@code true} if the object is an unclosed multipolygon */ static boolean unclosed_multipolygon(Environment e) { // NO_UCD (unused code) return e.osm instanceof Relation && ((Relation) e.osm).isMultipolygon() && !e.osm.isIncomplete() && !((Relation) e.osm).hasIncompleteMembers() && !MultipolygonCache.getInstance().get((Relation) e.osm).getOpenEnds().isEmpty(); } private static final Predicate<OsmPrimitive> IN_DOWNLOADED_AREA = new InDataSourceArea(false); /** * {@code in-downloaded-area} tests whether the object is within source area ("downloaded area"). * @param e MapCSS environment * @return {@code true} if the object is within source area ("downloaded area") * @see InDataSourceArea */ static boolean inDownloadedArea(Environment e) { // NO_UCD (unused code) return IN_DOWNLOADED_AREA.test(e.osm); } static boolean completely_downloaded(Environment e) { // NO_UCD (unused code) if (e.osm instanceof Relation) { return !((Relation) e.osm).hasIncompleteMembers(); } else { return true; } } static boolean closed2(Environment e) { // NO_UCD (unused code) if (e.osm instanceof Way && ((Way) e.osm).isClosed()) return true; if (e.osm instanceof Relation && ((Relation) e.osm).isMultipolygon()) return MultipolygonCache.getInstance().get((Relation) e.osm).getOpenEnds().isEmpty(); return false; } static boolean selected(Environment e) { // NO_UCD (unused code) Cascade c = e.mc.getCascade(e.layer); c.setDefaultSelectedHandling(false); return e.osm.isSelected(); } } /** * Pseudo class condition. */ public static class PseudoClassCondition implements Condition { final Method method; final boolean not; protected PseudoClassCondition(Method method, boolean not) { this.method = method; this.not = not; } /** * Create a new pseudo class condition * @param id The id of the pseudo class * @param not <code>true</code> to invert the condition * @param context The context the class is found in. * @return The new condition */ public static PseudoClassCondition createPseudoClassCondition(String id, boolean not, Context context) { CheckParameterUtil.ensureThat(!"sameTags".equals(id) || Context.LINK.equals(context), "sameTags only supported in LINK context"); if ("open_end".equals(id)) { return new OpenEndPseudoClassCondition(not); } final Method method = getMethod(id); if (method != null) { return new PseudoClassCondition(method, not); } throw new MapCSSException("Invalid pseudo class specified: " + id); } protected static Method getMethod(String id) { String cleanId = id.replaceAll("-|_", ""); for (Method method : PseudoClasses.class.getDeclaredMethods()) { // for backwards compatibility, consider :sameTags == :same-tags == :same_tags (#11150) final String methodName = method.getName().replaceAll("-|_", ""); if (methodName.equalsIgnoreCase(cleanId)) { return method; } } return null; } @Override public boolean applies(Environment e) { try { return not ^ (Boolean) method.invoke(null, e); } catch (IllegalAccessException | InvocationTargetException ex) { throw new JosmRuntimeException(ex); } } @Override public String toString() { return (not ? "!" : "") + ':' + method.getName(); } } /** * Open end pseudo class condition. */ public static class OpenEndPseudoClassCondition extends PseudoClassCondition { /** * Constructs a new {@code OpenEndPseudoClassCondition}. * @param not negation or not */ public OpenEndPseudoClassCondition(boolean not) { super(null, not); } @Override public boolean applies(Environment e) { return true; } } /** * A condition that is fulfilled whenever the expression is evaluated to be true. */ public static class ExpressionCondition implements Condition { final Expression e; /** * Constructs a new {@code ExpressionFactory} * @param e expression */ public ExpressionCondition(Expression e) { this.e = e; } @Override public boolean applies(Environment env) { Boolean b = Cascade.convertTo(e.evaluate(env), Boolean.class); return b != null && b; } @Override public String toString() { return '[' + e.toString() + ']'; } } }