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).setSize(width, 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 Main.debug(e); 332 HelpAwareOptionPane.showOptionDialog(Main.parent, msg, tr("Warning"), JOptionPane.WARNING_MESSAGE, 333 HelpUtil.ht("/Styles/MapCSSImplementation")); 334 } 335 return null; 336 } 337 338 /** 339 * reload styles 340 * preferences are the same, but the file source may have changed 341 * @param sel the indices of styles to reload 342 */ 343 public static void reloadStyles(final int... sel) { 344 List<StyleSource> toReload = new ArrayList<>(); 345 List<StyleSource> data = styles.getStyleSources(); 346 for (int i : sel) { 347 toReload.add(data.get(i)); 348 } 349 Main.worker.submit(new MapPaintStyleLoader(toReload)); 350 } 351 352 public static class MapPaintStyleLoader extends PleaseWaitRunnable { 353 private boolean canceled; 354 private final Collection<StyleSource> sources; 355 356 public MapPaintStyleLoader(Collection<StyleSource> sources) { 357 super(tr("Reloading style sources")); 358 this.sources = sources; 359 } 360 361 @Override 362 protected void cancel() { 363 canceled = true; 364 } 365 366 @Override 367 protected void finish() { 368 SwingUtilities.invokeLater(new Runnable() { 369 @Override 370 public void run() { 371 fireMapPaintSylesUpdated(); 372 styles.clearCached(); 373 if (Main.isDisplayingMapView()) { 374 Main.map.mapView.preferenceChanged(null); 375 Main.map.mapView.repaint(); 376 } 377 } 378 }); 379 } 380 381 @Override 382 protected void realRun() { 383 ProgressMonitor monitor = getProgressMonitor(); 384 monitor.setTicksCount(sources.size()); 385 for (StyleSource s : sources) { 386 if (canceled) 387 return; 388 monitor.subTask(tr("loading style ''{0}''...", s.getDisplayString())); 389 s.loadStyleSource(); 390 monitor.worked(1); 391 } 392 } 393 } 394 395 /** 396 * Move position of entries in the current list of StyleSources 397 * @param sel The indices of styles to be moved. 398 * @param delta The number of lines it should move. positive int moves 399 * down and negative moves up. 400 */ 401 public static void moveStyles(int[] sel, int delta) { 402 if (!canMoveStyles(sel, delta)) 403 return; 404 int[] selSorted = Utils.copyArray(sel); 405 Arrays.sort(selSorted); 406 List<StyleSource> data = new ArrayList<>(styles.getStyleSources()); 407 for (int row: selSorted) { 408 StyleSource t1 = data.get(row); 409 StyleSource t2 = data.get(row + delta); 410 data.set(row, t2); 411 data.set(row + delta, t1); 412 } 413 styles.setStyleSources(data); 414 MapPaintPrefHelper.INSTANCE.put(data); 415 fireMapPaintSylesUpdated(); 416 styles.clearCached(); 417 Main.map.mapView.repaint(); 418 } 419 420 public static boolean canMoveStyles(int[] sel, int i) { 421 if (sel.length == 0) 422 return false; 423 int[] selSorted = Utils.copyArray(sel); 424 Arrays.sort(selSorted); 425 426 if (i < 0) // Up 427 return selSorted[0] >= -i; 428 else if (i > 0) // Down 429 return selSorted[selSorted.length-1] <= styles.getStyleSources().size() - 1 - i; 430 else 431 return true; 432 } 433 434 public static void toggleStyleActive(int... sel) { 435 List<StyleSource> data = styles.getStyleSources(); 436 for (int p : sel) { 437 StyleSource s = data.get(p); 438 s.active = !s.active; 439 } 440 MapPaintPrefHelper.INSTANCE.put(data); 441 if (sel.length == 1) { 442 fireMapPaintStyleEntryUpdated(sel[0]); 443 } else { 444 fireMapPaintSylesUpdated(); 445 } 446 styles.clearCached(); 447 Main.map.mapView.repaint(); 448 } 449 450 /** 451 * Add a new map paint style. 452 * @param entry map paint style 453 * @return loaded style source, or {@code null} 454 */ 455 public static StyleSource addStyle(SourceEntry entry) { 456 StyleSource source = fromSourceEntry(entry); 457 if (source != null) { 458 styles.add(source); 459 loadStyleForFirstTime(source); 460 MapPaintPrefHelper.INSTANCE.put(styles.getStyleSources()); 461 fireMapPaintSylesUpdated(); 462 styles.clearCached(); 463 if (Main.isDisplayingMapView()) { 464 Main.map.mapView.repaint(); 465 } 466 } 467 return source; 468 } 469 470 /*********************************** 471 * MapPaintSylesUpdateListener & related code 472 * (get informed when the list of MapPaint StyleSources changes) 473 */ 474 475 public interface MapPaintSylesUpdateListener { 476 void mapPaintStylesUpdated(); 477 478 void mapPaintStyleEntryUpdated(int idx); 479 } 480 481 private static final CopyOnWriteArrayList<MapPaintSylesUpdateListener> listeners 482 = new CopyOnWriteArrayList<>(); 483 484 public static void addMapPaintSylesUpdateListener(MapPaintSylesUpdateListener listener) { 485 if (listener != null) { 486 listeners.addIfAbsent(listener); 487 } 488 } 489 490 public static void removeMapPaintSylesUpdateListener(MapPaintSylesUpdateListener listener) { 491 listeners.remove(listener); 492 } 493 494 public static void fireMapPaintSylesUpdated() { 495 for (MapPaintSylesUpdateListener l : listeners) { 496 l.mapPaintStylesUpdated(); 497 } 498 } 499 500 public static void fireMapPaintStyleEntryUpdated(int idx) { 501 for (MapPaintSylesUpdateListener l : listeners) { 502 l.mapPaintStyleEntryUpdated(idx); 503 } 504 } 505}