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 applied. 055 */ 056public class ElemStyles implements PreferenceChangedListener { 057 private final List<StyleSource> styleSources; 058 private boolean drawMultipolygon; 059 060 private short cacheIdx = 1; 061 062 private boolean defaultNodes; 063 private boolean defaultLines; 064 065 private short defaultNodesIdx; 066 private short defaultLinesIdx; 067 068 private final Map<String, String> preferenceCache = new HashMap<>(); 069 070 /** 071 * Constructs a new {@code ElemStyles}. 072 */ 073 public ElemStyles() { 074 styleSources = new ArrayList<>(); 075 Main.pref.addPreferenceChangeListener(this); 076 } 077 078 /** 079 * Clear the style cache for all primitives of all DataSets. 080 */ 081 public void clearCached() { 082 // run in EDT to make sure this isn't called during rendering run 083 GuiHelper.runInEDT(() -> { 084 cacheIdx++; 085 preferenceCache.clear(); 086 }); 087 } 088 089 public List<StyleSource> getStyleSources() { 090 return Collections.<StyleSource>unmodifiableList(styleSources); 091 } 092 093 /** 094 * Create the list of styles for one primitive. 095 * 096 * @param osm the primitive 097 * @param scale the scale (in meters per 100 pixel) 098 * @param nc display component 099 * @return list of styles 100 */ 101 public StyleElementList get(OsmPrimitive osm, double scale, NavigatableComponent nc) { 102 return getStyleCacheWithRange(osm, scale, nc).a; 103 } 104 105 /** 106 * Create the list of styles and its valid scale range for one primitive. 107 * 108 * Automatically adds default styles in case no proper style was found. 109 * Uses the cache, if possible, and saves the results to the cache. 110 * @param osm OSM primitive 111 * @param scale scale 112 * @param nc navigatable component 113 * @return pair containing style list and range 114 */ 115 public Pair<StyleElementList, Range> getStyleCacheWithRange(OsmPrimitive osm, double scale, NavigatableComponent nc) { 116 if (osm.mappaintStyle == null || osm.getMappaintCacheIdx() != cacheIdx || scale <= 0) { 117 osm.mappaintStyle = StyleCache.EMPTY_STYLECACHE; 118 } else { 119 Pair<StyleElementList, Range> lst = osm.mappaintStyle.getWithRange(scale, osm.isSelected()); 120 if (lst.a != null) 121 return lst; 122 } 123 Pair<StyleElementList, Range> p = getImpl(osm, scale, nc); 124 if (osm instanceof Node && isDefaultNodes()) { 125 if (p.a.isEmpty()) { 126 if (TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) { 127 p.a = NodeElement.DEFAULT_NODE_STYLELIST_TEXT; 128 } else { 129 p.a = NodeElement.DEFAULT_NODE_STYLELIST; 130 } 131 } else { 132 boolean hasNonModifier = false; 133 boolean hasText = false; 134 for (StyleElement s : p.a) { 135 if (s instanceof BoxTextElement) { 136 hasText = true; 137 } else { 138 if (!s.isModifier) { 139 hasNonModifier = true; 140 } 141 } 142 } 143 if (!hasNonModifier) { 144 p.a = new StyleElementList(p.a, NodeElement.SIMPLE_NODE_ELEMSTYLE); 145 if (!hasText && TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) { 146 p.a = new StyleElementList(p.a, BoxTextElement.SIMPLE_NODE_TEXT_ELEMSTYLE); 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.setMappaintCacheIdx(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 && !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 return p; 333 } 334 return null; 335 } 336 337 /** 338 * Create the list of styles and its valid scale range for one primitive. 339 * 340 * Loops over the list of style sources, to generate the map of properties. 341 * From these properties, it generates the different types of styles. 342 * 343 * @param osm the primitive to create styles for 344 * @param scale the scale (in meters per 100 px), must be > 0 345 * @param pretendWayIsClosed For styles that require the way to be closed, 346 * we pretend it is. This is useful for generating area styles from the (segmented) 347 * outer ways of a multipolygon. 348 * @return the generated styles and the valid range as a pair 349 */ 350 public Pair<StyleElementList, Range> generateStyles(OsmPrimitive osm, double scale, boolean pretendWayIsClosed) { 351 352 List<StyleElement> sl = new ArrayList<>(); 353 MultiCascade mc = new MultiCascade(); 354 Environment env = new Environment(osm, mc, null, null); 355 356 for (StyleSource s : styleSources) { 357 if (s.active) { 358 s.apply(mc, osm, scale, pretendWayIsClosed); 359 } 360 } 361 362 for (Entry<String, Cascade> e : mc.getLayers()) { 363 if ("*".equals(e.getKey())) { 364 continue; 365 } 366 env.layer = e.getKey(); 367 if (osm instanceof Way) { 368 addIfNotNull(sl, AreaElement.create(env)); 369 addIfNotNull(sl, RepeatImageElement.create(env)); 370 addIfNotNull(sl, LineElement.createLine(env)); 371 addIfNotNull(sl, LineElement.createLeftCasing(env)); 372 addIfNotNull(sl, LineElement.createRightCasing(env)); 373 addIfNotNull(sl, LineElement.createCasing(env)); 374 addIfNotNull(sl, LineTextElement.create(env)); 375 } else if (osm instanceof Node) { 376 NodeElement nodeStyle = NodeElement.create(env); 377 if (nodeStyle != null) { 378 sl.add(nodeStyle); 379 addIfNotNull(sl, BoxTextElement.create(env, nodeStyle.getBoxProvider())); 380 } else { 381 addIfNotNull(sl, BoxTextElement.create(env, NodeElement.SIMPLE_NODE_ELEMSTYLE_BOXPROVIDER)); 382 } 383 } else if (osm instanceof Relation) { 384 if (((Relation) osm).isMultipolygon()) { 385 addIfNotNull(sl, AreaElement.create(env)); 386 addIfNotNull(sl, RepeatImageElement.create(env)); 387 addIfNotNull(sl, LineElement.createLine(env)); 388 addIfNotNull(sl, LineElement.createCasing(env)); 389 addIfNotNull(sl, LineTextElement.create(env)); 390 } else if ("restriction".equals(osm.get("type"))) { 391 addIfNotNull(sl, NodeElement.create(env)); 392 } 393 } 394 } 395 return new Pair<>(new StyleElementList(sl), mc.range); 396 } 397 398 private static <T> void addIfNotNull(List<T> list, T obj) { 399 if (obj != null) { 400 list.add(obj); 401 } 402 } 403 404 /** 405 * Draw a default node symbol for nodes that have no style? 406 * @return {@code true} if default node symbol must be drawn 407 */ 408 private boolean isDefaultNodes() { 409 if (defaultNodesIdx == cacheIdx) 410 return defaultNodes; 411 defaultNodes = fromCanvas("default-points", Boolean.TRUE, Boolean.class); 412 defaultNodesIdx = cacheIdx; 413 return defaultNodes; 414 } 415 416 /** 417 * Draw a default line for ways that do not have an own line style? 418 * @return {@code true} if default line must be drawn 419 */ 420 private boolean isDefaultLines() { 421 if (defaultLinesIdx == cacheIdx) 422 return defaultLines; 423 defaultLines = fromCanvas("default-lines", Boolean.TRUE, Boolean.class); 424 defaultLinesIdx = cacheIdx; 425 return defaultLines; 426 } 427 428 private <T> T fromCanvas(String key, T def, Class<T> c) { 429 MultiCascade mc = new MultiCascade(); 430 Relation r = new Relation(); 431 r.put("#canvas", "query"); 432 433 for (StyleSource s : styleSources) { 434 if (s.active) { 435 s.apply(mc, r, 1, false); 436 } 437 } 438 return mc.getCascade("default").get(key, def, c); 439 } 440 441 public boolean isDrawMultipolygon() { 442 return drawMultipolygon; 443 } 444 445 public void setDrawMultipolygon(boolean drawMultipolygon) { 446 this.drawMultipolygon = drawMultipolygon; 447 } 448 449 /** 450 * remove all style sources; only accessed from MapPaintStyles 451 */ 452 void clear() { 453 styleSources.clear(); 454 } 455 456 /** 457 * add a style source; only accessed from MapPaintStyles 458 * @param style style source to add 459 */ 460 void add(StyleSource style) { 461 styleSources.add(style); 462 } 463 464 /** 465 * set the style sources; only accessed from MapPaintStyles 466 * @param sources new style sources 467 */ 468 void setStyleSources(Collection<StyleSource> sources) { 469 styleSources.clear(); 470 styleSources.addAll(sources); 471 } 472 473 /** 474 * Returns the first AreaElement for a given primitive. 475 * @param p the OSM primitive 476 * @param pretendWayIsClosed For styles that require the way to be closed, 477 * we pretend it is. This is useful for generating area styles from the (segmented) 478 * outer ways of a multipolygon. 479 * @return first AreaElement found or {@code null}. 480 */ 481 public static AreaElement getAreaElemStyle(OsmPrimitive p, boolean pretendWayIsClosed) { 482 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock(); 483 try { 484 if (MapPaintStyles.getStyles() == null) 485 return null; 486 for (StyleElement s : MapPaintStyles.getStyles().generateStyles(p, 1.0, pretendWayIsClosed).a) { 487 if (s instanceof AreaElement) 488 return (AreaElement) s; 489 } 490 return null; 491 } finally { 492 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock(); 493 } 494 } 495 496 /** 497 * Determines whether primitive has an AreaElement. 498 * @param p the OSM primitive 499 * @param pretendWayIsClosed For styles that require the way to be closed, 500 * we pretend it is. This is useful for generating area styles from the (segmented) 501 * outer ways of a multipolygon. 502 * @return {@code true} if primitive has an AreaElement 503 */ 504 public static boolean hasAreaElemStyle(OsmPrimitive p, boolean pretendWayIsClosed) { 505 return getAreaElemStyle(p, pretendWayIsClosed) != null; 506 } 507 508 /** 509 * Determines whether primitive has <b>only</b> an AreaElement. 510 * @param p the OSM primitive 511 * @return {@code true} if primitive has only an AreaElement 512 * @since 7486 513 */ 514 public static boolean hasOnlyAreaElemStyle(OsmPrimitive p) { 515 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock(); 516 try { 517 if (MapPaintStyles.getStyles() == null) 518 return false; 519 StyleElementList styles = MapPaintStyles.getStyles().generateStyles(p, 1.0, false).a; 520 if (styles.isEmpty()) { 521 return false; 522 } 523 for (StyleElement s : styles) { 524 if (!(s instanceof AreaElement)) { 525 return false; 526 } 527 } 528 return true; 529 } finally { 530 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock(); 531 } 532 } 533 534 /** 535 * Looks up a preference value and ensures the style cache is invalidated 536 * as soon as this preference value is changed by the user. 537 * 538 * In addition, it adds an intermediate cache for the preference values, 539 * as frequent preference lookup (using <code>Main.pref.get()</code>) for 540 * each primitive can be slow during rendering. 541 * 542 * @param key preference key 543 * @param def default value 544 * @return the corresponding preference value 545 * @see org.openstreetmap.josm.data.Preferences#get(String, String) 546 */ 547 public String getPreferenceCached(String key, String def) { 548 String res; 549 if (preferenceCache.containsKey(key)) { 550 res = preferenceCache.get(key); 551 } else { 552 res = Main.pref.get(key, null); 553 preferenceCache.put(key, res); 554 } 555 return res != null ? res : def; 556 } 557 558 @Override 559 public void preferenceChanged(PreferenceChangeEvent e) { 560 if (preferenceCache.containsKey(e.getKey())) { 561 clearCached(); 562 } 563 } 564}