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

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

sonar - squid:S2386 - Mutable fields should not be "public static"

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