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