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