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