@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/CHANGELOG.md +51 -0
- package/README.md +4 -0
- package/dist/index.cjs +112 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -10
- package/dist/index.d.ts +27 -10
- package/dist/index.js +112 -16
- package/dist/index.js.map +1 -1
- package/package.json +20 -3
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.**
|
|
2604
|
-
*
|
|
2605
|
-
*
|
|
2606
|
-
*
|
|
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
|
-
*
|
|
2613
|
-
*
|
|
2614
|
-
*
|
|
2615
|
-
*
|
|
2616
|
-
*
|
|
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.**
|
|
2604
|
-
*
|
|
2605
|
-
*
|
|
2606
|
-
*
|
|
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
|
-
*
|
|
2613
|
-
*
|
|
2614
|
-
*
|
|
2615
|
-
*
|
|
2616
|
-
*
|
|
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"
|
|
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
|
|
27491
|
-
|
|
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
|
-
|
|
27532
|
+
parts.push(["!", dominatedByMinFilter(D)]);
|
|
27495
27533
|
}
|
|
27496
27534
|
}
|
|
27497
27535
|
if (lockedBounds && isRequestedBand) {
|
|
27498
|
-
|
|
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
|
|
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
|
-
//
|
|
27634
|
-
//
|
|
27635
|
-
//
|
|
27636
|
-
//
|
|
27637
|
-
//
|
|
27638
|
-
//
|
|
27639
|
-
//
|
|
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
|
};
|