Index: trunk/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java	(revision 11840)
+++ trunk/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java	(revision 11841)
@@ -13,8 +13,10 @@
 import java.awt.Image;
 import java.awt.Point;
+import java.awt.Shape;
 import java.awt.Toolkit;
 import java.awt.event.ActionEvent;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
 import java.awt.geom.Point2D;
 import java.awt.geom.Rectangle2D;
@@ -25,4 +27,5 @@
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.text.MessageFormat;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -62,4 +65,5 @@
 import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
 import org.openstreetmap.gui.jmapviewer.Tile;
+import org.openstreetmap.gui.jmapviewer.TileAnchor;
 import org.openstreetmap.gui.jmapviewer.TileRange;
 import org.openstreetmap.gui.jmapviewer.TileXY;
@@ -1044,6 +1048,6 @@
      * @return  the image of the tile or null.
      */
-    private Image getLoadedTileImage(Tile tile) {
-        Image img = tile.getImage();
+    private BufferedImage getLoadedTileImage(Tile tile) {
+        BufferedImage img = tile.getImage();
         if (!imageLoaded(img))
             return null;
@@ -1051,59 +1055,42 @@
     }
 
-    // 'source' is the pixel coordinates for the area that the img is capable of filling in.
-    // However, we probably only want a portion of it.
-    //
-    // 'border' is the screen cordinates that need to be drawn. We must not draw outside of it.
-    private void drawImageInside(Graphics g, Image sourceImg, Rectangle2D source, Rectangle2D border) {
-        Rectangle2D target = source;
-
-        // If a border is specified, only draw the intersection if what we have combined with what we are supposed to draw.
-        if (border != null) {
-            target = source.createIntersection(border);
-            if (Main.isDebugEnabled()) {
-                Main.debug("source: " + source + "\nborder: " + border + "\nintersection: " + target);
-            }
-        }
-
-        // All of the rectangles are in screen coordinates. We need to how these correlate to the sourceImg pixels.
-        // We could avoid doing this by scaling the image up to the 'source' size, but this should be cheaper.
-        //
-        // In some projections, x any y are scaled differently enough to
-        // cause a pixel or two of fudge.  Calculate them separately.
-        double imageYScaling = sourceImg.getHeight(this) / source.getHeight();
-        double imageXScaling = sourceImg.getWidth(this) / source.getWidth();
-
-        // How many pixels into the 'source' rectangle are we drawing?
-        double screenXoffset = target.getX() - source.getX();
-        double screenYoffset = target.getY() - source.getY();
-        // And how many pixels into the image itself does that correlate to?
-        int imgXoffset = (int) (screenXoffset * imageXScaling + 0.5);
-        int imgYoffset = (int) (screenYoffset * imageYScaling + 0.5);
-        // Now calculate the other corner of the image that we need
-        // by scaling the 'target' rectangle's dimensions.
-        int imgXend = imgXoffset + (int) (target.getWidth() * imageXScaling + 0.5);
-        int imgYend = imgYoffset + (int) (target.getHeight() * imageYScaling + 0.5);
-
-        if (Main.isDebugEnabled()) {
-            Main.debug("drawing image into target rect: " + target);
-        }
-        g.drawImage(sourceImg,
-                (int) target.getX(), (int) target.getY(),
-                (int) target.getMaxX(), (int) target.getMaxY(),
-                imgXoffset, imgYoffset,
-                imgXend, imgYend,
-                this);
-    }
-
-    private List<Tile> paintTileImages(Graphics g, TileSet ts) {
+    /**
+     * Draw a tile image on screen.
+     * @param g the Graphics2D
+     * @param toDrawImg tile image
+     * @param anchorImage tile anchor in image coordinates
+     * @param anchorScreen tile anchor in screen coordinates
+     * @param clip clipping region in screen coordinates (can be null)
+     */
+    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);
+        Shape oldClip = null;
+        if (clip != null) {
+            oldClip = g.getClip();
+            g.clip(clip);
+        }
+        g.drawImage(toDrawImg, (int) Math.round(screen0.getX()), (int) Math.round(screen0.getY()),
+                (int) Math.round(screen1.getX() - screen0.getX()), (int) Math.round(screen1.getY() - screen0.getY()), this);
+        if (clip != null) {
+            g.setClip(oldClip);
+        }
+    }
+
+    private List<Tile> paintTileImages(Graphics2D g, TileSet ts) {
         Object paintMutex = new Object();
         List<TilePosition> missed = Collections.synchronizedList(new ArrayList<>());
         ts.visitTiles(tile -> {
             boolean miss = false;
-            Image img = null;
+            BufferedImage img = null;
+            TileAnchor anchorImage = null;
             if (!tile.isLoaded() || tile.hasError()) {
                 miss = true;
             } else {
-                img = getLoadedTileImage(tile);
+                synchronized (tile) {
+                    img = getLoadedTileImage(tile);
+                    anchorImage = tile.getAnchor();
+                }
                 if (img == null) {
                     miss = true;
@@ -1115,8 +1102,8 @@
             }
             img = applyImageProcessors((BufferedImage) img);
-            Rectangle2D sourceRect = coordinateConverter.getRectangleForTile(tile);
+            TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile);
             synchronized (paintMutex) {
                 //cannot paint in parallel
-                drawImageInside(g, img, sourceRect, null);
+                drawImageInside(g, img, anchorImage, anchorScreen, null);
             }
         }, missed::add);
@@ -1132,7 +1119,7 @@
     // It will not be from the zoom level that is being drawn currently.
     // If drawing the displayZoomLevel, border is null and we draw the entire tile set.
-    private List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {
+    private List<Tile> paintTileImages(Graphics2D g, TileSet ts, int zoom, Tile border) {
         if (zoom <= 0) return Collections.emptyList();
-        Rectangle2D borderRect = coordinateConverter.getRectangleForTile(border);
+        Shape borderClip = coordinateConverter.getScreenQuadrilateralForTile(border);
         List<Tile> missedTiles = new LinkedList<>();
         // The callers of this code *require* that we return any tiles that we do not draw in missedTiles.
@@ -1141,9 +1128,13 @@
         for (Tile tile : ts.allTilesCreate()) {
             boolean miss = false;
-            Image img = null;
+            BufferedImage img = null;
+            TileAnchor anchorImage = null;
             if (!tile.isLoaded() || tile.hasError()) {
                 miss = true;
             } else {
-                img = getLoadedTileImage(tile);
+               synchronized (tile) {
+                    img = getLoadedTileImage(tile);
+                    anchorImage = tile.getAnchor();
+                }
                 if (img == null) {
                     miss = true;
@@ -1158,14 +1149,14 @@
             img = applyImageProcessors((BufferedImage) img);
 
-            Rectangle2D sourceRect = coordinateConverter.getRectangleForTile(tile);
-            Rectangle2D clipRect;
+            Shape clip;
             if (tileSource.isInside(tile, border)) {
-                clipRect = null;
+                clip = null;
             } else if (tileSource.isInside(border, tile)) {
-                clipRect = borderRect;
+                clip = borderClip;
             } else {
                 continue;
             }
-            drawImageInside(g, img, sourceRect, clipRect);
+            TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile);
+            drawImageInside(g, img, anchorImage, anchorScreen, clip);
         }
         return missedTiles;
Index: trunk/src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java	(revision 11840)
+++ trunk/src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java	(revision 11841)
@@ -2,11 +2,16 @@
 package org.openstreetmap.josm.gui.layer.imagery;
 
+import java.awt.Shape;
+import java.awt.geom.Path2D;
 import java.awt.geom.Point2D;
 import java.awt.geom.Rectangle2D;
 
 import org.openstreetmap.gui.jmapviewer.Tile;
+import org.openstreetmap.gui.jmapviewer.TileAnchor;
 import org.openstreetmap.gui.jmapviewer.TileXY;
 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.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.projection.Projecting;
@@ -41,4 +46,8 @@
     }
 
+    private MapViewPoint pos(IProjected p) {
+        return mapView.getState().getPointFor(new EastNorth(p)).add(settings.getDisplacement());
+    }
+
     /**
      * Gets the projecting instance to use to convert between latlon and eastnorth coordinates.
@@ -51,10 +60,21 @@
     /**
      * Gets the top left position of the tile inside the map view.
+     * @param x x tile index
+     * @param y y tile index
+     * @param zoom zoom level
+     * @return the position
+     */
+    public Point2D getPixelForTile(int x, int y, int zoom) {
+        ICoordinate coord = tileSource.tileXYToLatLon(x, y, zoom);
+        return pos(coord).getInView();
+    }
+
+    /**
+     * Gets the top left position of the tile inside the map view.
      * @param tile The tile
-     * @return The positon.
+     * @return The position.
      */
     public Point2D getPixelForTile(Tile tile) {
-        ICoordinate coord = tile.getTileSource().tileXYToLatLon(tile);
-        return pos(coord).getInView();
+        return this.getPixelForTile(tile.getXtile(), tile.getYtile(), tile.getZoom());
     }
 
@@ -69,4 +89,28 @@
 
         return pos(c1).rectTo(pos(c2)).getInView();
+    }
+
+    /**
+     * Returns a quadrilateral formed by the 4 corners of the tile in screen coordinates.
+     *
+     * If the tile is rectangular, this will be the exact border of the tile.
+     * The tile may be more oddly shaped due to reprojection, then it is an approximation
+     * of the tile outline.
+     * @param tile the tile
+     * @return quadrilateral tile outline in screen coordinates
+     */
+    public Shape getScreenQuadrilateralForTile(Tile tile) {
+        Point2D p00 = this.getPixelForTile(tile.getXtile(), tile.getYtile(), tile.getZoom());
+        Point2D p10 = this.getPixelForTile(tile.getXtile() + 1, tile.getYtile(), tile.getZoom());
+        Point2D p11 = this.getPixelForTile(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
+        Point2D p01 = this.getPixelForTile(tile.getXtile(), tile.getYtile() + 1, tile.getZoom());
+
+        Path2D pth = new Path2D.Double();
+        pth.moveTo(p00.getX(), p00.getY());
+        pth.lineTo(p01.getX(), p01.getY());
+        pth.lineTo(p11.getX(), p11.getY());
+        pth.lineTo(p10.getX(), p10.getY());
+        pth.closePath();
+        return pth;
     }
 
@@ -87,3 +131,14 @@
         return screenPixels/tilePixels;
     }
+
+    /**
+     * Get {@link TileAnchor} for a tile in screen pixel coordinates.
+     * @param tile the tile
+     * @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());
+    }
 }
