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