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

Last change on this file since 12841 was 12841, checked in by bastiK, 5 weeks ago

see #15229 - fix deprecations caused by [12840]

  • 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.getList("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.