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