Index: plications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/BingAerialTileSource.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/BingAerialTileSource.java	(revision 25368)
+++ 	(revision )
@@ -1,219 +1,0 @@
-package org.openstreetmap.gui.jmapviewer;
-
-import java.awt.Image;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URL;
-import java.net.URLConnection;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.Callable;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-
-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 Future<List<Attribution>> attributions;
-
-    public BingAerialTileSource() {
-        super("Bing Aerial Maps", "http://ecn.t2.tiles.virtualearth.net/tiles/");
-
-        if (attributions == null) {
-            attributions = Executors.newSingleThreadExecutor().submit(new Callable<List<Attribution>>() {
-                public List<Attribution> call() throws Exception {
-                    return loadAttributionText();
-                }
-            });
-        }
-    }
-
-    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");
-            URLConnection conn = u.openConnection();
-
-            // This is not JOSM! Do not use anything other than standard JRE classes within this package!
-            // See package.html for details
-            //conn.setConnectTimeout(Main.pref.getInteger("imagery.bing.load-attribution-text.timeout", 4000));
-
-            InputStream stream = conn.getInputStream();
-
-            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) throws IOException {
-        try {
-            if (attributions.get() == null)
-                throw new IOException("Cannot load Bing attribution");
-            String quadtree = computeQuadTree(zoom, tilex, tiley);
-            return "/tiles/a" + quadtree + "." + getExtension() + "?g=587";
-        } catch (Exception e) {
-            throw new IOException("Cannot load Bing attribution", e);
-        }
-    }
-
-    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) {
-        try {
-            if (!attributions.isDone())
-                return "Loading Bing attribution data...";
-            if (attributions.get() == null)
-                return "Error loading Bing attribution data";
-            StringBuilder a = new StringBuilder();
-            for (Attribution attr : attributions.get()) {
-                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();
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-        return "Error loading Bing attribution data";
-    }
-
-    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/Demo.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/Demo.java	(revision 25368)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/Demo.java	(revision 25369)
@@ -19,4 +19,6 @@
 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
+import org.openstreetmap.gui.jmapviewer.tilesources.BingAerialTileSource;
+import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource;
 
 /**
Index: /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/JMapViewer.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/JMapViewer.java	(revision 25368)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/JMapViewer.java	(revision 25369)
@@ -32,4 +32,5 @@
 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
+import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource;
 
 /**
@@ -745,5 +746,6 @@
 
     private void paintAttribution(Graphics g) {
-        if (!tileSource.requiresAttribution()) return;
+        if (!tileSource.requiresAttribution())
+            return;
         // Draw attribution
         Font font = g.getFont();
@@ -771,9 +773,9 @@
 
         g.setFont(ATTR_FONT);
-        Coordinate topLeft = getPosition(0,0);
-        Coordinate bottomRight = getPosition(getWidth(),getHeight());
+        Coordinate topLeft = getPosition(0, 0);
+        Coordinate bottomRight = getPosition(getWidth(), getHeight());
         String attributionText = tileSource.getAttributionText(zoom, topLeft, bottomRight);
-        Rectangle2D stringBounds = g.getFontMetrics().getStringBounds(attributionText, g);
-        {
+        if (attributionText != null) {
+            Rectangle2D stringBounds = g.getFontMetrics().getStringBounds(attributionText, g);
             int x = getWidth() - (int) stringBounds.getWidth();
             int y = getHeight() - textHeight;
Index: plications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/OsmTileSource.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/OsmTileSource.java	(revision 25368)
+++ 	(revision )
@@ -1,195 +1,0 @@
-package org.openstreetmap.gui.jmapviewer;
-
-import java.awt.Image;
-import java.io.IOException;
-
-import javax.swing.ImageIcon;
-
-import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
-
-public class OsmTileSource {
-
-    public static final String MAP_MAPNIK = "http://tile.openstreetmap.org";
-    public static final String MAP_OSMA = "http://tah.openstreetmap.org/Tiles";
-
-    public static abstract class AbstractOsmTileSource implements TileSource {
-        protected String NAME;
-        protected String BASE_URL;
-        protected String ATTR_IMG_URL;
-        protected boolean REQUIRES_ATTRIBUTION = true;
-
-        public AbstractOsmTileSource(String name, String base_url) {
-            this(name, base_url, null);
-        }
-
-        public AbstractOsmTileSource(String name, String base_url, String attr_img_url) {
-            NAME = name;
-            BASE_URL = base_url;
-            ATTR_IMG_URL = attr_img_url;
-            if(ATTR_IMG_URL == null) {
-                 REQUIRES_ATTRIBUTION = false;
-            }
-        }
-
-        public String getName() {
-            return NAME;
-        }
-
-        public int getMaxZoom() {
-            return 18;
-        }
-
-        public int getMinZoom() {
-            return 0;
-        }
-
-        public String getExtension() {
-            return "png";
-        }
-
-        /**
-         * @throws IOException when subclass cannot return the tile URL
-         */
-        public String getTilePath(int zoom, int tilex, int tiley) throws IOException {
-            return "/" + zoom + "/" + tilex + "/" + tiley + "." + getExtension();
-        }
-
-        public String getBaseUrl() {
-            return this.BASE_URL;
-        }
-
-        public String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
-            return this.getBaseUrl() + getTilePath(zoom, tilex, tiley);
-        }
-
-        @Override
-        public String toString() {
-            return getName();
-        }
-
-        public String getTileType() {
-            return "png";
-        }
-
-        public int getTileSize() {
-            return 256;
-        }
-
-        public Image getAttributionImage() {
-            if (ATTR_IMG_URL != null)
-                return new ImageIcon(ATTR_IMG_URL).getImage();
-            else
-                return null;
-        }
-
-        public boolean requiresAttribution() {
-            return REQUIRES_ATTRIBUTION;
-        }
-
-        public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight) {
-            return "© OpenStreetMap contributors, CC-BY-SA ";
-        }
-
-        public String getAttributionLinkURL() {
-            return "http://openstreetmap.org/";
-        }
-
-        public String getTermsOfUseURL() {
-            return "http://www.openstreetmap.org/copyright";
-        }
-
-        public double latToTileY(double lat, int zoom) {
-            double l = lat / 180 * Math.PI;
-            double pf = Math.log(Math.tan(l) + (1 / Math.cos(l)));
-            return Math.pow(2.0, zoom - 1) * (Math.PI - pf) / Math.PI;
-        }
-
-        public double lonToTileX(double lon, int zoom) {
-            return Math.pow(2.0, zoom - 3) * (lon + 180.0) / 45.0;
-        }
-
-        public double tileYToLat(int y, int zoom) {
-            return Math.atan(Math.sinh(Math.PI - (Math.PI * y / Math.pow(2.0, zoom - 1)))) * 180 / Math.PI;
-        }
-
-        public double tileXToLon(int x, int zoom) {
-            return x * 45.0 / Math.pow(2.0, zoom - 3) - 180.0;
-        }
-    }
-
-    public static class Mapnik extends AbstractOsmTileSource {
-        public Mapnik() {
-            super("Mapnik", MAP_MAPNIK);
-        }
-
-        public TileUpdate getTileUpdate() {
-            return TileUpdate.IfNoneMatch;
-        }
-
-    }
-
-    public static class CycleMap extends AbstractOsmTileSource {
-
-        private static final String PATTERN = "http://%s.tile.opencyclemap.org/cycle";
-
-        private static final String[] SERVER = { "a", "b", "c" };
-
-        private int SERVER_NUM = 0;
-
-        public CycleMap() {
-            super("OSM Cycle Map", PATTERN);
-        }
-
-        @Override
-        public String getBaseUrl() {
-            String url = String.format(this.BASE_URL, new Object[] { SERVER[SERVER_NUM] });
-            SERVER_NUM = (SERVER_NUM + 1) % SERVER.length;
-            return url;
-        }
-
-        @Override
-        public int getMaxZoom() {
-            return 17;
-        }
-
-        public TileUpdate getTileUpdate() {
-            return TileUpdate.LastModified;
-        }
-
-    }
-
-    public static abstract class OsmaSource extends AbstractOsmTileSource {
-        String osmaSuffix;
-
-        public OsmaSource(String name, String osmaSuffix) {
-            super(name, MAP_OSMA);
-            this.osmaSuffix = osmaSuffix;
-        }
-
-        @Override
-        public int getMaxZoom() {
-            return 17;
-        }
-
-        @Override
-        public String getBaseUrl() {
-            return MAP_OSMA + "/" + osmaSuffix;
-        }
-
-        public TileUpdate getTileUpdate() {
-            return TileUpdate.IfModifiedSince;
-        }
-    }
-
-    public static class TilesAtHome extends OsmaSource {
-        public TilesAtHome() {
-            super("TilesAtHome", "tile");
-        }
-    }
-
-    public static class Maplint extends OsmaSource {
-        public Maplint() {
-            super("Maplint", "maplint");
-        }
-    }
-}
Index: plications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/ScanexTileSource.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/ScanexTileSource.java	(revision 25368)
+++ 	(revision )
@@ -1,110 +1,0 @@
-package org.openstreetmap.gui.jmapviewer;
-
-public class ScanexTileSource extends OsmTileSource.AbstractOsmTileSource {
-    private static String API_KEY = "4018C5A9AECAD8868ED5DEB2E41D09F7";
-
-    private enum ScanexLayer {
-        IRS("irs", "/TileSender.ashx?ModeKey=tile&MapName=F7B8CF651682420FA1749D894C8AD0F6&LayerName=BAC78D764F0443BD9AF93E7A998C9F5B"),
-        SPOT("spot", "/TileSender.ashx?ModeKey=tile&MapName=F7B8CF651682420FA1749D894C8AD0F6&LayerName=F51CE95441284AF6B2FC319B609C7DEC");
-
-        private String name;
-        private String uri;
-
-        ScanexLayer(String name, String uri) {
-            this.name = name;
-            this.uri = uri;
-        }
-        public String getName() {
-            return name;
-        }
-        public String getUri() {
-            return uri;
-        }
-    }
-
-    /* IRS by default */
-    private ScanexLayer Layer = ScanexLayer.IRS;
-
-    public ScanexTileSource(String url) {
-        super("Scanex" + url, "http://maps.kosmosnimki.ru");
-
-        for (ScanexLayer layer : ScanexLayer.values()) {
-            if (url.equalsIgnoreCase(layer.getName())) {
-                this.Layer = layer;
-                break;
-            }
-        }
-    }
-
-    @Override
-    public int getMaxZoom() {
-        return 14;
-    }
-
-    @Override
-    public String getExtension() {
-        return("jpeg");
-    }
-
-    @Override
-    public String getTilePath(int zoom, int tilex, int tiley) {
-        int tmp = (int)Math.pow(2.0, zoom - 1);
-
-        tilex = tilex - tmp;
-        tiley = tmp - tiley - 1;
-
-        return this.Layer.getUri() + "&apikey=" + API_KEY + "&x=" + tilex + "&y=" + tiley + "&z=" + zoom;
-    }
-
-    public TileUpdate getTileUpdate() {
-        return TileUpdate.IfNoneMatch;
-    }
-
-    private static double RADIUS_E = 6378137;	/* radius of Earth at equator, m */
-    private static double EQUATOR = 40075016.68557849; /* equator length, m */
-    private static double E = 0.0818191908426;	/* eccentricity of Earth's ellipsoid */
-
-    @Override
-    public double latToTileY(double lat, int zoom) {
-        double tmp = Math.tan(Math.PI/4 * (1 + lat/90));
-        double pow = Math.pow(Math.tan(Math.PI/4 + Math.asin(E * Math.sin(Math.toRadians(lat)))/2), E);
-
-        return (EQUATOR/2 - (RADIUS_E * Math.log(tmp/pow))) * Math.pow(2.0, zoom) / EQUATOR;
-    }
-
-    @Override
-    public double lonToTileX(double lon, int zoom) {
-        return (RADIUS_E * lon * Math.PI / (90*EQUATOR) + 1) * Math.pow(2.0, zoom - 1);
-    }
-
-    /*
-     * DIRTY HACK ALERT!
-     *
-     * Until I can't solve the equation, use dihotomy :(
-     */
-    @Override
-    public double tileYToLat(int y, int zoom) {
-        double lat = 0;
-        double minl = OsmMercator.MIN_LAT;
-        double maxl = OsmMercator.MAX_LAT;
-        double c;
-
-        for (int i=0; i < 60; i++) {
-            c = latToTileY(lat, zoom);
-            if (c < y) {
-                maxl = lat;
-                lat -= (lat - minl)/2;
-            } else {
-                minl = lat;
-                lat += (maxl - lat)/2;
-            }
-        }
-
-        return lat;
-    }
-
-    @Override
-    public double tileXToLon(int x, int zoom) {
-        return (x / Math.pow(2.0, zoom - 1) - 1) * (90*EQUATOR) / RADIUS_E / Math.PI;
-    }
-}
Index: plications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/TMSTileSource.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/TMSTileSource.java	(revision 25368)
+++ 	(revision )
@@ -1,20 +1,0 @@
-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;
-    }
-
-    public TileUpdate getTileUpdate() {
-        return TileUpdate.IfNoneMatch;
-    }
-}
Index: plications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/TemplatedTMSTileSource.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/TemplatedTMSTileSource.java	(revision 25368)
+++ 	(revision )
@@ -1,28 +1,0 @@
-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;
-    }
-
-    public TileUpdate getTileUpdate() {
-        return TileUpdate.IfNoneMatch;
-    }
-}
Index: /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/TileController.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/TileController.java	(revision 25368)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/TileController.java	(revision 25369)
@@ -6,4 +6,5 @@
 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
+import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource;
 
 public class TileController {
Index: /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java	(revision 25368)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java	(revision 25369)
@@ -98,5 +98,5 @@
 
     /**
-     * @return True if the tile source requires attribution.
+     * @return True if the tile source requires attribution in text or image form.
      */
     public boolean requiresAttribution();
@@ -128,7 +128,7 @@
 
     public double lonToTileX(double lon, int zoom);
-     
+
     public double tileYToLat(int y, int zoom);
-            
+
     public double tileXToLon(int x, int zoom);
 }
Index: /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractOsmTileSource.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractOsmTileSource.java	(revision 25369)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractOsmTileSource.java	(revision 25369)
@@ -0,0 +1,114 @@
+/**
+ * 
+ */
+package org.openstreetmap.gui.jmapviewer.tilesources;
+
+import java.awt.Image;
+import java.io.IOException;
+
+import javax.swing.ImageIcon;
+
+import org.openstreetmap.gui.jmapviewer.Coordinate;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
+
+public abstract class AbstractOsmTileSource implements TileSource {
+
+    protected String name;
+    protected String baseUrl;
+    protected String attrImgUrl;
+
+    public AbstractOsmTileSource(String name, String base_url) {
+        this(name, base_url, null);
+    }
+
+    public AbstractOsmTileSource(String name, String base_url, String attr_img_url) {
+        this.name = name;
+        this.baseUrl = base_url;
+        attrImgUrl = attr_img_url;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getMaxZoom() {
+        return 18;
+    }
+
+    public int getMinZoom() {
+        return 0;
+    }
+
+    public String getExtension() {
+        return "png";
+    }
+
+    /**
+     * @throws IOException when subclass cannot return the tile URL
+     */
+    public String getTilePath(int zoom, int tilex, int tiley) throws IOException {
+        return "/" + zoom + "/" + tilex + "/" + tiley + "." + getExtension();
+    }
+
+    public String getBaseUrl() {
+        return this.baseUrl;
+    }
+
+    public String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
+        return this.getBaseUrl() + getTilePath(zoom, tilex, tiley);
+    }
+
+    @Override
+    public String toString() {
+        return getName();
+    }
+
+    public String getTileType() {
+        return "png";
+    }
+
+    public int getTileSize() {
+        return 256;
+    }
+
+    public Image getAttributionImage() {
+        if (attrImgUrl != null)
+            return new ImageIcon(attrImgUrl).getImage();
+        else
+            return null;
+    }
+
+    public boolean requiresAttribution() {
+        return true;
+    }
+
+    public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight) {
+        return "© OpenStreetMap contributors, CC-BY-SA ";
+    }
+
+    public String getAttributionLinkURL() {
+        return "http://openstreetmap.org/";
+    }
+
+    public String getTermsOfUseURL() {
+        return "http://www.openstreetmap.org/copyright";
+    }
+
+    public double latToTileY(double lat, int zoom) {
+        double l = lat / 180 * Math.PI;
+        double pf = Math.log(Math.tan(l) + (1 / Math.cos(l)));
+        return Math.pow(2.0, zoom - 1) * (Math.PI - pf) / Math.PI;
+    }
+
+    public double lonToTileX(double lon, int zoom) {
+        return Math.pow(2.0, zoom - 3) * (lon + 180.0) / 45.0;
+    }
+
+    public double tileYToLat(int y, int zoom) {
+        return Math.atan(Math.sinh(Math.PI - (Math.PI * y / Math.pow(2.0, zoom - 1)))) * 180 / Math.PI;
+    }
+
+    public double tileXToLon(int x, int zoom) {
+        return x * 45.0 / Math.pow(2.0, zoom - 3) - 180.0;
+    }
+}
Index: /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/BingAerialTileSource.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/BingAerialTileSource.java	(revision 25369)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/BingAerialTileSource.java	(revision 25369)
@@ -0,0 +1,220 @@
+package org.openstreetmap.gui.jmapviewer.tilesources;
+
+import java.awt.Image;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import javax.imageio.ImageIO;
+
+import org.openstreetmap.gui.jmapviewer.Coordinate;
+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 AbstractOsmTileSource {
+    private static String API_KEY = "Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU";
+    private static Future<List<Attribution>> attributions;
+
+    public BingAerialTileSource() {
+        super("Bing Aerial Maps", "http://ecn.t2.tiles.virtualearth.net/tiles/");
+
+        if (attributions == null) {
+            attributions = Executors.newSingleThreadExecutor().submit(new Callable<List<Attribution>>() {
+                public List<Attribution> call() throws Exception {
+                    return loadAttributionText();
+                }
+            });
+        }
+    }
+
+    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");
+            URLConnection conn = u.openConnection();
+
+            // This is not JOSM! Do not use anything other than standard JRE classes within this package!
+            // See package.html for details
+            //conn.setConnectTimeout(Main.pref.getInteger("imagery.bing.load-attribution-text.timeout", 4000));
+
+            InputStream stream = conn.getInputStream();
+
+            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) throws IOException {
+        try {
+            if (attributions.get() == null)
+                throw new IOException("Cannot load Bing attribution");
+            String quadtree = computeQuadTree(zoom, tilex, tiley);
+            return "/tiles/a" + quadtree + "." + getExtension() + "?g=587";
+        } catch (Exception e) {
+            throw new IOException("Cannot load Bing attribution", e);
+        }
+    }
+
+    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) {
+        try {
+            if (!attributions.isDone())
+                return "Loading Bing attribution data...";
+            if (attributions.get() == null)
+                return "Error loading Bing attribution data";
+            StringBuilder a = new StringBuilder();
+            for (Attribution attr : attributions.get()) {
+                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();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return "Error loading Bing attribution data";
+    }
+
+    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/tilesources/OsmTileSource.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/OsmTileSource.java	(revision 25369)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/OsmTileSource.java	(revision 25369)
@@ -0,0 +1,83 @@
+package org.openstreetmap.gui.jmapviewer.tilesources;
+
+public class OsmTileSource {
+
+    public static final String MAP_MAPNIK = "http://tile.openstreetmap.org";
+    public static final String MAP_OSMA = "http://tah.openstreetmap.org/Tiles";
+
+    public static class Mapnik extends AbstractOsmTileSource {
+        public Mapnik() {
+            super("Mapnik", MAP_MAPNIK);
+        }
+
+        public TileUpdate getTileUpdate() {
+            return TileUpdate.IfNoneMatch;
+        }
+
+    }
+
+    public static class CycleMap extends AbstractOsmTileSource {
+
+        private static final String PATTERN = "http://%s.tile.opencyclemap.org/cycle";
+
+        private static final String[] SERVER = { "a", "b", "c" };
+
+        private int SERVER_NUM = 0;
+
+        public CycleMap() {
+            super("OSM Cycle Map", PATTERN);
+        }
+
+        @Override
+        public String getBaseUrl() {
+            String url = String.format(this.baseUrl, new Object[] { SERVER[SERVER_NUM] });
+            SERVER_NUM = (SERVER_NUM + 1) % SERVER.length;
+            return url;
+        }
+
+        @Override
+        public int getMaxZoom() {
+            return 17;
+        }
+
+        public TileUpdate getTileUpdate() {
+            return TileUpdate.LastModified;
+        }
+
+    }
+
+    public static abstract class OsmaSource extends AbstractOsmTileSource {
+        String osmaSuffix;
+
+        public OsmaSource(String name, String osmaSuffix) {
+            super(name, MAP_OSMA);
+            this.osmaSuffix = osmaSuffix;
+        }
+
+        @Override
+        public int getMaxZoom() {
+            return 17;
+        }
+
+        @Override
+        public String getBaseUrl() {
+            return MAP_OSMA + "/" + osmaSuffix;
+        }
+
+        public TileUpdate getTileUpdate() {
+            return TileUpdate.IfModifiedSince;
+        }
+    }
+
+    public static class TilesAtHome extends OsmaSource {
+        public TilesAtHome() {
+            super("TilesAtHome", "tile");
+        }
+    }
+
+    public static class Maplint extends OsmaSource {
+        public Maplint() {
+            super("Maplint", "maplint");
+        }
+    }
+}
Index: /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/ScanexTileSource.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/ScanexTileSource.java	(revision 25369)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/ScanexTileSource.java	(revision 25369)
@@ -0,0 +1,112 @@
+package org.openstreetmap.gui.jmapviewer.tilesources;
+
+import org.openstreetmap.gui.jmapviewer.OsmMercator;
+
+public class ScanexTileSource extends AbstractOsmTileSource {
+    private static String API_KEY = "4018C5A9AECAD8868ED5DEB2E41D09F7";
+
+    private enum ScanexLayer {
+        IRS("irs", "/TileSender.ashx?ModeKey=tile&MapName=F7B8CF651682420FA1749D894C8AD0F6&LayerName=BAC78D764F0443BD9AF93E7A998C9F5B"),
+        SPOT("spot", "/TileSender.ashx?ModeKey=tile&MapName=F7B8CF651682420FA1749D894C8AD0F6&LayerName=F51CE95441284AF6B2FC319B609C7DEC");
+
+        private String name;
+        private String uri;
+
+        ScanexLayer(String name, String uri) {
+            this.name = name;
+            this.uri = uri;
+        }
+        public String getName() {
+            return name;
+        }
+        public String getUri() {
+            return uri;
+        }
+    }
+
+    /* IRS by default */
+    private ScanexLayer Layer = ScanexLayer.IRS;
+
+    public ScanexTileSource(String url) {
+        super("Scanex" + url, "http://maps.kosmosnimki.ru");
+
+        for (ScanexLayer layer : ScanexLayer.values()) {
+            if (url.equalsIgnoreCase(layer.getName())) {
+                this.Layer = layer;
+                break;
+            }
+        }
+    }
+
+    @Override
+    public int getMaxZoom() {
+        return 14;
+    }
+
+    @Override
+    public String getExtension() {
+        return("jpeg");
+    }
+
+    @Override
+    public String getTilePath(int zoom, int tilex, int tiley) {
+        int tmp = (int)Math.pow(2.0, zoom - 1);
+
+        tilex = tilex - tmp;
+        tiley = tmp - tiley - 1;
+
+        return this.Layer.getUri() + "&apikey=" + API_KEY + "&x=" + tilex + "&y=" + tiley + "&z=" + zoom;
+    }
+
+    public TileUpdate getTileUpdate() {
+        return TileUpdate.IfNoneMatch;
+    }
+
+    private static double RADIUS_E = 6378137;	/* radius of Earth at equator, m */
+    private static double EQUATOR = 40075016.68557849; /* equator length, m */
+    private static double E = 0.0818191908426;	/* eccentricity of Earth's ellipsoid */
+
+    @Override
+    public double latToTileY(double lat, int zoom) {
+        double tmp = Math.tan(Math.PI/4 * (1 + lat/90));
+        double pow = Math.pow(Math.tan(Math.PI/4 + Math.asin(E * Math.sin(Math.toRadians(lat)))/2), E);
+
+        return (EQUATOR/2 - (RADIUS_E * Math.log(tmp/pow))) * Math.pow(2.0, zoom) / EQUATOR;
+    }
+
+    @Override
+    public double lonToTileX(double lon, int zoom) {
+        return (RADIUS_E * lon * Math.PI / (90*EQUATOR) + 1) * Math.pow(2.0, zoom - 1);
+    }
+
+    /*
+     * DIRTY HACK ALERT!
+     *
+     * Until I can't solve the equation, use dihotomy :(
+     */
+    @Override
+    public double tileYToLat(int y, int zoom) {
+        double lat = 0;
+        double minl = OsmMercator.MIN_LAT;
+        double maxl = OsmMercator.MAX_LAT;
+        double c;
+
+        for (int i=0; i < 60; i++) {
+            c = latToTileY(lat, zoom);
+            if (c < y) {
+                maxl = lat;
+                lat -= (lat - minl)/2;
+            } else {
+                minl = lat;
+                lat += (maxl - lat)/2;
+            }
+        }
+
+        return lat;
+    }
+
+    @Override
+    public double tileXToLon(int x, int zoom) {
+        return (x / Math.pow(2.0, zoom - 1) - 1) * (90*EQUATOR) / RADIUS_E / Math.PI;
+    }
+}
Index: /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/TMSTileSource.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/TMSTileSource.java	(revision 25369)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/TMSTileSource.java	(revision 25369)
@@ -0,0 +1,20 @@
+package org.openstreetmap.gui.jmapviewer.tilesources;
+
+
+public class TMSTileSource extends 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;
+    }
+
+    public TileUpdate getTileUpdate() {
+        return TileUpdate.IfNoneMatch;
+    }
+}
Index: /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java	(revision 25369)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java	(revision 25369)
@@ -0,0 +1,28 @@
+package org.openstreetmap.gui.jmapviewer.tilesources;
+
+
+public class TemplatedTMSTileSource extends 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.baseUrl
+        .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;
+    }
+
+    public TileUpdate getTileUpdate() {
+        return TileUpdate.IfNoneMatch;
+    }
+}
