diff --git a/web/src/layers/index.ts b/web/src/layers/index.ts index 69f9033..52037f1 100644 --- a/web/src/layers/index.ts +++ b/web/src/layers/index.ts @@ -2,7 +2,7 @@ import { alerts } from './alerts'; import { cape } from './cape'; import { precip } from './precip'; import { temp } from './temp'; -import type { WeatherLayer } from './types'; +import { defineLayer, type WeatherLayer } from './types'; import { wind } from './wind'; export type { LayerCtx, WeatherLayer } from './types'; @@ -10,6 +10,14 @@ export type { LayerCtx, WeatherLayer } from './types'; /** * The layer registry. Array order is paint order (the filled rasters at the * bottom, vector/point layers on top). Adding a layer is a one-line change here - * plus its module — `App` never has to learn the layer exists. + * plus its module — `App` never has to learn the layer exists. Each layer is + * wrapped in `defineLayer`, which type-checks it as its own `WeatherLayer` + * and erases the data type to `unknown` for the shared array (no `any`). */ -export const LAYERS: WeatherLayer[] = [precip, cape, alerts, wind, temp]; +export const LAYERS: WeatherLayer[] = [ + defineLayer(precip), + defineLayer(cape), + defineLayer(alerts), + defineLayer(wind), + defineLayer(temp), +]; diff --git a/web/src/layers/temp.tsx b/web/src/layers/temp.tsx index 4411594..ccddd8e 100644 --- a/web/src/layers/temp.tsx +++ b/web/src/layers/temp.tsx @@ -6,8 +6,8 @@ import { import { TileLayer } from '@deck.gl/geo-layers'; import { TextLayer } from '@deck.gl/layers'; import { GRID_ZOOM_SPLIT, WEATHER_BASE } from '../config'; -import type { CityForecast, CityTileIndex } from '../generated/weather'; -import { age, type LatticeFc } from '../weather'; +import type { CityTileIndex } from '../generated/weather'; +import { age, type CityTileFc, type LatticeFc } from '../weather'; import { Swatch } from './swatch'; import type { WeatherLayer } from './types'; @@ -47,20 +47,12 @@ interface TempData { lattice: LatticeFc | null; } -interface CityTile { - hours: number[]; - features: { - geometry: { coordinates: [number, number] }; - properties: CityForecast; - }[]; -} - /** Flatten a city tile into one record per (city, forecast hour). */ -function explodeCities(tile: CityTile | null): TempRecord[] { +function explodeCities(tile: CityTileFc | null): TempRecord[] { if (!tile) return []; const out: TempRecord[] = []; for (const f of tile.features) { - const position = f.geometry.coordinates; + const position = f.geometry.coordinates as [number, number]; const { name, t } = f.properties; for (let k = 0; k < t.length; k++) { out.push({ position, hour: tile.hours[k], temp: t[k], name }); diff --git a/web/src/layers/types.ts b/web/src/layers/types.ts index 13efe1b..85d345d 100644 --- a/web/src/layers/types.ts +++ b/web/src/layers/types.ts @@ -44,3 +44,18 @@ export interface WeatherLayer { /** Optional tooltip for a picked object belonging to this layer. */ tooltip?: (object: any) => string | null; } + +/** + * Register a layer into the registry, erasing its data type to `unknown`. + * + * `WeatherLayer` is invariant in `D` (it both produces `D` via `select` and + * consumes it via `build`), so a `WeatherLayer` is not directly + * assignable to a shared `WeatherLayer[]`. This helper type-checks each + * layer as its own `WeatherLayer` (so `select`→`build` stays sound per layer) + * and erases only the registry's view — far tighter than typing the array + * `WeatherLayer[]`, which would accept a malformed layer. `App` only ever + * pairs a layer's own `select` with its own `build`, so the erasure is safe. + */ +export function defineLayer(layer: WeatherLayer): WeatherLayer { + return layer as WeatherLayer; +} diff --git a/web/src/weather.ts b/web/src/weather.ts index a31e874..1ae722d 100644 --- a/web/src/weather.ts +++ b/web/src/weather.ts @@ -10,6 +10,7 @@ import type { FeatureCollection, Geometry, Point } from './generated/geojson'; import type { AlertProps, CapeTexIndex, + CityForecast, CityTileIndex, LatticeForecast, RefcTexIndex, @@ -37,6 +38,17 @@ export type LatticeFc = FeatureCollection & { hours: number[]; }; +/** A single point-forecast city tile (the `citytile/{snapshotMs}/{z}/{x}/{y}` + * pyramid): a FeatureCollection of GFS-sampled cities, each carrying a + * temperature series, plus the snapshot + hour axis. Same envelope as the + * lattice, but per-tile rather than global — and built from the generated + * `CityForecast`/`Point` so it tracks the Rust contract instead of being + * hand-declared at the use site. */ +export type CityTileFc = FeatureCollection & { + snapshotMs: number; + hours: number[]; +}; + /** Human "N min ago" for a feed's `generated_ms` (or model snapshot) time. */ export function age(ms?: number): string { if (!ms) return '—';