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

Last change on this file since 16438 was 16438, checked in by simon04, 4 years ago

see #19251 - Java 8: use Stream

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