@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.cjs
CHANGED
|
@@ -26,9 +26,13 @@ __export(index_exports, {
|
|
|
26
26
|
ATON_EXPRESSION_CLASSES: () => ATON_EXPRESSION_CLASSES,
|
|
27
27
|
BAND_DRAW_ORDER: () => BAND_DRAW_ORDER,
|
|
28
28
|
BAND_PRIORITY_RANGES: () => BAND_PRIORITY_RANGES,
|
|
29
|
+
BASE_MAXZOOM: () => BASE_MAXZOOM,
|
|
30
|
+
BASE_MINZOOM: () => BASE_MINZOOM,
|
|
31
|
+
BASE_REGION: () => BASE_REGION,
|
|
29
32
|
BRIDGE_QUERY_LAYERS: () => BRIDGE_QUERY_LAYERS,
|
|
30
33
|
BUILD_MAXZOOM: () => BUILD_MAXZOOM,
|
|
31
34
|
BUILD_MINZOOM: () => BUILD_MINZOOM,
|
|
35
|
+
BUILD_PARAM: () => BUILD_PARAM,
|
|
32
36
|
CHART_BAND_CODES: () => CHART_BAND_CODES,
|
|
33
37
|
CHART_CASCADE_CODES: () => CHART_CASCADE_CODES,
|
|
34
38
|
CHART_SECTIONS: () => CHART_SECTIONS,
|
|
@@ -210,6 +214,7 @@ __export(index_exports, {
|
|
|
210
214
|
scaleSizeValue: () => scaleSizeValue,
|
|
211
215
|
scaminToMinZoom: () => scaminToMinZoom,
|
|
212
216
|
styleModeBand: () => styleModeBand,
|
|
217
|
+
tileTemplate: () => tileTemplate,
|
|
213
218
|
withinBboxFilter: () => withinBboxFilter,
|
|
214
219
|
worldMinusBboxPolygon: () => worldMinusBboxPolygon
|
|
215
220
|
});
|
|
@@ -351,7 +356,7 @@ var TileTokenError = class extends Error {
|
|
|
351
356
|
this.status = status;
|
|
352
357
|
}
|
|
353
358
|
};
|
|
354
|
-
var TILE_URL_PATH = /\/v1\/tiles\/[A-Za-z0-9_]+(?:\/[a-z][a-z0-9-]*)?\/\d+\/\d+\/\d+\.mvt(?:\?.*)?$/i;
|
|
359
|
+
var TILE_URL_PATH = /\/v1\/tiles\/[A-Za-z0-9_]+(?:\/[a-z][a-z0-9-]*)?\/\d+\/\d+\/\d+\.(?:mvt|png)(?:\?.*)?$/i;
|
|
355
360
|
var STYLE_URL_PATH = /\/v1\/styles\/[A-Za-z0-9_]+\/style\.json(?:\?.*)?$/i;
|
|
356
361
|
var REFRESH_MARGIN_SECONDS = 30;
|
|
357
362
|
var STALE_GRACE_SECONDS = 1;
|
|
@@ -362,6 +367,15 @@ function nowSeconds() {
|
|
|
362
367
|
function sleep2(ms) {
|
|
363
368
|
return new Promise((r) => setTimeout(r, ms));
|
|
364
369
|
}
|
|
370
|
+
function readBuildTags(raw, region, buildTag) {
|
|
371
|
+
const out = { [region]: buildTag };
|
|
372
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
373
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
374
|
+
if (typeof value === "string" && value) out[key] = value;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
365
379
|
function mapErrorCode(raw, status) {
|
|
366
380
|
const known = [
|
|
367
381
|
"invalid_api_key",
|
|
@@ -419,6 +433,7 @@ var TileAuth = class {
|
|
|
419
433
|
expiresAt: b.expires_at,
|
|
420
434
|
kid: b.kid,
|
|
421
435
|
buildTag: b.build_tag,
|
|
436
|
+
buildTags: readBuildTags(b.build_tags, b.region, b.build_tag),
|
|
422
437
|
region: b.region
|
|
423
438
|
};
|
|
424
439
|
}
|
|
@@ -494,6 +509,22 @@ var TileAuth = class {
|
|
|
494
509
|
if (entry.response.expiresAt - nowSeconds() <= STALE_GRACE_SECONDS) return null;
|
|
495
510
|
return entry.response.token;
|
|
496
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* Synchronous peek at the cached mint's per-region build ids — `{}` when the
|
|
514
|
+
* cache is cold.
|
|
515
|
+
*
|
|
516
|
+
* `chrt.style()` is synchronous and cannot mint, so this is how a style
|
|
517
|
+
* generated after the cache is warm picks up the build discriminator without
|
|
518
|
+
* the caller threading it by hand. A cold cache yields `{}`, which produces
|
|
519
|
+
* the undiscriminated URL the SDK emitted before this existed — never an
|
|
520
|
+
* error, and never a wrong build.
|
|
521
|
+
*/
|
|
522
|
+
peekBuildTags(region) {
|
|
523
|
+
const entry = this.cache.get(region ?? this.defaultRegion);
|
|
524
|
+
if (!entry) return {};
|
|
525
|
+
if (entry.response.expiresAt - nowSeconds() <= STALE_GRACE_SECONDS) return {};
|
|
526
|
+
return entry.response.buildTags;
|
|
527
|
+
}
|
|
497
528
|
/**
|
|
498
529
|
* Produce a MapLibre / Mapbox GL `transformRequest`. Attaches
|
|
499
530
|
* `Authorization: Bearer <jwt>` only to CHRT tile URLs and the CHRT
|
|
@@ -547,6 +578,78 @@ var TileAuth = class {
|
|
|
547
578
|
}
|
|
548
579
|
};
|
|
549
580
|
|
|
581
|
+
// src/atlas/tile-zoom.ts
|
|
582
|
+
var BUILD_MINZOOM = 0;
|
|
583
|
+
var BUILD_MAXZOOM = 14;
|
|
584
|
+
|
|
585
|
+
// src/atlas/source-registry.ts
|
|
586
|
+
var LAND_REGION = "CHRT_LAND";
|
|
587
|
+
var RELIEF_REGION = "CHRT_RELIEF";
|
|
588
|
+
var BASE_REGION = "CHRT_BASE";
|
|
589
|
+
var BASE_MINZOOM = 0;
|
|
590
|
+
var BASE_MAXZOOM = 11;
|
|
591
|
+
var RELIEF_MINZOOM = 8;
|
|
592
|
+
var RELIEF_MAXZOOM = 11;
|
|
593
|
+
var RELIEF_TILE_SIZE = 512;
|
|
594
|
+
var BUILD_PARAM = "v";
|
|
595
|
+
function tileTemplate(base, region, extension, buildId) {
|
|
596
|
+
const url = `${base}/v1/tiles/${region}/{z}/{x}/{y}.${extension}`;
|
|
597
|
+
const id = buildId?.trim();
|
|
598
|
+
if (!id) return url;
|
|
599
|
+
return `${url}?${BUILD_PARAM}=${encodeURIComponent(id)}`;
|
|
600
|
+
}
|
|
601
|
+
var DEFAULT_TILE_BASE_URL2 = "https://tiles.chrt.co";
|
|
602
|
+
var DEFAULT_REGION = "NOAA_ENC";
|
|
603
|
+
function buildSources(opts = {}) {
|
|
604
|
+
const mode = opts.mode ?? "pmtiles";
|
|
605
|
+
const region = opts.region ?? DEFAULT_REGION;
|
|
606
|
+
if (mode === "mapbox") {
|
|
607
|
+
return {
|
|
608
|
+
[region]: {
|
|
609
|
+
type: "vector",
|
|
610
|
+
url: `mapbox://chrt.${region.toLowerCase()}`
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
const base = (opts.tileBaseUrl ?? DEFAULT_TILE_BASE_URL2).replace(/\/+$/, "");
|
|
615
|
+
const buildFor = (r) => opts.buildTags?.[r] ?? (r === region ? opts.buildTag : void 0);
|
|
616
|
+
const sources = {
|
|
617
|
+
[region]: {
|
|
618
|
+
type: "vector",
|
|
619
|
+
tiles: [tileTemplate(base, region, "mvt", buildFor(region))],
|
|
620
|
+
minzoom: BUILD_MINZOOM,
|
|
621
|
+
maxzoom: BUILD_MAXZOOM
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
if (opts.land) {
|
|
625
|
+
sources[LAND_REGION] = {
|
|
626
|
+
type: "vector",
|
|
627
|
+
tiles: [tileTemplate(base, LAND_REGION, "mvt", buildFor(LAND_REGION))],
|
|
628
|
+
minzoom: BUILD_MINZOOM,
|
|
629
|
+
maxzoom: BUILD_MAXZOOM
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
if (opts.base) {
|
|
633
|
+
sources[BASE_REGION] = {
|
|
634
|
+
type: "vector",
|
|
635
|
+
tiles: [`${base}/v1/tiles/${BASE_REGION}/{z}/{x}/{y}.mvt`],
|
|
636
|
+
minzoom: BASE_MINZOOM,
|
|
637
|
+
maxzoom: BASE_MAXZOOM
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
if (opts.relief) {
|
|
641
|
+
sources[RELIEF_REGION] = {
|
|
642
|
+
type: "raster-dem",
|
|
643
|
+
tiles: [tileTemplate(base, RELIEF_REGION, "png", buildFor(RELIEF_REGION))],
|
|
644
|
+
encoding: "terrarium",
|
|
645
|
+
tileSize: RELIEF_TILE_SIZE,
|
|
646
|
+
minzoom: RELIEF_MINZOOM,
|
|
647
|
+
maxzoom: RELIEF_MAXZOOM
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
return sources;
|
|
651
|
+
}
|
|
652
|
+
|
|
550
653
|
// src/endpoints/tiles.ts
|
|
551
654
|
var TilesEndpoint = class {
|
|
552
655
|
constructor(http, tileAuth, opts) {
|
|
@@ -618,13 +721,20 @@ var TilesEndpoint = class {
|
|
|
618
721
|
return await res.json();
|
|
619
722
|
}
|
|
620
723
|
/**
|
|
621
|
-
* Vector tile URL
|
|
622
|
-
*
|
|
724
|
+
* Vector tile URL for the configured region, served by the tile-worker at
|
|
725
|
+
* `tiles.chrt.co`. Tiles are JWT-gated — pair this with
|
|
623
726
|
* `chrt.transformRequest()`, which attaches the `Authorization: Bearer`
|
|
624
727
|
* header. Prefer `chrt.style()`, which wires the source for you.
|
|
728
|
+
*
|
|
729
|
+
* Carries the build discriminator (`?v=`) once the token cache is warm, so
|
|
730
|
+
* two buildTags never share one cache entry — see `BUILD_PARAM`. A cold cache
|
|
731
|
+
* yields the bare URL, which is equally valid: the server ignores the
|
|
732
|
+
* parameter, and warming is a `chrt.warmTiles()` (or one
|
|
733
|
+
* `chrt.transformRequest()` round-trip) away.
|
|
625
734
|
*/
|
|
626
735
|
tileUrl(z, x, y) {
|
|
627
|
-
|
|
736
|
+
const build = this.tileAuth.peekBuildTags(this.region)[this.region];
|
|
737
|
+
return tileTemplate(this.tileBaseUrl, this.region, "mvt", build).replace("{z}", String(z)).replace("{x}", String(x)).replace("{y}", String(y));
|
|
628
738
|
}
|
|
629
739
|
/**
|
|
630
740
|
* @deprecated Tilesets were consolidated to one PMTiles per region (2026-04);
|
|
@@ -850,6 +960,27 @@ var SearchEndpoint = class {
|
|
|
850
960
|
}
|
|
851
961
|
};
|
|
852
962
|
|
|
963
|
+
// src/endpoints/poi.ts
|
|
964
|
+
var PoiEndpoint = class {
|
|
965
|
+
constructor(http) {
|
|
966
|
+
this.http = http;
|
|
967
|
+
}
|
|
968
|
+
async list(options) {
|
|
969
|
+
return this.http.request(
|
|
970
|
+
"/poi",
|
|
971
|
+
{
|
|
972
|
+
bbox: options.bbox ? options.bbox.join(",") : void 0,
|
|
973
|
+
near: options.near ? `${options.near[0]},${options.near[1]}` : void 0,
|
|
974
|
+
radius_m: options.radiusM,
|
|
975
|
+
category: options.category,
|
|
976
|
+
limit: options.limit,
|
|
977
|
+
offset: options.offset
|
|
978
|
+
},
|
|
979
|
+
options.signal
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
|
|
853
984
|
// src/endpoints/query.ts
|
|
854
985
|
var METRES_PER_NAUTICAL_MILE2 = 1852;
|
|
855
986
|
var QueryEndpoint = class {
|
|
@@ -6370,6 +6501,38 @@ var S52_LOOKUP = {
|
|
|
6370
6501
|
// polygons that overlap the security-zone CTNARE polygons; with both
|
|
6371
6502
|
// as flood fills, the harbor inner waters disappeared under stacked
|
|
6372
6503
|
// magenta. Same boundary-only treatment as CTNARE.
|
|
6504
|
+
// ── DASHED, 2026-08-24. A solid line here read as a second coastline.
|
|
6505
|
+
//
|
|
6506
|
+
// User report: "narragansett bay has 2 offset coastlines... this is so
|
|
6507
|
+
// sloppy. we can't sell this right now." Measured by ablating classes
|
|
6508
|
+
// out of the shipping style and diffing offscreen renders of that exact
|
|
6509
|
+
// frame (z10.54, -71.1745/41.6031): dropping every magenta-painting
|
|
6510
|
+
// class changed 38,580 px and the doubled shoreline vanished; RESARE
|
|
6511
|
+
// ALONE was 37,613 of them — 97.5%. CBLARE, CTNARE, ACHARE and ADMARE
|
|
6512
|
+
// each changed zero.
|
|
6513
|
+
//
|
|
6514
|
+
// Restricted areas in Narragansett Bay follow the shore, so their
|
|
6515
|
+
// boundary runs parallel to the coastline a short distance off it. Drawn
|
|
6516
|
+
// SOLID, in a chart where the coastline is also a solid line, that is a
|
|
6517
|
+
// second coastline to any reader — the geometry is right and the
|
|
6518
|
+
// portrayal lies about what it is.
|
|
6519
|
+
//
|
|
6520
|
+
// This is not a SCAMIN problem, and the first fix attempted was wrong:
|
|
6521
|
+
// the tile pipeline already honours SCAMIN (the z10 tile carries 9
|
|
6522
|
+
// RESARE features, all SCAMIN 1:119,999-1:999,999 — NOAA's own
|
|
6523
|
+
// instruction to draw them at overview scale — while the z11 tile
|
|
6524
|
+
// carries 35). Gating harder would have hidden features the HO asks for.
|
|
6525
|
+
//
|
|
6526
|
+
// Dashed restores the convention this class was already believed to
|
|
6527
|
+
// follow: MIPARE [4,3], TESARE [6,4] and PRDARE [4,2] are all dashed
|
|
6528
|
+
// magenta boundaries, and PRDARE's own comment describes itself as
|
|
6529
|
+
// "matching RESARE/CTNARE/MIPARE/TESARE convention". RESARE was the lone
|
|
6530
|
+
// solid outlier. It also moves toward the S-52 line style for this
|
|
6531
|
+
// class, RESARE51 ("boundary of a restricted area"), a broken magenta
|
|
6532
|
+
// line with inward T-marks — which cannot be used directly yet: the
|
|
6533
|
+
// served sprite (s52-day, 101 entries) carries no RESARE51 pattern, and
|
|
6534
|
+
// a `line-pattern` naming a missing sprite renders NOTHING, which would
|
|
6535
|
+
// turn a portrayal complaint into invisible restricted areas.
|
|
6373
6536
|
objectClass: "RESARE",
|
|
6374
6537
|
name: "Restricted Area",
|
|
6375
6538
|
category: "Regulated Areas",
|
|
@@ -6380,7 +6543,8 @@ var S52_LOOKUP = {
|
|
|
6380
6543
|
paint: {
|
|
6381
6544
|
"line-color": "TRFCD",
|
|
6382
6545
|
"line-width": 1.2,
|
|
6383
|
-
"line-opacity": 0.7
|
|
6546
|
+
"line-opacity": 0.7,
|
|
6547
|
+
"line-dasharray": [4, 3]
|
|
6384
6548
|
}
|
|
6385
6549
|
},
|
|
6386
6550
|
viewingGroup: 26040,
|
|
@@ -6464,7 +6628,13 @@ var S52_LOOKUP = {
|
|
|
6464
6628
|
category: "Regulated Areas",
|
|
6465
6629
|
displayCategory: "OTHER",
|
|
6466
6630
|
int1Ref: "N23",
|
|
6467
|
-
|
|
6631
|
+
// 0.15 painted Hanapepe Bay's dumping ground as a solid land-green wash
|
|
6632
|
+
// over the white deep-water ground — owner-read as "land painting up to
|
|
6633
|
+
// the coastline" (2026-08-31). INT1 N23/N24 portrays a dumping ground
|
|
6634
|
+
// as a dashed limit with a label, not an area tint; 0.04 matches the
|
|
6635
|
+
// TSSLPT/TSSRON faintness so the area reads as WATER with a regulated
|
|
6636
|
+
// overlay, at every tier that shows it.
|
|
6637
|
+
style: { type: "fill", paint: { "fill-color": "LITYW", "fill-opacity": 0.04 } },
|
|
6468
6638
|
viewingGroup: 26230,
|
|
6469
6639
|
scamin: 22e3
|
|
6470
6640
|
},
|
|
@@ -7514,12 +7684,19 @@ var S52_LOOKUP = {
|
|
|
7514
7684
|
// Inherits RESARE (S-57 112) — boundary-only, no flood fill. The stacked
|
|
7515
7685
|
// magenta wash that motivated RESARE's treatment applies here too: an
|
|
7516
7686
|
// inland reach can carry overlapping regulated areas just as a harbour can.
|
|
7687
|
+
// ── DASHED, matching RESARE (2026-08-25). The inland twin was missed
|
|
7688
|
+
// when RESARE was dashed the day before, so USACE inland waters kept
|
|
7689
|
+
// drawing restricted areas as a SOLID magenta line running alongside
|
|
7690
|
+
// the bank — the same "second coastline" a user reported on the coastal
|
|
7691
|
+
// chart, still shipping on the inland one. Found by auditing every
|
|
7692
|
+
// lowercase variant against its NOAA parent: of 14, this was the only
|
|
7693
|
+
// pair that diverged.
|
|
7517
7694
|
objectClass: "resare",
|
|
7518
7695
|
name: "Restricted Area (Inland)",
|
|
7519
7696
|
category: "Regulated Areas",
|
|
7520
7697
|
displayCategory: "STANDARD",
|
|
7521
7698
|
int1Ref: "N2",
|
|
7522
|
-
style: { type: "line", paint: { "line-color": "TRFCD", "line-width": 1.2, "line-opacity": 0.7 } },
|
|
7699
|
+
style: { type: "line", paint: { "line-color": "TRFCD", "line-width": 1.2, "line-opacity": 0.7, "line-dasharray": [4, 3] } },
|
|
7523
7700
|
viewingGroup: 26010,
|
|
7524
7701
|
scamin: 9e4
|
|
7525
7702
|
},
|
|
@@ -25972,7 +26149,7 @@ var S52_POINT_SVGS = {
|
|
|
25972
26149
|
"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',
|
|
25973
26150
|
"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',
|
|
25974
26151
|
"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',
|
|
25975
|
-
"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 -
|
|
26152
|
+
"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',
|
|
25976
26153
|
"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',
|
|
25977
26154
|
"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',
|
|
25978
26155
|
"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',
|
|
@@ -26487,6 +26664,8 @@ var CHRT_GLYPH_SVGS = {
|
|
|
26487
26664
|
"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>',
|
|
26488
26665
|
"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>',
|
|
26489
26666
|
"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>',
|
|
26667
|
+
"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>',
|
|
26668
|
+
"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>',
|
|
26490
26669
|
"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>',
|
|
26491
26670
|
"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>',
|
|
26492
26671
|
"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>',
|
|
@@ -26935,59 +27114,6 @@ function iconExpressionForFamily(objectClass, family) {
|
|
|
26935
27114
|
}
|
|
26936
27115
|
}
|
|
26937
27116
|
|
|
26938
|
-
// src/atlas/tile-zoom.ts
|
|
26939
|
-
var BUILD_MINZOOM = 0;
|
|
26940
|
-
var BUILD_MAXZOOM = 14;
|
|
26941
|
-
|
|
26942
|
-
// src/atlas/source-registry.ts
|
|
26943
|
-
var LAND_REGION = "CHRT_LAND";
|
|
26944
|
-
var RELIEF_REGION = "CHRT_RELIEF";
|
|
26945
|
-
var RELIEF_MINZOOM = 8;
|
|
26946
|
-
var RELIEF_MAXZOOM = 13;
|
|
26947
|
-
var RELIEF_TILE_SIZE = 512;
|
|
26948
|
-
var DEFAULT_TILE_BASE_URL2 = "https://tiles.chrt.co";
|
|
26949
|
-
var DEFAULT_REGION = "NOAA_ENC";
|
|
26950
|
-
function buildSources(opts = {}) {
|
|
26951
|
-
const mode = opts.mode ?? "pmtiles";
|
|
26952
|
-
const region = opts.region ?? DEFAULT_REGION;
|
|
26953
|
-
if (mode === "mapbox") {
|
|
26954
|
-
return {
|
|
26955
|
-
[region]: {
|
|
26956
|
-
type: "vector",
|
|
26957
|
-
url: `mapbox://chrt.${region.toLowerCase()}`
|
|
26958
|
-
}
|
|
26959
|
-
};
|
|
26960
|
-
}
|
|
26961
|
-
const base = (opts.tileBaseUrl ?? DEFAULT_TILE_BASE_URL2).replace(/\/+$/, "");
|
|
26962
|
-
const sources = {
|
|
26963
|
-
[region]: {
|
|
26964
|
-
type: "vector",
|
|
26965
|
-
tiles: [`${base}/v1/tiles/${region}/{z}/{x}/{y}.mvt`],
|
|
26966
|
-
minzoom: BUILD_MINZOOM,
|
|
26967
|
-
maxzoom: BUILD_MAXZOOM
|
|
26968
|
-
}
|
|
26969
|
-
};
|
|
26970
|
-
if (opts.land) {
|
|
26971
|
-
sources[LAND_REGION] = {
|
|
26972
|
-
type: "vector",
|
|
26973
|
-
tiles: [`${base}/v1/tiles/${LAND_REGION}/{z}/{x}/{y}.mvt`],
|
|
26974
|
-
minzoom: BUILD_MINZOOM,
|
|
26975
|
-
maxzoom: BUILD_MAXZOOM
|
|
26976
|
-
};
|
|
26977
|
-
}
|
|
26978
|
-
if (opts.relief) {
|
|
26979
|
-
sources[RELIEF_REGION] = {
|
|
26980
|
-
type: "raster-dem",
|
|
26981
|
-
tiles: [`${base}/v1/tiles/${RELIEF_REGION}/{z}/{x}/{y}.png`],
|
|
26982
|
-
encoding: "terrarium",
|
|
26983
|
-
tileSize: RELIEF_TILE_SIZE,
|
|
26984
|
-
minzoom: RELIEF_MINZOOM,
|
|
26985
|
-
maxzoom: RELIEF_MAXZOOM
|
|
26986
|
-
};
|
|
26987
|
-
}
|
|
26988
|
-
return sources;
|
|
26989
|
-
}
|
|
26990
|
-
|
|
26991
27117
|
// src/atlas/layer-builder.ts
|
|
26992
27118
|
var SCAMIN_DECLUTTER_CLASSES = /* @__PURE__ */ new Set([
|
|
26993
27119
|
"OFSPLF",
|
|
@@ -27315,8 +27441,40 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
|
|
|
27315
27441
|
layer.layout = {
|
|
27316
27442
|
...layer.layout,
|
|
27317
27443
|
"icon-allow-overlap": true,
|
|
27318
|
-
"icon-ignore-placement": true
|
|
27444
|
+
"icon-ignore-placement": true,
|
|
27445
|
+
// Flare-specific size (owner ask 2026-08-31: "make the flares a
|
|
27446
|
+
// little smaller — they should scale down a lot as you zoom out").
|
|
27447
|
+
// The safety-critical curve serves buoy/beacon HULLS, whose set
|
|
27448
|
+
// this overrides for LIGHTS only: flares are annotations on a
|
|
27449
|
+
// point, not shapes a mariner ranges against, so they start small
|
|
27450
|
+
// at planning scales and only approach full size at berthing
|
|
27451
|
+
// zooms. Applied after the safety-critical spread so it wins.
|
|
27452
|
+
"icon-size": [
|
|
27453
|
+
"interpolate",
|
|
27454
|
+
["linear"],
|
|
27455
|
+
["zoom"],
|
|
27456
|
+
6,
|
|
27457
|
+
0.3,
|
|
27458
|
+
12,
|
|
27459
|
+
0.42,
|
|
27460
|
+
14,
|
|
27461
|
+
0.6,
|
|
27462
|
+
18,
|
|
27463
|
+
1
|
|
27464
|
+
]
|
|
27465
|
+
};
|
|
27466
|
+
const valnmrNum = ["to-number", ["coalesce", ["get", "VALNMR"], ["get", "valnmr"], 0], 0];
|
|
27467
|
+
const isMajor = [">=", valnmrNum, 10];
|
|
27468
|
+
const majorLayer = {
|
|
27469
|
+
...layer,
|
|
27470
|
+
id: `${layer.id}-major`,
|
|
27471
|
+
filter: layer.filter ? ["all", layer.filter, isMajor] : isMajor,
|
|
27472
|
+
layout: { ...layer.layout },
|
|
27473
|
+
minzoom: 6
|
|
27319
27474
|
};
|
|
27475
|
+
layer.filter = layer.filter ? ["all", layer.filter, ["!", isMajor]] : ["!", isMajor];
|
|
27476
|
+
layer.minzoom = scaminToMinZoom(entry?.scamin) ?? layer.minzoom;
|
|
27477
|
+
layers.push(majorLayer);
|
|
27320
27478
|
}
|
|
27321
27479
|
} else if (resolved.layout) {
|
|
27322
27480
|
layer.layout = resolved.layout;
|
|
@@ -28268,91 +28426,272 @@ function getOrderedClasses() {
|
|
|
28268
28426
|
return ordered;
|
|
28269
28427
|
}
|
|
28270
28428
|
|
|
28271
|
-
// src/atlas/
|
|
28272
|
-
var
|
|
28273
|
-
|
|
28274
|
-
|
|
28275
|
-
|
|
28429
|
+
// src/atlas/band-priority.ts
|
|
28430
|
+
var BAND_PRIORITY_RANGES = {
|
|
28431
|
+
"1": { minzoom: 0 },
|
|
28432
|
+
"2": { minzoom: 5 },
|
|
28433
|
+
"3": { minzoom: 7 },
|
|
28434
|
+
"4": { minzoom: 9 },
|
|
28435
|
+
"5": { minzoom: 11 },
|
|
28436
|
+
"6": { minzoom: 11 }
|
|
28437
|
+
};
|
|
28438
|
+
var BAND_DATA_MINZOOM = {
|
|
28439
|
+
"1": 0,
|
|
28440
|
+
"2": 5,
|
|
28441
|
+
"3": 7,
|
|
28442
|
+
"4": 9,
|
|
28443
|
+
"5": 11,
|
|
28444
|
+
"6": 13
|
|
28445
|
+
};
|
|
28446
|
+
function dominatedVariantMaxzoom(D) {
|
|
28447
|
+
return Math.max(BAND_PRIORITY_RANGES[D].minzoom, BAND_DATA_MINZOOM[D]);
|
|
28276
28448
|
}
|
|
28277
|
-
var
|
|
28278
|
-
|
|
28279
|
-
|
|
28280
|
-
|
|
28281
|
-
|
|
28282
|
-
|
|
28283
|
-
|
|
28284
|
-
|
|
28285
|
-
|
|
28286
|
-
|
|
28287
|
-
|
|
28288
|
-
|
|
28289
|
-
fade: "#e8f2f8",
|
|
28290
|
-
// Raised from 0.45 on 2026-08-23. ⚠ Above the ~0.5 that
|
|
28291
|
-
// hillshade-style.mjs warns "starts competing with depth areas" — a
|
|
28292
|
-
// deliberate, sighted choice made against rendered evidence, not a
|
|
28293
|
-
// default. If soundings or DEPARE ever read as fighting the terrain,
|
|
28294
|
-
// this number is the first thing to walk back.
|
|
28295
|
-
exaggeration: 0.8
|
|
28296
|
-
},
|
|
28297
|
-
DUSK: {
|
|
28298
|
-
base: "#4a4433",
|
|
28299
|
-
cover: { wood: "#414a38", wetland: "#3f4740", urban: "#4e4a42", sand: "#544c39" },
|
|
28300
|
-
contour: "#6b6250",
|
|
28301
|
-
contourIndex: "#837961",
|
|
28302
|
-
road: "#565040",
|
|
28303
|
-
roadMajor: "#63594440",
|
|
28304
|
-
label: "#c3b898",
|
|
28305
|
-
halo: "#2b2820",
|
|
28306
|
-
shadow: "#2e2a20",
|
|
28307
|
-
highlight: "#8a8068",
|
|
28308
|
-
fade: "#16202a",
|
|
28309
|
-
// Unchanged. DUSK was not the palette the 0.8 decision was made against.
|
|
28310
|
-
exaggeration: 0.45
|
|
28311
|
-
},
|
|
28312
|
-
NIGHT: {
|
|
28313
|
-
base: "#2a2620",
|
|
28314
|
-
cover: { wood: "#242a20", wetland: "#232823", urban: "#2c2925", sand: "#302b22" },
|
|
28315
|
-
contour: "#413b30",
|
|
28316
|
-
contourIndex: "#524a3b",
|
|
28317
|
-
road: "#332f27",
|
|
28318
|
-
roadMajor: "#3d382d",
|
|
28319
|
-
label: "#7d745e",
|
|
28320
|
-
halo: "#15130f",
|
|
28321
|
-
shadow: "#1a1712",
|
|
28322
|
-
highlight: "#4a4436",
|
|
28323
|
-
fade: "#07090b",
|
|
28324
|
-
// ⚠ DELIBERATELY NOT RAISED. See the type's note: a strong hillshade
|
|
28325
|
-
// under red night vision is a grey haze over exactly the contrast a
|
|
28326
|
-
// skipper is navigating by. Left at the shipped value rather than
|
|
28327
|
-
// lowered, because lowering it is a separate decision from this one.
|
|
28328
|
-
exaggeration: 0.45
|
|
28449
|
+
var DOMINANCE_EXEMPT_FILL_CLASSES = /* @__PURE__ */ new Set([
|
|
28450
|
+
"LNDARE",
|
|
28451
|
+
"DEPARE",
|
|
28452
|
+
"UNSARE",
|
|
28453
|
+
"LAKARE",
|
|
28454
|
+
"DRGARE",
|
|
28455
|
+
"SEAARE"
|
|
28456
|
+
]);
|
|
28457
|
+
function isExemptOpaqueFill(layer) {
|
|
28458
|
+
if (layer.type !== "fill") return false;
|
|
28459
|
+
for (const cls of DOMINANCE_EXEMPT_FILL_CLASSES) {
|
|
28460
|
+
if (layer.id.startsWith(`s52-${cls}-`)) return true;
|
|
28329
28461
|
}
|
|
28330
|
-
|
|
28331
|
-
|
|
28332
|
-
var
|
|
28333
|
-
|
|
28334
|
-
"
|
|
28335
|
-
|
|
28336
|
-
|
|
28337
|
-
|
|
28338
|
-
|
|
28339
|
-
|
|
28340
|
-
|
|
28341
|
-
|
|
28342
|
-
|
|
28343
|
-
|
|
28344
|
-
var
|
|
28345
|
-
|
|
28346
|
-
|
|
28347
|
-
|
|
28348
|
-
|
|
28349
|
-
|
|
28350
|
-
|
|
28351
|
-
|
|
28352
|
-
|
|
28353
|
-
|
|
28354
|
-
|
|
28355
|
-
|
|
28462
|
+
return false;
|
|
28463
|
+
}
|
|
28464
|
+
var FILL_FLOOR_FREE_BANDS = /* @__PURE__ */ new Set([
|
|
28465
|
+
"1",
|
|
28466
|
+
"2",
|
|
28467
|
+
"3"
|
|
28468
|
+
]);
|
|
28469
|
+
function bandVariantMinzoom(layer, B) {
|
|
28470
|
+
const classMinzoom = layer.minzoom ?? 0;
|
|
28471
|
+
if (isExemptOpaqueFill(layer) && FILL_FLOOR_FREE_BANDS.has(B)) {
|
|
28472
|
+
return classMinzoom;
|
|
28473
|
+
}
|
|
28474
|
+
return Math.max(classMinzoom, BAND_PRIORITY_RANGES[B].minzoom);
|
|
28475
|
+
}
|
|
28476
|
+
var GENERALISED_AREA_FILL_CLASSES = /* @__PURE__ */ new Set([
|
|
28477
|
+
"BUAARE",
|
|
28478
|
+
// 2026-08-28, New York Harbor: the "New York Marine Inspection Zone and
|
|
28479
|
+
// Captain of the Port Zone" RESARE exists at bands 2/3 as a generalized
|
|
28480
|
+
// shore-hugging polygon whose point-sampled dominance tag MISSED (the
|
|
28481
|
+
// Providence-BUAARE failure mode), so its magenta dashed boundary drew
|
|
28482
|
+
// OFFSET over the band-5 detail at z11.5 — read in the field as "a
|
|
28483
|
+
// magenta shoreline bleeding down from a less coarse band". The same
|
|
28484
|
+
// tile carried untagged band-2 ADMARE and CTNARE giants in the same
|
|
28485
|
+
// state. All three are area classes whose coarse-band boundaries hug
|
|
28486
|
+
// shorelines and whose fine-band copies are authoritative.
|
|
28487
|
+
"RESARE",
|
|
28488
|
+
"CTNARE",
|
|
28489
|
+
"ADMARE"
|
|
28490
|
+
]);
|
|
28491
|
+
var GENERALISED_AREA_MAX_BAND = 3;
|
|
28492
|
+
function generalisedAreaCeiling(layer, B) {
|
|
28493
|
+
if (layer.type !== "fill" && layer.type !== "line") return null;
|
|
28494
|
+
if (parseInt(B, 10) > GENERALISED_AREA_MAX_BAND) return null;
|
|
28495
|
+
for (const cls of GENERALISED_AREA_FILL_CLASSES) {
|
|
28496
|
+
if (layer.id.startsWith(`s52-${cls}-`)) return BAND_PRIORITY_RANGES["5"].minzoom;
|
|
28497
|
+
}
|
|
28498
|
+
return null;
|
|
28499
|
+
}
|
|
28500
|
+
var BAND_DRAW_ORDER = ["1", "2", "3", "4", "5", "6"];
|
|
28501
|
+
var CHART_BAND_CODES = {
|
|
28502
|
+
Overview: ["1"],
|
|
28503
|
+
General: ["2"],
|
|
28504
|
+
Coastal: ["3"],
|
|
28505
|
+
Approach: ["4"],
|
|
28506
|
+
Harbor: ["5", "6"]
|
|
28507
|
+
};
|
|
28508
|
+
var CHART_CASCADE_CODES = {
|
|
28509
|
+
Overview: ["1"],
|
|
28510
|
+
General: ["1", "2"],
|
|
28511
|
+
Coastal: ["1", "2", "3"],
|
|
28512
|
+
Approach: ["1", "2", "3", "4"],
|
|
28513
|
+
Harbor: ["1", "2", "3", "4", "5", "6"]
|
|
28514
|
+
};
|
|
28515
|
+
var CHRT_STYLE_MODES = [
|
|
28516
|
+
"continuous",
|
|
28517
|
+
...Object.keys(CHART_BAND_CODES).map(
|
|
28518
|
+
(band) => `chart:${band}`
|
|
28519
|
+
)
|
|
28520
|
+
];
|
|
28521
|
+
function normalizeStyleMode(raw) {
|
|
28522
|
+
if (raw === null || raw === void 0 || raw === "") return "continuous";
|
|
28523
|
+
const lower = raw.toLowerCase();
|
|
28524
|
+
return CHRT_STYLE_MODES.find((mode) => mode.toLowerCase() === lower) ?? null;
|
|
28525
|
+
}
|
|
28526
|
+
function styleModeBand(mode) {
|
|
28527
|
+
return mode === "continuous" ? null : mode.slice("chart:".length);
|
|
28528
|
+
}
|
|
28529
|
+
function bandFilterExpression(codes) {
|
|
28530
|
+
const list = Array.isArray(codes) ? codes : [codes];
|
|
28531
|
+
if (list.length === 1) {
|
|
28532
|
+
return ["==", ["slice", ["get", "_cell_id"], 2, 3], list[0]];
|
|
28533
|
+
}
|
|
28534
|
+
return ["in", ["slice", ["get", "_cell_id"], 2, 3], ["literal", [...list]]];
|
|
28535
|
+
}
|
|
28536
|
+
function combineWithBandFilter(baseFilter, bandFilter) {
|
|
28537
|
+
if (!baseFilter) return bandFilter;
|
|
28538
|
+
if (baseFilter[0] === "all") {
|
|
28539
|
+
return ["all", ...baseFilter.slice(1), bandFilter];
|
|
28540
|
+
}
|
|
28541
|
+
return ["all", baseFilter, bandFilter];
|
|
28542
|
+
}
|
|
28543
|
+
var NOT_DOMINATED_FILTER = [
|
|
28544
|
+
"all",
|
|
28545
|
+
["!", ["has", "_dominated_by_min"]],
|
|
28546
|
+
["!=", ["get", "_dominated"], true]
|
|
28547
|
+
];
|
|
28548
|
+
function dominatedByMinFilter(D) {
|
|
28549
|
+
const dInt = parseInt(D, 10);
|
|
28550
|
+
if (D === "6") {
|
|
28551
|
+
return [
|
|
28552
|
+
"any",
|
|
28553
|
+
["==", ["get", "_dominated_by_min"], dInt],
|
|
28554
|
+
["==", ["get", "_dominated"], true]
|
|
28555
|
+
];
|
|
28556
|
+
}
|
|
28557
|
+
return ["==", ["get", "_dominated_by_min"], dInt];
|
|
28558
|
+
}
|
|
28559
|
+
function withinBboxFilter(bbox) {
|
|
28560
|
+
const [w, s] = bbox.sw;
|
|
28561
|
+
const [e, n] = bbox.ne;
|
|
28562
|
+
return [
|
|
28563
|
+
"within",
|
|
28564
|
+
{
|
|
28565
|
+
type: "Polygon",
|
|
28566
|
+
coordinates: [
|
|
28567
|
+
[
|
|
28568
|
+
[w, s],
|
|
28569
|
+
[e, s],
|
|
28570
|
+
[e, n],
|
|
28571
|
+
[w, n],
|
|
28572
|
+
[w, s]
|
|
28573
|
+
]
|
|
28574
|
+
]
|
|
28575
|
+
}
|
|
28576
|
+
];
|
|
28577
|
+
}
|
|
28578
|
+
function worldMinusBboxPolygon(bbox) {
|
|
28579
|
+
const [w, s] = bbox.sw;
|
|
28580
|
+
const [e, n] = bbox.ne;
|
|
28581
|
+
return {
|
|
28582
|
+
type: "Polygon",
|
|
28583
|
+
coordinates: [
|
|
28584
|
+
[
|
|
28585
|
+
[-180, -85],
|
|
28586
|
+
[180, -85],
|
|
28587
|
+
[180, 85],
|
|
28588
|
+
[-180, 85],
|
|
28589
|
+
[-180, -85]
|
|
28590
|
+
],
|
|
28591
|
+
// Inner ring (hole) — winding opposite to outer ring per GeoJSON
|
|
28592
|
+
// spec. MapLibre is permissive but we follow strict winding.
|
|
28593
|
+
[
|
|
28594
|
+
[w, s],
|
|
28595
|
+
[w, n],
|
|
28596
|
+
[e, n],
|
|
28597
|
+
[e, s],
|
|
28598
|
+
[w, s]
|
|
28599
|
+
]
|
|
28600
|
+
]
|
|
28601
|
+
};
|
|
28602
|
+
}
|
|
28603
|
+
|
|
28604
|
+
// src/atlas/land-layers.ts
|
|
28605
|
+
var LAND_SURFACE_MINZOOM = BAND_PRIORITY_RANGES["5"].minzoom;
|
|
28606
|
+
var RELIEF_MINZOOM2 = 10;
|
|
28607
|
+
var LAND_SURFACE_LAYERS = (id) => id === "chrtland-base" || id === "chrtland-cover" || id === "chrtland-contour" || id.startsWith("chrtland-fade-");
|
|
28608
|
+
var FADE_PX = 80;
|
|
28609
|
+
var FADE_BANDS = 16;
|
|
28610
|
+
function fadeAlpha(r) {
|
|
28611
|
+
return Math.max(0, Math.min(1, (FADE_PX / 2 - Math.abs(r)) / FADE_PX));
|
|
28612
|
+
}
|
|
28613
|
+
function landBaseColor(palette) {
|
|
28614
|
+
return (LAND_THEMES[palette] ?? LAND_THEMES.DAY).base;
|
|
28615
|
+
}
|
|
28616
|
+
var LAND_THEMES = {
|
|
28617
|
+
DAY: {
|
|
28618
|
+
base: "#e8d9b0",
|
|
28619
|
+
cover: { wood: "#cfd8b4", wetland: "#c9d3c4", urban: "#dedad2", sand: "#efe4c2" },
|
|
28620
|
+
contour: "#a89878",
|
|
28621
|
+
contourIndex: "#8a7a5c",
|
|
28622
|
+
road: "#d9c9a4",
|
|
28623
|
+
roadMajor: "#d2bd8e",
|
|
28624
|
+
label: "#5b4f3a",
|
|
28625
|
+
halo: "#f2ead6",
|
|
28626
|
+
shadow: "#6b5a3e",
|
|
28627
|
+
highlight: "#fffaf0",
|
|
28628
|
+
fade: "#e8f2f8",
|
|
28629
|
+
// Raised from 0.45 on 2026-08-23. ⚠ Above the ~0.5 that
|
|
28630
|
+
// hillshade-style.mjs warns "starts competing with depth areas" — a
|
|
28631
|
+
// deliberate, sighted choice made against rendered evidence, not a
|
|
28632
|
+
// default. If soundings or DEPARE ever read as fighting the terrain,
|
|
28633
|
+
// this number is the first thing to walk back.
|
|
28634
|
+
exaggeration: 0.8
|
|
28635
|
+
},
|
|
28636
|
+
DUSK: {
|
|
28637
|
+
base: "#4a4433",
|
|
28638
|
+
cover: { wood: "#414a38", wetland: "#3f4740", urban: "#4e4a42", sand: "#544c39" },
|
|
28639
|
+
contour: "#6b6250",
|
|
28640
|
+
contourIndex: "#837961",
|
|
28641
|
+
road: "#565040",
|
|
28642
|
+
roadMajor: "#63594440",
|
|
28643
|
+
label: "#c3b898",
|
|
28644
|
+
halo: "#2b2820",
|
|
28645
|
+
shadow: "#2e2a20",
|
|
28646
|
+
highlight: "#8a8068",
|
|
28647
|
+
fade: "#16202a",
|
|
28648
|
+
// Unchanged. DUSK was not the palette the 0.8 decision was made against.
|
|
28649
|
+
exaggeration: 0.45
|
|
28650
|
+
},
|
|
28651
|
+
NIGHT: {
|
|
28652
|
+
base: "#2a2620",
|
|
28653
|
+
cover: { wood: "#242a20", wetland: "#232823", urban: "#2c2925", sand: "#302b22" },
|
|
28654
|
+
contour: "#413b30",
|
|
28655
|
+
contourIndex: "#524a3b",
|
|
28656
|
+
road: "#332f27",
|
|
28657
|
+
roadMajor: "#3d382d",
|
|
28658
|
+
label: "#7d745e",
|
|
28659
|
+
halo: "#15130f",
|
|
28660
|
+
shadow: "#1a1712",
|
|
28661
|
+
highlight: "#4a4436",
|
|
28662
|
+
fade: "#07090b",
|
|
28663
|
+
// ⚠ DELIBERATELY NOT RAISED. See the type's note: a strong hillshade
|
|
28664
|
+
// under red night vision is a grey haze over exactly the contrast a
|
|
28665
|
+
// skipper is navigating by. Left at the shipped value rather than
|
|
28666
|
+
// lowered, because lowering it is a separate decision from this one.
|
|
28667
|
+
exaggeration: 0.45
|
|
28668
|
+
}
|
|
28669
|
+
};
|
|
28670
|
+
var FITS = ["<=", ["coalesce", ["get", "min_z"], 0], ["zoom"]];
|
|
28671
|
+
var ANCHOR = ["coalesce", ["get", "anchor"], "top"];
|
|
28672
|
+
var anchorOffset = (em) => [
|
|
28673
|
+
"match",
|
|
28674
|
+
["get", "anchor"],
|
|
28675
|
+
"left",
|
|
28676
|
+
["literal", [0.6, 0]],
|
|
28677
|
+
"right",
|
|
28678
|
+
["literal", [-0.6, 0]],
|
|
28679
|
+
"bottom",
|
|
28680
|
+
["literal", [0, -em]],
|
|
28681
|
+
["literal", [0, em]]
|
|
28682
|
+
];
|
|
28683
|
+
var FONT = ["Open Sans Regular"];
|
|
28684
|
+
var FONT_ITALIC = ["Open Sans Italic"];
|
|
28685
|
+
function landLayers(opts = {}) {
|
|
28686
|
+
const palette = opts.palette ?? "DAY";
|
|
28687
|
+
const T = LAND_THEMES[palette] ?? LAND_THEMES.DAY;
|
|
28688
|
+
const src = opts.sourceId ?? LAND_REGION;
|
|
28689
|
+
const demSrc = opts.reliefSourceId ?? RELIEF_REGION;
|
|
28690
|
+
const L = (o) => o;
|
|
28691
|
+
const layers = [
|
|
28692
|
+
// 1. The land itself — LNDARE ∩ coverage, so it can never outrun the chart.
|
|
28693
|
+
L({
|
|
28694
|
+
id: "chrtland-base",
|
|
28356
28695
|
type: "fill",
|
|
28357
28696
|
source: src,
|
|
28358
28697
|
"source-layer": "land_base",
|
|
@@ -28387,6 +28726,7 @@ function landLayers(opts = {}) {
|
|
|
28387
28726
|
id: "chrtland-hillshade",
|
|
28388
28727
|
type: "hillshade",
|
|
28389
28728
|
source: demSrc,
|
|
28729
|
+
minzoom: RELIEF_MINZOOM2,
|
|
28390
28730
|
paint: {
|
|
28391
28731
|
"hillshade-exaggeration": T.exaggeration,
|
|
28392
28732
|
"hillshade-shadow-color": T.shadow,
|
|
@@ -28438,13 +28778,78 @@ function landLayers(opts = {}) {
|
|
|
28438
28778
|
"line-opacity": 0.8
|
|
28439
28779
|
}
|
|
28440
28780
|
}));
|
|
28781
|
+
layers.push(L({
|
|
28782
|
+
id: "chrtland-city-major",
|
|
28783
|
+
type: "symbol",
|
|
28784
|
+
source: src,
|
|
28785
|
+
"source-layer": "land_town",
|
|
28786
|
+
minzoom: 6.5,
|
|
28787
|
+
filter: [">=", ["coalesce", ["get", "prank"], 0], 10],
|
|
28788
|
+
layout: {
|
|
28789
|
+
"text-field": [
|
|
28790
|
+
"case",
|
|
28791
|
+
["==", ["get", "upcase"], true],
|
|
28792
|
+
["upcase", ["get", "name"]],
|
|
28793
|
+
["get", "name"]
|
|
28794
|
+
],
|
|
28795
|
+
"text-font": FONT,
|
|
28796
|
+
"text-size": [
|
|
28797
|
+
"interpolate",
|
|
28798
|
+
["linear"],
|
|
28799
|
+
["zoom"],
|
|
28800
|
+
6.5,
|
|
28801
|
+
11.5,
|
|
28802
|
+
9,
|
|
28803
|
+
["+", 10, ["*", 0.4, ["coalesce", ["get", "prank"], 10]]],
|
|
28804
|
+
13,
|
|
28805
|
+
["+", 13, ["*", 0.5, ["coalesce", ["get", "prank"], 10]]]
|
|
28806
|
+
],
|
|
28807
|
+
"text-letter-spacing": ["case", ["==", ["get", "upcase"], true], 0.14, 0.02],
|
|
28808
|
+
"text-offset": anchorOffset(0.75),
|
|
28809
|
+
"text-anchor": ANCHOR,
|
|
28810
|
+
"text-padding": 10,
|
|
28811
|
+
"text-allow-overlap": false,
|
|
28812
|
+
"symbol-sort-key": ["*", -1, ["coalesce", ["get", "pop"], 0]]
|
|
28813
|
+
},
|
|
28814
|
+
paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.6 }
|
|
28815
|
+
}));
|
|
28816
|
+
layers.push(L({
|
|
28817
|
+
id: "chrtland-town-port",
|
|
28818
|
+
type: "symbol",
|
|
28819
|
+
source: src,
|
|
28820
|
+
"source-layer": "land_town",
|
|
28821
|
+
minzoom: 8.5,
|
|
28822
|
+
filter: [
|
|
28823
|
+
"all",
|
|
28824
|
+
["==", ["get", "enc_dup"], true],
|
|
28825
|
+
["<", ["coalesce", ["get", "prank"], 0], 10]
|
|
28826
|
+
],
|
|
28827
|
+
layout: {
|
|
28828
|
+
"text-field": ["get", "name"],
|
|
28829
|
+
"text-font": FONT,
|
|
28830
|
+
"text-size": ["interpolate", ["linear"], ["zoom"], 8.5, 11, 12, 13.5],
|
|
28831
|
+
"text-offset": anchorOffset(0.75),
|
|
28832
|
+
"text-anchor": ANCHOR,
|
|
28833
|
+
"text-padding": 8,
|
|
28834
|
+
"text-allow-overlap": false,
|
|
28835
|
+
// Within the tier, the bigger place wins the space: without this the
|
|
28836
|
+
// tie broke on data order and "West Gloucester" beat Gloucester.
|
|
28837
|
+
"symbol-sort-key": ["*", -1, ["coalesce", ["get", "pop"], 0]]
|
|
28838
|
+
},
|
|
28839
|
+
paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.4 }
|
|
28840
|
+
}));
|
|
28441
28841
|
layers.push(L({
|
|
28442
28842
|
id: "chrtland-town",
|
|
28443
28843
|
type: "symbol",
|
|
28444
28844
|
source: src,
|
|
28445
28845
|
"source-layer": "land_town",
|
|
28446
28846
|
minzoom: 9,
|
|
28447
|
-
filter:
|
|
28847
|
+
filter: [
|
|
28848
|
+
"all",
|
|
28849
|
+
FITS,
|
|
28850
|
+
["<", ["coalesce", ["get", "prank"], 0], 10],
|
|
28851
|
+
["!=", ["get", "enc_dup"], true]
|
|
28852
|
+
],
|
|
28448
28853
|
layout: {
|
|
28449
28854
|
"text-field": [
|
|
28450
28855
|
"case",
|
|
@@ -28477,187 +28882,316 @@ function landLayers(opts = {}) {
|
|
|
28477
28882
|
"text-offset": anchorOffset(0.8),
|
|
28478
28883
|
"text-anchor": ANCHOR,
|
|
28479
28884
|
"text-padding": 5,
|
|
28480
|
-
"text-allow-overlap": false,
|
|
28481
|
-
"symbol-sort-key": 3
|
|
28482
|
-
},
|
|
28483
|
-
paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
|
|
28484
|
-
}));
|
|
28485
|
-
layers.push(L({
|
|
28486
|
-
id: "chrtland-poi",
|
|
28487
|
-
type: "symbol",
|
|
28488
|
-
source: src,
|
|
28489
|
-
"source-layer": "land_poi",
|
|
28490
|
-
minzoom: 13,
|
|
28491
|
-
filter: FITS,
|
|
28492
|
-
layout: {
|
|
28493
|
-
"text-field": ["get", "name"],
|
|
28494
|
-
"text-font": FONT_ITALIC,
|
|
28495
|
-
"text-size": ["interpolate", ["linear"], ["zoom"], 13, 8, 14, 9.5],
|
|
28496
|
-
"text-offset": anchorOffset(0.7),
|
|
28497
|
-
"text-anchor": ANCHOR,
|
|
28498
|
-
"text-padding": 5,
|
|
28499
|
-
"text-allow-overlap": false,
|
|
28500
|
-
"symbol-sort-key": 5
|
|
28501
|
-
},
|
|
28502
|
-
paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
|
|
28503
|
-
}));
|
|
28504
|
-
|
|
28505
|
-
|
|
28506
|
-
|
|
28507
|
-
|
|
28508
|
-
var BAND_PRIORITY_RANGES = {
|
|
28509
|
-
"1": { minzoom: 0 },
|
|
28510
|
-
"2": { minzoom: 5 },
|
|
28511
|
-
"3": { minzoom: 7 },
|
|
28512
|
-
"4": { minzoom: 9 },
|
|
28513
|
-
"5": { minzoom: 11 },
|
|
28514
|
-
"6": { minzoom: 11 }
|
|
28515
|
-
};
|
|
28516
|
-
var BAND_DATA_MINZOOM = {
|
|
28517
|
-
"1": 0,
|
|
28518
|
-
"2": 5,
|
|
28519
|
-
"3": 7,
|
|
28520
|
-
"4": 9,
|
|
28521
|
-
"5": 11,
|
|
28522
|
-
"6": 13
|
|
28523
|
-
};
|
|
28524
|
-
function dominatedVariantMaxzoom(D) {
|
|
28525
|
-
return Math.max(BAND_PRIORITY_RANGES[D].minzoom, BAND_DATA_MINZOOM[D]);
|
|
28526
|
-
}
|
|
28527
|
-
var DOMINANCE_EXEMPT_FILL_CLASSES = /* @__PURE__ */ new Set([
|
|
28528
|
-
"LNDARE",
|
|
28529
|
-
"DEPARE",
|
|
28530
|
-
"UNSARE",
|
|
28531
|
-
"LAKARE",
|
|
28532
|
-
"DRGARE",
|
|
28533
|
-
"SEAARE"
|
|
28534
|
-
]);
|
|
28535
|
-
function isExemptOpaqueFill(layer) {
|
|
28536
|
-
if (layer.type !== "fill") return false;
|
|
28537
|
-
for (const cls of DOMINANCE_EXEMPT_FILL_CLASSES) {
|
|
28538
|
-
if (layer.id.startsWith(`s52-${cls}-`)) return true;
|
|
28885
|
+
"text-allow-overlap": false,
|
|
28886
|
+
"symbol-sort-key": 3
|
|
28887
|
+
},
|
|
28888
|
+
paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
|
|
28889
|
+
}));
|
|
28890
|
+
layers.push(L({
|
|
28891
|
+
id: "chrtland-poi",
|
|
28892
|
+
type: "symbol",
|
|
28893
|
+
source: src,
|
|
28894
|
+
"source-layer": "land_poi",
|
|
28895
|
+
minzoom: 13,
|
|
28896
|
+
filter: FITS,
|
|
28897
|
+
layout: {
|
|
28898
|
+
"text-field": ["get", "name"],
|
|
28899
|
+
"text-font": FONT_ITALIC,
|
|
28900
|
+
"text-size": ["interpolate", ["linear"], ["zoom"], 13, 8, 14, 9.5],
|
|
28901
|
+
"text-offset": anchorOffset(0.7),
|
|
28902
|
+
"text-anchor": ANCHOR,
|
|
28903
|
+
"text-padding": 5,
|
|
28904
|
+
"text-allow-overlap": false,
|
|
28905
|
+
"symbol-sort-key": 5
|
|
28906
|
+
},
|
|
28907
|
+
paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
|
|
28908
|
+
}));
|
|
28909
|
+
for (const layer of layers) {
|
|
28910
|
+
const l = layer;
|
|
28911
|
+
if (!LAND_SURFACE_LAYERS(l.id)) continue;
|
|
28912
|
+
l.minzoom = Math.max(l.minzoom ?? 0, LAND_SURFACE_MINZOOM);
|
|
28539
28913
|
}
|
|
28540
|
-
return
|
|
28914
|
+
return layers;
|
|
28541
28915
|
}
|
|
28542
|
-
|
|
28543
|
-
|
|
28544
|
-
|
|
28545
|
-
|
|
28546
|
-
|
|
28547
|
-
|
|
28548
|
-
|
|
28549
|
-
|
|
28550
|
-
|
|
28916
|
+
|
|
28917
|
+
// src/atlas/base-layers.ts
|
|
28918
|
+
var NOCOVERAGE_PATTERN = "chrt:hatch-nocoverage";
|
|
28919
|
+
var UNCHARTED_PATTERN = "chrt:hatch-uncharted";
|
|
28920
|
+
var BASE_THEMES = {
|
|
28921
|
+
DAY: {
|
|
28922
|
+
earth: "#2A3439",
|
|
28923
|
+
water: "#1B2328",
|
|
28924
|
+
river: "#24313A",
|
|
28925
|
+
boundary: "#4A575E",
|
|
28926
|
+
jurisdiction: "#E8D9B0",
|
|
28927
|
+
// CHRTland's own tan — one land colour
|
|
28928
|
+
inlandWater: "#B9CFDD",
|
|
28929
|
+
// muted, unmistakably not a charted blue
|
|
28930
|
+
// State furniture. These three were referenced by the layers but missing
|
|
28931
|
+
// from this table (and its type — the long-standing TS2339 trio), so the
|
|
28932
|
+
// lines shipped on MapLibre's DEFAULT black. Now deliberate: a hairline
|
|
28933
|
+
// a shade deeper than the tan, quiet caps, tan halo.
|
|
28934
|
+
stateLine: "#C4B389",
|
|
28935
|
+
stateLabel: "#8E8468",
|
|
28936
|
+
stateHalo: "#E8D9B0",
|
|
28937
|
+
// Rough-lake furniture (owner ask 2026-08-28: "the lakes need labels,
|
|
28938
|
+
// and shorelines drawn"): a shoreline a shade deeper than the rough
|
|
28939
|
+
// water, and a name in the water's own voice, haloed by the water.
|
|
28940
|
+
inlandShore: "#8FA9BA",
|
|
28941
|
+
inlandLabel: "#54707F",
|
|
28942
|
+
inlandHalo: "#B9CFDD"
|
|
28943
|
+
},
|
|
28944
|
+
DUSK: {
|
|
28945
|
+
earth: "#232B30",
|
|
28946
|
+
water: "#161D21",
|
|
28947
|
+
river: "#1E2930",
|
|
28948
|
+
boundary: "#3E4A50",
|
|
28949
|
+
jurisdiction: "#C9BC96",
|
|
28950
|
+
inlandWater: "#93A9B7",
|
|
28951
|
+
stateLine: "#A6976F",
|
|
28952
|
+
stateLabel: "#776E55",
|
|
28953
|
+
stateHalo: "#C9BC96",
|
|
28954
|
+
inlandShore: "#74899A",
|
|
28955
|
+
inlandLabel: "#41586A",
|
|
28956
|
+
inlandHalo: "#93A9B7"
|
|
28957
|
+
},
|
|
28958
|
+
NIGHT: {
|
|
28959
|
+
earth: "#1A2125",
|
|
28960
|
+
water: "#10161A",
|
|
28961
|
+
river: "#182228",
|
|
28962
|
+
boundary: "#333E44",
|
|
28963
|
+
jurisdiction: "#8F866C",
|
|
28964
|
+
inlandWater: "#5F7280",
|
|
28965
|
+
stateLine: "#6B6350",
|
|
28966
|
+
stateLabel: "#565040",
|
|
28967
|
+
stateHalo: "#8F866C",
|
|
28968
|
+
inlandShore: "#4A5C68",
|
|
28969
|
+
inlandLabel: "#8CA2B0",
|
|
28970
|
+
inlandHalo: "#5F7280"
|
|
28551
28971
|
}
|
|
28552
|
-
return Math.max(classMinzoom, BAND_PRIORITY_RANGES[B].minzoom);
|
|
28553
|
-
}
|
|
28554
|
-
var BAND_DRAW_ORDER = ["1", "2", "3", "4", "5", "6"];
|
|
28555
|
-
var CHART_BAND_CODES = {
|
|
28556
|
-
Overview: ["1"],
|
|
28557
|
-
General: ["2"],
|
|
28558
|
-
Coastal: ["3"],
|
|
28559
|
-
Approach: ["4"],
|
|
28560
|
-
Harbor: ["5", "6"]
|
|
28561
|
-
};
|
|
28562
|
-
var CHART_CASCADE_CODES = {
|
|
28563
|
-
Overview: ["1"],
|
|
28564
|
-
General: ["1", "2"],
|
|
28565
|
-
Coastal: ["1", "2", "3"],
|
|
28566
|
-
Approach: ["1", "2", "3", "4"],
|
|
28567
|
-
Harbor: ["1", "2", "3", "4", "5", "6"]
|
|
28568
28972
|
};
|
|
28569
|
-
|
|
28570
|
-
"
|
|
28571
|
-
|
|
28572
|
-
|
|
28573
|
-
)
|
|
28574
|
-
];
|
|
28575
|
-
function normalizeStyleMode(raw) {
|
|
28576
|
-
if (raw === null || raw === void 0 || raw === "") return "continuous";
|
|
28577
|
-
const lower = raw.toLowerCase();
|
|
28578
|
-
return CHRT_STYLE_MODES.find((mode) => mode.toLowerCase() === lower) ?? null;
|
|
28579
|
-
}
|
|
28580
|
-
function styleModeBand(mode) {
|
|
28581
|
-
return mode === "continuous" ? null : mode.slice("chart:".length);
|
|
28582
|
-
}
|
|
28583
|
-
function bandFilterExpression(codes) {
|
|
28584
|
-
const list = Array.isArray(codes) ? codes : [codes];
|
|
28585
|
-
if (list.length === 1) {
|
|
28586
|
-
return ["==", ["slice", ["get", "_cell_id"], 2, 3], list[0]];
|
|
28587
|
-
}
|
|
28588
|
-
return ["in", ["slice", ["get", "_cell_id"], 2, 3], ["literal", [...list]]];
|
|
28589
|
-
}
|
|
28590
|
-
function combineWithBandFilter(baseFilter, bandFilter) {
|
|
28591
|
-
if (!baseFilter) return bandFilter;
|
|
28592
|
-
if (baseFilter[0] === "all") {
|
|
28593
|
-
return ["all", ...baseFilter.slice(1), bandFilter];
|
|
28594
|
-
}
|
|
28595
|
-
return ["all", baseFilter, bandFilter];
|
|
28596
|
-
}
|
|
28597
|
-
var NOT_DOMINATED_FILTER = [
|
|
28598
|
-
"all",
|
|
28599
|
-
["!", ["has", "_dominated_by_min"]],
|
|
28600
|
-
["!=", ["get", "_dominated"], true]
|
|
28601
|
-
];
|
|
28602
|
-
function dominatedByMinFilter(D) {
|
|
28603
|
-
const dInt = parseInt(D, 10);
|
|
28604
|
-
if (D === "6") {
|
|
28605
|
-
return [
|
|
28606
|
-
"any",
|
|
28607
|
-
["==", ["get", "_dominated_by_min"], dInt],
|
|
28608
|
-
["==", ["get", "_dominated"], true]
|
|
28609
|
-
];
|
|
28610
|
-
}
|
|
28611
|
-
return ["==", ["get", "_dominated_by_min"], dInt];
|
|
28612
|
-
}
|
|
28613
|
-
function withinBboxFilter(bbox) {
|
|
28614
|
-
const [w, s] = bbox.sw;
|
|
28615
|
-
const [e, n] = bbox.ne;
|
|
28973
|
+
function baseLayers(opts = {}) {
|
|
28974
|
+
const palette = opts.palette ?? "DAY";
|
|
28975
|
+
const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
|
|
28976
|
+
const src = opts.sourceId ?? BASE_REGION;
|
|
28977
|
+
const L = (o) => o;
|
|
28616
28978
|
return [
|
|
28617
|
-
|
|
28618
|
-
|
|
28619
|
-
|
|
28620
|
-
|
|
28621
|
-
|
|
28622
|
-
|
|
28623
|
-
|
|
28624
|
-
|
|
28625
|
-
|
|
28626
|
-
|
|
28627
|
-
|
|
28628
|
-
|
|
28629
|
-
|
|
28979
|
+
// 1. Geography — the world as it is, in the quiet dark voice. The OCEAN
|
|
28980
|
+
// is a background, not a polygon: the archive ships land and lakes,
|
|
28981
|
+
// and "everything else is sea" is exactly what a background expresses
|
|
28982
|
+
// — geometry-free, gap-free at every antimeridian and pole. Without
|
|
28983
|
+
// it the sea outside coverage was bare S-52 NODTA, a light grey that
|
|
28984
|
+
// read as "charted ground" with a hatch on it.
|
|
28985
|
+
L({
|
|
28986
|
+
id: "chrtbase-ocean",
|
|
28987
|
+
type: "background",
|
|
28988
|
+
paint: { "background-color": T.water }
|
|
28989
|
+
}),
|
|
28990
|
+
L({
|
|
28991
|
+
id: "chrtbase-earth",
|
|
28992
|
+
type: "fill",
|
|
28993
|
+
source: src,
|
|
28994
|
+
"source-layer": "base_earth",
|
|
28995
|
+
paint: { "fill-color": T.earth }
|
|
28996
|
+
}),
|
|
28997
|
+
L({
|
|
28998
|
+
id: "chrtbase-water",
|
|
28999
|
+
type: "fill",
|
|
29000
|
+
source: src,
|
|
29001
|
+
"source-layer": "base_water",
|
|
29002
|
+
paint: { "fill-color": T.water }
|
|
29003
|
+
}),
|
|
29004
|
+
L({
|
|
29005
|
+
id: "chrtbase-river",
|
|
29006
|
+
type: "line",
|
|
29007
|
+
source: src,
|
|
29008
|
+
"source-layer": "base_river",
|
|
29009
|
+
minzoom: 9,
|
|
29010
|
+
paint: {
|
|
29011
|
+
"line-color": T.river,
|
|
29012
|
+
"line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.6, 14, 1.6]
|
|
29013
|
+
}
|
|
29014
|
+
}),
|
|
29015
|
+
L({
|
|
29016
|
+
id: "chrtbase-boundary",
|
|
29017
|
+
type: "line",
|
|
29018
|
+
source: src,
|
|
29019
|
+
"source-layer": "base_boundary",
|
|
29020
|
+
paint: { "line-color": T.boundary, "line-width": 0.6, "line-opacity": 0.8 }
|
|
29021
|
+
}),
|
|
29022
|
+
// 2. THE DEFAULT: no coverage, everywhere, as a geometry-free background.
|
|
29023
|
+
// Constant opacity by design — see the header. What kills it is being
|
|
29024
|
+
// painted OVER, never fading.
|
|
29025
|
+
L({
|
|
29026
|
+
id: "chrtbase-nocoverage",
|
|
29027
|
+
type: "background",
|
|
29028
|
+
paint: { "background-pattern": NOCOVERAGE_PATTERN, "background-opacity": 0.55 }
|
|
29029
|
+
}),
|
|
29030
|
+
// 3. The jurisdiction: countries whose charts CHRT carries go tan — the
|
|
29031
|
+
// same tan as CHRTland, so "land in our world" is ONE colour. Opaque,
|
|
29032
|
+
// so it takes the crosshatch off its ground entirely.
|
|
29033
|
+
L({
|
|
29034
|
+
id: "chrtbase-jurisdiction",
|
|
29035
|
+
type: "fill",
|
|
29036
|
+
source: src,
|
|
29037
|
+
"source-layer": "jurisdiction",
|
|
29038
|
+
paint: { "fill-color": T.jurisdiction }
|
|
29039
|
+
}),
|
|
29040
|
+
// 4. States — internal boundaries and names, inside the jurisdiction
|
|
29041
|
+
// only (owner decision 2026-08-28). A hairline a shade deeper than the
|
|
29042
|
+
// tan, and quiet letter-spaced caps that fade as harbour detail takes
|
|
29043
|
+
// over. The one deliberate exception to no-names-on-the-basemap:
|
|
29044
|
+
// ADMIN furniture, nothing else — see the fence test.
|
|
29045
|
+
L({
|
|
29046
|
+
id: "chrtbase-state-boundary",
|
|
29047
|
+
type: "line",
|
|
29048
|
+
source: src,
|
|
29049
|
+
"source-layer": "state_boundary",
|
|
29050
|
+
minzoom: 4,
|
|
29051
|
+
paint: {
|
|
29052
|
+
"line-color": T.stateLine,
|
|
29053
|
+
"line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1],
|
|
29054
|
+
"line-dasharray": [3, 2],
|
|
29055
|
+
"line-opacity": 0.8
|
|
29056
|
+
}
|
|
29057
|
+
}),
|
|
29058
|
+
// 5. Country borders, drawn to be HOISTED above the whole chart by the
|
|
29059
|
+
// generator (owner, 2026-08-31: "where there is charted land, can we
|
|
29060
|
+
// add country borders on top of everything? I want it clear where
|
|
29061
|
+
// there is a boarder."). Sourced from NE boundary_lines_land — land
|
|
29062
|
+
// borders only, never coastline — so riding on top cannot stroke a
|
|
29063
|
+
// coast. Dashed so it reads as an administrative line, not chart
|
|
29064
|
+
// linework.
|
|
29065
|
+
L({
|
|
29066
|
+
id: "chrtbase-country-boundary",
|
|
29067
|
+
type: "line",
|
|
29068
|
+
source: src,
|
|
29069
|
+
"source-layer": "country_boundary",
|
|
29070
|
+
paint: {
|
|
29071
|
+
"line-color": T.boundary,
|
|
29072
|
+
"line-width": ["interpolate", ["linear"], ["zoom"], 3, 0.8, 8, 1.4, 12, 2.2],
|
|
29073
|
+
"line-dasharray": [4, 2],
|
|
29074
|
+
"line-opacity": 0.9
|
|
29075
|
+
}
|
|
29076
|
+
}),
|
|
29077
|
+
L({
|
|
29078
|
+
id: "chrtbase-state-label",
|
|
29079
|
+
type: "symbol",
|
|
29080
|
+
source: src,
|
|
29081
|
+
"source-layer": "state_label",
|
|
29082
|
+
minzoom: 4,
|
|
29083
|
+
maxzoom: 10,
|
|
29084
|
+
layout: {
|
|
29085
|
+
"text-field": ["upcase", ["get", "name"]],
|
|
29086
|
+
"text-font": ["Open Sans Regular"],
|
|
29087
|
+
"text-size": ["interpolate", ["linear"], ["zoom"], 4, 9, 8, 12],
|
|
29088
|
+
"text-letter-spacing": 0.25,
|
|
29089
|
+
"text-allow-overlap": false,
|
|
29090
|
+
"text-padding": 8
|
|
29091
|
+
},
|
|
29092
|
+
paint: {
|
|
29093
|
+
"text-color": T.stateLabel,
|
|
29094
|
+
"text-halo-color": T.stateHalo,
|
|
29095
|
+
"text-halo-width": 1.2,
|
|
29096
|
+
"text-opacity": ["interpolate", ["linear"], ["zoom"], 4, 0.7, 8, 0.85, 10, 0.4]
|
|
29097
|
+
}
|
|
29098
|
+
})
|
|
28630
29099
|
];
|
|
28631
29100
|
}
|
|
28632
|
-
function
|
|
28633
|
-
const
|
|
28634
|
-
const [
|
|
28635
|
-
|
|
28636
|
-
|
|
28637
|
-
|
|
28638
|
-
|
|
28639
|
-
|
|
28640
|
-
|
|
28641
|
-
|
|
28642
|
-
|
|
28643
|
-
|
|
28644
|
-
|
|
28645
|
-
|
|
28646
|
-
|
|
28647
|
-
|
|
28648
|
-
|
|
28649
|
-
|
|
28650
|
-
|
|
28651
|
-
|
|
28652
|
-
|
|
28653
|
-
|
|
28654
|
-
|
|
28655
|
-
|
|
29101
|
+
function baseInlandWaterLayers(opts = {}) {
|
|
29102
|
+
const palette = opts.palette ?? "DAY";
|
|
29103
|
+
const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
|
|
29104
|
+
const src = opts.sourceId ?? BASE_REGION;
|
|
29105
|
+
const L = (o) => o;
|
|
29106
|
+
return [
|
|
29107
|
+
L({
|
|
29108
|
+
id: "chrtbase-inland-water",
|
|
29109
|
+
type: "fill",
|
|
29110
|
+
source: src,
|
|
29111
|
+
"source-layer": "inland_water",
|
|
29112
|
+
paint: { "fill-color": T.inlandWater }
|
|
29113
|
+
}),
|
|
29114
|
+
L({
|
|
29115
|
+
id: "chrtbase-inland-hatch",
|
|
29116
|
+
type: "fill",
|
|
29117
|
+
source: src,
|
|
29118
|
+
"source-layer": "inland_water",
|
|
29119
|
+
paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
|
|
29120
|
+
}),
|
|
29121
|
+
// The rough shoreline (owner ask 2026-08-28). Drawn ABOVE the hatch so
|
|
29122
|
+
// the edge stays crisp through the texture, and BELOW the charted-water
|
|
29123
|
+
// repaint clones the generator splices in after it — so wherever the
|
|
29124
|
+
// chart repaints its own water, the rough NE10m line yields to the
|
|
29125
|
+
// chart's COALNE instead of double-striking it.
|
|
29126
|
+
//
|
|
29127
|
+
// NE-tier geometry ONLY (`kind` absent): the planet detail tier joins
|
|
29128
|
+
// the same source-layer from z9 as MANY polygons whose chunk-clip seams
|
|
29129
|
+
// rejoin invisibly as FILLS — but a line layer strokes every seam, and
|
|
29130
|
+
// Winnipesaukee rendered as a mesh of internal edges. Planet features
|
|
29131
|
+
// carry `kind` (the INLAND_KINDS filter's own field); NE's carry only
|
|
29132
|
+
// `name`, so the absence of `kind` IS the tier discriminator.
|
|
29133
|
+
L({
|
|
29134
|
+
id: "chrtbase-inland-shore",
|
|
29135
|
+
type: "line",
|
|
29136
|
+
source: src,
|
|
29137
|
+
"source-layer": "inland_water",
|
|
29138
|
+
filter: ["!", ["has", "kind"]],
|
|
29139
|
+
paint: {
|
|
29140
|
+
"line-color": T.inlandShore,
|
|
29141
|
+
"line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1.1, 14, 1.6],
|
|
29142
|
+
"line-opacity": 0.9
|
|
29143
|
+
}
|
|
29144
|
+
}),
|
|
29145
|
+
// The lake's name (owner ask 2026-08-28) — the second scoped exception
|
|
29146
|
+
// to no-names-on-the-basemap, after the states: WATER-BODY names only.
|
|
29147
|
+
// No maxzoom: an uncharted lake keeps its name at every zoom; over
|
|
29148
|
+
// charted lakes the chart's own SEAARE label takes over at detail zooms
|
|
29149
|
+
// and collision culls the echo.
|
|
29150
|
+
//
|
|
29151
|
+
// The archive's `inland_water_label` anchors (v3+). v3 shipped ONE
|
|
29152
|
+
// PointOnSurface anchor per lake, and a big lake's single anchor pans
|
|
29153
|
+
// off-viewport — Winnipesaukee at z11.7 read nameless with its name two
|
|
29154
|
+
// tiles west of the screen (owner-reported). Labelling the polygons
|
|
29155
|
+
// instead is NOT the fix: tippecanoe's coalescing drops the NE polygon
|
|
29156
|
+
// from some z11 tiles (measured: 11/619/747 carries only planet pieces),
|
|
29157
|
+
// so a polygon-sourced label vanishes tile by tile. v4 therefore bakes a
|
|
29158
|
+
// deterministic anchor GRID into big lakes — the paper-chart convention
|
|
29159
|
+
// of repeating a name along a large water body — and this one layer
|
|
29160
|
+
// renders however many anchors are in view; collision culls the rest.
|
|
29161
|
+
L({
|
|
29162
|
+
id: "chrtbase-inland-label",
|
|
29163
|
+
type: "symbol",
|
|
29164
|
+
source: src,
|
|
29165
|
+
"source-layer": "inland_water_label",
|
|
29166
|
+
minzoom: 5,
|
|
29167
|
+
layout: {
|
|
29168
|
+
"text-field": ["get", "name"],
|
|
29169
|
+
"text-font": ["Open Sans Regular"],
|
|
29170
|
+
"text-size": ["interpolate", ["linear"], ["zoom"], 5, 9.5, 10, 12],
|
|
29171
|
+
"text-max-width": 7,
|
|
29172
|
+
"text-allow-overlap": false,
|
|
29173
|
+
// Zoom-scaled spacing for the anchor GRID: at z8.9 the 0.08-deg
|
|
29174
|
+
// anchors sit ~54 px apart and a flat padding let six copies of
|
|
29175
|
+
// "Lake Ontario" crowd one screen (owner-reported 2026-08-30).
|
|
29176
|
+
// Wide collision boxes at low zoom thin the grid to the repeat-
|
|
29177
|
+
// by-convention cadence; by z12 the anchors are ~850 px apart and
|
|
29178
|
+
// the padding relaxes to normal.
|
|
29179
|
+
"text-padding": ["interpolate", ["linear"], ["zoom"], 5, 80, 9, 64, 12, 16]
|
|
29180
|
+
},
|
|
29181
|
+
paint: {
|
|
29182
|
+
"text-color": T.inlandLabel,
|
|
29183
|
+
"text-halo-color": T.inlandHalo,
|
|
29184
|
+
"text-halo-width": 1.1
|
|
29185
|
+
}
|
|
29186
|
+
})
|
|
29187
|
+
];
|
|
28656
29188
|
}
|
|
28657
29189
|
|
|
28658
29190
|
// src/atlas/band-collapse.ts
|
|
28659
|
-
function collapsedDominanceGate(B, minzoom) {
|
|
28660
|
-
const clauses = [
|
|
29191
|
+
function collapsedDominanceGate(B, minzoom, notDominatedCeiling = null) {
|
|
29192
|
+
const clauses = [
|
|
29193
|
+
notDominatedCeiling !== null && notDominatedCeiling > minzoom ? ["all", NOT_DOMINATED_FILTER, ["<", ["zoom"], notDominatedCeiling]] : NOT_DOMINATED_FILTER
|
|
29194
|
+
];
|
|
28661
29195
|
const bInt = parseInt(B, 10);
|
|
28662
29196
|
for (const D of BAND_DRAW_ORDER) {
|
|
28663
29197
|
const dInt = parseInt(D, 10);
|
|
@@ -28686,7 +29220,7 @@ function makeCollapsedBandVariant(layer, B, isExempt) {
|
|
|
28686
29220
|
filter: combineWithBandFilter(layer.filter, [
|
|
28687
29221
|
"all",
|
|
28688
29222
|
bandFilterExpression(B),
|
|
28689
|
-
collapsedDominanceGate(B, minzoom)
|
|
29223
|
+
collapsedDominanceGate(B, minzoom, generalisedAreaCeiling(layer, B))
|
|
28690
29224
|
]),
|
|
28691
29225
|
minzoom
|
|
28692
29226
|
};
|
|
@@ -28821,6 +29355,7 @@ function makeBandVariants(layer, B) {
|
|
|
28821
29355
|
delete exempt.maxzoom;
|
|
28822
29356
|
return [exempt];
|
|
28823
29357
|
}
|
|
29358
|
+
const ceiling = generalisedAreaCeiling(layer, B);
|
|
28824
29359
|
const notDom = {
|
|
28825
29360
|
...layer,
|
|
28826
29361
|
id: `${layer.id}-band${B}`,
|
|
@@ -28831,6 +29366,7 @@ function makeBandVariants(layer, B) {
|
|
|
28831
29366
|
minzoom
|
|
28832
29367
|
};
|
|
28833
29368
|
delete notDom.maxzoom;
|
|
29369
|
+
if (ceiling !== null && ceiling > minzoom) notDom.maxzoom = ceiling;
|
|
28834
29370
|
variants.push(notDom);
|
|
28835
29371
|
const bInt = parseInt(B, 10);
|
|
28836
29372
|
for (const D of BAND_DRAW_ORDER) {
|
|
@@ -28980,12 +29516,43 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
28980
29516
|
paint: { "background-color": resolveColor("NODTA", palette) }
|
|
28981
29517
|
};
|
|
28982
29518
|
const layers = [backgroundLayer];
|
|
29519
|
+
if (opts.base) {
|
|
29520
|
+
const base = baseLayers({ palette });
|
|
29521
|
+
const coverageGround = {
|
|
29522
|
+
id: "s52-coverage-ground",
|
|
29523
|
+
type: "fill",
|
|
29524
|
+
source: sourceId,
|
|
29525
|
+
"source-layer": "M_COVR",
|
|
29526
|
+
filter: [
|
|
29527
|
+
"all",
|
|
29528
|
+
["any", ["==", ["geometry-type"], "Polygon"], ["==", ["geometry-type"], "MultiPolygon"]],
|
|
29529
|
+
["==", ["to-number", ["coalesce", ["get", "CATCOV"], 0]], 1]
|
|
29530
|
+
],
|
|
29531
|
+
paint: { "fill-color": resolveColor("DEPDW", palette) },
|
|
29532
|
+
metadata: { "s52:objectClass": "M_COVR", "s52:role": "coverage-ground" }
|
|
29533
|
+
};
|
|
29534
|
+
const jurisdictionAt = base.findIndex(
|
|
29535
|
+
(l) => l.id === "chrtbase-jurisdiction"
|
|
29536
|
+
);
|
|
29537
|
+
if (jurisdictionAt === -1) {
|
|
29538
|
+
layers.push(...base, coverageGround);
|
|
29539
|
+
} else {
|
|
29540
|
+
layers.push(
|
|
29541
|
+
...base.slice(0, jurisdictionAt),
|
|
29542
|
+
coverageGround,
|
|
29543
|
+
...base.slice(jurisdictionAt)
|
|
29544
|
+
);
|
|
29545
|
+
}
|
|
29546
|
+
}
|
|
28983
29547
|
if (opts.land) {
|
|
28984
29548
|
layers.push(...landLayers({
|
|
28985
29549
|
palette,
|
|
28986
29550
|
...opts.relief ? { relief: true } : {}
|
|
28987
29551
|
}));
|
|
28988
29552
|
}
|
|
29553
|
+
if (opts.base) {
|
|
29554
|
+
layers.push(...baseInlandWaterLayers({ palette }));
|
|
29555
|
+
}
|
|
28989
29556
|
const labelLayers = [];
|
|
28990
29557
|
const chartCascade = opts.chartMode ? CHART_CASCADE_CODES[opts.chartMode.band] : null;
|
|
28991
29558
|
const chartRequestedCodes = opts.chartMode ? CHART_BAND_CODES[opts.chartMode.band] : null;
|
|
@@ -29102,6 +29669,22 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
29102
29669
|
}
|
|
29103
29670
|
}
|
|
29104
29671
|
}
|
|
29672
|
+
{
|
|
29673
|
+
const TERMINAL_FILL_RE = /^s52-(LNDARE|DEPARE)-fill-band([456])$/;
|
|
29674
|
+
for (let i = layers.length - 1; i >= 0; i--) {
|
|
29675
|
+
const src = layers[i];
|
|
29676
|
+
const m = TERMINAL_FILL_RE.exec(src.id);
|
|
29677
|
+
if (!m) continue;
|
|
29678
|
+
const clone = {
|
|
29679
|
+
...layers[i],
|
|
29680
|
+
id: `s52-${m[1]}-fill-terminal-band${m[2]}`,
|
|
29681
|
+
filter: ["all", src.filter, ["==", ["get", "_terminal_fill"], true]]
|
|
29682
|
+
};
|
|
29683
|
+
delete clone.minzoom;
|
|
29684
|
+
clone.maxzoom = src.minzoom ?? 0;
|
|
29685
|
+
layers.splice(i + 1, 0, clone);
|
|
29686
|
+
}
|
|
29687
|
+
}
|
|
29105
29688
|
if (opts.labels === "placename") {
|
|
29106
29689
|
labelLayers.push(...buildPlaceNameLayers(sourceId));
|
|
29107
29690
|
} else {
|
|
@@ -29131,13 +29714,115 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
29131
29714
|
const symbolSet = opts.symbolSet ?? "s52";
|
|
29132
29715
|
const spriteBase = `${opts.tileBaseUrl ?? "https://tiles.chrt.co"}/v1/sprites`;
|
|
29133
29716
|
const isLndareFill = (l) => /^s52-LNDARE-fill-band\d+$/.test(l.id);
|
|
29134
|
-
const
|
|
29717
|
+
const isWaterBaseFill = (l) => /^s52-(DEPARE|DRGARE|UNSARE)-fill-band\d+$/.test(l.id);
|
|
29718
|
+
const bandOf = (l) => {
|
|
29719
|
+
const m = /-band(\d+)$/.exec(l.id);
|
|
29720
|
+
return m ? Number(m[1]) : 0;
|
|
29721
|
+
};
|
|
29135
29722
|
const emitted = opts.land ? (() => {
|
|
29136
|
-
const
|
|
29137
|
-
const
|
|
29138
|
-
const
|
|
29139
|
-
|
|
29723
|
+
const relocated = layers.filter((l) => isLndareFill(l) || isWaterBaseFill(l));
|
|
29724
|
+
const others = layers.filter((l) => !isLndareFill(l) && !isWaterBaseFill(l));
|
|
29725
|
+
const isSeaareFill = (l) => /^s52-SEAARE-fill-band\d+$/.test(l.id);
|
|
29726
|
+
const seaare = others.filter(isSeaareFill);
|
|
29727
|
+
const rest = others.filter((l) => !isSeaareFill(l));
|
|
29728
|
+
const isBaseLayer = (l) => {
|
|
29729
|
+
const id = l.id;
|
|
29730
|
+
if (id.startsWith("chrtbase-inland-")) return false;
|
|
29731
|
+
return id.startsWith("chrtbase-") || id === "s52-coverage-ground";
|
|
29732
|
+
};
|
|
29733
|
+
const baseBlock = rest.filter(isBaseLayer);
|
|
29734
|
+
const restAbove = rest.filter((l) => !isBaseLayer(l));
|
|
29735
|
+
const bands = [...new Set(relocated.map(bandOf))].sort((a, b) => a - b);
|
|
29736
|
+
const oneTan = landBaseColor(palette);
|
|
29737
|
+
const asBaseTan = (l) => ({
|
|
29738
|
+
...l,
|
|
29739
|
+
paint: {
|
|
29740
|
+
...l.paint ?? {},
|
|
29741
|
+
"fill-color": oneTan
|
|
29742
|
+
}
|
|
29743
|
+
});
|
|
29744
|
+
const ordered = [];
|
|
29745
|
+
for (const b of bands) {
|
|
29746
|
+
ordered.push(...relocated.filter((l) => bandOf(l) === b && isWaterBaseFill(l)));
|
|
29747
|
+
ordered.push(...relocated.filter((l) => bandOf(l) === b && isLndareFill(l)).map(asBaseTan));
|
|
29748
|
+
}
|
|
29749
|
+
const findRest = (id) => restAbove.findIndex((l) => l.id === id);
|
|
29750
|
+
const shoreAt = findRest("chrtbase-inland-shore");
|
|
29751
|
+
const pairAt = shoreAt !== -1 ? shoreAt : findRest("chrtbase-inland-hatch");
|
|
29752
|
+
let restFinal = restAbove;
|
|
29753
|
+
if (pairAt !== -1) {
|
|
29754
|
+
const repaint = [];
|
|
29755
|
+
for (const b of bands) {
|
|
29756
|
+
for (const l of relocated.filter((x) => bandOf(x) === b && isWaterBaseFill(x))) {
|
|
29757
|
+
const src = l;
|
|
29758
|
+
const meta = { ...src.metadata ?? {}, "chrt:role": "charted-water-repaint" };
|
|
29759
|
+
const notDom = {
|
|
29760
|
+
...l,
|
|
29761
|
+
id: `${src.id}-overpair`,
|
|
29762
|
+
filter: ["all", src.filter, NOT_DOMINATED_FILTER],
|
|
29763
|
+
metadata: meta
|
|
29764
|
+
};
|
|
29765
|
+
delete notDom.maxzoom;
|
|
29766
|
+
delete notDom.minzoom;
|
|
29767
|
+
repaint.push(notDom);
|
|
29768
|
+
for (const D of BAND_DRAW_ORDER) {
|
|
29769
|
+
if (parseInt(D, 10) <= b) continue;
|
|
29770
|
+
const dMaxzoom = dominatedVariantMaxzoom(D);
|
|
29771
|
+
if (dMaxzoom <= 0) continue;
|
|
29772
|
+
const domVariant = {
|
|
29773
|
+
...l,
|
|
29774
|
+
id: `${src.id}-overpair-domBy${D}`,
|
|
29775
|
+
filter: ["all", src.filter, dominatedByMinFilter(D)],
|
|
29776
|
+
metadata: meta,
|
|
29777
|
+
maxzoom: dMaxzoom
|
|
29778
|
+
};
|
|
29779
|
+
delete domVariant.minzoom;
|
|
29780
|
+
repaint.push(domVariant);
|
|
29781
|
+
}
|
|
29782
|
+
}
|
|
29783
|
+
}
|
|
29784
|
+
const isCoalneLine = (l) => /^s52-COALNE-line-band\d(-domBy\d)?$/.test(l.id);
|
|
29785
|
+
for (const l of layers.filter(isCoalneLine)) {
|
|
29786
|
+
const src = l;
|
|
29787
|
+
const meta = { ...src.metadata ?? {}, "chrt:role": "charted-water-repaint" };
|
|
29788
|
+
const banded = {
|
|
29789
|
+
...l,
|
|
29790
|
+
id: `${src.id}-overpair`,
|
|
29791
|
+
metadata: meta,
|
|
29792
|
+
minzoom: 9
|
|
29793
|
+
};
|
|
29794
|
+
repaint.push(banded);
|
|
29795
|
+
const terminal = {
|
|
29796
|
+
...l,
|
|
29797
|
+
id: `${src.id}-overpair-terminal`,
|
|
29798
|
+
filter: ["all", src.filter, ["==", ["get", "_terminal_shore"], true]],
|
|
29799
|
+
metadata: meta
|
|
29800
|
+
};
|
|
29801
|
+
delete terminal.minzoom;
|
|
29802
|
+
terminal.maxzoom = 9;
|
|
29803
|
+
repaint.push(terminal);
|
|
29804
|
+
}
|
|
29805
|
+
restFinal = [
|
|
29806
|
+
...restAbove.slice(0, pairAt + 1),
|
|
29807
|
+
...repaint,
|
|
29808
|
+
...restAbove.slice(pairAt + 1)
|
|
29809
|
+
];
|
|
29810
|
+
}
|
|
29811
|
+
return [restFinal[0], ...baseBlock, ...seaare, ...ordered, ...restFinal.slice(1)];
|
|
29140
29812
|
})() : layers;
|
|
29813
|
+
for (const id of [
|
|
29814
|
+
"chrtbase-country-boundary",
|
|
29815
|
+
"chrtland-town-port",
|
|
29816
|
+
"chrtland-city-major"
|
|
29817
|
+
]) {
|
|
29818
|
+
const at = emitted.findIndex(
|
|
29819
|
+
(l) => l.id === id
|
|
29820
|
+
);
|
|
29821
|
+
if (at !== -1) {
|
|
29822
|
+
const [layer] = emitted.splice(at, 1);
|
|
29823
|
+
emitted.push(layer);
|
|
29824
|
+
}
|
|
29825
|
+
}
|
|
29141
29826
|
const style = {
|
|
29142
29827
|
version: 8,
|
|
29143
29828
|
name: `CHRT ${SYMBOL_SET_LABELS[symbolSet]} ${PALETTE_NAMES[palette]}${opts.chartMode ? ` \u2014 chart:${opts.chartMode.band}` : ""}`,
|
|
@@ -29149,6 +29834,7 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
29149
29834
|
"chrt:layer-count": emitted.length,
|
|
29150
29835
|
"chrt:land": Boolean(opts.land),
|
|
29151
29836
|
"chrt:relief": Boolean(opts.relief),
|
|
29837
|
+
"chrt:base": Boolean(opts.base),
|
|
29152
29838
|
"chrt:render-mode": opts.chartMode ? `chart:${opts.chartMode.band}` : "continuous",
|
|
29153
29839
|
"chrt:description": "IHO S-52 portrayal rules for Electronic Navigational Charts (ENCs). Generated from the CHRT atlas engine."
|
|
29154
29840
|
},
|
|
@@ -29642,6 +30328,7 @@ var Chrt = class {
|
|
|
29642
30328
|
interact;
|
|
29643
30329
|
featuresEp;
|
|
29644
30330
|
searchEp;
|
|
30331
|
+
poiEp;
|
|
29645
30332
|
queryEp;
|
|
29646
30333
|
http;
|
|
29647
30334
|
tileAuth;
|
|
@@ -29663,6 +30350,7 @@ var Chrt = class {
|
|
|
29663
30350
|
this.weather = new WeatherEndpoint(this.http);
|
|
29664
30351
|
this.featuresEp = new FeaturesEndpoint(this.http);
|
|
29665
30352
|
this.searchEp = new SearchEndpoint(this.http);
|
|
30353
|
+
this.poiEp = new PoiEndpoint(this.http);
|
|
29666
30354
|
this.queryEp = new QueryEndpoint(this.http);
|
|
29667
30355
|
this.offline = new OfflineEndpoint(this.http, config.deviceId, config.manifestPath);
|
|
29668
30356
|
this.interact = new InteractClient(this.http);
|
|
@@ -29698,6 +30386,26 @@ var Chrt = class {
|
|
|
29698
30386
|
async search(options) {
|
|
29699
30387
|
return this.searchEp.find(options);
|
|
29700
30388
|
}
|
|
30389
|
+
/**
|
|
30390
|
+
* CHRTpoi — charted marine facilities in a viewport or radius.
|
|
30391
|
+
*
|
|
30392
|
+
* v1 serves marinas, derived from the ENC's own HRBFAC facility class —
|
|
30393
|
+
* the chart's data, with the source cell's currency inline per POI.
|
|
30394
|
+
*
|
|
30395
|
+
* ```ts
|
|
30396
|
+
* const { pois, coverage } = await chrt.pois({ bbox: [-70.7, 41.4, -70.5, 41.6] });
|
|
30397
|
+
* ```
|
|
30398
|
+
*
|
|
30399
|
+
* **Read `coverage.state` before interpreting an empty list.** `covered` +
|
|
30400
|
+
* empty means no marinas are charted there — a real answer. `none` means
|
|
30401
|
+
* CHRT cannot answer for that area at all, and an empty list means nothing.
|
|
30402
|
+
* The two must never be shown the same way, and `coverage.message` is
|
|
30403
|
+
* written to be relayed unedited. Metered on its own allowance
|
|
30404
|
+
* (`poi_called`), separate from features and search; not gated by tier.
|
|
30405
|
+
*/
|
|
30406
|
+
async pois(options) {
|
|
30407
|
+
return this.poiEp.list(options);
|
|
30408
|
+
}
|
|
29701
30409
|
/** Spatial proximity query around a point */
|
|
29702
30410
|
async query(options) {
|
|
29703
30411
|
return this.queryEp.nearby(options);
|
|
@@ -29868,6 +30576,18 @@ var Chrt = class {
|
|
|
29868
30576
|
return generateStyle(palette, {
|
|
29869
30577
|
region: this.region,
|
|
29870
30578
|
tileBaseUrl: this.tileBaseUrl,
|
|
30579
|
+
// Pin each source's tile URL to the build it serves, from whatever the
|
|
30580
|
+
// token cache already holds. Without a discriminator one URL is shared
|
|
30581
|
+
// by every build, so after a cutover a map keeps painting already-fetched
|
|
30582
|
+
// tiles from the OLD build beside newly-fetched ones from the new.
|
|
30583
|
+
//
|
|
30584
|
+
// BEFORE the spread, so an explicit `buildTags` wins — and only ever a
|
|
30585
|
+
// warm-cache read: this method is synchronous and must not mint. A cold
|
|
30586
|
+
// cache yields `{}`, i.e. exactly the URLs this SDK emitted before the
|
|
30587
|
+
// discriminator existed. Warm it with `chrt.transformRequest()` (which
|
|
30588
|
+
// kicks off a mint) or `await chrt.styleUrl()`; `await chrt.warmTiles()`
|
|
30589
|
+
// is the explicit form, and it returns the map.
|
|
30590
|
+
buildTags: this.tileAuth.peekBuildTags(this.region),
|
|
29871
30591
|
...styleOpts,
|
|
29872
30592
|
// The vessel's safety depth IS the style's safety contour — same number, same
|
|
29873
30593
|
// meaning. Threading it here is what stops the chart shading at one depth while
|
|
@@ -29941,9 +30661,21 @@ var Chrt = class {
|
|
|
29941
30661
|
* Pre-mint the tile token for a region. Optional — `transformRequest()` warms
|
|
29942
30662
|
* the cache automatically — but useful if you want to surface a mint error
|
|
29943
30663
|
* (e.g. `region_not_licensed`, `tile_quota_exceeded`) before the map loads.
|
|
30664
|
+
*
|
|
30665
|
+
* Returns what it minted, so a caller building its own sources can take the
|
|
30666
|
+
* `buildTags` map straight from here:
|
|
30667
|
+
*
|
|
30668
|
+
* ```ts
|
|
30669
|
+
* const { buildTags } = await chrt.warmTiles();
|
|
30670
|
+
* map.addSource("chrt", buildSources({ region: "NOAA_ENC", buildTags })["NOAA_ENC"]);
|
|
30671
|
+
* ```
|
|
30672
|
+
*
|
|
30673
|
+
* That pins each tile URL to the build it serves, so a build cutover gets its
|
|
30674
|
+
* own client cache namespace instead of leaving old and new tiles mixed on
|
|
30675
|
+
* screen. `chrt.style()` reads the same cache and needs no argument.
|
|
29944
30676
|
*/
|
|
29945
30677
|
async warmTiles(region) {
|
|
29946
|
-
|
|
30678
|
+
return this.tileAuth.getToken(region);
|
|
29947
30679
|
}
|
|
29948
30680
|
};
|
|
29949
30681
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -29954,9 +30686,13 @@ var Chrt = class {
|
|
|
29954
30686
|
ATON_EXPRESSION_CLASSES,
|
|
29955
30687
|
BAND_DRAW_ORDER,
|
|
29956
30688
|
BAND_PRIORITY_RANGES,
|
|
30689
|
+
BASE_MAXZOOM,
|
|
30690
|
+
BASE_MINZOOM,
|
|
30691
|
+
BASE_REGION,
|
|
29957
30692
|
BRIDGE_QUERY_LAYERS,
|
|
29958
30693
|
BUILD_MAXZOOM,
|
|
29959
30694
|
BUILD_MINZOOM,
|
|
30695
|
+
BUILD_PARAM,
|
|
29960
30696
|
CHART_BAND_CODES,
|
|
29961
30697
|
CHART_CASCADE_CODES,
|
|
29962
30698
|
CHART_SECTIONS,
|
|
@@ -30138,6 +30874,7 @@ var Chrt = class {
|
|
|
30138
30874
|
scaleSizeValue,
|
|
30139
30875
|
scaminToMinZoom,
|
|
30140
30876
|
styleModeBand,
|
|
30877
|
+
tileTemplate,
|
|
30141
30878
|
withinBboxFilter,
|
|
30142
30879
|
worldMinusBboxPolygon
|
|
30143
30880
|
});
|