Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
# @suaveplan/tides - Tidal prediction engine: harmonic constituents, equilibrium-tide physics, ADMIRALTY UK Hydrographic Office API client, and station search — exposed via a unified TidalProvider interface.
## Summary
Tidal prediction providers.
<!-- begin:auto-docs -->
## Documentation tree
- [admiralty](./src/admiralty/admiralty.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [admiralty](./src/admiralty/admiralty.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [harmonic](./src/harmonic/harmonic.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [harmonic-constituents](./src/harmonic/harmonic-constituents.md) — Astronomical helper functions, Doodson coefficient tables, and per-constituent angular-speed tables consumed by the har…
- [harmonic](./src/harmonic/harmonic.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [physics](./src/physics/physics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [physics](./src/physics/physics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [stations](./src/stations/stations.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [stations](./src/stations/stations.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [admiralty](./src/admiralty/admiralty.md) — <!-- begin:auto-docs --> _(no submodules)_- [harmonic](./src/harmonic/harmonic.md) — <!-- begin:auto-docs --> _(no submodules)_- [harmonic-constituents](./src/harmonic/harmonic-constituents.md) — Astronomical helper functions, Doodson coefficient tables, and per-constituent angular-speed tables consumed by the har…
- [harmonic](./src/harmonic/harmonic.md) — <!-- begin:auto-docs --> _(no submodules)_- [physics](./src/physics/physics.md) — <!-- begin:auto-docs --> _(no submodules)_- [physics](./src/physics/physics.md) — <!-- begin:auto-docs --> _(no submodules)_- [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [stations](./src/stations/stations.md) — <!-- begin:auto-docs --> _(no submodules)_- [stations](./src/stations/stations.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [admiralty](./src/admiralty/admiralty.md) — <!-- begin:auto-docs --> _(no submodules)_- [harmonic](./src/harmonic/harmonic.md) — <!-- begin:auto-docs --> _(no submodules)_- [harmonic-constituents](./src/harmonic/harmonic-constituents.md) — Astronomical helper functions, Doodson coefficient tables, and per-constituent angular-speed tables consumed by the har…
- [harmonic](./src/harmonic/harmonic.md) — <!-- begin:auto-docs --> _(no submodules)_- [physics](./src/physics/physics.md) — <!-- begin:auto-docs --> _(no submodules)_- [physics](./src/physics/physics.md) — <!-- begin:auto-docs --> _(no submodules)_- [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [stations](./src/stations/stations.md) — <!-- begin:auto-docs --> _(no submodules)_- [stations](./src/stations/stations.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [admiralty](./src/admiralty/admiralty.md) — <!-- begin:auto-docs --> _(no submodules)_- [harmonic](./src/harmonic/harmonic.md) — <!-- begin:auto-docs --> _(no submodules)_- [harmonic-constituents](./src/harmonic/harmonic-constituents.md) — Astronomical helper functions, Doodson coefficient tables, and per-constituent angular-speed tables consumed by the har…
- [harmonic](./src/harmonic/harmonic.md) — <!-- begin:auto-docs --> _(no submodules)_- [physics](./src/physics/physics.md) — <!-- begin:auto-docs --> _(no submodules)_- [physics](./src/physics/physics.md) — <!-- begin:auto-docs --> _(no submodules)_- [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [stations](./src/stations/stations.md) — <!-- begin:auto-docs --> _(no submodules)_- [stations](./src/stations/stations.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [admiralty](./src/admiralty/admiralty.md) — <!-- begin:auto-docs --> _(no submodules)_- [harmonic](./src/harmonic/harmonic.md) — <!-- begin:auto-docs --> _(no submodules)_ - [harmonic-constituents](./src/harmonic/harmonic-constituents.md) — Astronomical helper functions, Doodson coefficient tables, and per-constituent angular-speed tables consumed by the har…
- [harmonic](./src/harmonic/harmonic.md) — <!-- begin:auto-docs --> _(no submodules)_- [physics](./src/physics/physics.md) — <!-- begin:auto-docs --> _(no submodules)_ - [physics](./src/physics/physics.md) — <!-- begin:auto-docs --> _(no submodules)_- [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_ - [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [stations](./src/stations/stations.md) — <!-- begin:auto-docs --> _(no submodules)_ - [stations](./src/stations/stations.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ - [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_
## Install
```bash
bun add @suaveplan/tides
```

View File

@@ -0,0 +1,73 @@
# admiralty
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Production HTTP client for the UK Hydrographic Office's ADMIRALTY Tidal API (`https://admiraltyapi.azure-api.net/uktidalapi/api/V1`). Wraps the `/Stations` and `/Stations/{id}/TidalEvents` endpoints with typed parsers, in-memory TTL caching, structured error mapping, and a tide-table builder that interpolates between published high/low events using a cosine progression. Designed for offline-tolerant UI flows where station catalogues and per-day extreme lists should be fetched once and reused.
## Features
- `createAdmiraltyProvider(options)` factory returning a fully configured `AdmiraltyProvider`.
- API-key authentication via `Ocp-Apim-Subscription-Key` header.
- Injectable `fetchFn` for testing or custom transport (e.g. retry middleware).
- Per-key in-memory cache with configurable `cacheTtlMs` (default 1 hour) plus dedicated unbounded station cache.
- Structured `TideError` mapping for 401/403 (`AUTH_ERROR`), 429 (`RATE_LIMIT`), other 4xx/5xx (`API_ERROR`), and empty payloads (`NO_DATA`).
- Cosine-progress interpolation between consecutive high/low events for sub-event height predictions.
- Multi-day tide-table builder with day-by-day extreme aggregation.
## Usage: Basic Example
```ts
import { createAdmiraltyProvider } from "@suaveplan/tides";
const provider = createAdmiraltyProvider({ apiKey: process.env.ADMIRALTY_KEY! });
const stations = await provider.getStations();
console.log(`${stations.length} ADMIRALTY stations available`);
```
## Usage: Advanced Example
```ts
import { createAdmiraltyProvider, type TideStation } from "@suaveplan/tides";
const provider = createAdmiraltyProvider({
apiKey: process.env.ADMIRALTY_KEY!,
cacheTtlMs: 30 * 60_000,
fetchFn: (url, init) =>
fetch(url, { ...init, signal: AbortSignal.timeout(10_000) }),
});
const { station, distanceKm } = await provider.getNearestStation({
latitude: 51.5074,
longitude: -0.1278,
});
console.log(`Nearest: ${station.name} (${distanceKm} km)`);
const table = await provider.getTideTable(
station,
new Date("2024-06-15T00:00:00Z"),
new Date("2024-06-17T23:59:59Z"),
60
);
for (const e of table.extremes) {
console.log(e.type, e.time.toISOString(), e.height.toFixed(2));
}
```
## API Reference
- `createAdmiraltyProvider(options: AdmiraltyProviderOptions): AdmiraltyProvider`.
- `AdmiraltyProviderOptions` — `{ apiKey: string; fetchFn?: typeof fetch; cacheTtlMs?: number }`.
- `AdmiraltyProvider`:
- `name: "admiralty"`.
- `getStations(signal?: AbortSignal): Promise<readonly TideStation[]>`.
- `getNearestStation(coord: SphericalCoord, signal?): Promise<NearestStationResult>`.
- `predictHeight(station, time, signal?): Promise<number>` — cosine-interpolated between extremes.
- `getExtremes(station, date, signal?): Promise<readonly TideExtreme[]>` — events whose `DateTime` falls on the UTC date.
- `getTideTable(station, start, end, intervalMinutes?, signal?): Promise<TideTable>`.
## Implementation Notes
Station IDs are namespaced as `admiralty-${rawId}` so they cannot collide with other-provider IDs in a merged catalogue. The cache uses a single `Map<string, CacheEntry<unknown>>` with per-entry `expiresAt` checked on read; expired entries are deleted lazily, so the map never grows beyond active keys. The cosine interpolation `(1 - cos(π·progress))/2` is the classical "tidal half-cosine" approximation — accurate to a few centimetres in the middle of the tidal cycle but degrades near slack water; for high-fidelity prediction prefer harmonic constituents via `createTiconProvider`. AbortSignal is forwarded conditionally (only when defined) to keep the `fetch` options object clean for environments that reject explicit `signal: undefined`.

View File

@@ -0,0 +1,54 @@
# harmonic-constituents
## Purpose
Astronomical helper functions, Doodson coefficient tables, and per-constituent angular-speed tables consumed by the harmonic tide-prediction engine. This module is split out of `harmonic.ts` so that the prediction algorithm and its lookup data each stay below the 300-line file budget while remaining colocated under `src/harmonic/`. Every export is a pure, stateless utility — no I/O, no shared state.
## Features
- `julianDate(date)` and J2000-relative century parameter helpers feeding mean-longitude polynomials.
- Mean longitude functions for Moon (`s`), Sun (`h`), lunar perigee (`p`), ascending lunar node (`N`), and solar perigee (`pp`).
- `astronomicalArgument(name, time)` — Doodson-coefficient combination yielding `V₀ + u` for a named constituent.
- `nodeFactor(name, time)` — Schureman-approximation `f` factor for amplitude modulation.
- `DOODSON_NUMBERS` table covering 30+ constituents (M2, S2, N2, K2, K1, O1, P1, Q1, M4, MS4, MN4, M6, …).
- `CONSTITUENT_SPEEDS` angular-speed table in degrees per hour.
## Usage: Basic Example
```ts
import { CONSTITUENT_SPEEDS } from "@suaveplan/tides";
const m2Speed = CONSTITUENT_SPEEDS.M2; // 28.984104 deg/hr
```
## Usage: Advanced Example
```ts
import {
astronomicalArgument,
DOODSON_NUMBERS,
nodeFactor,
} from "@suaveplan/tides";
const time = new Date("2024-06-15T12:00:00Z");
for (const name of Object.keys(DOODSON_NUMBERS)) {
const v0 = astronomicalArgument(name, time);
const f = nodeFactor(name, time);
console.log(name, v0.toFixed(2), f.toFixed(4));
}
```
## API Reference
- `toRad(deg: number): number` — degrees to radians.
- `julianDate(date: Date): number` — Julian Day Number including fractional day.
- `normalize(deg: number): number` — wrap angle into `[0, 360)`.
- `meanLongitudeMoon|Sun|LunarPerigee|LunarNode|SolarPerigee(T: number): number` — mean longitudes evaluated at Julian centuries since J2000.
- `astronomicalArgument(name: string, time: Date): number` — `V₀ + u` in degrees, returns `0` for unknown names.
- `nodeFactor(name: string, time: Date): number` — `f` in `[~0.7, ~1.3]`, returns `1.0` for unrecognised constituents.
- `DOODSON_NUMBERS: Record<string, readonly [number, number, number, number, number, number]>` — six-tuple `(τ, s, h, p, N, pp)` coefficients.
- `CONSTITUENT_SPEEDS: Record<string, number>` — angular speed in degrees per hour.
## Implementation Notes
Mean-longitude polynomials are truncated to the linear term in `T` (Julian centuries since J2000.0). This is adequate for short-range tide prediction but not for high-precision astronomical work — for centennial accuracy, quadratic and cubic terms from Meeus' *Astronomical Algorithms* are required. Node factors are simplified Schureman approximations: full-rigour evaluation requires lunar node `N` and lunar-perigee `p` together, but for engineering work the dominant `cos N`/`cos 2N` terms suffice. The Doodson and speed tables are intentionally exposed as plain `Record<string, …>` to keep registry extension a one-liner for callers shipping additional shallow-water or compound constituents.

View File

@@ -0,0 +1,69 @@
# harmonic
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements the classical harmonic tide-prediction algorithm — superposition of cosine constituents with node-factor amplitude modulation and time-varying equilibrium arguments. The module exposes a stateless `predictHeight` function and a slope-tracking `findExtremes` scan with bisection refinement, both consuming an arbitrary list of `HarmonicConstituent` records (name, amplitude, phase lag, angular speed). It is the engine behind the TICON-style provider and any caller bringing its own constituent table.
## Features
- Pure-function `predictHeight(constituents, time, meanSeaLevel?)` evaluating `h(t) = H₀ + Σ f(t)·Hₙ·cos(ωₙ·t + V₀ₙ(t) - Gₙ)`.
- Slope-sign-change detection in `findExtremes(constituents, start, end, meanSeaLevel?, scanMinutes?)` followed by 10-iteration bisection refinement to ~1-minute accuracy.
- Built-in node-factor (`f`) and equilibrium-argument (`V₀ + u`) tables for 30+ constituents (M2, S2, N2, K2, K1, O1, P1, Q1, M4, MS4, MN4, M6, etc.) — re-exported from `harmonic-constituents`.
- `TideError` thrown on empty constituent list (`NO_CONSTITUENTS`) and inverted ranges (`INVALID_RANGE`).
- Outputs rounded to millimetre precision for stable equality comparisons.
## Usage: Basic Example
```ts
import { predictHeight } from "@suaveplan/tides";
const height = predictHeight(
[{ name: "M2", amplitude: 1.2, phase: 45, speed: 28.984104 }],
new Date("2024-06-15T12:00:00Z"),
3.5
);
```
## Usage: Advanced Example
```ts
import {
CONSTITUENT_SPEEDS,
findExtremes,
type HarmonicConstituent,
} from "@suaveplan/tides";
const constituents: HarmonicConstituent[] = ["M2", "S2", "N2", "K1", "O1"].map(
(name) => ({
name,
amplitude: name === "M2" ? 2.1 : 0.4,
phase: 0,
speed: CONSTITUENT_SPEEDS[name]!,
})
);
const extremes = findExtremes(
constituents,
new Date("2024-06-15T00:00:00Z"),
new Date("2024-06-16T00:00:00Z"),
0,
6
);
for (const e of extremes) {
console.log(e.type, e.time.toISOString(), e.height.toFixed(2));
}
```
## API Reference
- `predictHeight(constituents: readonly HarmonicConstituent[], time: Date, meanSeaLevel?: number): number` — predicted height in metres above datum. Throws `TideError("NO_CONSTITUENTS")` if `constituents` is empty.
- `findExtremes(constituents, start: Date, end: Date, meanSeaLevel?: number, scanMinutes?: number): TideExtreme[]` — ordered high/low events between `start` and `end`. Throws `TideError("INVALID_RANGE")` if `start >= end`.
- `nodeFactor(constituentName: string, time: Date): number` — Schureman-approximation amplitude modulation factor for the named constituent.
- `CONSTITUENT_SPEEDS`, `DOODSON_NUMBERS` re-exported from `./harmonic-constituents`.
## Implementation Notes
The algorithm uses J2000.0 (JD 2451545.0) as the reference epoch for `hoursSinceEpoch`. Equilibrium arguments are computed from mean longitudes of Moon, Sun, lunar/solar perigee, and ascending lunar node via Meeus polynomials truncated to the linear term — sufficient for short-range engineering predictions but not for centennial accuracy. Node factors fall back to `1.0` for unrecognised constituents, matching the classical convention. `findExtremes` uses a midpoint-finite-difference slope at each scan step rather than a true derivative, so `scanMinutes` should stay below the Nyquist limit of the highest-frequency constituent (default 6 minutes is safe through M6).

View File

@@ -0,0 +1,57 @@
# physics
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
A simplified equilibrium-tide model that estimates ocean-surface height from latitude, longitude, and UTC time without requiring station data, harmonic constituents, or network access. It superposes a semi-diurnal lunar (M2) and solar (S2) cosine with a latitude- and declination-dependent amplitude factor, giving a useful first-order estimate for any coordinate on Earth. The module is the engine behind `createPhysicsBasedProvider` and is the recommended fallback when no gauge data exists.
## Features
- `equilibriumTideHeight(latitude, longitude, time)` returning a metre-scale height value.
- Latitude-modulated amplitude via `cos(lat) · cos(declination)` so polar points correctly attenuate.
- `findEquilibriumExtremes(latitude, longitude, date, scanMinutes?)` scanning a 24-hour window for local maxima and minima.
- Three-sample local-max/local-min detector with millimetre rounding.
- Pure functions — no closures, no I/O, no allocation beyond the result array.
## Usage: Basic Example
```ts
import { createPhysicsBasedProvider } from "@suaveplan/tides";
const provider = createPhysicsBasedProvider();
const height = await provider.predictHeight(
{ name: "Earth", radius: 6_371_000, rotationPeriod: 86_164 },
{ latitude: 51.5074, longitude: -0.1278 },
new Date("2024-06-15T12:00:00Z")
);
```
## Usage: Advanced Example
```ts
import { createPhysicsBasedProvider } from "@suaveplan/tides";
const provider = createPhysicsBasedProvider();
const earth = { name: "Earth", radius: 6_371_000, rotationPeriod: 86_164 };
const coord = { latitude: 51.5074, longitude: -0.1278 };
const extremes = await provider.getExtremes(
earth,
coord,
new Date("2024-06-15T00:00:00Z")
);
const highs = extremes.filter((e) => e.type === "high");
const lows = extremes.filter((e) => e.type === "low");
console.log(`${highs.length} highs, ${lows.length} lows`);
```
## API Reference
- `equilibriumTideHeight(latitude: number, _longitude: number, time: Date): number` — predicted height in metres. Longitude is unused but preserved in the signature for parity with station-based providers.
- `findEquilibriumExtremes(latitude: number, longitude: number, date: Date, scanMinutes?: number): TideExtreme[]` — high/low water events found within the UTC day containing `date`. Default `scanMinutes` is 15.
## Implementation Notes
The model uses J2000.0 as the reference epoch and combines two cosines (lunar at 28.984104°/h, solar at 30°/h) with a slowly varying solar declination term. It deliberately omits node factors, equilibrium arguments, and shallow-water constituents — for accurate predictions, prefer `createTiconProvider` with a real harmonic catalogue or `createAdmiraltyProvider` for UK waters. The `findEquilibriumExtremes` scanner uses a coarse three-point local-extremum test rather than bisection; when adjacent samples are equal the test rejects the candidate, so very flat sections of the curve will not produce spurious extremes. Output is rounded to millimetre precision.

View File

@@ -0,0 +1,66 @@
# provider
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Adapts the three tide-prediction backends — TICON-style harmonic constituents, the ADMIRALTY HTTP API, and the equilibrium-tide physics model — to the universal `TidalProvider` shape from `@suaveplan/types`. Every factory returns a pair of methods (`predictHeight`, `getExtremes`) that accept a `PlanetConfig`, a `SphericalCoord`, and a `Date`, so callers can swap backends without changing call sites or domain logic. This is the recommended entry point for any consumer that doesn't need the lower-level station-rooted `AdmiraltyProvider` API.
## Features
- `createTiconProvider(constituents, meanSeaLevel?)` — pure, offline, deterministic. Wraps `predictHeight`/`findExtremes` from `@suaveplan/tides/harmonic`.
- `createAdmiraltyProvider(options)` — async, network-bound. Routes via `getNearestStation` so callers can pass any UK coordinate.
- `createPhysicsBasedProvider()` — zero-config, no network, no constituents. Equilibrium-tide model from `@suaveplan/tides` `physics`.
- All three return objects matching `TidalProvider` from `@suaveplan/types`, so they compose with anything in the universe stack expecting that contract.
- AbortSignal is forwarded for the ADMIRALTY adapter; ignored for the deterministic backends.
## Usage: Basic Example
```ts
import { createTiconProvider } from "@suaveplan/tides";
const provider = createTiconProvider([
{ name: "M2", amplitude: 1.0, phase: 0, speed: 28.984104 },
]);
const earth = { name: "Earth", radius: 6_371_000, rotationPeriod: 86_164 };
const height = await provider.predictHeight(
earth,
{ latitude: 51.5074, longitude: -0.1278 },
new Date("2024-06-15T12:00:00Z")
);
```
## Usage: Advanced Example
```ts
import {
createAdmiraltyProvider,
createPhysicsBasedProvider,
createTiconProvider,
} from "@suaveplan/tides";
import type { TidalProvider } from "@suaveplan/types";
function pickProvider(input: {
apiKey?: string;
constituents?: Parameters<typeof createTiconProvider>[0];
}): TidalProvider {
if (input.constituents?.length)
return createTiconProvider(input.constituents);
if (input.apiKey) return createAdmiraltyProvider({ apiKey: input.apiKey });
return createPhysicsBasedProvider();
}
```
## API Reference
- `createTiconProvider(constituents: readonly HarmonicConstituent[], meanSeaLevel?: number): TidalProvider`.
- `createAdmiraltyProvider(options: AdmiraltyProviderOptions): TidalProvider` — internally wraps a station-rooted `AdmiraltyProvider`.
- `createPhysicsBasedProvider(): TidalProvider`.
- Each `TidalProvider` exposes:
- `predictHeight(planet, coord, time, signal?): Promise<number>`.
- `getExtremes(planet, coord, date, signal?): Promise<TideExtreme[]>`.
## Implementation Notes
The `_planet` parameter is currently unused by every backend — it is preserved on the contract so future backends (e.g. Mars or exoplanet equilibrium tide models) can vary behaviour without breaking call sites. The ADMIRALTY adapter performs an extra `getNearestStation` round-trip per call; for batch workloads, consider hoisting that to once-per-coord and operating on the station-rooted `AdmiraltyProvider` API directly to amortise it. The TICON and physics adapters are synchronous internally but return Promises to satisfy the universal contract — they never reject and never await, so `await` is essentially zero-cost. `getExtremes` for the TICON adapter scans the full UTC day containing `date` (`startOfDay → +24h`) regardless of the time-of-day component on the input.

View File

@@ -0,0 +1,50 @@
# stations
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Geospatial helpers that find tide-gauge stations near a query point on the WGS84-equivalent reference ellipsoid. Wraps `@suaveplan/geo`'s `greatCircleDistance` so that callers do not need to choose an ellipsoid or unit themselves. Used by every provider that ships its own catalogue (ADMIRALTY, custom TICON datasets) and by user-facing UI surfaces that need a "nearest station" picker without instantiating a full provider.
## Features
- `findNearestStation(lat, lon, stations)` returning the single closest entry plus the great-circle distance in kilometres.
- `findStationsInRadius(lat, lon, radiusKm, stations)` returning every station within a radius, pre-sorted ascending by distance.
- Distances rounded to two decimal places (10 m precision) for stable display.
- Strict `TideError` codes for pathological inputs: `NO_STATIONS` and `INVALID_RADIUS`.
- Pure functions — no allocation beyond the result array, no shared state.
## Usage: Basic Example
```ts
import { findNearestStation } from "@suaveplan/tides";
const result = findNearestStation(51.5074, -0.1278, [
{ id: "1", name: "London", latitude: 51.5074, longitude: -0.1278, provider: "ticon" },
{ id: "2", name: "Dover", latitude: 51.1278, longitude: 1.3134, provider: "ticon" },
]);
console.log(result.station.name, result.distanceKm);
```
## Usage: Advanced Example
```ts
import { findStationsInRadius, type TideStation } from "@suaveplan/tides";
const stations: TideStation[] = await loadCatalogue();
const within50km = findStationsInRadius(51.5074, -0.1278, 50, stations);
for (const { station, distanceKm } of within50km) {
console.log(`${station.name}: ${distanceKm} km`);
}
```
## API Reference
- `findNearestStation(latitude: number, longitude: number, stations: readonly TideStation[]): NearestStationResult` — throws `TideError("NO_STATIONS")` when `stations` is empty.
- `findStationsInRadius(latitude: number, longitude: number, radiusKm: number, stations: readonly TideStation[]): NearestStationResult[]` — throws `TideError("INVALID_RADIUS")` when `radiusKm <= 0`. Result is sorted ascending by distance.
- `NearestStationResult` — `{ station: TideStation; distanceKm: number }`.
## Implementation Notes
Internally uses a single ellipsoid built via `defineEllipsoid(6_371_000, 6_356_000)`, matching the values used elsewhere in the geo plugin family. This is a slight approximation versus the strict WGS84 spheroid (6_378_137 / 6_356_752) but is consistent with the rest of the package family and avoids importing the full WGS84 constants table. The radius search performs a linear scan rather than a spatial index — for catalogues of more than a few thousand stations, callers should consider pre-bucketing by tile or using an external R-tree library; tide-gauge networks rarely exceed that size in practice. Distances are reported in kilometres (`"kilometre"` unit on `greatCircleDistance`).

View File

@@ -0,0 +1,68 @@
# types
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The canonical TypeScript surface shared by every other module in `@suaveplan/tides` — harmonic constituent records, station metadata, prediction and extreme records, table containers, provider contracts, and the package-local `TideError` class. Re-exports `TideExtreme` from `@suaveplan/types` so downstream callers never need a second import path. This module owns no runtime logic beyond the `TideError` class.
## Features
- `HarmonicConstituent` interface — name, amplitude (m), phase lag (deg), angular speed (deg/hr).
- `TideStation` interface — id, name, lat/lon, provider, optional offline `constituents` and `meanSeaLevel`.
- `TidePrediction` and `TideTable` containers — height + time + originating station, plus aggregated extremes.
- `NearestStationResult` — station + distance in km.
- `TidalProviderLike` — minimal structural contract mirroring `@suaveplan/universe`'s `TidalProvider` so client code can mock or wrap providers without heavy dependencies.
- `TidesClientOptions` — ordered provider list for first-match-wins routing.
- `TideError extends Error` — typed code-bearing error class used by every other module.
## Usage: Basic Example
```ts
import {
type HarmonicConstituent,
type TideStation,
TideError,
} from "@suaveplan/tides";
const m2: HarmonicConstituent = {
name: "M2",
amplitude: 1.2,
phase: 0,
speed: 28.984104,
};
```
## Usage: Advanced Example
```ts
import {
type TidalProviderLike,
type TidesClientOptions,
TideError,
} from "@suaveplan/tides";
function createClient(options: TidesClientOptions): TidalProviderLike {
if (options.providers.length === 0) {
throw new TideError("No providers configured", "NO_PROVIDERS");
}
const first = options.providers[0]!;
return first;
}
```
## API Reference
- `HarmonicConstituent` — `{ name, amplitude, phase, speed }` all readonly.
- `TideStation` — `{ id, name, latitude, longitude, provider, constituents?, meanSeaLevel? }` all readonly.
- `TidePrediction` — `{ height, time, station }` all readonly.
- `TideTable` — `{ station, predictions, extremes }` all readonly.
- `NearestStationResult` — `{ station, distanceKm }`.
- `TidalProviderLike` — async methods `getStations`, `getNearestStation`, `predictHeight`, `getExtremes`, `getTideTable`.
- `TidesClientOptions` — `{ providers: readonly TidalProviderLike[] }`.
- `TideError` — `new TideError(message, code)`; `error.code` exposes the stable code string.
## Implementation Notes
`TideExtreme` lives in `@suaveplan/types` to keep the shape stable across the `geo`/`universe`/`tides` triangle without a circular dependency. `TidalProviderLike` deliberately diverges from `@suaveplan/universe`'s `TidalProvider` (which takes a `PlanetConfig` first argument) — this local shape is the _station-rooted_ contract used by `AdmiraltyProvider` and the `TidesClientOptions` flow, while `provider.ts` adapts station providers to the `PlanetConfig`-shaped universal contract. `TideError` is intentionally a hand-rolled class rather than a `SuaveplanError` subclass so the package can stay zero-runtime-deps for downstream tree-shaking; promoting it to the framework error registry is a future migration if the surface area grows.