@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.js CHANGED
@@ -4542,7 +4542,7 @@ var S52_DAY = {
4542
4542
  SNDG1: "#7d898c",
4543
4543
  SNDG2: "#070707",
4544
4544
  DEPSC: "#525a5c",
4545
- DEPCN: "#7d898c",
4545
+ DEPCN: "#555555",
4546
4546
  // Utility / infrastructure
4547
4547
  UINFD: "#070707",
4548
4548
  UINFF: "#7d898c",
@@ -4616,7 +4616,7 @@ var S52_DUSK = {
4616
4616
  SNDG1: "#606060",
4617
4617
  SNDG2: "#b0b0b0",
4618
4618
  DEPSC: "#606060",
4619
- DEPCN: "#606060",
4619
+ DEPCN: "#2E2E2E",
4620
4620
  UINFD: "#b0b0b0",
4621
4621
  UINFF: "#606060",
4622
4622
  UINFR: "#b05060",
@@ -4687,7 +4687,24 @@ var S52_NIGHT = {
4687
4687
  SNDG1: "#383028",
4688
4688
  SNDG2: "#c8aa78",
4689
4689
  DEPSC: "#383028",
4690
- DEPCN: "#383028",
4690
+ // ⚠ DELIBERATE DEVIATION from the published S-52 NIGHT value (#1f2223),
4691
+ // recorded here because `docs/validation/s52-spec-gap-matrix.json` tracks
4692
+ // this token as `value-differs` and the CHRTstyles programme is bringing
4693
+ // colours TO spec — this one moves further away, on purpose.
4694
+ //
4695
+ // Spec NIGHT DEPCN is near-black, which measures ~1.1:1 against this
4696
+ // palette's own deep water (#051a28). S-52's NIGHT table assumes a
4697
+ // dark-adapted eye in a darkened wheelhouse; on a phone or a laptop at
4698
+ // anchor it is not dim, it is absent — reported as "can't see bottom
4699
+ // contours in night mode only". A contour nobody can see fails the
4700
+ // standard's intent while matching its number.
4701
+ //
4702
+ // The palette already makes this trade elsewhere and in the same
4703
+ // direction: NIGHT's CHBLK/CSTLN/OUTLW are #c8aa78, light warm linework on
4704
+ // dark water. This value is that family, dimmed to stay subordinate to the
4705
+ // coastline. Revisit with the CHRTstyles programme, which owns the
4706
+ // spec-vs-legibility call across all 63 tokens.
4707
+ DEPCN: "#A68E84",
4691
4708
  UINFD: "#c8aa78",
4692
4709
  UINFF: "#383028",
4693
4710
  UINFR: "#682030",
@@ -5621,7 +5638,16 @@ var S52_LOOKUP = {
5621
5638
  category: "Depths",
5622
5639
  displayCategory: "STANDARD",
5623
5640
  int1Ref: "I30",
5624
- style: { type: "line", paint: { "line-color": "CHGRD", "line-width": 0.5 } },
5641
+ // S-52 names a dedicated depth-contour colour, and this used the shared
5642
+ // chart-grey instead. Reported 2026-09-02: "can't see bottom contours in
5643
+ // night mode only" — measured at 1.06:1 against the deepest NIGHT water,
5644
+ // which is invisible. DAY was 2.00:1 and DUSK 1.63:1 off the same token,
5645
+ // so the bug was worst where the palette is darkest.
5646
+ //
5647
+ // CHGRD is shared by dozens of linestyles (s52-linestyles-generated.ts),
5648
+ // so it could not be retuned for contours without moving all of them.
5649
+ // DEPCN exists in all three palettes and is the token S-52 specifies.
5650
+ style: { type: "line", paint: { "line-color": "DEPCN", "line-width": 0.5 } },
5625
5651
  viewingGroup: 13030,
5626
5652
  scamin: 45e3
5627
5653
  },
@@ -22431,13 +22457,23 @@ var HAS_OBJNAM = [
22431
22457
  ["has", "OBJNAM"],
22432
22458
  ["!=", ["coalesce", ["get", "OBJNAM"], ""], ""]
22433
22459
  ];
22460
+ var OBJNAM_TEXT = [
22461
+ "case",
22462
+ [
22463
+ "all",
22464
+ ["has", "NOBJNM"],
22465
+ [">=", ["index-of", "?", ["coalesce", ["get", "OBJNAM"], ""]], 0]
22466
+ ],
22467
+ ["get", "NOBJNM"],
22468
+ ["get", "OBJNAM"]
22469
+ ];
22434
22470
  function catalogueNamePairs(acronym) {
22435
22471
  const names = s57NameTable(acronym);
22436
22472
  return Object.keys(names).map(Number).sort((a, b) => a - b).map((code) => [String(code), names[code]]);
22437
22473
  }
22438
22474
  function buildLandmarkLabelExpression() {
22439
22475
  const categoryExpr = listAttrCase("CATLMK", catalogueNamePairs("CATLMK"), "");
22440
- const base = ["case", HAS_OBJNAM, ["get", "OBJNAM"], categoryExpr];
22476
+ const base = ["case", HAS_OBJNAM, OBJNAM_TEXT, categoryExpr];
22441
22477
  const conspic = s57AbbrTable("CONVIS")[1] ?? "";
22442
22478
  const conspicExpr = [
22443
22479
  "case",
@@ -22456,7 +22492,7 @@ function buildHarbourFacilityLabelExpression() {
22456
22492
  return [
22457
22493
  "case",
22458
22494
  HAS_OBJNAM,
22459
- ["get", "OBJNAM"],
22495
+ OBJNAM_TEXT,
22460
22496
  listAttrCase("CATHAF", catalogueNamePairs("CATHAF"), "")
22461
22497
  ];
22462
22498
  }
@@ -23013,31 +23049,37 @@ function extractNavAidDesignation(objnam) {
23013
23049
  return token;
23014
23050
  }
23015
23051
  function navAidDesignationTextExpression(objnam, colourPrefix) {
23016
- const len = ["length", objnam];
23052
+ const name = ["var", "navAidName"];
23053
+ const len = ["length", name];
23017
23054
  const tokenBranches = [];
23018
23055
  for (let k = 1; k <= 4; k++) {
23019
23056
  tokenBranches.push(
23020
23057
  [
23021
23058
  "all",
23022
23059
  [">", len, k],
23023
- ["==", ["slice", objnam, ["-", len, k + 1], ["-", len, k]], " "]
23060
+ ["==", ["slice", name, ["-", len, k + 1], ["-", len, k]], " "]
23024
23061
  ],
23025
- ["slice", objnam, ["-", len, k]]
23062
+ ["slice", name, ["-", len, k]]
23026
23063
  );
23027
23064
  }
23028
23065
  return [
23029
23066
  "let",
23030
- "desig",
23031
- ["case", ...tokenBranches, ""],
23067
+ "navAidName",
23068
+ objnam,
23032
23069
  [
23033
- "case",
23070
+ "let",
23071
+ "desig",
23072
+ ["case", ...tokenBranches, ""],
23034
23073
  [
23035
- "any",
23036
- ["==", ["var", "desig"], ""],
23037
- ["match", ["var", "desig"], [...NAV_DESIGNATION_STOPWORDS], true, false]
23038
- ],
23039
- ["concat", colourPrefix, objnam],
23040
- ["concat", colourPrefix, '"', ["var", "desig"], '"']
23074
+ "case",
23075
+ [
23076
+ "any",
23077
+ ["==", ["var", "desig"], ""],
23078
+ ["match", ["var", "desig"], [...NAV_DESIGNATION_STOPWORDS], true, false]
23079
+ ],
23080
+ ["concat", colourPrefix, name],
23081
+ ["concat", colourPrefix, '"', ["var", "desig"], '"']
23082
+ ]
23041
23083
  ]
23042
23084
  ];
23043
23085
  }
@@ -23144,7 +23186,7 @@ function getLabelConfig(layerName, opts = {}) {
23144
23186
  const tierExpr = buildLandRegionTierExpression();
23145
23187
  return {
23146
23188
  type: "symbol",
23147
- textField: ["get", "OBJNAM"],
23189
+ textField: OBJNAM_TEXT,
23148
23190
  // Tier-1 entry moved from z5 to z7: undersea-relief names without a
23149
23191
  // generic keyword ("Three Dory", "Outer Fall") can't be demoted by
23150
23192
  // pattern, so the overview/general bands stay clean by holding ALL
@@ -23213,7 +23255,7 @@ function getLabelConfig(layerName, opts = {}) {
23213
23255
  case "BOYLAT":
23214
23256
  return {
23215
23257
  type: "symbol",
23216
- textField: ["coalesce", ["get", "_navAidLabel"], ["get", "OBJNAM"]],
23258
+ textField: ["coalesce", ["get", "_navAidLabel"], OBJNAM_TEXT],
23217
23259
  textSize: NAV_AID_LABEL_TEXT_SIZE,
23218
23260
  textFont: NAV_AID_LABEL_FONT,
23219
23261
  textColor: DARK_BG,
@@ -23226,7 +23268,7 @@ function getLabelConfig(layerName, opts = {}) {
23226
23268
  case "BCNLAT":
23227
23269
  return {
23228
23270
  type: "symbol",
23229
- textField: ["coalesce", ["get", "_navAidLabel"], ["get", "OBJNAM"]],
23271
+ textField: ["coalesce", ["get", "_navAidLabel"], OBJNAM_TEXT],
23230
23272
  textSize: NAV_AID_LABEL_TEXT_SIZE,
23231
23273
  textFont: NAV_AID_LABEL_FONT,
23232
23274
  textColor: DARK_BG,
@@ -23240,7 +23282,7 @@ function getLabelConfig(layerName, opts = {}) {
23240
23282
  case "BCNCAR":
23241
23283
  return {
23242
23284
  type: "symbol",
23243
- textField: ["coalesce", ["get", "_navAidLabel"], ["get", "OBJNAM"]],
23285
+ textField: ["coalesce", ["get", "_navAidLabel"], OBJNAM_TEXT],
23244
23286
  textSize: NAV_AID_LABEL_TEXT_SIZE,
23245
23287
  textFont: NAV_AID_LABEL_FONT,
23246
23288
  textColor: DARK_BG,
@@ -23254,7 +23296,7 @@ function getLabelConfig(layerName, opts = {}) {
23254
23296
  case "BCNSPP":
23255
23297
  return {
23256
23298
  type: "symbol",
23257
- textField: ["coalesce", ["get", "_navAidLabel"], ["get", "OBJNAM"]],
23299
+ textField: ["coalesce", ["get", "_navAidLabel"], OBJNAM_TEXT],
23258
23300
  textSize: NAV_AID_LABEL_TEXT_SIZE,
23259
23301
  textFont: NAV_AID_LABEL_FONT,
23260
23302
  textColor: DARK_BG,
@@ -23268,7 +23310,7 @@ function getLabelConfig(layerName, opts = {}) {
23268
23310
  case "BOYSAW":
23269
23311
  return {
23270
23312
  type: "symbol",
23271
- textField: ["coalesce", ["get", "_navAidLabel"], ["get", "OBJNAM"]],
23313
+ textField: ["coalesce", ["get", "_navAidLabel"], OBJNAM_TEXT],
23272
23314
  textSize: NAV_AID_LABEL_TEXT_SIZE,
23273
23315
  textFont: NAV_AID_LABEL_FONT,
23274
23316
  textColor: DARK_BG,
@@ -23326,7 +23368,7 @@ function getLabelConfig(layerName, opts = {}) {
23326
23368
  const tierExpr = buildSeaAreaTierExpression();
23327
23369
  return {
23328
23370
  type: "symbol",
23329
- textField: ["get", "OBJNAM"],
23371
+ textField: OBJNAM_TEXT,
23330
23372
  // Default tier (CATSEA=0, uncategorised: canyons, seamounts, fishing
23331
23373
  // banks etc.) is suppressed at overview/general/coastal scales because
23332
23374
  // it floods the overview with hundreds of small italic labels that
@@ -23448,7 +23490,7 @@ function getLabelConfig(layerName, opts = {}) {
23448
23490
  const tierExpr = buildLakeTierExpression();
23449
23491
  return {
23450
23492
  type: "symbol",
23451
- textField: ["get", "OBJNAM"],
23493
+ textField: OBJNAM_TEXT,
23452
23494
  textSize: tieredSizeCurve(tierExpr, [
23453
23495
  [7, [9, 0]],
23454
23496
  [9, [11, 0]],
@@ -23469,7 +23511,7 @@ function getLabelConfig(layerName, opts = {}) {
23469
23511
  const tierExpr = buildRiverTierExpression();
23470
23512
  return {
23471
23513
  type: "symbol",
23472
- textField: ["get", "OBJNAM"],
23514
+ textField: OBJNAM_TEXT,
23473
23515
  textSize: tieredSizeCurve(tierExpr, [
23474
23516
  [7, [9, 0]],
23475
23517
  [9, [11, 0]],
@@ -23489,7 +23531,7 @@ function getLabelConfig(layerName, opts = {}) {
23489
23531
  case "CANALS":
23490
23532
  return {
23491
23533
  type: "symbol",
23492
- textField: ["get", "OBJNAM"],
23534
+ textField: OBJNAM_TEXT,
23493
23535
  textSize: [
23494
23536
  "interpolate",
23495
23537
  ["linear"],
@@ -23513,7 +23555,7 @@ function getLabelConfig(layerName, opts = {}) {
23513
23555
  const tierExpr = buildLandAreaTierExpression();
23514
23556
  return {
23515
23557
  type: "symbol",
23516
- textField: ["get", "OBJNAM"],
23558
+ textField: OBJNAM_TEXT,
23517
23559
  // Tier 2 is the SMALL island — the one the owner cannot identify.
23518
23560
  // Its old curve peaked at 10 px and fell to 10 px, i.e. it never got
23519
23561
  // its own scale: a name that only exists because you zoomed in was
@@ -23545,7 +23587,7 @@ function getLabelConfig(layerName, opts = {}) {
23545
23587
  const tierExpr = buildBuiltUpAreaTierExpression();
23546
23588
  return {
23547
23589
  type: "symbol",
23548
- textField: ["get", "OBJNAM"],
23590
+ textField: OBJNAM_TEXT,
23549
23591
  textSize: tieredSizeCurve(tierExpr, [
23550
23592
  [3, [11, 0, 0]],
23551
23593
  [5, [13, 0, 0]],
@@ -23569,7 +23611,7 @@ function getLabelConfig(layerName, opts = {}) {
23569
23611
  case "BRIDGE":
23570
23612
  return {
23571
23613
  type: "symbol",
23572
- textField: ["get", "OBJNAM"],
23614
+ textField: OBJNAM_TEXT,
23573
23615
  textSize: 9,
23574
23616
  textColor: "#bfa88a",
23575
23617
  textHaloColor: DARK_BG,
@@ -23588,7 +23630,7 @@ function getLabelConfig(layerName, opts = {}) {
23588
23630
  case "PILPNT":
23589
23631
  return {
23590
23632
  type: "symbol",
23591
- textField: ["get", "OBJNAM"],
23633
+ textField: OBJNAM_TEXT,
23592
23634
  textSize: 8,
23593
23635
  textColor: "#c8c0b0",
23594
23636
  textHaloColor: DARK_BG,
@@ -23599,7 +23641,7 @@ function getLabelConfig(layerName, opts = {}) {
23599
23641
  case "MORFAC":
23600
23642
  return {
23601
23643
  type: "symbol",
23602
- textField: ["get", "OBJNAM"],
23644
+ textField: OBJNAM_TEXT,
23603
23645
  textSize: 8,
23604
23646
  textColor: "#c8c0b0",
23605
23647
  textHaloColor: DARK_BG,
@@ -23610,7 +23652,7 @@ function getLabelConfig(layerName, opts = {}) {
23610
23652
  case "SLCONS":
23611
23653
  return {
23612
23654
  type: "symbol",
23613
- textField: ["get", "OBJNAM"],
23655
+ textField: OBJNAM_TEXT,
23614
23656
  textSize: 8,
23615
23657
  textColor: "#bfa88a",
23616
23658
  textHaloColor: DARK_BG,
@@ -23625,7 +23667,7 @@ function getLabelConfig(layerName, opts = {}) {
23625
23667
  case "BUISGL":
23626
23668
  return {
23627
23669
  type: "symbol",
23628
- textField: ["get", "OBJNAM"],
23670
+ textField: OBJNAM_TEXT,
23629
23671
  textSize: 8,
23630
23672
  textColor: DARK_BG,
23631
23673
  textHaloColor: "#c8c0b0",
@@ -23641,7 +23683,7 @@ function getLabelConfig(layerName, opts = {}) {
23641
23683
  const tierExpr = buildAirportTierExpression();
23642
23684
  return {
23643
23685
  type: "symbol",
23644
- textField: ["get", "OBJNAM"],
23686
+ textField: OBJNAM_TEXT,
23645
23687
  textSize: tieredSizeCurve(tierExpr, [
23646
23688
  [7, [9, 0]],
23647
23689
  [9, [11, 0]],
@@ -23725,7 +23767,7 @@ function getLabelConfig(layerName, opts = {}) {
23725
23767
  case "PRDARE":
23726
23768
  return {
23727
23769
  type: "symbol",
23728
- textField: ["get", "OBJNAM"],
23770
+ textField: OBJNAM_TEXT,
23729
23771
  textSize: [
23730
23772
  "interpolate",
23731
23773
  ["linear"],
@@ -27105,7 +27147,7 @@ function buildNavAidLabelExpression() {
27105
27147
  // doesn't precompute `_navAidLabel`, so before this expression every
27106
27148
  // production buoy label carried its full 30-40 glyph OBJNAM, the
27107
27149
  // biggest single driver of Harbor/Approach label overprint (E3).
27108
- navAidDesignationTextExpression(["get", "OBJNAM"], colourPrefix),
27150
+ navAidDesignationTextExpression(OBJNAM_TEXT, colourPrefix),
27109
27151
  ""
27110
27152
  ];
27111
27153
  }
@@ -27521,7 +27563,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
27521
27563
  "icon-allow-overlap": true,
27522
27564
  "icon-ignore-placement": false,
27523
27565
  "symbol-placement": "point",
27524
- "text-field": ["case", ["has", "OBJNAM"], ["get", "OBJNAM"], ""],
27566
+ "text-field": ["case", ["has", "OBJNAM"], OBJNAM_TEXT, ""],
27525
27567
  "text-size": ["interpolate", ["linear"], ["zoom"], 11, 9, 16, 11],
27526
27568
  "text-anchor": "top",
27527
27569
  "text-offset": [0, 1.4],
@@ -28238,6 +28280,18 @@ function buildLabelLayer2(objectClass, ct, sourceId = DEFAULT_REGION, depthUnits
28238
28280
  ["round", ["*", ["to-number", ["get", "VALDCO"]], 3.28084]],
28239
28281
  "ft"
28240
28282
  ];
28283
+ } else if (objectClass === "UWTROC" || objectClass === "OBSTRN" || objectClass === "WRECKS") {
28284
+ const valsouFt = [
28285
+ "to-number",
28286
+ ["coalesce", ["get", "VALSOU"], ["get", "valsou"], 0],
28287
+ 0
28288
+ ];
28289
+ textField = [
28290
+ "case",
28291
+ [">", valsouFt, 0],
28292
+ ["concat", ["to-string", ["round", ["*", valsouFt, 3.28084]]], "ft"],
28293
+ ""
28294
+ ];
28241
28295
  }
28242
28296
  }
28243
28297
  const labelPriority = getLabelPriority(objectClass);
@@ -29383,6 +29437,7 @@ var DOMINANCE_EXEMPT_CLASSES = /* @__PURE__ */ new Set([
29383
29437
  "RTPBCN",
29384
29438
  "RDOSTA"
29385
29439
  ]);
29440
+ var FINE_LAND_REPAINT_MIN_BAND = 4;
29386
29441
  function isDominanceExempt(layer) {
29387
29442
  for (const cls of DOMINANCE_EXEMPT_CLASSES) {
29388
29443
  if (layer.id.startsWith(`s52-${cls}-`)) return true;
@@ -29824,6 +29879,7 @@ function generateStyle(palette = "DAY", opts = {}) {
29824
29879
  }
29825
29880
  const symbolSet = opts.symbolSet ?? "s52";
29826
29881
  const spriteBase = `${opts.tileBaseUrl ?? "https://tiles.chrt.co"}/v1/sprites`;
29882
+ const glyphsBase = `${opts.tileBaseUrl ?? "https://tiles.chrt.co"}/v1/glyphs`;
29827
29883
  const isLndareFill = (l) => /^s52-LNDARE-fill-band\d+$/.test(l.id);
29828
29884
  const isWaterBaseFill = (l) => /^s52-(DEPARE|DRGARE|UNSARE)-fill-band\d+$/.test(l.id);
29829
29885
  const bandOf = (l) => {
@@ -29891,6 +29947,20 @@ function generateStyle(palette = "DAY", opts = {}) {
29891
29947
  repaint.push(domVariant);
29892
29948
  }
29893
29949
  }
29950
+ if (parseInt(b, 10) >= FINE_LAND_REPAINT_MIN_BAND) {
29951
+ for (const l of relocated.filter(
29952
+ (x) => bandOf(x) === b && isLndareFill(x)
29953
+ )) {
29954
+ const src = l;
29955
+ const landClone = {
29956
+ ...l,
29957
+ id: `${src.id}-overpair`,
29958
+ metadata: { ...src.metadata ?? {}, "chrt:role": "charted-land-repaint" }
29959
+ };
29960
+ delete landClone.maxzoom;
29961
+ repaint.push(landClone);
29962
+ }
29963
+ }
29894
29964
  }
29895
29965
  const isCoalneLine = (l) => /^s52-COALNE-line-band\d(-domBy\d)?$/.test(l.id);
29896
29966
  for (const l of layers.filter(isCoalneLine)) {
@@ -29965,7 +30035,23 @@ function generateStyle(palette = "DAY", opts = {}) {
29965
30035
  ...symbolSet !== "s101" && {
29966
30036
  sprite: `${spriteBase}/${symbolSet}-${palette.toLowerCase()}`
29967
30037
  },
29968
- glyphs: "https://fonts.openmaptiles.org/{fontstack}/{range}.pbf",
30038
+ // ⚠ DEPLOY ORDER. This URL is baked into the style by callers on BOTH
30039
+ // sides: the tile-worker's /v1/styles route AND the app's own
30040
+ // `generateStyle` call in ChartCanvas.tsx, which ships from Railway on a
30041
+ // SEPARATE deploy from the Workers. The glyph objects must be in R2 and
30042
+ // the /v1/glyphs route live BEFORE any client asks for this URL — if the
30043
+ // frontend ships first, every label on the chart goes blank, not just the
30044
+ // ones with an ʻokina. Land the tile-worker deploy first, or revert this
30045
+ // one line to the openmaptiles URL to fall back.
30046
+ //
30047
+ // Self-hosted, beside the sprite sheets on the same host. Was
30048
+ // fonts.openmaptiles.org, whose 2013-vintage Open Sans has NO GLYPH for
30049
+ // U+02BB ʻOKINA (a CONSONANT in Hawaiian) or five other codepoints that
30050
+ // occur in NOAA names — MapLibre draws a missing glyph as nothing, so
30051
+ // "Island of Hawaiʻi" rendered with a hole in it. Our ranges keep every
30052
+ // upstream glyph byte-identical and composite only those six in from
30053
+ // Noto Sans; see scripts/generate-glyphs.mjs.
30054
+ glyphs: `${glyphsBase}/{fontstack}/{range}.pbf`,
29969
30055
  layers: emitted
29970
30056
  };
29971
30057
  if (symbolSet === "s101") return applyS101SymbolSet(style);