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