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 }