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;
012
013import org.openstreetmap.josm.Main;
014import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
015import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
016import org.openstreetmap.josm.data.osm.Node;
017import org.openstreetmap.josm.data.osm.OsmPrimitive;
018import org.openstreetmap.josm.data.osm.Relation;
019import org.openstreetmap.josm.data.osm.Way;
020import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
021import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
022import org.openstreetmap.josm.gui.NavigatableComponent;
023import org.openstreetmap.josm.gui.mappaint.DividedScale.RangeViolatedError;
024import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
025import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement;
026import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement;
027import org.openstreetmap.josm.gui.mappaint.styleelement.LineElement;
028import org.openstreetmap.josm.gui.mappaint.styleelement.LineTextElement;
029import org.openstreetmap.josm.gui.mappaint.styleelement.NodeElement;
030import org.openstreetmap.josm.gui.mappaint.styleelement.RepeatImageElement;
031import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement;
032import org.openstreetmap.josm.gui.mappaint.styleelement.TextLabel;
033import org.openstreetmap.josm.gui.util.GuiHelper;
034import org.openstreetmap.josm.tools.Pair;
035import org.openstreetmap.josm.tools.Utils;
036
037public class ElemStyles implements PreferenceChangedListener {
038    private final List<StyleSource> styleSources;
039    private boolean drawMultipolygon;
040
041    private int cacheIdx = 1;
042
043    private boolean defaultNodes, defaultLines;
044    private int defaultNodesIdx, defaultLinesIdx;
045
046    private final Map<String, String> preferenceCache = new HashMap<>();
047
048    /**
049     * Constructs a new {@code ElemStyles}.
050     */
051    public ElemStyles() {
052        styleSources = new ArrayList<>();
053        Main.pref.addPreferenceChangeListener(this);
054    }
055
056    /**
057     * Clear the style cache for all primitives of all DataSets.
058     */
059    public void clearCached() {
060        // run in EDT to make sure this isn't called during rendering run
061        GuiHelper.runInEDT(new Runnable() {
062            @Override
063            public void run() {
064                cacheIdx++;
065                preferenceCache.clear();
066            }
067        });
068    }
069
070    public List<StyleSource> getStyleSources() {
071        return Collections.<StyleSource>unmodifiableList(styleSources);
072    }
073
074    /**
075     * Create the list of styles for one primitive.
076     *
077     * @param osm the primitive
078     * @param scale the scale (in meters per 100 pixel)
079     * @param nc display component
080     * @return list of styles
081     */
082    public StyleElementList get(OsmPrimitive osm, double scale, NavigatableComponent nc) {
083        return getStyleCacheWithRange(osm, scale, nc).a;
084    }
085
086    /**
087     * Create the list of styles and its valid scale range for one primitive.
088     *
089     * Automatically adds default styles in case no proper style was found.
090     * Uses the cache, if possible, and saves the results to the cache.
091     * @param osm OSM primitive
092     * @param scale scale
093     * @param nc navigatable component
094     * @return pair containing style list and range
095     */
096    public Pair<StyleElementList, Range> getStyleCacheWithRange(OsmPrimitive osm, double scale, NavigatableComponent nc) {
097        if (osm.mappaintStyle == null || osm.mappaintCacheIdx != cacheIdx || scale <= 0) {
098            osm.mappaintStyle = StyleCache.EMPTY_STYLECACHE;
099        } 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}