Index: trunk/src/org/openstreetmap/josm/gui/layer/imagery/ReprojectionTile.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/imagery/ReprojectionTile.java	(revision 11896)
+++ trunk/src/org/openstreetmap/josm/gui/layer/imagery/ReprojectionTile.java	(revision 11897)
@@ -103,5 +103,5 @@
         double scaleMapView = Main.map.mapView.getScale();
         ImageWarp.Interpolation interpolation;
-        switch (Main.pref.get("imagery.warp.interpolation", "bilinear")) {
+        switch (Main.pref.get("imagery.warp.pixel-interpolation", "bilinear")) {
             case "nearest_neighbor":
                 interpolation = ImageWarp.Interpolation.NEAREST_NEIGHBOR;
@@ -148,7 +148,14 @@
                 (pbTargetAligned.maxNorth - en11Current.north()) / scale);
 
+        ImageWarp.PointTransform transform;
+        int stride = Main.pref.getInteger("imagery.warp.projection-interpolation.stride", 7);
+        if (stride > 0) {
+            transform = new ImageWarp.GridTransform(pointTransform, stride);
+        } else {
+            transform = pointTransform;
+        }
         BufferedImage imageOut = ImageWarp.warp(
-                imageIn, getDimension(pbTargetAligned, scale), pointTransform,
-                interpolation);
+                imageIn, getDimension(pbTargetAligned, scale),
+                transform, interpolation);
         synchronized (this) {
             this.image = imageOut;
Index: trunk/src/org/openstreetmap/josm/tools/ImageWarp.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/ImageWarp.java	(revision 11896)
+++ trunk/src/org/openstreetmap/josm/tools/ImageWarp.java	(revision 11897)
@@ -6,4 +6,8 @@
 import java.awt.geom.Rectangle2D;
 import java.awt.image.BufferedImage;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
 
 /**
@@ -20,4 +24,95 @@
     public interface PointTransform {
         Point2D transform(Point2D pt);
+    }
+
+    /**
+     * Wrapper that optimizes a given {@link ImageWarp.PointTransform}.
+     *
+     * It does so by spanning a grid with certain step size. It will invoke the
+     * potentially expensive master transform only at those grid points and use
+     * bilinear interpolation to approximate transformed values in between.
+     * <p>
+     * For memory optimization, this class assumes that rows are more or less scanned
+     * one-by-one as is done in {@link ImageWarp#warp}. I.e. this transform is <em>not</em>
+     * random access in the y coordinate.
+     */
+    public static class GridTransform implements ImageWarp.PointTransform {
+
+        private final double stride;
+        private final ImageWarp.PointTransform trfm;
+
+        private final Map<Integer, Map<Integer, Point2D>> cache;
+
+        private static final boolean CONSISTENCY_TEST = false;
+        private final Set<Integer> deletedRows;
+
+        /**
+         * Create a new GridTransform.
+         * @param trfm the master transform, that needs to be optimized
+         * @param stride step size
+         */
+        public GridTransform(ImageWarp.PointTransform trfm, double stride) {
+            this.trfm = trfm;
+            this.stride = stride;
+            this.cache = new HashMap<>();
+            if (CONSISTENCY_TEST) {
+                deletedRows = new HashSet<>();
+            } else {
+                deletedRows = null;
+            }
+        }
+
+        @Override
+        public Point2D transform(Point2D pt) {
+            int xIdx = (int) Math.floor(pt.getX() / stride);
+            int yIdx = (int) Math.floor(pt.getY() / stride);
+            double dx = pt.getX() / stride - xIdx;
+            double dy = pt.getY() / stride - yIdx;
+            Point2D value00 = getValue(xIdx, yIdx);
+            Point2D value01 = getValue(xIdx, yIdx + 1);
+            Point2D value10 = getValue(xIdx + 1, yIdx);
+            Point2D value11 = getValue(xIdx + 1, yIdx + 1);
+            double valueX = (value00.getX() * (1-dx) + value10.getX() * dx) * (1-dy) +
+                    (value01.getX() * (1-dx) + value11.getX() * dx) * dy;
+            double valueY = (value00.getY() * (1-dx) + value10.getY() * dx) * (1-dy) +
+                    (value01.getY() * (1-dx) + value11.getY() * dx) * dy;
+            return new Point2D.Double(valueX, valueY);
+        }
+
+        private Point2D getValue(int xIdx, int yIdx) {
+            Map<Integer, Point2D> row = getRow(yIdx);
+            Point2D val = row.get(xIdx);
+            if (val == null) {
+                val = trfm.transform(new Point2D.Double(xIdx * stride, yIdx * stride));
+                row.put(xIdx, val);
+            }
+            return val;
+        }
+
+        private Map<Integer, Point2D> getRow(int yIdx) {
+            cleanUp(yIdx - 2);
+            Map<Integer, Point2D> row = cache.get(yIdx);
+            if (row == null) {
+                row = new HashMap<>();
+                cache.put(yIdx, row);
+                if (CONSISTENCY_TEST) {
+                    // should not create a row that has been deleted before
+                    if (deletedRows.contains(yIdx)) throw new AssertionError();
+                    // only ever cache 2 rows at once
+                    if (cache.size() > 2) throw new AssertionError();
+                }
+            }
+            return row;
+        }
+
+        // remove rows from cache that will no longer be used
+        private void cleanUp(int yIdx) {
+            Map<Integer, Point2D> del = cache.remove(yIdx);
+            if (CONSISTENCY_TEST && del != null) {
+                // should delete each row only once
+                if (deletedRows.contains(yIdx)) throw new AssertionError();
+                deletedRows.add(yIdx);
+            }
+        }
     }
 
