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