// 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//{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; let cache: { at: number; data: WaybackRelease[] } | null = null; let inflight: Promise | 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 { 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; }