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