diff --git a/images/dialogs/layerlist/scale.png b/images/dialogs/layerlist/scale.png
new file mode 100644
index 0000000..f47f480
Binary files /dev/null and b/images/dialogs/layerlist/scale.png differ
diff --git a/src/org/openstreetmap/gui/jmapviewer/OsmMercator.java b/src/org/openstreetmap/gui/jmapviewer/OsmMercator.java
index fbd1028..7bfdff5 100644
--- a/src/org/openstreetmap/gui/jmapviewer/OsmMercator.java
+++ b/src/org/openstreetmap/gui/jmapviewer/OsmMercator.java
@@ -21,7 +21,7 @@ public class OsmMercator {
     /** minimum latitude (south) for mercator display */
     public static final double MIN_LAT = -85.05112877980659;
     /** equatorial earth radius for EPSG:3857 (Mercator) */
-    private static double EARTH_RADIUS = 6378137;
+    public static final double EARTH_RADIUS = 6378137;
 
     /**
      * instance with tile size of 256 for easy conversions
diff --git a/src/org/openstreetmap/josm/actions/ZoomInAction.java b/src/org/openstreetmap/josm/actions/ZoomInAction.java
index 3b32f49..20853e9 100644
--- a/src/org/openstreetmap/josm/actions/ZoomInAction.java
+++ b/src/org/openstreetmap/josm/actions/ZoomInAction.java
@@ -46,7 +46,7 @@ public final class ZoomInAction extends JosmAction {
     @Override
     public void actionPerformed(ActionEvent e) {
         if (!Main.isDisplayingMapView()) return;
-        Main.map.mapView.zoomToFactor(1/Math.sqrt(2));
+        Main.map.mapView.zoomIn();
     }
 
     @Override
diff --git a/src/org/openstreetmap/josm/actions/ZoomOutAction.java b/src/org/openstreetmap/josm/actions/ZoomOutAction.java
index 33f4a84..98bb1c2 100644
--- a/src/org/openstreetmap/josm/actions/ZoomOutAction.java
+++ b/src/org/openstreetmap/josm/actions/ZoomOutAction.java
@@ -32,7 +32,7 @@ public final class ZoomOutAction extends JosmAction {
     @Override
     public void actionPerformed(ActionEvent e) {
         if (!Main.isDisplayingMapView()) return;
-        Main.map.mapView.zoomToFactor(Math.sqrt(2));
+        Main.map.mapView.zoomOut();
     }
 
     @Override
diff --git a/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java b/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java
index 60217ae..4a66efd 100644
--- a/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java
+++ b/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java
@@ -46,6 +46,8 @@ import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.projection.Projection;
 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;
 import org.openstreetmap.josm.tools.GBC;
@@ -285,7 +287,7 @@ public class WMTSTileSource extends AbstractTMSTileSource implements TemplatedTi
         return output.toString();
     }
 
-    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
         factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
@@ -708,10 +710,10 @@ public class WMTSTileSource extends AbstractTMSTileSource implements TemplatedTi
         if (zoom > getMaxZoom()) {
             return null;
         }
-        if (zoom < 1) {
+        if (zoom < 0) {
             return null;
         }
-        return this.currentTileMatrixSet.tileMatrix.get(zoom - 1);
+        return this.currentTileMatrixSet.tileMatrix.get(zoom);
     }
 
     @Override
@@ -829,7 +831,7 @@ public class WMTSTileSource extends AbstractTMSTileSource implements TemplatedTi
     @Override
     public int getMaxZoom() {
         if (this.currentTileMatrixSet != null) {
-            return this.currentTileMatrixSet.tileMatrix.size();
+            return this.currentTileMatrixSet.tileMatrix.size()-1;
         }
         return 0;
     }
@@ -910,4 +912,19 @@ public class WMTSTileSource extends AbstractTMSTileSource implements TemplatedTi
         EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax());
         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;
+    }
+
 }
diff --git a/src/org/openstreetmap/josm/data/preferences/DoubleProperty.java b/src/org/openstreetmap/josm/data/preferences/DoubleProperty.java
new file mode 100644
index 0000000..44ca4c2
--- /dev/null
+++ b/src/org/openstreetmap/josm/data/preferences/DoubleProperty.java
@@ -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;
+        }
+    }
+}
diff --git a/src/org/openstreetmap/josm/gui/MapMover.java b/src/org/openstreetmap/josm/gui/MapMover.java
index 4de6049..5580884 100644
--- a/src/org/openstreetmap/josm/gui/MapMover.java
+++ b/src/org/openstreetmap/josm/gui/MapMover.java
@@ -213,7 +213,7 @@ public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse
      */
     @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());
     }
 
     /**
diff --git a/src/org/openstreetmap/josm/gui/MapSlider.java b/src/org/openstreetmap/josm/gui/MapSlider.java
index fab953d..f6f2fc1 100644
--- a/src/org/openstreetmap/josm/gui/MapSlider.java
+++ b/src/org/openstreetmap/josm/gui/MapSlider.java
@@ -10,16 +10,17 @@ import javax.swing.JSlider;
 import javax.swing.event.ChangeEvent;
 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;
         mv.addPropertyChangeListener("scale", this);
@@ -30,27 +31,11 @@ class MapSlider extends JSlider implements PropertyChangeListener, ChangeListene
 
     @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;
     }
 
@@ -58,12 +43,19 @@ class MapSlider extends JSlider implements PropertyChangeListener, ChangeListene
     public void stateChanged(ChangeEvent e) {
         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);
     }
 
     @Override
diff --git a/src/org/openstreetmap/josm/gui/MapView.java b/src/org/openstreetmap/josm/gui/MapView.java
index 530856e..83e9b36 100644
--- a/src/org/openstreetmap/josm/gui/MapView.java
+++ b/src/org/openstreetmap/josm/gui/MapView.java
@@ -59,6 +59,7 @@ import org.openstreetmap.josm.gui.layer.ImageryLayer;
 import org.openstreetmap.josm.gui.layer.Layer;
 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;
 import org.openstreetmap.josm.gui.layer.markerlayer.PlayHeadMarker;
@@ -412,6 +413,10 @@ implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.Layer
                 ((OsmDataLayer) layer).addLayerStateChangeListener(this);
             }
 
+            if (layer instanceof NativeScaleLayer) {
+                Main.map.mapView.setNativeScaleLayer((NativeScaleLayer) layer);
+            }
+
             layer.addPropertyChangeListener(this);
             Main.addProjectionChangeListener(layer);
             AudioPlayer.reset();
@@ -914,7 +919,7 @@ implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.Layer
      * of {@link OsmDataLayer} also sets {@link #editLayer} to <code>layer</code>.
      *
      * @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) {
         EnumSet<LayerListenerType> listenersToFire;
diff --git a/src/org/openstreetmap/josm/gui/NavigatableComponent.java b/src/org/openstreetmap/josm/gui/NavigatableComponent.java
index cfdb495..1939de3 100644
--- a/src/org/openstreetmap/josm/gui/NavigatableComponent.java
+++ b/src/org/openstreetmap/josm/gui/NavigatableComponent.java
@@ -44,11 +44,16 @@ import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.osm.WaySegment;
 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;
 import org.openstreetmap.josm.data.projection.Projections;
 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;
 import org.openstreetmap.josm.gui.util.CursorManager;
@@ -89,11 +94,18 @@ public class NavigatableComponent extends JComponent implements Helpful {
     };
 
     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;
+
+    /**
      * the zoom listeners
      */
     private static final CopyOnWriteArrayList<ZoomChangeListener> zoomChangeListeners = new CopyOnWriteArrayList<>();
@@ -143,6 +155,113 @@ public class NavigatableComponent extends JComponent implements Helpful {
      */
     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
+     */
+    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());
     }
 
     protected DataSet getCurrentDataSet() {
@@ -435,6 +554,9 @@ public class NavigatableComponent extends JComponent implements Helpful {
             }
         }
 
+        // snap scale to imagery if needed
+        scale = scaleRound(scale);
+
         if (!newCenter.equals(center) || !Utils.equalsEpsilon(scale, newScale)) {
             if (!initial) {
                 pushZoomUndo(center, scale);
@@ -516,6 +638,12 @@ public class NavigatableComponent extends JComponent implements Helpful {
         }
     }
 
+    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;
         // New center position so that point under the mouse pointer stays the same place as it was before zooming
@@ -549,6 +677,7 @@ public class NavigatableComponent extends JComponent implements Helpful {
         double scaleY = (box.maxNorth-box.minNorth)/h;
         double newScale = Math.max(scaleX, scaleY);
 
+        newScale = scaleFloor(newScale);
         zoomTo(box.getCenter(), newScale);
     }
 
@@ -1504,4 +1633,16 @@ public class NavigatableComponent extends JComponent implements Helpful {
         }
         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;
+    }
 }
diff --git a/src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java b/src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java
index 4223c02..4031c5e 100644
--- a/src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java
+++ b/src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java
@@ -60,6 +60,7 @@ import org.openstreetmap.josm.gui.layer.JumpToMarkerActions;
 import org.openstreetmap.josm.gui.layer.Layer;
 import org.openstreetmap.josm.gui.layer.Layer.LayerAction;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.layer.NativeScaleLayer;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField;
 import org.openstreetmap.josm.gui.widgets.JosmTextField;
@@ -187,13 +188,21 @@ public class LayerListDialog extends ToggleDialog {
         layerList.getColumnModel().getColumn(0).setMaxWidth(12);
         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[] {
                 KeyStroke.getKeyStroke(KeyEvent.VK_C, GuiHelper.getMenuShortcutKeyMaskEx()),
@@ -1025,6 +1034,16 @@ public class LayerListDialog extends ToggleDialog {
         }
     }
 
+    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;
 
@@ -1037,7 +1056,7 @@ public class LayerListDialog extends ToggleDialog {
 
         @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)"));
             return cb;
@@ -1078,6 +1097,34 @@ public class LayerListDialog extends ToggleDialog {
         }
     }
 
+    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;
+        }
+    }
+
     private class LayerNameCellRenderer extends DefaultTableCellRenderer {
 
         protected boolean isActiveLayer(Layer layer) {
@@ -1549,6 +1596,16 @@ public class LayerListDialog extends ToggleDialog {
             return Main.map.mapView.getActiveLayer();
         }
 
+        /**
+         * 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                                                           */
         /* ------------------------------------------------------------------------------ */
@@ -1562,7 +1619,7 @@ public class LayerListDialog extends ToggleDialog {
 
         @Override
         public int getColumnCount() {
-            return 3;
+            return 4;
         }
 
         @Override
@@ -1573,6 +1630,7 @@ public class LayerListDialog extends ToggleDialog {
                 case 0: return layers.get(row) == getActiveLayer();
                 case 1: return layers.get(row);
                 case 2: return layers.get(row);
+                case 3: return layers.get(row);
                 default: throw new RuntimeException();
                 }
             }
@@ -1597,9 +1655,26 @@ public class LayerListDialog extends ToggleDialog {
                     l.setVisible(true);
                     break;
                 case 1:
-                    l.setVisible((Boolean) value);
+                    if (Main.map.mapView.getNativeScaleLayer() == l) {
+                        Main.map.mapView.setNativeScaleLayer(null);
+                    } else if (l instanceof NativeScaleLayer) {
+                        Main.map.mapView.setNativeScaleLayer((NativeScaleLayer) l);
+                        l.setVisible(true);
+                        // set above imagery layers invisible
+                        for (int i=0; i<row; i++) {
+                            Layer above = layers.get(i);
+                            if (above instanceof ImageryLayer &&
+                                above.isVisible() &&
+                                above.getOpacity() == 1) {
+                                    above.setVisible(false);
+                            }
+                        }
+                    }
                     break;
                 case 2:
+                    l.setVisible((Boolean) value);
+                    break;
+                case 3:
                     l.rename((String) value);
                     break;
                 default: throw new RuntimeException();
diff --git a/src/org/openstreetmap/josm/gui/layer/NativeScaleLayer.java b/src/org/openstreetmap/josm/gui/layer/NativeScaleLayer.java
new file mode 100644
index 0000000..e12d2ac
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/layer/NativeScaleLayer.java
@@ -0,0 +1,238 @@
+// 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
+     */
+    public ScaleList getNativeScales();
+
+    /**
+     * Represents a scale with native flag, used in {@link ScaleList}
+     */
+    public static 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
+         */
+        public Scale(double scale) {
+            this.scale = scale;
+            this.isNative = true;
+        }
+
+        /**
+         * Constructs a new Scale with given scale and native values.
+         * @param scale
+         * @param isNative
+         */
+        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
+         * @param isNative
+         * @param index
+         */
+        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
+     */
+    public static 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) {
+            int size = size();
+            ScaleList result = new ScaleList();
+            Scale previous = null;
+            for (int i=0; i<size; i++) {
+                Scale current = this.get(i);
+                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();
+            int size = size();
+            for (int i=0; i<size; i++) {
+                stringBuilder.append(get(i) + "\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);
+            }
+        }
+    }
+}
diff --git a/src/org/openstreetmap/josm/gui/layer/TMSLayer.java b/src/org/openstreetmap/josm/gui/layer/TMSLayer.java
index aad0c67..58561df 100644
--- a/src/org/openstreetmap/josm/gui/layer/TMSLayer.java
+++ b/src/org/openstreetmap/josm/gui/layer/TMSLayer.java
@@ -4,6 +4,7 @@ package org.openstreetmap.josm.gui.layer;
 import static org.openstreetmap.josm.tools.I18n.tr;
 
 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;
 import org.openstreetmap.gui.jmapviewer.tilesources.ScanexTileSource;
@@ -28,7 +29,7 @@ import org.openstreetmap.josm.data.projection.Projection;
  * @author Upliner &lt;upliner@gmail.com&gt;
  *
  */
-public class TMSLayer extends AbstractCachedTileSourceLayer {
+public class TMSLayer extends AbstractCachedTileSourceLayer implements NativeScaleLayer {
     private static final String CACHE_REGION_NAME = "TMS";
 
     private static final String PREFERENCE_PREFIX = "imagery.tms";
@@ -144,4 +145,14 @@ public class TMSLayer extends AbstractCachedTileSourceLayer {
     public static CacheAccess<String, BufferedImageCacheEntry> getCache() {
         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;
+    }
 }
diff --git a/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java b/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java
index ad643e3..5da5d31 100644
--- a/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java
+++ b/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java
@@ -30,7 +30,7 @@ import org.openstreetmap.josm.gui.MapView;
  * @author Wiktor Niesiobędzki
  *
  */
-public class WMTSLayer extends AbstractCachedTileSourceLayer {
+public class WMTSLayer extends AbstractCachedTileSourceLayer implements NativeScaleLayer {
     /**
      * default setting of autozoom per layer
      */
@@ -81,15 +81,25 @@ public class WMTSLayer extends AbstractCachedTileSourceLayer {
 
     @Override
     protected int getBestZoom() {
-        if (!Main.isDisplayingMapView()) return 1;
-
-        for (int i = getMinZoomLvl() + 1; i <= getMaxZoomLvl(); i++) {
-            double ret = getTileToScreenRatio(i);
-            if (ret < 1) {
-                return i - 1;
+        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 getMaxZoomLvl();
+        return 0;
+    }
+
+    @Override
+    protected int getMaxZoomLvl() {
+        return getNativeScales().size()-1;
+    }
+
+    @Override
+    protected int getMinZoomLvl() {
+        return 0;
     }
 
     @Override
@@ -129,4 +139,9 @@ public class WMTSLayer extends AbstractCachedTileSourceLayer {
     public static CacheAccess<String, BufferedImageCacheEntry> getCache() {
         return AbstractCachedTileSourceLayer.getCache(CACHE_REGION_NAME);
     }
+
+    @Override
+    public ScaleList getNativeScales() {
+        return ((WMTSTileSource) tileSource).getNativeScales();
+    }
 }
