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 public static ImageIcon getIcon(IconReference ref, int width, int height) { 105 final String namespace = ref.source.getPrefName(); 106 ImageIcon i = new ImageProvider(ref.iconName) 107 .setDirs(getIconSourceDirs(ref.source)) 108 .setId("mappaint."+namespace) 109 .setArchive(ref.source.zipIcons) 110 .setInArchiveDir(ref.source.getZipEntryDirName()) 111 .setWidth(width) 112 .setHeight(height) 113 .setOptional(true).get(); 114 if (i == null) { 115 Main.warn("Mappaint style \""+namespace+"\" ("+ref.source.getDisplayString()+") icon \"" + ref.iconName + "\" not found."); 116 return null; 117 } 118 return i; 119 } 120 121 /** 122 * No icon with the given name was found, show a dummy icon instead 123 * @return the icon misc/no_icon.png, in descending priority: 124 * - relative to source file 125 * - from user icon paths 126 * - josm's default icon 127 * can be null if the defaults are turned off by user 128 */ 129 public static ImageIcon getNoIcon_Icon(StyleSource source) { 130 return new ImageProvider("misc/no_icon.png") 131 .setDirs(getIconSourceDirs(source)) 132 .setId("mappaint."+source.getPrefName()) 133 .setArchive(source.zipIcons) 134 .setInArchiveDir(source.getZipEntryDirName()) 135 .setOptional(true).get(); 136 } 137 138 public static ImageIcon getNodeIcon(Tag tag) { 139 return getNodeIcon(tag, true); 140 } 141 142 /** 143 * Returns the node icon that would be displayed for the given tag. 144 * @param tag The tag to look an icon for 145 * @param includeDeprecatedIcon if {@code true}, the special deprecated icon will be returned if applicable 146 * @return {@code null} if no icon found, or if the icon is deprecated and not wanted 147 */ 148 public static ImageIcon getNodeIcon(Tag tag, boolean includeDeprecatedIcon) { 149 if (tag != null) { 150 DataSet ds = new DataSet(); 151 Node virtualNode = new Node(LatLon.ZERO); 152 virtualNode.put(tag.getKey(), tag.getValue()); 153 StyleList styleList; 154 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock(); 155 try { 156 // Add primitive to dataset to avoid DataIntegrityProblemException when evaluating selectors 157 ds.addPrimitive(virtualNode); 158 styleList = getStyles().generateStyles(virtualNode, 0.5, false).a; 159 ds.removePrimitive(virtualNode); 160 } finally { 161 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock(); 162 } 163 if (styleList != null) { 164 for (ElemStyle style : styleList) { 165 if (style instanceof NodeElemStyle) { 166 MapImage mapImage = ((NodeElemStyle) style).mapImage; 167 if (mapImage != null) { 168 if (includeDeprecatedIcon || mapImage.name == null || !"misc/deprecated.png".equals(mapImage.name)) { 169 return new ImageIcon(mapImage.getDisplayedNodeIcon(false)); 170 } else { 171 return null; // Deprecated icon found but not wanted 172 } 173 } 174 } 175 } 176 } 177 } 178 return null; 179 } 180 181 public static List<String> getIconSourceDirs(StyleSource source) { 182 List<String> dirs = new LinkedList<>(); 183 184 File sourceDir = source.getLocalSourceDir(); 185 if (sourceDir != null) { 186 dirs.add(sourceDir.getPath()); 187 } 188 189 Collection<String> prefIconDirs = Main.pref.getCollection("mappaint.icon.sources"); 190 for (String fileset : prefIconDirs) { 191 String[] a; 192 if(fileset.indexOf('=') >= 0) { 193 a = fileset.split("=", 2); 194 } else { 195 a = new String[] {"", fileset}; 196 } 197 198 /* non-prefixed path is generic path, always take it */ 199 if(a[0].length() == 0 || source.getPrefName().equals(a[0])) { 200 dirs.add(a[1]); 201 } 202 } 203 204 if (Main.pref.getBoolean("mappaint.icon.enable-defaults", true)) { 205 /* don't prefix icon path, as it should be generic */ 206 dirs.add("resource://images/styles/standard/"); 207 dirs.add("resource://images/styles/"); 208 } 209 210 return dirs; 211 } 212 213 public static void readFromPreferences() { 214 styles.clear(); 215 216 Collection<? extends SourceEntry> sourceEntries = MapPaintPrefHelper.INSTANCE.get(); 217 218 for (SourceEntry entry : sourceEntries) { 219 StyleSource source = fromSourceEntry(entry); 220 if (source != null) { 221 styles.add(source); 222 } 223 } 224 for (StyleSource source : styles.getStyleSources()) { 225 loadStyleForFirstTime(source); 226 } 227 fireMapPaintSylesUpdated(); 228 } 229 230 private static void loadStyleForFirstTime(StyleSource source) { 231 final long startTime = System.currentTimeMillis(); 232 source.loadStyleSource(); 233 if (Main.pref.getBoolean("mappaint.auto_reload_local_styles", true) && source.isLocal()) { 234 try { 235 Main.fileWatcher.registerStyleSource(source); 236 } catch (IOException e) { 237 Main.error(e); 238 } 239 } 240 if (Main.isDebugEnabled()) { 241 final long elapsedTime = System.currentTimeMillis() - startTime; 242 Main.debug("Initializing map style " + source.url + " completed in " + Utils.getDurationString(elapsedTime)); 243 } 244 } 245 246 private static StyleSource fromSourceEntry(SourceEntry entry) { 247 CachedFile cf = null; 248 try { 249 Set<String> mimes = new HashSet<>(); 250 mimes.addAll(Arrays.asList(XmlStyleSource.XML_STYLE_MIME_TYPES.split(", "))); 251 mimes.addAll(Arrays.asList(MapCSSStyleSource.MAPCSS_STYLE_MIME_TYPES.split(", "))); 252 cf = new CachedFile(entry.url).setHttpAccept(Utils.join(", ", mimes)); 253 String zipEntryPath = cf.findZipEntryPath("mapcss", "style"); 254 if (zipEntryPath != null) { 255 entry.isZip = true; 256 entry.zipEntryPath = zipEntryPath; 257 return new MapCSSStyleSource(entry); 258 } 259 zipEntryPath = cf.findZipEntryPath("xml", "style"); 260 if (zipEntryPath != null) 261 return new XmlStyleSource(entry); 262 if (entry.url.toLowerCase().endsWith(".mapcss")) 263 return new MapCSSStyleSource(entry); 264 if (entry.url.toLowerCase().endsWith(".xml")) 265 return new XmlStyleSource(entry); 266 else { 267 try (InputStreamReader reader = new InputStreamReader(cf.getInputStream(), StandardCharsets.UTF_8)) { 268 WHILE: while (true) { 269 int c = reader.read(); 270 switch (c) { 271 case -1: 272 break WHILE; 273 case ' ': 274 case '\t': 275 case '\n': 276 case '\r': 277 continue; 278 case '<': 279 return new XmlStyleSource(entry); 280 default: 281 return new MapCSSStyleSource(entry); 282 } 283 } 284 } 285 Main.warn("Could not detect style type. Using default (xml)."); 286 return new XmlStyleSource(entry); 287 } 288 } catch (IOException e) { 289 Main.warn(tr("Failed to load Mappaint styles from ''{0}''. Exception was: {1}", entry.url, e.toString())); 290 Main.error(e); 291 } 292 return null; 293 } 294 295 /** 296 * reload styles 297 * preferences are the same, but the file source may have changed 298 * @param sel the indices of styles to reload 299 */ 300 public static void reloadStyles(final int... sel) { 301 List<StyleSource> toReload = new ArrayList<>(); 302 List<StyleSource> data = styles.getStyleSources(); 303 for (int i : sel) { 304 toReload.add(data.get(i)); 305 } 306 Main.worker.submit(new MapPaintStyleLoader(toReload)); 307 } 308 309 public static class MapPaintStyleLoader extends PleaseWaitRunnable { 310 private boolean canceled; 311 private Collection<StyleSource> sources; 312 313 public MapPaintStyleLoader(Collection<StyleSource> sources) { 314 super(tr("Reloading style sources")); 315 this.sources = sources; 316 } 317 318 @Override 319 protected void cancel() { 320 canceled = true; 321 } 322 323 @Override 324 protected void finish() { 325 SwingUtilities.invokeLater(new Runnable() { 326 @Override 327 public void run() { 328 fireMapPaintSylesUpdated(); 329 styles.clearCached(); 330 if (Main.isDisplayingMapView()) { 331 Main.map.mapView.preferenceChanged(null); 332 Main.map.mapView.repaint(); 333 } 334 } 335 }); 336 } 337 338 @Override 339 protected void realRun() { 340 ProgressMonitor monitor = getProgressMonitor(); 341 monitor.setTicksCount(sources.size()); 342 for (StyleSource s : sources) { 343 if (canceled) 344 return; 345 monitor.subTask(tr("loading style ''{0}''...", s.getDisplayString())); 346 s.loadStyleSource(); 347 monitor.worked(1); 348 } 349 } 350 } 351 352 /** 353 * Move position of entries in the current list of StyleSources 354 * @param sel The indices of styles to be moved. 355 * @param delta The number of lines it should move. positive int moves 356 * down and negative moves up. 357 */ 358 public static void moveStyles(int[] sel, int delta) { 359 if (!canMoveStyles(sel, delta)) 360 return; 361 int[] selSorted = Utils.copyArray(sel); 362 Arrays.sort(selSorted); 363 List<StyleSource> data = new ArrayList<>(styles.getStyleSources()); 364 for (int row: selSorted) { 365 StyleSource t1 = data.get(row); 366 StyleSource t2 = data.get(row + delta); 367 data.set(row, t2); 368 data.set(row + delta, t1); 369 } 370 styles.setStyleSources(data); 371 MapPaintPrefHelper.INSTANCE.put(data); 372 fireMapPaintSylesUpdated(); 373 styles.clearCached(); 374 Main.map.mapView.repaint(); 375 } 376 377 public static boolean canMoveStyles(int[] sel, int i) { 378 if (sel.length == 0) 379 return false; 380 int[] selSorted = Utils.copyArray(sel); 381 Arrays.sort(selSorted); 382 383 if (i < 0) // Up 384 return selSorted[0] >= -i; 385 else if (i > 0) // Down 386 return selSorted[selSorted.length-1] <= styles.getStyleSources().size() - 1 - i; 387 else 388 return true; 389 } 390 391 public static void toggleStyleActive(int... sel) { 392 List<StyleSource> data = styles.getStyleSources(); 393 for (int p : sel) { 394 StyleSource s = data.get(p); 395 s.active = !s.active; 396 } 397 MapPaintPrefHelper.INSTANCE.put(data); 398 if (sel.length == 1) { 399 fireMapPaintStyleEntryUpdated(sel[0]); 400 } else { 401 fireMapPaintSylesUpdated(); 402 } 403 styles.clearCached(); 404 Main.map.mapView.repaint(); 405 } 406 407 public static void addStyle(SourceEntry entry) { 408 StyleSource source = fromSourceEntry(entry); 409 if (source != null) { 410 styles.add(source); 411 loadStyleForFirstTime(source); 412 MapPaintPrefHelper.INSTANCE.put(styles.getStyleSources()); 413 fireMapPaintSylesUpdated(); 414 styles.clearCached(); 415 Main.map.mapView.repaint(); 416 } 417 } 418 419 /*********************************** 420 * MapPaintSylesUpdateListener & related code 421 * (get informed when the list of MapPaint StyleSources changes) 422 */ 423 424 public interface MapPaintSylesUpdateListener { 425 public void mapPaintStylesUpdated(); 426 public void mapPaintStyleEntryUpdated(int idx); 427 } 428 429 protected static final CopyOnWriteArrayList<MapPaintSylesUpdateListener> listeners 430 = new CopyOnWriteArrayList<>(); 431 432 public static void addMapPaintSylesUpdateListener(MapPaintSylesUpdateListener listener) { 433 if (listener != null) { 434 listeners.addIfAbsent(listener); 435 } 436 } 437 438 public static void removeMapPaintSylesUpdateListener(MapPaintSylesUpdateListener listener) { 439 listeners.remove(listener); 440 } 441 442 public static void fireMapPaintSylesUpdated() { 443 for (MapPaintSylesUpdateListener l : listeners) { 444 l.mapPaintStylesUpdated(); 445 } 446 } 447 448 public static void fireMapPaintStyleEntryUpdated(int idx) { 449 for (MapPaintSylesUpdateListener l : listeners) { 450 l.mapPaintStyleEntryUpdated(idx); 451 } 452 } 453}