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.LinkedList; 015import java.util.List; 016import java.util.Map; 017import java.util.concurrent.ConcurrentHashMap; 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 ConcurrentHashMap<>(); 082 083 public static TemplateEntryProperty forMarker(String layerName) { 084 String key = "draw.rawgps.layer.wpt.pattern"; 085 if (layerName != null) { 086 return CACHE.computeIfAbsent(key + '.' + layerName, k -> new TemplateEntryProperty(k, "", forMarker(null))); 087 } else { 088 return CACHE.computeIfAbsent(key, k -> new TemplateEntryProperty(k, LABEL_PATTERN_AUTO, null)); 089 } 090 } 091 092 public static TemplateEntryProperty forAudioMarker(String layerName) { 093 String key = "draw.rawgps.layer.audiowpt.pattern"; 094 if (layerName != null) { 095 return CACHE.computeIfAbsent(key + '.' + layerName, k -> 096 new TemplateEntryProperty(k, "", forAudioMarker(null))); 097 } else { 098 return CACHE.computeIfAbsent(key, k -> 099 new TemplateEntryProperty(k, "?{ '{name}' | '{desc}' | '{" + MARKER_FORMATTED_OFFSET + "}' }", null)); 100 } 101 } 102 103 private final TemplateEntryProperty parent; 104 105 private TemplateEntryProperty(String key, String defaultValue, TemplateEntryProperty parent) { 106 super(key, defaultValue); 107 this.parent = parent; 108 updateValue(); // Needs to be called because parent wasn't know in super constructor 109 } 110 111 @Override 112 protected TemplateEntry fromString(String s) { 113 try { 114 return new TemplateParser(s).parse(); 115 } catch (ParseError e) { 116 Logging.debug(e); 117 Logging.warn("Unable to parse template engine pattern ''{0}'' for property {1}. Using default (''{2}'') instead", 118 s, getKey(), super.getDefaultValueAsString()); 119 return getDefaultValue(); 120 } 121 } 122 123 @Override 124 public String getDefaultValueAsString() { 125 if (parent == null) 126 return super.getDefaultValueAsString(); 127 else 128 return parent.getAsString(); 129 } 130 131 @Override 132 public void preferenceChanged(PreferenceChangeEvent e) { 133 if (e.getKey().equals(key) || (parent != null && e.getKey().equals(parent.getKey()))) { 134 updateValue(); 135 } 136 } 137 } 138 139 /** 140 * Plugins can add their Marker creation stuff at the bottom or top of this list 141 * (depending on whether they want to override default behaviour or just add new stuff). 142 */ 143 private static final List<MarkerProducers> markerProducers = new LinkedList<>(); 144 145 // Add one Marker specifying the default behaviour. 146 static { 147 Marker.markerProducers.add(new DefaultMarkerProducers()); 148 } 149 150 /** 151 * Add a new marker producers at the end of the JOSM list. 152 * @param mp a new marker producers 153 * @since 11850 154 */ 155 public static void appendMarkerProducer(MarkerProducers mp) { 156 markerProducers.add(mp); 157 } 158 159 /** 160 * Add a new marker producers at the beginning of the JOSM list. 161 * @param mp a new marker producers 162 * @since 11850 163 */ 164 public static void prependMarkerProducer(MarkerProducers mp) { 165 markerProducers.add(0, mp); 166 } 167 168 /** 169 * Returns an object of class Marker or one of its subclasses 170 * created from the parameters given. 171 * 172 * @param wpt waypoint data for marker 173 * @param relativePath An path to use for constructing relative URLs or 174 * <code>null</code> for no relative URLs 175 * @param parentLayer the <code>MarkerLayer</code> that will contain the created <code>Marker</code> 176 * @param time time of the marker in seconds since epoch 177 * @param offset double in seconds as the time offset of this marker from 178 * the GPX file from which it was derived (if any). 179 * @return a new Marker object 180 */ 181 public static Collection<Marker> createMarkers(WayPoint wpt, File relativePath, MarkerLayer parentLayer, double time, double offset) { 182 for (MarkerProducers maker : Marker.markerProducers) { 183 final Collection<Marker> markers = maker.createMarkers(wpt, relativePath, parentLayer, time, offset); 184 if (markers != null) 185 return markers; 186 } 187 return null; 188 } 189 190 public static final String MARKER_OFFSET = "waypointOffset"; 191 public static final String MARKER_FORMATTED_OFFSET = "formattedWaypointOffset"; 192 193 public static final String LABEL_PATTERN_AUTO = "?{ '{name} ({desc})' | '{name} ({cmt})' | '{name}' | '{desc}' | '{cmt}' }"; 194 public static final String LABEL_PATTERN_NAME = "{name}"; 195 public static final String LABEL_PATTERN_DESC = "{desc}"; 196 197 private final TemplateEngineDataProvider dataProvider; 198 private final String text; 199 200 protected final ImageIcon symbol; 201 private BufferedImage redSymbol; 202 public final MarkerLayer parentLayer; 203 /** Absolute time of marker in seconds since epoch */ 204 public double time; 205 /** 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 */ 206 public double offset; 207 208 private String cachedText; 209 private int textVersion = -1; 210 private CachedLatLon coor; 211 212 private boolean erroneous; 213 214 public Marker(LatLon ll, TemplateEngineDataProvider dataProvider, String iconName, MarkerLayer parentLayer, 215 double time, double offset) { 216 this(ll, dataProvider, null, iconName, parentLayer, time, offset); 217 } 218 219 public Marker(LatLon ll, String text, String iconName, MarkerLayer parentLayer, double time, double offset) { 220 this(ll, null, text, iconName, parentLayer, time, offset); 221 } 222 223 private Marker(LatLon ll, TemplateEngineDataProvider dataProvider, String text, String iconName, MarkerLayer parentLayer, 224 double time, double offset) { 225 setCoor(ll); 226 227 this.offset = offset; 228 this.time = time; 229 /* tell icon checking that we expect these names to exist */ 230 // /* ICON(markers/) */"Bridge" 231 // /* ICON(markers/) */"Crossing" 232 this.symbol = iconName != null ? ImageProvider.getIfAvailable("markers", iconName) : null; 233 this.parentLayer = parentLayer; 234 235 this.dataProvider = dataProvider; 236 this.text = text; 237 } 238 239 /** 240 * Convert Marker to WayPoint so it can be exported to a GPX file. 241 * 242 * Override in subclasses to add all necessary attributes. 243 * 244 * @return the corresponding WayPoint with all relevant attributes 245 */ 246 public WayPoint convertToWayPoint() { 247 WayPoint wpt = new WayPoint(getCoor()); 248 wpt.setTimeInMillis((long) (time * 1000)); 249 if (text != null) { 250 wpt.addExtension("text", text); 251 } else if (dataProvider != null) { 252 for (String key : dataProvider.getTemplateKeys()) { 253 Object value = dataProvider.getTemplateValue(key, false); 254 if (value != null && GpxConstants.WPT_KEYS.contains(key)) { 255 wpt.put(key, value); 256 } 257 } 258 } 259 return wpt; 260 } 261 262 /** 263 * Sets the marker's coordinates. 264 * @param coor The marker's coordinates (lat/lon) 265 */ 266 public final void setCoor(LatLon coor) { 267 this.coor = new CachedLatLon(coor); 268 } 269 270 /** 271 * Returns the marker's coordinates. 272 * @return The marker's coordinates (lat/lon) 273 */ 274 public final LatLon getCoor() { 275 return coor; 276 } 277 278 /** 279 * Sets the marker's projected coordinates. 280 * @param eastNorth The marker's projected coordinates (easting/northing) 281 */ 282 public final void setEastNorth(EastNorth eastNorth) { 283 this.coor = new CachedLatLon(eastNorth); 284 } 285 286 /** 287 * @since 12725 288 */ 289 @Override 290 public double lon() { 291 return coor == null ? Double.NaN : coor.lon(); 292 } 293 294 /** 295 * @since 12725 296 */ 297 @Override 298 public double lat() { 299 return coor == null ? Double.NaN : coor.lat(); 300 } 301 302 /** 303 * Checks whether the marker display area contains the given point. 304 * Markers not interested in mouse clicks may always return false. 305 * 306 * @param p The point to check 307 * @return <code>true</code> if the marker "hotspot" contains the point. 308 */ 309 public boolean containsPoint(Point p) { 310 return false; 311 } 312 313 /** 314 * Called when the mouse is clicked in the marker's hotspot. Never 315 * called for markers which always return false from containsPoint. 316 * 317 * @param ev A dummy ActionEvent 318 */ 319 public void actionPerformed(ActionEvent ev) { 320 // Do nothing 321 } 322 323 /** 324 * Paints the marker. 325 * @param g graphics context 326 * @param mv map view 327 * @param mousePressed true if the left mouse button is pressed 328 * @param showTextOrIcon true if text and icon shall be drawn 329 */ 330 public void paint(Graphics g, MapView mv, boolean mousePressed, boolean showTextOrIcon) { 331 Point screen = mv.getPoint(this); 332 if (symbol != null && showTextOrIcon) { 333 paintIcon(mv, g, screen.x-symbol.getIconWidth()/2, screen.y-symbol.getIconHeight()/2); 334 } else { 335 g.drawLine(screen.x-2, screen.y-2, screen.x+2, screen.y+2); 336 g.drawLine(screen.x+2, screen.y-2, screen.x-2, screen.y+2); 337 } 338 339 String labelText = getText(); 340 if ((labelText != null) && showTextOrIcon) { 341 g.drawString(labelText, screen.x+4, screen.y+2); 342 } 343 } 344 345 protected void paintIcon(MapView mv, Graphics g, int x, int y) { 346 if (!erroneous) { 347 symbol.paintIcon(mv, g, x, y); 348 } else { 349 if (redSymbol == null) { 350 int width = symbol.getIconWidth(); 351 int height = symbol.getIconHeight(); 352 353 redSymbol = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 354 Graphics2D gbi = redSymbol.createGraphics(); 355 gbi.drawImage(symbol.getImage(), 0, 0, null); 356 gbi.setColor(Color.RED); 357 gbi.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.666f)); 358 gbi.fillRect(0, 0, width, height); 359 gbi.dispose(); 360 } 361 g.drawImage(redSymbol, x, y, mv); 362 } 363 } 364 365 protected TemplateEntryProperty getTextTemplate() { 366 return TemplateEntryProperty.forMarker(parentLayer.getName()); 367 } 368 369 /** 370 * Returns the Text which should be displayed, depending on chosen preference 371 * @return Text of the label 372 */ 373 public String getText() { 374 if (text != null) 375 return text; 376 else { 377 TemplateEntryProperty property = getTextTemplate(); 378 if (property.getUpdateCount() != textVersion) { 379 TemplateEntry templateEntry = property.get(); 380 StringBuilder sb = new StringBuilder(); 381 templateEntry.appendText(sb, this); 382 383 cachedText = sb.toString(); 384 textVersion = property.getUpdateCount(); 385 } 386 return cachedText; 387 } 388 } 389 390 @Override 391 public Collection<String> getTemplateKeys() { 392 Collection<String> result; 393 if (dataProvider != null) { 394 result = dataProvider.getTemplateKeys(); 395 } else { 396 result = new ArrayList<>(); 397 } 398 result.add(MARKER_FORMATTED_OFFSET); 399 result.add(MARKER_OFFSET); 400 return result; 401 } 402 403 private String formatOffset() { 404 int wholeSeconds = (int) (offset + 0.5); 405 if (wholeSeconds < 60) 406 return Integer.toString(wholeSeconds); 407 else if (wholeSeconds < 3600) 408 return String.format("%d:%02d", wholeSeconds / 60, wholeSeconds % 60); 409 else 410 return String.format("%d:%02d:%02d", wholeSeconds / 3600, (wholeSeconds % 3600)/60, wholeSeconds % 60); 411 } 412 413 @Override 414 public Object getTemplateValue(String name, boolean special) { 415 if (MARKER_FORMATTED_OFFSET.equals(name)) 416 return formatOffset(); 417 else if (MARKER_OFFSET.equals(name)) 418 return offset; 419 else if (dataProvider != null) 420 return dataProvider.getTemplateValue(name, special); 421 else 422 return null; 423 } 424 425 @Override 426 public boolean evaluateCondition(Match condition) { 427 throw new UnsupportedOperationException(); 428 } 429 430 /** 431 * Determines if this marker is erroneous. 432 * @return {@code true} if this markers has any kind of error, {@code false} otherwise 433 * @since 6299 434 */ 435 public final boolean isErroneous() { 436 return erroneous; 437 } 438 439 /** 440 * Sets this marker erroneous or not. 441 * @param erroneous {@code true} if this markers has any kind of error, {@code false} otherwise 442 * @since 6299 443 */ 444 public final void setErroneous(boolean erroneous) { 445 this.erroneous = erroneous; 446 if (!erroneous) { 447 redSymbol = null; 448 } 449 } 450}