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