@chrtco/sdk 0.10.12 → 0.10.13

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
@@ -2531,7 +2531,9 @@ declare function getCollisionSettings(viewingGroup?: number): {
2531
2531
  /**
2532
2532
  * Get the resolved Mapbox GL style for a given S-57 layer.
2533
2533
  */
2534
- declare function resolveStyle(layerName: string, geomKind?: GeomType, ct?: ColorTableName): ResolvedStyle;
2534
+ declare function resolveStyle(layerName: string, geomKind?: GeomType, ct?: ColorTableName, opts?: {
2535
+ safetyContour?: number;
2536
+ }): ResolvedStyle;
2535
2537
  declare function getLookupEntry(layerName: string): LookupEntry;
2536
2538
 
2537
2539
  /**
@@ -2551,20 +2553,17 @@ declare function getLookupEntry(layerName: string): LookupEntry;
2551
2553
  * high-contrast shade (see `getLabelConfig` in labels.ts).
2552
2554
  * - §CSYM02 — isolated dangers shoaler than the contour get the
2553
2555
  * DNGCIR01 ring (see `buildLayersForClass` in layer-builder.ts).
2556
+ * - §DEPARE04 — the MS/MD shade boundary (owner decision 2026-08-21,
2557
+ * replacing a hardcoded 5 m middle cut; see the DEPARE block below
2558
+ * for the full IHO correspondence).
2554
2559
  *
2555
- * Both are overridden by the user's setting (sounding panel slider,
2560
+ * All are overridden by the user's setting (sounding panel slider,
2556
2561
  * `safetyContour` in the style options); this is only the fallback when
2557
2562
  * none is supplied. It matches `custom-style.ts`'s config default.
2558
- *
2559
- * NOTE: `depareFillExpression` below deliberately does NOT use this — its
2560
- * 2/5/18.2 m bands are the four-shade DEPARE ramp (see DEPARE_DEEP_CUT_M
2561
- * for why the deep cut is 18.2), a separate concept from the safety
2562
- * contour. Wiring those to the user's contour would change rendering for
2563
- * every existing user and needs its own decision.
2564
2563
  */
2565
2564
  declare const DEFAULT_SAFETY_CONTOUR_M = 10;
2566
- declare function depareColor(props: Record<string, unknown>, ct?: ColorTableName): string;
2567
- declare function depareFillExpression(ct?: ColorTableName): unknown[];
2565
+ declare function depareColor(props: Record<string, unknown>, ct?: ColorTableName, safetyContourM?: number): string;
2566
+ declare function depareFillExpression(ct?: ColorTableName, safetyContourM?: number): unknown[];
2568
2567
  declare function lightColor(props: Record<string, unknown>, ct?: ColorTableName): string;
2569
2568
  declare function lightFillExpression(ct?: ColorTableName): unknown[];
2570
2569
  declare function lateralColor(props: Record<string, unknown>, ct?: ColorTableName): string;
package/dist/index.d.ts CHANGED
@@ -2531,7 +2531,9 @@ declare function getCollisionSettings(viewingGroup?: number): {
2531
2531
  /**
2532
2532
  * Get the resolved Mapbox GL style for a given S-57 layer.
2533
2533
  */
2534
- declare function resolveStyle(layerName: string, geomKind?: GeomType, ct?: ColorTableName): ResolvedStyle;
2534
+ declare function resolveStyle(layerName: string, geomKind?: GeomType, ct?: ColorTableName, opts?: {
2535
+ safetyContour?: number;
2536
+ }): ResolvedStyle;
2535
2537
  declare function getLookupEntry(layerName: string): LookupEntry;
2536
2538
 
2537
2539
  /**
@@ -2551,20 +2553,17 @@ declare function getLookupEntry(layerName: string): LookupEntry;
2551
2553
  * high-contrast shade (see `getLabelConfig` in labels.ts).
2552
2554
  * - §CSYM02 — isolated dangers shoaler than the contour get the
2553
2555
  * DNGCIR01 ring (see `buildLayersForClass` in layer-builder.ts).
2556
+ * - §DEPARE04 — the MS/MD shade boundary (owner decision 2026-08-21,
2557
+ * replacing a hardcoded 5 m middle cut; see the DEPARE block below
2558
+ * for the full IHO correspondence).
2554
2559
  *
2555
- * Both are overridden by the user's setting (sounding panel slider,
2560
+ * All are overridden by the user's setting (sounding panel slider,
2556
2561
  * `safetyContour` in the style options); this is only the fallback when
2557
2562
  * none is supplied. It matches `custom-style.ts`'s config default.
2558
- *
2559
- * NOTE: `depareFillExpression` below deliberately does NOT use this — its
2560
- * 2/5/18.2 m bands are the four-shade DEPARE ramp (see DEPARE_DEEP_CUT_M
2561
- * for why the deep cut is 18.2), a separate concept from the safety
2562
- * contour. Wiring those to the user's contour would change rendering for
2563
- * every existing user and needs its own decision.
2564
2563
  */
2565
2564
  declare const DEFAULT_SAFETY_CONTOUR_M = 10;
2566
- declare function depareColor(props: Record<string, unknown>, ct?: ColorTableName): string;
2567
- declare function depareFillExpression(ct?: ColorTableName): unknown[];
2565
+ declare function depareColor(props: Record<string, unknown>, ct?: ColorTableName, safetyContourM?: number): string;
2566
+ declare function depareFillExpression(ct?: ColorTableName, safetyContourM?: number): unknown[];
2568
2567
  declare function lightColor(props: Record<string, unknown>, ct?: ColorTableName): string;
2569
2568
  declare function lightFillExpression(ct?: ColorTableName): unknown[];
2570
2569
  declare function lateralColor(props: Record<string, unknown>, ct?: ColorTableName): string;
package/dist/index.js CHANGED
@@ -5271,16 +5271,23 @@ function patternSpriteId(patternName) {
5271
5271
  // src/atlas/s52-conditional.ts
5272
5272
  var DEFAULT_SAFETY_CONTOUR_M = 10;
5273
5273
  var DEPARE_DEEP_CUT_M = 18.2;
5274
- function depareColor(props, ct = "DAY") {
5274
+ var DEPARE_SHALLOW_CUT_M = 2;
5275
+ function effectiveSafetyContour(safetyContourM) {
5276
+ if (!Number.isFinite(safetyContourM)) return DEFAULT_SAFETY_CONTOUR_M;
5277
+ return Math.min(Math.max(safetyContourM, DEPARE_SHALLOW_CUT_M), DEPARE_DEEP_CUT_M);
5278
+ }
5279
+ function depareColor(props, ct = "DAY", safetyContourM = DEFAULT_SAFETY_CONTOUR_M) {
5275
5280
  const drval1 = Number(props.DRVAL1 ?? props.drval1 ?? -1);
5276
5281
  const drval2 = Number(props.DRVAL2 ?? props.drval2 ?? drval1);
5282
+ const safety = effectiveSafetyContour(safetyContourM);
5277
5283
  if (drval1 < 0 && drval2 <= 0) return resolveColor("DEPIT", ct);
5278
- if (drval1 < 2) return resolveColor("DEPVS", ct);
5279
- if (drval1 < 5) return resolveColor("DEPMS", ct);
5284
+ if (drval1 < DEPARE_SHALLOW_CUT_M) return resolveColor("DEPVS", ct);
5285
+ if (drval1 < safety) return resolveColor("DEPMS", ct);
5280
5286
  if (drval1 < DEPARE_DEEP_CUT_M) return resolveColor("DEPMD", ct);
5281
5287
  return resolveColor("DEPDW", ct);
5282
5288
  }
5283
- function depareFillExpression(ct = "DAY") {
5289
+ function depareFillExpression(ct = "DAY", safetyContourM = DEFAULT_SAFETY_CONTOUR_M) {
5290
+ const safety = effectiveSafetyContour(safetyContourM);
5284
5291
  return [
5285
5292
  "case",
5286
5293
  [
@@ -5289,9 +5296,9 @@ function depareFillExpression(ct = "DAY") {
5289
5296
  ["<=", ["coalesce", ["get", "DRVAL2"], ["get", "drval2"], 0], 0]
5290
5297
  ],
5291
5298
  resolveColor("DEPIT", ct),
5292
- ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], 2],
5299
+ ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], DEPARE_SHALLOW_CUT_M],
5293
5300
  resolveColor("DEPVS", ct),
5294
- ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], 5],
5301
+ ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], safety],
5295
5302
  resolveColor("DEPMS", ct),
5296
5303
  ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], DEPARE_DEEP_CUT_M],
5297
5304
  resolveColor("DEPMD", ct),
@@ -22070,15 +22077,32 @@ function isVisibleInMode(displayCategory, mode) {
22070
22077
  if (mode === "STANDARD") return displayCategory === "BASE" || displayCategory === "STANDARD";
22071
22078
  return displayCategory === "BASE";
22072
22079
  }
22080
+ var SCAMIN_ZOOM_BUCKETS = [
22081
+ [12e3, 14],
22082
+ [22e3, 12],
22083
+ [45e3, 11],
22084
+ [9e4, 10],
22085
+ [35e4, 8],
22086
+ [15e5, 6]
22087
+ ];
22073
22088
  function scaminToMinZoom(scamin) {
22074
22089
  if (scamin === void 0 || scamin === null || scamin === 0) return void 0;
22075
- if (scamin >= 15e5) return 6;
22076
- if (scamin >= 35e4) return 8;
22077
- if (scamin >= 9e4) return 10;
22078
- if (scamin >= 45e3) return 11;
22079
- if (scamin >= 22e3) return 12;
22080
- if (scamin >= 12e3) return 14;
22081
- return void 0;
22090
+ let zoom;
22091
+ for (const [denom, z] of SCAMIN_ZOOM_BUCKETS) {
22092
+ if (scamin >= denom) zoom = z;
22093
+ }
22094
+ return zoom;
22095
+ }
22096
+ function scaminZoomGateFilter() {
22097
+ const step = [
22098
+ "step",
22099
+ ["to-number", ["coalesce", ["get", "SCAMIN"], ["get", "scamin"], 0]],
22100
+ 0
22101
+ ];
22102
+ for (const [denom, z] of SCAMIN_ZOOM_BUCKETS) {
22103
+ step.push(denom, z);
22104
+ }
22105
+ return [">=", ["zoom"], step];
22082
22106
  }
22083
22107
  var SAFETY_CRITICAL_GROUPS = /* @__PURE__ */ new Set([24020, 27010, 27070, 27080]);
22084
22108
  var OPAQUE_FILL_LAYERS = /* @__PURE__ */ new Set(["DEPARE", "LNDARE", "UNSARE", "LAKARE", "DRGARE", "SEAARE"]);
@@ -22112,7 +22136,7 @@ var ZOOM_SCALED_LINES = {
22112
22136
  BRIDGE: [8, 1, 14, 2],
22113
22137
  DAMCON: [8, 1, 14, 2]
22114
22138
  };
22115
- function resolvePaint(layerName, paint, geomType, ct = "DAY") {
22139
+ function resolvePaint(layerName, paint, geomType, ct = "DAY", opts = {}) {
22116
22140
  const resolved = {};
22117
22141
  for (const [key, value] of Object.entries(paint)) {
22118
22142
  if (typeof value === "string" && /^[A-Z]{4,5}[0-9]?$/.test(value)) {
@@ -22122,7 +22146,7 @@ function resolvePaint(layerName, paint, geomType, ct = "DAY") {
22122
22146
  }
22123
22147
  }
22124
22148
  if (layerName === "DEPARE" && geomType === "fill") {
22125
- resolved["fill-color"] = depareFillExpression(ct);
22149
+ resolved["fill-color"] = depareFillExpression(ct, opts.safetyContour);
22126
22150
  } else if (layerName === "LIGHTS" && geomType === "circle") {
22127
22151
  resolved["circle-color"] = lightFillExpression(ct);
22128
22152
  } else if ((layerName === "BOYLAT" || layerName === "BCNLAT") && geomType === "circle") {
@@ -22134,7 +22158,7 @@ function resolvePaint(layerName, paint, geomType, ct = "DAY") {
22134
22158
  }
22135
22159
  return resolved;
22136
22160
  }
22137
- function resolveStyle(layerName, geomKind, ct = "DAY") {
22161
+ function resolveStyle(layerName, geomKind, ct = "DAY", opts = {}) {
22138
22162
  const entry = S52_LOOKUP[layerName];
22139
22163
  if (!entry) {
22140
22164
  const type = geomKind ?? "circle";
@@ -22170,7 +22194,7 @@ function resolveStyle(layerName, geomKind, ct = "DAY") {
22170
22194
  const paint2 = effectiveType === "fill" ? { "fill-color": hexColor, "fill-opacity": 0.3 } : effectiveType === "line" ? { "line-color": hexColor, "line-width": 1 } : { "circle-color": hexColor, "circle-radius": 3, "circle-stroke-width": 0 };
22171
22195
  return { type: effectiveType, paint: paint2 };
22172
22196
  }
22173
- const paint = resolvePaint(layerName, { ...entry.style.paint }, effectiveType, ct);
22197
+ const paint = resolvePaint(layerName, { ...entry.style.paint }, effectiveType, ct, opts);
22174
22198
  if (effectiveType === "fill" && OPAQUE_FILL_LAYERS.has(layerName)) {
22175
22199
  paint["fill-opacity"] = 1;
22176
22200
  }
@@ -23119,21 +23143,19 @@ function getLabelConfig(layerName, opts = {}) {
23119
23143
  layout: orientationLabelLayout({
23120
23144
  "text-letter-spacing": 0.15,
23121
23145
  "text-max-width": 9,
23122
- // SEAARE is always-paint, unlike the other orientation classes.
23123
- // MapLibre places symbols topmost-layer-first, so with
23124
- // allow-overlap:false the dense sounding field (drawn above)
23125
- // collision-starves big-water names — observed on staging at z8:
23126
- // only 2 SEAARE labels survived viewport-wide, with Cape Cod Bay
23127
- // and Gulf of Maine dropped entirely. Always-paint is safe for
23128
- // this class because (a) the dominance composition in
23129
- // fanOrientationLabelByCellBand suppresses cross-band duplicate
23130
- // copies, and (b) tier sizing zeroes relief/anchorage names at
23131
- // the zooms where density is a problem. Rivers/lakes/land keep
23132
- // collision-dedup — their within-band multi-polygon stacking was
23133
- // the original complaint, and they lack big-water headline value.
23134
- "text-allow-overlap": true,
23135
- "text-ignore-placement": true,
23136
- "text-optional": false,
23146
+ // SEAARE was always-paint (allow-overlap + ignore-placement)
23147
+ // because collision starved big-water names at z8. That trade
23148
+ // inverted in the field on 2026-08-21: the always-paint copies —
23149
+ // per band, per adjacent cell, and one per TILE-CLIPPED piece of
23150
+ // a big polygon — stack into unreadable black smears (dozens of
23151
+ // "Chesapeake Bay" at z9, owner screenshot). With collision ON
23152
+ // the duplicates collide with EACH OTHER, so MapLibre keeps one
23153
+ // and drops the rest: a possibly-starved name beats an illegible
23154
+ // smear categorically. This is the interim state until the
23155
+ // PLACENAME layer (one anchor per named place, PR #1308) reaches
23156
+ // the tiles and replaces this fan outright — do not restore
23157
+ // always-paint on a starvation report; flip `labels: "placename"`
23158
+ // instead once the build carries the layer.
23137
23159
  "text-padding": 2
23138
23160
  })
23139
23161
  };
@@ -26740,6 +26762,13 @@ function buildSources(opts = {}) {
26740
26762
  }
26741
26763
 
26742
26764
  // src/atlas/layer-builder.ts
26765
+ var SCAMIN_DECLUTTER_CLASSES = /* @__PURE__ */ new Set([
26766
+ "OFSPLF",
26767
+ "PIPSOL",
26768
+ "PIPARE",
26769
+ "CBLSUB",
26770
+ "CBLARE"
26771
+ ]);
26743
26772
  var GEOM_FILTERS = {
26744
26773
  fill: ["any", ["==", ["geometry-type"], "Polygon"], ["==", ["geometry-type"], "MultiPolygon"]],
26745
26774
  line: ["any", ["==", ["geometry-type"], "LineString"], ["==", ["geometry-type"], "MultiLineString"]],
@@ -26898,7 +26927,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
26898
26927
  const layers = [];
26899
26928
  const geomTypes = getGeomTypesForClass(objectClass, entry?.style.type);
26900
26929
  for (const geomType of geomTypes) {
26901
- const resolved = resolveStyle(objectClass, geomType, ct);
26930
+ const resolved = resolveStyle(objectClass, geomType, ct, { safetyContour: opts.safetyContour });
26902
26931
  const layerId = `s52-${objectClass}-${resolved.type}`;
26903
26932
  const layer = {
26904
26933
  id: layerId,
@@ -26917,6 +26946,9 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
26917
26946
  } else if (GEOM_FILTERS[resolved.type]) {
26918
26947
  layer.filter = GEOM_FILTERS[resolved.type];
26919
26948
  }
26949
+ if (SCAMIN_DECLUTTER_CLASSES.has(objectClass)) {
26950
+ layer.filter = layer.filter ? ["all", layer.filter, scaminZoomGateFilter()] : ["all", scaminZoomGateFilter()];
26951
+ }
26920
26952
  if (resolved.minzoom) {
26921
26953
  layer.minzoom = resolved.minzoom;
26922
26954
  }
@@ -28212,6 +28244,18 @@ function isExemptOpaqueFill(layer) {
28212
28244
  }
28213
28245
  return false;
28214
28246
  }
28247
+ var FILL_FLOOR_FREE_BANDS = /* @__PURE__ */ new Set([
28248
+ "1",
28249
+ "2",
28250
+ "3"
28251
+ ]);
28252
+ function bandVariantMinzoom(layer, B) {
28253
+ const classMinzoom = layer.minzoom ?? 0;
28254
+ if (isExemptOpaqueFill(layer) && FILL_FLOOR_FREE_BANDS.has(B)) {
28255
+ return classMinzoom;
28256
+ }
28257
+ return Math.max(classMinzoom, BAND_PRIORITY_RANGES[B].minzoom);
28258
+ }
28215
28259
  var BAND_DRAW_ORDER = ["1", "2", "3", "4", "5", "6"];
28216
28260
  var CHART_BAND_CODES = {
28217
28261
  Overview: ["1"],
@@ -28330,9 +28374,7 @@ function collapsedDominanceGate(B, minzoom) {
28330
28374
  return ["any", ...clauses];
28331
28375
  }
28332
28376
  function makeCollapsedBandVariant(layer, B, isExempt) {
28333
- const range = BAND_PRIORITY_RANGES[B];
28334
- const classMinzoom = layer.minzoom ?? 0;
28335
- const minzoom = isExemptOpaqueFill(layer) ? classMinzoom : Math.max(classMinzoom, range.minzoom);
28377
+ const minzoom = bandVariantMinzoom(layer, B);
28336
28378
  if (isExempt) {
28337
28379
  const exempt = {
28338
28380
  ...layer,
@@ -28471,8 +28513,7 @@ function isDominanceExempt(layer) {
28471
28513
  var TOP_STACK_LIGHT_CLASSES = /* @__PURE__ */ new Set(["LIGHTS"]);
28472
28514
  function makeBandVariants(layer, B) {
28473
28515
  const range = BAND_PRIORITY_RANGES[B];
28474
- const classMinzoom = layer.minzoom ?? 0;
28475
- const minzoom = isExemptOpaqueFill(layer) ? classMinzoom : Math.max(classMinzoom, range.minzoom);
28516
+ const minzoom = bandVariantMinzoom(layer, B);
28476
28517
  const variants = [];
28477
28518
  if (isDominanceExempt(layer)) {
28478
28519
  const exempt = {
@@ -28780,6 +28821,12 @@ function generateStyle(palette = "DAY", opts = {}) {
28780
28821
  }
28781
28822
  const symbolSet = opts.symbolSet ?? "s52";
28782
28823
  const spriteBase = `${opts.tileBaseUrl ?? "https://tiles.chrt.co"}/v1/sprites`;
28824
+ const isLndareFill = (l) => /^s52-LNDARE-fill-band\d+$/.test(l.id);
28825
+ const emitted = opts.land ? (() => {
28826
+ const fills = layers.filter(isLndareFill);
28827
+ const rest = layers.filter((l) => !isLndareFill(l));
28828
+ return [rest[0], ...fills, ...rest.slice(1)];
28829
+ })() : layers;
28783
28830
  const style = {
28784
28831
  version: 8,
28785
28832
  name: `CHRT ${SYMBOL_SET_LABELS[symbolSet]} ${PALETTE_NAMES[palette]}${opts.chartMode ? ` \u2014 chart:${opts.chartMode.band}` : ""}`,
@@ -28788,7 +28835,7 @@ function generateStyle(palette = "DAY", opts = {}) {
28788
28835
  "chrt:symbol-set": symbolSet,
28789
28836
  "chrt:color-table": palette,
28790
28837
  "chrt:generated-at": (/* @__PURE__ */ new Date()).toISOString(),
28791
- "chrt:layer-count": layers.length,
28838
+ "chrt:layer-count": emitted.length,
28792
28839
  "chrt:land": Boolean(opts.land),
28793
28840
  "chrt:relief": Boolean(opts.relief),
28794
28841
  "chrt:render-mode": opts.chartMode ? `chart:${opts.chartMode.band}` : "continuous",
@@ -28811,7 +28858,7 @@ function generateStyle(palette = "DAY", opts = {}) {
28811
28858
  sprite: `${spriteBase}/${symbolSet}-${palette.toLowerCase()}`
28812
28859
  },
28813
28860
  glyphs: "https://fonts.openmaptiles.org/{fontstack}/{range}.pbf",
28814
- layers
28861
+ layers: emitted
28815
28862
  };
28816
28863
  if (symbolSet === "s101") return applyS101SymbolSet(style);
28817
28864
  if (symbolSet === "chrt") return applyChrtSymbolSet(style, palette);