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

Last change on this file since 7672 was 7672, checked in by stoecker, 10 years ago

see #10479 - readd two markers and their keep-alive code

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