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