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.MalformedURLException;
010import java.net.URL;
011import java.util.ArrayList;
012import java.util.Arrays;
013import java.util.Collection;
014import java.util.Collections;
015import java.util.Comparator;
016
017import javax.swing.AbstractAction;
018import javax.swing.JFileChooser;
019import javax.swing.JOptionPane;
020import javax.swing.filechooser.FileFilter;
021
022import org.openstreetmap.josm.Main;
023import org.openstreetmap.josm.actions.DiskAccessAction;
024import org.openstreetmap.josm.data.gpx.GpxData;
025import org.openstreetmap.josm.data.gpx.GpxTrack;
026import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
027import org.openstreetmap.josm.data.gpx.WayPoint;
028import org.openstreetmap.josm.gui.HelpAwareOptionPane;
029import org.openstreetmap.josm.gui.layer.GpxLayer;
030import org.openstreetmap.josm.gui.layer.markerlayer.AudioMarker;
031import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
032import org.openstreetmap.josm.tools.AudioUtil;
033import org.openstreetmap.josm.tools.ImageProvider;
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 = null;
128        boolean hasTracks = layer.data.tracks != null && !layer.data.tracks.isEmpty();
129        boolean hasWaypoints = layer.data.waypoints != null && !layer.data.waypoints.isEmpty();
130        try {
131            url = wavFile.toURI().toURL();
132        } catch (MalformedURLException e) {
133            Main.error("Unable to convert filename " + wavFile.getAbsolutePath() + " to URL");
134        }
135        Collection<WayPoint> waypoints = new ArrayList<WayPoint>();
136        boolean timedMarkersOmitted = false;
137        boolean untimedMarkersOmitted = false;
138        double snapDistance = Main.pref.getDouble("marker.audiofromuntimedwaypoints.distance", 1.0e-3); /*
139         * about
140         * 25
141         * m
142         */
143        WayPoint wayPointFromTimeStamp = null;
144
145        // determine time of first point in track
146        double firstTime = -1.0;
147        if (hasTracks) {
148            for (GpxTrack track : layer.data.tracks) {
149                for (GpxTrackSegment seg : track.getSegments()) {
150                    for (WayPoint w : seg.getWayPoints()) {
151                        firstTime = w.time;
152                        break;
153                    }
154                    if (firstTime >= 0.0) {
155                        break;
156                    }
157                }
158                if (firstTime >= 0.0) {
159                    break;
160                }
161            }
162        }
163        if (firstTime < 0.0) {
164            JOptionPane.showMessageDialog(
165                    Main.parent,
166                    tr("No GPX track available in layer to associate audio with."),
167                    tr("Error"),
168                    JOptionPane.ERROR_MESSAGE
169                    );
170            return;
171        }
172
173        // (a) try explicit timestamped waypoints - unless suppressed
174        if (Main.pref.getBoolean("marker.audiofromexplicitwaypoints", true) && hasWaypoints) {
175            for (WayPoint w : layer.data.waypoints) {
176                if (w.time > firstTime) {
177                    waypoints.add(w);
178                } else if (w.time > 0.0) {
179                    timedMarkersOmitted = true;
180                }
181            }
182        }
183
184        // (b) try explicit waypoints without timestamps - unless suppressed
185        if (Main.pref.getBoolean("marker.audiofromuntimedwaypoints", true) && hasWaypoints) {
186            for (WayPoint w : layer.data.waypoints) {
187                if (waypoints.contains(w)) {
188                    continue;
189                }
190                WayPoint wNear = layer.data.nearestPointOnTrack(w.getEastNorth(), snapDistance);
191                if (wNear != null) {
192                    WayPoint wc = new WayPoint(w.getCoor());
193                    wc.time = wNear.time;
194                    if (w.attr.containsKey("name")) {
195                        wc.attr.put("name", w.getString("name"));
196                    }
197                    waypoints.add(wc);
198                } else {
199                    untimedMarkersOmitted = true;
200                }
201            }
202        }
203
204        // (c) use explicitly named track points, again unless suppressed
205        if ((Main.pref.getBoolean("marker.audiofromnamedtrackpoints", false)) && layer.data.tracks != null
206                && !layer.data.tracks.isEmpty()) {
207            for (GpxTrack track : layer.data.tracks) {
208                for (GpxTrackSegment seg : track.getSegments()) {
209                    for (WayPoint w : seg.getWayPoints()) {
210                        if (w.attr.containsKey("name") || w.attr.containsKey("desc")) {
211                            waypoints.add(w);
212                        }
213                    }
214                }
215            }
216        }
217
218        // (d) use timestamp of file as location on track
219        if ((Main.pref.getBoolean("marker.audiofromwavtimestamps", false)) && hasTracks) {
220            double lastModified = wavFile.lastModified() / 1000.0; // lastModified is in
221            // milliseconds
222            double duration = AudioUtil.getCalibratedDuration(wavFile);
223            double startTime = lastModified - duration;
224            startTime = firstStartTime + (startTime - firstStartTime)
225                    / Main.pref.getDouble("audio.calibration", 1.0 /* default, ratio */);
226            WayPoint w1 = null;
227            WayPoint w2 = null;
228
229            for (GpxTrack track : layer.data.tracks) {
230                for (GpxTrackSegment seg : track.getSegments()) {
231                    for (WayPoint w : seg.getWayPoints()) {
232                        if (startTime < w.time) {
233                            w2 = w;
234                            break;
235                        }
236                        w1 = w;
237                    }
238                    if (w2 != null) {
239                        break;
240                    }
241                }
242            }
243
244            if (w1 == null || w2 == null) {
245                timedMarkersOmitted = true;
246            } else {
247                wayPointFromTimeStamp = new WayPoint(w1.getCoor().interpolate(w2.getCoor(),
248                        (startTime - w1.time) / (w2.time - w1.time)));
249                wayPointFromTimeStamp.time = startTime;
250                String name = wavFile.getName();
251                int dot = name.lastIndexOf('.');
252                if (dot > 0) {
253                    name = name.substring(0, dot);
254                }
255                wayPointFromTimeStamp.attr.put("name", name);
256                waypoints.add(wayPointFromTimeStamp);
257            }
258        }
259
260        // (e) analyse audio for spoken markers here, in due course
261
262        // (f) simply add a single marker at the start of the track
263        if ((Main.pref.getBoolean("marker.audiofromstart") || waypoints.isEmpty()) && hasTracks) {
264            boolean gotOne = false;
265            for (GpxTrack track : layer.data.tracks) {
266                for (GpxTrackSegment seg : track.getSegments()) {
267                    for (WayPoint w : seg.getWayPoints()) {
268                        WayPoint wStart = new WayPoint(w.getCoor());
269                        wStart.attr.put("name", "start");
270                        wStart.time = w.time;
271                        waypoints.add(wStart);
272                        gotOne = true;
273                        break;
274                    }
275                    if (gotOne) {
276                        break;
277                    }
278                }
279                if (gotOne) {
280                    break;
281                }
282            }
283        }
284
285        /* we must have got at least one waypoint now */
286
287        Collections.sort((ArrayList<WayPoint>) waypoints, new Comparator<WayPoint>() {
288            @Override
289            public int compare(WayPoint a, WayPoint b) {
290                return a.time <= b.time ? -1 : 1;
291            }
292        });
293
294        firstTime = -1.0; /* this time of the first waypoint, not first trackpoint */
295        for (WayPoint w : waypoints) {
296            if (firstTime < 0.0) {
297                firstTime = w.time;
298            }
299            double offset = w.time - firstTime;
300            AudioMarker am = new AudioMarker(w.getCoor(), w, url, ml, w.time, offset);
301            /*
302             * timeFromAudio intended for future use to shift markers of this type on
303             * synchronization
304             */
305            if (w == wayPointFromTimeStamp) {
306                am.timeFromAudio = true;
307            }
308            ml.data.add(am);
309        }
310
311        if (timedMarkersOmitted && !markers.timedMarkersOmitted) {
312            JOptionPane
313            .showMessageDialog(
314                    Main.parent,
315                    tr("Some waypoints with timestamps from before the start of the track or after the end were omitted or moved to the start."));
316            markers.timedMarkersOmitted = timedMarkersOmitted;
317        }
318        if (untimedMarkersOmitted && !markers.untimedMarkersOmitted) {
319            JOptionPane
320            .showMessageDialog(
321                    Main.parent,
322                    tr("Some waypoints which were too far from the track to sensibly estimate their time were omitted."));
323            markers.untimedMarkersOmitted = untimedMarkersOmitted;
324        }
325    }
326}