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

Last change on this file since 12725 was 12725, checked in by bastiK, 7 years ago

see #15229 - deprecate ILatLon#getEastNorth() so ILatLon has no dependency on Main.proj

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