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