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 }