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

Last change on this file since 14759 was 14759, checked in by simon04, 5 years ago

Marker: avoid ConcurrentModificationException

Relates to r14747.

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