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.Objects; 013import java.util.Optional; 014 015import org.openstreetmap.josm.data.osm.INode; 016import org.openstreetmap.josm.data.osm.IPrimitive; 017import org.openstreetmap.josm.data.osm.IRelation; 018import org.openstreetmap.josm.data.osm.IWay; 019import org.openstreetmap.josm.data.osm.Relation; 020import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; 021import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon; 022import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; 023import org.openstreetmap.josm.gui.MainApplication; 024import org.openstreetmap.josm.gui.NavigatableComponent; 025import org.openstreetmap.josm.gui.layer.OsmDataLayer; 026import org.openstreetmap.josm.gui.mappaint.DividedScale.RangeViolatedError; 027import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource; 028import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement; 029import org.openstreetmap.josm.gui.mappaint.styleelement.AreaIconElement; 030import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement; 031import org.openstreetmap.josm.gui.mappaint.styleelement.DefaultStyles; 032import org.openstreetmap.josm.gui.mappaint.styleelement.LineElement; 033import org.openstreetmap.josm.gui.mappaint.styleelement.NodeElement; 034import org.openstreetmap.josm.gui.mappaint.styleelement.RepeatImageElement; 035import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement; 036import org.openstreetmap.josm.gui.mappaint.styleelement.TextElement; 037import org.openstreetmap.josm.gui.mappaint.styleelement.TextLabel; 038import org.openstreetmap.josm.gui.util.GuiHelper; 039import org.openstreetmap.josm.spi.preferences.Config; 040import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent; 041import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener; 042import org.openstreetmap.josm.tools.Pair; 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 = Collections.synchronizedList(new ArrayList<>()); 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 = Collections.synchronizedMap(new HashMap<>()); 076 077 private volatile Color backgroundColorCache; 078 079 /** 080 * Constructs a new {@code ElemStyles}. 081 */ 082 public ElemStyles() { 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 = DefaultStyles.DEFAULT_NODE_STYLELIST_TEXT; 164 } else { 165 p.a = DefaultStyles.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, DefaultStyles.SIMPLE_NODE_ELEMSTYLE); 181 if (!hasText && TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) { 182 p.a = new StyleElementList(p.a, DefaultStyles.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 LineElement line = LineElement.UNTAGGED_WAY; 196 for (StyleElement element : p.a) { 197 if (element instanceof AreaElement) { 198 line = LineElement.createSimpleLineStyle(((AreaElement) element).color, true); 199 break; 200 } 201 } 202 p.a = new StyleElementList(p.a, line); 203 } 204 } 205 StyleCache style = osm.getCachedStyle() != null ? osm.getCachedStyle() : StyleCache.EMPTY_STYLECACHE; 206 try { 207 osm.setCachedStyle(style.put(p.a, p.b, osm.isSelected())); 208 } catch (RangeViolatedError e) { 209 throw new AssertionError("Range violated: " + e.getMessage() 210 + " (object: " + osm.getPrimitiveId() + ", current style: "+osm.getCachedStyle() 211 + ", scale: " + scale + ", new stylelist: " + p.a + ", new range: " + p.b + ')', e); 212 } 213 osm.declareCachedStyleUpToDate(); 214 return p; 215 } 216 217 /** 218 * Create the list of styles and its valid scale range for one primitive. 219 * 220 * This method does multipolygon handling. 221 * 222 * If the primitive is a way, look for multipolygon parents. In case it 223 * is indeed member of some multipolygon as role "outer", all area styles 224 * are removed. (They apply to the multipolygon area.) 225 * Outer ways can have their own independent line styles, e.g. a road as 226 * boundary of a forest. Otherwise, in case, the way does not have an 227 * independent line style, take a line style from the multipolygon. 228 * If the multipolygon does not have a line style either, at least create a 229 * default line style from the color of the area. 230 * 231 * Now consider the case that the way is not an outer way of any multipolygon, 232 * but is member of a multipolygon as "inner". 233 * First, the style list is regenerated, considering only tags of this way. 234 * Then check, if the way describes something in its own right. (linear feature 235 * or area) If not, add a default line style from the area color of the multipolygon. 236 * 237 * @param osm OSM primitive 238 * @param scale scale 239 * @param nc navigatable component 240 * @return pair containing style list and range 241 */ 242 private Pair<StyleElementList, Range> getImpl(IPrimitive osm, double scale, NavigatableComponent nc) { 243 if (osm instanceof INode) 244 return generateStyles(osm, scale, false); 245 else if (osm instanceof IWay) { 246 Pair<StyleElementList, Range> p = generateStyles(osm, scale, false); 247 248 boolean isOuterWayOfSomeMP = false; 249 Color wayColor = null; 250 251 // FIXME: Maybe in the future outer way styles apply to outers ignoring the multipolygon? 252 for (IPrimitive referrer : osm.getReferrers()) { 253 IRelation<?> r = (IRelation<?>) referrer; 254 if (!drawMultipolygon || !r.isMultipolygon() || !r.isUsable() || !(r instanceof Relation)) { 255 continue; 256 } 257 Multipolygon multipolygon = MultipolygonCache.getInstance().get((Relation) r); 258 259 if (multipolygon.getOuterWays().contains(osm)) { 260 boolean hasIndependentLineStyle = false; 261 if (!isOuterWayOfSomeMP) { // do this only one time 262 List<StyleElement> tmp = new ArrayList<>(p.a.size()); 263 for (StyleElement s : p.a) { 264 if (s instanceof AreaElement) { 265 wayColor = ((AreaElement) s).color; 266 } else { 267 tmp.add(s); 268 if (s.isProperLineStyle()) { 269 hasIndependentLineStyle = true; 270 } 271 } 272 } 273 p.a = new StyleElementList(tmp); 274 isOuterWayOfSomeMP = true; 275 } 276 277 if (!hasIndependentLineStyle) { 278 Pair<StyleElementList, Range> mpElemStyles; 279 synchronized (r) { 280 mpElemStyles = getStyleCacheWithRange(r, scale, nc); 281 } 282 StyleElement mpLine = null; 283 for (StyleElement s : mpElemStyles.a) { 284 if (s.isProperLineStyle()) { 285 mpLine = s; 286 break; 287 } 288 } 289 p.b = Range.cut(p.b, mpElemStyles.b); 290 if (mpLine != null) { 291 p.a = new StyleElementList(p.a, mpLine); 292 break; 293 } else if (wayColor == null && isDefaultLines()) { 294 for (StyleElement element : mpElemStyles.a) { 295 if (element instanceof AreaElement) { 296 wayColor = ((AreaElement) element).color; 297 break; 298 } 299 } 300 } 301 } 302 } 303 } 304 if (isOuterWayOfSomeMP) { 305 if (isDefaultLines()) { 306 boolean hasLineStyle = false; 307 for (StyleElement s : p.a) { 308 if (s.isProperLineStyle()) { 309 hasLineStyle = true; 310 break; 311 } 312 } 313 if (!hasLineStyle) { 314 p.a = new StyleElementList(p.a, LineElement.createSimpleLineStyle(wayColor, true)); 315 } 316 } 317 return p; 318 } 319 320 if (!isDefaultLines()) return p; 321 322 for (IPrimitive referrer : osm.getReferrers()) { 323 IRelation<?> ref = (IRelation<?>) referrer; 324 if (!drawMultipolygon || !ref.isMultipolygon() || !ref.isUsable() || !(ref instanceof Relation)) { 325 continue; 326 } 327 final Multipolygon multipolygon = MultipolygonCache.getInstance().get((Relation) ref); 328 329 if (multipolygon.getInnerWays().contains(osm)) { 330 p = generateStyles(osm, scale, false); 331 boolean hasIndependentElemStyle = false; 332 for (StyleElement s : p.a) { 333 if (s.isProperLineStyle() || s instanceof AreaElement) { 334 hasIndependentElemStyle = true; 335 break; 336 } 337 } 338 if (!hasIndependentElemStyle && !multipolygon.getOuterWays().isEmpty()) { 339 Color mpColor = null; 340 StyleElementList mpElemStyles; 341 synchronized (ref) { 342 mpElemStyles = get(ref, scale, nc); 343 } 344 for (StyleElement mpS : mpElemStyles) { 345 if (mpS instanceof AreaElement) { 346 mpColor = ((AreaElement) mpS).color; 347 break; 348 } 349 } 350 p.a = new StyleElementList(p.a, LineElement.createSimpleLineStyle(mpColor, true)); 351 } 352 return p; 353 } 354 } 355 return p; 356 } else if (osm instanceof IRelation) { 357 return generateStyles(osm, scale, true); 358 } 359 return null; 360 } 361 362 /** 363 * Create the list of styles and its valid scale range for one primitive. 364 * 365 * Loops over the list of style sources, to generate the map of properties. 366 * From these properties, it generates the different types of styles. 367 * 368 * @param osm the primitive to create styles for 369 * @param scale the scale (in meters per 100 px), must be > 0 370 * @param pretendWayIsClosed For styles that require the way to be closed, 371 * we pretend it is. This is useful for generating area styles from the (segmented) 372 * outer ways of a multipolygon. 373 * @return the generated styles and the valid range as a pair 374 * @since 13810 (signature) 375 */ 376 public Pair<StyleElementList, Range> generateStyles(IPrimitive osm, double scale, boolean pretendWayIsClosed) { 377 378 List<StyleElement> sl = new ArrayList<>(); 379 MultiCascade mc = new MultiCascade(); 380 Environment env = new Environment(osm, mc, null, null); 381 382 for (StyleSource s : styleSources) { 383 if (s.active) { 384 s.apply(mc, osm, scale, pretendWayIsClosed); 385 } 386 } 387 388 for (Entry<String, Cascade> e : mc.getLayers()) { 389 if ("*".equals(e.getKey())) { 390 continue; 391 } 392 env.layer = e.getKey(); 393 if (osm instanceof IWay) { 394 AreaElement areaStyle = AreaElement.create(env); 395 addIfNotNull(sl, areaStyle); 396 addIfNotNull(sl, RepeatImageElement.create(env)); 397 addIfNotNull(sl, LineElement.createLine(env)); 398 addIfNotNull(sl, LineElement.createLeftCasing(env)); 399 addIfNotNull(sl, LineElement.createRightCasing(env)); 400 addIfNotNull(sl, LineElement.createCasing(env)); 401 addIfNotNull(sl, AreaIconElement.create(env)); 402 addIfNotNull(sl, TextElement.create(env)); 403 if (areaStyle != null) { 404 //TODO: Warn about this, or even remove it completely 405 addIfNotNull(sl, TextElement.createForContent(env)); 406 } 407 } else if (osm instanceof INode) { 408 NodeElement nodeStyle = NodeElement.create(env); 409 if (nodeStyle != null) { 410 sl.add(nodeStyle); 411 addIfNotNull(sl, BoxTextElement.create(env, nodeStyle.getBoxProvider())); 412 } else { 413 addIfNotNull(sl, BoxTextElement.create(env, DefaultStyles.SIMPLE_NODE_ELEMSTYLE_BOXPROVIDER)); 414 } 415 } else if (osm instanceof IRelation) { 416 if (((IRelation<?>) osm).isMultipolygon()) { 417 AreaElement areaStyle = AreaElement.create(env); 418 addIfNotNull(sl, areaStyle); 419 addIfNotNull(sl, RepeatImageElement.create(env)); 420 addIfNotNull(sl, LineElement.createLine(env)); 421 addIfNotNull(sl, LineElement.createCasing(env)); 422 addIfNotNull(sl, AreaIconElement.create(env)); 423 addIfNotNull(sl, TextElement.create(env)); 424 if (areaStyle != null) { 425 //TODO: Warn about this, or even remove it completely 426 addIfNotNull(sl, TextElement.createForContent(env)); 427 } 428 } else if (osm.hasTag("type", "restriction")) { 429 addIfNotNull(sl, NodeElement.create(env)); 430 } 431 } 432 } 433 return new Pair<>(new StyleElementList(sl), mc.range); 434 } 435 436 private static <T> void addIfNotNull(List<T> list, T obj) { 437 if (obj != null) { 438 list.add(obj); 439 } 440 } 441 442 /** 443 * Draw a default node symbol for nodes that have no style? 444 * @return {@code true} if default node symbol must be drawn 445 */ 446 private boolean isDefaultNodes() { 447 if (defaultNodesIdx == cacheIdx) 448 return defaultNodes; 449 defaultNodes = fromCanvas("default-points", Boolean.TRUE, Boolean.class); 450 defaultNodesIdx = cacheIdx; 451 return defaultNodes; 452 } 453 454 /** 455 * Draw a default line for ways that do not have an own line style? 456 * @return {@code true} if default line must be drawn 457 */ 458 private boolean isDefaultLines() { 459 if (defaultLinesIdx == cacheIdx) 460 return defaultLines; 461 defaultLines = fromCanvas("default-lines", Boolean.TRUE, Boolean.class); 462 defaultLinesIdx = cacheIdx; 463 return defaultLines; 464 } 465 466 private <T> T fromCanvas(String key, T def, Class<T> c) { 467 MultiCascade mc = new MultiCascade(); 468 Relation r = new Relation(); 469 r.put("#canvas", "query"); 470 471 for (StyleSource s : styleSources) { 472 if (s.active) { 473 s.apply(mc, r, 1, false); 474 } 475 } 476 return mc.getCascade("default").get(key, def, c); 477 } 478 479 /** 480 * Determines whether multipolygons must be drawn. 481 * @return whether multipolygons must be drawn. 482 */ 483 public boolean isDrawMultipolygon() { 484 return drawMultipolygon; 485 } 486 487 /** 488 * Sets whether multipolygons must be drawn. 489 * @param drawMultipolygon whether multipolygons must be drawn 490 */ 491 public void setDrawMultipolygon(boolean drawMultipolygon) { 492 this.drawMultipolygon = drawMultipolygon; 493 } 494 495 /** 496 * remove all style sources; only accessed from MapPaintStyles 497 */ 498 void clear() { 499 styleSources.clear(); 500 } 501 502 /** 503 * add a style source; only accessed from MapPaintStyles 504 * @param style style source to add 505 */ 506 void add(StyleSource style) { 507 styleSources.add(Objects.requireNonNull(style)); 508 } 509 510 /** 511 * remove a style source; only accessed from MapPaintStyles 512 * @param style style source to remove 513 * @return {@code true} if this list contained the specified element 514 */ 515 boolean remove(StyleSource style) { 516 return styleSources.remove(Objects.requireNonNull(style)); 517 } 518 519 /** 520 * set the style sources; only accessed from MapPaintStyles 521 * @param sources new style sources 522 */ 523 void setStyleSources(Collection<StyleSource> sources) { 524 styleSources.clear(); 525 sources.forEach(this::add); 526 } 527 528 /** 529 * Returns the first AreaElement for a given primitive. 530 * @param p the OSM primitive 531 * @param pretendWayIsClosed For styles that require the way to be closed, 532 * we pretend it is. This is useful for generating area styles from the (segmented) 533 * outer ways of a multipolygon. 534 * @return first AreaElement found or {@code null}. 535 * @since 13810 (signature) 536 */ 537 public static AreaElement getAreaElemStyle(IPrimitive p, boolean pretendWayIsClosed) { 538 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock(); 539 try { 540 if (MapPaintStyles.getStyles() == null) 541 return null; 542 for (StyleElement s : MapPaintStyles.getStyles().generateStyles(p, 1.0, pretendWayIsClosed).a) { 543 if (s instanceof AreaElement) 544 return (AreaElement) s; 545 } 546 return null; 547 } finally { 548 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock(); 549 } 550 } 551 552 /** 553 * Determines whether primitive has an AreaElement. 554 * @param p the OSM primitive 555 * @param pretendWayIsClosed For styles that require the way to be closed, 556 * we pretend it is. This is useful for generating area styles from the (segmented) 557 * outer ways of a multipolygon. 558 * @return {@code true} if primitive has an AreaElement 559 * @since 13810 (signature) 560 */ 561 public static boolean hasAreaElemStyle(IPrimitive p, boolean pretendWayIsClosed) { 562 return getAreaElemStyle(p, pretendWayIsClosed) != null; 563 } 564 565 /** 566 * Determines whether primitive has area-type {@link StyleElement}s, but 567 * no line-type StyleElements. 568 * 569 * {@link TextElement} is ignored, as it can be both line and area-type. 570 * @param p the OSM primitive 571 * @return {@code true} if primitive has area elements, but no line elements 572 * @since 12700 573 * @since 13810 (signature) 574 */ 575 public static boolean hasOnlyAreaElements(IPrimitive p) { 576 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock(); 577 try { 578 if (MapPaintStyles.getStyles() == null) 579 return false; 580 StyleElementList styles = MapPaintStyles.getStyles().generateStyles(p, 1.0, false).a; 581 boolean hasAreaElement = false; 582 for (StyleElement s : styles) { 583 if (s instanceof TextElement) { 584 continue; 585 } 586 if (s instanceof AreaElement) { 587 hasAreaElement = true; 588 } else { 589 return false; 590 } 591 } 592 return hasAreaElement; 593 } finally { 594 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock(); 595 } 596 } 597 598 /** 599 * Looks up a preference value and ensures the style cache is invalidated 600 * as soon as this preference value is changed by the user. 601 * 602 * In addition, it adds an intermediate cache for the preference values, 603 * as frequent preference lookup (using <code>Config.getPref().get()</code>) for 604 * each primitive can be slow during rendering. 605 * 606 * @param key preference key 607 * @param def default value 608 * @return the corresponding preference value 609 * @see org.openstreetmap.josm.data.Preferences#get(String, String) 610 */ 611 public String getPreferenceCached(String key, String def) { 612 String res; 613 if (preferenceCache.containsKey(key)) { 614 res = preferenceCache.get(key); 615 } else { 616 res = Config.getPref().get(key, null); 617 preferenceCache.put(key, res); 618 } 619 return res != null ? res : def; 620 } 621 622 @Override 623 public void preferenceChanged(PreferenceChangeEvent e) { 624 if (preferenceCache.containsKey(e.getKey())) { 625 clearCached(); 626 } 627 } 628}