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; 012import java.util.Objects; 013import java.util.Optional; 014 015import org.openstreetmap.josm.data.osm.INode; 016import org.openstreetmap.josm.data.osm.IPrimitive; 017import org.openstreetmap.josm.data.osm.IRelation; 018import org.openstreetmap.josm.data.osm.IWay; 019import org.openstreetmap.josm.data.osm.Relation; 020import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; 021import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon; 022import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; 023import org.openstreetmap.josm.data.preferences.NamedColorProperty; 024import org.openstreetmap.josm.gui.MainApplication; 025import org.openstreetmap.josm.gui.NavigatableComponent; 026import org.openstreetmap.josm.gui.layer.OsmDataLayer; 027import org.openstreetmap.josm.gui.mappaint.DividedScale.RangeViolatedError; 028import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource; 029import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement; 030import org.openstreetmap.josm.gui.mappaint.styleelement.AreaIconElement; 031import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement; 032import org.openstreetmap.josm.gui.mappaint.styleelement.DefaultStyles; 033import org.openstreetmap.josm.gui.mappaint.styleelement.LineElement; 034import org.openstreetmap.josm.gui.mappaint.styleelement.NodeElement; 035import org.openstreetmap.josm.gui.mappaint.styleelement.RepeatImageElement; 036import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement; 037import org.openstreetmap.josm.gui.mappaint.styleelement.TextElement; 038import org.openstreetmap.josm.gui.mappaint.styleelement.TextLabel; 039import org.openstreetmap.josm.gui.util.GuiHelper; 040import org.openstreetmap.josm.spi.preferences.Config; 041import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent; 042import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener; 043import org.openstreetmap.josm.tools.ColorHelper; 044import org.openstreetmap.josm.tools.Pair; 045 046/** 047 * Generates a list of {@link StyleElement}s for a primitive, to 048 * be drawn on the map. 049 * There are several steps to derive the list of elements for display: 050 * <ol> 051 * <li>{@link #generateStyles(IPrimitive, double, boolean)} applies the 052 * {@link StyleSource}s one after another to get a key-value map of MapCSS 053 * properties. Then a preliminary set of StyleElements is derived from the 054 * properties map.</li> 055 * <li>{@link #getImpl(IPrimitive, double, NavigatableComponent)} handles the 056 * different forms of multipolygon tagging.</li> 057 * <li>{@link #getStyleCacheWithRange(IPrimitive, double, NavigatableComponent)} 058 * adds a default StyleElement for primitives that would be invisible otherwise. 059 * (For example untagged nodes and ways.)</li> 060 * </ol> 061 * The results are cached with respect to the current scale. 062 * 063 * Use {@link #setStyleSources(Collection)} to select the StyleSources that are applied. 064 */ 065public class ElemStyles implements PreferenceChangedListener { 066 private final List<StyleSource> styleSources = Collections.synchronizedList(new ArrayList<>()); 067 private boolean drawMultipolygon; 068 069 private short cacheIdx = 1; 070 071 private boolean defaultNodes; 072 private boolean defaultLines; 073 074 private short defaultNodesIdx; 075 private short defaultLinesIdx; 076 077 private final Map<String, String> preferenceCache = Collections.synchronizedMap(new HashMap<>()); 078 079 private volatile Color backgroundColorCache; 080 081 /** 082 * Constructs a new {@code ElemStyles}. 083 */ 084 public ElemStyles() { 085 Config.getPref().addPreferenceChangeListener(this); 086 } 087 088 /** 089 * Constructs a new {@code ElemStyles} with specific style sources. This does not listen to preference changes, 090 * and therefore should only be used with layers that have specific drawing requirements. 091 * 092 * @param sources The style sources (these cannot be added to, or removed from) 093 * @since 17862 094 */ 095 public ElemStyles(Collection<StyleSource> sources) { 096 this.styleSources.addAll(sources); 097 } 098 099 /** 100 * Clear the style cache for all primitives of all DataSets. 101 */ 102 public void clearCached() { 103 // run in EDT to make sure this isn't called during rendering run 104 GuiHelper.runInEDT(() -> { 105 cacheIdx++; 106 preferenceCache.clear(); 107 backgroundColorCache = null; 108 MainApplication.getLayerManager().getLayersOfType(OsmDataLayer.class).forEach( 109 dl -> dl.data.clearMappaintCache()); 110 }); 111 } 112 113 /** 114 * Returns the list of style sources. 115 * @return the list of style sources 116 */ 117 public List<StyleSource> getStyleSources() { 118 return Collections.<StyleSource>unmodifiableList(styleSources); 119 } 120 121 /** 122 * Returns the background color. 123 * @return the background color 124 */ 125 public Color getBackgroundColor() { 126 if (backgroundColorCache != null) 127 return backgroundColorCache; 128 for (StyleSource s : styleSources) { 129 if (!s.active) { 130 continue; 131 } 132 Color backgroundColorOverride = s.getBackgroundColorOverride(); 133 if (backgroundColorOverride != null) { 134 backgroundColorCache = backgroundColorOverride; 135 } 136 } 137 return Optional.ofNullable(backgroundColorCache).orElseGet(PaintColors.BACKGROUND::get); 138 } 139 140 /** 141 * Create the list of styles for one primitive. 142 * 143 * @param osm the primitive 144 * @param scale the scale (in meters per 100 pixel) 145 * @param nc display component 146 * @return list of styles 147 * @since 13810 (signature) 148 */ 149 public StyleElementList get(IPrimitive osm, double scale, NavigatableComponent nc) { 150 return getStyleCacheWithRange(osm, scale, nc).a; 151 } 152 153 /** 154 * Create the list of styles and its valid scale range for one primitive. 155 * 156 * Automatically adds default styles in case no proper style was found. 157 * Uses the cache, if possible, and saves the results to the cache. 158 * @param osm OSM primitive 159 * @param scale scale 160 * @param nc navigable component 161 * @return pair containing style list and range 162 * @since 13810 (signature) 163 */ 164 public Pair<StyleElementList, Range> getStyleCacheWithRange(IPrimitive osm, double scale, NavigatableComponent nc) { 165 synchronized (osm.getStyleCacheSyncObject()) { 166 if (!osm.isCachedStyleUpToDate() || scale <= 0) { 167 osm.setCachedStyle(StyleCache.EMPTY_STYLECACHE); 168 } else { 169 Pair<StyleElementList, Range> lst = osm.getCachedStyle().getWithRange(scale, osm.isSelected()); 170 if (lst.a != null) 171 return lst; 172 } 173 Pair<StyleElementList, Range> p = getImpl(osm, scale, nc); 174 if (osm instanceof INode && isDefaultNodes()) { 175 if (p.a.isEmpty()) { 176 if (TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) { 177 p.a = DefaultStyles.DEFAULT_NODE_STYLELIST_TEXT; 178 } else { 179 p.a = DefaultStyles.DEFAULT_NODE_STYLELIST; 180 } 181 } else { 182 boolean hasNonModifier = false; 183 boolean hasText = false; 184 for (StyleElement s : p.a) { 185 if (s instanceof BoxTextElement) { 186 hasText = true; 187 } else { 188 if (!s.isModifier) { 189 hasNonModifier = true; 190 } 191 } 192 } 193 if (!hasNonModifier) { 194 p.a = new StyleElementList(p.a, DefaultStyles.SIMPLE_NODE_ELEMSTYLE); 195 if (!hasText && TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) { 196 p.a = new StyleElementList(p.a, DefaultStyles.SIMPLE_NODE_TEXT_ELEMSTYLE); 197 } 198 } 199 } 200 } else if (osm instanceof IWay && isDefaultLines()) { 201 boolean hasProperLineStyle = false; 202 for (StyleElement s : p.a) { 203 if (s.isProperLineStyle()) { 204 hasProperLineStyle = true; 205 break; 206 } 207 } 208 if (!hasProperLineStyle) { 209 LineElement line = LineElement.UNTAGGED_WAY; 210 for (StyleElement element : p.a) { 211 if (element instanceof AreaElement) { 212 line = LineElement.createSimpleLineStyle(((AreaElement) element).color, true); 213 break; 214 } 215 } 216 p.a = new StyleElementList(p.a, line); 217 } 218 } 219 StyleCache style = osm.getCachedStyle() != null ? osm.getCachedStyle() : StyleCache.EMPTY_STYLECACHE; 220 try { 221 osm.setCachedStyle(style.put(p.a, p.b, osm.isSelected())); 222 } catch (RangeViolatedError e) { 223 throw new AssertionError("Range violated: " + e.getMessage() 224 + " (object: " + osm.getPrimitiveId() + ", current style: " + osm.getCachedStyle() 225 + ", scale: " + scale + ", new stylelist: " + p.a + ", new range: " + p.b + ')', e); 226 } 227 osm.declareCachedStyleUpToDate(); 228 return p; 229 } 230 } 231 232 /** 233 * Create the list of styles and its valid scale range for one primitive. 234 * 235 * This method does multipolygon handling. 236 * 237 * If the primitive is a way, look for multipolygon parents. In case it 238 * is indeed member of some multipolygon as role "outer", all area styles 239 * are removed. (They apply to the multipolygon area.) 240 * Outer ways can have their own independent line styles, e.g. a road as 241 * boundary of a forest. Otherwise, in case, the way does not have an 242 * independent line style, take a line style from the multipolygon. 243 * If the multipolygon does not have a line style either, at least create a 244 * default line style from the color of the area. 245 * 246 * Now consider the case that the way is not an outer way of any multipolygon, 247 * but is member of a multipolygon as "inner". 248 * First, the style list is regenerated, considering only tags of this way. 249 * Then check, if the way describes something in its own right. (linear feature 250 * or area) If not, add a default line style from the area color of the multipolygon. 251 * 252 * @param osm OSM primitive 253 * @param scale scale 254 * @param nc navigable component 255 * @return pair containing style list and range 256 */ 257 private Pair<StyleElementList, Range> getImpl(IPrimitive osm, double scale, NavigatableComponent nc) { 258 if (osm instanceof INode) 259 return generateStyles(osm, scale, false); 260 else if (osm instanceof IWay) { 261 Pair<StyleElementList, Range> p = generateStyles(osm, scale, false); 262 263 boolean isOuterWayOfSomeMP = false; 264 Color wayColor = null; 265 266 // FIXME: Maybe in the future outer way styles apply to outers ignoring the multipolygon? 267 for (IPrimitive referrer : osm.getReferrers()) { 268 IRelation<?> r = (IRelation<?>) referrer; 269 if (!drawMultipolygon || !r.isMultipolygon() || !r.isUsable() || !(r instanceof Relation)) { 270 continue; 271 } 272 Multipolygon multipolygon = MultipolygonCache.getInstance().get((Relation) r); 273 274 if (multipolygon.getOuterWays().contains(osm)) { 275 boolean hasIndependentLineStyle = false; 276 if (!isOuterWayOfSomeMP) { // do this only one time 277 List<StyleElement> tmp = new ArrayList<>(p.a.size()); 278 for (StyleElement s : p.a) { 279 if (s instanceof AreaElement) { 280 wayColor = ((AreaElement) s).color; 281 } else { 282 tmp.add(s); 283 if (s.isProperLineStyle()) { 284 hasIndependentLineStyle = true; 285 } 286 } 287 } 288 p.a = new StyleElementList(tmp); 289 isOuterWayOfSomeMP = true; 290 } 291 292 if (!hasIndependentLineStyle) { 293 Pair<StyleElementList, Range> mpElemStyles; 294 synchronized (r) { 295 mpElemStyles = getStyleCacheWithRange(r, scale, nc); 296 } 297 StyleElement mpLine = null; 298 for (StyleElement s : mpElemStyles.a) { 299 if (s.isProperLineStyle()) { 300 mpLine = s; 301 break; 302 } 303 } 304 p.b = Range.cut(p.b, mpElemStyles.b); 305 if (mpLine != null) { 306 p.a = new StyleElementList(p.a, mpLine); 307 break; 308 } else if (wayColor == null && isDefaultLines()) { 309 for (StyleElement element : mpElemStyles.a) { 310 if (element instanceof AreaElement) { 311 wayColor = ((AreaElement) element).color; 312 break; 313 } 314 } 315 } 316 } 317 } 318 } 319 if (isOuterWayOfSomeMP) { 320 if (isDefaultLines()) { 321 boolean hasLineStyle = false; 322 for (StyleElement s : p.a) { 323 if (s.isProperLineStyle()) { 324 hasLineStyle = true; 325 break; 326 } 327 } 328 if (!hasLineStyle) { 329 p.a = new StyleElementList(p.a, LineElement.createSimpleLineStyle(wayColor, true)); 330 } 331 } 332 return p; 333 } 334 335 if (!isDefaultLines()) return p; 336 337 for (IPrimitive referrer : osm.getReferrers()) { 338 IRelation<?> ref = (IRelation<?>) referrer; 339 if (!drawMultipolygon || !ref.isMultipolygon() || !ref.isUsable() || !(ref instanceof Relation)) { 340 continue; 341 } 342 final Multipolygon multipolygon = MultipolygonCache.getInstance().get((Relation) ref); 343 344 if (multipolygon.getInnerWays().contains(osm)) { 345 p = generateStyles(osm, scale, false); 346 boolean hasIndependentElemStyle = false; 347 for (StyleElement s : p.a) { 348 if (s.isProperLineStyle() || s instanceof AreaElement) { 349 hasIndependentElemStyle = true; 350 break; 351 } 352 } 353 if (!hasIndependentElemStyle && !multipolygon.getOuterWays().isEmpty()) { 354 Color mpColor = null; 355 StyleElementList mpElemStyles; 356 synchronized (ref) { 357 mpElemStyles = get(ref, scale, nc); 358 } 359 for (StyleElement mpS : mpElemStyles) { 360 if (mpS instanceof AreaElement) { 361 mpColor = ((AreaElement) mpS).color; 362 break; 363 } 364 } 365 p.a = new StyleElementList(p.a, LineElement.createSimpleLineStyle(mpColor, true)); 366 } 367 return p; 368 } 369 } 370 return p; 371 } else if (osm instanceof IRelation) { 372 return generateStyles(osm, scale, true); 373 } 374 return null; 375 } 376 377 /** 378 * Create the list of styles and its valid scale range for one primitive. 379 * 380 * Loops over the list of style sources, to generate the map of properties. 381 * From these properties, it generates the different types of styles. 382 * 383 * @param osm the primitive to create styles for 384 * @param scale the scale (in meters per 100 px), must be > 0 385 * @param pretendWayIsClosed For styles that require the way to be closed, 386 * we pretend it is. This is useful for generating area styles from the (segmented) 387 * outer ways of a multipolygon. 388 * @return the generated styles and the valid range as a pair 389 * @since 13810 (signature) 390 */ 391 public Pair<StyleElementList, Range> generateStyles(IPrimitive osm, double scale, boolean pretendWayIsClosed) { 392 List<StyleElement> sl = new ArrayList<>(); 393 MultiCascade mc = new MultiCascade(); 394 Environment env = new Environment(osm, mc, null, null); 395 396 for (StyleSource s : styleSources) { 397 if (s.active) { 398 s.apply(mc, osm, scale, pretendWayIsClosed); 399 } 400 } 401 402 for (Entry<String, Cascade> e : mc.getLayers()) { 403 if ("*".equals(e.getKey())) { 404 continue; 405 } 406 env.layer = e.getKey(); 407 if (osm instanceof IWay) { 408 AreaElement areaStyle = AreaElement.create(env); 409 addIfNotNull(sl, areaStyle); 410 addIfNotNull(sl, RepeatImageElement.create(env)); 411 addIfNotNull(sl, LineElement.createLine(env)); 412 addIfNotNull(sl, LineElement.createLeftCasing(env)); 413 addIfNotNull(sl, LineElement.createRightCasing(env)); 414 addIfNotNull(sl, LineElement.createCasing(env)); 415 addIfNotNull(sl, AreaIconElement.create(env)); 416 addIfNotNull(sl, TextElement.create(env)); 417 if (areaStyle != null) { 418 //TODO: Warn about this, or even remove it completely 419 addIfNotNull(sl, TextElement.createForContent(env)); 420 } 421 } else if (osm instanceof INode) { 422 NodeElement nodeStyle = NodeElement.create(env); 423 if (nodeStyle != null) { 424 sl.add(nodeStyle); 425 addIfNotNull(sl, BoxTextElement.create(env, nodeStyle.getBoxProvider())); 426 } else { 427 addIfNotNull(sl, BoxTextElement.create(env, DefaultStyles.SIMPLE_NODE_ELEMSTYLE_BOXPROVIDER)); 428 } 429 } else if (osm instanceof IRelation) { 430 if (((IRelation<?>) osm).isMultipolygon()) { 431 AreaElement areaStyle = AreaElement.create(env); 432 addIfNotNull(sl, areaStyle); 433 addIfNotNull(sl, RepeatImageElement.create(env)); 434 addIfNotNull(sl, LineElement.createLine(env)); 435 addIfNotNull(sl, LineElement.createCasing(env)); 436 addIfNotNull(sl, AreaIconElement.create(env)); 437 addIfNotNull(sl, TextElement.create(env)); 438 if (areaStyle != null) { 439 //TODO: Warn about this, or even remove it completely 440 addIfNotNull(sl, TextElement.createForContent(env)); 441 } 442 } else if (osm.hasTag("type", "restriction")) { 443 addIfNotNull(sl, NodeElement.create(env)); 444 } 445 } 446 } 447 return new Pair<>(new StyleElementList(sl), mc.range); 448 } 449 450 private static <T> void addIfNotNull(List<T> list, T obj) { 451 if (obj != null) { 452 list.add(obj); 453 } 454 } 455 456 /** 457 * Draw a default node symbol for nodes that have no style? 458 * @return {@code true} if default node symbol must be drawn 459 */ 460 private boolean isDefaultNodes() { 461 if (defaultNodesIdx == cacheIdx) 462 return defaultNodes; 463 defaultNodes = fromCanvas("default-points", Boolean.TRUE, Boolean.class); 464 defaultNodesIdx = cacheIdx; 465 return defaultNodes; 466 } 467 468 /** 469 * Draw a default line for ways that do not have an own line style? 470 * @return {@code true} if default line must be drawn 471 */ 472 private boolean isDefaultLines() { 473 if (defaultLinesIdx == cacheIdx) 474 return defaultLines; 475 defaultLines = fromCanvas("default-lines", Boolean.TRUE, Boolean.class); 476 defaultLinesIdx = cacheIdx; 477 return defaultLines; 478 } 479 480 private <T> T fromCanvas(String key, T def, Class<T> c) { 481 MultiCascade mc = new MultiCascade(); 482 Relation r = new Relation(); 483 r.put("#canvas", "query"); 484 485 for (StyleSource s : styleSources) { 486 if (s.active) { 487 s.apply(mc, r, 1, false); 488 } 489 } 490 return mc.getCascade("default").get(key, def, c); 491 } 492 493 /** 494 * Determines whether multipolygons must be drawn. 495 * @return whether multipolygons must be drawn. 496 */ 497 public boolean isDrawMultipolygon() { 498 return drawMultipolygon; 499 } 500 501 /** 502 * Sets whether multipolygons must be drawn. 503 * @param drawMultipolygon whether multipolygons must be drawn 504 */ 505 public void setDrawMultipolygon(boolean drawMultipolygon) { 506 this.drawMultipolygon = drawMultipolygon; 507 } 508 509 /** 510 * remove all style sources; only accessed from MapPaintStyles 511 */ 512 void clear() { 513 styleSources.clear(); 514 } 515 516 /** 517 * add a style source; only accessed from MapPaintStyles 518 * @param style style source to add 519 */ 520 void add(StyleSource style) { 521 styleSources.add(Objects.requireNonNull(style)); 522 } 523 524 /** 525 * remove a style source; only accessed from MapPaintStyles 526 * @param style style source to remove 527 * @return {@code true} if this list contained the specified element 528 */ 529 boolean remove(StyleSource style) { 530 return styleSources.remove(Objects.requireNonNull(style)); 531 } 532 533 /** 534 * set the style sources; only accessed from MapPaintStyles 535 * @param sources new style sources 536 */ 537 void setStyleSources(Collection<StyleSource> sources) { 538 styleSources.clear(); 539 sources.forEach(this::add); 540 } 541 542 /** 543 * Returns the first AreaElement for a given primitive. 544 * @param p the OSM primitive 545 * @param pretendWayIsClosed For styles that require the way to be closed, 546 * we pretend it is. This is useful for generating area styles from the (segmented) 547 * outer ways of a multipolygon. 548 * @return first AreaElement found or {@code null}. 549 * @since 13810 (signature) 550 */ 551 public static AreaElement getAreaElemStyle(IPrimitive p, boolean pretendWayIsClosed) { 552 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock(); 553 try { 554 if (MapPaintStyles.getStyles() == null) 555 return null; 556 for (StyleElement s : MapPaintStyles.getStyles().generateStyles(p, 1.0, pretendWayIsClosed).a) { 557 if (s instanceof AreaElement) 558 return (AreaElement) s; 559 } 560 return null; 561 } finally { 562 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock(); 563 } 564 } 565 566 /** 567 * Determines whether primitive has an AreaElement. 568 * @param p the OSM primitive 569 * @param pretendWayIsClosed For styles that require the way to be closed, 570 * we pretend it is. This is useful for generating area styles from the (segmented) 571 * outer ways of a multipolygon. 572 * @return {@code true} if primitive has an AreaElement 573 * @since 13810 (signature) 574 */ 575 public static boolean hasAreaElemStyle(IPrimitive p, boolean pretendWayIsClosed) { 576 return getAreaElemStyle(p, pretendWayIsClosed) != null; 577 } 578 579 /** 580 * Determines whether primitive has area-type {@link StyleElement}s, but 581 * no line-type StyleElements. 582 * 583 * {@link TextElement} is ignored, as it can be both line and area-type. 584 * @param p the OSM primitive 585 * @return {@code true} if primitive has area elements, but no line elements 586 * @since 12700 587 * @since 13810 (signature) 588 */ 589 public static boolean hasOnlyAreaElements(IPrimitive p) { 590 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock(); 591 try { 592 if (MapPaintStyles.getStyles() == null) 593 return false; 594 StyleElementList styles = MapPaintStyles.getStyles().generateStyles(p, 1.0, false).a; 595 boolean hasAreaElement = false; 596 for (StyleElement s : styles) { 597 if (s instanceof TextElement) { 598 continue; 599 } 600 if (s instanceof AreaElement) { 601 hasAreaElement = true; 602 } else { 603 return false; 604 } 605 } 606 return hasAreaElement; 607 } finally { 608 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock(); 609 } 610 } 611 612 /** 613 * Looks up a preference value and ensures the style cache is invalidated 614 * as soon as this preference value is changed by the user. 615 * 616 * In addition, it adds an intermediate cache for the preference values, 617 * as frequent preference lookup (using <code>Config.getPref().get()</code>) for 618 * each primitive can be slow during rendering. 619 * 620 * If the default value can be {@linkplain Cascade#convertTo converted} to a {@link Color}, 621 * the {@link NamedColorProperty} is retrieved as string. 622 * 623 * @param source style source 624 * @param key preference key 625 * @param def default value 626 * @return the corresponding preference value 627 * @see org.openstreetmap.josm.data.Preferences#get(String, String) 628 */ 629 public String getPreferenceCached(StyleSource source, String key, String def) { 630 String res; 631 if (preferenceCache.containsKey(key)) { 632 res = preferenceCache.get(key); 633 } else { 634 Color realDef = Cascade.convertTo(def, Color.class); 635 if (realDef != null) { 636 String prefName = source != null ? source.getFileNamePart() : "unknown"; 637 NamedColorProperty property = new NamedColorProperty(NamedColorProperty.COLOR_CATEGORY_MAPPAINT, prefName, key, realDef); 638 res = ColorHelper.color2html(property.get()); 639 } else { 640 res = Config.getPref().get(key, null); 641 } 642 preferenceCache.put(key, res); 643 } 644 return res != null ? res : def; 645 } 646 647 @Override 648 public void preferenceChanged(PreferenceChangeEvent e) { 649 if (preferenceCache.containsKey(e.getKey())) { 650 clearCached(); 651 } 652 } 653}