/** * Pure logic for the global (Open-Meteo Flood / GloFAS v4) river path. * * GloFAS discharge is modeled per ~0.05-degree grid cell, and a cell that * misses the river channel reports local runoff rather than the river — the * live probe behind this module found 0.63 m³/s at 35.125,-90.075 and * 11,640 m³/s one cell west at 35.125,-90.125, both nominally "Memphis". * Everything here exists to pick the right cell and then describe it * honestly. No I/O, no service calls — only the response type is imported. * * See docs/plans/global-rivers-plan.md D3 (channel snapping) and D4 (presentation). */ import type { OpenMeteoFloodResponse } from '../types/openmeteo.js'; /** Probe offsets in degrees — one GloFAS cell pitch either side of center. */ export declare const PROBE_OFFSETS_DEG: readonly [-0.05, 0, 0.05]; /** * Index of the requested (center) point in the array returned by * `buildProbeGrid`. The grid is emitted in a stable latitude-major order, so * the middle offset in both axes always lands at index 4. */ export declare const PROBE_GRID_CENTER_INDEX = 4; /** Below this mean discharge the winning cell is not a real river. */ export declare const MINOR_DRAINAGE_THRESHOLD_CMS = 0.1; /** Label for a winner below the minor-drainage threshold. */ export declare const MINOR_DRAINAGE_LABEL = "minor local drainage \u2014 no significant river within ~8 km"; /** * Relative change (percent) below which discharge reads "steady". Deliberately * not `computeStageTrend`'s ±0.05 ft rule — that threshold is a stage height in * feet and means nothing applied to a volumetric flow in m³/s. */ export declare const TREND_STEADY_THRESHOLD_PCT = 10; /** Ratio at or above which today's discharge is called out as elevated. */ export declare const CONTEXT_ELEVATED_RATIO = 1.25; /** Ratio below which today's discharge is called out as well below average. */ export declare const CONTEXT_LOW_RATIO = 0.75; /** Days of history requested from the Flood API (`past_days`). */ export declare const PAST_DAYS = 31; /** A single coordinate in the probe grid. */ export interface ProbePoint { latitude: number; longitude: number; } /** The cell selected as the river channel, plus how far we had to move. */ export interface ChannelCellPick { /** Index into the cells array (and into the probe grid that produced it). */ index: number; /** Mean discharge (m³/s) over the past-31-day window, nulls ignored. */ meanDischarge: number; /** True when the requested point's own cell won. */ isCenter: boolean; /** Distance from the center cell to the winning cell, in km (0 if center). */ snapDistanceKm: number; /** 8-point compass direction from center to winner (undefined if center). */ snapBearing?: string; } /** Direction of travel in the recent discharge series. */ export interface DischargeTrend { direction: 'rising' | 'falling' | 'steady'; /** * Signed percent change from the window's first real value to its last. * Undefined when the baseline is zero, where a ratio is meaningless. */ percentChange?: number; /** Days actually spanned between the first and last real values. */ windowDays: number; } /** Today's discharge expressed against its own recent history. */ export interface DischargeContext { ratio: number; label: string; } /** * Build the 3x3 probe grid centered on a point — the neighborhood fetched in a * single multi-coordinate Flood API request so channel snapping costs one HTTP * call rather than nine. * * Emitted in latitude-major order (south row first, west to east within each * row), which puts the requested point at `PROBE_GRID_CENTER_INDEX`. * * @param latitude - Requested latitude * @param longitude - Requested longitude * @returns Nine probe points; near a pole, latitude clamping may make some of * them coincide, which is harmless (duplicate cells simply tie). */ export declare function buildProbeGrid(latitude: number, longitude: number): ProbePoint[]; /** * Locate today's entry in a daily `time` array. * * The series spans `past_days=31` plus the forecast horizon, so today is * nowhere near index 0 — everything downstream (current level, trend window, * forecast start) depends on finding it correctly. Dates come back in the * response's own local timezone (`timezone=auto`), so the comparison is made * against local "today", derived from `utc_offset_seconds`. * * @param time - Daily date strings (`YYYY-MM-DD`) * @param utcOffsetSeconds - The response's UTC offset * @param now - Injectable clock, for deterministic tests * @returns Index of today, or of the latest date at/before today; 0 when the * whole series lies in the future or the array is empty. */ export declare function findTodayIndex(time: string[] | undefined, utcOffsetSeconds?: number, now?: Date): number; /** * The past-history slice of a daily series: everything before today. * Falls back to the whole series when today sits at index 0, so a response * without history still yields a usable basis for cell selection. */ export declare function pastWindowValues(series: Array | undefined, todayIndex: number): Array; /** * The trailing slice used for the observed trend: the last `days` entries up to * and including today. */ export declare function recentWindowValues(series: Array | undefined, todayIndex: number, days?: number): Array; /** * Pick the cell that actually carries the river. * * Each cell is scored by its mean discharge over the past-31-day window, * ignoring null days; cells whose series is entirely null are excluded outright * (ocean, desert). The highest mean wins. * * Ties resolve to the center cell when it is among the tied, otherwise to the * lowest index — deterministic either way, so identical input always produces * identical output. * * @param cells - One Flood API response per probe point, in probe-grid order * @param centerIndex - Index of the requested point (`PROBE_GRID_CENTER_INDEX`) * @param now - Injectable clock, for deterministic tests * @returns The winning cell, or null when every cell is all-null */ export declare function pickChannelCell(cells: OpenMeteoFloodResponse[], centerIndex?: number, now?: Date): ChannelCellPick | null; /** * Label a winning cell that is below the minor-drainage threshold, so a * plausible-looking 0.04 m³/s is not presented as a river. * * @returns The label, or undefined when the discharge is river-scale */ export declare function describeMinorDrainage(meanDischarge: number): string | undefined; /** * Classify the direction of a recent discharge series. * * Compares the first real value in the window to the last, using a relative * ±10% threshold (exactly ±10% counts as a trend, mirroring the `<` convention * in `computeStageTrend`). * * @param recentSeries - The trailing window (see `recentWindowValues`) * @returns The trend, or undefined with fewer than two real points */ export declare function classifyDischargeTrend(recentSeries: Array | undefined): DischargeTrend | undefined; /** * Render a trend as an inline clause, e.g. "↗ rising (+23% / 6d)". * Steady trends omit the near-zero magnitude, matching the NWPS wording style. */ export declare function formatDischargeTrend(trend: DischargeTrend): string; /** * Express today's discharge against its own past-31-day mean — the closest * available stand-in for the flood categories GloFAS does not provide. * * Buckets: >= 1.25x elevated, 0.75x-1.25x near average, < 0.75x well below. * * @returns The ratio and its wording, or undefined when the mean is unusable */ export declare function classifyAgainstRecentMean(today: number, mean31: number | undefined): DischargeContext | undefined; /** * Disclose that the reported discharge came from a neighboring cell rather than * the requested point. * * @returns The note, or undefined when the requested point's own cell won (or * the snap is under half a kilometre, which rounds to nothing worth saying) */ export declare function formatSnapNote(distanceKm: number | undefined, bearing: string | undefined): string | undefined; //# sourceMappingURL=riverDischarge.d.ts.map