001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.mappaint; 003 004import java.io.File; 005import java.io.IOException; 006import java.util.ArrayList; 007import java.util.Arrays; 008import java.util.Collection; 009import java.util.LinkedList; 010import java.util.List; 011 012import javax.swing.ImageIcon; 013import javax.swing.SwingUtilities; 014 015import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 016import org.openstreetmap.josm.data.osm.Tag; 017import org.openstreetmap.josm.data.preferences.sources.MapPaintPrefHelper; 018import org.openstreetmap.josm.data.preferences.sources.SourceEntry; 019import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource; 020import org.openstreetmap.josm.io.CachedFile; 021import org.openstreetmap.josm.io.FileWatcher; 022import org.openstreetmap.josm.spi.preferences.Config; 023import org.openstreetmap.josm.spi.preferences.IPreferences; 024import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent; 025import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener; 026import org.openstreetmap.josm.tools.ImageProvider; 027import org.openstreetmap.josm.tools.ListenerList; 028import org.openstreetmap.josm.tools.Logging; 029import org.openstreetmap.josm.tools.OsmPrimitiveImageProvider; 030import org.openstreetmap.josm.tools.Stopwatch; 031import org.openstreetmap.josm.tools.Utils; 032 033/** 034 * This class manages the list of available map paint styles and gives access to 035 * the ElemStyles singleton. 036 * 037 * On change, {@link MapPaintStylesUpdateListener#mapPaintStylesUpdated()} is fired 038 * for all listeners. 039 */ 040public final class MapPaintStyles { 041 042 private static final Collection<String> DEPRECATED_IMAGE_NAMES = Arrays.asList( 043 "presets/misc/deprecated.svg", 044 "misc/deprecated.png"); 045 046 private static final ListenerList<MapPaintStylesUpdateListener> listeners = ListenerList.createUnchecked(); 047 048 private static final class MapPaintStylesPreferenceListener implements PreferenceChangedListener { 049 private final IPreferences pref; 050 /** Preferences to ignore (i.e., if they change, don't reload) */ 051 private final List<String> preferenceIgnoreList; 052 053 MapPaintStylesPreferenceListener(IPreferences pref) { 054 this.pref = pref; 055 this.preferenceIgnoreList = Arrays.asList("mappaint.style.entries", "mappaint.style.known-defaults", 056 "mappaint.renderer-class-name"); 057 } 058 059 @Override 060 public void preferenceChanged(PreferenceChangeEvent e) { 061 if (e.getKey().contains("mappaint") && !this.preferenceIgnoreList.contains(e.getKey())) { 062 // We need to remove this from the listeners, so that we don't recursively call ourselves. 063 pref.removePreferenceChangeListener(this); 064 MapPaintStyles.readFromPreferences(); 065 pref.addPreferenceChangeListener(this); 066 } 067 } 068 } 069 070 static { 071 listeners.addListener(new MapPaintStylesUpdateListener() { 072 @Override 073 public void mapPaintStylesUpdated() { 074 SwingUtilities.invokeLater(styles::clearCached); 075 } 076 077 @Override 078 public void mapPaintStyleEntryUpdated(int index) { 079 mapPaintStylesUpdated(); 080 } 081 }); 082 Config.getPref().addPreferenceChangeListener(new MapPaintStylesPreferenceListener(Config.getPref())); 083 } 084 085 private static final ElemStyles styles = new ElemStyles(); 086 087 /** 088 * Returns the {@link ElemStyles} singleton instance. 089 * 090 * The returned object is read only, any manipulation happens via one of 091 * the other wrapper methods in this class. ({@link #readFromPreferences}, 092 * {@link #moveStyles}, ...) 093 * @return the {@code ElemStyles} singleton instance 094 */ 095 public static ElemStyles getStyles() { 096 return styles; 097 } 098 099 private MapPaintStyles() { 100 // Hide default constructor for utils classes 101 } 102 103 /** 104 * Value holder for a reference to a tag name. A style instruction 105 * <pre> 106 * text: a_tag_name; 107 * </pre> 108 * results in a tag reference for the tag <code>a_tag_name</code> in the 109 * style cascade. 110 */ 111 public static class TagKeyReference { 112 /** 113 * The tag name 114 */ 115 public final String key; 116 117 /** 118 * Create a new {@link TagKeyReference} 119 * @param key The tag name 120 */ 121 public TagKeyReference(String key) { 122 this.key = key.intern(); 123 } 124 125 @Override 126 public String toString() { 127 return "TagKeyReference{" + "key='" + key + "'}"; 128 } 129 } 130 131 /** 132 * IconReference is used to remember the associated style source for each icon URL. 133 * This is necessary because image URLs can be paths relative 134 * to the source file and we have cascading of properties from different source files. 135 */ 136 public static class IconReference { 137 138 /** 139 * The name of the icon 140 */ 141 public final String iconName; 142 /** 143 * The style source this reference occurred in 144 */ 145 public final StyleSource source; 146 147 /** 148 * Create a new {@link IconReference} 149 * @param iconName The icon name 150 * @param source The current style source 151 */ 152 public IconReference(String iconName, StyleSource source) { 153 this.iconName = iconName; 154 this.source = source; 155 } 156 157 @Override 158 public String toString() { 159 return "IconReference{" + "iconName='" + iconName + "' source='" + source.getDisplayString() + "'}"; 160 } 161 162 /** 163 * Determines whether this icon represents a deprecated icon 164 * @return whether this icon represents a deprecated icon 165 * @since 10927 166 */ 167 public boolean isDeprecatedIcon() { 168 return DEPRECATED_IMAGE_NAMES.contains(iconName); 169 } 170 } 171 172 /** 173 * Image provider for icon. Note that this is a provider only. A {@link ImageProvider#get()} call may still fail! 174 * 175 * @param ref reference to the requested icon 176 * @param test if <code>true</code> than the icon is request is tested 177 * @return image provider for icon (can be <code>null</code> when <code>test</code> is <code>true</code>). 178 * @see #getIcon(IconReference, int,int) 179 * @since 8097 180 */ 181 public static ImageProvider getIconProvider(IconReference ref, boolean test) { 182 final String namespace = ref.source.getPrefName(); 183 ImageProvider i = new ImageProvider(ref.iconName) 184 .setDirs(getIconSourceDirs(ref.source)) 185 .setId("mappaint."+namespace) 186 .setArchive(ref.source.zipIcons) 187 .setInArchiveDir(ref.source.getZipEntryDirName()) 188 .setOptional(true); 189 if (test && i.get() == null) { 190 String msg = "Mappaint style \""+namespace+"\" ("+ref.source.getDisplayString()+") icon \"" + ref.iconName + "\" not found."; 191 ref.source.logWarning(msg); 192 Logging.warn(msg); 193 return null; 194 } 195 return i; 196 } 197 198 /** 199 * Return scaled icon. 200 * 201 * @param ref reference to the requested icon 202 * @param width icon width or -1 for autoscale 203 * @param height icon height or -1 for autoscale 204 * @return image icon or <code>null</code>. 205 * @see #getIconProvider(IconReference, boolean) 206 */ 207 public static ImageIcon getIcon(IconReference ref, int width, int height) { 208 final String namespace = ref.source.getPrefName(); 209 ImageIcon i = getIconProvider(ref, false).setSize(width, height).get(); 210 if (i == null) { 211 Logging.warn("Mappaint style \""+namespace+"\" ("+ref.source.getDisplayString()+") icon \"" + ref.iconName + "\" not found."); 212 return null; 213 } 214 return i; 215 } 216 217 /** 218 * No icon with the given name was found, show a dummy icon instead 219 * @param source style source 220 * @return the icon misc/no_icon.png, in descending priority: 221 * - relative to source file 222 * - from user icon paths 223 * - josm's default icon 224 * can be null if the defaults are turned off by user 225 */ 226 public static ImageIcon getNoIconIcon(StyleSource source) { 227 return new ImageProvider("presets/misc/no_icon") 228 .setDirs(getIconSourceDirs(source)) 229 .setId("mappaint."+source.getPrefName()) 230 .setArchive(source.zipIcons) 231 .setInArchiveDir(source.getZipEntryDirName()) 232 .setOptional(true).get(); 233 } 234 235 /** 236 * Returns the node icon that would be displayed for the given tag. 237 * @param tag The tag to look an icon for 238 * @return {@code null} if no icon found 239 * @deprecated use {@link OsmPrimitiveImageProvider#getResource} 240 */ 241 @Deprecated 242 public static ImageIcon getNodeIcon(Tag tag) { 243 if (tag != null) { 244 return OsmPrimitiveImageProvider.getResource(tag.getKey(), tag.getValue(), OsmPrimitiveType.NODE) 245 .map(resource -> resource.getPaddedIcon(ImageProvider.ImageSizes.SMALLICON.getImageDimension())) 246 .orElse(null); 247 } 248 return null; 249 } 250 251 /** 252 * Gets the directories that should be searched for icons 253 * @param source The style source the icon is from 254 * @return A list of directory names 255 */ 256 public static List<String> getIconSourceDirs(StyleSource source) { 257 List<String> dirs = new LinkedList<>(); 258 259 File sourceDir = source.getLocalSourceDir(); 260 if (sourceDir != null) { 261 dirs.add(sourceDir.getPath()); 262 } 263 264 Collection<String> prefIconDirs = Config.getPref().getList("mappaint.icon.sources"); 265 for (String fileset : prefIconDirs) { 266 String[] a; 267 if (fileset.indexOf('=') >= 0) { 268 a = fileset.split("=", 2); 269 } else { 270 a = new String[] {"", fileset}; 271 } 272 273 /* non-prefixed path is generic path, always take it */ 274 if (a[0].isEmpty() || source.getPrefName().equals(a[0])) { 275 dirs.add(a[1]); 276 } 277 } 278 279 if (Config.getPref().getBoolean("mappaint.icon.enable-defaults", true)) { 280 /* don't prefix icon path, as it should be generic */ 281 dirs.add("resource://images/"); 282 } 283 284 return dirs; 285 } 286 287 /** 288 * Reloads all styles from the preferences. 289 */ 290 public static void readFromPreferences() { 291 styles.clear(); 292 293 Collection<? extends SourceEntry> sourceEntries = MapPaintPrefHelper.INSTANCE.get(); 294 295 for (SourceEntry entry : sourceEntries) { 296 try { 297 styles.add(fromSourceEntry(entry)); 298 } catch (IllegalArgumentException e) { 299 Logging.error("Failed to load map paint style {0}", entry); 300 Logging.error(e); 301 } 302 } 303 for (StyleSource source : styles.getStyleSources()) { 304 if (source.active) { 305 loadStyleForFirstTime(source); 306 } else { 307 source.loadStyleSource(true); 308 } 309 } 310 fireMapPaintStylesUpdated(); 311 } 312 313 private static void loadStyleForFirstTime(StyleSource source) { 314 final Stopwatch stopwatch = Stopwatch.createStarted(); 315 source.loadStyleSource(); 316 if (Config.getPref().getBoolean("mappaint.auto_reload_local_styles", true) && source.isLocal()) { 317 try { 318 FileWatcher.getDefaultInstance().registerSource(source); 319 } catch (IOException | IllegalStateException | IllegalArgumentException e) { 320 Logging.error(e); 321 } 322 } 323 if (Logging.isDebugEnabled() || !source.isValid()) { 324 String message = stopwatch.toString("Initializing map style " + source.url); 325 if (!source.isValid()) { 326 Logging.warn(message + " (" + source.getErrors().size() + " errors, " + source.getWarnings().size() + " warnings)"); 327 } else { 328 Logging.debug(message); 329 } 330 } 331 } 332 333 private static StyleSource fromSourceEntry(SourceEntry entry) { 334 if (entry.url == null && entry instanceof MapCSSStyleSource) { 335 return (MapCSSStyleSource) entry; 336 } 337 try (CachedFile cf = new CachedFile(entry.url).setHttpAccept(MapCSSStyleSource.MAPCSS_STYLE_MIME_TYPES)) { 338 String zipEntryPath = cf.findZipEntryPath("mapcss", "style"); 339 if (zipEntryPath != null) { 340 entry.isZip = true; 341 entry.zipEntryPath = zipEntryPath; 342 } 343 return new MapCSSStyleSource(entry); 344 } 345 } 346 347 /** 348 * Move position of entries in the current list of StyleSources 349 * @param sel The indices of styles to be moved. 350 * @param delta The number of lines it should move. positive int moves 351 * down and negative moves up. 352 */ 353 public static void moveStyles(int[] sel, int delta) { 354 if (!canMoveStyles(sel, delta)) 355 return; 356 int[] selSorted = Utils.copyArray(sel); 357 Arrays.sort(selSorted); 358 List<StyleSource> data = new ArrayList<>(styles.getStyleSources()); 359 for (int row: selSorted) { 360 StyleSource t1 = data.get(row); 361 StyleSource t2 = data.get(row + delta); 362 data.set(row, t2); 363 data.set(row + delta, t1); 364 } 365 styles.setStyleSources(data); 366 MapPaintPrefHelper.INSTANCE.put(data); 367 fireMapPaintStylesUpdated(); 368 } 369 370 /** 371 * Check if the styles can be moved 372 * @param sel The indexes of the selected styles 373 * @param i The number of places to move the styles 374 * @return <code>true</code> if that movement is possible 375 */ 376 public static boolean canMoveStyles(int[] sel, int i) { 377 if (sel.length == 0) 378 return false; 379 int[] selSorted = Utils.copyArray(sel); 380 Arrays.sort(selSorted); 381 382 if (i < 0) // Up 383 return selSorted[0] >= -i; 384 else if (i > 0) // Down 385 return selSorted[selSorted.length-1] <= styles.getStyleSources().size() - 1 - i; 386 else 387 return true; 388 } 389 390 /** 391 * Toggles the active state of several styles 392 * @param sel The style indexes 393 */ 394 public static void toggleStyleActive(int... sel) { 395 List<StyleSource> data = styles.getStyleSources(); 396 for (int p : sel) { 397 StyleSource s = data.get(p); 398 s.active = !s.active; 399 if (s.active && !s.isLoaded()) { 400 loadStyleForFirstTime(s); 401 } 402 } 403 MapPaintPrefHelper.INSTANCE.put(data); 404 if (sel.length == 1) { 405 fireMapPaintStyleEntryUpdated(sel[0]); 406 } else { 407 fireMapPaintStylesUpdated(); 408 } 409 } 410 411 /** 412 * Add a new map paint style. 413 * @param entry map paint style 414 * @return loaded style source 415 */ 416 public static StyleSource addStyle(SourceEntry entry) { 417 StyleSource source = fromSourceEntry(entry); 418 styles.add(source); 419 loadStyleForFirstTime(source); 420 refreshStyles(); 421 return source; 422 } 423 424 /** 425 * Remove a map paint style. 426 * @param entry map paint style 427 * @since 11493 428 */ 429 public static void removeStyle(SourceEntry entry) { 430 StyleSource source = fromSourceEntry(entry); 431 if (styles.remove(source)) { 432 refreshStyles(); 433 } 434 } 435 436 private static void refreshStyles() { 437 MapPaintPrefHelper.INSTANCE.put(styles.getStyleSources()); 438 fireMapPaintStylesUpdated(); 439 } 440 441 /*********************************** 442 * MapPaintStylesUpdateListener & related code 443 * (get informed when the list of MapPaint StyleSources changes) 444 */ 445 public interface MapPaintStylesUpdateListener { 446 /** 447 * Called on any style source changes that are not handled by {@link #mapPaintStyleEntryUpdated(int)} 448 */ 449 void mapPaintStylesUpdated(); 450 451 /** 452 * Called whenever a single style source entry was changed. 453 * @param index The index of the entry. 454 */ 455 void mapPaintStyleEntryUpdated(int index); 456 } 457 458 /** 459 * Add a listener that listens to global style changes. 460 * @param listener The listener 461 */ 462 public static void addMapPaintStylesUpdateListener(MapPaintStylesUpdateListener listener) { 463 listeners.addListener(listener); 464 } 465 466 /** 467 * Removes a listener that listens to global style changes. 468 * @param listener The listener 469 */ 470 public static void removeMapPaintStylesUpdateListener(MapPaintStylesUpdateListener listener) { 471 listeners.removeListener(listener); 472 } 473 474 /** 475 * Notifies all listeners that there was any update to the map paint styles 476 */ 477 public static void fireMapPaintStylesUpdated() { 478 listeners.fireEvent(MapPaintStylesUpdateListener::mapPaintStylesUpdated); 479 } 480 481 /** 482 * Notifies all listeners that there was an update to a specific map paint style 483 * @param index The style index 484 */ 485 public static void fireMapPaintStyleEntryUpdated(int index) { 486 listeners.fireEvent(l -> l.mapPaintStyleEntryUpdated(index)); 487 } 488}