@chrtco/sdk 0.9.2 → 0.10.1
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 +126 -1
- package/README.md +406 -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 +9919 -778
- 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 +9898 -780
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 });
|
|
102
|
+
```
|
|
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
|
+
}
|
|
38
122
|
```
|
|
39
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,150 @@ 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
|
+
> **Prefer codegen?** The whole HTTP surface is described by an OpenAPI 3.1 spec at
|
|
214
|
+
> **https://api.chrt.co/openapi.json** — generate a typed Swift/Kotlin/Python/Go client
|
|
215
|
+
> from it instead of hand-rolling the calls below.
|
|
216
|
+
|
|
217
|
+
Native iOS/Android, Swift, Kotlin, Python and plain `curl` clients cannot call `chrt.style()` — it is
|
|
218
|
+
a JavaScript function that builds a style object in-process. **Use the served style API instead.**
|
|
219
|
+
It is two HTTP calls, no SDK required.
|
|
220
|
+
|
|
221
|
+
**1. Mint a tile token** (valid for a short window; re-mint before `expires_at`):
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
curl -X POST https://api.chrt.co/v1/mint/tile-token \
|
|
225
|
+
-H "x-api-key: $CHRT_API_KEY" \
|
|
226
|
+
-H "content-type: application/json" \
|
|
227
|
+
-d '{"region":"NOAA_ENC"}'
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
```jsonc
|
|
231
|
+
{
|
|
232
|
+
"token": "eyJhbGciOiJFUzI1NiIs…", // ES256 JWT — do not log
|
|
233
|
+
"expires_at": 1752600000, // unix seconds, matches the JWT `exp`
|
|
234
|
+
"kid": "chrt-tile-2026-04",
|
|
235
|
+
"build_tag": "…",
|
|
236
|
+
"region": "NOAA_ENC"
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**2. Fetch the style and the tiles** with that token as a Bearer credential:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
curl https://tiles.chrt.co/v1/styles/NOAA_ENC/style.json?theme=day \
|
|
244
|
+
-H "Authorization: Bearer $TOKEN"
|
|
245
|
+
|
|
246
|
+
curl https://tiles.chrt.co/v1/tiles/NOAA_ENC/12/1234/1543.mvt \
|
|
247
|
+
-H "Authorization: Bearer $TOKEN"
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The returned document is a standard MapLibre style — hand it to MapLibre Native (iOS/Android),
|
|
251
|
+
`maplibre-gl`, or any renderer that speaks the style spec. Point your renderer's request transformer
|
|
252
|
+
at the same `Authorization: Bearer` header so tile requests carry it too.
|
|
253
|
+
|
|
254
|
+
The REST endpoints in the table below (`/v1/features`, `/v1/query`, `/v1/query/ahead`, `/v1/cells`,
|
|
255
|
+
`/v1/coverage`) take the API key directly as `x-api-key` — no token minting needed.
|
|
256
|
+
|
|
257
|
+
## Click-to-inspect (CHRTinteract)
|
|
258
|
+
|
|
259
|
+
Wires click → decode onto any MapLibre / Mapbox GL map showing CHRT tiles. **Requires the Dev plan or
|
|
260
|
+
above**; the entitlement is enforced server-side at attach time.
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
const handle = await chrt.interact.attach(map, {
|
|
264
|
+
onInspect: (result) => {
|
|
265
|
+
// result.kind: 'navaid' | 'bridge' | 'area'
|
|
266
|
+
// e.g. a light characteristic decoded to "Fl(2+1) W 10s", sector arcs,
|
|
267
|
+
// bridge clearances, restriction lists, coordinates
|
|
268
|
+
console.log(result);
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
handle.detach();
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
With no `onInspect`/`onCandidates` callback, `attach()` renders a minimal built-in popup instead.
|
|
276
|
+
Every result carries a `notice` — the non-SOLAS disclaimer, issuing-HO attribution and data
|
|
277
|
+
currency. The built-in popup always renders them; **if you build your own UI, render them too.**
|
|
278
|
+
|
|
279
|
+
A chart click rarely hits exactly one feature (a buoy on a channel over a depth area), so interact
|
|
280
|
+
collects every candidate under the click, dedupes them across tile boundaries by S-57 identity, and
|
|
281
|
+
orders them point → line → area with safety-critical classes first.
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
// Headless candidate collection for any screen point or [lng, lat].
|
|
285
|
+
const { candidates, select } = await chrt.interact.inspectAll(map, [-70.86, 42.02]);
|
|
286
|
+
const result = await select(candidates[0]);
|
|
287
|
+
|
|
288
|
+
// Decode a single feature you already have.
|
|
289
|
+
const one = await chrt.interact.inspect(feature);
|
|
290
|
+
|
|
291
|
+
// Resolve full S-57 source properties + cell provenance (edition, update number,
|
|
292
|
+
// issue date). Metered against your `featureCalls` quota.
|
|
293
|
+
const enriched = await chrt.interact.inspect(feature, { enrich: true });
|
|
294
|
+
```
|
|
80
295
|
|
|
81
296
|
## Configuration
|
|
82
297
|
|
|
83
298
|
```typescript
|
|
84
299
|
const chrt = new Chrt({
|
|
85
|
-
apiKey:
|
|
86
|
-
baseUrl: 'https://api.chrt.co/v1',
|
|
87
|
-
retries: 2, // retry on 5xx, default 2
|
|
300
|
+
apiKey: process.env.CHRT_API_KEY!,
|
|
301
|
+
baseUrl: 'https://api.chrt.co/v1', // default
|
|
302
|
+
retries: 2, // retry on 5xx + network errors, default 2
|
|
88
303
|
region: 'NOAA_ENC', // default tile region
|
|
89
304
|
tileBaseUrl: 'https://tiles.chrt.co', // override for staging tiles
|
|
305
|
+
deviceId: 'my-app-install-uuid', // required for offline.* — see below
|
|
306
|
+
manifestPath: './chrt-offline-manifest.json', // optional — enables offline budget + list()
|
|
307
|
+
vessel: { safetyDepthM: 3 }, // one safety depth for every depth-aware call
|
|
90
308
|
});
|
|
91
309
|
```
|
|
92
310
|
|
|
311
|
+
`new Chrt('chrt_sk_live_…')` is shorthand for `new Chrt({ apiKey: 'chrt_sk_live_…' })`.
|
|
312
|
+
|
|
313
|
+
### `vessel` — one safety depth
|
|
314
|
+
|
|
315
|
+
Set your draft-plus-margin once and every depth-aware call inherits it: `ahead()` and
|
|
316
|
+
`alongRoute()` filter at it, and `style()` shades soundings and draws danger rings at it. An
|
|
317
|
+
explicit per-call `safetyDepth` / `safetyContour` always wins.
|
|
318
|
+
|
|
319
|
+
It is **client-side and per-instance, deliberately — not bound to your API key**. A consumer app
|
|
320
|
+
has one key and many skippers with different boats; a key-scoped draft would give them all the
|
|
321
|
+
same number. Construct a `Chrt` per user, or pass `safetyDepth` per call.
|
|
322
|
+
|
|
323
|
+
**With no `vessel`, CHRT sends no depth at all** rather than guessing a draft — filtering on an
|
|
324
|
+
invented one would silently drop real hazards.
|
|
325
|
+
|
|
326
|
+
> Not yet reachable from the *served* style: `styleUrl()` takes `theme` + `symbols` only, so a
|
|
327
|
+
> non-JS client cannot currently get per-user depth shading.
|
|
328
|
+
|
|
93
329
|
## API Reference
|
|
94
330
|
|
|
331
|
+
**Which methods can a non-JS client use?** Everything under *HTTP endpoint* below is a plain REST
|
|
332
|
+
call any language can make. Everything marked **JS-only** runs in-process and has no HTTP equivalent.
|
|
333
|
+
|
|
334
|
+
| Method | HTTP endpoint | Notes |
|
|
335
|
+
|---|---|---|
|
|
336
|
+
| `chrt.features(opts)` | `GET /v1/features` | |
|
|
337
|
+
| `chrt.query(opts)` | `GET /v1/query` | |
|
|
338
|
+
| `chrt.ahead(opts)` | `GET /v1/query/ahead` | what's charted in a sector ahead |
|
|
339
|
+
| `chrt.alongRoute(opts)` | `GET /v1/query/route` | same, for a planned passage |
|
|
340
|
+
| `chrt.alongRouteIter(opts)` | `GET /v1/query/route` | auto-paginates |
|
|
341
|
+
| `chrt.cells.list(opts?)` | `GET /v1/cells` | |
|
|
342
|
+
| `chrt.cells.get(id)` | `GET /v1/cells/{id}` | |
|
|
343
|
+
| `chrt.coverage.list(opts?)` | `GET /v1/coverage` | |
|
|
344
|
+
| `chrt.coverage.forRoute(opts)` | `GET /v1/coverage/route` | which cells a passage needs |
|
|
345
|
+
| `chrt.warmTiles(region?)` | `POST /v1/mint/tile-token` | |
|
|
346
|
+
| `chrt.styleUrl(opts?)` | `GET {tileBaseUrl}/v1/styles/{region}/style.json` | mints first |
|
|
347
|
+
| `chrt.fetchStyle(opts?)` | same, then fetches it | |
|
|
348
|
+
| `chrt.interact.entitlement()` | `POST /v1/interact/entitlement` | |
|
|
349
|
+
| `chrt.style(opts?)` | — | **JS-only** — local generator |
|
|
350
|
+
| `chrt.transformRequest(opts?)` | — | **JS-only** — returns a closure |
|
|
351
|
+
| `chrt.tiles.tileUrl(z, x, y)` | — | **JS-only** — string builder |
|
|
352
|
+
| `chrt.interact.attach/inspectAll` | — | **JS-only** — needs a GL JS map |
|
|
353
|
+
| `chrt.featuresIter` / `cellsIter` | — | **JS-only** — async iterators |
|
|
354
|
+
|
|
95
355
|
### `chrt.features(options)`
|
|
96
356
|
Query GeoJSON features by bounding box.
|
|
97
357
|
|
|
@@ -99,8 +359,11 @@ Query GeoJSON features by bounding box.
|
|
|
99
359
|
|--------|------|----------|-------------|
|
|
100
360
|
| `bbox` | `[w, s, e, n]` | ✓ | Bounding box |
|
|
101
361
|
| `layer` | `string` | | S-57 layer code (e.g. `DEPARE`) |
|
|
102
|
-
| `limit` | `number` | | Max results (default 100) |
|
|
362
|
+
| `limit` | `number` | | Max results (default 100, max 1000) |
|
|
103
363
|
| `offset` | `number` | | Pagination offset |
|
|
364
|
+
| `signal` | `AbortSignal` | | |
|
|
365
|
+
|
|
366
|
+
Returns a GeoJSON `FeatureCollection` with `total_count`, `limit` and `offset` alongside `features`.
|
|
104
367
|
|
|
105
368
|
### `chrt.query(options)`
|
|
106
369
|
Spatial proximity search around a point.
|
|
@@ -108,37 +371,130 @@ Spatial proximity search around a point.
|
|
|
108
371
|
| Option | Type | Required | Description |
|
|
109
372
|
|--------|------|----------|-------------|
|
|
110
373
|
| `point` | `[lng, lat]` | ✓ | Centre point |
|
|
111
|
-
| `radius` | `number` | | Radius in metres (default
|
|
374
|
+
| `radius` | `number` | | Radius in metres (default 500, clamped 1–50 000) |
|
|
112
375
|
| `layer` | `string` | | Comma-separated layer codes |
|
|
113
|
-
| `limit` | `number` | | Max results |
|
|
376
|
+
| `limit` | `number` | | Max results (default 50, max 500) |
|
|
377
|
+
| `signal` | `AbortSignal` | | |
|
|
378
|
+
|
|
379
|
+
Returns a `QueryResponse`: a GeoJSON `FeatureCollection` whose `properties` carry `layer`, `cell_id`
|
|
380
|
+
and `distance_m` (geodesic metres from `point`), plus a `query` block echoing what the server
|
|
381
|
+
**actually applied after clamping** — read `res.query.radius_m` to see that `radius: 90000` silently
|
|
382
|
+
became `50000`. Unlike `chrt.features()`, this endpoint is **not paginated**: it returns no `offset`,
|
|
383
|
+
so widen `radius` rather than paging.
|
|
384
|
+
|
|
385
|
+
### `chrt.ahead(options)`
|
|
386
|
+
The fan-ahead query — the significant features in a sector from a point along a bearing. Built for
|
|
387
|
+
the position-plus-COG case: a craft with a heading but no planned route.
|
|
388
|
+
|
|
389
|
+
| Option | Type | Required | Description |
|
|
390
|
+
|--------|------|----------|-------------|
|
|
391
|
+
| `point` | `[lng, lat]` | ✓ | Origin |
|
|
392
|
+
| `bearing` | `number` | ✓ | Degrees true; out-of-range values normalise |
|
|
393
|
+
| `arc` | `number` | | **Full** sector width centred on `bearing` (default 90 — `arc: 60` sweeps ±30°) |
|
|
394
|
+
| `range` | `number` | | Metres (default 5000, clamped 1–50 000) |
|
|
395
|
+
| `rangeNm` | `number` | | Nautical miles — mutually exclusive with `range` (passing both throws) |
|
|
396
|
+
| `safetyDepth` | `number` | | Filters out only what is **known** to be deeper |
|
|
397
|
+
| `layer` | `string` | | Defaults to CHRT's significant-feature set |
|
|
398
|
+
| `limit` | `number` | | Default 50, max 500 |
|
|
399
|
+
|
|
400
|
+
Each feature carries `distance_m`, `bearing_deg`, `least_depth_m` and `depth_clearance_m` (negative =
|
|
401
|
+
shallower than your `safetyDepth`).
|
|
402
|
+
|
|
403
|
+
> **Facts that sort, never a verdict.** There is deliberately no severity score and no `safe` flag —
|
|
404
|
+
> a risk model is a navigation-grade claim. You sort; CHRT ranks nothing.
|
|
405
|
+
>
|
|
406
|
+
> **An empty result is not an all-clear.** Zero features in covered water and zero features because
|
|
407
|
+
> CHRT has no cells there are different answers. Every response carries a `coverage` block
|
|
408
|
+
> (`covered`, `covered_fraction`, `cells`) — **check `coverage.covered` before telling a user
|
|
409
|
+
> anything.**
|
|
114
410
|
|
|
115
411
|
### `chrt.cells.list(options?)`
|
|
116
412
|
List available ENC cells.
|
|
117
413
|
|
|
118
|
-
|
|
119
|
-
|
|
414
|
+
| Option | Type | Required | Description |
|
|
415
|
+
|--------|------|----------|-------------|
|
|
416
|
+
| `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. |
|
|
417
|
+
| `status` | `string` | | e.g. `'CURRENT'`. Case-insensitive. |
|
|
418
|
+
| `scaleBand` | `ScaleBand` | | `'Overview'` \| `'General'` \| `'Coastal'` \| `'Approach'` \| `'Harbor'`. Matched **case-sensitively** — `'harbor'` matches nothing. |
|
|
419
|
+
| `bbox` | `[w, s, e, n]` | | Cells whose bounds *intersect* this box |
|
|
420
|
+
| `limit` | `number` | | Max results (default 100, max 1000) |
|
|
421
|
+
| `offset` | `number` | | Pagination offset |
|
|
422
|
+
| `signal` | `AbortSignal` | | |
|
|
120
423
|
|
|
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.
|
|
424
|
+
Returns `{ cells, total_count, limit, offset }`. Use `chrt.cellsIter(options?)` to page automatically.
|
|
124
425
|
|
|
125
|
-
### `chrt.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
426
|
+
### `chrt.cells.get(cellId)`
|
|
427
|
+
Get metadata for a specific cell. Takes the id positionally: `chrt.cells.get('US5MA11M')`.
|
|
428
|
+
Returns a `CellDetail`:
|
|
429
|
+
|
|
430
|
+
| Field | Type | Description |
|
|
431
|
+
|-------|------|-------------|
|
|
432
|
+
| `cell_id` | `string` | e.g. `'US5MA11M'` |
|
|
433
|
+
| `title` | `string \| null` | Chart title |
|
|
434
|
+
| `chart_source` | `string` | Source id, e.g. `'NOAA_ENC'` |
|
|
435
|
+
| `status` | `string` | e.g. `'CURRENT'` |
|
|
436
|
+
| `scale` | `number \| null` | Compilation scale denominator |
|
|
437
|
+
| `scale_band` | `string \| null` | Usage band — a `ScaleBand` name, e.g. `'Harbor'` |
|
|
438
|
+
| `edition` · `update_number` · `issue_date` | | **Data currency** — the fields that say how current the chart is |
|
|
439
|
+
| `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`. |
|
|
440
|
+
| `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. |
|
|
441
|
+
|
|
442
|
+
> **Bounds are flat here and on `coverage.forRoute()` — one shape across the API.** Up to
|
|
443
|
+
> 0.9.2 the SDK declared these flat fields while `/v1/cells` alone returned a nested `bbox`
|
|
444
|
+
> object, so `cell.bbox_north` typechecked and read `undefined` on every response. The
|
|
445
|
+
> endpoint was the outlier; it now sends what this type has always declared, and reading
|
|
446
|
+
> `cell.bbox_north` works against any deployed CHRT API.
|
|
447
|
+
|
|
448
|
+
### `chrt.coverage.list(options?)`
|
|
449
|
+
List the chart sources available to your key. Options: `country` (e.g. `'US'`), `source`.
|
|
450
|
+
Returns `{ sources, total_cells }`, where each source carries `source` (the **id** you filter cells
|
|
451
|
+
by, e.g. `'NOAA_ENC'`), `agency` (the issuing office's display name, e.g. `'NOAA'`), `cell_count` and
|
|
452
|
+
`scale_bands` (counts keyed by band name). This is the endpoint that tells you which `source` ids are
|
|
453
|
+
valid — passing an agency name anywhere else silently matches nothing.
|
|
454
|
+
|
|
455
|
+
### `chrt.style(opts?)` — **JS-only**
|
|
456
|
+
Returns a ready-to-use MapLibre/Mapbox GL **S-52** style object. Generated locally: no network, works
|
|
457
|
+
offline, and **does not apply a custom style bound to your key**. Options: `palette`
|
|
458
|
+
(`'DAY' | 'DUSK' | 'NIGHT'`), `symbolSet`, `region`, `depthUnits`, `safetyContour`, `chartMode`, plus
|
|
459
|
+
the other atlas style options. Non-JS clients want `styleUrl()` instead.
|
|
460
|
+
|
|
461
|
+
### `chrt.styleUrl(opts?)`
|
|
462
|
+
URL of the **served** S-52 style — the CHRTstyle Style API at
|
|
463
|
+
`{tileBaseUrl}/v1/styles/{region}/style.json`. `async`: it pre-mints the tile token so the style
|
|
464
|
+
request (which MapLibre does not retry) carries auth. Options: `theme`
|
|
465
|
+
(`'day' | 'dusk' | 'night'`), `symbols`. Always pair with `transformRequest()`.
|
|
466
|
+
|
|
467
|
+
The served document honours the CHRTstyle custom style bound to your key (a **Dev+** feature; Free
|
|
468
|
+
keys and keys with no bound style get the default themes). This is the style path for any client that
|
|
469
|
+
can't run `chrt.style()`.
|
|
470
|
+
|
|
471
|
+
### `chrt.fetchStyle(opts?)`
|
|
472
|
+
Fetches the served style as a MapLibre style object. Same options as `styleUrl()`, plus `signal`.
|
|
473
|
+
Prefer it over `styleUrl()` when you want to inspect or mutate the document, or when you can't use
|
|
474
|
+
`transformRequest()` for the style request itself. Throws `ChrtError` on failure; does not retry.
|
|
475
|
+
|
|
476
|
+
### `chrt.transformRequest(opts?)` — **JS-only**
|
|
477
|
+
Returns a `transformRequest` closure that attaches the tile JWT as `Authorization: Bearer` on
|
|
478
|
+
`tiles.chrt.co` **tile and served-style** requests only. Sprite, glyph and third-party URLs pass
|
|
479
|
+
through untouched. Pass `{ region }` to override the default region.
|
|
129
480
|
|
|
130
481
|
### `chrt.warmTiles(region?)`
|
|
131
|
-
Pre-mints the tile token so you can surface auth errors (
|
|
132
|
-
`tile_quota_exceeded
|
|
133
|
-
|
|
482
|
+
Pre-mints the tile token so you can surface auth errors (`region_not_licensed`,
|
|
483
|
+
`tile_quota_exceeded`, …) before the map loads. Optional — `transformRequest()` warms the cache on
|
|
484
|
+
its own.
|
|
485
|
+
|
|
486
|
+
### `chrt.tiles.tileUrl(z, x, y)` — **JS-only**
|
|
487
|
+
Concrete vector tile URL at `tiles.chrt.co` for one tile. Prefer `chrt.style()` / `chrt.styleUrl()`,
|
|
488
|
+
which wire the source for you.
|
|
134
489
|
|
|
135
|
-
### `chrt.
|
|
136
|
-
|
|
137
|
-
|
|
490
|
+
### `chrt.interact.*`
|
|
491
|
+
See [Click-to-inspect](#click-to-inspect-chrtinteract). `attach(map, opts?)` and
|
|
492
|
+
`inspectAll(map, point, opts?)` need a GL JS map instance; `inspect(feature, opts?)` decodes locally
|
|
493
|
+
but checks entitlement over HTTP first.
|
|
138
494
|
|
|
139
|
-
### `chrt.
|
|
140
|
-
|
|
141
|
-
|
|
495
|
+
### `chrt.tiles.groups()` — deprecated
|
|
496
|
+
Tilesets were consolidated to one source per region (2026-04). The route no longer exists — calling
|
|
497
|
+
it fails with `ChrtError` 404. Use `chrt.style()` or `chrt.styleUrl()`.
|
|
142
498
|
|
|
143
499
|
## Error Handling
|
|
144
500
|
|
|
@@ -154,10 +510,21 @@ try {
|
|
|
154
510
|
}
|
|
155
511
|
```
|
|
156
512
|
|
|
513
|
+
Typed errors by area: `ChrtError` (REST), `TileTokenError` (minting — `invalid_api_key`,
|
|
514
|
+
`region_not_licensed`, `tile_quota_exceeded`, `region_not_available`, `feature_flag_disabled`,
|
|
515
|
+
`network_error`, `unexpected_response`), `InteractError` (`interact_not_enabled` — includes the
|
|
516
|
+
current `plan` and an upgrade hint — `invalid_api_key`, `missing_scope`, `enrich_failed`,
|
|
517
|
+
`unknown_layer`, `network_error`, `unexpected_response`), and `OfflineError` (below).
|
|
518
|
+
|
|
157
519
|
## Offline Downloads (CHRToffline)
|
|
158
520
|
|
|
159
521
|
Download MBTiles bundles for offline rendering. Bundles are SHA-256 verified before commit. Interrupted downloads resume from a `.partial` sidecar file.
|
|
160
522
|
|
|
523
|
+
**Requires the Pro plan or above**, enforced server-side at mint time — Free and Dev keys get
|
|
524
|
+
`OfflineError` code `offline_requires_pro_plan`. The key also needs the `offline:download`
|
|
525
|
+
scope, which is checked **before** the plan: a key without it gets `offline_not_enabled`
|
|
526
|
+
regardless of tier, so add the scope first or the plan refusal never surfaces.
|
|
527
|
+
|
|
161
528
|
**Runtime support in v1:** Node 18+, Bun, Deno. Browser and React Native are deferred to a follow-up.
|
|
162
529
|
|
|
163
530
|
```typescript
|
|
@@ -211,7 +578,8 @@ try {
|
|
|
211
578
|
case 'download_corrupt': console.log('SHA mismatch'); break;
|
|
212
579
|
case 'network_interrupted': console.log('resume later'); break;
|
|
213
580
|
case 'coverage_denied': console.log('region not licensed'); break;
|
|
214
|
-
case '
|
|
581
|
+
case 'offline_requires_pro_plan': console.log('CHRToffline needs Pro+ — chrt.co/pricing'); break;
|
|
582
|
+
case 'offline_not_enabled': console.log('add the offline:download scope to this API key'); break;
|
|
215
583
|
}
|
|
216
584
|
}
|
|
217
585
|
}
|