@chrtco/sdk 0.10.18 → 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 +1050 -325
- 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 +1045 -325
- 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,7 +27441,27 @@ 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
|
+
]
|
|
27319
27465
|
};
|
|
27320
27466
|
const valnmrNum = ["to-number", ["coalesce", ["get", "VALNMR"], ["get", "valnmr"], 0], 0];
|
|
27321
27467
|
const isMajor = [">=", valnmrNum, 10];
|
|
@@ -28280,92 +28426,273 @@ function getOrderedClasses() {
|
|
|
28280
28426
|
return ordered;
|
|
28281
28427
|
}
|
|
28282
28428
|
|
|
28283
|
-
// src/atlas/
|
|
28284
|
-
var
|
|
28285
|
-
|
|
28286
|
-
|
|
28287
|
-
|
|
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]);
|
|
28288
28448
|
}
|
|
28289
|
-
var
|
|
28290
|
-
|
|
28291
|
-
|
|
28292
|
-
|
|
28293
|
-
|
|
28294
|
-
|
|
28295
|
-
|
|
28296
|
-
|
|
28297
|
-
|
|
28298
|
-
|
|
28299
|
-
|
|
28300
|
-
|
|
28301
|
-
fade: "#e8f2f8",
|
|
28302
|
-
// Raised from 0.45 on 2026-08-23. ⚠ Above the ~0.5 that
|
|
28303
|
-
// hillshade-style.mjs warns "starts competing with depth areas" — a
|
|
28304
|
-
// deliberate, sighted choice made against rendered evidence, not a
|
|
28305
|
-
// default. If soundings or DEPARE ever read as fighting the terrain,
|
|
28306
|
-
// this number is the first thing to walk back.
|
|
28307
|
-
exaggeration: 0.8
|
|
28308
|
-
},
|
|
28309
|
-
DUSK: {
|
|
28310
|
-
base: "#4a4433",
|
|
28311
|
-
cover: { wood: "#414a38", wetland: "#3f4740", urban: "#4e4a42", sand: "#544c39" },
|
|
28312
|
-
contour: "#6b6250",
|
|
28313
|
-
contourIndex: "#837961",
|
|
28314
|
-
road: "#565040",
|
|
28315
|
-
roadMajor: "#63594440",
|
|
28316
|
-
label: "#c3b898",
|
|
28317
|
-
halo: "#2b2820",
|
|
28318
|
-
shadow: "#2e2a20",
|
|
28319
|
-
highlight: "#8a8068",
|
|
28320
|
-
fade: "#16202a",
|
|
28321
|
-
// Unchanged. DUSK was not the palette the 0.8 decision was made against.
|
|
28322
|
-
exaggeration: 0.45
|
|
28323
|
-
},
|
|
28324
|
-
NIGHT: {
|
|
28325
|
-
base: "#2a2620",
|
|
28326
|
-
cover: { wood: "#242a20", wetland: "#232823", urban: "#2c2925", sand: "#302b22" },
|
|
28327
|
-
contour: "#413b30",
|
|
28328
|
-
contourIndex: "#524a3b",
|
|
28329
|
-
road: "#332f27",
|
|
28330
|
-
roadMajor: "#3d382d",
|
|
28331
|
-
label: "#7d745e",
|
|
28332
|
-
halo: "#15130f",
|
|
28333
|
-
shadow: "#1a1712",
|
|
28334
|
-
highlight: "#4a4436",
|
|
28335
|
-
fade: "#07090b",
|
|
28336
|
-
// ⚠ DELIBERATELY NOT RAISED. See the type's note: a strong hillshade
|
|
28337
|
-
// under red night vision is a grey haze over exactly the contrast a
|
|
28338
|
-
// skipper is navigating by. Left at the shipped value rather than
|
|
28339
|
-
// lowered, because lowering it is a separate decision from this one.
|
|
28340
|
-
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;
|
|
28341
28461
|
}
|
|
28342
|
-
|
|
28343
|
-
|
|
28344
|
-
var
|
|
28345
|
-
|
|
28346
|
-
"
|
|
28347
|
-
|
|
28348
|
-
|
|
28349
|
-
|
|
28350
|
-
|
|
28351
|
-
|
|
28352
|
-
|
|
28353
|
-
|
|
28354
|
-
|
|
28355
|
-
|
|
28356
|
-
var
|
|
28357
|
-
|
|
28358
|
-
|
|
28359
|
-
|
|
28360
|
-
|
|
28361
|
-
|
|
28362
|
-
|
|
28363
|
-
|
|
28364
|
-
|
|
28365
|
-
|
|
28366
|
-
|
|
28367
|
-
|
|
28368
|
-
|
|
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",
|
|
28695
|
+
type: "fill",
|
|
28369
28696
|
source: src,
|
|
28370
28697
|
"source-layer": "land_base",
|
|
28371
28698
|
paint: { "fill-color": T.base }
|
|
@@ -28399,6 +28726,7 @@ function landLayers(opts = {}) {
|
|
|
28399
28726
|
id: "chrtland-hillshade",
|
|
28400
28727
|
type: "hillshade",
|
|
28401
28728
|
source: demSrc,
|
|
28729
|
+
minzoom: RELIEF_MINZOOM2,
|
|
28402
28730
|
paint: {
|
|
28403
28731
|
"hillshade-exaggeration": T.exaggeration,
|
|
28404
28732
|
"hillshade-shadow-color": T.shadow,
|
|
@@ -28450,13 +28778,78 @@ function landLayers(opts = {}) {
|
|
|
28450
28778
|
"line-opacity": 0.8
|
|
28451
28779
|
}
|
|
28452
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
|
+
}));
|
|
28453
28841
|
layers.push(L({
|
|
28454
28842
|
id: "chrtland-town",
|
|
28455
28843
|
type: "symbol",
|
|
28456
28844
|
source: src,
|
|
28457
28845
|
"source-layer": "land_town",
|
|
28458
28846
|
minzoom: 9,
|
|
28459
|
-
filter:
|
|
28847
|
+
filter: [
|
|
28848
|
+
"all",
|
|
28849
|
+
FITS,
|
|
28850
|
+
["<", ["coalesce", ["get", "prank"], 0], 10],
|
|
28851
|
+
["!=", ["get", "enc_dup"], true]
|
|
28852
|
+
],
|
|
28460
28853
|
layout: {
|
|
28461
28854
|
"text-field": [
|
|
28462
28855
|
"case",
|
|
@@ -28489,187 +28882,316 @@ function landLayers(opts = {}) {
|
|
|
28489
28882
|
"text-offset": anchorOffset(0.8),
|
|
28490
28883
|
"text-anchor": ANCHOR,
|
|
28491
28884
|
"text-padding": 5,
|
|
28492
|
-
"text-allow-overlap": false,
|
|
28493
|
-
"symbol-sort-key": 3
|
|
28494
|
-
},
|
|
28495
|
-
paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
|
|
28496
|
-
}));
|
|
28497
|
-
layers.push(L({
|
|
28498
|
-
id: "chrtland-poi",
|
|
28499
|
-
type: "symbol",
|
|
28500
|
-
source: src,
|
|
28501
|
-
"source-layer": "land_poi",
|
|
28502
|
-
minzoom: 13,
|
|
28503
|
-
filter: FITS,
|
|
28504
|
-
layout: {
|
|
28505
|
-
"text-field": ["get", "name"],
|
|
28506
|
-
"text-font": FONT_ITALIC,
|
|
28507
|
-
"text-size": ["interpolate", ["linear"], ["zoom"], 13, 8, 14, 9.5],
|
|
28508
|
-
"text-offset": anchorOffset(0.7),
|
|
28509
|
-
"text-anchor": ANCHOR,
|
|
28510
|
-
"text-padding": 5,
|
|
28511
|
-
"text-allow-overlap": false,
|
|
28512
|
-
"symbol-sort-key": 5
|
|
28513
|
-
},
|
|
28514
|
-
paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
|
|
28515
|
-
}));
|
|
28516
|
-
|
|
28517
|
-
|
|
28518
|
-
|
|
28519
|
-
|
|
28520
|
-
var BAND_PRIORITY_RANGES = {
|
|
28521
|
-
"1": { minzoom: 0 },
|
|
28522
|
-
"2": { minzoom: 5 },
|
|
28523
|
-
"3": { minzoom: 7 },
|
|
28524
|
-
"4": { minzoom: 9 },
|
|
28525
|
-
"5": { minzoom: 11 },
|
|
28526
|
-
"6": { minzoom: 11 }
|
|
28527
|
-
};
|
|
28528
|
-
var BAND_DATA_MINZOOM = {
|
|
28529
|
-
"1": 0,
|
|
28530
|
-
"2": 5,
|
|
28531
|
-
"3": 7,
|
|
28532
|
-
"4": 9,
|
|
28533
|
-
"5": 11,
|
|
28534
|
-
"6": 13
|
|
28535
|
-
};
|
|
28536
|
-
function dominatedVariantMaxzoom(D) {
|
|
28537
|
-
return Math.max(BAND_PRIORITY_RANGES[D].minzoom, BAND_DATA_MINZOOM[D]);
|
|
28538
|
-
}
|
|
28539
|
-
var DOMINANCE_EXEMPT_FILL_CLASSES = /* @__PURE__ */ new Set([
|
|
28540
|
-
"LNDARE",
|
|
28541
|
-
"DEPARE",
|
|
28542
|
-
"UNSARE",
|
|
28543
|
-
"LAKARE",
|
|
28544
|
-
"DRGARE",
|
|
28545
|
-
"SEAARE"
|
|
28546
|
-
]);
|
|
28547
|
-
function isExemptOpaqueFill(layer) {
|
|
28548
|
-
if (layer.type !== "fill") return false;
|
|
28549
|
-
for (const cls of DOMINANCE_EXEMPT_FILL_CLASSES) {
|
|
28550
|
-
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);
|
|
28551
28913
|
}
|
|
28552
|
-
return
|
|
28914
|
+
return layers;
|
|
28553
28915
|
}
|
|
28554
|
-
|
|
28555
|
-
|
|
28556
|
-
|
|
28557
|
-
|
|
28558
|
-
|
|
28559
|
-
|
|
28560
|
-
|
|
28561
|
-
|
|
28562
|
-
|
|
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"
|
|
28563
28971
|
}
|
|
28564
|
-
return Math.max(classMinzoom, BAND_PRIORITY_RANGES[B].minzoom);
|
|
28565
|
-
}
|
|
28566
|
-
var BAND_DRAW_ORDER = ["1", "2", "3", "4", "5", "6"];
|
|
28567
|
-
var CHART_BAND_CODES = {
|
|
28568
|
-
Overview: ["1"],
|
|
28569
|
-
General: ["2"],
|
|
28570
|
-
Coastal: ["3"],
|
|
28571
|
-
Approach: ["4"],
|
|
28572
|
-
Harbor: ["5", "6"]
|
|
28573
|
-
};
|
|
28574
|
-
var CHART_CASCADE_CODES = {
|
|
28575
|
-
Overview: ["1"],
|
|
28576
|
-
General: ["1", "2"],
|
|
28577
|
-
Coastal: ["1", "2", "3"],
|
|
28578
|
-
Approach: ["1", "2", "3", "4"],
|
|
28579
|
-
Harbor: ["1", "2", "3", "4", "5", "6"]
|
|
28580
28972
|
};
|
|
28581
|
-
|
|
28582
|
-
"
|
|
28583
|
-
|
|
28584
|
-
|
|
28585
|
-
)
|
|
28586
|
-
];
|
|
28587
|
-
function normalizeStyleMode(raw) {
|
|
28588
|
-
if (raw === null || raw === void 0 || raw === "") return "continuous";
|
|
28589
|
-
const lower = raw.toLowerCase();
|
|
28590
|
-
return CHRT_STYLE_MODES.find((mode) => mode.toLowerCase() === lower) ?? null;
|
|
28591
|
-
}
|
|
28592
|
-
function styleModeBand(mode) {
|
|
28593
|
-
return mode === "continuous" ? null : mode.slice("chart:".length);
|
|
28594
|
-
}
|
|
28595
|
-
function bandFilterExpression(codes) {
|
|
28596
|
-
const list = Array.isArray(codes) ? codes : [codes];
|
|
28597
|
-
if (list.length === 1) {
|
|
28598
|
-
return ["==", ["slice", ["get", "_cell_id"], 2, 3], list[0]];
|
|
28599
|
-
}
|
|
28600
|
-
return ["in", ["slice", ["get", "_cell_id"], 2, 3], ["literal", [...list]]];
|
|
28601
|
-
}
|
|
28602
|
-
function combineWithBandFilter(baseFilter, bandFilter) {
|
|
28603
|
-
if (!baseFilter) return bandFilter;
|
|
28604
|
-
if (baseFilter[0] === "all") {
|
|
28605
|
-
return ["all", ...baseFilter.slice(1), bandFilter];
|
|
28606
|
-
}
|
|
28607
|
-
return ["all", baseFilter, bandFilter];
|
|
28608
|
-
}
|
|
28609
|
-
var NOT_DOMINATED_FILTER = [
|
|
28610
|
-
"all",
|
|
28611
|
-
["!", ["has", "_dominated_by_min"]],
|
|
28612
|
-
["!=", ["get", "_dominated"], true]
|
|
28613
|
-
];
|
|
28614
|
-
function dominatedByMinFilter(D) {
|
|
28615
|
-
const dInt = parseInt(D, 10);
|
|
28616
|
-
if (D === "6") {
|
|
28617
|
-
return [
|
|
28618
|
-
"any",
|
|
28619
|
-
["==", ["get", "_dominated_by_min"], dInt],
|
|
28620
|
-
["==", ["get", "_dominated"], true]
|
|
28621
|
-
];
|
|
28622
|
-
}
|
|
28623
|
-
return ["==", ["get", "_dominated_by_min"], dInt];
|
|
28624
|
-
}
|
|
28625
|
-
function withinBboxFilter(bbox) {
|
|
28626
|
-
const [w, s] = bbox.sw;
|
|
28627
|
-
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;
|
|
28628
28978
|
return [
|
|
28629
|
-
|
|
28630
|
-
|
|
28631
|
-
|
|
28632
|
-
|
|
28633
|
-
|
|
28634
|
-
|
|
28635
|
-
|
|
28636
|
-
|
|
28637
|
-
|
|
28638
|
-
|
|
28639
|
-
|
|
28640
|
-
|
|
28641
|
-
|
|
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
|
+
})
|
|
28642
29099
|
];
|
|
28643
29100
|
}
|
|
28644
|
-
function
|
|
28645
|
-
const
|
|
28646
|
-
const [
|
|
28647
|
-
|
|
28648
|
-
|
|
28649
|
-
|
|
28650
|
-
|
|
28651
|
-
|
|
28652
|
-
|
|
28653
|
-
|
|
28654
|
-
|
|
28655
|
-
|
|
28656
|
-
|
|
28657
|
-
|
|
28658
|
-
|
|
28659
|
-
|
|
28660
|
-
|
|
28661
|
-
|
|
28662
|
-
|
|
28663
|
-
|
|
28664
|
-
|
|
28665
|
-
|
|
28666
|
-
|
|
28667
|
-
|
|
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
|
+
];
|
|
28668
29188
|
}
|
|
28669
29189
|
|
|
28670
29190
|
// src/atlas/band-collapse.ts
|
|
28671
|
-
function collapsedDominanceGate(B, minzoom) {
|
|
28672
|
-
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
|
+
];
|
|
28673
29195
|
const bInt = parseInt(B, 10);
|
|
28674
29196
|
for (const D of BAND_DRAW_ORDER) {
|
|
28675
29197
|
const dInt = parseInt(D, 10);
|
|
@@ -28698,7 +29220,7 @@ function makeCollapsedBandVariant(layer, B, isExempt) {
|
|
|
28698
29220
|
filter: combineWithBandFilter(layer.filter, [
|
|
28699
29221
|
"all",
|
|
28700
29222
|
bandFilterExpression(B),
|
|
28701
|
-
collapsedDominanceGate(B, minzoom)
|
|
29223
|
+
collapsedDominanceGate(B, minzoom, generalisedAreaCeiling(layer, B))
|
|
28702
29224
|
]),
|
|
28703
29225
|
minzoom
|
|
28704
29226
|
};
|
|
@@ -28833,6 +29355,7 @@ function makeBandVariants(layer, B) {
|
|
|
28833
29355
|
delete exempt.maxzoom;
|
|
28834
29356
|
return [exempt];
|
|
28835
29357
|
}
|
|
29358
|
+
const ceiling = generalisedAreaCeiling(layer, B);
|
|
28836
29359
|
const notDom = {
|
|
28837
29360
|
...layer,
|
|
28838
29361
|
id: `${layer.id}-band${B}`,
|
|
@@ -28843,6 +29366,7 @@ function makeBandVariants(layer, B) {
|
|
|
28843
29366
|
minzoom
|
|
28844
29367
|
};
|
|
28845
29368
|
delete notDom.maxzoom;
|
|
29369
|
+
if (ceiling !== null && ceiling > minzoom) notDom.maxzoom = ceiling;
|
|
28846
29370
|
variants.push(notDom);
|
|
28847
29371
|
const bInt = parseInt(B, 10);
|
|
28848
29372
|
for (const D of BAND_DRAW_ORDER) {
|
|
@@ -28992,12 +29516,43 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
28992
29516
|
paint: { "background-color": resolveColor("NODTA", palette) }
|
|
28993
29517
|
};
|
|
28994
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
|
+
}
|
|
28995
29547
|
if (opts.land) {
|
|
28996
29548
|
layers.push(...landLayers({
|
|
28997
29549
|
palette,
|
|
28998
29550
|
...opts.relief ? { relief: true } : {}
|
|
28999
29551
|
}));
|
|
29000
29552
|
}
|
|
29553
|
+
if (opts.base) {
|
|
29554
|
+
layers.push(...baseInlandWaterLayers({ palette }));
|
|
29555
|
+
}
|
|
29001
29556
|
const labelLayers = [];
|
|
29002
29557
|
const chartCascade = opts.chartMode ? CHART_CASCADE_CODES[opts.chartMode.band] : null;
|
|
29003
29558
|
const chartRequestedCodes = opts.chartMode ? CHART_BAND_CODES[opts.chartMode.band] : null;
|
|
@@ -29114,6 +29669,22 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
29114
29669
|
}
|
|
29115
29670
|
}
|
|
29116
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
|
+
}
|
|
29117
29688
|
if (opts.labels === "placename") {
|
|
29118
29689
|
labelLayers.push(...buildPlaceNameLayers(sourceId));
|
|
29119
29690
|
} else {
|
|
@@ -29143,13 +29714,115 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
29143
29714
|
const symbolSet = opts.symbolSet ?? "s52";
|
|
29144
29715
|
const spriteBase = `${opts.tileBaseUrl ?? "https://tiles.chrt.co"}/v1/sprites`;
|
|
29145
29716
|
const isLndareFill = (l) => /^s52-LNDARE-fill-band\d+$/.test(l.id);
|
|
29146
|
-
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
|
+
};
|
|
29147
29722
|
const emitted = opts.land ? (() => {
|
|
29148
|
-
const
|
|
29149
|
-
const
|
|
29150
|
-
const
|
|
29151
|
-
|
|
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)];
|
|
29152
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
|
+
}
|
|
29153
29826
|
const style = {
|
|
29154
29827
|
version: 8,
|
|
29155
29828
|
name: `CHRT ${SYMBOL_SET_LABELS[symbolSet]} ${PALETTE_NAMES[palette]}${opts.chartMode ? ` \u2014 chart:${opts.chartMode.band}` : ""}`,
|
|
@@ -29161,6 +29834,7 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
29161
29834
|
"chrt:layer-count": emitted.length,
|
|
29162
29835
|
"chrt:land": Boolean(opts.land),
|
|
29163
29836
|
"chrt:relief": Boolean(opts.relief),
|
|
29837
|
+
"chrt:base": Boolean(opts.base),
|
|
29164
29838
|
"chrt:render-mode": opts.chartMode ? `chart:${opts.chartMode.band}` : "continuous",
|
|
29165
29839
|
"chrt:description": "IHO S-52 portrayal rules for Electronic Navigational Charts (ENCs). Generated from the CHRT atlas engine."
|
|
29166
29840
|
},
|
|
@@ -29654,6 +30328,7 @@ var Chrt = class {
|
|
|
29654
30328
|
interact;
|
|
29655
30329
|
featuresEp;
|
|
29656
30330
|
searchEp;
|
|
30331
|
+
poiEp;
|
|
29657
30332
|
queryEp;
|
|
29658
30333
|
http;
|
|
29659
30334
|
tileAuth;
|
|
@@ -29675,6 +30350,7 @@ var Chrt = class {
|
|
|
29675
30350
|
this.weather = new WeatherEndpoint(this.http);
|
|
29676
30351
|
this.featuresEp = new FeaturesEndpoint(this.http);
|
|
29677
30352
|
this.searchEp = new SearchEndpoint(this.http);
|
|
30353
|
+
this.poiEp = new PoiEndpoint(this.http);
|
|
29678
30354
|
this.queryEp = new QueryEndpoint(this.http);
|
|
29679
30355
|
this.offline = new OfflineEndpoint(this.http, config.deviceId, config.manifestPath);
|
|
29680
30356
|
this.interact = new InteractClient(this.http);
|
|
@@ -29710,6 +30386,26 @@ var Chrt = class {
|
|
|
29710
30386
|
async search(options) {
|
|
29711
30387
|
return this.searchEp.find(options);
|
|
29712
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
|
+
}
|
|
29713
30409
|
/** Spatial proximity query around a point */
|
|
29714
30410
|
async query(options) {
|
|
29715
30411
|
return this.queryEp.nearby(options);
|
|
@@ -29880,6 +30576,18 @@ var Chrt = class {
|
|
|
29880
30576
|
return generateStyle(palette, {
|
|
29881
30577
|
region: this.region,
|
|
29882
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),
|
|
29883
30591
|
...styleOpts,
|
|
29884
30592
|
// The vessel's safety depth IS the style's safety contour — same number, same
|
|
29885
30593
|
// meaning. Threading it here is what stops the chart shading at one depth while
|
|
@@ -29953,9 +30661,21 @@ var Chrt = class {
|
|
|
29953
30661
|
* Pre-mint the tile token for a region. Optional — `transformRequest()` warms
|
|
29954
30662
|
* the cache automatically — but useful if you want to surface a mint error
|
|
29955
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.
|
|
29956
30676
|
*/
|
|
29957
30677
|
async warmTiles(region) {
|
|
29958
|
-
|
|
30678
|
+
return this.tileAuth.getToken(region);
|
|
29959
30679
|
}
|
|
29960
30680
|
};
|
|
29961
30681
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -29966,9 +30686,13 @@ var Chrt = class {
|
|
|
29966
30686
|
ATON_EXPRESSION_CLASSES,
|
|
29967
30687
|
BAND_DRAW_ORDER,
|
|
29968
30688
|
BAND_PRIORITY_RANGES,
|
|
30689
|
+
BASE_MAXZOOM,
|
|
30690
|
+
BASE_MINZOOM,
|
|
30691
|
+
BASE_REGION,
|
|
29969
30692
|
BRIDGE_QUERY_LAYERS,
|
|
29970
30693
|
BUILD_MAXZOOM,
|
|
29971
30694
|
BUILD_MINZOOM,
|
|
30695
|
+
BUILD_PARAM,
|
|
29972
30696
|
CHART_BAND_CODES,
|
|
29973
30697
|
CHART_CASCADE_CODES,
|
|
29974
30698
|
CHART_SECTIONS,
|
|
@@ -30150,6 +30874,7 @@ var Chrt = class {
|
|
|
30150
30874
|
scaleSizeValue,
|
|
30151
30875
|
scaminToMinZoom,
|
|
30152
30876
|
styleModeBand,
|
|
30877
|
+
tileTemplate,
|
|
30153
30878
|
withinBboxFilter,
|
|
30154
30879
|
worldMinusBboxPolygon
|
|
30155
30880
|
});
|