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