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