source: josm/trunk/src/org/openstreetmap/josm/gui/mappaint/ElemStyles.java@ 9341

Last change on this file since 9341 was 9341, checked in by bastiK, 9 years ago

mapcss: basic support for :selected pseudoclass (see #9891)

  • Property svn:eol-style set to native
File size: 22.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.mappaint;
3
4import java.awt.Color;
5import java.util.ArrayList;
6import java.util.Collection;
7import java.util.Collections;
8import java.util.HashMap;
9import java.util.List;
10import java.util.Map;
11import java.util.Map.Entry;
12
13import org.openstreetmap.josm.Main;
14import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
15import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
16import org.openstreetmap.josm.data.osm.Node;
17import org.openstreetmap.josm.data.osm.OsmPrimitive;
18import org.openstreetmap.josm.data.osm.Relation;
19import org.openstreetmap.josm.data.osm.Way;
20import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
21import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
22import org.openstreetmap.josm.gui.NavigatableComponent;
23import org.openstreetmap.josm.gui.mappaint.DividedScale.RangeViolatedError;
24import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
25import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement;
26import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement;
27import org.openstreetmap.josm.gui.mappaint.styleelement.LineElement;
28import org.openstreetmap.josm.gui.mappaint.styleelement.LineTextElement;
29import org.openstreetmap.josm.gui.mappaint.styleelement.NodeElement;
30import org.openstreetmap.josm.gui.mappaint.styleelement.RepeatImageElement;
31import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement;
32import org.openstreetmap.josm.gui.mappaint.styleelement.TextLabel;
33import org.openstreetmap.josm.gui.util.GuiHelper;
34import org.openstreetmap.josm.tools.Pair;
35import org.openstreetmap.josm.tools.Utils;
36
37public class ElemStyles implements PreferenceChangedListener {
38 private final List<StyleSource> styleSources;
39 private boolean drawMultipolygon;
40
41 private int cacheIdx = 1;
42
43 private boolean defaultNodes, defaultLines;
44 private int defaultNodesIdx, defaultLinesIdx;
45
46 private final Map<String, String> preferenceCache = new HashMap<>();
47
48 /**
49 * Constructs a new {@code ElemStyles}.
50 */
51 public ElemStyles() {
52 styleSources = new ArrayList<>();
53 Main.pref.addPreferenceChangeListener(this);
54 }
55
56 /**
57 * Clear the style cache for all primitives of all DataSets.
58 */
59 public void clearCached() {
60 // run in EDT to make sure this isn't called during rendering run
61 GuiHelper.runInEDT(new Runnable() {
62 @Override
63 public void run() {
64 cacheIdx++;
65 preferenceCache.clear();
66 }
67 });
68 }
69
70 public List<StyleSource> getStyleSources() {
71 return Collections.<StyleSource>unmodifiableList(styleSources);
72 }
73
74 /**
75 * Create the list of styles for one primitive.
76 *
77 * @param osm the primitive
78 * @param scale the scale (in meters per 100 pixel)
79 * @param nc display component
80 * @return list of styles
81 */
82 public StyleElementList get(OsmPrimitive osm, double scale, NavigatableComponent nc) {
83 return getStyleCacheWithRange(osm, scale, nc).a;
84 }
85
86 /**
87 * Create the list of styles and its valid scale range for one primitive.
88 *
89 * Automatically adds default styles in case no proper style was found.
90 * Uses the cache, if possible, and saves the results to the cache.
91 * @param osm OSM primitive
92 * @param scale scale
93 * @param nc navigatable component
94 * @return pair containing style list and range
95 */
96 public Pair<StyleElementList, Range> getStyleCacheWithRange(OsmPrimitive osm, double scale, NavigatableComponent nc) {
97 if (osm.mappaintStyle == null || osm.mappaintCacheIdx != cacheIdx || scale <= 0) {
98 osm.mappaintStyle = StyleCache.EMPTY_STYLECACHE;
99 } else {
100 Pair<StyleElementList, Range> lst = osm.mappaintStyle.getWithRange(scale, osm.isSelected());
101 if (lst.a != null)
102 return lst;
103 }
104 Pair<StyleElementList, Range> p = getImpl(osm, scale, nc);
105 if (osm instanceof Node && isDefaultNodes()) {
106 if (p.a.isEmpty()) {
107 if (TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) {
108 p.a = NodeElement.DEFAULT_NODE_STYLELIST_TEXT;
109 } else {
110 p.a = NodeElement.DEFAULT_NODE_STYLELIST;
111 }
112 } else {
113 boolean hasNonModifier = false;
114 boolean hasText = false;
115 for (StyleElement s : p.a) {
116 if (s instanceof BoxTextElement) {
117 hasText = true;
118 } else {
119 if (!s.isModifier) {
120 hasNonModifier = true;
121 }
122 }
123 }
124 if (!hasNonModifier) {
125 p.a = new StyleElementList(p.a, NodeElement.SIMPLE_NODE_ELEMSTYLE);
126 if (!hasText) {
127 if (TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) {
128 p.a = new StyleElementList(p.a, BoxTextElement.SIMPLE_NODE_TEXT_ELEMSTYLE);
129 }
130 }
131 }
132 }
133 } else if (osm instanceof Way && isDefaultLines()) {
134 boolean hasProperLineStyle = false;
135 for (StyleElement s : p.a) {
136 if (s.isProperLineStyle()) {
137 hasProperLineStyle = true;
138 break;
139 }
140 }
141 if (!hasProperLineStyle) {
142 AreaElement area = Utils.find(p.a, AreaElement.class);
143 LineElement line = area == null ? LineElement.UNTAGGED_WAY : LineElement.createSimpleLineStyle(area.color, true);
144 p.a = new StyleElementList(p.a, line);
145 }
146 }
147 StyleCache style = osm.mappaintStyle != null ? osm.mappaintStyle : StyleCache.EMPTY_STYLECACHE;
148 try {
149 osm.mappaintStyle = style.put(p.a, p.b, osm.isSelected());
150 } catch (RangeViolatedError e) {
151 throw new AssertionError("Range violated: " + e.getMessage()
152 + " (object: " + osm.getPrimitiveId() + ", current style: "+osm.mappaintStyle
153 + ", scale: " + scale + ", new stylelist: " + p.a + ", new range: " + p.b + ')', e);
154 }
155 osm.mappaintCacheIdx = cacheIdx;
156 return p;
157 }
158
159 /**
160 * Create the list of styles and its valid scale range for one primitive.
161 *
162 * This method does multipolygon handling.
163 *
164 * There are different tagging styles for multipolygons, that have to be respected:
165 * - tags on the relation
166 * - tags on the outer way (deprecated)
167 *
168 * If the primitive is a way, look for multipolygon parents. In case it
169 * is indeed member of some multipolygon as role "outer", all area styles
170 * are removed. (They apply to the multipolygon area.)
171 * Outer ways can have their own independent line styles, e.g. a road as
172 * boundary of a forest. Otherwise, in case, the way does not have an
173 * independent line style, take a line style from the multipolygon.
174 * If the multipolygon does not have a line style either, at least create a
175 * default line style from the color of the area.
176 *
177 * Now consider the case that the way is not an outer way of any multipolygon,
178 * but is member of a multipolygon as "inner".
179 * First, the style list is regenerated, considering only tags of this way.
180 * Then check, if the way describes something in its own right. (linear feature
181 * or area) If not, add a default line style from the area color of the multipolygon.
182 *
183 * @param osm OSM primitive
184 * @param scale scale
185 * @param nc navigatable component
186 * @return pair containing style list and range
187 */
188 private Pair<StyleElementList, Range> getImpl(OsmPrimitive osm, double scale, NavigatableComponent nc) {
189 if (osm instanceof Node)
190 return generateStyles(osm, scale, false);
191 else if (osm instanceof Way) {
192 Pair<StyleElementList, Range> p = generateStyles(osm, scale, false);
193
194 boolean isOuterWayOfSomeMP = false;
195 Color wayColor = null;
196
197 for (OsmPrimitive referrer : osm.getReferrers()) {
198 Relation r = (Relation) referrer;
199 if (!drawMultipolygon || !r.isMultipolygon() || !r.isUsable()) {
200 continue;
201 }
202 Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, r);
203
204 if (multipolygon.getOuterWays().contains(osm)) {
205 boolean hasIndependentLineStyle = false;
206 if (!isOuterWayOfSomeMP) { // do this only one time
207 List<StyleElement> tmp = new ArrayList<>(p.a.size());
208 for (StyleElement s : p.a) {
209 if (s instanceof AreaElement) {
210 wayColor = ((AreaElement) s).color;
211 } else {
212 tmp.add(s);
213 if (s.isProperLineStyle()) {
214 hasIndependentLineStyle = true;
215 }
216 }
217 }
218 p.a = new StyleElementList(tmp);
219 isOuterWayOfSomeMP = true;
220 }
221
222 if (!hasIndependentLineStyle) {
223 Pair<StyleElementList, Range> mpElemStyles;
224 synchronized (r) {
225 mpElemStyles = getStyleCacheWithRange(r, scale, nc);
226 }
227 StyleElement mpLine = null;
228 for (StyleElement s : mpElemStyles.a) {
229 if (s.isProperLineStyle()) {
230 mpLine = s;
231 break;
232 }
233 }
234 p.b = Range.cut(p.b, mpElemStyles.b);
235 if (mpLine != null) {
236 p.a = new StyleElementList(p.a, mpLine);
237 break;
238 } else if (wayColor == null && isDefaultLines()) {
239 AreaElement mpArea = Utils.find(mpElemStyles.a, AreaElement.class);
240 if (mpArea != null) {
241 wayColor = mpArea.color;
242 }
243 }
244 }
245 }
246 }
247 if (isOuterWayOfSomeMP) {
248 if (isDefaultLines()) {
249 boolean hasLineStyle = false;
250 for (StyleElement s : p.a) {
251 if (s.isProperLineStyle()) {
252 hasLineStyle = true;
253 break;
254 }
255 }
256 if (!hasLineStyle) {
257 p.a = new StyleElementList(p.a, LineElement.createSimpleLineStyle(wayColor, true));
258 }
259 }
260 return p;
261 }
262
263 if (!isDefaultLines()) return p;
264
265 for (OsmPrimitive referrer : osm.getReferrers()) {
266 Relation ref = (Relation) referrer;
267 if (!drawMultipolygon || !ref.isMultipolygon() || !ref.isUsable()) {
268 continue;
269 }
270 final Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, ref);
271
272 if (multipolygon.getInnerWays().contains(osm)) {
273 p = generateStyles(osm, scale, false);
274 boolean hasIndependentElemStyle = false;
275 for (StyleElement s : p.a) {
276 if (s.isProperLineStyle() || s instanceof AreaElement) {
277 hasIndependentElemStyle = true;
278 break;
279 }
280 }
281 if (!hasIndependentElemStyle && !multipolygon.getOuterWays().isEmpty()) {
282 Color mpColor = null;
283 StyleElementList mpElemStyles = null;
284 synchronized (ref) {
285 mpElemStyles = get(ref, scale, nc);
286 }
287 for (StyleElement mpS : mpElemStyles) {
288 if (mpS instanceof AreaElement) {
289 mpColor = ((AreaElement) mpS).color;
290 break;
291 }
292 }
293 p.a = new StyleElementList(p.a, LineElement.createSimpleLineStyle(mpColor, true));
294 }
295 return p;
296 }
297 }
298 return p;
299 } else if (osm instanceof Relation) {
300 Pair<StyleElementList, Range> p = generateStyles(osm, scale, true);
301 if (drawMultipolygon && ((Relation) osm).isMultipolygon()) {
302 if (!Utils.exists(p.a, AreaElement.class) && Main.pref.getBoolean("multipolygon.deprecated.outerstyle", true)) {
303 // look at outer ways to find area style
304 Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, (Relation) osm);
305 for (Way w : multipolygon.getOuterWays()) {
306 Pair<StyleElementList, Range> wayStyles = generateStyles(w, scale, false);
307 p.b = Range.cut(p.b, wayStyles.b);
308 StyleElement area = Utils.find(wayStyles.a, AreaElement.class);
309 if (area != null) {
310 p.a = new StyleElementList(p.a, area);
311 break;
312 }
313 }
314 }
315 }
316 return p;
317 }
318 return null;
319 }
320
321 /**
322 * Create the list of styles and its valid scale range for one primitive.
323 *
324 * Loops over the list of style sources, to generate the map of properties.
325 * From these properties, it generates the different types of styles.
326 *
327 * @param osm the primitive to create styles for
328 * @param scale the scale (in meters per 100 px), must be &gt; 0
329 * @param pretendWayIsClosed For styles that require the way to be closed,
330 * we pretend it is. This is useful for generating area styles from the (segmented)
331 * outer ways of a multipolygon.
332 * @return the generated styles and the valid range as a pair
333 */
334 public Pair<StyleElementList, Range> generateStyles(OsmPrimitive osm, double scale, boolean pretendWayIsClosed) {
335
336 List<StyleElement> sl = new ArrayList<>();
337 MultiCascade mc = new MultiCascade();
338 Environment env = new Environment(osm, mc, null, null);
339
340 for (StyleSource s : styleSources) {
341 if (s.active) {
342 s.apply(mc, osm, scale, pretendWayIsClosed);
343 }
344 }
345
346 for (Entry<String, Cascade> e : mc.getLayers()) {
347 if ("*".equals(e.getKey())) {
348 continue;
349 }
350 env.layer = e.getKey();
351 if (osm instanceof Way) {
352 addIfNotNull(sl, AreaElement.create(env));
353 addIfNotNull(sl, RepeatImageElement.create(env));
354 addIfNotNull(sl, LineElement.createLine(env));
355 addIfNotNull(sl, LineElement.createLeftCasing(env));
356 addIfNotNull(sl, LineElement.createRightCasing(env));
357 addIfNotNull(sl, LineElement.createCasing(env));
358 addIfNotNull(sl, LineTextElement.create(env));
359 } else if (osm instanceof Node) {
360 NodeElement nodeStyle = NodeElement.create(env);
361 if (nodeStyle != null) {
362 sl.add(nodeStyle);
363 addIfNotNull(sl, BoxTextElement.create(env, nodeStyle.getBoxProvider()));
364 } else {
365 addIfNotNull(sl, BoxTextElement.create(env, NodeElement.SIMPLE_NODE_ELEMSTYLE_BOXPROVIDER));
366 }
367 } else if (osm instanceof Relation) {
368 if (((Relation) osm).isMultipolygon()) {
369 addIfNotNull(sl, AreaElement.create(env));
370 addIfNotNull(sl, RepeatImageElement.create(env));
371 addIfNotNull(sl, LineElement.createLine(env));
372 addIfNotNull(sl, LineElement.createCasing(env));
373 addIfNotNull(sl, LineTextElement.create(env));
374 } else if ("restriction".equals(osm.get("type"))) {
375 addIfNotNull(sl, NodeElement.create(env));
376 }
377 }
378 }
379 return new Pair<>(new StyleElementList(sl), mc.range);
380 }
381
382 private static <T> void addIfNotNull(List<T> list, T obj) {
383 if (obj != null) {
384 list.add(obj);
385 }
386 }
387
388 /**
389 * Draw a default node symbol for nodes that have no style?
390 * @return {@code true} if default node symbol must be drawn
391 */
392 private boolean isDefaultNodes() {
393 if (defaultNodesIdx == cacheIdx)
394 return defaultNodes;
395 defaultNodes = fromCanvas("default-points", Boolean.TRUE, Boolean.class);
396 defaultNodesIdx = cacheIdx;
397 return defaultNodes;
398 }
399
400 /**
401 * Draw a default line for ways that do not have an own line style?
402 * @return {@code true} if default line must be drawn
403 */
404 private boolean isDefaultLines() {
405 if (defaultLinesIdx == cacheIdx)
406 return defaultLines;
407 defaultLines = fromCanvas("default-lines", Boolean.TRUE, Boolean.class);
408 defaultLinesIdx = cacheIdx;
409 return defaultLines;
410 }
411
412 private <T> T fromCanvas(String key, T def, Class<T> c) {
413 MultiCascade mc = new MultiCascade();
414 Relation r = new Relation();
415 r.put("#canvas", "query");
416
417 for (StyleSource s : styleSources) {
418 if (s.active) {
419 s.apply(mc, r, 1, false);
420 }
421 }
422 return mc.getCascade("default").get(key, def, c);
423 }
424
425 public boolean isDrawMultipolygon() {
426 return drawMultipolygon;
427 }
428
429 public void setDrawMultipolygon(boolean drawMultipolygon) {
430 this.drawMultipolygon = drawMultipolygon;
431 }
432
433 /**
434 * remove all style sources; only accessed from MapPaintStyles
435 */
436 void clear() {
437 styleSources.clear();
438 }
439
440 /**
441 * add a style source; only accessed from MapPaintStyles
442 * @param style style source to add
443 */
444 void add(StyleSource style) {
445 styleSources.add(style);
446 }
447
448 /**
449 * set the style sources; only accessed from MapPaintStyles
450 * @param sources new style sources
451 */
452 void setStyleSources(Collection<StyleSource> sources) {
453 styleSources.clear();
454 styleSources.addAll(sources);
455 }
456
457 /**
458 * Returns the first AreaElement for a given primitive.
459 * @param p the OSM primitive
460 * @param pretendWayIsClosed For styles that require the way to be closed,
461 * we pretend it is. This is useful for generating area styles from the (segmented)
462 * outer ways of a multipolygon.
463 * @return first AreaElement found or {@code null}.
464 */
465 public static AreaElement getAreaElemStyle(OsmPrimitive p, boolean pretendWayIsClosed) {
466 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
467 try {
468 if (MapPaintStyles.getStyles() == null)
469 return null;
470 for (StyleElement s : MapPaintStyles.getStyles().generateStyles(p, 1.0, pretendWayIsClosed).a) {
471 if (s instanceof AreaElement)
472 return (AreaElement) s;
473 }
474 return null;
475 } finally {
476 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
477 }
478 }
479
480 /**
481 * Determines whether primitive has an AreaElement.
482 * @param p the OSM primitive
483 * @param pretendWayIsClosed For styles that require the way to be closed,
484 * we pretend it is. This is useful for generating area styles from the (segmented)
485 * outer ways of a multipolygon.
486 * @return {@code true} if primitive has an AreaElement
487 */
488 public static boolean hasAreaElemStyle(OsmPrimitive p, boolean pretendWayIsClosed) {
489 return getAreaElemStyle(p, pretendWayIsClosed) != null;
490 }
491
492 /**
493 * Determines whether primitive has <b>only</b> an AreaElement.
494 * @param p the OSM primitive
495 * @return {@code true} if primitive has only an AreaElement
496 * @since 7486
497 */
498 public static boolean hasOnlyAreaElemStyle(OsmPrimitive p) {
499 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
500 try {
501 if (MapPaintStyles.getStyles() == null)
502 return false;
503 StyleElementList styles = MapPaintStyles.getStyles().generateStyles(p, 1.0, false).a;
504 if (styles.isEmpty()) {
505 return false;
506 }
507 for (StyleElement s : styles) {
508 if (!(s instanceof AreaElement)) {
509 return false;
510 }
511 }
512 return true;
513 } finally {
514 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
515 }
516 }
517
518 /**
519 * Looks up a preference value and ensures the style cache is invalidated
520 * as soon as this preference value is changed by the user.
521 *
522 * In addition, it adds an intermediate cache for the preference values,
523 * as frequent preference lookup (using <code>Main.pref.get()</code>) for
524 * each primitive can be slow during rendering.
525 *
526 * @param key preference key
527 * @param def default value
528 * @return the corresponding preference value
529 * @see org.openstreetmap.josm.data.Preferences#get(String, String)
530 */
531 public String getPreferenceCached(String key, String def) {
532 String res;
533 if (preferenceCache.containsKey(key)) {
534 res = preferenceCache.get(key);
535 } else {
536 res = Main.pref.get(key, null);
537 preferenceCache.put(key, res);
538 }
539 return res != null ? res : def;
540 }
541
542 @Override
543 public void preferenceChanged(PreferenceChangeEvent e) {
544 if (preferenceCache.containsKey(e.getKey())) {
545 clearCached();
546 }
547 }
548}
Note: See TracBrowser for help on using the repository browser.