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.URL; 010import java.util.ArrayList; 011import java.util.Arrays; 012import java.util.Collection; 013import java.util.Collections; 014import java.util.Comparator; 015 016import javax.swing.AbstractAction; 017import javax.swing.JFileChooser; 018import javax.swing.JOptionPane; 019import javax.swing.filechooser.FileFilter; 020 021import org.openstreetmap.josm.Main; 022import org.openstreetmap.josm.actions.DiskAccessAction; 023import org.openstreetmap.josm.data.gpx.GpxConstants; 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.gui.widgets.AbstractFileChooser; 033import org.openstreetmap.josm.tools.AudioUtil; 034import org.openstreetmap.josm.tools.ImageProvider; 035import org.openstreetmap.josm.tools.Utils; 036 037/** 038 * Import audio files into a GPX layer to enable audio playback functions. 039 * @since 5715 040 */ 041public class ImportAudioAction extends AbstractAction { 042 private final GpxLayer layer; 043 044 private static class Markers { 045 public boolean timedMarkersOmitted = false; 046 public boolean untimedMarkersOmitted = false; 047 } 048 049 /** 050 * Constructs a new {@code ImportAudioAction}. 051 * @param layer The associated GPX layer 052 */ 053 public ImportAudioAction(final GpxLayer layer) { 054 super(tr("Import Audio"), ImageProvider.get("importaudio")); 055 this.layer = layer; 056 putValue("help", ht("/Action/ImportAudio")); 057 } 058 059 private void warnCantImportIntoServerLayer(GpxLayer layer) { 060 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()); 061 HelpAwareOptionPane.showOptionDialog(Main.parent, msg, tr("Import not possible"), JOptionPane.WARNING_MESSAGE, ht("/Action/ImportAudio#CantImportIntoGpxLayerFromServer")); 062 } 063 064 @Override 065 public void actionPerformed(ActionEvent e) { 066 if (layer.data.fromServer) { 067 warnCantImportIntoServerLayer(layer); 068 return; 069 } 070 FileFilter filter = new FileFilter() { 071 @Override 072 public boolean accept(File f) { 073 return f.isDirectory() || f.getName().toLowerCase().endsWith(".wav"); 074 } 075 076 @Override 077 public String getDescription() { 078 return tr("Wave Audio files (*.wav)"); 079 } 080 }; 081 AbstractFileChooser fc = DiskAccessAction.createAndOpenFileChooser(true, true, null, filter, JFileChooser.FILES_ONLY, "markers.lastaudiodirectory"); 082 if (fc != null) { 083 File[] sel = fc.getSelectedFiles(); 084 // sort files in increasing order of timestamp (this is the end time, but so 085 // long as they don't overlap, that's fine) 086 if (sel.length > 1) { 087 Arrays.sort(sel, new Comparator<File>() { 088 @Override 089 public int compare(File a, File b) { 090 return a.lastModified() <= b.lastModified() ? -1 : 1; 091 } 092 }); 093 } 094 String names = null; 095 for (File file : sel) { 096 if (names == null) { 097 names = " ("; 098 } else { 099 names += ", "; 100 } 101 names += file.getName(); 102 } 103 if (names != null) { 104 names += ")"; 105 } else { 106 names = ""; 107 } 108 MarkerLayer ml = new MarkerLayer(new GpxData(), tr("Audio markers from {0}", layer.getName()) + names, layer.getAssociatedFile(), layer); 109 double firstStartTime = sel[0].lastModified() / 1000.0 - AudioUtil.getCalibratedDuration(sel[0]); 110 Markers m = new Markers(); 111 for (File file : sel) { 112 importAudio(file, ml, firstStartTime, m); 113 } 114 Main.main.addLayer(ml); 115 Main.map.repaint(); 116 } 117 } 118 119 /** 120 * Makes a new marker layer derived from this GpxLayer containing at least one audio marker 121 * which the given audio file is associated with. Markers are derived from the following (a) 122 * explict waypoints in the GPX layer, or (b) named trackpoints in the GPX layer, or (d) 123 * timestamp on the wav file (e) (in future) voice recognised markers in the sound recording (f) 124 * a single marker at the beginning of the track 125 * @param wavFile : the file to be associated with the markers in the new marker layer 126 * @param markers : keeps track of warning messages to avoid repeated warnings 127 */ 128 private void importAudio(File wavFile, MarkerLayer ml, double firstStartTime, Markers markers) { 129 URL url = Utils.fileToURL(wavFile); 130 boolean hasTracks = layer.data.tracks != null && !layer.data.tracks.isEmpty(); 131 boolean hasWaypoints = layer.data.waypoints != null && !layer.data.waypoints.isEmpty(); 132 Collection<WayPoint> waypoints = new ArrayList<>(); 133 boolean timedMarkersOmitted = false; 134 boolean untimedMarkersOmitted = false; 135 double snapDistance = Main.pref.getDouble("marker.audiofromuntimedwaypoints.distance", 1.0e-3); 136 // about 25 m 137 WayPoint wayPointFromTimeStamp = null; 138 139 // determine time of first point in track 140 double firstTime = -1.0; 141 if (hasTracks) { 142 for (GpxTrack track : layer.data.tracks) { 143 for (GpxTrackSegment seg : track.getSegments()) { 144 for (WayPoint w : seg.getWayPoints()) { 145 firstTime = w.time; 146 break; 147 } 148 if (firstTime >= 0.0) { 149 break; 150 } 151 } 152 if (firstTime >= 0.0) { 153 break; 154 } 155 } 156 } 157 if (firstTime < 0.0) { 158 JOptionPane.showMessageDialog( 159 Main.parent, 160 tr("No GPX track available in layer to associate audio with."), 161 tr("Error"), 162 JOptionPane.ERROR_MESSAGE 163 ); 164 return; 165 } 166 167 // (a) try explicit timestamped waypoints - unless suppressed 168 if (Main.pref.getBoolean("marker.audiofromexplicitwaypoints", true) && hasWaypoints) { 169 for (WayPoint w : layer.data.waypoints) { 170 if (w.time > firstTime) { 171 waypoints.add(w); 172 } else if (w.time > 0.0) { 173 timedMarkersOmitted = true; 174 } 175 } 176 } 177 178 // (b) try explicit waypoints without timestamps - unless suppressed 179 if (Main.pref.getBoolean("marker.audiofromuntimedwaypoints", true) && hasWaypoints) { 180 for (WayPoint w : layer.data.waypoints) { 181 if (waypoints.contains(w)) { 182 continue; 183 } 184 WayPoint wNear = layer.data.nearestPointOnTrack(w.getEastNorth(), snapDistance); 185 if (wNear != null) { 186 WayPoint wc = new WayPoint(w.getCoor()); 187 wc.time = wNear.time; 188 if (w.attr.containsKey(GpxConstants.GPX_NAME)) { 189 wc.put(GpxConstants.GPX_NAME, w.getString(GpxConstants.GPX_NAME)); 190 } 191 waypoints.add(wc); 192 } else { 193 untimedMarkersOmitted = true; 194 } 195 } 196 } 197 198 // (c) use explicitly named track points, again unless suppressed 199 if ((Main.pref.getBoolean("marker.audiofromnamedtrackpoints", false)) && layer.data.tracks != null 200 && !layer.data.tracks.isEmpty()) { 201 for (GpxTrack track : layer.data.tracks) { 202 for (GpxTrackSegment seg : track.getSegments()) { 203 for (WayPoint w : seg.getWayPoints()) { 204 if (w.attr.containsKey(GpxConstants.GPX_NAME) || w.attr.containsKey(GpxConstants.GPX_DESC)) { 205 waypoints.add(w); 206 } 207 } 208 } 209 } 210 } 211 212 // (d) use timestamp of file as location on track 213 if ((Main.pref.getBoolean("marker.audiofromwavtimestamps", false)) && hasTracks) { 214 double lastModified = wavFile.lastModified() / 1000.0; // lastModified is in 215 // milliseconds 216 double duration = AudioUtil.getCalibratedDuration(wavFile); 217 double startTime = lastModified - duration; 218 startTime = firstStartTime + (startTime - firstStartTime) 219 / Main.pref.getDouble("audio.calibration", 1.0 /* default, ratio */); 220 WayPoint w1 = null; 221 WayPoint w2 = null; 222 223 for (GpxTrack track : layer.data.tracks) { 224 for (GpxTrackSegment seg : track.getSegments()) { 225 for (WayPoint w : seg.getWayPoints()) { 226 if (startTime < w.time) { 227 w2 = w; 228 break; 229 } 230 w1 = w; 231 } 232 if (w2 != null) { 233 break; 234 } 235 } 236 } 237 238 if (w1 == null || w2 == null) { 239 timedMarkersOmitted = true; 240 } else { 241 wayPointFromTimeStamp = new WayPoint(w1.getCoor().interpolate(w2.getCoor(), 242 (startTime - w1.time) / (w2.time - w1.time))); 243 wayPointFromTimeStamp.time = startTime; 244 String name = wavFile.getName(); 245 int dot = name.lastIndexOf('.'); 246 if (dot > 0) { 247 name = name.substring(0, dot); 248 } 249 wayPointFromTimeStamp.put(GpxConstants.GPX_NAME, name); 250 waypoints.add(wayPointFromTimeStamp); 251 } 252 } 253 254 // (e) analyse audio for spoken markers here, in due course 255 256 // (f) simply add a single marker at the start of the track 257 if ((Main.pref.getBoolean("marker.audiofromstart") || waypoints.isEmpty()) && hasTracks) { 258 boolean gotOne = false; 259 for (GpxTrack track : layer.data.tracks) { 260 for (GpxTrackSegment seg : track.getSegments()) { 261 for (WayPoint w : seg.getWayPoints()) { 262 WayPoint wStart = new WayPoint(w.getCoor()); 263 wStart.put(GpxConstants.GPX_NAME, "start"); 264 wStart.time = w.time; 265 waypoints.add(wStart); 266 gotOne = true; 267 break; 268 } 269 if (gotOne) { 270 break; 271 } 272 } 273 if (gotOne) { 274 break; 275 } 276 } 277 } 278 279 /* we must have got at least one waypoint now */ 280 281 Collections.sort((ArrayList<WayPoint>) waypoints, new Comparator<WayPoint>() { 282 @Override 283 public int compare(WayPoint a, WayPoint b) { 284 return a.time <= b.time ? -1 : 1; 285 } 286 }); 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 /* 296 * timeFromAudio intended for future use to shift markers of this type on 297 * synchronization 298 */ 299 if (w == wayPointFromTimeStamp) { 300 am.timeFromAudio = true; 301 } 302 ml.data.add(am); 303 } 304 305 if (timedMarkersOmitted && !markers.timedMarkersOmitted) { 306 JOptionPane 307 .showMessageDialog( 308 Main.parent, 309 tr("Some waypoints with timestamps from before the start of the track or after the end were omitted or moved to the start.")); 310 markers.timedMarkersOmitted = timedMarkersOmitted; 311 } 312 if (untimedMarkersOmitted && !markers.untimedMarkersOmitted) { 313 JOptionPane 314 .showMessageDialog( 315 Main.parent, 316 tr("Some waypoints which were too far from the track to sensibly estimate their time were omitted.")); 317 markers.untimedMarkersOmitted = untimedMarkersOmitted; 318 } 319 } 320}