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