@chrtco/sdk 0.7.6 → 0.8.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 +10 -0
- package/README.md +49 -16
- package/dist/atlas/browser.cjs +35 -11
- package/dist/atlas/browser.cjs.map +1 -1
- package/dist/atlas/browser.js +35 -11
- package/dist/atlas/browser.js.map +1 -1
- package/dist/index.cjs +384 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +291 -117
- package/dist/index.d.ts +291 -117
- package/dist/index.js +381 -77
- package/dist/index.js.map +1 -1
- package/package.json +3 -20
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
|
|
@@ -5315,7 +5500,10 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5315
5500
|
getSvg: landmarkSpire,
|
|
5316
5501
|
group: "landmarks",
|
|
5317
5502
|
description: "Church spire or steeple, conspicuous from seaward. Useful for visual fixing.",
|
|
5318
|
-
|
|
5503
|
+
// CATLMK 20 alone has no dedicated PL symbol (renders POSGEN01/03); church spires carry
|
|
5504
|
+
// FUNCTN 20/21 and render as the religious-building symbol BUIREL01 (BUIREL13 when conspicuous)
|
|
5505
|
+
s52PlRef: "BUIREL01",
|
|
5506
|
+
s57Class: "LNDMRK",
|
|
5319
5507
|
attributeKey: "CATLMK",
|
|
5320
5508
|
attributeValue: 20,
|
|
5321
5509
|
renderSize: "32x32",
|
|
@@ -5332,9 +5520,11 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5332
5520
|
getSvg: landmarkTower,
|
|
5333
5521
|
group: "landmarks",
|
|
5334
5522
|
description: "Tower conspicuous from seaward. General-purpose landmark symbol.",
|
|
5335
|
-
s52PlRef: "
|
|
5523
|
+
s52PlRef: "TOWERS01",
|
|
5524
|
+
s57Class: "LNDMRK",
|
|
5336
5525
|
attributeKey: "CATLMK",
|
|
5337
5526
|
attributeValue: 17,
|
|
5527
|
+
// TOWERS03 = conspicuous variant
|
|
5338
5528
|
renderSize: "32x32",
|
|
5339
5529
|
colors: [{ token: "CHBLK", hex: "#000000", usage: "Tower body, cap, position circle" }],
|
|
5340
5530
|
lineStyles: [{ element: "Tower outline", width: 0.8, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
|
|
@@ -5349,7 +5539,8 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5349
5539
|
getSvg: landmarkChimney,
|
|
5350
5540
|
group: "landmarks",
|
|
5351
5541
|
description: "Industrial chimney or smokestack visible from the sea.",
|
|
5352
|
-
s52PlRef: "
|
|
5542
|
+
s52PlRef: "CHIMNY01",
|
|
5543
|
+
s57Class: "LNDMRK",
|
|
5353
5544
|
attributeKey: "CATLMK",
|
|
5354
5545
|
attributeValue: 3,
|
|
5355
5546
|
renderSize: "32x32",
|
|
@@ -5366,7 +5557,8 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5366
5557
|
getSvg: landmarkFlagstaff,
|
|
5367
5558
|
group: "landmarks",
|
|
5368
5559
|
description: "Flagstaff or flagpole conspicuous from seaward.",
|
|
5369
|
-
s52PlRef: "
|
|
5560
|
+
s52PlRef: "FLGSTF01",
|
|
5561
|
+
s57Class: "LNDMRK",
|
|
5370
5562
|
attributeKey: "CATLMK",
|
|
5371
5563
|
attributeValue: 5,
|
|
5372
5564
|
renderSize: "32x32",
|
|
@@ -5386,9 +5578,11 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5386
5578
|
getSvg: landmarkMonument,
|
|
5387
5579
|
group: "landmarks",
|
|
5388
5580
|
description: "Monument or memorial conspicuous from the sea.",
|
|
5389
|
-
s52PlRef: "
|
|
5581
|
+
s52PlRef: "MONUMT12",
|
|
5582
|
+
s57Class: "LNDMRK",
|
|
5390
5583
|
attributeKey: "CATLMK",
|
|
5391
5584
|
attributeValue: 9,
|
|
5585
|
+
// MONUMT02 = non-conspicuous variant
|
|
5392
5586
|
renderSize: "32x32",
|
|
5393
5587
|
colors: [{ token: "CHBLK", hex: "#000000", usage: "Monument body, base, position circle" }],
|
|
5394
5588
|
lineStyles: [{ element: "Body outline", width: 0.8, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
|
|
@@ -5404,6 +5598,7 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5404
5598
|
group: "landmarks",
|
|
5405
5599
|
description: "Traditional windmill with rotating vanes. Conspicuous landmark.",
|
|
5406
5600
|
s52PlRef: "WNDMIL12",
|
|
5601
|
+
s57Class: "LNDMRK",
|
|
5407
5602
|
attributeKey: "CATLMK",
|
|
5408
5603
|
attributeValue: 18,
|
|
5409
5604
|
renderSize: "32x32",
|
|
@@ -5421,6 +5616,7 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5421
5616
|
group: "landmarks",
|
|
5422
5617
|
description: "Modern 3-blade wind turbine. Increasingly common offshore landmark.",
|
|
5423
5618
|
s52PlRef: "WIMCON11",
|
|
5619
|
+
s57Class: "LNDMRK",
|
|
5424
5620
|
attributeKey: "CATLMK",
|
|
5425
5621
|
attributeValue: 19,
|
|
5426
5622
|
renderSize: "32x32",
|
|
@@ -5437,9 +5633,11 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5437
5633
|
getSvg: landmarkDome,
|
|
5438
5634
|
group: "landmarks",
|
|
5439
5635
|
description: "Dome or cupola (e.g., observatory, mosque) visible from the sea.",
|
|
5440
|
-
s52PlRef: "
|
|
5636
|
+
s52PlRef: "DOMES011",
|
|
5637
|
+
s57Class: "LNDMRK",
|
|
5441
5638
|
attributeKey: "CATLMK",
|
|
5442
5639
|
attributeValue: 15,
|
|
5640
|
+
// DOMES001 = non-conspicuous variant
|
|
5443
5641
|
renderSize: "32x32",
|
|
5444
5642
|
colors: [{ token: "CHBLK", hex: "#000000", usage: "Dome, base, position circle" }],
|
|
5445
5643
|
lineStyles: [{ element: "Base outline", width: 0.8, color: "#000000" }, { element: "Dome curve", width: 0.8, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
|
|
@@ -5454,7 +5652,8 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5454
5652
|
getSvg: landmarkRadarScanner,
|
|
5455
5653
|
group: "landmarks",
|
|
5456
5654
|
description: "Radar scanner antenna conspicuous from seaward.",
|
|
5457
|
-
s52PlRef: "
|
|
5655
|
+
s52PlRef: "RASCAN01",
|
|
5656
|
+
s57Class: "LNDMRK",
|
|
5458
5657
|
attributeKey: "CATLMK",
|
|
5459
5658
|
attributeValue: 16,
|
|
5460
5659
|
renderSize: "32x32",
|
|
@@ -5471,7 +5670,8 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5471
5670
|
getSvg: landmarkDishAerial,
|
|
5472
5671
|
group: "landmarks",
|
|
5473
5672
|
description: "Satellite dish or parabolic antenna visible from the sea.",
|
|
5474
|
-
s52PlRef: "
|
|
5673
|
+
s52PlRef: "DSHAER01",
|
|
5674
|
+
s57Class: "LNDMRK",
|
|
5475
5675
|
attributeKey: "CATLMK",
|
|
5476
5676
|
attributeValue: 4,
|
|
5477
5677
|
renderSize: "32x32",
|
|
@@ -5489,6 +5689,7 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5489
5689
|
group: "landmarks",
|
|
5490
5690
|
description: "Water tower. Elevated tank on supporting structure, conspicuous landmark.",
|
|
5491
5691
|
s52PlRef: "TOWERS12",
|
|
5692
|
+
s57Class: "SILTNK",
|
|
5492
5693
|
attributeKey: "CATSIL",
|
|
5493
5694
|
attributeValue: 4,
|
|
5494
5695
|
renderSize: "32x32",
|
|
@@ -5505,7 +5706,9 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5505
5706
|
getSvg: landmarkRadioTvMast,
|
|
5506
5707
|
group: "landmarks",
|
|
5507
5708
|
description: "Radio or television transmission mast with horizontal cross-arms.",
|
|
5508
|
-
|
|
5709
|
+
// Radio/TV masts are CATLMK 7 (mast) + FUNCTN in S-57, so both mast entries share MSTCON04
|
|
5710
|
+
s52PlRef: "MSTCON04",
|
|
5711
|
+
s57Class: "LNDMRK",
|
|
5509
5712
|
attributeKey: "CATLMK",
|
|
5510
5713
|
attributeValue: 7,
|
|
5511
5714
|
renderSize: "32x32",
|
|
@@ -5522,7 +5725,10 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5522
5725
|
getSvg: landmarkCross,
|
|
5523
5726
|
group: "landmarks",
|
|
5524
5727
|
description: "Religious cross or calvary conspicuous from seaward.",
|
|
5525
|
-
|
|
5728
|
+
// The PL has no dedicated symbol for CATLMK 14 (cross/calvary) — S-52 falls back to the
|
|
5729
|
+
// generic position symbol POSGEN01 (POSGEN03 when conspicuous)
|
|
5730
|
+
s52PlRef: "POSGEN01",
|
|
5731
|
+
s57Class: "LNDMRK",
|
|
5526
5732
|
attributeKey: "CATLMK",
|
|
5527
5733
|
attributeValue: 14,
|
|
5528
5734
|
renderSize: "32x32",
|
|
@@ -5539,9 +5745,11 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5539
5745
|
getSvg: landmarkMast,
|
|
5540
5746
|
group: "landmarks",
|
|
5541
5747
|
description: "General mast (not radio/TV). Pole with spherical top.",
|
|
5542
|
-
s52PlRef: "
|
|
5748
|
+
s52PlRef: "MSTCON04",
|
|
5749
|
+
s57Class: "LNDMRK",
|
|
5543
5750
|
attributeKey: "CATLMK",
|
|
5544
5751
|
attributeValue: 7,
|
|
5752
|
+
// MSTCON14 = conspicuous variant
|
|
5545
5753
|
renderSize: "32x32",
|
|
5546
5754
|
colors: [{ token: "CHBLK", hex: "#000000", usage: "Mast pole, top ball, position circle" }],
|
|
5547
5755
|
lineStyles: [{ element: "Mast pole", width: 2, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
|
|
@@ -5557,6 +5765,7 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5557
5765
|
group: "landmarks",
|
|
5558
5766
|
description: "Silo, grain elevator, or storage tank conspicuous from the sea.",
|
|
5559
5767
|
s52PlRef: "SILBUI01",
|
|
5768
|
+
s57Class: "SILTNK",
|
|
5560
5769
|
attributeKey: "CATSIL",
|
|
5561
5770
|
attributeValue: 1,
|
|
5562
5771
|
renderSize: "32x32",
|
|
@@ -8267,6 +8476,18 @@ var S101_FEATURES = [
|
|
|
8267
8476
|
isNewInS101: false,
|
|
8268
8477
|
attributes: ["categoryOfLandmark", "function", "visuallyConspicuous"]
|
|
8269
8478
|
},
|
|
8479
|
+
{
|
|
8480
|
+
featureClass: "SiloStorageTank",
|
|
8481
|
+
name: "Silo / Storage Tank",
|
|
8482
|
+
s57Equivalent: "SILTNK",
|
|
8483
|
+
section: "E",
|
|
8484
|
+
group: "landmarks",
|
|
8485
|
+
category: "geo",
|
|
8486
|
+
description: "An enclosed structure for storing bulk solids or liquids (silo, grain elevator, water tower, tank). Often conspicuous and useful for position fixing.",
|
|
8487
|
+
portrayalRef: "SILTNK_S101",
|
|
8488
|
+
isNewInS101: false,
|
|
8489
|
+
attributes: ["categoryOfSiloTank", "colour", "visuallyConspicuous"]
|
|
8490
|
+
},
|
|
8270
8491
|
// ── Section D: Bridges ──
|
|
8271
8492
|
{
|
|
8272
8493
|
featureClass: "Bridge",
|
|
@@ -9443,7 +9664,7 @@ var BUILD_MINZOOM = 3;
|
|
|
9443
9664
|
var BUILD_MAXZOOM = 14;
|
|
9444
9665
|
|
|
9445
9666
|
// src/atlas/source-registry.ts
|
|
9446
|
-
var
|
|
9667
|
+
var DEFAULT_TILE_BASE_URL2 = "https://tiles.chrt.co";
|
|
9447
9668
|
var DEFAULT_REGION = "NOAA_ENC";
|
|
9448
9669
|
function buildSources(opts = {}) {
|
|
9449
9670
|
const mode = opts.mode ?? "pmtiles";
|
|
@@ -9456,7 +9677,7 @@ function buildSources(opts = {}) {
|
|
|
9456
9677
|
}
|
|
9457
9678
|
};
|
|
9458
9679
|
}
|
|
9459
|
-
const base = (opts.tileBaseUrl ??
|
|
9680
|
+
const base = (opts.tileBaseUrl ?? DEFAULT_TILE_BASE_URL2).replace(/\/+$/, "");
|
|
9460
9681
|
return {
|
|
9461
9682
|
[region]: {
|
|
9462
9683
|
type: "vector",
|
|
@@ -11183,6 +11404,47 @@ function getStyleStats(style) {
|
|
|
11183
11404
|
};
|
|
11184
11405
|
}
|
|
11185
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
|
+
|
|
11186
11448
|
// src/index.ts
|
|
11187
11449
|
var Chrt = class {
|
|
11188
11450
|
/** Tile endpoints — styleUrl, tileUrl, groups */
|
|
@@ -11200,15 +11462,22 @@ var Chrt = class {
|
|
|
11200
11462
|
featuresEp;
|
|
11201
11463
|
queryEp;
|
|
11202
11464
|
http;
|
|
11465
|
+
tileAuth;
|
|
11466
|
+
region;
|
|
11467
|
+
tileBaseUrl;
|
|
11203
11468
|
constructor(apiKeyOrConfig) {
|
|
11204
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;
|
|
11205
11472
|
this.http = new HttpClient(config.apiKey, config.baseUrl, config.retries);
|
|
11206
|
-
|
|
11473
|
+
const tileOpts = { region: this.region, tileBaseUrl: this.tileBaseUrl };
|
|
11474
|
+
this.tiles = new TilesEndpoint(this.http, tileOpts);
|
|
11207
11475
|
this.cells = new CellsEndpoint(this.http);
|
|
11208
11476
|
this.coverage = new CoverageEndpoint(this.http);
|
|
11209
11477
|
this.featuresEp = new FeaturesEndpoint(this.http);
|
|
11210
11478
|
this.queryEp = new QueryEndpoint(this.http);
|
|
11211
11479
|
this.offline = new OfflineEndpoint(this.http, config.deviceId, config.manifestPath);
|
|
11480
|
+
this.tileAuth = new TileAuth(this.http, tileOpts);
|
|
11212
11481
|
}
|
|
11213
11482
|
/* ── Convenience shortcuts ───────────────────────── */
|
|
11214
11483
|
/** Query GeoJSON features by bounding box */
|
|
@@ -11285,28 +11554,63 @@ var Chrt = class {
|
|
|
11285
11554
|
);
|
|
11286
11555
|
}
|
|
11287
11556
|
/* ── Map integration helpers ─────────────────────── */
|
|
11288
|
-
/**
|
|
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
|
+
*/
|
|
11289
11588
|
styleUrl() {
|
|
11290
11589
|
return this.tiles.styleUrl();
|
|
11291
11590
|
}
|
|
11292
11591
|
/**
|
|
11293
|
-
* Returns a `transformRequest`
|
|
11294
|
-
*
|
|
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.
|
|
11295
11597
|
*
|
|
11296
11598
|
* @example
|
|
11297
|
-
* const map = new
|
|
11298
|
-
* style: chrt.
|
|
11599
|
+
* const map = new maplibregl.Map({
|
|
11600
|
+
* style: chrt.style(),
|
|
11299
11601
|
* transformRequest: chrt.transformRequest(),
|
|
11300
11602
|
* });
|
|
11301
11603
|
*/
|
|
11302
|
-
transformRequest() {
|
|
11303
|
-
|
|
11304
|
-
|
|
11305
|
-
|
|
11306
|
-
|
|
11307
|
-
|
|
11308
|
-
|
|
11309
|
-
|
|
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);
|
|
11310
11614
|
}
|
|
11311
11615
|
};
|
|
11312
11616
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -11322,6 +11626,7 @@ var Chrt = class {
|
|
|
11322
11626
|
ChrtError,
|
|
11323
11627
|
DEFAULT_REGION,
|
|
11324
11628
|
DEFAULT_TILE_BASE_URL,
|
|
11629
|
+
DEFAULT_TILE_REGION,
|
|
11325
11630
|
LABELED_LAYERS,
|
|
11326
11631
|
LABEL_BEHAVIOR,
|
|
11327
11632
|
NAUTICAL_SYMBOLS,
|
|
@@ -11339,6 +11644,8 @@ var Chrt = class {
|
|
|
11339
11644
|
S52_NIGHT,
|
|
11340
11645
|
SYMBOL_GROUPS,
|
|
11341
11646
|
SYMBOL_MAP,
|
|
11647
|
+
TileAuth,
|
|
11648
|
+
TileTokenError,
|
|
11342
11649
|
applyColorTheme,
|
|
11343
11650
|
bandFilterExpression,
|
|
11344
11651
|
buildLabelLayer,
|