Index: trunk/src/org/openstreetmap/josm/gui/layer/TMSLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/TMSLayer.java	(revision 3784)
+++ trunk/src/org/openstreetmap/josm/gui/layer/TMSLayer.java	(revision 3785)
@@ -72,4 +72,5 @@
  * @author LuVar <lubomir.varga@freemap.sk>
  * @author Dave Hansen <dave@sr71.net>
+ * @author Upliner <upliner@gmail.com>
  *
  */
@@ -148,18 +149,7 @@
      */
     public int currentZoomLevel;
-    /**
-     * Optimal TMS Zoomlevel for current mapview.
-     * Works correctly only for Mercatator, so currently used only for initial zoom.
-     */
-    public int bestZoomLevel;
-    /**
-     * Painting zoomlevel. Set to the currentZoomLevel when first tile at this zoomlevel is loaded.
-     */
-    public int displayZoomLevel = 0;
 
     private Tile clickedTile;
     private boolean needRedraw;
-    private boolean overZoomed;
-    private boolean overZoomedFlag;
     private JPopupMenu tileOptionMenu;
     JCheckBoxMenuItem autoZoomPopup;
@@ -266,6 +256,5 @@
         }
 
-        updateBestZoom();
-        currentZoomLevel = bestZoomLevel;
+        currentZoomLevel = getBestZoom();
 
         clearTileCache();
@@ -289,23 +278,32 @@
     }
 
-    private double getPPDeg() {
+    /**
+     * Returns average number of screen pixels per tile pixel for current mapview
+     */
+    private double getScaleFactor(int zoom) {
+        if (Main.map == null || Main.map.mapView == null) return 1;
         MapView mv = Main.map.mapView;
-        return mv.getWidth()/(mv.getLatLon(mv.getWidth(), mv.getHeight()/2).lon()-mv.getLatLon(0, mv.getHeight()/2).lon());
+        LatLon topLeft = mv.getLatLon(0, 0);
+        LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
+        double x1 = lonToTileX(topLeft.lon(), zoom);
+        double y1 = latToTileY(topLeft.lat(), zoom);
+        double x2 = lonToTileX(botRight.lon(), zoom);
+        double y2 = latToTileY(botRight.lat(), zoom);
+
+        int screenPixels = mv.getWidth()*mv.getHeight();
+        double tilePixels = Math.abs((y2-y1)*(x2-x1)*tileSource.getTileSize()*tileSource.getTileSize());
+        if (screenPixels == 0 || tilePixels == 0) return 1;
+        return screenPixels/tilePixels;
     }
 
     private int getBestZoom() {
-        if (Main.map == null || Main.map.mapView == null) return 3;
-        double ret = Math.log(getPPDeg()*360/tileSource.getTileSize())/Math.log(2);
-        return (int)Math.round(ret);
-    }
-
-    private void updateBestZoom() {
-        bestZoomLevel = getBestZoom();
-        if (bestZoomLevel > getMaxZoomLvl()) {
-            bestZoomLevel = getMaxZoomLvl();
-        }
-        if (bestZoomLevel < getMinZoomLvl()) {
-            bestZoomLevel = getMinZoomLvl();
-        }
+        double factor = getScaleFactor(1);
+        double result = Math.log(factor)/Math.log(2)/2+1;
+        int intResult = (int)Math.round(result);
+        if (intResult > getMaxZoomLvl())
+            return getMaxZoomLvl();
+        if (intResult < getMinZoomLvl())
+            return getMinZoomLvl();
+        return intResult;
     }
 
@@ -413,12 +411,5 @@
                     @Override
                     public void actionPerformed(ActionEvent ae) {
-                        if (lastImageScale == null) {
-                            out("please wait for a tile to be loaded before snapping");
-                            return;
-                        }
-                        double new_factor = Math.sqrt(lastImageScale);
-                        if (debug) {
-                            out("tile snap: scale was: " + lastImageScale + ", new factor: " + new_factor);
-                        }
+                        double new_factor = Math.sqrt(getScaleFactor(currentZoomLevel));
                         Main.map.mapView.zoomToFactor(new_factor);
                         redraw();
@@ -517,8 +508,4 @@
     }
 
-    boolean isOverZoomed() {
-        return overZoomed || overZoomedFlag;
-    }
-
     /**
      * Zoom in, go closer to map.
@@ -528,5 +515,5 @@
     public boolean zoomIncreaseAllowed()
     {
-        boolean zia = currentZoomLevel < this.getMaxZoomLvl() && !isOverZoomed();
+        boolean zia = currentZoomLevel < this.getMaxZoomLvl();
         if (debug) {
             out("zoomIncreaseAllowed(): " + zia + " " + currentZoomLevel + " vs. " + this.getMaxZoomLvl() );
@@ -536,5 +523,4 @@
     public boolean increaseZoomLevel()
     {
-        lastImageScale = null;
         if (zoomIncreaseAllowed()) {
             currentZoomLevel++;
@@ -553,8 +539,8 @@
     public boolean setZoomLevel(int zoom)
     {
+        if (zoom == currentZoomLevel) return true;
         if (zoom > this.getMaxZoomLvl()) return false;
         if (zoom < this.getMinZoomLvl()) return false;
         currentZoomLevel = zoom;
-        lastImageScale = null;
         zoomChanged();
         return true;
@@ -572,5 +558,4 @@
     public boolean decreaseZoomLevel() {
         int minZoom = this.getMinZoomLvl();
-        lastImageScale = null;
         if (zoomDecreaseAllowed()) {
             if (debug) {
@@ -689,41 +674,4 @@
     }
 
-    double getImageScaling(Image img, Rectangle r) {
-        int realWidth = -1;
-        int realHeight = -1;
-        if (img != null) {
-            realWidth = img.getHeight(this);
-            realWidth = img.getWidth(this);
-        }
-        if (realWidth == -1 || realHeight == -1) {
-            /*
-             * We need a good image against which to work. If
-             * the current one isn't loaded, then try the last one.
-             * Should be good enough. If we've never seen one, then
-             * guess.
-             */
-            if (lastScaledImage != null)
-                return getImageScaling(lastScaledImage, r);
-            realWidth = 256;
-            realHeight = 256;
-        } else {
-            lastScaledImage = img;
-        }
-        /*
-         * If the zoom scale gets really, really off, these can get into
-         * the millions, so make this a double to prevent integer
-         * overflows.
-         */
-        double drawWidth = r.width;
-        double drawHeight = r.height;
-        // stem.out.println("drawWidth: " + drawWidth + " drawHeight: " +
-        // drawHeight);
-
-        double drawArea = drawWidth * drawHeight;
-        double realArea = realWidth * realHeight;
-
-        return drawArea / realArea;
-    }
-
     LatLon tileLatLon(Tile t)
     {
@@ -733,47 +681,4 @@
     }
 
-    int paintFromOtherZooms(Graphics g, Tile topLeftTile, Tile botRightTile)
-    {
-        LatLon topLeft  = tileLatLon(topLeftTile);
-        LatLon botRight = tileLatLon(botRightTile);
-
-
-        /*
-         * Go looking for tiles in zoom levels *other* than the current
-         * one. Even if they might look bad, they look better than a
-         * blank tile.
-         *
-         * Make darn sure that the tilesCache can either hold all of
-         * these "fake" tiles or that they don't get inserted in it to
-         * begin with.
-         */
-        //int otherZooms[] = {-5, -4, -3, 2, -2, 1, -1};
-        int otherZooms[] = { -1, 1, -2, 2, -3, -4, -5};
-        int painted = 0;
-        debug = true;
-        for (int zoomOff : otherZooms) {
-            int zoom = displayZoomLevel + zoomOff;
-            if ((zoom < this.getMinZoomLvl()) ||
-                    (zoom > this.getMaxZoomLvl())) {
-                continue;
-            }
-            TileSet ts = new TileSet(topLeft, botRight, zoom);
-            int zoom_painted = 0;
-            this.paintTileImages(g, ts, zoom, null);
-            if (debug && zoom_painted > 0) {
-                out("painted " + zoom_painted + "/"+ ts.size() +
-                        " tiles from zoom("+zoomOff+"): " + zoom);
-            }
-            painted += zoom_painted;
-            if (zoom_painted >= ts.size()) {
-                if (debug) {
-                    out("broke after drawing " + zoom_painted + "/"+ ts.size() + " at zoomOff: " + zoomOff);
-                }
-                break;
-            }
-        }
-        debug = false;
-        return painted;
-    }
     Rectangle tileToRect(Tile t1)
     {
@@ -847,5 +752,4 @@
         }
     }
-    Double lastImageScale = null;
     // This function is called for several zoom levels, not just
     // the current one.  It should not trigger any tiles to be
@@ -864,5 +768,4 @@
         }
         List<Tile> missedTiles = new LinkedList<Tile>();
-        boolean imageScaleRecorded = false;
         for (Tile tile : ts.allTiles()) {
             Image img = getLoadedTileImage(tile);
@@ -879,8 +782,4 @@
             }
             drawImageInside(g, img, sourceRect, borderRect);
-            if (!imageScaleRecorded && zoom == displayZoomLevel) {
-                lastImageScale = new Double(getImageScaling(img, sourceRect));
-                imageScaleRecorded = true;
-            }
         }// end of for
         return missedTiles;
@@ -978,4 +877,5 @@
         return new Coordinate(ll.lat(),ll.lon());
     }
+    private final TileSet nullTileSet = new TileSet((LatLon)null, (LatLon)null, 0);
     private class TileSet {
         int x0, x1, y0, y1;
@@ -995,9 +895,11 @@
         TileSet(LatLon topLeft, LatLon botRight, int zoom) {
             this.zoom = zoom;
-
-            x0 = lonToTileX(topLeft.lon(),  zoom);
-            y0 = latToTileY(topLeft.lat(),  zoom);
-            x1 = lonToTileX(botRight.lon(), zoom);
-            y1 = latToTileY(botRight.lat(), zoom);
+            if (zoom == 0)
+                return;
+
+            x0 = (int)lonToTileX(topLeft.lon(),  zoom);
+            y0 = (int)latToTileY(topLeft.lat(),  zoom);
+            x1 = (int)lonToTileX(botRight.lon(), zoom);
+            y1 = (int)latToTileY(botRight.lat(), zoom);
             if (x0 > x1) {
                 int tmp = x0;
@@ -1037,7 +939,7 @@
         }
 
-        double size() {
-            double x_span = x1 - x0 + 1.0;
-            double y_span = y1 - y0 + 1.0;
+        int size() {
+            int x_span = x1 - x0 + 1;
+            int y_span = y1 - y0 + 1;
             return x_span * y_span;
         }
@@ -1053,9 +955,8 @@
         private List<Tile> allTiles(boolean create)
         {
+            // Tileset is either empty or too large
+            if (zoom == 0 || this.insane())
+                return Collections.emptyList();
             List<Tile> ret = new ArrayList<Tile>();
-            // Don't even try to iterate over the set.
-            // Someone created a crazy number of them
-            if (this.insane())
-                return ret;
             for (int x = x0; x <= x1; x++) {
                 for (int y = y0; y <= y1; y++) {
@@ -1074,8 +975,4 @@
         }
 
-        int totalTiles() {
-            return (y1 - y0 + 1) * (x1 - x0 + 1);
-        }
-
         void loadAllTiles(boolean force)
         {
@@ -1096,11 +993,64 @@
     }
 
-    boolean az_disable = false;
-    boolean autoZoomEnabled()
-    {
-        if (az_disable)
-            return false;
-        return autoZoom;
-    }
+
+    private static class TileSetInfo {
+        public boolean hasVisibleTiles = false;
+        public boolean hasOverzoomedTiles = false;
+        public boolean hasLoadingTiles = false;
+    }
+
+    private static TileSetInfo getTileSetInfo(TileSet ts) {
+        List<Tile> allTiles = ts.allTiles();
+        TileSetInfo result = new TileSetInfo();
+        result.hasLoadingTiles = allTiles.size() < ts.size();
+        for (Tile t : allTiles) {
+            if (t.isLoaded()) {
+                if (!t.hasError()) {
+                    result.hasVisibleTiles = true;
+                }
+                if ("no-tile".equals(t.getValue("tile-info"))) {
+                    result.hasOverzoomedTiles = true;
+                }
+            } else {
+                result.hasLoadingTiles = true;
+            }
+        }
+        return result;
+    }
+
+    private class DeepTileSet {
+        final EastNorth topLeft, botRight;
+        final int minZoom;
+        private final TileSet[] tileSets;
+        private final TileSetInfo[] tileSetInfos;
+        public DeepTileSet(EastNorth topLeft, EastNorth botRight, int minZoom, int maxZoom) {
+            this.topLeft = topLeft;
+            this.botRight = botRight;
+            this.minZoom = minZoom;
+            this.tileSets = new TileSet[maxZoom - minZoom + 1];
+            this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];
+        }
+        public TileSet getTileSet(int zoom) {
+            if (zoom < minZoom)
+                return nullTileSet;
+            TileSet ts = tileSets[zoom-minZoom];
+            if (ts == null) {
+                ts = new TileSet(topLeft, botRight, zoom);
+                tileSets[zoom-minZoom] = ts;
+            }
+            return ts;
+        }
+        public TileSetInfo getTileSetInfo(int zoom) {
+            if (zoom < minZoom)
+                return new TileSetInfo();
+            TileSetInfo tsi = tileSetInfos[zoom-minZoom];
+            if (tsi == null) {
+                tsi = TMSLayer.getTileSetInfo(getTileSet(zoom));
+                tileSetInfos[zoom-minZoom] = tsi;
+            }
+            return tsi;
+        }
+    }
+
     /**
      */
@@ -1120,80 +1070,52 @@
 
         int zoom = currentZoomLevel;
-        TileSet ts = new TileSet(topLeft, botRight, zoom);
-
-        if (autoZoomEnabled()) {
-            if (zoomDecreaseAllowed() && ts.tooLarge()) {
-                if (debug) {
-                    out("too many tiles, decreasing zoom from " + currentZoomLevel);
-                }
-                if (decreaseZoomLevel()) {
-                    this.paint(g, mv, bounds);
-                    return;
-                }
-            }
-
-            // Auto-detection of Bing zoomlevel
-            if (tileSource instanceof BingAerialTileSource) {
-                List<Tile> allTiles = ts.allTiles();
-                boolean hasVisibleTiles = false;
-                boolean hasOverzoomedTiles = false;
-                boolean hasLoadingTiles = allTiles.size() < ts.totalTiles();
-                for (Tile t : allTiles) {
-                    if (t.isLoaded()) {
-                        if (!t.hasError()) {
-                            hasVisibleTiles = true;
-                        }
-                        if ("no-tile".equals(t.getValue("tile-info"))) {
-                            hasOverzoomedTiles = true;
-                        }
-                    } else {
-                        hasLoadingTiles = true;
-                    }
-                }
-                if (!hasLoadingTiles) {
-                    overZoomed = false;
-                }
-                if (!hasVisibleTiles && hasOverzoomedTiles) {
-                    overZoomed = true;
-                    if (displayZoomLevel == 0 || !hasLoadingTiles) {
-                        boolean tmp = overZoomedFlag;
-                        overZoomedFlag = true;
-                        if (decreaseZoomLevel()) {
-                            this.paint(g, mv, bounds);
-                            overZoomedFlag = tmp;
-                            return;
-                        }
-                        overZoomedFlag = tmp;
-                    }
-                } else if (hasVisibleTiles) {
-                    displayZoomLevel = currentZoomLevel;
-                }
-            } else {
-                displayZoomLevel = currentZoomLevel;
-                if (zoomIncreaseAllowed() && ts.tooSmall()) {
-                    if (debug) {
-                        out("too zoomed in, (" + ts.tilesSpanned()
-                                + "), increasing zoom from " + currentZoomLevel);
-                    }
-                    // This is a hack.  ts.tooSmall() is proabably a bad thing, and this works
-                    // around it.  If we have a very small window, the tileSet may be well
-                    // less than 1 real tile wide, but that's expected.  But, this sees the
-                    // tile set as too small and zooms in.  The code below that checks for
-                    // pixel stretching disagrees and tries to zoom out.  Both calls recurse,
-                    // hillarity ensues, and the stack overflows.
-                    //
-                    // This really needs to get fixed properly.  We probably shouldn't even
-                    // have the tooSmall() check on tileSets.  But, this also helps the zoom
-                    // converge to the correct place much faster.
-                    boolean tmp = az_disable;
-                    az_disable = true;
-                    increaseZoomLevel();
-                    this.paint(g, mv, bounds);
-                    az_disable = tmp;
-                    return;
-                }
-            }
-        } else {
-            displayZoomLevel = currentZoomLevel;
+        if (autoZoom) {
+            double pixelScaling = getScaleFactor(zoom);
+            if (pixelScaling > 3 || pixelScaling < 0.45) {
+                zoom = getBestZoom();
+            }
+        }
+
+        DeepTileSet dts = new DeepTileSet(topLeft, botRight, getMinZoomLvl(), getMaxZoomLvl());
+        TileSet ts = dts.getTileSet(zoom);
+
+        int displayZoomLevel = zoom;
+
+        boolean noTilesAtZoom = false;
+        if (autoZoom && autoLoad) {
+            // Auto-detection of tilesource maxzoom (currently fully works only for Bing)
+            TileSetInfo tsi = dts.getTileSetInfo(zoom);
+            if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) {
+                noTilesAtZoom = true;
+            }
+            if (!tsi.hasVisibleTiles && tsi.hasOverzoomedTiles) {
+                while (displayZoomLevel > dts.minZoom && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles){
+                    displayZoomLevel--;
+                    tsi = dts.getTileSetInfo(displayZoomLevel);
+                }
+                if (zoom > displayZoomLevel && !dts.getTileSetInfo(displayZoomLevel+1).hasLoadingTiles) {
+                    zoom = displayZoomLevel+1;
+                } else {
+                    zoom = displayZoomLevel;
+                }
+                while (displayZoomLevel >= dts.minZoom && !tsi.hasVisibleTiles){
+                    displayZoomLevel--;
+                    tsi = dts.getTileSetInfo(displayZoomLevel);
+                }
+                if (displayZoomLevel < dts.minZoom) {
+                    displayZoomLevel = 0;
+                }
+                tsi = dts.getTileSetInfo(zoom);
+            }
+            setZoomLevel(zoom);
+            // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
+            // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
+            while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) {
+                zoom--;
+                tsi = dts.getTileSetInfo(zoom);
+            }
+            ts = dts.getTileSet(zoom);
+        } else if (autoZoom) {
+            setZoomLevel(zoom);
         }
 
@@ -1205,5 +1127,5 @@
 
         if (displayZoomLevel != zoom) {
-            ts = new TileSet(topLeft, botRight, displayZoomLevel);
+            ts = dts.getTileSet(displayZoomLevel);
         }
 
@@ -1213,5 +1135,5 @@
         int otherZooms[] = { -1, 1, -2, 2, -3, -4, -5};
         for (int zoomOffset : otherZooms) {
-            if (!autoZoomEnabled()) {
+            if (!autoZoom) {
                 break;
             }
@@ -1225,4 +1147,9 @@
             List<Tile> newlyMissedTiles = new LinkedList<Tile>();
             for (Tile missed : missedTiles) {
+                if ("no-tile".equals(missed.getValue("tile-info")) && zoomOffset > 0) {
+                    // Don't try to paint from higher zoom levels when tile is overzoomed
+                    newlyMissedTiles.add(missed);
+                    continue;
+                }
                 Tile t2 = tempCornerTile(missed);
                 LatLon topLeft2  = tileLatLon(missed);
@@ -1289,23 +1216,4 @@
         }
 
-        if (autoZoomEnabled() && lastImageScale != null) {
-            // If each source image pixel is being stretched into > 3
-            // drawn pixels, zoom in... getting too pixelated
-            if (lastImageScale > 3 && zoomIncreaseAllowed()) {
-                if (debug) {
-                    out("autozoom increase: scale: " + lastImageScale);
-                }
-                increaseZoomLevel();
-                this.paint(g, mv, bounds);
-                // If each source image pixel is being squished into > 0.32
-                // of a drawn pixels, zoom out.
-            } else if ((lastImageScale < 0.45) && (lastImageScale > 0) && zoomDecreaseAllowed()) {
-                if (debug) {
-                    out("autozoom decrease: scale: " + lastImageScale);
-                }
-                decreaseZoomLevel();
-                this.paint(g, mv, bounds);
-            }
-        }
         //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120);
         g.setColor(Color.lightGray);
@@ -1319,5 +1227,5 @@
             }
         }
-        if (isOverZoomed()) {
+        if (noTilesAtZoom) {
             myDrawString(g, tr("No tiles at this zoom level"), 120, 120);
         }
@@ -1325,4 +1233,6 @@
             myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);
             myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);
+            myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
+            myDrawString(g, tr("Best zoom: {0}", Math.log(getScaleFactor(1))/Math.log(2)/2+1), 50, 185);
         }
     }// end of paint method
@@ -1363,5 +1273,5 @@
             return null;
         System.out.println("clicked on tile: " + clickedTile.getXtile() + " " + clickedTile.getYtile() +
-                " scale: " + lastImageScale + " currentZoomLevel: " + currentZoomLevel);
+                " currentZoomLevel: " + currentZoomLevel);
         return clickedTile;
     }
@@ -1394,15 +1304,15 @@
     }
 
-    private int latToTileY(double lat, int zoom) {
+    private static double latToTileY(double lat, int zoom) {
         double l = lat / 180 * Math.PI;
         double pf = Math.log(Math.tan(l) + (1 / Math.cos(l)));
-        return (int) (Math.pow(2.0, zoom - 1) * (Math.PI - pf) / Math.PI);
-    }
-
-    private int lonToTileX(double lon, int zoom) {
-        return (int) (Math.pow(2.0, zoom - 3) * (lon + 180.0) / 45.0);
-    }
-
-    private double tileYToLat(int y, int zoom) {
+        return Math.pow(2.0, zoom - 1) * (Math.PI - pf) / Math.PI;
+    }
+
+    private static double lonToTileX(double lon, int zoom) {
+        return Math.pow(2.0, zoom - 3) * (lon + 180.0) / 45.0;
+    }
+
+    private static double tileYToLat(int y, int zoom) {
         return Math.atan(Math.sinh(Math.PI
                 - (Math.PI * y / Math.pow(2.0, zoom - 1))))
@@ -1410,5 +1320,5 @@
     }
 
-    private double tileXToLon(int x, int zoom) {
+    private static double tileXToLon(int x, int zoom) {
         return x * 45.0 / Math.pow(2.0, zoom - 3) - 180.0;
     }
