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

Last change on this file since 7138 was 7138, checked in by bastiK, 10 years ago

see #9691 - add tag based index (speed-up of factor 3 in single thread mode)

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