@chrtco/sdk 0.7.7 → 0.8.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 +10 -0
- package/README.md +49 -16
- package/dist/index.cjs +337 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +289 -117
- package/dist/index.d.ts +289 -117
- package/dist/index.js +334 -66
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@chrtco/sdk` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.8.0] — 2026-06-30
|
|
6
|
+
- **Online tile rendering now works end-to-end against `tiles.chrt.co`.** `chrt.transformRequest()` mints a short-lived ES256 tile JWT from your API key (`POST {baseUrl}/mint/tile-token`), caches and refreshes it, and attaches `Authorization: Bearer` on `tiles.chrt.co/v1/tiles/{region}/…/.mvt` requests only. This replaces the previous no-op transformer that injected `x-api-key` against a removed `api.chrt.co/v1/tiles/…?key=` path.
|
|
7
|
+
- **New `chrt.style(opts?)`** — returns a ready-to-use IHO **S-52** MapLibre / Mapbox GL style object (`palette`: `DAY` | `DUSK` | `NIGHT`, plus `region`, `depthUnits`, and the other atlas options), with the tile source wired in. Mutate the returned object, or build a fully custom style over `buildSources()`. S-101 portrayal is on the roadmap.
|
|
8
|
+
- **New `chrt.warmTiles(region?)`** — optionally pre-mint the tile token to surface `region_not_licensed` / `tile_quota_exceeded` before the map loads.
|
|
9
|
+
- **New config options**: `region` (default `NOAA_ENC`) and `tileBaseUrl` (default `https://tiles.chrt.co`).
|
|
10
|
+
- **New exports**: `TileAuth`, `TileTokenError`, and the `TileTokenResponse` / `TileTokenErrorCode` / `ChrtStyleOptions` types.
|
|
11
|
+
- **Deprecated**: `styleUrl()` — CHRT serves no remote `style.json`; it now throws with guidance to use `style()`. `tiles.groups()` — legacy pre-consolidation per-group catalogue. `tiles.tileUrl()` now returns the real `tiles.chrt.co` URL.
|
|
12
|
+
- `TransformRequest.headers` is now optional (matches MapLibre; the transformer omits headers on unauthenticated passthrough).
|
|
13
|
+
- **Requires** the public `POST /v1/mint/tile-token` route on `api.chrt.co` (the sibling of the existing `/v1/mint/offline-token`).
|
|
14
|
+
|
|
5
15
|
## [0.7.0] — 2026-05-08
|
|
6
16
|
- **Atlas — dominance cascade & scale-band rendering**: per-feature dominance cascade in continuous mode, cascading band fallback, band-then-class layer emission, chart-mode per-cell cascade fallback to coarser bands, dominance filter on orientation-cross-band labels.
|
|
7
17
|
- **Atlas — cartographic labels**: orientation labels + typography hierarchy.
|
package/README.md
CHANGED
|
@@ -37,30 +37,56 @@ const cells = await chrt.cells.list({ source: 'NOAA' });
|
|
|
37
37
|
const cell = await chrt.cells.get('US5MA11M');
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
##
|
|
40
|
+
## Maps (MapLibre GL JS / Mapbox GL JS)
|
|
41
|
+
|
|
42
|
+
`chrt.style()` returns a ready-to-use IHO **S-52** nautical style; `chrt.transformRequest()`
|
|
43
|
+
mints a short-lived tile token from your API key and attaches it to tile requests
|
|
44
|
+
automatically. Examples use MapLibre — substitute `mapbox-gl` if you're already on Mapbox.
|
|
41
45
|
|
|
42
46
|
```typescript
|
|
43
|
-
import
|
|
47
|
+
import maplibregl from 'maplibre-gl';
|
|
44
48
|
import { Chrt } from '@chrtco/sdk';
|
|
45
49
|
|
|
46
50
|
const chrt = new Chrt('chrt_live_...');
|
|
47
51
|
|
|
48
|
-
const map = new
|
|
52
|
+
const map = new maplibregl.Map({
|
|
49
53
|
container: 'map',
|
|
50
|
-
style: chrt.
|
|
54
|
+
style: chrt.style({ palette: 'DAY' }), // 'DAY' | 'DUSK' | 'NIGHT'
|
|
51
55
|
center: [-70.25, 43.66],
|
|
52
56
|
zoom: 12,
|
|
53
57
|
transformRequest: chrt.transformRequest(),
|
|
54
58
|
});
|
|
55
59
|
```
|
|
56
60
|
|
|
61
|
+
Tiles are served from `tiles.chrt.co` and gated by a short-lived ES256 JWT. The SDK mints
|
|
62
|
+
it from your key, attaches it as `Authorization: Bearer` on tile requests only, and refreshes
|
|
63
|
+
it before expiry — you never handle tokens directly.
|
|
64
|
+
|
|
65
|
+
### Custom styles
|
|
66
|
+
|
|
67
|
+
`chrt.style()` returns a standard MapLibre style object you can mutate. For a fully custom
|
|
68
|
+
look, build your own style over CHRT's vector source with `buildSources()` and keep
|
|
69
|
+
`chrt.transformRequest()` for auth:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { buildSources } from '@chrtco/sdk';
|
|
73
|
+
|
|
74
|
+
const sources = buildSources({ region: 'NOAA_ENC' });
|
|
75
|
+
// sources['NOAA_ENC'].tiles → ['https://tiles.chrt.co/v1/tiles/NOAA_ENC/{z}/{x}/{y}.mvt']
|
|
76
|
+
// Each S-57 class (DEPARE, LIGHTS, BOYLAT, …) is a `source-layer` inside that one source.
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> S-101 portrayal is on the roadmap; today `chrt.style()` emits S-52.
|
|
80
|
+
|
|
57
81
|
## Configuration
|
|
58
82
|
|
|
59
83
|
```typescript
|
|
60
84
|
const chrt = new Chrt({
|
|
61
85
|
apiKey: 'chrt_live_...',
|
|
62
|
-
baseUrl: 'https://api.chrt.co/v1',
|
|
63
|
-
retries: 2,
|
|
86
|
+
baseUrl: 'https://api.chrt.co/v1', // default
|
|
87
|
+
retries: 2, // retry on 5xx, default 2
|
|
88
|
+
region: 'NOAA_ENC', // default tile region
|
|
89
|
+
tileBaseUrl: 'https://tiles.chrt.co', // override for staging tiles
|
|
64
90
|
});
|
|
65
91
|
```
|
|
66
92
|
|
|
@@ -92,20 +118,27 @@ List available ENC cells.
|
|
|
92
118
|
### `chrt.cells.get(cellId)`
|
|
93
119
|
Get metadata for a specific cell.
|
|
94
120
|
|
|
95
|
-
### `chrt.
|
|
96
|
-
Returns a Mapbox
|
|
121
|
+
### `chrt.style(opts?)`
|
|
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.
|
|
97
124
|
|
|
98
|
-
### `chrt.
|
|
99
|
-
Returns a
|
|
125
|
+
### `chrt.transformRequest(opts?)`
|
|
126
|
+
Returns a `transformRequest` that mints a short-lived tile JWT from your API key and attaches
|
|
127
|
+
it as `Authorization: Bearer` on `tiles.chrt.co` requests only. Pass `{ region }` to override
|
|
128
|
+
the default region.
|
|
100
129
|
|
|
101
|
-
### `chrt.
|
|
102
|
-
|
|
130
|
+
### `chrt.warmTiles(region?)`
|
|
131
|
+
Pre-mints the tile token so you can surface auth errors (e.g. `region_not_licensed`,
|
|
132
|
+
`tile_quota_exceeded`) before the map loads. Optional — `transformRequest()` warms the cache
|
|
133
|
+
on its own.
|
|
103
134
|
|
|
104
|
-
### `chrt.
|
|
105
|
-
|
|
135
|
+
### `chrt.tiles.tileUrl(z, x, y)`
|
|
136
|
+
Single vector tile URL template at `tiles.chrt.co`. Prefer `chrt.style()`, which wires the
|
|
137
|
+
source for you.
|
|
106
138
|
|
|
107
|
-
### `chrt.
|
|
108
|
-
|
|
139
|
+
### `chrt.styleUrl()` · `chrt.tiles.groups()` — deprecated
|
|
140
|
+
CHRT serves no remote `style.json` — use `chrt.style()`. Tilesets were consolidated to one
|
|
141
|
+
source per region (2026-04), so the per-group catalogue is legacy.
|
|
109
142
|
|
|
110
143
|
## Error Handling
|
|
111
144
|
|
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ __export(index_exports, {
|
|
|
31
31
|
ChrtError: () => ChrtError,
|
|
32
32
|
DEFAULT_REGION: () => DEFAULT_REGION,
|
|
33
33
|
DEFAULT_TILE_BASE_URL: () => DEFAULT_TILE_BASE_URL,
|
|
34
|
+
DEFAULT_TILE_REGION: () => DEFAULT_TILE_REGION,
|
|
34
35
|
LABELED_LAYERS: () => LABELED_LAYERS,
|
|
35
36
|
LABEL_BEHAVIOR: () => LABEL_BEHAVIOR,
|
|
36
37
|
NAUTICAL_SYMBOLS: () => NAUTICAL_SYMBOLS,
|
|
@@ -48,6 +49,8 @@ __export(index_exports, {
|
|
|
48
49
|
S52_NIGHT: () => S52_NIGHT,
|
|
49
50
|
SYMBOL_GROUPS: () => SYMBOL_GROUPS,
|
|
50
51
|
SYMBOL_MAP: () => SYMBOL_MAP,
|
|
52
|
+
TileAuth: () => TileAuth,
|
|
53
|
+
TileTokenError: () => TileTokenError,
|
|
51
54
|
applyColorTheme: () => applyColorTheme,
|
|
52
55
|
bandFilterExpression: () => bandFilterExpression,
|
|
53
56
|
buildLabelLayer: () => buildLabelLayer,
|
|
@@ -226,20 +229,243 @@ function sleep(ms) {
|
|
|
226
229
|
return new Promise((r) => setTimeout(r, ms));
|
|
227
230
|
}
|
|
228
231
|
|
|
232
|
+
// src/tile-auth.ts
|
|
233
|
+
var DEFAULT_TILE_REGION = "NOAA_ENC";
|
|
234
|
+
var DEFAULT_TILE_BASE_URL = "https://tiles.chrt.co";
|
|
235
|
+
var TileTokenError = class extends Error {
|
|
236
|
+
code;
|
|
237
|
+
status;
|
|
238
|
+
constructor(code, message, status) {
|
|
239
|
+
super(message);
|
|
240
|
+
this.name = "TileTokenError";
|
|
241
|
+
this.code = code;
|
|
242
|
+
this.status = status;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
var TILE_URL_PATH = /\/v1\/tiles\/[A-Za-z0-9_]+(?:\/[a-z][a-z0-9-]*)?\/\d+\/\d+\/\d+\.mvt(?:\?.*)?$/i;
|
|
246
|
+
var REFRESH_MARGIN_SECONDS = 30;
|
|
247
|
+
var STALE_GRACE_SECONDS = 1;
|
|
248
|
+
var MINT_RETRY_DELAYS_MS = [500, 1e3, 2e3];
|
|
249
|
+
function nowSeconds() {
|
|
250
|
+
return Math.floor(Date.now() / 1e3);
|
|
251
|
+
}
|
|
252
|
+
function sleep2(ms) {
|
|
253
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
254
|
+
}
|
|
255
|
+
function mapErrorCode(raw, status) {
|
|
256
|
+
const known = [
|
|
257
|
+
"invalid_api_key",
|
|
258
|
+
"feature_flag_disabled",
|
|
259
|
+
"region_not_licensed",
|
|
260
|
+
"tile_quota_exceeded",
|
|
261
|
+
"region_not_available"
|
|
262
|
+
];
|
|
263
|
+
if (typeof raw === "string" && known.includes(raw)) {
|
|
264
|
+
return raw;
|
|
265
|
+
}
|
|
266
|
+
if (status === 401) return "invalid_api_key";
|
|
267
|
+
if (status === 403) return "feature_flag_disabled";
|
|
268
|
+
if (status === 429) return "tile_quota_exceeded";
|
|
269
|
+
if (status === 503) return "region_not_available";
|
|
270
|
+
return "unexpected_response";
|
|
271
|
+
}
|
|
272
|
+
var TileAuth = class {
|
|
273
|
+
constructor(http, opts) {
|
|
274
|
+
this.http = http;
|
|
275
|
+
this.defaultRegion = opts?.region ?? DEFAULT_TILE_REGION;
|
|
276
|
+
this.tileBaseUrl = (opts?.tileBaseUrl ?? DEFAULT_TILE_BASE_URL).replace(/\/+$/, "");
|
|
277
|
+
}
|
|
278
|
+
cache = /* @__PURE__ */ new Map();
|
|
279
|
+
pending = /* @__PURE__ */ new Map();
|
|
280
|
+
defaultRegion;
|
|
281
|
+
tileBaseUrl;
|
|
282
|
+
lifecycleRegistered = false;
|
|
283
|
+
/** Mint once via the HTTP client (POST `/mint/tile-token`, `x-api-key`). */
|
|
284
|
+
async mintOnce(region) {
|
|
285
|
+
let res;
|
|
286
|
+
try {
|
|
287
|
+
res = await this.http.postJson("/mint/tile-token", { region });
|
|
288
|
+
} catch (err) {
|
|
289
|
+
throw new TileTokenError(
|
|
290
|
+
"network_error",
|
|
291
|
+
err instanceof Error ? err.message : "mint request failed"
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
if (!res.ok) {
|
|
295
|
+
const code = mapErrorCode(res.body?.error, res.status);
|
|
296
|
+
const msg = typeof res.body?.error === "string" ? res.body.error : `HTTP ${res.status}`;
|
|
297
|
+
throw new TileTokenError(code, msg, res.status);
|
|
298
|
+
}
|
|
299
|
+
const b = res.body;
|
|
300
|
+
if (!b || typeof b.token !== "string" || typeof b.expires_at !== "number" || typeof b.kid !== "string" || typeof b.build_tag !== "string" || typeof b.region !== "string") {
|
|
301
|
+
throw new TileTokenError(
|
|
302
|
+
"unexpected_response",
|
|
303
|
+
"mint-tile-token returned a malformed body",
|
|
304
|
+
res.status
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
token: b.token,
|
|
309
|
+
expiresAt: b.expires_at,
|
|
310
|
+
kid: b.kid,
|
|
311
|
+
buildTag: b.build_tag,
|
|
312
|
+
region: b.region
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/** Mint with exponential backoff on network errors only. */
|
|
316
|
+
async mintWithRetry(region) {
|
|
317
|
+
let lastErr;
|
|
318
|
+
for (let attempt = 0; attempt <= MINT_RETRY_DELAYS_MS.length; attempt++) {
|
|
319
|
+
try {
|
|
320
|
+
return await this.mintOnce(region);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
lastErr = err;
|
|
323
|
+
if (err instanceof TileTokenError && err.code !== "network_error") throw err;
|
|
324
|
+
if (attempt === MINT_RETRY_DELAYS_MS.length) break;
|
|
325
|
+
await sleep2(MINT_RETRY_DELAYS_MS[attempt]);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
throw lastErr instanceof Error ? lastErr : new TileTokenError("network_error", "mint failed");
|
|
329
|
+
}
|
|
330
|
+
scheduleRefresh(region, expiresAt) {
|
|
331
|
+
const delaySeconds = expiresAt - nowSeconds() - REFRESH_MARGIN_SECONDS;
|
|
332
|
+
if (delaySeconds <= 0) return null;
|
|
333
|
+
const timer = setTimeout(() => {
|
|
334
|
+
void this.refreshInBackground(region);
|
|
335
|
+
}, delaySeconds * 1e3);
|
|
336
|
+
timer.unref?.();
|
|
337
|
+
return timer;
|
|
338
|
+
}
|
|
339
|
+
async refreshInBackground(region) {
|
|
340
|
+
try {
|
|
341
|
+
const response = await this.mintWithRetry(region);
|
|
342
|
+
this.store(region, response);
|
|
343
|
+
} catch {
|
|
344
|
+
this.invalidate(region);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
store(region, response) {
|
|
348
|
+
const existing = this.cache.get(region);
|
|
349
|
+
if (existing?.refreshTimer) clearTimeout(existing.refreshTimer);
|
|
350
|
+
this.cache.set(region, {
|
|
351
|
+
response,
|
|
352
|
+
refreshTimer: this.scheduleRefresh(region, response.expiresAt)
|
|
353
|
+
});
|
|
354
|
+
this.registerLifecycleHandlers();
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Fetch (or reuse a cached) tile token for `region`. Concurrent callers for
|
|
358
|
+
* the same region await the same in-flight mint.
|
|
359
|
+
*/
|
|
360
|
+
async getToken(region) {
|
|
361
|
+
const r = region ?? this.defaultRegion;
|
|
362
|
+
const cached = this.cache.get(r);
|
|
363
|
+
if (cached && cached.response.expiresAt - nowSeconds() > REFRESH_MARGIN_SECONDS) {
|
|
364
|
+
return cached.response;
|
|
365
|
+
}
|
|
366
|
+
const inflight = this.pending.get(r);
|
|
367
|
+
if (inflight) return inflight;
|
|
368
|
+
const promise = (async () => {
|
|
369
|
+
try {
|
|
370
|
+
const response = await this.mintWithRetry(r);
|
|
371
|
+
this.store(r, response);
|
|
372
|
+
return response;
|
|
373
|
+
} finally {
|
|
374
|
+
this.pending.delete(r);
|
|
375
|
+
}
|
|
376
|
+
})();
|
|
377
|
+
this.pending.set(r, promise);
|
|
378
|
+
return promise;
|
|
379
|
+
}
|
|
380
|
+
/** Synchronous cache peek — null if absent or within the stale grace window. */
|
|
381
|
+
peek(region) {
|
|
382
|
+
const entry = this.cache.get(region ?? this.defaultRegion);
|
|
383
|
+
if (!entry) return null;
|
|
384
|
+
if (entry.response.expiresAt - nowSeconds() <= STALE_GRACE_SECONDS) return null;
|
|
385
|
+
return entry.response.token;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Produce a MapLibre / Mapbox GL `transformRequest`. Attaches
|
|
389
|
+
* `Authorization: Bearer <jwt>` only to CHRT tile URLs; style, sprite, and
|
|
390
|
+
* glyph URLs pass through untouched (never leak the token to third-party
|
|
391
|
+
* CDNs). Kicks off a background mint immediately so the cache warms within
|
|
392
|
+
* one round-trip; the first tile or two may go unauthenticated and be
|
|
393
|
+
* retried by the renderer.
|
|
394
|
+
*/
|
|
395
|
+
transformRequest(region) {
|
|
396
|
+
const r = region ?? this.defaultRegion;
|
|
397
|
+
void this.getToken(r).catch(() => void 0);
|
|
398
|
+
return (url) => {
|
|
399
|
+
if (!url.startsWith(this.tileBaseUrl) || !TILE_URL_PATH.test(url)) return void 0;
|
|
400
|
+
const token = this.peek(r);
|
|
401
|
+
if (!token) return { url };
|
|
402
|
+
return { url, headers: { Authorization: `Bearer ${token}` } };
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/** Clear the cached token + timer for a region. */
|
|
406
|
+
invalidate(region) {
|
|
407
|
+
const r = region ?? this.defaultRegion;
|
|
408
|
+
const entry = this.cache.get(r);
|
|
409
|
+
if (entry?.refreshTimer) clearTimeout(entry.refreshTimer);
|
|
410
|
+
this.cache.delete(r);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Browser-only defense against `setTimeout` freezing while a tab is
|
|
414
|
+
* suspended (laptop sleep, mobile background, bfcache restore): proactively
|
|
415
|
+
* re-mint near-expiry entries on wake. Inert under SSR / Node / Deno.
|
|
416
|
+
*/
|
|
417
|
+
registerLifecycleHandlers() {
|
|
418
|
+
if (this.lifecycleRegistered) return;
|
|
419
|
+
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
420
|
+
const onWake = () => {
|
|
421
|
+
if (document.visibilityState !== "visible") return;
|
|
422
|
+
for (const [region, entry] of this.cache.entries()) {
|
|
423
|
+
if (entry.response.expiresAt - nowSeconds() >= REFRESH_MARGIN_SECONDS) continue;
|
|
424
|
+
void this.refreshInBackground(region);
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
document.addEventListener("visibilitychange", onWake);
|
|
428
|
+
window.addEventListener("pageshow", (e) => {
|
|
429
|
+
if (e.persisted) onWake();
|
|
430
|
+
});
|
|
431
|
+
this.lifecycleRegistered = true;
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
229
435
|
// src/endpoints/tiles.ts
|
|
230
436
|
var TilesEndpoint = class {
|
|
231
|
-
constructor(http) {
|
|
437
|
+
constructor(http, opts) {
|
|
232
438
|
this.http = http;
|
|
439
|
+
this.region = opts?.region ?? DEFAULT_TILE_REGION;
|
|
440
|
+
this.tileBaseUrl = (opts?.tileBaseUrl ?? DEFAULT_TILE_BASE_URL).replace(/\/+$/, "");
|
|
233
441
|
}
|
|
234
|
-
|
|
442
|
+
region;
|
|
443
|
+
tileBaseUrl;
|
|
444
|
+
/**
|
|
445
|
+
* @deprecated CHRT has no remote `style.json` endpoint — the S-52 style is
|
|
446
|
+
* generated client-side. Use `chrt.style(opts)` to get a ready-to-use
|
|
447
|
+
* MapLibre style object instead. Calling this throws to prevent silently
|
|
448
|
+
* pointing a map at a dead URL.
|
|
449
|
+
*/
|
|
235
450
|
styleUrl() {
|
|
236
|
-
|
|
451
|
+
throw new Error(
|
|
452
|
+
"chrt.tiles.styleUrl() is no longer supported: CHRT serves no remote style.json. Use chrt.style(opts) to generate an S-52 MapLibre style object, and chrt.transformRequest() for tile auth. See https://chrt.co/docs/vector-tiles"
|
|
453
|
+
);
|
|
237
454
|
}
|
|
238
|
-
/**
|
|
239
|
-
|
|
240
|
-
|
|
455
|
+
/**
|
|
456
|
+
* Vector tile URL template for the configured region, served by the
|
|
457
|
+
* tile-worker at `tiles.chrt.co`. Tiles are JWT-gated — pair this with
|
|
458
|
+
* `chrt.transformRequest()`, which attaches the `Authorization: Bearer`
|
|
459
|
+
* header. Prefer `chrt.style()`, which wires the source for you.
|
|
460
|
+
*/
|
|
461
|
+
tileUrl(z, x, y) {
|
|
462
|
+
return `${this.tileBaseUrl}/v1/tiles/${this.region}/${z}/${x}/${y}.mvt`;
|
|
241
463
|
}
|
|
242
|
-
/**
|
|
464
|
+
/**
|
|
465
|
+
* @deprecated Tilesets were consolidated to one PMTiles per region (2026-04);
|
|
466
|
+
* the per-group catalogue endpoint is legacy and no longer served. Render via
|
|
467
|
+
* `chrt.style()` instead, which emits a single vector source per region.
|
|
468
|
+
*/
|
|
243
469
|
async groups() {
|
|
244
470
|
return this.http.request("/tiles/groups");
|
|
245
471
|
}
|
|
@@ -431,7 +657,7 @@ function mapServerError(body, status) {
|
|
|
431
657
|
}
|
|
432
658
|
|
|
433
659
|
// src/endpoints/offline-token-manager.ts
|
|
434
|
-
var
|
|
660
|
+
var REFRESH_MARGIN_SECONDS2 = 30;
|
|
435
661
|
function now() {
|
|
436
662
|
return Math.floor(Date.now() / 1e3);
|
|
437
663
|
}
|
|
@@ -452,7 +678,7 @@ var OfflineTokenManager = class {
|
|
|
452
678
|
}
|
|
453
679
|
const key = this.cacheKey(deviceId, regions);
|
|
454
680
|
const cached = this.cache.get(key);
|
|
455
|
-
if (cached && cached.expiresAt - now() >
|
|
681
|
+
if (cached && cached.expiresAt - now() > REFRESH_MARGIN_SECONDS2) {
|
|
456
682
|
return cached;
|
|
457
683
|
}
|
|
458
684
|
const inflight = this.pending.get(key);
|
|
@@ -1074,47 +1300,6 @@ function dirname(p) {
|
|
|
1074
1300
|
return p.slice(0, k);
|
|
1075
1301
|
}
|
|
1076
1302
|
|
|
1077
|
-
// src/pagination.ts
|
|
1078
|
-
async function* paginate(fetchPage, getItems, getTotalCount, options) {
|
|
1079
|
-
const pageSize = options?.pageSize ?? 100;
|
|
1080
|
-
const signal = options?.signal;
|
|
1081
|
-
let offset = 0;
|
|
1082
|
-
while (true) {
|
|
1083
|
-
if (signal?.aborted) return;
|
|
1084
|
-
const response = await fetchPage(offset, signal);
|
|
1085
|
-
const items = getItems(response);
|
|
1086
|
-
for (const item of items) {
|
|
1087
|
-
yield item;
|
|
1088
|
-
}
|
|
1089
|
-
if (items.length === 0) return;
|
|
1090
|
-
offset += items.length;
|
|
1091
|
-
const total = getTotalCount(response);
|
|
1092
|
-
if (total > 0 && offset >= total) return;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
// src/webhooks.ts
|
|
1097
|
-
var VALID_TYPES = /* @__PURE__ */ new Set(["cell.updated", "cell.new", "cell.deleted"]);
|
|
1098
|
-
function parseWebhook(body) {
|
|
1099
|
-
if (typeof body !== "object" || body === null) {
|
|
1100
|
-
throw new Error("Invalid webhook payload: expected an object");
|
|
1101
|
-
}
|
|
1102
|
-
const obj = body;
|
|
1103
|
-
if (typeof obj.id !== "string") {
|
|
1104
|
-
throw new Error("Invalid webhook payload: missing 'id'");
|
|
1105
|
-
}
|
|
1106
|
-
if (typeof obj.timestamp !== "string") {
|
|
1107
|
-
throw new Error("Invalid webhook payload: missing 'timestamp'");
|
|
1108
|
-
}
|
|
1109
|
-
if (typeof obj.type !== "string" || !VALID_TYPES.has(obj.type)) {
|
|
1110
|
-
throw new Error(`Invalid webhook payload: unknown type '${String(obj.type)}'`);
|
|
1111
|
-
}
|
|
1112
|
-
if (typeof obj.data !== "object" || obj.data === null) {
|
|
1113
|
-
throw new Error("Invalid webhook payload: missing 'data'");
|
|
1114
|
-
}
|
|
1115
|
-
return obj;
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
1303
|
// src/atlas/s52-colors.ts
|
|
1119
1304
|
var S52_DAY = {
|
|
1120
1305
|
// Depth
|
|
@@ -9479,7 +9664,7 @@ var BUILD_MINZOOM = 3;
|
|
|
9479
9664
|
var BUILD_MAXZOOM = 14;
|
|
9480
9665
|
|
|
9481
9666
|
// src/atlas/source-registry.ts
|
|
9482
|
-
var
|
|
9667
|
+
var DEFAULT_TILE_BASE_URL2 = "https://tiles.chrt.co";
|
|
9483
9668
|
var DEFAULT_REGION = "NOAA_ENC";
|
|
9484
9669
|
function buildSources(opts = {}) {
|
|
9485
9670
|
const mode = opts.mode ?? "pmtiles";
|
|
@@ -9492,7 +9677,7 @@ function buildSources(opts = {}) {
|
|
|
9492
9677
|
}
|
|
9493
9678
|
};
|
|
9494
9679
|
}
|
|
9495
|
-
const base = (opts.tileBaseUrl ??
|
|
9680
|
+
const base = (opts.tileBaseUrl ?? DEFAULT_TILE_BASE_URL2).replace(/\/+$/, "");
|
|
9496
9681
|
return {
|
|
9497
9682
|
[region]: {
|
|
9498
9683
|
type: "vector",
|
|
@@ -11219,6 +11404,47 @@ function getStyleStats(style) {
|
|
|
11219
11404
|
};
|
|
11220
11405
|
}
|
|
11221
11406
|
|
|
11407
|
+
// src/pagination.ts
|
|
11408
|
+
async function* paginate(fetchPage, getItems, getTotalCount, options) {
|
|
11409
|
+
const pageSize = options?.pageSize ?? 100;
|
|
11410
|
+
const signal = options?.signal;
|
|
11411
|
+
let offset = 0;
|
|
11412
|
+
while (true) {
|
|
11413
|
+
if (signal?.aborted) return;
|
|
11414
|
+
const response = await fetchPage(offset, signal);
|
|
11415
|
+
const items = getItems(response);
|
|
11416
|
+
for (const item of items) {
|
|
11417
|
+
yield item;
|
|
11418
|
+
}
|
|
11419
|
+
if (items.length === 0) return;
|
|
11420
|
+
offset += items.length;
|
|
11421
|
+
const total = getTotalCount(response);
|
|
11422
|
+
if (total > 0 && offset >= total) return;
|
|
11423
|
+
}
|
|
11424
|
+
}
|
|
11425
|
+
|
|
11426
|
+
// src/webhooks.ts
|
|
11427
|
+
var VALID_TYPES = /* @__PURE__ */ new Set(["cell.updated", "cell.new", "cell.deleted"]);
|
|
11428
|
+
function parseWebhook(body) {
|
|
11429
|
+
if (typeof body !== "object" || body === null) {
|
|
11430
|
+
throw new Error("Invalid webhook payload: expected an object");
|
|
11431
|
+
}
|
|
11432
|
+
const obj = body;
|
|
11433
|
+
if (typeof obj.id !== "string") {
|
|
11434
|
+
throw new Error("Invalid webhook payload: missing 'id'");
|
|
11435
|
+
}
|
|
11436
|
+
if (typeof obj.timestamp !== "string") {
|
|
11437
|
+
throw new Error("Invalid webhook payload: missing 'timestamp'");
|
|
11438
|
+
}
|
|
11439
|
+
if (typeof obj.type !== "string" || !VALID_TYPES.has(obj.type)) {
|
|
11440
|
+
throw new Error(`Invalid webhook payload: unknown type '${String(obj.type)}'`);
|
|
11441
|
+
}
|
|
11442
|
+
if (typeof obj.data !== "object" || obj.data === null) {
|
|
11443
|
+
throw new Error("Invalid webhook payload: missing 'data'");
|
|
11444
|
+
}
|
|
11445
|
+
return obj;
|
|
11446
|
+
}
|
|
11447
|
+
|
|
11222
11448
|
// src/index.ts
|
|
11223
11449
|
var Chrt = class {
|
|
11224
11450
|
/** Tile endpoints — styleUrl, tileUrl, groups */
|
|
@@ -11236,15 +11462,22 @@ var Chrt = class {
|
|
|
11236
11462
|
featuresEp;
|
|
11237
11463
|
queryEp;
|
|
11238
11464
|
http;
|
|
11465
|
+
tileAuth;
|
|
11466
|
+
region;
|
|
11467
|
+
tileBaseUrl;
|
|
11239
11468
|
constructor(apiKeyOrConfig) {
|
|
11240
11469
|
const config = typeof apiKeyOrConfig === "string" ? { apiKey: apiKeyOrConfig } : apiKeyOrConfig;
|
|
11470
|
+
this.region = config.region ?? DEFAULT_TILE_REGION;
|
|
11471
|
+
this.tileBaseUrl = config.tileBaseUrl ?? DEFAULT_TILE_BASE_URL;
|
|
11241
11472
|
this.http = new HttpClient(config.apiKey, config.baseUrl, config.retries);
|
|
11242
|
-
|
|
11473
|
+
const tileOpts = { region: this.region, tileBaseUrl: this.tileBaseUrl };
|
|
11474
|
+
this.tiles = new TilesEndpoint(this.http, tileOpts);
|
|
11243
11475
|
this.cells = new CellsEndpoint(this.http);
|
|
11244
11476
|
this.coverage = new CoverageEndpoint(this.http);
|
|
11245
11477
|
this.featuresEp = new FeaturesEndpoint(this.http);
|
|
11246
11478
|
this.queryEp = new QueryEndpoint(this.http);
|
|
11247
11479
|
this.offline = new OfflineEndpoint(this.http, config.deviceId, config.manifestPath);
|
|
11480
|
+
this.tileAuth = new TileAuth(this.http, tileOpts);
|
|
11248
11481
|
}
|
|
11249
11482
|
/* ── Convenience shortcuts ───────────────────────── */
|
|
11250
11483
|
/** Query GeoJSON features by bounding box */
|
|
@@ -11321,28 +11554,63 @@ var Chrt = class {
|
|
|
11321
11554
|
);
|
|
11322
11555
|
}
|
|
11323
11556
|
/* ── Map integration helpers ─────────────────────── */
|
|
11324
|
-
/**
|
|
11557
|
+
/**
|
|
11558
|
+
* Build a ready-to-use MapLibre / Mapbox GL style object for CHRT's nautical
|
|
11559
|
+
* charts. The default is the full IHO **S-52** portrayal; pick a palette and
|
|
11560
|
+
* tweak rendering via `opts`. The returned object is a standard MapLibre
|
|
11561
|
+
* style — mutate it freely, or build your own from scratch over the CHRT
|
|
11562
|
+
* vector source (see `buildSources`) for a fully custom look.
|
|
11563
|
+
*
|
|
11564
|
+
* Pair with `transformRequest()` so tile requests carry their auth token.
|
|
11565
|
+
*
|
|
11566
|
+
* @example
|
|
11567
|
+
* const map = new maplibregl.Map({
|
|
11568
|
+
* container: 'map',
|
|
11569
|
+
* style: chrt.style({ palette: 'DAY' }),
|
|
11570
|
+
* transformRequest: chrt.transformRequest(),
|
|
11571
|
+
* });
|
|
11572
|
+
*
|
|
11573
|
+
* @remarks S-101 portrayal is on the roadmap; today `style()` emits S-52.
|
|
11574
|
+
*/
|
|
11575
|
+
style(opts = {}) {
|
|
11576
|
+
const { palette = "DAY", ...styleOpts } = opts;
|
|
11577
|
+
return generateStyle(palette, {
|
|
11578
|
+
region: this.region,
|
|
11579
|
+
tileBaseUrl: this.tileBaseUrl,
|
|
11580
|
+
...styleOpts
|
|
11581
|
+
});
|
|
11582
|
+
}
|
|
11583
|
+
/**
|
|
11584
|
+
* @deprecated CHRT serves no remote `style.json`. Use {@link style} to get a
|
|
11585
|
+
* MapLibre style object instead. Throws to avoid silently pointing a map at a
|
|
11586
|
+
* dead URL.
|
|
11587
|
+
*/
|
|
11325
11588
|
styleUrl() {
|
|
11326
11589
|
return this.tiles.styleUrl();
|
|
11327
11590
|
}
|
|
11328
11591
|
/**
|
|
11329
|
-
* Returns a `transformRequest`
|
|
11330
|
-
*
|
|
11592
|
+
* Returns a `transformRequest` for Mapbox GL JS / MapLibre GL JS that mints a
|
|
11593
|
+
* short-lived tile JWT from your API key and attaches it as
|
|
11594
|
+
* `Authorization: Bearer` on requests to `tiles.chrt.co` only. Style, sprite,
|
|
11595
|
+
* and glyph URLs pass through untouched. The cache warms in the background on
|
|
11596
|
+
* first call, so the first tile or two may be retried by the renderer.
|
|
11331
11597
|
*
|
|
11332
11598
|
* @example
|
|
11333
|
-
* const map = new
|
|
11334
|
-
* style: chrt.
|
|
11599
|
+
* const map = new maplibregl.Map({
|
|
11600
|
+
* style: chrt.style(),
|
|
11335
11601
|
* transformRequest: chrt.transformRequest(),
|
|
11336
11602
|
* });
|
|
11337
11603
|
*/
|
|
11338
|
-
transformRequest() {
|
|
11339
|
-
|
|
11340
|
-
|
|
11341
|
-
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
|
|
11345
|
-
|
|
11604
|
+
transformRequest(opts) {
|
|
11605
|
+
return this.tileAuth.transformRequest(opts?.region);
|
|
11606
|
+
}
|
|
11607
|
+
/**
|
|
11608
|
+
* Pre-mint the tile token for a region. Optional — `transformRequest()` warms
|
|
11609
|
+
* the cache automatically — but useful if you want to surface a mint error
|
|
11610
|
+
* (e.g. `region_not_licensed`, `tile_quota_exceeded`) before the map loads.
|
|
11611
|
+
*/
|
|
11612
|
+
async warmTiles(region) {
|
|
11613
|
+
await this.tileAuth.getToken(region);
|
|
11346
11614
|
}
|
|
11347
11615
|
};
|
|
11348
11616
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -11358,6 +11626,7 @@ var Chrt = class {
|
|
|
11358
11626
|
ChrtError,
|
|
11359
11627
|
DEFAULT_REGION,
|
|
11360
11628
|
DEFAULT_TILE_BASE_URL,
|
|
11629
|
+
DEFAULT_TILE_REGION,
|
|
11361
11630
|
LABELED_LAYERS,
|
|
11362
11631
|
LABEL_BEHAVIOR,
|
|
11363
11632
|
NAUTICAL_SYMBOLS,
|
|
@@ -11375,6 +11644,8 @@ var Chrt = class {
|
|
|
11375
11644
|
S52_NIGHT,
|
|
11376
11645
|
SYMBOL_GROUPS,
|
|
11377
11646
|
SYMBOL_MAP,
|
|
11647
|
+
TileAuth,
|
|
11648
|
+
TileTokenError,
|
|
11378
11649
|
applyColorTheme,
|
|
11379
11650
|
bandFilterExpression,
|
|
11380
11651
|
buildLabelLayer,
|