first commit
This commit is contained in:
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.
|
||||
Reference in New Issue
Block a user