<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Planetary Computer · deck.gl-raster (no build)</title>
<link href="https://esm.sh/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
<style>
html, body { margin: 0; height: 100%; font-family: system-ui, sans-serif; }
#map { position: absolute; inset: 0; }
#panel {
position: absolute; top: 16px; left: 16px; z-index: 2; width: 240px;
background: #ffffff; border-radius: 10px; padding: 16px 18px;
box-shadow: 0 2px 14px #0003; font-size: 13px; color: #1a1a1a;
}
#panel h1 { font-size: 15px; margin: 0 0 6px; }
#panel .muted { color: #666; line-height: 1.4; }
#panel label { display: block; margin-top: 14px; font-weight: 600; }
#panel input[type=range] { width: 100%; }
#scene { font-family: ui-monospace, monospace; font-size: 11px; word-break: break-all; }
</style>
<!-- No build step: an import map resolves every dependency from a CDN.
All @deck.gl/* and @luma.gl/core MUST share one version (mismatched
patch versions throw "deck.gl - multiple versions detected"). The
deck.gl-geotiff entry marks deck/luma/geotiff as `external` so they
resolve to the singletons above instead of being bundled again. -->
<script type="importmap">
{
"imports": {
"maplibre-gl": "https://esm.sh/maplibre-gl@4.7.1",
"@deck.gl/core": "https://esm.sh/@deck.gl/core@9.3.2",
"@deck.gl/layers": "https://esm.sh/@deck.gl/layers@9.3.2",
"@deck.gl/geo-layers": "https://esm.sh/@deck.gl/geo-layers@9.3.2",
"@deck.gl/mesh-layers": "https://esm.sh/@deck.gl/mesh-layers@9.3.2",
"@deck.gl/mapbox": "https://esm.sh/@deck.gl/mapbox@9.3.2?external=@deck.gl/core,maplibre-gl",
"@luma.gl/core": "https://esm.sh/@luma.gl/core@9.3.2",
"@developmentseed/geotiff": "https://esm.sh/@developmentseed/geotiff@0.7.0",
"@developmentseed/deck.gl-geotiff": "https://esm.sh/@developmentseed/deck.gl-geotiff@0.7.0?external=@deck.gl/core,@deck.gl/layers,@deck.gl/geo-layers,@deck.gl/mesh-layers,@luma.gl/core,@developmentseed/geotiff"
}
}
</script>
</head>
<body>
<div id="map"></div>
<div id="panel">
<h1>NAIP over Portland</h1>
<div class="muted">A Cloud Optimized GeoTIFF, decoded and reprojected in your browser with <b>deck.gl-raster</b>. No tile server.</div>
<label>Imagery opacity</label>
<input id="opacity" type="range" min="0" max="100" value="100" />
<label>Scene</label>
<div id="scene" class="muted">searching…</div>
</div>
<script type="module">
import maplibregl from "maplibre-gl";
import { MapboxOverlay } from "@deck.gl/mapbox";
import { COGLayer } from "@developmentseed/deck.gl-geotiff";
import { DecoderPool } from "@developmentseed/geotiff";
const STAC = "https://planetarycomputer.microsoft.com/api/stac/v1";
const SIGN = "https://planetarycomputer.microsoft.com/api/sas/v1/sign?href=";
const map = new maplibregl.Map({
container: "map",
style: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
center: [-122.62, 45.52],
zoom: 11,
});
// size: 0 keeps decoding on the main thread. The default pool spawns a
// Web Worker from the package URL, which browsers block when that URL is
// cross-origin (a CDN). Main-thread decoding sidesteps that for a
// single-file app; add a same-origin worker if you need the throughput.
const pool = new DecoderPool({ size: 0 });
const overlay = new MapboxOverlay({ interleaved: true, layers: [] });
map.addControl(overlay);
let opacity = 1;
let geotiff = null;
let beforeId = null; // draw imagery beneath the basemap's labels
let styleReady = false;
let fitted = false;
function render() {
if (!geotiff || !styleReady) return;
overlay.setProps({
layers: [
new COGLayer({
id: "naip",
geotiff,
pool,
opacity,
beforeId,
// Frame the map to the COG's own bounds once it has loaded.
onGeoTIFFLoad: (tiff, { geographicBounds }) => {
if (fitted) return;
fitted = true;
const { west, south, east, north } = geographicBounds;
map.fitBounds([[west, south], [east, north]], { padding: 40, duration: 0 });
},
}),
],
});
}
document.getElementById("opacity").addEventListener("input", (e) => {
opacity = e.target.value / 100;
render();
});
map.on("load", () => {
// Insert deck layers before the first label layer so basemap text
// (place names, roads) stays legible on top of the imagery.
beforeId = map.getStyle().layers.find((l) => l.type === "symbol")?.id;
styleReady = true;
render();
});
// Find a NAIP scene over Portland, then sign its asset href. The Planetary
// Computer signing endpoint is public. No key, no backend.
const search = await fetch(`${STAC}/search`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
collections: ["naip"],
bbox: [-122.70, 45.50, -122.55, 45.57],
datetime: "2022-01-01/2023-01-01",
limit: 1,
}),
}).then((r) => r.json());
const item = search.features[0];
const signed = await fetch(SIGN + encodeURIComponent(item.assets.image.href)).then((r) => r.json());
geotiff = signed.href;
document.getElementById("scene").textContent = item.id;
render();
</script>
</body>
</html>
See https://github.com/microsoft/PlanetaryComputerDataCatalog/pull/530/changes#diff-75f8c328e1b62b6317e072b79dea671d5638af89f2c75bbe1b7f29491bf559f7