001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.layer.gpx;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.event.ActionEvent;
008import java.io.File;
009import java.net.URL;
010import java.util.ArrayList;
011import java.util.Arrays;
012import java.util.Collection;
013import java.util.Collections;
014import java.util.Comparator;
015
016import javax.swing.AbstractAction;
017import javax.swing.JFileChooser;
018import javax.swing.JOptionPane;
019import javax.swing.filechooser.FileFilter;
020
021import org.openstreetmap.josm.Main;
022import org.openstreetmap.josm.actions.DiskAccessAction;
023import org.openstreetmap.josm.data.gpx.GpxData;
024import org.openstreetmap.josm.data.gpx.GpxTrack;
025import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
026import org.openstreetmap.josm.data.gpx.WayPoint;
027import org.openstreetmap.josm.gui.HelpAwareOptionPane;
028import org.openstreetmap.josm.gui.layer.GpxLayer;
029import org.openstreetmap.josm.gui.layer.markerlayer.AudioMarker;
030import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
031import org.openstreetmap.josm.tools.AudioUtil;
032import org.openstreetmap.josm.tools.ImageProvider;
033import org.openstreetmap.josm.tools.Utils;
034
035/**
036 * Import audio files into a GPX layer to enable audio playback functions.
037 * @since 5715
038 */
039public class ImportAudioAction extends AbstractAction {
040    private final GpxLayer layer;
041
042    private static class Markers {
043        public boolean timedMarkersOmitted = false;
044        public boolean untimedMarkersOmitted = false;
045    }
046
047    /**
048     * Constructs a new {@code ImportAudioAction}.
049     * @param layer The associated GPX layer
050     */
051    public ImportAudioAction(final GpxLayer layer) {
052        super(tr("Import Audio"), ImageProvider.get("importaudio"));
053        this.layer = layer;
054        putValue("help", ht("/Action/ImportAudio"));
055    }
056
057    private void warnCantImportIntoServerLayer(GpxLayer layer) {
058        String msg = tr("<html>The data in the GPX layer ''{0}'' has been downloaded from the server.<br>" + "Because its way points do not include a timestamp we cannot correlate them with audio data.</html>", layer.getName());
059        HelpAwareOptionPane.showOptionDialog(Main.parent, msg, tr("Import not possible"), JOptionPane.WARNING_MESSAGE, ht("/Action/ImportAudio#CantImportIntoGpxLayerFromServer"));
060    }
061
062    @Override
063    public void actionPerformed(ActionEvent e) {
064        if (layer.data.fromServer) {
065            warnCantImportIntoServerLayer(layer);
066            return;
067        }
068        FileFilter filter = new FileFilter() {
069            @Override
070            public boolean accept(File f) {
071                return f.isDirectory() || f.getName().toLowerCase().endsWith(".wav");
072            }
073
074            @Override
075            public String getDescription() {
076                return tr("Wave Audio files (*.wav)");
077            }
078        };
079        JFileChooser fc = DiskAccessAction.createAndOpenFileChooser(true, true, null, filter, JFileChooser.FILES_ONLY, "markers.lastaudiodirectory");
080        if (fc != null) {
081            File[] sel = fc.getSelectedFiles();
082            // sort files in increasing order of timestamp (this is the end time, but so
083            // long as they don't overlap, that's fine)
084            if (sel.length > 1) {
085                Arrays.sort(sel, new Comparator<File>() {
086                    @Override
087                    public int compare(File a, File b) {
088                        return a.lastModified() <= b.lastModified() ? -1 : 1;
089                    }
090                });
091            }
092            String names = null;
093            for (File file : sel) {
094                if (names == null) {
095                    names = " (";
096                } else {
097                    names += ", ";
098                }
099                names += file.getName();
100            }
101            if (names != null) {
102                names += ")";
103            } else {
104                names = "";
105            }
106            MarkerLayer ml = new MarkerLayer(new GpxData(), tr("Audio markers from {0}", layer.getName()) + names, layer.getAssociatedFile(), layer);
107            double firstStartTime = sel[0].lastModified() / 1000.0 - AudioUtil.getCalibratedDuration(sel[0]);
108            Markers m = new Markers();
109            for (File file : sel) {
110                importAudio(file, ml, firstStartTime, m);
111            }
112            Main.main.addLayer(ml);
113            Main.map.repaint();
114        }
115    }
116
117    /**
118     * Makes a new marker layer derived from this GpxLayer containing at least one audio marker
119     * which the given audio file is associated with. Markers are derived from the following (a)
120     * explict waypoints in the GPX layer, or (b) named trackpoints in the GPX layer, or (d)
121     * timestamp on the wav file (e) (in future) voice recognised markers in the sound recording (f)
122     * a single marker at the beginning of the track
123     * @param wavFile : the file to be associated with the markers in the new marker layer
124     * @param markers : keeps track of warning messages to avoid repeated warnings
125     */
126    private void importAudio(File wavFile, MarkerLayer ml, double firstStartTime, Markers markers) {
127        URL url = Utils.fileToURL(wavFile);
128        boolean hasTracks = layer.data.tracks != null && !layer.data.tracks.isEmpty();
129        boolean hasWaypoints = layer.data.waypoints != null && !layer.data.waypoints.isEmpty();
130        Collection<WayPoint> waypoints = new ArrayList<>();
131        boolean timedMarkersOmitted = false;
132        boolean untimedMarkersOmitted = false;
133        double snapDistance = Main.pref.getDouble("marker.audiofromuntimedwaypoints.distance", 1.0e-3); 
134        // about 25 m
135        WayPoint wayPointFromTimeStamp = null;
136
137        // determine time of first point in track
138        double firstTime = -1.0;
139        if (hasTracks) {
140            for (GpxTrack track : layer.data.tracks) {
141                for (GpxTrackSegment seg : track.getSegments()) {
142                    for (WayPoint w : seg.getWayPoints()) {
143                        firstTime = w.time;
144                        break;
145                    }
146                    if (firstTime >= 0.0) {
147                        break;
148                    }
149                }
150                if (firstTime >= 0.0) {
151                    break;
152                }
153            }
154        }
155        if (firstTime < 0.0) {
156            JOptionPane.showMessageDialog(
157                    Main.parent,
158                    tr("No GPX track available in layer to associate audio with."),
159                    tr("Error"),
160                    JOptionPane.ERROR_MESSAGE
161                    );
162            return;
163        }
164
165        // (a) try explicit timestamped waypoints - unless suppressed
166        if (Main.pref.getBoolean("marker.audiofromexplicitwaypoints", true) && hasWaypoints) {
167            for (WayPoint w : layer.data.waypoints) {
168                if (w.time > firstTime) {
169                    waypoints.add(w);
170                } else if (w.time > 0.0) {
171                    timedMarkersOmitted = true;
172                }
173            }
174        }
175
176        // (b) try explicit waypoints without timestamps - unless suppressed
177        if (Main.pref.getBoolean("marker.audiofromuntimedwaypoints", true) && hasWaypoints) {
178            for (WayPoint w : layer.data.waypoints) {
179                if (waypoints.contains(w)) {
180                    continue;
181                }
182                WayPoint wNear = layer.data.nearestPointOnTrack(w.getEastNorth(), snapDistance);
183                if (wNear != null) {
184                    WayPoint wc = new WayPoint(w.getCoor());
185                    wc.time = wNear.time;
186                    if (w.attr.containsKey("name")) {
187                        wc.attr.put("name", w.getString("name"));
188                    }
189                    waypoints.add(wc);
190                } else {
191                    untimedMarkersOmitted = true;
192                }
193            }
194        }
195
196        // (c) use explicitly named track points, again unless suppressed
197        if ((Main.pref.getBoolean("marker.audiofromnamedtrackpoints", false)) && layer.data.tracks != null
198                && !layer.data.tracks.isEmpty()) {
199            for (GpxTrack track : layer.data.tracks) {
200                for (GpxTrackSegment seg : track.getSegments()) {
201                    for (WayPoint w : seg.getWayPoints()) {
202                        if (w.attr.containsKey("name") || w.attr.containsKey("desc")) {
203                            waypoints.add(w);
204                        }
205                    }
206                }
207            }
208        }
209
210        // (d) use timestamp of file as location on track
211        if ((Main.pref.getBoolean("marker.audiofromwavtimestamps", false)) && hasTracks) {
212            double lastModified = wavFile.lastModified() / 1000.0; // lastModified is in
213            // milliseconds
214            double duration = AudioUtil.getCalibratedDuration(wavFile);
215            double startTime = lastModified - duration;
216            startTime = firstStartTime + (startTime - firstStartTime)
217                    / Main.pref.getDouble("audio.calibration", 1.0 /* default, ratio */);
218            WayPoint w1 = null;
219            WayPoint w2 = null;
220
221            for (GpxTrack track : layer.data.tracks) {
222                for (GpxTrackSegment seg : track.getSegments()) {
223                    for (WayPoint w : seg.getWayPoints()) {
224                        if (startTime < w.time) {
225                            w2 = w;
226                            break;
227                        }
228                        w1 = w;
229                    }
230                    if (w2 != null) {
231                        break;
232                    }
233                }
234            }
235
236            if (w1 == null || w2 == null) {
237                timedMarkersOmitted = true;
238            } else {
239                wayPointFromTimeStamp = new WayPoint(w1.getCoor().interpolate(w2.getCoor(),
240                        (startTime - w1.time) / (w2.time - w1.time)));
241                wayPointFromTimeStamp.time = startTime;
242                String name = wavFile.getName();
243                int dot = name.lastIndexOf('.');
244                if (dot > 0) {
245                    name = name.substring(0, dot);
246                }
247                wayPointFromTimeStamp.attr.put("name", name);
248                waypoints.add(wayPointFromTimeStamp);
249            }
250        }
251
252        // (e) analyse audio for spoken markers here, in due course
253
254        // (f) simply add a single marker at the start of the track
255        if ((Main.pref.getBoolean("marker.audiofromstart") || waypoints.isEmpty()) && hasTracks) {
256            boolean gotOne = false;
257            for (GpxTrack track : layer.data.tracks) {
258                for (GpxTrackSegment seg : track.getSegments()) {
259                    for (WayPoint w : seg.getWayPoints()) {
260                        WayPoint wStart = new WayPoint(w.getCoor());
261                        wStart.attr.put("name", "start");
262                        wStart.time = w.time;
263                        waypoints.add(wStart);
264                        gotOne = true;
265                        break;
266                    }
267                    if (gotOne) {
268                        break;
269                    }
270                }
271                if (gotOne) {
272                    break;
273                }
274            }
275        }
276
277        /* we must have got at least one waypoint now */
278
279        Collections.sort((ArrayList<WayPoint>) waypoints, new Comparator<WayPoint>() {
280            @Override
281            public int compare(WayPoint a, WayPoint b) {
282                return a.time <= b.time ? -1 : 1;
283            }
284        });
285
286        firstTime = -1.0; /* this time of the first waypoint, not first trackpoint */
287        for (WayPoint w : waypoints) {
288            if (firstTime < 0.0) {
289                firstTime = w.time;
290            }
291            double offset = w.time - firstTime;
292            AudioMarker am = new AudioMarker(w.getCoor(), w, url, ml, w.time, offset);
293            /*
294             * timeFromAudio intended for future use to shift markers of this type on
295             * synchronization
296             */
297            if (w == wayPointFromTimeStamp) {
298                am.timeFromAudio = true;
299            }
300            ml.data.add(am);
301        }
302
303        if (timedMarkersOmitted && !markers.timedMarkersOmitted) {
304            JOptionPane
305            .showMessageDialog(
306                    Main.parent,
307                    tr("Some waypoints with timestamps from before the start of the track or after the end were omitted or moved to the start."));
308            markers.timedMarkersOmitted = timedMarkersOmitted;
309        }
310        if (untimedMarkersOmitted && !markers.untimedMarkersOmitted) {
311            JOptionPane
312            .showMessageDialog(
313                    Main.parent,
314                    tr("Some waypoints which were too far from the track to sensibly estimate their time were omitted."));
315            markers.untimedMarkersOmitted = untimedMarkersOmitted;
316        }
317    }
318}