Skip to content

Instantly share code, notes, and snippets.

@beryllium
Forked from veltman/tiles.md
Created May 3, 2019 16:55
Show Gist options
  • Save beryllium/f1857ec68ea77125166d32049ec9ff61 to your computer and use it in GitHub Desktop.
Save beryllium/f1857ec68ea77125166d32049ec9ff61 to your computer and use it in GitHub Desktop.
Making a big image zoomable

Making a big image zoomable

When you have a giant image and you want to make it easy to pan and zoom without downloading the whole 50MB image into someone's browser, a nice workaround is to cut that image into tiles at different zoom levels and view it as it were a map. An example where I've used this technique is The "Snowpiercer" Scenario.

One way to cut your big image into the requisite tiles is with gdal2tiles.py.

Alternatively, this Node script will do the cutting after you install node-canvas and mkdirp:

const fs = require("fs"),
  path = require("path"),
  mkdirp = require("mkdirp"),
  { createCanvas, loadImage } = require("canvas");

const minZoom = 0,
  maxZoom = 6,
  tileDirectory = "tiles";

const tile = createCanvas(256, 256).getContext("2d");

loadImage("original-image.png").then(function(img) {
  // Center the image in a square
  const size = Math.max(img.width, img.height);
  const centered = createCanvas(size, size);
  centered
    .getContext("2d")
    .drawImage(
      img,
      Math.round((size - img.width) / 2),
      Math.round((size - img.height) / 2)
    );

  // Make each zoom level
  for (let z = minZoom; z <= maxZoom; z++) {
    const dim = 256 * Math.pow(2, z);
    const numTiles = dim / 256;
    const rescaled = createCanvas(dim, dim);
    rescaled.getContext("2d").drawImage(centered, 0, 0, dim, dim);

    // Render each tile
    for (let x = 0; x < numTiles; x++) {
      const dir = path.join(tileDirectory, z.toString(), x.toString());
      mkdirp.sync(dir);
      for (let y = 0; y < numTiles; y++) {
        console.warn(z, x, y);
        tile.clearRect(0, 0, 256, 256);
        tile.drawImage(rescaled, x * 256, y * 256, 256, 256, 0, 0, 256, 256);
        fs.writeFileSync(path.join(dir, y + ".png"), tile.canvas.toBuffer());
      }
    }
  }
});

This will create a tiles directory will all of your tiles in the file structure that something like Leaflet expects.

Now, you can wrap the image in a viewer like this:

<!DOCTYPE html>
<meta charset="utf-8" />
<link
  rel="stylesheet"
  href="https://unpkg.com/[email protected]/dist/leaflet.css"
  integrity="sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA=="
  crossorigin=""
/>
<style>
  body {
    margin: 0;
    padding: 0;
    background-color: #fff;
  }

  #map {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
  }
</style>

<body>
  <div id="map"></div>
  <script
    src="https://unpkg.com/[email protected]/dist/leaflet.js"
    integrity="sha512-QVftwZFqvtRNi0ZyCtsznlKSWOStnDORoefr1enyq5mVL4tmKB3S/EnC3rRJcxCPavG10IcrVGSmPh6Qw5lwrg=="
    crossorigin=""
  ></script>
  <script>
    var map = L.map("map").setView([0, 0], 2);
    L.tileLayer("tiles/{z}/{x}/{y}.png", {
      minZoom: 0,
      maxZoom: 6,
      noWrap: true
    }).addTo(map);

    map.setMaxBounds([[-90, -180], [90, 180]]);
  </script>
</body>

Enhancements

  • You could speed this up by adding some concurrency
  • You could also skip tiles that are completely outside the original image
  • You could generate 512x512 retina tiles
  • You could use fs.mkdirSync with recursive: true instead of mkdirp on recent versions of Node.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment