@chrtco/sdk 0.10.19 → 0.10.20

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
@@ -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 = 8;
592
- var RELIEF_MAXZOOM = 11;
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 orientationLabelLayout(extras = {}) {
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": ORIENTATION_BAND_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, t) => v === 0 && nv[t] > 0 ? 0 : Math.round((v + (nv[t] - v) * f) * 10) / 10
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, 9]],
23358
- [13, [11, 11]],
23359
- [16, [10, 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
- textHaloWidth: ORIENTATION_HALO_WIDTH,
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, [12, 13, 14, 0, 9]],
23537
- [11, [10, 12, 13, 0, 11]],
23538
- [13, [9, 11, 12, 10, 11]],
23539
- [16, [9, 10, 11, 11, 10]]
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
- "text-padding": 2
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, 9]],
23642
- [13, [10, 10]],
23643
- [16, [9, 10]]
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
- textHaloWidth: ORIENTATION_HALO_WIDTH,
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 RELIEF_MINZOOM2 = 10;
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,
@@ -29103,12 +29215,38 @@ function baseInlandWaterLayers(opts = {}) {
29103
29215
  const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
29104
29216
  const src = opts.sourceId ?? BASE_REGION;
29105
29217
  const L = (o) => o;
29218
+ const NE_TIER = ["!", ["has", "kind"]];
29219
+ const PLANET_TIER = ["has", "kind"];
29220
+ const HANDOVER = 9;
29106
29221
  return [
29222
+ // ── The fill pair, SPLIT BY TIER at the handover ────────────────────────
29223
+ //
29224
+ // Both tiers share `inland_water` (tile-join concatenates the same layer
29225
+ // name), and an unsplit fill draws BOTH — which means the NE polygon is
29226
+ // painted over the planet one at every zoom. That is not a cosmetic
29227
+ // double-draw: NE 10m carries no interior rings, so its blob has no
29228
+ // ISLANDS in it, and painting it over the planet's holed polygons buries
29229
+ // every island in the lake under "uncharted water". Owner-reported at
29230
+ // Winnipesaukee z11.7 ("there is land (and islands) being covered by the
29231
+ // 'no chart here' boundary"), and measured: rendering the planet tier
29232
+ // alone brings back the whole archipelago — Rattlesnake, Bear, Long
29233
+ // Island, Diamond, the Barndoors — while the NE tier alone is a faceted
29234
+ // blob whose edges bear no relation to the shoreline drawn beneath it.
29235
+ //
29236
+ // So each tier owns its own zoom range, the same handover the shoreline
29237
+ // below uses. Known residue, measured over the whole archive: of the 141
29238
+ // NE lakes we still carry, 138 have planet water beneath them; the
29239
+ // exceptions are two unnamed western-Alaska tundra lakes and a 2e-5 deg²
29240
+ // Lake Michigan sliver, which lose their rough fill above z9 because the
29241
+ // planet has no `water` polygon there at all (checked against the raw
29242
+ // pre-clip NDJSON — not a clip artefact).
29107
29243
  L({
29108
29244
  id: "chrtbase-inland-water",
29109
29245
  type: "fill",
29110
29246
  source: src,
29111
29247
  "source-layer": "inland_water",
29248
+ filter: NE_TIER,
29249
+ maxzoom: HANDOVER,
29112
29250
  paint: { "fill-color": T.inlandWater }
29113
29251
  }),
29114
29252
  L({
@@ -29116,6 +29254,26 @@ function baseInlandWaterLayers(opts = {}) {
29116
29254
  type: "fill",
29117
29255
  source: src,
29118
29256
  "source-layer": "inland_water",
29257
+ filter: NE_TIER,
29258
+ maxzoom: HANDOVER,
29259
+ paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
29260
+ }),
29261
+ L({
29262
+ id: "chrtbase-inland-water-detail",
29263
+ type: "fill",
29264
+ source: src,
29265
+ "source-layer": "inland_water",
29266
+ filter: PLANET_TIER,
29267
+ minzoom: HANDOVER,
29268
+ paint: { "fill-color": T.inlandWater }
29269
+ }),
29270
+ L({
29271
+ id: "chrtbase-inland-hatch-detail",
29272
+ type: "fill",
29273
+ source: src,
29274
+ "source-layer": "inland_water",
29275
+ filter: PLANET_TIER,
29276
+ minzoom: HANDOVER,
29119
29277
  paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
29120
29278
  }),
29121
29279
  // The rough shoreline (owner ask 2026-08-28). Drawn ABOVE the hatch so
@@ -29135,13 +29293,47 @@ function baseInlandWaterLayers(opts = {}) {
29135
29293
  type: "line",
29136
29294
  source: src,
29137
29295
  "source-layer": "inland_water",
29138
- filter: ["!", ["has", "kind"]],
29296
+ filter: NE_TIER,
29297
+ // Hands the shoreline over to the planet tier at ITS floor. Both
29298
+ // tiers live in `inland_water` from z9 up (tile-join concatenates the
29299
+ // same layer name), and their FILLS merge invisibly — but two
29300
+ // outlines do not: NE 10m generalises Winnipesaukee's shape, so its
29301
+ // outline ran straight across the middle of the lake the planet had
29302
+ // drawn correctly around. The coarse line is right only while it is
29303
+ // the only shape we have.
29304
+ maxzoom: HANDOVER,
29139
29305
  paint: {
29140
29306
  "line-color": T.inlandShore,
29141
29307
  "line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1.1, 14, 1.6],
29142
29308
  "line-opacity": 0.9
29143
29309
  }
29144
29310
  }),
29311
+ // The PLANET tier's shoreline (owner ask 2026-08-31: "uncharted small
29312
+ // lakes render with no outline"). It reads its own `inland_shore` layer,
29313
+ // NOT `inland_water`, and that indirection is the whole fix: the mesh
29314
+ // that forced the `!has kind` fence above is a CLIP artefact — an MVT
29315
+ // feature arrives already cut to its own z10 tile, so stroking the
29316
+ // polygon strokes every tile boundary it crosses. The build now walks
29317
+ // those rings, drops the axis-aligned runs that sit on the read zoom's
29318
+ // tile grid, and emits what is left as lines (measured on one New
29319
+ // England chunk: 61 long axis-aligned mesh edges → 5, and the 5 survivors
29320
+ // are 0.02–0.5 tiles off the grid, i.e. real straight water edges).
29321
+ //
29322
+ // Thinner and quieter than the NE line: this is finer geometry describing
29323
+ // water nobody has surveyed, and approximate data should look
29324
+ // approximate.
29325
+ L({
29326
+ id: "chrtbase-inland-shore-detail",
29327
+ type: "line",
29328
+ source: src,
29329
+ "source-layer": "inland_shore",
29330
+ minzoom: HANDOVER,
29331
+ paint: {
29332
+ "line-color": T.inlandShore,
29333
+ "line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.5, 12, 0.9, 15, 1.2],
29334
+ "line-opacity": 0.85
29335
+ }
29336
+ }),
29145
29337
  // The lake's name (owner ask 2026-08-28) — the second scoped exception
29146
29338
  // to no-names-on-the-basemap, after the states: WATER-BODY names only.
29147
29339
  // No maxzoom: an uncharted lake keeps its name at every zoom; over
@@ -29158,6 +29350,47 @@ function baseInlandWaterLayers(opts = {}) {
29158
29350
  // deterministic anchor GRID into big lakes — the paper-chart convention
29159
29351
  // of repeating a name along a large water body — and this one layer
29160
29352
  // renders however many anchors are in view; collision culls the rest.
29353
+ // Planet-tier water-body NAMES (owner ask 2026-08-31: NE 10m names only
29354
+ // 113 US lakes). Emitted BELOW the NE layer on purpose — MapLibre places
29355
+ // symbols topmost-layer-first, so the big NE lake wins collision against
29356
+ // the pond beside it, and both lose to the chart's own labels (every ENC
29357
+ // class is emitted after this pair).
29358
+ //
29359
+ // The size threshold is OSM's own `min_zoom`, carried through the build:
29360
+ // Lake Winnipesaukee reads 10, Squam Lake 12, so a pond simply is not in
29361
+ // the filter until the user is close enough for it to be the answer to
29362
+ // "what is that water". `["zoom"]` in a filter evaluates at integer zoom
29363
+ // levels and removes the feature from placement entirely — verified
29364
+ // against maplibre-gl-native rather than assumed, because a text-size-0
29365
+ // gate would still claim a collision slot.
29366
+ //
29367
+ // The tiers are separate LAYERS, not a filtered split of one — see the
29368
+ // builder for why: an archive that a not-yet-deployed style would
29369
+ // mis-render cannot be promoted first, and this one has to be.
29370
+ L({
29371
+ id: "chrtbase-inland-label-detail",
29372
+ type: "symbol",
29373
+ source: src,
29374
+ "source-layer": "inland_water_label_detail",
29375
+ minzoom: HANDOVER,
29376
+ filter: [">=", ["zoom"], ["get", "min_zoom"]],
29377
+ layout: {
29378
+ "text-field": ["get", "name"],
29379
+ "text-font": ["Open Sans Regular"],
29380
+ "text-size": ["interpolate", ["linear"], ["zoom"], 9, 9, 12, 10.5, 15, 11],
29381
+ "text-max-width": 8,
29382
+ "text-allow-overlap": false,
29383
+ "text-padding": 6,
29384
+ // Bigger water first within the tier — min_zoom IS the size rank.
29385
+ "symbol-sort-key": ["get", "min_zoom"]
29386
+ },
29387
+ paint: {
29388
+ "text-color": T.inlandLabel,
29389
+ "text-halo-color": T.inlandHalo,
29390
+ "text-halo-width": 1,
29391
+ "text-opacity": 0.9
29392
+ }
29393
+ }),
29161
29394
  L({
29162
29395
  id: "chrtbase-inland-label",
29163
29396
  type: "symbol",
@@ -29389,14 +29622,21 @@ function makeBandVariants(layer, B) {
29389
29622
  return variants;
29390
29623
  }
29391
29624
  var CELL_BAND_OPACITY_CURVE = {
29392
- "1": [[3, 0.5], [5, 0.3], [16, 0.15]],
29393
- "2": [[5, 0.5], [7, 0.3], [16, 0.15]],
29394
- "3": [[7, 0.55], [9, 0.35], [16, 0.18]],
29395
- "4": [[9, 0.55], [11, 0.35], [16, 0.2]],
29396
- "5": [[11, 0.6], [13, 0.35], [18, 0.25]],
29397
- "6": [[13, 0.75], [16, 0.55], [22, 0.45]]
29625
+ "1": [[3, 0.5], [5, 0.42], [16, 0.34]],
29626
+ "2": [[5, 0.52], [7, 0.44], [16, 0.34]],
29627
+ "3": [[7, 0.55], [9, 0.46], [16, 0.36]],
29628
+ "4": [[9, 0.58], [11, 0.5], [16, 0.42]],
29629
+ "5": [[11, 0.62], [13, 0.52], [18, 0.44]],
29630
+ "6": [[13, 0.75], [16, 0.6], [22, 0.5]]
29631
+ };
29632
+ var ORIENTATION_OPACITY_SCALE = {
29633
+ LNDARE: 1.45,
29634
+ LNDRGN: 1.4,
29635
+ BUAARE: 1.25
29398
29636
  };
29399
- function fanOrientationLabelByCellBand(label, bandFan) {
29637
+ var ORIENTATION_OPACITY_MAX = 0.92;
29638
+ var ORIENTATION_BOOST_MINZOOM = 11;
29639
+ function fanOrientationLabelByCellBand(label, bandFan, objectClass) {
29400
29640
  const cellBandExpr = [
29401
29641
  "slice",
29402
29642
  ["coalesce", ["get", "_cell_id"], "??9???"],
@@ -29404,11 +29644,15 @@ function fanOrientationLabelByCellBand(label, bandFan) {
29404
29644
  3
29405
29645
  ];
29406
29646
  const variants = [];
29647
+ const scale = objectClass ? ORIENTATION_OPACITY_SCALE[objectClass] ?? 1 : 1;
29407
29648
  for (const code of BAND_DRAW_ORDER) {
29408
29649
  const range = BAND_PRIORITY_RANGES[code];
29409
29650
  const curve = CELL_BAND_OPACITY_CURVE[code];
29410
29651
  const opacityExpr = ["interpolate", ["linear"], ["zoom"]];
29411
- for (const [z, o] of curve) opacityExpr.push(z, o);
29652
+ for (const [z, o] of curve) {
29653
+ const s = z >= ORIENTATION_BOOST_MINZOOM ? scale : 1;
29654
+ opacityExpr.push(z, Math.round(Math.min(o * s, ORIENTATION_OPACITY_MAX) * 100) / 100);
29655
+ }
29412
29656
  const cellFiltered = combineWithBandFilter(
29413
29657
  label.filter,
29414
29658
  ["==", cellBandExpr, code]
@@ -29455,6 +29699,33 @@ function fanOrientationLabelByCellBand(label, bandFan) {
29455
29699
  }
29456
29700
  return variants;
29457
29701
  }
29702
+ var MAJOR_WATER_TEXT_PADDING = [
29703
+ "interpolate",
29704
+ ["linear"],
29705
+ ["zoom"],
29706
+ 5,
29707
+ 24,
29708
+ 9,
29709
+ 20,
29710
+ 13,
29711
+ 16,
29712
+ 16,
29713
+ 14
29714
+ ];
29715
+ function splitOrientationFan(fan, hoistedFilter, watermarkFilter, hoistedLayout) {
29716
+ return {
29717
+ hoist: fan.map((l) => ({
29718
+ ...l,
29719
+ id: `${l.id}-major`,
29720
+ filter: combineWithBandFilter(l.filter, hoistedFilter),
29721
+ ...hoistedLayout ? { layout: { ...l.layout ?? {}, ...hoistedLayout } } : {}
29722
+ })),
29723
+ watermark: fan.map((l) => ({
29724
+ ...l,
29725
+ filter: combineWithBandFilter(l.filter, watermarkFilter)
29726
+ }))
29727
+ };
29728
+ }
29458
29729
  function byLabelPriorityBottomUp(bundles) {
29459
29730
  return [...bundles].sort(
29460
29731
  (a, b) => getLabelPriority(b.objectClass) - getLabelPriority(a.objectClass)
@@ -29554,6 +29825,7 @@ function generateStyle(palette = "DAY", opts = {}) {
29554
29825
  layers.push(...baseInlandWaterLayers({ palette }));
29555
29826
  }
29556
29827
  const labelLayers = [];
29828
+ const majorWaterLayers = [];
29557
29829
  const chartCascade = opts.chartMode ? CHART_CASCADE_CODES[opts.chartMode.band] : null;
29558
29830
  const chartRequestedCodes = opts.chartMode ? CHART_BAND_CODES[opts.chartMode.band] : null;
29559
29831
  const bandFan = opts.bandFan ?? "collapsed";
@@ -29670,19 +29942,18 @@ function generateStyle(palette = "DAY", opts = {}) {
29670
29942
  }
29671
29943
  }
29672
29944
  {
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);
29945
+ const TERMINAL_FILL_RE = /^s52-(?:LNDARE|DEPARE)-fill-band[456]$/;
29946
+ for (const l of layers) {
29947
+ const lay = l;
29948
+ if (!TERMINAL_FILL_RE.test(lay.id)) continue;
29949
+ const floor = lay.minzoom ?? 0;
29950
+ if (floor <= 0) continue;
29951
+ lay.filter = [
29952
+ "all",
29953
+ lay.filter,
29954
+ ["any", [">=", ["zoom"], floor], ["==", ["get", "_terminal_fill"], true]]
29955
+ ];
29956
+ delete lay.minzoom;
29686
29957
  }
29687
29958
  }
29688
29959
  if (opts.labels === "placename") {
@@ -29691,7 +29962,23 @@ function generateStyle(palette = "DAY", opts = {}) {
29691
29962
  for (const { objectClass, label } of bundles) {
29692
29963
  if (!label) continue;
29693
29964
  if (getLabelBehavior(objectClass) === "orientation-cross-band") {
29694
- labelLayers.push(...fanOrientationLabelByCellBand(label, bandFan));
29965
+ if (objectClass === "LNDARE" || objectClass === "SEAARE") {
29966
+ const Z = ISLAND_LABEL_HOIST_MINZOOM;
29967
+ const { hoist, watermark } = objectClass === "LNDARE" ? splitOrientationFan(
29968
+ fanOrientationLabelByCellBand(label, bandFan, objectClass),
29969
+ [">=", ["zoom"], Z],
29970
+ ["<", ["zoom"], Z]
29971
+ ) : splitOrientationFan(
29972
+ fanOrientationLabelByCellBand(label, bandFan, objectClass),
29973
+ ["<=", buildSeaAreaTierExpression(), MAJOR_WATER_TIER_MAX],
29974
+ [">", buildSeaAreaTierExpression(), MAJOR_WATER_TIER_MAX],
29975
+ { "text-padding": MAJOR_WATER_TEXT_PADDING }
29976
+ );
29977
+ majorWaterLayers.push(...hoist);
29978
+ labelLayers.push(...watermark);
29979
+ continue;
29980
+ }
29981
+ labelLayers.push(...fanOrientationLabelByCellBand(label, bandFan, objectClass));
29695
29982
  }
29696
29983
  }
29697
29984
  }
@@ -29700,7 +29987,12 @@ function generateStyle(palette = "DAY", opts = {}) {
29700
29987
  (b) => b.label !== null && getLabelBehavior(b.objectClass) !== "orientation-cross-band"
29701
29988
  )
29702
29989
  );
29703
- for (const { label } of featureLabelBundles) {
29990
+ let majorsEmitted = majorWaterLayers.length === 0;
29991
+ for (const { label, objectClass: cls } of featureLabelBundles) {
29992
+ if (!majorsEmitted && getLabelPriority(cls) < MAJOR_WATER_LABEL_PRIORITY) {
29993
+ labelLayers.push(...majorWaterLayers);
29994
+ majorsEmitted = true;
29995
+ }
29704
29996
  if (bandFan === "collapsed") {
29705
29997
  labelLayers.push(makeCollapsedCrossBandLabel(label, isDominanceExempt(label)));
29706
29998
  continue;
@@ -29709,6 +30001,7 @@ function generateStyle(palette = "DAY", opts = {}) {
29709
30001
  labelLayers.push(...emitBand(label, code));
29710
30002
  }
29711
30003
  }
30004
+ if (!majorsEmitted) labelLayers.push(...majorWaterLayers);
29712
30005
  layers.push(...labelLayers);
29713
30006
  }
29714
30007
  const symbolSet = opts.symbolSet ?? "s52";