Index: trunk/src/org/openstreetmap/josm/data/coor/ILatLon.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/coor/ILatLon.java	(revision 12164)
+++ trunk/src/org/openstreetmap/josm/data/coor/ILatLon.java	(revision 12164)
@@ -0,0 +1,66 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.coor;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.projection.Projecting;
+
+/**
+ * This interface represents a coordinate in LatLon space.
+ * <p>
+ * It provides methods to get the coordinates. The coordinates may be unknown.
+ * In this case, both {@link #lat()} and {@link #lon()} need to return a NaN value and {@link #isLatLonKnown()} needs to return false.
+ * <p>
+ * Whether the coordinates are immutable or not is implementation specific.
+ *
+ * @author Michael Zangl
+ * @since 12161
+ */
+public interface ILatLon {
+
+    /**
+     * Returns the longitude, i.e., the east-west position in degrees.
+     * @return the longitude or NaN if {@link #isLatLonKnown()} returns false
+     */
+    public double lon();
+
+    /**
+     * Returns the latitude, i.e., the north-south position in degrees.
+     * @return the latitude or NaN if {@link #isLatLonKnown()} returns false
+     */
+    public double lat();
+
+    /**
+     * Determines if this object has valid coordinates.
+     * @return {@code true} if this object has valid coordinates
+     */
+    default boolean isLatLonKnown() {
+        return !Double.isNaN(lat()) && !Double.isNaN(lon());
+    }
+
+    /**
+     * <p>Replies the projected east/north coordinates.</p>
+     *
+     * <p>Uses the {@link Main#getProjection() global projection} to project the lan/lon-coordinates.</p>
+     *
+     * @return the east north coordinates or {@code null} if #is
+     */
+    default EastNorth getEastNorth() {
+        return getEastNorth(Main.getProjection());
+    }
+
+    /**
+     * Replies the projected east/north coordinates.
+     * <p>
+     * The result of the last conversion may be cached. Null is returned in case this object is invalid.
+     * @param projecting The projection to use.
+     * @return The projected east/north coordinates
+     * @since 10827
+     */
+    default EastNorth getEastNorth(Projecting projecting) {
+        if (!isLatLonKnown()) {
+            return null;
+        } else {
+            return projecting.latlon2eastNorth(this);
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/data/gpx/GpxData.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/gpx/GpxData.java	(revision 12163)
+++ trunk/src/org/openstreetmap/josm/data/gpx/GpxData.java	(revision 12164)
@@ -40,7 +40,7 @@
     public String creator;
 
-    private ArrayList<GpxTrack> privateTracks = new ArrayList<>();
-    private ArrayList<GpxRoute> privateRoutes = new ArrayList<>();
-    private ArrayList<WayPoint> privateWaypoints = new ArrayList<>();
+    private final ArrayList<GpxTrack> privateTracks = new ArrayList<>();
+    private final ArrayList<GpxRoute> privateRoutes = new ArrayList<>();
+    private final ArrayList<WayPoint> privateWaypoints = new ArrayList<>();
     private final GpxTrackChangeListener proxy = e -> fireInvalidate();
 
@@ -580,8 +580,8 @@
         final int prime = 31;
         int result = 1;
-        result = prime * result + ((dataSources == null) ? 0 : dataSources.hashCode());
-        result = prime * result + ((routes == null) ? 0 : routes.hashCode());
-        result = prime * result + ((tracks == null) ? 0 : tracks.hashCode());
-        result = prime * result + ((waypoints == null) ? 0 : waypoints.hashCode());
+        result = prime * result + dataSources.hashCode();
+        result = prime * result + privateRoutes.hashCode();
+        result = prime * result + privateTracks.hashCode();
+        result = prime * result + privateWaypoints.hashCode();
         return result;
     }
@@ -601,18 +601,18 @@
         } else if (!dataSources.equals(other.dataSources))
             return false;
-        if (routes == null) {
-            if (other.routes != null)
+        if (privateRoutes == null) {
+            if (other.privateRoutes != null)
                 return false;
-        } else if (!routes.equals(other.routes))
+        } else if (!privateRoutes.equals(other.privateRoutes))
             return false;
-        if (tracks == null) {
-            if (other.tracks != null)
+        if (privateTracks == null) {
+            if (other.privateTracks != null)
                 return false;
-        } else if (!tracks.equals(other.tracks))
+        } else if (!privateTracks.equals(other.privateTracks))
             return false;
-        if (waypoints == null) {
-            if (other.waypoints != null)
+        if (privateWaypoints == null) {
+            if (other.privateWaypoints != null)
                 return false;
-        } else if (!waypoints.equals(other.waypoints))
+        } else if (!privateWaypoints.equals(other.privateWaypoints))
             return false;
         return true;
Index: trunk/test/unit/org/openstreetmap/josm/data/gpx/GpxDataTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/data/gpx/GpxDataTest.java	(revision 12163)
+++ trunk/test/unit/org/openstreetmap/josm/data/gpx/GpxDataTest.java	(revision 12164)
@@ -2,4 +2,12 @@
 package org.openstreetmap.josm.data.gpx;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Collections;
+
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -22,4 +30,316 @@
     public JOSMTestRules test = new JOSMTestRules();
 
+    private GpxData data;
+
+    /**
+     * @throws java.lang.Exception
+     */
+    @Before
+    public void setUp() throws Exception {
+        data = new GpxData();
+    }
+
+
+    /**
+     * Test method for {@link GpxData#mergeFrom(GpxData)}.
+     */
+    @Test
+    public void testMergeFrom() {
+        fail("Not yet implemented");
+    }
+
+    /**
+     * Test method for {@link GpxData#getTracks()},  {@link GpxData#addTrack(GpxTrack)},  {@link GpxData#removeTrack(GpxTrack)}.
+     */
+    @Test
+    public void testTracks() {
+        assertEquals(0, data.getTracks().size());
+
+        ImmutableGpxTrack track1 = emptyGpxTrack();
+        ImmutableGpxTrack track2 = emptyGpxTrack();
+        data.addTrack(track1);
+        assertEquals(1, data.getTracks().size());
+        data.addTrack(track2);
+        assertEquals(2, data.getTracks().size());
+        assertTrue(data.getTracks().contains(track1));
+        assertTrue(data.getTracks().contains(track2));
+
+        data.removeTrack(track1);
+        assertEquals(1, data.getTracks().size());
+        assertFalse(data.getTracks().contains(track1));
+        assertTrue(data.getTracks().contains(track2));
+    }
+
+    /**
+     * Test method for {@link GpxData#addTrack(GpxTrack)}.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testAddTrackFails() {
+        ImmutableGpxTrack track1 = emptyGpxTrack();
+        data.addTrack(track1);
+        data.addTrack(track1);
+    }
+
+    /**
+     * Test method for {@link GpxData#removeTrack(GpxTrack)}.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testRemoveTrackFails() {
+        ImmutableGpxTrack track1 = emptyGpxTrack();
+        data.addTrack(track1);
+        data.removeTrack(track1);
+        data.removeTrack(track1);
+    }
+
+    /**
+     * Test method for {@link GpxData#getRoutes()}, {@link GpxData#addRoute(GpxRoute)}, {@link GpxData#removeRoute(GpxRoute)}.
+     */
+    @Test
+    public void testRoutes() {
+        assertEquals(0, data.getTracks().size());
+
+        GpxRoute route1 = new GpxRoute();
+        GpxRoute route2 = new GpxRoute();
+        data.addRoute(route1);
+        assertEquals(1, data.getRoutes().size());
+        data.addRoute(route2);
+        assertEquals(2, data.getRoutes().size());
+        assertTrue(data.getRoutes().contains(route1));
+        assertTrue(data.getRoutes().contains(route2));
+
+        data.removeRoute(route1);
+        assertEquals(1, data.getRoutes().size());
+        assertFalse(data.getRoutes().contains(route1));
+        assertTrue(data.getRoutes().contains(route2));
+    }
+
+    /**
+     * Test method for {@link GpxData#addRoute(GpxRoute)}.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testAddRouteFails() {
+        GpxRoute route1 = new GpxRoute();
+        data.addRoute(route1);
+        data.addRoute(route1);
+    }
+
+    /**
+     * Test method for {@link GpxData#removeRoute(GpxRoute)}.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testRemoveRouteFails() {
+        GpxRoute route1 = new GpxRoute();
+        data.addRoute(route1);
+        data.removeRoute(route1);
+        data.removeRoute(route1);
+    }
+
+    /**
+     * Test method for {@link GpxData#getWaypoints()}, {@link GpxData#addWaypoint(WayPoint)}, {@link GpxData#removeWaypoint(WayPoint)}.
+     */
+    @Test
+    public void testWaypoints() {
+        assertEquals(0, data.getTracks().size());
+
+        WayPoint waypoint1 = new WayPoint(LatLon.ZERO);
+        WayPoint waypoint2 = new WayPoint(LatLon.ZERO);
+        data.addWaypoint(waypoint1);
+        assertEquals(1, data.getWaypoints().size());
+        data.addWaypoint(waypoint2);
+        assertEquals(2, data.getWaypoints().size());
+        assertTrue(data.getWaypoints().contains(waypoint1));
+        assertTrue(data.getWaypoints().contains(waypoint2));
+
+        data.removeWaypoint(waypoint1);
+        assertEquals(1, data.getWaypoints().size());
+        assertFalse(data.getWaypoints().contains(waypoint1));
+        assertTrue(data.getWaypoints().contains(waypoint2));
+    }
+
+    /**
+     * Test method for {@link GpxData#addWaypoint(WayPoint)}.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testAddWaypointFails() {
+        WayPoint waypoint1 = new WayPoint(LatLon.ZERO);
+        data.addWaypoint(waypoint1);
+        data.addWaypoint(waypoint1);
+    }
+
+    /**
+     * Test method for {@link GpxData#removeWaypoint(WayPoint)}.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testRemoveWaypointFails() {
+        WayPoint waypoint1 = new WayPoint(LatLon.ZERO);
+        data.addWaypoint(waypoint1);
+        data.removeWaypoint(waypoint1);
+        data.removeWaypoint(waypoint1);
+    }
+
+    /**
+     * Test method for {@link GpxData#hasTrackPoints()}.
+     */
+    @Test
+    public void testHasTrackPoints() {
+        assertFalse(data.hasTrackPoints());
+        ImmutableGpxTrack track1 = emptyGpxTrack();
+        data.addTrack(track1);
+        assertFalse(data.hasTrackPoints());
+        ImmutableGpxTrack track2 = singleWaypointGpxTrack();
+        data.addTrack(track2);
+        assertTrue(data.hasTrackPoints());
+    }
+
+    /**
+     * Test method for {@link GpxData#getTrackPoints()}.
+     */
+    @Test
+    public void testGetTrackPoints() {
+        assertEquals(0, data.getTrackPoints().count());
+        ImmutableGpxTrack track1 = singleWaypointGpxTrack();
+        data.addTrack(track1);
+        assertEquals(1, data.getTrackPoints().count());
+        ImmutableGpxTrack track2 = singleWaypointGpxTrack();
+        data.addTrack(track2);
+        assertEquals(2, data.getTrackPoints().count());
+    }
+
+    /**
+     * Test method for {@link GpxData#hasRoutePoints()}.
+     */
+    @Test
+    public void testHasRoutePoints() {
+        fail("Not yet implemented");
+    }
+
+    /**
+     * Test method for {@link GpxData#isEmpty()}.
+     */
+    @Test
+    public void testIsEmpty() {
+        fail("Not yet implemented");
+    }
+
+    /**
+     * Test method for {@link GpxData#getMetaBounds()}.
+     */
+    @Test
+    public void testGetMetaBounds() {
+        fail("Not yet implemented");
+    }
+
+    /**
+     * Test method for {@link GpxData#recalculateBounds()}.
+     */
+    @Test
+    public void testRecalculateBounds() {
+        fail("Not yet implemented");
+    }
+
+    /**
+     * Test method for {@link GpxData#length()}.
+     */
+    @Test
+    public void testLength() {
+        fail("Not yet implemented");
+    }
+
+    /**
+     * Test method for {@link GpxData#getMinMaxTimeForTrack(GpxTrack)}.
+     */
+    @Test
+    public void testGetMinMaxTimeForTrack() {
+        fail("Not yet implemented");
+    }
+
+    /**
+     * Test method for {@link GpxData#getMinMaxTimeForAllTracks()}.
+     */
+    @Test
+    public void testGetMinMaxTimeForAllTracks() {
+        fail("Not yet implemented");
+    }
+
+    /**
+     * Test method for {@link GpxData#nearestPointOnTrack(org.openstreetmap.josm.data.coor.EastNorth, double)}.
+     */
+    @Test
+    public void testNearestPointOnTrack() {
+        fail("Not yet implemented");
+    }
+
+    /**
+     * Test method for {@link GpxData#getLinesIterable(boolean[])}.
+     */
+    @Test
+    public void testGetLinesIterable() {
+        fail("Not yet implemented");
+    }
+
+    /**
+     * Test method for {@link GpxData#resetEastNorthCache()}.
+     */
+    @Test
+    public void testResetEastNorthCache() {
+        fail("Not yet implemented");
+    }
+
+    /**
+     * Test method for {@link GpxData#getDataSources()}.
+     */
+    @Test
+    public void testGetDataSources() {
+        fail("Not yet implemented");
+    }
+
+    /**
+     * Test method for {@link GpxData#getDataSourceArea()}.
+     */
+    @Test
+    public void testGetDataSourceArea() {
+        fail("Not yet implemented");
+    }
+
+    /**
+     * Test method for {@link GpxData#getDataSourceBounds()}.
+     */
+    @Test
+    public void testGetDataSourceBounds() {
+        fail("Not yet implemented");
+    }
+
+    /**
+     * Test method for {@link GpxData#addChangeListener(GpxData.GpxDataChangeListener)}.
+     */
+    @Test
+    public void testAddChangeListener() {
+        fail("Not yet implemented");
+    }
+
+    /**
+     * Test method for {@link GpxData#addWeakChangeListener(GpxData.GpxDataChangeListener)}.
+     */
+    @Test
+    public void testAddWeakChangeListener() {
+        fail("Not yet implemented");
+    }
+
+    /**
+     * Test method for {@link GpxData#removeChangeListener(GpxData.GpxDataChangeListener)}.
+     */
+    @Test
+    public void testRemoveChangeListener() {
+        fail("Not yet implemented");
+    }
+
+    private static ImmutableGpxTrack emptyGpxTrack() {
+        return new ImmutableGpxTrack(Collections.emptyList(), Collections.emptyMap());
+    }
+
+    private static ImmutableGpxTrack singleWaypointGpxTrack() {
+        return new ImmutableGpxTrack(Collections.singleton(Collections.singleton(new WayPoint(LatLon.ZERO))), Collections.emptyMap());
+    }
+
     /**
      * Unit test of methods {@link GpxData#equals} and {@link GpxData#hashCode}.
@@ -28,5 +348,5 @@
     public void testEqualsContract() {
         EqualsVerifier.forClass(GpxData.class).usingGetClass()
-            .withIgnoredFields("attr", "creator", "fromServer", "storageFile")
+            .withIgnoredFields("attr", "creator", "fromServer", "storageFile", "listeners")
             .withPrefabValues(WayPoint.class, new WayPoint(LatLon.NORTH_POLE), new WayPoint(LatLon.SOUTH_POLE))
             .verify();
