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