source: josm/trunk/src/org/openstreetmap/josm/gui/layer/gpx/ImportAudioAction.java@ 12377

Last change on this file since 12377 was 12328, checked in by Don-vip, 7 years ago

fix #2089 - Add support for MP3, AIFF and AAC audio codecs (.mp3, .aac, .aif, .aiff files) if Java FX is on the classpath (i.e. Windows, macOS, nearly all major Linux distributions). The classes are not public on purpose, as the whole system will have to be simplified when all Linux distributions propose Java FX and so we can get rid of old Java Sound implementation.

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