@chrtco/sdk 0.10.38 → 0.10.40

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}`;
@@ -638,14 +673,15 @@ function buildSources(opts = {}) {
638
673
  type: "vector",
639
674
  tiles: [tileTemplate(base, BASE_REGION, "mvt", buildFor(BASE_REGION))],
640
675
  minzoom: BASE_MINZOOM,
641
- maxzoom: BASE_MAXZOOM
676
+ maxzoom: BASE_MAXZOOM,
677
+ attribution: BASE_ATTRIBUTION
642
678
  };
643
679
  }
644
680
  if (opts.relief) {
645
681
  sources[RELIEF_REGION] = {
646
682
  type: "raster-dem",
647
683
  tiles: [tileTemplate(base, RELIEF_REGION, "png", buildFor(RELIEF_REGION))],
648
- encoding: "terrarium",
684
+ encoding: RELIEF_ENCODING,
649
685
  tileSize: RELIEF_TILE_SIZE,
650
686
  minzoom: RELIEF_MINZOOM,
651
687
  maxzoom: RELIEF_MAXZOOM
@@ -671,6 +707,7 @@ var TilesEndpoint = class {
671
707
  if (opts.mode) params.set("mode", opts.mode);
672
708
  if (opts.land) params.set("land", "1");
673
709
  if (opts.relief) params.set("relief", "1");
710
+ if (opts.units === "imperial") params.set("units", "imperial");
674
711
  const query2 = params.size > 0 ? `?${params.toString()}` : "";
675
712
  return `${this.tileBaseUrl}/v1/styles/${this.region}/style.json${query2}`;
676
713
  }
@@ -796,6 +833,9 @@ var WeatherEndpoint = class {
796
833
  *
797
834
  * `unavailable` names every field the station did not report, so absence is
798
835
  * stated rather than inferred from a missing key.
836
+ *
837
+ * Pass `hours` for the station's recent readings as `series` — NDBC's own
838
+ * readings at NDBC's own instants, never interpolated.
799
839
  */
800
840
  async conditions(options) {
801
841
  return this.http.request(
@@ -804,7 +844,8 @@ var WeatherEndpoint = class {
804
844
  lat: options.lat,
805
845
  lon: options.lon,
806
846
  units: options.units,
807
- station: options.station
847
+ station: options.station,
848
+ hours: options.hours
808
849
  },
809
850
  options.signal
810
851
  );
@@ -1268,10 +1309,10 @@ async function readManifest(manifestPath, fs) {
1268
1309
  if (!manifestPath) return emptyManifest();
1269
1310
  if (!await fs.exists(manifestPath)) return emptyManifest();
1270
1311
  const raw = await fs.readFile(manifestPath);
1271
- const text = new TextDecoder().decode(raw);
1312
+ const text2 = new TextDecoder().decode(raw);
1272
1313
  let parsed;
1273
1314
  try {
1274
- parsed = JSON.parse(text);
1315
+ parsed = JSON.parse(text2);
1275
1316
  } catch {
1276
1317
  throw new OfflineError(
1277
1318
  "unexpected_response",
@@ -1295,8 +1336,8 @@ async function readManifest(manifestPath, fs) {
1295
1336
  };
1296
1337
  }
1297
1338
  async function writeManifest(manifestPath, fs, manifest) {
1298
- const text = JSON.stringify(manifest, null, 2);
1299
- const bytes = new TextEncoder().encode(text);
1339
+ const text2 = JSON.stringify(manifest, null, 2);
1340
+ const bytes = new TextEncoder().encode(text2);
1300
1341
  await fs.writeFile(manifestPath, bytes);
1301
1342
  }
1302
1343
  function usedBytes(manifest, excludeRegion) {
@@ -2261,110 +2302,438 @@ function s57AbbrTable(acronym) {
2261
2302
  return cached;
2262
2303
  }
2263
2304
 
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"
2305
+ // src/atlas/s52-colors.ts
2306
+ var S52_DAY = {
2307
+ // Depth
2308
+ NODTA: "#a4a4a4",
2309
+ DEPIT: "#009678",
2310
+ DEPVS: "#7fbfdf",
2311
+ DEPMS: "#a7d4e8",
2312
+ DEPMD: "#cae7f3",
2313
+ DEPDW: "#ecf7fc",
2314
+ // Land / topography
2315
+ LANDA: "#e6d1a1",
2316
+ LANDF: "#c8b078",
2317
+ CHBRN: "#ab7948",
2318
+ CHGRF: "#878787",
2319
+ CHGRD: "#555555",
2320
+ // Chart symbology
2321
+ CHBLK: "#000000",
2322
+ CSTLN: "#000000",
2323
+ OUTLW: "#000000",
2324
+ BKAJ1: "#4d4d4d",
2325
+ // Navigation / traffic
2326
+ TRFCD: "#d200d2",
2327
+ TRFCF: "#e680e6",
2328
+ RESBL: "#0064b4",
2329
+ APTS1: "#d200d2",
2330
+ // Danger / hazard
2331
+ DNGHL: "#ff0000",
2332
+ ISHDG: "#cc0000",
2333
+ // Lights
2334
+ LITRD: "#ee0000",
2335
+ LITGN: "#00c800",
2336
+ LITYW: "#fddc00",
2337
+ LITWT: "#ffffff",
2338
+ LITBL: "#0064ff",
2339
+ LITOR: "#ff8c00",
2340
+ // Buoys / marks
2341
+ RADHI: "#68e456",
2342
+ RADLO: "#3f8a34",
2343
+ PLRTE: "#dc4025",
2344
+ ARPAT: "#3fa56f",
2345
+ ISDNG: "#c545c3",
2346
+ // Chart colours
2347
+ CHRED: "#f15469",
2348
+ CHGRN: "#68e456",
2349
+ CHYLW: "#f4da48",
2350
+ CHMGD: "#c545c3",
2351
+ CHMGF: "#d3a6e9",
2352
+ CHWHT: "#d4eaee",
2353
+ CHCOR: "#eb7d36",
2354
+ // Soundings
2355
+ SNDG1: "#7d898c",
2356
+ SNDG2: "#070707",
2357
+ DEPSC: "#525a5c",
2358
+ DEPCN: "#555555",
2359
+ // Utility / infrastructure
2360
+ UINFD: "#070707",
2361
+ UINFF: "#7d898c",
2362
+ UINFR: "#f15469",
2363
+ UINFG: "#68e456",
2364
+ UINFO: "#eb7d36",
2365
+ UINFB: "#3a78f0",
2366
+ UINFM: "#c545c3",
2367
+ UIBCK: "#d4eaee",
2368
+ UIBDR: "#7d898c",
2369
+ UIAFD: "#73b6ef",
2370
+ UIAFF: "#c9b97a",
2371
+ PIPLN: "#55aa00",
2372
+ // Outlines
2373
+ OUTLL: "#c9b97a",
2374
+ // Miscellaneous
2375
+ SCLBR: "#eb7d36",
2376
+ NINFO: "#eb7d36",
2377
+ ADINF: "#b29f34",
2378
+ RESGR: "#7d898c",
2379
+ SHIPS: "#070707",
2380
+ PSTRK: "#070707",
2381
+ SYTRK: "#7d898c",
2382
+ APLRT: "#eb7d36",
2383
+ CURSR: "#eb7d36",
2384
+ BKAJ2: "#232728",
2385
+ RES01: "#a3b4b7",
2386
+ RES02: "#a3b4b7",
2387
+ RES03: "#a3b4b7"
2289
2388
  };
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"
2389
+ var S52_DUSK = {
2390
+ NODTA: "#6b6b6b",
2391
+ DEPIT: "#006650",
2392
+ DEPVS: "#487085",
2393
+ DEPMS: "#608a9d",
2394
+ DEPMD: "#7ba6b8",
2395
+ DEPDW: "#95bfd0",
2396
+ LANDA: "#8a7d5e",
2397
+ LANDF: "#7a6a48",
2398
+ CHBRN: "#6e4e30",
2399
+ CHGRF: "#585858",
2400
+ CHGRD: "#3a3a3a",
2401
+ CHBLK: "#b0b0b0",
2402
+ CSTLN: "#b0b0b0",
2403
+ OUTLW: "#b0b0b0",
2404
+ BKAJ1: "#707070",
2405
+ TRFCD: "#a000a0",
2406
+ TRFCF: "#b060b0",
2407
+ RESBL: "#004a88",
2408
+ APTS1: "#a000a0",
2409
+ DNGHL: "#cc0000",
2410
+ ISHDG: "#990000",
2411
+ LITRD: "#bb0000",
2412
+ LITGN: "#009900",
2413
+ LITYW: "#c8b000",
2414
+ LITWT: "#d0d0d0",
2415
+ LITBL: "#004abb",
2416
+ LITOR: "#cc7000",
2417
+ RADHI: "#009940",
2418
+ RADLO: "#2d6330",
2419
+ PLRTE: "#a03020",
2420
+ ARPAT: "#306040",
2421
+ ISDNG: "#903090",
2422
+ CHRED: "#b05060",
2423
+ CHGRN: "#509040",
2424
+ CHYLW: "#b0a030",
2425
+ CHMGD: "#903090",
2426
+ CHMGF: "#905090",
2427
+ CHWHT: "#90a0a0",
2428
+ CHCOR: "#a06030",
2429
+ SNDG1: "#606060",
2430
+ SNDG2: "#b0b0b0",
2431
+ DEPSC: "#606060",
2432
+ DEPCN: "#2E2E2E",
2433
+ UINFD: "#b0b0b0",
2434
+ UINFF: "#606060",
2435
+ UINFR: "#b05060",
2436
+ UINFG: "#509040",
2437
+ UINFO: "#a06030",
2438
+ UINFB: "#304090",
2439
+ UINFM: "#903090",
2440
+ UIBCK: "#1a1a1a",
2441
+ UIBDR: "#606060",
2442
+ UIAFD: "#405060",
2443
+ UIAFF: "#605840",
2444
+ PIPLN: "#3d7a00",
2445
+ OUTLL: "#605840",
2446
+ SCLBR: "#a06030",
2447
+ NINFO: "#a06030",
2448
+ ADINF: "#807020",
2449
+ RESGR: "#606060",
2450
+ SHIPS: "#b0b0b0",
2451
+ PSTRK: "#b0b0b0",
2452
+ SYTRK: "#606060",
2453
+ APLRT: "#a06030",
2454
+ CURSR: "#cc0000",
2455
+ BKAJ2: "#252525",
2456
+ RES01: "#606060",
2457
+ RES02: "#606060",
2458
+ RES03: "#606060"
2336
2459
  };
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",
2460
+ var S52_NIGHT = {
2461
+ NODTA: "#3a3a32",
2462
+ DEPIT: "#003828",
2463
+ DEPVS: "#356577",
2464
+ DEPMS: "#265066",
2465
+ DEPMD: "#102e44",
2466
+ DEPDW: "#051a28",
2467
+ LANDA: "#3a3020",
2468
+ LANDF: "#302818",
2469
+ CHBRN: "#382410",
2470
+ CHGRF: "#2a2a22",
2471
+ CHGRD: "#1e1e18",
2472
+ CHBLK: "#c8aa78",
2473
+ CSTLN: "#c8aa78",
2474
+ OUTLW: "#c8aa78",
2475
+ BKAJ1: "#504830",
2476
+ TRFCD: "#6e006e",
2477
+ TRFCF: "#803880",
2478
+ RESBL: "#002a50",
2479
+ APTS1: "#6e006e",
2480
+ DNGHL: "#a00000",
2481
+ ISHDG: "#700000",
2482
+ LITRD: "#880000",
2483
+ LITGN: "#006600",
2484
+ LITYW: "#887800",
2485
+ LITWT: "#a09878",
2486
+ LITBL: "#003088",
2487
+ LITOR: "#884800",
2488
+ RADHI: "#005528",
2489
+ RADLO: "#1a3a20",
2490
+ PLRTE: "#6e2018",
2491
+ ARPAT: "#1a3028",
2492
+ ISDNG: "#481848",
2493
+ CHRED: "#682030",
2494
+ CHGRN: "#285020",
2495
+ CHYLW: "#685828",
2496
+ CHMGD: "#481848",
2497
+ CHMGF: "#482848",
2498
+ CHWHT: "#484840",
2499
+ CHCOR: "#683818",
2500
+ SNDG1: "#383028",
2501
+ SNDG2: "#c8aa78",
2502
+ DEPSC: "#383028",
2503
+ // ⚠ DELIBERATE DEVIATION from the published S-52 NIGHT value (#1f2223),
2504
+ // recorded here because `docs/validation/s52-spec-gap-matrix.json` tracks
2505
+ // this token as `value-differs` and the CHRTstyles programme is bringing
2506
+ // colours TO spec — this one moves further away, on purpose.
2507
+ //
2508
+ // Spec NIGHT DEPCN is near-black, which measures ~1.1:1 against this
2509
+ // palette's own deep water (#051a28). S-52's NIGHT table assumes a
2510
+ // dark-adapted eye in a darkened wheelhouse; on a phone or a laptop at
2511
+ // anchor it is not dim, it is absent — reported as "can't see bottom
2512
+ // contours in night mode only". A contour nobody can see fails the
2513
+ // standard's intent while matching its number.
2514
+ //
2515
+ // The palette already makes this trade elsewhere and in the same
2516
+ // direction: NIGHT's CHBLK/CSTLN/OUTLW are #c8aa78, light warm linework on
2517
+ // dark water. This value is that family, dimmed to stay subordinate to the
2518
+ // coastline. Revisit with the CHRTstyles programme, which owns the
2519
+ // spec-vs-legibility call across all 63 tokens.
2520
+ DEPCN: "#A68E84",
2521
+ UINFD: "#c8aa78",
2522
+ UINFF: "#383028",
2523
+ UINFR: "#682030",
2524
+ UINFG: "#285020",
2525
+ UINFO: "#683818",
2526
+ UINFB: "#182848",
2527
+ UINFM: "#481848",
2528
+ UIBCK: "#0a0a08",
2529
+ UIBDR: "#383028",
2530
+ UIAFD: "#182838",
2531
+ UIAFF: "#302818",
2532
+ PIPLN: "#284800",
2533
+ OUTLL: "#302818",
2534
+ SCLBR: "#683818",
2535
+ NINFO: "#683818",
2536
+ ADINF: "#483818",
2537
+ RESGR: "#383028",
2538
+ SHIPS: "#c8aa78",
2539
+ PSTRK: "#c8aa78",
2540
+ SYTRK: "#484030",
2541
+ APLRT: "#683818",
2542
+ CURSR: "#a00000",
2543
+ BKAJ2: "#0a0a08",
2544
+ RES01: "#383028",
2545
+ RES02: "#383028",
2546
+ RES03: "#383028"
2547
+ };
2548
+ var S52_COLORS = S52_DAY;
2549
+ var COLOR_TABLES = {
2550
+ DAY: S52_DAY,
2551
+ DUSK: S52_DUSK,
2552
+ NIGHT: S52_NIGHT
2553
+ };
2554
+ function getColorTable(name) {
2555
+ return COLOR_TABLES[name];
2556
+ }
2557
+ function resolveColor(token, table = "DAY") {
2558
+ const t = COLOR_TABLES[table];
2559
+ return t[token] ?? (table === "NIGHT" ? "#c8aa78" : "#000000");
2560
+ }
2561
+
2562
+ // src/interact/light-sigseq.ts
2563
+ var SIGSEQ_MAX_TERMS = 24;
2564
+ var SIGPER_TOLERANCE_SECONDS = 0.06;
2565
+ var LAMP_TICK_MS = 100;
2566
+ var TERM = /^\d+(?:\.\d+)?$/;
2567
+ function text(v) {
2568
+ if (v === null || v === void 0) return null;
2569
+ const s = String(v).trim();
2570
+ return s === "" ? null : s;
2571
+ }
2572
+ function parseSigseqTerms(sigseq, sigper) {
2573
+ const raw = text(sigseq);
2574
+ if (raw === null) return { refusal: "absent" };
2575
+ const terms = raw.split("+");
2576
+ if (terms.length === 0 || terms.length > SIGSEQ_MAX_TERMS) return { refusal: "malformed" };
2577
+ const phases = [];
2578
+ for (const term of terms) {
2579
+ const t = term.trim();
2580
+ if (t === "") return { refusal: "malformed" };
2581
+ const isEclipse = t.startsWith("(") && t.endsWith(")");
2582
+ const body = (isEclipse ? t.slice(1, -1) : t).trim();
2583
+ if (!TERM.test(body)) return { refusal: "malformed" };
2584
+ const seconds = Number(body);
2585
+ if (!(seconds > 0) || !Number.isFinite(seconds)) return { refusal: "nonPositiveDuration" };
2586
+ phases.push({ seconds, isOn: !isEclipse });
2587
+ }
2588
+ const sum = phases.reduce((t, p) => t + p.seconds, 0);
2589
+ if (!(sum > 0)) return { refusal: "nonPositiveDuration" };
2590
+ if (sigper !== null && sigper > 0) {
2591
+ if (Math.abs(sum - sigper) > SIGPER_TOLERANCE_SECONDS) return { refusal: "sigperMismatch" };
2592
+ return { phases, periodSeconds: sigper, checkedAgainstSigper: true };
2593
+ }
2594
+ return { phases, periodSeconds: sum, checkedAgainstSigper: false };
2595
+ }
2596
+ function rhythmProperties(parsed, onColoursHex) {
2597
+ if (parsed === null || parsed.phases.length === 0) return { rp: 0, rn: 0 };
2598
+ const out = { rp: parsed.periodSeconds, rn: parsed.phases.length };
2599
+ let end = 0;
2600
+ let onIndex = 0;
2601
+ parsed.phases.forEach((p, i) => {
2602
+ end += p.seconds;
2603
+ const k = i + 1;
2604
+ out[`r${k}`] = Math.round(end * 1e3) / 1e3;
2605
+ out[`o${k}`] = p.isOn ? 1 : 0;
2606
+ if (p.isOn) {
2607
+ out[`c${k}`] = onColoursHex[onIndex % Math.max(1, onColoursHex.length)] ?? "#ffffff";
2608
+ onIndex += 1;
2609
+ }
2610
+ });
2611
+ return out;
2612
+ }
2613
+ function phaseExpr(nowSeconds2) {
2614
+ return ["%", nowSeconds2, ["max", ["get", "rp"], 1e-3]];
2615
+ }
2616
+ function lampRhythmOpacity(nowSeconds2, base) {
2617
+ const cases = ["case", ["<=", ["get", "rn"], 0], base];
2618
+ for (let k = 1; k <= SIGSEQ_MAX_TERMS; k++) {
2619
+ cases.push(["<", ["var", "ph"], ["coalesce", ["get", `r${k}`], 1e9]], ["*", base, ["coalesce", ["get", `o${k}`], 1]]);
2620
+ }
2621
+ cases.push(0);
2622
+ return ["let", "ph", phaseExpr(nowSeconds2), cases];
2623
+ }
2624
+ function lampRhythmColour(nowSeconds2, fallback) {
2625
+ const cases = ["case", ["<=", ["get", "rn"], 0], fallback];
2626
+ for (let k = 1; k <= SIGSEQ_MAX_TERMS; k++) {
2627
+ cases.push(["<", ["var", "ph"], ["coalesce", ["get", `r${k}`], 1e9]], ["coalesce", ["get", `c${k}`], fallback]);
2628
+ }
2629
+ cases.push(fallback);
2630
+ return ["let", "ph", phaseExpr(nowSeconds2), cases];
2631
+ }
2632
+
2633
+ // src/interact/decode.ts
2634
+ function nameAbbrTable(acronym) {
2635
+ const names = s57NameTable(acronym);
2636
+ const abbrs = s57AbbrTable(acronym);
2637
+ const out = {};
2638
+ for (const [code, name] of Object.entries(names)) {
2639
+ const abbr = abbrs[Number(code)];
2640
+ if (abbr !== void 0) out[Number(code)] = { name, abbr };
2641
+ }
2642
+ return out;
2643
+ }
2644
+ var COLOUR_HEX = {
2645
+ 1: "#ffffff",
2646
+ 2: "#333333",
2647
+ 3: "#ef4444",
2648
+ 4: "#22c55e",
2649
+ 5: "#3b82f6",
2650
+ 6: "#eab308",
2651
+ 7: "#9ca3af",
2652
+ 8: "#92400e",
2653
+ 9: "#f59e0b",
2654
+ 10: "#8b5cf6",
2655
+ 11: "#f97316",
2656
+ 12: "#d946ef",
2657
+ 13: "#f472b6"
2658
+ };
2659
+ var LITCHR_TABLE = nameAbbrTable("LITCHR");
2660
+ var COLOUR_TABLE = Object.fromEntries(
2661
+ 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] }])
2662
+ );
2663
+ var BOYSHP_TABLE = s57NameTable("BOYSHP");
2664
+ var CATLAM_TABLE = s57NameTable("CATLAM");
2665
+ var CATCAM_TABLE = s57NameTable("CATCAM");
2666
+ var CATLMK_TABLE = s57NameTable("CATLMK");
2667
+ var CATFOG_TABLE = s57NameTable("CATFOG");
2668
+ var CATFOG_ABBR_TABLE = s57AbbrTable("CATFOG");
2669
+ var CATFOG_NAUTOPHONE = Object.values(CATFOG_CODES).find(
2670
+ (c) => c.name.toLowerCase() === "nautophone"
2671
+ )?.code;
2672
+ var CATLIT_TABLE = s57NameTable("CATLIT");
2673
+ var TOPSHP_TABLE = s57NameTable("TOPSHP");
2674
+ var STATUS_TABLE = s57NameTable("STATUS");
2675
+ var MARSYS_TABLE = s57NameTable("MARSYS");
2676
+ var LAYER_TYPE_MAP = {
2677
+ BOYLAT: "Lateral Buoy",
2678
+ BOYCAR: "Cardinal Buoy",
2679
+ BOYSPP: "Special Purpose Buoy",
2680
+ BOYISD: "Isolated Danger Buoy",
2681
+ BOYSAW: "Safe Water Buoy",
2682
+ BCNLAT: "Lateral Beacon",
2683
+ BCNCAR: "Cardinal Beacon",
2684
+ BCNSPP: "Special Purpose Beacon",
2685
+ BCNISD: "Isolated Danger Beacon",
2686
+ BCNSAW: "Safe Water Beacon",
2687
+ LIGHTS: "Light",
2688
+ DAYMAR: "Daymark",
2689
+ FOGSIG: "Fog Signal",
2690
+ RTPBCN: "Radar Transponder Beacon",
2691
+ RDOSTA: "Radio/Radar Station",
2692
+ TOPMAR: "Topmark",
2693
+ MORFAC: "Mooring Facility",
2694
+ LNDMRK: "Landmark",
2695
+ SISTAT: "Signal Station",
2696
+ PILBOP: "Pilot Boarding Place",
2697
+ HRBFAC: "Harbour Facility",
2698
+ SMCFAC: "Small Craft Facility",
2699
+ CRANES: "Crane",
2700
+ OFSPLF: "Offshore Platform",
2701
+ ACHBRT: "Anchor Berth",
2702
+ DISMAR: "Distance Mark",
2703
+ LITVES: "Light Vessel",
2704
+ LITFLT: "Light Float"
2705
+ };
2706
+ var NAV_AID_QUERY_LAYERS = [
2707
+ "enc-BOYLAT",
2708
+ "enc-BOYCAR",
2709
+ "enc-BOYSPP",
2710
+ "enc-BOYISD",
2711
+ "enc-BOYSAW",
2712
+ "enc-BCNLAT",
2713
+ "enc-BCNCAR",
2714
+ "enc-BCNSPP",
2715
+ "enc-BCNISD",
2716
+ "enc-BCNSAW",
2717
+ "enc-LIGHTS",
2718
+ "enc-LIGHTS-point",
2719
+ "enc-DAYMAR",
2720
+ "enc-FOGSIG",
2721
+ "enc-RTPBCN",
2722
+ "enc-RDOSTA",
2723
+ "enc-TOPMAR",
2724
+ "enc-MORFAC",
2725
+ "enc-MORFAC-point",
2726
+ "enc-LNDMRK",
2727
+ "enc-LNDMRK-point",
2728
+ "enc-SISTAT",
2729
+ "enc-SISTAT-point",
2730
+ "enc-PILBOP",
2731
+ "enc-PILBOP-point",
2732
+ "enc-HRBFAC",
2733
+ "enc-SMCFAC",
2734
+ "enc-CRANES",
2735
+ "enc-CRANES-point",
2736
+ "enc-OFSPLF",
2368
2737
  "enc-OFSPLF-point",
2369
2738
  "enc-ACHBRT",
2370
2739
  "enc-DISMAR",
@@ -3167,14 +3536,8 @@ function decodeNavAidProps(layerName, props, coords) {
3167
3536
  const valnmr = props.VALNMR != null ? Number(props.VALNMR) : null;
3168
3537
  const nominalRange = valnmr != null && !isNaN(valnmr) && valnmr > 0 ? valnmr : null;
3169
3538
  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
- });
3539
+ const sec = sectorFromProps(props, sectr1, sectr2, "DAY", false);
3540
+ if (sec) sectors.push({ ...sec, notation });
3178
3541
  }
3179
3542
  return {
3180
3543
  name,
@@ -3192,6 +3555,7 @@ function decodeNavAidProps(layerName, props, coords) {
3192
3555
  lightCharacterFull: litchrEntry?.name ?? null,
3193
3556
  period: sigper,
3194
3557
  group,
3558
+ sigseq: props.SIGSEQ != null && String(props.SIGSEQ).trim() !== "" ? String(props.SIGSEQ).trim() : null,
3195
3559
  fogSignal: fogSignal2,
3196
3560
  fogSignalAbbr,
3197
3561
  radarReflector,
@@ -3222,6 +3586,18 @@ function buildNotation(litchrAbbr, colourAbbrs, sigper, group) {
3222
3586
  function buildFlashPattern(decoded) {
3223
3587
  const litchr = decoded.lightCharacter;
3224
3588
  if (!litchr) return null;
3589
+ const charted = parseSigseqTerms(decoded.sigseq, decoded.period);
3590
+ if (!("refusal" in charted)) {
3591
+ const colours2 = decoded.colourHexList.length > 0 ? decoded.colourHexList : [decoded.lightColourHex];
3592
+ let onIndex = 0;
3593
+ const chartedPhases = charted.phases.map((p) => {
3594
+ if (!p.isOn) return { on: false, duration: p.seconds * 1e3 };
3595
+ const color = colours2[onIndex % colours2.length];
3596
+ onIndex += 1;
3597
+ return { on: true, duration: p.seconds * 1e3, color };
3598
+ });
3599
+ return { phases: chartedPhases, totalCycle: charted.periodSeconds * 1e3, notation: decoded.notation, timing: "charted" };
3600
+ }
3225
3601
  const period = decoded.period && decoded.period > 0 ? decoded.period * 1e3 : 6e3;
3226
3602
  const groups = decoded.group ?? [1];
3227
3603
  const totalFlashes = groups.reduce((a, b) => a + b, 0);
@@ -3390,7 +3766,7 @@ function buildFlashPattern(decoded) {
3390
3766
  }
3391
3767
  }
3392
3768
  const totalCycle = phases.reduce((sum, p) => sum + p.duration, 0);
3393
- return { phases, totalCycle, notation };
3769
+ return { timing: "invented", phases, totalCycle, notation };
3394
3770
  }
3395
3771
  function uncoveredSectorArcs(sectors) {
3396
3772
  const norm = (d) => (d % 360 + 360) % 360;
@@ -3429,7 +3805,70 @@ function uncoveredSectorArcs(sectors) {
3429
3805
  }
3430
3806
  return gaps.filter(([a, b]) => Math.abs(b - a) >= 0.05 || b < a).map(([a, b]) => ({ start: norm(a), end: norm(b) }));
3431
3807
  }
3432
- function buildSectorsFromFeatures(features) {
3808
+ function parseCodes(val) {
3809
+ if (val == null) return [];
3810
+ const str = String(val).trim();
3811
+ let parts;
3812
+ if (str.startsWith("[")) {
3813
+ try {
3814
+ const arr = JSON.parse(str);
3815
+ parts = Array.isArray(arr) ? arr.map(String) : [];
3816
+ } catch {
3817
+ parts = str.split(/[,;]/);
3818
+ }
3819
+ } else {
3820
+ parts = str.split(/[,;]/);
3821
+ }
3822
+ return parts.map((p) => Number(p.trim())).filter((n) => !isNaN(n));
3823
+ }
3824
+ var OBSCURED_LITVIS = /* @__PURE__ */ new Set([3, 7, 8]);
3825
+ function sectorArcColourToken(colourCodes, obscured) {
3826
+ if (obscured) return "CHBRN";
3827
+ const has = (c) => colourCodes.includes(c);
3828
+ if (colourCodes.length === 1) {
3829
+ if (has(3)) return "LITRD";
3830
+ if (has(4)) return "LITGN";
3831
+ if (has(1) || has(6) || has(11)) return "LITYW";
3832
+ return "CHMGD";
3833
+ }
3834
+ if (colourCodes.length === 2) {
3835
+ if (has(1) && has(3)) return "LITRD";
3836
+ if (has(1) && has(4)) return "LITGN";
3837
+ return "CHMGD";
3838
+ }
3839
+ return "CHMGD";
3840
+ }
3841
+ function sectorFromProps(props, s1, s2, ct, allRound) {
3842
+ const colourCodes = parseCodes(props.COLOUR);
3843
+ const entries = colourCodes.map((c) => COLOUR_TABLE[c]).filter((e) => e != null);
3844
+ if (allRound && entries.length === 0) return null;
3845
+ const obscured = parseCodes(props.LITVIS).some((v) => OBSCURED_LITVIS.has(v));
3846
+ const litchrCode = parseCode(props.LITCHR);
3847
+ const le = litchrCode ? LITCHR_TABLE[litchrCode] : null;
3848
+ const sigper = props.SIGPER != null ? Number(props.SIGPER) : null;
3849
+ const group = parseGroup(props.SIGGRP);
3850
+ const base = buildNotation(le?.abbr ?? null, entries.length > 0 ? [entries.map((e) => e.abbr).join("")] : [], sigper, group);
3851
+ const notation = obscured && base ? `${base} (obsc)` : base;
3852
+ const valnmr = props.VALNMR != null ? Number(props.VALNMR) : null;
3853
+ const nominalRange = valnmr != null && !isNaN(valnmr) && valnmr > 0 ? valnmr : null;
3854
+ const arcColourToken = sectorArcColourToken(colourCodes, obscured);
3855
+ return {
3856
+ sectorStart: s1,
3857
+ sectorEnd: s2,
3858
+ colour: entries[0]?.name ?? "White",
3859
+ colourHex: entries[0]?.hex ?? "#ffffff",
3860
+ notation,
3861
+ nominalRange,
3862
+ colourCodes,
3863
+ arcColourToken,
3864
+ arcColourHex: resolveColor(arcColourToken, ct),
3865
+ obscured,
3866
+ sigseq: props.SIGSEQ != null && String(props.SIGSEQ).trim() !== "" ? String(props.SIGSEQ).trim() : null,
3867
+ sigper: sigper != null && !isNaN(sigper) && sigper > 0 ? sigper : null,
3868
+ ...allRound ? { allRound: true } : {}
3869
+ };
3870
+ }
3871
+ function buildSectorsFromFeatures(features, ct = "DAY") {
3433
3872
  const sectors = [];
3434
3873
  const allRound = [];
3435
3874
  for (const f of features) {
@@ -3440,47 +3879,15 @@ function buildSectorsFromFeatures(features) {
3440
3879
  if (props.SECTR1 == null && props.SECTR2 == null && parseCode(props.COLOUR) != null) allRound.push(props);
3441
3880
  continue;
3442
3881
  }
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
- });
3882
+ const sec = sectorFromProps(props, s1, s2, ct, false);
3883
+ if (sec) sectors.push(sec);
3460
3884
  }
3461
3885
  if (sectors.length > 0 && allRound.length > 0) {
3462
3886
  const gaps = uncoveredSectorArcs(sectors);
3463
3887
  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
3888
  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
- });
3889
+ const sec = sectorFromProps(props, gap.start, gap.end, ct, true);
3890
+ if (sec) sectors.push(sec);
3484
3891
  }
3485
3892
  }
3486
3893
  }
@@ -3530,12 +3937,16 @@ function buildSectorArcGeoJSON(center, startBearing, endBearing, radiusNm = 0.5,
3530
3937
  }
3531
3938
  function buildSectorArcsFC(center, sectors, radiusNm = 0.4, innerRadiusFrac = 0.92) {
3532
3939
  const innerNm = innerRadiusFrac > 0 ? radiusNm * innerRadiusFrac : 0;
3533
- const features = sectors.map((sec, i) => {
3940
+ const drawn = sectors.filter((sec) => !sec.allRound);
3941
+ const features = drawn.map((sec, i) => {
3534
3942
  const arc = buildSectorArcGeoJSON(center, sec.sectorStart, sec.sectorEnd, radiusNm, innerNm);
3535
3943
  arc.properties = {
3536
3944
  sectorIndex: i,
3537
3945
  colour: sec.colour,
3538
3946
  colourHex: sec.colourHex,
3947
+ arcColourHex: sec.arcColourHex,
3948
+ arcColourToken: sec.arcColourToken,
3949
+ obscured: sec.obscured,
3539
3950
  notation: sec.notation,
3540
3951
  sectorStart: sec.sectorStart,
3541
3952
  sectorEnd: sec.sectorEnd,
@@ -3548,7 +3959,7 @@ function buildSectorArcsFC(center, sectors, radiusNm = 0.4, innerRadiusFrac = 0.
3548
3959
  const seenBearings = /* @__PURE__ */ new Set();
3549
3960
  const TICK_OUTER_OVERHANG = 1.05;
3550
3961
  const TICK_INNER_OVERHANG = 0.93;
3551
- for (const sec of sectors) {
3962
+ for (const sec of drawn) {
3552
3963
  for (const bearing of [sec.sectorStart, sec.sectorEnd]) {
3553
3964
  const rad = sectorLimitToArcBearing(bearing) * Math.PI / 180;
3554
3965
  const radiusDeg = radiusNm / 60;
@@ -4418,10 +4829,10 @@ function ensureStyles(doc) {
4418
4829
  doc.head.appendChild(style);
4419
4830
  styleInjected = true;
4420
4831
  }
4421
- function el(doc, tag, className, text) {
4832
+ function el(doc, tag, className, text2) {
4422
4833
  const node = doc.createElement(tag);
4423
4834
  node.className = className;
4424
- if (text != null) node.textContent = text;
4835
+ if (text2 != null) node.textContent = text2;
4425
4836
  return node;
4426
4837
  }
4427
4838
  function animateDot(dot, pattern) {
@@ -4587,460 +4998,203 @@ ${notice.dataCurrency}`;
4587
4998
  this.place();
4588
4999
  this.map.on("move", this.reposition);
4589
5000
  }
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"
5001
+ place() {
5002
+ if (!this.root || !this.lngLat) return;
5003
+ const { x, y } = this.map.project(this.lngLat);
5004
+ this.root.style.left = `${x}px`;
5005
+ this.root.style.top = `${y}px`;
5006
+ }
5007
+ close() {
5008
+ if (this.stopAnimation) {
5009
+ this.stopAnimation();
5010
+ this.stopAnimation = null;
5011
+ }
5012
+ if (this.root) {
5013
+ this.map.off("move", this.reposition);
5014
+ this.root.remove();
5015
+ this.root = null;
5016
+ }
5017
+ this.lngLat = null;
5018
+ }
5030
5019
  };
5031
- var S52_COLORS = S52_DAY;
5032
- var COLOR_TABLES = {
5033
- DAY: S52_DAY,
5034
- DUSK: S52_DUSK,
5035
- NIGHT: S52_NIGHT
5020
+
5021
+ // src/interact/index.ts
5022
+ var InteractClient = class {
5023
+ constructor(http) {
5024
+ this.http = http;
5025
+ this.entitlementClient = new EntitlementClient(http);
5026
+ }
5027
+ entitlementClient;
5028
+ /**
5029
+ * Check the CHRTinteract entitlement for this API key (server-side, plan
5030
+ * derived, cached per client). Throws `InteractError` with code
5031
+ * `interact_not_enabled` for Free-plan teams.
5032
+ */
5033
+ async entitlement() {
5034
+ return this.entitlementClient.check();
5035
+ }
5036
+ /**
5037
+ * Decode a chart feature (from `queryRenderedFeatures`, the features API,
5038
+ * or plain `{ layer, properties }`) into a typed {@link InspectResult}.
5039
+ *
5040
+ * The first call performs the server-side entitlement check. With
5041
+ * `{ enrich: true }` the result is upgraded with full S-57 properties and
5042
+ * source-cell provenance (edition / update / issue date) via the features
5043
+ * API — metered against the key's `featureCalls` quota.
5044
+ *
5045
+ * @example
5046
+ * const result = await chrt.interact.inspect(feature);
5047
+ * if (result.kind === "navaid") console.log(result.characteristic); // "Fl R 4s"
5048
+ */
5049
+ async inspect(feature, opts) {
5050
+ await this.entitlementClient.check();
5051
+ const result = buildInspectResult(feature, opts);
5052
+ if (opts?.enrich) {
5053
+ return enrichResult(this.http, result, opts.signal);
5054
+ }
5055
+ return result;
5056
+ }
5057
+ /**
5058
+ * Collect EVERY inspectable feature under a point on a CHRT map —
5059
+ * deduplicated across tile boundaries by S-57 identity (`LNAM`) and
5060
+ * ordered by class priority (point targets > lines > areas, safety
5061
+ * classes first within each band). Area features never preempt the buoy
5062
+ * on top of them but always stay selectable: a click on open water
5063
+ * resolves to its DEPARE.
5064
+ *
5065
+ * `point` is a screen pixel point (`e.point` from a map click) or
5066
+ * `[lng, lat]`. Point/line targets are picked up within
5067
+ * `opts.tolerance` px (default 8); polygon candidates require the exact
5068
+ * point to fall inside them.
5069
+ *
5070
+ * The first call performs the server-side entitlement check.
5071
+ *
5072
+ * @example
5073
+ * const { candidates, select } = await chrt.interact.inspectAll(map, e.point);
5074
+ * const result = await select(candidates[0]); // full decode (+ merge)
5075
+ */
5076
+ async inspectAll(map, point, opts = {}) {
5077
+ if (!map || typeof map.queryRenderedFeatures !== "function") {
5078
+ throw new InteractError(
5079
+ "unexpected_response",
5080
+ "inspectAll(): expected a MapLibre / Mapbox GL map instance"
5081
+ );
5082
+ }
5083
+ await this.entitlementClient.check();
5084
+ const screen = Array.isArray(point) ? map.project([point[0], point[1]]) : point;
5085
+ const lngLat = Array.isArray(point) ? { lng: point[0], lat: point[1] } : map.unproject?.([point.x, point.y]) ?? null;
5086
+ const at = { point: screen, lngLat };
5087
+ const candidates = collectClickCandidates(map, screen, opts);
5088
+ const http = this.http;
5089
+ return {
5090
+ candidates,
5091
+ select: async (candidate, selectOpts) => {
5092
+ const result = decodeCandidate(map, candidate, at, opts);
5093
+ if (selectOpts?.enrich) {
5094
+ return enrichResult(http, result, selectOpts.signal);
5095
+ }
5096
+ return result;
5097
+ }
5098
+ };
5099
+ }
5100
+ /**
5101
+ * Attach click-to-inspect to a MapLibre / Mapbox GL map rendering CHRT
5102
+ * tiles. Resolves once the server-side entitlement check passes; rejects
5103
+ * with `InteractError` (`interact_not_enabled` on Free plans) otherwise —
5104
+ * no listeners are wired on denial.
5105
+ *
5106
+ * Headless-first: pass `onInspect` to receive every decoded
5107
+ * {@link InspectResult} and render your own UI. Without callbacks, the
5108
+ * built-in minimal popup is shown (opt back in explicitly with
5109
+ * `popup: true` to have both).
5110
+ *
5111
+ * A click that hits SEVERAL features (deduplicated by S-57 identity)
5112
+ * disambiguates instead of silently taking the topmost:
5113
+ * - `onCandidates` provided → it receives the priority-ordered list;
5114
+ * resolve a choice with `ctx.select(candidate)`.
5115
+ * - built-in popup → shows a candidate list; picking a row opens the
5116
+ * result card.
5117
+ * - `onInspect` only → the top candidate is inspected immediately, and
5118
+ * clicking the SAME spot again cycles to the next candidate — so an
5119
+ * area feature under a buoy is reachable with a second click.
5120
+ *
5121
+ * @example
5122
+ * const handle = await chrt.interact.attach(map, {
5123
+ * onInspect: (result) => console.log(result.title, result.characteristic),
5124
+ * });
5125
+ * // later: handle.detach();
5126
+ */
5127
+ async attach(map, opts = {}) {
5128
+ if (!map || typeof map.queryRenderedFeatures !== "function" || typeof map.on !== "function") {
5129
+ throw new InteractError(
5130
+ "unexpected_response",
5131
+ "attach(): expected a MapLibre / Mapbox GL map instance"
5132
+ );
5133
+ }
5134
+ await this.entitlementClient.check();
5135
+ const showPopup = opts.popup ?? (!opts.onInspect && !opts.onCandidates);
5136
+ const popup = showPopup ? new InteractPopup(map) : null;
5137
+ const http = this.http;
5138
+ let cycle = null;
5139
+ const samePoint = (a, b) => {
5140
+ const pad = opts.tolerance ?? DEFAULT_CLICK_TOLERANCE;
5141
+ return Math.abs(a.x - b.x) <= pad && Math.abs(a.y - b.y) <= pad;
5142
+ };
5143
+ const select = (candidate, e) => {
5144
+ const result = decodeCandidate(map, candidate, e, opts);
5145
+ const finish = (r) => {
5146
+ popup?.show(r, r.coordinates ?? [e.lngLat.lng, e.lngLat.lat]);
5147
+ opts.onInspect?.(r, { lngLat: e.lngLat, point: e.point, feature: candidate.feature });
5148
+ return r;
5149
+ };
5150
+ if (opts.enrich) {
5151
+ return enrichResult(http, result).then(finish).catch(() => finish(result));
5152
+ }
5153
+ return Promise.resolve(finish(result));
5154
+ };
5155
+ const clickHandler = (e) => {
5156
+ const candidates = collectClickCandidates(map, e.point, opts);
5157
+ if (candidates.length === 0) {
5158
+ cycle = null;
5159
+ popup?.close();
5160
+ opts.onMiss?.({ lngLat: e.lngLat });
5161
+ return;
5162
+ }
5163
+ if (candidates.length === 1) {
5164
+ cycle = null;
5165
+ void select(candidates[0], e);
5166
+ return;
5167
+ }
5168
+ if (opts.onCandidates || popup) {
5169
+ cycle = null;
5170
+ if (popup) {
5171
+ popup.showList(candidates, [e.lngLat.lng, e.lngLat.lat], (c) => void select(c, e));
5172
+ }
5173
+ opts.onCandidates?.(candidates, {
5174
+ lngLat: e.lngLat,
5175
+ point: e.point,
5176
+ select: (c) => select(c, e)
5177
+ });
5178
+ return;
5179
+ }
5180
+ const key = candidates.map((c) => c.identity ?? c.layerId).join("/");
5181
+ if (cycle && cycle.key === key && samePoint(cycle.point, e.point)) {
5182
+ cycle.index = (cycle.index + 1) % candidates.length;
5183
+ cycle.point = e.point;
5184
+ } else {
5185
+ cycle = { key, point: e.point, index: 0 };
5186
+ }
5187
+ void select(candidates[cycle.index], e);
5188
+ };
5189
+ map.on("click", clickHandler);
5190
+ return {
5191
+ detach: () => {
5192
+ map.off("click", clickHandler);
5193
+ popup?.close();
5194
+ }
5195
+ };
5196
+ }
5036
5197
  };
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
5198
 
5045
5199
  // src/atlas/s52-linestyles-generated.ts
5046
5200
  var S52_LINESTYLES = {
@@ -27691,6 +27845,35 @@ function iconExpressionForFamily(objectClass, family) {
27691
27845
  }
27692
27846
  }
27693
27847
 
27848
+ // src/atlas/base-bundle.ts
27849
+ var BASE_BUNDLE_ID = BASE_REGION;
27850
+ var BASE_BUNDLE_SCHEMA = "chrt.basemap-bundle/1";
27851
+ var BASE_BUNDLE_MAXZOOM = 6;
27852
+ var BASE_BUNDLE_SOURCE_LAYERS = [
27853
+ "base_earth",
27854
+ "base_water",
27855
+ "base_boundary",
27856
+ "country_boundary"
27857
+ ];
27858
+ var BASE_BUNDLE_ATTRIBUTION = "\xA9 CHRT \xB7 Made with Natural Earth";
27859
+ var BASE_CLIENT_RASTER_BEFORE_ID = "chrtbase-nocoverage";
27860
+ function applyBaseBundle(style, opts) {
27861
+ const keep = new Set(BASE_BUNDLE_SOURCE_LAYERS);
27862
+ style.layers = style.layers.filter((raw) => {
27863
+ const layer = raw;
27864
+ if (layer.source !== BASE_REGION) return true;
27865
+ return typeof layer["source-layer"] === "string" && keep.has(layer["source-layer"]);
27866
+ });
27867
+ style.sources[BASE_REGION] = {
27868
+ type: "vector",
27869
+ tiles: [opts.tiles],
27870
+ minzoom: BASE_MINZOOM,
27871
+ maxzoom: BASE_BUNDLE_MAXZOOM,
27872
+ attribution: BASE_BUNDLE_ATTRIBUTION
27873
+ };
27874
+ return style;
27875
+ }
27876
+
27694
27877
  // src/atlas/layer-builder.ts
27695
27878
  var SCAMIN_DECLUTTER_CLASSES = /* @__PURE__ */ new Set([
27696
27879
  "OFSPLF",
@@ -29031,6 +29214,99 @@ function getOrderedClasses() {
29031
29214
  return ordered;
29032
29215
  }
29033
29216
 
29217
+ // src/atlas/terrain-order.ts
29218
+ var DRAPEABLE_LAYER_TYPES = /* @__PURE__ */ new Set([
29219
+ "background",
29220
+ "fill",
29221
+ "line",
29222
+ "raster",
29223
+ "hillshade",
29224
+ "color-relief"
29225
+ ]);
29226
+ function isDrapeable(layer) {
29227
+ return DRAPEABLE_LAYER_TYPES.has(layer.type);
29228
+ }
29229
+ function drapedFirst(layers) {
29230
+ const draped = [];
29231
+ const direct = [];
29232
+ for (const l of layers) (isDrapeable(l) ? draped : direct).push(l);
29233
+ return [...draped, ...direct];
29234
+ }
29235
+ function terrainStackCount(layers) {
29236
+ let stacks = 0;
29237
+ let prevDraped = false;
29238
+ for (const l of layers) {
29239
+ const d = isDrapeable(l);
29240
+ if (d && !prevDraped) stacks++;
29241
+ prevDraped = d;
29242
+ }
29243
+ return stacks;
29244
+ }
29245
+ function drapeHalves(layers) {
29246
+ const draped = [];
29247
+ const direct = [];
29248
+ for (const l of layers) (isDrapeable(l) ? draped : direct).push(l);
29249
+ return { draped, direct };
29250
+ }
29251
+
29252
+ // src/atlas/lamps.ts
29253
+ var LAMP_GLOW_LAYER_ID = "s52-LIGHTS-lamp-glow";
29254
+ var LAMP_CORE_LAYER_ID = "s52-LIGHTS-lamp-core";
29255
+ var LAMP_LAYER_IDS = [LAMP_GLOW_LAYER_ID, LAMP_CORE_LAYER_ID];
29256
+ var RANGE_NM = ["to-number", ["coalesce", ["get", "VALNMR"], ["get", "valnmr"]], 6];
29257
+ var UNSECTORED_POINTS = [
29258
+ "all",
29259
+ ["any", ["==", ["geometry-type"], "Point"], ["==", ["geometry-type"], "MultiPoint"]],
29260
+ ["!", ["has", "SECTR1"]],
29261
+ // …and not lights with a charted rhythm either: those flash, and the
29262
+ // flashing is computed by the app from `SIGSEQ` (light-sigseq.ts). What is
29263
+ // left here is exactly the set that is steady from every bearing.
29264
+ ["!", ["has", "SIGSEQ"]]
29265
+ ];
29266
+ function buildLampLayers(sourceId, ct) {
29267
+ const colour = lightFillExpression(ct);
29268
+ const common = {
29269
+ type: "circle",
29270
+ source: sourceId,
29271
+ "source-layer": "LIGHTS",
29272
+ filter: UNSECTORED_POINTS,
29273
+ layout: { visibility: "none" }
29274
+ };
29275
+ const metadata = { "chrt:role": "lamp", "s52:objectClass": "LIGHTS" };
29276
+ return [
29277
+ {
29278
+ ...common,
29279
+ id: LAMP_GLOW_LAYER_ID,
29280
+ paint: {
29281
+ "circle-color": colour,
29282
+ // 4 nm → 7 px, 12 nm → 12 px, 25 nm → 20 px; capped, because a
29283
+ // primary seacoast light is not a searchlight.
29284
+ "circle-radius": ["interpolate", ["linear"], RANGE_NM, 0, 5, 4, 7, 12, 12, 25, 20],
29285
+ "circle-blur": 1,
29286
+ "circle-opacity": 0.55,
29287
+ // A lamp is a point of light: it does not grow as it comes near, and
29288
+ // it does not lie down with the chart.
29289
+ "circle-pitch-alignment": "viewport",
29290
+ "circle-pitch-scale": "viewport"
29291
+ },
29292
+ metadata: { ...metadata, "chrt:lamp": "glow" }
29293
+ },
29294
+ {
29295
+ ...common,
29296
+ id: LAMP_CORE_LAYER_ID,
29297
+ paint: {
29298
+ "circle-color": colour,
29299
+ "circle-radius": ["interpolate", ["linear"], RANGE_NM, 0, 1.5, 12, 2.5, 25, 3.5],
29300
+ "circle-blur": 0.25,
29301
+ "circle-opacity": 1,
29302
+ "circle-pitch-alignment": "viewport",
29303
+ "circle-pitch-scale": "viewport"
29304
+ },
29305
+ metadata: { ...metadata, "chrt:lamp": "core" }
29306
+ }
29307
+ ];
29308
+ }
29309
+
29034
29310
  // src/atlas/band-priority.ts
29035
29311
  var BAND_PRIORITY_RANGES = {
29036
29312
  "1": { minzoom: 0 },
@@ -29066,6 +29342,17 @@ function isExemptOpaqueFill(layer) {
29066
29342
  }
29067
29343
  return false;
29068
29344
  }
29345
+ var GROUND_FILL_CLASSES = /* @__PURE__ */ new Set([
29346
+ ...DOMINANCE_EXEMPT_FILL_CLASSES,
29347
+ "RIVERS"
29348
+ ]);
29349
+ function isGroundFill(layer) {
29350
+ if (layer.type !== "fill") return false;
29351
+ for (const cls of GROUND_FILL_CLASSES) {
29352
+ if (layer.id.startsWith(`s52-${cls}-`)) return true;
29353
+ }
29354
+ return false;
29355
+ }
29069
29356
  var FILL_FLOOR_FREE_BANDS = /* @__PURE__ */ new Set([
29070
29357
  "1",
29071
29358
  "2",
@@ -29073,10 +29360,10 @@ var FILL_FLOOR_FREE_BANDS = /* @__PURE__ */ new Set([
29073
29360
  ]);
29074
29361
  function bandVariantMinzoom(layer, B) {
29075
29362
  const classMinzoom = layer.minzoom ?? 0;
29076
- if (isExemptOpaqueFill(layer) && FILL_FLOOR_FREE_BANDS.has(B)) {
29077
- return classMinzoom;
29363
+ if (isGroundFill(layer) && FILL_FLOOR_FREE_BANDS.has(B)) {
29364
+ return 0;
29078
29365
  }
29079
- if (isExemptOpaqueFill(layer)) {
29366
+ if (isGroundFill(layer)) {
29080
29367
  return BAND_PRIORITY_RANGES[B].minzoom;
29081
29368
  }
29082
29369
  return Math.max(classMinzoom, BAND_PRIORITY_RANGES[B].minzoom);
@@ -29135,11 +29422,21 @@ function styleModeBand(mode) {
29135
29422
  return mode === "continuous" ? null : mode.slice("chart:".length);
29136
29423
  }
29137
29424
  function bandFilterExpression(codes) {
29425
+ return cellBandMatch(["slice", ["get", "_cell_id"], 2, 3], codes);
29426
+ }
29427
+ var BAND_CODE_ALIASES = {
29428
+ "5": ["7"]
29429
+ };
29430
+ function cellBandCharacters(code) {
29431
+ return [code, ...BAND_CODE_ALIASES[code] ?? []];
29432
+ }
29433
+ function cellBandMatch(cellBandExpr, codes) {
29138
29434
  const list = Array.isArray(codes) ? codes : [codes];
29139
- if (list.length === 1) {
29140
- return ["==", ["slice", ["get", "_cell_id"], 2, 3], list[0]];
29435
+ const chars = [...new Set(list.flatMap((c) => cellBandCharacters(c)))];
29436
+ if (chars.length === 1) {
29437
+ return ["==", cellBandExpr, chars[0]];
29141
29438
  }
29142
- return ["in", ["slice", ["get", "_cell_id"], 2, 3], ["literal", [...list]]];
29439
+ return ["in", cellBandExpr, ["literal", chars]];
29143
29440
  }
29144
29441
  function combineWithBandFilter(baseFilter, bandFilter) {
29145
29442
  if (!baseFilter) return bandFilter;
@@ -30223,7 +30520,8 @@ function fanOrientationLabelByCellBand(label, bandFan, objectClass) {
30223
30520
  }
30224
30521
  const cellFiltered = combineWithBandFilter(
30225
30522
  label.filter,
30226
- ["==", cellBandExpr, code]
30523
+ // Aliases included (USACE `7` rides with band 5 — #269).
30524
+ cellBandMatch(cellBandExpr, code)
30227
30525
  );
30228
30526
  const minzoom = Math.max(
30229
30527
  label.minzoom ?? range.minzoom,
@@ -30428,6 +30726,7 @@ function generateStyle(palette = "DAY", opts = {}) {
30428
30726
  (b) => !TOP_STACK_LIGHT_CLASSES.has(b.objectClass) && !BOTTOM_STACK_WATER_BASE_CLASSES.has(b.objectClass)
30429
30727
  );
30430
30728
  const topLightBundles = bundles.filter((b) => TOP_STACK_LIGHT_CLASSES.has(b.objectClass));
30729
+ const lampLayers = buildLampLayers(sourceId, palette);
30431
30730
  const waterBaseBundles = bundles.filter(
30432
30731
  (b) => BOTTOM_STACK_WATER_BASE_CLASSES.has(b.objectClass)
30433
30732
  );
@@ -30461,6 +30760,7 @@ function generateStyle(palette = "DAY", opts = {}) {
30461
30760
  emitChartFeatureVariants(waterBaseBundles);
30462
30761
  emitChartFeatureVariants(regularBundles);
30463
30762
  emitChartFeatureVariants(topLightBundles);
30763
+ layers.push(...lampLayers);
30464
30764
  const chartLabelBundles = byLabelPriorityBottomUp(
30465
30765
  bundles.filter(
30466
30766
  (b) => b.label !== null && (opts.labels !== "placename" || getLabelBehavior(b.objectClass) !== "orientation-cross-band")
@@ -30538,6 +30838,7 @@ function generateStyle(palette = "DAY", opts = {}) {
30538
30838
  }
30539
30839
  }
30540
30840
  }
30841
+ layers.push(...lampLayers);
30541
30842
  {
30542
30843
  const TERMINAL_FILL_RE = new RegExp(
30543
30844
  `^s52-(?:${[...DOMINANCE_EXEMPT_FILL_CLASSES].join("|")})-fill-band[456]$`
@@ -30626,13 +30927,18 @@ function generateStyle(palette = "DAY", opts = {}) {
30626
30927
  const glyphsBase = `${opts.tileBaseUrl ?? "https://tiles.chrt.co"}/v1/glyphs`;
30627
30928
  const isLndareFill = (l) => /^s52-LNDARE-fill-band\d+$/.test(l.id);
30628
30929
  const isWaterBaseFill = (l) => /^s52-(DEPARE|DRGARE|UNSARE)-fill-band\d+$/.test(l.id);
30930
+ const isLakareFill = (l) => /^s52-LAKARE-fill-band\d+$/.test(l.id);
30629
30931
  const bandOf = (l) => {
30630
30932
  const m = /-band(\d+)$/.exec(l.id);
30631
30933
  return m ? Number(m[1]) : 0;
30632
30934
  };
30633
30935
  const emitted = opts.land ? (() => {
30634
- const relocated = layers.filter((l) => isLndareFill(l) || isWaterBaseFill(l));
30635
- const others = layers.filter((l) => !isLndareFill(l) && !isWaterBaseFill(l));
30936
+ const relocated = layers.filter(
30937
+ (l) => isLndareFill(l) || isWaterBaseFill(l) || isLakareFill(l)
30938
+ );
30939
+ const others = layers.filter(
30940
+ (l) => !isLndareFill(l) && !isWaterBaseFill(l) && !isLakareFill(l)
30941
+ );
30636
30942
  const isSeaareFill = (l) => /^s52-SEAARE-fill-band\d+$/.test(l.id);
30637
30943
  const seaare = others.filter(isSeaareFill);
30638
30944
  const rest = others.filter((l) => !isSeaareFill(l));
@@ -30656,40 +30962,43 @@ function generateStyle(palette = "DAY", opts = {}) {
30656
30962
  for (const b of bands) {
30657
30963
  ordered.push(...relocated.filter((l) => bandOf(l) === b && isWaterBaseFill(l)));
30658
30964
  ordered.push(...relocated.filter((l) => bandOf(l) === b && isLndareFill(l)).map(asBaseTan));
30965
+ ordered.push(...relocated.filter((l) => bandOf(l) === b && isLakareFill(l)));
30659
30966
  }
30660
30967
  const findRest = (id) => restAbove.findIndex((l) => l.id === id);
30661
- const shoreAt = findRest("chrtbase-inland-shore");
30662
- const pairAt = shoreAt !== -1 ? shoreAt : findRest("chrtbase-inland-hatch");
30968
+ const pairAt = findRest("chrtbase-inland-water");
30663
30969
  let restFinal = restAbove;
30664
30970
  if (pairAt !== -1) {
30665
30971
  const repaint = [];
30666
- for (const b of bands) {
30667
- for (const l of relocated.filter((x) => bandOf(x) === b && isWaterBaseFill(x))) {
30668
- const src = l;
30669
- const meta = { ...src.metadata ?? {}, "chrt:role": "charted-water-repaint" };
30670
- const notDom = {
30972
+ const pushCascadeClones = (l, b) => {
30973
+ const src = l;
30974
+ const meta = { ...src.metadata ?? {}, "chrt:role": "charted-water-repaint" };
30975
+ const notDom = {
30976
+ ...l,
30977
+ id: `${src.id}-overpair`,
30978
+ filter: ["all", src.filter, NOT_DOMINATED_FILTER],
30979
+ metadata: meta
30980
+ };
30981
+ delete notDom.maxzoom;
30982
+ delete notDom.minzoom;
30983
+ repaint.push(notDom);
30984
+ for (const D of BAND_DRAW_ORDER) {
30985
+ if (parseInt(D, 10) <= b) continue;
30986
+ const dMaxzoom = dominatedVariantMaxzoom(D);
30987
+ if (dMaxzoom <= 0) continue;
30988
+ const domVariant = {
30671
30989
  ...l,
30672
- id: `${src.id}-overpair`,
30673
- filter: ["all", src.filter, NOT_DOMINATED_FILTER],
30674
- metadata: meta
30990
+ id: `${src.id}-overpair-domBy${D}`,
30991
+ filter: ["all", src.filter, dominatedByMinFilter(D)],
30992
+ metadata: meta,
30993
+ maxzoom: dMaxzoom
30675
30994
  };
30676
- delete notDom.maxzoom;
30677
- delete notDom.minzoom;
30678
- repaint.push(notDom);
30679
- for (const D of BAND_DRAW_ORDER) {
30680
- if (parseInt(D, 10) <= b) continue;
30681
- const dMaxzoom = dominatedVariantMaxzoom(D);
30682
- if (dMaxzoom <= 0) continue;
30683
- const domVariant = {
30684
- ...l,
30685
- id: `${src.id}-overpair-domBy${D}`,
30686
- filter: ["all", src.filter, dominatedByMinFilter(D)],
30687
- metadata: meta,
30688
- maxzoom: dMaxzoom
30689
- };
30690
- delete domVariant.minzoom;
30691
- repaint.push(domVariant);
30692
- }
30995
+ delete domVariant.minzoom;
30996
+ repaint.push(domVariant);
30997
+ }
30998
+ };
30999
+ for (const b of bands) {
31000
+ for (const l of relocated.filter((x) => bandOf(x) === b && isWaterBaseFill(x))) {
31001
+ pushCascadeClones(l, b);
30693
31002
  }
30694
31003
  if (parseInt(b, 10) >= FINE_LAND_REPAINT_MIN_BAND) {
30695
31004
  for (const l of relocated.filter(
@@ -30705,6 +31014,9 @@ function generateStyle(palette = "DAY", opts = {}) {
30705
31014
  repaint.push(landClone);
30706
31015
  }
30707
31016
  }
31017
+ for (const l of relocated.filter((x) => bandOf(x) === b && isLakareFill(x))) {
31018
+ pushCascadeClones(l, b);
31019
+ }
30708
31020
  }
30709
31021
  const isCoalneLine = (l) => /^s52-COALNE-line-band\d(-domBy\d)?$/.test(l.id);
30710
31022
  for (const l of layers.filter(isCoalneLine)) {
@@ -30728,9 +31040,9 @@ function generateStyle(palette = "DAY", opts = {}) {
30728
31040
  repaint.push(terminal);
30729
31041
  }
30730
31042
  restFinal = [
30731
- ...restAbove.slice(0, pairAt + 1),
31043
+ ...restAbove.slice(0, pairAt),
30732
31044
  ...repaint,
30733
- ...restAbove.slice(pairAt + 1)
31045
+ ...restAbove.slice(pairAt)
30734
31046
  ];
30735
31047
  }
30736
31048
  {
@@ -30779,6 +31091,7 @@ function generateStyle(palette = "DAY", opts = {}) {
30779
31091
  emitted.push(layer);
30780
31092
  }
30781
31093
  }
31094
+ const finalLayers = drapedFirst(emitted);
30782
31095
  const style = {
30783
31096
  version: 8,
30784
31097
  name: `CHRT ${SYMBOL_SET_LABELS[symbolSet]} ${PALETTE_NAMES[palette]}${opts.chartMode ? ` \u2014 chart:${opts.chartMode.band}` : ""}`,
@@ -30787,7 +31100,7 @@ function generateStyle(palette = "DAY", opts = {}) {
30787
31100
  "chrt:symbol-set": symbolSet,
30788
31101
  "chrt:color-table": palette,
30789
31102
  "chrt:generated-at": (/* @__PURE__ */ new Date()).toISOString(),
30790
- "chrt:layer-count": emitted.length,
31103
+ "chrt:layer-count": finalLayers.length,
30791
31104
  "chrt:land": Boolean(opts.land),
30792
31105
  "chrt:relief": Boolean(opts.relief),
30793
31106
  "chrt:base": Boolean(opts.base),
@@ -30827,7 +31140,7 @@ function generateStyle(palette = "DAY", opts = {}) {
30827
31140
  // upstream glyph byte-identical and composite only those six in from
30828
31141
  // Noto Sans; see scripts/generate-glyphs.mjs.
30829
31142
  glyphs: `${glyphsBase}/{fontstack}/{range}.pbf`,
30830
- layers: emitted
31143
+ layers: finalLayers
30831
31144
  };
30832
31145
  if (symbolSet === "s101") return applyS101SymbolSet(style);
30833
31146
  if (symbolSet === "chrt") return applyChrtSymbolSet(style, palette);
@@ -31656,8 +31969,16 @@ var Chrt = class {
31656
31969
  ALL_WATER_BODY_LAYERS,
31657
31970
  AREA_QUERY_LAYERS,
31658
31971
  ATON_EXPRESSION_CLASSES,
31972
+ BAND_CODE_ALIASES,
31659
31973
  BAND_DRAW_ORDER,
31660
31974
  BAND_PRIORITY_RANGES,
31975
+ BASE_ATTRIBUTION,
31976
+ BASE_BUNDLE_ATTRIBUTION,
31977
+ BASE_BUNDLE_ID,
31978
+ BASE_BUNDLE_MAXZOOM,
31979
+ BASE_BUNDLE_SCHEMA,
31980
+ BASE_BUNDLE_SOURCE_LAYERS,
31981
+ BASE_CLIENT_RASTER_BEFORE_ID,
31661
31982
  BASE_MAXZOOM,
31662
31983
  BASE_MINZOOM,
31663
31984
  BASE_REGION,
@@ -31690,6 +32011,7 @@ var Chrt = class {
31690
32011
  DEFAULT_TILE_BASE_URL,
31691
32012
  DEFAULT_TILE_REGION,
31692
32013
  DEFAULT_WATER_BODY_SETTINGS,
32014
+ DRAPEABLE_LAYER_TYPES,
31693
32015
  INSPECT_CLASS_RANK,
31694
32016
  INSPECT_RANK_DEVIATIONS,
31695
32017
  InteractClient,
@@ -31698,6 +32020,11 @@ var Chrt = class {
31698
32020
  LABEL_BEHAVIOR,
31699
32021
  LABEL_LAYER_GROUPS,
31700
32022
  LABEL_PRIORITY,
32023
+ LAMP_CORE_LAYER_ID,
32024
+ LAMP_GLOW_LAYER_ID,
32025
+ LAMP_LAYER_IDS,
32026
+ LAMP_TICK_MS,
32027
+ LAND_ATTRIBUTION,
31701
32028
  LAND_BUILD_MAXZOOM,
31702
32029
  LAND_REGION,
31703
32030
  NAUTICAL_SYMBOLS,
@@ -31713,7 +32040,10 @@ var Chrt = class {
31713
32040
  RASTER_ATON_CLASSES,
31714
32041
  RASTER_GLYPH_NAMES,
31715
32042
  RASTER_ICON_PREFIX,
32043
+ RELIEF_ATTRIBUTION,
32044
+ RELIEF_ENCODING,
31716
32045
  RELIEF_REGION,
32046
+ RELIEF_TILE_SIZE,
31717
32047
  S101_AREA_FILL_NAMES,
31718
32048
  S101_CHART_SECTIONS,
31719
32049
  S101_FEATURES,
@@ -31733,6 +32063,8 @@ var Chrt = class {
31733
32063
  S52_LOOKUP,
31734
32064
  S52_NIGHT,
31735
32065
  S52_PATTERN_SPRITE_PREFIX,
32066
+ SIGPER_TOLERANCE_SECONDS,
32067
+ SIGSEQ_MAX_TERMS,
31736
32068
  SOUNDING_LAYERS,
31737
32069
  SYMBOL_FAMILIES,
31738
32070
  SYMBOL_FAMILY_META,
@@ -31742,6 +32074,7 @@ var Chrt = class {
31742
32074
  TileTokenError,
31743
32075
  WATER_BODY_LAYER_GROUPS,
31744
32076
  WeatherEndpoint,
32077
+ applyBaseBundle,
31745
32078
  applyChrtSymbolSet,
31746
32079
  applyColorTheme,
31747
32080
  applyCustomStyleSettings,
@@ -31754,6 +32087,7 @@ var Chrt = class {
31754
32087
  buildFlashPattern,
31755
32088
  buildLabelLayer,
31756
32089
  buildLakeTierExpression,
32090
+ buildLampLayers,
31757
32091
  buildLandAreaTierExpression,
31758
32092
  buildLandRegionTierExpression,
31759
32093
  buildLayersForClass,
@@ -31766,6 +32100,8 @@ var Chrt = class {
31766
32100
  buildSectorsFromFeatures,
31767
32101
  buildSources,
31768
32102
  candidateRank,
32103
+ cellBandCharacters,
32104
+ cellBandMatch,
31769
32105
  chrtAtonTintExpression,
31770
32106
  chrtLightTintExpression,
31771
32107
  collectCandidates,
@@ -31780,6 +32116,8 @@ var Chrt = class {
31780
32116
  depareColor,
31781
32117
  depareFillExpression,
31782
32118
  deriveMarinerName,
32119
+ drapeHalves,
32120
+ drapedFirst,
31783
32121
  extractNavAidDesignation,
31784
32122
  featureIdentity,
31785
32123
  generateCustomStyle,
@@ -31812,6 +32150,7 @@ var Chrt = class {
31812
32150
  hazardColor,
31813
32151
  hazardFillExpression,
31814
32152
  iconExpressionForFamily,
32153
+ isDrapeable,
31815
32154
  isLabelLayerVisible,
31816
32155
  isNavAidLayer,
31817
32156
  isNavAidLayerVisible,
@@ -31819,6 +32158,8 @@ var Chrt = class {
31819
32158
  isVisibleInMode,
31820
32159
  isWaterBodyLayerVisible,
31821
32160
  labelSortKey,
32161
+ lampRhythmColour,
32162
+ lampRhythmOpacity,
31822
32163
  lateralColor,
31823
32164
  lateralFillExpression,
31824
32165
  lightColor,
@@ -31828,6 +32169,7 @@ var Chrt = class {
31828
32169
  normalizeTheme,
31829
32170
  paginate,
31830
32171
  parseListCodes,
32172
+ parseSigseqTerms,
31831
32173
  parseSymbolFamily,
31832
32174
  parseWebhook,
31833
32175
  patternSpriteId,
@@ -31842,12 +32184,14 @@ var Chrt = class {
31842
32184
  resolveS101Color,
31843
32185
  resolveStyle,
31844
32186
  resolveTargetClass,
32187
+ rhythmProperties,
31845
32188
  s101ArtworkProvenance,
31846
32189
  sanitizeCustomStyleConfig,
31847
32190
  scaleSizeValue,
31848
32191
  scaminToMinZoom,
31849
32192
  sectorLimitToArcBearing,
31850
32193
  styleModeBand,
32194
+ terrainStackCount,
31851
32195
  tileTemplate,
31852
32196
  uncoveredSectorArcs,
31853
32197
  withinBboxFilter,