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