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