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

Last change on this file since 12725 was 12725, checked in by bastiK, 7 weeks ago

see #15229 - deprecate ILatLon#getEastNorth() so ILatLon has no dependency on Main.proj

  • Property svn:eol-style set to native
File size: 13.2 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.MainApplication;
25import org.openstreetmap.josm.gui.MapFrame;
26import org.openstreetmap.josm.gui.MapView;
27import org.openstreetmap.josm.gui.layer.GpxLayer;
28import org.openstreetmap.josm.io.audio.AudioPlayer;
29import org.openstreetmap.josm.io.audio.AudioUtil;
30
31/**
32 * Singleton marker class to track position of audio.
33 *
34 * @author David Earl <david@frankieandshadow.com>
35 * @since 572
36 */
37public final class PlayHeadMarker extends Marker {
38
39    private Timer timer;
40    private double animationInterval; // seconds
41    private static volatile PlayHeadMarker playHead;
42    private MapMode oldMode;
43    private LatLon oldCoor;
44    private final boolean enabled;
45    private boolean wasPlaying;
46    private int dropTolerance; /* pixels */
47    private boolean jumpToMarker;
48
49    /**
50     * Returns the unique instance of {@code PlayHeadMarker}.
51     * @return The unique instance of {@code PlayHeadMarker}.
52     */
53    public static PlayHeadMarker create() {
54        if (playHead == null) {
55            playHead = new PlayHeadMarker();
56        }
57        return playHead;
58    }
59
60    private PlayHeadMarker() {
61        super(LatLon.ZERO, "",
62                Main.pref.get("marker.audiotracericon", "audio-tracer"),
63                null, -1.0, 0.0);
64        enabled = Main.pref.getBoolean("marker.traceaudio", true);
65        if (!enabled) return;
66        dropTolerance = Main.pref.getInteger("marker.playHeadDropTolerance", 50);
67        if (MainApplication.isDisplayingMapView()) {
68            MapFrame map = MainApplication.getMap();
69            map.mapView.addMouseListener(new MouseAdapter() {
70                @Override public void mousePressed(MouseEvent ev) {
71                    if (ev.getButton() == MouseEvent.BUTTON1 && playHead.containsPoint(ev.getPoint())) {
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) */
74                        oldMode = map.mapMode;
75                        oldCoor = getCoor();
76                        PlayHeadDragMode playHeadDragMode = new PlayHeadDragMode(playHead);
77                        map.selectMapMode(playHeadDragMode);
78                        playHeadDragMode.mousePressed(ev);
79                    }
80                }
81            });
82        }
83    }
84
85    @Override
86    public boolean containsPoint(Point p) {
87        Point screen = MainApplication.getMap().mapView.getPoint(this);
88        Rectangle r = new Rectangle(screen.x, screen.y, symbol.getIconWidth(),
89                symbol.getIconHeight());
90        return r.contains(p);
91    }
92
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() {
98        if (timer != null) {
99            timer.stop();
100        }
101        wasPlaying = AudioPlayer.playing();
102        if (wasPlaying) {
103            try {
104                AudioPlayer.pause();
105            } catch (IOException | InterruptedException ex) {
106                AudioUtil.audioMalfunction(ex);
107            }
108        }
109    }
110
111    /**
112     * reinstate the old map mode after switching temporarily to do a play head drag
113     * @param reset whether to reset state (pause audio and restore old coordinates)
114     */
115    private void endDrag(boolean reset) {
116        if (!wasPlaying || reset) {
117            try {
118                AudioPlayer.pause();
119            } catch (IOException | InterruptedException ex) {
120                AudioUtil.audioMalfunction(ex);
121            }
122        }
123        if (reset) {
124            setCoor(oldCoor);
125        }
126        MapFrame map = MainApplication.getMap();
127        map.selectMapMode(oldMode);
128        map.mapView.repaint();
129        if (timer != null) {
130            timer.start();
131        }
132    }
133
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) {
139        setEastNorth(en);
140        MainApplication.getMap().mapView.repaint();
141    }
142
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 */
154            MapView mapView = MainApplication.getMap().mapView;
155            Point p = mapView.getPoint(en);
156            EastNorth enPlus25px = mapView.getEastNorth(p.x+dropTolerance, p.y);
157            cw = recent.parentLayer.fromLayer.data.nearestPointOnTrack(en, enPlus25px.east() - en.east());
158        }
159
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 */
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;
170                    }
171                    ca = a;
172                }
173            }
174        }
175
176        if (ca == null) {
177            /* Not close enough to track, or no audio marker found for some other reason */
178            JOptionPane.showMessageDialog(
179                    Main.parent,
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)."),
182                    tr("Warning"),
183                    JOptionPane.WARNING_MESSAGE
184                    );
185            endDrag(true);
186        } else {
187            if (cw != null) {
188                setCoor(cw.getCoor());
189                ca.play(cw.time - ca.time);
190            }
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();
206        if (recent == null)
207            return;
208        /* First, see if we dropped onto an existing audio marker in the layer being played */
209        MapView mapView = MainApplication.getMap().mapView;
210        Point startPoint = mapView.getPoint(en);
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) {
216                    double distanceSquared = m.getEastNorth(Main.getProjection()).distanceSq(en);
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? */
226        if (ca != null && !ca.containsPoint(startPoint)) {
227            ca = null;
228        }
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 */
233            Point p = mapView.getPoint(en);
234            EastNorth enPlus25px = mapView.getEastNorth(p.x+dropTolerance, p.y);
235            WayPoint cw = recent.parentLayer.fromLayer.data.nearestPointOnTrack(en, enPlus25px.east() - en.east());
236            if (cw == null) {
237                JOptionPane.showMessageDialog(
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
242                        );
243                endDrag(true);
244                return;
245            }
246            ca = recent.parentLayer.addAudioMarker(cw.time, cw.getCoor());
247        }
248
249        /* Actually do the synchronization */
250        if (ca == null) {
251            JOptionPane.showMessageDialog(
252                    Main.parent,
253                    tr("Unable to create new audio marker."),
254                    tr("Error"),
255                    JOptionPane.ERROR_MESSAGE
256                    );
257            endDrag(true);
258        } else if (recent.parentLayer.synchronizeAudioMarkers(ca)) {
259            JOptionPane.showMessageDialog(
260                    Main.parent,
261                    tr("Audio synchronized at point {0}.", recent.parentLayer.syncAudioMarker.getText()),
262                    tr("Information"),
263                    JOptionPane.INFORMATION_MESSAGE
264                    );
265            setCoor(recent.parentLayer.syncAudioMarker.getCoor());
266            endDrag(false);
267        } else {
268            JOptionPane.showMessageDialog(
269                    Main.parent,
270                    tr("Unable to synchronize in layer being played."),
271                    tr("Error"),
272                    JOptionPane.ERROR_MESSAGE
273                    );
274            endDrag(true);
275        }
276    }
277
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) {
284        if (time < 0.0) return;
285        Point screen = mv.getPoint(this);
286        paintIcon(mv, g, screen.x, screen.y);
287    }
288
289    /**
290     * Animates the marker along the track.
291     */
292    public void animate() {
293        if (!enabled) return;
294        jumpToMarker = true;
295        if (timer == null) {
296            animationInterval = Main.pref.getDouble("marker.audioanimationinterval", 1.0); //milliseconds
297            timer = new Timer((int) (animationInterval * 1000.0), e -> timerAction());
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 +
313                AudioPlayer.position() -
314                recentlyPlayedMarker.offset -
315                recentlyPlayedMarker.syncOffset;
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
328        for (GpxTrack track : trackLayer.data.getTracks()) {
329            for (GpxTrackSegment trackseg : track.getSegments()) {
330                for (WayPoint w: trackseg.getWayPoints()) {
331                    if (audioTime < w.time) {
332                        w2 = w;
333                        break;
334                    }
335                    w1 = w;
336                }
337                if (w2 != null) {
338                    break;
339                }
340            }
341            if (w2 != null) {
342                break;
343            }
344        }
345
346        if (w1 == null)
347            return;
348        setEastNorth(w2 == null ?
349                w1.getEastNorth(Main.getProjection()) :
350                    w1.getEastNorth(Main.getProjection()).interpolate(w2.getEastNorth(Main.getProjection()),
351                            (audioTime - w1.time)/(w2.time - w1.time)));
352        time = audioTime;
353        MapView mapView = MainApplication.getMap().mapView;
354        if (jumpToMarker) {
355            jumpToMarker = false;
356            mapView.zoomTo(w1);
357        }
358        mapView.repaint();
359    }
360}
Note: See TracBrowser for help on using the repository browser.