@chrtco/sdk 0.10.44 → 0.10.46

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.
@@ -42,7 +42,7 @@ interface SymbolImageSpec {
42
42
  */
43
43
  declare const BASE_PATTERN_GLYPHS: readonly string[];
44
44
 
45
- /** The two world hatches, in this palette. */
45
+ /** The two world hatches, in this palette (coloured by `basePatternSvg`). */
46
46
  declare function basePatternImageSpecs(palette: ColorTableName): SymbolImageSpec[];
47
47
  /** The CHRT house glyphs (not the hatches, which `basePatternImageSpecs` owns). */
48
48
  declare function chrtImageSpecs(palette: ColorTableName): SymbolImageSpec[];
@@ -217,7 +217,7 @@ declare function reloadS101SymbolImages(map: maplibregl.Map, colorTable: ColorTa
217
217
  * NOTHING with no error anywhere (the crosshatch silently absent from the
218
218
  * whole world map). Two images, loaded beside every family's loader.
219
219
  *
220
- * Themed per palette (`themeChrtGlyph`): the hatches cover the whole uncharted
220
+ * Themed per palette (`basePatternSvg`): the hatches cover the whole uncharted
221
221
  * world, and at NIGHT they were the one grey-blue texture on a dark chart.
222
222
  */
223
223
  declare function loadBasePatternImages(map: maplibregl.Map, colorTable?: ColorTableName): Promise<void>;
@@ -343,6 +343,8 @@ interface RethemeResult {
343
343
  /**
344
344
  * Register every spec the map does not already hold. A first load, so there is
345
345
  * nothing to update in place; images the map already has are left alone.
346
+ *
347
+ * A map removed while a decode was out is left alone too — see `isMapLive`.
346
348
  */
347
349
  declare function loadSymbolImageSpecs(map: maplibregl.Map, specs: readonly SymbolImageSpec[]): Promise<void>;
348
350
  /**
@@ -352,6 +354,9 @@ declare function loadSymbolImageSpecs(map: maplibregl.Map, specs: readonly Symbo
352
354
  * reload). One it does not hold, or holds at another size, is added — the
353
355
  * slow path, which only a first load or a drifted spec should ever take, and
354
356
  * `RethemeResult.added` says how often it did.
357
+ *
358
+ * A map removed part-way (`isMapLive`) ends the re-theme as `aborted`: there is
359
+ * nothing left to draw on, and whoever removed it is building the next one.
355
360
  */
356
361
  declare function rethemeSymbolImages(map: maplibregl.Map, specs: readonly SymbolImageSpec[], opts?: RethemeOptions): Promise<RethemeResult>;
357
362
 
@@ -42,7 +42,7 @@ interface SymbolImageSpec {
42
42
  */
43
43
  declare const BASE_PATTERN_GLYPHS: readonly string[];
44
44
 
45
- /** The two world hatches, in this palette. */
45
+ /** The two world hatches, in this palette (coloured by `basePatternSvg`). */
46
46
  declare function basePatternImageSpecs(palette: ColorTableName): SymbolImageSpec[];
47
47
  /** The CHRT house glyphs (not the hatches, which `basePatternImageSpecs` owns). */
48
48
  declare function chrtImageSpecs(palette: ColorTableName): SymbolImageSpec[];
@@ -217,7 +217,7 @@ declare function reloadS101SymbolImages(map: maplibregl.Map, colorTable: ColorTa
217
217
  * NOTHING with no error anywhere (the crosshatch silently absent from the
218
218
  * whole world map). Two images, loaded beside every family's loader.
219
219
  *
220
- * Themed per palette (`themeChrtGlyph`): the hatches cover the whole uncharted
220
+ * Themed per palette (`basePatternSvg`): the hatches cover the whole uncharted
221
221
  * world, and at NIGHT they were the one grey-blue texture on a dark chart.
222
222
  */
223
223
  declare function loadBasePatternImages(map: maplibregl.Map, colorTable?: ColorTableName): Promise<void>;
@@ -343,6 +343,8 @@ interface RethemeResult {
343
343
  /**
344
344
  * Register every spec the map does not already hold. A first load, so there is
345
345
  * nothing to update in place; images the map already has are left alone.
346
+ *
347
+ * A map removed while a decode was out is left alone too — see `isMapLive`.
346
348
  */
347
349
  declare function loadSymbolImageSpecs(map: maplibregl.Map, specs: readonly SymbolImageSpec[]): Promise<void>;
348
350
  /**
@@ -352,6 +354,9 @@ declare function loadSymbolImageSpecs(map: maplibregl.Map, specs: readonly Symbo
352
354
  * reload). One it does not hold, or holds at another size, is added — the
353
355
  * slow path, which only a first load or a drifted spec should ever take, and
354
356
  * `RethemeResult.added` says how often it did.
357
+ *
358
+ * A map removed part-way (`isMapLive`) ends the re-theme as `aborted`: there is
359
+ * nothing left to draw on, and whoever removed it is building the next one.
355
360
  */
356
361
  declare function rethemeSymbolImages(map: maplibregl.Map, specs: readonly SymbolImageSpec[], opts?: RethemeOptions): Promise<RethemeResult>;
357
362
 
@@ -1112,6 +1112,11 @@ function applyStrokeBoost(svgString, multiplier) {
1112
1112
  }
1113
1113
  var S52_STROKE_BOOST = 1;
1114
1114
 
1115
+ // src/atlas/map-live.ts
1116
+ function isMapLive(map) {
1117
+ return map._removed !== true;
1118
+ }
1119
+
1115
1120
  // src/atlas/symbol-images-browser.ts
1116
1121
  function dataUrl(svg2) {
1117
1122
  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg2)}`;
@@ -1252,9 +1257,9 @@ function existingSize(map, id) {
1252
1257
  async function loadSymbolImageSpecs(map, specs) {
1253
1258
  await Promise.all(
1254
1259
  specs.map(async (spec) => {
1255
- if (map.hasImage(spec.id)) return;
1260
+ if (!isMapLive(map) || map.hasImage(spec.id)) return;
1256
1261
  const img = await decode(spec);
1257
- if (!img || map.hasImage(spec.id)) return;
1262
+ if (!img || !isMapLive(map) || map.hasImage(spec.id)) return;
1258
1263
  map.addImage(spec.id, img, { sdf: spec.sdf, pixelRatio: spec.pixelRatio });
1259
1264
  })
1260
1265
  );
@@ -1278,7 +1283,7 @@ async function rethemeSymbolImages(map, specs, opts = {}) {
1278
1283
  }
1279
1284
  const sources = await sourcesFor(unit, decoded[u]);
1280
1285
  for (let i = 0; i < unitSpecs.length; i++) {
1281
- if (opts.signal?.aborted) {
1286
+ if (opts.signal?.aborted || !isMapLive(map)) {
1282
1287
  result.aborted = true;
1283
1288
  break outer;
1284
1289
  }
@@ -1312,6 +1317,10 @@ async function rethemeSymbolImages(map, specs, opts = {}) {
1312
1317
  }
1313
1318
  }
1314
1319
  }
1320
+ if (!isMapLive(map)) {
1321
+ result.aborted = true;
1322
+ return result;
1323
+ }
1315
1324
  map.triggerRepaint();
1316
1325
  opts.onProgress?.({ done, total });
1317
1326
  return result;
@@ -1488,7 +1497,7 @@ async function imageToBitmap(img, size) {
1488
1497
  }
1489
1498
  }
1490
1499
  function addImageToMap(map, name, image) {
1491
- if (map.hasImage(name)) return;
1500
+ if (!isMapLive(map) || map.hasImage(name)) return;
1492
1501
  try {
1493
1502
  map.addImage(name, image, {
1494
1503
  sdf: SDF_SYMBOLS2.has(name),
@@ -1515,6 +1524,7 @@ async function loadS52SymbolImagesCached(map, colorTable = "DAY", opts = {}) {
1515
1524
  cached = /* @__PURE__ */ new Map();
1516
1525
  }
1517
1526
  }
1527
+ if (!isMapLive(map)) return;
1518
1528
  for (const [name, bitmap] of cached) {
1519
1529
  addImageToMap(map, name, bitmap);
1520
1530
  }
@@ -1528,7 +1538,7 @@ async function loadS52SymbolImagesCached(map, colorTable = "DAY", opts = {}) {
1528
1538
  const size = rasterSize2(name, themed);
1529
1539
  const img = await decodeSvgToImage(themed, size);
1530
1540
  if (!img) return;
1531
- addImageToMap(map, name, img);
1541
+ if (isMapLive(map)) addImageToMap(map, name, img);
1532
1542
  try {
1533
1543
  const bitmap = await imageToBitmap(img, size);
1534
1544
  if (!bitmap) return;
@@ -15543,7 +15553,7 @@ async function loadSymbolImages(map) {
15543
15553
  const svgStr = sym.getSvg();
15544
15554
  const img = new Image(SIZE2, SIZE2);
15545
15555
  img.onload = () => {
15546
- if (!map.hasImage(sym.id)) {
15556
+ if (isMapLive(map) && !map.hasImage(sym.id)) {
15547
15557
  map.addImage(sym.id, img, { sdf: false });
15548
15558
  }
15549
15559
  resolve();
@@ -15573,7 +15583,7 @@ async function loadThemedBridgeImages(map, colorTable) {
15573
15583
  const svgStr = sym.getThemedSvg(chblk, trfcd);
15574
15584
  const img = new Image(SIZE2, SIZE2);
15575
15585
  img.onload = () => {
15576
- if (!map.hasImage(sym.id)) {
15586
+ if (isMapLive(map) && !map.hasImage(sym.id)) {
15577
15587
  map.addImage(sym.id, img, { sdf: false });
15578
15588
  }
15579
15589
  resolve();
@@ -15691,6 +15701,126 @@ function themeChrtGlyph(svg2, palette = "DAY") {
15691
15701
  );
15692
15702
  }
15693
15703
 
15704
+ // src/atlas/base-layers.ts
15705
+ var UNCHARTED_PATTERN = "chrt:hatch-uncharted";
15706
+ function inlandHatchColor(palette) {
15707
+ return (BASE_THEMES[palette] ?? BASE_THEMES.DAY).inlandHatch;
15708
+ }
15709
+ function basePatternSvg(name, palette) {
15710
+ const raw = CHRT_GLYPH_SVGS[name];
15711
+ if (!raw) return null;
15712
+ if (`${CHRT_ICON_PREFIX}${name}` === UNCHARTED_PATTERN) {
15713
+ return raw.replace(/stroke="#[0-9a-fA-F]{3,6}"/g, `stroke="${inlandHatchColor(palette)}"`);
15714
+ }
15715
+ return themeChrtGlyph(raw, palette);
15716
+ }
15717
+ var BASE_THEMES = {
15718
+ DAY: {
15719
+ // ── `earth` is the jurisdiction tan DIMMED (~0.55), not a blue-grey ──
15720
+ // Outside the jurisdiction the map still owes the reader "what is land".
15721
+ // State 3 in the header is "the geography, under a crosshatch", and until
15722
+ // 2026-08-31 that geography was unreadable: earth #2A3439 against sea
15723
+ // #1B2328 measured 1.25:1, so Hispaniola showed charted boxes with tan
15724
+ // land inside and nothing recognisable around them (owner report: "no
15725
+ // land painting down near the DR at some zooms"). Land is warm and sea is
15726
+ // cool because that is what land and sea are; dimming the JURISDICTION
15727
+ // tan rather than inventing a colour says the true thing — this is the
15728
+ // same land, we simply do not carry charts for it. Held against BOTH
15729
+ // neighbours: legible over sea, and never mistakable for the full tan of
15730
+ // a country we do carry. DAY 3.59:1 vs sea, 3.17:1 vs tan.
15731
+ earth: "#807761",
15732
+ water: "#1B2328",
15733
+ river: "#24313A",
15734
+ boundary: "#4A575E",
15735
+ jurisdiction: "#D4C69D",
15736
+ // CHRTland's own tan — one land colour
15737
+ inlandWater: "#B9CFDD",
15738
+ // muted, unmistakably not a charted blue
15739
+ // The uncharted hatch's stripe, held to 2.3:1 against `inlandWater` in
15740
+ // every palette (`inland-hatch-contrast.test.ts`) — the hatch says "we have
15741
+ // not charted this water", and stripes the eye cannot find say nothing.
15742
+ // DAY 3.08:1, darker than its ground. Also the colour the artwork file
15743
+ // itself is drawn in.
15744
+ inlandHatch: "#5B7383",
15745
+ // State furniture. These three were referenced by the layers but missing
15746
+ // from this table (and its type — the long-standing TS2339 trio), so the
15747
+ // lines shipped on MapLibre's DEFAULT black. Now deliberate: a hairline
15748
+ // a shade deeper than the tan, quiet caps, tan halo.
15749
+ stateLine: "#B1A077",
15750
+ stateLabel: "#8E8468",
15751
+ stateHalo: "#D4C69D",
15752
+ // Rough-lake furniture (owner ask 2026-08-28: "the lakes need labels,
15753
+ // and shorelines drawn"): a shoreline a shade deeper than the rough
15754
+ // water, and a name in the water's own voice, haloed by the water.
15755
+ inlandShore: "#8FA9BA",
15756
+ inlandLabel: "#54707F",
15757
+ inlandHalo: "#B9CFDD"
15758
+ },
15759
+ DUSK: {
15760
+ // ── Re-derived 2026-09-01: the basemap sits UNDER the chart ──────────
15761
+ // These were the DAY tones scaled by a constant, never measured against
15762
+ // the DUSK chart — so `jurisdiction` came out at L=0.506 against a chart
15763
+ // whose own land (LANDA #8a7d5e) is L=0.209, i.e. the context layer was
15764
+ // 2.4x brighter than the data layer. Now capped at the chart's own land.
15765
+ // The rest of the block rides the same 0.666 channel factor so every
15766
+ // relationship the palette already chose is preserved, only moved down.
15767
+ // `water` is deliberately NOT scaled: it is the reference the land/sea
15768
+ // floor is measured against.
15769
+ earth: "#544E3E",
15770
+ water: "#161D21",
15771
+ river: "#141B20",
15772
+ boundary: "#293135",
15773
+ jurisdiction: "#867D64",
15774
+ inlandWater: "#62717A",
15775
+ // Owner's choice, 2026-10-04: dark stripes, 2.37:1. The DAY stripe here
15776
+ // was 1.02:1 — invisible, so an uncharted lake read as plain water.
15777
+ inlandHatch: "#2E383F",
15778
+ // Label stays DARK here — the halo is still mid-toned, so 3:1 is reachable
15779
+ // downward and a bright label would be a new light source at dusk.
15780
+ stateLine: "#B5A987",
15781
+ stateLabel: "#38342A",
15782
+ stateHalo: "#867D64",
15783
+ inlandShore: "#4D5B67",
15784
+ inlandLabel: "#212629",
15785
+ inlandHalo: "#62717A"
15786
+ },
15787
+ NIGHT: {
15788
+ // ── Re-derived 2026-09-01 — the owner-reported night defect ──────────
15789
+ // `jurisdiction` was L=0.240 against a chart whose own land (LANDA
15790
+ // #3a3020) is L=0.031: the world basemap was 7.7x BRIGHTER than the chart
15791
+ // it sits under, so at z<11 the uncharted interior read as a pale
15792
+ // continent-sized slab on a night chart. Capped at the chart's own land.
15793
+ //
15794
+ // ⚠ `earth` AND `jurisdiction` CONVERGE HERE, and that is forced, not
15795
+ // sloppy. Holding earth 2:1 above the ocean AND 2:1 below the jurisdiction
15796
+ // tan requires jurisdiction >= L 0.174 — 5.6x the chart's own land. At
15797
+ // night the range between the ocean (L 0.006) and the cap (L 0.031) is too
15798
+ // compressed to carry two land tones. COVERAGE IS STILL SAID, by the
15799
+ // `chrt:hatch-nocoverage` crosshatch, which is a stronger signal than tone
15800
+ // and is the one thing that cannot be lost to palette compression.
15801
+ //
15802
+ // Labels invert to light-on-dark: 3:1 downward is unreachable off a halo
15803
+ // this close to black, and CHRTland's own night town labels already sit at
15804
+ // L=0.177 for exactly this reason.
15805
+ earth: "#333026",
15806
+ water: "#10161A",
15807
+ river: "#090D0F",
15808
+ boundary: "#131719",
15809
+ jurisdiction: "#353128",
15810
+ inlandWater: "#232A30",
15811
+ // The DAY stripe, lighter than this ground: 2.92:1, and L 0.16, under the
15812
+ // NIGHT muted-ink ceiling (`night-luminance.test.ts`). Red night draws the
15813
+ // NIGHT chart, so this is its stripe too.
15814
+ inlandHatch: "#5B7383",
15815
+ stateLine: "#484236",
15816
+ stateLabel: "#837963",
15817
+ stateHalo: "#353128",
15818
+ inlandShore: "#1C2227",
15819
+ inlandLabel: "#617484",
15820
+ inlandHalo: "#232A30"
15821
+ }
15822
+ };
15823
+
15694
15824
  // src/atlas/chrtraster-svgs-generated.ts
15695
15825
  var CHRTRASTER_SVGS = {
15696
15826
  "BCNGEN05": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M 11.6 26.8 L 11.6 1.0 L 20.0 1.0 L 20.0 27.2 L 19.4 25.8 L 19.3 24.7 L 18.9 24.3 L 17.6 23.7 L 16.5 23.4 L 16.2 23.2 L 15.1 23.4 L 14.5 23.4 L 13.8 23.7 L 13.3 24.5 L 12.7 25.3 L 12.0 25.8 L 11.6 26.6 Z" fill="CHWHT" /><path d="M 11.6 26.8 L 11.6 1.0 L 20.0 1.0 L 20.0 27.2 L 19.4 25.8 L 19.3 24.7 L 18.9 24.3 L 17.6 23.7 L 16.5 23.4 L 16.2 23.2 L 15.1 23.4 L 14.5 23.4 L 13.8 23.7 L 13.3 24.5 L 12.7 25.3 L 12.0 25.8 L 11.6 26.6 Z" fill="none" stroke="CHBLK" stroke-width="1.2" stroke-linejoin="round" /><circle cx="16.00" cy="27.21" r="3.79" fill="none" stroke="CHBLK" stroke-width="1.2" /><path d="M 7.3 27.2 L 12.2 27.2" fill="none" stroke="CHBLK" stroke-width="1.2" /><path d="M 19.8 27.2 L 24.7 27.2" fill="none" stroke="CHBLK" stroke-width="1.2" /></svg>',
@@ -16290,10 +16420,9 @@ function declared(id, svg2) {
16290
16420
  }
16291
16421
  function basePatternImageSpecs(palette) {
16292
16422
  return BASE_PATTERN_GLYPHS.flatMap((name) => {
16293
- const raw = CHRT_GLYPH_SVGS[name];
16294
- if (!raw) return [];
16423
+ const svg2 = basePatternSvg(name, palette);
16424
+ if (!svg2) return [];
16295
16425
  const id = `${CHRT_ICON_PREFIX}${name}`;
16296
- const svg2 = themeChrtGlyph(raw, palette);
16297
16426
  return [{ id, svg: svg2, ...declared(id, svg2), pixelRatio: 1, sdf: false }];
16298
16427
  });
16299
16428
  }