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