@chrtco/sdk 0.10.48 → 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",
@@ -25706,32 +25718,27 @@ function getLabelConfig(layerName, opts = {}) {
25706
25718
  // UWTROC/OBSTRN/WRECKS are dominance-PARTICIPATING classes —
25707
25719
  // exactly one band's copy renders at any zoom, so anchor-shifting
25708
25720
  // can't split a cross-band duplicate pair into two visible labels.
25709
- // Shallowest (most dangerous) hazard wins within-class contests;
25710
- // VALSOU-less hazards (awash / unsounded) rank first of all.
25711
25721
  layout: {
25712
25722
  "text-variable-anchor": ["top-left", "bottom-left", "top-right", "bottom-right"],
25713
25723
  "text-radial-offset": 0.9,
25714
25724
  "text-justify": "auto",
25715
- // ⚠ A DANGER DEPTH IS NEVER DROPPED (owner, 2026-10-06). Before,
25716
- // a callout with no free corner was culled and the rock, wreck or
25717
- // obstruction drew with no depth beside it: 15 of 136 at Boston
25718
- // z12.3. ALLOW-OVERLAP ONLY, and that is the whole point:
25719
- // - the variable anchors still fan out first — MapLibre 5.24
25720
- // tries every corner without overlap and overlaps, at the
25721
- // first corner, only when none is free (`placement.ts`,
25722
- // `placementPasses`), so only the callouts that used to vanish
25723
- // draw over something;
25724
- // - `text-ignore-placement` stays false, so a callout keeps its
25725
- // collision space and blocks what it blocked before. Ignoring
25726
- // placement as well freed that space for buoy and place names,
25727
- // which then pushed shallow soundings off: 11 shoals uncovered
25728
- // at z12.3 on the bench. This form: 0.
25729
- // The census and its numbers: `scripts/chart-bench/
25730
- // 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.
25731
25738
  "text-allow-overlap": true,
25732
25739
  "text-optional": true,
25733
25740
  "text-padding": 2,
25734
- "symbol-sort-key": labelSortKey(100, ["*", 10, valsou])
25741
+ "symbol-sort-key": DANGER_DEPTH_PLACEMENT_KEY
25735
25742
  }
25736
25743
  };
25737
25744
  }
@@ -26492,6 +26499,62 @@ function buildBridgeNoteLayers(sourceId, palette = "DAY", depthUnits = "meters")
26492
26499
  ];
26493
26500
  }
26494
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
+
26495
26558
  // src/atlas/feature-decimation.ts
26496
26559
  function getGridSizeForZoom(zoom) {
26497
26560
  if (zoom < 7) return 50;
@@ -29648,7 +29711,6 @@ function buildLayersForClass(objectClass2, ct, sourceId = DEFAULT_REGION, opts =
29648
29711
  };
29649
29712
  if (HAZARD_GROUPS.has(entry?.viewingGroup ?? 0)) {
29650
29713
  const watlev = ["to-number", ["coalesce", ["get", "WATLEV"], ["get", "watlev"], 0], 0];
29651
- const valsou = ["to-number", ["coalesce", ["get", "VALSOU"], ["get", "valsou"], 99], 99];
29652
29714
  const hazardIconSize = [
29653
29715
  "interpolate",
29654
29716
  ["linear"],
@@ -29692,9 +29754,9 @@ function buildLayersForClass(objectClass2, ct, sourceId = DEFAULT_REGION, opts =
29692
29754
  //
29693
29755
  // Deconflicting hazards against each other stays; it just no longer
29694
29756
  // runs through suppression:
29695
- // - `symbol-sort-key` (below) still ranks by danger, and with
29696
- // overlap allowed it now sets DRAW ORDER, so the worst hazard is
29697
- // 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.
29698
29760
  // - the icon-size curve above is what keeps a dense ledge legible
29699
29761
  // at the zoomed-out end of Harbor. That is now the only thing
29700
29762
  // doing it, which is why its low end was lowered in the same
@@ -29711,18 +29773,59 @@ function buildLayersForClass(objectClass2, ct, sourceId = DEFAULT_REGION, opts =
29711
29773
  // hazards present to OTHER classes' placement, keeping labels off
29712
29774
  // them.
29713
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.
29714
29820
  "symbol-sort-key": [
29715
29821
  "case",
29716
29822
  ["==", watlev, 4],
29717
- 1,
29823
+ 3,
29718
29824
  ["==", watlev, 5],
29719
29825
  2,
29720
- // Submerged rocks: sounded depth determines priority. Shallower
29721
- // = smaller sort-key = wins collisions. Offset by 10 so any
29722
- // submerged rock still loses to a covers-and-uncovers / awash
29723
- // hazard at the same point, but ranks dangerously-shallow
29724
- // submerged rocks above deeper ones.
29725
- ["+", 10, valsou]
29826
+ ["any", ["==", watlev, 1], ["==", watlev, 2]],
29827
+ 0,
29828
+ 1
29726
29829
  ]
29727
29830
  };
29728
29831
  }
@@ -32294,7 +32397,14 @@ function generateStyle(palette = "DAY", opts = {}) {
32294
32397
  layers: finalLayers
32295
32398
  };
32296
32399
  const familyStyle = symbolSet === "s101" ? applyS101SymbolSet(style) : symbolSet === "chrt" ? applyChrtSymbolSet(style, palette) : symbolSet === "raster" ? applyRasterSymbolSet(style) : style;
32297
- 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
+ };
32298
32408
  const sized = applyChartSymbolScales(portrayed.layers, opts.symbolScales, opts.soundingsRule);
32299
32409
  return sized === portrayed.layers ? portrayed : { ...portrayed, layers: sized };
32300
32410
  }
@@ -33220,6 +33330,10 @@ var Chrt = class {
33220
33330
  CHRT_STYLE_MODES,
33221
33331
  Chrt,
33222
33332
  ChrtError,
33333
+ DANGER_DEPTH_LABEL_ID,
33334
+ DANGER_DEPTH_PLACER_ID,
33335
+ DANGER_DEPTH_PLACER_LAYOUT,
33336
+ DANGER_DEPTH_VISIBLE_LAYOUT,
33223
33337
  DEFAULT_CLICK_TOLERANCE,
33224
33338
  DEFAULT_CUSTOM_STYLE_CONFIG,
33225
33339
  DEFAULT_INSPECT_CLASS_RANK,
@@ -33318,6 +33432,7 @@ var Chrt = class {
33318
33432
  WATER_BODY_LAYER_GROUPS,
33319
33433
  WATER_TEXT_CLASSES,
33320
33434
  WeatherEndpoint,
33435
+ addDangerDepthPlacers,
33321
33436
  applyBaseBundle,
33322
33437
  applyChartSymbolScales,
33323
33438
  applyChartSymbolScalesToLayer,
@@ -33360,6 +33475,7 @@ var Chrt = class {
33360
33475
  combineWithBandFilter,
33361
33476
  contourValueFactor,
33362
33477
  contrastRatio,
33478
+ dangerDepthPlacerId,
33363
33479
  daymarkColor,
33364
33480
  decimateFeatures,
33365
33481
  decodeAreaFeatureProps,
@@ -33410,6 +33526,7 @@ var Chrt = class {
33410
33526
  hazardFillExpression,
33411
33527
  iconExpressionForFamily,
33412
33528
  iconSliderFor,
33529
+ isDangerDepthPlacer,
33413
33530
  isDepthAreaClassLayerId,
33414
33531
  isDrapeable,
33415
33532
  isEinkColorTable,