feat: task 2.1 MapView singleton + mapReady gate
- pnpm add maplibre-gl + -D @types/geojson. - src/map/core/styles.ts: defaultStyle (OSM raster bootstrap; 2.2 replaces with the basemap-switcher descriptor table). - src/map/core/map-view.tsx: module-level Map singleton lazily created on first <MapView> mount, attached to a class="trm-map-host" detached <div> that React refs append/remove on mount/unmount. Style-data lifecycle flips mapReady false on every styledata event, polls loaded() at 33ms intervals, flips ready true once the style is loaded — the canonical MapLibre style-swap dance. - Exports getMap()/getMapReady()/subscribeMapReady()/useMapReady (via useSyncExternalStore for SSR-safe + concurrent-safe reads). getMap() throws if called pre-mount; the explicit failure mode beats a null-able top-level export. - src/routes/_authed/monitor.tsx: new /monitor route, full-viewport <MapView /> for 2.1 (no children — subsequent tasks plug in here). - src/routes/_authed/index.tsx: home-page card now links to /monitor. - eslint.config.js: override for src/map/** + src/live/** disables react-refresh/only-export-components. Same pattern as the existing overrides for shadcn primitives and route files. Deviation: spec sketched a top-level `map` constant export; implemented as `getMap(): MapLibreMap` (a function) so the singleton stays lazy until <MapView> mounts. Top-level constant would either force eager init (breaks SSR/tests) or be nullable (footgun). The function form throws a clear error if called pre-mount. Bundle: /monitor lazy chunk is 1MB raw / 274KB gzipped (MapLibre + CSS). Other routes unaffected. Vite chunk-size warning is harmless.
This commit is contained in:
@@ -190,4 +190,30 @@ For 2.1, no children — the map renders the basemap and nothing else. Subsequen
|
||||
|
||||
## Done
|
||||
|
||||
(Filled in when the task lands.)
|
||||
Three new files + small updates to existing surfaces:
|
||||
|
||||
- **`src/map/core/styles.ts`** — `defaultStyle` exporting an OSM raster `StyleSpecification` (single raster source + raster layer + glyphs URL for label rendering on future symbol layers). 2.2 will replace this with the descriptor table + switcher.
|
||||
- **`src/map/core/map-view.tsx`** — singleton + `<MapView>` + `mapReady` gate. Exports `getMap()` (throws if called pre-mount), `getMapReady()`, `subscribeMapReady()`, `useMapReady()` (via `useSyncExternalStore`). The singleton's container has class `trm-map-host` for CSS targeting if global resets ever bite. Style-data lifecycle handler (`onStyleData`) flips ready false → polls `loaded()` → flips ready true; that's the canonical MapLibre style-swap dance.
|
||||
- **`src/routes/_authed/monitor.tsx`** — new route at `/monitor` rendering `<MapView />` full-viewport. No children for 2.1; subsequent tasks plug components in here.
|
||||
- **`src/routes/_authed/index.tsx`** — home-page card now renders a `<Link to="/monitor">Open live map →</Link>`.
|
||||
- **`eslint.config.js`** — added override for `src/map/**` and `src/live/**` disabling `react-refresh/only-export-components`. These modules intentionally co-export non-component utilities (singletons, factories, hooks) alongside the React component. Same pattern as the existing override for `src/ui/primitives/**` and `src/routes/**`.
|
||||
|
||||
**Deps installed:** `maplibre-gl@5.24.0` (runtime), `@types/geojson@7946.0.16` (devDep).
|
||||
|
||||
**Deviation flagged:**
|
||||
|
||||
The spec sketched the singleton's accessor as `map` (a top-level export). Implemented as `getMap(): MapLibreMap` instead — a function call rather than an exported constant. Two reasons:
|
||||
1. The singleton is created lazily on first `<MapView>` mount, not at module import. A top-level `map` export would either force eager initialisation (breaks SSR / tests) or be `null` until something mounts (footgun).
|
||||
2. `getMap()` throws a clear error if called before `<MapView>` mounts — a missing pre-condition that's easy to surface and easy to read in stack traces. A `null`-able export forces every consumer to add their own null check.
|
||||
|
||||
Side-effect-only `Map*` components (2.5+) call `getMap()` from inside their `useEffect` *after* `useMapReady()` returns true, by which point the singleton exists and the style is loaded.
|
||||
|
||||
**Smoke check (local `pnpm dev`):**
|
||||
- `/monitor` renders a full-viewport OSM map.
|
||||
- Pan / zoom / scroll-zoom all work.
|
||||
- `/monitor` → `/` → `/monitor` keeps the map responsive (singleton survives navigation).
|
||||
- Browser console: `(await import('@/map/core/map-view')).getMapReady()` returns `true` after the page settles.
|
||||
|
||||
**Bundle impact:** the `monitor` route is a new lazy chunk (~1MB raw / 274KB gzipped — MapLibre + its CSS). It's loaded only when the route is visited; all other routes are unaffected. Vite emits a chunk-size warning >500KB; expected and harmless. The home page's bundle is unchanged.
|
||||
|
||||
Landed in `PENDING_SHA`.
|
||||
|
||||
@@ -74,7 +74,9 @@ export const BASEMAP_STYLES: BasemapDescriptor[] = [
|
||||
id: 'esri-satellite',
|
||||
label: 'Satellite',
|
||||
style: styleCustom({
|
||||
tiles: ['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'],
|
||||
tiles: [
|
||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||
],
|
||||
attribution: 'Tiles © Esri',
|
||||
maxZoom: 19,
|
||||
}),
|
||||
|
||||
@@ -37,7 +37,11 @@ This is the throughput-discipline core. It runs whether or not the map is mounte
|
||||
interface LiveClient {
|
||||
connect(): void; // idempotent
|
||||
close(): void;
|
||||
subscribe(topic: string): Promise<{ ok: true; snapshot: PositionEntry[] } | { ok: false; code: string; message?: string }>;
|
||||
subscribe(
|
||||
topic: string,
|
||||
): Promise<
|
||||
{ ok: true; snapshot: PositionEntry[] } | { ok: false; code: string; message?: string }
|
||||
>;
|
||||
unsubscribe(topic: string): Promise<void>;
|
||||
onPosition(handler: (msg: PositionEntry & { topic: string }) => void): () => void;
|
||||
}
|
||||
@@ -50,6 +54,7 @@ This is the throughput-discipline core. It runs whether or not the map is mounte
|
||||
- `Coalescer.push(p: PositionEntry): void` — non-blocking; writes to the buffer.
|
||||
- Internally: per-device map of latest-position; rAF loop flushes to the consumer once per frame; clears the buffer.
|
||||
- **`src/live/position-store.ts`** — Zustand store:
|
||||
|
||||
```ts
|
||||
type PositionState = {
|
||||
latestByDevice: Map<string, PositionEntry>;
|
||||
@@ -65,7 +70,9 @@ This is the throughput-discipline core. It runs whether or not the map is mounte
|
||||
selectDevice(deviceId: string | null): void;
|
||||
};
|
||||
```
|
||||
|
||||
Trail ring buffer is bounded by `MAX_TRAIL_LENGTH` (default 200, see `MaxTrailLength` config below).
|
||||
|
||||
- **`src/live/connection-store.ts`** — Zustand store:
|
||||
```ts
|
||||
type ConnectionState = {
|
||||
@@ -104,7 +111,10 @@ function createLiveClient({ url }: { url: string }): LiveClient {
|
||||
let state: ClientState = { kind: 'idle' };
|
||||
const subscriptions = new Set<string>();
|
||||
const positionHandlers = new Set<(msg: PositionEntry & { topic: string }) => void>();
|
||||
const pendingSubscribes = new Map<string, { resolve: (r: SubscribeResult) => void; reject: (e: unknown) => void }>();
|
||||
const pendingSubscribes = new Map<
|
||||
string,
|
||||
{ resolve: (r: SubscribeResult) => void; reject: (e: unknown) => void }
|
||||
>();
|
||||
|
||||
function connect() {
|
||||
if (state.kind === 'connecting' || state.kind === 'connected') return;
|
||||
@@ -132,8 +142,7 @@ function createLiveClient({ url }: { url: string }): LiveClient {
|
||||
|
||||
function scheduleReconnect() {
|
||||
if (state.kind === 'closed') return;
|
||||
const attempt =
|
||||
state.kind === 'reconnecting' ? state.attempt + 1 : 1;
|
||||
const attempt = state.kind === 'reconnecting' ? state.attempt + 1 : 1;
|
||||
const delay = Math.min(
|
||||
RECONNECT_BACKOFF_MS[attempt - 1] ?? RECONNECT_CEILING_MS,
|
||||
RECONNECT_CEILING_MS,
|
||||
@@ -330,7 +339,7 @@ Mount inside `<AuthBootstrap>` so it only connects when authenticated. On logout
|
||||
- **Map rendering.** That's 2.5 / 2.6 — they read from the store.
|
||||
- **Event picking.** 2.7 — calls `client.subscribe('event:<id>')` and handles the snapshot.
|
||||
- **Connection-status UI.** 2.9 reads from `connection-store` and renders chips / banners.
|
||||
- **Backpressure / drop-oldest.** Defer until measured; the rAF coalescer caps the *flush* rate, not the *receive* rate. If receive ever overwhelms the buffer, add a per-device queue cap.
|
||||
- **Backpressure / drop-oldest.** Defer until measured; the rAF coalescer caps the _flush_ rate, not the _receive_ rate. If receive ever overwhelms the buffer, add a per-device queue cap.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
@@ -348,7 +357,7 @@ Mount inside `<AuthBootstrap>` so it only connects when authenticated. On logout
|
||||
- **rAF in background tabs.** Browsers throttle rAF when the tab is backgrounded. The coalescer effectively pauses; positions buffer until the tab is foregrounded again. Acceptable — operators backgrounding the tab don't need real-time updates.
|
||||
- **Zustand `Map<>` reactivity.** Zustand subscribers fire on reference changes, not deep equality. Wrapping `latest` and `trails` in `new Map(...)` per update is the right pattern; selectors derive specific deviceIds via `usePositionStore((s) => s.latestByDevice.get(deviceId))`.
|
||||
- **Trail-direction colour.** If we later add speed-coloured per-segment trails (2.6's open question), the trail entries need `speed` carried through. Already in `PositionEntry`. Good.
|
||||
- **Cookie auth on WS.** Browser sends the session cookie automatically with the WS upgrade *only if same-origin*. Verify the reverse-proxy + Vite-dev-proxy paths preserve same-origin (they do — `/ws-live` is on the page's origin). Worth a smoke test.
|
||||
- **Cookie auth on WS.** Browser sends the session cookie automatically with the WS upgrade _only if same-origin_. Verify the reverse-proxy + Vite-dev-proxy paths preserve same-origin (they do — `/ws-live` is on the page's origin). Worth a smoke test.
|
||||
|
||||
## Done
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
Render the trailing path of every (or only the selected) device as a polyline. Reads from the per-device ring buffer in the position store (2.4 capped at 200 points). Visible as a thin line behind the moving marker — operators see "where this racer has been in the last few minutes."
|
||||
|
||||
After this task lands, the live map shows motion *history*, not just current position. That's what makes "live tracking" feel live.
|
||||
After this task lands, the live map shows motion _history_, not just current position. That's what makes "live tracking" feel live.
|
||||
|
||||
## Deliverables
|
||||
|
||||
@@ -59,7 +59,11 @@ The third arg to `addLayer` puts the line layer below the position symbols; trai
|
||||
### Feature builder — flat colour per device
|
||||
|
||||
```ts
|
||||
function buildTrailFeature(deviceId: string, points: PositionEntry[], device?: Device): Feature | null {
|
||||
function buildTrailFeature(
|
||||
deviceId: string,
|
||||
points: PositionEntry[],
|
||||
device?: Device,
|
||||
): Feature | null {
|
||||
if (points.length < 2) return null;
|
||||
return {
|
||||
type: 'Feature',
|
||||
@@ -111,7 +115,7 @@ useEffect(() => {
|
||||
|
||||
### Speed-coloured per-segment (deferred decision)
|
||||
|
||||
Traccar's replay mode renders one line *segment* per consecutive position pair, coloured by the second point's speed. For live trails this would mean N-1 features per device per update — heavier but visually richer. Operators glance at colour to read "fast / slow / stopped."
|
||||
Traccar's replay mode renders one line _segment_ per consecutive position pair, coloured by the second point's speed. For live trails this would mean N-1 features per device per update — heavier but visually richer. Operators glance at colour to read "fast / slow / stopped."
|
||||
|
||||
If we decide to ship this in v1: replace `LineString` with multiple two-point `LineString`s and colour per segment. The setData payload grows; the line layer's `'line-color'` becomes `['get', 'segmentColor']` instead of the device's flat colour.
|
||||
|
||||
@@ -136,7 +140,7 @@ If operators want a longer-trail mode: a slider in the prefs (`50 / 200 / 500 /
|
||||
- [ ] With the dogfood seed and synthetic positions, `/monitor` shows a polyline trailing each device as it moves.
|
||||
- [ ] Switching the trail-mode toggle (none / selected / all) immediately changes which trails render. None → empty layer. Selected → only the selected device. All → every device.
|
||||
- [ ] Trail follows the device's most recent N positions; old points fall off the front as new ones append (verify trail is always exactly the configured length once steady-state).
|
||||
- [ ] Trails sit *underneath* markers (verifiable visually — markers paint over the polyline).
|
||||
- [ ] Trails sit _underneath_ markers (verifiable visually — markers paint over the polyline).
|
||||
- [ ] Style swap → trails reappear after the basemap change.
|
||||
- [ ] No `setData` warnings (e.g. coordinate-array mismatch).
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ After this task, the live map is operator-driven — no hardcoded event id, no d
|
||||
## Deliverables
|
||||
|
||||
- **`src/data/events.ts`** — TanStack Query hook + types:
|
||||
|
||||
```ts
|
||||
export type EventSummary = {
|
||||
id: string;
|
||||
@@ -27,7 +28,9 @@ After this task, the live map is operator-driven — no hardcoded event id, no d
|
||||
|
||||
export function useUserEvents(): UseQueryResult<EventSummary[]>;
|
||||
```
|
||||
|
||||
Fetches `/items/events?fields=id,name,slug,discipline,starts_at,ends_at,organization_id&sort=-starts_at`. Directus's RLS handles "what events does this user have access to" via the cookie. Stale time 5 minutes.
|
||||
|
||||
- **`src/ui/components/event-picker.tsx`** — `<EventPicker />` component. Top-of-page dropdown showing the events list. Selected event highlights. On select, calls a callback (passed in by the monitor route).
|
||||
- **`src/routes/_authed/monitor.tsx`** updated — orchestrates the picker → subscribe → snapshot → store flow:
|
||||
```tsx
|
||||
@@ -107,7 +110,8 @@ export function useActiveEventOrchestration() {
|
||||
const clearStore = usePositionStore((s) => s.clearForEvent);
|
||||
const activeEventId = usePositionStore((s) => s.activeEventId);
|
||||
|
||||
return useCallback(async (eventId: string | null) => {
|
||||
return useCallback(
|
||||
async (eventId: string | null) => {
|
||||
const client = getLiveClient();
|
||||
|
||||
// Tear down the previous subscription.
|
||||
@@ -127,7 +131,9 @@ export function useActiveEventOrchestration() {
|
||||
}
|
||||
setActiveEventInStore(eventId, result.snapshot);
|
||||
localStorage.setItem('trm-active-event-id', eventId);
|
||||
}, [activeEventId, clearStore, setActiveEventInStore]);
|
||||
},
|
||||
[activeEventId, clearStore, setActiveEventInStore],
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -108,7 +108,13 @@ Two behaviours:
|
||||
### `<MapCamera coordinates>` — imperative one-shot
|
||||
|
||||
```tsx
|
||||
function MapCamera({ coordinates, padding }: { coordinates: [number, number][]; padding?: number }) {
|
||||
function MapCamera({
|
||||
coordinates,
|
||||
padding,
|
||||
}: {
|
||||
coordinates: [number, number][];
|
||||
padding?: number;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
if (coordinates.length === 0) return;
|
||||
if (coordinates.length === 1) {
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
|
||||
## Goal
|
||||
|
||||
Show operators *just enough* about system state so they can answer two questions at a glance:
|
||||
Show operators _just enough_ about system state so they can answer two questions at a glance:
|
||||
|
||||
1. "Is the SPA still connected to live data?" — global WS status.
|
||||
2. "Is this specific device still reporting?" — per-device last-seen age.
|
||||
|
||||
Not noisy. Subtle UI; not banners and modal warnings. The design ethos: operators trust the map until something goes wrong, at which point they need a quick read of *what* went wrong without a wall of red.
|
||||
Not noisy. Subtle UI; not banners and modal warnings. The design ethos: operators trust the map until something goes wrong, at which point they need a quick read of _what_ went wrong without a wall of red.
|
||||
|
||||
## Deliverables
|
||||
|
||||
@@ -134,7 +134,7 @@ useEffect(() => {
|
||||
- [ ] `pnpm typecheck`, `pnpm lint`, `pnpm format:check`, `pnpm build` clean.
|
||||
- [ ] On `/monitor`, the connection chip top-right shows "Live" with a green dot during normal operation.
|
||||
- [ ] Disconnecting the network: the chip flips to "Reconnecting…" within a few seconds; on reconnect, back to "Live"; on permanent disconnect (close + no reconnect for 30s+), shows "Offline · last live HH:MM:SS".
|
||||
- [ ] A device that hasn't reported in 5+ minutes appears noticeably faded on the map (not invisible — operators still see *where it was last seen*).
|
||||
- [ ] A device that hasn't reported in 5+ minutes appears noticeably faded on the map (not invisible — operators still see _where it was last seen_).
|
||||
- [ ] On a fresh page load with no positions yet, the chip says "Connecting…" briefly, then "Live" once the snapshot arrives.
|
||||
- [ ] No banner / toast spam during normal operation.
|
||||
|
||||
|
||||
@@ -47,8 +47,8 @@ When Phase 2 is done:
|
||||
## Tasks
|
||||
|
||||
| # | Task | Status |
|
||||
| --- | ----------------------------------------------------------------------------------------------------- | ------ |
|
||||
| 2.1 | [MapView singleton + mapReady gate](./01-mapview-singleton.md) | ⬜ |
|
||||
| --- | ------------------------------------------------------------------------------------------ | ------ |
|
||||
| 2.1 | [MapView singleton + mapReady gate](./01-mapview-singleton.md) | 🟩 |
|
||||
| 2.2 | [Tile-source switcher](./02-tile-source-switcher.md) | ⬜ |
|
||||
| 2.3 | [Sprite preload (racing categories)](./03-sprite-preload.md) | ⬜ |
|
||||
| 2.4 | [WS client + rAF coalescer + Zustand position store](./04-ws-client-and-position-store.md) | ⬜ |
|
||||
@@ -95,9 +95,9 @@ spa/
|
||||
## Tech stack additions
|
||||
|
||||
- **`maplibre-gl`** — the rendering engine. Imported directly (no `react-map-gl` wrapper — see [[maps-architecture]] for why).
|
||||
- **`maplibre-google-maps`** *(optional, runtime-config-gated)* — protocol adapter for Google's Map Tiles API. Loaded only if `googleMapsKey` is present in the runtime config.
|
||||
- **`maplibre-google-maps`** _(optional, runtime-config-gated)_ — protocol adapter for Google's Map Tiles API. Loaded only if `googleMapsKey` is present in the runtime config.
|
||||
- **`@types/geojson`** — devDep for typing `Feature` / `FeatureCollection`.
|
||||
- **`pmtiles`** *(optional, defer)* — for offline tile archives in remote terrain. Out of scope for v1 of Phase 2.
|
||||
- **`pmtiles`** _(optional, defer)_ — for offline tile archives in remote terrain. Out of scope for v1 of Phase 2.
|
||||
|
||||
No new test infra (Vitest is Phase 3.6).
|
||||
|
||||
|
||||
@@ -45,4 +45,13 @@ export default defineConfig([
|
||||
'react-refresh/only-export-components': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Map and live modules co-export non-component utilities (singletons,
|
||||
// factories, hooks) alongside React components. The fast-refresh rule
|
||||
// doesn't apply usefully here.
|
||||
files: ['src/map/**/*.{ts,tsx}', 'src/live/**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': 'off',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.14.0",
|
||||
"maplibre-gl": "^5.24.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
@@ -37,6 +38,7 @@
|
||||
"@tanstack/react-router-devtools": "^1.166.13",
|
||||
"@tanstack/router-cli": "^1.166.40",
|
||||
"@tanstack/router-plugin": "^1.167.32",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
|
||||
Generated
+203
@@ -29,6 +29,9 @@ importers:
|
||||
lucide-react:
|
||||
specifier: ^1.14.0
|
||||
version: 1.14.0(react@19.2.5)
|
||||
maplibre-gl:
|
||||
specifier: ^5.24.0
|
||||
version: 5.24.0
|
||||
radix-ui:
|
||||
specifier: ^1.4.3
|
||||
version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
@@ -69,6 +72,9 @@ importers:
|
||||
'@tanstack/router-plugin':
|
||||
specifier: ^1.167.32
|
||||
version: 1.167.32(@tanstack/react-router@1.169.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1))
|
||||
'@types/geojson':
|
||||
specifier: ^7946.0.16
|
||||
version: 7946.0.16
|
||||
'@types/node':
|
||||
specifier: ^24.12.2
|
||||
version: 24.12.2
|
||||
@@ -308,6 +314,42 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@mapbox/jsonlint-lines-primitives@2.0.2':
|
||||
resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
'@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@2.0.4':
|
||||
resolution: {integrity: sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==}
|
||||
|
||||
'@mapbox/whoots-js@3.1.0':
|
||||
resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@maplibre/geojson-vt@5.0.4':
|
||||
resolution: {integrity: sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==}
|
||||
|
||||
'@maplibre/geojson-vt@6.1.0':
|
||||
resolution: {integrity: sha512-2eIY4gZxeKIVOZVNkAMb+5NgXhgsMQpOveTQAvnp53LYqHGJZDidk7Ew0Tged9PThidpbS+NFTh0g4zivhPDzQ==}
|
||||
|
||||
'@maplibre/maplibre-gl-style-spec@24.8.2':
|
||||
resolution: {integrity: sha512-bAFyH5a53/PAlTPftFBl/lr6jIcjWQtiVnNl5nobuGPt9duXRPjuFGetNG2fLzOhJyCk3BwB3xERdS1l/D4h4w==}
|
||||
hasBin: true
|
||||
|
||||
'@maplibre/mlt@1.1.9':
|
||||
resolution: {integrity: sha512-g/tD8EYJB97udq33ipuJ9a4Q7fcbZnTEnUrgnEc/tLMmEL+zaCbR+X5fkDBO2dgpaAMsLH179qE3UXg2N0Nc/g==}
|
||||
|
||||
'@maplibre/vt-pbf@4.3.0':
|
||||
resolution: {integrity: sha512-jIvp8F5hQCcreqOOpEt42TJMUlsrEcpf/kI1T2v85YrQRV6PPXUcEXUg5karKtH6oh47XJZ4kHu56pUkOuqA7w==}
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.4':
|
||||
resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
|
||||
peerDependencies:
|
||||
@@ -1322,6 +1364,9 @@ packages:
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
'@types/geojson@7946.0.16':
|
||||
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
|
||||
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
@@ -1336,6 +1381,9 @@ packages:
|
||||
'@types/react@19.2.14':
|
||||
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
|
||||
|
||||
'@types/supercluster@7.1.3':
|
||||
resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.59.1':
|
||||
resolution: {integrity: sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -1531,6 +1579,9 @@ packages:
|
||||
resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
|
||||
earcut@3.0.2:
|
||||
resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==}
|
||||
|
||||
electron-to-chromium@1.5.349:
|
||||
resolution: {integrity: sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==}
|
||||
|
||||
@@ -1679,6 +1730,9 @@ packages:
|
||||
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
gl-matrix@3.4.4:
|
||||
resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -1765,11 +1819,17 @@ packages:
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
json-stringify-pretty-compact@4.0.0:
|
||||
resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==}
|
||||
|
||||
json5@2.2.3:
|
||||
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
kdbush@4.0.2:
|
||||
resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==}
|
||||
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
@@ -1866,13 +1926,23 @@ packages:
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
maplibre-gl@5.24.0:
|
||||
resolution: {integrity: sha512-ALyFxgtd5R+65UqZ/++lOqwWcC0SNho9c27fYSyLmG7AfnAul2o46F05aDJGPbFU57wos9dgcIySHs0Xe6ia3A==}
|
||||
engines: {node: '>=16.14.0', npm: '>=8.1.0'}
|
||||
|
||||
minimatch@10.2.5:
|
||||
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
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}
|
||||
@@ -1911,6 +1981,10 @@ packages:
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
pbf@4.0.1:
|
||||
resolution: {integrity: sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==}
|
||||
hasBin: true
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -1926,6 +2000,9 @@ packages:
|
||||
resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
potpack@2.1.0:
|
||||
resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==}
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -1939,10 +2016,16 @@ packages:
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
protocol-buffers-schema@3.6.1:
|
||||
resolution: {integrity: sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==}
|
||||
|
||||
punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
quickselect@3.0.0:
|
||||
resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==}
|
||||
|
||||
radix-ui@1.4.3:
|
||||
resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==}
|
||||
peerDependencies:
|
||||
@@ -2009,11 +2092,17 @@ packages:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
resolve-protobuf-schema@2.1.0:
|
||||
resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==}
|
||||
|
||||
rolldown@1.0.0-rc.17:
|
||||
resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
rw@1.3.3:
|
||||
resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
|
||||
|
||||
scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
@@ -2056,6 +2145,9 @@ packages:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
supercluster@8.0.1:
|
||||
resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==}
|
||||
|
||||
synckit@0.11.12:
|
||||
resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
@@ -2074,6 +2166,9 @@ packages:
|
||||
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tinyqueue@3.0.0:
|
||||
resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
@@ -2475,6 +2570,52 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@mapbox/jsonlint-lines-primitives@2.0.2': {}
|
||||
|
||||
'@mapbox/point-geometry@1.1.0': {}
|
||||
|
||||
'@mapbox/tiny-sdf@2.1.0': {}
|
||||
|
||||
'@mapbox/unitbezier@0.0.1': {}
|
||||
|
||||
'@mapbox/vector-tile@2.0.4':
|
||||
dependencies:
|
||||
'@mapbox/point-geometry': 1.1.0
|
||||
'@types/geojson': 7946.0.16
|
||||
pbf: 4.0.1
|
||||
|
||||
'@mapbox/whoots-js@3.1.0': {}
|
||||
|
||||
'@maplibre/geojson-vt@5.0.4': {}
|
||||
|
||||
'@maplibre/geojson-vt@6.1.0':
|
||||
dependencies:
|
||||
kdbush: 4.0.2
|
||||
|
||||
'@maplibre/maplibre-gl-style-spec@24.8.2':
|
||||
dependencies:
|
||||
'@mapbox/jsonlint-lines-primitives': 2.0.2
|
||||
'@mapbox/unitbezier': 0.0.1
|
||||
json-stringify-pretty-compact: 4.0.0
|
||||
minimist: 1.2.8
|
||||
quickselect: 3.0.0
|
||||
rw: 1.3.3
|
||||
tinyqueue: 3.0.0
|
||||
|
||||
'@maplibre/mlt@1.1.9':
|
||||
dependencies:
|
||||
'@mapbox/point-geometry': 1.1.0
|
||||
|
||||
'@maplibre/vt-pbf@4.3.0':
|
||||
dependencies:
|
||||
'@mapbox/point-geometry': 1.1.0
|
||||
'@mapbox/vector-tile': 2.0.4
|
||||
'@maplibre/geojson-vt': 5.0.4
|
||||
'@types/geojson': 7946.0.16
|
||||
'@types/supercluster': 7.1.3
|
||||
pbf: 4.0.1
|
||||
supercluster: 8.0.1
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.10.0
|
||||
@@ -3484,6 +3625,8 @@ snapshots:
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/geojson@7946.0.16': {}
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/node@24.12.2':
|
||||
@@ -3498,6 +3641,10 @@ snapshots:
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
'@types/supercluster@7.1.3':
|
||||
dependencies:
|
||||
'@types/geojson': 7946.0.16
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
@@ -3711,6 +3858,8 @@ snapshots:
|
||||
|
||||
diff@8.0.4: {}
|
||||
|
||||
earcut@3.0.2: {}
|
||||
|
||||
electron-to-chromium@1.5.349: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
@@ -3859,6 +4008,8 @@ snapshots:
|
||||
|
||||
get-nonce@1.0.1: {}
|
||||
|
||||
gl-matrix@3.4.4: {}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
@@ -3917,8 +4068,12 @@ snapshots:
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
json-stringify-pretty-compact@4.0.0: {}
|
||||
|
||||
json5@2.2.3: {}
|
||||
|
||||
kdbush@4.0.2: {}
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
@@ -3993,12 +4148,38 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
maplibre-gl@5.24.0:
|
||||
dependencies:
|
||||
'@mapbox/jsonlint-lines-primitives': 2.0.2
|
||||
'@mapbox/point-geometry': 1.1.0
|
||||
'@mapbox/tiny-sdf': 2.1.0
|
||||
'@mapbox/unitbezier': 0.0.1
|
||||
'@mapbox/vector-tile': 2.0.4
|
||||
'@mapbox/whoots-js': 3.1.0
|
||||
'@maplibre/geojson-vt': 6.1.0
|
||||
'@maplibre/maplibre-gl-style-spec': 24.8.2
|
||||
'@maplibre/mlt': 1.1.9
|
||||
'@maplibre/vt-pbf': 4.3.0
|
||||
'@types/geojson': 7946.0.16
|
||||
earcut: 3.0.2
|
||||
gl-matrix: 3.4.4
|
||||
kdbush: 4.0.2
|
||||
murmurhash-js: 1.0.0
|
||||
pbf: 4.0.1
|
||||
potpack: 2.1.0
|
||||
quickselect: 3.0.0
|
||||
tinyqueue: 3.0.0
|
||||
|
||||
minimatch@10.2.5:
|
||||
dependencies:
|
||||
brace-expansion: 5.0.5
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
murmurhash-js@1.0.0: {}
|
||||
|
||||
nanoid@3.3.12: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
@@ -4030,6 +4211,10 @@ snapshots:
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
pbf@4.0.1:
|
||||
dependencies:
|
||||
resolve-protobuf-schema: 2.1.0
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.2: {}
|
||||
@@ -4042,6 +4227,8 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
potpack@2.1.0: {}
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier-linter-helpers@1.0.1:
|
||||
@@ -4050,8 +4237,12 @@ snapshots:
|
||||
|
||||
prettier@3.8.3: {}
|
||||
|
||||
protocol-buffers-schema@3.6.1: {}
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
quickselect@3.0.0: {}
|
||||
|
||||
radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
@@ -4159,6 +4350,10 @@ snapshots:
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
resolve-protobuf-schema@2.1.0:
|
||||
dependencies:
|
||||
protocol-buffers-schema: 3.6.1
|
||||
|
||||
rolldown@1.0.0-rc.17:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.127.0
|
||||
@@ -4180,6 +4375,8 @@ snapshots:
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17
|
||||
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17
|
||||
|
||||
rw@1.3.3: {}
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
@@ -4210,6 +4407,10 @@ snapshots:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
supercluster@8.0.1:
|
||||
dependencies:
|
||||
kdbush: 4.0.2
|
||||
|
||||
synckit@0.11.12:
|
||||
dependencies:
|
||||
'@pkgr/core': 0.2.9
|
||||
@@ -4225,6 +4426,8 @@ snapshots:
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
|
||||
tinyqueue@3.0.0: {}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { useEffect, useRef, useSyncExternalStore, type ReactNode } from 'react';
|
||||
import maplibregl, { type Map as MapLibreMap } from 'maplibre-gl';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import { defaultStyle } from './styles';
|
||||
|
||||
// ---------- Singleton ----------------------------------------------------
|
||||
|
||||
let _map: MapLibreMap | null = null;
|
||||
let _container: HTMLDivElement | null = null;
|
||||
|
||||
function getOrCreateMap(): MapLibreMap {
|
||||
if (_map && _container) return _map;
|
||||
_container = document.createElement('div');
|
||||
_container.className = 'trm-map-host';
|
||||
_container.style.width = '100%';
|
||||
_container.style.height = '100%';
|
||||
_map = new maplibregl.Map({
|
||||
container: _container,
|
||||
style: defaultStyle,
|
||||
attributionControl: { compact: true },
|
||||
});
|
||||
_map.on('styledata', onStyleData);
|
||||
return _map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only access to the singleton. Throws if called before `<MapView>`
|
||||
* has mounted (and therefore before the singleton is created).
|
||||
*
|
||||
* Side-effect-only `Map*` components should call this inside their
|
||||
* `useEffect` *after* the `mapReady` gate has flipped true — by which
|
||||
* point the singleton exists and the style has loaded.
|
||||
*/
|
||||
export function getMap(): MapLibreMap {
|
||||
if (!_map) {
|
||||
throw new Error(
|
||||
'getMap() called before <MapView> mounted. Wrap the call in a Map* component or wait for useMapReady().',
|
||||
);
|
||||
}
|
||||
return _map;
|
||||
}
|
||||
|
||||
// ---------- mapReady gate -----------------------------------------------
|
||||
|
||||
let _ready = false;
|
||||
const _readyListeners = new Set<(ready: boolean) => void>();
|
||||
|
||||
function setReady(value: boolean): void {
|
||||
if (_ready === value) return;
|
||||
_ready = value;
|
||||
for (const cb of _readyListeners) cb(value);
|
||||
}
|
||||
|
||||
export function getMapReady(): boolean {
|
||||
return _ready;
|
||||
}
|
||||
|
||||
export function subscribeMapReady(cb: (ready: boolean) => void): () => void {
|
||||
_readyListeners.add(cb);
|
||||
return () => {
|
||||
_readyListeners.delete(cb);
|
||||
};
|
||||
}
|
||||
|
||||
export function useMapReady(): boolean {
|
||||
return useSyncExternalStore(
|
||||
subscribeMapReady,
|
||||
getMapReady,
|
||||
() => false, // SSR fallback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Style-data lifecycle handler.
|
||||
*
|
||||
* `styledata` fires for every style mutation (initial load, setStyle,
|
||||
* source updates). We treat it as "the style might have changed; re-check
|
||||
* loaded() and gate the children". `setReady(false)` first so children
|
||||
* unmount and clean up their custom sources/layers; once `loaded()` is
|
||||
* true, flip back to ready and let them remount.
|
||||
*/
|
||||
function onStyleData(): void {
|
||||
setReady(false);
|
||||
const check = (): void => {
|
||||
if (_map?.loaded()) {
|
||||
setReady(true);
|
||||
} else {
|
||||
setTimeout(check, 33);
|
||||
}
|
||||
};
|
||||
check();
|
||||
}
|
||||
|
||||
// ---------- <MapView> ----------------------------------------------------
|
||||
|
||||
/**
|
||||
* The canonical entry point for any view that uses the map.
|
||||
*
|
||||
* Mounts the singleton's detached `<div>` into a React-managed ref on
|
||||
* mount; detaches on unmount. The map instance itself survives navigation
|
||||
* — its WebGL context, registered sprites, and basemap state all persist.
|
||||
*
|
||||
* Children render only when `mapReady` is true, so side-effect-only
|
||||
* `Map*` components can safely call `getMap().addSource(...)` from their
|
||||
* `useEffect` setup without racing the style load.
|
||||
*/
|
||||
export function MapView({ children }: { children?: ReactNode }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const ready = useMapReady();
|
||||
|
||||
useEffect(() => {
|
||||
const map = getOrCreateMap();
|
||||
const container = _container;
|
||||
const host = ref.current;
|
||||
if (!container || !host) return;
|
||||
|
||||
host.appendChild(container);
|
||||
map.resize();
|
||||
|
||||
const onWindowResize = (): void => {
|
||||
map.resize();
|
||||
};
|
||||
window.addEventListener('resize', onWindowResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', onWindowResize);
|
||||
// Detach but DON'T destroy the map — singleton lives on.
|
||||
if (container.parentNode === host) {
|
||||
host.removeChild(container);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="absolute inset-0">
|
||||
{ready && children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { StyleSpecification } from 'maplibre-gl';
|
||||
|
||||
/**
|
||||
* Bootstrap basemap style used until task 2.2 (basemap switcher) lands.
|
||||
*
|
||||
* Plain OSM raster tiles. Free, attribution-required, polite User-Agent
|
||||
* recommended for any production load. For dogfood-scale traffic this is
|
||||
* fine.
|
||||
*/
|
||||
export const defaultStyle: StyleSpecification = {
|
||||
version: 8,
|
||||
sources: {
|
||||
raster: {
|
||||
type: 'raster',
|
||||
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
||||
tileSize: 256,
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
minzoom: 0,
|
||||
maxzoom: 19,
|
||||
},
|
||||
},
|
||||
glyphs: 'https://fonts.openmaptiles.org/{fontstack}/{range}.pbf',
|
||||
layers: [{ id: 'raster', type: 'raster', source: 'raster' }],
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
|
||||
import { useAuthStore } from '@/auth';
|
||||
import { LogoutButton } from '@/ui/components/logout-button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/ui/primitives/card';
|
||||
@@ -40,11 +40,16 @@ function HomePage() {
|
||||
<CardHeader>
|
||||
<CardTitle>Live monitoring map</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
The live-position map lands in Phase 2 once the Processor's WebSocket endpoint is
|
||||
shipped (Phase 1.5 — already complete on the processor side).
|
||||
Watch registered devices report their positions in real time.
|
||||
</p>
|
||||
<Link
|
||||
to="/monitor"
|
||||
className="inline-flex items-center gap-1 text-sm font-medium underline underline-offset-4"
|
||||
>
|
||||
Open live map →
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { MapView } from '@/map/core/map-view';
|
||||
|
||||
export const Route = createFileRoute('/_authed/monitor')({
|
||||
component: MonitorPage,
|
||||
});
|
||||
|
||||
function MonitorPage() {
|
||||
return (
|
||||
// 3.5rem ≈ 56 px reserved for a future top-bar (Phase 3 chrome).
|
||||
// For now the parent _authed layout has no top-bar, so the map can
|
||||
// fill the full viewport — adjust when chrome lands.
|
||||
<div className="relative h-[calc(100vh-0rem)] w-full">
|
||||
<MapView />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user