Umar666 commited on
Commit
d1ac326
·
verified ·
1 Parent(s): 07c8635

Deploy curated change-detection demo (FastAPI + React/MapLibre, Docker :7860); bundles pulled from the Model repo at startup

Browse files
.dockerignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ frontend/node_modules
2
+ frontend/dist
3
+ backend/__pycache__
4
+ **/__pycache__
5
+ **/*.pyc
6
+ .venv
7
+ venv
8
+ *.log
.gitattributes CHANGED
@@ -33,3 +33,17 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ backend/data/curated/levir_00_test77/after.png filter=lfs diff=lfs merge=lfs -text
37
+ backend/data/curated/levir_00_test77/before.png filter=lfs diff=lfs merge=lfs -text
38
+ backend/data/curated/levir_01_test45/after.png filter=lfs diff=lfs merge=lfs -text
39
+ backend/data/curated/levir_01_test45/before.png filter=lfs diff=lfs merge=lfs -text
40
+ backend/data/curated/levir_02_test20/after.png filter=lfs diff=lfs merge=lfs -text
41
+ backend/data/curated/levir_02_test20/before.png filter=lfs diff=lfs merge=lfs -text
42
+ backend/data/curated/levir_03_test118/after.png filter=lfs diff=lfs merge=lfs -text
43
+ backend/data/curated/levir_03_test118/before.png filter=lfs diff=lfs merge=lfs -text
44
+ backend/data/curated/levir_04_test34/after.png filter=lfs diff=lfs merge=lfs -text
45
+ backend/data/curated/levir_04_test34/before.png filter=lfs diff=lfs merge=lfs -text
46
+ backend/data/curated/levir_05_test08/after.png filter=lfs diff=lfs merge=lfs -text
47
+ backend/data/curated/levir_05_test08/before.png filter=lfs diff=lfs merge=lfs -text
48
+ backend/data/curated/levir_06_test32/after.png filter=lfs diff=lfs merge=lfs -text
49
+ backend/data/curated/levir_06_test32/before.png filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Curated change-detection Space — single container, FastAPI serves the React/MapLibre build +
2
+ # the inference API on :7860 (PRD §10). Multi-stage: node builds the static frontend, then a slim
3
+ # python runtime serves it alongside onnxruntime (CPU) inference.
4
+
5
+ # ---- stage 1: build the frontend ------------------------------------------------------------
6
+ FROM node:20-slim AS frontend
7
+ WORKDIR /frontend
8
+ COPY frontend/package.json frontend/package-lock.json* ./
9
+ RUN npm ci || npm install
10
+ COPY frontend/ ./
11
+ RUN npm run build
12
+
13
+ # ---- stage 2: python runtime ----------------------------------------------------------------
14
+ FROM python:3.11-slim AS runtime
15
+ WORKDIR /app
16
+ ENV PYTHONUNBUFFERED=1 \
17
+ PIP_NO_CACHE_DIR=1 \
18
+ HF_HOME=/tmp/hf \
19
+ BUNDLES_DIR=/app/models \
20
+ CURATED_DIR=/app/data/curated \
21
+ FRONTEND_DIST=/app/frontend_dist
22
+
23
+ COPY backend/requirements.txt ./requirements.txt
24
+ RUN pip install --no-cache-dir -r requirements.txt
25
+
26
+ COPY backend/ ./
27
+ COPY --from=frontend /frontend/dist ./frontend_dist
28
+
29
+ # Model bundles: either baked into ./models at build time, or pulled at startup from a companion
30
+ # HF Model repo by setting HF_BUNDLE_REPO (PRD §3/§9 — the Space pulls weights, never trains).
31
+ EXPOSE 7860
32
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,66 @@
1
  ---
2
- title: Geospatial1
3
- emoji: 🌖
4
- colorFrom: yellow
5
- colorTo: purple
6
  sdk: docker
 
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Satellite Change Detection (Curated)
3
+ emoji: 🛰️
4
+ colorFrom: blue
5
+ colorTo: gray
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
+ license: mit
10
  ---
11
 
12
+ # Satellite Change Detection curated demo (Track A)
13
+
14
+ A single-container [Hugging Face Docker Space](https://huggingface.co/docs/hub/spaces-sdks-docker):
15
+ **FastAPI** (uvicorn, `:7860`) serves a **React + MapLibre GL** build and a CPU **onnxruntime**
16
+ inference API. This is the **curated** before/after mode (PRD §10.1) — the always-fast demo path;
17
+ the live Sentinel-2 AOI mode is a later milestone.
18
+
19
+ ## What it does
20
+
21
+ - Pick a curated before/after scene pair and a model bundle.
22
+ - The backend runs the pair through the exported **ONNX** change-detection model (CPU) and returns a
23
+ change-mask overlay + stats (% area changed, mean confidence, inference time).
24
+ - A MapLibre **swipe slider** compares before vs after; the detected change is a colored overlay with
25
+ an **opacity** control. A **model-card** page carries the real LEVIR-CD results and limitations.
26
+
27
+ ## Model bundles (the contract)
28
+
29
+ The app consumes only **artifact bundles** produced by `src/export.py` (PRD §3/§9), never the
30
+ training code. Each bundle is a directory with:
31
+
32
+ ```
33
+ model.onnx # exported graph (parity-checked against PyTorch)
34
+ preprocessing.json # normalization, input size, band order, tiling, recommended threshold
35
+ config.yaml # provenance
36
+ metrics_card.md # headline metrics
37
+ parity.json # recorded PyTorch↔ONNXRuntime parity
38
+ ```
39
+
40
+ Bundles are either baked into `./models` at build time or pulled at startup from a companion HF
41
+ **Model repo** by setting the `HF_BUNDLE_REPO` env var — this keeps the Space lean and separates
42
+ weights from the app.
43
+
44
+ ## Run locally
45
+
46
+ ```bash
47
+ # 1. export at least one bundle (from the repo root, with the train env):
48
+ python -m src.export --config configs/levircd_segformer.yaml # or --random-init to smoke it
49
+ cp -r bundles/* app/backend/models/
50
+
51
+ # 2. synthesize curated pairs (or drop real LEVIR-CD tiles into app/backend/data/curated/):
52
+ python app/backend/gen_sample_pairs.py --out app/backend/data/curated
53
+
54
+ # 3. build the frontend + run the API (serves the build at http://localhost:7860):
55
+ cd app/frontend && npm ci && npm run build && cd ..
56
+ BUNDLES_DIR=backend/models CURATED_DIR=backend/data/curated FRONTEND_DIST=frontend/dist \
57
+ uvicorn backend.app:app --host 0.0.0.0 --port 7860
58
+ ```
59
+
60
+ Or build the whole thing with Docker: `docker build -t sat-cd app/ && docker run -p 7860:7860 sat-cd`.
61
+
62
+ ## Notes / honesty
63
+
64
+ - Change-class metrics only — overall pixel accuracy is meaningless when change is a tiny pixel
65
+ fraction (see the model card).
66
+ - Trained weights inherit LEVIR-CD research/non-commercial terms — showcase use only.
backend/app.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI app for the curated change-detection Space (PRD §10).
2
+
3
+ Single container (HF Spaces Docker SDK): this process serves the built React/MapLibre frontend as
4
+ static files AND the inference API on port 7860. CPU-only ``onnxruntime``.
5
+
6
+ Endpoints
7
+ GET /api/health liveness + what's loaded
8
+ GET /api/models available model bundles (id, input size, threshold, ...)
9
+ GET /api/models/{id}/card the bundle's metrics_card.md (markdown)
10
+ GET /api/curated curated before/after pairs (id, title, size, ...)
11
+ GET /api/curated/{id}/{which}.png the before/after image
12
+ POST /api/predict {pair_id, model_id} -> change overlay (PNG data URL) + stats
13
+
14
+ Config via env:
15
+ BUNDLES_DIR dir of exported model bundles (default: app/backend/models)
16
+ CURATED_DIR dir of curated pairs + manifest (default: app/backend/data/curated)
17
+ FRONTEND_DIST built React app to serve at / (default: app/frontend/dist)
18
+ HF_BUNDLE_REPO optional HF model repo to pull bundles from at startup (id or url)
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import os
25
+ import threading
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ from curated import CuratedRegistry
30
+ from fastapi import FastAPI, HTTPException
31
+ from fastapi.middleware.cors import CORSMiddleware
32
+ from fastapi.responses import FileResponse, HTMLResponse, PlainTextResponse
33
+ from fastapi.staticfiles import StaticFiles
34
+ from inference import BundleRegistry
35
+ from pydantic import BaseModel
36
+
37
+ _HERE = Path(__file__).resolve().parent
38
+ BUNDLES_DIR = Path(os.environ.get("BUNDLES_DIR", _HERE / "models"))
39
+ CURATED_DIR = Path(os.environ.get("CURATED_DIR", _HERE / "data" / "curated"))
40
+ FRONTEND_DIST = Path(os.environ.get("FRONTEND_DIST", _HERE.parent / "frontend" / "dist"))
41
+
42
+
43
+ def _maybe_pull_bundles() -> None:
44
+ """If HF_BUNDLE_REPO is set and BUNDLES_DIR is empty, pull the bundles from the HF Model repo.
45
+
46
+ This is the HF-Space networking path (PRD §3/§9): the Space pulls weights from a companion
47
+ Model repo at startup. It is a no-op locally (repo unset) and never runs on Leonardo.
48
+ """
49
+ repo = os.environ.get("HF_BUNDLE_REPO", "").strip()
50
+ if not repo or (BUNDLES_DIR.exists() and any(BUNDLES_DIR.iterdir())):
51
+ return
52
+ try:
53
+ from huggingface_hub import snapshot_download
54
+
55
+ BUNDLES_DIR.mkdir(parents=True, exist_ok=True)
56
+ snapshot_download(repo_id=repo, repo_type="model", local_dir=str(BUNDLES_DIR))
57
+ except Exception as exc: # noqa: BLE001 — startup pull is best-effort; app still serves UI
58
+ print(f"[startup] bundle pull from {repo!r} failed: {exc}")
59
+
60
+
61
+ _maybe_pull_bundles()
62
+ models = BundleRegistry(BUNDLES_DIR)
63
+ curated = CuratedRegistry(CURATED_DIR)
64
+ _predict_cache: dict[tuple[str, str], dict[str, Any]] = {}
65
+ # Curated pairs + models are fixed, so predictions are deterministic. tile+stitch inference is a few
66
+ # seconds of CPU per scene (DINOv2), so we bake predictions to disk (the deploy plan): the file is
67
+ # loaded instantly at startup and any gaps are filled by a background prewarm.
68
+ _CACHE_FILE = CURATED_DIR / "_predictions.json"
69
+
70
+
71
+ def _load_prediction_cache() -> None:
72
+ if not _CACHE_FILE.exists():
73
+ return
74
+ try:
75
+ data = json.loads(_CACHE_FILE.read_text())
76
+ except Exception as exc: # noqa: BLE001
77
+ print(f"[cache] could not read {_CACHE_FILE}: {exc}")
78
+ return
79
+ valid = set(curated.pairs)
80
+ for key, result in data.items():
81
+ pair_id, _, model_id = key.partition("||")
82
+ if pair_id in valid and model_id in models.ids():
83
+ _predict_cache[(pair_id, model_id)] = result
84
+
85
+
86
+ def _save_prediction_cache() -> None:
87
+ try:
88
+ data = {f"{p}||{m}": r for (p, m), r in _predict_cache.items()}
89
+ _CACHE_FILE.write_text(json.dumps(data))
90
+ except Exception as exc: # noqa: BLE001
91
+ print(f"[cache] could not write {_CACHE_FILE}: {exc}")
92
+
93
+
94
+ def _compute_prediction(pair_id: str, model_id: str) -> dict[str, Any]:
95
+ """Cached tile+stitch prediction for a (pair, model) — populated on demand and by prewarm."""
96
+ key = (pair_id, model_id)
97
+ cached = _predict_cache.get(key)
98
+ if cached is not None:
99
+ return cached
100
+ before, after = curated.get_pair(pair_id)
101
+ result = models.predict(model_id, before, after)
102
+ result.update({"pair_id": pair_id, "model_id": model_id})
103
+ _predict_cache[key] = result
104
+ return result
105
+
106
+
107
+ def _prewarm() -> None:
108
+ """Fill any (pair, model) predictions missing from the on-disk cache, then persist them so the
109
+ next startup is instant. On-demand requests still work while this runs."""
110
+ changed = False
111
+ for pair_id in list(curated.pairs):
112
+ for model_id in models.ids():
113
+ if (pair_id, model_id) in _predict_cache:
114
+ continue
115
+ try:
116
+ _compute_prediction(pair_id, model_id)
117
+ changed = True
118
+ except Exception as exc: # noqa: BLE001 — best-effort warm; on-demand path still serves
119
+ print(f"[prewarm] {pair_id}/{model_id} failed: {exc}")
120
+ if changed:
121
+ _save_prediction_cache()
122
+
123
+
124
+ _load_prediction_cache()
125
+ threading.Thread(target=_prewarm, daemon=True).start()
126
+
127
+ app = FastAPI(title="Satellite Change Detection — curated demo", version="1.0")
128
+ app.add_middleware(
129
+ CORSMiddleware,
130
+ allow_origins=["*"], # single-origin in prod; permissive so `vite dev` can call the API too
131
+ allow_methods=["*"],
132
+ allow_headers=["*"],
133
+ )
134
+
135
+
136
+ class PredictRequest(BaseModel):
137
+ pair_id: str
138
+ model_id: str
139
+
140
+
141
+ @app.get("/api/health")
142
+ def health() -> dict[str, Any]:
143
+ return {
144
+ "status": "ok",
145
+ "models": models.ids(),
146
+ "n_curated": len(curated.pairs),
147
+ "bundles_dir": str(BUNDLES_DIR),
148
+ }
149
+
150
+
151
+ @app.get("/api/models")
152
+ def list_models() -> list[dict[str, Any]]:
153
+ return models.summaries()
154
+
155
+
156
+ @app.get("/api/models/{model_id}/card", response_class=PlainTextResponse)
157
+ def model_card(model_id: str) -> str:
158
+ try:
159
+ return models.get(model_id).metrics_card or "_No metrics card in bundle._"
160
+ except KeyError as exc:
161
+ raise HTTPException(404, f"unknown model {model_id!r}") from exc
162
+
163
+
164
+ @app.get("/api/curated")
165
+ def list_curated() -> list[dict[str, Any]]:
166
+ return curated.list()
167
+
168
+
169
+ @app.get("/api/curated/{pair_id}/{which}.png")
170
+ def curated_image(pair_id: str, which: str) -> FileResponse:
171
+ try:
172
+ path = curated.image_path(pair_id, which)
173
+ except (KeyError, ValueError) as exc:
174
+ raise HTTPException(404, f"no {which} image for pair {pair_id!r}") from exc
175
+ return FileResponse(path, media_type="image/png")
176
+
177
+
178
+ @app.post("/api/predict")
179
+ def predict(req: PredictRequest) -> dict[str, Any]:
180
+ if req.model_id not in models.ids():
181
+ raise HTTPException(404, f"unknown model {req.model_id!r}")
182
+ if req.pair_id not in curated.pairs:
183
+ raise HTTPException(404, f"unknown pair {req.pair_id!r}")
184
+ return _compute_prediction(req.pair_id, req.model_id)
185
+
186
+
187
+ # --- static frontend (mounted LAST so the /api/* routes above take precedence) ---------------
188
+ # html=True serves index.html for "/" and correctly serves hashed JS/CSS + binary assets.
189
+ if FRONTEND_DIST.exists():
190
+ app.mount("/", StaticFiles(directory=FRONTEND_DIST, html=True), name="frontend")
191
+ else:
192
+
193
+ @app.get("/", response_class=HTMLResponse)
194
+ def index_missing() -> str:
195
+ return (
196
+ "<h1>Frontend not built</h1><p>Run <code>npm ci &amp;&amp; npm run build</code> in "
197
+ "<code>app/frontend</code>, or use the multi-stage Dockerfile.</p>"
198
+ )
backend/curated.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Curated before/after pair registry for the ``/curated`` demo mode (PRD §10.1).
2
+
3
+ A curated *pair* is two co-registered RGB tiles of the same place at two times. On the real Space
4
+ these are LEVIR-CD test samples (plus a couple of well-known change events); the registry reads a
5
+ ``manifest.json`` describing each pair and serves the images from disk. ``scripts/gen_sample_pairs``
6
+ can synthesize placeholder pairs so the pipeline is demonstrable before the real tiles are staged.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from PIL import Image
16
+
17
+
18
+ class CuratedRegistry:
19
+ """Reads ``<data_dir>/manifest.json`` -> pairs, each with ``before.png`` / ``after.png``."""
20
+
21
+ def __init__(self, data_dir: str | Path) -> None:
22
+ self.data_dir = Path(data_dir)
23
+ self.pairs: dict[str, dict[str, Any]] = {}
24
+ self.reload()
25
+
26
+ def reload(self) -> None:
27
+ self.pairs.clear()
28
+ manifest = self.data_dir / "manifest.json"
29
+ if not manifest.exists():
30
+ return
31
+ for entry in json.loads(manifest.read_text()).get("pairs", []):
32
+ pid = str(entry["id"])
33
+ if (self.data_dir / pid / "before.png").exists():
34
+ self.pairs[pid] = entry
35
+
36
+ def list(self) -> list[dict[str, Any]]:
37
+ out = []
38
+ for pid, entry in self.pairs.items():
39
+ before = self.data_dir / pid / "before.png"
40
+ with Image.open(before) as im:
41
+ w, h = im.size
42
+ out.append(
43
+ {
44
+ "id": pid,
45
+ "title": entry.get("title", pid),
46
+ "description": entry.get("description", ""),
47
+ "source": entry.get("source", ""),
48
+ "width": w,
49
+ "height": h,
50
+ }
51
+ )
52
+ return out
53
+
54
+ def image_path(self, pair_id: str, which: str) -> Path:
55
+ if pair_id not in self.pairs:
56
+ raise KeyError(pair_id)
57
+ if which not in ("before", "after"):
58
+ raise ValueError(which)
59
+ return self.data_dir / pair_id / f"{which}.png"
60
+
61
+ def get_pair(self, pair_id: str) -> tuple[Image.Image, Image.Image]:
62
+ before = Image.open(self.image_path(pair_id, "before")).convert("RGB")
63
+ after = Image.open(self.image_path(pair_id, "after")).convert("RGB")
64
+ return before, after
backend/data/curated/_predictions.json ADDED
The diff for this file is too large to render. See raw diff
 
backend/data/curated/levir_00_test77/after.png ADDED

Git LFS Details

  • SHA256: 485129a931c0b084d8d5ec747cb1387c2502ebc0bf0d92d8e8e1b1ef6fc05eb1
  • Pointer size: 132 Bytes
  • Size of remote file: 2.03 MB
backend/data/curated/levir_00_test77/before.png ADDED

Git LFS Details

  • SHA256: 65e16076bf05cf8e72b09a976a7564a897ce88c443616215f84088679dc32105
  • Pointer size: 132 Bytes
  • Size of remote file: 2.53 MB
backend/data/curated/levir_01_test45/after.png ADDED

Git LFS Details

  • SHA256: b97dc05966c479035f9a9a4a69286fffe677c3e9e1165678bc81d42bbed6d580
  • Pointer size: 132 Bytes
  • Size of remote file: 2.22 MB
backend/data/curated/levir_01_test45/before.png ADDED

Git LFS Details

  • SHA256: b73411abc1cff505b25a723dfff0743050d89f36c7ea58756d5a926a8e27030e
  • Pointer size: 132 Bytes
  • Size of remote file: 2.58 MB
backend/data/curated/levir_02_test20/after.png ADDED

Git LFS Details

  • SHA256: 0f9a2799208ca9ec118f39bcec29055296469e307ec3e11d9aac5b61def2ff12
  • Pointer size: 132 Bytes
  • Size of remote file: 2.21 MB
backend/data/curated/levir_02_test20/before.png ADDED

Git LFS Details

  • SHA256: 3764758671053bdacb3e1812c52f68de3f763690e1165078ad8fb61f0a671956
  • Pointer size: 132 Bytes
  • Size of remote file: 2.51 MB
backend/data/curated/levir_03_test118/after.png ADDED

Git LFS Details

  • SHA256: 0957ad19219a7271a5fb7e719660f3a45c8ec385c38e6d44c5251a1638641345
  • Pointer size: 132 Bytes
  • Size of remote file: 2.01 MB
backend/data/curated/levir_03_test118/before.png ADDED

Git LFS Details

  • SHA256: d8874c998557b0cfe85260bb92113d8328d84efeb07ec3322b07840706e86fa3
  • Pointer size: 132 Bytes
  • Size of remote file: 2.45 MB
backend/data/curated/levir_04_test34/after.png ADDED

Git LFS Details

  • SHA256: d50ae790149be040bfe2fa7c4bbf3e3003cc9446f4624e2bdc5a72ef51df263d
  • Pointer size: 132 Bytes
  • Size of remote file: 2.24 MB
backend/data/curated/levir_04_test34/before.png ADDED

Git LFS Details

  • SHA256: e967d6580438294de371f6aabe534e65b8e67a3595b9dba8ba6ddefb7b34cc68
  • Pointer size: 132 Bytes
  • Size of remote file: 2.38 MB
backend/data/curated/levir_05_test08/after.png ADDED

Git LFS Details

  • SHA256: c13bfcaeee4f0c1a7bbdc2c6bf353fd93983fc1a9914fad027b6c3fb107cc895
  • Pointer size: 132 Bytes
  • Size of remote file: 2.16 MB
backend/data/curated/levir_05_test08/before.png ADDED

Git LFS Details

  • SHA256: c3587c4e0d089bfc8693387e9fafeaf80a982ed31ca98ac66b5cc413f2dda399
  • Pointer size: 132 Bytes
  • Size of remote file: 2.45 MB
backend/data/curated/levir_06_test32/after.png ADDED

Git LFS Details

  • SHA256: 5d3c20d88e3b6e2f5800477e72f51b4ac7d63641cc7241e22f683f9c17044637
  • Pointer size: 132 Bytes
  • Size of remote file: 2.36 MB
backend/data/curated/levir_06_test32/before.png ADDED

Git LFS Details

  • SHA256: c9f25776ee5e8680335676c0e4babc35b863640444c2cb394a681381006208ea
  • Pointer size: 132 Bytes
  • Size of remote file: 2.73 MB
backend/data/curated/manifest.json ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "pairs": [
3
+ {
4
+ "id": "levir_00_test77",
5
+ "title": "Building change",
6
+ "description": "Real LEVIR-CD test crop; ~15.0% of pixels are annotated change.",
7
+ "source": "LEVIR-CD test_77"
8
+ },
9
+ {
10
+ "id": "levir_01_test45",
11
+ "title": "Building change",
12
+ "description": "Real LEVIR-CD test crop; ~25.1% of pixels are annotated change.",
13
+ "source": "LEVIR-CD test_45"
14
+ },
15
+ {
16
+ "id": "levir_02_test20",
17
+ "title": "Building change",
18
+ "description": "Real LEVIR-CD test crop; ~20.6% of pixels are annotated change.",
19
+ "source": "LEVIR-CD test_20"
20
+ },
21
+ {
22
+ "id": "levir_03_test118",
23
+ "title": "Building change",
24
+ "description": "Real LEVIR-CD test crop; ~14.3% of pixels are annotated change.",
25
+ "source": "LEVIR-CD test_118"
26
+ },
27
+ {
28
+ "id": "levir_04_test34",
29
+ "title": "Building change",
30
+ "description": "Real LEVIR-CD test crop; ~13.4% of pixels are annotated change.",
31
+ "source": "LEVIR-CD test_34"
32
+ },
33
+ {
34
+ "id": "levir_05_test08",
35
+ "title": "Building change",
36
+ "description": "Real LEVIR-CD test crop; ~8.1% of pixels are annotated change.",
37
+ "source": "LEVIR-CD test_8"
38
+ },
39
+ {
40
+ "id": "levir_06_test32",
41
+ "title": "Stable scene",
42
+ "description": "Sharpest LEVIR-CD test tile; ~1.5% annotated change \u2014 false-positive check.",
43
+ "source": "LEVIR-CD test_32"
44
+ }
45
+ ]
46
+ }
backend/gen_sample_pairs.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate synthetic before/after curated pairs so the Space is demonstrable offline.
2
+
3
+ These are **placeholders**, not LEVIR-CD: textured pseudo-aerial tiles where the "after" image
4
+ adds/removes a few bright rectangles ("buildings"). They exercise the full curated pipeline (slider,
5
+ overlay, stats) end to end. Replace with real LEVIR-CD test tiles for the shipped Space.
6
+
7
+ python app/backend/gen_sample_pairs.py --out app/backend/data/curated
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ from pathlib import Path
15
+
16
+ import numpy as np
17
+ from PIL import Image
18
+
19
+ SIZE = 256
20
+
21
+
22
+ def _texture(rng: np.random.Generator, base: tuple[int, int, int]) -> np.ndarray:
23
+ """Low-frequency muted tonal ground texture around a base RGB colour (pseudo-aerial)."""
24
+ # a single smooth grayscale field (upsampled low-res noise) drives tonal variation on all
25
+ # channels so the result reads like ground/vegetation, not colour noise.
26
+ small = rng.normal(0, 1, (12, 12)).astype(np.float32)
27
+ norm = (small - small.min()) / (float(small.max() - small.min()) + 1e-6)
28
+ field_img = Image.fromarray((norm * 255).astype("uint8")).resize((SIZE, SIZE), Image.BICUBIC)
29
+ field = (np.asarray(field_img, dtype=np.float32) - 128.0) * 0.42 # signed, ~[-54, 54]
30
+ img = np.asarray(base, dtype=np.float32)[None, None, :] + field[:, :, None]
31
+ img += rng.normal(0, 4, (SIZE, SIZE, 3)) # fine grain
32
+ return np.clip(img, 0, 255)
33
+
34
+
35
+ def _add_building(img: np.ndarray, rng: np.random.Generator) -> None:
36
+ """Paint one bright rectangular 'building' with a slight roof-colour jitter (in place)."""
37
+ h = int(rng.integers(14, 40))
38
+ w = int(rng.integers(14, 40))
39
+ y = int(rng.integers(0, SIZE - h))
40
+ x = int(rng.integers(0, SIZE - w))
41
+ roof = np.asarray([210, 205, 195], dtype=np.float32) + rng.normal(0, 12, 3)
42
+ img[y : y + h, x : x + w] = np.clip(roof, 60, 255)
43
+ img[y : y + 2, x : x + w] = np.clip(roof - 45, 0, 255) # a little shadow/eave
44
+
45
+
46
+ def make_pair(
47
+ seed: int, n_common: int, n_added: int, n_removed: int
48
+ ) -> tuple[Image.Image, Image.Image]:
49
+ rng = np.random.default_rng(seed)
50
+ base = (95 + int(rng.integers(-10, 10)), 110 + int(rng.integers(-10, 10)), 80)
51
+ ground = _texture(rng, base)
52
+ before = ground.copy()
53
+ after = ground.copy()
54
+
55
+ for _ in range(n_common): # unchanged buildings — present in both dates
56
+ r2 = np.random.default_rng(int(rng.integers(0, 1 << 31)))
57
+ h = int(r2.integers(14, 40))
58
+ w = int(r2.integers(14, 40))
59
+ y = int(r2.integers(0, SIZE - h))
60
+ x = int(r2.integers(0, SIZE - w))
61
+ roof = np.clip(np.asarray([205, 200, 190], np.float32) + r2.normal(0, 10, 3), 60, 255)
62
+ before[y : y + h, x : x + w] = roof
63
+ after[y : y + h, x : x + w] = roof
64
+
65
+ for _ in range(n_removed): # in before only (demolished)
66
+ _add_building(before, rng)
67
+ for _ in range(n_added): # in after only (constructed) — the real "change"
68
+ _add_building(after, rng)
69
+
70
+ return (
71
+ Image.fromarray(before.astype("uint8"), "RGB"),
72
+ Image.fromarray(after.astype("uint8"), "RGB"),
73
+ )
74
+
75
+
76
+ SPECS = [
77
+ (
78
+ "suburban_growth",
79
+ "Suburban growth",
80
+ "Several new houses appear on former open ground.",
81
+ 5,
82
+ 4,
83
+ 0,
84
+ ),
85
+ (
86
+ "infill_and_demo",
87
+ "Infill + demolition",
88
+ "Two buildings removed, three added between dates.",
89
+ 6,
90
+ 3,
91
+ 2,
92
+ ),
93
+ (
94
+ "dense_block",
95
+ "Dense urban block",
96
+ "Mostly stable; one new structure on the block edge.",
97
+ 9,
98
+ 1,
99
+ 0,
100
+ ),
101
+ (
102
+ "no_change",
103
+ "Stable scene (control)",
104
+ "No construction — tests the false-positive rate.",
105
+ 7,
106
+ 0,
107
+ 0,
108
+ ),
109
+ ]
110
+
111
+
112
+ def main() -> None:
113
+ ap = argparse.ArgumentParser()
114
+ ap.add_argument("--out", default="app/backend/data/curated")
115
+ args = ap.parse_args()
116
+ out = Path(args.out)
117
+ out.mkdir(parents=True, exist_ok=True)
118
+
119
+ pairs = []
120
+ for i, (pid, title, desc, common, added, removed) in enumerate(SPECS):
121
+ before, after = make_pair(seed=100 + i, n_common=common, n_added=added, n_removed=removed)
122
+ (out / pid).mkdir(exist_ok=True)
123
+ before.save(out / pid / "before.png")
124
+ after.save(out / pid / "after.png")
125
+ pairs.append(
126
+ {"id": pid, "title": title, "description": desc, "source": "synthetic placeholder"}
127
+ )
128
+ (out / "manifest.json").write_text(json.dumps({"pairs": pairs}, indent=2))
129
+ print(f"wrote {len(pairs)} synthetic pairs to {out}")
130
+
131
+
132
+ if __name__ == "__main__":
133
+ main()
backend/inference.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ONNX bundle inference for the curated Space (Track A, CPU onnxruntime).
2
+
3
+ A *bundle* is exactly what ``src/export.py`` emits (PRD §3/§9): a directory with ``model.onnx``
4
+ and ``preprocessing.json`` (normalization, input size, band order, tiling, recommended threshold).
5
+ The demo consumes only the bundle — it never imports the training code. This module discovers
6
+ bundles under ``BUNDLES_DIR``, lazily builds an ``onnxruntime`` session per model, and runs a
7
+ before/after RGB pair through the documented preprocessing to produce a change-mask overlay + stats.
8
+
9
+ The preprocessing here mirrors ``src/data/levircd.py`` + the export contract: resize each date to
10
+ the bundle's ``input_size`` (DINOv2 needs the fixed 448 grid; the CNN tiers accept it too), scale to
11
+ [0, 1], standardize with the bundle's mean/std, stack the two dates to ``(1, 2, 3, S, S)``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import base64
17
+ import io
18
+ import json
19
+ import time
20
+ from dataclasses import dataclass, field
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ import numpy as np
25
+ import onnxruntime as ort
26
+ from PIL import Image, ImageFilter
27
+
28
+ # Amber "thermal" change signal, painted directly into the overlay PNG (the frontend no longer
29
+ # hue-rotates). Rendered as a translucent fill with a brighter, crisp 1-2px outline so the buildings
30
+ # underneath stay visible. The opacity slider (CSS) scales the whole overlay.
31
+ FILL_RGB = (255, 141, 52)
32
+ EDGE_RGB = (255, 201, 112)
33
+ FILL_ALPHA = 130 # ~51% in-PNG; slider default 0.75 -> ~0.38 effective fill
34
+ EDGE_ALPHA = 255
35
+
36
+
37
+ @dataclass
38
+ class Bundle:
39
+ """A loaded model bundle: its preprocessing contract + a lazily-created ORT session."""
40
+
41
+ model_id: str
42
+ root: Path
43
+ preprocessing: dict[str, Any]
44
+ metrics_card: str
45
+ _session: ort.InferenceSession | None = field(default=None, repr=False)
46
+
47
+ @property
48
+ def input_size(self) -> int:
49
+ return int(self.preprocessing.get("input_size", 256))
50
+
51
+ @property
52
+ def threshold(self) -> float:
53
+ return float(self.preprocessing.get("output", {}).get("recommended_threshold", 0.5))
54
+
55
+ @property
56
+ def tile_size(self) -> int:
57
+ """Native tile the model was trained on (0.5 m/px). The full scene is tiled into these,
58
+ each inferred at ``input_size`` and stitched — the bundle's documented preprocessing."""
59
+ return int(self.preprocessing.get("tiling", {}).get("tile_size", self.input_size))
60
+
61
+ @property
62
+ def mean(self) -> np.ndarray:
63
+ m = self.preprocessing.get("normalization", {}).get("mean", [0.485, 0.456, 0.406])
64
+ return np.asarray(m, dtype=np.float32).reshape(3, 1, 1)
65
+
66
+ @property
67
+ def std(self) -> np.ndarray:
68
+ s = self.preprocessing.get("normalization", {}).get("std", [0.229, 0.224, 0.225])
69
+ return np.asarray(s, dtype=np.float32).reshape(3, 1, 1)
70
+
71
+ @property
72
+ def is_placeholder(self) -> bool:
73
+ return "RANDOM-INIT" in str(self.preprocessing.get("weights", ""))
74
+
75
+ def session(self) -> ort.InferenceSession:
76
+ if self._session is None:
77
+ so = ort.SessionOptions()
78
+ so.intra_op_num_threads = 2 # HF free tier is ~2 vCPU; ORT clamps to the host anyway
79
+ self._session = ort.InferenceSession(
80
+ str(self.root / "model.onnx"),
81
+ sess_options=so,
82
+ providers=["CPUExecutionProvider"],
83
+ )
84
+ return self._session
85
+
86
+ def summary(self) -> dict[str, Any]:
87
+ cfg_name = self.preprocessing.get("dinov2_note")
88
+ return {
89
+ "id": self.model_id,
90
+ "input_size": self.input_size,
91
+ "dynamic_hw": bool(self.preprocessing.get("dynamic_hw", False)),
92
+ "threshold": self.threshold,
93
+ "band_order": self.preprocessing.get("band_order", ["R", "G", "B"]),
94
+ "is_placeholder": self.is_placeholder,
95
+ "fixed_grid": cfg_name is not None,
96
+ }
97
+
98
+
99
+ def _to_input(img: Image.Image, size: int, mean: np.ndarray, std: np.ndarray) -> np.ndarray:
100
+ """PIL RGB -> normalized ``(3, size, size)`` float32 (resize bilinear, /255, standardize)."""
101
+ arr = np.asarray(img.convert("RGB").resize((size, size), Image.BILINEAR), dtype=np.float32)
102
+ chw = arr.transpose(2, 0, 1) / 255.0
103
+ return (chw - mean) / std
104
+
105
+
106
+ def _overlay_png(mask: np.ndarray, out_size: tuple[int, int]) -> str:
107
+ """Render the change mask as a translucent amber fill + a crisp outline, at display resolution.
108
+
109
+ Display-only: the ``mask`` is the model's thresholded output (stats are computed from it
110
+ upstream, unchanged). Here we only (a) lightly smooth the contour to drop stair-step edges,
111
+ (b) fill each changed region at a low, uniform alpha so buildings stay visible, and (c) trace a
112
+ brighter 1-2px outline around each region. Returned as a PNG data URL.
113
+ """
114
+ binary = Image.fromarray((mask.astype(np.uint8) * 255), mode="L")
115
+ # smooth the boundary (blur then re-binarize) — removes the stair-stepping of a hard mask
116
+ smoothed = np.asarray(binary.filter(ImageFilter.GaussianBlur(1.2))) >= 128
117
+ sm_img = Image.fromarray((smoothed.astype(np.uint8) * 255), mode="L")
118
+ dil = np.asarray(sm_img.filter(ImageFilter.MaxFilter(3))) >= 128 # ~1px grow
119
+ ero = np.asarray(sm_img.filter(ImageFilter.MinFilter(3))) >= 128 # ~1px shrink
120
+ edge = dil & ~ero # ~2px band straddling the boundary
121
+
122
+ h, w = mask.shape
123
+ rgba = np.zeros((h, w, 4), dtype=np.uint8)
124
+ for c in range(3):
125
+ rgba[..., c] = np.where(smoothed, FILL_RGB[c], 0)
126
+ rgba[..., 3] = np.where(smoothed, FILL_ALPHA, 0).astype(np.uint8)
127
+ for c in range(3):
128
+ rgba[edge, c] = EDGE_RGB[c]
129
+ rgba[edge, 3] = EDGE_ALPHA
130
+
131
+ im = Image.fromarray(rgba, mode="RGBA")
132
+ if im.size != out_size:
133
+ im = im.resize(out_size, Image.BILINEAR)
134
+ buf = io.BytesIO()
135
+ im.save(buf, format="PNG")
136
+ return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode("ascii")
137
+
138
+
139
+ class BundleRegistry:
140
+ """Discovers and caches model bundles under ``bundles_dir``."""
141
+
142
+ def __init__(self, bundles_dir: str | Path) -> None:
143
+ self.bundles_dir = Path(bundles_dir)
144
+ self._bundles: dict[str, Bundle] = {}
145
+ self.reload()
146
+
147
+ def reload(self) -> None:
148
+ self._bundles.clear()
149
+ if not self.bundles_dir.exists():
150
+ return
151
+ for child in sorted(self.bundles_dir.iterdir()):
152
+ pre = child / "preprocessing.json"
153
+ onnx = child / "model.onnx"
154
+ if not (pre.exists() and onnx.exists()):
155
+ continue
156
+ card = child / "metrics_card.md"
157
+ self._bundles[child.name] = Bundle(
158
+ model_id=child.name,
159
+ root=child,
160
+ preprocessing=json.loads(pre.read_text()),
161
+ metrics_card=card.read_text() if card.exists() else "",
162
+ )
163
+
164
+ def ids(self) -> list[str]:
165
+ return list(self._bundles)
166
+
167
+ def get(self, model_id: str) -> Bundle:
168
+ if model_id not in self._bundles:
169
+ raise KeyError(model_id)
170
+ return self._bundles[model_id]
171
+
172
+ def summaries(self) -> list[dict[str, Any]]:
173
+ return [b.summary() for b in self._bundles.values()]
174
+
175
+ def _infer_tile(
176
+ self, bundle: Bundle, before_t: Image.Image, after_t: Image.Image
177
+ ) -> np.ndarray:
178
+ """One native tile through the model -> per-pixel change probability (model output grid)."""
179
+ size = bundle.input_size
180
+ x = np.stack(
181
+ [
182
+ _to_input(before_t, size, bundle.mean, bundle.std),
183
+ _to_input(after_t, size, bundle.mean, bundle.std),
184
+ ],
185
+ axis=0,
186
+ )[None].astype(np.float32) # (1, 2, 3, S, S)
187
+ logits = bundle.session().run(["logits"], {"input": x})[0]
188
+ return 1.0 / (1.0 + np.exp(-logits[0, 0])) # (S, S)
189
+
190
+ def predict(self, model_id: str, before: Image.Image, after: Image.Image) -> dict[str, Any]:
191
+ """Tile the full scene into native tiles, infer each, stitch the probability map, then
192
+ threshold + render. This is the bundle's documented preprocessing (``tiling.tile_size``);
193
+ the model, threshold and per-pixel metric are unchanged — they are just applied per tile,
194
+ exactly as the evaluation harness does. Feeding the whole scene in one pass would break the
195
+ model (a 0.5 m/px model at ~4x the trained field of view detects almost nothing)."""
196
+ bundle = self.get(model_id)
197
+ before = before.convert("RGB")
198
+ after = after.convert("RGB")
199
+ w, h = before.size
200
+ tile = bundle.tile_size
201
+
202
+ prob = np.zeros((h, w), dtype=np.float32)
203
+ n_tiles = 0
204
+ t0 = time.perf_counter()
205
+ for y0 in range(0, h, tile):
206
+ for x0 in range(0, w, tile):
207
+ x1, y1 = min(x0 + tile, w), min(y0 + tile, h)
208
+ box = (x0, y0, x1, y1)
209
+ p = self._infer_tile(bundle, before.crop(box), after.crop(box)) # (S, S)
210
+ # resize this tile's probability back to its native footprint, then place it
211
+ p_tile = np.asarray(
212
+ Image.fromarray(p.astype(np.float32), mode="F").resize(
213
+ (x1 - x0, y1 - y0), Image.BILINEAR
214
+ ),
215
+ dtype=np.float32,
216
+ )
217
+ prob[y0:y1, x0:x1] = p_tile
218
+ n_tiles += 1
219
+ elapsed_ms = (time.perf_counter() - t0) * 1000.0
220
+
221
+ thr = bundle.threshold
222
+ mask = prob >= thr # stitched native-resolution mask; stats derive from this
223
+
224
+ overlay = _overlay_png(mask, (w, h))
225
+ changed_frac = float(mask.mean())
226
+ mean_conf_changed = float(prob[mask].mean()) if mask.any() else 0.0
227
+ return {
228
+ "overlay_png": overlay,
229
+ "threshold": thr,
230
+ "is_placeholder": bundle.is_placeholder,
231
+ "stats": {
232
+ "changed_fraction": changed_frac,
233
+ "changed_percent": round(100.0 * changed_frac, 2),
234
+ "mean_confidence_changed": round(mean_conf_changed, 4),
235
+ "mean_confidence_overall": round(float(prob.mean()), 4),
236
+ "changed_pixels": int(mask.sum()),
237
+ "total_pixels": int(mask.size),
238
+ },
239
+ "elapsed_ms": round(elapsed_ms, 1),
240
+ "input_size": bundle.input_size,
241
+ "n_tiles": n_tiles,
242
+ }
backend/requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # Curated HF Space runtime (CPU). Pinned loosely; the Space image resolves at build time.
2
+ fastapi>=0.111
3
+ uvicorn[standard]>=0.30
4
+ onnxruntime>=1.18
5
+ numpy>=1.26,<2.1
6
+ pillow>=10.0
7
+ pyyaml>=6.0
8
+ huggingface-hub>=0.24
frontend/index.html ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Satellite Change Detection — curated demo</title>
7
+ <meta
8
+ name="description"
9
+ content="Before/after satellite change detection with ONNX models (LEVIR-CD Track A)."
10
+ />
11
+ <meta name="theme-color" content="#080b10" />
12
+ <style>
13
+ html,
14
+ body {
15
+ background: #080b10;
16
+ }
17
+ </style>
18
+ </head>
19
+ <body>
20
+ <div id="root"></div>
21
+ <script type="module" src="/src/main.tsx"></script>
22
+ </body>
23
+ </html>
frontend/package-lock.json ADDED
@@ -0,0 +1,2130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "sat-change-detection-frontend",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "sat-change-detection-frontend",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "@fontsource/ibm-plex-mono": "^5.2.7",
12
+ "@fontsource/ibm-plex-sans": "^5.2.8",
13
+ "maplibre-gl": "^4.7.1",
14
+ "react": "^18.3.1",
15
+ "react-dom": "^18.3.1"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^18.3.12",
19
+ "@types/react-dom": "^18.3.1",
20
+ "@vitejs/plugin-react": "^4.3.4",
21
+ "typescript": "^5.6.3",
22
+ "vite": "^5.4.11"
23
+ }
24
+ },
25
+ "node_modules/@babel/code-frame": {
26
+ "version": "7.29.7",
27
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
28
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
29
+ "dev": true,
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "@babel/helper-validator-identifier": "^7.29.7",
33
+ "js-tokens": "^4.0.0",
34
+ "picocolors": "^1.1.1"
35
+ },
36
+ "engines": {
37
+ "node": ">=6.9.0"
38
+ }
39
+ },
40
+ "node_modules/@babel/compat-data": {
41
+ "version": "7.29.7",
42
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
43
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
44
+ "dev": true,
45
+ "license": "MIT",
46
+ "engines": {
47
+ "node": ">=6.9.0"
48
+ }
49
+ },
50
+ "node_modules/@babel/core": {
51
+ "version": "7.29.7",
52
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
53
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
54
+ "dev": true,
55
+ "license": "MIT",
56
+ "dependencies": {
57
+ "@babel/code-frame": "^7.29.7",
58
+ "@babel/generator": "^7.29.7",
59
+ "@babel/helper-compilation-targets": "^7.29.7",
60
+ "@babel/helper-module-transforms": "^7.29.7",
61
+ "@babel/helpers": "^7.29.7",
62
+ "@babel/parser": "^7.29.7",
63
+ "@babel/template": "^7.29.7",
64
+ "@babel/traverse": "^7.29.7",
65
+ "@babel/types": "^7.29.7",
66
+ "@jridgewell/remapping": "^2.3.5",
67
+ "convert-source-map": "^2.0.0",
68
+ "debug": "^4.1.0",
69
+ "gensync": "^1.0.0-beta.2",
70
+ "json5": "^2.2.3",
71
+ "semver": "^6.3.1"
72
+ },
73
+ "engines": {
74
+ "node": ">=6.9.0"
75
+ },
76
+ "funding": {
77
+ "type": "opencollective",
78
+ "url": "https://opencollective.com/babel"
79
+ }
80
+ },
81
+ "node_modules/@babel/generator": {
82
+ "version": "7.29.7",
83
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
84
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
85
+ "dev": true,
86
+ "license": "MIT",
87
+ "dependencies": {
88
+ "@babel/parser": "^7.29.7",
89
+ "@babel/types": "^7.29.7",
90
+ "@jridgewell/gen-mapping": "^0.3.12",
91
+ "@jridgewell/trace-mapping": "^0.3.28",
92
+ "jsesc": "^3.0.2"
93
+ },
94
+ "engines": {
95
+ "node": ">=6.9.0"
96
+ }
97
+ },
98
+ "node_modules/@babel/helper-compilation-targets": {
99
+ "version": "7.29.7",
100
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
101
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
102
+ "dev": true,
103
+ "license": "MIT",
104
+ "dependencies": {
105
+ "@babel/compat-data": "^7.29.7",
106
+ "@babel/helper-validator-option": "^7.29.7",
107
+ "browserslist": "^4.24.0",
108
+ "lru-cache": "^5.1.1",
109
+ "semver": "^6.3.1"
110
+ },
111
+ "engines": {
112
+ "node": ">=6.9.0"
113
+ }
114
+ },
115
+ "node_modules/@babel/helper-globals": {
116
+ "version": "7.29.7",
117
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
118
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
119
+ "dev": true,
120
+ "license": "MIT",
121
+ "engines": {
122
+ "node": ">=6.9.0"
123
+ }
124
+ },
125
+ "node_modules/@babel/helper-module-imports": {
126
+ "version": "7.29.7",
127
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
128
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
129
+ "dev": true,
130
+ "license": "MIT",
131
+ "dependencies": {
132
+ "@babel/traverse": "^7.29.7",
133
+ "@babel/types": "^7.29.7"
134
+ },
135
+ "engines": {
136
+ "node": ">=6.9.0"
137
+ }
138
+ },
139
+ "node_modules/@babel/helper-module-transforms": {
140
+ "version": "7.29.7",
141
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
142
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
143
+ "dev": true,
144
+ "license": "MIT",
145
+ "dependencies": {
146
+ "@babel/helper-module-imports": "^7.29.7",
147
+ "@babel/helper-validator-identifier": "^7.29.7",
148
+ "@babel/traverse": "^7.29.7"
149
+ },
150
+ "engines": {
151
+ "node": ">=6.9.0"
152
+ },
153
+ "peerDependencies": {
154
+ "@babel/core": "^7.0.0"
155
+ }
156
+ },
157
+ "node_modules/@babel/helper-plugin-utils": {
158
+ "version": "7.29.7",
159
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
160
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
161
+ "dev": true,
162
+ "license": "MIT",
163
+ "engines": {
164
+ "node": ">=6.9.0"
165
+ }
166
+ },
167
+ "node_modules/@babel/helper-string-parser": {
168
+ "version": "7.29.7",
169
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
170
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
171
+ "dev": true,
172
+ "license": "MIT",
173
+ "engines": {
174
+ "node": ">=6.9.0"
175
+ }
176
+ },
177
+ "node_modules/@babel/helper-validator-identifier": {
178
+ "version": "7.29.7",
179
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
180
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
181
+ "dev": true,
182
+ "license": "MIT",
183
+ "engines": {
184
+ "node": ">=6.9.0"
185
+ }
186
+ },
187
+ "node_modules/@babel/helper-validator-option": {
188
+ "version": "7.29.7",
189
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
190
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
191
+ "dev": true,
192
+ "license": "MIT",
193
+ "engines": {
194
+ "node": ">=6.9.0"
195
+ }
196
+ },
197
+ "node_modules/@babel/helpers": {
198
+ "version": "7.29.7",
199
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
200
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
201
+ "dev": true,
202
+ "license": "MIT",
203
+ "dependencies": {
204
+ "@babel/template": "^7.29.7",
205
+ "@babel/types": "^7.29.7"
206
+ },
207
+ "engines": {
208
+ "node": ">=6.9.0"
209
+ }
210
+ },
211
+ "node_modules/@babel/parser": {
212
+ "version": "7.29.7",
213
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
214
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
215
+ "dev": true,
216
+ "license": "MIT",
217
+ "dependencies": {
218
+ "@babel/types": "^7.29.7"
219
+ },
220
+ "bin": {
221
+ "parser": "bin/babel-parser.js"
222
+ },
223
+ "engines": {
224
+ "node": ">=6.0.0"
225
+ }
226
+ },
227
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
228
+ "version": "7.29.7",
229
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
230
+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
231
+ "dev": true,
232
+ "license": "MIT",
233
+ "dependencies": {
234
+ "@babel/helper-plugin-utils": "^7.29.7"
235
+ },
236
+ "engines": {
237
+ "node": ">=6.9.0"
238
+ },
239
+ "peerDependencies": {
240
+ "@babel/core": "^7.0.0-0"
241
+ }
242
+ },
243
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
244
+ "version": "7.29.7",
245
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
246
+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
247
+ "dev": true,
248
+ "license": "MIT",
249
+ "dependencies": {
250
+ "@babel/helper-plugin-utils": "^7.29.7"
251
+ },
252
+ "engines": {
253
+ "node": ">=6.9.0"
254
+ },
255
+ "peerDependencies": {
256
+ "@babel/core": "^7.0.0-0"
257
+ }
258
+ },
259
+ "node_modules/@babel/template": {
260
+ "version": "7.29.7",
261
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
262
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
263
+ "dev": true,
264
+ "license": "MIT",
265
+ "dependencies": {
266
+ "@babel/code-frame": "^7.29.7",
267
+ "@babel/parser": "^7.29.7",
268
+ "@babel/types": "^7.29.7"
269
+ },
270
+ "engines": {
271
+ "node": ">=6.9.0"
272
+ }
273
+ },
274
+ "node_modules/@babel/traverse": {
275
+ "version": "7.29.7",
276
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
277
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
278
+ "dev": true,
279
+ "license": "MIT",
280
+ "dependencies": {
281
+ "@babel/code-frame": "^7.29.7",
282
+ "@babel/generator": "^7.29.7",
283
+ "@babel/helper-globals": "^7.29.7",
284
+ "@babel/parser": "^7.29.7",
285
+ "@babel/template": "^7.29.7",
286
+ "@babel/types": "^7.29.7",
287
+ "debug": "^4.3.1"
288
+ },
289
+ "engines": {
290
+ "node": ">=6.9.0"
291
+ }
292
+ },
293
+ "node_modules/@babel/types": {
294
+ "version": "7.29.7",
295
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
296
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
297
+ "dev": true,
298
+ "license": "MIT",
299
+ "dependencies": {
300
+ "@babel/helper-string-parser": "^7.29.7",
301
+ "@babel/helper-validator-identifier": "^7.29.7"
302
+ },
303
+ "engines": {
304
+ "node": ">=6.9.0"
305
+ }
306
+ },
307
+ "node_modules/@esbuild/aix-ppc64": {
308
+ "version": "0.21.5",
309
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
310
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
311
+ "cpu": [
312
+ "ppc64"
313
+ ],
314
+ "dev": true,
315
+ "license": "MIT",
316
+ "optional": true,
317
+ "os": [
318
+ "aix"
319
+ ],
320
+ "engines": {
321
+ "node": ">=12"
322
+ }
323
+ },
324
+ "node_modules/@esbuild/android-arm": {
325
+ "version": "0.21.5",
326
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
327
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
328
+ "cpu": [
329
+ "arm"
330
+ ],
331
+ "dev": true,
332
+ "license": "MIT",
333
+ "optional": true,
334
+ "os": [
335
+ "android"
336
+ ],
337
+ "engines": {
338
+ "node": ">=12"
339
+ }
340
+ },
341
+ "node_modules/@esbuild/android-arm64": {
342
+ "version": "0.21.5",
343
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
344
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
345
+ "cpu": [
346
+ "arm64"
347
+ ],
348
+ "dev": true,
349
+ "license": "MIT",
350
+ "optional": true,
351
+ "os": [
352
+ "android"
353
+ ],
354
+ "engines": {
355
+ "node": ">=12"
356
+ }
357
+ },
358
+ "node_modules/@esbuild/android-x64": {
359
+ "version": "0.21.5",
360
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
361
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
362
+ "cpu": [
363
+ "x64"
364
+ ],
365
+ "dev": true,
366
+ "license": "MIT",
367
+ "optional": true,
368
+ "os": [
369
+ "android"
370
+ ],
371
+ "engines": {
372
+ "node": ">=12"
373
+ }
374
+ },
375
+ "node_modules/@esbuild/darwin-arm64": {
376
+ "version": "0.21.5",
377
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
378
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
379
+ "cpu": [
380
+ "arm64"
381
+ ],
382
+ "dev": true,
383
+ "license": "MIT",
384
+ "optional": true,
385
+ "os": [
386
+ "darwin"
387
+ ],
388
+ "engines": {
389
+ "node": ">=12"
390
+ }
391
+ },
392
+ "node_modules/@esbuild/darwin-x64": {
393
+ "version": "0.21.5",
394
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
395
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
396
+ "cpu": [
397
+ "x64"
398
+ ],
399
+ "dev": true,
400
+ "license": "MIT",
401
+ "optional": true,
402
+ "os": [
403
+ "darwin"
404
+ ],
405
+ "engines": {
406
+ "node": ">=12"
407
+ }
408
+ },
409
+ "node_modules/@esbuild/freebsd-arm64": {
410
+ "version": "0.21.5",
411
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
412
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
413
+ "cpu": [
414
+ "arm64"
415
+ ],
416
+ "dev": true,
417
+ "license": "MIT",
418
+ "optional": true,
419
+ "os": [
420
+ "freebsd"
421
+ ],
422
+ "engines": {
423
+ "node": ">=12"
424
+ }
425
+ },
426
+ "node_modules/@esbuild/freebsd-x64": {
427
+ "version": "0.21.5",
428
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
429
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
430
+ "cpu": [
431
+ "x64"
432
+ ],
433
+ "dev": true,
434
+ "license": "MIT",
435
+ "optional": true,
436
+ "os": [
437
+ "freebsd"
438
+ ],
439
+ "engines": {
440
+ "node": ">=12"
441
+ }
442
+ },
443
+ "node_modules/@esbuild/linux-arm": {
444
+ "version": "0.21.5",
445
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
446
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
447
+ "cpu": [
448
+ "arm"
449
+ ],
450
+ "dev": true,
451
+ "license": "MIT",
452
+ "optional": true,
453
+ "os": [
454
+ "linux"
455
+ ],
456
+ "engines": {
457
+ "node": ">=12"
458
+ }
459
+ },
460
+ "node_modules/@esbuild/linux-arm64": {
461
+ "version": "0.21.5",
462
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
463
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
464
+ "cpu": [
465
+ "arm64"
466
+ ],
467
+ "dev": true,
468
+ "license": "MIT",
469
+ "optional": true,
470
+ "os": [
471
+ "linux"
472
+ ],
473
+ "engines": {
474
+ "node": ">=12"
475
+ }
476
+ },
477
+ "node_modules/@esbuild/linux-ia32": {
478
+ "version": "0.21.5",
479
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
480
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
481
+ "cpu": [
482
+ "ia32"
483
+ ],
484
+ "dev": true,
485
+ "license": "MIT",
486
+ "optional": true,
487
+ "os": [
488
+ "linux"
489
+ ],
490
+ "engines": {
491
+ "node": ">=12"
492
+ }
493
+ },
494
+ "node_modules/@esbuild/linux-loong64": {
495
+ "version": "0.21.5",
496
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
497
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
498
+ "cpu": [
499
+ "loong64"
500
+ ],
501
+ "dev": true,
502
+ "license": "MIT",
503
+ "optional": true,
504
+ "os": [
505
+ "linux"
506
+ ],
507
+ "engines": {
508
+ "node": ">=12"
509
+ }
510
+ },
511
+ "node_modules/@esbuild/linux-mips64el": {
512
+ "version": "0.21.5",
513
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
514
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
515
+ "cpu": [
516
+ "mips64el"
517
+ ],
518
+ "dev": true,
519
+ "license": "MIT",
520
+ "optional": true,
521
+ "os": [
522
+ "linux"
523
+ ],
524
+ "engines": {
525
+ "node": ">=12"
526
+ }
527
+ },
528
+ "node_modules/@esbuild/linux-ppc64": {
529
+ "version": "0.21.5",
530
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
531
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
532
+ "cpu": [
533
+ "ppc64"
534
+ ],
535
+ "dev": true,
536
+ "license": "MIT",
537
+ "optional": true,
538
+ "os": [
539
+ "linux"
540
+ ],
541
+ "engines": {
542
+ "node": ">=12"
543
+ }
544
+ },
545
+ "node_modules/@esbuild/linux-riscv64": {
546
+ "version": "0.21.5",
547
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
548
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
549
+ "cpu": [
550
+ "riscv64"
551
+ ],
552
+ "dev": true,
553
+ "license": "MIT",
554
+ "optional": true,
555
+ "os": [
556
+ "linux"
557
+ ],
558
+ "engines": {
559
+ "node": ">=12"
560
+ }
561
+ },
562
+ "node_modules/@esbuild/linux-s390x": {
563
+ "version": "0.21.5",
564
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
565
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
566
+ "cpu": [
567
+ "s390x"
568
+ ],
569
+ "dev": true,
570
+ "license": "MIT",
571
+ "optional": true,
572
+ "os": [
573
+ "linux"
574
+ ],
575
+ "engines": {
576
+ "node": ">=12"
577
+ }
578
+ },
579
+ "node_modules/@esbuild/linux-x64": {
580
+ "version": "0.21.5",
581
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
582
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
583
+ "cpu": [
584
+ "x64"
585
+ ],
586
+ "dev": true,
587
+ "license": "MIT",
588
+ "optional": true,
589
+ "os": [
590
+ "linux"
591
+ ],
592
+ "engines": {
593
+ "node": ">=12"
594
+ }
595
+ },
596
+ "node_modules/@esbuild/netbsd-x64": {
597
+ "version": "0.21.5",
598
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
599
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
600
+ "cpu": [
601
+ "x64"
602
+ ],
603
+ "dev": true,
604
+ "license": "MIT",
605
+ "optional": true,
606
+ "os": [
607
+ "netbsd"
608
+ ],
609
+ "engines": {
610
+ "node": ">=12"
611
+ }
612
+ },
613
+ "node_modules/@esbuild/openbsd-x64": {
614
+ "version": "0.21.5",
615
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
616
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
617
+ "cpu": [
618
+ "x64"
619
+ ],
620
+ "dev": true,
621
+ "license": "MIT",
622
+ "optional": true,
623
+ "os": [
624
+ "openbsd"
625
+ ],
626
+ "engines": {
627
+ "node": ">=12"
628
+ }
629
+ },
630
+ "node_modules/@esbuild/sunos-x64": {
631
+ "version": "0.21.5",
632
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
633
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
634
+ "cpu": [
635
+ "x64"
636
+ ],
637
+ "dev": true,
638
+ "license": "MIT",
639
+ "optional": true,
640
+ "os": [
641
+ "sunos"
642
+ ],
643
+ "engines": {
644
+ "node": ">=12"
645
+ }
646
+ },
647
+ "node_modules/@esbuild/win32-arm64": {
648
+ "version": "0.21.5",
649
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
650
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
651
+ "cpu": [
652
+ "arm64"
653
+ ],
654
+ "dev": true,
655
+ "license": "MIT",
656
+ "optional": true,
657
+ "os": [
658
+ "win32"
659
+ ],
660
+ "engines": {
661
+ "node": ">=12"
662
+ }
663
+ },
664
+ "node_modules/@esbuild/win32-ia32": {
665
+ "version": "0.21.5",
666
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
667
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
668
+ "cpu": [
669
+ "ia32"
670
+ ],
671
+ "dev": true,
672
+ "license": "MIT",
673
+ "optional": true,
674
+ "os": [
675
+ "win32"
676
+ ],
677
+ "engines": {
678
+ "node": ">=12"
679
+ }
680
+ },
681
+ "node_modules/@esbuild/win32-x64": {
682
+ "version": "0.21.5",
683
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
684
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
685
+ "cpu": [
686
+ "x64"
687
+ ],
688
+ "dev": true,
689
+ "license": "MIT",
690
+ "optional": true,
691
+ "os": [
692
+ "win32"
693
+ ],
694
+ "engines": {
695
+ "node": ">=12"
696
+ }
697
+ },
698
+ "node_modules/@fontsource/ibm-plex-mono": {
699
+ "version": "5.2.7",
700
+ "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.2.7.tgz",
701
+ "integrity": "sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w==",
702
+ "license": "OFL-1.1",
703
+ "funding": {
704
+ "url": "https://github.com/sponsors/ayuhito"
705
+ }
706
+ },
707
+ "node_modules/@fontsource/ibm-plex-sans": {
708
+ "version": "5.2.8",
709
+ "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-sans/-/ibm-plex-sans-5.2.8.tgz",
710
+ "integrity": "sha512-eztSXjDhPhcpxNIiGTgMebdLP9qS4rWkysuE1V7c+DjOR0qiezaiDaTwQE7bTnG5HxAY/8M43XKDvs3cYq6ZYQ==",
711
+ "license": "OFL-1.1",
712
+ "funding": {
713
+ "url": "https://github.com/sponsors/ayuhito"
714
+ }
715
+ },
716
+ "node_modules/@jridgewell/gen-mapping": {
717
+ "version": "0.3.13",
718
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
719
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
720
+ "dev": true,
721
+ "license": "MIT",
722
+ "dependencies": {
723
+ "@jridgewell/sourcemap-codec": "^1.5.0",
724
+ "@jridgewell/trace-mapping": "^0.3.24"
725
+ }
726
+ },
727
+ "node_modules/@jridgewell/remapping": {
728
+ "version": "2.3.5",
729
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
730
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
731
+ "dev": true,
732
+ "license": "MIT",
733
+ "dependencies": {
734
+ "@jridgewell/gen-mapping": "^0.3.5",
735
+ "@jridgewell/trace-mapping": "^0.3.24"
736
+ }
737
+ },
738
+ "node_modules/@jridgewell/resolve-uri": {
739
+ "version": "3.1.2",
740
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
741
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
742
+ "dev": true,
743
+ "license": "MIT",
744
+ "engines": {
745
+ "node": ">=6.0.0"
746
+ }
747
+ },
748
+ "node_modules/@jridgewell/sourcemap-codec": {
749
+ "version": "1.5.5",
750
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
751
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
752
+ "dev": true,
753
+ "license": "MIT"
754
+ },
755
+ "node_modules/@jridgewell/trace-mapping": {
756
+ "version": "0.3.31",
757
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
758
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
759
+ "dev": true,
760
+ "license": "MIT",
761
+ "dependencies": {
762
+ "@jridgewell/resolve-uri": "^3.1.0",
763
+ "@jridgewell/sourcemap-codec": "^1.4.14"
764
+ }
765
+ },
766
+ "node_modules/@mapbox/geojson-rewind": {
767
+ "version": "0.5.2",
768
+ "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
769
+ "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
770
+ "license": "ISC",
771
+ "dependencies": {
772
+ "get-stream": "^6.0.1",
773
+ "minimist": "^1.2.6"
774
+ },
775
+ "bin": {
776
+ "geojson-rewind": "geojson-rewind"
777
+ }
778
+ },
779
+ "node_modules/@mapbox/jsonlint-lines-primitives": {
780
+ "version": "2.0.3",
781
+ "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.3.tgz",
782
+ "integrity": "sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg==",
783
+ "license": "MIT",
784
+ "engines": {
785
+ "node": ">= 22"
786
+ }
787
+ },
788
+ "node_modules/@mapbox/point-geometry": {
789
+ "version": "0.1.0",
790
+ "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
791
+ "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==",
792
+ "license": "ISC"
793
+ },
794
+ "node_modules/@mapbox/tiny-sdf": {
795
+ "version": "2.2.0",
796
+ "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz",
797
+ "integrity": "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==",
798
+ "license": "BSD-2-Clause"
799
+ },
800
+ "node_modules/@mapbox/unitbezier": {
801
+ "version": "0.0.1",
802
+ "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
803
+ "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
804
+ "license": "BSD-2-Clause"
805
+ },
806
+ "node_modules/@mapbox/vector-tile": {
807
+ "version": "1.3.1",
808
+ "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz",
809
+ "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
810
+ "license": "BSD-3-Clause",
811
+ "dependencies": {
812
+ "@mapbox/point-geometry": "~0.1.0"
813
+ }
814
+ },
815
+ "node_modules/@mapbox/whoots-js": {
816
+ "version": "3.1.0",
817
+ "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
818
+ "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
819
+ "license": "ISC",
820
+ "engines": {
821
+ "node": ">=6.0.0"
822
+ }
823
+ },
824
+ "node_modules/@maplibre/maplibre-gl-style-spec": {
825
+ "version": "20.4.0",
826
+ "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz",
827
+ "integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==",
828
+ "license": "ISC",
829
+ "dependencies": {
830
+ "@mapbox/jsonlint-lines-primitives": "~2.0.2",
831
+ "@mapbox/unitbezier": "^0.0.1",
832
+ "json-stringify-pretty-compact": "^4.0.0",
833
+ "minimist": "^1.2.8",
834
+ "quickselect": "^2.0.0",
835
+ "rw": "^1.3.3",
836
+ "tinyqueue": "^3.0.0"
837
+ },
838
+ "bin": {
839
+ "gl-style-format": "dist/gl-style-format.mjs",
840
+ "gl-style-migrate": "dist/gl-style-migrate.mjs",
841
+ "gl-style-validate": "dist/gl-style-validate.mjs"
842
+ }
843
+ },
844
+ "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/quickselect": {
845
+ "version": "2.0.0",
846
+ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
847
+ "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==",
848
+ "license": "ISC"
849
+ },
850
+ "node_modules/@rolldown/pluginutils": {
851
+ "version": "1.0.0-beta.27",
852
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
853
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
854
+ "dev": true,
855
+ "license": "MIT"
856
+ },
857
+ "node_modules/@rollup/rollup-android-arm-eabi": {
858
+ "version": "4.62.2",
859
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
860
+ "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
861
+ "cpu": [
862
+ "arm"
863
+ ],
864
+ "dev": true,
865
+ "license": "MIT",
866
+ "optional": true,
867
+ "os": [
868
+ "android"
869
+ ]
870
+ },
871
+ "node_modules/@rollup/rollup-android-arm64": {
872
+ "version": "4.62.2",
873
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
874
+ "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
875
+ "cpu": [
876
+ "arm64"
877
+ ],
878
+ "dev": true,
879
+ "license": "MIT",
880
+ "optional": true,
881
+ "os": [
882
+ "android"
883
+ ]
884
+ },
885
+ "node_modules/@rollup/rollup-darwin-arm64": {
886
+ "version": "4.62.2",
887
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
888
+ "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
889
+ "cpu": [
890
+ "arm64"
891
+ ],
892
+ "dev": true,
893
+ "license": "MIT",
894
+ "optional": true,
895
+ "os": [
896
+ "darwin"
897
+ ]
898
+ },
899
+ "node_modules/@rollup/rollup-darwin-x64": {
900
+ "version": "4.62.2",
901
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
902
+ "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
903
+ "cpu": [
904
+ "x64"
905
+ ],
906
+ "dev": true,
907
+ "license": "MIT",
908
+ "optional": true,
909
+ "os": [
910
+ "darwin"
911
+ ]
912
+ },
913
+ "node_modules/@rollup/rollup-freebsd-arm64": {
914
+ "version": "4.62.2",
915
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
916
+ "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
917
+ "cpu": [
918
+ "arm64"
919
+ ],
920
+ "dev": true,
921
+ "license": "MIT",
922
+ "optional": true,
923
+ "os": [
924
+ "freebsd"
925
+ ]
926
+ },
927
+ "node_modules/@rollup/rollup-freebsd-x64": {
928
+ "version": "4.62.2",
929
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
930
+ "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
931
+ "cpu": [
932
+ "x64"
933
+ ],
934
+ "dev": true,
935
+ "license": "MIT",
936
+ "optional": true,
937
+ "os": [
938
+ "freebsd"
939
+ ]
940
+ },
941
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
942
+ "version": "4.62.2",
943
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
944
+ "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
945
+ "cpu": [
946
+ "arm"
947
+ ],
948
+ "dev": true,
949
+ "license": "MIT",
950
+ "optional": true,
951
+ "os": [
952
+ "linux"
953
+ ]
954
+ },
955
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
956
+ "version": "4.62.2",
957
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
958
+ "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
959
+ "cpu": [
960
+ "arm"
961
+ ],
962
+ "dev": true,
963
+ "license": "MIT",
964
+ "optional": true,
965
+ "os": [
966
+ "linux"
967
+ ]
968
+ },
969
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
970
+ "version": "4.62.2",
971
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
972
+ "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
973
+ "cpu": [
974
+ "arm64"
975
+ ],
976
+ "dev": true,
977
+ "license": "MIT",
978
+ "optional": true,
979
+ "os": [
980
+ "linux"
981
+ ]
982
+ },
983
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
984
+ "version": "4.62.2",
985
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
986
+ "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
987
+ "cpu": [
988
+ "arm64"
989
+ ],
990
+ "dev": true,
991
+ "license": "MIT",
992
+ "optional": true,
993
+ "os": [
994
+ "linux"
995
+ ]
996
+ },
997
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
998
+ "version": "4.62.2",
999
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
1000
+ "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
1001
+ "cpu": [
1002
+ "loong64"
1003
+ ],
1004
+ "dev": true,
1005
+ "license": "MIT",
1006
+ "optional": true,
1007
+ "os": [
1008
+ "linux"
1009
+ ]
1010
+ },
1011
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
1012
+ "version": "4.62.2",
1013
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
1014
+ "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
1015
+ "cpu": [
1016
+ "loong64"
1017
+ ],
1018
+ "dev": true,
1019
+ "license": "MIT",
1020
+ "optional": true,
1021
+ "os": [
1022
+ "linux"
1023
+ ]
1024
+ },
1025
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
1026
+ "version": "4.62.2",
1027
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
1028
+ "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
1029
+ "cpu": [
1030
+ "ppc64"
1031
+ ],
1032
+ "dev": true,
1033
+ "license": "MIT",
1034
+ "optional": true,
1035
+ "os": [
1036
+ "linux"
1037
+ ]
1038
+ },
1039
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
1040
+ "version": "4.62.2",
1041
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
1042
+ "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
1043
+ "cpu": [
1044
+ "ppc64"
1045
+ ],
1046
+ "dev": true,
1047
+ "license": "MIT",
1048
+ "optional": true,
1049
+ "os": [
1050
+ "linux"
1051
+ ]
1052
+ },
1053
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
1054
+ "version": "4.62.2",
1055
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
1056
+ "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
1057
+ "cpu": [
1058
+ "riscv64"
1059
+ ],
1060
+ "dev": true,
1061
+ "license": "MIT",
1062
+ "optional": true,
1063
+ "os": [
1064
+ "linux"
1065
+ ]
1066
+ },
1067
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
1068
+ "version": "4.62.2",
1069
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
1070
+ "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
1071
+ "cpu": [
1072
+ "riscv64"
1073
+ ],
1074
+ "dev": true,
1075
+ "license": "MIT",
1076
+ "optional": true,
1077
+ "os": [
1078
+ "linux"
1079
+ ]
1080
+ },
1081
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
1082
+ "version": "4.62.2",
1083
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
1084
+ "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
1085
+ "cpu": [
1086
+ "s390x"
1087
+ ],
1088
+ "dev": true,
1089
+ "license": "MIT",
1090
+ "optional": true,
1091
+ "os": [
1092
+ "linux"
1093
+ ]
1094
+ },
1095
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
1096
+ "version": "4.62.2",
1097
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
1098
+ "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
1099
+ "cpu": [
1100
+ "x64"
1101
+ ],
1102
+ "dev": true,
1103
+ "license": "MIT",
1104
+ "optional": true,
1105
+ "os": [
1106
+ "linux"
1107
+ ]
1108
+ },
1109
+ "node_modules/@rollup/rollup-linux-x64-musl": {
1110
+ "version": "4.62.2",
1111
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
1112
+ "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
1113
+ "cpu": [
1114
+ "x64"
1115
+ ],
1116
+ "dev": true,
1117
+ "license": "MIT",
1118
+ "optional": true,
1119
+ "os": [
1120
+ "linux"
1121
+ ]
1122
+ },
1123
+ "node_modules/@rollup/rollup-openbsd-x64": {
1124
+ "version": "4.62.2",
1125
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
1126
+ "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
1127
+ "cpu": [
1128
+ "x64"
1129
+ ],
1130
+ "dev": true,
1131
+ "license": "MIT",
1132
+ "optional": true,
1133
+ "os": [
1134
+ "openbsd"
1135
+ ]
1136
+ },
1137
+ "node_modules/@rollup/rollup-openharmony-arm64": {
1138
+ "version": "4.62.2",
1139
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
1140
+ "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
1141
+ "cpu": [
1142
+ "arm64"
1143
+ ],
1144
+ "dev": true,
1145
+ "license": "MIT",
1146
+ "optional": true,
1147
+ "os": [
1148
+ "openharmony"
1149
+ ]
1150
+ },
1151
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1152
+ "version": "4.62.2",
1153
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
1154
+ "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
1155
+ "cpu": [
1156
+ "arm64"
1157
+ ],
1158
+ "dev": true,
1159
+ "license": "MIT",
1160
+ "optional": true,
1161
+ "os": [
1162
+ "win32"
1163
+ ]
1164
+ },
1165
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1166
+ "version": "4.62.2",
1167
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
1168
+ "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
1169
+ "cpu": [
1170
+ "ia32"
1171
+ ],
1172
+ "dev": true,
1173
+ "license": "MIT",
1174
+ "optional": true,
1175
+ "os": [
1176
+ "win32"
1177
+ ]
1178
+ },
1179
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1180
+ "version": "4.62.2",
1181
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
1182
+ "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
1183
+ "cpu": [
1184
+ "x64"
1185
+ ],
1186
+ "dev": true,
1187
+ "license": "MIT",
1188
+ "optional": true,
1189
+ "os": [
1190
+ "win32"
1191
+ ]
1192
+ },
1193
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1194
+ "version": "4.62.2",
1195
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
1196
+ "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
1197
+ "cpu": [
1198
+ "x64"
1199
+ ],
1200
+ "dev": true,
1201
+ "license": "MIT",
1202
+ "optional": true,
1203
+ "os": [
1204
+ "win32"
1205
+ ]
1206
+ },
1207
+ "node_modules/@types/babel__core": {
1208
+ "version": "7.20.5",
1209
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1210
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1211
+ "dev": true,
1212
+ "license": "MIT",
1213
+ "dependencies": {
1214
+ "@babel/parser": "^7.20.7",
1215
+ "@babel/types": "^7.20.7",
1216
+ "@types/babel__generator": "*",
1217
+ "@types/babel__template": "*",
1218
+ "@types/babel__traverse": "*"
1219
+ }
1220
+ },
1221
+ "node_modules/@types/babel__generator": {
1222
+ "version": "7.27.0",
1223
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1224
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1225
+ "dev": true,
1226
+ "license": "MIT",
1227
+ "dependencies": {
1228
+ "@babel/types": "^7.0.0"
1229
+ }
1230
+ },
1231
+ "node_modules/@types/babel__template": {
1232
+ "version": "7.4.4",
1233
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1234
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1235
+ "dev": true,
1236
+ "license": "MIT",
1237
+ "dependencies": {
1238
+ "@babel/parser": "^7.1.0",
1239
+ "@babel/types": "^7.0.0"
1240
+ }
1241
+ },
1242
+ "node_modules/@types/babel__traverse": {
1243
+ "version": "7.28.0",
1244
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1245
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1246
+ "dev": true,
1247
+ "license": "MIT",
1248
+ "dependencies": {
1249
+ "@babel/types": "^7.28.2"
1250
+ }
1251
+ },
1252
+ "node_modules/@types/estree": {
1253
+ "version": "1.0.9",
1254
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
1255
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
1256
+ "dev": true,
1257
+ "license": "MIT"
1258
+ },
1259
+ "node_modules/@types/geojson": {
1260
+ "version": "7946.0.16",
1261
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
1262
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
1263
+ "license": "MIT"
1264
+ },
1265
+ "node_modules/@types/geojson-vt": {
1266
+ "version": "3.2.5",
1267
+ "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz",
1268
+ "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==",
1269
+ "license": "MIT",
1270
+ "dependencies": {
1271
+ "@types/geojson": "*"
1272
+ }
1273
+ },
1274
+ "node_modules/@types/mapbox__point-geometry": {
1275
+ "version": "0.1.4",
1276
+ "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
1277
+ "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==",
1278
+ "license": "MIT"
1279
+ },
1280
+ "node_modules/@types/mapbox__vector-tile": {
1281
+ "version": "1.3.4",
1282
+ "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz",
1283
+ "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==",
1284
+ "license": "MIT",
1285
+ "dependencies": {
1286
+ "@types/geojson": "*",
1287
+ "@types/mapbox__point-geometry": "*",
1288
+ "@types/pbf": "*"
1289
+ }
1290
+ },
1291
+ "node_modules/@types/pbf": {
1292
+ "version": "3.0.5",
1293
+ "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
1294
+ "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==",
1295
+ "license": "MIT"
1296
+ },
1297
+ "node_modules/@types/prop-types": {
1298
+ "version": "15.7.15",
1299
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
1300
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
1301
+ "dev": true,
1302
+ "license": "MIT"
1303
+ },
1304
+ "node_modules/@types/react": {
1305
+ "version": "18.3.31",
1306
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
1307
+ "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
1308
+ "dev": true,
1309
+ "license": "MIT",
1310
+ "dependencies": {
1311
+ "@types/prop-types": "*",
1312
+ "csstype": "^3.2.2"
1313
+ }
1314
+ },
1315
+ "node_modules/@types/react-dom": {
1316
+ "version": "18.3.7",
1317
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
1318
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
1319
+ "dev": true,
1320
+ "license": "MIT",
1321
+ "peerDependencies": {
1322
+ "@types/react": "^18.0.0"
1323
+ }
1324
+ },
1325
+ "node_modules/@types/supercluster": {
1326
+ "version": "7.1.3",
1327
+ "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
1328
+ "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
1329
+ "license": "MIT",
1330
+ "dependencies": {
1331
+ "@types/geojson": "*"
1332
+ }
1333
+ },
1334
+ "node_modules/@vitejs/plugin-react": {
1335
+ "version": "4.7.0",
1336
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
1337
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
1338
+ "dev": true,
1339
+ "license": "MIT",
1340
+ "dependencies": {
1341
+ "@babel/core": "^7.28.0",
1342
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1343
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1344
+ "@rolldown/pluginutils": "1.0.0-beta.27",
1345
+ "@types/babel__core": "^7.20.5",
1346
+ "react-refresh": "^0.17.0"
1347
+ },
1348
+ "engines": {
1349
+ "node": "^14.18.0 || >=16.0.0"
1350
+ },
1351
+ "peerDependencies": {
1352
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1353
+ }
1354
+ },
1355
+ "node_modules/baseline-browser-mapping": {
1356
+ "version": "2.10.41",
1357
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.41.tgz",
1358
+ "integrity": "sha512-WwS7MHhqGHHlaVsqRZnhvCEMS0owDX+SxRlve7JkuH7My1Ara3ZriTmCQupPfYjxMZ8I/tgxtJYr2t7taHaH4A==",
1359
+ "dev": true,
1360
+ "license": "Apache-2.0",
1361
+ "bin": {
1362
+ "baseline-browser-mapping": "dist/cli.cjs"
1363
+ },
1364
+ "engines": {
1365
+ "node": ">=6.0.0"
1366
+ }
1367
+ },
1368
+ "node_modules/browserslist": {
1369
+ "version": "4.28.4",
1370
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz",
1371
+ "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==",
1372
+ "dev": true,
1373
+ "funding": [
1374
+ {
1375
+ "type": "opencollective",
1376
+ "url": "https://opencollective.com/browserslist"
1377
+ },
1378
+ {
1379
+ "type": "tidelift",
1380
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1381
+ },
1382
+ {
1383
+ "type": "github",
1384
+ "url": "https://github.com/sponsors/ai"
1385
+ }
1386
+ ],
1387
+ "license": "MIT",
1388
+ "dependencies": {
1389
+ "baseline-browser-mapping": "^2.10.38",
1390
+ "caniuse-lite": "^1.0.30001799",
1391
+ "electron-to-chromium": "^1.5.376",
1392
+ "node-releases": "^2.0.48",
1393
+ "update-browserslist-db": "^1.2.3"
1394
+ },
1395
+ "bin": {
1396
+ "browserslist": "cli.js"
1397
+ },
1398
+ "engines": {
1399
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1400
+ }
1401
+ },
1402
+ "node_modules/caniuse-lite": {
1403
+ "version": "1.0.30001800",
1404
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz",
1405
+ "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==",
1406
+ "dev": true,
1407
+ "funding": [
1408
+ {
1409
+ "type": "opencollective",
1410
+ "url": "https://opencollective.com/browserslist"
1411
+ },
1412
+ {
1413
+ "type": "tidelift",
1414
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1415
+ },
1416
+ {
1417
+ "type": "github",
1418
+ "url": "https://github.com/sponsors/ai"
1419
+ }
1420
+ ],
1421
+ "license": "CC-BY-4.0"
1422
+ },
1423
+ "node_modules/convert-source-map": {
1424
+ "version": "2.0.0",
1425
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1426
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1427
+ "dev": true,
1428
+ "license": "MIT"
1429
+ },
1430
+ "node_modules/csstype": {
1431
+ "version": "3.2.3",
1432
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1433
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1434
+ "dev": true,
1435
+ "license": "MIT"
1436
+ },
1437
+ "node_modules/debug": {
1438
+ "version": "4.4.3",
1439
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1440
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1441
+ "dev": true,
1442
+ "license": "MIT",
1443
+ "dependencies": {
1444
+ "ms": "^2.1.3"
1445
+ },
1446
+ "engines": {
1447
+ "node": ">=6.0"
1448
+ },
1449
+ "peerDependenciesMeta": {
1450
+ "supports-color": {
1451
+ "optional": true
1452
+ }
1453
+ }
1454
+ },
1455
+ "node_modules/earcut": {
1456
+ "version": "3.2.3",
1457
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.3.tgz",
1458
+ "integrity": "sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==",
1459
+ "license": "ISC"
1460
+ },
1461
+ "node_modules/electron-to-chromium": {
1462
+ "version": "1.5.385",
1463
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.385.tgz",
1464
+ "integrity": "sha512-78sa/M08MNAYHQfjoWMvOlKQqZ0ElhSm/L5HNUc96VZ3b+KvDVnngFm8sYQy0XrhTRgAhggHr5abA7yTvRdo4Q==",
1465
+ "dev": true,
1466
+ "license": "ISC"
1467
+ },
1468
+ "node_modules/esbuild": {
1469
+ "version": "0.21.5",
1470
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
1471
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
1472
+ "dev": true,
1473
+ "hasInstallScript": true,
1474
+ "license": "MIT",
1475
+ "bin": {
1476
+ "esbuild": "bin/esbuild"
1477
+ },
1478
+ "engines": {
1479
+ "node": ">=12"
1480
+ },
1481
+ "optionalDependencies": {
1482
+ "@esbuild/aix-ppc64": "0.21.5",
1483
+ "@esbuild/android-arm": "0.21.5",
1484
+ "@esbuild/android-arm64": "0.21.5",
1485
+ "@esbuild/android-x64": "0.21.5",
1486
+ "@esbuild/darwin-arm64": "0.21.5",
1487
+ "@esbuild/darwin-x64": "0.21.5",
1488
+ "@esbuild/freebsd-arm64": "0.21.5",
1489
+ "@esbuild/freebsd-x64": "0.21.5",
1490
+ "@esbuild/linux-arm": "0.21.5",
1491
+ "@esbuild/linux-arm64": "0.21.5",
1492
+ "@esbuild/linux-ia32": "0.21.5",
1493
+ "@esbuild/linux-loong64": "0.21.5",
1494
+ "@esbuild/linux-mips64el": "0.21.5",
1495
+ "@esbuild/linux-ppc64": "0.21.5",
1496
+ "@esbuild/linux-riscv64": "0.21.5",
1497
+ "@esbuild/linux-s390x": "0.21.5",
1498
+ "@esbuild/linux-x64": "0.21.5",
1499
+ "@esbuild/netbsd-x64": "0.21.5",
1500
+ "@esbuild/openbsd-x64": "0.21.5",
1501
+ "@esbuild/sunos-x64": "0.21.5",
1502
+ "@esbuild/win32-arm64": "0.21.5",
1503
+ "@esbuild/win32-ia32": "0.21.5",
1504
+ "@esbuild/win32-x64": "0.21.5"
1505
+ }
1506
+ },
1507
+ "node_modules/escalade": {
1508
+ "version": "3.2.0",
1509
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1510
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1511
+ "dev": true,
1512
+ "license": "MIT",
1513
+ "engines": {
1514
+ "node": ">=6"
1515
+ }
1516
+ },
1517
+ "node_modules/fsevents": {
1518
+ "version": "2.3.3",
1519
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1520
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1521
+ "dev": true,
1522
+ "hasInstallScript": true,
1523
+ "license": "MIT",
1524
+ "optional": true,
1525
+ "os": [
1526
+ "darwin"
1527
+ ],
1528
+ "engines": {
1529
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1530
+ }
1531
+ },
1532
+ "node_modules/gensync": {
1533
+ "version": "1.0.0-beta.2",
1534
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1535
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1536
+ "dev": true,
1537
+ "license": "MIT",
1538
+ "engines": {
1539
+ "node": ">=6.9.0"
1540
+ }
1541
+ },
1542
+ "node_modules/geojson-vt": {
1543
+ "version": "4.0.3",
1544
+ "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.3.tgz",
1545
+ "integrity": "sha512-jR1MwkLaZGa8Zftct9ZFruyWFrdl9ZyD2OliXNy9Qq5bBPeg5wHVpBQF9p5GjnicSDQqvBVpysxTPKmWdsfWMA==",
1546
+ "license": "ISC"
1547
+ },
1548
+ "node_modules/get-stream": {
1549
+ "version": "6.0.1",
1550
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
1551
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
1552
+ "license": "MIT",
1553
+ "engines": {
1554
+ "node": ">=10"
1555
+ },
1556
+ "funding": {
1557
+ "url": "https://github.com/sponsors/sindresorhus"
1558
+ }
1559
+ },
1560
+ "node_modules/gl-matrix": {
1561
+ "version": "3.4.4",
1562
+ "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
1563
+ "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
1564
+ "license": "MIT"
1565
+ },
1566
+ "node_modules/global-prefix": {
1567
+ "version": "4.0.0",
1568
+ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz",
1569
+ "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==",
1570
+ "license": "MIT",
1571
+ "dependencies": {
1572
+ "ini": "^4.1.3",
1573
+ "kind-of": "^6.0.3",
1574
+ "which": "^4.0.0"
1575
+ },
1576
+ "engines": {
1577
+ "node": ">=16"
1578
+ }
1579
+ },
1580
+ "node_modules/ieee754": {
1581
+ "version": "1.2.1",
1582
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
1583
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
1584
+ "funding": [
1585
+ {
1586
+ "type": "github",
1587
+ "url": "https://github.com/sponsors/feross"
1588
+ },
1589
+ {
1590
+ "type": "patreon",
1591
+ "url": "https://www.patreon.com/feross"
1592
+ },
1593
+ {
1594
+ "type": "consulting",
1595
+ "url": "https://feross.org/support"
1596
+ }
1597
+ ],
1598
+ "license": "BSD-3-Clause"
1599
+ },
1600
+ "node_modules/ini": {
1601
+ "version": "4.1.3",
1602
+ "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz",
1603
+ "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==",
1604
+ "license": "ISC",
1605
+ "engines": {
1606
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
1607
+ }
1608
+ },
1609
+ "node_modules/isexe": {
1610
+ "version": "3.1.5",
1611
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz",
1612
+ "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==",
1613
+ "license": "BlueOak-1.0.0",
1614
+ "engines": {
1615
+ "node": ">=18"
1616
+ }
1617
+ },
1618
+ "node_modules/js-tokens": {
1619
+ "version": "4.0.0",
1620
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1621
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1622
+ "license": "MIT"
1623
+ },
1624
+ "node_modules/jsesc": {
1625
+ "version": "3.1.0",
1626
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1627
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1628
+ "dev": true,
1629
+ "license": "MIT",
1630
+ "bin": {
1631
+ "jsesc": "bin/jsesc"
1632
+ },
1633
+ "engines": {
1634
+ "node": ">=6"
1635
+ }
1636
+ },
1637
+ "node_modules/json-stringify-pretty-compact": {
1638
+ "version": "4.0.0",
1639
+ "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
1640
+ "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
1641
+ "license": "MIT"
1642
+ },
1643
+ "node_modules/json5": {
1644
+ "version": "2.2.3",
1645
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1646
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1647
+ "dev": true,
1648
+ "license": "MIT",
1649
+ "bin": {
1650
+ "json5": "lib/cli.js"
1651
+ },
1652
+ "engines": {
1653
+ "node": ">=6"
1654
+ }
1655
+ },
1656
+ "node_modules/kdbush": {
1657
+ "version": "4.1.0",
1658
+ "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz",
1659
+ "integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==",
1660
+ "license": "ISC"
1661
+ },
1662
+ "node_modules/kind-of": {
1663
+ "version": "6.0.3",
1664
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
1665
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
1666
+ "license": "MIT",
1667
+ "engines": {
1668
+ "node": ">=0.10.0"
1669
+ }
1670
+ },
1671
+ "node_modules/loose-envify": {
1672
+ "version": "1.4.0",
1673
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
1674
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
1675
+ "license": "MIT",
1676
+ "dependencies": {
1677
+ "js-tokens": "^3.0.0 || ^4.0.0"
1678
+ },
1679
+ "bin": {
1680
+ "loose-envify": "cli.js"
1681
+ }
1682
+ },
1683
+ "node_modules/lru-cache": {
1684
+ "version": "5.1.1",
1685
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1686
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1687
+ "dev": true,
1688
+ "license": "ISC",
1689
+ "dependencies": {
1690
+ "yallist": "^3.0.2"
1691
+ }
1692
+ },
1693
+ "node_modules/maplibre-gl": {
1694
+ "version": "4.7.1",
1695
+ "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz",
1696
+ "integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==",
1697
+ "license": "BSD-3-Clause",
1698
+ "dependencies": {
1699
+ "@mapbox/geojson-rewind": "^0.5.2",
1700
+ "@mapbox/jsonlint-lines-primitives": "^2.0.2",
1701
+ "@mapbox/point-geometry": "^0.1.0",
1702
+ "@mapbox/tiny-sdf": "^2.0.6",
1703
+ "@mapbox/unitbezier": "^0.0.1",
1704
+ "@mapbox/vector-tile": "^1.3.1",
1705
+ "@mapbox/whoots-js": "^3.1.0",
1706
+ "@maplibre/maplibre-gl-style-spec": "^20.3.1",
1707
+ "@types/geojson": "^7946.0.14",
1708
+ "@types/geojson-vt": "3.2.5",
1709
+ "@types/mapbox__point-geometry": "^0.1.4",
1710
+ "@types/mapbox__vector-tile": "^1.3.4",
1711
+ "@types/pbf": "^3.0.5",
1712
+ "@types/supercluster": "^7.1.3",
1713
+ "earcut": "^3.0.0",
1714
+ "geojson-vt": "^4.0.2",
1715
+ "gl-matrix": "^3.4.3",
1716
+ "global-prefix": "^4.0.0",
1717
+ "kdbush": "^4.0.2",
1718
+ "murmurhash-js": "^1.0.0",
1719
+ "pbf": "^3.3.0",
1720
+ "potpack": "^2.0.0",
1721
+ "quickselect": "^3.0.0",
1722
+ "supercluster": "^8.0.1",
1723
+ "tinyqueue": "^3.0.0",
1724
+ "vt-pbf": "^3.1.3"
1725
+ },
1726
+ "engines": {
1727
+ "node": ">=16.14.0",
1728
+ "npm": ">=8.1.0"
1729
+ },
1730
+ "funding": {
1731
+ "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
1732
+ }
1733
+ },
1734
+ "node_modules/minimist": {
1735
+ "version": "1.2.8",
1736
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
1737
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
1738
+ "license": "MIT",
1739
+ "funding": {
1740
+ "url": "https://github.com/sponsors/ljharb"
1741
+ }
1742
+ },
1743
+ "node_modules/ms": {
1744
+ "version": "2.1.3",
1745
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1746
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1747
+ "dev": true,
1748
+ "license": "MIT"
1749
+ },
1750
+ "node_modules/murmurhash-js": {
1751
+ "version": "1.0.0",
1752
+ "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
1753
+ "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
1754
+ "license": "MIT"
1755
+ },
1756
+ "node_modules/nanoid": {
1757
+ "version": "3.3.15",
1758
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
1759
+ "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
1760
+ "dev": true,
1761
+ "funding": [
1762
+ {
1763
+ "type": "github",
1764
+ "url": "https://github.com/sponsors/ai"
1765
+ }
1766
+ ],
1767
+ "license": "MIT",
1768
+ "bin": {
1769
+ "nanoid": "bin/nanoid.cjs"
1770
+ },
1771
+ "engines": {
1772
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1773
+ }
1774
+ },
1775
+ "node_modules/node-releases": {
1776
+ "version": "2.0.50",
1777
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz",
1778
+ "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==",
1779
+ "dev": true,
1780
+ "license": "MIT",
1781
+ "engines": {
1782
+ "node": ">=18"
1783
+ }
1784
+ },
1785
+ "node_modules/pbf": {
1786
+ "version": "3.3.0",
1787
+ "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz",
1788
+ "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==",
1789
+ "license": "BSD-3-Clause",
1790
+ "dependencies": {
1791
+ "ieee754": "^1.1.12",
1792
+ "resolve-protobuf-schema": "^2.1.0"
1793
+ },
1794
+ "bin": {
1795
+ "pbf": "bin/pbf"
1796
+ }
1797
+ },
1798
+ "node_modules/picocolors": {
1799
+ "version": "1.1.1",
1800
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1801
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1802
+ "dev": true,
1803
+ "license": "ISC"
1804
+ },
1805
+ "node_modules/postcss": {
1806
+ "version": "8.5.16",
1807
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
1808
+ "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
1809
+ "dev": true,
1810
+ "funding": [
1811
+ {
1812
+ "type": "opencollective",
1813
+ "url": "https://opencollective.com/postcss/"
1814
+ },
1815
+ {
1816
+ "type": "tidelift",
1817
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1818
+ },
1819
+ {
1820
+ "type": "github",
1821
+ "url": "https://github.com/sponsors/ai"
1822
+ }
1823
+ ],
1824
+ "license": "MIT",
1825
+ "dependencies": {
1826
+ "nanoid": "^3.3.12",
1827
+ "picocolors": "^1.1.1",
1828
+ "source-map-js": "^1.2.1"
1829
+ },
1830
+ "engines": {
1831
+ "node": "^10 || ^12 || >=14"
1832
+ }
1833
+ },
1834
+ "node_modules/potpack": {
1835
+ "version": "2.1.0",
1836
+ "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz",
1837
+ "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==",
1838
+ "license": "ISC"
1839
+ },
1840
+ "node_modules/protocol-buffers-schema": {
1841
+ "version": "3.6.1",
1842
+ "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz",
1843
+ "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==",
1844
+ "license": "MIT"
1845
+ },
1846
+ "node_modules/quickselect": {
1847
+ "version": "3.0.0",
1848
+ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
1849
+ "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
1850
+ "license": "ISC"
1851
+ },
1852
+ "node_modules/react": {
1853
+ "version": "18.3.1",
1854
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1855
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1856
+ "license": "MIT",
1857
+ "dependencies": {
1858
+ "loose-envify": "^1.1.0"
1859
+ },
1860
+ "engines": {
1861
+ "node": ">=0.10.0"
1862
+ }
1863
+ },
1864
+ "node_modules/react-dom": {
1865
+ "version": "18.3.1",
1866
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
1867
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1868
+ "license": "MIT",
1869
+ "dependencies": {
1870
+ "loose-envify": "^1.1.0",
1871
+ "scheduler": "^0.23.2"
1872
+ },
1873
+ "peerDependencies": {
1874
+ "react": "^18.3.1"
1875
+ }
1876
+ },
1877
+ "node_modules/react-refresh": {
1878
+ "version": "0.17.0",
1879
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
1880
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
1881
+ "dev": true,
1882
+ "license": "MIT",
1883
+ "engines": {
1884
+ "node": ">=0.10.0"
1885
+ }
1886
+ },
1887
+ "node_modules/resolve-protobuf-schema": {
1888
+ "version": "2.1.0",
1889
+ "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
1890
+ "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
1891
+ "license": "MIT",
1892
+ "dependencies": {
1893
+ "protocol-buffers-schema": "^3.3.1"
1894
+ }
1895
+ },
1896
+ "node_modules/rollup": {
1897
+ "version": "4.62.2",
1898
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
1899
+ "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
1900
+ "dev": true,
1901
+ "license": "MIT",
1902
+ "dependencies": {
1903
+ "@types/estree": "1.0.9"
1904
+ },
1905
+ "bin": {
1906
+ "rollup": "dist/bin/rollup"
1907
+ },
1908
+ "engines": {
1909
+ "node": ">=18.0.0",
1910
+ "npm": ">=8.0.0"
1911
+ },
1912
+ "optionalDependencies": {
1913
+ "@rollup/rollup-android-arm-eabi": "4.62.2",
1914
+ "@rollup/rollup-android-arm64": "4.62.2",
1915
+ "@rollup/rollup-darwin-arm64": "4.62.2",
1916
+ "@rollup/rollup-darwin-x64": "4.62.2",
1917
+ "@rollup/rollup-freebsd-arm64": "4.62.2",
1918
+ "@rollup/rollup-freebsd-x64": "4.62.2",
1919
+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
1920
+ "@rollup/rollup-linux-arm-musleabihf": "4.62.2",
1921
+ "@rollup/rollup-linux-arm64-gnu": "4.62.2",
1922
+ "@rollup/rollup-linux-arm64-musl": "4.62.2",
1923
+ "@rollup/rollup-linux-loong64-gnu": "4.62.2",
1924
+ "@rollup/rollup-linux-loong64-musl": "4.62.2",
1925
+ "@rollup/rollup-linux-ppc64-gnu": "4.62.2",
1926
+ "@rollup/rollup-linux-ppc64-musl": "4.62.2",
1927
+ "@rollup/rollup-linux-riscv64-gnu": "4.62.2",
1928
+ "@rollup/rollup-linux-riscv64-musl": "4.62.2",
1929
+ "@rollup/rollup-linux-s390x-gnu": "4.62.2",
1930
+ "@rollup/rollup-linux-x64-gnu": "4.62.2",
1931
+ "@rollup/rollup-linux-x64-musl": "4.62.2",
1932
+ "@rollup/rollup-openbsd-x64": "4.62.2",
1933
+ "@rollup/rollup-openharmony-arm64": "4.62.2",
1934
+ "@rollup/rollup-win32-arm64-msvc": "4.62.2",
1935
+ "@rollup/rollup-win32-ia32-msvc": "4.62.2",
1936
+ "@rollup/rollup-win32-x64-gnu": "4.62.2",
1937
+ "@rollup/rollup-win32-x64-msvc": "4.62.2",
1938
+ "fsevents": "~2.3.2"
1939
+ }
1940
+ },
1941
+ "node_modules/rw": {
1942
+ "version": "1.3.3",
1943
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
1944
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
1945
+ "license": "BSD-3-Clause"
1946
+ },
1947
+ "node_modules/scheduler": {
1948
+ "version": "0.23.2",
1949
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
1950
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
1951
+ "license": "MIT",
1952
+ "dependencies": {
1953
+ "loose-envify": "^1.1.0"
1954
+ }
1955
+ },
1956
+ "node_modules/semver": {
1957
+ "version": "6.3.1",
1958
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
1959
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
1960
+ "dev": true,
1961
+ "license": "ISC",
1962
+ "bin": {
1963
+ "semver": "bin/semver.js"
1964
+ }
1965
+ },
1966
+ "node_modules/source-map-js": {
1967
+ "version": "1.2.1",
1968
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1969
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1970
+ "dev": true,
1971
+ "license": "BSD-3-Clause",
1972
+ "engines": {
1973
+ "node": ">=0.10.0"
1974
+ }
1975
+ },
1976
+ "node_modules/supercluster": {
1977
+ "version": "8.0.1",
1978
+ "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
1979
+ "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
1980
+ "license": "ISC",
1981
+ "dependencies": {
1982
+ "kdbush": "^4.0.2"
1983
+ }
1984
+ },
1985
+ "node_modules/tinyqueue": {
1986
+ "version": "3.0.0",
1987
+ "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
1988
+ "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
1989
+ "license": "ISC"
1990
+ },
1991
+ "node_modules/typescript": {
1992
+ "version": "5.9.3",
1993
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
1994
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
1995
+ "dev": true,
1996
+ "license": "Apache-2.0",
1997
+ "bin": {
1998
+ "tsc": "bin/tsc",
1999
+ "tsserver": "bin/tsserver"
2000
+ },
2001
+ "engines": {
2002
+ "node": ">=14.17"
2003
+ }
2004
+ },
2005
+ "node_modules/update-browserslist-db": {
2006
+ "version": "1.2.3",
2007
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
2008
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
2009
+ "dev": true,
2010
+ "funding": [
2011
+ {
2012
+ "type": "opencollective",
2013
+ "url": "https://opencollective.com/browserslist"
2014
+ },
2015
+ {
2016
+ "type": "tidelift",
2017
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
2018
+ },
2019
+ {
2020
+ "type": "github",
2021
+ "url": "https://github.com/sponsors/ai"
2022
+ }
2023
+ ],
2024
+ "license": "MIT",
2025
+ "dependencies": {
2026
+ "escalade": "^3.2.0",
2027
+ "picocolors": "^1.1.1"
2028
+ },
2029
+ "bin": {
2030
+ "update-browserslist-db": "cli.js"
2031
+ },
2032
+ "peerDependencies": {
2033
+ "browserslist": ">= 4.21.0"
2034
+ }
2035
+ },
2036
+ "node_modules/vite": {
2037
+ "version": "5.4.21",
2038
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
2039
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
2040
+ "dev": true,
2041
+ "license": "MIT",
2042
+ "dependencies": {
2043
+ "esbuild": "^0.21.3",
2044
+ "postcss": "^8.4.43",
2045
+ "rollup": "^4.20.0"
2046
+ },
2047
+ "bin": {
2048
+ "vite": "bin/vite.js"
2049
+ },
2050
+ "engines": {
2051
+ "node": "^18.0.0 || >=20.0.0"
2052
+ },
2053
+ "funding": {
2054
+ "url": "https://github.com/vitejs/vite?sponsor=1"
2055
+ },
2056
+ "optionalDependencies": {
2057
+ "fsevents": "~2.3.3"
2058
+ },
2059
+ "peerDependencies": {
2060
+ "@types/node": "^18.0.0 || >=20.0.0",
2061
+ "less": "*",
2062
+ "lightningcss": "^1.21.0",
2063
+ "sass": "*",
2064
+ "sass-embedded": "*",
2065
+ "stylus": "*",
2066
+ "sugarss": "*",
2067
+ "terser": "^5.4.0"
2068
+ },
2069
+ "peerDependenciesMeta": {
2070
+ "@types/node": {
2071
+ "optional": true
2072
+ },
2073
+ "less": {
2074
+ "optional": true
2075
+ },
2076
+ "lightningcss": {
2077
+ "optional": true
2078
+ },
2079
+ "sass": {
2080
+ "optional": true
2081
+ },
2082
+ "sass-embedded": {
2083
+ "optional": true
2084
+ },
2085
+ "stylus": {
2086
+ "optional": true
2087
+ },
2088
+ "sugarss": {
2089
+ "optional": true
2090
+ },
2091
+ "terser": {
2092
+ "optional": true
2093
+ }
2094
+ }
2095
+ },
2096
+ "node_modules/vt-pbf": {
2097
+ "version": "3.1.3",
2098
+ "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
2099
+ "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
2100
+ "license": "MIT",
2101
+ "dependencies": {
2102
+ "@mapbox/point-geometry": "0.1.0",
2103
+ "@mapbox/vector-tile": "^1.3.1",
2104
+ "pbf": "^3.2.1"
2105
+ }
2106
+ },
2107
+ "node_modules/which": {
2108
+ "version": "4.0.0",
2109
+ "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
2110
+ "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
2111
+ "license": "ISC",
2112
+ "dependencies": {
2113
+ "isexe": "^3.1.1"
2114
+ },
2115
+ "bin": {
2116
+ "node-which": "bin/which.js"
2117
+ },
2118
+ "engines": {
2119
+ "node": "^16.13.0 || >=18.0.0"
2120
+ }
2121
+ },
2122
+ "node_modules/yallist": {
2123
+ "version": "3.1.1",
2124
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
2125
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
2126
+ "dev": true,
2127
+ "license": "ISC"
2128
+ }
2129
+ }
2130
+ }
frontend/package.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "sat-change-detection-frontend",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc --noEmit && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@fontsource/ibm-plex-mono": "^5.2.7",
13
+ "@fontsource/ibm-plex-sans": "^5.2.8",
14
+ "maplibre-gl": "^4.7.1",
15
+ "react": "^18.3.1",
16
+ "react-dom": "^18.3.1"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^18.3.12",
20
+ "@types/react-dom": "^18.3.1",
21
+ "@vitejs/plugin-react": "^4.3.4",
22
+ "typescript": "^5.6.3",
23
+ "vite": "^5.4.11"
24
+ }
25
+ }
frontend/src/App.tsx ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react";
2
+ import { api } from "./api";
3
+ import CompareView from "./components/CompareView";
4
+ import ModelCard from "./components/ModelCard";
5
+ import type { CuratedPair, ModelSummary, PredictResult } from "./types";
6
+
7
+ type Tab = "curated" | "card";
8
+
9
+ // friendly labels for the served bundle ids (falls back to the raw id)
10
+ const MODEL_LABELS: Record<string, string> = {
11
+ levircd_dinov2: "DINOv2 + LoRA",
12
+ levircd_segformer: "SegFormer MiT-b2",
13
+ };
14
+ const modelLabel = (id: string) => MODEL_LABELS[id] ?? id;
15
+
16
+ // short scene tag from a source string like "LEVIR-CD test_10" -> "T10"
17
+ const sceneTag = (source: string) => {
18
+ const m = source.match(/(\d+)\s*$/);
19
+ return m ? `T${m[1]}` : source.slice(0, 3).toUpperCase();
20
+ };
21
+ // strip the "(test N)" suffix from a pair title for a tighter label
22
+ const sceneTitle = (title: string) => title.replace(/\s*\(test[^)]*\)\s*$/i, "");
23
+ // annotated ground-truth change % from the description, if present
24
+ const annotatedPct = (desc: string): number | null => {
25
+ const m = desc.match(/([\d.]+)\s*%/);
26
+ return m ? parseFloat(m[1]) : null;
27
+ };
28
+ // full-scene tile+stitch inference is seconds on CPU (baked once, then served from cache) — format
29
+ // the real model cost sensibly instead of a raw millisecond count.
30
+ const fmtDuration = (ms: number): { v: string; u: string } =>
31
+ ms >= 1000 ? { v: (ms / 1000).toFixed(ms >= 10000 ? 0 : 1), u: "s" } : { v: String(Math.round(ms)), u: "ms" };
32
+ const fmtDurStr = (ms: number): string => {
33
+ const d = fmtDuration(ms);
34
+ return `${d.v} ${d.u}`;
35
+ };
36
+
37
+ export default function App() {
38
+ const [tab, setTab] = useState<Tab>("curated");
39
+ const [models, setModels] = useState<ModelSummary[]>([]);
40
+ const [pairs, setPairs] = useState<CuratedPair[]>([]);
41
+ const [modelId, setModelId] = useState<string>("");
42
+ const [pairId, setPairId] = useState<string>("");
43
+ const [result, setResult] = useState<PredictResult | null>(null);
44
+ const [busy, setBusy] = useState(false);
45
+ const [error, setError] = useState<string | null>(null);
46
+
47
+ const [showOverlay, setShowOverlay] = useState(true);
48
+ const [opacity, setOpacity] = useState(0.75);
49
+
50
+ useEffect(() => {
51
+ Promise.all([api.models(), api.curated()])
52
+ .then(([m, p]) => {
53
+ setModels(m);
54
+ setPairs(p);
55
+ if (m.length) setModelId(m[0].id);
56
+ if (p.length) setPairId(p[0].id);
57
+ })
58
+ .catch((e) => setError(String(e)));
59
+ }, []);
60
+
61
+ useEffect(() => {
62
+ if (!modelId || !pairId) return;
63
+ let cancelled = false;
64
+ setBusy(true);
65
+ setError(null);
66
+ setResult(null); // clear stale overlay/stats so the new imagery never shows the old mask
67
+ api
68
+ .predict(pairId, modelId)
69
+ .then((r) => {
70
+ if (!cancelled) setResult(r);
71
+ })
72
+ .catch((e) => !cancelled && setError(String(e)))
73
+ .finally(() => !cancelled && setBusy(false));
74
+ return () => {
75
+ cancelled = true;
76
+ };
77
+ }, [modelId, pairId]);
78
+
79
+ const model = models.find((m) => m.id === modelId);
80
+ const pair = pairs.find((p) => p.id === pairId);
81
+ const placeholder = result?.is_placeholder ?? model?.is_placeholder ?? false;
82
+ const healthy = models.length > 0 && !error;
83
+ const activeThreshold = result?.threshold ?? model?.threshold;
84
+
85
+ return (
86
+ <div className="app">
87
+ <header className="topbar">
88
+ <div className="brand">
89
+ <svg className="mark" viewBox="0 0 32 32" fill="none" aria-hidden="true">
90
+ <circle cx="16" cy="16" r="11" stroke="currentColor" strokeWidth="1.3" opacity=".5" />
91
+ <circle cx="16" cy="16" r="4.4" stroke="currentColor" strokeWidth="1.3" />
92
+ <path d="M16 1v7M16 24v7M1 16h7M24 16h7" stroke="currentColor" strokeWidth="1.3" />
93
+ <rect x="13.4" y="13.4" width="5.2" height="5.2" fill="currentColor" />
94
+ </svg>
95
+ <div>
96
+ <h1>Satellite Change Detection</h1>
97
+ <div className="sub">
98
+ TRACK&nbsp;A · AERIAL 0.5&nbsp;M/PX · <b>ONNX · CPU</b>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ <nav className="nav">
103
+ <button className={`tab ${tab === "curated" ? "on" : ""}`} onClick={() => setTab("curated")}>
104
+ Curated
105
+ </button>
106
+ <button className={`tab ${tab === "card" ? "on" : ""}`} onClick={() => setTab("card")}>
107
+ Model card
108
+ </button>
109
+ <div className="status">
110
+ <span className={`dot ${healthy ? "" : "down"}`} />
111
+ {healthy ? "RUNTIME NOMINAL" : "RUNTIME OFFLINE"}
112
+ </div>
113
+ </nav>
114
+ </header>
115
+
116
+ {placeholder && tab === "curated" && (
117
+ <div className="banner">
118
+ <span className="bicon">⚠</span> Serving <strong>placeholder (random-init) weights</strong> — the
119
+ pipeline is real, the predictions are not. Swap in the trained bundle to ship.
120
+ </div>
121
+ )}
122
+ {error && (
123
+ <div className="banner err">
124
+ <span className="bicon">✕</span> {error}
125
+ </div>
126
+ )}
127
+
128
+ {tab === "card" ? (
129
+ <ModelCard />
130
+ ) : (
131
+ <div className="body">
132
+ <aside className="rail">
133
+ <section className="sec">
134
+ <h2>
135
+ <span className="idx">01</span> Scene pair <span className="rule" />
136
+ </h2>
137
+ {pairs.map((p) => {
138
+ const pct = annotatedPct(p.description);
139
+ return (
140
+ <button
141
+ key={p.id}
142
+ className={`scene ${p.id === pairId ? "on" : ""}`}
143
+ onClick={() => setPairId(p.id)}
144
+ title={p.description}
145
+ >
146
+ <span className="tag">{sceneTag(p.source || p.id)}</span>
147
+ <span className="meta">
148
+ <span className="t">{sceneTitle(p.title)}</span>
149
+ <span className="bar">
150
+ <i style={{ width: `${pct ?? 2}%` }} />
151
+ </span>
152
+ </span>
153
+ <span className="pct">{pct != null ? `${pct}%` : "ctrl"}</span>
154
+ </button>
155
+ );
156
+ })}
157
+ <p className="scene-note">
158
+ Real LEVIR-CD test crops · the bar is <b>annotated</b> change (ground truth) · 0.5 m/px
159
+ aerial.
160
+ </p>
161
+ </section>
162
+
163
+ <section className="sec">
164
+ <h2>
165
+ <span className="idx">02</span> Model <span className="rule" />
166
+ </h2>
167
+ <div className="select">
168
+ <select value={modelId} onChange={(e) => setModelId(e.target.value)}>
169
+ {models.map((m) => (
170
+ <option key={m.id} value={m.id}>
171
+ {modelLabel(m.id)}
172
+ {m.is_placeholder ? " (placeholder)" : ""}
173
+ </option>
174
+ ))}
175
+ </select>
176
+ </div>
177
+ {model && (
178
+ <div className="kv">
179
+ <span className="chip">
180
+ grid <b>{model.input_size}²</b>
181
+ </span>
182
+ <span className="chip">{model.fixed_grid ? "fixed grid" : "dynamic H/W"}</span>
183
+ <span className="chip">
184
+ thr <b>{model.threshold.toFixed(3)}</b>
185
+ </span>
186
+ </div>
187
+ )}
188
+ </section>
189
+
190
+ <section className="sec">
191
+ <h2>
192
+ <span className="idx">03</span> Change overlay <span className="rule" />
193
+ </h2>
194
+ <div className="toggle">
195
+ <span className="lbl">Show detected change</span>
196
+ <div
197
+ className={`sw ${showOverlay ? "on" : ""}`}
198
+ role="switch"
199
+ aria-checked={showOverlay}
200
+ onClick={() => setShowOverlay((v) => !v)}
201
+ />
202
+ </div>
203
+ <div className="slabel">
204
+ Opacity <b>{Math.round(opacity * 100)}%</b>
205
+ </div>
206
+ <input
207
+ type="range"
208
+ min={0}
209
+ max={1}
210
+ step={0.01}
211
+ value={opacity}
212
+ style={{ ["--fill" as string]: `${opacity * 100}%` }}
213
+ onChange={(e) => setOpacity(Number(e.target.value))}
214
+ />
215
+ <div className="legend">
216
+ <span className="sq" /> Detected building change
217
+ {activeThreshold != null && <> · thr {activeThreshold.toFixed(3)}</>}
218
+ </div>
219
+ </section>
220
+
221
+ <section className="sec">
222
+ <h2>
223
+ <span className="idx">04</span> Analysis <span className="rule" />
224
+ </h2>
225
+ {result ? (
226
+ <>
227
+ <div className="readout">
228
+ <div className="row">
229
+ <span className="k">Area changed</span>
230
+ <span className="v">
231
+ {result.stats.changed_percent}
232
+ <small>%</small>
233
+ </span>
234
+ </div>
235
+ <div className="row">
236
+ <span className="k">Mean conf · changed</span>
237
+ <span className="v">{result.stats.mean_confidence_changed.toFixed(3)}</span>
238
+ </div>
239
+ <div className="row">
240
+ <span className="k">Changed pixels</span>
241
+ <span className="v">{result.stats.changed_pixels.toLocaleString()}</span>
242
+ </div>
243
+ <div className="row">
244
+ <span className="k">Full-scene inference · CPU</span>
245
+ <span className="v">
246
+ {fmtDuration(result.elapsed_ms).v}
247
+ <small>{fmtDuration(result.elapsed_ms).u}</small>
248
+ </span>
249
+ </div>
250
+ </div>
251
+ <div className="gauge">
252
+ <div className="track">
253
+ <i style={{ width: `${Math.min(100, result.stats.changed_percent)}%` }} />
254
+ </div>
255
+ <div className="cap">
256
+ <span>0%</span>
257
+ <span>AREA CHANGED</span>
258
+ <span>100%</span>
259
+ </div>
260
+ </div>
261
+ <p className="readout-caption">
262
+ {result.n_tiles ?? 16}× 256&nbsp;px tiles, stitched · precomputed, served from
263
+ cache
264
+ </p>
265
+ </>
266
+ ) : busy ? (
267
+ <div className="readout">
268
+ {["Area changed", "Mean conf · changed", "Changed pixels", "Inference · CPU"].map(
269
+ (k) => (
270
+ <div className="row" key={k}>
271
+ <span className="k">{k}</span>
272
+ <span className="v skeleton" />
273
+ </div>
274
+ ),
275
+ )}
276
+ </div>
277
+ ) : (
278
+ <p className="readout-empty">Select a scene to run inference.</p>
279
+ )}
280
+ </section>
281
+ </aside>
282
+
283
+ <main className="stage">
284
+ <div className="viewer">
285
+ {pairId && modelId ? (
286
+ <CompareView
287
+ beforeUrl={api.imageUrl(pairId, "before")}
288
+ afterUrl={api.imageUrl(pairId, "after")}
289
+ overlayUrl={result?.overlay_png ?? null}
290
+ overlayOpacity={opacity}
291
+ showOverlay={showOverlay}
292
+ loading={busy && !result}
293
+ legendLabel={modelLabel(modelId)}
294
+ />
295
+ ) : (
296
+ <div className="empty">Initializing…</div>
297
+ )}
298
+ </div>
299
+
300
+ <footer className="telemetry">
301
+ <div className="cell accent">
302
+ <span className="k">MODEL</span> <b>{modelId || "—"}</b>
303
+ </div>
304
+ <div className="cell">
305
+ <span className="k">INPUT</span>{" "}
306
+ <b>{model ? `${model.input_size}×${model.input_size}` : "—"}</b>
307
+ </div>
308
+ <div className="cell hide-narrow">
309
+ <span className="k">GRID</span> <b>{model?.fixed_grid ? "FIXED" : "DYN H/W"}</b>
310
+ </div>
311
+ <div className="cell">
312
+ <span className="k">THR</span>{" "}
313
+ <b>{activeThreshold != null ? activeThreshold.toFixed(3) : "—"}</b>
314
+ </div>
315
+ <div className="cell">
316
+ <span className="k">INFER</span>{" "}
317
+ <b>{result ? fmtDurStr(result.elapsed_ms) : busy ? "…" : "—"}</b>
318
+ </div>
319
+ <div className="spacer" />
320
+ <div className="cell hide-narrow">
321
+ <span className="k">BANDS</span> <b>{model ? model.band_order.join("") : "—"}</b>
322
+ </div>
323
+ <div className="cell hide-narrow">
324
+ <span className="k">GSD</span> <b>0.5 m/px</b>
325
+ </div>
326
+ <div className="cell">
327
+ <span className="k">SRC</span> <b>{pair?.source || pair?.id || "—"}</b>
328
+ </div>
329
+ </footer>
330
+ </main>
331
+ </div>
332
+ )}
333
+ </div>
334
+ );
335
+ }
frontend/src/api.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { CuratedPair, ModelSummary, PredictResult } from "./types";
2
+
3
+ // Same-origin in production (FastAPI serves this build); vite dev proxies /api to :7860.
4
+ const BASE = "";
5
+
6
+ async function getJSON<T>(path: string): Promise<T> {
7
+ const res = await fetch(`${BASE}${path}`);
8
+ if (!res.ok) throw new Error(`${path} -> ${res.status}`);
9
+ return (await res.json()) as T;
10
+ }
11
+
12
+ export const api = {
13
+ models: () => getJSON<ModelSummary[]>("/api/models"),
14
+ curated: () => getJSON<CuratedPair[]>("/api/curated"),
15
+ card: async (modelId: string): Promise<string> => {
16
+ const res = await fetch(`${BASE}/api/models/${modelId}/card`);
17
+ if (!res.ok) throw new Error(`card ${modelId} -> ${res.status}`);
18
+ return res.text();
19
+ },
20
+ predict: async (pairId: string, modelId: string): Promise<PredictResult> => {
21
+ const res = await fetch(`${BASE}/api/predict`, {
22
+ method: "POST",
23
+ headers: { "Content-Type": "application/json" },
24
+ body: JSON.stringify({ pair_id: pairId, model_id: modelId }),
25
+ });
26
+ if (!res.ok) throw new Error(`predict -> ${res.status}`);
27
+ return (await res.json()) as PredictResult;
28
+ },
29
+ imageUrl: (pairId: string, which: "before" | "after") =>
30
+ `${BASE}/api/curated/${pairId}/${which}.png`,
31
+ };
frontend/src/components/CompareView.tsx ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import maplibregl from "maplibre-gl";
2
+ import { useEffect, useRef, useState } from "react";
3
+
4
+ // A fixed geographic quad to place the (non-geo) image tiles into MapLibre. Square so 256² tiles
5
+ // keep their aspect ratio. TL, TR, BR, BL as [lng, lat].
6
+ const COORDS: [
7
+ [number, number],
8
+ [number, number],
9
+ [number, number],
10
+ [number, number],
11
+ ] = [
12
+ [-0.02, 0.02],
13
+ [0.02, 0.02],
14
+ [0.02, -0.02],
15
+ [-0.02, -0.02],
16
+ ];
17
+ const BOUNDS: [[number, number], [number, number]] = [
18
+ [-0.02, -0.02],
19
+ [0.02, 0.02],
20
+ ];
21
+
22
+ interface Props {
23
+ beforeUrl: string;
24
+ afterUrl: string;
25
+ overlayUrl: string | null;
26
+ overlayOpacity: number;
27
+ showOverlay: boolean;
28
+ loading?: boolean;
29
+ legendLabel?: string;
30
+ }
31
+
32
+ // Cover-fit: fill the whole stage edge-to-edge (no letterbox voids), cropping any overflow. The
33
+ // square image quad is grown until it covers the viewport's LONGER axis; overflow is clipped by
34
+ // .compare-wrap, pan/zoom reaches the cropped areas, and the overlay tracks via map.project().
35
+ // On an ultra-wide stage a 1024px tile can't fill without upscaling — MapLibre's linear resampling
36
+ // keeps that smooth (never blocky), and zooming recovers native pixels.
37
+ function fitCover(map: maplibregl.Map): void {
38
+ const cam = map.cameraForBounds(BOUNDS, { padding: 0 }); // "contain" zoom (fits the shorter axis)
39
+ if (!cam || cam.zoom == null) return;
40
+ const c = map.getContainer();
41
+ const vw = c.clientWidth;
42
+ const vh = c.clientHeight;
43
+ if (!vw || !vh) return;
44
+ // bump the contain zoom by log2(longer/shorter) so the square also covers the longer axis
45
+ const coverBump = Math.log2(Math.max(vw, vh) / Math.min(vw, vh));
46
+ map.jumpTo({ center: cam.center, zoom: cam.zoom + coverBump });
47
+ }
48
+
49
+ function makeMap(container: HTMLDivElement): maplibregl.Map {
50
+ const map = new maplibregl.Map({
51
+ container,
52
+ style: { version: 8, sources: {}, layers: [] },
53
+ center: [0, 0],
54
+ zoom: 12,
55
+ attributionControl: false,
56
+ dragRotate: false,
57
+ pitchWithRotate: false,
58
+ renderWorldCopies: false,
59
+ // pan + zoom are on by default (dragPan / scrollZoom / dblclick / touch); rotate/pitch stay off
60
+ });
61
+ map.on("load", () => fitCover(map));
62
+ return map;
63
+ }
64
+
65
+ // Base imagery as a MapLibre raster image layer. (The change overlay is *not* a MapLibre layer — a
66
+ // second stacked image-source raster proved unreliable to paint; it is an aligned HTML <img>
67
+ // positioned over the map via map.project(), which always renders and gives smooth CSS opacity.)
68
+ function setImage(map: maplibregl.Map, url: string): void {
69
+ const src = map.getSource("img") as maplibregl.ImageSource | undefined;
70
+ if (src) {
71
+ src.updateImage({ url, coordinates: COORDS });
72
+ return;
73
+ }
74
+ map.addSource("img", { type: "image", url, coordinates: COORDS });
75
+ map.addLayer({
76
+ id: "img",
77
+ type: "raster",
78
+ source: "img",
79
+ // explicit linear resampling: smooth photographic interpolation, never nearest-neighbor blocks
80
+ paint: { "raster-opacity": 1, "raster-fade-duration": 0, "raster-resampling": "linear" },
81
+ });
82
+ }
83
+
84
+ export default function CompareView({
85
+ beforeUrl,
86
+ afterUrl,
87
+ overlayUrl,
88
+ overlayOpacity,
89
+ showOverlay,
90
+ loading = false,
91
+ legendLabel,
92
+ }: Props) {
93
+ const beforeDiv = useRef<HTMLDivElement>(null);
94
+ const afterDiv = useRef<HTMLDivElement>(null);
95
+ const beforeMap = useRef<maplibregl.Map | null>(null);
96
+ const afterMap = useRef<maplibregl.Map | null>(null);
97
+ const overlayImg = useRef<HTMLImageElement>(null);
98
+ const syncing = useRef(false);
99
+ const [ready, setReady] = useState(false);
100
+ const [split, setSplit] = useState(50); // divider position, %
101
+ const dragging = useRef(false);
102
+ const wrap = useRef<HTMLDivElement>(null);
103
+
104
+ // Position the overlay <img> to exactly cover the image quad, in the before map's screen space
105
+ // (the two maps are view-synced, so this also lines up under the clipped after map).
106
+ const alignOverlay = () => {
107
+ const map = beforeMap.current;
108
+ const el = overlayImg.current;
109
+ if (!map || !el) return;
110
+ const tl = map.project(COORDS[0]);
111
+ const br = map.project(COORDS[2]);
112
+ el.style.left = `${tl.x}px`;
113
+ el.style.top = `${tl.y}px`;
114
+ el.style.width = `${br.x - tl.x}px`;
115
+ el.style.height = `${br.y - tl.y}px`;
116
+ };
117
+
118
+ // create the two maps once, keep their views in sync, and keep the overlay aligned
119
+ useEffect(() => {
120
+ if (!beforeDiv.current || !afterDiv.current || beforeMap.current) return;
121
+ const bm = makeMap(beforeDiv.current);
122
+ const am = makeMap(afterDiv.current);
123
+ beforeMap.current = bm;
124
+ afterMap.current = am;
125
+ bm.on("error", (e) => console.warn("map error (before):", e.error?.message ?? e));
126
+ am.on("error", (e) => console.warn("map error (after):", e.error?.message ?? e));
127
+
128
+ const sync = (src: maplibregl.Map, dst: maplibregl.Map) => () => {
129
+ if (syncing.current) return;
130
+ syncing.current = true;
131
+ dst.jumpTo({
132
+ center: src.getCenter(),
133
+ zoom: src.getZoom(),
134
+ bearing: src.getBearing(),
135
+ pitch: src.getPitch(),
136
+ });
137
+ syncing.current = false;
138
+ alignOverlay();
139
+ };
140
+ bm.on("move", sync(bm, am));
141
+ am.on("move", sync(am, bm));
142
+ // on container resize, re-cover the stage from the before map; its jumpTo syncs the after map
143
+ bm.on("resize", () => {
144
+ fitCover(bm);
145
+ alignOverlay();
146
+ });
147
+ bm.on("idle", alignOverlay);
148
+
149
+ let loaded = 0;
150
+ const onLoad = () => {
151
+ loaded += 1;
152
+ if (loaded === 2) {
153
+ setReady(true);
154
+ alignOverlay();
155
+ }
156
+ };
157
+ bm.on("load", onLoad);
158
+ am.on("load", onLoad);
159
+
160
+ return () => {
161
+ bm.remove();
162
+ am.remove();
163
+ beforeMap.current = null;
164
+ afterMap.current = null;
165
+ setReady(false);
166
+ };
167
+ }, []);
168
+
169
+ // update imagery when the pair changes
170
+ useEffect(() => {
171
+ if (!ready || !beforeMap.current || !afterMap.current) return;
172
+ setImage(beforeMap.current, beforeUrl);
173
+ setImage(afterMap.current, afterUrl);
174
+ alignOverlay();
175
+ }, [ready, beforeUrl, afterUrl]);
176
+
177
+ // keep the overlay aligned whenever it (re)appears
178
+ useEffect(() => {
179
+ alignOverlay();
180
+ }, [ready, overlayUrl]);
181
+
182
+ // clip the top (after) map to the right of the divider
183
+ useEffect(() => {
184
+ if (afterDiv.current) afterDiv.current.style.clipPath = `inset(0 0 0 ${split}%)`;
185
+ }, [split]);
186
+
187
+ const onPointerMove = (e: React.PointerEvent) => {
188
+ if (!dragging.current || !wrap.current) return;
189
+ const rect = wrap.current.getBoundingClientRect();
190
+ const pct = ((e.clientX - rect.left) / rect.width) * 100;
191
+ setSplit(Math.max(4, Math.min(96, pct)));
192
+ };
193
+
194
+ return (
195
+ <div
196
+ className="compare-wrap"
197
+ ref={wrap}
198
+ onPointerMove={onPointerMove}
199
+ onPointerUp={() => (dragging.current = false)}
200
+ onPointerLeave={() => (dragging.current = false)}
201
+ >
202
+ <div ref={beforeDiv} className="map-layer" />
203
+ <div ref={afterDiv} className="map-layer map-top" />
204
+ <img
205
+ ref={overlayImg}
206
+ className="change-overlay"
207
+ alt=""
208
+ src={overlayUrl ?? ""}
209
+ style={{ opacity: showOverlay && overlayUrl ? overlayOpacity : 0 }}
210
+ />
211
+
212
+ <div className="frame" aria-hidden="true">
213
+ <span className="tl" />
214
+ <span className="tr" />
215
+ <span className="bl" />
216
+ <span className="br" />
217
+ </div>
218
+
219
+ <div className="hud chip before">BEFORE</div>
220
+ <div className="hud chip after">AFTER</div>
221
+ <div className="hud chip legendchip">
222
+ <span className="sq" /> CHANGE MASK{legendLabel ? ` · ${legendLabel}` : ""}
223
+ </div>
224
+ <div className="hud chip gsd">SOURCE GSD 0.5&nbsp;m/px</div>
225
+
226
+ <div
227
+ className="swipe-divider"
228
+ style={{ left: `${split}%` }}
229
+ onPointerDown={(e) => {
230
+ dragging.current = true;
231
+ (e.target as HTMLElement).setPointerCapture?.(e.pointerId);
232
+ }}
233
+ >
234
+ <div className="swipe-handle">⇄</div>
235
+ </div>
236
+
237
+ {loading && (
238
+ <div className="scanning">
239
+ <div className="beam" />
240
+ <span className="tagword">Analyzing</span>
241
+ </div>
242
+ )}
243
+ </div>
244
+ );
245
+ }
frontend/src/components/ModelCard.tsx ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ interface Row {
2
+ model: string;
3
+ params: string;
4
+ f1: string;
5
+ iou: string;
6
+ ap: string;
7
+ note?: string;
8
+ }
9
+
10
+ // Real LEVIR-CD test results (threshold selected on val, applied to test) — from the committed
11
+ // comparison in docs/results/. This is the genuine project story; the served ONNX may be a
12
+ // placeholder until the trained bundles are staged.
13
+ const ROWS: Row[] = [
14
+ { model: "FC-Siam-diff (baseline)", params: "0.83M", f1: "0.886", iou: "0.796", ap: "0.932" },
15
+ {
16
+ model: "Siamese-SegFormer / MiT-b2 (diff)",
17
+ params: "24.72M",
18
+ f1: "0.911",
19
+ iou: "0.836",
20
+ ap: "0.943",
21
+ note: "ImageNet-pretrained strong model",
22
+ },
23
+ {
24
+ model: "DINOv2-base frozen linear-probe",
25
+ params: "1.64M",
26
+ f1: "0.889",
27
+ iou: "0.800",
28
+ ap: "0.924",
29
+ note: "frozen FM features only",
30
+ },
31
+ {
32
+ model: "DINOv2-base + LoRA",
33
+ params: "2.82M",
34
+ f1: "0.913",
35
+ iou: "0.839",
36
+ ap: "0.946",
37
+ note: "foundation-model tier — headline",
38
+ },
39
+ ];
40
+
41
+ export default function ModelCard() {
42
+ return (
43
+ <div className="card-page">
44
+ <div className="card-inner">
45
+ <p className="card-eyebrow">Model card · Track A · high-res aerial</p>
46
+ <h2>Siamese change detection on LEVIR-CD</h2>
47
+ <p className="card-lead">
48
+ Weight-shared Siamese change-detection models on 0.5&nbsp;m RGB aerial imagery. Two dates of
49
+ the same place go in; a per-pixel building-change map comes out. Three tiers are compared on
50
+ the identical LEVIR-CD test split through one evaluation harness.
51
+ </p>
52
+
53
+ <table className="metrics-table">
54
+ <thead>
55
+ <tr>
56
+ <th>Model</th>
57
+ <th>Trainable</th>
58
+ <th>F1</th>
59
+ <th>IoU</th>
60
+ <th>AP</th>
61
+ </tr>
62
+ </thead>
63
+ <tbody>
64
+ {ROWS.map((r) => (
65
+ <tr key={r.model} className={r.note?.includes("headline") ? "row-headline" : ""}>
66
+ <td>
67
+ {r.model}
68
+ {r.note && <span className="row-note">{r.note}</span>}
69
+ </td>
70
+ <td className="num">{r.params}</td>
71
+ <td className="num">{r.f1}</td>
72
+ <td className="num">{r.iou}</td>
73
+ <td className="num">{r.ap}</td>
74
+ </tr>
75
+ ))}
76
+ </tbody>
77
+ </table>
78
+
79
+ <h3>The defensible claim — parameter efficiency</h3>
80
+ <p className="muted">
81
+ DINOv2-base + LoRA reaches F1&nbsp;0.913 with only <strong>2.82M</strong> trainable
82
+ parameters versus the SegFormer strong model&apos;s <strong>24.72M</strong> at F1&nbsp;0.911
83
+ — a statistical tie on accuracy (within noise) at ~9× fewer trainable params. The frozen
84
+ linear-probe (1.64M, decoder only) already ≈ the baseline, so the self-supervised
85
+ representation carries most of the change signal and LoRA supplies the adaptation lift.
86
+ </p>
87
+
88
+ <h3>Honest limitations</h3>
89
+ <ul className="muted">
90
+ <li>
91
+ Per-scene F1 has high variance (mean ≈ 0.77, std ≈ 0.31, min 0.00). The foundation model
92
+ lifts the mean but does <em>not</em> fix the hardest small/subtle-change tiles.
93
+ </li>
94
+ <li>
95
+ Overall pixel accuracy is deliberately not reported: change is a tiny pixel fraction, so
96
+ &quot;predict no change&quot; scores ~99% and is meaningless. Metrics are change-class only.
97
+ </li>
98
+ <li>Trained weights inherit LEVIR-CD research/non-commercial terms — showcase use only.</li>
99
+ </ul>
100
+
101
+ <h3>Domain gap — why the live mode is a different model</h3>
102
+ <div className="callout">
103
+ <p className="muted">
104
+ These models are trained on 0.5&nbsp;m aerial imagery and do <strong>not</strong> transfer
105
+ to 10&nbsp;m Sentinel-2. The live-AOI mode (a later milestone) uses a Sentinel-2-native
106
+ model so it produces meaningful output on satellite scenes — the domain split is a design
107
+ decision, not an afterthought.
108
+ </p>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ );
113
+ }
frontend/src/main.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import App from "./App";
4
+ // Self-hosted fonts (vendored by @fontsource, bundled by Vite) — no external CDN at runtime, so
5
+ // the Docker Space renders identically offline. IBM Plex Sans (UI) + IBM Plex Mono (data/labels).
6
+ import "@fontsource/ibm-plex-sans/300.css";
7
+ import "@fontsource/ibm-plex-sans/400.css";
8
+ import "@fontsource/ibm-plex-sans/500.css";
9
+ import "@fontsource/ibm-plex-sans/600.css";
10
+ import "@fontsource/ibm-plex-mono/400.css";
11
+ import "@fontsource/ibm-plex-mono/500.css";
12
+ import "@fontsource/ibm-plex-mono/600.css";
13
+ import "maplibre-gl/dist/maplibre-gl.css";
14
+ import "./styles.css";
15
+
16
+ ReactDOM.createRoot(document.getElementById("root")!).render(
17
+ <React.StrictMode>
18
+ <App />
19
+ </React.StrictMode>,
20
+ );
frontend/src/styles.css ADDED
@@ -0,0 +1,1016 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================================
2
+ Satellite Change Detection — "Observatory Console"
3
+ Cool, instrument-grade GEOINT theme. Imagery is the hero; chrome recedes.
4
+ Type: IBM Plex Sans (UI) + IBM Plex Mono (labels, coordinates, numbers).
5
+ ========================================================================== */
6
+ :root {
7
+ --bg: #080b10;
8
+ --bg-2: #0b0f16;
9
+ --panel: #0c1119;
10
+ --panel-2: #0f151f;
11
+ --line: #182231;
12
+ --line-2: #101823;
13
+ --text: #dfe7f0;
14
+ --text-dim: #8494a6;
15
+ --text-faint: #54637a;
16
+ --accent: #3fd0e6; /* controlled instrument cyan */
17
+ --accent-deep: #0f3a45;
18
+ --accent-glow: rgba(63, 208, 230, 0.15);
19
+ --signal: #ffb454; /* amber change signal / legend */
20
+ --ok: #5fd39a;
21
+ --warn: #ffcf6b;
22
+ --err: #ff6b6b;
23
+ --radius: 8px;
24
+ --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
25
+ --sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
26
+ }
27
+
28
+ * {
29
+ box-sizing: border-box;
30
+ }
31
+ html,
32
+ body,
33
+ #root {
34
+ height: 100%;
35
+ margin: 0;
36
+ }
37
+ body {
38
+ background: var(--bg);
39
+ color: var(--text);
40
+ font-family: var(--sans);
41
+ font-size: 14px;
42
+ -webkit-font-smoothing: antialiased;
43
+ text-rendering: optimizeLegibility;
44
+ overflow: hidden;
45
+ }
46
+ ::selection {
47
+ background: var(--accent-deep);
48
+ color: #fff;
49
+ }
50
+ .app {
51
+ display: flex;
52
+ flex-direction: column;
53
+ height: 100%;
54
+ }
55
+
56
+ /* ---------- top bar ---------- */
57
+ .topbar {
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: space-between;
61
+ height: 54px;
62
+ flex: none;
63
+ padding: 0 18px;
64
+ border-bottom: 1px solid var(--line);
65
+ background: linear-gradient(180deg, #0c1119, #080b10);
66
+ }
67
+ .brand {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 13px;
71
+ }
72
+ .mark {
73
+ width: 30px;
74
+ height: 30px;
75
+ flex: none;
76
+ color: var(--accent);
77
+ }
78
+ .brand h1 {
79
+ font-size: 13px;
80
+ font-weight: 600;
81
+ margin: 0;
82
+ letter-spacing: 0.14em;
83
+ text-transform: uppercase;
84
+ color: var(--text);
85
+ }
86
+ .brand .sub {
87
+ font-family: var(--mono);
88
+ font-size: 10.5px;
89
+ letter-spacing: 0.08em;
90
+ color: var(--text-faint);
91
+ margin-top: 3px;
92
+ }
93
+ .brand .sub b {
94
+ color: var(--accent);
95
+ font-weight: 500;
96
+ }
97
+ .nav {
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 4px;
101
+ }
102
+ .tab {
103
+ font-family: var(--mono);
104
+ font-size: 11px;
105
+ letter-spacing: 0.06em;
106
+ text-transform: uppercase;
107
+ color: var(--text-dim);
108
+ background: transparent;
109
+ border: 1px solid transparent;
110
+ padding: 8px 13px;
111
+ border-radius: 6px;
112
+ cursor: pointer;
113
+ transition: color 0.15s, background 0.15s;
114
+ }
115
+ .tab:hover {
116
+ color: var(--text);
117
+ }
118
+ .tab.on {
119
+ color: var(--bg);
120
+ background: var(--accent);
121
+ border-color: var(--accent);
122
+ font-weight: 600;
123
+ }
124
+ .status {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 7px;
128
+ margin-left: 10px;
129
+ padding-left: 14px;
130
+ border-left: 1px solid var(--line);
131
+ font-family: var(--mono);
132
+ font-size: 10.5px;
133
+ letter-spacing: 0.06em;
134
+ color: var(--text-dim);
135
+ }
136
+ .dot {
137
+ width: 7px;
138
+ height: 7px;
139
+ border-radius: 50%;
140
+ background: var(--ok);
141
+ box-shadow: 0 0 8px var(--ok);
142
+ animation: pulse 2.6s ease-in-out infinite;
143
+ }
144
+ .dot.down {
145
+ background: var(--err);
146
+ box-shadow: 0 0 8px var(--err);
147
+ }
148
+ @keyframes pulse {
149
+ 0%,
150
+ 100% {
151
+ opacity: 1;
152
+ }
153
+ 50% {
154
+ opacity: 0.4;
155
+ }
156
+ }
157
+
158
+ /* ---------- banners ---------- */
159
+ .banner {
160
+ flex: none;
161
+ padding: 9px 20px;
162
+ font-size: 12.5px;
163
+ font-family: var(--mono);
164
+ letter-spacing: 0.01em;
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 9px;
168
+ background: #2a2410;
169
+ color: var(--warn);
170
+ border-bottom: 1px solid #4a3d16;
171
+ }
172
+ .banner strong {
173
+ color: #fff;
174
+ }
175
+ .banner.err {
176
+ background: #2a1414;
177
+ color: var(--err);
178
+ border-color: #4a1c1c;
179
+ }
180
+ .banner .bicon {
181
+ font-size: 13px;
182
+ }
183
+
184
+ /* ---------- body / rail ---------- */
185
+ .body {
186
+ flex: 1;
187
+ display: flex;
188
+ min-height: 0;
189
+ }
190
+ .rail {
191
+ width: 326px;
192
+ flex: none;
193
+ border-right: 1px solid var(--line);
194
+ background: var(--panel);
195
+ overflow-y: auto;
196
+ }
197
+ .rail::-webkit-scrollbar {
198
+ width: 8px;
199
+ }
200
+ .rail::-webkit-scrollbar-thumb {
201
+ background: #16202e;
202
+ border-radius: 4px;
203
+ }
204
+ .sec {
205
+ padding: 16px 18px;
206
+ border-bottom: 1px solid var(--line-2);
207
+ animation: rise 0.5s cubic-bezier(0.2, 0.7, 0.2, 1) both;
208
+ }
209
+ .sec:nth-child(1) {
210
+ animation-delay: 0.02s;
211
+ }
212
+ .sec:nth-child(2) {
213
+ animation-delay: 0.07s;
214
+ }
215
+ .sec:nth-child(3) {
216
+ animation-delay: 0.12s;
217
+ }
218
+ .sec:nth-child(4) {
219
+ animation-delay: 0.17s;
220
+ }
221
+ @keyframes rise {
222
+ from {
223
+ opacity: 0;
224
+ transform: translateY(7px);
225
+ }
226
+ to {
227
+ opacity: 1;
228
+ transform: none;
229
+ }
230
+ }
231
+ .sec h2 {
232
+ display: flex;
233
+ align-items: center;
234
+ gap: 8px;
235
+ font-family: var(--mono);
236
+ font-size: 10.5px;
237
+ font-weight: 500;
238
+ letter-spacing: 0.18em;
239
+ text-transform: uppercase;
240
+ color: var(--text-faint);
241
+ margin: 0 0 13px;
242
+ }
243
+ .sec h2 .idx {
244
+ color: var(--accent);
245
+ opacity: 0.85;
246
+ }
247
+ .sec h2 .rule {
248
+ flex: 1;
249
+ height: 1px;
250
+ background: var(--line);
251
+ }
252
+
253
+ /* scene list */
254
+ .scene {
255
+ display: flex;
256
+ align-items: center;
257
+ gap: 11px;
258
+ width: 100%;
259
+ text-align: left;
260
+ padding: 9px 10px;
261
+ margin-bottom: 5px;
262
+ border-radius: 7px;
263
+ cursor: pointer;
264
+ background: var(--panel-2);
265
+ border: 1px solid var(--line-2);
266
+ color: var(--text);
267
+ transition: border-color 0.15s, background 0.15s;
268
+ }
269
+ .scene:hover {
270
+ border-color: #22303f;
271
+ }
272
+ .scene.on {
273
+ border-color: var(--accent);
274
+ background: linear-gradient(90deg, var(--accent-glow), transparent 72%);
275
+ }
276
+ .scene .tag {
277
+ font-family: var(--mono);
278
+ font-size: 10px;
279
+ letter-spacing: 0.04em;
280
+ color: var(--text-faint);
281
+ border: 1px solid var(--line);
282
+ border-radius: 4px;
283
+ padding: 3px 5px;
284
+ flex: none;
285
+ min-width: 34px;
286
+ text-align: center;
287
+ }
288
+ .scene.on .tag {
289
+ color: var(--accent);
290
+ border-color: var(--accent-deep);
291
+ }
292
+ .scene .meta {
293
+ flex: 1;
294
+ min-width: 0;
295
+ }
296
+ .scene .meta .t {
297
+ font-size: 12.5px;
298
+ font-weight: 500;
299
+ white-space: nowrap;
300
+ overflow: hidden;
301
+ text-overflow: ellipsis;
302
+ }
303
+ .scene .meta .bar {
304
+ height: 3px;
305
+ border-radius: 2px;
306
+ background: #13202c;
307
+ margin-top: 6px;
308
+ overflow: hidden;
309
+ }
310
+ .scene .meta .bar > i {
311
+ display: block;
312
+ height: 100%;
313
+ background: linear-gradient(90deg, var(--signal), #ff7a59);
314
+ }
315
+ .scene .pct {
316
+ font-family: var(--mono);
317
+ font-size: 10.5px;
318
+ color: var(--text-dim);
319
+ flex: none;
320
+ }
321
+ .scene-note {
322
+ font-family: var(--mono);
323
+ font-size: 10.5px;
324
+ line-height: 1.55;
325
+ color: var(--text-faint);
326
+ margin: 10px 2px 0;
327
+ }
328
+
329
+ /* model select + chips */
330
+ .select {
331
+ position: relative;
332
+ }
333
+ .select select {
334
+ width: 100%;
335
+ appearance: none;
336
+ background: var(--panel-2);
337
+ color: var(--text);
338
+ border: 1px solid var(--line);
339
+ border-radius: 7px;
340
+ padding: 10px 30px 10px 12px;
341
+ font-family: var(--mono);
342
+ font-size: 12px;
343
+ letter-spacing: 0.02em;
344
+ cursor: pointer;
345
+ }
346
+ .select select:focus {
347
+ outline: none;
348
+ border-color: var(--accent);
349
+ box-shadow: 0 0 0 3px var(--accent-glow);
350
+ }
351
+ .select::after {
352
+ content: "▾";
353
+ position: absolute;
354
+ right: 12px;
355
+ top: 9px;
356
+ color: var(--text-dim);
357
+ pointer-events: none;
358
+ }
359
+ .kv {
360
+ display: flex;
361
+ flex-wrap: wrap;
362
+ gap: 6px;
363
+ margin-top: 11px;
364
+ }
365
+ .chip {
366
+ font-family: var(--mono);
367
+ font-size: 10px;
368
+ letter-spacing: 0.03em;
369
+ color: var(--text-dim);
370
+ border: 1px solid var(--line);
371
+ border-radius: 5px;
372
+ padding: 4px 7px;
373
+ background: var(--bg-2);
374
+ }
375
+ .chip b {
376
+ color: var(--accent);
377
+ font-weight: 500;
378
+ }
379
+
380
+ /* overlay controls */
381
+ .toggle {
382
+ display: flex;
383
+ align-items: center;
384
+ justify-content: space-between;
385
+ margin-bottom: 14px;
386
+ }
387
+ .toggle .lbl {
388
+ font-size: 12.5px;
389
+ color: var(--text);
390
+ }
391
+ .sw {
392
+ position: relative;
393
+ width: 38px;
394
+ height: 20px;
395
+ border-radius: 20px;
396
+ background: #182430;
397
+ border: 1px solid var(--line);
398
+ cursor: pointer;
399
+ transition: 0.15s;
400
+ flex: none;
401
+ }
402
+ .sw::after {
403
+ content: "";
404
+ position: absolute;
405
+ top: 2px;
406
+ left: 2px;
407
+ width: 14px;
408
+ height: 14px;
409
+ border-radius: 50%;
410
+ background: var(--text-dim);
411
+ transition: 0.15s;
412
+ }
413
+ .sw.on {
414
+ background: var(--accent-deep);
415
+ border-color: var(--accent);
416
+ }
417
+ .sw.on::after {
418
+ left: 20px;
419
+ background: var(--accent);
420
+ }
421
+ .slabel {
422
+ display: flex;
423
+ justify-content: space-between;
424
+ font-family: var(--mono);
425
+ font-size: 10.5px;
426
+ letter-spacing: 0.06em;
427
+ color: var(--text-faint);
428
+ text-transform: uppercase;
429
+ margin-bottom: 8px;
430
+ }
431
+ .slabel b {
432
+ color: var(--text);
433
+ font-weight: 500;
434
+ }
435
+ input[type="range"] {
436
+ -webkit-appearance: none;
437
+ appearance: none;
438
+ width: 100%;
439
+ height: 3px;
440
+ border-radius: 3px;
441
+ background: linear-gradient(
442
+ 90deg,
443
+ var(--accent) var(--fill, 70%),
444
+ #16212e var(--fill, 70%)
445
+ );
446
+ outline: none;
447
+ cursor: pointer;
448
+ }
449
+ input[type="range"]::-webkit-slider-thumb {
450
+ -webkit-appearance: none;
451
+ width: 14px;
452
+ height: 14px;
453
+ border-radius: 50%;
454
+ background: var(--text);
455
+ border: 2px solid var(--accent);
456
+ box-shadow: 0 0 0 3px var(--accent-glow);
457
+ }
458
+ input[type="range"]::-moz-range-thumb {
459
+ width: 12px;
460
+ height: 12px;
461
+ border-radius: 50%;
462
+ background: var(--text);
463
+ border: 2px solid var(--accent);
464
+ }
465
+ .legend {
466
+ display: flex;
467
+ align-items: center;
468
+ gap: 9px;
469
+ margin-top: 14px;
470
+ font-family: var(--mono);
471
+ font-size: 10.5px;
472
+ color: var(--text-dim);
473
+ }
474
+ .legend .sq {
475
+ width: 12px;
476
+ height: 12px;
477
+ border-radius: 3px;
478
+ background: var(--signal);
479
+ box-shadow: 0 0 8px rgba(255, 180, 84, 0.5);
480
+ flex: none;
481
+ }
482
+
483
+ /* analysis readout */
484
+ .readout {
485
+ font-family: var(--mono);
486
+ }
487
+ .readout .row {
488
+ display: flex;
489
+ align-items: baseline;
490
+ justify-content: space-between;
491
+ padding: 9px 0;
492
+ border-bottom: 1px solid var(--line-2);
493
+ }
494
+ .readout .row:last-child {
495
+ border-bottom: 0;
496
+ }
497
+ .readout .k {
498
+ font-size: 10.5px;
499
+ letter-spacing: 0.06em;
500
+ text-transform: uppercase;
501
+ color: var(--text-faint);
502
+ }
503
+ .readout .v {
504
+ font-size: 16px;
505
+ font-weight: 500;
506
+ color: var(--text);
507
+ font-variant-numeric: tabular-nums;
508
+ }
509
+ .readout .v small {
510
+ font-size: 11px;
511
+ color: var(--text-dim);
512
+ margin-left: 3px;
513
+ }
514
+ .readout .v.skeleton {
515
+ color: transparent;
516
+ border-radius: 4px;
517
+ background: linear-gradient(90deg, #131c28, #1b2836, #131c28);
518
+ background-size: 200% 100%;
519
+ animation: shimmer 1.2s linear infinite;
520
+ min-width: 46px;
521
+ display: inline-block;
522
+ height: 14px;
523
+ }
524
+ @keyframes shimmer {
525
+ to {
526
+ background-position: -200% 0;
527
+ }
528
+ }
529
+ .gauge {
530
+ margin-top: 14px;
531
+ }
532
+ .gauge .track {
533
+ height: 6px;
534
+ border-radius: 4px;
535
+ background: #13202c;
536
+ overflow: hidden;
537
+ }
538
+ .gauge .track > i {
539
+ display: block;
540
+ height: 100%;
541
+ background: linear-gradient(90deg, var(--signal), #ff7a59);
542
+ box-shadow: 0 0 12px rgba(255, 140, 80, 0.4);
543
+ transition: width 0.5s cubic-bezier(0.2, 0.7, 0.2, 1);
544
+ }
545
+ .gauge .cap {
546
+ display: flex;
547
+ justify-content: space-between;
548
+ font-family: var(--mono);
549
+ font-size: 9.5px;
550
+ letter-spacing: 0.05em;
551
+ color: var(--text-faint);
552
+ margin-top: 6px;
553
+ }
554
+ .readout-empty {
555
+ font-family: var(--mono);
556
+ font-size: 11px;
557
+ color: var(--text-faint);
558
+ line-height: 1.6;
559
+ }
560
+ .readout-caption {
561
+ font-family: var(--mono);
562
+ font-size: 10px;
563
+ letter-spacing: 0.03em;
564
+ color: var(--text-faint);
565
+ line-height: 1.5;
566
+ margin: 12px 0 0;
567
+ }
568
+
569
+ /* ---------- stage ---------- */
570
+ .stage {
571
+ flex: 1;
572
+ display: flex;
573
+ flex-direction: column;
574
+ min-width: 0;
575
+ min-height: 0;
576
+ position: relative;
577
+ }
578
+ .viewer {
579
+ position: relative;
580
+ flex: 1;
581
+ min-height: 0;
582
+ }
583
+ .compare-wrap {
584
+ position: absolute;
585
+ inset: 0;
586
+ overflow: hidden;
587
+ background: #04070b;
588
+ touch-action: none;
589
+ }
590
+ .map-layer {
591
+ position: absolute;
592
+ inset: 0;
593
+ }
594
+ .map-top {
595
+ z-index: 2;
596
+ }
597
+ .maplibregl-map {
598
+ background: #04070b;
599
+ }
600
+ .maplibregl-canvas {
601
+ outline: none;
602
+ }
603
+
604
+ /* change overlay — the mask is rendered server-side at native resolution as a translucent amber
605
+ fill + crisp outline. Smooth interpolation (never blocky) + a faint thermal glow. */
606
+ .change-overlay {
607
+ position: absolute;
608
+ z-index: 3;
609
+ pointer-events: none;
610
+ image-rendering: auto;
611
+ filter: drop-shadow(0 0 2px rgba(255, 170, 80, 0.4));
612
+ transition: opacity 0.14s ease;
613
+ }
614
+
615
+ /* HUD */
616
+ .hud {
617
+ position: absolute;
618
+ pointer-events: none;
619
+ font-family: var(--mono);
620
+ z-index: 5;
621
+ }
622
+ .hud.chip {
623
+ padding: 5px 9px;
624
+ border-radius: 6px;
625
+ font-size: 10.5px;
626
+ letter-spacing: 0.1em;
627
+ background: rgba(6, 10, 16, 0.72);
628
+ backdrop-filter: blur(6px);
629
+ -webkit-backdrop-filter: blur(6px);
630
+ border: 1px solid rgba(63, 208, 230, 0.18);
631
+ color: var(--text);
632
+ display: flex;
633
+ align-items: center;
634
+ gap: 8px;
635
+ }
636
+ .hud.chip .muted {
637
+ color: var(--text-faint);
638
+ letter-spacing: 0.04em;
639
+ }
640
+ .hud.before {
641
+ top: 14px;
642
+ left: 14px;
643
+ }
644
+ .hud.after {
645
+ top: 14px;
646
+ right: 14px;
647
+ }
648
+ .hud.legendchip {
649
+ bottom: 14px;
650
+ left: 14px;
651
+ gap: 9px;
652
+ }
653
+ .hud.legendchip .sq {
654
+ width: 11px;
655
+ height: 11px;
656
+ border-radius: 2px;
657
+ background: var(--signal);
658
+ box-shadow: 0 0 8px rgba(255, 180, 84, 0.6);
659
+ }
660
+ .hud.gsd {
661
+ bottom: 14px;
662
+ right: 14px;
663
+ color: rgba(223, 231, 240, 0.75);
664
+ font-size: 10px;
665
+ letter-spacing: 0.08em;
666
+ background: rgba(6, 10, 16, 0.6);
667
+ border-color: rgba(223, 231, 240, 0.14);
668
+ }
669
+ /* frame ticks */
670
+ .frame {
671
+ position: absolute;
672
+ inset: 10px;
673
+ pointer-events: none;
674
+ z-index: 4;
675
+ }
676
+ .frame span {
677
+ position: absolute;
678
+ width: 12px;
679
+ height: 12px;
680
+ border: 1px solid rgba(63, 208, 230, 0.4);
681
+ }
682
+ .frame .tl {
683
+ top: 0;
684
+ left: 0;
685
+ border-right: none;
686
+ border-bottom: none;
687
+ }
688
+ .frame .tr {
689
+ top: 0;
690
+ right: 0;
691
+ border-left: none;
692
+ border-bottom: none;
693
+ }
694
+ .frame .bl {
695
+ bottom: 0;
696
+ left: 0;
697
+ border-right: none;
698
+ border-top: none;
699
+ }
700
+ .frame .br {
701
+ bottom: 0;
702
+ right: 0;
703
+ border-left: none;
704
+ border-top: none;
705
+ }
706
+
707
+ /* swipe divider */
708
+ .swipe-divider {
709
+ position: absolute;
710
+ top: 0;
711
+ bottom: 0;
712
+ width: 2px;
713
+ background: linear-gradient(
714
+ 180deg,
715
+ rgba(63, 208, 230, 0.2),
716
+ var(--accent),
717
+ rgba(63, 208, 230, 0.2)
718
+ );
719
+ z-index: 6;
720
+ transform: translateX(-1px);
721
+ cursor: ew-resize;
722
+ box-shadow: 0 0 12px rgba(63, 208, 230, 0.5);
723
+ }
724
+ .swipe-handle {
725
+ position: absolute;
726
+ top: 50%;
727
+ left: 50%;
728
+ transform: translate(-50%, -50%);
729
+ width: 38px;
730
+ height: 38px;
731
+ border-radius: 50%;
732
+ background: rgba(8, 12, 18, 0.85);
733
+ backdrop-filter: blur(6px);
734
+ -webkit-backdrop-filter: blur(6px);
735
+ border: 1px solid var(--accent);
736
+ color: var(--accent);
737
+ display: flex;
738
+ align-items: center;
739
+ justify-content: center;
740
+ font-family: var(--mono);
741
+ font-size: 13px;
742
+ box-shadow: 0 0 16px rgba(63, 208, 230, 0.35);
743
+ cursor: ew-resize;
744
+ }
745
+
746
+ /* loading scan veil (shown while a fresh prediction runs on CPU) */
747
+ .scanning {
748
+ position: absolute;
749
+ inset: 0;
750
+ z-index: 7;
751
+ pointer-events: none;
752
+ display: flex;
753
+ align-items: center;
754
+ justify-content: center;
755
+ background: rgba(4, 7, 11, 0.35);
756
+ backdrop-filter: blur(1px);
757
+ }
758
+ .scanning .beam {
759
+ position: absolute;
760
+ inset: 0;
761
+ background: linear-gradient(
762
+ 180deg,
763
+ transparent,
764
+ rgba(63, 208, 230, 0.12) 48%,
765
+ rgba(63, 208, 230, 0.18) 50%,
766
+ rgba(63, 208, 230, 0.12) 52%,
767
+ transparent
768
+ );
769
+ transform: translateY(-100%);
770
+ animation: sweep 1.5s cubic-bezier(0.4, 0, 0.2, 1) infinite;
771
+ }
772
+ @keyframes sweep {
773
+ to {
774
+ transform: translateY(100%);
775
+ }
776
+ }
777
+ .scanning .tagword {
778
+ font-family: var(--mono);
779
+ font-size: 11px;
780
+ letter-spacing: 0.3em;
781
+ color: var(--accent);
782
+ text-transform: uppercase;
783
+ padding: 8px 16px;
784
+ border: 1px solid rgba(63, 208, 230, 0.3);
785
+ border-radius: 6px;
786
+ background: rgba(6, 10, 16, 0.7);
787
+ box-shadow: 0 0 20px rgba(63, 208, 230, 0.2);
788
+ }
789
+ .scanning .tagword::after {
790
+ content: "";
791
+ animation: dots 1.2s steps(4, end) infinite;
792
+ }
793
+ @keyframes dots {
794
+ 0% {
795
+ content: "";
796
+ }
797
+ 25% {
798
+ content: "·";
799
+ }
800
+ 50% {
801
+ content: "··";
802
+ }
803
+ 75% {
804
+ content: "···";
805
+ }
806
+ }
807
+
808
+ .empty {
809
+ position: absolute;
810
+ inset: 0;
811
+ display: flex;
812
+ align-items: center;
813
+ justify-content: center;
814
+ color: var(--text-faint);
815
+ font-family: var(--mono);
816
+ font-size: 12px;
817
+ letter-spacing: 0.1em;
818
+ text-transform: uppercase;
819
+ }
820
+
821
+ /* telemetry footer */
822
+ .telemetry {
823
+ height: 30px;
824
+ flex: none;
825
+ display: flex;
826
+ align-items: center;
827
+ border-top: 1px solid var(--line);
828
+ background: var(--panel);
829
+ font-family: var(--mono);
830
+ font-size: 10.5px;
831
+ letter-spacing: 0.05em;
832
+ color: var(--text-dim);
833
+ overflow: hidden;
834
+ }
835
+ .telemetry .cell {
836
+ padding: 0 14px;
837
+ height: 100%;
838
+ display: flex;
839
+ align-items: center;
840
+ gap: 7px;
841
+ border-right: 1px solid var(--line-2);
842
+ white-space: nowrap;
843
+ }
844
+ .telemetry .cell .k {
845
+ color: var(--text-faint);
846
+ }
847
+ .telemetry .cell b {
848
+ color: var(--text);
849
+ font-weight: 500;
850
+ }
851
+ .telemetry .cell.accent b {
852
+ color: var(--accent);
853
+ }
854
+ .telemetry .spacer {
855
+ flex: 1;
856
+ }
857
+ .telemetry .cell.hide-narrow {
858
+ display: flex;
859
+ }
860
+
861
+ /* ---------- model card ---------- */
862
+ .card-page {
863
+ flex: 1;
864
+ overflow-y: auto;
865
+ background: radial-gradient(
866
+ 1100px 500px at 80% -10%,
867
+ rgba(63, 208, 230, 0.05),
868
+ transparent 60%
869
+ ),
870
+ var(--bg);
871
+ }
872
+ .card-inner {
873
+ max-width: 900px;
874
+ margin: 0 auto;
875
+ padding: 42px 40px 64px;
876
+ animation: rise 0.5s cubic-bezier(0.2, 0.7, 0.2, 1) both;
877
+ }
878
+ .card-eyebrow {
879
+ font-family: var(--mono);
880
+ font-size: 10.5px;
881
+ letter-spacing: 0.2em;
882
+ text-transform: uppercase;
883
+ color: var(--accent);
884
+ margin: 0 0 12px;
885
+ }
886
+ .card-page h2 {
887
+ font-size: 26px;
888
+ font-weight: 600;
889
+ letter-spacing: -0.01em;
890
+ margin: 0 0 12px;
891
+ }
892
+ .card-lead {
893
+ color: var(--text-dim);
894
+ font-size: 14.5px;
895
+ line-height: 1.65;
896
+ max-width: 68ch;
897
+ margin: 0 0 30px;
898
+ }
899
+ .card-page h3 {
900
+ font-size: 12px;
901
+ font-family: var(--mono);
902
+ font-weight: 500;
903
+ letter-spacing: 0.14em;
904
+ text-transform: uppercase;
905
+ color: var(--text-faint);
906
+ margin: 34px 0 14px;
907
+ display: flex;
908
+ align-items: center;
909
+ gap: 10px;
910
+ }
911
+ .card-page h3::after {
912
+ content: "";
913
+ flex: 1;
914
+ height: 1px;
915
+ background: var(--line);
916
+ }
917
+ .metrics-table {
918
+ width: 100%;
919
+ border-collapse: collapse;
920
+ margin: 6px 0 8px;
921
+ font-size: 13.5px;
922
+ border: 1px solid var(--line);
923
+ border-radius: 10px;
924
+ overflow: hidden;
925
+ }
926
+ .metrics-table th,
927
+ .metrics-table td {
928
+ text-align: left;
929
+ padding: 12px 14px;
930
+ border-bottom: 1px solid var(--line-2);
931
+ }
932
+ .metrics-table thead th {
933
+ color: var(--text-faint);
934
+ font-weight: 500;
935
+ font-size: 10px;
936
+ font-family: var(--mono);
937
+ text-transform: uppercase;
938
+ letter-spacing: 0.12em;
939
+ background: var(--panel);
940
+ }
941
+ .metrics-table td.num {
942
+ font-family: var(--mono);
943
+ font-variant-numeric: tabular-nums;
944
+ color: var(--text);
945
+ }
946
+ .metrics-table tbody tr:last-child td {
947
+ border-bottom: 0;
948
+ }
949
+ .metrics-table tr.row-headline {
950
+ background: linear-gradient(90deg, var(--accent-glow), transparent 80%);
951
+ }
952
+ .metrics-table tr.row-headline td:first-child {
953
+ box-shadow: inset 3px 0 0 var(--accent);
954
+ }
955
+ .metrics-table tr.row-headline td.num {
956
+ color: var(--accent);
957
+ font-weight: 600;
958
+ }
959
+ .row-note {
960
+ display: block;
961
+ font-size: 11px;
962
+ color: var(--text-faint);
963
+ margin-top: 3px;
964
+ }
965
+ .card-page p.muted,
966
+ .card-page ul.muted {
967
+ color: var(--text-dim);
968
+ font-size: 13.5px;
969
+ line-height: 1.7;
970
+ }
971
+ .card-page ul.muted {
972
+ padding-left: 18px;
973
+ }
974
+ .card-page ul.muted li {
975
+ margin-bottom: 9px;
976
+ }
977
+ .card-page ul.muted li::marker {
978
+ color: var(--accent);
979
+ }
980
+ .card-page strong {
981
+ color: var(--text);
982
+ }
983
+ .callout {
984
+ border: 1px solid var(--line);
985
+ border-left: 2px solid var(--signal);
986
+ background: var(--panel);
987
+ border-radius: 8px;
988
+ padding: 16px 18px;
989
+ margin: 8px 0;
990
+ }
991
+ .callout p {
992
+ margin: 0;
993
+ }
994
+
995
+ /* ---------- responsive ---------- */
996
+ @media (max-width: 980px) {
997
+ .telemetry .cell.hide-narrow {
998
+ display: none;
999
+ }
1000
+ }
1001
+ @media (max-width: 820px) {
1002
+ body {
1003
+ overflow: auto;
1004
+ }
1005
+ .body {
1006
+ flex-direction: column;
1007
+ }
1008
+ .rail {
1009
+ width: 100%;
1010
+ border-right: 0;
1011
+ border-bottom: 1px solid var(--line);
1012
+ }
1013
+ .viewer {
1014
+ min-height: 62vh;
1015
+ }
1016
+ }
frontend/src/types.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface ModelSummary {
2
+ id: string;
3
+ input_size: number;
4
+ dynamic_hw: boolean;
5
+ threshold: number;
6
+ band_order: string[];
7
+ is_placeholder: boolean;
8
+ fixed_grid: boolean;
9
+ }
10
+
11
+ export interface CuratedPair {
12
+ id: string;
13
+ title: string;
14
+ description: string;
15
+ source: string;
16
+ width: number;
17
+ height: number;
18
+ }
19
+
20
+ export interface PredictStats {
21
+ changed_fraction: number;
22
+ changed_percent: number;
23
+ mean_confidence_changed: number;
24
+ mean_confidence_overall: number;
25
+ changed_pixels: number;
26
+ total_pixels: number;
27
+ }
28
+
29
+ export interface PredictResult {
30
+ overlay_png: string;
31
+ threshold: number;
32
+ is_placeholder: boolean;
33
+ stats: PredictStats;
34
+ elapsed_ms: number;
35
+ input_size: number;
36
+ pair_id: string;
37
+ model_id: string;
38
+ n_tiles?: number;
39
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2021",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2021", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "moduleDetection": "force",
13
+ "noEmit": true,
14
+ "jsx": "react-jsx",
15
+ "strict": true,
16
+ "noUnusedLocals": true,
17
+ "noUnusedParameters": true,
18
+ "noFallthroughCasesInSwitch": true
19
+ },
20
+ "include": ["src"]
21
+ }
frontend/vite.config.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import react from "@vitejs/plugin-react";
2
+ import { defineConfig } from "vite";
3
+
4
+ // Dev server proxies /api to the FastAPI backend on :7860 so `npm run dev` works against it.
5
+ // In production the same FastAPI process serves this build as static files (single origin).
6
+ export default defineConfig({
7
+ plugins: [react()],
8
+ server: {
9
+ port: 5173,
10
+ proxy: {
11
+ "/api": "http://localhost:7860",
12
+ },
13
+ },
14
+ build: {
15
+ outDir: "dist",
16
+ sourcemap: false,
17
+ },
18
+ });