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