diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 000000000..295f096d4 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "reactmap-app", + "runtimeExecutable": "bunx", + "runtimeArgs": ["vite", "--port", "5273", "--strictPort", "--no-open"], + "port": 5273 + } + ] +} diff --git a/app/build-isolation.test.ts b/app/build-isolation.test.ts new file mode 100644 index 000000000..881aa2c47 --- /dev/null +++ b/app/build-isolation.test.ts @@ -0,0 +1,90 @@ +import { expect, test } from 'bun:test' +import { existsSync, readdirSync, readFileSync } from 'node:fs' +import { join } from 'node:path' + +/** + * The 1.0 client is what essentially every real user is served. Its bundle must + * not carry code that only the 2.0 map can reach. + * + * This exists because vite.config.js decides chunk membership by matching + * package paths, and that rule has now leaked five separate things into the + * chunk 1.0 loads eagerly: tailwind's preflight reset, the fontsource font + * faces, maplibre's stylesheet, deck.gl's rendering engine, and then deck.gl's + * gesture, logging and text dependencies under scopes of their own. The fifth + * shipped in the same commit whose comment predicted a fifth. + * + * Enumerating package names is not a fix for that, it is a fix for one instance + * of it. Every leak so far was silent: nothing errors, no test fails, and the + * only symptom is that people downloading a map they never open pay for it. + * So this asserts the property directly against the built output rather than + * trusting the rule that is supposed to produce it. + * + * If this fails, do not add the offending package to an ignore list here. Add + * it to the map bucket in vite.config.js, or fix the rule properly by deciding + * chunk membership from which entry actually reaches a module. + */ + +const DIST = join(import.meta.dir, '..', 'dist') + +// Substrings that only exist because of the 2.0 map stack. Each is a real +// package name as it appears in bundled module paths and identifiers. +const MAP_ONLY = [ + 'maplibre-gl', + '@deck.gl', + '@luma.gl', + '@math.gl', + '@loaders.gl', + 'mjolnir.js', + '@probe.gl', + 'tiny-sdf', +] + +const readIfBuilt = () => { + if (!existsSync(DIST)) return null + const html = join(DIST, 'index.html') + if (!existsSync(html)) return null + return readFileSync(html, 'utf8') +} + +test('the 1.0 entry loads no chunk containing map-only code', () => { + const html = readIfBuilt() + // CI runs tests before the build step, so a missing dist is not a failure. + // The check still runs locally and in any job that builds first. + if (html === null) return + + const referenced = [...html.matchAll(/["'(]\/([A-Za-z0-9._-]+\.js)/g)].map( + (match) => match[1], + ) + expect(referenced.length).toBeGreaterThan(0) + + const offenders: string[] = [] + for (const file of referenced) { + // Read the sourcemap, not the chunk. Minification strips module paths, so + // a package name does not survive into the bundled code and grepping for + // one finds nothing however much of that package shipped. The sourcemap's + // `sources` list is where the provenance actually lives, which is how the + // 43 kB leak this guard exists for was originally attributed. + const map = join(DIST, `${file}.map`) + if (!existsSync(map)) continue + const sources: string[] = JSON.parse(readFileSync(map, 'utf8')).sources + for (const name of MAP_ONLY) { + if (sources.some((source) => source.includes(name))) { + offenders.push(`${file} carries ${name}`) + } + } + } + + expect(offenders).toEqual([]) +}) + +test('the map libraries are emitted as their own chunks', () => { + if (!existsSync(DIST)) return + const files = readdirSync(DIST) + // Both should exist as separate chunks so the map route pays for them alone. + expect( + files.some((f) => f.startsWith('maplibre-') && f.endsWith('.js')), + ).toBe(true) + expect(files.some((f) => f.startsWith('deckgl-') && f.endsWith('.js'))).toBe( + true, + ) +}) diff --git a/app/map/MapCanvas.tsx b/app/map/MapCanvas.tsx new file mode 100644 index 000000000..2fe84bb80 --- /dev/null +++ b/app/map/MapCanvas.tsx @@ -0,0 +1,262 @@ +import 'maplibre-gl/dist/maplibre-gl.css' + +import type { PickingInfo } from '@deck.gl/core' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { createAtlas } from './atlas' +import { + drawClusterIcon, + drawGymIcon, + drawPokemonIcon, + resetSharedIconCaches, +} from './draw-icon' +import { FIXTURE_EPOCH } from './fixtures' +import type { MapLayersResult } from './layers' +import { + buildMapLayers, + buildPokemonTextLayer, + POKEMON_LABEL_LAYER_ID, +} from './layers' +import { Popup } from './Popup' +import { createFixtureSource } from './source' +import type { + GymEntity, + MapEntity, + MapQuery, + PokemonEntity, + Viewport, +} from './types' +import { useDismissOnEscape } from './useDismissOnEscape' +import type { Camera } from './useMapLibre' +import { anchorFor, pickedEntityFrom, useMapLibre } from './useMapLibre' +import { useWebglContextRecovery } from './useWebglContextRecovery' + +export interface MapCanvasProps { + initialCamera: Camera + onCameraChange?: (camera: Camera) => void +} + +/** + * A source with no live transport yet has no notion of "the current + * viewport", so this queries the whole world rather than the camera's + * bounds; a later task narrows this to what `useMapLibre`'s camera state + * actually frames. + */ +const WORLD_BOUNDS = { west: -180, south: -90, east: 180, north: 90 } +const POKEMON_QUERY: MapQuery = { + kind: 'pokemon', + bounds: WORLD_BOUNDS, + zoom: 12, +} +const GYM_QUERY: MapQuery = { kind: 'gym', bounds: WORLD_BOUNDS, zoom: 12 } + +function isPokemon(entity: MapEntity): entity is PokemonEntity { + return entity.kind === 'pokemon' +} + +function isGym(entity: MapEntity): entity is GymEntity { + return entity.kind === 'gym' +} + +/** What the layer memo falls back to before the atlas exists. */ +const EMPTY_LAYERS: MapLayersResult = { + layers: [], + limitHit: { pokemon: false, gyms: false }, + renderedPokemon: [], +} + +/** How often the countdown/IV text layer is rebuilt against a fresh clock. */ +const CLOCK_TICK_MS = 1000 + +/** + * Mounts one MapLibre instance sized to the viewport minus the bottom nav + * (`Shell` reserves that with `pb-16`, 4rem, so this container claims the + * rest of the dynamic viewport height rather than the full 100dvh, or the + * map would render 4rem taller than what is actually visible above the + * nav). + * + * The stylesheet import above is load-bearing: without it the canvas still + * renders, which is exactly what makes it easy to miss, but the + * NavigationControl and attribution end up unstyled and mispositioned. + */ +export function MapCanvas({ initialCamera, onCameraChange }: MapCanvasProps) { + // Lazy-initialized on the ref directly (a documented React pattern) so + // the atlas's LRU cache and the fixture source are each created exactly + // once for the component's lifetime, not rebuilt on every render. + const atlasRef = useRef | null>(null) + if (atlasRef.current === null) { + atlasRef.current = createAtlas({ draw: drawPokemonIcon }) + } + const sourceRef = useRef | null>(null) + if (sourceRef.current === null) { + sourceRef.current = createFixtureSource() + } + + const [pokemon, setPokemon] = useState([]) + const [gyms, setGyms] = useState([]) + // Countdown/IV text is layer data, not a per-marker component: this is + // the one clock this whole tree reads, and every timer-bearing layer is + // rebuilt from it on the same tick rather than each owning its own. + // Fixture expiries are measured from a fixed epoch so the generator stays + // reproducible, which means real wall-clock time is long past all of them and + // every countdown would read 0:00. Running the clock from that epoch instead + // keeps the fixtures deterministic and the timers live. When a real source + // replaces the fixtures this becomes Date.now() again. + const timeOrigin = useRef(Date.now()) + const [now, setNow] = useState(() => FIXTURE_EPOCH) + + // The one selected entity, or none. This is the entire replacement for + // 1.0's per-marker ref plus useForcePopup/useMarkerTimer: deck.gl's + // picking reports what is under the cursor, and there is exactly one + // popup, so one nullable slot is the whole model. + const [selected, setSelected] = useState(null) + + // Null only until the map mounts and reports its first frame. + const [viewport, setViewport] = useState(null) + + // Bumped on WebGL context restoration purely to force `layers` below to + // recompute immediately. buildMapLayers already gets fresh Layer + // instances every clock tick regardless, since `now` is in its deps - + // this just closes the gap between "context restored" and "next tick" + // rather than leaving the map blank for up to a second. + const [rebuildToken, setRebuildToken] = useState(0) + + const handlePick = useCallback((info: PickingInfo) => { + setSelected(pickedEntityFrom(info)) + }, []) + + const closePopup = useCallback(() => setSelected(null), []) + + // The atlas's cached icons are gone the moment the context that held + // their composited pixels dies, so restoration re-warms it before + // forcing the layer rebuild - a rebuild that reused the stale cache + // would still hand deck.gl icons composited for a context that no + // longer exists, which task-6-7-brief.md calls out as rendering an + // empty map with no error. + const handleContextRestore = useCallback(() => { + atlasRef.current?.clear() + // The gym and cluster icons live in module state, not in the atlas, so + // they need clearing too or the restore reuses dead textures. + resetSharedIconCaches() + setRebuildToken((token) => token + 1) + }, []) + + useDismissOnEscape(selected !== null, closePopup) + + // Recreated only when the selection itself changes, not on every render: + // useMapLibre's reprojection effect keys off this object's identity, and + // a fresh object every render would refire it needlessly. + const anchor = useMemo(() => anchorFor(selected), [selected]) + + useEffect(() => { + const source = sourceRef.current + if (!source) return undefined + const unsubscribePokemon = source.subscribe(POKEMON_QUERY, (entities) => { + setPokemon(entities.filter(isPokemon)) + }) + const unsubscribeGyms = source.subscribe(GYM_QUERY, (entities) => { + setGyms(entities.filter(isGym)) + }) + return () => { + unsubscribePokemon() + unsubscribeGyms() + } + }, []) + + useEffect(() => { + const interval = setInterval( + () => setNow(FIXTURE_EPOCH + (Date.now() - timeOrigin.current)), + CLOCK_TICK_MS, + ) + return () => clearInterval(interval) + }, []) + + // What the camera frames, reported by useMapLibre on mount and on every + // moveend. Passing it into buildMapLayers is what makes any of the + // clustering run at all: without it every entity renders individually and + // no forcedLimit applies, which is the state task 6 shipped in. + // Clustering builds a fresh Supercluster index over every subscribed entity, + // so it must not depend on the clock. It previously sat in one memo with + // `now`, which ticks every second, meaning the whole set was re-indexed once + // a second forever whether or not anything moved. Splitting it means the + // per-second work is rebuilding the text layer's array, which is what the + // spec asks for, rather than re-clustering thousands of points. + const clustered = useMemo(() => { + const atlas = atlasRef.current + if (!atlas) return null + return buildMapLayers({ + pokemon, + gyms, + getIconFor: atlas.getIconFor, + getGymIcon: drawGymIcon, + getClusterIcon: drawClusterIcon, + // Any value: the text layer this produces is replaced on every tick. + now: 0, + ...(viewport ? { viewport } : {}), + }) + // rebuildToken is intentionally in this array with no other purpose + // than to invalidate this memo; see handleContextRestore above. + }, [pokemon, gyms, viewport, rebuildToken]) + + const built = useMemo(() => { + const atlas = atlasRef.current + if (!atlas || !clustered) return EMPTY_LAYERS + // Only the text layer reads the clock, so only it is rebuilt on a tick. + return { + layers: clustered.layers.map((layer) => + layer.id === POKEMON_LABEL_LAYER_ID + ? buildPokemonTextLayer(clustered.renderedPokemon, now) + : layer, + ), + limitHit: clustered.limitHit, + } + }, [clustered, now]) + const layers = built.layers + const capped = built.limitHit.pokemon || built.limitHit.gyms + + const { containerRef, screenPosition } = useMapLibre({ + initialCamera, + ...(onCameraChange ? { onCameraChange } : {}), + layers, + onPick: handlePick, + anchor, + onViewportChange: setViewport, + }) + + const { restoring } = useWebglContextRecovery(containerRef, { + onRestore: handleContextRestore, + }) + + return ( +
+
+ {restoring && ( +
+ Restoring map… +
+ )} + {capped && !restoring && ( +
+ Too much to draw here. Zoom in for detail. +
+ )} + {selected && screenPosition && ( + + )} +
+ ) +} diff --git a/app/map/Popup.test.tsx b/app/map/Popup.test.tsx new file mode 100644 index 000000000..0cca40227 --- /dev/null +++ b/app/map/Popup.test.tsx @@ -0,0 +1,98 @@ +import { afterAll, afterEach, beforeAll, expect, test } from 'bun:test' +import { cleanup, fireEvent, render, within } from '@testing-library/react' +import { setupDom, teardownDom } from '../test-setup' +import { Popup } from './Popup' +import type { GymEntity, PokemonEntity } from './types' + +/* + * The popup is a plain function of `entity`, `x`, `y`, `now` and + * `onClose`; nothing here needs a map, a camera or WebGL. What genuinely + * needs a browser - whether the position this renders at actually + * survives a real pan or zoom - is `useMapLibre`'s reprojection, covered + * (as far as it can be without a GPU) in useMapLibre.test.ts, and stated + * plainly in task-5-report.md as something only manual/browser + * verification can back. + */ + +beforeAll(setupDom) +afterAll(teardownDom) +afterEach(cleanup) + +const POKEMON: PokemonEntity = { + kind: 'pokemon', + spawnId: 'spawn-1', + pokemonId: 25, + form: 0, + costume: 0, + gender: 1, + lat: 51.5, + lon: -0.1, + expiresAt: 90_000, + iv: 87, +} + +const GYM: GymEntity = { + kind: 'gym', + gymId: 'gym-1', + lat: 40.7, + lon: -74, + team: 2, + inBattle: true, +} + +test('positions itself at the fixed x/y it is given, not something it computes', () => { + const { container } = render( + {}} now={0} />, + ) + const card = container.querySelector('[data-slot="map-popup"]') + expect(card).toBeTruthy() + expect((card as HTMLElement).style.left).toBe('123px') + expect((card as HTMLElement).style.top).toBe('33px') +}) + +test("shows a pokemon entity's species, IV and countdown", () => { + const { container } = render( + {}} now={0} />, + ) + expect(within(container).getByText('Pokemon #25')).toBeTruthy() + expect(within(container).getByText(/87% IV/)).toBeTruthy() + expect(within(container).getByText(/1:30/)).toBeTruthy() +}) + +test("shows a gym entity's team and battle state", () => { + const { container } = render( + {}} now={0} />, + ) + expect(within(container).getByText('Gym (in battle)')).toBeTruthy() + expect(within(container).getByText('Team 2')).toBeTruthy() +}) + +test('the team swatch reads its colour from the data palette token, not a literal', () => { + const { container } = render( + {}} now={0} />, + ) + const swatch = container.querySelector('[aria-hidden="true"].rounded-full') + expect((swatch as HTMLElement).style.backgroundColor).toBe( + 'var(--color-team-2)', + ) +}) + +test('closing calls back exactly once per click', () => { + let closeCount = 0 + const { container } = render( + { + closeCount += 1 + }} + now={0} + />, + ) + const closeButton = within(container).getByRole('button', { + name: 'Close', + }) + fireEvent.click(closeButton) + expect(closeCount).toBe(1) +}) diff --git a/app/map/Popup.tsx b/app/map/Popup.tsx new file mode 100644 index 000000000..8b8240c7b --- /dev/null +++ b/app/map/Popup.tsx @@ -0,0 +1,91 @@ +import { Button } from '@app/components/ui/button' +import { + Card, + CardAction, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@app/components/ui/card' +import { XIcon } from 'lucide-react' +import { formatCountdown } from './layers' +import type { MapEntity } from './types' + +export interface PopupProps { + /** The entity this popup describes. Exactly one of these exists at a time. */ + entity: MapEntity + /** + * Where `entity`'s coordinate currently projects to, in pixels relative + * to the map container. Owned by `useMapLibre`, which recomputes it on + * every camera frame; this component only ever reads a position, it + * never computes one. See task-5-report.md for why. + */ + x: number + y: number + onClose: () => void + /** Clock the countdown reads against. Defaults to the real time; tests + * pin this to get a deterministic countdown string. */ + now?: number +} + +function titleFor(entity: MapEntity): string { + return entity.kind === 'pokemon' + ? `Pokemon #${entity.pokemonId}` + : `Gym${entity.inBattle ? ' (in battle)' : ''}` +} + +/** + * The one popup this map ever shows. Positioned with a plain absolute + * offset rather than through the `Popover` primitive's own floating-ui + * placement: `Popover`'s auto-update tracks its anchor element's DOM rect, + * but this component's anchor is a data coordinate reprojected by + * `useMapLibre` on every camera frame, not a DOM element that moves on its + * own. Driving position from `x`/`y` props keeps this component a pure + * function of its inputs, which is what makes it testable without a map at + * all (see Popup.test.tsx). `Card` still supplies the chrome - rounded + * panel, header, close affordance - so nothing here is hand-built. + */ +export function Popup({ entity, x, y, onClose, now = Date.now() }: PopupProps) { + return ( + + + {titleFor(entity)} + + + + + + {entity.kind === 'pokemon' ? ( + + {entity.iv !== undefined ? `${entity.iv}% IV` : 'IV unknown'} + {' · '} + {formatCountdown(entity.expiresAt, now)} + + ) : ( + + + )} + + + ) +} diff --git a/app/map/atlas.test.ts b/app/map/atlas.test.ts new file mode 100644 index 000000000..28ec3177c --- /dev/null +++ b/app/map/atlas.test.ts @@ -0,0 +1,268 @@ +import { expect, test } from 'bun:test' +import { createAtlas, type IconDescriptor, iconKeyFor, LruCache } from './atlas' +import { getFixturePokemon } from './fixtures' +import type { PokemonEntity } from './types' + +const BASE: PokemonEntity = { + kind: 'pokemon', + spawnId: 'spawn-a', + pokemonId: 6, + form: 0, + costume: 0, + gender: 1, + lat: 51.5, + lon: 0, + expiresAt: 1_000, +} + +test('two entities identical in appearance but differing in position, expiry, spawnId and stats share a key', () => { + const a: PokemonEntity = { ...BASE } + const b: PokemonEntity = { + ...BASE, + spawnId: 'spawn-b', + lat: 40, + lon: 90, + expiresAt: 999_999, + iv: 100, + level: 35, + size: 5, + } + expect(iconKeyFor(a)).toBe(iconKeyFor(b)) +}) + +test('a difference in pokemonId changes the key', () => { + expect(iconKeyFor(BASE)).not.toBe(iconKeyFor({ ...BASE, pokemonId: 7 })) +}) + +test('a difference in form changes the key', () => { + expect(iconKeyFor(BASE)).not.toBe(iconKeyFor({ ...BASE, form: 1 })) +}) + +test('a difference in costume changes the key', () => { + expect(iconKeyFor(BASE)).not.toBe(iconKeyFor({ ...BASE, costume: 1 })) +}) + +test('a difference in gender changes the key', () => { + expect(iconKeyFor(BASE)).not.toBe(iconKeyFor({ ...BASE, gender: 2 })) +}) + +test('an entity carrying a badge produces a different key than one without one', () => { + expect(iconKeyFor(BASE)).not.toBe(iconKeyFor({ ...BASE, badge: 1 })) +}) + +test('an entity carrying a background produces a different key than one without one', () => { + expect(iconKeyFor(BASE)).not.toBe(iconKeyFor({ ...BASE, background: 1 })) +}) + +test('an entity carrying weather produces a different key than one without it', () => { + expect(iconKeyFor(BASE)).not.toBe(iconKeyFor({ ...BASE, weather: 1 })) +}) + +test('an absent optional field cannot collide with a present one sharing its numeric value', () => { + // badge=1 vs no badge must differ, even though '1' vs '' would tempt a + // naive string join into looking equal after trimming. + const withBadge: PokemonEntity = { ...BASE, badge: 1 } + const withoutBadge: PokemonEntity = { ...BASE } + expect(iconKeyFor(withBadge)).not.toBe(iconKeyFor(withoutBadge)) +}) + +/** + * The capacity `createAtlas` defaults to. Duplicated rather than exported + * from atlas.ts so that moving the default is a deliberate edit here too. + */ +const DEFAULT_CAPACITY = 512 + +/** + * A box inside the fixture area holding roughly one screenful of markers. + * Viewport scale is the condition that matters: the map draws a few hundred + * markers at a time, not all five thousand. + */ +const VIEWPORT = { west: -0.12, south: 51.25, east: 0.12, north: 51.75 } + +/** + * Thresholds are a floor on usefulness, not a record of the current number. + * A cache that serves under a quarter of a cold viewport pass is not paying + * for itself, and the fixture feeding it has stopped resembling a real + * viewport. Measured against the fixtures at the time of writing: 42.5 + * percent over a viewport batch and 73.2 percent over the full set, so both + * floors have room beneath them. Uniformly random fixtures score 1.1 and 1.4 + * percent, so the gap either side of these lines is wide. + */ +const VIEWPORT_MIN_HIT_RATE = 0.25 +const FULL_SET_MIN_HIT_RATE = 0.5 + +function viewportBatch(): PokemonEntity[] { + return getFixturePokemon().filter( + (entity) => + entity.lat >= VIEWPORT.south && + entity.lat <= VIEWPORT.north && + entity.lon >= VIEWPORT.west && + entity.lon <= VIEWPORT.east, + ) +} + +/** An atlas that counts composites, so a pass reports its own hit rate. */ +function countingAtlas() { + let draws = 0 + const atlas = createAtlas({ + capacity: DEFAULT_CAPACITY, + draw: (_entity, key): IconDescriptor => { + draws += 1 + return { id: key, url: `data:${key}`, width: 32, height: 32 } + }, + }) + return { + pass(entities: PokemonEntity[]) { + const before = draws + for (const entity of entities) atlas.getIconFor(entity) + const drawn = draws - before + return { drawn, hitRate: 1 - drawn / entities.length } + }, + } +} + +test('a cold pass over a viewport-sized batch serves a quarter of its markers from cache', () => { + const batch = viewportBatch() + expect(batch.length).toBeGreaterThan(100) + expect(countingAtlas().pass(batch).hitRate).toBeGreaterThanOrEqual( + VIEWPORT_MIN_HIT_RATE, + ) +}) + +test('a cold pass over the whole fixture set serves half its markers from cache', () => { + const pokemon = getFixturePokemon() + // The engine exists because past three thousand markers the old renderer + // goes choppy, so the fixture set has to clear that bar to be evidence. + expect(pokemon.length).toBeGreaterThanOrEqual(3000) + expect(countingAtlas().pass(pokemon).hitRate).toBeGreaterThanOrEqual( + FULL_SET_MIN_HIT_RATE, + ) +}) + +test("a viewport's icons fit inside capacity, so a second pass over it draws nothing", () => { + const batch = viewportBatch() + const atlas = countingAtlas() + atlas.pass(batch) + // Nothing was evicted during the first pass, so every marker still on + // screen is already composited. This is what makes panning and redrawing + // cheap, and it fails the moment a viewport's icon variety outgrows the + // cache. + expect(atlas.pass(batch).drawn).toBe(0) +}) + +test('LruCache returns what was set', () => { + const cache = new LruCache(3) + cache.set('a', 1) + expect(cache.get('a')).toBe(1) + expect(cache.get('missing')).toBeUndefined() +}) + +test('LruCache evicts the least recently used entry once over capacity', () => { + const cache = new LruCache(2) + cache.set('a', 1) + cache.set('b', 2) + cache.set('c', 3) // capacity 2: 'a' is oldest, evicted + expect(cache.has('a')).toBe(false) + expect(cache.has('b')).toBe(true) + expect(cache.has('c')).toBe(true) + expect(cache.size).toBe(2) +}) + +test('LruCache reading a key refreshes its recency, so it survives the next eviction', () => { + const cache = new LruCache(2) + cache.set('a', 1) + cache.set('b', 2) + cache.get('a') // touch 'a': 'b' is now the oldest + cache.set('c', 3) + expect(cache.has('a')).toBe(true) + expect(cache.has('b')).toBe(false) + expect(cache.has('c')).toBe(true) +}) + +test('LruCache never exceeds its bound across many insertions', () => { + const cache = new LruCache(50) + for (let i = 0; i < 5000; i++) { + cache.set(i, i) + expect(cache.size).toBeLessThanOrEqual(50) + } + expect(cache.size).toBe(50) +}) + +test('rejects a non-positive capacity', () => { + expect(() => new LruCache(0)).toThrow() +}) + +test('clear empties the cache', () => { + const cache = new LruCache(3) + cache.set('a', 1) + cache.set('b', 2) + cache.clear() + expect(cache.size).toBe(0) + expect(cache.has('a')).toBe(false) + expect(cache.get('b')).toBeUndefined() +}) + +test('atlas.clear forces the next getIconFor for a previously-cached key to redraw', () => { + let drawCount = 0 + const atlas = createAtlas({ + capacity: 10, + draw: (_entity, key): IconDescriptor => { + drawCount += 1 + return { id: key, url: `data:${key}`, width: 32, height: 32 } + }, + }) + + atlas.getIconFor(BASE) + atlas.getIconFor(BASE) + expect(drawCount).toBe(1) + + atlas.clear() + atlas.getIconFor(BASE) + + expect(drawCount).toBe(2) + expect(atlas.cache.size).toBe(1) +}) + +test('getIconFor draws once per distinct key and reuses the cached descriptor on repeats', () => { + let drawCount = 0 + const seenKeys: string[] = [] + const atlas = createAtlas({ + capacity: 10, + draw: (_entity, key): IconDescriptor => { + drawCount += 1 + seenKeys.push(key) + return { id: key, url: `data:${key}`, width: 32, height: 32 } + }, + }) + + const a: PokemonEntity = { ...BASE } + const sameAppearance: PokemonEntity = { ...BASE, spawnId: 'spawn-other' } + const differentAppearance: PokemonEntity = { ...BASE, pokemonId: 999 } + + const first = atlas.getIconFor(a) + const second = atlas.getIconFor(sameAppearance) + const third = atlas.getIconFor(differentAppearance) + + expect(drawCount).toBe(2) + expect(second).toEqual(first) + expect(third).not.toEqual(first) + expect(seenKeys).toEqual([iconKeyFor(a), iconKeyFor(differentAppearance)]) +}) + +test('getIconFor respects the configured capacity via the exposed cache', () => { + const atlas = createAtlas({ + capacity: 4, + draw: (_entity, key): IconDescriptor => ({ + id: key, + url: `data:${key}`, + width: 32, + height: 32, + }), + }) + + const pokemon = getFixturePokemon().slice(0, 200) + for (const entity of pokemon) { + atlas.getIconFor(entity) + expect(atlas.cache.size).toBeLessThanOrEqual(4) + } +}) diff --git a/app/map/atlas.ts b/app/map/atlas.ts new file mode 100644 index 000000000..018b5f910 --- /dev/null +++ b/app/map/atlas.ts @@ -0,0 +1,136 @@ +import type { PokemonEntity } from './types' + +/** + * Everything that determines what a pokemon marker looks like. Deliberately + * NOT `spawnId`: that field is unique per encounter and would give the cache + * one entry per marker, defeating it entirely. `pokemonId` is the species; + * `id` does not exist on `PokemonEntity` (see types.ts) so there is nothing + * to misread here even by typo. + * + * `form`, `costume` and `gender` are always present. `badge`, `background` + * and `weather` are optional appearance modifiers; each is folded in with a + * sentinel so an absent modifier can never collide with a present one that + * happens to share a numeric value. + */ +export function iconKeyFor(entity: PokemonEntity): string { + return [ + entity.pokemonId, + entity.form, + entity.costume, + entity.gender, + entity.badge ?? 'none', + entity.background ?? 'none', + entity.weather ?? 'none', + ].join(':') +} + +/** + * Fixed-capacity least-recently-used cache. Plain `Map` for storage: Map + * iterates in insertion order, and re-inserting a key on every `get`/`set` + * keeps that order equal to recency, so the first key in iteration order is + * always the one to evict. + */ +export class LruCache { + #capacity: number + #store = new Map() + + constructor(capacity: number) { + if (capacity < 1) { + throw new RangeError('LruCache capacity must be at least 1') + } + this.#capacity = capacity + } + + get size(): number { + return this.#store.size + } + + has(key: K): boolean { + return this.#store.has(key) + } + + get(key: K): V | undefined { + const value = this.#store.get(key) + if (value === undefined) return undefined + // Refresh recency: delete then re-insert moves this key to the end. + this.#store.delete(key) + this.#store.set(key, value) + return value + } + + set(key: K, value: V): void { + this.#store.delete(key) + this.#store.set(key, value) + if (this.#store.size > this.#capacity) { + const oldest = this.#store.keys().next().value + if (oldest !== undefined) this.#store.delete(oldest) + } + } + + /** + * Drops every entry. Used to re-warm the atlas after WebGL context + * restoration (see useWebglContextRecovery.ts / MapCanvas.tsx): the + * cached descriptors' composited pixels rode on the GPU resources the + * lost context took with it, so keeping them around would serve stale + * icons instead of ones drawn for the new context. + */ + clear(): void { + this.#store.clear() + } +} + +/** What `IconLayer`'s `getIcon` accessor needs for one marker. */ +export interface IconDescriptor { + id: string + url: string + width: number + height: number +} + +/** + * Composites one marker's pixels and hands back everything `IconLayer` + * needs to place it. Takes an already-computed key rather than an entity so + * the drawer never has to re-derive or second-guess what determines + * appearance; that decision lives solely in `iconKeyFor`. Uses + * `OffscreenCanvas`, so it only runs in a browser, never in this module's + * tests. + */ +export type IconDrawer = (entity: PokemonEntity, key: string) => IconDescriptor + +const DEFAULT_CAPACITY = 512 + +export interface AtlasOptions { + draw: IconDrawer + capacity?: number +} + +/** + * The pipeline: key the entity, serve the cached descriptor on a hit, draw + * and cache on a miss. The cache and the key are plain data-in data-out + * functions with no canvas dependency, so they're exercised directly in + * tests; `draw` is the only piece that needs a real browser, and it's + * injected rather than imported, so nothing here reaches for + * `OffscreenCanvas` itself. + */ +export function createAtlas({ + draw, + capacity = DEFAULT_CAPACITY, +}: AtlasOptions): { + getIconFor: (entity: PokemonEntity) => IconDescriptor + cache: LruCache + /** Drops every cached icon. See `LruCache.clear` for why. */ + clear: () => void +} { + const cache = new LruCache(capacity) + + function getIconFor(entity: PokemonEntity): IconDescriptor { + const key = iconKeyFor(entity) + const cached = cache.get(key) + if (cached) return cached + const drawn = draw(entity, key) + cache.set(key, drawn) + return drawn + } + + return { getIconFor, cache, clear: () => cache.clear() } +} diff --git a/app/map/basemap.test.ts b/app/map/basemap.test.ts new file mode 100644 index 000000000..b0c26b4ad --- /dev/null +++ b/app/map/basemap.test.ts @@ -0,0 +1,48 @@ +import { afterEach, expect, test } from 'bun:test' +import { resolveBasemapStyle } from './basemap' + +const ENV_KEY = 'VITE_BASEMAP_URL' +const original = process.env[ENV_KEY] + +afterEach(() => { + if (original === undefined) { + delete process.env[ENV_KEY] + } else { + process.env[ENV_KEY] = original + } +}) + +/* + * These three cover the pure decision this module makes and nothing about + * MapLibre itself: rendering a WebGL canvas needs a real browser, so that + * part is left to manual/browser verification and is not asserted here. + */ + +test('defaults to the keyless vector style when unconfigured', () => { + delete process.env[ENV_KEY] + const style = resolveBasemapStyle() + expect(style).toBe('https://tiles.openfreemap.org/styles/liberty') +}) + +test('treats a {z}/{x}/{y} template as a raster tile source, not a style url', () => { + process.env[ENV_KEY] = 'https://tile.example.com/{z}/{x}/{y}.png' + const style = resolveBasemapStyle() + expect(typeof style).not.toBe('string') + if (typeof style === 'string') throw new Error('unreachable') + expect(style.sources.basemap).toEqual({ + type: 'raster', + tiles: ['https://tile.example.com/{z}/{x}/{y}.png'], + tileSize: 256, + attribution: + '© OpenStreetMap contributors', + }) + expect(style.layers).toEqual([ + { id: 'basemap', type: 'raster', source: 'basemap' }, + ]) +}) + +test('passes a non-template url straight through as a vector style document url', () => { + process.env[ENV_KEY] = 'https://example.com/my-style.json' + const style = resolveBasemapStyle() + expect(style).toBe('https://example.com/my-style.json') +}) diff --git a/app/map/basemap.ts b/app/map/basemap.ts new file mode 100644 index 000000000..fa4bd1c00 --- /dev/null +++ b/app/map/basemap.ts @@ -0,0 +1,68 @@ +import type { StyleSpecification } from 'maplibre-gl' + +/** + * Keyless vector style, no signup and no rate-limited key required. This is + * the default every self-hoster gets with zero configuration: OpenFreeMap + * serves the full planet's OSM data as vector tiles under ODbL, funded + * separately from any single provider's key quota. + * + * https://openfreemap.org + */ +const KEYLESS_VECTOR_STYLE_URL = 'https://tiles.openfreemap.org/styles/liberty' + +/** + * `VITE_BASEMAP_URL` is the one config field a self-hoster touches to + * change the basemap, mirroring 1.0's `tileServers[].url` field in shape: a + * `{z}/{x}/{y}` template means raster tiles, anything else is treated as a + * vector style document to load directly. Unset keeps the keyless vector + * default above, so requiring an API key is never the out-of-the-box + * experience. + */ +function readConfiguredBasemapUrl(): string | undefined { + const value = import.meta.env.VITE_BASEMAP_URL + return typeof value === 'string' && value.length > 0 ? value : undefined +} + +function isRasterTemplate(url: string): boolean { + return url.includes('{z}') && url.includes('{x}') && url.includes('{y}') +} + +/** + * Builds a minimal raster-only style around one tile URL template, the same + * shape 1.0's `tileServers` entries already carry. `tileSize` is 256 because + * every server in 1.0's default `tileServers` list serves 256px tiles. + */ +function buildRasterStyle(tileUrlTemplate: string): StyleSpecification { + return { + version: 8, + sources: { + basemap: { + type: 'raster', + tiles: [tileUrlTemplate], + tileSize: 256, + attribution: + '© OpenStreetMap contributors', + }, + }, + layers: [ + { + id: 'basemap', + type: 'raster', + source: 'basemap', + }, + ], + } +} + +/** + * Resolves what `MapCanvas` should hand MapLibre as `style`: either a style + * document (raster, built locally) or a URL MapLibre fetches itself (the + * keyless vector default, or a self-hoster's own vector style document). + */ +export function resolveBasemapStyle(): StyleSpecification | string { + const configured = readConfiguredBasemapUrl() + if (!configured) return KEYLESS_VECTOR_STYLE_URL + return isRasterTemplate(configured) + ? buildRasterStyle(configured) + : configured +} diff --git a/app/map/clustering.test.ts b/app/map/clustering.test.ts new file mode 100644 index 000000000..e68fd9b8c --- /dev/null +++ b/app/map/clustering.test.ts @@ -0,0 +1,178 @@ +import { expect, test } from 'bun:test' +import { + type ClusterableEntity, + type ClusterRules, + clusterEntities, +} from './clustering' +import type { Bounds } from './types' + +const BOUNDS: Bounds = { west: -1, south: 51, east: 1, north: 52 } + +/** Scatters `count` points across `BOUNDS` with a deterministic PRNG, so a + * run that fails does so for the same reason every time. */ +function scatter(count: number): ClusterableEntity[] { + let state = 20260824 + const rng = () => { + state = (state + 0x6d2b79f5) | 0 + let t = state + t = Math.imul(t ^ (t >>> 15), t | 1) + t ^= t + Math.imul(t ^ (t >>> 7), t | 61) + return ((t ^ (t >>> 14)) >>> 0) / 4294967296 + } + return Array.from({ length: count }, (_, index) => ({ + id: `entity-${index}`, + lat: BOUNDS.south + rng() * (BOUNDS.north - BOUNDS.south), + lon: BOUNDS.west + rng() * (BOUNDS.east - BOUNDS.west), + })) +} + +/** + * `groupCount` well-separated groups of `size` entities each, laid out on a + * grid across `BOUNDS`. This is the shape the uniform `scatter` above cannot + * produce and the one that broke the cap: every group is far enough from its + * neighbours to survive as its own cluster, so the cluster count alone runs + * past `forcedLimit` while the loose-point budget is already zero. A map of + * many small towns, each with a handful of gyms, viewed at country zoom. + */ +function groupsOf(groupCount: number, size: number): ClusterableEntity[] { + const columns = Math.ceil(Math.sqrt(groupCount)) + const rows = Math.ceil(groupCount / columns) + const entities: ClusterableEntity[] = [] + for (let group = 0; group < groupCount; group += 1) { + const column = group % columns + const row = Math.floor(group / columns) + const lon = + BOUNDS.west + ((column + 0.5) / columns) * (BOUNDS.east - BOUNDS.west) + const lat = + BOUNDS.south + ((row + 0.5) / rows) * (BOUNDS.north - BOUNDS.south) + for (let member = 0; member < size; member += 1) { + entities.push({ + id: `group-${group}-${member}`, + lat: lat + member * 0.00002, + lon: lon + member * 0.00002, + }) + } + } + return entities +} + +const RULES: ClusterRules = { zoomLevel: 10, forcedLimit: 200, minPoints: 5 } + +/* + * This is the regression test for the 1.0 forcedLimit bug (see the task + * brief for the line-numbered trace through Clustering.jsx). Supercluster's + * `maxZoom` is the zoom ABOVE WHICH it stops clustering and returns raw + * points; 1.0 built its clusterer with `maxZoom: rules.zoomLevel` and + * trusted that same clusterer to enforce `forcedLimit`. Above `zoomLevel`, + * clustering switches itself off and the cap goes with it, at exactly the + * zoom where the most markers are on screen. + * + * 2000 entities, a limit of 200, queried at zoom 20 (10 past zoomLevel): + * the ported bug returns every one of the 2000 unclustered, because + * supercluster has stopped clustering by then and nothing else was + * capping the result. + */ +test('clusterEntities bounds the rendered count at a zoom past zoomLevel', () => { + const entities = scatter(2000) + const result = clusterEntities(entities, BOUNDS, 20, RULES) + const rendered = result.clusters.length + result.points.length + expect(rendered).toBeLessThanOrEqual(RULES.forcedLimit) + expect(result.limitHit).toBe(true) +}) + +test('clusterEntities does not cap or flag limitHit when under the limit', () => { + const entities = scatter(50) + const result = clusterEntities(entities, BOUNDS, 20, RULES) + expect(result.limitHit).toBe(false) + expect(result.clusters.length + result.points.length).toBe(50) +}) + +test('clusterEntities groups nearby points into clusters below zoomLevel', () => { + const entities = scatter(500) + const result = clusterEntities(entities, BOUNDS, 2, RULES) + expect(result.clusters.length).toBeGreaterThan(0) + const clusteredCount = result.clusters.reduce((sum, c) => sum + c.count, 0) + expect(clusteredCount + result.points.length).toBe(500) +}) + +/* + * This asserted the earlier policy, keep every cluster and truncate points, + * and it is now wrong on purpose: capping coarsens instead of truncating, so + * a set that came back as 2000 loose points at zoom 20 comes back as clusters + * drawn at whatever lower zoom first fits. What matters is that the total + * binds and that nothing was thrown away to make it bind. + */ +test('clusterEntities caps by coarsening rather than by dropping markers', () => { + const entities = scatter(2000) + const result = clusterEntities(entities, BOUNDS, 20, RULES) + expect(result.clusters.length).toBeGreaterThan(0) + expect(result.clusters.length + result.points.length).toBeLessThanOrEqual( + RULES.forcedLimit, + ) + const represented = + result.clusters.reduce((sum, cluster) => sum + cluster.count, 0) + + result.points.length + expect(represented).toBe(2000) +}) + +test('clusterEntities is deterministic for the same inputs', () => { + const entities = scatter(2000) + const first = clusterEntities(entities, BOUNDS, 20, RULES) + const second = clusterEntities(entities, BOUNDS, 20, RULES) + expect(first.points.map((p) => p.id)).toEqual(second.points.map((p) => p.id)) +}) + +/* + * The many-small-groups shape. `scatter` above is uniform, so at any zoom + * past `zoomLevel` it yields loose points only and the cap comes entirely out + * of the point budget - which is exactly why it passed while this failed. + * With 800 separated groups of 5 the clusters alone are 800 against a limit + * of 200, the point budget is already zero, and capping only points caps + * nothing. Measured on the pre-fix code at gym rules and 3500 groups: + * zoom 8, 10 and 13 each rendered 3500 clusters against a limit of 2500. + */ +test('clusterEntities bounds the total when clusters alone exceed the limit', () => { + const entities = groupsOf(800, 5) + for (const zoom of [4, 8, 10, 13, 20]) { + const result = clusterEntities(entities, BOUNDS, zoom, RULES) + const rendered = result.clusters.length + result.points.length + expect(`zoom ${zoom}: ${rendered}`).toBe( + `zoom ${zoom}: ${Math.min(rendered, RULES.forcedLimit)}`, + ) + } +}) + +test('clusterEntities flags limitHit for the many-small-groups shape', () => { + const result = clusterEntities(groupsOf(800, 5), BOUNDS, 10, RULES) + expect(result.limitHit).toBe(true) +}) + +/* + * Coarsening is what keeps the cap from being a silent deletion: every entity + * in view is still standing behind some marker, just a bigger one. If this + * ever regresses to truncation the sum drops below the input count. + */ +test('clusterEntities represents every entity even when it has to cap', () => { + const entities = groupsOf(800, 5) + const result = clusterEntities(entities, BOUNDS, 10, RULES) + const represented = + result.clusters.reduce((sum, cluster) => sum + cluster.count, 0) + + result.points.length + expect(represented).toBe(entities.length) +}) + +test('clusterEntities bounds a mix of one dense blob and many small groups', () => { + const blob = Array.from({ length: 4000 }, (_, index) => ({ + id: `blob-${index}`, + lat: BOUNDS.south + 0.5 + index * 0.000001, + lon: BOUNDS.west + 0.5 + index * 0.000001, + })) + const entities = [...groupsOf(600, 5), ...blob, ...scatter(1000)] + for (const zoom of [4, 10, 14, 20]) { + const result = clusterEntities(entities, BOUNDS, zoom, RULES) + const rendered = result.clusters.length + result.points.length + expect(`zoom ${zoom}: ${rendered}`).toBe( + `zoom ${zoom}: ${Math.min(rendered, RULES.forcedLimit)}`, + ) + } +}) diff --git a/app/map/clustering.ts b/app/map/clustering.ts new file mode 100644 index 000000000..eb2b6699d --- /dev/null +++ b/app/map/clustering.ts @@ -0,0 +1,195 @@ +import Supercluster from 'supercluster' +import type { Bounds } from './types' + +/** + * Ported from 1.0's `config/default.json` `clustering` block + * (`src/pages/map/components/Clustering.jsx` line 72 for the shape). + * `zoomLevel` is the zoom above which supercluster stops clustering and + * hands back raw points; `forcedLimit` is the hard cap on how many + * markers may render at once for the category. + */ +export interface ClusterRules { + zoomLevel: number + forcedLimit: number + minPoints: number +} + +/** From `config/default.json`'s `clustering` block. */ +export const DEFAULT_POKEMON_CLUSTER_RULES: ClusterRules = { + zoomLevel: 15, + forcedLimit: 3000, + minPoints: 7, +} + +export const DEFAULT_GYM_CLUSTER_RULES: ClusterRules = { + zoomLevel: 13, + forcedLimit: 2500, + minPoints: 5, +} + +export interface ClusterableEntity { + id: string + lat: number + lon: number +} + +/** A synthetic marker standing in for `count` entities too close together to render individually. */ +export interface ClusterMarker { + kind: 'cluster' + id: string + lat: number + lon: number + count: number +} + +export interface ClusterResult { + /** Individual entities to render as their own markers. */ + points: readonly T[] + /** Synthetic cluster markers, each standing in for several entities. */ + clusters: readonly ClusterMarker[] + /** + * True when `rules.forcedLimit` was exceeded and the rendered set had to + * be capped. The caller should show this, not just cap silently - see + * clustering.test.ts and the task brief for why a silent cap is worse + * than a visible one. + */ + limitHit: boolean +} + +function toFeature(entity: ClusterableEntity): { + type: 'Feature' + id: string + properties: Record + geometry: { type: 'Point'; coordinates: [number, number] } +} { + return { + type: 'Feature', + id: entity.id, + properties: {}, + geometry: { type: 'Point', coordinates: [entity.lon, entity.lat] }, + } +} + +interface Rendered { + clusters: ClusterMarker[] + points: T[] +} + +/** + * Last-resort cap for when even zoom 0 will not fit: keep the markers + * standing for the most entities. A cluster covers at least `minPoints` + * entities and a loose point covers one, so clusters by descending count come + * first and points fill whatever budget is left. This maximises how much of + * the data is still represented on screen, which is the only defensible + * ordering when something genuinely has to go. + */ +function keepDensest(rendered: Rendered, limit: number): Rendered { + const clusters = [...rendered.clusters] + .sort((a, b) => b.count - a.count) + .slice(0, limit) + return { + clusters, + points: rendered.points.slice(0, Math.max(0, limit - clusters.length)), + } +} + +/** + * Clusters `entities` for one viewport/zoom and enforces `rules.forcedLimit` + * as a hard cap on the total rendered set - clusters plus loose points - + * independent of zoom. + * + * supercluster's `maxZoom` is the zoom ABOVE WHICH it stops clustering and + * returns raw, unclustered points - it is not a bound on how many markers + * come back. 1.0 built its clusterer with `maxZoom: rules.zoomLevel` and + * relied on that same clusterer to keep the count under `forcedLimit` + * (`src/pages/map/components/Clustering.jsx` lines 92-98, 157). Above + * `zoomLevel` - exactly where the most markers are on screen - supercluster + * stops clustering, so every entity in view comes back individually and + * nothing enforces the limit any more. + * + * An earlier pass at this capped only `points`, against a budget of + * `forcedLimit - clusters.length`, which binds for a uniformly scattered + * map and for nothing else. Give it many well-separated small groups - a + * country of small towns with a few gyms each - and every group survives as + * its own cluster, the point budget is zero before any capping happens, and + * the cluster count runs unbounded. Measured at gym rules with 3500 groups + * of 5, zooms 8, 10 and 13 each rendered 3500 markers against a limit of + * 2500. + * + * The cap here is on the total, and it is applied by COARSENING rather than + * by dropping markers: if the count at the requested zoom does not fit, the + * same index is queried at successively lower zooms until it does. Every + * entity in view is then still standing behind some marker, just a bigger + * one, which is the property that separates a decluttered map from a map + * that has silently deleted a third of its towns. Dropping the smallest + * clusters would have made whole regions vanish with nothing on screen + * saying so, and truncating an arbitrary slice is worse again. + * + * Only if even zoom 0 does not fit does anything get dropped, and then the + * markers kept are the ones standing for the most entities (see + * `keepDensest`). That path needs a `forcedLimit` smaller than the number of + * clusters the whole world collapses to, so in practice it is a guard, not a + * behaviour. + */ +export function clusterEntities( + entities: readonly T[], + bounds: Bounds, + zoom: number, + rules: ClusterRules, +): ClusterResult { + const index = new Supercluster({ + radius: 60, + extent: 256, + maxZoom: rules.zoomLevel, + minPoints: rules.minPoints, + }) + index.load(entities.map(toFeature)) + + const bbox: [number, number, number, number] = [ + bounds.west, + bounds.south, + bounds.east, + bounds.north, + ] + const byId = new Map(entities.map((entity) => [entity.id, entity])) + + const collect = (at: number): Rendered => { + const clusters: ClusterMarker[] = [] + const points: T[] = [] + for (const feature of index.getClusters(bbox, at)) { + const [lon, lat] = feature.geometry.coordinates + const properties = feature.properties + if (properties.cluster === true) { + clusters.push({ + kind: 'cluster', + id: `cluster-${feature.id}`, + lat, + lon, + count: properties.point_count as number, + }) + } else if (feature.id !== undefined) { + const entity = byId.get(String(feature.id)) + if (entity) points.push(entity) + } + } + return { clusters, points } + } + + const fits = (rendered: Rendered) => + rendered.clusters.length + rendered.points.length <= rules.forcedLimit + + const requested = Math.round(zoom) + const first = collect(requested) + if (fits(first)) return { ...first, limitHit: false } + + // Everything above `zoomLevel` returns the same unclustered set, so the + // descent starts at `zoomLevel` rather than walking those zooms one by one + // to get the identical answer each time. + let coarsest = first + for (let at = Math.min(requested - 1, rules.zoomLevel); at >= 0; at -= 1) { + coarsest = collect(at) + if (fits(coarsest)) return { ...coarsest, limitHit: true } + } + + return { ...keepDensest(coarsest, rules.forcedLimit), limitHit: true } +} diff --git a/app/map/draw-icon.ts b/app/map/draw-icon.ts new file mode 100644 index 000000000..c7fca9a08 --- /dev/null +++ b/app/map/draw-icon.ts @@ -0,0 +1,179 @@ +import type { IconDescriptor, IconDrawer } from './atlas' +import type { PokemonEntity } from './types' + +const ICON_SIZE = 64 + +/** + * Colours a pokemon's base circle by whether it carries a badge or weather + * boost, the only two appearance modifiers this placeholder renders + * distinctly (background and costume/form influence real sprite art, which + * this project has none of yet). This is a stand-in for real species + * artwork, not a claim that it is one; swapping in sprite sheets later + * replaces this function's body without touching its signature or anyone + * that calls it. + */ +function baseColorFor(entity: PokemonEntity): string { + if (entity.weather !== undefined) return '#7dd3fc' + if (entity.badge !== undefined) return '#fbbf24' + return '#e5e7eb' +} + +/** + * Draws one pokemon marker's pixels onto an `OffscreenCanvas` - a circle in + * `baseColorFor`'s colour with the species id centred on it, plus a ring + * when the entity carries a badge - and hands back a data URL `IconLayer` + * can fetch. + * + * `OffscreenCanvas` has no synchronous way to produce a data URL + * (`convertToBlob` is promise-based, and `IconDrawer` is synchronous so the + * atlas's cache-then-draw pipeline never has to deal with a pending + * result). This draws on the offscreen surface, then blits that surface + * onto a same-size, never-attached `` purely to call its + * synchronous `toDataURL`. All pixel compositing happens on the + * `OffscreenCanvas`; the bridge canvas does no drawing of its own. + * + * Only runs in a browser: `OffscreenCanvas` does not exist in the `bun + * test` environment, so this function is exercised by manual/browser + * verification, not a unit test (see task-4-report.md). + */ +export const drawPokemonIcon: IconDrawer = ( + entity: PokemonEntity, + key: string, +): IconDescriptor => { + const offscreen = new OffscreenCanvas(ICON_SIZE, ICON_SIZE) + const ctx = offscreen.getContext('2d') + if (!ctx) throw new Error('2d context unavailable on OffscreenCanvas') + + const center = ICON_SIZE / 2 + const radius = ICON_SIZE / 2 - 4 + + ctx.fillStyle = baseColorFor(entity) + ctx.beginPath() + ctx.arc(center, center, radius, 0, Math.PI * 2) + ctx.fill() + + if (entity.badge !== undefined) { + ctx.strokeStyle = '#dc2626' + ctx.lineWidth = 3 + ctx.beginPath() + ctx.arc(center, center, radius - 1.5, 0, Math.PI * 2) + ctx.stroke() + } + + ctx.fillStyle = '#111827' + ctx.font = `${ICON_SIZE * 0.3}px sans-serif` + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillText(String(entity.pokemonId), center, center) + + const bridge = document.createElement('canvas') + bridge.width = ICON_SIZE + bridge.height = ICON_SIZE + const bridgeCtx = bridge.getContext('2d') + if (!bridgeCtx) throw new Error('2d context unavailable on bridge canvas') + bridgeCtx.drawImage(offscreen, 0, 0) + + return { + id: key, + url: bridge.toDataURL('image/png'), + width: ICON_SIZE, + height: ICON_SIZE, + } +} + +const CLUSTER_ICON_ID = 'cluster-marker-mask' +let cachedClusterIcon: IconDescriptor | undefined + +/** + * The single mask icon every cluster bubble shares: a plain circle, same + * pattern as `drawGymIcon` and for the same reason - a cluster's only + * per-entity property is its count, which is rendered as text on top (see + * `buildClusterTextLayer` in layers.ts), not baked into the icon itself. + * `IconLayer.getColor` tints this per cluster by size bucket. + */ +export function drawClusterIcon(): IconDescriptor { + if (cachedClusterIcon) return cachedClusterIcon + + const offscreen = new OffscreenCanvas(ICON_SIZE, ICON_SIZE) + const ctx = offscreen.getContext('2d') + if (!ctx) throw new Error('2d context unavailable on OffscreenCanvas') + + const center = ICON_SIZE / 2 + const radius = ICON_SIZE / 2 - 4 + + ctx.fillStyle = '#ffffff' + ctx.beginPath() + ctx.arc(center, center, radius, 0, Math.PI * 2) + ctx.fill() + + const bridge = document.createElement('canvas') + bridge.width = ICON_SIZE + bridge.height = ICON_SIZE + const bridgeCtx = bridge.getContext('2d') + if (!bridgeCtx) throw new Error('2d context unavailable on bridge canvas') + bridgeCtx.drawImage(offscreen, 0, 0) + + cachedClusterIcon = { + id: CLUSTER_ICON_ID, + url: bridge.toDataURL('image/png'), + width: ICON_SIZE, + height: ICON_SIZE, + } + return cachedClusterIcon +} + +const GYM_ICON_ID = 'gym-marker-mask' +let cachedGymIcon: IconDescriptor | undefined + +/** + * The single mask icon every gym marker shares: a plain circle, drawn once + * and cached at module scope rather than through Task 2's atlas, because a + * gym has one shape regardless of team - there is nothing per-entity for a + * cache keyed on appearance to do here. `IconLayer.getColor` tints this per + * gym from `readTeamColor` (see layers.ts); the pixels drawn here carry no + * colour of their own beyond full opacity, which is what makes tinting + * work. + */ +export function drawGymIcon(): IconDescriptor { + if (cachedGymIcon) return cachedGymIcon + + const offscreen = new OffscreenCanvas(ICON_SIZE, ICON_SIZE) + const ctx = offscreen.getContext('2d') + if (!ctx) throw new Error('2d context unavailable on OffscreenCanvas') + + const center = ICON_SIZE / 2 + const radius = ICON_SIZE / 2 - 4 + + ctx.fillStyle = '#ffffff' + ctx.beginPath() + ctx.arc(center, center, radius, 0, Math.PI * 2) + ctx.fill() + + const bridge = document.createElement('canvas') + bridge.width = ICON_SIZE + bridge.height = ICON_SIZE + const bridgeCtx = bridge.getContext('2d') + if (!bridgeCtx) throw new Error('2d context unavailable on bridge canvas') + bridgeCtx.drawImage(offscreen, 0, 0) + + cachedGymIcon = { + id: GYM_ICON_ID, + url: bridge.toDataURL('image/png'), + width: ICON_SIZE, + height: ICON_SIZE, + } + return cachedGymIcon +} + +/** + * Drops the module-level icon caches. + * + * These hold canvas-derived descriptors that die with the WebGL context, and + * unlike the pokemon atlas they are plain module state with no owner to clear + * them. A restore that leaves them populated hands deck.gl textures that no + * longer exist, which renders nothing and reports nothing. + */ +export function resetSharedIconCaches() { + cachedClusterIcon = undefined + cachedGymIcon = undefined +} diff --git a/app/map/env.d.ts b/app/map/env.d.ts new file mode 100644 index 000000000..fcb3fed0f --- /dev/null +++ b/app/map/env.d.ts @@ -0,0 +1,15 @@ +/** + * Vite does not ship `ImportMetaEnv` typings unless `vite/client` is in + * `tsconfig`'s `types`, and this project's `tsconfig.json` only lists + * `bun-types` (shared with the Node-side `server/`). Declaring the one env + * var this module reads here, rather than adding `vite/client` globally, + * avoids pulling in `vite/client`'s own DOM lib assumptions for the whole + * `app/` tree over one field. + */ +interface ImportMetaEnv { + readonly VITE_BASEMAP_URL?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/app/map/fixtures.test.ts b/app/map/fixtures.test.ts new file mode 100644 index 000000000..d9948ae03 --- /dev/null +++ b/app/map/fixtures.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from 'bun:test' +import { generateFixtureGyms, generateFixturePokemon } from './fixtures' + +test('regenerating pokemon fixtures produces byte-identical entities', () => { + const first = generateFixturePokemon() + const second = generateFixturePokemon() + + expect(first).not.toBe(second) + expect(second).toEqual(first) +}) + +test('regenerating gym fixtures produces byte-identical entities', () => { + const first = generateFixtureGyms() + const second = generateFixtureGyms() + + expect(first).not.toBe(second) + expect(second).toEqual(first) +}) + +test('expiry timestamps do not move with wall-clock time', async () => { + const first = generateFixturePokemon() + await new Promise((resolve) => setTimeout(resolve, 25)) + const second = generateFixturePokemon() + + const moved = first.filter( + (entity, index) => entity.expiresAt !== second[index]?.expiresAt, + ) + expect(moved).toHaveLength(0) +}) diff --git a/app/map/fixtures.ts b/app/map/fixtures.ts new file mode 100644 index 000000000..96c7a26af --- /dev/null +++ b/app/map/fixtures.ts @@ -0,0 +1,167 @@ +import type { Gender, GymEntity, PokemonEntity, Team } from './types' + +/** + * The area every fixture is scattered across. Chosen to comfortably + * contain both the wide and narrow bounding boxes exercised by + * source.test.ts. + */ +const FIXTURE_AREA = { west: -1, south: 51, east: 1, north: 52 } + +/** + * 3000 is the floor the test asserts: the whole reason for this engine + * is that rendering past three thousand markers goes choppy, so the + * fixture set has to clear that bar with room to spare rather than + * exactly hit it. + */ +const POKEMON_COUNT = 5000 +const GYM_COUNT = 100 + +const SEED = 20260824 + +/** + * A fixed point in time that expiry is measured from. Date.now() here + * would make every expiresAt differ between runs, which defeats the + * reason these fixtures are seeded at all: a countdown that changes on + * every reload cannot be compared against a screenshot from yesterday. + */ +/** + * The instant fixture expiries are measured from. + * + * Fixed rather than wall-clock so a given seed always produces byte-identical + * entities. The cost is that against real time every fixture is long expired, + * so anything rendering a countdown from `Date.now()` shows 0:00 for all of + * them and the map reads as dead. A caller wanting live countdowns should run + * its clock from here rather than from now; see MapCanvas. + */ +export const FIXTURE_EPOCH = Date.UTC(2026, 7, 24) + +/** Deterministic PRNG (mulberry32) so fixtures reproduce across runs. */ +function createRng(seed: number) { + let state = seed + return () => { + state = (state + 0x6d2b79f5) | 0 + let t = state + t = Math.imul(t ^ (t >>> 15), t | 1) + t ^= t + Math.imul(t ^ (t >>> 7), t | 61) + return ((t ^ (t >>> 14)) >>> 0) / 4294967296 + } +} + +function randomInRange(rng: () => number, min: number, max: number): number { + return min + rng() * (max - min) +} + +function randomInt(rng: () => number, min: number, max: number): number { + return Math.floor(randomInRange(rng, min, max + 1)) +} + +function pick(rng: () => number, values: readonly [T, ...T[]]): T { + return values[randomInt(rng, 0, values.length - 1)] as T +} + +const TEAMS: readonly [Team, ...Team[]] = [0, 1, 2, 3] + +/** + * A viewport does not contain a uniform sample of the species list. A + * dozen commons account for most of what is on screen, a wider pool of + * regulars fills in around them, and anything else shows up rarely. + * Drawing uniformly across all 493 instead made almost every fixture + * visually distinct, which is close to the worst case for a cache keyed + * on appearance and made these fixtures useless as evidence that one + * works. + */ +const COMMON_SPECIES = [16, 19, 21, 23, 41, 43, 46, 52, 60, 69, 74, 96] +const REGULAR_SPECIES = [ + 1, 4, 7, 10, 13, 25, 27, 29, 32, 35, 37, 39, 50, 54, 58, 63, 66, 72, 77, 79, + 81, 84, 90, 92, 100, 102, 104, 109, 111, 118, +] +const COMMON_SHARE = 0.65 +const REGULAR_SHARE = 0.95 +const MAX_SPECIES = 493 + +function randomSpecies(rng: () => number): number { + const roll = rng() + if (roll < COMMON_SHARE) { + return COMMON_SPECIES[ + randomInt(rng, 0, COMMON_SPECIES.length - 1) + ] as number + } + if (roll < REGULAR_SHARE) { + return REGULAR_SPECIES[ + randomInt(rng, 0, REGULAR_SPECIES.length - 1) + ] as number + } + return randomInt(rng, 1, MAX_SPECIES) +} + +function buildPokemon(rng: () => number, index: number): PokemonEntity { + const pokemonId = randomSpecies(rng) + // Alternate forms belong to a minority of species, and even those spawn + // in their default form most of the time. Costumes are event-limited and + // rarer still. Gender is a property of the species: a tenth of them are + // genderless, and the rest split evenly. + const hasAlternateForms = pokemonId % 7 === 0 + const isGenderless = pokemonId % 10 === 0 + const gender: Gender = isGenderless ? 3 : rng() < 0.5 ? 1 : 2 + const entity: PokemonEntity = { + kind: 'pokemon', + spawnId: `pokemon-${index}`, + pokemonId, + form: hasAlternateForms && rng() < 0.4 ? randomInt(rng, 1, 3) : 0, + costume: rng() < 0.02 ? randomInt(rng, 1, 2) : 0, + gender, + lat: randomInRange(rng, FIXTURE_AREA.south, FIXTURE_AREA.north), + lon: randomInRange(rng, FIXTURE_AREA.west, FIXTURE_AREA.east), + expiresAt: FIXTURE_EPOCH + randomInt(rng, 60, 1800) * 1000, + } + if (rng() < 0.05) entity.badge = randomInt(rng, 1, 3) + if (rng() < 0.1) entity.background = randomInt(rng, 1, 5) + if (rng() < 0.15) entity.weather = randomInt(rng, 1, 7) + if (rng() < 0.6) entity.iv = randomInt(rng, 0, 100) + if (rng() < 0.6) entity.level = randomInt(rng, 1, 35) + if (rng() < 0.4) entity.size = randomInt(rng, 1, 5) + return entity +} + +function buildGym(rng: () => number, index: number): GymEntity { + return { + kind: 'gym', + gymId: `gym-${index}`, + lat: randomInRange(rng, FIXTURE_AREA.south, FIXTURE_AREA.north), + lon: randomInRange(rng, FIXTURE_AREA.west, FIXTURE_AREA.east), + team: pick(rng, TEAMS), + inBattle: rng() < 0.1, + } +} + +/** + * Builds a fresh set every call. The cached getters below return the + * same array identity, so a determinism test written against them + * would pass without proving anything; these are what that test needs. + */ +export function generateFixturePokemon(): PokemonEntity[] { + const rng = createRng(SEED) + return Array.from({ length: POKEMON_COUNT }, (_, index) => + buildPokemon(rng, index), + ) +} + +export function generateFixtureGyms(): GymEntity[] { + // Separate seed offset so gym placement doesn't consume the same + // PRNG stream as pokemon and shift its sequence. + const rng = createRng(SEED + 1) + return Array.from({ length: GYM_COUNT }, (_, index) => buildGym(rng, index)) +} + +let cachedPokemon: PokemonEntity[] | undefined +let cachedGyms: GymEntity[] | undefined + +export function getFixturePokemon(): PokemonEntity[] { + if (!cachedPokemon) cachedPokemon = generateFixturePokemon() + return cachedPokemon +} + +export function getFixtureGyms(): GymEntity[] { + if (!cachedGyms) cachedGyms = generateFixtureGyms() + return cachedGyms +} diff --git a/app/map/layers.test.ts b/app/map/layers.test.ts new file mode 100644 index 000000000..4a1eafcf0 --- /dev/null +++ b/app/map/layers.test.ts @@ -0,0 +1,339 @@ +import { afterAll, beforeAll, expect, test } from 'bun:test' +import { setupDom, teardownDom } from '../test-setup' +import type { IconDescriptor } from './atlas' +import { + buildClusterIconLayer, + buildClusterTextLayer, + buildGymIconLayer, + buildMapLayers, + buildPokemonIconLayer, + buildPokemonTextLayer, + formatCountdown, + GYM_CLUSTER_ICON_LAYER_ID, + GYM_CLUSTER_LABEL_LAYER_ID, + GYM_ICON_LAYER_ID, + POKEMON_ICON_LAYER_ID, + POKEMON_LABEL_LAYER_ID, + readClusterColor, + readClusterLabelColor, + readTeamColor, +} from './layers' +import type { GymEntity, PokemonEntity } from './types' + +/* + * Layers need a real WebGL context to render, which the test environment + * lacks (see task-4-report.md). What is tested here is construction: given + * entities, do the right layers come out with the right ids, counts and + * accessors. Whether markers actually sit under street labels in an + * interleaved MapLibre map is a claim only a browser can back; nothing + * below asserts that. + */ + +beforeAll(setupDom) +afterAll(teardownDom) + +const POKEMON: PokemonEntity = { + kind: 'pokemon', + spawnId: 'spawn-1', + pokemonId: 6, + form: 0, + costume: 0, + gender: 1, + lat: 51.5, + lon: -0.1, + expiresAt: 1_000, +} + +const GYM: GymEntity = { + kind: 'gym', + gymId: 'gym-1', + lat: 51.6, + lon: -0.2, + team: 2, + inBattle: false, +} + +const STUB_ICON: IconDescriptor = { + id: 'stub', + url: 'data:image/png;base64,stub', + width: 64, + height: 64, +} + +test('formatCountdown counts down from a pinned now', () => { + expect(formatCountdown(90_000, 0)).toBe('1:30') + expect(formatCountdown(5_000, 0)).toBe('0:05') +}) + +test('formatCountdown clamps an already-expired entity to zero rather than going negative', () => { + // This is the fixtures' actual situation: FIXTURE_EPOCH is a fixed past + // instant, so every fixture pokemon's expiresAt is behind real + // wall-clock now. A countdown must not read as a negative or wrap. + expect(formatCountdown(1_000, 999_999)).toBe('0:00') +}) + +test('buildPokemonIconLayer produces one instance per entity with the atlas descriptor', () => { + const layer = buildPokemonIconLayer([POKEMON], () => STUB_ICON) + expect(layer.id).toBe(POKEMON_ICON_LAYER_ID) + expect(layer.props.data).toHaveLength(1) + const icon = (layer.props.getIcon as (entity: PokemonEntity) => unknown)( + POKEMON, + ) + expect(icon).toEqual({ + id: STUB_ICON.id, + url: STUB_ICON.url, + width: STUB_ICON.width, + height: STUB_ICON.height, + }) +}) + +test('buildPokemonIconLayer calls getIconFor once per accessor invocation, not eagerly for the whole set', () => { + let calls = 0 + const layer = buildPokemonIconLayer([POKEMON], () => { + calls += 1 + return STUB_ICON + }) + expect(calls).toBe(0) + ;(layer.props.getIcon as (entity: PokemonEntity) => unknown)(POKEMON) + expect(calls).toBe(1) +}) + +test('buildPokemonTextLayer renders iv and countdown together when iv is present', () => { + const withIv: PokemonEntity = { ...POKEMON, iv: 82, expiresAt: 65_000 } + const layer = buildPokemonTextLayer([withIv], 5_000) + expect(layer.id).toBe(POKEMON_LABEL_LAYER_ID) + const text = (layer.props.getText as (entity: PokemonEntity) => string)( + withIv, + ) + expect(text).toBe('82% 1:00') +}) + +test('buildPokemonTextLayer omits iv entirely when the entity has none', () => { + const layer = buildPokemonTextLayer([POKEMON], 0) + const text = (layer.props.getText as (entity: PokemonEntity) => string)( + POKEMON, + ) + expect(text).not.toContain('%') +}) + +test('buildPokemonTextLayer data count matches the visible set, one entry per timer', () => { + const many = Array.from({ length: 12 }, (_, index) => ({ + ...POKEMON, + spawnId: `spawn-${index}`, + })) + const layer = buildPokemonTextLayer(many, 0) + expect(layer.props.data).toHaveLength(12) +}) + +test('readTeamColor resolves a token set on the given root element', () => { + // happy-dom (and real browsers) only resolve computed style, custom + // properties included, for an element actually attached to the document; + // a detached element's getComputedStyle reads every property as empty, + // which looks identical to a pruned token if this were skipped. + const root = document.createElement('div') + root.style.setProperty('--color-team-2', '#d83c22') + document.body.appendChild(root) + try { + expect(readTeamColor(2, root)).toEqual([0xd8, 0x3c, 0x22, 255]) + } finally { + root.remove() + } +}) + +test('readTeamColor falls back to a visible grey, not a colourless value, when the token does not resolve', () => { + const root = document.createElement('div') + // No --color-team-1 set on this element: simulates the token being + // pruned or the palette stylesheet not having loaded. + const warn = console.warn + let warned = false + console.warn = () => { + warned = true + } + try { + const color = readTeamColor(1, root) + expect(color).toEqual([128, 128, 128, 255]) + expect(warned).toBe(true) + } finally { + console.warn = warn + } +}) + +test('buildGymIconLayer tints its shared mask icon per entity from the team token', () => { + const root = document.createElement('div') + root.style.setProperty('--color-team-2', '#d83c22') + document.body.appendChild(root) + try { + const layer = buildGymIconLayer([GYM], () => STUB_ICON, root) + expect(layer.id).toBe(GYM_ICON_LAYER_ID) + expect(layer.props.data).toHaveLength(1) + const color = (layer.props.getColor as (entity: GymEntity) => unknown)(GYM) + expect(color).toEqual([0xd8, 0x3c, 0x22, 255]) + } finally { + root.remove() + } +}) + +test('buildGymIconLayer marks the shared icon as a mask so getColor actually tints it', () => { + const layer = buildGymIconLayer([GYM], () => STUB_ICON) + const getIcon = layer.props.getIcon as unknown as (entity: GymEntity) => { + mask?: boolean + } + expect(getIcon(GYM).mask).toBe(true) +}) + +test('buildMapLayers returns gyms, pokemon icons and pokemon labels, in that draw order', () => { + const { layers } = buildMapLayers({ + pokemon: [POKEMON], + gyms: [GYM], + getIconFor: () => STUB_ICON, + getGymIcon: () => STUB_ICON, + now: 0, + }) + expect(layers.map((layer) => layer.id)).toEqual([ + GYM_ICON_LAYER_ID, + POKEMON_ICON_LAYER_ID, + POKEMON_LABEL_LAYER_ID, + ]) +}) + +test('buildMapLayers scales to the viewport-sized set the fixtures exist to exercise', () => { + const pokemon = Array.from({ length: 500 }, (_, index) => ({ + ...POKEMON, + spawnId: `spawn-${index}`, + })) + const { layers } = buildMapLayers({ + pokemon, + gyms: [], + getIconFor: () => STUB_ICON, + getGymIcon: () => STUB_ICON, + now: 0, + }) + const iconLayer = layers.find((layer) => layer.id === POKEMON_ICON_LAYER_ID) + expect(iconLayer?.props.data).toHaveLength(500) +}) + +/* + * The point of the flag is that a capped map and an empty area look the same + * on screen, so the flag has to reach a caller that can say which one it is. + * An earlier pass computed it in clusterEntities and dropped it here, which + * is the same as not computing it. + */ +test('buildMapLayers reports no cap when nothing was clustered', () => { + const result = buildMapLayers({ + pokemon: [POKEMON], + gyms: [GYM], + getIconFor: () => STUB_ICON, + getGymIcon: () => STUB_ICON, + now: 0, + }) + expect(result.limitHit).toEqual({ pokemon: false, gyms: false }) +}) + +test('buildMapLayers carries limitHit out of the gym clusterer', () => { + const gyms = Array.from({ length: 400 }, (_, index) => ({ + ...GYM, + gymId: `gym-${index}`, + lat: 51 + (index % 20) * 0.05, + lon: -0.5 + Math.floor(index / 20) * 0.05, + })) + const result = buildMapLayers({ + pokemon: [], + gyms, + getIconFor: () => STUB_ICON, + getGymIcon: () => STUB_ICON, + now: 0, + viewport: { + bounds: { west: -1, south: 50, east: 1, north: 53 }, + zoom: 14, + }, + clusterRules: { + gyms: { zoomLevel: 13, forcedLimit: 50, minPoints: 2 }, + }, + }) + expect(result.limitHit.gyms).toBe(true) + expect(result.limitHit.pokemon).toBe(false) +}) + +/* + * Cluster bubbles read the same palette the gym layer does. These were + * hardcoded RGBA literals in this file until the tokens existed, which is the + * one thing the plan's colour constraint exists to prevent: a map colour that + * cannot be changed from the palette is a map colour nothing can review. + */ +test('readClusterColor picks a bucket token by count and keeps the bubble alpha', () => { + const root = document.createElement('div') + root.style.setProperty('--color-cluster-small', '#6ecc39') + root.style.setProperty('--color-cluster-medium', '#f0c20c') + root.style.setProperty('--color-cluster-large', '#f18017') + document.body.appendChild(root) + try { + expect(readClusterColor(12, root)).toEqual([0x6e, 0xcc, 0x39, 230]) + expect(readClusterColor(100, root)).toEqual([0xf0, 0xc2, 0x0c, 230]) + expect(readClusterColor(1000, root)).toEqual([0xf1, 0x80, 0x17, 230]) + } finally { + root.remove() + } +}) + +test('readClusterColor falls back to a visible grey when its token does not resolve', () => { + const root = document.createElement('div') + const warn = console.warn + let warned = false + console.warn = () => { + warned = true + } + try { + expect(readClusterColor(5, root)).toEqual([128, 128, 128, 230]) + expect(warned).toBe(true) + } finally { + console.warn = warn + } +}) + +test('buildClusterIconLayer and buildClusterTextLayer take their colours from the palette', () => { + const root = document.createElement('div') + root.style.setProperty('--color-cluster-medium', '#f0c20c') + root.style.setProperty('--color-cluster-label', '#111827') + document.body.appendChild(root) + const cluster = { + kind: 'cluster' as const, + id: 'cluster-1', + lat: 51.5, + lon: -0.1, + count: 250, + } + try { + const icons = buildClusterIconLayer( + [cluster], + GYM_CLUSTER_ICON_LAYER_ID, + () => STUB_ICON, + root, + ) + const getColor = icons.props.getColor as (marker: typeof cluster) => unknown + expect(getColor(cluster)).toEqual([0xf0, 0xc2, 0x0c, 230]) + + const labels = buildClusterTextLayer( + [cluster], + GYM_CLUSTER_LABEL_LAYER_ID, + root, + ) + expect(labels.props.getColor).toEqual([0x11, 0x18, 0x27, 255]) + } finally { + root.remove() + } +}) + +test('readClusterLabelColor falls back to a visible grey when its token does not resolve', () => { + const root = document.createElement('div') + const warn = console.warn + let warned = false + console.warn = () => { + warned = true + } + try { + expect(readClusterLabelColor(root)).toEqual([128, 128, 128, 255]) + expect(warned).toBe(true) + } finally { + console.warn = warn + } +}) diff --git a/app/map/layers.ts b/app/map/layers.ts new file mode 100644 index 000000000..eb614a71d --- /dev/null +++ b/app/map/layers.ts @@ -0,0 +1,500 @@ +import type { Color, Layer } from '@deck.gl/core' +import { IconLayer, TextLayer } from '@deck.gl/layers' +import type { IconDescriptor } from './atlas' +import { + type ClusterMarker, + type ClusterRules, + clusterEntities, + DEFAULT_GYM_CLUSTER_RULES, + DEFAULT_POKEMON_CLUSTER_RULES, +} from './clustering' +import type { GymEntity, PokemonEntity, Team, Viewport } from './types' + +export const POKEMON_ICON_LAYER_ID = 'pokemon-icons' +export const POKEMON_LABEL_LAYER_ID = 'pokemon-labels' +export const GYM_ICON_LAYER_ID = 'gym-icons' +export const POKEMON_CLUSTER_ICON_LAYER_ID = 'pokemon-cluster-icons' +export const POKEMON_CLUSTER_LABEL_LAYER_ID = 'pokemon-cluster-labels' +export const GYM_CLUSTER_ICON_LAYER_ID = 'gym-cluster-icons' +export const GYM_CLUSTER_LABEL_LAYER_ID = 'gym-cluster-labels' + +/** Fallback for a team colour token that failed to resolve. Neutral grey, + * distinct from every real team colour, so a broken token is visible on + * the map rather than invisible in a colourless marker. */ +const FALLBACK_TEAM_COLOR: Color = [128, 128, 128, 255] + +/** + * Maps each `Team` value to the custom property carrying its colour in + * `app/tokens/data-palette.css`. That file's `@theme static` block is what + * keeps these from being pruned by Tailwind's unused-variable scan; see + * the comment there for why a scanner can't see this file's usage. + */ +const TEAM_COLOR_TOKEN: Record = { + 0: '--color-team-0', + 1: '--color-team-1', + 2: '--color-team-2', + 3: '--color-team-3', +} + +function parseHexColor(value: string): Color | undefined { + const match = /^#([0-9a-f]{6})$/i.exec(value.trim()) + if (!match) return undefined + const hex = match[1] as string + return [ + Number.parseInt(hex.slice(0, 2), 16), + Number.parseInt(hex.slice(2, 4), 16), + Number.parseInt(hex.slice(4, 6), 16), + 255, + ] +} + +/** + * Reads one team's colour from the data palette's CSS custom properties at + * runtime, rather than importing a colour table, so the map surface and the + * rest of the design system share one source of truth. + * + * A pruned or otherwise unresolved custom property comes back from + * `getComputedStyle` as an empty string, and nothing about that fails + * loudly; `getIcon`/`getColor` would just receive nothing and the marker + * would render without colour. This checks what the property actually + * resolved to and falls back to a visible neutral grey with a console + * warning instead of letting that happen silently. + */ +export function readTeamColor( + team: Team, + root: HTMLElement = document.documentElement, +): Color { + const token = TEAM_COLOR_TOKEN[team] + const raw = getComputedStyle(root).getPropertyValue(token) + const color = parseHexColor(raw) + if (!color) { + // eslint-disable-next-line no-console + console.warn( + `[map] team colour token ${token} resolved to ${JSON.stringify(raw)}, expected a #rrggbb value. Falling back to grey.`, + ) + return FALLBACK_TEAM_COLOR + } + return color +} + +/** + * Formats the time remaining until `expiresAt` as `m:ss`, clamped at zero + * rather than going negative once a fixture (or a real encounter) expires. + * + * Takes `now` as a parameter instead of reading `Date.now()` internally so + * a test can pin a moment and compare against it, and so the caller is the + * single place that decides how often this gets recomputed (once a second, + * driven by a timer that rebuilds the layer's data array; see MapCanvas). + */ +export function formatCountdown(expiresAt: number, now: number): string { + const remainingSeconds = Math.max(0, Math.floor((expiresAt - now) / 1000)) + const minutes = Math.floor(remainingSeconds / 60) + const seconds = remainingSeconds % 60 + return `${minutes}:${seconds.toString().padStart(2, '0')}` +} + +function pokemonLabelText(entity: PokemonEntity, now: number): string { + const countdown = formatCountdown(entity.expiresAt, now) + return entity.iv === undefined ? countdown : `${entity.iv}% ${countdown}` +} + +/** + * Markers for every visible pokemon. `getIcon` calls `getIconFor` per + * entity rather than pre-drawing the whole array: the atlas's own LRU is + * what keeps that cheap on a hit, and staying entity-in-descriptor-out here + * keeps this layer ignorant of the drawing/caching machinery entirely. + */ +export function buildPokemonIconLayer( + pokemon: readonly PokemonEntity[], + getIconFor: (entity: PokemonEntity) => IconDescriptor, +): IconLayer { + return new IconLayer({ + id: POKEMON_ICON_LAYER_ID, + data: pokemon, + pickable: true, + getPosition: (entity) => [entity.lon, entity.lat], + getIcon: (entity) => { + const icon = getIconFor(entity) + return { + id: icon.id, + url: icon.url, + width: icon.width, + height: icon.height, + } + }, + getSize: 32, + }) +} + +/** + * Countdown (and, when present, IV) text for every visible pokemon. This is + * plain layer data: one `TextLayer` covering the whole array, rebuilt with + * a fresh `now` once a second by the caller. A `TextLayer` diffs its data + * array itself and only touches the instances whose text actually changed, + * which is the entire reason this exists instead of a per-marker timer + * component - thousands of those is exactly the pattern this rewrite + * replaces. + */ +export function buildPokemonTextLayer( + pokemon: readonly PokemonEntity[], + now: number, +): TextLayer { + return new TextLayer({ + id: POKEMON_LABEL_LAYER_ID, + data: pokemon, + pickable: false, + getPosition: (entity) => [entity.lon, entity.lat], + getText: (entity) => pokemonLabelText(entity, now), + getSize: 12, + getPixelOffset: [0, 20], + background: true, + getBackgroundColor: [0, 0, 0, 140], + }) +} + +/** + * Markers for every visible gym. `getGymIcon` supplies one shared mask + * icon (a gym has no per-entity appearance the way a pokemon does, so + * there is nothing here for Task 2's atlas to key on); `getColor` tints + * that mask per gym from `readTeamColor`, which is what actually carries + * the team's meaning. + */ +export function buildGymIconLayer( + gyms: readonly GymEntity[], + getGymIcon: () => IconDescriptor, + root?: HTMLElement, +): IconLayer { + return new IconLayer({ + id: GYM_ICON_LAYER_ID, + data: gyms, + pickable: true, + getPosition: (entity) => [entity.lon, entity.lat], + getIcon: () => { + const icon = getGymIcon() + return { + id: icon.id, + url: icon.url, + width: icon.width, + height: icon.height, + mask: true, + } + }, + getColor: (entity) => readTeamColor(entity.team, root), + getSize: 28, + }) +} + +/** + * How opaque a cluster bubble is painted. Not a token: this is the bubble's + * material, letting a little of the basemap through so a cluster does not + * read as a hole punched in the map, and it is the same for every bucket. + * The colour tokens carry the meaning; this carries none. + */ +const CLUSTER_ALPHA = 230 + +/** + * Buckets a cluster by how many entities it stands in for, same three-tier + * split as 1.0's `marker-cluster-{small,medium,large}` CSS classes + * (`Clustering.jsx`'s `createClusterIcon`): a cluster of a few dozen reads + * very differently from one standing in for a thousand entities, and the + * colour is the only signal of that at a glance. + */ +function clusterColorToken(count: number): string { + if (count < 100) return '--color-cluster-small' + if (count < 1000) return '--color-cluster-medium' + return '--color-cluster-large' +} + +/** + * Reads a cluster bubble's fill from the data palette, the same way + * `readTeamColor` reads a team's, and for the same reason: map entity + * colours live in `app/tokens/data-palette.css` and nowhere else. Same + * unresolved-token fallback too, since a pruned custom property fails + * silently rather than loudly. + */ +export function readClusterColor( + count: number, + root: HTMLElement = document.documentElement, +): Color { + const token = clusterColorToken(count) + const raw = getComputedStyle(root).getPropertyValue(token) + const color = parseHexColor(raw) + if (!color) { + console.warn( + `[map] cluster colour token ${token} resolved to ${JSON.stringify(raw)}, expected a #rrggbb value. Falling back to grey.`, + ) + return [ + FALLBACK_TEAM_COLOR[0], + FALLBACK_TEAM_COLOR[1], + FALLBACK_TEAM_COLOR[2], + CLUSTER_ALPHA, + ] + } + return [color[0], color[1], color[2], CLUSTER_ALPHA] +} + +/** The count drawn on a bubble, read from the same palette as the fill. */ +export function readClusterLabelColor( + root: HTMLElement = document.documentElement, +): Color { + const raw = getComputedStyle(root).getPropertyValue('--color-cluster-label') + const color = parseHexColor(raw) + if (!color) { + console.warn( + `[map] cluster label colour token --color-cluster-label resolved to ${JSON.stringify(raw)}, expected a #rrggbb value. Falling back to grey.`, + ) + return FALLBACK_TEAM_COLOR + } + return color +} + +/** + * Icon bubble for a set of cluster markers, tinted by `clusterColorFor`. + * Shared between the pokemon and gym cluster layers; only the layer `id` + * and the input array differ between them. + */ +export function buildClusterIconLayer( + clusters: readonly ClusterMarker[], + id: string, + getClusterIcon: () => IconDescriptor, + root?: HTMLElement, +): IconLayer { + return new IconLayer({ + id, + data: clusters, + pickable: false, + getPosition: (cluster) => [cluster.lon, cluster.lat], + getIcon: () => { + const icon = getClusterIcon() + return { + id: icon.id, + url: icon.url, + width: icon.width, + height: icon.height, + mask: true, + } + }, + getColor: (cluster) => readClusterColor(cluster.count, root), + getSize: 36, + }) +} + +/** The entity count centred on each cluster bubble. */ +export function buildClusterTextLayer( + clusters: readonly ClusterMarker[], + id: string, + root?: HTMLElement, +): TextLayer { + return new TextLayer({ + id, + data: clusters, + pickable: false, + getPosition: (cluster) => [cluster.lon, cluster.lat], + getText: (cluster) => String(cluster.count), + getSize: 13, + getColor: readClusterLabelColor(root), + }) +} + +export interface BuildMapLayersOptions { + pokemon: readonly PokemonEntity[] + gyms: readonly GymEntity[] + getIconFor: (entity: PokemonEntity) => IconDescriptor + getGymIcon: () => IconDescriptor + now: number + root?: HTMLElement + /** + * The current camera bounds/zoom. When present, `buildMapLayers` clusters + * pokemon and gyms for that viewport and enforces each category's + * `forcedLimit` (see clustering.ts) - the fix for the bug traced in the + * task brief, where 1.0's limit stopped applying above its clusterer's + * `maxZoom`. Omitted, every entity renders individually as before; this + * keeps every existing caller (and layers.test.ts) working unchanged. + */ + viewport?: Viewport + /** Per-category override of the default rules read from `config/default.json`. */ + clusterRules?: { pokemon?: ClusterRules; gyms?: ClusterRules } + getClusterIcon?: () => IconDescriptor +} + +/** + * All layers this task adds, in the order deck.gl should draw them: gyms + * and pokemon icons first, pokemon labels last so text always sits on top + * of the markers it describes. Draw order between layers is independent of + * `interleaved`, which only controls where the whole deck.gl stack sits + * relative to MapLibre's own street-label layer. + */ +interface ClusteredEntities { + points: readonly T[] + clusters: readonly ClusterMarker[] + /** Carried straight out of `clusterEntities`; see `MapLayersResult`. */ + limitHit: boolean +} + +function clusterPokemon( + pokemon: readonly PokemonEntity[], + viewport: Viewport, + rules: ClusterRules, +): ClusteredEntities { + const wrapped = pokemon.map((entity) => ({ + id: entity.spawnId, + lat: entity.lat, + lon: entity.lon, + entity, + })) + const result = clusterEntities(wrapped, viewport.bounds, viewport.zoom, rules) + return { + points: result.points.map((wrapper) => wrapper.entity), + clusters: result.clusters, + limitHit: result.limitHit, + } +} + +function clusterGyms( + gyms: readonly GymEntity[], + viewport: Viewport, + rules: ClusterRules, +): ClusteredEntities { + const wrapped = gyms.map((entity) => ({ + id: entity.gymId, + lat: entity.lat, + lon: entity.lon, + entity, + })) + const result = clusterEntities(wrapped, viewport.bounds, viewport.zoom, rules) + return { + points: result.points.map((wrapper) => wrapper.entity), + clusters: result.clusters, + limitHit: result.limitHit, + } +} + +/** + * Whether each category's `forcedLimit` engaged for the frame just built. + * + * This exists because a capped map and a genuinely empty area are pixel for + * pixel identical: the user has no way to tell "there is nothing here" from + * "there is too much here to draw and some of it has been merged away". The + * flag `clusterEntities` computes has to survive the trip out to something + * that can say so, which means `buildMapLayers` cannot return a bare array. + * + * Both are false when no `viewport` is given, since nothing was clustered and + * nothing was capped. + */ +export interface LimitHit { + pokemon: boolean + gyms: boolean +} + +export interface MapLayersResult { + /** + * The pokemon actually rendered after clustering, so a caller can rebuild + * just the countdown text on a tick instead of re-running the clustering. + * Building a Supercluster index over every subscribed entity once a second + * is what this exists to avoid. + */ + renderedPokemon: readonly PokemonEntity[] + + layers: Layer[] + limitHit: LimitHit +} + +const NOTHING_CAPPED: LimitHit = { pokemon: false, gyms: false } + +/** + * All layers this task adds, in the order deck.gl should draw them: gyms + * and pokemon icons first, pokemon labels last so text always sits on top + * of the markers it describes. Draw order between layers is independent of + * `interleaved`, which only controls where the whole deck.gl stack sits + * relative to MapLibre's own street-label layer. + * + * Without `viewport`, every entity renders individually - the original + * Task 4 behaviour, still what layers.test.ts exercises. With `viewport`, + * pokemon and gyms are each clustered and capped against their + * `forcedLimit` (clustering.ts; see clustering.test.ts for the bug this + * closes) before their icon/text layers are built, and any resulting + * cluster bubbles are added as their own layers when `getClusterIcon` is + * supplied. + * + * Returns the layers alongside a per-category `limitHit`, rather than a bare + * array, so a caller can tell the user the view is capped. See `LimitHit`. + */ +export function buildMapLayers({ + pokemon, + gyms, + getIconFor, + getGymIcon, + now, + root, + viewport, + clusterRules, + getClusterIcon, +}: BuildMapLayersOptions): MapLayersResult { + if (!viewport) { + return { + layers: [ + buildGymIconLayer(gyms, getGymIcon, root), + buildPokemonIconLayer(pokemon, getIconFor), + buildPokemonTextLayer(pokemon, now), + ], + limitHit: NOTHING_CAPPED, + renderedPokemon: pokemon, + } + } + + const pokemonResult = clusterPokemon( + pokemon, + viewport, + clusterRules?.pokemon ?? DEFAULT_POKEMON_CLUSTER_RULES, + ) + const gymResult = clusterGyms( + gyms, + viewport, + clusterRules?.gyms ?? DEFAULT_GYM_CLUSTER_RULES, + ) + + const layers: Layer[] = [ + buildGymIconLayer(gymResult.points, getGymIcon, root), + buildPokemonIconLayer(pokemonResult.points, getIconFor), + buildPokemonTextLayer(pokemonResult.points, now), + ] + + if (getClusterIcon) { + if (gymResult.clusters.length > 0) { + layers.push( + buildClusterIconLayer( + gymResult.clusters, + GYM_CLUSTER_ICON_LAYER_ID, + getClusterIcon, + root, + ), + buildClusterTextLayer( + gymResult.clusters, + GYM_CLUSTER_LABEL_LAYER_ID, + root, + ), + ) + } + if (pokemonResult.clusters.length > 0) { + layers.push( + buildClusterIconLayer( + pokemonResult.clusters, + POKEMON_CLUSTER_ICON_LAYER_ID, + getClusterIcon, + root, + ), + buildClusterTextLayer( + pokemonResult.clusters, + POKEMON_CLUSTER_LABEL_LAYER_ID, + root, + ), + ) + } + } + + return { + layers, + limitHit: { pokemon: pokemonResult.limitHit, gyms: gymResult.limitHit }, + renderedPokemon: pokemonResult.points, + } +} diff --git a/app/map/query.ts b/app/map/query.ts new file mode 100644 index 000000000..30c894487 --- /dev/null +++ b/app/map/query.ts @@ -0,0 +1,29 @@ +import type { MapEntity, MapQuery, MapSource, Unsubscribe } from './types' + +/** + * Reads a source once: takes the first delivered set and unsubscribes. + * + * A source may deliver that first set synchronously from inside + * `subscribe`, before it has returned the unsubscribe function, so the + * handler cannot always call it. The flag covers that case by + * unsubscribing once the function is in hand. + */ +export function queryOnce( + source: MapSource, + request: MapQuery, +): Promise { + return new Promise((resolve) => { + let settled = false + let unsubscribe: Unsubscribe | undefined + + const stop = source.subscribe(request, (entities) => { + if (settled) return + settled = true + resolve(entities) + unsubscribe?.() + }) + + unsubscribe = stop + if (settled) stop() + }) +} diff --git a/app/map/source.test.ts b/app/map/source.test.ts new file mode 100644 index 000000000..01e03d17b --- /dev/null +++ b/app/map/source.test.ts @@ -0,0 +1,90 @@ +import { expect, test } from 'bun:test' +import { queryOnce } from './query' +import { createFixtureSource } from './source' +import type { MapChangeHandler, MapEntity, MapSource } from './types' + +test('returns only entities inside the requested bounds', async () => { + const source = createFixtureSource() + const inside = await queryOnce(source, { + kind: 'pokemon', + bounds: { west: -0.1, south: 51.5, east: 0.1, north: 51.6 }, + zoom: 15, + }) + expect(inside.length).toBeGreaterThan(0) + for (const entity of inside) { + expect(entity.lon).toBeGreaterThanOrEqual(-0.1) + expect(entity.lon).toBeLessThanOrEqual(0.1) + expect(entity.lat).toBeGreaterThanOrEqual(51.5) + expect(entity.lat).toBeLessThanOrEqual(51.6) + } +}) + +test('produces enough pokemon to exercise the count problem', async () => { + const source = createFixtureSource() + const all = await queryOnce(source, { + kind: 'pokemon', + bounds: { west: -1, south: 51, east: 1, north: 52 }, + zoom: 12, + }) + expect(all.length).toBeGreaterThanOrEqual(3000) +}) + +test('subscribe delivers the current set and returns an unsubscribe', () => { + const source = createFixtureSource() + const deliveries: MapEntity[][] = [] + const unsubscribe = source.subscribe( + { + kind: 'gym', + bounds: { west: -1, south: 51, east: 1, north: 52 }, + zoom: 12, + }, + (entities) => deliveries.push(entities), + ) + + expect(deliveries).toHaveLength(1) + expect(deliveries[0]?.length).toBeGreaterThan(0) + expect(typeof unsubscribe).toBe('function') + expect(() => unsubscribe()).not.toThrow() +}) + +test('queryOnce stops listening after the first delivery', async () => { + let deliveries = 0 + let stopped = false + const source: MapSource = { + subscribe(_request, onChange: MapChangeHandler) { + deliveries += 1 + onChange([]) + return () => { + stopped = true + } + }, + } + + const result = await queryOnce(source, { + kind: 'pokemon', + bounds: { west: -1, south: 51, east: 1, north: 52 }, + zoom: 12, + }) + + expect(result).toEqual([]) + expect(deliveries).toBe(1) + expect(stopped).toBe(true) +}) + +test('spawnId is unique per entity while pokemonId repeats across species', async () => { + const source = createFixtureSource() + const all = await queryOnce(source, { + kind: 'pokemon', + bounds: { west: -1, south: 51, east: 1, north: 52 }, + zoom: 12, + }) + const pokemon = all.filter((entity) => entity.kind === 'pokemon') + + const spawnIds = new Set(pokemon.map((entity) => entity.spawnId)) + const speciesIds = new Set(pokemon.map((entity) => entity.pokemonId)) + + expect(spawnIds.size).toBe(pokemon.length) + // Anything caching by appearance must key on pokemonId: there are far + // fewer species than spawns, which is the whole point of the cache. + expect(speciesIds.size).toBeLessThan(pokemon.length / 2) +}) diff --git a/app/map/source.ts b/app/map/source.ts new file mode 100644 index 000000000..f0d09344c --- /dev/null +++ b/app/map/source.ts @@ -0,0 +1,46 @@ +import { getFixtureGyms, getFixturePokemon } from './fixtures' +import type { + Bounds, + MapChangeHandler, + MapEntity, + MapQuery, + MapSource, + Unsubscribe, +} from './types' + +function isInside(bounds: Bounds, lat: number, lon: number): boolean { + return ( + lat >= bounds.south && + lat <= bounds.north && + lon >= bounds.west && + lon <= bounds.east + ) +} + +function matching(request: MapQuery): MapEntity[] { + const entities = + request.kind === 'pokemon' ? getFixturePokemon() : getFixtureGyms() + return entities.filter((entity) => + isInside(request.bounds, entity.lat, entity.lon), + ) +} + +/** + * A MapSource backed by deterministic in-memory fixtures. Stands in for + * the real WebSocket-backed source a later session implements against + * the same interface. + * + * Fixtures never change, so `subscribe` delivers the matching set once + * and hands back an unsubscribe that has nothing to undo. That is the + * point: consumers are written against push semantics from the start, + * so the real transport has somewhere to deliver updates without any + * of them being rewritten. + */ +export function createFixtureSource(): MapSource { + return { + subscribe(request: MapQuery, onChange: MapChangeHandler): Unsubscribe { + onChange(matching(request)) + return () => undefined + }, + } +} diff --git a/app/map/supercluster.d.ts b/app/map/supercluster.d.ts new file mode 100644 index 000000000..870f83936 --- /dev/null +++ b/app/map/supercluster.d.ts @@ -0,0 +1,45 @@ +/** + * `supercluster` ships no types and there is no `@types/supercluster` in + * this project. This declares only the surface `clustering.ts` actually + * calls, typed to what supercluster's README documents, so the dependency + * does not force `any` through strict TypeScript. + */ +declare module 'supercluster' { + export interface SuperclusterOptions { + /** Cluster radius, in pixels. Default 40. */ + radius?: number + /** Tile extent, radius is relative to this. Default 512. */ + extent?: number + /** Min zoom to generate clusters at. Default 0. */ + minZoom?: number + /** Max zoom level at which clusters are generated. Above it, `getClusters` returns raw points. Default 16. */ + maxZoom?: number + /** Minimum points to form a cluster. Default 2. */ + minPoints?: number + } + + export interface SuperclusterPointFeature { + type: 'Feature' + id?: string | number + properties: Record + geometry: { type: 'Point'; coordinates: [number, number] } + } + + export interface SuperclusterClusterProperties { + cluster: true + cluster_id: number + point_count: number + point_count_abbreviated: string | number + } + + export type SuperclusterFeature = SuperclusterPointFeature + + export default class Supercluster { + constructor(options?: SuperclusterOptions) + load(points: SuperclusterPointFeature[]): this + getClusters( + bbox: [number, number, number, number], + zoom: number, + ): SuperclusterFeature[] + } +} diff --git a/app/map/types.ts b/app/map/types.ts new file mode 100644 index 000000000..d65ed593d --- /dev/null +++ b/app/map/types.ts @@ -0,0 +1,90 @@ +/** + * The map source interface. A later session implements this for real + * against a WebSocket transport; every task in this plan is written + * against fixtures behind the same shape. + */ + +export interface Bounds { + west: number + south: number + east: number + north: number +} + +/** What the camera currently frames: the area, and how far in it is. */ +export interface Viewport { + bounds: Bounds + zoom: number +} + +export type MapEntityKind = 'pokemon' | 'gym' + +export interface MapQuery { + kind: MapEntityKind + bounds: Bounds + zoom: number +} + +/** 0 unset, 1 male, 2 female, 3 genderless. */ +export type Gender = 0 | 1 | 2 | 3 + +/** 0 uncontested, 1 Mystic, 2 Valor, 3 Instinct. */ +export type Team = 0 | 1 | 2 | 3 + +/** + * Fields carried here are exactly what determines the icon (pokemonId, + * form, costume, gender, badge, background, weather), what's needed to + * place and expire the marker (lat, lon, expiresAt), and the optional + * stats rendered as marker text (iv, level, size). Nothing speculative: + * a field added later is cheap, one removed after something depends on + * it is not. + * + * `spawnId` identifies one encounter and changes every time the pokemon + * respawns, so it is useless as a cache key. `pokemonId` is the species + * and is what anything keyed on appearance must use. + */ +export interface PokemonEntity { + kind: 'pokemon' + spawnId: string + pokemonId: number + /** Left as open numbers: valid values vary per species. */ + form: number + costume: number + gender: Gender + badge?: number + background?: number + weather?: number + lat: number + lon: number + expiresAt: number + iv?: number + level?: number + size?: number +} + +export interface GymEntity { + kind: 'gym' + gymId: string + lat: number + lon: number + team: Team + inBattle: boolean +} + +export type MapEntity = PokemonEntity | GymEntity + +/** Called with the full current set, then again whenever it changes. */ +export type MapChangeHandler = (entities: MapEntity[]) => void + +/** Returned by subscribe; calling it stops delivery. */ +export type Unsubscribe = () => void + +/** + * Subscription is the only operation because the real transport pushes. + * A caller that wants a single answer uses `queryOnce` from ./query, + * which is that push stream read once, so there is one code path to + * implement and one to get wrong. + */ +export interface MapSource { + subscribe(request: MapQuery, onChange: MapChangeHandler): Unsubscribe +} diff --git a/app/map/useDismissOnEscape.test.tsx b/app/map/useDismissOnEscape.test.tsx new file mode 100644 index 000000000..d185c7861 --- /dev/null +++ b/app/map/useDismissOnEscape.test.tsx @@ -0,0 +1,54 @@ +import { afterAll, afterEach, beforeAll, expect, test } from 'bun:test' +import { cleanup, render } from '@testing-library/react' +import { setupDom, teardownDom } from '../test-setup' +import { useDismissOnEscape } from './useDismissOnEscape' + +beforeAll(setupDom) +afterAll(teardownDom) +afterEach(cleanup) + +function Harness({ + active, + onDismiss, +}: { + active: boolean + onDismiss: () => void +}) { + useDismissOnEscape(active, onDismiss) + return
harness
+} + +const pressEscape = () => { + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })) +} + +test('dismisses on escape while active', () => { + let count = 0 + render( (count += 1)} />) + pressEscape() + expect(count).toBe(1) +}) + +test('does nothing while inactive', () => { + let count = 0 + render( (count += 1)} />) + pressEscape() + expect(count).toBe(0) +}) + +test('ignores other keys', () => { + let count = 0 + render( (count += 1)} />) + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })) + expect(count).toBe(0) +}) + +test('stops listening once unmounted', () => { + let count = 0 + const { unmount } = render( (count += 1)} />) + unmount() + pressEscape() + // A listener left on document survives navigation and fires against a + // component that no longer exists, which is silent until it throws. + expect(count).toBe(0) +}) diff --git a/app/map/useDismissOnEscape.ts b/app/map/useDismissOnEscape.ts new file mode 100644 index 000000000..92f3ece2c --- /dev/null +++ b/app/map/useDismissOnEscape.ts @@ -0,0 +1,25 @@ +import { useEffect } from 'react' + +/** + * Closes something when Escape is pressed, while it is open. + * + * This exists as its own hook rather than an effect inside MapCanvas so that it + * can be tested. MapCanvas needs a real WebGL context to render at all, so + * anything living inside it is only ever asserted, never exercised. + * + * Radix's Popover would have provided this for free, but its positioning tracks + * an element's DOM rect through a ResizeObserver, and the map popup's anchor + * moves by an inline style rewritten on every camera frame, which that does not + * follow. Dismissal is independent of layout, so it does not get dropped along + * with the layout engine. + */ +export function useDismissOnEscape(active: boolean, onDismiss: () => void) { + useEffect(() => { + if (!active) return undefined + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') onDismiss() + } + document.addEventListener('keydown', onKeyDown) + return () => document.removeEventListener('keydown', onKeyDown) + }, [active, onDismiss]) +} diff --git a/app/map/useMapLibre.test.ts b/app/map/useMapLibre.test.ts new file mode 100644 index 000000000..44e1c8350 --- /dev/null +++ b/app/map/useMapLibre.test.ts @@ -0,0 +1,124 @@ +import { expect, test } from 'bun:test' +import type { PickingInfo } from '@deck.gl/core' +import { IconLayer } from '@deck.gl/layers' +import type { GymEntity, PokemonEntity } from './types' +import { + anchorFor, + buildOverlayProps, + pickedEntityFrom, + viewportFrom, +} from './useMapLibre' + +/* + * Mounting a real MapLibre map needs a WebGL context the test environment + * does not have, so `useMapLibre` itself is not exercised here. What is + * asserted instead is the one setting that determines whether markers + * render under or over MapLibre's street labels: `interleaved: true` on + * the props `MapboxOverlay` is constructed with. Getting it wrong renders + * without erroring, so this is the only automated guard against that + * regressing; whether the interleaving visually looks right still needs a + * browser (see task-4-report.md). + */ + +test('overlay props request interleaved rendering', () => { + const props = buildOverlayProps([]) + expect(props.interleaved).toBe(true) +}) + +test('overlay props carry whatever layers are passed through unchanged', () => { + const layer = new IconLayer({ + id: 'probe', + data: [], + getPosition: () => [0, 0], + getIcon: () => ({ id: 'x', url: 'u', width: 1, height: 1 }), + }) + const props = buildOverlayProps([layer]) + expect(props.layers).toEqual([layer]) +}) + +test('overlay props omit onClick when none is given, so deck.gl skips picking work on every click', () => { + const props = buildOverlayProps([]) + expect('onClick' in props).toBe(false) +}) + +test('overlay props carry the click handler through when one is given', () => { + const onClick = () => {} + const props = buildOverlayProps([], onClick) + expect(props.onClick).toBe(onClick) +}) + +/* + * Picking and reprojection themselves need a WebGL context and a mounted + * MapLibre map, which this environment does not have (see the file-level + * comment above). What is testable in isolation is the selection state + * transition - a picking result in, an entity or null out - and the pure + * coordinate lookup that decides what a popup anchors to. + */ + +const POKEMON: PokemonEntity = { + kind: 'pokemon', + spawnId: 'spawn-1', + pokemonId: 25, + form: 0, + costume: 0, + gender: 1, + lat: 51.5, + lon: -0.1, + expiresAt: 1_000, +} + +const GYM: GymEntity = { + kind: 'gym', + gymId: 'gym-1', + lat: 40.7, + lon: -74, + team: 2, + inBattle: false, +} + +function pickingInfoFor(object: PokemonEntity | GymEntity | undefined) { + return { object } as PickingInfo +} + +test('a picking hit on a pokemon selects that pokemon', () => { + expect(pickedEntityFrom(pickingInfoFor(POKEMON))).toEqual(POKEMON) +}) + +test('a picking hit on a gym selects that gym', () => { + expect(pickedEntityFrom(pickingInfoFor(GYM))).toEqual(GYM) +}) + +test('a picking miss (empty map, no object) clears the selection', () => { + expect(pickedEntityFrom(pickingInfoFor(undefined))).toBeNull() +}) + +test('the anchor for a selected entity is exactly its coordinate', () => { + expect(anchorFor(POKEMON)).toEqual({ lat: 51.5, lon: -0.1 }) +}) + +test('the anchor for no selection is null', () => { + expect(anchorFor(null)).toBeNull() +}) + +/* + * The bounds-to-Bounds mapping is four accessors read in an order that is + * easy to get wrong and renders without erroring when it is: a swapped west + * and east gives a bbox supercluster quietly returns nothing for, which + * looks exactly like an empty map. Values here are deliberately asymmetric + * so a swap cannot pass. + */ +test('viewportFrom reads each edge of the camera bounds into its own field', () => { + const viewport = viewportFrom({ + getBounds: () => ({ + getWest: () => -3, + getSouth: () => 50, + getEast: () => 1, + getNorth: () => 54, + }), + getZoom: () => 11.4, + }) + expect(viewport).toEqual({ + bounds: { west: -3, south: 50, east: 1, north: 54 }, + zoom: 11.4, + }) +}) diff --git a/app/map/useMapLibre.ts b/app/map/useMapLibre.ts new file mode 100644 index 000000000..809347068 --- /dev/null +++ b/app/map/useMapLibre.ts @@ -0,0 +1,281 @@ +import type { Layer, PickingInfo } from '@deck.gl/core' +import type { MapboxOverlayProps } from '@deck.gl/mapbox' +import { MapboxOverlay } from '@deck.gl/mapbox' +import { Map as MapLibreMap, NavigationControl } from 'maplibre-gl' +import { type RefObject, useEffect, useRef, useState } from 'react' +import { resolveBasemapStyle } from './basemap' +import type { MapEntity, Viewport } from './types' + +export interface Camera { + lat: number + lon: number + zoom: number +} + +/** A point to keep a popup anchored to, in map coordinates. */ +export interface AnchorCoordinate { + lat: number + lon: number +} + +/** Where `AnchorCoordinate` currently projects to, in container pixels. */ +export interface ScreenPosition { + x: number + y: number +} + +/** + * Reads the entity a deck.gl click landed on, if any. Pulled out as a + * plain function so the selection transition (miss clears, hit replaces) + * is assertable without a WebGL context - `PickingInfo` is a plain object + * this can be constructed by hand in a test; nothing here touches the map. + */ +export function pickedEntityFrom(info: PickingInfo): MapEntity | null { + return (info.object as MapEntity | undefined) ?? null +} + +/** The coordinate a selected entity's popup should stay anchored to. */ +export function anchorFor(entity: MapEntity | null): AnchorCoordinate | null { + return entity ? { lat: entity.lat, lon: entity.lon } : null +} + +/** + * Reads what a map currently frames. Typed against the accessors rather than + * against `MapLibreMap` so a test can hand it a plain object: mounting a real + * map needs a WebGL context this environment does not have, and the + * bounds-to-`Bounds` mapping is exactly the kind of thing that is wrong by a + * swapped field and renders without erroring. + */ +export function viewportFrom(map: { + getBounds(): { + getWest(): number + getSouth(): number + getEast(): number + getNorth(): number + } + getZoom(): number +}): Viewport { + const bounds = map.getBounds() + return { + bounds: { + west: bounds.getWest(), + south: bounds.getSouth(), + east: bounds.getEast(), + north: bounds.getNorth(), + }, + zoom: map.getZoom(), + } +} + +export interface UseMapLibreOptions { + /** Camera to center on the moment the map is created. */ + initialCamera: Camera + /** + * Fired on `moveend`, i.e. once per pan/zoom gesture rather than once per + * animation frame. The caller (`MapPage`) uses this to sync the URL + * without pushing a history entry per frame; a listener that instead + * pushed on `move` would make the back button useless after one pan. + */ + onCameraChange?: (camera: Camera) => void + /** + * deck.gl layers to render on top of the basemap, attached through a + * `MapboxOverlay` created with `interleaved: true`. Interleaved is not + * cosmetic: it is what places these layers beneath MapLibre's own street + * label layer instead of painting over it, and it is the reason this + * hook builds on MapLibre rather than deck.gl replacing it outright. + * Updates flow through `overlay.setProps` on every change of this array, + * independent of the mount effect below, so panning/zooming the camera + * never tears down and rebuilds the overlay. + */ + layers?: Layer[] + /** + * Fired on every deck.gl click, picked or not (`info.object` is + * `undefined` on a miss). The caller owns selection state; this hook + * only reports what was under the cursor. + */ + onPick?: (info: PickingInfo) => void + /** + * The coordinate the caller wants a popup anchored to, or `null` when + * nothing is selected. Reprojected to container pixels on every camera + * frame; see `screenPosition` below and task-5-report.md for why this + * hook reprojects per frame rather than handing the coordinate to a + * MapLibre `Marker`. + */ + anchor?: AnchorCoordinate | null + /** + * Fired with what the camera frames, once when the map mounts and again on + * every `moveend`. The caller feeds this straight back in as the area + * `buildMapLayers` clusters and caps against; without it there is no + * viewport, the clustering path never runs, and the whole of task 6 sits + * inert behind green tests. + * + * This is a callback rather than a returned value because the caller's + * `layers` are derived from it and are themselves an input to this hook. + * Returning it would close that loop inside one render pass; handing the + * caller the value to store breaks it, the same shape `onCameraChange` + * already uses. + * + * Fires on `moveend`, not on `move`. Clustering a viewport is real work and + * doing it once per animation frame through a pan would cost far more than + * it buys: markers already move with the map during a gesture, so all that + * is stale mid-gesture is the clustering granularity, and it settles the + * moment the gesture does. + */ + onViewportChange?: (viewport: Viewport) => void +} + +export interface UseMapLibreResult { + /** Attach to the element MapLibre should mount into. */ + containerRef: RefObject + /** + * `anchor` projected through the current camera, in pixels relative to + * `containerRef`'s element. `null` when there is no anchor, or when the + * map has not mounted yet. + */ + screenPosition: ScreenPosition | null +} + +/** + * Builds the props `MapboxOverlay` is constructed with. Pulled out as a + * plain function, rather than inlined in the mount effect, so its one + * load-bearing setting is assertable without a real WebGL context: + * `interleaved: true` is what places these layers beneath MapLibre's own + * street label layer instead of over it, and getting it wrong renders + * without erroring, so a test asserting the props object is the only + * automated guard against that regressing. Actual interleaving - markers + * visually sitting under labels - still needs a browser; see + * task-4-report.md. + */ +export function buildOverlayProps( + layers: Layer[], + onClick?: (info: PickingInfo) => void, +): MapboxOverlayProps { + return { interleaved: true, layers, ...(onClick ? { onClick } : {}) } +} + +/** + * Owns one MapLibre instance for the lifetime of the component that calls + * this hook: creates it against `containerRef`'s element on mount, tears it + * down on unmount, and re-centers it if `initialCamera` changes identity + * (the deep-link redirect case, where the map is already mounted and a new + * `/@/:lat/:lon/:zoom` link arrives at the same `/map` route). + */ +export function useMapLibre({ + initialCamera, + onCameraChange, + layers, + onPick, + anchor, + onViewportChange, +}: UseMapLibreOptions): UseMapLibreResult { + const containerRef = useRef(null) + const mapRef = useRef(null) + const overlayRef = useRef(null) + const onCameraChangeRef = useRef(onCameraChange) + onCameraChangeRef.current = onCameraChange + const onPickRef = useRef(onPick) + onPickRef.current = onPick + const onViewportChangeRef = useRef(onViewportChange) + onViewportChangeRef.current = onViewportChange + // Read by the mount effect below, once, so the overlay starts with + // whatever layers are already available instead of an empty frame while + // waiting for the layers-sync effect to run. + const initialLayersRef = useRef(layers) + + const [screenPosition, setScreenPosition] = useState( + null, + ) + // Read by the 'move' handler on every frame, so the anchor coordinate is + // always current without re-subscribing that handler on every render. + const anchorRef = useRef(anchor) + anchorRef.current = anchor + + useEffect(() => { + const container = containerRef.current + if (!container) return undefined + + const map = new MapLibreMap({ + container, + style: resolveBasemapStyle(), + center: [initialCamera.lon, initialCamera.lat], + zoom: initialCamera.zoom, + attributionControl: { compact: true }, + }) + map.addControl(new NavigationControl(), 'top-right') + + const overlay = new MapboxOverlay( + buildOverlayProps(initialLayersRef.current ?? [], (info) => + onPickRef.current?.(info), + ), + ) + map.addControl(overlay) + overlayRef.current = overlay + + const handleMoveEnd = () => { + onViewportChangeRef.current?.(viewportFrom(map)) + const center = map.getCenter() + onCameraChangeRef.current?.({ + lat: center.lat, + lon: center.lng, + zoom: map.getZoom(), + }) + } + map.on('moveend', handleMoveEnd) + + // Reprojects the popup's anchor coordinate on every camera frame, not + // just on 'moveend'. A popup positioned only at gesture-end would + // visibly detach from its entity and snap back into place once the + // pan/zoom settled; 'move' fires continuously through the gesture and + // the whole animation, which is what keeps it glued to the coordinate. + // See task-5-report.md for why this reprojects instead of riding a + // MapLibre `Marker`. + const handleMove = () => { + const current = anchorRef.current + if (!current) { + setScreenPosition(null) + return + } + const point = map.project([current.lon, current.lat]) + setScreenPosition({ x: point.x, y: point.y }) + } + map.on('move', handleMove) + + // Reported here rather than waiting for the first gesture, or the map + // would spend its whole first view unclustered and uncapped. + onViewportChangeRef.current?.(viewportFrom(map)) + + mapRef.current = map + return () => { + map.off('moveend', handleMoveEnd) + map.off('move', handleMove) + // map.remove() tears down every control it holds, this overlay + // included, so there is nothing extra to release here. + map.remove() + overlayRef.current = null + mapRef.current = null + } + // Camera is only read once, to seed the map on creation; every camera + // change after that flows through moveend and the caller's own state, + // not back into this effect. Re-running this effect on every camera + // change would tear down and recreate the whole map on every pan. + }, []) + + useEffect(() => { + overlayRef.current?.setProps({ layers: layers ?? [] }) + }, [layers]) + + // Projects immediately when the anchor itself changes (a new selection, + // or a selection clearing), rather than waiting for the next 'move' + // event, which may not come until the user next pans. + useEffect(() => { + const map = mapRef.current + if (!anchor || !map) { + setScreenPosition(null) + return + } + const point = map.project([anchor.lon, anchor.lat]) + setScreenPosition({ x: point.x, y: point.y }) + }, [anchor]) + + return { containerRef, screenPosition } +} diff --git a/app/map/useWebglContextRecovery.test.tsx b/app/map/useWebglContextRecovery.test.tsx new file mode 100644 index 000000000..768fa518d --- /dev/null +++ b/app/map/useWebglContextRecovery.test.tsx @@ -0,0 +1,105 @@ +import { afterAll, afterEach, beforeAll, expect, test } from 'bun:test' +import { act, cleanup, render } from '@testing-library/react' +import { useRef } from 'react' +import { setupDom, teardownDom } from '../test-setup' +import { + type UseWebglContextRecoveryResult, + useWebglContextRecovery, +} from './useWebglContextRecovery' + +beforeAll(setupDom) +afterAll(teardownDom) +afterEach(cleanup) + +/* + * The real canvas comes from MapLibre, created inside the container this + * hook watches. The harness stands one up by hand - a plain is + * enough, since this hook only ever touches it as a DOM EventTarget and + * never reaches for a WebGL context. See task-6-7 notes: this is the + * state machine, not the GPU. + */ +function Harness({ + onRestore, + onResult, +}: { + onRestore: () => void + onResult: (result: UseWebglContextRecoveryResult) => void +}) { + const containerRef = useRef(null) + const result = useWebglContextRecovery(containerRef, { onRestore }) + onResult(result) + return ( +
+ +
+ ) +} + +function dispatchOn(canvas: HTMLCanvasElement, type: string): Event { + const event = new Event(type, { cancelable: true }) + act(() => { + canvas.dispatchEvent(event) + }) + return event +} + +test('flips restoring true on context loss and prevents the default', () => { + let latest: UseWebglContextRecoveryResult | undefined + const { container } = render( + undefined} onResult={(r) => (latest = r)} />, + ) + const canvas = container.querySelector('canvas') as HTMLCanvasElement + + const event = dispatchOn(canvas, 'webglcontextlost') + + expect(event.defaultPrevented).toBe(true) + expect(latest?.restoring).toBe(true) +}) + +test('clears restoring and calls onRestore when the context comes back', () => { + let latest: UseWebglContextRecoveryResult | undefined + let restoreCount = 0 + const { container } = render( + (restoreCount += 1)} + onResult={(r) => (latest = r)} + />, + ) + const canvas = container.querySelector('canvas') as HTMLCanvasElement + + dispatchOn(canvas, 'webglcontextlost') + expect(latest?.restoring).toBe(true) + + dispatchOn(canvas, 'webglcontextrestored') + + expect(restoreCount).toBe(1) + expect(latest?.restoring).toBe(false) +}) + +test('onRestore never fires without a preceding loss', () => { + let restoreCount = 0 + render( + (restoreCount += 1)} + onResult={() => undefined} + />, + ) + expect(restoreCount).toBe(0) +}) + +test('stops listening once unmounted', () => { + let restoreCount = 0 + const { container, unmount } = render( + (restoreCount += 1)} + onResult={() => undefined} + />, + ) + const canvas = container.querySelector('canvas') as HTMLCanvasElement + unmount() + + dispatchOn(canvas, 'webglcontextlost') + dispatchOn(canvas, 'webglcontextrestored') + + expect(restoreCount).toBe(0) +}) diff --git a/app/map/useWebglContextRecovery.ts b/app/map/useWebglContextRecovery.ts new file mode 100644 index 000000000..51beba61c --- /dev/null +++ b/app/map/useWebglContextRecovery.ts @@ -0,0 +1,72 @@ +import { type RefObject, useEffect, useRef, useState } from 'react' + +export interface UseWebglContextRecoveryOptions { + /** + * Called once `webglcontextrestored` fires, before `restoring` clears. + * This is where a caller re-warms whatever died with the old context - + * MapCanvas clears the icon atlas and forces a fresh layer build here, + * since the atlas's composited icons are gone along with the GPU + * resources that held them. + */ + onRestore: () => void +} + +export interface UseWebglContextRecoveryResult { + /** True from `webglcontextlost` until `webglcontextrestored` fires. */ + restoring: boolean +} + +/** + * Watches the canvas inside `containerRef` for WebGL context loss and + * restoration, and tracks whether the map is currently in the gap between + * them. + * + * `webglcontextlost` fires with the event's default action being "let the + * context die for good" - calling `preventDefault()` on it is what tells + * the browser recovery is wanted at all. Skipping that line is the classic + * mistake here: the event still fires, `restoring` would still flip true, + * and nothing would look wrong until `webglcontextrestored` never comes + * and the map stays blank forever. This hook exists as its own module, + * separate from MapCanvas, specifically so that line has one place to + * live and one test that can fail if it goes missing. + * + * The canvas itself is looked up from `containerRef`'s DOM subtree rather + * than threaded through as a prop, because `useMapLibre` owns the + * MapLibre instance that creates it and does not (and should not) expose + * the raw canvas element - `containerRef` is already public on its + * result, and MapLibre places its canvas inside that container + * synchronously when it mounts, before this hook's own effect runs. + */ +export function useWebglContextRecovery( + containerRef: RefObject, + { onRestore }: UseWebglContextRecoveryOptions, +): UseWebglContextRecoveryResult { + const [restoring, setRestoring] = useState(false) + const onRestoreRef = useRef(onRestore) + onRestoreRef.current = onRestore + + useEffect(() => { + const canvas = containerRef.current?.querySelector('canvas') ?? null + if (!canvas) return undefined + + const handleContextLost = (event: Event) => { + // Without this, the browser treats the loss as permanent and + // webglcontextrestored never fires. + event.preventDefault() + setRestoring(true) + } + const handleContextRestored = () => { + onRestoreRef.current() + setRestoring(false) + } + + canvas.addEventListener('webglcontextlost', handleContextLost) + canvas.addEventListener('webglcontextrestored', handleContextRestored) + return () => { + canvas.removeEventListener('webglcontextlost', handleContextLost) + canvas.removeEventListener('webglcontextrestored', handleContextRestored) + } + }, [containerRef]) + + return { restoring } +} diff --git a/app/pages/DeepLink.test.tsx b/app/pages/DeepLink.test.tsx new file mode 100644 index 000000000..80c3d149e --- /dev/null +++ b/app/pages/DeepLink.test.tsx @@ -0,0 +1,43 @@ +import { afterAll, afterEach, beforeAll, expect, test } from 'bun:test' +import { cleanup, render } from '@testing-library/react' +import { MemoryRouter, Route, Routes, useLocation } from 'react-router' +import { setupDom, teardownDom } from '../test-setup' +import { DeepLink } from './DeepLink' + +beforeAll(setupDom) +afterAll(teardownDom) +afterEach(cleanup) + +function LocationProbe() { + const location = useLocation() + return ( + + {location.pathname} + {location.search} + + ) +} + +function renderAt(initialEntry: string) { + return render( + + + } /> + } /> + } /> + + , + ) +} + +test('a 1.0 lat/lon/zoom deep link redirects to /map with matching query params', () => { + const { container } = renderAt('/@/40.7/-74.0/12') + const location = container.querySelector('[data-testid="location"]') + expect(location?.textContent).toBe('/map?lat=40.7&lon=-74.0&zoom=12') +}) + +test('a 1.0 lat/lon deep link without zoom redirects without a zoom param', () => { + const { container } = renderAt('/@/40.7/-74.0') + const location = container.querySelector('[data-testid="location"]') + expect(location?.textContent).toBe('/map?lat=40.7&lon=-74.0') +}) diff --git a/app/pages/DeepLink.tsx b/app/pages/DeepLink.tsx new file mode 100644 index 000000000..934948229 --- /dev/null +++ b/app/pages/DeepLink.tsx @@ -0,0 +1,25 @@ +import { Navigate, useParams } from 'react-router' + +/** + * 1.0 deep links are `/@/:lat/:lon` or `/@/:lat/:lon/:zoom`, and are in the + * wild (bookmarks, shared links). The 2.0 route table owns `/map`, so this + * component's only job is translating those params into `/map`'s own + * camera query params and handing off with a redirect. It never mounts + * MapLibre itself, and is registered directly rather than through + * `lazy()`: it has no heavy dependency to defer, and going through `/map` + * is what actually loads the map lazily, on the same terms as a visitor + * who typed `/map` directly. + * + * Missing or non-numeric params still redirect, just without a camera, so + * `/map` falls back to `MapPage`'s own default rather than this route + * needing to duplicate that fallback. + */ +export function DeepLink() { + const { lat, lon, zoom } = useParams() + const params = new URLSearchParams() + if (lat) params.set('lat', lat) + if (lon) params.set('lon', lon) + if (zoom) params.set('zoom', zoom) + const search = params.toString() + return +} diff --git a/app/pages/MapPage.tsx b/app/pages/MapPage.tsx index 70b236c54..3dc3ee3aa 100644 --- a/app/pages/MapPage.tsx +++ b/app/pages/MapPage.tsx @@ -1,10 +1,57 @@ +import { MapCanvas } from '@app/map/MapCanvas' +import type { Camera } from '@app/map/useMapLibre' +import { useCallback, useMemo } from 'react' +import { useSearchParams } from 'react-router' + +/** + * Where the 1.0 map opened by default: roughly the middle of the North + * Atlantic, a deliberately uninteresting fallback used only when the URL + * carries no camera at all. + */ +const DEFAULT_CAMERA: Camera = { lat: 0, lon: 0, zoom: 2 } + +function parseCamera(params: URLSearchParams): Camera { + const lat = Number(params.get('lat')) + const lon = Number(params.get('lon')) + const zoom = Number(params.get('zoom')) + return { + lat: Number.isFinite(lat) && params.has('lat') ? lat : DEFAULT_CAMERA.lat, + lon: Number.isFinite(lon) && params.has('lon') ? lon : DEFAULT_CAMERA.lon, + zoom: + Number.isFinite(zoom) && params.has('zoom') ? zoom : DEFAULT_CAMERA.zoom, + } +} + +/** + * `lat`/`lon`/`zoom` query params are the camera's URL representation. + * `useMapLibre` only reads `initialCamera` once (see its own comment), so + * changes made here after mount do not fight the map; they exist so a + * refresh, share, or back navigation lands on the same view, and are + * written with `replace: true` so panning does not grow history one entry + * per gesture. + */ export function MapPage() { + const [searchParams, setSearchParams] = useSearchParams() + const initialCamera = useMemo(() => parseCamera(searchParams), []) + + const handleCameraChange = useCallback( + (camera: Camera) => { + setSearchParams( + { + lat: camera.lat.toFixed(5), + lon: camera.lon.toFixed(5), + zoom: camera.zoom.toFixed(2), + }, + { replace: true }, + ) + }, + [setSearchParams], + ) + return ( -
-

Map

-

- The map arrives in a later plan. -

-
+ ) } diff --git a/app/routes.test.tsx b/app/routes.test.tsx index cad833d6a..525e9aa8f 100644 --- a/app/routes.test.tsx +++ b/app/routes.test.tsx @@ -7,6 +7,8 @@ test('every spec route is present exactly once', () => { [ '*', '/', + '/@/:lat/:lon', + '/@/:lat/:lon/:zoom', '/alerts', '/filters', '/locales', @@ -17,8 +19,28 @@ test('every spec route is present exactly once', () => { ) }) -test('every route element is lazy so it becomes its own chunk', () => { +/* + * `/@/:lat/:lon(/:zoom)` are 1.0's deep links, kept for links already in + * the wild. They redirect straight into `/map` and never mount MapLibre + * themselves, so they carry no heavy dependency worth deferring; asserting + * `lazy` on them would only prove they import `react-router`'s own + * `Navigate`, which is already in every other chunk. + */ +const DEEP_LINK_PATHS = new Set(['/@/:lat/:lon', '/@/:lat/:lon/:zoom']) + +test('every route that is not a deep-link redirect is lazy so it becomes its own chunk', () => { for (const route of ROUTES) { + if (typeof route.path === 'string' && DEEP_LINK_PATHS.has(route.path)) { + continue + } expect(typeof route.lazy).toBe('function') } }) + +test('deep-link routes redirect through a plain Component, not lazy', () => { + for (const route of ROUTES) { + if (typeof route.path === 'string' && DEEP_LINK_PATHS.has(route.path)) { + expect(typeof route.Component).toBe('function') + } + } +}) diff --git a/app/routes.tsx b/app/routes.tsx index ea495850b..cecab2368 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -1,5 +1,6 @@ import type { RouteObject } from 'react-router' import { Shell } from './layout/Shell' +import { DeepLink } from './pages/DeepLink' /* * Every route is lazy so the bundler gives each one its own chunk. The map @@ -17,6 +18,21 @@ export const ROUTES: RouteObject[] = [ Component: (await import('./pages/MapPage')).MapPage, }), }, + /* + * 1.0's deep links, `/@/:lat/:lon` and `/@/:lat/:lon/:zoom`, are in the + * wild. `DeepLink` is registered directly, not through `lazy()`: it has + * no heavy dependency of its own, it only redirects into `/map`, and + * that redirect is what actually triggers the lazy MapLibre load, on + * the same terms as a visitor who navigated to `/map` directly. + */ + { + path: '/@/:lat/:lon', + Component: DeepLink, + }, + { + path: '/@/:lat/:lon/:zoom', + Component: DeepLink, + }, { path: '/filters', lazy: async () => ({ diff --git a/app/tokens/contrast.test.ts b/app/tokens/contrast.test.ts index 827963a62..a4196c12e 100644 --- a/app/tokens/contrast.test.ts +++ b/app/tokens/contrast.test.ts @@ -113,3 +113,47 @@ for (const theme of ['light', 'dark'] as const) { } }) } + +/* + * Cluster bubbles. The count is drawn straight onto the fill, so the pair + * that has to hold is the label against each of the three buckets. Nothing + * else in this repo can see a colour, and these were hardcoded literals in + * the layer until they moved into the palette, which is exactly the state in + * which a contrast failure ships unnoticed. + * + * Both themes assert the same pairs because the tokens do not vary by theme: + * a bubble sits on map tiles, not on the page background. The test still runs + * per theme so a future theme override cannot slip past it. + */ +const CLUSTER_FILLS = [ + '--color-cluster-small', + '--color-cluster-medium', + '--color-cluster-large', +] + +for (const theme of ['light', 'dark'] as const) { + test(`${theme}: the cluster count clears 4.5:1 on every bubble`, () => { + for (const fill of CLUSTER_FILLS) { + const ratio = contrast( + hex(theme, '--color-cluster-label'), + hex(theme, fill), + ) + expect(`${fill}: ${ratio.toFixed(2)}`).toBe( + `${fill}: ${Math.max(ratio, 4.5).toFixed(2)}`, + ) + } + }) + + test(`${theme}: the three cluster buckets stay apart for dichromats`, () => { + for (let i = 0; i < CLUSTER_FILLS.length; i += 1) { + for (let j = i + 1; j < CLUSTER_FILLS.length; j += 1) { + const a = CLUSTER_FILLS[i] as string + const b = CLUSTER_FILLS[j] as string + const apart = separation(hex(theme, a), hex(theme, b)) + expect(`${a} vs ${b}: ${apart.toFixed(1)}`).toBe( + `${a} vs ${b}: ${Math.max(apart, 7).toFixed(1)}`, + ) + } + } + }) +} diff --git a/app/tokens/data-palette.css b/app/tokens/data-palette.css index 2da241a50..cee232782 100644 --- a/app/tokens/data-palette.css +++ b/app/tokens/data-palette.css @@ -57,6 +57,30 @@ --color-iv-mid: #ed6c02; /* < 82%, warning.main */ --color-iv-mid-high: #2ab5f6; /* < 100%, info.main (1.0 override) */ --color-iv-perfect: #2e7d32; /* = 100%, success.main */ + + /* + * Cluster bubble colours. Source: src/assets/css/main.css lines 623-644, + * the .marker-cluster-{small,medium,large} div fills, keyed by the same + * count thresholds src/pages/map/components/Clustering.jsx line 29 uses. + * A bubble covering a few dozen reads very differently from one covering a + * thousand, and colour is the only signal of that at a glance. + * + * The map engine paints these at higher opacity than 1.0's 0.6, which is + * the one thing here not carried across: the opacity is the bubble's + * material rather than its meaning, so it lives with the layer and not in + * this file. + * + * One set for both themes, like the team colours above and unlike the + * chart series below, because a bubble is painted on map tiles rather than + * on the page background and the theme does not change what sits behind + * it. The count uses --color-cluster-label and clears 4.5:1 on all three + * fills, which is therefore true in both themes by construction (verified + * in contrast.test.ts). + */ + --color-cluster-small: #6ecc39; /* < 100 entities */ + --color-cluster-medium: #f0c20c; /* < 1000 */ + --color-cluster-large: #f18017; /* 1000 and up */ + --color-cluster-label: #111827; } /* diff --git a/bun.lock b/bun.lock index dd14f619b..f2e1c2753 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,9 @@ "@apollo/server": "5.5.1", "@as-integrations/express5": "1.1.2", "@base-ui/react": "1.7.0", + "@deck.gl/core": "9.3.10", + "@deck.gl/layers": "9.3.10", + "@deck.gl/mapbox": "9.3.10", "@emotion/react": "11.14.0", "@emotion/styled": "11.14.1", "@fontsource-variable/fredoka": "5.3.0", @@ -45,6 +48,7 @@ "cors": "^2.8.6", "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", + "deck.gl": "9.3.10", "discord.js": "^14.26.4", "dlv": "^1.1.3", "dotenv": "^16.3.1", @@ -70,6 +74,7 @@ "lodash": "^4.18.1", "long": "^4.0.0", "lucide-react": "1.33.0", + "maplibre-gl": "6.5.0", "moment-timezone": "^0.6.2", "mysql2": "3.11.0", "node-cache": "^5.1.2", @@ -222,6 +227,8 @@ "uuid": "11.1.1", }, "packages": { + "@amcharts/amcharts5": ["@amcharts/amcharts5@5.14.4", "", { "dependencies": { "@types/d3": "^7.0.0", "@types/d3-chord": "^3.0.0", "@types/d3-hierarchy": "3.1.1", "@types/d3-sankey": "^0.11.1", "@types/d3-shape": "^3.0.0", "@types/geojson": "^7946.0.8", "@types/polylabel": "^1.0.5", "@types/svg-arc-to-cubic-bezier": "^3.2.0", "d3": "^7.0.0", "d3-chord": "^3.0.0", "d3-force": "^3.0.0", "d3-geo": "^3.0.0", "d3-hierarchy": "^3.0.0", "d3-sankey": "^0.12.3", "d3-selection": "^3.0.0", "d3-shape": "^3.0.0", "d3-transition": "^3.0.0", "d3-voronoi-treemap": "^1.1.2", "flatpickr": "^4.6.13", "markerjs2": "^2.29.4", "pdfmake": "^0.2.2", "polylabel": "^1.1.0", "seedrandom": "^3.0.5", "svg-arc-to-cubic-bezier": "^3.2.0", "tslib": "^2.2.0" } }, "sha512-Tl7wQLWvsvyWVtlCIm1yhZtJviSDYjuNTnlUkO0D49GyByoK0nb9fr0DK4rUw4DVgyLcySxWBsb2lzTJm5Rd9Q=="], + "@apollo/cache-control-types": ["@apollo/cache-control-types@1.0.3", "", { "peerDependencies": { "graphql": "14.x || 15.x || 16.x" } }, "sha512-F17/vCp7QVwom9eG7ToauIKdAxpSoadsJnqIfyryLFSkLSOEqu+eC5Z3N8OXcUVStuOMcNHlyraRsA6rRICu4g=="], "@apollo/client": ["@apollo/client@3.14.1", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@wry/caches": "^1.0.0", "@wry/equality": "^0.5.6", "@wry/trie": "^0.5.0", "graphql-tag": "^2.12.6", "hoist-non-react-statics": "^3.3.2", "optimism": "^0.18.0", "prop-types": "^15.7.2", "rehackt": "^0.1.0", "symbol-observable": "^4.0.0", "ts-invariant": "^0.10.3", "tslib": "^2.3.0", "zen-observable-ts": "^1.2.5" }, "peerDependencies": { "graphql": "^15.0.0 || ^16.0.0", "graphql-ws": "^5.5.5 || ^6.0.3", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" }, "optionalPeers": ["graphql-ws", "react", "react-dom", "subscriptions-transport-ws"] }, "sha512-SgGX6E23JsZhUdG2anxiyHvEvvN6CUaI4ZfMsndZFeuHPXL3H0IsaiNAhLITSISbeyeYd+CBd9oERXQDdjXWZw=="], @@ -258,6 +265,12 @@ "@apollo/utils.withrequired": ["@apollo/utils.withrequired@3.0.0", "", {}, "sha512-aaxeavfJ+RHboh7c2ofO5HHtQobGX4AgUujXP4CXpREHp9fQ9jPi6K9T1jrAKe7HIipoP0OJ1gd6JamSkFIpvA=="], + "@arcgis/core": ["@arcgis/core@4.34.8", "", { "dependencies": { "@amcharts/amcharts5": "~5.14.1", "@arcgis/toolkit": "^4.34.0", "@esri/arcgis-html-sanitizer": "~4.1.0", "@esri/calcite-components": "^3.3.2", "@vaadin/grid": "~24.9.1", "@zip.js/zip.js": "~2.8.7", "luxon": "~3.7.2", "marked": "~16.3.0", "tslib": "^2.8.1" } }, "sha512-UrEBTjXpSA9fhmmnAENBzz9GG81xALTezQFMXUs2iMB+tiOckmJyBbhATI/W4lIQyUfNEK7Zm/46EP2PhDga/A=="], + + "@arcgis/lumina": ["@arcgis/lumina@4.34.9", "", { "dependencies": { "@arcgis/toolkit": "~4.34.9", "csstype": "^3.1.3", "tslib": "^2.8.1" }, "peerDependencies": { "@lit/context": "^1.1.5", "lit": "^3.3.0" }, "optionalPeers": ["@lit/context"] }, "sha512-efqO+SwR+1IYf29AATh1l2FUeypRyRINTBNkaJY+KkaFe+8gqSJ45qOmputhyzF5WTRDb7WhOYgnChjp6VYPpA=="], + + "@arcgis/toolkit": ["@arcgis/toolkit@4.34.9", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-wFST+eVnCwmg9NyICVyn9bsBnR+TlWklsGqG3L7xqSTgfXo6TuCThE7wtTb8xWxsTBkGvImqMUgpgLuwQuTQ1g=="], + "@as-integrations/express5": ["@as-integrations/express5@1.1.2", "", { "peerDependencies": { "@apollo/server": "^4.0.0 || ^5.0.0", "express": "^5.0.0" } }, "sha512-BxfwtcWNf2CELDkuPQxi5Zl3WqY/dQVJYafeCBOGoFQjv5M0fjhxmAFZ9vKx/5YKKNeok4UY6PkFbHzmQrdxIA=="], "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], @@ -340,6 +353,8 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.10", "", { "os": "win32", "cpu": "x64" }, "sha512-M+2dgBsl3lXRiTfgPVc2p3anS4Tocojke4rzFLScZ2Y/wmF+36dRb1iHCLiyGqOzQGyTplZH1HnEYviiAqi3nA=="], + "@carto/api-client": ["@carto/api-client@0.5.33", "", { "dependencies": { "@loaders.gl/schema": "^4.3.3", "@types/geojson": "^7946.0.16", "d3-format": "^3.1.2", "d3-scale": "^4.0.2", "h3-js": "^4.1.0", "jsep": "^1.4.0", "quadbin": "^0.4.1-alpha.0" } }, "sha512-WXVJUT9ljMTo1C+o9C6Dz5smsUUHyIXXJPJWiRoneuc41VBSdMWDsUp93g3Q7Wu8gI+2uWM4Xwby5jzjy92KUg=="], + "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], "@commitlint/cli": ["@commitlint/cli@19.8.1", "", { "dependencies": { "@commitlint/format": "^19.8.1", "@commitlint/lint": "^19.8.1", "@commitlint/load": "^19.8.1", "@commitlint/read": "^19.8.1", "@commitlint/types": "^19.8.1", "tinyexec": "^1.0.0", "yargs": "^17.0.0" }, "bin": { "commitlint": "./cli.js" } }, "sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA=="], @@ -378,6 +393,32 @@ "@date-fns/tz": ["@date-fns/tz@1.5.0", "", {}, "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg=="], + "@deck.gl/aggregation-layers": ["@deck.gl/aggregation-layers@9.3.10", "", { "dependencies": { "@luma.gl/shadertools": "^9.3.5", "@math.gl/core": "^4.1.0", "@math.gl/web-mercator": "^4.1.0", "d3-hexbin": "^0.2.1" }, "peerDependencies": { "@deck.gl/core": "~9.3.0", "@deck.gl/layers": "~9.3.0", "@luma.gl/core": "~9.3.5", "@luma.gl/engine": "~9.3.5" } }, "sha512-Fnb/Azwq9CHSP5sBgAewRbRTmHNrFiQEFZShlreEkzXDkv+nl8DJo2OzU51Uxo+m0JMO+bwYFNgQFV4Nc3DtfQ=="], + + "@deck.gl/arcgis": ["@deck.gl/arcgis@9.3.10", "", { "dependencies": { "esri-loader": "^3.7.0" }, "peerDependencies": { "@arcgis/core": "^4.0.0", "@deck.gl/core": "~9.3.0", "@luma.gl/core": "~9.3.5", "@luma.gl/engine": "~9.3.5", "@luma.gl/webgl": "~9.3.5" } }, "sha512-8XT1HWONOC6hA5V1kxNHiCGUeZYRexz8WY4KG7YWNIfM+yumqfXF1nJFd8DFLBWrv8yVyMeREX1VvLgBGniKFA=="], + + "@deck.gl/carto": ["@deck.gl/carto@9.3.10", "", { "dependencies": { "@carto/api-client": "^0.5.19", "@loaders.gl/compression": "^4.4.3", "@loaders.gl/gis": "^4.4.3", "@loaders.gl/loader-utils": "^4.4.3", "@loaders.gl/mvt": "^4.4.3", "@loaders.gl/schema": "^4.4.3", "@loaders.gl/tiles": "^4.4.3", "@luma.gl/core": "^9.3.5", "@luma.gl/shadertools": "^9.3.5", "@math.gl/web-mercator": "^4.1.0", "@types/d3-array": "^3.0.2", "@types/d3-color": "^3.1.3", "@types/d3-scale": "^3.0.0", "cartocolor": "^5.0.2", "d3-array": "^3.2.0", "d3-color": "^3.1.0", "d3-format": "^3.1.0", "d3-scale": "^4.0.0", "earcut": "^2.2.4", "h3-js": "^4.4.0", "moment-timezone": "^0.6.0", "pbf": "^3.2.1", "quadbin": "^0.4.0" }, "peerDependencies": { "@deck.gl/aggregation-layers": "~9.3.0", "@deck.gl/core": "~9.3.0", "@deck.gl/extensions": "~9.3.0", "@deck.gl/geo-layers": "~9.3.0", "@deck.gl/layers": "~9.3.0", "@loaders.gl/core": "^4.4.3" } }, "sha512-YMXnScpqP2rXfzjrwn19l8HFZathH16e9YkZ8ZxyPNUkfC6CC67V/+NQGwQuWGodhChAEgpKjR3kofSirTGQAg=="], + + "@deck.gl/core": ["@deck.gl/core@9.3.10", "", { "dependencies": { "@loaders.gl/core": "^4.4.3", "@loaders.gl/images": "^4.4.3", "@luma.gl/core": "^9.3.5", "@luma.gl/engine": "^9.3.5", "@luma.gl/shadertools": "^9.3.5", "@luma.gl/webgl": "^9.3.5", "@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.6.4", "gl-matrix": "^3.0.0", "mjolnir.js": "^3.0.0" } }, "sha512-BAJ6r+eRV4euuGcIi2SnlzrodgZ+sD2VuXsoL1DER0LwwKXmKmQu6WA8ax6DB9KFsBghLVoTUN5zaCGF6uKHuQ=="], + + "@deck.gl/extensions": ["@deck.gl/extensions@9.3.10", "", { "dependencies": { "@luma.gl/shadertools": "^9.3.5", "@luma.gl/webgl": "^9.3.5", "@math.gl/core": "^4.1.0" }, "peerDependencies": { "@deck.gl/core": "~9.3.0", "@luma.gl/core": "~9.3.5", "@luma.gl/engine": "~9.3.5" } }, "sha512-VT5NxR+SgUCwvaWeKFKkql5HV90KjyiPyXYlzKfxxXoE8wxlOgt5Enl2MTAOLcIhlCymLp9vtWO/HEgTfvlzDw=="], + + "@deck.gl/geo-layers": ["@deck.gl/geo-layers@9.3.10", "", { "dependencies": { "@loaders.gl/3d-tiles": "^4.4.3", "@loaders.gl/gis": "^4.4.3", "@loaders.gl/loader-utils": "^4.4.3", "@loaders.gl/mvt": "^4.4.3", "@loaders.gl/schema": "^4.4.3", "@loaders.gl/terrain": "^4.4.3", "@loaders.gl/tiles": "^4.4.3", "@loaders.gl/wms": "^4.4.3", "@luma.gl/gltf": "^9.3.5", "@luma.gl/shadertools": "^9.3.5", "@math.gl/core": "^4.1.0", "@math.gl/culling": "^4.1.0", "@math.gl/web-mercator": "^4.1.0", "@types/geojson": "^7946.0.8", "a5-js": "^0.7.2", "h3-js": "^4.4.0", "long": "^3.2.0" }, "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.3", "@luma.gl/core": "~9.3.5", "@luma.gl/engine": "~9.3.5" } }, "sha512-MH6/MyWTiJ02yxrkMz6bMP1Z8d13JEmxogwZmWx2I9XLDNpCnLxnXElnVxxvDvhIbWdWq/i/xl8Kn9WOLiywBg=="], + + "@deck.gl/google-maps": ["@deck.gl/google-maps@9.3.10", "", { "dependencies": { "@luma.gl/webgl": "^9.3.5", "@math.gl/core": "^4.1.0", "@types/google.maps": "^3.48.6" }, "peerDependencies": { "@deck.gl/core": "~9.3.0", "@luma.gl/core": "~9.3.5" } }, "sha512-S84742VZ0SAdYGNVp4C4qHHeuREFLoUZHc9AP3boC69lgosOrHGHNK35zRr2sFYJNhJBLNTiTmPAHdUjIZ4yHw=="], + + "@deck.gl/json": ["@deck.gl/json@9.3.10", "", { "dependencies": { "jsep": "^0.3.0" }, "peerDependencies": { "@deck.gl/core": "~9.3.0" } }, "sha512-MhKcaaGVfpnPlHwLDp1o0MHCcq+JW5SOR4f5ZYriSHKKVG1gK1+xvI/2eos4Et2sLAtuS3r4OwaXHw8AjWnEEg=="], + + "@deck.gl/layers": ["@deck.gl/layers@9.3.10", "", { "dependencies": { "@loaders.gl/images": "^4.4.3", "@loaders.gl/schema": "^4.4.3", "@luma.gl/shadertools": "^9.3.5", "@mapbox/tiny-sdf": "^2.0.5", "@math.gl/core": "^4.1.0", "@math.gl/polygon": "^4.1.0", "@math.gl/web-mercator": "^4.1.0", "@types/geojson": "^7946.0.16", "earcut": "^2.2.4" }, "peerDependencies": { "@deck.gl/core": "~9.3.0", "@loaders.gl/core": "^4.4.3", "@luma.gl/core": "~9.3.5", "@luma.gl/engine": "~9.3.5" } }, "sha512-F9CRJlRWXvTKlgjKmmTotm0cA5V7I1zMtdnwJbjBZTyCJ4skuskbIIt6f+BSQ+rSxM2uW4q5/8VkcCJ2SePhXg=="], + + "@deck.gl/mapbox": ["@deck.gl/mapbox@9.3.10", "", { "dependencies": { "@math.gl/web-mercator": "^4.1.0" }, "peerDependencies": { "@deck.gl/core": "~9.3.0", "@luma.gl/core": "~9.3.5" } }, "sha512-go5+imfjdq/+P5ELMX88ncuzNSpUpL4wolV7LbegwXnKcBIgU13IKbCxJnoGCSW8oxHZu+J99sGDcDyvkYtZcw=="], + + "@deck.gl/mesh-layers": ["@deck.gl/mesh-layers@9.3.10", "", { "dependencies": { "@loaders.gl/gltf": "^4.4.3", "@loaders.gl/schema": "^4.4.3", "@luma.gl/gltf": "^9.3.5", "@luma.gl/shadertools": "^9.3.5" }, "peerDependencies": { "@deck.gl/core": "~9.3.0", "@luma.gl/core": "~9.3.5", "@luma.gl/engine": "~9.3.5" } }, "sha512-HzExbHWQ05fU7YgXcCQPHjHKoMc2PTWUTu3THzdXc5HXhGqpiAd/X1YA7McA9fRJBjw1yD36otMwGEXqUOEKAA=="], + + "@deck.gl/react": ["@deck.gl/react@9.3.10", "", { "peerDependencies": { "@deck.gl/core": "~9.3.0", "@deck.gl/widgets": "~9.3.0", "react": ">=16.3.0", "react-dom": ">=16.3.0" } }, "sha512-kbfceQ6uZxFS5DVwM3JLY1gSVfx+IJ8ihnkSomxX5Nhhfos1GWnH5wJo8wznKGl9WdLd/iY5bHc8q9i0f5TVjA=="], + + "@deck.gl/widgets": ["@deck.gl/widgets@9.3.10", "", { "dependencies": { "@floating-ui/dom": "^1.7.5", "preact": "^10.17.0" }, "peerDependencies": { "@deck.gl/core": "~9.3.0", "@luma.gl/core": "~9.3.5" } }, "sha512-eEQuxQeqIkm04d9MgUH0MtGTmW3xSvqS/nkyIGPhaoAj6VWGu850JfXVdQbnORxFweoQWPI1jfU0Q8j8twhkEA=="], + "@discordjs/builders": ["@discordjs/builders@1.14.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.40", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ=="], "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], @@ -480,6 +521,12 @@ "@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], + "@esri/arcgis-html-sanitizer": ["@esri/arcgis-html-sanitizer@4.1.0", "", { "dependencies": { "xss": "1.0.13" } }, "sha512-einEveDJ/k1180NOp78PB/4Hje9eBy3dyOGLLtLn6bSkizpUfCwuYBIXOA7Y3F/k/BsTQXgKqUVwQ0eiscWMdA=="], + + "@esri/calcite-components": ["@esri/calcite-components@3.3.3", "", { "dependencies": { "@arcgis/lumina": ">=4.34.0-next.158 <4.35.0", "@arcgis/toolkit": ">=4.34.0-next.158 <4.35.0", "@esri/calcite-ui-icons": "4.3.0", "@floating-ui/dom": "^1.6.12", "@floating-ui/utils": "^0.2.8", "@types/sortablejs": "^1.15.8", "color": "^5.0.0", "composed-offset-position": "^0.0.6", "es-toolkit": "^1.39.8", "focus-trap": "^7.6.5", "interactjs": "^1.10.27", "lit": "^3.3.0", "sortablejs": "^1.15.6", "timezone-groups": "^0.10.4", "type-fest": "^4.30.1" } }, "sha512-tw+EfJ3pb+Odj71W6E9GUkm8rMbNxfW1KeiI8GgsKDzhr39hMKwY+zYYFFYuO0FONxWGvAB+B8yqB0NvH7WeHw=="], + + "@esri/calcite-ui-icons": ["@esri/calcite-ui-icons@4.3.0", "", { "bin": { "spriter": "bin/spriter.js" } }, "sha512-iOOuRurpjFxFVw6+aXW2JpSkRBrdOpBcbdibfPOmSPqMd1aoHBtYmYXetKoH9vfrXoBiPyO2PkDnczhsu/N9IA=="], + "@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="], "@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="], @@ -488,6 +535,14 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="], + "@foliojs-fork/fontkit": ["@foliojs-fork/fontkit@1.9.2", "", { "dependencies": { "@foliojs-fork/restructure": "^2.0.2", "brotli": "^1.2.0", "clone": "^1.0.4", "deep-equal": "^1.0.0", "dfa": "^1.2.0", "tiny-inflate": "^1.0.2", "unicode-properties": "^1.2.2", "unicode-trie": "^2.0.0" } }, "sha512-IfB5EiIb+GZk+77TRB86AHroVaqfq8JRFlUbz0WEwsInyCG0epX2tCPOy+UfaWPju30DeVoUAXfzWXmhn753KA=="], + + "@foliojs-fork/linebreak": ["@foliojs-fork/linebreak@1.1.2", "", { "dependencies": { "base64-js": "1.3.1", "unicode-trie": "^2.0.0" } }, "sha512-ZPohpxxbuKNE0l/5iBJnOAfUaMACwvUIKCvqtWGKIMv1lPYoNjYXRfhi9FeeV9McBkBLxsMFWTVVhHJA8cyzvg=="], + + "@foliojs-fork/pdfkit": ["@foliojs-fork/pdfkit@0.15.3", "", { "dependencies": { "@foliojs-fork/fontkit": "^1.9.2", "@foliojs-fork/linebreak": "^1.1.1", "crypto-js": "^4.2.0", "jpeg-exif": "^1.1.4", "png-js": "^1.0.0" } }, "sha512-Obc0Wmy3bm7BINFVvPhcl2rnSSK61DQrlHU8aXnAqDk9LCjWdUOPwhgD8Ywz5VtuFjRxmVOM/kQ/XLIBjDvltw=="], + + "@foliojs-fork/restructure": ["@foliojs-fork/restructure@2.0.2", "", {}, "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA=="], + "@fontsource-variable/fredoka": ["@fontsource-variable/fredoka@5.3.0", "", {}, "sha512-13w98iw2RcmYfZQRZPLSu+0YJK3HHm5A2XrpU26moFIAePmfiKfvkmEJUgkc8r5GNR+pbDijOueS1KdGAMAWmQ=="], "@fontsource-variable/nunito": ["@fontsource-variable/nunito@5.3.0", "", {}, "sha512-tw0ch7evIDSwoeUy+0XzocYHfbRWLeCRPJy22KgbrLjDLAbNw/fErBiil10Ssrs3Sjr55zaZrr36a975j8cBkQ=="], @@ -518,6 +573,8 @@ "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], + "@interactjs/types": ["@interactjs/types@1.10.28", "", {}, "sha512-vPmu4HWmsg0Fub3BPKGk7DuozkhgVK84TVkziY47v8Uh0A14gph55/vVRaQN4oZov0lGNJNK6Jyddim2qd/4vw=="], + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -532,6 +589,96 @@ "@keyv/serialize": ["@keyv/serialize@1.1.1", "", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="], + "@lit-labs/ssr-dom-shim": ["@lit-labs/ssr-dom-shim@1.6.0", "", {}, "sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ=="], + + "@lit/reactive-element": ["@lit/reactive-element@2.1.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0" } }, "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A=="], + + "@loaders.gl/3d-tiles": ["@loaders.gl/3d-tiles@4.4.5", "", { "dependencies": { "@loaders.gl/compression": "4.4.5", "@loaders.gl/crypto": "4.4.5", "@loaders.gl/draco": "4.4.5", "@loaders.gl/gltf": "4.4.5", "@loaders.gl/images": "4.4.5", "@loaders.gl/loader-utils": "4.4.5", "@loaders.gl/math": "4.4.5", "@loaders.gl/tiles": "4.4.5", "@loaders.gl/zip": "4.4.5", "@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.2.1" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-+W9ncpVw5+OX9pjhZFmpbK+pBqT1XNCAaUbUY/qItFlK1PcTSQdy34NVvAziRR7dFxJ0VmWPEu5Q75LcNCCX2A=="], + + "@loaders.gl/compression": ["@loaders.gl/compression@4.4.5", "", { "dependencies": { "@loaders.gl/loader-utils": "4.4.5", "@loaders.gl/worker-utils": "4.4.5", "@types/pako": "^1.0.1", "fflate": "0.7.4", "pako": "1.0.11", "snappyjs": "^0.6.1" }, "optionalDependencies": { "@types/brotli": "^1.3.0", "brotli": "^1.3.2", "lz4js": "^0.2.0", "zstd-codec": "^0.1" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-vsuvdIyn3xLqiJxXhK970ENO3T8bLPtmM0jka8/y0zVAEover6q3Vd8gVsaqgWGfc28cWx5RYI4zKhJzRcTHNg=="], + + "@loaders.gl/core": ["@loaders.gl/core@4.4.5", "", { "dependencies": { "@loaders.gl/loader-utils": "4.4.5", "@loaders.gl/schema": "4.4.5", "@loaders.gl/schema-utils": "4.4.5", "@loaders.gl/worker-utils": "4.4.5", "@probe.gl/log": "^4.1.1" } }, "sha512-hlpE200MinDAyU0kMjEM4yk+0TDxiB9nT+GcwOCrXc7buE0e0YwTn+Ynz2Q85dwDKvJZw3kNHTTBGvQwq9533A=="], + + "@loaders.gl/crypto": ["@loaders.gl/crypto@4.4.5", "", { "dependencies": { "@loaders.gl/loader-utils": "4.4.5", "@loaders.gl/worker-utils": "4.4.5", "@types/crypto-js": "^4.0.2" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-P0n43KWN7GGsWF7+E094Nj1mwngN6ypfgts5tu2hSEroUS5dB01TgDa0Pr3sVeZ672jFSKKRgRuFDazRcITOFA=="], + + "@loaders.gl/draco": ["@loaders.gl/draco@4.4.5", "", { "dependencies": { "@loaders.gl/loader-utils": "4.4.5", "@loaders.gl/schema": "4.4.5", "@loaders.gl/schema-utils": "4.4.5", "@loaders.gl/worker-utils": "4.4.5", "draco3d": "1.5.7" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-J4DuzoUQNJhjxmErCArF5J1PC0LCMbhPkY/YdkTaDmUwxSQdh1M/VWg85AXKLAxUiwGkhmC8AUUaSyPxTpCUrQ=="], + + "@loaders.gl/geoarrow": ["@loaders.gl/geoarrow@4.4.5", "", { "dependencies": { "@math.gl/polygon": "^4.1.0", "apache-arrow": ">= 17.0.0" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-uUWp+5nZxQe6Sv/o36R6jcmCCNhguY0VhjougyMoH6FWqF6iJHrOnwx5UdEtvkw/zV9mHabs05Hv91dBvhfljQ=="], + + "@loaders.gl/gis": ["@loaders.gl/gis@4.4.5", "", { "dependencies": { "@loaders.gl/geoarrow": "4.4.5", "@loaders.gl/loader-utils": "4.4.5", "@loaders.gl/schema": "4.4.5", "@loaders.gl/schema-utils": "4.4.5", "@mapbox/vector-tile": "^1.3.1", "@math.gl/polygon": "^4.1.0", "pbf": "^3.2.1" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-r3klnZ6jjzaEZlUSCmGufPbH1pQpM5PCcGS70zLlGIFbpJPo80gaXDtZDVOSQvpWmqY8u8d7/6moAsogVoymzA=="], + + "@loaders.gl/gltf": ["@loaders.gl/gltf@4.4.5", "", { "dependencies": { "@loaders.gl/draco": "4.4.5", "@loaders.gl/images": "4.4.5", "@loaders.gl/loader-utils": "4.4.5", "@loaders.gl/schema": "4.4.5", "@loaders.gl/textures": "4.4.5", "@math.gl/core": "^4.1.0", "meshoptimizer": "^1.2.0" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-YIhgeoDYdRbfXQS+cpRWYbiRiiaajF5UCqpEoFpDEpOpcW6pKC2sfp5vZ1LiUIXu2CVv/HFJaDeLuQjeX5uVlQ=="], + + "@loaders.gl/images": ["@loaders.gl/images@4.4.5", "", { "dependencies": { "@loaders.gl/loader-utils": "4.4.5" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-7/odHt5n67OVLHelTm1euNNw5gCwycezQI7Wg3NBUg7q3bHWmTPY++Z4uz2egiuzJNY3GLegfg8AI1c3fD64Jw=="], + + "@loaders.gl/loader-utils": ["@loaders.gl/loader-utils@4.4.5", "", { "dependencies": { "@loaders.gl/schema": "4.4.5", "@loaders.gl/worker-utils": "4.4.5", "@probe.gl/log": "^4.1.1", "@probe.gl/stats": "^4.1.1" } }, "sha512-c4h2RUMEru+hbfvgZEIy5cLQKyQka2TPQZW/2Wy+yIPr3FU7RKwfp+vLwIfir6Xkdn2zniWyWretZwE02aY/Ug=="], + + "@loaders.gl/math": ["@loaders.gl/math@4.4.5", "", { "dependencies": { "@math.gl/core": "^4.1.0" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-Az7rMw7oMNMzi1bXgPaabdkpPtk0safkWBr+I+dHjZHzny1RK5mFF4VjRkalw++DZBKbL0TskMw/OcdiRaL/Bw=="], + + "@loaders.gl/mvt": ["@loaders.gl/mvt@4.4.5", "", { "dependencies": { "@loaders.gl/gis": "4.4.5", "@loaders.gl/images": "4.4.5", "@loaders.gl/loader-utils": "4.4.5", "@loaders.gl/schema": "4.4.5", "@loaders.gl/schema-utils": "4.4.5", "@math.gl/polygon": "^4.1.0", "@probe.gl/stats": "^4.1.1", "pbf": "^3.2.1" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-3c4ojvF6+Fb0XjX2xGeRKIUAshaNgBfi0VkszooFNCNvrVv7faGcn69jI9wh1Pjf2ge1PtsZnAoOuLQXLLzWSQ=="], + + "@loaders.gl/schema": ["@loaders.gl/schema@4.4.5", "", { "dependencies": { "@types/geojson": "^7946.0.7", "apache-arrow": ">= 17.0.0" } }, "sha512-q2XNquegw+hX+ZEshacKYL/+FKBUIS5FHA+2Ft3JWmcNiR6umGnYCdXQhDcFCqAm8Ks87fMCV+z/zTloxaIZgQ=="], + + "@loaders.gl/schema-utils": ["@loaders.gl/schema-utils@4.4.5", "", { "dependencies": { "@loaders.gl/schema": "4.4.5", "@math.gl/types": "^4.1.0", "@types/geojson": "^7946.0.7", "apache-arrow": ">= 17.0.0" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-vi6AROLWvoLIN609tVmchwotTaHK5rHv8SWjoVUWLhPCXmIc9npz0rCl7NJScCgr7UjYifZG1rWOnrAxaYTlyA=="], + + "@loaders.gl/terrain": ["@loaders.gl/terrain@4.4.5", "", { "dependencies": { "@loaders.gl/images": "4.4.5", "@loaders.gl/loader-utils": "4.4.5", "@loaders.gl/schema": "4.4.5", "@mapbox/martini": "^0.2.0" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-H8BED2/0I1wrviE8mBr1ny/01nK1vqctMHBYSoAHS36JJm5m+Tt2JfKXIgnNY2TU3Ulfr9L9x1VfH9a/z9V1jw=="], + + "@loaders.gl/textures": ["@loaders.gl/textures@4.4.5", "", { "dependencies": { "@loaders.gl/images": "4.4.5", "@loaders.gl/loader-utils": "4.4.5", "@loaders.gl/schema": "4.4.5", "@loaders.gl/worker-utils": "4.4.5", "@math.gl/types": "^4.1.0", "ktx-parse": "^0.7.0", "texture-compressor": "^1.0.2" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-zB9xMIKiU3tQN6q5/A6yaXw/DbodA+bqMDTVu7pzk4lw4zcBwWiGtD5LV0QjgtlTj2ZnwV8cCd1rYk9bwiX2Bw=="], + + "@loaders.gl/tiles": ["@loaders.gl/tiles@4.4.5", "", { "dependencies": { "@loaders.gl/loader-utils": "4.4.5", "@loaders.gl/math": "4.4.5", "@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" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-XO9NeUsukjj+90YN1GEJWOgt7rI5GIv20dCpPIYdgj299aa8iYB5DWcm1YGcOuNjGqdQNZXIgPSv6ocdN/X7Mw=="], + + "@loaders.gl/wms": ["@loaders.gl/wms@4.4.5", "", { "dependencies": { "@loaders.gl/images": "4.4.5", "@loaders.gl/loader-utils": "4.4.5", "@loaders.gl/schema": "4.4.5", "@loaders.gl/xml": "4.4.5", "@turf/rewind": "^5.1.5", "deep-strict-equal": "^0.2.0" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-0+nXHY017bAl3tELIefUa0FB3ptUpEVxtQD1imNqyUtRr1vrzhyc+kbBcc3uRIcDj/F6iW9pq2V6lBwcIGZXFw=="], + + "@loaders.gl/worker-utils": ["@loaders.gl/worker-utils@4.4.5", "", { "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-ZQDoM6f/HpZ3Lzj6vjtPHXSvi+cUK0QG7yJz+MKWAokmeWa98WMZwswS8zkn/qNkY95rLcxh66N4Sl96+7hqAQ=="], + + "@loaders.gl/xml": ["@loaders.gl/xml@4.4.5", "", { "dependencies": { "@loaders.gl/loader-utils": "4.4.5", "@loaders.gl/schema": "4.4.5", "fast-xml-parser": "^5.3.6" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-UsN7T9e67Sa1Q6Ra/u+j2mtaT9GTcFhH8iz8ba5QFU90YrRn3dwBEwq9SVxQiJrfC6dE4jFdMannW5mzeV5owg=="], + + "@loaders.gl/zip": ["@loaders.gl/zip@4.4.5", "", { "dependencies": { "@loaders.gl/compression": "4.4.5", "@loaders.gl/crypto": "4.4.5", "@loaders.gl/loader-utils": "4.4.5", "jszip": "^3.1.5", "md5": "^2.3.0" }, "peerDependencies": { "@loaders.gl/core": "~4.4.0" } }, "sha512-vXlwnJ2QCCtXc8Pf55YJUNTJ0sjtjbUr34StDY91GPmoutdAXovqikuwzehrAlMDE69ldbnMtVATmNe4XeWGXQ=="], + + "@luma.gl/core": ["@luma.gl/core@9.3.6", "", { "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" } }, "sha512-eqHnCPh2xHYkdd9rEkiIIkGMixNiJkq1ROrnTob6npvmZNVHXL0ubIw5KueL7rqW0+J1tm+p2+TXZaCmn0sP7Q=="], + + "@luma.gl/engine": ["@luma.gl/engine@9.3.6", "", { "dependencies": { "@math.gl/core": "^4.1.0", "@math.gl/types": "^4.1.0", "@probe.gl/log": "^4.1.1", "@probe.gl/stats": "^4.1.1" }, "peerDependencies": { "@luma.gl/core": "~9.3.0", "@luma.gl/shadertools": "~9.3.0" } }, "sha512-NYTdhn2NaH/MjN8qXCl2ukrT1Ac9iTKu1mgN0wz11XRd1QYnlMNBXswRROD7IZ2PUsBWdmYHc9qE8wKBQqMuIA=="], + + "@luma.gl/gltf": ["@luma.gl/gltf@9.3.6", "", { "dependencies": { "@loaders.gl/core": "~4.4.0", "@loaders.gl/gltf": "~4.4.0", "@loaders.gl/textures": "~4.4.0", "@math.gl/core": "^4.1.0" }, "peerDependencies": { "@luma.gl/core": "~9.3.0", "@luma.gl/engine": "~9.3.0", "@luma.gl/shadertools": "~9.3.0" } }, "sha512-9R27aYwvu6KBJzd9Kxs4cqIJpgJsI1AyL6Pn6+5CrmGbaCqMtgWi7Sf9Wo+bqv0PRxwzH0j1oAv7qRk0hGbstw=="], + + "@luma.gl/shadertools": ["@luma.gl/shadertools@9.3.6", "", { "dependencies": { "@math.gl/core": "^4.1.0", "@math.gl/types": "^4.1.0" }, "peerDependencies": { "@luma.gl/core": "~9.3.0" } }, "sha512-dDuD8lCOkAE1L7hJGZEyZAi7upR1HG3wEEIQ1gCLaTTbJWC7tNtnVz853lqUA+VXgjgS9NiQFFRcosiIZmW4Vg=="], + + "@luma.gl/webgl": ["@luma.gl/webgl@9.3.6", "", { "dependencies": { "@math.gl/types": "^4.1.0", "@probe.gl/env": "^4.1.1" }, "peerDependencies": { "@luma.gl/core": "~9.3.0" } }, "sha512-tGm7FGWPmJKxGZMvkRPRi219x3tKmBxR7Uke09nTp2ujNak/O5V9xPIQ9lUBGTxqAb8U2yjKkXG8j+f/4b2+sw=="], + + "@mapbox/jsonlint-lines-primitives": ["@mapbox/jsonlint-lines-primitives@2.0.3", "", {}, "sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg=="], + + "@mapbox/martini": ["@mapbox/martini@0.2.0", "", {}, "sha512-7hFhtkb0KTLEls+TRw/rWayq5EeHtTaErgm/NskVoXmtgAQu/9D299aeyj6mzAR/6XUnYRp2lU+4IcrYRFjVsQ=="], + + "@mapbox/point-geometry": ["@mapbox/point-geometry@1.1.0", "", {}, "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ=="], + + "@mapbox/tiny-sdf": ["@mapbox/tiny-sdf@2.2.0", "", {}, "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg=="], + + "@mapbox/unitbezier": ["@mapbox/unitbezier@1.0.0", "", {}, "sha512-fqd515fjBmANKGGsQ286E2Wvj/XvDFpGzwJxq4CI6jMQue6Oy04uCKp+JWKF00xRTmk6cEu1jPJ9p3xqH8YWqQ=="], + + "@mapbox/vector-tile": ["@mapbox/vector-tile@3.0.0", "", { "dependencies": { "@mapbox/point-geometry": "~1.1.0", "@types/geojson": "^7946.0.16", "pbf": "^5.0.0" } }, "sha512-Qf10S1uIHMk20ri/IVBnpS+esUEkVaR5Hftmz88jTInrpmWgPGJfPe3LVjjlE77trLx8tH6qjTG7uWH9hIq/0Q=="], + + "@maplibre/geojson-vt": ["@maplibre/geojson-vt@6.1.1", "", { "dependencies": { "kdbush": "^4.1.0" } }, "sha512-FVMOcmSP/yqol45t7StApEyTL5/vmqBCuFhH9n+fFuINenhaX+YgHHIt1yJ86S8kln3uJLcMvmEU2cfn6E2eCQ=="], + + "@maplibre/maplibre-gl-style-spec": ["@maplibre/maplibre-gl-style-spec@26.2.1", "", { "dependencies": { "@mapbox/jsonlint-lines-primitives": "^2.0.3", "@mapbox/unitbezier": "^1.0.0", "json-stringify-pretty-compact": "^4.0.0", "minimist": "^1.2.8", "quickselect": "^3.0.0", "tinyqueue": "^3.0.0" }, "bin": { "gl-style-format": "dist/gl-style-format.mjs", "gl-style-migrate": "dist/gl-style-migrate.mjs", "gl-style-validate": "dist/gl-style-validate.mjs" } }, "sha512-QFKCXkOeSzOr8jF75jm6kySOg+dUvOehPhRi68gcOYPHb7U5JloUq0dJW0Y5/fZV8ygfT0Vp2RWodvq+fyxFWA=="], + + "@maplibre/mlt": ["@maplibre/mlt@1.2.0", "", { "dependencies": { "@mapbox/point-geometry": "^1.1.0" } }, "sha512-g45M8gEI4sMO3X9ib4K7n3ZKFf0qQOL+NMkHCnEK51lOrYFHiEssOuZncx1+miIgi1IEE2NcbRMcq8RBkL+QHA=="], + + "@maplibre/vt-pbf": ["@maplibre/vt-pbf@4.3.2", "", { "dependencies": { "@mapbox/point-geometry": "^1.1.0", "@types/geojson": "^7946.0.16", "pbf": "^5.1.0" } }, "sha512-j6p0AdjvAR19Z3XaCysle7A4ZSo08tYOzxD0Y9NQylwPAkwJJeYub5b2eVucdeDh7erhv69DahoLOevDRERRUw=="], + + "@math.gl/core": ["@math.gl/core@4.1.0", "", { "dependencies": { "@math.gl/types": "4.1.0" } }, "sha512-FrdHBCVG3QdrworwrUSzXIaK+/9OCRLscxI2OUy6sLOHyHgBMyfnEGs99/m3KNvs+95BsnQLWklVfpKfQzfwKA=="], + + "@math.gl/culling": ["@math.gl/culling@4.1.0", "", { "dependencies": { "@math.gl/core": "4.1.0", "@math.gl/types": "4.1.0" } }, "sha512-jFmjFEACnP9kVl8qhZxFNhCyd47qPfSVmSvvjR0/dIL6R9oD5zhR1ub2gN16eKDO/UM7JF9OHKU3EBIfeR7gtg=="], + + "@math.gl/geospatial": ["@math.gl/geospatial@4.1.0", "", { "dependencies": { "@math.gl/core": "4.1.0", "@math.gl/types": "4.1.0" } }, "sha512-BzsUhpVvnmleyYF6qdqJIip6FtIzJmnWuPTGhlBuPzh7VBHLonCFSPtQpbkRuoyAlbSyaGXcVt6p6lm9eK2vtg=="], + + "@math.gl/polygon": ["@math.gl/polygon@4.1.0", "", { "dependencies": { "@math.gl/core": "4.1.0" } }, "sha512-YA/9PzaCRHbIP5/0E9uTYrqe+jsYTQoqoDWhf6/b0Ixz8bPZBaGDEafLg3z7ffBomZLacUty9U3TlPjqMtzPjA=="], + + "@math.gl/sun": ["@math.gl/sun@4.1.0", "", {}, "sha512-i3q6OCBLSZ5wgZVhXg+X7gsjY/TUtuFW/2KBiq/U1ypLso3S4sEykoU/MGjxUv1xiiGtr+v8TeMbO1OBIh/HmA=="], + + "@math.gl/types": ["@math.gl/types@4.1.0", "", {}, "sha512-clYZdHcmRvMzVK5fjeDkQlHUzXQSNdZ7s4xOqC3nJPgz4C/TZkUecTo9YS4PruZqtDda/ag4erndP0MIn40dGA=="], + + "@math.gl/web-mercator": ["@math.gl/web-mercator@4.1.0", "", { "dependencies": { "@math.gl/core": "4.1.0" } }, "sha512-HZo3vO5GCMkXJThxRJ5/QYUYRr3XumfT8CzNNCwoJfinxy5NtKUd7dusNTXn7yJ40UoB8FMIwkVwNlqaiRZZAw=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.30.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA=="], "@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="], @@ -562,6 +709,8 @@ "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], + "@nodable/entities": ["@nodable/entities@3.0.0", "", {}, "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -590,6 +739,8 @@ "@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], + "@open-wc/dedupe-mixin": ["@open-wc/dedupe-mixin@1.4.0", "", {}, "sha512-Sj7gKl1TLcDbF7B6KUhtvr+1UCxdhMbNY5KxdU5IfMFWqL8oy1ZeAcCANjoB1TL0AJTcPmcCFsCbHf8X2jGDUA=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], "@pnpm/config.env-replace": ["@pnpm/config.env-replace@1.1.0", "", {}, "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w=="], @@ -598,8 +749,16 @@ "@pnpm/npm-conf": ["@pnpm/npm-conf@3.0.3", "", { "dependencies": { "@pnpm/config.env-replace": "^1.1.0", "@pnpm/network.ca-file": "^1.0.1", "config-chain": "^1.1.11" } }, "sha512-//0sR/cow/s4ICQaYoAobOl4aU8cjU6x/V24V7XkKotb9+O+3zySIYp146vpaobYHnxa4pZX8NkV54Z5AwbDKA=="], + "@polymer/polymer": ["@polymer/polymer@3.5.2", "", { "dependencies": { "@webcomponents/shadycss": "^1.9.1" } }, "sha512-fWwImY/UH4bb2534DVSaX+Azs2yKg8slkMBHOyGeU2kKx7Xmxp6Lee0jP8p6B3d7c1gFUPB2Z976dTUtX81pQA=="], + "@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="], + "@probe.gl/env": ["@probe.gl/env@4.1.1", "", {}, "sha512-+68seNDMVsEegRB47pFA/Ws1Fjy8agcFYXxzorKToyPcD6zd+gZ5uhwoLd7TzsSw6Ydns//2KEszWn+EnNHTbA=="], + + "@probe.gl/log": ["@probe.gl/log@4.1.1", "", { "dependencies": { "@probe.gl/env": "4.1.1" } }, "sha512-kcZs9BT44pL7hS1OkRGKYRXI/SN9KejUlPD+BY40DguRLzdC5tLG/28WGMyfKdn/51GT4a0p+0P8xvDn1Ez+Kg=="], + + "@probe.gl/stats": ["@probe.gl/stats@4.1.1", "", {}, "sha512-4VpAyMHOqydSvPlEyHwXaE+AkIdR03nX+Qhlxsk2D/IW4OVmDZgIsvJB1cDzyEEtcfKcnaEbfXeiPgejBceT6g=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], @@ -958,6 +1117,8 @@ "@turf/bbox": ["@turf/bbox@7.3.5", "", { "dependencies": { "@turf/helpers": "7.3.5", "@turf/meta": "7.3.5", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-oG1ya/HtBjAIg4TimbWx+nOYPbY0bCvt82Bq8tm6sBw3qqtbOyRSfDz79Sq90TnH7DXJprJ1qnVGKNtZ6jemfw=="], + "@turf/boolean-clockwise": ["@turf/boolean-clockwise@5.1.5", "", { "dependencies": { "@turf/helpers": "^5.1.5", "@turf/invariant": "^5.1.5" } }, "sha512-FqbmEEOJ4rU4/2t7FKx0HUWmjFEVqR+NJrFP7ymGSjja2SQ7Q91nnBihGuT+yuHHl6ElMjQ3ttsB/eTmyCycxA=="], + "@turf/boolean-contains": ["@turf/boolean-contains@7.3.5", "", { "dependencies": { "@turf/bbox": "7.3.5", "@turf/boolean-point-in-polygon": "7.3.5", "@turf/boolean-point-on-line": "7.3.5", "@turf/helpers": "7.3.5", "@turf/invariant": "7.3.5", "@turf/line-split": "7.3.5", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-P4JUAHgvJkD+8ybQ6d1OHp9TBsGsjJxF5lWeXJgp0k4+Hd/D0CVy4/mhLkZdNa6QdljVdwNcfU0CTqy1WsSQig=="], "@turf/boolean-overlap": ["@turf/boolean-overlap@7.3.5", "", { "dependencies": { "@turf/helpers": "7.3.5", "@turf/invariant": "7.3.5", "@turf/line-intersect": "7.3.5", "@turf/line-overlap": "7.3.5", "@turf/meta": "7.3.5", "@types/geojson": "^7946.0.10", "geojson-equality-ts": "^1.0.2", "tslib": "^2.8.1" } }, "sha512-DUeiPVqFSTjW79erPbo780pRcKCRad59NcscVsTYkZD/92peF/4rQvHbvAUpUDPZaQCxe+mvfeDHfUFmfVKCGA=="], @@ -968,6 +1129,8 @@ "@turf/center": ["@turf/center@7.3.5", "", { "dependencies": { "@turf/bbox": "7.3.5", "@turf/helpers": "7.3.5", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-eub5/Kfdmn89ZqwCONHI7astmTDEtN5M6+JfOkgoSyhKKFhUJYNxUyH1F/vCtIP7j1K369Vs4L9TYiuGapvIKQ=="], + "@turf/clone": ["@turf/clone@5.1.5", "", { "dependencies": { "@turf/helpers": "^5.1.5" } }, "sha512-//pITsQ8xUdcQ9pVb4JqXiSqG4dos5Q9N4sYFoWghX21tfOV2dhc5TGqYOhnHrQS7RiKQL1vQ48kIK34gQ5oRg=="], + "@turf/destination": ["@turf/destination@7.3.5", "", { "dependencies": { "@turf/helpers": "7.3.5", "@turf/invariant": "7.3.5", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-x6ylChhOlIbucRSw7wF6z2gSEqqQl+dE0nSH5AL/ojZkqqGYahiw+2P4A8ZMuDM6rzH+FIqRYDes0o4nSArZCw=="], "@turf/distance": ["@turf/distance@7.3.5", "", { "dependencies": { "@turf/helpers": "7.3.5", "@turf/invariant": "7.3.5", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-uQAC63zg/l91KUxzfhqio7Ii3+UXTrPOVJScIdRj6EO6+9XHI4kC+AdyIS4cPAv14sZfJLIBxzMnzcGrss+kEA=="], @@ -990,38 +1153,92 @@ "@turf/nearest-point-on-line": ["@turf/nearest-point-on-line@7.3.5", "", { "dependencies": { "@turf/distance": "7.3.5", "@turf/helpers": "7.3.5", "@turf/invariant": "7.3.5", "@turf/meta": "7.3.5", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-MZn6OkEFZpjS6BNUANfqiHMIbQSivu7TNji3a+OAIrnPJ71vp8cbz0N2aVEa5M7I8ipvxoxAPIV3eqg3h280Vg=="], + "@turf/rewind": ["@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.1.5", "@turf/meta": "^5.1.5" } }, "sha512-Gdem7JXNu+G4hMllQHXRFRihJl3+pNl7qY+l4qhQFxq+hiU1cQoVFnyoleIqWKIrdK/i2YubaSwc3SCM7N5mMw=="], + "@turf/truncate": ["@turf/truncate@7.3.5", "", { "dependencies": { "@turf/helpers": "7.3.5", "@turf/meta": "7.3.5", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-Qx2iv3KIqKuDAUduMfaJ5fFegEWBeRve5zePalRevS16bMUqEX+jnKPK9fWGyUuPqT61qP1Kybz0PTWPbUbljQ=="], "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + "@types/brotli": ["@types/brotli@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-9xoNr+bcxT236/7ZgcWw/6Pb2RRetE13p4bFy1xYSckKwyOiRfmInay8baUWZgH7/284Wl6IPe7+nOI9+OQg/A=="], + "@types/config": ["@types/config@3.3.5", "", {}, "sha512-itq2HtXQBrNUKwMNZnb9mBRE3T99VYCdl1gjST9rq+9kFaB1iMMGuDeZnP88qid73DnpAMKH9ZolqDpS1Lz7+w=="], "@types/conventional-commits-parser": ["@types/conventional-commits-parser@5.0.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g=="], + "@types/crypto-js": ["@types/crypto-js@4.2.2", "", {}, "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ=="], + + "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], + + "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], + + "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], + + "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], + + "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], + + "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], + + "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], + + "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], + + "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], + + "@types/d3-geo": ["@types/d3-geo@3.1.1", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w=="], + + "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.1", "", {}, "sha512-QwjxA3+YCKH3N1Rs3uSiSy1bdxlLB1uUiENXeJudBoAFvtDuswUxLcanoOaR2JYn1melDTuIXR8VhnVyI3yG/A=="], + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], - "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], + + "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], + + "@types/d3-random": ["@types/d3-random@3.0.4", "", {}, "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA=="], + + "@types/d3-sankey": ["@types/d3-sankey@0.11.2", "", { "dependencies": { "@types/d3-shape": "^1" } }, "sha512-U6SrTWUERSlOhnpSrgvMX64WblX1AxX6nEjI2t3mLK2USpQrnbwYYK+AS9SwiE7wgYmOsSSKoSdr8aoKBH0HgQ=="], + + "@types/d3-scale": ["@types/d3-scale@3.3.5", "", { "dependencies": { "@types/d3-time": "^2" } }, "sha512-YOpKj0kIEusRf7ofeJcSZQsvKbnTwpe1DUF+P2qsotqG53kEsjm7EzzliqQxMkAWdkZcHrg5rRhB4JiDOQPX+A=="], + + "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], + + "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], "@types/d3-shape": ["@types/d3-shape@3.2.0", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw=="], "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], + + "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], + "@types/dlv": ["@types/dlv@1.1.5", "", {}, "sha512-JHOWNfiWepAhfwlSw17kiWrWrk6od2dEQgHltJw9AS0JPFoLZJBge5+Dnil2NfdjAvJ/+vGSX60/BRW20PpUXw=="], "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + "@types/google.maps": ["@types/google.maps@3.65.5", "", {}, "sha512-oR9Hq/WaKGzIguZEwoCuyQ0ofDhCbefmS9RrjLUZ5scMziwTE6xA90drJYt2ZsyUCYmCqnxQcDpfGqeRE/Zgog=="], + "@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="], "@types/leaflet": ["@types/leaflet@1.9.22", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-h3lhECYEKDasG7LFHu+GiHqAvsgLuQvlJvVZzJDGONo3sEL+wUOqSFLnwkZlK0qVxnxbuGFW8iBlJNYs5wgndA=="], @@ -1032,8 +1249,14 @@ "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], + "@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="], + + "@types/pako": ["@types/pako@1.0.7", "", {}, "sha512-YBtzT2ztNF6R/9+UXj2wTGFnC9NklAnASt3sC0h2m1bbH7G6FyBIkt4AN8ThZpNfxUo1b2iMVO0UawiJymEt8A=="], + "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], + "@types/polylabel": ["@types/polylabel@1.1.3", "", {}, "sha512-9Zw2KoDpi+T4PZz2G6pO2xArE0m/GSMTW1MIxF2s8ZY8x9XDO6fv9um0ydRGvcbkFLlaq8yNK6eZxnmMZtDgWQ=="], + "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="], @@ -1042,8 +1265,14 @@ "@types/react-transition-group": ["@types/react-transition-group@4.4.12", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="], + "@types/sortablejs": ["@types/sortablejs@1.15.9", "", {}, "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ=="], + + "@types/svg-arc-to-cubic-bezier": ["@types/svg-arc-to-cubic-bezier@3.2.3", "", {}, "sha512-UNOnbTtl0nVTm8hwKaz5R5VZRvSulFMGojO5+Q7yucKxBoCaTtS4ibSQVRHo5VW5AaRo145U8p1Vfg5KrYe9Bg=="], + "@types/tinycolor2": ["@types/tinycolor2@1.4.6", "", {}, "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], "@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="], @@ -1054,10 +1283,40 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="], + "@vaadin/a11y-base": ["@vaadin/a11y-base@24.9.17", "", { "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", "@polymer/polymer": "^3.0.0", "@vaadin/component-base": "~24.9.17", "lit": "^3.0.0" } }, "sha512-y0uxPHx89aobXvL0q9i81XNJ9oUrINiKmwjXZh9vWygN9Tj02nsBJWad6Q0N1nunEN2M6EvrT4c3vmE1TnI6lg=="], + + "@vaadin/checkbox": ["@vaadin/checkbox@24.9.17", "", { "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", "@polymer/polymer": "^3.0.0", "@vaadin/a11y-base": "~24.9.17", "@vaadin/component-base": "~24.9.17", "@vaadin/field-base": "~24.9.17", "@vaadin/vaadin-lumo-styles": "~24.9.17", "@vaadin/vaadin-material-styles": "~24.9.17", "@vaadin/vaadin-themable-mixin": "~24.9.17", "lit": "^3.0.0" } }, "sha512-iSsjNt4heOatYyVqBKCPooSi3qwauMSPZOdhHKX3bpOvnA2vJRAthtsbIWRyWIxk5i3vAFeh48MGfCvGjCyiHQ=="], + + "@vaadin/component-base": ["@vaadin/component-base@24.9.17", "", { "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", "@polymer/polymer": "^3.0.0", "@vaadin/vaadin-development-mode-detector": "^2.0.0", "@vaadin/vaadin-usage-statistics": "^2.1.0", "lit": "^3.0.0" } }, "sha512-nmE1G9bjpaHiEvrUhOaPk0SEHq3OY8lVsimYnqLHwZ9g1DlNzN7YoCZvBy0A4dC8+X9+RKWo7+AK3mbXqawr4w=="], + + "@vaadin/field-base": ["@vaadin/field-base@24.9.17", "", { "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", "@polymer/polymer": "^3.0.0", "@vaadin/a11y-base": "~24.9.17", "@vaadin/component-base": "~24.9.17", "lit": "^3.0.0" } }, "sha512-aVda8LDkAzQ7AbFLbKMTvfMlCgrqkC7wPsqjXIASkWV2tIhWQqbnKabGlpf/yopuIUx8l+q90um0Yga5oQ9kqw=="], + + "@vaadin/grid": ["@vaadin/grid@24.9.17", "", { "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", "@polymer/polymer": "^3.0.0", "@vaadin/a11y-base": "~24.9.17", "@vaadin/checkbox": "~24.9.17", "@vaadin/component-base": "~24.9.17", "@vaadin/lit-renderer": "~24.9.17", "@vaadin/text-field": "~24.9.17", "@vaadin/vaadin-lumo-styles": "~24.9.17", "@vaadin/vaadin-material-styles": "~24.9.17", "@vaadin/vaadin-themable-mixin": "~24.9.17", "lit": "^3.0.0" } }, "sha512-vZXy9jaqtnugZyZ3XdL43i1E53dVktMEOcg7ZA4IW/KoIeoiy5ioJp+X4JaPCfElY3+rAr0EWIGOcWHrF/dWXg=="], + + "@vaadin/icon": ["@vaadin/icon@24.9.17", "", { "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", "@polymer/polymer": "^3.0.0", "@vaadin/component-base": "~24.9.17", "@vaadin/vaadin-lumo-styles": "~24.9.17", "@vaadin/vaadin-themable-mixin": "~24.9.17", "lit": "^3.0.0" } }, "sha512-yxFBv6SdCGehbhYz5OAbiVeA83dW8eL2oxdeJu7VbyTztxCdnGPioSQN63K9IGVY0dmLlmVNnpA8Li18UgEAdQ=="], + + "@vaadin/input-container": ["@vaadin/input-container@24.9.17", "", { "dependencies": { "@polymer/polymer": "^3.0.0", "@vaadin/component-base": "~24.9.17", "@vaadin/vaadin-lumo-styles": "~24.9.17", "@vaadin/vaadin-material-styles": "~24.9.17", "@vaadin/vaadin-themable-mixin": "~24.9.17", "lit": "^3.0.0" } }, "sha512-h4zfY4HmNQ9b61e0F/BMCMPkkNcDN2vyVjH64DFQcYwhT42Eay+lGfRo675wj+O1sqvouQ4KjPZS8MsFeVlGHA=="], + + "@vaadin/lit-renderer": ["@vaadin/lit-renderer@24.9.17", "", { "dependencies": { "lit": "^3.0.0" } }, "sha512-la43FB3cF5zVtphr8rQas/6AUq8Q/SqR4sYp8jrnXWPGIc8nBqWTMPcIAsJkm6f2SacWc69ymr6GCV0nZQ1QWA=="], + + "@vaadin/text-field": ["@vaadin/text-field@24.9.17", "", { "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", "@polymer/polymer": "^3.0.0", "@vaadin/a11y-base": "~24.9.17", "@vaadin/component-base": "~24.9.17", "@vaadin/field-base": "~24.9.17", "@vaadin/input-container": "~24.9.17", "@vaadin/vaadin-lumo-styles": "~24.9.17", "@vaadin/vaadin-material-styles": "~24.9.17", "@vaadin/vaadin-themable-mixin": "~24.9.17", "lit": "^3.0.0" } }, "sha512-WM1luEbUOYZ94EPFhabc37HtMJmCymABw7QuVSnIMa0txZ0UIX77Nr/fJclwB9JUwMTOEf9Sx5UftqqftYg7OA=="], + + "@vaadin/vaadin-development-mode-detector": ["@vaadin/vaadin-development-mode-detector@2.0.7", "", {}, "sha512-9FhVhr0ynSR3X2ao+vaIEttcNU5XfzCbxtmYOV8uIRnUCtNgbvMOIcyGBvntsX9I5kvIP2dV3cFAOG9SILJzEA=="], + + "@vaadin/vaadin-lumo-styles": ["@vaadin/vaadin-lumo-styles@24.9.17", "", { "dependencies": { "@polymer/polymer": "^3.0.0", "@vaadin/component-base": "~24.9.17", "@vaadin/icon": "~24.9.17", "@vaadin/vaadin-themable-mixin": "~24.9.17" } }, "sha512-5Fjhku/jkopiyBTDNukJMV4j3WwzBN+YUeNK88zxBqr+Qd/61tIzUz5mvH6FLjjKE003FYC9/SlIFfWRzPzIrA=="], + + "@vaadin/vaadin-material-styles": ["@vaadin/vaadin-material-styles@24.9.17", "", { "dependencies": { "@polymer/polymer": "^3.0.0", "@vaadin/component-base": "~24.9.17", "@vaadin/vaadin-themable-mixin": "~24.9.17" } }, "sha512-FCwaV9j7Ox2AR0hpvcZgClMzhZicQku5xIv0TBm99Be2FVQpfo/KAowkoAYQoDpmrXNzpByamG3qF4hvDW451w=="], + + "@vaadin/vaadin-themable-mixin": ["@vaadin/vaadin-themable-mixin@24.9.17", "", { "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", "lit": "^3.0.0" } }, "sha512-3DpdPhhxVjpb3+91YhrfTcmRHn3XM2gMdXWmtSUwLZnUj2zyprJsAL5TK3/MSqVFuPaOwaUWqZXWsk809Ohd7w=="], + + "@vaadin/vaadin-usage-statistics": ["@vaadin/vaadin-usage-statistics@2.1.3", "", { "dependencies": { "@vaadin/vaadin-development-mode-detector": "^2.0.0" } }, "sha512-8r4TNknD7OJQADe3VygeofFR7UNAXZ2/jjBFP5dgI8+2uMfnuGYgbuHivasKr9WSQ64sPej6m8rDoM1uSllXjQ=="], + "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.11.0", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.27", "@swc/core": "^1.12.11" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7" } }, "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w=="], "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], + "@webcomponents/shadycss": ["@webcomponents/shadycss@1.11.2", "", {}, "sha512-vRq+GniJAYSBmTRnhCYPAPq6THYqovJ/gzGThWbgEZUQaBccndGTi1hdiUP15HzEco0I6t4RCtXyX0rsSmwgPw=="], + "@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@1.3.2", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA=="], "@wry/caches": ["@wry/caches@1.0.1", "", { "dependencies": { "tslib": "^2.3.0" } }, "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA=="], @@ -1068,8 +1327,12 @@ "@wry/trie": ["@wry/trie@0.5.0", "", { "dependencies": { "tslib": "^2.3.0" } }, "sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA=="], + "@zip.js/zip.js": ["@zip.js/zip.js@2.8.57", "", {}, "sha512-cd5a0s7CS0MOD52f7FW4ju84wc/+b+YNu5XIG72PfXRLk6rInEdvIrB/BjR8D8hS4fMKuYJOBjv6FoLh3e7DHw=="], + "JSONStream": ["JSONStream@1.3.5", "", { "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" }, "bin": { "JSONStream": "./bin.js" } }, "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ=="], + "a5-js": ["a5-js@0.7.3", "", { "dependencies": { "gl-matrix": "^3.4.3" } }, "sha512-3aoMwHmNkyuMDHS4q6GRRInpOawamen2pokIbc0MQmR9cqG0Y9+B0bZpzswwetjrSG2ckbYtShH+nKru6+3O5Q=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], @@ -1096,6 +1359,10 @@ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + "anynum": ["anynum@1.0.1", "", {}, "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A=="], + + "apache-arrow": ["apache-arrow@21.2.0", "", { "dependencies": { "@types/node": "^25.2.0", "flatbuffers": "^25.1.24", "json-with-bigint": "^3.5.3", "tslib": "^2.6.2" }, "bin": { "arrow2csv": "bin/arrow2csv.js" } }, "sha512-Hxe6Agq26gQOM954qpzYSllJBPJl+e16U5CkfuMUhLrNba+5nKkttIVlflaovN6oaTratqMGAO8H5u/aNhmHWQ=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "argv-formatter": ["argv-formatter@1.0.0", "", {}, "sha512-F2+Hkm9xFaRg+GkaNnbwXNDV5O6pnCFEmqyhvfC/Ic5LbgOWjJh3L+mN/s91rxVL3znE7DYVpW0GJFT+4YBgWw=="], @@ -1152,8 +1419,14 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="], + + "browserify-zlib": ["browserify-zlib@0.2.0", "", { "dependencies": { "pako": "~1.0.5" } }, "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA=="], + "browserslist": ["browserslist@4.28.8", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="], + "buf-compare": ["buf-compare@1.0.1", "", {}, "sha512-Bvx4xH00qweepGc43xFvMs5BKASXTbHaHm6+kDYIK9p/4iFwjATQkmPKHQSgJZzKbAymhztRbXUf1Nqhzl73/Q=="], + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], @@ -1184,6 +1457,8 @@ "caniuse-lite": ["caniuse-lite@1.0.30001809", "", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="], + "cartocolor": ["cartocolor@5.0.2", "", { "dependencies": { "colorbrewer": "1.5.6" } }, "sha512-Ihb/wU5V6BVbHwapd8l/zg7bnhZ4YPFVfa7quSpL86lfkPJSf4YuNBT+EvesPRP5vSqhl6vZVsQJwCR8alBooQ=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "chalkercli": ["chalkercli-v2@github:TurtIeSocks/chalkercli#2322333", { "dependencies": { "chalk": "^2.3.2", "gradient-string": "^1.1.0", "meow": "^13.0.0" }, "bin": "./cli.js" }, "TurtIeSocks-chalkercli-2322333", "sha512-CF8gq40DSpVof0edtC3yz43IWFEp4odiAgCQk7l1AqAAZbNz4wtUnuhZuyAGnxEsJJbKXgZ3BlN+kBpharMijw=="], @@ -1192,6 +1467,8 @@ "chardet": ["chardet@2.2.0", "", {}, "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA=="], + "charenc": ["charenc@0.0.2", "", {}, "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA=="], + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "chunk-data": ["chunk-data@0.1.0", "", {}, "sha512-zFyPtyC0SZ6Zu79b9sOYtXZcgrsXe0RpePrzRyj52hYVFG1+Rk6rBqjjOEk+GNQwc3PIX+86teQMok970pod1g=="], @@ -1222,10 +1499,16 @@ "code-point-at": ["code-point-at@1.1.0", "", {}, "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA=="], + "color": ["color@5.0.3", "", { "dependencies": { "color-convert": "^3.1.3", "color-string": "^2.1.3" } }, "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "color-string": ["color-string@2.1.4", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg=="], + + "colorbrewer": ["colorbrewer@1.5.6", "", {}, "sha512-fONg2pGXyID8zNgKHBlagW8sb/AMShGzj4rRJfz5biZ7iuHQZYquSCLE/Co1oSQFmt/vvwjyezJCejQl7FG/tg=="], + "colorette": ["colorette@2.0.19", "", {}, "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ=="], "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], @@ -1234,6 +1517,8 @@ "compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="], + "composed-offset-position": ["composed-offset-position@0.0.6", "", { "peerDependencies": { "@floating-ui/utils": "^0.2.5" } }, "sha512-Q7dLompI6lUwd7LWyIcP66r4WcS9u7AL2h8HaeipiRfCRPLMWqRx8fYsjb4OHi6UQFifO7XtNC2IlEJ1ozIFxw=="], + "compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="], "compression": ["compression@1.8.1", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w=="], @@ -1274,6 +1559,8 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "core-assert": ["core-assert@0.2.1", "", { "dependencies": { "buf-compare": "^1.0.0", "is-error": "^2.2.0" } }, "sha512-IG97qShIP+nrJCXMCgkNZgH7jZQ4n8RpPyPeXX++T6avR/KhLhgLiHKoEn5Rc1KjfycSfA9DMa6m+4C4eguHhw=="], + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], @@ -1288,30 +1575,76 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "crypt": ["crypt@0.0.2", "", {}, "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow=="], + + "crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="], + "crypto-random-string": ["crypto-random-string@4.0.0", "", { "dependencies": { "type-fest": "^1.0.1" } }, "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA=="], "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + "cssfilter": ["cssfilter@0.0.10", "", {}, "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "cz-conventional-changelog": ["cz-conventional-changelog@3.3.0", "", { "dependencies": { "chalk": "^2.4.1", "commitizen": "^4.0.3", "conventional-commit-types": "^3.0.0", "lodash.map": "^4.5.1", "longest": "^2.0.1", "word-wrap": "^1.0.3" }, "optionalDependencies": { "@commitlint/load": ">6.1.1" } }, "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw=="], "cz-conventional-commit": ["cz-conventional-commit@1.0.6", "", { "dependencies": { "chalk": "^2.4.1", "commitizen": "^4.0.3", "lodash.map": "^4.5.1", "longest": "^2.0.1", "right-pad": "^1.0.1", "word-wrap": "^1.0.3" }, "optionalDependencies": { "@commitlint/load": ">6.1.1" } }, "sha512-E/mzC3hHGQo36oyFlGfOTI01S1Cq9Zg2Y5kcZvMN+w6Uv3ZlACaTk8Z3iXCnxoT2i2KB7wEGDZpgPIAkNQNF8Q=="], + "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], + + "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], + + "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], + + "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], + + "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + + "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], + + "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], + + "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], + + "d3-hexbin": ["d3-hexbin@0.2.2", "", {}, "sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w=="], + + "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], + + "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], + + "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], + + "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], + + "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], @@ -1320,6 +1653,16 @@ "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], + + "d3-voronoi-map": ["d3-voronoi-map@2.1.1", "", { "dependencies": { "d3-dispatch": "2.*", "d3-polygon": "2.*", "d3-timer": "2.*", "d3-weighted-voronoi": "1.*" } }, "sha512-mCXfz/kD9IQxjHaU2IMjkO8fSo4J6oysPR2iL+omDsCy1i1Qn6BQ/e4hEAW8C6ms2kfuHwqtbNom80Hih94YsA=="], + + "d3-voronoi-treemap": ["d3-voronoi-treemap@1.1.2", "", { "dependencies": { "d3-voronoi-map": "2.*" } }, "sha512-7odu9HdG/yLPWwzDteJq4yd9Q/NwgQV7IE/u36VQtcCK7k1sZwDqbkHCeMKNTBsq5mQjDwolTsrXcU0j8ZEMCA=="], + + "d3-weighted-voronoi": ["d3-weighted-voronoi@1.1.3", "", { "dependencies": { "d3-array": "2", "d3-polygon": "2" } }, "sha512-C3WdvSKl9aqhAy+f3QT3PPsQG6V+ajDfYO3BSclQDSD+araW2xDBFIH67aKzsSuuuKaX8K2y2dGq1fq/dWTVig=="], + + "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + "dargs": ["dargs@8.1.0", "", {}, "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw=="], "date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="], @@ -1334,14 +1677,20 @@ "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + "deck.gl": ["deck.gl@9.3.10", "", { "dependencies": { "@deck.gl/aggregation-layers": "9.3.10", "@deck.gl/arcgis": "9.3.10", "@deck.gl/carto": "9.3.10", "@deck.gl/core": "9.3.10", "@deck.gl/extensions": "9.3.10", "@deck.gl/geo-layers": "9.3.10", "@deck.gl/google-maps": "9.3.10", "@deck.gl/json": "9.3.10", "@deck.gl/layers": "9.3.10", "@deck.gl/mapbox": "9.3.10", "@deck.gl/mesh-layers": "9.3.10", "@deck.gl/react": "9.3.10", "@deck.gl/widgets": "9.3.10", "@loaders.gl/core": "^4.4.3", "@luma.gl/core": "^9.3.5", "@luma.gl/engine": "^9.3.5" }, "peerDependencies": { "@arcgis/core": "^4.0.0", "react": ">=16.3.0", "react-dom": ">=16.3.0" }, "optionalPeers": ["@arcgis/core", "react", "react-dom"] }, "sha512-iy+qGha/xj7isGwzF0eoDnF/VwzHQXyMIBb2pl3TBb8FXqzvheA/h4IKnsd7F4lji1SEeQ0A5kyqPmKnqTMgfQ=="], + "decompress-response": ["decompress-response@10.0.0", "", { "dependencies": { "mimic-response": "^4.0.0" } }, "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q=="], "dedent": ["dedent@0.7.0", "", {}, "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA=="], + "deep-equal": ["deep-equal@1.1.2", "", { "dependencies": { "is-arguments": "^1.1.1", "is-date-object": "^1.0.5", "is-regex": "^1.1.4", "object-is": "^1.1.5", "object-keys": "^1.1.1", "regexp.prototype.flags": "^1.5.1" } }, "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg=="], + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "deep-strict-equal": ["deep-strict-equal@0.2.0", "", { "dependencies": { "core-assert": "^0.2.0" } }, "sha512-3daSWyvZ/zwJvuMGlzG1O+Ow0YSadGfb3jsh9xoCutv2tWyB9dA4YvR9L9/fSdDZa2dByYQe+TqapSGUrjnkoA=="], + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], "default-browser": ["default-browser@5.5.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw=="], @@ -1354,8 +1703,12 @@ "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "del": ["del@6.1.1", "", { "dependencies": { "globby": "^11.0.1", "graceful-fs": "^4.2.4", "is-glob": "^4.0.1", "is-path-cwd": "^2.2.0", "is-path-inside": "^3.0.2", "p-map": "^4.0.0", "rimraf": "^3.0.2", "slash": "^3.0.0" } }, "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg=="], + "delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="], + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], @@ -1374,6 +1727,8 @@ "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "dfa": ["dfa@1.2.0", "", {}, "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="], + "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], @@ -1394,10 +1749,14 @@ "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "draco3d": ["draco3d@1.5.7", "", {}, "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "duplexer2": ["duplexer2@0.1.4", "", { "dependencies": { "readable-stream": "^2.0.2" } }, "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA=="], + "earcut": ["earcut@2.2.4", "", {}, "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], @@ -1460,6 +1819,8 @@ "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + "esri-loader": ["esri-loader@3.7.0", "", {}, "sha512-cB1Sw9EQjtW4mtT7eFBjn/6VaaIWNTjmTd2asnnEyuZk1xVSFRMCfLZSBSjZM7ZarDcVu5WIjOP0t0MYVu4hVQ=="], + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], @@ -1494,10 +1855,16 @@ "fast-uri": ["fast-uri@3.1.6", "", {}, "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q=="], + "fast-xml-builder": ["fast-xml-builder@1.3.1", "", { "dependencies": { "path-expression-matcher": "^1.6.2", "xml-naming": "^0.3.0" } }, "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug=="], + + "fast-xml-parser": ["fast-xml-parser@5.11.0", "", { "dependencies": { "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", "is-unsafe": "^2.0.0", "path-expression-matcher": "^1.6.2", "strnum": "^2.4.2", "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-9IGxMqvqLOnqP+Egi1nqDHKv5k8aZ7r9n558enxcucmyVGEBNPAU+MOg/8jPIS7rO7sSq4gFm1/nHtiaubMruw=="], + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fflate": ["fflate@0.7.4", "", {}, "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw=="], + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], @@ -1524,8 +1891,14 @@ "flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="], + "flatbuffers": ["flatbuffers@25.9.23", "", {}, "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ=="], + + "flatpickr": ["flatpickr@4.6.13", "", {}, "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw=="], + "flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="], + "focus-trap": ["focus-trap@7.8.0", "", { "dependencies": { "tabbable": "^6.4.0" } }, "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA=="], + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], @@ -1544,6 +1917,8 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + "fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="], "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], @@ -1580,6 +1955,8 @@ "git-raw-commits": ["git-raw-commits@4.0.0", "", { "dependencies": { "dargs": "^8.0.0", "meow": "^12.0.1", "split2": "^4.0.0" }, "bin": { "git-raw-commits": "cli.mjs" } }, "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ=="], + "gl-matrix": ["gl-matrix@3.4.4", "", {}, "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ=="], + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -1612,6 +1989,8 @@ "graphql-type-json": ["graphql-type-json@0.3.2", "", { "peerDependencies": { "graphql": ">=0.8.0" } }, "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg=="], + "h3-js": ["h3-js@4.5.0", "", {}, "sha512-uKmdPc+DuarnR+XqZxEuWOMw7KzzKROrx3MLeJpFnMOs78S9M5eZ+X5RieS9UcSFQqbeXzbfWuX/W9Pyajinqw=="], + "handlebars": ["handlebars@4.7.9", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ=="], "happy-dom": ["happy-dom@20.11.6", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-Hldbg8AdAa5a5oDcZpjqnGitp7JB0hqWmfv/8qr+kft4vzSD8BHsbdRfzYvL/0QcbKcURC/yyoygbeDQarPvYg=="], @@ -1674,6 +2053,8 @@ "ignore-by-default": ["ignore-by-default@1.0.1", "", {}, "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="], + "image-size": ["image-size@0.7.5", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g=="], + "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], @@ -1700,6 +2081,8 @@ "inquirer": ["inquirer@8.2.7", "", { "dependencies": { "@inquirer/external-editor": "^1.0.0", "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", "wrap-ansi": "^6.0.1" } }, "sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA=="], + "interactjs": ["interactjs@1.10.28", "", { "dependencies": { "@interactjs/types": "1.10.28" } }, "sha512-QCa4ksTPd2p/FZ4I6hrH6fogjLqru1TxYywRiKYNuLZtrlAxTIYUa/MYsE7PnUgWZJU3SrGVDjiz065BeaXYng=="], + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], "interpret": ["interpret@2.2.0", "", {}, "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw=="], @@ -1710,16 +2093,24 @@ "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="], + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + "is-buffer": ["is-buffer@1.1.6", "", {}, "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="], + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + "is-error": ["is-error@2.2.2", "", {}, "sha512-IOQqts/aHWbiisY5DuPJQ0gcbvaLFCa7fBa9xoLfxBZvQ+ZI/Zh9xoI7Gk+G64N0FdK4AbibytHht2tWgpJWLg=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-finite": ["is-finite@1.1.0", "", {}, "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w=="], @@ -1750,6 +2141,8 @@ "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + "is-regexp": ["is-regexp@3.1.0", "", {}, "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA=="], "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], @@ -1760,6 +2153,8 @@ "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + "is-unsafe": ["is-unsafe@2.0.2", "", {}, "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ=="], + "is-utf8": ["is-utf8@0.2.1", "", {}, "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q=="], "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], @@ -1780,10 +2175,14 @@ "jose": ["jose@6.2.10", "", {}, "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g=="], + "jpeg-exif": ["jpeg-exif@1.1.4", "", {}, "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="], + "jsep": ["jsep@0.3.5", "", {}, "sha512-AoRLBDc6JNnKjNcmonituEABS5bcfqDhQAWWXNTFrqu6nVXBpBAGfcoTGZMFlIrh9FjmE1CQyX9CTNwZrXMMDA=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], @@ -1798,8 +2197,12 @@ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "json-stringify-pretty-compact": ["json-stringify-pretty-compact@4.0.0", "", {}, "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q=="], + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + "json-with-bigint": ["json-with-bigint@3.5.12", "", {}, "sha512-uwbF/wSSuOgC7qqlq27Xp5B6a2MHVug3t0idZdTqu0JnlFvgJuH7ju+KAk/J06C7GfhoYy2gnb9wz2INqcne7w=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], @@ -1816,6 +2219,8 @@ "knex": ["knex@3.1.0", "", { "dependencies": { "colorette": "2.0.19", "commander": "^10.0.0", "debug": "4.3.4", "escalade": "^3.1.1", "esm": "^3.2.25", "get-package-type": "^0.1.0", "getopts": "2.3.0", "interpret": "^2.2.0", "lodash": "^4.17.21", "pg-connection-string": "2.6.2", "rechoir": "^0.8.0", "resolve-from": "^5.0.0", "tarn": "^3.0.2", "tildify": "2.0.0" }, "bin": { "knex": "bin/cli.js" } }, "sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw=="], + "ktx-parse": ["ktx-parse@0.7.1", "", {}, "sha512-FeA3g56ksdFNwjXJJsc1CCc7co+AJYDp6ipIp878zZ2bU8kWROatLYf39TQEd4/XRSUvBXovQ8gaVKWPXsCLEQ=="], + "latest-version": ["latest-version@2.0.0", "", { "dependencies": { "package-json": "^2.0.0" } }, "sha512-8925wFYLfWBciewimt0VmDyYw0GFCRcbFSTrZGt4JgQ7lh5jb/kodMlUt0uMaxXdRKVi+7F3ib30N7fTv83ikw=="], "leaflet": ["leaflet@1.9.4", "", {}, "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="], @@ -1856,6 +2261,12 @@ "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + "lit": ["lit@3.3.3", "", { "dependencies": { "@lit/reactive-element": "^2.1.0", "lit-element": "^4.2.0", "lit-html": "^3.3.0" } }, "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw=="], + + "lit-element": ["lit-element@4.2.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0", "@lit/reactive-element": "^2.1.0", "lit-html": "^3.3.0" } }, "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w=="], + + "lit-html": ["lit-html@3.3.3", "", { "dependencies": { "@types/trusted-types": "^2.0.2" } }, "sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA=="], + "load-json-file": ["load-json-file@4.0.0", "", { "dependencies": { "graceful-fs": "^4.1.2", "parse-json": "^4.0.0", "pify": "^3.0.0", "strip-bom": "^3.0.0" } }, "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw=="], "localforage": ["localforage@1.10.0", "", { "dependencies": { "lie": "3.1.1" } }, "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg=="], @@ -1914,22 +2325,32 @@ "lucide-react": ["lucide-react@1.33.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-MTRwMy0ZlL8Ur/vOAiJ9XGHE+kFPC7brq6MxAm0GiGXEBj0qy0jA/pG4N675oSzciO/UCdX8T+5yUQdmDeTLxg=="], + "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="], + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + "lz4js": ["lz4js@0.2.0", "", {}, "sha512-gY2Ia9Lm7Ep8qMiuGRhvUq0Q7qUereeldZPP1PMEJxPtEWHJLqw9pgX68oHajBH0nzJK4MaZEA/YNV3jT8u8Bg=="], + "magic-bytes.js": ["magic-bytes.js@1.13.1", "", {}, "sha512-x5sn4UX2k5gCWlcfmoFwG4TPie8+dctESyqOBdhB5p6MsgWXdBKGmt9nXPObj/JI50TTL928lc5Yt1WntMn1bw=="], "magic-string": ["magic-string@0.30.8", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ=="], + "maplibre-gl": ["maplibre-gl@6.5.0", "", { "dependencies": { "@mapbox/point-geometry": "^1.1.0", "@mapbox/tiny-sdf": "^2.2.0", "@mapbox/unitbezier": "^1.0.0", "@mapbox/vector-tile": "^3.0.0", "@maplibre/geojson-vt": "^6.1.1", "@maplibre/maplibre-gl-style-spec": "^26.2.1", "@maplibre/mlt": "^1.2.0", "@maplibre/vt-pbf": "^4.3.2", "@types/geojson": "^7946.0.16", "earcut": "^3.2.3", "gl-matrix": "^3.4.4", "kdbush": "^4.1.0", "murmurhash-js": "^1.0.0", "pbf": "^5.1.2", "potpack": "^2.1.0", "quickselect": "^3.0.0", "tinyqueue": "^3.0.0" } }, "sha512-kVStPz9Rw/ATjWV5tQ3iCR0tY+viz16Nh3E14iZNlBj0HloMAzFaDNtFYqPGkZSFRnv56txMh3ImjR0g6oClTw=="], + "marked": ["marked@9.1.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q=="], "marked-terminal": ["marked-terminal@6.3.0", "", { "dependencies": { "ansi-escapes": "^6.2.0", "chalk": "^5.3.0", "cli-highlight": "^2.1.11", "cli-table3": "^0.6.3", "node-emoji": "^2.1.3", "supports-hyperlinks": "^3.0.0" }, "peerDependencies": { "marked": ">=1 <13" } }, "sha512-icQT4cpNHOC3uxYr0DjZx8yYbFVnb40dzbvbqe9G2CRpFPnCe/RjfPXzP3xKm2vrd8sw4Sqsc3O+IsoPMzE/Iw=="], + "markerjs2": ["markerjs2@2.32.7", "", {}, "sha512-HeFRZjmc43DOG3lSQp92z49cq2oCYpYn2pX++SkJAW1Dij4xJtRquVRf+cXeSZQWDX3ufns1Ry/bGk+zveP7rA=="], + "math-float64-exponent": ["math-float64-exponent@1.0.0", "", { "dependencies": { "math-float64-get-high-word": "^1.0.0" } }, "sha512-IC+IUir6tLqFeGAxap2842rGpYWl4UBItglxDSPCjJTh1vD6Tbuap+aaL+k1LzMxVm9PrXpKTb9uhTqLJRR35w=="], "math-float64-get-high-word": ["math-float64-get-high-word@1.0.0", "", { "dependencies": { "utils-is-little-endian": "^1.0.0" } }, "sha512-kSmcZEyDx/mS9Zahx5kKU/eVggrSvN2MZ/CLTgk7N0we41vqnpFUpoAeNMW5kj+1dJu4EhnMdgiP9P1xiwr7ug=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "md5": ["md5@2.3.0", "", { "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", "is-buffer": "~1.1.6" } }, "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g=="], + "media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], "meow": ["meow@13.2.0", "", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="], @@ -1942,6 +2363,8 @@ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "meshoptimizer": ["meshoptimizer@1.2.0", "", {}, "sha512-davRZeIJbxJrE24cwQle7ZDsxjdk/OphNOV83oX+efQinyoHY9Jcyz3MHbaoG0qySZajldGztNZ1RN/T19PZsg=="], + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], @@ -1964,6 +2387,8 @@ "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "mjolnir.js": ["mjolnir.js@3.1.0", "", {}, "sha512-fWcei0lo+xauQK0CVb5vwC8FaS9xeEBiNPd5vMIM89DchQAi6ENMq9itOjq+G8zAvhKmJu5UPnuEnU8kVrHzdg=="], + "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], @@ -1974,6 +2399,8 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "murmurhash-js": ["murmurhash-js@1.0.0", "", {}, "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw=="], + "mute-stream": ["mute-stream@0.0.8", "", {}, "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="], "mysql2": ["mysql2@3.11.0", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.6.3", "long": "^5.2.1", "lru-cache": "^8.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA=="], @@ -2024,6 +2451,10 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + "object-is": ["object-is@1.1.6", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" } }, "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + "object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="], "objection": ["objection@3.1.5", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^2.1.1", "db-errors": "^0.2.3" }, "peerDependencies": { "knex": ">=1.0.1" } }, "sha512-Hx/ipAwXSuRBbOMWFKtRsAN0yITafqXtWB4OT4Z9wED7ty1h7bOnBdhLtcNus23GwLJqcMsRWdodL2p5GwlnfQ=="], @@ -2104,6 +2535,8 @@ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "path-expression-matcher": ["path-expression-matcher@1.6.2", "", {}, "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ=="], + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -2122,6 +2555,8 @@ "pbf": ["pbf@3.3.0", "", { "dependencies": { "ieee754": "^1.1.12", "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q=="], + "pdfmake": ["pdfmake@0.2.23", "", { "dependencies": { "@foliojs-fork/linebreak": "^1.1.2", "@foliojs-fork/pdfkit": "^0.15.3", "iconv-lite": "^0.7.1", "xmldoc": "^2.0.3" } }, "sha512-A/IksoKb/ikOZH1edSDJ/2zBbqJKDghD4+fXn3rT7quvCJDlsZMs3NmIB3eajLMMFU9Bd3bZPVvlUMXhvFI+bQ=="], + "pg-connection-string": ["pg-connection-string@2.6.2", "", {}, "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -2138,18 +2573,26 @@ "pkginfo": ["pkginfo@0.3.1", "", {}, "sha512-yO5feByMzAp96LtP58wvPKSbaKAi/1C4kV9XpTctr6EepnP6F33RBNOiVrdz9BrPA98U2BMFsTNHo44TWcbQ2A=="], + "png-js": ["png-js@1.1.0", "", { "dependencies": { "browserify-zlib": "^0.2.0" } }, "sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q=="], + "pogo-data-generator": ["pogo-data-generator@1.21.2", "", { "dependencies": { "@na-ji/pogo-protos": "<3.0.0", "jszip": "^3.10.1", "node-fetch": "2.6.7" } }, "sha512-hc5ZAYSonPAuhIYgTV/6RzWYm8ZYF2iW3vdU4xO2BJxwu3vNLxn9SZ3Zd5oCe2fzjiUdDmQHog4Rj6BO9yRXAA=="], "point-in-polygon-hao": ["point-in-polygon-hao@1.2.4", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ=="], + "polylabel": ["polylabel@1.1.0", "", { "dependencies": { "tinyqueue": "^2.0.3" } }, "sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA=="], + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], "postcss-selector-parser": ["postcss-selector-parser@7.1.5", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw=="], + "potpack": ["potpack@2.1.0", "", {}, "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ=="], + "powershell-utils": ["powershell-utils@0.2.0", "", {}, "sha512-ZlsFlG7MtSFCoc5xreOvBAozCJ6Pf06opgJjh9ONEv418xpZSAzNjstD36C6+JwOnfSqOW/9uDkqKjezTdxZhw=="], + "preact": ["preact@10.29.8", "", { "peerDependencies": { "preact-render-to-string": ">=5" }, "optionalPeers": ["preact-render-to-string"] }, "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], @@ -2180,11 +2623,13 @@ "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], + "quadbin": ["quadbin@0.4.2", "", { "dependencies": { "@math.gl/web-mercator": "^4.1.0" } }, "sha512-1NFzjFVM23Um51/ttD6lFDqGtUHNS5Ky1slZHk3YPwMbC+7Jl3ULLb4QvDo6+Nerv8b8SgUV+ysOhziUh4B5cQ=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], - "quickselect": ["quickselect@2.0.0", "", {}, "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw=="], + "quickselect": ["quickselect@3.0.0", "", {}, "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g=="], "radix-ui": ["radix-ui@1.6.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-accessible-icon": "1.1.15", "@radix-ui/react-accordion": "1.2.20", "@radix-ui/react-alert-dialog": "1.1.23", "@radix-ui/react-arrow": "1.1.15", "@radix-ui/react-aspect-ratio": "1.1.15", "@radix-ui/react-avatar": "1.2.6", "@radix-ui/react-checkbox": "1.3.11", "@radix-ui/react-collapsible": "1.1.20", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-context-menu": "2.3.7", "@radix-ui/react-dialog": "1.1.23", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-dropdown-menu": "2.1.24", "@radix-ui/react-focus-guards": "1.1.6", "@radix-ui/react-focus-scope": "1.1.16", "@radix-ui/react-form": "0.1.16", "@radix-ui/react-hover-card": "1.1.23", "@radix-ui/react-label": "2.1.15", "@radix-ui/react-menu": "2.1.24", "@radix-ui/react-menubar": "1.1.24", "@radix-ui/react-navigation-menu": "1.2.22", "@radix-ui/react-one-time-password-field": "0.1.16", "@radix-ui/react-password-toggle-field": "0.1.11", "@radix-ui/react-popover": "1.1.23", "@radix-ui/react-popper": "1.3.7", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-progress": "1.1.16", "@radix-ui/react-radio-group": "1.4.7", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-scroll-area": "1.2.18", "@radix-ui/react-select": "2.3.7", "@radix-ui/react-separator": "1.1.15", "@radix-ui/react-slider": "1.4.7", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-switch": "1.3.7", "@radix-ui/react-tabs": "1.1.21", "@radix-ui/react-toast": "1.2.23", "@radix-ui/react-toggle": "1.1.18", "@radix-ui/react-toggle-group": "1.1.19", "@radix-ui/react-toolbar": "1.1.19", "@radix-ui/react-tooltip": "1.2.16", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-escape-keydown": "1.1.5", "@radix-ui/react-use-is-hydrated": "0.1.3", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-use-size": "1.1.4", "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QBdhh1arIEUvPC0dQ5+nwWAxt7+N+oP/9jPwjJkGFoSk/sqxg32gJtSXGtFh8frAIcS6oC9cx2Q+7KYCQLOAeA=="], @@ -2246,6 +2691,8 @@ "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + "registry-auth-token": ["registry-auth-token@5.1.1", "", { "dependencies": { "@pnpm/npm-conf": "^3.0.2" } }, "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q=="], "registry-url": ["registry-url@3.1.0", "", { "dependencies": { "rc": "^1.0.1" } }, "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA=="], @@ -2302,14 +2749,20 @@ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "seedrandom": ["seedrandom@3.0.5", "", {}, "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="], + "semantic-release": ["semantic-release@22.0.12", "", { "dependencies": { "@semantic-release/commit-analyzer": "^11.0.0", "@semantic-release/error": "^4.0.0", "@semantic-release/github": "^9.0.0", "@semantic-release/npm": "^11.0.0", "@semantic-release/release-notes-generator": "^12.0.0", "aggregate-error": "^5.0.0", "cosmiconfig": "^8.0.0", "debug": "^4.0.0", "env-ci": "^10.0.0", "execa": "^8.0.0", "figures": "^6.0.0", "find-versions": "^5.1.0", "get-stream": "^6.0.0", "git-log-parser": "^1.2.0", "hook-std": "^3.0.0", "hosted-git-info": "^7.0.0", "import-from-esm": "^1.3.1", "lodash-es": "^4.17.21", "marked": "^9.0.0", "marked-terminal": "^6.0.0", "micromatch": "^4.0.2", "p-each-series": "^3.0.0", "p-reduce": "^3.0.0", "read-pkg-up": "^11.0.0", "resolve-from": "^5.0.0", "semver": "^7.3.2", "semver-diff": "^4.0.0", "signale": "^1.2.1", "yargs": "^17.5.1" }, "bin": { "semantic-release": "bin/semantic-release.js" } }, "sha512-0mhiCR/4sZb00RVFJIUlMuiBkW3NMpVIW2Gse7noqEMoFGkvfPPAImEQbkBV8xga4KOPP4FdTRYuLLy32R1fPw=="], "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], @@ -2326,6 +2779,8 @@ "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], @@ -2366,10 +2821,14 @@ "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + "snappyjs": ["snappyjs@0.6.1", "", {}, "sha512-YIK6I2lsH072UE0aOFxxY1dPDCS43I5ktqHpeAsuLNYWkE5pGxRGWfDM4/vSUfNzXjC1Ivzt3qx31PCLmc9yqg=="], + "socks": ["socks@2.8.9", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw=="], "sonner": ["sonner@2.0.8", "", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg=="], + "sortablejs": ["sortablejs@1.15.7", "", {}, "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A=="], + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -2386,6 +2845,8 @@ "split2": ["split2@1.0.0", "", { "dependencies": { "through2": "~2.0.0" } }, "sha512-NKywug4u4pX/AZBB1FCPzZ6/7O+Xhz1qMVbzTvvKvikjO99oPN87SkK08mEY9P63/5lWjK+wgOOgApnTg5r6qg=="], + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], @@ -2416,6 +2877,8 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "strnum": ["strnum@2.4.2", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw=="], + "stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], "suncalc": ["suncalc@1.9.0", "", {}, "sha512-vMJ8Byp1uIPoj+wb9c1AdK4jpkSKVAywgHX0lqY7zt6+EWRRC3Z+0Ucfjy/0yxTVO1hwwchZe4uoFNqrIC24+A=="], @@ -2428,12 +2891,16 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "svg-arc-to-cubic-bezier": ["svg-arc-to-cubic-bezier@3.2.0", "", {}, "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g=="], + "sweepline-intersections": ["sweepline-intersections@1.5.0", "", { "dependencies": { "tinyqueue": "^2.0.0" } }, "sha512-AoVmx72QHpKtItPu72TzFL+kcYjd67BPLDoR0LarIk+xyaRg+pDTMFXndIEvZf9xEKnJv6JdhgRMnocoG0D3AQ=="], "symbol-observable": ["symbol-observable@4.0.0", "", {}, "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ=="], "systeminformation": ["systeminformation@5.33.1", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-DEN6ICHk3Tk0Uf/hrAHh7xlt7iL5CJFBtPZinA0H62DrGG/KPKqq/Nzj6lCXPS4Ay/sf/14zNnk9LpqKzBIc+w=="], + "tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="], + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], @@ -2454,6 +2921,8 @@ "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], + "texture-compressor": ["texture-compressor@1.0.2", "", { "dependencies": { "argparse": "^1.0.10", "image-size": "^0.7.4" }, "bin": { "texture-compressor": "./bin/texture-compressor.js" } }, "sha512-dStVgoaQ11mA5htJ+RzZ51ZxIZqNOgWKAIvtjLrW1AliQQLCmrDqNzQZ8Jh91YealQ95DXt4MEduLzJmbs6lig=="], + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], @@ -2464,6 +2933,10 @@ "tildify": ["tildify@2.0.0", "", {}, "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw=="], + "timezone-groups": ["timezone-groups@0.10.4", "", {}, "sha512-AnkJYrbb7uPkDCEqGeVJiawZNiwVlSkkeX4jZg1gTEguClhyX+/Ezn07KB6DT29tG3UN418ldmS/W6KqGOTDjg=="], + + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], @@ -2474,7 +2947,7 @@ "tinygradient": ["tinygradient@0.4.3", "", { "dependencies": { "@types/tinycolor2": "^1.4.0", "tinycolor2": "^1.0.0" } }, "sha512-tBPYQSs6eWukzzAITBSmqcOwZCKACvRa/XjPPh1mj4mnx4G3Drm51HxyCTU/TKnY8kG4hmTe5QlOh9O82aNtJQ=="], - "tinyqueue": ["tinyqueue@2.0.3", "", {}, "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="], + "tinyqueue": ["tinyqueue@3.0.0", "", {}, "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g=="], "to-buffer": ["to-buffer@1.2.2", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw=="], @@ -2528,6 +3001,10 @@ "unicode-emoji-modifier-base": ["unicode-emoji-modifier-base@1.0.0", "", {}, "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g=="], + "unicode-properties": ["unicode-properties@1.4.1", "", { "dependencies": { "base64-js": "^1.3.0", "unicode-trie": "^2.0.0" } }, "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg=="], + + "unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="], + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], "unique-string": ["unique-string@3.0.0", "", { "dependencies": { "crypto-random-string": "^4.0.0" } }, "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ=="], @@ -2630,6 +3107,12 @@ "xdg-basedir": ["xdg-basedir@2.0.0", "", { "dependencies": { "os-homedir": "^1.0.0" } }, "sha512-NF1pPn594TaRSUO/HARoB4jK8I+rWgcpVlpQCK6/6o5PHyLUt2CSiDrpUZbQ6rROck+W2EwF8mBJcTs+W98J9w=="], + "xml-naming": ["xml-naming@0.3.0", "", {}, "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ=="], + + "xmldoc": ["xmldoc@2.0.3", "", { "dependencies": { "sax": "^1.4.3" } }, "sha512-6gRk4NY/Jvg67xn7OzJuxLRsGgiXBaPUQplVJ/9l99uIugxh4FTOewYz5ic8WScj7Xx/2WvhENiQKwkK9RpE4w=="], + + "xss": ["xss@1.0.13", "", { "dependencies": { "commander": "^2.20.3", "cssfilter": "0.0.10" }, "bin": { "xss": "bin/xss" } }, "sha512-clu7dxTm1e8Mo5fz3n/oW3UCXBfV89xZ72jM8yzo1vR/pIS0w3sgB3XV2H8Vm6zfGnHL0FzvLJPJEBhd86/z4Q=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -2656,10 +3139,14 @@ "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "zstd-codec": ["zstd-codec@0.1.5", "", {}, "sha512-v3fyjpK8S/dpY/X5WxqTK3IoCnp/ZOLxn144GZVlNUjtwAchzrVo03h+oMATFhCIiJ5KTr4V3vDQQYz4RU684g=="], + "zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], "@apollo/server/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + "@arcgis/core/marked": ["marked@16.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], @@ -2670,6 +3157,8 @@ "@base-ui/utils/reselect": ["reselect@5.3.0", "", {}, "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg=="], + "@carto/api-client/jsep": ["jsep@1.4.0", "", {}, "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw=="], + "@commitlint/format/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "@commitlint/load/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -2680,6 +3169,8 @@ "@commitlint/types/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "@deck.gl/geo-layers/long": ["long@3.2.0", "", {}, "sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg=="], + "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], @@ -2700,6 +3191,10 @@ "@eslint/eslintrc/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "@foliojs-fork/fontkit/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + + "@foliojs-fork/linebreak/base64-js": ["base64-js@1.3.1", "", {}, "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="], + "@graphql-tools/import/@graphql-tools/utils": ["@graphql-tools/utils@12.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-aMdIo/l+8j4lhamWAf+MWHr+lOW8zArSBKXspqtKHgt5I2JF9LS55KDLUe/L9AiJOV/O6qJJ60hgYydbnJHbxg=="], "@graphql-tools/merge/@graphql-tools/utils": ["@graphql-tools/utils@12.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-aMdIo/l+8j4lhamWAf+MWHr+lOW8zArSBKXspqtKHgt5I2JF9LS55KDLUe/L9AiJOV/O6qJJ60hgYydbnJHbxg=="], @@ -2714,6 +3209,14 @@ "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "@loaders.gl/3d-tiles/long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "@loaders.gl/gis/@mapbox/vector-tile": ["@mapbox/vector-tile@1.3.1", "", { "dependencies": { "@mapbox/point-geometry": "~0.1.0" } }, "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw=="], + + "@mapbox/vector-tile/pbf": ["pbf@5.1.2", "", { "dependencies": { "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w=="], + + "@maplibre/vt-pbf/pbf": ["pbf@5.1.2", "", { "dependencies": { "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w=="], + "@modelcontextprotocol/sdk/ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "@modelcontextprotocol/sdk/express-rate-limit": ["express-rate-limit@8.6.2", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A=="], @@ -2776,10 +3279,32 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@turf/boolean-clockwise/@turf/helpers": ["@turf/helpers@5.1.5", "", {}, "sha512-/lF+JR+qNDHZ8bF9d+Cp58nxtZWJ3sqFe6n3u3Vpj+/0cqkjk4nXKYBSY0azm+GIYB5mWKxUXvuP/m0ZnKj1bw=="], + + "@turf/boolean-clockwise/@turf/invariant": ["@turf/invariant@5.2.0", "", { "dependencies": { "@turf/helpers": "^5.1.5" } }, "sha512-28RCBGvCYsajVkw2EydpzLdcYyhSA77LovuOvgCJplJWaNVyJYH6BOR3HR9w50MEkPqb/Vc/jdo6I6ermlRtQA=="], + + "@turf/clone/@turf/helpers": ["@turf/helpers@5.1.5", "", {}, "sha512-/lF+JR+qNDHZ8bF9d+Cp58nxtZWJ3sqFe6n3u3Vpj+/0cqkjk4nXKYBSY0azm+GIYB5mWKxUXvuP/m0ZnKj1bw=="], + + "@turf/rewind/@turf/helpers": ["@turf/helpers@5.1.5", "", {}, "sha512-/lF+JR+qNDHZ8bF9d+Cp58nxtZWJ3sqFe6n3u3Vpj+/0cqkjk4nXKYBSY0azm+GIYB5mWKxUXvuP/m0ZnKj1bw=="], + + "@turf/rewind/@turf/invariant": ["@turf/invariant@5.2.0", "", { "dependencies": { "@turf/helpers": "^5.1.5" } }, "sha512-28RCBGvCYsajVkw2EydpzLdcYyhSA77LovuOvgCJplJWaNVyJYH6BOR3HR9w50MEkPqb/Vc/jdo6I6ermlRtQA=="], + + "@turf/rewind/@turf/meta": ["@turf/meta@5.2.0", "", { "dependencies": { "@turf/helpers": "^5.1.5" } }, "sha512-ZjQ3Ii62X9FjnK4hhdsbT+64AYRpaI8XMBMcyftEOGSmPMUVnkbvuv3C9geuElAXfQU7Zk1oWGOcrGOD9zr78Q=="], + + "@types/brotli/@types/node": ["@types/node@25.9.5", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg=="], + + "@types/d3/@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-sankey/@types/d3-shape": ["@types/d3-shape@1.3.12", "", { "dependencies": { "@types/d3-path": "^1" } }, "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q=="], + + "@types/d3-scale/@types/d3-time": ["@types/d3-time@2.1.4", "", {}, "sha512-BTfLsxTeo7yFxI/haOOf1ZwJ6xKgQLT9dCp+EcmQv87Gox6X+oKl4mLKfO6fnWm3P22+A6DknMNEZany8ql2Rw=="], + "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "apache-arrow/@types/node": ["@types/node@25.9.5", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg=="], + "babel-plugin-macros/cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -2802,6 +3327,10 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "color/color-convert": ["color-convert@3.1.3", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg=="], + + "color-string/color-name": ["color-name@2.1.1", "", {}, "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg=="], + "commitizen/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -2830,6 +3359,24 @@ "cz-conventional-commit/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + + "d3-dsv/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], + + "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], + + "d3-voronoi-map/d3-dispatch": ["d3-dispatch@2.0.0", "", {}, "sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA=="], + + "d3-voronoi-map/d3-polygon": ["d3-polygon@2.0.0", "", {}, "sha512-MsexrCK38cTGermELs0cO1d79DcTsQRN7IWMJKczD/2kBjzNXxLUWP33qRF6VDpiLV/4EI4r6Gs0DAWQkE8pSQ=="], + + "d3-voronoi-map/d3-timer": ["d3-timer@2.0.0", "", {}, "sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA=="], + + "d3-weighted-voronoi/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], + + "d3-weighted-voronoi/d3-polygon": ["d3-polygon@2.0.0", "", {}, "sha512-MsexrCK38cTGermELs0cO1d79DcTsQRN7IWMJKczD/2kBjzNXxLUWP33qRF6VDpiLV/4EI4r6Gs0DAWQkE8pSQ=="], + "debounce-fn/mimic-fn": ["mimic-fn@3.1.0", "", {}, "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ=="], "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], @@ -2910,6 +3457,10 @@ "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], + "maplibre-gl/earcut": ["earcut@3.2.3", "", {}, "sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q=="], + + "maplibre-gl/pbf": ["pbf@5.1.2", "", { "dependencies": { "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w=="], + "marked-terminal/ansi-escapes": ["ansi-escapes@6.2.1", "", {}, "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig=="], "marked-terminal/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -3304,6 +3855,8 @@ "pogo-data-generator/node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="], + "polylabel/tinyqueue": ["tinyqueue@2.0.3", "", {}, "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="], + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], @@ -3314,6 +3867,8 @@ "protobufjs/long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "rbush/quickselect": ["quickselect@2.0.0", "", {}, "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw=="], + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], "react-day-picker/date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="], @@ -3358,22 +3913,30 @@ "supports-hyperlinks/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "sweepline-intersections/tinyqueue": ["tinyqueue@2.0.3", "", {}, "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="], + "tempy/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], "tempy/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], + "texture-compressor/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "to-buffer/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], "tsconfig-paths/strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], "type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], + "unixify/normalize-path": ["normalize-path@2.1.1", "", { "dependencies": { "remove-trailing-separator": "^1.0.1" } }, "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w=="], "update-notifier/chalk": ["chalk@1.1.3", "", { "dependencies": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", "has-ansi": "^2.0.0", "strip-ansi": "^3.0.0", "supports-color": "^2.0.0" } }, "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A=="], "update-notifier/semver-diff": ["semver-diff@2.1.0", "", { "dependencies": { "semver": "^5.0.3" } }, "sha512-gL8F8L4ORwsS0+iQ34yCYv///jsOq0ZL7WP55d1HnJ32o7tyFYEFQZQA22mrLIacZdU6xecaBBZ+uEiffGNyXw=="], + "victory-vendor/@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + "vite-plugin-checker/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "vscode-languageclient/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], @@ -3384,6 +3947,8 @@ "wsl-utils/powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + "xss/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "@commitlint/top-level/find-up/locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="], @@ -3406,6 +3971,8 @@ "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "@loaders.gl/gis/@mapbox/vector-tile/@mapbox/point-geometry": ["@mapbox/point-geometry@0.1.0", "", {}, "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ=="], + "@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], "@octokit/plugin-throttling/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], @@ -3470,6 +4037,12 @@ "@sentry/cli/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "@types/brotli/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + + "@types/d3-sankey/@types/d3-shape/@types/d3-path": ["@types/d3-path@1.0.11", "", {}, "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw=="], + + "apache-arrow/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + "babel-plugin-macros/cosmiconfig/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], "boxen/chalk/ansi-styles": ["ansi-styles@2.2.1", "", {}, "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA=="], @@ -3492,6 +4065,8 @@ "cli-highlight/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + "color/color-convert/color-name": ["color-name@2.1.1", "", {}, "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg=="], + "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -3500,6 +4075,12 @@ "cz-conventional-commit/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + "d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], + + "d3-weighted-voronoi/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + "env-ci/execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], "env-ci/execa/human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], diff --git a/docs/superpowers/plans/2026-08-24-map-engine.md b/docs/superpowers/plans/2026-08-24-map-engine.md new file mode 100644 index 000000000..0d4fbece5 --- /dev/null +++ b/docs/superpowers/plans/2026-08-24-map-engine.md @@ -0,0 +1,336 @@ +# Map Engine Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the placeholder at `/map` with a real map: MapLibre owning the basemap and camera, deck.gl riding it in interleaved mode, an atlas pipeline that composites marker icons off the main thread, and text rendered as layer data rather than as thousands of React components. + +**Architecture:** MapLibre GL owns the canvas. deck.gl attaches through `MapboxOverlay` in interleaved mode so markers sit beneath street labels. Marker icons are composited on an `OffscreenCanvas`, keyed by the combination that determines their appearance, and cached under an LRU bound. Layer data arrives through a source interface that this plan backs with fixtures; session 3 implements the real transport behind the same interface. + +**Tech Stack:** maplibre-gl 6.5.0, deck.gl 9.3.10, supercluster 8.0.1 (already present), React 19, TypeScript strict. + +## Assumptions + +Five calls made while writing this plan, each stated because a reader could expect otherwise. + +1. **Data comes from fixtures behind an interface, not from a server.** The 2.0 client has no marker data source today; nothing in `app/` fetches map data at all. The spec places transport in session 3 and says plans 1 to 4 do not depend on it, which can only mean the engine is built against an interface and fed fixtures. This plan defines that interface. Session 3 implements it. Getting the interface shape wrong is the main risk here, so it stays deliberately small. +2. **Two layers, not five.** Pokémon, because thousands per viewport is the actual hard problem and the reason for this rewrite, and gyms as a plain `IconLayer` to prove the pattern generalises without the text and clustering machinery. Routes, S2 cells and interaction ranges follow the same shape and land with the features that need them, against entity shapes session 3 defines rather than shapes invented here. +3. **The basemap defaults to a keyless vector source and raster keeps working.** Requiring every self-hoster to get a MapTiler key would be a regression from today's configurable raster `tileServers`. MapLibre renders raster natively, so existing entries keep working through the same config field. +4. **1.0's map is untouched.** `src/` keeps Leaflet, react-leaflet and every marker component. Nothing here changes what current users are served, and the per-user shell flag stays off. +5. **No timers are ported.** The measured "reallllly choppy" behaviour is thousands of React components each re-rendering on a 1 Hz interval. Moving text into `TextLayer` data eliminates that structurally rather than optimising it, so there is no per-marker timer component to port and none should be written. + +## Global Constraints + +- Nothing under `src/` or `server/` changes. +- `app/` is strict TypeScript with `exactOptionalPropertyTypes`. The vendored shadcn components are patched to satisfy it; new code must satisfy it too. +- Exactly ONE dark mechanism, the `prefers-color-scheme` media query. No `.dark` class variant; it has been stripped from this branch three times. +- Map entity colours come from `app/tokens/data-palette.css` and never from the brand accent. That file is a language about map data and a test enforces its separation, including by literal value. +- Any new colour pair needs its WCAG contrast computed in both themes. No gate in this repo can see contrast; a green suite proves nothing about it. +- Component tests scope queries to their own render container and clean up after each test. +- Do not add a `preload` to `bunfig.toml`. It is process-wide and broke three Node-side suites. +- Prose in commits carries no em dashes, no bold, no inline bulleted headers, and never refers to the maintainer by name. +- The pre-commit hook runs `biome check` and `tsc -p tsconfig.app.json --noEmit` and blocks on either. + +--- + +## File Structure + +``` +app/ + map/ + types.ts the source interface, entity shapes, viewport + source.ts fixture-backed implementation of that interface + fixtures.ts generated entities, enough to exercise the count problem + atlas.ts composite key, OffscreenCanvas draw, LRU cache + atlas.test.ts keying and eviction, the silent-failure surface + useMapLibre.ts map instance, camera, deep-link sync + layers.ts deck.gl layer construction from source data + MapCanvas.tsx the map itself, overlay attached + Popup.tsx the single React popup, positioned from a picked coordinate + pages/ + MapPage.tsx renders MapCanvas instead of a placeholder +``` + +--- + +## Task 1: The source interface and its fixtures + +**Files:** +- Create: `app/map/types.ts`, `app/map/fixtures.ts`, `app/map/source.ts` +- Test: `app/map/source.test.ts` + +**Interfaces:** +- Produces: `MapSource`, consumed by every later task and implemented for real in session 3. + +This comes first because everything else depends on its shape, and because getting it wrong is the expensive mistake in this plan. + +- [ ] **Step 1: Write the failing test** + +Create `app/map/source.test.ts`: + +```ts +import { expect, test } from 'bun:test' +import { createFixtureSource } from './source' + +test('returns only entities inside the requested bounds', async () => { + const source = createFixtureSource() + const inside = await source.query({ + kind: 'pokemon', + bounds: { west: -0.1, south: 51.5, east: 0.1, north: 51.6 }, + zoom: 15, + }) + expect(inside.length).toBeGreaterThan(0) + for (const entity of inside) { + expect(entity.lon).toBeGreaterThanOrEqual(-0.1) + expect(entity.lon).toBeLessThanOrEqual(0.1) + expect(entity.lat).toBeGreaterThanOrEqual(51.5) + expect(entity.lat).toBeLessThanOrEqual(51.6) + } +}) + +test('produces enough pokemon to exercise the count problem', async () => { + const source = createFixtureSource() + const all = await source.query({ + kind: 'pokemon', + bounds: { west: -1, south: 51, east: 1, north: 52 }, + zoom: 12, + }) + expect(all.length).toBeGreaterThanOrEqual(3000) +}) +``` + +- [ ] **Step 2: Run it to verify it fails** + +```bash +bun test app/map/source.test.ts +``` + +Expected: FAIL, cannot find `./source`. + +- [ ] **Step 3: Define the types** + +`app/map/types.ts` holds a `Bounds`, a `MapQuery` carrying kind, bounds and zoom, an entity union, and: + +```ts +export interface MapSource { + query(request: MapQuery): Promise +} +``` + +The pokemon entity needs whatever the atlas keys on: id, form, costume, gender, plus lat, lon, an expiry timestamp, and optional iv, level and size. The gym entity needs id, lat, lon, team and an in-battle flag. Keep both minimal. Every field added here is a field session 3 has to honour. + +- [ ] **Step 4: Write the fixtures and the source** + +Generate deterministically, seeded, so tests and visual checks are reproducible. Three thousand pokemon is the floor the test asserts, since the whole point of this engine is the count problem and a fixture set of thirty proves nothing. + +- [ ] **Step 5: Run the tests to verify they pass** + +Expected: PASS, 2 tests. + +- [ ] **Step 6: Commit** + +```bash +git add app/map/ && git commit -m "feat(map): define the source interface and its fixtures" +``` + +--- + +## Task 2: The atlas pipeline + +**Files:** +- Create: `app/map/atlas.ts` +- Test: `app/map/atlas.test.ts` + +**Interfaces:** +- Consumes: the pokemon entity from Task 1. +- Produces: `getIconFor(entity)` returning what `IconLayer`'s `getIcon` needs. + +The spec calls this out as one of four things that genuinely need tests, because both failure modes are silent: a colliding key renders the wrong icon, an over-specific key destroys the hit rate. + +- [ ] **Step 1: Write the failing test** + +Cover the two silent failures directly. Two entities differing in any appearance-determining field must produce different keys. Two entities differing only in a field that does not affect appearance, such as position or expiry, must produce the SAME key. Then assert the cache evicts under its bound rather than growing without limit. + +- [ ] **Step 2: Run it to verify it fails** + +Expected: FAIL, cannot find `./atlas`. + +- [ ] **Step 3: Implement the key and the cache** + +The key is the combination that determines appearance: **`pokemonId` (the species number), NOT `id`**, plus form, costume, gender, badge, background and weather. + +That distinction is the whole task. `PokemonEntity` carries both an `id`, which is unique per spawn, +and a `pokemonId`, which is the species. Keying on `id` produces a cache entry per marker, so the +cache never hits once, the atlas repacks constantly, and the rendering goes exactly as choppy as the +Leaflet version this rewrite exists to replace. Every test would still pass. Read the field names in +`app/map/types.ts` rather than trusting this sentence. The cache is LRU with an explicit bound. `OffscreenCanvas` is not available in the test environment, so structure the module so the key function and the cache are testable without it and the drawing is injected. That separation is the point: keying is what has silent failure modes, and drawing is what needs a browser. + +- [ ] **Step 4: Run the tests to verify they pass** + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add app/map/ && git commit -m "feat(map): composite marker icons behind an lru cache" +``` + +--- + +## Task 3: MapLibre basemap and camera + +**Files:** +- Create: `app/map/useMapLibre.ts`, `app/map/MapCanvas.tsx` +- Modify: `app/pages/MapPage.tsx`, `package.json` + +- [ ] **Step 1: Install** + +```bash +bun add maplibre-gl +``` + +Verified at 6.5.0. Note `[install] exact = true`; do not add carets by hand. + +- [ ] **Step 2: Render a map** + +`MapCanvas` mounts a MapLibre instance into a container sized to the viewport minus the bottom nav. Default to a keyless vector style. Import MapLibre's stylesheet; without it the canvas renders but every control is unstyled and positioning breaks. + +- [ ] **Step 3: Wire the deep link** + +`/@/:lat/:lon/:zoom` already exists in 1.0 and those links are in the wild. The 2.0 route table owns `/map`; decide deliberately how a deep link reaches it and say what you chose. Camera changes should update the URL without pushing a history entry per frame. + +- [ ] **Step 4: Confirm it builds and the route still lazy-loads** + +```bash +bun run build && ls dist/ | grep -c MapPage +``` + +Expected: non-zero, and MapLibre must land in the map route's chunk or a shared vendor chunk, NOT in the entry. The whole point of lazy routes is that a visitor to the hub does not download a mapping library. Check which chunk grew. + +- [ ] **Step 5: Commit** + +```bash +git add app/ package.json bun.lock && git commit -m "feat(map): render the basemap and sync the camera" +``` + +--- + +## Task 4: The deck.gl overlay, interleaved + +**Files:** +- Create: `app/map/layers.ts` +- Modify: `app/map/MapCanvas.tsx`, `package.json` + +- [ ] **Step 1: Install** + +```bash +bun add deck.gl @deck.gl/mapbox @deck.gl/layers @deck.gl/core +``` + +All verified at 9.3.10. + +- [ ] **Step 2: Attach the overlay** + +`MapboxOverlay` with `interleaved: true`. Interleaved is not cosmetic: it is what puts markers beneath street labels instead of over them, and it is the reason this rides MapLibre rather than replacing it. + +- [ ] **Step 3: Build the pokemon and gym layers** + +`IconLayer` for both, `TextLayer` for pokemon labels. Icons come from Task 2's atlas. Gym colours come from the data palette's team tokens, read at runtime; those tokens ship only because their block is marked static, and a value read here that resolves to nothing renders a colourless marker without erroring. + +- [ ] **Step 4: Verify the interleaving actually happened** + +A screenshot is not available, so assert on the overlay's configuration and say plainly that visual confirmation is outstanding. Do not claim markers sit under labels without having seen it. + +- [ ] **Step 5: Run everything and commit** + +```bash +bun test && bun run typecheck && bun run lint && bun run build +``` + +--- + +## Task 5: Picking and the single popup + +**Files:** +- Create: `app/map/Popup.tsx` +- Modify: `app/map/MapCanvas.tsx` + +- [ ] **Step 1: Wire picking** + +deck.gl's picking replaces marker refs and Leaflet's popup events. One popup exists at a time, positioned by projecting the picked coordinate through the camera. + +- [ ] **Step 2: Keep it correct while the camera moves** + +A popup anchored to a coordinate must follow that coordinate on pan and zoom. Decide whether it reprojects per frame or rides a MapLibre marker, and say which and why. + +- [ ] **Step 3: Test what is testable without a GPU** + +Picking needs a real WebGL context. Test the popup component itself with a fixed projected position, and the selection state transitions. State plainly what needs a browser. + +- [ ] **Step 4: Commit** + +--- + +## Task 6: Clustering, and fix `forcedLimit` + +**Files:** +- Modify: `app/map/layers.ts` +- Test: `app/map/clustering.test.ts` + +- [ ] **Step 1: Write the failing test** + +The spec records a real bug: `forcedLimit` is supposed to bound how many markers render, and today it builds a clusterer that declines to cluster above `maxZoom`, so the bound does not bind. Read the 1.0 implementation in `src/` first to understand what it does, then write a test asserting the count actually stays under the limit at high zoom with more entities than the limit. + +- [ ] **Step 2: Run it to verify it fails against the ported behaviour** + +If it passes immediately, you have ported the bug rather than the behaviour, or the test does not reach the path. Investigate before proceeding. + +- [ ] **Step 3: Fix it and make the test pass** + +- [ ] **Step 4: Commit** + +--- + +## Task 7: WebGL context loss + +**Files:** +- Modify: `app/map/MapCanvas.tsx`, `app/map/atlas.ts` + +New surface, most likely to bite on iOS under memory pressure, and invisible until it happens to a user. + +- [ ] **Step 1: Handle the events** + +`webglcontextlost` and `webglcontextrestored`. On loss, show a visible restoring state rather than a blank canvas. On restore, rebuild layers and re-warm the atlas cache. + +- [ ] **Step 2: Test the state machine, not the GPU** + +Context loss can be simulated by dispatching the events. Assert the restoring state appears and clears, and that the atlas is asked to rebuild. Do not attempt to actually lose a real context in a test. + +- [ ] **Step 3: Run everything and commit** + +--- + +## Done criteria + +```bash +bun test +bun run typecheck +bun run lint +bun run build +``` + +All four succeed. Then: + +- `/map` renders a real basemap with markers drawn from fixtures. +- MapLibre and deck.gl are in the map route's chunk or a vendor chunk, not the entry chunk. +- The atlas produces the same key for entities that look the same and different keys for entities that do not, and evicts under its bound. +- Clustering keeps the rendered count under `forcedLimit` at high zoom. +- `dist/index.html` and `dist/app.html` still load different stylesheets with `@layer base` absent from the 1.0 one. +- Nothing under `src/` or `server/` changed. + +## What this plan does not do + +- No real data. Every entity comes from fixtures behind the source interface. Session 3 implements the transport. +- Only pokemon and gyms. Routes, S2 cells, scan areas, submission rings and interaction ranges land with their features. +- No filters. Which entities are requested is Plan 5's question, and the rules model behind it is session 2's. +- No 1.0 changes. Leaflet stays exactly where it is. +- No visual confirmation. Nothing here has been seen rendered; every claim is structural until someone opens it in a browser. diff --git a/package.json b/package.json index 1e160f68d..3feb60ace 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,9 @@ "@apollo/server": "5.5.1", "@as-integrations/express5": "1.1.2", "@base-ui/react": "1.7.0", + "@deck.gl/core": "9.3.10", + "@deck.gl/layers": "9.3.10", + "@deck.gl/mapbox": "9.3.10", "@emotion/react": "11.14.0", "@emotion/styled": "11.14.1", "@fontsource-variable/fredoka": "5.3.0", @@ -134,6 +137,7 @@ "cors": "^2.8.6", "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", + "deck.gl": "9.3.10", "discord.js": "^14.26.4", "dlv": "^1.1.3", "dotenv": "^16.3.1", @@ -159,6 +163,7 @@ "lodash": "^4.18.1", "long": "^4.0.0", "lucide-react": "1.33.0", + "maplibre-gl": "6.5.0", "moment-timezone": "^0.6.2", "mysql2": "3.11.0", "node-cache": "^5.1.2", diff --git a/vite.config.js b/vite.config.js index 036c33322..e060cfcfa 100644 --- a/vite.config.js +++ b/vite.config.js @@ -168,6 +168,52 @@ const viteConfig = defineConfig(({ mode }) => { ], output: { manualChunks: (id) => { + // MapLibre is only ever imported from app/map, behind the /map + // route's own lazy() call, so it needs a bucket of its own. + // Grouping it into the same 'vendor'/'index' buckets as + // everything else would put it behind app.html's unconditional + // modulepreload of vendor (every 2.0 route) or, for its CSS, + // behind index.html's stylesheet (every 1.0 route too) - + // either way a visitor who never opens /map would pay for it. + if (id.includes('node_modules/maplibre-gl')) return 'maplibre' + // deck.gl is the same story: only ever reached from app/map, + // behind /map's lazy() call. Left ungrouped it falls into the + // generic 'vendor' check below, which app.html preloads on + // every 2.0 route - confirmed by build output before this rule + // existed, where deck.gl's ~1.4MB landed in vendor and pushed + // every route's preload past 2.2MB, not just /map's. + // deck.gl's rendering engine ships under separate npm scopes, so + // matching only its own name left luma.gl, math.gl and loaders.gl + // falling through to the generic vendor bucket that every entry + // preloads, the 1.0 hub included. Measured at the time: 414 kB + // raw and 118 kB gzipped of WebGL engine in front of visitors who + // never open the map. + // + // Enumerating scopes is fragile, and this is the fourth thing to + // leak through this rule after tailwind's preflight, the font + // faces and maplibre's stylesheet. The durable fix is to stop + // bucketing by path prefix and let rollup place modules by which + // entry reaches them, but dropping the vendor catch-all changes + // the chunk 1.0 users are served, so that wants its own change + // with its own verification rather than riding along here. + if ( + id.includes('node_modules/@deck.gl') || + id.includes('node_modules/deck.gl') || + id.includes('node_modules/@luma.gl') || + id.includes('node_modules/@math.gl') || + id.includes('node_modules/@loaders.gl') || + // deck.gl's gesture, logging and text dependencies sit under + // scopes of their own again. Enumerating is how this rule keeps + // failing: the fifth leak shipped in the same commit whose + // comment predicted a fifth. Measured at the time, 43,773 bytes + // of mjolnir.js, probe.gl and tiny-sdf were reaching every 1.0 + // visitor through the vendor catch-all below. + id.includes('node_modules/mjolnir.js') || + id.includes('node_modules/@probe.gl') || + id.includes('node_modules/@mapbox/tiny-sdf') + ) { + return 'deckgl' + } if (id.endsWith('.css')) { return id.startsWith(appDir) ? 'app' : 'index' }