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

Last change on this file since 13845 was 13845, checked in by Don-vip, 6 years ago

fix #16321, see #10047 - mapcss metadata is required

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