[8378] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
[626] | 2 | package org.openstreetmap.josm.gui.layer.markerlayer;
|
---|
| 3 |
|
---|
| 4 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
| 5 |
|
---|
| 6 | import java.awt.Graphics;
|
---|
| 7 | import java.awt.Point;
|
---|
| 8 | import java.awt.Rectangle;
|
---|
| 9 | import java.awt.event.MouseAdapter;
|
---|
| 10 | import java.awt.event.MouseEvent;
|
---|
[11746] | 11 | import java.io.IOException;
|
---|
[626] | 12 |
|
---|
| 13 | import javax.swing.JOptionPane;
|
---|
| 14 | import javax.swing.Timer;
|
---|
| 15 |
|
---|
| 16 | import org.openstreetmap.josm.Main;
|
---|
| 17 | import org.openstreetmap.josm.actions.mapmode.MapMode;
|
---|
| 18 | import org.openstreetmap.josm.actions.mapmode.PlayHeadDragMode;
|
---|
| 19 | import org.openstreetmap.josm.data.coor.EastNorth;
|
---|
| 20 | import org.openstreetmap.josm.data.coor.LatLon;
|
---|
| 21 | import org.openstreetmap.josm.data.gpx.GpxTrack;
|
---|
[2907] | 22 | import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
|
---|
[626] | 23 | import org.openstreetmap.josm.data.gpx.WayPoint;
|
---|
[12630] | 24 | import org.openstreetmap.josm.gui.MainApplication;
|
---|
| 25 | import org.openstreetmap.josm.gui.MapFrame;
|
---|
[626] | 26 | import org.openstreetmap.josm.gui.MapView;
|
---|
| 27 | import org.openstreetmap.josm.gui.layer.GpxLayer;
|
---|
[12326] | 28 | import org.openstreetmap.josm.io.audio.AudioPlayer;
|
---|
[12328] | 29 | import org.openstreetmap.josm.io.audio.AudioUtil;
|
---|
[626] | 30 |
|
---|
| 31 | /**
|
---|
| 32 | * Singleton marker class to track position of audio.
|
---|
[1169] | 33 | *
|
---|
[6830] | 34 | * @author David Earl <david@frankieandshadow.com>
|
---|
[5871] | 35 | * @since 572
|
---|
[626] | 36 | */
|
---|
[6362] | 37 | public final class PlayHeadMarker extends Marker {
|
---|
[626] | 38 |
|
---|
[8840] | 39 | private Timer timer;
|
---|
| 40 | private double animationInterval; // seconds
|
---|
| 41 | private static volatile PlayHeadMarker playHead;
|
---|
| 42 | private MapMode oldMode;
|
---|
[1724] | 43 | private LatLon oldCoor;
|
---|
[9078] | 44 | private final boolean enabled;
|
---|
[8840] | 45 | private boolean wasPlaying;
|
---|
[1245] | 46 | private int dropTolerance; /* pixels */
|
---|
[8840] | 47 | private boolean jumpToMarker;
|
---|
[572] | 48 |
|
---|
[5871] | 49 | /**
|
---|
| 50 | * Returns the unique instance of {@code PlayHeadMarker}.
|
---|
| 51 | * @return The unique instance of {@code PlayHeadMarker}.
|
---|
| 52 | */
|
---|
[1169] | 53 | public static PlayHeadMarker create() {
|
---|
| 54 | if (playHead == null) {
|
---|
[10212] | 55 | playHead = new PlayHeadMarker();
|
---|
[1169] | 56 | }
|
---|
| 57 | return playHead;
|
---|
| 58 | }
|
---|
[572] | 59 |
|
---|
[1169] | 60 | private PlayHeadMarker() {
|
---|
[9214] | 61 | super(LatLon.ZERO, "",
|
---|
[1865] | 62 | Main.pref.get("marker.audiotracericon", "audio-tracer"),
|
---|
| 63 | null, -1.0, 0.0);
|
---|
[1169] | 64 | enabled = Main.pref.getBoolean("marker.traceaudio", true);
|
---|
[8444] | 65 | if (!enabled) return;
|
---|
[1245] | 66 | dropTolerance = Main.pref.getInteger("marker.playHeadDropTolerance", 50);
|
---|
[12630] | 67 | if (MainApplication.isDisplayingMapView()) {
|
---|
| 68 | MapFrame map = MainApplication.getMap();
|
---|
| 69 | map.mapView.addMouseListener(new MouseAdapter() {
|
---|
[9779] | 70 | @Override public void mousePressed(MouseEvent ev) {
|
---|
[11381] | 71 | if (ev.getButton() == MouseEvent.BUTTON1 && playHead.containsPoint(ev.getPoint())) {
|
---|
[9779] | 72 | /* when we get a click on the marker, we need to switch mode to avoid
|
---|
| 73 | * getting confused with other drag operations (like select) */
|
---|
[12630] | 74 | oldMode = map.mapMode;
|
---|
[9779] | 75 | oldCoor = getCoor();
|
---|
| 76 | PlayHeadDragMode playHeadDragMode = new PlayHeadDragMode(playHead);
|
---|
[12630] | 77 | map.selectMapMode(playHeadDragMode);
|
---|
[9779] | 78 | playHeadDragMode.mousePressed(ev);
|
---|
| 79 | }
|
---|
[1169] | 80 | }
|
---|
[9779] | 81 | });
|
---|
| 82 | }
|
---|
[1169] | 83 | }
|
---|
[572] | 84 |
|
---|
[8510] | 85 | @Override
|
---|
| 86 | public boolean containsPoint(Point p) {
|
---|
[12725] | 87 | Point screen = MainApplication.getMap().mapView.getPoint(this);
|
---|
[1245] | 88 | Rectangle r = new Rectangle(screen.x, screen.y, symbol.getIconWidth(),
|
---|
[1865] | 89 | symbol.getIconHeight());
|
---|
[1169] | 90 | return r.contains(p);
|
---|
| 91 | }
|
---|
[572] | 92 |
|
---|
[1169] | 93 | /**
|
---|
| 94 | * called back from drag mode to say when we started dragging for real
|
---|
| 95 | * (at least a short distance)
|
---|
| 96 | */
|
---|
| 97 | public void startDrag() {
|
---|
[1865] | 98 | if (timer != null) {
|
---|
[1169] | 99 | timer.stop();
|
---|
[1865] | 100 | }
|
---|
[1169] | 101 | wasPlaying = AudioPlayer.playing();
|
---|
| 102 | if (wasPlaying) {
|
---|
[8510] | 103 | try {
|
---|
| 104 | AudioPlayer.pause();
|
---|
[11746] | 105 | } catch (IOException | InterruptedException ex) {
|
---|
[12328] | 106 | AudioUtil.audioMalfunction(ex);
|
---|
[8510] | 107 | }
|
---|
[1169] | 108 | }
|
---|
| 109 | }
|
---|
[582] | 110 |
|
---|
[1169] | 111 | /**
|
---|
[1724] | 112 | * reinstate the old map mode after switching temporarily to do a play head drag
|
---|
[9239] | 113 | * @param reset whether to reset state (pause audio and restore old coordinates)
|
---|
[1169] | 114 | */
|
---|
| 115 | private void endDrag(boolean reset) {
|
---|
[8444] | 116 | if (!wasPlaying || reset) {
|
---|
[8510] | 117 | try {
|
---|
| 118 | AudioPlayer.pause();
|
---|
[11746] | 119 | } catch (IOException | InterruptedException ex) {
|
---|
[12328] | 120 | AudioUtil.audioMalfunction(ex);
|
---|
[8510] | 121 | }
|
---|
[1865] | 122 | }
|
---|
| 123 | if (reset) {
|
---|
[1724] | 124 | setCoor(oldCoor);
|
---|
[1865] | 125 | }
|
---|
[12630] | 126 | MapFrame map = MainApplication.getMap();
|
---|
| 127 | map.selectMapMode(oldMode);
|
---|
| 128 | map.mapView.repaint();
|
---|
[12579] | 129 | if (timer != null) {
|
---|
| 130 | timer.start();
|
---|
| 131 | }
|
---|
[1169] | 132 | }
|
---|
[572] | 133 |
|
---|
[1169] | 134 | /**
|
---|
| 135 | * apply the new position resulting from a drag in progress
|
---|
| 136 | * @param en the new position in map terms
|
---|
| 137 | */
|
---|
| 138 | public void drag(EastNorth en) {
|
---|
[1724] | 139 | setEastNorth(en);
|
---|
[12630] | 140 | MainApplication.getMap().mapView.repaint();
|
---|
[1169] | 141 | }
|
---|
[572] | 142 |
|
---|
[1169] | 143 | /**
|
---|
| 144 | * reposition the play head at the point on the track nearest position given,
|
---|
| 145 | * providing we are within reasonable distance from the track; otherwise reset to the
|
---|
| 146 | * original position.
|
---|
| 147 | * @param en the position to start looking from
|
---|
| 148 | */
|
---|
| 149 | public void reposition(EastNorth en) {
|
---|
| 150 | WayPoint cw = null;
|
---|
| 151 | AudioMarker recent = AudioMarker.recentlyPlayedMarker();
|
---|
| 152 | if (recent != null && recent.parentLayer != null && recent.parentLayer.fromLayer != null) {
|
---|
| 153 | /* work out EastNorth equivalent of 50 (default) pixels tolerance */
|
---|
[12630] | 154 | MapView mapView = MainApplication.getMap().mapView;
|
---|
| 155 | Point p = mapView.getPoint(en);
|
---|
| 156 | EastNorth enPlus25px = mapView.getEastNorth(p.x+dropTolerance, p.y);
|
---|
[5715] | 157 | cw = recent.parentLayer.fromLayer.data.nearestPointOnTrack(en, enPlus25px.east() - en.east());
|
---|
[1169] | 158 | }
|
---|
[572] | 159 |
|
---|
[1169] | 160 | AudioMarker ca = null;
|
---|
| 161 | /* Find the prior audio marker (there should always be one in the
|
---|
| 162 | * layer, even if it is only one at the start of the track) to
|
---|
| 163 | * offset the audio from */
|
---|
[6883] | 164 | if (cw != null && recent != null && recent.parentLayer != null) {
|
---|
| 165 | for (Marker m : recent.parentLayer.data) {
|
---|
| 166 | if (m instanceof AudioMarker) {
|
---|
| 167 | AudioMarker a = (AudioMarker) m;
|
---|
| 168 | if (a.time > cw.time) {
|
---|
| 169 | break;
|
---|
[1169] | 170 | }
|
---|
[6883] | 171 | ca = a;
|
---|
[1169] | 172 | }
|
---|
| 173 | }
|
---|
| 174 | }
|
---|
[572] | 175 |
|
---|
[1169] | 176 | if (ca == null) {
|
---|
| 177 | /* Not close enough to track, or no audio marker found for some other reason */
|
---|
[2017] | 178 | JOptionPane.showMessageDialog(
|
---|
[1865] | 179 | Main.parent,
|
---|
[8510] | 180 | tr("You need to drag the play head near to the GPX track " +
|
---|
| 181 | "whose associated sound track you were playing (after the first marker)."),
|
---|
[1865] | 182 | tr("Warning"),
|
---|
| 183 | JOptionPane.WARNING_MESSAGE
|
---|
[4282] | 184 | );
|
---|
[1169] | 185 | endDrag(true);
|
---|
| 186 | } else {
|
---|
[9239] | 187 | if (cw != null) {
|
---|
| 188 | setCoor(cw.getCoor());
|
---|
| 189 | ca.play(cw.time - ca.time);
|
---|
| 190 | }
|
---|
[1169] | 191 | endDrag(false);
|
---|
| 192 | }
|
---|
| 193 | }
|
---|
| 194 |
|
---|
| 195 | /**
|
---|
| 196 | * Synchronize the audio at the position where the play head was paused before
|
---|
| 197 | * dragging with the position on the track where it was dropped.
|
---|
| 198 | * If this is quite near an audio marker, we use that
|
---|
| 199 | * marker as the sync. location, otherwise we create a new marker at the
|
---|
| 200 | * trackpoint nearest the end point of the drag point to apply the
|
---|
| 201 | * sync to.
|
---|
| 202 | * @param en : the EastNorth end point of the drag
|
---|
| 203 | */
|
---|
| 204 | public void synchronize(EastNorth en) {
|
---|
| 205 | AudioMarker recent = AudioMarker.recentlyPlayedMarker();
|
---|
[8510] | 206 | if (recent == null)
|
---|
[1169] | 207 | return;
|
---|
| 208 | /* First, see if we dropped onto an existing audio marker in the layer being played */
|
---|
[12630] | 209 | MapView mapView = MainApplication.getMap().mapView;
|
---|
| 210 | Point startPoint = mapView.getPoint(en);
|
---|
[1169] | 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) {
|
---|
[12725] | 216 | double distanceSquared = m.getEastNorth(Main.getProjection()).distanceSq(en);
|
---|
[1169] | 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? */
|
---|
[8444] | 226 | if (ca != null && !ca.containsPoint(startPoint)) {
|
---|
[1865] | 227 | ca = null;
|
---|
| 228 | }
|
---|
[1169] | 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 */
|
---|
[12630] | 233 | Point p = mapView.getPoint(en);
|
---|
| 234 | EastNorth enPlus25px = mapView.getEastNorth(p.x+dropTolerance, p.y);
|
---|
[5715] | 235 | WayPoint cw = recent.parentLayer.fromLayer.data.nearestPointOnTrack(en, enPlus25px.east() - en.east());
|
---|
[1169] | 236 | if (cw == null) {
|
---|
[2017] | 237 | JOptionPane.showMessageDialog(
|
---|
[1865] | 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
|
---|
[4282] | 242 | );
|
---|
[1169] | 243 | endDrag(true);
|
---|
| 244 | return;
|
---|
| 245 | }
|
---|
[1724] | 246 | ca = recent.parentLayer.addAudioMarker(cw.time, cw.getCoor());
|
---|
[1169] | 247 | }
|
---|
| 248 |
|
---|
| 249 | /* Actually do the synchronization */
|
---|
[8510] | 250 | if (ca == null) {
|
---|
[2017] | 251 | JOptionPane.showMessageDialog(
|
---|
[1865] | 252 | Main.parent,
|
---|
| 253 | tr("Unable to create new audio marker."),
|
---|
| 254 | tr("Error"),
|
---|
| 255 | JOptionPane.ERROR_MESSAGE
|
---|
[4282] | 256 | );
|
---|
[1169] | 257 | endDrag(true);
|
---|
[8342] | 258 | } else if (recent.parentLayer.synchronizeAudioMarkers(ca)) {
|
---|
[2017] | 259 | JOptionPane.showMessageDialog(
|
---|
[1865] | 260 | Main.parent,
|
---|
[7310] | 261 | tr("Audio synchronized at point {0}.", recent.parentLayer.syncAudioMarker.getText()),
|
---|
[1865] | 262 | tr("Information"),
|
---|
| 263 | JOptionPane.INFORMATION_MESSAGE
|
---|
[4282] | 264 | );
|
---|
[7310] | 265 | setCoor(recent.parentLayer.syncAudioMarker.getCoor());
|
---|
[1169] | 266 | endDrag(false);
|
---|
| 267 | } else {
|
---|
[2017] | 268 | JOptionPane.showMessageDialog(
|
---|
[1865] | 269 | Main.parent,
|
---|
| 270 | tr("Unable to synchronize in layer being played."),
|
---|
| 271 | tr("Error"),
|
---|
| 272 | JOptionPane.ERROR_MESSAGE
|
---|
[4282] | 273 | );
|
---|
[1169] | 274 | endDrag(true);
|
---|
| 275 | }
|
---|
| 276 | }
|
---|
| 277 |
|
---|
[5871] | 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) {
|
---|
[1169] | 284 | if (time < 0.0) return;
|
---|
[12725] | 285 | Point screen = mv.getPoint(this);
|
---|
[6299] | 286 | paintIcon(mv, g, screen.x, screen.y);
|
---|
[1169] | 287 | }
|
---|
| 288 |
|
---|
[5871] | 289 | /**
|
---|
| 290 | * Animates the marker along the track.
|
---|
| 291 | */
|
---|
[1169] | 292 | public void animate() {
|
---|
[8444] | 293 | if (!enabled) return;
|
---|
[7094] | 294 | jumpToMarker = true;
|
---|
[1169] | 295 | if (timer == null) {
|
---|
[5871] | 296 | animationInterval = Main.pref.getDouble("marker.audioanimationinterval", 1.0); //milliseconds
|
---|
[10611] | 297 | timer = new Timer((int) (animationInterval * 1000.0), e -> timerAction());
|
---|
[1169] | 298 | timer.setInitialDelay(0);
|
---|
| 299 | } else {
|
---|
| 300 | timer.stop();
|
---|
| 301 | }
|
---|
| 302 | timer.start();
|
---|
| 303 | }
|
---|
| 304 |
|
---|
| 305 | /**
|
---|
| 306 | * callback for moving play head marker according to audio player position
|
---|
| 307 | */
|
---|
| 308 | public void timerAction() {
|
---|
| 309 | AudioMarker recentlyPlayedMarker = AudioMarker.recentlyPlayedMarker();
|
---|
| 310 | if (recentlyPlayedMarker == null)
|
---|
| 311 | return;
|
---|
| 312 | double audioTime = recentlyPlayedMarker.time +
|
---|
[4282] | 313 | AudioPlayer.position() -
|
---|
| 314 | recentlyPlayedMarker.offset -
|
---|
| 315 | recentlyPlayedMarker.syncOffset;
|
---|
[1169] | 316 | if (Math.abs(audioTime - time) < animationInterval)
|
---|
| 317 | return;
|
---|
| 318 | if (recentlyPlayedMarker.parentLayer == null) return;
|
---|
| 319 | GpxLayer trackLayer = recentlyPlayedMarker.parentLayer.fromLayer;
|
---|
| 320 | if (trackLayer == null)
|
---|
| 321 | return;
|
---|
| 322 | /* find the pair of track points for this position (adjusted by the syncOffset)
|
---|
| 323 | * and interpolate between them
|
---|
| 324 | */
|
---|
| 325 | WayPoint w1 = null;
|
---|
| 326 | WayPoint w2 = null;
|
---|
| 327 |
|
---|
[12156] | 328 | for (GpxTrack track : trackLayer.data.getTracks()) {
|
---|
[2907] | 329 | for (GpxTrackSegment trackseg : track.getSegments()) {
|
---|
| 330 | for (WayPoint w: trackseg.getWayPoints()) {
|
---|
[1169] | 331 | if (audioTime < w.time) {
|
---|
| 332 | w2 = w;
|
---|
| 333 | break;
|
---|
| 334 | }
|
---|
| 335 | w1 = w;
|
---|
| 336 | }
|
---|
[1865] | 337 | if (w2 != null) {
|
---|
| 338 | break;
|
---|
| 339 | }
|
---|
[1169] | 340 | }
|
---|
[1865] | 341 | if (w2 != null) {
|
---|
| 342 | break;
|
---|
| 343 | }
|
---|
[1169] | 344 | }
|
---|
| 345 |
|
---|
| 346 | if (w1 == null)
|
---|
| 347 | return;
|
---|
[1724] | 348 | setEastNorth(w2 == null ?
|
---|
[12725] | 349 | w1.getEastNorth(Main.getProjection()) :
|
---|
| 350 | w1.getEastNorth(Main.getProjection()).interpolate(w2.getEastNorth(Main.getProjection()),
|
---|
[1865] | 351 | (audioTime - w1.time)/(w2.time - w1.time)));
|
---|
[1169] | 352 | time = audioTime;
|
---|
[12630] | 353 | MapView mapView = MainApplication.getMap().mapView;
|
---|
[7094] | 354 | if (jumpToMarker) {
|
---|
| 355 | jumpToMarker = false;
|
---|
[12725] | 356 | mapView.zoomTo(w1);
|
---|
[7094] | 357 | }
|
---|
[12630] | 358 | mapView.repaint();
|
---|
[1169] | 359 | }
|
---|
[626] | 360 | }
|
---|