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