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