first commit
This commit is contained in:
31
docs/geospatial/README.md
Normal file
31
docs/geospatial/README.md
Normal 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 -->
|
||||
3
docs/geospatial/_intro.md
Normal file
3
docs/geospatial/_intro.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `geospatial`
|
||||
|
||||
Geospatial data, mapping, planetary coordinates, and logistics utilities.
|
||||
19
docs/geospatial/geo/README.md
Normal file
19
docs/geospatial/geo/README.md
Normal 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 -->
|
||||
1
docs/geospatial/geo/_intro.md
Normal file
1
docs/geospatial/geo/_intro.md
Normal file
@@ -0,0 +1 @@
|
||||
Geospatial coordinates, planetary data, and solar-system modelling.
|
||||
31
docs/geospatial/geo/geo-earth/README.md
Normal file
31
docs/geospatial/geo/geo-earth/README.md
Normal 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 });
|
||||
```
|
||||
63
docs/geospatial/geo/geo-earth/src/coordinates/coordinates.md
Normal file
63
docs/geospatial/geo/geo-earth/src/coordinates/coordinates.md
Normal 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.
|
||||
@@ -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.
|
||||
78
docs/geospatial/geo/geo-earth/src/geocoding/geocoding.md
Normal file
78
docs/geospatial/geo/geo-earth/src/geocoding/geocoding.md
Normal 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.
|
||||
73
docs/geospatial/geo/geo-earth/src/geohash/geohash.md
Normal file
73
docs/geospatial/geo/geo-earth/src/geohash/geohash.md
Normal 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.
|
||||
53
docs/geospatial/geo/geo-planet/README.md
Normal file
53
docs/geospatial/geo/geo-planet/README.md
Normal 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)
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
84
docs/geospatial/geo/geo-planet/src/types/schemas.md
Normal file
84
docs/geospatial/geo/geo-planet/src/types/schemas.md
Normal 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.
|
||||
117
docs/geospatial/geo/geo-planet/src/types/types.md
Normal file
117
docs/geospatial/geo/geo-planet/src/types/types.md
Normal 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.
|
||||
69
docs/geospatial/geo/geo/README.md
Normal file
69
docs/geospatial/geo/geo/README.md
Normal 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
|
||||
79
docs/geospatial/geo/geo/src/angular/angular.md
Normal file
79
docs/geospatial/geo/geo/src/angular/angular.md
Normal 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.
|
||||
55
docs/geospatial/geo/geo/src/bearing/bearing.md
Normal file
55
docs/geospatial/geo/geo/src/bearing/bearing.md
Normal 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`.
|
||||
66
docs/geospatial/geo/geo/src/bounds/bounds.md
Normal file
66
docs/geospatial/geo/geo/src/bounds/bounds.md
Normal 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.
|
||||
100
docs/geospatial/geo/geo/src/distance/distance.md
Normal file
100
docs/geospatial/geo/geo/src/distance/distance.md
Normal 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.
|
||||
51
docs/geospatial/geo/geo/src/ellipsoid/ellipsoid.md
Normal file
51
docs/geospatial/geo/geo/src/ellipsoid/ellipsoid.md
Normal 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.
|
||||
17
docs/geospatial/geo/geo/src/observability/observability.md
Normal file
17
docs/geospatial/geo/geo/src/observability/observability.md
Normal 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());
|
||||
```
|
||||
63
docs/geospatial/geo/geo/src/polygon/polygon.md
Normal file
63
docs/geospatial/geo/geo/src/polygon/polygon.md
Normal 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.
|
||||
59
docs/geospatial/geo/geo/src/transforms/transforms.md
Normal file
59
docs/geospatial/geo/geo/src/transforms/transforms.md
Normal 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.
|
||||
102
docs/geospatial/geo/geo/src/types/schemas.md
Normal file
102
docs/geospatial/geo/geo/src/types/schemas.md
Normal 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.
|
||||
114
docs/geospatial/geo/geo/src/types/types.md
Normal file
114
docs/geospatial/geo/geo/src/types/types.md
Normal 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.
|
||||
95
docs/geospatial/geo/geo/src/units/units.md
Normal file
95
docs/geospatial/geo/geo/src/units/units.md
Normal 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.
|
||||
19
docs/geospatial/geo/solar-system/README.md
Normal file
19
docs/geospatial/geo/solar-system/README.md
Normal 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
|
||||
```
|
||||
61
docs/geospatial/geo/solar-system/src/provider/provider.md
Normal file
61
docs/geospatial/geo/solar-system/src/provider/provider.md
Normal 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`.
|
||||
65
docs/geospatial/geo/solar-system/src/seasons/seasons.md
Normal file
65
docs/geospatial/geo/solar-system/src/seasons/seasons.md
Normal 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.
|
||||
24
docs/geospatial/geo/stars/README.md
Normal file
24
docs/geospatial/geo/stars/README.md
Normal 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
|
||||
```
|
||||
50
docs/geospatial/geo/stars/src/catalog/catalog.md
Normal file
50
docs/geospatial/geo/stars/src/catalog/catalog.md
Normal 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.
|
||||
57
docs/geospatial/geo/stars/src/provider/provider.md
Normal file
57
docs/geospatial/geo/stars/src/provider/provider.md
Normal 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.
|
||||
48
docs/geospatial/geo/stars/src/types.md
Normal file
48
docs/geospatial/geo/stars/src/types.md
Normal 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`.
|
||||
33
docs/geospatial/geo/tides/README.md
Normal file
33
docs/geospatial/geo/tides/README.md
Normal 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
|
||||
```
|
||||
|
||||
73
docs/geospatial/geo/tides/src/admiralty/admiralty.md
Normal file
73
docs/geospatial/geo/tides/src/admiralty/admiralty.md
Normal 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`.
|
||||
@@ -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.
|
||||
69
docs/geospatial/geo/tides/src/harmonic/harmonic.md
Normal file
69
docs/geospatial/geo/tides/src/harmonic/harmonic.md
Normal 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).
|
||||
57
docs/geospatial/geo/tides/src/physics/physics.md
Normal file
57
docs/geospatial/geo/tides/src/physics/physics.md
Normal 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.
|
||||
66
docs/geospatial/geo/tides/src/provider/provider.md
Normal file
66
docs/geospatial/geo/tides/src/provider/provider.md
Normal 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.
|
||||
50
docs/geospatial/geo/tides/src/stations/stations.md
Normal file
50
docs/geospatial/geo/tides/src/stations/stations.md
Normal 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`).
|
||||
68
docs/geospatial/geo/tides/src/types/types.md
Normal file
68
docs/geospatial/geo/tides/src/types/types.md
Normal 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.
|
||||
27
docs/geospatial/geo/universe/README.md
Normal file
27
docs/geospatial/geo/universe/README.md
Normal 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
|
||||
```
|
||||
138
docs/geospatial/geo/universe/src/client/client.md
Normal file
138
docs/geospatial/geo/universe/src/client/client.md
Normal 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.
|
||||
91
docs/geospatial/geo/universe/src/frames/frames.md
Normal file
91
docs/geospatial/geo/universe/src/frames/frames.md
Normal 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."
|
||||
114
docs/geospatial/geo/universe/src/observer/observer.md
Normal file
114
docs/geospatial/geo/universe/src/observer/observer.md
Normal 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.
|
||||
121
docs/geospatial/geo/universe/src/packets/packets.md
Normal file
121
docs/geospatial/geo/universe/src/packets/packets.md
Normal 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.
|
||||
139
docs/geospatial/geo/universe/src/providers/providers.md
Normal file
139
docs/geospatial/geo/universe/src/providers/providers.md
Normal 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.
|
||||
137
docs/geospatial/geo/universe/src/types/types.md
Normal file
137
docs/geospatial/geo/universe/src/types/types.md
Normal 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.
|
||||
13
docs/geospatial/logistics/README.md
Normal file
13
docs/geospatial/logistics/README.md
Normal 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 -->
|
||||
1
docs/geospatial/logistics/_intro.md
Normal file
1
docs/geospatial/logistics/_intro.md
Normal file
@@ -0,0 +1 @@
|
||||
Port data and logistics primitives.
|
||||
104
docs/geospatial/logistics/ports/README.md
Normal file
104
docs/geospatial/logistics/ports/README.md
Normal 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).
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
69
docs/geospatial/logistics/ports/src/distance/distance.md
Normal file
69
docs/geospatial/logistics/ports/src/distance/distance.md
Normal 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).
|
||||
@@ -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: "..." }`.
|
||||
70
docs/geospatial/logistics/ports/src/eta/eta.md
Normal file
70
docs/geospatial/logistics/ports/src/eta/eta.md
Normal 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.
|
||||
78
docs/geospatial/logistics/ports/src/find-port/find-port.md
Normal file
78
docs/geospatial/logistics/ports/src/find-port/find-port.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
72
docs/geospatial/logistics/ports/src/otel/otel.md
Normal file
72
docs/geospatial/logistics/ports/src/otel/otel.md
Normal 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.
|
||||
74
docs/geospatial/logistics/ports/src/schemas/schemas.md
Normal file
74
docs/geospatial/logistics/ports/src/schemas/schemas.md
Normal 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.
|
||||
@@ -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.
|
||||
80
docs/geospatial/logistics/ports/src/types/port.types.md
Normal file
80
docs/geospatial/logistics/ports/src/types/port.types.md
Normal 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.
|
||||
Reference in New Issue
Block a user