Index: applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/Projected.java
===================================================================
--- applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/Projected.java	(revision 33207)
+++ applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/Projected.java	(revision 33207)
@@ -0,0 +1,70 @@
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer;
+
+import java.awt.geom.Point2D;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.Objects;
+
+import org.openstreetmap.gui.jmapviewer.interfaces.IProjected;
+
+/**
+ * Projected coordinates represented by an encapsulates a Point2D.Double value.
+ */
+public class Projected implements IProjected {
+    private transient Point2D.Double data;
+
+    public Projected(double east, double north) {
+        data = new Point2D.Double(east, north);
+    }
+
+    @Override
+    public double getEast() {
+        return data.x;
+    }
+
+    @Override
+    public double getNorth() {
+        return data.y;
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        out.writeObject(data.x);
+        out.writeObject(data.y);
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        data = new Point2D.Double();
+        data.x = (Double) in.readObject();
+        data.y = (Double) in.readObject();
+    }
+
+    @Override
+    public String toString() {
+        return "Projected[" + data.y + ", " + data.x + ']';
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 3;
+        hash = 61 * hash + Objects.hashCode(this.data);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final Projected other = (Projected) obj;
+        if (!Objects.equals(this.data, other.data)) {
+            return false;
+        }
+        return true;
+    }
+}
+
Index: applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/TileRange.java
===================================================================
--- applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/TileRange.java	(revision 33206)
+++ applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/TileRange.java	(revision 33207)
@@ -15,5 +15,5 @@
     }
 
-    protected TileRange(TileXY t1, TileXY t2, int zoom) {
+    public TileRange(TileXY t1, TileXY t2, int zoom) {
         minX = (int) Math.floor(Math.min(t1.getX(), t2.getX()));
         minY = (int) Math.floor(Math.min(t1.getY(), t2.getY()));
Index: applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/interfaces/IProjected.java
===================================================================
--- applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/interfaces/IProjected.java	(revision 33207)
+++ applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/interfaces/IProjected.java	(revision 33207)
@@ -0,0 +1,14 @@
+// License: GPL. For details, see Readme.txt file.
+package org.openstreetmap.gui.jmapviewer.interfaces;
+
+/**
+ * Projected coordinates (east / north space).
+ *
+ * For most projections, one unit in projected space is roughly one meter, but
+ * can also be degrees or feet.
+ */
+public interface IProjected {
+    double getEast();
+    double getNorth();
+}
+
Index: applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java
===================================================================
--- applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java	(revision 33206)
+++ applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java	(revision 33207)
@@ -9,4 +9,5 @@
 import org.openstreetmap.gui.jmapviewer.JMapViewer;
 import org.openstreetmap.gui.jmapviewer.Tile;
+import org.openstreetmap.gui.jmapviewer.TileRange;
 import org.openstreetmap.gui.jmapviewer.TileXY;
 
@@ -210,3 +211,54 @@
      */
     Map<String, String> getMetadata(Map<String, List<String>> headers);
+
+    /**
+     * Convert tile indeces (x/y/zoom) into projected coordinates of the tile origin.
+     * @param x x tile index
+     * @param y z tile index
+     * @param zoom zoom level
+     * @return projected coordinates of the tile origin
+     */
+    IProjected tileXYtoProjected(int x, int y, int zoom);
+
+    /**
+     * Convert projected coordinates to tile indices.
+     * @param p projected coordinates
+     * @param zoom zoom level
+     * @return corresponding tile index x/y (floating point, truncate to integer
+     * for tile index)
+     */
+    TileXY projectedToTileXY(IProjected p, int zoom);
+
+    /**
+     * Check if one tile is inside another tile.
+     * @param inner the tile that is suspected to be inside the other tile
+     * @param outer the tile that might contain the first tile
+     * @return true if first tile is inside second tile (or both are identical),
+     * false otherwise
+     */
+    boolean isInside(Tile inner, Tile outer);
+
+    /**
+     * Returns a range of tiles, that cover a given tile, which is
+     * usually at a different zoom level.
+     *
+     * In standard tile layout, 4 tiles cover a tile one zoom lower, 16 tiles
+     * cover a tile 2 zoom levels below etc.
+     * If the zoom level of the covering tiles is greater or equal, a single
+     * tile suffices.
+     *
+     * @param tile the tile to cover
+     * @param newZoom zoom level of the covering tiles
+     * @return TileRange of all covering tiles at zoom <code>newZoom</code>
+     */
+    TileRange getCoveringTileRange(Tile tile, int newZoom);
+
+    /**
+     * Get content reference system for this tile source.
+     *
+     * E.g. "EPSG:3857" for Google-Mercator.
+     * @return code for the content reference system in use
+     */
+    String getServerCRS();
+
 }
Index: applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/TMSTileSource.java
===================================================================
--- applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/TMSTileSource.java	(revision 33206)
+++ applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/TMSTileSource.java	(revision 33207)
@@ -6,6 +6,10 @@
 import org.openstreetmap.gui.jmapviewer.Coordinate;
 import org.openstreetmap.gui.jmapviewer.OsmMercator;
+import org.openstreetmap.gui.jmapviewer.Projected;
+import org.openstreetmap.gui.jmapviewer.Tile;
+import org.openstreetmap.gui.jmapviewer.TileRange;
 import org.openstreetmap.gui.jmapviewer.TileXY;
 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
+import org.openstreetmap.gui.jmapviewer.interfaces.IProjected;
 
 /**
@@ -75,3 +79,44 @@
                 );
     }
+
+    @Override
+    public IProjected tileXYtoProjected(int x, int y, int zoom) {
+        double mercatorWidth = 2 * Math.PI * OsmMercator.EARTH_RADIUS;
+        double f = mercatorWidth * getTileSize() / osmMercator.getMaxPixels(zoom);
+        return new Projected(f * x - mercatorWidth / 2, -(f * y - mercatorWidth / 2));
+    }
+
+    @Override
+    public TileXY projectedToTileXY(IProjected p, int zoom) {
+        double mercatorWidth = 2 * Math.PI * OsmMercator.EARTH_RADIUS;
+        double f = mercatorWidth * getTileSize() / osmMercator.getMaxPixels(zoom);
+        return new TileXY((p.getEast() + mercatorWidth / 2) / f , (-p.getNorth() + mercatorWidth / 2) / f);
+    }
+
+    @Override
+    public boolean isInside(Tile inner, Tile outer) {
+        int dz = inner.getZoom() - outer.getZoom();
+        if (dz < 0) return false;
+        return outer.getXtile() == inner.getXtile() >> dz &&
+                outer.getYtile() == inner.getYtile() >> dz;
+    }
+
+    @Override
+    public TileRange getCoveringTileRange(Tile tile, int newZoom) {
+        if (newZoom <= tile.getZoom()) {
+            int dz = tile.getZoom() - newZoom;
+            TileXY xy = new TileXY(tile.getXtile() >> dz, tile.getYtile() >> dz);
+            return new TileRange(xy, xy, newZoom);
+        } else {
+            int dz = newZoom - tile.getZoom();
+            TileXY t1 = new TileXY(tile.getXtile() << dz, tile.getYtile() << dz);
+            TileXY t2 = new TileXY(t1.getX() + (1 << dz) - 1, t1.getY() + (1 << dz) - 1);
+            return new TileRange(t1, t2, newZoom);
+        }
+    }
+
+    @Override
+    public String getServerCRS() {
+        return "EPSG:3857";
+    }
 }
