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