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

Last change on this file since 13130 was 13130, checked in by Don-vip, 6 years ago

fix #15572 - use ImageProvider attach API for all JOSM actions to ensure proper icon size everywhere

  • Property svn:eol-style set to native
File size: 13.1 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.MainApplication;
29import org.openstreetmap.josm.gui.layer.GpxLayer;
30import org.openstreetmap.josm.gui.layer.markerlayer.AudioMarker;
31import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
32import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
33import org.openstreetmap.josm.io.audio.AudioUtil;
34import org.openstreetmap.josm.spi.preferences.Config;
35import org.openstreetmap.josm.tools.ImageProvider;
36import org.openstreetmap.josm.tools.Utils;
37
38/**
39 * Import audio files into a GPX layer to enable audio playback functions.
40 * @since 5715
41 */
42public class ImportAudioAction extends AbstractAction {
43 private final transient GpxLayer layer;
44
45 /**
46 * Audio file filter.
47 * @since 12328
48 */
49 public static final class AudioFileFilter extends FileFilter {
50 @Override
51 public boolean accept(File f) {
52 return f.isDirectory() || Utils.hasExtension(f, "wav", "mp3", "aac", "aif", "aiff");
53 }
54
55 @Override
56 public String getDescription() {
57 return tr("Audio files (*.wav, *.mp3, *.aac, *.aif, *.aiff)");
58 }
59 }
60
61 private static class Markers {
62 public boolean timedMarkersOmitted;
63 public boolean untimedMarkersOmitted;
64 }
65
66 /**
67 * Constructs a new {@code ImportAudioAction}.
68 * @param layer The associated GPX layer
69 */
70 public ImportAudioAction(final GpxLayer layer) {
71 super(tr("Import Audio"));
72 new ImageProvider("importaudio").getResource().attachImageIcon(this, true);
73 this.layer = layer;
74 putValue("help", ht("/Action/ImportAudio"));
75 }
76
77 private static void warnCantImportIntoServerLayer(GpxLayer layer) {
78 String msg = tr("<html>The data in the GPX layer ''{0}'' has been downloaded from the server.<br>" +
79 "Because its way points do not include a timestamp we cannot correlate them with audio data.</html>",
80 Utils.escapeReservedCharactersHTML(layer.getName()));
81 HelpAwareOptionPane.showOptionDialog(Main.parent, msg, tr("Import not possible"),
82 JOptionPane.WARNING_MESSAGE, ht("/Action/ImportAudio#CantImportIntoGpxLayerFromServer"));
83 }
84
85 @Override
86 public void actionPerformed(ActionEvent e) {
87 if (layer.data.fromServer) {
88 warnCantImportIntoServerLayer(layer);
89 return;
90 }
91 AbstractFileChooser fc = DiskAccessAction.createAndOpenFileChooser(true, true, null, new AudioFileFilter(),
92 JFileChooser.FILES_ONLY, "markers.lastaudiodirectory");
93 if (fc != null) {
94 File[] sel = fc.getSelectedFiles();
95 // sort files in increasing order of timestamp (this is the end time, but so
96 // long as they don't overlap, that's fine)
97 if (sel.length > 1) {
98 Arrays.sort(sel, Comparator.comparingLong(File::lastModified));
99 }
100 StringBuilder names = new StringBuilder();
101 for (File file : sel) {
102 if (names.length() == 0) {
103 names.append(" (");
104 } else {
105 names.append(", ");
106 }
107 names.append(file.getName());
108 }
109 if (names.length() > 0) {
110 names.append(')');
111 }
112 MarkerLayer ml = new MarkerLayer(new GpxData(),
113 tr("Audio markers from {0}", layer.getName()) + names, layer.getAssociatedFile(), layer);
114 double firstStartTime = sel[0].lastModified() / 1000.0 - AudioUtil.getCalibratedDuration(sel[0]);
115 Markers m = new Markers();
116 for (File file : sel) {
117 importAudio(file, ml, firstStartTime, m);
118 }
119 MainApplication.getLayerManager().addLayer(ml);
120 MainApplication.getMap().repaint();
121 }
122 }
123
124 /**
125 * Makes a new marker layer derived from this GpxLayer containing at least one audio marker
126 * which the given audio file is associated with. Markers are derived from the following (a)
127 * explict waypoints in the GPX layer, or (b) named trackpoints in the GPX layer, or (d)
128 * timestamp on the audio file (e) (in future) voice recognised markers in the sound recording (f)
129 * a single marker at the beginning of the track
130 * @param audioFile the file to be associated with the markers in the new marker layer
131 * @param ml marker layer
132 * @param firstStartTime first start time in milliseconds, used for (d)
133 * @param markers keeps track of warning messages to avoid repeated warnings
134 */
135 private void importAudio(File audioFile, MarkerLayer ml, double firstStartTime, Markers markers) {
136 URL url = Utils.fileToURL(audioFile);
137 boolean hasTracks = layer.data.tracks != null && !layer.data.tracks.isEmpty();
138 boolean hasWaypoints = layer.data.waypoints != null && !layer.data.waypoints.isEmpty();
139 Collection<WayPoint> waypoints = new ArrayList<>();
140 boolean timedMarkersOmitted = false;
141 boolean untimedMarkersOmitted = false;
142 double snapDistance = Config.getPref().getDouble("marker.audiofromuntimedwaypoints.distance", 1.0e-3);
143 // about 25 m
144 WayPoint wayPointFromTimeStamp = null;
145
146 // determine time of first point in track
147 double firstTime = -1.0;
148 if (hasTracks) {
149 for (GpxTrack track : layer.data.tracks) {
150 for (GpxTrackSegment seg : track.getSegments()) {
151 for (WayPoint w : seg.getWayPoints()) {
152 firstTime = w.time;
153 break;
154 }
155 if (firstTime >= 0.0) {
156 break;
157 }
158 }
159 if (firstTime >= 0.0) {
160 break;
161 }
162 }
163 }
164 if (firstTime < 0.0) {
165 JOptionPane.showMessageDialog(
166 Main.parent,
167 tr("No GPX track available in layer to associate audio with."),
168 tr("Error"),
169 JOptionPane.ERROR_MESSAGE
170 );
171 return;
172 }
173
174 // (a) try explicit timestamped waypoints - unless suppressed
175 if (hasWaypoints && Config.getPref().getBoolean("marker.audiofromexplicitwaypoints", true)) {
176 for (WayPoint w : layer.data.waypoints) {
177 if (w.time > firstTime) {
178 waypoints.add(w);
179 } else if (w.time > 0.0) {
180 timedMarkersOmitted = true;
181 }
182 }
183 }
184
185 // (b) try explicit waypoints without timestamps - unless suppressed
186 if (hasWaypoints && Config.getPref().getBoolean("marker.audiofromuntimedwaypoints", true)) {
187 for (WayPoint w : layer.data.waypoints) {
188 if (waypoints.contains(w)) {
189 continue;
190 }
191 WayPoint wNear = layer.data.nearestPointOnTrack(w.getEastNorth(Main.getProjection()), snapDistance);
192 if (wNear != null) {
193 WayPoint wc = new WayPoint(w.getCoor());
194 wc.time = wNear.time;
195 if (w.attr.containsKey(GpxConstants.GPX_NAME)) {
196 wc.put(GpxConstants.GPX_NAME, w.getString(GpxConstants.GPX_NAME));
197 }
198 waypoints.add(wc);
199 } else {
200 untimedMarkersOmitted = true;
201 }
202 }
203 }
204
205 // (c) use explicitly named track points, again unless suppressed
206 if (layer.data.tracks != null && Config.getPref().getBoolean("marker.audiofromnamedtrackpoints", false)
207 && !layer.data.tracks.isEmpty()) {
208 for (GpxTrack track : layer.data.tracks) {
209 for (GpxTrackSegment seg : track.getSegments()) {
210 for (WayPoint w : seg.getWayPoints()) {
211 if (w.attr.containsKey(GpxConstants.GPX_NAME) || w.attr.containsKey(GpxConstants.GPX_DESC)) {
212 waypoints.add(w);
213 }
214 }
215 }
216 }
217 }
218
219 // (d) use timestamp of file as location on track
220 if (hasTracks && Config.getPref().getBoolean("marker.audiofromwavtimestamps", false)) {
221 double lastModified = audioFile.lastModified() / 1000.0; // lastModified is in milliseconds
222 double duration = AudioUtil.getCalibratedDuration(audioFile);
223 double startTime = lastModified - duration;
224 startTime = firstStartTime + (startTime - firstStartTime)
225 / Config.getPref().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 = audioFile.getName();
251 int dot = name.lastIndexOf('.');
252 if (dot > 0) {
253 name = name.substring(0, dot);
254 }
255 wayPointFromTimeStamp.put(GpxConstants.GPX_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 ((Config.getPref().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.put(GpxConstants.GPX_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 ((ArrayList<WayPoint>) waypoints).sort(Comparator.comparingDouble(o -> o.time));
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 // timeFromAudio intended for future use to shift markers of this type on synchronization
296 if (w == wayPointFromTimeStamp) {
297 am.timeFromAudio = true;
298 }
299 ml.data.add(am);
300 }
301
302 if (timedMarkersOmitted && !markers.timedMarkersOmitted) {
303 JOptionPane.showMessageDialog(Main.parent,
304 tr("Some waypoints with timestamps from before the start of the track or after the end were omitted or moved to the start."));
305 markers.timedMarkersOmitted = timedMarkersOmitted;
306 }
307 if (untimedMarkersOmitted && !markers.untimedMarkersOmitted) {
308 JOptionPane.showMessageDialog(Main.parent,
309 tr("Some waypoints which were too far from the track to sensibly estimate their time were omitted."));
310 markers.untimedMarkersOmitted = untimedMarkersOmitted;
311 }
312 }
313}
Note: See TracBrowser for help on using the repository browser.