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