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