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

Last change on this file since 12825 was 12825, checked in by Don-vip, 7 years ago

see #15229 - see #15182 - make FileWatcher generic so it has no more dependence on MapCSS

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