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 > 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}