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,31 @@
# `geospatial`
Geospatial data, mapping, planetary coordinates, and logistics utilities.
<!-- begin:auto-docs -->
## Subcategories
2 subcategories, 8 packages.
### `geo`
Geospatial coordinates, planetary data, and solar-system modelling.
| Package | Description |
|---|---|
| [`@suaveplan/geo`](./geo/README.md) | Abstract spherical and ellipsoidal geometry — pure math with no planet-specific constants. Every operation that needs a body's shape takes an explicit `Ellipso… |
| [`@suaveplan/geo-earth`](./geo-earth/README.md) | Earth configuration and legacy geo utilities. |
| [`@suaveplan/geo-planet`](./geo-planet/README.md) | Configurable planet surface geometry from `PlanetConfig` — ellipsoid, atmosphere, satellites, and orbital elements. |
| [`@suaveplan/solar-system`](./solar-system/README.md) | Solar system provider with astronomy engine. |
| [`@suaveplan/stars`](./stars/README.md) | Star catalog provider. |
| [`@suaveplan/tides`](./tides/README.md) | Tidal prediction providers. |
| [`@suaveplan/universe`](./universe/README.md) | Observer model, coordinate frames, wire protocol, and provider registry. |
### `logistics`
Port data and logistics primitives.
| Package | Description |
|---|---|
| [`@suaveplan/ports`](./ports/README.md) | Global port database with shipping-lane routing, ETA estimation, and UN/LOCODE port lookups for sea ports, airports, and rail terminals. |
<!-- end:auto-docs -->

View File

@@ -0,0 +1,3 @@
# `geospatial`
Geospatial data, mapping, planetary coordinates, and logistics utilities.

View File

@@ -0,0 +1,19 @@
# `geo`
Geospatial coordinates, planetary data, and solar-system modelling.
<!-- begin:auto-docs -->
## Packages in `geo`
7 packages.
| Package | Description |
|---|---|
| [`@suaveplan/geo`](./geo/README.md) | Abstract spherical and ellipsoidal geometry — pure math with no planet-specific constants. Every operation that needs a body's shape takes an explicit `Ellipso… |
| [`@suaveplan/geo-earth`](./geo-earth/README.md) | Earth configuration and legacy geo utilities. |
| [`@suaveplan/geo-planet`](./geo-planet/README.md) | Configurable planet surface geometry from `PlanetConfig` — ellipsoid, atmosphere, satellites, and orbital elements. |
| [`@suaveplan/solar-system`](./solar-system/README.md) | Solar system provider with astronomy engine. |
| [`@suaveplan/stars`](./stars/README.md) | Star catalog provider. |
| [`@suaveplan/tides`](./tides/README.md) | Tidal prediction providers. |
| [`@suaveplan/universe`](./universe/README.md) | Observer model, coordinate frames, wire protocol, and provider registry. |
<!-- end:auto-docs -->

View File

@@ -0,0 +1 @@
Geospatial coordinates, planetary data, and solar-system modelling.

View File

@@ -0,0 +1,31 @@
# @suaveplan/geo-earth - Earth-specific geo utilities: EARTH PlanetConfig and earthSurface, decimal/DMS coordinate parse + format, geohash encode/decode, and an in-memory geocoding provider.
## Summary
Earth configuration and legacy geo utilities.
<!-- begin:auto-docs -->
## Documentation tree
- [coordinates](./src/coordinates/coordinates.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [coordinates](./src/coordinates/coordinates.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [earth-config](./src/earth-config/earth-config.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [earth-config](./src/earth-config/earth-config.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [geocoding](./src/geocoding/geocoding.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [geocoding](./src/geocoding/geocoding.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [geohash](./src/geohash/geohash.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [geohash](./src/geohash/geohash.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [coordinates](./src/coordinates/coordinates.md) — <!-- begin:auto-docs --> _(no submodules)_- [earth-config](./src/earth-config/earth-config.md) — <!-- begin:auto-docs --> _(no submodules)_- [earth-config](./src/earth-config/earth-config.md) — <!-- begin:auto-docs --> _(no submodules)_- [geocoding](./src/geocoding/geocoding.md) — <!-- begin:auto-docs --> _(no submodules)_- [geocoding](./src/geocoding/geocoding.md) — <!-- begin:auto-docs --> _(no submodules)_- [geohash](./src/geohash/geohash.md) — <!-- begin:auto-docs --> _(no submodules)_- [geohash](./src/geohash/geohash.md) — <!-- begin:auto-docs --> _(no submodules)_- [coordinates](./src/coordinates/coordinates.md) — <!-- begin:auto-docs --> _(no submodules)_- [earth-config](./src/earth-config/earth-config.md) — <!-- begin:auto-docs --> _(no submodules)_- [earth-config](./src/earth-config/earth-config.md) — <!-- begin:auto-docs --> _(no submodules)_- [geocoding](./src/geocoding/geocoding.md) — <!-- begin:auto-docs --> _(no submodules)_- [geocoding](./src/geocoding/geocoding.md) — <!-- begin:auto-docs --> _(no submodules)_- [geohash](./src/geohash/geohash.md) — <!-- begin:auto-docs --> _(no submodules)_- [geohash](./src/geohash/geohash.md) — <!-- begin:auto-docs --> _(no submodules)_- [coordinates](./src/coordinates/coordinates.md) — <!-- begin:auto-docs --> _(no submodules)_- [earth-config](./src/earth-config/earth-config.md) — <!-- begin:auto-docs --> _(no submodules)_- [earth-config](./src/earth-config/earth-config.md) — <!-- begin:auto-docs --> _(no submodules)_- [geocoding](./src/geocoding/geocoding.md) — <!-- begin:auto-docs --> _(no submodules)_- [geocoding](./src/geocoding/geocoding.md) — <!-- begin:auto-docs --> _(no submodules)_- [geohash](./src/geohash/geohash.md) — <!-- begin:auto-docs --> _(no submodules)_- [geohash](./src/geohash/geohash.md) — <!-- begin:auto-docs --> _(no submodules)_- [coordinates](./src/coordinates/coordinates.md) — <!-- begin:auto-docs --> _(no submodules)_- [earth-config](./src/earth-config/earth-config.md) — <!-- begin:auto-docs --> _(no submodules)_ - [earth-config](./src/earth-config/earth-config.md) — <!-- begin:auto-docs --> _(no submodules)_- [geocoding](./src/geocoding/geocoding.md) — <!-- begin:auto-docs --> _(no submodules)_ - [geocoding](./src/geocoding/geocoding.md) — <!-- begin:auto-docs --> _(no submodules)_- [geohash](./src/geohash/geohash.md) — <!-- begin:auto-docs --> _(no submodules)_ - [geohash](./src/geohash/geohash.md) — <!-- begin:auto-docs --> _(no submodules)_
## Install
```bash
bun add @suaveplan/geo-earth
```
## Quick start
```ts
import { EARTH, earthSurface, encodeGeohash } from "@suaveplan/geo-earth";
const hash = encodeGeohash({ latitude: 51.5, longitude: -0.1 });
```

View File

@@ -0,0 +1,63 @@
# coordinates
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Tolerant string parser and human-readable formatter for latitude/longitude pairs on Earth. The parser accepts comma- or whitespace-separated decimal degrees (returning `null` on malformed input rather than throwing), and the formatter emits either fixed-precision decimal degrees or degrees/minutes/seconds (DMS) with hemispheric `N`/`S`/`E`/`W` suffixes.
Together these cover the two everyday surfaces a geo application touches: ingesting user-typed coordinates from forms, search bars, or imported CSVs, and rendering coordinates back to operators in a stable, copy-pasteable form. Both helpers are pure, allocation-light, and carry no dependency on the rest of the package.
## Features
- `parseCoordinate(input)` accepts `"51.5, -0.1"`, `"51.5 -0.1"`, or any mix of comma + whitespace.
- Returns `null` for unparseable input — never throws.
- `formatCoordinate(coord, format)` supports `"decimal"` (default, six decimal places per axis) or `"dms"`.
- DMS output uses Unicode `°`, `'`, `"` and a hemisphere letter derived from the sign.
- Pure functions; no I/O, no clock, no RNG; safe in any tier (universal).
## Usage: Basic Example
```ts
import { formatCoordinate, parseCoordinate } from "@suaveplan/geo-earth";
const parsed = parseCoordinate("51.5, -0.1");
// { latitude: 51.5, longitude: -0.1 }
const decimal = formatCoordinate({ latitude: 51.5, longitude: -0.1 });
// "51.500000, -0.100000"
```
## Usage: Advanced Example
```ts
import { formatCoordinate, parseCoordinate } from "@suaveplan/geo-earth";
function normalize(raw: string): string | null {
const parsed = parseCoordinate(raw);
if (parsed === null) return null;
return formatCoordinate(parsed, "dms");
}
normalize(" -33.8688\t151.2093 ");
// "33°52'7.68\"S, 151°12'33.48\"E"
normalize("not a coord"); // null
```
## API Reference
- `parseCoordinate(input: string): { latitude: number; longitude: number } | null`
- Splits on `[,\s]+`. Requires exactly two numeric tokens. Returns `null` if either token fails `Number.parseFloat`.
- `formatCoordinate(coord: { latitude: number; longitude: number }, format?: "decimal" | "dms"): string`
- `"decimal"` (default): each axis as `toFixed(6)`, joined by `", "`.
- `"dms"`: each axis as `D°M'S.ss"H` where `H` is `N`/`S` for latitude and `E`/`W` for longitude.
Neither function validates that values lie within `[-90, 90]` / `[-180, 180]`; treat range checking as a separate concern.
## Implementation Notes
- DMS conversion uses `Math.floor` on the absolute value, then derives minutes and fractional seconds; the sign is consumed only by the hemisphere selector. This avoids the negative-modulo ambiguity that bites naive ports.
- Decimal output is fixed at six decimals (~11 cm precision at the equator) to keep round-trips through `parseCoordinate` lossless within typical surveying tolerances.
- `parseCoordinate` deliberately accepts the union of common separators rather than forcing a single canonical form so that pasted output from Google Maps, GPS exports, and CSV cells all parse without preprocessing.

View File

@@ -0,0 +1,67 @@
# earth-config
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Canonical `PlanetConfig` for Earth and a pre-instantiated `PlanetSurface` bound to it. Application code that needs to reason about Earth — great-circle distance, surface gravity, atmospheric scale height, or the Moon as a satellite — should import these constants rather than re-deriving them at every call site so that every package agrees on a single set of physical values.
The numbers chosen are conservative round figures suitable for game and visualisation use (mean radius `6_371_000 m`, polar radius `6_356_000 m`, surface gravity `9.81 m/s²`, sidereal-rounded rotation period of one solar day, axial tilt `23.44°`, surface pressure `101 325 Pa`, scale height `8 500 m`). They are not intended to substitute for ephemeris-grade geodetic constants (WGS-84) when sub-metre accuracy is required.
## Features
- `EARTH` — frozen-shape `PlanetConfig` literal: mass, mean + polar radii, surface gravity, rotation period, axial tilt, atmosphere model, and the Moon as a satellite.
- `earthSurface` — a `PlanetSurface` instance constructed once from `EARTH`, ready for distance/bearing math without re-allocation.
- Atmosphere block exposes the four scattering coefficients (`rayleighCoefficient`, `mieCoefficient`, `extinctionPerAirMass`, `scaleHeight`) consumed by `@suaveplan/geo-planet`.
- Single satellite entry: Moon, with mass, radius, orbital radius, eccentricity, inclination, period, and albedo.
- Pure data + a single object instance — zero side effects at module load.
## Usage: Basic Example
```ts
import { EARTH, earthSurface } from "@suaveplan/geo-earth";
console.log(EARTH.name); // "Earth"
console.log(earthSurface.planet.radius); // 6371000
```
## Usage: Advanced Example
```ts
import { EARTH, earthSurface } from "@suaveplan/geo-earth";
import type { PlanetConfig } from "@suaveplan/geo-planet";
import { PlanetSurface } from "@suaveplan/geo-planet";
// Derive a denser-atmosphere variant for a what-if simulation
// without mutating the shared EARTH constant.
const denserEarth: PlanetConfig = {
...EARTH,
atmosphere: {
...EARTH.atmosphere!,
surfacePressure: 200000,
scaleHeight: 9500,
},
};
const denserSurface = new PlanetSurface(denserEarth);
console.log(denserSurface.planet.atmosphere?.surfacePressure); // 200000
// The default earthSurface is unchanged.
console.log(earthSurface.planet.atmosphere?.surfacePressure); // 101325
```
## API Reference
- `EARTH: PlanetConfig` — re-exported from `@suaveplan/geo-planet`'s `PlanetConfig` shape. Notable fields:
- `name: "Earth"`, `mass: 5.972e24` kg, `radius: 6_371_000` m, `polarRadius: 6_356_000` m.
- `surfaceGravity: 9.81` m/s², `rotationPeriod: 86_400` s, `axialTilt: 23.44°`.
- `atmosphere`: `{ surfacePressure: 101325, scaleHeight: 8500, rayleighCoefficient: 1, mieCoefficient: 1, extinctionPerAirMass: 1 }`.
- `satellites[0]`: Moon (`mass: 7.342e22`, `radius: 1_737_000`, `orbitalRadius: 384_400_000`, `eccentricity: 0.0549`, `orbitalInclination: 5.14°`, `orbitalPeriod: 2_360_520` s, `albedo: 0.12`).
- `earthSurface: PlanetSurface` — `new PlanetSurface(EARTH)`. See `@suaveplan/geo-planet` for the surface API.
## Implementation Notes
- `EARTH` is exported as a value, not frozen at runtime, but is treated as immutable by convention — clone before mutating.
- The scattering coefficients are placeholder unit values; replace via the spread pattern shown above when integrating a calibrated atmospheric model.
- `earthSurface` is constructed at module import. Because `PlanetSurface` is a thin wrapper, this is cheap and safe to do eagerly.

View File

@@ -0,0 +1,78 @@
# geocoding
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the `GeocodingProvider` contract every geocoding backend in the repo must implement, plus a deterministic in-memory provider keyed by an input `Map`. The contract is intentionally narrow — `geocode(query)` for one-shot lookups and `batchGeocode(queries)` for batched workflows — so that swapping a fixture-backed implementation for a Nominatim, Mapbox, or Google adapter is a one-line dependency change in the calling site.
The in-memory provider is the canonical fixture for tests and offline development. It returns the array stored against each query verbatim (or an empty array for misses), giving callers full control over result ordering, multiplicity, and edge cases such as duplicate names.
## Features
- `GeocodingResult` — `{ name, coord }` where `coord` is a `SphericalCoord` from `@suaveplan/geo`.
- `GeocodingProvider` — minimal interface: `geocode(query)` and `batchGeocode(queries)`.
- `createInMemoryProvider(data)` — wraps a `Map<string, GeocodingResult[]>` into the provider interface.
- Misses return `[]`, never `undefined`, simplifying downstream `.length`/iteration code.
- Pure, synchronous, allocation-free per call beyond the result arrays the caller supplied.
## Usage: Basic Example
```ts
import { createInMemoryProvider } from "@suaveplan/geo-earth";
const provider = createInMemoryProvider(
new Map([
[
"London",
[{ name: "London", coord: { latitude: 51.5, longitude: -0.1 } }],
],
])
);
const results = provider.geocode("London");
// [{ name: "London", coord: { latitude: 51.5, longitude: -0.1 } }]
```
## Usage: Advanced Example
```ts
import {
createInMemoryProvider,
type GeocodingProvider,
type GeocodingResult,
} from "@suaveplan/geo-earth";
const fixtures = new Map<string, GeocodingResult[]>([
[
"Paris",
[
{ name: "Paris, FR", coord: { latitude: 48.8566, longitude: 2.3522 } },
{ name: "Paris, TX", coord: { latitude: 33.6609, longitude: -95.5555 } },
],
],
["Tokyo", [{ name: "Tokyo, JP", coord: { latitude: 35.6762, longitude: 139.6503 } }]],
]);
const provider: GeocodingProvider = createInMemoryProvider(fixtures);
const batched = provider.batchGeocode(["Paris", "Tokyo", "Atlantis"]);
// batched[0].length === 2 (Paris ambiguity preserved)
// batched[1].length === 1
// batched[2] === [] (miss → empty array)
```
## API Reference
- `interface GeocodingResult { name: string; coord: SphericalCoord }`
- `interface GeocodingProvider`
- `geocode(query: string): GeocodingResult[]` — synchronous lookup, empty array for miss.
- `batchGeocode(queries: string[]): GeocodingResult[][]` — same length and order as input; per-query miss is an empty array.
- `createInMemoryProvider(data: Map<string, GeocodingResult[]>): GeocodingProvider` — constructs a provider that reads directly from `data`. The `Map` is captured by reference; mutating it after construction is observable on subsequent calls.
## Implementation Notes
- The interface is synchronous because the in-memory provider is, and because a synchronous lower bound forces async adapters (HTTP-backed Nominatim/Mapbox) to declare their own wrapper interface that returns `Promise<GeocodingResult[]>` — they should not silently `await` here.
- `batchGeocode` is a `.map` over `geocode`, which preserves input order and length. Adapters that can issue a true batched request to a remote API should override this for throughput.
- `SphericalCoord` is imported as a type from `@suaveplan/geo` to keep this module type-only against its sibling and avoid runtime coupling.

View File

@@ -0,0 +1,73 @@
# geohash
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Encodes a `(latitude, longitude)` pair into a Geohash base32 string and decodes it back to the centre coordinate of the cell. Geohash is a hierarchical spatial index in which a longer string addresses a smaller bounding box, and any prefix of a hash addresses the enclosing cell — making it useful for proximity bucketing, key-prefix range scans on KV stores, and tiled spatial joins without pulling in a full R-tree or quadkey library.
This implementation follows the canonical Niemeyer/Morton algorithm: alternating-axis bit interleaving (longitude on even bits, latitude on odd) with the Geohash base32 alphabet `0123456789bcdefghjkmnpqrstuvwxyz` (intentionally omits `a`, `i`, `l`, `o` to avoid visual confusion).
## Features
- `encodeGeohash(coord, precision = 9)` — produces a base32 string of length `precision`.
- `decodeGeohash(hash)` — returns the centre `{ latitude, longitude }` of the addressed cell.
- Default precision `9` (~4.77 m × 4.77 m at the equator) suits typical store-front / asset-tracking use.
- Pure, deterministic, allocation-light: a single growing string and integer state.
- Universal — no DOM, no Node, no clock, no RNG.
## Usage: Basic Example
```ts
import { decodeGeohash, encodeGeohash } from "@suaveplan/geo-earth";
const hash = encodeGeohash({ latitude: 51.5, longitude: -0.1 }, 9);
const coord = decodeGeohash(hash);
// coord ≈ { latitude: 51.5, longitude: -0.1 }
```
## Usage: Advanced Example
```ts
import { encodeGeohash } from "@suaveplan/geo-earth";
// Bucket a stream of points by 5-character prefix (~4.9 km × 4.9 km cell)
// for cheap proximity grouping in a key/value store.
function bucketByCell<T extends { latitude: number; longitude: number }>(
points: readonly T[],
precision = 5,
): Map<string, T[]> {
const buckets = new Map<string, T[]>();
for (const p of points) {
const cell = encodeGeohash(p, precision);
const list = buckets.get(cell);
if (list) list.push(p);
else buckets.set(cell, [p]);
}
return buckets;
}
const grouped = bucketByCell([
{ latitude: 51.5, longitude: -0.1 },
{ latitude: 51.51, longitude: -0.09 },
{ latitude: 48.86, longitude: 2.35 },
]);
// grouped has two keys: one shared London bucket and one Paris bucket.
```
## API Reference
- `encodeGeohash(coord: { latitude: number; longitude: number }, precision?: number): string`
- `precision` defaults to `9`. Each character contributes 5 bits of resolution (~halving cell size every two characters).
- Returns a string of exactly `precision` characters from the Geohash base32 alphabet.
- `decodeGeohash(hash: string): { latitude: number; longitude: number }`
- Returns the centre of the cell addressed by `hash`. Round-trip fidelity is bounded by the cell size at the chosen precision; the unit tests assert `toBeCloseTo` at one decimal for precision-9 round-trips.
Inputs outside `[-90, 90]` / `[-180, 180]` are not validated; callers that accept arbitrary user input should clamp first.
## Implementation Notes
- Bit interleaving is implemented with the `evenBit` boolean flipping every iteration; on even iterations longitude is bisected, on odd iterations latitude is bisected. This matches the canonical Geohash bit ordering and is what makes string prefixes correspond to enclosing cells.
- Base32 lookup uses `BASE32.indexOf(char)` per character in `decodeGeohash`. For very large decode batches a precomputed `Map<string, number>` would be marginally faster but adds module-load cost; the linear scan is fine for typical workloads at length ≤ 12.
- The decoder returns the cell centre, not the bounding box. Callers needing the box (e.g. for tile rendering) can re-run the bisection loop and capture `minLat/maxLat/minLon/maxLon` themselves.

View File

@@ -0,0 +1,53 @@
# @suaveplan/geo-planet - Configurable planet surface geometry from PlanetConfig — ellipsoid, atmosphere, satellites, and orbital elements.
## Summary
Configurable planet surface geometry from `PlanetConfig` — ellipsoid, atmosphere, satellites, and orbital elements.
<!-- begin:auto-docs -->
## Documentation tree
- [atmospheric](./src/atmospheric/atmospheric.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [atmospheric](./src/atmospheric/atmospheric.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [schemas](./src/types/schemas.md) — The `schemas` module exposes Zod runtime schemas mirroring every public type declared in `types.ts`. Validation is the…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [atmospheric](./src/atmospheric/atmospheric.md) — <!-- begin:auto-docs --> _(no submodules)_- [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_- [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/types/schemas.md) — The `schemas` module exposes Zod runtime schemas mirroring every public type declared in `types.ts`. Validation is the…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [atmospheric](./src/atmospheric/atmospheric.md) — <!-- begin:auto-docs --> _(no submodules)_- [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_- [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/types/schemas.md) — The `schemas` module exposes Zod runtime schemas mirroring every public type declared in `types.ts`. Validation is the…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [atmospheric](./src/atmospheric/atmospheric.md) — <!-- begin:auto-docs --> _(no submodules)_- [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_- [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/types/schemas.md) — The `schemas` module exposes Zod runtime schemas mirroring every public type declared in `types.ts`. Validation is the…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [atmospheric](./src/atmospheric/atmospheric.md) — <!-- begin:auto-docs --> _(no submodules)_- [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_ - [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ - [schemas](./src/types/schemas.md) — The `schemas` module exposes Zod runtime schemas mirroring every public type declared in `types.ts`. Validation is the…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_
## Install
```bash
bun add @suaveplan/geo-planet
```
## Quick start
```ts
import { PlanetSurface, PlanetConfigSchema } from "@suaveplan/geo-planet";
const config = PlanetConfigSchema.parse({
name: "Mars",
mass: 6.39e23,
radius: 3_396_200,
polarRadius: 3_376_200,
surfaceGravity: 3.72,
rotationPeriod: 88_620,
axialTilt: 25.2,
atmosphere: null,
satellites: [],
});
const surface = new PlanetSurface(config);
console.log(surface.distance(a, b, "kilometre"));
```
## Modules
- [`src/planet-surface/planet-surface.md`](src/planet-surface/planet-surface.md)
- [`src/atmospheric/atmospheric.md`](src/atmospheric/atmospheric.md)
- [`src/types/types.md`](src/types/types.md)

View File

@@ -0,0 +1,86 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `atmospheric` module provides the two physics primitives every planetary renderer, sky-colour solver, and observation-quality estimator needs: the local density of an atmospheric column at altitude, and the relative path length light travels through that column for a given elevation angle. Both functions accept a `PlanetConfig` so the same code path serves Earth, Mars, Titan, or any other modelled body.
These primitives are intentionally pure and free of vendor coupling so they can be reused by `@suaveplan/universe`, `@suaveplan/gaming-environment-lighting`, server-side astronomical computations, and offline analysis without dragging a graphics runtime into the call site.
## Features
- `atmosphericDensity(planet, altitudeMetres)` — exponential isothermal scale-height model that returns `0` for airless bodies
- `airMass(altitudeDeg)` — Kasten–Young 1989 air-mass approximation valid from zenith down to the horizon
- Numerical-stable horizon handling: negative altitudes are clamped to `0` so the formula remains finite at the bound
- Pure functions: no I/O, no side effects, no global state — safe for `Worker` threads, SSR, and deterministic tests
## Usage
### Basic Example
```ts
import { atmosphericDensity } from "@suaveplan/geo-planet";
import type { PlanetConfig } from "@suaveplan/geo-planet";
const earth: PlanetConfig = {
name: "Earth",
mass: 5.972e24,
radius: 6_378_137,
polarRadius: 6_356_752,
surfaceGravity: 9.81,
rotationPeriod: 86_164,
axialTilt: 23.44,
atmosphere: {
surfacePressure: 101_325,
scaleHeight: 8_500,
rayleighCoefficient: 5.8e-6,
mieCoefficient: 21e-6,
extinctionPerAirMass: 0.13,
},
satellites: [],
};
const surface = atmosphericDensity(earth, 0); // 101325 Pa
const cruise = atmosphericDensity(earth, 11_000); // ~27 kPa
```
### Advanced Example
```ts
import { airMass, atmosphericDensity } from "@suaveplan/geo-planet";
import type { PlanetConfig } from "@suaveplan/geo-planet";
function transmittance(
planet: PlanetConfig,
altitudeMetres: number,
sunElevationDeg: number,
): number {
if (!planet.atmosphere) return 1;
const density = atmosphericDensity(planet, altitudeMetres);
const am = airMass(sunElevationDeg);
const tau = planet.atmosphere.extinctionPerAirMass * am;
return Math.exp(-tau) * (density / planet.atmosphere.surfacePressure);
}
// Use at every camera step in a sky shader, or to score
// observability for ground-station pass scheduling.
```
## API Reference
### `atmosphericDensity(planet: PlanetConfig, altitudeMetres: number): number`
Returns the local pressure (`Pa`) using `p₀ · exp(-h / H)` where `p₀` is `planet.atmosphere.surfacePressure` and `H` is `planet.atmosphere.scaleHeight`. Returns `0` when `planet.atmosphere === null`. No clamping is applied to altitude; negative altitudes (subterranean, sub-sea-level) yield pressures above `p₀`, which is the physically correct extrapolation for a barometric law.
### `airMass(altitudeDeg: number): number`
Returns the Kasten–Young air mass:
`1 / (sin(alt) + 0.50572 · (alt + 6.07995)^-1.6364)`
with `alt` in degrees. Negative altitudes are clamped to `0` so the result remains finite at the horizon (returning ≈ `38`). At zenith the return value approaches `1`.
## Implementation Notes
The density function uses the isothermal barometric equation rather than a layered ISA model because every consumer in this project treats density as a smooth analytic function for shader sampling. The Kasten–Young formulation was chosen over the simpler `1 / sin(alt)` secant model because it remains stable down to the horizon and only diverges by ≈ `0.4%` from full atmospheric ray-tracing above 5° elevation. Both functions are allocation-free hot-path code; consumers can call them once per pixel without GC pressure.

View File

@@ -0,0 +1,111 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `planet-surface` module provides the `PlanetSurface` class, an oblate-ellipsoid wrapper around a `PlanetConfig` that exposes the geodetic operations every multi-planet system needs: great-circle distance, initial bearing, terminal-point projection from a heading and distance, and point-in-polygon tests. It is the single bridge between body-specific configuration (Earth, Mars, Titan, …) and the body-agnostic geometry primitives in `@suaveplan/geo`.
By deriving the ellipsoid once per `PlanetSurface` instance from the `radius` and `polarRadius` fields of `PlanetConfig`, callers never have to thread an ellipsoid through every distance call, and they cannot accidentally mix Earth-flattening into a Mars computation.
## Features
- `PlanetSurface` class — owns the planet config and the cached `Ellipsoid`
- `distance(a, b, unit?)` — geodesic great-circle distance with selectable distance unit
- `bearing(from, to)` — initial forward azimuth in degrees from north, clockwise
- `destination(start, bearingDeg, distanceM)` — terminal-point projection by direct-geodesic solution
- `pointInPolygon(point, polygon)` — spherical-coordinate ray-cast / winding test for arbitrary closed rings
- Body-agnostic — works for any planet whose oblateness is well-modelled by an ellipsoid of revolution
## Usage
### Basic Example
```ts
import { PlanetSurface } from "@suaveplan/geo-planet";
import type { PlanetConfig } from "@suaveplan/geo-planet";
const earth: PlanetConfig = {
name: "Earth",
mass: 5.972e24,
radius: 6_378_137,
polarRadius: 6_356_752,
surfaceGravity: 9.81,
rotationPeriod: 86_164,
axialTilt: 23.44,
atmosphere: null,
satellites: [],
};
const surface = new PlanetSurface(earth);
const km = surface.distance(
{ latitude: 51.5, longitude: -0.13 }, // London
{ latitude: 40.71, longitude: -74.0 }, // New York
"kilometre",
);
```
### Advanced Example
```ts
import { PlanetSurface } from "@suaveplan/geo-planet";
import type { PlanetConfig } from "@suaveplan/geo-planet";
const mars: PlanetConfig = {
name: "Mars",
mass: 6.39e23,
radius: 3_396_200,
polarRadius: 3_376_200,
surfaceGravity: 3.72,
rotationPeriod: 88_620,
axialTilt: 25.2,
atmosphere: null,
satellites: [],
};
const surface = new PlanetSurface(mars);
// Project a 100 km traverse from Jezero crater on a 45° heading.
const start = { latitude: 18.44, longitude: 77.45 };
const end = surface.destination(start, 45, 100_000);
// Heading toward Olympus Mons.
const heading = surface.bearing(start, { latitude: 18.65, longitude: -133.8 });
// Region containment for science-target zones.
const aoi = [
{ latitude: 18.0, longitude: 77.0 },
{ latitude: 19.0, longitude: 77.0 },
{ latitude: 19.0, longitude: 78.0 },
{ latitude: 18.0, longitude: 78.0 },
];
const inside = surface.pointInPolygon(end, aoi);
```
## API Reference
### `class PlanetSurface`
#### `constructor(planet: PlanetConfig)`
Stores the planet on `surface.planet` and constructs an `Ellipsoid` via `defineEllipsoid(planet.radius, planet.polarRadius)` once per instance.
#### `distance(a, b, unit?: DistanceUnit): number`
Returns the great-circle distance between two `SphericalCoord` points along the cached ellipsoid. `unit` defaults to `"metre"`; pass `"kilometre"`, `"mile"`, etc. as supported by `@suaveplan/geo`.
#### `bearing(from, to): number`
Returns the initial forward azimuth in degrees clockwise from north. Note: the spherical bearing in `@suaveplan/geo` does not depend on the ellipsoid.
#### `destination(start, bearingDeg, distanceM): SphericalCoord`
Solves the direct geodesic problem on the cached ellipsoid: given a start point, an initial bearing, and a distance in metres, returns the terminal point.
#### `pointInPolygon(point, polygon: SphericalCoord[]): boolean`
Tests containment of `point` inside the closed polygon ring; uses spherical-coordinate point-in-polygon math from `@suaveplan/geo`.
## Implementation Notes
The ellipsoid is built once at construction and then reused on every method call, which keeps every distance / destination call allocation-free in steady state. Bearing intentionally does not pass the ellipsoid because the underlying `bearing` from `@suaveplan/geo` is a pure spherical primitive — for the precision needed in mission-planning UIs, the difference between spherical and geodetic bearing is below display granularity, and the geometric simplicity is worth more than the marginal accuracy gain. Storing `planet` as a public readonly property lets consumers retain the original config (for atmosphere, satellites, textures, etc.) without round-tripping through a separate registry.

View File

@@ -0,0 +1,84 @@
## Purpose
The `schemas` module exposes Zod runtime schemas mirroring every public type declared in `types.ts`. Validation is the boundary contract for any consumer accepting raw JSON, configuration files, IPC payloads, or untrusted user input that will later be handed to `PlanetSurface`, atmospheric solvers, or astronomical providers.
By keeping schemas co-located with the type module they validate, `geo-planet` guarantees that the runtime parser and the static `interface` shapes never drift. Every numeric invariant required by physical models (positive masses, normalised eccentricities, non-negative pressures) is enforced at parse time so downstream code can assume well-formed inputs without defensive null checks.
## Features
- `AtmosphereConfigSchema` — non-negative pressure / coefficients, positive scale height
- `SatelliteConfigSchema` — positive mass/radius/orbit, eccentricity ∈ `[0, 1]`, albedo ∈ `[0, 1]`
- `OrbitalConfigSchema` — Keplerian elements (semi-major axis AU, inclination, perihelion, ascending node, mean anomaly, epoch JD)
- `PlanetTextureManifestSchema` — optional diffuse/normal/specular/clouds/night texture URLs
- `PlanetConfigSchema` — composite root validating physical, orbital, and rendering metadata
- Inferred types via `z.infer<typeof Schema>` interoperate with the hand-authored `interface`s in `types.ts`
## Usage
### Basic Example
```ts
import { PlanetConfigSchema } from "@suaveplan/geo-planet";
const earth = PlanetConfigSchema.parse({
name: "Earth",
mass: 5.972e24,
radius: 6_378_137,
polarRadius: 6_356_752,
surfaceGravity: 9.81,
rotationPeriod: 86_164,
axialTilt: 23.44,
atmosphere: null,
satellites: [],
});
```
### Advanced Example
```ts
import {
AtmosphereConfigSchema,
OrbitalConfigSchema,
PlanetConfigSchema,
SatelliteConfigSchema,
} from "@suaveplan/geo-planet";
const result = PlanetConfigSchema.safeParse(rawJson);
if (!result.success) {
for (const issue of result.error.issues) {
console.error(issue.path.join("."), issue.message);
}
throw new Error("invalid planet config");
}
// Per-field schemas compose for partial validation pipelines.
const atmo = AtmosphereConfigSchema.parse(rawJson.atmosphere);
const orbit = OrbitalConfigSchema.parse(rawJson.orbital);
const moon = SatelliteConfigSchema.parse(rawJson.satellites[0]);
```
## API Reference
### `AtmosphereConfigSchema`
Validates `surfacePressure ≥ 0`, `scaleHeight > 0`, and non-negative Rayleigh, Mie, and air-mass extinction coefficients. Throws on negative numbers or missing fields.
### `SatelliteConfigSchema`
Validates non-empty `name`, positive `mass` / `radius` / `orbitalRadius` / `orbitalPeriod`, free `orbitalInclination`, and clamped `eccentricity` and `albedo` in `[0, 1]`.
### `OrbitalConfigSchema`
Validates positive `semiMajorAxisAU`, eccentricity ∈ `[0, 1]`, free angular elements, and any finite epoch Julian Date.
### `PlanetTextureManifestSchema`
All texture URL fields are optional strings; missing fields default to `undefined`.
### `PlanetConfigSchema`
Composite schema enforcing positive `mass` / `radius` / `polarRadius` / `surfaceGravity` / `rotationPeriod`, free `axialTilt`, `atmosphere: AtmosphereConfig | null`, an array of satellites, and optional `orbital` and `textures`.
## Implementation Notes
Every schema is built directly with `zod` and exports the parser; `z.infer` keeps inferred shapes structurally identical to the hand-authored interfaces. The `null`-permissive atmosphere field models airless bodies (Mercury, Moon) without forcing consumers into an option-type at the validation layer. Eccentricity is clamped to a closed interval rather than a half-open one because a parabolic trajectory (`e === 1`) is a useful boundary case for astronomical fixtures even though it is not bound. Schemas avoid `.strict()` so additive forward-compatible fields can be carried by raw payloads without forcing a parser bump.

View File

@@ -0,0 +1,117 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `types` module declares the static TypeScript interfaces describing every value `geo-planet` accepts and emits. These are the canonical shapes consumed by `PlanetSurface`, atmospheric models, and any downstream renderer or astronomical provider that needs a single source of truth for what defines a planetary body.
Keeping the interfaces in a hand-authored file (rather than relying solely on `z.infer`) lets the compiler emit precise, documented declarations into `.d.ts` outputs without dragging the Zod runtime into type-only consumers. The runtime mirror lives in `schemas.ts` and is verified by tests to stay structurally aligned with these interfaces.
## Features
- `AtmosphereConfig` — exponential atmosphere parameters (surface pressure, scale height, Rayleigh / Mie coefficients, extinction)
- `SatelliteConfig` — physical and orbital parameters for a natural satellite
- `OrbitalConfig` — heliocentric Keplerian elements with epoch Julian Date
- `PlanetTextureManifest` — optional URLs for diffuse / normal / specular / cloud / night textures
- `PlanetConfig` — composite top-level shape: physical body + optional atmosphere + satellites + optional orbital and texture metadata
## Usage
### Basic Example
```ts
import type { PlanetConfig } from "@suaveplan/geo-planet";
const mars: PlanetConfig = {
name: "Mars",
mass: 6.39e23,
radius: 3_396_200,
polarRadius: 3_376_200,
surfaceGravity: 3.72,
rotationPeriod: 88_620,
axialTilt: 25.2,
atmosphere: null,
satellites: [],
};
```
### Advanced Example
```ts
import type {
AtmosphereConfig,
OrbitalConfig,
PlanetConfig,
SatelliteConfig,
} from "@suaveplan/geo-planet";
const earthAtmosphere: AtmosphereConfig = {
surfacePressure: 101_325,
scaleHeight: 8_500,
rayleighCoefficient: 5.8e-6,
mieCoefficient: 21e-6,
extinctionPerAirMass: 0.13,
};
const earthOrbit: OrbitalConfig = {
semiMajorAxisAU: 1.0,
eccentricity: 0.0167,
inclinationDeg: 0.0,
argumentOfPerihelionDeg: 102.94,
ascendingNodeDeg: -11.26,
meanAnomalyAtEpochDeg: 357.51,
epochJd: 2_451_545,
};
const luna: SatelliteConfig = {
name: "Luna",
mass: 7.342e22,
radius: 1_737_400,
orbitalRadius: 384_400_000,
orbitalPeriod: 2_360_592,
orbitalInclination: 5.145,
eccentricity: 0.0549,
albedo: 0.12,
};
const earth: PlanetConfig = {
name: "Earth",
mass: 5.972e24,
radius: 6_378_137,
polarRadius: 6_356_752,
surfaceGravity: 9.81,
rotationPeriod: 86_164,
axialTilt: 23.44,
atmosphere: earthAtmosphere,
satellites: [luna],
orbital: earthOrbit,
textures: { diffuse: "/textures/earth-diffuse.ktx2" },
};
```
## API Reference
### `AtmosphereConfig`
`surfacePressure: number`, `scaleHeight: number`, `rayleighCoefficient: number`, `mieCoefficient: number`, `extinctionPerAirMass: number`.
### `SatelliteConfig`
`name: string`, plus the same physical and orbital scalars as a planet, restricted to `eccentricity ∈ [0, 1]` and `albedo ∈ [0, 1]` at the runtime layer.
### `OrbitalConfig`
`semiMajorAxisAU`, `eccentricity`, `inclinationDeg`, `argumentOfPerihelionDeg`, `ascendingNodeDeg`, `meanAnomalyAtEpochDeg`, `epochJd`.
### `PlanetTextureManifest`
Optional `diffuse`, `normal`, `specular`, `clouds`, `night` texture URL strings.
### `PlanetConfig`
`name`, `mass`, `radius`, `polarRadius`, `surfaceGravity`, `rotationPeriod`, `axialTilt`, `atmosphere: AtmosphereConfig | null`, `satellites: SatelliteConfig[]`, optional `orbital`, optional `textures`.
## Implementation Notes
The atmosphere field is `AtmosphereConfig | null` rather than `AtmosphereConfig | undefined` so airless bodies (Mercury, Moon) are explicitly modelled rather than accidentally omitted; renderers and atmospheric solvers branch on `=== null` directly. Both equatorial `radius` and `polarRadius` are required because `PlanetSurface` constructs an oblate ellipsoid for accurate great-circle math; spherical bodies pass equal values. Orbital and texture metadata are optional because tests, fixtures, and offline renderers often only need physical scalars.

View File

@@ -0,0 +1,69 @@
# @suaveplan/geo - Abstract spherical and ellipsoidal geometry — pure math with no planet-specific constants.
## Summary
Abstract spherical and ellipsoidal geometry — pure math with no planet-specific constants. Every operation that needs a body's shape takes an explicit `Ellipsoid` parameter.
<!-- begin:auto-docs -->
## Documentation tree
- [angular](./src/angular/angular.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [angular](./src/angular/angular.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [bearing](./src/bearing/bearing.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [bearing](./src/bearing/bearing.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [bounds](./src/bounds/bounds.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [bounds](./src/bounds/bounds.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [distance](./src/distance/distance.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [distance](./src/distance/distance.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [ellipsoid](./src/ellipsoid/ellipsoid.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [ellipsoid](./src/ellipsoid/ellipsoid.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [observability](./src/observability/observability.md) — OpenTelemetry tracer wrapper for `@suaveplan/geo`.
- [observability](./src/observability/observability.md) — OpenTelemetry tracer wrapper for `@suaveplan/geo`.
- [polygon](./src/polygon/polygon.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [polygon](./src/polygon/polygon.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [transforms](./src/transforms/transforms.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [transforms](./src/transforms/transforms.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [schemas](./src/types/schemas.md) — The `schemas` module exports Zod schemas that validate every public geometric type defined in `@suaveplan/geo`. The sch…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [units](./src/units/units.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [units](./src/units/units.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [angular](./src/angular/angular.md) — <!-- begin:auto-docs --> _(no submodules)_- [bearing](./src/bearing/bearing.md) — <!-- begin:auto-docs --> _(no submodules)_- [bearing](./src/bearing/bearing.md) — <!-- begin:auto-docs --> _(no submodules)_- [bounds](./src/bounds/bounds.md) — <!-- begin:auto-docs --> _(no submodules)_- [bounds](./src/bounds/bounds.md) — <!-- begin:auto-docs --> _(no submodules)_- [distance](./src/distance/distance.md) — <!-- begin:auto-docs --> _(no submodules)_- [distance](./src/distance/distance.md) — <!-- begin:auto-docs --> _(no submodules)_- [ellipsoid](./src/ellipsoid/ellipsoid.md) — <!-- begin:auto-docs --> _(no submodules)_- [ellipsoid](./src/ellipsoid/ellipsoid.md) — <!-- begin:auto-docs --> _(no submodules)_- [polygon](./src/polygon/polygon.md) — <!-- begin:auto-docs --> _(no submodules)_- [polygon](./src/polygon/polygon.md) — <!-- begin:auto-docs --> _(no submodules)_- [transforms](./src/transforms/transforms.md) — <!-- begin:auto-docs --> _(no submodules)_- [transforms](./src/transforms/transforms.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/types/schemas.md) — The `schemas` module exports Zod schemas that validate every public geometric type defined in `@suaveplan/geo`. The sch…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [units](./src/units/units.md) — <!-- begin:auto-docs --> _(no submodules)_- [units](./src/units/units.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability.md) — OpenTelemetry tracer wrapper for `@suaveplan/geo`.
- [angular](./src/angular/angular.md) — <!-- begin:auto-docs --> _(no submodules)_- [bearing](./src/bearing/bearing.md) — <!-- begin:auto-docs --> _(no submodules)_- [bearing](./src/bearing/bearing.md) — <!-- begin:auto-docs --> _(no submodules)_- [bounds](./src/bounds/bounds.md) — <!-- begin:auto-docs --> _(no submodules)_- [bounds](./src/bounds/bounds.md) — <!-- begin:auto-docs --> _(no submodules)_- [distance](./src/distance/distance.md) — <!-- begin:auto-docs --> _(no submodules)_- [distance](./src/distance/distance.md) — <!-- begin:auto-docs --> _(no submodules)_- [ellipsoid](./src/ellipsoid/ellipsoid.md) — <!-- begin:auto-docs --> _(no submodules)_- [ellipsoid](./src/ellipsoid/ellipsoid.md) — <!-- begin:auto-docs --> _(no submodules)_- [polygon](./src/polygon/polygon.md) — <!-- begin:auto-docs --> _(no submodules)_- [polygon](./src/polygon/polygon.md) — <!-- begin:auto-docs --> _(no submodules)_- [transforms](./src/transforms/transforms.md) — <!-- begin:auto-docs --> _(no submodules)_- [transforms](./src/transforms/transforms.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/types/schemas.md) — The `schemas` module exports Zod schemas that validate every public geometric type defined in `@suaveplan/geo`. The sch…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [units](./src/units/units.md) — <!-- begin:auto-docs --> _(no submodules)_- [units](./src/units/units.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability.md) — OpenTelemetry tracer wrapper for `@suaveplan/geo`.
- [angular](./src/angular/angular.md) — <!-- begin:auto-docs --> _(no submodules)_- [bearing](./src/bearing/bearing.md) — <!-- begin:auto-docs --> _(no submodules)_- [bearing](./src/bearing/bearing.md) — <!-- begin:auto-docs --> _(no submodules)_- [bounds](./src/bounds/bounds.md) — <!-- begin:auto-docs --> _(no submodules)_- [bounds](./src/bounds/bounds.md) — <!-- begin:auto-docs --> _(no submodules)_- [distance](./src/distance/distance.md) — <!-- begin:auto-docs --> _(no submodules)_- [distance](./src/distance/distance.md) — <!-- begin:auto-docs --> _(no submodules)_- [ellipsoid](./src/ellipsoid/ellipsoid.md) — <!-- begin:auto-docs --> _(no submodules)_- [ellipsoid](./src/ellipsoid/ellipsoid.md) — <!-- begin:auto-docs --> _(no submodules)_- [polygon](./src/polygon/polygon.md) — <!-- begin:auto-docs --> _(no submodules)_- [polygon](./src/polygon/polygon.md) — <!-- begin:auto-docs --> _(no submodules)_- [transforms](./src/transforms/transforms.md) — <!-- begin:auto-docs --> _(no submodules)_- [transforms](./src/transforms/transforms.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/types/schemas.md) — The `schemas` module exports Zod schemas that validate every public geometric type defined in `@suaveplan/geo`. The sch…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [units](./src/units/units.md) — <!-- begin:auto-docs --> _(no submodules)_- [units](./src/units/units.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability.md) — OpenTelemetry tracer wrapper for `@suaveplan/geo`.
- [angular](./src/angular/angular.md) — <!-- begin:auto-docs --> _(no submodules)_- [bearing](./src/bearing/bearing.md) — <!-- begin:auto-docs --> _(no submodules)_ - [bearing](./src/bearing/bearing.md) — <!-- begin:auto-docs --> _(no submodules)_- [bounds](./src/bounds/bounds.md) — <!-- begin:auto-docs --> _(no submodules)_ - [bounds](./src/bounds/bounds.md) — <!-- begin:auto-docs --> _(no submodules)_- [distance](./src/distance/distance.md) — <!-- begin:auto-docs --> _(no submodules)_ - [distance](./src/distance/distance.md) — <!-- begin:auto-docs --> _(no submodules)_- [ellipsoid](./src/ellipsoid/ellipsoid.md) — <!-- begin:auto-docs --> _(no submodules)_ - [ellipsoid](./src/ellipsoid/ellipsoid.md) — <!-- begin:auto-docs --> _(no submodules)_- [polygon](./src/polygon/polygon.md) — <!-- begin:auto-docs --> _(no submodules)_ - [polygon](./src/polygon/polygon.md) — <!-- begin:auto-docs --> _(no submodules)_- [transforms](./src/transforms/transforms.md) — <!-- begin:auto-docs --> _(no submodules)_ - [transforms](./src/transforms/transforms.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ - [schemas](./src/types/schemas.md) — The `schemas` module exports Zod schemas that validate every public geometric type defined in `@suaveplan/geo`. The sch…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [units](./src/units/units.md) — <!-- begin:auto-docs --> _(no submodules)_ - [units](./src/units/units.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability.md) — OpenTelemetry tracer wrapper for `@suaveplan/geo`.
## Install
```bash
bun add @suaveplan/geo
```
## Quick start
```ts
import { defineEllipsoid, greatCircleDistance, bearing, destination } from "@suaveplan/geo";
const wgs84 = defineEllipsoid(6_378_137, 6_356_752.314245);
const london = { latitude: 51.5074, longitude: -0.1278 };
const paris = { latitude: 48.8566, longitude: 2.3522 };
console.log(greatCircleDistance(london, paris, wgs84, "kilometre")); // ~344
console.log(bearing(london, paris)); // ~154°
```
## Modules
- [`src/ellipsoid/ellipsoid.md`](src/ellipsoid/ellipsoid.md) — `Ellipsoid`, `defineEllipsoid`
- [`src/transforms/transforms.md`](src/transforms/transforms.md) — `sphericalToCartesian`, `cartesianToSpherical`, `toRadians`, `toDegrees`
- [`src/distance/distance.md`](src/distance/distance.md) — `greatCircleDistance`, `vincentyDistance`
- [`src/bearing/bearing.md`](src/bearing/bearing.md) — `bearing`, `destination`, `midpoint`
- [`src/angular/angular.md`](src/angular/angular.md) — `angularSeparation`
- [`src/bounds/bounds.md`](src/bounds/bounds.md) — `BoundingBox`, `createBoundingBox`, `containsPoint`, `expandBox`, `mergeBoxes`, `intersects`
- [`src/polygon/polygon.md`](src/polygon/polygon.md) — `pointInPolygon`, `polygonArea`, `polygonPerimeter`, `simplifyPolygon`
- [`src/units/units.md`](src/units/units.md) — `DistanceUnit`, `convertDistance`
- [`src/types/types.md`](src/types/types.md) — Zod schemas for all public types

View File

@@ -0,0 +1,79 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `angular` module computes the angular separation between two points
expressed as `SphericalCoord` values, returning the central angle subtended
on a unit sphere. The result is a pure spherical angle in radians,
independent of any planetary radius or ellipsoid, which makes it the right
primitive for astronomy (angular distance between celestial objects),
antenna and dish pointing, instrument field-of-view checks, and any
calculation where you want to multiply by a chosen radius later rather than
bake one in upfront. Building on this primitive keeps the higher-level
distance kernels (`greatCircleDistance`, `vincentyDistance`) free to apply
their own ellipsoid-aware radii.
## Features
- `angularSeparation(a, b)` — central angle in radians using the spherical
law of cosines.
- Inputs are degrees (matching the rest of `@suaveplan/geo`); the kernel
converts to radians internally.
- Result is clamped to `[0, π]` by guarding the `acos` argument against
floating-point drift past `±1`.
## Usage
### Basic Example
```ts
import { angularSeparation } from "@suaveplan/geo";
const angle = angularSeparation(
{ latitude: 0, longitude: 0 },
{ latitude: 0, longitude: 90 }
);
console.log(angle); // ~1.5708 radians (90°)
```
### Advanced Example
Combine `angularSeparation` with `toDegrees` and a chosen radius to compute
arc-length on bodies other than Earth — for example, on the Moon's mean
radius — without instantiating an ellipsoid:
```ts
import { angularSeparation, toDegrees } from "@suaveplan/geo";
const MOON_RADIUS_KM = 1737.4;
const sep = angularSeparation(
{ latitude: 0, longitude: 23.47 }, // Apollo 11 site
{ latitude: -3.01, longitude: -23.42 } // Apollo 12 site
);
const arcKm = sep * MOON_RADIUS_KM;
console.log(`${toDegrees(sep).toFixed(2)}° / ${arcKm.toFixed(1)} km`);
```
## API Reference
### `angularSeparation(a: SphericalCoord, b: SphericalCoord): number`
Returns the spherical angle in radians between `a` and `b`. The argument to
`Math.acos` is clamped to `[-1, 1]` so identical inputs deterministically
return `0` and antipodal inputs return `π` even when intermediate
floating-point arithmetic produces values like `1.0000000000000002`.
## Implementation Notes
The implementation uses the spherical law of cosines:
`acos(sin φ₁ sin φ₂ + cos φ₁ cos φ₂ cos Δλ)`. For separations below ~1 km
on Earth the haversine formula is more numerically stable, but for the
astronomy and pointing use cases this primitive targets — separations of
many degrees — the clamped cosine formulation is sufficient and faster.
Consumers that need sub-metre Earth-surface precision should reach for
`greatCircleDistance` (haversine) or `vincentyDistance` (ellipsoidal)
instead.

View File

@@ -0,0 +1,55 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `bearing` module computes initial bearings, destination points, and midpoints on an ellipsoid. These are the fundamental navigation primitives for routing, waypoint generation, and boundary interpolation.
## Features
- `bearing(from, to)` — initial compass bearing in degrees (0–360)
- `destination(start, bearingDeg, distanceM, ellipsoid)` — computes the point reached after travelling a given distance on a given bearing
- `midpoint(a, b)` — geographic midpoint between two coordinates
## Usage
### Basic Example
```ts
import { bearing } from "@suaveplan/geo";
const brg = bearing(
{ latitude: 51.5, longitude: -0.1 },
{ latitude: 48.9, longitude: 2.3 }
);
```
### Advanced Example
```ts
import { defineEllipsoid, destination, bearing } from "@suaveplan/geo";
const wgs84 = defineEllipsoid(6_378_137, 6_356_752.314245);
const start = { latitude: 51.5, longitude: -0.1 };
const end = destination(start, 90, 100_000, wgs84);
const back = bearing(end, start); // ~270
```
## API Reference
### `bearing(from, to): number`
Returns initial bearing in degrees clockwise from north.
### `destination(start, bearingDeg, distanceM, ellipsoid): SphericalCoord`
Returns the coordinate reached after travelling `distanceM` metres on `bearingDeg`.
### `midpoint(a, b): SphericalCoord`
Returns the geographic midpoint.
## Implementation Notes
Destination uses the spherical law of cosines with the ellipsoid's semi-major axis as radius. For high-precision geodesic destinations, consumers should iterate `bearing` + `vincentyDistance`.

View File

@@ -0,0 +1,66 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `bounds` module provides axis-aligned bounding boxes for spherical coordinates. Boxes support containment tests, expansion, merging, and intersection — the primitives needed for spatial indexing, map viewport clipping, and collision detection.
## Features
- `createBoundingBox` — construct from an array of coordinates
- `containsPoint` — point-in-box test
- `expandBox` — grow a box to include a new point
- `mergeBoxes` — combine two boxes
- `intersects` — box-box overlap test
## Usage
### Basic Example
```ts
import { createBoundingBox, containsPoint } from "@suaveplan/geo";
const box = createBoundingBox([
{ latitude: 0, longitude: 0 },
{ latitude: 10, longitude: 10 },
]);
console.log(containsPoint(box, { latitude: 5, longitude: 5 })); // true
```
### Advanced Example
```ts
import { createBoundingBox, mergeBoxes, intersects } from "@suaveplan/geo";
const a = createBoundingBox([{ latitude: 0, longitude: 0 }, { latitude: 10, longitude: 10 }]);
const b = createBoundingBox([{ latitude: 5, longitude: 5 }, { latitude: 15, longitude: 15 }]);
console.log(intersects(a, b)); // true
const merged = mergeBoxes(a, b);
```
## API Reference
### `createBoundingBox(coords): BoundingBox | null`
Returns `null` for an empty array.
### `containsPoint(box, point): boolean`
Inclusive on all boundaries.
### `expandBox(box, point): BoundingBox`
Returns a new box; does not mutate the input.
### `mergeBoxes(a, b): BoundingBox`
Returns the smallest box containing both inputs.
### `intersects(a, b): boolean`
Returns `true` if the boxes share any area.
## Implementation Notes
Bounding boxes use simple min/max on latitude and longitude. They do not handle the antimeridian (`±180°` longitude wrap). Consumers crossing the antimeridian should split geometries or use a custom coordinate system.

View File

@@ -0,0 +1,100 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `distance` module computes geodesic distances between two
`SphericalCoord` values on a configurable `Ellipsoid`. Two algorithms are
exported so callers can pick the speed/accuracy tradeoff that fits the use
case: a fast haversine great-circle approximation that treats the
ellipsoid's semi-major axis as a sphere radius, and the iterative Vincenty
inverse formula which solves on the actual ellipsoid and is accurate to
millimetres for any pair of points that aren't nearly antipodal. Both
functions return their result in a caller-chosen `DistanceUnit`, delegating
the conversion to `convertDistance` so the caller never has to remember
the metre→mile or metre→nautical-mile factor.
## Features
- `greatCircleDistance(a, b, ellipsoid, unit?)` — O(1) haversine; ideal
when the ellipsoid is approximately spherical or when ~0.5% accuracy is
acceptable.
- `vincentyDistance(a, b, ellipsoid, unit?)` — iterative inverse Vincenty
with up to 100 iterations and a `1e-12` convergence tolerance; converges
to millimetre precision on WGS-84.
- Both functions accept any `DistanceUnit` (`"metre"`, `"kilometre"`,
`"mile"`, `"nauticalMile"`) for the return value, defaulting to `"metre"`.
- Both functions handle identical inputs deterministically: distance `0`,
no NaN.
## Usage
### Basic Example
```ts
import { defineEllipsoid, greatCircleDistance } from "@suaveplan/geo";
const wgs84 = defineEllipsoid(6_378_137, 6_356_752.314245);
const km = greatCircleDistance(
{ latitude: 51.5, longitude: -0.1 }, // London
{ latitude: 48.9, longitude: 2.3 }, // Paris
wgs84,
"kilometre"
);
console.log(km.toFixed(1)); // ~344.0
```
### Advanced Example
Compare the two algorithms to characterise the haversine error against
Vincenty for a particular use case:
```ts
import {
defineEllipsoid,
greatCircleDistance,
vincentyDistance,
} from "@suaveplan/geo";
const wgs84 = defineEllipsoid(6_378_137, 6_356_752.314245);
const a = { latitude: 35.6762, longitude: 139.6503 }; // Tokyo
const b = { latitude: -33.8688, longitude: 151.2093 }; // Sydney
const haversineMetres = greatCircleDistance(a, b, wgs84, "metre");
const vincentyMetres = vincentyDistance(a, b, wgs84, "metre");
const errorPpm =
((haversineMetres - vincentyMetres) / vincentyMetres) * 1_000_000;
console.log(`haversine: ${haversineMetres.toFixed(0)} m`);
console.log(`vincenty: ${vincentyMetres.toFixed(0)} m`);
console.log(`error: ${errorPpm.toFixed(1)} ppm`);
```
## API Reference
### `greatCircleDistance(a, b, ellipsoid, unit?): number`
Haversine great-circle distance using `ellipsoid.semiMajorAxis` as the
sphere radius. `unit` defaults to `"metre"`. Treats both points as
spherical, so the answer is a great-circle arc, not an ellipsoidal
geodesic.
### `vincentyDistance(a, b, ellipsoid, unit?): number`
Vincenty's inverse formula. Iterates up to 100 times with a `1e-12`
convergence tolerance on `λ`. `unit` defaults to `"metre"`. Returns `0`
immediately for coincident points (when `sinSigma === 0`).
## Implementation Notes
Vincenty does not converge for nearly antipodal points; the 100-iteration
ceiling will simply return the last computed value rather than throw. In
production code that may receive arbitrary input, validate the input
geometry first or fall back to `greatCircleDistance` when convergence is
suspect. For all realistic Earth-surface use cases — same-continent or
even trans-oceanic — the algorithm converges in well under a dozen
iterations. The kernels read only `semiMajorAxis`, `semiMinorAxis`, and
`flattening` from the ellipsoid, so any ellipsoid produced by
`defineEllipsoid` is compatible.

View File

@@ -0,0 +1,51 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `ellipsoid` module defines the `Ellipsoid` type and the `defineEllipsoid` factory used throughout the `@suaveplan/geo` family. An ellipsoid is the geometric shape that approximates planets and moons, characterized by a semi-major axis (equatorial radius) and a semi-minor axis (polar radius). This module derives flattening and eccentricity automatically so consumers never hand-roll the same arithmetic.
## Features
- Immutable `Ellipsoid` interface with derived properties
- `defineEllipsoid(a, b)` factory that computes flattening and eccentricity
- Pure functions — no side effects, no planet-specific constants
## Usage
### Basic Example
```ts
import { defineEllipsoid } from "@suaveplan/geo/ellipsoid/ellipsoid";
const wgs84 = defineEllipsoid(6_378_137, 6_356_752.314245);
console.log(wgs84.flattening); // ~0.00335
```
### Advanced Example
```ts
import { defineEllipsoid } from "@suaveplan/geo/ellipsoid/ellipsoid";
const mars = defineEllipsoid(3_396_200, 3_376_200);
const sphere = defineEllipsoid(6_371_000, 6_371_000);
console.log(sphere.flattening); // 0
```
## API Reference
### `interface Ellipsoid`
- `semiMajorAxis: number` — equatorial radius in metres
- `semiMinorAxis: number` — polar radius in metres
- `flattening: number` — derived `(a − b) / a`
- `eccentricity: number` — derived from flattening
### `defineEllipsoid(semiMajorAxis, semiMinorAxis): Ellipsoid`
Creates an `Ellipsoid` from the given axes and derives flattening and eccentricity.
## Implementation Notes
Eccentricity is computed as `sqrt(2f − f²)` where `f` is flattening. For a perfect sphere (`a === b`), both flattening and eccentricity are exactly zero.

View File

@@ -0,0 +1,17 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
OpenTelemetry tracer wrapper for `@suaveplan/geo`.
## Usage
```ts
import { withSpan } from "@suaveplan/geo";
const result = withSpan("compute", () => compute());
```

View File

@@ -0,0 +1,63 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `polygon` module provides point-in-polygon, area, perimeter, and simplification for spherical polygons. These are essential for geofencing, territory mapping, and route buffer calculations.
## Features
- `pointInPolygon` — ray-casting algorithm for spherical coordinates
- `polygonArea` — spherical excess area on a given ellipsoid
- `polygonPerimeter` — sum of geodesic edge lengths
- `simplifyPolygon` — Douglas-Peucker with great-circle perpendicular distance
## Usage
### Basic Example
```ts
import { pointInPolygon } from "@suaveplan/geo";
const square = [
{ latitude: 0, longitude: 0 },
{ latitude: 0, longitude: 10 },
{ latitude: 10, longitude: 10 },
{ latitude: 10, longitude: 0 },
];
console.log(pointInPolygon({ latitude: 5, longitude: 5 }, square)); // true
```
### Advanced Example
```ts
import { defineEllipsoid, polygonArea, simplifyPolygon } from "@suaveplan/geo";
const wgs84 = defineEllipsoid(6_378_137, 6_356_752.314245);
const complex = [/* many points */];
const simple = simplifyPolygon(complex, 100, wgs84);
const area = polygonArea(simple, wgs84);
```
## API Reference
### `pointInPolygon(point, polygon): boolean`
Ray-casting (even-odd rule). Does not handle polygons that cross the antimeridian.
### `polygonArea(polygon, ellipsoid): number`
Returns area in square metres. Returns `0` for fewer than 3 vertices.
### `polygonPerimeter(polygon, ellipsoid): number`
Returns perimeter in metres using `greatCircleDistance` for each edge.
### `simplifyPolygon(polygon, toleranceMetres, ellipsoid): SphericalCoord[]`
Douglas-Peucker simplification. Returns the original polygon if it has 2 or fewer points.
## Implementation Notes
`polygonArea` uses the spherical excess formula with the ellipsoid's semi-major axis as radius. `simplifyPolygon` computes perpendicular distance using great-circle distances on the supplied ellipsoid rather than planar Euclidean distance, preserving accuracy for large polygons.

View File

@@ -0,0 +1,59 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `transforms` module converts between spherical coordinates (latitude, longitude, altitude) and 3-D Cartesian vectors on a given ellipsoid. It also provides radian/degree helpers used by every other module in `@suaveplan/geo`.
## Features
- `sphericalToCartesian` — converts lat/lon/alt to ECEF (Earth-Centered Earth-Fixed) Cartesian
- `cartesianToSpherical` — inverse conversion using Bowring's iterative method
- `toRadians` / `toDegrees` — conversion helpers
- `SphericalCoord`, `Vec3`, `Mat3x3` types
## Usage
### Basic Example
```ts
import { defineEllipsoid, sphericalToCartesian } from "@suaveplan/geo";
const wgs84 = defineEllipsoid(6_378_137, 6_356_752.314245);
const vec = sphericalToCartesian({ latitude: 51.5, longitude: -0.1, altitude: 0 }, wgs84);
```
### Advanced Example
```ts
import { defineEllipsoid, sphericalToCartesian, cartesianToSpherical } from "@suaveplan/geo";
const wgs84 = defineEllipsoid(6_378_137, 6_356_752.314245);
const coord = { latitude: 51.5, longitude: -0.1, altitude: 100 };
const vec = sphericalToCartesian(coord, wgs84);
const back = cartesianToSpherical(vec, wgs84);
console.log(back.latitude); // ~51.5
```
## API Reference
### `sphericalToCartesian(coord, ellipsoid): Vec3`
Converts a `SphericalCoord` to a Cartesian `Vec3` on the given `Ellipsoid`.
### `cartesianToSpherical(vec, ellipsoid): SphericalCoord`
Converts a `Vec3` back to a `SphericalCoord` on the given `Ellipsoid`.
### `toRadians(degrees): number`
Converts degrees to radians.
### `toDegrees(radians): number`
Converts radians to degrees.
## Implementation Notes
Cartesian-to-spherical uses Bowring's iterative formula which converges rapidly for all latitudes including the poles. The prime vertical radius of curvature `N` is computed from the eccentricity squared.

View File

@@ -0,0 +1,102 @@
## Purpose
The `schemas` module exports Zod schemas that validate every public geometric
type defined in `@suaveplan/geo`. The schemas are used at API boundaries —
HTTP handlers, message-bus envelopes, persistence reads — wherever runtime
input must be coerced and rejected before it reaches the geometry kernels.
Pairing the schemas with the typed interfaces in `types.ts` lets consumers
infer the same TypeScript shape via `z.infer` without re-declaring a parallel
type, eliminating drift between compile-time and runtime contracts.
## Features
- `EllipsoidSchema` — validates `semiMajorAxis`/`semiMinorAxis` are positive
numbers and `flattening`/`eccentricity` are finite numbers.
- `SphericalCoordSchema` — clamps `latitude` to `[-90, 90]`, `longitude` to
`[-180, 180]`, and accepts an optional `altitude` number.
- `Vec3Schema` — three finite-number Cartesian components.
- `Mat3x3Schema` — nine finite-number row-major rotation-matrix entries.
- `BoundingBoxSchema` — both latitude bounds in `[-90, 90]` and both
longitude bounds in `[-180, 180]`.
- `DistanceUnitSchema` — enum over `"metre" | "kilometre" | "mile" |
"nauticalMile"`.
## Usage
### Basic Example
```ts
import { SphericalCoordSchema } from "@suaveplan/geo";
const coord = SphericalCoordSchema.parse({
latitude: 51.5,
longitude: -0.1,
});
```
### Advanced Example
```ts
import {
BoundingBoxSchema,
DistanceUnitSchema,
EllipsoidSchema,
} from "@suaveplan/geo";
import type { z } from "zod";
type ParsedEllipsoid = z.infer<typeof EllipsoidSchema>;
function loadConfig(raw: unknown): {
ellipsoid: ParsedEllipsoid;
box: z.infer<typeof BoundingBoxSchema>;
unit: z.infer<typeof DistanceUnitSchema>;
} {
const obj = raw as Record<string, unknown>;
return {
ellipsoid: EllipsoidSchema.parse(obj.ellipsoid),
box: BoundingBoxSchema.parse(obj.box),
unit: DistanceUnitSchema.parse(obj.unit),
};
}
```
## API Reference
### `EllipsoidSchema`
`z.object({ semiMajorAxis: z.number().positive(), semiMinorAxis:
z.number().positive(), flattening: z.number(), eccentricity: z.number() })`.
Throws `ZodError` on negative or non-finite axes.
### `SphericalCoordSchema`
`z.object({ latitude: z.number().min(-90).max(90), longitude:
z.number().min(-180).max(180), altitude: z.number().optional() })`.
### `Vec3Schema`
`z.object({ x, y, z })` — three `z.number()` fields.
### `Mat3x3Schema`
`z.object({ m00..m22 })` — nine `z.number()` fields in row-major order.
### `BoundingBoxSchema`
`z.object({ minLatitude, maxLatitude, minLongitude, maxLongitude })` with
each value clamped to spherical ranges. Note that the schema does NOT verify
`minLatitude <= maxLatitude`; that ordering check is the caller's job.
### `DistanceUnitSchema`
`z.enum(["metre", "kilometre", "mile", "nauticalMile"])`.
## Implementation Notes
The schemas are deliberately permissive on cross-field invariants — for
example, `BoundingBoxSchema` does not verify that `min` ≤ `max`, and
`EllipsoidSchema` does not verify that `semiMinorAxis ≤ semiMajorAxis`.
Cross-field invariants belong with the constructors (`createBoundingBox`,
`defineEllipsoid`), which are tested for those invariants separately. The
schemas exist to reject malformed wire data, not to substitute for the
domain constructors.

View File

@@ -0,0 +1,114 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `types` module declares the shared TypeScript interfaces and enums that
every other module in `@suaveplan/geo` consumes — `Ellipsoid`,
`SphericalCoord`, `Vec3`, `Mat3x3`, `BoundingBox`, and the `DistanceUnit`
string-literal union. Centralising the shapes here keeps the rest of the
package free of duplicate declarations and ensures that any change to a
core geometric structure has exactly one source of truth. The module is
runtime-free: it emits zero JavaScript and is safe to import from any
tier (universal, browser, or server).
## Features
- `Ellipsoid` — readonly `semiMajorAxis`/`semiMinorAxis`/`flattening`/
`eccentricity`; produced by `defineEllipsoid`.
- `SphericalCoord` — `latitude`, `longitude`, optional `altitude` (all
degrees / metres).
- `Vec3` — `x`, `y`, `z` Cartesian components.
- `Mat3x3` — nine row-major rotation-matrix entries (`m00`..`m22`).
- `BoundingBox` — `minLatitude`, `maxLatitude`, `minLongitude`,
`maxLongitude`.
- `DistanceUnit` — `"metre" | "kilometre" | "mile" | "nauticalMile"`.
## Usage
### Basic Example
```ts
import type { Ellipsoid, SphericalCoord } from "@suaveplan/geo";
function describe(point: SphericalCoord, ellipsoid: Ellipsoid): string {
return `(${point.latitude}, ${point.longitude}) on ${ellipsoid.semiMajorAxis}m sphere`;
}
```
### Advanced Example
Compose the types into a domain-specific record without re-declaring any
field shapes:
```ts
import type {
BoundingBox,
DistanceUnit,
SphericalCoord,
} from "@suaveplan/geo";
interface Region {
name: string;
centre: SphericalCoord;
bbox: BoundingBox;
measurementUnit: DistanceUnit;
}
const london: Region = {
name: "Greater London",
centre: { latitude: 51.5074, longitude: -0.1278 },
bbox: {
minLatitude: 51.28,
maxLatitude: 51.69,
minLongitude: -0.51,
maxLongitude: 0.33,
},
measurementUnit: "kilometre",
};
```
## API Reference
### `Ellipsoid`
`{ readonly semiMajorAxis: number; readonly semiMinorAxis: number;
readonly flattening: number; readonly eccentricity: number }`. All values
are in metres except the dimensionless `flattening` and `eccentricity`.
### `SphericalCoord`
`{ latitude: number; longitude: number; altitude?: number }`. `latitude`
and `longitude` are degrees; `altitude` (when present) is metres above
the ellipsoid surface.
### `Vec3`
`{ x: number; y: number; z: number }` — Cartesian components, typically in
ECEF metres.
### `Mat3x3`
Nine numeric fields `m00`..`m22` in row-major order. The rotation
operators in the `transforms` module produce and consume this shape.
### `BoundingBox`
`{ minLatitude: number; maxLatitude: number; minLongitude: number;
maxLongitude: number }`. Caller is responsible for ensuring `min ≤ max`;
`createBoundingBox` enforces this invariant.
### `DistanceUnit`
`"metre" | "kilometre" | "mile" | "nauticalMile"` — accepted everywhere a
unit is required. Conversion factors live in `units.ts`.
## Implementation Notes
The interfaces are intentionally structural — there are no nominal brands
— so values produced by other libraries that match the shape are accepted
without conversion. Runtime validation lives in the sibling `schemas.ts`
module via Zod, which mirrors these shapes one-for-one. When you change a
type here, update the matching schema in the same commit; the package's
test suite includes round-trip checks that catch drift between the two.

View File

@@ -0,0 +1,95 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `units` module converts scalar distance values between metres,
kilometres, statute miles, and nautical miles. It is the single conversion
hub used by every higher-level kernel in `@suaveplan/geo` — every distance
function returns its result by piping the kernel-internal metre value
through `convertDistance` to the caller's chosen unit, so consumers never
have to remember a conversion factor or copy-paste one. Exposing the
function publicly also lets callers normalise mixed-unit external inputs
(e.g. an aviation feed that mixes nautical miles and metres) into a single
working unit before invoking other geometry primitives.
## Features
- `convertDistance(value, from, to)` — bidirectional conversion across all
supported units in O(1).
- `DistanceUnit` string-literal union — `"metre" | "kilometre" | "mile" |
"nauticalMile"`.
- Identity short-circuit: when `from === to` the input is returned as-is
without any floating-point round-trip.
- Conversion factors are exact per international standards (no
hand-rounded constants).
## Usage
### Basic Example
```ts
import { convertDistance } from "@suaveplan/geo";
const km = convertDistance(1000, "metre", "kilometre"); // 1
const miles = convertDistance(10_000, "metre", "mile"); // 6.2137...
```
### Advanced Example
Build a unit-aware helper that accepts any input unit and emits a
canonicalised metre value for downstream geometry kernels:
```ts
import {
convertDistance,
type DistanceUnit,
greatCircleDistance,
defineEllipsoid,
} from "@suaveplan/geo";
interface Reading {
value: number;
unit: DistanceUnit;
}
function toMetres(r: Reading): number {
return convertDistance(r.value, r.unit, "metre");
}
const wgs84 = defineEllipsoid(6_378_137, 6_356_752.314245);
const allowance = toMetres({ value: 5, unit: "nauticalMile" }); // 9260
const distance = greatCircleDistance(
{ latitude: 51.5, longitude: -0.1 },
{ latitude: 48.9, longitude: 2.3 },
wgs84,
"metre"
);
const withinRange = distance <= allowance;
```
## API Reference
### `convertDistance(value: number, from: DistanceUnit, to: DistanceUnit): number`
Returns `value` expressed in `to`. When `from === to` the function returns
`value` unchanged — no multiplication and no division, which keeps the
identity case bit-exact. Otherwise it computes
`value * factor[from] / factor[to]` where the factor table is
`{ metre: 1, kilometre: 1000, mile: 1609.344, nauticalMile: 1852 }`.
### `type DistanceUnit`
`"metre" | "kilometre" | "mile" | "nauticalMile"`. Re-exported from the
package root.
## Implementation Notes
Conversion factors are exact under the modern international definitions:
1 statute mile = 1609.344 m and 1 nautical mile = 1852 m. The function
performs at most one multiply and one divide, so the output is within
one ULP of the mathematical result and round-trip
(`x → from → to → from`) is stable. No rounding is applied internally;
callers that need a fixed display precision should round at presentation
time.

View File

@@ -0,0 +1,19 @@
# @suaveplan/solar-system - Solar-system provider with injectable astronomy engine and season/equinox/solstice/sunrise/sunset/lunar-phase calculations; emits CelestialBody records for the Sun, Moon, and inner/outer planets via a SolarProvider contract.
## Summary
Solar system provider with astronomy engine.
<!-- begin:auto-docs -->
## Documentation tree
- [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 -->
- [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_- [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_- [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_- [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_- [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_- [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_- [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_ - [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_
## Install
```bash
bun add @suaveplan/solar-system
```

View File

@@ -0,0 +1,61 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Composes a `SolarProvider` (from `@suaveplan/types`) over an injectable `AstronomyEngine` so callers can swap a deterministic stub, a high-precision ephemeris (e.g. VSOP87, JPL DE), or a remote API without touching downstream code. The provider emits `CelestialBody` records for the Sun, Moon, and the classical naked-eye planets, and delegates sunrise/sunset to the `seasons` module so a single calling site covers the full solar-day surface.
## Features
- `AstronomyEngine` interface — single seam (`bodyPosition(name, jd)`) for arbitrary ephemeris back-ends.
- `DefaultEngine` — pure-deterministic placeholder useful for tests, demos, and offline builds.
- `createSolarSystemProvider(engine?)` — factory returning a `SolarProvider` with sensible defaults.
- Returns the seven canonical bodies (Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn) in `getBodies()`.
- Per-body lookup via `getBody(name)` with automatic `"star" | "moon" | "planet"` typing.
- Sunrise / sunset honour an `Observer` argument so future engines can plug in topocentric refinements.
## Usage: Basic Example
```ts
import { createSolarSystemProvider } from "@suaveplan/solar-system";
const provider = createSolarSystemProvider();
const sun = await provider.getBody("Sun");
console.log(sun.type); // "star"
```
## Usage: Advanced Example
```ts
import {
type AstronomyEngine,
createSolarSystemProvider,
} from "@suaveplan/solar-system";
class Vsop87Engine implements AstronomyEngine {
bodyPosition(name: string, jd: number) {
// delegate to a real ephemeris — values are heliocentric AU
return computeVsop87(name, jd);
}
}
const provider = createSolarSystemProvider(new Vsop87Engine());
const bodies = await provider.getBodies();
const observer = {
kind: "surface" as const,
planet: { name: "Earth", radius: 6_371_000, rotationPeriod: 86_164 },
coord: { latitude: 51.5, longitude: -0.1 },
};
const sunrise = await provider.getSunrise(observer, new Date());
```
## API Reference
- `interface AstronomyEngine` — `bodyPosition(name: string, jd: number): { x: number; y: number; z: number }`. Returns heliocentric coordinates (AU) at the given Julian Date.
- `class DefaultEngine implements AstronomyEngine` — deterministic placeholder; `bodyPosition` returns `{ x, y, z }` derived from the first character code of `name`, yielding `{0,0,0}` for empty names.
- `createSolarSystemProvider(engine?: AstronomyEngine): SolarProvider` — `engine` defaults to a fresh `DefaultEngine`. Returned provider exposes `getBodies()`, `getBody(name)`, `getSunrise(observer, date)`, and `getSunset(observer, date)`. None of the methods throw under normal usage; engine errors surface as rejected promises.
## Implementation Notes
The provider is intentionally engine-agnostic: every astronomical computation flows through `AstronomyEngine.bodyPosition`, so swapping engines never requires editing `provider.ts`. `makeBody` currently fixes apparent magnitude, angular diameter, and topocentric altitude/azimuth — these are hooks for future engine extensions rather than computed values, and call sites should treat them as best-effort placeholders until a precision engine is wired in. Sunrise and sunset are delegated to the sibling `seasons` module to keep the time-of-day surface cohesive and testable. The factory accepts an `engine` argument so consumers can substitute deterministic engines in tests without `vi.mock`.

View File

@@ -0,0 +1,65 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Pure functions covering the solar-calendar surface: meteorological seasons, the next equinox / solstice after a given instant, sunrise / sunset offsets, and lunar-phase fraction. The module is intentionally side-effect-free and dependency-free so it ships in universal bundles (browser, server, R3F, edge) and remains trivial to substitute with a high-precision ephemeris later.
## Features
- `getSeason(date)` — returns one of `"spring" | "summer" | "autumn" | "winter"` using meteorological month boundaries (UTC).
- `getNextEquinox(date)` / `getNextSolstice(date)` — placeholder implementations returning the next canonical event Date.
- `getSunrise(date)` / `getSunset(date)` — return Dates offset by 6 h and 18 h respectively, suitable for tests and as a default until a precision engine is wired into `provider.ts`.
- `getLunarPhase(date)` — returns a fraction in `[0, 1]` (currently a constant `0.5` placeholder).
- Zero runtime dependencies; all functions are deterministic and synchronous.
- Designed to be re-implemented behind the same signatures with a real ephemeris (e.g. SunCalc, Astronomy Engine) without changing call sites.
## Usage: Basic Example
```ts
import { getSeason } from "@suaveplan/solar-system";
const season = getSeason(new Date("2026-04-15T00:00:00Z"));
console.log(season); // "spring"
```
## Usage: Advanced Example
```ts
import {
getLunarPhase,
getNextEquinox,
getNextSolstice,
getSeason,
getSunrise,
getSunset,
} from "@suaveplan/solar-system";
const today = new Date();
const summary = {
season: getSeason(today),
sunrise: getSunrise(today),
sunset: getSunset(today),
nextEquinox: getNextEquinox(today),
nextSolstice: getNextSolstice(today),
moonPhase: getLunarPhase(today),
};
// Pipe into a UI panel, ICS export, or scheduling rule.
console.log(summary);
```
## API Reference
- `type Season = "spring" | "summer" | "autumn" | "winter"` — UTC-month-based labelling.
- `getSeason(date: Date): Season` — Mar–May spring, Jun–Aug summer, Sep–Nov autumn, otherwise winter.
- `getNextEquinox(date: Date): Date` — currently returns the canonical March 2026 equinox; signature stable for a future precision impl.
- `getNextSolstice(date: Date): Date` — currently returns the canonical June 2026 solstice; signature stable.
- `getSunrise(date: Date): Date` — returns `date + 6h`.
- `getSunset(date: Date): Date` — returns `date + 18h`.
- `getLunarPhase(date: Date): number` — fraction in `[0, 1]`; `0` = new moon, `0.5` = full moon.
## Implementation Notes
The package favours a stable public surface over astronomical precision so swapping in a real ephemeris is a non-breaking change. `getSeason` uses UTC months to stay deterministic across runtimes — callers in the southern hemisphere should remap the labels at the application layer. The sunrise / sunset offsets are deliberate placeholders that satisfy `provider.getSunrise/getSunset` contracts (the returned Date is strictly after the input Date). When integrating a precision engine, replace function bodies in-place; tests rely only on monotonicity and return-type invariants, not exact times.

View File

@@ -0,0 +1,24 @@
# @suaveplan/stars - Star catalog and observation provider: in-memory catalog source, multi-source aggregating provider, cone queries, and observed-star projections built on the @suaveplan/types StarProvider contract.
## Summary
Star catalog provider.
<!-- begin:auto-docs -->
## Documentation tree
- [catalog](./src/catalog/catalog.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [catalog](./src/catalog/catalog.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 -->
- [types](./src/types.md) — The types module declares the two foundational shapes that `@suaveplan/stars` is built on: `RawStar`, the lossless cata…
<!-- end:auto-docs --> - [catalog](./src/catalog/catalog.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)_- [types](./src/types.md) — The types module declares the two foundational shapes that `@suaveplan/stars` is built on: `RawStar`, the lossless cata…
- [catalog](./src/catalog/catalog.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)_- [types](./src/types.md) — The types module declares the two foundational shapes that `@suaveplan/stars` is built on: `RawStar`, the lossless cata…
- [catalog](./src/catalog/catalog.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)_- [types](./src/types.md) — The types module declares the two foundational shapes that `@suaveplan/stars` is built on: `RawStar`, the lossless cata…
- [catalog](./src/catalog/catalog.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)_- [types](./src/types.md) — The types module declares the two foundational shapes that `@suaveplan/stars` is built on: `RawStar`, the lossless cata…
## Install
```bash
bun add @suaveplan/stars
```

View File

@@ -0,0 +1,50 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The catalog module provides an in-memory implementation of the `StarCatalogSource` contract used by `@suaveplan/stars` providers. It accepts a static array of `RawStar` records and exposes them through the same `queryCone` and `getStar` surface that production catalog adapters (Hipparcos, Tycho-2, Gaia DR3) implement, so consumers can swap real catalogs for fixtures without touching call sites.
It is the recommended source for tests, demos, offline planetariums, and bootstrap data flows where pulling a remote catalog is unnecessary or undesirable. The implementation favours predictability over scale: lookups are O(n) for cone queries and O(1) for id resolution backed by a single `Map`.
## Features
- Pure in-memory `StarCatalogSource` with no external dependencies.
- Constant-time `getStar(catalogId)` lookup via internal `Map`.
- Naive Cartesian cone filter on `(ra, dec)` for deterministic test output.
- Returns the same `RawStar` shape consumed by `createStarProvider`.
- Zero allocations on the hot path beyond the result array.
## Usage: Basic Example
```ts
import { createInMemoryCatalogSource } from "@suaveplan/stars";
const source = createInMemoryCatalogSource([
{ catalogId: "HIP-32349", ra: 101.287, dec: -16.716, apparentMagnitude: -1.46 },
]);
const sirius = await source.getStar("HIP-32349");
```
## Usage: Advanced Example
```ts
import { createInMemoryCatalogSource, createStarProvider } from "@suaveplan/stars";
const source = createInMemoryCatalogSource([
{ catalogId: "HIP-32349", ra: 101.287, dec: -16.716, apparentMagnitude: -1.46, spectralClass: "A1V", distance: 2.64 },
{ catalogId: "HIP-37279", ra: 114.825, dec: 5.225, apparentMagnitude: 0.4, spectralClass: "F5IV-V" },
]);
const provider = createStarProvider([source]);
const nearby = await provider.queryCone(101, -16, 5);
```
## API Reference
- `createInMemoryCatalogSource(stars: RawStar[]): StarCatalogSource` — builds a source whose `queryCone(ra, dec, radiusDeg)` returns every star inside the Cartesian disk of the given radius (degrees) around the centre, and whose `getStar(catalogId)` returns the matching record or `null`. Both methods are async to satisfy the interface; neither throws.
## Implementation Notes
The cone filter uses Euclidean distance on raw `(ra, dec)` pairs rather than a spherical-trig great-circle distance. This is deliberate: the source is for fixtures and small catalogs, where simplicity and determinism beat correctness near the poles or across the RA wrap. For real-sky use, layer a cosine-correction adapter or replace the source with a HEALPix-backed implementation. The internal `Map` is built once at construction; mutating the input array post-construction is not supported.

View File

@@ -0,0 +1,57 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The provider module composes one or more `StarCatalogSource` instances into a single `StarProvider` (from `@suaveplan/types`) that consumers — sky renderers, planetarium UIs, telescope planners — can program against without binding to a specific catalog backend. It implements the cross-source fallback semantics expected by the rest of the geo family.
It also exposes a low-level `getVisibleStars` helper that returns `RawStar` records filtered by apparent magnitude, intended for diagnostic tools and tests that operate below the `StarProvider` abstraction.
## Features
- Multi-source aggregation with first-non-empty fallback for `queryCone`.
- First-hit short-circuit for `getStar` across sources.
- `getVisibleStars` projects every queried star into an `ObservedStar` with placeholder altitude/azimuth/extincted-magnitude fields suitable for the contract surface.
- Total support for the optional `RawStar` columns (`spectralClass`, `distance`, `properMotionRa`, `properMotionDec`, `parallax`) — present fields are forwarded, absent ones omitted under `exactOptionalPropertyTypes`.
- Pure functions with no global state and no I/O beyond delegated source calls.
## Usage: Basic Example
```ts
import { createInMemoryCatalogSource, createStarProvider } from "@suaveplan/stars";
const source = createInMemoryCatalogSource([
{ catalogId: "HIP-32349", ra: 101.287, dec: -16.716, apparentMagnitude: -1.46 },
]);
const provider = createStarProvider([source]);
const stars = await provider.queryCone(101, -16, 5);
```
## Usage: Advanced Example
```ts
import { createInMemoryCatalogSource, createStarProvider, getVisibleStars } from "@suaveplan/stars";
const primary = createInMemoryCatalogSource([]);
const fallback = createInMemoryCatalogSource([
{ catalogId: "HIP-37279", ra: 114.825, dec: 5.225, apparentMagnitude: 0.4 },
{ catalogId: "HIP-91262", ra: 279.234, dec: 38.784, apparentMagnitude: 0.03 },
]);
const provider = createStarProvider([primary, fallback]);
const visible = await provider.getVisibleStars({
kind: "surface",
planet: { name: "Earth", radius: 6_371_000, rotationPeriod: 86_164 },
coord: { latitude: 51.4779, longitude: -0.0015 },
});
const bright = await getVisibleStars([fallback], { latitude: 0, longitude: 0 }, 0, 1);
```
## API Reference
- `createStarProvider(sources: StarCatalogSource[]): StarProvider` — returns a `StarProvider` whose `queryCone(ra, dec, radiusDeg)` walks `sources` in order and returns the first non-empty result mapped to `CatalogStar`; whose `getStar(catalogId)` returns the first match across sources or `null`; and whose `getVisibleStars(observer)` returns every star across all sources projected to `ObservedStar`.
- `getVisibleStars(sources, observer, minAltitudeDeg?, maxMagnitude?): Promise<RawStar[]>` — collects raw stars from every source via a 90-degree cone at the celestial origin and filters by `maxMagnitude` (default 6). `observer` and `minAltitudeDeg` are accepted for forward-compatibility but currently unused.
## Implementation Notes
`toCatalogStar` uses guarded `Object.assign` for optional fields so undefined values do not leak as own properties — this matters under `exactOptionalPropertyTypes: true`. `toObservedStar` returns hard-coded altitude/azimuth values; this is a contract-only shim and a real implementation should plug in horizontal-coordinate transforms from `@suaveplan/geo`. `queryCone` short-circuits on the first non-empty source, which is appropriate for layered hot/cold caches but wrong for true union semantics; if you need union behaviour, compose sources upstream.

View File

@@ -0,0 +1,48 @@
## Purpose
The types module declares the two foundational shapes that `@suaveplan/stars` is built on: `RawStar`, the lossless catalog record returned by every source, and `StarCatalogSource`, the source contract that providers compose. They sit deliberately one layer below `@suaveplan/types`'s `CatalogStar` / `ObservedStar` / `StarProvider` so that catalog-backend authors have a stable, narrowly-scoped surface to implement against.
Keeping these types local to the `stars` package — instead of in `@suaveplan/types` — avoids forcing every downstream universe-tier consumer to take on the source-adapter shape, while still letting the provider layer translate cleanly into the public `StarProvider` contract.
## Features
- `RawStar` carries the required astrometry (`catalogId`, `ra`, `dec`, `apparentMagnitude`) plus optional photometric and motion columns (`spectralClass`, `distance`, `properMotionRa`, `properMotionDec`, `parallax`).
- `StarCatalogSource` is a structural interface — any object meeting the shape works, no class inheritance required.
- Async-by-default methods so HTTP-, file-, and DB-backed sources fit without wrapping.
- No runtime cost; the module is types-only and erased at build time.
## Usage: Basic Example
```ts
import type { RawStar, StarCatalogSource } from "@suaveplan/stars";
const star: RawStar = { catalogId: "HIP-32349", ra: 101.287, dec: -16.716, apparentMagnitude: -1.46 };
```
## Usage: Advanced Example
```ts
import type { RawStar, StarCatalogSource } from "@suaveplan/stars";
class HttpCatalogSource implements StarCatalogSource {
constructor(private readonly endpoint: string) {}
async queryCone(ra: number, dec: number, radiusDeg: number): Promise<RawStar[]> {
const url = `${this.endpoint}/cone?ra=${ra}&dec=${dec}&r=${radiusDeg}`;
const res = await fetch(url);
return (await res.json()) as RawStar[];
}
async getStar(catalogId: string): Promise<RawStar | null> {
const res = await fetch(`${this.endpoint}/star/${encodeURIComponent(catalogId)}`);
return res.ok ? ((await res.json()) as RawStar) : null;
}
}
```
## API Reference
- `RawStar` — `{ catalogId: string; ra: number; dec: number; apparentMagnitude: number; spectralClass?: string; distance?: number; properMotionRa?: number; properMotionDec?: number; parallax?: number }`. Right ascension and declination are degrees (J2000); apparent magnitude is the visual band; distance is parsecs; proper motions are mas/yr; parallax is mas.
- `StarCatalogSource` — `{ queryCone(ra, dec, radiusDeg): Promise<RawStar[]>; getStar(catalogId): Promise<RawStar | null> }`. Implementations must be referentially safe (no shared mutable result arrays) and must not throw for missing ids — return `null` instead.
## Implementation Notes
`RawStar` intentionally omits derived horizontal coordinates and extincted magnitudes — those belong on `ObservedStar` and are computed by the provider layer against an observer pose. The optional fields use `T | undefined` rather than nullable so authors can `Object.assign` only when present (see `provider.ts`), which keeps records small and TypeScript happy under `exactOptionalPropertyTypes`.

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.

View File

@@ -0,0 +1,27 @@
# @suaveplan/universe - Universe-scene aggregation client: typed solar / star / tidal / weather provider interfaces, observer-position discriminated union, coordinate-frame conversions, and binary packet codecs for celestial bodies and stars.
## Summary
Observer model, coordinate frames, wire protocol, and provider registry.
<!-- begin:auto-docs -->
## Documentation tree
- [client](./src/client/client.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [client](./src/client/client.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [frames](./src/frames/frames.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [frames](./src/frames/frames.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [observer](./src/observer/observer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [observer](./src/observer/observer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [packets](./src/packets/packets.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [packets](./src/packets/packets.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [providers](./src/providers/providers.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [providers](./src/providers/providers.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 --> - [client](./src/client/client.md) — <!-- begin:auto-docs --> _(no submodules)_- [frames](./src/frames/frames.md) — <!-- begin:auto-docs --> _(no submodules)_- [frames](./src/frames/frames.md) — <!-- begin:auto-docs --> _(no submodules)_- [observer](./src/observer/observer.md) — <!-- begin:auto-docs --> _(no submodules)_- [observer](./src/observer/observer.md) — <!-- begin:auto-docs --> _(no submodules)_- [packets](./src/packets/packets.md) — <!-- begin:auto-docs --> _(no submodules)_- [packets](./src/packets/packets.md) — <!-- begin:auto-docs --> _(no submodules)_- [providers](./src/providers/providers.md) — <!-- begin:auto-docs --> _(no submodules)_- [providers](./src/providers/providers.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)_- [client](./src/client/client.md) — <!-- begin:auto-docs --> _(no submodules)_- [frames](./src/frames/frames.md) — <!-- begin:auto-docs --> _(no submodules)_- [frames](./src/frames/frames.md) — <!-- begin:auto-docs --> _(no submodules)_- [observer](./src/observer/observer.md) — <!-- begin:auto-docs --> _(no submodules)_- [observer](./src/observer/observer.md) — <!-- begin:auto-docs --> _(no submodules)_- [packets](./src/packets/packets.md) — <!-- begin:auto-docs --> _(no submodules)_- [packets](./src/packets/packets.md) — <!-- begin:auto-docs --> _(no submodules)_- [providers](./src/providers/providers.md) — <!-- begin:auto-docs --> _(no submodules)_- [providers](./src/providers/providers.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)_- [client](./src/client/client.md) — <!-- begin:auto-docs --> _(no submodules)_- [frames](./src/frames/frames.md) — <!-- begin:auto-docs --> _(no submodules)_- [frames](./src/frames/frames.md) — <!-- begin:auto-docs --> _(no submodules)_- [observer](./src/observer/observer.md) — <!-- begin:auto-docs --> _(no submodules)_- [observer](./src/observer/observer.md) — <!-- begin:auto-docs --> _(no submodules)_- [packets](./src/packets/packets.md) — <!-- begin:auto-docs --> _(no submodules)_- [packets](./src/packets/packets.md) — <!-- begin:auto-docs --> _(no submodules)_- [providers](./src/providers/providers.md) — <!-- begin:auto-docs --> _(no submodules)_- [providers](./src/providers/providers.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)_- [client](./src/client/client.md) — <!-- begin:auto-docs --> _(no submodules)_- [frames](./src/frames/frames.md) — <!-- begin:auto-docs --> _(no submodules)_ - [frames](./src/frames/frames.md) — <!-- begin:auto-docs --> _(no submodules)_- [observer](./src/observer/observer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [observer](./src/observer/observer.md) — <!-- begin:auto-docs --> _(no submodules)_- [packets](./src/packets/packets.md) — <!-- begin:auto-docs --> _(no submodules)_ - [packets](./src/packets/packets.md) — <!-- begin:auto-docs --> _(no submodules)_- [providers](./src/providers/providers.md) — <!-- begin:auto-docs --> _(no submodules)_ - [providers](./src/providers/providers.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/universe
```

View File

@@ -0,0 +1,138 @@
# UniverseClient
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`UniverseClient` is the single composition point that aggregates solar, star, tidal, and weather provider implementations into one cohesive `UniverseScene` snapshot for a given observer at a given instant. It owns no domain knowledge of its own — every datum on the returned scene is delegated to a provider the consumer registered. This indirection lets the same client power a real-time astronomy app, a deterministic game world, a server-rendered ephemeris page, or a unit test, simply by swapping providers.
The client is intentionally minimal: no caching, no retry, no fan-out; it propagates the caller's `AbortSignal` to every registered provider and assembles their results into one `UniverseScene` value. Tides are skipped when the observer is in space (no surface to compute against). Unregistered domains contribute empty arrays (`bodies`, `stars`) or are omitted entirely (`tides`, `weather`).
## Features
- Fluent registration: `registerSolar` / `registerStars` / `registerTides` / `registerWeather` chain on `this`.
- One-shot scene assembly via `getScene(observer, time, signal?)`.
- AbortSignal forwarded to every provider call.
- Tides automatically skipped for `kind: "space"` observers.
- Optional fields (`tides`, `weather`) only present when their provider is registered and produces a value.
- Provider-agnostic — interfaces only, zero base classes.
## Usage: Basic Example
```ts
import { UniverseClient } from "@suaveplan/universe";
const client = new UniverseClient();
const scene = await client.getScene(
{ kind: "space", heliocentricVec: { x: 0, y: 0, z: 0 }, frame: "icrs" },
new Date()
);
// scene.bodies === [], scene.stars === [], scene.tides/weather undefined
```
## Usage: Advanced Example
```ts
import { UniverseClient, type UniverseScene } from "@suaveplan/universe";
import type {
SolarProvider,
StarProvider,
WeatherProvider,
} from "@suaveplan/universe";
const solar: SolarProvider = {
async getBodies() {
return [];
},
async getBody(name) {
return {
name,
type: "star",
altitude: 45,
azimuth: 180,
apparentMagnitude: -26.7,
angularDiameterArcmin: 32,
distanceAu: 1,
heliocentricPosition: { x: 0, y: 0, z: 0 },
};
},
async getSunrise() {
return new Date("2026-01-01T06:00:00Z");
},
async getSunset() {
return new Date("2026-01-01T18:00:00Z");
},
};
const stars: StarProvider = {
async getVisibleStars() {
return [];
},
async queryCone() {
return [];
},
async getStar() {
return null;
},
};
const weather: WeatherProvider = {
async getCurrent() {
return { temperature: 18, pressure: 1015, humidity: 60 };
},
};
const controller = new AbortController();
const client = new UniverseClient()
.registerSolar(solar)
.registerStars(stars)
.registerWeather(weather);
const scene: UniverseScene = await client.getScene(
{
kind: "surface",
planet: {
name: "Earth",
mass: 1,
radius: 1,
polarRadius: 1,
surfaceGravity: 1,
rotationPeriod: 1,
axialTilt: 23.4,
atmosphere: null,
satellites: [],
},
coord: { latitude: 51.5, longitude: -0.12 },
},
new Date(),
controller.signal
);
```
## API Reference
### `class UniverseClient`
| Method | Returns | Description |
|---|---|---|
| `registerSolar(provider)` | `this` | Register a `SolarProvider`. |
| `registerStars(provider)` | `this` | Register a `StarProvider`. |
| `registerTides(provider)` | `this` | Register a `TidalProvider`. |
| `registerWeather(provider)` | `this` | Register a `WeatherProvider`. |
| `getScene(observer, time, signal?)` | `Promise<UniverseScene>` | Assemble the scene. Forwards `signal` to every provider. |
### `interface UniverseScene`
| Property | Type | Notes |
|---|---|---|
| `bodies` | `CelestialBody[]` | Empty when no solar provider. |
| `stars` | `ObservedStar[]` | Empty when no star provider. |
| `tides?` | `TideExtreme[]` | Omitted for space observers and when no tidal provider. |
| `weather?` | `WeatherState` | Omitted when no weather provider. |
The client throws (propagates) any error a provider throws, including `AbortError` from a fired signal.
## Implementation Notes
`UniverseClient` is a coordinator, not a cache. Each `getScene` call invokes every registered provider unconditionally (subject to the space-tide skip). Consumers that need memoisation, retry, or rate limiting should compose those policies into the providers themselves rather than wrapping the client. Re-exporting the four provider interfaces and `WeatherState` from this module (in addition to `@suaveplan/universe/providers/providers`) is intentional so consumers can `import { type SolarProvider } from "@suaveplan/universe"` without reaching into a sub-path.

View File

@@ -0,0 +1,91 @@
# Coordinate Frames
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the `CoordinateFrame` discriminator (`"heliocentric" | "galactic" | "custom"`) and the conversion entry points used across the universe-family packages. The module is the single seam through which any astronomical coordinate transformation is requested, so consumers do not need to know which rotation matrix or epoch correction is in play — they hand off a `SphericalCoord`, name the source and target frames, and receive a `SphericalCoord` in the new frame.
The current implementation is a placeholder identity transform: `convertFrame`, `toHeliocentric`, and `toGalactic` return the input unchanged. The public surface and discriminator are stable; consumers can wire to it now and pick up real conversions when this package adds the rotation pipeline without touching any call site.
## Features
- `CoordinateFrame` string-literal union — exhaustively switchable.
- `convertFrame(coord, from, to, observer?)` — generic frame-to-frame conversion entry point.
- `toHeliocentric(coord, observer)` — convenience wrapper for the most common conversion (observer-relative to heliocentric).
- `toGalactic(coord)` — observer-independent rotation into galactic coordinates.
- Identity short-circuit when `from === to`.
- Observer parameter accepts the full `ObserverPosition` discriminated union (surface / atmospheric / space).
## Usage: Basic Example
```ts
import { convertFrame } from "@suaveplan/universe";
const coord = { latitude: 10, longitude: 20 };
const same = convertFrame(coord, "heliocentric", "heliocentric");
// same === coord (identity short-circuit)
```
## Usage: Advanced Example
```ts
import {
convertFrame,
toGalactic,
toHeliocentric,
type CoordinateFrame,
} from "@suaveplan/universe";
const target: CoordinateFrame = "galactic";
const observer = {
kind: "surface" as const,
planet: {
name: "Earth",
mass: 1,
radius: 1,
polarRadius: 1,
surfaceGravity: 1,
rotationPeriod: 1,
axialTilt: 23.4,
atmosphere: null,
satellites: [],
},
coord: { latitude: 51.5, longitude: -0.12 },
};
const skyDirection = { latitude: 45, longitude: 270 };
const heliocentric = toHeliocentric(skyDirection, observer);
const galactic = toGalactic(heliocentric);
const fromHelio = convertFrame(skyDirection, "heliocentric", target, observer);
```
## API Reference
### `type CoordinateFrame`
`"heliocentric" | "galactic" | "custom"` — string-literal union safe to switch on with exhaustiveness checking.
### `convertFrame(coord, from, to, observer?)`
| Param | Type | Description |
|---|---|---|
| `coord` | `SphericalCoord` | Latitude/longitude pair in source frame. |
| `from` | `CoordinateFrame` | Source frame. |
| `to` | `CoordinateFrame` | Target frame. |
| `observer` | `ObserverPosition?` | Required for any observer-dependent conversion (e.g. topocentric → heliocentric). |
Returns a `SphericalCoord` in the target frame. Returns `coord` unchanged when `from === to`.
### `toHeliocentric(coord, observer)`
Convenience wrapper that conceptually corresponds to `convertFrame(coord, observer-frame, "heliocentric", observer)`.
### `toGalactic(coord)`
Observer-independent rotation from equatorial / heliocentric into galactic coordinates.
## Implementation Notes
The body of every function is currently the identity, which keeps the API contract stable while the rotation matrices (J2000 → galactic, ICRS → heliocentric ecliptic, axial-tilt / sidereal-time corrections) are implemented in follow-up changes. Consumers should treat `"custom"` as an extension point — any future conversion that requires caller-supplied basis vectors is expected to flow through `convertFrame(..., "custom", ...)`. Do not bake the identity behaviour into call sites: the contract is "returns the coordinate in the target frame," not "returns the coordinate unchanged."

View File

@@ -0,0 +1,114 @@
# Observer
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Re-exports the `ObserverPosition` discriminated union that every universe-family entry point consumes. The union has three branches — `SurfaceObserver`, `AtmosphericObserver`, and `SpaceObserver` — distinguished by a `kind` literal field so call sites can narrow exhaustively without runtime type tags or class hierarchies.
Centralising these types in one module gives `@suaveplan/universe/client/client`, providers, and frame-conversion utilities a single shape to consume. Because the source declarations live in `@suaveplan/types` (zero-runtime, types-only), this re-export adds no bundle weight; it exists so consumers can import observer shapes without crossing into the foundational types package directly.
## Features
- `ObserverPosition` — full discriminated union, switch on `kind`.
- `SurfaceObserver` — planet-bound observer at a `{latitude, longitude}` coordinate.
- `AtmosphericObserver` — same as surface but with `altitudeMetres` (planes, balloons, drones).
- `SpaceObserver` — heliocentric vector + reference `frame` (e.g. `"icrs"`).
- All three branches share the `kind` literal as the discriminator.
- Pure types — zero runtime cost, tree-shaken to nothing.
## Usage: Basic Example
```ts
import type { ObserverPosition } from "@suaveplan/universe";
const observer: ObserverPosition = {
kind: "space",
heliocentricVec: { x: 1, y: 0, z: 0 },
frame: "icrs",
};
```
## Usage: Advanced Example
```ts
import type {
AtmosphericObserver,
ObserverPosition,
SpaceObserver,
SurfaceObserver,
} from "@suaveplan/universe";
function describe(o: ObserverPosition): string {
switch (o.kind) {
case "surface":
return `surface at ${o.coord.latitude},${o.coord.longitude}`;
case "atmospheric":
return `${o.altitudeMetres} m above ${o.planet.name}`;
case "space":
return `space, ${o.frame} vector ${JSON.stringify(o.heliocentricVec)}`;
}
}
const earth = {
name: "Earth",
mass: 1,
radius: 1,
polarRadius: 1,
surfaceGravity: 1,
rotationPeriod: 1,
axialTilt: 23.4,
atmosphere: null,
satellites: [],
};
const surface: SurfaceObserver = {
kind: "surface",
planet: earth,
coord: { latitude: 51.5, longitude: -0.12 },
};
const balloon: AtmosphericObserver = {
kind: "atmospheric",
planet: earth,
coord: { latitude: 51.5, longitude: -0.12 },
altitudeMetres: 12000,
};
const probe: SpaceObserver = {
kind: "space",
heliocentricVec: { x: 1.0, y: 0.0, z: 0.0 },
frame: "icrs",
};
[surface, balloon, probe].forEach((o) => describe(o));
```
## API Reference
### `type SurfaceObserver`
| Property | Type | Description |
|---|---|---|
| `kind` | `"surface"` | Discriminator literal. |
| `planet` | `PlanetConfig` | Host body (mass, radius, axial tilt, satellites …). |
| `coord` | `{ latitude: number; longitude: number }` | Geographic location in degrees. |
### `type AtmosphericObserver`
Extends `SurfaceObserver` shape with an additional `altitudeMetres: number` and `kind: "atmospheric"`.
### `type SpaceObserver`
| Property | Type | Description |
|---|---|---|
| `kind` | `"space"` | Discriminator literal. |
| `heliocentricVec` | `{ x: number; y: number; z: number }` | Cartesian position in AU. |
| `frame` | `string` | Reference frame label, e.g. `"icrs"`. |
### `type ObserverPosition`
`SurfaceObserver | AtmosphericObserver | SpaceObserver` — exhaustive switch on `kind`.
## Implementation Notes
These types are re-exported from `@suaveplan/types`; this module contains no runtime code and contributes nothing to the emitted bundle. Keeping the canonical declarations in `@suaveplan/types` preserves the universe-family rule that any observer-shaped value flowing through providers, the client, frame conversions, or packet codecs is structurally identical no matter which sibling package produced it.

View File

@@ -0,0 +1,121 @@
# Packets
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Compact little-endian binary codecs for the two highest-volume payloads in a universe scene: catalogues of observed stars and lists of observed celestial bodies. The codecs trade negligible accuracy for an order-of-magnitude reduction in bytes-on-the-wire compared to a JSON encoding, which matters when streaming thousands of stars to a renderer or syncing scene snapshots over a constrained transport.
Each codec is a pure pair of `pack(...)` / `unpack(...)` functions. They are deterministic, allocation-free aside from the one output `Uint8Array`, and produce identical bytes for identical input regardless of platform endianness because every `DataView` write is explicit `littleEndian = true`.
## Features
- `packStars` / `unpackStars` — encode `StarPacket[]` with quantised RA, Dec, magnitude, altitude, azimuth, and a UTF-8 spectral class.
- `packBodies` / `unpackBodies` — encode `CelestialBodyPacket[]` with full `Float64` precision for altitude, azimuth, angular diameter, and apparent magnitude plus UTF-8 name and type.
- Length-prefixed strings (per-element `uint32` length header) so variable-width identifiers are unambiguous.
- Quantised star fields: RA in 1/10⁷° (uint32), Dec in 1/10⁷° (int32), magnitude × 100 with a +5 offset (uint16), altitude as int8 degrees, azimuth as uint8 (≈1.41° resolution).
- Empty-array handling: 4-byte count-only buffer.
- Self-describing count header so a receiver can preallocate.
## Usage: Basic Example
```ts
import { packStars, unpackStars, type StarPacket } from "@suaveplan/universe";
const stars: StarPacket[] = [
{
ra: 10,
dec: 20,
magnitude: 3.5,
altitude: 45,
azimuth: 180,
spectralClass: "G2V",
},
];
const bytes = packStars(stars);
const round = unpackStars(bytes);
// round[0].spectralClass === "G2V"
```
## Usage: Advanced Example
```ts
import {
packBodies,
packStars,
unpackBodies,
unpackStars,
type CelestialBodyPacket,
type StarPacket,
} from "@suaveplan/universe";
function snapshot(
stars: StarPacket[],
bodies: CelestialBodyPacket[]
): Uint8Array {
const s = packStars(stars);
const b = packBodies(bodies);
const out = new Uint8Array(4 + s.length + 4 + b.length);
new DataView(out.buffer).setUint32(0, s.length, true);
out.set(s, 4);
new DataView(out.buffer).setUint32(4 + s.length, b.length, true);
out.set(b, 4 + s.length + 4);
return out;
}
function restore(buf: Uint8Array): {
stars: StarPacket[];
bodies: CelestialBodyPacket[];
} {
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
const sLen = view.getUint32(0, true);
const stars = unpackStars(buf.subarray(4, 4 + sLen));
const bodies = unpackBodies(buf.subarray(4 + sLen + 4));
return { stars, bodies };
}
```
## API Reference
### `interface StarPacket`
| Field | Type | Quantisation |
|---|---|---|
| `ra` | `number` | uint32, `(ra + 180) × 10⁷` |
| `dec` | `number` | int32, `dec × 10⁷` |
| `magnitude` | `number` | uint16, `(mag + 5) × 100` |
| `altitude` | `number` | int8 degrees |
| `azimuth` | `number` | uint8, scaled by `255/360` |
| `spectralClass` | `string` | UTF-8, length-prefixed |
### `packStars(stars: StarPacket[]): Uint8Array`
Returns the packed buffer. Layout: `uint32 count` then per-element `uint32 specLen` headers, then the payload region containing each spectral-class string followed by the 12-byte fixed-width quantised fields.
### `unpackStars(bytes: Uint8Array): StarPacket[]`
Inverse of `packStars`. Reverses every quantisation; small accuracy losses are inherent (azimuth ≈ ±0.7°, magnitude ≈ ±0.005, RA/Dec ≈ ±5×10⁻⁸°).
### `interface CelestialBodyPacket`
| Field | Type | Encoding |
|---|---|---|
| `name` | `string` | UTF-8, length-prefixed |
| `type` | `string` | UTF-8, length-prefixed |
| `altitude` | `number` | float64 |
| `azimuth` | `number` | float64 |
| `angularDiameterArcmin` | `number` | float64 |
| `apparentMagnitude` | `number` | float64 |
### `packBodies(bodies: CelestialBodyPacket[]): Uint8Array`
Layout: `uint32 count`, per-element `uint32 nameLen` + `uint32 typeLen` headers, then per-element `name` + `type` bytes followed by 32 bytes of float64 fields.
### `unpackBodies(bytes: Uint8Array): CelestialBodyPacket[]`
Inverse of `packBodies` — fully lossless for the float64 fields.
## Implementation Notes
Star encoding is quantised because telescope-grade precision is not the use case; renderers, mini-maps, and HUDs are. The `+ 180` offset on RA and `+ 5` offset on magnitude let the smallest unsigned integer types cover the full physical range without negative-number gymnastics. Body encoding stays at float64 because the population is small (handfuls of objects per scene) and keeping ephemeris precision avoids visible jitter when the same body is repeatedly encoded and decoded over a long-running session. Both codecs use little-endian writes unconditionally so the wire format is platform-independent.

View File

@@ -0,0 +1,139 @@
# Providers
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provider interfaces are the integration surface between `UniverseClient` and any data source — a real-time ephemeris service, a static catalogue, a mocked provider in a test, or a deterministic game-engine implementation. Each interface exposes only the operations the client actually consumes and is shaped so a reasonable mock can be hand-written in a few lines.
This module is a re-export of the canonical provider contracts from `@suaveplan/types`. Keeping the declarations in `@suaveplan/types` ensures every consumer in the universe family — client, frame conversions, packet codecs, sibling packages — sees structurally identical types without forming a dependency cycle. Consumers may import from either `@suaveplan/universe` (the package root re-exports the same names) or `@suaveplan/universe/providers/providers` for explicit sub-path imports.
## Features
- `SolarProvider` — body positions, sunrise, sunset, single-body lookup.
- `StarProvider` — visible stars for an observer, cone queries in catalogue space, single-star lookup.
- `TidalProvider` — height prediction and high/low extreme events for a planet/coordinate.
- `WeatherProvider` — current conditions snapshot for an observer.
- `WeatherState` — value object returned by `WeatherProvider.getCurrent`.
- Every async method accepts an optional `AbortSignal` so callers can cancel propagation through the entire client.
## Usage: Basic Example
```ts
import type { SolarProvider } from "@suaveplan/universe";
const mockSun: SolarProvider = {
async getBodies() {
return [];
},
async getBody(name) {
return {
name,
type: "star",
altitude: 45,
azimuth: 180,
apparentMagnitude: -26.7,
angularDiameterArcmin: 32,
distanceAu: 1,
heliocentricPosition: { x: 0, y: 0, z: 0 },
};
},
async getSunrise() {
return new Date("2026-01-01T06:00:00Z");
},
async getSunset() {
return new Date("2026-01-01T18:00:00Z");
},
};
```
## Usage: Advanced Example
```ts
import { UniverseClient } from "@suaveplan/universe";
import type {
SolarProvider,
StarProvider,
TidalProvider,
WeatherProvider,
} from "@suaveplan/universe";
declare const realSolar: SolarProvider;
declare const realStars: StarProvider;
declare const realTides: TidalProvider;
declare const realWeather: WeatherProvider;
const client = new UniverseClient()
.registerSolar(realSolar)
.registerStars(realStars)
.registerTides(realTides)
.registerWeather(realWeather);
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
const scene = await client.getScene(
{
kind: "surface",
planet: {
name: "Earth",
mass: 1,
radius: 1,
polarRadius: 1,
surfaceGravity: 1,
rotationPeriod: 1,
axialTilt: 23.4,
atmosphere: null,
satellites: [],
},
coord: { latitude: 51.5, longitude: -0.12 },
},
new Date(),
controller.signal
);
```
## API Reference
### `interface SolarProvider`
| Method | Returns | Description |
|---|---|---|
| `getBodies(observer, time, signal?)` | `Promise<CelestialBody[]>` | All major bodies for the observer. |
| `getBody(name, observer, time, signal?)` | `Promise<CelestialBody>` | Single named body. |
| `getSunrise(observer, date, signal?)` | `Promise<Date>` | Sunrise UTC for the date. |
| `getSunset(observer, date, signal?)` | `Promise<Date>` | Sunset UTC for the date. |
### `interface StarProvider`
| Method | Returns | Description |
|---|---|---|
| `getVisibleStars(observer, options?, signal?)` | `Promise<ObservedStar[]>` | Brightness-sorted stars above the observer's horizon. |
| `queryCone(ra, dec, radiusDeg, signal?)` | `Promise<CatalogStar[]>` | Catalog-space cone query. |
| `getStar(catalogId, signal?)` | `Promise<CatalogStar \| null>` | Lookup by catalogue identifier. |
### `interface TidalProvider`
| Method | Returns | Description |
|---|---|---|
| `predictHeight(planet, coord, time, signal?)` | `Promise<number>` | Tidal height in metres above chart datum. |
| `getExtremes(planet, coord, date, signal?)` | `Promise<TideExtreme[]>` | High and low events for the date. |
### `interface WeatherProvider`
| Method | Returns | Description |
|---|---|---|
| `getCurrent(observer, signal?)` | `Promise<WeatherState>` | Snapshot of current conditions. |
### `interface WeatherState`
| Property | Type | Description |
|---|---|---|
| `temperature` | `number` | Degrees Celsius. |
| `pressure` | `number` | Hectopascals. |
| `humidity` | `number` | Percent (0 – 100). |
## Implementation Notes
All four contracts are interfaces — there is no abstract base class, no decorator, and no required adapter to implement them. This keeps mocks trivial and lets consumers register remote-API wrappers, deterministic game providers, in-memory caches, or stub fixtures interchangeably. `UniverseClient` does not retry, cache, or fan out; consumers that need those policies should compose them inside the provider implementations so cross-cutting behaviour is contained and testable in isolation. The `signal` parameter is plumbed unconditionally — providers should propagate it into any underlying fetch / database call so an aborted scene request cancels in-flight work.

View File

@@ -0,0 +1,137 @@
# Types
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Re-exports the canonical domain types shared across the universe-family packages: celestial bodies as observed from a position, catalogue and observed stars, query options for star lookups, and tide-extreme events plus their option bundles. These are pure types — zero runtime — sourced from `@suaveplan/types`, where they live so every sibling package, every provider implementation, and every transport / packet codec can refer to one structurally identical shape.
The module is the place to look first when wiring a new provider or codec: every value flowing through `UniverseClient`, `packStars`, `packBodies`, frame conversions, and provider methods is built from these shapes. Consumers should never redefine local copies, because providers and the client are defined to consume exactly these types and any structural drift would break interface compatibility silently.
## Features
- `CelestialBody` — observed body with altitude, azimuth, magnitude, angular diameter, distance, and heliocentric position.
- `CatalogStar` — catalog-space star (RA, Dec, magnitude, optional spectral class / distance / proper motion / parallax).
- `ObservedStar` — `CatalogStar` with observer-dependent altitude, azimuth, and extincted magnitude.
- `StarQueryOptions` — filters for visible-star queries (`minAltitudeDeg`, `maxMagnitude`, `limit`).
- `TideExtreme` — high or low water event with metric height and UTC timestamp.
- `TidePredictionOptions` and `TideExtremeOptions` — bundle types for tidal-provider call sites.
## Usage: Basic Example
```ts
import type { CelestialBody } from "@suaveplan/universe";
const venus: CelestialBody = {
name: "Venus",
type: "planet",
altitude: 30,
azimuth: 180,
apparentMagnitude: -4.5,
angularDiameterArcmin: 0.5,
distanceAu: 0.28,
heliocentricPosition: { x: 0.5, y: 0, z: 0 },
};
```
## Usage: Advanced Example
```ts
import type {
CatalogStar,
ObservedStar,
StarQueryOptions,
TideExtreme,
} from "@suaveplan/universe";
const polaris: CatalogStar = {
catalogId: "HIP_11767",
ra: 37.95,
dec: 89.26,
apparentMagnitude: 2.0,
spectralClass: "F7:Ib-IIv",
distance: 430,
properMotionRa: 43.5,
properMotionDec: -11.9,
parallax: 7.56,
};
const observed: ObservedStar = {
...polaris,
altitude: 51.5,
azimuth: 0,
extinctedMagnitude: 2.1,
};
const options: StarQueryOptions = {
minAltitudeDeg: 10,
maxMagnitude: 5,
limit: 250,
};
const extreme: TideExtreme = {
type: "high",
height: 5.2,
time: new Date("2026-06-01T12:00:00Z"),
};
void [observed, options, extreme];
```
## API Reference
### `interface CelestialBody`
| Property | Type | Description |
|---|---|---|
| `name` | `string` | Common name. |
| `type` | `string` | Category (`star`, `planet`, `moon`, `dwarf-planet`, …). |
| `altitude` | `number` | Degrees above horizon. |
| `azimuth` | `number` | Degrees clockwise from north. |
| `apparentMagnitude` | `number` | Visual magnitude. |
| `angularDiameterArcmin` | `number` | Arcminutes. |
| `distanceAu` | `number` | Distance in astronomical units. |
| `heliocentricPosition` | `{ x: number; y: number; z: number }` | Cartesian position in AU. |
### `interface CatalogStar`
| Property | Type | Description |
|---|---|---|
| `catalogId` | `string` | Unique identifier (e.g. Hipparcos). |
| `ra` | `number` | Right ascension (0 – 360°). |
| `dec` | `number` | Declination (–90 to +90°). |
| `apparentMagnitude` | `number` | Visual magnitude. |
| `spectralClass?` | `string` | e.g. `"G2V"`. |
| `distance?` | `number` | Parsecs. |
| `properMotionRa?` | `number` | mas/yr. |
| `properMotionDec?` | `number` | mas/yr. |
| `parallax?` | `number` | milliarcseconds. |
### `interface ObservedStar` extends `CatalogStar`
Adds `altitude: number`, `azimuth: number`, and `extinctedMagnitude: number` (visual magnitude after atmospheric extinction).
### `interface StarQueryOptions`
| Property | Type | Default | Description |
|---|---|---|---|
| `minAltitudeDeg?` | `number` | `0` | Minimum altitude. |
| `maxMagnitude?` | `number` | `6` | Faintest magnitude. |
| `limit?` | `number` | — | Maximum results. |
### `interface TideExtreme`
| Property | Type | Description |
|---|---|---|
| `type` | `"high" \| "low"` | Event type. |
| `height` | `number` | Metres above chart datum. |
| `time` | `Date` | UTC timestamp. |
### `interface TidePredictionOptions` / `interface TideExtremeOptions`
Argument bundles consumed by `TidalProvider.predictHeight` and `TidalProvider.getExtremes`. See `@suaveplan/types` for the canonical fields (planet, coordinate, time / date).
## Implementation Notes
All declarations are `readonly`-style interfaces; consumers should treat them as immutable value objects. Providers compute these shapes; the client and codecs read them. Re-exporting (rather than duplicating) ensures this package contributes zero additional runtime bytes — the universe-family packages collectively share one canonical type for every domain concept, which is what allows `packStars(unpackStars(packStars(x)))` to round-trip through arbitrary transports without coercion glue.

View File

@@ -0,0 +1,13 @@
# `logistics`
Port data and logistics primitives.
<!-- begin:auto-docs -->
## Packages in `logistics`
1 package.
| Package | Description |
|---|---|
| [`@suaveplan/ports`](./ports/README.md) | Global port database with shipping-lane routing, ETA estimation, and UN/LOCODE port lookups for sea ports, airports, and rail terminals. |
<!-- end:auto-docs -->

View File

@@ -0,0 +1 @@
Port data and logistics primitives.

View File

@@ -0,0 +1,104 @@
# @suaveplan/ports - Global port database with shipping-lane routing, ETA estimation, and UN/LOCODE port lookups for sea ports, airports, and rail terminals.
Global port database with shipping-lane routing, ETA estimation, and
UN/LOCODE port lookups for sea ports, airports, and rail terminals.
<!-- begin:auto-docs -->
## Documentation tree
- [airports](./src/data/airports/airports.md) — Curated dataset of major international air-cargo airports, exported as the readonly `airports` array. Each record confo…
- [airports](./src/data/airports/airports.md) — Curated dataset of major international air-cargo airports, exported as the readonly `airports` array. Each record confo…
- [all-ports](./src/data/all-ports/all-ports.md) — Aggregates every curated dataset — sea ports, airports, and rail terminals — into a single readonly `allPorts` array an…
- [all-ports](./src/data/all-ports/all-ports.md) — Aggregates every curated dataset — sea ports, airports, and rail terminals — into a single readonly `allPorts` array an…
- [rail-terminals](./src/data/rail-terminals/rail-terminals.md) — Curated dataset of major rail freight terminals — primarily the inland container hubs on the New Silk Road and Trans-Eu…
- [rail-terminals](./src/data/rail-terminals/rail-terminals.md) — Curated dataset of major rail freight terminals — primarily the inland container hubs on the New Silk Road and Trans-Eu…
- [sea-ports](./src/data/sea-ports/sea-ports.md) — Aggregates every regional sea-port dataset (`seaPortsAsia`, `seaPortsEurope`, `seaPortsAmericas`, `seaPortsAfricaMena`,…
- [sea-ports](./src/data/sea-ports/sea-ports.md) — Aggregates every regional sea-port dataset (`seaPortsAsia`, `seaPortsEurope`, `seaPortsAmericas`, `seaPortsAfricaMena`,…
- [sea-ports-africa-mena](./src/data/sea-ports-africa-mena/sea-ports-africa-mena.md) — Curated dataset of major sea ports across Africa and the Middle East / North Africa region, exported as the readonly `s…
- [sea-ports-africa-mena](./src/data/sea-ports-africa-mena/sea-ports-africa-mena.md) — Curated dataset of major sea ports across Africa and the Middle East / North Africa region, exported as the readonly `s…
- [sea-ports-americas](./src/data/sea-ports-americas/sea-ports-americas.md) — Curated dataset of major sea ports across the Americas, exported as the readonly `seaPortsAmericas` array. Every record…
- [sea-ports-americas](./src/data/sea-ports-americas/sea-ports-americas.md) — Curated dataset of major sea ports across the Americas, exported as the readonly `seaPortsAmericas` array. Every record…
- [sea-ports-asia](./src/data/sea-ports-asia/sea-ports-asia.md) — Curated dataset of major sea ports across Asia, exported as the readonly `seaPortsAsia` array. Every record conforms to…
- [sea-ports-asia](./src/data/sea-ports-asia/sea-ports-asia.md) — Curated dataset of major sea ports across Asia, exported as the readonly `seaPortsAsia` array. Every record conforms to…
- [sea-ports-europe](./src/data/sea-ports-europe/sea-ports-europe.md) — Curated dataset of major sea ports across Europe, exported as the readonly `seaPortsEurope` array. Every record conform…
- [sea-ports-europe](./src/data/sea-ports-europe/sea-ports-europe.md) — Curated dataset of major sea ports across Europe, exported as the readonly `seaPortsEurope` array. Every record conform…
- [sea-ports-oceania](./src/data/sea-ports-oceania/sea-ports-oceania.md) — Curated dataset of major sea ports across Oceania, exported as the readonly `seaPortsOceania` array. Every record confo…
- [sea-ports-oceania](./src/data/sea-ports-oceania/sea-ports-oceania.md) — Curated dataset of major sea ports across Oceania, exported as the readonly `seaPortsOceania` array. Every record confo…
- [shipping-lanes](./src/data/shipping-lanes/shipping-lanes.md) — Curated registry of major global shipping lanes — Asia–Europe via Suez, Asia–Europe via Cape, Transpacific, Transatlant…
- [shipping-lanes](./src/data/shipping-lanes/shipping-lanes.md) — Curated registry of major global shipping lanes — Asia–Europe via Suez, Asia–Europe via Cape, Transpacific, Transatlant…
- [distance](./src/distance/distance.md) — Wraps `@suaveplan/geo`'s `greatCircleDistance` against a sphere whose radius matches the IUGG mean Earth radius (6 371…
- [distance](./src/distance/distance.md) — Wraps `@suaveplan/geo`'s `greatCircleDistance` against a sphere whose radius matches the IUGG mean Earth radius (6 371…
- [error-codes](./src/error-codes/error-codes.md) — Declares the eight stable error codes emitted by `@suaveplan/ports` and the matching typed `PortsError` subclasses. Eve…
- [error-codes](./src/error-codes/error-codes.md) — Declares the eight stable error codes emitted by `@suaveplan/ports` and the matching typed `PortsError` subclasses. Eve…
- [eta](./src/eta/eta.md) — Estimates the time of arrival for a vessel travelling along a `ShippingLane`. Resolves an effective cruise speed from (…
- [eta](./src/eta/eta.md) — Estimates the time of arrival for a vessel travelling along a `ShippingLane`. Resolves an effective cruise speed from (…
- [find-port](./src/find-port/find-port.md) — Provides the lookup surface for `@suaveplan/ports`. Every public function consumes the curated `allPorts` registry by d…
- [find-port](./src/find-port/find-port.md) — Provides the lookup surface for `@suaveplan/ports`. Every public function consumes the curated `allPorts` registry by d…
- [lane-distance](./src/lane-distance/lane-distance.md) — Computes great-circle distance metrics for a `ShippingLane`'s waypoint chain. `estimateLaneDistance` sums every segment…
- [lane-distance](./src/lane-distance/lane-distance.md) — Computes great-circle distance metrics for a `ShippingLane`'s waypoint chain. `estimateLaneDistance` sums every segment…
- [lane-lookup](./src/lane-lookup/lane-lookup.md) — Public lookup surface for the curated `shippingLanes` registry. Mirrors the port-lookup module in style: an O(1) `Map`…
- [lane-lookup](./src/lane-lookup/lane-lookup.md) — Public lookup surface for the curated `shippingLanes` registry. Mirrors the port-lookup module in style: an O(1) `Map`…
- [lane-progress](./src/lane-progress/lane-progress.md) — Projects a vessel position onto a shipping lane and reports how much of the lane has been traversed, how much remains,…
- [lane-progress](./src/lane-progress/lane-progress.md) — Projects a vessel position onto a shipping lane and reports how much of the lane has been traversed, how much remains,…
- [nearest-port](./src/nearest-port/nearest-port.md) — Public proximity-search API. `findNearestPort` returns the single closest port to a coordinate; `findPortsInRadius` ret…
- [nearest-port](./src/nearest-port/nearest-port.md) — Public proximity-search API. `findNearestPort` returns the single closest port to a coordinate; `findPortsInRadius` ret…
- [otel](./src/otel/otel.md) — Wires `@suaveplan/ports`' OpenTelemetry instrumentation: three counters that track lookup volume per kind, lane lookup…
- [otel](./src/otel/otel.md) — Wires `@suaveplan/ports`' OpenTelemetry instrumentation: three counters that track lookup volume per kind, lane lookup…
- [schemas](./src/schemas/schemas.md) — Defines Zod schemas for every public type emitted by `@suaveplan/ports`. API plugins, message-queue handlers, and any o…
- [schemas](./src/schemas/schemas.md) — Defines Zod schemas for every public type emitted by `@suaveplan/ports`. API plugins, message-queue handlers, and any o…
- [spatial-index](./src/spatial-index/spatial-index.md) — `PortSpatialIndex` buckets a port set by 5-degree latitude/longitude cells at construction time so `findNearest` and `f…
- [spatial-index](./src/spatial-index/spatial-index.md) — `PortSpatialIndex` buckets a port set by 5-degree latitude/longitude cells at construction time so `findNearest` and `f…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/ports
```
## What's in the box
- Curated `Port` registry: ~100 sea ports across five regions, ~30
international cargo airports, ~30 rail freight terminals.
- Lookups: by UN/LOCODE (`findPort` / `findPortStrict`), by ISO country
(`findPortsByCountry`), by `PortType` (`findPortsByType`), by name
substring (`searchPorts`).
- Proximity: `findNearestPort` and `findPortsInRadius` (5°-bucketed
`PortSpatialIndex` under the hood).
- Curated `shippingLanes` waypoint graph for the major sea, rail, and
air freight routes.
- Lane distance (`estimateLaneDistance`, `cumulativeDistances`), lane
projection (`findPositionOnLane`), and ETA estimation (`estimateETA`).
- 8-code typed error registry under the `PORTS.*` namespace via
`@suaveplan/error`.
- Zod schemas for every public type under `./schemas/schemas`.
- OpenTelemetry instrumentation: three counters and three histograms
via `./otel/otel`.
## Quickstart
```ts
import {
findPort,
findNearestPort,
getShippingLaneStrict,
estimateETA,
} from "@suaveplan/ports";
const shanghai = findPort("CNSHA");
const nearestSea = findNearestPort({ latitude: 1.26, longitude: 103.82 });
const suez = getShippingLaneStrict("asia-europe-suez");
const eta = estimateETA(
{ latitude: 12.5, longitude: 43.3 },
suez,
14,
{ departureTime: new Date() }
);
console.log(eta.etaDate, eta.confidence);
```
## Capability spec
See the canonical capability
specification (requirements + scenarios + `**Verified by:**` links).

View File

@@ -0,0 +1,56 @@
# airports
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Curated dataset of major international air-cargo airports, exported as
the readonly `airports` array. Each record conforms to the `Port`
interface (with `type: "air"`) and carries the IATA code, runway
length, and freight-throughput figures used by ETA estimation on air
shipping lanes and proximity searches in `findNearestPort`.
## Features
- Coverage of the world's busiest cargo hubs across Asia, Europe, North
America, the Middle East, and Latin America.
- Each record carries `iataCode`, `runwayLengthM`, and
`annualThroughputTonnes` so consumers can filter by capacity.
- Facility tags reflect cargo-handling capability (`cargo`,
`intermodal`, `bonded_warehouse`, `free_trade_zone`, `passenger`,
`reefer`).
- IANA timezones populated for every record.
## Usage: Basic
```ts
import { airports } from "@suaveplan/ports/data/airports";
const memphis = airports.find((p) => p.iataCode === "MEM");
```
## Usage: Advanced
```ts
import { airports, findNearestPort } from "@suaveplan/ports";
const nearest = findNearestPort(
{ latitude: 25.2532, longitude: 55.3657 },
airports
);
```
## API Reference
- `airports: readonly Port[]` — curated international air-cargo airport set.
## Implementation Notes
Every entry uses the IATA-prefixed UN/LOCODE convention (e.g. `USMEM`
for Memphis). Throughput figures use underscores as digit separators
for readability. The dataset is curated rather than exhaustive — it
targets the cargo network's primary hubs and the regional gateways
most commonly used in international logistics workflows, not every
licensed airfield.

View File

@@ -0,0 +1,60 @@
# all-ports
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Aggregates every curated dataset — sea ports, airports, and rail
terminals — into a single readonly `allPorts` array and re-exports each
sub-list. This is the default candidate set consumed by `findPort`,
`findNearestPort`, and `findPortsInRadius` when callers do not supply a
custom port set.
## Features
- Single import point for the full curated port universe.
- Each sub-list is re-exported with its original name so consumers can
scope lookups by transport mode.
- Order is `seaPorts → airports → railTerminals`, matching the
main-branch convention; consumers rely on `locode` as the identity.
- All entries conform to the `Port` interface and validate against
`PortSchema`.
## Usage: Basic
```ts
import { allPorts } from "@suaveplan/ports";
console.log("total curated ports:", allPorts.length);
```
## Usage: Advanced
```ts
import {
allPorts,
findPortsByType,
findNearestPort,
} from "@suaveplan/ports";
const air = findPortsByType("air");
const nearestSeaToCargoHub = findNearestPort(
{ latitude: 35.61, longitude: 139.73 },
allPorts
);
```
## API Reference
- `allPorts: readonly Port[]` — combined sea + air + rail set.
- `seaPorts`, `airports`, `railTerminals` — sub-lists re-exported.
## Implementation Notes
Aggregation is computed once at module load via spread. Every lookup
path keys on `locode` so callers can treat the aggregate as a flat
namespace without worrying about cross-modal collisions (UN/LOCODE
allocations are unique per terminal). Sub-list re-exports preserve
referential identity so identity comparisons keep working.

View File

@@ -0,0 +1,57 @@
# rail-terminals
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Curated dataset of major rail freight terminals — primarily the
inland container hubs on the New Silk Road and Trans-European rail
freight corridors — exported as the readonly `railTerminals` array.
Each record conforms to the `Port` interface (with `type: "rail"`) and
carries a UIC railway code for cross-system identification.
## Features
- Coverage spans China's interior rail hubs, the Trans-Siberian and
Trans-Mongolian routes, the European TEN-T corridors, and select
North American intermodal centres.
- Each record carries `uicCode` (where assigned) and `iataCode`
(intermodal interchange terminals).
- Facility tags reflect rail-specific capabilities (`intermodal`,
`bonded_warehouse`, `container`, `bulk`, `roro`).
- IANA timezones populated for every record so consumers can derive
local times for cross-border schedules.
## Usage: Basic
```ts
import { railTerminals } from "@suaveplan/ports/data/rail-terminals";
console.log("rail terminal count:", railTerminals.length);
```
## Usage: Advanced
```ts
import { railTerminals, findNearestPort } from "@suaveplan/ports";
const nearest = findNearestPort(
{ latitude: 50.1, longitude: 8.7 }, // Frankfurt
railTerminals
);
```
## API Reference
- `railTerminals: readonly Port[]` — curated rail-freight terminal set.
## Implementation Notes
Every entry uses a UN/LOCODE primary key. `uicCode` is populated only
where the terminal has an assigned UIC code — pure intermodal yards
without a UIC registration leave the field undefined. The dataset is
curated rather than exhaustive: it captures the rail-freight hubs most
likely to appear in cross-modal ETA calculations against this package's
shipping-lane registry.

View File

@@ -0,0 +1,60 @@
# sea-ports-africa-mena
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Curated dataset of major sea ports across Africa and the Middle East / North Africa region, exported as the
readonly `seaPortsAfricaMena` array. Every record conforms to the `Port`
interface and the runtime `PortSchema`. The list focuses on
container-handling ports ranked by TEU throughput plus regional
dry-bulk, tanker, and roll-on/roll-off hubs to give realistic coverage
for shipping-lane calculations and proximity searches.
## Features
- ~14 entries covering the major maritime nations of Africa and the Middle East / North Africa region.
- Container throughput hints in TEU (latest published figures at time
of curation).
- Maximum-vessel-size hints in TEU where available.
- Facility tags (container, bulk, tanker, reefer, roro, passenger,
free_trade_zone, bonded_warehouse) reflect real operational
capabilities.
- IANA timezones populated for every record so consumers can derive
local times without lookup tables of their own.
## Usage: Basic
```ts
import { seaPortsAfricaMena } from "@suaveplan/ports/data/sea-ports";
console.log(seaPortsAfricaMena.length, "Africa and the Middle East / North Africa region sea ports");
```
## Usage: Advanced
```ts
import { seaPortsAfricaMena, findNearestPort } from "@suaveplan/ports";
const nearest = findNearestPort(
{ latitude: 51.9, longitude: 4.49 },
seaPortsAfricaMena
);
```
## API Reference
- `seaPortsAfricaMena: readonly Port[]` — curated Africa and the Middle East / North Africa region sea-port set.
## Implementation Notes
Every entry uses a UN/LOCODE primary key. Throughput figures use
underscores as digit separators for readability. The dataset is
intentionally append-only — re-ordering entries would not break
consumers but would generate a misleading diff against the main-branch
source. This is a curated reference set, not a complete UN/LOCODE
mirror; it targets the busiest container ports and the regional hubs
most likely to appear in shipping-lane calculations against this
Africa and the Middle East / North Africa region region.

View File

@@ -0,0 +1,60 @@
# sea-ports-americas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Curated dataset of major sea ports across the Americas, exported as the
readonly `seaPortsAmericas` array. Every record conforms to the `Port`
interface and the runtime `PortSchema`. The list focuses on
container-handling ports ranked by TEU throughput plus regional
dry-bulk, tanker, and roll-on/roll-off hubs to give realistic coverage
for shipping-lane calculations and proximity searches.
## Features
- ~20 entries covering the major maritime nations of the Americas.
- Container throughput hints in TEU (latest published figures at time
of curation).
- Maximum-vessel-size hints in TEU where available.
- Facility tags (container, bulk, tanker, reefer, roro, passenger,
free_trade_zone, bonded_warehouse) reflect real operational
capabilities.
- IANA timezones populated for every record so consumers can derive
local times without lookup tables of their own.
## Usage: Basic
```ts
import { seaPortsAmericas } from "@suaveplan/ports/data/sea-ports";
console.log(seaPortsAmericas.length, "the Americas sea ports");
```
## Usage: Advanced
```ts
import { seaPortsAmericas, findNearestPort } from "@suaveplan/ports";
const nearest = findNearestPort(
{ latitude: 51.9, longitude: 4.49 },
seaPortsAmericas
);
```
## API Reference
- `seaPortsAmericas: readonly Port[]` — curated the Americas sea-port set.
## Implementation Notes
Every entry uses a UN/LOCODE primary key. Throughput figures use
underscores as digit separators for readability. The dataset is
intentionally append-only — re-ordering entries would not break
consumers but would generate a misleading diff against the main-branch
source. This is a curated reference set, not a complete UN/LOCODE
mirror; it targets the busiest container ports and the regional hubs
most likely to appear in shipping-lane calculations against this
the Americas region.

View File

@@ -0,0 +1,59 @@
# sea-ports-asia
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Curated dataset of major sea ports across Asia, exported as the readonly
`seaPortsAsia` array. Every record conforms to the `Port` interface and
the runtime `PortSchema`. The list focuses on container-handling ports
ranked by TEU throughput plus regional dry-bulk and tanker hubs to give
realistic coverage for shipping-lane calculations.
## Features
- ~30 entries covering China, Japan, South Korea, Hong Kong, Taiwan,
Singapore, Malaysia, Vietnam, Thailand, Indonesia, the Philippines,
India, Sri Lanka, Bangladesh, and Pakistan.
- Container throughput hints in TEU (latest published figures at time
of curation).
- Maximum-vessel-size hints in TEU.
- Facility tags (container, bulk, tanker, reefer, free_trade_zone,
passenger, roro) reflect real operational capabilities.
- IANA timezones populated for every record so consumers can derive
local times without lookup tables of their own.
## Usage: Basic
```ts
import { seaPortsAsia } from "@suaveplan/ports/data/sea-ports";
console.log(seaPortsAsia.length, "Asia sea ports");
```
## Usage: Advanced
```ts
import { seaPortsAsia, findNearestPort } from "@suaveplan/ports";
const nearest = findNearestPort(
{ latitude: 22.32, longitude: 114.17 },
seaPortsAsia
);
```
## API Reference
- `seaPortsAsia: readonly Port[]` — curated Asia sea-port set.
## Implementation Notes
Every entry uses a UN/LOCODE primary key. Some throughput figures use
underscores as digit separators for readability. The dataset is
intentionally append-only: re-ordering entries would not break
consumers but would generate a misleading diff against the main-branch
source. Curated rather than complete — this is not a substitute for a
live UN/LOCODE database, it's a fixed reference set for ETA and
proximity calculations against the world's busiest container ports.

View File

@@ -0,0 +1,60 @@
# sea-ports-europe
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Curated dataset of major sea ports across Europe, exported as the
readonly `seaPortsEurope` array. Every record conforms to the `Port`
interface and the runtime `PortSchema`. The list focuses on
container-handling ports ranked by TEU throughput plus regional
dry-bulk, tanker, and roll-on/roll-off hubs to give realistic coverage
for shipping-lane calculations and proximity searches.
## Features
- ~24 entries covering the major maritime nations of Europe.
- Container throughput hints in TEU (latest published figures at time
of curation).
- Maximum-vessel-size hints in TEU where available.
- Facility tags (container, bulk, tanker, reefer, roro, passenger,
free_trade_zone, bonded_warehouse) reflect real operational
capabilities.
- IANA timezones populated for every record so consumers can derive
local times without lookup tables of their own.
## Usage: Basic
```ts
import { seaPortsEurope } from "@suaveplan/ports/data/sea-ports";
console.log(seaPortsEurope.length, "Europe sea ports");
```
## Usage: Advanced
```ts
import { seaPortsEurope, findNearestPort } from "@suaveplan/ports";
const nearest = findNearestPort(
{ latitude: 51.9, longitude: 4.49 },
seaPortsEurope
);
```
## API Reference
- `seaPortsEurope: readonly Port[]` — curated Europe sea-port set.
## Implementation Notes
Every entry uses a UN/LOCODE primary key. Throughput figures use
underscores as digit separators for readability. The dataset is
intentionally append-only — re-ordering entries would not break
consumers but would generate a misleading diff against the main-branch
source. This is a curated reference set, not a complete UN/LOCODE
mirror; it targets the busiest container ports and the regional hubs
most likely to appear in shipping-lane calculations against this
Europe region.

View File

@@ -0,0 +1,60 @@
# sea-ports-oceania
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Curated dataset of major sea ports across Oceania, exported as the
readonly `seaPortsOceania` array. Every record conforms to the `Port`
interface and the runtime `PortSchema`. The list focuses on
container-handling ports ranked by TEU throughput plus regional
dry-bulk, tanker, and roll-on/roll-off hubs to give realistic coverage
for shipping-lane calculations and proximity searches.
## Features
- ~10 entries covering the major maritime nations of Oceania.
- Container throughput hints in TEU (latest published figures at time
of curation).
- Maximum-vessel-size hints in TEU where available.
- Facility tags (container, bulk, tanker, reefer, roro, passenger,
free_trade_zone, bonded_warehouse) reflect real operational
capabilities.
- IANA timezones populated for every record so consumers can derive
local times without lookup tables of their own.
## Usage: Basic
```ts
import { seaPortsOceania } from "@suaveplan/ports/data/sea-ports";
console.log(seaPortsOceania.length, "Oceania sea ports");
```
## Usage: Advanced
```ts
import { seaPortsOceania, findNearestPort } from "@suaveplan/ports";
const nearest = findNearestPort(
{ latitude: 51.9, longitude: 4.49 },
seaPortsOceania
);
```
## API Reference
- `seaPortsOceania: readonly Port[]` — curated Oceania sea-port set.
## Implementation Notes
Every entry uses a UN/LOCODE primary key. Throughput figures use
underscores as digit separators for readability. The dataset is
intentionally append-only — re-ordering entries would not break
consumers but would generate a misleading diff against the main-branch
source. This is a curated reference set, not a complete UN/LOCODE
mirror; it targets the busiest container ports and the regional hubs
most likely to appear in shipping-lane calculations against this
Oceania region.

View File

@@ -0,0 +1,65 @@
# sea-ports
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Aggregates every regional sea-port dataset (`seaPortsAsia`,
`seaPortsEurope`, `seaPortsAmericas`, `seaPortsAfricaMena`,
`seaPortsOceania`) into a single readonly `seaPorts` array and re-exports
each regional sub-list. This is the curated set consumed by
`@suaveplan/ports`' `findPort`, `findNearestPort`, and
`findPortsInRadius` lookups when callers do not pass a custom port set.
## Features
- Single import point for the global sea-port set.
- Every regional sub-list is re-exported with the original name so
callers can scope a search to a region.
- Order is `Asia → Europe → Americas → Africa/MENA → Oceania`, matching
the main-branch convention; consumers rely on `locode` as the
identity rather than array index.
- All entries conform to the `Port` interface and validate against
`PortSchema`.
## Usage: Basic
```ts
import { seaPorts } from "@suaveplan/ports/data/sea-ports";
console.log("global sea-port count:", seaPorts.length);
```
## Usage: Advanced
```ts
import {
seaPortsAsia,
seaPortsEurope,
findPortsInRadius,
} from "@suaveplan/ports";
const pair = [...seaPortsAsia, ...seaPortsEurope];
const candidates = findPortsInRadius(
{ latitude: 36, longitude: 30 },
1500,
pair
);
```
## API Reference
- `seaPorts: readonly Port[]` — global aggregate.
- `seaPortsAsia`, `seaPortsEurope`, `seaPortsAmericas`,
`seaPortsAfricaMena`, `seaPortsOceania` — regional sub-lists
re-exported.
## Implementation Notes
Aggregation is computed once at module load via spread. The cost is
linear in the total port count and pays off immediately because every
lookup-by-locode path hits the merged set. Regional sub-lists remain
referentially distinct from their aggregate appearance, so identity
checks (`port === seaPortsAsia[0]`) keep working after aggregation.

View File

@@ -0,0 +1,57 @@
# shipping-lanes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Curated registry of major global shipping lanes — Asia–Europe via Suez,
Asia–Europe via Cape, Transpacific, Transatlantic, Europe–MENA, plus
rail and air freight lanes — exported as the readonly `shippingLanes`
array. Each lane carries an ordered waypoint chain that ETA, lane
distance, and lane-progress calculations project vessel positions
against.
## Features
- Sea lanes: Asia–Europe via Suez, Asia–Europe via Cape, Transpacific
Asia–US West Coast, Transatlantic Europe–US East Coast.
- Rail lane: China–Europe via Trans-Siberian / Trans-Mongolian.
- Air lane: Asia–Europe.
- Each lane carries `typicalTransitDays` and `typicalSpeedKnots`
(sea/rail) or `typicalSpeedKmh` (air/rail metric).
- Waypoint chains capture strait passages (Malacca, Bab el-Mandeb,
Gibraltar, Suez Canal) so lane-progress reporting reflects real
navigational milestones.
## Usage: Basic
```ts
import { shippingLanes } from "@suaveplan/ports/data/shipping-lanes";
console.log("curated lane count:", shippingLanes.length);
```
## Usage: Advanced
```ts
import { shippingLanes, estimateLaneDistance } from "@suaveplan/ports";
for (const lane of shippingLanes) {
console.log(lane.id, estimateLaneDistance(lane), "nm");
}
```
## API Reference
- `shippingLanes: readonly ShippingLane[]` — curated lane set.
## Implementation Notes
Waypoint chains are coarse — they capture the strategic geography of
the route, not turn-by-turn navigation. Distance and ETA computations
are therefore representative of typical transit, not exact in-water
distance with weather routing. Each lane id is stable across versions
so external systems can reference lanes by string id without worrying
about array reorder.

View File

@@ -0,0 +1,69 @@
# distance
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Wraps `@suaveplan/geo`'s `greatCircleDistance` against a sphere whose
radius matches the IUGG mean Earth radius (6 371 km), bound once as the
exported `EARTH_SPHERE` ellipsoid. The package's lane-distance,
lane-progress, ETA, and nearest-port modules all consume these wrappers
so the entire package agrees on a single radius and the rounding policy
is centralised.
## Features
- `distanceNm(a, b)` — great-circle distance in nautical miles, rounded
to two decimals.
- `distanceKm(a, b)` — same in kilometres, rounded to two decimals.
- `distanceMetresRaw(a, b)` — unrounded metres, useful for accumulating
segment sums without compounding rounding error.
- Symmetric: `distanceNm(a, b) === distanceNm(b, a)` to floating
precision.
- Non-negative for any valid coordinate pair.
- Coordinate inputs use `@suaveplan/geo`'s `SphericalCoord` shape so the
wrappers are interoperable with the entire geospatial stack.
## Usage: Basic
```ts
import { distanceNm } from "@suaveplan/ports";
const nm = distanceNm(
{ latitude: 1.26, longitude: 103.82 },
{ latitude: 51.9, longitude: 4.49 }
);
console.log(nm); // ~5800 nautical miles
```
## Usage: Advanced
```ts
import { distanceMetresRaw } from "@suaveplan/ports";
function totalMetres(coords: { latitude: number; longitude: number }[]): number {
let total = 0;
for (let i = 0; i < coords.length - 1; i++) {
total += distanceMetresRaw(coords[i], coords[i + 1]);
}
return total;
}
```
## API Reference
- `distanceNm(a, b)` → `number`.
- `distanceKm(a, b)` → `number`.
- `distanceMetresRaw(a, b)` → `number` (unrounded).
## Implementation Notes
All three wrappers route through the single `EARTH_SPHERE` constant so
the cost of ellipsoid construction is paid once at module load. Rounded
helpers multiply by 100, round, then divide — this matches the
main-branch rounding convention so cross-version diffs remain
byte-for-byte stable on the rounded values. `distanceMetresRaw` is
deliberately exposed for callers that need full precision (segment
accumulation in lane-distance, deviation-ratio computation in ETA).

View File

@@ -0,0 +1,96 @@
# error-codes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Declares the eight stable error codes emitted by `@suaveplan/ports` and
the matching typed `PortsError` subclasses. Every public function in the
package throws one of these subclasses when a precondition fails, so
downstream consumers — loggers, problem+json responders, retry middleware
— can switch on `.code` without re-implementing the classifier. Codes
flow through `defineCodes("PORTS", [...])` from `@suaveplan/error` so the
full namespace is `PORTS.NO_PORTS`, `PORTS.INVALID_RADIUS`, and so on.
## Features
- Eight codes: `NO_PORTS`, `INVALID_RADIUS`,
`INSUFFICIENT_WAYPOINTS`, `INVALID_SPEED`, `MISSING_SPEED`,
`PORT_NOT_FOUND`, `LANE_NOT_FOUND`, `INVALID_LANE`.
- Eight typed subclasses, each extending `PortsError` which itself
extends `SuaveplanError<PortsErrorCode>`.
- Context carriers — `PortsPortNotFoundError` exposes `locode`,
`PortsLaneNotFoundError` exposes `laneId`, `PortsInvalidLaneError`
exposes `issues` (Zod issue array).
- i18n-ready: every constructor accepts a `message` override defaulting
to an i18n key (`ports.errors.<camelCase>`).
- Cause-chain pass-through (`cause`, `traceId`, `context`) honours the
full `SuaveplanErrorOptions` shape.
## Usage: Basic
```ts
import {
PortsNoPortsError,
PortsInvalidRadiusError,
} from "@suaveplan/ports";
function safeFind(): void {
try {
// ...
} catch (err) {
if (err instanceof PortsInvalidRadiusError) {
// 400 Bad Request
} else if (err instanceof PortsNoPortsError) {
// 503 Service Unavailable
}
}
}
```
## Usage: Advanced
```ts
import {
PORTS_CODES,
PortsPortNotFoundError,
} from "@suaveplan/ports";
const err = new PortsPortNotFoundError({
locode: "XXNON",
traceId: "01HXYZ",
context: { source: "rest" },
});
switch (err.code) {
case PORTS_CODES.PORT_NOT_FOUND:
// Render i18n-aware 404 message with locode in context.
break;
default:
throw err;
}
```
## API Reference
- `PORTS_CODES` — frozen registry: `{ NO_PORTS, INVALID_RADIUS,
INSUFFICIENT_WAYPOINTS, INVALID_SPEED, MISSING_SPEED, PORT_NOT_FOUND,
LANE_NOT_FOUND, INVALID_LANE }`.
- `PortsErrorCode` — union type of every code.
- `PortsError` — abstract base.
- `PortsNoPortsError`, `PortsInvalidRadiusError`,
`PortsInsufficientWaypointsError`, `PortsInvalidSpeedError`,
`PortsMissingSpeedError`, `PortsPortNotFoundError`,
`PortsLaneNotFoundError`, `PortsInvalidLaneError`.
## Implementation Notes
The registry is registered eagerly at module load via `defineCodes` so
collisions across packages fail fast. Each subclass forwards the
`pickOpts` filtered options bundle to `SuaveplanError`'s protected
constructor so context, cause, and traceId remain optional but
correctly typed. Default message keys follow `ports.errors.<camelCase>`
so an i18n resolver can translate them at the boundary; consumers that
prefer literal messages pass `{ message: "..." }`.

View File

@@ -0,0 +1,70 @@
# eta
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Estimates the time of arrival for a vessel travelling along a
`ShippingLane`. Resolves an effective cruise speed from (in order) an
explicit override, the lane's `typicalSpeedKnots`, or the km/h
equivalent, then divides the remaining-distance figure produced by
`findPositionOnLane` to compute `etaHours` and projects to an
`etaDate`. The result includes a `confidence` band derived from
deviation between travelled+remaining and the lane's total length so
operators can flag vessels that have drifted off the planned route.
## Features
- Three-tier speed resolution: explicit override → lane knots → lane
km/h (converted at 1.852 km/nm).
- Injectable wall-clock — `opts.clock?: Clock` defaults to `Date.now`.
- Explicit `opts.departureTime?` overrides the clock.
- Confidence band: `high` (off-lane ≤3% of total length), `medium`
(≤10%), `low` (more than 10%).
- Throws `PortsInvalidSpeedError` on non-positive speed, and
`PortsMissingSpeedError` when no speed source resolves.
- Emits `ports.eta_total{confidence}` counter and
`ports.eta_duration_ms` histogram per call.
- `etaHours` rounded to two decimals; `etaDate` uses unrounded hours
for sub-minute precision.
## Usage: Basic
```ts
import { estimateETA, getShippingLaneStrict } from "@suaveplan/ports";
const suez = getShippingLaneStrict("asia-europe-suez");
const result = estimateETA({ latitude: 12.5, longitude: 43.3 }, suez);
console.log(result.etaDate, result.confidence);
```
## Usage: Advanced
```ts
import { estimateETA } from "@suaveplan/ports";
import { createClock } from "@suaveplan/testing";
const clock = createClock(new Date("2026-05-25T00:00:00Z"));
const eta = estimateETA(coord, lane, 18, {
clock: () => clock.now(),
});
```
## API Reference
- `estimateETA(currentCoord, lane, avgSpeedKnots?, opts?)` → `ETAEstimate`.
- `EstimateETAOptions`:
- `departureTime?: Date` — explicit override.
- `clock?: Clock` — wall-clock source (`() => number`).
## Implementation Notes
Speed resolution is centralised in a private helper so the order of
fallbacks stays explicit. The `etaDate` is computed from the unrounded
hour figure — rounding to two decimals before `* 3_600_000` would
quantise arrival to 36-second buckets. The deviation ratio uses the
lane's full great-circle length as the denominator so the band stays
calibrated independent of vessel position. `performance.now()` is
preferred over `Date.now()` for the duration histogram when available.

View File

@@ -0,0 +1,78 @@
# find-port
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides the lookup surface for `@suaveplan/ports`. Every public
function consumes the curated `allPorts` registry by default but accepts
an explicit `ports` argument so consumers can scope a search to a
subset (a tenant-specific allowlist, an environment-restricted set, or
test fixtures). UN/LOCODE lookups go through a lazily-built `Map`
index keyed on the uppercased locode so the call cost is O(1) once the
index is initialised.
## Features
- `findPort(locode)` — O(1) lookup by UN/LOCODE, case-insensitive,
returns `undefined` on miss.
- `findPortStrict(locode)` — throws `PortsPortNotFoundError`
(`PORTS.PORT_NOT_FOUND`) on miss, carrying the locode in error
context.
- `findPortsByCountry(country, ports?)` — ISO 3166-1 alpha-2 filter
(input uppercased).
- `findPortsByType(type, ports?)` — `PortType` filter.
- `searchPorts(query, ports?)` — case-insensitive substring match on
`name`.
- Every call emits the OTel counter `ports.lookup_total{kind}`.
## Usage: Basic
```ts
import { findPort, findPortsByCountry } from "@suaveplan/ports";
const shanghai = findPort("cnsha");
const dutchPorts = findPortsByCountry("nl");
```
## Usage: Advanced
```ts
import {
findPortStrict,
findPortsByType,
searchPorts,
PortsPortNotFoundError,
} from "@suaveplan/ports";
try {
const port = findPortStrict("ZZZZZ");
} catch (err) {
if (err instanceof PortsPortNotFoundError) {
// 404
}
}
const sea = findPortsByType("sea");
const named = searchPorts("rotterdam", sea); // scoped search
```
## API Reference
- `findPort(locode: string): Port | undefined`.
- `findPortStrict(locode: string): Port` — throws on miss.
- `findPortsByCountry(country: string, ports?: readonly Port[]): Port[]`.
- `findPortsByType(type: PortType, ports?: readonly Port[]): Port[]`.
- `searchPorts(query: string, ports?: readonly Port[]): Port[]`.
## Implementation Notes
The locode index is built lazily on first lookup and cached at module
scope for the lifetime of the process. Re-importing the module never
duplicates the index because ES modules are singletons within a realm.
Filtering helpers return fresh arrays so callers can mutate without
affecting the curated registry. The substring match is allocator-light
— it lowercases the query once and the port name once per record, no
regex compilation in the hot path.

View File

@@ -0,0 +1,65 @@
# lane-distance
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Computes great-circle distance metrics for a `ShippingLane`'s waypoint
chain. `estimateLaneDistance` sums every segment for the lane total;
`cumulativeDistances` returns the per-waypoint cumulative array used by
`findPositionOnLane` and `estimateETA`. Both functions reject lanes
with fewer than two waypoints because any distance metric over a
single-point chain is undefined.
## Features
- `estimateLaneDistance(lane)` — total nautical miles, rounded to two
decimals.
- `cumulativeDistances(lane)` — `number[]` of length `waypoints.length`
where entry 0 is 0 and entry i is the cumulative nm distance from
`waypoints[0]` to `waypoints[i]`.
- Throws `PortsInsufficientWaypointsError`
(`PORTS.INSUFFICIENT_WAYPOINTS`) for lanes shorter than two
waypoints.
- Emits the `ports.lane_distance_duration_ms` histogram.
- Uses the package's shared `distanceNm` helper so the rounding policy
is centralised.
## Usage: Basic
```ts
import { estimateLaneDistance, getShippingLaneStrict } from "@suaveplan/ports";
const suez = getShippingLaneStrict("asia-europe-suez");
const totalNm = estimateLaneDistance(suez);
```
## Usage: Advanced
```ts
import { cumulativeDistances, getShippingLaneStrict } from "@suaveplan/ports";
const lane = getShippingLaneStrict("asia-europe-suez");
const cum = cumulativeDistances(lane);
// cum[i] - cum[i-1] gives segment length between waypoints i-1 and i.
for (let i = 1; i < cum.length; i++) {
console.log("segment", i, cum[i] - cum[i - 1], "nm");
}
```
## API Reference
- `estimateLaneDistance(lane: ShippingLane): number`.
- `cumulativeDistances(lane: ShippingLane): number[]`.
## Implementation Notes
Each segment runs through `distanceNm` which itself routes through
`@suaveplan/geo`'s `greatCircleDistance` against the
`EARTH_SPHERE` constant. The cumulative function recomputes segment
distances rather than reusing the running sum from
`estimateLaneDistance` — this keeps the two functions independent so
callers can use either in isolation without paying for both. The
trivial perf cost is acceptable at curated-dataset scale.

View File

@@ -0,0 +1,67 @@
# lane-lookup
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Public lookup surface for the curated `shippingLanes` registry. Mirrors
the port-lookup module in style: an O(1) `Map` index by lane id, mode
filtering, and a "get all" accessor for downstream consumers that want
to render lists or build their own indices. The strict variant of
`getShippingLane` throws a typed `PortsLaneNotFoundError` so consumers
can branch on `.code` rather than nullable returns.
## Features
- `getShippingLane(id)` — lazy `Map<string, ShippingLane>` lookup.
- `getShippingLaneStrict(id)` — throws on miss with `laneId` in error
context.
- `getShippingLanesByMode(mode, lanes?)` — `LaneMode` filter.
- `getAllShippingLanes()` — returns the curated registry directly
(readonly).
- Every call emits `ports.lane_lookup_total{mode}` (mode is `all` for
`getAllShippingLanes` and miss-paths).
## Usage: Basic
```ts
import { getShippingLane, getShippingLanesByMode } from "@suaveplan/ports";
const suez = getShippingLane("asia-europe-suez");
const seaLanes = getShippingLanesByMode("sea");
```
## Usage: Advanced
```ts
import {
getShippingLaneStrict,
PortsLaneNotFoundError,
} from "@suaveplan/ports";
try {
const lane = getShippingLaneStrict("missing-lane");
} catch (err) {
if (err instanceof PortsLaneNotFoundError) {
// err.context.laneId === "missing-lane"
}
}
```
## API Reference
- `getShippingLane(id: string): ShippingLane | undefined`.
- `getShippingLaneStrict(id: string): ShippingLane`.
- `getShippingLanesByMode(mode: LaneMode, lanes?: readonly ShippingLane[]):
ShippingLane[]`.
- `getAllShippingLanes(): readonly ShippingLane[]`.
## Implementation Notes
The lane id index is built lazily on first call and cached at module
scope for the lifetime of the process. `getAllShippingLanes` returns
the curated registry directly rather than a copy — callers must not
mutate it. Filtering helpers return fresh arrays so callers retain
mutation safety on derived sets.

View File

@@ -0,0 +1,71 @@
# lane-progress
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Projects a vessel position onto a shipping lane and reports how much of
the lane has been traversed, how much remains, and which waypoint is
nearest. Used by `estimateETA` to derive the remaining-distance figure
that feeds the ETA computation, and exposed publicly so downstream UIs
can render lane-progress bars without re-implementing the geometry.
## Features
- Finds the nearest waypoint by great-circle distance.
- Estimates fractional progress within the nearest waypoint's adjacent
segment (prev or next, whichever is closer).
- Returns `{ progress, remainingDistanceNm, distanceTravelledNm,
nearestWaypointIndex }` with `progress` clamped to `[0, 1]` and
rounded to four decimals; distances clamped to `>= 0` and rounded to
two decimals.
- Throws `PortsInsufficientWaypointsError` for lanes with fewer than
two waypoints.
- Handles degenerate zero-length lanes (every waypoint identical) by
returning zero progress.
## Usage: Basic
```ts
import { findPositionOnLane, getShippingLaneStrict } from "@suaveplan/ports";
const lane = getShippingLaneStrict("asia-europe-suez");
const progress = findPositionOnLane(
{ latitude: 12.5, longitude: 43.3 },
lane
);
```
## Usage: Advanced
```ts
import { findPositionOnLane, type ShippingLane } from "@suaveplan/ports";
function eta(
coord: { latitude: number; longitude: number },
lane: ShippingLane,
knots: number,
departure: Date
): Date {
const { remainingDistanceNm } = findPositionOnLane(coord, lane);
const hours = remainingDistanceNm / knots;
return new Date(departure.getTime() + hours * 3_600_000);
}
```
## API Reference
- `findPositionOnLane(coord, lane): LaneProgress`.
## Implementation Notes
The "nearest waypoint" pass is a linear scan. The adjacent-segment
projection uses a 1D ratio (distance-to-nearest / segment-length)
clamped to `[0, 1]`; this is an approximation that works well when the
coord is reasonably close to the lane and degrades gracefully when the
coord is far off-lane (the ETA `confidence` band downgrades to
`medium`/`low`). The function never throws on out-of-lane coords —
callers receive a clamped progress fraction and a non-negative
remaining distance regardless of how far off-lane the vessel is.

View File

@@ -0,0 +1,65 @@
# nearest-port
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Public proximity-search API. `findNearestPort` returns the single
closest port to a coordinate; `findPortsInRadius` returns every port
within a great-circle radius, sorted ascending by distance. When called
against the default `allPorts` registry the work delegates to a
pre-built `PortSpatialIndex`; explicit overrides fall through to a
linear scan so the caller retains full control over the candidate set.
## Features
- O(neighbours) on the default registry via `PortSpatialIndex`.
- Linear scan fallback for custom port sets.
- `findNearestPort` throws `PortsNoPortsError` (`PORTS.NO_PORTS`) on an
empty input.
- `findPortsInRadius` throws `PortsInvalidRadiusError`
(`PORTS.INVALID_RADIUS`) on `radiusKm <= 0`.
- Distance returned in kilometres, rounded to two decimals.
- Emits `ports.lookup_total{kind}` per call and the
`ports.find_nearest_duration_ms` histogram for `findNearestPort`.
## Usage: Basic
```ts
import { findNearestPort, findPortsInRadius } from "@suaveplan/ports";
const nearest = findNearestPort({ latitude: 1.26, longitude: 103.82 });
const within500 = findPortsInRadius(
{ latitude: 1.26, longitude: 103.82 },
500
);
```
## Usage: Advanced
```ts
import { findNearestPort, findPortsByType } from "@suaveplan/ports";
const onlySea = findPortsByType("sea");
const nearestSea = findNearestPort(
{ latitude: 51.9, longitude: 4.49 },
onlySea
);
```
## API Reference
- `findNearestPort(coord, ports?)` — `NearestPortResult`.
- `findPortsInRadius(coord, radiusKm, ports?)` — `NearestPortResult[]`,
sorted ascending by `distanceKm`.
## Implementation Notes
The spatial index is built lazily on first call against the default
registry. The duration histogram uses `performance.now()` when
available, falling back to `Date.now()` so the package stays usable in
runtimes without a high-resolution timer. Custom port sets bypass the
index because building a fresh index per call would dominate the
linear-scan cost we are trying to avoid.

View File

@@ -0,0 +1,72 @@
# otel
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Wires `@suaveplan/ports`' OpenTelemetry instrumentation: three counters
that track lookup volume per kind, lane lookup volume per mode, and ETA
estimations per confidence band; plus three histograms recording the
duration of `findNearestPort`, `estimateLaneDistance`, and `estimateETA`.
Every public function in the package emits exactly one counter
increment and (where relevant) one histogram sample on each call so
operators can build dashboards without re-instrumenting consumers.
## Features
- Three counters: `ports.lookup_total{kind}`,
`ports.lane_lookup_total{mode}`, `ports.eta_total{confidence}`.
- Three histograms: `ports.find_nearest_duration_ms`,
`ports.lane_distance_duration_ms`, `ports.eta_duration_ms`, all in
millisecond units.
- `refreshPortsMeters()` rebinds every instrument against the current
global `MeterProvider` — tests call this after wiring an in-memory
exporter so subsequent record calls land on the test provider rather
than the module-load meter.
- Recorder helpers (`recordPortLookup`, `recordLaneLookup`,
`recordEta`, `record*Duration`) are 1:1 with the named instruments
and accept the literal-typed label values directly.
- Meter is named `@suaveplan/ports` at version `0.1.0`.
## Usage: Basic
```ts
import { recordPortLookup } from "@suaveplan/ports";
recordPortLookup("findPort");
```
## Usage: Advanced
```ts
import { MeterProvider } from "@opentelemetry/sdk-metrics";
import { metrics } from "@opentelemetry/api";
import { refreshPortsMeters } from "@suaveplan/ports";
const provider = new MeterProvider();
metrics.setGlobalMeterProvider(provider);
refreshPortsMeters();
// Now every subsequent ports operation lands on `provider`.
```
## API Reference
- `recordPortLookup(kind)` — kind ∈ `findPort | byCountry | byType |
search | findNearest | inRadius`.
- `recordLaneLookup(mode)` — mode ∈ `sea | rail | air | all`.
- `recordEta(confidence)` — confidence ∈ `high | medium | low`.
- `recordFindNearestDuration(ms)`,
`recordLaneDistanceDuration(ms)`, `recordEtaDuration(ms)`.
- `refreshPortsMeters()` — re-resolve every instrument.
## Implementation Notes
Instruments are module-scope `let` bindings so `refreshPortsMeters` can
swap them in place; otherwise the OTel API would route emissions to the
original meter even after `setGlobalMeterProvider`. The histograms use
the `ms` unit hint so exporters render them as durations. The package
deliberately does not start its own spans — span management is the
caller's responsibility, and instruments stay focused on aggregate
behaviour rather than per-call tracing.

View File

@@ -0,0 +1,74 @@
# schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines Zod schemas for every public type emitted by `@suaveplan/ports`.
API plugins, message-queue handlers, and any other consumer that
receives a `Port` or `ShippingLane` over the wire passes the payload
through `PortSchema.parse(...)` / `ShippingLaneSchema.parse(...)` to
guarantee shape before the curated functions touch the data. The
schemas mirror the TypeScript interfaces in `types/port.types.ts`
exactly so a runtime success implies a sound compile-time `Port` or
`ShippingLane`.
## Features
- Every TS interface (`Port`, `ShippingLane`, `LaneProgress`,
`ETAEstimate`, `NearestPortResult`, `SphericalCoord`) has a matching
Zod schema.
- Locode is regex-validated as five uppercase A–Z characters.
- Country is regex-validated as ISO 3166-1 alpha-2 (two uppercase).
- IATA code (when present) is regex-validated as three uppercase A–Z.
- Latitude clamped to `[-90, 90]`; longitude to `[-180, 180]`.
- `ShippingLaneSchema` enforces at least two waypoints — matching the
precondition of every lane-distance / lane-progress / ETA function.
- Throughput and capacity fields are non-negative integers.
## Usage: Basic
```ts
import { PortSchema } from "@suaveplan/ports";
const port = PortSchema.parse(payload); // throws on shape mismatch
```
## Usage: Advanced
```ts
import { ShippingLaneSchema, PortsInvalidLaneError } from "@suaveplan/ports";
const result = ShippingLaneSchema.safeParse(payload);
if (!result.success) {
throw new PortsInvalidLaneError({
issues: result.error.issues,
});
}
const lane = result.data;
```
## API Reference
- `SphericalCoordSchema` — `{ latitude, longitude, altitude? }`.
- `PortSchema` — full `Port` shape.
- `PortTypeSchema` — `z.enum(["sea","air","rail","multimodal"])`.
- `PortFacilitySchema` — 10-value enum.
- `ShippingLaneSchema` — `id, name, mode, waypoints≥2, typical*?`.
- `LaneModeSchema` — `z.enum(["sea","rail","air"])`.
- `LaneProgressSchema` — clamped-progress object.
- `ETAEstimateSchema` — `{ remainingDistanceNm, etaHours, etaDate,
confidence }`.
- `NearestPortResultSchema` — `{ port, distanceKm }`.
## Implementation Notes
Schemas use Zod's default-import form (`import z from "zod"`) as
mandated by project rules. Optional fields are declared with
`.optional()` so undefined and absence are interchangeable on the wire.
The schemas do not enforce cross-field invariants (e.g. that
`distanceTravelledNm + remainingDistanceNm <= totalLaneDistance`) —
those invariants are guaranteed by the producing functions, not the
inbound parse step.

View File

@@ -0,0 +1,76 @@
# spatial-index
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`PortSpatialIndex` buckets a port set by 5-degree latitude/longitude
cells at construction time so `findNearest` and `findInRadius` can run
against a small candidate set instead of the full registry. The 3×3
cell neighbourhood is ~600 km wide, so a 500-km radius query (the
common case) visits at most nine cells. For the curated ~120-port
dataset the absolute saving is small, but the API stays O(neighbours)
as the dataset grows.
## Features
- 5° lat/lng cell bucketing — bucket key is
`floor(lat / 5) + ":" + floor(lng / 5)`.
- O(neighbours) `findNearest` that falls back to a full scan when the
3×3 neighbourhood is empty (Pacific gyre, polar regions).
- `findInRadius(coord, radiusKm)` returns ports within `radiusKm`
sorted ascending by distance, switching to a full scan when the
requested radius exceeds the half-width of the 3×3 neighbourhood
(~830 km) to guarantee correctness.
- Immutable after construction — no add/remove API, so callers rebuild
to reflect updates and the index never falls out of sync with its
source set.
- `size` exposes the total indexed port count.
## Usage: Basic
```ts
import { PortSpatialIndex, allPorts } from "@suaveplan/ports";
const index = new PortSpatialIndex(allPorts);
const nearest = index.findNearest({ latitude: 1.26, longitude: 103.82 });
```
## Usage: Advanced
```ts
import { PortSpatialIndex } from "@suaveplan/ports";
// Tenant-scoped index built once per request.
function buildIndex(allowed: Port[]): PortSpatialIndex {
return new PortSpatialIndex(allowed);
}
const tenant = buildIndex(allowedPortsForTenant);
const nearby = tenant.findInRadius(
{ latitude: 35.4, longitude: 139.6 },
250
);
```
## API Reference
- `new PortSpatialIndex(ports)` — pre-bucket the input.
- `index.size` — total indexed port count.
- `index.neighbourhood(coord)` — every port in the 3×3 cell around
`coord`.
- `index.findNearest(coord)` — nearest `NearestPortResult` or
`undefined` when the index is empty.
- `index.findInRadius(coord, radiusKm)` — sorted `NearestPortResult[]`.
## Implementation Notes
The 5° cell width was chosen because individual seas (the Med, the
Caribbean, the Sea of Japan) span 2–4 cells, which keeps the
neighbourhood candidate set tight without artificially excluding ports
on cell boundaries. The radius branch threshold (`> 1.5 × 5° × 111`)
ensures we never miss a port outside the neighbourhood: a circle of
radius greater than half the neighbourhood width must be tested
against the full set.

View File

@@ -0,0 +1,80 @@
# port.types
## Purpose
Defines every type emitted by `@suaveplan/ports`: the curated `Port`
record, the `ShippingLane` waypoint graph, the result types for lane
projection (`LaneProgress`) and ETA estimation (`ETAEstimate`), and the
discriminating unions for port type and facility tags. Every public
function in the package consumes or returns one of these types so the
file is the single source of truth for the package's data contract.
## Features
- `Port` carries a UN/LOCODE primary key plus geographic coordinate,
country, type, timezone, and facility tags.
- Optional capacity hints (`maxVesselSizeTEU`, `maxVesselSizeDWT`,
`runwayLengthM`, `annualThroughputTEU`, `annualThroughputTonnes`).
- `PortType` discriminates between `sea | air | rail | multimodal`.
- `ShippingLane.waypoints` is an ordered `SphericalCoord` array.
- `LaneMode` is `sea | rail | air`.
- `ETAEstimate.confidence` is `high | medium | low` derived from
deviation between travelled+remaining and the lane's total distance.
- Re-exports `SphericalCoord` from `@suaveplan/geo` so consumers only
import from a single canonical location.
## Usage: Basic
```ts
import type { Port } from "@suaveplan/ports";
const port: Port = {
locode: "SGSIN",
name: "Singapore",
country: "SG",
coordinate: { latitude: 1.26, longitude: 103.82 },
type: "sea",
timezone: "Asia/Singapore",
facilities: ["container", "bulk"],
};
```
## Usage: Advanced
```ts
import type { ETAEstimate, ShippingLane } from "@suaveplan/ports";
function logEta(eta: ETAEstimate, lane: ShippingLane): void {
console.info({
lane: lane.id,
etaHours: eta.etaHours,
confidence: eta.confidence,
remainingNm: eta.remainingDistanceNm,
});
}
```
## API Reference
- `Port` — curated port record with optional capacity hints.
- `PortType` — `"sea" | "air" | "rail" | "multimodal"`.
- `PortFacility` — 10-value union: container, bulk, roro, tanker,
passenger, cargo, reefer, intermodal, bonded_warehouse,
free_trade_zone.
- `ShippingLane` — id, name, mode, waypoints, optional typical transit
days and speeds (knots or km/h).
- `LaneMode` — `"sea" | "rail" | "air"`.
- `LaneProgress` — `{ progress, remainingDistanceNm,
distanceTravelledNm, nearestWaypointIndex }`.
- `ETAEstimate` — `{ remainingDistanceNm, etaHours, etaDate, confidence }`.
- `NearestPortResult` — `{ port, distanceKm }`.
- `SphericalCoord` — re-exported from `@suaveplan/geo`.
## Implementation Notes
Every interface is `readonly` end-to-end so curated records remain
immutable across consumers. Optional fields are non-nullable
unioned with `undefined` via TypeScript's exact-optional semantics —
schemas in `schemas/schemas.ts` enforce the same shape at runtime for
incoming payloads. The file deliberately contains no runtime code so
type-only imports do not pay any bundle cost.