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

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

fix Sonar issue squid:ModifiersOrderCheck - Modifiers should be declared in the correct order

  • Property svn:eol-style set to native
File size: 21.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.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.Collection;
16import java.util.Collections;
17import java.util.HashMap;
18import java.util.HashSet;
19import java.util.List;
20import java.util.Map;
21import java.util.Map.Entry;
22import java.util.Set;
23import java.util.concurrent.locks.ReadWriteLock;
24import java.util.concurrent.locks.ReentrantReadWriteLock;
25import java.util.zip.ZipEntry;
26import java.util.zip.ZipFile;
27
28import org.openstreetmap.josm.Main;
29import org.openstreetmap.josm.data.Version;
30import org.openstreetmap.josm.data.osm.Node;
31import org.openstreetmap.josm.data.osm.OsmPrimitive;
32import org.openstreetmap.josm.data.osm.Relation;
33import org.openstreetmap.josm.data.osm.Way;
34import org.openstreetmap.josm.gui.mappaint.Cascade;
35import org.openstreetmap.josm.gui.mappaint.Environment;
36import org.openstreetmap.josm.gui.mappaint.LineElemStyle;
37import org.openstreetmap.josm.gui.mappaint.MultiCascade;
38import org.openstreetmap.josm.gui.mappaint.Range;
39import org.openstreetmap.josm.gui.mappaint.StyleKeys;
40import org.openstreetmap.josm.gui.mappaint.StyleSetting;
41import org.openstreetmap.josm.gui.mappaint.StyleSetting.BooleanStyleSetting;
42import org.openstreetmap.josm.gui.mappaint.StyleSource;
43import org.openstreetmap.josm.gui.mappaint.mapcss.Condition.SimpleKeyValueCondition;
44import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.ChildOrParentSelector;
45import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector;
46import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.OptimizedGeneralSelector;
47import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser;
48import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
49import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.TokenMgrError;
50import org.openstreetmap.josm.gui.preferences.SourceEntry;
51import org.openstreetmap.josm.io.CachedFile;
52import org.openstreetmap.josm.tools.CheckParameterUtil;
53import org.openstreetmap.josm.tools.LanguageInfo;
54import org.openstreetmap.josm.tools.Utils;
55
56public class MapCSSStyleSource extends StyleSource {
57
58 /**
59 * The accepted MIME types sent in the HTTP Accept header.
60 * @since 6867
61 */
62 public static final String MAPCSS_STYLE_MIME_TYPES = "text/x-mapcss, text/mapcss, text/css; q=0.9, text/plain; q=0.8, application/zip, application/octet-stream; q=0.5";
63
64 // all rules
65 public final List<MapCSSRule> rules = new ArrayList<>();
66 // rule indices, filtered by primitive type
67 public final MapCSSRuleIndex nodeRules = new MapCSSRuleIndex(); // nodes
68 public final MapCSSRuleIndex wayRules = new MapCSSRuleIndex(); // ways without tag area=no
69 public final MapCSSRuleIndex wayNoAreaRules = new MapCSSRuleIndex(); // ways with tag area=no
70 public final MapCSSRuleIndex relationRules = new MapCSSRuleIndex(); // relations that are not multipolygon relations
71 public final MapCSSRuleIndex multipolygonRules = new MapCSSRuleIndex(); // multipolygon relations
72 public final MapCSSRuleIndex canvasRules = new MapCSSRuleIndex(); // rules to apply canvas properties
73
74 private Color backgroundColorOverride;
75 private String css = null;
76 private ZipFile zipFile;
77
78 /**
79 * This lock prevents concurrent execution of {@link MapCSSRuleIndex#clear() } /
80 * {@link MapCSSRuleIndex#initIndex()} and {@link MapCSSRuleIndex#getRuleCandidates }.
81 *
82 * For efficiency reasons, these methods are synchronized higher up the
83 * stack trace.
84 */
85 public static final ReadWriteLock STYLE_SOURCE_LOCK = new ReentrantReadWriteLock();
86
87 /**
88 * Set of all supported MapCSS keys.
89 */
90 public static final Set<String> SUPPORTED_KEYS = new HashSet<>();
91 static {
92 Field[] declaredFields = StyleKeys.class.getDeclaredFields();
93 for (Field f : declaredFields) {
94 try {
95 SUPPORTED_KEYS.add((String) f.get(null));
96 if (!f.getName().toLowerCase().replace("_", "-").equals(f.get(null))) {
97 throw new RuntimeException(f.getName());
98 }
99 } catch (IllegalArgumentException | IllegalAccessException ex) {
100 throw new RuntimeException(ex);
101 }
102 }
103 for (LineElemStyle.LineType lt : LineElemStyle.LineType.values()) {
104 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.COLOR);
105 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES);
106 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES_BACKGROUND_COLOR);
107 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES_BACKGROUND_OPACITY);
108 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES_OFFSET);
109 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.LINECAP);
110 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.LINEJOIN);
111 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.MITERLIMIT);
112 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.OFFSET);
113 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.OPACITY);
114 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.REAL_WIDTH);
115 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.WIDTH);
116 }
117 }
118
119 /**
120 * A collection of {@link MapCSSRule}s, that are indexed by tag key and value.
121 *
122 * Speeds up the process of finding all rules that match a certain primitive.
123 *
124 * Rules with a {@link SimpleKeyValueCondition} [key=value] are indexed by
125 * key and value in a HashMap. Now you only need to loop the tags of a
126 * primitive to retrieve the possibly matching rules.
127 *
128 * Rules with no SimpleKeyValueCondition in the selector have to be
129 * checked separately.
130 *
131 * The order of rules gets mixed up by this and needs to be sorted later.
132 */
133 public static class MapCSSRuleIndex {
134 /* all rules for this index */
135 public final List<MapCSSRule> rules = new ArrayList<>();
136 /* tag based index */
137 public final Map<String,Map<String,Set<MapCSSRule>>> index = new HashMap<>();
138 /* rules without SimpleKeyValueCondition */
139 public final Set<MapCSSRule> remaining = new HashSet<>();
140
141 public void add(MapCSSRule rule) {
142 rules.add(rule);
143 }
144
145 /**
146 * Initialize the index.
147 *
148 * You must own the write lock of STYLE_SOURCE_LOCK when calling this method.
149 */
150 public void initIndex() {
151 for (MapCSSRule r: rules) {
152 // find the rightmost selector, this must be a GeneralSelector
153 Selector selRightmost = r.selector;
154 while (selRightmost instanceof ChildOrParentSelector) {
155 selRightmost = ((ChildOrParentSelector) selRightmost).right;
156 }
157 OptimizedGeneralSelector s = (OptimizedGeneralSelector) selRightmost;
158 if (s.conds == null) {
159 remaining.add(r);
160 continue;
161 }
162 List<SimpleKeyValueCondition> sk = new ArrayList<>(Utils.filteredCollection(s.conds, SimpleKeyValueCondition.class));
163 if (sk.isEmpty()) {
164 remaining.add(r);
165 continue;
166 }
167 SimpleKeyValueCondition c = sk.get(sk.size() - 1);
168 Map<String,Set<MapCSSRule>> rulesWithMatchingKey = index.get(c.k);
169 if (rulesWithMatchingKey == null) {
170 rulesWithMatchingKey = new HashMap<>();
171 index.put(c.k, rulesWithMatchingKey);
172 }
173 Set<MapCSSRule> rulesWithMatchingKeyValue = rulesWithMatchingKey.get(c.v);
174 if (rulesWithMatchingKeyValue == null) {
175 rulesWithMatchingKeyValue = new HashSet<>();
176 rulesWithMatchingKey.put(c.v, rulesWithMatchingKeyValue);
177 }
178 rulesWithMatchingKeyValue.add(r);
179 }
180 }
181
182 /**
183 * Get a subset of all rules that might match the primitive.
184 * @param osm the primitive to match
185 * @return a Collection of rules that filters out most of the rules
186 * that cannot match, based on the tags of the primitive
187 *
188 * You must have a read lock of STYLE_SOURCE_LOCK when calling this method.
189 */
190 public Collection<MapCSSRule> getRuleCandidates(OsmPrimitive osm) {
191 List<MapCSSRule> ruleCandidates = new ArrayList<>(remaining);
192 for (Map.Entry<String,String> e : osm.getKeys().entrySet()) {
193 Map<String,Set<MapCSSRule>> v = index.get(e.getKey());
194 if (v != null) {
195 Set<MapCSSRule> rs = v.get(e.getValue());
196 if (rs != null) {
197 ruleCandidates.addAll(rs);
198 }
199 }
200 }
201 Collections.sort(ruleCandidates);
202 return ruleCandidates;
203 }
204
205 /**
206 * Clear the index.
207 *
208 * You must own the write lock STYLE_SOURCE_LOCK when calling this method.
209 */
210 public void clear() {
211 rules.clear();
212 index.clear();
213 remaining.clear();
214 }
215 }
216
217 public MapCSSStyleSource(String url, String name, String shortdescription) {
218 super(url, name, shortdescription);
219 }
220
221 public MapCSSStyleSource(SourceEntry entry) {
222 super(entry);
223 }
224
225 /**
226 * <p>Creates a new style source from the MapCSS styles supplied in
227 * {@code css}</p>
228 *
229 * @param css the MapCSS style declaration. Must not be null.
230 * @throws IllegalArgumentException thrown if {@code css} is null
231 */
232 public MapCSSStyleSource(String css) throws IllegalArgumentException{
233 super(null, null, null);
234 CheckParameterUtil.ensureParameterNotNull(css);
235 this.css = css;
236 }
237
238 @Override
239 public void loadStyleSource() {
240 STYLE_SOURCE_LOCK.writeLock().lock();
241 try {
242 init();
243 rules.clear();
244 nodeRules.clear();
245 wayRules.clear();
246 wayNoAreaRules.clear();
247 relationRules.clear();
248 multipolygonRules.clear();
249 canvasRules.clear();
250 try (InputStream in = getSourceInputStream()) {
251 try {
252 // evaluate @media { ... } blocks
253 MapCSSParser preprocessor = new MapCSSParser(in, "UTF-8", MapCSSParser.LexicalState.PREPROCESSOR);
254 String mapcss = preprocessor.pp_root(this);
255
256 // do the actual mapcss parsing
257 InputStream in2 = new ByteArrayInputStream(mapcss.getBytes(StandardCharsets.UTF_8));
258 MapCSSParser parser = new MapCSSParser(in2, "UTF-8", MapCSSParser.LexicalState.DEFAULT);
259 parser.sheet(this);
260
261 loadMeta();
262 loadCanvas();
263 loadSettings();
264 } finally {
265 closeSourceInputStream(in);
266 }
267 } catch (IOException e) {
268 Main.warn(tr("Failed to load Mappaint styles from ''{0}''. Exception was: {1}", url, e.toString()));
269 Main.error(e);
270 logError(e);
271 } catch (TokenMgrError e) {
272 Main.warn(tr("Failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage()));
273 Main.error(e);
274 logError(e);
275 } catch (ParseException e) {
276 Main.warn(tr("Failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage()));
277 Main.error(e);
278 logError(new ParseException(e.getMessage())); // allow e to be garbage collected, it links to the entire token stream
279 }
280 // optimization: filter rules for different primitive types
281 for (MapCSSRule r: rules) {
282 // find the rightmost selector, this must be a GeneralSelector
283 Selector selRightmost = r.selector;
284 while (selRightmost instanceof ChildOrParentSelector) {
285 selRightmost = ((ChildOrParentSelector) selRightmost).right;
286 }
287 MapCSSRule optRule = new MapCSSRule(r.selector.optimizedBaseCheck(), r.declaration);
288 final String base = ((GeneralSelector) selRightmost).getBase();
289 switch (base) {
290 case "node":
291 nodeRules.add(optRule);
292 break;
293 case "way":
294 wayNoAreaRules.add(optRule);
295 wayRules.add(optRule);
296 break;
297 case "area":
298 wayRules.add(optRule);
299 multipolygonRules.add(optRule);
300 break;
301 case "relation":
302 relationRules.add(optRule);
303 multipolygonRules.add(optRule);
304 break;
305 case "*":
306 nodeRules.add(optRule);
307 wayRules.add(optRule);
308 wayNoAreaRules.add(optRule);
309 relationRules.add(optRule);
310 multipolygonRules.add(optRule);
311 break;
312 case "canvas":
313 canvasRules.add(r);
314 break;
315 case "meta":
316 case "setting":
317 break;
318 default:
319 final RuntimeException e = new RuntimeException(MessageFormat.format("Unknown MapCSS base selector {0}", base));
320 Main.warn(tr("Failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage()));
321 Main.error(e);
322 logError(e);
323 }
324 }
325 nodeRules.initIndex();
326 wayRules.initIndex();
327 wayNoAreaRules.initIndex();
328 relationRules.initIndex();
329 multipolygonRules.initIndex();
330 canvasRules.initIndex();
331 } finally {
332 STYLE_SOURCE_LOCK.writeLock().unlock();
333 }
334 }
335
336 @Override
337 public InputStream getSourceInputStream() throws IOException {
338 if (css != null) {
339 return new ByteArrayInputStream(css.getBytes(StandardCharsets.UTF_8));
340 }
341 CachedFile cf = getCachedFile();
342 if (isZip) {
343 File file = cf.getFile();
344 zipFile = new ZipFile(file, StandardCharsets.UTF_8);
345 zipIcons = file;
346 ZipEntry zipEntry = zipFile.getEntry(zipEntryPath);
347 return zipFile.getInputStream(zipEntry);
348 } else {
349 zipFile = null;
350 zipIcons = null;
351 return cf.getInputStream();
352 }
353 }
354
355 @Override
356 public CachedFile getCachedFile() throws IOException {
357 return new CachedFile(url).setHttpAccept(MAPCSS_STYLE_MIME_TYPES);
358 }
359
360 @Override
361 public void closeSourceInputStream(InputStream is) {
362 super.closeSourceInputStream(is);
363 if (isZip) {
364 Utils.close(zipFile);
365 }
366 }
367
368 /**
369 * load meta info from a selector "meta"
370 */
371 private void loadMeta() {
372 Cascade c = constructSpecial("meta");
373 String pTitle = c.get("title", null, String.class);
374 if (title == null) {
375 title = pTitle;
376 }
377 String pIcon = c.get("icon", null, String.class);
378 if (icon == null) {
379 icon = pIcon;
380 }
381 }
382
383 private void loadCanvas() {
384 Cascade c = constructSpecial("canvas");
385 backgroundColorOverride = c.get("fill-color", null, Color.class);
386 if (backgroundColorOverride == null) {
387 backgroundColorOverride = c.get("background-color", null, Color.class);
388 if (backgroundColorOverride != null) {
389 Main.warn(tr("Detected deprecated ''{0}'' in ''{1}'' which will be removed shortly. Use ''{2}'' instead.", "canvas{background-color}", url, "fill-color"));
390 }
391 }
392 }
393
394 private void loadSettings() {
395 settings.clear();
396 settingValues.clear();
397 MultiCascade mc = new MultiCascade();
398 Node n = new Node();
399 String code = LanguageInfo.getJOSMLocaleCode();
400 n.put("lang", code);
401 // create a fake environment to read the meta data block
402 Environment env = new Environment(n, mc, "default", this);
403
404 for (MapCSSRule r : rules) {
405 if ((r.selector instanceof GeneralSelector)) {
406 GeneralSelector gs = (GeneralSelector) r.selector;
407 if (gs.getBase().equals("setting")) {
408 if (!gs.matchesConditions(env)) {
409 continue;
410 }
411 env.layer = null;
412 env.layer = gs.getSubpart().getId(env);
413 r.execute(env);
414 }
415 }
416 }
417 for (Entry<String, Cascade> e : mc.getLayers()) {
418 if ("default".equals(e.getKey())) {
419 Main.warn("setting requires layer identifier e.g. 'setting::my_setting {...}'");
420 continue;
421 }
422 Cascade c = e.getValue();
423 String type = c.get("type", null, String.class);
424 StyleSetting set = null;
425 if ("boolean".equals(type)) {
426 set = BooleanStyleSetting.create(c, this, e.getKey());
427 } else {
428 Main.warn("Unkown setting type: "+type);
429 }
430 if (set != null) {
431 settings.add(set);
432 settingValues.put(e.getKey(), set.getValue());
433 }
434 }
435 }
436
437 private Cascade constructSpecial(String type) {
438
439 MultiCascade mc = new MultiCascade();
440 Node n = new Node();
441 String code = LanguageInfo.getJOSMLocaleCode();
442 n.put("lang", code);
443 // create a fake environment to read the meta data block
444 Environment env = new Environment(n, mc, "default", this);
445
446 for (MapCSSRule r : rules) {
447 if ((r.selector instanceof GeneralSelector)) {
448 GeneralSelector gs = (GeneralSelector) r.selector;
449 if (gs.getBase().equals(type)) {
450 if (!gs.matchesConditions(env)) {
451 continue;
452 }
453 r.execute(env);
454 }
455 }
456 }
457 return mc.getCascade("default");
458 }
459
460 @Override
461 public Color getBackgroundColorOverride() {
462 return backgroundColorOverride;
463 }
464
465 @Override
466 public void apply(MultiCascade mc, OsmPrimitive osm, double scale, boolean pretendWayIsClosed) {
467 Environment env = new Environment(osm, mc, null, this);
468 MapCSSRuleIndex matchingRuleIndex;
469 if (osm instanceof Node) {
470 matchingRuleIndex = nodeRules;
471 } else if (osm instanceof Way) {
472 if (osm.isKeyFalse("area")) {
473 matchingRuleIndex = wayNoAreaRules;
474 } else {
475 matchingRuleIndex = wayRules;
476 }
477 } else {
478 if (((Relation) osm).isMultipolygon()) {
479 matchingRuleIndex = multipolygonRules;
480 } else if (osm.hasKey("#canvas")) {
481 matchingRuleIndex = canvasRules;
482 } else {
483 matchingRuleIndex = relationRules;
484 }
485 }
486
487 // the declaration indices are sorted, so it suffices to save the
488 // last used index
489 int lastDeclUsed = -1;
490
491 for (MapCSSRule r : matchingRuleIndex.getRuleCandidates(osm)) {
492 env.clearSelectorMatchingInformation();
493 env.layer = null;
494 String sub = env.layer = r.selector.getSubpart().getId(env);
495 if (r.selector.matches(env)) { // as side effect env.parent will be set (if s is a child selector)
496 Selector s = r.selector;
497 if (s.getRange().contains(scale)) {
498 mc.range = Range.cut(mc.range, s.getRange());
499 } else {
500 mc.range = mc.range.reduceAround(scale, s.getRange());
501 continue;
502 }
503
504 if (r.declaration.idx == lastDeclUsed) continue; // don't apply one declaration more than once
505 lastDeclUsed = r.declaration.idx;
506 if ("*".equals(sub)) {
507 for (Entry<String, Cascade> entry : mc.getLayers()) {
508 env.layer = entry.getKey();
509 if ("*".equals(env.layer)) {
510 continue;
511 }
512 r.execute(env);
513 }
514 }
515 env.layer = sub;
516 r.execute(env);
517 }
518 }
519 }
520
521 public boolean evalSupportsDeclCondition(String feature, Object val) {
522 if (feature == null) return false;
523 if (SUPPORTED_KEYS.contains(feature)) return true;
524 switch (feature) {
525 case "user-agent":
526 {
527 String s = Cascade.convertTo(val, String.class);
528 return "josm".equals(s);
529 }
530 case "min-josm-version":
531 {
532 Float v = Cascade.convertTo(val, Float.class);
533 return v != null && Math.round(v) <= Version.getInstance().getVersion();
534 }
535 case "max-josm-version":
536 {
537 Float v = Cascade.convertTo(val, Float.class);
538 return v != null && Math.round(v) >= Version.getInstance().getVersion();
539 }
540 default:
541 return false;
542 }
543 }
544
545 @Override
546 public String toString() {
547 return Utils.join("\n", rules);
548 }
549}
Note: See TracBrowser for help on using the repository browser.