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

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

see #15229 - deprecate all Main methods related to projections. New ProjectionRegistry class

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