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 if (ev.getButton() == MouseEvent.BUTTON1 && playHead.containsPoint(ev.getPoint())) { 067 /* when we get a click on the marker, we need to switch mode to avoid 068 * getting confused with other drag operations (like select) */ 069 oldMode = Main.map.mapMode; 070 oldCoor = getCoor(); 071 PlayHeadDragMode playHeadDragMode = new PlayHeadDragMode(playHead); 072 Main.map.selectMapMode(playHeadDragMode); 073 playHeadDragMode.mousePressed(ev); 074 } 075 } 076 }); 077 } 078 } 079 080 @Override 081 public boolean containsPoint(Point p) { 082 Point screen = Main.map.mapView.getPoint(getEastNorth()); 083 Rectangle r = new Rectangle(screen.x, screen.y, symbol.getIconWidth(), 084 symbol.getIconHeight()); 085 return r.contains(p); 086 } 087 088 /** 089 * called back from drag mode to say when we started dragging for real 090 * (at least a short distance) 091 */ 092 public void startDrag() { 093 if (timer != null) { 094 timer.stop(); 095 } 096 wasPlaying = AudioPlayer.playing(); 097 if (wasPlaying) { 098 try { 099 AudioPlayer.pause(); 100 } catch (Exception ex) { 101 AudioPlayer.audioMalfunction(ex); 102 } 103 } 104 } 105 106 /** 107 * reinstate the old map mode after switching temporarily to do a play head drag 108 * @param reset whether to reset state (pause audio and restore old coordinates) 109 */ 110 private void endDrag(boolean reset) { 111 if (!wasPlaying || reset) { 112 try { 113 AudioPlayer.pause(); 114 } catch (Exception ex) { 115 AudioPlayer.audioMalfunction(ex); 116 } 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 " + 172 "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 if (cw != null) { 179 setCoor(cw.getCoor()); 180 ca.play(cw.time - ca.time); 181 } 182 endDrag(false); 183 } 184 } 185 186 /** 187 * Synchronize the audio at the position where the play head was paused before 188 * dragging with the position on the track where it was dropped. 189 * If this is quite near an audio marker, we use that 190 * marker as the sync. location, otherwise we create a new marker at the 191 * trackpoint nearest the end point of the drag point to apply the 192 * sync to. 193 * @param en : the EastNorth end point of the drag 194 */ 195 public void synchronize(EastNorth en) { 196 AudioMarker recent = AudioMarker.recentlyPlayedMarker(); 197 if (recent == null) 198 return; 199 /* First, see if we dropped onto an existing audio marker in the layer being played */ 200 Point startPoint = Main.map.mapView.getPoint(en); 201 AudioMarker ca = null; 202 if (recent.parentLayer != null) { 203 double closestAudioMarkerDistanceSquared = 1.0E100; 204 for (Marker m : recent.parentLayer.data) { 205 if (m instanceof AudioMarker) { 206 double distanceSquared = m.getEastNorth().distanceSq(en); 207 if (distanceSquared < closestAudioMarkerDistanceSquared) { 208 ca = (AudioMarker) m; 209 closestAudioMarkerDistanceSquared = distanceSquared; 210 } 211 } 212 } 213 } 214 215 /* We found the closest marker: did we actually hit it? */ 216 if (ca != null && !ca.containsPoint(startPoint)) { 217 ca = null; 218 } 219 220 /* If we didn't hit an audio marker, we need to create one at the nearest point on the track */ 221 if (ca == null) { 222 /* work out EastNorth equivalent of 50 (default) pixels tolerance */ 223 Point p = Main.map.mapView.getPoint(en); 224 EastNorth enPlus25px = Main.map.mapView.getEastNorth(p.x+dropTolerance, p.y); 225 WayPoint cw = recent.parentLayer.fromLayer.data.nearestPointOnTrack(en, enPlus25px.east() - en.east()); 226 if (cw == null) { 227 JOptionPane.showMessageDialog( 228 Main.parent, 229 tr("You need to SHIFT-drag the play head onto an audio marker or onto the track point where you want to synchronize."), 230 tr("Warning"), 231 JOptionPane.WARNING_MESSAGE 232 ); 233 endDrag(true); 234 return; 235 } 236 ca = recent.parentLayer.addAudioMarker(cw.time, cw.getCoor()); 237 } 238 239 /* Actually do the synchronization */ 240 if (ca == null) { 241 JOptionPane.showMessageDialog( 242 Main.parent, 243 tr("Unable to create new audio marker."), 244 tr("Error"), 245 JOptionPane.ERROR_MESSAGE 246 ); 247 endDrag(true); 248 } else if (recent.parentLayer.synchronizeAudioMarkers(ca)) { 249 JOptionPane.showMessageDialog( 250 Main.parent, 251 tr("Audio synchronized at point {0}.", recent.parentLayer.syncAudioMarker.getText()), 252 tr("Information"), 253 JOptionPane.INFORMATION_MESSAGE 254 ); 255 setCoor(recent.parentLayer.syncAudioMarker.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 jumpToMarker = true; 285 if (timer == null) { 286 animationInterval = Main.pref.getDouble("marker.audioanimationinterval", 1.0); //milliseconds 287 timer = new Timer((int) (animationInterval * 1000.0), e -> timerAction()); 288 timer.setInitialDelay(0); 289 } else { 290 timer.stop(); 291 } 292 timer.start(); 293 } 294 295 /** 296 * callback for moving play head marker according to audio player position 297 */ 298 public void timerAction() { 299 AudioMarker recentlyPlayedMarker = AudioMarker.recentlyPlayedMarker(); 300 if (recentlyPlayedMarker == null) 301 return; 302 double audioTime = recentlyPlayedMarker.time + 303 AudioPlayer.position() - 304 recentlyPlayedMarker.offset - 305 recentlyPlayedMarker.syncOffset; 306 if (Math.abs(audioTime - time) < animationInterval) 307 return; 308 if (recentlyPlayedMarker.parentLayer == null) return; 309 GpxLayer trackLayer = recentlyPlayedMarker.parentLayer.fromLayer; 310 if (trackLayer == null) 311 return; 312 /* find the pair of track points for this position (adjusted by the syncOffset) 313 * and interpolate between them 314 */ 315 WayPoint w1 = null; 316 WayPoint w2 = null; 317 318 for (GpxTrack track : trackLayer.data.tracks) { 319 for (GpxTrackSegment trackseg : track.getSegments()) { 320 for (WayPoint w: trackseg.getWayPoints()) { 321 if (audioTime < w.time) { 322 w2 = w; 323 break; 324 } 325 w1 = w; 326 } 327 if (w2 != null) { 328 break; 329 } 330 } 331 if (w2 != null) { 332 break; 333 } 334 } 335 336 if (w1 == null) 337 return; 338 setEastNorth(w2 == null ? 339 w1.getEastNorth() : 340 w1.getEastNorth().interpolate(w2.getEastNorth(), 341 (audioTime - w1.time)/(w2.time - w1.time))); 342 time = audioTime; 343 if (jumpToMarker) { 344 jumpToMarker = false; 345 Main.map.mapView.zoomTo(w1.getEastNorth()); 346 } 347 Main.map.mapView.repaint(); 348 } 349}