Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions web/src/layers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@ 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';

/**
* 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<D>`
* and erases the data type to `unknown` for the shared array (no `any`).
*/
export const LAYERS: WeatherLayer<any>[] = [precip, cape, alerts, wind, temp];
export const LAYERS: WeatherLayer<unknown>[] = [
defineLayer(precip),
defineLayer(cape),
defineLayer(alerts),
defineLayer(wind),
defineLayer(temp),
];
16 changes: 4 additions & 12 deletions web/src/layers/temp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 });
Expand Down
15 changes: 15 additions & 0 deletions web/src/layers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,18 @@ export interface WeatherLayer<D = unknown> {
/** 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<D>` is invariant in `D` (it both produces `D` via `select` and
* consumes it via `build`), so a `WeatherLayer<SpecificData>` is not directly
* assignable to a shared `WeatherLayer<unknown>[]`. This helper type-checks each
* layer as its own `WeatherLayer<D>` (so `select`→`build` stays sound per layer)
* and erases only the registry's view — far tighter than typing the array
* `WeatherLayer<any>[]`, 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<D>(layer: WeatherLayer<D>): WeatherLayer<unknown> {
return layer as WeatherLayer<unknown>;
}
12 changes: 12 additions & 0 deletions web/src/weather.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { FeatureCollection, Geometry, Point } from './generated/geojson';
import type {
AlertProps,
CapeTexIndex,
CityForecast,
CityTileIndex,
LatticeForecast,
RefcTexIndex,
Expand Down Expand Up @@ -37,6 +38,17 @@ export type LatticeFc = FeatureCollection<Point, LatticeForecast> & {
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<Point, CityForecast> & {
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 '—';
Expand Down