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

Last change on this file since 4431 was 4431, checked in by jttt, 13 years ago

Custom primitive name formatters via tagging presets

  • Property svn:eol-style set to native
File size: 16.0 KB
Line 
1// License: GPL. Copyright 2008 by Immanuel Scholz and others
2package org.openstreetmap.josm.gui.layer.markerlayer;
3
4import java.awt.Graphics;
5import java.awt.Point;
6import java.awt.event.ActionEvent;
7import java.io.File;
8import java.net.MalformedURLException;
9import java.net.URL;
10import java.util.ArrayList;
11import java.util.Collection;
12import java.util.HashMap;
13import java.util.LinkedList;
14import java.util.List;
15import java.util.Map;
16
17import javax.swing.Icon;
18
19import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
20import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
21import org.openstreetmap.josm.data.coor.CachedLatLon;
22import org.openstreetmap.josm.data.coor.EastNorth;
23import org.openstreetmap.josm.data.coor.LatLon;
24import org.openstreetmap.josm.data.gpx.GpxData;
25import org.openstreetmap.josm.data.gpx.GpxLink;
26import org.openstreetmap.josm.data.gpx.WayPoint;
27import org.openstreetmap.josm.data.preferences.CachedProperty;
28import org.openstreetmap.josm.data.preferences.IntegerProperty;
29import org.openstreetmap.josm.gui.MapView;
30import org.openstreetmap.josm.tools.ImageProvider;
31import org.openstreetmap.josm.tools.template_engine.ParseError;
32import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider;
33import org.openstreetmap.josm.tools.template_engine.TemplateEntry;
34import org.openstreetmap.josm.tools.template_engine.TemplateParser;
35
36/**
37 * Basic marker class. Requires a position, and supports
38 * a custom icon and a name.
39 *
40 * This class is also used to create appropriate Marker-type objects
41 * when waypoints are imported.
42 *
43 * It hosts a public list object, named makers, containing implementations of
44 * the MarkerMaker interface. Whenever a Marker needs to be created, each
45 * object in makers is called with the waypoint parameters (Lat/Lon and tag
46 * data), and the first one to return a Marker object wins.
47 *
48 * By default, one the list contains one default "Maker" implementation that
49 * will create AudioMarkers for .wav files, ImageMarkers for .png/.jpg/.jpeg
50 * files, and WebMarkers for everything else. (The creation of a WebMarker will
51 * fail if there's no vaild URL in the <link> tag, so it might still make sense
52 * to add Makers for such waypoints at the end of the list.)
53 *
54 * The default implementation only looks at the value of the <link> tag inside
55 * the <wpt> tag of the GPX file.
56 *
57 * <h2>HowTo implement a new Marker</h2>
58 * <ul>
59 * <li> Subclass Marker or ButtonMarker and override <code>containsPoint</code>
60 * if you like to respond to user clicks</li>
61 * <li> Override paint, if you want a custom marker look (not "a label and a symbol")</li>
62 * <li> Implement MarkerCreator to return a new instance of your marker class</li>
63 * <li> In you plugin constructor, add an instance of your MarkerCreator
64 * implementation either on top or bottom of Marker.markerProducers.
65 * Add at top, if your marker should overwrite an current marker or at bottom
66 * if you only add a new marker style.</li>
67 * </ul>
68 *
69 * @author Frederik Ramm <frederik@remote.org>
70 */
71public class Marker implements TemplateEngineDataProvider {
72
73 public static class TemplateEntryProperty extends CachedProperty<TemplateEntry> {
74 // This class is a bit complicated because it supports both global and per layer settings. I've added per layer settings because
75 // GPXSettingsPanel had possibility to set waypoint label but then I've realized that markers use different layer then gpx data
76 // so per layer settings is useless. Anyway it's possible to specify marker layer pattern in Einstein preferences and maybe somebody
77 // will make gui for it so I'm keeping it here
78
79 private final static Map<String, TemplateEntryProperty> cache = new HashMap<String, Marker.TemplateEntryProperty>();
80
81 // Legacy code - convert label from int to template engine expression
82 private static final IntegerProperty PROP_LABEL = new IntegerProperty("draw.rawgps.layer.wpt", 0 );
83 private static String getDefaultLabelPattern() {
84 switch (PROP_LABEL.get()) {
85 case 1:
86 return LABEL_PATTERN_NAME;
87 case 2:
88 return LABEL_PATTERN_DESC;
89 case 0:
90 case 3:
91 return LABEL_PATTERN_AUTO;
92 default:
93 return "";
94 }
95 }
96
97 public static TemplateEntryProperty forMarker(String layerName) {
98 String key = "draw.rawgps.layer.wpt.pattern";
99 if (layerName != null) {
100 key += "." + layerName;
101 }
102 TemplateEntryProperty result = cache.get(key);
103 if (result == null) {
104 String defaultValue = layerName == null?getDefaultLabelPattern():"";
105 TemplateEntryProperty parent = layerName == null?null:forMarker(null);
106 result = new TemplateEntryProperty(key, defaultValue, parent);
107 cache.put(key, result);
108 }
109 return result;
110 }
111
112 public static TemplateEntryProperty forAudioMarker(String layerName) {
113 String key = "draw.rawgps.layer.audiowpt.pattern";
114 if (layerName != null) {
115 key += "." + layerName;
116 }
117 TemplateEntryProperty result = cache.get(key);
118 if (result == null) {
119 String defaultValue = layerName == null?"?{ '{name}' | '{desc}' | '{" + Marker.MARKER_FORMATTED_OFFSET + "}' }":"";
120 TemplateEntryProperty parent = layerName == null?null:forAudioMarker(null);
121 result = new TemplateEntryProperty(key, defaultValue, parent);
122 cache.put(key, result);
123 }
124 return result;
125 }
126
127 private TemplateEntryProperty parent;
128
129
130 private TemplateEntryProperty(String key, String defaultValue, TemplateEntryProperty parent) {
131 super(key, defaultValue);
132 this.parent = parent;
133 updateValue(); // Needs to be called because parent wasn't know in super constructor
134 }
135
136 @Override
137 protected TemplateEntry fromString(String s) {
138 try {
139 return new TemplateParser(s).parse();
140 } catch (ParseError e) {
141 System.out.println(String.format("Unable to parse template engine pattern '%s' for property %s. Using default ('%s') instead",
142 s, getKey(), defaultValue));
143 return getDefaultValue();
144 }
145 }
146
147 @Override
148 public String getDefaultValueAsString() {
149 if (parent == null)
150 return super.getDefaultValueAsString();
151 else
152 return parent.getAsString();
153 }
154
155 @Override
156 public void preferenceChanged(PreferenceChangeEvent e) {
157 if (e.getKey().equals(key) || (parent != null && e.getKey().equals(parent.getKey()))) {
158 updateValue();
159 }
160 }
161 }
162
163
164 /**
165 * Plugins can add their Marker creation stuff at the bottom or top of this list
166 * (depending on whether they want to override default behaviour or just add new
167 * stuff).
168 */
169 public static List<MarkerProducers> markerProducers = new LinkedList<MarkerProducers>();
170
171 // Add one Maker specifying the default behaviour.
172 static {
173 Marker.markerProducers.add(new MarkerProducers() {
174 @SuppressWarnings("unchecked")
175 public Marker createMarker(WayPoint wpt, File relativePath, MarkerLayer parentLayer, double time, double offset) {
176 String uri = null;
177 // cheapest way to check whether "link" object exists and is a non-empty
178 // collection of GpxLink objects...
179 Collection<GpxLink> links = (Collection<GpxLink>)wpt.attr.get(GpxData.META_LINKS);
180 if (links != null) {
181 for (GpxLink oneLink : links ) {
182 uri = oneLink.uri;
183 break;
184 }
185 }
186
187 URL url = null;
188 if (uri != null) {
189 try {
190 url = new URL(uri);
191 } catch (MalformedURLException e) {
192 // Try a relative file:// url, if the link is not in an URL-compatible form
193 if (relativePath != null) {
194 try {
195 url = new File(relativePath.getParentFile(), uri).toURI().toURL();
196 } catch (MalformedURLException e1) {
197 System.err.println("Unable to convert uri " + uri + " to URL: " + e1.getMessage());
198 }
199 }
200 }
201 }
202
203
204 if (url == null) {
205 String symbolName = wpt.getString("symbol");
206 if (symbolName == null) {
207 symbolName = wpt.getString("sym");
208 }
209 return new Marker(wpt.getCoor(), wpt, symbolName, parentLayer, time, offset);
210 }
211 else if (url.toString().endsWith(".wav"))
212 return new AudioMarker(wpt.getCoor(), wpt, url, parentLayer, time, offset);
213 else if (url.toString().endsWith(".png") || url.toString().endsWith(".jpg") || url.toString().endsWith(".jpeg") || url.toString().endsWith(".gif"))
214 return new ImageMarker(wpt.getCoor(), url, parentLayer, time, offset);
215 else
216 return new WebMarker(wpt.getCoor(), url, parentLayer, time, offset);
217 }
218 });
219 }
220
221 /**
222 * Returns an object of class Marker or one of its subclasses
223 * created from the parameters given.
224 *
225 * @param wpt waypoint data for marker
226 * @param relativePath An path to use for constructing relative URLs or
227 * <code>null</code> for no relative URLs
228 * @param offset double in seconds as the time offset of this marker from
229 * the GPX file from which it was derived (if any).
230 * @return a new Marker object
231 */
232 public static Marker createMarker(WayPoint wpt, File relativePath, MarkerLayer parentLayer, double time, double offset) {
233 for (MarkerProducers maker : Marker.markerProducers) {
234 Marker marker = maker.createMarker(wpt, relativePath, parentLayer, time, offset);
235 if (marker != null)
236 return marker;
237 }
238 return null;
239 }
240
241 public static final String MARKER_OFFSET = "waypointOffset";
242 public static final String MARKER_FORMATTED_OFFSET = "formattedWaypointOffset";
243
244 public static final String LABEL_PATTERN_AUTO = "?{ '{name} - {desc}' | '{name}' | '{desc}' }";
245 public static final String LABEL_PATTERN_NAME = "{name}";
246 public static final String LABEL_PATTERN_DESC = "{desc}";
247
248
249 private final TemplateEngineDataProvider dataProvider;
250 private final String text;
251
252 public final Icon symbol;
253 public final MarkerLayer parentLayer;
254 public double time; /* absolute time of marker since epoch */
255 public double offset; /* time offset in seconds from the gpx point from which it was derived,
256 may be adjusted later to sync with other data, so not final */
257
258 private String cachedText;
259 private int textVersion = -1;
260 private CachedLatLon coor;
261
262 public Marker(LatLon ll, TemplateEngineDataProvider dataProvider, String iconName, MarkerLayer parentLayer, double time, double offset) {
263 setCoor(ll);
264
265 this.offset = offset;
266 this.time = time;
267 // /* ICON(markers/) */"Bridge"
268 // /* ICON(markers/) */"Crossing"
269 this.symbol = ImageProvider.getIfAvailable("markers",iconName);
270 this.parentLayer = parentLayer;
271
272 this.dataProvider = dataProvider;
273 this.text = null;
274 }
275
276 public Marker(LatLon ll, String text, String iconName, MarkerLayer parentLayer, double time, double offset) {
277 setCoor(ll);
278
279 this.offset = offset;
280 this.time = time;
281 // /* ICON(markers/) */"Bridge"
282 // /* ICON(markers/) */"Crossing"
283 this.symbol = ImageProvider.getIfAvailable("markers",iconName);
284 this.parentLayer = parentLayer;
285
286 this.dataProvider = null;
287 this.text = text;
288 }
289
290 public final void setCoor(LatLon coor) {
291 if(this.coor == null) {
292 this.coor = new CachedLatLon(coor);
293 } else {
294 this.coor.setCoor(coor);
295 }
296 }
297
298 public final LatLon getCoor() {
299 return coor;
300 }
301
302 public final void setEastNorth(EastNorth eastNorth) {
303 coor.setEastNorth(eastNorth);
304 }
305
306 public final EastNorth getEastNorth() {
307 return coor.getEastNorth();
308 }
309
310
311 /**
312 * Checks whether the marker display area contains the given point.
313 * Markers not interested in mouse clicks may always return false.
314 *
315 * @param p The point to check
316 * @return <code>true</code> if the marker "hotspot" contains the point.
317 */
318 public boolean containsPoint(Point p) {
319 return false;
320 }
321
322 /**
323 * Called when the mouse is clicked in the marker's hotspot. Never
324 * called for markers which always return false from containsPoint.
325 *
326 * @param ev A dummy ActionEvent
327 */
328 public void actionPerformed(ActionEvent ev) {
329 }
330
331
332 /**
333 * Paints the marker.
334 * @param g graphics context
335 * @param mv map view
336 * @param mousePressed true if the left mouse button is pressed
337 */
338 public void paint(Graphics g, MapView mv, boolean mousePressed, boolean showTextOrIcon) {
339 Point screen = mv.getPoint(getEastNorth());
340 if (symbol != null && showTextOrIcon) {
341 symbol.paintIcon(mv, g, screen.x-symbol.getIconWidth()/2, screen.y-symbol.getIconHeight()/2);
342 } else {
343 g.drawLine(screen.x-2, screen.y-2, screen.x+2, screen.y+2);
344 g.drawLine(screen.x+2, screen.y-2, screen.x-2, screen.y+2);
345 }
346
347 String labelText = getText();
348 if ((labelText != null) && showTextOrIcon) {
349 g.drawString(labelText, screen.x+4, screen.y+2);
350 }
351 }
352
353
354 protected TemplateEntryProperty getTextTemplate() {
355 return TemplateEntryProperty.forMarker(parentLayer.getName());
356 }
357
358 /**
359 * Returns the Text which should be displayed, depending on chosen preference
360 * @return Text
361 */
362 public String getText() {
363 if (text != null)
364 return text;
365 else {
366 TemplateEntryProperty property = getTextTemplate();
367 if (property.getUpdateCount() != textVersion) {
368 TemplateEntry templateEntry = property.get();
369 StringBuilder sb = new StringBuilder();
370 templateEntry.appendText(sb, this);
371
372 cachedText = sb.toString();
373 textVersion = property.getUpdateCount();
374 }
375 return cachedText;
376 }
377 }
378
379 @Override
380 public Collection<String> getTemplateKeys() {
381 Collection<String> result;
382 if (dataProvider != null) {
383 result = dataProvider.getTemplateKeys();
384 } else {
385 result = new ArrayList<String>();
386 }
387 result.add(MARKER_FORMATTED_OFFSET);
388 result.add(MARKER_OFFSET);
389 return result;
390 }
391
392 private String formatOffset () {
393 int wholeSeconds = (int)(offset + 0.5);
394 if (wholeSeconds < 60)
395 return Integer.toString(wholeSeconds);
396 else if (wholeSeconds < 3600)
397 return String.format("%d:%02d", wholeSeconds / 60, wholeSeconds % 60);
398 else
399 return String.format("%d:%02d:%02d", wholeSeconds / 3600, (wholeSeconds % 3600)/60, wholeSeconds % 60);
400 }
401
402 @Override
403 public Object getTemplateValue(String name, boolean special) {
404 if (MARKER_FORMATTED_OFFSET.equals(name))
405 return formatOffset();
406 else if (MARKER_OFFSET.equals(name))
407 return offset;
408 else if (dataProvider != null)
409 return dataProvider.getTemplateValue(name, special);
410 else
411 return null;
412 }
413
414 @Override
415 public boolean evaluateCondition(Match condition) {
416 throw new UnsupportedOperationException();
417 }
418}
Note: See TracBrowser for help on using the repository browser.