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