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