source: josm/trunk/src/org/openstreetmap/josm/gui/mappaint/mapcss/MapCSSStyleSource.java@ 8742

Last change on this file since 8742 was 8742, checked in by simon04, 9 years ago

fix #11834 - KeyValueVisitor: also provide primitive to the visitor

  • Property svn:eol-style set to native
File size: 27.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.mappaint.mapcss;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Color;
7import java.io.ByteArrayInputStream;
8import java.io.File;
9import java.io.IOException;
10import java.io.InputStream;
11import java.lang.reflect.Field;
12import java.nio.charset.StandardCharsets;
13import java.text.MessageFormat;
14import java.util.ArrayList;
15import java.util.BitSet;
16import java.util.Collections;
17import java.util.HashMap;
18import java.util.HashSet;
19import java.util.Iterator;
20import java.util.List;
21import java.util.Locale;
22import java.util.Map;
23import java.util.Map.Entry;
24import java.util.Set;
25import java.util.concurrent.locks.ReadWriteLock;
26import java.util.concurrent.locks.ReentrantReadWriteLock;
27import java.util.zip.ZipEntry;
28import java.util.zip.ZipFile;
29
30import org.openstreetmap.josm.Main;
31import org.openstreetmap.josm.data.Version;
32import org.openstreetmap.josm.data.osm.AbstractPrimitive;
33import org.openstreetmap.josm.data.osm.AbstractPrimitive.KeyValueVisitor;
34import org.openstreetmap.josm.data.osm.Node;
35import org.openstreetmap.josm.data.osm.OsmPrimitive;
36import org.openstreetmap.josm.data.osm.Relation;
37import org.openstreetmap.josm.data.osm.Way;
38import org.openstreetmap.josm.gui.mappaint.Cascade;
39import org.openstreetmap.josm.gui.mappaint.Environment;
40import org.openstreetmap.josm.gui.mappaint.LineElemStyle;
41import org.openstreetmap.josm.gui.mappaint.MultiCascade;
42import org.openstreetmap.josm.gui.mappaint.Range;
43import org.openstreetmap.josm.gui.mappaint.StyleKeys;
44import org.openstreetmap.josm.gui.mappaint.StyleSetting;
45import org.openstreetmap.josm.gui.mappaint.StyleSetting.BooleanStyleSetting;
46import org.openstreetmap.josm.gui.mappaint.StyleSource;
47import org.openstreetmap.josm.gui.mappaint.mapcss.Condition.KeyCondition;
48import org.openstreetmap.josm.gui.mappaint.mapcss.Condition.KeyMatchType;
49import org.openstreetmap.josm.gui.mappaint.mapcss.Condition.KeyValueCondition;
50import org.openstreetmap.josm.gui.mappaint.mapcss.Condition.Op;
51import org.openstreetmap.josm.gui.mappaint.mapcss.Condition.SimpleKeyValueCondition;
52import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.ChildOrParentSelector;
53import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector;
54import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.OptimizedGeneralSelector;
55import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser;
56import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
57import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.TokenMgrError;
58import org.openstreetmap.josm.gui.preferences.SourceEntry;
59import org.openstreetmap.josm.io.CachedFile;
60import org.openstreetmap.josm.tools.CheckParameterUtil;
61import org.openstreetmap.josm.tools.LanguageInfo;
62import org.openstreetmap.josm.tools.Utils;
63
64/**
65 * This is a mappaint style that is based on MapCSS rules.
66 */
67public class MapCSSStyleSource extends StyleSource {
68
69 /**
70 * The accepted MIME types sent in the HTTP Accept header.
71 * @since 6867
72 */
73 public static final String MAPCSS_STYLE_MIME_TYPES =
74 "text/x-mapcss, text/mapcss, text/css; q=0.9, text/plain; q=0.8, application/zip, application/octet-stream; q=0.5";
75
76 // all rules
77 public final List<MapCSSRule> rules = new ArrayList<>();
78 // rule indices, filtered by primitive type
79 public final MapCSSRuleIndex nodeRules = new MapCSSRuleIndex(); // nodes
80 public final MapCSSRuleIndex wayRules = new MapCSSRuleIndex(); // ways without tag area=no
81 public final MapCSSRuleIndex wayNoAreaRules = new MapCSSRuleIndex(); // ways with tag area=no
82 public final MapCSSRuleIndex relationRules = new MapCSSRuleIndex(); // relations that are not multipolygon relations
83 public final MapCSSRuleIndex multipolygonRules = new MapCSSRuleIndex(); // multipolygon relations
84 public final MapCSSRuleIndex canvasRules = new MapCSSRuleIndex(); // rules to apply canvas properties
85
86 private Color backgroundColorOverride;
87 private String css = null;
88 private ZipFile zipFile;
89
90 /**
91 * This lock prevents concurrent execution of {@link MapCSSRuleIndex#clear() } /
92 * {@link MapCSSRuleIndex#initIndex()} and {@link MapCSSRuleIndex#getRuleCandidates }.
93 *
94 * For efficiency reasons, these methods are synchronized higher up the
95 * stack trace.
96 */
97 public static final ReadWriteLock STYLE_SOURCE_LOCK = new ReentrantReadWriteLock();
98
99 /**
100 * Set of all supported MapCSS keys.
101 */
102 public static final Set<String> SUPPORTED_KEYS = new HashSet<>();
103 static {
104 Field[] declaredFields = StyleKeys.class.getDeclaredFields();
105 for (Field f : declaredFields) {
106 try {
107 SUPPORTED_KEYS.add((String) f.get(null));
108 if (!f.getName().toLowerCase(Locale.ENGLISH).replace("_", "-").equals(f.get(null))) {
109 throw new RuntimeException(f.getName());
110 }
111 } catch (IllegalArgumentException | IllegalAccessException ex) {
112 throw new RuntimeException(ex);
113 }
114 }
115 for (LineElemStyle.LineType lt : LineElemStyle.LineType.values()) {
116 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.COLOR);
117 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES);
118 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES_BACKGROUND_COLOR);
119 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES_BACKGROUND_OPACITY);
120 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES_OFFSET);
121 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.LINECAP);
122 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.LINEJOIN);
123 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.MITERLIMIT);
124 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.OFFSET);
125 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.OPACITY);
126 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.REAL_WIDTH);
127 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.WIDTH);
128 }
129 }
130
131 /**
132 * A collection of {@link MapCSSRule}s, that are indexed by tag key and value.
133 *
134 * Speeds up the process of finding all rules that match a certain primitive.
135 *
136 * Rules with a {@link SimpleKeyValueCondition} [key=value] or rules that require a specific key to be set are
137 * indexed. Now you only need to loop the tags of a primitive to retrieve the possibly matching rules.
138 *
139 * To use this index, you need to {@link #add(MapCSSRule)} all rules to it. You then need to call
140 * {@link #initIndex()}. Afterwards, you can use {@link #getRuleCandidates(OsmPrimitive)} to get an iterator over
141 * all rules that might be applied to that primitive.
142 */
143 public static class MapCSSRuleIndex {
144 /**
145 * This is an iterator over all rules that are marked as possible in the bitset.
146 *
147 * @author Michael Zangl
148 */
149 private final class RuleCandidatesIterator implements Iterator<MapCSSRule>, KeyValueVisitor {
150 private final BitSet ruleCandidates;
151 private int next;
152
153 private RuleCandidatesIterator(BitSet ruleCandidates) {
154 this.ruleCandidates = ruleCandidates;
155 }
156
157 @Override
158 public boolean hasNext() {
159 return next >= 0;
160 }
161
162 @Override
163 public MapCSSRule next() {
164 MapCSSRule rule = rules.get(next);
165 next = ruleCandidates.nextSetBit(next + 1);
166 return rule;
167 }
168
169 @Override
170 public void remove() {
171 throw new UnsupportedOperationException();
172 }
173
174 @Override
175 public void visitKeyValue(AbstractPrimitive p, String key, String value) {
176 MapCSSKeyRules v = index.get(key);
177 if (v != null) {
178 BitSet rs = v.get(value);
179 ruleCandidates.or(rs);
180 }
181 }
182
183 /**
184 * Call this before using the iterator.
185 */
186 public void prepare() {
187 next = ruleCandidates.nextSetBit(0);
188 }
189 }
190
191 /**
192 * This is a map of all rules that are only applied if the primitive has a given key (and possibly value)
193 *
194 * @author Michael Zangl
195 */
196 private static final class MapCSSKeyRules {
197 /**
198 * The indexes of rules that might be applied if this tag is present and the value has no special handling.
199 */
200 BitSet generalRules = new BitSet();
201
202 /**
203 * A map that sores the indexes of rules that might be applied if the key=value pair is present on this
204 * primitive. This includes all key=* rules.
205 */
206 Map<String, BitSet> specialRules = new HashMap<>();
207
208 public void addForKey(int ruleIndex) {
209 generalRules.set(ruleIndex);
210 for (BitSet r : specialRules.values()) {
211 r.set(ruleIndex);
212 }
213 }
214
215 public void addForKeyAndValue(String value, int ruleIndex) {
216 BitSet forValue = specialRules.get(value);
217 if (forValue == null) {
218 forValue = new BitSet();
219 forValue.or(generalRules);
220 specialRules.put(value.intern(), forValue);
221 }
222 forValue.set(ruleIndex);
223 }
224
225 public BitSet get(String value) {
226 BitSet forValue = specialRules.get(value);
227 if (forValue != null) return forValue; else return generalRules;
228 }
229 }
230
231 /**
232 * All rules this index is for. Once this index is built, this list is sorted.
233 */
234 private final List<MapCSSRule> rules = new ArrayList<>();
235 /**
236 * All rules that only apply when the given key is present.
237 */
238 private final Map<String, MapCSSKeyRules> index = new HashMap<>();
239 /**
240 * Rules that do not require any key to be present. Only the index in the {@link #rules} array is stored.
241 */
242 private final BitSet remaining = new BitSet();
243
244 /**
245 * Add a rule to this index. This needs to be called before {@link #initIndex()} is called.
246 * @param rule The rule to add.
247 */
248 public void add(MapCSSRule rule) {
249 rules.add(rule);
250 }
251
252 /**
253 * Initialize the index.
254 * <p>
255 * You must own the write lock of STYLE_SOURCE_LOCK when calling this method.
256 */
257 public void initIndex() {
258 Collections.sort(rules);
259 for (int ruleIndex = 0; ruleIndex < rules.size(); ruleIndex++) {
260 MapCSSRule r = rules.get(ruleIndex);
261 // find the rightmost selector, this must be a GeneralSelector
262 Selector selRightmost = r.selector;
263 while (selRightmost instanceof ChildOrParentSelector) {
264 selRightmost = ((ChildOrParentSelector) selRightmost).right;
265 }
266 OptimizedGeneralSelector s = (OptimizedGeneralSelector) selRightmost;
267 if (s.conds == null) {
268 remaining.set(ruleIndex);
269 continue;
270 }
271 List<SimpleKeyValueCondition> sk = new ArrayList<>(Utils.filteredCollection(s.conds,
272 SimpleKeyValueCondition.class));
273 if (!sk.isEmpty()) {
274 SimpleKeyValueCondition c = sk.get(sk.size() - 1);
275 getEntryInIndex(c.k).addForKeyAndValue(c.v, ruleIndex);
276 } else {
277 String key = findAnyRequiredKey(s.conds);
278 if (key != null) {
279 getEntryInIndex(key).addForKey(ruleIndex);
280 } else {
281 remaining.set(ruleIndex);
282 }
283 }
284 }
285 }
286
287 /**
288 * Search for any key that condition might depend on.
289 *
290 * @param conds The conditions to search through.
291 * @return An arbitrary key this rule depends on or <code>null</code> if there is no such key.
292 */
293 private String findAnyRequiredKey(List<Condition> conds) {
294 String key = null;
295 for (Condition c : conds) {
296 if (c instanceof KeyCondition) {
297 KeyCondition keyCondition = (KeyCondition) c;
298 if (!keyCondition.negateResult && conditionRequiresKeyPresence(keyCondition.matchType)) {
299 key = keyCondition.label;
300 }
301 } else if (c instanceof KeyValueCondition) {
302 KeyValueCondition keyValueCondition = (KeyValueCondition) c;
303 if (!Op.NEGATED_OPS.contains(keyValueCondition.op)) {
304 key = keyValueCondition.k;
305 }
306 }
307 }
308 return key;
309 }
310
311 private boolean conditionRequiresKeyPresence(KeyMatchType matchType) {
312 return matchType != KeyMatchType.REGEX;
313 }
314
315 private MapCSSKeyRules getEntryInIndex(String key) {
316 MapCSSKeyRules rulesWithMatchingKey = index.get(key);
317 if (rulesWithMatchingKey == null) {
318 rulesWithMatchingKey = new MapCSSKeyRules();
319 index.put(key.intern(), rulesWithMatchingKey);
320 }
321 return rulesWithMatchingKey;
322 }
323
324 /**
325 * Get a subset of all rules that might match the primitive. Rules not included in the result are guaranteed to
326 * not match this primitive.
327 * <p>
328 * You must have a read lock of STYLE_SOURCE_LOCK when calling this method.
329 *
330 * @param osm the primitive to match
331 * @return An iterator over possible rules in the right order.
332 */
333 public Iterator<MapCSSRule> getRuleCandidates(OsmPrimitive osm) {
334 final BitSet ruleCandidates = new BitSet(rules.size());
335 ruleCandidates.or(remaining);
336
337 final RuleCandidatesIterator candidatesIterator = new RuleCandidatesIterator(ruleCandidates);
338 osm.visitKeys(candidatesIterator);
339 candidatesIterator.prepare();
340 return candidatesIterator;
341 }
342
343 /**
344 * Clear the index.
345 * <p>
346 * You must own the write lock STYLE_SOURCE_LOCK when calling this method.
347 */
348 public void clear() {
349 rules.clear();
350 index.clear();
351 remaining.clear();
352 }
353 }
354
355 /**
356 * Constructs a new, active {@link MapCSSStyleSource}.
357 * @param url URL that {@link org.openstreetmap.josm.io.CachedFile} understands
358 * @param name The name for this StyleSource
359 * @param shortdescription The title for that source.
360 */
361 public MapCSSStyleSource(String url, String name, String shortdescription) {
362 super(url, name, shortdescription);
363 }
364
365 /**
366 * Constructs a new {@link MapCSSStyleSource}
367 * @param entry The entry to copy the data (url, name, ...) from.
368 */
369 public MapCSSStyleSource(SourceEntry entry) {
370 super(entry);
371 }
372
373 /**
374 * <p>Creates a new style source from the MapCSS styles supplied in
375 * {@code css}</p>
376 *
377 * @param css the MapCSS style declaration. Must not be null.
378 * @throws IllegalArgumentException if {@code css} is null
379 */
380 public MapCSSStyleSource(String css) {
381 super(null, null, null);
382 CheckParameterUtil.ensureParameterNotNull(css);
383 this.css = css;
384 }
385
386 @Override
387 public void loadStyleSource() {
388 STYLE_SOURCE_LOCK.writeLock().lock();
389 try {
390 init();
391 rules.clear();
392 nodeRules.clear();
393 wayRules.clear();
394 wayNoAreaRules.clear();
395 relationRules.clear();
396 multipolygonRules.clear();
397 canvasRules.clear();
398 try (InputStream in = getSourceInputStream()) {
399 try {
400 // evaluate @media { ... } blocks
401 MapCSSParser preprocessor = new MapCSSParser(in, "UTF-8", MapCSSParser.LexicalState.PREPROCESSOR);
402 String mapcss = preprocessor.pp_root(this);
403
404 // do the actual mapcss parsing
405 InputStream in2 = new ByteArrayInputStream(mapcss.getBytes(StandardCharsets.UTF_8));
406 MapCSSParser parser = new MapCSSParser(in2, "UTF-8", MapCSSParser.LexicalState.DEFAULT);
407 parser.sheet(this);
408
409 loadMeta();
410 loadCanvas();
411 loadSettings();
412 } finally {
413 closeSourceInputStream(in);
414 }
415 } catch (IOException e) {
416 Main.warn(tr("Failed to load Mappaint styles from ''{0}''. Exception was: {1}", url, e.toString()));
417 Main.error(e);
418 logError(e);
419 } catch (TokenMgrError e) {
420 Main.warn(tr("Failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage()));
421 Main.error(e);
422 logError(e);
423 } catch (ParseException e) {
424 Main.warn(tr("Failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage()));
425 Main.error(e);
426 logError(new ParseException(e.getMessage())); // allow e to be garbage collected, it links to the entire token stream
427 }
428 // optimization: filter rules for different primitive types
429 for (MapCSSRule r: rules) {
430 // find the rightmost selector, this must be a GeneralSelector
431 Selector selRightmost = r.selector;
432 while (selRightmost instanceof ChildOrParentSelector) {
433 selRightmost = ((ChildOrParentSelector) selRightmost).right;
434 }
435 MapCSSRule optRule = new MapCSSRule(r.selector.optimizedBaseCheck(), r.declaration);
436 final String base = ((GeneralSelector) selRightmost).getBase();
437 switch (base) {
438 case "node":
439 nodeRules.add(optRule);
440 break;
441 case "way":
442 wayNoAreaRules.add(optRule);
443 wayRules.add(optRule);
444 break;
445 case "area":
446 wayRules.add(optRule);
447 multipolygonRules.add(optRule);
448 break;
449 case "relation":
450 relationRules.add(optRule);
451 multipolygonRules.add(optRule);
452 break;
453 case "*":
454 nodeRules.add(optRule);
455 wayRules.add(optRule);
456 wayNoAreaRules.add(optRule);
457 relationRules.add(optRule);
458 multipolygonRules.add(optRule);
459 break;
460 case "canvas":
461 canvasRules.add(r);
462 break;
463 case "meta":
464 case "setting":
465 break;
466 default:
467 final RuntimeException e = new RuntimeException(MessageFormat.format("Unknown MapCSS base selector {0}", base));
468 Main.warn(tr("Failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage()));
469 Main.error(e);
470 logError(e);
471 }
472 }
473 nodeRules.initIndex();
474 wayRules.initIndex();
475 wayNoAreaRules.initIndex();
476 relationRules.initIndex();
477 multipolygonRules.initIndex();
478 canvasRules.initIndex();
479 } finally {
480 STYLE_SOURCE_LOCK.writeLock().unlock();
481 }
482 }
483
484 @Override
485 public InputStream getSourceInputStream() throws IOException {
486 if (css != null) {
487 return new ByteArrayInputStream(css.getBytes(StandardCharsets.UTF_8));
488 }
489 CachedFile cf = getCachedFile();
490 if (isZip) {
491 File file = cf.getFile();
492 zipFile = new ZipFile(file, StandardCharsets.UTF_8);
493 zipIcons = file;
494 ZipEntry zipEntry = zipFile.getEntry(zipEntryPath);
495 return zipFile.getInputStream(zipEntry);
496 } else {
497 zipFile = null;
498 zipIcons = null;
499 return cf.getInputStream();
500 }
501 }
502
503 @Override
504 public CachedFile getCachedFile() throws IOException {
505 return new CachedFile(url).setHttpAccept(MAPCSS_STYLE_MIME_TYPES);
506 }
507
508 @Override
509 public void closeSourceInputStream(InputStream is) {
510 super.closeSourceInputStream(is);
511 if (isZip) {
512 Utils.close(zipFile);
513 }
514 }
515
516 /**
517 * load meta info from a selector "meta"
518 */
519 private void loadMeta() {
520 Cascade c = constructSpecial("meta");
521 String pTitle = c.get("title", null, String.class);
522 if (title == null) {
523 title = pTitle;
524 }
525 String pIcon = c.get("icon", null, String.class);
526 if (icon == null) {
527 icon = pIcon;
528 }
529 }
530
531 private void loadCanvas() {
532 Cascade c = constructSpecial("canvas");
533 backgroundColorOverride = c.get("fill-color", null, Color.class);
534 if (backgroundColorOverride == null) {
535 backgroundColorOverride = c.get("background-color", null, Color.class);
536 if (backgroundColorOverride != null) {
537 Main.warn(tr("Detected deprecated ''{0}'' in ''{1}'' which will be removed shortly. Use ''{2}'' instead.",
538 "canvas{background-color}", url, "fill-color"));
539 }
540 }
541 }
542
543 private void loadSettings() {
544 settings.clear();
545 settingValues.clear();
546 MultiCascade mc = new MultiCascade();
547 Node n = new Node();
548 String code = LanguageInfo.getJOSMLocaleCode();
549 n.put("lang", code);
550 // create a fake environment to read the meta data block
551 Environment env = new Environment(n, mc, "default", this);
552
553 for (MapCSSRule r : rules) {
554 if (r.selector instanceof GeneralSelector) {
555 GeneralSelector gs = (GeneralSelector) r.selector;
556 if ("setting".equals(gs.getBase())) {
557 if (!gs.matchesConditions(env)) {
558 continue;
559 }
560 env.layer = null;
561 env.layer = gs.getSubpart().getId(env);
562 r.execute(env);
563 }
564 }
565 }
566 for (Entry<String, Cascade> e : mc.getLayers()) {
567 if ("default".equals(e.getKey())) {
568 Main.warn("setting requires layer identifier e.g. 'setting::my_setting {...}'");
569 continue;
570 }
571 Cascade c = e.getValue();
572 String type = c.get("type", null, String.class);
573 StyleSetting set = null;
574 if ("boolean".equals(type)) {
575 set = BooleanStyleSetting.create(c, this, e.getKey());
576 } else {
577 Main.warn("Unkown setting type: "+type);
578 }
579 if (set != null) {
580 settings.add(set);
581 settingValues.put(e.getKey(), set.getValue());
582 }
583 }
584 }
585
586 private Cascade constructSpecial(String type) {
587
588 MultiCascade mc = new MultiCascade();
589 Node n = new Node();
590 String code = LanguageInfo.getJOSMLocaleCode();
591 n.put("lang", code);
592 // create a fake environment to read the meta data block
593 Environment env = new Environment(n, mc, "default", this);
594
595 for (MapCSSRule r : rules) {
596 if (r.selector instanceof GeneralSelector) {
597 GeneralSelector gs = (GeneralSelector) r.selector;
598 if (gs.getBase().equals(type)) {
599 if (!gs.matchesConditions(env)) {
600 continue;
601 }
602 r.execute(env);
603 }
604 }
605 }
606 return mc.getCascade("default");
607 }
608
609 @Override
610 public Color getBackgroundColorOverride() {
611 return backgroundColorOverride;
612 }
613
614 @Override
615 public void apply(MultiCascade mc, OsmPrimitive osm, double scale, boolean pretendWayIsClosed) {
616 Environment env = new Environment(osm, mc, null, this);
617 MapCSSRuleIndex matchingRuleIndex;
618 if (osm instanceof Node) {
619 matchingRuleIndex = nodeRules;
620 } else if (osm instanceof Way) {
621 if (osm.isKeyFalse("area")) {
622 matchingRuleIndex = wayNoAreaRules;
623 } else {
624 matchingRuleIndex = wayRules;
625 }
626 } else {
627 if (((Relation) osm).isMultipolygon()) {
628 matchingRuleIndex = multipolygonRules;
629 } else if (osm.hasKey("#canvas")) {
630 matchingRuleIndex = canvasRules;
631 } else {
632 matchingRuleIndex = relationRules;
633 }
634 }
635
636 // the declaration indices are sorted, so it suffices to save the
637 // last used index
638 int lastDeclUsed = -1;
639
640 Iterator<MapCSSRule> candidates = matchingRuleIndex.getRuleCandidates(osm);
641 while (candidates.hasNext()) {
642 MapCSSRule r = candidates.next();
643 env.clearSelectorMatchingInformation();
644 env.layer = null;
645 String sub = env.layer = r.selector.getSubpart().getId(env);
646 if (r.selector.matches(env)) { // as side effect env.parent will be set (if s is a child selector)
647 Selector s = r.selector;
648 if (s.getRange().contains(scale)) {
649 mc.range = Range.cut(mc.range, s.getRange());
650 } else {
651 mc.range = mc.range.reduceAround(scale, s.getRange());
652 continue;
653 }
654
655 if (r.declaration.idx == lastDeclUsed)
656 continue; // don't apply one declaration more than once
657 lastDeclUsed = r.declaration.idx;
658 if ("*".equals(sub)) {
659 for (Entry<String, Cascade> entry : mc.getLayers()) {
660 env.layer = entry.getKey();
661 if ("*".equals(env.layer)) {
662 continue;
663 }
664 r.execute(env);
665 }
666 }
667 env.layer = sub;
668 r.execute(env);
669 }
670 }
671 }
672
673 public boolean evalSupportsDeclCondition(String feature, Object val) {
674 if (feature == null) return false;
675 if (SUPPORTED_KEYS.contains(feature)) return true;
676 switch (feature) {
677 case "user-agent":
678 {
679 String s = Cascade.convertTo(val, String.class);
680 return "josm".equals(s);
681 }
682 case "min-josm-version":
683 {
684 Float v = Cascade.convertTo(val, Float.class);
685 return v != null && Math.round(v) <= Version.getInstance().getVersion();
686 }
687 case "max-josm-version":
688 {
689 Float v = Cascade.convertTo(val, Float.class);
690 return v != null && Math.round(v) >= Version.getInstance().getVersion();
691 }
692 default:
693 return false;
694 }
695 }
696
697 @Override
698 public String toString() {
699 return Utils.join("\n", rules);
700 }
701}
Note: See TracBrowser for help on using the repository browser.