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