@chrtco/sdk 0.10.39 → 0.10.41

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
@@ -24,8 +24,16 @@ __export(index_exports, {
24
24
  ALL_WATER_BODY_LAYERS: () => ALL_WATER_BODY_LAYERS,
25
25
  AREA_QUERY_LAYERS: () => AREA_QUERY_LAYERS,
26
26
  ATON_EXPRESSION_CLASSES: () => ATON_EXPRESSION_CLASSES,
27
+ BAND_CODE_ALIASES: () => BAND_CODE_ALIASES,
27
28
  BAND_DRAW_ORDER: () => BAND_DRAW_ORDER,
28
29
  BAND_PRIORITY_RANGES: () => BAND_PRIORITY_RANGES,
30
+ BASE_ATTRIBUTION: () => BASE_ATTRIBUTION,
31
+ BASE_BUNDLE_ATTRIBUTION: () => BASE_BUNDLE_ATTRIBUTION,
32
+ BASE_BUNDLE_ID: () => BASE_BUNDLE_ID,
33
+ BASE_BUNDLE_MAXZOOM: () => BASE_BUNDLE_MAXZOOM,
34
+ BASE_BUNDLE_SCHEMA: () => BASE_BUNDLE_SCHEMA,
35
+ BASE_BUNDLE_SOURCE_LAYERS: () => BASE_BUNDLE_SOURCE_LAYERS,
36
+ BASE_CLIENT_RASTER_BEFORE_ID: () => BASE_CLIENT_RASTER_BEFORE_ID,
29
37
  BASE_MAXZOOM: () => BASE_MAXZOOM,
30
38
  BASE_MINZOOM: () => BASE_MINZOOM,
31
39
  BASE_REGION: () => BASE_REGION,
@@ -58,6 +66,7 @@ __export(index_exports, {
58
66
  DEFAULT_TILE_BASE_URL: () => DEFAULT_TILE_BASE_URL,
59
67
  DEFAULT_TILE_REGION: () => DEFAULT_TILE_REGION,
60
68
  DEFAULT_WATER_BODY_SETTINGS: () => DEFAULT_WATER_BODY_SETTINGS,
69
+ DRAPEABLE_LAYER_TYPES: () => DRAPEABLE_LAYER_TYPES,
61
70
  INSPECT_CLASS_RANK: () => INSPECT_CLASS_RANK,
62
71
  INSPECT_RANK_DEVIATIONS: () => INSPECT_RANK_DEVIATIONS,
63
72
  InteractClient: () => InteractClient,
@@ -66,6 +75,11 @@ __export(index_exports, {
66
75
  LABEL_BEHAVIOR: () => LABEL_BEHAVIOR,
67
76
  LABEL_LAYER_GROUPS: () => LABEL_LAYER_GROUPS,
68
77
  LABEL_PRIORITY: () => LABEL_PRIORITY,
78
+ LAMP_CORE_LAYER_ID: () => LAMP_CORE_LAYER_ID,
79
+ LAMP_GLOW_LAYER_ID: () => LAMP_GLOW_LAYER_ID,
80
+ LAMP_LAYER_IDS: () => LAMP_LAYER_IDS,
81
+ LAMP_TICK_MS: () => LAMP_TICK_MS,
82
+ LAND_ATTRIBUTION: () => LAND_ATTRIBUTION,
69
83
  LAND_BUILD_MAXZOOM: () => LAND_BUILD_MAXZOOM,
70
84
  LAND_REGION: () => LAND_REGION,
71
85
  NAUTICAL_SYMBOLS: () => NAUTICAL_SYMBOLS,
@@ -81,7 +95,10 @@ __export(index_exports, {
81
95
  RASTER_ATON_CLASSES: () => RASTER_ATON_CLASSES,
82
96
  RASTER_GLYPH_NAMES: () => RASTER_GLYPH_NAMES,
83
97
  RASTER_ICON_PREFIX: () => RASTER_ICON_PREFIX,
98
+ RELIEF_ATTRIBUTION: () => RELIEF_ATTRIBUTION,
99
+ RELIEF_ENCODING: () => RELIEF_ENCODING,
84
100
  RELIEF_REGION: () => RELIEF_REGION,
101
+ RELIEF_TILE_SIZE: () => RELIEF_TILE_SIZE,
85
102
  S101_AREA_FILL_NAMES: () => S101_AREA_FILL_NAMES,
86
103
  S101_CHART_SECTIONS: () => S101_CHART_SECTIONS,
87
104
  S101_FEATURES: () => S101_FEATURES,
@@ -101,6 +118,8 @@ __export(index_exports, {
101
118
  S52_LOOKUP: () => S52_LOOKUP,
102
119
  S52_NIGHT: () => S52_NIGHT,
103
120
  S52_PATTERN_SPRITE_PREFIX: () => S52_PATTERN_SPRITE_PREFIX,
121
+ SIGPER_TOLERANCE_SECONDS: () => SIGPER_TOLERANCE_SECONDS,
122
+ SIGSEQ_MAX_TERMS: () => SIGSEQ_MAX_TERMS,
104
123
  SOUNDING_LAYERS: () => SOUNDING_LAYERS,
105
124
  SYMBOL_FAMILIES: () => SYMBOL_FAMILIES,
106
125
  SYMBOL_FAMILY_META: () => SYMBOL_FAMILY_META,
@@ -110,6 +129,7 @@ __export(index_exports, {
110
129
  TileTokenError: () => TileTokenError,
111
130
  WATER_BODY_LAYER_GROUPS: () => WATER_BODY_LAYER_GROUPS,
112
131
  WeatherEndpoint: () => WeatherEndpoint,
132
+ applyBaseBundle: () => applyBaseBundle,
113
133
  applyChrtSymbolSet: () => applyChrtSymbolSet,
114
134
  applyColorTheme: () => applyColorTheme,
115
135
  applyCustomStyleSettings: () => applyCustomStyleSettings,
@@ -122,6 +142,7 @@ __export(index_exports, {
122
142
  buildFlashPattern: () => buildFlashPattern,
123
143
  buildLabelLayer: () => buildLabelLayer,
124
144
  buildLakeTierExpression: () => buildLakeTierExpression,
145
+ buildLampLayers: () => buildLampLayers,
125
146
  buildLandAreaTierExpression: () => buildLandAreaTierExpression,
126
147
  buildLandRegionTierExpression: () => buildLandRegionTierExpression,
127
148
  buildLayersForClass: () => buildLayersForClass,
@@ -134,6 +155,8 @@ __export(index_exports, {
134
155
  buildSectorsFromFeatures: () => buildSectorsFromFeatures,
135
156
  buildSources: () => buildSources,
136
157
  candidateRank: () => candidateRank,
158
+ cellBandCharacters: () => cellBandCharacters,
159
+ cellBandMatch: () => cellBandMatch,
137
160
  chrtAtonTintExpression: () => chrtAtonTintExpression,
138
161
  chrtLightTintExpression: () => chrtLightTintExpression,
139
162
  collectCandidates: () => collectCandidates,
@@ -148,6 +171,8 @@ __export(index_exports, {
148
171
  depareColor: () => depareColor,
149
172
  depareFillExpression: () => depareFillExpression,
150
173
  deriveMarinerName: () => deriveMarinerName,
174
+ drapeHalves: () => drapeHalves,
175
+ drapedFirst: () => drapedFirst,
151
176
  extractNavAidDesignation: () => extractNavAidDesignation,
152
177
  featureIdentity: () => featureIdentity,
153
178
  generateCustomStyle: () => generateCustomStyle,
@@ -180,6 +205,7 @@ __export(index_exports, {
180
205
  hazardColor: () => hazardColor,
181
206
  hazardFillExpression: () => hazardFillExpression,
182
207
  iconExpressionForFamily: () => iconExpressionForFamily,
208
+ isDrapeable: () => isDrapeable,
183
209
  isLabelLayerVisible: () => isLabelLayerVisible,
184
210
  isNavAidLayer: () => isNavAidLayer,
185
211
  isNavAidLayerVisible: () => isNavAidLayerVisible,
@@ -187,6 +213,8 @@ __export(index_exports, {
187
213
  isVisibleInMode: () => isVisibleInMode,
188
214
  isWaterBodyLayerVisible: () => isWaterBodyLayerVisible,
189
215
  labelSortKey: () => labelSortKey,
216
+ lampRhythmColour: () => lampRhythmColour,
217
+ lampRhythmOpacity: () => lampRhythmOpacity,
190
218
  lateralColor: () => lateralColor,
191
219
  lateralFillExpression: () => lateralFillExpression,
192
220
  lightColor: () => lightColor,
@@ -196,6 +224,7 @@ __export(index_exports, {
196
224
  normalizeTheme: () => normalizeTheme,
197
225
  paginate: () => paginate,
198
226
  parseListCodes: () => parseListCodes,
227
+ parseSigseqTerms: () => parseSigseqTerms,
199
228
  parseSymbolFamily: () => parseSymbolFamily,
200
229
  parseWebhook: () => parseWebhook,
201
230
  patternSpriteId: () => patternSpriteId,
@@ -210,12 +239,14 @@ __export(index_exports, {
210
239
  resolveS101Color: () => resolveS101Color,
211
240
  resolveStyle: () => resolveStyle,
212
241
  resolveTargetClass: () => resolveTargetClass,
242
+ rhythmProperties: () => rhythmProperties,
213
243
  s101ArtworkProvenance: () => s101ArtworkProvenance,
214
244
  sanitizeCustomStyleConfig: () => sanitizeCustomStyleConfig,
215
245
  scaleSizeValue: () => scaleSizeValue,
216
246
  scaminToMinZoom: () => scaminToMinZoom,
217
247
  sectorLimitToArcBearing: () => sectorLimitToArcBearing,
218
248
  styleModeBand: () => styleModeBand,
249
+ terrainStackCount: () => terrainStackCount,
219
250
  tileTemplate: () => tileTemplate,
220
251
  uncoveredSectorArcs: () => uncoveredSectorArcs,
221
252
  withinBboxFilter: () => withinBboxFilter,
@@ -592,9 +623,13 @@ var RELIEF_REGION = "CHRT_RELIEF";
592
623
  var BASE_REGION = "CHRT_BASE";
593
624
  var BASE_MINZOOM = 0;
594
625
  var BASE_MAXZOOM = 11;
626
+ var BASE_ATTRIBUTION = "\xA9 CHRT \xB7 Made with Natural Earth \xB7 \xA9 OpenStreetMap contributors";
627
+ var LAND_ATTRIBUTION = "\xA9 OpenStreetMap contributors";
628
+ var RELIEF_ATTRIBUTION = "Elevation: USGS 3D Elevation Program";
595
629
  var RELIEF_MINZOOM = 9;
596
630
  var RELIEF_MAXZOOM = 13;
597
631
  var RELIEF_TILE_SIZE = 512;
632
+ var RELIEF_ENCODING = "terrarium";
598
633
  var BUILD_PARAM = "v";
599
634
  function tileTemplate(base, region, extension, buildId) {
600
635
  const url = `${base}/v1/tiles/${region}/{z}/{x}/{y}.${extension}`;
@@ -630,7 +665,8 @@ function buildSources(opts = {}) {
630
665
  type: "vector",
631
666
  tiles: [tileTemplate(base, LAND_REGION, "mvt", buildFor(LAND_REGION))],
632
667
  minzoom: BUILD_MINZOOM,
633
- maxzoom: LAND_BUILD_MAXZOOM
668
+ maxzoom: LAND_BUILD_MAXZOOM,
669
+ attribution: LAND_ATTRIBUTION
634
670
  };
635
671
  }
636
672
  if (opts.base) {
@@ -638,17 +674,19 @@ function buildSources(opts = {}) {
638
674
  type: "vector",
639
675
  tiles: [tileTemplate(base, BASE_REGION, "mvt", buildFor(BASE_REGION))],
640
676
  minzoom: BASE_MINZOOM,
641
- maxzoom: BASE_MAXZOOM
677
+ maxzoom: BASE_MAXZOOM,
678
+ attribution: BASE_ATTRIBUTION
642
679
  };
643
680
  }
644
681
  if (opts.relief) {
645
682
  sources[RELIEF_REGION] = {
646
683
  type: "raster-dem",
647
684
  tiles: [tileTemplate(base, RELIEF_REGION, "png", buildFor(RELIEF_REGION))],
648
- encoding: "terrarium",
685
+ encoding: RELIEF_ENCODING,
649
686
  tileSize: RELIEF_TILE_SIZE,
650
687
  minzoom: RELIEF_MINZOOM,
651
- maxzoom: RELIEF_MAXZOOM
688
+ maxzoom: RELIEF_MAXZOOM,
689
+ attribution: RELIEF_ATTRIBUTION
652
690
  };
653
691
  }
654
692
  return sources;
@@ -671,6 +709,7 @@ var TilesEndpoint = class {
671
709
  if (opts.mode) params.set("mode", opts.mode);
672
710
  if (opts.land) params.set("land", "1");
673
711
  if (opts.relief) params.set("relief", "1");
712
+ if (opts.units === "imperial") params.set("units", "imperial");
674
713
  const query2 = params.size > 0 ? `?${params.toString()}` : "";
675
714
  return `${this.tileBaseUrl}/v1/styles/${this.region}/style.json${query2}`;
676
715
  }
@@ -796,6 +835,9 @@ var WeatherEndpoint = class {
796
835
  *
797
836
  * `unavailable` names every field the station did not report, so absence is
798
837
  * stated rather than inferred from a missing key.
838
+ *
839
+ * Pass `hours` for the station's recent readings as `series` — NDBC's own
840
+ * readings at NDBC's own instants, never interpolated.
799
841
  */
800
842
  async conditions(options) {
801
843
  return this.http.request(
@@ -804,7 +846,8 @@ var WeatherEndpoint = class {
804
846
  lat: options.lat,
805
847
  lon: options.lon,
806
848
  units: options.units,
807
- station: options.station
849
+ station: options.station,
850
+ hours: options.hours
808
851
  },
809
852
  options.signal
810
853
  );
@@ -1268,10 +1311,10 @@ async function readManifest(manifestPath, fs) {
1268
1311
  if (!manifestPath) return emptyManifest();
1269
1312
  if (!await fs.exists(manifestPath)) return emptyManifest();
1270
1313
  const raw = await fs.readFile(manifestPath);
1271
- const text = new TextDecoder().decode(raw);
1314
+ const text2 = new TextDecoder().decode(raw);
1272
1315
  let parsed;
1273
1316
  try {
1274
- parsed = JSON.parse(text);
1317
+ parsed = JSON.parse(text2);
1275
1318
  } catch {
1276
1319
  throw new OfflineError(
1277
1320
  "unexpected_response",
@@ -1295,8 +1338,8 @@ async function readManifest(manifestPath, fs) {
1295
1338
  };
1296
1339
  }
1297
1340
  async function writeManifest(manifestPath, fs, manifest) {
1298
- const text = JSON.stringify(manifest, null, 2);
1299
- const bytes = new TextEncoder().encode(text);
1341
+ const text2 = JSON.stringify(manifest, null, 2);
1342
+ const bytes = new TextEncoder().encode(text2);
1300
1343
  await fs.writeFile(manifestPath, bytes);
1301
1344
  }
1302
1345
  function usedBytes(manifest, excludeRegion) {
@@ -2261,110 +2304,438 @@ function s57AbbrTable(acronym) {
2261
2304
  return cached;
2262
2305
  }
2263
2306
 
2264
- // src/interact/decode.ts
2265
- function nameAbbrTable(acronym) {
2266
- const names = s57NameTable(acronym);
2267
- const abbrs = s57AbbrTable(acronym);
2268
- const out = {};
2269
- for (const [code, name] of Object.entries(names)) {
2270
- const abbr = abbrs[Number(code)];
2271
- if (abbr !== void 0) out[Number(code)] = { name, abbr };
2272
- }
2273
- return out;
2274
- }
2275
- var COLOUR_HEX = {
2276
- 1: "#ffffff",
2277
- 2: "#333333",
2278
- 3: "#ef4444",
2279
- 4: "#22c55e",
2280
- 5: "#3b82f6",
2281
- 6: "#eab308",
2282
- 7: "#9ca3af",
2283
- 8: "#92400e",
2284
- 9: "#f59e0b",
2285
- 10: "#8b5cf6",
2286
- 11: "#f97316",
2287
- 12: "#d946ef",
2288
- 13: "#f472b6"
2307
+ // src/atlas/s52-colors.ts
2308
+ var S52_DAY = {
2309
+ // Depth
2310
+ NODTA: "#a4a4a4",
2311
+ DEPIT: "#009678",
2312
+ DEPVS: "#7fbfdf",
2313
+ DEPMS: "#a7d4e8",
2314
+ DEPMD: "#cae7f3",
2315
+ DEPDW: "#ecf7fc",
2316
+ // Land / topography
2317
+ LANDA: "#e6d1a1",
2318
+ LANDF: "#c8b078",
2319
+ CHBRN: "#ab7948",
2320
+ CHGRF: "#878787",
2321
+ CHGRD: "#555555",
2322
+ // Chart symbology
2323
+ CHBLK: "#000000",
2324
+ CSTLN: "#000000",
2325
+ OUTLW: "#000000",
2326
+ BKAJ1: "#4d4d4d",
2327
+ // Navigation / traffic
2328
+ TRFCD: "#d200d2",
2329
+ TRFCF: "#e680e6",
2330
+ RESBL: "#0064b4",
2331
+ APTS1: "#d200d2",
2332
+ // Danger / hazard
2333
+ DNGHL: "#ff0000",
2334
+ ISHDG: "#cc0000",
2335
+ // Lights
2336
+ LITRD: "#ee0000",
2337
+ LITGN: "#00c800",
2338
+ LITYW: "#fddc00",
2339
+ LITWT: "#ffffff",
2340
+ LITBL: "#0064ff",
2341
+ LITOR: "#ff8c00",
2342
+ // Buoys / marks
2343
+ RADHI: "#68e456",
2344
+ RADLO: "#3f8a34",
2345
+ PLRTE: "#dc4025",
2346
+ ARPAT: "#3fa56f",
2347
+ ISDNG: "#c545c3",
2348
+ // Chart colours
2349
+ CHRED: "#f15469",
2350
+ CHGRN: "#68e456",
2351
+ CHYLW: "#f4da48",
2352
+ CHMGD: "#c545c3",
2353
+ CHMGF: "#d3a6e9",
2354
+ CHWHT: "#d4eaee",
2355
+ CHCOR: "#eb7d36",
2356
+ // Soundings
2357
+ SNDG1: "#7d898c",
2358
+ SNDG2: "#070707",
2359
+ DEPSC: "#525a5c",
2360
+ DEPCN: "#555555",
2361
+ // Utility / infrastructure
2362
+ UINFD: "#070707",
2363
+ UINFF: "#7d898c",
2364
+ UINFR: "#f15469",
2365
+ UINFG: "#68e456",
2366
+ UINFO: "#eb7d36",
2367
+ UINFB: "#3a78f0",
2368
+ UINFM: "#c545c3",
2369
+ UIBCK: "#d4eaee",
2370
+ UIBDR: "#7d898c",
2371
+ UIAFD: "#73b6ef",
2372
+ UIAFF: "#c9b97a",
2373
+ PIPLN: "#55aa00",
2374
+ // Outlines
2375
+ OUTLL: "#c9b97a",
2376
+ // Miscellaneous
2377
+ SCLBR: "#eb7d36",
2378
+ NINFO: "#eb7d36",
2379
+ ADINF: "#b29f34",
2380
+ RESGR: "#7d898c",
2381
+ SHIPS: "#070707",
2382
+ PSTRK: "#070707",
2383
+ SYTRK: "#7d898c",
2384
+ APLRT: "#eb7d36",
2385
+ CURSR: "#eb7d36",
2386
+ BKAJ2: "#232728",
2387
+ RES01: "#a3b4b7",
2388
+ RES02: "#a3b4b7",
2389
+ RES03: "#a3b4b7"
2289
2390
  };
2290
- var LITCHR_TABLE = nameAbbrTable("LITCHR");
2291
- var COLOUR_TABLE = Object.fromEntries(
2292
- Object.values(COLOUR_CODES).filter((c) => c.abbr !== void 0 && COLOUR_HEX[c.code] !== void 0).map((c) => [c.code, { name: c.name, abbr: c.abbr, hex: COLOUR_HEX[c.code] }])
2293
- );
2294
- var BOYSHP_TABLE = s57NameTable("BOYSHP");
2295
- var CATLAM_TABLE = s57NameTable("CATLAM");
2296
- var CATCAM_TABLE = s57NameTable("CATCAM");
2297
- var CATLMK_TABLE = s57NameTable("CATLMK");
2298
- var CATFOG_TABLE = s57NameTable("CATFOG");
2299
- var CATFOG_ABBR_TABLE = s57AbbrTable("CATFOG");
2300
- var CATFOG_NAUTOPHONE = Object.values(CATFOG_CODES).find(
2301
- (c) => c.name.toLowerCase() === "nautophone"
2302
- )?.code;
2303
- var CATLIT_TABLE = s57NameTable("CATLIT");
2304
- var TOPSHP_TABLE = s57NameTable("TOPSHP");
2305
- var STATUS_TABLE = s57NameTable("STATUS");
2306
- var MARSYS_TABLE = s57NameTable("MARSYS");
2307
- var LAYER_TYPE_MAP = {
2308
- BOYLAT: "Lateral Buoy",
2309
- BOYCAR: "Cardinal Buoy",
2310
- BOYSPP: "Special Purpose Buoy",
2311
- BOYISD: "Isolated Danger Buoy",
2312
- BOYSAW: "Safe Water Buoy",
2313
- BCNLAT: "Lateral Beacon",
2314
- BCNCAR: "Cardinal Beacon",
2315
- BCNSPP: "Special Purpose Beacon",
2316
- BCNISD: "Isolated Danger Beacon",
2317
- BCNSAW: "Safe Water Beacon",
2318
- LIGHTS: "Light",
2319
- DAYMAR: "Daymark",
2320
- FOGSIG: "Fog Signal",
2321
- RTPBCN: "Radar Transponder Beacon",
2322
- RDOSTA: "Radio/Radar Station",
2323
- TOPMAR: "Topmark",
2324
- MORFAC: "Mooring Facility",
2325
- LNDMRK: "Landmark",
2326
- SISTAT: "Signal Station",
2327
- PILBOP: "Pilot Boarding Place",
2328
- HRBFAC: "Harbour Facility",
2329
- SMCFAC: "Small Craft Facility",
2330
- CRANES: "Crane",
2331
- OFSPLF: "Offshore Platform",
2332
- ACHBRT: "Anchor Berth",
2333
- DISMAR: "Distance Mark",
2334
- LITVES: "Light Vessel",
2335
- LITFLT: "Light Float"
2391
+ var S52_DUSK = {
2392
+ NODTA: "#6b6b6b",
2393
+ DEPIT: "#006650",
2394
+ DEPVS: "#487085",
2395
+ DEPMS: "#608a9d",
2396
+ DEPMD: "#7ba6b8",
2397
+ DEPDW: "#95bfd0",
2398
+ LANDA: "#8a7d5e",
2399
+ LANDF: "#7a6a48",
2400
+ CHBRN: "#6e4e30",
2401
+ CHGRF: "#585858",
2402
+ CHGRD: "#3a3a3a",
2403
+ CHBLK: "#b0b0b0",
2404
+ CSTLN: "#b0b0b0",
2405
+ OUTLW: "#b0b0b0",
2406
+ BKAJ1: "#707070",
2407
+ TRFCD: "#a000a0",
2408
+ TRFCF: "#b060b0",
2409
+ RESBL: "#004a88",
2410
+ APTS1: "#a000a0",
2411
+ DNGHL: "#cc0000",
2412
+ ISHDG: "#990000",
2413
+ LITRD: "#bb0000",
2414
+ LITGN: "#009900",
2415
+ LITYW: "#c8b000",
2416
+ LITWT: "#d0d0d0",
2417
+ LITBL: "#004abb",
2418
+ LITOR: "#cc7000",
2419
+ RADHI: "#009940",
2420
+ RADLO: "#2d6330",
2421
+ PLRTE: "#a03020",
2422
+ ARPAT: "#306040",
2423
+ ISDNG: "#903090",
2424
+ CHRED: "#b05060",
2425
+ CHGRN: "#509040",
2426
+ CHYLW: "#b0a030",
2427
+ CHMGD: "#903090",
2428
+ CHMGF: "#905090",
2429
+ CHWHT: "#90a0a0",
2430
+ CHCOR: "#a06030",
2431
+ SNDG1: "#606060",
2432
+ SNDG2: "#b0b0b0",
2433
+ DEPSC: "#606060",
2434
+ DEPCN: "#2E2E2E",
2435
+ UINFD: "#b0b0b0",
2436
+ UINFF: "#606060",
2437
+ UINFR: "#b05060",
2438
+ UINFG: "#509040",
2439
+ UINFO: "#a06030",
2440
+ UINFB: "#304090",
2441
+ UINFM: "#903090",
2442
+ UIBCK: "#1a1a1a",
2443
+ UIBDR: "#606060",
2444
+ UIAFD: "#405060",
2445
+ UIAFF: "#605840",
2446
+ PIPLN: "#3d7a00",
2447
+ OUTLL: "#605840",
2448
+ SCLBR: "#a06030",
2449
+ NINFO: "#a06030",
2450
+ ADINF: "#807020",
2451
+ RESGR: "#606060",
2452
+ SHIPS: "#b0b0b0",
2453
+ PSTRK: "#b0b0b0",
2454
+ SYTRK: "#606060",
2455
+ APLRT: "#a06030",
2456
+ CURSR: "#cc0000",
2457
+ BKAJ2: "#252525",
2458
+ RES01: "#606060",
2459
+ RES02: "#606060",
2460
+ RES03: "#606060"
2336
2461
  };
2337
- var NAV_AID_QUERY_LAYERS = [
2338
- "enc-BOYLAT",
2339
- "enc-BOYCAR",
2340
- "enc-BOYSPP",
2341
- "enc-BOYISD",
2342
- "enc-BOYSAW",
2343
- "enc-BCNLAT",
2344
- "enc-BCNCAR",
2345
- "enc-BCNSPP",
2346
- "enc-BCNISD",
2347
- "enc-BCNSAW",
2348
- "enc-LIGHTS",
2349
- "enc-LIGHTS-point",
2350
- "enc-DAYMAR",
2351
- "enc-FOGSIG",
2352
- "enc-RTPBCN",
2353
- "enc-RDOSTA",
2354
- "enc-TOPMAR",
2355
- "enc-MORFAC",
2356
- "enc-MORFAC-point",
2357
- "enc-LNDMRK",
2358
- "enc-LNDMRK-point",
2359
- "enc-SISTAT",
2360
- "enc-SISTAT-point",
2361
- "enc-PILBOP",
2362
- "enc-PILBOP-point",
2363
- "enc-HRBFAC",
2364
- "enc-SMCFAC",
2365
- "enc-CRANES",
2366
- "enc-CRANES-point",
2367
- "enc-OFSPLF",
2462
+ var S52_NIGHT = {
2463
+ NODTA: "#3a3a32",
2464
+ DEPIT: "#003828",
2465
+ DEPVS: "#356577",
2466
+ DEPMS: "#265066",
2467
+ DEPMD: "#102e44",
2468
+ DEPDW: "#051a28",
2469
+ LANDA: "#3a3020",
2470
+ LANDF: "#302818",
2471
+ CHBRN: "#382410",
2472
+ CHGRF: "#2a2a22",
2473
+ CHGRD: "#1e1e18",
2474
+ CHBLK: "#c8aa78",
2475
+ CSTLN: "#c8aa78",
2476
+ OUTLW: "#c8aa78",
2477
+ BKAJ1: "#504830",
2478
+ TRFCD: "#6e006e",
2479
+ TRFCF: "#803880",
2480
+ RESBL: "#002a50",
2481
+ APTS1: "#6e006e",
2482
+ DNGHL: "#a00000",
2483
+ ISHDG: "#700000",
2484
+ LITRD: "#880000",
2485
+ LITGN: "#006600",
2486
+ LITYW: "#887800",
2487
+ LITWT: "#a09878",
2488
+ LITBL: "#003088",
2489
+ LITOR: "#884800",
2490
+ RADHI: "#005528",
2491
+ RADLO: "#1a3a20",
2492
+ PLRTE: "#6e2018",
2493
+ ARPAT: "#1a3028",
2494
+ ISDNG: "#481848",
2495
+ CHRED: "#682030",
2496
+ CHGRN: "#285020",
2497
+ CHYLW: "#685828",
2498
+ CHMGD: "#481848",
2499
+ CHMGF: "#482848",
2500
+ CHWHT: "#484840",
2501
+ CHCOR: "#683818",
2502
+ SNDG1: "#383028",
2503
+ SNDG2: "#c8aa78",
2504
+ DEPSC: "#383028",
2505
+ // ⚠ DELIBERATE DEVIATION from the published S-52 NIGHT value (#1f2223),
2506
+ // recorded here because `docs/validation/s52-spec-gap-matrix.json` tracks
2507
+ // this token as `value-differs` and the CHRTstyles programme is bringing
2508
+ // colours TO spec — this one moves further away, on purpose.
2509
+ //
2510
+ // Spec NIGHT DEPCN is near-black, which measures ~1.1:1 against this
2511
+ // palette's own deep water (#051a28). S-52's NIGHT table assumes a
2512
+ // dark-adapted eye in a darkened wheelhouse; on a phone or a laptop at
2513
+ // anchor it is not dim, it is absent — reported as "can't see bottom
2514
+ // contours in night mode only". A contour nobody can see fails the
2515
+ // standard's intent while matching its number.
2516
+ //
2517
+ // The palette already makes this trade elsewhere and in the same
2518
+ // direction: NIGHT's CHBLK/CSTLN/OUTLW are #c8aa78, light warm linework on
2519
+ // dark water. This value is that family, dimmed to stay subordinate to the
2520
+ // coastline. Revisit with the CHRTstyles programme, which owns the
2521
+ // spec-vs-legibility call across all 63 tokens.
2522
+ DEPCN: "#A68E84",
2523
+ UINFD: "#c8aa78",
2524
+ UINFF: "#383028",
2525
+ UINFR: "#682030",
2526
+ UINFG: "#285020",
2527
+ UINFO: "#683818",
2528
+ UINFB: "#182848",
2529
+ UINFM: "#481848",
2530
+ UIBCK: "#0a0a08",
2531
+ UIBDR: "#383028",
2532
+ UIAFD: "#182838",
2533
+ UIAFF: "#302818",
2534
+ PIPLN: "#284800",
2535
+ OUTLL: "#302818",
2536
+ SCLBR: "#683818",
2537
+ NINFO: "#683818",
2538
+ ADINF: "#483818",
2539
+ RESGR: "#383028",
2540
+ SHIPS: "#c8aa78",
2541
+ PSTRK: "#c8aa78",
2542
+ SYTRK: "#484030",
2543
+ APLRT: "#683818",
2544
+ CURSR: "#a00000",
2545
+ BKAJ2: "#0a0a08",
2546
+ RES01: "#383028",
2547
+ RES02: "#383028",
2548
+ RES03: "#383028"
2549
+ };
2550
+ var S52_COLORS = S52_DAY;
2551
+ var COLOR_TABLES = {
2552
+ DAY: S52_DAY,
2553
+ DUSK: S52_DUSK,
2554
+ NIGHT: S52_NIGHT
2555
+ };
2556
+ function getColorTable(name) {
2557
+ return COLOR_TABLES[name];
2558
+ }
2559
+ function resolveColor(token, table = "DAY") {
2560
+ const t = COLOR_TABLES[table];
2561
+ return t[token] ?? (table === "NIGHT" ? "#c8aa78" : "#000000");
2562
+ }
2563
+
2564
+ // src/interact/light-sigseq.ts
2565
+ var SIGSEQ_MAX_TERMS = 24;
2566
+ var SIGPER_TOLERANCE_SECONDS = 0.06;
2567
+ var LAMP_TICK_MS = 100;
2568
+ var TERM = /^\d+(?:\.\d+)?$/;
2569
+ function text(v) {
2570
+ if (v === null || v === void 0) return null;
2571
+ const s = String(v).trim();
2572
+ return s === "" ? null : s;
2573
+ }
2574
+ function parseSigseqTerms(sigseq, sigper) {
2575
+ const raw = text(sigseq);
2576
+ if (raw === null) return { refusal: "absent" };
2577
+ const terms = raw.split("+");
2578
+ if (terms.length === 0 || terms.length > SIGSEQ_MAX_TERMS) return { refusal: "malformed" };
2579
+ const phases = [];
2580
+ for (const term of terms) {
2581
+ const t = term.trim();
2582
+ if (t === "") return { refusal: "malformed" };
2583
+ const isEclipse = t.startsWith("(") && t.endsWith(")");
2584
+ const body = (isEclipse ? t.slice(1, -1) : t).trim();
2585
+ if (!TERM.test(body)) return { refusal: "malformed" };
2586
+ const seconds = Number(body);
2587
+ if (!(seconds > 0) || !Number.isFinite(seconds)) return { refusal: "nonPositiveDuration" };
2588
+ phases.push({ seconds, isOn: !isEclipse });
2589
+ }
2590
+ const sum = phases.reduce((t, p) => t + p.seconds, 0);
2591
+ if (!(sum > 0)) return { refusal: "nonPositiveDuration" };
2592
+ if (sigper !== null && sigper > 0) {
2593
+ if (Math.abs(sum - sigper) > SIGPER_TOLERANCE_SECONDS) return { refusal: "sigperMismatch" };
2594
+ return { phases, periodSeconds: sigper, checkedAgainstSigper: true };
2595
+ }
2596
+ return { phases, periodSeconds: sum, checkedAgainstSigper: false };
2597
+ }
2598
+ function rhythmProperties(parsed, onColoursHex) {
2599
+ if (parsed === null || parsed.phases.length === 0) return { rp: 0, rn: 0 };
2600
+ const out = { rp: parsed.periodSeconds, rn: parsed.phases.length };
2601
+ let end = 0;
2602
+ let onIndex = 0;
2603
+ parsed.phases.forEach((p, i) => {
2604
+ end += p.seconds;
2605
+ const k = i + 1;
2606
+ out[`r${k}`] = Math.round(end * 1e3) / 1e3;
2607
+ out[`o${k}`] = p.isOn ? 1 : 0;
2608
+ if (p.isOn) {
2609
+ out[`c${k}`] = onColoursHex[onIndex % Math.max(1, onColoursHex.length)] ?? "#ffffff";
2610
+ onIndex += 1;
2611
+ }
2612
+ });
2613
+ return out;
2614
+ }
2615
+ function phaseExpr(nowSeconds2) {
2616
+ return ["%", nowSeconds2, ["max", ["get", "rp"], 1e-3]];
2617
+ }
2618
+ function lampRhythmOpacity(nowSeconds2, base) {
2619
+ const cases = ["case", ["<=", ["get", "rn"], 0], base];
2620
+ for (let k = 1; k <= SIGSEQ_MAX_TERMS; k++) {
2621
+ cases.push(["<", ["var", "ph"], ["coalesce", ["get", `r${k}`], 1e9]], ["*", base, ["coalesce", ["get", `o${k}`], 1]]);
2622
+ }
2623
+ cases.push(0);
2624
+ return ["let", "ph", phaseExpr(nowSeconds2), cases];
2625
+ }
2626
+ function lampRhythmColour(nowSeconds2, fallback) {
2627
+ const cases = ["case", ["<=", ["get", "rn"], 0], fallback];
2628
+ for (let k = 1; k <= SIGSEQ_MAX_TERMS; k++) {
2629
+ cases.push(["<", ["var", "ph"], ["coalesce", ["get", `r${k}`], 1e9]], ["coalesce", ["get", `c${k}`], fallback]);
2630
+ }
2631
+ cases.push(fallback);
2632
+ return ["let", "ph", phaseExpr(nowSeconds2), cases];
2633
+ }
2634
+
2635
+ // src/interact/decode.ts
2636
+ function nameAbbrTable(acronym) {
2637
+ const names = s57NameTable(acronym);
2638
+ const abbrs = s57AbbrTable(acronym);
2639
+ const out = {};
2640
+ for (const [code, name] of Object.entries(names)) {
2641
+ const abbr = abbrs[Number(code)];
2642
+ if (abbr !== void 0) out[Number(code)] = { name, abbr };
2643
+ }
2644
+ return out;
2645
+ }
2646
+ var COLOUR_HEX = {
2647
+ 1: "#ffffff",
2648
+ 2: "#333333",
2649
+ 3: "#ef4444",
2650
+ 4: "#22c55e",
2651
+ 5: "#3b82f6",
2652
+ 6: "#eab308",
2653
+ 7: "#9ca3af",
2654
+ 8: "#92400e",
2655
+ 9: "#f59e0b",
2656
+ 10: "#8b5cf6",
2657
+ 11: "#f97316",
2658
+ 12: "#d946ef",
2659
+ 13: "#f472b6"
2660
+ };
2661
+ var LITCHR_TABLE = nameAbbrTable("LITCHR");
2662
+ var COLOUR_TABLE = Object.fromEntries(
2663
+ Object.values(COLOUR_CODES).filter((c) => c.abbr !== void 0 && COLOUR_HEX[c.code] !== void 0).map((c) => [c.code, { name: c.name, abbr: c.abbr, hex: COLOUR_HEX[c.code] }])
2664
+ );
2665
+ var BOYSHP_TABLE = s57NameTable("BOYSHP");
2666
+ var CATLAM_TABLE = s57NameTable("CATLAM");
2667
+ var CATCAM_TABLE = s57NameTable("CATCAM");
2668
+ var CATLMK_TABLE = s57NameTable("CATLMK");
2669
+ var CATFOG_TABLE = s57NameTable("CATFOG");
2670
+ var CATFOG_ABBR_TABLE = s57AbbrTable("CATFOG");
2671
+ var CATFOG_NAUTOPHONE = Object.values(CATFOG_CODES).find(
2672
+ (c) => c.name.toLowerCase() === "nautophone"
2673
+ )?.code;
2674
+ var CATLIT_TABLE = s57NameTable("CATLIT");
2675
+ var TOPSHP_TABLE = s57NameTable("TOPSHP");
2676
+ var STATUS_TABLE = s57NameTable("STATUS");
2677
+ var MARSYS_TABLE = s57NameTable("MARSYS");
2678
+ var LAYER_TYPE_MAP = {
2679
+ BOYLAT: "Lateral Buoy",
2680
+ BOYCAR: "Cardinal Buoy",
2681
+ BOYSPP: "Special Purpose Buoy",
2682
+ BOYISD: "Isolated Danger Buoy",
2683
+ BOYSAW: "Safe Water Buoy",
2684
+ BCNLAT: "Lateral Beacon",
2685
+ BCNCAR: "Cardinal Beacon",
2686
+ BCNSPP: "Special Purpose Beacon",
2687
+ BCNISD: "Isolated Danger Beacon",
2688
+ BCNSAW: "Safe Water Beacon",
2689
+ LIGHTS: "Light",
2690
+ DAYMAR: "Daymark",
2691
+ FOGSIG: "Fog Signal",
2692
+ RTPBCN: "Radar Transponder Beacon",
2693
+ RDOSTA: "Radio/Radar Station",
2694
+ TOPMAR: "Topmark",
2695
+ MORFAC: "Mooring Facility",
2696
+ LNDMRK: "Landmark",
2697
+ SISTAT: "Signal Station",
2698
+ PILBOP: "Pilot Boarding Place",
2699
+ HRBFAC: "Harbour Facility",
2700
+ SMCFAC: "Small Craft Facility",
2701
+ CRANES: "Crane",
2702
+ OFSPLF: "Offshore Platform",
2703
+ ACHBRT: "Anchor Berth",
2704
+ DISMAR: "Distance Mark",
2705
+ LITVES: "Light Vessel",
2706
+ LITFLT: "Light Float"
2707
+ };
2708
+ var NAV_AID_QUERY_LAYERS = [
2709
+ "enc-BOYLAT",
2710
+ "enc-BOYCAR",
2711
+ "enc-BOYSPP",
2712
+ "enc-BOYISD",
2713
+ "enc-BOYSAW",
2714
+ "enc-BCNLAT",
2715
+ "enc-BCNCAR",
2716
+ "enc-BCNSPP",
2717
+ "enc-BCNISD",
2718
+ "enc-BCNSAW",
2719
+ "enc-LIGHTS",
2720
+ "enc-LIGHTS-point",
2721
+ "enc-DAYMAR",
2722
+ "enc-FOGSIG",
2723
+ "enc-RTPBCN",
2724
+ "enc-RDOSTA",
2725
+ "enc-TOPMAR",
2726
+ "enc-MORFAC",
2727
+ "enc-MORFAC-point",
2728
+ "enc-LNDMRK",
2729
+ "enc-LNDMRK-point",
2730
+ "enc-SISTAT",
2731
+ "enc-SISTAT-point",
2732
+ "enc-PILBOP",
2733
+ "enc-PILBOP-point",
2734
+ "enc-HRBFAC",
2735
+ "enc-SMCFAC",
2736
+ "enc-CRANES",
2737
+ "enc-CRANES-point",
2738
+ "enc-OFSPLF",
2368
2739
  "enc-OFSPLF-point",
2369
2740
  "enc-ACHBRT",
2370
2741
  "enc-DISMAR",
@@ -3167,14 +3538,8 @@ function decodeNavAidProps(layerName, props, coords) {
3167
3538
  const valnmr = props.VALNMR != null ? Number(props.VALNMR) : null;
3168
3539
  const nominalRange = valnmr != null && !isNaN(valnmr) && valnmr > 0 ? valnmr : null;
3169
3540
  if (isSectorLight) {
3170
- sectors.push({
3171
- sectorStart: sectr1,
3172
- sectorEnd: sectr2,
3173
- colour: colourEntry?.name ?? "White",
3174
- colourHex: colourEntry?.hex ?? "#ffffff",
3175
- notation,
3176
- nominalRange
3177
- });
3541
+ const sec = sectorFromProps(props, sectr1, sectr2, "DAY", false);
3542
+ if (sec) sectors.push({ ...sec, notation });
3178
3543
  }
3179
3544
  return {
3180
3545
  name,
@@ -3192,6 +3557,7 @@ function decodeNavAidProps(layerName, props, coords) {
3192
3557
  lightCharacterFull: litchrEntry?.name ?? null,
3193
3558
  period: sigper,
3194
3559
  group,
3560
+ sigseq: props.SIGSEQ != null && String(props.SIGSEQ).trim() !== "" ? String(props.SIGSEQ).trim() : null,
3195
3561
  fogSignal: fogSignal2,
3196
3562
  fogSignalAbbr,
3197
3563
  radarReflector,
@@ -3222,6 +3588,18 @@ function buildNotation(litchrAbbr, colourAbbrs, sigper, group) {
3222
3588
  function buildFlashPattern(decoded) {
3223
3589
  const litchr = decoded.lightCharacter;
3224
3590
  if (!litchr) return null;
3591
+ const charted = parseSigseqTerms(decoded.sigseq, decoded.period);
3592
+ if (!("refusal" in charted)) {
3593
+ const colours2 = decoded.colourHexList.length > 0 ? decoded.colourHexList : [decoded.lightColourHex];
3594
+ let onIndex = 0;
3595
+ const chartedPhases = charted.phases.map((p) => {
3596
+ if (!p.isOn) return { on: false, duration: p.seconds * 1e3 };
3597
+ const color = colours2[onIndex % colours2.length];
3598
+ onIndex += 1;
3599
+ return { on: true, duration: p.seconds * 1e3, color };
3600
+ });
3601
+ return { phases: chartedPhases, totalCycle: charted.periodSeconds * 1e3, notation: decoded.notation, timing: "charted" };
3602
+ }
3225
3603
  const period = decoded.period && decoded.period > 0 ? decoded.period * 1e3 : 6e3;
3226
3604
  const groups = decoded.group ?? [1];
3227
3605
  const totalFlashes = groups.reduce((a, b) => a + b, 0);
@@ -3390,7 +3768,7 @@ function buildFlashPattern(decoded) {
3390
3768
  }
3391
3769
  }
3392
3770
  const totalCycle = phases.reduce((sum, p) => sum + p.duration, 0);
3393
- return { phases, totalCycle, notation };
3771
+ return { timing: "invented", phases, totalCycle, notation };
3394
3772
  }
3395
3773
  function uncoveredSectorArcs(sectors) {
3396
3774
  const norm = (d) => (d % 360 + 360) % 360;
@@ -3429,7 +3807,70 @@ function uncoveredSectorArcs(sectors) {
3429
3807
  }
3430
3808
  return gaps.filter(([a, b]) => Math.abs(b - a) >= 0.05 || b < a).map(([a, b]) => ({ start: norm(a), end: norm(b) }));
3431
3809
  }
3432
- function buildSectorsFromFeatures(features) {
3810
+ function parseCodes(val) {
3811
+ if (val == null) return [];
3812
+ const str = String(val).trim();
3813
+ let parts;
3814
+ if (str.startsWith("[")) {
3815
+ try {
3816
+ const arr = JSON.parse(str);
3817
+ parts = Array.isArray(arr) ? arr.map(String) : [];
3818
+ } catch {
3819
+ parts = str.split(/[,;]/);
3820
+ }
3821
+ } else {
3822
+ parts = str.split(/[,;]/);
3823
+ }
3824
+ return parts.map((p) => Number(p.trim())).filter((n) => !isNaN(n));
3825
+ }
3826
+ var OBSCURED_LITVIS = /* @__PURE__ */ new Set([3, 7, 8]);
3827
+ function sectorArcColourToken(colourCodes, obscured) {
3828
+ if (obscured) return "CHBRN";
3829
+ const has = (c) => colourCodes.includes(c);
3830
+ if (colourCodes.length === 1) {
3831
+ if (has(3)) return "LITRD";
3832
+ if (has(4)) return "LITGN";
3833
+ if (has(1) || has(6) || has(11)) return "LITYW";
3834
+ return "CHMGD";
3835
+ }
3836
+ if (colourCodes.length === 2) {
3837
+ if (has(1) && has(3)) return "LITRD";
3838
+ if (has(1) && has(4)) return "LITGN";
3839
+ return "CHMGD";
3840
+ }
3841
+ return "CHMGD";
3842
+ }
3843
+ function sectorFromProps(props, s1, s2, ct, allRound) {
3844
+ const colourCodes = parseCodes(props.COLOUR);
3845
+ const entries = colourCodes.map((c) => COLOUR_TABLE[c]).filter((e) => e != null);
3846
+ if (allRound && entries.length === 0) return null;
3847
+ const obscured = parseCodes(props.LITVIS).some((v) => OBSCURED_LITVIS.has(v));
3848
+ const litchrCode = parseCode(props.LITCHR);
3849
+ const le = litchrCode ? LITCHR_TABLE[litchrCode] : null;
3850
+ const sigper = props.SIGPER != null ? Number(props.SIGPER) : null;
3851
+ const group = parseGroup(props.SIGGRP);
3852
+ const base = buildNotation(le?.abbr ?? null, entries.length > 0 ? [entries.map((e) => e.abbr).join("")] : [], sigper, group);
3853
+ const notation = obscured && base ? `${base} (obsc)` : base;
3854
+ const valnmr = props.VALNMR != null ? Number(props.VALNMR) : null;
3855
+ const nominalRange = valnmr != null && !isNaN(valnmr) && valnmr > 0 ? valnmr : null;
3856
+ const arcColourToken = sectorArcColourToken(colourCodes, obscured);
3857
+ return {
3858
+ sectorStart: s1,
3859
+ sectorEnd: s2,
3860
+ colour: entries[0]?.name ?? "White",
3861
+ colourHex: entries[0]?.hex ?? "#ffffff",
3862
+ notation,
3863
+ nominalRange,
3864
+ colourCodes,
3865
+ arcColourToken,
3866
+ arcColourHex: resolveColor(arcColourToken, ct),
3867
+ obscured,
3868
+ sigseq: props.SIGSEQ != null && String(props.SIGSEQ).trim() !== "" ? String(props.SIGSEQ).trim() : null,
3869
+ sigper: sigper != null && !isNaN(sigper) && sigper > 0 ? sigper : null,
3870
+ ...allRound ? { allRound: true } : {}
3871
+ };
3872
+ }
3873
+ function buildSectorsFromFeatures(features, ct = "DAY") {
3433
3874
  const sectors = [];
3434
3875
  const allRound = [];
3435
3876
  for (const f of features) {
@@ -3440,47 +3881,15 @@ function buildSectorsFromFeatures(features) {
3440
3881
  if (props.SECTR1 == null && props.SECTR2 == null && parseCode(props.COLOUR) != null) allRound.push(props);
3441
3882
  continue;
3442
3883
  }
3443
- const colourCode = parseCode(props.COLOUR);
3444
- const ce = colourCode ? COLOUR_TABLE[colourCode] : null;
3445
- const litchrCode = parseCode(props.LITCHR);
3446
- const le = litchrCode ? LITCHR_TABLE[litchrCode] : null;
3447
- const sigper = props.SIGPER != null ? Number(props.SIGPER) : null;
3448
- const group = parseGroup(props.SIGGRP);
3449
- const notation = buildNotation(le?.abbr ?? null, ce ? [ce.abbr] : [], sigper, group);
3450
- const valnmr = props.VALNMR != null ? Number(props.VALNMR) : null;
3451
- const nominalRange = valnmr != null && !isNaN(valnmr) && valnmr > 0 ? valnmr : null;
3452
- sectors.push({
3453
- sectorStart: s1,
3454
- sectorEnd: s2,
3455
- colour: ce?.name ?? "White",
3456
- colourHex: ce?.hex ?? "#ffffff",
3457
- notation,
3458
- nominalRange
3459
- });
3884
+ const sec = sectorFromProps(props, s1, s2, ct, false);
3885
+ if (sec) sectors.push(sec);
3460
3886
  }
3461
3887
  if (sectors.length > 0 && allRound.length > 0) {
3462
3888
  const gaps = uncoveredSectorArcs(sectors);
3463
3889
  for (const props of allRound) {
3464
- const colourCode = parseCode(props.COLOUR);
3465
- const ce = colourCode ? COLOUR_TABLE[colourCode] : null;
3466
- if (!ce) continue;
3467
- const litchrCode = parseCode(props.LITCHR);
3468
- const le = litchrCode ? LITCHR_TABLE[litchrCode] : null;
3469
- const sigper = props.SIGPER != null ? Number(props.SIGPER) : null;
3470
- const group = parseGroup(props.SIGGRP);
3471
- const notation = buildNotation(le?.abbr ?? null, [ce.abbr], sigper, group);
3472
- const valnmr = props.VALNMR != null ? Number(props.VALNMR) : null;
3473
- const nominalRange = valnmr != null && !isNaN(valnmr) && valnmr > 0 ? valnmr : null;
3474
3890
  for (const gap of gaps) {
3475
- sectors.push({
3476
- sectorStart: gap.start,
3477
- sectorEnd: gap.end,
3478
- colour: ce.name,
3479
- colourHex: ce.hex,
3480
- notation,
3481
- nominalRange,
3482
- allRound: true
3483
- });
3891
+ const sec = sectorFromProps(props, gap.start, gap.end, ct, true);
3892
+ if (sec) sectors.push(sec);
3484
3893
  }
3485
3894
  }
3486
3895
  }
@@ -3530,12 +3939,16 @@ function buildSectorArcGeoJSON(center, startBearing, endBearing, radiusNm = 0.5,
3530
3939
  }
3531
3940
  function buildSectorArcsFC(center, sectors, radiusNm = 0.4, innerRadiusFrac = 0.92) {
3532
3941
  const innerNm = innerRadiusFrac > 0 ? radiusNm * innerRadiusFrac : 0;
3533
- const features = sectors.map((sec, i) => {
3942
+ const drawn = sectors.filter((sec) => !sec.allRound);
3943
+ const features = drawn.map((sec, i) => {
3534
3944
  const arc = buildSectorArcGeoJSON(center, sec.sectorStart, sec.sectorEnd, radiusNm, innerNm);
3535
3945
  arc.properties = {
3536
3946
  sectorIndex: i,
3537
3947
  colour: sec.colour,
3538
3948
  colourHex: sec.colourHex,
3949
+ arcColourHex: sec.arcColourHex,
3950
+ arcColourToken: sec.arcColourToken,
3951
+ obscured: sec.obscured,
3539
3952
  notation: sec.notation,
3540
3953
  sectorStart: sec.sectorStart,
3541
3954
  sectorEnd: sec.sectorEnd,
@@ -3548,7 +3961,7 @@ function buildSectorArcsFC(center, sectors, radiusNm = 0.4, innerRadiusFrac = 0.
3548
3961
  const seenBearings = /* @__PURE__ */ new Set();
3549
3962
  const TICK_OUTER_OVERHANG = 1.05;
3550
3963
  const TICK_INNER_OVERHANG = 0.93;
3551
- for (const sec of sectors) {
3964
+ for (const sec of drawn) {
3552
3965
  for (const bearing of [sec.sectorStart, sec.sectorEnd]) {
3553
3966
  const rad = sectorLimitToArcBearing(bearing) * Math.PI / 180;
3554
3967
  const radiusDeg = radiusNm / 60;
@@ -4418,10 +4831,10 @@ function ensureStyles(doc) {
4418
4831
  doc.head.appendChild(style);
4419
4832
  styleInjected = true;
4420
4833
  }
4421
- function el(doc, tag, className, text) {
4834
+ function el(doc, tag, className, text2) {
4422
4835
  const node = doc.createElement(tag);
4423
4836
  node.className = className;
4424
- if (text != null) node.textContent = text;
4837
+ if (text2 != null) node.textContent = text2;
4425
4838
  return node;
4426
4839
  }
4427
4840
  function animateDot(dot, pattern) {
@@ -4581,466 +4994,209 @@ ${notice.dataCurrency}`;
4581
4994
  if (position === "static" || position === "") {
4582
4995
  container.style.position = "relative";
4583
4996
  }
4584
- container.appendChild(root);
4585
- this.root = root;
4586
- this.lngLat = lngLat;
4587
- this.place();
4588
- this.map.on("move", this.reposition);
4589
- }
4590
- place() {
4591
- if (!this.root || !this.lngLat) return;
4592
- const { x, y } = this.map.project(this.lngLat);
4593
- this.root.style.left = `${x}px`;
4594
- this.root.style.top = `${y}px`;
4595
- }
4596
- close() {
4597
- if (this.stopAnimation) {
4598
- this.stopAnimation();
4599
- this.stopAnimation = null;
4600
- }
4601
- if (this.root) {
4602
- this.map.off("move", this.reposition);
4603
- this.root.remove();
4604
- this.root = null;
4605
- }
4606
- this.lngLat = null;
4607
- }
4608
- };
4609
-
4610
- // src/interact/index.ts
4611
- var InteractClient = class {
4612
- constructor(http) {
4613
- this.http = http;
4614
- this.entitlementClient = new EntitlementClient(http);
4615
- }
4616
- entitlementClient;
4617
- /**
4618
- * Check the CHRTinteract entitlement for this API key (server-side, plan
4619
- * derived, cached per client). Throws `InteractError` with code
4620
- * `interact_not_enabled` for Free-plan teams.
4621
- */
4622
- async entitlement() {
4623
- return this.entitlementClient.check();
4624
- }
4625
- /**
4626
- * Decode a chart feature (from `queryRenderedFeatures`, the features API,
4627
- * or plain `{ layer, properties }`) into a typed {@link InspectResult}.
4628
- *
4629
- * The first call performs the server-side entitlement check. With
4630
- * `{ enrich: true }` the result is upgraded with full S-57 properties and
4631
- * source-cell provenance (edition / update / issue date) via the features
4632
- * API — metered against the key's `featureCalls` quota.
4633
- *
4634
- * @example
4635
- * const result = await chrt.interact.inspect(feature);
4636
- * if (result.kind === "navaid") console.log(result.characteristic); // "Fl R 4s"
4637
- */
4638
- async inspect(feature, opts) {
4639
- await this.entitlementClient.check();
4640
- const result = buildInspectResult(feature, opts);
4641
- if (opts?.enrich) {
4642
- return enrichResult(this.http, result, opts.signal);
4643
- }
4644
- return result;
4645
- }
4646
- /**
4647
- * Collect EVERY inspectable feature under a point on a CHRT map —
4648
- * deduplicated across tile boundaries by S-57 identity (`LNAM`) and
4649
- * ordered by class priority (point targets > lines > areas, safety
4650
- * classes first within each band). Area features never preempt the buoy
4651
- * on top of them but always stay selectable: a click on open water
4652
- * resolves to its DEPARE.
4653
- *
4654
- * `point` is a screen pixel point (`e.point` from a map click) or
4655
- * `[lng, lat]`. Point/line targets are picked up within
4656
- * `opts.tolerance` px (default 8); polygon candidates require the exact
4657
- * point to fall inside them.
4658
- *
4659
- * The first call performs the server-side entitlement check.
4660
- *
4661
- * @example
4662
- * const { candidates, select } = await chrt.interact.inspectAll(map, e.point);
4663
- * const result = await select(candidates[0]); // full decode (+ merge)
4664
- */
4665
- async inspectAll(map, point, opts = {}) {
4666
- if (!map || typeof map.queryRenderedFeatures !== "function") {
4667
- throw new InteractError(
4668
- "unexpected_response",
4669
- "inspectAll(): expected a MapLibre / Mapbox GL map instance"
4670
- );
4671
- }
4672
- await this.entitlementClient.check();
4673
- const screen = Array.isArray(point) ? map.project([point[0], point[1]]) : point;
4674
- const lngLat = Array.isArray(point) ? { lng: point[0], lat: point[1] } : map.unproject?.([point.x, point.y]) ?? null;
4675
- const at = { point: screen, lngLat };
4676
- const candidates = collectClickCandidates(map, screen, opts);
4677
- const http = this.http;
4678
- return {
4679
- candidates,
4680
- select: async (candidate, selectOpts) => {
4681
- const result = decodeCandidate(map, candidate, at, opts);
4682
- if (selectOpts?.enrich) {
4683
- return enrichResult(http, result, selectOpts.signal);
4684
- }
4685
- return result;
4686
- }
4687
- };
4688
- }
4689
- /**
4690
- * Attach click-to-inspect to a MapLibre / Mapbox GL map rendering CHRT
4691
- * tiles. Resolves once the server-side entitlement check passes; rejects
4692
- * with `InteractError` (`interact_not_enabled` on Free plans) otherwise —
4693
- * no listeners are wired on denial.
4694
- *
4695
- * Headless-first: pass `onInspect` to receive every decoded
4696
- * {@link InspectResult} and render your own UI. Without callbacks, the
4697
- * built-in minimal popup is shown (opt back in explicitly with
4698
- * `popup: true` to have both).
4699
- *
4700
- * A click that hits SEVERAL features (deduplicated by S-57 identity)
4701
- * disambiguates instead of silently taking the topmost:
4702
- * - `onCandidates` provided → it receives the priority-ordered list;
4703
- * resolve a choice with `ctx.select(candidate)`.
4704
- * - built-in popup → shows a candidate list; picking a row opens the
4705
- * result card.
4706
- * - `onInspect` only → the top candidate is inspected immediately, and
4707
- * clicking the SAME spot again cycles to the next candidate — so an
4708
- * area feature under a buoy is reachable with a second click.
4709
- *
4710
- * @example
4711
- * const handle = await chrt.interact.attach(map, {
4712
- * onInspect: (result) => console.log(result.title, result.characteristic),
4713
- * });
4714
- * // later: handle.detach();
4715
- */
4716
- async attach(map, opts = {}) {
4717
- if (!map || typeof map.queryRenderedFeatures !== "function" || typeof map.on !== "function") {
4718
- throw new InteractError(
4719
- "unexpected_response",
4720
- "attach(): expected a MapLibre / Mapbox GL map instance"
4721
- );
4722
- }
4723
- await this.entitlementClient.check();
4724
- const showPopup = opts.popup ?? (!opts.onInspect && !opts.onCandidates);
4725
- const popup = showPopup ? new InteractPopup(map) : null;
4726
- const http = this.http;
4727
- let cycle = null;
4728
- const samePoint = (a, b) => {
4729
- const pad = opts.tolerance ?? DEFAULT_CLICK_TOLERANCE;
4730
- return Math.abs(a.x - b.x) <= pad && Math.abs(a.y - b.y) <= pad;
4731
- };
4732
- const select = (candidate, e) => {
4733
- const result = decodeCandidate(map, candidate, e, opts);
4734
- const finish = (r) => {
4735
- popup?.show(r, r.coordinates ?? [e.lngLat.lng, e.lngLat.lat]);
4736
- opts.onInspect?.(r, { lngLat: e.lngLat, point: e.point, feature: candidate.feature });
4737
- return r;
4738
- };
4739
- if (opts.enrich) {
4740
- return enrichResult(http, result).then(finish).catch(() => finish(result));
4741
- }
4742
- return Promise.resolve(finish(result));
4743
- };
4744
- const clickHandler = (e) => {
4745
- const candidates = collectClickCandidates(map, e.point, opts);
4746
- if (candidates.length === 0) {
4747
- cycle = null;
4748
- popup?.close();
4749
- opts.onMiss?.({ lngLat: e.lngLat });
4750
- return;
4751
- }
4752
- if (candidates.length === 1) {
4753
- cycle = null;
4754
- void select(candidates[0], e);
4755
- return;
4756
- }
4757
- if (opts.onCandidates || popup) {
4758
- cycle = null;
4759
- if (popup) {
4760
- popup.showList(candidates, [e.lngLat.lng, e.lngLat.lat], (c) => void select(c, e));
4761
- }
4762
- opts.onCandidates?.(candidates, {
4763
- lngLat: e.lngLat,
4764
- point: e.point,
4765
- select: (c) => select(c, e)
4766
- });
4767
- return;
4768
- }
4769
- const key = candidates.map((c) => c.identity ?? c.layerId).join("/");
4770
- if (cycle && cycle.key === key && samePoint(cycle.point, e.point)) {
4771
- cycle.index = (cycle.index + 1) % candidates.length;
4772
- cycle.point = e.point;
4773
- } else {
4774
- cycle = { key, point: e.point, index: 0 };
4775
- }
4776
- void select(candidates[cycle.index], e);
4777
- };
4778
- map.on("click", clickHandler);
4779
- return {
4780
- detach: () => {
4781
- map.off("click", clickHandler);
4782
- popup?.close();
4783
- }
4784
- };
4785
- }
4786
- };
4787
-
4788
- // src/atlas/s52-colors.ts
4789
- var S52_DAY = {
4790
- // Depth
4791
- NODTA: "#a4a4a4",
4792
- DEPIT: "#009678",
4793
- DEPVS: "#7fbfdf",
4794
- DEPMS: "#a7d4e8",
4795
- DEPMD: "#cae7f3",
4796
- DEPDW: "#ecf7fc",
4797
- // Land / topography
4798
- LANDA: "#e6d1a1",
4799
- LANDF: "#c8b078",
4800
- CHBRN: "#ab7948",
4801
- CHGRF: "#878787",
4802
- CHGRD: "#555555",
4803
- // Chart symbology
4804
- CHBLK: "#000000",
4805
- CSTLN: "#000000",
4806
- OUTLW: "#000000",
4807
- BKAJ1: "#4d4d4d",
4808
- // Navigation / traffic
4809
- TRFCD: "#d200d2",
4810
- TRFCF: "#e680e6",
4811
- RESBL: "#0064b4",
4812
- APTS1: "#d200d2",
4813
- // Danger / hazard
4814
- DNGHL: "#ff0000",
4815
- ISHDG: "#cc0000",
4816
- // Lights
4817
- LITRD: "#ee0000",
4818
- LITGN: "#00c800",
4819
- LITYW: "#fddc00",
4820
- LITWT: "#ffffff",
4821
- LITBL: "#0064ff",
4822
- LITOR: "#ff8c00",
4823
- // Buoys / marks
4824
- RADHI: "#68e456",
4825
- RADLO: "#3f8a34",
4826
- PLRTE: "#dc4025",
4827
- ARPAT: "#3fa56f",
4828
- ISDNG: "#c545c3",
4829
- // Chart colours
4830
- CHRED: "#f15469",
4831
- CHGRN: "#68e456",
4832
- CHYLW: "#f4da48",
4833
- CHMGD: "#c545c3",
4834
- CHMGF: "#d3a6e9",
4835
- CHWHT: "#d4eaee",
4836
- CHCOR: "#eb7d36",
4837
- // Soundings
4838
- SNDG1: "#7d898c",
4839
- SNDG2: "#070707",
4840
- DEPSC: "#525a5c",
4841
- DEPCN: "#555555",
4842
- // Utility / infrastructure
4843
- UINFD: "#070707",
4844
- UINFF: "#7d898c",
4845
- UINFR: "#f15469",
4846
- UINFG: "#68e456",
4847
- UINFO: "#eb7d36",
4848
- UINFB: "#3a78f0",
4849
- UINFM: "#c545c3",
4850
- UIBCK: "#d4eaee",
4851
- UIBDR: "#7d898c",
4852
- UIAFD: "#73b6ef",
4853
- UIAFF: "#c9b97a",
4854
- PIPLN: "#55aa00",
4855
- // Outlines
4856
- OUTLL: "#c9b97a",
4857
- // Miscellaneous
4858
- SCLBR: "#eb7d36",
4859
- NINFO: "#eb7d36",
4860
- ADINF: "#b29f34",
4861
- RESGR: "#7d898c",
4862
- SHIPS: "#070707",
4863
- PSTRK: "#070707",
4864
- SYTRK: "#7d898c",
4865
- APLRT: "#eb7d36",
4866
- CURSR: "#eb7d36",
4867
- BKAJ2: "#232728",
4868
- RES01: "#a3b4b7",
4869
- RES02: "#a3b4b7",
4870
- RES03: "#a3b4b7"
4871
- };
4872
- var S52_DUSK = {
4873
- NODTA: "#6b6b6b",
4874
- DEPIT: "#006650",
4875
- DEPVS: "#487085",
4876
- DEPMS: "#608a9d",
4877
- DEPMD: "#7ba6b8",
4878
- DEPDW: "#95bfd0",
4879
- LANDA: "#8a7d5e",
4880
- LANDF: "#7a6a48",
4881
- CHBRN: "#6e4e30",
4882
- CHGRF: "#585858",
4883
- CHGRD: "#3a3a3a",
4884
- CHBLK: "#b0b0b0",
4885
- CSTLN: "#b0b0b0",
4886
- OUTLW: "#b0b0b0",
4887
- BKAJ1: "#707070",
4888
- TRFCD: "#a000a0",
4889
- TRFCF: "#b060b0",
4890
- RESBL: "#004a88",
4891
- APTS1: "#a000a0",
4892
- DNGHL: "#cc0000",
4893
- ISHDG: "#990000",
4894
- LITRD: "#bb0000",
4895
- LITGN: "#009900",
4896
- LITYW: "#c8b000",
4897
- LITWT: "#d0d0d0",
4898
- LITBL: "#004abb",
4899
- LITOR: "#cc7000",
4900
- RADHI: "#009940",
4901
- RADLO: "#2d6330",
4902
- PLRTE: "#a03020",
4903
- ARPAT: "#306040",
4904
- ISDNG: "#903090",
4905
- CHRED: "#b05060",
4906
- CHGRN: "#509040",
4907
- CHYLW: "#b0a030",
4908
- CHMGD: "#903090",
4909
- CHMGF: "#905090",
4910
- CHWHT: "#90a0a0",
4911
- CHCOR: "#a06030",
4912
- SNDG1: "#606060",
4913
- SNDG2: "#b0b0b0",
4914
- DEPSC: "#606060",
4915
- DEPCN: "#2E2E2E",
4916
- UINFD: "#b0b0b0",
4917
- UINFF: "#606060",
4918
- UINFR: "#b05060",
4919
- UINFG: "#509040",
4920
- UINFO: "#a06030",
4921
- UINFB: "#304090",
4922
- UINFM: "#903090",
4923
- UIBCK: "#1a1a1a",
4924
- UIBDR: "#606060",
4925
- UIAFD: "#405060",
4926
- UIAFF: "#605840",
4927
- PIPLN: "#3d7a00",
4928
- OUTLL: "#605840",
4929
- SCLBR: "#a06030",
4930
- NINFO: "#a06030",
4931
- ADINF: "#807020",
4932
- RESGR: "#606060",
4933
- SHIPS: "#b0b0b0",
4934
- PSTRK: "#b0b0b0",
4935
- SYTRK: "#606060",
4936
- APLRT: "#a06030",
4937
- CURSR: "#cc0000",
4938
- BKAJ2: "#252525",
4939
- RES01: "#606060",
4940
- RES02: "#606060",
4941
- RES03: "#606060"
4942
- };
4943
- var S52_NIGHT = {
4944
- NODTA: "#3a3a32",
4945
- DEPIT: "#003828",
4946
- DEPVS: "#356577",
4947
- DEPMS: "#265066",
4948
- DEPMD: "#102e44",
4949
- DEPDW: "#051a28",
4950
- LANDA: "#3a3020",
4951
- LANDF: "#302818",
4952
- CHBRN: "#382410",
4953
- CHGRF: "#2a2a22",
4954
- CHGRD: "#1e1e18",
4955
- CHBLK: "#c8aa78",
4956
- CSTLN: "#c8aa78",
4957
- OUTLW: "#c8aa78",
4958
- BKAJ1: "#504830",
4959
- TRFCD: "#6e006e",
4960
- TRFCF: "#803880",
4961
- RESBL: "#002a50",
4962
- APTS1: "#6e006e",
4963
- DNGHL: "#a00000",
4964
- ISHDG: "#700000",
4965
- LITRD: "#880000",
4966
- LITGN: "#006600",
4967
- LITYW: "#887800",
4968
- LITWT: "#a09878",
4969
- LITBL: "#003088",
4970
- LITOR: "#884800",
4971
- RADHI: "#005528",
4972
- RADLO: "#1a3a20",
4973
- PLRTE: "#6e2018",
4974
- ARPAT: "#1a3028",
4975
- ISDNG: "#481848",
4976
- CHRED: "#682030",
4977
- CHGRN: "#285020",
4978
- CHYLW: "#685828",
4979
- CHMGD: "#481848",
4980
- CHMGF: "#482848",
4981
- CHWHT: "#484840",
4982
- CHCOR: "#683818",
4983
- SNDG1: "#383028",
4984
- SNDG2: "#c8aa78",
4985
- DEPSC: "#383028",
4986
- // ⚠ DELIBERATE DEVIATION from the published S-52 NIGHT value (#1f2223),
4987
- // recorded here because `docs/validation/s52-spec-gap-matrix.json` tracks
4988
- // this token as `value-differs` and the CHRTstyles programme is bringing
4989
- // colours TO spec — this one moves further away, on purpose.
4990
- //
4991
- // Spec NIGHT DEPCN is near-black, which measures ~1.1:1 against this
4992
- // palette's own deep water (#051a28). S-52's NIGHT table assumes a
4993
- // dark-adapted eye in a darkened wheelhouse; on a phone or a laptop at
4994
- // anchor it is not dim, it is absent — reported as "can't see bottom
4995
- // contours in night mode only". A contour nobody can see fails the
4996
- // standard's intent while matching its number.
4997
- //
4998
- // The palette already makes this trade elsewhere and in the same
4999
- // direction: NIGHT's CHBLK/CSTLN/OUTLW are #c8aa78, light warm linework on
5000
- // dark water. This value is that family, dimmed to stay subordinate to the
5001
- // coastline. Revisit with the CHRTstyles programme, which owns the
5002
- // spec-vs-legibility call across all 63 tokens.
5003
- DEPCN: "#A68E84",
5004
- UINFD: "#c8aa78",
5005
- UINFF: "#383028",
5006
- UINFR: "#682030",
5007
- UINFG: "#285020",
5008
- UINFO: "#683818",
5009
- UINFB: "#182848",
5010
- UINFM: "#481848",
5011
- UIBCK: "#0a0a08",
5012
- UIBDR: "#383028",
5013
- UIAFD: "#182838",
5014
- UIAFF: "#302818",
5015
- PIPLN: "#284800",
5016
- OUTLL: "#302818",
5017
- SCLBR: "#683818",
5018
- NINFO: "#683818",
5019
- ADINF: "#483818",
5020
- RESGR: "#383028",
5021
- SHIPS: "#c8aa78",
5022
- PSTRK: "#c8aa78",
5023
- SYTRK: "#484030",
5024
- APLRT: "#683818",
5025
- CURSR: "#a00000",
5026
- BKAJ2: "#0a0a08",
5027
- RES01: "#383028",
5028
- RES02: "#383028",
5029
- RES03: "#383028"
4997
+ container.appendChild(root);
4998
+ this.root = root;
4999
+ this.lngLat = lngLat;
5000
+ this.place();
5001
+ this.map.on("move", this.reposition);
5002
+ }
5003
+ place() {
5004
+ if (!this.root || !this.lngLat) return;
5005
+ const { x, y } = this.map.project(this.lngLat);
5006
+ this.root.style.left = `${x}px`;
5007
+ this.root.style.top = `${y}px`;
5008
+ }
5009
+ close() {
5010
+ if (this.stopAnimation) {
5011
+ this.stopAnimation();
5012
+ this.stopAnimation = null;
5013
+ }
5014
+ if (this.root) {
5015
+ this.map.off("move", this.reposition);
5016
+ this.root.remove();
5017
+ this.root = null;
5018
+ }
5019
+ this.lngLat = null;
5020
+ }
5030
5021
  };
5031
- var S52_COLORS = S52_DAY;
5032
- var COLOR_TABLES = {
5033
- DAY: S52_DAY,
5034
- DUSK: S52_DUSK,
5035
- NIGHT: S52_NIGHT
5022
+
5023
+ // src/interact/index.ts
5024
+ var InteractClient = class {
5025
+ constructor(http) {
5026
+ this.http = http;
5027
+ this.entitlementClient = new EntitlementClient(http);
5028
+ }
5029
+ entitlementClient;
5030
+ /**
5031
+ * Check the CHRTinteract entitlement for this API key (server-side, plan
5032
+ * derived, cached per client). Throws `InteractError` with code
5033
+ * `interact_not_enabled` for Free-plan teams.
5034
+ */
5035
+ async entitlement() {
5036
+ return this.entitlementClient.check();
5037
+ }
5038
+ /**
5039
+ * Decode a chart feature (from `queryRenderedFeatures`, the features API,
5040
+ * or plain `{ layer, properties }`) into a typed {@link InspectResult}.
5041
+ *
5042
+ * The first call performs the server-side entitlement check. With
5043
+ * `{ enrich: true }` the result is upgraded with full S-57 properties and
5044
+ * source-cell provenance (edition / update / issue date) via the features
5045
+ * API — metered against the key's `featureCalls` quota.
5046
+ *
5047
+ * @example
5048
+ * const result = await chrt.interact.inspect(feature);
5049
+ * if (result.kind === "navaid") console.log(result.characteristic); // "Fl R 4s"
5050
+ */
5051
+ async inspect(feature, opts) {
5052
+ await this.entitlementClient.check();
5053
+ const result = buildInspectResult(feature, opts);
5054
+ if (opts?.enrich) {
5055
+ return enrichResult(this.http, result, opts.signal);
5056
+ }
5057
+ return result;
5058
+ }
5059
+ /**
5060
+ * Collect EVERY inspectable feature under a point on a CHRT map —
5061
+ * deduplicated across tile boundaries by S-57 identity (`LNAM`) and
5062
+ * ordered by class priority (point targets > lines > areas, safety
5063
+ * classes first within each band). Area features never preempt the buoy
5064
+ * on top of them but always stay selectable: a click on open water
5065
+ * resolves to its DEPARE.
5066
+ *
5067
+ * `point` is a screen pixel point (`e.point` from a map click) or
5068
+ * `[lng, lat]`. Point/line targets are picked up within
5069
+ * `opts.tolerance` px (default 8); polygon candidates require the exact
5070
+ * point to fall inside them.
5071
+ *
5072
+ * The first call performs the server-side entitlement check.
5073
+ *
5074
+ * @example
5075
+ * const { candidates, select } = await chrt.interact.inspectAll(map, e.point);
5076
+ * const result = await select(candidates[0]); // full decode (+ merge)
5077
+ */
5078
+ async inspectAll(map, point, opts = {}) {
5079
+ if (!map || typeof map.queryRenderedFeatures !== "function") {
5080
+ throw new InteractError(
5081
+ "unexpected_response",
5082
+ "inspectAll(): expected a MapLibre / Mapbox GL map instance"
5083
+ );
5084
+ }
5085
+ await this.entitlementClient.check();
5086
+ const screen = Array.isArray(point) ? map.project([point[0], point[1]]) : point;
5087
+ const lngLat = Array.isArray(point) ? { lng: point[0], lat: point[1] } : map.unproject?.([point.x, point.y]) ?? null;
5088
+ const at = { point: screen, lngLat };
5089
+ const candidates = collectClickCandidates(map, screen, opts);
5090
+ const http = this.http;
5091
+ return {
5092
+ candidates,
5093
+ select: async (candidate, selectOpts) => {
5094
+ const result = decodeCandidate(map, candidate, at, opts);
5095
+ if (selectOpts?.enrich) {
5096
+ return enrichResult(http, result, selectOpts.signal);
5097
+ }
5098
+ return result;
5099
+ }
5100
+ };
5101
+ }
5102
+ /**
5103
+ * Attach click-to-inspect to a MapLibre / Mapbox GL map rendering CHRT
5104
+ * tiles. Resolves once the server-side entitlement check passes; rejects
5105
+ * with `InteractError` (`interact_not_enabled` on Free plans) otherwise —
5106
+ * no listeners are wired on denial.
5107
+ *
5108
+ * Headless-first: pass `onInspect` to receive every decoded
5109
+ * {@link InspectResult} and render your own UI. Without callbacks, the
5110
+ * built-in minimal popup is shown (opt back in explicitly with
5111
+ * `popup: true` to have both).
5112
+ *
5113
+ * A click that hits SEVERAL features (deduplicated by S-57 identity)
5114
+ * disambiguates instead of silently taking the topmost:
5115
+ * - `onCandidates` provided → it receives the priority-ordered list;
5116
+ * resolve a choice with `ctx.select(candidate)`.
5117
+ * - built-in popup → shows a candidate list; picking a row opens the
5118
+ * result card.
5119
+ * - `onInspect` only → the top candidate is inspected immediately, and
5120
+ * clicking the SAME spot again cycles to the next candidate — so an
5121
+ * area feature under a buoy is reachable with a second click.
5122
+ *
5123
+ * @example
5124
+ * const handle = await chrt.interact.attach(map, {
5125
+ * onInspect: (result) => console.log(result.title, result.characteristic),
5126
+ * });
5127
+ * // later: handle.detach();
5128
+ */
5129
+ async attach(map, opts = {}) {
5130
+ if (!map || typeof map.queryRenderedFeatures !== "function" || typeof map.on !== "function") {
5131
+ throw new InteractError(
5132
+ "unexpected_response",
5133
+ "attach(): expected a MapLibre / Mapbox GL map instance"
5134
+ );
5135
+ }
5136
+ await this.entitlementClient.check();
5137
+ const showPopup = opts.popup ?? (!opts.onInspect && !opts.onCandidates);
5138
+ const popup = showPopup ? new InteractPopup(map) : null;
5139
+ const http = this.http;
5140
+ let cycle = null;
5141
+ const samePoint = (a, b) => {
5142
+ const pad = opts.tolerance ?? DEFAULT_CLICK_TOLERANCE;
5143
+ return Math.abs(a.x - b.x) <= pad && Math.abs(a.y - b.y) <= pad;
5144
+ };
5145
+ const select = (candidate, e) => {
5146
+ const result = decodeCandidate(map, candidate, e, opts);
5147
+ const finish = (r) => {
5148
+ popup?.show(r, r.coordinates ?? [e.lngLat.lng, e.lngLat.lat]);
5149
+ opts.onInspect?.(r, { lngLat: e.lngLat, point: e.point, feature: candidate.feature });
5150
+ return r;
5151
+ };
5152
+ if (opts.enrich) {
5153
+ return enrichResult(http, result).then(finish).catch(() => finish(result));
5154
+ }
5155
+ return Promise.resolve(finish(result));
5156
+ };
5157
+ const clickHandler = (e) => {
5158
+ const candidates = collectClickCandidates(map, e.point, opts);
5159
+ if (candidates.length === 0) {
5160
+ cycle = null;
5161
+ popup?.close();
5162
+ opts.onMiss?.({ lngLat: e.lngLat });
5163
+ return;
5164
+ }
5165
+ if (candidates.length === 1) {
5166
+ cycle = null;
5167
+ void select(candidates[0], e);
5168
+ return;
5169
+ }
5170
+ if (opts.onCandidates || popup) {
5171
+ cycle = null;
5172
+ if (popup) {
5173
+ popup.showList(candidates, [e.lngLat.lng, e.lngLat.lat], (c) => void select(c, e));
5174
+ }
5175
+ opts.onCandidates?.(candidates, {
5176
+ lngLat: e.lngLat,
5177
+ point: e.point,
5178
+ select: (c) => select(c, e)
5179
+ });
5180
+ return;
5181
+ }
5182
+ const key = candidates.map((c) => c.identity ?? c.layerId).join("/");
5183
+ if (cycle && cycle.key === key && samePoint(cycle.point, e.point)) {
5184
+ cycle.index = (cycle.index + 1) % candidates.length;
5185
+ cycle.point = e.point;
5186
+ } else {
5187
+ cycle = { key, point: e.point, index: 0 };
5188
+ }
5189
+ void select(candidates[cycle.index], e);
5190
+ };
5191
+ map.on("click", clickHandler);
5192
+ return {
5193
+ detach: () => {
5194
+ map.off("click", clickHandler);
5195
+ popup?.close();
5196
+ }
5197
+ };
5198
+ }
5036
5199
  };
5037
- function getColorTable(name) {
5038
- return COLOR_TABLES[name];
5039
- }
5040
- function resolveColor(token, table = "DAY") {
5041
- const t = COLOR_TABLES[table];
5042
- return t[token] ?? (table === "NIGHT" ? "#c8aa78" : "#000000");
5043
- }
5044
5200
 
5045
5201
  // src/atlas/s52-linestyles-generated.ts
5046
5202
  var S52_LINESTYLES = {
@@ -27691,6 +27847,35 @@ function iconExpressionForFamily(objectClass, family) {
27691
27847
  }
27692
27848
  }
27693
27849
 
27850
+ // src/atlas/base-bundle.ts
27851
+ var BASE_BUNDLE_ID = BASE_REGION;
27852
+ var BASE_BUNDLE_SCHEMA = "chrt.basemap-bundle/1";
27853
+ var BASE_BUNDLE_MAXZOOM = 6;
27854
+ var BASE_BUNDLE_SOURCE_LAYERS = [
27855
+ "base_earth",
27856
+ "base_water",
27857
+ "base_boundary",
27858
+ "country_boundary"
27859
+ ];
27860
+ var BASE_BUNDLE_ATTRIBUTION = "\xA9 CHRT \xB7 Made with Natural Earth";
27861
+ var BASE_CLIENT_RASTER_BEFORE_ID = "chrtbase-nocoverage";
27862
+ function applyBaseBundle(style, opts) {
27863
+ const keep = new Set(BASE_BUNDLE_SOURCE_LAYERS);
27864
+ style.layers = style.layers.filter((raw) => {
27865
+ const layer = raw;
27866
+ if (layer.source !== BASE_REGION) return true;
27867
+ return typeof layer["source-layer"] === "string" && keep.has(layer["source-layer"]);
27868
+ });
27869
+ style.sources[BASE_REGION] = {
27870
+ type: "vector",
27871
+ tiles: [opts.tiles],
27872
+ minzoom: BASE_MINZOOM,
27873
+ maxzoom: BASE_BUNDLE_MAXZOOM,
27874
+ attribution: BASE_BUNDLE_ATTRIBUTION
27875
+ };
27876
+ return style;
27877
+ }
27878
+
27694
27879
  // src/atlas/layer-builder.ts
27695
27880
  var SCAMIN_DECLUTTER_CLASSES = /* @__PURE__ */ new Set([
27696
27881
  "OFSPLF",
@@ -29031,6 +29216,99 @@ function getOrderedClasses() {
29031
29216
  return ordered;
29032
29217
  }
29033
29218
 
29219
+ // src/atlas/terrain-order.ts
29220
+ var DRAPEABLE_LAYER_TYPES = /* @__PURE__ */ new Set([
29221
+ "background",
29222
+ "fill",
29223
+ "line",
29224
+ "raster",
29225
+ "hillshade",
29226
+ "color-relief"
29227
+ ]);
29228
+ function isDrapeable(layer) {
29229
+ return DRAPEABLE_LAYER_TYPES.has(layer.type);
29230
+ }
29231
+ function drapedFirst(layers) {
29232
+ const draped = [];
29233
+ const direct = [];
29234
+ for (const l of layers) (isDrapeable(l) ? draped : direct).push(l);
29235
+ return [...draped, ...direct];
29236
+ }
29237
+ function terrainStackCount(layers) {
29238
+ let stacks = 0;
29239
+ let prevDraped = false;
29240
+ for (const l of layers) {
29241
+ const d = isDrapeable(l);
29242
+ if (d && !prevDraped) stacks++;
29243
+ prevDraped = d;
29244
+ }
29245
+ return stacks;
29246
+ }
29247
+ function drapeHalves(layers) {
29248
+ const draped = [];
29249
+ const direct = [];
29250
+ for (const l of layers) (isDrapeable(l) ? draped : direct).push(l);
29251
+ return { draped, direct };
29252
+ }
29253
+
29254
+ // src/atlas/lamps.ts
29255
+ var LAMP_GLOW_LAYER_ID = "s52-LIGHTS-lamp-glow";
29256
+ var LAMP_CORE_LAYER_ID = "s52-LIGHTS-lamp-core";
29257
+ var LAMP_LAYER_IDS = [LAMP_GLOW_LAYER_ID, LAMP_CORE_LAYER_ID];
29258
+ var RANGE_NM = ["to-number", ["coalesce", ["get", "VALNMR"], ["get", "valnmr"]], 6];
29259
+ var UNSECTORED_POINTS = [
29260
+ "all",
29261
+ ["any", ["==", ["geometry-type"], "Point"], ["==", ["geometry-type"], "MultiPoint"]],
29262
+ ["!", ["has", "SECTR1"]],
29263
+ // …and not lights with a charted rhythm either: those flash, and the
29264
+ // flashing is computed by the app from `SIGSEQ` (light-sigseq.ts). What is
29265
+ // left here is exactly the set that is steady from every bearing.
29266
+ ["!", ["has", "SIGSEQ"]]
29267
+ ];
29268
+ function buildLampLayers(sourceId, ct) {
29269
+ const colour = lightFillExpression(ct);
29270
+ const common = {
29271
+ type: "circle",
29272
+ source: sourceId,
29273
+ "source-layer": "LIGHTS",
29274
+ filter: UNSECTORED_POINTS,
29275
+ layout: { visibility: "none" }
29276
+ };
29277
+ const metadata = { "chrt:role": "lamp", "s52:objectClass": "LIGHTS" };
29278
+ return [
29279
+ {
29280
+ ...common,
29281
+ id: LAMP_GLOW_LAYER_ID,
29282
+ paint: {
29283
+ "circle-color": colour,
29284
+ // 4 nm → 7 px, 12 nm → 12 px, 25 nm → 20 px; capped, because a
29285
+ // primary seacoast light is not a searchlight.
29286
+ "circle-radius": ["interpolate", ["linear"], RANGE_NM, 0, 5, 4, 7, 12, 12, 25, 20],
29287
+ "circle-blur": 1,
29288
+ "circle-opacity": 0.55,
29289
+ // A lamp is a point of light: it does not grow as it comes near, and
29290
+ // it does not lie down with the chart.
29291
+ "circle-pitch-alignment": "viewport",
29292
+ "circle-pitch-scale": "viewport"
29293
+ },
29294
+ metadata: { ...metadata, "chrt:lamp": "glow" }
29295
+ },
29296
+ {
29297
+ ...common,
29298
+ id: LAMP_CORE_LAYER_ID,
29299
+ paint: {
29300
+ "circle-color": colour,
29301
+ "circle-radius": ["interpolate", ["linear"], RANGE_NM, 0, 1.5, 12, 2.5, 25, 3.5],
29302
+ "circle-blur": 0.25,
29303
+ "circle-opacity": 1,
29304
+ "circle-pitch-alignment": "viewport",
29305
+ "circle-pitch-scale": "viewport"
29306
+ },
29307
+ metadata: { ...metadata, "chrt:lamp": "core" }
29308
+ }
29309
+ ];
29310
+ }
29311
+
29034
29312
  // src/atlas/band-priority.ts
29035
29313
  var BAND_PRIORITY_RANGES = {
29036
29314
  "1": { minzoom: 0 },
@@ -29146,11 +29424,21 @@ function styleModeBand(mode) {
29146
29424
  return mode === "continuous" ? null : mode.slice("chart:".length);
29147
29425
  }
29148
29426
  function bandFilterExpression(codes) {
29427
+ return cellBandMatch(["slice", ["get", "_cell_id"], 2, 3], codes);
29428
+ }
29429
+ var BAND_CODE_ALIASES = {
29430
+ "5": ["7"]
29431
+ };
29432
+ function cellBandCharacters(code) {
29433
+ return [code, ...BAND_CODE_ALIASES[code] ?? []];
29434
+ }
29435
+ function cellBandMatch(cellBandExpr, codes) {
29149
29436
  const list = Array.isArray(codes) ? codes : [codes];
29150
- if (list.length === 1) {
29151
- return ["==", ["slice", ["get", "_cell_id"], 2, 3], list[0]];
29437
+ const chars = [...new Set(list.flatMap((c) => cellBandCharacters(c)))];
29438
+ if (chars.length === 1) {
29439
+ return ["==", cellBandExpr, chars[0]];
29152
29440
  }
29153
- return ["in", ["slice", ["get", "_cell_id"], 2, 3], ["literal", [...list]]];
29441
+ return ["in", cellBandExpr, ["literal", chars]];
29154
29442
  }
29155
29443
  function combineWithBandFilter(baseFilter, bandFilter) {
29156
29444
  if (!baseFilter) return bandFilter;
@@ -30234,7 +30522,8 @@ function fanOrientationLabelByCellBand(label, bandFan, objectClass) {
30234
30522
  }
30235
30523
  const cellFiltered = combineWithBandFilter(
30236
30524
  label.filter,
30237
- ["==", cellBandExpr, code]
30525
+ // Aliases included (USACE `7` rides with band 5 — #269).
30526
+ cellBandMatch(cellBandExpr, code)
30238
30527
  );
30239
30528
  const minzoom = Math.max(
30240
30529
  label.minzoom ?? range.minzoom,
@@ -30439,6 +30728,7 @@ function generateStyle(palette = "DAY", opts = {}) {
30439
30728
  (b) => !TOP_STACK_LIGHT_CLASSES.has(b.objectClass) && !BOTTOM_STACK_WATER_BASE_CLASSES.has(b.objectClass)
30440
30729
  );
30441
30730
  const topLightBundles = bundles.filter((b) => TOP_STACK_LIGHT_CLASSES.has(b.objectClass));
30731
+ const lampLayers = buildLampLayers(sourceId, palette);
30442
30732
  const waterBaseBundles = bundles.filter(
30443
30733
  (b) => BOTTOM_STACK_WATER_BASE_CLASSES.has(b.objectClass)
30444
30734
  );
@@ -30472,6 +30762,7 @@ function generateStyle(palette = "DAY", opts = {}) {
30472
30762
  emitChartFeatureVariants(waterBaseBundles);
30473
30763
  emitChartFeatureVariants(regularBundles);
30474
30764
  emitChartFeatureVariants(topLightBundles);
30765
+ layers.push(...lampLayers);
30475
30766
  const chartLabelBundles = byLabelPriorityBottomUp(
30476
30767
  bundles.filter(
30477
30768
  (b) => b.label !== null && (opts.labels !== "placename" || getLabelBehavior(b.objectClass) !== "orientation-cross-band")
@@ -30549,6 +30840,7 @@ function generateStyle(palette = "DAY", opts = {}) {
30549
30840
  }
30550
30841
  }
30551
30842
  }
30843
+ layers.push(...lampLayers);
30552
30844
  {
30553
30845
  const TERMINAL_FILL_RE = new RegExp(
30554
30846
  `^s52-(?:${[...DOMINANCE_EXEMPT_FILL_CLASSES].join("|")})-fill-band[456]$`
@@ -30637,13 +30929,18 @@ function generateStyle(palette = "DAY", opts = {}) {
30637
30929
  const glyphsBase = `${opts.tileBaseUrl ?? "https://tiles.chrt.co"}/v1/glyphs`;
30638
30930
  const isLndareFill = (l) => /^s52-LNDARE-fill-band\d+$/.test(l.id);
30639
30931
  const isWaterBaseFill = (l) => /^s52-(DEPARE|DRGARE|UNSARE)-fill-band\d+$/.test(l.id);
30932
+ const isLakareFill = (l) => /^s52-LAKARE-fill-band\d+$/.test(l.id);
30640
30933
  const bandOf = (l) => {
30641
30934
  const m = /-band(\d+)$/.exec(l.id);
30642
30935
  return m ? Number(m[1]) : 0;
30643
30936
  };
30644
30937
  const emitted = opts.land ? (() => {
30645
- const relocated = layers.filter((l) => isLndareFill(l) || isWaterBaseFill(l));
30646
- const others = layers.filter((l) => !isLndareFill(l) && !isWaterBaseFill(l));
30938
+ const relocated = layers.filter(
30939
+ (l) => isLndareFill(l) || isWaterBaseFill(l) || isLakareFill(l)
30940
+ );
30941
+ const others = layers.filter(
30942
+ (l) => !isLndareFill(l) && !isWaterBaseFill(l) && !isLakareFill(l)
30943
+ );
30647
30944
  const isSeaareFill = (l) => /^s52-SEAARE-fill-band\d+$/.test(l.id);
30648
30945
  const seaare = others.filter(isSeaareFill);
30649
30946
  const rest = others.filter((l) => !isSeaareFill(l));
@@ -30667,40 +30964,43 @@ function generateStyle(palette = "DAY", opts = {}) {
30667
30964
  for (const b of bands) {
30668
30965
  ordered.push(...relocated.filter((l) => bandOf(l) === b && isWaterBaseFill(l)));
30669
30966
  ordered.push(...relocated.filter((l) => bandOf(l) === b && isLndareFill(l)).map(asBaseTan));
30967
+ ordered.push(...relocated.filter((l) => bandOf(l) === b && isLakareFill(l)));
30670
30968
  }
30671
30969
  const findRest = (id) => restAbove.findIndex((l) => l.id === id);
30672
- const shoreAt = findRest("chrtbase-inland-shore");
30673
- const pairAt = shoreAt !== -1 ? shoreAt : findRest("chrtbase-inland-hatch");
30970
+ const pairAt = findRest("chrtbase-inland-water");
30674
30971
  let restFinal = restAbove;
30675
30972
  if (pairAt !== -1) {
30676
30973
  const repaint = [];
30677
- for (const b of bands) {
30678
- for (const l of relocated.filter((x) => bandOf(x) === b && isWaterBaseFill(x))) {
30679
- const src = l;
30680
- const meta = { ...src.metadata ?? {}, "chrt:role": "charted-water-repaint" };
30681
- const notDom = {
30974
+ const pushCascadeClones = (l, b) => {
30975
+ const src = l;
30976
+ const meta = { ...src.metadata ?? {}, "chrt:role": "charted-water-repaint" };
30977
+ const notDom = {
30978
+ ...l,
30979
+ id: `${src.id}-overpair`,
30980
+ filter: ["all", src.filter, NOT_DOMINATED_FILTER],
30981
+ metadata: meta
30982
+ };
30983
+ delete notDom.maxzoom;
30984
+ delete notDom.minzoom;
30985
+ repaint.push(notDom);
30986
+ for (const D of BAND_DRAW_ORDER) {
30987
+ if (parseInt(D, 10) <= b) continue;
30988
+ const dMaxzoom = dominatedVariantMaxzoom(D);
30989
+ if (dMaxzoom <= 0) continue;
30990
+ const domVariant = {
30682
30991
  ...l,
30683
- id: `${src.id}-overpair`,
30684
- filter: ["all", src.filter, NOT_DOMINATED_FILTER],
30685
- metadata: meta
30992
+ id: `${src.id}-overpair-domBy${D}`,
30993
+ filter: ["all", src.filter, dominatedByMinFilter(D)],
30994
+ metadata: meta,
30995
+ maxzoom: dMaxzoom
30686
30996
  };
30687
- delete notDom.maxzoom;
30688
- delete notDom.minzoom;
30689
- repaint.push(notDom);
30690
- for (const D of BAND_DRAW_ORDER) {
30691
- if (parseInt(D, 10) <= b) continue;
30692
- const dMaxzoom = dominatedVariantMaxzoom(D);
30693
- if (dMaxzoom <= 0) continue;
30694
- const domVariant = {
30695
- ...l,
30696
- id: `${src.id}-overpair-domBy${D}`,
30697
- filter: ["all", src.filter, dominatedByMinFilter(D)],
30698
- metadata: meta,
30699
- maxzoom: dMaxzoom
30700
- };
30701
- delete domVariant.minzoom;
30702
- repaint.push(domVariant);
30703
- }
30997
+ delete domVariant.minzoom;
30998
+ repaint.push(domVariant);
30999
+ }
31000
+ };
31001
+ for (const b of bands) {
31002
+ for (const l of relocated.filter((x) => bandOf(x) === b && isWaterBaseFill(x))) {
31003
+ pushCascadeClones(l, b);
30704
31004
  }
30705
31005
  if (parseInt(b, 10) >= FINE_LAND_REPAINT_MIN_BAND) {
30706
31006
  for (const l of relocated.filter(
@@ -30716,6 +31016,9 @@ function generateStyle(palette = "DAY", opts = {}) {
30716
31016
  repaint.push(landClone);
30717
31017
  }
30718
31018
  }
31019
+ for (const l of relocated.filter((x) => bandOf(x) === b && isLakareFill(x))) {
31020
+ pushCascadeClones(l, b);
31021
+ }
30719
31022
  }
30720
31023
  const isCoalneLine = (l) => /^s52-COALNE-line-band\d(-domBy\d)?$/.test(l.id);
30721
31024
  for (const l of layers.filter(isCoalneLine)) {
@@ -30739,9 +31042,9 @@ function generateStyle(palette = "DAY", opts = {}) {
30739
31042
  repaint.push(terminal);
30740
31043
  }
30741
31044
  restFinal = [
30742
- ...restAbove.slice(0, pairAt + 1),
31045
+ ...restAbove.slice(0, pairAt),
30743
31046
  ...repaint,
30744
- ...restAbove.slice(pairAt + 1)
31047
+ ...restAbove.slice(pairAt)
30745
31048
  ];
30746
31049
  }
30747
31050
  {
@@ -30790,6 +31093,7 @@ function generateStyle(palette = "DAY", opts = {}) {
30790
31093
  emitted.push(layer);
30791
31094
  }
30792
31095
  }
31096
+ const finalLayers = drapedFirst(emitted);
30793
31097
  const style = {
30794
31098
  version: 8,
30795
31099
  name: `CHRT ${SYMBOL_SET_LABELS[symbolSet]} ${PALETTE_NAMES[palette]}${opts.chartMode ? ` \u2014 chart:${opts.chartMode.band}` : ""}`,
@@ -30798,7 +31102,7 @@ function generateStyle(palette = "DAY", opts = {}) {
30798
31102
  "chrt:symbol-set": symbolSet,
30799
31103
  "chrt:color-table": palette,
30800
31104
  "chrt:generated-at": (/* @__PURE__ */ new Date()).toISOString(),
30801
- "chrt:layer-count": emitted.length,
31105
+ "chrt:layer-count": finalLayers.length,
30802
31106
  "chrt:land": Boolean(opts.land),
30803
31107
  "chrt:relief": Boolean(opts.relief),
30804
31108
  "chrt:base": Boolean(opts.base),
@@ -30838,7 +31142,7 @@ function generateStyle(palette = "DAY", opts = {}) {
30838
31142
  // upstream glyph byte-identical and composite only those six in from
30839
31143
  // Noto Sans; see scripts/generate-glyphs.mjs.
30840
31144
  glyphs: `${glyphsBase}/{fontstack}/{range}.pbf`,
30841
- layers: emitted
31145
+ layers: finalLayers
30842
31146
  };
30843
31147
  if (symbolSet === "s101") return applyS101SymbolSet(style);
30844
31148
  if (symbolSet === "chrt") return applyChrtSymbolSet(style, palette);
@@ -31667,8 +31971,16 @@ var Chrt = class {
31667
31971
  ALL_WATER_BODY_LAYERS,
31668
31972
  AREA_QUERY_LAYERS,
31669
31973
  ATON_EXPRESSION_CLASSES,
31974
+ BAND_CODE_ALIASES,
31670
31975
  BAND_DRAW_ORDER,
31671
31976
  BAND_PRIORITY_RANGES,
31977
+ BASE_ATTRIBUTION,
31978
+ BASE_BUNDLE_ATTRIBUTION,
31979
+ BASE_BUNDLE_ID,
31980
+ BASE_BUNDLE_MAXZOOM,
31981
+ BASE_BUNDLE_SCHEMA,
31982
+ BASE_BUNDLE_SOURCE_LAYERS,
31983
+ BASE_CLIENT_RASTER_BEFORE_ID,
31672
31984
  BASE_MAXZOOM,
31673
31985
  BASE_MINZOOM,
31674
31986
  BASE_REGION,
@@ -31701,6 +32013,7 @@ var Chrt = class {
31701
32013
  DEFAULT_TILE_BASE_URL,
31702
32014
  DEFAULT_TILE_REGION,
31703
32015
  DEFAULT_WATER_BODY_SETTINGS,
32016
+ DRAPEABLE_LAYER_TYPES,
31704
32017
  INSPECT_CLASS_RANK,
31705
32018
  INSPECT_RANK_DEVIATIONS,
31706
32019
  InteractClient,
@@ -31709,6 +32022,11 @@ var Chrt = class {
31709
32022
  LABEL_BEHAVIOR,
31710
32023
  LABEL_LAYER_GROUPS,
31711
32024
  LABEL_PRIORITY,
32025
+ LAMP_CORE_LAYER_ID,
32026
+ LAMP_GLOW_LAYER_ID,
32027
+ LAMP_LAYER_IDS,
32028
+ LAMP_TICK_MS,
32029
+ LAND_ATTRIBUTION,
31712
32030
  LAND_BUILD_MAXZOOM,
31713
32031
  LAND_REGION,
31714
32032
  NAUTICAL_SYMBOLS,
@@ -31724,7 +32042,10 @@ var Chrt = class {
31724
32042
  RASTER_ATON_CLASSES,
31725
32043
  RASTER_GLYPH_NAMES,
31726
32044
  RASTER_ICON_PREFIX,
32045
+ RELIEF_ATTRIBUTION,
32046
+ RELIEF_ENCODING,
31727
32047
  RELIEF_REGION,
32048
+ RELIEF_TILE_SIZE,
31728
32049
  S101_AREA_FILL_NAMES,
31729
32050
  S101_CHART_SECTIONS,
31730
32051
  S101_FEATURES,
@@ -31744,6 +32065,8 @@ var Chrt = class {
31744
32065
  S52_LOOKUP,
31745
32066
  S52_NIGHT,
31746
32067
  S52_PATTERN_SPRITE_PREFIX,
32068
+ SIGPER_TOLERANCE_SECONDS,
32069
+ SIGSEQ_MAX_TERMS,
31747
32070
  SOUNDING_LAYERS,
31748
32071
  SYMBOL_FAMILIES,
31749
32072
  SYMBOL_FAMILY_META,
@@ -31753,6 +32076,7 @@ var Chrt = class {
31753
32076
  TileTokenError,
31754
32077
  WATER_BODY_LAYER_GROUPS,
31755
32078
  WeatherEndpoint,
32079
+ applyBaseBundle,
31756
32080
  applyChrtSymbolSet,
31757
32081
  applyColorTheme,
31758
32082
  applyCustomStyleSettings,
@@ -31765,6 +32089,7 @@ var Chrt = class {
31765
32089
  buildFlashPattern,
31766
32090
  buildLabelLayer,
31767
32091
  buildLakeTierExpression,
32092
+ buildLampLayers,
31768
32093
  buildLandAreaTierExpression,
31769
32094
  buildLandRegionTierExpression,
31770
32095
  buildLayersForClass,
@@ -31777,6 +32102,8 @@ var Chrt = class {
31777
32102
  buildSectorsFromFeatures,
31778
32103
  buildSources,
31779
32104
  candidateRank,
32105
+ cellBandCharacters,
32106
+ cellBandMatch,
31780
32107
  chrtAtonTintExpression,
31781
32108
  chrtLightTintExpression,
31782
32109
  collectCandidates,
@@ -31791,6 +32118,8 @@ var Chrt = class {
31791
32118
  depareColor,
31792
32119
  depareFillExpression,
31793
32120
  deriveMarinerName,
32121
+ drapeHalves,
32122
+ drapedFirst,
31794
32123
  extractNavAidDesignation,
31795
32124
  featureIdentity,
31796
32125
  generateCustomStyle,
@@ -31823,6 +32152,7 @@ var Chrt = class {
31823
32152
  hazardColor,
31824
32153
  hazardFillExpression,
31825
32154
  iconExpressionForFamily,
32155
+ isDrapeable,
31826
32156
  isLabelLayerVisible,
31827
32157
  isNavAidLayer,
31828
32158
  isNavAidLayerVisible,
@@ -31830,6 +32160,8 @@ var Chrt = class {
31830
32160
  isVisibleInMode,
31831
32161
  isWaterBodyLayerVisible,
31832
32162
  labelSortKey,
32163
+ lampRhythmColour,
32164
+ lampRhythmOpacity,
31833
32165
  lateralColor,
31834
32166
  lateralFillExpression,
31835
32167
  lightColor,
@@ -31839,6 +32171,7 @@ var Chrt = class {
31839
32171
  normalizeTheme,
31840
32172
  paginate,
31841
32173
  parseListCodes,
32174
+ parseSigseqTerms,
31842
32175
  parseSymbolFamily,
31843
32176
  parseWebhook,
31844
32177
  patternSpriteId,
@@ -31853,12 +32186,14 @@ var Chrt = class {
31853
32186
  resolveS101Color,
31854
32187
  resolveStyle,
31855
32188
  resolveTargetClass,
32189
+ rhythmProperties,
31856
32190
  s101ArtworkProvenance,
31857
32191
  sanitizeCustomStyleConfig,
31858
32192
  scaleSizeValue,
31859
32193
  scaminToMinZoom,
31860
32194
  sectorLimitToArcBearing,
31861
32195
  styleModeBand,
32196
+ terrainStackCount,
31862
32197
  tileTemplate,
31863
32198
  uncoveredSectorArcs,
31864
32199
  withinBboxFilter,