@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.cjs CHANGED
@@ -5488,16 +5488,23 @@ function patternSpriteId(patternName) {
5488
5488
  // src/atlas/s52-conditional.ts
5489
5489
  var DEFAULT_SAFETY_CONTOUR_M = 10;
5490
5490
  var DEPARE_DEEP_CUT_M = 18.2;
5491
- function depareColor(props, ct = "DAY") {
5491
+ var DEPARE_SHALLOW_CUT_M = 2;
5492
+ function effectiveSafetyContour(safetyContourM) {
5493
+ if (!Number.isFinite(safetyContourM)) return DEFAULT_SAFETY_CONTOUR_M;
5494
+ return Math.min(Math.max(safetyContourM, DEPARE_SHALLOW_CUT_M), DEPARE_DEEP_CUT_M);
5495
+ }
5496
+ function depareColor(props, ct = "DAY", safetyContourM = DEFAULT_SAFETY_CONTOUR_M) {
5492
5497
  const drval1 = Number(props.DRVAL1 ?? props.drval1 ?? -1);
5493
5498
  const drval2 = Number(props.DRVAL2 ?? props.drval2 ?? drval1);
5499
+ const safety = effectiveSafetyContour(safetyContourM);
5494
5500
  if (drval1 < 0 && drval2 <= 0) return resolveColor("DEPIT", ct);
5495
- if (drval1 < 2) return resolveColor("DEPVS", ct);
5496
- if (drval1 < 5) return resolveColor("DEPMS", ct);
5501
+ if (drval1 < DEPARE_SHALLOW_CUT_M) return resolveColor("DEPVS", ct);
5502
+ if (drval1 < safety) return resolveColor("DEPMS", ct);
5497
5503
  if (drval1 < DEPARE_DEEP_CUT_M) return resolveColor("DEPMD", ct);
5498
5504
  return resolveColor("DEPDW", ct);
5499
5505
  }
5500
- function depareFillExpression(ct = "DAY") {
5506
+ function depareFillExpression(ct = "DAY", safetyContourM = DEFAULT_SAFETY_CONTOUR_M) {
5507
+ const safety = effectiveSafetyContour(safetyContourM);
5501
5508
  return [
5502
5509
  "case",
5503
5510
  [
@@ -5506,9 +5513,9 @@ function depareFillExpression(ct = "DAY") {
5506
5513
  ["<=", ["coalesce", ["get", "DRVAL2"], ["get", "drval2"], 0], 0]
5507
5514
  ],
5508
5515
  resolveColor("DEPIT", ct),
5509
- ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], 2],
5516
+ ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], DEPARE_SHALLOW_CUT_M],
5510
5517
  resolveColor("DEPVS", ct),
5511
- ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], 5],
5518
+ ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], safety],
5512
5519
  resolveColor("DEPMS", ct),
5513
5520
  ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], DEPARE_DEEP_CUT_M],
5514
5521
  resolveColor("DEPMD", ct),
@@ -22287,15 +22294,32 @@ function isVisibleInMode(displayCategory, mode) {
22287
22294
  if (mode === "STANDARD") return displayCategory === "BASE" || displayCategory === "STANDARD";
22288
22295
  return displayCategory === "BASE";
22289
22296
  }
22297
+ var SCAMIN_ZOOM_BUCKETS = [
22298
+ [12e3, 14],
22299
+ [22e3, 12],
22300
+ [45e3, 11],
22301
+ [9e4, 10],
22302
+ [35e4, 8],
22303
+ [15e5, 6]
22304
+ ];
22290
22305
  function scaminToMinZoom(scamin) {
22291
22306
  if (scamin === void 0 || scamin === null || scamin === 0) return void 0;
22292
- if (scamin >= 15e5) return 6;
22293
- if (scamin >= 35e4) return 8;
22294
- if (scamin >= 9e4) return 10;
22295
- if (scamin >= 45e3) return 11;
22296
- if (scamin >= 22e3) return 12;
22297
- if (scamin >= 12e3) return 14;
22298
- return void 0;
22307
+ let zoom;
22308
+ for (const [denom, z] of SCAMIN_ZOOM_BUCKETS) {
22309
+ if (scamin >= denom) zoom = z;
22310
+ }
22311
+ return zoom;
22312
+ }
22313
+ function scaminZoomGateFilter() {
22314
+ const step = [
22315
+ "step",
22316
+ ["to-number", ["coalesce", ["get", "SCAMIN"], ["get", "scamin"], 0]],
22317
+ 0
22318
+ ];
22319
+ for (const [denom, z] of SCAMIN_ZOOM_BUCKETS) {
22320
+ step.push(denom, z);
22321
+ }
22322
+ return [">=", ["zoom"], step];
22299
22323
  }
22300
22324
  var SAFETY_CRITICAL_GROUPS = /* @__PURE__ */ new Set([24020, 27010, 27070, 27080]);
22301
22325
  var OPAQUE_FILL_LAYERS = /* @__PURE__ */ new Set(["DEPARE", "LNDARE", "UNSARE", "LAKARE", "DRGARE", "SEAARE"]);
@@ -22329,7 +22353,7 @@ var ZOOM_SCALED_LINES = {
22329
22353
  BRIDGE: [8, 1, 14, 2],
22330
22354
  DAMCON: [8, 1, 14, 2]
22331
22355
  };
22332
- function resolvePaint(layerName, paint, geomType, ct = "DAY") {
22356
+ function resolvePaint(layerName, paint, geomType, ct = "DAY", opts = {}) {
22333
22357
  const resolved = {};
22334
22358
  for (const [key, value] of Object.entries(paint)) {
22335
22359
  if (typeof value === "string" && /^[A-Z]{4,5}[0-9]?$/.test(value)) {
@@ -22339,7 +22363,7 @@ function resolvePaint(layerName, paint, geomType, ct = "DAY") {
22339
22363
  }
22340
22364
  }
22341
22365
  if (layerName === "DEPARE" && geomType === "fill") {
22342
- resolved["fill-color"] = depareFillExpression(ct);
22366
+ resolved["fill-color"] = depareFillExpression(ct, opts.safetyContour);
22343
22367
  } else if (layerName === "LIGHTS" && geomType === "circle") {
22344
22368
  resolved["circle-color"] = lightFillExpression(ct);
22345
22369
  } else if ((layerName === "BOYLAT" || layerName === "BCNLAT") && geomType === "circle") {
@@ -22351,7 +22375,7 @@ function resolvePaint(layerName, paint, geomType, ct = "DAY") {
22351
22375
  }
22352
22376
  return resolved;
22353
22377
  }
22354
- function resolveStyle(layerName, geomKind, ct = "DAY") {
22378
+ function resolveStyle(layerName, geomKind, ct = "DAY", opts = {}) {
22355
22379
  const entry = S52_LOOKUP[layerName];
22356
22380
  if (!entry) {
22357
22381
  const type = geomKind ?? "circle";
@@ -22387,7 +22411,7 @@ function resolveStyle(layerName, geomKind, ct = "DAY") {
22387
22411
  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 };
22388
22412
  return { type: effectiveType, paint: paint2 };
22389
22413
  }
22390
- const paint = resolvePaint(layerName, { ...entry.style.paint }, effectiveType, ct);
22414
+ const paint = resolvePaint(layerName, { ...entry.style.paint }, effectiveType, ct, opts);
22391
22415
  if (effectiveType === "fill" && OPAQUE_FILL_LAYERS.has(layerName)) {
22392
22416
  paint["fill-opacity"] = 1;
22393
22417
  }
@@ -23336,21 +23360,19 @@ function getLabelConfig(layerName, opts = {}) {
23336
23360
  layout: orientationLabelLayout({
23337
23361
  "text-letter-spacing": 0.15,
23338
23362
  "text-max-width": 9,
23339
- // SEAARE is always-paint, unlike the other orientation classes.
23340
- // MapLibre places symbols topmost-layer-first, so with
23341
- // allow-overlap:false the dense sounding field (drawn above)
23342
- // collision-starves big-water names — observed on staging at z8:
23343
- // only 2 SEAARE labels survived viewport-wide, with Cape Cod Bay
23344
- // and Gulf of Maine dropped entirely. Always-paint is safe for
23345
- // this class because (a) the dominance composition in
23346
- // fanOrientationLabelByCellBand suppresses cross-band duplicate
23347
- // copies, and (b) tier sizing zeroes relief/anchorage names at
23348
- // the zooms where density is a problem. Rivers/lakes/land keep
23349
- // collision-dedup — their within-band multi-polygon stacking was
23350
- // the original complaint, and they lack big-water headline value.
23351
- "text-allow-overlap": true,
23352
- "text-ignore-placement": true,
23353
- "text-optional": false,
23363
+ // SEAARE was always-paint (allow-overlap + ignore-placement)
23364
+ // because collision starved big-water names at z8. That trade
23365
+ // inverted in the field on 2026-08-21: the always-paint copies —
23366
+ // per band, per adjacent cell, and one per TILE-CLIPPED piece of
23367
+ // a big polygon — stack into unreadable black smears (dozens of
23368
+ // "Chesapeake Bay" at z9, owner screenshot). With collision ON
23369
+ // the duplicates collide with EACH OTHER, so MapLibre keeps one
23370
+ // and drops the rest: a possibly-starved name beats an illegible
23371
+ // smear categorically. This is the interim state until the
23372
+ // PLACENAME layer (one anchor per named place, PR #1308) reaches
23373
+ // the tiles and replaces this fan outright — do not restore
23374
+ // always-paint on a starvation report; flip `labels: "placename"`
23375
+ // instead once the build carries the layer.
23354
23376
  "text-padding": 2
23355
23377
  })
23356
23378
  };
@@ -26957,6 +26979,13 @@ function buildSources(opts = {}) {
26957
26979
  }
26958
26980
 
26959
26981
  // src/atlas/layer-builder.ts
26982
+ var SCAMIN_DECLUTTER_CLASSES = /* @__PURE__ */ new Set([
26983
+ "OFSPLF",
26984
+ "PIPSOL",
26985
+ "PIPARE",
26986
+ "CBLSUB",
26987
+ "CBLARE"
26988
+ ]);
26960
26989
  var GEOM_FILTERS = {
26961
26990
  fill: ["any", ["==", ["geometry-type"], "Polygon"], ["==", ["geometry-type"], "MultiPolygon"]],
26962
26991
  line: ["any", ["==", ["geometry-type"], "LineString"], ["==", ["geometry-type"], "MultiLineString"]],
@@ -27115,7 +27144,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
27115
27144
  const layers = [];
27116
27145
  const geomTypes = getGeomTypesForClass(objectClass, entry?.style.type);
27117
27146
  for (const geomType of geomTypes) {
27118
- const resolved = resolveStyle(objectClass, geomType, ct);
27147
+ const resolved = resolveStyle(objectClass, geomType, ct, { safetyContour: opts.safetyContour });
27119
27148
  const layerId = `s52-${objectClass}-${resolved.type}`;
27120
27149
  const layer = {
27121
27150
  id: layerId,
@@ -27134,6 +27163,9 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
27134
27163
  } else if (GEOM_FILTERS[resolved.type]) {
27135
27164
  layer.filter = GEOM_FILTERS[resolved.type];
27136
27165
  }
27166
+ if (SCAMIN_DECLUTTER_CLASSES.has(objectClass)) {
27167
+ layer.filter = layer.filter ? ["all", layer.filter, scaminZoomGateFilter()] : ["all", scaminZoomGateFilter()];
27168
+ }
27137
27169
  if (resolved.minzoom) {
27138
27170
  layer.minzoom = resolved.minzoom;
27139
27171
  }
@@ -28429,6 +28461,18 @@ function isExemptOpaqueFill(layer) {
28429
28461
  }
28430
28462
  return false;
28431
28463
  }
28464
+ var FILL_FLOOR_FREE_BANDS = /* @__PURE__ */ new Set([
28465
+ "1",
28466
+ "2",
28467
+ "3"
28468
+ ]);
28469
+ function bandVariantMinzoom(layer, B) {
28470
+ const classMinzoom = layer.minzoom ?? 0;
28471
+ if (isExemptOpaqueFill(layer) && FILL_FLOOR_FREE_BANDS.has(B)) {
28472
+ return classMinzoom;
28473
+ }
28474
+ return Math.max(classMinzoom, BAND_PRIORITY_RANGES[B].minzoom);
28475
+ }
28432
28476
  var BAND_DRAW_ORDER = ["1", "2", "3", "4", "5", "6"];
28433
28477
  var CHART_BAND_CODES = {
28434
28478
  Overview: ["1"],
@@ -28547,9 +28591,7 @@ function collapsedDominanceGate(B, minzoom) {
28547
28591
  return ["any", ...clauses];
28548
28592
  }
28549
28593
  function makeCollapsedBandVariant(layer, B, isExempt) {
28550
- const range = BAND_PRIORITY_RANGES[B];
28551
- const classMinzoom = layer.minzoom ?? 0;
28552
- const minzoom = isExemptOpaqueFill(layer) ? classMinzoom : Math.max(classMinzoom, range.minzoom);
28594
+ const minzoom = bandVariantMinzoom(layer, B);
28553
28595
  if (isExempt) {
28554
28596
  const exempt = {
28555
28597
  ...layer,
@@ -28688,8 +28730,7 @@ function isDominanceExempt(layer) {
28688
28730
  var TOP_STACK_LIGHT_CLASSES = /* @__PURE__ */ new Set(["LIGHTS"]);
28689
28731
  function makeBandVariants(layer, B) {
28690
28732
  const range = BAND_PRIORITY_RANGES[B];
28691
- const classMinzoom = layer.minzoom ?? 0;
28692
- const minzoom = isExemptOpaqueFill(layer) ? classMinzoom : Math.max(classMinzoom, range.minzoom);
28733
+ const minzoom = bandVariantMinzoom(layer, B);
28693
28734
  const variants = [];
28694
28735
  if (isDominanceExempt(layer)) {
28695
28736
  const exempt = {
@@ -28997,6 +29038,12 @@ function generateStyle(palette = "DAY", opts = {}) {
28997
29038
  }
28998
29039
  const symbolSet = opts.symbolSet ?? "s52";
28999
29040
  const spriteBase = `${opts.tileBaseUrl ?? "https://tiles.chrt.co"}/v1/sprites`;
29041
+ const isLndareFill = (l) => /^s52-LNDARE-fill-band\d+$/.test(l.id);
29042
+ const emitted = opts.land ? (() => {
29043
+ const fills = layers.filter(isLndareFill);
29044
+ const rest = layers.filter((l) => !isLndareFill(l));
29045
+ return [rest[0], ...fills, ...rest.slice(1)];
29046
+ })() : layers;
29000
29047
  const style = {
29001
29048
  version: 8,
29002
29049
  name: `CHRT ${SYMBOL_SET_LABELS[symbolSet]} ${PALETTE_NAMES[palette]}${opts.chartMode ? ` \u2014 chart:${opts.chartMode.band}` : ""}`,
@@ -29005,7 +29052,7 @@ function generateStyle(palette = "DAY", opts = {}) {
29005
29052
  "chrt:symbol-set": symbolSet,
29006
29053
  "chrt:color-table": palette,
29007
29054
  "chrt:generated-at": (/* @__PURE__ */ new Date()).toISOString(),
29008
- "chrt:layer-count": layers.length,
29055
+ "chrt:layer-count": emitted.length,
29009
29056
  "chrt:land": Boolean(opts.land),
29010
29057
  "chrt:relief": Boolean(opts.relief),
29011
29058
  "chrt:render-mode": opts.chartMode ? `chart:${opts.chartMode.band}` : "continuous",
@@ -29028,7 +29075,7 @@ function generateStyle(palette = "DAY", opts = {}) {
29028
29075
  sprite: `${spriteBase}/${symbolSet}-${palette.toLowerCase()}`
29029
29076
  },
29030
29077
  glyphs: "https://fonts.openmaptiles.org/{fontstack}/{range}.pbf",
29031
- layers
29078
+ layers: emitted
29032
29079
  };
29033
29080
  if (symbolSet === "s101") return applyS101SymbolSet(style);
29034
29081
  if (symbolSet === "chrt") return applyChrtSymbolSet(style, palette);