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