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

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

spotbugs - IC_INIT_CIRCULARITY

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