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

Last change on this file since 15717 was 15717, checked in by simon04, 4 years ago

Java 8: use Collectors.joining

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