source: josm/trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/Marker.java

Last change on this file was 18613, checked in by taylor.smock, 2 months ago

Fix #21605: Add tabs to ImageViewerDialog for use with different image layers

This allows users to have multiple geotagged image layers, and
quickly switch between them.

This is a complete rework of the functionality introduced in r18591 after user feedback.

Changes:

  • Tabs are now scrollable
  • Tabs now have a close button
  • Removes the functions where plugins could send a layer in (plugins should use IGeoImageLayer instead)
  • Tabs are sorted (by layer order)
  • GeoImageLayers will use a darker red for selected but not viewed images
  • Tabs are only opened when the user clicks on a geoimage from a different layer
  • GpxMarkers now implement IQuadBucketType. This was to speed up the containsImage method in MarkerLayer.
  • Property svn:eol-style set to native
File size: 16.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer.markerlayer;
3
4import java.awt.AlphaComposite;
5import java.awt.Color;
6import java.awt.Graphics;
7import java.awt.Graphics2D;
8import java.awt.Point;
9import java.awt.Stroke;
10import java.awt.event.ActionEvent;
11import java.awt.image.BufferedImage;
12import java.io.File;
13import java.util.ArrayList;
14import java.util.Collection;
15import java.util.HashMap;
16import java.util.LinkedList;
17import java.util.List;
18import java.util.Map;
19import java.util.Objects;
20
21import javax.swing.ImageIcon;
22
23import org.openstreetmap.josm.data.IQuadBucketType;
24import org.openstreetmap.josm.data.Preferences;
25import org.openstreetmap.josm.data.coor.CachedLatLon;
26import org.openstreetmap.josm.data.coor.EastNorth;
27import org.openstreetmap.josm.data.coor.ILatLon;
28import org.openstreetmap.josm.data.coor.LatLon;
29import org.openstreetmap.josm.data.gpx.GpxConstants;
30import org.openstreetmap.josm.data.gpx.WayPoint;
31import org.openstreetmap.josm.data.osm.BBox;
32import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
33import org.openstreetmap.josm.gui.MapView;
34import org.openstreetmap.josm.gui.layer.GpxLayer;
35import org.openstreetmap.josm.gui.preferences.display.GPXSettingsPanel;
36import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
37import org.openstreetmap.josm.tools.Destroyable;
38import org.openstreetmap.josm.tools.ImageProvider;
39import org.openstreetmap.josm.tools.Logging;
40import org.openstreetmap.josm.tools.template_engine.ParseError;
41import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider;
42import org.openstreetmap.josm.tools.template_engine.TemplateEntry;
43import org.openstreetmap.josm.tools.template_engine.TemplateParser;
44
45/**
46 * Basic marker class. Requires a position, and supports
47 * a custom icon and a name.
48 *
49 * This class is also used to create appropriate Marker-type objects
50 * when waypoints are imported.
51 *
52 * It hosts a public list object, named makers, containing implementations of
53 * the MarkerMaker interface. Whenever a Marker needs to be created, each
54 * object in makers is called with the waypoint parameters (Lat/Lon and tag
55 * data), and the first one to return a Marker object wins.
56 *
57 * By default, one the list contains one default "Maker" implementation that
58 * will create AudioMarkers for supported audio files, ImageMarkers for supported image
59 * files, and WebMarkers for everything else. (The creation of a WebMarker will
60 * fail if there's no valid URL in the <link> tag, so it might still make sense
61 * to add Makers for such waypoints at the end of the list.)
62 *
63 * The default implementation only looks at the value of the <link> tag inside
64 * the <wpt> tag of the GPX file.
65 *
66 * <h2>HowTo implement a new Marker</h2>
67 * <ul>
68 * <li> Subclass Marker or ButtonMarker and override <code>containsPoint</code>
69 *      if you like to respond to user clicks</li>
70 * <li> Override paint, if you want a custom marker look (not "a label and a symbol")</li>
71 * <li> Implement MarkerCreator to return a new instance of your marker class</li>
72 * <li> In you plugin constructor, add an instance of your MarkerCreator
73 *      implementation either on top or bottom of Marker.markerProducers.
74 *      Add at top, if your marker should overwrite an current marker or at bottom
75 *      if you only add a new marker style.</li>
76 * </ul>
77 *
78 * @author Frederik Ramm
79 */
80public class Marker implements TemplateEngineDataProvider, ILatLon, Destroyable, IQuadBucketType {
81
82    /**
83     * Plugins can add their Marker creation stuff at the bottom or top of this list
84     * (depending on whether they want to override default behaviour or just add new stuff).
85     */
86    private static final List<MarkerProducers> markerProducers = new LinkedList<>();
87
88    // Add one Marker specifying the default behaviour.
89    static {
90        Marker.markerProducers.add(new DefaultMarkerProducers());
91    }
92
93    /**
94     * Add a new marker producers at the end of the JOSM list.
95     * @param mp a new marker producers
96     * @since 11850
97     */
98    public static void appendMarkerProducer(MarkerProducers mp) {
99        markerProducers.add(mp);
100    }
101
102    /**
103     * Add a new marker producers at the beginning of the JOSM list.
104     * @param mp a new marker producers
105     * @since 11850
106     */
107    public static void prependMarkerProducer(MarkerProducers mp) {
108        markerProducers.add(0, mp);
109    }
110
111    /**
112     * Returns an object of class Marker or one of its subclasses
113     * created from the parameters given.
114     *
115     * @param wpt waypoint data for marker
116     * @param relativePath An path to use for constructing relative URLs or
117     *        <code>null</code> for no relative URLs
118     * @param parentLayer the <code>MarkerLayer</code> that will contain the created <code>Marker</code>
119     * @param time time of the marker in seconds since epoch
120     * @param offset double in seconds as the time offset of this marker from
121     *        the GPX file from which it was derived (if any).
122     * @return a new Marker object
123     */
124    public static Collection<Marker> createMarkers(WayPoint wpt, File relativePath, MarkerLayer parentLayer, double time, double offset) {
125        return Marker.markerProducers.stream()
126                .map(maker -> maker.createMarkers(wpt, relativePath, parentLayer, time, offset))
127                .filter(Objects::nonNull)
128                .findFirst().orElse(null);
129    }
130
131    public static final String MARKER_OFFSET = "waypointOffset";
132    public static final String MARKER_FORMATTED_OFFSET = "formattedWaypointOffset";
133
134    public static final String LABEL_PATTERN_AUTO = "?{ '{name} ({desc})' | '{name} ({cmt})' | '{name}' | '{desc}' | '{cmt}' }";
135    public static final String LABEL_PATTERN_NAME = "{name}";
136    public static final String LABEL_PATTERN_DESC = "{desc}";
137
138    private final TemplateEngineDataProvider dataProvider;
139    private final String text;
140
141    protected final ImageIcon symbol;
142    private BufferedImage redSymbol;
143    public final MarkerLayer parentLayer;
144    /** Absolute time of marker in seconds since epoch */
145    public double time;
146    /** 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 */
147    public double offset;
148
149    private String cachedText;
150    private static Map<GpxLayer, String> cachedTemplates = new HashMap<>();
151    private String cachedDefaultTemplate;
152
153    private CachedLatLon coor;
154    private PreferenceChangedListener listener = l -> updateText();
155
156    private boolean erroneous;
157
158    public Marker(LatLon ll, TemplateEngineDataProvider dataProvider, String iconName, MarkerLayer parentLayer,
159            double time, double offset) {
160        this(ll, dataProvider, null, iconName, parentLayer, time, offset);
161    }
162
163    public Marker(LatLon ll, String text, String iconName, MarkerLayer parentLayer, double time, double offset) {
164        this(ll, null, text, iconName, parentLayer, time, offset);
165    }
166
167    private Marker(LatLon ll, TemplateEngineDataProvider dataProvider, String text, String iconName, MarkerLayer parentLayer,
168            double time, double offset) {
169        setCoor(ll);
170
171        this.offset = offset;
172        this.time = time;
173        /* tell icon checking that we expect these names to exist */
174        // /* ICON(markers/) */"Bridge"
175        // /* ICON(markers/) */"Crossing"
176        this.symbol = iconName != null ? ImageProvider.getIfAvailable("markers", iconName) : null;
177        this.parentLayer = parentLayer;
178
179        this.dataProvider = dataProvider;
180        this.text = text;
181
182        Preferences.main().addKeyPreferenceChangeListener(getPreferenceKey(), listener);
183    }
184
185    /**
186     * Convert Marker to WayPoint so it can be exported to a GPX file.
187     *
188     * Override in subclasses to add all necessary attributes.
189     *
190     * @return the corresponding WayPoint with all relevant attributes
191     */
192    public WayPoint convertToWayPoint() {
193        WayPoint wpt = new WayPoint(getCoor());
194        if (time > 0d) {
195            wpt.setTimeInMillis((long) (time * 1000));
196        }
197        if (text != null) {
198            wpt.getExtensions().add("josm", "text", text);
199        } else if (dataProvider != null) {
200            for (String key : dataProvider.getTemplateKeys()) {
201                Object value = dataProvider.getTemplateValue(key, false);
202                if (value != null && GpxConstants.WPT_KEYS.contains(key)) {
203                    wpt.put(key, value);
204                }
205            }
206        }
207        return wpt;
208    }
209
210    /**
211     * Sets the marker's coordinates.
212     * @param coor The marker's coordinates (lat/lon)
213     */
214    public final void setCoor(LatLon coor) {
215        this.coor = new CachedLatLon(coor);
216    }
217
218    /**
219     * Returns the marker's coordinates.
220     * @return The marker's coordinates (lat/lon)
221     */
222    public final LatLon getCoor() {
223        return coor;
224    }
225
226    /**
227     * Sets the marker's projected coordinates.
228     * @param eastNorth The marker's projected coordinates (easting/northing)
229     */
230    public final void setEastNorth(EastNorth eastNorth) {
231        this.coor = new CachedLatLon(eastNorth);
232    }
233
234    /**
235     * @since 12725
236     */
237    @Override
238    public double lon() {
239        return coor == null ? Double.NaN : coor.lon();
240    }
241
242    /**
243     * @since 12725
244     */
245    @Override
246    public double lat() {
247        return coor == null ? Double.NaN : coor.lat();
248    }
249
250    /**
251     * Checks whether the marker display area contains the given point.
252     * Markers not interested in mouse clicks may always return false.
253     *
254     * @param p The point to check
255     * @return <code>true</code> if the marker "hotspot" contains the point.
256     */
257    public boolean containsPoint(Point p) {
258        return false;
259    }
260
261    /**
262     * Called when the mouse is clicked in the marker's hotspot. Never
263     * called for markers which always return false from containsPoint.
264     *
265     * @param ev A dummy ActionEvent
266     */
267    public void actionPerformed(ActionEvent ev) {
268        // Do nothing
269    }
270
271    /**
272     * Paints the marker.
273     * @param g graphics context
274     * @param mv map view
275     * @param mousePressed true if the left mouse button is pressed
276     * @param showTextOrIcon true if text and icon shall be drawn
277     */
278    public void paint(Graphics2D g, MapView mv, boolean mousePressed, boolean showTextOrIcon) {
279        Point screen = mv.getPoint(this);
280        int size2 = parentLayer.markerSize / 2;
281
282        if (symbol != null && showTextOrIcon) {
283            paintIcon(mv, g, screen.x-symbol.getIconWidth()/2, screen.y-symbol.getIconHeight()/2);
284        } else {
285            Stroke stroke = g.getStroke();
286            g.setStroke(parentLayer.markerStroke);
287            g.drawLine(screen.x - size2, screen.y - size2, screen.x + size2, screen.y + size2);
288            g.drawLine(screen.x + size2, screen.y - size2, screen.x - size2, screen.y + size2);
289            g.setStroke(stroke);
290        }
291
292        String labelText = getText();
293        if (!labelText.isEmpty() && showTextOrIcon) {
294            g.drawString(labelText, screen.x + size2 + 2, screen.y + size2);
295        }
296    }
297
298    protected void paintIcon(MapView mv, Graphics g, int x, int y) {
299        if (!erroneous) {
300            symbol.paintIcon(mv, g, x, y);
301        } else {
302            if (redSymbol == null) {
303                int width = symbol.getIconWidth();
304                int height = symbol.getIconHeight();
305
306                redSymbol = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
307                Graphics2D gbi = redSymbol.createGraphics();
308                gbi.drawImage(symbol.getImage(), 0, 0, null);
309                gbi.setColor(Color.RED);
310                gbi.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.666f));
311                gbi.fillRect(0, 0, width, height);
312                gbi.dispose();
313            }
314            g.drawImage(redSymbol, x, y, mv);
315        }
316    }
317
318    protected String getTextTemplateKey() {
319        return "markers.pattern";
320    }
321
322    private String getTextTemplate() {
323        String tmpl;
324        if (cachedTemplates.containsKey(parentLayer.fromLayer)) {
325            tmpl = cachedTemplates.get(parentLayer.fromLayer);
326        } else {
327            tmpl = GPXSettingsPanel.getLayerPref(parentLayer.fromLayer, getTextTemplateKey());
328            cachedTemplates.put(parentLayer.fromLayer, tmpl);
329        }
330        return tmpl;
331    }
332
333    private String getDefaultTextTemplate() {
334        if (cachedDefaultTemplate == null) {
335            cachedDefaultTemplate = GPXSettingsPanel.getLayerPref(null, getTextTemplateKey());
336        }
337        return cachedDefaultTemplate;
338    }
339
340    /**
341     * Returns the Text which should be displayed, depending on chosen preference
342     * @return Text of the label
343     */
344    public String getText() {
345        if (text != null) {
346            return text;
347        } else if (cachedText == null) {
348            TemplateEntry template;
349            String templateString = getTextTemplate();
350            try {
351                template = new TemplateParser(templateString).parse();
352            } catch (ParseError e) {
353                Logging.debug(e);
354                String def = getDefaultTextTemplate();
355                Logging.warn("Unable to parse template engine pattern ''{0}'' for property {1}. Using default (''{2}'') instead",
356                        templateString, getTextTemplateKey(), def);
357                try {
358                    template = new TemplateParser(def).parse();
359                } catch (ParseError e1) {
360                    Logging.error(e1);
361                    cachedText = "";
362                    return "";
363                }
364            }
365            StringBuilder sb = new StringBuilder();
366            template.appendText(sb, this);
367            cachedText = sb.toString();
368
369        }
370        return cachedText;
371    }
372
373    /**
374     * Called when the template changes
375     */
376    public void updateText() {
377        cachedText = null;
378        cachedDefaultTemplate = null;
379        cachedTemplates.clear();
380    }
381
382    @Override
383    public Collection<String> getTemplateKeys() {
384        Collection<String> result;
385        if (dataProvider != null) {
386            result = dataProvider.getTemplateKeys();
387        } else {
388            result = new ArrayList<>();
389        }
390        result.add(MARKER_FORMATTED_OFFSET);
391        result.add(MARKER_OFFSET);
392        return result;
393    }
394
395    private String formatOffset() {
396        int wholeSeconds = (int) (offset + 0.5);
397        if (wholeSeconds < 60)
398            return Integer.toString(wholeSeconds);
399        else if (wholeSeconds < 3600)
400            return String.format("%d:%02d", wholeSeconds / 60, wholeSeconds % 60);
401        else
402            return String.format("%d:%02d:%02d", wholeSeconds / 3600, (wholeSeconds % 3600)/60, wholeSeconds % 60);
403    }
404
405    @Override
406    public Object getTemplateValue(String name, boolean special) {
407        if (MARKER_FORMATTED_OFFSET.equals(name))
408            return formatOffset();
409        else if (MARKER_OFFSET.equals(name))
410            return offset;
411        else if (dataProvider != null)
412            return dataProvider.getTemplateValue(name, special);
413        else
414            return null;
415    }
416
417    @Override
418    public boolean evaluateCondition(Match condition) {
419        throw new UnsupportedOperationException();
420    }
421
422    /**
423     * Determines if this marker is erroneous.
424     * @return {@code true} if this markers has any kind of error, {@code false} otherwise
425     * @since 6299
426     */
427    public final boolean isErroneous() {
428        return erroneous;
429    }
430
431    /**
432     * Sets this marker erroneous or not.
433     * @param erroneous {@code true} if this markers has any kind of error, {@code false} otherwise
434     * @since 6299
435     */
436    public final void setErroneous(boolean erroneous) {
437        this.erroneous = erroneous;
438        if (!erroneous) {
439            redSymbol = null;
440        }
441    }
442
443    @Override
444    public void destroy() {
445        cachedTemplates.clear();
446        Preferences.main().removeKeyPreferenceChangeListener(getPreferenceKey(), listener);
447    }
448
449    private String getPreferenceKey() {
450        return "draw.rawgps." + getTextTemplateKey();
451    }
452
453    @Override
454    public BBox getBBox() {
455        return new BBox(this);
456    }
457}
Note: See TracBrowser for help on using the repository browser.