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