001    // License: GPL. Copyright 2008 by Immanuel Scholz and others
002    package org.openstreetmap.josm.gui.layer.markerlayer;
003    
004    import java.awt.Graphics;
005    import java.awt.Point;
006    import java.awt.event.ActionEvent;
007    import java.io.File;
008    import java.net.MalformedURLException;
009    import java.net.URL;
010    import java.util.ArrayList;
011    import java.util.Collection;
012    import java.util.HashMap;
013    import java.util.LinkedList;
014    import java.util.List;
015    import java.util.Map;
016    
017    import javax.swing.Icon;
018    
019    import org.openstreetmap.josm.Main;
020    import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
021    import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
022    import org.openstreetmap.josm.data.coor.CachedLatLon;
023    import org.openstreetmap.josm.data.coor.EastNorth;
024    import org.openstreetmap.josm.data.coor.LatLon;
025    import org.openstreetmap.josm.data.gpx.GpxData;
026    import org.openstreetmap.josm.data.gpx.GpxLink;
027    import org.openstreetmap.josm.data.gpx.WayPoint;
028    import org.openstreetmap.josm.data.preferences.CachedProperty;
029    import org.openstreetmap.josm.data.preferences.IntegerProperty;
030    import org.openstreetmap.josm.gui.MapView;
031    import org.openstreetmap.josm.tools.ImageProvider;
032    import org.openstreetmap.josm.tools.template_engine.ParseError;
033    import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider;
034    import org.openstreetmap.josm.tools.template_engine.TemplateEntry;
035    import org.openstreetmap.josm.tools.template_engine.TemplateParser;
036    
037    /**
038     * Basic marker class. Requires a position, and supports
039     * a custom icon and a name.
040     *
041     * This class is also used to create appropriate Marker-type objects
042     * when waypoints are imported.
043     *
044     * It hosts a public list object, named makers, containing implementations of
045     * the MarkerMaker interface. Whenever a Marker needs to be created, each
046     * object in makers is called with the waypoint parameters (Lat/Lon and tag
047     * data), and the first one to return a Marker object wins.
048     *
049     * By default, one the list contains one default "Maker" implementation that
050     * will create AudioMarkers for .wav files, ImageMarkers for .png/.jpg/.jpeg
051     * files, and WebMarkers for everything else. (The creation of a WebMarker will
052     * fail if there's no valid URL in the <link> tag, so it might still make sense
053     * to add Makers for such waypoints at the end of the list.)
054     *
055     * The default implementation only looks at the value of the <link> tag inside
056     * the <wpt> tag of the GPX file.
057     *
058     * <h2>HowTo implement a new Marker</h2>
059     * <ul>
060     * <li> Subclass Marker or ButtonMarker and override <code>containsPoint</code>
061     *      if you like to respond to user clicks</li>
062     * <li> Override paint, if you want a custom marker look (not "a label and a symbol")</li>
063     * <li> Implement MarkerCreator to return a new instance of your marker class</li>
064     * <li> In you plugin constructor, add an instance of your MarkerCreator
065     *      implementation either on top or bottom of Marker.markerProducers.
066     *      Add at top, if your marker should overwrite an current marker or at bottom
067     *      if you only add a new marker style.</li>
068     * </ul>
069     *
070     * @author Frederik Ramm <frederik@remote.org>
071     */
072    public class Marker implements TemplateEngineDataProvider {
073    
074        public static class TemplateEntryProperty extends CachedProperty<TemplateEntry> {
075            // This class is a bit complicated because it supports both global and per layer settings. I've added per layer settings because
076            // GPXSettingsPanel had possibility to set waypoint label but then I've realized that markers use different layer then gpx data
077            // so per layer settings is useless. Anyway it's possible to specify marker layer pattern in Einstein preferences and maybe somebody
078            // will make gui for it so I'm keeping it here
079    
080            private final static Map<String, TemplateEntryProperty> cache = new HashMap<String, TemplateEntryProperty>();
081    
082            // Legacy code - convert label from int to template engine expression
083            private static final IntegerProperty PROP_LABEL = new IntegerProperty("draw.rawgps.layer.wpt", 0 );
084            private static String getDefaultLabelPattern() {
085                switch (PROP_LABEL.get()) {
086                case 1:
087                    return LABEL_PATTERN_NAME;
088                case 2:
089                    return LABEL_PATTERN_DESC;
090                case 0:
091                case 3:
092                    return LABEL_PATTERN_AUTO;
093                default:
094                    return "";
095                }
096            }
097    
098            public static TemplateEntryProperty forMarker(String layerName) {
099                String key = "draw.rawgps.layer.wpt.pattern";
100                if (layerName != null) {
101                    key += "." + layerName;
102                }
103                TemplateEntryProperty result = cache.get(key);
104                if (result == null) {
105                    String defaultValue = layerName == null ? getDefaultLabelPattern():"";
106                    TemplateEntryProperty parent = layerName == null ? null : forMarker(null);
107                    try {
108                        result = new TemplateEntryProperty(key, defaultValue, parent);
109                        cache.put(key, result);
110                    } catch (ParseError e) {
111                        Main.warn("Unable to parse template engine pattern ''{0}'' for property {1}", defaultValue, key);
112                    }
113                }
114                return result;
115            }
116    
117            public static TemplateEntryProperty forAudioMarker(String layerName) {
118                String key = "draw.rawgps.layer.audiowpt.pattern";
119                if (layerName != null) {
120                    key += "." + layerName;
121                }
122                TemplateEntryProperty result = cache.get(key);
123                if (result == null) {
124                    String defaultValue = layerName == null?"?{ '{name}' | '{desc}' | '{" + Marker.MARKER_FORMATTED_OFFSET + "}' }":"";
125                    TemplateEntryProperty parent = layerName == null ? null : forAudioMarker(null);
126                    try {
127                        result = new TemplateEntryProperty(key, defaultValue, parent);
128                        cache.put(key, result);
129                    } catch (ParseError e) {
130                        Main.warn("Unable to parse template engine pattern ''{0}'' for property {1}", defaultValue, key);
131                    }
132                }
133                return result;
134            }
135    
136            private TemplateEntryProperty parent;
137    
138    
139            private TemplateEntryProperty(String key, String defaultValue, TemplateEntryProperty parent) throws ParseError {
140                super(key, defaultValue);
141                this.parent = parent;
142                updateValue(); // Needs to be called because parent wasn't know in super constructor
143            }
144    
145            @Override
146            protected TemplateEntry fromString(String s) {
147                try {
148                    return new TemplateParser(s).parse();
149                } catch (ParseError e) {
150                    Main.warn("Unable to parse template engine pattern ''{0}'' for property {1}. Using default (''{2}'') instead",
151                            s, getKey(), super.getDefaultValueAsString());
152                    return getDefaultValue();
153                }
154            }
155    
156            @Override
157            public String getDefaultValueAsString() {
158                if (parent == null)
159                    return super.getDefaultValueAsString();
160                else
161                    return parent.getAsString();
162            }
163    
164            @Override
165            public void preferenceChanged(PreferenceChangeEvent e) {
166                if (e.getKey().equals(key) || (parent != null && e.getKey().equals(parent.getKey()))) {
167                    updateValue();
168                }
169            }
170        }
171    
172    
173        /**
174         * Plugins can add their Marker creation stuff at the bottom or top of this list
175         * (depending on whether they want to override default behaviour or just add new
176         * stuff).
177         */
178        public static final List<MarkerProducers> markerProducers = new LinkedList<MarkerProducers>();
179    
180        // Add one Marker specifying the default behaviour.
181        static {
182            Marker.markerProducers.add(new MarkerProducers() {
183                @SuppressWarnings("unchecked")
184                public Marker createMarker(WayPoint wpt, File relativePath, MarkerLayer parentLayer, double time, double offset) {
185                    String uri = null;
186                    // cheapest way to check whether "link" object exists and is a non-empty
187                    // collection of GpxLink objects...
188                    Collection<GpxLink> links = (Collection<GpxLink>)wpt.attr.get(GpxData.META_LINKS);
189                    if (links != null) {
190                        for (GpxLink oneLink : links ) {
191                            uri = oneLink.uri;
192                            break;
193                        }
194                    }
195    
196                    URL url = null;
197                    if (uri != null) {
198                        try {
199                            url = new URL(uri);
200                        } catch (MalformedURLException e) {
201                            // Try a relative file:// url, if the link is not in an URL-compatible form
202                            if (relativePath != null) {
203                                try {
204                                    url = new File(relativePath.getParentFile(), uri).toURI().toURL();
205                                } catch (MalformedURLException e1) {
206                                    Main.warn("Unable to convert uri {0} to URL: {1}", uri, e1.getMessage());
207                                }
208                            }
209                        }
210                    }
211    
212    
213                    if (url == null) {
214                        String symbolName = wpt.getString("symbol");
215                        if (symbolName == null) {
216                            symbolName = wpt.getString("sym");
217                        }
218                        return new Marker(wpt.getCoor(), wpt, symbolName, parentLayer, time, offset);
219                    }
220                    else if (url.toString().endsWith(".wav")) {
221                        return new AudioMarker(wpt.getCoor(), wpt, url, parentLayer, time, offset);
222                    } else if (url.toString().endsWith(".png") || url.toString().endsWith(".jpg") || url.toString().endsWith(".jpeg") || url.toString().endsWith(".gif")) {
223                        return new ImageMarker(wpt.getCoor(), url, parentLayer, time, offset);
224                    } else {
225                        return new WebMarker(wpt.getCoor(), url, parentLayer, time, offset);
226                    }
227                }
228            });
229        }
230    
231        /**
232         * Returns an object of class Marker or one of its subclasses
233         * created from the parameters given.
234         *
235         * @param wpt waypoint data for marker
236         * @param relativePath An path to use for constructing relative URLs or
237         *        <code>null</code> for no relative URLs
238         * @param offset double in seconds as the time offset of this marker from
239         *        the GPX file from which it was derived (if any).
240         * @return a new Marker object
241         */
242        public static Marker createMarker(WayPoint wpt, File relativePath, MarkerLayer parentLayer, double time, double offset) {
243            for (MarkerProducers maker : Marker.markerProducers) {
244                Marker marker = maker.createMarker(wpt, relativePath, parentLayer, time, offset);
245                if (marker != null)
246                    return marker;
247            }
248            return null;
249        }
250    
251        public static final String MARKER_OFFSET = "waypointOffset";
252        public static final String MARKER_FORMATTED_OFFSET = "formattedWaypointOffset";
253    
254        public static final String LABEL_PATTERN_AUTO = "?{ '{name} - {desc}' | '{name}' | '{desc}' }";
255        public static final String LABEL_PATTERN_NAME = "{name}";
256        public static final String LABEL_PATTERN_DESC = "{desc}";
257    
258    
259        private final TemplateEngineDataProvider dataProvider;
260        private final String text;
261    
262        public final Icon symbol;
263        public final MarkerLayer parentLayer;
264        public double time; /* absolute time of marker since epoch */
265        public double offset; /* time offset in seconds from the gpx point from which it was derived,
266                                 may be adjusted later to sync with other data, so not final */
267    
268        private String cachedText;
269        private int textVersion = -1;
270        private CachedLatLon coor;
271    
272        public Marker(LatLon ll, TemplateEngineDataProvider dataProvider, String iconName, MarkerLayer parentLayer, double time, double offset) {
273            setCoor(ll);
274    
275            this.offset = offset;
276            this.time = time;
277            // /* ICON(markers/) */"Bridge"
278            // /* ICON(markers/) */"Crossing"
279            this.symbol = iconName != null ? ImageProvider.getIfAvailable("markers",iconName) : null;
280            this.parentLayer = parentLayer;
281    
282            this.dataProvider = dataProvider;
283            this.text = null;
284        }
285    
286        public Marker(LatLon ll, String text, String iconName, MarkerLayer parentLayer, double time, double offset) {
287            setCoor(ll);
288    
289            this.offset = offset;
290            this.time = time;
291            // /* ICON(markers/) */"Bridge"
292            // /* ICON(markers/) */"Crossing"
293            this.symbol = iconName != null ? ImageProvider.getIfAvailable("markers",iconName) : null;
294            this.parentLayer = parentLayer;
295    
296            this.dataProvider = null;
297            this.text = text;
298        }
299    
300        public final void setCoor(LatLon coor) {
301            if(this.coor == null) {
302                this.coor = new CachedLatLon(coor);
303            } else {
304                this.coor.setCoor(coor);
305            }
306        }
307    
308        public final LatLon getCoor() {
309            return coor;
310        }
311    
312        public final void setEastNorth(EastNorth eastNorth) {
313            coor.setEastNorth(eastNorth);
314        }
315    
316        public final EastNorth getEastNorth() {
317            return coor.getEastNorth();
318        }
319    
320    
321        /**
322         * Checks whether the marker display area contains the given point.
323         * Markers not interested in mouse clicks may always return false.
324         *
325         * @param p The point to check
326         * @return <code>true</code> if the marker "hotspot" contains the point.
327         */
328        public boolean containsPoint(Point p) {
329            return false;
330        }
331    
332        /**
333         * Called when the mouse is clicked in the marker's hotspot. Never
334         * called for markers which always return false from containsPoint.
335         *
336         * @param ev A dummy ActionEvent
337         */
338        public void actionPerformed(ActionEvent ev) {
339        }
340    
341    
342        /**
343         * Paints the marker.
344         * @param g graphics context
345         * @param mv map view
346         * @param mousePressed true if the left mouse button is pressed
347         */
348        public void paint(Graphics g, MapView mv, boolean mousePressed, boolean showTextOrIcon) {
349            Point screen = mv.getPoint(getEastNorth());
350            if (symbol != null && showTextOrIcon) {
351                symbol.paintIcon(mv, g, screen.x-symbol.getIconWidth()/2, screen.y-symbol.getIconHeight()/2);
352            } else {
353                g.drawLine(screen.x-2, screen.y-2, screen.x+2, screen.y+2);
354                g.drawLine(screen.x+2, screen.y-2, screen.x-2, screen.y+2);
355            }
356    
357            String labelText = getText();
358            if ((labelText != null) && showTextOrIcon) {
359                g.drawString(labelText, screen.x+4, screen.y+2);
360            }
361        }
362    
363    
364        protected TemplateEntryProperty getTextTemplate() {
365            return TemplateEntryProperty.forMarker(parentLayer.getName());
366        }
367    
368        /**
369         * Returns the Text which should be displayed, depending on chosen preference
370         * @return Text of the label
371         */
372        public String getText() {
373            if (text != null)
374                return text;
375            else {
376                TemplateEntryProperty property = getTextTemplate();
377                if (property.getUpdateCount() != textVersion) {
378                    TemplateEntry templateEntry = property.get();
379                    StringBuilder sb = new StringBuilder();
380                    templateEntry.appendText(sb, this);
381    
382                    cachedText = sb.toString();
383                    textVersion = property.getUpdateCount();
384                }
385                return cachedText;
386            }
387        }
388    
389        @Override
390        public Collection<String> getTemplateKeys() {
391            Collection<String> result;
392            if (dataProvider != null) {
393                result = dataProvider.getTemplateKeys();
394            } else {
395                result = new ArrayList<String>();
396            }
397            result.add(MARKER_FORMATTED_OFFSET);
398            result.add(MARKER_OFFSET);
399            return result;
400        }
401    
402        private String formatOffset () {
403            int wholeSeconds = (int)(offset + 0.5);
404            if (wholeSeconds < 60)
405                return Integer.toString(wholeSeconds);
406            else if (wholeSeconds < 3600)
407                return String.format("%d:%02d", wholeSeconds / 60, wholeSeconds % 60);
408            else
409                return String.format("%d:%02d:%02d", wholeSeconds / 3600, (wholeSeconds % 3600)/60, wholeSeconds % 60);
410        }
411    
412        @Override
413        public Object getTemplateValue(String name, boolean special) {
414            if (MARKER_FORMATTED_OFFSET.equals(name))
415                return formatOffset();
416            else if (MARKER_OFFSET.equals(name))
417                return offset;
418            else if (dataProvider != null)
419                return dataProvider.getTemplateValue(name, special);
420            else
421                return null;
422        }
423    
424        @Override
425        public boolean evaluateCondition(Match condition) {
426            throw new UnsupportedOperationException();
427        }
428    }