@chrtco/sdk 0.10.0 → 0.10.2

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.d.cts CHANGED
@@ -2600,20 +2600,37 @@ declare function worldMinusBboxPolygon(bbox: BBox): GeoJSON.Polygon;
2600
2600
  * `band-collapse.test.ts` re-proves it on every CI run by differentially
2601
2601
  * evaluating legacy-vs-collapsed through MapLibre's real three gates.
2602
2602
  *
2603
- * **Scope.** This is axis 1 ONLY: the per-band layers survive, with identical
2604
- * ids and identical positions, so the band-major draw order is untouched.
2605
- * "Axis 2" (collapsing the per-band copies into one layer per class) was
2606
- * evaluated and **KILLED** — it is unsound for the opaque-fill classes
2603
+ * **Scope.** For FEATURE layers (fills, lines, feature symbols) this is
2604
+ * axis 1 ONLY: the per-band layers survive, with identical ids and identical
2605
+ * positions, so the band-major draw order is untouched. "Axis 2" (collapsing
2606
+ * the per-band copies into one layer per class) was evaluated and **KILLED**
2607
+ * for feature layers — it is unsound for the opaque-fill classes
2607
2608
  * (`DOMINANCE_EXEMPT_FILL_CLASSES` ≡ `OPAQUE_FILL_LAYERS`), where band-major
2608
2609
  * order is the ONLY thing separating the bands, and 15/15 class pairs are
2609
- * provably unsatisfiable with one layer each. Do not reintroduce it.
2610
+ * provably unsatisfiable with one layer each. Do not reintroduce it there.
2610
2611
  * Evidence: `.chrt/perf/2026-07-14-p8-d4-spike.md`.
2611
2612
  *
2612
- * **Load-bearing invariant.** The collapsed layer MUST keep the `-band{B}`
2613
- * id segment: `custom-style.ts` `bandFloor()` parses `/-band(\d)/` to clamp
2614
- * customer minzoom/scamin overrides to the band's data floor, on the live
2615
- * public Style API path. Renaming the id silently unclamps those overrides.
2616
- * Asserted in `band-collapse.test.ts`.
2613
+ * LABEL layers are different (CHRTsail #121 — MapLibre Native iOS silently
2614
+ * collapses its whole render pass above 482 symbol layers, and the served
2615
+ * style carried 575): `makeCollapsedCrossBandLabel` folds a label's ×6 band
2616
+ * fan into ONE layer per class, and `collapsedOrientationGate` folds the
2617
+ * orientation fan's per-dominator variants into its per-cell layers. That is
2618
+ * NOT the killed axis 2, because what the fill argument protects — paint
2619
+ * order as the sole band separator — does not apply to labels: their
2620
+ * load-bearing order is the cross-class P4 priority ladder (each class keeps
2621
+ * exactly one layer in its ladder slot), and within-class placement priority
2622
+ * rides the `rank*1000 + within-key` symbol-sort-key that labels.ts
2623
+ * documents as designed to survive precisely this merge.
2624
+ *
2625
+ * **Load-bearing invariant.** A collapsed per-band FEATURE layer MUST keep
2626
+ * the `-band{B}` id segment: `custom-style.ts` `bandFloor()` parses
2627
+ * `/-band(\d)/` to clamp customer minzoom/scamin overrides to the band's
2628
+ * data floor, on the live public Style API path. Renaming the id silently
2629
+ * unclamps those overrides. Cross-band LABEL layers are the deliberate
2630
+ * exception: they carry every band floor INSIDE the filter (tile-zoom
2631
+ * clauses a customer override cannot touch), which supersedes the clamp —
2632
+ * `bandFloor()` returning 0 for a bare label id is correct, not a gap.
2633
+ * Both halves asserted in `band-collapse.test.ts`.
2617
2634
  */
2618
2635
 
2619
2636
  /**
package/dist/index.d.ts CHANGED
@@ -2600,20 +2600,37 @@ declare function worldMinusBboxPolygon(bbox: BBox): GeoJSON.Polygon;
2600
2600
  * `band-collapse.test.ts` re-proves it on every CI run by differentially
2601
2601
  * evaluating legacy-vs-collapsed through MapLibre's real three gates.
2602
2602
  *
2603
- * **Scope.** This is axis 1 ONLY: the per-band layers survive, with identical
2604
- * ids and identical positions, so the band-major draw order is untouched.
2605
- * "Axis 2" (collapsing the per-band copies into one layer per class) was
2606
- * evaluated and **KILLED** — it is unsound for the opaque-fill classes
2603
+ * **Scope.** For FEATURE layers (fills, lines, feature symbols) this is
2604
+ * axis 1 ONLY: the per-band layers survive, with identical ids and identical
2605
+ * positions, so the band-major draw order is untouched. "Axis 2" (collapsing
2606
+ * the per-band copies into one layer per class) was evaluated and **KILLED**
2607
+ * for feature layers — it is unsound for the opaque-fill classes
2607
2608
  * (`DOMINANCE_EXEMPT_FILL_CLASSES` ≡ `OPAQUE_FILL_LAYERS`), where band-major
2608
2609
  * order is the ONLY thing separating the bands, and 15/15 class pairs are
2609
- * provably unsatisfiable with one layer each. Do not reintroduce it.
2610
+ * provably unsatisfiable with one layer each. Do not reintroduce it there.
2610
2611
  * Evidence: `.chrt/perf/2026-07-14-p8-d4-spike.md`.
2611
2612
  *
2612
- * **Load-bearing invariant.** The collapsed layer MUST keep the `-band{B}`
2613
- * id segment: `custom-style.ts` `bandFloor()` parses `/-band(\d)/` to clamp
2614
- * customer minzoom/scamin overrides to the band's data floor, on the live
2615
- * public Style API path. Renaming the id silently unclamps those overrides.
2616
- * Asserted in `band-collapse.test.ts`.
2613
+ * LABEL layers are different (CHRTsail #121 — MapLibre Native iOS silently
2614
+ * collapses its whole render pass above 482 symbol layers, and the served
2615
+ * style carried 575): `makeCollapsedCrossBandLabel` folds a label's ×6 band
2616
+ * fan into ONE layer per class, and `collapsedOrientationGate` folds the
2617
+ * orientation fan's per-dominator variants into its per-cell layers. That is
2618
+ * NOT the killed axis 2, because what the fill argument protects — paint
2619
+ * order as the sole band separator — does not apply to labels: their
2620
+ * load-bearing order is the cross-class P4 priority ladder (each class keeps
2621
+ * exactly one layer in its ladder slot), and within-class placement priority
2622
+ * rides the `rank*1000 + within-key` symbol-sort-key that labels.ts
2623
+ * documents as designed to survive precisely this merge.
2624
+ *
2625
+ * **Load-bearing invariant.** A collapsed per-band FEATURE layer MUST keep
2626
+ * the `-band{B}` id segment: `custom-style.ts` `bandFloor()` parses
2627
+ * `/-band(\d)/` to clamp customer minzoom/scamin overrides to the band's
2628
+ * data floor, on the live public Style API path. Renaming the id silently
2629
+ * unclamps those overrides. Cross-band LABEL layers are the deliberate
2630
+ * exception: they carry every band floor INSIDE the filter (tile-zoom
2631
+ * clauses a customer override cannot touch), which supersedes the clamp —
2632
+ * `bandFloor()` returning 0 for a bare label id is correct, not a gap.
2633
+ * Both halves asserted in `band-collapse.test.ts`.
2617
2634
  */
2618
2635
 
2619
2636
  /**
package/dist/index.js CHANGED
@@ -22224,7 +22224,7 @@ function getLabelConfig(layerName, opts = {}) {
22224
22224
  screenColors.deeper
22225
22225
  ] : screenColors.shoaler;
22226
22226
  const haloColor = screenColors.halo;
22227
- const fontStack = weight === "semibold" ? ["Open Sans Semibold", "Open Sans Regular"] : ["Open Sans Regular"];
22227
+ const fontStack = weight === "semibold" ? ["Open Sans Semibold"] : ["Open Sans Regular"];
22228
22228
  return {
22229
22229
  type: "symbol",
22230
22230
  // Property name varies by source path:
@@ -27282,6 +27282,34 @@ function makeCollapsedBandVariant(layer, B, isExempt) {
27282
27282
  delete collapsed.maxzoom;
27283
27283
  return [collapsed];
27284
27284
  }
27285
+ function collapsedOrientationGate(minzoom) {
27286
+ const clauses = [NOT_DOMINATED_FILTER];
27287
+ for (const D of BAND_DRAW_ORDER) {
27288
+ const dMaxzoom = dominatedVariantMaxzoom(D);
27289
+ if (dMaxzoom <= minzoom) continue;
27290
+ clauses.push(["all", dominatedByMinFilter(D), ["<", ["zoom"], dMaxzoom]]);
27291
+ }
27292
+ return ["any", ...clauses];
27293
+ }
27294
+ function makeCollapsedCrossBandLabel(label, isExempt) {
27295
+ const classMinzoom = label.minzoom ?? 0;
27296
+ const clauses = [];
27297
+ for (const B of BAND_DRAW_ORDER) {
27298
+ const floor = BAND_PRIORITY_RANGES[B].minzoom;
27299
+ const parts = [bandFilterExpression(B)];
27300
+ if (floor > 0) parts.push([">=", ["zoom"], floor]);
27301
+ if (!isExempt) {
27302
+ parts.push(collapsedDominanceGate(B, Math.max(classMinzoom, floor)));
27303
+ }
27304
+ clauses.push(parts.length === 1 ? parts[0] : ["all", ...parts]);
27305
+ }
27306
+ const collapsed = {
27307
+ ...label,
27308
+ filter: combineWithBandFilter(label.filter, ["any", ...clauses])
27309
+ };
27310
+ delete collapsed.maxzoom;
27311
+ return collapsed;
27312
+ }
27285
27313
 
27286
27314
  // src/atlas/style-generator.ts
27287
27315
  var META_LAYERS = /* @__PURE__ */ new Set([
@@ -27433,7 +27461,7 @@ var CELL_BAND_OPACITY_CURVE = {
27433
27461
  "5": [[11, 0.6], [13, 0.35], [18, 0.25]],
27434
27462
  "6": [[13, 0.75], [16, 0.55], [22, 0.45]]
27435
27463
  };
27436
- function fanOrientationLabelByCellBand(label) {
27464
+ function fanOrientationLabelByCellBand(label, bandFan) {
27437
27465
  const cellBandExpr = [
27438
27466
  "slice",
27439
27467
  ["coalesce", ["get", "_cell_id"], "??9???"],
@@ -27458,6 +27486,16 @@ function fanOrientationLabelByCellBand(label) {
27458
27486
  ...label.paint ?? {},
27459
27487
  "text-opacity": opacityExpr
27460
27488
  };
27489
+ if (bandFan === "collapsed") {
27490
+ variants.push({
27491
+ ...label,
27492
+ id: `${label.id}-cell${code}-notdom`,
27493
+ filter: combineWithBandFilter(cellFiltered, collapsedOrientationGate(minzoom)),
27494
+ minzoom,
27495
+ paint
27496
+ });
27497
+ continue;
27498
+ }
27461
27499
  variants.push({
27462
27500
  ...label,
27463
27501
  id: `${label.id}-cell${code}-notdom`,
@@ -27487,26 +27525,66 @@ function byLabelPriorityBottomUp(bundles) {
27487
27525
  (a, b) => getLabelPriority(b.objectClass) - getLabelPriority(a.objectClass)
27488
27526
  );
27489
27527
  }
27490
- function chartModeFilterForBand(layer, B, higherCascadeBands, lockedBounds, isRequestedBand) {
27491
- let filter = combineWithBandFilter(layer.filter, bandFilterExpression(B));
27528
+ function chartModeBandClauses(layer, B, higherCascadeBands, lockedBounds, isRequestedBand) {
27529
+ const parts = [bandFilterExpression(B)];
27492
27530
  if (!isDominanceExempt(layer)) {
27493
27531
  for (const D of higherCascadeBands) {
27494
- filter = combineWithBandFilter(filter, ["!", dominatedByMinFilter(D)]);
27532
+ parts.push(["!", dominatedByMinFilter(D)]);
27495
27533
  }
27496
27534
  }
27497
27535
  if (lockedBounds && isRequestedBand) {
27498
- filter = combineWithBandFilter(filter, withinBboxFilter(lockedBounds));
27536
+ parts.push(withinBboxFilter(lockedBounds));
27537
+ }
27538
+ return parts;
27539
+ }
27540
+ function chartModeFilterForBand(layer, B, higherCascadeBands, lockedBounds, isRequestedBand) {
27541
+ const [first, ...rest] = chartModeBandClauses(
27542
+ layer,
27543
+ B,
27544
+ higherCascadeBands,
27545
+ lockedBounds,
27546
+ isRequestedBand
27547
+ );
27548
+ let filter = combineWithBandFilter(layer.filter, first);
27549
+ for (const part of rest) {
27550
+ filter = combineWithBandFilter(filter, part);
27499
27551
  }
27500
27552
  return filter;
27501
27553
  }
27554
+ function makeCollapsedChartLabel(label, cascade, requestedSet, lockedBounds) {
27555
+ const clauses = cascade.map((B, i) => {
27556
+ const parts = chartModeBandClauses(
27557
+ label,
27558
+ B,
27559
+ cascade.slice(i + 1),
27560
+ lockedBounds,
27561
+ requestedSet.has(B)
27562
+ );
27563
+ return parts.length === 1 ? parts[0] : ["all", ...parts];
27564
+ });
27565
+ const collapsed = {
27566
+ ...label,
27567
+ filter: combineWithBandFilter(label.filter, ["any", ...clauses])
27568
+ };
27569
+ delete collapsed.maxzoom;
27570
+ return collapsed;
27571
+ }
27502
27572
  function generateStyle(palette = "DAY", opts = {}) {
27503
27573
  const sources = buildSources(opts);
27504
27574
  const sourceId = opts.region ?? DEFAULT_REGION;
27505
27575
  const orderedClasses = getOrderedClasses();
27506
- const layers = [];
27576
+ const backgroundLayer = {
27577
+ id: "chrt-background",
27578
+ type: "background",
27579
+ // A background layer takes no source; StyleLayer models sourced layers,
27580
+ // so the never-used source fields are absent via the unknown hop.
27581
+ paint: { "background-color": resolveColor("NODTA", palette) }
27582
+ };
27583
+ const layers = [backgroundLayer];
27507
27584
  const labelLayers = [];
27508
27585
  const chartCascade = opts.chartMode ? CHART_CASCADE_CODES[opts.chartMode.band] : null;
27509
27586
  const chartRequestedCodes = opts.chartMode ? CHART_BAND_CODES[opts.chartMode.band] : null;
27587
+ const bandFan = opts.bandFan ?? "collapsed";
27510
27588
  const lockedBounds = opts.chartMode?.lockedBounds;
27511
27589
  const bundles = [];
27512
27590
  for (const objectClass of orderedClasses) {
@@ -27560,6 +27638,12 @@ function generateStyle(palette = "DAY", opts = {}) {
27560
27638
  )
27561
27639
  );
27562
27640
  for (const { label } of chartLabelBundles) {
27641
+ if (bandFan === "collapsed") {
27642
+ labelLayers.push(
27643
+ makeCollapsedChartLabel(label, chartCascade, requestedSet, lockedBounds)
27644
+ );
27645
+ continue;
27646
+ }
27563
27647
  for (let i = 0; i < chartCascade.length; i++) {
27564
27648
  const B = chartCascade[i];
27565
27649
  const higher = chartCascade.slice(i + 1);
@@ -27582,7 +27666,6 @@ function generateStyle(palette = "DAY", opts = {}) {
27582
27666
  }
27583
27667
  layers.push(...labelLayers);
27584
27668
  } else {
27585
- const bandFan = opts.bandFan ?? "collapsed";
27586
27669
  const emitBand = (base, code) => bandFan === "legacy" ? makeBandVariants(base, code) : makeCollapsedBandVariant(base, code, isDominanceExempt(base));
27587
27670
  for (const code of BAND_DRAW_ORDER) {
27588
27671
  for (const { featureLayers } of regularBundles) {
@@ -27601,7 +27684,7 @@ function generateStyle(palette = "DAY", opts = {}) {
27601
27684
  for (const { objectClass, label } of bundles) {
27602
27685
  if (!label) continue;
27603
27686
  if (getLabelBehavior(objectClass) === "orientation-cross-band") {
27604
- labelLayers.push(...fanOrientationLabelByCellBand(label));
27687
+ labelLayers.push(...fanOrientationLabelByCellBand(label, bandFan));
27605
27688
  }
27606
27689
  }
27607
27690
  const featureLabelBundles = byLabelPriorityBottomUp(
@@ -27610,6 +27693,10 @@ function generateStyle(palette = "DAY", opts = {}) {
27610
27693
  )
27611
27694
  );
27612
27695
  for (const { label } of featureLabelBundles) {
27696
+ if (bandFan === "collapsed") {
27697
+ labelLayers.push(makeCollapsedCrossBandLabel(label, isDominanceExempt(label)));
27698
+ continue;
27699
+ }
27613
27700
  for (const code of BAND_DRAW_ORDER) {
27614
27701
  labelLayers.push(...emitBand(label, code));
27615
27702
  }
@@ -27617,6 +27704,7 @@ function generateStyle(palette = "DAY", opts = {}) {
27617
27704
  layers.push(...labelLayers);
27618
27705
  }
27619
27706
  const symbolSet = opts.symbolSet ?? "s52";
27707
+ const spriteBase = `${opts.tileBaseUrl ?? "https://tiles.chrt.co"}/v1/sprites`;
27620
27708
  const style = {
27621
27709
  version: 8,
27622
27710
  name: `CHRT ${SYMBOL_SET_LABELS[symbolSet]} ${PALETTE_NAMES[palette]}${opts.chartMode ? ` \u2014 chart:${opts.chartMode.band}` : ""}`,
@@ -27630,13 +27718,21 @@ function generateStyle(palette = "DAY", opts = {}) {
27630
27718
  "chrt:description": "IHO S-52 portrayal rules for Electronic Navigational Charts (ENCs). Generated from the CHRT atlas engine."
27631
27719
  },
27632
27720
  sources,
27633
- // TODO(Phase 9): the superadmin style-generator emits a reference JSON
27634
- // intended for external upload to Mapbox Studio. That flow is vestigial —
27635
- // we're decommissioning Mapbox MTS. When the page is repurposed to
27636
- // generate a MapLibre-native style for self-hosted consumers, wire the
27637
- // `sprite` field back in against a CHRT-hosted sprite route (see
27638
- // `.chrt/` follow-ups). Runtime maps load S-52 symbols via
27639
- // `loadS52SymbolImages()` — no sprite fetch happens at runtime.
27721
+ // A standard GL sprite, declared in the style — the mechanism every GL
27722
+ // renderer (JS and native) already implements. The web app additionally
27723
+ // registers atlas images at runtime via loadS52SymbolImages() (which wins
27724
+ // when both are present, so JS behavior is unchanged); native consumers
27725
+ // get their icons from this declaration alone. s52 sheets are per-palette
27726
+ // (the artwork carries S-52 colour tokens); chrt and raster are
27727
+ // fixed-palette, one sheet each. s101 deliberately declares NO sprite
27728
+ // until the vendored S-100 artwork clears licence review — see
27729
+ // assets/s101/PROVENANCE.md. CHRTsail #122.
27730
+ // Every non-s101 family is palette-scoped: chrt and raster keep fixed
27731
+ // palettes for their OWN glyphs, but both fall back to s52 artwork for
27732
+ // classes they don't remap — and that artwork carries colour tokens.
27733
+ ...symbolSet !== "s101" && {
27734
+ sprite: `${spriteBase}/${symbolSet}-${palette.toLowerCase()}`
27735
+ },
27640
27736
  glyphs: "https://fonts.openmaptiles.org/{fontstack}/{range}.pbf",
27641
27737
  layers
27642
27738
  };