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