Index: trunk/src/org/openstreetmap/josm/actions/ZoomInAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/ZoomInAction.java	(revision 9817)
+++ trunk/src/org/openstreetmap/josm/actions/ZoomInAction.java	(revision 9818)
@@ -47,5 +47,5 @@
     public void actionPerformed(ActionEvent e) {
         if (!Main.isDisplayingMapView()) return;
-        Main.map.mapView.zoomToFactor(1/Math.sqrt(2));
+        Main.map.mapView.zoomIn();
     }
 
Index: trunk/src/org/openstreetmap/josm/actions/ZoomOutAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/ZoomOutAction.java	(revision 9817)
+++ trunk/src/org/openstreetmap/josm/actions/ZoomOutAction.java	(revision 9818)
@@ -33,5 +33,5 @@
     public void actionPerformed(ActionEvent e) {
         if (!Main.isDisplayingMapView()) return;
-        Main.map.mapView.zoomToFactor(Math.sqrt(2));
+        Main.map.mapView.zoomOut();
     }
 
Index: trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java	(revision 9817)
+++ trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java	(revision 9818)
@@ -47,4 +47,6 @@
 import org.openstreetmap.josm.data.projection.Projections;
 import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.layer.NativeScaleLayer.Scale;
+import org.openstreetmap.josm.gui.layer.NativeScaleLayer.ScaleList;
 import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
@@ -286,5 +288,5 @@
     }
 
-    private Collection<Layer> getCapabilities() throws IOException {
+    private Collection<Layer> getCapabilities() {
         XMLInputFactory factory = XMLInputFactory.newFactory();
         // do not try to load external entities, nor validate the XML
@@ -709,8 +711,8 @@
             return null;
         }
-        if (zoom < 1) {
+        if (zoom < 0) {
             return null;
         }
-        return this.currentTileMatrixSet.tileMatrix.get(zoom - 1);
+        return this.currentTileMatrixSet.tileMatrix.get(zoom);
     }
 
@@ -830,5 +832,5 @@
     public int getMaxZoom() {
         if (this.currentTileMatrixSet != null) {
-            return this.currentTileMatrixSet.tileMatrix.size();
+            return this.currentTileMatrixSet.tileMatrix.size()-1;
         }
         return 0;
@@ -911,3 +913,18 @@
         return (int) Math.ceil(Math.abs(max.east() - min.east()) / scale);
     }
+
+    /**
+     * Get native scales of tile source.
+     * @return {@link ScaleList} of native scales
+     */
+    public ScaleList getNativeScales() {
+        ScaleList scales = new ScaleList();
+        if (currentTileMatrixSet != null) {
+            for (TileMatrix tileMatrix : currentTileMatrixSet.tileMatrix) {
+                scales.add(new Scale(tileMatrix.scaleDenominator * 0.28e-03));
+            }
+        }
+        return scales;
+    }
+
 }
Index: trunk/src/org/openstreetmap/josm/data/preferences/DoubleProperty.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/preferences/DoubleProperty.java	(revision 9818)
+++ trunk/src/org/openstreetmap/josm/data/preferences/DoubleProperty.java	(revision 9818)
@@ -0,0 +1,44 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.preferences;
+
+import org.openstreetmap.josm.Main;
+
+/**
+ * A property containing an {@code Double} value.
+ * @since 3246
+ */
+public class DoubleProperty extends AbstractProperty<Double> {
+
+    /**
+     * Constructs a new {@code DoubleProperty}.
+     * @param key The property key
+     * @param defaultValue The default value
+     */
+    public DoubleProperty(String key, double defaultValue) {
+        super(key, defaultValue);
+    }
+
+    @Override
+    public Double get() {
+        return Main.pref.getDouble(getKey(), getDefaultValue());
+    }
+
+    @Override
+    public boolean put(Double value) {
+        return Main.pref.putDouble(getKey(), value);
+    }
+
+    /**
+     * parses and saves a double precision value
+     * @param value the value to be parsed
+     * @return true - preference value has changed
+     *         false - parsing failed or preference value has not changed
+     */
+    public boolean parseAndPut(String value) {
+        try {
+            return put(Double.valueOf(value));
+        } catch (NumberFormatException ex) {
+            return false;
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/MapMover.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/MapMover.java	(revision 9817)
+++ trunk/src/org/openstreetmap/josm/gui/MapMover.java	(revision 9818)
@@ -214,5 +214,5 @@
     @Override
     public void mouseWheelMoved(MouseWheelEvent e) {
-        nc.zoomToFactor(e.getX(), e.getY(), Math.pow(Math.sqrt(2), e.getWheelRotation()));
+        nc.zoomManyTimes(e.getX(), e.getY(), e.getWheelRotation());
     }
 
Index: trunk/src/org/openstreetmap/josm/gui/MapSlider.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/MapSlider.java	(revision 9817)
+++ trunk/src/org/openstreetmap/josm/gui/MapSlider.java	(revision 9818)
@@ -11,14 +11,15 @@
 import javax.swing.event.ChangeListener;
 
-import org.openstreetmap.josm.data.ProjectionBounds;
 import org.openstreetmap.josm.gui.help.Helpful;
 
 class MapSlider extends JSlider implements PropertyChangeListener, ChangeListener, Helpful {
 
+    private static final double zoomStep = 1.1;
     private final MapView mv;
     private boolean preventChange;
+    private int lastValue;
 
     MapSlider(MapView mv) {
-        super(35, 150);
+        super(0, 150);
         setOpaque(false);
         this.mv = mv;
@@ -31,25 +32,9 @@
     @Override
     public void propertyChange(PropertyChangeEvent evt) {
-        if (getModel().getValueIsAdjusting()) return;
-
-        ProjectionBounds world = this.mv.getMaxProjectionBounds();
-        ProjectionBounds current = this.mv.getProjectionBounds();
-
-        double cur_e = current.maxEast-current.minEast;
-        double cur_n = current.maxNorth-current.minNorth;
-        double e = world.maxEast-world.minEast;
-        double n = world.maxNorth-world.minNorth;
-        int zoom = 0;
-
-        while (zoom <= 150) {
-            e /= 1.1;
-            n /= 1.1;
-            if (e < cur_e && n < cur_n) {
-                break;
-            }
-            ++zoom;
-        }
+        double maxScale = this.mv.getMaxScale();
+        int zoom = (int) Math.round(Math.log(maxScale/mv.getScale())/Math.log(zoomStep));
         preventChange = true;
         setValue(zoom);
+        lastValue = zoom;
         preventChange = false;
     }
@@ -59,10 +44,17 @@
         if (preventChange) return;
 
-        ProjectionBounds world = this.mv.getMaxProjectionBounds();
-        double fact = Math.pow(1.1, getValue());
-        double es = world.maxEast-world.minEast;
-        double n = world.maxNorth-world.minNorth;
-
-        this.mv.zoomTo(new ProjectionBounds(this.mv.getCenter(), es/fact, n/fact));
+        if (!getModel().getValueIsAdjusting() && mv.getNativeScaleLayer() != null) {
+            if (getValue() < lastValue) {
+                mv.zoomOut();
+            } else if (getValue() > lastValue) {
+                mv.zoomIn();
+            }
+        } else {
+            double maxScale = this.mv.getMaxScale();
+            double scale = maxScale/Math.pow(zoomStep, getValue());
+            double snapped = mv.scaleFloor(scale);
+            mv.zoomTo(this.mv.getCenter(), snapped);
+        }
+        propertyChange(null);
     }
 
Index: trunk/src/org/openstreetmap/josm/gui/MapView.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/MapView.java	(revision 9817)
+++ trunk/src/org/openstreetmap/josm/gui/MapView.java	(revision 9818)
@@ -60,4 +60,5 @@
 import org.openstreetmap.josm.gui.layer.MapViewPaintable;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.layer.NativeScaleLayer;
 import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer;
 import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
@@ -411,4 +412,8 @@
             if (isOsmDataLayer) {
                 ((OsmDataLayer) layer).addLayerStateChangeListener(this);
+            }
+
+            if (layer instanceof NativeScaleLayer) {
+                Main.map.mapView.setNativeScaleLayer((NativeScaleLayer) layer);
             }
 
@@ -915,5 +920,5 @@
      *
      * @param layer the layer to be activate; must be one of the layers in the list of layers
-     * @throws IllegalArgumentException if layer is not in the lis of layers
+     * @throws IllegalArgumentException if layer is not in the list of layers
      */
     public void setActiveLayer(Layer layer) {
Index: trunk/src/org/openstreetmap/josm/gui/NavigatableComponent.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/NavigatableComponent.java	(revision 9817)
+++ trunk/src/org/openstreetmap/josm/gui/NavigatableComponent.java	(revision 9818)
@@ -45,4 +45,6 @@
 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
 import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
+import org.openstreetmap.josm.data.preferences.BooleanProperty;
+import org.openstreetmap.josm.data.preferences.DoubleProperty;
 import org.openstreetmap.josm.data.preferences.IntegerProperty;
 import org.openstreetmap.josm.data.projection.Projection;
@@ -50,4 +52,7 @@
 import org.openstreetmap.josm.gui.download.DownloadDialog;
 import org.openstreetmap.josm.gui.help.Helpful;
+import org.openstreetmap.josm.gui.layer.NativeScaleLayer;
+import org.openstreetmap.josm.gui.layer.NativeScaleLayer.Scale;
+import org.openstreetmap.josm.gui.layer.NativeScaleLayer.ScaleList;
 import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
 import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
@@ -90,7 +95,14 @@
 
     public static final IntegerProperty PROP_SNAP_DISTANCE = new IntegerProperty("mappaint.node.snap-distance", 10);
+    public static final DoubleProperty PROP_ZOOM_RATIO = new DoubleProperty("zoom.ratio", 2.0);
+    public static final BooleanProperty PROP_ZOOM_INTERMEDIATE_STEPS = new BooleanProperty("zoom.intermediate-steps", true);
 
     public static final String PROPNAME_CENTER = "center";
     public static final String PROPNAME_SCALE  = "scale";
+
+    /**
+     * The layer which scale is set to.
+     */
+    private transient NativeScaleLayer nativeScaleLayer;
 
     /**
@@ -144,4 +156,111 @@
     public NavigatableComponent() {
         setLayout(null);
+        PROP_ZOOM_RATIO.get(); // make sure it is available in preferences
+    }
+
+    /**
+     * Choose a layer that scale will be snap to its native scales.
+     * @param nativeScaleLayer layer to which scale will be snapped
+     */
+    public void setNativeScaleLayer(NativeScaleLayer nativeScaleLayer) {
+        this.nativeScaleLayer = nativeScaleLayer;
+        zoomTo(center, scaleRound(scale));
+        repaint();
+    }
+
+    /**
+     * Replies the layer which scale is set to.
+     * @return the current scale layer (may be null)
+     */
+    public NativeScaleLayer getNativeScaleLayer() {
+        return nativeScaleLayer;
+    }
+
+    /**
+     * Get a new scale that is zoomed in from previous scale
+     * and snapped to selected native scale layer.
+     * @return new scale
+     */
+    public double scaleZoomIn() {
+        return scaleZoomManyTimes(-1);
+    }
+
+    /**
+     * Get a new scale that is zoomed out from previous scale
+     * and snapped to selected native scale layer.
+     * @return new scale
+     */
+    public double scaleZoomOut() {
+        return scaleZoomManyTimes(1);
+    }
+
+    /**
+     * Get a new scale that is zoomed in/out a number of times
+     * from previous scale and snapped to selected native scale layer.
+     * @param times count of zoom operations, negative means zoom in
+     * @return new scale
+     */
+    public double scaleZoomManyTimes(int times) {
+        if (nativeScaleLayer != null) {
+            ScaleList scaleList = nativeScaleLayer.getNativeScales();
+            if (PROP_ZOOM_INTERMEDIATE_STEPS.get()) {
+                scaleList = scaleList.withIntermediateSteps(PROP_ZOOM_RATIO.get());
+            }
+            Scale scale = scaleList.scaleZoomTimes(getScale(), PROP_ZOOM_RATIO.get(), times);
+            return scale.scale;
+        } else {
+            return getScale() * Math.pow(PROP_ZOOM_RATIO.get(), times);
+        }
+    }
+
+    /**
+     * Get a scale snapped to native resolutions, use round method.
+     * It gives nearest step from scale list.
+     * Use round method.
+     * @param scale to snap
+     * @return snapped scale
+     */
+    public double scaleRound(double scale) {
+        return scaleSnap(scale, false);
+    }
+
+    /**
+     * Get a scale snapped to native resolutions.
+     * It gives nearest lower step from scale list, usable to fit objects.
+     * @param scale to snap
+     * @return snapped scale
+     */
+    public double scaleFloor(double scale) {
+        return scaleSnap(scale, true);
+    }
+
+    /**
+     * Get a scale snapped to native resolutions.
+     * It gives nearest lower step from scale list, usable to fit objects.
+     * @param scale to snap
+     * @param floor use floor instead of round, set true when fitting view to objects
+     * @return new scale
+     */
+    public double scaleSnap(double scale, boolean floor) {
+        if (nativeScaleLayer != null) {
+            ScaleList scaleList = nativeScaleLayer.getNativeScales();
+            return scaleList.getSnapScale(scale, PROP_ZOOM_RATIO.get(), floor).scale;
+        } else {
+            return scale;
+        }
+    }
+
+    /**
+     * Zoom in current view. Use configured zoom step and scaling settings.
+     */
+    public void zoomIn() {
+        zoomTo(center, scaleZoomIn());
+    }
+
+    /**
+     * Zoom out current view. Use configured zoom step and scaling settings.
+     */
+    public void zoomOut() {
+        zoomTo(center, scaleZoomOut());
     }
 
@@ -435,4 +554,7 @@
             }
         }
+
+        // snap scale to imagery if needed
+        scale = scaleRound(scale);
 
         if (!newCenter.equals(center) || !Utils.equalsEpsilon(scale, newScale)) {
@@ -517,4 +639,10 @@
     }
 
+    public void zoomManyTimes(double x, double y, int times) {
+        double oldScale = scale;
+        double newScale = scaleZoomManyTimes(times);
+        zoomToFactor(x, y, newScale / oldScale);
+    }
+
     public void zoomToFactor(double x, double y, double factor) {
         double newScale = scale*factor;
@@ -550,4 +678,5 @@
         double newScale = Math.max(scaleX, scaleY);
 
+        newScale = scaleFloor(newScale);
         zoomTo(box.getCenter(), newScale);
     }
@@ -1505,3 +1634,15 @@
         repaint();
     }
+
+    /**
+     * Get a max scale for projection that describes world in 256 pixels
+     * @return max scale
+     */
+    public double getMaxScale() {
+        ProjectionBounds world = getMaxProjectionBounds();
+        return Math.max(
+            world.maxNorth-world.minNorth,
+            world.maxEast-world.minEast
+        )/256;
+    }
 }
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java	(revision 9817)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java	(revision 9818)
@@ -60,4 +60,5 @@
 import org.openstreetmap.josm.gui.layer.Layer;
 import org.openstreetmap.josm.gui.layer.Layer.LayerAction;
+import org.openstreetmap.josm.gui.layer.NativeScaleLayer;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.gui.util.GuiHelper;
@@ -188,11 +189,19 @@
         layerList.getColumnModel().getColumn(0).setPreferredWidth(12);
         layerList.getColumnModel().getColumn(0).setResizable(false);
-        layerList.getColumnModel().getColumn(1).setCellRenderer(new LayerVisibleCellRenderer());
-        layerList.getColumnModel().getColumn(1).setCellEditor(new LayerVisibleCellEditor(new LayerVisibleCheckBox()));
-        layerList.getColumnModel().getColumn(1).setMaxWidth(16);
-        layerList.getColumnModel().getColumn(1).setPreferredWidth(16);
+
+        layerList.getColumnModel().getColumn(1).setCellRenderer(new NativeScaleLayerCellRenderer());
+        layerList.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(new NativeScaleLayerCheckBox()));
+        layerList.getColumnModel().getColumn(1).setMaxWidth(12);
+        layerList.getColumnModel().getColumn(1).setPreferredWidth(12);
         layerList.getColumnModel().getColumn(1).setResizable(false);
-        layerList.getColumnModel().getColumn(2).setCellRenderer(new LayerNameCellRenderer());
-        layerList.getColumnModel().getColumn(2).setCellEditor(new LayerNameCellEditor(new DisableShortcutsOnFocusGainedTextField()));
+
+        layerList.getColumnModel().getColumn(2).setCellRenderer(new LayerVisibleCellRenderer());
+        layerList.getColumnModel().getColumn(2).setCellEditor(new LayerVisibleCellEditor(new LayerVisibleCheckBox()));
+        layerList.getColumnModel().getColumn(2).setMaxWidth(16);
+        layerList.getColumnModel().getColumn(2).setPreferredWidth(16);
+        layerList.getColumnModel().getColumn(2).setResizable(false);
+
+        layerList.getColumnModel().getColumn(3).setCellRenderer(new LayerNameCellRenderer());
+        layerList.getColumnModel().getColumn(3).setCellEditor(new LayerNameCellEditor(new DisableShortcutsOnFocusGainedTextField()));
         // Disable some default JTable shortcuts to use JOSM ones (see #5678, #10458)
         for (KeyStroke ks : new KeyStroke[] {
@@ -1026,4 +1035,14 @@
     }
 
+    private static class NativeScaleLayerCheckBox extends JCheckBox {
+        NativeScaleLayerCheckBox() {
+            setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
+            ImageIcon blank = ImageProvider.get("dialogs/layerlist", "blank");
+            ImageIcon active = ImageProvider.get("dialogs/layerlist", "scale");
+            setIcon(blank);
+            setSelectedIcon(active);
+        }
+    }
+
     private static class ActiveLayerCellRenderer implements TableCellRenderer {
         private final JCheckBox cb;
@@ -1038,5 +1057,5 @@
         @Override
         public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
-            boolean active =  value != null && (Boolean) value;
+            boolean active = value != null && (Boolean) value;
             cb.setSelected(active);
             cb.setToolTipText(active ? tr("this layer is the active layer") : tr("this layer is not currently active (click to activate)"));
@@ -1075,4 +1094,32 @@
         public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
             cb.updateStatus((Layer) value);
+            return cb;
+        }
+    }
+
+    private static class NativeScaleLayerCellRenderer implements TableCellRenderer {
+        private final JCheckBox cb;
+
+        /**
+         * Constructs a new {@code ActiveLayerCellRenderer}.
+         */
+        NativeScaleLayerCellRenderer() {
+            cb = new NativeScaleLayerCheckBox();
+        }
+
+        @Override
+        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
+            Layer layer = (Layer) value;
+            if (layer instanceof NativeScaleLayer) {
+                boolean active = layer != null && layer == Main.map.mapView.getNativeScaleLayer();
+                cb.setSelected(active);
+                cb.setToolTipText(active
+                    ? tr("scale follows native resolution of this layer")
+                    : tr("scale follows native resolution of another layer (click to set this layer)")
+                );
+            } else {
+                cb.setSelected(false);
+                cb.setToolTipText(tr("this layer has no native resolution"));
+            }
             return cb;
         }
@@ -1550,4 +1597,14 @@
         }
 
+        /**
+         * Replies the scale layer. null, if no active layer is available
+         *
+         * @return the scale layer. null, if no active layer is available
+         */
+        protected NativeScaleLayer getNativeScaleLayer() {
+            if (!Main.isDisplayingMapView()) return null;
+            return Main.map.mapView.getNativeScaleLayer();
+        }
+
         /* ------------------------------------------------------------------------------ */
         /* Interface TableModel                                                           */
@@ -1563,5 +1620,5 @@
         @Override
         public int getColumnCount() {
-            return 3;
+            return 4;
         }
 
@@ -1574,4 +1631,5 @@
                 case 1: return layers.get(row);
                 case 2: return layers.get(row);
+                case 3: return layers.get(row);
                 default: throw new RuntimeException();
                 }
@@ -1598,7 +1656,14 @@
                     break;
                 case 1:
+                    if (Main.map.mapView.getNativeScaleLayer() == l) {
+                        Main.map.mapView.setNativeScaleLayer(null);
+                    } else if (l instanceof NativeScaleLayer) {
+                        Main.map.mapView.setNativeScaleLayer((NativeScaleLayer) l);
+                    }
+                    break;
+                case 2:
                     l.setVisible((Boolean) value);
                     break;
-                case 2:
+                case 3:
                     l.rename((String) value);
                     break;
Index: trunk/src/org/openstreetmap/josm/gui/layer/NativeScaleLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/NativeScaleLayer.java	(revision 9818)
+++ trunk/src/org/openstreetmap/josm/gui/layer/NativeScaleLayer.java	(revision 9818)
@@ -0,0 +1,235 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.layer;
+
+import java.util.ArrayList;
+
+import org.openstreetmap.josm.gui.NavigatableComponent;
+
+/**
+ * Represents a layer that has native scales.
+ * @author András Kolesár
+ */
+public interface NativeScaleLayer {
+
+    /**
+     * Get native scales of this layer.
+     * @return {@link ScaleList} of native scales
+     */
+    ScaleList getNativeScales();
+
+    /**
+     * Represents a scale with native flag, used in {@link ScaleList}
+     */
+    class Scale {
+        /**
+         * Scale factor, same unit as in {@link NavigatableComponent}
+         */
+        public double scale;
+
+        /**
+         * True if this scale is native resolution for data source.
+         */
+        public boolean isNative;
+
+        private int index;
+
+        /**
+         * Constructs a new Scale with given scale, native defaults to true.
+         * @param scale as defined in WMTS (scaleDenominator)
+         */
+        public Scale(double scale) {
+            this.scale = scale;
+            this.isNative = true;
+        }
+
+        /**
+         * Constructs a new Scale with given scale and native values.
+         * @param scale as defined in WMTS (scaleDenominator)
+         * @param isNative is this scale native to the source or not
+         */
+        public Scale(double scale, boolean isNative) {
+            this.scale = scale;
+            this.isNative = isNative;
+        }
+
+        /**
+         * Constructs a new Scale with given scale, native and index values.
+         * @param scale as defined in WMTS (scaleDenominator)
+         * @param isNative is this scale native to the source or not
+         * @param index zoom index for this scale
+         */
+        public Scale(double scale, boolean isNative, int index) {
+            this.scale = scale;
+            this.isNative = isNative;
+            this.index = index;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("%f [%s]", scale, isNative);
+        }
+
+        /**
+         * Get index of this scale in a {@link ScaleList}
+         * @return index
+         */
+        public int getIndex() {
+            return index;
+        }
+    }
+
+    /**
+     * List of scales, may include intermediate steps
+     * between native resolutions
+     */
+    class ScaleList extends ArrayList<Scale> {
+
+        /**
+         * Returns a ScaleList that has intermediate steps between native scales.
+         * Native steps are split to equal steps near given ratio.
+         * @param ratio user defined zoom ratio
+         * @return a {@link ScaleList} with intermediate steps
+         */
+        public ScaleList withIntermediateSteps(double ratio) {
+            ScaleList result = new ScaleList();
+            Scale previous = null;
+            for (Scale current: this) {
+                if (previous != null) {
+                    double step = previous.scale / current.scale;
+                    double factor = Math.log(step) / Math.log(ratio);
+                    int steps = (int) Math.round(factor);
+                    double smallStep = Math.pow(step, 1.0/steps);
+                    for (int j = 1; j < steps; j++) {
+                        double intermediate = previous.scale / Math.pow(smallStep, j);
+                        result.add(new Scale(intermediate, false));
+                    }
+                }
+                result.add(current);
+                previous = current;
+            }
+            return result;
+        }
+
+        /**
+         * Get a scale from this ScaleList or a new scale if zoomed outside.
+         * @param scale previous scale
+         * @param ratio zoom ratio from starting from previous scale
+         * @param floor use floor instead of round, set true when fitting view to objects
+         * @return new {@link Scale}
+         */
+        public Scale getSnapScale(double scale, double ratio, boolean floor) {
+            int size = size();
+            Scale first = get(0);
+            Scale last = get(size-1);
+            if (scale > first.scale) {
+                double step = scale / first.scale;
+                double factor = Math.log(step) / Math.log(ratio);
+                int steps = (int) (floor ? Math.floor(factor) : Math.round(factor));
+                if (steps == 0) {
+                    return new Scale(first.scale, first.isNative, steps);
+                } else {
+                    return new Scale(first.scale * Math.pow(ratio, steps), false, steps);
+                }
+            } else if (scale < last.scale) {
+                double step = last.scale / scale;
+                double factor = Math.log(step) / Math.log(ratio);
+                int steps = (int) (floor ? Math.floor(factor) : Math.round(factor));
+                if (steps == 0) {
+                    return new Scale(last.scale, last.isNative, size-1+steps);
+                } else {
+                    return new Scale(last.scale / Math.pow(ratio, steps), false, size-1+steps);
+                }
+            } else {
+                Scale previous = null;
+                for (int i = 0; i < size; i++) {
+                    Scale current = this.get(i);
+                    if (previous != null) {
+                        if (scale <= previous.scale && scale >= current.scale) {
+                            if (floor || previous.scale / scale < scale / current.scale) {
+                                return new Scale(previous.scale, previous.isNative, i-1);
+                            } else {
+                                return new Scale(current.scale, current.isNative, i);
+                            }
+                        }
+                    }
+                    previous = current;
+                }
+                return null;
+            }
+        }
+
+        /**
+         * Get new scale for zoom in/out with a ratio at a number of times.
+         * Used by mousewheel zoom where wheel can step more than one between events.
+         * @param scale previois scale
+         * @param ratio user defined zoom ratio
+         * @param times number of times to zoom
+         * @return new {@link Scale} object from {@link ScaleList} or outside
+         */
+        public Scale scaleZoomTimes(double scale, double ratio, int times) {
+            Scale next = getSnapScale(scale, ratio, false);
+            int abs = Math.abs(times);
+            for (int i = 0; i < abs; i++) {
+                if (times < 0) {
+                    next = getNextIn(next, ratio);
+                } else {
+                    next = getNextOut(next, ratio);
+                }
+            }
+            return next;
+        }
+
+        /**
+         * Get new scale for zoom in.
+         * @param scale previous scale
+         * @param ratio user defined zoom ratio
+         * @return next scale in list or a new scale when zoomed outside
+         */
+        public Scale scaleZoomIn(double scale, double ratio) {
+            Scale snap = getSnapScale(scale, ratio, false);
+            Scale next = getNextIn(snap, ratio);
+            return next;
+        }
+
+        /**
+         * Get new scale for zoom out.
+         * @param scale previous scale
+         * @param ratio user defined zoom ratio
+         * @return next scale in list or a new scale when zoomed outside
+         */
+        public Scale scaleZoomOut(double scale, double ratio) {
+            Scale snap = getSnapScale(scale, ratio, false);
+            Scale next = getNextOut(snap, ratio);
+            return next;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder stringBuilder = new StringBuilder();
+            for (Scale s: this) {
+                stringBuilder.append(s + "\n");
+            }
+            return stringBuilder.toString();
+        }
+
+        private Scale getNextIn(Scale scale, double ratio) {
+            int nextIndex = scale.getIndex() + 1;
+            if (nextIndex <= 0 || nextIndex > size()-1) {
+                return new Scale(scale.scale / ratio, nextIndex == 0, nextIndex);
+            } else {
+                Scale nextScale = get(nextIndex);
+                return new Scale(nextScale.scale, nextScale.isNative, nextIndex);
+            }
+        }
+
+        private Scale getNextOut(Scale scale, double ratio) {
+            int nextIndex = scale.getIndex() - 1;
+            if (nextIndex < 0 || nextIndex >= size()-1) {
+                return new Scale(scale.scale * ratio, nextIndex == size()-1, nextIndex);
+            } else {
+                Scale nextScale = get(nextIndex);
+                return new Scale(nextScale.scale, nextScale.isNative, nextIndex);
+            }
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/layer/TMSLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/TMSLayer.java	(revision 9817)
+++ trunk/src/org/openstreetmap/josm/gui/layer/TMSLayer.java	(revision 9818)
@@ -5,4 +5,5 @@
 
 import org.apache.commons.jcs.access.CacheAccess;
+import org.openstreetmap.gui.jmapviewer.OsmMercator;
 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
 import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
@@ -29,5 +30,5 @@
  *
  */
-public class TMSLayer extends AbstractCachedTileSourceLayer {
+public class TMSLayer extends AbstractCachedTileSourceLayer implements NativeScaleLayer {
     private static final String CACHE_REGION_NAME = "TMS";
 
@@ -145,3 +146,13 @@
         return AbstractCachedTileSourceLayer.getCache(CACHE_REGION_NAME);
     }
+
+    @Override
+    public ScaleList getNativeScales() {
+        ScaleList scales = new ScaleList();
+        for (int zoom = info.getMinZoom(); zoom <= info.getMaxZoom(); zoom++) {
+            double scale = OsmMercator.EARTH_RADIUS * Math.PI * 2 / Math.pow(2, zoom) / OsmMercator.DEFAUL_TILE_SIZE;
+            scales.add(new Scale(scale));
+        }
+        return scales;
+    }
 }
Index: trunk/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java	(revision 9817)
+++ trunk/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java	(revision 9818)
@@ -6,11 +6,8 @@
 
 import org.apache.commons.jcs.access.CacheAccess;
-import org.openstreetmap.gui.jmapviewer.TileXY;
-import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
 import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
-import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.imagery.ImageryInfo;
 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
@@ -19,5 +16,4 @@
 import org.openstreetmap.josm.data.preferences.BooleanProperty;
 import org.openstreetmap.josm.data.projection.Projection;
-import org.openstreetmap.josm.gui.MapView;
 
 /**
@@ -28,8 +24,8 @@
  * http://www.opengeospatial.org/standards/wmts
  *
- * @author Wiktor Niesiobędzki
+ * @author Wiktor NiesiobÄ™dzki
  *
  */
-public class WMTSLayer extends AbstractCachedTileSourceLayer {
+public class WMTSLayer extends AbstractCachedTileSourceLayer implements NativeScaleLayer {
     /**
      * default setting of autozoom per layer
@@ -63,32 +59,25 @@
     }
 
-    /**
-     * @param zoom level of the tile
-     * @return how many pixels of the screen occupies one pixel of the tile
-     */
-    private double getTileToScreenRatio(int zoom) {
-         MapView mv = Main.map.mapView;
-         LatLon topLeft = mv.getLatLon(0, 0);
-         LatLon botLeft = mv.getLatLon(0, tileSource.getTileSize());
-
-         TileXY topLeftTile = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom);
-
-         ICoordinate north = tileSource.tileXYToLatLon(topLeftTile.getXIndex(), topLeftTile.getYIndex(), zoom);
-         ICoordinate south = tileSource.tileXYToLatLon(topLeftTile.getXIndex(), topLeftTile.getYIndex() + 1, zoom);
-
-         return Math.abs((north.getLat() - south.getLat()) / (topLeft.lat() - botLeft.lat()));
+    @Override
+    protected int getBestZoom() {
+        if (!Main.isDisplayingMapView()) return 0;
+        ScaleList scaleList = getNativeScales();
+        for (int i = scaleList.size()-1; i >= 0; i--) {
+            Scale scale = scaleList.get(i);
+            if (scale.scale >= Main.map.mapView.getScale()) {
+                return i;
+            }
+        }
+        return 0;
     }
 
     @Override
-    protected int getBestZoom() {
-        if (!Main.isDisplayingMapView()) return 1;
+    protected int getMaxZoomLvl() {
+        return getNativeScales().size()-1;
+    }
 
-        for (int i = getMinZoomLvl() + 1; i <= getMaxZoomLvl(); i++) {
-            double ret = getTileToScreenRatio(i);
-            if (ret < 1) {
-                return i - 1;
-            }
-        }
-        return getMaxZoomLvl();
+    @Override
+    protected int getMinZoomLvl() {
+        return 0;
     }
 
@@ -130,3 +119,8 @@
         return AbstractCachedTileSourceLayer.getCache(CACHE_REGION_NAME);
     }
+
+    @Override
+    public ScaleList getNativeScales() {
+        return ((WMTSTileSource) tileSource).getNativeScales();
+    }
 }
