47d6ba329c
Adds a 5th basemap option ("Istoric") that loads historical ESRI World
Imagery snapshots from the public Wayback service. Free, CORS-open,
193+ releases dating back to 2014; each release is identified by a
numeric id baked into the tile URL pattern.
How it works:
- wayback-catalog.ts fetches the public waybackconfig.json once per
24h, parses each release's title for an ISO date, and exposes a
newest-first list of { id, date, title, itemUrl }.
- BasemapSwitcher reveals a date dropdown beneath the basemap buttons
when "Istoric" is selected. Auto-picks the latest release on first
show; user can pick any past date.
- map-viewer rebuilds the MapLibre style when basemap=="wayback" with
the user-picked release id patched into the raster source tiles[].
Tile URL format (WMTS, {z}/{y}/{x} not {z}/{x}/{y}):
https://wayback.maptiles.arcgis.com/arcgis/rest/services
/World_Imagery/WMTS/1.0.0/default028mm/MapServer
/tile/<releaseId>/{z}/{y}/{x}
Esri's edge deduplicates identical tiles via 301 → another release id
(MapLibre/browser follows the redirect transparently), so picking a
random old date doesn't always show a different image when nothing
changed in that pixel — that's a feature of Wayback, not a bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
93 lines
3.4 KiB
TypeScript
93 lines
3.4 KiB
TypeScript
// ESRI World Imagery Wayback — catalog helper for the "Satelit istoric"
|
|
// basemap. Wayback is a free, public, CORS-open WMTS service that keeps
|
|
// every snapshot of Esri's World Imagery layer back to 2014 (~193
|
|
// releases as of 2026-05). Each release is identified by a numeric
|
|
// `releaseId` baked into the tile URL:
|
|
//
|
|
// https://wayback.maptiles.arcgis.com/arcgis/rest/services/World_Imagery
|
|
// /WMTS/1.0.0/default028mm/MapServer/tile/<releaseId>/{z}/{y}/{x}
|
|
//
|
|
// The catalog at /waybackconfig.json lists every release with its title
|
|
// ("World Imagery (Wayback 2026-04-30)") which is the only place the
|
|
// date lives. We parse the title to expose a sortable timeline.
|
|
//
|
|
// Caching: catalog is ~100 KB and updates monthly when Esri ships a new
|
|
// release. Module-level cache + lazy fetch keeps the picker responsive
|
|
// while still picking up new releases without a panel reload.
|
|
|
|
export type WaybackRelease = {
|
|
/** Numeric release id baked into the tile URL (e.g. 49059). */
|
|
id: string;
|
|
/** Parsed release date — "2026-04-30" — from the item title. */
|
|
date: string;
|
|
/** Human-readable title verbatim from catalog. */
|
|
title: string;
|
|
/** Templated tile URL with {level}/{row}/{col} placeholders. */
|
|
itemUrl: string;
|
|
};
|
|
|
|
const CATALOG_URL =
|
|
"https://s3-us-west-2.amazonaws.com/config.maptiles.arcgis.com/waybackconfig.json";
|
|
|
|
type CatalogEntry = {
|
|
itemID?: string;
|
|
itemTitle?: string;
|
|
itemURL?: string;
|
|
};
|
|
|
|
type Catalog = Record<string, CatalogEntry>;
|
|
|
|
let cache: { at: number; data: WaybackRelease[] } | null = null;
|
|
let inflight: Promise<WaybackRelease[]> | null = null;
|
|
|
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
|
|
function parseDate(title: string): string | null {
|
|
// "World Imagery (Wayback 2026-04-30)" → 2026-04-30
|
|
const m = /(\d{4}-\d{2}-\d{2})/.exec(title);
|
|
return m?.[1] ?? null;
|
|
}
|
|
|
|
/** Fetch + parse the Wayback catalog. Memoized 24h. */
|
|
export async function loadWaybackReleases(): Promise<WaybackRelease[]> {
|
|
if (cache && Date.now() - cache.at < CACHE_TTL_MS) return cache.data;
|
|
if (inflight) return inflight;
|
|
inflight = (async () => {
|
|
try {
|
|
const res = await fetch(CATALOG_URL, { cache: "no-store" });
|
|
if (!res.ok) throw new Error(`wayback catalog ${res.status}`);
|
|
const json = (await res.json()) as Catalog;
|
|
const releases: WaybackRelease[] = [];
|
|
for (const [id, entry] of Object.entries(json)) {
|
|
if (!entry?.itemTitle || !entry?.itemURL) continue;
|
|
const date = parseDate(entry.itemTitle);
|
|
if (!date) continue;
|
|
releases.push({
|
|
id,
|
|
date,
|
|
title: entry.itemTitle,
|
|
itemUrl: entry.itemURL,
|
|
});
|
|
}
|
|
// Newest first — date string comparison works because ISO yyyy-mm-dd.
|
|
releases.sort((a, b) => (a.date < b.date ? 1 : -1));
|
|
cache = { at: Date.now(), data: releases };
|
|
return releases;
|
|
} finally {
|
|
inflight = null;
|
|
}
|
|
})();
|
|
return inflight;
|
|
}
|
|
|
|
/** Build the tile-URL template MapLibre expects for a given release id.
|
|
* WMTS uses {z}/{y}/{x} order in the path (NOT {z}/{x}/{y}). */
|
|
export function waybackTileUrl(releaseId: string): string {
|
|
return `https://wayback.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/WMTS/1.0.0/default028mm/MapServer/tile/${releaseId}/{z}/{y}/{x}`;
|
|
}
|
|
|
|
/** Pick a reasonable default release: the most recent one. */
|
|
export function latestWaybackRelease(releases: WaybackRelease[]): WaybackRelease | null {
|
|
return releases[0] ?? null;
|
|
}
|