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