Search code examples
javascriptleaflettileddeepzoomlarge-image

Use leaflet to display non-geographical tiled images


The leaflet documentation provides an insightful tutorial to work with non-geographical image, but it is based on imageOverlay.

const imageSize = {
  width: 2315,
  height: 2315,
}
const maxZoom = 12
const minZoom = 8
const toLatLng = (x, y) => L.CRS.Simple.pointToLatLng(new L.Point(x, y), maxZoom);
const bounds = [
  toLatLng(0, 0),
  toLatLng(imageSize.width, imageSize.height),
];

var viewer = L.map('viewer', {
  crs: L.CRS.Simple,
  maxBounds: bounds,
  minZoom,
  maxZoom,
  zoomSnap: 0,
}).fitBounds(bounds);
L.imageOverlay('https://leafletjs.com/examples/crs-simple/uqm_map_full.png', bounds).addTo(viewer);
#viewer {
  width: 100vw;
  height: 100vh;
  background: none;
}
<link href="https://unpkg.com/[email protected]/dist/leaflet.css" rel="stylesheet"/>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<div id="viewer"/>

How can we transpose this example with tileLayer to display a tiled image based on Deep Zoom Image for instance.


Solution

  • Here an example using the Duomo dataset from OpenSeadragon.

    const source = {
      "Image": {
        "xmlns": "http://schemas.microsoft.com/deepzoom/2008",
        "Url": "https://openseadragon.github.io/example-images/duomo/duomo_files",
        "Format": "jpg",
        "Overlap": 1,
        "TileSize": 254,
        "Size": {
          "Width":  13920,
          "Height": 10200,
        },
      },
    };
    
    const maxZoom = Math.ceil(Math.log2(Math.max(source.Image.Size.Width, source.Image.Size.Height)));
    const minZoom = Math.ceil(Math.log2(source.Image.TileSize + 2 * source.Image.Overlap));
    const toLatLng = (x, y) => L.CRS.Simple.pointToLatLng(new L.Point(x, y), maxZoom);
    const bounds = [
      toLatLng(0, 0),
      toLatLng(source.Image.Size.Width, source.Image.Size.Height),
    ];
    
    const viewer = L.map("viewer", {
      crs: L.CRS.Simple,
      maxBounds: bounds,
      minZoom,
      maxZoom,
      zoomSnap: 0,
    }).fitBounds(bounds);
    L.tileLayer(`${source.Image.Url}/{z}/{x}_{y}.${source.Image.Format}`, {
      noWrap: true,  // do not query tiles outside the bounds
      bounds: bounds,
    }).addTo(viewer);
    #viewer {
      width: 100vw;
      height: 100vh;
      background: none;
    }
    
    img.leaflet-tile {
      height: auto !important;
      width: auto !important;
    }
    <link href="https://unpkg.com/[email protected]/dist/leaflet.css" rel="stylesheet"/>
    <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
    <div id="viewer"/>

    According to Deep Zoom Image documentation, large scale images are represented by a pyramid. Each resolution of the pyramid is equivalent to a level. Levels are counted from the 1x1 pixel resolution as level 0, then resolution is doubled for each following level. Therefore we can compute the resolution from the level: resolution = 2 ^ level and infer the level required to display a specific resolution: level = ceil(log2(resolution)).

    In our example, we use the full image resolution to compute the maximum zoom level and the tile resolution to compute the minimum zoom level.

    For the edges of the image, Deep Zoom Image returns non square tiles with a different size. This is why we need to override leaflet-tile class properties to prevent leaflet from distorting the image.

    Note: the Duomo dataset seems to contain non-edge tiles with invalid size (255px instead of 256px), this is why a white line could appear for some zoom levels. But we don't expect this to happen for a dataset with only valid tile sizes.