diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bdc45dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +compose/.env +web/node_modules +web/dist +web/.env +web/.env.local +web/*.tsbuildinfo +.DS_Store +db_forgejo.txt diff --git a/README.md b/README.md index de66cc5..a36c3e2 100644 --- a/README.md +++ b/README.md @@ -1 +1,83 @@ -This is the site for tracksolid deckgl ui +# deckgl_tracksolid + +Public dashboard that animates a day's vehicle trips from `tracksolid_db`, +colored and filtered by `cost_centre`. Stack: deck.gl `TripsLayer` over +Mapbox GL, fed by PostgREST in front of TimescaleDB. + +``` +React + deck.gl SPA → PostgREST → tracksolid_db (Timescale) + (web/) (compose/) public.trips_viz_v1 + public.trips_for_day(date, text[]) + public.trips_for_range(date, date, text[]) + public.list_cost_centres() +``` + +The full design lives in `trips_deckgl_tracksolid.md` — read that first. + +## Layout + +| Path | Purpose | +| --- | --- | +| `db/migrations/` | Five SQL files. Idempotent. Apply in order. Add only `public.*` objects. | +| `compose/` | `docker-compose.yaml` for the PostgREST API + the built web container. | +| `web/` | Vite + React + TS SPA. Renders `TripsLayer` + `PathLayer` over Mapbox. | +| `trips_deckgl_tracksolid.md` | Design doc / plan. | + +## First-time setup + +1. **Apply migrations** to `tracksolid_db`: + ```bash + psql "$ADMIN_DSN" \ + -f db/migrations/001_viz_anon_role.sql \ + -f db/migrations/002_trips_viz_view.sql \ + -f db/migrations/003_trips_for_day_rpc.sql \ + -f db/migrations/004_trips_for_range_rpc.sql \ + -f db/migrations/005_list_cost_centres_rpc.sql + ``` +2. **Set the `viz_anon` login password** out of band (do not commit): + ```sql + ALTER ROLE viz_anon LOGIN PASSWORD ''; + ``` +3. **Configure env**: + ```bash + cp compose/.env.example compose/.env + # fill in VIZ_DATA_PASSWORD, VITE_MAPBOX_TOKEN, … + cp web/.env.example web/.env.local + # fill in VITE_API_URL (e.g. http://localhost:3000) and VITE_MAPBOX_TOKEN + ``` + +## Local development + +```bash +# 1. Start PostgREST against the staging DB +cd compose && docker compose up postgrest + +# 2. Run the web app against it +cd web && pnpm install && pnpm dev +# → http://localhost:5173 +``` + +The web app reads `VITE_API_URL` (default `http://localhost:3000`) and +`VITE_MAPBOX_TOKEN`. Without a Mapbox token the deck.gl layers still render +on a black background — fine for testing data, ugly for demos. + +## Production deployment (Coolify) + +Two services live alongside the existing Dekart stack: + +- `postgrest` — read-only API at `api.trips.rahamafresh.com`. CORS allow-listed + to the web FQDN. +- `trips_web` — Caddy serving the built SPA at `trips.rahamafresh.com`. + +Both inherit env vars from the Coolify resource panel — see `compose/.env.example` +for the canonical list. + +## Security boundary + +The `viz_anon` role has **no grants on `tracksolid.*`** — verified in setup. It +can only call the four RPCs in `public`, which run `SECURITY DEFINER` and only +expose the columns listed in `trips_viz_v1`. Direct queries against any +`tracksolid` table return `permission denied for schema tracksolid`. + +If the public dashboard ever needs to be locked down, add Traefik basic-auth in +Coolify (same pattern Dekart uses) — no app-level changes needed. diff --git a/compose/.env.example b/compose/.env.example new file mode 100644 index 0000000..78d2cf6 --- /dev/null +++ b/compose/.env.example @@ -0,0 +1,22 @@ +# Trips visualization stack — copy to compose/.env (gitignored) for local dev, +# or set these in the Coolify resource's Environment Variables tab for prod. + +# --- Database (read-only role created by db/migrations/001_viz_anon_role.sql) --- +VIZ_DATA_USER=viz_anon +VIZ_DATA_PASSWORD=replace-me-with-the-password-from-ALTER-ROLE + +# Host where tracksolid_db is reachable. +# Local Mac dev: stage.rahamafresh.com +# Coolify prod : host.docker.internal (the Coolify host's gateway) +VIZ_DB_HOST=host.docker.internal +VIZ_DB_PORT=5433 +VIZ_DB_NAME=tracksolid_db + +# --- PostgREST public URL (for OpenAPI links + CORS preflight reflection) --- +PGRST_PUBLIC_URL=https://api.trips.rahamafresh.com + +# --- Web app --- +TRIPS_WEB_URL=https://trips.rahamafresh.com +VITE_API_URL=https://api.trips.rahamafresh.com +# Reuse DEKART_MAPBOX_TOKEN from the existing Dekart stack — same token is fine. +VITE_MAPBOX_TOKEN=pk.xxxxxxxxxxxxxxxxxxxxxxx diff --git a/compose/docker-compose.yaml b/compose/docker-compose.yaml new file mode 100644 index 0000000..c061481 --- /dev/null +++ b/compose/docker-compose.yaml @@ -0,0 +1,37 @@ +services: + postgrest: + image: postgrest/postgrest:v12.2.3 + restart: always + expose: + - "3000" + extra_hosts: + # In Coolify the DB is reached via host.docker.internal; locally we override + # VIZ_DB_HOST in .env to stage.rahamafresh.com. + - "host.docker.internal:host-gateway" + environment: + PGRST_DB_URI: postgres://${VIZ_DATA_USER:-viz_anon}:${VIZ_DATA_PASSWORD}@${VIZ_DB_HOST:-host.docker.internal}:${VIZ_DB_PORT:-5433}/${VIZ_DB_NAME:-tracksolid_db}?sslmode=disable + PGRST_DB_SCHEMAS: public + PGRST_DB_ANON_ROLE: ${VIZ_DATA_USER:-viz_anon} + PGRST_DB_MAX_ROWS: "10000" + PGRST_OPENAPI_SERVER_PROXY_URI: ${PGRST_PUBLIC_URL:-http://localhost:3000} + PGRST_SERVER_CORS_ALLOWED_ORIGINS: ${TRIPS_WEB_URL:-http://localhost:5173} + # Local-dev convenience: bind to host port 3000 so the React dev server + # can hit it without going through Caddy. Coolify deployments should + # remove this and route via the platform's reverse proxy instead. + ports: + - "3000:3000" + + trips_web: + build: + context: ../web + dockerfile: Dockerfile + args: + VITE_API_URL: ${VITE_API_URL:-https://api.trips.rahamafresh.com} + VITE_MAPBOX_TOKEN: ${VITE_MAPBOX_TOKEN} + restart: always + expose: + - "8080" + depends_on: + - postgrest + # No host port binding for production. For local dev, prefer running + # `pnpm dev` directly against the postgrest service above. diff --git a/db/migrations/001_viz_anon_role.sql b/db/migrations/001_viz_anon_role.sql new file mode 100644 index 0000000..da11fb7 --- /dev/null +++ b/db/migrations/001_viz_anon_role.sql @@ -0,0 +1,16 @@ +-- 001_viz_anon_role.sql +-- Creates the read-only role used by PostgREST as its anonymous identity. +-- Only granted on the public schema; never on tracksolid.* directly. +-- +-- After running, set a login password out-of-band (do NOT commit): +-- ALTER ROLE viz_anon LOGIN PASSWORD ''; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'viz_anon') THEN + CREATE ROLE viz_anon NOLOGIN; + END IF; +END +$$; + +GRANT USAGE ON SCHEMA public TO viz_anon; diff --git a/db/migrations/002_trips_viz_view.sql b/db/migrations/002_trips_viz_view.sql new file mode 100644 index 0000000..d4a3a5c --- /dev/null +++ b/db/migrations/002_trips_viz_view.sql @@ -0,0 +1,25 @@ +-- 002_trips_viz_view.sql +-- Thin metadata view: one row per trip, joined to its device's cost_centre. +-- No path here — path is built per-trip inside the RPCs (003/004) so a +-- naive SELECT * never triggers an ST_MakeLine across the whole hypertable. +-- +-- 'Unassigned' bucket: NULL or whitespace-only cost_centre is coalesced. + +CREATE OR REPLACE VIEW public.trips_viz_v1 AS +SELECT + t.id AS trip_id, + t.imei, + d.vehicle_name, + d.vehicle_number, + COALESCE(NULLIF(TRIM(d.cost_centre), ''), 'Unassigned') AS cost_centre, + t.start_time, + t.end_time, + t.distance_km, + t.avg_speed_kmh, + t.max_speed_kmh +FROM tracksolid.trips t +JOIN tracksolid.devices d ON d.imei = t.imei +WHERE t.start_time IS NOT NULL + AND t.end_time IS NOT NULL; + +GRANT SELECT ON public.trips_viz_v1 TO viz_anon; diff --git a/db/migrations/003_trips_for_day_rpc.sql b/db/migrations/003_trips_for_day_rpc.sql new file mode 100644 index 0000000..23ad0be --- /dev/null +++ b/db/migrations/003_trips_for_day_rpc.sql @@ -0,0 +1,62 @@ +-- 003_trips_for_day_rpc.sql +-- Returns one row per trip on p_date, with the path reconstructed from +-- tracksolid.position_history points falling inside the trip window. +-- +-- path_geojson : LineString GeoJSON, deck.gl reads {type, coordinates} directly. +-- timestamps_rel: integer seconds elapsed since trip start, one per vertex. +-- Frontend offsets these to "seconds since 00:00 of selected date" +-- so a single TripsLayer can animate every trip on one timeline. +-- +-- HAVING count >= 2 drops trips with too few points to draw a line (rare, +-- but happens for very short trips that started/ended between GPS pings). + +CREATE OR REPLACE FUNCTION public.trips_for_day( + p_date date, + p_cost_centres text[] DEFAULT NULL +) RETURNS TABLE ( + trip_id bigint, + imei text, + vehicle_name text, + vehicle_number text, + cost_centre text, + start_time timestamptz, + end_time timestamptz, + distance_km numeric, + path_geojson json, + timestamps_rel int[] +) +LANGUAGE sql STABLE +SECURITY DEFINER +SET search_path = public, tracksolid +AS $$ + WITH day_trips AS ( + SELECT v.* + FROM public.trips_viz_v1 v + WHERE v.start_time::date = p_date + AND (p_cost_centres IS NULL OR v.cost_centre = ANY(p_cost_centres)) + ) + SELECT + dt.trip_id, + dt.imei, + dt.vehicle_name, + dt.vehicle_number, + dt.cost_centre, + dt.start_time, + dt.end_time, + dt.distance_km, + ST_AsGeoJSON(ST_MakeLine(ph.geom ORDER BY ph.gps_time))::json AS path_geojson, + array_agg( + EXTRACT(EPOCH FROM ph.gps_time - dt.start_time)::int + ORDER BY ph.gps_time + ) AS timestamps_rel + FROM day_trips dt + JOIN tracksolid.position_history ph + ON ph.imei = dt.imei + AND ph.gps_time BETWEEN dt.start_time AND dt.end_time + AND ph.geom IS NOT NULL + GROUP BY dt.trip_id, dt.imei, dt.vehicle_name, dt.vehicle_number, + dt.cost_centre, dt.start_time, dt.end_time, dt.distance_km + HAVING count(ph.geom) >= 2; +$$; + +GRANT EXECUTE ON FUNCTION public.trips_for_day(date, text[]) TO viz_anon; diff --git a/db/migrations/004_trips_for_range_rpc.sql b/db/migrations/004_trips_for_range_rpc.sql new file mode 100644 index 0000000..7946f9c --- /dev/null +++ b/db/migrations/004_trips_for_range_rpc.sql @@ -0,0 +1,65 @@ +-- 004_trips_for_range_rpc.sql +-- Multi-day variant of trips_for_day. The 14-day cap is enforced here in SQL +-- so PostgREST can't be tricked into asking for an absurd range via direct RPC. + +CREATE OR REPLACE FUNCTION public.trips_for_range( + p_start date, + p_end date, + p_cost_centres text[] DEFAULT NULL +) RETURNS TABLE ( + trip_id bigint, + imei text, + vehicle_name text, + vehicle_number text, + cost_centre text, + start_time timestamptz, + end_time timestamptz, + distance_km numeric, + path_geojson json, + timestamps_rel int[] +) +LANGUAGE plpgsql STABLE +SECURITY DEFINER +SET search_path = public, tracksolid +AS $$ +BEGIN + IF p_start IS NULL OR p_end IS NULL OR p_end < p_start THEN + RAISE EXCEPTION 'Invalid date range: p_start=%, p_end=%', p_start, p_end; + END IF; + IF (p_end - p_start) > 13 THEN + RAISE EXCEPTION 'Range too large: max 14 days, got %', (p_end - p_start + 1); + END IF; + + RETURN QUERY + WITH range_trips AS ( + SELECT v.* + FROM public.trips_viz_v1 v + WHERE v.start_time::date BETWEEN p_start AND p_end + AND (p_cost_centres IS NULL OR v.cost_centre = ANY(p_cost_centres)) + ) + SELECT + rt.trip_id, + rt.imei, + rt.vehicle_name, + rt.vehicle_number, + rt.cost_centre, + rt.start_time, + rt.end_time, + rt.distance_km, + ST_AsGeoJSON(ST_MakeLine(ph.geom ORDER BY ph.gps_time))::json AS path_geojson, + array_agg( + EXTRACT(EPOCH FROM ph.gps_time - rt.start_time)::int + ORDER BY ph.gps_time + ) AS timestamps_rel + FROM range_trips rt + JOIN tracksolid.position_history ph + ON ph.imei = rt.imei + AND ph.gps_time BETWEEN rt.start_time AND rt.end_time + AND ph.geom IS NOT NULL + GROUP BY rt.trip_id, rt.imei, rt.vehicle_name, rt.vehicle_number, + rt.cost_centre, rt.start_time, rt.end_time, rt.distance_km + HAVING count(ph.geom) >= 2; +END; +$$; + +GRANT EXECUTE ON FUNCTION public.trips_for_range(date, date, text[]) TO viz_anon; diff --git a/db/migrations/005_list_cost_centres_rpc.sql b/db/migrations/005_list_cost_centres_rpc.sql new file mode 100644 index 0000000..6c937a9 --- /dev/null +++ b/db/migrations/005_list_cost_centres_rpc.sql @@ -0,0 +1,22 @@ +-- 005_list_cost_centres_rpc.sql +-- Lookup for the sidebar multi-select. Returns each cost_centre with its +-- vehicle count, ordered by descending count so the busiest groups appear first. + +CREATE OR REPLACE FUNCTION public.list_cost_centres() +RETURNS TABLE ( + cost_centre text, + vehicle_count bigint +) +LANGUAGE sql STABLE +SECURITY DEFINER +SET search_path = public, tracksolid +AS $$ + SELECT + COALESCE(NULLIF(TRIM(d.cost_centre), ''), 'Unassigned') AS cost_centre, + count(*) AS vehicle_count + FROM tracksolid.devices d + GROUP BY 1 + ORDER BY 2 DESC, 1 ASC; +$$; + +GRANT EXECUTE ON FUNCTION public.list_cost_centres() TO viz_anon; diff --git a/trips_deckgl_tracksolid.md b/trips_deckgl_tracksolid.md new file mode 100644 index 0000000..471efcb --- /dev/null +++ b/trips_deckgl_tracksolid.md @@ -0,0 +1,353 @@ +# Trips Visualization — Daily TripsLayer by Cost Centre + +## Context + +`tracksolid_db` (PostgreSQL 16 + TimescaleDB on `stage.rahamafresh.com:5433`) already powers a Dekart instance for ad-hoc map exploration (see `compose/docker-compose.yaml`). The user wants a **purpose-built, public visualization** that animates a day's vehicle trips, colored and filtered by cost centre, with a 24-hour time scrubber and per-vehicle drill-down. Dekart's SQL-driven Kepler.gl UI is too rigid for this — they want a custom React app driving deck.gl directly. + +Confirmed answers from clarifying questions: + +| Decision | Answer | +| --- | --- | +| Delivery | Custom deck.gl React app (not Dekart, not raw Kepler.gl) | +| Department linkage | Via vehicle / device (`devices.imei` → `devices.cost_centre`) | +| Trip shape | Path is **not stored** on `trips` — must be reconstructed from `tracksolid.position_history` per trip window | +| Daily volume | 172 vehicles, ~900 trips/day, ~74k position pings/day (avg 82 pings/trip) | +| Backend | PostgREST auto-API on `tracksolid_db` | +| UX controls | Date picker (single day) + date range + cost-centre multi-select + vehicle drill-down | +| Auth / hosting | Public dashboard, no login (Coolify deployment) | +| Grouping | `cost_centre` only — no `department` column exists. NULL/blank coalesced to `'Unassigned'` and shown by default. | + +Outcome: a public URL (e.g. `trips.rahamafresh.com`) where anyone can pick a date or range, toggle cost centres, scrub a time slider, and watch animated trip paths replay. + +--- + +## Schema findings (verified against the live DB) + +**Source tables (`tracksolid` schema):** + +- `trips(id bigint, imei text, start_time tstz, end_time tstz, start_geom, end_geom, distance_km, avg_speed_kmh, …)` — 6,761 rows total, ~900/day. **No path column.** +- `position_history(imei text, gps_time tstz, geom geometry, lat, lng, speed, …)` — TimescaleDB hypertable, ~74k rows/day. This is where the actual GPS path lives. +- `devices(imei text PK, vehicle_name, vehicle_number, cost_centre, device_group, depot_geom, …)` — 172 rows. `cost_centre` (British spelling) has 17 distinct values: `isp` (42), `osp` (39), `fds` (22), `roll out`, `general`, `regional`, `osp patrol`, `personal`, `mtn`, `planning`, `deliveries`, `management`, `airtel`, `qehs`, plus ~22 NULL/blank. +- `v_fleet_trace` — existing view that segments `position_history` into trips by speed + 5-min idle gap. Useful as a reference for window functions; not used directly. +- `dwh_gold.dim_vehicles` — only `(vehicle_key, imei, vehicle_number, is_active)`. **Not useful** for cost-centre data; we use `tracksolid.devices` directly. + +**Implication for the design:** the path for each trip is built on-the-fly by joining `position_history` to `trips` on `imei` between `start_time` and `end_time`. Wire payload at full GPS resolution is ~1.8 MB raw / ~600 KB gzipped per day — small enough that **no `ST_SimplifyPreserveTopology` down-sampling is needed** at this volume. + +--- + +## Architecture + +``` +┌─────────────────────┐ ┌────────────────────┐ ┌──────────────────────┐ +│ React + deck.gl SPA │ HTTPS │ PostgREST (read) │ SQL │ tracksolid_db │ +│ (Vite, TS) │ ────► │ Coolify service │ ───► │ • new view + │ +│ • TripsLayer │ │ role: viz_anon │ │ RPCs in public. │ +│ • PathLayer │ │ /rpc/trips_for_day │ │ • untouched: │ +│ • Filters / slider │ │ │ │ tracksolid.* │ +└─────────────────────┘ └────────────────────┘ └──────────────────────┘ + ▲ ▲ + │ Mapbox GL base tiles │ host.docker.internal:5433 + ▼ │ (read-only role on public schema) +┌─────────────────────┐ │ +│ Mapbox tiles (pk.*) │ │ +└─────────────────────┘ ▼ + (no schema changes to + tracksolid.*; only new + public.* view + RPC + role) +``` + +Three deployable units, all in the same Coolify project alongside the existing Dekart stack: + +1. **DB layer (additive)** — new `public.trips_viz_v1` view + RPCs + `viz_anon` role inside `tracksolid_db`. **No mutations to existing `tracksolid.*` tables.** +2. **API layer** — PostgREST as a tiny container, read-only against the new view + RPCs. +3. **Web layer** — static React build served by Caddy under a public FQDN. + +--- + +## Component design + +### 1. Database layer (additive, in `tracksolid_db`) + +Three migrations under `db/migrations/`: + +```sql +-- 001_viz_anon_role.sql +CREATE ROLE viz_anon NOLOGIN; +GRANT USAGE ON SCHEMA public TO viz_anon; +-- We never grant on schema tracksolid; the role only sees what we expose in public.* + +-- 002_trips_viz_view.sql +-- Thin metadata view (no path). Used as a stable contract + perm boundary. +CREATE OR REPLACE VIEW public.trips_viz_v1 AS +SELECT + t.id AS trip_id, + t.imei, + d.vehicle_name, + d.vehicle_number, + COALESCE(NULLIF(TRIM(d.cost_centre), ''), 'Unassigned') AS cost_centre, + t.start_time, + t.end_time, + t.distance_km, + t.avg_speed_kmh, + t.max_speed_kmh +FROM tracksolid.trips t +JOIN tracksolid.devices d ON d.imei = t.imei +WHERE t.start_time IS NOT NULL + AND t.end_time IS NOT NULL; + +GRANT SELECT ON public.trips_viz_v1 TO viz_anon; +``` + +```sql +-- 003_trips_for_day_rpc.sql +-- RPC that returns trips + reconstructed path for a single date. +CREATE OR REPLACE FUNCTION public.trips_for_day( + p_date date, + p_cost_centres text[] DEFAULT NULL +) RETURNS TABLE ( + trip_id bigint, + imei text, + vehicle_name text, + vehicle_number text, + cost_centre text, + start_time timestamptz, + end_time timestamptz, + distance_km numeric, + path_geojson json, + timestamps_rel int[] -- seconds since trip start, per vertex (for TripsLayer) +) +LANGUAGE sql STABLE AS $$ + WITH day_trips AS ( + SELECT v.* FROM public.trips_viz_v1 v + WHERE v.start_time::date = p_date + AND (p_cost_centres IS NULL OR v.cost_centre = ANY(p_cost_centres)) + ) + SELECT + dt.trip_id, + dt.imei, + dt.vehicle_name, + dt.vehicle_number, + dt.cost_centre, + dt.start_time, + dt.end_time, + dt.distance_km, + ST_AsGeoJSON(ST_MakeLine(ph.geom ORDER BY ph.gps_time))::json AS path_geojson, + array_agg(EXTRACT(EPOCH FROM ph.gps_time - dt.start_time)::int + ORDER BY ph.gps_time) AS timestamps_rel + FROM day_trips dt + JOIN tracksolid.position_history ph + ON ph.imei = dt.imei + AND ph.gps_time BETWEEN dt.start_time AND dt.end_time + AND ph.geom IS NOT NULL + GROUP BY dt.trip_id, dt.imei, dt.vehicle_name, dt.vehicle_number, + dt.cost_centre, dt.start_time, dt.end_time, dt.distance_km + HAVING count(ph.geom) >= 2; -- need at least 2 points for a LineString +$$; + +GRANT EXECUTE ON FUNCTION public.trips_for_day(date, text[]) TO viz_anon; +``` + +```sql +-- 004_trips_for_range_rpc.sql +-- Multi-day variant; capped at 14 days at the API layer to bound payload size. +CREATE OR REPLACE FUNCTION public.trips_for_range( + p_start date, p_end date, p_cost_centres text[] DEFAULT NULL +) RETURNS TABLE (...same shape...) +LANGUAGE sql STABLE AS $$ + -- identical body, with WHERE start_time::date BETWEEN p_start AND p_end +$$; + +-- 005_list_cost_centres_rpc.sql +CREATE OR REPLACE FUNCTION public.list_cost_centres() +RETURNS TABLE (cost_centre text, vehicle_count bigint) +LANGUAGE sql STABLE AS $$ + SELECT COALESCE(NULLIF(TRIM(d.cost_centre), ''), 'Unassigned') AS cost_centre, + count(*) AS vehicle_count + FROM tracksolid.devices d + GROUP BY 1 + ORDER BY 2 DESC; +$$; +GRANT EXECUTE ON FUNCTION public.list_cost_centres() TO viz_anon; +``` + +**Why thin view + path-building RPC instead of one fat view?** The path build is expensive (`ST_MakeLine` + `array_agg` over `position_history`) — we don't want it firing on every metadata-only query. The view is the perm/contract surface; RPCs do the heavy lifting only when filtered to a date. + +**Why `cost_centre` (British) and not `cost_center`?** That's the actual column name in `tracksolid.devices`. We mirror it verbatim throughout — view, RPC params, frontend types — so there's never a translation layer to debug. + +**Why no `ST_SimplifyPreserveTopology`?** A full day at full GPS resolution is ~1.8 MB / ~600 KB gzipped. Simplification would save bytes we can spend without thinking, and risks visual artifacts when TripsLayer interpolates between vertices. Keep raw. + +### 2. API layer + +Add to `compose/docker-compose.yaml` (alongside existing `dekart` service): + +```yaml +postgrest: + image: postgrest/postgrest:v12.2.0 + restart: always + expose: ["3000"] + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + PGRST_DB_URI: postgres://${VIZ_DATA_USER:-viz_anon}:${VIZ_DATA_PASSWORD}@host.docker.internal:5433/${DEKART_DATA_DB:-tracksolid_db}?sslmode=disable + PGRST_DB_SCHEMAS: public + PGRST_DB_ANON_ROLE: viz_anon + PGRST_OPENAPI_SERVER_PROXY_URI: https://api.trips.rahamafresh.com + PGRST_DB_MAX_ROWS: 10000 # safety cap + PGRST_SERVER_CORS_ALLOWED_ORIGINS: https://trips.rahamafresh.com +``` + +Coolify points the public FQDN `api.trips.rahamafresh.com` → `postgrest:3000`. No auth — the `viz_anon` role itself is the authorization (it can only see `public.trips_viz_v1` + the three RPCs). + +`viz_anon` needs a login password set with `ALTER ROLE viz_anon LOGIN PASSWORD '…'` separately (`NOLOGIN` was shown in the migration to make grants explicit; PostgREST needs `LOGIN`). + +### 3. Web layer (custom React + deck.gl SPA) + +Tech stack: + +- **Vite + React 18 + TypeScript**. +- **deck.gl** (`@deck.gl/react`, `@deck.gl/layers`, `@deck.gl/geo-layers`) for `TripsLayer` and `PathLayer`. +- **react-map-gl** + Mapbox GL JS — base map (reuse existing `DEKART_MAPBOX_TOKEN`). +- **TanStack Query** — fetch + cache; essential when toggling cost centres fires repeated calls. +- **Zustand** — cross-component UI state (date, selected cost centres, currentTime, focused imei). +- **date-fns** for date math; **shadcn/ui** for picker / multi-select / slider primitives. + +Component tree: + +``` +src/ + App.tsx — layout shell: + + + store/ + useVizStore.ts — Zustand: { date, range, costCentres[], focusedImei, currentTime, playing, speed } + hooks/ + useTripsForDay.ts — TanStack Query → /rpc/trips_for_day + useCostCentres.ts — TanStack Query → /rpc/list_cost_centres + useAnimationLoop.ts — requestAnimationFrame → store.setCurrentTime + components/ + MapView.tsx — DeckGL + react-map-gl; builds [PathLayer, TripsLayer] + Sidebar/ + DateControls.tsx — single-day vs range toggle + CostCentreFilter.tsx — multi-select with color swatches + per-bucket vehicle count + VehicleDrilldown.tsx — list of vehicles inside current filter; click = isolate that imei + Legend.tsx — cost_centre → color + Timebar/ + TimeSlider.tsx — 0–86400s slider, play/pause, 1×/10×/60×/600× speed + layers/ + buildLayers.ts — pure fn: (trips, currentTime, focusedImei) → Layer[] + lib/ + api.ts — typed PostgREST client (no auth header) + color.ts — stable hash(cost_centre) → palette index + decode.ts — GeoJSON LineString → Float32Array of coords for TripsLayer + types/ + Trip.ts — { trip_id, imei, vehicle_name, vehicle_number, cost_centre, + start_time, end_time, distance_km, + path_geojson: { type:'LineString', coordinates:[[lng,lat],…] }, + timestamps_rel: number[] } +``` + +**Layer composition:** at any moment two layers stack: + +1. `PathLayer` — full route at 30% opacity for the *focused* set (selected cost centres + drilled imei). Users see the whole route, not just the comet head. +2. `TripsLayer` — animated comets driven by `currentTime` (relative seconds since 00:00 of the selected date, **not** since trip start — so we offset each trip's `timestamps_rel` by `EXTRACT(EPOCH FROM start_time - midnight)` on the client). `trailLength` defaults to 600 s (~10 min tail), exposed as a slider. + +`getColor` maps `cost_centre` → 12-color categorical palette; `'Unassigned'` always maps to a neutral grey (`#888`) so the gap is visually obvious without yelling. + +**Performance budget at observed scale (172 vehicles × ~900 trips × ~82 pts):** + +- Wire payload: ~600 KB gzipped per day — load once per (date, costCentres) tuple, cache via TanStack Query. +- GPU memory: TripsLayer holds positions + timestamps buffers, ~600 KB each — trivial on any laptop. +- Fetch strategy: prefetch tomorrow's date on idle. + +### 4. Deployment + +- New service `postgrest` in `compose/docker-compose.yaml`. +- New service `trips-web` — static Caddy container serving `dist/` from CI build. +- Two new FQDNs in Coolify: `trips.rahamafresh.com` (web), `api.trips.rahamafresh.com` (PostgREST). +- New env vars: `VIZ_DATA_USER`, `VIZ_DATA_PASSWORD`, `VITE_API_URL`, `VITE_MAPBOX_TOKEN`. + +--- + +## Files to create / modify + +### New +- `db/migrations/001_viz_anon_role.sql` +- `db/migrations/002_trips_viz_view.sql` +- `db/migrations/003_trips_for_day_rpc.sql` +- `db/migrations/004_trips_for_range_rpc.sql` +- `db/migrations/005_list_cost_centres_rpc.sql` +- `web/` — full Vite + React + TS scaffold (package.json, vite.config.ts, tsconfig.json, src/*) +- `web/Dockerfile` — multi-stage build → Caddy +- `web/Caddyfile` +- `compose/postgrest.env.example` + +### Modify +- `compose/docker-compose.yaml` — add `postgrest` and `trips_web` services. +- `compose/.env.example` — add `VIZ_DATA_USER`, `VIZ_DATA_PASSWORD`, `VITE_API_URL`, `VITE_MAPBOX_TOKEN`. + +### Reuse (do not modify) +- Existing `dekart` service in `compose/docker-compose.yaml` — runs alongside, untouched. +- Existing `DEKART_MAPBOX_TOKEN` — same token works for the web app's Mapbox GL. +- Existing `grafana_ro` role — *not* reused; we create a stricter `viz_anon` role to limit the public API's surface. + +--- + +## Verification + +### 1. Database smoke test (run after migrations) + +```sql +-- pick a recent date with traffic +SELECT count(*) AS trip_rows, + count(DISTINCT cost_centre) AS cost_centres, + count(DISTINCT imei) AS vehicles, + sum(jsonb_array_length(path_geojson->'coordinates')) AS total_vertices +FROM public.trips_for_day(current_date - 1); +-- expect: trip_rows ~900, cost_centres ≥ 10, vehicles 50–150, total_vertices ~70k + +SELECT cost_centre, vehicle_count FROM public.list_cost_centres(); +-- expect 'Unassigned' present plus the 17 known buckets +``` + +### 2. PostgREST smoke test + +```bash +docker compose up -d postgrest +curl -s "http://localhost:3000/rpc/list_cost_centres" | jq '.[].cost_centre' +curl -s "http://localhost:3000/rpc/trips_for_day?p_date=2026-04-29" | jq '.[0]' +# expect a JSON object with trip_id, imei, cost_centre, path_geojson, timestamps_rel + +curl -s -X POST "http://localhost:3000/rpc/trips_for_day" \ + -H 'content-type: application/json' \ + -d '{"p_date":"2026-04-29","p_cost_centres":["isp","osp"]}' | jq 'length' +# expect a non-zero count, less than the unfiltered total +``` + +### 3. Web app end-to-end (dev) + +```bash +cd web && pnpm dev +# Visit http://localhost:5173, then verify: +# – Cost-centre legend lists all 17 buckets + 'Unassigned' (greyed) +# – Date picker defaults to yesterday; switching dates triggers fresh fetch +# – Toggling 'isp' alone narrows the map to ~42 vehicles' trips +# – Clicking a vehicle in drill-down isolates only that imei's path (Path + Trips layers both filter) +# – Time slider scrubs 00:00–24:00; play button animates at 60× by default +# – Range mode toggle switches the slider to multi-day and calls /rpc/trips_for_range +# – No console errors; Mapbox base map renders; deck.gl canvas overlays cleanly +``` + +### 4. Production deploy check + +```bash +curl -I https://api.trips.rahamafresh.com/ # 200 + CORS allow trips.rahamafresh.com +# Visit https://trips.rahamafresh.com — verify the same checks as §3. +``` + +--- + +## Open items deferred to implementation + +- **Trail length default** — currently 600 s; expose as a slider in case 10-minute tails feel too long for short urban trips. +- **Color palette** — start with deck.gl's 12-color categorical palette + neutral grey for `'Unassigned'`; allow `config/cost-centre-colors.json` to lock specific brand colors later. +- **Range cap** — `trips_for_range` capped at 14 days at the API layer; raise once real usage shows we need more. +- **Index check** — confirm `position_history` has an index on `(imei, gps_time)`; if not, the per-trip path subquery will be slow on cold cache. (Likely already present since it's a Timescale hypertable, but verify with `\d+ tracksolid.position_history`.) +- **`viz_anon` LOGIN** — set its password via `ALTER ROLE viz_anon LOGIN PASSWORD '…'` after migrations; not committed to git. diff --git a/web/.dockerignore b/web/.dockerignore new file mode 100644 index 0000000..b1640b5 --- /dev/null +++ b/web/.dockerignore @@ -0,0 +1,4 @@ +node_modules +dist +.env +.env.local diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 0000000..ff629d7 --- /dev/null +++ b/web/.env.example @@ -0,0 +1,3 @@ +# Copy to web/.env.local for dev. Vite injects VITE_* into the bundle at build time. +VITE_API_URL=http://localhost:3000 +VITE_MAPBOX_TOKEN=pk.xxxxxxxxxxxxxxxxxxxxxxx diff --git a/web/Caddyfile b/web/Caddyfile new file mode 100644 index 0000000..ce7e31f --- /dev/null +++ b/web/Caddyfile @@ -0,0 +1,8 @@ +# Caddy serves the SPA on :8080. Coolify's reverse proxy handles TLS + the +# public FQDN; this container just serves static files with SPA fallback. +:8080 { + root * /srv + encode gzip zstd + try_files {path} /index.html + file_server +} diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..ca3007a --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,16 @@ +FROM node:20-alpine AS build +WORKDIR /app +RUN corepack enable +COPY package.json pnpm-lock.yaml* ./ +RUN pnpm install --frozen-lockfile || pnpm install +COPY . . +ARG VITE_API_URL +ARG VITE_MAPBOX_TOKEN +ENV VITE_API_URL=${VITE_API_URL} +ENV VITE_MAPBOX_TOKEN=${VITE_MAPBOX_TOKEN} +RUN pnpm build + +FROM caddy:2.8-alpine +COPY Caddyfile /etc/caddy/Caddyfile +COPY --from=build /app/dist /srv +EXPOSE 8080 diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..60dda2f --- /dev/null +++ b/web/index.html @@ -0,0 +1,12 @@ + + + + + + Tracksolid Trips + + +
+ + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..5f87071 --- /dev/null +++ b/web/package.json @@ -0,0 +1,33 @@ +{ + "name": "deckgl-tracksolid-web", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview --host 0.0.0.0 --port 8080", + "typecheck": "tsc -b --noEmit" + }, + "dependencies": { + "@deck.gl/core": "^9.0.27", + "@deck.gl/geo-layers": "^9.0.27", + "@deck.gl/layers": "^9.0.27", + "@deck.gl/react": "^9.0.27", + "@tanstack/react-query": "^5.51.0", + "date-fns": "^3.6.0", + "mapbox-gl": "^3.5.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-map-gl": "^7.1.7", + "zustand": "^4.5.4" + }, + "devDependencies": { + "@types/mapbox-gl": "^3.4.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.5.3", + "vite": "^5.3.4" + } +} diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml new file mode 100644 index 0000000..f4d565d --- /dev/null +++ b/web/pnpm-lock.yaml @@ -0,0 +1,2763 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@deck.gl/core': + specifier: ^9.0.27 + version: 9.3.2 + '@deck.gl/geo-layers': + specifier: ^9.0.27 + version: 9.3.2(@deck.gl/core@9.3.2)(@deck.gl/extensions@9.3.2(@deck.gl/core@9.3.2)(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3))))(@deck.gl/layers@9.3.2(@deck.gl/core@9.3.2)(@loaders.gl/core@4.4.1)(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3))))(@deck.gl/mesh-layers@9.3.2(@deck.gl/core@9.3.2)(@loaders.gl/core@4.4.1)(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))(@luma.gl/gltf@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))(@loaders.gl/core@4.4.1)(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3))) + '@deck.gl/layers': + specifier: ^9.0.27 + version: 9.3.2(@deck.gl/core@9.3.2)(@loaders.gl/core@4.4.1)(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3))) + '@deck.gl/react': + specifier: ^9.0.27 + version: 9.3.2(@deck.gl/core@9.3.2)(@deck.gl/widgets@9.3.2(@deck.gl/core@9.3.2)(@luma.gl/core@9.3.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-query': + specifier: ^5.51.0 + version: 5.100.6(react@18.3.1) + date-fns: + specifier: ^3.6.0 + version: 3.6.0 + mapbox-gl: + specifier: ^3.5.2 + version: 3.23.0 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-map-gl: + specifier: ^7.1.7 + version: 7.1.9(mapbox-gl@3.23.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + zustand: + specifier: ^4.5.4 + version: 4.5.7(@types/react@18.3.28)(react@18.3.1) + devDependencies: + '@types/mapbox-gl': + specifier: ^3.4.0 + version: 3.5.0 + '@types/react': + specifier: ^18.3.3 + version: 18.3.28 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.28) + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.7.0(vite@5.4.21(@types/node@25.6.0)) + typescript: + specifier: ^5.5.3 + version: 5.9.3 + vite: + specifier: ^5.3.4 + version: 5.4.21(@types/node@25.6.0) + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@deck.gl/core@9.3.2': + resolution: {integrity: sha512-32Va3np0Zdlz/LBNtDWCs4EkKqdHmXcbGmVp4+7i1Cpdza8y8CFmJs2VPOmSX1fwHvNCGkAZV/SFZOfDb2INsg==} + + '@deck.gl/extensions@9.3.2': + resolution: {integrity: sha512-P2gPCCmGC5R6HRB3Mv3JraDnsSpvStjFFUGKxW810SXmo2eTft/5xpvliiyJeFGDjqttwo8V4Qk6oD3BNVGvRw==} + peerDependencies: + '@deck.gl/core': ~9.3.0 + '@luma.gl/core': ~9.3.3 + '@luma.gl/engine': ~9.3.3 + + '@deck.gl/geo-layers@9.3.2': + resolution: {integrity: sha512-3sndOyq5A3b2DMCBWFCeX9/QkBSp5MD8EUD3eu4hHfCDV4IrbJHtxE/pv60J848Yz8D5u7ftUqXf9gLWnEeBeg==} + peerDependencies: + '@deck.gl/core': ~9.3.0 + '@deck.gl/extensions': ~9.3.0 + '@deck.gl/layers': ~9.3.0 + '@deck.gl/mesh-layers': ~9.3.0 + '@loaders.gl/core': ^4.4.1 + '@luma.gl/core': ~9.3.3 + '@luma.gl/engine': ~9.3.3 + + '@deck.gl/layers@9.3.2': + resolution: {integrity: sha512-TeVfhQ/cQU1oTlTn16mCp7268d1uBJ6dwfgmKXThe2TzW9hql3iJaxbYTKg2phDg5YSiGmeEOpXbeBh59jyUcA==} + peerDependencies: + '@deck.gl/core': ~9.3.0 + '@loaders.gl/core': ^4.4.1 + '@luma.gl/core': ~9.3.3 + '@luma.gl/engine': ~9.3.3 + + '@deck.gl/mesh-layers@9.3.2': + resolution: {integrity: sha512-9KeEnEx8PYalFPq/jSb1983QtjwZeuz64OfHH8I2VOB3eOhtRDxaTAaelbkVkD3E9HqlU2dD6f9huYsuv1WZfw==} + peerDependencies: + '@deck.gl/core': ~9.3.0 + '@luma.gl/core': ~9.3.3 + '@luma.gl/engine': ~9.3.3 + '@luma.gl/gltf': ~9.3.3 + '@luma.gl/shadertools': ~9.3.3 + + '@deck.gl/react@9.3.2': + resolution: {integrity: sha512-XGxoyTQiQWvBHt+q5bATOHlv9INdl0xwt/IxP4eMbbE9XhLreeGTTb0CDGbk+SwDHVwQuLFp5JHZNibUt0J3fA==} + peerDependencies: + '@deck.gl/core': ~9.3.0 + '@deck.gl/widgets': ~9.3.0 + react: '>=16.3.0' + react-dom: '>=16.3.0' + + '@deck.gl/widgets@9.3.2': + resolution: {integrity: sha512-EdNxecpUlZHhfCSD5qpMVva7fjd48u6lDSXMxQRnT2uCGCMHBViZadkCmyK3lPwac4nylM9vRWek/NPSOqPKcQ==} + peerDependencies: + '@deck.gl/core': ~9.3.0 + '@luma.gl/core': ~9.3.3 + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@loaders.gl/3d-tiles@4.4.1': + resolution: {integrity: sha512-837MynN5/lqVbuZcqdxFb0CMfT8v0yRlX7TUFKIBdmkS7AeRRrgcrB+XKblrkdZINUcxOs2N/YLVkwC9wLH1Uw==} + peerDependencies: + '@loaders.gl/core': ~4.4.0 + + '@loaders.gl/compression@4.4.1': + resolution: {integrity: sha512-MKtGbqHBH7xRVFKyB3E9xRqRMwNW8H72OKpUBDdFwP+hQ0mjHZuud0GeYm5pP50+7o3J2PrES06kHTwT4fg7oQ==} + peerDependencies: + '@loaders.gl/core': ~4.4.0 + + '@loaders.gl/core@4.4.1': + resolution: {integrity: sha512-/s4IuvCCQUepvhjLnmePwQppGko2d1pxRS+sp7lyExU0uiqo5dVsAKaCZ2VnddBkFWgDVb/wvcZUBmv/dWcj0Q==} + + '@loaders.gl/crypto@4.4.1': + resolution: {integrity: sha512-ORhS9GSYr9uVTU4I2Taa46XBgPPG+nKErKcyDGIXov3gs0EtgMqs8nU4epuLbsJN3+du6FkQaILyGSZlTxbA7Q==} + peerDependencies: + '@loaders.gl/core': ~4.4.0 + + '@loaders.gl/draco@4.4.1': + resolution: {integrity: sha512-EcapVlkP8Pz53VKg9pYRQUzqm9jH+A+7vGE1kV8nkv63lN8/qtFzBSWMiC6IX1CwxjKJDEINU9Sh8YB1AfMwbQ==} + peerDependencies: + '@loaders.gl/core': ~4.4.0 + + '@loaders.gl/geoarrow@4.4.1': + resolution: {integrity: sha512-d9+AxsNpdJzilgHTFnyycoIocp4b+iEX3bbCCAEdUm/7eZbOdM7sFcgLLiGVTehtGnOUOICskjrzT27gqmzDqg==} + peerDependencies: + '@loaders.gl/core': ~4.4.0 + + '@loaders.gl/gis@4.4.1': + resolution: {integrity: sha512-M9Z9jXwye4SjlD1hAFJwE3+eZiN1lprwlSkWIo7R642kN5r3R60M9fqBD1mvCTBj96FPmbsyOm1eYKS0XCpKxQ==} + peerDependencies: + '@loaders.gl/core': ~4.4.0 + + '@loaders.gl/gltf@4.4.1': + resolution: {integrity: sha512-9ESHEm3YoMgsQh8QS1N99uwA+cij6p6xhCmZnHX4rQnqHm0jvE5RAHlGV1D/Xjvr4PR8IiXaBn/QDl/qdGIxkw==} + peerDependencies: + '@loaders.gl/core': ~4.4.0 + + '@loaders.gl/images@4.4.1': + resolution: {integrity: sha512-v9A4BliEKGxhLuEbh0Ke8ElUlp04KxpKIknUtXXWoEaszAMTSrHI3YhaL/JdRlHraC1VUF/sjzbSBFkKh7nxJg==} + peerDependencies: + '@loaders.gl/core': ~4.4.0 + + '@loaders.gl/loader-utils@4.4.1': + resolution: {integrity: sha512-waosL7VtVRfXsNOXtAM3rOjZyNQD0lQBlhuB5/oY+E+lNzYNFlzgiGXiDOwBpcs7dK7kW2Vv8+KcxyIGIyXOtg==} + + '@loaders.gl/math@4.4.1': + resolution: {integrity: sha512-xenAPOAUd7HDlus5V/g4LKVh1l7FpyVRSYXa+g7tBj91xzhRYgLEXSxdrGfRNAFMDOSGC1ITwCGQwlwSX4Mpxw==} + peerDependencies: + '@loaders.gl/core': ~4.4.0 + + '@loaders.gl/mvt@4.4.1': + resolution: {integrity: sha512-ou1Oyec7hcpCQ2onF1FefNXVv1MwPjwUkII6IFrrRZ/f0/ei0b8yc5IVwO4gkhta/Ve/Y+mFcs/GaeQZMOEBOg==} + peerDependencies: + '@loaders.gl/core': ~4.4.0 + + '@loaders.gl/schema-utils@4.4.1': + resolution: {integrity: sha512-4upip2O6MFaWzk68/lnna7P2uRj9NQ8MIk/ff3CLbciP5/9lKl1qyuzObz5JrJRYzfGB6I81vpOn6FSVQ6m6KQ==} + peerDependencies: + '@loaders.gl/core': ~4.4.0 + + '@loaders.gl/schema@4.4.1': + resolution: {integrity: sha512-s7NjEnyK6jZvJJSWj/mHq+S9mHRHVzIYtFP+C7sMf1gVCQbdkt6OSAMUWRzwPr9+whQNVWjZ9pbLsI/IPW3zvw==} + + '@loaders.gl/terrain@4.4.1': + resolution: {integrity: sha512-cBLT+G0HefySTppxqqkMKcN5kfOfIRRx0WDPHa0VHFJw9rbnxoEDhrXvfsXfOATNFFNtcpgQUDqDqhEBp0XvZw==} + peerDependencies: + '@loaders.gl/core': ~4.4.0 + + '@loaders.gl/textures@4.4.1': + resolution: {integrity: sha512-r1//6sO29GOHso+IvXQ3GrvXZ4cl03VWc34XcnXPn3sAV7O96uRGd5xkyx60lMYAl7Jv7qK/smT3z4Mdxdd4aA==} + peerDependencies: + '@loaders.gl/core': ~4.4.0 + + '@loaders.gl/tiles@4.4.1': + resolution: {integrity: sha512-EbF81/c1oXJocVAKR0rx+vWSOnmBBWWhM7pZpYk6oNUQAJfA99APhiRNstAJiJomAgqAxr7vfnhXHjPZg6osZw==} + peerDependencies: + '@loaders.gl/core': ~4.4.0 + + '@loaders.gl/wms@4.4.1': + resolution: {integrity: sha512-sIaqyHXPuLQnkN2eebvczZYVvapkjA8EZaI8feaPxj4jZk/Hk5EuZzIbxJ4eftLotZwDHd3XzEVIs6YlFOSJ+Q==} + peerDependencies: + '@loaders.gl/core': ~4.4.0 + + '@loaders.gl/worker-utils@4.4.1': + resolution: {integrity: sha512-ovMyIyj9dlChuHuD64Bel7Mir2UYlmLqlZ9MMzVxzTTLvaudJoNAXi6Disp0ooxwF62ZqjNXXutaSbS6UDeuIg==} + peerDependencies: + '@loaders.gl/core': ~4.4.0 + + '@loaders.gl/xml@4.4.1': + resolution: {integrity: sha512-+8Dtxp0BZZj1CVUkiIlKGDLmhwsPILK9yJvc1P7tuJO9KsaQ5cywJk/b8A7lmqb2SfPkEg0xlQOK2FWIo1ATMA==} + peerDependencies: + '@loaders.gl/core': ~4.4.0 + + '@loaders.gl/zip@4.4.1': + resolution: {integrity: sha512-fV7oqREEzzqYl2/b4tiM+J4qeSq6pB4gw1hHngpCtVyjVwWVtsNH2r1ly9kkv4XssIdXJxPcrX/GR0mDIwmp6w==} + peerDependencies: + '@loaders.gl/core': ~4.4.0 + + '@luma.gl/core@9.3.3': + resolution: {integrity: sha512-jCFm2htvrVpcXIy85TBTF1ROgMfknKnfw2OH+Vydr41hiCFd6nqr79gM3f2uhaNkal0BghFNqF3qDioKiUWtew==} + + '@luma.gl/engine@9.3.3': + resolution: {integrity: sha512-StmMTzUcUlpKMU3wvWU48A6OQyphptD9zVGBsSkK6iHIBdtBKlOcmqRkyfvRouo8JHtlrnoJDHLVKhxorwhGAg==} + peerDependencies: + '@luma.gl/core': ~9.3.0 + '@luma.gl/shadertools': ~9.3.0 + + '@luma.gl/gltf@9.3.3': + resolution: {integrity: sha512-/wty4PHYeQelXvDJesyuMdqtAfpL1XcyEQffcEAwKwu9w7JdkygSShdUwTT1iF7no0uGKuWgq824dVC9WBBQcw==} + peerDependencies: + '@luma.gl/core': ~9.3.0 + '@luma.gl/engine': ~9.3.0 + '@luma.gl/shadertools': ~9.3.0 + + '@luma.gl/shadertools@9.3.3': + resolution: {integrity: sha512-4ZfG4/Utix951vqyiG/JIx+Eg+GMNwOxgr/07/i0gf7bK1gJZIEQ5BxVcDw4MCQfdoVlGPGzl0cQKbdqBvaCAQ==} + peerDependencies: + '@luma.gl/core': ~9.3.0 + + '@luma.gl/webgl@9.3.3': + resolution: {integrity: sha512-X+aavdP5o6VFHSA0es9gKZTT145jfcFbhKJt/gwJrptnKNoIW4+Y37ZEpCo1AzAnr+FQCxjgcM2kOCpoWMfSVA==} + peerDependencies: + '@luma.gl/core': ~9.3.0 + + '@mapbox/jsonlint-lines-primitives@2.0.2': + resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==} + engines: {node: '>= 0.6'} + + '@mapbox/mapbox-gl-supported@3.0.0': + resolution: {integrity: sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg==} + + '@mapbox/martini@0.2.0': + resolution: {integrity: sha512-7hFhtkb0KTLEls+TRw/rWayq5EeHtTaErgm/NskVoXmtgAQu/9D299aeyj6mzAR/6XUnYRp2lU+4IcrYRFjVsQ==} + + '@mapbox/point-geometry@0.1.0': + resolution: {integrity: sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==} + + '@mapbox/point-geometry@1.1.0': + resolution: {integrity: sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==} + + '@mapbox/tiny-sdf@2.1.0': + resolution: {integrity: sha512-uFJhNh36BR4OCuWIEiWaEix9CA2WzT6CAIcqVjWYpnx8+QDtS+oC4QehRrx5cX4mgWs37MmKnwUejeHxVymzNg==} + + '@mapbox/unitbezier@0.0.1': + resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==} + + '@mapbox/vector-tile@1.3.1': + resolution: {integrity: sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==} + + '@mapbox/vector-tile@2.0.4': + resolution: {integrity: sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==} + + '@maplibre/maplibre-gl-style-spec@19.3.3': + resolution: {integrity: sha512-cOZZOVhDSulgK0meTsTkmNXb1ahVvmTmWmfx9gRBwc6hq98wS9JP35ESIoNq3xqEan+UN+gn8187Z6E4NKhLsw==} + hasBin: true + + '@math.gl/core@4.1.0': + resolution: {integrity: sha512-FrdHBCVG3QdrworwrUSzXIaK+/9OCRLscxI2OUy6sLOHyHgBMyfnEGs99/m3KNvs+95BsnQLWklVfpKfQzfwKA==} + + '@math.gl/culling@4.1.0': + resolution: {integrity: sha512-jFmjFEACnP9kVl8qhZxFNhCyd47qPfSVmSvvjR0/dIL6R9oD5zhR1ub2gN16eKDO/UM7JF9OHKU3EBIfeR7gtg==} + + '@math.gl/geospatial@4.1.0': + resolution: {integrity: sha512-BzsUhpVvnmleyYF6qdqJIip6FtIzJmnWuPTGhlBuPzh7VBHLonCFSPtQpbkRuoyAlbSyaGXcVt6p6lm9eK2vtg==} + + '@math.gl/polygon@4.1.0': + resolution: {integrity: sha512-YA/9PzaCRHbIP5/0E9uTYrqe+jsYTQoqoDWhf6/b0Ixz8bPZBaGDEafLg3z7ffBomZLacUty9U3TlPjqMtzPjA==} + + '@math.gl/sun@4.1.0': + resolution: {integrity: sha512-i3q6OCBLSZ5wgZVhXg+X7gsjY/TUtuFW/2KBiq/U1ypLso3S4sEykoU/MGjxUv1xiiGtr+v8TeMbO1OBIh/HmA==} + + '@math.gl/types@4.1.0': + resolution: {integrity: sha512-clYZdHcmRvMzVK5fjeDkQlHUzXQSNdZ7s4xOqC3nJPgz4C/TZkUecTo9YS4PruZqtDda/ag4erndP0MIn40dGA==} + + '@math.gl/web-mercator@4.1.0': + resolution: {integrity: sha512-HZo3vO5GCMkXJThxRJ5/QYUYRr3XumfT8CzNNCwoJfinxy5NtKUd7dusNTXn7yJ40UoB8FMIwkVwNlqaiRZZAw==} + + '@nodable/entities@2.1.0': + resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} + + '@probe.gl/env@4.1.1': + resolution: {integrity: sha512-+68seNDMVsEegRB47pFA/Ws1Fjy8agcFYXxzorKToyPcD6zd+gZ5uhwoLd7TzsSw6Ydns//2KEszWn+EnNHTbA==} + + '@probe.gl/log@4.1.1': + resolution: {integrity: sha512-kcZs9BT44pL7hS1OkRGKYRXI/SN9KejUlPD+BY40DguRLzdC5tLG/28WGMyfKdn/51GT4a0p+0P8xvDn1Ez+Kg==} + + '@probe.gl/stats@4.1.1': + resolution: {integrity: sha512-4VpAyMHOqydSvPlEyHwXaE+AkIdR03nX+Qhlxsk2D/IW4OVmDZgIsvJB1cDzyEEtcfKcnaEbfXeiPgejBceT6g==} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.60.2': + resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.2': + resolution: {integrity: sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.2': + resolution: {integrity: sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.2': + resolution: {integrity: sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.2': + resolution: {integrity: sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.2': + resolution: {integrity: sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': + resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.2': + resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.2': + resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.2': + resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.2': + resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.2': + resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.2': + resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.2': + resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.2': + resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.2': + resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.2': + resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.2': + resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.2': + resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.2': + resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.2': + resolution: {integrity: sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.2': + resolution: {integrity: sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.2': + resolution: {integrity: sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.2': + resolution: {integrity: sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.2': + resolution: {integrity: sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==} + cpu: [x64] + os: [win32] + + '@swc/helpers@0.5.21': + resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} + + '@tanstack/query-core@5.100.6': + resolution: {integrity: sha512-Os2CPUr98to98RYm+D4qGqGkiffn7MGSyl2547a4MljVkHE30AMJRqTiyCqBfMwzAx/I91vCkAxp5tHSla6Twg==} + + '@tanstack/react-query@5.100.6': + resolution: {integrity: sha512-uVSrps0PV16Cxmcn2rvL+dUhwTpTUtiRW347AEeYxMZXO2pZe9ja7E24PAMGoQ5u2g89DD8u4QhOviBk+RN8RA==} + peerDependencies: + react: ^18 || ^19 + + '@turf/boolean-clockwise@5.1.5': + resolution: {integrity: sha512-FqbmEEOJ4rU4/2t7FKx0HUWmjFEVqR+NJrFP7ymGSjja2SQ7Q91nnBihGuT+yuHHl6ElMjQ3ttsB/eTmyCycxA==} + + '@turf/clone@5.1.5': + resolution: {integrity: sha512-//pITsQ8xUdcQ9pVb4JqXiSqG4dos5Q9N4sYFoWghX21tfOV2dhc5TGqYOhnHrQS7RiKQL1vQ48kIK34gQ5oRg==} + + '@turf/helpers@5.1.5': + resolution: {integrity: sha512-/lF+JR+qNDHZ8bF9d+Cp58nxtZWJ3sqFe6n3u3Vpj+/0cqkjk4nXKYBSY0azm+GIYB5mWKxUXvuP/m0ZnKj1bw==} + + '@turf/invariant@5.2.0': + resolution: {integrity: sha512-28RCBGvCYsajVkw2EydpzLdcYyhSA77LovuOvgCJplJWaNVyJYH6BOR3HR9w50MEkPqb/Vc/jdo6I6ermlRtQA==} + + '@turf/meta@5.2.0': + resolution: {integrity: sha512-ZjQ3Ii62X9FjnK4hhdsbT+64AYRpaI8XMBMcyftEOGSmPMUVnkbvuv3C9geuElAXfQU7Zk1oWGOcrGOD9zr78Q==} + + '@turf/rewind@5.1.5': + resolution: {integrity: sha512-Gdem7JXNu+G4hMllQHXRFRihJl3+pNl7qY+l4qhQFxq+hiU1cQoVFnyoleIqWKIrdK/i2YubaSwc3SCM7N5mMw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/brotli@1.3.5': + resolution: {integrity: sha512-9xoNr+bcxT236/7ZgcWw/6Pb2RRetE13p4bFy1xYSckKwyOiRfmInay8baUWZgH7/284Wl6IPe7+nOI9+OQg/A==} + + '@types/command-line-args@5.2.3': + resolution: {integrity: sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==} + + '@types/command-line-usage@5.0.4': + resolution: {integrity: sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==} + + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/geojson-vt@3.2.5': + resolution: {integrity: sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/mapbox-gl@3.5.0': + resolution: {integrity: sha512-3wVAUTC6q1UKatLP9YxFBnGJWi3neJUF9OKeyRdUf/BsYjZAP35xmZkL4zogVJbO3vdExuSVYCAkzUXjpjdhOg==} + deprecated: This is a stub types definition. mapbox-gl provides its own type definitions, so you do not need this installed. + + '@types/node@24.12.2': + resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} + + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + + '@types/offscreencanvas@2019.7.3': + resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} + + '@types/pako@1.0.7': + resolution: {integrity: sha512-YBtzT2ztNF6R/9+UXj2wTGFnC9NklAnASt3sC0h2m1bbH7G6FyBIkt4AN8ThZpNfxUo1b2iMVO0UawiJymEt8A==} + + '@types/pbf@3.0.5': + resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.28': + resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} + + '@types/supercluster@7.1.3': + resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + a5-js@0.7.3: + resolution: {integrity: sha512-3aoMwHmNkyuMDHS4q6GRRInpOawamen2pokIbc0MQmR9cqG0Y9+B0bZpzswwetjrSG2ckbYtShH+nKru6+3O5Q==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + apache-arrow@21.1.0: + resolution: {integrity: sha512-kQrYLxhC+NTVVZ4CCzGF6L/uPVOzJmD1T3XgbiUnP7oTeVFOFgEUu6IKNwCDkpFoBVqDKQivlX4RUFqqnWFlEA==} + hasBin: true + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + arr-union@3.1.0: + resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} + engines: {node: '>=0.10.0'} + + array-back@6.2.3: + resolution: {integrity: sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==} + engines: {node: '>=12.17'} + + assign-symbols@1.0.0: + resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} + engines: {node: '>=0.10.0'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.24: + resolution: {integrity: sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==} + engines: {node: '>=6.0.0'} + hasBin: true + + brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buf-compare@1.0.1: + resolution: {integrity: sha512-Bvx4xH00qweepGc43xFvMs5BKASXTbHaHm6+kDYIK9p/4iFwjATQkmPKHQSgJZzKbAymhztRbXUf1Nqhzl73/Q==} + engines: {node: '>=0.10.0'} + + bytewise-core@1.2.3: + resolution: {integrity: sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA==} + + bytewise@1.1.0: + resolution: {integrity: sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ==} + + caniuse-lite@1.0.30001791: + resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==} + + chalk-template@0.4.0: + resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} + engines: {node: '>=12'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + + cheap-ruler@4.0.0: + resolution: {integrity: sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + command-line-args@6.0.2: + resolution: {integrity: sha512-AIjYVxrV9X752LmPDLbVYv8aMCuHPSLZJXEo2qo/xJfv+NYhaZ4sMSF01rM+gHPaMgvPM0l5D/F+Qx+i2WfSmQ==} + engines: {node: '>=12.20'} + peerDependencies: + '@75lb/nature': latest + peerDependenciesMeta: + '@75lb/nature': + optional: true + + command-line-usage@7.0.4: + resolution: {integrity: sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg==} + engines: {node: '>=12.20.0'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + core-assert@0.2.1: + resolution: {integrity: sha512-IG97qShIP+nrJCXMCgkNZgH7jZQ4n8RpPyPeXX++T6avR/KhLhgLiHKoEn5Rc1KjfycSfA9DMa6m+4C4eguHhw==} + engines: {node: '>=0.10.0'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + + csscolorparser@1.0.3: + resolution: {integrity: sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-strict-equal@0.2.0: + resolution: {integrity: sha512-3daSWyvZ/zwJvuMGlzG1O+Ow0YSadGfb3jsh9xoCutv2tWyB9dA4YvR9L9/fSdDZa2dByYQe+TqapSGUrjnkoA==} + engines: {node: '>=0.10.0'} + + draco3d@1.5.7: + resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} + + earcut@2.2.4: + resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} + + earcut@3.0.2: + resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==} + + electron-to-chromium@1.5.346: + resolution: {integrity: sha512-3PGbvVwt9AppQsta0Kuq5DIcSj7aQfDfCVS7KnV3nhXEDtuJVRS7kK28Q+qy5KRkQ4bICV4xOaXNeUaXe78dDg==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend-shallow@3.0.2: + resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} + engines: {node: '>=0.10.0'} + + fast-xml-builder@1.1.5: + resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==} + + fast-xml-parser@5.7.2: + resolution: {integrity: sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==} + hasBin: true + + fflate@0.7.4: + resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} + + find-replace@5.0.2: + resolution: {integrity: sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==} + engines: {node: '>=14'} + peerDependencies: + '@75lb/nature': latest + peerDependenciesMeta: + '@75lb/nature': + optional: true + + flatbuffers@25.9.23: + resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + geojson-vt@4.0.2: + resolution: {integrity: sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==} + + get-value@2.0.6: + resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} + engines: {node: '>=0.10.0'} + + gl-matrix@3.4.4: + resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==} + + grid-index@1.1.0: + resolution: {integrity: sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==} + + h3-js@4.4.0: + resolution: {integrity: sha512-DvJh07MhGgY2KcC4OeZc8SSyA+ZXpdvoh6uCzGpoKvWtZxJB+g6VXXC1+eWYkaMIsLz7J/ErhOalHCpcs1KYog==} + engines: {node: '>=4', npm: '>=3', yarn: '>=1.3.0'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + image-size@0.7.5: + resolution: {integrity: sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==} + engines: {node: '>=6.9.0'} + hasBin: true + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + + is-error@2.2.2: + resolution: {integrity: sha512-IOQqts/aHWbiisY5DuPJQ0gcbvaLFCa7fBa9xoLfxBZvQ+ZI/Zh9xoI7Gk+G64N0FdK4AbibytHht2tWgpJWLg==} + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extendable@1.0.1: + resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} + engines: {node: '>=0.10.0'} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-bignum@0.0.3: + resolution: {integrity: sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==} + engines: {node: '>=0.8'} + + json-stringify-pretty-compact@3.0.0: + resolution: {integrity: sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + kdbush@4.0.2: + resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==} + + ktx-parse@0.7.1: + resolution: {integrity: sha512-FeA3g56ksdFNwjXJJsc1CCc7co+AJYDp6ipIp878zZ2bU8kWROatLYf39TQEd4/XRSUvBXovQ8gaVKWPXsCLEQ==} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + long@3.2.0: + resolution: {integrity: sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg==} + engines: {node: '>=0.6'} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz4js@0.2.0: + resolution: {integrity: sha512-gY2Ia9Lm7Ep8qMiuGRhvUq0Q7qUereeldZPP1PMEJxPtEWHJLqw9pgX68oHajBH0nzJK4MaZEA/YNV3jT8u8Bg==} + + mapbox-gl@3.23.0: + resolution: {integrity: sha512-zzjNAaMNvXnAVEUrYpOWmRVEBCIWgDAMLRPvSOoKY3smKvrINFVrRK/1jEpUDbEa7Ppf5Q/nwC6E07tz/i7IKw==} + + martinez-polygon-clipping@0.8.1: + resolution: {integrity: sha512-9PLLMzMPI6ihHox4Ns6LpVBLpRc7sbhULybZ/wyaY8sY3ECNe2+hxm1hA2/9bEEpRrdpjoeduBuZLg2aq1cSIQ==} + + md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mjolnir.js@3.0.0: + resolution: {integrity: sha512-siX3YCG7N2HnmN1xMH3cK4JkUZJhbkhRFJL+G5N1vH0mh1t5088rJknIoqDFWDIU6NPGvRRgLnYW3ZHjSMEBLA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + murmurhash-js@1.0.0: + resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + + pbf@3.3.0: + resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==} + hasBin: true + + pbf@4.0.1: + resolution: {integrity: sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.13: + resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==} + engines: {node: ^10 || ^12 || >=14} + + potpack@2.1.0: + resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==} + + preact@10.29.1: + resolution: {integrity: sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + protocol-buffers-schema@3.6.1: + resolution: {integrity: sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==} + + quickselect@3.0.0: + resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-map-gl@7.1.9: + resolution: {integrity: sha512-KsCc8Gyn05wVGlHZoopaiiCr0RCAQ6LDISo5sEy1/pV/d7RlozkF946tiX7IgyijJQMRujHol5QdwUPESjh73w==} + peerDependencies: + mapbox-gl: '>=1.13.0' + maplibre-gl: '>=1.13.0 <5.0.0' + react: '>=16.3.0' + react-dom: '>=16.3.0' + peerDependenciesMeta: + mapbox-gl: + optional: true + maplibre-gl: + optional: true + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + resolve-protobuf-schema@2.1.0: + resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} + + robust-predicates@2.0.4: + resolution: {integrity: sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg==} + + rollup@4.60.2: + resolution: {integrity: sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + set-value@2.0.1: + resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} + engines: {node: '>=0.10.0'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + snappyjs@0.6.1: + resolution: {integrity: sha512-YIK6I2lsH072UE0aOFxxY1dPDCS43I5ktqHpeAsuLNYWkE5pGxRGWfDM4/vSUfNzXjC1Ivzt3qx31PCLmc9yqg==} + + sort-asc@0.2.0: + resolution: {integrity: sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==} + engines: {node: '>=0.10.0'} + + sort-desc@0.2.0: + resolution: {integrity: sha512-NqZqyvL4VPW+RAxxXnB8gvE1kyikh8+pR+T+CXLksVRN9eiQqkQlPwqWYU0mF9Jm7UnctShlxLyAt1CaBOTL1w==} + engines: {node: '>=0.10.0'} + + sort-object@3.0.3: + resolution: {integrity: sha512-nK7WOY8jik6zaG9CRwZTaD5O7ETWDLZYMM12pqY8htll+7dYeqGfEUPcUBHOpSJg2vJOrvFIY2Dl5cX2ih1hAQ==} + engines: {node: '>=0.10.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + splaytree@0.1.4: + resolution: {integrity: sha512-D50hKrjZgBzqD3FT2Ek53f2dcDLAQT8SSGrzj3vidNH5ISRgceeGVJ2dQIthKOuayqFXfFjXheHNo4bbt9LhRQ==} + + split-string@3.1.0: + resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + strnum@2.2.3: + resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} + + supercluster@8.0.1: + resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + table-layout@4.1.1: + resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==} + engines: {node: '>=12.17'} + + texture-compressor@1.0.2: + resolution: {integrity: sha512-dStVgoaQ11mA5htJ+RzZ51ZxIZqNOgWKAIvtjLrW1AliQQLCmrDqNzQZ8Jh91YealQ95DXt4MEduLzJmbs6lig==} + hasBin: true + + tinyqueue@3.0.0: + resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + typewise-core@1.2.0: + resolution: {integrity: sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==} + + typewise@1.0.3: + resolution: {integrity: sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ==} + + typical@7.3.0: + resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==} + engines: {node: '>=12.17'} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + + union-value@1.0.1: + resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} + engines: {node: '>=0.10.0'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + wordwrapjs@5.1.1: + resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==} + engines: {node: '>=12.17'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + zstd-codec@0.1.5: + resolution: {integrity: sha512-v3fyjpK8S/dpY/X5WxqTK3IoCnp/ZOLxn144GZVlNUjtwAchzrVo03h+oMATFhCIiJ5KTr4V3vDQQYz4RU684g==} + + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@deck.gl/core@9.3.2': + dependencies: + '@loaders.gl/core': 4.4.1 + '@loaders.gl/images': 4.4.1(@loaders.gl/core@4.4.1) + '@luma.gl/core': 9.3.3 + '@luma.gl/engine': 9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)) + '@luma.gl/shadertools': 9.3.3(@luma.gl/core@9.3.3) + '@luma.gl/webgl': 9.3.3(@luma.gl/core@9.3.3) + '@math.gl/core': 4.1.0 + '@math.gl/sun': 4.1.0 + '@math.gl/types': 4.1.0 + '@math.gl/web-mercator': 4.1.0 + '@probe.gl/env': 4.1.1 + '@probe.gl/log': 4.1.1 + '@probe.gl/stats': 4.1.1 + '@types/offscreencanvas': 2019.7.3 + gl-matrix: 3.4.4 + mjolnir.js: 3.0.0 + transitivePeerDependencies: + - '@75lb/nature' + + '@deck.gl/extensions@9.3.2(@deck.gl/core@9.3.2)(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))': + dependencies: + '@deck.gl/core': 9.3.2 + '@luma.gl/core': 9.3.3 + '@luma.gl/engine': 9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)) + '@luma.gl/shadertools': 9.3.3(@luma.gl/core@9.3.3) + '@luma.gl/webgl': 9.3.3(@luma.gl/core@9.3.3) + '@math.gl/core': 4.1.0 + + '@deck.gl/geo-layers@9.3.2(@deck.gl/core@9.3.2)(@deck.gl/extensions@9.3.2(@deck.gl/core@9.3.2)(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3))))(@deck.gl/layers@9.3.2(@deck.gl/core@9.3.2)(@loaders.gl/core@4.4.1)(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3))))(@deck.gl/mesh-layers@9.3.2(@deck.gl/core@9.3.2)(@loaders.gl/core@4.4.1)(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))(@luma.gl/gltf@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))(@loaders.gl/core@4.4.1)(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))': + dependencies: + '@deck.gl/core': 9.3.2 + '@deck.gl/extensions': 9.3.2(@deck.gl/core@9.3.2)(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3))) + '@deck.gl/layers': 9.3.2(@deck.gl/core@9.3.2)(@loaders.gl/core@4.4.1)(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3))) + '@deck.gl/mesh-layers': 9.3.2(@deck.gl/core@9.3.2)(@loaders.gl/core@4.4.1)(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))(@luma.gl/gltf@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)) + '@loaders.gl/3d-tiles': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/core': 4.4.1 + '@loaders.gl/gis': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/loader-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/mvt': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/schema': 4.4.1 + '@loaders.gl/terrain': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/tiles': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/wms': 4.4.1(@loaders.gl/core@4.4.1) + '@luma.gl/core': 9.3.3 + '@luma.gl/engine': 9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)) + '@luma.gl/gltf': 9.3.3(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)) + '@luma.gl/shadertools': 9.3.3(@luma.gl/core@9.3.3) + '@math.gl/core': 4.1.0 + '@math.gl/culling': 4.1.0 + '@math.gl/web-mercator': 4.1.0 + '@types/geojson': 7946.0.16 + a5-js: 0.7.3 + h3-js: 4.4.0 + long: 3.2.0 + transitivePeerDependencies: + - '@75lb/nature' + + '@deck.gl/layers@9.3.2(@deck.gl/core@9.3.2)(@loaders.gl/core@4.4.1)(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))': + dependencies: + '@deck.gl/core': 9.3.2 + '@loaders.gl/core': 4.4.1 + '@loaders.gl/images': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/schema': 4.4.1 + '@luma.gl/core': 9.3.3 + '@luma.gl/engine': 9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)) + '@luma.gl/shadertools': 9.3.3(@luma.gl/core@9.3.3) + '@mapbox/tiny-sdf': 2.1.0 + '@math.gl/core': 4.1.0 + '@math.gl/polygon': 4.1.0 + '@math.gl/web-mercator': 4.1.0 + earcut: 2.2.4 + transitivePeerDependencies: + - '@75lb/nature' + + '@deck.gl/mesh-layers@9.3.2(@deck.gl/core@9.3.2)(@loaders.gl/core@4.4.1)(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))(@luma.gl/gltf@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3))': + dependencies: + '@deck.gl/core': 9.3.2 + '@loaders.gl/gltf': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/schema': 4.4.1 + '@luma.gl/core': 9.3.3 + '@luma.gl/engine': 9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)) + '@luma.gl/gltf': 9.3.3(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)) + '@luma.gl/shadertools': 9.3.3(@luma.gl/core@9.3.3) + transitivePeerDependencies: + - '@75lb/nature' + - '@loaders.gl/core' + + '@deck.gl/react@9.3.2(@deck.gl/core@9.3.2)(@deck.gl/widgets@9.3.2(@deck.gl/core@9.3.2)(@luma.gl/core@9.3.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@deck.gl/core': 9.3.2 + '@deck.gl/widgets': 9.3.2(@deck.gl/core@9.3.2)(@luma.gl/core@9.3.3) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@deck.gl/widgets@9.3.2(@deck.gl/core@9.3.2)(@luma.gl/core@9.3.3)': + dependencies: + '@deck.gl/core': 9.3.2 + '@floating-ui/dom': 1.7.6 + '@luma.gl/core': 9.3.3 + preact: 10.29.1 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@loaders.gl/3d-tiles@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/compression': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/core': 4.4.1 + '@loaders.gl/crypto': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/draco': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/gltf': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/images': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/loader-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/math': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/tiles': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/zip': 4.4.1(@loaders.gl/core@4.4.1) + '@math.gl/core': 4.1.0 + '@math.gl/culling': 4.1.0 + '@math.gl/geospatial': 4.1.0 + '@probe.gl/log': 4.1.1 + long: 5.3.2 + transitivePeerDependencies: + - '@75lb/nature' + + '@loaders.gl/compression@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/core': 4.4.1 + '@loaders.gl/loader-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/worker-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@types/pako': 1.0.7 + fflate: 0.7.4 + pako: 1.0.11 + snappyjs: 0.6.1 + optionalDependencies: + '@types/brotli': 1.3.5 + brotli: 1.3.3 + lz4js: 0.2.0 + zstd-codec: 0.1.5 + transitivePeerDependencies: + - '@75lb/nature' + + '@loaders.gl/core@4.4.1': + dependencies: + '@loaders.gl/loader-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/schema': 4.4.1 + '@loaders.gl/schema-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/worker-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@probe.gl/log': 4.1.1 + transitivePeerDependencies: + - '@75lb/nature' + + '@loaders.gl/crypto@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/core': 4.4.1 + '@loaders.gl/loader-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/worker-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@types/crypto-js': 4.2.2 + transitivePeerDependencies: + - '@75lb/nature' + + '@loaders.gl/draco@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/core': 4.4.1 + '@loaders.gl/loader-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/schema': 4.4.1 + '@loaders.gl/schema-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/worker-utils': 4.4.1(@loaders.gl/core@4.4.1) + draco3d: 1.5.7 + transitivePeerDependencies: + - '@75lb/nature' + + '@loaders.gl/geoarrow@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/core': 4.4.1 + '@math.gl/polygon': 4.1.0 + apache-arrow: 21.1.0 + transitivePeerDependencies: + - '@75lb/nature' + + '@loaders.gl/gis@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/core': 4.4.1 + '@loaders.gl/geoarrow': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/loader-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/schema': 4.4.1 + '@loaders.gl/schema-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@mapbox/vector-tile': 1.3.1 + '@math.gl/polygon': 4.1.0 + pbf: 3.3.0 + transitivePeerDependencies: + - '@75lb/nature' + + '@loaders.gl/gltf@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/core': 4.4.1 + '@loaders.gl/draco': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/images': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/loader-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/schema': 4.4.1 + '@loaders.gl/textures': 4.4.1(@loaders.gl/core@4.4.1) + '@math.gl/core': 4.1.0 + transitivePeerDependencies: + - '@75lb/nature' + + '@loaders.gl/images@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/core': 4.4.1 + '@loaders.gl/loader-utils': 4.4.1(@loaders.gl/core@4.4.1) + transitivePeerDependencies: + - '@75lb/nature' + + '@loaders.gl/loader-utils@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/schema': 4.4.1 + '@loaders.gl/worker-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@probe.gl/log': 4.1.1 + '@probe.gl/stats': 4.1.1 + transitivePeerDependencies: + - '@75lb/nature' + - '@loaders.gl/core' + + '@loaders.gl/math@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/core': 4.4.1 + '@math.gl/core': 4.1.0 + + '@loaders.gl/mvt@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/core': 4.4.1 + '@loaders.gl/gis': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/images': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/loader-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/schema': 4.4.1 + '@math.gl/polygon': 4.1.0 + '@probe.gl/stats': 4.1.1 + pbf: 3.3.0 + transitivePeerDependencies: + - '@75lb/nature' + + '@loaders.gl/schema-utils@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/core': 4.4.1 + '@loaders.gl/schema': 4.4.1 + '@types/geojson': 7946.0.16 + apache-arrow: 21.1.0 + transitivePeerDependencies: + - '@75lb/nature' + + '@loaders.gl/schema@4.4.1': + dependencies: + '@types/geojson': 7946.0.16 + apache-arrow: 21.1.0 + transitivePeerDependencies: + - '@75lb/nature' + + '@loaders.gl/terrain@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/core': 4.4.1 + '@loaders.gl/images': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/loader-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/schema': 4.4.1 + '@mapbox/martini': 0.2.0 + transitivePeerDependencies: + - '@75lb/nature' + + '@loaders.gl/textures@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/core': 4.4.1 + '@loaders.gl/images': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/loader-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/schema': 4.4.1 + '@loaders.gl/worker-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@math.gl/types': 4.1.0 + ktx-parse: 0.7.1 + texture-compressor: 1.0.2 + transitivePeerDependencies: + - '@75lb/nature' + + '@loaders.gl/tiles@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/core': 4.4.1 + '@loaders.gl/loader-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/math': 4.4.1(@loaders.gl/core@4.4.1) + '@math.gl/core': 4.1.0 + '@math.gl/culling': 4.1.0 + '@math.gl/geospatial': 4.1.0 + '@math.gl/web-mercator': 4.1.0 + '@probe.gl/stats': 4.1.1 + transitivePeerDependencies: + - '@75lb/nature' + + '@loaders.gl/wms@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/core': 4.4.1 + '@loaders.gl/images': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/loader-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/schema': 4.4.1 + '@loaders.gl/xml': 4.4.1(@loaders.gl/core@4.4.1) + '@turf/rewind': 5.1.5 + deep-strict-equal: 0.2.0 + transitivePeerDependencies: + - '@75lb/nature' + + '@loaders.gl/worker-utils@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/core': 4.4.1 + + '@loaders.gl/xml@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/core': 4.4.1 + '@loaders.gl/loader-utils': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/schema': 4.4.1 + fast-xml-parser: 5.7.2 + transitivePeerDependencies: + - '@75lb/nature' + + '@loaders.gl/zip@4.4.1(@loaders.gl/core@4.4.1)': + dependencies: + '@loaders.gl/compression': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/core': 4.4.1 + '@loaders.gl/crypto': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/loader-utils': 4.4.1(@loaders.gl/core@4.4.1) + jszip: 3.10.1 + md5: 2.3.0 + transitivePeerDependencies: + - '@75lb/nature' + + '@luma.gl/core@9.3.3': + dependencies: + '@math.gl/types': 4.1.0 + '@probe.gl/env': 4.1.1 + '@probe.gl/log': 4.1.1 + '@probe.gl/stats': 4.1.1 + '@types/offscreencanvas': 2019.7.3 + + '@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3))': + dependencies: + '@luma.gl/core': 9.3.3 + '@luma.gl/shadertools': 9.3.3(@luma.gl/core@9.3.3) + '@math.gl/core': 4.1.0 + '@math.gl/types': 4.1.0 + '@probe.gl/log': 4.1.1 + '@probe.gl/stats': 4.1.1 + + '@luma.gl/gltf@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/engine@9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)))(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3))': + dependencies: + '@loaders.gl/core': 4.4.1 + '@loaders.gl/gltf': 4.4.1(@loaders.gl/core@4.4.1) + '@loaders.gl/textures': 4.4.1(@loaders.gl/core@4.4.1) + '@luma.gl/core': 9.3.3 + '@luma.gl/engine': 9.3.3(@luma.gl/core@9.3.3)(@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)) + '@luma.gl/shadertools': 9.3.3(@luma.gl/core@9.3.3) + '@math.gl/core': 4.1.0 + transitivePeerDependencies: + - '@75lb/nature' + + '@luma.gl/shadertools@9.3.3(@luma.gl/core@9.3.3)': + dependencies: + '@luma.gl/core': 9.3.3 + '@math.gl/core': 4.1.0 + '@math.gl/types': 4.1.0 + + '@luma.gl/webgl@9.3.3(@luma.gl/core@9.3.3)': + dependencies: + '@luma.gl/core': 9.3.3 + '@math.gl/types': 4.1.0 + '@probe.gl/env': 4.1.1 + + '@mapbox/jsonlint-lines-primitives@2.0.2': {} + + '@mapbox/mapbox-gl-supported@3.0.0': {} + + '@mapbox/martini@0.2.0': {} + + '@mapbox/point-geometry@0.1.0': {} + + '@mapbox/point-geometry@1.1.0': {} + + '@mapbox/tiny-sdf@2.1.0': {} + + '@mapbox/unitbezier@0.0.1': {} + + '@mapbox/vector-tile@1.3.1': + dependencies: + '@mapbox/point-geometry': 0.1.0 + + '@mapbox/vector-tile@2.0.4': + dependencies: + '@mapbox/point-geometry': 1.1.0 + '@types/geojson': 7946.0.16 + pbf: 4.0.1 + + '@maplibre/maplibre-gl-style-spec@19.3.3': + dependencies: + '@mapbox/jsonlint-lines-primitives': 2.0.2 + '@mapbox/unitbezier': 0.0.1 + json-stringify-pretty-compact: 3.0.0 + minimist: 1.2.8 + rw: 1.3.3 + sort-object: 3.0.3 + + '@math.gl/core@4.1.0': + dependencies: + '@math.gl/types': 4.1.0 + + '@math.gl/culling@4.1.0': + dependencies: + '@math.gl/core': 4.1.0 + '@math.gl/types': 4.1.0 + + '@math.gl/geospatial@4.1.0': + dependencies: + '@math.gl/core': 4.1.0 + '@math.gl/types': 4.1.0 + + '@math.gl/polygon@4.1.0': + dependencies: + '@math.gl/core': 4.1.0 + + '@math.gl/sun@4.1.0': {} + + '@math.gl/types@4.1.0': {} + + '@math.gl/web-mercator@4.1.0': + dependencies: + '@math.gl/core': 4.1.0 + + '@nodable/entities@2.1.0': {} + + '@probe.gl/env@4.1.1': {} + + '@probe.gl/log@4.1.1': + dependencies: + '@probe.gl/env': 4.1.1 + + '@probe.gl/stats@4.1.1': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.60.2': + optional: true + + '@rollup/rollup-android-arm64@4.60.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.2': + optional: true + + '@rollup/rollup-darwin-x64@4.60.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.2': + optional: true + + '@swc/helpers@0.5.21': + dependencies: + tslib: 2.8.1 + + '@tanstack/query-core@5.100.6': {} + + '@tanstack/react-query@5.100.6(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.100.6 + react: 18.3.1 + + '@turf/boolean-clockwise@5.1.5': + dependencies: + '@turf/helpers': 5.1.5 + '@turf/invariant': 5.2.0 + + '@turf/clone@5.1.5': + dependencies: + '@turf/helpers': 5.1.5 + + '@turf/helpers@5.1.5': {} + + '@turf/invariant@5.2.0': + dependencies: + '@turf/helpers': 5.1.5 + + '@turf/meta@5.2.0': + dependencies: + '@turf/helpers': 5.1.5 + + '@turf/rewind@5.1.5': + dependencies: + '@turf/boolean-clockwise': 5.1.5 + '@turf/clone': 5.1.5 + '@turf/helpers': 5.1.5 + '@turf/invariant': 5.2.0 + '@turf/meta': 5.2.0 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/brotli@1.3.5': + dependencies: + '@types/node': 25.6.0 + optional: true + + '@types/command-line-args@5.2.3': {} + + '@types/command-line-usage@5.0.4': {} + + '@types/crypto-js@4.2.2': {} + + '@types/estree@1.0.8': {} + + '@types/geojson-vt@3.2.5': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/geojson@7946.0.16': {} + + '@types/mapbox-gl@3.5.0': + dependencies: + mapbox-gl: 3.23.0 + + '@types/node@24.12.2': + dependencies: + undici-types: 7.16.0 + + '@types/node@25.6.0': + dependencies: + undici-types: 7.19.2 + optional: true + + '@types/offscreencanvas@2019.7.3': {} + + '@types/pako@1.0.7': {} + + '@types/pbf@3.0.5': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + + '@types/react@18.3.28': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@types/supercluster@7.1.3': + dependencies: + '@types/geojson': 7946.0.16 + + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@25.6.0))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21(@types/node@25.6.0) + transitivePeerDependencies: + - supports-color + + a5-js@0.7.3: + dependencies: + gl-matrix: 3.4.4 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + apache-arrow@21.1.0: + dependencies: + '@swc/helpers': 0.5.21 + '@types/command-line-args': 5.2.3 + '@types/command-line-usage': 5.0.4 + '@types/node': 24.12.2 + command-line-args: 6.0.2 + command-line-usage: 7.0.4 + flatbuffers: 25.9.23 + json-bignum: 0.0.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@75lb/nature' + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + arr-union@3.1.0: {} + + array-back@6.2.3: {} + + assign-symbols@1.0.0: {} + + base64-js@1.5.1: + optional: true + + baseline-browser-mapping@2.10.24: {} + + brotli@1.3.3: + dependencies: + base64-js: 1.5.1 + optional: true + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.24 + caniuse-lite: 1.0.30001791 + electron-to-chromium: 1.5.346 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + buf-compare@1.0.1: {} + + bytewise-core@1.2.3: + dependencies: + typewise-core: 1.2.0 + + bytewise@1.1.0: + dependencies: + bytewise-core: 1.2.3 + typewise: 1.0.3 + + caniuse-lite@1.0.30001791: {} + + chalk-template@0.4.0: + dependencies: + chalk: 4.1.2 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + charenc@0.0.2: {} + + cheap-ruler@4.0.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + command-line-args@6.0.2: + dependencies: + array-back: 6.2.3 + find-replace: 5.0.2 + lodash.camelcase: 4.3.0 + typical: 7.3.0 + + command-line-usage@7.0.4: + dependencies: + array-back: 6.2.3 + chalk-template: 0.4.0 + table-layout: 4.1.1 + typical: 7.3.0 + + convert-source-map@2.0.0: {} + + core-assert@0.2.1: + dependencies: + buf-compare: 1.0.1 + is-error: 2.2.2 + + core-util-is@1.0.3: {} + + crypt@0.0.2: {} + + csscolorparser@1.0.3: {} + + csstype@3.2.3: {} + + date-fns@3.6.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-strict-equal@0.2.0: + dependencies: + core-assert: 0.2.1 + + draco3d@1.5.7: {} + + earcut@2.2.4: {} + + earcut@3.0.2: {} + + electron-to-chromium@1.5.346: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend-shallow@3.0.2: + dependencies: + assign-symbols: 1.0.0 + is-extendable: 1.0.1 + + fast-xml-builder@1.1.5: + dependencies: + path-expression-matcher: 1.5.0 + + fast-xml-parser@5.7.2: + dependencies: + '@nodable/entities': 2.1.0 + fast-xml-builder: 1.1.5 + path-expression-matcher: 1.5.0 + strnum: 2.2.3 + + fflate@0.7.4: {} + + find-replace@5.0.2: {} + + flatbuffers@25.9.23: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + geojson-vt@4.0.2: {} + + get-value@2.0.6: {} + + gl-matrix@3.4.4: {} + + grid-index@1.1.0: {} + + h3-js@4.4.0: {} + + has-flag@4.0.0: {} + + ieee754@1.2.1: {} + + image-size@0.7.5: {} + + immediate@3.0.6: {} + + inherits@2.0.4: {} + + is-buffer@1.1.6: {} + + is-error@2.2.2: {} + + is-extendable@0.1.1: {} + + is-extendable@1.0.1: + dependencies: + is-plain-object: 2.0.4 + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + + isarray@1.0.0: {} + + isobject@3.0.1: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json-bignum@0.0.3: {} + + json-stringify-pretty-compact@3.0.0: {} + + json5@2.2.3: {} + + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + kdbush@4.0.2: {} + + ktx-parse@0.7.1: {} + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + lodash.camelcase@4.3.0: {} + + long@3.2.0: {} + + long@5.3.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz4js@0.2.0: + optional: true + + mapbox-gl@3.23.0: + dependencies: + '@mapbox/mapbox-gl-supported': 3.0.0 + '@mapbox/point-geometry': 1.1.0 + '@mapbox/tiny-sdf': 2.1.0 + '@mapbox/unitbezier': 0.0.1 + '@mapbox/vector-tile': 2.0.4 + '@types/geojson': 7946.0.16 + '@types/geojson-vt': 3.2.5 + '@types/pbf': 3.0.5 + '@types/supercluster': 7.1.3 + cheap-ruler: 4.0.0 + csscolorparser: 1.0.3 + earcut: 3.0.2 + geojson-vt: 4.0.2 + gl-matrix: 3.4.4 + grid-index: 1.1.0 + kdbush: 4.0.2 + martinez-polygon-clipping: 0.8.1 + murmurhash-js: 1.0.0 + pbf: 4.0.1 + potpack: 2.1.0 + quickselect: 3.0.0 + supercluster: 8.0.1 + tinyqueue: 3.0.0 + + martinez-polygon-clipping@0.8.1: + dependencies: + robust-predicates: 2.0.4 + splaytree: 0.1.4 + tinyqueue: 3.0.0 + + md5@2.3.0: + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + + minimist@1.2.8: {} + + mjolnir.js@3.0.0: {} + + ms@2.1.3: {} + + murmurhash-js@1.0.0: {} + + nanoid@3.3.12: {} + + node-releases@2.0.38: {} + + pako@1.0.11: {} + + path-expression-matcher@1.5.0: {} + + pbf@3.3.0: + dependencies: + ieee754: 1.2.1 + resolve-protobuf-schema: 2.1.0 + + pbf@4.0.1: + dependencies: + resolve-protobuf-schema: 2.1.0 + + picocolors@1.1.1: {} + + postcss@8.5.13: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + potpack@2.1.0: {} + + preact@10.29.1: {} + + process-nextick-args@2.0.1: {} + + protocol-buffers-schema@3.6.1: {} + + quickselect@3.0.0: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-map-gl@7.1.9(mapbox-gl@3.23.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@maplibre/maplibre-gl-style-spec': 19.3.3 + '@types/mapbox-gl': 3.5.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + mapbox-gl: 3.23.0 + + react-refresh@0.17.0: {} + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + resolve-protobuf-schema@2.1.0: + dependencies: + protocol-buffers-schema: 3.6.1 + + robust-predicates@2.0.4: {} + + rollup@4.60.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.2 + '@rollup/rollup-android-arm64': 4.60.2 + '@rollup/rollup-darwin-arm64': 4.60.2 + '@rollup/rollup-darwin-x64': 4.60.2 + '@rollup/rollup-freebsd-arm64': 4.60.2 + '@rollup/rollup-freebsd-x64': 4.60.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.2 + '@rollup/rollup-linux-arm-musleabihf': 4.60.2 + '@rollup/rollup-linux-arm64-gnu': 4.60.2 + '@rollup/rollup-linux-arm64-musl': 4.60.2 + '@rollup/rollup-linux-loong64-gnu': 4.60.2 + '@rollup/rollup-linux-loong64-musl': 4.60.2 + '@rollup/rollup-linux-ppc64-gnu': 4.60.2 + '@rollup/rollup-linux-ppc64-musl': 4.60.2 + '@rollup/rollup-linux-riscv64-gnu': 4.60.2 + '@rollup/rollup-linux-riscv64-musl': 4.60.2 + '@rollup/rollup-linux-s390x-gnu': 4.60.2 + '@rollup/rollup-linux-x64-gnu': 4.60.2 + '@rollup/rollup-linux-x64-musl': 4.60.2 + '@rollup/rollup-openbsd-x64': 4.60.2 + '@rollup/rollup-openharmony-arm64': 4.60.2 + '@rollup/rollup-win32-arm64-msvc': 4.60.2 + '@rollup/rollup-win32-ia32-msvc': 4.60.2 + '@rollup/rollup-win32-x64-gnu': 4.60.2 + '@rollup/rollup-win32-x64-msvc': 4.60.2 + fsevents: 2.3.3 + + rw@1.3.3: {} + + safe-buffer@5.1.2: {} + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + set-value@2.0.1: + dependencies: + extend-shallow: 2.0.1 + is-extendable: 0.1.1 + is-plain-object: 2.0.4 + split-string: 3.1.0 + + setimmediate@1.0.5: {} + + snappyjs@0.6.1: {} + + sort-asc@0.2.0: {} + + sort-desc@0.2.0: {} + + sort-object@3.0.3: + dependencies: + bytewise: 1.1.0 + get-value: 2.0.6 + is-extendable: 0.1.1 + sort-asc: 0.2.0 + sort-desc: 0.2.0 + union-value: 1.0.1 + + source-map-js@1.2.1: {} + + splaytree@0.1.4: {} + + split-string@3.1.0: + dependencies: + extend-shallow: 3.0.2 + + sprintf-js@1.0.3: {} + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + strnum@2.2.3: {} + + supercluster@8.0.1: + dependencies: + kdbush: 4.0.2 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + table-layout@4.1.1: + dependencies: + array-back: 6.2.3 + wordwrapjs: 5.1.1 + + texture-compressor@1.0.2: + dependencies: + argparse: 1.0.10 + image-size: 0.7.5 + + tinyqueue@3.0.0: {} + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + typewise-core@1.2.0: {} + + typewise@1.0.3: + dependencies: + typewise-core: 1.2.0 + + typical@7.3.0: {} + + undici-types@7.16.0: {} + + undici-types@7.19.2: + optional: true + + union-value@1.0.1: + dependencies: + arr-union: 3.1.0 + get-value: 2.0.6 + is-extendable: 0.1.1 + set-value: 2.0.1 + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + + util-deprecate@1.0.2: {} + + vite@5.4.21(@types/node@25.6.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.13 + rollup: 4.60.2 + optionalDependencies: + '@types/node': 25.6.0 + fsevents: 2.3.3 + + wordwrapjs@5.1.1: {} + + yallist@3.1.1: {} + + zstd-codec@0.1.5: + optional: true + + zustand@4.5.7(@types/react@18.3.28)(react@18.3.1): + dependencies: + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + react: 18.3.1 diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..6e82a31 --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,50 @@ +import { useMemo } from 'react'; +import { differenceInCalendarDays, parseISO } from 'date-fns'; +import { useTrips } from './hooks/useTrips'; +import { useVizStore } from './store/useVizStore'; +import { MapView } from './components/MapView'; +import { DateControls } from './components/Sidebar/DateControls'; +import { CostCentreFilter } from './components/Sidebar/CostCentreFilter'; +import { VehicleDrilldown } from './components/Sidebar/VehicleDrilldown'; +import { TimeSlider } from './components/Timebar/TimeSlider'; + +export default function App() { + const mode = useVizStore((s) => s.mode); + const date = useVizStore((s) => s.date); + const rangeStart = useVizStore((s) => s.rangeStart); + const rangeEnd = useVizStore((s) => s.rangeEnd); + const { data: trips, isLoading, error } = useTrips(); + + const windowStartIso = mode === 'day' ? date : rangeStart; + const windowDays = useMemo(() => { + if (mode === 'day') return 1; + return Math.max(1, differenceInCalendarDays(parseISO(rangeEnd), parseISO(rangeStart)) + 1); + }, [mode, rangeStart, rangeEnd]); + const maxSeconds = windowDays * 86400; + + return ( +
+ + + +
+ ); +} diff --git a/web/src/components/MapView.tsx b/web/src/components/MapView.tsx new file mode 100644 index 0000000..d5289ee --- /dev/null +++ b/web/src/components/MapView.tsx @@ -0,0 +1,50 @@ +import { useMemo } from 'react'; +import DeckGL from '@deck.gl/react'; +import { Map } from 'react-map-gl'; +import 'mapbox-gl/dist/mapbox-gl.css'; +import type { Trip } from '../types/Trip'; +import { buildLayers } from '../layers/buildLayers'; +import { useVizStore } from '../store/useVizStore'; + +const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_TOKEN as string | undefined; + +interface Props { + trips: Trip[]; + windowStartIso: string; +} + +const INITIAL_VIEW_STATE = { + longitude: 36.8219, + latitude: -1.2921, + zoom: 9, + pitch: 0, + bearing: 0, +}; + +export function MapView({ trips, windowStartIso }: Props) { + const currentTime = useVizStore((s) => s.currentTime); + const trailLength = useVizStore((s) => s.trailLength); + const focusedImei = useVizStore((s) => s.focusedImei); + + const layers = useMemo( + () => buildLayers({ trips, windowStartIso, currentTime, trailLength, focusedImei }), + [trips, windowStartIso, currentTime, trailLength, focusedImei], + ); + + return ( +
+ + {MAPBOX_TOKEN ? ( + + ) : null} + + {!MAPBOX_TOKEN ? ( +
No VITE_MAPBOX_TOKEN set — base map disabled.
+ ) : null} +
+ ); +} diff --git a/web/src/components/Sidebar/CostCentreFilter.tsx b/web/src/components/Sidebar/CostCentreFilter.tsx new file mode 100644 index 0000000..6c3129b --- /dev/null +++ b/web/src/components/Sidebar/CostCentreFilter.tsx @@ -0,0 +1,67 @@ +import { useCostCentres } from '../../hooks/useTrips'; +import { useVizStore } from '../../store/useVizStore'; +import { colorFor, rgbCss } from '../../lib/color'; + +export function CostCentreFilter() { + const { data, isLoading, error } = useCostCentres(); + const selected = useVizStore((s) => s.costCentres); + const setCostCentres = useVizStore((s) => s.setCostCentres); + + if (isLoading) return
Loading cost centres…
; + if (error) return
{(error as Error).message}
; + if (!data) return null; + + const allNames = data.map((d) => d.cost_centre); + const isAll = selected === null; + const isOn = (cc: string) => isAll || selected!.includes(cc); + + const toggle = (cc: string) => { + if (isAll) { + // From "all selected" → uncheck just this one + setCostCentres(allNames.filter((x) => x !== cc)); + return; + } + const cur = selected!; + const next = cur.includes(cc) ? cur.filter((x) => x !== cc) : [...cur, cc]; + // If user re-checks every box, collapse back to null = ALL + setCostCentres(next.length === allNames.length ? null : next); + }; + + return ( +
+

Cost centre

+
+ + +
+
+ {data.map((d) => { + const on = isOn(d.cost_centre); + return ( +
toggle(d.cost_centre)} + > + + {d.cost_centre} + {d.vehicle_count} +
+ ); + })} +
+
+ ); +} diff --git a/web/src/components/Sidebar/DateControls.tsx b/web/src/components/Sidebar/DateControls.tsx new file mode 100644 index 0000000..cb83a78 --- /dev/null +++ b/web/src/components/Sidebar/DateControls.tsx @@ -0,0 +1,47 @@ +import { useVizStore } from '../../store/useVizStore'; + +export function DateControls() { + const mode = useVizStore((s) => s.mode); + const setMode = useVizStore((s) => s.setMode); + const date = useVizStore((s) => s.date); + const setDate = useVizStore((s) => s.setDate); + const rangeStart = useVizStore((s) => s.rangeStart); + const rangeEnd = useVizStore((s) => s.rangeEnd); + const setRange = useVizStore((s) => s.setRange); + + return ( +
+

Time window

+
+ + +
+ {mode === 'day' ? ( +
+ setDate(e.target.value)} /> +
+ ) : ( + <> +
+ setRange(e.target.value, rangeEnd)} + /> + + setRange(rangeStart, e.target.value)} + /> +
+

Server caps range at 14 days.

+ + )} +
+ ); +} diff --git a/web/src/components/Sidebar/VehicleDrilldown.tsx b/web/src/components/Sidebar/VehicleDrilldown.tsx new file mode 100644 index 0000000..0be76ea --- /dev/null +++ b/web/src/components/Sidebar/VehicleDrilldown.tsx @@ -0,0 +1,63 @@ +import { useMemo } from 'react'; +import { useTrips } from '../../hooks/useTrips'; +import { useVizStore } from '../../store/useVizStore'; + +export function VehicleDrilldown() { + const { data, isLoading } = useTrips(); + const focusedImei = useVizStore((s) => s.focusedImei); + const setFocusedImei = useVizStore((s) => s.setFocusedImei); + + const vehicles = useMemo(() => { + if (!data) return []; + const seen = new Map(); + for (const t of data) { + const cur = seen.get(t.imei); + if (cur) cur.trips += 1; + else + seen.set(t.imei, { + imei: t.imei, + label: t.vehicle_name || t.vehicle_number || t.imei, + trips: 1, + }); + } + return Array.from(seen.values()).sort((a, b) => b.trips - a.trips); + }, [data]); + + return ( +
+

Vehicles ({vehicles.length})

+ {isLoading ? ( +
Loading…
+ ) : ( + <> + {focusedImei ? ( + + ) : null} +
+ {vehicles.map((v) => ( +
setFocusedImei(focusedImei === v.imei ? null : v.imei)} + title={v.imei} + > + {v.label} · {v.trips} +
+ ))} + {vehicles.length === 0 ? ( +
+ No trips in this window. +
+ ) : null} +
+ + )} +
+ ); +} diff --git a/web/src/components/Timebar/TimeSlider.tsx b/web/src/components/Timebar/TimeSlider.tsx new file mode 100644 index 0000000..c73c8e6 --- /dev/null +++ b/web/src/components/Timebar/TimeSlider.tsx @@ -0,0 +1,61 @@ +import { useVizStore, type Speed } from '../../store/useVizStore'; +import { useAnimationLoop } from '../../hooks/useAnimationLoop'; + +const SPEEDS: Speed[] = [1, 10, 60, 600]; + +function fmtClock(seconds: number): string { + const sec = Math.max(0, Math.floor(seconds)); + const days = Math.floor(sec / 86400); + const h = Math.floor((sec % 86400) / 3600); + const m = Math.floor((sec % 3600) / 60); + const s = sec % 60; + const hms = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; + return days > 0 ? `D${days + 1} ${hms}` : hms; +} + +interface Props { + maxSeconds: number; +} + +export function TimeSlider({ maxSeconds }: Props) { + const currentTime = useVizStore((s) => s.currentTime); + const setCurrentTime = useVizStore((s) => s.setCurrentTime); + const playing = useVizStore((s) => s.playing); + const setPlaying = useVizStore((s) => s.setPlaying); + const speed = useVizStore((s) => s.speed); + const setSpeed = useVizStore((s) => s.setSpeed); + + useAnimationLoop(maxSeconds); + + return ( +
+ + {fmtClock(currentTime)} + setCurrentTime(Number(e.target.value))} + /> +
+ {SPEEDS.map((sp) => ( + + ))} +
+
+ ); +} diff --git a/web/src/hooks/useAnimationLoop.ts b/web/src/hooks/useAnimationLoop.ts new file mode 100644 index 0000000..eeed72d --- /dev/null +++ b/web/src/hooks/useAnimationLoop.ts @@ -0,0 +1,30 @@ +import { useEffect, useRef } from 'react'; +import { useVizStore } from '../store/useVizStore'; + +// Drives currentTime forward while `playing`. Wraps at the end of the window. +export function useAnimationLoop(maxSeconds: number) { + const playing = useVizStore((s) => s.playing); + const speed = useVizStore((s) => s.speed); + const setCurrentTime = useVizStore((s) => s.setCurrentTime); + const lastFrameRef = useRef(null); + + useEffect(() => { + if (!playing || maxSeconds <= 0) return; + let raf = 0; + const tick = (ts: number) => { + const last = lastFrameRef.current ?? ts; + const dtSec = (ts - last) / 1000; + lastFrameRef.current = ts; + const cur = useVizStore.getState().currentTime; + let next = cur + dtSec * speed; + if (next >= maxSeconds) next = next % maxSeconds; + setCurrentTime(next); + raf = requestAnimationFrame(tick); + }; + raf = requestAnimationFrame(tick); + return () => { + cancelAnimationFrame(raf); + lastFrameRef.current = null; + }; + }, [playing, speed, maxSeconds, setCurrentTime]); +} diff --git a/web/src/hooks/useTrips.ts b/web/src/hooks/useTrips.ts new file mode 100644 index 0000000..3751929 --- /dev/null +++ b/web/src/hooks/useTrips.ts @@ -0,0 +1,24 @@ +import { useQuery } from '@tanstack/react-query'; +import { api } from '../lib/api'; +import { useVizStore } from '../store/useVizStore'; + +export function useTrips() { + const { mode, date, rangeStart, rangeEnd, costCentres } = useVizStore(); + const ccKey = costCentres ? [...costCentres].sort().join(',') : 'ALL'; + + return useQuery({ + queryKey: ['trips', mode, mode === 'day' ? date : `${rangeStart}..${rangeEnd}`, ccKey], + queryFn: () => + mode === 'day' + ? api.tripsForDay(date, costCentres) + : api.tripsForRange(rangeStart, rangeEnd, costCentres), + }); +} + +export function useCostCentres() { + return useQuery({ + queryKey: ['cost_centres'], + queryFn: () => api.listCostCentres(), + staleTime: 60 * 60_000, + }); +} diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000..aaa9701 --- /dev/null +++ b/web/src/index.css @@ -0,0 +1,230 @@ +:root { + color-scheme: dark; + --bg: #0b0d12; + --panel: #11151c; + --panel-2: #161b25; + --border: #232a36; + --text: #e6e8ec; + --text-dim: #9aa3b2; + --accent: #5b8def; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; +} + +* { box-sizing: border-box; } + +html, body, #root { + margin: 0; + height: 100%; + background: var(--bg); + color: var(--text); + overflow: hidden; +} + +button, input, select { + font: inherit; + color: inherit; +} + +.app { + display: grid; + grid-template-columns: 320px 1fr; + grid-template-rows: 1fr auto; + grid-template-areas: + "sidebar map" + "sidebar timebar"; + height: 100vh; +} + +.sidebar { + grid-area: sidebar; + background: var(--panel); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + overflow-y: auto; + padding: 16px; + gap: 18px; +} + +.sidebar h1 { + font-size: 16px; + margin: 0 0 4px 0; + letter-spacing: 0.02em; +} +.sidebar h2 { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-dim); + margin: 0 0 6px 0; + font-weight: 600; +} + +.section { + display: flex; + flex-direction: column; + gap: 6px; +} + +.input-row { + display: flex; + align-items: center; + gap: 8px; +} + +.input-row input[type="date"], +.input-row select { + background: var(--panel-2); + border: 1px solid var(--border); + border-radius: 6px; + padding: 6px 8px; + flex: 1; +} + +.toggle { + display: inline-flex; + background: var(--panel-2); + border: 1px solid var(--border); + border-radius: 6px; + overflow: hidden; +} +.toggle button { + background: transparent; + border: 0; + padding: 6px 10px; + cursor: pointer; + color: var(--text-dim); +} +.toggle button.active { + background: var(--accent); + color: white; +} + +.cc-row { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + border-radius: 6px; + cursor: pointer; + user-select: none; +} +.cc-row:hover { background: var(--panel-2); } +.cc-row .swatch { + width: 12px; + height: 12px; + border-radius: 2px; + flex-shrink: 0; +} +.cc-row .name { + flex: 1; + text-transform: capitalize; +} +.cc-row .count { + color: var(--text-dim); + font-variant-numeric: tabular-nums; + font-size: 12px; +} +.cc-row.muted .name, .cc-row.muted .count { + color: var(--text-dim); + text-decoration: line-through; +} + +.vehicle-list { + display: flex; + flex-direction: column; + max-height: 220px; + overflow-y: auto; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--panel-2); +} +.vehicle-row { + padding: 6px 10px; + cursor: pointer; + border-bottom: 1px solid var(--border); + font-size: 13px; +} +.vehicle-row:last-child { border-bottom: 0; } +.vehicle-row:hover { background: rgba(255,255,255,0.04); } +.vehicle-row.active { background: rgba(91,141,239,0.18); } + +.map-area { + grid-area: map; + position: relative; + overflow: hidden; +} + +.timebar { + grid-area: timebar; + background: var(--panel); + border-top: 1px solid var(--border); + padding: 12px 16px; + display: flex; + align-items: center; + gap: 12px; +} +.timebar .clock { + font-variant-numeric: tabular-nums; + font-size: 14px; + min-width: 110px; + color: var(--text-dim); +} +.timebar input[type="range"] { + flex: 1; + accent-color: var(--accent); +} +.timebar .play { + background: var(--accent); + color: white; + border: 0; + width: 34px; + height: 34px; + border-radius: 50%; + cursor: pointer; + font-size: 14px; +} +.timebar .speed { + display: inline-flex; + background: var(--panel-2); + border: 1px solid var(--border); + border-radius: 6px; + overflow: hidden; +} +.timebar .speed button { + background: transparent; + border: 0; + color: var(--text-dim); + padding: 6px 8px; + cursor: pointer; + font-size: 12px; +} +.timebar .speed button.active { + background: var(--accent); + color: white; +} + +.status { + font-size: 12px; + color: var(--text-dim); + padding: 4px 0; +} +.status.error { color: #ff7b72; } + +.legend-note { + font-size: 11px; + color: var(--text-dim); + line-height: 1.4; +} + +.banner { + position: absolute; + top: 12px; + left: 12px; + background: rgba(11, 13, 18, 0.85); + border: 1px solid var(--border); + padding: 6px 10px; + border-radius: 6px; + font-size: 12px; + pointer-events: none; +} diff --git a/web/src/layers/buildLayers.ts b/web/src/layers/buildLayers.ts new file mode 100644 index 0000000..ed0dd1b --- /dev/null +++ b/web/src/layers/buildLayers.ts @@ -0,0 +1,77 @@ +import { PathLayer } from '@deck.gl/layers'; +import { TripsLayer } from '@deck.gl/geo-layers'; +import type { Layer } from '@deck.gl/core'; +import type { Trip } from '../types/Trip'; +import { colorFor } from '../lib/color'; + +interface NormalizedTrip extends Trip { + // path coordinates flattened into [[lng,lat], ...] (already that shape from PostGIS) + // timestamps offset to "seconds since 00:00 of the first day in view" + abs_timestamps: number[]; + color: [number, number, number]; +} + +/** + * Offset a trip's per-vertex timestamps from "seconds since trip start" + * to "seconds since 00:00 of `windowStartIso`" so a single TripsLayer + * can animate every trip on one shared timeline. + */ +function normalize(trip: Trip, windowStartUtc: number): NormalizedTrip { + const tripStartUtc = new Date(trip.start_time).getTime() / 1000; + const offset = tripStartUtc - windowStartUtc; + return { + ...trip, + abs_timestamps: trip.timestamps_rel.map((t) => t + offset), + color: colorFor(trip.cost_centre), + }; +} + +export interface BuildLayersArgs { + trips: Trip[]; + windowStartIso: string; // 'yyyy-MM-dd' for day mode, range start for range mode + currentTime: number; // seconds since windowStart 00:00 + trailLength: number; // seconds + focusedImei: string | null; // when set, dim everything else +} + +export function buildLayers({ + trips, + windowStartIso, + currentTime, + trailLength, + focusedImei, +}: BuildLayersArgs): Layer[] { + const windowStartUtc = new Date(`${windowStartIso}T00:00:00Z`).getTime() / 1000; + const normalized = trips.map((t) => normalize(t, windowStartUtc)); + const visible = focusedImei + ? normalized.filter((t) => t.imei === focusedImei) + : normalized; + + const pathLayer = new PathLayer({ + id: 'paths', + data: visible, + getPath: (d) => d.path_geojson.coordinates as unknown as [number, number][], + getColor: (d) => [...d.color, 90] as [number, number, number, number], + getWidth: 3, + widthUnits: 'pixels', + pickable: false, + }); + + const tripsLayer = new TripsLayer({ + id: 'trips', + data: visible, + getPath: (d) => d.path_geojson.coordinates as unknown as [number, number][], + getTimestamps: (d) => d.abs_timestamps, + getColor: (d) => d.color, + getWidth: 4, + widthUnits: 'pixels', + capRounded: true, + jointRounded: true, + trailLength, + currentTime, + fadeTrail: true, + pickable: true, + }); + + return [pathLayer, tripsLayer]; +} diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts new file mode 100644 index 0000000..0461a3d --- /dev/null +++ b/web/src/lib/api.ts @@ -0,0 +1,33 @@ +import type { Trip, CostCentreCount } from '../types/Trip'; + +const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:3000'; + +async function postRpc(rpc: string, body: Record): Promise { + const res = await fetch(`${API_URL}/rpc/${rpc}`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`PostgREST ${rpc} ${res.status}: ${text}`); + } + return res.json() as Promise; +} + +async function getRpc(rpc: string): Promise { + const res = await fetch(`${API_URL}/rpc/${rpc}`); + if (!res.ok) { + const text = await res.text(); + throw new Error(`PostgREST ${rpc} ${res.status}: ${text}`); + } + return res.json() as Promise; +} + +export const api = { + listCostCentres: () => getRpc('list_cost_centres'), + tripsForDay: (p_date: string, p_cost_centres: string[] | null) => + postRpc('trips_for_day', { p_date, p_cost_centres }), + tripsForRange: (p_start: string, p_end: string, p_cost_centres: string[] | null) => + postRpc('trips_for_range', { p_start, p_end, p_cost_centres }), +}; diff --git a/web/src/lib/color.ts b/web/src/lib/color.ts new file mode 100644 index 0000000..2c28c8b --- /dev/null +++ b/web/src/lib/color.ts @@ -0,0 +1,36 @@ +// Categorical 12-color palette (deck.gl's "Vivid" applied to dark backgrounds). +// 'Unassigned' always renders as a neutral grey so the gap is visible but quiet. +const PALETTE: [number, number, number][] = [ + [91, 141, 239], + [239, 138, 91], + [122, 209, 145], + [231, 99, 152], + [240, 199, 81], + [156, 124, 235], + [80, 200, 200], + [223, 124, 90], + [180, 211, 96], + [220, 124, 195], + [120, 168, 240], + [222, 165, 84], +]; + +const UNASSIGNED: [number, number, number] = [136, 136, 136]; + +function hashStr(s: string): number { + let h = 2166136261; + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619); + } + return h >>> 0; +} + +export function colorFor(costCentre: string): [number, number, number] { + if (costCentre === 'Unassigned') return UNASSIGNED; + return PALETTE[hashStr(costCentre) % PALETTE.length]; +} + +export function rgbCss([r, g, b]: [number, number, number]): string { + return `rgb(${r}, ${g}, ${b})`; +} diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000..5d2183e --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,24 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import App from './App'; +import './index.css'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 5 * 60_000, + gcTime: 30 * 60_000, + refetchOnWindowFocus: false, + retry: 1, + }, + }, +}); + +createRoot(document.getElementById('root')!).render( + + + + + , +); diff --git a/web/src/store/useVizStore.ts b/web/src/store/useVizStore.ts new file mode 100644 index 0000000..dc60e95 --- /dev/null +++ b/web/src/store/useVizStore.ts @@ -0,0 +1,65 @@ +import { create } from 'zustand'; +import { format, subDays } from 'date-fns'; + +export type Mode = 'day' | 'range'; +export type Speed = 1 | 10 | 60 | 600; + +interface VizState { + mode: Mode; + date: string; // ISO yyyy-MM-dd, used in 'day' mode + rangeStart: string; + rangeEnd: string; + costCentres: string[] | null; // null = all + focusedImei: string | null; + currentTime: number; // seconds since 00:00 of the first day in view + playing: boolean; + speed: Speed; + trailLength: number; // seconds of comet tail + + setMode: (m: Mode) => void; + setDate: (d: string) => void; + setRange: (start: string, end: string) => void; + setCostCentres: (cc: string[] | null) => void; + toggleCostCentre: (cc: string) => void; + setFocusedImei: (imei: string | null) => void; + setCurrentTime: (t: number) => void; + setPlaying: (p: boolean) => void; + setSpeed: (s: Speed) => void; + setTrailLength: (n: number) => void; +} + +const yesterday = format(subDays(new Date(), 1), 'yyyy-MM-dd'); + +export const useVizStore = create((set, get) => ({ + mode: 'day', + date: yesterday, + rangeStart: format(subDays(new Date(), 7), 'yyyy-MM-dd'), + rangeEnd: yesterday, + costCentres: null, + focusedImei: null, + currentTime: 0, + playing: false, + speed: 60, + trailLength: 600, + + setMode: (m) => set({ mode: m, currentTime: 0, playing: false }), + setDate: (d) => set({ date: d, currentTime: 0 }), + setRange: (rangeStart, rangeEnd) => set({ rangeStart, rangeEnd, currentTime: 0 }), + setCostCentres: (cc) => set({ costCentres: cc }), + toggleCostCentre: (cc) => { + const cur = get().costCentres; + if (cur === null) { + // first toggle: deselect just this one + set({ costCentres: ['__placeholder__'] }); // will be refined by caller with full list + return; + } + set({ + costCentres: cur.includes(cc) ? cur.filter((x) => x !== cc) : [...cur, cc], + }); + }, + setFocusedImei: (imei) => set({ focusedImei: imei }), + setCurrentTime: (t) => set({ currentTime: t }), + setPlaying: (p) => set({ playing: p }), + setSpeed: (s) => set({ speed: s }), + setTrailLength: (n) => set({ trailLength: n }), +})); diff --git a/web/src/types/Trip.ts b/web/src/types/Trip.ts new file mode 100644 index 0000000..6ec649a --- /dev/null +++ b/web/src/types/Trip.ts @@ -0,0 +1,22 @@ +export interface LineStringGeoJSON { + type: 'LineString'; + coordinates: [number, number][]; +} + +export interface Trip { + trip_id: number; + imei: string; + vehicle_name: string | null; + vehicle_number: string | null; + cost_centre: string; + start_time: string; + end_time: string; + distance_km: number | null; + path_geojson: LineStringGeoJSON; + timestamps_rel: number[]; +} + +export interface CostCentreCount { + cost_centre: string; + vehicle_count: number; +} diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts new file mode 100644 index 0000000..67a89e1 --- /dev/null +++ b/web/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_URL?: string; + readonly VITE_MAPBOX_TOKEN?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json new file mode 100644 index 0000000..ca12b88 --- /dev/null +++ b/web/tsconfig.app.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": false, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true + }, + "include": ["src"] +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json new file mode 100644 index 0000000..1b2185a --- /dev/null +++ b/web/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "noEmit": true + }, + "include": ["vite.config.ts"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..f821ff9 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + host: '0.0.0.0', + port: 5173, + }, + preview: { + host: '0.0.0.0', + port: 8080, + }, +});