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

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

fix #14103 - GPX → OSM: convert additional tags

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