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

Last change on this file since 12378 was 12026, checked in by stoecker, 7 years ago

see #10529 - drop code completely

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