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.URL;
|
---|
10 | import java.util.ArrayList;
|
---|
11 | import java.util.Arrays;
|
---|
12 | import java.util.Collection;
|
---|
13 | import java.util.Comparator;
|
---|
14 |
|
---|
15 | import javax.swing.AbstractAction;
|
---|
16 | import javax.swing.JFileChooser;
|
---|
17 | import javax.swing.JOptionPane;
|
---|
18 | import javax.swing.filechooser.FileFilter;
|
---|
19 |
|
---|
20 | import org.openstreetmap.josm.Main;
|
---|
21 | import org.openstreetmap.josm.actions.DiskAccessAction;
|
---|
22 | import org.openstreetmap.josm.data.gpx.GpxConstants;
|
---|
23 | import org.openstreetmap.josm.data.gpx.GpxData;
|
---|
24 | import org.openstreetmap.josm.data.gpx.GpxTrack;
|
---|
25 | import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
|
---|
26 | import org.openstreetmap.josm.data.gpx.WayPoint;
|
---|
27 | import org.openstreetmap.josm.gui.HelpAwareOptionPane;
|
---|
28 | import org.openstreetmap.josm.gui.layer.GpxLayer;
|
---|
29 | import org.openstreetmap.josm.gui.layer.markerlayer.AudioMarker;
|
---|
30 | import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
|
---|
31 | import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
|
---|
32 | import org.openstreetmap.josm.tools.AudioUtil;
|
---|
33 | import org.openstreetmap.josm.tools.ImageProvider;
|
---|
34 | import org.openstreetmap.josm.tools.Utils;
|
---|
35 |
|
---|
36 | /**
|
---|
37 | * Import audio files into a GPX layer to enable audio playback functions.
|
---|
38 | * @since 5715
|
---|
39 | */
|
---|
40 | public class ImportAudioAction extends AbstractAction {
|
---|
41 | private final transient GpxLayer layer;
|
---|
42 |
|
---|
43 | private static class Markers {
|
---|
44 | public boolean timedMarkersOmitted;
|
---|
45 | public boolean untimedMarkersOmitted;
|
---|
46 | }
|
---|
47 |
|
---|
48 | /**
|
---|
49 | * Constructs a new {@code ImportAudioAction}.
|
---|
50 | * @param layer The associated GPX layer
|
---|
51 | */
|
---|
52 | public ImportAudioAction(final GpxLayer layer) {
|
---|
53 | super(tr("Import Audio"), ImageProvider.get("importaudio"));
|
---|
54 | this.layer = layer;
|
---|
55 | putValue("help", ht("/Action/ImportAudio"));
|
---|
56 | }
|
---|
57 |
|
---|
58 | private static void warnCantImportIntoServerLayer(GpxLayer layer) {
|
---|
59 | String msg = tr("<html>The data in the GPX layer ''{0}'' has been downloaded from the server.<br>" +
|
---|
60 | "Because its way points do not include a timestamp we cannot correlate them with audio data.</html>",
|
---|
61 | layer.getName());
|
---|
62 | HelpAwareOptionPane.showOptionDialog(Main.parent, msg, tr("Import not possible"),
|
---|
63 | JOptionPane.WARNING_MESSAGE, ht("/Action/ImportAudio#CantImportIntoGpxLayerFromServer"));
|
---|
64 | }
|
---|
65 |
|
---|
66 | @Override
|
---|
67 | public void actionPerformed(ActionEvent e) {
|
---|
68 | if (layer.data.fromServer) {
|
---|
69 | warnCantImportIntoServerLayer(layer);
|
---|
70 | return;
|
---|
71 | }
|
---|
72 | FileFilter filter = new FileFilter() {
|
---|
73 | @Override
|
---|
74 | public boolean accept(File f) {
|
---|
75 | return f.isDirectory() || Utils.hasExtension(f, "wav");
|
---|
76 | }
|
---|
77 |
|
---|
78 | @Override
|
---|
79 | public String getDescription() {
|
---|
80 | return tr("Wave Audio files (*.wav)");
|
---|
81 | }
|
---|
82 | };
|
---|
83 | AbstractFileChooser fc = DiskAccessAction.createAndOpenFileChooser(true, true, null, filter,
|
---|
84 | JFileChooser.FILES_ONLY, "markers.lastaudiodirectory");
|
---|
85 | if (fc != null) {
|
---|
86 | File[] sel = fc.getSelectedFiles();
|
---|
87 | // sort files in increasing order of timestamp (this is the end time, but so
|
---|
88 | // long as they don't overlap, that's fine)
|
---|
89 | if (sel.length > 1) {
|
---|
90 | Arrays.sort(sel, Comparator.comparingLong(File::lastModified));
|
---|
91 | }
|
---|
92 | StringBuilder names = new StringBuilder();
|
---|
93 | for (File file : sel) {
|
---|
94 | if (names.length() == 0) {
|
---|
95 | names.append(" (");
|
---|
96 | } else {
|
---|
97 | names.append(", ");
|
---|
98 | }
|
---|
99 | names.append(file.getName());
|
---|
100 | }
|
---|
101 | if (names.length() > 0) {
|
---|
102 | names.append(')');
|
---|
103 | }
|
---|
104 | MarkerLayer ml = new MarkerLayer(new GpxData(),
|
---|
105 | tr("Audio markers from {0}", layer.getName()) + names, layer.getAssociatedFile(), layer);
|
---|
106 | double firstStartTime = sel[0].lastModified() / 1000.0 - AudioUtil.getCalibratedDuration(sel[0]);
|
---|
107 | Markers m = new Markers();
|
---|
108 | for (File file : sel) {
|
---|
109 | importAudio(file, ml, firstStartTime, m);
|
---|
110 | }
|
---|
111 | Main.getLayerManager().addLayer(ml);
|
---|
112 | Main.map.repaint();
|
---|
113 | }
|
---|
114 | }
|
---|
115 |
|
---|
116 | /**
|
---|
117 | * Makes a new marker layer derived from this GpxLayer containing at least one audio marker
|
---|
118 | * which the given audio file is associated with. Markers are derived from the following (a)
|
---|
119 | * explict waypoints in the GPX layer, or (b) named trackpoints in the GPX layer, or (d)
|
---|
120 | * timestamp on the wav file (e) (in future) voice recognised markers in the sound recording (f)
|
---|
121 | * a single marker at the beginning of the track
|
---|
122 | * @param wavFile the file to be associated with the markers in the new marker layer
|
---|
123 | * @param ml marker layer
|
---|
124 | * @param firstStartTime first start time in milliseconds, used for (d)
|
---|
125 | * @param markers keeps track of warning messages to avoid repeated warnings
|
---|
126 | */
|
---|
127 | private void importAudio(File wavFile, MarkerLayer ml, double firstStartTime, Markers markers) {
|
---|
128 | URL url = Utils.fileToURL(wavFile);
|
---|
129 | boolean hasTracks = layer.data.tracks != null && !layer.data.tracks.isEmpty();
|
---|
130 | boolean hasWaypoints = layer.data.waypoints != null && !layer.data.waypoints.isEmpty();
|
---|
131 | Collection<WayPoint> waypoints = new ArrayList<>();
|
---|
132 | boolean timedMarkersOmitted = false;
|
---|
133 | boolean untimedMarkersOmitted = false;
|
---|
134 | double snapDistance = Main.pref.getDouble("marker.audiofromuntimedwaypoints.distance", 1.0e-3);
|
---|
135 | // about 25 m
|
---|
136 | WayPoint wayPointFromTimeStamp = null;
|
---|
137 |
|
---|
138 | // determine time of first point in track
|
---|
139 | double firstTime = -1.0;
|
---|
140 | if (hasTracks) {
|
---|
141 | for (GpxTrack track : layer.data.tracks) {
|
---|
142 | for (GpxTrackSegment seg : track.getSegments()) {
|
---|
143 | for (WayPoint w : seg.getWayPoints()) {
|
---|
144 | firstTime = w.time;
|
---|
145 | break;
|
---|
146 | }
|
---|
147 | if (firstTime >= 0.0) {
|
---|
148 | break;
|
---|
149 | }
|
---|
150 | }
|
---|
151 | if (firstTime >= 0.0) {
|
---|
152 | break;
|
---|
153 | }
|
---|
154 | }
|
---|
155 | }
|
---|
156 | if (firstTime < 0.0) {
|
---|
157 | JOptionPane.showMessageDialog(
|
---|
158 | Main.parent,
|
---|
159 | tr("No GPX track available in layer to associate audio with."),
|
---|
160 | tr("Error"),
|
---|
161 | JOptionPane.ERROR_MESSAGE
|
---|
162 | );
|
---|
163 | return;
|
---|
164 | }
|
---|
165 |
|
---|
166 | // (a) try explicit timestamped waypoints - unless suppressed
|
---|
167 | if (Main.pref.getBoolean("marker.audiofromexplicitwaypoints", true) && hasWaypoints) {
|
---|
168 | for (WayPoint w : layer.data.waypoints) {
|
---|
169 | if (w.time > firstTime) {
|
---|
170 | waypoints.add(w);
|
---|
171 | } else if (w.time > 0.0) {
|
---|
172 | timedMarkersOmitted = true;
|
---|
173 | }
|
---|
174 | }
|
---|
175 | }
|
---|
176 |
|
---|
177 | // (b) try explicit waypoints without timestamps - unless suppressed
|
---|
178 | if (Main.pref.getBoolean("marker.audiofromuntimedwaypoints", true) && hasWaypoints) {
|
---|
179 | for (WayPoint w : layer.data.waypoints) {
|
---|
180 | if (waypoints.contains(w)) {
|
---|
181 | continue;
|
---|
182 | }
|
---|
183 | WayPoint wNear = layer.data.nearestPointOnTrack(w.getEastNorth(), snapDistance);
|
---|
184 | if (wNear != null) {
|
---|
185 | WayPoint wc = new WayPoint(w.getCoor());
|
---|
186 | wc.time = wNear.time;
|
---|
187 | if (w.attr.containsKey(GpxConstants.GPX_NAME)) {
|
---|
188 | wc.put(GpxConstants.GPX_NAME, w.getString(GpxConstants.GPX_NAME));
|
---|
189 | }
|
---|
190 | waypoints.add(wc);
|
---|
191 | } else {
|
---|
192 | untimedMarkersOmitted = true;
|
---|
193 | }
|
---|
194 | }
|
---|
195 | }
|
---|
196 |
|
---|
197 | // (c) use explicitly named track points, again unless suppressed
|
---|
198 | if ((Main.pref.getBoolean("marker.audiofromnamedtrackpoints", false)) && layer.data.tracks != null
|
---|
199 | && !layer.data.tracks.isEmpty()) {
|
---|
200 | for (GpxTrack track : layer.data.tracks) {
|
---|
201 | for (GpxTrackSegment seg : track.getSegments()) {
|
---|
202 | for (WayPoint w : seg.getWayPoints()) {
|
---|
203 | if (w.attr.containsKey(GpxConstants.GPX_NAME) || w.attr.containsKey(GpxConstants.GPX_DESC)) {
|
---|
204 | waypoints.add(w);
|
---|
205 | }
|
---|
206 | }
|
---|
207 | }
|
---|
208 | }
|
---|
209 | }
|
---|
210 |
|
---|
211 | // (d) use timestamp of file as location on track
|
---|
212 | if ((Main.pref.getBoolean("marker.audiofromwavtimestamps", false)) && hasTracks) {
|
---|
213 | double lastModified = wavFile.lastModified() / 1000.0; // lastModified is in
|
---|
214 | // milliseconds
|
---|
215 | double duration = AudioUtil.getCalibratedDuration(wavFile);
|
---|
216 | double startTime = lastModified - duration;
|
---|
217 | startTime = firstStartTime + (startTime - firstStartTime)
|
---|
218 | / Main.pref.getDouble("audio.calibration", 1.0 /* default, ratio */);
|
---|
219 | WayPoint w1 = null;
|
---|
220 | WayPoint w2 = null;
|
---|
221 |
|
---|
222 | for (GpxTrack track : layer.data.tracks) {
|
---|
223 | for (GpxTrackSegment seg : track.getSegments()) {
|
---|
224 | for (WayPoint w : seg.getWayPoints()) {
|
---|
225 | if (startTime < w.time) {
|
---|
226 | w2 = w;
|
---|
227 | break;
|
---|
228 | }
|
---|
229 | w1 = w;
|
---|
230 | }
|
---|
231 | if (w2 != null) {
|
---|
232 | break;
|
---|
233 | }
|
---|
234 | }
|
---|
235 | }
|
---|
236 |
|
---|
237 | if (w1 == null || w2 == null) {
|
---|
238 | timedMarkersOmitted = true;
|
---|
239 | } else {
|
---|
240 | wayPointFromTimeStamp = new WayPoint(w1.getCoor().interpolate(w2.getCoor(),
|
---|
241 | (startTime - w1.time) / (w2.time - w1.time)));
|
---|
242 | wayPointFromTimeStamp.time = startTime;
|
---|
243 | String name = wavFile.getName();
|
---|
244 | int dot = name.lastIndexOf('.');
|
---|
245 | if (dot > 0) {
|
---|
246 | name = name.substring(0, dot);
|
---|
247 | }
|
---|
248 | wayPointFromTimeStamp.put(GpxConstants.GPX_NAME, name);
|
---|
249 | waypoints.add(wayPointFromTimeStamp);
|
---|
250 | }
|
---|
251 | }
|
---|
252 |
|
---|
253 | // (e) analyse audio for spoken markers here, in due course
|
---|
254 |
|
---|
255 | // (f) simply add a single marker at the start of the track
|
---|
256 | if ((Main.pref.getBoolean("marker.audiofromstart") || waypoints.isEmpty()) && hasTracks) {
|
---|
257 | boolean gotOne = false;
|
---|
258 | for (GpxTrack track : layer.data.tracks) {
|
---|
259 | for (GpxTrackSegment seg : track.getSegments()) {
|
---|
260 | for (WayPoint w : seg.getWayPoints()) {
|
---|
261 | WayPoint wStart = new WayPoint(w.getCoor());
|
---|
262 | wStart.put(GpxConstants.GPX_NAME, "start");
|
---|
263 | wStart.time = w.time;
|
---|
264 | waypoints.add(wStart);
|
---|
265 | gotOne = true;
|
---|
266 | break;
|
---|
267 | }
|
---|
268 | if (gotOne) {
|
---|
269 | break;
|
---|
270 | }
|
---|
271 | }
|
---|
272 | if (gotOne) {
|
---|
273 | break;
|
---|
274 | }
|
---|
275 | }
|
---|
276 | }
|
---|
277 |
|
---|
278 | /* we must have got at least one waypoint now */
|
---|
279 |
|
---|
280 | ((ArrayList<WayPoint>) waypoints).sort(Comparator.comparingDouble(o -> o.time));
|
---|
281 |
|
---|
282 | firstTime = -1.0; /* this time of the first waypoint, not first trackpoint */
|
---|
283 | for (WayPoint w : waypoints) {
|
---|
284 | if (firstTime < 0.0) {
|
---|
285 | firstTime = w.time;
|
---|
286 | }
|
---|
287 | double offset = w.time - firstTime;
|
---|
288 | AudioMarker am = new AudioMarker(w.getCoor(), w, url, ml, w.time, offset);
|
---|
289 | /*
|
---|
290 | * timeFromAudio intended for future use to shift markers of this type on synchronization
|
---|
291 | */
|
---|
292 | if (w == wayPointFromTimeStamp) {
|
---|
293 | am.timeFromAudio = true;
|
---|
294 | }
|
---|
295 | ml.data.add(am);
|
---|
296 | }
|
---|
297 |
|
---|
298 | if (timedMarkersOmitted && !markers.timedMarkersOmitted) {
|
---|
299 | JOptionPane
|
---|
300 | .showMessageDialog(
|
---|
301 | Main.parent,
|
---|
302 | // CHECKSTYLE.OFF: LineLength
|
---|
303 | tr("Some waypoints with timestamps from before the start of the track or after the end were omitted or moved to the start."));
|
---|
304 | // CHECKSTYLE.ON: LineLength
|
---|
305 | markers.timedMarkersOmitted = timedMarkersOmitted;
|
---|
306 | }
|
---|
307 | if (untimedMarkersOmitted && !markers.untimedMarkersOmitted) {
|
---|
308 | JOptionPane
|
---|
309 | .showMessageDialog(
|
---|
310 | Main.parent,
|
---|
311 | tr("Some waypoints which were too far from the track to sensibly estimate their time were omitted."));
|
---|
312 | markers.untimedMarkersOmitted = untimedMarkersOmitted;
|
---|
313 | }
|
---|
314 | }
|
---|
315 | }
|
---|