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