@chrtco/sdk 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +75 -1
- package/README.md +402 -38
- package/dist/atlas/browser.cjs +9011 -609
- package/dist/atlas/browser.cjs.map +1 -1
- package/dist/atlas/browser.d.cts +16 -1
- package/dist/atlas/browser.d.ts +16 -1
- package/dist/atlas/browser.js +9009 -609
- package/dist/atlas/browser.js.map +1 -1
- package/dist/index.cjs +9896 -771
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +777 -56
- package/dist/index.d.ts +777 -56
- package/dist/index.js +9874 -772
- package/dist/index.js.map +1 -1
- package/package.json +3 -20
package/CHANGELOG.md
CHANGED
|
@@ -3,11 +3,85 @@
|
|
|
3
3
|
All notable changes to `@chrtco/sdk` will be documented in this file.
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
|
+
- **`/v1/cells` now sends flat `bbox_north` / `bbox_south` / `bbox_east` / `bbox_west`, matching `CellDetail`.** This is a **wire change to `/v1/cells` and `/v1/cells/{id}`**, not just a type change — read it before upgrading a REST client.
|
|
7
|
+
- The bug: the SDK declared flat `bbox_*`, the endpoint returned a nested `bbox` object. `cell.bbox_north` typechecked and was `undefined` on **every** response — TypeScript vouching for a field that never arrived, which is worse than no type at all.
|
|
8
|
+
- **The endpoint was the outlier, so the endpoint moved.** `enc_cells`' own columns, `/v1/coverage/route`'s `CoverageRouteCell`, and every published `CellDetail` are flat; `formatCell()` alone nested. One shape across the API is the point.
|
|
9
|
+
- **REST clients reading `cell.bbox.north` must move to `cell.bbox_north`.** SDK callers need no change: this is the shape `@chrtco/sdk` has always declared, so **0.9.2 callers are fixed the moment the endpoint deploys** — the phantom field starts carrying real values without a release.
|
|
10
|
+
- Bounds are now passed through **independently** rather than gated on `bbox_west != null`, which would have discarded three real bounds had that one column ever been null alone.
|
|
11
|
+
- The shape is now pinned from both sides: `serve-cells/format-cell.test.ts` (Deno) asserts what the server emits, `endpoints/cells.test.ts` (vitest) asserts what the SDK expects. Neither existed, which is why the two drifted for months. They cannot share a fixture across runtimes — **change one, change the other.**
|
|
12
|
+
- New: **`cell.last_checked_at`** (ISO 8601 | null) — when CHRT last checked the issuing office's catalogue for the cell. It is the freshness of the *check*, not of the chart: `edition` / `update_number` / `issue_date` remain the data-currency fields.
|
|
13
|
+
- **CHRToffline is Pro-and-above, and `chrt.offline.*` now says so.** New typed error code `offline_requires_pro_plan` (403 from `POST /v1/mint/offline-token`). This refusal previously fell through to the generic 403 mapping and surfaced as **`coverage_denied`** — reporting a plan gate as a coverage problem, and pointing the user at the one thing that could not fix it. The per-key `offline:download` scope (`offline_not_enabled`) is a **separate, narrower gate that is checked first**: a key without the scope is refused before the plan is ever read.
|
|
14
|
+
- **`chrt.cells.list()` gains the `scaleBand` and `bbox` filters** that `/v1/cells` has always supported and the SDK could not reach:
|
|
15
|
+
- `scaleBand?: ScaleBand` — new exported union `'Overview' | 'General' | 'Coastal' | 'Approach' | 'Harbor'`, matched **case-sensitively** (`'harbor'` matches nothing). These are the S-57 usage bands CHRT derives from compilation scale; they are **words, not band numbers** — `scale_band: '4'` matches no cell.
|
|
16
|
+
- `bbox?: [west, south, east, north]` — cells whose bounds *intersect* the box (same tuple encoding as `chrt.features()`).
|
|
17
|
+
- **`chrt.query()` returns the new `QueryResponse`** instead of a bare `FeatureCollection` — a widening, so no caller breaks:
|
|
18
|
+
- `res.query` (`{ point, radius_m }`) is now declared. The server echoes back what it **actually applied after clamping**, which is the only way to learn that `radius: 90000` was cut to `50000`. It was previously invisible to typed callers.
|
|
19
|
+
- `res.features[].properties` is typed `QueryFeatureProperties` (`layer`, `cell_id`, `distance_m`, plus an open index signature) — `distance_m` no longer needs a cast, which is the whole point of a proximity search.
|
|
20
|
+
- Documented that `/v1/query` is **not paginated**: it returns no `offset`. Widen `radius` instead of paging.
|
|
21
|
+
- **Corrected docs that sent callers to empty results** (no code change, but they were wrong in the same way the types were):
|
|
22
|
+
- `source` takes the chart-source **id** (`'NOAA_ENC'`), not the agency name (`'NOAA'`). Passing the agency **returns an empty list rather than an error** — a silent zero. Affects `CellsOptions`, `CoverageOptions`, `CoverageSource.source`, and the README quick-start, which shipped the broken `{ source: 'NOAA' }`.
|
|
23
|
+
- `chrt.query()`'s `limit` default (50) and cap (500) are now documented on `QueryOptions`, alongside the `radius` default corrected in the README rewrite below.
|
|
24
|
+
- New exported types: `ScaleBand`, `QueryResponse`, `QueryFeatureProperties`.
|
|
25
|
+
- **`vessel` on `ChrtConfig` — one safety depth, read by every depth-aware call (ADR 0020 F2).** CHRT had grown four disagreeing notions of "how deep is safe"; this is the single place a client states it once.
|
|
26
|
+
```ts
|
|
27
|
+
const chrt = new Chrt({ apiKey, vessel: { safetyDepthM: 3 } });
|
|
28
|
+
await chrt.ahead({ point, bearing }); // filters at 3 m
|
|
29
|
+
chrt.style(); // labels + danger rings at 3 m
|
|
30
|
+
```
|
|
31
|
+
- Defaults `ahead({ safetyDepth })`, `alongRoute({ safetyDepth })` and `style({ safetyContour })`. An explicit per-call value always wins. One `safetyDepthM` drives **30 sites** in the generated style (SOUNDG two-shade + all four danger-ring classes) at a single value.
|
|
32
|
+
- **Client-side and per-instance, deliberately — never bound to your API key.** A consumer app has one key and many skippers with different boats; a key-scoped draft would give them all the same number, which is worse than none — it's a confidently wrong one. Construct a `Chrt` per user, or pass `safetyDepth` per call.
|
|
33
|
+
- **With no profile, nothing is sent.** CHRT never guesses a draft: filtering on an invented one silently drops real hazards.
|
|
34
|
+
- **Known gap:** `styleUrl()` (the *served* style — the only style path for non-JS clients) still takes `theme` + `symbols` only, so a **native app cannot yet give each user their own depth shading**. And `depareFillExpression` bins depth into fixed 0/2/5/10 tiers, so the profile does not reshade depth *areas* — only soundings and danger rings. Both are tracked in ADR 0020.
|
|
35
|
+
- New type: `VesselProfile`.
|
|
36
|
+
- **`chrt.alongRoute()` + `chrt.coverage.forRoute()` — the along-route pair (CHRTquery; ADR 0020 D1).** Completes the family the staged `routeQuery` / `routeFeatures` stubs never shipped, and **splits them by the question they answer**:
|
|
37
|
+
- **`chrt.alongRoute({ route, buffer | bufferNm, safetyDepth, layer, limit, offset })`** → `GET /v1/query/route`. What's charted in a corridor either side of a passage — the planning sibling of {@link ahead}. Each feature carries **`along_m`** (metres along the route to closest approach) alongside `distance_m`, `least_depth_m` and `depth_clearance_m`, and results are **ordered by `along_m`** — the order you actually meet them, not lateral offset. `chrt.alongRouteIter()` auto-paginates.
|
|
38
|
+
- **`chrt.coverage.forRoute({ route, buffer | bufferNm, scaleBand, limit })`** → `GET /v1/coverage/route`. Which ENC cells a passage needs, largest-scale first (S-57 says believe the finer chart), each with `edition` / `update_number` / `issue_date` so you can tell whether the copy you hold is stale. **It lives on `coverage`, not `query`, deliberately** — it returns *cells*, so it answers "which charts must I hold?", a download-planning question and CHRToffline's companion. Filing it with the hazard queries is what made the family look like two overlapping things.
|
|
39
|
+
- **`alongRoute` does not replace `ahead`, and `ahead` does not replace it.** A boat with a route still drifts, and COG rarely equals the route bearing — a passage app wants both: this once per route edit, `ahead` continuously against the real track.
|
|
40
|
+
- Same invariants as `ahead`: **facts that sort, never a verdict** (no severity, no `safe` flag), `safetyDepth` omits only what is *known* to be deeper, and **an empty result is not a clear passage** — every response carries `coverage` plus `attribution` / `disclaimer` / `notice`.
|
|
41
|
+
- Both validate locally before the round trip (≥2 waypoints; `buffer` and `bufferNm` are mutually exclusive).
|
|
42
|
+
- New types: `AlongRouteOptions`, `AlongRouteResponse`, `AlongRouteFeatureProperties`, `CoverageRouteOptions`, `CoverageRouteResponse`, `CoverageRouteCell`. **Removed** the never-shipped staged types `RouteQueryOptions`, `RouteQueryResponse`, `RouteFeatureQueryOptions`, `RouteFeatureQueryResponse` — they were exported but unreachable (their methods were commented out), so nothing could have depended on them.
|
|
43
|
+
- **`chrt.ahead()` — the fan-ahead query (CHRTquery; ADR 0020).** *"What's charted ahead of me?"* — the significant features in a sector from a point along a bearing. Built for the position-plus-COG case: a craft that has a heading but not a planned route.
|
|
44
|
+
- `chrt.ahead({ point, bearing, arc, range | rangeNm, safetyDepth, layer, limit })` → `GET /v1/query/ahead`. `bearing` is degrees true (a COG/heading or a fixed input; out-of-range values normalise, so a heading that wraps past north is fine). `arc` is the **full** sector width centred on the bearing — `arc: 60` sweeps ±30°. `arc: 360` degenerates to a full circle, making `ahead` a strict generalisation of `query()`.
|
|
45
|
+
- **Facts that sort, never a verdict.** Each feature carries `distance_m`, `bearing_deg`, `least_depth_m` and `depth_clearance_m` (negative = shallower than your declared `safetyDepth`). There is **no severity score and no `safe` flag, deliberately**: a risk model is a navigation-grade claim, and CHRT is situational-awareness only. You sort; CHRT ranks nothing.
|
|
46
|
+
- **`safetyDepth` filters out only what is *known* to be deeper.** Features with no charted depth are always kept — a wreck with no sounded value is still a wreck. Fail-safe by construction.
|
|
47
|
+
- **An empty result is not an all-clear**, so every response carries a `coverage` block (`covered`, `covered_fraction`, and `cells` with real `edition` / `update_number` / `issue_date` per source cell) alongside inline `attribution`, `disclaimer` and `notice`. Zero features in covered water and zero features because CHRT has no cells there are different answers; `coverage` is what tells them apart. **Check `coverage.covered` before telling a user anything.**
|
|
48
|
+
- **No `speed` parameter, by design** — deriving range from SOG means deciding how many minutes of lookahead a vessel needs, which is a navigational judgment that belongs to the client. Recipe: `rangeNm = SOG_knots × lookahead_hours`, narrowing `arc` as speed rises; re-fan on a meaningful course change rather than a timer.
|
|
49
|
+
- Defaults to CHRT's significant-feature set (S-52 viewing group 24020 — wrecks, obstructions, rocks, plus the depth-carrying classes). Pass `layer` for a different set, or use `query()` for an unfiltered point-radius search.
|
|
50
|
+
- New types: `AheadOptions`, `AheadResponse`, `AheadFeatureProperties`, `AheadCoverageCell`.
|
|
51
|
+
- **README rewritten against the real surface — three artifacts in the published tarball disagreed.**
|
|
52
|
+
The API Reference had not been updated past 0.8.0, so `README.md`, `CHANGELOG.md` and
|
|
53
|
+
`dist/index.d.ts` shipped inside `@chrtco/sdk@0.9.2` contradicting each other:
|
|
54
|
+
- **`styleUrl()` is no longer documented as deprecated.** The README still said *"CHRT serves no
|
|
55
|
+
remote `style.json` — use `chrt.style()`"*, three minor versions after the served Style API
|
|
56
|
+
shipped (`GET tiles.chrt.co/v1/styles/{region}/style.json`, live and auth-gated in production).
|
|
57
|
+
This was the severity-1 case: `chrt.style()` is a **JavaScript function**, so native iOS/Android,
|
|
58
|
+
Swift, Kotlin and Python clients cannot call it — the served style URL is the only style path
|
|
59
|
+
they have, and the README told them it did not exist.
|
|
60
|
+
- **Every method is now marked JS-only or HTTP-backed**, with the HTTP method and path for each, so
|
|
61
|
+
non-JS clients can see what they can actually reach. New section: *Using CHRT without
|
|
62
|
+
JavaScript* — the two-call mint → served-style recipe in `curl`, documenting the **wire** shape
|
|
63
|
+
(`expires_at`, `build_tag` — snake_case; the SDK's `TileTokenResponse` camelCases them).
|
|
64
|
+
- **`interact`, `fetchStyle()`, `coverage.list()` and `ahead()` are documented for the first time.**
|
|
65
|
+
`interact`, `fetchStyle` and `coverage` were all present in `0.9.2`'s `.d.ts` but absent from its
|
|
66
|
+
README — shipped and invisible.
|
|
67
|
+
- **`tiles.groups()` stays deprecated** — unlike `styleUrl()` it is genuinely gone (`/v1/tiles/*`
|
|
68
|
+
is absent from the gateway route table; it fails with `ChrtError` 404). The old heading's error
|
|
69
|
+
was bundling a live method and a dead one under one "deprecated" label.
|
|
70
|
+
- **Quick Start no longer hard-codes a secret**, and no longer uses `chrt_live_…` — a key format
|
|
71
|
+
CHRT has never generated. Real keys are `chrt_{pk|sk}_{live|test}_{team}_{random}`. New *API
|
|
72
|
+
keys* section documents the real `pk`/`sk` distinction: they differ in whether the plaintext can
|
|
73
|
+
be re-revealed, **not in privilege**. `pk` is not a capability boundary, so the README says to
|
|
74
|
+
treat any key embedded in a client as public rather than implying the prefix protects it.
|
|
75
|
+
- **The non-SOLAS disclaimer is now in the README.** The SDK enforces the notice on every
|
|
76
|
+
`InspectResult`, but the most-read CHRT artifact — the npm landing page — carried no disclaimer
|
|
77
|
+
at all.
|
|
78
|
+
- `QueryOptions.radius` JSDoc corrected: the server default is **500 m**, not 1000. `ChrtConfig.apiKey`
|
|
79
|
+
JSDoc corrected to the real key format. (JSDoc only — no behaviour change; both ship in `.d.ts`.)
|
|
6
80
|
- **Symbol families (`symbolSet`)** — `chrt.style({ symbolSet })`, `styleUrl({ symbols })` and `fetchStyle({ symbols })` select the symbol artwork:
|
|
7
81
|
- **`s52`** (default) — now **spec-exact for ATONs and LIGHTS**: buoy/beacon icon selection is compiled from the S-52 Presentation Library lookup tables (COLOUR-list keyed — preferred-channel junction marks render correctly; no IALA region hardcoding), and light flares follow the LIGHTS05/06 conditional-symbology procedure (LIGHTS11/12/13 colour-specific flares, LIGHTS81/82 strip/floodlight, LITDEF11 default).
|
|
8
82
|
- **`s101`** — the official IHO S-101 Portrayal Catalogue ATON/LIGHTS artwork (vendored at a pinned commit, themed through the official S-101 day/dusk/night profiles). Load images via `loadS101SymbolImages` (`@chrtco/sdk/atlas/browser`).
|
|
9
83
|
- **`chrt`** — the CHRT house set: simplified colour-true silhouettes (tinted at runtime from the feature's COLOUR — green marks render green, red marks red), fixed-palette identity marks (cardinals, isolated danger, safe water), and capability badges (sound for fog signals, radar for racons). Load via `loadChrtSymbolImages`.
|
|
10
|
-
- Served Style API: `GET /v1/styles/{region}/style.json?symbols=s52|s101|chrt` (own cache-key slot, `X-Style-Symbols` response header)
|
|
84
|
+
- Served Style API: `GET /v1/styles/{region}/style.json?symbols=s52|s101|chrt|raster` (own cache-key slot, `X-Style-Symbols` response header). All four families are **free on every plan** — the style route reads no `plan_id`, and the served document is byte-identical across tiers. Composes with a bound **custom style**, which is the one part of CHRTstyle that is paid (Dev and above).
|
|
11
85
|
- **Behaviour change (S-52 correctness):** LIGHTS icons are no longer SDF-tinted by COLOUR; the procedure-selected flare carries its own colour. Blue/orange lights now render the spec's LITDEF11 default flare (use the `chrt` family for exact-colour flares).
|
|
12
86
|
|
|
13
87
|
## [0.9.1] — 2026-07-14
|
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@ Official TypeScript SDK for the [CHRT](https://chrt.co) nautical chart API.
|
|
|
4
4
|
|
|
5
5
|
Zero dependencies · Works in Node 18+, browsers, Deno, Bun · Full TypeScript types
|
|
6
6
|
|
|
7
|
+
> **Situational awareness only — not for primary navigation.** CHRT is not an ECDIS and is not
|
|
8
|
+
> SOLAS-compliant. Do not use it as your primary means of navigation. Every chart CHRT serves comes
|
|
9
|
+
> from the issuing hydrographic office; keep the source attribution visible in anything you build.
|
|
10
|
+
|
|
7
11
|
## Install
|
|
8
12
|
|
|
9
13
|
```bash
|
|
@@ -15,7 +19,8 @@ npm install @chrtco/sdk
|
|
|
15
19
|
```typescript
|
|
16
20
|
import { Chrt } from '@chrtco/sdk';
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
// Read the key from the environment — never hard-code it in source.
|
|
23
|
+
const chrt = new Chrt(process.env.CHRT_API_KEY!);
|
|
19
24
|
|
|
20
25
|
// Get depth areas in a bounding box
|
|
21
26
|
const features = await chrt.features({
|
|
@@ -30,25 +35,119 @@ const nearby = await chrt.query({
|
|
|
30
35
|
layer: 'BOYCAR,BOYLAT',
|
|
31
36
|
});
|
|
32
37
|
|
|
33
|
-
// List ENC cells
|
|
34
|
-
|
|
38
|
+
// List ENC cells — `source` takes the chart-source id ('NOAA_ENC'), not the
|
|
39
|
+
// agency name ('NOAA'), which matches nothing. chrt.coverage.list() lists the ids.
|
|
40
|
+
const cells = await chrt.cells.list({ source: 'NOAA_ENC' });
|
|
35
41
|
|
|
36
42
|
// Get a specific cell
|
|
37
43
|
const cell = await chrt.cells.get('US5MA11M');
|
|
44
|
+
console.log(cell.bbox_north, cell.bbox_west); // → 42.38 -71.05
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API keys
|
|
48
|
+
|
|
49
|
+
Keys are issued in the CHRT platform under **Settings → API keys** and look like:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
chrt_pk_live_<team>_<random> chrt_sk_live_<team>_<random>
|
|
53
|
+
chrt_pk_test_<team>_<random> chrt_sk_test_<team>_<random>
|
|
54
|
+
▲ ▲
|
|
55
|
+
│ └── environment: live | test
|
|
56
|
+
└── key type: pk (publishable) | sk (secret)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| | `pk` — publishable | `sk` — secret |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| Intended for | client-side code (browser, mobile app) | servers, CI, backend jobs |
|
|
62
|
+
| Retrievable later | ✓ — re-reveal it in the dashboard | ✗ — shown once at creation; regenerate if lost |
|
|
63
|
+
|
|
64
|
+
> **`pk` and `sk` carry identical API privileges.** The prefix records intent and whether CHRT keeps
|
|
65
|
+
> the plaintext for you to re-reveal — it is **not** a capability boundary. Any key you ship to a
|
|
66
|
+
> client can be extracted from it and replayed with full access.
|
|
67
|
+
>
|
|
68
|
+
> So treat any key embedded in a browser bundle or mobile app as public: develop against a
|
|
69
|
+
> `test`-environment key, grant it only the scopes it needs, and rotate it if it leaks.
|
|
70
|
+
>
|
|
71
|
+
> For a key you ship to a browser, set **Allowed domains** on it (platform → API keys). A browser
|
|
72
|
+
> request whose `Origin` isn't on the list is refused, so a key lifted from your bundle won't work
|
|
73
|
+
> on someone else's site. It is a **browser-only deterrent, never a boundary** — a non-browser
|
|
74
|
+
> caller sends no `Origin` and is allowed through, by design. Do **not** set the **IP allowlist**:
|
|
75
|
+
> it does not work, and setting it bricks the key on every real request path.
|
|
76
|
+
|
|
77
|
+
## Spatial queries
|
|
78
|
+
|
|
79
|
+
`query()` answers "what's around me". Underway, the question is narrower.
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// What's charted AHEAD — position + bearing, no route needed.
|
|
83
|
+
const fan = await chrt.ahead({
|
|
84
|
+
point: [-70.9245, 42.33604],
|
|
85
|
+
bearing: gps.cog, // degrees true
|
|
86
|
+
arc: 60, // FULL sector width, so ±30°
|
|
87
|
+
rangeNm: 3,
|
|
88
|
+
safetyDepth: 3, // omit if you set `vessel`
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// What's charted along a PLANNED PASSAGE — ordered by how far in you meet it.
|
|
92
|
+
const passage = await chrt.alongRoute({
|
|
93
|
+
route: [[-70.95, 42.33], [-71.0, 42.35], [-71.02, 42.36]],
|
|
94
|
+
bufferNm: 0.5,
|
|
95
|
+
});
|
|
96
|
+
for (const f of passage.features) {
|
|
97
|
+
const { along_m, distance_m, depth_clearance_m } = f.properties;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Which CHARTS that passage needs — a different question, so a different call.
|
|
101
|
+
const { cells } = await chrt.coverage.forRoute({ route, bufferNm: 1 });
|
|
38
102
|
```
|
|
39
103
|
|
|
104
|
+
Neither replaces the other: a boat with a route still drifts, and COG rarely equals the route
|
|
105
|
+
bearing — so a passage app wants `alongRoute()` once per route edit and `ahead()` continuously
|
|
106
|
+
against the real track.
|
|
107
|
+
|
|
108
|
+
**These tell you what is CHARTED, never whether it is safe.** There is no severity score and no
|
|
109
|
+
`safe` flag, deliberately — a risk model is a navigation-grade claim, and CHRT is
|
|
110
|
+
situational-awareness only. You get facts that sort (`distance_m`, `bearing_deg`, `along_m`,
|
|
111
|
+
`least_depth_m`, `depth_clearance_m` — negative means shallower than you declared safe) and your
|
|
112
|
+
application owns the judgment.
|
|
113
|
+
|
|
114
|
+
**An empty result is not an all-clear.** It can mean nothing is charted there, or that CHRT has no
|
|
115
|
+
coverage there at all. Every response carries a `coverage` block — check `coverage.covered` before
|
|
116
|
+
telling a user anything.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
if (!fan.coverage.covered) {
|
|
120
|
+
// CHRT holds no chart for this sector. Say so — do not report "clear".
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
There is no `speed` parameter, by design: turning SOG into a lookahead distance is a navigational
|
|
125
|
+
judgment that belongs to you. The usual recipe is `rangeNm = SOG_knots × lookahead_hours`,
|
|
126
|
+
narrowing `arc` as speed rises. Re-fan on a meaningful course change rather than on a timer.
|
|
127
|
+
|
|
40
128
|
## Maps (MapLibre GL JS / Mapbox GL JS)
|
|
41
129
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
130
|
+
CHRT gives you two ways to get an IHO **S-52** style. They are not interchangeable — pick by whether
|
|
131
|
+
your client runs JavaScript:
|
|
132
|
+
|
|
133
|
+
| | `chrt.style()` | `chrt.styleUrl()` / `chrt.fetchStyle()` |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| What it is | a JS function that **builds** the style locally | a **URL CHRT serves** (`tiles.chrt.co/v1/styles/…`) |
|
|
136
|
+
| Network | none — works offline | one authenticated request |
|
|
137
|
+
| Usable from | **JavaScript only** | any HTTP client (Swift, Kotlin, Python, curl) |
|
|
138
|
+
| Custom style bound to your key | not applied | **applied server-side** (Dev plan and above) |
|
|
139
|
+
|
|
140
|
+
`chrt.transformRequest()` mints a short-lived tile token from your API key and attaches it to tile
|
|
141
|
+
**and served-style** requests automatically. Examples use MapLibre — substitute `mapbox-gl` if you're
|
|
142
|
+
already on Mapbox.
|
|
45
143
|
|
|
46
144
|
```typescript
|
|
47
145
|
import maplibregl from 'maplibre-gl';
|
|
48
146
|
import { Chrt } from '@chrtco/sdk';
|
|
49
147
|
|
|
50
|
-
const chrt = new Chrt(
|
|
148
|
+
const chrt = new Chrt(process.env.CHRT_API_KEY!);
|
|
51
149
|
|
|
150
|
+
// Locally generated style — no network, no custom style applied.
|
|
52
151
|
const map = new maplibregl.Map({
|
|
53
152
|
container: 'map',
|
|
54
153
|
style: chrt.style({ palette: 'DAY' }), // 'DAY' | 'DUSK' | 'NIGHT'
|
|
@@ -58,14 +157,47 @@ const map = new maplibregl.Map({
|
|
|
58
157
|
});
|
|
59
158
|
```
|
|
60
159
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
160
|
+
Or use the **served** style, which honours the CHRTstyle custom style bound to your key:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
const map = new maplibregl.Map({
|
|
164
|
+
container: 'map',
|
|
165
|
+
style: await chrt.styleUrl({ theme: 'dusk' }), // 'day' | 'dusk' | 'night'
|
|
166
|
+
transformRequest: chrt.transformRequest(), // required — authenticates the style request too
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`styleUrl()` is `async`: it pre-mints the tile token so the style request — which MapLibre does
|
|
171
|
+
**not** retry — is guaranteed to carry auth. Always pair it with `transformRequest()`.
|
|
172
|
+
|
|
173
|
+
Tiles are served from `tiles.chrt.co` and gated by a short-lived ES256 JWT. The SDK mints it from
|
|
174
|
+
your key, attaches it as `Authorization: Bearer`, and refreshes it before expiry — you never handle
|
|
175
|
+
tokens directly.
|
|
176
|
+
|
|
177
|
+
### Symbol families
|
|
178
|
+
|
|
179
|
+
`style()`, `styleUrl()` and `fetchStyle()` all select symbol artwork, but **the option is spelled
|
|
180
|
+
differently** on the local and served paths:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
chrt.style({ symbolSet: 's101' }); // local — symbolSet, palette: 'DAY' | 'DUSK' | 'NIGHT'
|
|
184
|
+
await chrt.styleUrl({ symbols: 's101' }); // served — symbols, theme: 'day' | 'dusk' | 'night'
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
| Value | Artwork |
|
|
188
|
+
|---|---|
|
|
189
|
+
| `s52` (default) | IHO S-52 Presentation Library — spec-exact ATON/LIGHTS selection |
|
|
190
|
+
| `s101` | official IHO S-101 Portrayal Catalogue artwork |
|
|
191
|
+
| `chrt` | CHRT house set — colour-true silhouettes, capability badges |
|
|
192
|
+
| `raster` | raster symbol set |
|
|
193
|
+
|
|
194
|
+
`s101` and `chrt` need their images loaded at runtime via `loadS101SymbolImages` /
|
|
195
|
+
`loadChrtSymbolImages` from `@chrtco/sdk/atlas/browser`.
|
|
64
196
|
|
|
65
197
|
### Custom styles
|
|
66
198
|
|
|
67
|
-
`chrt.style()` returns a standard MapLibre style object you can mutate. For a fully custom
|
|
68
|
-
|
|
199
|
+
`chrt.style()` returns a standard MapLibre style object you can mutate. For a fully custom look,
|
|
200
|
+
build your own style over CHRT's vector source with `buildSources()` and keep
|
|
69
201
|
`chrt.transformRequest()` for auth:
|
|
70
202
|
|
|
71
203
|
```typescript
|
|
@@ -76,22 +208,146 @@ const sources = buildSources({ region: 'NOAA_ENC' });
|
|
|
76
208
|
// Each S-57 class (DEPARE, LIGHTS, BOYLAT, …) is a `source-layer` inside that one source.
|
|
77
209
|
```
|
|
78
210
|
|
|
79
|
-
|
|
211
|
+
## Using CHRT without JavaScript
|
|
212
|
+
|
|
213
|
+
Native iOS/Android, Swift, Kotlin, Python and plain `curl` clients cannot call `chrt.style()` — it is
|
|
214
|
+
a JavaScript function that builds a style object in-process. **Use the served style API instead.**
|
|
215
|
+
It is two HTTP calls, no SDK required.
|
|
216
|
+
|
|
217
|
+
**1. Mint a tile token** (valid for a short window; re-mint before `expires_at`):
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
curl -X POST https://api.chrt.co/v1/mint/tile-token \
|
|
221
|
+
-H "x-api-key: $CHRT_API_KEY" \
|
|
222
|
+
-H "content-type: application/json" \
|
|
223
|
+
-d '{"region":"NOAA_ENC"}'
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```jsonc
|
|
227
|
+
{
|
|
228
|
+
"token": "eyJhbGciOiJFUzI1NiIs…", // ES256 JWT — do not log
|
|
229
|
+
"expires_at": 1752600000, // unix seconds, matches the JWT `exp`
|
|
230
|
+
"kid": "chrt-tile-2026-04",
|
|
231
|
+
"build_tag": "…",
|
|
232
|
+
"region": "NOAA_ENC"
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**2. Fetch the style and the tiles** with that token as a Bearer credential:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
curl https://tiles.chrt.co/v1/styles/NOAA_ENC/style.json?theme=day \
|
|
240
|
+
-H "Authorization: Bearer $TOKEN"
|
|
241
|
+
|
|
242
|
+
curl https://tiles.chrt.co/v1/tiles/NOAA_ENC/12/1234/1543.mvt \
|
|
243
|
+
-H "Authorization: Bearer $TOKEN"
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The returned document is a standard MapLibre style — hand it to MapLibre Native (iOS/Android),
|
|
247
|
+
`maplibre-gl`, or any renderer that speaks the style spec. Point your renderer's request transformer
|
|
248
|
+
at the same `Authorization: Bearer` header so tile requests carry it too.
|
|
249
|
+
|
|
250
|
+
The REST endpoints in the table below (`/v1/features`, `/v1/query`, `/v1/query/ahead`, `/v1/cells`,
|
|
251
|
+
`/v1/coverage`) take the API key directly as `x-api-key` — no token minting needed.
|
|
252
|
+
|
|
253
|
+
## Click-to-inspect (CHRTinteract)
|
|
254
|
+
|
|
255
|
+
Wires click → decode onto any MapLibre / Mapbox GL map showing CHRT tiles. **Requires the Dev plan or
|
|
256
|
+
above**; the entitlement is enforced server-side at attach time.
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
const handle = await chrt.interact.attach(map, {
|
|
260
|
+
onInspect: (result) => {
|
|
261
|
+
// result.kind: 'navaid' | 'bridge' | 'area'
|
|
262
|
+
// e.g. a light characteristic decoded to "Fl(2+1) W 10s", sector arcs,
|
|
263
|
+
// bridge clearances, restriction lists, coordinates
|
|
264
|
+
console.log(result);
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
handle.detach();
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
With no `onInspect`/`onCandidates` callback, `attach()` renders a minimal built-in popup instead.
|
|
272
|
+
Every result carries a `notice` — the non-SOLAS disclaimer, issuing-HO attribution and data
|
|
273
|
+
currency. The built-in popup always renders them; **if you build your own UI, render them too.**
|
|
274
|
+
|
|
275
|
+
A chart click rarely hits exactly one feature (a buoy on a channel over a depth area), so interact
|
|
276
|
+
collects every candidate under the click, dedupes them across tile boundaries by S-57 identity, and
|
|
277
|
+
orders them point → line → area with safety-critical classes first.
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// Headless candidate collection for any screen point or [lng, lat].
|
|
281
|
+
const { candidates, select } = await chrt.interact.inspectAll(map, [-70.86, 42.02]);
|
|
282
|
+
const result = await select(candidates[0]);
|
|
283
|
+
|
|
284
|
+
// Decode a single feature you already have.
|
|
285
|
+
const one = await chrt.interact.inspect(feature);
|
|
286
|
+
|
|
287
|
+
// Resolve full S-57 source properties + cell provenance (edition, update number,
|
|
288
|
+
// issue date). Metered against your `featureCalls` quota.
|
|
289
|
+
const enriched = await chrt.interact.inspect(feature, { enrich: true });
|
|
290
|
+
```
|
|
80
291
|
|
|
81
292
|
## Configuration
|
|
82
293
|
|
|
83
294
|
```typescript
|
|
84
295
|
const chrt = new Chrt({
|
|
85
|
-
apiKey:
|
|
86
|
-
baseUrl: 'https://api.chrt.co/v1',
|
|
87
|
-
retries: 2, // retry on 5xx, default 2
|
|
296
|
+
apiKey: process.env.CHRT_API_KEY!,
|
|
297
|
+
baseUrl: 'https://api.chrt.co/v1', // default
|
|
298
|
+
retries: 2, // retry on 5xx + network errors, default 2
|
|
88
299
|
region: 'NOAA_ENC', // default tile region
|
|
89
300
|
tileBaseUrl: 'https://tiles.chrt.co', // override for staging tiles
|
|
301
|
+
deviceId: 'my-app-install-uuid', // required for offline.* — see below
|
|
302
|
+
manifestPath: './chrt-offline-manifest.json', // optional — enables offline budget + list()
|
|
303
|
+
vessel: { safetyDepthM: 3 }, // one safety depth for every depth-aware call
|
|
90
304
|
});
|
|
91
305
|
```
|
|
92
306
|
|
|
307
|
+
`new Chrt('chrt_sk_live_…')` is shorthand for `new Chrt({ apiKey: 'chrt_sk_live_…' })`.
|
|
308
|
+
|
|
309
|
+
### `vessel` — one safety depth
|
|
310
|
+
|
|
311
|
+
Set your draft-plus-margin once and every depth-aware call inherits it: `ahead()` and
|
|
312
|
+
`alongRoute()` filter at it, and `style()` shades soundings and draws danger rings at it. An
|
|
313
|
+
explicit per-call `safetyDepth` / `safetyContour` always wins.
|
|
314
|
+
|
|
315
|
+
It is **client-side and per-instance, deliberately — not bound to your API key**. A consumer app
|
|
316
|
+
has one key and many skippers with different boats; a key-scoped draft would give them all the
|
|
317
|
+
same number. Construct a `Chrt` per user, or pass `safetyDepth` per call.
|
|
318
|
+
|
|
319
|
+
**With no `vessel`, CHRT sends no depth at all** rather than guessing a draft — filtering on an
|
|
320
|
+
invented one would silently drop real hazards.
|
|
321
|
+
|
|
322
|
+
> Not yet reachable from the *served* style: `styleUrl()` takes `theme` + `symbols` only, so a
|
|
323
|
+
> non-JS client cannot currently get per-user depth shading.
|
|
324
|
+
|
|
93
325
|
## API Reference
|
|
94
326
|
|
|
327
|
+
**Which methods can a non-JS client use?** Everything under *HTTP endpoint* below is a plain REST
|
|
328
|
+
call any language can make. Everything marked **JS-only** runs in-process and has no HTTP equivalent.
|
|
329
|
+
|
|
330
|
+
| Method | HTTP endpoint | Notes |
|
|
331
|
+
|---|---|---|
|
|
332
|
+
| `chrt.features(opts)` | `GET /v1/features` | |
|
|
333
|
+
| `chrt.query(opts)` | `GET /v1/query` | |
|
|
334
|
+
| `chrt.ahead(opts)` | `GET /v1/query/ahead` | what's charted in a sector ahead |
|
|
335
|
+
| `chrt.alongRoute(opts)` | `GET /v1/query/route` | same, for a planned passage |
|
|
336
|
+
| `chrt.alongRouteIter(opts)` | `GET /v1/query/route` | auto-paginates |
|
|
337
|
+
| `chrt.cells.list(opts?)` | `GET /v1/cells` | |
|
|
338
|
+
| `chrt.cells.get(id)` | `GET /v1/cells/{id}` | |
|
|
339
|
+
| `chrt.coverage.list(opts?)` | `GET /v1/coverage` | |
|
|
340
|
+
| `chrt.coverage.forRoute(opts)` | `GET /v1/coverage/route` | which cells a passage needs |
|
|
341
|
+
| `chrt.warmTiles(region?)` | `POST /v1/mint/tile-token` | |
|
|
342
|
+
| `chrt.styleUrl(opts?)` | `GET {tileBaseUrl}/v1/styles/{region}/style.json` | mints first |
|
|
343
|
+
| `chrt.fetchStyle(opts?)` | same, then fetches it | |
|
|
344
|
+
| `chrt.interact.entitlement()` | `POST /v1/interact/entitlement` | |
|
|
345
|
+
| `chrt.style(opts?)` | — | **JS-only** — local generator |
|
|
346
|
+
| `chrt.transformRequest(opts?)` | — | **JS-only** — returns a closure |
|
|
347
|
+
| `chrt.tiles.tileUrl(z, x, y)` | — | **JS-only** — string builder |
|
|
348
|
+
| `chrt.interact.attach/inspectAll` | — | **JS-only** — needs a GL JS map |
|
|
349
|
+
| `chrt.featuresIter` / `cellsIter` | — | **JS-only** — async iterators |
|
|
350
|
+
|
|
95
351
|
### `chrt.features(options)`
|
|
96
352
|
Query GeoJSON features by bounding box.
|
|
97
353
|
|
|
@@ -99,8 +355,11 @@ Query GeoJSON features by bounding box.
|
|
|
99
355
|
|--------|------|----------|-------------|
|
|
100
356
|
| `bbox` | `[w, s, e, n]` | ✓ | Bounding box |
|
|
101
357
|
| `layer` | `string` | | S-57 layer code (e.g. `DEPARE`) |
|
|
102
|
-
| `limit` | `number` | | Max results (default 100) |
|
|
358
|
+
| `limit` | `number` | | Max results (default 100, max 1000) |
|
|
103
359
|
| `offset` | `number` | | Pagination offset |
|
|
360
|
+
| `signal` | `AbortSignal` | | |
|
|
361
|
+
|
|
362
|
+
Returns a GeoJSON `FeatureCollection` with `total_count`, `limit` and `offset` alongside `features`.
|
|
104
363
|
|
|
105
364
|
### `chrt.query(options)`
|
|
106
365
|
Spatial proximity search around a point.
|
|
@@ -108,37 +367,130 @@ Spatial proximity search around a point.
|
|
|
108
367
|
| Option | Type | Required | Description |
|
|
109
368
|
|--------|------|----------|-------------|
|
|
110
369
|
| `point` | `[lng, lat]` | ✓ | Centre point |
|
|
111
|
-
| `radius` | `number` | | Radius in metres (default
|
|
370
|
+
| `radius` | `number` | | Radius in metres (default 500, clamped 1–50 000) |
|
|
112
371
|
| `layer` | `string` | | Comma-separated layer codes |
|
|
113
|
-
| `limit` | `number` | | Max results |
|
|
372
|
+
| `limit` | `number` | | Max results (default 50, max 500) |
|
|
373
|
+
| `signal` | `AbortSignal` | | |
|
|
374
|
+
|
|
375
|
+
Returns a `QueryResponse`: a GeoJSON `FeatureCollection` whose `properties` carry `layer`, `cell_id`
|
|
376
|
+
and `distance_m` (geodesic metres from `point`), plus a `query` block echoing what the server
|
|
377
|
+
**actually applied after clamping** — read `res.query.radius_m` to see that `radius: 90000` silently
|
|
378
|
+
became `50000`. Unlike `chrt.features()`, this endpoint is **not paginated**: it returns no `offset`,
|
|
379
|
+
so widen `radius` rather than paging.
|
|
380
|
+
|
|
381
|
+
### `chrt.ahead(options)`
|
|
382
|
+
The fan-ahead query — the significant features in a sector from a point along a bearing. Built for
|
|
383
|
+
the position-plus-COG case: a craft with a heading but no planned route.
|
|
384
|
+
|
|
385
|
+
| Option | Type | Required | Description |
|
|
386
|
+
|--------|------|----------|-------------|
|
|
387
|
+
| `point` | `[lng, lat]` | ✓ | Origin |
|
|
388
|
+
| `bearing` | `number` | ✓ | Degrees true; out-of-range values normalise |
|
|
389
|
+
| `arc` | `number` | | **Full** sector width centred on `bearing` (default 90 — `arc: 60` sweeps ±30°) |
|
|
390
|
+
| `range` | `number` | | Metres (default 5000, clamped 1–50 000) |
|
|
391
|
+
| `rangeNm` | `number` | | Nautical miles — mutually exclusive with `range` (passing both throws) |
|
|
392
|
+
| `safetyDepth` | `number` | | Filters out only what is **known** to be deeper |
|
|
393
|
+
| `layer` | `string` | | Defaults to CHRT's significant-feature set |
|
|
394
|
+
| `limit` | `number` | | Default 50, max 500 |
|
|
395
|
+
|
|
396
|
+
Each feature carries `distance_m`, `bearing_deg`, `least_depth_m` and `depth_clearance_m` (negative =
|
|
397
|
+
shallower than your `safetyDepth`).
|
|
398
|
+
|
|
399
|
+
> **Facts that sort, never a verdict.** There is deliberately no severity score and no `safe` flag —
|
|
400
|
+
> a risk model is a navigation-grade claim. You sort; CHRT ranks nothing.
|
|
401
|
+
>
|
|
402
|
+
> **An empty result is not an all-clear.** Zero features in covered water and zero features because
|
|
403
|
+
> CHRT has no cells there are different answers. Every response carries a `coverage` block
|
|
404
|
+
> (`covered`, `covered_fraction`, `cells`) — **check `coverage.covered` before telling a user
|
|
405
|
+
> anything.**
|
|
114
406
|
|
|
115
407
|
### `chrt.cells.list(options?)`
|
|
116
408
|
List available ENC cells.
|
|
117
409
|
|
|
118
|
-
|
|
119
|
-
|
|
410
|
+
| Option | Type | Required | Description |
|
|
411
|
+
|--------|------|----------|-------------|
|
|
412
|
+
| `source` | `string` | | Chart-source **id**, e.g. `'NOAA_ENC'` — *not* the agency name `'NOAA'`, which matches nothing and returns an empty list rather than an error. `chrt.coverage.list()` enumerates the ids. |
|
|
413
|
+
| `status` | `string` | | e.g. `'CURRENT'`. Case-insensitive. |
|
|
414
|
+
| `scaleBand` | `ScaleBand` | | `'Overview'` \| `'General'` \| `'Coastal'` \| `'Approach'` \| `'Harbor'`. Matched **case-sensitively** — `'harbor'` matches nothing. |
|
|
415
|
+
| `bbox` | `[w, s, e, n]` | | Cells whose bounds *intersect* this box |
|
|
416
|
+
| `limit` | `number` | | Max results (default 100, max 1000) |
|
|
417
|
+
| `offset` | `number` | | Pagination offset |
|
|
418
|
+
| `signal` | `AbortSignal` | | |
|
|
120
419
|
|
|
121
|
-
|
|
122
|
-
Returns a ready-to-use MapLibre/Mapbox GL **S-52** style object. Options: `palette`
|
|
123
|
-
(`'DAY'` | `'DUSK'` | `'NIGHT'`), `region`, `depthUnits`, plus the other atlas style options.
|
|
420
|
+
Returns `{ cells, total_count, limit, offset }`. Use `chrt.cellsIter(options?)` to page automatically.
|
|
124
421
|
|
|
125
|
-
### `chrt.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
422
|
+
### `chrt.cells.get(cellId)`
|
|
423
|
+
Get metadata for a specific cell. Takes the id positionally: `chrt.cells.get('US5MA11M')`.
|
|
424
|
+
Returns a `CellDetail`:
|
|
425
|
+
|
|
426
|
+
| Field | Type | Description |
|
|
427
|
+
|-------|------|-------------|
|
|
428
|
+
| `cell_id` | `string` | e.g. `'US5MA11M'` |
|
|
429
|
+
| `title` | `string \| null` | Chart title |
|
|
430
|
+
| `chart_source` | `string` | Source id, e.g. `'NOAA_ENC'` |
|
|
431
|
+
| `status` | `string` | e.g. `'CURRENT'` |
|
|
432
|
+
| `scale` | `number \| null` | Compilation scale denominator |
|
|
433
|
+
| `scale_band` | `string \| null` | Usage band — a `ScaleBand` name, e.g. `'Harbor'` |
|
|
434
|
+
| `edition` · `update_number` · `issue_date` | | **Data currency** — the fields that say how current the chart is |
|
|
435
|
+
| `bbox_north` · `bbox_south` · `bbox_east` · `bbox_west` | `number \| null` | Bounds in decimal degrees (WGS 84), `null` if the issuing office recorded none. Written as a group, so in practice all four are set or all four are `null`. |
|
|
436
|
+
| `last_checked_at` | `string \| null` | ISO 8601 — when CHRT last checked the issuing office's catalogue for this cell. Freshness of the *check*, not of the chart; use `edition` / `update_number` / `issue_date` for chart currency. |
|
|
437
|
+
|
|
438
|
+
> **Bounds are flat here and on `coverage.forRoute()` — one shape across the API.** Up to
|
|
439
|
+
> 0.9.2 the SDK declared these flat fields while `/v1/cells` alone returned a nested `bbox`
|
|
440
|
+
> object, so `cell.bbox_north` typechecked and read `undefined` on every response. The
|
|
441
|
+
> endpoint was the outlier; it now sends what this type has always declared, and reading
|
|
442
|
+
> `cell.bbox_north` works against any deployed CHRT API.
|
|
443
|
+
|
|
444
|
+
### `chrt.coverage.list(options?)`
|
|
445
|
+
List the chart sources available to your key. Options: `country` (e.g. `'US'`), `source`.
|
|
446
|
+
Returns `{ sources, total_cells }`, where each source carries `source` (the **id** you filter cells
|
|
447
|
+
by, e.g. `'NOAA_ENC'`), `agency` (the issuing office's display name, e.g. `'NOAA'`), `cell_count` and
|
|
448
|
+
`scale_bands` (counts keyed by band name). This is the endpoint that tells you which `source` ids are
|
|
449
|
+
valid — passing an agency name anywhere else silently matches nothing.
|
|
450
|
+
|
|
451
|
+
### `chrt.style(opts?)` — **JS-only**
|
|
452
|
+
Returns a ready-to-use MapLibre/Mapbox GL **S-52** style object. Generated locally: no network, works
|
|
453
|
+
offline, and **does not apply a custom style bound to your key**. Options: `palette`
|
|
454
|
+
(`'DAY' | 'DUSK' | 'NIGHT'`), `symbolSet`, `region`, `depthUnits`, `safetyContour`, `chartMode`, plus
|
|
455
|
+
the other atlas style options. Non-JS clients want `styleUrl()` instead.
|
|
456
|
+
|
|
457
|
+
### `chrt.styleUrl(opts?)`
|
|
458
|
+
URL of the **served** S-52 style — the CHRTstyle Style API at
|
|
459
|
+
`{tileBaseUrl}/v1/styles/{region}/style.json`. `async`: it pre-mints the tile token so the style
|
|
460
|
+
request (which MapLibre does not retry) carries auth. Options: `theme`
|
|
461
|
+
(`'day' | 'dusk' | 'night'`), `symbols`. Always pair with `transformRequest()`.
|
|
462
|
+
|
|
463
|
+
The served document honours the CHRTstyle custom style bound to your key (a **Dev+** feature; Free
|
|
464
|
+
keys and keys with no bound style get the default themes). This is the style path for any client that
|
|
465
|
+
can't run `chrt.style()`.
|
|
466
|
+
|
|
467
|
+
### `chrt.fetchStyle(opts?)`
|
|
468
|
+
Fetches the served style as a MapLibre style object. Same options as `styleUrl()`, plus `signal`.
|
|
469
|
+
Prefer it over `styleUrl()` when you want to inspect or mutate the document, or when you can't use
|
|
470
|
+
`transformRequest()` for the style request itself. Throws `ChrtError` on failure; does not retry.
|
|
471
|
+
|
|
472
|
+
### `chrt.transformRequest(opts?)` — **JS-only**
|
|
473
|
+
Returns a `transformRequest` closure that attaches the tile JWT as `Authorization: Bearer` on
|
|
474
|
+
`tiles.chrt.co` **tile and served-style** requests only. Sprite, glyph and third-party URLs pass
|
|
475
|
+
through untouched. Pass `{ region }` to override the default region.
|
|
129
476
|
|
|
130
477
|
### `chrt.warmTiles(region?)`
|
|
131
|
-
Pre-mints the tile token so you can surface auth errors (
|
|
132
|
-
`tile_quota_exceeded
|
|
133
|
-
|
|
478
|
+
Pre-mints the tile token so you can surface auth errors (`region_not_licensed`,
|
|
479
|
+
`tile_quota_exceeded`, …) before the map loads. Optional — `transformRequest()` warms the cache on
|
|
480
|
+
its own.
|
|
481
|
+
|
|
482
|
+
### `chrt.tiles.tileUrl(z, x, y)` — **JS-only**
|
|
483
|
+
Concrete vector tile URL at `tiles.chrt.co` for one tile. Prefer `chrt.style()` / `chrt.styleUrl()`,
|
|
484
|
+
which wire the source for you.
|
|
134
485
|
|
|
135
|
-
### `chrt.
|
|
136
|
-
|
|
137
|
-
|
|
486
|
+
### `chrt.interact.*`
|
|
487
|
+
See [Click-to-inspect](#click-to-inspect-chrtinteract). `attach(map, opts?)` and
|
|
488
|
+
`inspectAll(map, point, opts?)` need a GL JS map instance; `inspect(feature, opts?)` decodes locally
|
|
489
|
+
but checks entitlement over HTTP first.
|
|
138
490
|
|
|
139
|
-
### `chrt.
|
|
140
|
-
|
|
141
|
-
|
|
491
|
+
### `chrt.tiles.groups()` — deprecated
|
|
492
|
+
Tilesets were consolidated to one source per region (2026-04). The route no longer exists — calling
|
|
493
|
+
it fails with `ChrtError` 404. Use `chrt.style()` or `chrt.styleUrl()`.
|
|
142
494
|
|
|
143
495
|
## Error Handling
|
|
144
496
|
|
|
@@ -154,10 +506,21 @@ try {
|
|
|
154
506
|
}
|
|
155
507
|
```
|
|
156
508
|
|
|
509
|
+
Typed errors by area: `ChrtError` (REST), `TileTokenError` (minting — `invalid_api_key`,
|
|
510
|
+
`region_not_licensed`, `tile_quota_exceeded`, `region_not_available`, `feature_flag_disabled`,
|
|
511
|
+
`network_error`, `unexpected_response`), `InteractError` (`interact_not_enabled` — includes the
|
|
512
|
+
current `plan` and an upgrade hint — `invalid_api_key`, `missing_scope`, `enrich_failed`,
|
|
513
|
+
`unknown_layer`, `network_error`, `unexpected_response`), and `OfflineError` (below).
|
|
514
|
+
|
|
157
515
|
## Offline Downloads (CHRToffline)
|
|
158
516
|
|
|
159
517
|
Download MBTiles bundles for offline rendering. Bundles are SHA-256 verified before commit. Interrupted downloads resume from a `.partial` sidecar file.
|
|
160
518
|
|
|
519
|
+
**Requires the Pro plan or above**, enforced server-side at mint time — Free and Dev keys get
|
|
520
|
+
`OfflineError` code `offline_requires_pro_plan`. The key also needs the `offline:download`
|
|
521
|
+
scope, which is checked **before** the plan: a key without it gets `offline_not_enabled`
|
|
522
|
+
regardless of tier, so add the scope first or the plan refusal never surfaces.
|
|
523
|
+
|
|
161
524
|
**Runtime support in v1:** Node 18+, Bun, Deno. Browser and React Native are deferred to a follow-up.
|
|
162
525
|
|
|
163
526
|
```typescript
|
|
@@ -211,7 +574,8 @@ try {
|
|
|
211
574
|
case 'download_corrupt': console.log('SHA mismatch'); break;
|
|
212
575
|
case 'network_interrupted': console.log('resume later'); break;
|
|
213
576
|
case 'coverage_denied': console.log('region not licensed'); break;
|
|
214
|
-
case '
|
|
577
|
+
case 'offline_requires_pro_plan': console.log('CHRToffline needs Pro+ — chrt.co/pricing'); break;
|
|
578
|
+
case 'offline_not_enabled': console.log('add the offline:download scope to this API key'); break;
|
|
215
579
|
}
|
|
216
580
|
}
|
|
217
581
|
}
|