1 | // License: GPL. For details, see LICENSE file.
|
---|
2 | package org.openstreetmap.josm.gui.layer.gpx;
|
---|
3 |
|
---|
4 | import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
|
---|
5 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
6 |
|
---|
7 | import java.awt.event.ActionEvent;
|
---|
8 | import java.io.File;
|
---|
9 | import java.net.MalformedURLException;
|
---|
10 | import java.net.URL;
|
---|
11 | import java.util.ArrayList;
|
---|
12 | import java.util.Arrays;
|
---|
13 | import java.util.Collection;
|
---|
14 | import java.util.Collections;
|
---|
15 | import java.util.Comparator;
|
---|
16 |
|
---|
17 | import javax.swing.AbstractAction;
|
---|
18 | import javax.swing.JFileChooser;
|
---|
19 | import javax.swing.JOptionPane;
|
---|
20 | import javax.swing.filechooser.FileFilter;
|
---|
21 |
|
---|
22 | import org.openstreetmap.josm.Main;
|
---|
23 | import org.openstreetmap.josm.actions.DiskAccessAction;
|
---|
24 | import org.openstreetmap.josm.data.gpx.GpxData;
|
---|
25 | import org.openstreetmap.josm.data.gpx.GpxTrack;
|
---|
26 | import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
|
---|
27 | import org.openstreetmap.josm.data.gpx.WayPoint;
|
---|
28 | import org.openstreetmap.josm.gui.HelpAwareOptionPane;
|
---|
29 | import org.openstreetmap.josm.gui.layer.GpxLayer;
|
---|
30 | import org.openstreetmap.josm.gui.layer.markerlayer.AudioMarker;
|
---|
31 | import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
|
---|
32 | import org.openstreetmap.josm.tools.AudioUtil;
|
---|
33 | import org.openstreetmap.josm.tools.ImageProvider;
|
---|
34 |
|
---|
35 | /**
|
---|
36 | * Import audio files into a GPX layer to enable audio playback functions.
|
---|
37 | * @since 5715
|
---|
38 | */
|
---|
39 | public class ImportAudioAction extends AbstractAction {
|
---|
40 | private final GpxLayer layer;
|
---|
41 |
|
---|
42 | private static class Markers {
|
---|
43 | public boolean timedMarkersOmitted = false;
|
---|
44 | public boolean untimedMarkersOmitted = false;
|
---|
45 | }
|
---|
46 |
|
---|
47 | /**
|
---|
48 | * Constructs a new {@code ImportAudioAction}.
|
---|
49 | * @param layer The associated GPX layer
|
---|
50 | */
|
---|
51 | public ImportAudioAction(final GpxLayer layer) {
|
---|
52 | super(tr("Import Audio"), ImageProvider.get("importaudio"));
|
---|
53 | this.layer = layer;
|
---|
54 | putValue("help", ht("/Action/ImportAudio"));
|
---|
55 | }
|
---|
56 |
|
---|
57 | private void warnCantImportIntoServerLayer(GpxLayer layer) {
|
---|
58 | 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());
|
---|
59 | HelpAwareOptionPane.showOptionDialog(Main.parent, msg, tr("Import not possible"), JOptionPane.WARNING_MESSAGE, ht("/Action/ImportAudio#CantImportIntoGpxLayerFromServer"));
|
---|
60 | }
|
---|
61 |
|
---|
62 | @Override
|
---|
63 | public void actionPerformed(ActionEvent e) {
|
---|
64 | if (layer.data.fromServer) {
|
---|
65 | warnCantImportIntoServerLayer(layer);
|
---|
66 | return;
|
---|
67 | }
|
---|
68 | FileFilter filter = new FileFilter() {
|
---|
69 | @Override
|
---|
70 | public boolean accept(File f) {
|
---|
71 | return f.isDirectory() || f.getName().toLowerCase().endsWith(".wav");
|
---|
72 | }
|
---|
73 |
|
---|
74 | @Override
|
---|
75 | public String getDescription() {
|
---|
76 | return tr("Wave Audio files (*.wav)");
|
---|
77 | }
|
---|
78 | };
|
---|
79 | JFileChooser fc = DiskAccessAction.createAndOpenFileChooser(true, true, null, filter, JFileChooser.FILES_ONLY, "markers.lastaudiodirectory");
|
---|
80 | if (fc != null) {
|
---|
81 | File[] sel = fc.getSelectedFiles();
|
---|
82 | // sort files in increasing order of timestamp (this is the end time, but so
|
---|
83 | // long as they don't overlap, that's fine)
|
---|
84 | if (sel.length > 1) {
|
---|
85 | Arrays.sort(sel, new Comparator<File>() {
|
---|
86 | @Override
|
---|
87 | public int compare(File a, File b) {
|
---|
88 | return a.lastModified() <= b.lastModified() ? -1 : 1;
|
---|
89 | }
|
---|
90 | });
|
---|
91 | }
|
---|
92 | String names = null;
|
---|
93 | for (int i = 0; i < sel.length; i++) {
|
---|
94 | if (names == null) {
|
---|
95 | names = " (";
|
---|
96 | } else {
|
---|
97 | names += ", ";
|
---|
98 | }
|
---|
99 | names += sel[i].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 (int i = 0; i < sel.length; i++) {
|
---|
110 | importAudio(sel[i], 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 | System.err.println("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 | }
|
---|