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.HashMap; 009import java.util.List; 010import java.util.Map; 011import java.util.Map.Entry; 012import java.util.Optional; 013 014import org.openstreetmap.josm.data.osm.Node; 015import org.openstreetmap.josm.data.osm.OsmPrimitive; 016import org.openstreetmap.josm.data.osm.Relation; 017import org.openstreetmap.josm.data.osm.Way; 018import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; 019import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon; 020import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; 021import org.openstreetmap.josm.gui.MainApplication; 022import org.openstreetmap.josm.gui.NavigatableComponent; 023import org.openstreetmap.josm.gui.layer.OsmDataLayer; 024import org.openstreetmap.josm.gui.mappaint.DividedScale.RangeViolatedError; 025import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource; 026import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement; 027import org.openstreetmap.josm.gui.mappaint.styleelement.AreaIconElement; 028import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement; 029import org.openstreetmap.josm.gui.mappaint.styleelement.LineElement; 030import org.openstreetmap.josm.gui.mappaint.styleelement.NodeElement; 031import org.openstreetmap.josm.gui.mappaint.styleelement.RepeatImageElement; 032import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement; 033import org.openstreetmap.josm.gui.mappaint.styleelement.TextElement; 034import org.openstreetmap.josm.gui.mappaint.styleelement.TextLabel; 035import org.openstreetmap.josm.gui.util.GuiHelper; 036import org.openstreetmap.josm.spi.preferences.Config; 037import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent; 038import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener; 039import org.openstreetmap.josm.tools.Pair; 040import org.openstreetmap.josm.tools.Utils; 041 042/** 043 * Generates a list of {@link StyleElement}s for a primitive, to 044 * be drawn on the map. 045 * There are several steps to derive the list of elements for display: 046 * <ol> 047 * <li>{@link #generateStyles(OsmPrimitive, double, boolean)} applies the 048 * {@link StyleSource}s one after another to get a key-value map of MapCSS 049 * properties. Then a preliminary set of StyleElements is derived from the 050 * properties map.</li> 051 * <li>{@link #getImpl(OsmPrimitive, double, NavigatableComponent)} handles the 052 * different forms of multipolygon tagging.</li> 053 * <li>{@link #getStyleCacheWithRange(OsmPrimitive, double, NavigatableComponent)} 054 * adds a default StyleElement for primitives that would be invisible otherwise. 055 * (For example untagged nodes and ways.)</li> 056 * </ol> 057 * The results are cached with respect to the current scale. 058 * 059 * Use {@link #setStyleSources(Collection)} to select the StyleSources that are applied. 060 */ 061public class ElemStyles implements PreferenceChangedListener { 062 private final List<StyleSource> styleSources; 063 private boolean drawMultipolygon; 064 065 private short cacheIdx = 1; 066 067 private boolean defaultNodes; 068 private boolean defaultLines; 069 070 private short defaultNodesIdx; 071 private short defaultLinesIdx; 072 073 private final Map<String, String> preferenceCache = new HashMap<>(); 074 075 private volatile Color backgroundColorCache; 076 077 /** 078 * Constructs a new {@code ElemStyles}. 079 */ 080 public ElemStyles() { 081 styleSources = new ArrayList<>(); 082 Config.getPref().addPreferenceChangeListener(this); 083 } 084 085 /** 086 * Clear the style cache for all primitives of all DataSets. 087 */ 088 public void clearCached() { 089 // run in EDT to make sure this isn't called during rendering run 090 GuiHelper.runInEDT(() -> { 091 cacheIdx++; 092 preferenceCache.clear(); 093 backgroundColorCache = null; 094 MainApplication.getLayerManager().getLayersOfType(OsmDataLayer.class).forEach( 095 dl -> dl.data.clearMappaintCache()); 096 }); 097 } 098 099 /** 100 * Returns the list of style sources. 101 * @return the list of style sources 102 */ 103 public List<StyleSource> getStyleSources() { 104 return Collections.<StyleSource>unmodifiableList(styleSources); 105 } 106 107 /** 108 * Returns the background color. 109 * @return the background color 110 */ 111 public Color getBackgroundColor() { 112 if (backgroundColorCache != null) 113 return backgroundColorCache; 114 for (StyleSource s : styleSources) { 115 if (!s.active) { 116 continue; 117 } 118 Color backgroundColorOverride = s.getBackgroundColorOverride(); 119 if (backgroundColorOverride != null) { 120 backgroundColorCache = backgroundColorOverride; 121 } 122 } 123 return Optional.ofNullable(backgroundColorCache).orElseGet(PaintColors.BACKGROUND::get); 124 } 125 126 /** 127 * Create the list of styles for one primitive. 128 * 129 * @param osm the primitive 130 * @param scale the scale (in meters per 100 pixel) 131 * @param nc display component 132 * @return list of styles 133 */ 134 public StyleElementList get(OsmPrimitive osm, double scale, NavigatableComponent nc) { 135 return getStyleCacheWithRange(osm, scale, nc).a; 136 } 137 138 /** 139 * Create the list of styles and its valid scale range for one primitive. 140 * 141 * Automatically adds default styles in case no proper style was found. 142 * Uses the cache, if possible, and saves the results to the cache. 143 * @param osm OSM primitive 144 * @param scale scale 145 * @param nc navigatable component 146 * @return pair containing style list and range 147 */ 148 public Pair<StyleElementList, Range> getStyleCacheWithRange(OsmPrimitive osm, double scale, NavigatableComponent nc) { 149 if (!osm.isCachedStyleUpToDate() || scale <= 0) { 150 osm.setCachedStyle(StyleCache.EMPTY_STYLECACHE); 151 } else { 152 Pair<StyleElementList, Range> lst = osm.getCachedStyle().getWithRange(scale, osm.isSelected()); 153 if (lst.a != null) 154 return lst; 155 } 156 Pair<StyleElementList, Range> p = getImpl(osm, scale, nc); 157 if (osm instanceof Node && isDefaultNodes()) { 158 if (p.a.isEmpty()) { 159 if (TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) { 160 p.a = NodeElement.DEFAULT_NODE_STYLELIST_TEXT; 161 } else { 162 p.a = NodeElement.DEFAULT_NODE_STYLELIST; 163 } 164 } else { 165 boolean hasNonModifier = false; 166 boolean hasText = false; 167 for (StyleElement s : p.a) { 168 if (s instanceof BoxTextElement) { 169 hasText = true; 170 } else { 171 if (!s.isModifier) { 172 hasNonModifier = true; 173 } 174 } 175 } 176 if (!hasNonModifier) { 177 p.a = new StyleElementList(p.a, NodeElement.SIMPLE_NODE_ELEMSTYLE); 178 if (!hasText && TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) { 179 p.a = new StyleElementList(p.a, BoxTextElement.SIMPLE_NODE_TEXT_ELEMSTYLE); 180 } 181 } 182 } 183 } else if (osm instanceof Way && isDefaultLines()) { 184 boolean hasProperLineStyle = false; 185 for (StyleElement s : p.a) { 186 if (s.isProperLineStyle()) { 187 hasProperLineStyle = true; 188 break; 189 } 190 } 191 if (!hasProperLineStyle) { 192 AreaElement area = Utils.find(p.a, AreaElement.class); 193 LineElement line = area == null ? LineElement.UNTAGGED_WAY : LineElement.createSimpleLineStyle(area.color, true); 194 p.a = new StyleElementList(p.a, line); 195 } 196 } 197 StyleCache style = osm.getCachedStyle() != null ? osm.getCachedStyle() : StyleCache.EMPTY_STYLECACHE; 198 try { 199 osm.setCachedStyle(style.put(p.a, p.b, osm.isSelected())); 200 } catch (RangeViolatedError e) { 201 throw new AssertionError("Range violated: " + e.getMessage() 202 + " (object: " + osm.getPrimitiveId() + ", current style: "+osm.getCachedStyle() 203 + ", scale: " + scale + ", new stylelist: " + p.a + ", new range: " + p.b + ')', e); 204 } 205 osm.declareCachedStyleUpToDate(); 206 return p; 207 } 208 209 /** 210 * Create the list of styles and its valid scale range for one primitive. 211 * 212 * This method does multipolygon handling. 213 * 214 * If the primitive is a way, look for multipolygon parents. In case it 215 * is indeed member of some multipolygon as role "outer", all area styles 216 * are removed. (They apply to the multipolygon area.) 217 * Outer ways can have their own independent line styles, e.g. a road as 218 * boundary of a forest. Otherwise, in case, the way does not have an 219 * independent line style, take a line style from the multipolygon. 220 * If the multipolygon does not have a line style either, at least create a 221 * default line style from the color of the area. 222 * 223 * Now consider the case that the way is not an outer way of any multipolygon, 224 * but is member of a multipolygon as "inner". 225 * First, the style list is regenerated, considering only tags of this way. 226 * Then check, if the way describes something in its own right. (linear feature 227 * or area) If not, add a default line style from the area color of the multipolygon. 228 * 229 * @param osm OSM primitive 230 * @param scale scale 231 * @param nc navigatable component 232 * @return pair containing style list and range 233 */ 234 private Pair<StyleElementList, Range> getImpl(OsmPrimitive osm, double scale, NavigatableComponent nc) { 235 if (osm instanceof Node) 236 return generateStyles(osm, scale, false); 237 else if (osm instanceof Way) { 238 Pair<StyleElementList, Range> p = generateStyles(osm, scale, false); 239 240 boolean isOuterWayOfSomeMP = false; 241 Color wayColor = null; 242 243 // FIXME: Maybe in the future outer way styles apply to outers ignoring the multipolygon? 244 for (OsmPrimitive referrer : osm.getReferrers()) { 245 Relation r = (Relation) referrer; 246 if (!drawMultipolygon || !r.isMultipolygon() || !r.isUsable()) { 247 continue; 248 } 249 Multipolygon multipolygon = MultipolygonCache.getInstance().get(r); 250 251 if (multipolygon.getOuterWays().contains(osm)) { 252 boolean hasIndependentLineStyle = false; 253 if (!isOuterWayOfSomeMP) { // do this only one time 254 List<StyleElement> tmp = new ArrayList<>(p.a.size()); 255 for (StyleElement s : p.a) { 256 if (s instanceof AreaElement) { 257 wayColor = ((AreaElement) s).color; 258 } else { 259 tmp.add(s); 260 if (s.isProperLineStyle()) { 261 hasIndependentLineStyle = true; 262 } 263 } 264 } 265 p.a = new StyleElementList(tmp); 266 isOuterWayOfSomeMP = true; 267 } 268 269 if (!hasIndependentLineStyle) { 270 Pair<StyleElementList, Range> mpElemStyles; 271 synchronized (r) { 272 mpElemStyles = getStyleCacheWithRange(r, scale, nc); 273 } 274 StyleElement mpLine = null; 275 for (StyleElement s : mpElemStyles.a) { 276 if (s.isProperLineStyle()) { 277 mpLine = s; 278 break; 279 } 280 } 281 p.b = Range.cut(p.b, mpElemStyles.b); 282 if (mpLine != null) { 283 p.a = new StyleElementList(p.a, mpLine); 284 break; 285 } else if (wayColor == null && isDefaultLines()) { 286 AreaElement mpArea = Utils.find(mpElemStyles.a, AreaElement.class); 287 if (mpArea != null) { 288 wayColor = mpArea.color; 289 } 290 } 291 } 292 } 293 } 294 if (isOuterWayOfSomeMP) { 295 if (isDefaultLines()) { 296 boolean hasLineStyle = false; 297 for (StyleElement s : p.a) { 298 if (s.isProperLineStyle()) { 299 hasLineStyle = true; 300 break; 301 } 302 } 303 if (!hasLineStyle) { 304 p.a = new StyleElementList(p.a, LineElement.createSimpleLineStyle(wayColor, true)); 305 } 306 } 307 return p; 308 } 309 310 if (!isDefaultLines()) return p; 311 312 for (OsmPrimitive referrer : osm.getReferrers()) { 313 Relation ref = (Relation) referrer; 314 if (!drawMultipolygon || !ref.isMultipolygon() || !ref.isUsable()) { 315 continue; 316 } 317 final Multipolygon multipolygon = MultipolygonCache.getInstance().get(ref); 318 319 if (multipolygon.getInnerWays().contains(osm)) { 320 p = generateStyles(osm, scale, false); 321 boolean hasIndependentElemStyle = false; 322 for (StyleElement s : p.a) { 323 if (s.isProperLineStyle() || s instanceof AreaElement) { 324 hasIndependentElemStyle = true; 325 break; 326 } 327 } 328 if (!hasIndependentElemStyle && !multipolygon.getOuterWays().isEmpty()) { 329 Color mpColor = null; 330 StyleElementList mpElemStyles; 331 synchronized (ref) { 332 mpElemStyles = get(ref, scale, nc); 333 } 334 for (StyleElement mpS : mpElemStyles) { 335 if (mpS instanceof AreaElement) { 336 mpColor = ((AreaElement) mpS).color; 337 break; 338 } 339 } 340 p.a = new StyleElementList(p.a, LineElement.createSimpleLineStyle(mpColor, true)); 341 } 342 return p; 343 } 344 } 345 return p; 346 } else if (osm instanceof Relation) { 347 return generateStyles(osm, scale, true); 348 } 349 return null; 350 } 351 352 /** 353 * Create the list of styles and its valid scale range for one primitive. 354 * 355 * Loops over the list of style sources, to generate the map of properties. 356 * From these properties, it generates the different types of styles. 357 * 358 * @param osm the primitive to create styles for 359 * @param scale the scale (in meters per 100 px), must be > 0 360 * @param pretendWayIsClosed For styles that require the way to be closed, 361 * we pretend it is. This is useful for generating area styles from the (segmented) 362 * outer ways of a multipolygon. 363 * @return the generated styles and the valid range as a pair 364 */ 365 public Pair<StyleElementList, Range> generateStyles(OsmPrimitive osm, double scale, boolean pretendWayIsClosed) { 366 367 List<StyleElement> sl = new ArrayList<>(); 368 MultiCascade mc = new MultiCascade(); 369 Environment env = new Environment(osm, mc, null, null); 370 371 for (StyleSource s : styleSources) { 372 if (s.active) { 373 s.apply(mc, osm, scale, pretendWayIsClosed); 374 } 375 } 376 377 for (Entry<String, Cascade> e : mc.getLayers()) { 378 if ("*".equals(e.getKey())) { 379 continue; 380 } 381 env.layer = e.getKey(); 382 if (osm instanceof Way) { 383 AreaElement areaStyle = AreaElement.create(env); 384 addIfNotNull(sl, areaStyle); 385 addIfNotNull(sl, RepeatImageElement.create(env)); 386 addIfNotNull(sl, LineElement.createLine(env)); 387 addIfNotNull(sl, LineElement.createLeftCasing(env)); 388 addIfNotNull(sl, LineElement.createRightCasing(env)); 389 addIfNotNull(sl, LineElement.createCasing(env)); 390 addIfNotNull(sl, AreaIconElement.create(env)); 391 addIfNotNull(sl, TextElement.create(env)); 392 if (areaStyle != null) { 393 //TODO: Warn about this, or even remove it completely 394 addIfNotNull(sl, TextElement.createForContent(env)); 395 } 396 } else if (osm instanceof Node) { 397 NodeElement nodeStyle = NodeElement.create(env); 398 if (nodeStyle != null) { 399 sl.add(nodeStyle); 400 addIfNotNull(sl, BoxTextElement.create(env, nodeStyle.getBoxProvider())); 401 } else { 402 addIfNotNull(sl, BoxTextElement.create(env, NodeElement.SIMPLE_NODE_ELEMSTYLE_BOXPROVIDER)); 403 } 404 } else if (osm instanceof Relation) { 405 if (((Relation) osm).isMultipolygon()) { 406 AreaElement areaStyle = AreaElement.create(env); 407 addIfNotNull(sl, areaStyle); 408 addIfNotNull(sl, RepeatImageElement.create(env)); 409 addIfNotNull(sl, LineElement.createLine(env)); 410 addIfNotNull(sl, LineElement.createCasing(env)); 411 addIfNotNull(sl, AreaIconElement.create(env)); 412 addIfNotNull(sl, TextElement.create(env)); 413 if (areaStyle != null) { 414 //TODO: Warn about this, or even remove it completely 415 addIfNotNull(sl, TextElement.createForContent(env)); 416 } 417 } else if (osm.hasTag("type", "restriction")) { 418 addIfNotNull(sl, NodeElement.create(env)); 419 } 420 } 421 } 422 return new Pair<>(new StyleElementList(sl), mc.range); 423 } 424 425 private static <T> void addIfNotNull(List<T> list, T obj) { 426 if (obj != null) { 427 list.add(obj); 428 } 429 } 430 431 /** 432 * Draw a default node symbol for nodes that have no style? 433 * @return {@code true} if default node symbol must be drawn 434 */ 435 private boolean isDefaultNodes() { 436 if (defaultNodesIdx == cacheIdx) 437 return defaultNodes; 438 defaultNodes = fromCanvas("default-points", Boolean.TRUE, Boolean.class); 439 defaultNodesIdx = cacheIdx; 440 return defaultNodes; 441 } 442 443 /** 444 * Draw a default line for ways that do not have an own line style? 445 * @return {@code true} if default line must be drawn 446 */ 447 private boolean isDefaultLines() { 448 if (defaultLinesIdx == cacheIdx) 449 return defaultLines; 450 defaultLines = fromCanvas("default-lines", Boolean.TRUE, Boolean.class); 451 defaultLinesIdx = cacheIdx; 452 return defaultLines; 453 } 454 455 private <T> T fromCanvas(String key, T def, Class<T> c) { 456 MultiCascade mc = new MultiCascade(); 457 Relation r = new Relation(); 458 r.put("#canvas", "query"); 459 460 for (StyleSource s : styleSources) { 461 if (s.active) { 462 s.apply(mc, r, 1, false); 463 } 464 } 465 return mc.getCascade("default").get(key, def, c); 466 } 467 468 /** 469 * Determines whether multipolygons must be drawn. 470 * @return whether multipolygons must be drawn. 471 */ 472 public boolean isDrawMultipolygon() { 473 return drawMultipolygon; 474 } 475 476 /** 477 * Sets whether multipolygons must be drawn. 478 * @param drawMultipolygon whether multipolygons must be drawn 479 */ 480 public void setDrawMultipolygon(boolean drawMultipolygon) { 481 this.drawMultipolygon = drawMultipolygon; 482 } 483 484 /** 485 * remove all style sources; only accessed from MapPaintStyles 486 */ 487 void clear() { 488 styleSources.clear(); 489 } 490 491 /** 492 * add a style source; only accessed from MapPaintStyles 493 * @param style style source to add 494 */ 495 void add(StyleSource style) { 496 styleSources.add(style); 497 } 498 499 /** 500 * remove a style source; only accessed from MapPaintStyles 501 * @param style style source to remove 502 * @return {@code true} if this list contained the specified element 503 */ 504 boolean remove(StyleSource style) { 505 return styleSources.remove(style); 506 } 507 508 /** 509 * set the style sources; only accessed from MapPaintStyles 510 * @param sources new style sources 511 */ 512 void setStyleSources(Collection<StyleSource> sources) { 513 styleSources.clear(); 514 styleSources.addAll(sources); 515 } 516 517 /** 518 * Returns the first AreaElement for a given primitive. 519 * @param p the OSM primitive 520 * @param pretendWayIsClosed For styles that require the way to be closed, 521 * we pretend it is. This is useful for generating area styles from the (segmented) 522 * outer ways of a multipolygon. 523 * @return first AreaElement found or {@code null}. 524 */ 525 public static AreaElement getAreaElemStyle(OsmPrimitive p, boolean pretendWayIsClosed) { 526 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock(); 527 try { 528 if (MapPaintStyles.getStyles() == null) 529 return null; 530 for (StyleElement s : MapPaintStyles.getStyles().generateStyles(p, 1.0, pretendWayIsClosed).a) { 531 if (s instanceof AreaElement) 532 return (AreaElement) s; 533 } 534 return null; 535 } finally { 536 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock(); 537 } 538 } 539 540 /** 541 * Determines whether primitive has an AreaElement. 542 * @param p the OSM primitive 543 * @param pretendWayIsClosed For styles that require the way to be closed, 544 * we pretend it is. This is useful for generating area styles from the (segmented) 545 * outer ways of a multipolygon. 546 * @return {@code true} if primitive has an AreaElement 547 */ 548 public static boolean hasAreaElemStyle(OsmPrimitive p, boolean pretendWayIsClosed) { 549 return getAreaElemStyle(p, pretendWayIsClosed) != null; 550 } 551 552 /** 553 * Determines whether primitive has area-type {@link StyleElement}s, but 554 * no line-type StyleElements. 555 * 556 * {@link TextElement} is ignored, as it can be both line and area-type. 557 * @param p the OSM primitive 558 * @return {@code true} if primitive has area elements, but no line elements 559 * @since 12700 560 */ 561 public static boolean hasOnlyAreaElements(OsmPrimitive p) { 562 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock(); 563 try { 564 if (MapPaintStyles.getStyles() == null) 565 return false; 566 StyleElementList styles = MapPaintStyles.getStyles().generateStyles(p, 1.0, false).a; 567 boolean hasAreaElement = false; 568 for (StyleElement s : styles) { 569 if (s instanceof TextElement) { 570 continue; 571 } 572 if (s instanceof AreaElement) { 573 hasAreaElement = true; 574 } else { 575 return false; 576 } 577 } 578 return hasAreaElement; 579 } finally { 580 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock(); 581 } 582 } 583 584 /** 585 * Looks up a preference value and ensures the style cache is invalidated 586 * as soon as this preference value is changed by the user. 587 * 588 * In addition, it adds an intermediate cache for the preference values, 589 * as frequent preference lookup (using <code>Config.getPref().get()</code>) for 590 * each primitive can be slow during rendering. 591 * 592 * @param key preference key 593 * @param def default value 594 * @return the corresponding preference value 595 * @see org.openstreetmap.josm.data.Preferences#get(String, String) 596 */ 597 public String getPreferenceCached(String key, String def) { 598 String res; 599 if (preferenceCache.containsKey(key)) { 600 res = preferenceCache.get(key); 601 } else { 602 res = Config.getPref().get(key, null); 603 preferenceCache.put(key, res); 604 } 605 return res != null ? res : def; 606 } 607 608 @Override 609 public void preferenceChanged(PreferenceChangeEvent e) { 610 if (preferenceCache.containsKey(e.getKey())) { 611 clearCached(); 612 } 613 } 614}