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 /** 174 * Plugins can add their Marker creation stuff at the bottom or top of this list 175 * (depending on whether they want to override default behaviour or just add new 176 * stuff). 177 */ 178 public static final List<MarkerProducers> markerProducers = new LinkedList<MarkerProducers>(); 179 180 // Add one Marker specifying the default behaviour. 181 static { 182 Marker.markerProducers.add(new MarkerProducers() { 183 @SuppressWarnings("unchecked") 184 public Marker createMarker(WayPoint wpt, File relativePath, MarkerLayer parentLayer, double time, double offset) { 185 String uri = null; 186 // cheapest way to check whether "link" object exists and is a non-empty 187 // collection of GpxLink objects... 188 Collection<GpxLink> links = (Collection<GpxLink>)wpt.attr.get(GpxData.META_LINKS); 189 if (links != null) { 190 for (GpxLink oneLink : links ) { 191 uri = oneLink.uri; 192 break; 193 } 194 } 195 196 URL url = null; 197 if (uri != null) { 198 try { 199 url = new URL(uri); 200 } catch (MalformedURLException e) { 201 // Try a relative file:// url, if the link is not in an URL-compatible form 202 if (relativePath != null) { 203 try { 204 url = new File(relativePath.getParentFile(), uri).toURI().toURL(); 205 } catch (MalformedURLException e1) { 206 Main.warn("Unable to convert uri {0} to URL: {1}", uri, e1.getMessage()); 207 } 208 } 209 } 210 } 211 212 213 if (url == null) { 214 String symbolName = wpt.getString("symbol"); 215 if (symbolName == null) { 216 symbolName = wpt.getString("sym"); 217 } 218 return new Marker(wpt.getCoor(), wpt, symbolName, parentLayer, time, offset); 219 } 220 else if (url.toString().endsWith(".wav")) { 221 return new AudioMarker(wpt.getCoor(), wpt, url, parentLayer, time, offset); 222 } else if (url.toString().endsWith(".png") || url.toString().endsWith(".jpg") || url.toString().endsWith(".jpeg") || url.toString().endsWith(".gif")) { 223 return new ImageMarker(wpt.getCoor(), url, parentLayer, time, offset); 224 } else { 225 return new WebMarker(wpt.getCoor(), url, parentLayer, time, offset); 226 } 227 } 228 }); 229 } 230 231 /** 232 * Returns an object of class Marker or one of its subclasses 233 * created from the parameters given. 234 * 235 * @param wpt waypoint data for marker 236 * @param relativePath An path to use for constructing relative URLs or 237 * <code>null</code> for no relative URLs 238 * @param offset double in seconds as the time offset of this marker from 239 * the GPX file from which it was derived (if any). 240 * @return a new Marker object 241 */ 242 public static Marker createMarker(WayPoint wpt, File relativePath, MarkerLayer parentLayer, double time, double offset) { 243 for (MarkerProducers maker : Marker.markerProducers) { 244 Marker marker = maker.createMarker(wpt, relativePath, parentLayer, time, offset); 245 if (marker != null) 246 return marker; 247 } 248 return null; 249 } 250 251 public static final String MARKER_OFFSET = "waypointOffset"; 252 public static final String MARKER_FORMATTED_OFFSET = "formattedWaypointOffset"; 253 254 public static final String LABEL_PATTERN_AUTO = "?{ '{name} - {desc}' | '{name}' | '{desc}' }"; 255 public static final String LABEL_PATTERN_NAME = "{name}"; 256 public static final String LABEL_PATTERN_DESC = "{desc}"; 257 258 259 private final TemplateEngineDataProvider dataProvider; 260 private final String text; 261 262 public final Icon symbol; 263 public final MarkerLayer parentLayer; 264 public double time; /* absolute time of marker since epoch */ 265 public double offset; /* time offset in seconds from the gpx point from which it was derived, 266 may be adjusted later to sync with other data, so not final */ 267 268 private String cachedText; 269 private int textVersion = -1; 270 private CachedLatLon coor; 271 272 public Marker(LatLon ll, TemplateEngineDataProvider dataProvider, String iconName, MarkerLayer parentLayer, double time, double offset) { 273 setCoor(ll); 274 275 this.offset = offset; 276 this.time = time; 277 // /* ICON(markers/) */"Bridge" 278 // /* ICON(markers/) */"Crossing" 279 this.symbol = iconName != null ? ImageProvider.getIfAvailable("markers",iconName) : null; 280 this.parentLayer = parentLayer; 281 282 this.dataProvider = dataProvider; 283 this.text = null; 284 } 285 286 public Marker(LatLon ll, String text, String iconName, MarkerLayer parentLayer, double time, double offset) { 287 setCoor(ll); 288 289 this.offset = offset; 290 this.time = time; 291 // /* ICON(markers/) */"Bridge" 292 // /* ICON(markers/) */"Crossing" 293 this.symbol = iconName != null ? ImageProvider.getIfAvailable("markers",iconName) : null; 294 this.parentLayer = parentLayer; 295 296 this.dataProvider = null; 297 this.text = text; 298 } 299 300 public final void setCoor(LatLon coor) { 301 if(this.coor == null) { 302 this.coor = new CachedLatLon(coor); 303 } else { 304 this.coor.setCoor(coor); 305 } 306 } 307 308 public final LatLon getCoor() { 309 return coor; 310 } 311 312 public final void setEastNorth(EastNorth eastNorth) { 313 coor.setEastNorth(eastNorth); 314 } 315 316 public final EastNorth getEastNorth() { 317 return coor.getEastNorth(); 318 } 319 320 321 /** 322 * Checks whether the marker display area contains the given point. 323 * Markers not interested in mouse clicks may always return false. 324 * 325 * @param p The point to check 326 * @return <code>true</code> if the marker "hotspot" contains the point. 327 */ 328 public boolean containsPoint(Point p) { 329 return false; 330 } 331 332 /** 333 * Called when the mouse is clicked in the marker's hotspot. Never 334 * called for markers which always return false from containsPoint. 335 * 336 * @param ev A dummy ActionEvent 337 */ 338 public void actionPerformed(ActionEvent ev) { 339 } 340 341 342 /** 343 * Paints the marker. 344 * @param g graphics context 345 * @param mv map view 346 * @param mousePressed true if the left mouse button is pressed 347 */ 348 public void paint(Graphics g, MapView mv, boolean mousePressed, boolean showTextOrIcon) { 349 Point screen = mv.getPoint(getEastNorth()); 350 if (symbol != null && showTextOrIcon) { 351 symbol.paintIcon(mv, g, screen.x-symbol.getIconWidth()/2, screen.y-symbol.getIconHeight()/2); 352 } else { 353 g.drawLine(screen.x-2, screen.y-2, screen.x+2, screen.y+2); 354 g.drawLine(screen.x+2, screen.y-2, screen.x-2, screen.y+2); 355 } 356 357 String labelText = getText(); 358 if ((labelText != null) && showTextOrIcon) { 359 g.drawString(labelText, screen.x+4, screen.y+2); 360 } 361 } 362 363 364 protected TemplateEntryProperty getTextTemplate() { 365 return TemplateEntryProperty.forMarker(parentLayer.getName()); 366 } 367 368 /** 369 * Returns the Text which should be displayed, depending on chosen preference 370 * @return Text of the label 371 */ 372 public String getText() { 373 if (text != null) 374 return text; 375 else { 376 TemplateEntryProperty property = getTextTemplate(); 377 if (property.getUpdateCount() != textVersion) { 378 TemplateEntry templateEntry = property.get(); 379 StringBuilder sb = new StringBuilder(); 380 templateEntry.appendText(sb, this); 381 382 cachedText = sb.toString(); 383 textVersion = property.getUpdateCount(); 384 } 385 return cachedText; 386 } 387 } 388 389 @Override 390 public Collection<String> getTemplateKeys() { 391 Collection<String> result; 392 if (dataProvider != null) { 393 result = dataProvider.getTemplateKeys(); 394 } else { 395 result = new ArrayList<String>(); 396 } 397 result.add(MARKER_FORMATTED_OFFSET); 398 result.add(MARKER_OFFSET); 399 return result; 400 } 401 402 private String formatOffset () { 403 int wholeSeconds = (int)(offset + 0.5); 404 if (wholeSeconds < 60) 405 return Integer.toString(wholeSeconds); 406 else if (wholeSeconds < 3600) 407 return String.format("%d:%02d", wholeSeconds / 60, wholeSeconds % 60); 408 else 409 return String.format("%d:%02d:%02d", wholeSeconds / 3600, (wholeSeconds % 3600)/60, wholeSeconds % 60); 410 } 411 412 @Override 413 public Object getTemplateValue(String name, boolean special) { 414 if (MARKER_FORMATTED_OFFSET.equals(name)) 415 return formatOffset(); 416 else if (MARKER_OFFSET.equals(name)) 417 return offset; 418 else if (dataProvider != null) 419 return dataProvider.getTemplateValue(name, special); 420 else 421 return null; 422 } 423 424 @Override 425 public boolean evaluateCondition(Match condition) { 426 throw new UnsupportedOperationException(); 427 } 428 }