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>
- 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
withrecursive: true
instead ofmkdirp
on recent versions of Node.
this is golden! very simple and clean. before I used https://github.com/VoidVolker/MagickSlicer for that