Skip to content

Instantly share code, notes, and snippets.

@veltman veltman/tiles.md
Last active Jul 8, 2019

Embed
What would you like to do?
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/leaflet@1.4.0/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/leaflet@1.4.0/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.
@cpietsch

This comment has been minimized.

Copy link

commented May 15, 2019

this is golden! very simple and clean. before I used https://github.com/VoidVolker/MagickSlicer for that

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.