001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer.gpx; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006 007import java.awt.event.ActionEvent; 008import java.io.File; 009import java.net.MalformedURLException; 010import java.net.URL; 011import java.util.ArrayList; 012import java.util.Arrays; 013import java.util.Collection; 014import java.util.Collections; 015import java.util.Comparator; 016 017import javax.swing.AbstractAction; 018import javax.swing.JFileChooser; 019import javax.swing.JOptionPane; 020import javax.swing.filechooser.FileFilter; 021 022import org.openstreetmap.josm.Main; 023import org.openstreetmap.josm.actions.DiskAccessAction; 024import org.openstreetmap.josm.data.gpx.GpxData; 025import org.openstreetmap.josm.data.gpx.GpxTrack; 026import org.openstreetmap.josm.data.gpx.GpxTrackSegment; 027import org.openstreetmap.josm.data.gpx.WayPoint; 028import org.openstreetmap.josm.gui.HelpAwareOptionPane; 029import org.openstreetmap.josm.gui.layer.GpxLayer; 030import org.openstreetmap.josm.gui.layer.markerlayer.AudioMarker; 031import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer; 032import org.openstreetmap.josm.tools.AudioUtil; 033import org.openstreetmap.josm.tools.ImageProvider; 034 035/** 036 * Import audio files into a GPX layer to enable audio playback functions. 037 * @since 5715 038 */ 039public class ImportAudioAction extends AbstractAction { 040 private final GpxLayer layer; 041 042 private static class Markers { 043 public boolean timedMarkersOmitted = false; 044 public boolean untimedMarkersOmitted = false; 045 } 046 047 /** 048 * Constructs a new {@code ImportAudioAction}. 049 * @param layer The associated GPX layer 050 */ 051 public ImportAudioAction(final GpxLayer layer) { 052 super(tr("Import Audio"), ImageProvider.get("importaudio")); 053 this.layer = layer; 054 putValue("help", ht("/Action/ImportAudio")); 055 } 056 057 private void warnCantImportIntoServerLayer(GpxLayer layer) { 058 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()); 059 HelpAwareOptionPane.showOptionDialog(Main.parent, msg, tr("Import not possible"), JOptionPane.WARNING_MESSAGE, ht("/Action/ImportAudio#CantImportIntoGpxLayerFromServer")); 060 } 061 062 @Override 063 public void actionPerformed(ActionEvent e) { 064 if (layer.data.fromServer) { 065 warnCantImportIntoServerLayer(layer); 066 return; 067 } 068 FileFilter filter = new FileFilter() { 069 @Override 070 public boolean accept(File f) { 071 return f.isDirectory() || f.getName().toLowerCase().endsWith(".wav"); 072 } 073 074 @Override 075 public String getDescription() { 076 return tr("Wave Audio files (*.wav)"); 077 } 078 }; 079 JFileChooser fc = DiskAccessAction.createAndOpenFileChooser(true, true, null, filter, JFileChooser.FILES_ONLY, "markers.lastaudiodirectory"); 080 if (fc != null) { 081 File[] sel = fc.getSelectedFiles(); 082 // sort files in increasing order of timestamp (this is the end time, but so 083 // long as they don't overlap, that's fine) 084 if (sel.length > 1) { 085 Arrays.sort(sel, new Comparator<File>() { 086 @Override 087 public int compare(File a, File b) { 088 return a.lastModified() <= b.lastModified() ? -1 : 1; 089 } 090 }); 091 } 092 String names = null; 093 for (File file : sel) { 094 if (names == null) { 095 names = " ("; 096 } else { 097 names += ", "; 098 } 099 names += file.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 (File file : sel) { 110 importAudio(file, 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 Main.error("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}