source: josm/trunk/src/org/openstreetmap/josm/gui/mappaint/MapPaintStyles.java@ 12342

Last change on this file since 12342 was 12342, checked in by michael2402, 7 years ago

Change MapPaintStyles listener to use a ListenerList

  • Property svn:eol-style set to native
File size: 17.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.mappaint;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.File;
7import java.io.IOException;
8import java.util.ArrayList;
9import java.util.Arrays;
10import java.util.Collection;
11import java.util.HashSet;
12import java.util.LinkedList;
13import java.util.List;
14import java.util.Set;
15
16import javax.swing.ImageIcon;
17import javax.swing.SwingUtilities;
18
19import org.openstreetmap.josm.Main;
20import org.openstreetmap.josm.data.coor.LatLon;
21import org.openstreetmap.josm.data.osm.DataSet;
22import org.openstreetmap.josm.data.osm.Node;
23import org.openstreetmap.josm.data.osm.Tag;
24import org.openstreetmap.josm.gui.PleaseWaitRunnable;
25import org.openstreetmap.josm.gui.layer.Layer;
26import org.openstreetmap.josm.gui.layer.OsmDataLayer;
27import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
28import org.openstreetmap.josm.gui.mappaint.styleelement.MapImage;
29import org.openstreetmap.josm.gui.mappaint.styleelement.NodeElement;
30import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement;
31import org.openstreetmap.josm.gui.preferences.SourceEntry;
32import org.openstreetmap.josm.gui.preferences.map.MapPaintPreference.MapPaintPrefHelper;
33import org.openstreetmap.josm.gui.progress.ProgressMonitor;
34import org.openstreetmap.josm.io.CachedFile;
35import org.openstreetmap.josm.tools.ImageProvider;
36import org.openstreetmap.josm.tools.ListenerList;
37import org.openstreetmap.josm.tools.Utils;
38
39/**
40 * This class manages the list of available map paint styles and gives access to
41 * the ElemStyles singleton.
42 *
43 * On change, {@link MapPaintSylesUpdateListener#mapPaintStylesUpdated()} is fired
44 * for all listeners.
45 */
46public final class MapPaintStyles {
47
48 private static final Collection<String> DEPRECATED_IMAGE_NAMES = Arrays.asList(
49 "presets/misc/deprecated.svg",
50 "misc/deprecated.png");
51
52 private static ElemStyles styles = new ElemStyles();
53
54 /**
55 * Returns the {@link ElemStyles} singleton instance.
56 *
57 * The returned object is read only, any manipulation happens via one of
58 * the other wrapper methods in this class. ({@link #readFromPreferences},
59 * {@link #moveStyles}, ...)
60 * @return the {@code ElemStyles} singleton instance
61 */
62 public static ElemStyles getStyles() {
63 return styles;
64 }
65
66 private MapPaintStyles() {
67 // Hide default constructor for utils classes
68 }
69
70 /**
71 * Value holder for a reference to a tag name. A style instruction
72 * <pre>
73 * text: a_tag_name;
74 * </pre>
75 * results in a tag reference for the tag <tt>a_tag_name</tt> in the
76 * style cascade.
77 */
78 public static class TagKeyReference {
79 public final String key;
80
81 public TagKeyReference(String key) {
82 this.key = key;
83 }
84
85 @Override
86 public String toString() {
87 return "TagKeyReference{" + "key='" + key + "'}";
88 }
89 }
90
91 /**
92 * IconReference is used to remember the associated style source for each icon URL.
93 * This is necessary because image URLs can be paths relative
94 * to the source file and we have cascading of properties from different source files.
95 */
96 public static class IconReference {
97
98 public final String iconName;
99 public final StyleSource source;
100
101 public IconReference(String iconName, StyleSource source) {
102 this.iconName = iconName;
103 this.source = source;
104 }
105
106 @Override
107 public String toString() {
108 return "IconReference{" + "iconName='" + iconName + "' source='" + source.getDisplayString() + "'}";
109 }
110
111 /**
112 * Determines whether this icon represents a deprecated icon
113 * @return whether this icon represents a deprecated icon
114 * @since 10927
115 */
116 public boolean isDeprecatedIcon() {
117 return DEPRECATED_IMAGE_NAMES.contains(iconName);
118 }
119 }
120
121 /**
122 * Image provider for icon. Note that this is a provider only. A @link{ImageProvider#get()} call may still fail!
123 *
124 * @param ref reference to the requested icon
125 * @param test if <code>true</code> than the icon is request is tested
126 * @return image provider for icon (can be <code>null</code> when <code>test</code> is <code>true</code>).
127 * @see #getIcon(IconReference, int,int)
128 * @since 8097
129 */
130 public static ImageProvider getIconProvider(IconReference ref, boolean test) {
131 final String namespace = ref.source.getPrefName();
132 ImageProvider i = new ImageProvider(ref.iconName)
133 .setDirs(getIconSourceDirs(ref.source))
134 .setId("mappaint."+namespace)
135 .setArchive(ref.source.zipIcons)
136 .setInArchiveDir(ref.source.getZipEntryDirName())
137 .setOptional(true);
138 if (test && i.get() == null) {
139 String msg = "Mappaint style \""+namespace+"\" ("+ref.source.getDisplayString()+") icon \"" + ref.iconName + "\" not found.";
140 ref.source.logWarning(msg);
141 Main.warn(msg);
142 return null;
143 }
144 return i;
145 }
146
147 /**
148 * Return scaled icon.
149 *
150 * @param ref reference to the requested icon
151 * @param width icon width or -1 for autoscale
152 * @param height icon height or -1 for autoscale
153 * @return image icon or <code>null</code>.
154 * @see #getIconProvider(IconReference, boolean)
155 */
156 public static ImageIcon getIcon(IconReference ref, int width, int height) {
157 final String namespace = ref.source.getPrefName();
158 ImageIcon i = getIconProvider(ref, false).setSize(width, height).get();
159 if (i == null) {
160 Main.warn("Mappaint style \""+namespace+"\" ("+ref.source.getDisplayString()+") icon \"" + ref.iconName + "\" not found.");
161 return null;
162 }
163 return i;
164 }
165
166 /**
167 * No icon with the given name was found, show a dummy icon instead
168 * @param source style source
169 * @return the icon misc/no_icon.png, in descending priority:
170 * - relative to source file
171 * - from user icon paths
172 * - josm's default icon
173 * can be null if the defaults are turned off by user
174 */
175 public static ImageIcon getNoIconIcon(StyleSource source) {
176 return new ImageProvider("presets/misc/no_icon")
177 .setDirs(getIconSourceDirs(source))
178 .setId("mappaint."+source.getPrefName())
179 .setArchive(source.zipIcons)
180 .setInArchiveDir(source.getZipEntryDirName())
181 .setOptional(true).get();
182 }
183
184 public static ImageIcon getNodeIcon(Tag tag) {
185 return getNodeIcon(tag, true);
186 }
187
188 /**
189 * Returns the node icon that would be displayed for the given tag.
190 * @param tag The tag to look an icon for
191 * @param includeDeprecatedIcon if {@code true}, the special deprecated icon will be returned if applicable
192 * @return {@code null} if no icon found, or if the icon is deprecated and not wanted
193 */
194 public static ImageIcon getNodeIcon(Tag tag, boolean includeDeprecatedIcon) {
195 if (tag != null) {
196 DataSet ds = new DataSet();
197 Node virtualNode = new Node(LatLon.ZERO);
198 virtualNode.put(tag.getKey(), tag.getValue());
199 StyleElementList styleList;
200 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
201 try {
202 // Add primitive to dataset to avoid DataIntegrityProblemException when evaluating selectors
203 ds.addPrimitive(virtualNode);
204 styleList = getStyles().generateStyles(virtualNode, 0.5, false).a;
205 ds.removePrimitive(virtualNode);
206 } finally {
207 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
208 }
209 if (styleList != null) {
210 for (StyleElement style : styleList) {
211 if (style instanceof NodeElement) {
212 MapImage mapImage = ((NodeElement) style).mapImage;
213 if (mapImage != null) {
214 if (includeDeprecatedIcon || mapImage.name == null || !DEPRECATED_IMAGE_NAMES.contains(mapImage.name)) {
215 return new ImageIcon(mapImage.getImage(false));
216 } else {
217 return null; // Deprecated icon found but not wanted
218 }
219 }
220 }
221 }
222 }
223 }
224 return null;
225 }
226
227 public static List<String> getIconSourceDirs(StyleSource source) {
228 List<String> dirs = new LinkedList<>();
229
230 File sourceDir = source.getLocalSourceDir();
231 if (sourceDir != null) {
232 dirs.add(sourceDir.getPath());
233 }
234
235 Collection<String> prefIconDirs = Main.pref.getCollection("mappaint.icon.sources");
236 for (String fileset : prefIconDirs) {
237 String[] a;
238 if (fileset.indexOf('=') >= 0) {
239 a = fileset.split("=", 2);
240 } else {
241 a = new String[] {"", fileset};
242 }
243
244 /* non-prefixed path is generic path, always take it */
245 if (a[0].isEmpty() || source.getPrefName().equals(a[0])) {
246 dirs.add(a[1]);
247 }
248 }
249
250 if (Main.pref.getBoolean("mappaint.icon.enable-defaults", true)) {
251 /* don't prefix icon path, as it should be generic */
252 dirs.add("resource://images/");
253 }
254
255 return dirs;
256 }
257
258 /**
259 * Reloads all styles from the preferences.
260 */
261 public static void readFromPreferences() {
262 styles.clear();
263
264 Collection<? extends SourceEntry> sourceEntries = MapPaintPrefHelper.INSTANCE.get();
265
266 for (SourceEntry entry : sourceEntries) {
267 styles.add(fromSourceEntry(entry));
268 }
269 for (StyleSource source : styles.getStyleSources()) {
270 loadStyleForFirstTime(source);
271 }
272 fireMapPaintSylesUpdated();
273 }
274
275 private static void loadStyleForFirstTime(StyleSource source) {
276 final long startTime = System.currentTimeMillis();
277 source.loadStyleSource();
278 if (Main.pref.getBoolean("mappaint.auto_reload_local_styles", true) && source.isLocal()) {
279 try {
280 Main.fileWatcher.registerStyleSource(source);
281 } catch (IOException | IllegalStateException e) {
282 Main.error(e);
283 }
284 }
285 if (Main.isDebugEnabled() || !source.isValid()) {
286 final long elapsedTime = System.currentTimeMillis() - startTime;
287 String message = "Initializing map style " + source.url + " completed in " + Utils.getDurationString(elapsedTime);
288 if (!source.isValid()) {
289 Main.warn(message + " (" + source.getErrors().size() + " errors, " + source.getWarnings().size() + " warnings)");
290 } else {
291 Main.debug(message);
292 }
293 }
294 }
295
296 private static StyleSource fromSourceEntry(SourceEntry entry) {
297 if (entry.url == null && entry instanceof MapCSSStyleSource) {
298 return (MapCSSStyleSource) entry;
299 }
300 Set<String> mimes = new HashSet<>(Arrays.asList(MapCSSStyleSource.MAPCSS_STYLE_MIME_TYPES.split(", ")));
301 try (CachedFile cf = new CachedFile(entry.url).setHttpAccept(Utils.join(", ", mimes))) {
302 String zipEntryPath = cf.findZipEntryPath("mapcss", "style");
303 if (zipEntryPath != null) {
304 entry.isZip = true;
305 entry.zipEntryPath = zipEntryPath;
306 }
307 return new MapCSSStyleSource(entry);
308 }
309 }
310
311 /**
312 * reload styles
313 * preferences are the same, but the file source may have changed
314 * @param sel the indices of styles to reload
315 */
316 public static void reloadStyles(final int... sel) {
317 List<StyleSource> toReload = new ArrayList<>();
318 List<StyleSource> data = styles.getStyleSources();
319 for (int i : sel) {
320 toReload.add(data.get(i));
321 }
322 Main.worker.submit(new MapPaintStyleLoader(toReload));
323 }
324
325 public static class MapPaintStyleLoader extends PleaseWaitRunnable {
326 private boolean canceled;
327 private final Collection<StyleSource> sources;
328
329 public MapPaintStyleLoader(Collection<StyleSource> sources) {
330 super(tr("Reloading style sources"));
331 this.sources = sources;
332 }
333
334 @Override
335 protected void cancel() {
336 canceled = true;
337 }
338
339 @Override
340 protected void finish() {
341 fireMapPaintSylesUpdated();
342 afterStyleUpdate();
343 }
344
345 @Override
346 protected void realRun() {
347 ProgressMonitor monitor = getProgressMonitor();
348 monitor.setTicksCount(sources.size());
349 for (StyleSource s : sources) {
350 if (canceled)
351 return;
352 monitor.subTask(tr("loading style ''{0}''...", s.getDisplayString()));
353 s.loadStyleSource();
354 monitor.worked(1);
355 }
356 }
357 }
358
359 /**
360 * Move position of entries in the current list of StyleSources
361 * @param sel The indices of styles to be moved.
362 * @param delta The number of lines it should move. positive int moves
363 * down and negative moves up.
364 */
365 public static void moveStyles(int[] sel, int delta) {
366 if (!canMoveStyles(sel, delta))
367 return;
368 int[] selSorted = Utils.copyArray(sel);
369 Arrays.sort(selSorted);
370 List<StyleSource> data = new ArrayList<>(styles.getStyleSources());
371 for (int row: selSorted) {
372 StyleSource t1 = data.get(row);
373 StyleSource t2 = data.get(row + delta);
374 data.set(row, t2);
375 data.set(row + delta, t1);
376 }
377 styles.setStyleSources(data);
378 MapPaintPrefHelper.INSTANCE.put(data);
379 fireMapPaintSylesUpdated();
380 afterStyleUpdate();
381 }
382
383 /**
384 * Manually trigger for now. TODO: Move this to a listener
385 */
386 private static void afterStyleUpdate() {
387 SwingUtilities.invokeLater(() -> {
388 styles.clearCached();
389
390 // Trigger a repaint of all data layers
391 Main.getLayerManager().getLayers()
392 .stream()
393 .filter(layer -> layer instanceof OsmDataLayer)
394 .forEach(Layer::invalidate);
395 });
396 }
397
398 public static boolean canMoveStyles(int[] sel, int i) {
399 if (sel.length == 0)
400 return false;
401 int[] selSorted = Utils.copyArray(sel);
402 Arrays.sort(selSorted);
403
404 if (i < 0) // Up
405 return selSorted[0] >= -i;
406 else if (i > 0) // Down
407 return selSorted[selSorted.length-1] <= styles.getStyleSources().size() - 1 - i;
408 else
409 return true;
410 }
411
412 public static void toggleStyleActive(int... sel) {
413 List<StyleSource> data = styles.getStyleSources();
414 for (int p : sel) {
415 StyleSource s = data.get(p);
416 s.active = !s.active;
417 }
418 MapPaintPrefHelper.INSTANCE.put(data);
419 if (sel.length == 1) {
420 fireMapPaintStyleEntryUpdated(sel[0]);
421 } else {
422 fireMapPaintSylesUpdated();
423 }
424 afterStyleUpdate();
425 }
426
427 /**
428 * Add a new map paint style.
429 * @param entry map paint style
430 * @return loaded style source, or {@code null}
431 */
432 public static StyleSource addStyle(SourceEntry entry) {
433 StyleSource source = fromSourceEntry(entry);
434 styles.add(source);
435 loadStyleForFirstTime(source);
436 refreshStyles();
437 return source;
438 }
439
440 /**
441 * Remove a map paint style.
442 * @param entry map paint style
443 * @since 11493
444 */
445 public static void removeStyle(SourceEntry entry) {
446 StyleSource source = fromSourceEntry(entry);
447 if (styles.remove(source)) {
448 refreshStyles();
449 }
450 }
451
452 private static void refreshStyles() {
453 MapPaintPrefHelper.INSTANCE.put(styles.getStyleSources());
454 fireMapPaintSylesUpdated();
455 afterStyleUpdate();
456 }
457
458 /***********************************
459 * MapPaintSylesUpdateListener &amp; related code
460 * (get informed when the list of MapPaint StyleSources changes)
461 */
462 public interface MapPaintSylesUpdateListener {
463 /**
464 * Called on any style source changes that are not handled by {@link #mapPaintStyleEntryUpdated(int)}
465 */
466 void mapPaintStylesUpdated();
467
468 /**
469 * Called whenever a single style source entry was changed.
470 * @param index The index of the entry.
471 */
472 void mapPaintStyleEntryUpdated(int index);
473 }
474
475 private static final ListenerList<MapPaintSylesUpdateListener> listeners = ListenerList.createUnchecked();
476
477 /**
478 * Add a listener that listens to global style changes.
479 * @param listener The listener
480 */
481 public static void addMapPaintSylesUpdateListener(MapPaintSylesUpdateListener listener) {
482 listeners.addListener(listener);
483 }
484
485 /**
486 * Removes a listener that listens to global style changes.
487 * @param listener The listener
488 */
489 public static void removeMapPaintSylesUpdateListener(MapPaintSylesUpdateListener listener) {
490 listeners.removeListener(listener);
491 }
492
493 public static void fireMapPaintSylesUpdated() {
494 listeners.fireEvent(MapPaintSylesUpdateListener::mapPaintStylesUpdated);
495 }
496
497 public static void fireMapPaintStyleEntryUpdated(int index) {
498 listeners.fireEvent(l -> l.mapPaintStyleEntryUpdated(index));
499 }
500}
Note: See TracBrowser for help on using the repository browser.