@chrtco/sdk 0.10.49 → 0.10.50

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/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ All notable changes to `@chrtco/sdk` will be documented in this file.
5
5
  ## [Unreleased]
6
6
 
7
7
  ### Added
8
+ - **`generateStyle({ dangerDepthPlacers: true })`** — the danger depths (the depth beside a rock,
9
+ wreck or obstruction) as a pair: an invisible placer `s52-{UWTROC,OBSTRN,WRECKS}-placer-label`
10
+ in 0.10.3's collision form (`text-opacity: 0`) beneath each callout, which is then always drawn
11
+ and inserts no collision box, so no sounding is hidden and a tile draws the callouts once.
12
+ Default off: the style is unchanged unless asked. `addDangerDepthPlacers`,
13
+ `isDangerDepthPlacer` (skip the placers when hit-testing), `dangerDepthPlacerId`,
14
+ `DANGER_DEPTH_LABEL_ID`, `DANGER_DEPTH_PLACER_ID`, `DANGER_DEPTH_PLACER_LAYOUT`,
15
+ `DANGER_DEPTH_VISIBLE_LAYOUT`. The style routes take it as `?placers=1`.
8
16
  - **`EINK_NIGHT`** — the e-ink night palette (`theme=eink_night`, offline `palette=EINK_NIGHT`,
9
17
  `EINK_COLOR_TABLES.night`): `EINK_MONO`'s four greys on a black page, for any e-ink screen at
10
18
  night with the front light on, in place of red night. Water black; lines and labels `#aaaaaa`;
package/dist/index.cjs CHANGED
@@ -61,6 +61,10 @@ __export(index_exports, {
61
61
  CHRT_STYLE_MODES: () => CHRT_STYLE_MODES,
62
62
  Chrt: () => Chrt,
63
63
  ChrtError: () => ChrtError,
64
+ DANGER_DEPTH_LABEL_ID: () => DANGER_DEPTH_LABEL_ID,
65
+ DANGER_DEPTH_PLACER_ID: () => DANGER_DEPTH_PLACER_ID,
66
+ DANGER_DEPTH_PLACER_LAYOUT: () => DANGER_DEPTH_PLACER_LAYOUT,
67
+ DANGER_DEPTH_VISIBLE_LAYOUT: () => DANGER_DEPTH_VISIBLE_LAYOUT,
64
68
  DEFAULT_CLICK_TOLERANCE: () => DEFAULT_CLICK_TOLERANCE,
65
69
  DEFAULT_CUSTOM_STYLE_CONFIG: () => DEFAULT_CUSTOM_STYLE_CONFIG,
66
70
  DEFAULT_INSPECT_CLASS_RANK: () => DEFAULT_INSPECT_CLASS_RANK,
@@ -159,6 +163,7 @@ __export(index_exports, {
159
163
  WATER_BODY_LAYER_GROUPS: () => WATER_BODY_LAYER_GROUPS,
160
164
  WATER_TEXT_CLASSES: () => WATER_TEXT_CLASSES,
161
165
  WeatherEndpoint: () => WeatherEndpoint,
166
+ addDangerDepthPlacers: () => addDangerDepthPlacers,
162
167
  applyBaseBundle: () => applyBaseBundle,
163
168
  applyChartSymbolScales: () => applyChartSymbolScales,
164
169
  applyChartSymbolScalesToLayer: () => applyChartSymbolScalesToLayer,
@@ -201,6 +206,7 @@ __export(index_exports, {
201
206
  combineWithBandFilter: () => combineWithBandFilter,
202
207
  contourValueFactor: () => contourValueFactor,
203
208
  contrastRatio: () => contrastRatio,
209
+ dangerDepthPlacerId: () => dangerDepthPlacerId,
204
210
  daymarkColor: () => daymarkColor,
205
211
  decimateFeatures: () => decimateFeatures,
206
212
  decodeAreaFeatureProps: () => decodeAreaFeatureProps,
@@ -251,6 +257,7 @@ __export(index_exports, {
251
257
  hazardFillExpression: () => hazardFillExpression,
252
258
  iconExpressionForFamily: () => iconExpressionForFamily,
253
259
  iconSliderFor: () => iconSliderFor,
260
+ isDangerDepthPlacer: () => isDangerDepthPlacer,
254
261
  isDepthAreaClassLayerId: () => isDepthAreaClassLayerId,
255
262
  isDrapeable: () => isDrapeable,
256
263
  isEinkColorTable: () => isEinkColorTable,
@@ -24903,6 +24910,11 @@ function labelSortKey(rank, within) {
24903
24910
  ["min", 999, ["max", 0, within]]
24904
24911
  ];
24905
24912
  }
24913
+ var DANGER_DEPTH_PLACEMENT_KEY = labelSortKey(100, [
24914
+ "*",
24915
+ 10,
24916
+ ["to-number", ["coalesce", ["get", "VALSOU"], ["get", "valsou"], 0], 0]
24917
+ ]);
24906
24918
  var NAV_DESIGNATION_STOPWORDS = [
24907
24919
  "Buoy",
24908
24920
  "Can",
@@ -25710,43 +25722,23 @@ function getLabelConfig(layerName, opts = {}) {
25710
25722
  "text-variable-anchor": ["top-left", "bottom-left", "top-right", "bottom-right"],
25711
25723
  "text-radial-offset": 0.9,
25712
25724
  "text-justify": "auto",
25713
- // ⚠ A DANGER DEPTH IS NEVER DROPPED (owner, 2026-10-06). Before,
25714
- // a callout with no free corner was culled and the rock, wreck or
25715
- // obstruction drew with no depth beside it: 15 of 136 at Boston
25716
- // z12.3. ALLOW-OVERLAP ONLY, and that is the whole point:
25717
- // - the variable anchors still fan out first — MapLibre 5.24
25718
- // tries every corner without overlap and overlaps, at the
25719
- // first corner, only when none is free (`placement.ts`,
25720
- // `placementPasses`), so only the callouts that used to vanish
25721
- // draw over something;
25722
- // - `text-ignore-placement` stays false, so a callout keeps its
25723
- // collision space and blocks what it blocked before. Ignoring
25724
- // placement as well freed that space for buoy and place names,
25725
- // which then pushed shallow soundings off: 11 shoals uncovered
25726
- // at z12.3 on the bench. This form: 0.
25727
- // The census and its numbers: `scripts/chart-bench/
25728
- // danger-depth-census.ts` and `.chrt/standards/s-52.md` §8a.
25725
+ // ⚠ A DANGER DEPTH IS NEVER DROPPED (owner, 2026-10-06). This is
25726
+ // 0.10.4's form, which the SERVED style keeps: always drawn
25727
+ // (`text-allow-overlap`), still in collision, fanned out by its four
25728
+ // variable anchors, placed shallowest first. Its cost, measured: a
25729
+ // newly drawn callout takes space a sounding held (7 soundings under
25730
+ // 3.048 m hidden at the 100 % default), and the depth key on an
25731
+ // overlapping layer draws one segment per depth (+14 % draw calls).
25732
+ // With `generateStyle({ dangerDepthPlacers: true })` — CHRTweb and
25733
+ // CHRTdesktop — `danger-depth-placer.ts` turns it into a PAIR: an
25734
+ // invisible placer in 0.10.3's form holds the space, and this label
25735
+ // is drawn over it, displacing nothing, with one key. The census:
25736
+ // `scripts/chart-bench/danger-depth-census.ts`; `.chrt/standards/
25737
+ // s-52.md` §8a.
25729
25738
  "text-allow-overlap": true,
25730
25739
  "text-optional": true,
25731
25740
  "text-padding": 2,
25732
- // ⚠ THE DEPTH SORT KEY IS LOAD-BEARING — do not "optimise" it to a
25733
- // constant. It costs draw calls: MapLibre 5.24 draws an overlapping
25734
- // layer with a data-driven key one segment per distinct key, twice
25735
- // with a halo over 1 px (`drawLayerSymbols`, `hasSortKey &&
25736
- // bucket.canOverlap`), so at 100 % the chart issues +14 % draw calls
25737
- // at z12.3 against 0.10.3 (R6's 0.10.4 gate). But the key is also
25738
- // the PLACEMENT order, and a callout keeps its collision box, so the
25739
- // order decides which corner each callout takes and therefore which
25740
- // soundings and buoy names survive beneath it. Measured with each
25741
- // form booted fresh (2026-10-06, Boston and Salem Sound, z11.5–16,
25742
- // at 1280×800 and the Pi's 1920×1185): a constant key, 3/4/5 depth
25743
- // bands and whole metres ALL uncover shoals that 0.10.3 drew (a
25744
- // constant key: 5 at z12.3 on the Pi's canvas, a 0.6 m one at
25745
- // 1280×800) and change buoy names; only this key matches 0.10.4's
25746
- // census. `danger-depth-census.ts --form <json>` re-runs that gate
25747
- // for any candidate. A 1 px halo (one pass, not two) halves the
25748
- // cost with placement byte-identical — a portrayal call, not taken.
25749
- "symbol-sort-key": labelSortKey(100, ["*", 10, valsou])
25741
+ "symbol-sort-key": DANGER_DEPTH_PLACEMENT_KEY
25750
25742
  }
25751
25743
  };
25752
25744
  }
@@ -26507,6 +26499,62 @@ function buildBridgeNoteLayers(sourceId, palette = "DAY", depthUnits = "meters")
26507
26499
  ];
26508
26500
  }
26509
26501
 
26502
+ // src/atlas/danger-depth-placer.ts
26503
+ var DANGER_DEPTH_LABEL_ID = /^s52-(UWTROC|OBSTRN|WRECKS)-label(?=$|-)/;
26504
+ var DANGER_DEPTH_PLACER_ID = /^s52-(UWTROC|OBSTRN|WRECKS)-placer-label(?=$|-)/;
26505
+ function dangerDepthPlacerId(labelId) {
26506
+ return labelId.replace(/^s52-(UWTROC|OBSTRN|WRECKS)-label/, "s52-$1-placer-label");
26507
+ }
26508
+ function isDangerDepthPlacer(layerId) {
26509
+ return DANGER_DEPTH_PLACER_ID.test(layerId);
26510
+ }
26511
+ var DANGER_DEPTH_VISIBLE_LAYOUT = {
26512
+ "text-allow-overlap": true,
26513
+ "text-ignore-placement": true,
26514
+ "symbol-sort-key": labelSortKey(100),
26515
+ "symbol-z-order": "source"
26516
+ };
26517
+ var DANGER_DEPTH_PLACER_LAYOUT = {
26518
+ "text-allow-overlap": false,
26519
+ "text-ignore-placement": false,
26520
+ "symbol-sort-key": DANGER_DEPTH_PLACEMENT_KEY
26521
+ };
26522
+ function addDangerDepthPlacers(layers) {
26523
+ if (!layers.some((l) => isVisibleDangerDepthLabel(l))) return layers;
26524
+ const out = [];
26525
+ for (let i = 0; i < layers.length; ) {
26526
+ const cls = classOf(layers[i]);
26527
+ if (cls === null) {
26528
+ out.push(layers[i]);
26529
+ i++;
26530
+ continue;
26531
+ }
26532
+ let j = i;
26533
+ while (j < layers.length && classOf(layers[j]) === cls) j++;
26534
+ const run = layers.slice(i, j);
26535
+ for (const l of run) out.push(placerFor(l));
26536
+ for (const l of run) out.push({ ...l, layout: { ...l.layout ?? {}, ...DANGER_DEPTH_VISIBLE_LAYOUT } });
26537
+ i = j;
26538
+ }
26539
+ return out;
26540
+ }
26541
+ function placerFor(l) {
26542
+ const { "symbol-z-order": _zOrder, ...layout } = l.layout ?? {};
26543
+ return {
26544
+ ...l,
26545
+ id: dangerDepthPlacerId(l.id),
26546
+ layout: { ...layout, ...DANGER_DEPTH_PLACER_LAYOUT },
26547
+ paint: { ...l.paint ?? {}, "text-opacity": 0 },
26548
+ metadata: { ...l.metadata ?? {}, "chrt:placerFor": l.id }
26549
+ };
26550
+ }
26551
+ function isVisibleDangerDepthLabel(l) {
26552
+ return l.type === "symbol" && DANGER_DEPTH_LABEL_ID.test(l.id) && l.layout?.["text-field"] !== void 0;
26553
+ }
26554
+ function classOf(l) {
26555
+ return isVisibleDangerDepthLabel(l) ? DANGER_DEPTH_LABEL_ID.exec(l.id)?.[1] ?? null : null;
26556
+ }
26557
+
26510
26558
  // src/atlas/feature-decimation.ts
26511
26559
  function getGridSizeForZoom(zoom) {
26512
26560
  if (zoom < 7) return 50;
@@ -29663,7 +29711,6 @@ function buildLayersForClass(objectClass2, ct, sourceId = DEFAULT_REGION, opts =
29663
29711
  };
29664
29712
  if (HAZARD_GROUPS.has(entry?.viewingGroup ?? 0)) {
29665
29713
  const watlev = ["to-number", ["coalesce", ["get", "WATLEV"], ["get", "watlev"], 0], 0];
29666
- const valsou = ["to-number", ["coalesce", ["get", "VALSOU"], ["get", "valsou"], 99], 99];
29667
29714
  const hazardIconSize = [
29668
29715
  "interpolate",
29669
29716
  ["linear"],
@@ -29707,9 +29754,9 @@ function buildLayersForClass(objectClass2, ct, sourceId = DEFAULT_REGION, opts =
29707
29754
  //
29708
29755
  // Deconflicting hazards against each other stays; it just no longer
29709
29756
  // runs through suppression:
29710
- // - `symbol-sort-key` (below) still ranks by danger, and with
29711
- // overlap allowed it now sets DRAW ORDER, so the worst hazard is
29712
- // the one painted on top rather than the one that survives.
29757
+ // - `symbol-sort-key` (below) sets PAINT ORDER, so the worst
29758
+ // hazard is the one painted on top rather than the one that
29759
+ // survives.
29713
29760
  // - the icon-size curve above is what keeps a dense ledge legible
29714
29761
  // at the zoomed-out end of Harbor. That is now the only thing
29715
29762
  // doing it, which is why its low end was lowered in the same
@@ -29726,18 +29773,59 @@ function buildLayersForClass(objectClass2, ct, sourceId = DEFAULT_REGION, opts =
29726
29773
  // hazards present to OTHER classes' placement, keeping labels off
29727
29774
  // them.
29728
29775
  "icon-padding": 2,
29776
+ // ── PAINT ORDER: THE WORST HAZARD ON TOP, IN FOUR KEYS ─────────────
29777
+ //
29778
+ // With overlap allowed MapLibre paints a layer in ascending key,
29779
+ // so a HIGHER key is painted OVER a lower one:
29780
+ // 3 covers and uncovers (WATLEV 4)
29781
+ // 2 awash (WATLEV 5)
29782
+ // 1 everything else: submerged, unknown, subject to inundation,
29783
+ // floating
29784
+ // 0 stands above the water (WATLEV 1 partly submerged at high
29785
+ // water, 2 always dry). Seen by eye, and it usually carries no
29786
+ // sounding, so the old key already ranked it last (a missing
29787
+ // VALSOU counted as 99 m).
29788
+ //
29789
+ // Until 2026-10-09 this was `case 4 → 1, 5 → 2, else 10 + VALSOU`,
29790
+ // meant as "worst first". With overlap that is "worst UNDERNEATH":
29791
+ // the deepest rock was painted over everything. Measured on the
29792
+ // chart bench, Boston z12.3 at 1280×800: of 758 overlapping pairs
29793
+ // with different icons, all 758 had the lesser hazard on top.
29794
+ //
29795
+ // Why four keys and not a depth ladder:
29796
+ // - An icon is picked by WATLEV alone (the `icon-image` match
29797
+ // above, every symbol family), never by depth. Each key holds
29798
+ // ONE icon per class (UWTROC 01 | 03 | 04 | 04, OBSTRN
29799
+ // 03 | 03 | 03 | 01, WRECKS 04 | 04 | 04 | 01), so two hazards
29800
+ // that share a key look the same, and which is on top cannot be
29801
+ // seen. With three keys the dry obstructions and wrecks shared
29802
+ // the lowest one and were painted over submerged ones up to 4
29803
+ // times a view; with four, never.
29804
+ // - MapLibre 5.24 draws a layer that can overlap and has a
29805
+ // feature-reading key ONE SEGMENT PER DISTINCT KEY PER TILE
29806
+ // (`SymbolBucket.addSymbols` → `prepareSegment`;
29807
+ // `drawLayerSymbols`, `hasSortKey && bucket.canOverlap`). The
29808
+ // old key had one value per sounded depth, so it cost a draw
29809
+ // call for every distinct VALSOU in every tile (up to 50 in one
29810
+ // bucket). Four keys cost at most four, and no bucket measured
29811
+ // held more than three.
29812
+ //
29813
+ // It should change paint order only. A hazard icon has one fixed
29814
+ // anchor and is always drawn, and every one of them puts its box in
29815
+ // the collision index whatever the order. The census measures that
29816
+ // rather than assuming it: every icon, label and sounding in view,
29817
+ // each form booted twice (`scripts/chart-bench/hazard-icon-census.ts`).
29818
+ // Its numbers are in `danger-depth-callouts.test.ts`, which pins
29819
+ // this order.
29729
29820
  "symbol-sort-key": [
29730
29821
  "case",
29731
29822
  ["==", watlev, 4],
29732
- 1,
29823
+ 3,
29733
29824
  ["==", watlev, 5],
29734
29825
  2,
29735
- // Submerged rocks: sounded depth determines priority. Shallower
29736
- // = smaller sort-key = wins collisions. Offset by 10 so any
29737
- // submerged rock still loses to a covers-and-uncovers / awash
29738
- // hazard at the same point, but ranks dangerously-shallow
29739
- // submerged rocks above deeper ones.
29740
- ["+", 10, valsou]
29826
+ ["any", ["==", watlev, 1], ["==", watlev, 2]],
29827
+ 0,
29828
+ 1
29741
29829
  ]
29742
29830
  };
29743
29831
  }
@@ -32309,7 +32397,14 @@ function generateStyle(palette = "DAY", opts = {}) {
32309
32397
  layers: finalLayers
32310
32398
  };
32311
32399
  const familyStyle = symbolSet === "s101" ? applyS101SymbolSet(style) : symbolSet === "chrt" ? applyChrtSymbolSet(style, palette) : symbolSet === "raster" ? applyRasterSymbolSet(style) : style;
32312
- const portrayed = applyEinkPortrayal(familyStyle, palette, { safetyContour: opts.safetyContour });
32400
+ const einked = applyEinkPortrayal(familyStyle, palette, { safetyContour: opts.safetyContour });
32401
+ const placed = opts.dangerDepthPlacers === true ? addDangerDepthPlacers(einked.layers) : einked.layers;
32402
+ const portrayed = placed === einked.layers ? einked : {
32403
+ ...einked,
32404
+ layers: placed,
32405
+ // The placers are emitted layers: the count is of what the style carries.
32406
+ metadata: { ...einked.metadata, "chrt:layer-count": placed.length }
32407
+ };
32313
32408
  const sized = applyChartSymbolScales(portrayed.layers, opts.symbolScales, opts.soundingsRule);
32314
32409
  return sized === portrayed.layers ? portrayed : { ...portrayed, layers: sized };
32315
32410
  }
@@ -33235,6 +33330,10 @@ var Chrt = class {
33235
33330
  CHRT_STYLE_MODES,
33236
33331
  Chrt,
33237
33332
  ChrtError,
33333
+ DANGER_DEPTH_LABEL_ID,
33334
+ DANGER_DEPTH_PLACER_ID,
33335
+ DANGER_DEPTH_PLACER_LAYOUT,
33336
+ DANGER_DEPTH_VISIBLE_LAYOUT,
33238
33337
  DEFAULT_CLICK_TOLERANCE,
33239
33338
  DEFAULT_CUSTOM_STYLE_CONFIG,
33240
33339
  DEFAULT_INSPECT_CLASS_RANK,
@@ -33333,6 +33432,7 @@ var Chrt = class {
33333
33432
  WATER_BODY_LAYER_GROUPS,
33334
33433
  WATER_TEXT_CLASSES,
33335
33434
  WeatherEndpoint,
33435
+ addDangerDepthPlacers,
33336
33436
  applyBaseBundle,
33337
33437
  applyChartSymbolScales,
33338
33438
  applyChartSymbolScalesToLayer,
@@ -33375,6 +33475,7 @@ var Chrt = class {
33375
33475
  combineWithBandFilter,
33376
33476
  contourValueFactor,
33377
33477
  contrastRatio,
33478
+ dangerDepthPlacerId,
33378
33479
  daymarkColor,
33379
33480
  decimateFeatures,
33380
33481
  decodeAreaFeatureProps,
@@ -33425,6 +33526,7 @@ var Chrt = class {
33425
33526
  hazardFillExpression,
33426
33527
  iconExpressionForFamily,
33427
33528
  iconSliderFor,
33529
+ isDangerDepthPlacer,
33428
33530
  isDepthAreaClassLayerId,
33429
33531
  isDrapeable,
33430
33532
  isEinkColorTable,