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 }