001 // License: GPL. Copyright 2008 by Immanuel Scholz and others
002 package org.openstreetmap.josm.gui.layer.markerlayer;
003
004 import java.awt.Graphics;
005 import java.awt.Point;
006 import java.awt.event.ActionEvent;
007 import java.io.File;
008 import java.net.MalformedURLException;
009 import java.net.URL;
010 import java.util.ArrayList;
011 import java.util.Collection;
012 import java.util.HashMap;
013 import java.util.LinkedList;
014 import java.util.List;
015 import java.util.Map;
016
017 import javax.swing.Icon;
018
019 import org.openstreetmap.josm.Main;
020 import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
021 import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
022 import org.openstreetmap.josm.data.coor.CachedLatLon;
023 import org.openstreetmap.josm.data.coor.EastNorth;
024 import org.openstreetmap.josm.data.coor.LatLon;
025 import org.openstreetmap.josm.data.gpx.GpxData;
026 import org.openstreetmap.josm.data.gpx.GpxLink;
027 import org.openstreetmap.josm.data.gpx.WayPoint;
028 import org.openstreetmap.josm.data.preferences.CachedProperty;
029 import org.openstreetmap.josm.data.preferences.IntegerProperty;
030 import org.openstreetmap.josm.gui.MapView;
031 import org.openstreetmap.josm.tools.ImageProvider;
032 import org.openstreetmap.josm.tools.template_engine.ParseError;
033 import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider;
034 import org.openstreetmap.josm.tools.template_engine.TemplateEntry;
035 import org.openstreetmap.josm.tools.template_engine.TemplateParser;
036
037 /**
038 * Basic marker class. Requires a position, and supports
039 * a custom icon and a name.
040 *
041 * This class is also used to create appropriate Marker-type objects
042 * when waypoints are imported.
043 *
044 * It hosts a public list object, named makers, containing implementations of
045 * the MarkerMaker interface. Whenever a Marker needs to be created, each
046 * object in makers is called with the waypoint parameters (Lat/Lon and tag
047 * data), and the first one to return a Marker object wins.
048 *
049 * By default, one the list contains one default "Maker" implementation that
050 * will create AudioMarkers for .wav files, ImageMarkers for .png/.jpg/.jpeg
051 * files, and WebMarkers for everything else. (The creation of a WebMarker will
052 * fail if there's no valid URL in the <link> tag, so it might still make sense
053 * to add Makers for such waypoints at the end of the list.)
054 *
055 * The default implementation only looks at the value of the <link> tag inside
056 * the <wpt> tag of the GPX file.
057 *
058 * <h2>HowTo implement a new Marker</h2>
059 * <ul>
060 * <li> Subclass Marker or ButtonMarker and override <code>containsPoint</code>
061 * if you like to respond to user clicks</li>
062 * <li> Override paint, if you want a custom marker look (not "a label and a symbol")</li>
063 * <li> Implement MarkerCreator to return a new instance of your marker class</li>
064 * <li> In you plugin constructor, add an instance of your MarkerCreator
065 * implementation either on top or bottom of Marker.markerProducers.
066 * Add at top, if your marker should overwrite an current marker or at bottom
067 * if you only add a new marker style.</li>
068 * </ul>
069 *
070 * @author Frederik Ramm <frederik@remote.org>
071 */
072 public class Marker implements TemplateEngineDataProvider {
073
074 public static class TemplateEntryProperty extends CachedProperty<TemplateEntry> {
075 // This class is a bit complicated because it supports both global and per layer settings. I've added per layer settings because
076 // GPXSettingsPanel had possibility to set waypoint label but then I've realized that markers use different layer then gpx data
077 // so per layer settings is useless. Anyway it's possible to specify marker layer pattern in Einstein preferences and maybe somebody
078 // will make gui for it so I'm keeping it here
079
080 private final static Map<String, TemplateEntryProperty> cache = new HashMap<String, TemplateEntryProperty>();
081
082 // Legacy code - convert label from int to template engine expression
083 private static final IntegerProperty PROP_LABEL = new IntegerProperty("draw.rawgps.layer.wpt", 0 );
084 private static String getDefaultLabelPattern() {
085 switch (PROP_LABEL.get()) {
086 case 1:
087 return LABEL_PATTERN_NAME;
088 case 2:
089 return LABEL_PATTERN_DESC;
090 case 0:
091 case 3:
092 return LABEL_PATTERN_AUTO;
093 default:
094 return "";
095 }
096 }
097
098 public static TemplateEntryProperty forMarker(String layerName) {
099 String key = "draw.rawgps.layer.wpt.pattern";
100 if (layerName != null) {
101 key += "." + layerName;
102 }
103 TemplateEntryProperty result = cache.get(key);
104 if (result == null) {
105 String defaultValue = layerName == null ? getDefaultLabelPattern():"";
106 TemplateEntryProperty parent = layerName == null ? null : forMarker(null);
107 try {
108 result = new TemplateEntryProperty(key, defaultValue, parent);
109 cache.put(key, result);
110 } catch (ParseError e) {
111 Main.warn("Unable to parse template engine pattern ''{0}'' for property {1}", defaultValue, key);
112 }
113 }
114 return result;
115 }
116
117 public static TemplateEntryProperty forAudioMarker(String layerName) {
118 String key = "draw.rawgps.layer.audiowpt.pattern";
119 if (layerName != null) {
120 key += "." + layerName;
121 }
122 TemplateEntryProperty result = cache.get(key);
123 if (result == null) {
124 String defaultValue = layerName == null?"?{ '{name}' | '{desc}' | '{" + Marker.MARKER_FORMATTED_OFFSET + "}' }":"";
125 TemplateEntryProperty parent = layerName == null ? null : forAudioMarker(null);
126 try {
127 result = new TemplateEntryProperty(key, defaultValue, parent);
128 cache.put(key, result);
129 } catch (ParseError e) {
130 Main.warn("Unable to parse template engine pattern ''{0}'' for property {1}", defaultValue, key);
131 }
132 }
133 return result;
134 }
135
136 private TemplateEntryProperty parent;
137
138
139 private TemplateEntryProperty(String key, String defaultValue, TemplateEntryProperty parent) throws ParseError {
140 super(key, defaultValue);
141 this.parent = parent;
142 updateValue(); // Needs to be called because parent wasn't know in super constructor
143 }
144
145 @Override
146 protected TemplateEntry fromString(String s) {
147 try {
148 return new TemplateParser(s).parse();
149 } catch (ParseError e) {
150 Main.warn("Unable to parse template engine pattern ''{0}'' for property {1}. Using default (''{2}'') instead",
151 s, getKey(), super.getDefaultValueAsString());
152 return getDefaultValue();
153 }
154 }
155
156 @Override
157 public String getDefaultValueAsString() {
158 if (parent == null)
159 return super.getDefaultValueAsString();
160 else
161 return parent.getAsString();
162 }
163
164 @Override
165 public void preferenceChanged(PreferenceChangeEvent e) {
166 if (e.getKey().equals(key) || (parent != null && e.getKey().equals(parent.getKey()))) {
167 updateValue();
168 }
169 }
170 }
171
172 /**
173 * Plugins can add their Marker creation stuff at the bottom or top of this list
174 * (depending on whether they want to override default behaviour or just add new
175 * stuff).
176 */
177 public static final List<MarkerProducers> markerProducers = new LinkedList<MarkerProducers>();
178
179 // Add one Marker specifying the default behaviour.
180 static {
181 Marker.markerProducers.add(new MarkerProducers() {
182 @SuppressWarnings("unchecked")
183 public Marker createMarker(WayPoint wpt, File relativePath, MarkerLayer parentLayer, double time, double offset) {
184 String uri = null;
185 // cheapest way to check whether "link" object exists and is a non-empty
186 // collection of GpxLink objects...
187 Collection<GpxLink> links = (Collection<GpxLink>)wpt.attr.get(GpxData.META_LINKS);
188 if (links != null) {
189 for (GpxLink oneLink : links ) {
190 uri = oneLink.uri;
191 break;
192 }
193 }
194
195 URL url = null;
196 if (uri != null) {
197 try {
198 url = new URL(uri);
199 } catch (MalformedURLException e) {
200 // Try a relative file:// url, if the link is not in an URL-compatible form
201 if (relativePath != null) {
202 try {
203 url = new File(relativePath.getParentFile(), uri).toURI().toURL();
204 } catch (MalformedURLException e1) {
205 Main.warn("Unable to convert uri {0} to URL: {1}", uri, e1.getMessage());
206 }
207 }
208 }
209 }
210
211 if (url == null) {
212 String symbolName = wpt.getString("symbol");
213 if (symbolName == null) {
214 symbolName = wpt.getString("sym");
215 }
216 return new Marker(wpt.getCoor(), wpt, symbolName, parentLayer, time, offset);
217 }
218 else if (url.toString().endsWith(".wav")) {
219 return new AudioMarker(wpt.getCoor(), wpt, url, parentLayer, time, offset);
220 } else if (url.toString().endsWith(".png") || url.toString().endsWith(".jpg") || url.toString().endsWith(".jpeg") || url.toString().endsWith(".gif")) {
221 return new ImageMarker(wpt.getCoor(), url, parentLayer, time, offset);
222 } else {
223 return new WebMarker(wpt.getCoor(), url, parentLayer, time, offset);
224 }
225 }
226 });
227 }
228
229 /**
230 * Returns an object of class Marker or one of its subclasses
231 * created from the parameters given.
232 *
233 * @param wpt waypoint data for marker
234 * @param relativePath An path to use for constructing relative URLs or
235 * <code>null</code> for no relative URLs
236 * @param offset double in seconds as the time offset of this marker from
237 * the GPX file from which it was derived (if any).
238 * @return a new Marker object
239 */
240 public static Marker createMarker(WayPoint wpt, File relativePath, MarkerLayer parentLayer, double time, double offset) {
241 for (MarkerProducers maker : Marker.markerProducers) {
242 Marker marker = maker.createMarker(wpt, relativePath, parentLayer, time, offset);
243 if (marker != null)
244 return marker;
245 }
246 return null;
247 }
248
249 public static final String MARKER_OFFSET = "waypointOffset";
250 public static final String MARKER_FORMATTED_OFFSET = "formattedWaypointOffset";
251
252 public static final String LABEL_PATTERN_AUTO = "?{ '{name} - {desc}' | '{name}' | '{desc}' }";
253 public static final String LABEL_PATTERN_NAME = "{name}";
254 public static final String LABEL_PATTERN_DESC = "{desc}";
255
256
257 private final TemplateEngineDataProvider dataProvider;
258 private final String text;
259
260 public final Icon symbol;
261 public final MarkerLayer parentLayer;
262 public double time; /* absolute time of marker since epoch */
263 public double offset; /* time offset in seconds from the gpx point from which it was derived,
264 may be adjusted later to sync with other data, so not final */
265
266 private String cachedText;
267 private int textVersion = -1;
268 private CachedLatLon coor;
269
270 public Marker(LatLon ll, TemplateEngineDataProvider dataProvider, String iconName, MarkerLayer parentLayer, double time, double offset) {
271 setCoor(ll);
272
273 this.offset = offset;
274 this.time = time;
275 // /* ICON(markers/) */"Bridge"
276 // /* ICON(markers/) */"Crossing"
277 this.symbol = iconName != null ? ImageProvider.getIfAvailable("markers",iconName) : null;
278 this.parentLayer = parentLayer;
279
280 this.dataProvider = dataProvider;
281 this.text = null;
282 }
283
284 public Marker(LatLon ll, String text, String iconName, MarkerLayer parentLayer, double time, double offset) {
285 setCoor(ll);
286
287 this.offset = offset;
288 this.time = time;
289 // /* ICON(markers/) */"Bridge"
290 // /* ICON(markers/) */"Crossing"
291 this.symbol = iconName != null ? ImageProvider.getIfAvailable("markers",iconName) : null;
292 this.parentLayer = parentLayer;
293
294 this.dataProvider = null;
295 this.text = text;
296 }
297
298 public final void setCoor(LatLon coor) {
299 if(this.coor == null) {
300 this.coor = new CachedLatLon(coor);
301 } else {
302 this.coor.setCoor(coor);
303 }
304 }
305
306 public final LatLon getCoor() {
307 return coor;
308 }
309
310 public final void setEastNorth(EastNorth eastNorth) {
311 coor.setEastNorth(eastNorth);
312 }
313
314 public final EastNorth getEastNorth() {
315 return coor.getEastNorth();
316 }
317
318
319 /**
320 * Checks whether the marker display area contains the given point.
321 * Markers not interested in mouse clicks may always return false.
322 *
323 * @param p The point to check
324 * @return <code>true</code> if the marker "hotspot" contains the point.
325 */
326 public boolean containsPoint(Point p) {
327 return false;
328 }
329
330 /**
331 * Called when the mouse is clicked in the marker's hotspot. Never
332 * called for markers which always return false from containsPoint.
333 *
334 * @param ev A dummy ActionEvent
335 */
336 public void actionPerformed(ActionEvent ev) {
337 }
338
339
340 /**
341 * Paints the marker.
342 * @param g graphics context
343 * @param mv map view
344 * @param mousePressed true if the left mouse button is pressed
345 */
346 public void paint(Graphics g, MapView mv, boolean mousePressed, boolean showTextOrIcon) {
347 Point screen = mv.getPoint(getEastNorth());
348 if (symbol != null && showTextOrIcon) {
349 symbol.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
362 protected TemplateEntryProperty getTextTemplate() {
363 return TemplateEntryProperty.forMarker(parentLayer.getName());
364 }
365
366 /**
367 * Returns the Text which should be displayed, depending on chosen preference
368 * @return Text of the label
369 */
370 public String getText() {
371 if (text != null)
372 return text;
373 else {
374 TemplateEntryProperty property = getTextTemplate();
375 if (property.getUpdateCount() != textVersion) {
376 TemplateEntry templateEntry = property.get();
377 StringBuilder sb = new StringBuilder();
378 templateEntry.appendText(sb, this);
379
380 cachedText = sb.toString();
381 textVersion = property.getUpdateCount();
382 }
383 return cachedText;
384 }
385 }
386
387 @Override
388 public Collection<String> getTemplateKeys() {
389 Collection<String> result;
390 if (dataProvider != null) {
391 result = dataProvider.getTemplateKeys();
392 } else {
393 result = new ArrayList<String>();
394 }
395 result.add(MARKER_FORMATTED_OFFSET);
396 result.add(MARKER_OFFSET);
397 return result;
398 }
399
400 private String formatOffset () {
401 int wholeSeconds = (int)(offset + 0.5);
402 if (wholeSeconds < 60)
403 return Integer.toString(wholeSeconds);
404 else if (wholeSeconds < 3600)
405 return String.format("%d:%02d", wholeSeconds / 60, wholeSeconds % 60);
406 else
407 return String.format("%d:%02d:%02d", wholeSeconds / 3600, (wholeSeconds % 3600)/60, wholeSeconds % 60);
408 }
409
410 @Override
411 public Object getTemplateValue(String name, boolean special) {
412 if (MARKER_FORMATTED_OFFSET.equals(name))
413 return formatOffset();
414 else if (MARKER_OFFSET.equals(name))
415 return offset;
416 else if (dataProvider != null)
417 return dataProvider.getTemplateValue(name, special);
418 else
419 return null;
420 }
421
422 @Override
423 public boolean evaluateCondition(Match condition) {
424 throw new UnsupportedOperationException();
425 }
426 }