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

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

see #16550 - use UTC by default for GPX timestamps

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