@chrtco/sdk 0.10.30 → 0.10.31

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 CHANGED
@@ -28842,455 +28842,138 @@ function worldMinusBboxPolygon(bbox) {
28842
28842
  };
28843
28843
  }
28844
28844
 
28845
- // src/atlas/land-layers.ts
28846
- var LAND_SURFACE_MINZOOM = BAND_PRIORITY_RANGES["5"].minzoom;
28847
- var LAND_SURFACE_LAYERS = (id) => id === "chrtland-base" || id === "chrtland-cover" || id === "chrtland-contour" || id === "chrtland-hillshade" || id.startsWith("chrtland-fade-");
28848
- var isLandGroundOverlay = (id) => LAND_SURFACE_LAYERS(id) && id !== "chrtland-base" || id === "chrtland-road";
28849
- var FADE_PX = 80;
28850
- var FADE_BANDS = 16;
28851
- function fadeAlpha(r) {
28852
- return Math.max(0, Math.min(1, (FADE_PX / 2 - Math.abs(r)) / FADE_PX));
28853
- }
28854
- function landBaseColor(palette) {
28855
- return (LAND_THEMES[palette] ?? LAND_THEMES.DAY).base;
28845
+ // src/atlas/base-layers.ts
28846
+ var NOCOVERAGE_PATTERN = "chrt:hatch-nocoverage";
28847
+ var UNCHARTED_PATTERN = "chrt:hatch-uncharted";
28848
+ function inlandWaterColor(palette) {
28849
+ return (BASE_THEMES[palette] ?? BASE_THEMES.DAY).inlandWater;
28856
28850
  }
28857
- var LAND_THEMES = {
28851
+ var BASE_THEMES = {
28858
28852
  DAY: {
28859
- base: "#e8d9b0",
28860
- cover: { wood: "#cfd8b4", wetland: "#c9d3c4", urban: "#dedad2", sand: "#efe4c2" },
28861
- contour: "#a89878",
28862
- contourIndex: "#8a7a5c",
28863
- road: "#d9c9a4",
28864
- roadMajor: "#d2bd8e",
28865
- label: "#5b4f3a",
28866
- halo: "#f2ead6",
28867
- shadow: "#6b5a3e",
28868
- highlight: "#fffaf0",
28869
- fade: "#e8f2f8",
28870
- // Raised from 0.45 on 2026-08-23. ⚠ Above the ~0.5 that
28871
- // hillshade-style.mjs warns "starts competing with depth areas" — a
28872
- // deliberate, sighted choice made against rendered evidence, not a
28873
- // default. If soundings or DEPARE ever read as fighting the terrain,
28874
- // this number is the first thing to walk back.
28875
- exaggeration: 0.8
28853
+ // ── `earth` is the jurisdiction tan DIMMED (~0.55), not a blue-grey ──
28854
+ // Outside the jurisdiction the map still owes the reader "what is land".
28855
+ // State 3 in the header is "the geography, under a crosshatch", and until
28856
+ // 2026-08-31 that geography was unreadable: earth #2A3439 against sea
28857
+ // #1B2328 measured 1.25:1, so Hispaniola showed charted boxes with tan
28858
+ // land inside and nothing recognisable around them (owner report: "no
28859
+ // land painting down near the DR at some zooms"). Land is warm and sea is
28860
+ // cool because that is what land and sea are; dimming the JURISDICTION
28861
+ // tan rather than inventing a colour says the true thing — this is the
28862
+ // same land, we simply do not carry charts for it. Held against BOTH
28863
+ // neighbours: legible over sea, and never mistakable for the full tan of
28864
+ // a country we do carry. DAY 3.59:1 vs sea, 3.17:1 vs tan.
28865
+ earth: "#807761",
28866
+ water: "#1B2328",
28867
+ river: "#24313A",
28868
+ boundary: "#4A575E",
28869
+ jurisdiction: "#D4C69D",
28870
+ // CHRTland's own tan — one land colour
28871
+ inlandWater: "#B9CFDD",
28872
+ // muted, unmistakably not a charted blue
28873
+ // State furniture. These three were referenced by the layers but missing
28874
+ // from this table (and its type — the long-standing TS2339 trio), so the
28875
+ // lines shipped on MapLibre's DEFAULT black. Now deliberate: a hairline
28876
+ // a shade deeper than the tan, quiet caps, tan halo.
28877
+ stateLine: "#B1A077",
28878
+ stateLabel: "#8E8468",
28879
+ stateHalo: "#D4C69D",
28880
+ // Rough-lake furniture (owner ask 2026-08-28: "the lakes need labels,
28881
+ // and shorelines drawn"): a shoreline a shade deeper than the rough
28882
+ // water, and a name in the water's own voice, haloed by the water.
28883
+ inlandShore: "#8FA9BA",
28884
+ inlandLabel: "#54707F",
28885
+ inlandHalo: "#B9CFDD"
28876
28886
  },
28877
28887
  DUSK: {
28878
- base: "#4a4433",
28879
- cover: { wood: "#414a38", wetland: "#3f4740", urban: "#4e4a42", sand: "#544c39" },
28880
- contour: "#6b6250",
28881
- contourIndex: "#837961",
28882
- road: "#565040",
28883
- roadMajor: "#63594440",
28884
- label: "#c3b898",
28885
- halo: "#2b2820",
28886
- shadow: "#2e2a20",
28887
- highlight: "#8a8068",
28888
- fade: "#16202a",
28889
- // Unchanged. DUSK was not the palette the 0.8 decision was made against.
28890
- exaggeration: 0.45
28888
+ // ── Re-derived 2026-09-01: the basemap sits UNDER the chart ──────────
28889
+ // These were the DAY tones scaled by a constant, never measured against
28890
+ // the DUSK chart — so `jurisdiction` came out at L=0.506 against a chart
28891
+ // whose own land (LANDA #8a7d5e) is L=0.209, i.e. the context layer was
28892
+ // 2.4x brighter than the data layer. Now capped at the chart's own land.
28893
+ // The rest of the block rides the same 0.666 channel factor so every
28894
+ // relationship the palette already chose is preserved, only moved down.
28895
+ // `water` is deliberately NOT scaled: it is the reference the land/sea
28896
+ // floor is measured against.
28897
+ earth: "#544E3E",
28898
+ water: "#161D21",
28899
+ river: "#141B20",
28900
+ boundary: "#293135",
28901
+ jurisdiction: "#867D64",
28902
+ inlandWater: "#62717A",
28903
+ // Label stays DARK here — the halo is still mid-toned, so 3:1 is reachable
28904
+ // downward and a bright label would be a new light source at dusk.
28905
+ stateLine: "#B5A987",
28906
+ stateLabel: "#38342A",
28907
+ stateHalo: "#867D64",
28908
+ inlandShore: "#4D5B67",
28909
+ inlandLabel: "#212629",
28910
+ inlandHalo: "#62717A"
28891
28911
  },
28892
28912
  NIGHT: {
28893
- base: "#2a2620",
28894
- cover: { wood: "#242a20", wetland: "#232823", urban: "#2c2925", sand: "#302b22" },
28895
- contour: "#413b30",
28896
- contourIndex: "#524a3b",
28897
- road: "#332f27",
28898
- roadMajor: "#3d382d",
28899
- label: "#7d745e",
28900
- halo: "#15130f",
28901
- shadow: "#1a1712",
28902
- highlight: "#4a4436",
28903
- fade: "#07090b",
28904
- // ⚠ DELIBERATELY NOT RAISED. See the type's note: a strong hillshade
28905
- // under red night vision is a grey haze over exactly the contrast a
28906
- // skipper is navigating by. Left at the shipped value rather than
28907
- // lowered, because lowering it is a separate decision from this one.
28908
- exaggeration: 0.45
28913
+ // ── Re-derived 2026-09-01 — the owner-reported night defect ──────────
28914
+ // `jurisdiction` was L=0.240 against a chart whose own land (LANDA
28915
+ // #3a3020) is L=0.031: the world basemap was 7.7x BRIGHTER than the chart
28916
+ // it sits under, so at z<11 the uncharted interior read as a pale
28917
+ // continent-sized slab on a night chart. Capped at the chart's own land.
28918
+ //
28919
+ // ⚠ `earth` AND `jurisdiction` CONVERGE HERE, and that is forced, not
28920
+ // sloppy. Holding earth 2:1 above the ocean AND 2:1 below the jurisdiction
28921
+ // tan requires jurisdiction >= L 0.174 — 5.6x the chart's own land. At
28922
+ // night the range between the ocean (L 0.006) and the cap (L 0.031) is too
28923
+ // compressed to carry two land tones. COVERAGE IS STILL SAID, by the
28924
+ // `chrt:hatch-nocoverage` crosshatch, which is a stronger signal than tone
28925
+ // and is the one thing that cannot be lost to palette compression.
28926
+ //
28927
+ // Labels invert to light-on-dark: 3:1 downward is unreachable off a halo
28928
+ // this close to black, and CHRTland's own night town labels already sit at
28929
+ // L=0.177 for exactly this reason.
28930
+ earth: "#333026",
28931
+ water: "#10161A",
28932
+ river: "#090D0F",
28933
+ boundary: "#131719",
28934
+ jurisdiction: "#353128",
28935
+ inlandWater: "#232A30",
28936
+ stateLine: "#484236",
28937
+ stateLabel: "#837963",
28938
+ stateHalo: "#353128",
28939
+ inlandShore: "#1C2227",
28940
+ inlandLabel: "#617484",
28941
+ inlandHalo: "#232A30"
28909
28942
  }
28910
28943
  };
28911
- var FITS = ["<=", ["coalesce", ["get", "min_z"], 0], ["zoom"]];
28912
- var ANCHOR = ["coalesce", ["get", "anchor"], "top"];
28913
- var anchorOffset = (em) => [
28914
- "match",
28915
- ["get", "anchor"],
28916
- "left",
28917
- ["literal", [0.6, 0]],
28918
- "right",
28919
- ["literal", [-0.6, 0]],
28920
- "bottom",
28921
- ["literal", [0, -em]],
28922
- ["literal", [0, em]]
28923
- ];
28924
- var FONT = ["Open Sans Regular"];
28925
- var FONT_ITALIC = ["Open Sans Italic"];
28926
- function landLayers(opts = {}) {
28944
+ function baseLayers(opts = {}) {
28927
28945
  const palette = opts.palette ?? "DAY";
28928
- const T = LAND_THEMES[palette] ?? LAND_THEMES.DAY;
28929
- const src = opts.sourceId ?? LAND_REGION;
28930
- const demSrc = opts.reliefSourceId ?? RELIEF_REGION;
28946
+ const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
28947
+ const src = opts.sourceId ?? BASE_REGION;
28931
28948
  const L = (o) => o;
28932
- const layers = [
28933
- // 1. The land itself — LNDARE ∩ coverage, so it can never outrun the chart.
28949
+ return [
28950
+ // 1. Geography — the world as it is, in the quiet dark voice. The OCEAN
28951
+ // is a background, not a polygon: the archive ships land and lakes,
28952
+ // and "everything else is sea" is exactly what a background expresses
28953
+ // — geometry-free, gap-free at every antimeridian and pole. Without
28954
+ // it the sea outside coverage was bare S-52 NODTA, a light grey that
28955
+ // read as "charted ground" with a hatch on it.
28934
28956
  L({
28935
- id: "chrtland-base",
28957
+ id: "chrtbase-ocean",
28958
+ type: "background",
28959
+ paint: { "background-color": T.water }
28960
+ }),
28961
+ L({
28962
+ id: "chrtbase-earth",
28936
28963
  type: "fill",
28937
28964
  source: src,
28938
- "source-layer": "land_base",
28939
- paint: { "fill-color": T.base }
28940
- })
28941
- ];
28942
- layers.push(L({
28943
- id: "chrtland-cover",
28944
- type: "fill",
28945
- source: src,
28946
- "source-layer": "land_cover",
28947
- filter: ["<=", ["coalesce", ["get", "min_z"], 0], ["zoom"]],
28948
- paint: {
28949
- "fill-color": [
28950
- "match",
28951
- ["get", "class"],
28952
- "wood",
28953
- T.cover.wood,
28954
- "wetland",
28955
- T.cover.wetland,
28956
- "urban",
28957
- T.cover.urban,
28958
- "sand",
28959
- T.cover.sand,
28960
- T.base
28961
- ],
28962
- "fill-opacity": 0.85
28963
- }
28964
- }));
28965
- if (opts.relief) {
28966
- layers.push(L({
28967
- // No minzoom here: LAND_SURFACE_LAYERS floors it with the rest of the
28968
- // surface, so the two can never drift apart again.
28969
- id: "chrtland-hillshade",
28970
- type: "hillshade",
28971
- source: demSrc,
28972
- paint: {
28973
- "hillshade-exaggeration": T.exaggeration,
28974
- "hillshade-shadow-color": T.shadow,
28975
- "hillshade-highlight-color": T.highlight,
28976
- "hillshade-accent-color": T.shadow
28977
- }
28978
- }));
28979
- }
28980
- const isIndexContour = [
28981
- "any",
28982
- ["<=", ["coalesce", ["get", "index_z"], 99], ["zoom"]],
28983
- ["==", ["get", "is_index"], 1]
28984
- ];
28985
- layers.push(L({
28986
- id: "chrtland-contour",
28987
- type: "line",
28988
- source: src,
28989
- "source-layer": "land_contour",
28990
- filter: ["<=", ["coalesce", ["get", "min_z"], 0], ["zoom"]],
28991
- paint: {
28992
- "line-color": ["case", isIndexContour, T.contourIndex, T.contour],
28993
- "line-width": ["case", isIndexContour, 0.8, 0.4],
28994
- "line-opacity": 0.7
28995
- }
28996
- }));
28997
- const d = FADE_PX / 2 / FADE_BANDS;
28998
- for (let k = 0; k < FADE_BANDS; k++) {
28999
- const width = k === 0 ? { "line-width": 2 * d } : { "line-width": d, "line-gap-width": 2 * k * d };
29000
- layers.push(L({
29001
- id: `chrtland-fade-${k}`,
29002
- type: "line",
29003
- source: src,
29004
- "source-layer": "land_edge",
29005
- layout: { "line-join": "bevel" },
29006
- paint: { "line-color": T.fade, "line-blur": 0, "line-opacity": fadeAlpha(k * d + d / 2), ...width }
29007
- }));
29008
- }
29009
- layers.push(L({
29010
- id: "chrtland-road",
29011
- type: "line",
29012
- source: src,
29013
- "source-layer": "land_road",
29014
- minzoom: 10,
29015
- paint: {
29016
- "line-color": ["match", ["get", "kind"], "highway", T.roadMajor, T.road],
29017
- "line-width": [
29018
- "interpolate",
29019
- ["linear"],
29020
- ["zoom"],
29021
- 10,
29022
- ["match", ["get", "kind"], "highway", 0.8, 0.4],
29023
- 14,
29024
- ["match", ["get", "kind"], "highway", 2.4, 1.2]
29025
- ],
29026
- "line-opacity": 0.8
29027
- }
29028
- }));
29029
- layers.push(L({
29030
- id: "chrtland-city-major",
29031
- type: "symbol",
29032
- source: src,
29033
- "source-layer": "land_town",
29034
- minzoom: 6.5,
29035
- filter: [">=", ["coalesce", ["get", "prank"], 0], 10],
29036
- layout: {
29037
- "text-field": [
29038
- "case",
29039
- ["==", ["get", "upcase"], true],
29040
- ["upcase", ["get", "name"]],
29041
- ["get", "name"]
29042
- ],
29043
- "text-font": FONT,
29044
- "text-size": [
29045
- "interpolate",
29046
- ["linear"],
29047
- ["zoom"],
29048
- 6.5,
29049
- 11.5,
29050
- 9,
29051
- ["+", 10, ["*", 0.4, ["coalesce", ["get", "prank"], 10]]],
29052
- 13,
29053
- ["+", 13, ["*", 0.5, ["coalesce", ["get", "prank"], 10]]]
29054
- ],
29055
- "text-letter-spacing": ["case", ["==", ["get", "upcase"], true], 0.14, 0.02],
29056
- "text-offset": anchorOffset(0.75),
29057
- "text-anchor": ANCHOR,
29058
- "text-padding": 10,
29059
- "text-allow-overlap": false,
29060
- "symbol-sort-key": ["*", -1, ["coalesce", ["get", "pop"], 0]]
29061
- },
29062
- paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.6 }
29063
- }));
29064
- layers.push(L({
29065
- id: "chrtland-town-port",
29066
- type: "symbol",
29067
- source: src,
29068
- "source-layer": "land_town",
29069
- minzoom: 8.5,
29070
- filter: [
29071
- "all",
29072
- ["==", ["get", "enc_dup"], true],
29073
- ["<", ["coalesce", ["get", "prank"], 0], 10]
29074
- ],
29075
- layout: {
29076
- "text-field": ["get", "name"],
29077
- "text-font": FONT,
29078
- "text-size": ["interpolate", ["linear"], ["zoom"], 8.5, 11, 12, 13.5],
29079
- "text-offset": anchorOffset(0.75),
29080
- "text-anchor": ANCHOR,
29081
- "text-padding": 8,
29082
- "text-allow-overlap": false,
29083
- // Within the tier, the bigger place wins the space: without this the
29084
- // tie broke on data order and "West Gloucester" beat Gloucester.
29085
- "symbol-sort-key": ["*", -1, ["coalesce", ["get", "pop"], 0]]
29086
- },
29087
- paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.4 }
29088
- }));
29089
- layers.push(L({
29090
- id: "chrtland-town",
29091
- type: "symbol",
29092
- source: src,
29093
- "source-layer": "land_town",
29094
- minzoom: 9,
29095
- filter: [
29096
- "all",
29097
- FITS,
29098
- ["<", ["coalesce", ["get", "prank"], 0], 10],
29099
- ["!=", ["get", "enc_dup"], true]
29100
- ],
29101
- layout: {
29102
- "text-field": [
29103
- "case",
29104
- ["==", ["get", "upcase"], true],
29105
- ["upcase", ["get", "name"]],
29106
- ["get", "name"]
29107
- ],
29108
- "text-font": FONT,
29109
- "text-size": ["interpolate", ["linear"], ["zoom"], 9, 10, 14, 13.5],
29110
- "text-letter-spacing": ["case", ["==", ["get", "upcase"], true], 0.14, 0],
29111
- "text-offset": anchorOffset(0.75),
29112
- "text-anchor": ANCHOR,
29113
- "text-padding": 6,
29114
- "text-allow-overlap": false,
29115
- "symbol-sort-key": 2
29116
- },
29117
- paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
29118
- }));
29119
- layers.push(L({
29120
- id: "chrtland-summit",
29121
- type: "symbol",
29122
- source: src,
29123
- "source-layer": "land_summit",
29124
- minzoom: 11.5,
29125
- filter: ["all", FITS, [">=", ["get", "peak_m"], 40]],
29126
- layout: {
29127
- "text-field": ["concat", ["get", "name"], "\n", ["to-string", ["get", "peak_m"]], " m"],
29128
- "text-font": FONT,
29129
- "text-size": ["interpolate", ["linear"], ["zoom"], 11.5, 8.5, 14, 11],
29130
- "text-offset": anchorOffset(0.8),
29131
- "text-anchor": ANCHOR,
29132
- "text-padding": 5,
29133
- "text-allow-overlap": false,
29134
- "symbol-sort-key": 3
29135
- },
29136
- paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
29137
- }));
29138
- layers.push(L({
29139
- id: "chrtland-poi",
29140
- type: "symbol",
29141
- source: src,
29142
- "source-layer": "land_poi",
29143
- minzoom: 13,
29144
- filter: FITS,
29145
- layout: {
29146
- "text-field": ["get", "name"],
29147
- "text-font": FONT_ITALIC,
29148
- "text-size": ["interpolate", ["linear"], ["zoom"], 13, 8, 14, 9.5],
29149
- "text-offset": anchorOffset(0.7),
29150
- "text-anchor": ANCHOR,
29151
- "text-padding": 5,
29152
- "text-allow-overlap": false,
29153
- "symbol-sort-key": 5
29154
- },
29155
- paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
29156
- }));
29157
- for (const layer of layers) {
29158
- const l = layer;
29159
- if (!LAND_SURFACE_LAYERS(l.id)) continue;
29160
- l.minzoom = Math.max(l.minzoom ?? 0, LAND_SURFACE_MINZOOM);
29161
- }
29162
- return layers;
29163
- }
29164
-
29165
- // src/atlas/base-layers.ts
29166
- var NOCOVERAGE_PATTERN = "chrt:hatch-nocoverage";
29167
- var UNCHARTED_PATTERN = "chrt:hatch-uncharted";
29168
- var BASE_THEMES = {
29169
- DAY: {
29170
- // ── `earth` is the jurisdiction tan DIMMED (~0.55), not a blue-grey ──
29171
- // Outside the jurisdiction the map still owes the reader "what is land".
29172
- // State 3 in the header is "the geography, under a crosshatch", and until
29173
- // 2026-08-31 that geography was unreadable: earth #2A3439 against sea
29174
- // #1B2328 measured 1.25:1, so Hispaniola showed charted boxes with tan
29175
- // land inside and nothing recognisable around them (owner report: "no
29176
- // land painting down near the DR at some zooms"). Land is warm and sea is
29177
- // cool because that is what land and sea are; dimming the JURISDICTION
29178
- // tan rather than inventing a colour says the true thing — this is the
29179
- // same land, we simply do not carry charts for it. Held against BOTH
29180
- // neighbours: legible over sea, and never mistakable for the full tan of
29181
- // a country we do carry. DAY 3.59:1 vs sea, 3.17:1 vs tan.
29182
- earth: "#807761",
29183
- water: "#1B2328",
29184
- river: "#24313A",
29185
- boundary: "#4A575E",
29186
- jurisdiction: "#E8D9B0",
29187
- // CHRTland's own tan — one land colour
29188
- inlandWater: "#B9CFDD",
29189
- // muted, unmistakably not a charted blue
29190
- // State furniture. These three were referenced by the layers but missing
29191
- // from this table (and its type — the long-standing TS2339 trio), so the
29192
- // lines shipped on MapLibre's DEFAULT black. Now deliberate: a hairline
29193
- // a shade deeper than the tan, quiet caps, tan halo.
29194
- stateLine: "#C4B389",
29195
- stateLabel: "#8E8468",
29196
- stateHalo: "#E8D9B0",
29197
- // Rough-lake furniture (owner ask 2026-08-28: "the lakes need labels,
29198
- // and shorelines drawn"): a shoreline a shade deeper than the rough
29199
- // water, and a name in the water's own voice, haloed by the water.
29200
- inlandShore: "#8FA9BA",
29201
- inlandLabel: "#54707F",
29202
- inlandHalo: "#B9CFDD"
29203
- },
29204
- DUSK: {
29205
- // ── Re-derived 2026-09-01: the basemap sits UNDER the chart ──────────
29206
- // These were the DAY tones scaled by a constant, never measured against
29207
- // the DUSK chart — so `jurisdiction` came out at L=0.506 against a chart
29208
- // whose own land (LANDA #8a7d5e) is L=0.209, i.e. the context layer was
29209
- // 2.4x brighter than the data layer. Now capped at the chart's own land.
29210
- // The rest of the block rides the same 0.666 channel factor so every
29211
- // relationship the palette already chose is preserved, only moved down.
29212
- // `water` is deliberately NOT scaled: it is the reference the land/sea
29213
- // floor is measured against.
29214
- earth: "#544E3E",
29215
- water: "#161D21",
29216
- river: "#141B20",
29217
- boundary: "#293135",
29218
- jurisdiction: "#867D64",
29219
- inlandWater: "#62717A",
29220
- // Label stays DARK here — the halo is still mid-toned, so 3:1 is reachable
29221
- // downward and a bright label would be a new light source at dusk.
29222
- stateLine: "#B5A987",
29223
- stateLabel: "#38342A",
29224
- stateHalo: "#867D64",
29225
- inlandShore: "#4D5B67",
29226
- inlandLabel: "#212629",
29227
- inlandHalo: "#62717A"
29228
- },
29229
- NIGHT: {
29230
- // ── Re-derived 2026-09-01 — the owner-reported night defect ──────────
29231
- // `jurisdiction` was L=0.240 against a chart whose own land (LANDA
29232
- // #3a3020) is L=0.031: the world basemap was 7.7x BRIGHTER than the chart
29233
- // it sits under, so at z<11 the uncharted interior read as a pale
29234
- // continent-sized slab on a night chart. Capped at the chart's own land.
29235
- //
29236
- // ⚠ `earth` AND `jurisdiction` CONVERGE HERE, and that is forced, not
29237
- // sloppy. Holding earth 2:1 above the ocean AND 2:1 below the jurisdiction
29238
- // tan requires jurisdiction >= L 0.174 — 5.6x the chart's own land. At
29239
- // night the range between the ocean (L 0.006) and the cap (L 0.031) is too
29240
- // compressed to carry two land tones. COVERAGE IS STILL SAID, by the
29241
- // `chrt:hatch-nocoverage` crosshatch, which is a stronger signal than tone
29242
- // and is the one thing that cannot be lost to palette compression.
29243
- //
29244
- // Labels invert to light-on-dark: 3:1 downward is unreachable off a halo
29245
- // this close to black, and CHRTland's own night town labels already sit at
29246
- // L=0.177 for exactly this reason.
29247
- earth: "#333026",
29248
- water: "#10161A",
29249
- river: "#090D0F",
29250
- boundary: "#131719",
29251
- jurisdiction: "#353128",
29252
- inlandWater: "#232A30",
29253
- stateLine: "#484236",
29254
- stateLabel: "#837963",
29255
- stateHalo: "#353128",
29256
- inlandShore: "#1C2227",
29257
- inlandLabel: "#617484",
29258
- inlandHalo: "#232A30"
29259
- }
29260
- };
29261
- function baseLayers(opts = {}) {
29262
- const palette = opts.palette ?? "DAY";
29263
- const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
29264
- const src = opts.sourceId ?? BASE_REGION;
29265
- const L = (o) => o;
29266
- return [
29267
- // 1. Geography — the world as it is, in the quiet dark voice. The OCEAN
29268
- // is a background, not a polygon: the archive ships land and lakes,
29269
- // and "everything else is sea" is exactly what a background expresses
29270
- // — geometry-free, gap-free at every antimeridian and pole. Without
29271
- // it the sea outside coverage was bare S-52 NODTA, a light grey that
29272
- // read as "charted ground" with a hatch on it.
29273
- L({
29274
- id: "chrtbase-ocean",
29275
- type: "background",
29276
- paint: { "background-color": T.water }
29277
- }),
29278
- L({
29279
- id: "chrtbase-earth",
29280
- type: "fill",
29281
- source: src,
29282
- "source-layer": "base_earth",
29283
- paint: { "fill-color": T.earth }
29284
- }),
29285
- L({
29286
- id: "chrtbase-water",
29287
- type: "fill",
29288
- source: src,
29289
- "source-layer": "base_water",
29290
- paint: { "fill-color": T.water }
29291
- }),
29292
- L({
29293
- id: "chrtbase-river",
28965
+ "source-layer": "base_earth",
28966
+ paint: { "fill-color": T.earth }
28967
+ }),
28968
+ L({
28969
+ id: "chrtbase-water",
28970
+ type: "fill",
28971
+ source: src,
28972
+ "source-layer": "base_water",
28973
+ paint: { "fill-color": T.water }
28974
+ }),
28975
+ L({
28976
+ id: "chrtbase-river",
29294
28977
  type: "line",
29295
28978
  source: src,
29296
28979
  "source-layer": "base_river",
@@ -29568,32 +29251,360 @@ function baseInlandWaterLayers(opts = {}) {
29568
29251
  }
29569
29252
  }),
29570
29253
  L({
29571
- id: "chrtbase-inland-label",
29572
- type: "symbol",
29254
+ id: "chrtbase-inland-label",
29255
+ type: "symbol",
29256
+ source: src,
29257
+ "source-layer": "inland_water_label",
29258
+ minzoom: 5,
29259
+ layout: {
29260
+ "text-field": ["get", "name"],
29261
+ "text-font": ["Open Sans Regular"],
29262
+ "text-size": ["interpolate", ["linear"], ["zoom"], 5, 9.5, 10, 12],
29263
+ "text-max-width": 7,
29264
+ "text-allow-overlap": false,
29265
+ // Zoom-scaled spacing for the anchor GRID: at z8.9 the 0.08-deg
29266
+ // anchors sit ~54 px apart and a flat padding let six copies of
29267
+ // "Lake Ontario" crowd one screen (owner-reported 2026-08-30).
29268
+ // Wide collision boxes at low zoom thin the grid to the repeat-
29269
+ // by-convention cadence; by z12 the anchors are ~850 px apart and
29270
+ // the padding relaxes to normal.
29271
+ "text-padding": ["interpolate", ["linear"], ["zoom"], 5, 80, 9, 64, 12, 16]
29272
+ },
29273
+ paint: {
29274
+ "text-color": T.inlandLabel,
29275
+ "text-halo-color": T.inlandHalo,
29276
+ "text-halo-width": 1.1
29277
+ }
29278
+ })
29279
+ ];
29280
+ }
29281
+
29282
+ // src/atlas/land-layers.ts
29283
+ var LAND_SURFACE_MINZOOM = BAND_PRIORITY_RANGES["5"].minzoom;
29284
+ var LAND_SURFACE_LAYERS = (id) => id === "chrtland-base" || id === "chrtland-cover" || id === "chrtland-contour" || id === "chrtland-hillshade" || id === "chrtland-water" || id.startsWith("chrtland-fade-");
29285
+ var isLandGroundOverlay = (id) => LAND_SURFACE_LAYERS(id) && id !== "chrtland-base" || id === "chrtland-road";
29286
+ var FADE_PX = 80;
29287
+ var FADE_BANDS = 16;
29288
+ function fadeAlpha(r) {
29289
+ return Math.max(0, Math.min(1, (FADE_PX / 2 - Math.abs(r)) / FADE_PX));
29290
+ }
29291
+ function landBaseColor(palette) {
29292
+ return (LAND_THEMES[palette] ?? LAND_THEMES.DAY).base;
29293
+ }
29294
+ var LAND_THEMES = {
29295
+ DAY: {
29296
+ base: "#d4c69d",
29297
+ cover: { wood: "#bcc5a1", wetland: "#b6c0b1", urban: "#cac6bf", sand: "#dbd0af" },
29298
+ contour: "#958666",
29299
+ contourIndex: "#78694b",
29300
+ road: "#c5b691",
29301
+ roadMajor: "#beaa7c",
29302
+ label: "#5b4f3a",
29303
+ halo: "#ded6c3",
29304
+ shadow: "#6b5a3e",
29305
+ highlight: "#fffaf0",
29306
+ fade: "#e8f2f8",
29307
+ // Raised from 0.45 on 2026-08-23. ⚠ Above the ~0.5 that
29308
+ // hillshade-style.mjs warns "starts competing with depth areas" — a
29309
+ // deliberate, sighted choice made against rendered evidence, not a
29310
+ // default. If soundings or DEPARE ever read as fighting the terrain,
29311
+ // this number is the first thing to walk back.
29312
+ exaggeration: 0.8
29313
+ },
29314
+ DUSK: {
29315
+ base: "#4a4433",
29316
+ cover: { wood: "#414a38", wetland: "#3f4740", urban: "#4e4a42", sand: "#544c39" },
29317
+ contour: "#6b6250",
29318
+ contourIndex: "#837961",
29319
+ road: "#565040",
29320
+ roadMajor: "#63594440",
29321
+ label: "#c3b898",
29322
+ halo: "#2b2820",
29323
+ shadow: "#2e2a20",
29324
+ highlight: "#8a8068",
29325
+ fade: "#16202a",
29326
+ // Unchanged. DUSK was not the palette the 0.8 decision was made against.
29327
+ exaggeration: 0.45
29328
+ },
29329
+ NIGHT: {
29330
+ base: "#2a2620",
29331
+ cover: { wood: "#242a20", wetland: "#232823", urban: "#2c2925", sand: "#302b22" },
29332
+ contour: "#413b30",
29333
+ contourIndex: "#524a3b",
29334
+ road: "#332f27",
29335
+ roadMajor: "#3d382d",
29336
+ label: "#7d745e",
29337
+ halo: "#15130f",
29338
+ shadow: "#1a1712",
29339
+ highlight: "#4a4436",
29340
+ fade: "#07090b",
29341
+ // ⚠ DELIBERATELY NOT RAISED. See the type's note: a strong hillshade
29342
+ // under red night vision is a grey haze over exactly the contrast a
29343
+ // skipper is navigating by. Left at the shipped value rather than
29344
+ // lowered, because lowering it is a separate decision from this one.
29345
+ exaggeration: 0.45
29346
+ }
29347
+ };
29348
+ var FITS = ["<=", ["coalesce", ["get", "min_z"], 0], ["zoom"]];
29349
+ var ANCHOR = ["coalesce", ["get", "anchor"], "top"];
29350
+ var anchorOffset = (em) => [
29351
+ "match",
29352
+ ["get", "anchor"],
29353
+ "left",
29354
+ ["literal", [0.6, 0]],
29355
+ "right",
29356
+ ["literal", [-0.6, 0]],
29357
+ "bottom",
29358
+ ["literal", [0, -em]],
29359
+ ["literal", [0, em]]
29360
+ ];
29361
+ var FONT = ["Open Sans Regular"];
29362
+ var FONT_ITALIC = ["Open Sans Italic"];
29363
+ function landLayers(opts = {}) {
29364
+ const palette = opts.palette ?? "DAY";
29365
+ const T = LAND_THEMES[palette] ?? LAND_THEMES.DAY;
29366
+ const src = opts.sourceId ?? LAND_REGION;
29367
+ const demSrc = opts.reliefSourceId ?? RELIEF_REGION;
29368
+ const L = (o) => o;
29369
+ const layers = [
29370
+ // 1. The land itself — LNDARE ∩ coverage, so it can never outrun the chart.
29371
+ L({
29372
+ id: "chrtland-base",
29373
+ type: "fill",
29573
29374
  source: src,
29574
- "source-layer": "inland_water_label",
29575
- minzoom: 5,
29576
- layout: {
29577
- "text-field": ["get", "name"],
29578
- "text-font": ["Open Sans Regular"],
29579
- "text-size": ["interpolate", ["linear"], ["zoom"], 5, 9.5, 10, 12],
29580
- "text-max-width": 7,
29581
- "text-allow-overlap": false,
29582
- // Zoom-scaled spacing for the anchor GRID: at z8.9 the 0.08-deg
29583
- // anchors sit ~54 px apart and a flat padding let six copies of
29584
- // "Lake Ontario" crowd one screen (owner-reported 2026-08-30).
29585
- // Wide collision boxes at low zoom thin the grid to the repeat-
29586
- // by-convention cadence; by z12 the anchors are ~850 px apart and
29587
- // the padding relaxes to normal.
29588
- "text-padding": ["interpolate", ["linear"], ["zoom"], 5, 80, 9, 64, 12, 16]
29589
- },
29375
+ "source-layer": "land_base",
29376
+ paint: { "fill-color": T.base }
29377
+ })
29378
+ ];
29379
+ layers.push(L({
29380
+ id: "chrtland-cover",
29381
+ type: "fill",
29382
+ source: src,
29383
+ "source-layer": "land_cover",
29384
+ filter: ["<=", ["coalesce", ["get", "min_z"], 0], ["zoom"]],
29385
+ paint: {
29386
+ "fill-color": [
29387
+ "match",
29388
+ ["get", "class"],
29389
+ "wood",
29390
+ T.cover.wood,
29391
+ "wetland",
29392
+ T.cover.wetland,
29393
+ "urban",
29394
+ T.cover.urban,
29395
+ "sand",
29396
+ T.cover.sand,
29397
+ T.base
29398
+ ],
29399
+ "fill-opacity": 0.85
29400
+ }
29401
+ }));
29402
+ if (opts.relief) {
29403
+ layers.push(L({
29404
+ // No minzoom here: LAND_SURFACE_LAYERS floors it with the rest of the
29405
+ // surface, so the two can never drift apart again.
29406
+ id: "chrtland-hillshade",
29407
+ type: "hillshade",
29408
+ source: demSrc,
29590
29409
  paint: {
29591
- "text-color": T.inlandLabel,
29592
- "text-halo-color": T.inlandHalo,
29593
- "text-halo-width": 1.1
29410
+ "hillshade-exaggeration": T.exaggeration,
29411
+ "hillshade-shadow-color": T.shadow,
29412
+ "hillshade-highlight-color": T.highlight,
29413
+ "hillshade-accent-color": T.shadow
29594
29414
  }
29595
- })
29415
+ }));
29416
+ }
29417
+ const isIndexContour = [
29418
+ "any",
29419
+ ["<=", ["coalesce", ["get", "index_z"], 99], ["zoom"]],
29420
+ ["==", ["get", "is_index"], 1]
29596
29421
  ];
29422
+ layers.push(L({
29423
+ id: "chrtland-contour",
29424
+ type: "line",
29425
+ source: src,
29426
+ "source-layer": "land_contour",
29427
+ filter: ["<=", ["coalesce", ["get", "min_z"], 0], ["zoom"]],
29428
+ paint: {
29429
+ "line-color": ["case", isIndexContour, T.contourIndex, T.contour],
29430
+ "line-width": ["case", isIndexContour, 0.8, 0.4],
29431
+ "line-opacity": 0.7
29432
+ }
29433
+ }));
29434
+ layers.push(L({
29435
+ id: "chrtland-water",
29436
+ type: "fill",
29437
+ source: src,
29438
+ "source-layer": "land_water",
29439
+ filter: ["<=", ["coalesce", ["get", "min_z"], 0], ["zoom"]],
29440
+ paint: { "fill-color": inlandWaterColor(palette) }
29441
+ }));
29442
+ const d = FADE_PX / 2 / FADE_BANDS;
29443
+ for (let k = 0; k < FADE_BANDS; k++) {
29444
+ const width = k === 0 ? { "line-width": 2 * d } : { "line-width": d, "line-gap-width": 2 * k * d };
29445
+ layers.push(L({
29446
+ id: `chrtland-fade-${k}`,
29447
+ type: "line",
29448
+ source: src,
29449
+ "source-layer": "land_edge",
29450
+ layout: { "line-join": "bevel" },
29451
+ paint: { "line-color": T.fade, "line-blur": 0, "line-opacity": fadeAlpha(k * d + d / 2), ...width }
29452
+ }));
29453
+ }
29454
+ layers.push(L({
29455
+ id: "chrtland-road",
29456
+ type: "line",
29457
+ source: src,
29458
+ "source-layer": "land_road",
29459
+ minzoom: 10,
29460
+ paint: {
29461
+ "line-color": ["match", ["get", "kind"], "highway", T.roadMajor, T.road],
29462
+ "line-width": [
29463
+ "interpolate",
29464
+ ["linear"],
29465
+ ["zoom"],
29466
+ 10,
29467
+ ["match", ["get", "kind"], "highway", 0.8, 0.4],
29468
+ 14,
29469
+ ["match", ["get", "kind"], "highway", 2.4, 1.2]
29470
+ ],
29471
+ "line-opacity": 0.8
29472
+ }
29473
+ }));
29474
+ layers.push(L({
29475
+ id: "chrtland-city-major",
29476
+ type: "symbol",
29477
+ source: src,
29478
+ "source-layer": "land_town",
29479
+ minzoom: 6.5,
29480
+ filter: [">=", ["coalesce", ["get", "prank"], 0], 10],
29481
+ layout: {
29482
+ "text-field": [
29483
+ "case",
29484
+ ["==", ["get", "upcase"], true],
29485
+ ["upcase", ["get", "name"]],
29486
+ ["get", "name"]
29487
+ ],
29488
+ "text-font": FONT,
29489
+ "text-size": [
29490
+ "interpolate",
29491
+ ["linear"],
29492
+ ["zoom"],
29493
+ 6.5,
29494
+ 11.5,
29495
+ 9,
29496
+ ["+", 10, ["*", 0.4, ["coalesce", ["get", "prank"], 10]]],
29497
+ 13,
29498
+ ["+", 13, ["*", 0.5, ["coalesce", ["get", "prank"], 10]]]
29499
+ ],
29500
+ "text-letter-spacing": ["case", ["==", ["get", "upcase"], true], 0.14, 0.02],
29501
+ "text-offset": anchorOffset(0.75),
29502
+ "text-anchor": ANCHOR,
29503
+ "text-padding": 10,
29504
+ "text-allow-overlap": false,
29505
+ "symbol-sort-key": ["*", -1, ["coalesce", ["get", "pop"], 0]]
29506
+ },
29507
+ paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.6 }
29508
+ }));
29509
+ layers.push(L({
29510
+ id: "chrtland-town-port",
29511
+ type: "symbol",
29512
+ source: src,
29513
+ "source-layer": "land_town",
29514
+ minzoom: 8.5,
29515
+ filter: [
29516
+ "all",
29517
+ ["==", ["get", "enc_dup"], true],
29518
+ ["<", ["coalesce", ["get", "prank"], 0], 10]
29519
+ ],
29520
+ layout: {
29521
+ "text-field": ["get", "name"],
29522
+ "text-font": FONT,
29523
+ "text-size": ["interpolate", ["linear"], ["zoom"], 8.5, 11, 12, 13.5],
29524
+ "text-offset": anchorOffset(0.75),
29525
+ "text-anchor": ANCHOR,
29526
+ "text-padding": 8,
29527
+ "text-allow-overlap": false,
29528
+ // Within the tier, the bigger place wins the space: without this the
29529
+ // tie broke on data order and "West Gloucester" beat Gloucester.
29530
+ "symbol-sort-key": ["*", -1, ["coalesce", ["get", "pop"], 0]]
29531
+ },
29532
+ paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.4 }
29533
+ }));
29534
+ layers.push(L({
29535
+ id: "chrtland-town",
29536
+ type: "symbol",
29537
+ source: src,
29538
+ "source-layer": "land_town",
29539
+ minzoom: 9,
29540
+ filter: [
29541
+ "all",
29542
+ FITS,
29543
+ ["<", ["coalesce", ["get", "prank"], 0], 10],
29544
+ ["!=", ["get", "enc_dup"], true]
29545
+ ],
29546
+ layout: {
29547
+ "text-field": [
29548
+ "case",
29549
+ ["==", ["get", "upcase"], true],
29550
+ ["upcase", ["get", "name"]],
29551
+ ["get", "name"]
29552
+ ],
29553
+ "text-font": FONT,
29554
+ "text-size": ["interpolate", ["linear"], ["zoom"], 9, 10, 14, 13.5],
29555
+ "text-letter-spacing": ["case", ["==", ["get", "upcase"], true], 0.14, 0],
29556
+ "text-offset": anchorOffset(0.75),
29557
+ "text-anchor": ANCHOR,
29558
+ "text-padding": 6,
29559
+ "text-allow-overlap": false,
29560
+ "symbol-sort-key": 2
29561
+ },
29562
+ paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
29563
+ }));
29564
+ layers.push(L({
29565
+ id: "chrtland-summit",
29566
+ type: "symbol",
29567
+ source: src,
29568
+ "source-layer": "land_summit",
29569
+ minzoom: 11.5,
29570
+ filter: ["all", FITS, [">=", ["get", "peak_m"], 40]],
29571
+ layout: {
29572
+ "text-field": ["concat", ["get", "name"], "\n", ["to-string", ["get", "peak_m"]], " m"],
29573
+ "text-font": FONT,
29574
+ "text-size": ["interpolate", ["linear"], ["zoom"], 11.5, 8.5, 14, 11],
29575
+ "text-offset": anchorOffset(0.8),
29576
+ "text-anchor": ANCHOR,
29577
+ "text-padding": 5,
29578
+ "text-allow-overlap": false,
29579
+ "symbol-sort-key": 3
29580
+ },
29581
+ paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
29582
+ }));
29583
+ layers.push(L({
29584
+ id: "chrtland-poi",
29585
+ type: "symbol",
29586
+ source: src,
29587
+ "source-layer": "land_poi",
29588
+ minzoom: 13,
29589
+ filter: FITS,
29590
+ layout: {
29591
+ "text-field": ["get", "name"],
29592
+ "text-font": FONT_ITALIC,
29593
+ "text-size": ["interpolate", ["linear"], ["zoom"], 13, 8, 14, 9.5],
29594
+ "text-offset": anchorOffset(0.7),
29595
+ "text-anchor": ANCHOR,
29596
+ "text-padding": 5,
29597
+ "text-allow-overlap": false,
29598
+ "symbol-sort-key": 5
29599
+ },
29600
+ paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
29601
+ }));
29602
+ for (const layer of layers) {
29603
+ const l = layer;
29604
+ if (!LAND_SURFACE_LAYERS(l.id)) continue;
29605
+ l.minzoom = Math.max(l.minzoom ?? 0, LAND_SURFACE_MINZOOM);
29606
+ }
29607
+ return layers;
29597
29608
  }
29598
29609
 
29599
29610
  // src/atlas/band-collapse.ts