Index: src/org/openstreetmap/josm/data/imagery/AbstractWMSTileSource.java
===================================================================
--- src/org/openstreetmap/josm/data/imagery/AbstractWMSTileSource.java	(revision 11857)
+++ src/org/openstreetmap/josm/data/imagery/AbstractWMSTileSource.java	(working copy)
@@ -10,7 +10,6 @@
 import org.openstreetmap.gui.jmapviewer.interfaces.IProjected;
 import org.openstreetmap.gui.jmapviewer.tilesources.TMSTileSource;
 import org.openstreetmap.gui.jmapviewer.tilesources.TileSourceInfo;
-import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.data.ProjectionBounds;
 import org.openstreetmap.josm.data.coor.EastNorth;
@@ -31,13 +30,15 @@
     private int[] tileYMax;
     private double[] degreesPerTile;
     private static final float SCALE_DENOMINATOR_ZOOM_LEVEL_1 = 559082264.0287178f;
+    private Projection tileProjection;
 
     /**
      * Constructs a new {@code AbstractWMSTileSource}.
      * @param info tile source info
      */
-    public AbstractWMSTileSource(TileSourceInfo info) {
+    public AbstractWMSTileSource(TileSourceInfo info, Projection tileProjection) {
         super(info);
+        this.tileProjection = tileProjection;
     }
 
     private void initAnchorPosition(Projection proj) {
@@ -47,11 +48,19 @@
         this.anchorPosition = new EastNorth(min.east(), max.north());
     }
 
+    public void setTileProjection(Projection tileProjection) {
+        this.tileProjection = tileProjection;
+    }
+
+    public Projection getTileProjection() {
+        return this.tileProjection;
+    }
+
     /**
      * Initializes class with current projection in JOSM. This call is needed every time projection changes.
      */
     public void initProjection() {
-        initProjection(Main.getProjection());
+        initProjection(this.tileProjection);
     }
 
     /**
@@ -98,7 +107,7 @@
 
     @Override
     public ICoordinate tileXYToLatLon(int x, int y, int zoom) {
-        return Main.getProjection().eastNorth2latlon(getTileEastNorth(x, y, zoom)).toCoordinate();
+        return tileProjection.eastNorth2latlon(getTileEastNorth(x, y, zoom)).toCoordinate();
     }
 
     private TileXY eastNorthToTileXY(EastNorth enPoint, int zoom) {
@@ -111,7 +120,7 @@
 
     @Override
     public TileXY latLonToTileXY(double lat, double lon, int zoom) {
-        EastNorth enPoint = Main.getProjection().latlon2eastNorth(new LatLon(lat, lon));
+        EastNorth enPoint = tileProjection.latlon2eastNorth(new LatLon(lat, lon));
         return eastNorthToTileXY(enPoint, zoom);
     }
 
@@ -143,7 +152,7 @@
     @Override
     public Point latLonToXY(double lat, double lon, int zoom) {
         double scale = getDegreesPerTile(zoom) / getTileSize();
-        EastNorth point = Main.getProjection().latlon2eastNorth(new LatLon(lat, lon));
+        EastNorth point = tileProjection.latlon2eastNorth(new LatLon(lat, lon));
         return new Point(
                 (int) Math.round((point.east() - anchorPosition.east()) / scale),
                 (int) Math.round((anchorPosition.north() - point.north()) / scale)
@@ -163,12 +172,11 @@
     @Override
     public ICoordinate xyToLatLon(int x, int y, int zoom) {
         double scale = getDegreesPerTile(zoom) / getTileSize();
-        Projection proj = Main.getProjection();
         EastNorth ret = new EastNorth(
                 anchorPosition.east() + x * scale,
                 anchorPosition.north() - y * scale
                 );
-        return proj.eastNorth2latlon(ret).toCoordinate();
+        return tileProjection.eastNorth2latlon(ret).toCoordinate();
     }
 
     protected EastNorth getTileEastNorth(int x, int y, int z) {
@@ -196,6 +204,6 @@
 
     @Override
     public String getServerCRS() {
-        return Main.getProjection().toCode();
+        return this.tileProjection.toCode();
     }
 }
Index: src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
===================================================================
--- src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java	(revision 11857)
+++ src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java	(working copy)
@@ -45,7 +45,7 @@
     private static final Logger LOG = FeatureAdapter.getLogger(TMSCachedTileLoaderJob.class.getCanonicalName());
     private static final LongProperty MAXIMUM_EXPIRES = new LongProperty("imagery.generic.maximum_expires", TimeUnit.DAYS.toMillis(30));
     private static final LongProperty MINIMUM_EXPIRES = new LongProperty("imagery.generic.minimum_expires", TimeUnit.HOURS.toMillis(1));
-    private final Tile tile;
+    protected final Tile tile;
     private volatile URL url;
 
     // we need another deduplication of Tile Loader listeners, as for each submit, new TMSCachedTileLoaderJob was created
Index: src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java
===================================================================
--- src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java	(revision 11857)
+++ src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java	(working copy)
@@ -18,6 +18,7 @@
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.projection.Projection;
 import org.openstreetmap.josm.gui.layer.WMSLayer;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
 
@@ -54,8 +55,8 @@
      * Creates a tile source based on imagery info
      * @param info imagery info
      */
-    public TemplatedWMSTileSource(ImageryInfo info) {
-        super(info);
+    public TemplatedWMSTileSource(ImageryInfo info, Projection tileProjection) {
+        super(info, tileProjection);
         this.serverProjections = new TreeSet<>(info.getServerProjections());
         handleTemplate();
         initProjection();
@@ -68,7 +69,7 @@
 
     @Override
     public String getTileUrl(int zoom, int tilex, int tiley) {
-        String myProjCode = Main.getProjection().toCode();
+        String myProjCode = getServerCRS();
 
         EastNorth nw = getTileEastNorth(tilex, tiley, zoom);
         EastNorth se = getTileEastNorth(tilex + 1, tiley + 1, zoom);
@@ -79,6 +80,7 @@
         double s = se.getY();
         double e = se.getX();
 
+        // FIXME
         if (!serverProjections.contains(myProjCode) && serverProjections.contains("EPSG:4326") && "EPSG:3857".equals(myProjCode)) {
             LatLon swll = Main.getProjection().eastNorth2latlon(new EastNorth(w, s));
             LatLon nell = Main.getProjection().eastNorth2latlon(new EastNorth(e, n));
Index: src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java
===================================================================
--- src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java	(revision 11857)
+++ src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java	(working copy)
@@ -7,7 +7,6 @@
 import org.apache.commons.jcs.access.behavior.ICacheAccess;
 import org.openstreetmap.gui.jmapviewer.Tile;
 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
-import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
 
 /**
@@ -39,7 +38,7 @@
         // include projection in cache key, as with different projections different response will be returned from server
         String key = super.getCacheKey();
         if (key != null) {
-            return key + Main.getProjection().toCode();
+            return key + tile.getSource().getServerCRS();
         }
         return null;
     }
Index: src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java
===================================================================
--- src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java	(revision 11857)
+++ src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java	(working copy)
@@ -61,7 +61,7 @@
 import org.openstreetmap.josm.tools.Utils;
 
 /**
- * Tile Source handling WMS providers
+ * Tile Source handling WMTS providers
  *
  * @author Wiktor Niesiobędzki
  * @since 8526
@@ -268,6 +268,8 @@
 
     private final WMTSDefaultLayer defaultLayer;
 
+    private Projection tileProjection;
+
     /**
      * Creates a tile source based on imagery info
      * @param info imagery info
@@ -597,36 +599,45 @@
      * @param proj projection to be used by this TileSource
      */
     public void initProjection(Projection proj) {
-        // getLayers will return only layers matching the name, if the user already choose the layer
-        // so we will not ask the user again to chose the layer, if he just changes projection
-        Collection<Layer> candidates = getLayers(
-                currentLayer != null ? new WMTSDefaultLayer(currentLayer.identifier, currentLayer.tileMatrixSet.identifier) : defaultLayer,
-                proj.toCode());
-
-        if (candidates.size() > 1 && defaultLayer != null) {
-            candidates = candidates.stream()
-                    .filter(t -> t.tileMatrixSet.identifier.equals(defaultLayer.getTileMatrixSet()))
-                    .collect(Collectors.toList());
-        }
-        if (candidates.size() == 1) {
-            Layer newLayer = candidates.iterator().next();
-            if (newLayer != null) {
-                this.currentTileMatrixSet = newLayer.tileMatrixSet;
-                this.currentLayer = newLayer;
-                Collection<Double> scales = new ArrayList<>(currentTileMatrixSet.tileMatrix.size());
-                for (TileMatrix tileMatrix : currentTileMatrixSet.tileMatrix) {
-                    scales.add(tileMatrix.scaleDenominator * 0.28e-03);
+        if (proj.equals(tileProjection))
+            return;
+        List<Layer> matchingLayers = layers.stream().filter(
+                l -> l.identifier.equals(defaultLayer.layerName) && l.tileMatrixSet.crs.equals(proj.toCode()))
+                .collect(Collectors.toList());
+        if (matchingLayers.size() > 1) {
+            this.currentLayer = matchingLayers.stream().filter(
+                    l -> l.tileMatrixSet.identifier.equals(defaultLayer.getTileMatrixSet()))
+                    .findFirst().orElse(null);
+            this.tileProjection = proj;
+        } else if (matchingLayers.size() == 1) {
+            this.currentLayer = matchingLayers.get(0);
+            this.tileProjection = proj;
+        } else {
+            // no tile matrix sets with current projection
+            if (this.currentLayer == null) {
+                this.tileProjection = null;
+                for (Layer layer : layers) {
+                    if (!layer.identifier.equals(defaultLayer.layerName)) {
+                        continue;
+                    }
+                    Projection pr = Projections.getProjectionByCode(layer.tileMatrixSet.crs);
+                    if (pr != null) {
+                        this.currentLayer = layer;
+                        this.tileProjection = pr;
+                        break;
+                    }
                 }
-                this.nativeScaleList = new ScaleList(scales);
-            }
-        } else if (candidates.size() > 1) {
-            Main.warn("More than one layer WMTS available: {0} for projection {1} and name {2}. Do not know which to process",
-                    candidates.stream().map(x -> x.getUserTitle() + ": " + x.tileMatrixSet.identifier).collect(Collectors.joining(", ")),
-                    proj.toCode(),
-                    currentLayer != null ? currentLayer.getUserTitle() : defaultLayer
-                    );
+                if (this.currentLayer == null)
+                    return;
+            } // else: keep currentLayer and tileProjection as is
         }
-        this.crsScale = getTileSize() * 0.28e-03 / proj.getMetersPerUnit();
+        this.currentTileMatrixSet = this.currentLayer.tileMatrixSet;
+        Collection<Double> scales = new ArrayList<>(currentTileMatrixSet.tileMatrix.size());
+        for (TileMatrix tileMatrix : currentTileMatrixSet.tileMatrix) {
+            scales.add(tileMatrix.scaleDenominator * 0.28e-03);
+        }
+        this.nativeScaleList = new ScaleList(scales);
+        this.crsScale = getTileSize() * 0.28e-03 / this.tileProjection.getMetersPerUnit();
     }
 
     /**
@@ -654,7 +665,7 @@
     public int getTileSize() {
         // no support for non-square tiles (tileHeight != tileWidth)
         // and for different tile sizes at different zoom levels
-        Collection<Layer> projLayers = getLayers(null, Main.getProjection().toCode());
+        Collection<Layer> projLayers = getLayers(null, tileProjection.toCode());
         if (!projLayers.isEmpty()) {
             return projLayers.iterator().next().tileMatrixSet.tileMatrix.get(0).tileHeight;
         }
@@ -735,11 +746,11 @@
     public ICoordinate tileXYToLatLon(int x, int y, int zoom) {
         TileMatrix matrix = getTileMatrix(zoom);
         if (matrix == null) {
-            return Main.getProjection().getWorldBoundsLatLon().getCenter().toCoordinate();
+            return tileProjection.getWorldBoundsLatLon().getCenter().toCoordinate();
         }
         double scale = matrix.scaleDenominator * this.crsScale;
         EastNorth ret = new EastNorth(matrix.topLeftCorner.east() + x * scale, matrix.topLeftCorner.north() - y * scale);
-        return Main.getProjection().eastNorth2latlon(ret).toCoordinate();
+        return tileProjection.eastNorth2latlon(ret).toCoordinate();
     }
 
     @Override
@@ -749,8 +760,7 @@
             return new TileXY(0, 0);
         }
 
-        Projection proj = Main.getProjection();
-        EastNorth enPoint = proj.latlon2eastNorth(new LatLon(lat, lon));
+        EastNorth enPoint = tileProjection.latlon2eastNorth(new LatLon(lat, lon));
         double scale = matrix.scaleDenominator * this.crsScale;
         return new TileXY(
                 (enPoint.east() - matrix.topLeftCorner.east()) / scale,
@@ -765,12 +775,12 @@
 
     @Override
     public int getTileXMax(int zoom) {
-        return getTileXMax(zoom, Main.getProjection());
+        return getTileXMax(zoom, tileProjection);
     }
 
     @Override
     public int getTileYMax(int zoom) {
-        return getTileYMax(zoom, Main.getProjection());
+        return getTileYMax(zoom, tileProjection);
     }
 
     @Override
@@ -780,7 +790,7 @@
             return new Point(0, 0);
         }
         double scale = matrix.scaleDenominator * this.crsScale;
-        EastNorth point = Main.getProjection().latlon2eastNorth(new LatLon(lat, lon));
+        EastNorth point = tileProjection.latlon2eastNorth(new LatLon(lat, lon));
         return new Point(
                     (int) Math.round((point.east() - matrix.topLeftCorner.east()) / scale),
                     (int) Math.round((matrix.topLeftCorner.north() - point.north()) / scale)
@@ -804,12 +814,11 @@
             return new Coordinate(0, 0);
         }
         double scale = matrix.scaleDenominator * this.crsScale;
-        Projection proj = Main.getProjection();
         EastNorth ret = new EastNorth(
                 matrix.topLeftCorner.east() + x * scale,
                 matrix.topLeftCorner.north() - y * scale
                 );
-        LatLon ll = proj.eastNorth2latlon(ret);
+        LatLon ll = tileProjection.eastNorth2latlon(ret);
         return new Coordinate(ll.lat(), ll.lon());
     }
 
@@ -911,6 +920,10 @@
         return nativeScaleList;
     }
 
+    public Projection getTileProjection() {
+        return tileProjection;
+    }
+
     @Override
     public IProjected tileXYtoProjected(int x, int y, int zoom) {
         TileMatrix matrix = getTileMatrix(zoom);
@@ -977,6 +990,6 @@
 
     @Override
     public String getServerCRS() {
-        return Main.getProjection().toCode();
+        return tileProjection.toCode();
     }
 }
Index: src/org/openstreetmap/josm/data/projection/CustomProjection.java
===================================================================
--- src/org/openstreetmap/josm/data/projection/CustomProjection.java	(revision 11857)
+++ src/org/openstreetmap/josm/data/projection/CustomProjection.java	(working copy)
@@ -874,4 +874,19 @@
         }
         return result;
     }
+
+    @Override
+    public ProjectionBounds getEastNorthBoundsBox(ProjectionBounds box, Projection boxProjection) {
+        final int n = 8;
+        ProjectionBounds result = null;
+        for (int i = 0; i < 4*n; i++) {
+            EastNorth en = latlon2eastNorth(boxProjection.eastNorth2latlon(getPointAlong(i, n, box)));
+            if (result == null) {
+                result = new ProjectionBounds(en);
+            } else {
+                result.extend(en);
+            }
+        }
+        return result;
+    }
 }
Index: src/org/openstreetmap/josm/data/projection/Projection.java
===================================================================
--- src/org/openstreetmap/josm/data/projection/Projection.java	(revision 11857)
+++ src/org/openstreetmap/josm/data/projection/Projection.java	(working copy)
@@ -86,6 +86,22 @@
     Bounds getLatLonBoundsBox(ProjectionBounds pb);
 
     /**
+     * Get a box in east/north space of this projection, that fully contains an
+     * east/north box of another projection.
+     *
+     * Reprojecting a rectangular box from one projection to another may distort/rotate
+     * the shape of the box, so in general one needs to walk along the boundary
+     * in small steps to get a reliable result.
+     *
+     * This is an approximate method.
+     *
+     * @param box the east/north box given in projection <code>boxProjection</code>
+     * @param boxProjection the projection of <code>box</code>
+     * @return an east/north box in this projection, containing the given box
+     */
+    ProjectionBounds getEastNorthBoundsBox(ProjectionBounds box, Projection boxProjection);
+
+    /**
      * Get the number of meters per unit of this projection. This more
      * defines the scale of the map, than real conversion of unit to meters
      * as this value is more less correct only along certain lines of true scale.
Index: src/org/openstreetmap/josm/gui/NavigatableComponent.java
===================================================================
--- src/org/openstreetmap/josm/gui/NavigatableComponent.java	(revision 11857)
+++ src/org/openstreetmap/josm/gui/NavigatableComponent.java	(working copy)
@@ -637,7 +637,12 @@
         MapViewState mvs = getState().usingScale(newScale);
         mvs = mvs.movedTo(mvs.getCenter(), newCenter);
         Point2D enOrigin = mvs.getPointFor(new EastNorth(0, 0)).getInView();
-        Point2D enOriginAligned = new Point2D.Double(Math.round(enOrigin.getX()), Math.round(enOrigin.getY()));
+        // as a result of the alignment, it is common to round "half integer" values
+        // like 1.49999, which is numerically unstable; add small epsilon to resolve this
+        double EPSILON = 1e-3;
+        Point2D enOriginAligned = new Point2D.Double(
+                Math.round(enOrigin.getX()) + EPSILON,
+                Math.round(enOrigin.getY()) + EPSILON);
         EastNorth enShift = mvs.getForView(enOriginAligned.getX(), enOriginAligned.getY()).getEastNorth();
         newCenter = newCenter.subtract(enShift);
 
Index: src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
===================================================================
--- src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java	(revision 11857)
+++ src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java	(working copy)
@@ -62,6 +62,7 @@
 import org.openstreetmap.gui.jmapviewer.AttributionSupport;
 import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
 import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
+import org.openstreetmap.gui.jmapviewer.Projected;
 import org.openstreetmap.gui.jmapviewer.Tile;
 import org.openstreetmap.gui.jmapviewer.TileRange;
 import org.openstreetmap.gui.jmapviewer.TileXY;
@@ -87,6 +88,8 @@
 import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
 import org.openstreetmap.josm.data.preferences.IntegerProperty;
+import org.openstreetmap.josm.data.projection.Projection;
+import org.openstreetmap.josm.data.projection.Projections;
 import org.openstreetmap.josm.gui.ExtendedDialog;
 import org.openstreetmap.josm.gui.MapFrame;
 import org.openstreetmap.josm.gui.MapView;
@@ -95,6 +98,7 @@
 import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
 import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
 import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings.FilterChangeListener;
+import org.openstreetmap.josm.gui.layer.imagery.ReprojectionTile;
 import org.openstreetmap.josm.gui.layer.imagery.TileAnchor;
 import org.openstreetmap.josm.gui.layer.imagery.TileCoordinateConverter;
 import org.openstreetmap.josm.gui.layer.imagery.TilePosition;
@@ -108,6 +112,7 @@
 import org.openstreetmap.josm.tools.MemoryManager;
 import org.openstreetmap.josm.tools.MemoryManager.MemoryHandle;
 import org.openstreetmap.josm.tools.MemoryManager.NotEnoughMemoryException;
+import org.openstreetmap.josm.tools.Utils;
 
 /**
  * Base abstract class that supports displaying images provided by TileSource. It might be TMS source, WMS or WMTS
@@ -184,7 +189,7 @@
 
     private final ImageryAdjustAction adjustAction = new ImageryAdjustAction(this);
     // prepared to be moved to the painter
-    private TileCoordinateConverter coordinateConverter;
+    protected TileCoordinateConverter coordinateConverter;
 
     /**
      * Creates Tile Source based Imagery Layer based on Imagery Info
@@ -362,7 +367,7 @@
      * @param zoom zoom level
      * @return average number of screen pixels per tile pixel
      */
-    private double getScaleFactor(int zoom) {
+    protected double getScaleFactor(int zoom) {
         if (coordinateConverter != null) {
             return coordinateConverter.getScaleFactor(zoom);
         } else {
@@ -383,9 +388,7 @@
          * maps as a imagery layer
          */
         int intResult = (int) Math.round(result + 1 + ZOOM_OFFSET.get() / 1.9);
-
-        intResult = Math.min(intResult, getMaxZoomLvl());
-        intResult = Math.max(intResult, getMinZoomLvl());
+        intResult = Utils.clamp(intResult, getMinZoomLvl(), getMaxZoomLvl());
         return intResult;
     }
 
@@ -425,19 +428,30 @@
                     Main.trace(e);
                 }
 
-                String[][] content = {
-                        {"Tile name", clickedTile.getKey()},
-                        {"Tile url", url},
-                        {"Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) },
-                        {"Tile display size", new StringBuilder().append(displaySize.getWidth())
-                                                                 .append('x')
-                                                                 .append(displaySize.getHeight()).toString()},
-                };
+                List<List<String>> content = new ArrayList<>();
+                content.add(Arrays.asList(tr("Tile name"), clickedTile.getKey()));
+                content.add(Arrays.asList(tr("Tile URL"), url));
+                content.add(Arrays.asList(tr("Tile size"),
+                        getSizeString(clickedTile.getTileSource().getTileSize())));
+                content.add(Arrays.asList(tr("Tile display size"),
+                        new StringBuilder().append(displaySize.getWidth())
+                                .append('x')
+                                .append(displaySize.getHeight()).toString()));
+                if (coordinateConverter.requiresReprojection()) {
+                    content.add(Arrays.asList(tr("Reprojection"),
+                            clickedTile.getTileSource().getServerCRS() +
+                            " -> " + Main.getProjection().toCode()));
+                    BufferedImage img = clickedTile.getImage();
+                    if (img != null) {
+                        content.add(Arrays.asList(tr("Reprojected tile size"),
+                            img.getWidth() + "x" + img.getHeight()));
 
-                for (String[] entry: content) {
-                    panel.add(new JLabel(tr(entry[0]) + ':'), GBC.std());
+                    }
+                }
+                for (List<String> entry: content) {
+                    panel.add(new JLabel(tr(entry.get(0)) + ':'), GBC.std());
                     panel.add(GBC.glue(5, 0), GBC.std());
-                    panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL));
+                    panel.add(createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL));
                 }
 
                 for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) {
@@ -945,7 +959,11 @@
     private Tile getOrCreateTile(int x, int y, int zoom) {
         Tile tile = getTile(x, y, zoom);
         if (tile == null) {
-            tile = new Tile(tileSource, x, y, zoom);
+            if (coordinateConverter.requiresReprojection()) {
+                tile = new ReprojectionTile(tileSource, x, y, zoom);
+            } else {
+                tile = new Tile(tileSource, x, y, zoom);
+            }
             tileCache.addTile(tile);
         }
         return tile;
@@ -1020,7 +1038,7 @@
     }
 
     /**
-     * Invalidate the layer at a time in the future so taht the user still sees the interface responsive.
+     * Invalidate the layer at a time in the future so that the user still sees the interface responsive.
      */
     private void invalidateLater() {
         GuiHelper.runInEDT(() -> {
@@ -1054,7 +1072,7 @@
         return img;
     }
 
-    /**
+     /**
      * Draw a tile image on screen.
      * @param g the Graphics2D
      * @param toDrawImg tile image
@@ -1065,7 +1083,9 @@
     private void drawImageInside(Graphics2D g, BufferedImage toDrawImg, TileAnchor anchorImage, TileAnchor anchorScreen, Shape clip) {
         AffineTransform imageToScreen = anchorImage.convert(anchorScreen);
         Point2D screen0 = imageToScreen.transform(new Point.Double(0, 0), null);
-        Point2D screen1 = imageToScreen.transform(new Point.Double(toDrawImg.getWidth(), toDrawImg.getHeight()), null);
+        Point2D screen1 = imageToScreen.transform(new Point.Double(
+                toDrawImg.getWidth(), toDrawImg.getHeight()), null);
+
         Shape oldClip = null;
         if (clip != null) {
             oldClip = g.getClip();
@@ -1084,11 +1104,15 @@
         ts.visitTiles(tile -> {
             boolean miss = false;
             BufferedImage img = null;
+            TileAnchor anchorImage = null;
             if (!tile.isLoaded() || tile.hasError()) {
                 miss = true;
             } else {
-                img = getLoadedTileImage(tile);
-                if (img == null) {
+                synchronized (tile) {
+                    img = getLoadedTileImage(tile);
+                    anchorImage = getAnchor(tile, img);
+                }
+                if (img == null || anchorImage == null) {
                     miss = true;
                 }
             }
@@ -1096,15 +1120,25 @@
                 missed.add(new TilePosition(tile));
                 return;
             }
-            TileAnchor anchorImage = new TileAnchor(
-                    new Point.Double(0, 0),
-                    new Point.Double(img.getWidth(), img.getHeight()));
-            img = applyImageProcessors((BufferedImage) img);
+
+            img = applyImageProcessors(img);
+
             TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile);
             synchronized (paintMutex) {
                 //cannot paint in parallel
                 drawImageInside(g, img, anchorImage, anchorScreen, null);
             }
+            if (tile instanceof ReprojectionTile) {
+                // This means we have a reprojected tile in memory cache, but not at
+                // current scale. Generally, the positioning of the tile will still
+                // be correct, but for best image quality, the tile should be
+                // reprojected to the target scale. The original tile image should
+                // still be in disk cache, so this is fairly cheap.
+                if (((ReprojectionTile) tile).needsUpdate(Main.map.mapView.getScale())) {
+                    loadTile(tile, true);
+                }
+            }
+
         }, missed::add);
 
         return missed.stream().map(this::getOrCreateTile).collect(Collectors.toList());
@@ -1127,11 +1161,16 @@
         for (Tile tile : ts.allTilesCreate()) {
             boolean miss = false;
             BufferedImage img = null;
+            TileAnchor anchorImage = null;
             if (!tile.isLoaded() || tile.hasError()) {
                 miss = true;
             } else {
-                img = getLoadedTileImage(tile);
-                if (img == null) {
+                synchronized (tile) {
+                    img = getLoadedTileImage(tile);
+                    anchorImage = getAnchor(tile, img);
+                }
+
+                if (img == null || anchorImage == null) {
                     miss = true;
                 }
             }
@@ -1139,9 +1178,6 @@
                 missedTiles.add(tile);
                 continue;
             }
-            TileAnchor anchorImage = new TileAnchor(
-                    new Point.Double(0, 0),
-                    new Point.Double(img.getWidth(), img.getHeight()));
 
             // applying all filters to this layer
             img = applyImageProcessors((BufferedImage) img);
@@ -1160,6 +1196,14 @@
         return missedTiles;
     }
 
+    private TileAnchor getAnchor(Tile tile, BufferedImage image) {
+        if (tile instanceof ReprojectionTile) {
+            return ((ReprojectionTile) tile).getAnchor();
+        } else {
+            return new TileAnchor(new Point.Double(0, 0), new Point.Double(image.getWidth(), image.getHeight()));
+        }
+    }
+
     private void myDrawString(Graphics g, String text, int x, int y) {
         Color oldColor = g.getColor();
         String textToDraw = text;
@@ -1187,7 +1231,7 @@
         }
     }
 
-    private void paintTileText(Tile tile, Graphics g, MapView mv) {
+    private void paintTileText(Tile tile, Graphics2D g) {
         if (tile == null) {
             return;
         }
@@ -1217,27 +1261,10 @@
             //texty += 1 + fontHeight;
         }
 
-        int xCursor = -1;
-        int yCursor = -1;
         if (Main.isDebugEnabled()) {
-            if (yCursor < tile.getYtile()) {
-                if (Math.abs(tile.getYtile() % 32) == 31) {
-                    g.fillRect(0, y - 1, mv.getWidth(), 3);
-                } else {
-                    g.drawLine(0, y, mv.getWidth(), y);
-                }
-                //yCursor = t.getYtile();
-            }
-            // This draws the vertical lines for the entire column. Only draw them for the top tile in the column.
-            if (xCursor < tile.getXtile()) {
-                if (tile.getXtile() % 32 == 0) {
-                    // level 7 tile boundary
-                    g.fillRect(x - 1, 0, 3, mv.getHeight());
-                } else {
-                    g.drawLine(x, 0, x, mv.getHeight());
-                }
-                //xCursor = t.getXtile();
-            }
+            // draw tile outline in semi-transparent red
+            g.setColor(new Color(255, 0, 0, 50));
+            g.draw(coordinateConverter.getScreenQuadrilateralForTile(tile));
         }
     }
 
@@ -1396,10 +1423,21 @@
      * @return the tile set
      */
     protected TileSet getTileSet(ProjectionBounds bounds, int zoom) {
-        IProjected topLeftUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMin());
-        IProjected botRightUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMax());
-        TileXY t1 = tileSource.projectedToTileXY(topLeftUnshifted, zoom);
-        TileXY t2 = tileSource.projectedToTileXY(botRightUnshifted, zoom);
+        if (zoom == 0)
+            return new TileSet();
+        TileXY t1, t2;
+        if (coordinateConverter.requiresReprojection()) {
+            Projection projCurrent = Main.getProjection();
+            Projection projServer = Projections.getProjectionByCode(tileSource.getServerCRS());
+            bounds = projServer.getEastNorthBoundsBox(bounds, projCurrent);
+            t1 = tileSource.projectedToTileXY(bounds.getMin().toProjected(), zoom);
+            t2 = tileSource.projectedToTileXY(bounds.getMax().toProjected(), zoom);
+        } else {
+            IProjected topLeftUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMin());
+            IProjected botRightUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMax());
+            t1 = tileSource.projectedToTileXY(topLeftUnshifted, zoom);
+            t2 = tileSource.projectedToTileXY(botRightUnshifted, zoom);
+        }
         return new TileSet(t1, t2, zoom);
     }
 
@@ -1592,7 +1630,7 @@
 
         // The current zoom tileset should have all of its tiles due to the loadAllTiles(), unless it to tooLarge()
         for (Tile t : ts.allExistingTiles()) {
-            this.paintTileText(t, g, mv);
+            this.paintTileText(t, g);
         }
 
         EastNorth min = pb.getMin();
@@ -1639,19 +1677,8 @@
         if (Main.isDebugEnabled()) {
             Main.debug("getTileForPixelpos("+px+", "+py+')');
         }
-        Point clicked = new Point(px, py);
-        TileSet ts = getVisibleTileSet();
-
-        if (!ts.tooLarge()) {
-            ts.loadAllTiles(false); // make sure there are tile objects for all tiles
-        }
-        Stream<Tile> clickedTiles = ts.allExistingTiles().stream()
-                .filter(t -> coordinateConverter.getRectangleForTile(t).contains(clicked));
-        if (Main.isTraceEnabled()) {
-            clickedTiles = clickedTiles.peek(t -> Main.trace("Clicked on tile: " + t.getXtile() + ' ' + t.getYtile() +
-                    " currentZoomLevel: " + currentZoomLevel));
-        }
-        return clickedTiles.findAny().orElse(null);
+        TileXY xy = coordinateConverter.getTileforPixel(px, py, currentZoomLevel);
+        return getTile(xy.getXIndex(), xy.getYIndex(), currentZoomLevel);
     }
 
     /**
Index: src/org/openstreetmap/josm/gui/layer/TMSLayer.java
===================================================================
--- src/org/openstreetmap/josm/gui/layer/TMSLayer.java	(revision 11857)
+++ src/org/openstreetmap/josm/gui/layer/TMSLayer.java	(working copy)
@@ -160,4 +160,4 @@
         }
         return new ScaleList(scales);
     }
- }
+}
Index: src/org/openstreetmap/josm/gui/layer/WMSLayer.java
===================================================================
--- src/org/openstreetmap/josm/gui/layer/WMSLayer.java	(revision 11857)
+++ src/org/openstreetmap/josm/gui/layer/WMSLayer.java	(working copy)
@@ -7,8 +7,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Set;
-import java.util.TreeSet;
+import java.util.Objects;
 
 import javax.swing.AbstractAction;
 import javax.swing.Action;
@@ -27,6 +26,7 @@
 import org.openstreetmap.josm.data.preferences.BooleanProperty;
 import org.openstreetmap.josm.data.preferences.IntegerProperty;
 import org.openstreetmap.josm.data.projection.Projection;
+import org.openstreetmap.josm.data.projection.Projections;
 import org.openstreetmap.josm.gui.ExtendedDialog;
 import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
@@ -54,7 +54,7 @@
 
     private static final String CACHE_REGION_NAME = "WMS";
 
-    private final Set<String> supportedProjections;
+    private final List<String> supportedProjections;
 
     /**
      * Constructs a new {@code WMSLayer}.
@@ -65,7 +65,7 @@
         CheckParameterUtil.ensureThat(info.getImageryType() == ImageryType.WMS, "ImageryType is WMS");
         CheckParameterUtil.ensureParameterNotNull(info.getUrl(), "info.url");
         TemplatedWMSTileSource.checkUrl(info.getUrl());
-        this.supportedProjections = new TreeSet<>(info.getServerProjections());
+        this.supportedProjections = new ArrayList<>(info.getServerProjections());
     }
 
     @Override
@@ -86,7 +86,8 @@
 
     @Override
     protected AbstractWMSTileSource getTileSource() {
-        AbstractWMSTileSource tileSource = new TemplatedWMSTileSource(info);
+        AbstractWMSTileSource tileSource = new TemplatedWMSTileSource(
+                info, chooseProjection(Main.getProjection()));
         info.setAttribution(tileSource);
         return tileSource;
     }
@@ -135,7 +136,7 @@
     @Override
     public void projectionChanged(Projection oldValue, Projection newValue) {
         // do not call super - we need custom warning dialog
-
+        Projection tileProjection = null;
         if (!isProjectionSupported(newValue)) {
             String message =
                     "<html><body><p>" + tr("The layer {0} does not support the new projection {1}.",
@@ -152,13 +153,33 @@
                 warningDialog.toggleEnable("imagery.wms.projectionSupportWarnings." + tileSource.getBaseUrl());
             }
             warningDialog.showDialog();
+            tileProjection = chooseProjection(newValue);
+        } else {
+            tileProjection = newValue;
         }
 
-        if (!newValue.equals(oldValue)) {
-            tileSource.initProjection(newValue);
+        if (!Objects.equals(tileSource.getTileProjection(), tileProjection)) {
+            tileSource.setTileProjection(tileProjection);
         }
     }
 
+    private Projection chooseProjection(Projection requested) {
+        if (isProjectionSupported(requested)) {
+            return requested;
+        } else {
+            Projection result = null;
+            for (String code : supportedProjections) {
+                result = Projections.getProjectionByCode(code);
+                if (result != null)
+                    break;
+            }
+            if (result == null) {
+                result = Projections.getProjectionByCode("EPSG:4326");
+            }
+            return result;
+        }
+    }
+
     @Override
     protected Class<? extends TileLoader> getTileLoaderClass() {
         return WMSCachedTileLoader.class;
Index: src/org/openstreetmap/josm/gui/layer/WMTSLayer.java
===================================================================
--- src/org/openstreetmap/josm/gui/layer/WMTSLayer.java	(revision 11857)
+++ src/org/openstreetmap/josm/gui/layer/WMTSLayer.java	(working copy)
@@ -14,6 +14,7 @@
 import org.openstreetmap.josm.data.imagery.WMTSTileSource;
 import org.openstreetmap.josm.data.projection.Projection;
 import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings;
+import org.openstreetmap.josm.tools.Utils;
 
 /**
  * WMTS layer based on AbstractTileSourceLayer. Overrides few methods to align WMTS to Tile based computations
@@ -74,15 +75,13 @@
         if (scaleList == null) {
             return getMaxZoomLvl();
         }
-        double displayScale = Main.map.mapView.getScale() * Main.getProjection().getMetersPerUnit(); // meter per pixel
+        double displayScale = Main.map.mapView.getScale();
+        if (coordinateConverter.requiresReprojection()) {
+            displayScale *= Main.getProjection().getMetersPerUnit();
+        }
         Scale snap = scaleList.getSnapScale(displayScale, false);
-        return Math.max(
-                getMinZoomLvl(),
-                Math.min(
-                        snap != null ? snap.getIndex() : getMaxZoomLvl(),
-                        getMaxZoomLvl()
-                        )
-                );
+        return Utils.clamp(snap != null ? snap.getIndex() : getMaxZoomLvl(),
+                getMinZoomLvl(), getMaxZoomLvl());
     }
 
     @Override
Index: src/org/openstreetmap/josm/gui/layer/imagery/ReprojectionTile.java
===================================================================
--- src/org/openstreetmap/josm/gui/layer/imagery/ReprojectionTile.java	(revision 0)
+++ src/org/openstreetmap/josm/gui/layer/imagery/ReprojectionTile.java	(working copy)
@@ -0,0 +1,168 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.layer.imagery;
+
+import java.awt.Dimension;
+import java.awt.geom.Point2D;
+import java.awt.image.BufferedImage;
+
+import org.openstreetmap.gui.jmapviewer.Tile;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.ProjectionBounds;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.projection.Projection;
+import org.openstreetmap.josm.data.projection.Projections;
+import org.openstreetmap.josm.tools.ImageWarp;
+import org.openstreetmap.josm.tools.Utils;
+
+/**
+ * Tile class that stores a reprojected version of the original tile.
+ */
+public class ReprojectionTile extends Tile {
+
+    protected TileAnchor anchor;
+    private double nativeScale;
+    protected boolean maxZoomReached;
+
+    public ReprojectionTile(TileSource source, int xtile, int ytile, int zoom) {
+        super(source, xtile, ytile, zoom);
+    }
+
+    /**
+     * Get the position of the tile inside the image.
+     * @return the position of the tile inside the image
+     * @see #getImage()
+     */
+    public TileAnchor getAnchor() {
+        return anchor;
+    }
+
+    public double getNativeScale() {
+        return nativeScale;
+    }
+
+    public boolean needsUpdate(double currentScale) {
+        if (Utils.equalsEpsilon(nativeScale, currentScale))
+            return false;
+        if (maxZoomReached && currentScale < nativeScale)
+            // zoomed in even more - max zoom already reached, so no update
+            return false;
+        return true;
+    }
+    
+    @Override
+    public void setImage(BufferedImage image) {
+        if (image == null) {
+            reset();
+        } else {
+            transform(image);
+        }
+    }
+
+    private synchronized void reset() {
+        this.image = null;
+        this.anchor = null;
+        this.maxZoomReached = false;
+    }
+
+    public void transform(BufferedImage imageIn) {
+        if (!Main.isDisplayingMapView()) {
+            reset();
+            return;
+        }
+        double scaleMapView = Main.map.mapView.getScale();
+        ImageWarp.Interpolation interpolation;
+        switch (Main.pref.get("imagery.warp.interpolation", "bilinear")) {
+            case "nearest_neighbor":
+                interpolation = ImageWarp.Interpolation.NEAREST_NEIGHBOR;
+                break;
+            default:
+                interpolation = ImageWarp.Interpolation.BILINEAR;
+        }
+        double margin = interpolation.getMargin();
+
+        Projection projCurrent = Main.getProjection();
+        Projection projServer = Projections.getProjectionByCode(source.getServerCRS());
+        EastNorth en00Server = new EastNorth(source.tileXYtoProjected(xtile, ytile, zoom));
+        EastNorth en11Server = new EastNorth(source.tileXYtoProjected(xtile + 1, ytile + 1, zoom));
+        ProjectionBounds pbServer = new ProjectionBounds(en00Server);
+        pbServer.extend(en11Server);
+        // find east-north rectangle in current projection, that will fully contain the tile
+        ProjectionBounds pbTarget = projCurrent.getEastNorthBoundsBox(pbServer, projServer);
+
+        // add margin and align to pixel grid
+        double minEast = Math.floor(pbTarget.minEast / scaleMapView - margin) * scaleMapView;
+        double minNorth = -Math.floor(-(pbTarget.minNorth / scaleMapView - margin)) * scaleMapView;
+        double maxEast = Math.ceil(pbTarget.maxEast / scaleMapView + margin) * scaleMapView;
+        double maxNorth = -Math.ceil(-(pbTarget.maxNorth / scaleMapView + margin)) * scaleMapView;
+        ProjectionBounds pbTargetAligned = new ProjectionBounds(minEast, minNorth, maxEast, maxNorth);
+
+        Dimension dim = getDimension(pbTargetAligned, scaleMapView);
+        Integer scaleFix = limitScale(source.getTileSize(), Math.sqrt(dim.getWidth() * dim.getHeight()));
+        double scale = scaleFix == null ? scaleMapView : scaleMapView * scaleFix;
+
+        ImageWarp.PointTransform pointTransform = pt -> {
+            EastNorth target = new EastNorth(pbTargetAligned.minEast + (pt.getX()) * scale,
+                    pbTargetAligned.maxNorth - (pt.getY()) * scale);
+            EastNorth sourceEN = projServer.latlon2eastNorth(projCurrent.eastNorth2latlon(target));
+            double x2 = source.getTileSize() *
+                    (sourceEN.east() - pbServer.minEast) / (pbServer.maxEast - pbServer.minEast);
+            double y2 = source.getTileSize() *
+                    (pbServer.maxNorth - sourceEN.north()) / (pbServer.maxNorth - pbServer.minNorth);
+            return new Point2D.Double(x2, y2);
+        };
+
+        // pixel coordinates of tile origin and opposite tile corner inside the target image
+        // (tile may be deformed / rotated by reprojection)
+        EastNorth en00Current = projCurrent.latlon2eastNorth(projServer.eastNorth2latlon(new EastNorth(en00Server.getX(), en00Server.getY())));
+        EastNorth en11Current = projCurrent.latlon2eastNorth(projServer.eastNorth2latlon(new EastNorth(en11Server.getX(), en11Server.getY())));
+        Point2D p00Img = new Point2D.Double(
+                (en00Current.east() - pbTargetAligned.minEast) / scale,
+                (pbTargetAligned.maxNorth - en00Current.north()) / scale);
+        Point2D p11Img = new Point2D.Double(
+                (en11Current.east() - pbTargetAligned.minEast) / scale,
+                (pbTargetAligned.maxNorth - en11Current.north()) / scale);
+
+        BufferedImage imageOut = ImageWarp.warp(
+                imageIn, getDimension(pbTargetAligned, scale), pointTransform,
+                interpolation);
+        synchronized (this) {
+            this.image = imageOut;
+            this.anchor = new TileAnchor(p00Img, p11Img);
+            this.nativeScale = scale;
+            this.maxZoomReached = scaleFix != null;
+        }
+    }
+
+    private Dimension getDimension(ProjectionBounds bounds, double scale) {
+        return new Dimension(
+                (int) Math.round((bounds.maxEast - bounds.minEast) / scale),
+                (int) Math.round((bounds.maxNorth - bounds.minNorth) / scale));
+    }
+
+    /**
+     * Make sure, the image is not scaled up too much.
+     *
+     * This would not give any significant improvement in image quality and may
+     * exceed the user's memory. The correction factor is a power of 2.
+     * @param lenOrig tile size of original image
+     * @param lenNow (averaged) tile size of warped image
+     * @return factor to shrink if limit is exceeded; 1 if it is already at the
+     * limit, but no change needed; null if it is well below the limit and can
+     * still be scaled up by at least a factor of 2.
+     */
+    protected Integer limitScale(double lenOrig, double lenNow) {
+        double LIMIT = 3;
+        if (lenNow > LIMIT * lenOrig) {
+            int n = (int) Math.ceil((Math.log(lenNow) - Math.log(LIMIT * lenOrig)) / Math.log(2));
+            int f = 1 << n;
+            double lenNowFixed = lenNow / f;
+            if (!(lenNowFixed <= LIMIT * lenOrig)) throw new AssertionError();
+            if (!(lenNowFixed > LIMIT * lenOrig / 2)) throw  new AssertionError();
+            return f;
+        }
+        if (lenNow > LIMIT * lenOrig / 2)
+            return 1;
+        return null;
+    }
+}

Property changes on: src/org/openstreetmap/josm/gui/layer/imagery/ReprojectionTile.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java
===================================================================
--- src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java	(revision 11857)
+++ src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java	(working copy)
@@ -11,6 +11,7 @@
 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
 import org.openstreetmap.gui.jmapviewer.interfaces.IProjected;
 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
+import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.projection.Projecting;
@@ -84,13 +85,30 @@
      * @return The position.
      */
     public Point2D getPixelForTile(Tile tile) {
-        return this.getPixelForTile(tile.getXtile(), tile.getYtile(), tile.getZoom());
+        return getPixelForTile(tile.getXtile(), tile.getYtile(), tile.getZoom());
     }
 
     /**
+     * Convert screen pixel coordinate to tile position at certain zoom level.
+     * @param sx x coordinate (screen pixel)
+     * @param sy y coordinate (screen pixel)
+     * @param zoom zoom level
+     * @return the tile
+     */
+    public TileXY getTileforPixel(int sx, int sy, int zoom) {
+        if (requiresReprojection()) {
+            LatLon ll = getProjecting().eastNorth2latlonClamped(mapView.getEastNorth(sx, sy));
+            return tileSource.latLonToTileXY(ll.toCoordinate(), zoom);
+        } else {
+            IProjected p = shiftDisplayToServer(mapView.getEastNorth(sx, sy));
+            return tileSource.projectedToTileXY(p, zoom);
+        }
+    }
+
+    /**
      * Gets the position of the tile inside the map view.
      * @param tile The tile
-     * @return The positon.
+     * @return The positon as a rectangle in screen coordinates
      */
     public Rectangle2D getRectangleForTile(Tile tile) {
         ICoordinate c1 = tile.getTileSource().tileXYToLatLon(tile);
@@ -129,11 +147,18 @@
      * @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);
-
+        TileXY t1, t2;
+        if (requiresReprojection()) {
+            LatLon topLeft = mapView.getLatLon(0, 0);
+            LatLon botRight = mapView.getLatLon(mapView.getWidth(), mapView.getHeight());
+            t1 = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom);
+            t2 = tileSource.latLonToTileXY(botRight.toCoordinate(), zoom);
+        }  else {
+            EastNorth topLeftEN = mapView.getEastNorth(0, 0);
+            EastNorth botRightEN = mapView.getEastNorth(mapView.getWidth(), mapView.getHeight());
+            t1 = tileSource.projectedToTileXY(topLeftEN.toProjected(), zoom);
+            t2 = tileSource.projectedToTileXY(botRightEN.toProjected(), 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;
@@ -146,8 +171,18 @@
      * @return position of the tile in screen coordinates
      */
     public TileAnchor getScreenAnchorForTile(Tile tile) {
-        IProjected p1 = tileSource.tileXYtoProjected(tile.getXtile(), tile.getYtile(), tile.getZoom());
-        IProjected p2 = tileSource.tileXYtoProjected(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
-        return new TileAnchor(pos(p1).getInView(), pos(p2).getInView());
+        if (requiresReprojection()) {
+            ICoordinate c1 = tile.getTileSource().tileXYToLatLon(tile);
+            ICoordinate c2 = tile.getTileSource().tileXYToLatLon(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
+            return new TileAnchor(pos(c1).getInView(), pos(c2).getInView());
+        } else {
+            IProjected p1 = tileSource.tileXYtoProjected(tile.getXtile(), tile.getYtile(), tile.getZoom());
+            IProjected p2 = tileSource.tileXYtoProjected(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
+            return new TileAnchor(pos(p1).getInView(), pos(p2).getInView());
+        }
     }
+
+    public boolean requiresReprojection() {
+        return !tileSource.getServerCRS().equals(Main.getProjection().toCode());
+    }
 }
Index: src/org/openstreetmap/josm/tools/ImageWarp.java
===================================================================
--- src/org/openstreetmap/josm/tools/ImageWarp.java	(revision 0)
+++ src/org/openstreetmap/josm/tools/ImageWarp.java	(working copy)
@@ -0,0 +1,146 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.WritableRaster;
+
+/**
+ * Image warping algorithm.
+ *
+ * Deforms an image geometrically according to a given transformation formula.
+ */
+public class ImageWarp {
+
+    /**
+     * Transformation that translates the pixel coordinates.
+     */
+    public static interface PointTransform {
+        Point2D transform(Point2D pt);
+    }
+
+    /**
+     * Interpolation method.
+     */
+    public enum Interpolation {
+        /**
+         * Nearest neighbor.
+         *
+         * Simplest possible method. Faster, but not very good quality.
+         */
+        NEAREST_NEIGHBOR(1),
+        /**
+         * Bilinear.
+         *
+         * Decent quality.
+         */
+        BILINEAR(2);
+
+        private final int margin;
+
+        private Interpolation(int margin) {
+            this.margin = margin;
+        }
+
+        /**
+         * Number of pixels to scan outside the source image.
+         * Used to get smoother borders.
+         * @return the margin
+         */
+        public int getMargin() {
+            return margin;
+        }
+    }
+
+    /**
+     * Warp an image.
+     * @param srcImg the original image
+     * @param targetDim dimension of the target image
+     * @param invTransform inverse transformation (translates pixel coordinates
+     * of the target image to pixel coordinates of the original image)
+     * @param interpolation the interpolation method
+     * @return the warped image
+     */
+    public static BufferedImage warp(BufferedImage srcImg, Dimension targetDim, PointTransform invTransform, Interpolation interpolation) {
+        BufferedImage imgTarget = new BufferedImage(targetDim.width, targetDim.height, BufferedImage.TYPE_INT_ARGB);
+        Rectangle2D srcRect = new Rectangle2D.Double(0, 0, srcImg.getWidth(), srcImg.getHeight());
+        for (int j = 0; j < imgTarget.getHeight(); j++) {
+            for (int i = 0; i < imgTarget.getWidth(); i++) {
+                Point2D srcCoord = invTransform.transform(new Point2D.Double(i, j));
+                if (isInside(srcCoord, srcRect, interpolation.getMargin())) {
+                        int rgb;
+                        switch (interpolation) {
+                            case NEAREST_NEIGHBOR:
+                                rgb = getColor((int) Math.round(srcCoord.getX()), (int) Math.round(srcCoord.getY()), srcImg).getRGB();
+                                break;
+                            case BILINEAR:
+                                int x0 = (int) Math.floor(srcCoord.getX());
+                                double dx = srcCoord.getX() - x0;
+                                int y0 = (int) Math.floor(srcCoord.getY());
+                                double dy = srcCoord.getY() - y0;
+                                Color c00 = getColor(x0, y0, srcImg);
+                                Color c01 = getColor(x0, y0 + 1, srcImg);
+                                Color c10 = getColor(x0 + 1, y0, srcImg);
+                                Color c11 = getColor(x0 + 1, y0 + 1, srcImg);
+                                int red = (int) Math.round(
+                                        (c00.getRed() * (1-dx) + c10.getRed() * dx) * (1-dy) +
+                                        (c01.getRed() * (1-dx) + c11.getRed() * dx) * dy);
+                                int green = (int) Math.round(
+                                        (c00.getGreen()* (1-dx) + c10.getGreen() * dx) * (1-dy) +
+                                        (c01.getGreen() * (1-dx) + c11.getGreen() * dx) * dy);
+                                int blue = (int) Math.round(
+                                        (c00.getBlue()* (1-dx) + c10.getBlue() * dx) * (1-dy) +
+                                        (c01.getBlue() * (1-dx) + c11.getBlue() * dx) * dy);
+                                int alpha = (int) Math.round(
+                                        (c00.getAlpha()* (1-dx) + c10.getAlpha() * dx) * (1-dy) +
+                                        (c01.getAlpha() * (1-dx) + c11.getAlpha() * dx) * dy);
+                                rgb = new Color(red, green, blue, alpha).getRGB();
+                                break;
+                            default:
+                                throw new AssertionError();
+                        }
+                        imgTarget.setRGB(i, j, rgb);
+                }
+            }
+        }
+        return imgTarget;
+    }
+
+    private static boolean isInside(Point2D p, Rectangle2D rect, double margin) {
+        return isInside(p.getX(), rect.getMinX(), rect.getMaxX(), margin) &&
+                isInside(p.getY(), rect.getMinY(), rect.getMaxY(), margin);
+    }
+
+    private static boolean isInside(double x, double xMin, double xMax, double margin) {
+        return x + margin >= xMin && x - margin <= xMax;
+    }
+
+    private static Color getColor(int x, int y, BufferedImage img) {
+        // border strategy: continue with the color of the outermost pixel,
+        // but change alpha component to fully translucent
+        boolean transparent = false;
+        if (x < 0) {
+            x = 0;
+            transparent = true;
+        } else if (x >= img.getWidth()) {
+            x = img.getWidth() - 1;
+            transparent = true;
+        }
+        if (y < 0) {
+            y = 0;
+            transparent = true;
+        } else if (y >= img.getHeight()) {
+            y = img.getHeight() - 1;
+            transparent = true;
+        }
+        Color clr = new Color(img.getRGB(x, y));
+        if (!transparent)
+            return clr;
+        // keep color components, but set transparency to 0
+        // (the idea is that border fades out and mixes with next tile)
+        return new Color(clr.getRed(), clr.getGreen(), clr.getBlue(), 0);
+    }
+}

Property changes on: src/org/openstreetmap/josm/tools/ImageWarp.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: src/org/openstreetmap/gui/jmapviewer/Tile.java
===================================================================
--- src/org/openstreetmap/gui/jmapviewer/Tile.java	(revision 33218)
+++ src/org/openstreetmap/gui/jmapviewer/Tile.java	(working copy)
@@ -75,7 +75,7 @@
         this.xtile = xtile;
         this.ytile = ytile;
         this.zoom = zoom;
-        this.setImage(image);
+        this.image = image;
         this.key = getTileKey(source, xtile, ytile, zoom);
     }
 
@@ -239,7 +239,7 @@
         return image;
     }
 
-    public final void setImage(BufferedImage image) {
+    public void setImage(BufferedImage image) {
         this.image = image;
     }
 
@@ -302,7 +302,17 @@
 
     @Override
     public String toString() {
-        return "Tile " + key;
+        StringBuilder sb = new StringBuilder("Tile ").append(key);
+        if (loading) {
+            sb.append(" [LOADING...]");
+        }
+        if (loaded) {
+            sb.append(" [loaded]");
+        }
+        if (error) {
+            sb.append(" [ERROR]");
+        }
+        return sb.toString();
     }
 
     /**
Index: src/org/openstreetmap/gui/jmapviewer/TileXY.java
===================================================================
--- src/org/openstreetmap/gui/jmapviewer/TileXY.java	(revision 33218)
+++ src/org/openstreetmap/gui/jmapviewer/TileXY.java	(working copy)
@@ -54,4 +54,9 @@
     public double getY() {
         return y;
     }
+
+    @Override
+    public String toString() {
+        return "TileXY{" + x + ", " + y + "}";
+    }
 }
