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