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

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

reworked MirroredInputStream (renamed to CachedFile):

  • no more awkwardly open and close InputStream if you just want the underlying file (e.g. to get file inside zip file)
  • make it easier to add configuration parameters, without having endless list of parameters for the constructor (Factory style, similar to ImageProvider)

breaks plugins; see #10139

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