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