@chrtco/sdk 0.10.23 → 0.10.25

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
@@ -4764,7 +4764,7 @@ var S52_DAY = {
4764
4764
  SNDG1: "#7d898c",
4765
4765
  SNDG2: "#070707",
4766
4766
  DEPSC: "#525a5c",
4767
- DEPCN: "#7d898c",
4767
+ DEPCN: "#555555",
4768
4768
  // Utility / infrastructure
4769
4769
  UINFD: "#070707",
4770
4770
  UINFF: "#7d898c",
@@ -4838,7 +4838,7 @@ var S52_DUSK = {
4838
4838
  SNDG1: "#606060",
4839
4839
  SNDG2: "#b0b0b0",
4840
4840
  DEPSC: "#606060",
4841
- DEPCN: "#606060",
4841
+ DEPCN: "#2E2E2E",
4842
4842
  UINFD: "#b0b0b0",
4843
4843
  UINFF: "#606060",
4844
4844
  UINFR: "#b05060",
@@ -4909,7 +4909,24 @@ var S52_NIGHT = {
4909
4909
  SNDG1: "#383028",
4910
4910
  SNDG2: "#c8aa78",
4911
4911
  DEPSC: "#383028",
4912
- DEPCN: "#383028",
4912
+ // ⚠ DELIBERATE DEVIATION from the published S-52 NIGHT value (#1f2223),
4913
+ // recorded here because `docs/validation/s52-spec-gap-matrix.json` tracks
4914
+ // this token as `value-differs` and the CHRTstyles programme is bringing
4915
+ // colours TO spec — this one moves further away, on purpose.
4916
+ //
4917
+ // Spec NIGHT DEPCN is near-black, which measures ~1.1:1 against this
4918
+ // palette's own deep water (#051a28). S-52's NIGHT table assumes a
4919
+ // dark-adapted eye in a darkened wheelhouse; on a phone or a laptop at
4920
+ // anchor it is not dim, it is absent — reported as "can't see bottom
4921
+ // contours in night mode only". A contour nobody can see fails the
4922
+ // standard's intent while matching its number.
4923
+ //
4924
+ // The palette already makes this trade elsewhere and in the same
4925
+ // direction: NIGHT's CHBLK/CSTLN/OUTLW are #c8aa78, light warm linework on
4926
+ // dark water. This value is that family, dimmed to stay subordinate to the
4927
+ // coastline. Revisit with the CHRTstyles programme, which owns the
4928
+ // spec-vs-legibility call across all 63 tokens.
4929
+ DEPCN: "#A68E84",
4913
4930
  UINFD: "#c8aa78",
4914
4931
  UINFF: "#383028",
4915
4932
  UINFR: "#682030",
@@ -5843,7 +5860,16 @@ var S52_LOOKUP = {
5843
5860
  category: "Depths",
5844
5861
  displayCategory: "STANDARD",
5845
5862
  int1Ref: "I30",
5846
- style: { type: "line", paint: { "line-color": "CHGRD", "line-width": 0.5 } },
5863
+ // S-52 names a dedicated depth-contour colour, and this used the shared
5864
+ // chart-grey instead. Reported 2026-09-02: "can't see bottom contours in
5865
+ // night mode only" — measured at 1.06:1 against the deepest NIGHT water,
5866
+ // which is invisible. DAY was 2.00:1 and DUSK 1.63:1 off the same token,
5867
+ // so the bug was worst where the palette is darkest.
5868
+ //
5869
+ // CHGRD is shared by dozens of linestyles (s52-linestyles-generated.ts),
5870
+ // so it could not be retuned for contours without moving all of them.
5871
+ // DEPCN exists in all three palettes and is the token S-52 specifies.
5872
+ style: { type: "line", paint: { "line-color": "DEPCN", "line-width": 0.5 } },
5847
5873
  viewingGroup: 13030,
5848
5874
  scamin: 45e3
5849
5875
  },
@@ -22653,13 +22679,23 @@ var HAS_OBJNAM = [
22653
22679
  ["has", "OBJNAM"],
22654
22680
  ["!=", ["coalesce", ["get", "OBJNAM"], ""], ""]
22655
22681
  ];
22682
+ var OBJNAM_TEXT = [
22683
+ "case",
22684
+ [
22685
+ "all",
22686
+ ["has", "NOBJNM"],
22687
+ [">=", ["index-of", "?", ["coalesce", ["get", "OBJNAM"], ""]], 0]
22688
+ ],
22689
+ ["get", "NOBJNM"],
22690
+ ["get", "OBJNAM"]
22691
+ ];
22656
22692
  function catalogueNamePairs(acronym) {
22657
22693
  const names = s57NameTable(acronym);
22658
22694
  return Object.keys(names).map(Number).sort((a, b) => a - b).map((code) => [String(code), names[code]]);
22659
22695
  }
22660
22696
  function buildLandmarkLabelExpression() {
22661
22697
  const categoryExpr = listAttrCase("CATLMK", catalogueNamePairs("CATLMK"), "");
22662
- const base = ["case", HAS_OBJNAM, ["get", "OBJNAM"], categoryExpr];
22698
+ const base = ["case", HAS_OBJNAM, OBJNAM_TEXT, categoryExpr];
22663
22699
  const conspic = s57AbbrTable("CONVIS")[1] ?? "";
22664
22700
  const conspicExpr = [
22665
22701
  "case",
@@ -22678,7 +22714,7 @@ function buildHarbourFacilityLabelExpression() {
22678
22714
  return [
22679
22715
  "case",
22680
22716
  HAS_OBJNAM,
22681
- ["get", "OBJNAM"],
22717
+ OBJNAM_TEXT,
22682
22718
  listAttrCase("CATHAF", catalogueNamePairs("CATHAF"), "")
22683
22719
  ];
22684
22720
  }
@@ -23235,31 +23271,37 @@ function extractNavAidDesignation(objnam) {
23235
23271
  return token;
23236
23272
  }
23237
23273
  function navAidDesignationTextExpression(objnam, colourPrefix) {
23238
- const len = ["length", objnam];
23274
+ const name = ["var", "navAidName"];
23275
+ const len = ["length", name];
23239
23276
  const tokenBranches = [];
23240
23277
  for (let k = 1; k <= 4; k++) {
23241
23278
  tokenBranches.push(
23242
23279
  [
23243
23280
  "all",
23244
23281
  [">", len, k],
23245
- ["==", ["slice", objnam, ["-", len, k + 1], ["-", len, k]], " "]
23282
+ ["==", ["slice", name, ["-", len, k + 1], ["-", len, k]], " "]
23246
23283
  ],
23247
- ["slice", objnam, ["-", len, k]]
23284
+ ["slice", name, ["-", len, k]]
23248
23285
  );
23249
23286
  }
23250
23287
  return [
23251
23288
  "let",
23252
- "desig",
23253
- ["case", ...tokenBranches, ""],
23289
+ "navAidName",
23290
+ objnam,
23254
23291
  [
23255
- "case",
23292
+ "let",
23293
+ "desig",
23294
+ ["case", ...tokenBranches, ""],
23256
23295
  [
23257
- "any",
23258
- ["==", ["var", "desig"], ""],
23259
- ["match", ["var", "desig"], [...NAV_DESIGNATION_STOPWORDS], true, false]
23260
- ],
23261
- ["concat", colourPrefix, objnam],
23262
- ["concat", colourPrefix, '"', ["var", "desig"], '"']
23296
+ "case",
23297
+ [
23298
+ "any",
23299
+ ["==", ["var", "desig"], ""],
23300
+ ["match", ["var", "desig"], [...NAV_DESIGNATION_STOPWORDS], true, false]
23301
+ ],
23302
+ ["concat", colourPrefix, name],
23303
+ ["concat", colourPrefix, '"', ["var", "desig"], '"']
23304
+ ]
23263
23305
  ]
23264
23306
  ];
23265
23307
  }
@@ -23366,7 +23408,7 @@ function getLabelConfig(layerName, opts = {}) {
23366
23408
  const tierExpr = buildLandRegionTierExpression();
23367
23409
  return {
23368
23410
  type: "symbol",
23369
- textField: ["get", "OBJNAM"],
23411
+ textField: OBJNAM_TEXT,
23370
23412
  // Tier-1 entry moved from z5 to z7: undersea-relief names without a
23371
23413
  // generic keyword ("Three Dory", "Outer Fall") can't be demoted by
23372
23414
  // pattern, so the overview/general bands stay clean by holding ALL
@@ -23435,7 +23477,7 @@ function getLabelConfig(layerName, opts = {}) {
23435
23477
  case "BOYLAT":
23436
23478
  return {
23437
23479
  type: "symbol",
23438
- textField: ["coalesce", ["get", "_navAidLabel"], ["get", "OBJNAM"]],
23480
+ textField: ["coalesce", ["get", "_navAidLabel"], OBJNAM_TEXT],
23439
23481
  textSize: NAV_AID_LABEL_TEXT_SIZE,
23440
23482
  textFont: NAV_AID_LABEL_FONT,
23441
23483
  textColor: DARK_BG,
@@ -23448,7 +23490,7 @@ function getLabelConfig(layerName, opts = {}) {
23448
23490
  case "BCNLAT":
23449
23491
  return {
23450
23492
  type: "symbol",
23451
- textField: ["coalesce", ["get", "_navAidLabel"], ["get", "OBJNAM"]],
23493
+ textField: ["coalesce", ["get", "_navAidLabel"], OBJNAM_TEXT],
23452
23494
  textSize: NAV_AID_LABEL_TEXT_SIZE,
23453
23495
  textFont: NAV_AID_LABEL_FONT,
23454
23496
  textColor: DARK_BG,
@@ -23462,7 +23504,7 @@ function getLabelConfig(layerName, opts = {}) {
23462
23504
  case "BCNCAR":
23463
23505
  return {
23464
23506
  type: "symbol",
23465
- textField: ["coalesce", ["get", "_navAidLabel"], ["get", "OBJNAM"]],
23507
+ textField: ["coalesce", ["get", "_navAidLabel"], OBJNAM_TEXT],
23466
23508
  textSize: NAV_AID_LABEL_TEXT_SIZE,
23467
23509
  textFont: NAV_AID_LABEL_FONT,
23468
23510
  textColor: DARK_BG,
@@ -23476,7 +23518,7 @@ function getLabelConfig(layerName, opts = {}) {
23476
23518
  case "BCNSPP":
23477
23519
  return {
23478
23520
  type: "symbol",
23479
- textField: ["coalesce", ["get", "_navAidLabel"], ["get", "OBJNAM"]],
23521
+ textField: ["coalesce", ["get", "_navAidLabel"], OBJNAM_TEXT],
23480
23522
  textSize: NAV_AID_LABEL_TEXT_SIZE,
23481
23523
  textFont: NAV_AID_LABEL_FONT,
23482
23524
  textColor: DARK_BG,
@@ -23490,7 +23532,7 @@ function getLabelConfig(layerName, opts = {}) {
23490
23532
  case "BOYSAW":
23491
23533
  return {
23492
23534
  type: "symbol",
23493
- textField: ["coalesce", ["get", "_navAidLabel"], ["get", "OBJNAM"]],
23535
+ textField: ["coalesce", ["get", "_navAidLabel"], OBJNAM_TEXT],
23494
23536
  textSize: NAV_AID_LABEL_TEXT_SIZE,
23495
23537
  textFont: NAV_AID_LABEL_FONT,
23496
23538
  textColor: DARK_BG,
@@ -23548,7 +23590,7 @@ function getLabelConfig(layerName, opts = {}) {
23548
23590
  const tierExpr = buildSeaAreaTierExpression();
23549
23591
  return {
23550
23592
  type: "symbol",
23551
- textField: ["get", "OBJNAM"],
23593
+ textField: OBJNAM_TEXT,
23552
23594
  // Default tier (CATSEA=0, uncategorised: canyons, seamounts, fishing
23553
23595
  // banks etc.) is suppressed at overview/general/coastal scales because
23554
23596
  // it floods the overview with hundreds of small italic labels that
@@ -23670,7 +23712,7 @@ function getLabelConfig(layerName, opts = {}) {
23670
23712
  const tierExpr = buildLakeTierExpression();
23671
23713
  return {
23672
23714
  type: "symbol",
23673
- textField: ["get", "OBJNAM"],
23715
+ textField: OBJNAM_TEXT,
23674
23716
  textSize: tieredSizeCurve(tierExpr, [
23675
23717
  [7, [9, 0]],
23676
23718
  [9, [11, 0]],
@@ -23691,7 +23733,7 @@ function getLabelConfig(layerName, opts = {}) {
23691
23733
  const tierExpr = buildRiverTierExpression();
23692
23734
  return {
23693
23735
  type: "symbol",
23694
- textField: ["get", "OBJNAM"],
23736
+ textField: OBJNAM_TEXT,
23695
23737
  textSize: tieredSizeCurve(tierExpr, [
23696
23738
  [7, [9, 0]],
23697
23739
  [9, [11, 0]],
@@ -23711,7 +23753,7 @@ function getLabelConfig(layerName, opts = {}) {
23711
23753
  case "CANALS":
23712
23754
  return {
23713
23755
  type: "symbol",
23714
- textField: ["get", "OBJNAM"],
23756
+ textField: OBJNAM_TEXT,
23715
23757
  textSize: [
23716
23758
  "interpolate",
23717
23759
  ["linear"],
@@ -23735,7 +23777,7 @@ function getLabelConfig(layerName, opts = {}) {
23735
23777
  const tierExpr = buildLandAreaTierExpression();
23736
23778
  return {
23737
23779
  type: "symbol",
23738
- textField: ["get", "OBJNAM"],
23780
+ textField: OBJNAM_TEXT,
23739
23781
  // Tier 2 is the SMALL island — the one the owner cannot identify.
23740
23782
  // Its old curve peaked at 10 px and fell to 10 px, i.e. it never got
23741
23783
  // its own scale: a name that only exists because you zoomed in was
@@ -23767,7 +23809,7 @@ function getLabelConfig(layerName, opts = {}) {
23767
23809
  const tierExpr = buildBuiltUpAreaTierExpression();
23768
23810
  return {
23769
23811
  type: "symbol",
23770
- textField: ["get", "OBJNAM"],
23812
+ textField: OBJNAM_TEXT,
23771
23813
  textSize: tieredSizeCurve(tierExpr, [
23772
23814
  [3, [11, 0, 0]],
23773
23815
  [5, [13, 0, 0]],
@@ -23791,7 +23833,7 @@ function getLabelConfig(layerName, opts = {}) {
23791
23833
  case "BRIDGE":
23792
23834
  return {
23793
23835
  type: "symbol",
23794
- textField: ["get", "OBJNAM"],
23836
+ textField: OBJNAM_TEXT,
23795
23837
  textSize: 9,
23796
23838
  textColor: "#bfa88a",
23797
23839
  textHaloColor: DARK_BG,
@@ -23810,7 +23852,7 @@ function getLabelConfig(layerName, opts = {}) {
23810
23852
  case "PILPNT":
23811
23853
  return {
23812
23854
  type: "symbol",
23813
- textField: ["get", "OBJNAM"],
23855
+ textField: OBJNAM_TEXT,
23814
23856
  textSize: 8,
23815
23857
  textColor: "#c8c0b0",
23816
23858
  textHaloColor: DARK_BG,
@@ -23821,7 +23863,7 @@ function getLabelConfig(layerName, opts = {}) {
23821
23863
  case "MORFAC":
23822
23864
  return {
23823
23865
  type: "symbol",
23824
- textField: ["get", "OBJNAM"],
23866
+ textField: OBJNAM_TEXT,
23825
23867
  textSize: 8,
23826
23868
  textColor: "#c8c0b0",
23827
23869
  textHaloColor: DARK_BG,
@@ -23832,7 +23874,7 @@ function getLabelConfig(layerName, opts = {}) {
23832
23874
  case "SLCONS":
23833
23875
  return {
23834
23876
  type: "symbol",
23835
- textField: ["get", "OBJNAM"],
23877
+ textField: OBJNAM_TEXT,
23836
23878
  textSize: 8,
23837
23879
  textColor: "#bfa88a",
23838
23880
  textHaloColor: DARK_BG,
@@ -23847,7 +23889,7 @@ function getLabelConfig(layerName, opts = {}) {
23847
23889
  case "BUISGL":
23848
23890
  return {
23849
23891
  type: "symbol",
23850
- textField: ["get", "OBJNAM"],
23892
+ textField: OBJNAM_TEXT,
23851
23893
  textSize: 8,
23852
23894
  textColor: DARK_BG,
23853
23895
  textHaloColor: "#c8c0b0",
@@ -23863,7 +23905,7 @@ function getLabelConfig(layerName, opts = {}) {
23863
23905
  const tierExpr = buildAirportTierExpression();
23864
23906
  return {
23865
23907
  type: "symbol",
23866
- textField: ["get", "OBJNAM"],
23908
+ textField: OBJNAM_TEXT,
23867
23909
  textSize: tieredSizeCurve(tierExpr, [
23868
23910
  [7, [9, 0]],
23869
23911
  [9, [11, 0]],
@@ -23947,7 +23989,7 @@ function getLabelConfig(layerName, opts = {}) {
23947
23989
  case "PRDARE":
23948
23990
  return {
23949
23991
  type: "symbol",
23950
- textField: ["get", "OBJNAM"],
23992
+ textField: OBJNAM_TEXT,
23951
23993
  textSize: [
23952
23994
  "interpolate",
23953
23995
  ["linear"],
@@ -27327,7 +27369,7 @@ function buildNavAidLabelExpression() {
27327
27369
  // doesn't precompute `_navAidLabel`, so before this expression every
27328
27370
  // production buoy label carried its full 30-40 glyph OBJNAM, the
27329
27371
  // biggest single driver of Harbor/Approach label overprint (E3).
27330
- navAidDesignationTextExpression(["get", "OBJNAM"], colourPrefix),
27372
+ navAidDesignationTextExpression(OBJNAM_TEXT, colourPrefix),
27331
27373
  ""
27332
27374
  ];
27333
27375
  }
@@ -27743,7 +27785,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
27743
27785
  "icon-allow-overlap": true,
27744
27786
  "icon-ignore-placement": false,
27745
27787
  "symbol-placement": "point",
27746
- "text-field": ["case", ["has", "OBJNAM"], ["get", "OBJNAM"], ""],
27788
+ "text-field": ["case", ["has", "OBJNAM"], OBJNAM_TEXT, ""],
27747
27789
  "text-size": ["interpolate", ["linear"], ["zoom"], 11, 9, 16, 11],
27748
27790
  "text-anchor": "top",
27749
27791
  "text-offset": [0, 1.4],
@@ -28460,6 +28502,18 @@ function buildLabelLayer2(objectClass, ct, sourceId = DEFAULT_REGION, depthUnits
28460
28502
  ["round", ["*", ["to-number", ["get", "VALDCO"]], 3.28084]],
28461
28503
  "ft"
28462
28504
  ];
28505
+ } else if (objectClass === "UWTROC" || objectClass === "OBSTRN" || objectClass === "WRECKS") {
28506
+ const valsouFt = [
28507
+ "to-number",
28508
+ ["coalesce", ["get", "VALSOU"], ["get", "valsou"], 0],
28509
+ 0
28510
+ ];
28511
+ textField = [
28512
+ "case",
28513
+ [">", valsouFt, 0],
28514
+ ["concat", ["to-string", ["round", ["*", valsouFt, 3.28084]]], "ft"],
28515
+ ""
28516
+ ];
28463
28517
  }
28464
28518
  }
28465
28519
  const labelPriority = getLabelPriority(objectClass);
@@ -29605,6 +29659,7 @@ var DOMINANCE_EXEMPT_CLASSES = /* @__PURE__ */ new Set([
29605
29659
  "RTPBCN",
29606
29660
  "RDOSTA"
29607
29661
  ]);
29662
+ var FINE_LAND_REPAINT_MIN_BAND = 4;
29608
29663
  function isDominanceExempt(layer) {
29609
29664
  for (const cls of DOMINANCE_EXEMPT_CLASSES) {
29610
29665
  if (layer.id.startsWith(`s52-${cls}-`)) return true;
@@ -30046,6 +30101,7 @@ function generateStyle(palette = "DAY", opts = {}) {
30046
30101
  }
30047
30102
  const symbolSet = opts.symbolSet ?? "s52";
30048
30103
  const spriteBase = `${opts.tileBaseUrl ?? "https://tiles.chrt.co"}/v1/sprites`;
30104
+ const glyphsBase = `${opts.tileBaseUrl ?? "https://tiles.chrt.co"}/v1/glyphs`;
30049
30105
  const isLndareFill = (l) => /^s52-LNDARE-fill-band\d+$/.test(l.id);
30050
30106
  const isWaterBaseFill = (l) => /^s52-(DEPARE|DRGARE|UNSARE)-fill-band\d+$/.test(l.id);
30051
30107
  const bandOf = (l) => {
@@ -30113,6 +30169,20 @@ function generateStyle(palette = "DAY", opts = {}) {
30113
30169
  repaint.push(domVariant);
30114
30170
  }
30115
30171
  }
30172
+ if (parseInt(b, 10) >= FINE_LAND_REPAINT_MIN_BAND) {
30173
+ for (const l of relocated.filter(
30174
+ (x) => bandOf(x) === b && isLndareFill(x)
30175
+ )) {
30176
+ const src = l;
30177
+ const landClone = {
30178
+ ...l,
30179
+ id: `${src.id}-overpair`,
30180
+ metadata: { ...src.metadata ?? {}, "chrt:role": "charted-land-repaint" }
30181
+ };
30182
+ delete landClone.maxzoom;
30183
+ repaint.push(landClone);
30184
+ }
30185
+ }
30116
30186
  }
30117
30187
  const isCoalneLine = (l) => /^s52-COALNE-line-band\d(-domBy\d)?$/.test(l.id);
30118
30188
  for (const l of layers.filter(isCoalneLine)) {
@@ -30187,7 +30257,23 @@ function generateStyle(palette = "DAY", opts = {}) {
30187
30257
  ...symbolSet !== "s101" && {
30188
30258
  sprite: `${spriteBase}/${symbolSet}-${palette.toLowerCase()}`
30189
30259
  },
30190
- glyphs: "https://fonts.openmaptiles.org/{fontstack}/{range}.pbf",
30260
+ // ⚠ DEPLOY ORDER. This URL is baked into the style by callers on BOTH
30261
+ // sides: the tile-worker's /v1/styles route AND the app's own
30262
+ // `generateStyle` call in ChartCanvas.tsx, which ships from Railway on a
30263
+ // SEPARATE deploy from the Workers. The glyph objects must be in R2 and
30264
+ // the /v1/glyphs route live BEFORE any client asks for this URL — if the
30265
+ // frontend ships first, every label on the chart goes blank, not just the
30266
+ // ones with an ʻokina. Land the tile-worker deploy first, or revert this
30267
+ // one line to the openmaptiles URL to fall back.
30268
+ //
30269
+ // Self-hosted, beside the sprite sheets on the same host. Was
30270
+ // fonts.openmaptiles.org, whose 2013-vintage Open Sans has NO GLYPH for
30271
+ // U+02BB ʻOKINA (a CONSONANT in Hawaiian) or five other codepoints that
30272
+ // occur in NOAA names — MapLibre draws a missing glyph as nothing, so
30273
+ // "Island of Hawaiʻi" rendered with a hole in it. Our ranges keep every
30274
+ // upstream glyph byte-identical and composite only those six in from
30275
+ // Noto Sans; see scripts/generate-glyphs.mjs.
30276
+ glyphs: `${glyphsBase}/{fontstack}/{range}.pbf`,
30191
30277
  layers: emitted
30192
30278
  };
30193
30279
  if (symbolSet === "s101") return applyS101SymbolSet(style);