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