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.LinkedList;
015import java.util.List;
016import java.util.Map;
017import java.util.concurrent.ConcurrentHashMap;
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 ConcurrentHashMap<>();
082
083        public static TemplateEntryProperty forMarker(String layerName) {
084            String key = "draw.rawgps.layer.wpt.pattern";
085            if (layerName != null) {
086                return CACHE.computeIfAbsent(key + '.' + layerName, k -> new TemplateEntryProperty(k, "", forMarker(null)));
087            } else {
088                return CACHE.computeIfAbsent(key, k -> new TemplateEntryProperty(k, LABEL_PATTERN_AUTO, null));
089            }
090        }
091
092        public static TemplateEntryProperty forAudioMarker(String layerName) {
093            String key = "draw.rawgps.layer.audiowpt.pattern";
094            if (layerName != null) {
095                return CACHE.computeIfAbsent(key + '.' + layerName, k ->
096                        new TemplateEntryProperty(k, "", forAudioMarker(null)));
097            } else {
098                return CACHE.computeIfAbsent(key, k ->
099                        new TemplateEntryProperty(k, "?{ '{name}' | '{desc}' | '{" + MARKER_FORMATTED_OFFSET + "}' }", null));
100            }
101        }
102
103        private final TemplateEntryProperty parent;
104
105        private TemplateEntryProperty(String key, String defaultValue, TemplateEntryProperty parent) {
106            super(key, defaultValue);
107            this.parent = parent;
108            updateValue(); // Needs to be called because parent wasn't know in super constructor
109        }
110
111        @Override
112        protected TemplateEntry fromString(String s) {
113            try {
114                return new TemplateParser(s).parse();
115            } catch (ParseError e) {
116                Logging.debug(e);
117                Logging.warn("Unable to parse template engine pattern ''{0}'' for property {1}. Using default (''{2}'') instead",
118                        s, getKey(), super.getDefaultValueAsString());
119                return getDefaultValue();
120            }
121        }
122
123        @Override
124        public String getDefaultValueAsString() {
125            if (parent == null)
126                return super.getDefaultValueAsString();
127            else
128                return parent.getAsString();
129        }
130
131        @Override
132        public void preferenceChanged(PreferenceChangeEvent e) {
133            if (e.getKey().equals(key) || (parent != null && e.getKey().equals(parent.getKey()))) {
134                updateValue();
135            }
136        }
137    }
138
139    /**
140     * Plugins can add their Marker creation stuff at the bottom or top of this list
141     * (depending on whether they want to override default behaviour or just add new stuff).
142     */
143    private static final List<MarkerProducers> markerProducers = new LinkedList<>();
144
145    // Add one Marker specifying the default behaviour.
146    static {
147        Marker.markerProducers.add(new DefaultMarkerProducers());
148    }
149
150    /**
151     * Add a new marker producers at the end of the JOSM list.
152     * @param mp a new marker producers
153     * @since 11850
154     */
155    public static void appendMarkerProducer(MarkerProducers mp) {
156        markerProducers.add(mp);
157    }
158
159    /**
160     * Add a new marker producers at the beginning of the JOSM list.
161     * @param mp a new marker producers
162     * @since 11850
163     */
164    public static void prependMarkerProducer(MarkerProducers mp) {
165        markerProducers.add(0, mp);
166    }
167
168    /**
169     * Returns an object of class Marker or one of its subclasses
170     * created from the parameters given.
171     *
172     * @param wpt waypoint data for marker
173     * @param relativePath An path to use for constructing relative URLs or
174     *        <code>null</code> for no relative URLs
175     * @param parentLayer the <code>MarkerLayer</code> that will contain the created <code>Marker</code>
176     * @param time time of the marker in seconds since epoch
177     * @param offset double in seconds as the time offset of this marker from
178     *        the GPX file from which it was derived (if any).
179     * @return a new Marker object
180     */
181    public static Collection<Marker> createMarkers(WayPoint wpt, File relativePath, MarkerLayer parentLayer, double time, double offset) {
182        for (MarkerProducers maker : Marker.markerProducers) {
183            final Collection<Marker> markers = maker.createMarkers(wpt, relativePath, parentLayer, time, offset);
184            if (markers != null)
185                return markers;
186        }
187        return null;
188    }
189
190    public static final String MARKER_OFFSET = "waypointOffset";
191    public static final String MARKER_FORMATTED_OFFSET = "formattedWaypointOffset";
192
193    public static final String LABEL_PATTERN_AUTO = "?{ '{name} ({desc})' | '{name} ({cmt})' | '{name}' | '{desc}' | '{cmt}' }";
194    public static final String LABEL_PATTERN_NAME = "{name}";
195    public static final String LABEL_PATTERN_DESC = "{desc}";
196
197    private final TemplateEngineDataProvider dataProvider;
198    private final String text;
199
200    protected final ImageIcon symbol;
201    private BufferedImage redSymbol;
202    public final MarkerLayer parentLayer;
203    /** Absolute time of marker in seconds since epoch */
204    public double time;
205    /** 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 */
206    public double offset;
207
208    private String cachedText;
209    private int textVersion = -1;
210    private CachedLatLon coor;
211
212    private boolean erroneous;
213
214    public Marker(LatLon ll, TemplateEngineDataProvider dataProvider, String iconName, MarkerLayer parentLayer,
215            double time, double offset) {
216        this(ll, dataProvider, null, iconName, parentLayer, time, offset);
217    }
218
219    public Marker(LatLon ll, String text, String iconName, MarkerLayer parentLayer, double time, double offset) {
220        this(ll, null, text, iconName, parentLayer, time, offset);
221    }
222
223    private Marker(LatLon ll, TemplateEngineDataProvider dataProvider, String text, String iconName, MarkerLayer parentLayer,
224            double time, double offset) {
225        setCoor(ll);
226
227        this.offset = offset;
228        this.time = time;
229        /* tell icon checking that we expect these names to exist */
230        // /* ICON(markers/) */"Bridge"
231        // /* ICON(markers/) */"Crossing"
232        this.symbol = iconName != null ? ImageProvider.getIfAvailable("markers", iconName) : null;
233        this.parentLayer = parentLayer;
234
235        this.dataProvider = dataProvider;
236        this.text = text;
237    }
238
239    /**
240     * Convert Marker to WayPoint so it can be exported to a GPX file.
241     *
242     * Override in subclasses to add all necessary attributes.
243     *
244     * @return the corresponding WayPoint with all relevant attributes
245     */
246    public WayPoint convertToWayPoint() {
247        WayPoint wpt = new WayPoint(getCoor());
248        wpt.setTimeInMillis((long) (time * 1000));
249        if (text != null) {
250            wpt.addExtension("text", text);
251        } else if (dataProvider != null) {
252            for (String key : dataProvider.getTemplateKeys()) {
253                Object value = dataProvider.getTemplateValue(key, false);
254                if (value != null && GpxConstants.WPT_KEYS.contains(key)) {
255                    wpt.put(key, value);
256                }
257            }
258        }
259        return wpt;
260    }
261
262    /**
263     * Sets the marker's coordinates.
264     * @param coor The marker's coordinates (lat/lon)
265     */
266    public final void setCoor(LatLon coor) {
267        this.coor = new CachedLatLon(coor);
268    }
269
270    /**
271     * Returns the marker's coordinates.
272     * @return The marker's coordinates (lat/lon)
273     */
274    public final LatLon getCoor() {
275        return coor;
276    }
277
278    /**
279     * Sets the marker's projected coordinates.
280     * @param eastNorth The marker's projected coordinates (easting/northing)
281     */
282    public final void setEastNorth(EastNorth eastNorth) {
283        this.coor = new CachedLatLon(eastNorth);
284    }
285
286    /**
287     * @since 12725
288     */
289    @Override
290    public double lon() {
291        return coor == null ? Double.NaN : coor.lon();
292    }
293
294    /**
295     * @since 12725
296     */
297    @Override
298    public double lat() {
299        return coor == null ? Double.NaN : coor.lat();
300    }
301
302    /**
303     * Checks whether the marker display area contains the given point.
304     * Markers not interested in mouse clicks may always return false.
305     *
306     * @param p The point to check
307     * @return <code>true</code> if the marker "hotspot" contains the point.
308     */
309    public boolean containsPoint(Point p) {
310        return false;
311    }
312
313    /**
314     * Called when the mouse is clicked in the marker's hotspot. Never
315     * called for markers which always return false from containsPoint.
316     *
317     * @param ev A dummy ActionEvent
318     */
319    public void actionPerformed(ActionEvent ev) {
320        // Do nothing
321    }
322
323    /**
324     * Paints the marker.
325     * @param g graphics context
326     * @param mv map view
327     * @param mousePressed true if the left mouse button is pressed
328     * @param showTextOrIcon true if text and icon shall be drawn
329     */
330    public void paint(Graphics g, MapView mv, boolean mousePressed, boolean showTextOrIcon) {
331        Point screen = mv.getPoint(this);
332        if (symbol != null && showTextOrIcon) {
333            paintIcon(mv, g, screen.x-symbol.getIconWidth()/2, screen.y-symbol.getIconHeight()/2);
334        } else {
335            g.drawLine(screen.x-2, screen.y-2, screen.x+2, screen.y+2);
336            g.drawLine(screen.x+2, screen.y-2, screen.x-2, screen.y+2);
337        }
338
339        String labelText = getText();
340        if ((labelText != null) && showTextOrIcon) {
341            g.drawString(labelText, screen.x+4, screen.y+2);
342        }
343    }
344
345    protected void paintIcon(MapView mv, Graphics g, int x, int y) {
346        if (!erroneous) {
347            symbol.paintIcon(mv, g, x, y);
348        } else {
349            if (redSymbol == null) {
350                int width = symbol.getIconWidth();
351                int height = symbol.getIconHeight();
352
353                redSymbol = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
354                Graphics2D gbi = redSymbol.createGraphics();
355                gbi.drawImage(symbol.getImage(), 0, 0, null);
356                gbi.setColor(Color.RED);
357                gbi.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.666f));
358                gbi.fillRect(0, 0, width, height);
359                gbi.dispose();
360            }
361            g.drawImage(redSymbol, x, y, mv);
362        }
363    }
364
365    protected TemplateEntryProperty getTextTemplate() {
366        return TemplateEntryProperty.forMarker(parentLayer.getName());
367    }
368
369    /**
370     * Returns the Text which should be displayed, depending on chosen preference
371     * @return Text of the label
372     */
373    public String getText() {
374        if (text != null)
375            return text;
376        else {
377            TemplateEntryProperty property = getTextTemplate();
378            if (property.getUpdateCount() != textVersion) {
379                TemplateEntry templateEntry = property.get();
380                StringBuilder sb = new StringBuilder();
381                templateEntry.appendText(sb, this);
382
383                cachedText = sb.toString();
384                textVersion = property.getUpdateCount();
385            }
386            return cachedText;
387        }
388    }
389
390    @Override
391    public Collection<String> getTemplateKeys() {
392        Collection<String> result;
393        if (dataProvider != null) {
394            result = dataProvider.getTemplateKeys();
395        } else {
396            result = new ArrayList<>();
397        }
398        result.add(MARKER_FORMATTED_OFFSET);
399        result.add(MARKER_OFFSET);
400        return result;
401    }
402
403    private String formatOffset() {
404        int wholeSeconds = (int) (offset + 0.5);
405        if (wholeSeconds < 60)
406            return Integer.toString(wholeSeconds);
407        else if (wholeSeconds < 3600)
408            return String.format("%d:%02d", wholeSeconds / 60, wholeSeconds % 60);
409        else
410            return String.format("%d:%02d:%02d", wholeSeconds / 3600, (wholeSeconds % 3600)/60, wholeSeconds % 60);
411    }
412
413    @Override
414    public Object getTemplateValue(String name, boolean special) {
415        if (MARKER_FORMATTED_OFFSET.equals(name))
416            return formatOffset();
417        else if (MARKER_OFFSET.equals(name))
418            return offset;
419        else if (dataProvider != null)
420            return dataProvider.getTemplateValue(name, special);
421        else
422            return null;
423    }
424
425    @Override
426    public boolean evaluateCondition(Match condition) {
427        throw new UnsupportedOperationException();
428    }
429
430    /**
431     * Determines if this marker is erroneous.
432     * @return {@code true} if this markers has any kind of error, {@code false} otherwise
433     * @since 6299
434     */
435    public final boolean isErroneous() {
436        return erroneous;
437    }
438
439    /**
440     * Sets this marker erroneous or not.
441     * @param erroneous {@code true} if this markers has any kind of error, {@code false} otherwise
442     * @since 6299
443     */
444    public final void setErroneous(boolean erroneous) {
445        this.erroneous = erroneous;
446        if (!erroneous) {
447            redSymbol = null;
448        }
449    }
450}