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