@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 CHANGED
@@ -3,6 +3,57 @@
3
3
  All notable changes to `@chrtco/sdk` will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
+
7
+ ### Added
8
+ - **Served styles now render in MapLibre Native as shipped.** Every generated
9
+ style begins with a `chrt-background` layer (S-52 NODTA backdrop per
10
+ palette) — previously native renderers drew a black void and
11
+ `didFinishLoading` never fired, because unlike a web app compositing over a
12
+ basemap, native draws exactly and only what the style declares.
13
+ - **`sprite` is declared in every served s52/chrt/raster style** —
14
+ `{tileBaseUrl}/v1/sprites/{family}-{theme}`, a standard GL sprite pair
15
+ (public, unauthenticated) generated from the same atlas registries the
16
+ style generator reads. GL JS behaviour is unchanged (runtime-registered
17
+ images win); native renderers get buoys, beacons, wrecks and patterns with
18
+ no client work. s101 styles deliberately declare **no** sprite until the
19
+ vendored S-100 artwork clears licence review.
20
+ - **Offline styles carry everything a chart needs**: `sprite` and `glyphs`
21
+ now point at `{protocol}://assets/…` inside the chart-assets pack served by
22
+ `GET /v1/offline/assets` (sprite sheets + Open Sans glyph PBFs). An offline
23
+ chart previously rendered mute geometry — no labels, no soundings figures,
24
+ no symbols.
25
+ - Style `metadata` carries `chrt:style-rev` beside `chrt:style-id` when a
26
+ custom style is bound — provenance that survives every cache and proxy.
27
+
28
+ ### Known limits
29
+ - MapLibre Native iOS silently collapses its entire render pass above ~482
30
+ symbol layers (measured externally; exact threshold). Served styles
31
+ currently ship 575 symbol layers, pinned by a ratchet test; reduction below
32
+ the cliff is in progress. Native consumers should avoid stacking many
33
+ additional symbol layers on top of the served style.
34
+
35
+ - **SOUNDG depth-number labels use a single-font stack (`["Open Sans
36
+ Semibold"]`), and every text-font in every served style is now pinned to
37
+ one vendored font.** The previous semibold stack carried a Regular
38
+ "fallback" — but MapLibre joins a multi-font stack into ONE glyph URL
39
+ (`Open Sans Semibold,Open Sans Regular/0-255.pbf`), which no static glyph
40
+ host serves. fonts.openmaptiles.org answers it with an HTML error page as
41
+ HTTP 200: GL JS swallowed the garbage and simply never drew those glyphs,
42
+ MapLibre **Native fatally parses it as protobuf** — the sounding depth
43
+ numbers were the casualty, on the one surface where they matter most. The
44
+ fallback therefore broke the primary; it is gone, and a served-style test
45
+ now rejects any multi-font stack. Caught by the new MapLibre Native
46
+ golden-image harness (`scripts/native-render/`) on its first run.
47
+ - **The offline chart-assets pack now vendors Open Sans Semibold** (0-255 /
48
+ 256-511, OFL, same origin as Regular/Italic). Offline charts previously
49
+ rendered SOUNDG depth numbers and all 60 nav-aid labels (beacons, fog
50
+ signals…) textless — the zip only carried fonts the style half-used.
51
+ - **`applyChrtSymbolSet` is passed the active palette at both serving
52
+ routes** (offline styles, and online custom-style composition). Omitting
53
+ it silently resolved the house cable/pipeline dash colours against DAY —
54
+ NIGHT/DUSK offline charts shipped day-bright dashes. Caught by the
55
+ harness's online≡offline twin gate.
56
+
6
57
  - **`/v1/cells` now sends flat `bbox_north` / `bbox_south` / `bbox_east` / `bbox_west`, matching `CellDetail`.** This is a **wire change to `/v1/cells` and `/v1/cells/{id}`**, not just a type change — read it before upgrading a REST client.
7
58
  - The bug: the SDK declared flat `bbox_*`, the endpoint returned a nested `bbox` object. `cell.bbox_north` typechecked and was `undefined` on **every** response — TypeScript vouching for a field that never arrived, which is worse than no type at all.
8
59
  - **The endpoint was the outlier, so the endpoint moved.** `enc_cells`' own columns, `/v1/coverage/route`'s `CoverageRouteCell`, and every published `CellDetail` are flat; `formatCell()` alone nested. One shape across the API is the point.
package/README.md CHANGED
@@ -210,6 +210,10 @@ const sources = buildSources({ region: 'NOAA_ENC' });
210
210
 
211
211
  ## Using CHRT without JavaScript
212
212
 
213
+ > **Prefer codegen?** The whole HTTP surface is described by an OpenAPI 3.1 spec at
214
+ > **https://api.chrt.co/openapi.json** — generate a typed Swift/Kotlin/Python/Go client
215
+ > from it instead of hand-rolling the calls below.
216
+
213
217
  Native iOS/Android, Swift, Kotlin, Python and plain `curl` clients cannot call `chrt.style()` — it is
214
218
  a JavaScript function that builds a style object in-process. **Use the served style API instead.**
215
219
  It is two HTTP calls, no SDK required.
package/dist/index.cjs CHANGED
@@ -22420,7 +22420,7 @@ function getLabelConfig(layerName, opts = {}) {
22420
22420
  screenColors.deeper
22421
22421
  ] : screenColors.shoaler;
22422
22422
  const haloColor = screenColors.halo;
22423
- const fontStack = weight === "semibold" ? ["Open Sans Semibold", "Open Sans Regular"] : ["Open Sans Regular"];
22423
+ const fontStack = weight === "semibold" ? ["Open Sans Semibold"] : ["Open Sans Regular"];
22424
22424
  return {
22425
22425
  type: "symbol",
22426
22426
  // Property name varies by source path:
@@ -27478,6 +27478,34 @@ function makeCollapsedBandVariant(layer, B, isExempt) {
27478
27478
  delete collapsed.maxzoom;
27479
27479
  return [collapsed];
27480
27480
  }
27481
+ function collapsedOrientationGate(minzoom) {
27482
+ const clauses = [NOT_DOMINATED_FILTER];
27483
+ for (const D of BAND_DRAW_ORDER) {
27484
+ const dMaxzoom = dominatedVariantMaxzoom(D);
27485
+ if (dMaxzoom <= minzoom) continue;
27486
+ clauses.push(["all", dominatedByMinFilter(D), ["<", ["zoom"], dMaxzoom]]);
27487
+ }
27488
+ return ["any", ...clauses];
27489
+ }
27490
+ function makeCollapsedCrossBandLabel(label, isExempt) {
27491
+ const classMinzoom = label.minzoom ?? 0;
27492
+ const clauses = [];
27493
+ for (const B of BAND_DRAW_ORDER) {
27494
+ const floor = BAND_PRIORITY_RANGES[B].minzoom;
27495
+ const parts = [bandFilterExpression(B)];
27496
+ if (floor > 0) parts.push([">=", ["zoom"], floor]);
27497
+ if (!isExempt) {
27498
+ parts.push(collapsedDominanceGate(B, Math.max(classMinzoom, floor)));
27499
+ }
27500
+ clauses.push(parts.length === 1 ? parts[0] : ["all", ...parts]);
27501
+ }
27502
+ const collapsed = {
27503
+ ...label,
27504
+ filter: combineWithBandFilter(label.filter, ["any", ...clauses])
27505
+ };
27506
+ delete collapsed.maxzoom;
27507
+ return collapsed;
27508
+ }
27481
27509
 
27482
27510
  // src/atlas/style-generator.ts
27483
27511
  var META_LAYERS = /* @__PURE__ */ new Set([
@@ -27629,7 +27657,7 @@ var CELL_BAND_OPACITY_CURVE = {
27629
27657
  "5": [[11, 0.6], [13, 0.35], [18, 0.25]],
27630
27658
  "6": [[13, 0.75], [16, 0.55], [22, 0.45]]
27631
27659
  };
27632
- function fanOrientationLabelByCellBand(label) {
27660
+ function fanOrientationLabelByCellBand(label, bandFan) {
27633
27661
  const cellBandExpr = [
27634
27662
  "slice",
27635
27663
  ["coalesce", ["get", "_cell_id"], "??9???"],
@@ -27654,6 +27682,16 @@ function fanOrientationLabelByCellBand(label) {
27654
27682
  ...label.paint ?? {},
27655
27683
  "text-opacity": opacityExpr
27656
27684
  };
27685
+ if (bandFan === "collapsed") {
27686
+ variants.push({
27687
+ ...label,
27688
+ id: `${label.id}-cell${code}-notdom`,
27689
+ filter: combineWithBandFilter(cellFiltered, collapsedOrientationGate(minzoom)),
27690
+ minzoom,
27691
+ paint
27692
+ });
27693
+ continue;
27694
+ }
27657
27695
  variants.push({
27658
27696
  ...label,
27659
27697
  id: `${label.id}-cell${code}-notdom`,
@@ -27683,26 +27721,66 @@ function byLabelPriorityBottomUp(bundles) {
27683
27721
  (a, b) => getLabelPriority(b.objectClass) - getLabelPriority(a.objectClass)
27684
27722
  );
27685
27723
  }
27686
- function chartModeFilterForBand(layer, B, higherCascadeBands, lockedBounds, isRequestedBand) {
27687
- let filter = combineWithBandFilter(layer.filter, bandFilterExpression(B));
27724
+ function chartModeBandClauses(layer, B, higherCascadeBands, lockedBounds, isRequestedBand) {
27725
+ const parts = [bandFilterExpression(B)];
27688
27726
  if (!isDominanceExempt(layer)) {
27689
27727
  for (const D of higherCascadeBands) {
27690
- filter = combineWithBandFilter(filter, ["!", dominatedByMinFilter(D)]);
27728
+ parts.push(["!", dominatedByMinFilter(D)]);
27691
27729
  }
27692
27730
  }
27693
27731
  if (lockedBounds && isRequestedBand) {
27694
- filter = combineWithBandFilter(filter, withinBboxFilter(lockedBounds));
27732
+ parts.push(withinBboxFilter(lockedBounds));
27733
+ }
27734
+ return parts;
27735
+ }
27736
+ function chartModeFilterForBand(layer, B, higherCascadeBands, lockedBounds, isRequestedBand) {
27737
+ const [first, ...rest] = chartModeBandClauses(
27738
+ layer,
27739
+ B,
27740
+ higherCascadeBands,
27741
+ lockedBounds,
27742
+ isRequestedBand
27743
+ );
27744
+ let filter = combineWithBandFilter(layer.filter, first);
27745
+ for (const part of rest) {
27746
+ filter = combineWithBandFilter(filter, part);
27695
27747
  }
27696
27748
  return filter;
27697
27749
  }
27750
+ function makeCollapsedChartLabel(label, cascade, requestedSet, lockedBounds) {
27751
+ const clauses = cascade.map((B, i) => {
27752
+ const parts = chartModeBandClauses(
27753
+ label,
27754
+ B,
27755
+ cascade.slice(i + 1),
27756
+ lockedBounds,
27757
+ requestedSet.has(B)
27758
+ );
27759
+ return parts.length === 1 ? parts[0] : ["all", ...parts];
27760
+ });
27761
+ const collapsed = {
27762
+ ...label,
27763
+ filter: combineWithBandFilter(label.filter, ["any", ...clauses])
27764
+ };
27765
+ delete collapsed.maxzoom;
27766
+ return collapsed;
27767
+ }
27698
27768
  function generateStyle(palette = "DAY", opts = {}) {
27699
27769
  const sources = buildSources(opts);
27700
27770
  const sourceId = opts.region ?? DEFAULT_REGION;
27701
27771
  const orderedClasses = getOrderedClasses();
27702
- const layers = [];
27772
+ const backgroundLayer = {
27773
+ id: "chrt-background",
27774
+ type: "background",
27775
+ // A background layer takes no source; StyleLayer models sourced layers,
27776
+ // so the never-used source fields are absent via the unknown hop.
27777
+ paint: { "background-color": resolveColor("NODTA", palette) }
27778
+ };
27779
+ const layers = [backgroundLayer];
27703
27780
  const labelLayers = [];
27704
27781
  const chartCascade = opts.chartMode ? CHART_CASCADE_CODES[opts.chartMode.band] : null;
27705
27782
  const chartRequestedCodes = opts.chartMode ? CHART_BAND_CODES[opts.chartMode.band] : null;
27783
+ const bandFan = opts.bandFan ?? "collapsed";
27706
27784
  const lockedBounds = opts.chartMode?.lockedBounds;
27707
27785
  const bundles = [];
27708
27786
  for (const objectClass of orderedClasses) {
@@ -27756,6 +27834,12 @@ function generateStyle(palette = "DAY", opts = {}) {
27756
27834
  )
27757
27835
  );
27758
27836
  for (const { label } of chartLabelBundles) {
27837
+ if (bandFan === "collapsed") {
27838
+ labelLayers.push(
27839
+ makeCollapsedChartLabel(label, chartCascade, requestedSet, lockedBounds)
27840
+ );
27841
+ continue;
27842
+ }
27759
27843
  for (let i = 0; i < chartCascade.length; i++) {
27760
27844
  const B = chartCascade[i];
27761
27845
  const higher = chartCascade.slice(i + 1);
@@ -27778,7 +27862,6 @@ function generateStyle(palette = "DAY", opts = {}) {
27778
27862
  }
27779
27863
  layers.push(...labelLayers);
27780
27864
  } else {
27781
- const bandFan = opts.bandFan ?? "collapsed";
27782
27865
  const emitBand = (base, code) => bandFan === "legacy" ? makeBandVariants(base, code) : makeCollapsedBandVariant(base, code, isDominanceExempt(base));
27783
27866
  for (const code of BAND_DRAW_ORDER) {
27784
27867
  for (const { featureLayers } of regularBundles) {
@@ -27797,7 +27880,7 @@ function generateStyle(palette = "DAY", opts = {}) {
27797
27880
  for (const { objectClass, label } of bundles) {
27798
27881
  if (!label) continue;
27799
27882
  if (getLabelBehavior(objectClass) === "orientation-cross-band") {
27800
- labelLayers.push(...fanOrientationLabelByCellBand(label));
27883
+ labelLayers.push(...fanOrientationLabelByCellBand(label, bandFan));
27801
27884
  }
27802
27885
  }
27803
27886
  const featureLabelBundles = byLabelPriorityBottomUp(
@@ -27806,6 +27889,10 @@ function generateStyle(palette = "DAY", opts = {}) {
27806
27889
  )
27807
27890
  );
27808
27891
  for (const { label } of featureLabelBundles) {
27892
+ if (bandFan === "collapsed") {
27893
+ labelLayers.push(makeCollapsedCrossBandLabel(label, isDominanceExempt(label)));
27894
+ continue;
27895
+ }
27809
27896
  for (const code of BAND_DRAW_ORDER) {
27810
27897
  labelLayers.push(...emitBand(label, code));
27811
27898
  }
@@ -27813,6 +27900,7 @@ function generateStyle(palette = "DAY", opts = {}) {
27813
27900
  layers.push(...labelLayers);
27814
27901
  }
27815
27902
  const symbolSet = opts.symbolSet ?? "s52";
27903
+ const spriteBase = `${opts.tileBaseUrl ?? "https://tiles.chrt.co"}/v1/sprites`;
27816
27904
  const style = {
27817
27905
  version: 8,
27818
27906
  name: `CHRT ${SYMBOL_SET_LABELS[symbolSet]} ${PALETTE_NAMES[palette]}${opts.chartMode ? ` \u2014 chart:${opts.chartMode.band}` : ""}`,
@@ -27826,13 +27914,21 @@ function generateStyle(palette = "DAY", opts = {}) {
27826
27914
  "chrt:description": "IHO S-52 portrayal rules for Electronic Navigational Charts (ENCs). Generated from the CHRT atlas engine."
27827
27915
  },
27828
27916
  sources,
27829
- // TODO(Phase 9): the superadmin style-generator emits a reference JSON
27830
- // intended for external upload to Mapbox Studio. That flow is vestigial —
27831
- // we're decommissioning Mapbox MTS. When the page is repurposed to
27832
- // generate a MapLibre-native style for self-hosted consumers, wire the
27833
- // `sprite` field back in against a CHRT-hosted sprite route (see
27834
- // `.chrt/` follow-ups). Runtime maps load S-52 symbols via
27835
- // `loadS52SymbolImages()` — no sprite fetch happens at runtime.
27917
+ // A standard GL sprite, declared in the style — the mechanism every GL
27918
+ // renderer (JS and native) already implements. The web app additionally
27919
+ // registers atlas images at runtime via loadS52SymbolImages() (which wins
27920
+ // when both are present, so JS behavior is unchanged); native consumers
27921
+ // get their icons from this declaration alone. s52 sheets are per-palette
27922
+ // (the artwork carries S-52 colour tokens); chrt and raster are
27923
+ // fixed-palette, one sheet each. s101 deliberately declares NO sprite
27924
+ // until the vendored S-100 artwork clears licence review — see
27925
+ // assets/s101/PROVENANCE.md. CHRTsail #122.
27926
+ // Every non-s101 family is palette-scoped: chrt and raster keep fixed
27927
+ // palettes for their OWN glyphs, but both fall back to s52 artwork for
27928
+ // classes they don't remap — and that artwork carries colour tokens.
27929
+ ...symbolSet !== "s101" && {
27930
+ sprite: `${spriteBase}/${symbolSet}-${palette.toLowerCase()}`
27931
+ },
27836
27932
  glyphs: "https://fonts.openmaptiles.org/{fontstack}/{range}.pbf",
27837
27933
  layers
27838
27934
  };