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