001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.mappaint.mapcss; 003 004import java.text.MessageFormat; 005import java.util.Arrays; 006import java.util.EnumSet; 007import java.util.Objects; 008import java.util.Set; 009import java.util.regex.Pattern; 010 011import org.openstreetmap.josm.data.osm.Node; 012import org.openstreetmap.josm.data.osm.OsmPrimitive; 013import org.openstreetmap.josm.data.osm.Relation; 014import org.openstreetmap.josm.data.osm.Tag; 015import org.openstreetmap.josm.data.osm.Way; 016import org.openstreetmap.josm.gui.mappaint.Cascade; 017import org.openstreetmap.josm.gui.mappaint.ElemStyles; 018import org.openstreetmap.josm.gui.mappaint.Environment; 019import org.openstreetmap.josm.tools.CheckParameterUtil; 020import org.openstreetmap.josm.tools.Predicate; 021import org.openstreetmap.josm.tools.Predicates; 022import org.openstreetmap.josm.tools.Utils; 023 024public abstract class Condition { 025 026 public abstract boolean applies(Environment e); 027 028 public static Condition createKeyValueCondition(String k, String v, Op op, Context context, boolean considerValAsKey) { 029 switch (context) { 030 case PRIMITIVE: 031 if (KeyValueRegexpCondition.SUPPORTED_OPS.contains(op) && !considerValAsKey) 032 return new KeyValueRegexpCondition(k, v, op, false); 033 if (!considerValAsKey && op.equals(Op.EQ)) 034 return new SimpleKeyValueCondition(k, v); 035 return new KeyValueCondition(k, v, op, considerValAsKey); 036 case LINK: 037 if (considerValAsKey) 038 throw new MapCSSException("''considerValAsKey'' not supported in LINK context"); 039 if ("role".equalsIgnoreCase(k)) 040 return new RoleCondition(v, op); 041 else if ("index".equalsIgnoreCase(k)) 042 return new IndexCondition(v, op); 043 else 044 throw new MapCSSException( 045 MessageFormat.format("Expected key ''role'' or ''index'' in link context. Got ''{0}''.", k)); 046 047 default: throw new AssertionError(); 048 } 049 } 050 051 public static Condition createKeyCondition(String k, boolean not, KeyMatchType matchType, Context context) { 052 switch (context) { 053 case PRIMITIVE: 054 return new KeyCondition(k, not, matchType); 055 case LINK: 056 if (matchType != null) 057 throw new MapCSSException("Question mark operator ''?'' and regexp match not supported in LINK context"); 058 if (not) 059 return new RoleCondition(k, Op.NEQ); 060 else 061 return new RoleCondition(k, Op.EQ); 062 063 default: throw new AssertionError(); 064 } 065 } 066 067 public static PseudoClassCondition createPseudoClassCondition(String id, boolean not, Context context) { 068 return new PseudoClassCondition(id, not, context); 069 } 070 071 public static ClassCondition createClassCondition(String id, boolean not, Context context) { 072 return new ClassCondition(id, not); 073 } 074 075 public static ExpressionCondition createExpressionCondition(Expression e, Context context) { 076 return new ExpressionCondition(e); 077 } 078 079 public static enum Op { 080 EQ, NEQ, GREATER_OR_EQUAL, GREATER, LESS_OR_EQUAL, LESS, 081 REGEX, NREGEX, ONE_OF, BEGINS_WITH, ENDS_WITH, CONTAINS; 082 083 private static final Set<Op> NEGATED_OPS = EnumSet.of(NEQ, NREGEX); 084 085 public boolean eval(String testString, String prototypeString) { 086 if (testString == null && !NEGATED_OPS.contains(this)) 087 return false; 088 switch (this) { 089 case EQ: 090 return Objects.equals(testString, prototypeString); 091 case NEQ: 092 return !Objects.equals(testString, prototypeString); 093 case REGEX: 094 case NREGEX: 095 final boolean contains = Pattern.compile(prototypeString).matcher(testString).find(); 096 return REGEX.equals(this) ? contains : !contains; 097 case ONE_OF: 098 return Arrays.asList(testString.split("\\s*;\\s*")).contains(prototypeString); 099 case BEGINS_WITH: 100 return testString.startsWith(prototypeString); 101 case ENDS_WITH: 102 return testString.endsWith(prototypeString); 103 case CONTAINS: 104 return testString.contains(prototypeString); 105 } 106 107 float test_float; 108 try { 109 test_float = Float.parseFloat(testString); 110 } catch (NumberFormatException e) { 111 return false; 112 } 113 float prototype_float = Float.parseFloat(prototypeString); 114 115 switch (this) { 116 case GREATER_OR_EQUAL: 117 return test_float >= prototype_float; 118 case GREATER: 119 return test_float > prototype_float; 120 case LESS_OR_EQUAL: 121 return test_float <= prototype_float; 122 case LESS: 123 return test_float < prototype_float; 124 default: 125 throw new AssertionError(); 126 } 127 } 128 } 129 130 /** 131 * Context, where the condition applies. 132 */ 133 public static enum Context { 134 /** 135 * normal primitive selector, e.g. way[highway=residential] 136 */ 137 PRIMITIVE, 138 139 /** 140 * link between primitives, e.g. relation >[role=outer] way 141 */ 142 LINK 143 } 144 145 public static final EnumSet<Op> COMPARISON_OPERATERS = 146 EnumSet.of(Op.GREATER_OR_EQUAL, Op.GREATER, Op.LESS_OR_EQUAL, Op.LESS); 147 148 /** 149 * Most common case of a KeyValueCondition. 150 * 151 * Extra class for performance reasons. 152 */ 153 public static class SimpleKeyValueCondition extends Condition { 154 public final String k; 155 public final String v; 156 157 public SimpleKeyValueCondition(String k, String v) { 158 this.k = k; 159 this.v = v; 160 } 161 162 @Override 163 public boolean applies(Environment e) { 164 return v.equals(e.osm.get(k)); 165 } 166 167 public Tag asTag() { 168 return new Tag(k, v); 169 } 170 171 @Override 172 public String toString() { 173 return '[' + k + '=' + v + ']'; 174 } 175 176 } 177 178 /** 179 * <p>Represents a key/value condition which is either applied to a primitive.</p> 180 * 181 */ 182 public static class KeyValueCondition extends Condition { 183 184 public final String k; 185 public final String v; 186 public final Op op; 187 public boolean considerValAsKey; 188 189 /** 190 * <p>Creates a key/value-condition.</p> 191 * 192 * @param k the key 193 * @param v the value 194 * @param op the operation 195 * @param considerValAsKey whether to consider {@code v} as another key and compare the values of key {@code k} and key {@code v}. 196 */ 197 public KeyValueCondition(String k, String v, Op op, boolean considerValAsKey) { 198 this.k = k; 199 this.v = v; 200 this.op = op; 201 this.considerValAsKey = considerValAsKey; 202 } 203 204 @Override 205 public boolean applies(Environment env) { 206 return op.eval(env.osm.get(k), considerValAsKey ? env.osm.get(v) : v); 207 } 208 209 public Tag asTag() { 210 return new Tag(k, v); 211 } 212 213 @Override 214 public String toString() { 215 return "[" + k + "'" + op + "'" + v + "]"; 216 } 217 } 218 219 public static class KeyValueRegexpCondition extends KeyValueCondition { 220 221 public final Pattern pattern; 222 public static final EnumSet<Op> SUPPORTED_OPS = EnumSet.of(Op.REGEX, Op.NREGEX); 223 224 public KeyValueRegexpCondition(String k, String v, Op op, boolean considerValAsKey) { 225 super(k, v, op, considerValAsKey); 226 CheckParameterUtil.ensureThat(!considerValAsKey, "considerValAsKey is not supported"); 227 CheckParameterUtil.ensureThat(SUPPORTED_OPS.contains(op), "Op must be REGEX or NREGEX"); 228 this.pattern = Pattern.compile(v); 229 } 230 231 @Override 232 public boolean applies(Environment env) { 233 final String value = env.osm.get(k); 234 if (Op.REGEX.equals(op)) { 235 return value != null && pattern.matcher(value).find(); 236 } else if (Op.NREGEX.equals(op)) { 237 return value == null || !pattern.matcher(value).find(); 238 } else { 239 throw new IllegalStateException(); 240 } 241 } 242 } 243 244 public static class RoleCondition extends Condition { 245 public final String role; 246 public final Op op; 247 248 public RoleCondition(String role, Op op) { 249 this.role = role; 250 this.op = op; 251 } 252 253 @Override 254 public boolean applies(Environment env) { 255 String testRole = env.getRole(); 256 if (testRole == null) return false; 257 return op.eval(testRole, role); 258 } 259 } 260 261 public static class IndexCondition extends Condition { 262 public final String index; 263 public final Op op; 264 265 public IndexCondition(String index, Op op) { 266 this.index = index; 267 this.op = op; 268 } 269 270 @Override 271 public boolean applies(Environment env) { 272 if (env.index == null) return false; 273 return op.eval(Integer.toString(env.index + 1), index); 274 } 275 } 276 277 public static enum KeyMatchType { 278 EQ, TRUE, FALSE, REGEX 279 } 280 281 /** 282 * <p>KeyCondition represent one of the following conditions in either the link or the 283 * primitive context:</p> 284 * <pre> 285 * ["a label"] PRIMITIVE: the primitive has a tag "a label" 286 * LINK: the parent is a relation and it has at least one member with the role 287 * "a label" referring to the child 288 * 289 * [!"a label"] PRIMITIVE: the primitive doesn't have a tag "a label" 290 * LINK: the parent is a relation but doesn't have a member with the role 291 * "a label" referring to the child 292 * 293 * ["a label"?] PRIMITIVE: the primitive has a tag "a label" whose value evaluates to a true-value 294 * LINK: not supported 295 * 296 * ["a label"?!] PRIMITIVE: the primitive has a tag "a label" whose value evaluates to a false-value 297 * LINK: not supported 298 * </pre> 299 */ 300 public static class KeyCondition extends Condition { 301 302 public final String label; 303 public final boolean negateResult; 304 public final KeyMatchType matchType; 305 public Predicate<String> containsPattern; 306 307 public KeyCondition(String label, boolean negateResult, KeyMatchType matchType){ 308 this.label = label; 309 this.negateResult = negateResult; 310 this.matchType = matchType; 311 this.containsPattern = KeyMatchType.REGEX.equals(matchType) 312 ? Predicates.stringContainsPattern(Pattern.compile(label)) 313 : null; 314 } 315 316 @Override 317 public boolean applies(Environment e) { 318 switch(e.getContext()) { 319 case PRIMITIVE: 320 if (KeyMatchType.TRUE.equals(matchType)) 321 return e.osm.isKeyTrue(label) ^ negateResult; 322 else if (KeyMatchType.FALSE.equals(matchType)) 323 return e.osm.isKeyFalse(label) ^ negateResult; 324 else if (KeyMatchType.REGEX.equals(matchType)) { 325 return Utils.exists(e.osm.keySet(), containsPattern) ^ negateResult; 326 } else { 327 return e.osm.hasKey(label) ^ negateResult; 328 } 329 case LINK: 330 Utils.ensure(false, "Illegal state: KeyCondition not supported in LINK context"); 331 return false; 332 default: throw new AssertionError(); 333 } 334 } 335 336 public Tag asTag() { 337 return new Tag(label); 338 } 339 340 @Override 341 public String toString() { 342 return "[" + (negateResult ? "!" : "") + label + "]"; 343 } 344 } 345 346 public static class ClassCondition extends Condition { 347 348 public final String id; 349 public final boolean not; 350 351 public ClassCondition(String id, boolean not) { 352 this.id = id; 353 this.not = not; 354 } 355 356 @Override 357 public boolean applies(Environment env) { 358 return env != null && env.getCascade(env.layer) != null && not ^ env.getCascade(env.layer).containsKey(id); 359 } 360 361 @Override 362 public String toString() { 363 return (not ? "!" : "") + "." + id; 364 } 365 } 366 367 public static class PseudoClassCondition extends Condition { 368 369 public final String id; 370 public final boolean not; 371 372 public PseudoClassCondition(String id, boolean not, Context context) { 373 this.id = id; 374 this.not = not; 375 CheckParameterUtil.ensureThat(!"sameTags".equals(id) || Context.LINK.equals(context), "sameTags only supported in LINK context"); 376 } 377 378 @Override 379 public boolean applies(Environment e) { 380 return not ^ appliesImpl(e); 381 } 382 383 public boolean appliesImpl(Environment e) { 384 switch(id) { 385 case "closed": 386 if (e.osm instanceof Way && ((Way) e.osm).isClosed()) 387 return true; 388 if (e.osm instanceof Relation && ((Relation) e.osm).isMultipolygon()) 389 return true; 390 break; 391 case "modified": 392 return e.osm.isModified() || e.osm.isNewOrUndeleted(); 393 case "new": 394 return e.osm.isNew(); 395 case "connection": 396 return e.osm instanceof Node && ((Node) e.osm).isConnectionNode(); 397 case "tagged": 398 return e.osm.isTagged(); 399 case "sameTags": 400 return e.osm.hasSameInterestingTags(Utils.firstNonNull(e.child, e.parent)); 401 case "areaStyle": 402 // only for validator 403 return ElemStyles.hasAreaElemStyle(e.osm, false); 404 case "unconnected": 405 return e.osm instanceof Node && OsmPrimitive.getFilteredList(e.osm.getReferrers(), Way.class).isEmpty(); 406 case "righthandtraffic": 407 return ExpressionFactory.Functions.is_right_hand_traffic(e); 408 } 409 return false; 410 } 411 412 @Override 413 public String toString() { 414 return ":" + (not ? "!" : "") + id; 415 } 416 } 417 418 public static class ExpressionCondition extends Condition { 419 420 private final Expression e; 421 422 public ExpressionCondition(Expression e) { 423 this.e = e; 424 } 425 426 @Override 427 public boolean applies(Environment env) { 428 Boolean b = Cascade.convertTo(e.evaluate(env), Boolean.class); 429 return b != null && b; 430 } 431 432 @Override 433 public String toString() { 434 return "[" + e + "]"; 435 } 436 } 437}