001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer.markerlayer; 003 004import java.awt.AlphaComposite; 005import java.awt.Color; 006import java.awt.Graphics; 007import java.awt.Graphics2D; 008import java.awt.Point; 009import java.awt.event.ActionEvent; 010import java.awt.image.BufferedImage; 011import java.io.File; 012import java.util.ArrayList; 013import java.util.Collection; 014import java.util.HashMap; 015import java.util.LinkedList; 016import java.util.List; 017import java.util.Map; 018 019import javax.swing.ImageIcon; 020 021import org.openstreetmap.josm.data.coor.CachedLatLon; 022import org.openstreetmap.josm.data.coor.EastNorth; 023import org.openstreetmap.josm.data.coor.ILatLon; 024import org.openstreetmap.josm.data.coor.LatLon; 025import org.openstreetmap.josm.data.gpx.GpxConstants; 026import org.openstreetmap.josm.data.gpx.WayPoint; 027import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match; 028import org.openstreetmap.josm.data.preferences.CachedProperty; 029import org.openstreetmap.josm.gui.MapView; 030import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent; 031import org.openstreetmap.josm.tools.ImageProvider; 032import org.openstreetmap.josm.tools.Logging; 033import org.openstreetmap.josm.tools.template_engine.ParseError; 034import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider; 035import org.openstreetmap.josm.tools.template_engine.TemplateEntry; 036import org.openstreetmap.josm.tools.template_engine.TemplateParser; 037 038/** 039 * Basic marker class. Requires a position, and supports 040 * a custom icon and a name. 041 * 042 * This class is also used to create appropriate Marker-type objects 043 * when waypoints are imported. 044 * 045 * It hosts a public list object, named makers, containing implementations of 046 * the MarkerMaker interface. Whenever a Marker needs to be created, each 047 * object in makers is called with the waypoint parameters (Lat/Lon and tag 048 * data), and the first one to return a Marker object wins. 049 * 050 * By default, one the list contains one default "Maker" implementation that 051 * will create AudioMarkers for supported audio files, ImageMarkers for supported image 052 * files, and WebMarkers for everything else. (The creation of a WebMarker will 053 * fail if there's no valid URL in the <link> tag, so it might still make sense 054 * to add Makers for such waypoints at the end of the list.) 055 * 056 * The default implementation only looks at the value of the <link> tag inside 057 * the <wpt> tag of the GPX file. 058 * 059 * <h2>HowTo implement a new Marker</h2> 060 * <ul> 061 * <li> Subclass Marker or ButtonMarker and override <code>containsPoint</code> 062 * if you like to respond to user clicks</li> 063 * <li> Override paint, if you want a custom marker look (not "a label and a symbol")</li> 064 * <li> Implement MarkerCreator to return a new instance of your marker class</li> 065 * <li> In you plugin constructor, add an instance of your MarkerCreator 066 * implementation either on top or bottom of Marker.markerProducers. 067 * Add at top, if your marker should overwrite an current marker or at bottom 068 * if you only add a new marker style.</li> 069 * </ul> 070 * 071 * @author Frederik Ramm 072 */ 073public class Marker implements TemplateEngineDataProvider, ILatLon { 074 075 public static final class TemplateEntryProperty extends CachedProperty<TemplateEntry> { 076 // This class is a bit complicated because it supports both global and per layer settings. I've added per layer settings because 077 // GPXSettingsPanel had possibility to set waypoint label but then I've realized that markers use different layer then gpx data 078 // so per layer settings is useless. Anyway it's possible to specify marker layer pattern in Einstein preferences and maybe somebody 079 // will make gui for it so I'm keeping it here 080 081 private static final Map<String, TemplateEntryProperty> CACHE = new HashMap<>(); 082 083 public static TemplateEntryProperty forMarker(String layerName) { 084 String key = "draw.rawgps.layer.wpt.pattern"; 085 if (layerName != null) { 086 key += '.' + layerName; 087 } 088 TemplateEntryProperty result = CACHE.get(key); 089 if (result == null) { 090 String defaultValue = layerName == null ? LABEL_PATTERN_AUTO : ""; 091 TemplateEntryProperty parent = layerName == null ? null : forMarker(null); 092 result = new TemplateEntryProperty(key, defaultValue, parent); 093 CACHE.put(key, result); 094 } 095 return result; 096 } 097 098 public static TemplateEntryProperty forAudioMarker(String layerName) { 099 String key = "draw.rawgps.layer.audiowpt.pattern"; 100 if (layerName != null) { 101 key += '.' + layerName; 102 } 103 TemplateEntryProperty result = CACHE.get(key); 104 if (result == null) { 105 String defaultValue = layerName == null ? "?{ '{name}' | '{desc}' | '{" + Marker.MARKER_FORMATTED_OFFSET + "}' }" : ""; 106 TemplateEntryProperty parent = layerName == null ? null : forAudioMarker(null); 107 result = new TemplateEntryProperty(key, defaultValue, parent); 108 CACHE.put(key, result); 109 } 110 return result; 111 } 112 113 private final TemplateEntryProperty parent; 114 115 private TemplateEntryProperty(String key, String defaultValue, TemplateEntryProperty parent) { 116 super(key, defaultValue); 117 this.parent = parent; 118 updateValue(); // Needs to be called because parent wasn't know in super constructor 119 } 120 121 @Override 122 protected TemplateEntry fromString(String s) { 123 try { 124 return new TemplateParser(s).parse(); 125 } catch (ParseError e) { 126 Logging.debug(e); 127 Logging.warn("Unable to parse template engine pattern ''{0}'' for property {1}. Using default (''{2}'') instead", 128 s, getKey(), super.getDefaultValueAsString()); 129 return getDefaultValue(); 130 } 131 } 132 133 @Override 134 public String getDefaultValueAsString() { 135 if (parent == null) 136 return super.getDefaultValueAsString(); 137 else 138 return parent.getAsString(); 139 } 140 141 @Override 142 public void preferenceChanged(PreferenceChangeEvent e) { 143 if (e.getKey().equals(key) || (parent != null && e.getKey().equals(parent.getKey()))) { 144 updateValue(); 145 } 146 } 147 } 148 149 /** 150 * Plugins can add their Marker creation stuff at the bottom or top of this list 151 * (depending on whether they want to override default behaviour or just add new stuff). 152 */ 153 private static final List<MarkerProducers> markerProducers = new LinkedList<>(); 154 155 // Add one Marker specifying the default behaviour. 156 static { 157 Marker.markerProducers.add(new DefaultMarkerProducers()); 158 } 159 160 /** 161 * Add a new marker producers at the end of the JOSM list. 162 * @param mp a new marker producers 163 * @since 11850 164 */ 165 public static void appendMarkerProducer(MarkerProducers mp) { 166 markerProducers.add(mp); 167 } 168 169 /** 170 * Add a new marker producers at the beginning of the JOSM list. 171 * @param mp a new marker producers 172 * @since 11850 173 */ 174 public static void prependMarkerProducer(MarkerProducers mp) { 175 markerProducers.add(0, mp); 176 } 177 178 /** 179 * Returns an object of class Marker or one of its subclasses 180 * created from the parameters given. 181 * 182 * @param wpt waypoint data for marker 183 * @param relativePath An path to use for constructing relative URLs or 184 * <code>null</code> for no relative URLs 185 * @param parentLayer the <code>MarkerLayer</code> that will contain the created <code>Marker</code> 186 * @param time time of the marker in seconds since epoch 187 * @param offset double in seconds as the time offset of this marker from 188 * the GPX file from which it was derived (if any). 189 * @return a new Marker object 190 */ 191 public static Collection<Marker> createMarkers(WayPoint wpt, File relativePath, MarkerLayer parentLayer, double time, double offset) { 192 for (MarkerProducers maker : Marker.markerProducers) { 193 final Collection<Marker> markers = maker.createMarkers(wpt, relativePath, parentLayer, time, offset); 194 if (markers != null) 195 return markers; 196 } 197 return null; 198 } 199 200 public static final String MARKER_OFFSET = "waypointOffset"; 201 public static final String MARKER_FORMATTED_OFFSET = "formattedWaypointOffset"; 202 203 public static final String LABEL_PATTERN_AUTO = "?{ '{name} ({desc})' | '{name} ({cmt})' | '{name}' | '{desc}' | '{cmt}' }"; 204 public static final String LABEL_PATTERN_NAME = "{name}"; 205 public static final String LABEL_PATTERN_DESC = "{desc}"; 206 207 private final TemplateEngineDataProvider dataProvider; 208 private final String text; 209 210 protected final ImageIcon symbol; 211 private BufferedImage redSymbol; 212 public final MarkerLayer parentLayer; 213 /** Absolute time of marker in seconds since epoch */ 214 public double time; 215 /** 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 */ 216 public double offset; 217 218 private String cachedText; 219 private int textVersion = -1; 220 private CachedLatLon coor; 221 222 private boolean erroneous; 223 224 public Marker(LatLon ll, TemplateEngineDataProvider dataProvider, String iconName, MarkerLayer parentLayer, 225 double time, double offset) { 226 this(ll, dataProvider, null, iconName, parentLayer, time, offset); 227 } 228 229 public Marker(LatLon ll, String text, String iconName, MarkerLayer parentLayer, double time, double offset) { 230 this(ll, null, text, iconName, parentLayer, time, offset); 231 } 232 233 private Marker(LatLon ll, TemplateEngineDataProvider dataProvider, String text, String iconName, MarkerLayer parentLayer, 234 double time, double offset) { 235 setCoor(ll); 236 237 this.offset = offset; 238 this.time = time; 239 /* tell icon checking that we expect these names to exist */ 240 // /* ICON(markers/) */"Bridge" 241 // /* ICON(markers/) */"Crossing" 242 this.symbol = iconName != null ? ImageProvider.getIfAvailable("markers", iconName) : null; 243 this.parentLayer = parentLayer; 244 245 this.dataProvider = dataProvider; 246 this.text = text; 247 } 248 249 /** 250 * Convert Marker to WayPoint so it can be exported to a GPX file. 251 * 252 * Override in subclasses to add all necessary attributes. 253 * 254 * @return the corresponding WayPoint with all relevant attributes 255 */ 256 public WayPoint convertToWayPoint() { 257 WayPoint wpt = new WayPoint(getCoor()); 258 wpt.setTimeInMillis((long) (time * 1000)); 259 if (text != null) { 260 wpt.addExtension("text", text); 261 } else if (dataProvider != null) { 262 for (String key : dataProvider.getTemplateKeys()) { 263 Object value = dataProvider.getTemplateValue(key, false); 264 if (value != null && GpxConstants.WPT_KEYS.contains(key)) { 265 wpt.put(key, value); 266 } 267 } 268 } 269 return wpt; 270 } 271 272 /** 273 * Sets the marker's coordinates. 274 * @param coor The marker's coordinates (lat/lon) 275 */ 276 public final void setCoor(LatLon coor) { 277 this.coor = new CachedLatLon(coor); 278 } 279 280 /** 281 * Returns the marker's coordinates. 282 * @return The marker's coordinates (lat/lon) 283 */ 284 public final LatLon getCoor() { 285 return coor; 286 } 287 288 /** 289 * Sets the marker's projected coordinates. 290 * @param eastNorth The marker's projected coordinates (easting/northing) 291 */ 292 public final void setEastNorth(EastNorth eastNorth) { 293 this.coor = new CachedLatLon(eastNorth); 294 } 295 296 /** 297 * @since 12725 298 */ 299 @Override 300 public double lon() { 301 return coor == null ? Double.NaN : coor.lon(); 302 } 303 304 /** 305 * @since 12725 306 */ 307 @Override 308 public double lat() { 309 return coor == null ? Double.NaN : coor.lat(); 310 } 311 312 /** 313 * Checks whether the marker display area contains the given point. 314 * Markers not interested in mouse clicks may always return false. 315 * 316 * @param p The point to check 317 * @return <code>true</code> if the marker "hotspot" contains the point. 318 */ 319 public boolean containsPoint(Point p) { 320 return false; 321 } 322 323 /** 324 * Called when the mouse is clicked in the marker's hotspot. Never 325 * called for markers which always return false from containsPoint. 326 * 327 * @param ev A dummy ActionEvent 328 */ 329 public void actionPerformed(ActionEvent ev) { 330 // Do nothing 331 } 332 333 /** 334 * Paints the marker. 335 * @param g graphics context 336 * @param mv map view 337 * @param mousePressed true if the left mouse button is pressed 338 * @param showTextOrIcon true if text and icon shall be drawn 339 */ 340 public void paint(Graphics g, MapView mv, boolean mousePressed, boolean showTextOrIcon) { 341 Point screen = mv.getPoint(this); 342 if (symbol != null && showTextOrIcon) { 343 paintIcon(mv, g, screen.x-symbol.getIconWidth()/2, screen.y-symbol.getIconHeight()/2); 344 } else { 345 g.drawLine(screen.x-2, screen.y-2, screen.x+2, screen.y+2); 346 g.drawLine(screen.x+2, screen.y-2, screen.x-2, screen.y+2); 347 } 348 349 String labelText = getText(); 350 if ((labelText != null) && showTextOrIcon) { 351 g.drawString(labelText, screen.x+4, screen.y+2); 352 } 353 } 354 355 protected void paintIcon(MapView mv, Graphics g, int x, int y) { 356 if (!erroneous) { 357 symbol.paintIcon(mv, g, x, y); 358 } else { 359 if (redSymbol == null) { 360 int width = symbol.getIconWidth(); 361 int height = symbol.getIconHeight(); 362 363 redSymbol = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 364 Graphics2D gbi = redSymbol.createGraphics(); 365 gbi.drawImage(symbol.getImage(), 0, 0, null); 366 gbi.setColor(Color.RED); 367 gbi.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.666f)); 368 gbi.fillRect(0, 0, width, height); 369 gbi.dispose(); 370 } 371 g.drawImage(redSymbol, x, y, mv); 372 } 373 } 374 375 protected TemplateEntryProperty getTextTemplate() { 376 return TemplateEntryProperty.forMarker(parentLayer.getName()); 377 } 378 379 /** 380 * Returns the Text which should be displayed, depending on chosen preference 381 * @return Text of the label 382 */ 383 public String getText() { 384 if (text != null) 385 return text; 386 else { 387 TemplateEntryProperty property = getTextTemplate(); 388 if (property.getUpdateCount() != textVersion) { 389 TemplateEntry templateEntry = property.get(); 390 StringBuilder sb = new StringBuilder(); 391 templateEntry.appendText(sb, this); 392 393 cachedText = sb.toString(); 394 textVersion = property.getUpdateCount(); 395 } 396 return cachedText; 397 } 398 } 399 400 @Override 401 public Collection<String> getTemplateKeys() { 402 Collection<String> result; 403 if (dataProvider != null) { 404 result = dataProvider.getTemplateKeys(); 405 } else { 406 result = new ArrayList<>(); 407 } 408 result.add(MARKER_FORMATTED_OFFSET); 409 result.add(MARKER_OFFSET); 410 return result; 411 } 412 413 private String formatOffset() { 414 int wholeSeconds = (int) (offset + 0.5); 415 if (wholeSeconds < 60) 416 return Integer.toString(wholeSeconds); 417 else if (wholeSeconds < 3600) 418 return String.format("%d:%02d", wholeSeconds / 60, wholeSeconds % 60); 419 else 420 return String.format("%d:%02d:%02d", wholeSeconds / 3600, (wholeSeconds % 3600)/60, wholeSeconds % 60); 421 } 422 423 @Override 424 public Object getTemplateValue(String name, boolean special) { 425 if (MARKER_FORMATTED_OFFSET.equals(name)) 426 return formatOffset(); 427 else if (MARKER_OFFSET.equals(name)) 428 return offset; 429 else if (dataProvider != null) 430 return dataProvider.getTemplateValue(name, special); 431 else 432 return null; 433 } 434 435 @Override 436 public boolean evaluateCondition(Match condition) { 437 throw new UnsupportedOperationException(); 438 } 439 440 /** 441 * Determines if this marker is erroneous. 442 * @return {@code true} if this markers has any kind of error, {@code false} otherwise 443 * @since 6299 444 */ 445 public final boolean isErroneous() { 446 return erroneous; 447 } 448 449 /** 450 * Sets this marker erroneous or not. 451 * @param erroneous {@code true} if this markers has any kind of error, {@code false} otherwise 452 * @since 6299 453 */ 454 public final void setErroneous(boolean erroneous) { 455 this.erroneous = erroneous; 456 if (!erroneous) { 457 redSymbol = null; 458 } 459 } 460}