@chrtco/sdk 0.10.19 → 0.10.21
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/index.cjs +367 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +367 -58
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -588,8 +588,8 @@ var RELIEF_REGION = "CHRT_RELIEF";
|
|
|
588
588
|
var BASE_REGION = "CHRT_BASE";
|
|
589
589
|
var BASE_MINZOOM = 0;
|
|
590
590
|
var BASE_MAXZOOM = 11;
|
|
591
|
-
var RELIEF_MINZOOM =
|
|
592
|
-
var RELIEF_MAXZOOM =
|
|
591
|
+
var RELIEF_MINZOOM = 9;
|
|
592
|
+
var RELIEF_MAXZOOM = 12;
|
|
593
593
|
var RELIEF_TILE_SIZE = 512;
|
|
594
594
|
var BUILD_PARAM = "v";
|
|
595
595
|
function tileTemplate(base, region, extension, buildId) {
|
|
@@ -23051,14 +23051,19 @@ var ORIENTATION_BAND_SORT_KEY = [
|
|
|
23051
23051
|
["slice", ["coalesce", ["get", "_cell_id"], "??9???"], 2, 3],
|
|
23052
23052
|
9
|
|
23053
23053
|
];
|
|
23054
|
-
function
|
|
23054
|
+
function orientationSortKey(tier) {
|
|
23055
|
+
const band = ["*", 100, ORIENTATION_BAND_SORT_KEY];
|
|
23056
|
+
if (!tier) return band;
|
|
23057
|
+
return ["+", band, tier];
|
|
23058
|
+
}
|
|
23059
|
+
function orientationLabelLayout(extras = {}, tier) {
|
|
23055
23060
|
return {
|
|
23056
23061
|
"text-anchor": "center",
|
|
23057
23062
|
"text-allow-overlap": false,
|
|
23058
23063
|
"text-ignore-placement": false,
|
|
23059
23064
|
"text-optional": true,
|
|
23060
23065
|
"text-padding": 15,
|
|
23061
|
-
"symbol-sort-key":
|
|
23066
|
+
"symbol-sort-key": orientationSortKey(tier),
|
|
23062
23067
|
...extras
|
|
23063
23068
|
};
|
|
23064
23069
|
}
|
|
@@ -23068,15 +23073,17 @@ function tieredSizeCurve(tierExpr, stops) {
|
|
|
23068
23073
|
if (i > 0) {
|
|
23069
23074
|
const [pz, pv] = stops[i - 1];
|
|
23070
23075
|
const [nz, nv] = stops[i];
|
|
23071
|
-
if (pv.some((v, t) => v === 0 && nv[t] > 0)) {
|
|
23076
|
+
if (pv.some((v, t) => v === 0 && nv[t] > 0 || v > 0 && nv[t] === 0)) {
|
|
23072
23077
|
const hz = Math.round((nz - 0.2) * 10) / 10;
|
|
23073
23078
|
if (hz > pz) {
|
|
23074
23079
|
const f = (hz - pz) / (nz - pz);
|
|
23075
23080
|
withHolds.push([
|
|
23076
23081
|
hz,
|
|
23077
|
-
pv.map(
|
|
23078
|
-
(v
|
|
23079
|
-
|
|
23082
|
+
pv.map((v, t) => {
|
|
23083
|
+
if (v === 0 && nv[t] > 0) return 0;
|
|
23084
|
+
if (v > 0 && nv[t] === 0) return v;
|
|
23085
|
+
return Math.round((v + (nv[t] - v) * f) * 10) / 10;
|
|
23086
|
+
})
|
|
23080
23087
|
]);
|
|
23081
23088
|
}
|
|
23082
23089
|
}
|
|
@@ -23105,6 +23112,19 @@ var ORIENTATION_HALO_WIDTH = [
|
|
|
23105
23112
|
16,
|
|
23106
23113
|
0.7
|
|
23107
23114
|
];
|
|
23115
|
+
var LAND_HALO_WIDTH = [
|
|
23116
|
+
"interpolate",
|
|
23117
|
+
["linear"],
|
|
23118
|
+
["zoom"],
|
|
23119
|
+
8,
|
|
23120
|
+
1.6,
|
|
23121
|
+
11,
|
|
23122
|
+
1.7,
|
|
23123
|
+
14,
|
|
23124
|
+
1.8,
|
|
23125
|
+
16,
|
|
23126
|
+
1.8
|
|
23127
|
+
];
|
|
23108
23128
|
var LABEL_BEHAVIOR = {
|
|
23109
23129
|
// Orientation labels — render once across all bands.
|
|
23110
23130
|
SEAARE: "orientation-cross-band",
|
|
@@ -23178,6 +23198,9 @@ var LABEL_PRIORITY = {
|
|
|
23178
23198
|
};
|
|
23179
23199
|
var DEFAULT_LABEL_PRIORITY = 450;
|
|
23180
23200
|
var ORIENTATION_LABEL_PRIORITY = 900;
|
|
23201
|
+
var MAJOR_WATER_TIER_MAX = 3;
|
|
23202
|
+
var MAJOR_WATER_LABEL_PRIORITY = 280;
|
|
23203
|
+
var ISLAND_LABEL_HOIST_MINZOOM = 12;
|
|
23181
23204
|
function getLabelPriority(layerName) {
|
|
23182
23205
|
if (getLabelBehavior(layerName) === "orientation-cross-band") {
|
|
23183
23206
|
return ORIENTATION_LABEL_PRIORITY;
|
|
@@ -23350,21 +23373,25 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
23350
23373
|
// LNDRGN labels until coastal zoom. The Carto basemap already labels
|
|
23351
23374
|
// major land regions at z5-z6. Columns: t1 major region, t2 local
|
|
23352
23375
|
// point (also the default).
|
|
23376
|
+
// Tier 2 (local point: "Bird Island", "Cottage Park") gets the same
|
|
23377
|
+
// lift as LNDARE's small islands — it is the other half of "show me
|
|
23378
|
+
// what this land is". Tier 1 is unchanged.
|
|
23353
23379
|
textSize: tieredSizeCurve(tierExpr, [
|
|
23354
23380
|
[5, [0, 0]],
|
|
23355
23381
|
[7, [11, 0]],
|
|
23356
23382
|
[9, [12, 0]],
|
|
23357
|
-
[11, [12,
|
|
23358
|
-
[13, [11, 11]],
|
|
23359
|
-
[16, [10,
|
|
23383
|
+
[11, [12, 10]],
|
|
23384
|
+
[13, [11.5, 11.5]],
|
|
23385
|
+
[16, [10.5, 11.5]]
|
|
23360
23386
|
]),
|
|
23361
23387
|
textColor: DARK_BG,
|
|
23362
23388
|
textHaloColor: COLOR.LAND_REGION,
|
|
23363
|
-
|
|
23389
|
+
// Land halo, not the shared taper — see LAND_HALO_WIDTH.
|
|
23390
|
+
textHaloWidth: LAND_HALO_WIDTH,
|
|
23364
23391
|
textOffset: [0, 0],
|
|
23365
23392
|
textFont: ["Open Sans Regular"],
|
|
23366
23393
|
minzoom: 5,
|
|
23367
|
-
layout: orientationLabelLayout()
|
|
23394
|
+
layout: orientationLabelLayout({}, tierExpr)
|
|
23368
23395
|
};
|
|
23369
23396
|
}
|
|
23370
23397
|
case "LNDMRK":
|
|
@@ -23529,14 +23556,56 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
23529
23556
|
// at approach scale (z9) when the user has zoomed in enough that
|
|
23530
23557
|
// individual banks are useful context. Columns: t1 ocean, t2 sea/gulf,
|
|
23531
23558
|
// t3 bay/sound, t4 anchorage, default.
|
|
23559
|
+
// ── The zoom ladder (owner, 2026-08-31) ────────────────────────────
|
|
23560
|
+
//
|
|
23561
|
+
// "When zoomed in I want water area labels (Nubble Channel etc.); as
|
|
23562
|
+
// I zoom out, the major stuff."
|
|
23563
|
+
//
|
|
23564
|
+
// The old curve gave EVERY tier a size at z9-z11, and the field that
|
|
23565
|
+
// produced is the defect. Rendered in isolation at Boston z10.4
|
|
23566
|
+
// (prod 48404b1d): ten copies of "North Atlantic Ocean" scattered
|
|
23567
|
+
// across the harbour, a dozen ledges and shoals — Finns, Boston,
|
|
23568
|
+
// Thieves, Ultonia, Harding, Tinkers, Dread, Newcomb, Three and
|
|
23569
|
+
// One-Half Fathom — and, competing with all of them for the same
|
|
23570
|
+
// pixels, the bays the user actually needs: Broad Sound, Dorchester,
|
|
23571
|
+
// Quincy, Hull, Hingham, President Roads. Nothing was missing. The
|
|
23572
|
+
// majors simply had no better claim on the space than a ledge did.
|
|
23573
|
+
//
|
|
23574
|
+
// So the ladder now steps tiers OUT as well as in:
|
|
23575
|
+
//
|
|
23576
|
+
// t1 ocean overview only — gone by z11. "North Atlantic
|
|
23577
|
+
// Ocean" inside Boston Harbor is noise, not context.
|
|
23578
|
+
// t2 sea/gulf through approach — gone by z13.
|
|
23579
|
+
// t3 bay/sound the spine: visible z5 through z16, peaking where
|
|
23580
|
+
// a bay IS the answer to "where am I".
|
|
23581
|
+
// t4 anchorage harbour detail, z13+ (unchanged).
|
|
23582
|
+
// t5 default channels, coves, ledges. Floor UNCHANGED at z9 —
|
|
23583
|
+
// and deliberately so. "Nubble Channel" is already
|
|
23584
|
+
// a z11 feature without touching this curve: NOAA
|
|
23585
|
+
// encodes it only in band-5 cells (US5BOSCD ×2,
|
|
23586
|
+
// US5BOSCE), band-5 features first exist in tiles at
|
|
23587
|
+
// z11, and the cell-band fan floors their label
|
|
23588
|
+
// variant at z11 too. The owner's "channels/coves at
|
|
23589
|
+
// z11+" is therefore satisfied by the DATA, and
|
|
23590
|
+
// moving this floor would only have cost the
|
|
23591
|
+
// offshore relief its approach-zoom entry (Georges
|
|
23592
|
+
// Bank, Hudson Canyon — the population this tier was
|
|
23593
|
+
// built for, pinned by seaare-tier.test.ts).
|
|
23594
|
+
//
|
|
23595
|
+
// What crowds the bays at z10 is fixed by RANK, not by floors:
|
|
23596
|
+
// `orientationSortKey` now places tier 3 ahead of tier 5 inside a
|
|
23597
|
+
// band, and the padding ladder below thins the field so that
|
|
23598
|
+
// ordering decides the survivors.
|
|
23599
|
+
//
|
|
23600
|
+
// Columns: t1 ocean, t2 sea/gulf, t3 bay/sound, t4 anchorage, default.
|
|
23532
23601
|
textSize: tieredSizeCurve(tierExpr, [
|
|
23533
23602
|
[3, [14, 8, 0, 0, 0]],
|
|
23534
23603
|
[5, [16, 13, 9, 0, 0]],
|
|
23535
23604
|
[7, [14, 14, 12, 0, 0]],
|
|
23536
|
-
[9, [
|
|
23537
|
-
[11, [
|
|
23538
|
-
[13, [
|
|
23539
|
-
[16, [
|
|
23605
|
+
[9, [11, 13, 14, 0, 9]],
|
|
23606
|
+
[11, [0, 11, 14, 0, 11]],
|
|
23607
|
+
[13, [0, 0, 13, 10, 11]],
|
|
23608
|
+
[16, [0, 0, 12, 11, 10]]
|
|
23540
23609
|
]),
|
|
23541
23610
|
textColor: DARK_BG,
|
|
23542
23611
|
textHaloColor: COLOR.WATER_DEEP,
|
|
@@ -23560,8 +23629,41 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
23560
23629
|
// the tiles and replaces this fan outright — do not restore
|
|
23561
23630
|
// always-paint on a starvation report; flip `labels: "placename"`
|
|
23562
23631
|
// instead once the build carries the layer.
|
|
23563
|
-
|
|
23564
|
-
|
|
23632
|
+
//
|
|
23633
|
+
// ── The padding is the zoom LADDER (owner, 2026-08-31) ───────────
|
|
23634
|
+
//
|
|
23635
|
+
// "When zoomed in I want water area labels (Nubble Channel etc.);
|
|
23636
|
+
// as I zoom out, the major stuff."
|
|
23637
|
+
//
|
|
23638
|
+
// A flat 2 px moat gives every name in view the same claim on the
|
|
23639
|
+
// space, so at z10 all 34 band-4 names in one Boston tile compete
|
|
23640
|
+
// as peers and the result reads as clutter rather than as
|
|
23641
|
+
// orientation. Widening the moat as the user zooms OUT thins that
|
|
23642
|
+
// field by collision alone: with a 20 px moat only a few names
|
|
23643
|
+
// survive per screen, and `orientationSortKey` decides which —
|
|
23644
|
+
// lowest band first, then lowest class tier, i.e. the bays and
|
|
23645
|
+
// sounds. Narrowing it back to 2 px by z13 restores exactly
|
|
23646
|
+
// today's harbour behaviour, where the channels and coves should
|
|
23647
|
+
// all be readable.
|
|
23648
|
+
//
|
|
23649
|
+
// Same mechanism (and the same lesson) as the lake-label grid:
|
|
23650
|
+
// a flat padding let six copies of "Lake Ontario" crowd z8.9.
|
|
23651
|
+
// Nothing is filtered and no name is invented — the ladder is
|
|
23652
|
+
// entirely a collision-space decision.
|
|
23653
|
+
"text-padding": [
|
|
23654
|
+
"interpolate",
|
|
23655
|
+
["linear"],
|
|
23656
|
+
["zoom"],
|
|
23657
|
+
5,
|
|
23658
|
+
20,
|
|
23659
|
+
9,
|
|
23660
|
+
14,
|
|
23661
|
+
11,
|
|
23662
|
+
8,
|
|
23663
|
+
13,
|
|
23664
|
+
2
|
|
23665
|
+
]
|
|
23666
|
+
}, tierExpr)
|
|
23565
23667
|
};
|
|
23566
23668
|
}
|
|
23567
23669
|
case "LAKARE": {
|
|
@@ -23582,7 +23684,7 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
23582
23684
|
textOffset: [0, 0],
|
|
23583
23685
|
textFont: ["Open Sans Italic"],
|
|
23584
23686
|
minzoom: 7,
|
|
23585
|
-
layout: orientationLabelLayout()
|
|
23687
|
+
layout: orientationLabelLayout({}, tierExpr)
|
|
23586
23688
|
};
|
|
23587
23689
|
}
|
|
23588
23690
|
case "RIVERS": {
|
|
@@ -23603,7 +23705,7 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
23603
23705
|
textOffset: [0, 0],
|
|
23604
23706
|
textFont: ["Open Sans Italic"],
|
|
23605
23707
|
minzoom: 7,
|
|
23606
|
-
layout: orientationLabelLayout()
|
|
23708
|
+
layout: orientationLabelLayout({}, tierExpr)
|
|
23607
23709
|
};
|
|
23608
23710
|
}
|
|
23609
23711
|
case "CANALS":
|
|
@@ -23634,21 +23736,31 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
23634
23736
|
return {
|
|
23635
23737
|
type: "symbol",
|
|
23636
23738
|
textField: ["get", "OBJNAM"],
|
|
23739
|
+
// Tier 2 is the SMALL island — the one the owner cannot identify.
|
|
23740
|
+
// Its old curve peaked at 10 px and fell to 10 px, i.e. it never got
|
|
23741
|
+
// its own scale: a name that only exists because you zoomed in was
|
|
23742
|
+
// rendered at the size used for ambient context. It now grows through
|
|
23743
|
+
// harbour zoom (10 → 11.5) while tier 1, the major island already
|
|
23744
|
+
// legible from coastal zoom, is left almost where it was. Small
|
|
23745
|
+
// absolute numbers on purpose: the ask was "clear", not "large" —
|
|
23746
|
+
// the legibility comes mostly from LAND_HALO_WIDTH and the opacity
|
|
23747
|
+
// scale, both of which cost no extra footprint.
|
|
23637
23748
|
textSize: tieredSizeCurve(tierExpr, [
|
|
23638
23749
|
[5, [8, 0]],
|
|
23639
23750
|
[7, [10, 0]],
|
|
23640
23751
|
[9, [11, 0]],
|
|
23641
|
-
[11, [11,
|
|
23642
|
-
[13, [
|
|
23643
|
-
[16, [
|
|
23752
|
+
[11, [11.5, 10]],
|
|
23753
|
+
[13, [11.5, 11.5]],
|
|
23754
|
+
[16, [10.5, 11.5]]
|
|
23644
23755
|
]),
|
|
23645
23756
|
textColor: DARK_BG,
|
|
23646
23757
|
textHaloColor: COLOR.LAND_NATURAL,
|
|
23647
|
-
|
|
23758
|
+
// Land halo, not the shared taper — see LAND_HALO_WIDTH.
|
|
23759
|
+
textHaloWidth: LAND_HALO_WIDTH,
|
|
23648
23760
|
textOffset: [0, 0],
|
|
23649
23761
|
textFont: ["Open Sans Regular"],
|
|
23650
23762
|
minzoom: 5,
|
|
23651
|
-
layout: orientationLabelLayout()
|
|
23763
|
+
layout: orientationLabelLayout({}, tierExpr)
|
|
23652
23764
|
};
|
|
23653
23765
|
}
|
|
23654
23766
|
case "BUAARE": {
|
|
@@ -23673,7 +23785,7 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
23673
23785
|
minzoom: 3,
|
|
23674
23786
|
layout: orientationLabelLayout({
|
|
23675
23787
|
"text-letter-spacing": 0.05
|
|
23676
|
-
})
|
|
23788
|
+
}, tierExpr)
|
|
23677
23789
|
};
|
|
23678
23790
|
}
|
|
23679
23791
|
case "BRIDGE":
|
|
@@ -23765,7 +23877,7 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
23765
23877
|
textOffset: [0, 0],
|
|
23766
23878
|
textFont: ["Open Sans Regular"],
|
|
23767
23879
|
minzoom: 7,
|
|
23768
|
-
layout: orientationLabelLayout()
|
|
23880
|
+
layout: orientationLabelLayout({}, tierExpr)
|
|
23769
23881
|
};
|
|
23770
23882
|
}
|
|
23771
23883
|
case "SBDARE":
|
|
@@ -28603,8 +28715,7 @@ function worldMinusBboxPolygon(bbox) {
|
|
|
28603
28715
|
|
|
28604
28716
|
// src/atlas/land-layers.ts
|
|
28605
28717
|
var LAND_SURFACE_MINZOOM = BAND_PRIORITY_RANGES["5"].minzoom;
|
|
28606
|
-
var
|
|
28607
|
-
var LAND_SURFACE_LAYERS = (id) => id === "chrtland-base" || id === "chrtland-cover" || id === "chrtland-contour" || id.startsWith("chrtland-fade-");
|
|
28718
|
+
var LAND_SURFACE_LAYERS = (id) => id === "chrtland-base" || id === "chrtland-cover" || id === "chrtland-contour" || id === "chrtland-hillshade" || id.startsWith("chrtland-fade-");
|
|
28608
28719
|
var FADE_PX = 80;
|
|
28609
28720
|
var FADE_BANDS = 16;
|
|
28610
28721
|
function fadeAlpha(r) {
|
|
@@ -28723,10 +28834,11 @@ function landLayers(opts = {}) {
|
|
|
28723
28834
|
}));
|
|
28724
28835
|
if (opts.relief) {
|
|
28725
28836
|
layers.push(L({
|
|
28837
|
+
// No minzoom here: LAND_SURFACE_LAYERS floors it with the rest of the
|
|
28838
|
+
// surface, so the two can never drift apart again.
|
|
28726
28839
|
id: "chrtland-hillshade",
|
|
28727
28840
|
type: "hillshade",
|
|
28728
28841
|
source: demSrc,
|
|
28729
|
-
minzoom: RELIEF_MINZOOM2,
|
|
28730
28842
|
paint: {
|
|
28731
28843
|
"hillshade-exaggeration": T.exaggeration,
|
|
28732
28844
|
"hillshade-shadow-color": T.shadow,
|
|
@@ -28919,7 +29031,19 @@ var NOCOVERAGE_PATTERN = "chrt:hatch-nocoverage";
|
|
|
28919
29031
|
var UNCHARTED_PATTERN = "chrt:hatch-uncharted";
|
|
28920
29032
|
var BASE_THEMES = {
|
|
28921
29033
|
DAY: {
|
|
28922
|
-
earth
|
|
29034
|
+
// ── `earth` is the jurisdiction tan DIMMED (~0.55), not a blue-grey ──
|
|
29035
|
+
// Outside the jurisdiction the map still owes the reader "what is land".
|
|
29036
|
+
// State 3 in the header is "the geography, under a crosshatch", and until
|
|
29037
|
+
// 2026-08-31 that geography was unreadable: earth #2A3439 against sea
|
|
29038
|
+
// #1B2328 measured 1.25:1, so Hispaniola showed charted boxes with tan
|
|
29039
|
+
// land inside and nothing recognisable around them (owner report: "no
|
|
29040
|
+
// land painting down near the DR at some zooms"). Land is warm and sea is
|
|
29041
|
+
// cool because that is what land and sea are; dimming the JURISDICTION
|
|
29042
|
+
// tan rather than inventing a colour says the true thing — this is the
|
|
29043
|
+
// same land, we simply do not carry charts for it. Held against BOTH
|
|
29044
|
+
// neighbours: legible over sea, and never mistakable for the full tan of
|
|
29045
|
+
// a country we do carry. DAY 3.59:1 vs sea, 3.17:1 vs tan.
|
|
29046
|
+
earth: "#807761",
|
|
28923
29047
|
water: "#1B2328",
|
|
28924
29048
|
river: "#24313A",
|
|
28925
29049
|
boundary: "#4A575E",
|
|
@@ -28942,7 +29066,8 @@ var BASE_THEMES = {
|
|
|
28942
29066
|
inlandHalo: "#B9CFDD"
|
|
28943
29067
|
},
|
|
28944
29068
|
DUSK: {
|
|
28945
|
-
|
|
29069
|
+
// Same rule, this palette's tan: 3.03:1 vs sea, 2.97:1 vs tan.
|
|
29070
|
+
earth: "#6F6752",
|
|
28946
29071
|
water: "#161D21",
|
|
28947
29072
|
river: "#1E2930",
|
|
28948
29073
|
boundary: "#3E4A50",
|
|
@@ -28956,7 +29081,10 @@ var BASE_THEMES = {
|
|
|
28956
29081
|
inlandHalo: "#93A9B7"
|
|
28957
29082
|
},
|
|
28958
29083
|
NIGHT: {
|
|
28959
|
-
|
|
29084
|
+
// Same rule. NIGHT is inherently compressed — every tone is dark — so it
|
|
29085
|
+
// reaches 2.06:1 vs sea / 2.44:1 vs tan rather than 3:1. Still 1.8x the
|
|
29086
|
+
// separation it had, and pushing further would collide with the tan.
|
|
29087
|
+
earth: "#4F4A3B",
|
|
28960
29088
|
water: "#10161A",
|
|
28961
29089
|
river: "#182228",
|
|
28962
29090
|
boundary: "#333E44",
|
|
@@ -29103,12 +29231,38 @@ function baseInlandWaterLayers(opts = {}) {
|
|
|
29103
29231
|
const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
|
|
29104
29232
|
const src = opts.sourceId ?? BASE_REGION;
|
|
29105
29233
|
const L = (o) => o;
|
|
29234
|
+
const NE_TIER = ["!", ["has", "kind"]];
|
|
29235
|
+
const PLANET_TIER = ["has", "kind"];
|
|
29236
|
+
const HANDOVER = 9;
|
|
29106
29237
|
return [
|
|
29238
|
+
// ── The fill pair, SPLIT BY TIER at the handover ────────────────────────
|
|
29239
|
+
//
|
|
29240
|
+
// Both tiers share `inland_water` (tile-join concatenates the same layer
|
|
29241
|
+
// name), and an unsplit fill draws BOTH — which means the NE polygon is
|
|
29242
|
+
// painted over the planet one at every zoom. That is not a cosmetic
|
|
29243
|
+
// double-draw: NE 10m carries no interior rings, so its blob has no
|
|
29244
|
+
// ISLANDS in it, and painting it over the planet's holed polygons buries
|
|
29245
|
+
// every island in the lake under "uncharted water". Owner-reported at
|
|
29246
|
+
// Winnipesaukee z11.7 ("there is land (and islands) being covered by the
|
|
29247
|
+
// 'no chart here' boundary"), and measured: rendering the planet tier
|
|
29248
|
+
// alone brings back the whole archipelago — Rattlesnake, Bear, Long
|
|
29249
|
+
// Island, Diamond, the Barndoors — while the NE tier alone is a faceted
|
|
29250
|
+
// blob whose edges bear no relation to the shoreline drawn beneath it.
|
|
29251
|
+
//
|
|
29252
|
+
// So each tier owns its own zoom range, the same handover the shoreline
|
|
29253
|
+
// below uses. Known residue, measured over the whole archive: of the 141
|
|
29254
|
+
// NE lakes we still carry, 138 have planet water beneath them; the
|
|
29255
|
+
// exceptions are two unnamed western-Alaska tundra lakes and a 2e-5 deg²
|
|
29256
|
+
// Lake Michigan sliver, which lose their rough fill above z9 because the
|
|
29257
|
+
// planet has no `water` polygon there at all (checked against the raw
|
|
29258
|
+
// pre-clip NDJSON — not a clip artefact).
|
|
29107
29259
|
L({
|
|
29108
29260
|
id: "chrtbase-inland-water",
|
|
29109
29261
|
type: "fill",
|
|
29110
29262
|
source: src,
|
|
29111
29263
|
"source-layer": "inland_water",
|
|
29264
|
+
filter: NE_TIER,
|
|
29265
|
+
maxzoom: HANDOVER,
|
|
29112
29266
|
paint: { "fill-color": T.inlandWater }
|
|
29113
29267
|
}),
|
|
29114
29268
|
L({
|
|
@@ -29116,6 +29270,26 @@ function baseInlandWaterLayers(opts = {}) {
|
|
|
29116
29270
|
type: "fill",
|
|
29117
29271
|
source: src,
|
|
29118
29272
|
"source-layer": "inland_water",
|
|
29273
|
+
filter: NE_TIER,
|
|
29274
|
+
maxzoom: HANDOVER,
|
|
29275
|
+
paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
|
|
29276
|
+
}),
|
|
29277
|
+
L({
|
|
29278
|
+
id: "chrtbase-inland-water-detail",
|
|
29279
|
+
type: "fill",
|
|
29280
|
+
source: src,
|
|
29281
|
+
"source-layer": "inland_water",
|
|
29282
|
+
filter: PLANET_TIER,
|
|
29283
|
+
minzoom: HANDOVER,
|
|
29284
|
+
paint: { "fill-color": T.inlandWater }
|
|
29285
|
+
}),
|
|
29286
|
+
L({
|
|
29287
|
+
id: "chrtbase-inland-hatch-detail",
|
|
29288
|
+
type: "fill",
|
|
29289
|
+
source: src,
|
|
29290
|
+
"source-layer": "inland_water",
|
|
29291
|
+
filter: PLANET_TIER,
|
|
29292
|
+
minzoom: HANDOVER,
|
|
29119
29293
|
paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
|
|
29120
29294
|
}),
|
|
29121
29295
|
// The rough shoreline (owner ask 2026-08-28). Drawn ABOVE the hatch so
|
|
@@ -29135,13 +29309,47 @@ function baseInlandWaterLayers(opts = {}) {
|
|
|
29135
29309
|
type: "line",
|
|
29136
29310
|
source: src,
|
|
29137
29311
|
"source-layer": "inland_water",
|
|
29138
|
-
filter:
|
|
29312
|
+
filter: NE_TIER,
|
|
29313
|
+
// Hands the shoreline over to the planet tier at ITS floor. Both
|
|
29314
|
+
// tiers live in `inland_water` from z9 up (tile-join concatenates the
|
|
29315
|
+
// same layer name), and their FILLS merge invisibly — but two
|
|
29316
|
+
// outlines do not: NE 10m generalises Winnipesaukee's shape, so its
|
|
29317
|
+
// outline ran straight across the middle of the lake the planet had
|
|
29318
|
+
// drawn correctly around. The coarse line is right only while it is
|
|
29319
|
+
// the only shape we have.
|
|
29320
|
+
maxzoom: HANDOVER,
|
|
29139
29321
|
paint: {
|
|
29140
29322
|
"line-color": T.inlandShore,
|
|
29141
29323
|
"line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1.1, 14, 1.6],
|
|
29142
29324
|
"line-opacity": 0.9
|
|
29143
29325
|
}
|
|
29144
29326
|
}),
|
|
29327
|
+
// The PLANET tier's shoreline (owner ask 2026-08-31: "uncharted small
|
|
29328
|
+
// lakes render with no outline"). It reads its own `inland_shore` layer,
|
|
29329
|
+
// NOT `inland_water`, and that indirection is the whole fix: the mesh
|
|
29330
|
+
// that forced the `!has kind` fence above is a CLIP artefact — an MVT
|
|
29331
|
+
// feature arrives already cut to its own z10 tile, so stroking the
|
|
29332
|
+
// polygon strokes every tile boundary it crosses. The build now walks
|
|
29333
|
+
// those rings, drops the axis-aligned runs that sit on the read zoom's
|
|
29334
|
+
// tile grid, and emits what is left as lines (measured on one New
|
|
29335
|
+
// England chunk: 61 long axis-aligned mesh edges → 5, and the 5 survivors
|
|
29336
|
+
// are 0.02–0.5 tiles off the grid, i.e. real straight water edges).
|
|
29337
|
+
//
|
|
29338
|
+
// Thinner and quieter than the NE line: this is finer geometry describing
|
|
29339
|
+
// water nobody has surveyed, and approximate data should look
|
|
29340
|
+
// approximate.
|
|
29341
|
+
L({
|
|
29342
|
+
id: "chrtbase-inland-shore-detail",
|
|
29343
|
+
type: "line",
|
|
29344
|
+
source: src,
|
|
29345
|
+
"source-layer": "inland_shore",
|
|
29346
|
+
minzoom: HANDOVER,
|
|
29347
|
+
paint: {
|
|
29348
|
+
"line-color": T.inlandShore,
|
|
29349
|
+
"line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.5, 12, 0.9, 15, 1.2],
|
|
29350
|
+
"line-opacity": 0.85
|
|
29351
|
+
}
|
|
29352
|
+
}),
|
|
29145
29353
|
// The lake's name (owner ask 2026-08-28) — the second scoped exception
|
|
29146
29354
|
// to no-names-on-the-basemap, after the states: WATER-BODY names only.
|
|
29147
29355
|
// No maxzoom: an uncharted lake keeps its name at every zoom; over
|
|
@@ -29158,6 +29366,47 @@ function baseInlandWaterLayers(opts = {}) {
|
|
|
29158
29366
|
// deterministic anchor GRID into big lakes — the paper-chart convention
|
|
29159
29367
|
// of repeating a name along a large water body — and this one layer
|
|
29160
29368
|
// renders however many anchors are in view; collision culls the rest.
|
|
29369
|
+
// Planet-tier water-body NAMES (owner ask 2026-08-31: NE 10m names only
|
|
29370
|
+
// 113 US lakes). Emitted BELOW the NE layer on purpose — MapLibre places
|
|
29371
|
+
// symbols topmost-layer-first, so the big NE lake wins collision against
|
|
29372
|
+
// the pond beside it, and both lose to the chart's own labels (every ENC
|
|
29373
|
+
// class is emitted after this pair).
|
|
29374
|
+
//
|
|
29375
|
+
// The size threshold is OSM's own `min_zoom`, carried through the build:
|
|
29376
|
+
// Lake Winnipesaukee reads 10, Squam Lake 12, so a pond simply is not in
|
|
29377
|
+
// the filter until the user is close enough for it to be the answer to
|
|
29378
|
+
// "what is that water". `["zoom"]` in a filter evaluates at integer zoom
|
|
29379
|
+
// levels and removes the feature from placement entirely — verified
|
|
29380
|
+
// against maplibre-gl-native rather than assumed, because a text-size-0
|
|
29381
|
+
// gate would still claim a collision slot.
|
|
29382
|
+
//
|
|
29383
|
+
// The tiers are separate LAYERS, not a filtered split of one — see the
|
|
29384
|
+
// builder for why: an archive that a not-yet-deployed style would
|
|
29385
|
+
// mis-render cannot be promoted first, and this one has to be.
|
|
29386
|
+
L({
|
|
29387
|
+
id: "chrtbase-inland-label-detail",
|
|
29388
|
+
type: "symbol",
|
|
29389
|
+
source: src,
|
|
29390
|
+
"source-layer": "inland_water_label_detail",
|
|
29391
|
+
minzoom: HANDOVER,
|
|
29392
|
+
filter: [">=", ["zoom"], ["get", "min_zoom"]],
|
|
29393
|
+
layout: {
|
|
29394
|
+
"text-field": ["get", "name"],
|
|
29395
|
+
"text-font": ["Open Sans Regular"],
|
|
29396
|
+
"text-size": ["interpolate", ["linear"], ["zoom"], 9, 9, 12, 10.5, 15, 11],
|
|
29397
|
+
"text-max-width": 8,
|
|
29398
|
+
"text-allow-overlap": false,
|
|
29399
|
+
"text-padding": 6,
|
|
29400
|
+
// Bigger water first within the tier — min_zoom IS the size rank.
|
|
29401
|
+
"symbol-sort-key": ["get", "min_zoom"]
|
|
29402
|
+
},
|
|
29403
|
+
paint: {
|
|
29404
|
+
"text-color": T.inlandLabel,
|
|
29405
|
+
"text-halo-color": T.inlandHalo,
|
|
29406
|
+
"text-halo-width": 1,
|
|
29407
|
+
"text-opacity": 0.9
|
|
29408
|
+
}
|
|
29409
|
+
}),
|
|
29161
29410
|
L({
|
|
29162
29411
|
id: "chrtbase-inland-label",
|
|
29163
29412
|
type: "symbol",
|
|
@@ -29389,14 +29638,21 @@ function makeBandVariants(layer, B) {
|
|
|
29389
29638
|
return variants;
|
|
29390
29639
|
}
|
|
29391
29640
|
var CELL_BAND_OPACITY_CURVE = {
|
|
29392
|
-
"1": [[3, 0.5], [5, 0.
|
|
29393
|
-
"2": [[5, 0.
|
|
29394
|
-
"3": [[7, 0.55], [9, 0.
|
|
29395
|
-
"4": [[9, 0.
|
|
29396
|
-
"5": [[11, 0.
|
|
29397
|
-
"6": [[13, 0.75], [16, 0.
|
|
29641
|
+
"1": [[3, 0.5], [5, 0.42], [16, 0.34]],
|
|
29642
|
+
"2": [[5, 0.52], [7, 0.44], [16, 0.34]],
|
|
29643
|
+
"3": [[7, 0.55], [9, 0.46], [16, 0.36]],
|
|
29644
|
+
"4": [[9, 0.58], [11, 0.5], [16, 0.42]],
|
|
29645
|
+
"5": [[11, 0.62], [13, 0.52], [18, 0.44]],
|
|
29646
|
+
"6": [[13, 0.75], [16, 0.6], [22, 0.5]]
|
|
29647
|
+
};
|
|
29648
|
+
var ORIENTATION_OPACITY_SCALE = {
|
|
29649
|
+
LNDARE: 1.45,
|
|
29650
|
+
LNDRGN: 1.4,
|
|
29651
|
+
BUAARE: 1.25
|
|
29398
29652
|
};
|
|
29399
|
-
|
|
29653
|
+
var ORIENTATION_OPACITY_MAX = 0.92;
|
|
29654
|
+
var ORIENTATION_BOOST_MINZOOM = 11;
|
|
29655
|
+
function fanOrientationLabelByCellBand(label, bandFan, objectClass) {
|
|
29400
29656
|
const cellBandExpr = [
|
|
29401
29657
|
"slice",
|
|
29402
29658
|
["coalesce", ["get", "_cell_id"], "??9???"],
|
|
@@ -29404,11 +29660,15 @@ function fanOrientationLabelByCellBand(label, bandFan) {
|
|
|
29404
29660
|
3
|
|
29405
29661
|
];
|
|
29406
29662
|
const variants = [];
|
|
29663
|
+
const scale = objectClass ? ORIENTATION_OPACITY_SCALE[objectClass] ?? 1 : 1;
|
|
29407
29664
|
for (const code of BAND_DRAW_ORDER) {
|
|
29408
29665
|
const range = BAND_PRIORITY_RANGES[code];
|
|
29409
29666
|
const curve = CELL_BAND_OPACITY_CURVE[code];
|
|
29410
29667
|
const opacityExpr = ["interpolate", ["linear"], ["zoom"]];
|
|
29411
|
-
for (const [z, o] of curve)
|
|
29668
|
+
for (const [z, o] of curve) {
|
|
29669
|
+
const s = z >= ORIENTATION_BOOST_MINZOOM ? scale : 1;
|
|
29670
|
+
opacityExpr.push(z, Math.round(Math.min(o * s, ORIENTATION_OPACITY_MAX) * 100) / 100);
|
|
29671
|
+
}
|
|
29412
29672
|
const cellFiltered = combineWithBandFilter(
|
|
29413
29673
|
label.filter,
|
|
29414
29674
|
["==", cellBandExpr, code]
|
|
@@ -29455,6 +29715,33 @@ function fanOrientationLabelByCellBand(label, bandFan) {
|
|
|
29455
29715
|
}
|
|
29456
29716
|
return variants;
|
|
29457
29717
|
}
|
|
29718
|
+
var MAJOR_WATER_TEXT_PADDING = [
|
|
29719
|
+
"interpolate",
|
|
29720
|
+
["linear"],
|
|
29721
|
+
["zoom"],
|
|
29722
|
+
5,
|
|
29723
|
+
24,
|
|
29724
|
+
9,
|
|
29725
|
+
20,
|
|
29726
|
+
13,
|
|
29727
|
+
16,
|
|
29728
|
+
16,
|
|
29729
|
+
14
|
|
29730
|
+
];
|
|
29731
|
+
function splitOrientationFan(fan, hoistedFilter, watermarkFilter, hoistedLayout) {
|
|
29732
|
+
return {
|
|
29733
|
+
hoist: fan.map((l) => ({
|
|
29734
|
+
...l,
|
|
29735
|
+
id: `${l.id}-major`,
|
|
29736
|
+
filter: combineWithBandFilter(l.filter, hoistedFilter),
|
|
29737
|
+
...hoistedLayout ? { layout: { ...l.layout ?? {}, ...hoistedLayout } } : {}
|
|
29738
|
+
})),
|
|
29739
|
+
watermark: fan.map((l) => ({
|
|
29740
|
+
...l,
|
|
29741
|
+
filter: combineWithBandFilter(l.filter, watermarkFilter)
|
|
29742
|
+
}))
|
|
29743
|
+
};
|
|
29744
|
+
}
|
|
29458
29745
|
function byLabelPriorityBottomUp(bundles) {
|
|
29459
29746
|
return [...bundles].sort(
|
|
29460
29747
|
(a, b) => getLabelPriority(b.objectClass) - getLabelPriority(a.objectClass)
|
|
@@ -29554,6 +29841,7 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
29554
29841
|
layers.push(...baseInlandWaterLayers({ palette }));
|
|
29555
29842
|
}
|
|
29556
29843
|
const labelLayers = [];
|
|
29844
|
+
const majorWaterLayers = [];
|
|
29557
29845
|
const chartCascade = opts.chartMode ? CHART_CASCADE_CODES[opts.chartMode.band] : null;
|
|
29558
29846
|
const chartRequestedCodes = opts.chartMode ? CHART_BAND_CODES[opts.chartMode.band] : null;
|
|
29559
29847
|
const bandFan = opts.bandFan ?? "collapsed";
|
|
@@ -29670,19 +29958,18 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
29670
29958
|
}
|
|
29671
29959
|
}
|
|
29672
29960
|
{
|
|
29673
|
-
const TERMINAL_FILL_RE = /^s52-(LNDARE|DEPARE)-fill-band
|
|
29674
|
-
for (
|
|
29675
|
-
const
|
|
29676
|
-
|
|
29677
|
-
|
|
29678
|
-
|
|
29679
|
-
|
|
29680
|
-
|
|
29681
|
-
|
|
29682
|
-
|
|
29683
|
-
|
|
29684
|
-
|
|
29685
|
-
layers.splice(i + 1, 0, clone);
|
|
29961
|
+
const TERMINAL_FILL_RE = /^s52-(?:LNDARE|DEPARE)-fill-band[456]$/;
|
|
29962
|
+
for (const l of layers) {
|
|
29963
|
+
const lay = l;
|
|
29964
|
+
if (!TERMINAL_FILL_RE.test(lay.id)) continue;
|
|
29965
|
+
const floor = lay.minzoom ?? 0;
|
|
29966
|
+
if (floor <= 0) continue;
|
|
29967
|
+
lay.filter = [
|
|
29968
|
+
"all",
|
|
29969
|
+
lay.filter,
|
|
29970
|
+
["any", [">=", ["zoom"], floor], ["==", ["get", "_terminal_fill"], true]]
|
|
29971
|
+
];
|
|
29972
|
+
delete lay.minzoom;
|
|
29686
29973
|
}
|
|
29687
29974
|
}
|
|
29688
29975
|
if (opts.labels === "placename") {
|
|
@@ -29691,7 +29978,23 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
29691
29978
|
for (const { objectClass, label } of bundles) {
|
|
29692
29979
|
if (!label) continue;
|
|
29693
29980
|
if (getLabelBehavior(objectClass) === "orientation-cross-band") {
|
|
29694
|
-
|
|
29981
|
+
if (objectClass === "LNDARE" || objectClass === "SEAARE") {
|
|
29982
|
+
const Z = ISLAND_LABEL_HOIST_MINZOOM;
|
|
29983
|
+
const { hoist, watermark } = objectClass === "LNDARE" ? splitOrientationFan(
|
|
29984
|
+
fanOrientationLabelByCellBand(label, bandFan, objectClass),
|
|
29985
|
+
[">=", ["zoom"], Z],
|
|
29986
|
+
["<", ["zoom"], Z]
|
|
29987
|
+
) : splitOrientationFan(
|
|
29988
|
+
fanOrientationLabelByCellBand(label, bandFan, objectClass),
|
|
29989
|
+
["<=", buildSeaAreaTierExpression(), MAJOR_WATER_TIER_MAX],
|
|
29990
|
+
[">", buildSeaAreaTierExpression(), MAJOR_WATER_TIER_MAX],
|
|
29991
|
+
{ "text-padding": MAJOR_WATER_TEXT_PADDING }
|
|
29992
|
+
);
|
|
29993
|
+
majorWaterLayers.push(...hoist);
|
|
29994
|
+
labelLayers.push(...watermark);
|
|
29995
|
+
continue;
|
|
29996
|
+
}
|
|
29997
|
+
labelLayers.push(...fanOrientationLabelByCellBand(label, bandFan, objectClass));
|
|
29695
29998
|
}
|
|
29696
29999
|
}
|
|
29697
30000
|
}
|
|
@@ -29700,7 +30003,12 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
29700
30003
|
(b) => b.label !== null && getLabelBehavior(b.objectClass) !== "orientation-cross-band"
|
|
29701
30004
|
)
|
|
29702
30005
|
);
|
|
29703
|
-
|
|
30006
|
+
let majorsEmitted = majorWaterLayers.length === 0;
|
|
30007
|
+
for (const { label, objectClass: cls } of featureLabelBundles) {
|
|
30008
|
+
if (!majorsEmitted && getLabelPriority(cls) < MAJOR_WATER_LABEL_PRIORITY) {
|
|
30009
|
+
labelLayers.push(...majorWaterLayers);
|
|
30010
|
+
majorsEmitted = true;
|
|
30011
|
+
}
|
|
29704
30012
|
if (bandFan === "collapsed") {
|
|
29705
30013
|
labelLayers.push(makeCollapsedCrossBandLabel(label, isDominanceExempt(label)));
|
|
29706
30014
|
continue;
|
|
@@ -29709,6 +30017,7 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
29709
30017
|
labelLayers.push(...emitBand(label, code));
|
|
29710
30018
|
}
|
|
29711
30019
|
}
|
|
30020
|
+
if (!majorsEmitted) labelLayers.push(...majorWaterLayers);
|
|
29712
30021
|
layers.push(...labelLayers);
|
|
29713
30022
|
}
|
|
29714
30023
|
const symbolSet = opts.symbolSet ?? "s52";
|