Index: trunk/src/org/openstreetmap/josm/data/imagery/AbstractWMSTileSource.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/AbstractWMSTileSource.java	(revision 11856)
+++ trunk/src/org/openstreetmap/josm/data/imagery/AbstractWMSTileSource.java	(revision 11858)
@@ -11,5 +11,4 @@
 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;
@@ -32,11 +31,14 @@
     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
+     * @param tileProjection the tile projection
      */
-    public AbstractWMSTileSource(TileSourceInfo info) {
+    public AbstractWMSTileSource(TileSourceInfo info, Projection tileProjection) {
         super(info);
+        this.tileProjection = tileProjection;
     }
 
@@ -48,9 +50,18 @@
     }
 
+    public void setTileProjection(Projection tileProjection) {
+        this.tileProjection = tileProjection;
+        initProjection();
+    }
+
+    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);
     }
 
@@ -99,5 +110,5 @@
     @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();
     }
 
@@ -112,5 +123,5 @@
     @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);
     }
@@ -144,5 +155,5 @@
     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),
@@ -164,10 +175,9 @@
     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();
     }
 
@@ -197,5 +207,5 @@
     @Override
     public String getServerCRS() {
-        return Main.getProjection().toCode();
+        return this.tileProjection.toCode();
     }
 }
Index: trunk/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java	(revision 11856)
+++ trunk/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java	(revision 11858)
@@ -204,6 +204,4 @@
     /** is the geo reference correct - don't offer offset handling */
     private boolean isGeoreferenceValid;
-    /** does the EPSG:4326 to mercator woraround work as expected */
-    private boolean isEpsg4326To3857Supported;
     /** which layers should be activated by default on layer addition. **/
     private Collection<DefaultLayer> defaultLayers = Collections.emptyList();
@@ -244,5 +242,4 @@
         @pref boolean valid_georeference;
         @pref boolean bestMarked;
-        @pref boolean supports_epsg_4326_to_3857_conversion;
         // TODO: disabled until change of layers is implemented
         // @pref String default_layers;
@@ -309,5 +306,4 @@
 
             valid_georeference = i.isGeoreferenceValid();
-            supports_epsg_4326_to_3857_conversion = i.isEpsg4326To3857Supported();
             // TODO disabled until change of layers is implemented
             // default_layers = i.defaultLayers.stream().collect(Collectors.joining(","));
@@ -439,5 +435,4 @@
         setTileSize(e.tileSize);
         metadataHeaders = e.metadataHeaders;
-        isEpsg4326To3857Supported = e.supports_epsg_4326_to_3857_conversion;
         isGeoreferenceValid = e.valid_georeference;
         // TODO disabled until change of layers is implemented
@@ -476,5 +471,4 @@
         this.noTileChecksums = i.noTileChecksums;
         this.metadataHeaders = i.metadataHeaders;
-        this.isEpsg4326To3857Supported = i.isEpsg4326To3857Supported;
         this.isGeoreferenceValid = i.isGeoreferenceValid;
         this.defaultLayers = i.defaultLayers;
@@ -506,5 +500,4 @@
                 Objects.equals(this.url, other.url) &&
                 Objects.equals(this.bestMarked, other.bestMarked) &&
-                Objects.equals(this.isEpsg4326To3857Supported, other.isEpsg4326To3857Supported) &&
                 Objects.equals(this.isGeoreferenceValid, other.isGeoreferenceValid) &&
                 Objects.equals(this.cookies, other.cookies) &&
@@ -1141,20 +1134,4 @@
             this.metadataHeaders = metadataHeaders;
         }
-    }
-
-    /**
-     * Gets the flag if epsg 4326 to 3857 is supported
-     * @return The flag.
-     */
-    public boolean isEpsg4326To3857Supported() {
-        return isEpsg4326To3857Supported;
-    }
-
-    /**
-     * Sets the flag that epsg 4326 to 3857 is supported
-     * @param isEpsg4326To3857Supported The flag.
-     */
-    public void setEpsg4326To3857Supported(boolean isEpsg4326To3857Supported) {
-        this.isEpsg4326To3857Supported = isEpsg4326To3857Supported;
     }
 
Index: trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java	(revision 11856)
+++ trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java	(revision 11858)
@@ -46,5 +46,5 @@
     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;
 
Index: trunk/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java	(revision 11856)
+++ trunk/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java	(revision 11858)
@@ -19,4 +19,5 @@
 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,7 +55,8 @@
      * Creates a tile source based on imagery info
      * @param info imagery info
+     * @param tileProjection the tile projection
      */
-    public TemplatedWMSTileSource(ImageryInfo info) {
-        super(info);
+    public TemplatedWMSTileSource(ImageryInfo info, Projection tileProjection) {
+        super(info, tileProjection);
         this.serverProjections = new TreeSet<>(info.getServerProjections());
         handleTemplate();
@@ -69,5 +71,5 @@
     @Override
     public String getTileUrl(int zoom, int tilex, int tiley) {
-        String myProjCode = Main.getProjection().toCode();
+        String myProjCode = getServerCRS();
 
         EastNorth nw = getTileEastNorth(tilex, tiley, zoom);
@@ -79,14 +81,4 @@
         double s = se.getY();
         double e = se.getX();
-
-        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));
-            myProjCode = "EPSG:4326";
-            s = swll.lat();
-            w = swll.lon();
-            n = nell.lat();
-            e = nell.lon();
-        }
 
         if ("EPSG:4326".equals(myProjCode) && !serverProjections.contains(myProjCode) && serverProjections.contains("CRS:84")) {
Index: trunk/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java	(revision 11856)
+++ trunk/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java	(revision 11858)
@@ -8,5 +8,4 @@
 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;
 
@@ -40,5 +39,5 @@
         String key = super.getCacheKey();
         if (key != null) {
-            return key + Main.getProjection().toCode();
+            return key + tile.getSource().getServerCRS();
         }
         return null;
Index: trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java	(revision 11856)
+++ trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java	(revision 11858)
@@ -14,5 +14,5 @@
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -20,5 +20,4 @@
 import java.util.Objects;
 import java.util.Optional;
-import java.util.Set;
 import java.util.SortedSet;
 import java.util.Stack;
@@ -62,5 +61,5 @@
 
 /**
- * Tile Source handling WMS providers
+ * Tile Source handling WMTS providers
  *
  * @author Wiktor Niesiobędzki
@@ -269,4 +268,6 @@
     private final WMTSDefaultLayer defaultLayer;
 
+    private Projection tileProjection;
+
     /**
      * Creates a tile source based on imagery info
@@ -598,34 +599,43 @@
      */
     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);
-                }
-                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
-                    );
-        }
-        this.crsScale = getTileSize() * 0.28e-03 / proj.getMetersPerUnit();
+        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;
+                    }
+                }
+                if (this.currentLayer == null)
+                    return;
+            } // else: keep currentLayer and tileProjection as is
+        }
+        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();
     }
 
@@ -655,5 +665,5 @@
         // 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;
@@ -736,9 +746,9 @@
         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();
     }
 
@@ -750,6 +760,5 @@
         }
 
-        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(
@@ -766,10 +775,10 @@
     @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);
     }
 
@@ -781,5 +790,5 @@
         }
         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),
@@ -805,10 +814,9 @@
         }
         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());
     }
@@ -857,6 +865,6 @@
      * @return set of projection codes that this TileSource supports
      */
-    public Set<String> getSupportedProjections() {
-        Set<String> ret = new HashSet<>();
+    public Collection<String> getSupportedProjections() {
+        Collection<String> ret = new LinkedHashSet<>();
         if (currentLayer == null) {
             for (Layer layer: this.layers) {
@@ -910,4 +918,8 @@
     public ScaleList getNativeScales() {
         return nativeScaleList;
+    }
+
+    public Projection getTileProjection() {
+        return tileProjection;
     }
 
@@ -978,5 +990,5 @@
     @Override
     public String getServerCRS() {
-        return Main.getProjection().toCode();
+        return tileProjection.toCode();
     }
 }
Index: trunk/src/org/openstreetmap/josm/data/projection/CustomProjection.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/projection/CustomProjection.java	(revision 11856)
+++ trunk/src/org/openstreetmap/josm/data/projection/CustomProjection.java	(revision 11858)
@@ -875,3 +875,18 @@
         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: trunk/src/org/openstreetmap/josm/data/projection/Projection.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/projection/Projection.java	(revision 11856)
+++ trunk/src/org/openstreetmap/josm/data/projection/Projection.java	(revision 11858)
@@ -87,4 +87,20 @@
 
     /**
+     * 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
Index: trunk/src/org/openstreetmap/josm/gui/NavigatableComponent.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/NavigatableComponent.java	(revision 11856)
+++ trunk/src/org/openstreetmap/josm/gui/NavigatableComponent.java	(revision 11858)
@@ -638,5 +638,10 @@
         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: trunk/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java	(revision 11856)
+++ trunk/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java	(revision 11858)
@@ -49,5 +49,4 @@
 import javax.swing.AbstractAction;
 import javax.swing.Action;
-import javax.swing.BorderFactory;
 import javax.swing.JCheckBoxMenuItem;
 import javax.swing.JLabel;
@@ -57,5 +56,4 @@
 import javax.swing.JPopupMenu;
 import javax.swing.JSeparator;
-import javax.swing.JTextField;
 import javax.swing.Timer;
 
@@ -88,4 +86,6 @@
 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;
@@ -96,4 +96,5 @@
 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;
@@ -109,4 +110,5 @@
 import org.openstreetmap.josm.tools.MemoryManager.MemoryHandle;
 import org.openstreetmap.josm.tools.MemoryManager.NotEnoughMemoryException;
+import org.openstreetmap.josm.tools.Utils;
 
 /**
@@ -185,5 +187,5 @@
     private final ImageryAdjustAction adjustAction = new ImageryAdjustAction(this);
     // prepared to be moved to the painter
-    private TileCoordinateConverter coordinateConverter;
+    protected TileCoordinateConverter coordinateConverter;
 
     /**
@@ -346,7 +348,18 @@
     public Object getInfoComponent() {
         JPanel panel = (JPanel) super.getInfoComponent();
+        List<List<String>> content = new ArrayList<>();
         EastNorth offset = getDisplaySettings().getDisplacement();
         if (offset.distanceSq(0, 0) > 1e-10) {
-            panel.add(new JLabel(tr("Offset: ") + offset.east() + ';' + offset.north()), GBC.eol().insets(0, 5, 10, 0));
+            content.add(Arrays.asList(tr("Offset"), offset.east() + ";" + offset.north()));
+        }
+        if (coordinateConverter.requiresReprojection()) {
+            content.add(Arrays.asList(tr("Tile download projection"), tileSource.getServerCRS()));
+            content.add(Arrays.asList(tr("Tile display projection"), Main.getProjection().toCode()));
+        }
+        content.add(Arrays.asList(tr("Current zoom"), Integer.toString(currentZoomLevel)));
+        for (List<String> entry: content) {
+            panel.add(new JLabel(entry.get(0) + ':'), GBC.std());
+            panel.add(GBC.glue(5, 0), GBC.std());
+            panel.add(createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL));
         }
         return panel;
@@ -363,5 +376,5 @@
      * @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);
@@ -384,7 +397,5 @@
          */
         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;
     }
@@ -398,15 +409,9 @@
         private ShowTileInfoAction() {
             super(tr("Show tile info"));
+            setEnabled(clickedTileHolder.getTile() != null);
         }
 
         private String getSizeString(int size) {
             return new StringBuilder().append(size).append('x').append(size).toString();
-        }
-
-        private JTextField createTextField(String text) {
-            JTextField ret = new JTextField(text);
-            ret.setEditable(false);
-            ret.setBorder(BorderFactory.createEmptyBorder());
-            return ret;
         }
 
@@ -426,17 +431,28 @@
                 }
 
-                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()},
-                };
-
-                for (String[] entry: content) {
-                    panel.add(new JLabel(tr(entry[0]) + ':'), GBC.std());
+                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 (List<String> entry: content) {
+                    panel.add(new JLabel(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));
                 }
 
@@ -462,4 +478,5 @@
         private LoadTileAction() {
             super(tr("Load tile"));
+            setEnabled(clickedTileHolder.getTile() != null);
         }
 
@@ -946,5 +963,9 @@
         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);
         }
@@ -1021,5 +1042,5 @@
 
     /**
-     * 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() {
@@ -1055,5 +1076,5 @@
     }
 
-    /**
+     /**
      * Draw a tile image on screen.
      * @param g the Graphics2D
@@ -1066,5 +1087,7 @@
         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) {
@@ -1085,9 +1108,13 @@
             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;
                 }
@@ -1097,8 +1124,7 @@
                 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) {
@@ -1106,4 +1132,15 @@
                 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);
 
@@ -1128,9 +1165,14 @@
             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;
                 }
@@ -1140,7 +1182,4 @@
                 continue;
             }
-            TileAnchor anchorImage = new TileAnchor(
-                    new Point.Double(0, 0),
-                    new Point.Double(img.getWidth(), img.getHeight()));
 
             // applying all filters to this layer
@@ -1159,4 +1198,12 @@
         }
         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()));
+        }
     }
 
@@ -1188,5 +1235,5 @@
     }
 
-    private void paintTileText(Tile tile, Graphics g, MapView mv) {
+    private void paintTileText(Tile tile, Graphics2D g) {
         if (tile == null) {
             return;
@@ -1218,25 +1265,8 @@
         }
 
-        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));
         }
     }
@@ -1397,8 +1427,22 @@
      */
     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 = new ProjectionBounds(
+                    new EastNorth(coordinateConverter.shiftDisplayToServer(bounds.getMin())),
+                    new EastNorth(coordinateConverter.shiftDisplayToServer(bounds.getMax())));
+            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);
     }
@@ -1593,5 +1637,5 @@
         // 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);
         }
 
@@ -1640,17 +1684,6 @@
             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: trunk/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java	(revision 11856)
+++ trunk/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java	(revision 11858)
@@ -2,5 +2,4 @@
 package org.openstreetmap.josm.gui.layer;
 
-import static org.openstreetmap.josm.tools.I18n.tr;
 import static org.openstreetmap.josm.tools.I18n.trc;
 
@@ -11,8 +10,11 @@
 import java.awt.image.BufferedImageOp;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 
 import javax.swing.AbstractAction;
 import javax.swing.Action;
+import javax.swing.BorderFactory;
 import javax.swing.Icon;
 import javax.swing.JCheckBoxMenuItem;
@@ -24,4 +26,5 @@
 import javax.swing.JPopupMenu;
 import javax.swing.JSeparator;
+import javax.swing.JTextField;
 
 import org.openstreetmap.josm.Main;
@@ -33,6 +36,6 @@
 import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
 import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings;
-import org.openstreetmap.josm.gui.widgets.UrlLabel;
 import org.openstreetmap.josm.tools.GBC;
+import static org.openstreetmap.josm.tools.I18n.tr;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
@@ -146,4 +149,6 @@
     public void mergeFrom(Layer from) {
     }
+    
+    public abstract Collection<String> getNativeProjections();
 
     @Override
@@ -152,11 +157,33 @@
         panel.add(new JLabel(getToolTipText()), GBC.eol());
         if (info != null) {
-            String url = info.getUrl();
-            if (url != null) {
-                panel.add(new JLabel(tr("URL: ")), GBC.std().insets(0, 5, 2, 0));
-                panel.add(new UrlLabel(url), GBC.eol().insets(2, 5, 10, 0));
+            List<List<String>> content = new ArrayList<>();
+            content.add(Arrays.asList(tr("Name"), info.getName()));
+            content.add(Arrays.asList(tr("Type"), info.getImageryType().getTypeString().toUpperCase()));
+            content.add(Arrays.asList(tr("URL"), info.getUrl()));
+            content.add(Arrays.asList(tr("Id"), info.getId() == null ? "-" : info.getId()));
+            if (info.getMinZoom() != 0) {
+                content.add(Arrays.asList(tr("Min. zoom"), Integer.toString(info.getMinZoom())));
+            }
+            if (info.getMaxZoom() != 0) {
+                content.add(Arrays.asList(tr("Max. zoom"), Integer.toString(info.getMaxZoom())));
+            }
+            if (info.getDescription() != null) {
+                content.add(Arrays.asList(tr("Description"), info.getDescription()));
+            }
+            content.add(Arrays.asList(tr("Native projections"), Utils.join(", ", getNativeProjections())));
+            for (List<String> entry: content) {
+                panel.add(new JLabel(entry.get(0) + ':'), GBC.std());
+                panel.add(GBC.glue(5, 0), GBC.std());
+                panel.add(createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL));
             }
         }
         return panel;
+    }
+
+    protected JTextField createTextField(String text) {
+        JTextField ret = new JTextField(text);
+        ret.setEditable(false);
+        ret.setBorder(BorderFactory.createEmptyBorder());
+        return ret;
     }
 
Index: trunk/src/org/openstreetmap/josm/gui/layer/TMSLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/TMSLayer.java	(revision 11856)
+++ trunk/src/org/openstreetmap/josm/gui/layer/TMSLayer.java	(revision 11858)
@@ -6,4 +6,6 @@
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 
 import org.apache.commons.jcs.access.CacheAccess;
@@ -79,11 +81,6 @@
 
     @Override
-    public final boolean isProjectionSupported(Projection proj) {
-        return "EPSG:3857".equals(proj.toCode()) || "EPSG:4326".equals(proj.toCode());
-    }
-
-    @Override
-    public final String nameSupportedProjections() {
-        return tr("EPSG:4326 and Mercator projection are supported");
+    public Collection<String> getNativeProjections() {
+        return Collections.singletonList("EPSG:3857");
     }
 
@@ -161,3 +158,3 @@
         return new ScaleList(scales);
     }
- }
+}
Index: trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java	(revision 11856)
+++ trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java	(revision 11858)
@@ -7,7 +7,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
-import java.util.Set;
-import java.util.TreeSet;
+import java.util.Objects;
 
 import javax.swing.AbstractAction;
@@ -28,4 +28,5 @@
 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;
@@ -55,5 +56,5 @@
     private static final String CACHE_REGION_NAME = "WMS";
 
-    private final Set<String> supportedProjections;
+    private final List<String> serverProjections;
 
     /**
@@ -66,5 +67,5 @@
         CheckParameterUtil.ensureParameterNotNull(info.getUrl(), "info.url");
         TemplatedWMSTileSource.checkUrl(info.getUrl());
-        this.supportedProjections = new TreeSet<>(info.getServerProjections());
+        this.serverProjections = new ArrayList<>(info.getServerProjections());
     }
 
@@ -87,5 +88,6 @@
     @Override
     protected AbstractWMSTileSource getTileSource() {
-        AbstractWMSTileSource tileSource = new TemplatedWMSTileSource(info);
+        AbstractWMSTileSource tileSource = new TemplatedWMSTileSource(
+                info, chooseProjection(Main.getProjection()));
         info.setAttribution(tileSource);
         return tileSource;
@@ -112,49 +114,31 @@
 
     @Override
-    public boolean isProjectionSupported(Projection proj) {
-        return supportedProjections == null || supportedProjections.isEmpty() || supportedProjections.contains(proj.toCode()) ||
-                (info.isEpsg4326To3857Supported() && supportedProjections.contains("EPSG:4326")
-                        && "EPSG:3857".equals(Main.getProjection().toCode()));
-    }
-
-    @Override
-    public String nameSupportedProjections() {
-        StringBuilder ret = new StringBuilder();
-        for (String e: supportedProjections) {
-            ret.append(e).append(", ");
-        }
-        String appendix = "";
-
-        if (isReprojectionPossible()) {
-            appendix = ". <p>" + tr("JOSM will use EPSG:4326 to query the server, but results may vary "
-                    + "depending on the WMS server") + "</p>";
-        }
-        return ret.substring(0, ret.length()-2) + appendix;
+    public Collection<String> getNativeProjections() {
+        return serverProjections;
     }
 
     @Override
     public void projectionChanged(Projection oldValue, Projection newValue) {
-        // do not call super - we need custom warning dialog
+        Projection tileProjection = chooseProjection(newValue);
+        if (!Objects.equals(tileSource.getTileProjection(), tileProjection)) {
+            tileSource.setTileProjection(tileProjection);
+        }
+    }
 
-        if (!isProjectionSupported(newValue)) {
-            String message =
-                    "<html><body><p>" + tr("The layer {0} does not support the new projection {1}.",
-                            Utils.escapeReservedCharactersHTML(getName()), newValue.toCode()) +
-                    "<p style='width: 450px; position: absolute; margin: 0px;'>" +
-                            tr("Supported projections are: {0}", nameSupportedProjections()) + "</p>" +
-                    "<p>" + tr("Change the projection again or remove the layer.");
-
-            ExtendedDialog warningDialog = new ExtendedDialog(Main.parent, tr("Warning"), new String[]{tr("OK")}).
-                    setContent(message).
-                    setIcon(JOptionPane.WARNING_MESSAGE);
-
-            if (isReprojectionPossible()) {
-                warningDialog.toggleEnable("imagery.wms.projectionSupportWarnings." + tileSource.getBaseUrl());
+    private Projection chooseProjection(Projection requested) {
+        if (serverProjections.contains(requested.toCode())) {
+            return requested;
+        } else {
+            for (String code : serverProjections) {
+                Projection proj = Projections.getProjectionByCode(code);
+                if (proj != null) {
+                    Main.info(tr("Reprojecting layer {0} from {1} to {2}. For best image quality and performance,"
+                            + " switch to one of the supported projections: {3}",
+                            getName(), proj.toCode(), Main.getProjection().toCode(), Utils.join(", ", getNativeProjections())));
+                    return proj;
+                }
             }
-            warningDialog.showDialog();
-        }
-
-        if (!newValue.equals(oldValue)) {
-            tileSource.initProjection(newValue);
+            Main.warn(tr("Unable to find supported projection for layer {0}. Using {1}.", getName(), requested.toCode()));
+            return requested;
         }
     }
@@ -176,7 +160,3 @@
         return AbstractCachedTileSourceLayer.getCache(CACHE_REGION_NAME);
     }
-
-    private boolean isReprojectionPossible() {
-        return supportedProjections.contains("EPSG:4326") && "EPSG:3857".equals(Main.getProjection().toCode());
-    }
 }
Index: trunk/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java	(revision 11856)
+++ trunk/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java	(revision 11858)
@@ -3,5 +3,5 @@
 
 import java.io.IOException;
-import java.util.Set;
+import java.util.Collection;
 
 import org.apache.commons.jcs.access.CacheAccess;
@@ -15,4 +15,5 @@
 import org.openstreetmap.josm.data.projection.Projection;
 import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings;
+import org.openstreetmap.josm.tools.Utils;
 
 /**
@@ -75,13 +76,11 @@
             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());
     }
 
@@ -92,16 +91,6 @@
 
     @Override
-    public boolean isProjectionSupported(Projection proj) {
-        Set<String> supportedProjections = tileSource.getSupportedProjections();
-        return supportedProjections.contains(proj.toCode());
-    }
-
-    @Override
-    public String nameSupportedProjections() {
-        StringBuilder ret = new StringBuilder();
-        for (String e: tileSource.getSupportedProjections()) {
-            ret.append(e).append(", ");
-        }
-        return ret.length() > 2 ? ret.substring(0, ret.length()-2) : ret.toString();
+    public Collection<String> getNativeProjections() {
+        return tileSource.getSupportedProjections();
     }
 
Index: trunk/src/org/openstreetmap/josm/gui/layer/imagery/ReprojectionTile.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/imagery/ReprojectionTile.java	(revision 11858)
+++ trunk/src/org/openstreetmap/josm/gui/layer/imagery/ReprojectionTile.java	(revision 11858)
@@ -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;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java	(revision 11856)
+++ trunk/src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java	(revision 11858)
@@ -12,4 +12,5 @@
 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;
@@ -85,5 +86,22 @@
      */
     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);
+        }
     }
 
@@ -91,5 +109,5 @@
      * 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) {
@@ -130,9 +148,16 @@
      */
     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());
@@ -147,7 +172,21 @@
      */
     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());
+        }
+    }
+
+    /**
+     * Return true if tiles need to be reprojected from server projection to display projection.
+     * @return true if tiles need to be reprojected from server projection to display projection
+     */
+    public boolean requiresReprojection() {
+        return !tileSource.getServerCRS().equals(Main.getProjection().toCode());
     }
 }
Index: trunk/src/org/openstreetmap/josm/io/imagery/ImageryReader.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/imagery/ImageryReader.java	(revision 11856)
+++ trunk/src/org/openstreetmap/josm/io/imagery/ImageryReader.java	(revision 11858)
@@ -458,7 +458,4 @@
                     entry.setGeoreferenceValid(Boolean.parseBoolean(accumulator.toString()));
                     break;
-                case "epsg4326to3857Supported":
-                    entry.setEpsg4326To3857Supported(Boolean.parseBoolean(accumulator.toString()));
-                    break;
                 default: // Do nothing
                 }
Index: trunk/src/org/openstreetmap/josm/tools/ImageWarp.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/ImageWarp.java	(revision 11858)
+++ trunk/src/org/openstreetmap/josm/tools/ImageWarp.java	(revision 11858)
@@ -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);
+    }
+}
