Index: /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/BingAerialTileSource.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/BingAerialTileSource.java	(revision 24706)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/BingAerialTileSource.java	(revision 24706)
@@ -0,0 +1,204 @@
+package org.openstreetmap.gui.jmapviewer;
+
+import java.awt.Image;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.imageio.ImageIO;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+import org.xml.sax.helpers.XMLReaderFactory;
+
+public class BingAerialTileSource extends OsmTileSource.AbstractOsmTileSource {
+    private static String API_KEY = "Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU";
+    private static List<Attribution> attributions;
+
+    public BingAerialTileSource() {
+        super("Bing Aerial Maps", "http://ecn.t2.tiles.virtualearth.net/tiles/");
+
+        if (attributions == null) {
+            Thread t = new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    attributions = loadAttributionText();
+                }
+            });
+            t.setDaemon(true);
+            t.start();
+        }
+    }
+
+    class Attribution {
+        String attribution;
+        int minZoom;
+        int maxZoom;
+        Coordinate min;
+        Coordinate max;
+    }
+
+    class AttrHandler extends DefaultHandler {
+
+        private String string;
+        private Attribution curr;
+        private List<Attribution> attributions = new ArrayList<Attribution>();
+        private double southLat;
+        private double northLat;
+        private double eastLon;
+        private double westLon;
+        private boolean inCoverage = false;
+
+        @Override
+        public void startElement(String uri, String stripped, String tagName,
+                Attributes attrs) throws SAXException {
+            if("ImageryProvider".equals(tagName)) {
+                curr = new Attribution();
+            } else if("CoverageArea".equals(tagName)) {
+                inCoverage = true;
+            }
+        }
+
+        @Override
+        public void characters(char[] ch, int start, int length)
+        throws SAXException {
+            string = new String(ch, start, length);
+        }
+
+        @Override
+        public void endElement(String uri, String stripped, String tagName)
+        throws SAXException {
+            if("ImageryProvider".equals(tagName)) {
+                attributions.add(curr);
+            } else if("Attribution".equals(tagName)) {
+                curr.attribution = string;
+            } else if(inCoverage && "ZoomMin".equals(tagName)) {
+                curr.minZoom = Integer.parseInt(string);
+            } else if(inCoverage && "ZoomMax".equals(tagName)) {
+                curr.maxZoom = Integer.parseInt(string);
+            } else if(inCoverage && "SouthLatitude".equals(tagName)) {
+                southLat = Double.parseDouble(string);
+            } else if(inCoverage && "NorthLatitude".equals(tagName)) {
+                northLat = Double.parseDouble(string);
+            } else if(inCoverage && "EastLongitude".equals(tagName)) {
+                eastLon = Double.parseDouble(string);
+            } else if(inCoverage && "WestLongitude".equals(tagName)) {
+                westLon = Double.parseDouble(string);
+            } else if("BoundingBox".equals(tagName)) {
+                curr.min = new Coordinate(southLat, westLon);
+                curr.max = new Coordinate(northLat, eastLon);
+            } else if("CoverageArea".equals(tagName)) {
+                inCoverage = false;
+            }
+            string = "";
+        }
+    }
+
+    private List<Attribution> loadAttributionText() {
+        try {
+            URL u = new URL("http://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial/0,0?zl=1&mapVersion=v1&key="+API_KEY+"&include=ImageryProviders&output=xml");
+            InputStream stream = u.openStream();
+            XMLReader parser = XMLReaderFactory.createXMLReader();
+            AttrHandler handler = new AttrHandler();
+            parser.setContentHandler(handler);
+            parser.parse(new InputSource(stream));
+            System.err.println("Added " + handler.attributions.size() + " attributions.");
+            return handler.attributions;
+        } catch (IOException e) {
+            System.err.println("Could not open Bing aerials attribution metadata.");
+        } catch (SAXException e) {
+            System.err.println("Could not parse Bing aerials attribution metadata.");
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    @Override
+    public int getMaxZoom() {
+        return 22;
+    }
+
+    @Override
+    public String getExtension() {
+        return("jpeg");
+    }
+
+    @Override
+    public String getTilePath(int zoom, int tilex, int tiley) {
+        String quadtree = computeQuadTree(zoom, tilex, tiley);
+        return "/tiles/a" + quadtree + "." + getExtension() + "?g=587";
+    }
+
+    @Override
+    public TileUpdate getTileUpdate() {
+        return TileUpdate.IfNoneMatch;
+    }
+
+    @Override
+    public boolean requiresAttribution() {
+        return true;
+    }
+
+    @Override
+    public Image getAttributionImage() {
+        try {
+            return ImageIO.read(getClass().getResourceAsStream("/images/bing_maps.png"));
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    @Override
+    public String getAttributionLinkURL() {
+        //return "http://bing.com/maps"
+        // FIXME: I've set attributionLinkURL temporarily to ToU URL to comply with bing ToU
+        // (the requirement is that we have such a link at the bottom of the window)
+        return "http://go.microsoft.com/?linkid=9710837";
+    }
+
+    @Override
+    public String getTermsOfUseURL() {
+        return "http://opengeodata.org/microsoft-imagery-details";
+    }
+
+    @Override
+    public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight) {
+        if (attributions == null)
+            // TODO: don't show Bing tiles until attribution data is loaded
+            return "";
+        StringBuilder a = new StringBuilder();
+        for (Attribution attr : attributions) {
+            if(zoom <= attr.maxZoom && zoom >= attr.minZoom) {
+                if(topLeft.getLon() < attr.max.getLon()
+                        && botRight.getLon() > attr.min.getLon()
+                        && topLeft.getLat() > attr.min.getLat()
+                        && botRight.getLat() < attr.max.getLat()) {
+                    a.append(attr.attribution);
+                    a.append(" ");
+                }
+            }
+        }
+        return a.toString();
+    }
+
+    static String computeQuadTree(int zoom, int tilex, int tiley) {
+        StringBuilder k = new StringBuilder();
+        for(int i = zoom; i > 0; i--) {
+            char digit = 48;
+            int mask = 1 << (i - 1);
+            if ((tilex & mask) != 0) {
+                digit += 1;
+            }
+            if ((tiley & mask) != 0) {
+                digit += 2;
+            }
+            k.append(digit);
+        }
+        return k.toString();
+    }
+}
Index: /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/JMapViewer.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/JMapViewer.java	(revision 24705)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/JMapViewer.java	(revision 24706)
@@ -32,5 +32,4 @@
 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
-import org.openstreetmap.josm.data.coor.LatLon;
 
 /**
@@ -774,7 +773,5 @@
         Coordinate topLeft = getPosition(0,0);
         Coordinate bottomRight = getPosition(getWidth(),getHeight());
-        String attributionText = tileSource.getAttributionText(zoom,
-                new LatLon(topLeft.getLat(),topLeft.getLon()),
-                new LatLon(bottomRight.getLat(),bottomRight.getLon()));
+        String attributionText = tileSource.getAttributionText(zoom, topLeft, bottomRight);
         Rectangle2D stringBounds = g.getFontMetrics().getStringBounds(attributionText, g);
         {
Index: /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/OsmTileSource.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/OsmTileSource.java	(revision 24705)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/OsmTileSource.java	(revision 24706)
@@ -6,5 +6,4 @@
 
 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
-import org.openstreetmap.josm.data.coor.LatLon;
 
 public class OsmTileSource {
@@ -85,5 +84,5 @@
         }
 
-        public String getAttributionText(int zoom, LatLon topLeft, LatLon botRight) {
+        public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight) {
             return "CC-BY-SA OpenStreetMap and Contributors";
         }
Index: /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/TMSTileSource.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/TMSTileSource.java	(revision 24706)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/TMSTileSource.java	(revision 24706)
@@ -0,0 +1,21 @@
+package org.openstreetmap.gui.jmapviewer;
+
+
+public class TMSTileSource extends OsmTileSource.AbstractOsmTileSource {
+    private int maxZoom;
+
+    public TMSTileSource(String name, String url, int maxZoom) {
+        super(name, url);
+        this.maxZoom = maxZoom;
+    }
+
+    @Override
+    public int getMaxZoom() {
+        return (maxZoom == 0) ? super.getMaxZoom() : maxZoom;
+    }
+
+    @Override
+    public TileUpdate getTileUpdate() {
+        return TileUpdate.IfNoneMatch;
+    }
+}
Index: /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/TemplatedTMSTileSource.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/TemplatedTMSTileSource.java	(revision 24706)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/TemplatedTMSTileSource.java	(revision 24706)
@@ -0,0 +1,29 @@
+package org.openstreetmap.gui.jmapviewer;
+
+
+public class TemplatedTMSTileSource extends OsmTileSource.AbstractOsmTileSource {
+    private int maxZoom;
+    
+    public TemplatedTMSTileSource(String name, String url, int maxZoom) {
+        super(name, url);
+        this.maxZoom = maxZoom;
+    }
+
+    public String getTileUrl(int zoom, int tilex, int tiley) {
+        return this.BASE_URL
+        .replaceAll("\\{zoom\\}", Integer.toString(zoom))
+        .replaceAll("\\{x\\}", Integer.toString(tilex))
+        .replaceAll("\\{y\\}", Integer.toString(tiley));
+        
+    }
+
+    @Override
+    public int getMaxZoom() {
+        return (maxZoom == 0) ? super.getMaxZoom() : maxZoom;
+    }
+
+    @Override
+    public TileUpdate getTileUpdate() {
+        return TileUpdate.IfNoneMatch;
+    }
+}
Index: /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java	(revision 24705)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java	(revision 24706)
@@ -3,6 +3,6 @@
 import java.awt.Image;
 
+import org.openstreetmap.gui.jmapviewer.Coordinate;
 import org.openstreetmap.gui.jmapviewer.JMapViewer;
-import org.openstreetmap.josm.data.coor.LatLon;
 
 //License: GPL. Copyright 2008 by Jan Peter Stotz
@@ -101,5 +101,4 @@
     public boolean requiresAttribution();
 
-    // FIXME: JMapViewer shouldn't reference JOSM classes.
     /**
      * @param zoom The optional zoom level for the view.
@@ -108,5 +107,5 @@
      * @return Attribution text for the image source.
      */
-    public String getAttributionText(int zoom, LatLon topLeft, LatLon botRight);
+    public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight);
 
     /**
