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.Iterator;
009import java.util.List;
010import java.util.Map.Entry;
011
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.util.GuiHelper;
021import org.openstreetmap.josm.tools.Pair;
022import org.openstreetmap.josm.tools.Utils;
023
024public class ElemStyles {
025    private List<StyleSource> styleSources;
026    private boolean drawMultipolygon;
027
028    private int cacheIdx = 1;
029
030    private boolean defaultNodes, defaultLines;
031    private int defaultNodesIdx, defaultLinesIdx;
032
033    /**
034     * Constructs a new {@code ElemStyles}.
035     */
036    public ElemStyles() {
037        styleSources = new ArrayList<>();
038    }
039
040    /**
041     * Clear the style cache for all primitives of all DataSets.
042     */
043    public void clearCached() {
044        // run in EDT to make sure this isn't called during rendering run 
045        // {@link org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer#render}
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. object: " + osm.getPrimitiveId() + ", current style: "+osm.mappaintStyle
132                    + ", scale: " + scale + ", new stylelist: " + p.a + ", new range: " + p.b, e);
133        }
134        osm.mappaintCacheIdx = cacheIdx;
135        return p;
136    }
137
138    /**
139     * Create the list of styles and its valid scale range for one primitive.
140     *
141     * This method does multipolygon handling.
142     *
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
147     * - tags on both, the outer and the inner way (very old style)
148     *
149     * If the primitive is a way, look for multipolygon parents. In case it
150     * is indeed member of some multipolygon as role "outer", all area styles
151     * are removed. (They apply to the multipolygon area.)
152     * Outer ways can have their own independent line styles, e.g. a road as
153     * boundary of a forest. Otherwise, in case, the way does not have an
154     * independent line style, take a line style from the multipolygon.
155     * If the multipolygon does not have a line style either, at least create a
156     * default line style from the color of the area.
157     *
158     * Now consider the case that the way is not an outer way of any multipolygon,
159     * but is member of a multipolygon as "inner".
160     * First, the style list is regenerated, considering only tags of this way
161     * minus the tags of outer way of the multipolygon (to care for the "very
162     * old style").
163     * Then check, if the way describes something in its own right. (linear feature
164     * or area) If not, add a default line style from the area color of the multipolygon.
165     *
166     */
167    private Pair<StyleList, Range> getImpl(OsmPrimitive osm, double scale, NavigatableComponent nc) {
168        if (osm instanceof Node)
169            return generateStyles(osm, scale, null, false);
170        else if (osm instanceof Way)
171        {
172            Pair<StyleList, Range> p = generateStyles(osm, scale, null, false);
173
174            boolean isOuterWayOfSomeMP = false;
175            Color wayColor = null;
176
177            for (OsmPrimitive referrer : osm.getReferrers()) {
178                Relation r = (Relation) referrer;
179                if (!drawMultipolygon || !r.isMultipolygon()  || !r.isUsable()) {
180                    continue;
181                }
182                Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, r);
183
184                if (multipolygon.getOuterWays().contains(osm)) {
185                    boolean hasIndependentLineStyle = false;
186                    if (!isOuterWayOfSomeMP) { // do this only one time
187                        List<ElemStyle> tmp = new ArrayList<>(p.a.size());
188                        for (ElemStyle s : p.a) {
189                            if (s instanceof AreaElemStyle) {
190                                wayColor = ((AreaElemStyle) s).color;
191                            } else {
192                                tmp.add(s);
193                                if (s.isProperLineStyle()) {
194                                    hasIndependentLineStyle = true;
195                                }
196                            }
197                        }
198                        p.a = new StyleList(tmp);
199                        isOuterWayOfSomeMP = true;
200                    }
201
202                    if (!hasIndependentLineStyle) {
203                        Pair<StyleList, Range> mpElemStyles;
204                        synchronized(r) {
205                            mpElemStyles = getStyleCacheWithRange(r, scale, nc);
206                        }
207                        ElemStyle mpLine = null;
208                        for (ElemStyle s : mpElemStyles.a) {
209                            if (s.isProperLineStyle()) {
210                                mpLine = s;
211                                break;
212                            }
213                        }
214                        p.b = Range.cut(p.b, mpElemStyles.b);
215                        if (mpLine != null) {
216                            p.a = new StyleList(p.a, mpLine);
217                            break;
218                        } else if (wayColor == null && isDefaultLines()) {
219                            AreaElemStyle mpArea = Utils.find(mpElemStyles.a, AreaElemStyle.class);
220                            if (mpArea != null) {
221                                wayColor = mpArea.color;
222                            }
223                        }
224                    }
225                }
226            }
227            if (isOuterWayOfSomeMP) {
228                if (isDefaultLines()) {
229                    boolean hasLineStyle = false;
230                    for (ElemStyle s : p.a) {
231                        if (s.isProperLineStyle()) {
232                            hasLineStyle = true;
233                            break;
234                        }
235                    }
236                    if (!hasLineStyle) {
237                        p.a = new StyleList(p.a, LineElemStyle.createSimpleLineStyle(wayColor, true));
238                    }
239                }
240                return p;
241            }
242
243            if (!isDefaultLines()) return p;
244
245            for (OsmPrimitive referrer : osm.getReferrers()) {
246                Relation ref = (Relation) referrer;
247                if (!drawMultipolygon || !ref.isMultipolygon() || !ref.isUsable()) {
248                    continue;
249                }
250                final Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, ref);
251
252                if (multipolygon.getInnerWays().contains(osm)) {
253                    Iterator<Way> it = multipolygon.getOuterWays().iterator();
254                    p = generateStyles(osm, scale, it.hasNext() ? it.next() : null, false);
255                    boolean hasIndependentElemStyle = false;
256                    for (ElemStyle s : p.a) {
257                        if (s.isProperLineStyle() || s instanceof AreaElemStyle) {
258                            hasIndependentElemStyle = true;
259                            break;
260                        }
261                    }
262                    if (!hasIndependentElemStyle && !multipolygon.getOuterWays().isEmpty()) {
263                        Color mpColor = null;
264                        StyleList mpElemStyles = null;
265                        synchronized (ref) {
266                            mpElemStyles = get(ref, scale, nc);
267                        }
268                        for (ElemStyle mpS : mpElemStyles) {
269                            if (mpS instanceof AreaElemStyle) {
270                                mpColor = ((AreaElemStyle) mpS).color;
271                                break;
272                            }
273                        }
274                        p.a = new StyleList(p.a, LineElemStyle.createSimpleLineStyle(mpColor, true));
275                    }
276                    return p;
277                }
278            }
279            return p;
280        }
281        else if (osm instanceof Relation)
282        {
283            Pair<StyleList, Range> p = generateStyles(osm, scale, null, true);
284            if (drawMultipolygon && ((Relation)osm).isMultipolygon()) {
285                if (!Utils.exists(p.a, AreaElemStyle.class)) {
286                    // look at outer ways to find area style
287                    Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, (Relation) osm);
288                    for (Way w : multipolygon.getOuterWays()) {
289                        Pair<StyleList, Range> wayStyles = generateStyles(w, scale, null, false);
290                        p.b = Range.cut(p.b, wayStyles.b);
291                        ElemStyle area = Utils.find(wayStyles.a, AreaElemStyle.class);
292                        if (area != null) {
293                            p.a = new StyleList(p.a, area);
294                            break;
295                        }
296                    }
297                }
298            }
299            return p;
300        }
301        return null;
302    }
303
304    /**
305     * Create the list of styles and its valid scale range for one primitive.
306     *
307     * Loops over the list of style sources, to generate the map of properties.
308     * From these properties, it generates the different types of styles.
309     *
310     * @param osm the primitive to create styles for
311     * @param scale the scale (in meters per 100 px), must be &gt; 0
312     * @param multipolyOuterWay support for a very old multipolygon tagging style
313     * where you add the tags both to the outer and the inner way.
314     * However, independent inner way style is also possible.
315     * @param pretendWayIsClosed For styles that require the way to be closed,
316     * we pretend it is. This is useful for generating area styles from the (segmented)
317     * outer ways of a multipolygon.
318     * @return the generated styles and the valid range as a pair
319     */
320    public Pair<StyleList, Range> generateStyles(OsmPrimitive osm, double scale, OsmPrimitive multipolyOuterWay, boolean pretendWayIsClosed) {
321
322        List<ElemStyle> sl = new ArrayList<>();
323        MultiCascade mc = new MultiCascade();
324        Environment env = new Environment(osm, mc, null, null);
325
326        for (StyleSource s : styleSources) {
327            if (s.active) {
328                s.apply(mc, osm, scale, multipolyOuterWay, pretendWayIsClosed);
329            }
330        }
331
332        for (Entry<String, Cascade> e : mc.getLayers()) {
333            if ("*".equals(e.getKey())) {
334                continue;
335            }
336            env.layer = e.getKey();
337            Cascade c = e.getValue();
338            if (osm instanceof Way) {
339                addIfNotNull(sl, AreaElemStyle.create(c));
340                addIfNotNull(sl, RepeatImageElemStyle.create(env));
341                addIfNotNull(sl, LineElemStyle.createLine(env));
342                addIfNotNull(sl, LineElemStyle.createLeftCasing(env));
343                addIfNotNull(sl, LineElemStyle.createRightCasing(env));
344                addIfNotNull(sl, LineElemStyle.createCasing(env));
345                addIfNotNull(sl, LineTextElemStyle.create(env));
346            } else if (osm instanceof Node) {
347                NodeElemStyle nodeStyle = NodeElemStyle.create(env);
348                if (nodeStyle != null) {
349                    sl.add(nodeStyle);
350                    addIfNotNull(sl, BoxTextElemStyle.create(env, nodeStyle.getBoxProvider()));
351                } else {
352                    addIfNotNull(sl, BoxTextElemStyle.create(env, NodeElemStyle.SIMPLE_NODE_ELEMSTYLE.getBoxProvider()));
353                }
354            } else if (osm instanceof Relation) {
355                if (((Relation)osm).isMultipolygon()) {
356                    addIfNotNull(sl, AreaElemStyle.create(c));
357                    addIfNotNull(sl, RepeatImageElemStyle.create(env));
358                    addIfNotNull(sl, LineElemStyle.createLine(env));
359                    addIfNotNull(sl, LineElemStyle.createCasing(env));
360                    addIfNotNull(sl, LineTextElemStyle.create(env));
361                } else if ("restriction".equals(osm.get("type"))) {
362                    addIfNotNull(sl, NodeElemStyle.create(env));
363                }
364            }
365        }
366        return new Pair<>(new StyleList(sl), mc.range);
367    }
368
369    private static <T> void addIfNotNull(List<T> list, T obj) {
370        if (obj != null) {
371            list.add(obj);
372        }
373    }
374
375    /**
376     * Draw a default node symbol for nodes that have no style?
377     */
378    private boolean isDefaultNodes() {
379        if (defaultNodesIdx == cacheIdx)
380            return defaultNodes;
381        defaultNodes = fromCanvas("default-points", true, Boolean.class);
382        defaultNodesIdx = cacheIdx;
383        return defaultNodes;
384    }
385
386    /**
387     * Draw a default line for ways that do not have an own line style?
388     */
389    private boolean isDefaultLines() {
390        if (defaultLinesIdx == cacheIdx)
391            return defaultLines;
392        defaultLines = fromCanvas("default-lines", true, Boolean.class);
393        defaultLinesIdx = cacheIdx;
394        return defaultLines;
395    }
396
397    private <T> T fromCanvas(String key, T def, Class<T> c) {
398        MultiCascade mc = new MultiCascade();
399        Relation r = new Relation();
400        r.put("#canvas", "query");
401
402        for (StyleSource s : styleSources) {
403            if (s.active) {
404                s.apply(mc, r, 1, null, false);
405            }
406        }
407        return mc.getCascade("default").get(key, def, c);
408    }
409
410    public boolean isDrawMultipolygon() {
411        return drawMultipolygon;
412    }
413
414    public void setDrawMultipolygon(boolean drawMultipolygon) {
415        this.drawMultipolygon = drawMultipolygon;
416    }
417
418    /**
419     * remove all style sources; only accessed from MapPaintStyles
420     */
421    void clear() {
422        styleSources.clear();
423    }
424
425    /**
426     * add a style source; only accessed from MapPaintStyles
427     */
428    void add(StyleSource style) {
429        styleSources.add(style);
430    }
431
432    /**
433     * set the style sources; only accessed from MapPaintStyles
434     */
435    void setStyleSources(Collection<StyleSource> sources) {
436        styleSources.clear();
437        styleSources.addAll(sources);
438    }
439
440    /**
441     * Returns the first AreaElemStyle for a given primitive.
442     * @param p the OSM primitive
443     * @param pretendWayIsClosed For styles that require the way to be closed,
444     * we pretend it is. This is useful for generating area styles from the (segmented)
445     * outer ways of a multipolygon.
446     * @return first AreaElemStyle found or {@code null}.
447     */
448    public static AreaElemStyle getAreaElemStyle(OsmPrimitive p, boolean pretendWayIsClosed) {
449        if (MapPaintStyles.getStyles() == null)
450            return null;
451        for (ElemStyle s : MapPaintStyles.getStyles().generateStyles(p, 1.0, null, pretendWayIsClosed).a) {
452            if (s instanceof AreaElemStyle)
453                return (AreaElemStyle) s;
454        }
455        return null;
456    }
457
458    /**
459     * Determines whether primitive has an AreaElemStyle.
460     * @param p the OSM primitive
461     * @param pretendWayIsClosed For styles that require the way to be closed,
462     * we pretend it is. This is useful for generating area styles from the (segmented)
463     * outer ways of a multipolygon.
464     * @return {@code true} if primitive has an AreaElemStyle
465     */
466    public static boolean hasAreaElemStyle(OsmPrimitive p, boolean pretendWayIsClosed) {
467        return getAreaElemStyle(p, pretendWayIsClosed) != null;
468    }
469}