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

Last change on this file since 7509 was 7509, checked in by stoecker, 10 years ago

remove tabs

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