Index: trunk/src/org/openstreetmap/josm/io/GpxReader.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/GpxReader.java	(revision 15482)
+++ trunk/src/org/openstreetmap/josm/io/GpxReader.java	(revision 15496)
@@ -19,10 +19,13 @@
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.data.coor.LatLon;
-import org.openstreetmap.josm.data.gpx.Extensions;
 import org.openstreetmap.josm.data.gpx.GpxConstants;
 import org.openstreetmap.josm.data.gpx.GpxData;
+import org.openstreetmap.josm.data.gpx.GpxData.XMLNamespace;
+import org.openstreetmap.josm.data.gpx.GpxExtensionCollection;
 import org.openstreetmap.josm.data.gpx.GpxLink;
 import org.openstreetmap.josm.data.gpx.GpxRoute;
-import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
+import org.openstreetmap.josm.data.gpx.GpxTrack;
+import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
+import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
 import org.openstreetmap.josm.data.gpx.WayPoint;
 import org.openstreetmap.josm.tools.Logging;
@@ -68,5 +71,5 @@
 
         private GpxData data;
-        private Collection<Collection<WayPoint>> currentTrack;
+        private Collection<IGpxTrackSegment> currentTrack;
         private Map<String, Object> currentTrackAttr;
         private Collection<WayPoint> currentTrackSeg;
@@ -77,5 +80,6 @@
 
         private GpxLink currentLink;
-        private Extensions currentExtensions;
+        private GpxExtensionCollection currentExtensionCollection;
+        private GpxExtensionCollection currentTrackExtensionCollection;
         private Stack<State> states;
         private final Stack<String> elements = new Stack<>();
@@ -89,5 +93,12 @@
             accumulator = new StringBuilder();
             states = new Stack<>();
-            data = new GpxData();
+            data = new GpxData(true);
+            currentExtensionCollection = new GpxExtensionCollection();
+            currentTrackExtensionCollection = new GpxExtensionCollection();
+        }
+
+        @Override
+        public void startPrefixMapping(String prefix, String uri) throws SAXException {
+            data.getNamespaces().add(new XMLNamespace(prefix, uri));
         }
 
@@ -134,4 +145,15 @@
                     version = "1.1";
                 }
+                String schemaLocation = atts.getValue(GpxConstants.XML_URI_XSD, "schemaLocation");
+                if (schemaLocation != null) {
+                    String[] schemaLocations = schemaLocation.split(" ");
+                    for (int i = 0; i < schemaLocations.length - 1; i += 2) {
+                        final String schemaURI = schemaLocations[i];
+                        final String schemaXSD = schemaLocations[i + 1];
+                        data.getNamespaces().stream().filter(xml -> xml.getURI().equals(schemaURI)).forEach(xml -> {
+                            xml.setLocation(schemaXSD);
+                        });
+                    }
+                }
                 break;
             case GPX:
@@ -160,5 +182,4 @@
                     states.push(currentState);
                     currentState = State.EXT;
-                    currentExtensions = new Extensions();
                     break;
                 case "gpx":
@@ -179,5 +200,4 @@
                     states.push(currentState);
                     currentState = State.EXT;
-                    currentExtensions = new Extensions();
                     break;
                 case "copyright":
@@ -229,5 +249,4 @@
                     states.push(currentState);
                     currentState = State.EXT;
-                    currentExtensions = new Extensions();
                     break;
                 default: // Do nothing
@@ -235,8 +254,14 @@
                 break;
             case TRKSEG:
-                if ("trkpt".equals(localName)) {
+                switch (localName) {
+                case "trkpt":
                     states.push(currentState);
                     currentState = State.WPT;
                     currentWayPoint = new WayPoint(parseLatLon(atts));
+                    break;
+                case "extensions":
+                    states.push(currentState);
+                    currentState = State.EXT;
+                    break;
                 }
                 break;
@@ -251,5 +276,4 @@
                     states.push(currentState);
                     currentState = State.EXT;
-                    currentExtensions = new Extensions();
                     break;
                 default: // Do nothing
@@ -271,7 +295,13 @@
                     states.push(currentState);
                     currentState = State.EXT;
-                    currentExtensions = new Extensions();
-                    break;
-                default: // Do nothing
+                    break;
+                default: // Do nothing
+                }
+                break;
+            case EXT:
+                if (states.lastElement() == State.TRK) {
+                    currentTrackExtensionCollection.openChild(namespaceURI, qName, atts);
+                } else {
+                    currentExtensionCollection.openChild(namespaceURI, qName, atts);
                 }
                 break;
@@ -350,7 +380,6 @@
                         (currentState == State.GPX && "gpx".equals(localName))) {
                         convertUrlToLink(data.attr);
-                        if (currentExtensions != null && !currentExtensions.isEmpty()) {
-                            data.put(META_EXTENSIONS, currentExtensions);
-                        }
+                        data.getExtensions().addAll(currentExtensionCollection);
+                        currentExtensionCollection.clear();
                         currentState = states.pop();
                     }
@@ -360,5 +389,4 @@
                     break;
                 default:
-                    //TODO: parse extensions
                 }
                 break;
@@ -465,8 +493,7 @@
                     currentState = states.pop();
                     convertUrlToLink(currentWayPoint.attr);
-                    if (currentExtensions != null && !currentExtensions.isEmpty()) {
-                        currentWayPoint.put(META_EXTENSIONS, currentExtensions);
-                    }
+                    currentWayPoint.getExtensions().addAll(currentExtensionCollection);
                     data.waypoints.add(currentWayPoint);
+                    currentExtensionCollection.clear();
                     break;
                 default: // Do nothing
@@ -476,5 +503,10 @@
                 if ("trkseg".equals(localName)) {
                     currentState = states.pop();
-                    currentTrack.add(currentTrackSeg);
+                    if (!currentTrackSeg.isEmpty()) {
+                        GpxTrackSegment seg = new GpxTrackSegment(currentTrackSeg);
+                        seg.getExtensions().addAll(currentExtensionCollection);
+                        currentTrack.add(seg);
+                    }
+                    currentExtensionCollection.clear();
                 }
                 break;
@@ -484,5 +516,8 @@
                     currentState = states.pop();
                     convertUrlToLink(currentTrackAttr);
-                    data.addTrack(new ImmutableGpxTrack(currentTrack, currentTrackAttr));
+                    GpxTrack trk = new GpxTrack(new ArrayList<>(currentTrack), currentTrackAttr);
+                    trk.getExtensions().addAll(currentTrackExtensionCollection);
+                    data.addTrack(trk);
+                    currentTrackExtensionCollection.clear();
                     break;
                 case "name":
@@ -502,7 +537,11 @@
                 if ("extensions".equals(localName)) {
                     currentState = states.pop();
-                } else if (JOSM_EXTENSIONS_NAMESPACE_URI.equals(namespaceURI)) {
-                    // only interested in extensions written by JOSM
-                    currentExtensions.put(localName, accumulator.toString());
+                } else if (currentExtensionCollection != null) {
+                    String acc = accumulator.toString().trim();
+                    if (states.lastElement() == State.TRK) {
+                        currentTrackExtensionCollection.closeChild(qName, acc); //a segment inside the track can have an extension too
+                    } else {
+                        currentExtensionCollection.closeChild(qName, acc);
+                    }
                 }
                 break;
@@ -520,4 +559,5 @@
                 }
             }
+            accumulator.setLength(0);
         }
 
@@ -526,8 +566,19 @@
             if (!states.empty())
                 throw new SAXException(tr("Parse error: invalid document structure for GPX document."));
-            Extensions metaExt = (Extensions) data.get(META_EXTENSIONS);
-            if (metaExt != null && "true".equals(metaExt.get("from-server"))) {
-                data.fromServer = true;
-            }
+
+            data.getExtensions().stream("josm", "from-server").findAny().ifPresent(ext -> {
+                data.fromServer = "true".equals(ext.getValue());
+            });
+
+            data.getExtensions().stream("josm", "layerPreferences").forEach(prefs -> {
+                prefs.getExtensions().stream("josm", "entry").forEach(prefEntry -> {
+                    Object key = prefEntry.get("key");
+                    Object val = prefEntry.get("value");
+                    if (key != null && val != null) {
+                        data.getLayerPrefs().put(key.toString(), val.toString());
+                    }
+                });
+            });
+            data.endUpdate();
             gpxData = data;
         }
Index: trunk/src/org/openstreetmap/josm/io/GpxWriter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/GpxWriter.java	(revision 15482)
+++ trunk/src/org/openstreetmap/josm/io/GpxWriter.java	(revision 15496)
@@ -9,9 +9,11 @@
 import java.io.PrintWriter;
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.stream.Collectors;
 
 import javax.xml.XMLConstants;
@@ -19,11 +21,13 @@
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.data.coor.LatLon;
-import org.openstreetmap.josm.data.gpx.Extensions;
 import org.openstreetmap.josm.data.gpx.GpxConstants;
 import org.openstreetmap.josm.data.gpx.GpxData;
+import org.openstreetmap.josm.data.gpx.GpxData.XMLNamespace;
+import org.openstreetmap.josm.data.gpx.GpxExtension;
+import org.openstreetmap.josm.data.gpx.GpxExtensionCollection;
 import org.openstreetmap.josm.data.gpx.GpxLink;
 import org.openstreetmap.josm.data.gpx.GpxRoute;
 import org.openstreetmap.josm.data.gpx.GpxTrack;
-import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
+import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
 import org.openstreetmap.josm.data.gpx.IWithAttributes;
 import org.openstreetmap.josm.data.gpx.WayPoint;
@@ -55,4 +59,5 @@
     private GpxData data;
     private String indent = "";
+    private List<String> validprefixes;
 
     private static final int WAY_POINT = 0;
@@ -65,27 +70,75 @@
      */
     public void write(GpxData data) {
+        write(data, ColorFormat.GPXD, true);
+    }
+
+    /**
+     * Writes the given GPX data.
+     *
+     * @param data The data to write
+     * @param colorFormat determines if colors are saved and which extension is to be used
+     * @param savePrefs whether layer specific preferences are saved
+     */
+    public void write(GpxData data, ColorFormat colorFormat, boolean savePrefs) {
         this.data = data;
-        // We write JOSM specific meta information into gpx 'extensions' elements.
-        // In particular it is noted whether the gpx data is from the OSM server
-        // (so the rendering of clouds of anonymous TrackPoints can be improved)
-        // and some extra synchronization info for export of AudioMarkers.
-        // It is checked in advance, if any extensions are used, so we know whether
-        // a namespace declaration is necessary.
-        boolean hasExtensions = data.fromServer;
-        if (!hasExtensions) {
-            for (WayPoint wpt : data.waypoints) {
-                Extensions extensions = (Extensions) wpt.get(META_EXTENSIONS);
-                if (extensions != null && !extensions.isEmpty()) {
-                    hasExtensions = true;
-                    break;
-                }
-            }
-        }
+
+        //Prepare extensions for writing
+        data.beginUpdate();
+        data.getTracks().forEach(trk -> trk.convertColor(colorFormat));
+        data.getExtensions().removeAllWithPrefix("josm");
+        if (data.fromServer) {
+            data.getExtensions().add("josm", "from-server", "true");
+        }
+        if (savePrefs && !data.getLayerPrefs().isEmpty()) {
+            GpxExtensionCollection layerExts = data.getExtensions().add("josm", "layerPreferences").getExtensions();
+            data.getLayerPrefs().entrySet()
+            .stream()
+            .sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey()))
+            .forEach(entry -> {
+                GpxExtension e = layerExts.add("josm", "entry");
+                e.put("key", entry.getKey());
+                e.put("value", entry.getValue());
+            });
+        }
+        data.endUpdate();
+
+        Collection<IWithAttributes> all = new ArrayList<>();
+
+        all.add(data);
+        all.addAll(data.getWaypoints());
+        all.addAll(data.getRoutes());
+        all.addAll(data.getTracks());
+        all.addAll(data.getTrackSegmentsStream().collect(Collectors.toList()));
+
+        List<XMLNamespace> namespaces = all
+                .stream()
+                .flatMap(w -> w.getExtensions().getPrefixesStream())
+                .distinct()
+                .map(p -> data.getNamespaces()
+                        .stream()
+                        .filter(s -> s.getPrefix().equals(p))
+                        .findAny()
+                        .orElse(GpxExtension.findNamespace(p)))
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+
+        validprefixes = namespaces.stream().map(n -> n.getPrefix()).collect(Collectors.toList());
 
         out.println("<?xml version='1.0' encoding='UTF-8'?>");
         out.println("<gpx version=\"1.1\" creator=\"JOSM GPX export\" xmlns=\"http://www.topografix.com/GPX/1/1\"");
-        out.println((hasExtensions ? String.format("    xmlns:josm=\"%s\"%n", JOSM_EXTENSIONS_NAMESPACE_URI) : "") +
-                    "    xmlns:xsi=\""+XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI+"\"");
-        out.println("    xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\">");
+
+        String schemaLocations = "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd";
+
+        for (XMLNamespace n : namespaces) {
+            if (n.getURI() != null && n.getPrefix() != null && !n.getPrefix().isEmpty()) {
+                out.println(String.format("    xmlns:%s=\"%s\"", n.getPrefix(), n.getURI()));
+                if (n.getLocation() != null) {
+                    schemaLocations += " " + n.getURI() + " " + n.getLocation();
+                }
+            }
+        }
+
+        out.println("    xmlns:xsi=\""+XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI+"\"");
+        out.println(String.format("    xsi:schemaLocation=\"%s\">", schemaLocations));
         indent = "  ";
         writeMetaData();
@@ -105,9 +158,4 @@
                         gpxLink(link);
                     }
-                }
-            } else if (META_EXTENSIONS.equals(key)) {
-                Extensions extensions = (Extensions) obj.get(key);
-                if (extensions != null) {
-                    gpxExtensions(extensions);
                 }
             } else {
@@ -148,5 +196,5 @@
                 String[] tmp = data.getString(META_AUTHOR_EMAIL).split("@");
                 if (tmp.length == 2) {
-                    inline("email", "id=\"" + tmp[0] + "\" domain=\""+tmp[1]+'\"');
+                    inline("email", "id=\"" + encode(tmp[0]) + "\" domain=\"" + encode(tmp[1]) +'\"');
                 }
             }
@@ -159,5 +207,5 @@
         if (attr.containsKey(META_COPYRIGHT_LICENSE)
                 || attr.containsKey(META_COPYRIGHT_YEAR)) {
-            openAtt("copyright", "author=\""+ data.get(META_COPYRIGHT_AUTHOR) +'\"');
+            openln("copyright", "author=\""+ encode(data.get(META_COPYRIGHT_AUTHOR).toString()) +'\"');
             if (attr.containsKey(META_COPYRIGHT_YEAR)) {
                 simpleTag("year", (String) data.get(META_COPYRIGHT_YEAR));
@@ -188,10 +236,5 @@
         }
 
-        if (data.fromServer) {
-            openln("extensions");
-            simpleTag("josm:from-server", "true");
-            closeln("extensions");
-        }
-
+        gpxExtensions(data.getExtensions());
         closeln("metadata");
     }
@@ -207,4 +250,5 @@
             openln("rte");
             writeAttr(rte, RTE_TRK_KEYS);
+            gpxExtensions(rte.getExtensions());
             for (WayPoint pnt : rte.routePoints) {
                 wayPoint(pnt, ROUTE_POINT);
@@ -218,6 +262,8 @@
             openln("trk");
             writeAttr(trk, RTE_TRK_KEYS);
-            for (GpxTrackSegment seg : trk.getSegments()) {
+            gpxExtensions(trk.getExtensions());
+            for (IGpxTrackSegment seg : trk.getSegments()) {
                 openln("trkseg");
+                gpxExtensions(seg.getExtensions());
                 for (WayPoint pnt : seg.getWayPoints()) {
                     wayPoint(pnt, TRACK_POINT);
@@ -234,4 +280,9 @@
     }
 
+    private void openln(String tag, String attributes) {
+        open(tag, attributes);
+        out.println();
+    }
+
     private void open(String tag) {
         out.print(indent + '<' + tag + '>');
@@ -239,11 +290,11 @@
     }
 
-    private void openAtt(String tag, String attributes) {
-        out.println(indent + '<' + tag + ' ' + attributes + '>');
+    private void open(String tag, String attributes) {
+        out.print(indent + '<' + tag + (attributes.isEmpty() ? "" : ' ') + attributes + '>');
         indent += "  ";
     }
 
     private void inline(String tag, String attributes) {
-        out.println(indent + '<' + tag + ' ' + attributes + "/>");
+        out.println(indent + '<' + tag + (attributes.isEmpty() ? "" : ' ') + attributes + "/>");
     }
 
@@ -273,4 +324,13 @@
     }
 
+    private void simpleTag(String tag, String content, String attributes) {
+        if (content != null && !content.isEmpty()) {
+            open(tag, attributes);
+            out.print(encode(content));
+            out.println("</" + tag + '>');
+            indent = indent.substring(2);
+        }
+    }
+
     /**
      * output link
@@ -279,5 +339,5 @@
     private void gpxLink(GpxLink link) {
         if (link != null) {
-            openAtt("link", "href=\"" + link.uri + '\"');
+            openln("link", "href=\"" + encode(link.uri) + '\"');
             simpleTag("text", link.text);
             simpleTag("type", link.type);
@@ -309,9 +369,10 @@
             LatLon c = pnt.getCoor();
             String coordAttr = "lat=\"" + c.lat() + "\" lon=\"" + c.lon() + '\"';
-            if (pnt.attr.isEmpty()) {
+            if (pnt.attr.isEmpty() && pnt.getExtensions().isEmpty()) {
                 inline(type, coordAttr);
             } else {
-                openAtt(type, coordAttr);
+                openln(type, coordAttr);
                 writeAttr(pnt, WPT_KEYS);
+                gpxExtensions(pnt.getExtensions());
                 closeln(type);
             }
@@ -319,12 +380,33 @@
     }
 
-    private void gpxExtensions(Extensions extensions) {
-        if (extensions != null && !extensions.isEmpty()) {
+    private void gpxExtensions(GpxExtensionCollection allExtensions) {
+        if (allExtensions.isVisible()) {
             openln("extensions");
-            for (Entry<String, String> e : extensions.entrySet()) {
-                simpleTag("josm:" + e.getKey(), e.getValue());
-            }
+            writeExtension(allExtensions);
             closeln("extensions");
         }
     }
+
+    private void writeExtension(List<GpxExtension> extensions) {
+        for (GpxExtension e : extensions) {
+            if (validprefixes.contains(e.getPrefix()) && e.isVisible()) {
+                // this might lead to loss of an unknown extension *after* the file was saved as .osm,
+                // but otherwise the file is invalid and can't even be parsed by SAX anymore
+                String k = (e.getPrefix().isEmpty() ? "" : e.getPrefix() + ":") + e.getKey();
+                String attr = String.join(" ", e.getAttributes().entrySet().stream().map(a -> encode(a.getKey()) + "=\"" + encode(a.getValue().toString()) + "\"").sorted().collect(Collectors.toList()));
+                if (e.getValue() == null && e.getExtensions().isEmpty()) {
+                    inline(k, attr);
+                } else if (e.getExtensions().isEmpty()) {
+                    simpleTag(k, e.getValue(), attr);
+                } else {
+                    openln(k, attr);
+                    if (e.getValue() != null) {
+                        out.print(encode(e.getValue()));
+                    }
+                    writeExtension(e.getExtensions());
+                    closeln(k);
+                }
+            }
+        }
+    }
 }
Index: trunk/src/org/openstreetmap/josm/io/nmea/NmeaReader.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/nmea/NmeaReader.java	(revision 15482)
+++ trunk/src/org/openstreetmap/josm/io/nmea/NmeaReader.java	(revision 15496)
@@ -21,5 +21,5 @@
 import org.openstreetmap.josm.data.gpx.GpxConstants;
 import org.openstreetmap.josm.data.gpx.GpxData;
-import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
+import org.openstreetmap.josm.data.gpx.GpxTrack;
 import org.openstreetmap.josm.data.gpx.WayPoint;
 import org.openstreetmap.josm.io.IGpxReader;
@@ -265,5 +265,5 @@
             }
             currentTrack.add(ps.waypoints);
-            data.tracks.add(new ImmutableGpxTrack(currentTrack, Collections.<String, Object>emptyMap()));
+            data.tracks.add(new GpxTrack(currentTrack, Collections.<String, Object>emptyMap()));
 
         } catch (IllegalDataException e) {
Index: trunk/src/org/openstreetmap/josm/io/rtklib/RtkLibPosReader.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/rtklib/RtkLibPosReader.java	(revision 15482)
+++ trunk/src/org/openstreetmap/josm/io/rtklib/RtkLibPosReader.java	(revision 15496)
@@ -19,5 +19,5 @@
 import org.openstreetmap.josm.data.gpx.GpxConstants;
 import org.openstreetmap.josm.data.gpx.GpxData;
-import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
+import org.openstreetmap.josm.data.gpx.GpxTrack;
 import org.openstreetmap.josm.data.gpx.WayPoint;
 import org.openstreetmap.josm.io.IGpxReader;
@@ -115,5 +115,5 @@
         }
         currentTrack.add(waypoints);
-        data.tracks.add(new ImmutableGpxTrack(currentTrack, Collections.<String, Object>emptyMap()));
+        data.tracks.add(new GpxTrack(currentTrack, Collections.<String, Object>emptyMap()));
         return true;
     }
Index: trunk/src/org/openstreetmap/josm/io/session/GenericSessionExporter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/session/GenericSessionExporter.java	(revision 15482)
+++ trunk/src/org/openstreetmap/josm/io/session/GenericSessionExporter.java	(revision 15496)
@@ -82,5 +82,5 @@
         @Override
         public void actionPerformed(ActionEvent e) {
-            SaveAction.getInstance().doSave(layer);
+            SaveAction.getInstance().doSave(layer, true);
             updateEnabledState();
         }
