diff --git a/src/org/openstreetmap/josm/data/projection/AbstractProjection.java b/src/org/openstreetmap/josm/data/projection/AbstractProjection.java
index c9a5ebf..8129cbc 100644
--- a/src/org/openstreetmap/josm/data/projection/AbstractProjection.java
+++ b/src/org/openstreetmap/josm/data/projection/AbstractProjection.java
@@ -1,12 +1,18 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.data.projection;
 
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.DoubleUnaryOperator;
+
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.data.ProjectionBounds;
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.projection.datum.Datum;
 import org.openstreetmap.josm.data.projection.proj.Proj;
+import org.openstreetmap.josm.tools.Utils;
 
 /**
  * Implementation of the Projection interface that represents a coordinate reference system and delegates
@@ -115,12 +121,51 @@ public abstract class AbstractProjection implements Projection {
 
     @Override
     public LatLon eastNorth2latlon(EastNorth en) {
+        return eastNorth2latlon(en, LatLon::normalizeLon);
+    }
+
+    @Override
+    public LatLon eastNorth2latlonClamped(EastNorth en) {
+        LatLon ll = eastNorth2latlon(en, lon -> Utils.clamp(lon, -180, 180));
+        Bounds bounds = getWorldBoundsLatLon();
+        return new LatLon(Utils.clamp(ll.lat(), bounds.getMinLat(), bounds.getMaxLat()),
+                Utils.clamp(ll.lon(), bounds.getMinLon(), bounds.getMaxLon()));
+    }
+
+    private LatLon eastNorth2latlon(EastNorth en, DoubleUnaryOperator normalizeLon) {
         double[] latlonRad = proj.invproject((en.east() * toMeter - x0) / ellps.a / k0, (en.north() * toMeter - y0) / ellps.a / k0);
-        LatLon ll = new LatLon(Math.toDegrees(latlonRad[0]), LatLon.normalizeLon(Math.toDegrees(latlonRad[1]) + lon0 + pm));
+        double lon = Math.toDegrees(latlonRad[1]) + lon0 + pm;
+        LatLon ll = new LatLon(Math.toDegrees(latlonRad[0]), normalizeLon.applyAsDouble(lon));
         return datum.toWGS84(ll);
     }
 
     @Override
+    public Map<ProjectionBounds, Projecting> getProjectingsForArea(ProjectionBounds area) {
+        if (proj.lonIsLinearToEast()) {
+            //FIXME: Respect datum?
+            // wrap the wrold around
+            Bounds bounds = getWorldBoundsLatLon();
+            double minEast = latlon2eastNorth(bounds.getMin()).east();
+            double maxEast = latlon2eastNorth(bounds.getMax()).east();
+            double dEast = maxEast - minEast;
+            if ((area.minEast < minEast || area.maxEast > maxEast) && dEast > 0) {
+                // We could handle the dEast < 0 case but we don't need it atm.
+                int minChunk = (int) Math.floor((area.minEast - minEast) / dEast);
+                int maxChunk = (int) Math.floor((area.maxEast - minEast) / dEast);
+                HashMap<ProjectionBounds, Projecting> ret = new HashMap<>();
+                for (int chunk = minChunk; chunk <= maxChunk; chunk++) {
+                    ret.put(new ProjectionBounds(Math.max(area.minEast, minEast + chunk * dEast), area.minNorth,
+                            Math.min(area.maxEast, maxEast + chunk * dEast), area.maxNorth),
+                            new ShiftedProjecting(this, new EastNorth(-chunk * dEast, 0)));
+                }
+                return ret;
+            }
+        }
+
+        return Collections.singletonMap(area, this);
+    }
+
+    @Override
     public double getDefaultZoomInPPD() {
         // this will set the map scaler to about 1000 m
         return 10;
@@ -178,4 +223,9 @@ public abstract class AbstractProjection implements Projection {
         }
         return projectionBoundsBox;
     }
+
+    @Override
+    public Projection getBaseProjection() {
+        return this;
+    }
 }
diff --git a/src/org/openstreetmap/josm/data/projection/Projecting.java b/src/org/openstreetmap/josm/data/projection/Projecting.java
new file mode 100644
index 0000000..e4e35cc
--- /dev/null
+++ b/src/org/openstreetmap/josm/data/projection/Projecting.java
@@ -0,0 +1,49 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.projection;
+
+import java.util.Map;
+
+import org.openstreetmap.josm.data.ProjectionBounds;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.LatLon;
+
+/**
+ * Classes implementing this are able to project between screen (east/north) and {@link LatLon} coordinates.
+ * <p>
+ * Each instance is backed by a base projection but may e.g. offset the resulting position.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public interface Projecting {
+
+    /**
+     * Convert from lat/lon to easting/northing.
+     *
+     * @param ll the geographical point to convert (in WGS84 lat/lon)
+     * @return the corresponding east/north coordinates
+     */
+    EastNorth latlon2eastNorth(LatLon ll);
+
+    /**
+     * Convert a east/north coordinate to the {@link LatLon} coordinate. This method clamps the lat/lon coordinate to the nearest point in the world bounds.
+     * @param en east/north
+     * @return The lat/lon coordinate.
+     */
+    LatLon eastNorth2latlonClamped(EastNorth en);
+
+    /**
+     * Gets the base projection instance used.
+     * @return The projection.
+     */
+    Projection getBaseProjection();
+
+    /**
+     * Returns an map or (subarea, projecting) paris that contains projecting instances to convert the coordinates inside the given area.
+     * This can be used by projections to support continuous projections.
+     *
+     * It is possible that the area covered by the map is bigger than the one given as area. There may be holes.
+     * @param area The base area
+     * @return a map of non-overlapping {@link ProjectionBounds} instances mapped to the {@link Projecting} object to use for that area.
+     */
+    Map<ProjectionBounds, Projecting> getProjectingsForArea(ProjectionBounds area);
+}
diff --git a/src/org/openstreetmap/josm/data/projection/Projection.java b/src/org/openstreetmap/josm/data/projection/Projection.java
index b2bd016..7477210 100644
--- a/src/org/openstreetmap/josm/data/projection/Projection.java
+++ b/src/org/openstreetmap/josm/data/projection/Projection.java
@@ -13,7 +13,7 @@ import org.openstreetmap.josm.data.coor.LatLon;
  * The conversion from east/north to the screen coordinates is simply a scale
  * factor and x/y offset.
  */
-public interface Projection {
+public interface Projection extends Projecting {
     /**
      * The default scale factor in east/north units per pixel
      * ({@link org.openstreetmap.josm.gui.NavigatableComponent#getState})).
@@ -23,14 +23,6 @@ public interface Projection {
     double getDefaultZoomInPPD();
 
     /**
-     * Convert from lat/lon to easting/northing.
-     *
-     * @param ll the geographical point to convert (in WGS84 lat/lon)
-     * @return the corresponding east/north coordinates
-     */
-    EastNorth latlon2eastNorth(LatLon ll);
-
-    /**
      * Convert from easting/norting to lat/lon.
      *
      * @param en the geographical point to convert (in projected coordinates)
@@ -110,4 +102,5 @@ public interface Projection {
      * @return true if natural order of coordinates is North East, false if East North
      */
     boolean switchXY();
+
 }
diff --git a/src/org/openstreetmap/josm/data/projection/ShiftedProjecting.java b/src/org/openstreetmap/josm/data/projection/ShiftedProjecting.java
new file mode 100644
index 0000000..0727c8a
--- /dev/null
+++ b/src/org/openstreetmap/josm/data/projection/ShiftedProjecting.java
@@ -0,0 +1,55 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.projection;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.openstreetmap.josm.data.ProjectionBounds;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.LatLon;
+
+/**
+ * This is a projecting instance that shifts the projection by a given eastnorth offset.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public class ShiftedProjecting implements Projecting {
+    private final Projecting base;
+    private final EastNorth offset;
+
+    /**
+     * Create a new {@link ShiftedProjecting}
+     * @param base The base to use
+     * @param offset The offset to move base. Subtracted when converting lat/lon->east/north.
+     */
+    public ShiftedProjecting(Projecting base, EastNorth offset) {
+        this.base = base;
+        this.offset = offset;
+    }
+
+    @Override
+    public EastNorth latlon2eastNorth(LatLon ll) {
+        return base.latlon2eastNorth(ll).add(offset);
+    }
+
+    @Override
+    public LatLon eastNorth2latlonClamped(EastNorth en) {
+        return base.eastNorth2latlonClamped(en.subtract(offset));
+    }
+
+    @Override
+    public Projection getBaseProjection() {
+        return base.getBaseProjection();
+    }
+
+    @Override
+    public Map<ProjectionBounds, Projecting> getProjectingsForArea(ProjectionBounds area) {
+        Map<ProjectionBounds, Projecting> forArea = base
+                .getProjectingsForArea(new ProjectionBounds(area.getMin().subtract(offset), area.getMax().subtract(offset)));
+        HashMap<ProjectionBounds, Projecting> ret = new HashMap<>();
+        forArea.forEach((pb, projecting) -> ret.put(
+                new ProjectionBounds(pb.getMin().add(offset), pb.getMax().add(offset)),
+                new ShiftedProjecting(projecting, offset)));
+        return ret;
+    }
+}
diff --git a/src/org/openstreetmap/josm/data/projection/proj/Mercator.java b/src/org/openstreetmap/josm/data/projection/proj/Mercator.java
index 10d61ca..592d9ef 100644
--- a/src/org/openstreetmap/josm/data/projection/proj/Mercator.java
+++ b/src/org/openstreetmap/josm/data/projection/proj/Mercator.java
@@ -119,4 +119,9 @@ public class Mercator extends AbstractProj implements IScaleFactorProvider {
     public double getScaleFactor() {
         return scaleFactor;
     }
+
+    @Override
+    public boolean lonIsLinearToEast() {
+        return true;
+    }
 }
diff --git a/src/org/openstreetmap/josm/data/projection/proj/Proj.java b/src/org/openstreetmap/josm/data/projection/proj/Proj.java
index fa9b3fe..b2bf5a5 100644
--- a/src/org/openstreetmap/josm/data/projection/proj/Proj.java
+++ b/src/org/openstreetmap/josm/data/projection/proj/Proj.java
@@ -90,4 +90,14 @@ public interface Proj {
      * @return true, if it is geographic
      */
     boolean isGeographic();
+
+    /**
+     * Checks wether the result of projecting a lon coordinate only has a linear relation to the east coordinate and
+     * is not related to lat/north at all.
+     * @return <code>true</code> if lon has a linear relationship to east only.
+     * @since xxx
+     */
+    default boolean lonIsLinearToEast() {
+        return false;
+    }
 }
diff --git a/src/org/openstreetmap/josm/gui/MapViewState.java b/src/org/openstreetmap/josm/gui/MapViewState.java
index f7dcd8d..cd7b292 100644
--- a/src/org/openstreetmap/josm/gui/MapViewState.java
+++ b/src/org/openstreetmap/josm/gui/MapViewState.java
@@ -16,6 +16,7 @@ import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.data.ProjectionBounds;
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.projection.Projecting;
 import org.openstreetmap.josm.data.projection.Projection;
 import org.openstreetmap.josm.gui.download.DownloadDialog;
 import org.openstreetmap.josm.tools.bugreport.BugReport;
@@ -27,7 +28,7 @@ import org.openstreetmap.josm.tools.bugreport.BugReport;
  */
 public final class MapViewState {
 
-    private final Projection projection;
+    private final Projecting projecting;
 
     private final int viewWidth;
     private final int viewHeight;
@@ -50,8 +51,8 @@ public final class MapViewState {
      * @param scale The scale to use
      * @param topLeft The top left corner in east/north space.
      */
-    private MapViewState(Projection projection, int viewWidth, int viewHeight, double scale, EastNorth topLeft) {
-        this.projection = projection;
+    private MapViewState(Projecting projection, int viewWidth, int viewHeight, double scale, EastNorth topLeft) {
+        this.projecting = projection;
         this.scale = scale;
         this.topLeft = topLeft;
 
@@ -62,7 +63,7 @@ public final class MapViewState {
     }
 
     private MapViewState(EastNorth topLeft, MapViewState mapViewState) {
-        this.projection = mapViewState.projection;
+        this.projecting = mapViewState.projecting;
         this.scale = mapViewState.scale;
         this.topLeft = topLeft;
 
@@ -73,7 +74,7 @@ public final class MapViewState {
     }
 
     private MapViewState(double scale, MapViewState mapViewState) {
-        this.projection = mapViewState.projection;
+        this.projecting = mapViewState.projecting;
         this.scale = scale;
         this.topLeft = mapViewState.topLeft;
 
@@ -84,7 +85,7 @@ public final class MapViewState {
     }
 
     private MapViewState(JComponent position, MapViewState mapViewState) {
-        this.projection = mapViewState.projection;
+        this.projecting = mapViewState.projecting;
         this.scale = mapViewState.scale;
         this.topLeft = mapViewState.topLeft;
 
@@ -105,8 +106,8 @@ public final class MapViewState {
         }
     }
 
-    private MapViewState(Projection projection, MapViewState mapViewState) {
-        this.projection = projection;
+    private MapViewState(Projecting projecting, MapViewState mapViewState) {
+        this.projecting = projecting;
         this.scale = mapViewState.scale;
         this.topLeft = mapViewState.topLeft;
 
@@ -200,7 +201,7 @@ public final class MapViewState {
      * @return The projection.
      */
     public Projection getProjection() {
-        return projection;
+        return projecting.getBaseProjection();
     }
 
     /**
@@ -268,7 +269,7 @@ public final class MapViewState {
      * @since 10486
      */
     public MapViewState usingProjection(Projection projection) {
-        if (projection.equals(this.projection)) {
+        if (projection.equals(this.projecting)) {
             return this;
         } else {
             return new MapViewState(projection, this);
@@ -357,9 +358,19 @@ public final class MapViewState {
         /**
          * Gets the current position in LatLon coordinates according to the current projection.
          * @return The positon as LatLon.
+         * @see #getLatLonClamped()
          */
         public LatLon getLatLon() {
-            return projection.eastNorth2latlon(getEastNorth());
+            return projecting.getBaseProjection().eastNorth2latlon(getEastNorth());
+        }
+
+        /**
+         * Gets the latlon coordinate clamped to the current world area.
+         * @return The lat/lon coordinate
+         * @since xxx
+         */
+        public LatLon getLatLonClamped() {
+            return projecting.eastNorth2latlonClamped(getEastNorth());
         }
 
         /**
@@ -473,7 +484,8 @@ public final class MapViewState {
          * @since 10458
          */
         public Bounds getLatLonBoundsBox() {
-            return projection.getLatLonBoundsBox(getProjectionBounds());
+            // TODO @michael2402: Use hillclimb.
+            return projecting.getBaseProjection().getLatLonBoundsBox(getProjectionBounds());
         }
 
         /**
diff --git a/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
index 52dcbe9..2179312 100644
--- a/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
+++ b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
@@ -230,7 +230,7 @@ implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi
     }
 
     protected void initTileSource(T tileSource) {
-        coordinateConverter = new TileCoordinateConverter(Main.map.mapView, getDisplaySettings());
+        coordinateConverter = new TileCoordinateConverter(Main.map.mapView, tileSource, getDisplaySettings());
         attribution.initialize(tileSource);
 
         currentZoomLevel = getBestZoom();
@@ -367,17 +367,11 @@ implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi
      * @return average number of screen pixels per tile pixel
      */
     private double getScaleFactor(int zoom) {
-        if (!Main.isDisplayingMapView()) return 1;
-        MapView mv = Main.map.mapView;
-        LatLon topLeft = mv.getLatLon(0, 0);
-        LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
-        TileXY t1 = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom);
-        TileXY t2 = tileSource.latLonToTileXY(botRight.toCoordinate(), zoom);
-
-        int screenPixels = mv.getWidth()*mv.getHeight();
-        double tilePixels = Math.abs((t2.getY()-t1.getY())*(t2.getX()-t1.getX())*tileSource.getTileSize()*tileSource.getTileSize());
-        if (screenPixels == 0 || tilePixels == 0) return 1;
-        return screenPixels/tilePixels;
+        if (coordinateConverter != null) {
+            return coordinateConverter.getScaleFactor(zoom);
+        } else {
+            return 1;
+        }
     }
 
     protected int getBestZoom() {
@@ -1245,7 +1239,7 @@ implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi
     }
 
     private LatLon getShiftedLatLon(EastNorth en) {
-        return Main.getProjection().eastNorth2latlon(en.add(-getDisplaySettings().getDx(), -getDisplaySettings().getDy()));
+        return coordinateConverter.getProjecting().eastNorth2latlonClamped(en);
     }
 
     private ICoordinate getShiftedCoord(EastNorth en) {
@@ -1516,10 +1510,10 @@ implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi
 
     @Override
     public void paint(Graphics2D g, MapView mv, Bounds bounds) {
-        ProjectionBounds pb = mv.getState().getViewArea().getProjectionBounds();
-
-        needRedraw = false;
+        // old and unused.
+    }
 
+    private void drawInViewArea(Graphics2D g, MapView mv, ProjectionBounds pb) {
         int zoom = currentZoomLevel;
         if (getDisplaySettings().isAutoZoom()) {
             zoom = getBestZoom();
@@ -1894,10 +1888,18 @@ implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi
         public void paint(MapViewGraphics graphics) {
             allocateCacheMemory();
             if (memory != null) {
-                super.paint(graphics);
+                doPaint(graphics);
             }
         }
 
+        private void doPaint(MapViewGraphics graphics) {
+            ProjectionBounds pb = graphics.getClipBounds().getProjectionBounds();
+
+            needRedraw = false; // TEMPORARY
+
+            drawInViewArea(graphics.getDefaultGraphics(), graphics.getMapView(), pb);
+        }
+
         private void allocateCacheMemory() {
             if (memory == null) {
                 MemoryManager manager = MemoryManager.getInstance();
diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java b/src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java
index e8c925d..1489ff3 100644
--- a/src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java
+++ b/src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java
@@ -5,8 +5,12 @@ import java.awt.geom.Point2D;
 import java.awt.geom.Rectangle2D;
 
 import org.openstreetmap.gui.jmapviewer.Tile;
+import org.openstreetmap.gui.jmapviewer.TileXY;
 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
 import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.projection.Projecting;
+import org.openstreetmap.josm.data.projection.ShiftedProjecting;
 import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
 
@@ -18,14 +22,17 @@ import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
 public class TileCoordinateConverter {
     private MapView mapView;
     private TileSourceDisplaySettings settings;
+    private TileSource tileSource;
 
     /**
      * Create a new coordinate converter for the map view.
      * @param mapView The map view.
+     * @param tileSource The tile source to use when converting coordinates.
      * @param settings displacement settings.
      */
-    public TileCoordinateConverter(MapView mapView, TileSourceDisplaySettings settings) {
+    public TileCoordinateConverter(MapView mapView, TileSource tileSource,  TileSourceDisplaySettings settings) {
         this.mapView = mapView;
+        this.tileSource = tileSource;
         this.settings = settings;
     }
 
@@ -34,6 +41,14 @@ public class TileCoordinateConverter {
     }
 
     /**
+     * Gets the projecting instance to use to convert between latlon and eastnorth coordinates.
+     * @return The {@link Projecting} instance.
+     */
+    public Projecting getProjecting() {
+        return new ShiftedProjecting(mapView.getProjection(), settings.getDisplacement());
+    }
+
+    /**
      * Gets the top left position of the tile inside the map view.
      * @param tile The tile
      * @return The positon.
@@ -54,4 +69,21 @@ public class TileCoordinateConverter {
 
         return pos(c1).rectTo(pos(c2)).getInView();
     }
+
+    /**
+     * Returns average number of screen pixels per tile pixel for current mapview
+     * @param zoom zoom level
+     * @return average number of screen pixels per tile pixel
+     */
+    public double getScaleFactor(int zoom) {
+        LatLon topLeft = mapView.getLatLon(0, 0);
+        LatLon botRight = mapView.getLatLon(mapView.getWidth(), mapView.getHeight());
+        TileXY t1 = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom);
+        TileXY t2 = tileSource.latLonToTileXY(botRight.toCoordinate(), zoom);
+
+        int screenPixels = mapView.getWidth()*mapView.getHeight();
+        double tilePixels = Math.abs((t2.getY()-t1.getY())*(t2.getX()-t1.getX())*tileSource.getTileSize()*tileSource.getTileSize());
+        if (screenPixels == 0 || tilePixels == 0) return 1;
+        return screenPixels/tilePixels;
+    }
 }
diff --git a/src/org/openstreetmap/josm/tools/Utils.java b/src/org/openstreetmap/josm/tools/Utils.java
index 0b92733..84968ba 100644
--- a/src/org/openstreetmap/josm/tools/Utils.java
+++ b/src/org/openstreetmap/josm/tools/Utils.java
@@ -1549,4 +1549,21 @@ public final class Utils {
             });
         }
     }
+
+    /**
+     * Clamp a value to the given range
+     * @param val The value
+     * @param min minimum value
+     * @param max maximum value
+     * @return the value
+     */
+    public static double clamp(double val, double min, double max) {
+        if (val < min) {
+            return min;
+        } else if (val > max) {
+            return max;
+        } else {
+            return val;
+        }
+    }
 }
diff --git a/test/unit/org/openstreetmap/josm/data/projection/ShiftedProjectionTest.java b/test/unit/org/openstreetmap/josm/data/projection/ShiftedProjectionTest.java
new file mode 100644
index 0000000..7748a44
--- /dev/null
+++ b/test/unit/org/openstreetmap/josm/data/projection/ShiftedProjectionTest.java
@@ -0,0 +1,148 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.projection;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
+
+import org.junit.Test;
+import org.openstreetmap.josm.data.ProjectionBounds;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.LatLon;
+
+/**
+ * Tests for {@link ShiftedProjecting}
+ * @author Michael Zangl
+ * @since xxx
+ */
+public class ShiftedProjectionTest {
+    private static final class ProjectingBase implements Projecting {
+        @Override
+        public EastNorth latlon2eastNorth(LatLon ll) {
+            return new EastNorth(ll.lat() * 2, ll.lon() * 3);
+        }
+
+        @Override
+        public Map<ProjectionBounds, Projecting> getProjectingsForArea(ProjectionBounds area) {
+            HashMap<ProjectionBounds, Projecting> map = new HashMap<>();
+            // split at east = 0
+            if (area.minEast < 0) {
+                map.put(new ProjectionBounds(area.minEast, area.minNorth, Math.min(area.maxEast, 0), area.maxNorth), this);
+            }
+            if (area.maxEast > 0) {
+                map.put(new ProjectionBounds(Math.max(area.minEast, 0), area.minNorth, area.maxEast, area.maxNorth), this);
+            }
+
+            return map;
+        }
+
+        @Override
+        public Projection getBaseProjection() {
+            throw new AssertionError();
+        }
+
+        @Override
+        public LatLon eastNorth2latlonClamped(EastNorth en) {
+            return new LatLon(en.east() / 2, en.north() / 3);
+        }
+    }
+
+    /**
+     * Test {@link ShiftedProjecting#latlon2eastNorth(LatLon)}
+     */
+    @Test
+    public void testLatlon2eastNorth() {
+        Projecting base = new ProjectingBase();
+
+        ShiftedProjecting unshifted = new ShiftedProjecting(base, new EastNorth(0, 0));
+        EastNorth unshift_00 = unshifted.latlon2eastNorth(new LatLon(0, 0));
+        assertEquals(0, unshift_00.east(), 1e-10);
+        assertEquals(0, unshift_00.north(), 1e-10);
+        EastNorth unshift_12 = unshifted.latlon2eastNorth(new LatLon(1, 2));
+        assertEquals(2, unshift_12.east(), 1e-10);
+        assertEquals(6, unshift_12.north(), 1e-10);
+
+        ShiftedProjecting shifted = new ShiftedProjecting(base, new EastNorth(5, 7));
+        EastNorth shift_00 = shifted.latlon2eastNorth(new LatLon(0, 0));
+        assertEquals(5, shift_00.east(), 1e-10);
+        assertEquals(7, shift_00.north(), 1e-10);
+        EastNorth shift_12 = shifted.latlon2eastNorth(new LatLon(1, 2));
+        assertEquals(2 + 5, shift_12.east(), 1e-10);
+        assertEquals(6 + 7, shift_12.north(), 1e-10);
+    }
+
+    /**
+     * Test {@link ShiftedProjecting#eastNorth2latlonClamped(EastNorth)}
+     */
+    @Test
+    public void testEastNorth2latlonClamped() {
+        Projecting base = new ProjectingBase();
+
+        ShiftedProjecting unshifted = new ShiftedProjecting(base, new EastNorth(0, 0));
+        LatLon unshift_00 = unshifted.eastNorth2latlonClamped(new EastNorth(0, 0));
+        assertEquals(0, unshift_00.lat(), 1e-10);
+        assertEquals(0, unshift_00.lon(), 1e-10);
+        LatLon unshift_12 = unshifted.eastNorth2latlonClamped(new EastNorth(2, 6));
+        assertEquals(1, unshift_12.lat(), 1e-10);
+        assertEquals(2, unshift_12.lon(), 1e-10);
+
+        ShiftedProjecting shifted = new ShiftedProjecting(base, new EastNorth(5, 7));
+        LatLon shift_00 = shifted.eastNorth2latlonClamped(new EastNorth(5, 7));
+        assertEquals(0, shift_00.lat(), 1e-10);
+        assertEquals(0, shift_00.lon(), 1e-10);
+        LatLon shift_12 = shifted.eastNorth2latlonClamped(new EastNorth(2 + 5, 6 + 7));
+        assertEquals(1, shift_12.lat(), 1e-10);
+        assertEquals(2, shift_12.lon(), 1e-10);
+    }
+
+    /**
+     * Test {@link ShiftedProjecting#getProjectingsForArea(ProjectionBounds)}, single area case
+     */
+    @Test
+    public void testGetProjectingsForArea() {
+        Projecting base = new ProjectingBase();
+        ShiftedProjecting shifted = new ShiftedProjecting(base, new EastNorth(5, 7));
+
+        ProjectionBounds area = new ProjectionBounds(10, 0, 20, 20);
+
+        Map<ProjectionBounds, Projecting> areas = shifted.getProjectingsForArea(area);
+        assertEquals(1, areas.size());
+        ProjectionBounds pb = areas.keySet().iterator().next();
+        assertEquals(area.minEast, pb.minEast, 1e-7);
+        assertEquals(area.maxEast, pb.maxEast, 1e-7);
+        assertEquals(area.minNorth, pb.minNorth, 1e-7);
+        assertEquals(area.maxNorth, pb.maxNorth, 1e-7);
+    }
+
+    /**
+     * Test {@link ShiftedProjecting#getProjectingsForArea(ProjectionBounds)}, multiple area case
+     */
+    @Test
+    public void testGetProjectingsForAreaMultiple() {
+        Projecting base = new ProjectingBase();
+        ShiftedProjecting shifted = new ShiftedProjecting(base, new EastNorth(5, 7));
+
+        ProjectionBounds area = new ProjectionBounds(-10, 0, 20, 20);
+
+        // breach is at:
+        EastNorth breachAt = shifted.latlon2eastNorth(base.eastNorth2latlonClamped(new EastNorth(0, 0)));
+        assertEquals(5, breachAt.east(), 1e-7);
+
+        Map<ProjectionBounds, Projecting> areas = shifted.getProjectingsForArea(area);
+        assertEquals(2, areas.size());
+        List<Entry<ProjectionBounds, Projecting>> entries = areas.entrySet().stream().sorted(Comparator.comparingDouble(b -> b.getKey().minEast)).collect(Collectors.toList());
+        assertEquals(area.minEast, entries.get(0).getKey().minEast, 1e-7);
+        assertEquals(5, entries.get(0).getKey().maxEast, 1e-7);
+        assertEquals(area.minNorth, entries.get(0).getKey().minNorth, 1e-7);
+        assertEquals(area.maxNorth, entries.get(0).getKey().maxNorth, 1e-7);
+        assertEquals(5, entries.get(1).getKey().minEast, 1e-7);
+        assertEquals(area.maxEast, entries.get(1).getKey().maxEast, 1e-7);
+        assertEquals(area.minNorth, entries.get(1).getKey().minNorth, 1e-7);
+        assertEquals(area.maxNorth, entries.get(1).getKey().maxNorth, 1e-7);
+    }
+}
