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

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

fix #20913 - fix handling of GPX files in sessions (patch by Bjoeni)

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