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

Last change on this file since 5684 was 5684, checked in by bastiK, 11 years ago

add session support for marker layers (see #4029)

The data is exported to a separate GPX file that contains one waypoint for each marker.
This is not very elegant, because most of the time, all the info is already contained in the original GPX File.
However, when dealing with audio markers, they can be synchronized, or additional markers are added
at certain playback positions. This info must be retained.
Another complication is, that two or more MarkerLayers can be merged to one.

All these problems are avoided by explicitly exporting the markers to a separate file (as done in this commit).

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