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

Last change on this file since 6070 was 6070, checked in by stoecker, 11 years ago

see #8853 remove tabs, trailing spaces, windows line ends, strange characters

  • 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.