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

Last change on this file since 14456 was 14456, checked in by Don-vip, 5 years ago

fix #16995 - de-duplicate storage of timestamp within WayPoint and refactor some methods, added documentation, added some robustness against legacy code (will also log a warning if detected). Patch by cmuelle8, modified

  • Property svn:eol-style set to native
File size: 16.9 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.event.ActionEvent;
10import java.awt.image.BufferedImage;
11import java.io.File;
12import java.util.ArrayList;
13import java.util.Collection;
14import java.util.HashMap;
15import java.util.LinkedList;
16import java.util.List;
17import java.util.Map;
18
19import javax.swing.ImageIcon;
20
21import org.openstreetmap.josm.data.coor.CachedLatLon;
22import org.openstreetmap.josm.data.coor.EastNorth;
23import org.openstreetmap.josm.data.coor.ILatLon;
24import org.openstreetmap.josm.data.coor.LatLon;
25import org.openstreetmap.josm.data.gpx.GpxConstants;
26import org.openstreetmap.josm.data.gpx.WayPoint;
27import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
28import org.openstreetmap.josm.data.preferences.CachedProperty;
29import org.openstreetmap.josm.gui.MapView;
30import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
31import org.openstreetmap.josm.tools.ImageProvider;
32import org.openstreetmap.josm.tools.Logging;
33import org.openstreetmap.josm.tools.template_engine.ParseError;
34import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider;
35import org.openstreetmap.josm.tools.template_engine.TemplateEntry;
36import org.openstreetmap.josm.tools.template_engine.TemplateParser;
37
38/**
39 * Basic marker class. Requires a position, and supports
40 * a custom icon and a name.
41 *
42 * This class is also used to create appropriate Marker-type objects
43 * when waypoints are imported.
44 *
45 * It hosts a public list object, named makers, containing implementations of
46 * the MarkerMaker interface. Whenever a Marker needs to be created, each
47 * object in makers is called with the waypoint parameters (Lat/Lon and tag
48 * data), and the first one to return a Marker object wins.
49 *
50 * By default, one the list contains one default "Maker" implementation that
51 * will create AudioMarkers for supported audio files, ImageMarkers for supported image
52 * files, and WebMarkers for everything else. (The creation of a WebMarker will
53 * fail if there's no valid URL in the <link> tag, so it might still make sense
54 * to add Makers for such waypoints at the end of the list.)
55 *
56 * The default implementation only looks at the value of the <link> tag inside
57 * the <wpt> tag of the GPX file.
58 *
59 * <h2>HowTo implement a new Marker</h2>
60 * <ul>
61 * <li> Subclass Marker or ButtonMarker and override <code>containsPoint</code>
62 * if you like to respond to user clicks</li>
63 * <li> Override paint, if you want a custom marker look (not "a label and a symbol")</li>
64 * <li> Implement MarkerCreator to return a new instance of your marker class</li>
65 * <li> In you plugin constructor, add an instance of your MarkerCreator
66 * implementation either on top or bottom of Marker.markerProducers.
67 * Add at top, if your marker should overwrite an current marker or at bottom
68 * if you only add a new marker style.</li>
69 * </ul>
70 *
71 * @author Frederik Ramm
72 */
73public class Marker implements TemplateEngineDataProvider, ILatLon {
74
75 public static final class TemplateEntryProperty extends CachedProperty<TemplateEntry> {
76 // This class is a bit complicated because it supports both global and per layer settings. I've added per layer settings because
77 // GPXSettingsPanel had possibility to set waypoint label but then I've realized that markers use different layer then gpx data
78 // so per layer settings is useless. Anyway it's possible to specify marker layer pattern in Einstein preferences and maybe somebody
79 // will make gui for it so I'm keeping it here
80
81 private static final Map<String, TemplateEntryProperty> CACHE = new HashMap<>();
82
83 public static TemplateEntryProperty forMarker(String layerName) {
84 String key = "draw.rawgps.layer.wpt.pattern";
85 if (layerName != null) {
86 key += '.' + layerName;
87 }
88 TemplateEntryProperty result = CACHE.get(key);
89 if (result == null) {
90 String defaultValue = layerName == null ? LABEL_PATTERN_AUTO : "";
91 TemplateEntryProperty parent = layerName == null ? null : forMarker(null);
92 result = new TemplateEntryProperty(key, defaultValue, parent);
93 CACHE.put(key, result);
94 }
95 return result;
96 }
97
98 public static TemplateEntryProperty forAudioMarker(String layerName) {
99 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}
Note: See TracBrowser for help on using the repository browser.