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