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.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     *
145     * There are different tagging styles for multipolygons, that have to be respected:
146     * - tags on the relation
147     * - tags on the outer way
148     * - tags on both, the outer and the inner way (very old style)
149     *
150     * If the primitive is a way, look for multipolygon parents. In case it
151     * is indeed member of some multipolygon as role "outer", all area styles
152     * are removed. (They apply to the multipolygon area.)
153     * Outer ways can have their own independent line styles, e.g. a road as
154     * boundary of a forest. Otherwise, in case, the way does not have an
155     * independent line style, take a line style from the multipolygon.
156     * If the multipolygon does not have a line style either, at least create a
157     * default line style from the color of the area.
158     *
159     * Now consider the case that the way is not an outer way of any multipolygon,
160     * but is member of a multipolygon as "inner".
161     * First, the style list is regenerated, considering only tags of this way
162     * minus the tags of outer way of the multipolygon (to care for the "very
163     * old style").
164     * Then check, if the way describes something in its own right. (linear feature
165     * or area) If not, add a default line style from the area color of the multipolygon.
166     *
167     */
168    private Pair<StyleList, Range> getImpl(OsmPrimitive osm, double scale, NavigatableComponent nc) {
169        if (osm instanceof Node)
170            return generateStyles(osm, scale, null, false);
171        else if (osm instanceof Way)
172        {
173            Pair<StyleList, Range> p = generateStyles(osm, scale, null, false);
174
175            boolean isOuterWayOfSomeMP = false;
176            Color wayColor = null;
177
178            for (OsmPrimitive referrer : osm.getReferrers()) {
179                Relation r = (Relation) referrer;
180                if (!drawMultipolygon || !r.isMultipolygon()  || !r.isUsable()) {
181                    continue;
182                }
183                Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, r);
184
185                if (multipolygon.getOuterWays().contains(osm)) {
186                    boolean hasIndependentLineStyle = false;
187                    if (!isOuterWayOfSomeMP) { // do this only one time
188                        List<ElemStyle> tmp = new ArrayList<>(p.a.size());
189                        for (ElemStyle s : p.a) {
190                            if (s instanceof AreaElemStyle) {
191                                wayColor = ((AreaElemStyle) s).color;
192                            } else {
193                                tmp.add(s);
194                                if (s.isProperLineStyle()) {
195                                    hasIndependentLineStyle = true;
196                                }
197                            }
198                        }
199                        p.a = new StyleList(tmp);
200                        isOuterWayOfSomeMP = true;
201                    }
202
203                    if (!hasIndependentLineStyle) {
204                        Pair<StyleList, Range> mpElemStyles;
205                        synchronized(r) {
206                            mpElemStyles = getStyleCacheWithRange(r, scale, nc);
207                        }
208                        ElemStyle mpLine = null;
209                        for (ElemStyle s : mpElemStyles.a) {
210                            if (s.isProperLineStyle()) {
211                                mpLine = s;
212                                break;
213                            }
214                        }
215                        p.b = Range.cut(p.b, mpElemStyles.b);
216                        if (mpLine != null) {
217                            p.a = new StyleList(p.a, mpLine);
218                            break;
219                        } else if (wayColor == null && isDefaultLines()) {
220                            AreaElemStyle mpArea = Utils.find(mpElemStyles.a, AreaElemStyle.class);
221                            if (mpArea != null) {
222                                wayColor = mpArea.color;
223                            }
224                        }
225                    }
226                }
227            }
228            if (isOuterWayOfSomeMP) {
229                if (isDefaultLines()) {
230                    boolean hasLineStyle = false;
231                    for (ElemStyle s : p.a) {
232                        if (s.isProperLineStyle()) {
233                            hasLineStyle = true;
234                            break;
235                        }
236                    }
237                    if (!hasLineStyle) {
238                        p.a = new StyleList(p.a, LineElemStyle.createSimpleLineStyle(wayColor, true));
239                    }
240                }
241                return p;
242            }
243
244            if (!isDefaultLines()) return p;
245
246            for (OsmPrimitive referrer : osm.getReferrers()) {
247                Relation ref = (Relation) referrer;
248                if (!drawMultipolygon || !ref.isMultipolygon() || !ref.isUsable()) {
249                    continue;
250                }
251                final Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, ref);
252
253                if (multipolygon.getInnerWays().contains(osm)) {
254                    Iterator<Way> it = multipolygon.getOuterWays().iterator();
255                    p = generateStyles(osm, scale, it.hasNext() ? it.next() : null, false);
256                    boolean hasIndependentElemStyle = false;
257                    for (ElemStyle s : p.a) {
258                        if (s.isProperLineStyle() || s instanceof AreaElemStyle) {
259                            hasIndependentElemStyle = true;
260                            break;
261                        }
262                    }
263                    if (!hasIndependentElemStyle && !multipolygon.getOuterWays().isEmpty()) {
264                        Color mpColor = null;
265                        StyleList mpElemStyles = null;
266                        synchronized (ref) {
267                            mpElemStyles = get(ref, scale, nc);
268                        }
269                        for (ElemStyle mpS : mpElemStyles) {
270                            if (mpS instanceof AreaElemStyle) {
271                                mpColor = ((AreaElemStyle) mpS).color;
272                                break;
273                            }
274                        }
275                        p.a = new StyleList(p.a, LineElemStyle.createSimpleLineStyle(mpColor, true));
276                    }
277                    return p;
278                }
279            }
280            return p;
281        }
282        else if (osm instanceof Relation)
283        {
284            Pair<StyleList, Range> p = generateStyles(osm, scale, null, true);
285            if (drawMultipolygon && ((Relation)osm).isMultipolygon()) {
286                if (!Utils.exists(p.a, AreaElemStyle.class)) {
287                    // look at outer ways to find area style
288                    Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, (Relation) osm);
289                    for (Way w : multipolygon.getOuterWays()) {
290                        Pair<StyleList, Range> wayStyles = generateStyles(w, scale, null, false);
291                        p.b = Range.cut(p.b, wayStyles.b);
292                        ElemStyle area = Utils.find(wayStyles.a, AreaElemStyle.class);
293                        if (area != null) {
294                            p.a = new StyleList(p.a, area);
295                            break;
296                        }
297                    }
298                }
299            }
300            return p;
301        }
302        return null;
303    }
304
305    /**
306     * Create the list of styles and its valid scale range for one primitive.
307     *
308     * Loops over the list of style sources, to generate the map of properties.
309     * From these properties, it generates the different types of styles.
310     *
311     * @param osm the primitive to create styles for
312     * @param scale the scale (in meters per 100 px), must be &gt; 0
313     * @param multipolyOuterWay support for a very old multipolygon tagging style
314     * where you add the tags both to the outer and the inner way.
315     * However, independent inner way style is also possible.
316     * @param pretendWayIsClosed For styles that require the way to be closed,
317     * we pretend it is. This is useful for generating area styles from the (segmented)
318     * outer ways of a multipolygon.
319     * @return the generated styles and the valid range as a pair
320     */
321    public Pair<StyleList, Range> generateStyles(OsmPrimitive osm, double scale, OsmPrimitive multipolyOuterWay, boolean pretendWayIsClosed) {
322
323        List<ElemStyle> sl = new ArrayList<>();
324        MultiCascade mc = new MultiCascade();
325        Environment env = new Environment(osm, mc, null, null);
326
327        for (StyleSource s : styleSources) {
328            if (s.active) {
329                s.apply(mc, osm, scale, multipolyOuterWay, pretendWayIsClosed);
330            }
331        }
332
333        for (Entry<String, Cascade> e : mc.getLayers()) {
334            if ("*".equals(e.getKey())) {
335                continue;
336            }
337            env.layer = e.getKey();
338            if (osm instanceof Way) {
339                addIfNotNull(sl, AreaElemStyle.create(env));
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(env));
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        MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
450        try {
451            if (MapPaintStyles.getStyles() == null)
452                return null;
453            for (ElemStyle s : MapPaintStyles.getStyles().generateStyles(p, 1.0, null, pretendWayIsClosed).a) {
454                if (s instanceof AreaElemStyle)
455                    return (AreaElemStyle) s;
456            }
457            return null;
458        } finally {
459            MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
460        }
461    }
462
463    /**
464     * Determines whether primitive has an AreaElemStyle.
465     * @param p the OSM primitive
466     * @param pretendWayIsClosed For styles that require the way to be closed,
467     * we pretend it is. This is useful for generating area styles from the (segmented)
468     * outer ways of a multipolygon.
469     * @return {@code true} if primitive has an AreaElemStyle
470     */
471    public static boolean hasAreaElemStyle(OsmPrimitive p, boolean pretendWayIsClosed) {
472        return getAreaElemStyle(p, pretendWayIsClosed) != null;
473    }
474}