001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer.markerlayer; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Graphics; 007import java.awt.Point; 008import java.awt.Rectangle; 009import java.awt.event.ActionEvent; 010import java.awt.event.ActionListener; 011import java.awt.event.MouseAdapter; 012import java.awt.event.MouseEvent; 013 014import javax.swing.JOptionPane; 015import javax.swing.Timer; 016 017import org.openstreetmap.josm.Main; 018import org.openstreetmap.josm.actions.mapmode.MapMode; 019import org.openstreetmap.josm.actions.mapmode.PlayHeadDragMode; 020import org.openstreetmap.josm.data.coor.EastNorth; 021import org.openstreetmap.josm.data.coor.LatLon; 022import org.openstreetmap.josm.data.gpx.GpxTrack; 023import org.openstreetmap.josm.data.gpx.GpxTrackSegment; 024import org.openstreetmap.josm.data.gpx.WayPoint; 025import org.openstreetmap.josm.gui.MapView; 026import org.openstreetmap.josm.gui.layer.GpxLayer; 027import org.openstreetmap.josm.tools.AudioPlayer; 028 029/** 030 * Singleton marker class to track position of audio. 031 * 032 * @author David Earl <david@frankieandshadow.com> 033 * @since 572 034 */ 035public final class PlayHeadMarker extends Marker { 036 037 private Timer timer; 038 private double animationInterval; // seconds 039 private static volatile PlayHeadMarker playHead; 040 private MapMode oldMode; 041 private LatLon oldCoor; 042 private final boolean enabled; 043 private boolean wasPlaying; 044 private int dropTolerance; /* pixels */ 045 private boolean jumpToMarker; 046 047 /** 048 * Returns the unique instance of {@code PlayHeadMarker}. 049 * @return The unique instance of {@code PlayHeadMarker}. 050 */ 051 public static PlayHeadMarker create() { 052 if (playHead == null) { 053 playHead = new PlayHeadMarker(); 054 } 055 return playHead; 056 } 057 058 private PlayHeadMarker() { 059 super(LatLon.ZERO, "", 060 Main.pref.get("marker.audiotracericon", "audio-tracer"), 061 null, -1.0, 0.0); 062 enabled = Main.pref.getBoolean("marker.traceaudio", true); 063 if (!enabled) return; 064 dropTolerance = Main.pref.getInteger("marker.playHeadDropTolerance", 50); 065 if (Main.isDisplayingMapView()) { 066 Main.map.mapView.addMouseListener(new MouseAdapter() { 067 @Override public void mousePressed(MouseEvent ev) { 068 Point p = ev.getPoint(); 069 if (ev.getButton() != MouseEvent.BUTTON1 || p == null) 070 return; 071 if (playHead.containsPoint(p)) { 072 /* when we get a click on the marker, we need to switch mode to avoid 073 * getting confused with other drag operations (like select) */ 074 oldMode = Main.map.mapMode; 075 oldCoor = getCoor(); 076 PlayHeadDragMode playHeadDragMode = new PlayHeadDragMode(playHead); 077 Main.map.selectMapMode(playHeadDragMode); 078 playHeadDragMode.mousePressed(ev); 079 } 080 } 081 }); 082 } 083 } 084 085 @Override 086 public boolean containsPoint(Point p) { 087 Point screen = Main.map.mapView.getPoint(getEastNorth()); 088 Rectangle r = new Rectangle(screen.x, screen.y, symbol.getIconWidth(), 089 symbol.getIconHeight()); 090 return r.contains(p); 091 } 092 093 /** 094 * called back from drag mode to say when we started dragging for real 095 * (at least a short distance) 096 */ 097 public void startDrag() { 098 if (timer != null) { 099 timer.stop(); 100 } 101 wasPlaying = AudioPlayer.playing(); 102 if (wasPlaying) { 103 try { 104 AudioPlayer.pause(); 105 } catch (Exception ex) { 106 AudioPlayer.audioMalfunction(ex); 107 } 108 } 109 } 110 111 /** 112 * reinstate the old map mode after switching temporarily to do a play head drag 113 * @param reset whether to reset state (pause audio and restore old coordinates) 114 */ 115 private void endDrag(boolean reset) { 116 if (!wasPlaying || reset) { 117 try { 118 AudioPlayer.pause(); 119 } catch (Exception ex) { 120 AudioPlayer.audioMalfunction(ex); 121 } 122 } 123 if (reset) { 124 setCoor(oldCoor); 125 } 126 Main.map.selectMapMode(oldMode); 127 Main.map.mapView.repaint(); 128 timer.start(); 129 } 130 131 /** 132 * apply the new position resulting from a drag in progress 133 * @param en the new position in map terms 134 */ 135 public void drag(EastNorth en) { 136 setEastNorth(en); 137 Main.map.mapView.repaint(); 138 } 139 140 /** 141 * reposition the play head at the point on the track nearest position given, 142 * providing we are within reasonable distance from the track; otherwise reset to the 143 * original position. 144 * @param en the position to start looking from 145 */ 146 public void reposition(EastNorth en) { 147 WayPoint cw = null; 148 AudioMarker recent = AudioMarker.recentlyPlayedMarker(); 149 if (recent != null && recent.parentLayer != null && recent.parentLayer.fromLayer != null) { 150 /* work out EastNorth equivalent of 50 (default) pixels tolerance */ 151 Point p = Main.map.mapView.getPoint(en); 152 EastNorth enPlus25px = Main.map.mapView.getEastNorth(p.x+dropTolerance, p.y); 153 cw = recent.parentLayer.fromLayer.data.nearestPointOnTrack(en, enPlus25px.east() - en.east()); 154 } 155 156 AudioMarker ca = null; 157 /* Find the prior audio marker (there should always be one in the 158 * layer, even if it is only one at the start of the track) to 159 * offset the audio from */ 160 if (cw != null && recent != null && recent.parentLayer != null) { 161 for (Marker m : recent.parentLayer.data) { 162 if (m instanceof AudioMarker) { 163 AudioMarker a = (AudioMarker) m; 164 if (a.time > cw.time) { 165 break; 166 } 167 ca = a; 168 } 169 } 170 } 171 172 if (ca == null) { 173 /* Not close enough to track, or no audio marker found for some other reason */ 174 JOptionPane.showMessageDialog( 175 Main.parent, 176 tr("You need to drag the play head near to the GPX track " + 177 "whose associated sound track you were playing (after the first marker)."), 178 tr("Warning"), 179 JOptionPane.WARNING_MESSAGE 180 ); 181 endDrag(true); 182 } else { 183 if (cw != null) { 184 setCoor(cw.getCoor()); 185 ca.play(cw.time - ca.time); 186 } 187 endDrag(false); 188 } 189 } 190 191 /** 192 * Synchronize the audio at the position where the play head was paused before 193 * dragging with the position on the track where it was dropped. 194 * If this is quite near an audio marker, we use that 195 * marker as the sync. location, otherwise we create a new marker at the 196 * trackpoint nearest the end point of the drag point to apply the 197 * sync to. 198 * @param en : the EastNorth end point of the drag 199 */ 200 public void synchronize(EastNorth en) { 201 AudioMarker recent = AudioMarker.recentlyPlayedMarker(); 202 if (recent == null) 203 return; 204 /* First, see if we dropped onto an existing audio marker in the layer being played */ 205 Point startPoint = Main.map.mapView.getPoint(en); 206 AudioMarker ca = null; 207 if (recent.parentLayer != null) { 208 double closestAudioMarkerDistanceSquared = 1.0E100; 209 for (Marker m : recent.parentLayer.data) { 210 if (m instanceof AudioMarker) { 211 double distanceSquared = m.getEastNorth().distanceSq(en); 212 if (distanceSquared < closestAudioMarkerDistanceSquared) { 213 ca = (AudioMarker) m; 214 closestAudioMarkerDistanceSquared = distanceSquared; 215 } 216 } 217 } 218 } 219 220 /* We found the closest marker: did we actually hit it? */ 221 if (ca != null && !ca.containsPoint(startPoint)) { 222 ca = null; 223 } 224 225 /* If we didn't hit an audio marker, we need to create one at the nearest point on the track */ 226 if (ca == null) { 227 /* work out EastNorth equivalent of 50 (default) pixels tolerance */ 228 Point p = Main.map.mapView.getPoint(en); 229 EastNorth enPlus25px = Main.map.mapView.getEastNorth(p.x+dropTolerance, p.y); 230 WayPoint cw = recent.parentLayer.fromLayer.data.nearestPointOnTrack(en, enPlus25px.east() - en.east()); 231 if (cw == null) { 232 JOptionPane.showMessageDialog( 233 Main.parent, 234 tr("You need to SHIFT-drag the play head onto an audio marker or onto the track point where you want to synchronize."), 235 tr("Warning"), 236 JOptionPane.WARNING_MESSAGE 237 ); 238 endDrag(true); 239 return; 240 } 241 ca = recent.parentLayer.addAudioMarker(cw.time, cw.getCoor()); 242 } 243 244 /* Actually do the synchronization */ 245 if (ca == null) { 246 JOptionPane.showMessageDialog( 247 Main.parent, 248 tr("Unable to create new audio marker."), 249 tr("Error"), 250 JOptionPane.ERROR_MESSAGE 251 ); 252 endDrag(true); 253 } else if (recent.parentLayer.synchronizeAudioMarkers(ca)) { 254 JOptionPane.showMessageDialog( 255 Main.parent, 256 tr("Audio synchronized at point {0}.", recent.parentLayer.syncAudioMarker.getText()), 257 tr("Information"), 258 JOptionPane.INFORMATION_MESSAGE 259 ); 260 setCoor(recent.parentLayer.syncAudioMarker.getCoor()); 261 endDrag(false); 262 } else { 263 JOptionPane.showMessageDialog( 264 Main.parent, 265 tr("Unable to synchronize in layer being played."), 266 tr("Error"), 267 JOptionPane.ERROR_MESSAGE 268 ); 269 endDrag(true); 270 } 271 } 272 273 /** 274 * Paint the marker icon in the given graphics context. 275 * @param g The graphics context 276 * @param mv The map 277 */ 278 public void paint(Graphics g, MapView mv) { 279 if (time < 0.0) return; 280 Point screen = mv.getPoint(getEastNorth()); 281 paintIcon(mv, g, screen.x, screen.y); 282 } 283 284 /** 285 * Animates the marker along the track. 286 */ 287 public void animate() { 288 if (!enabled) return; 289 jumpToMarker = true; 290 if (timer == null) { 291 animationInterval = Main.pref.getDouble("marker.audioanimationinterval", 1.0); //milliseconds 292 timer = new Timer((int) (animationInterval * 1000.0), new ActionListener() { 293 @Override 294 public void actionPerformed(ActionEvent e) { 295 timerAction(); 296 } 297 }); 298 timer.setInitialDelay(0); 299 } else { 300 timer.stop(); 301 } 302 timer.start(); 303 } 304 305 /** 306 * callback for moving play head marker according to audio player position 307 */ 308 public void timerAction() { 309 AudioMarker recentlyPlayedMarker = AudioMarker.recentlyPlayedMarker(); 310 if (recentlyPlayedMarker == null) 311 return; 312 double audioTime = recentlyPlayedMarker.time + 313 AudioPlayer.position() - 314 recentlyPlayedMarker.offset - 315 recentlyPlayedMarker.syncOffset; 316 if (Math.abs(audioTime - time) < animationInterval) 317 return; 318 if (recentlyPlayedMarker.parentLayer == null) return; 319 GpxLayer trackLayer = recentlyPlayedMarker.parentLayer.fromLayer; 320 if (trackLayer == null) 321 return; 322 /* find the pair of track points for this position (adjusted by the syncOffset) 323 * and interpolate between them 324 */ 325 WayPoint w1 = null; 326 WayPoint w2 = null; 327 328 for (GpxTrack track : trackLayer.data.tracks) { 329 for (GpxTrackSegment trackseg : track.getSegments()) { 330 for (WayPoint w: trackseg.getWayPoints()) { 331 if (audioTime < w.time) { 332 w2 = w; 333 break; 334 } 335 w1 = w; 336 } 337 if (w2 != null) { 338 break; 339 } 340 } 341 if (w2 != null) { 342 break; 343 } 344 } 345 346 if (w1 == null) 347 return; 348 setEastNorth(w2 == null ? 349 w1.getEastNorth() : 350 w1.getEastNorth().interpolate(w2.getEastNorth(), 351 (audioTime - w1.time)/(w2.time - w1.time))); 352 time = audioTime; 353 if (jumpToMarker) { 354 jumpToMarker = false; 355 Main.map.mapView.zoomTo(w1.getEastNorth()); 356 } 357 Main.map.mapView.repaint(); 358 } 359}