CINXE.COM

Zoom levels - Leaflet - a JavaScript library for interactive maps

<!DOCTYPE html> <html lang="en"> <head> <title>Zoom levels - Leaflet - a JavaScript library for interactive maps</title> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="shortcut icon" type="image/x-icon" href="../../docs/images/favicon.ico" /> <link href="https://leafletjs.com/atom.xml" type="application/atom+xml" rel="alternate" title="Leaflet Dev Blog Atom Feed" /> <link rel="stylesheet" href="../../docs/css/normalize.css" /> <link rel="stylesheet" href="../../docs/css/main.css" /> <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,400italic,700,300' rel='stylesheet' type='text/css'> <script src="../../docs/highlight/highlight.pack.js"></script> <link rel="stylesheet" href="../../docs/highlight/styles/github-gist.css" /> <!-- Leaflet --> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script> <script> ACCESS_TOKEN = 'pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw'; MB_ATTR = 'Map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, ' + 'Imagery © <a href="https://www.mapbox.com/">Mapbox</a>'; MB_URL = 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token=' + ACCESS_TOKEN; OSM_URL = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; OSM_ATTRIB = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'; </script> </head> <body> <header> <h1><a href="https://leafletjs.com/"><img src="../../docs/images/logo.png" alt="Leaflet" width="300" /></a></h1> <p class="tagline">an open-source JavaScript library<br> for mobile-friendly interactive maps</p> </header> <nav> <ul class="nav"> <li> <a href="../../index.html">Overview</a> </li> <li> <a href="../../examples.html">Tutorials</a> </li> <li> <a href="../../reference.html">Docs</a> </li> <li> <a href="../../download.html">Download</a> </li> <li> <a href="../../plugins.html">Plugins</a> </li> <li> <a href="../../blog.html">Blog</a> </li> </ul> </nav> <main> <div class="container"> <p class="tutorials-back"><a href="../../examples.html">&larr; Tutorials</a></p> <style> .tiles img { border: 1px solid #ccc; border-radius: 5px; margin: 5px; } .tiles.small img { border: 1px solid #ccc; border-radius: 5px; margin: 1px; width: 64px; height: 64px; } .tiles { line-height: 0; } .tiles.legend { line-height: 1; } </style> <h2 id="zoom-levels">Zoom levels</h2> <p>Leaflet works with <a href="https://en.wikipedia.org/wiki/Latitude">latitude</a>, <a href="https://en.wikipedia.org/wiki/Longitude">longitude</a> and &#8220;zoom level&#8221;.</p> <p>Lower zoom levels means that the map shows entire continents, while higher zoom levels means that the map can show details of a city.</p> <p>To understand how zoom levels work, first we need a basic introduction to <i>geodesy</i>.</p> <h2 id="the-shape-of-the-earth">The shape of the earth</h2> <p>Let&#8217;s have a look at a simple map locked at zoom zero:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>var map = L.map('map', { minZoom: 0, maxZoom: 0 }); var cartodbAttribution = '&amp;copy; &lt;a href="https://www.openstreetmap.org/copyright"&gt;OpenStreetMap&lt;/a&gt; contributors, &amp;copy; &lt;a href="https://carto.com/attribution"&gt;CARTO&lt;/a&gt;'; var positron = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', { attribution: cartodbAttribution }).addTo(map); map.setView([0, 0], 0); </code></pre></div></div> <table role="presentation"> <tr><td style="text-align: center; border: none; padding: 0;"> <iframe src="example-zero.html" width="600" height="400" style="max-width: 100%; max-height: 90vh; box-sizing: border-box;"></iframe> </td></tr> <tr><td style="text-align: center; border: none"> <small><a href="example-zero.html">See this example stand-alone.</a></small> </td></tr></table> <p>Notice that the &#8220;whole earth&#8221; is just one image, 256 pixels wide and 256 pixels high:</p> <div class="tiles" style="text-align: center"> <img src="https://a.basemaps.cartocdn.com/light_all/0/0/0.png" class="bordered-img" alt="" /> </div> <p>Just to be clear: the earth is not a square. Rather, the earth has an irregular shape that can be approximated to <a href="https://en.wikipedia.org/wiki/Geoid">something similar to a sphere</a>.</p> <p>So we <em>assume</em> that the earth is mostly round. To make it flat, we put an imaginary cylinder around, unroll it, and cut it so it looks square:</p> <div class="tiles legend" style="text-align: center"> <a title="By derived from US Government USGS [Public domain], via Wikimedia Commons" href="https://en.wikipedia.org/wiki/Map_projection#Cylindrical"><img width="512" alt="Usgs map mercator" src="https://upload.wikimedia.org/wikipedia/commons/thumb/6/62/Usgs_map_mercator.svg/512px-Usgs_map_mercator.svg.png" /> <br /> This is called a "cylindrical map projection". </a> </div> <p>This is not the only way of displaying the surface on the earth on a plane. There are <a href="https://en.wikipedia.org/wiki/Map_projection">hundreds of ways</a>, each of them with its own advantages and disadvantages. The following 6-minute video is a nice introduction to the topic:</p> <center><iframe width="696" height="392" src="https://www.youtube.com/embed/kIID5FDi2JQ" frameborder="0" allowfullscreen=""></iframe></center> <p>Things like geodesy, map projections and coordinate systems are hard, <em>very hard</em> (and out of scope for this tutorial). Assuming that the earth is a square is not always the right thing to do, but most of the time works fine enough, makes things simpler, and allows Leaflet (and other map libraries) to be fast.</p> <h2 id="powers-of-two">Powers of two</h2> <p>For now, let&#8217;s just <strong><em>assume</em></strong> that the world is a square:</p> <div class="tiles" style="text-align: center"> <img src="https://a.basemaps.cartocdn.com/light_all/0/0/0.png" class="bordered-img" alt="" /> </div> <p>When we represent the world at zoom level <strong>zero</strong>, it&#8217;s 256 pixels wide and high. When we go into zoom level <strong>one</strong>, it doubles its width and height, and can be represented by four 256-pixel-by-256-pixel images:</p> <div class="tiles" style="text-align: center"> <div> <img src="https://a.basemaps.cartocdn.com/light_all/1/0/0.png" class="bordered-img" alt="" /><img src="https://a.basemaps.cartocdn.com/light_all/1/1/0.png" class="bordered-img" alt="" /> </div> <div> <img src="https://a.basemaps.cartocdn.com/light_all/1/0/1.png" class="bordered-img" alt="" /><img src="https://a.basemaps.cartocdn.com/light_all/1/1/1.png" class="bordered-img" alt="" /> </div> </div> <p>At each zoom level, each tile is divided in four, and its size (length of the edge, given by the <code class="language-plaintext highlighter-rouge">tileSize</code> option) doubles, quadrupling the area. (in other words, the width and height of the world is <code>256·2<sup>zoomlevel</sup></code> pixels):</p> <table><tr><td> <div class="tiles small" style="text-align: center"> <img src="https://a.basemaps.cartocdn.com/light_all/0/0/0.png" class="bordered-img" alt="" /> </div> </td><td> <div class="tiles small" style="text-align: center"> <div> <img src="https://a.basemaps.cartocdn.com/light_all/1/0/0.png" class="bordered-img" alt="" /><img src="https://a.basemaps.cartocdn.com/light_all/1/1/0.png" class="bordered-img" alt="" /> </div> <div> <img src="https://a.basemaps.cartocdn.com/light_all/1/0/1.png" class="bordered-img" alt="" /><img src="https://a.basemaps.cartocdn.com/light_all/1/1/1.png" class="bordered-img" alt="" /> </div> </div> </td><td> <div class="tiles small" style="text-align: center"> <div> <img src="https://a.basemaps.cartocdn.com/light_all/2/0/0.png" class="bordered-img" alt="" /><img src="https://a.basemaps.cartocdn.com/light_all/2/1/0.png" class="bordered-img" alt="" /><img src="https://a.basemaps.cartocdn.com/light_all/2/2/0.png" class="bordered-img" alt="" /><img src="https://a.basemaps.cartocdn.com/light_all/2/3/0.png" class="bordered-img" alt="" /> </div> <div> <img src="https://a.basemaps.cartocdn.com/light_all/2/0/1.png" class="bordered-img" alt="" /><img src="https://a.basemaps.cartocdn.com/light_all/2/1/1.png" class="bordered-img" alt="" /><img src="https://a.basemaps.cartocdn.com/light_all/2/2/1.png" class="bordered-img" alt="" /><img src="https://a.basemaps.cartocdn.com/light_all/2/3/1.png" class="bordered-img" alt="" /> </div> <div> <img src="https://a.basemaps.cartocdn.com/light_all/2/0/2.png" class="bordered-img" alt="" /><img src="https://a.basemaps.cartocdn.com/light_all/2/1/2.png" class="bordered-img" alt="" /><img src="https://a.basemaps.cartocdn.com/light_all/2/2/2.png" class="bordered-img" alt="" /><img src="https://a.basemaps.cartocdn.com/light_all/2/3/2.png" class="bordered-img" alt="" /> </div> <div> <img src="https://a.basemaps.cartocdn.com/light_all/2/0/3.png" class="bordered-img" alt="" /><img src="https://a.basemaps.cartocdn.com/light_all/2/1/3.png" class="bordered-img" alt="" /><img src="https://a.basemaps.cartocdn.com/light_all/2/2/3.png" class="bordered-img" alt="" /><img src="https://a.basemaps.cartocdn.com/light_all/2/3/3.png" class="bordered-img" alt="" /> </div> </div> </td></tr> <tr><td>Zoom 0</td><td>Zoom 1</td><td>Zoom 2</td></tr></table> <p>This goes on and on. Most tile services offer tiles up to zoom level 18, depending on their coverage. This is enough to see a few city blocks per tile.</p> <h2 id="a-note-about-scale">A note about scale</h2> <p>One of the disadvantages of using a cylindrical projection is that the scale is not constant, and measuring distances or sizes is not reliable, specially at low zoom levels.</p> <p>In <a href="https://en.wikipedia.org/wiki/Map_projection#Projections_by_preservation_of_a_metric_property">technical terms</a>, the cylindrical projection that Leaflet uses is <i>conformal</i> (preserves shapes), but not <i>equidistant</i> (does not preserve distances), and not <i>equal-area</i> (does not preserve areas, as things near the equator appear smaller than they are).</p> <p>By adding a <code class="language-plaintext highlighter-rouge">L.Control.Scale</code> to a map, and panning to the equator and to 60° north, we can see how the scale factor <b>doubles</b>. The following example uses <a href="https://developer.mozilla.org/docs/Web/API/WindowTimers/setTimeout">javascript timeouts</a> to do this automatically:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>L.control.scale().addTo(map); setInterval(function(){ map.setView([0, 0]); setTimeout(function(){ map.setView([60, 0]); }, 2000); }, 4000); </code></pre></div></div> <table role="presentation"> <tr><td style="text-align: center; border: none; padding: 0;"> <iframe src="example-scale.html" width="600" height="400" style="max-width: 100%; max-height: 90vh; box-sizing: border-box;"></iframe> </td></tr> <tr><td style="text-align: center; border: none"> <small><a href="example-scale.html">See this example stand-alone.</a></small> </td></tr></table> <p><code class="language-plaintext highlighter-rouge">L.Control.Scale</code> shows the scale which applies to the center point of the map. At high zoom levels, the scale changes very little, and is not noticeable.</p> <h2 id="controlling-the-zoom">Controlling the zoom</h2> <p>A leaflet map has several ways to control the zoom level shown, but the most obvious one is <a href="/reference.html#map-setzoom"><code class="language-plaintext highlighter-rouge">setZoom()</code></a>. For example, <code class="language-plaintext highlighter-rouge">map.setZoom(0);</code> will set the zoom level of <code class="language-plaintext highlighter-rouge">map</code> to <code class="language-plaintext highlighter-rouge">0</code>.</p> <p>This example again uses timeouts to alternate between zoom levels <code class="language-plaintext highlighter-rouge">0</code> and <code class="language-plaintext highlighter-rouge">1</code> automatically:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>setInterval(function(){ map.setZoom(0); setTimeout(function(){ map.setZoom(1); }, 2000); }, 4000); </code></pre></div></div> <table role="presentation"> <tr><td style="text-align: center; border: none; padding: 0;"> <iframe src="example-setzoom.html" width="600" height="400" style="max-width: 100%; max-height: 90vh; box-sizing: border-box;"></iframe> </td></tr> <tr><td style="text-align: center; border: none"> <small><a href="example-setzoom.html">See this example stand-alone.</a></small> </td></tr></table> <p>Notice how the images shown at zoom levels 0 and one correspond with the images shown in the previous section!</p> <p>Other ways of setting the zoom are:</p> <ul> <li><a href="/reference.html#map-setview"><code class="language-plaintext highlighter-rouge">setView(center, zoom)</code></a>, which also sets the map center</li> <li><a href="/reference.html#map-flyto"><code class="language-plaintext highlighter-rouge">flyTo(center, zoom)</code></a>, like <code class="language-plaintext highlighter-rouge">setView</code> but with a smooth animation</li> <li><a href="/reference.html#map-zoomin"><code class="language-plaintext highlighter-rouge">zoomIn()</code> / <code class="language-plaintext highlighter-rouge">zoomIn(delta)</code></a>, zooms in <code class="language-plaintext highlighter-rouge">delta</code> zoom levels, <code class="language-plaintext highlighter-rouge">1</code> by default</li> <li><a href="/reference.html#map-zoomout"><code class="language-plaintext highlighter-rouge">zoomOut()</code> / <code class="language-plaintext highlighter-rouge">zoomOut(delta)</code></a>, zooms out <code class="language-plaintext highlighter-rouge">delta</code> zoom levels, <code class="language-plaintext highlighter-rouge">1</code> by default</li> <li><a href="/reference.html#map-setzoomaround"><code class="language-plaintext highlighter-rouge">setZoomAround(fixedPoint, zoom)</code></a>, sets the zoom level while keeping a point fixed (what scrollwheel zooming does)</li> <li><a href="/reference.html#map-fitbounds"><code class="language-plaintext highlighter-rouge">fitBounds(bounds)</code></a>, automatically calculates the zoom to fit a rectangular area on the map</li> </ul> <h2 id="fractional-zoom">Fractional zoom</h2> <p>A feature introduced in Leaflet 1.0.0 was the concept of <em>fractional zoom</em>. Before this, the zoom level of the map could be only an integer number (<code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">1</code>, <code class="language-plaintext highlighter-rouge">2</code>, and so on); but now you can use fractional numbers like <code class="language-plaintext highlighter-rouge">1.5</code> or <code class="language-plaintext highlighter-rouge">1.25</code>.</p> <p>Fractional zoom is disabled by default. To enable it, use the <a href="/reference.html#map-zoomsnap">map&#8217;s <code class="language-plaintext highlighter-rouge">zoomSnap</code> option</a>. The <code class="language-plaintext highlighter-rouge">zoomSnap</code> option has a default value of <code class="language-plaintext highlighter-rouge">1</code> (which means that the zoom level of the map can be <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">1</code>, <code class="language-plaintext highlighter-rouge">2</code>, and so on).</p> <p>If you set the value of <code class="language-plaintext highlighter-rouge">zoomSnap</code> to <code class="language-plaintext highlighter-rouge">0.5</code>, the valid zoom levels of the map will be <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">0.5</code>, <code class="language-plaintext highlighter-rouge">1</code>, <code class="language-plaintext highlighter-rouge">1.5</code>, <code class="language-plaintext highlighter-rouge">2</code>, and so on.</p> <p>If you set a value of <code class="language-plaintext highlighter-rouge">0.1</code>, the valid zoom levels of the map will be <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">0.1</code>, <code class="language-plaintext highlighter-rouge">0.2</code>, <code class="language-plaintext highlighter-rouge">0.3</code>, <code class="language-plaintext highlighter-rouge">0.4</code>, and so on.</p> <p>The following example uses a <code class="language-plaintext highlighter-rouge">zoomSnap</code> value of <code class="language-plaintext highlighter-rouge">0.25</code>:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>var map = L.map('map', { zoomSnap: 0.25 }); </code></pre></div></div> <table role="presentation"> <tr><td style="text-align: center; border: none; padding: 0;"> <iframe src="example-fractional.html" width="600" height="400" style="max-width: 100%; max-height: 90vh; box-sizing: border-box;"></iframe> </td></tr> <tr><td style="text-align: center; border: none"> <small><a href="example-fractional.html">See this example stand-alone.</a></small> </td></tr></table> <p>As you can see, Leaflet will only load the tiles for zoom levels <code class="language-plaintext highlighter-rouge">0</code> or <code class="language-plaintext highlighter-rouge">1</code>, and will scale them as needed.</p> <p>Leaflet will <em>snap</em> the zoom level to the closest valid one. For example, if you have <code class="language-plaintext highlighter-rouge">zoomSnap: 0.25</code> and you try to do <code class="language-plaintext highlighter-rouge">map.setZoom(0.8)</code>, the zoom will snap back to <code class="language-plaintext highlighter-rouge">0.75</code>. The same happens with <code class="language-plaintext highlighter-rouge">map.fitBounds(bounds)</code>, or when ending a pinch-zoom gesture on a touchscreen.</p> <p><code class="language-plaintext highlighter-rouge">zoomSnap</code> can be set to zero. This means that Leaflet will <strong>not</strong> snap the zoom level.</p> <p>There is another important map option related to <code class="language-plaintext highlighter-rouge">zoomSnap</code>: <a href="/reference.html#map-zoomdelta">the <code class="language-plaintext highlighter-rouge">zoomDelta</code> option</a>. This controls how many zoom levels to zoom in/out when using the zoom buttons (from the default <a href="/reference.html#control-zoom"><code class="language-plaintext highlighter-rouge">L.Control.Zoom</code></a>) or the <code class="language-plaintext highlighter-rouge">+</code>/<code class="language-plaintext highlighter-rouge">-</code> keys in your keyboard.</p> <p>For the mousewheel zoom, the <a href="/reference.html#map-wheelpxperzoomlevel"><code class="language-plaintext highlighter-rouge">wheelPxPerZoomLevel</code></a> option controls how fast the mousewheel zooms in or out.</p> <p>Here is an example with <code class="language-plaintext highlighter-rouge">zoomSnap</code> set to zero:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>var map = L.map('map', { zoomDelta: 0.25, zoomSnap: 0 }); </code></pre></div></div> <p>Try the following, and see how the zoom level changes:</p> <ul> <li>Pinch-zoom if you have a touchscreen</li> <li>Zoom in/out with your mousewheel</li> <li>Do a box zoom (drag with your mouse while pressing the <code class="language-plaintext highlighter-rouge">shift</code> key in your keyboard)</li> <li>Use the zoom in/out buttons</li> </ul> <table role="presentation"> <tr><td style="text-align: center; border: none; padding: 0;"> <iframe src="example-delta.html" width="600" height="400" style="max-width: 100%; max-height: 90vh; box-sizing: border-box;"></iframe> </td></tr> <tr><td style="text-align: center; border: none"> <small><a href="example-delta.html">See this example stand-alone.</a></small> </td></tr></table> <p>That concludes this tutorial. Now play with your zoom levels in your maps!</p> </div> </main> <footer class="container"> <div class="footer"> <p>&copy; 2010–2024 <a href="https://agafonkin.com">Volodymyr Agafonkin</a>. Maps &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors.</p> </div> <nav class="ext-links"> <a class="ext-link twitter" href="https://twitter.com/LeafletJS" title="Follow LeafletJS on X"><img alt="Follow LeafletJS on X" src="../../docs/images/x-round.svg" width="46" /></a> <a class="ext-link github" href="http://github.com/Leaflet/Leaflet" title="View Source on GitHub"><img alt="View Source on GitHub" src="../../docs/images/github-round.svg" width="46" /></a> <a class="ext-link forum" href="https://stackoverflow.com/questions/tagged/leaflet" title="Ask for help on Stack Overflow"><img alt="Leaflet questions on Stack Overflow" src="../../docs/images/forum-round.svg" width="46" /></a> </nav> <button id="back-to-top" title="Scroll back to top"> <svg viewBox="0 0 16 16"> <path d="M7.3,3.3c0.4-0.4,1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4s-1,0.4-1.4,0L8,5.4l-5.3,5.3c-0.4,0.4-1,0.4-1.4,0s-0.4-1,0-1.4L7.3,3.3L7.3,3.3z"/> </svg> </button> </footer> <script> var _gaq = _gaq || []; _gaq.push([ '_setAccount', 'UA-4147697-4' ]); _gaq.push([ '_trackPageview' ]); (function() { var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); })(); </script> <script src="../../dialog/dialog.js"></script> <script src="../../docs/js/docs.js"></script> </body> </html>

Pages: 1 2 3 4 5 6 7 8 9 10