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