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