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