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.List;
009import java.util.Map.Entry;
010
011import org.openstreetmap.josm.Main;
012import org.openstreetmap.josm.data.osm.Node;
013import org.openstreetmap.josm.data.osm.OsmPrimitive;
014import org.openstreetmap.josm.data.osm.Relation;
015import org.openstreetmap.josm.data.osm.Way;
016import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
017import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
018import org.openstreetmap.josm.gui.NavigatableComponent;
019import org.openstreetmap.josm.gui.mappaint.StyleCache.StyleList;
020import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
021import org.openstreetmap.josm.gui.util.GuiHelper;
022import org.openstreetmap.josm.tools.Pair;
023import org.openstreetmap.josm.tools.Utils;
024
025public class ElemStyles {
026    private List<StyleSource> styleSources;
027    private boolean drawMultipolygon;
028
029    private int cacheIdx = 1;
030
031    private boolean defaultNodes, defaultLines;
032    private int defaultNodesIdx, defaultLinesIdx;
033
034    /**
035     * Constructs a new {@code ElemStyles}.
036     */
037    public ElemStyles() {
038        styleSources = new ArrayList<>();
039    }
040
041    /**
042     * Clear the style cache for all primitives of all DataSets.
043     */
044    public void clearCached() {
045        // run in EDT to make sure this isn't called during rendering run
046        GuiHelper.runInEDT(new Runnable() {
047            @Override
048            public void run() {
049                cacheIdx++;
050            }
051        });
052    }
053
054    public List<StyleSource> getStyleSources() {
055        return Collections.<StyleSource>unmodifiableList(styleSources);
056    }
057
058    /**
059     * Create the list of styles for one primitive.
060     *
061     * @param osm the primitive
062     * @param scale the scale (in meters per 100 pixel)
063     * @param nc display component
064     * @return list of styles
065     */
066    public StyleList get(OsmPrimitive osm, double scale, NavigatableComponent nc) {
067        return getStyleCacheWithRange(osm, scale, nc).a;
068    }
069
070    /**
071     * Create the list of styles and its valid scale range for one primitive.
072     *
073     * Automatically adds default styles in case no proper style was found.
074     * Uses the cache, if possible, and saves the results to the cache.
075     */
076    public Pair<StyleList, Range> getStyleCacheWithRange(OsmPrimitive osm, double scale, NavigatableComponent nc) {
077        if (osm.mappaintStyle == null || osm.mappaintCacheIdx != cacheIdx || scale <= 0) {
078            osm.mappaintStyle = StyleCache.EMPTY_STYLECACHE;
079        } else {
080            Pair<StyleList, Range> lst = osm.mappaintStyle.getWithRange(scale);
081            if (lst.a != null)
082                return lst;
083        }
084        Pair<StyleList, Range> p = getImpl(osm, scale, nc);
085        if (osm instanceof Node && isDefaultNodes()) {
086            if (p.a.isEmpty()) {
087                if (TextElement.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) {
088                    p.a = NodeElemStyle.DEFAULT_NODE_STYLELIST_TEXT;
089                } else {
090                    p.a = NodeElemStyle.DEFAULT_NODE_STYLELIST;
091                }
092            } else {
093                boolean hasNonModifier = false;
094                boolean hasText = false;
095                for (ElemStyle s : p.a) {
096                    if (s instanceof BoxTextElemStyle) {
097                        hasText = true;
098                    } else {
099                        if (!s.isModifier) {
100                            hasNonModifier = true;
101                        }
102                    }
103                }
104                if (!hasNonModifier) {
105                    p.a = new StyleList(p.a, NodeElemStyle.SIMPLE_NODE_ELEMSTYLE);
106                    if (!hasText) {
107                        if (TextElement.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) {
108                            p.a = new StyleList(p.a, BoxTextElemStyle.SIMPLE_NODE_TEXT_ELEMSTYLE);
109                        }
110                    }
111                }
112            }
113        } else if (osm instanceof Way && isDefaultLines()) {
114            boolean hasProperLineStyle = false;
115            for (ElemStyle s : p.a) {
116                if (s.isProperLineStyle()) {
117                    hasProperLineStyle = true;
118                    break;
119                }
120            }
121            if (!hasProperLineStyle) {
122                AreaElemStyle area = Utils.find(p.a, AreaElemStyle.class);
123                LineElemStyle line = area == null ? LineElemStyle.UNTAGGED_WAY : LineElemStyle.createSimpleLineStyle(area.color, true);
124                p.a = new StyleList(p.a, line);
125            }
126        }
127        StyleCache style = osm.mappaintStyle != null ? osm.mappaintStyle : StyleCache.EMPTY_STYLECACHE;
128        try {
129            osm.mappaintStyle = style.put(p.a, p.b);
130        } catch (StyleCache.RangeViolatedError e) {
131            throw new AssertionError("Range violated: " + e.getMessage()
132                    + " (object: " + osm.getPrimitiveId() + ", current style: "+osm.mappaintStyle
133                    + ", scale: " + scale + ", new stylelist: " + p.a + ", new range: " + p.b + ')', e);
134        }
135        osm.mappaintCacheIdx = cacheIdx;
136        return p;
137    }
138
139    /**
140     * Create the list of styles and its valid scale range for one primitive.
141     *
142     * This method does multipolygon handling.
143     *
144     * There are different tagging styles for multipolygons, that have to be respected:
145     * - tags on the relation
146     * - tags on the outer way (deprecated)
147     *
148     * If the primitive is a way, look for multipolygon parents. In case it
149     * is indeed member of some multipolygon as role "outer", all area styles
150     * are removed. (They apply to the multipolygon area.)
151     * Outer ways can have their own independent line styles, e.g. a road as
152     * boundary of a forest. Otherwise, in case, the way does not have an
153     * independent line style, take a line style from the multipolygon.
154     * If the multipolygon does not have a line style either, at least create a
155     * default line style from the color of the area.
156     *
157     * Now consider the case that the way is not an outer way of any multipolygon,
158     * but is member of a multipolygon as "inner".
159     * First, the style list is regenerated, considering only tags of this way.
160     * Then check, if the way describes something in its own right. (linear feature
161     * or area) If not, add a default line style from the area color of the multipolygon.
162     *
163     */
164    private Pair<StyleList, Range> getImpl(OsmPrimitive osm, double scale, NavigatableComponent nc) {
165        if (osm instanceof Node)
166            return generateStyles(osm, scale, false);
167        else if (osm instanceof Way) {
168            Pair<StyleList, Range> p = generateStyles(osm, scale, false);
169
170            boolean isOuterWayOfSomeMP = false;
171            Color wayColor = null;
172
173            for (OsmPrimitive referrer : osm.getReferrers()) {
174                Relation r = (Relation) referrer;
175                if (!drawMultipolygon || !r.isMultipolygon()  || !r.isUsable()) {
176                    continue;
177                }
178                Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, r);
179
180                if (multipolygon.getOuterWays().contains(osm)) {
181                    boolean hasIndependentLineStyle = false;
182                    if (!isOuterWayOfSomeMP) { // do this only one time
183                        List<ElemStyle> tmp = new ArrayList<>(p.a.size());
184                        for (ElemStyle s : p.a) {
185                            if (s instanceof AreaElemStyle) {
186                                wayColor = ((AreaElemStyle) s).color;
187                            } else {
188                                tmp.add(s);
189                                if (s.isProperLineStyle()) {
190                                    hasIndependentLineStyle = true;
191                                }
192                            }
193                        }
194                        p.a = new StyleList(tmp);
195                        isOuterWayOfSomeMP = true;
196                    }
197
198                    if (!hasIndependentLineStyle) {
199                        Pair<StyleList, Range> mpElemStyles;
200                        synchronized (r) {
201                            mpElemStyles = getStyleCacheWithRange(r, scale, nc);
202                        }
203                        ElemStyle mpLine = null;
204                        for (ElemStyle s : mpElemStyles.a) {
205                            if (s.isProperLineStyle()) {
206                                mpLine = s;
207                                break;
208                            }
209                        }
210                        p.b = Range.cut(p.b, mpElemStyles.b);
211                        if (mpLine != null) {
212                            p.a = new StyleList(p.a, mpLine);
213                            break;
214                        } else if (wayColor == null && isDefaultLines()) {
215                            AreaElemStyle mpArea = Utils.find(mpElemStyles.a, AreaElemStyle.class);
216                            if (mpArea != null) {
217                                wayColor = mpArea.color;
218                            }
219                        }
220                    }
221                }
222            }
223            if (isOuterWayOfSomeMP) {
224                if (isDefaultLines()) {
225                    boolean hasLineStyle = false;
226                    for (ElemStyle s : p.a) {
227                        if (s.isProperLineStyle()) {
228                            hasLineStyle = true;
229                            break;
230                        }
231                    }
232                    if (!hasLineStyle) {
233                        p.a = new StyleList(p.a, LineElemStyle.createSimpleLineStyle(wayColor, true));
234                    }
235                }
236                return p;
237            }
238
239            if (!isDefaultLines()) return p;
240
241            for (OsmPrimitive referrer : osm.getReferrers()) {
242                Relation ref = (Relation) referrer;
243                if (!drawMultipolygon || !ref.isMultipolygon() || !ref.isUsable()) {
244                    continue;
245                }
246                final Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, ref);
247
248                if (multipolygon.getInnerWays().contains(osm)) {
249                    p = generateStyles(osm, scale, false);
250                    boolean hasIndependentElemStyle = false;
251                    for (ElemStyle s : p.a) {
252                        if (s.isProperLineStyle() || s instanceof AreaElemStyle) {
253                            hasIndependentElemStyle = true;
254                            break;
255                        }
256                    }
257                    if (!hasIndependentElemStyle && !multipolygon.getOuterWays().isEmpty()) {
258                        Color mpColor = null;
259                        StyleList mpElemStyles = null;
260                        synchronized (ref) {
261                            mpElemStyles = get(ref, scale, nc);
262                        }
263                        for (ElemStyle mpS : mpElemStyles) {
264                            if (mpS instanceof AreaElemStyle) {
265                                mpColor = ((AreaElemStyle) mpS).color;
266                                break;
267                            }
268                        }
269                        p.a = new StyleList(p.a, LineElemStyle.createSimpleLineStyle(mpColor, true));
270                    }
271                    return p;
272                }
273            }
274            return p;
275        } else if (osm instanceof Relation) {
276            Pair<StyleList, Range> p = generateStyles(osm, scale, true);
277            if (drawMultipolygon && ((Relation) osm).isMultipolygon()) {
278                if (!Utils.exists(p.a, AreaElemStyle.class) && Main.pref.getBoolean("multipolygon.deprecated.outerstyle", true)) {
279                    // look at outer ways to find area style
280                    Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, (Relation) osm);
281                    for (Way w : multipolygon.getOuterWays()) {
282                        Pair<StyleList, Range> wayStyles = generateStyles(w, scale, false);
283                        p.b = Range.cut(p.b, wayStyles.b);
284                        ElemStyle area = Utils.find(wayStyles.a, AreaElemStyle.class);
285                        if (area != null) {
286                            p.a = new StyleList(p.a, area);
287                            break;
288                        }
289                    }
290                }
291            }
292            return p;
293        }
294        return null;
295    }
296
297    /**
298     * Create the list of styles and its valid scale range for one primitive.
299     *
300     * Loops over the list of style sources, to generate the map of properties.
301     * From these properties, it generates the different types of styles.
302     *
303     * @param osm the primitive to create styles for
304     * @param scale the scale (in meters per 100 px), must be &gt; 0
305     * @param pretendWayIsClosed For styles that require the way to be closed,
306     * we pretend it is. This is useful for generating area styles from the (segmented)
307     * outer ways of a multipolygon.
308     * @return the generated styles and the valid range as a pair
309     */
310    public Pair<StyleList, Range> generateStyles(OsmPrimitive osm, double scale, boolean pretendWayIsClosed) {
311
312        List<ElemStyle> sl = new ArrayList<>();
313        MultiCascade mc = new MultiCascade();
314        Environment env = new Environment(osm, mc, null, null);
315
316        for (StyleSource s : styleSources) {
317            if (s.active) {
318                s.apply(mc, osm, scale, pretendWayIsClosed);
319            }
320        }
321
322        for (Entry<String, Cascade> e : mc.getLayers()) {
323            if ("*".equals(e.getKey())) {
324                continue;
325            }
326            env.layer = e.getKey();
327            if (osm instanceof Way) {
328                addIfNotNull(sl, AreaElemStyle.create(env));
329                addIfNotNull(sl, RepeatImageElemStyle.create(env));
330                addIfNotNull(sl, LineElemStyle.createLine(env));
331                addIfNotNull(sl, LineElemStyle.createLeftCasing(env));
332                addIfNotNull(sl, LineElemStyle.createRightCasing(env));
333                addIfNotNull(sl, LineElemStyle.createCasing(env));
334                addIfNotNull(sl, LineTextElemStyle.create(env));
335            } else if (osm instanceof Node) {
336                NodeElemStyle nodeStyle = NodeElemStyle.create(env);
337                if (nodeStyle != null) {
338                    sl.add(nodeStyle);
339                    addIfNotNull(sl, BoxTextElemStyle.create(env, nodeStyle.getBoxProvider()));
340                } else {
341                    addIfNotNull(sl, BoxTextElemStyle.create(env, NodeElemStyle.SIMPLE_NODE_ELEMSTYLE_BOXPROVIDER));
342                }
343            } else if (osm instanceof Relation) {
344                if (((Relation) osm).isMultipolygon()) {
345                    addIfNotNull(sl, AreaElemStyle.create(env));
346                    addIfNotNull(sl, RepeatImageElemStyle.create(env));
347                    addIfNotNull(sl, LineElemStyle.createLine(env));
348                    addIfNotNull(sl, LineElemStyle.createCasing(env));
349                    addIfNotNull(sl, LineTextElemStyle.create(env));
350                } else if ("restriction".equals(osm.get("type"))) {
351                    addIfNotNull(sl, NodeElemStyle.create(env));
352                }
353            }
354        }
355        return new Pair<>(new StyleList(sl), mc.range);
356    }
357
358    private static <T> void addIfNotNull(List<T> list, T obj) {
359        if (obj != null) {
360            list.add(obj);
361        }
362    }
363
364    /**
365     * Draw a default node symbol for nodes that have no style?
366     */
367    private boolean isDefaultNodes() {
368        if (defaultNodesIdx == cacheIdx)
369            return defaultNodes;
370        defaultNodes = fromCanvas("default-points", Boolean.TRUE, Boolean.class);
371        defaultNodesIdx = cacheIdx;
372        return defaultNodes;
373    }
374
375    /**
376     * Draw a default line for ways that do not have an own line style?
377     */
378    private boolean isDefaultLines() {
379        if (defaultLinesIdx == cacheIdx)
380            return defaultLines;
381        defaultLines = fromCanvas("default-lines", Boolean.TRUE, Boolean.class);
382        defaultLinesIdx = cacheIdx;
383        return defaultLines;
384    }
385
386    private <T> T fromCanvas(String key, T def, Class<T> c) {
387        MultiCascade mc = new MultiCascade();
388        Relation r = new Relation();
389        r.put("#canvas", "query");
390
391        for (StyleSource s : styleSources) {
392            if (s.active) {
393                s.apply(mc, r, 1, false);
394            }
395        }
396        return mc.getCascade("default").get(key, def, c);
397    }
398
399    public boolean isDrawMultipolygon() {
400        return drawMultipolygon;
401    }
402
403    public void setDrawMultipolygon(boolean drawMultipolygon) {
404        this.drawMultipolygon = drawMultipolygon;
405    }
406
407    /**
408     * remove all style sources; only accessed from MapPaintStyles
409     */
410    void clear() {
411        styleSources.clear();
412    }
413
414    /**
415     * add a style source; only accessed from MapPaintStyles
416     */
417    void add(StyleSource style) {
418        styleSources.add(style);
419    }
420
421    /**
422     * set the style sources; only accessed from MapPaintStyles
423     */
424    void setStyleSources(Collection<StyleSource> sources) {
425        styleSources.clear();
426        styleSources.addAll(sources);
427    }
428
429    /**
430     * Returns the first AreaElemStyle for a given primitive.
431     * @param p the OSM primitive
432     * @param pretendWayIsClosed For styles that require the way to be closed,
433     * we pretend it is. This is useful for generating area styles from the (segmented)
434     * outer ways of a multipolygon.
435     * @return first AreaElemStyle found or {@code null}.
436     */
437    public static AreaElemStyle getAreaElemStyle(OsmPrimitive p, boolean pretendWayIsClosed) {
438        MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
439        try {
440            if (MapPaintStyles.getStyles() == null)
441                return null;
442            for (ElemStyle s : MapPaintStyles.getStyles().generateStyles(p, 1.0, pretendWayIsClosed).a) {
443                if (s instanceof AreaElemStyle)
444                    return (AreaElemStyle) s;
445            }
446            return null;
447        } finally {
448            MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
449        }
450    }
451
452    /**
453     * Determines whether primitive has an AreaElemStyle.
454     * @param p the OSM primitive
455     * @param pretendWayIsClosed For styles that require the way to be closed,
456     * we pretend it is. This is useful for generating area styles from the (segmented)
457     * outer ways of a multipolygon.
458     * @return {@code true} if primitive has an AreaElemStyle
459     */
460    public static boolean hasAreaElemStyle(OsmPrimitive p, boolean pretendWayIsClosed) {
461        return getAreaElemStyle(p, pretendWayIsClosed) != null;
462    }
463
464    /**
465     * Determines whether primitive has <b>only</b> an AreaElemStyle.
466     * @param p the OSM primitive
467     * @return {@code true} if primitive has only an AreaElemStyle
468     * @since 7486
469     */
470    public static boolean hasOnlyAreaElemStyle(OsmPrimitive p) {
471        MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
472        try {
473            if (MapPaintStyles.getStyles() == null)
474                return false;
475            StyleList styles = MapPaintStyles.getStyles().generateStyles(p, 1.0, false).a;
476            if (styles.isEmpty()) {
477                return false;
478            }
479            for (ElemStyle s : styles) {
480                if (!(s instanceof AreaElemStyle)) {
481                    return false;
482                }
483            }
484            return true;
485        } finally {
486            MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
487        }
488    }
489}