source: josm/trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/PlayHeadMarker.java@ 12574

Last change on this file since 12574 was 12328, checked in by Don-vip, 7 years ago

fix #2089 - Add support for MP3, AIFF and AAC audio codecs (.mp3, .aac, .aif, .aiff files) if Java FX is on the classpath (i.e. Windows, macOS, nearly all major Linux distributions). The classes are not public on purpose, as the whole system will have to be simplified when all Linux distributions propose Java FX and so we can get rid of old Java Sound implementation.

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