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