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