feat(geoportal-v2): "S2" basemap — Sentinel-2 cloudless annual mosaics

Adds a 6th basemap option ("S2") backed by EOX's free, public,
CORS-open Sentinel-2 cloudless WMTS service. Annual mosaics from 2016
to 2024 (2025/2026 not yet shipped by EOX); 10 m/pixel resolution
good for large-scale rural change detection (deforestation,
greenhouses, halls, agriculture) but not for individual buildings.

Companion to the Wayback basemap shipped earlier — Wayback gives
high-res city detail at irregular snapshot dates, Sentinel-2 gives
predictable yearly cadence at coarse rural-scale resolution.

UI mirrors Wayback: when "S2" is selected the switcher reveals a year
dropdown beneath the basemap row; the map-viewer rebuilds the raster
source with the right EOX layer ID. Default year = latest (2024).

Note on licensing: EOX's 2018+ mosaics are CC BY-NC-SA 4.0 — non-
commercial. The UI surfaces this + the commercial-licence pointer
(cloudless.eox.at). 2016 (s2cloudless) + 2017 are CC BY 4.0, no
non-commercial restriction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude VM
2026-05-25 07:03:51 +03:00
parent 47d6ba329c
commit 04f666638e
4 changed files with 126 additions and 3 deletions
+45 -1
View File
@@ -8,8 +8,15 @@ import {
latestWaybackRelease,
type WaybackRelease,
} from "./wayback-catalog";
import { SENTINEL_YEARS, type SentinelYear } from "./sentinel-catalog";
export type BasemapId = "liberty" | "dark" | "satellite" | "google" | "wayback";
export type BasemapId =
| "liberty"
| "dark"
| "satellite"
| "google"
| "wayback"
| "sentinel";
const OPTIONS: Array<{ id: BasemapId; label: string }> = [
{ id: "liberty", label: "Liberty" },
@@ -17,6 +24,7 @@ const OPTIONS: Array<{ id: BasemapId; label: string }> = [
{ id: "satellite", label: "Satelit" },
{ id: "google", label: "Google" },
{ id: "wayback", label: "Istoric" },
{ id: "sentinel", label: "S2" },
];
interface Props {
@@ -26,6 +34,10 @@ interface Props {
waybackReleaseId?: string | null;
/** Fired when the user picks a different Wayback date. */
onWaybackReleaseChange?: (release: WaybackRelease) => void;
/** Selected Sentinel-2 cloudless year (only meaningful when value=sentinel). */
sentinelYear?: string | null;
/** Fired when the user picks a different Sentinel-2 year. */
onSentinelYearChange?: (year: SentinelYear) => void;
}
export function BasemapSwitcher({
@@ -33,6 +45,8 @@ export function BasemapSwitcher({
onChange,
waybackReleaseId,
onWaybackReleaseChange,
sentinelYear,
onSentinelYearChange,
}: Props) {
const [releases, setReleases] = useState<WaybackRelease[] | null>(null);
const [loading, setLoading] = useState(false);
@@ -80,6 +94,36 @@ export function BasemapSwitcher({
</div>
</div>
{value === "sentinel" && (
<div className="rounded-md border bg-background/95 px-2 py-1.5 shadow-sm backdrop-blur">
<label className="flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
<Clock className="h-3 w-3" />
An mosaic Sentinel-2
</label>
<select
value={sentinelYear ?? SENTINEL_YEARS[0]!.year}
onChange={(e) => {
const y = SENTINEL_YEARS.find((s) => s.year === e.target.value);
if (y && onSentinelYearChange) onSentinelYearChange(y);
}}
className="mt-1 w-full rounded border bg-background px-1.5 py-1 text-xs"
>
{SENTINEL_YEARS.map((y) => (
<option key={y.year} value={y.year}>
{y.year}
{y.year === "2016" ? " (+2017)" : ""}
</option>
))}
</select>
<p className="mt-1 text-[9px] leading-tight text-muted-foreground">
Mozaic anual cloud-free Sentinel-2 (rezoluție ~10 m). Bun
pentru schimbări rurale large-scale (defrișări, hale,
sere). Nu vezi case sau detalii fine. © EOX / Copernicus.
Uz comercial: cloudless.eox.at.
</p>
</div>
)}
{value === "wayback" && (
<div className="rounded-md border bg-background/95 px-2 py-1.5 shadow-sm backdrop-blur">
<label className="flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">