Index: trunk/.classpath
===================================================================
--- trunk/.classpath	(revision 4281)
+++ trunk/.classpath	(revision 4282)
@@ -17,4 +17,10 @@
 	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
 	<classpathentry exported="true" kind="con" path="GROOVY_SUPPORT"/>
+	<classpathentry kind="lib" path="test/lib/unitils-core/commons-collections-3.2.jar"/>
+	<classpathentry kind="lib" path="test/lib/unitils-core/commons-lang-2.3.jar"/>
+	<classpathentry kind="lib" path="test/lib/unitils-core/commons-logging-1.1.jar"/>
+	<classpathentry kind="lib" path="test/lib/unitils-core/junit-4.4.jar"/>
+	<classpathentry kind="lib" path="test/lib/unitils-core/ognl-2.6.9.jar"/>
+	<classpathentry kind="lib" path="test/lib/unitils-core/unitils-core-3.1.jar"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
Index: trunk/src/org/openstreetmap/josm/data/gpx/WayPoint.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/gpx/WayPoint.java	(revision 4281)
+++ trunk/src/org/openstreetmap/josm/data/gpx/WayPoint.java	(revision 4282)
@@ -5,13 +5,17 @@
 
 import java.awt.Color;
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.List;
 
 import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.projection.Projections;
 import org.openstreetmap.josm.tools.PrimaryDateParser;
+import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider;
 
-public class WayPoint extends WithAttributes implements Comparable<WayPoint> {
+public class WayPoint extends WithAttributes implements Comparable<WayPoint>, TemplateEngineDataProvider {
 
     private static ThreadLocal<PrimaryDateParser> dateParser = new ThreadLocal<PrimaryDateParser>() {
@@ -117,3 +121,18 @@
         return new Date((long) (time * 1000));
     }
+
+    @Override
+    public Object getTemplateValue(String name) {
+        return attr.get(name);
+    }
+
+    @Override
+    public boolean evaluateCondition(Match condition) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public List<String> getTemplateKeys() {
+        return new ArrayList<String>(attr.keySet());
+    }
 }
Index: trunk/src/org/openstreetmap/josm/data/preferences/AbstractProperty.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/preferences/AbstractProperty.java	(revision 4281)
+++ trunk/src/org/openstreetmap/josm/data/preferences/AbstractProperty.java	(revision 4282)
@@ -7,5 +7,5 @@
  * captures the common functionality of preference properties
  */
-public class AbstractProperty {
+public abstract class AbstractProperty<T> {
     protected final String key;
 
@@ -21,3 +21,10 @@
         return Main.pref.hasKey(key);
     }
+
+    public abstract T getDefaultValue();
+
+    public void remove() {
+        Main.pref.put(getKey(), String.valueOf(getDefaultValue()));
+    }
+
 }
Index: trunk/src/org/openstreetmap/josm/data/preferences/BooleanProperty.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/preferences/BooleanProperty.java	(revision 4281)
+++ trunk/src/org/openstreetmap/josm/data/preferences/BooleanProperty.java	(revision 4282)
@@ -4,5 +4,5 @@
 import org.openstreetmap.josm.Main;
 
-public class BooleanProperty extends AbstractProperty {
+public class BooleanProperty extends AbstractProperty<Boolean> {
 
     protected final boolean defaultValue;
@@ -14,5 +14,5 @@
 
     public boolean get() {
-        return Main.pref.getBoolean(getKey(), isDefaultValue());
+        return Main.pref.getBoolean(getKey(), defaultValue);
     }
 
@@ -21,5 +21,6 @@
     }
 
-    public boolean isDefaultValue() {
+    @Override
+    public Boolean getDefaultValue() {
         return defaultValue;
     }
Index: trunk/src/org/openstreetmap/josm/data/preferences/CachedProperty.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/preferences/CachedProperty.java	(revision 4282)
+++ trunk/src/org/openstreetmap/josm/data/preferences/CachedProperty.java	(revision 4282)
@@ -0,0 +1,66 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.preferences;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
+import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
+
+public abstract class CachedProperty<T> extends AbstractProperty<T> implements PreferenceChangedListener {
+
+    protected final String defaultValue;
+    private T value;
+    private int updateCount;
+
+    protected CachedProperty(String key, String defaultValue) {
+        super(key);
+        Main.pref.addPreferenceChangeListener(this);
+        this.defaultValue = defaultValue;
+        updateValue();
+    }
+
+    protected void updateValue() {
+        if (Main.pref.hasKey(key)) {
+            this.value = fromString(Main.pref.get(key));
+        } else {
+            this.value = getDefaultValue();
+        }
+        updateCount++;
+    }
+
+    protected abstract T fromString(String s);
+
+    public T get() {
+        return value;
+    }
+
+    public void put(String value) {
+        Main.pref.put(key, value);
+        this.value = fromString(value);
+        updateCount++;
+    }
+
+    public int getUpdateCount() {
+        return updateCount;
+    }
+
+    @Override
+    public T getDefaultValue() {
+        return fromString(getDefaultValueAsString());
+    }
+
+    public String getDefaultValueAsString() {
+        return defaultValue;
+    }
+
+    public String getAsString() {
+        return Main.pref.get(getKey(), getDefaultValueAsString());
+    }
+
+    @Override
+    public void preferenceChanged(PreferenceChangeEvent e) {
+        if (e.getKey().equals(key)) {
+            updateValue();
+        }
+    }
+
+}
Index: trunk/src/org/openstreetmap/josm/data/preferences/CollectionProperty.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/preferences/CollectionProperty.java	(revision 4281)
+++ trunk/src/org/openstreetmap/josm/data/preferences/CollectionProperty.java	(revision 4282)
@@ -6,5 +6,5 @@
 import org.openstreetmap.josm.Main;
 
-public class CollectionProperty extends AbstractProperty {
+public class CollectionProperty extends AbstractProperty<Collection<String>> {
     protected final Collection<String> defaultValue;
 
@@ -22,4 +22,5 @@
     }
 
+    @Override
     public Collection<String> getDefaultValue() {
         return defaultValue;
Index: trunk/src/org/openstreetmap/josm/data/preferences/IntegerProperty.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/preferences/IntegerProperty.java	(revision 4281)
+++ trunk/src/org/openstreetmap/josm/data/preferences/IntegerProperty.java	(revision 4282)
@@ -4,5 +4,5 @@
 import org.openstreetmap.josm.Main;
 
-public class IntegerProperty extends AbstractProperty {
+public class IntegerProperty extends AbstractProperty<Integer> {
 
     protected final int defaultValue;
@@ -37,5 +37,6 @@
     }
 
-    public int getDefaultValue() {
+    @Override
+    public Integer getDefaultValue() {
         return defaultValue;
     }
Index: trunk/src/org/openstreetmap/josm/data/preferences/StringProperty.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/preferences/StringProperty.java	(revision 4281)
+++ trunk/src/org/openstreetmap/josm/data/preferences/StringProperty.java	(revision 4282)
@@ -4,5 +4,5 @@
 import org.openstreetmap.josm.Main;
 
-public class StringProperty extends AbstractProperty {
+public class StringProperty extends AbstractProperty<String> {
 
     protected final String defaultValue;
@@ -21,4 +21,5 @@
     }
 
+    @Override
     public String getDefaultValue() {
         return defaultValue;
Index: trunk/src/org/openstreetmap/josm/gui/layer/GpxLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/GpxLayer.java	(revision 4281)
+++ trunk/src/org/openstreetmap/josm/gui/layer/GpxLayer.java	(revision 4282)
@@ -20,4 +20,6 @@
 import java.awt.geom.Rectangle2D;
 import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.text.DateFormat;
 import java.util.ArrayList;
@@ -26,16 +28,10 @@
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.Map;
 import java.util.concurrent.Future;
 
 import javax.swing.AbstractAction;
 import javax.swing.Action;
-import javax.swing.Box;
-import javax.swing.ButtonGroup;
-import javax.swing.ButtonModel;
 import javax.swing.Icon;
 import javax.swing.JFileChooser;
@@ -45,5 +41,4 @@
 import javax.swing.JOptionPane;
 import javax.swing.JPanel;
-import javax.swing.JRadioButton;
 import javax.swing.JScrollPane;
 import javax.swing.SwingUtilities;
@@ -358,5 +353,5 @@
 
     /**
-     * transition function: 
+     * transition function:
      *  w(0)=1, w(1)=0, 0<=w(x)<=1
      * @param x number: 0<=x<=1
@@ -364,11 +359,10 @@
      */
     private static float w(float x) {
-        if (x < 0.5) {
+        if (x < 0.5)
             return 1 - 2*x*x;
-        } else {
-        return 2*(1-x)*(1-x);
-        }
-    }
-    
+        else
+            return 2*(1-x)*(1-x);
+    }
+
     // lookup array to draw arrows without doing any math
     private final static int ll0 = 9;
@@ -463,5 +457,7 @@
                     for (GpxTrack trk : data.tracks) {
                         for (GpxTrackSegment segment : trk.getSegments()) {
-                            if(!forceLines) oldWp = null;
+                            if(!forceLines) {
+                                oldWp = null;
+                            }
                             for (WayPoint trkPnt : segment.getWayPoints()) {
                                 LatLon c = trkPnt.getCoor();
@@ -472,6 +468,10 @@
                                     double vel = c.greatCircleDistance(oldWp.getCoor())
                                             / (trkPnt.time - oldWp.time);
-                                    if(vel > maxval) maxval = vel;
-                                    if(vel < minval) minval = vel;
+                                    if(vel > maxval) {
+                                        maxval = vel;
+                                    }
+                                    if(vel < minval) {
+                                        minval = vel;
+                                    }
                                 }
                                 oldWp = trkPnt;
@@ -486,6 +486,10 @@
                                 if (val != null) {
                                     double hdop = ((Float) val).doubleValue();
-                                    if(hdop > maxval) maxval = hdop;
-                                    if(hdop < minval) minval = hdop;
+                                    if(hdop > maxval) {
+                                        maxval = hdop;
+                                    }
+                                    if(hdop < minval) {
+                                        minval = hdop;
+                                    }
                                 }
                             }
@@ -496,16 +500,22 @@
             }
             if (colored == colorModes.time) {
-                    for (GpxTrack trk : data.tracks) {
-                        for (GpxTrackSegment segment : trk.getSegments()) {
-                            for (WayPoint trkPnt : segment.getWayPoints()) {
-                               double t=trkPnt.time;
-                               if (t==0) continue; // skip non-dated trackpoints
-                               if(t > maxval) maxval = t;
-                               if(t < minval) minval = t;
+                for (GpxTrack trk : data.tracks) {
+                    for (GpxTrackSegment segment : trk.getSegments()) {
+                        for (WayPoint trkPnt : segment.getWayPoints()) {
+                            double t=trkPnt.time;
+                            if (t==0) {
+                                continue; // skip non-dated trackpoints
                             }
-                        }
-                    }
-                }
-            
+                            if(t > maxval) {
+                                maxval = t;
+                            }
+                            if(t < minval) {
+                                minval = t;
+                            }
+                        }
+                    }
+                }
+            }
+
             for (GpxTrack trk : data.tracks) {
                 for (GpxTrackSegment segment : trk.getSegments()) {
@@ -522,5 +532,5 @@
                             float hdop = ((Float) trkPnt.attr.get("hdop")).floatValue();
                             int hdoplvl =(int) Math.round(colorModeDynamic ? ((hdop-minval)*255/(maxval-minval))
-                            : (hdop <= 0 ? 0 : hdop * hdopfactor));
+                                    : (hdop <= 0 ? 0 : hdop * hdopfactor));
                             // High hdop is bad, but high values in colors are green.
                             // Therefore inverse the logic
@@ -537,5 +547,5 @@
                                     float vel = (float) (dist / dtime);
                                     int velColor =(int) Math.round(colorModeDynamic ? ((vel-minval)*255/(maxval-minval))
-                                    : (vel <= 0 ? 0 : vel / colorTracksTune * 255));
+                                            : (vel <= 0 ? 0 : vel / colorTracksTune * 255));
                                     trkPnt.customColoring = colors[velColor > 255 ? 255 : velColor];
                                 } else {
@@ -561,5 +571,5 @@
                                 break;
                             }
-                            
+
                             if (!noDraw && (maxLineLength == -1 || dist <= maxLineLength)) {
                                 trkPnt.drawLine = true;
@@ -585,10 +595,12 @@
                 {
                     Bounds b = new Bounds(pt.getCoor());
-                    if(pt.drawLine) // last should never be null when this is true!
+                    // last should never be null when this is true!
+                    if(pt.drawLine) {
                         b.extend(last.getCoor());
+                    }
                     if(b.intersects(box))
                     {
                         if(last != null && (visibleSegments.isEmpty()
-                        || visibleSegments.getLast() != last)) {
+                                || visibleSegments.getLast() != last)) {
                             if(last.drawLine) {
                                 WayPoint l = new WayPoint(last);
@@ -646,5 +658,5 @@
                     if (old != null
                             && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta
-                                    || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) {
+                            || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) {
                         g.setColor(trkPnt.customColoring);
                         double t = Math.atan2(screen.y - old.y, screen.x - old.x) + Math.PI;
@@ -676,5 +688,5 @@
                     if (old != null
                             && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta
-                                    || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) {
+                            || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) {
                         g.setColor(trkPnt.customColoring);
                         g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][0], screen.y
@@ -859,5 +871,5 @@
                     JOptionPane.OK_CANCEL_OPTION,
                     JOptionPane.QUESTION_MESSAGE
-            );
+                    );
             switch(ret) {
             case JOptionPane.CANCEL_OPTION:
@@ -982,5 +994,5 @@
                         JOptionPane.OK_CANCEL_OPTION,
                         JOptionPane.PLAIN_MESSAGE
-                );
+                        );
                 switch(ret) {
                 case JOptionPane.CANCEL_OPTION:
@@ -1006,5 +1018,5 @@
                         }
                     }
-            );
+                    );
         }
     }
@@ -1049,5 +1061,10 @@
      */
     private void importAudio(File wavFile, MarkerLayer ml, double firstStartTime, Markers markers) {
-        String uri = "file:".concat(wavFile.getAbsolutePath());
+        URL url = null;
+        try {
+            url = wavFile.toURI().toURL();
+        } catch (MalformedURLException e) {
+            System.err.println("Unable to convert filename " + wavFile.getAbsolutePath() + " to URL");
+        }
         Collection<WayPoint> waypoints = new ArrayList<WayPoint>();
         boolean timedMarkersOmitted = false;
@@ -1084,5 +1101,5 @@
                     tr("Error"),
                     JOptionPane.ERROR_MESSAGE
-            );
+                    );
             return;
         }
@@ -1143,5 +1160,5 @@
             double startTime = lastModified - duration;
             startTime = firstStartTime + (startTime - firstStartTime)
-            / Main.pref.getDouble("audio.calibration", "1.0" /* default, ratio */);
+                    / Main.pref.getDouble("audio.calibration", "1.0" /* default, ratio */);
             WayPoint w1 = null;
             WayPoint w2 = null;
@@ -1219,13 +1236,5 @@
             }
             double offset = w.time - firstTime;
-            String name;
-            if (w.attr.containsKey("name")) {
-                name = w.getString("name");
-            } else if (w.attr.containsKey("desc")) {
-                name = w.getString("desc");
-            } else {
-                name = AudioMarker.inventName(offset);
-            }
-            AudioMarker am = AudioMarker.create(w.getCoor(), name, uri, ml, w.time, offset);
+            AudioMarker am = new AudioMarker(w.getCoor(), w, url, ml, w.time, offset);
             /*
              * timeFromAudio intended for future use to shift markers of this type on
@@ -1379,5 +1388,5 @@
             this();
             layers = new LinkedList<Layer>();
-            layers.add(l); 
+            layers.add(l);
         }
 
@@ -1410,9 +1419,12 @@
             for (Layer layer : layers) {
                 if (layer instanceof GpxLayer) {
-                    if (((GpxLayer) layer).isLocalFile) hasLocal = true;
-                    else hasNonlocal = true;
-                }
-            }
-            GPXSettingsPanel panel=new GPXSettingsPanel(getName(), hasLocal, hasNonlocal); 
+                    if (((GpxLayer) layer).isLocalFile) {
+                        hasLocal = true;
+                    } else {
+                        hasNonlocal = true;
+                    }
+                }
+            }
+            GPXSettingsPanel panel=new GPXSettingsPanel(getName(), hasLocal, hasNonlocal);
 
             int answer = JOptionPane.showConfirmDialog(Main.parent, panel,
@@ -1421,7 +1433,9 @@
             for(Layer layer : layers) {
                 // save preferences for all layers
-                boolean f=false; 
-                if (layer instanceof GpxLayer) f=((GpxLayer)layer).isLocalFile;
-                    panel.savePreferences(layer.getName(),f);
+                boolean f=false;
+                if (layer instanceof GpxLayer) {
+                    f=((GpxLayer)layer).isLocalFile;
+                }
+                panel.savePreferences(layer.getName(),f);
             }
             Main.map.repaint();
@@ -1468,5 +1482,5 @@
                     + "Because its way points do not include a timestamp we cannot correlate them with audio data.</html>",
                     layer.getName()
-            );
+                    );
             HelpAwareOptionPane.showOptionDialog(
                     Main.parent,
@@ -1475,5 +1489,5 @@
                     JOptionPane.WARNING_MESSAGE,
                     ht("/Action/ImportAudio#CantImportIntoGpxLayerFromServer")
-            );
+                    );
         }
 
@@ -1534,5 +1548,5 @@
                         getAssociatedFile(), GpxLayer.this);
                 double firstStartTime = sel[0].lastModified() / 1000.0 /* ms -> seconds */
-                - AudioUtil.getCalibratedDuration(sel[0]);
+                        - AudioUtil.getCalibratedDuration(sel[0]);
 
                 Markers m = new Markers();
@@ -1558,5 +1572,5 @@
                     + "Because its way points do not include a timestamp we cannot correlate them with images.</html>",
                     layer.getName()
-            );
+                    );
             HelpAwareOptionPane.showOptionDialog(
                     Main.parent,
@@ -1565,5 +1579,5 @@
                     JOptionPane.WARNING_MESSAGE,
                     ht("/Action/ImportImages#CantImportIntoGpxLayerFromServer")
-            );
+                    );
         }
 
Index: trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/AudioMarker.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/AudioMarker.java	(revision 4281)
+++ trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/AudioMarker.java	(revision 4282)
@@ -8,4 +8,5 @@
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.tools.AudioPlayer;
+import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider;
 
 /**
@@ -22,18 +23,6 @@
     public boolean timeFromAudio = false; // as opposed to from the GPX track
 
-    /**
-     * Verifies the parameter whether a new AudioMarker can be created and return
-     * one or return <code>null</code>.
-     */
-    public static AudioMarker create(LatLon ll, String text, String url, MarkerLayer parentLayer, double time, double offset) {
-        try {
-            return new AudioMarker(ll, text, new URL(url), parentLayer, time, offset);
-        } catch (Exception ex) {
-            return null;
-        }
-    }
-
-    private AudioMarker(LatLon ll, String text, URL audioUrl, MarkerLayer parentLayer, double time, double offset) {
-        super(ll, text, "speech.png", parentLayer, time, offset);
+    public AudioMarker(LatLon ll, TemplateEngineDataProvider dataProvider, URL audioUrl, MarkerLayer parentLayer, double time, double offset) {
+        super(ll, dataProvider, "speech.png", parentLayer, time, offset);
         this.audioUrl = audioUrl;
         this.syncOffset = 0.0;
@@ -84,12 +73,7 @@
     }
 
-    public static String inventName (double offset) {
-        int wholeSeconds = (int)(offset + 0.5);
-        if (wholeSeconds < 60)
-            return Integer.toString(wholeSeconds);
-        else if (wholeSeconds < 3600)
-            return String.format("%d:%02d", wholeSeconds / 60, wholeSeconds % 60);
-        else
-            return String.format("%d:%02d:%02d", wholeSeconds / 3600, (wholeSeconds % 3600)/60, wholeSeconds % 60);
+    @Override
+    protected TemplateEntryProperty getTextTemplate() {
+        return TemplateEntryProperty.forAudioMarker(parentLayer.getName());
     }
 }
Index: trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/ButtonMarker.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/ButtonMarker.java	(revision 4281)
+++ trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/ButtonMarker.java	(revision 4282)
@@ -14,4 +14,5 @@
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider;
 
 /**
@@ -26,10 +27,10 @@
 
     public ButtonMarker(LatLon ll, String buttonImage, MarkerLayer parentLayer, double time, double offset) {
-        super(ll, "", buttonImage, parentLayer, time, offset);
+        super(ll, null, buttonImage, parentLayer, time, offset);
         buttonRectangle = new Rectangle(0, 0, symbol.getIconWidth(), symbol.getIconHeight());
     }
 
-    public ButtonMarker(LatLon ll, String text, String buttonImage, MarkerLayer parentLayer, double time, double offset) {
-        super(ll, text, buttonImage, parentLayer, time, offset);
+    public ButtonMarker(LatLon ll, TemplateEngineDataProvider dataProvider, String buttonImage, MarkerLayer parentLayer, double time, double offset) {
+        super(ll, dataProvider, buttonImage, parentLayer, time, offset);
         buttonRectangle = new Rectangle(0, 0, symbol.getIconWidth(), symbol.getIconHeight());
     }
Index: trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/ImageMarker.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/ImageMarker.java	(revision 4281)
+++ trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/ImageMarker.java	(revision 4282)
@@ -34,13 +34,5 @@
     public URL imageUrl;
 
-    public static ImageMarker create(LatLon ll, String url, MarkerLayer parentLayer, double time, double offset) {
-        try {
-            return new ImageMarker(ll, new URL(url), parentLayer, time, offset);
-        } catch (Exception ex) {
-            return null;
-        }
-    }
-
-    private ImageMarker(LatLon ll, URL imageUrl, MarkerLayer parentLayer, double time, double offset) {
+    public ImageMarker(LatLon ll, URL imageUrl, MarkerLayer parentLayer, double time, double offset) {
         super(ll, "photo.png", parentLayer, time, 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 4281)
+++ trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/Marker.java	(revision 4282)
@@ -5,15 +5,18 @@
 import java.awt.Point;
 import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
 import java.io.File;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.LinkedList;
+import java.util.List;
 import java.util.Map;
 
 import javax.swing.Icon;
 
+import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
+import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
 import org.openstreetmap.josm.data.coor.CachedLatLon;
 import org.openstreetmap.josm.data.coor.EastNorth;
@@ -22,7 +25,12 @@
 import org.openstreetmap.josm.data.gpx.GpxLink;
 import org.openstreetmap.josm.data.gpx.WayPoint;
+import org.openstreetmap.josm.data.preferences.CachedProperty;
 import org.openstreetmap.josm.data.preferences.IntegerProperty;
 import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.template_engine.ParseError;
+import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider;
+import org.openstreetmap.josm.tools.template_engine.TemplateEntry;
+import org.openstreetmap.josm.tools.template_engine.TemplateParser;
 
 /**
@@ -61,34 +69,96 @@
  * @author Frederik Ramm <frederik@remote.org>
  */
-public class Marker implements ActionListener {
-    public final String text;
-    public final Map<String,String> textMap = new HashMap<String,String>();
-    public final Icon symbol;
-    public final MarkerLayer parentLayer;
-    public double time; /* absolute time of marker since epoch */
-    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 */
-
-    private CachedLatLon coor;
-
-    public final void setCoor(LatLon coor) {
-        if(this.coor == null) {
-            this.coor = new CachedLatLon(coor);
-        } else {
-            this.coor.setCoor(coor);
-        }
-    }
-
-    public final LatLon getCoor() {
-        return coor;
-    }
-
-    public final void setEastNorth(EastNorth eastNorth) {
-        coor.setEastNorth(eastNorth);
-    }
-
-    public final EastNorth getEastNorth() {
-        return coor.getEastNorth();
-    }
+public class Marker implements TemplateEngineDataProvider {
+
+    public static class TemplateEntryProperty extends CachedProperty<TemplateEntry> {
+        // This class is a bit complicated because it supports both global and per layer settings. I've added per layer settings because
+        // GPXSettingsPanel had possibility to set waypoint label but then I've realized that markers use different layer then gpx data
+        // so per layer settings is useless. Anyway it's possible to specify marker layer pattern in Einstein preferences and maybe somebody
+        // will make gui for it so I'm keeping it here
+
+        private final static Map<String, TemplateEntryProperty> cache = new HashMap<String, Marker.TemplateEntryProperty>();
+
+        // Legacy code - convert label from int to template engine expression
+        private static final IntegerProperty PROP_LABEL = new IntegerProperty("draw.rawgps.layer.wpt", 0 );
+        private static String getDefaultLabelPattern() {
+            switch (PROP_LABEL.get()) {
+            case 1:
+                return LABEL_PATTERN_NAME;
+            case 2:
+                return LABEL_PATTERN_DESC;
+            case 0:
+            case 3:
+                return LABEL_PATTERN_AUTO;
+            default:
+                return "";
+            }
+        }
+
+        public static TemplateEntryProperty forMarker(String layerName) {
+            String key = "draw.rawgps.layer.wpt.pattern";
+            if (layerName != null) {
+                key += "." + layerName;
+            }
+            TemplateEntryProperty result = cache.get(key);
+            if (result == null) {
+                String defaultValue = layerName == null?getDefaultLabelPattern():"";
+                TemplateEntryProperty parent = layerName == null?null:forMarker(null);
+                result = new TemplateEntryProperty(key, defaultValue, parent);
+                cache.put(key, result);
+            }
+            return result;
+        }
+
+        public static TemplateEntryProperty forAudioMarker(String layerName) {
+            String key = "draw.rawgps.layer.audiowpt.pattern";
+            if (layerName != null) {
+                key += "." + layerName;
+            }
+            TemplateEntryProperty result = cache.get(key);
+            if (result == null) {
+                String defaultValue = layerName == null?"?{ '{name}' | '{desc}' | '{" + Marker.MARKER_FORMATTED_OFFSET + "}' }":"";
+                TemplateEntryProperty parent = layerName == null?null:forAudioMarker(null);
+                result = new TemplateEntryProperty(key, defaultValue, parent);
+                cache.put(key, result);
+            }
+            return result;
+        }
+
+        private TemplateEntryProperty parent;
+
+
+        private TemplateEntryProperty(String key, String defaultValue, TemplateEntryProperty parent) {
+            super(key, defaultValue);
+            this.parent = parent;
+            updateValue(); // Needs to be called because parent wasn't know in super constructor
+        }
+
+        @Override
+        protected TemplateEntry fromString(String s) {
+            try {
+                return new TemplateParser(s).parse();
+            } catch (ParseError e) {
+                System.out.println(String.format("Unable to parse template engine pattern '%s' for property %s. Using default ('%s') instead",
+                        s, getKey(), defaultValue));
+                return getDefaultValue();
+            }
+        }
+
+        @Override
+        public String getDefaultValueAsString() {
+            if (parent == null)
+                return super.getDefaultValueAsString();
+            else
+                return parent.getAsString();
+        }
+
+        @Override
+        public void preferenceChanged(PreferenceChangeEvent e) {
+            if (e.getKey().equals(key) || (parent != null && e.getKey().equals(parent.getKey()))) {
+                updateValue();
+            }
+        }
+    }
+
 
     /**
@@ -97,8 +167,5 @@
      * stuff).
      */
-    public static LinkedList<MarkerProducers> markerProducers = new LinkedList<MarkerProducers>();
-
-    private static final IntegerProperty PROP_LABEL = new IntegerProperty("draw.rawgps.layer.wpt", 0 );
-    private static final String[] labelAttributes = new String[] {"name", "desc"};
+    public static List<MarkerProducers> markerProducers = new LinkedList<MarkerProducers>();
 
     // Add one Maker specifying the default behaviour.
@@ -110,118 +177,44 @@
                 // cheapest way to check whether "link" object exists and is a non-empty
                 // collection of GpxLink objects...
-                try {
-                    for (GpxLink oneLink : (Collection<GpxLink>) wpt.attr.get(GpxData.META_LINKS)) {
+                Collection<GpxLink> links = (Collection<GpxLink>)wpt.attr.get(GpxData.META_LINKS);
+                if (links != null) {
+                    for (GpxLink oneLink : links ) {
                         uri = oneLink.uri;
                         break;
                     }
-                } catch (Exception ex) {}
-
-                // Try a relative file:// url, if the link is not in an URL-compatible form
-                if (relativePath != null && uri != null && !isWellFormedAddress(uri)) {
-                    uri = new File(relativePath.getParentFile(), uri).toURI().toString();
                 }
 
-                Map<String,String> nameDesc = new HashMap<String,String>();
-                for(String attribute : labelAttributes) {
-                    if (wpt.attr.containsKey(attribute)) {
-                        nameDesc.put(attribute, wpt.getString(attribute));
+                URL url = null;
+                if (uri != null) {
+                    try {
+                        url = new URL(uri);
+                    } catch (MalformedURLException e) {
+                        // Try a relative file:// url, if the link is not in an URL-compatible form
+                        if (relativePath != null) {
+                            try {
+                                url = new File(relativePath.getParentFile(), uri).toURI().toURL();
+                            } catch (MalformedURLException e1) {
+                                System.err.println("Unable to convert uri " + uri + " to URL: "  + e1.getMessage());
+                            }
+                        }
                     }
                 }
 
-                if (uri == null) {
+
+                if (url == null) {
                     String symbolName = wpt.getString("symbol");
                     if (symbolName == null) {
                         symbolName = wpt.getString("sym");
                     }
-                    return new Marker(wpt.getCoor(), nameDesc, symbolName, parentLayer, time, offset);
+                    return new Marker(wpt.getCoor(), wpt, symbolName, parentLayer, time, offset);
                 }
-                else if (uri.endsWith(".wav"))
-                    return AudioMarker.create(wpt.getCoor(), getText(nameDesc), uri, parentLayer, time, offset);
-                else if (uri.endsWith(".png") || uri.endsWith(".jpg") || uri.endsWith(".jpeg") || uri.endsWith(".gif"))
-                    return ImageMarker.create(wpt.getCoor(), uri, parentLayer, time, offset);
+                else if (url.toString().endsWith(".wav"))
+                    return new AudioMarker(wpt.getCoor(), wpt, url, parentLayer, time, offset);
+                else if (url.toString().endsWith(".png") || url.toString().endsWith(".jpg") || url.toString().endsWith(".jpeg") || url.toString().endsWith(".gif"))
+                    return new ImageMarker(wpt.getCoor(), url, parentLayer, time, offset);
                 else
-                    return WebMarker.create(wpt.getCoor(), uri, parentLayer, time, offset);
-            }
-
-            private boolean isWellFormedAddress(String link) {
-                try {
-                    new URL(link);
-                    return true;
-                } catch (MalformedURLException x) {
-                    return false;
-                }
+                    return new WebMarker(wpt.getCoor(), url, parentLayer, time, offset);
             }
         });
-    }
-
-    public Marker(LatLon ll, String text, String iconName, MarkerLayer parentLayer, double time, double offset) {
-        setCoor(ll);
-        if (text == null || text.length() == 0) {
-            this.text = null;
-        }
-        else {
-            this.text = text;
-        }
-        this.offset = offset;
-        this.time = time;
-        this.symbol = ImageProvider.getIfAvailable("markers",iconName);
-        this.parentLayer = parentLayer;
-    }
-
-    public Marker(LatLon ll, Map<String,String> textMap, String iconName, MarkerLayer parentLayer, double time, double offset) {
-        setCoor(ll);
-        if (textMap != null) {
-            this.textMap.clear();
-            this.textMap.putAll(textMap);
-        }
-
-        this.text = null;
-        this.offset = offset;
-        this.time = time;
-        // /* ICON(markers/) */"Bridge"
-        // /* ICON(markers/) */"Crossing"
-        this.symbol = ImageProvider.getIfAvailable("markers",iconName);
-        this.parentLayer = parentLayer;
-    }
-
-    /**
-     * Checks whether the marker display area contains the given point.
-     * Markers not interested in mouse clicks may always return false.
-     *
-     * @param p The point to check
-     * @return <code>true</code> if the marker "hotspot" contains the point.
-     */
-    public boolean containsPoint(Point p) {
-        return false;
-    }
-
-    /**
-     * Called when the mouse is clicked in the marker's hotspot. Never
-     * called for markers which always return false from containsPoint.
-     *
-     * @param ev A dummy ActionEvent
-     */
-    public void actionPerformed(ActionEvent ev) {
-    }
-
-    /**
-     * Paints the marker.
-     * @param g graphics context
-     * @param mv map view
-     * @param mousePressed true if the left mouse button is pressed
-     */
-    public void paint(Graphics g, MapView mv, boolean mousePressed, boolean showTextOrIcon) {
-        Point screen = mv.getPoint(getEastNorth());
-        if (symbol != null && showTextOrIcon) {
-            symbol.paintIcon(mv, g, screen.x-symbol.getIconWidth()/2, screen.y-symbol.getIconHeight()/2);
-        } else {
-            g.drawLine(screen.x-2, screen.y-2, screen.x+2, screen.y+2);
-            g.drawLine(screen.x+2, screen.y-2, screen.x-2, screen.y+2);
-        }
-
-        String labelText = getText();
-        if ((labelText != null) && showTextOrIcon) {
-            g.drawString(labelText, screen.x+4, screen.y+2);
-        }
     }
 
@@ -246,17 +239,102 @@
     }
 
-    /**
-     * 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.
+    public static final String MARKER_OFFSET = "waypointOffset";
+    public static final String MARKER_FORMATTED_OFFSET = "formattedWaypointOffset";
+
+    public static final String LABEL_PATTERN_AUTO = "?{ '{name} - {desc}' | '{name}' | '{desc}' }";
+    public static final String LABEL_PATTERN_NAME = "{name}";
+    public static final String LABEL_PATTERN_DESC = "{desc}";
+
+
+    private final TemplateEngineDataProvider dataProvider;
+    public final Icon symbol;
+    public final MarkerLayer parentLayer;
+    public double time; /* absolute time of marker since epoch */
+    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 */
+
+    private String cachedText;
+    private int textVersion = -1;
+    private CachedLatLon coor;
+
+    public Marker(LatLon ll, TemplateEngineDataProvider dataProvider, String iconName, MarkerLayer parentLayer, double time, double offset) {
+        setCoor(ll);
+
+        this.offset = offset;
+        this.time = time;
+        // /* ICON(markers/) */"Bridge"
+        // /* ICON(markers/) */"Crossing"
+        this.symbol = ImageProvider.getIfAvailable("markers",iconName);
+        this.parentLayer = parentLayer;
+
+        this.dataProvider = dataProvider;
+    }
+
+    public final void setCoor(LatLon coor) {
+        if(this.coor == null) {
+            this.coor = new CachedLatLon(coor);
+        } else {
+            this.coor.setCoor(coor);
+        }
+    }
+
+    public final LatLon getCoor() {
+        return coor;
+    }
+
+    public final void setEastNorth(EastNorth eastNorth) {
+        coor.setEastNorth(eastNorth);
+    }
+
+    public final EastNorth getEastNorth() {
+        return coor.getEastNorth();
+    }
+
+
+    /**
+     * Checks whether the marker display area contains the given point.
+     * Markers not interested in mouse clicks may always return false.
      *
-     * @param uri uri of wave file
-     * @return AudioMarker
-     */
-
-    public AudioMarker audioMarkerFromMarker(String uri) {
-        AudioMarker audioMarker = AudioMarker.create(getCoor(), this.getText(), uri, this.parentLayer, this.time, this.offset);
-        return audioMarker;
+     * @param p The point to check
+     * @return <code>true</code> if the marker "hotspot" contains the point.
+     */
+    public boolean containsPoint(Point p) {
+        return false;
+    }
+
+    /**
+     * Called when the mouse is clicked in the marker's hotspot. Never
+     * called for markers which always return false from containsPoint.
+     *
+     * @param ev A dummy ActionEvent
+     */
+    public void actionPerformed(ActionEvent ev) {
+    }
+
+
+    /**
+     * Paints the marker.
+     * @param g graphics context
+     * @param mv map view
+     * @param mousePressed true if the left mouse button is pressed
+     */
+    public void paint(Graphics g, MapView mv, boolean mousePressed, boolean showTextOrIcon) {
+        Point screen = mv.getPoint(getEastNorth());
+        if (symbol != null && showTextOrIcon) {
+            symbol.paintIcon(mv, g, screen.x-symbol.getIconWidth()/2, screen.y-symbol.getIconHeight()/2);
+        } else {
+            g.drawLine(screen.x-2, screen.y-2, screen.x+2, screen.y+2);
+            g.drawLine(screen.x+2, screen.y-2, screen.x-2, screen.y+2);
+        }
+
+        String labelText = getText();
+        if ((labelText != null) && showTextOrIcon) {
+            g.drawString(labelText, screen.x+4, screen.y+2);
+        }
+    }
+
+
+    protected TemplateEntryProperty getTextTemplate() {
+        return TemplateEntryProperty.forMarker(parentLayer.getName());
     }
 
@@ -266,72 +344,54 @@
      */
     public String getText() {
-        if (this.text != null )
-            return this.text;
+        TemplateEntryProperty property = getTextTemplate();
+        if (property.getUpdateCount() != textVersion) {
+            TemplateEntry templateEntry = property.get();
+            StringBuilder sb = new StringBuilder();
+            templateEntry.appendText(sb, this);
+
+            cachedText = sb.toString();
+            textVersion = property.getUpdateCount();
+        }
+        return cachedText;
+    }
+
+    @Override
+    public List<String> getTemplateKeys() {
+        List<String> result;
+        if (dataProvider != null) {
+            result = dataProvider.getTemplateKeys();
+        } else {
+            result = new ArrayList<String>();
+        }
+        result.add(MARKER_FORMATTED_OFFSET);
+        result.add(MARKER_OFFSET);
+        return result;
+    }
+
+    private String formatOffset () {
+        int wholeSeconds = (int)(offset + 0.5);
+        if (wholeSeconds < 60)
+            return Integer.toString(wholeSeconds);
+        else if (wholeSeconds < 3600)
+            return String.format("%d:%02d", wholeSeconds / 60, wholeSeconds % 60);
         else
-            return getText(this.textMap);
-    }
-
-    /**
-     * Returns the Text which should be displayed, depending on chosen preference.
-     * The possible attributes are read from textMap.
-     *
-     * @param textMap A map with available texts/attributes
-     * @return Text
-     */
-    private static String getText(Map<String,String> textMap) {
-        String text = "";
-
-        if (textMap != null && !textMap.isEmpty()) {
-            switch(PROP_LABEL.get())
-            {
-                // name
-                case 1:
-                {
-                    if (textMap.containsKey("name")) {
-                        text = textMap.get("name");
-                    }
-                    break;
-                }
-
-                // desc
-                case 2:
-                {
-                    if (textMap.containsKey("desc")) {
-                        text = textMap.get("desc");
-                    }
-                    break;
-                }
-
-                // auto
-                case 0:
-                // both
-                case 3:
-                {
-                    if (textMap.containsKey("name")) {
-                        text = textMap.get("name");
-
-                        if (textMap.containsKey("desc")) {
-                            if (PROP_LABEL.get() != 0 || !text.equals(textMap.get("desc"))) {
-                                text += " - " + textMap.get("desc");
-                            }
-                        }
-                    }
-                    else if (textMap.containsKey("desc")) {
-                        text = textMap.get("desc");
-                    }
-                    break;
-                }
-
-                // none
-                case 4:
-                default:
-                {
-                    text = "";
-                    break;
-                }
-            }
-        }
-
-        return text;
+            return String.format("%d:%02d:%02d", wholeSeconds / 3600, (wholeSeconds % 3600)/60, wholeSeconds % 60);
+    }
+
+    @Override
+    public Object getTemplateValue(String name) {
+        if (MARKER_FORMATTED_OFFSET.equals(name))
+            return formatOffset();
+        else if (MARKER_OFFSET.equals(name))
+            return offset;
+        else if (dataProvider != null)
+            return dataProvider.getTemplateValue(name);
+        else
+            return null;
+    }
+
+    @Override
+    public boolean evaluateCondition(Match condition) {
+        throw new UnsupportedOperationException();
     }
 }
Index: trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayer.java	(revision 4281)
+++ trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayer.java	(revision 4282)
@@ -32,5 +32,4 @@
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.gpx.GpxData;
-import org.openstreetmap.josm.data.gpx.GpxLink;
 import org.openstreetmap.josm.data.gpx.WayPoint;
 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
@@ -72,25 +71,9 @@
         this.fromLayer = fromLayer;
         double firstTime = -1.0;
-        String lastLinkedFile = "";
 
         for (WayPoint wpt : indata.waypoints) {
-            /* calculate time differences in waypoints */
             double time = wpt.time;
-            boolean wpt_has_link = wpt.attr.containsKey(GpxData.META_LINKS);
-            if (firstTime < 0 && wpt_has_link) {
+            if (firstTime < 0) {
                 firstTime = time;
-                for (GpxLink oneLink : (Collection<GpxLink>) wpt.attr.get(GpxData.META_LINKS)) {
-                    lastLinkedFile = oneLink.uri;
-                    break;
-                }
-            }
-            if (wpt_has_link) {
-                for (GpxLink oneLink : (Collection<GpxLink>) wpt.attr.get(GpxData.META_LINKS)) {
-                    if (!oneLink.uri.equals(lastLinkedFile)) {
-                        firstTime = time;
-                    }
-                    lastLinkedFile = oneLink.uri;
-                    break;
-                }
             }
             Marker m = Marker.createMarker(wpt, indata.storageFile, this, time, time - firstTime);
@@ -274,11 +257,11 @@
                     tr("Error"),
                     JOptionPane.ERROR_MESSAGE
-            );
+                    );
             return null;
         }
 
         // make our new marker
-        AudioMarker newAudioMarker = AudioMarker.create(coor,
-                AudioMarker.inventName(offset), AudioPlayer.url().toString(), this, time, offset);
+        AudioMarker newAudioMarker = new AudioMarker(coor,
+                null, AudioPlayer.url(), this, time, offset);
 
         // insert it at the right place in a copy the collection
@@ -425,5 +408,5 @@
                         tr("Warning"),
                         JOptionPane.WARNING_MESSAGE
-                );
+                        );
                 return;
             }
@@ -432,8 +415,8 @@
                 JOptionPane.showMessageDialog(
                         Main.parent,
-                        tr("Audio synchronized at point {0}.", recent.text),
+                        tr("Audio synchronized at point {0}.", recent.getText()),
                         tr("Information"),
                         JOptionPane.INFORMATION_MESSAGE
-                );
+                        );
             } else {
                 JOptionPane.showMessageDialog(
@@ -442,5 +425,5 @@
                         tr("Error"),
                         JOptionPane.ERROR_MESSAGE
-                );
+                        );
             }
         }
@@ -462,5 +445,5 @@
                         tr("Warning"),
                         JOptionPane.WARNING_MESSAGE
-                );
+                        );
                 return;
             }
Index: trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/PlayHeadMarker.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/PlayHeadMarker.java	(revision 4281)
+++ trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/PlayHeadMarker.java	(revision 4282)
@@ -59,5 +59,5 @@
 
     private PlayHeadMarker() {
-        super(new LatLon(0.0,0.0), "",
+        super(new LatLon(0.0,0.0), null,
                 Main.pref.get("marker.audiotracericon", "audio-tracer"),
                 null, -1.0, 0.0);
@@ -171,5 +171,5 @@
                     tr("Warning"),
                     JOptionPane.WARNING_MESSAGE
-            );
+                    );
             endDrag(true);
         } else {
@@ -226,5 +226,5 @@
                         tr("Warning"),
                         JOptionPane.WARNING_MESSAGE
-                );
+                        );
                 endDrag(true);
                 return;
@@ -241,5 +241,5 @@
                     tr("Error"),
                     JOptionPane.ERROR_MESSAGE
-            );
+                    );
             endDrag(true);
         }
@@ -247,8 +247,8 @@
             JOptionPane.showMessageDialog(
                     Main.parent,
-                    tr("Audio synchronized at point {0}.", ca.text),
+                    tr("Audio synchronized at point {0}.", ca.getText()),
                     tr("Information"),
                     JOptionPane.INFORMATION_MESSAGE
-            );
+                    );
             setCoor(ca.getCoor());
             endDrag(false);
@@ -259,5 +259,5 @@
                     tr("Error"),
                     JOptionPane.ERROR_MESSAGE
-            );
+                    );
             endDrag(true);
         }
@@ -294,7 +294,7 @@
             return;
         double audioTime = recentlyPlayedMarker.time +
-        AudioPlayer.position() -
-        recentlyPlayedMarker.offset -
-        recentlyPlayedMarker.syncOffset;
+                AudioPlayer.position() -
+                recentlyPlayedMarker.offset -
+                recentlyPlayedMarker.syncOffset;
         if (Math.abs(audioTime - time) < animationInterval)
             return;
Index: trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/WebMarker.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/WebMarker.java	(revision 4281)
+++ trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/WebMarker.java	(revision 4282)
@@ -21,15 +21,7 @@
 public class WebMarker extends ButtonMarker {
 
-    public URL webUrl;
+    public final URL webUrl;
 
-    public static WebMarker create (LatLon ll, String url, MarkerLayer parentLayer, double time, double offset) {
-        try {
-            return new WebMarker(ll, new URL(url), parentLayer, time, offset);
-        } catch (Exception ex) {
-            return null;
-        }
-    }
-
-    private WebMarker(LatLon ll, URL webUrl, MarkerLayer parentLayer, double time, double offset) {
+    public WebMarker(LatLon ll, URL webUrl, MarkerLayer parentLayer, double time, double offset) {
         super(ll, "web.png", parentLayer, time, offset);
         this.webUrl = webUrl;
@@ -41,7 +33,7 @@
             JOptionPane.showMessageDialog(Main.parent,
                     "<html><b>" +
-                    tr("There was an error while trying to display the URL for this marker") +
-                    "</b><br>" + tr("(URL was: ") + webUrl.toString() + ")" + "<br>" + error,
-                    tr("Error displaying URL"), JOptionPane.ERROR_MESSAGE);
+                            tr("There was an error while trying to display the URL for this marker") +
+                            "</b><br>" + tr("(URL was: ") + webUrl.toString() + ")" + "<br>" + error,
+                            tr("Error displaying URL"), JOptionPane.ERROR_MESSAGE);
         }
     }
Index: trunk/src/org/openstreetmap/josm/gui/preferences/AudioPreference.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/preferences/AudioPreference.java	(revision 4281)
+++ trunk/src/org/openstreetmap/josm/gui/preferences/AudioPreference.java	(revision 4282)
@@ -60,5 +60,5 @@
 
         // buttonLabels
-        markerButtonLabels.setSelected(Main.pref.getBoolean("marker.buttonlabels"));
+        markerButtonLabels.setSelected(Main.pref.getBoolean("marker.buttonlabels", true));
         markerButtonLabels.setToolTipText(tr("Put text labels against audio (and image and web) markers as well as their button icons."));
         gui.audio.add(markerButtonLabels, GBC.eol().insets(0,0,0,0));
Index: trunk/src/org/openstreetmap/josm/gui/preferences/GPXSettingsPanel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/preferences/GPXSettingsPanel.java	(revision 4281)
+++ trunk/src/org/openstreetmap/josm/gui/preferences/GPXSettingsPanel.java	(revision 4282)
@@ -1,26 +1,37 @@
 package org.openstreetmap.josm.gui.preferences;
 
-import javax.swing.AbstractButton;
+import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trc;
+
+import java.awt.GridBagLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.BorderFactory;
 import javax.swing.Box;
-import javax.swing.event.ChangeEvent;
-import javax.swing.event.ChangeListener;
-import java.awt.event.ActionEvent;
-import javax.swing.JLabel;
-import javax.swing.BorderFactory;
-import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.tools.GBC;
-import java.awt.GridBagLayout;
-import java.awt.event.ActionListener;
 import javax.swing.ButtonGroup;
 import javax.swing.JCheckBox;
 import javax.swing.JComboBox;
+import javax.swing.JLabel;
 import javax.swing.JPanel;
 import javax.swing.JRadioButton;
 import javax.swing.JTextField;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-import static org.openstreetmap.josm.tools.I18n.trc;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.layer.markerlayer.Marker;
+import org.openstreetmap.josm.gui.layer.markerlayer.Marker.TemplateEntryProperty;
+import org.openstreetmap.josm.tools.GBC;
 
 public class GPXSettingsPanel extends JPanel {
+
+    private static final int WAYPOINT_LABEL_CUSTOM = 6;
+    private static final String[] LABEL_PATTERN_TEMPLATE = new String[] {Marker.LABEL_PATTERN_AUTO, Marker.LABEL_PATTERN_NAME,
+        Marker.LABEL_PATTERN_DESC, "{*}", "?{ '{name}' | '{desc}' | '{formattedWaypointOffset}' }", ""};
+    private static final String[] LABEL_PATTERN_DESC = new String[] {tr("Auto"), /* gpx data field name */ trc("gpx_field", "Name"),
+        /* gpx data field name */ trc("gpx_field", "Desc(ription)"), tr("Everything"), tr("Name or offset"), tr("None"), tr("Custom")};
+
+
     private JRadioButton drawRawGpsLinesGlobal = new JRadioButton(tr("Use global settings."));
     private JRadioButton drawRawGpsLinesAll = new JRadioButton(tr("All"));
@@ -46,9 +57,12 @@
     private JTextField drawGpsArrowsMinDist = new JTextField(8);
     private JCheckBox colorDynamic = new JCheckBox(tr("Dynamic color range based on data limits"));
-    private JComboBox waypointLabel = new JComboBox(new String[] {tr("Auto"), /* gpx data field name */ trc("gpx_field", "Name"),
-            /* gpx data field name */ trc("gpx_field", "Desc(ription)"), tr("Both"), tr("None")});
+    private JComboBox waypointLabel = new JComboBox(LABEL_PATTERN_DESC);
+    private JTextField waypointLabelPattern = new JTextField();
+    private JComboBox audioWaypointLabel = new JComboBox(LABEL_PATTERN_DESC);
+    private JTextField audioWaypointLabelPattern = new JTextField();
+
     private String layerName;
-    private boolean local; // flag to display LocalOnly checkbox 
-    private boolean nonlocal; // flag to display AllLines checkbox 
+    private boolean local; // flag to display LocalOnly checkbox
+    private boolean nonlocal; // flag to display AllLines checkbox
 
     public GPXSettingsPanel(String layerName, boolean local, boolean nonlocal) {
@@ -76,5 +90,7 @@
         // drawRawGpsLines
         ButtonGroup gpsLinesGroup = new ButtonGroup();
-        if (layerName!=null) gpsLinesGroup.add(drawRawGpsLinesGlobal);
+        if (layerName!=null) {
+            gpsLinesGroup.add(drawRawGpsLinesGlobal);
+        }
         gpsLinesGroup.add(drawRawGpsLinesNone);
         gpsLinesGroup.add(drawRawGpsLinesLocal);
@@ -84,8 +100,14 @@
 
         add(new JLabel(tr("Draw lines between raw GPS points")), GBC.eol().insets(20,0,0,0));
-        if (layerName!=null) add(drawRawGpsLinesGlobal, GBC.eol().insets(40,0,0,0));
+        if (layerName!=null) {
+            add(drawRawGpsLinesGlobal, GBC.eol().insets(40,0,0,0));
+        }
         add(drawRawGpsLinesNone, GBC.eol().insets(40,0,0,0));
-        if (layerName==null || local) add(drawRawGpsLinesLocal, GBC.eol().insets(40,0,0,0)); 
-        if (layerName==null || nonlocal) add(drawRawGpsLinesAll, GBC.eol().insets(40,0,0,0)); 
+        if (layerName==null || local) {
+            add(drawRawGpsLinesLocal, GBC.eol().insets(40,0,0,0));
+        }
+        if (layerName==null || nonlocal) {
+            add(drawRawGpsLinesAll, GBC.eol().insets(40,0,0,0));
+        }
 
         drawRawGpsLinesActionListener = new ActionListener(){
@@ -149,5 +171,7 @@
         // colorTracks
         colorGroup = new ButtonGroup();
-        if (layerName!=null) colorGroup.add(colorTypeGlobal);
+        if (layerName!=null) {
+            colorGroup.add(colorTypeGlobal);
+        }
         colorGroup.add(colorTypeNone);
         colorGroup.add(colorTypeVelocity);
@@ -180,5 +204,7 @@
 
         add(new JLabel(tr("Track and Point Coloring")), GBC.eol().insets(20,0,0,0));
-        if (layerName!=null) add(colorTypeGlobal, GBC.eol().insets(40,0,0,0));
+        if (layerName!=null) {
+            add(colorTypeGlobal, GBC.eol().insets(40,0,0,0));
+        }
         add(colorTypeNone, GBC.eol().insets(40,0,0,0));
         add(colorTypeVelocity, GBC.std().insets(40,0,0,0));
@@ -191,10 +217,35 @@
         add(colorDynamic, GBC.eop().insets(40,0,0,0));
 
-        // waypointLabel
-        add(Box.createVerticalGlue(), GBC.eol().insets(0, 20, 0, 0));
-        add(new JLabel(tr("Waypoint labelling")), GBC.std().insets(20,0,0,0));
-        if(layerName!= null)
-            waypointLabel.addItem(tr("Global settings"));
-        add(waypointLabel, GBC.eol().fill(GBC.HORIZONTAL).insets(5,0,0,5));
+        if (layerName == null) {
+            // Setting waypoints for gpx layer doesn't make sense - waypoints are shown in marker layer that has different name - so show
+            // this only for global config
+
+            // waypointLabel
+            add(new JLabel(tr("Waypoint labelling")), GBC.std().insets(20,0,0,0));
+            add(waypointLabel, GBC.eol().fill(GBC.HORIZONTAL).insets(5,0,0,5));
+            waypointLabel.addActionListener(new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    updateWaypointPattern(waypointLabel, waypointLabelPattern);
+                }
+            });
+            updateWaypointLabelCombobox(waypointLabel, waypointLabelPattern, TemplateEntryProperty.forMarker(layerName));
+            add(waypointLabelPattern, GBC.eol().fill(GBC.HORIZONTAL).insets(20,0,0,5));
+
+            // audioWaypointLabel
+            add(Box.createVerticalGlue(), GBC.eol().insets(0, 20, 0, 0));
+
+            add(new JLabel(tr("Audio waypoint labelling")), GBC.std().insets(20,0,0,0));
+            add(audioWaypointLabel, GBC.eol().fill(GBC.HORIZONTAL).insets(5,0,0,5));
+            audioWaypointLabel.addActionListener(new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    updateWaypointPattern(audioWaypointLabel, audioWaypointLabelPattern);
+                }
+            });
+            updateWaypointLabelCombobox(audioWaypointLabel, audioWaypointLabelPattern, TemplateEntryProperty.forAudioMarker(layerName));
+            add(audioWaypointLabelPattern, GBC.eol().fill(GBC.HORIZONTAL).insets(20,0,0,5));
+        }
+
         add(Box.createVerticalGlue(), GBC.eol().fill(GBC.BOTH));
     }
@@ -235,5 +286,5 @@
             colorDynamic.setEnabled(false);
         } else {
-         switch(Main.pref.getInteger("draw.rawgps.colors",layerName, 0)) {
+            switch(Main.pref.getInteger("draw.rawgps.colors",layerName, 0)) {
             case 0: colorTypeNone.setSelected(true);   break;
             case 1: colorTypeVelocity.setSelected(true);  break;
@@ -248,8 +299,4 @@
             colorDynamic.setEnabled(colorTypeVelocity.isSelected() || colorTypeDilution.isSelected());
         }
-        if(layerName != null)
-          waypointLabel.setSelectedIndex(Main.pref.getInteger("draw.rawgps.layer.wpt."+layerName, 5));
-        else
-          waypointLabel.setSelectedIndex(Main.pref.getInteger("draw.rawgps.layer.wpt", 0));
     }
 
@@ -261,5 +308,7 @@
     public boolean savePreferences (String layerName, boolean locLayer) {
         String layerNameDot = ".layer "+layerName;
-        if (layerName==null) layerNameDot="";
+        if (layerName==null) {
+            layerNameDot="";
+        }
         Main.pref.put("marker.makeautomarkers"+layerNameDot, makeAutoMarkers.isSelected());
         if (drawRawGpsLinesGlobal.isSelected()) {
@@ -289,6 +338,7 @@
         Main.pref.put("draw.rawgps.hdopcircle"+layerNameDot, hdopCircleGpsPoints.isSelected());
         Main.pref.put("draw.rawgps.large"+layerNameDot, largeGpsPoints.isSelected());
-        if (waypointLabel.getSelectedIndex()==5) Main.pref.put("draw.rawgps.layer.wpt"+layerNameDot,null);
-        else Main.pref.putInteger("draw.rawgps.layer.wpt"+layerNameDot, waypointLabel.getSelectedIndex());
+
+        TemplateEntryProperty.forMarker(layerName).put(waypointLabelPattern.getText());
+        TemplateEntryProperty.forAudioMarker(layerName).put(audioWaypointLabelPattern.getText());
 
         if(colorTypeGlobal.isSelected()) {
@@ -320,3 +370,30 @@
         savePreferences(null, false);
     }
+
+    private void updateWaypointLabelCombobox(JComboBox cb, JTextField tf, TemplateEntryProperty property) {
+        String labelPattern = property.getAsString();
+        boolean found = false;
+        for (int i=0; i<LABEL_PATTERN_TEMPLATE.length; i++) {
+            if (LABEL_PATTERN_TEMPLATE[i].equals(labelPattern)) {
+                cb.setSelectedIndex(i);
+                found = true;
+                break;
+            }
+        }
+        if (!found) {
+            cb.setSelectedIndex(WAYPOINT_LABEL_CUSTOM);
+            tf.setEnabled(true);
+            tf.setText(labelPattern);
+        }
+    }
+
+    private void updateWaypointPattern(JComboBox cb, JTextField tf) {
+        if (cb.getSelectedIndex() == WAYPOINT_LABEL_CUSTOM) {
+            tf.setEnabled(true);
+        } else {
+            tf.setEnabled(false);
+            tf.setText(LABEL_PATTERN_TEMPLATE[cb.getSelectedIndex()]);
+        }
+    }
+
 }
Index: trunk/src/org/openstreetmap/josm/tools/template_engine/CompoundTemplateEntry.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/template_engine/CompoundTemplateEntry.java	(revision 4282)
+++ trunk/src/org/openstreetmap/josm/tools/template_engine/CompoundTemplateEntry.java	(revision 4282)
@@ -0,0 +1,49 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools.template_engine;
+
+
+public class CompoundTemplateEntry implements TemplateEntry {
+
+    public static TemplateEntry fromArray(TemplateEntry... entry) {
+        if (entry.length == 0)
+            return new StaticText("");
+        else if (entry.length == 1)
+            return entry[0];
+        else
+            return new CompoundTemplateEntry(entry);
+    }
+
+    private CompoundTemplateEntry(TemplateEntry[] entries) {
+        this.entries = entries;
+    }
+
+    private final TemplateEntry[] entries;
+
+    @Override
+    public void appendText(StringBuilder result, TemplateEngineDataProvider dataProvider) {
+        for (TemplateEntry te: entries) {
+            te.appendText(result, dataProvider);
+        }
+    }
+
+    @Override
+    public boolean isValid(TemplateEngineDataProvider dataProvider) {
+        for (TemplateEntry te: entries) {
+            if (!te.isValid(dataProvider))
+                return false;
+        }
+        return true;
+    }
+
+
+    @Override
+    public String toString() {
+        StringBuilder result = new StringBuilder();
+        for (TemplateEntry te: entries) {
+            result.append(te.toString());
+        }
+        return result.toString();
+    }
+
+
+}
Index: trunk/src/org/openstreetmap/josm/tools/template_engine/Condition.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/template_engine/Condition.java	(revision 4282)
+++ trunk/src/org/openstreetmap/josm/tools/template_engine/Condition.java	(revision 4282)
@@ -0,0 +1,58 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools.template_engine;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class Condition implements TemplateEntry {
+
+    private final List<TemplateEntry> entries = new ArrayList<TemplateEntry>();
+
+    public List<TemplateEntry> getEntries() {
+        return entries;
+    }
+
+    @Override
+    public void appendText(StringBuilder result, TemplateEngineDataProvider dataProvider) {
+        for (TemplateEntry entry: entries) {
+            if (entry.isValid(dataProvider)) {
+                entry.appendText(result, dataProvider);
+                return;
+            }
+        }
+
+        // Fallback to last entry
+        TemplateEntry entry = entries.get(entries.size() - 1);
+        entry.appendText(result, dataProvider);
+    }
+
+    @Override
+    public boolean isValid(TemplateEngineDataProvider dataProvider) {
+
+        for (TemplateEntry entry: entries) {
+            if (entry.isValid(dataProvider))
+                return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("?{");
+        for (TemplateEntry entry: entries) {
+            if (entry instanceof SearchExpressionCondition) {
+                sb.append(entry.toString());
+            } else {
+                sb.append("'");
+                sb.append(entry.toString());
+                sb.append("'");
+            }
+            sb.append("|");
+        }
+        return sb.toString();
+    }
+
+}
Index: trunk/src/org/openstreetmap/josm/tools/template_engine/ParseError.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/template_engine/ParseError.java	(revision 4282)
+++ trunk/src/org/openstreetmap/josm/tools/template_engine/ParseError.java	(revision 4282)
@@ -0,0 +1,31 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools.template_engine;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import org.openstreetmap.josm.tools.template_engine.Tokenizer.Token;
+import org.openstreetmap.josm.tools.template_engine.Tokenizer.TokenType;
+
+public class ParseError extends Exception {
+
+    private final Token unexpectedToken;
+
+    public ParseError(Token unexpectedToken) {
+        super(tr("Unexpected token ({0}) on position {1}", unexpectedToken.getType(), unexpectedToken.getPosition()));
+        this.unexpectedToken = unexpectedToken;
+    }
+
+    public ParseError(Token unexpectedToken, TokenType expected) {
+        super(tr("Unexpected token on position {0}. Expected {1}, found {2}", unexpectedToken.getPosition(), expected, unexpectedToken.getType()));
+        this.unexpectedToken = unexpectedToken;
+    }
+
+    public ParseError(org.openstreetmap.josm.actions.search.SearchCompiler.ParseError e) {
+        super(tr("Error while parsing search expression"), e);
+        unexpectedToken = null;
+    }
+
+    public Token getUnexpectedToken() {
+        return unexpectedToken;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/tools/template_engine/SearchExpressionCondition.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/template_engine/SearchExpressionCondition.java	(revision 4282)
+++ trunk/src/org/openstreetmap/josm/tools/template_engine/SearchExpressionCondition.java	(revision 4282)
@@ -0,0 +1,31 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools.template_engine;
+
+import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
+
+public class SearchExpressionCondition implements TemplateEntry {
+
+    private final Match condition;
+    private final TemplateEntry text;
+
+    public SearchExpressionCondition(Match condition, TemplateEntry text) {
+        this.condition = condition;
+        this.text = text;
+    }
+
+    @Override
+    public void appendText(StringBuilder result, TemplateEngineDataProvider dataProvider) {
+        text.appendText(result, dataProvider);
+    }
+
+    @Override
+    public boolean isValid(TemplateEngineDataProvider dataProvider) {
+        return dataProvider.evaluateCondition(condition);
+    }
+
+    @Override
+    public String toString() {
+        return condition.toString() + " '" + text.toString() + "'";
+    }
+
+}
Index: trunk/src/org/openstreetmap/josm/tools/template_engine/StaticText.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/template_engine/StaticText.java	(revision 4282)
+++ trunk/src/org/openstreetmap/josm/tools/template_engine/StaticText.java	(revision 4282)
@@ -0,0 +1,29 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools.template_engine;
+
+
+public class StaticText implements TemplateEntry {
+
+    private final String staticText;
+
+    public StaticText(String staticText) {
+        this.staticText = staticText;
+    }
+
+    @Override
+    public void appendText(StringBuilder result, TemplateEngineDataProvider dataProvider) {
+        result.append(staticText);
+    }
+
+    @Override
+    public boolean isValid(TemplateEngineDataProvider dataProvider) {
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return staticText;
+    }
+
+
+}
Index: trunk/src/org/openstreetmap/josm/tools/template_engine/TemplateEngineDataProvider.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/template_engine/TemplateEngineDataProvider.java	(revision 4282)
+++ trunk/src/org/openstreetmap/josm/tools/template_engine/TemplateEngineDataProvider.java	(revision 4282)
@@ -0,0 +1,12 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools.template_engine;
+
+import java.util.List;
+
+import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
+
+public interface TemplateEngineDataProvider {
+    List<String> getTemplateKeys();
+    Object getTemplateValue(String name);
+    boolean evaluateCondition(Match condition);
+}
Index: trunk/src/org/openstreetmap/josm/tools/template_engine/TemplateEntry.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/template_engine/TemplateEntry.java	(revision 4282)
+++ trunk/src/org/openstreetmap/josm/tools/template_engine/TemplateEntry.java	(revision 4282)
@@ -0,0 +1,8 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools.template_engine;
+
+
+public interface TemplateEntry {
+    void appendText(StringBuilder result, TemplateEngineDataProvider dataProvider);
+    boolean isValid(TemplateEngineDataProvider dataProvider);
+}
Index: trunk/src/org/openstreetmap/josm/tools/template_engine/TemplateParser.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/template_engine/TemplateParser.java	(revision 4282)
+++ trunk/src/org/openstreetmap/josm/tools/template_engine/TemplateParser.java	(revision 4282)
@@ -0,0 +1,103 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools.template_engine;
+
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import org.openstreetmap.josm.actions.search.SearchCompiler;
+import org.openstreetmap.josm.tools.template_engine.Tokenizer.Token;
+import org.openstreetmap.josm.tools.template_engine.Tokenizer.TokenType;
+
+
+public class TemplateParser {
+    private final Tokenizer tokenizer;
+
+    private static final Collection<TokenType> EXPRESSION_END_TOKENS = Arrays.asList(TokenType.EOF);
+    private static final Collection<TokenType> CONDITION_WITH_APOSTROPHES_END_TOKENS = Arrays.asList(TokenType.APOSTROPHE);
+
+    public TemplateParser(String template) {
+        this.tokenizer = new Tokenizer(template);
+    }
+
+    private Token check(TokenType expectedToken) throws ParseError {
+        Token token = tokenizer.nextToken();
+        if (token.getType() != expectedToken)
+            throw new ParseError(token, expectedToken);
+        else
+            return token;
+    }
+
+    public TemplateEntry parse() throws ParseError {
+        return parseExpression(EXPRESSION_END_TOKENS);
+    }
+
+    private TemplateEntry parseExpression(Collection<TokenType> endTokens) throws ParseError {
+        List<TemplateEntry> entries = new ArrayList<TemplateEntry>();
+        while (true) {
+            TemplateEntry templateEntry;
+            Token token = tokenizer.lookAhead();
+            if (token.getType() == TokenType.CONDITION_START) {
+                templateEntry = parseCondition();
+            } else if (token.getType() == TokenType.VARIABLE_START) {
+                templateEntry = parseVariable();
+            } else if (endTokens.contains(token.getType()))
+                return CompoundTemplateEntry.fromArray(entries.toArray(new TemplateEntry[entries.size()]));
+            else if (token.getType() == TokenType.TEXT) {
+                tokenizer.nextToken();
+                templateEntry = new StaticText(token.getText());
+            } else
+                throw new ParseError(token);
+            entries.add(templateEntry);
+        }
+    }
+
+    private TemplateEntry parseVariable() throws ParseError {
+        check(TokenType.VARIABLE_START);
+        String variableName = check(TokenType.TEXT).getText();
+        check(TokenType.END);
+
+        return new Variable(variableName);
+    }
+
+    private void skipWhitespace() {
+        Token token = tokenizer.lookAhead();
+        if (token.getType() == TokenType.TEXT && token.getText().trim().isEmpty()) {
+            tokenizer.nextToken();
+        }
+    }
+
+    private TemplateEntry parseCondition() throws ParseError {
+        check(TokenType.CONDITION_START);
+        Condition result = new Condition();
+        while (true) {
+
+            TemplateEntry condition;
+            String searchExpression = tokenizer.skip('\'');
+            check(TokenType.APOSTROPHE);
+            condition = parseExpression(CONDITION_WITH_APOSTROPHES_END_TOKENS);
+            check(TokenType.APOSTROPHE);
+            if (searchExpression.trim().isEmpty()) {
+                result.getEntries().add(condition);
+            } else {
+                try {
+                    result.getEntries().add(new SearchExpressionCondition(SearchCompiler.compile(searchExpression, false, false), condition));
+                } catch (org.openstreetmap.josm.actions.search.SearchCompiler.ParseError e) {
+                    throw new ParseError(e);
+                }
+            }
+            skipWhitespace();
+
+            Token token = tokenizer.lookAhead();
+            if (token.getType()  == TokenType.END) {
+                tokenizer.nextToken();
+                return result;
+            } else {
+                check(TokenType.PIPE);
+            }
+        }
+    }
+
+}
Index: trunk/src/org/openstreetmap/josm/tools/template_engine/Tokenizer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/template_engine/Tokenizer.java	(revision 4282)
+++ trunk/src/org/openstreetmap/josm/tools/template_engine/Tokenizer.java	(revision 4282)
@@ -0,0 +1,133 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools.template_engine;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class Tokenizer {
+
+    public static class Token {
+        private final TokenType type;
+        private final int position;
+        private final String text;
+
+        public Token(TokenType type, int position) {
+            this(type, position, null);
+        }
+
+        public Token(TokenType type, int position, String text) {
+            this.type = type;
+            this.position = position;
+            this.text = text;
+        }
+
+        public TokenType getType() {
+            return type;
+        }
+
+        public int getPosition() {
+            return position;
+        }
+
+        public String getText() {
+            return text;
+        }
+
+        @Override
+        public String toString() {
+            return type + (text != null?" " + text:"");
+        }
+    }
+
+    public enum TokenType { CONDITION_START, VARIABLE_START, END, PIPE, APOSTROPHE, TEXT, EOF }
+
+    private final List<Character> specialCharaters = Arrays.asList(new Character[] {'$', '?', '{', '}', '|', '\''});
+
+    private final String template;
+
+    private int c;
+    private int index;
+    private Token currentToken;
+    private StringBuilder text = new StringBuilder();
+
+    public Tokenizer(String template) {
+        this.template = template;
+        getChar();
+    }
+
+    private void getChar() {
+        if (index >= template.length()) {
+            c = -1;
+        } else {
+            c = template.charAt(index++);
+        }
+    }
+
+    public Token nextToken() {
+        if (currentToken != null) {
+            Token result = currentToken;
+            currentToken = null;
+            return result;
+        }
+        int position = index;
+
+        text.setLength(0);
+        switch (c) {
+        case -1:
+            return new Token(TokenType.EOF, position);
+        case '{':
+            getChar();
+            return new Token(TokenType.VARIABLE_START, position);
+
+        case '?':
+            getChar();
+            if (c == '{') {
+                getChar();
+                return new Token(TokenType.CONDITION_START, position);
+            } else
+                throw new AssertionError();
+        case '}':
+            getChar();
+            return new Token(TokenType.END, position);
+        case '|':
+            getChar();
+            return new Token(TokenType.PIPE, position);
+        case '\'':
+            getChar();
+            return new Token(TokenType.APOSTROPHE, position);
+        default:
+            while (c != -1 && !specialCharaters.contains((char)c)) {
+                if (c == '\\') {
+                    getChar();
+                    if (c == 'n') {
+                        c = '\n';
+                    }
+                }
+                text.append((char)c);
+                getChar();
+            }
+            return new Token(TokenType.TEXT, position, text.toString());
+        }
+    }
+
+    public Token lookAhead() {
+        if (currentToken == null) {
+            currentToken = nextToken();
+        }
+        return currentToken;
+    }
+
+    public String skip(char lastChar) {
+        currentToken = null;
+        StringBuilder result = new StringBuilder();
+        while (c != lastChar && c != -1) {
+            if (c == '\\') {
+                getChar();
+            }
+            result.append((char)c);
+            getChar();
+        }
+        return result.toString();
+    }
+
+}
Index: trunk/src/org/openstreetmap/josm/tools/template_engine/Variable.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/template_engine/Variable.java	(revision 4282)
+++ trunk/src/org/openstreetmap/josm/tools/template_engine/Variable.java	(revision 4282)
@@ -0,0 +1,49 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools.template_engine;
+
+import java.util.List;
+
+
+public class Variable implements TemplateEntry {
+
+    private final String variableName;
+
+    public Variable(String variableName) {
+        this.variableName = variableName;
+    }
+
+    @Override
+    public void appendText(StringBuilder result, TemplateEngineDataProvider dataProvider) {
+        if ("*".equals(variableName)) {
+            List<String> keys = dataProvider.getTemplateKeys();
+            boolean first = true;
+            for (String key: keys) {
+                if (!first) {
+                    result.append(", ");
+                } else {
+                    first = false;
+                }
+                result.append(key).append("=").append(dataProvider.getTemplateValue(key));
+            }
+        } else {
+            Object value = dataProvider.getTemplateValue(variableName);
+            if (value != null) {
+                result.append(value);
+            }
+        }
+    }
+
+    @Override
+    public boolean isValid(TemplateEngineDataProvider dataProvider) {
+        if ("*".equals(variableName))
+            return true;
+        else
+            return dataProvider.getTemplateValue(variableName) != null;
+    }
+
+    @Override
+    public String toString() {
+        return "{" + variableName + "}";
+    }
+
+}
Index: trunk/test/unit/org/openstreetmap/josm/tools/template_engine/TemplateEngineTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/tools/template_engine/TemplateEngineTest.java	(revision 4282)
+++ trunk/test/unit/org/openstreetmap/josm/tools/template_engine/TemplateEngineTest.java	(revision 4282)
@@ -0,0 +1,110 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools.template_engine;
+
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.openstreetmap.josm.actions.search.SearchCompiler;
+import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
+import org.unitils.reflectionassert.ReflectionAssert;
+
+public class TemplateEngineTest {
+
+    @Test
+    public void testEmpty() throws ParseError {
+        TemplateParser parser = new TemplateParser("");
+        ReflectionAssert.assertReflectionEquals(new StaticText(""), parser.parse());
+    }
+
+    @Test
+    public void testVariable() throws ParseError {
+        TemplateParser parser = new TemplateParser("abc{var}\\{ef\\$\\{g");
+        ReflectionAssert.assertReflectionEquals(CompoundTemplateEntry.fromArray(new StaticText("abc"), new Variable("var"), new StaticText("{ef${g")), parser.parse());
+    }
+
+    @Test
+    public void testConditionWhitespace() throws ParseError {
+        TemplateParser parser = new TemplateParser("?{ '{name} {desc}' | '{name}' | '{desc}'    }");
+        Condition condition = new Condition();
+        condition.getEntries().add(CompoundTemplateEntry.fromArray(new Variable("name"), new StaticText(" "), new Variable("desc")));
+        condition.getEntries().add(new Variable("name"));
+        condition.getEntries().add(new Variable("desc"));
+        ReflectionAssert.assertReflectionEquals(condition, parser.parse());
+    }
+
+    @Test
+    public void testConditionNoWhitespace() throws ParseError {
+        TemplateParser parser = new TemplateParser("?{'{name} {desc}'|'{name}'|'{desc}'}");
+        Condition condition = new Condition();
+        condition.getEntries().add(CompoundTemplateEntry.fromArray(new Variable("name"), new StaticText(" "), new Variable("desc")));
+        condition.getEntries().add(new Variable("name"));
+        condition.getEntries().add(new Variable("desc"));
+        ReflectionAssert.assertReflectionEquals(condition, parser.parse());
+    }
+
+    private static Match compile(String expression) throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError {
+        return SearchCompiler.compile(expression, false, false);
+    }
+
+    @Test
+    public void testConditionSearchExpression() throws Exception {
+        TemplateParser parser = new TemplateParser("?{ admin_level = 2 'NUTS 1' | admin_level = 4 'NUTS 2' |  '{admin_level}'}");
+        Condition condition = new Condition();
+        condition.getEntries().add(new SearchExpressionCondition(compile("admin_level = 2"), new StaticText("NUTS 1")));
+        condition.getEntries().add(new SearchExpressionCondition(compile("admin_level = 4"), new StaticText("NUTS 2")));
+        condition.getEntries().add(new Variable("admin_level"));
+        ReflectionAssert.assertReflectionEquals(condition, parser.parse());
+    }
+
+    TemplateEngineDataProvider dataProvider = new TemplateEngineDataProvider() {
+        @Override
+        public Object getTemplateValue(String name) {
+            if ("name".equals(name))
+                return "waypointName";
+            else if ("number".equals(name))
+                return 10;
+            else
+                return null;
+        }
+        @Override
+        public boolean evaluateCondition(Match condition) {
+            return true;
+        }
+        @Override
+        public List<String> getTemplateKeys() {
+            return Arrays.asList("name", "number");
+        }
+    };
+
+    @Test
+    public void testFilling() throws Exception {
+        TemplateParser parser = new TemplateParser("{name} u{unknown}u i{number}i");
+        TemplateEntry entry = parser.parse();
+        StringBuilder sb = new StringBuilder();
+        entry.appendText(sb, dataProvider);
+        Assert.assertEquals("waypointName uu i10i", sb.toString());
+    }
+
+    @Test
+    public void testPrintAll() throws Exception {
+        TemplateParser parser = new TemplateParser("{*}");
+        TemplateEntry entry = parser.parse();
+        StringBuilder sb = new StringBuilder();
+        entry.appendText(sb, dataProvider);
+        Assert.assertEquals("name=waypointName, number=10", sb.toString());
+    }
+
+    @Test
+    public void testPrintMultiline() throws Exception {
+        TemplateParser parser = new TemplateParser("{name}\\n{number}");
+        TemplateEntry entry = parser.parse();
+        StringBuilder sb = new StringBuilder();
+        entry.appendText(sb, dataProvider);
+        Assert.assertEquals("waypointName\n10", sb.toString());
+    }
+
+
+}
