Index: trunk/src/org/openstreetmap/josm/actions/audio/AudioBackAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/audio/AudioBackAction.java	(revision 547)
+++ trunk/src/org/openstreetmap/josm/actions/audio/AudioBackAction.java	(revision 547)
@@ -0,0 +1,40 @@
+// License: GPL. Copyright 2007 by Immanuel Scholz and others
+package org.openstreetmap.josm.actions.audio;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
+import org.openstreetmap.josm.tools.AudioPlayer;
+
+public class AudioBackAction extends JosmAction {
+
+	private double amount; // note, normally negative, i.e. jump backwards in time
+	
+	public AudioBackAction() {
+		super(tr("Back"), "audio-back", tr("Jump back."), 0, 0, true);
+		try {
+			amount = - Double.parseDouble(Main.pref.get("audio.forwardbackamount","10.0"));
+		} catch (NumberFormatException e) {
+			amount = 10.0;
+		}
+	}
+
+	public void actionPerformed(ActionEvent e) {
+		try {
+			if (AudioPlayer.playing() || AudioPlayer.paused())
+				AudioPlayer.play(AudioPlayer.url(), AudioPlayer.position() + amount);
+			else
+				MarkerLayer.playAudio();
+		} catch (Exception ex) {
+			AudioPlayer.audioMalfunction(ex);
+		}
+	}
+}
Index: trunk/src/org/openstreetmap/josm/actions/audio/AudioFwdAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/audio/AudioFwdAction.java	(revision 547)
+++ trunk/src/org/openstreetmap/josm/actions/audio/AudioFwdAction.java	(revision 547)
@@ -0,0 +1,40 @@
+// License: GPL. Copyright 2007 by Immanuel Scholz and others
+package org.openstreetmap.josm.actions.audio;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
+import org.openstreetmap.josm.tools.AudioPlayer;
+
+public class AudioFwdAction extends JosmAction {
+
+	private double amount;
+	
+	public AudioFwdAction() {
+		super(tr("Forward"), "audio-fwd", tr("Jump forward"), 0, 0, true);
+		try {
+			amount = Double.parseDouble(Main.pref.get("audio.forwardbackamount","10.0"));
+		} catch (NumberFormatException e) {
+			amount = 10.0;
+		}
+	}
+
+	public void actionPerformed(ActionEvent e) {
+		try {
+			if (AudioPlayer.playing() || AudioPlayer.paused())
+				AudioPlayer.play(AudioPlayer.url(), AudioPlayer.position() + amount);
+			else
+				MarkerLayer.playAudio();
+		} catch (Exception ex) {
+			AudioPlayer.audioMalfunction(ex);
+		}
+	}
+}
Index: trunk/src/org/openstreetmap/josm/actions/audio/AudioNextAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/audio/AudioNextAction.java	(revision 547)
+++ trunk/src/org/openstreetmap/josm/actions/audio/AudioNextAction.java	(revision 547)
@@ -0,0 +1,25 @@
+// License: GPL. Copyright 2007 by Immanuel Scholz and others
+package org.openstreetmap.josm.actions.audio;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
+
+public class AudioNextAction extends JosmAction {
+
+	public AudioNextAction() {
+		super(tr("Next Marker"), "audio-next", tr("Play next marker."), 0, 0, true);
+	}
+
+	public void actionPerformed(ActionEvent e) {
+		MarkerLayer.playNextMarker();
+	}
+}
Index: trunk/src/org/openstreetmap/josm/actions/audio/AudioPlayPauseAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/audio/AudioPlayPauseAction.java	(revision 547)
+++ trunk/src/org/openstreetmap/josm/actions/audio/AudioPlayPauseAction.java	(revision 547)
@@ -0,0 +1,37 @@
+// License: GPL. Copyright 2008 by David Earl and others
+package org.openstreetmap.josm.actions.audio;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.net.URL;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.tools.AudioPlayer;
+import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
+
+public class AudioPlayPauseAction extends JosmAction {
+
+	public AudioPlayPauseAction() {
+		super(tr("Play/pause"), "audio-playpause", tr("Play/pause audio."), KeyEvent.VK_PERIOD, 0, true);
+	}
+
+	public void actionPerformed(ActionEvent e) {
+		URL url = AudioPlayer.url();
+		try {
+			if (AudioPlayer.paused() && url != null) {
+				AudioPlayer.play(url);
+			} else if (AudioPlayer.playing()){
+				AudioPlayer.pause();
+			} else {
+				// find first audio marker to play
+				MarkerLayer.playAudio();
+			}
+		} catch (Exception ex) {
+			AudioPlayer.audioMalfunction(ex);
+		}
+	}	
+}
Index: trunk/src/org/openstreetmap/josm/actions/audio/AudioPrevAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/audio/AudioPrevAction.java	(revision 547)
+++ trunk/src/org/openstreetmap/josm/actions/audio/AudioPrevAction.java	(revision 547)
@@ -0,0 +1,25 @@
+// License: GPL. Copyright 2007 by Immanuel Scholz and others
+package org.openstreetmap.josm.actions.audio;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
+
+public class AudioPrevAction extends JosmAction {
+
+	public AudioPrevAction() {
+		super(tr("Previous Marker"), "audio-prev", tr("Play previous marker."), 0, 0, true);
+	}
+
+	public void actionPerformed(ActionEvent e) {
+		MarkerLayer.playPreviousMarker();
+	}
+}
Index: trunk/src/org/openstreetmap/josm/gui/MainMenu.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/MainMenu.java	(revision 546)
+++ trunk/src/org/openstreetmap/josm/gui/MainMenu.java	(revision 547)
@@ -41,4 +41,9 @@
 import org.openstreetmap.josm.actions.UnselectAllAction;
 import org.openstreetmap.josm.actions.UploadAction;
+import org.openstreetmap.josm.actions.audio.AudioBackAction;
+import org.openstreetmap.josm.actions.audio.AudioFwdAction;
+import org.openstreetmap.josm.actions.audio.AudioNextAction;
+import org.openstreetmap.josm.actions.audio.AudioPlayPauseAction;
+import org.openstreetmap.josm.actions.audio.AudioPrevAction;
 import org.openstreetmap.josm.actions.search.SearchAction;
 import org.openstreetmap.josm.data.DataSetChecker;
@@ -87,4 +92,11 @@
 	public final JosmAction joinNodeWay = new JoinNodeWayAction();
 
+	/* Audio menu */
+	public final JosmAction audioPlayPause = new AudioPlayPauseAction();
+	public final JosmAction audioNext = new AudioNextAction();
+	public final JosmAction audioPrev = new AudioPrevAction();
+	public final JosmAction audioFwd = new AudioFwdAction();
+	public final JosmAction audioBack = new AudioBackAction();
+
 	/* Help menu */
 	public final HelpAction help = new HelpAction();
@@ -95,4 +107,5 @@
 	public final JMenu viewMenu = new JMenu(tr("View"));
 	public final JMenu toolsMenu = new JMenu(tr("Tools"));
+	public final JMenu audioMenu = new JMenu(tr("Audio"));
 	public final JMenu presetsMenu = new JMenu(tr("Presets"));
 	public final JMenu helpMenu = new JMenu(tr("Help"));
@@ -193,4 +206,19 @@
 		add(toolsMenu);
 
+		if (! Main.pref.getBoolean("audio.menuinvisible")) {
+			audioMenu.setMnemonic('A');
+			current = audioMenu.add(audioPlayPause);
+			current.setAccelerator(audioPlayPause.shortCut);
+			current = audioMenu.add(audioNext);
+			current.setAccelerator(audioNext.shortCut);
+			current = audioMenu.add(audioPrev);
+			current.setAccelerator(audioPrev.shortCut);
+			current = audioMenu.add(audioFwd);
+			current.setAccelerator(audioFwd.shortCut);
+			current = audioMenu.add(audioBack);
+			current.setAccelerator(audioBack.shortCut);
+			add(audioMenu);
+		}
+
 		add(presetsMenu);
 		presetsMenu.setMnemonic('P');
Index: trunk/src/org/openstreetmap/josm/gui/layer/GpxLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/GpxLayer.java	(revision 546)
+++ trunk/src/org/openstreetmap/josm/gui/layer/GpxLayer.java	(revision 547)
@@ -57,4 +57,6 @@
 import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
 import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
+import org.openstreetmap.josm.gui.layer.markerlayer.Marker;
+import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
 import org.openstreetmap.josm.tools.ColorHelper;
 import org.openstreetmap.josm.tools.DontShowAgainInfo;
@@ -135,4 +137,26 @@
 		});
 
+		JMenuItem markersFromNamedTrackpoints = new JMenuItem(tr("Markers From Named Points"), ImageProvider.get("addmarkers"));
+		markersFromNamedTrackpoints.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+/*
+				public Collection<GpxTrack> tracks = new LinkedList<GpxTrack>();
+				public Collection<Collection<WayPoint>> trackSegs
+				= new LinkedList<Collection<WayPoint>>();
+				*/
+				GpxData namedTrackPoints = new GpxData();
+				for (GpxTrack track : data.tracks)
+					for (Collection<WayPoint> seg : track.trackSegs)
+						for (WayPoint point : seg)
+							if (point.attr.containsKey("name") || point.attr.containsKey("desc")) 
+								namedTrackPoints.waypoints.add(point);
+
+	            MarkerLayer ml = new MarkerLayer(namedTrackPoints, tr("Named Trackpoints from {0}", name), associatedFile);
+	            if (ml.data.size() > 0) {
+	            	Main.main.addLayer(ml);
+	            }
+			}
+		});
+
 		JMenuItem tagimage = new JMenuItem(tr("Import images"), ImageProvider.get("tagimages"));
 		tagimage.addActionListener(new ActionListener() {
@@ -192,4 +216,5 @@
 				line,
 				tagimage,
+				markersFromNamedTrackpoints,
 				new JMenuItem(new ConvertToDataLayerAction()),
 				new JSeparator(),
Index: trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/AudioMarker.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/AudioMarker.java	(revision 546)
+++ trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/AudioMarker.java	(revision 547)
@@ -17,4 +17,5 @@
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.tools.AudioPlayer;
 
 /**
@@ -27,4 +28,6 @@
 
 	private URL audioUrl;
+	private double syncOffset;
+	private static AudioMarker recentlyPlayedMarker = null;
 
 	/**
@@ -32,7 +35,7 @@
 	 * one or return <code>null</code>.
 	 */
-	public static AudioMarker create(LatLon ll, String url) {
+	public static AudioMarker create(LatLon ll, String text, String url, double offset) {
 		try {
-			return new AudioMarker(ll, new URL(url));
+			return new AudioMarker(ll, text, new URL(url), offset);
 		} catch (Exception ex) {
 			return null;
@@ -40,52 +43,38 @@
 	}
 
-	private AudioMarker(LatLon ll, URL audioUrl) {
-		super(ll, "speech.png");
+	private AudioMarker(LatLon ll, String text, URL audioUrl, double offset) {
+		super(ll, text, "speech.png", offset);
 		this.audioUrl = audioUrl;
+		this.syncOffset = 0.0;
 	}
 
 	@Override public void actionPerformed(ActionEvent ev) {
-		AudioInputStream audioInputStream = null;
-		try {
-			audioInputStream = AudioSystem.getAudioInputStream(audioUrl);
-		} catch (Exception e) {
-			audioMalfunction(e);
-			return;
-		}
-		AudioFormat	audioFormat = audioInputStream.getFormat();
-		SourceDataLine line = null;
-		DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
-		try {
-			line = (SourceDataLine) AudioSystem.getLine(info);
-			line.open(audioFormat);
-		} catch (Exception e)	{
-			audioMalfunction(e);
-			return;
-		}
-		line.start();
-
-		int	nBytesRead = 0;
-		byte[]	abData = new byte[16384];
-		while (nBytesRead != -1) {
-			try {
-				nBytesRead = audioInputStream.read(abData, 0, abData.length);
-			} catch (IOException e) {
-				audioMalfunction(e);
-				return;
-			}
-			if (nBytesRead >= 0) {
-				/* int	nBytesWritten = */ line.write(abData, 0, nBytesRead);
-			}
-		}
-		line.drain();
-		line.close();
+		play();
 	}
 
-	void audioMalfunction(Exception ex) {
-		JOptionPane.showMessageDialog(Main.parent, 
-				"<html><b>" + 
-				tr("There was an error while trying to play the sound file for this marker.") +
-				"</b><br>" + ex.getClass().getName() + ":<br><i>" + ex.getMessage() + "</i></html>",
-				tr("Error playing sound"), JOptionPane.ERROR_MESSAGE);
+	public static AudioMarker recentlyPlayedMarker() {
+		return recentlyPlayedMarker;
+	}
+	
+	public URL url() {
+		return audioUrl;
+	}
+	
+	/**
+	 * Starts playing the audio associated with the marker: used in response to pressing
+	 * the marker as well as indirectly 
+	 *
+	 */
+	public void play() {
+		try {
+			AudioPlayer.play(audioUrl, offset + syncOffset);
+			recentlyPlayedMarker = this;
+		} catch (Exception e) {
+			AudioPlayer.audioMalfunction(e);
+		}
+	}
+	
+	public void adjustOffset(double adjustment) {
+		syncOffset = adjustment; // added to offset may turn out negative, but that's ok
 	}
 }
Index: trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/ButtonMarker.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/ButtonMarker.java	(revision 546)
+++ trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/ButtonMarker.java	(revision 547)
@@ -14,4 +14,5 @@
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.layer.Layer;
 
 /**
@@ -25,6 +26,11 @@
 	private Rectangle buttonRectangle;
 	
-	public ButtonMarker(LatLon ll, String buttonImage) {
-		super(ll, null, buttonImage);
+	public ButtonMarker(LatLon ll, String buttonImage, double offset) {
+		super(ll, null, buttonImage, offset);
+		buttonRectangle = new Rectangle(0, 0, symbol.getIconWidth(), symbol.getIconHeight());
+	}
+	
+	public ButtonMarker(LatLon ll, String text, String buttonImage, double offset) {
+		super(ll, text, buttonImage, offset);
 		buttonRectangle = new Rectangle(0, 0, symbol.getIconWidth(), symbol.getIconHeight());
 	}
@@ -42,16 +48,19 @@
 		Border b;
 		Point mousePosition = mv.getMousePosition();
-		if (mousePosition == null)
-			return; // mouse outside the whole window
 		
-		if (mousePressed) {
-			b = BorderFactory.createBevelBorder(BevelBorder.LOWERED);
-		} else {
-			b = BorderFactory.createBevelBorder(BevelBorder.RAISED);
+		if (mousePosition != null) {
+			// mouse is inside the window
+			if (mousePressed) {
+				b = BorderFactory.createBevelBorder(BevelBorder.LOWERED);
+			} else {
+				b = BorderFactory.createBevelBorder(BevelBorder.RAISED);
+			}
+			Insets inset = b.getBorderInsets(mv);
+			Rectangle r = new Rectangle(buttonRectangle);
+			r.grow((inset.top+inset.bottom)/2, (inset.left+inset.right)/2);
+			b.paintBorder(mv, g, r.x, r.y, r.width, r.height);
 		}
-		Insets inset = b.getBorderInsets(mv);
-		Rectangle r = new Rectangle(buttonRectangle);
-		r.grow((inset.top+inset.bottom)/2, (inset.left+inset.right)/2);
-		b.paintBorder(mv, g, r.x, r.y, r.width, r.height);
+		if ((text != null) && (show.equalsIgnoreCase("show")) && Main.pref.getBoolean("marker.buttonlabels"))
+			g.drawString(text, screen.x+4, screen.y+2);
 	}
 }
Index: trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/ImageMarker.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/ImageMarker.java	(revision 546)
+++ trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/ImageMarker.java	(revision 547)
@@ -34,7 +34,7 @@
 	public URL imageUrl;
 
-	public static ImageMarker create(LatLon ll, String url) {
+	public static ImageMarker create(LatLon ll, String url, double offset) {
 		try {
-			return new ImageMarker(ll, new URL(url));
+			return new ImageMarker(ll, new URL(url), offset);
 		} catch (Exception ex) {
 			return null;
@@ -42,6 +42,6 @@
 	}
 
-	private ImageMarker(LatLon ll, URL imageUrl) {
-		super(ll, "photo.png");
+	private ImageMarker(LatLon ll, URL imageUrl, double offset) {
+		super(ll, "photo.png", offset);
 		this.imageUrl = imageUrl;
 	}
Index: trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/Marker.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/Marker.java	(revision 546)
+++ trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/Marker.java	(revision 547)
@@ -64,4 +64,6 @@
 	public final String text;
 	public final Icon symbol;
+	public double offset; /* time offset in seconds from the gpx point from which it was derived,
+							 may be adjusted later to sync with other data, so not final */
 
 	/**
@@ -76,4 +78,8 @@
 		Marker.markerProducers.add(new MarkerProducers() {
 			public Marker createMarker(WayPoint wpt, File relativePath) {
+				return createMarker(wpt, relativePath, 0.0);
+			}
+			
+			public Marker createMarker(WayPoint wpt, File relativePath, double offset) {
 				String uri = null;
 				// cheapest way to check whether "link" object exists and is a non-empty
@@ -90,20 +96,19 @@
                     uri = new File(relativePath, uri).toURI().toString();
 
-                if (uri == null) {
-                    String name_desc = "";
-                    if (wpt.attr.containsKey("name")) {
-                        name_desc = wpt.getString("name");
-                    } else if (wpt.attr.containsKey("desc")) {
-                        name_desc = wpt.getString("desc");
-                    }
-                    return new Marker(wpt.latlon, name_desc, wpt.getString("symbol"));
+                String name_desc = "";
+                if (wpt.attr.containsKey("name")) {
+                	name_desc = wpt.getString("name");
+                } else if (wpt.attr.containsKey("desc")) {
+                    name_desc = wpt.getString("desc");
                 }
-
-                if (uri.endsWith(".wav"))
-                    return AudioMarker.create(wpt.latlon, uri);
+                
+                if (uri == null)
+                    return new Marker(wpt.latlon, name_desc, wpt.getString("symbol"), offset);
+                else if (uri.endsWith(".wav"))
+                    return AudioMarker.create(wpt.latlon, name_desc, uri, offset);
                 else if (uri.endsWith(".png") || uri.endsWith(".jpg") || uri.endsWith(".jpeg") || uri.endsWith(".gif"))
-					return ImageMarker.create(wpt.latlon, uri);
+					return ImageMarker.create(wpt.latlon, uri, offset);
 				else
-					return WebMarker.create(wpt.latlon, uri);
+					return WebMarker.create(wpt.latlon, uri, offset);
 			}
 
@@ -119,7 +124,8 @@
 	}
 
-	public Marker(LatLon ll, String text, String iconName) {
+	public Marker(LatLon ll, String text, String iconName, double offset) {
 		eastNorth = Main.proj.latlon2eastNorth(ll); 
 		this.text = text;
+		this.offset = offset;
 		Icon symbol = ImageProvider.getIfAvailable("markers",iconName);
 		if (symbol == null)
@@ -177,9 +183,11 @@
 	 * @param relativePath An path to use for constructing relative URLs or 
 	 *        <code>null</code> for no relative URLs
+	 * @param offset double in seconds as the time offset of this marker from 
+	 * 		  the GPX file from which it was derived (if any).  
 	 * @return a new Marker object
 	 */
-	public static Marker createMarker(WayPoint wpt, File relativePath) {
+	public static Marker createMarker(WayPoint wpt, File relativePath, double offset) {
 		for (MarkerProducers maker : Marker.markerProducers) {
-			Marker marker = maker.createMarker(wpt, relativePath);
+			Marker marker = maker.createMarker(wpt, relativePath, offset);
 			if (marker != null)
 				return marker;
@@ -187,3 +195,18 @@
 		return null;
 	}
+	
+	/**
+	 * Returns an AudioMarker derived from this Marker and the provided uri
+	 * Subclasses of specific marker types override this to return null as they can't
+	 * be turned into AudioMarkers. This includes AudioMarkers themselves, as they 
+	 * already have audio.  
+	 * 
+	 * @param uri uri of wave file
+	 * @return AudioMarker
+	 */
+	
+	public AudioMarker audioMarkerFromMarker(String uri) {
+		AudioMarker audioMarker = AudioMarker.create(Main.proj.eastNorth2latlon(this.eastNorth), this.text, uri, this.offset);
+		return audioMarker;
+	}
 }
Index: trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayer.java	(revision 546)
+++ trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayer.java	(revision 547)
@@ -16,11 +16,19 @@
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Iterator;
+import java.util.Date;
+import java.text.SimpleDateFormat;
+import java.text.ParsePosition;
+import java.text.ParseException;
+import java.net.URL;
 
 import javax.swing.Icon;
 import javax.swing.JColorChooser;
+import javax.swing.JFileChooser;
 import javax.swing.JMenuItem;
 import javax.swing.JOptionPane;
 import javax.swing.JSeparator;
 import javax.swing.SwingUtilities;
+import javax.swing.filechooser.FileFilter;
 
 import org.openstreetmap.josm.Main;
@@ -33,6 +41,8 @@
 import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
 import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.markerlayer.AudioMarker;
 import org.openstreetmap.josm.tools.ColorHelper;
 import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.AudioPlayer;
 
 /**
@@ -60,7 +70,23 @@
 		this.associatedFile = associatedFile;
 		this.data = new ArrayList<Marker>();
-		
+		double offset = 0.0;
+		Date firstDate = null;
+
 		for (WayPoint wpt : indata.waypoints) {
-            Marker m = Marker.createMarker(wpt, indata.storageFile);
+			/* calculate time differences in waypoints */
+			if (wpt.attr.containsKey("time")) {
+				SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); // ignore timezone, as it is all relative
+				Date d = f.parse(wpt.attr.get("time").toString(), new ParsePosition(0));
+				if (d == null /* failed to parse */) {
+					offset = 0.0;
+				} else if (firstDate == null) {
+					firstDate = d;
+					offset = 0.0;
+				} else {
+					offset = (d.getTime() - firstDate.getTime()) / 1000.0; /* ms => seconds */
+				}
+			}
+			
+            Marker m = Marker.createMarker(wpt, indata.storageFile, offset);
             if (m != null)
             	data.add(m);
@@ -73,4 +99,15 @@
 						if (e.getButton() != MouseEvent.BUTTON1)
 							return;
+						boolean mousePressedInButton = false;
+						if (e.getPoint() != null) {
+							for (Marker mkr : data) {
+								if (mkr.containsPoint(e.getPoint())) {
+									mousePressedInButton = true;
+									break;
+								}
+							}
+						}
+						if (! mousePressedInButton)
+							return;
 						mousePressed  = true;
 						if (visible)
@@ -78,5 +115,5 @@
 					}
 					@Override public void mouseReleased(MouseEvent ev) {
-						if (ev.getButton() != MouseEvent.BUTTON1)
+						if (ev.getButton() != MouseEvent.BUTTON1 || ! mousePressed)
 							return;
 						mousePressed = false;
@@ -116,5 +153,5 @@
 		else
 			g.setColor(Color.GRAY);
-
+		
 		for (Marker mkr : data) {
 			if (mousePos != null && mkr.containsPoint(mousePos)) {
@@ -143,4 +180,19 @@
 		for (Marker mkr : data)
 			v.visit(mkr.eastNorth);
+	}
+
+	public void applyAudio(File wavFile) {
+		String uri = "file:".concat(wavFile.getAbsolutePath());
+		Collection<Marker> markers = new ArrayList<Marker>();
+	    for (Marker mkr : data) {
+	    	AudioMarker audioMarker = mkr.audioMarkerFromMarker(uri);
+	    	if (audioMarker == null) {
+	    		markers.add(mkr);
+	    	} else { 
+	            markers.add(audioMarker);
+	    	}
+	    }
+	    data.clear();
+	    data.addAll(markers);
 	}
 
@@ -171,4 +223,34 @@
 		});
 
+		JMenuItem applyaudio = new JMenuItem(tr("Apply Audio"), ImageProvider.get("applyaudio"));
+		applyaudio.addActionListener(new ActionListener(){
+			public void actionPerformed(ActionEvent e) {
+				JFileChooser fc = new JFileChooser(Main.pref.get("tagimages.lastdirectory"));
+				fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
+				fc.setAcceptAllFileFilterUsed(false);
+				fc.setFileFilter(new FileFilter(){
+					@Override public boolean accept(File f) {
+						return f.isDirectory() || f.getName().toLowerCase().endsWith(".wav");
+					}
+					@Override public String getDescription() {
+						return tr("Wave Audio files (*.wav)");
+					}
+				});
+				fc.showOpenDialog(Main.parent);
+				File sel = fc.getSelectedFile();
+				if (sel == null)
+					return;
+				applyAudio(sel);
+				Main.map.repaint();
+			}
+		});
+
+		JMenuItem syncaudio = new JMenuItem(tr("Synchronize Audio"), ImageProvider.get("audio-sync"));
+		syncaudio.addActionListener(new ActionListener(){
+			public void actionPerformed(ActionEvent e) {
+				adjustOffsetsOnAudioMarkers();
+			}
+		});
+
 		return new Component[] {
 			new JMenuItem(new LayerListDialog.ShowHideLayerAction(this)),
@@ -177,4 +259,7 @@
 			new JSeparator(),
 			color,
+			new JSeparator(),
+			syncaudio,
+			applyaudio,
 			new JMenuItem(new RenameLayerAction(associatedFile, this)),
 			new JSeparator(),
@@ -182,3 +267,95 @@
 		};
 	}
+
+	private void adjustOffsetsOnAudioMarkers() {
+		Marker startMarker = AudioMarker.recentlyPlayedMarker();
+		if (startMarker != null && ! data.contains(startMarker)) {
+			// message?
+			startMarker = null;
+		}
+		if (startMarker == null) {
+			// find the first audioMarker in this layer
+			for (Marker m : data) {
+				if (m.getClass() == AudioMarker.class) {
+					startMarker = m;
+					break;
+				}
+			}
+		}
+		if (startMarker == null) {
+			// still no marker to work from - message?
+			return;
+		}
+		// apply adjustment to all subsequent audio markers in the layer
+		double adjustment = AudioPlayer.position(); // in seconds
+		boolean seenStart = false;
+		URL url = ((AudioMarker)startMarker).url();
+		for (Marker m : data) {
+			if (m == startMarker)
+				seenStart = true;
+			if (seenStart) {
+				AudioMarker ma = (AudioMarker) m; // it must be an AudioMarker
+				if (! ma.url().equals(url))
+					break;
+				ma.adjustOffset(adjustment);
+			}
+		}
+	}
+	
+	public static void playAudio() {
+		if (Main.map == null || Main.map.mapView == null)
+			return;
+		for (Layer layer : Main.map.mapView.getAllLayers()) {
+			if (layer.getClass() == MarkerLayer.class) {
+				MarkerLayer markerLayer = (MarkerLayer) layer;
+				for (Marker marker : markerLayer.data) {
+					if (marker.getClass() == AudioMarker.class) {
+						((AudioMarker)marker).play();
+						break;
+					}
+				}
+			}
+		}
+	}
+
+	public static void playNextMarker() {
+		playAdjacentMarker(true);
+	}
+	
+	public static void playPreviousMarker() {
+		playAdjacentMarker(false);
+	}
+	
+	private static void playAdjacentMarker(boolean next) {
+		Marker startMarker = AudioMarker.recentlyPlayedMarker();
+		if (startMarker == null) {
+			// message?
+			return;
+		}
+		Marker previousMarker = null;
+		Marker targetMarker = null;
+		boolean nextTime = false;
+		if (Main.map == null || Main.map.mapView == null)
+			return;
+		for (Layer layer : Main.map.mapView.getAllLayers()) {
+			if (layer.getClass() == MarkerLayer.class) {
+				MarkerLayer markerLayer = (MarkerLayer) layer;
+				for (Marker marker : markerLayer.data) {
+					if (marker == startMarker) {
+						if (next) {
+							nextTime = true;
+						} else {
+							((AudioMarker)previousMarker).play();
+							break;
+						}
+					} else if (nextTime) {
+						((AudioMarker)marker).play();
+						return;
+					}
+					previousMarker = marker;
+				}
+			}
+		}
+	}
+	
 }
Index: trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerProducers.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerProducers.java	(revision 546)
+++ trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerProducers.java	(revision 547)
@@ -27,4 +27,4 @@
 	 * @return A Marker object, or <code>null</code>.
 	 */
-	public Marker createMarker(WayPoint wp, File relativePath);
+	public Marker createMarker(WayPoint wp, File relativePath, double offset);
 }
Index: trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/WebMarker.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/WebMarker.java	(revision 546)
+++ trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/WebMarker.java	(revision 547)
@@ -23,7 +23,7 @@
 	public URL webUrl;
 
-	public static WebMarker create (LatLon ll, String url) {
+	public static WebMarker create (LatLon ll, String url, double offset) {
 		try {
-			return new WebMarker(ll, new URL(url));
+			return new WebMarker(ll, new URL(url), offset);
 		} catch (Exception ex) {
 			return null;
@@ -31,6 +31,6 @@
 	}
 
-	private WebMarker(LatLon ll, URL webUrl) {
-		super(ll, "web.png");
+	private WebMarker(LatLon ll, URL webUrl, double offset) {
+		super(ll, "web.png", offset);
 		this.webUrl = webUrl;
 	}
Index: trunk/src/org/openstreetmap/josm/io/GpxReader.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/GpxReader.java	(revision 546)
+++ trunk/src/org/openstreetmap/josm/io/GpxReader.java	(revision 547)
@@ -238,7 +238,5 @@
 					currentState = states.pop();
 					currentTrackSeg.add(currentWayPoint);
-					String option = "marker.namedtrackpoints";
-					if (Main.pref.hasKey(option) && 
-						Main.pref.getBoolean(option) && 
+					if (Main.pref.getBoolean("marker.namedtrackpoints") && 
 						(currentWayPoint.attr.containsKey("name") || 
 							currentWayPoint.attr.containsKey("desc"))) 
Index: trunk/src/org/openstreetmap/josm/tools/AudioPlayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/AudioPlayer.java	(revision 547)
+++ trunk/src/org/openstreetmap/josm/tools/AudioPlayer.java	(revision 547)
@@ -0,0 +1,315 @@
+// License: GPL. Copyright 2008 by David Earl and others
+package org.openstreetmap.josm.tools;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.lang.Thread;
+import java.net.URL;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioInputStream;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.DataLine;
+import javax.sound.sampled.SourceDataLine;
+import javax.swing.JOptionPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.Preferences;
+
+/**
+ * Creates and controls a separate audio player thread.
+ * 
+ * @author David Earl <david@frankieandshadow.com>
+ *
+ */
+public class AudioPlayer extends Thread {
+
+	private static AudioPlayer audioPlayer = null;
+
+	private enum State { INITIALIZING, NOTPLAYING, PLAYING, PAUSED, INTERRUPTED } 
+	private State state;
+    private enum Command { PLAY, PAUSE, JUMP }
+    private enum Result { WAITING, OK, FAILED }
+    private URL playingUrl;
+    private double leadIn; // seconds
+	private double position; // seconds
+	private double bytesPerSecond; 
+	
+	/**
+	 * Passes information from the control thread to the playing thread 
+	 */
+	private class Execute {
+		private Command command;
+		private Result result;
+		private Exception exception;
+		private URL url;
+		private double offset; // seconds
+
+		/*
+		 * Called to execute the commands in the other thread 
+		 */
+		protected void play(URL url, double offset) throws Exception {
+			this.url = url;
+			this.offset = offset;
+			command = Command.PLAY;
+			result = Result.WAITING;
+			send();
+		}
+		protected void pause() throws Exception {
+			command = Command.PAUSE;
+			send();
+		}
+		protected void jump(double offset) throws Exception {
+			this.offset = offset;
+			command = Command.JUMP;
+			send();
+		}
+		private void send() throws Exception {
+			result = Result.WAITING;
+			interrupt();
+			while (result == Result.WAITING) { sleep(10); /* yield(); */ }
+			if (result == Result.FAILED) { throw exception; }
+		}
+		private void possiblyInterrupt() throws InterruptedException {
+			if (interrupted() || result == Result.WAITING)
+				throw new InterruptedException();
+		}
+		protected void failed (Exception e) {
+			exception = e;
+			result = Result.FAILED;
+			state = State.NOTPLAYING;
+		}
+		protected void ok (State newState) {
+			result = Result.OK;
+			state = newState;
+		}
+		protected double offset() {
+			return offset;
+		}
+		protected URL url() {
+			return url;
+		}
+		protected Command command() {
+			return command;
+		}
+	}
+	
+	private Execute command;
+
+	/**
+	 * Plays a WAV audio file from the beginning. See also the variant which doesn't 
+	 * start at the beginning of the stream
+	 * @param url The resource to play, which must be a WAV file or stream
+	 * @throws audio fault exception, e.g. can't open stream,  unhandleable audio format
+	 */
+	public static void play(URL url) throws Exception {
+		AudioPlayer.play(url, 0.0);
+	}
+	
+	/**
+	 * Plays a WAV audio file from a specified position.
+	 * @param url The resource to play, which must be a WAV file or stream
+	 * @param seconds The number of seconds into the audio to start playing
+	 * @throws audio fault exception, e.g. can't open stream,  unhandleable audio format
+	 */
+	public static void play(URL url, double seconds) throws Exception {
+		AudioPlayer.get().command.play(url, seconds);
+	}
+	
+	/**
+	 * Fast Forward or Rewind the audio stream by the given number of seconds. 
+	 * Terminates playing if would jump after the end
+	 * Starts from the beginning if would jump before start 
+	 * @param seconds fast forwards (positive) or rewinds (negative) by this number of seconds
+	 * @throws audio fault exception, e.g. can't open stream,  unhandleable audio format
+	 */
+	public static void jump(double seconds) throws Exception {
+		AudioPlayer.get().command.jump(seconds);
+	}
+	
+	/**
+	 * Pauses the currently playing audio stream. Does nothing if nothing playing.
+	 * @throws audio fault exception, e.g. can't open stream,  unhandleable audio format
+	 */
+	public static void pause() throws Exception {
+		AudioPlayer.get().command.pause();
+	}
+	
+	/**
+	 * To get the Url of the playing or recently played audio.
+	 * @returns url - could be null
+	 */
+	public static URL url() {
+		return AudioPlayer.get().playingUrl;
+	}
+	
+	/**
+	 * Whether or not we are paused.
+	 * @returns boolean whether or not paused
+	 */
+	public static boolean paused() {
+		return AudioPlayer.get().state == State.PAUSED;
+	}
+
+	/**
+	 * Whether or not we are playing.
+	 * @returns boolean whether or not playing
+	 */
+	public static boolean playing() {
+		return AudioPlayer.get().state == State.PLAYING;
+	}
+
+	/**
+	 * How far we are through playing, in seconds.
+	 * @returns double seconds
+	 */
+	public static double position() {
+		return AudioPlayer.get().position;
+	}
+	
+	/**
+	 *  gets the singleton object, and if this is the first time, creates it along with 
+	 *  the thread to support audio
+	 */
+	private static AudioPlayer get() {
+		if (audioPlayer != null)
+			return audioPlayer;
+		try {
+			audioPlayer = new AudioPlayer();
+			return audioPlayer;
+		} catch (Exception ex) {
+			return null;
+		}
+	}
+
+	private AudioPlayer() {
+		state = State.INITIALIZING;
+		command = new Execute();
+		playingUrl = null;
+		try {
+			leadIn = Double.parseDouble(Main.pref.get("audio.leadin", "1.0" /* default, seconds */));
+		} catch (NumberFormatException e) {
+			leadIn = 1.0; // failed to parse
+		}
+		start();
+		while (state == State.INITIALIZING) { yield(); }
+	}
+
+	/**
+	 * Starts the thread to actually play the audio, per Thread interface
+	 * Not to be used as public, though Thread interface doesn't allow it to be made private
+	 */
+	@Override public void run() {
+		/* code running in separate thread */
+
+		playingUrl = null;
+		AudioInputStream audioInputStream = null;
+		int nBytesRead = 0;
+		SourceDataLine audioOutputLine = null;
+		AudioFormat	audioFormat = null;
+		byte[] abData = new byte[8192];
+		
+		for (;;) {
+			try {
+				switch (state) {
+				case INITIALIZING:
+					// we're ready to take interrupts
+					state = State.NOTPLAYING;
+					break;
+				case NOTPLAYING:
+				case PAUSED:
+					sleep(200);
+					break;
+				case PLAYING:
+					for(;;) {
+						nBytesRead = audioInputStream.read(abData, 0, abData.length);
+						position += nBytesRead / bytesPerSecond;
+						command.possiblyInterrupt();
+						if (nBytesRead < 0) { break; }
+						audioOutputLine.write(abData, 0, nBytesRead); // => int nBytesWritten
+						command.possiblyInterrupt();
+					}
+					// end of audio, clean up
+					audioOutputLine.drain();
+					audioOutputLine.close();
+					audioOutputLine = null;
+					audioInputStream.close();
+					audioInputStream = null;
+					playingUrl = null;
+					state = State.NOTPLAYING;
+					command.possiblyInterrupt();
+					break;
+				}
+			} catch (InterruptedException e) {
+				interrupted(); // just in case we get an interrupt
+				State stateChange = state;
+				state = State.INTERRUPTED;
+				try {
+					switch (command.command()) {
+					case PLAY:	
+						double offset = command.offset();
+						if (playingUrl != command.url() || 
+							stateChange != State.PAUSED || 
+							offset != 0.0) 
+						{
+							if (audioInputStream != null) {
+								audioInputStream.close();
+								audioInputStream = null;
+							}
+							playingUrl = command.url();
+							audioInputStream = AudioSystem.getAudioInputStream(playingUrl);
+							audioFormat = audioInputStream.getFormat();
+							DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
+							nBytesRead = 0;
+							position = 0.0;
+							double adjustedOffset = offset - leadIn;
+							bytesPerSecond = audioFormat.getFrameRate() /* frames per second */
+								* audioFormat.getFrameSize() /* bytes per frame */;
+							if (offset != 0.0 && adjustedOffset > 0.0) {
+								long bytesToSkip = (long)(
+									adjustedOffset /* seconds (double) */ * bytesPerSecond);
+								/* skip doesn't seem to want to skip big chunks, so 
+								 * reduce it to smaller ones 
+								 */
+								// audioInputStream.skip(bytesToSkip);
+								int skipsize = 8192;
+								while (bytesToSkip > skipsize) {
+									audioInputStream.skip(skipsize);
+									bytesToSkip -= skipsize;
+								}
+								audioInputStream.skip(bytesToSkip);
+								position = adjustedOffset;
+							}
+							if (audioOutputLine == null) {
+								audioOutputLine = (SourceDataLine) AudioSystem.getLine(info);
+								audioOutputLine.open(audioFormat);
+								audioOutputLine.start();
+							}
+						}
+						stateChange = State.PLAYING;
+						break;
+					case PAUSE:
+						stateChange = state.PAUSED;
+						break;
+					case JUMP:
+						stateChange = state.PAUSED; // for now
+						break;
+					}
+					command.ok(stateChange);
+				} catch (Exception startPlayingException) {
+					command.failed(startPlayingException); // sets state
+				}
+			} catch (Exception e) {
+				state = State.NOTPLAYING;
+			}
+		}
+	}
+
+	public static void audioMalfunction(Exception ex) {
+		JOptionPane.showMessageDialog(Main.parent, 
+				"<html><b>" + 
+				tr("There was an error while trying to play the sound file for this marker.") +
+				"</b><br>" + ex.getClass().getName() + ":<br><i>" + ex.getMessage() + "</i></html>",
+				tr("Error playing sound"), JOptionPane.ERROR_MESSAGE);
+	}
+}
