@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.js
CHANGED
|
@@ -121,20 +121,243 @@ function sleep(ms) {
|
|
|
121
121
|
return new Promise((r) => setTimeout(r, ms));
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
// src/tile-auth.ts
|
|
125
|
+
var DEFAULT_TILE_REGION = "NOAA_ENC";
|
|
126
|
+
var DEFAULT_TILE_BASE_URL = "https://tiles.chrt.co";
|
|
127
|
+
var TileTokenError = class extends Error {
|
|
128
|
+
code;
|
|
129
|
+
status;
|
|
130
|
+
constructor(code, message, status) {
|
|
131
|
+
super(message);
|
|
132
|
+
this.name = "TileTokenError";
|
|
133
|
+
this.code = code;
|
|
134
|
+
this.status = status;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
var TILE_URL_PATH = /\/v1\/tiles\/[A-Za-z0-9_]+(?:\/[a-z][a-z0-9-]*)?\/\d+\/\d+\/\d+\.mvt(?:\?.*)?$/i;
|
|
138
|
+
var REFRESH_MARGIN_SECONDS = 30;
|
|
139
|
+
var STALE_GRACE_SECONDS = 1;
|
|
140
|
+
var MINT_RETRY_DELAYS_MS = [500, 1e3, 2e3];
|
|
141
|
+
function nowSeconds() {
|
|
142
|
+
return Math.floor(Date.now() / 1e3);
|
|
143
|
+
}
|
|
144
|
+
function sleep2(ms) {
|
|
145
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
146
|
+
}
|
|
147
|
+
function mapErrorCode(raw, status) {
|
|
148
|
+
const known = [
|
|
149
|
+
"invalid_api_key",
|
|
150
|
+
"feature_flag_disabled",
|
|
151
|
+
"region_not_licensed",
|
|
152
|
+
"tile_quota_exceeded",
|
|
153
|
+
"region_not_available"
|
|
154
|
+
];
|
|
155
|
+
if (typeof raw === "string" && known.includes(raw)) {
|
|
156
|
+
return raw;
|
|
157
|
+
}
|
|
158
|
+
if (status === 401) return "invalid_api_key";
|
|
159
|
+
if (status === 403) return "feature_flag_disabled";
|
|
160
|
+
if (status === 429) return "tile_quota_exceeded";
|
|
161
|
+
if (status === 503) return "region_not_available";
|
|
162
|
+
return "unexpected_response";
|
|
163
|
+
}
|
|
164
|
+
var TileAuth = class {
|
|
165
|
+
constructor(http, opts) {
|
|
166
|
+
this.http = http;
|
|
167
|
+
this.defaultRegion = opts?.region ?? DEFAULT_TILE_REGION;
|
|
168
|
+
this.tileBaseUrl = (opts?.tileBaseUrl ?? DEFAULT_TILE_BASE_URL).replace(/\/+$/, "");
|
|
169
|
+
}
|
|
170
|
+
cache = /* @__PURE__ */ new Map();
|
|
171
|
+
pending = /* @__PURE__ */ new Map();
|
|
172
|
+
defaultRegion;
|
|
173
|
+
tileBaseUrl;
|
|
174
|
+
lifecycleRegistered = false;
|
|
175
|
+
/** Mint once via the HTTP client (POST `/mint/tile-token`, `x-api-key`). */
|
|
176
|
+
async mintOnce(region) {
|
|
177
|
+
let res;
|
|
178
|
+
try {
|
|
179
|
+
res = await this.http.postJson("/mint/tile-token", { region });
|
|
180
|
+
} catch (err) {
|
|
181
|
+
throw new TileTokenError(
|
|
182
|
+
"network_error",
|
|
183
|
+
err instanceof Error ? err.message : "mint request failed"
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (!res.ok) {
|
|
187
|
+
const code = mapErrorCode(res.body?.error, res.status);
|
|
188
|
+
const msg = typeof res.body?.error === "string" ? res.body.error : `HTTP ${res.status}`;
|
|
189
|
+
throw new TileTokenError(code, msg, res.status);
|
|
190
|
+
}
|
|
191
|
+
const b = res.body;
|
|
192
|
+
if (!b || typeof b.token !== "string" || typeof b.expires_at !== "number" || typeof b.kid !== "string" || typeof b.build_tag !== "string" || typeof b.region !== "string") {
|
|
193
|
+
throw new TileTokenError(
|
|
194
|
+
"unexpected_response",
|
|
195
|
+
"mint-tile-token returned a malformed body",
|
|
196
|
+
res.status
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
token: b.token,
|
|
201
|
+
expiresAt: b.expires_at,
|
|
202
|
+
kid: b.kid,
|
|
203
|
+
buildTag: b.build_tag,
|
|
204
|
+
region: b.region
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/** Mint with exponential backoff on network errors only. */
|
|
208
|
+
async mintWithRetry(region) {
|
|
209
|
+
let lastErr;
|
|
210
|
+
for (let attempt = 0; attempt <= MINT_RETRY_DELAYS_MS.length; attempt++) {
|
|
211
|
+
try {
|
|
212
|
+
return await this.mintOnce(region);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
lastErr = err;
|
|
215
|
+
if (err instanceof TileTokenError && err.code !== "network_error") throw err;
|
|
216
|
+
if (attempt === MINT_RETRY_DELAYS_MS.length) break;
|
|
217
|
+
await sleep2(MINT_RETRY_DELAYS_MS[attempt]);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
throw lastErr instanceof Error ? lastErr : new TileTokenError("network_error", "mint failed");
|
|
221
|
+
}
|
|
222
|
+
scheduleRefresh(region, expiresAt) {
|
|
223
|
+
const delaySeconds = expiresAt - nowSeconds() - REFRESH_MARGIN_SECONDS;
|
|
224
|
+
if (delaySeconds <= 0) return null;
|
|
225
|
+
const timer = setTimeout(() => {
|
|
226
|
+
void this.refreshInBackground(region);
|
|
227
|
+
}, delaySeconds * 1e3);
|
|
228
|
+
timer.unref?.();
|
|
229
|
+
return timer;
|
|
230
|
+
}
|
|
231
|
+
async refreshInBackground(region) {
|
|
232
|
+
try {
|
|
233
|
+
const response = await this.mintWithRetry(region);
|
|
234
|
+
this.store(region, response);
|
|
235
|
+
} catch {
|
|
236
|
+
this.invalidate(region);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
store(region, response) {
|
|
240
|
+
const existing = this.cache.get(region);
|
|
241
|
+
if (existing?.refreshTimer) clearTimeout(existing.refreshTimer);
|
|
242
|
+
this.cache.set(region, {
|
|
243
|
+
response,
|
|
244
|
+
refreshTimer: this.scheduleRefresh(region, response.expiresAt)
|
|
245
|
+
});
|
|
246
|
+
this.registerLifecycleHandlers();
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Fetch (or reuse a cached) tile token for `region`. Concurrent callers for
|
|
250
|
+
* the same region await the same in-flight mint.
|
|
251
|
+
*/
|
|
252
|
+
async getToken(region) {
|
|
253
|
+
const r = region ?? this.defaultRegion;
|
|
254
|
+
const cached = this.cache.get(r);
|
|
255
|
+
if (cached && cached.response.expiresAt - nowSeconds() > REFRESH_MARGIN_SECONDS) {
|
|
256
|
+
return cached.response;
|
|
257
|
+
}
|
|
258
|
+
const inflight = this.pending.get(r);
|
|
259
|
+
if (inflight) return inflight;
|
|
260
|
+
const promise = (async () => {
|
|
261
|
+
try {
|
|
262
|
+
const response = await this.mintWithRetry(r);
|
|
263
|
+
this.store(r, response);
|
|
264
|
+
return response;
|
|
265
|
+
} finally {
|
|
266
|
+
this.pending.delete(r);
|
|
267
|
+
}
|
|
268
|
+
})();
|
|
269
|
+
this.pending.set(r, promise);
|
|
270
|
+
return promise;
|
|
271
|
+
}
|
|
272
|
+
/** Synchronous cache peek — null if absent or within the stale grace window. */
|
|
273
|
+
peek(region) {
|
|
274
|
+
const entry = this.cache.get(region ?? this.defaultRegion);
|
|
275
|
+
if (!entry) return null;
|
|
276
|
+
if (entry.response.expiresAt - nowSeconds() <= STALE_GRACE_SECONDS) return null;
|
|
277
|
+
return entry.response.token;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Produce a MapLibre / Mapbox GL `transformRequest`. Attaches
|
|
281
|
+
* `Authorization: Bearer <jwt>` only to CHRT tile URLs; style, sprite, and
|
|
282
|
+
* glyph URLs pass through untouched (never leak the token to third-party
|
|
283
|
+
* CDNs). Kicks off a background mint immediately so the cache warms within
|
|
284
|
+
* one round-trip; the first tile or two may go unauthenticated and be
|
|
285
|
+
* retried by the renderer.
|
|
286
|
+
*/
|
|
287
|
+
transformRequest(region) {
|
|
288
|
+
const r = region ?? this.defaultRegion;
|
|
289
|
+
void this.getToken(r).catch(() => void 0);
|
|
290
|
+
return (url) => {
|
|
291
|
+
if (!url.startsWith(this.tileBaseUrl) || !TILE_URL_PATH.test(url)) return void 0;
|
|
292
|
+
const token = this.peek(r);
|
|
293
|
+
if (!token) return { url };
|
|
294
|
+
return { url, headers: { Authorization: `Bearer ${token}` } };
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
/** Clear the cached token + timer for a region. */
|
|
298
|
+
invalidate(region) {
|
|
299
|
+
const r = region ?? this.defaultRegion;
|
|
300
|
+
const entry = this.cache.get(r);
|
|
301
|
+
if (entry?.refreshTimer) clearTimeout(entry.refreshTimer);
|
|
302
|
+
this.cache.delete(r);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Browser-only defense against `setTimeout` freezing while a tab is
|
|
306
|
+
* suspended (laptop sleep, mobile background, bfcache restore): proactively
|
|
307
|
+
* re-mint near-expiry entries on wake. Inert under SSR / Node / Deno.
|
|
308
|
+
*/
|
|
309
|
+
registerLifecycleHandlers() {
|
|
310
|
+
if (this.lifecycleRegistered) return;
|
|
311
|
+
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
312
|
+
const onWake = () => {
|
|
313
|
+
if (document.visibilityState !== "visible") return;
|
|
314
|
+
for (const [region, entry] of this.cache.entries()) {
|
|
315
|
+
if (entry.response.expiresAt - nowSeconds() >= REFRESH_MARGIN_SECONDS) continue;
|
|
316
|
+
void this.refreshInBackground(region);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
document.addEventListener("visibilitychange", onWake);
|
|
320
|
+
window.addEventListener("pageshow", (e) => {
|
|
321
|
+
if (e.persisted) onWake();
|
|
322
|
+
});
|
|
323
|
+
this.lifecycleRegistered = true;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
124
327
|
// src/endpoints/tiles.ts
|
|
125
328
|
var TilesEndpoint = class {
|
|
126
|
-
constructor(http) {
|
|
329
|
+
constructor(http, opts) {
|
|
127
330
|
this.http = http;
|
|
331
|
+
this.region = opts?.region ?? DEFAULT_TILE_REGION;
|
|
332
|
+
this.tileBaseUrl = (opts?.tileBaseUrl ?? DEFAULT_TILE_BASE_URL).replace(/\/+$/, "");
|
|
128
333
|
}
|
|
129
|
-
|
|
334
|
+
region;
|
|
335
|
+
tileBaseUrl;
|
|
336
|
+
/**
|
|
337
|
+
* @deprecated CHRT has no remote `style.json` endpoint — the S-52 style is
|
|
338
|
+
* generated client-side. Use `chrt.style(opts)` to get a ready-to-use
|
|
339
|
+
* MapLibre style object instead. Calling this throws to prevent silently
|
|
340
|
+
* pointing a map at a dead URL.
|
|
341
|
+
*/
|
|
130
342
|
styleUrl() {
|
|
131
|
-
|
|
343
|
+
throw new Error(
|
|
344
|
+
"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"
|
|
345
|
+
);
|
|
132
346
|
}
|
|
133
|
-
/**
|
|
134
|
-
|
|
135
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Vector tile URL template for the configured region, served by the
|
|
349
|
+
* tile-worker at `tiles.chrt.co`. Tiles are JWT-gated — pair this with
|
|
350
|
+
* `chrt.transformRequest()`, which attaches the `Authorization: Bearer`
|
|
351
|
+
* header. Prefer `chrt.style()`, which wires the source for you.
|
|
352
|
+
*/
|
|
353
|
+
tileUrl(z, x, y) {
|
|
354
|
+
return `${this.tileBaseUrl}/v1/tiles/${this.region}/${z}/${x}/${y}.mvt`;
|
|
136
355
|
}
|
|
137
|
-
/**
|
|
356
|
+
/**
|
|
357
|
+
* @deprecated Tilesets were consolidated to one PMTiles per region (2026-04);
|
|
358
|
+
* the per-group catalogue endpoint is legacy and no longer served. Render via
|
|
359
|
+
* `chrt.style()` instead, which emits a single vector source per region.
|
|
360
|
+
*/
|
|
138
361
|
async groups() {
|
|
139
362
|
return this.http.request("/tiles/groups");
|
|
140
363
|
}
|
|
@@ -326,7 +549,7 @@ function mapServerError(body, status) {
|
|
|
326
549
|
}
|
|
327
550
|
|
|
328
551
|
// src/endpoints/offline-token-manager.ts
|
|
329
|
-
var
|
|
552
|
+
var REFRESH_MARGIN_SECONDS2 = 30;
|
|
330
553
|
function now() {
|
|
331
554
|
return Math.floor(Date.now() / 1e3);
|
|
332
555
|
}
|
|
@@ -347,7 +570,7 @@ var OfflineTokenManager = class {
|
|
|
347
570
|
}
|
|
348
571
|
const key = this.cacheKey(deviceId, regions);
|
|
349
572
|
const cached = this.cache.get(key);
|
|
350
|
-
if (cached && cached.expiresAt - now() >
|
|
573
|
+
if (cached && cached.expiresAt - now() > REFRESH_MARGIN_SECONDS2) {
|
|
351
574
|
return cached;
|
|
352
575
|
}
|
|
353
576
|
const inflight = this.pending.get(key);
|
|
@@ -969,47 +1192,6 @@ function dirname(p) {
|
|
|
969
1192
|
return p.slice(0, k);
|
|
970
1193
|
}
|
|
971
1194
|
|
|
972
|
-
// src/pagination.ts
|
|
973
|
-
async function* paginate(fetchPage, getItems, getTotalCount, options) {
|
|
974
|
-
const pageSize = options?.pageSize ?? 100;
|
|
975
|
-
const signal = options?.signal;
|
|
976
|
-
let offset = 0;
|
|
977
|
-
while (true) {
|
|
978
|
-
if (signal?.aborted) return;
|
|
979
|
-
const response = await fetchPage(offset, signal);
|
|
980
|
-
const items = getItems(response);
|
|
981
|
-
for (const item of items) {
|
|
982
|
-
yield item;
|
|
983
|
-
}
|
|
984
|
-
if (items.length === 0) return;
|
|
985
|
-
offset += items.length;
|
|
986
|
-
const total = getTotalCount(response);
|
|
987
|
-
if (total > 0 && offset >= total) return;
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
// src/webhooks.ts
|
|
992
|
-
var VALID_TYPES = /* @__PURE__ */ new Set(["cell.updated", "cell.new", "cell.deleted"]);
|
|
993
|
-
function parseWebhook(body) {
|
|
994
|
-
if (typeof body !== "object" || body === null) {
|
|
995
|
-
throw new Error("Invalid webhook payload: expected an object");
|
|
996
|
-
}
|
|
997
|
-
const obj = body;
|
|
998
|
-
if (typeof obj.id !== "string") {
|
|
999
|
-
throw new Error("Invalid webhook payload: missing 'id'");
|
|
1000
|
-
}
|
|
1001
|
-
if (typeof obj.timestamp !== "string") {
|
|
1002
|
-
throw new Error("Invalid webhook payload: missing 'timestamp'");
|
|
1003
|
-
}
|
|
1004
|
-
if (typeof obj.type !== "string" || !VALID_TYPES.has(obj.type)) {
|
|
1005
|
-
throw new Error(`Invalid webhook payload: unknown type '${String(obj.type)}'`);
|
|
1006
|
-
}
|
|
1007
|
-
if (typeof obj.data !== "object" || obj.data === null) {
|
|
1008
|
-
throw new Error("Invalid webhook payload: missing 'data'");
|
|
1009
|
-
}
|
|
1010
|
-
return obj;
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
1195
|
// src/atlas/s52-colors.ts
|
|
1014
1196
|
var S52_DAY = {
|
|
1015
1197
|
// Depth
|
|
@@ -5210,7 +5392,10 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5210
5392
|
getSvg: landmarkSpire,
|
|
5211
5393
|
group: "landmarks",
|
|
5212
5394
|
description: "Church spire or steeple, conspicuous from seaward. Useful for visual fixing.",
|
|
5213
|
-
|
|
5395
|
+
// CATLMK 20 alone has no dedicated PL symbol (renders POSGEN01/03); church spires carry
|
|
5396
|
+
// FUNCTN 20/21 and render as the religious-building symbol BUIREL01 (BUIREL13 when conspicuous)
|
|
5397
|
+
s52PlRef: "BUIREL01",
|
|
5398
|
+
s57Class: "LNDMRK",
|
|
5214
5399
|
attributeKey: "CATLMK",
|
|
5215
5400
|
attributeValue: 20,
|
|
5216
5401
|
renderSize: "32x32",
|
|
@@ -5227,9 +5412,11 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5227
5412
|
getSvg: landmarkTower,
|
|
5228
5413
|
group: "landmarks",
|
|
5229
5414
|
description: "Tower conspicuous from seaward. General-purpose landmark symbol.",
|
|
5230
|
-
s52PlRef: "
|
|
5415
|
+
s52PlRef: "TOWERS01",
|
|
5416
|
+
s57Class: "LNDMRK",
|
|
5231
5417
|
attributeKey: "CATLMK",
|
|
5232
5418
|
attributeValue: 17,
|
|
5419
|
+
// TOWERS03 = conspicuous variant
|
|
5233
5420
|
renderSize: "32x32",
|
|
5234
5421
|
colors: [{ token: "CHBLK", hex: "#000000", usage: "Tower body, cap, position circle" }],
|
|
5235
5422
|
lineStyles: [{ element: "Tower outline", width: 0.8, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
|
|
@@ -5244,7 +5431,8 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5244
5431
|
getSvg: landmarkChimney,
|
|
5245
5432
|
group: "landmarks",
|
|
5246
5433
|
description: "Industrial chimney or smokestack visible from the sea.",
|
|
5247
|
-
s52PlRef: "
|
|
5434
|
+
s52PlRef: "CHIMNY01",
|
|
5435
|
+
s57Class: "LNDMRK",
|
|
5248
5436
|
attributeKey: "CATLMK",
|
|
5249
5437
|
attributeValue: 3,
|
|
5250
5438
|
renderSize: "32x32",
|
|
@@ -5261,7 +5449,8 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5261
5449
|
getSvg: landmarkFlagstaff,
|
|
5262
5450
|
group: "landmarks",
|
|
5263
5451
|
description: "Flagstaff or flagpole conspicuous from seaward.",
|
|
5264
|
-
s52PlRef: "
|
|
5452
|
+
s52PlRef: "FLGSTF01",
|
|
5453
|
+
s57Class: "LNDMRK",
|
|
5265
5454
|
attributeKey: "CATLMK",
|
|
5266
5455
|
attributeValue: 5,
|
|
5267
5456
|
renderSize: "32x32",
|
|
@@ -5281,9 +5470,11 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5281
5470
|
getSvg: landmarkMonument,
|
|
5282
5471
|
group: "landmarks",
|
|
5283
5472
|
description: "Monument or memorial conspicuous from the sea.",
|
|
5284
|
-
s52PlRef: "
|
|
5473
|
+
s52PlRef: "MONUMT12",
|
|
5474
|
+
s57Class: "LNDMRK",
|
|
5285
5475
|
attributeKey: "CATLMK",
|
|
5286
5476
|
attributeValue: 9,
|
|
5477
|
+
// MONUMT02 = non-conspicuous variant
|
|
5287
5478
|
renderSize: "32x32",
|
|
5288
5479
|
colors: [{ token: "CHBLK", hex: "#000000", usage: "Monument body, base, position circle" }],
|
|
5289
5480
|
lineStyles: [{ element: "Body outline", width: 0.8, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
|
|
@@ -5299,6 +5490,7 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5299
5490
|
group: "landmarks",
|
|
5300
5491
|
description: "Traditional windmill with rotating vanes. Conspicuous landmark.",
|
|
5301
5492
|
s52PlRef: "WNDMIL12",
|
|
5493
|
+
s57Class: "LNDMRK",
|
|
5302
5494
|
attributeKey: "CATLMK",
|
|
5303
5495
|
attributeValue: 18,
|
|
5304
5496
|
renderSize: "32x32",
|
|
@@ -5316,6 +5508,7 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5316
5508
|
group: "landmarks",
|
|
5317
5509
|
description: "Modern 3-blade wind turbine. Increasingly common offshore landmark.",
|
|
5318
5510
|
s52PlRef: "WIMCON11",
|
|
5511
|
+
s57Class: "LNDMRK",
|
|
5319
5512
|
attributeKey: "CATLMK",
|
|
5320
5513
|
attributeValue: 19,
|
|
5321
5514
|
renderSize: "32x32",
|
|
@@ -5332,9 +5525,11 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5332
5525
|
getSvg: landmarkDome,
|
|
5333
5526
|
group: "landmarks",
|
|
5334
5527
|
description: "Dome or cupola (e.g., observatory, mosque) visible from the sea.",
|
|
5335
|
-
s52PlRef: "
|
|
5528
|
+
s52PlRef: "DOMES011",
|
|
5529
|
+
s57Class: "LNDMRK",
|
|
5336
5530
|
attributeKey: "CATLMK",
|
|
5337
5531
|
attributeValue: 15,
|
|
5532
|
+
// DOMES001 = non-conspicuous variant
|
|
5338
5533
|
renderSize: "32x32",
|
|
5339
5534
|
colors: [{ token: "CHBLK", hex: "#000000", usage: "Dome, base, position circle" }],
|
|
5340
5535
|
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" }],
|
|
@@ -5349,7 +5544,8 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5349
5544
|
getSvg: landmarkRadarScanner,
|
|
5350
5545
|
group: "landmarks",
|
|
5351
5546
|
description: "Radar scanner antenna conspicuous from seaward.",
|
|
5352
|
-
s52PlRef: "
|
|
5547
|
+
s52PlRef: "RASCAN01",
|
|
5548
|
+
s57Class: "LNDMRK",
|
|
5353
5549
|
attributeKey: "CATLMK",
|
|
5354
5550
|
attributeValue: 16,
|
|
5355
5551
|
renderSize: "32x32",
|
|
@@ -5366,7 +5562,8 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5366
5562
|
getSvg: landmarkDishAerial,
|
|
5367
5563
|
group: "landmarks",
|
|
5368
5564
|
description: "Satellite dish or parabolic antenna visible from the sea.",
|
|
5369
|
-
s52PlRef: "
|
|
5565
|
+
s52PlRef: "DSHAER01",
|
|
5566
|
+
s57Class: "LNDMRK",
|
|
5370
5567
|
attributeKey: "CATLMK",
|
|
5371
5568
|
attributeValue: 4,
|
|
5372
5569
|
renderSize: "32x32",
|
|
@@ -5384,6 +5581,7 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5384
5581
|
group: "landmarks",
|
|
5385
5582
|
description: "Water tower. Elevated tank on supporting structure, conspicuous landmark.",
|
|
5386
5583
|
s52PlRef: "TOWERS12",
|
|
5584
|
+
s57Class: "SILTNK",
|
|
5387
5585
|
attributeKey: "CATSIL",
|
|
5388
5586
|
attributeValue: 4,
|
|
5389
5587
|
renderSize: "32x32",
|
|
@@ -5400,7 +5598,9 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5400
5598
|
getSvg: landmarkRadioTvMast,
|
|
5401
5599
|
group: "landmarks",
|
|
5402
5600
|
description: "Radio or television transmission mast with horizontal cross-arms.",
|
|
5403
|
-
|
|
5601
|
+
// Radio/TV masts are CATLMK 7 (mast) + FUNCTN in S-57, so both mast entries share MSTCON04
|
|
5602
|
+
s52PlRef: "MSTCON04",
|
|
5603
|
+
s57Class: "LNDMRK",
|
|
5404
5604
|
attributeKey: "CATLMK",
|
|
5405
5605
|
attributeValue: 7,
|
|
5406
5606
|
renderSize: "32x32",
|
|
@@ -5417,7 +5617,10 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5417
5617
|
getSvg: landmarkCross,
|
|
5418
5618
|
group: "landmarks",
|
|
5419
5619
|
description: "Religious cross or calvary conspicuous from seaward.",
|
|
5420
|
-
|
|
5620
|
+
// The PL has no dedicated symbol for CATLMK 14 (cross/calvary) — S-52 falls back to the
|
|
5621
|
+
// generic position symbol POSGEN01 (POSGEN03 when conspicuous)
|
|
5622
|
+
s52PlRef: "POSGEN01",
|
|
5623
|
+
s57Class: "LNDMRK",
|
|
5421
5624
|
attributeKey: "CATLMK",
|
|
5422
5625
|
attributeValue: 14,
|
|
5423
5626
|
renderSize: "32x32",
|
|
@@ -5434,9 +5637,11 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5434
5637
|
getSvg: landmarkMast,
|
|
5435
5638
|
group: "landmarks",
|
|
5436
5639
|
description: "General mast (not radio/TV). Pole with spherical top.",
|
|
5437
|
-
s52PlRef: "
|
|
5640
|
+
s52PlRef: "MSTCON04",
|
|
5641
|
+
s57Class: "LNDMRK",
|
|
5438
5642
|
attributeKey: "CATLMK",
|
|
5439
5643
|
attributeValue: 7,
|
|
5644
|
+
// MSTCON14 = conspicuous variant
|
|
5440
5645
|
renderSize: "32x32",
|
|
5441
5646
|
colors: [{ token: "CHBLK", hex: "#000000", usage: "Mast pole, top ball, position circle" }],
|
|
5442
5647
|
lineStyles: [{ element: "Mast pole", width: 2, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
|
|
@@ -5452,6 +5657,7 @@ var NAUTICAL_SYMBOLS = [
|
|
|
5452
5657
|
group: "landmarks",
|
|
5453
5658
|
description: "Silo, grain elevator, or storage tank conspicuous from the sea.",
|
|
5454
5659
|
s52PlRef: "SILBUI01",
|
|
5660
|
+
s57Class: "SILTNK",
|
|
5455
5661
|
attributeKey: "CATSIL",
|
|
5456
5662
|
attributeValue: 1,
|
|
5457
5663
|
renderSize: "32x32",
|
|
@@ -8162,6 +8368,18 @@ var S101_FEATURES = [
|
|
|
8162
8368
|
isNewInS101: false,
|
|
8163
8369
|
attributes: ["categoryOfLandmark", "function", "visuallyConspicuous"]
|
|
8164
8370
|
},
|
|
8371
|
+
{
|
|
8372
|
+
featureClass: "SiloStorageTank",
|
|
8373
|
+
name: "Silo / Storage Tank",
|
|
8374
|
+
s57Equivalent: "SILTNK",
|
|
8375
|
+
section: "E",
|
|
8376
|
+
group: "landmarks",
|
|
8377
|
+
category: "geo",
|
|
8378
|
+
description: "An enclosed structure for storing bulk solids or liquids (silo, grain elevator, water tower, tank). Often conspicuous and useful for position fixing.",
|
|
8379
|
+
portrayalRef: "SILTNK_S101",
|
|
8380
|
+
isNewInS101: false,
|
|
8381
|
+
attributes: ["categoryOfSiloTank", "colour", "visuallyConspicuous"]
|
|
8382
|
+
},
|
|
8165
8383
|
// ── Section D: Bridges ──
|
|
8166
8384
|
{
|
|
8167
8385
|
featureClass: "Bridge",
|
|
@@ -9338,7 +9556,7 @@ var BUILD_MINZOOM = 3;
|
|
|
9338
9556
|
var BUILD_MAXZOOM = 14;
|
|
9339
9557
|
|
|
9340
9558
|
// src/atlas/source-registry.ts
|
|
9341
|
-
var
|
|
9559
|
+
var DEFAULT_TILE_BASE_URL2 = "https://tiles.chrt.co";
|
|
9342
9560
|
var DEFAULT_REGION = "NOAA_ENC";
|
|
9343
9561
|
function buildSources(opts = {}) {
|
|
9344
9562
|
const mode = opts.mode ?? "pmtiles";
|
|
@@ -9351,7 +9569,7 @@ function buildSources(opts = {}) {
|
|
|
9351
9569
|
}
|
|
9352
9570
|
};
|
|
9353
9571
|
}
|
|
9354
|
-
const base = (opts.tileBaseUrl ??
|
|
9572
|
+
const base = (opts.tileBaseUrl ?? DEFAULT_TILE_BASE_URL2).replace(/\/+$/, "");
|
|
9355
9573
|
return {
|
|
9356
9574
|
[region]: {
|
|
9357
9575
|
type: "vector",
|
|
@@ -11078,6 +11296,47 @@ function getStyleStats(style) {
|
|
|
11078
11296
|
};
|
|
11079
11297
|
}
|
|
11080
11298
|
|
|
11299
|
+
// src/pagination.ts
|
|
11300
|
+
async function* paginate(fetchPage, getItems, getTotalCount, options) {
|
|
11301
|
+
const pageSize = options?.pageSize ?? 100;
|
|
11302
|
+
const signal = options?.signal;
|
|
11303
|
+
let offset = 0;
|
|
11304
|
+
while (true) {
|
|
11305
|
+
if (signal?.aborted) return;
|
|
11306
|
+
const response = await fetchPage(offset, signal);
|
|
11307
|
+
const items = getItems(response);
|
|
11308
|
+
for (const item of items) {
|
|
11309
|
+
yield item;
|
|
11310
|
+
}
|
|
11311
|
+
if (items.length === 0) return;
|
|
11312
|
+
offset += items.length;
|
|
11313
|
+
const total = getTotalCount(response);
|
|
11314
|
+
if (total > 0 && offset >= total) return;
|
|
11315
|
+
}
|
|
11316
|
+
}
|
|
11317
|
+
|
|
11318
|
+
// src/webhooks.ts
|
|
11319
|
+
var VALID_TYPES = /* @__PURE__ */ new Set(["cell.updated", "cell.new", "cell.deleted"]);
|
|
11320
|
+
function parseWebhook(body) {
|
|
11321
|
+
if (typeof body !== "object" || body === null) {
|
|
11322
|
+
throw new Error("Invalid webhook payload: expected an object");
|
|
11323
|
+
}
|
|
11324
|
+
const obj = body;
|
|
11325
|
+
if (typeof obj.id !== "string") {
|
|
11326
|
+
throw new Error("Invalid webhook payload: missing 'id'");
|
|
11327
|
+
}
|
|
11328
|
+
if (typeof obj.timestamp !== "string") {
|
|
11329
|
+
throw new Error("Invalid webhook payload: missing 'timestamp'");
|
|
11330
|
+
}
|
|
11331
|
+
if (typeof obj.type !== "string" || !VALID_TYPES.has(obj.type)) {
|
|
11332
|
+
throw new Error(`Invalid webhook payload: unknown type '${String(obj.type)}'`);
|
|
11333
|
+
}
|
|
11334
|
+
if (typeof obj.data !== "object" || obj.data === null) {
|
|
11335
|
+
throw new Error("Invalid webhook payload: missing 'data'");
|
|
11336
|
+
}
|
|
11337
|
+
return obj;
|
|
11338
|
+
}
|
|
11339
|
+
|
|
11081
11340
|
// src/index.ts
|
|
11082
11341
|
var Chrt = class {
|
|
11083
11342
|
/** Tile endpoints — styleUrl, tileUrl, groups */
|
|
@@ -11095,15 +11354,22 @@ var Chrt = class {
|
|
|
11095
11354
|
featuresEp;
|
|
11096
11355
|
queryEp;
|
|
11097
11356
|
http;
|
|
11357
|
+
tileAuth;
|
|
11358
|
+
region;
|
|
11359
|
+
tileBaseUrl;
|
|
11098
11360
|
constructor(apiKeyOrConfig) {
|
|
11099
11361
|
const config = typeof apiKeyOrConfig === "string" ? { apiKey: apiKeyOrConfig } : apiKeyOrConfig;
|
|
11362
|
+
this.region = config.region ?? DEFAULT_TILE_REGION;
|
|
11363
|
+
this.tileBaseUrl = config.tileBaseUrl ?? DEFAULT_TILE_BASE_URL;
|
|
11100
11364
|
this.http = new HttpClient(config.apiKey, config.baseUrl, config.retries);
|
|
11101
|
-
|
|
11365
|
+
const tileOpts = { region: this.region, tileBaseUrl: this.tileBaseUrl };
|
|
11366
|
+
this.tiles = new TilesEndpoint(this.http, tileOpts);
|
|
11102
11367
|
this.cells = new CellsEndpoint(this.http);
|
|
11103
11368
|
this.coverage = new CoverageEndpoint(this.http);
|
|
11104
11369
|
this.featuresEp = new FeaturesEndpoint(this.http);
|
|
11105
11370
|
this.queryEp = new QueryEndpoint(this.http);
|
|
11106
11371
|
this.offline = new OfflineEndpoint(this.http, config.deviceId, config.manifestPath);
|
|
11372
|
+
this.tileAuth = new TileAuth(this.http, tileOpts);
|
|
11107
11373
|
}
|
|
11108
11374
|
/* ── Convenience shortcuts ───────────────────────── */
|
|
11109
11375
|
/** Query GeoJSON features by bounding box */
|
|
@@ -11180,28 +11446,63 @@ var Chrt = class {
|
|
|
11180
11446
|
);
|
|
11181
11447
|
}
|
|
11182
11448
|
/* ── Map integration helpers ─────────────────────── */
|
|
11183
|
-
/**
|
|
11449
|
+
/**
|
|
11450
|
+
* Build a ready-to-use MapLibre / Mapbox GL style object for CHRT's nautical
|
|
11451
|
+
* charts. The default is the full IHO **S-52** portrayal; pick a palette and
|
|
11452
|
+
* tweak rendering via `opts`. The returned object is a standard MapLibre
|
|
11453
|
+
* style — mutate it freely, or build your own from scratch over the CHRT
|
|
11454
|
+
* vector source (see `buildSources`) for a fully custom look.
|
|
11455
|
+
*
|
|
11456
|
+
* Pair with `transformRequest()` so tile requests carry their auth token.
|
|
11457
|
+
*
|
|
11458
|
+
* @example
|
|
11459
|
+
* const map = new maplibregl.Map({
|
|
11460
|
+
* container: 'map',
|
|
11461
|
+
* style: chrt.style({ palette: 'DAY' }),
|
|
11462
|
+
* transformRequest: chrt.transformRequest(),
|
|
11463
|
+
* });
|
|
11464
|
+
*
|
|
11465
|
+
* @remarks S-101 portrayal is on the roadmap; today `style()` emits S-52.
|
|
11466
|
+
*/
|
|
11467
|
+
style(opts = {}) {
|
|
11468
|
+
const { palette = "DAY", ...styleOpts } = opts;
|
|
11469
|
+
return generateStyle(palette, {
|
|
11470
|
+
region: this.region,
|
|
11471
|
+
tileBaseUrl: this.tileBaseUrl,
|
|
11472
|
+
...styleOpts
|
|
11473
|
+
});
|
|
11474
|
+
}
|
|
11475
|
+
/**
|
|
11476
|
+
* @deprecated CHRT serves no remote `style.json`. Use {@link style} to get a
|
|
11477
|
+
* MapLibre style object instead. Throws to avoid silently pointing a map at a
|
|
11478
|
+
* dead URL.
|
|
11479
|
+
*/
|
|
11184
11480
|
styleUrl() {
|
|
11185
11481
|
return this.tiles.styleUrl();
|
|
11186
11482
|
}
|
|
11187
11483
|
/**
|
|
11188
|
-
* Returns a `transformRequest`
|
|
11189
|
-
*
|
|
11484
|
+
* Returns a `transformRequest` for Mapbox GL JS / MapLibre GL JS that mints a
|
|
11485
|
+
* short-lived tile JWT from your API key and attaches it as
|
|
11486
|
+
* `Authorization: Bearer` on requests to `tiles.chrt.co` only. Style, sprite,
|
|
11487
|
+
* and glyph URLs pass through untouched. The cache warms in the background on
|
|
11488
|
+
* first call, so the first tile or two may be retried by the renderer.
|
|
11190
11489
|
*
|
|
11191
11490
|
* @example
|
|
11192
|
-
* const map = new
|
|
11193
|
-
* style: chrt.
|
|
11491
|
+
* const map = new maplibregl.Map({
|
|
11492
|
+
* style: chrt.style(),
|
|
11194
11493
|
* transformRequest: chrt.transformRequest(),
|
|
11195
11494
|
* });
|
|
11196
11495
|
*/
|
|
11197
|
-
transformRequest() {
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
-
|
|
11201
|
-
|
|
11202
|
-
|
|
11203
|
-
|
|
11204
|
-
|
|
11496
|
+
transformRequest(opts) {
|
|
11497
|
+
return this.tileAuth.transformRequest(opts?.region);
|
|
11498
|
+
}
|
|
11499
|
+
/**
|
|
11500
|
+
* Pre-mint the tile token for a region. Optional — `transformRequest()` warms
|
|
11501
|
+
* the cache automatically — but useful if you want to surface a mint error
|
|
11502
|
+
* (e.g. `region_not_licensed`, `tile_quota_exceeded`) before the map loads.
|
|
11503
|
+
*/
|
|
11504
|
+
async warmTiles(region) {
|
|
11505
|
+
await this.tileAuth.getToken(region);
|
|
11205
11506
|
}
|
|
11206
11507
|
};
|
|
11207
11508
|
export {
|
|
@@ -11216,6 +11517,7 @@ export {
|
|
|
11216
11517
|
ChrtError,
|
|
11217
11518
|
DEFAULT_REGION,
|
|
11218
11519
|
DEFAULT_TILE_BASE_URL,
|
|
11520
|
+
DEFAULT_TILE_REGION,
|
|
11219
11521
|
LABELED_LAYERS,
|
|
11220
11522
|
LABEL_BEHAVIOR,
|
|
11221
11523
|
NAUTICAL_SYMBOLS,
|
|
@@ -11233,6 +11535,8 @@ export {
|
|
|
11233
11535
|
S52_NIGHT,
|
|
11234
11536
|
SYMBOL_GROUPS,
|
|
11235
11537
|
SYMBOL_MAP,
|
|
11538
|
+
TileAuth,
|
|
11539
|
+
TileTokenError,
|
|
11236
11540
|
applyColorTheme,
|
|
11237
11541
|
bandFilterExpression,
|
|
11238
11542
|
buildLabelLayer,
|