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