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;
146            MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
147            try {
148                styleList = getStyles().generateStyles(virtualNode, 0.5, null, false).a;
149            } finally {
150                MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
151            }
152            if (styleList != null) {
153                for (ElemStyle style : styleList) {
154                    if (style instanceof NodeElemStyle) {
155                        MapImage mapImage = ((NodeElemStyle) style).mapImage;
156                        if (mapImage != null) {
157                            if (includeDeprecatedIcon || mapImage.name == null || !"misc/deprecated.png".equals(mapImage.name)) {
158                                return new ImageIcon(mapImage.getDisplayedNodeIcon(false));
159                            } else {
160                                return null; // Deprecated icon found but not wanted
161                            }
162                        }
163                    }
164                }
165            }
166        }
167        return null;
168    }
169
170    public static List<String> getIconSourceDirs(StyleSource source) {
171        List<String> dirs = new LinkedList<>();
172
173        File sourceDir = source.getLocalSourceDir();
174        if (sourceDir != null) {
175            dirs.add(sourceDir.getPath());
176        }
177
178        Collection<String> prefIconDirs = Main.pref.getCollection("mappaint.icon.sources");
179        for (String fileset : prefIconDirs) {
180            String[] a;
181            if(fileset.indexOf('=') >= 0) {
182                a = fileset.split("=", 2);
183            } else {
184                a = new String[] {"", fileset};
185            }
186
187            /* non-prefixed path is generic path, always take it */
188            if(a[0].length() == 0 || source.getPrefName().equals(a[0])) {
189                dirs.add(a[1]);
190            }
191        }
192
193        if (Main.pref.getBoolean("mappaint.icon.enable-defaults", true)) {
194            /* don't prefix icon path, as it should be generic */
195            dirs.add("resource://images/styles/standard/");
196            dirs.add("resource://images/styles/");
197        }
198
199        return dirs;
200    }
201
202    public static void readFromPreferences() {
203        styles.clear();
204
205        Collection<? extends SourceEntry> sourceEntries = MapPaintPrefHelper.INSTANCE.get();
206
207        for (SourceEntry entry : sourceEntries) {
208            StyleSource source = fromSourceEntry(entry);
209            if (source != null) {
210                styles.add(source);
211            }
212        }
213        for (StyleSource source : styles.getStyleSources()) {
214            loadStyleForFirstTime(source);
215        }
216        fireMapPaintSylesUpdated();
217    }
218
219    private static void loadStyleForFirstTime(StyleSource source) {
220        final long startTime = System.currentTimeMillis();
221        source.loadStyleSource();
222        if (Main.pref.getBoolean("mappaint.auto_reload_local_styles", true) && source.isLocal()) {
223            try {
224                Main.fileWatcher.registerStyleSource(source);
225            } catch (IOException e) {
226                Main.error(e);
227            }
228        }
229        if (Main.isDebugEnabled()) {
230            final long elapsedTime = System.currentTimeMillis() - startTime;
231            Main.debug("Initializing map style " + source.url + " completed in " + Utils.getDurationString(elapsedTime));
232        }
233    }
234
235    private static StyleSource fromSourceEntry(SourceEntry entry) {
236        CachedFile cf = null;
237        try {
238            Set<String> mimes = new HashSet<>();
239            mimes.addAll(Arrays.asList(XmlStyleSource.XML_STYLE_MIME_TYPES.split(", ")));
240            mimes.addAll(Arrays.asList(MapCSSStyleSource.MAPCSS_STYLE_MIME_TYPES.split(", ")));
241            cf = new CachedFile(entry.url).setHttpAccept(Utils.join(", ", mimes));
242            String zipEntryPath = cf.findZipEntryPath("mapcss", "style");
243            if (zipEntryPath != null) {
244                entry.isZip = true;
245                entry.zipEntryPath = zipEntryPath;
246                return new MapCSSStyleSource(entry);
247            }
248            zipEntryPath = cf.findZipEntryPath("xml", "style");
249            if (zipEntryPath != null)
250                return new XmlStyleSource(entry);
251            if (entry.url.toLowerCase().endsWith(".mapcss"))
252                return new MapCSSStyleSource(entry);
253            if (entry.url.toLowerCase().endsWith(".xml"))
254                return new XmlStyleSource(entry);
255            else {
256                try (InputStreamReader reader = new InputStreamReader(cf.getInputStream(), StandardCharsets.UTF_8)) {
257                    WHILE: while (true) {
258                        int c = reader.read();
259                        switch (c) {
260                            case -1:
261                                break WHILE;
262                            case ' ':
263                            case '\t':
264                            case '\n':
265                            case '\r':
266                                continue;
267                            case '<':
268                                return new XmlStyleSource(entry);
269                            default:
270                                return new MapCSSStyleSource(entry);
271                        }
272                    }
273                }
274                Main.warn("Could not detect style type. Using default (xml).");
275                return new XmlStyleSource(entry);
276            }
277        } catch (IOException e) {
278            Main.warn(tr("Failed to load Mappaint styles from ''{0}''. Exception was: {1}", entry.url, e.toString()));
279            Main.error(e);
280        }
281        return null;
282    }
283
284    /**
285     * reload styles
286     * preferences are the same, but the file source may have changed
287     * @param sel the indices of styles to reload
288     */
289    public static void reloadStyles(final int... sel) {
290        List<StyleSource> toReload = new ArrayList<>();
291        List<StyleSource> data = styles.getStyleSources();
292        for (int i : sel) {
293            toReload.add(data.get(i));
294        }
295        Main.worker.submit(new MapPaintStyleLoader(toReload));
296    }
297
298    public static class MapPaintStyleLoader extends PleaseWaitRunnable {
299        private boolean canceled;
300        private Collection<StyleSource> sources;
301
302        public MapPaintStyleLoader(Collection<StyleSource> sources) {
303            super(tr("Reloading style sources"));
304            this.sources = sources;
305        }
306
307        @Override
308        protected void cancel() {
309            canceled = true;
310        }
311
312        @Override
313        protected void finish() {
314            SwingUtilities.invokeLater(new Runnable() {
315                @Override
316                public void run() {
317                    fireMapPaintSylesUpdated();
318                    styles.clearCached();
319                    if (Main.isDisplayingMapView()) {
320                        Main.map.mapView.preferenceChanged(null);
321                        Main.map.mapView.repaint();
322                    }
323                }
324            });
325        }
326
327        @Override
328        protected void realRun() {
329            ProgressMonitor monitor = getProgressMonitor();
330            monitor.setTicksCount(sources.size());
331            for (StyleSource s : sources) {
332                if (canceled)
333                    return;
334                monitor.subTask(tr("loading style ''{0}''...", s.getDisplayString()));
335                s.loadStyleSource();
336                monitor.worked(1);
337            }
338        }
339    }
340
341    /**
342     * Move position of entries in the current list of StyleSources
343     * @param sel The indices of styles to be moved.
344     * @param delta The number of lines it should move. positive int moves
345     *      down and negative moves up.
346     */
347    public static void moveStyles(int[] sel, int delta) {
348        if (!canMoveStyles(sel, delta))
349            return;
350        int[] selSorted = Utils.copyArray(sel);
351        Arrays.sort(selSorted);
352        List<StyleSource> data = new ArrayList<>(styles.getStyleSources());
353        for (int row: selSorted) {
354            StyleSource t1 = data.get(row);
355            StyleSource t2 = data.get(row + delta);
356            data.set(row, t2);
357            data.set(row + delta, t1);
358        }
359        styles.setStyleSources(data);
360        MapPaintPrefHelper.INSTANCE.put(data);
361        fireMapPaintSylesUpdated();
362        styles.clearCached();
363        Main.map.mapView.repaint();
364    }
365
366    public static boolean canMoveStyles(int[] sel, int i) {
367        if (sel.length == 0)
368            return false;
369        int[] selSorted = Utils.copyArray(sel);
370        Arrays.sort(selSorted);
371
372        if (i < 0) // Up
373            return selSorted[0] >= -i;
374        else if (i > 0) // Down
375            return selSorted[selSorted.length-1] <= styles.getStyleSources().size() - 1 - i;
376        else
377            return true;
378    }
379
380    public static void toggleStyleActive(int... sel) {
381        List<StyleSource> data = styles.getStyleSources();
382        for (int p : sel) {
383            StyleSource s = data.get(p);
384            s.active = !s.active;
385        }
386        MapPaintPrefHelper.INSTANCE.put(data);
387        if (sel.length == 1) {
388            fireMapPaintStyleEntryUpdated(sel[0]);
389        } else {
390            fireMapPaintSylesUpdated();
391        }
392        styles.clearCached();
393        Main.map.mapView.repaint();
394    }
395
396    public static void addStyle(SourceEntry entry) {
397        StyleSource source = fromSourceEntry(entry);
398        if (source != null) {
399            styles.add(source);
400            loadStyleForFirstTime(source);
401            MapPaintPrefHelper.INSTANCE.put(styles.getStyleSources());
402            fireMapPaintSylesUpdated();
403            styles.clearCached();
404            Main.map.mapView.repaint();
405        }
406    }
407
408    /***********************************
409     * MapPaintSylesUpdateListener &amp; related code
410     *  (get informed when the list of MapPaint StyleSources changes)
411     */
412
413    public interface MapPaintSylesUpdateListener {
414        public void mapPaintStylesUpdated();
415        public void mapPaintStyleEntryUpdated(int idx);
416    }
417
418    protected static final CopyOnWriteArrayList<MapPaintSylesUpdateListener> listeners
419            = new CopyOnWriteArrayList<>();
420
421    public static void addMapPaintSylesUpdateListener(MapPaintSylesUpdateListener listener) {
422        if (listener != null) {
423            listeners.addIfAbsent(listener);
424        }
425    }
426
427    public static void removeMapPaintSylesUpdateListener(MapPaintSylesUpdateListener listener) {
428        listeners.remove(listener);
429    }
430
431    public static void fireMapPaintSylesUpdated() {
432        for (MapPaintSylesUpdateListener l : listeners) {
433            l.mapPaintStylesUpdated();
434        }
435    }
436
437    public static void fireMapPaintStyleEntryUpdated(int idx) {
438        for (MapPaintSylesUpdateListener l : listeners) {
439            l.mapPaintStyleEntryUpdated(idx);
440        }
441    }
442}