@chrtco/sdk 0.10.17 → 0.10.19
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/dist/atlas/browser.cjs +29 -1
- package/dist/atlas/browser.cjs.map +1 -1
- package/dist/atlas/browser.d.cts +2 -2
- package/dist/atlas/browser.d.ts +2 -2
- package/dist/atlas/browser.js +28 -1
- package/dist/atlas/browser.js.map +1 -1
- package/dist/index.cjs +1061 -324
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +283 -6
- package/dist/index.d.ts +283 -6
- package/dist/index.js +1056 -324
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -134,7 +134,7 @@ var TileTokenError = class extends Error {
|
|
|
134
134
|
this.status = status;
|
|
135
135
|
}
|
|
136
136
|
};
|
|
137
|
-
var TILE_URL_PATH = /\/v1\/tiles\/[A-Za-z0-9_]+(?:\/[a-z][a-z0-9-]*)?\/\d+\/\d+\/\d+\.mvt(?:\?.*)?$/i;
|
|
137
|
+
var TILE_URL_PATH = /\/v1\/tiles\/[A-Za-z0-9_]+(?:\/[a-z][a-z0-9-]*)?\/\d+\/\d+\/\d+\.(?:mvt|png)(?:\?.*)?$/i;
|
|
138
138
|
var STYLE_URL_PATH = /\/v1\/styles\/[A-Za-z0-9_]+\/style\.json(?:\?.*)?$/i;
|
|
139
139
|
var REFRESH_MARGIN_SECONDS = 30;
|
|
140
140
|
var STALE_GRACE_SECONDS = 1;
|
|
@@ -145,6 +145,15 @@ function nowSeconds() {
|
|
|
145
145
|
function sleep2(ms) {
|
|
146
146
|
return new Promise((r) => setTimeout(r, ms));
|
|
147
147
|
}
|
|
148
|
+
function readBuildTags(raw, region, buildTag) {
|
|
149
|
+
const out = { [region]: buildTag };
|
|
150
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
151
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
152
|
+
if (typeof value === "string" && value) out[key] = value;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
148
157
|
function mapErrorCode(raw, status) {
|
|
149
158
|
const known = [
|
|
150
159
|
"invalid_api_key",
|
|
@@ -202,6 +211,7 @@ var TileAuth = class {
|
|
|
202
211
|
expiresAt: b.expires_at,
|
|
203
212
|
kid: b.kid,
|
|
204
213
|
buildTag: b.build_tag,
|
|
214
|
+
buildTags: readBuildTags(b.build_tags, b.region, b.build_tag),
|
|
205
215
|
region: b.region
|
|
206
216
|
};
|
|
207
217
|
}
|
|
@@ -277,6 +287,22 @@ var TileAuth = class {
|
|
|
277
287
|
if (entry.response.expiresAt - nowSeconds() <= STALE_GRACE_SECONDS) return null;
|
|
278
288
|
return entry.response.token;
|
|
279
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Synchronous peek at the cached mint's per-region build ids — `{}` when the
|
|
292
|
+
* cache is cold.
|
|
293
|
+
*
|
|
294
|
+
* `chrt.style()` is synchronous and cannot mint, so this is how a style
|
|
295
|
+
* generated after the cache is warm picks up the build discriminator without
|
|
296
|
+
* the caller threading it by hand. A cold cache yields `{}`, which produces
|
|
297
|
+
* the undiscriminated URL the SDK emitted before this existed — never an
|
|
298
|
+
* error, and never a wrong build.
|
|
299
|
+
*/
|
|
300
|
+
peekBuildTags(region) {
|
|
301
|
+
const entry = this.cache.get(region ?? this.defaultRegion);
|
|
302
|
+
if (!entry) return {};
|
|
303
|
+
if (entry.response.expiresAt - nowSeconds() <= STALE_GRACE_SECONDS) return {};
|
|
304
|
+
return entry.response.buildTags;
|
|
305
|
+
}
|
|
280
306
|
/**
|
|
281
307
|
* Produce a MapLibre / Mapbox GL `transformRequest`. Attaches
|
|
282
308
|
* `Authorization: Bearer <jwt>` only to CHRT tile URLs and the CHRT
|
|
@@ -330,6 +356,78 @@ var TileAuth = class {
|
|
|
330
356
|
}
|
|
331
357
|
};
|
|
332
358
|
|
|
359
|
+
// src/atlas/tile-zoom.ts
|
|
360
|
+
var BUILD_MINZOOM = 0;
|
|
361
|
+
var BUILD_MAXZOOM = 14;
|
|
362
|
+
|
|
363
|
+
// src/atlas/source-registry.ts
|
|
364
|
+
var LAND_REGION = "CHRT_LAND";
|
|
365
|
+
var RELIEF_REGION = "CHRT_RELIEF";
|
|
366
|
+
var BASE_REGION = "CHRT_BASE";
|
|
367
|
+
var BASE_MINZOOM = 0;
|
|
368
|
+
var BASE_MAXZOOM = 11;
|
|
369
|
+
var RELIEF_MINZOOM = 8;
|
|
370
|
+
var RELIEF_MAXZOOM = 11;
|
|
371
|
+
var RELIEF_TILE_SIZE = 512;
|
|
372
|
+
var BUILD_PARAM = "v";
|
|
373
|
+
function tileTemplate(base, region, extension, buildId) {
|
|
374
|
+
const url = `${base}/v1/tiles/${region}/{z}/{x}/{y}.${extension}`;
|
|
375
|
+
const id = buildId?.trim();
|
|
376
|
+
if (!id) return url;
|
|
377
|
+
return `${url}?${BUILD_PARAM}=${encodeURIComponent(id)}`;
|
|
378
|
+
}
|
|
379
|
+
var DEFAULT_TILE_BASE_URL2 = "https://tiles.chrt.co";
|
|
380
|
+
var DEFAULT_REGION = "NOAA_ENC";
|
|
381
|
+
function buildSources(opts = {}) {
|
|
382
|
+
const mode = opts.mode ?? "pmtiles";
|
|
383
|
+
const region = opts.region ?? DEFAULT_REGION;
|
|
384
|
+
if (mode === "mapbox") {
|
|
385
|
+
return {
|
|
386
|
+
[region]: {
|
|
387
|
+
type: "vector",
|
|
388
|
+
url: `mapbox://chrt.${region.toLowerCase()}`
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
const base = (opts.tileBaseUrl ?? DEFAULT_TILE_BASE_URL2).replace(/\/+$/, "");
|
|
393
|
+
const buildFor = (r) => opts.buildTags?.[r] ?? (r === region ? opts.buildTag : void 0);
|
|
394
|
+
const sources = {
|
|
395
|
+
[region]: {
|
|
396
|
+
type: "vector",
|
|
397
|
+
tiles: [tileTemplate(base, region, "mvt", buildFor(region))],
|
|
398
|
+
minzoom: BUILD_MINZOOM,
|
|
399
|
+
maxzoom: BUILD_MAXZOOM
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
if (opts.land) {
|
|
403
|
+
sources[LAND_REGION] = {
|
|
404
|
+
type: "vector",
|
|
405
|
+
tiles: [tileTemplate(base, LAND_REGION, "mvt", buildFor(LAND_REGION))],
|
|
406
|
+
minzoom: BUILD_MINZOOM,
|
|
407
|
+
maxzoom: BUILD_MAXZOOM
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
if (opts.base) {
|
|
411
|
+
sources[BASE_REGION] = {
|
|
412
|
+
type: "vector",
|
|
413
|
+
tiles: [`${base}/v1/tiles/${BASE_REGION}/{z}/{x}/{y}.mvt`],
|
|
414
|
+
minzoom: BASE_MINZOOM,
|
|
415
|
+
maxzoom: BASE_MAXZOOM
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if (opts.relief) {
|
|
419
|
+
sources[RELIEF_REGION] = {
|
|
420
|
+
type: "raster-dem",
|
|
421
|
+
tiles: [tileTemplate(base, RELIEF_REGION, "png", buildFor(RELIEF_REGION))],
|
|
422
|
+
encoding: "terrarium",
|
|
423
|
+
tileSize: RELIEF_TILE_SIZE,
|
|
424
|
+
minzoom: RELIEF_MINZOOM,
|
|
425
|
+
maxzoom: RELIEF_MAXZOOM
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
return sources;
|
|
429
|
+
}
|
|
430
|
+
|
|
333
431
|
// src/endpoints/tiles.ts
|
|
334
432
|
var TilesEndpoint = class {
|
|
335
433
|
constructor(http, tileAuth, opts) {
|
|
@@ -401,13 +499,20 @@ var TilesEndpoint = class {
|
|
|
401
499
|
return await res.json();
|
|
402
500
|
}
|
|
403
501
|
/**
|
|
404
|
-
* Vector tile URL
|
|
405
|
-
*
|
|
502
|
+
* Vector tile URL for the configured region, served by the tile-worker at
|
|
503
|
+
* `tiles.chrt.co`. Tiles are JWT-gated — pair this with
|
|
406
504
|
* `chrt.transformRequest()`, which attaches the `Authorization: Bearer`
|
|
407
505
|
* header. Prefer `chrt.style()`, which wires the source for you.
|
|
506
|
+
*
|
|
507
|
+
* Carries the build discriminator (`?v=`) once the token cache is warm, so
|
|
508
|
+
* two buildTags never share one cache entry — see `BUILD_PARAM`. A cold cache
|
|
509
|
+
* yields the bare URL, which is equally valid: the server ignores the
|
|
510
|
+
* parameter, and warming is a `chrt.warmTiles()` (or one
|
|
511
|
+
* `chrt.transformRequest()` round-trip) away.
|
|
408
512
|
*/
|
|
409
513
|
tileUrl(z, x, y) {
|
|
410
|
-
|
|
514
|
+
const build = this.tileAuth.peekBuildTags(this.region)[this.region];
|
|
515
|
+
return tileTemplate(this.tileBaseUrl, this.region, "mvt", build).replace("{z}", String(z)).replace("{x}", String(x)).replace("{y}", String(y));
|
|
411
516
|
}
|
|
412
517
|
/**
|
|
413
518
|
* @deprecated Tilesets were consolidated to one PMTiles per region (2026-04);
|
|
@@ -633,6 +738,27 @@ var SearchEndpoint = class {
|
|
|
633
738
|
}
|
|
634
739
|
};
|
|
635
740
|
|
|
741
|
+
// src/endpoints/poi.ts
|
|
742
|
+
var PoiEndpoint = class {
|
|
743
|
+
constructor(http) {
|
|
744
|
+
this.http = http;
|
|
745
|
+
}
|
|
746
|
+
async list(options) {
|
|
747
|
+
return this.http.request(
|
|
748
|
+
"/poi",
|
|
749
|
+
{
|
|
750
|
+
bbox: options.bbox ? options.bbox.join(",") : void 0,
|
|
751
|
+
near: options.near ? `${options.near[0]},${options.near[1]}` : void 0,
|
|
752
|
+
radius_m: options.radiusM,
|
|
753
|
+
category: options.category,
|
|
754
|
+
limit: options.limit,
|
|
755
|
+
offset: options.offset
|
|
756
|
+
},
|
|
757
|
+
options.signal
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
|
|
636
762
|
// src/endpoints/query.ts
|
|
637
763
|
var METRES_PER_NAUTICAL_MILE2 = 1852;
|
|
638
764
|
var QueryEndpoint = class {
|
|
@@ -6153,6 +6279,38 @@ var S52_LOOKUP = {
|
|
|
6153
6279
|
// polygons that overlap the security-zone CTNARE polygons; with both
|
|
6154
6280
|
// as flood fills, the harbor inner waters disappeared under stacked
|
|
6155
6281
|
// magenta. Same boundary-only treatment as CTNARE.
|
|
6282
|
+
// ── DASHED, 2026-08-24. A solid line here read as a second coastline.
|
|
6283
|
+
//
|
|
6284
|
+
// User report: "narragansett bay has 2 offset coastlines... this is so
|
|
6285
|
+
// sloppy. we can't sell this right now." Measured by ablating classes
|
|
6286
|
+
// out of the shipping style and diffing offscreen renders of that exact
|
|
6287
|
+
// frame (z10.54, -71.1745/41.6031): dropping every magenta-painting
|
|
6288
|
+
// class changed 38,580 px and the doubled shoreline vanished; RESARE
|
|
6289
|
+
// ALONE was 37,613 of them — 97.5%. CBLARE, CTNARE, ACHARE and ADMARE
|
|
6290
|
+
// each changed zero.
|
|
6291
|
+
//
|
|
6292
|
+
// Restricted areas in Narragansett Bay follow the shore, so their
|
|
6293
|
+
// boundary runs parallel to the coastline a short distance off it. Drawn
|
|
6294
|
+
// SOLID, in a chart where the coastline is also a solid line, that is a
|
|
6295
|
+
// second coastline to any reader — the geometry is right and the
|
|
6296
|
+
// portrayal lies about what it is.
|
|
6297
|
+
//
|
|
6298
|
+
// This is not a SCAMIN problem, and the first fix attempted was wrong:
|
|
6299
|
+
// the tile pipeline already honours SCAMIN (the z10 tile carries 9
|
|
6300
|
+
// RESARE features, all SCAMIN 1:119,999-1:999,999 — NOAA's own
|
|
6301
|
+
// instruction to draw them at overview scale — while the z11 tile
|
|
6302
|
+
// carries 35). Gating harder would have hidden features the HO asks for.
|
|
6303
|
+
//
|
|
6304
|
+
// Dashed restores the convention this class was already believed to
|
|
6305
|
+
// follow: MIPARE [4,3], TESARE [6,4] and PRDARE [4,2] are all dashed
|
|
6306
|
+
// magenta boundaries, and PRDARE's own comment describes itself as
|
|
6307
|
+
// "matching RESARE/CTNARE/MIPARE/TESARE convention". RESARE was the lone
|
|
6308
|
+
// solid outlier. It also moves toward the S-52 line style for this
|
|
6309
|
+
// class, RESARE51 ("boundary of a restricted area"), a broken magenta
|
|
6310
|
+
// line with inward T-marks — which cannot be used directly yet: the
|
|
6311
|
+
// served sprite (s52-day, 101 entries) carries no RESARE51 pattern, and
|
|
6312
|
+
// a `line-pattern` naming a missing sprite renders NOTHING, which would
|
|
6313
|
+
// turn a portrayal complaint into invisible restricted areas.
|
|
6156
6314
|
objectClass: "RESARE",
|
|
6157
6315
|
name: "Restricted Area",
|
|
6158
6316
|
category: "Regulated Areas",
|
|
@@ -6163,7 +6321,8 @@ var S52_LOOKUP = {
|
|
|
6163
6321
|
paint: {
|
|
6164
6322
|
"line-color": "TRFCD",
|
|
6165
6323
|
"line-width": 1.2,
|
|
6166
|
-
"line-opacity": 0.7
|
|
6324
|
+
"line-opacity": 0.7,
|
|
6325
|
+
"line-dasharray": [4, 3]
|
|
6167
6326
|
}
|
|
6168
6327
|
},
|
|
6169
6328
|
viewingGroup: 26040,
|
|
@@ -6247,7 +6406,13 @@ var S52_LOOKUP = {
|
|
|
6247
6406
|
category: "Regulated Areas",
|
|
6248
6407
|
displayCategory: "OTHER",
|
|
6249
6408
|
int1Ref: "N23",
|
|
6250
|
-
|
|
6409
|
+
// 0.15 painted Hanapepe Bay's dumping ground as a solid land-green wash
|
|
6410
|
+
// over the white deep-water ground — owner-read as "land painting up to
|
|
6411
|
+
// the coastline" (2026-08-31). INT1 N23/N24 portrays a dumping ground
|
|
6412
|
+
// as a dashed limit with a label, not an area tint; 0.04 matches the
|
|
6413
|
+
// TSSLPT/TSSRON faintness so the area reads as WATER with a regulated
|
|
6414
|
+
// overlay, at every tier that shows it.
|
|
6415
|
+
style: { type: "fill", paint: { "fill-color": "LITYW", "fill-opacity": 0.04 } },
|
|
6251
6416
|
viewingGroup: 26230,
|
|
6252
6417
|
scamin: 22e3
|
|
6253
6418
|
},
|
|
@@ -7297,12 +7462,19 @@ var S52_LOOKUP = {
|
|
|
7297
7462
|
// Inherits RESARE (S-57 112) — boundary-only, no flood fill. The stacked
|
|
7298
7463
|
// magenta wash that motivated RESARE's treatment applies here too: an
|
|
7299
7464
|
// inland reach can carry overlapping regulated areas just as a harbour can.
|
|
7465
|
+
// ── DASHED, matching RESARE (2026-08-25). The inland twin was missed
|
|
7466
|
+
// when RESARE was dashed the day before, so USACE inland waters kept
|
|
7467
|
+
// drawing restricted areas as a SOLID magenta line running alongside
|
|
7468
|
+
// the bank — the same "second coastline" a user reported on the coastal
|
|
7469
|
+
// chart, still shipping on the inland one. Found by auditing every
|
|
7470
|
+
// lowercase variant against its NOAA parent: of 14, this was the only
|
|
7471
|
+
// pair that diverged.
|
|
7300
7472
|
objectClass: "resare",
|
|
7301
7473
|
name: "Restricted Area (Inland)",
|
|
7302
7474
|
category: "Regulated Areas",
|
|
7303
7475
|
displayCategory: "STANDARD",
|
|
7304
7476
|
int1Ref: "N2",
|
|
7305
|
-
style: { type: "line", paint: { "line-color": "TRFCD", "line-width": 1.2, "line-opacity": 0.7 } },
|
|
7477
|
+
style: { type: "line", paint: { "line-color": "TRFCD", "line-width": 1.2, "line-opacity": 0.7, "line-dasharray": [4, 3] } },
|
|
7306
7478
|
viewingGroup: 26010,
|
|
7307
7479
|
scamin: 9e4
|
|
7308
7480
|
},
|
|
@@ -25755,7 +25927,7 @@ var S52_POINT_SVGS = {
|
|
|
25755
25927
|
"LIGHTS14": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">\n <g data-chrt-scale="0.8438" transform="translate(16 16) scale(0.8438) translate(-16 -16)">\n <path d="M 16.3 28.0 L 11.8 7.1 L 11.8 6.3 L 11.8 4.9 L 12.4 3.5 L 13.2 3.0 L 14.3 2.1 L 16.0 1.9 L 17.4 2.1 L 18.5 2.4 L 19.6 3.2 L 20.2 4.6 L 20.4 5.7 L 20.4 6.9 L 16.3 28.0 Z" fill="CHMGD" />\n <path d="M 16.3 28.0 L 11.8 7.1 L 11.8 6.3 L 11.8 4.9 L 12.4 3.5 L 13.2 3.0 L 14.3 2.1 L 16.0 1.9 L 17.4 2.1 L 18.5 2.4 L 19.6 3.2 L 20.2 4.6 L 20.4 5.7 L 20.4 6.9 L 16.3 28.0" fill="none" stroke="CHMGD" stroke-width="1.185" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 21.3 5.2 L 21.0 6.6 L 16.3 31.0" fill="none" stroke="CHBLK" stroke-width="1.185" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 16.3 28.0 L 11.5 6.6 L 11.8 5.2 L 12.1 4.1 L 12.9 3.2 L 14.1 2.1 L 15.4 1.9 L 16.3 1.9 L 17.4 2.1 L 18.8 2.7 L 19.6 3.5 L 20.2 4.6 L 20.4 5.7 L 20.2 7.7 L 16.3 28.0" fill="none" stroke="CHMGD" stroke-width="1.185" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 16.3 31.0 L 10.7 6.0 L 11.5 3.5 L 13.8 1.6 L 16.3 1.0 L 18.5 1.9 L 19.9 2.7 L 21.0 4.9" fill="none" stroke="CHBLK" stroke-width="1.185" stroke-linecap="butt" stroke-linejoin="round" />\n </g>\n</svg>\n',
|
|
25756
25928
|
"LIGHTS81": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">\n <g data-chrt-scale="0.3896" transform="translate(16 16) scale(0.3896) translate(-16 -16)">\n <path d="M 1.0 25.9 L 6.9 5.9 L 12.9 26.0 L 19.0 5.9 L 25.0 26.1 L 31.0 5.9" fill="none" stroke="CHMGD" stroke-width="2.567" stroke-linecap="butt" stroke-linejoin="round" />\n </g>\n</svg>\n',
|
|
25757
25929
|
"LIGHTS82": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">\n <g data-chrt-scale="0.6083" transform="translate(16 16) scale(0.6083) translate(-16 -15.98)">\n <circle cx="6.96" cy="17.80" r="5.96" fill="none" stroke="CHMGD" stroke-width="3.288" />\n <path d="M 14.9 23.1 L 25.9 23.5" fill="none" stroke="CHMGD" stroke-width="3.288" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 13.3 12.1 L 28.0 8.2" fill="none" stroke="CHMGD" stroke-width="3.288" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 15.8 17.5 L 31.0 16.0" fill="none" stroke="CHMGD" stroke-width="3.288" stroke-linecap="butt" stroke-linejoin="round" />\n </g>\n</svg>\n',
|
|
25758
|
-
"LITDEF11": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">\n <g data-chrt-scale="0.7292" transform="translate(16 16) scale(0.7292) translate(-16 -
|
|
25930
|
+
"LITDEF11": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">\n <g data-chrt-scale="0.7292" transform="translate(16 16) scale(0.7292) translate(-16.1 -14.95)">\n <g transform="translate(10 10) rotate(135) scale(0.9) translate(-16.3 -31)">\n <path d="M 16.3 31.0 L 10.9 6.9 L 10.9 5.8 L 11.2 4.2 L 12.0 2.8 L 12.8 2.1 L 14.1 1.0 L 16.0 1.0 L 17.6 1.0 L 18.7 1.3 L 20.3 2.3 L 20.8 3.9 L 21.1 5.3 L 21.1 6.6 L 16.3 31.0 Z" fill="CHMGD" />\n <path d="M 16.3 31.0 L 10.9 6.9 L 10.9 5.8 L 11.2 4.2 L 12.0 2.8 L 12.8 2.1 L 14.1 1.0 L 16.0 1.0 L 17.6 1.0 L 18.7 1.3 L 20.3 2.3 L 20.8 3.9 L 21.1 5.3 L 21.1 6.6 L 16.3 31.0" fill="none" stroke="CHMGD" stroke-width="1.371" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 16.3 31.0 L 10.9 6.4 L 10.9 4.7 L 11.4 3.1 L 12.3 2.3 L 13.6 1.3 L 15.2 1.0 L 16.3 1.0 L 17.6 1.0 L 18.9 1.5 L 20.0 2.6 L 20.8 4.2 L 21.1 5.3 L 20.8 7.7 L 16.3 31.0" fill="none" stroke="CHMGD" stroke-width="1.371" stroke-linecap="butt" stroke-linejoin="round" />\n </g>\n </g>\n</svg>\n',
|
|
25759
25931
|
"LITFLT01": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">\n <g data-chrt-scale="0.9375" transform="translate(16 16) scale(0.9375) translate(-16 -15.985)">\n <circle cx="16.00" cy="18.97" r="2.40" fill="none" stroke="CHBLK" stroke-width="2.133" />\n <path d="M 1.0 19.0 L 12.7 19.0" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 6.0 19.0 L 2.7 12.3" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 26.0 19.0 L 29.3 12.3" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 11.0 14.0 L 12.7 10.6 L 19.3 10.6 L 21.0 14.0" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 2.7 12.3 L 9.3 14.0 L 22.7 14.0 L 29.3 12.3" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 19.3 19.0 L 31.0 19.0" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n </g>\n</svg>\n',
|
|
25760
25932
|
"LITFLT02": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">\n <g data-chrt-scale="0.6385" transform="translate(16 16) scale(0.6385) translate(-16 -16)">\n <path d="M 1.0 13.0 L 5.6 21.5 L 26.9 21.5 L 31.0 12.9 Z" fill="CHBLK" />\n <path d="M 1.0 13.0 L 5.6 21.5 L 26.9 21.5 L 31.0 12.9" fill="none" stroke="CHBLK" stroke-width="1.566" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 1.0 12.9 L 5.6 21.5 L 26.9 21.5 L 30.9 12.9 L 1.0 12.9 Z" fill="CHBLK" />\n <path d="M 1.0 12.9 L 5.6 21.5 L 26.9 21.5 L 30.9 12.9 L 1.0 12.9" fill="none" stroke="CHBLK" stroke-width="1.566" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 10.9 12.9 L 10.9 10.5 L 21.2 10.5 L 21.2 12.9" fill="none" stroke="CHBLK" stroke-width="1.566" stroke-linecap="butt" stroke-linejoin="round" />\n </g>\n</svg>\n',
|
|
25761
25933
|
"LITVES01": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">\n <g data-chrt-scale="0.9375" transform="translate(16 16) scale(0.9375) translate(-16 -15.98)">\n <circle cx="16.00" cy="24.83" r="2.33" fill="none" stroke="CHBLK" stroke-width="2.133" />\n <path d="M 1.0 24.8 L 12.7 24.8" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 6.0 24.8 L 2.7 16.5" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 26.0 24.8 L 29.3 16.5" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 16.0 19.0 L 16.0 9.8" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 9.3 19.0 L 9.3 13.0" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 22.7 19.0 L 22.7 13.2" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 16.0 9.8 L 16.0 4.8" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 12.7 6.5 L 19.3 9.8" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 12.7 9.8 L 19.3 6.5" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 4.7 21.5 L 10.2 21.5" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 21.8 21.5 L 27.3 21.5" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 2.7 16.5 L 9.2 19.0 L 16.0 19.0 L 22.7 19.0 L 29.3 16.5" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 19.3 24.8 L 31.0 24.8" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n </g>\n</svg>\n',
|
|
@@ -26270,6 +26442,8 @@ var CHRT_GLYPH_SVGS = {
|
|
|
26270
26442
|
"cardinal-w": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 6 12 1.5h8Z" fill="#101418"/><path d="M16 6.5 20 11h-8Z" fill="#101418"/><path d="M13 12h6l1 13H12Z" fill="#FFC805"/><path d="M6 28h20" stroke="#101418" stroke-width="2.4" stroke-linecap="round"/></svg>',
|
|
26271
26443
|
"daymark-square": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect x="6" y="6" width="20" height="20" rx="1.5" fill="#fff"/></svg>',
|
|
26272
26444
|
"daymark-triangle": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 5 28 26H4Z" fill="#fff"/></svg>',
|
|
26445
|
+
"hatch-nocoverage": '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M-3 9 L9 -3 M-3 21 L21 -3 M3 27 L27 3 M15 27 L27 15" stroke="#8A97A0" stroke-width="0.8" fill="none"/><path d="M-3 15 L15 33 M-3 3 L21 27 M3 -3 L27 21 M15 -3 L27 9" stroke="#8A97A0" stroke-width="0.8" fill="none"/></svg>',
|
|
26446
|
+
"hatch-uncharted": '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M-3 9 L9 -3 M-3 21 L21 -3 M3 27 L27 3 M15 27 L27 15" stroke="#5B7383" stroke-width="0.65" fill="none"/></svg>',
|
|
26273
26447
|
"isolated-danger": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="3.5" r="2.4" fill="#101418"/><circle cx="16" cy="9.5" r="2.4" fill="#101418"/><path d="M13 13h6l1 12H12Z" fill="#101418"/><rect x="12.4" y="17.5" width="7.2" height="3.4" fill="#E5484D"/><path d="M6 28h20" stroke="#101418" stroke-width="2.4" stroke-linecap="round"/></svg>',
|
|
26274
26448
|
"light-flare": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="11" cy="11" r="3.2" fill="#fff"/><path d="M13.4 13.4 24 19.5a2.6 2.6 0 0 1-4.5 4.5L13.4 13.4Z" fill="#fff"/></svg>',
|
|
26275
26449
|
"lightship": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M6 20l3-5h14l3 5-3.5 4h-13Z" fill="#fff"/><path d="M16 15V8" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/><circle cx="16" cy="6" r="2.2" fill="#fff"/><path d="M5 28h22" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
|
|
@@ -26718,59 +26892,6 @@ function iconExpressionForFamily(objectClass, family) {
|
|
|
26718
26892
|
}
|
|
26719
26893
|
}
|
|
26720
26894
|
|
|
26721
|
-
// src/atlas/tile-zoom.ts
|
|
26722
|
-
var BUILD_MINZOOM = 0;
|
|
26723
|
-
var BUILD_MAXZOOM = 14;
|
|
26724
|
-
|
|
26725
|
-
// src/atlas/source-registry.ts
|
|
26726
|
-
var LAND_REGION = "CHRT_LAND";
|
|
26727
|
-
var RELIEF_REGION = "CHRT_RELIEF";
|
|
26728
|
-
var RELIEF_MINZOOM = 8;
|
|
26729
|
-
var RELIEF_MAXZOOM = 13;
|
|
26730
|
-
var RELIEF_TILE_SIZE = 512;
|
|
26731
|
-
var DEFAULT_TILE_BASE_URL2 = "https://tiles.chrt.co";
|
|
26732
|
-
var DEFAULT_REGION = "NOAA_ENC";
|
|
26733
|
-
function buildSources(opts = {}) {
|
|
26734
|
-
const mode = opts.mode ?? "pmtiles";
|
|
26735
|
-
const region = opts.region ?? DEFAULT_REGION;
|
|
26736
|
-
if (mode === "mapbox") {
|
|
26737
|
-
return {
|
|
26738
|
-
[region]: {
|
|
26739
|
-
type: "vector",
|
|
26740
|
-
url: `mapbox://chrt.${region.toLowerCase()}`
|
|
26741
|
-
}
|
|
26742
|
-
};
|
|
26743
|
-
}
|
|
26744
|
-
const base = (opts.tileBaseUrl ?? DEFAULT_TILE_BASE_URL2).replace(/\/+$/, "");
|
|
26745
|
-
const sources = {
|
|
26746
|
-
[region]: {
|
|
26747
|
-
type: "vector",
|
|
26748
|
-
tiles: [`${base}/v1/tiles/${region}/{z}/{x}/{y}.mvt`],
|
|
26749
|
-
minzoom: BUILD_MINZOOM,
|
|
26750
|
-
maxzoom: BUILD_MAXZOOM
|
|
26751
|
-
}
|
|
26752
|
-
};
|
|
26753
|
-
if (opts.land) {
|
|
26754
|
-
sources[LAND_REGION] = {
|
|
26755
|
-
type: "vector",
|
|
26756
|
-
tiles: [`${base}/v1/tiles/${LAND_REGION}/{z}/{x}/{y}.mvt`],
|
|
26757
|
-
minzoom: BUILD_MINZOOM,
|
|
26758
|
-
maxzoom: BUILD_MAXZOOM
|
|
26759
|
-
};
|
|
26760
|
-
}
|
|
26761
|
-
if (opts.relief) {
|
|
26762
|
-
sources[RELIEF_REGION] = {
|
|
26763
|
-
type: "raster-dem",
|
|
26764
|
-
tiles: [`${base}/v1/tiles/${RELIEF_REGION}/{z}/{x}/{y}.png`],
|
|
26765
|
-
encoding: "terrarium",
|
|
26766
|
-
tileSize: RELIEF_TILE_SIZE,
|
|
26767
|
-
minzoom: RELIEF_MINZOOM,
|
|
26768
|
-
maxzoom: RELIEF_MAXZOOM
|
|
26769
|
-
};
|
|
26770
|
-
}
|
|
26771
|
-
return sources;
|
|
26772
|
-
}
|
|
26773
|
-
|
|
26774
26895
|
// src/atlas/layer-builder.ts
|
|
26775
26896
|
var SCAMIN_DECLUTTER_CLASSES = /* @__PURE__ */ new Set([
|
|
26776
26897
|
"OFSPLF",
|
|
@@ -27098,8 +27219,40 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
|
|
|
27098
27219
|
layer.layout = {
|
|
27099
27220
|
...layer.layout,
|
|
27100
27221
|
"icon-allow-overlap": true,
|
|
27101
|
-
"icon-ignore-placement": true
|
|
27222
|
+
"icon-ignore-placement": true,
|
|
27223
|
+
// Flare-specific size (owner ask 2026-08-31: "make the flares a
|
|
27224
|
+
// little smaller — they should scale down a lot as you zoom out").
|
|
27225
|
+
// The safety-critical curve serves buoy/beacon HULLS, whose set
|
|
27226
|
+
// this overrides for LIGHTS only: flares are annotations on a
|
|
27227
|
+
// point, not shapes a mariner ranges against, so they start small
|
|
27228
|
+
// at planning scales and only approach full size at berthing
|
|
27229
|
+
// zooms. Applied after the safety-critical spread so it wins.
|
|
27230
|
+
"icon-size": [
|
|
27231
|
+
"interpolate",
|
|
27232
|
+
["linear"],
|
|
27233
|
+
["zoom"],
|
|
27234
|
+
6,
|
|
27235
|
+
0.3,
|
|
27236
|
+
12,
|
|
27237
|
+
0.42,
|
|
27238
|
+
14,
|
|
27239
|
+
0.6,
|
|
27240
|
+
18,
|
|
27241
|
+
1
|
|
27242
|
+
]
|
|
27243
|
+
};
|
|
27244
|
+
const valnmrNum = ["to-number", ["coalesce", ["get", "VALNMR"], ["get", "valnmr"], 0], 0];
|
|
27245
|
+
const isMajor = [">=", valnmrNum, 10];
|
|
27246
|
+
const majorLayer = {
|
|
27247
|
+
...layer,
|
|
27248
|
+
id: `${layer.id}-major`,
|
|
27249
|
+
filter: layer.filter ? ["all", layer.filter, isMajor] : isMajor,
|
|
27250
|
+
layout: { ...layer.layout },
|
|
27251
|
+
minzoom: 6
|
|
27102
27252
|
};
|
|
27253
|
+
layer.filter = layer.filter ? ["all", layer.filter, ["!", isMajor]] : ["!", isMajor];
|
|
27254
|
+
layer.minzoom = scaminToMinZoom(entry?.scamin) ?? layer.minzoom;
|
|
27255
|
+
layers.push(majorLayer);
|
|
27103
27256
|
}
|
|
27104
27257
|
} else if (resolved.layout) {
|
|
27105
27258
|
layer.layout = resolved.layout;
|
|
@@ -28051,91 +28204,272 @@ function getOrderedClasses() {
|
|
|
28051
28204
|
return ordered;
|
|
28052
28205
|
}
|
|
28053
28206
|
|
|
28054
|
-
// src/atlas/
|
|
28055
|
-
var
|
|
28056
|
-
|
|
28057
|
-
|
|
28058
|
-
|
|
28207
|
+
// src/atlas/band-priority.ts
|
|
28208
|
+
var BAND_PRIORITY_RANGES = {
|
|
28209
|
+
"1": { minzoom: 0 },
|
|
28210
|
+
"2": { minzoom: 5 },
|
|
28211
|
+
"3": { minzoom: 7 },
|
|
28212
|
+
"4": { minzoom: 9 },
|
|
28213
|
+
"5": { minzoom: 11 },
|
|
28214
|
+
"6": { minzoom: 11 }
|
|
28215
|
+
};
|
|
28216
|
+
var BAND_DATA_MINZOOM = {
|
|
28217
|
+
"1": 0,
|
|
28218
|
+
"2": 5,
|
|
28219
|
+
"3": 7,
|
|
28220
|
+
"4": 9,
|
|
28221
|
+
"5": 11,
|
|
28222
|
+
"6": 13
|
|
28223
|
+
};
|
|
28224
|
+
function dominatedVariantMaxzoom(D) {
|
|
28225
|
+
return Math.max(BAND_PRIORITY_RANGES[D].minzoom, BAND_DATA_MINZOOM[D]);
|
|
28059
28226
|
}
|
|
28060
|
-
var
|
|
28061
|
-
|
|
28062
|
-
|
|
28063
|
-
|
|
28064
|
-
|
|
28065
|
-
|
|
28066
|
-
|
|
28067
|
-
|
|
28068
|
-
|
|
28069
|
-
|
|
28070
|
-
|
|
28071
|
-
|
|
28072
|
-
fade: "#e8f2f8",
|
|
28073
|
-
// Raised from 0.45 on 2026-08-23. ⚠ Above the ~0.5 that
|
|
28074
|
-
// hillshade-style.mjs warns "starts competing with depth areas" — a
|
|
28075
|
-
// deliberate, sighted choice made against rendered evidence, not a
|
|
28076
|
-
// default. If soundings or DEPARE ever read as fighting the terrain,
|
|
28077
|
-
// this number is the first thing to walk back.
|
|
28078
|
-
exaggeration: 0.8
|
|
28079
|
-
},
|
|
28080
|
-
DUSK: {
|
|
28081
|
-
base: "#4a4433",
|
|
28082
|
-
cover: { wood: "#414a38", wetland: "#3f4740", urban: "#4e4a42", sand: "#544c39" },
|
|
28083
|
-
contour: "#6b6250",
|
|
28084
|
-
contourIndex: "#837961",
|
|
28085
|
-
road: "#565040",
|
|
28086
|
-
roadMajor: "#63594440",
|
|
28087
|
-
label: "#c3b898",
|
|
28088
|
-
halo: "#2b2820",
|
|
28089
|
-
shadow: "#2e2a20",
|
|
28090
|
-
highlight: "#8a8068",
|
|
28091
|
-
fade: "#16202a",
|
|
28092
|
-
// Unchanged. DUSK was not the palette the 0.8 decision was made against.
|
|
28093
|
-
exaggeration: 0.45
|
|
28094
|
-
},
|
|
28095
|
-
NIGHT: {
|
|
28096
|
-
base: "#2a2620",
|
|
28097
|
-
cover: { wood: "#242a20", wetland: "#232823", urban: "#2c2925", sand: "#302b22" },
|
|
28098
|
-
contour: "#413b30",
|
|
28099
|
-
contourIndex: "#524a3b",
|
|
28100
|
-
road: "#332f27",
|
|
28101
|
-
roadMajor: "#3d382d",
|
|
28102
|
-
label: "#7d745e",
|
|
28103
|
-
halo: "#15130f",
|
|
28104
|
-
shadow: "#1a1712",
|
|
28105
|
-
highlight: "#4a4436",
|
|
28106
|
-
fade: "#07090b",
|
|
28107
|
-
// ⚠ DELIBERATELY NOT RAISED. See the type's note: a strong hillshade
|
|
28108
|
-
// under red night vision is a grey haze over exactly the contrast a
|
|
28109
|
-
// skipper is navigating by. Left at the shipped value rather than
|
|
28110
|
-
// lowered, because lowering it is a separate decision from this one.
|
|
28111
|
-
exaggeration: 0.45
|
|
28227
|
+
var DOMINANCE_EXEMPT_FILL_CLASSES = /* @__PURE__ */ new Set([
|
|
28228
|
+
"LNDARE",
|
|
28229
|
+
"DEPARE",
|
|
28230
|
+
"UNSARE",
|
|
28231
|
+
"LAKARE",
|
|
28232
|
+
"DRGARE",
|
|
28233
|
+
"SEAARE"
|
|
28234
|
+
]);
|
|
28235
|
+
function isExemptOpaqueFill(layer) {
|
|
28236
|
+
if (layer.type !== "fill") return false;
|
|
28237
|
+
for (const cls of DOMINANCE_EXEMPT_FILL_CLASSES) {
|
|
28238
|
+
if (layer.id.startsWith(`s52-${cls}-`)) return true;
|
|
28112
28239
|
}
|
|
28113
|
-
|
|
28114
|
-
|
|
28115
|
-
var
|
|
28116
|
-
|
|
28117
|
-
"
|
|
28118
|
-
|
|
28119
|
-
|
|
28120
|
-
|
|
28121
|
-
|
|
28122
|
-
|
|
28123
|
-
|
|
28124
|
-
|
|
28125
|
-
|
|
28126
|
-
|
|
28127
|
-
var
|
|
28128
|
-
|
|
28129
|
-
|
|
28130
|
-
|
|
28131
|
-
|
|
28132
|
-
|
|
28133
|
-
|
|
28134
|
-
|
|
28135
|
-
|
|
28136
|
-
|
|
28137
|
-
|
|
28138
|
-
|
|
28240
|
+
return false;
|
|
28241
|
+
}
|
|
28242
|
+
var FILL_FLOOR_FREE_BANDS = /* @__PURE__ */ new Set([
|
|
28243
|
+
"1",
|
|
28244
|
+
"2",
|
|
28245
|
+
"3"
|
|
28246
|
+
]);
|
|
28247
|
+
function bandVariantMinzoom(layer, B) {
|
|
28248
|
+
const classMinzoom = layer.minzoom ?? 0;
|
|
28249
|
+
if (isExemptOpaqueFill(layer) && FILL_FLOOR_FREE_BANDS.has(B)) {
|
|
28250
|
+
return classMinzoom;
|
|
28251
|
+
}
|
|
28252
|
+
return Math.max(classMinzoom, BAND_PRIORITY_RANGES[B].minzoom);
|
|
28253
|
+
}
|
|
28254
|
+
var GENERALISED_AREA_FILL_CLASSES = /* @__PURE__ */ new Set([
|
|
28255
|
+
"BUAARE",
|
|
28256
|
+
// 2026-08-28, New York Harbor: the "New York Marine Inspection Zone and
|
|
28257
|
+
// Captain of the Port Zone" RESARE exists at bands 2/3 as a generalized
|
|
28258
|
+
// shore-hugging polygon whose point-sampled dominance tag MISSED (the
|
|
28259
|
+
// Providence-BUAARE failure mode), so its magenta dashed boundary drew
|
|
28260
|
+
// OFFSET over the band-5 detail at z11.5 — read in the field as "a
|
|
28261
|
+
// magenta shoreline bleeding down from a less coarse band". The same
|
|
28262
|
+
// tile carried untagged band-2 ADMARE and CTNARE giants in the same
|
|
28263
|
+
// state. All three are area classes whose coarse-band boundaries hug
|
|
28264
|
+
// shorelines and whose fine-band copies are authoritative.
|
|
28265
|
+
"RESARE",
|
|
28266
|
+
"CTNARE",
|
|
28267
|
+
"ADMARE"
|
|
28268
|
+
]);
|
|
28269
|
+
var GENERALISED_AREA_MAX_BAND = 3;
|
|
28270
|
+
function generalisedAreaCeiling(layer, B) {
|
|
28271
|
+
if (layer.type !== "fill" && layer.type !== "line") return null;
|
|
28272
|
+
if (parseInt(B, 10) > GENERALISED_AREA_MAX_BAND) return null;
|
|
28273
|
+
for (const cls of GENERALISED_AREA_FILL_CLASSES) {
|
|
28274
|
+
if (layer.id.startsWith(`s52-${cls}-`)) return BAND_PRIORITY_RANGES["5"].minzoom;
|
|
28275
|
+
}
|
|
28276
|
+
return null;
|
|
28277
|
+
}
|
|
28278
|
+
var BAND_DRAW_ORDER = ["1", "2", "3", "4", "5", "6"];
|
|
28279
|
+
var CHART_BAND_CODES = {
|
|
28280
|
+
Overview: ["1"],
|
|
28281
|
+
General: ["2"],
|
|
28282
|
+
Coastal: ["3"],
|
|
28283
|
+
Approach: ["4"],
|
|
28284
|
+
Harbor: ["5", "6"]
|
|
28285
|
+
};
|
|
28286
|
+
var CHART_CASCADE_CODES = {
|
|
28287
|
+
Overview: ["1"],
|
|
28288
|
+
General: ["1", "2"],
|
|
28289
|
+
Coastal: ["1", "2", "3"],
|
|
28290
|
+
Approach: ["1", "2", "3", "4"],
|
|
28291
|
+
Harbor: ["1", "2", "3", "4", "5", "6"]
|
|
28292
|
+
};
|
|
28293
|
+
var CHRT_STYLE_MODES = [
|
|
28294
|
+
"continuous",
|
|
28295
|
+
...Object.keys(CHART_BAND_CODES).map(
|
|
28296
|
+
(band) => `chart:${band}`
|
|
28297
|
+
)
|
|
28298
|
+
];
|
|
28299
|
+
function normalizeStyleMode(raw) {
|
|
28300
|
+
if (raw === null || raw === void 0 || raw === "") return "continuous";
|
|
28301
|
+
const lower = raw.toLowerCase();
|
|
28302
|
+
return CHRT_STYLE_MODES.find((mode) => mode.toLowerCase() === lower) ?? null;
|
|
28303
|
+
}
|
|
28304
|
+
function styleModeBand(mode) {
|
|
28305
|
+
return mode === "continuous" ? null : mode.slice("chart:".length);
|
|
28306
|
+
}
|
|
28307
|
+
function bandFilterExpression(codes) {
|
|
28308
|
+
const list = Array.isArray(codes) ? codes : [codes];
|
|
28309
|
+
if (list.length === 1) {
|
|
28310
|
+
return ["==", ["slice", ["get", "_cell_id"], 2, 3], list[0]];
|
|
28311
|
+
}
|
|
28312
|
+
return ["in", ["slice", ["get", "_cell_id"], 2, 3], ["literal", [...list]]];
|
|
28313
|
+
}
|
|
28314
|
+
function combineWithBandFilter(baseFilter, bandFilter) {
|
|
28315
|
+
if (!baseFilter) return bandFilter;
|
|
28316
|
+
if (baseFilter[0] === "all") {
|
|
28317
|
+
return ["all", ...baseFilter.slice(1), bandFilter];
|
|
28318
|
+
}
|
|
28319
|
+
return ["all", baseFilter, bandFilter];
|
|
28320
|
+
}
|
|
28321
|
+
var NOT_DOMINATED_FILTER = [
|
|
28322
|
+
"all",
|
|
28323
|
+
["!", ["has", "_dominated_by_min"]],
|
|
28324
|
+
["!=", ["get", "_dominated"], true]
|
|
28325
|
+
];
|
|
28326
|
+
function dominatedByMinFilter(D) {
|
|
28327
|
+
const dInt = parseInt(D, 10);
|
|
28328
|
+
if (D === "6") {
|
|
28329
|
+
return [
|
|
28330
|
+
"any",
|
|
28331
|
+
["==", ["get", "_dominated_by_min"], dInt],
|
|
28332
|
+
["==", ["get", "_dominated"], true]
|
|
28333
|
+
];
|
|
28334
|
+
}
|
|
28335
|
+
return ["==", ["get", "_dominated_by_min"], dInt];
|
|
28336
|
+
}
|
|
28337
|
+
function withinBboxFilter(bbox) {
|
|
28338
|
+
const [w, s] = bbox.sw;
|
|
28339
|
+
const [e, n] = bbox.ne;
|
|
28340
|
+
return [
|
|
28341
|
+
"within",
|
|
28342
|
+
{
|
|
28343
|
+
type: "Polygon",
|
|
28344
|
+
coordinates: [
|
|
28345
|
+
[
|
|
28346
|
+
[w, s],
|
|
28347
|
+
[e, s],
|
|
28348
|
+
[e, n],
|
|
28349
|
+
[w, n],
|
|
28350
|
+
[w, s]
|
|
28351
|
+
]
|
|
28352
|
+
]
|
|
28353
|
+
}
|
|
28354
|
+
];
|
|
28355
|
+
}
|
|
28356
|
+
function worldMinusBboxPolygon(bbox) {
|
|
28357
|
+
const [w, s] = bbox.sw;
|
|
28358
|
+
const [e, n] = bbox.ne;
|
|
28359
|
+
return {
|
|
28360
|
+
type: "Polygon",
|
|
28361
|
+
coordinates: [
|
|
28362
|
+
[
|
|
28363
|
+
[-180, -85],
|
|
28364
|
+
[180, -85],
|
|
28365
|
+
[180, 85],
|
|
28366
|
+
[-180, 85],
|
|
28367
|
+
[-180, -85]
|
|
28368
|
+
],
|
|
28369
|
+
// Inner ring (hole) — winding opposite to outer ring per GeoJSON
|
|
28370
|
+
// spec. MapLibre is permissive but we follow strict winding.
|
|
28371
|
+
[
|
|
28372
|
+
[w, s],
|
|
28373
|
+
[w, n],
|
|
28374
|
+
[e, n],
|
|
28375
|
+
[e, s],
|
|
28376
|
+
[w, s]
|
|
28377
|
+
]
|
|
28378
|
+
]
|
|
28379
|
+
};
|
|
28380
|
+
}
|
|
28381
|
+
|
|
28382
|
+
// src/atlas/land-layers.ts
|
|
28383
|
+
var LAND_SURFACE_MINZOOM = BAND_PRIORITY_RANGES["5"].minzoom;
|
|
28384
|
+
var RELIEF_MINZOOM2 = 10;
|
|
28385
|
+
var LAND_SURFACE_LAYERS = (id) => id === "chrtland-base" || id === "chrtland-cover" || id === "chrtland-contour" || id.startsWith("chrtland-fade-");
|
|
28386
|
+
var FADE_PX = 80;
|
|
28387
|
+
var FADE_BANDS = 16;
|
|
28388
|
+
function fadeAlpha(r) {
|
|
28389
|
+
return Math.max(0, Math.min(1, (FADE_PX / 2 - Math.abs(r)) / FADE_PX));
|
|
28390
|
+
}
|
|
28391
|
+
function landBaseColor(palette) {
|
|
28392
|
+
return (LAND_THEMES[palette] ?? LAND_THEMES.DAY).base;
|
|
28393
|
+
}
|
|
28394
|
+
var LAND_THEMES = {
|
|
28395
|
+
DAY: {
|
|
28396
|
+
base: "#e8d9b0",
|
|
28397
|
+
cover: { wood: "#cfd8b4", wetland: "#c9d3c4", urban: "#dedad2", sand: "#efe4c2" },
|
|
28398
|
+
contour: "#a89878",
|
|
28399
|
+
contourIndex: "#8a7a5c",
|
|
28400
|
+
road: "#d9c9a4",
|
|
28401
|
+
roadMajor: "#d2bd8e",
|
|
28402
|
+
label: "#5b4f3a",
|
|
28403
|
+
halo: "#f2ead6",
|
|
28404
|
+
shadow: "#6b5a3e",
|
|
28405
|
+
highlight: "#fffaf0",
|
|
28406
|
+
fade: "#e8f2f8",
|
|
28407
|
+
// Raised from 0.45 on 2026-08-23. ⚠ Above the ~0.5 that
|
|
28408
|
+
// hillshade-style.mjs warns "starts competing with depth areas" — a
|
|
28409
|
+
// deliberate, sighted choice made against rendered evidence, not a
|
|
28410
|
+
// default. If soundings or DEPARE ever read as fighting the terrain,
|
|
28411
|
+
// this number is the first thing to walk back.
|
|
28412
|
+
exaggeration: 0.8
|
|
28413
|
+
},
|
|
28414
|
+
DUSK: {
|
|
28415
|
+
base: "#4a4433",
|
|
28416
|
+
cover: { wood: "#414a38", wetland: "#3f4740", urban: "#4e4a42", sand: "#544c39" },
|
|
28417
|
+
contour: "#6b6250",
|
|
28418
|
+
contourIndex: "#837961",
|
|
28419
|
+
road: "#565040",
|
|
28420
|
+
roadMajor: "#63594440",
|
|
28421
|
+
label: "#c3b898",
|
|
28422
|
+
halo: "#2b2820",
|
|
28423
|
+
shadow: "#2e2a20",
|
|
28424
|
+
highlight: "#8a8068",
|
|
28425
|
+
fade: "#16202a",
|
|
28426
|
+
// Unchanged. DUSK was not the palette the 0.8 decision was made against.
|
|
28427
|
+
exaggeration: 0.45
|
|
28428
|
+
},
|
|
28429
|
+
NIGHT: {
|
|
28430
|
+
base: "#2a2620",
|
|
28431
|
+
cover: { wood: "#242a20", wetland: "#232823", urban: "#2c2925", sand: "#302b22" },
|
|
28432
|
+
contour: "#413b30",
|
|
28433
|
+
contourIndex: "#524a3b",
|
|
28434
|
+
road: "#332f27",
|
|
28435
|
+
roadMajor: "#3d382d",
|
|
28436
|
+
label: "#7d745e",
|
|
28437
|
+
halo: "#15130f",
|
|
28438
|
+
shadow: "#1a1712",
|
|
28439
|
+
highlight: "#4a4436",
|
|
28440
|
+
fade: "#07090b",
|
|
28441
|
+
// ⚠ DELIBERATELY NOT RAISED. See the type's note: a strong hillshade
|
|
28442
|
+
// under red night vision is a grey haze over exactly the contrast a
|
|
28443
|
+
// skipper is navigating by. Left at the shipped value rather than
|
|
28444
|
+
// lowered, because lowering it is a separate decision from this one.
|
|
28445
|
+
exaggeration: 0.45
|
|
28446
|
+
}
|
|
28447
|
+
};
|
|
28448
|
+
var FITS = ["<=", ["coalesce", ["get", "min_z"], 0], ["zoom"]];
|
|
28449
|
+
var ANCHOR = ["coalesce", ["get", "anchor"], "top"];
|
|
28450
|
+
var anchorOffset = (em) => [
|
|
28451
|
+
"match",
|
|
28452
|
+
["get", "anchor"],
|
|
28453
|
+
"left",
|
|
28454
|
+
["literal", [0.6, 0]],
|
|
28455
|
+
"right",
|
|
28456
|
+
["literal", [-0.6, 0]],
|
|
28457
|
+
"bottom",
|
|
28458
|
+
["literal", [0, -em]],
|
|
28459
|
+
["literal", [0, em]]
|
|
28460
|
+
];
|
|
28461
|
+
var FONT = ["Open Sans Regular"];
|
|
28462
|
+
var FONT_ITALIC = ["Open Sans Italic"];
|
|
28463
|
+
function landLayers(opts = {}) {
|
|
28464
|
+
const palette = opts.palette ?? "DAY";
|
|
28465
|
+
const T = LAND_THEMES[palette] ?? LAND_THEMES.DAY;
|
|
28466
|
+
const src = opts.sourceId ?? LAND_REGION;
|
|
28467
|
+
const demSrc = opts.reliefSourceId ?? RELIEF_REGION;
|
|
28468
|
+
const L = (o) => o;
|
|
28469
|
+
const layers = [
|
|
28470
|
+
// 1. The land itself — LNDARE ∩ coverage, so it can never outrun the chart.
|
|
28471
|
+
L({
|
|
28472
|
+
id: "chrtland-base",
|
|
28139
28473
|
type: "fill",
|
|
28140
28474
|
source: src,
|
|
28141
28475
|
"source-layer": "land_base",
|
|
@@ -28170,6 +28504,7 @@ function landLayers(opts = {}) {
|
|
|
28170
28504
|
id: "chrtland-hillshade",
|
|
28171
28505
|
type: "hillshade",
|
|
28172
28506
|
source: demSrc,
|
|
28507
|
+
minzoom: RELIEF_MINZOOM2,
|
|
28173
28508
|
paint: {
|
|
28174
28509
|
"hillshade-exaggeration": T.exaggeration,
|
|
28175
28510
|
"hillshade-shadow-color": T.shadow,
|
|
@@ -28221,13 +28556,78 @@ function landLayers(opts = {}) {
|
|
|
28221
28556
|
"line-opacity": 0.8
|
|
28222
28557
|
}
|
|
28223
28558
|
}));
|
|
28559
|
+
layers.push(L({
|
|
28560
|
+
id: "chrtland-city-major",
|
|
28561
|
+
type: "symbol",
|
|
28562
|
+
source: src,
|
|
28563
|
+
"source-layer": "land_town",
|
|
28564
|
+
minzoom: 6.5,
|
|
28565
|
+
filter: [">=", ["coalesce", ["get", "prank"], 0], 10],
|
|
28566
|
+
layout: {
|
|
28567
|
+
"text-field": [
|
|
28568
|
+
"case",
|
|
28569
|
+
["==", ["get", "upcase"], true],
|
|
28570
|
+
["upcase", ["get", "name"]],
|
|
28571
|
+
["get", "name"]
|
|
28572
|
+
],
|
|
28573
|
+
"text-font": FONT,
|
|
28574
|
+
"text-size": [
|
|
28575
|
+
"interpolate",
|
|
28576
|
+
["linear"],
|
|
28577
|
+
["zoom"],
|
|
28578
|
+
6.5,
|
|
28579
|
+
11.5,
|
|
28580
|
+
9,
|
|
28581
|
+
["+", 10, ["*", 0.4, ["coalesce", ["get", "prank"], 10]]],
|
|
28582
|
+
13,
|
|
28583
|
+
["+", 13, ["*", 0.5, ["coalesce", ["get", "prank"], 10]]]
|
|
28584
|
+
],
|
|
28585
|
+
"text-letter-spacing": ["case", ["==", ["get", "upcase"], true], 0.14, 0.02],
|
|
28586
|
+
"text-offset": anchorOffset(0.75),
|
|
28587
|
+
"text-anchor": ANCHOR,
|
|
28588
|
+
"text-padding": 10,
|
|
28589
|
+
"text-allow-overlap": false,
|
|
28590
|
+
"symbol-sort-key": ["*", -1, ["coalesce", ["get", "pop"], 0]]
|
|
28591
|
+
},
|
|
28592
|
+
paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.6 }
|
|
28593
|
+
}));
|
|
28594
|
+
layers.push(L({
|
|
28595
|
+
id: "chrtland-town-port",
|
|
28596
|
+
type: "symbol",
|
|
28597
|
+
source: src,
|
|
28598
|
+
"source-layer": "land_town",
|
|
28599
|
+
minzoom: 8.5,
|
|
28600
|
+
filter: [
|
|
28601
|
+
"all",
|
|
28602
|
+
["==", ["get", "enc_dup"], true],
|
|
28603
|
+
["<", ["coalesce", ["get", "prank"], 0], 10]
|
|
28604
|
+
],
|
|
28605
|
+
layout: {
|
|
28606
|
+
"text-field": ["get", "name"],
|
|
28607
|
+
"text-font": FONT,
|
|
28608
|
+
"text-size": ["interpolate", ["linear"], ["zoom"], 8.5, 11, 12, 13.5],
|
|
28609
|
+
"text-offset": anchorOffset(0.75),
|
|
28610
|
+
"text-anchor": ANCHOR,
|
|
28611
|
+
"text-padding": 8,
|
|
28612
|
+
"text-allow-overlap": false,
|
|
28613
|
+
// Within the tier, the bigger place wins the space: without this the
|
|
28614
|
+
// tie broke on data order and "West Gloucester" beat Gloucester.
|
|
28615
|
+
"symbol-sort-key": ["*", -1, ["coalesce", ["get", "pop"], 0]]
|
|
28616
|
+
},
|
|
28617
|
+
paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.4 }
|
|
28618
|
+
}));
|
|
28224
28619
|
layers.push(L({
|
|
28225
28620
|
id: "chrtland-town",
|
|
28226
28621
|
type: "symbol",
|
|
28227
28622
|
source: src,
|
|
28228
28623
|
"source-layer": "land_town",
|
|
28229
28624
|
minzoom: 9,
|
|
28230
|
-
filter:
|
|
28625
|
+
filter: [
|
|
28626
|
+
"all",
|
|
28627
|
+
FITS,
|
|
28628
|
+
["<", ["coalesce", ["get", "prank"], 0], 10],
|
|
28629
|
+
["!=", ["get", "enc_dup"], true]
|
|
28630
|
+
],
|
|
28231
28631
|
layout: {
|
|
28232
28632
|
"text-field": [
|
|
28233
28633
|
"case",
|
|
@@ -28260,187 +28660,316 @@ function landLayers(opts = {}) {
|
|
|
28260
28660
|
"text-offset": anchorOffset(0.8),
|
|
28261
28661
|
"text-anchor": ANCHOR,
|
|
28262
28662
|
"text-padding": 5,
|
|
28263
|
-
"text-allow-overlap": false,
|
|
28264
|
-
"symbol-sort-key": 3
|
|
28265
|
-
},
|
|
28266
|
-
paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
|
|
28267
|
-
}));
|
|
28268
|
-
layers.push(L({
|
|
28269
|
-
id: "chrtland-poi",
|
|
28270
|
-
type: "symbol",
|
|
28271
|
-
source: src,
|
|
28272
|
-
"source-layer": "land_poi",
|
|
28273
|
-
minzoom: 13,
|
|
28274
|
-
filter: FITS,
|
|
28275
|
-
layout: {
|
|
28276
|
-
"text-field": ["get", "name"],
|
|
28277
|
-
"text-font": FONT_ITALIC,
|
|
28278
|
-
"text-size": ["interpolate", ["linear"], ["zoom"], 13, 8, 14, 9.5],
|
|
28279
|
-
"text-offset": anchorOffset(0.7),
|
|
28280
|
-
"text-anchor": ANCHOR,
|
|
28281
|
-
"text-padding": 5,
|
|
28282
|
-
"text-allow-overlap": false,
|
|
28283
|
-
"symbol-sort-key": 5
|
|
28284
|
-
},
|
|
28285
|
-
paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
|
|
28286
|
-
}));
|
|
28287
|
-
|
|
28288
|
-
|
|
28289
|
-
|
|
28290
|
-
|
|
28291
|
-
var BAND_PRIORITY_RANGES = {
|
|
28292
|
-
"1": { minzoom: 0 },
|
|
28293
|
-
"2": { minzoom: 5 },
|
|
28294
|
-
"3": { minzoom: 7 },
|
|
28295
|
-
"4": { minzoom: 9 },
|
|
28296
|
-
"5": { minzoom: 11 },
|
|
28297
|
-
"6": { minzoom: 11 }
|
|
28298
|
-
};
|
|
28299
|
-
var BAND_DATA_MINZOOM = {
|
|
28300
|
-
"1": 0,
|
|
28301
|
-
"2": 5,
|
|
28302
|
-
"3": 7,
|
|
28303
|
-
"4": 9,
|
|
28304
|
-
"5": 11,
|
|
28305
|
-
"6": 13
|
|
28306
|
-
};
|
|
28307
|
-
function dominatedVariantMaxzoom(D) {
|
|
28308
|
-
return Math.max(BAND_PRIORITY_RANGES[D].minzoom, BAND_DATA_MINZOOM[D]);
|
|
28309
|
-
}
|
|
28310
|
-
var DOMINANCE_EXEMPT_FILL_CLASSES = /* @__PURE__ */ new Set([
|
|
28311
|
-
"LNDARE",
|
|
28312
|
-
"DEPARE",
|
|
28313
|
-
"UNSARE",
|
|
28314
|
-
"LAKARE",
|
|
28315
|
-
"DRGARE",
|
|
28316
|
-
"SEAARE"
|
|
28317
|
-
]);
|
|
28318
|
-
function isExemptOpaqueFill(layer) {
|
|
28319
|
-
if (layer.type !== "fill") return false;
|
|
28320
|
-
for (const cls of DOMINANCE_EXEMPT_FILL_CLASSES) {
|
|
28321
|
-
if (layer.id.startsWith(`s52-${cls}-`)) return true;
|
|
28663
|
+
"text-allow-overlap": false,
|
|
28664
|
+
"symbol-sort-key": 3
|
|
28665
|
+
},
|
|
28666
|
+
paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
|
|
28667
|
+
}));
|
|
28668
|
+
layers.push(L({
|
|
28669
|
+
id: "chrtland-poi",
|
|
28670
|
+
type: "symbol",
|
|
28671
|
+
source: src,
|
|
28672
|
+
"source-layer": "land_poi",
|
|
28673
|
+
minzoom: 13,
|
|
28674
|
+
filter: FITS,
|
|
28675
|
+
layout: {
|
|
28676
|
+
"text-field": ["get", "name"],
|
|
28677
|
+
"text-font": FONT_ITALIC,
|
|
28678
|
+
"text-size": ["interpolate", ["linear"], ["zoom"], 13, 8, 14, 9.5],
|
|
28679
|
+
"text-offset": anchorOffset(0.7),
|
|
28680
|
+
"text-anchor": ANCHOR,
|
|
28681
|
+
"text-padding": 5,
|
|
28682
|
+
"text-allow-overlap": false,
|
|
28683
|
+
"symbol-sort-key": 5
|
|
28684
|
+
},
|
|
28685
|
+
paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
|
|
28686
|
+
}));
|
|
28687
|
+
for (const layer of layers) {
|
|
28688
|
+
const l = layer;
|
|
28689
|
+
if (!LAND_SURFACE_LAYERS(l.id)) continue;
|
|
28690
|
+
l.minzoom = Math.max(l.minzoom ?? 0, LAND_SURFACE_MINZOOM);
|
|
28322
28691
|
}
|
|
28323
|
-
return
|
|
28692
|
+
return layers;
|
|
28324
28693
|
}
|
|
28325
|
-
|
|
28326
|
-
|
|
28327
|
-
|
|
28328
|
-
|
|
28329
|
-
|
|
28330
|
-
|
|
28331
|
-
|
|
28332
|
-
|
|
28333
|
-
|
|
28694
|
+
|
|
28695
|
+
// src/atlas/base-layers.ts
|
|
28696
|
+
var NOCOVERAGE_PATTERN = "chrt:hatch-nocoverage";
|
|
28697
|
+
var UNCHARTED_PATTERN = "chrt:hatch-uncharted";
|
|
28698
|
+
var BASE_THEMES = {
|
|
28699
|
+
DAY: {
|
|
28700
|
+
earth: "#2A3439",
|
|
28701
|
+
water: "#1B2328",
|
|
28702
|
+
river: "#24313A",
|
|
28703
|
+
boundary: "#4A575E",
|
|
28704
|
+
jurisdiction: "#E8D9B0",
|
|
28705
|
+
// CHRTland's own tan — one land colour
|
|
28706
|
+
inlandWater: "#B9CFDD",
|
|
28707
|
+
// muted, unmistakably not a charted blue
|
|
28708
|
+
// State furniture. These three were referenced by the layers but missing
|
|
28709
|
+
// from this table (and its type — the long-standing TS2339 trio), so the
|
|
28710
|
+
// lines shipped on MapLibre's DEFAULT black. Now deliberate: a hairline
|
|
28711
|
+
// a shade deeper than the tan, quiet caps, tan halo.
|
|
28712
|
+
stateLine: "#C4B389",
|
|
28713
|
+
stateLabel: "#8E8468",
|
|
28714
|
+
stateHalo: "#E8D9B0",
|
|
28715
|
+
// Rough-lake furniture (owner ask 2026-08-28: "the lakes need labels,
|
|
28716
|
+
// and shorelines drawn"): a shoreline a shade deeper than the rough
|
|
28717
|
+
// water, and a name in the water's own voice, haloed by the water.
|
|
28718
|
+
inlandShore: "#8FA9BA",
|
|
28719
|
+
inlandLabel: "#54707F",
|
|
28720
|
+
inlandHalo: "#B9CFDD"
|
|
28721
|
+
},
|
|
28722
|
+
DUSK: {
|
|
28723
|
+
earth: "#232B30",
|
|
28724
|
+
water: "#161D21",
|
|
28725
|
+
river: "#1E2930",
|
|
28726
|
+
boundary: "#3E4A50",
|
|
28727
|
+
jurisdiction: "#C9BC96",
|
|
28728
|
+
inlandWater: "#93A9B7",
|
|
28729
|
+
stateLine: "#A6976F",
|
|
28730
|
+
stateLabel: "#776E55",
|
|
28731
|
+
stateHalo: "#C9BC96",
|
|
28732
|
+
inlandShore: "#74899A",
|
|
28733
|
+
inlandLabel: "#41586A",
|
|
28734
|
+
inlandHalo: "#93A9B7"
|
|
28735
|
+
},
|
|
28736
|
+
NIGHT: {
|
|
28737
|
+
earth: "#1A2125",
|
|
28738
|
+
water: "#10161A",
|
|
28739
|
+
river: "#182228",
|
|
28740
|
+
boundary: "#333E44",
|
|
28741
|
+
jurisdiction: "#8F866C",
|
|
28742
|
+
inlandWater: "#5F7280",
|
|
28743
|
+
stateLine: "#6B6350",
|
|
28744
|
+
stateLabel: "#565040",
|
|
28745
|
+
stateHalo: "#8F866C",
|
|
28746
|
+
inlandShore: "#4A5C68",
|
|
28747
|
+
inlandLabel: "#8CA2B0",
|
|
28748
|
+
inlandHalo: "#5F7280"
|
|
28334
28749
|
}
|
|
28335
|
-
return Math.max(classMinzoom, BAND_PRIORITY_RANGES[B].minzoom);
|
|
28336
|
-
}
|
|
28337
|
-
var BAND_DRAW_ORDER = ["1", "2", "3", "4", "5", "6"];
|
|
28338
|
-
var CHART_BAND_CODES = {
|
|
28339
|
-
Overview: ["1"],
|
|
28340
|
-
General: ["2"],
|
|
28341
|
-
Coastal: ["3"],
|
|
28342
|
-
Approach: ["4"],
|
|
28343
|
-
Harbor: ["5", "6"]
|
|
28344
|
-
};
|
|
28345
|
-
var CHART_CASCADE_CODES = {
|
|
28346
|
-
Overview: ["1"],
|
|
28347
|
-
General: ["1", "2"],
|
|
28348
|
-
Coastal: ["1", "2", "3"],
|
|
28349
|
-
Approach: ["1", "2", "3", "4"],
|
|
28350
|
-
Harbor: ["1", "2", "3", "4", "5", "6"]
|
|
28351
28750
|
};
|
|
28352
|
-
|
|
28353
|
-
"
|
|
28354
|
-
|
|
28355
|
-
|
|
28356
|
-
)
|
|
28357
|
-
];
|
|
28358
|
-
function normalizeStyleMode(raw) {
|
|
28359
|
-
if (raw === null || raw === void 0 || raw === "") return "continuous";
|
|
28360
|
-
const lower = raw.toLowerCase();
|
|
28361
|
-
return CHRT_STYLE_MODES.find((mode) => mode.toLowerCase() === lower) ?? null;
|
|
28362
|
-
}
|
|
28363
|
-
function styleModeBand(mode) {
|
|
28364
|
-
return mode === "continuous" ? null : mode.slice("chart:".length);
|
|
28365
|
-
}
|
|
28366
|
-
function bandFilterExpression(codes) {
|
|
28367
|
-
const list = Array.isArray(codes) ? codes : [codes];
|
|
28368
|
-
if (list.length === 1) {
|
|
28369
|
-
return ["==", ["slice", ["get", "_cell_id"], 2, 3], list[0]];
|
|
28370
|
-
}
|
|
28371
|
-
return ["in", ["slice", ["get", "_cell_id"], 2, 3], ["literal", [...list]]];
|
|
28372
|
-
}
|
|
28373
|
-
function combineWithBandFilter(baseFilter, bandFilter) {
|
|
28374
|
-
if (!baseFilter) return bandFilter;
|
|
28375
|
-
if (baseFilter[0] === "all") {
|
|
28376
|
-
return ["all", ...baseFilter.slice(1), bandFilter];
|
|
28377
|
-
}
|
|
28378
|
-
return ["all", baseFilter, bandFilter];
|
|
28379
|
-
}
|
|
28380
|
-
var NOT_DOMINATED_FILTER = [
|
|
28381
|
-
"all",
|
|
28382
|
-
["!", ["has", "_dominated_by_min"]],
|
|
28383
|
-
["!=", ["get", "_dominated"], true]
|
|
28384
|
-
];
|
|
28385
|
-
function dominatedByMinFilter(D) {
|
|
28386
|
-
const dInt = parseInt(D, 10);
|
|
28387
|
-
if (D === "6") {
|
|
28388
|
-
return [
|
|
28389
|
-
"any",
|
|
28390
|
-
["==", ["get", "_dominated_by_min"], dInt],
|
|
28391
|
-
["==", ["get", "_dominated"], true]
|
|
28392
|
-
];
|
|
28393
|
-
}
|
|
28394
|
-
return ["==", ["get", "_dominated_by_min"], dInt];
|
|
28395
|
-
}
|
|
28396
|
-
function withinBboxFilter(bbox) {
|
|
28397
|
-
const [w, s] = bbox.sw;
|
|
28398
|
-
const [e, n] = bbox.ne;
|
|
28751
|
+
function baseLayers(opts = {}) {
|
|
28752
|
+
const palette = opts.palette ?? "DAY";
|
|
28753
|
+
const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
|
|
28754
|
+
const src = opts.sourceId ?? BASE_REGION;
|
|
28755
|
+
const L = (o) => o;
|
|
28399
28756
|
return [
|
|
28400
|
-
|
|
28401
|
-
|
|
28402
|
-
|
|
28403
|
-
|
|
28404
|
-
|
|
28405
|
-
|
|
28406
|
-
|
|
28407
|
-
|
|
28408
|
-
|
|
28409
|
-
|
|
28410
|
-
|
|
28411
|
-
|
|
28412
|
-
|
|
28757
|
+
// 1. Geography — the world as it is, in the quiet dark voice. The OCEAN
|
|
28758
|
+
// is a background, not a polygon: the archive ships land and lakes,
|
|
28759
|
+
// and "everything else is sea" is exactly what a background expresses
|
|
28760
|
+
// — geometry-free, gap-free at every antimeridian and pole. Without
|
|
28761
|
+
// it the sea outside coverage was bare S-52 NODTA, a light grey that
|
|
28762
|
+
// read as "charted ground" with a hatch on it.
|
|
28763
|
+
L({
|
|
28764
|
+
id: "chrtbase-ocean",
|
|
28765
|
+
type: "background",
|
|
28766
|
+
paint: { "background-color": T.water }
|
|
28767
|
+
}),
|
|
28768
|
+
L({
|
|
28769
|
+
id: "chrtbase-earth",
|
|
28770
|
+
type: "fill",
|
|
28771
|
+
source: src,
|
|
28772
|
+
"source-layer": "base_earth",
|
|
28773
|
+
paint: { "fill-color": T.earth }
|
|
28774
|
+
}),
|
|
28775
|
+
L({
|
|
28776
|
+
id: "chrtbase-water",
|
|
28777
|
+
type: "fill",
|
|
28778
|
+
source: src,
|
|
28779
|
+
"source-layer": "base_water",
|
|
28780
|
+
paint: { "fill-color": T.water }
|
|
28781
|
+
}),
|
|
28782
|
+
L({
|
|
28783
|
+
id: "chrtbase-river",
|
|
28784
|
+
type: "line",
|
|
28785
|
+
source: src,
|
|
28786
|
+
"source-layer": "base_river",
|
|
28787
|
+
minzoom: 9,
|
|
28788
|
+
paint: {
|
|
28789
|
+
"line-color": T.river,
|
|
28790
|
+
"line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.6, 14, 1.6]
|
|
28791
|
+
}
|
|
28792
|
+
}),
|
|
28793
|
+
L({
|
|
28794
|
+
id: "chrtbase-boundary",
|
|
28795
|
+
type: "line",
|
|
28796
|
+
source: src,
|
|
28797
|
+
"source-layer": "base_boundary",
|
|
28798
|
+
paint: { "line-color": T.boundary, "line-width": 0.6, "line-opacity": 0.8 }
|
|
28799
|
+
}),
|
|
28800
|
+
// 2. THE DEFAULT: no coverage, everywhere, as a geometry-free background.
|
|
28801
|
+
// Constant opacity by design — see the header. What kills it is being
|
|
28802
|
+
// painted OVER, never fading.
|
|
28803
|
+
L({
|
|
28804
|
+
id: "chrtbase-nocoverage",
|
|
28805
|
+
type: "background",
|
|
28806
|
+
paint: { "background-pattern": NOCOVERAGE_PATTERN, "background-opacity": 0.55 }
|
|
28807
|
+
}),
|
|
28808
|
+
// 3. The jurisdiction: countries whose charts CHRT carries go tan — the
|
|
28809
|
+
// same tan as CHRTland, so "land in our world" is ONE colour. Opaque,
|
|
28810
|
+
// so it takes the crosshatch off its ground entirely.
|
|
28811
|
+
L({
|
|
28812
|
+
id: "chrtbase-jurisdiction",
|
|
28813
|
+
type: "fill",
|
|
28814
|
+
source: src,
|
|
28815
|
+
"source-layer": "jurisdiction",
|
|
28816
|
+
paint: { "fill-color": T.jurisdiction }
|
|
28817
|
+
}),
|
|
28818
|
+
// 4. States — internal boundaries and names, inside the jurisdiction
|
|
28819
|
+
// only (owner decision 2026-08-28). A hairline a shade deeper than the
|
|
28820
|
+
// tan, and quiet letter-spaced caps that fade as harbour detail takes
|
|
28821
|
+
// over. The one deliberate exception to no-names-on-the-basemap:
|
|
28822
|
+
// ADMIN furniture, nothing else — see the fence test.
|
|
28823
|
+
L({
|
|
28824
|
+
id: "chrtbase-state-boundary",
|
|
28825
|
+
type: "line",
|
|
28826
|
+
source: src,
|
|
28827
|
+
"source-layer": "state_boundary",
|
|
28828
|
+
minzoom: 4,
|
|
28829
|
+
paint: {
|
|
28830
|
+
"line-color": T.stateLine,
|
|
28831
|
+
"line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1],
|
|
28832
|
+
"line-dasharray": [3, 2],
|
|
28833
|
+
"line-opacity": 0.8
|
|
28834
|
+
}
|
|
28835
|
+
}),
|
|
28836
|
+
// 5. Country borders, drawn to be HOISTED above the whole chart by the
|
|
28837
|
+
// generator (owner, 2026-08-31: "where there is charted land, can we
|
|
28838
|
+
// add country borders on top of everything? I want it clear where
|
|
28839
|
+
// there is a boarder."). Sourced from NE boundary_lines_land — land
|
|
28840
|
+
// borders only, never coastline — so riding on top cannot stroke a
|
|
28841
|
+
// coast. Dashed so it reads as an administrative line, not chart
|
|
28842
|
+
// linework.
|
|
28843
|
+
L({
|
|
28844
|
+
id: "chrtbase-country-boundary",
|
|
28845
|
+
type: "line",
|
|
28846
|
+
source: src,
|
|
28847
|
+
"source-layer": "country_boundary",
|
|
28848
|
+
paint: {
|
|
28849
|
+
"line-color": T.boundary,
|
|
28850
|
+
"line-width": ["interpolate", ["linear"], ["zoom"], 3, 0.8, 8, 1.4, 12, 2.2],
|
|
28851
|
+
"line-dasharray": [4, 2],
|
|
28852
|
+
"line-opacity": 0.9
|
|
28853
|
+
}
|
|
28854
|
+
}),
|
|
28855
|
+
L({
|
|
28856
|
+
id: "chrtbase-state-label",
|
|
28857
|
+
type: "symbol",
|
|
28858
|
+
source: src,
|
|
28859
|
+
"source-layer": "state_label",
|
|
28860
|
+
minzoom: 4,
|
|
28861
|
+
maxzoom: 10,
|
|
28862
|
+
layout: {
|
|
28863
|
+
"text-field": ["upcase", ["get", "name"]],
|
|
28864
|
+
"text-font": ["Open Sans Regular"],
|
|
28865
|
+
"text-size": ["interpolate", ["linear"], ["zoom"], 4, 9, 8, 12],
|
|
28866
|
+
"text-letter-spacing": 0.25,
|
|
28867
|
+
"text-allow-overlap": false,
|
|
28868
|
+
"text-padding": 8
|
|
28869
|
+
},
|
|
28870
|
+
paint: {
|
|
28871
|
+
"text-color": T.stateLabel,
|
|
28872
|
+
"text-halo-color": T.stateHalo,
|
|
28873
|
+
"text-halo-width": 1.2,
|
|
28874
|
+
"text-opacity": ["interpolate", ["linear"], ["zoom"], 4, 0.7, 8, 0.85, 10, 0.4]
|
|
28875
|
+
}
|
|
28876
|
+
})
|
|
28413
28877
|
];
|
|
28414
28878
|
}
|
|
28415
|
-
function
|
|
28416
|
-
const
|
|
28417
|
-
const [
|
|
28418
|
-
|
|
28419
|
-
|
|
28420
|
-
|
|
28421
|
-
|
|
28422
|
-
|
|
28423
|
-
|
|
28424
|
-
|
|
28425
|
-
|
|
28426
|
-
|
|
28427
|
-
|
|
28428
|
-
|
|
28429
|
-
|
|
28430
|
-
|
|
28431
|
-
|
|
28432
|
-
|
|
28433
|
-
|
|
28434
|
-
|
|
28435
|
-
|
|
28436
|
-
|
|
28437
|
-
|
|
28438
|
-
|
|
28879
|
+
function baseInlandWaterLayers(opts = {}) {
|
|
28880
|
+
const palette = opts.palette ?? "DAY";
|
|
28881
|
+
const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
|
|
28882
|
+
const src = opts.sourceId ?? BASE_REGION;
|
|
28883
|
+
const L = (o) => o;
|
|
28884
|
+
return [
|
|
28885
|
+
L({
|
|
28886
|
+
id: "chrtbase-inland-water",
|
|
28887
|
+
type: "fill",
|
|
28888
|
+
source: src,
|
|
28889
|
+
"source-layer": "inland_water",
|
|
28890
|
+
paint: { "fill-color": T.inlandWater }
|
|
28891
|
+
}),
|
|
28892
|
+
L({
|
|
28893
|
+
id: "chrtbase-inland-hatch",
|
|
28894
|
+
type: "fill",
|
|
28895
|
+
source: src,
|
|
28896
|
+
"source-layer": "inland_water",
|
|
28897
|
+
paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
|
|
28898
|
+
}),
|
|
28899
|
+
// The rough shoreline (owner ask 2026-08-28). Drawn ABOVE the hatch so
|
|
28900
|
+
// the edge stays crisp through the texture, and BELOW the charted-water
|
|
28901
|
+
// repaint clones the generator splices in after it — so wherever the
|
|
28902
|
+
// chart repaints its own water, the rough NE10m line yields to the
|
|
28903
|
+
// chart's COALNE instead of double-striking it.
|
|
28904
|
+
//
|
|
28905
|
+
// NE-tier geometry ONLY (`kind` absent): the planet detail tier joins
|
|
28906
|
+
// the same source-layer from z9 as MANY polygons whose chunk-clip seams
|
|
28907
|
+
// rejoin invisibly as FILLS — but a line layer strokes every seam, and
|
|
28908
|
+
// Winnipesaukee rendered as a mesh of internal edges. Planet features
|
|
28909
|
+
// carry `kind` (the INLAND_KINDS filter's own field); NE's carry only
|
|
28910
|
+
// `name`, so the absence of `kind` IS the tier discriminator.
|
|
28911
|
+
L({
|
|
28912
|
+
id: "chrtbase-inland-shore",
|
|
28913
|
+
type: "line",
|
|
28914
|
+
source: src,
|
|
28915
|
+
"source-layer": "inland_water",
|
|
28916
|
+
filter: ["!", ["has", "kind"]],
|
|
28917
|
+
paint: {
|
|
28918
|
+
"line-color": T.inlandShore,
|
|
28919
|
+
"line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1.1, 14, 1.6],
|
|
28920
|
+
"line-opacity": 0.9
|
|
28921
|
+
}
|
|
28922
|
+
}),
|
|
28923
|
+
// The lake's name (owner ask 2026-08-28) — the second scoped exception
|
|
28924
|
+
// to no-names-on-the-basemap, after the states: WATER-BODY names only.
|
|
28925
|
+
// No maxzoom: an uncharted lake keeps its name at every zoom; over
|
|
28926
|
+
// charted lakes the chart's own SEAARE label takes over at detail zooms
|
|
28927
|
+
// and collision culls the echo.
|
|
28928
|
+
//
|
|
28929
|
+
// The archive's `inland_water_label` anchors (v3+). v3 shipped ONE
|
|
28930
|
+
// PointOnSurface anchor per lake, and a big lake's single anchor pans
|
|
28931
|
+
// off-viewport — Winnipesaukee at z11.7 read nameless with its name two
|
|
28932
|
+
// tiles west of the screen (owner-reported). Labelling the polygons
|
|
28933
|
+
// instead is NOT the fix: tippecanoe's coalescing drops the NE polygon
|
|
28934
|
+
// from some z11 tiles (measured: 11/619/747 carries only planet pieces),
|
|
28935
|
+
// so a polygon-sourced label vanishes tile by tile. v4 therefore bakes a
|
|
28936
|
+
// deterministic anchor GRID into big lakes — the paper-chart convention
|
|
28937
|
+
// of repeating a name along a large water body — and this one layer
|
|
28938
|
+
// renders however many anchors are in view; collision culls the rest.
|
|
28939
|
+
L({
|
|
28940
|
+
id: "chrtbase-inland-label",
|
|
28941
|
+
type: "symbol",
|
|
28942
|
+
source: src,
|
|
28943
|
+
"source-layer": "inland_water_label",
|
|
28944
|
+
minzoom: 5,
|
|
28945
|
+
layout: {
|
|
28946
|
+
"text-field": ["get", "name"],
|
|
28947
|
+
"text-font": ["Open Sans Regular"],
|
|
28948
|
+
"text-size": ["interpolate", ["linear"], ["zoom"], 5, 9.5, 10, 12],
|
|
28949
|
+
"text-max-width": 7,
|
|
28950
|
+
"text-allow-overlap": false,
|
|
28951
|
+
// Zoom-scaled spacing for the anchor GRID: at z8.9 the 0.08-deg
|
|
28952
|
+
// anchors sit ~54 px apart and a flat padding let six copies of
|
|
28953
|
+
// "Lake Ontario" crowd one screen (owner-reported 2026-08-30).
|
|
28954
|
+
// Wide collision boxes at low zoom thin the grid to the repeat-
|
|
28955
|
+
// by-convention cadence; by z12 the anchors are ~850 px apart and
|
|
28956
|
+
// the padding relaxes to normal.
|
|
28957
|
+
"text-padding": ["interpolate", ["linear"], ["zoom"], 5, 80, 9, 64, 12, 16]
|
|
28958
|
+
},
|
|
28959
|
+
paint: {
|
|
28960
|
+
"text-color": T.inlandLabel,
|
|
28961
|
+
"text-halo-color": T.inlandHalo,
|
|
28962
|
+
"text-halo-width": 1.1
|
|
28963
|
+
}
|
|
28964
|
+
})
|
|
28965
|
+
];
|
|
28439
28966
|
}
|
|
28440
28967
|
|
|
28441
28968
|
// src/atlas/band-collapse.ts
|
|
28442
|
-
function collapsedDominanceGate(B, minzoom) {
|
|
28443
|
-
const clauses = [
|
|
28969
|
+
function collapsedDominanceGate(B, minzoom, notDominatedCeiling = null) {
|
|
28970
|
+
const clauses = [
|
|
28971
|
+
notDominatedCeiling !== null && notDominatedCeiling > minzoom ? ["all", NOT_DOMINATED_FILTER, ["<", ["zoom"], notDominatedCeiling]] : NOT_DOMINATED_FILTER
|
|
28972
|
+
];
|
|
28444
28973
|
const bInt = parseInt(B, 10);
|
|
28445
28974
|
for (const D of BAND_DRAW_ORDER) {
|
|
28446
28975
|
const dInt = parseInt(D, 10);
|
|
@@ -28469,7 +28998,7 @@ function makeCollapsedBandVariant(layer, B, isExempt) {
|
|
|
28469
28998
|
filter: combineWithBandFilter(layer.filter, [
|
|
28470
28999
|
"all",
|
|
28471
29000
|
bandFilterExpression(B),
|
|
28472
|
-
collapsedDominanceGate(B, minzoom)
|
|
29001
|
+
collapsedDominanceGate(B, minzoom, generalisedAreaCeiling(layer, B))
|
|
28473
29002
|
]),
|
|
28474
29003
|
minzoom
|
|
28475
29004
|
};
|
|
@@ -28604,6 +29133,7 @@ function makeBandVariants(layer, B) {
|
|
|
28604
29133
|
delete exempt.maxzoom;
|
|
28605
29134
|
return [exempt];
|
|
28606
29135
|
}
|
|
29136
|
+
const ceiling = generalisedAreaCeiling(layer, B);
|
|
28607
29137
|
const notDom = {
|
|
28608
29138
|
...layer,
|
|
28609
29139
|
id: `${layer.id}-band${B}`,
|
|
@@ -28614,6 +29144,7 @@ function makeBandVariants(layer, B) {
|
|
|
28614
29144
|
minzoom
|
|
28615
29145
|
};
|
|
28616
29146
|
delete notDom.maxzoom;
|
|
29147
|
+
if (ceiling !== null && ceiling > minzoom) notDom.maxzoom = ceiling;
|
|
28617
29148
|
variants.push(notDom);
|
|
28618
29149
|
const bInt = parseInt(B, 10);
|
|
28619
29150
|
for (const D of BAND_DRAW_ORDER) {
|
|
@@ -28763,12 +29294,43 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
28763
29294
|
paint: { "background-color": resolveColor("NODTA", palette) }
|
|
28764
29295
|
};
|
|
28765
29296
|
const layers = [backgroundLayer];
|
|
29297
|
+
if (opts.base) {
|
|
29298
|
+
const base = baseLayers({ palette });
|
|
29299
|
+
const coverageGround = {
|
|
29300
|
+
id: "s52-coverage-ground",
|
|
29301
|
+
type: "fill",
|
|
29302
|
+
source: sourceId,
|
|
29303
|
+
"source-layer": "M_COVR",
|
|
29304
|
+
filter: [
|
|
29305
|
+
"all",
|
|
29306
|
+
["any", ["==", ["geometry-type"], "Polygon"], ["==", ["geometry-type"], "MultiPolygon"]],
|
|
29307
|
+
["==", ["to-number", ["coalesce", ["get", "CATCOV"], 0]], 1]
|
|
29308
|
+
],
|
|
29309
|
+
paint: { "fill-color": resolveColor("DEPDW", palette) },
|
|
29310
|
+
metadata: { "s52:objectClass": "M_COVR", "s52:role": "coverage-ground" }
|
|
29311
|
+
};
|
|
29312
|
+
const jurisdictionAt = base.findIndex(
|
|
29313
|
+
(l) => l.id === "chrtbase-jurisdiction"
|
|
29314
|
+
);
|
|
29315
|
+
if (jurisdictionAt === -1) {
|
|
29316
|
+
layers.push(...base, coverageGround);
|
|
29317
|
+
} else {
|
|
29318
|
+
layers.push(
|
|
29319
|
+
...base.slice(0, jurisdictionAt),
|
|
29320
|
+
coverageGround,
|
|
29321
|
+
...base.slice(jurisdictionAt)
|
|
29322
|
+
);
|
|
29323
|
+
}
|
|
29324
|
+
}
|
|
28766
29325
|
if (opts.land) {
|
|
28767
29326
|
layers.push(...landLayers({
|
|
28768
29327
|
palette,
|
|
28769
29328
|
...opts.relief ? { relief: true } : {}
|
|
28770
29329
|
}));
|
|
28771
29330
|
}
|
|
29331
|
+
if (opts.base) {
|
|
29332
|
+
layers.push(...baseInlandWaterLayers({ palette }));
|
|
29333
|
+
}
|
|
28772
29334
|
const labelLayers = [];
|
|
28773
29335
|
const chartCascade = opts.chartMode ? CHART_CASCADE_CODES[opts.chartMode.band] : null;
|
|
28774
29336
|
const chartRequestedCodes = opts.chartMode ? CHART_BAND_CODES[opts.chartMode.band] : null;
|
|
@@ -28885,6 +29447,22 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
28885
29447
|
}
|
|
28886
29448
|
}
|
|
28887
29449
|
}
|
|
29450
|
+
{
|
|
29451
|
+
const TERMINAL_FILL_RE = /^s52-(LNDARE|DEPARE)-fill-band([456])$/;
|
|
29452
|
+
for (let i = layers.length - 1; i >= 0; i--) {
|
|
29453
|
+
const src = layers[i];
|
|
29454
|
+
const m = TERMINAL_FILL_RE.exec(src.id);
|
|
29455
|
+
if (!m) continue;
|
|
29456
|
+
const clone = {
|
|
29457
|
+
...layers[i],
|
|
29458
|
+
id: `s52-${m[1]}-fill-terminal-band${m[2]}`,
|
|
29459
|
+
filter: ["all", src.filter, ["==", ["get", "_terminal_fill"], true]]
|
|
29460
|
+
};
|
|
29461
|
+
delete clone.minzoom;
|
|
29462
|
+
clone.maxzoom = src.minzoom ?? 0;
|
|
29463
|
+
layers.splice(i + 1, 0, clone);
|
|
29464
|
+
}
|
|
29465
|
+
}
|
|
28888
29466
|
if (opts.labels === "placename") {
|
|
28889
29467
|
labelLayers.push(...buildPlaceNameLayers(sourceId));
|
|
28890
29468
|
} else {
|
|
@@ -28914,13 +29492,115 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
28914
29492
|
const symbolSet = opts.symbolSet ?? "s52";
|
|
28915
29493
|
const spriteBase = `${opts.tileBaseUrl ?? "https://tiles.chrt.co"}/v1/sprites`;
|
|
28916
29494
|
const isLndareFill = (l) => /^s52-LNDARE-fill-band\d+$/.test(l.id);
|
|
28917
|
-
const
|
|
29495
|
+
const isWaterBaseFill = (l) => /^s52-(DEPARE|DRGARE|UNSARE)-fill-band\d+$/.test(l.id);
|
|
29496
|
+
const bandOf = (l) => {
|
|
29497
|
+
const m = /-band(\d+)$/.exec(l.id);
|
|
29498
|
+
return m ? Number(m[1]) : 0;
|
|
29499
|
+
};
|
|
28918
29500
|
const emitted = opts.land ? (() => {
|
|
28919
|
-
const
|
|
28920
|
-
const
|
|
28921
|
-
const
|
|
28922
|
-
|
|
29501
|
+
const relocated = layers.filter((l) => isLndareFill(l) || isWaterBaseFill(l));
|
|
29502
|
+
const others = layers.filter((l) => !isLndareFill(l) && !isWaterBaseFill(l));
|
|
29503
|
+
const isSeaareFill = (l) => /^s52-SEAARE-fill-band\d+$/.test(l.id);
|
|
29504
|
+
const seaare = others.filter(isSeaareFill);
|
|
29505
|
+
const rest = others.filter((l) => !isSeaareFill(l));
|
|
29506
|
+
const isBaseLayer = (l) => {
|
|
29507
|
+
const id = l.id;
|
|
29508
|
+
if (id.startsWith("chrtbase-inland-")) return false;
|
|
29509
|
+
return id.startsWith("chrtbase-") || id === "s52-coverage-ground";
|
|
29510
|
+
};
|
|
29511
|
+
const baseBlock = rest.filter(isBaseLayer);
|
|
29512
|
+
const restAbove = rest.filter((l) => !isBaseLayer(l));
|
|
29513
|
+
const bands = [...new Set(relocated.map(bandOf))].sort((a, b) => a - b);
|
|
29514
|
+
const oneTan = landBaseColor(palette);
|
|
29515
|
+
const asBaseTan = (l) => ({
|
|
29516
|
+
...l,
|
|
29517
|
+
paint: {
|
|
29518
|
+
...l.paint ?? {},
|
|
29519
|
+
"fill-color": oneTan
|
|
29520
|
+
}
|
|
29521
|
+
});
|
|
29522
|
+
const ordered = [];
|
|
29523
|
+
for (const b of bands) {
|
|
29524
|
+
ordered.push(...relocated.filter((l) => bandOf(l) === b && isWaterBaseFill(l)));
|
|
29525
|
+
ordered.push(...relocated.filter((l) => bandOf(l) === b && isLndareFill(l)).map(asBaseTan));
|
|
29526
|
+
}
|
|
29527
|
+
const findRest = (id) => restAbove.findIndex((l) => l.id === id);
|
|
29528
|
+
const shoreAt = findRest("chrtbase-inland-shore");
|
|
29529
|
+
const pairAt = shoreAt !== -1 ? shoreAt : findRest("chrtbase-inland-hatch");
|
|
29530
|
+
let restFinal = restAbove;
|
|
29531
|
+
if (pairAt !== -1) {
|
|
29532
|
+
const repaint = [];
|
|
29533
|
+
for (const b of bands) {
|
|
29534
|
+
for (const l of relocated.filter((x) => bandOf(x) === b && isWaterBaseFill(x))) {
|
|
29535
|
+
const src = l;
|
|
29536
|
+
const meta = { ...src.metadata ?? {}, "chrt:role": "charted-water-repaint" };
|
|
29537
|
+
const notDom = {
|
|
29538
|
+
...l,
|
|
29539
|
+
id: `${src.id}-overpair`,
|
|
29540
|
+
filter: ["all", src.filter, NOT_DOMINATED_FILTER],
|
|
29541
|
+
metadata: meta
|
|
29542
|
+
};
|
|
29543
|
+
delete notDom.maxzoom;
|
|
29544
|
+
delete notDom.minzoom;
|
|
29545
|
+
repaint.push(notDom);
|
|
29546
|
+
for (const D of BAND_DRAW_ORDER) {
|
|
29547
|
+
if (parseInt(D, 10) <= b) continue;
|
|
29548
|
+
const dMaxzoom = dominatedVariantMaxzoom(D);
|
|
29549
|
+
if (dMaxzoom <= 0) continue;
|
|
29550
|
+
const domVariant = {
|
|
29551
|
+
...l,
|
|
29552
|
+
id: `${src.id}-overpair-domBy${D}`,
|
|
29553
|
+
filter: ["all", src.filter, dominatedByMinFilter(D)],
|
|
29554
|
+
metadata: meta,
|
|
29555
|
+
maxzoom: dMaxzoom
|
|
29556
|
+
};
|
|
29557
|
+
delete domVariant.minzoom;
|
|
29558
|
+
repaint.push(domVariant);
|
|
29559
|
+
}
|
|
29560
|
+
}
|
|
29561
|
+
}
|
|
29562
|
+
const isCoalneLine = (l) => /^s52-COALNE-line-band\d(-domBy\d)?$/.test(l.id);
|
|
29563
|
+
for (const l of layers.filter(isCoalneLine)) {
|
|
29564
|
+
const src = l;
|
|
29565
|
+
const meta = { ...src.metadata ?? {}, "chrt:role": "charted-water-repaint" };
|
|
29566
|
+
const banded = {
|
|
29567
|
+
...l,
|
|
29568
|
+
id: `${src.id}-overpair`,
|
|
29569
|
+
metadata: meta,
|
|
29570
|
+
minzoom: 9
|
|
29571
|
+
};
|
|
29572
|
+
repaint.push(banded);
|
|
29573
|
+
const terminal = {
|
|
29574
|
+
...l,
|
|
29575
|
+
id: `${src.id}-overpair-terminal`,
|
|
29576
|
+
filter: ["all", src.filter, ["==", ["get", "_terminal_shore"], true]],
|
|
29577
|
+
metadata: meta
|
|
29578
|
+
};
|
|
29579
|
+
delete terminal.minzoom;
|
|
29580
|
+
terminal.maxzoom = 9;
|
|
29581
|
+
repaint.push(terminal);
|
|
29582
|
+
}
|
|
29583
|
+
restFinal = [
|
|
29584
|
+
...restAbove.slice(0, pairAt + 1),
|
|
29585
|
+
...repaint,
|
|
29586
|
+
...restAbove.slice(pairAt + 1)
|
|
29587
|
+
];
|
|
29588
|
+
}
|
|
29589
|
+
return [restFinal[0], ...baseBlock, ...seaare, ...ordered, ...restFinal.slice(1)];
|
|
28923
29590
|
})() : layers;
|
|
29591
|
+
for (const id of [
|
|
29592
|
+
"chrtbase-country-boundary",
|
|
29593
|
+
"chrtland-town-port",
|
|
29594
|
+
"chrtland-city-major"
|
|
29595
|
+
]) {
|
|
29596
|
+
const at = emitted.findIndex(
|
|
29597
|
+
(l) => l.id === id
|
|
29598
|
+
);
|
|
29599
|
+
if (at !== -1) {
|
|
29600
|
+
const [layer] = emitted.splice(at, 1);
|
|
29601
|
+
emitted.push(layer);
|
|
29602
|
+
}
|
|
29603
|
+
}
|
|
28924
29604
|
const style = {
|
|
28925
29605
|
version: 8,
|
|
28926
29606
|
name: `CHRT ${SYMBOL_SET_LABELS[symbolSet]} ${PALETTE_NAMES[palette]}${opts.chartMode ? ` \u2014 chart:${opts.chartMode.band}` : ""}`,
|
|
@@ -28932,6 +29612,7 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
28932
29612
|
"chrt:layer-count": emitted.length,
|
|
28933
29613
|
"chrt:land": Boolean(opts.land),
|
|
28934
29614
|
"chrt:relief": Boolean(opts.relief),
|
|
29615
|
+
"chrt:base": Boolean(opts.base),
|
|
28935
29616
|
"chrt:render-mode": opts.chartMode ? `chart:${opts.chartMode.band}` : "continuous",
|
|
28936
29617
|
"chrt:description": "IHO S-52 portrayal rules for Electronic Navigational Charts (ENCs). Generated from the CHRT atlas engine."
|
|
28937
29618
|
},
|
|
@@ -29425,6 +30106,7 @@ var Chrt = class {
|
|
|
29425
30106
|
interact;
|
|
29426
30107
|
featuresEp;
|
|
29427
30108
|
searchEp;
|
|
30109
|
+
poiEp;
|
|
29428
30110
|
queryEp;
|
|
29429
30111
|
http;
|
|
29430
30112
|
tileAuth;
|
|
@@ -29446,6 +30128,7 @@ var Chrt = class {
|
|
|
29446
30128
|
this.weather = new WeatherEndpoint(this.http);
|
|
29447
30129
|
this.featuresEp = new FeaturesEndpoint(this.http);
|
|
29448
30130
|
this.searchEp = new SearchEndpoint(this.http);
|
|
30131
|
+
this.poiEp = new PoiEndpoint(this.http);
|
|
29449
30132
|
this.queryEp = new QueryEndpoint(this.http);
|
|
29450
30133
|
this.offline = new OfflineEndpoint(this.http, config.deviceId, config.manifestPath);
|
|
29451
30134
|
this.interact = new InteractClient(this.http);
|
|
@@ -29481,6 +30164,26 @@ var Chrt = class {
|
|
|
29481
30164
|
async search(options) {
|
|
29482
30165
|
return this.searchEp.find(options);
|
|
29483
30166
|
}
|
|
30167
|
+
/**
|
|
30168
|
+
* CHRTpoi — charted marine facilities in a viewport or radius.
|
|
30169
|
+
*
|
|
30170
|
+
* v1 serves marinas, derived from the ENC's own HRBFAC facility class —
|
|
30171
|
+
* the chart's data, with the source cell's currency inline per POI.
|
|
30172
|
+
*
|
|
30173
|
+
* ```ts
|
|
30174
|
+
* const { pois, coverage } = await chrt.pois({ bbox: [-70.7, 41.4, -70.5, 41.6] });
|
|
30175
|
+
* ```
|
|
30176
|
+
*
|
|
30177
|
+
* **Read `coverage.state` before interpreting an empty list.** `covered` +
|
|
30178
|
+
* empty means no marinas are charted there — a real answer. `none` means
|
|
30179
|
+
* CHRT cannot answer for that area at all, and an empty list means nothing.
|
|
30180
|
+
* The two must never be shown the same way, and `coverage.message` is
|
|
30181
|
+
* written to be relayed unedited. Metered on its own allowance
|
|
30182
|
+
* (`poi_called`), separate from features and search; not gated by tier.
|
|
30183
|
+
*/
|
|
30184
|
+
async pois(options) {
|
|
30185
|
+
return this.poiEp.list(options);
|
|
30186
|
+
}
|
|
29484
30187
|
/** Spatial proximity query around a point */
|
|
29485
30188
|
async query(options) {
|
|
29486
30189
|
return this.queryEp.nearby(options);
|
|
@@ -29651,6 +30354,18 @@ var Chrt = class {
|
|
|
29651
30354
|
return generateStyle(palette, {
|
|
29652
30355
|
region: this.region,
|
|
29653
30356
|
tileBaseUrl: this.tileBaseUrl,
|
|
30357
|
+
// Pin each source's tile URL to the build it serves, from whatever the
|
|
30358
|
+
// token cache already holds. Without a discriminator one URL is shared
|
|
30359
|
+
// by every build, so after a cutover a map keeps painting already-fetched
|
|
30360
|
+
// tiles from the OLD build beside newly-fetched ones from the new.
|
|
30361
|
+
//
|
|
30362
|
+
// BEFORE the spread, so an explicit `buildTags` wins — and only ever a
|
|
30363
|
+
// warm-cache read: this method is synchronous and must not mint. A cold
|
|
30364
|
+
// cache yields `{}`, i.e. exactly the URLs this SDK emitted before the
|
|
30365
|
+
// discriminator existed. Warm it with `chrt.transformRequest()` (which
|
|
30366
|
+
// kicks off a mint) or `await chrt.styleUrl()`; `await chrt.warmTiles()`
|
|
30367
|
+
// is the explicit form, and it returns the map.
|
|
30368
|
+
buildTags: this.tileAuth.peekBuildTags(this.region),
|
|
29654
30369
|
...styleOpts,
|
|
29655
30370
|
// The vessel's safety depth IS the style's safety contour — same number, same
|
|
29656
30371
|
// meaning. Threading it here is what stops the chart shading at one depth while
|
|
@@ -29724,9 +30439,21 @@ var Chrt = class {
|
|
|
29724
30439
|
* Pre-mint the tile token for a region. Optional — `transformRequest()` warms
|
|
29725
30440
|
* the cache automatically — but useful if you want to surface a mint error
|
|
29726
30441
|
* (e.g. `region_not_licensed`, `tile_quota_exceeded`) before the map loads.
|
|
30442
|
+
*
|
|
30443
|
+
* Returns what it minted, so a caller building its own sources can take the
|
|
30444
|
+
* `buildTags` map straight from here:
|
|
30445
|
+
*
|
|
30446
|
+
* ```ts
|
|
30447
|
+
* const { buildTags } = await chrt.warmTiles();
|
|
30448
|
+
* map.addSource("chrt", buildSources({ region: "NOAA_ENC", buildTags })["NOAA_ENC"]);
|
|
30449
|
+
* ```
|
|
30450
|
+
*
|
|
30451
|
+
* That pins each tile URL to the build it serves, so a build cutover gets its
|
|
30452
|
+
* own client cache namespace instead of leaving old and new tiles mixed on
|
|
30453
|
+
* screen. `chrt.style()` reads the same cache and needs no argument.
|
|
29727
30454
|
*/
|
|
29728
30455
|
async warmTiles(region) {
|
|
29729
|
-
|
|
30456
|
+
return this.tileAuth.getToken(region);
|
|
29730
30457
|
}
|
|
29731
30458
|
};
|
|
29732
30459
|
export {
|
|
@@ -29736,9 +30463,13 @@ export {
|
|
|
29736
30463
|
ATON_EXPRESSION_CLASSES,
|
|
29737
30464
|
BAND_DRAW_ORDER,
|
|
29738
30465
|
BAND_PRIORITY_RANGES,
|
|
30466
|
+
BASE_MAXZOOM,
|
|
30467
|
+
BASE_MINZOOM,
|
|
30468
|
+
BASE_REGION,
|
|
29739
30469
|
BRIDGE_QUERY_LAYERS,
|
|
29740
30470
|
BUILD_MAXZOOM,
|
|
29741
30471
|
BUILD_MINZOOM,
|
|
30472
|
+
BUILD_PARAM,
|
|
29742
30473
|
CHART_BAND_CODES,
|
|
29743
30474
|
CHART_CASCADE_CODES,
|
|
29744
30475
|
CHART_SECTIONS,
|
|
@@ -29920,6 +30651,7 @@ export {
|
|
|
29920
30651
|
scaleSizeValue,
|
|
29921
30652
|
scaminToMinZoom,
|
|
29922
30653
|
styleModeBand,
|
|
30654
|
+
tileTemplate,
|
|
29923
30655
|
withinBboxFilter,
|
|
29924
30656
|
worldMinusBboxPolygon
|
|
29925
30657
|
};
|