001 // License: GPL. For details, see LICENSE file. 002 package org.openstreetmap.josm.gui.mappaint.mapcss; 003 004 import static org.openstreetmap.josm.tools.Utils.equal; 005 006 import java.text.MessageFormat; 007 import java.util.EnumSet; 008 import java.util.regex.Matcher; 009 import java.util.regex.Pattern; 010 011 import org.openstreetmap.josm.data.osm.Node; 012 import org.openstreetmap.josm.data.osm.OsmUtils; 013 import org.openstreetmap.josm.data.osm.Relation; 014 import org.openstreetmap.josm.data.osm.Way; 015 import org.openstreetmap.josm.gui.mappaint.Cascade; 016 import org.openstreetmap.josm.gui.mappaint.Environment; 017 import org.openstreetmap.josm.tools.Utils; 018 019 abstract public class Condition { 020 021 abstract public boolean applies(Environment e); 022 023 public static Condition create(String k, String v, Op op, Context context) { 024 switch (context) { 025 case PRIMITIVE: 026 return new KeyValueCondition(k, v, op); 027 case LINK: 028 if ("role".equalsIgnoreCase(k)) 029 return new RoleCondition(v, op); 030 else if ("index".equalsIgnoreCase(k)) 031 return new IndexCondition(v, op); 032 else 033 throw new MapCSSException( 034 MessageFormat.format("Expected key ''role'' or ''index'' in link context. Got ''{0}''.", k)); 035 036 default: throw new AssertionError(); 037 } 038 } 039 040 public static Condition create(String k, boolean not, boolean yes, Context context) { 041 switch (context) { 042 case PRIMITIVE: 043 return new KeyCondition(k, not, yes); 044 case LINK: 045 if (yes) 046 throw new MapCSSException("Question mark operator ''?'' not supported in LINK context"); 047 if (not) 048 return new RoleCondition(k, Op.NEQ); 049 else 050 return new RoleCondition(k, Op.EQ); 051 052 default: throw new AssertionError(); 053 } 054 } 055 056 public static Condition create(String id, boolean not, Context context) { 057 return new PseudoClassCondition(id, not); 058 } 059 060 public static Condition create(Expression e, Context context) { 061 return new ExpressionCondition(e); 062 } 063 064 public static enum Op { 065 EQ, NEQ, GREATER_OR_EQUAL, GREATER, LESS_OR_EQUAL, LESS, 066 REGEX, ONE_OF, BEGINS_WITH, ENDS_WITH, CONTAINS; 067 068 public boolean eval(String testString, String prototypeString) { 069 if (testString == null && this != NEQ) 070 return false; 071 switch (this) { 072 case EQ: 073 return equal(testString, prototypeString); 074 case NEQ: 075 return !equal(testString, prototypeString); 076 case REGEX: 077 Pattern p = Pattern.compile(prototypeString); 078 Matcher m = p.matcher(testString); 079 return m.find(); 080 case ONE_OF: 081 String[] parts = testString.split(";"); 082 for (String part : parts) { 083 if (equal(prototypeString, part.trim())) 084 return true; 085 } 086 return false; 087 case BEGINS_WITH: 088 return testString.startsWith(prototypeString); 089 case ENDS_WITH: 090 return testString.endsWith(prototypeString); 091 case CONTAINS: 092 return testString.contains(prototypeString); 093 } 094 095 float test_float; 096 try { 097 test_float = Float.parseFloat(testString); 098 } catch (NumberFormatException e) { 099 return false; 100 } 101 float prototype_float = Float.parseFloat(prototypeString); 102 103 switch (this) { 104 case GREATER_OR_EQUAL: 105 return test_float >= prototype_float; 106 case GREATER: 107 return test_float > prototype_float; 108 case LESS_OR_EQUAL: 109 return test_float <= prototype_float; 110 case LESS: 111 return test_float < prototype_float; 112 default: 113 throw new AssertionError(); 114 } 115 } 116 } 117 118 /** 119 * context, where the condition applies 120 */ 121 public static enum Context { 122 /** 123 * normal primitive selector, e.g. way[highway=residential] 124 */ 125 PRIMITIVE, 126 127 /** 128 * link between primitives, e.g. relation >[role=outer] way 129 */ 130 LINK 131 } 132 133 public final static EnumSet<Op> COMPARISON_OPERATERS = 134 EnumSet.of(Op.GREATER_OR_EQUAL, Op.GREATER, Op.LESS_OR_EQUAL, Op.LESS); 135 136 /** 137 * <p>Represents a key/value condition which is either applied to a primitive.</p> 138 * 139 */ 140 public static class KeyValueCondition extends Condition { 141 142 public String k; 143 public String v; 144 public Op op; 145 146 /** 147 * <p>Creates a key/value-condition.</p> 148 * 149 * @param k the key 150 * @param v the value 151 * @param op the operation 152 */ 153 public KeyValueCondition(String k, String v, Op op) { 154 this.k = k; 155 this.v = v; 156 this.op = op; 157 } 158 159 @Override 160 public boolean applies(Environment env) { 161 return op.eval(env.osm.get(k), v); 162 } 163 164 @Override 165 public String toString() { 166 return "[" + k + "'" + op + "'" + v + "]"; 167 } 168 } 169 170 public static class RoleCondition extends Condition { 171 public String role; 172 public Op op; 173 174 public RoleCondition(String role, Op op) { 175 this.role = role; 176 this.op = op; 177 } 178 179 @Override 180 public boolean applies(Environment env) { 181 String testRole = env.getRole(); 182 if (testRole == null) return false; 183 return op.eval(testRole, role); 184 } 185 } 186 187 public static class IndexCondition extends Condition { 188 public String index; 189 public Op op; 190 191 public IndexCondition(String index, Op op) { 192 this.index = index; 193 this.op = op; 194 } 195 196 @Override 197 public boolean applies(Environment env) { 198 if (env.index == null) return false; 199 return op.eval(Integer.toString(env.index + 1), index); 200 } 201 } 202 203 /** 204 * <p>KeyCondition represent one of the following conditions in either the link or the 205 * primitive context:</p> 206 * <pre> 207 * ["a label"] PRIMITIVE: the primitive has a tag "a label" 208 * LINK: the parent is a relation and it has at least one member with the role 209 * "a label" referring to the child 210 * 211 * [!"a label"] PRIMITIVE: the primitive doesn't have a tag "a label" 212 * LINK: the parent is a relation but doesn't have a member with the role 213 * "a label" referring to the child 214 * 215 * ["a label"?] PRIMITIVE: the primitive has a tag "a label" whose value evaluates to a true-value 216 * LINK: not supported 217 * </pre> 218 */ 219 public static class KeyCondition extends Condition { 220 221 private String label; 222 private boolean exclamationMarkPresent; 223 private boolean questionMarkPresent; 224 225 /** 226 * 227 * @param label 228 * @param exclamationMarkPresent 229 * @param questionMarkPresent 230 */ 231 public KeyCondition(String label, boolean exclamationMarkPresent, boolean questionMarkPresent){ 232 this.label = label; 233 this.exclamationMarkPresent = exclamationMarkPresent; 234 this.questionMarkPresent = questionMarkPresent; 235 } 236 237 @Override 238 public boolean applies(Environment e) { 239 switch(e.getContext()) { 240 case PRIMITIVE: 241 if (questionMarkPresent) 242 return OsmUtils.isTrue(e.osm.get(label)) ^ exclamationMarkPresent; 243 else 244 return e.osm.hasKey(label) ^ exclamationMarkPresent; 245 case LINK: 246 Utils.ensure(false, "Illegal state: KeyCondition not supported in LINK context"); 247 return false; 248 default: throw new AssertionError(); 249 } 250 } 251 252 @Override 253 public String toString() { 254 return "[" + (exclamationMarkPresent ? "!" : "") + label + "]"; 255 } 256 } 257 258 public static class PseudoClassCondition extends Condition { 259 260 String id; 261 boolean not; 262 263 public PseudoClassCondition(String id, boolean not) { 264 this.id = id; 265 this.not = not; 266 } 267 268 @Override 269 public boolean applies(Environment e) { 270 return not ^ appliesImpl(e); 271 } 272 273 public boolean appliesImpl(Environment e) { 274 if (equal(id, "closed")) { 275 if (e.osm instanceof Way && ((Way) e.osm).isClosed()) 276 return true; 277 if (e.osm instanceof Relation && ((Relation) e.osm).isMultipolygon()) 278 return true; 279 return false; 280 } else if (equal(id, "modified")) 281 return e.osm.isModified() || e.osm.isNewOrUndeleted(); 282 else if (equal(id, "new")) 283 return e.osm.isNew(); 284 else if (equal(id, "connection") && (e.osm instanceof Node)) 285 return ((Node) e.osm).isConnectionNode(); 286 else if (equal(id, "tagged")) 287 return e.osm.isTagged(); 288 return true; 289 } 290 291 @Override 292 public String toString() { 293 return ":" + (not ? "!" : "") + id; 294 } 295 } 296 297 public static class ExpressionCondition extends Condition { 298 299 private Expression e; 300 301 public ExpressionCondition(Expression e) { 302 this.e = e; 303 } 304 305 @Override 306 public boolean applies(Environment env) { 307 Boolean b = Cascade.convertTo(e.evaluate(env), Boolean.class); 308 return b != null && b; 309 } 310 311 @Override 312 public String toString() { 313 return "[" + e + "]"; 314 } 315 } 316 }