@chrtco/sdk 0.8.1 → 0.9.0

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
@@ -20,8 +20,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ALL_NAV_AID_LAYERS: () => ALL_NAV_AID_LAYERS,
24
+ ALL_WATER_BODY_LAYERS: () => ALL_WATER_BODY_LAYERS,
25
+ AREA_QUERY_LAYERS: () => AREA_QUERY_LAYERS,
23
26
  BAND_DRAW_ORDER: () => BAND_DRAW_ORDER,
24
27
  BAND_PRIORITY_RANGES: () => BAND_PRIORITY_RANGES,
28
+ BRIDGE_QUERY_LAYERS: () => BRIDGE_QUERY_LAYERS,
25
29
  BUILD_MAXZOOM: () => BUILD_MAXZOOM,
26
30
  BUILD_MINZOOM: () => BUILD_MINZOOM,
27
31
  CHART_BAND_CODES: () => CHART_BAND_CODES,
@@ -29,12 +33,25 @@ __export(index_exports, {
29
33
  CHART_SECTIONS: () => CHART_SECTIONS,
30
34
  Chrt: () => Chrt,
31
35
  ChrtError: () => ChrtError,
36
+ DEFAULT_CUSTOM_STYLE_CONFIG: () => DEFAULT_CUSTOM_STYLE_CONFIG,
37
+ DEFAULT_LABEL_SETTINGS: () => DEFAULT_LABEL_SETTINGS,
38
+ DEFAULT_NAV_AID_SETTINGS: () => DEFAULT_NAV_AID_SETTINGS,
32
39
  DEFAULT_REGION: () => DEFAULT_REGION,
40
+ DEFAULT_SOUNDING_SETTINGS: () => DEFAULT_SOUNDING_SETTINGS,
33
41
  DEFAULT_TILE_BASE_URL: () => DEFAULT_TILE_BASE_URL,
34
42
  DEFAULT_TILE_REGION: () => DEFAULT_TILE_REGION,
43
+ DEFAULT_WATER_BODY_SETTINGS: () => DEFAULT_WATER_BODY_SETTINGS,
44
+ InteractClient: () => InteractClient,
45
+ InteractError: () => InteractError,
35
46
  LABELED_LAYERS: () => LABELED_LAYERS,
36
47
  LABEL_BEHAVIOR: () => LABEL_BEHAVIOR,
48
+ LABEL_LAYER_GROUPS: () => LABEL_LAYER_GROUPS,
49
+ LABEL_PRIORITY: () => LABEL_PRIORITY,
37
50
  NAUTICAL_SYMBOLS: () => NAUTICAL_SYMBOLS,
51
+ NAV_AID_LAYER_GROUPS: () => NAV_AID_LAYER_GROUPS,
52
+ NAV_AID_QUERY_LAYERS: () => NAV_AID_QUERY_LAYERS,
53
+ NON_SOLAS_DISCLAIMER: () => NON_SOLAS_DISCLAIMER,
54
+ NON_SOLAS_DISCLAIMER_SHORT: () => NON_SOLAS_DISCLAIMER_SHORT,
38
55
  OfflineError: () => OfflineError,
39
56
  OfflineStyleError: () => OfflineStyleError,
40
57
  S101_CHART_SECTIONS: () => S101_CHART_SECTIONS,
@@ -47,21 +64,34 @@ __export(index_exports, {
47
64
  S52_DUSK: () => S52_DUSK,
48
65
  S52_LOOKUP: () => S52_LOOKUP,
49
66
  S52_NIGHT: () => S52_NIGHT,
67
+ SOUNDING_LAYERS: () => SOUNDING_LAYERS,
50
68
  SYMBOL_GROUPS: () => SYMBOL_GROUPS,
51
69
  SYMBOL_MAP: () => SYMBOL_MAP,
52
70
  TileAuth: () => TileAuth,
53
71
  TileTokenError: () => TileTokenError,
72
+ WATER_BODY_LAYER_GROUPS: () => WATER_BODY_LAYER_GROUPS,
54
73
  applyColorTheme: () => applyColorTheme,
74
+ applyCustomStyleSettings: () => applyCustomStyleSettings,
55
75
  bandFilterExpression: () => bandFilterExpression,
76
+ buildFlashPattern: () => buildFlashPattern,
56
77
  buildLabelLayer: () => buildLabelLayer,
57
78
  buildLayersForClass: () => buildLayersForClass,
58
79
  buildOfflineStyle: () => buildOfflineStyle,
80
+ buildSectorArcGeoJSON: () => buildSectorArcGeoJSON,
81
+ buildSectorArcsFC: () => buildSectorArcsFC,
82
+ buildSectorsFromFeatures: () => buildSectorsFromFeatures,
59
83
  buildSources: () => buildSources,
60
84
  combineWithBandFilter: () => combineWithBandFilter,
61
85
  daymarkColor: () => daymarkColor,
62
86
  decimateFeatures: () => decimateFeatures,
87
+ decodeAreaFeatureProps: () => decodeAreaFeatureProps,
88
+ decodeBridgeProps: () => decodeBridgeProps,
89
+ decodeNavAidProps: () => decodeNavAidProps,
63
90
  depareColor: () => depareColor,
64
91
  depareFillExpression: () => depareFillExpression,
92
+ deriveMarinerName: () => deriveMarinerName,
93
+ extractNavAidDesignation: () => extractNavAidDesignation,
94
+ generateCustomStyle: () => generateCustomStyle,
65
95
  generateSpriteManifest: () => generateSpriteManifest,
66
96
  generateSpriteSheet: () => generateSpriteSheet,
67
97
  generateStyle: () => generateStyle,
@@ -76,6 +106,7 @@ __export(index_exports, {
76
106
  getIconExpression: () => getIconExpression,
77
107
  getLabelBehavior: () => getLabelBehavior,
78
108
  getLabelConfig: () => getLabelConfig,
109
+ getLabelPriority: () => getLabelPriority,
79
110
  getLookupEntry: () => getLookupEntry,
80
111
  getMaxCellsForZoom: () => getMaxCellsForZoom,
81
112
  getOrderedClasses: () => getOrderedClasses,
@@ -84,22 +115,33 @@ __export(index_exports, {
84
115
  getStyleStats: () => getStyleStats,
85
116
  getSymbolSvgString: () => getSymbolSvgString,
86
117
  getThemedS52Svg: () => getThemedS52Svg,
118
+ getTopmarkMeaning: () => getTopmarkMeaning,
87
119
  hazardColor: () => hazardColor,
88
120
  hazardFillExpression: () => hazardFillExpression,
121
+ isLabelLayerVisible: () => isLabelLayerVisible,
89
122
  isNavAidLayer: () => isNavAidLayer,
123
+ isNavAidLayerVisible: () => isNavAidLayerVisible,
124
+ isSoundingLayerVisible: () => isSoundingLayerVisible,
90
125
  isVisibleInMode: () => isVisibleInMode,
126
+ isWaterBodyLayerVisible: () => isWaterBodyLayerVisible,
127
+ labelSortKey: () => labelSortKey,
91
128
  lateralColor: () => lateralColor,
92
129
  lateralFillExpression: () => lateralFillExpression,
93
130
  lightColor: () => lightColor,
94
131
  lightFillExpression: () => lightFillExpression,
132
+ normalizeTheme: () => normalizeTheme,
95
133
  paginate: () => paginate,
134
+ parseListCodes: () => parseListCodes,
96
135
  parseWebhook: () => parseWebhook,
97
136
  preprocessLights: () => preprocessLights,
98
137
  preprocessNavAids: () => preprocessNavAids,
99
138
  preprocessSoundings: () => preprocessSoundings,
100
139
  preprocessWaterBodies: () => preprocessWaterBodies,
101
140
  resolveColor: () => resolveColor,
141
+ resolveCustomTheme: () => resolveCustomTheme,
102
142
  resolveStyle: () => resolveStyle,
143
+ sanitizeCustomStyleConfig: () => sanitizeCustomStyleConfig,
144
+ scaleSizeValue: () => scaleSizeValue,
103
145
  scaminToMinZoom: () => scaminToMinZoom,
104
146
  withinBboxFilter: () => withinBboxFilter,
105
147
  worldMinusBboxPolygon: () => worldMinusBboxPolygon
@@ -243,6 +285,7 @@ var TileTokenError = class extends Error {
243
285
  }
244
286
  };
245
287
  var TILE_URL_PATH = /\/v1\/tiles\/[A-Za-z0-9_]+(?:\/[a-z][a-z0-9-]*)?\/\d+\/\d+\/\d+\.mvt(?:\?.*)?$/i;
288
+ var STYLE_URL_PATH = /\/v1\/styles\/[A-Za-z0-9_]+\/style\.json(?:\?.*)?$/i;
246
289
  var REFRESH_MARGIN_SECONDS = 30;
247
290
  var STALE_GRACE_SECONDS = 1;
248
291
  var MINT_RETRY_DELAYS_MS = [500, 1e3, 2e3];
@@ -386,17 +429,22 @@ var TileAuth = class {
386
429
  }
387
430
  /**
388
431
  * Produce a MapLibre / Mapbox GL `transformRequest`. Attaches
389
- * `Authorization: Bearer <jwt>` only to CHRT tile URLs; style, sprite, and
390
- * glyph URLs pass through untouched (never leak the token to third-party
391
- * CDNs). Kicks off a background mint immediately so the cache warms within
392
- * one round-trip; the first tile or two may go unauthenticated and be
393
- * retried by the renderer.
432
+ * `Authorization: Bearer <jwt>` only to CHRT tile URLs and the CHRT
433
+ * served-style URL (`/v1/styles/{region}/style.json`); sprite, glyph, and
434
+ * third-party URLs pass through untouched (never leak the token to
435
+ * third-party CDNs). Kicks off a background mint immediately so the cache
436
+ * warms within one round-trip; the first tile or two may go
437
+ * unauthenticated and be retried by the renderer. When pointing a map's
438
+ * `style` at the served style URL, obtain it via `chrt.styleUrl()` — it
439
+ * awaits the mint, so the style request (which MapLibre does NOT retry)
440
+ * is guaranteed to carry auth.
394
441
  */
395
442
  transformRequest(region) {
396
443
  const r = region ?? this.defaultRegion;
397
444
  void this.getToken(r).catch(() => void 0);
398
445
  return (url) => {
399
- if (!url.startsWith(this.tileBaseUrl) || !TILE_URL_PATH.test(url)) return void 0;
446
+ if (!url.startsWith(this.tileBaseUrl)) return void 0;
447
+ if (!TILE_URL_PATH.test(url) && !STYLE_URL_PATH.test(url)) return void 0;
400
448
  const token = this.peek(r);
401
449
  if (!token) return { url };
402
450
  return { url, headers: { Authorization: `Bearer ${token}` } };
@@ -434,23 +482,67 @@ var TileAuth = class {
434
482
 
435
483
  // src/endpoints/tiles.ts
436
484
  var TilesEndpoint = class {
437
- constructor(http, opts) {
485
+ constructor(http, tileAuth, opts) {
438
486
  this.http = http;
487
+ this.tileAuth = tileAuth;
439
488
  this.region = opts?.region ?? DEFAULT_TILE_REGION;
440
489
  this.tileBaseUrl = (opts?.tileBaseUrl ?? DEFAULT_TILE_BASE_URL).replace(/\/+$/, "");
441
490
  }
442
491
  region;
443
492
  tileBaseUrl;
493
+ styleEndpoint(theme) {
494
+ const query2 = theme ? `?theme=${theme}` : "";
495
+ return `${this.tileBaseUrl}/v1/styles/${this.region}/style.json${query2}`;
496
+ }
444
497
  /**
445
- * @deprecated CHRT has no remote `style.json` endpoint — the S-52 style is
446
- * generated client-side. Use `chrt.style(opts)` to get a ready-to-use
447
- * MapLibre style object instead. Calling this throws to prevent silently
448
- * pointing a map at a dead URL.
498
+ * URL of the served S-52 style for the configured region — the CHRTstyle
499
+ * Style API (`GET /v1/styles/{region}/style.json`). The served document
500
+ * honors the API key's bound custom style (a Dev+ feature configured at
501
+ * platform → Map Styles); Free keys and unbound keys get the default
502
+ * themes, identical to the locally generated `chrt.style()`.
503
+ *
504
+ * The endpoint is JWT-gated, so pair the URL with
505
+ * `chrt.transformRequest()`, which attaches the Bearer token to both the
506
+ * style request and its tile requests. This method is async because it
507
+ * pre-mints the tile token, guaranteeing the auth cache is warm before
508
+ * MapLibre fetches the URL.
509
+ *
510
+ * @example
511
+ * const map = new maplibregl.Map({
512
+ * container: 'map',
513
+ * style: await chrt.styleUrl({ theme: 'dusk' }),
514
+ * transformRequest: chrt.transformRequest(),
515
+ * });
449
516
  */
450
- styleUrl() {
451
- throw new Error(
452
- "chrt.tiles.styleUrl() is no longer supported: CHRT serves no remote style.json. Use chrt.style(opts) to generate an S-52 MapLibre style object, and chrt.transformRequest() for tile auth. See https://chrt.co/docs/vector-tiles"
453
- );
517
+ async styleUrl(opts = {}) {
518
+ await this.tileAuth.getToken(this.region);
519
+ return this.styleEndpoint(opts.theme);
520
+ }
521
+ /**
522
+ * Fetch the served S-52 style as a MapLibre style object (see
523
+ * {@link styleUrl} for what "served" means). Prefer this over `styleUrl()`
524
+ * when you want to inspect or mutate the document, or when you can't use
525
+ * `transformRequest` for the style request itself.
526
+ *
527
+ * Falls back nowhere: a network or auth failure throws (`ChrtError` /
528
+ * `TileTokenError`). For an offline-capable local fallback, use
529
+ * `chrt.style()` — the client-side generator.
530
+ */
531
+ async fetchStyle(opts = {}) {
532
+ const token = await this.tileAuth.getToken(this.region);
533
+ const res = await fetch(this.styleEndpoint(opts.theme), {
534
+ headers: { authorization: `Bearer ${token.token}` },
535
+ signal: opts.signal
536
+ });
537
+ if (!res.ok) {
538
+ let body;
539
+ try {
540
+ body = await res.json();
541
+ } catch {
542
+ }
543
+ throw new ChrtError(res.status, body);
544
+ }
545
+ return await res.json();
454
546
  }
455
547
  /**
456
548
  * Vector tile URL template for the configured region, served by the
@@ -1254,51 +1346,2409 @@ var OfflineEndpoint = class {
1254
1346
  { resumable: true, bytes_written: bytesDownloaded }
1255
1347
  );
1256
1348
  }
1257
- emit(true);
1258
- const computedSha = await fs.sha256(partialPath);
1259
- if (computedSha.toLowerCase() !== bundle.sha256) {
1260
- await fs.unlink(partialPath);
1261
- throw new OfflineError(
1262
- "download_corrupt",
1263
- `SHA-256 mismatch for ${region}: expected ${bundle.sha256}, got ${computedSha}`,
1264
- void 0,
1265
- { expected: bundle.sha256, actual: computedSha }
1349
+ emit(true);
1350
+ const computedSha = await fs.sha256(partialPath);
1351
+ if (computedSha.toLowerCase() !== bundle.sha256) {
1352
+ await fs.unlink(partialPath);
1353
+ throw new OfflineError(
1354
+ "download_corrupt",
1355
+ `SHA-256 mismatch for ${region}: expected ${bundle.sha256}, got ${computedSha}`,
1356
+ void 0,
1357
+ { expected: bundle.sha256, actual: computedSha }
1358
+ );
1359
+ }
1360
+ await fs.rename(partialPath, path);
1361
+ await this.persistManifestEntry(region, bundle, path, fs);
1362
+ return {
1363
+ path,
1364
+ bytes: bundle.bytes,
1365
+ sha256: bundle.sha256,
1366
+ build_id: bundle.build_id,
1367
+ build_tag: bundle.build_tag,
1368
+ resumed
1369
+ };
1370
+ }
1371
+ async persistManifestEntry(region, bundle, path, fs) {
1372
+ if (!this.manifestPath) return;
1373
+ const manifest = await readManifest(this.manifestPath, fs);
1374
+ const nowSec = Math.floor(Date.now() / 1e3);
1375
+ manifest.regions[region] = {
1376
+ path,
1377
+ bytes: bundle.bytes,
1378
+ sha256: bundle.sha256,
1379
+ build_id: bundle.build_id,
1380
+ build_tag: bundle.build_tag,
1381
+ downloaded_at: nowSec,
1382
+ last_accessed_at: nowSec
1383
+ };
1384
+ await writeManifest(this.manifestPath, fs, manifest);
1385
+ }
1386
+ };
1387
+ function dirname(p) {
1388
+ const i = p.lastIndexOf("/");
1389
+ const j = p.lastIndexOf("\\");
1390
+ const k = Math.max(i, j);
1391
+ if (k <= 0) return ".";
1392
+ return p.slice(0, k);
1393
+ }
1394
+
1395
+ // src/interact/decode.ts
1396
+ var LITCHR_TABLE = {
1397
+ 1: { name: "Fixed", abbr: "F" },
1398
+ 2: { name: "Flashing", abbr: "Fl" },
1399
+ 3: { name: "Long Flashing", abbr: "LFl" },
1400
+ 4: { name: "Occulting", abbr: "Oc" },
1401
+ 5: { name: "Isophase", abbr: "Iso" },
1402
+ 6: { name: "Short Flashing", abbr: "S.Fl" },
1403
+ 7: { name: "Short Long Flashing", abbr: "S.LFl" },
1404
+ 8: { name: "Alternating Occulting", abbr: "Al.Oc" },
1405
+ 9: { name: "Alternating Long Flashing", abbr: "Al.LFl" },
1406
+ 10: { name: "Alternating Flashing", abbr: "Al.Fl" },
1407
+ 11: { name: "Alternating Group", abbr: "Al.Gp" },
1408
+ 12: { name: "Morse", abbr: "Mo" },
1409
+ 13: { name: "Quick", abbr: "Q" },
1410
+ 14: { name: "Very Quick", abbr: "VQ" },
1411
+ 15: { name: "Ultra Quick", abbr: "UQ" },
1412
+ 16: { name: "Alternating", abbr: "Al" },
1413
+ 17: { name: "Alternating Fixed/Flash", abbr: "Al.F.Fl" },
1414
+ 25: { name: "Quick + Long Flash", abbr: "Q+LFl" },
1415
+ 26: { name: "Very Quick + Long Flash", abbr: "VQ+LFl" },
1416
+ 27: { name: "Ultra Quick + Long Flash", abbr: "UQ+LFl" },
1417
+ 28: { name: "Alternating", abbr: "Al" },
1418
+ 29: { name: "Fixed + Long Flash", abbr: "F.LFl" }
1419
+ };
1420
+ var COLOUR_TABLE = {
1421
+ 1: { name: "White", abbr: "W", hex: "#ffffff" },
1422
+ 2: { name: "Black", abbr: "B", hex: "#333333" },
1423
+ 3: { name: "Red", abbr: "R", hex: "#ef4444" },
1424
+ 4: { name: "Green", abbr: "G", hex: "#22c55e" },
1425
+ 5: { name: "Blue", abbr: "Bu", hex: "#3b82f6" },
1426
+ 6: { name: "Yellow", abbr: "Y", hex: "#eab308" },
1427
+ 7: { name: "Grey", abbr: "Gr", hex: "#9ca3af" },
1428
+ 8: { name: "Brown", abbr: "Bn", hex: "#92400e" },
1429
+ 9: { name: "Amber", abbr: "Am", hex: "#f59e0b" },
1430
+ 10: { name: "Violet", abbr: "Vi", hex: "#8b5cf6" },
1431
+ 11: { name: "Orange", abbr: "Or", hex: "#f97316" },
1432
+ 12: { name: "Magenta", abbr: "Mg", hex: "#d946ef" },
1433
+ 13: { name: "Pink", abbr: "Pk", hex: "#f472b6" }
1434
+ };
1435
+ var BOYSHP_TABLE = {
1436
+ 1: "Conical (nun)",
1437
+ 2: "Can (cylindrical)",
1438
+ 3: "Spherical",
1439
+ 4: "Pillar",
1440
+ 5: "Spar",
1441
+ 6: "Barrel",
1442
+ 7: "Super buoy",
1443
+ 8: "Ice buoy"
1444
+ };
1445
+ var CATLAM_TABLE = {
1446
+ 1: "Port",
1447
+ 2: "Starboard",
1448
+ 3: "Preferred channel to starboard",
1449
+ 4: "Preferred channel to port"
1450
+ };
1451
+ var CATCAM_TABLE = {
1452
+ 1: "North cardinal",
1453
+ 2: "East cardinal",
1454
+ 3: "South cardinal",
1455
+ 4: "West cardinal"
1456
+ };
1457
+ var CATLMK_TABLE = {
1458
+ 1: "Cairn",
1459
+ 2: "Cemetery",
1460
+ 3: "Chimney",
1461
+ 4: "Dish aerial",
1462
+ 5: "Flagstaff",
1463
+ 6: "Flare stack",
1464
+ 7: "Mast",
1465
+ 8: "Windsock",
1466
+ 9: "Monument",
1467
+ 10: "Column / pillar",
1468
+ 11: "Memorial plaque",
1469
+ 12: "Obelisk",
1470
+ 13: "Statue",
1471
+ 14: "Cross",
1472
+ 15: "Dome",
1473
+ 16: "Radar scanner",
1474
+ 17: "Tower",
1475
+ 18: "Windmill",
1476
+ 19: "Wind turbine",
1477
+ 20: "Spire"
1478
+ };
1479
+ var CATFOG_TABLE = {
1480
+ 1: "Explosive",
1481
+ 2: "Diaphone",
1482
+ 3: "Siren",
1483
+ 4: "Nautophone",
1484
+ 5: "Bell",
1485
+ 6: "Whistle",
1486
+ 7: "Reed",
1487
+ 8: "Gong",
1488
+ 9: "Horn"
1489
+ };
1490
+ var CATFOG_ABBR_TABLE = {
1491
+ 1: "EXPLOS",
1492
+ 2: "DIA",
1493
+ 3: "SIREN",
1494
+ 4: "NAUTO",
1495
+ 5: "BELL",
1496
+ 6: "WHIS",
1497
+ 7: "REED",
1498
+ 8: "GONG",
1499
+ 9: "HORN"
1500
+ };
1501
+ var TOPSHP_TABLE = {
1502
+ 1: "Cone, point up",
1503
+ 2: "Cone, point down",
1504
+ 3: "Sphere",
1505
+ 4: "2 spheres",
1506
+ 5: "Cylinder",
1507
+ 6: "Board",
1508
+ 7: "X-shape",
1509
+ 8: "Upright cross",
1510
+ 9: "Cube, point up",
1511
+ 10: "2 cones, point to point",
1512
+ 11: "2 cones, base to base",
1513
+ 12: "Rhombus",
1514
+ 13: "2 cones, up",
1515
+ 14: "2 cones, down",
1516
+ 15: "Besom, point up",
1517
+ 16: "Besom, point down",
1518
+ 17: "Flag",
1519
+ 18: "Sphere over rhombus",
1520
+ 19: "Square",
1521
+ 33: "T-shape"
1522
+ };
1523
+ var STATUS_TABLE = {
1524
+ 1: "Permanent",
1525
+ 2: "Occasional",
1526
+ 3: "Recommended",
1527
+ 4: "Not in use",
1528
+ 5: "Periodic/intermittent",
1529
+ 6: "Reserved",
1530
+ 7: "Temporary",
1531
+ 8: "Private",
1532
+ 9: "Mandatory",
1533
+ 11: "Extinguished",
1534
+ 12: "Illuminated",
1535
+ 13: "Historic",
1536
+ 14: "Public",
1537
+ 15: "Synchronized",
1538
+ 16: "Watched",
1539
+ 17: "Un-watched",
1540
+ 18: "Existence doubtful"
1541
+ };
1542
+ var MARSYS_TABLE = {
1543
+ 1: "IALA-A",
1544
+ 2: "IALA-B"
1545
+ };
1546
+ var LAYER_TYPE_MAP = {
1547
+ BOYLAT: "Lateral Buoy",
1548
+ BOYCAR: "Cardinal Buoy",
1549
+ BOYSPP: "Special Purpose Buoy",
1550
+ BOYISD: "Isolated Danger Buoy",
1551
+ BOYSAW: "Safe Water Buoy",
1552
+ BCNLAT: "Lateral Beacon",
1553
+ BCNCAR: "Cardinal Beacon",
1554
+ BCNSPP: "Special Purpose Beacon",
1555
+ BCNISD: "Isolated Danger Beacon",
1556
+ BCNSAW: "Safe Water Beacon",
1557
+ LIGHTS: "Light",
1558
+ DAYMAR: "Daymark",
1559
+ FOGSIG: "Fog Signal",
1560
+ RTPBCN: "Radar Transponder Beacon",
1561
+ RDOSTA: "Radio/Radar Station",
1562
+ TOPMAR: "Topmark",
1563
+ MORFAC: "Mooring Facility",
1564
+ LNDMRK: "Landmark",
1565
+ SISTAT: "Signal Station",
1566
+ PILBOP: "Pilot Boarding Place",
1567
+ HRBFAC: "Harbour Facility",
1568
+ SMCFAC: "Small Craft Facility",
1569
+ CRANES: "Crane",
1570
+ OFSPLF: "Offshore Platform",
1571
+ ACHBRT: "Anchor Berth",
1572
+ DISMAR: "Distance Mark",
1573
+ LITVES: "Light Vessel",
1574
+ LITFLT: "Light Float"
1575
+ };
1576
+ var NAV_AID_QUERY_LAYERS = [
1577
+ "enc-BOYLAT",
1578
+ "enc-BOYCAR",
1579
+ "enc-BOYSPP",
1580
+ "enc-BOYISD",
1581
+ "enc-BOYSAW",
1582
+ "enc-BCNLAT",
1583
+ "enc-BCNCAR",
1584
+ "enc-BCNSPP",
1585
+ "enc-BCNISD",
1586
+ "enc-BCNSAW",
1587
+ "enc-LIGHTS",
1588
+ "enc-LIGHTS-point",
1589
+ "enc-DAYMAR",
1590
+ "enc-FOGSIG",
1591
+ "enc-RTPBCN",
1592
+ "enc-RDOSTA",
1593
+ "enc-TOPMAR",
1594
+ "enc-MORFAC",
1595
+ "enc-MORFAC-point",
1596
+ "enc-LNDMRK",
1597
+ "enc-LNDMRK-point",
1598
+ "enc-SISTAT",
1599
+ "enc-SISTAT-point",
1600
+ "enc-PILBOP",
1601
+ "enc-PILBOP-point",
1602
+ "enc-HRBFAC",
1603
+ "enc-SMCFAC",
1604
+ "enc-CRANES",
1605
+ "enc-CRANES-point",
1606
+ "enc-OFSPLF",
1607
+ "enc-OFSPLF-point",
1608
+ "enc-ACHBRT",
1609
+ "enc-DISMAR",
1610
+ "enc-LITVES",
1611
+ "enc-LITFLT"
1612
+ ];
1613
+ var AREA_QUERY_LAYERS = [
1614
+ "enc-DEPARE",
1615
+ "enc-DEPARE-polygon",
1616
+ "enc-OBSTRN",
1617
+ "enc-OBSTRN-polygon",
1618
+ "enc-OBSTRN-point",
1619
+ "enc-WRECKS",
1620
+ "enc-WRECKS-polygon",
1621
+ "enc-WRECKS-point",
1622
+ "enc-RESARE",
1623
+ "enc-RESARE-polygon",
1624
+ "enc-ACHARE",
1625
+ "enc-ACHARE-polygon",
1626
+ "enc-CTNARE",
1627
+ "enc-CTNARE-polygon",
1628
+ "enc-DRGARE",
1629
+ "enc-DRGARE-polygon",
1630
+ "enc-FAIRWY",
1631
+ "enc-FAIRWY-polygon",
1632
+ "enc-DMPGRD",
1633
+ "enc-DMPGRD-polygon",
1634
+ "enc-MIPARE",
1635
+ "enc-MIPARE-polygon",
1636
+ "enc-PRCARE",
1637
+ "enc-PRCARE-polygon",
1638
+ "enc-TSSLPT",
1639
+ "enc-TSSLPT-polygon",
1640
+ "enc-TSELNE",
1641
+ "enc-TSELNE-line",
1642
+ "enc-ISTZNE",
1643
+ "enc-ISTZNE-polygon",
1644
+ "enc-OSPARE",
1645
+ "enc-OSPARE-polygon",
1646
+ "enc-MARCUL",
1647
+ "enc-MARCUL-polygon",
1648
+ "enc-CBLARE",
1649
+ "enc-CBLARE-polygon",
1650
+ "enc-PIPARE",
1651
+ "enc-PIPARE-polygon",
1652
+ // Tier 3: infrastructure lines (BRIDGE handled by BRIDGE_QUERY_LAYERS)
1653
+ "enc-CBLSUB",
1654
+ "enc-CBLSUB-line",
1655
+ "enc-CBLOHD",
1656
+ "enc-CBLOHD-line",
1657
+ "enc-PIPSOL",
1658
+ "enc-PIPSOL-line",
1659
+ "enc-PIPOHD",
1660
+ "enc-PIPOHD-line",
1661
+ "enc-BERTHS",
1662
+ "enc-BERTHS-line",
1663
+ "enc-DAMCON",
1664
+ "enc-DAMCON-line",
1665
+ "enc-FERYRT",
1666
+ "enc-FERYRT-line",
1667
+ "enc-RECTRC",
1668
+ "enc-RECTRC-line",
1669
+ // Tier 4: land/context features
1670
+ "enc-BUISGL",
1671
+ "enc-BUISGL-polygon",
1672
+ "enc-BUISGL-point",
1673
+ "enc-SILTNK",
1674
+ "enc-SILTNK-polygon",
1675
+ "enc-SILTNK-point",
1676
+ "enc-HULKES",
1677
+ "enc-HULKES-polygon",
1678
+ "enc-HULKES-point",
1679
+ "enc-DRYDOC",
1680
+ "enc-DRYDOC-polygon",
1681
+ "enc-FLODOC",
1682
+ "enc-FLODOC-polygon",
1683
+ "enc-PONTON",
1684
+ "enc-PONTON-polygon",
1685
+ "enc-GATCON",
1686
+ "enc-GATCON-polygon",
1687
+ "enc-GATCON-point",
1688
+ "enc-LOGPON",
1689
+ "enc-LOGPON-polygon",
1690
+ "enc-AIRARE",
1691
+ "enc-AIRARE-polygon",
1692
+ "enc-SLCONS",
1693
+ "enc-SLCONS-polygon",
1694
+ "enc-SLCONS-line",
1695
+ "enc-SLCONS-point",
1696
+ // Tier 5: hydrographic context
1697
+ "enc-COALNE",
1698
+ "enc-COALNE-line",
1699
+ "enc-DEPCNT",
1700
+ "enc-DEPCNT-line",
1701
+ "enc-RIVERS",
1702
+ "enc-RIVERS-polygon",
1703
+ "enc-RIVERS-line",
1704
+ "enc-CANALS",
1705
+ "enc-CANALS-polygon",
1706
+ "enc-CANALS-line",
1707
+ "enc-LAKARE",
1708
+ "enc-LAKARE-polygon",
1709
+ "enc-RAPIDS",
1710
+ "enc-RAPIDS-polygon",
1711
+ "enc-RAPIDS-point",
1712
+ "enc-WATFAL",
1713
+ "enc-WATFAL-polygon",
1714
+ "enc-WATFAL-point",
1715
+ "enc-LNDARE",
1716
+ "enc-LNDARE-polygon"
1717
+ ];
1718
+ var BRIDGE_QUERY_LAYERS = [
1719
+ "enc-BRIDGE-midpoint",
1720
+ "enc-BRIDGE",
1721
+ "enc-BRIDGE-line",
1722
+ "s52-BRIDGE-polygon",
1723
+ "s52-BRIDGE-polygon-outline"
1724
+ ];
1725
+ var CATBRG_LABELS = {
1726
+ 1: "Fixed bridge",
1727
+ 2: "Opening bridge",
1728
+ 3: "Swing bridge",
1729
+ 4: "Lifting bridge",
1730
+ 5: "Bascule bridge",
1731
+ 6: "Pontoon bridge",
1732
+ 7: "Drawbridge",
1733
+ 8: "Transporter bridge",
1734
+ 9: "Footbridge",
1735
+ 10: "Viaduct",
1736
+ 11: "Aqueduct",
1737
+ 12: "Suspension bridge"
1738
+ };
1739
+ var CONDTN_TABLE = {
1740
+ 1: "Under construction",
1741
+ 2: "Ruined",
1742
+ 3: "Under reclamation",
1743
+ 5: "Planned"
1744
+ };
1745
+ function parseFirstNum(val) {
1746
+ if (val == null) return null;
1747
+ const s = String(val);
1748
+ if (s.startsWith("[")) {
1749
+ try {
1750
+ const arr = JSON.parse(s);
1751
+ if (Array.isArray(arr)) {
1752
+ const n2 = Number(arr[0]);
1753
+ return Number.isFinite(n2) ? n2 : null;
1754
+ }
1755
+ } catch {
1756
+ }
1757
+ }
1758
+ const n = Number(s.split(",")[0]);
1759
+ return Number.isFinite(n) ? n : null;
1760
+ }
1761
+ function asNumber(val) {
1762
+ if (val == null || val === "") return null;
1763
+ const n = Number(val);
1764
+ return Number.isFinite(n) ? n : null;
1765
+ }
1766
+ function decodeBridgeProps(props) {
1767
+ const catCode = parseFirstNum(props.CATBRG);
1768
+ const statusCode = parseFirstNum(props.STATUS);
1769
+ const condCode = parseFirstNum(props.CONDTN);
1770
+ return {
1771
+ name: props.OBJNAM ? String(props.OBJNAM) : null,
1772
+ categoryCode: catCode,
1773
+ categoryLabel: catCode ? CATBRG_LABELS[catCode] ?? null : null,
1774
+ verticalClearance: asNumber(props.VERCLR),
1775
+ verticalClearanceClosed: asNumber(props.VERCCL),
1776
+ verticalClearanceOpen: asNumber(props.VERCOP),
1777
+ horizontalClearance: asNumber(props.HORCLR),
1778
+ status: statusCode ? STATUS_TABLE[statusCode] ?? null : null,
1779
+ condition: condCode ? CONDTN_TABLE[condCode] ?? null : null,
1780
+ information: props.INFORM ? String(props.INFORM) : null,
1781
+ nationalName: props.NOBJNM ? String(props.NOBJNM) : null,
1782
+ sourceIndication: props.SORIND ? String(props.SORIND) : null,
1783
+ sourceDate: props.SORDAT ? String(props.SORDAT) : null
1784
+ };
1785
+ }
1786
+ var CATOBS_TABLE = {
1787
+ 1: "Snag / stump",
1788
+ 2: "Wellhead",
1789
+ 3: "Diffuser",
1790
+ 4: "Crib",
1791
+ 5: "Fish haven",
1792
+ 6: "Foul area",
1793
+ 7: "Foul ground",
1794
+ 8: "Ice boom",
1795
+ 9: "Ground tackle",
1796
+ 10: "Boom",
1797
+ 11: "Platform"
1798
+ };
1799
+ var CATWRK_TABLE = {
1800
+ 1: "Non-dangerous",
1801
+ 2: "Dangerous",
1802
+ 3: "Distributed remains",
1803
+ 4: "Mast showing",
1804
+ 5: "Hull showing"
1805
+ };
1806
+ var WATLEV_TABLE = {
1807
+ 1: "Partly submerged at high water",
1808
+ 2: "Always dry",
1809
+ 3: "Always submerged",
1810
+ 4: "Covers and uncovers",
1811
+ 5: "Awash",
1812
+ 6: "Subject to inundation / flooding",
1813
+ 7: "Floating"
1814
+ };
1815
+ var CATREA_TABLE = {
1816
+ 1: "Offshore safety zone",
1817
+ 2: "Anchoring prohibition",
1818
+ 3: "Fishing prohibition",
1819
+ 4: "Nature reserve",
1820
+ 5: "Bird sanctuary",
1821
+ 6: "Game preserve",
1822
+ 7: "Seal sanctuary",
1823
+ 8: "Degaussing range",
1824
+ 9: "Military area",
1825
+ 10: "Historic wreck",
1826
+ 12: "Navigational aid safety zone",
1827
+ 14: "No wake zone",
1828
+ 18: "Swimming area",
1829
+ 19: "Waiting area",
1830
+ 20: "Research area",
1831
+ 21: "Dredging area",
1832
+ 22: "Fish sanctuary",
1833
+ 23: "Ecological reserve",
1834
+ 24: "No discharge zone",
1835
+ 25: "No anchoring area",
1836
+ 26: "No fishing area",
1837
+ 27: "Speed restriction zone",
1838
+ 28: "To be avoided area"
1839
+ };
1840
+ var RESTRN_TABLE = {
1841
+ 1: "Anchoring prohibited",
1842
+ 2: "Anchoring restricted",
1843
+ 3: "Fishing prohibited",
1844
+ 4: "Fishing restricted",
1845
+ 5: "Trawling prohibited",
1846
+ 6: "Trawling restricted",
1847
+ 7: "Entry prohibited",
1848
+ 8: "Entry restricted",
1849
+ 9: "Dredging prohibited",
1850
+ 10: "Dredging restricted",
1851
+ 11: "Diving prohibited",
1852
+ 12: "Diving restricted",
1853
+ 13: "No wake",
1854
+ 14: "To be avoided",
1855
+ 15: "Construction prohibited",
1856
+ 16: "Discharging prohibited",
1857
+ 17: "Discharging restricted",
1858
+ 18: "Industrial prohibited",
1859
+ 19: "Metallic hull vessels prohibited",
1860
+ 20: "Navigating prohibited",
1861
+ 21: "Speed restricted",
1862
+ 24: "Stopping prohibited",
1863
+ 25: "Landing prohibited"
1864
+ };
1865
+ var CATHAF_TABLE = {
1866
+ 1: "RoRo terminal",
1867
+ 3: "Ferry terminal",
1868
+ 4: "Fishing harbour",
1869
+ 5: "Yacht harbour / marina",
1870
+ 6: "Naval base",
1871
+ 7: "Tanker terminal",
1872
+ 8: "Passenger terminal",
1873
+ 9: "Shipyard",
1874
+ 10: "Container terminal",
1875
+ 11: "Bulk terminal",
1876
+ 12: "Synchrolift",
1877
+ 13: "Straddle carrier",
1878
+ 14: "Port terminal"
1879
+ };
1880
+ var CATSCF_TABLE = {
1881
+ 1: "Visitors' berth",
1882
+ 2: "Nautical club",
1883
+ 3: "Boat hoist",
1884
+ 4: "Sailmaker",
1885
+ 5: "Boatyard",
1886
+ 6: "Public inn",
1887
+ 7: "Restaurant",
1888
+ 8: "Chandler",
1889
+ 9: "Provisions",
1890
+ 10: "Doctor",
1891
+ 11: "Pharmacy",
1892
+ 12: "Water tap",
1893
+ 13: "Fuel station",
1894
+ 14: "Electricity",
1895
+ 15: "Bottle gas",
1896
+ 16: "Showers",
1897
+ 17: "Launderette",
1898
+ 18: "Public toilets",
1899
+ 19: "Post box"
1900
+ };
1901
+ var CATCRN_TABLE = {
1902
+ 2: "Container crane / gantry",
1903
+ 3: "Sheerlegs",
1904
+ 4: "Travelling crane"
1905
+ };
1906
+ var CATACH_TABLE = {
1907
+ 1: "Unrestricted anchorage",
1908
+ 2: "Deep water anchorage",
1909
+ 3: "Tanker anchorage",
1910
+ 4: "Explosives anchorage",
1911
+ 5: "Quarantine anchorage",
1912
+ 6: "Sea-plane anchorage",
1913
+ 7: "Small craft anchorage",
1914
+ 8: "Small craft mooring area",
1915
+ 9: "Anchorage for 24 hours or less"
1916
+ };
1917
+ var CATDPG_TABLE = {
1918
+ 1: "General dumping ground",
1919
+ 2: "Chemical waste",
1920
+ 3: "Nuclear waste",
1921
+ 4: "Explosives",
1922
+ 5: "Spoil ground",
1923
+ 6: "Vessel dumping ground"
1924
+ };
1925
+ var CATMFA_TABLE = {
1926
+ 1: "Crustaceans",
1927
+ 2: "Oysters / mussels",
1928
+ 3: "Fish",
1929
+ 4: "Seaweed",
1930
+ 5: "Pearl culture"
1931
+ };
1932
+ var CATCBL_TABLE = {
1933
+ 1: "Submarine power cable",
1934
+ 2: "Submarine transmission cable",
1935
+ 3: "Submarine telegraph cable",
1936
+ 4: "Submarine telephone cable",
1937
+ 5: "Mooring cable",
1938
+ 6: "Ferry cable"
1939
+ };
1940
+ var CATPIP_TABLE = {
1941
+ 1: "Pipeline",
1942
+ 2: "Outfall pipe",
1943
+ 3: "Intake pipe",
1944
+ 4: "Sewer",
1945
+ 5: "Bubbler system",
1946
+ 6: "Supply pipe"
1947
+ };
1948
+ var TRAFIC_TABLE = {
1949
+ 1: "Inbound",
1950
+ 2: "Outbound",
1951
+ 3: "One-way",
1952
+ 4: "Two-way"
1953
+ };
1954
+ var QUASOU_TABLE = {
1955
+ 1: "Depth known",
1956
+ 2: "Depth unknown",
1957
+ 3: "Doubtful sounding",
1958
+ 4: "Unreliable sounding",
1959
+ 6: "Least depth known",
1960
+ 7: "Least depth unknown",
1961
+ 9: "Not regularly maintained",
1962
+ 10: "Maintained depth",
1963
+ 11: "Not surveyed"
1964
+ };
1965
+ function decodeAreaFeatureProps(layerName, props) {
1966
+ const rawLayer = layerName.replace(/^enc-/, "").replace(/-(point|line|polygon|midpoint)$/, "");
1967
+ const parseCodeLocal = (val) => {
1968
+ if (val == null) return null;
1969
+ const s = String(val);
1970
+ try {
1971
+ const arr = JSON.parse(s);
1972
+ if (Array.isArray(arr)) return Number(arr[0]) || null;
1973
+ } catch {
1974
+ }
1975
+ return Number(s.split(",")[0]) || null;
1976
+ };
1977
+ const entries = [];
1978
+ let type = rawLayer;
1979
+ let title = props.OBJNAM ? String(props.OBJNAM) : "";
1980
+ if (rawLayer === "DEPARE") {
1981
+ type = "Depth Area";
1982
+ const drval1 = props.DRVAL1 != null ? Number(props.DRVAL1) : null;
1983
+ const drval2 = props.DRVAL2 != null ? Number(props.DRVAL2) : null;
1984
+ if (drval1 != null && drval2 != null) {
1985
+ title = title || `${drval1}m \u2013 ${drval2}m`;
1986
+ entries.push({ label: "Depth Range", value: `${drval1}m \u2013 ${drval2}m` });
1987
+ } else if (drval1 != null) {
1988
+ entries.push({ label: "Min Depth", value: `${drval1}m` });
1989
+ }
1990
+ const qc = parseCodeLocal(props.QUASOU);
1991
+ if (qc && QUASOU_TABLE[qc]) entries.push({ label: "Quality", value: QUASOU_TABLE[qc] });
1992
+ } else if (rawLayer === "OBSTRN") {
1993
+ type = "Obstruction";
1994
+ const cat = parseCodeLocal(props.CATOBS);
1995
+ if (cat && CATOBS_TABLE[cat]) {
1996
+ entries.push({ label: "Category", value: CATOBS_TABLE[cat] });
1997
+ if (!title) title = CATOBS_TABLE[cat];
1998
+ }
1999
+ if (props.VALSOU != null) entries.push({ label: "Depth", value: `${props.VALSOU}m` });
2000
+ const wl = parseCodeLocal(props.WATLEV);
2001
+ if (wl && WATLEV_TABLE[wl]) entries.push({ label: "Water Level", value: WATLEV_TABLE[wl] });
2002
+ const qc = parseCodeLocal(props.QUASOU);
2003
+ if (qc && QUASOU_TABLE[qc]) entries.push({ label: "Sounding Quality", value: QUASOU_TABLE[qc] });
2004
+ } else if (rawLayer === "WRECKS") {
2005
+ type = "Wreck";
2006
+ const cat = parseCodeLocal(props.CATWRK);
2007
+ if (cat && CATWRK_TABLE[cat]) {
2008
+ entries.push({ label: "Category", value: CATWRK_TABLE[cat] });
2009
+ if (!title) title = CATWRK_TABLE[cat];
2010
+ }
2011
+ if (props.VALSOU != null) entries.push({ label: "Depth", value: `${props.VALSOU}m` });
2012
+ const wl = parseCodeLocal(props.WATLEV);
2013
+ if (wl && WATLEV_TABLE[wl]) entries.push({ label: "Water Level", value: WATLEV_TABLE[wl] });
2014
+ const qc = parseCodeLocal(props.QUASOU);
2015
+ if (qc && QUASOU_TABLE[qc]) entries.push({ label: "Sounding Quality", value: QUASOU_TABLE[qc] });
2016
+ } else if (rawLayer === "RESARE") {
2017
+ type = "Restricted Area";
2018
+ const cat = parseCodeLocal(props.CATREA);
2019
+ if (cat && CATREA_TABLE[cat]) {
2020
+ entries.push({ label: "Category", value: CATREA_TABLE[cat] });
2021
+ if (!title) title = CATREA_TABLE[cat];
2022
+ }
2023
+ if (props.RESTRN) {
2024
+ const codes = parseListCodes(props.RESTRN).filter((n) => RESTRN_TABLE[n]);
2025
+ if (codes.length > 0) {
2026
+ entries.push({ label: "Restrictions", value: codes.map((c) => RESTRN_TABLE[c]).join("; ") });
2027
+ }
2028
+ }
2029
+ } else if (rawLayer === "ACHARE") {
2030
+ type = "Anchorage Area";
2031
+ const cat = parseCodeLocal(props.CATACH);
2032
+ if (cat && CATACH_TABLE[cat]) {
2033
+ entries.push({ label: "Category", value: CATACH_TABLE[cat] });
2034
+ if (!title) title = CATACH_TABLE[cat];
2035
+ }
2036
+ if (props.RESTRN) {
2037
+ const codes = parseListCodes(props.RESTRN).filter((n) => RESTRN_TABLE[n]);
2038
+ if (codes.length > 0) entries.push({ label: "Restrictions", value: codes.map((c) => RESTRN_TABLE[c]).join("; ") });
2039
+ }
2040
+ const st2 = parseCodeLocal(props.STATUS);
2041
+ if (st2 && STATUS_TABLE[st2]) entries.push({ label: "Status", value: STATUS_TABLE[st2] });
2042
+ } else if (rawLayer === "CTNARE") {
2043
+ type = "Caution Area";
2044
+ } else if (rawLayer === "DRGARE") {
2045
+ type = "Dredged Area";
2046
+ const drval1 = props.DRVAL1 != null ? Number(props.DRVAL1) : null;
2047
+ const drval2 = props.DRVAL2 != null ? Number(props.DRVAL2) : null;
2048
+ if (drval1 != null && drval2 != null) {
2049
+ entries.push({ label: "Maintained Depth", value: `${drval1}m \u2013 ${drval2}m` });
2050
+ } else if (drval1 != null) {
2051
+ entries.push({ label: "Maintained Depth", value: `${drval1}m` });
2052
+ }
2053
+ const qc = parseCodeLocal(props.QUASOU);
2054
+ if (qc && QUASOU_TABLE[qc]) entries.push({ label: "Quality", value: QUASOU_TABLE[qc] });
2055
+ } else if (rawLayer === "FAIRWY") {
2056
+ type = "Fairway";
2057
+ const trafic = parseCodeLocal(props.TRAFIC);
2058
+ if (trafic && TRAFIC_TABLE[trafic]) entries.push({ label: "Traffic", value: TRAFIC_TABLE[trafic] });
2059
+ if (props.ORIENT != null) entries.push({ label: "Orientation", value: `${props.ORIENT}\xB0` });
2060
+ } else if (rawLayer === "DMPGRD") {
2061
+ type = "Dumping Ground";
2062
+ const cat = parseCodeLocal(props.CATDPG);
2063
+ if (cat && CATDPG_TABLE[cat]) {
2064
+ entries.push({ label: "Category", value: CATDPG_TABLE[cat] });
2065
+ if (!title) title = CATDPG_TABLE[cat];
2066
+ }
2067
+ } else if (rawLayer === "MIPARE") {
2068
+ type = "Military Practice Area";
2069
+ if (props.RESTRN) {
2070
+ const codes = parseListCodes(props.RESTRN).filter((n) => RESTRN_TABLE[n]);
2071
+ if (codes.length > 0) entries.push({ label: "Restrictions", value: codes.map((c) => RESTRN_TABLE[c]).join("; ") });
2072
+ }
2073
+ } else if (rawLayer === "PRCARE") {
2074
+ type = "Precautionary Area";
2075
+ const trafic = parseCodeLocal(props.TRAFIC);
2076
+ if (trafic && TRAFIC_TABLE[trafic]) entries.push({ label: "Traffic", value: TRAFIC_TABLE[trafic] });
2077
+ if (props.ORIENT != null) entries.push({ label: "Orientation", value: `${props.ORIENT}\xB0` });
2078
+ } else if (rawLayer === "TSSLPT") {
2079
+ type = "TSS Lane";
2080
+ const trafic = parseCodeLocal(props.TRAFIC);
2081
+ if (trafic && TRAFIC_TABLE[trafic]) entries.push({ label: "Traffic", value: TRAFIC_TABLE[trafic] });
2082
+ if (props.ORIENT != null) entries.push({ label: "Orientation", value: `${props.ORIENT}\xB0` });
2083
+ } else if (rawLayer === "TSELNE") {
2084
+ type = "TSS Boundary";
2085
+ } else if (rawLayer === "ISTZNE") {
2086
+ type = "Inshore Traffic Zone";
2087
+ } else if (rawLayer === "OSPARE") {
2088
+ type = "Offshore Production Area";
2089
+ if (props.RESTRN) {
2090
+ const codes = parseListCodes(props.RESTRN).filter((n) => RESTRN_TABLE[n]);
2091
+ if (codes.length > 0) entries.push({ label: "Restrictions", value: codes.map((c) => RESTRN_TABLE[c]).join("; ") });
2092
+ }
2093
+ } else if (rawLayer === "MARCUL") {
2094
+ type = "Marine Farm / Culture";
2095
+ const cat = parseCodeLocal(props.CATMFA);
2096
+ if (cat && CATMFA_TABLE[cat]) {
2097
+ entries.push({ label: "Category", value: CATMFA_TABLE[cat] });
2098
+ if (!title) title = CATMFA_TABLE[cat];
2099
+ }
2100
+ if (props.RESTRN) {
2101
+ const codes = parseListCodes(props.RESTRN).filter((n) => RESTRN_TABLE[n]);
2102
+ if (codes.length > 0) entries.push({ label: "Restrictions", value: codes.map((c) => RESTRN_TABLE[c]).join("; ") });
2103
+ }
2104
+ } else if (rawLayer === "CBLARE") {
2105
+ type = "Cable Area";
2106
+ const cat = parseCodeLocal(props.CATCBL);
2107
+ if (cat && CATCBL_TABLE[cat]) {
2108
+ entries.push({ label: "Category", value: CATCBL_TABLE[cat] });
2109
+ if (!title) title = `${CATCBL_TABLE[cat]} area`;
2110
+ }
2111
+ const st2 = parseCodeLocal(props.STATUS);
2112
+ if (st2 && STATUS_TABLE[st2]) entries.push({ label: "Status", value: STATUS_TABLE[st2] });
2113
+ if (props.RESTRN) {
2114
+ const codes = parseListCodes(props.RESTRN).filter((n) => RESTRN_TABLE[n]);
2115
+ if (codes.length > 0) entries.push({ label: "Restrictions", value: codes.map((c) => RESTRN_TABLE[c]).join("; ") });
2116
+ }
2117
+ } else if (rawLayer === "PIPARE") {
2118
+ type = "Pipeline Area";
2119
+ const cat = parseCodeLocal(props.CATPIP);
2120
+ if (cat && CATPIP_TABLE[cat]) {
2121
+ entries.push({ label: "Category", value: CATPIP_TABLE[cat] });
2122
+ if (!title) title = `${CATPIP_TABLE[cat]} area`;
2123
+ }
2124
+ const st2 = parseCodeLocal(props.STATUS);
2125
+ if (st2 && STATUS_TABLE[st2]) entries.push({ label: "Status", value: STATUS_TABLE[st2] });
2126
+ if (props.RESTRN) {
2127
+ const codes = parseListCodes(props.RESTRN).filter((n) => RESTRN_TABLE[n]);
2128
+ if (codes.length > 0) entries.push({ label: "Restrictions", value: codes.map((c) => RESTRN_TABLE[c]).join("; ") });
2129
+ }
2130
+ } else if (rawLayer === "BRIDGE") {
2131
+ const CATBRG_SHORT = {
2132
+ 1: "Fixed",
2133
+ 2: "Opening",
2134
+ 3: "Swing",
2135
+ 4: "Lifting",
2136
+ 5: "Bascule",
2137
+ 6: "Pontoon",
2138
+ 7: "Draw",
2139
+ 8: "Transporter",
2140
+ 9: "Footbridge",
2141
+ 10: "Viaduct",
2142
+ 11: "Aqueduct",
2143
+ 12: "Suspension"
2144
+ };
2145
+ const NATCON_TABLE = {
2146
+ 1: "Masonry",
2147
+ 2: "Concrete",
2148
+ 3: "Loose boulders",
2149
+ 4: "Hard-surfaced",
2150
+ 5: "Unsurfaced",
2151
+ 6: "Wooden",
2152
+ 7: "Metal",
2153
+ 8: "GRP"
2154
+ };
2155
+ const CONDTN_SHORT = {
2156
+ 1: "Under construction",
2157
+ 2: "Ruined",
2158
+ 3: "Under reclamation",
2159
+ 5: "Planned"
2160
+ };
2161
+ const catCode = parseCodeLocal(props.CATBRG);
2162
+ const catName = catCode ? CATBRG_SHORT[catCode] : null;
2163
+ type = catName ? `${catName} Bridge` : "Bridge";
2164
+ if (catName) entries.push({ label: "Category", value: catName });
2165
+ if (props.VERCLR != null) entries.push({ label: "Vertical Clearance", value: `${props.VERCLR} m` });
2166
+ if (props.VERCCL != null) entries.push({ label: "Clearance (closed)", value: `${props.VERCCL} m` });
2167
+ if (props.VERCOP != null) entries.push({ label: "Clearance (open)", value: `${props.VERCOP} m` });
2168
+ if (props.HORCLR != null) entries.push({ label: "Horizontal Clearance", value: `${props.HORCLR} m` });
2169
+ const natCode = parseCodeLocal(props.NATCON);
2170
+ if (natCode && NATCON_TABLE[natCode]) entries.push({ label: "Construction", value: NATCON_TABLE[natCode] });
2171
+ const condCode = parseCodeLocal(props.CONDTN);
2172
+ if (condCode && CONDTN_SHORT[condCode]) entries.push({ label: "Condition", value: CONDTN_SHORT[condCode] });
2173
+ const st2 = parseCodeLocal(props.STATUS);
2174
+ if (st2 && STATUS_TABLE[st2]) entries.push({ label: "Status", value: STATUS_TABLE[st2] });
2175
+ if (props.NOBJNM) entries.push({ label: "National Name", value: String(props.NOBJNM) });
2176
+ } else if (rawLayer === "CBLSUB" || rawLayer === "CBLOHD") {
2177
+ const CATCBL_LINE = {
2178
+ 1: "Power line",
2179
+ 2: "Transmission line",
2180
+ 3: "Telephone",
2181
+ 4: "Telegraph",
2182
+ 5: "Mooring cable/chain",
2183
+ 6: "Ferry cable"
2184
+ };
2185
+ type = rawLayer === "CBLSUB" ? "Submarine Cable" : "Overhead Cable";
2186
+ const cat = parseCodeLocal(props.CATCBL);
2187
+ if (cat && CATCBL_LINE[cat]) {
2188
+ entries.push({ label: "Category", value: CATCBL_LINE[cat] });
2189
+ if (!title) title = `${CATCBL_LINE[cat]} (${rawLayer === "CBLSUB" ? "submarine" : "overhead"})`;
2190
+ }
2191
+ if (props.VERCLR != null) entries.push({ label: "Vertical Clearance", value: `${props.VERCLR}m` });
2192
+ if (props.VERCSA != null) entries.push({ label: "Clearance (safe)", value: `${props.VERCSA}m` });
2193
+ if (props.RESTRN) {
2194
+ const codes = parseListCodes(props.RESTRN).filter((n) => RESTRN_TABLE[n]);
2195
+ if (codes.length > 0) entries.push({ label: "Restrictions", value: codes.map((c) => RESTRN_TABLE[c]).join("; ") });
2196
+ }
2197
+ } else if (rawLayer === "PIPSOL" || rawLayer === "PIPOHD") {
2198
+ const CATPIP_LINE = {
2199
+ 2: "Outfall pipe",
2200
+ 3: "Intake pipe",
2201
+ 4: "Sewer",
2202
+ 5: "Bubbler system",
2203
+ 6: "Supply pipe"
2204
+ };
2205
+ type = rawLayer === "PIPSOL" ? "Submarine Pipeline" : "Overhead Pipeline";
2206
+ const cat = parseCodeLocal(props.CATPIP);
2207
+ if (cat && CATPIP_LINE[cat]) {
2208
+ entries.push({ label: "Category", value: CATPIP_LINE[cat] });
2209
+ if (!title) title = `${CATPIP_LINE[cat]} (${rawLayer === "PIPSOL" ? "submarine" : "overhead"})`;
2210
+ }
2211
+ if (props.VERCLR != null) entries.push({ label: "Vertical Clearance", value: `${props.VERCLR}m` });
2212
+ if (props.RESTRN) {
2213
+ const codes = parseListCodes(props.RESTRN).filter((n) => RESTRN_TABLE[n]);
2214
+ if (codes.length > 0) entries.push({ label: "Restrictions", value: codes.map((c) => RESTRN_TABLE[c]).join("; ") });
2215
+ }
2216
+ } else if (rawLayer === "BERTHS") {
2217
+ type = "Berth";
2218
+ } else if (rawLayer === "DAMCON") {
2219
+ type = "Dam";
2220
+ if (props.VERCLR != null) entries.push({ label: "Vertical Clearance", value: `${props.VERCLR}m` });
2221
+ } else if (rawLayer === "FERYRT") {
2222
+ type = "Ferry Route";
2223
+ } else if (rawLayer === "RECTRC") {
2224
+ type = "Recommended Track";
2225
+ const trafic = parseCodeLocal(props.TRAFIC);
2226
+ if (trafic && TRAFIC_TABLE[trafic]) entries.push({ label: "Traffic", value: TRAFIC_TABLE[trafic] });
2227
+ if (props.ORIENT != null) entries.push({ label: "Orientation", value: `${props.ORIENT}\xB0` });
2228
+ } else if (rawLayer === "BUISGL") {
2229
+ type = "Building";
2230
+ const cat = parseCodeLocal(props.CATBUA);
2231
+ if (cat) {
2232
+ const CATBUA = {
2233
+ 1: "Urban area",
2234
+ 5: "Settlement",
2235
+ 6: "Town",
2236
+ 7: "Village",
2237
+ 8: "City"
2238
+ };
2239
+ if (CATBUA[cat]) {
2240
+ entries.push({ label: "Category", value: CATBUA[cat] });
2241
+ if (!title) title = CATBUA[cat];
2242
+ }
2243
+ }
2244
+ const func = parseCodeLocal(props.FUNCTN);
2245
+ if (func) {
2246
+ const FUNCTN = {
2247
+ 2: "Harbour master's office",
2248
+ 3: "Customs office",
2249
+ 4: "Health office",
2250
+ 5: "Hospital",
2251
+ 6: "Post office",
2252
+ 7: "Hotel",
2253
+ 8: "Railway station",
2254
+ 9: "Police station",
2255
+ 10: "Water-police station",
2256
+ 11: "Pilot office",
2257
+ 13: "Bank",
2258
+ 14: "Headquarters",
2259
+ 15: "Transit shed/warehouse",
2260
+ 18: "Church",
2261
+ 19: "Chapel",
2262
+ 20: "Temple",
2263
+ 21: "Synagogue",
2264
+ 22: "Mosque",
2265
+ 24: "Lookout",
2266
+ 25: "Communication",
2267
+ 28: "Power station",
2268
+ 33: "Factory",
2269
+ 35: "Refinery",
2270
+ 36: "Religious order",
2271
+ 37: "School",
2272
+ 38: "University"
2273
+ };
2274
+ if (FUNCTN[func]) entries.push({ label: "Function", value: FUNCTN[func] });
2275
+ }
2276
+ if (props.VERLEN != null) entries.push({ label: "Height", value: `${props.VERLEN}m` });
2277
+ } else if (rawLayer === "SILTNK") {
2278
+ type = "Silo / Tank";
2279
+ const cat = parseCodeLocal(props.CATSIL);
2280
+ if (cat) {
2281
+ const CATSIL = { 1: "Silo", 2: "Tank", 3: "Grain elevator", 4: "Water tower" };
2282
+ if (CATSIL[cat]) {
2283
+ type = CATSIL[cat];
2284
+ if (!title) title = CATSIL[cat];
2285
+ }
2286
+ }
2287
+ if (parseCodeLocal(props.CONVIS) === 1) entries.push({ label: "Visibility", value: "Conspicuous from seaward" });
2288
+ if (props.VERLEN != null) entries.push({ label: "Height", value: `${props.VERLEN}m` });
2289
+ } else if (rawLayer === "HULKES") {
2290
+ type = "Hulk";
2291
+ if (props.CATHLK != null) {
2292
+ const cat = parseCodeLocal(props.CATHLK);
2293
+ const CATHLK = { 1: "Floating restaurant", 2: "Historic ship", 3: "Museum", 4: "Accommodation", 5: "Floating breakwater" };
2294
+ if (cat && CATHLK[cat]) {
2295
+ entries.push({ label: "Use", value: CATHLK[cat] });
2296
+ if (!title) title = CATHLK[cat];
2297
+ }
2298
+ }
2299
+ } else if (rawLayer === "DRYDOC") {
2300
+ type = "Dry Dock";
2301
+ } else if (rawLayer === "FLODOC") {
2302
+ type = "Floating Dock";
2303
+ if (props.VERLEN != null) entries.push({ label: "Length", value: `${props.VERLEN}m` });
2304
+ if (props.HORLEN != null) entries.push({ label: "Horiz. Length", value: `${props.HORLEN}m` });
2305
+ } else if (rawLayer === "PONTON") {
2306
+ type = "Pontoon";
2307
+ } else if (rawLayer === "PYLONS") {
2308
+ type = "Pylon / Bridge Support";
2309
+ const cat = parseCodeLocal(props.CATPYL);
2310
+ if (cat) {
2311
+ const CATPYL = {
2312
+ 1: "Power transmission pylon",
2313
+ 2: "Telecom pylon",
2314
+ 3: "Aerial cableway pylon",
2315
+ 4: "Bridge pylon",
2316
+ 5: "Bridge pier",
2317
+ 6: "Chimney",
2318
+ 7: "Flare stack"
2319
+ };
2320
+ if (CATPYL[cat]) {
2321
+ entries.push({ label: "Type", value: CATPYL[cat] });
2322
+ if (!title) title = CATPYL[cat];
2323
+ }
2324
+ }
2325
+ if (props.VERLEN != null) entries.push({ label: "Height", value: `${props.VERLEN}m` });
2326
+ } else if (rawLayer === "GATCON") {
2327
+ type = "Gate";
2328
+ const cat = parseCodeLocal(props.CATGAT);
2329
+ if (cat) {
2330
+ const CATGAT = { 1: "General gate", 2: "Flood barrage gate", 3: "Caisson", 4: "Lock gate", 5: "Dyke gate" };
2331
+ if (CATGAT[cat]) {
2332
+ entries.push({ label: "Type", value: CATGAT[cat] });
2333
+ if (!title) title = CATGAT[cat];
2334
+ }
2335
+ }
2336
+ } else if (rawLayer === "LOGPON") {
2337
+ type = "Log Pond";
2338
+ } else if (rawLayer === "AIRARE") {
2339
+ type = "Airport / Airfield";
2340
+ const cat = parseCodeLocal(props.CATAIR);
2341
+ if (cat) {
2342
+ const CATAIR = { 1: "Airport", 2: "Heliport", 3: "Helipad", 4: "Emergency helipad" };
2343
+ if (CATAIR[cat]) {
2344
+ entries.push({ label: "Type", value: CATAIR[cat] });
2345
+ if (!title) title = CATAIR[cat];
2346
+ }
2347
+ }
2348
+ } else if (rawLayer === "SLCONS") {
2349
+ type = "Shoreline Construction";
2350
+ const cat = parseCodeLocal(props.CATSLC);
2351
+ if (cat) {
2352
+ const CATSLC = {
2353
+ 1: "Breakwater",
2354
+ 2: "Groyne",
2355
+ 3: "Mole",
2356
+ 4: "Pier",
2357
+ 5: "Promenade pier",
2358
+ 6: "Wharf",
2359
+ 7: "Training wall",
2360
+ 8: "Rip rap",
2361
+ 9: "Revetment",
2362
+ 10: "Sea wall",
2363
+ 11: "Landing stairs",
2364
+ 12: "Landing place",
2365
+ 13: "Fender",
2366
+ 14: "Lock",
2367
+ 15: "Flood gate",
2368
+ 16: "Slipway",
2369
+ 17: "Log ramp"
2370
+ };
2371
+ if (CATSLC[cat]) {
2372
+ entries.push({ label: "Type", value: CATSLC[cat] });
2373
+ if (!title) title = CATSLC[cat];
2374
+ }
2375
+ }
2376
+ const wl = parseCodeLocal(props.WATLEV);
2377
+ if (wl && WATLEV_TABLE[wl]) entries.push({ label: "Water Level", value: WATLEV_TABLE[wl] });
2378
+ } else if (rawLayer === "COALNE") {
2379
+ type = "Coastline";
2380
+ const cat = parseCodeLocal(props.CATCOA);
2381
+ if (cat) {
2382
+ const CATCOA = {
2383
+ 1: "Steep coast",
2384
+ 2: "Flat coast",
2385
+ 3: "Sandy shore",
2386
+ 4: "Stony shore",
2387
+ 5: "Shingly shore",
2388
+ 6: "Glacier",
2389
+ 7: "Mangrove",
2390
+ 8: "Marshy shore",
2391
+ 9: "Coral reef",
2392
+ 10: "Ice coast",
2393
+ 11: "Rocky shore"
2394
+ };
2395
+ if (CATCOA[cat]) {
2396
+ entries.push({ label: "Type", value: CATCOA[cat] });
2397
+ if (!title) title = CATCOA[cat];
2398
+ }
2399
+ }
2400
+ } else if (rawLayer === "DEPCNT") {
2401
+ type = "Depth Contour";
2402
+ if (props.VALDCO != null) {
2403
+ title = title || `${props.VALDCO}m contour`;
2404
+ entries.push({ label: "Depth", value: `${props.VALDCO}m` });
2405
+ }
2406
+ } else if (rawLayer === "RIVERS") {
2407
+ type = "River";
2408
+ } else if (rawLayer === "CANALS") {
2409
+ type = "Canal";
2410
+ if (props.HORWID != null) entries.push({ label: "Width", value: `${props.HORWID}m` });
2411
+ } else if (rawLayer === "LAKARE") {
2412
+ type = "Lake";
2413
+ } else if (rawLayer === "RAPIDS") {
2414
+ type = "Rapids";
2415
+ } else if (rawLayer === "WATFAL") {
2416
+ type = "Waterfall";
2417
+ if (props.HEIGHT != null) entries.push({ label: "Height", value: `${props.HEIGHT}m` });
2418
+ } else if (rawLayer === "LNDARE") {
2419
+ type = "Land Area";
2420
+ }
2421
+ if (props.INFORM) entries.push({ label: "Information", value: String(props.INFORM) });
2422
+ if (props.TXTDSC) entries.push({ label: "Text Description", value: String(props.TXTDSC) });
2423
+ const st = parseCodeLocal(props.STATUS);
2424
+ if (st && STATUS_TABLE[st]) entries.push({ label: "Status", value: STATUS_TABLE[st] });
2425
+ return { type, title: title || type, properties: entries };
2426
+ }
2427
+ function parseCode(val) {
2428
+ if (val == null) return null;
2429
+ let s = String(val).trim();
2430
+ if (s.startsWith("[")) {
2431
+ try {
2432
+ const arr = JSON.parse(s);
2433
+ if (Array.isArray(arr) && arr.length > 0) s = String(arr[0]);
2434
+ else return null;
2435
+ } catch {
2436
+ }
2437
+ }
2438
+ s = s.split(/[,;]/)[0].trim();
2439
+ const n = Number(s);
2440
+ return isNaN(n) ? null : n;
2441
+ }
2442
+ function parseListCodes(val) {
2443
+ if (val == null) return [];
2444
+ if (Array.isArray(val)) {
2445
+ return val.map(Number).filter((n) => !isNaN(n) && n > 0);
2446
+ }
2447
+ const s = String(val).trim();
2448
+ if (s.startsWith("[")) {
2449
+ try {
2450
+ const arr = JSON.parse(s);
2451
+ if (Array.isArray(arr)) return arr.map(Number).filter((n) => !isNaN(n) && n > 0);
2452
+ } catch {
2453
+ }
2454
+ }
2455
+ return s.split(/[,;]/).map((v) => Number(v.trim())).filter((n) => !isNaN(n) && n > 0);
2456
+ }
2457
+ function parseGroup(val) {
2458
+ if (val == null) return null;
2459
+ const s = String(val).trim();
2460
+ const parenMatch = s.match(/^\(?([\d+]+)\)?$/);
2461
+ if (parenMatch) {
2462
+ const parts = parenMatch[1].split("+").map(Number);
2463
+ if (parts.every((n2) => !isNaN(n2) && n2 > 0)) return parts;
2464
+ }
2465
+ const n = Number(s);
2466
+ return isNaN(n) || n <= 0 ? null : [n];
2467
+ }
2468
+ var CHILD_LAYERS = /* @__PURE__ */ new Set(["LIGHTS", "FOGSIG", "RTPBCN", "DAYMAR", "TOPMAR"]);
2469
+ var PARENT_LAYERS = [
2470
+ "enc-BOYLAT",
2471
+ "enc-BOYCAR",
2472
+ "enc-BOYSPP",
2473
+ "enc-BOYISD",
2474
+ "enc-BOYSAW",
2475
+ "enc-BCNLAT",
2476
+ "enc-BCNCAR",
2477
+ "enc-BCNSPP",
2478
+ "enc-BCNISD",
2479
+ "enc-BCNSAW"
2480
+ ];
2481
+ function deriveMarinerName(objnam, colourCode) {
2482
+ if (!objnam) return "";
2483
+ const m = objnam.match(/(\d+[A-Z]?)\s*$/);
2484
+ if (!m) return objnam;
2485
+ const num = m[1];
2486
+ const colourAbbr = colourCode ? COLOUR_TABLE[colourCode]?.abbr : null;
2487
+ if (colourAbbr && colourAbbr !== "W") {
2488
+ return `${colourAbbr} "${num}"`;
2489
+ }
2490
+ return `"${num}"`;
2491
+ }
2492
+ function getTopmarkMeaning(topshpCode, rawLayer) {
2493
+ if (topshpCode == null) return null;
2494
+ switch (topshpCode) {
2495
+ case 10:
2496
+ return "West cardinal \u2014 pass to the west";
2497
+ // 2 cones, point to point
2498
+ case 11:
2499
+ return "East cardinal \u2014 pass to the east";
2500
+ // 2 cones, base to base
2501
+ case 13:
2502
+ return "North cardinal \u2014 pass to the north";
2503
+ // 2 cones, points up
2504
+ case 14:
2505
+ return "South cardinal \u2014 pass to the south";
2506
+ }
2507
+ const parent = rawLayer.replace(/^(BOY|BCN)/, "");
2508
+ if (parent === "LAT") {
2509
+ if (topshpCode === 1) return "Starboard-hand mark";
2510
+ if (topshpCode === 5) return "Port-hand mark";
2511
+ }
2512
+ if (parent === "SAW" && topshpCode === 3) return "Safe water \u2014 navigable water all around";
2513
+ if (parent === "ISD" && topshpCode === 4) return "Isolated danger \u2014 danger of limited extent";
2514
+ if (parent === "SPP" && topshpCode === 7) return "Special-purpose mark";
2515
+ return null;
2516
+ }
2517
+ function decodeNavAidProps(layerName, props, coords) {
2518
+ const rawLayer = layerName.replace(/^enc-/, "").replace(/-(point|line|polygon|midpoint)$/, "");
2519
+ const fullName = props.OBJNAM || props.NOBJNM || "";
2520
+ const colourCode = parseCode(props.COLOUR);
2521
+ const marinerName = deriveMarinerName(fullName, colourCode);
2522
+ const name = marinerName || fullName;
2523
+ const type = LAYER_TYPE_MAP[rawLayer] ?? rawLayer;
2524
+ const lightColourSource = props.LIGHT_COLOUR ?? props.COLOUR;
2525
+ const lightColourCodes = parseListCodes(lightColourSource);
2526
+ const colourEntry = colourCode ? COLOUR_TABLE[colourCode] : null;
2527
+ const lightColourHexList = lightColourCodes.map((c) => COLOUR_TABLE[c]?.hex ?? "#ffffff");
2528
+ const boyshpCode = parseCode(props.BOYSHP);
2529
+ const shape = boyshpCode ? BOYSHP_TABLE[boyshpCode] ?? null : null;
2530
+ const topshpCode = parseCode(props.TOPSHP);
2531
+ const topmark = topshpCode ? TOPSHP_TABLE[topshpCode] ?? null : null;
2532
+ const topmarkMeaning = getTopmarkMeaning(topshpCode, rawLayer);
2533
+ const litchrCode = parseCode(props.LITCHR);
2534
+ const litchrEntry = litchrCode ? LITCHR_TABLE[litchrCode] : null;
2535
+ const sigper = props.SIGPER != null ? Number(props.SIGPER) : null;
2536
+ const group = parseGroup(props.SIGGRP);
2537
+ const nameForFog = String(props.OBJNAM ?? "").toLowerCase();
2538
+ let catfogCode = null;
2539
+ if (/\bwhistle\b/.test(nameForFog)) catfogCode = 6;
2540
+ else if (/\bgong\b/.test(nameForFog)) catfogCode = 8;
2541
+ else if (/\bbell\b/.test(nameForFog)) catfogCode = 5;
2542
+ else if (/\bhorn\b/.test(nameForFog)) catfogCode = 9;
2543
+ else if (/\bsiren\b/.test(nameForFog)) catfogCode = 3;
2544
+ else if (/diaphone/.test(nameForFog)) catfogCode = 2;
2545
+ else if (/nautophone|nauto/.test(nameForFog)) catfogCode = 4;
2546
+ if (!catfogCode) catfogCode = parseCode(props.CATFOG);
2547
+ const fogSignal2 = catfogCode ? CATFOG_TABLE[catfogCode] ?? null : null;
2548
+ const fogSignalAbbr = catfogCode ? CATFOG_ABBR_TABLE[catfogCode] ?? null : null;
2549
+ const radarReflector = props.RETRFL != null && String(props.RETRFL) !== "0" && String(props.RETRFL) !== "";
2550
+ const valsou = props.VALSOU != null ? Number(props.VALSOU) : null;
2551
+ const depth = valsou != null && !isNaN(valsou) ? valsou : null;
2552
+ const statusCode = parseCode(props.STATUS);
2553
+ const status = statusCode ? STATUS_TABLE[statusCode] ?? null : null;
2554
+ const marsysCode = parseCode(props.MARSYS);
2555
+ const markingSystem = marsysCode ? MARSYS_TABLE[marsysCode] ?? null : null;
2556
+ const catlamCode = parseCode(props.CATLAM);
2557
+ const catcamCode = parseCode(props.CATCAM);
2558
+ const cathafCode = parseCode(props.CATHAF);
2559
+ const catscfCode = parseCode(props.CATSCF);
2560
+ const catcrnCode = parseCode(props.CATCRN);
2561
+ const catlmkNames = parseListCodes(props.CATLMK).map((c) => CATLMK_TABLE[c]).filter((n) => Boolean(n));
2562
+ const category = catlamCode ? CATLAM_TABLE[catlamCode] ?? null : catcamCode ? CATCAM_TABLE[catcamCode] ?? null : cathafCode ? CATHAF_TABLE[cathafCode] ?? null : catscfCode ? CATSCF_TABLE[catscfCode] ?? null : catcrnCode ? CATCRN_TABLE[catcrnCode] ?? null : catlmkNames.length > 0 ? catlmkNames.join(" / ") : null;
2563
+ const information = props.INFORM || null;
2564
+ const lightColourAbbrs = lightColourCodes.map((c) => COLOUR_TABLE[c]?.abbr).filter(Boolean);
2565
+ const notation = buildNotation(litchrEntry?.abbr ?? null, lightColourAbbrs, sigper, group);
2566
+ const sectr1 = props.SECTR1 != null ? Number(props.SECTR1) : null;
2567
+ const sectr2 = props.SECTR2 != null ? Number(props.SECTR2) : null;
2568
+ const orient = props.ORIENT != null ? Number(props.ORIENT) : null;
2569
+ const catlit = parseCode(props.CATLIT);
2570
+ const isDirectional = catlit === 1;
2571
+ const sectors = [];
2572
+ const isSectorLight = sectr1 != null && sectr2 != null && !isNaN(sectr1) && !isNaN(sectr2);
2573
+ const valnmr = props.VALNMR != null ? Number(props.VALNMR) : null;
2574
+ const nominalRange = valnmr != null && !isNaN(valnmr) && valnmr > 0 ? valnmr : null;
2575
+ if (isSectorLight) {
2576
+ sectors.push({
2577
+ sectorStart: sectr1,
2578
+ sectorEnd: sectr2,
2579
+ colour: colourEntry?.name ?? "White",
2580
+ colourHex: colourEntry?.hex ?? "#ffffff",
2581
+ notation,
2582
+ nominalRange
2583
+ });
2584
+ }
2585
+ return {
2586
+ name,
2587
+ fullName: fullName && fullName !== name ? fullName : null,
2588
+ type,
2589
+ layerName: rawLayer,
2590
+ colour: colourEntry?.name ?? null,
2591
+ colourHex: colourEntry?.hex ?? "#ffffff",
2592
+ lightColourHex: lightColourHexList[0] ?? colourEntry?.hex ?? "#ffffff",
2593
+ colourHexList: lightColourHexList.length > 0 ? lightColourHexList : [colourEntry?.hex ?? "#ffffff"],
2594
+ shape,
2595
+ topmark,
2596
+ topmarkMeaning,
2597
+ lightCharacter: litchrEntry?.abbr ?? null,
2598
+ lightCharacterFull: litchrEntry?.name ?? null,
2599
+ period: sigper,
2600
+ group,
2601
+ fogSignal: fogSignal2,
2602
+ fogSignalAbbr,
2603
+ radarReflector,
2604
+ depth,
2605
+ status,
2606
+ markingSystem,
2607
+ category,
2608
+ information,
2609
+ notation,
2610
+ coordinates: coords ?? null,
2611
+ sectors,
2612
+ orientation: orient != null && !isNaN(orient) ? orient : null,
2613
+ isDirectional,
2614
+ isSectorLight
2615
+ };
2616
+ }
2617
+ function buildNotation(litchrAbbr, colourAbbrs, sigper, group) {
2618
+ if (!litchrAbbr) return "";
2619
+ const parts = [];
2620
+ let charPart = litchrAbbr;
2621
+ const totalFlashes = group ? group.reduce((a, b) => a + b, 0) : 0;
2622
+ if (totalFlashes > 1) charPart += `(${group.join("+")})`;
2623
+ parts.push(charPart);
2624
+ if (colourAbbrs.length > 0) parts.push(colourAbbrs.join("."));
2625
+ if (sigper && sigper > 0) parts.push(`${sigper}s`);
2626
+ return parts.join(" ");
2627
+ }
2628
+ function buildFlashPattern(decoded) {
2629
+ const litchr = decoded.lightCharacter;
2630
+ if (!litchr) return null;
2631
+ const period = decoded.period && decoded.period > 0 ? decoded.period * 1e3 : 6e3;
2632
+ const groups = decoded.group ?? [1];
2633
+ const totalFlashes = groups.reduce((a, b) => a + b, 0);
2634
+ const hex = decoded.lightColourHex;
2635
+ const colours = decoded.colourHexList;
2636
+ const notation = decoded.notation;
2637
+ const phases = [];
2638
+ switch (litchr) {
2639
+ case "F": {
2640
+ phases.push({ on: true, duration: period, color: hex });
2641
+ break;
2642
+ }
2643
+ case "Fl":
2644
+ case "S.Fl": {
2645
+ const flashDur = 400;
2646
+ const interFlashGap = 300;
2647
+ const interGroupGap = 800;
2648
+ const activeDur = groups.reduce((sum, g) => sum + g * flashDur + (g - 1) * interFlashGap, 0) + (groups.length - 1) * interGroupGap;
2649
+ const eclipseDur = Math.max(period - activeDur, 500);
2650
+ for (let gi = 0; gi < groups.length; gi++) {
2651
+ const g = groups[gi];
2652
+ for (let i = 0; i < g; i++) {
2653
+ phases.push({ on: true, duration: flashDur, color: hex });
2654
+ if (i < g - 1) phases.push({ on: false, duration: interFlashGap });
2655
+ }
2656
+ if (gi < groups.length - 1) phases.push({ on: false, duration: interGroupGap });
2657
+ }
2658
+ phases.push({ on: false, duration: eclipseDur });
2659
+ break;
2660
+ }
2661
+ case "LFl":
2662
+ case "S.LFl": {
2663
+ const onDur = 2e3;
2664
+ phases.push({ on: true, duration: onDur, color: hex });
2665
+ phases.push({ on: false, duration: Math.max(period - onDur, 1e3) });
2666
+ break;
2667
+ }
2668
+ case "Oc": {
2669
+ const offDur = 400;
2670
+ const interGroupOn = 300;
2671
+ const interGroupGap = 800;
2672
+ const totalOcclusions = totalFlashes;
2673
+ const activeDur = totalOcclusions * offDur + (totalOcclusions - 1) * interGroupOn + (groups.length > 1 ? (groups.length - 1) * (interGroupGap - interGroupOn) : 0);
2674
+ const onDur = Math.max(period - activeDur, 500);
2675
+ phases.push({ on: true, duration: onDur, color: hex });
2676
+ for (let gi = 0; gi < groups.length; gi++) {
2677
+ const g = groups[gi];
2678
+ for (let i = 0; i < g; i++) {
2679
+ phases.push({ on: false, duration: offDur });
2680
+ if (i < g - 1) phases.push({ on: true, duration: interGroupOn, color: hex });
2681
+ }
2682
+ if (gi < groups.length - 1) phases.push({ on: true, duration: interGroupGap, color: hex });
2683
+ }
2684
+ break;
2685
+ }
2686
+ case "Iso": {
2687
+ const half = period / 2;
2688
+ phases.push({ on: true, duration: half, color: hex });
2689
+ phases.push({ on: false, duration: half });
2690
+ break;
2691
+ }
2692
+ case "Q": {
2693
+ const cycleDur = 1e3;
2694
+ const count = Math.max(totalFlashes, 1);
2695
+ if (count === 1) {
2696
+ phases.push({ on: true, duration: cycleDur / 2, color: hex });
2697
+ phases.push({ on: false, duration: cycleDur / 2 });
2698
+ } else {
2699
+ const flashDur = 250;
2700
+ const gap = 250;
2701
+ for (let i = 0; i < count; i++) {
2702
+ phases.push({ on: true, duration: flashDur, color: hex });
2703
+ if (i < count - 1) phases.push({ on: false, duration: gap });
2704
+ }
2705
+ phases.push({ on: false, duration: Math.max(period - count * (flashDur + gap), 500) });
2706
+ }
2707
+ break;
2708
+ }
2709
+ case "VQ": {
2710
+ const flashDur = 150;
2711
+ const gap = 150;
2712
+ const count = Math.max(totalFlashes, 1);
2713
+ if (count === 1) {
2714
+ phases.push({ on: true, duration: flashDur, color: hex });
2715
+ phases.push({ on: false, duration: gap });
2716
+ } else {
2717
+ for (let i = 0; i < count; i++) {
2718
+ phases.push({ on: true, duration: flashDur, color: hex });
2719
+ if (i < count - 1) phases.push({ on: false, duration: gap });
2720
+ }
2721
+ phases.push({ on: false, duration: Math.max(period - count * (flashDur + gap), 500) });
2722
+ }
2723
+ break;
2724
+ }
2725
+ case "UQ": {
2726
+ const flashDur = 80;
2727
+ const gap = 80;
2728
+ phases.push({ on: true, duration: flashDur, color: hex });
2729
+ phases.push({ on: false, duration: gap });
2730
+ break;
2731
+ }
2732
+ case "Mo": {
2733
+ const dot = 300;
2734
+ const dash = 900;
2735
+ const gap = 300;
2736
+ phases.push({ on: true, duration: dot, color: hex });
2737
+ phases.push({ on: false, duration: gap });
2738
+ phases.push({ on: true, duration: dash, color: hex });
2739
+ phases.push({ on: false, duration: gap });
2740
+ phases.push({ on: true, duration: dot, color: hex });
2741
+ phases.push({ on: false, duration: Math.max(period - 3 * gap - dot * 2 - dash, 1e3) });
2742
+ break;
2743
+ }
2744
+ case "Al":
2745
+ case "Al.Gp":
2746
+ case "Al.Fl":
2747
+ case "Al.LFl":
2748
+ case "Al.Oc":
2749
+ case "Al.F.Fl": {
2750
+ const isFlashing = litchr.includes("Fl") || litchr === "Al.Gp";
2751
+ const isOcculting = litchr.includes("Oc");
2752
+ const isFixed = litchr === "Al" || litchr === "Al.F.Fl";
2753
+ if (isFlashing && totalFlashes > 1) {
2754
+ const flashDur = 400;
2755
+ const interFlashGap = 300;
2756
+ const interGroupGap = 800;
2757
+ const activeDur = groups.reduce((sum, g) => sum + g * flashDur + (g - 1) * interFlashGap, 0) + (groups.length - 1) * interGroupGap;
2758
+ const eclipseDur = Math.max(period - activeDur, 500);
2759
+ for (let gi = 0; gi < groups.length; gi++) {
2760
+ const g = groups[gi];
2761
+ const groupColour = colours[gi % colours.length];
2762
+ for (let i = 0; i < g; i++) {
2763
+ phases.push({ on: true, duration: flashDur, color: groupColour });
2764
+ if (i < g - 1) phases.push({ on: false, duration: interFlashGap });
2765
+ }
2766
+ if (gi < groups.length - 1) phases.push({ on: false, duration: interGroupGap });
2767
+ }
2768
+ phases.push({ on: false, duration: eclipseDur });
2769
+ } else if (isOcculting) {
2770
+ const numColours = Math.max(colours.length, 2);
2771
+ const offDur = 400;
2772
+ const onDur = (period - numColours * offDur) / numColours;
2773
+ for (let i = 0; i < numColours; i++) {
2774
+ phases.push({ on: true, duration: Math.max(onDur, 300), color: colours[i % colours.length] });
2775
+ phases.push({ on: false, duration: offDur });
2776
+ }
2777
+ } else if (isFixed) {
2778
+ const numColours = Math.max(colours.length, 2);
2779
+ const phaseDur = period / numColours;
2780
+ for (let i = 0; i < numColours; i++) {
2781
+ phases.push({ on: true, duration: phaseDur, color: colours[i % colours.length] });
2782
+ }
2783
+ } else {
2784
+ const numColours = Math.max(colours.length, 2);
2785
+ const phaseDur = period / numColours;
2786
+ for (let i = 0; i < numColours; i++) {
2787
+ phases.push({ on: true, duration: phaseDur, color: colours[i % colours.length] });
2788
+ }
2789
+ }
2790
+ break;
2791
+ }
2792
+ default: {
2793
+ phases.push({ on: true, duration: 500, color: hex });
2794
+ phases.push({ on: false, duration: Math.max(period - 500, 500) });
2795
+ break;
2796
+ }
2797
+ }
2798
+ const totalCycle = phases.reduce((sum, p) => sum + p.duration, 0);
2799
+ return { phases, totalCycle, notation };
2800
+ }
2801
+ function buildSectorsFromFeatures(features) {
2802
+ const sectors = [];
2803
+ for (const f of features) {
2804
+ const props = f.properties ?? {};
2805
+ const s1 = props.SECTR1 != null ? Number(props.SECTR1) : null;
2806
+ const s2 = props.SECTR2 != null ? Number(props.SECTR2) : null;
2807
+ if (s1 == null || s2 == null || isNaN(s1) || isNaN(s2)) continue;
2808
+ const colourCode = parseCode(props.COLOUR);
2809
+ const ce = colourCode ? COLOUR_TABLE[colourCode] : null;
2810
+ const litchrCode = parseCode(props.LITCHR);
2811
+ const le = litchrCode ? LITCHR_TABLE[litchrCode] : null;
2812
+ const sigper = props.SIGPER != null ? Number(props.SIGPER) : null;
2813
+ const group = parseGroup(props.SIGGRP);
2814
+ const notation = buildNotation(le?.abbr ?? null, ce ? [ce.abbr] : [], sigper, group);
2815
+ const valnmr = props.VALNMR != null ? Number(props.VALNMR) : null;
2816
+ const nominalRange = valnmr != null && !isNaN(valnmr) && valnmr > 0 ? valnmr : null;
2817
+ sectors.push({
2818
+ sectorStart: s1,
2819
+ sectorEnd: s2,
2820
+ colour: ce?.name ?? "White",
2821
+ colourHex: ce?.hex ?? "#ffffff",
2822
+ notation,
2823
+ nominalRange
2824
+ });
2825
+ }
2826
+ return sectors;
2827
+ }
2828
+ function buildSectorArcGeoJSON(center, startBearing, endBearing, radiusNm = 0.5, innerRadiusNm = 0, steps = 64) {
2829
+ const radiusDeg = radiusNm / 60;
2830
+ const innerDeg = innerRadiusNm > 0 ? innerRadiusNm / 60 : 0;
2831
+ const [lng, lat] = center;
2832
+ const cosLat = Math.cos(lat * Math.PI / 180);
2833
+ const s = (startBearing % 360 + 360) % 360;
2834
+ const e = (endBearing % 360 + 360) % 360;
2835
+ let sweep = e - s;
2836
+ if (sweep <= 0) sweep += 360;
2837
+ const project = (bearingDeg, rDeg) => {
2838
+ const rad = bearingDeg * Math.PI / 180;
2839
+ return [
2840
+ lng + rDeg * Math.sin(rad) / cosLat,
2841
+ lat + rDeg * Math.cos(rad)
2842
+ ];
2843
+ };
2844
+ const outerCoords = [];
2845
+ for (let i = 0; i <= steps; i++) {
2846
+ outerCoords.push(project(s + sweep * i / steps, radiusDeg));
2847
+ }
2848
+ let ring;
2849
+ if (innerDeg > 0) {
2850
+ const innerCoords = [];
2851
+ for (let i = steps; i >= 0; i--) {
2852
+ innerCoords.push(project(s + sweep * i / steps, innerDeg));
2853
+ }
2854
+ ring = [...outerCoords, ...innerCoords, outerCoords[0]];
2855
+ } else {
2856
+ ring = [[lng, lat], ...outerCoords, [lng, lat]];
2857
+ }
2858
+ return {
2859
+ type: "Feature",
2860
+ properties: {},
2861
+ geometry: {
2862
+ type: "Polygon",
2863
+ coordinates: [ring]
2864
+ }
2865
+ };
2866
+ }
2867
+ function buildSectorArcsFC(center, sectors, radiusNm = 0.4, innerRadiusFrac = 0.92) {
2868
+ const innerNm = innerRadiusFrac > 0 ? radiusNm * innerRadiusFrac : 0;
2869
+ const features = sectors.map((sec, i) => {
2870
+ const arc = buildSectorArcGeoJSON(center, sec.sectorStart, sec.sectorEnd, radiusNm, innerNm);
2871
+ arc.properties = {
2872
+ sectorIndex: i,
2873
+ colour: sec.colour,
2874
+ colourHex: sec.colourHex,
2875
+ notation: sec.notation,
2876
+ sectorStart: sec.sectorStart,
2877
+ sectorEnd: sec.sectorEnd,
2878
+ nominalRange: sec.nominalRange ?? null
2879
+ };
2880
+ return arc;
2881
+ });
2882
+ const lineFeatures = [];
2883
+ const labelFeatures = [];
2884
+ const seenBearings = /* @__PURE__ */ new Set();
2885
+ const TICK_OUTER_OVERHANG = 1.05;
2886
+ const TICK_INNER_OVERHANG = 0.93;
2887
+ for (const sec of sectors) {
2888
+ for (const bearing of [sec.sectorStart, sec.sectorEnd]) {
2889
+ const rad = bearing * Math.PI / 180;
2890
+ const radiusDeg = radiusNm / 60;
2891
+ const innerRadiusDeg = innerNm > 0 ? innerNm / 60 : 0;
2892
+ const cosLat = Math.cos(center[1] * Math.PI / 180);
2893
+ const sinB = Math.sin(rad);
2894
+ const cosB = Math.cos(rad);
2895
+ const tickOuterDeg = radiusDeg * TICK_OUTER_OVERHANG;
2896
+ const outerPt = [
2897
+ center[0] + tickOuterDeg * sinB / cosLat,
2898
+ center[1] + tickOuterDeg * cosB
2899
+ ];
2900
+ const tickInnerDeg = innerRadiusDeg > 0 ? Math.min(innerRadiusDeg * TICK_INNER_OVERHANG, innerRadiusDeg) : 0;
2901
+ const innerPt = tickInnerDeg > 0 ? [center[0] + tickInnerDeg * sinB / cosLat, center[1] + tickInnerDeg * cosB] : center;
2902
+ lineFeatures.push({
2903
+ type: "Feature",
2904
+ properties: { type: "sector-boundary" },
2905
+ geometry: {
2906
+ type: "LineString",
2907
+ coordinates: [innerPt, outerPt]
2908
+ }
2909
+ });
2910
+ const roundedBearing = Math.round(bearing * 10) / 10;
2911
+ if (!seenBearings.has(roundedBearing)) {
2912
+ seenBearings.add(roundedBearing);
2913
+ const labelRadius = radiusDeg * 1.12;
2914
+ const ldlat = labelRadius * Math.cos(rad);
2915
+ const ldlng = labelRadius * Math.sin(rad) / cosLat;
2916
+ labelFeatures.push({
2917
+ type: "Feature",
2918
+ properties: {
2919
+ type: "bearing-label",
2920
+ bearing: `${roundedBearing}\xB0`,
2921
+ bearingDeg: roundedBearing
2922
+ },
2923
+ geometry: {
2924
+ type: "Point",
2925
+ coordinates: [center[0] + ldlng, center[1] + ldlat]
2926
+ }
2927
+ });
2928
+ }
2929
+ }
2930
+ }
2931
+ const rangeRingFeatures = [];
2932
+ const seenRanges = /* @__PURE__ */ new Set();
2933
+ for (const sec of sectors) {
2934
+ if (sec.nominalRange != null && sec.nominalRange > 0 && !seenRanges.has(sec.nominalRange)) {
2935
+ seenRanges.add(sec.nominalRange);
2936
+ const rangeDeg = sec.nominalRange / 60;
2937
+ const cosLat = Math.cos(center[1] * Math.PI / 180);
2938
+ const ringCoords = [];
2939
+ const ringSteps = 72;
2940
+ for (let i = 0; i <= ringSteps; i++) {
2941
+ const angle = 360 * i / ringSteps;
2942
+ const rad = angle * Math.PI / 180;
2943
+ const dlat = rangeDeg * Math.cos(rad);
2944
+ const dlng = rangeDeg * Math.sin(rad) / cosLat;
2945
+ ringCoords.push([center[0] + dlng, center[1] + dlat]);
2946
+ }
2947
+ rangeRingFeatures.push({
2948
+ type: "Feature",
2949
+ properties: { type: "range-ring", nominalRange: sec.nominalRange },
2950
+ geometry: { type: "LineString", coordinates: ringCoords }
2951
+ });
2952
+ rangeRingFeatures.push({
2953
+ type: "Feature",
2954
+ properties: { type: "range-label", rangeLabel: `${sec.nominalRange} NM` },
2955
+ geometry: { type: "Point", coordinates: [center[0], center[1] + rangeDeg] }
2956
+ });
2957
+ }
2958
+ }
2959
+ return {
2960
+ type: "FeatureCollection",
2961
+ features: [...features, ...lineFeatures, ...labelFeatures, ...rangeRingFeatures]
2962
+ };
2963
+ }
2964
+
2965
+ // src/interact/notices.ts
2966
+ var NON_SOLAS_DISCLAIMER = "\u26A0\uFE0F CHRT data is derived from official electronic navigational charts but is NOT FOR PRIMARY NAVIGATION and is NOT SOLAS-compliant. Always use official, type-approved ECDIS or paper charts for navigation safety. CHRT provides situational awareness data only.";
2967
+ var NON_SOLAS_DISCLAIMER_SHORT = "Not for primary navigation";
2968
+ function baseNotice() {
2969
+ return {
2970
+ disclaimer: NON_SOLAS_DISCLAIMER,
2971
+ attribution: "Derived from official Electronic Navigational Charts issued by national hydrographic offices (e.g. NOAA).",
2972
+ dataCurrency: "Tile attributes carry no per-feature edition/update metadata; call inspect({ enrich: true }) to resolve the source cell's edition, update_number and issue_date."
2973
+ };
2974
+ }
2975
+ function enrichedNotice(cell) {
2976
+ const title = cell.title ? ` \u2014 ${cell.title}` : "";
2977
+ return {
2978
+ disclaimer: NON_SOLAS_DISCLAIMER,
2979
+ attribution: `Official ENC data \xA9 ${cell.chart_source} (cell ${cell.cell_id}${title}).`,
2980
+ dataCurrency: `Edition ${cell.edition ?? "unknown"}, update ${cell.update_number ?? "unknown"}, issued ${cell.issue_date ?? "unknown"}.`
2981
+ };
2982
+ }
2983
+
2984
+ // src/interact/errors.ts
2985
+ var InteractError = class extends Error {
2986
+ code;
2987
+ status;
2988
+ /** The team's current plan id, when the server included it (denials). */
2989
+ plan;
2990
+ constructor(code, message, opts) {
2991
+ super(message);
2992
+ this.name = "InteractError";
2993
+ this.code = code;
2994
+ this.status = opts?.status;
2995
+ this.plan = opts?.plan;
2996
+ }
2997
+ };
2998
+
2999
+ // src/interact/inspect.ts
3000
+ var NAV_AID_RAW_LAYERS = new Set(
3001
+ NAV_AID_QUERY_LAYERS.map((id) => stripLayerId(id))
3002
+ );
3003
+ var BRIDGE_RAW_LAYERS = new Set(BRIDGE_QUERY_LAYERS.map((id) => stripLayerId(id)));
3004
+ function stripLayerId(layerId) {
3005
+ return layerId.replace(/^(enc|s52)-/, "").replace(/-(point|line|polygon|midpoint|outline)$/, "").replace(/-(point|line|polygon|midpoint)$/, "");
3006
+ }
3007
+ function resolveLayerId(feature, opts) {
3008
+ if (opts?.layer) {
3009
+ return opts.layer.includes("-") || opts.layer.startsWith("enc") ? opts.layer : `enc-${opts.layer}`;
3010
+ }
3011
+ const layer = feature.layer;
3012
+ if (typeof layer === "string" && layer) {
3013
+ return layer.includes("-") ? layer : `enc-${layer}`;
3014
+ }
3015
+ if (layer && typeof layer === "object" && typeof layer.id === "string" && layer.id) {
3016
+ return layer.id;
3017
+ }
3018
+ if (feature.sourceLayer) return `enc-${feature.sourceLayer}`;
3019
+ throw new InteractError(
3020
+ "unknown_layer",
3021
+ "inspect(): cannot determine the feature's chart layer \u2014 pass { layer } or a feature from queryRenderedFeatures()"
3022
+ );
3023
+ }
3024
+ function resolveCoordinates(feature, opts) {
3025
+ if (opts?.coordinates) return opts.coordinates;
3026
+ const geom = feature.geometry;
3027
+ if (geom && geom.type === "Point" && Array.isArray(geom.coordinates)) {
3028
+ const [lng, lat] = geom.coordinates;
3029
+ if (typeof lng === "number" && typeof lat === "number") return [lng, lat];
3030
+ }
3031
+ return null;
3032
+ }
3033
+ function navAidPropertyRows(decoded) {
3034
+ const rows = [];
3035
+ const push = (label, value) => {
3036
+ if (value == null || value === "") return;
3037
+ rows.push({ label, value: String(value) });
3038
+ };
3039
+ push("Colour", decoded.colour);
3040
+ push("Shape", decoded.shape);
3041
+ push("Topmark", decoded.topmark);
3042
+ push("Topmark meaning", decoded.topmarkMeaning);
3043
+ push("Light", decoded.lightCharacterFull);
3044
+ push("Characteristic", decoded.notation || null);
3045
+ if (decoded.orientation != null) push("Orientation", `${decoded.orientation}\xB0`);
3046
+ push("Fog Signal", decoded.fogSignal);
3047
+ if (decoded.radarReflector) push("Radar", "Reflector");
3048
+ if (decoded.depth != null) push("Depth", `${decoded.depth}m`);
3049
+ push("Status", decoded.status);
3050
+ push("System", decoded.markingSystem);
3051
+ push("Category", decoded.category);
3052
+ return rows;
3053
+ }
3054
+ function bridgePropertyRows(decoded) {
3055
+ const rows = [];
3056
+ const push = (label, value) => {
3057
+ if (value == null || value === "") return;
3058
+ rows.push({ label, value });
3059
+ };
3060
+ push("Category", decoded.categoryLabel);
3061
+ if (decoded.verticalClearanceClosed != null && decoded.verticalClearanceOpen != null) {
3062
+ push(
3063
+ "Vertical clearance",
3064
+ `Closed: ${decoded.verticalClearanceClosed}m / Open: ${decoded.verticalClearanceOpen}m`
3065
+ );
3066
+ } else if (decoded.verticalClearance != null) {
3067
+ push("Vertical clearance", `${decoded.verticalClearance}m`);
3068
+ } else if (decoded.verticalClearanceClosed != null) {
3069
+ push("Vertical clearance", `${decoded.verticalClearanceClosed}m (closed)`);
3070
+ } else if (decoded.verticalClearanceOpen != null) {
3071
+ push("Vertical clearance", `${decoded.verticalClearanceOpen}m (open)`);
3072
+ }
3073
+ if (decoded.horizontalClearance != null) {
3074
+ push("Horizontal clearance", `${decoded.horizontalClearance}m`);
3075
+ }
3076
+ push("Status", decoded.status);
3077
+ push("Condition", decoded.condition);
3078
+ push("National name", decoded.nationalName);
3079
+ push("Source", decoded.sourceIndication);
3080
+ push("Source date", decoded.sourceDate);
3081
+ return rows;
3082
+ }
3083
+ function buildInspectResult(feature, opts) {
3084
+ const layerId = resolveLayerId(feature, opts);
3085
+ const rawLayer = stripLayerId(layerId);
3086
+ const props = { ...feature.properties ?? {} };
3087
+ const coords = resolveCoordinates(feature, opts);
3088
+ const notice = baseNotice();
3089
+ if (BRIDGE_RAW_LAYERS.has(rawLayer)) {
3090
+ const bridge = decodeBridgeProps(props);
3091
+ const result2 = {
3092
+ kind: "bridge",
3093
+ layer: "BRIDGE",
3094
+ layerId,
3095
+ objectClass: bridge.categoryLabel ?? "Bridge",
3096
+ title: bridge.name || bridge.categoryLabel || "Bridge",
3097
+ coordinates: coords,
3098
+ properties: bridgePropertyRows(bridge),
3099
+ raw: props,
3100
+ notice,
3101
+ enrichment: null,
3102
+ bridge
3103
+ };
3104
+ return result2;
3105
+ }
3106
+ if (NAV_AID_RAW_LAYERS.has(rawLayer)) {
3107
+ const navAid = decodeNavAidProps(layerId, props, coords ?? void 0);
3108
+ const flashPattern = buildFlashPattern(navAid);
3109
+ const result2 = {
3110
+ kind: "navaid",
3111
+ layer: navAid.layerName,
3112
+ layerId,
3113
+ objectClass: navAid.type,
3114
+ title: navAid.fullName || navAid.name || navAid.type,
3115
+ coordinates: navAid.coordinates,
3116
+ properties: navAidPropertyRows(navAid),
3117
+ raw: props,
3118
+ notice,
3119
+ enrichment: null,
3120
+ navAid,
3121
+ characteristic: navAid.notation || null,
3122
+ flashPattern
3123
+ };
3124
+ return result2;
3125
+ }
3126
+ const area = decodeAreaFeatureProps(layerId, props);
3127
+ const result = {
3128
+ kind: "area",
3129
+ layer: rawLayer,
3130
+ layerId,
3131
+ objectClass: area.type,
3132
+ title: area.title,
3133
+ coordinates: coords,
3134
+ properties: area.properties,
3135
+ raw: props,
3136
+ notice,
3137
+ enrichment: null,
3138
+ area
3139
+ };
3140
+ return result;
3141
+ }
3142
+
3143
+ // src/interact/attach.ts
3144
+ function bboxAround(point, pad) {
3145
+ return [
3146
+ [point.x - pad, point.y - pad],
3147
+ [point.x + pad, point.y + pad]
3148
+ ];
3149
+ }
3150
+ function existingLayers(map, ids) {
3151
+ return ids.filter((id) => {
3152
+ try {
3153
+ return Boolean(map.getLayer(id));
3154
+ } catch {
3155
+ return false;
3156
+ }
3157
+ });
3158
+ }
3159
+ function query(map, geometry, layers) {
3160
+ if (layers.length === 0) return [];
3161
+ try {
3162
+ return map.queryRenderedFeatures(geometry, { layers }) ?? [];
3163
+ } catch {
3164
+ return [];
3165
+ }
3166
+ }
3167
+ function featureLayerId(f) {
3168
+ const layer = f.layer;
3169
+ if (layer && typeof layer === "object" && typeof layer.id === "string") return layer.id;
3170
+ if (typeof layer === "string") return layer;
3171
+ return f.sourceLayer ? `enc-${f.sourceLayer}` : "";
3172
+ }
3173
+ function decodeClickedNavAid(map, feature, e, navAidLayers) {
3174
+ const layerId = featureLayerId(feature);
3175
+ const props = { ...feature.properties ?? {} };
3176
+ const geom = feature.geometry;
3177
+ const coords = geom?.type === "Point" && Array.isArray(geom.coordinates) ? [geom.coordinates[0], geom.coordinates[1]] : [e.lngLat.lng, e.lngLat.lat];
3178
+ const rawLayer = stripLayerId(layerId);
3179
+ if (!props.OBJNAM && CHILD_LAYERS.has(rawLayer)) {
3180
+ const parentLayers = existingLayers(map, PARENT_LAYERS);
3181
+ const parents = query(map, bboxAround(e.point, 8), parentLayers);
3182
+ for (const p of parents) {
3183
+ const pName = p.properties?.OBJNAM;
3184
+ if (pName) {
3185
+ props.OBJNAM = pName;
3186
+ if (!props.COLOUR && p.properties?.COLOUR) {
3187
+ props.COLOUR = p.properties.COLOUR;
3188
+ }
3189
+ break;
3190
+ }
3191
+ }
3192
+ }
3193
+ if (!props.CATFOG) {
3194
+ const fogFeats = query(map, bboxAround(e.point, 10), existingLayers(map, ["enc-FOGSIG"]));
3195
+ for (const f of fogFeats) {
3196
+ if (f.properties?.CATFOG) {
3197
+ props.CATFOG = f.properties.CATFOG;
3198
+ break;
3199
+ }
3200
+ }
3201
+ }
3202
+ if (!props.RETRFL) {
3203
+ const radarFeats = query(map, bboxAround(e.point, 10), existingLayers(map, ["enc-RTPBCN"]));
3204
+ if (radarFeats.length > 0) props.RETRFL = "1";
3205
+ }
3206
+ if (!props.TOPSHP) {
3207
+ const topmarFeats = query(map, bboxAround(e.point, 10), existingLayers(map, ["enc-TOPMAR"]));
3208
+ for (const f of topmarFeats) {
3209
+ if (f.properties?.TOPSHP) {
3210
+ props.TOPSHP = f.properties.TOPSHP;
3211
+ break;
3212
+ }
3213
+ }
3214
+ }
3215
+ const lightLayerIds = existingLayers(
3216
+ map,
3217
+ navAidLayers.filter((id) => id === "enc-LIGHTS" || id === "enc-LIGHTS-point")
3218
+ );
3219
+ let coLocatedLightFeats = null;
3220
+ const isLightsLayer = layerId.includes("LIGHTS");
3221
+ if (!props.LITCHR) {
3222
+ const lightFeats = query(map, bboxAround(e.point, 10), lightLayerIds);
3223
+ if (lightFeats.length > 0) {
3224
+ coLocatedLightFeats = lightFeats;
3225
+ let primaryLight = null;
3226
+ for (const lf of lightFeats) {
3227
+ if (lf.properties?.LITCHR) {
3228
+ if (!lf.properties?.SECTR1 && !lf.properties?.SECTR2) {
3229
+ primaryLight = lf;
3230
+ break;
3231
+ }
3232
+ if (!primaryLight) primaryLight = lf;
3233
+ }
3234
+ }
3235
+ if (primaryLight?.properties) {
3236
+ props.LITCHR = primaryLight.properties.LITCHR;
3237
+ if (!props.SIGPER && primaryLight.properties.SIGPER) props.SIGPER = primaryLight.properties.SIGPER;
3238
+ if (!props.SIGGRP && primaryLight.properties.SIGGRP) props.SIGGRP = primaryLight.properties.SIGGRP;
3239
+ if (primaryLight.properties.COLOUR) props.LIGHT_COLOUR = primaryLight.properties.COLOUR;
3240
+ if (primaryLight.properties.CATLIT) props.CATLIT = primaryLight.properties.CATLIT;
3241
+ }
3242
+ }
3243
+ }
3244
+ const result = buildInspectResult(
3245
+ { ...feature, properties: props },
3246
+ { layer: layerId, coordinates: coords }
3247
+ );
3248
+ if (result.kind === "navaid") {
3249
+ let sectorSources = null;
3250
+ if (isLightsLayer) {
3251
+ const nearby = query(map, bboxAround(e.point, 12), lightLayerIds);
3252
+ if (nearby.length > 1) sectorSources = nearby;
3253
+ } else if (coLocatedLightFeats && coLocatedLightFeats.length > 0) {
3254
+ sectorSources = coLocatedLightFeats;
3255
+ }
3256
+ if (sectorSources) {
3257
+ const sectors = buildSectorsFromFeatures(
3258
+ sectorSources.map((f) => ({ properties: f.properties ?? {} }))
3259
+ );
3260
+ if (sectors.length > 0) {
3261
+ result.navAid.sectors = sectors;
3262
+ result.navAid.isSectorLight = true;
3263
+ }
3264
+ }
3265
+ }
3266
+ return result;
3267
+ }
3268
+ function decodeClick(map, e, opts) {
3269
+ const navAidLayers = opts.navAidLayers ?? NAV_AID_QUERY_LAYERS;
3270
+ const bridgeLayers = opts.bridgeLayers ?? BRIDGE_QUERY_LAYERS;
3271
+ const areaLayers = opts.areaLayers ?? AREA_QUERY_LAYERS;
3272
+ const navFeatures = query(map, e.point, existingLayers(map, navAidLayers));
3273
+ if (navFeatures.length > 0) {
3274
+ const feature = navFeatures[0];
3275
+ return { result: decodeClickedNavAid(map, feature, e, navAidLayers), feature };
3276
+ }
3277
+ const bridgeFeatures = query(map, e.point, existingLayers(map, bridgeLayers));
3278
+ if (bridgeFeatures.length > 0) {
3279
+ const feature = bridgeFeatures[0];
3280
+ const result = buildInspectResult(feature, {
3281
+ layer: featureLayerId(feature) || "enc-BRIDGE",
3282
+ coordinates: [e.lngLat.lng, e.lngLat.lat]
3283
+ });
3284
+ return { result, feature };
3285
+ }
3286
+ const areaFeatures = query(map, e.point, existingLayers(map, areaLayers));
3287
+ if (areaFeatures.length > 0) {
3288
+ const feature = areaFeatures[0];
3289
+ const result = buildInspectResult(feature, {
3290
+ layer: featureLayerId(feature),
3291
+ coordinates: [e.lngLat.lng, e.lngLat.lat]
3292
+ });
3293
+ return { result, feature };
3294
+ }
3295
+ return null;
3296
+ }
3297
+
3298
+ // src/interact/entitlement.ts
3299
+ function mapDenialCode(raw, status) {
3300
+ if (raw === "interact_not_enabled") return "interact_not_enabled";
3301
+ if (status === 401) return "invalid_api_key";
3302
+ if (status === 403) return "missing_scope";
3303
+ return "unexpected_response";
3304
+ }
3305
+ var EntitlementClient = class {
3306
+ constructor(http) {
3307
+ this.http = http;
3308
+ }
3309
+ cached = null;
3310
+ pending = null;
3311
+ /**
3312
+ * Check (and cache) the interact entitlement for this client's API key.
3313
+ * Concurrent callers share one in-flight request; a denial is NOT cached so
3314
+ * a plan upgrade takes effect on the next call.
3315
+ */
3316
+ async check() {
3317
+ if (this.cached) return this.cached;
3318
+ if (this.pending) return this.pending;
3319
+ this.pending = (async () => {
3320
+ try {
3321
+ const result = await this.checkOnce();
3322
+ this.cached = result;
3323
+ return result;
3324
+ } finally {
3325
+ this.pending = null;
3326
+ }
3327
+ })();
3328
+ return this.pending;
3329
+ }
3330
+ async checkOnce() {
3331
+ let res;
3332
+ try {
3333
+ res = await this.http.postJson("/interact/entitlement", {});
3334
+ } catch (err) {
3335
+ throw new InteractError(
3336
+ "network_error",
3337
+ err instanceof Error ? err.message : "entitlement request failed"
3338
+ );
3339
+ }
3340
+ if (!res.ok) {
3341
+ const code = mapDenialCode(res.body?.error, res.status);
3342
+ const message = typeof res.body?.message === "string" ? res.body.message : code === "interact_not_enabled" ? "CHRTinteract requires the Dev plan or above. Upgrade at https://chrt.co/pricing." : `entitlement check failed (HTTP ${res.status})`;
3343
+ const plan = typeof res.body?.plan === "string" ? res.body.plan : void 0;
3344
+ throw new InteractError(code, message, { status: res.status, plan });
3345
+ }
3346
+ if (res.body?.enabled !== true) {
3347
+ throw new InteractError(
3348
+ "unexpected_response",
3349
+ "entitlement endpoint returned a malformed body",
3350
+ { status: res.status }
3351
+ );
3352
+ }
3353
+ return {
3354
+ enabled: true,
3355
+ ...typeof res.body.plan === "string" ? { plan: res.body.plan } : {}
3356
+ };
3357
+ }
3358
+ /** Drop the cached grant (e.g. after an API-key swap in tests). */
3359
+ reset() {
3360
+ this.cached = null;
3361
+ this.pending = null;
3362
+ }
3363
+ };
3364
+
3365
+ // src/interact/enrich.ts
3366
+ var ENRICH_BBOX_PAD_DEG = 1e-3;
3367
+ var ENRICH_LIMIT = 5;
3368
+ function featurePoint(f) {
3369
+ if (f.geometry?.type === "Point" && Array.isArray(f.geometry.coordinates)) {
3370
+ const [lng, lat] = f.geometry.coordinates;
3371
+ if (typeof lng === "number" && typeof lat === "number") return [lng, lat];
3372
+ }
3373
+ return null;
3374
+ }
3375
+ function distSq(a, b) {
3376
+ return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2;
3377
+ }
3378
+ function matchEnrichedFeature(candidates, raw, coords) {
3379
+ if (candidates.length === 0) return null;
3380
+ const objnam = typeof raw.OBJNAM === "string" && raw.OBJNAM ? raw.OBJNAM : null;
3381
+ if (objnam) {
3382
+ const byName = candidates.find(
3383
+ (f) => f.properties?.OBJNAM === objnam
3384
+ );
3385
+ if (byName) return byName;
3386
+ }
3387
+ if (coords) {
3388
+ let best = null;
3389
+ let bestD = Infinity;
3390
+ for (const f of candidates) {
3391
+ const pt = featurePoint(f);
3392
+ if (!pt) continue;
3393
+ const d = distSq(pt, coords);
3394
+ if (d < bestD) {
3395
+ bestD = d;
3396
+ best = f;
3397
+ }
3398
+ }
3399
+ if (best) return best;
3400
+ }
3401
+ return candidates[0];
3402
+ }
3403
+ async function enrichResult(http, result, signal) {
3404
+ const coords = result.coordinates;
3405
+ if (!coords) {
3406
+ throw new InteractError(
3407
+ "enrich_failed",
3408
+ "enrich: the feature has no coordinates to look up \u2014 pass { coordinates } to inspect()"
3409
+ );
3410
+ }
3411
+ const [lng, lat] = coords;
3412
+ const bbox = [
3413
+ lng - ENRICH_BBOX_PAD_DEG,
3414
+ lat - ENRICH_BBOX_PAD_DEG,
3415
+ lng + ENRICH_BBOX_PAD_DEG,
3416
+ lat + ENRICH_BBOX_PAD_DEG
3417
+ ].join(",");
3418
+ let fc;
3419
+ try {
3420
+ fc = await http.request(
3421
+ "/features",
3422
+ { bbox, layer: result.layer, limit: ENRICH_LIMIT },
3423
+ signal
3424
+ );
3425
+ } catch (err) {
3426
+ if (err instanceof ChrtError) {
3427
+ throw new InteractError("enrich_failed", `features lookup failed: ${err.message}`, {
3428
+ status: err.status
3429
+ });
3430
+ }
3431
+ throw new InteractError(
3432
+ "network_error",
3433
+ err instanceof Error ? err.message : "features lookup failed"
3434
+ );
3435
+ }
3436
+ const matched = matchEnrichedFeature(fc.features ?? [], result.raw, coords);
3437
+ const enrichment = {
3438
+ matched: matched != null,
3439
+ properties: matched ? matched.properties : null,
3440
+ cellId: null,
3441
+ cell: null
3442
+ };
3443
+ if (matched) {
3444
+ const cellId = matched.properties?.cell_id;
3445
+ if (typeof cellId === "string" && cellId) {
3446
+ enrichment.cellId = cellId;
3447
+ try {
3448
+ const cell = await http.request(
3449
+ `/cells/${encodeURIComponent(cellId)}`,
3450
+ void 0,
3451
+ signal
3452
+ );
3453
+ enrichment.cell = cell;
3454
+ result.notice = enrichedNotice(cell);
3455
+ } catch {
3456
+ }
3457
+ }
3458
+ }
3459
+ result.enrichment = enrichment;
3460
+ return result;
3461
+ }
3462
+
3463
+ // src/interact/popup.ts
3464
+ var POPUP_CLASS = "chrt-interact-popup";
3465
+ var POPUP_CSS = `
3466
+ .${POPUP_CLASS} {
3467
+ position: absolute;
3468
+ z-index: 40;
3469
+ min-width: 220px;
3470
+ max-width: 320px;
3471
+ transform: translate(-50%, calc(-100% - 12px));
3472
+ background: var(--chrt-interact-bg, #0b1524);
3473
+ color: var(--chrt-interact-fg, #e2e8f0);
3474
+ border: 1px solid var(--chrt-interact-border, rgba(148, 163, 184, 0.25));
3475
+ border-radius: 8px;
3476
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
3477
+ font: 12px/1.45 var(--chrt-interact-font, ui-sans-serif, system-ui, sans-serif);
3478
+ pointer-events: auto;
3479
+ }
3480
+ .${POPUP_CLASS} * { box-sizing: border-box; }
3481
+ .${POPUP_CLASS}__inner { padding: 10px 12px 8px; }
3482
+ .${POPUP_CLASS}__close {
3483
+ position: absolute; top: 4px; right: 6px;
3484
+ border: 0; background: none; color: inherit; opacity: 0.55;
3485
+ font-size: 14px; line-height: 1; cursor: pointer; padding: 4px;
3486
+ }
3487
+ .${POPUP_CLASS}__close:hover { opacity: 1; }
3488
+ .${POPUP_CLASS}__title { font-weight: 600; font-size: 13px; padding-right: 16px; }
3489
+ .${POPUP_CLASS}__subtitle { color: var(--chrt-interact-muted, #94a3b8); margin-top: 1px; }
3490
+ .${POPUP_CLASS}__badge {
3491
+ display: inline-flex; align-items: center; gap: 6px;
3492
+ margin-top: 6px; padding: 2px 8px;
3493
+ border: 1px solid var(--chrt-interact-border, rgba(148, 163, 184, 0.25));
3494
+ border-radius: 4px;
3495
+ font-family: var(--chrt-interact-mono, ui-monospace, monospace);
3496
+ font-weight: 600; font-size: 11px;
3497
+ }
3498
+ .${POPUP_CLASS}__dot {
3499
+ width: 10px; height: 10px; border-radius: 50%;
3500
+ background: #555; opacity: 0.15;
3501
+ }
3502
+ .${POPUP_CLASS}__rows { margin-top: 8px; border-top: 1px solid var(--chrt-interact-border, rgba(148,163,184,0.18)); padding-top: 6px; }
3503
+ .${POPUP_CLASS}__row { display: flex; justify-content: space-between; gap: 12px; padding: 1px 0; }
3504
+ .${POPUP_CLASS}__label { color: var(--chrt-interact-muted, #94a3b8); flex-shrink: 0; }
3505
+ .${POPUP_CLASS}__value { text-align: right; font-weight: 500; }
3506
+ .${POPUP_CLASS}__info { margin-top: 4px; font-style: italic; color: var(--chrt-interact-muted, #94a3b8); font-size: 11px; }
3507
+ .${POPUP_CLASS}__footer {
3508
+ margin-top: 8px; padding: 6px 12px 8px;
3509
+ border-top: 1px solid var(--chrt-interact-border, rgba(148,163,184,0.18));
3510
+ color: var(--chrt-interact-muted, #94a3b8);
3511
+ font-size: 10px;
3512
+ }
3513
+ .${POPUP_CLASS}__footer strong { color: var(--chrt-interact-warn, #fbbf24); font-weight: 600; }
3514
+ `;
3515
+ var styleInjected = false;
3516
+ function ensureStyles(doc) {
3517
+ if (styleInjected && doc.getElementById(`${POPUP_CLASS}-style`)) return;
3518
+ const style = doc.createElement("style");
3519
+ style.id = `${POPUP_CLASS}-style`;
3520
+ style.textContent = POPUP_CSS;
3521
+ doc.head.appendChild(style);
3522
+ styleInjected = true;
3523
+ }
3524
+ function el(doc, tag, className, text) {
3525
+ const node = doc.createElement(tag);
3526
+ node.className = className;
3527
+ if (text != null) node.textContent = text;
3528
+ return node;
3529
+ }
3530
+ function animateDot(dot, pattern) {
3531
+ if (pattern.totalCycle <= 0 || typeof requestAnimationFrame === "undefined") {
3532
+ return () => void 0;
3533
+ }
3534
+ let raf = 0;
3535
+ const start = performance.now();
3536
+ const tick = (now2) => {
3537
+ const elapsed = (now2 - start) % pattern.totalCycle;
3538
+ let acc = 0;
3539
+ let phase = pattern.phases[0];
3540
+ for (const p of pattern.phases) {
3541
+ acc += p.duration;
3542
+ if (elapsed < acc) {
3543
+ phase = p;
3544
+ break;
3545
+ }
3546
+ }
3547
+ if (phase.on) {
3548
+ const color = phase.color || "#ffffff";
3549
+ dot.style.opacity = "1";
3550
+ dot.style.backgroundColor = color;
3551
+ dot.style.boxShadow = `0 0 8px 2px ${color}88`;
3552
+ } else {
3553
+ dot.style.opacity = "0.15";
3554
+ dot.style.backgroundColor = "#555";
3555
+ dot.style.boxShadow = "none";
3556
+ }
3557
+ raf = requestAnimationFrame(tick);
3558
+ };
3559
+ raf = requestAnimationFrame(tick);
3560
+ return () => cancelAnimationFrame(raf);
3561
+ }
3562
+ var InteractPopup = class {
3563
+ constructor(map) {
3564
+ this.map = map;
3565
+ }
3566
+ root = null;
3567
+ stopAnimation = null;
3568
+ lngLat = null;
3569
+ reposition = () => this.place();
3570
+ show(result, lngLat) {
3571
+ const container = this.map.getContainer();
3572
+ const doc = container.ownerDocument;
3573
+ ensureStyles(doc);
3574
+ this.close();
3575
+ const root = el(doc, "div", POPUP_CLASS);
3576
+ const inner = el(doc, "div", `${POPUP_CLASS}__inner`);
3577
+ const close = el(doc, "button", `${POPUP_CLASS}__close`, "\u2715");
3578
+ close.setAttribute("aria-label", "Close");
3579
+ close.addEventListener("click", () => this.close());
3580
+ root.appendChild(close);
3581
+ inner.appendChild(el(doc, "div", `${POPUP_CLASS}__title`, result.title));
3582
+ const subtitleParts = [result.objectClass];
3583
+ if (result.kind === "navaid") {
3584
+ if (result.navAid.shape) subtitleParts.push(result.navAid.shape);
3585
+ if (result.navAid.isSectorLight) subtitleParts.push("Sector");
3586
+ if (result.navAid.isDirectional) subtitleParts.push("Directional");
3587
+ }
3588
+ inner.appendChild(
3589
+ el(doc, "div", `${POPUP_CLASS}__subtitle`, subtitleParts.filter(Boolean).join(" \xB7 "))
3590
+ );
3591
+ if (result.kind === "navaid" && result.characteristic) {
3592
+ const badge = el(doc, "div", `${POPUP_CLASS}__badge`);
3593
+ const dot = el(doc, "span", `${POPUP_CLASS}__dot`);
3594
+ badge.appendChild(dot);
3595
+ badge.appendChild(doc.createTextNode(result.characteristic));
3596
+ if (result.navAid.lightColourHex && result.navAid.lightColourHex !== "#ffffff") {
3597
+ badge.style.color = result.navAid.lightColourHex;
3598
+ }
3599
+ inner.appendChild(badge);
3600
+ if (result.flashPattern) {
3601
+ this.stopAnimation = animateDot(dot, result.flashPattern);
3602
+ }
3603
+ }
3604
+ if (result.properties.length > 0) {
3605
+ const rows = el(doc, "div", `${POPUP_CLASS}__rows`);
3606
+ for (const p of result.properties) {
3607
+ const row = el(doc, "div", `${POPUP_CLASS}__row`);
3608
+ row.appendChild(el(doc, "span", `${POPUP_CLASS}__label`, p.label));
3609
+ row.appendChild(el(doc, "span", `${POPUP_CLASS}__value`, p.value));
3610
+ rows.appendChild(row);
3611
+ }
3612
+ inner.appendChild(rows);
3613
+ }
3614
+ if (result.kind === "navaid" && result.navAid.information) {
3615
+ inner.appendChild(el(doc, "div", `${POPUP_CLASS}__info`, result.navAid.information));
3616
+ }
3617
+ root.appendChild(inner);
3618
+ const footer = el(doc, "div", `${POPUP_CLASS}__footer`);
3619
+ footer.title = `${NON_SOLAS_DISCLAIMER}
3620
+
3621
+ ${result.notice.attribution}
3622
+ ${result.notice.dataCurrency}`;
3623
+ const warn = el(doc, "strong", "", `\u26A0 ${NON_SOLAS_DISCLAIMER_SHORT}`);
3624
+ footer.appendChild(warn);
3625
+ footer.appendChild(doc.createElement("br"));
3626
+ footer.appendChild(doc.createTextNode(result.notice.attribution));
3627
+ const currency = result.notice.dataCurrency;
3628
+ if (currency.startsWith("Edition")) {
3629
+ footer.appendChild(doc.createElement("br"));
3630
+ footer.appendChild(doc.createTextNode(currency));
3631
+ }
3632
+ root.appendChild(footer);
3633
+ const position = typeof getComputedStyle !== "undefined" ? getComputedStyle(container).position : container.style.position;
3634
+ if (position === "static" || position === "") {
3635
+ container.style.position = "relative";
3636
+ }
3637
+ container.appendChild(root);
3638
+ this.root = root;
3639
+ this.lngLat = lngLat;
3640
+ this.place();
3641
+ this.map.on("move", this.reposition);
3642
+ }
3643
+ place() {
3644
+ if (!this.root || !this.lngLat) return;
3645
+ const { x, y } = this.map.project(this.lngLat);
3646
+ this.root.style.left = `${x}px`;
3647
+ this.root.style.top = `${y}px`;
3648
+ }
3649
+ close() {
3650
+ if (this.stopAnimation) {
3651
+ this.stopAnimation();
3652
+ this.stopAnimation = null;
3653
+ }
3654
+ if (this.root) {
3655
+ this.map.off("move", this.reposition);
3656
+ this.root.remove();
3657
+ this.root = null;
3658
+ }
3659
+ this.lngLat = null;
3660
+ }
3661
+ };
3662
+
3663
+ // src/interact/index.ts
3664
+ var InteractClient = class {
3665
+ constructor(http) {
3666
+ this.http = http;
3667
+ this.entitlementClient = new EntitlementClient(http);
3668
+ }
3669
+ entitlementClient;
3670
+ /**
3671
+ * Check the CHRTinteract entitlement for this API key (server-side, plan
3672
+ * derived, cached per client). Throws `InteractError` with code
3673
+ * `interact_not_enabled` for Free-plan teams.
3674
+ */
3675
+ async entitlement() {
3676
+ return this.entitlementClient.check();
3677
+ }
3678
+ /**
3679
+ * Decode a chart feature (from `queryRenderedFeatures`, the features API,
3680
+ * or plain `{ layer, properties }`) into a typed {@link InspectResult}.
3681
+ *
3682
+ * The first call performs the server-side entitlement check. With
3683
+ * `{ enrich: true }` the result is upgraded with full S-57 properties and
3684
+ * source-cell provenance (edition / update / issue date) via the features
3685
+ * API — metered against the key's `featureCalls` quota.
3686
+ *
3687
+ * @example
3688
+ * const result = await chrt.interact.inspect(feature);
3689
+ * if (result.kind === "navaid") console.log(result.characteristic); // "Fl R 4s"
3690
+ */
3691
+ async inspect(feature, opts) {
3692
+ await this.entitlementClient.check();
3693
+ const result = buildInspectResult(feature, opts);
3694
+ if (opts?.enrich) {
3695
+ return enrichResult(this.http, result, opts.signal);
3696
+ }
3697
+ return result;
3698
+ }
3699
+ /**
3700
+ * Attach click-to-inspect to a MapLibre / Mapbox GL map rendering CHRT
3701
+ * tiles. Resolves once the server-side entitlement check passes; rejects
3702
+ * with `InteractError` (`interact_not_enabled` on Free plans) otherwise —
3703
+ * no listeners are wired on denial.
3704
+ *
3705
+ * Headless-first: pass `onInspect` to receive every decoded
3706
+ * {@link InspectResult} and render your own UI. Without `onInspect`, the
3707
+ * built-in minimal popup is shown (opt back in explicitly with
3708
+ * `popup: true` to have both).
3709
+ *
3710
+ * @example
3711
+ * const handle = await chrt.interact.attach(map, {
3712
+ * onInspect: (result) => console.log(result.title, result.characteristic),
3713
+ * });
3714
+ * // later: handle.detach();
3715
+ */
3716
+ async attach(map, opts = {}) {
3717
+ if (!map || typeof map.queryRenderedFeatures !== "function" || typeof map.on !== "function") {
3718
+ throw new InteractError(
3719
+ "unexpected_response",
3720
+ "attach(): expected a MapLibre / Mapbox GL map instance"
1266
3721
  );
1267
3722
  }
1268
- await fs.rename(partialPath, path);
1269
- await this.persistManifestEntry(region, bundle, path, fs);
1270
- return {
1271
- path,
1272
- bytes: bundle.bytes,
1273
- sha256: bundle.sha256,
1274
- build_id: bundle.build_id,
1275
- build_tag: bundle.build_tag,
1276
- resumed
3723
+ await this.entitlementClient.check();
3724
+ const showPopup = opts.popup ?? !opts.onInspect;
3725
+ const popup = showPopup ? new InteractPopup(map) : null;
3726
+ const clickHandler = (e) => {
3727
+ const hit = decodeClick(map, e, opts);
3728
+ if (!hit) {
3729
+ popup?.close();
3730
+ opts.onMiss?.({ lngLat: e.lngLat });
3731
+ return;
3732
+ }
3733
+ const finish = (result) => {
3734
+ popup?.show(result, result.coordinates ?? [e.lngLat.lng, e.lngLat.lat]);
3735
+ opts.onInspect?.(result, { lngLat: e.lngLat, point: e.point, feature: hit.feature });
3736
+ };
3737
+ if (opts.enrich) {
3738
+ enrichResult(this.http, hit.result).then(finish).catch(() => finish(hit.result));
3739
+ } else {
3740
+ finish(hit.result);
3741
+ }
1277
3742
  };
1278
- }
1279
- async persistManifestEntry(region, bundle, path, fs) {
1280
- if (!this.manifestPath) return;
1281
- const manifest = await readManifest(this.manifestPath, fs);
1282
- const nowSec = Math.floor(Date.now() / 1e3);
1283
- manifest.regions[region] = {
1284
- path,
1285
- bytes: bundle.bytes,
1286
- sha256: bundle.sha256,
1287
- build_id: bundle.build_id,
1288
- build_tag: bundle.build_tag,
1289
- downloaded_at: nowSec,
1290
- last_accessed_at: nowSec
3743
+ map.on("click", clickHandler);
3744
+ return {
3745
+ detach: () => {
3746
+ map.off("click", clickHandler);
3747
+ popup?.close();
3748
+ }
1291
3749
  };
1292
- await writeManifest(this.manifestPath, fs, manifest);
1293
3750
  }
1294
3751
  };
1295
- function dirname(p) {
1296
- const i = p.lastIndexOf("/");
1297
- const j = p.lastIndexOf("\\");
1298
- const k = Math.max(i, j);
1299
- if (k <= 0) return ".";
1300
- return p.slice(0, k);
1301
- }
1302
3752
 
1303
3753
  // src/atlas/s52-colors.ts
1304
3754
  var S52_DAY = {
@@ -7022,6 +9472,118 @@ var LABEL_ANCHOR_FILTER = [
7022
9472
  ["get", "_label_anchor"],
7023
9473
  false
7024
9474
  ];
9475
+ var LABEL_PRIORITY = {
9476
+ LIGHTS: 100,
9477
+ UWTROC: 100,
9478
+ OBSTRN: 100,
9479
+ WRECKS: 100,
9480
+ CBLOHD: 100,
9481
+ PIPOHD: 100,
9482
+ BOYLAT: 200,
9483
+ BOYCAR: 200,
9484
+ BOYSPP: 200,
9485
+ BOYISD: 200,
9486
+ BOYSAW: 200,
9487
+ BCNLAT: 200,
9488
+ BCNCAR: 200,
9489
+ BCNSPP: 200,
9490
+ FOGSIG: 250,
9491
+ SOUNDG: 300,
9492
+ DEPCNT: 300,
9493
+ BRIDGE: 400,
9494
+ LNDMRK: 400,
9495
+ PILPNT: 400,
9496
+ MORFAC: 400,
9497
+ SLCONS: 400,
9498
+ BUISGL: 400,
9499
+ SBDARE: 500,
9500
+ MAGVAR: 500,
9501
+ WEDKLP: 500
9502
+ };
9503
+ var DEFAULT_LABEL_PRIORITY = 450;
9504
+ var ORIENTATION_LABEL_PRIORITY = 900;
9505
+ function getLabelPriority(layerName) {
9506
+ if (getLabelBehavior(layerName) === "orientation-cross-band") {
9507
+ return ORIENTATION_LABEL_PRIORITY;
9508
+ }
9509
+ return LABEL_PRIORITY[layerName] ?? DEFAULT_LABEL_PRIORITY;
9510
+ }
9511
+ function labelSortKey(rank, within) {
9512
+ if (within === void 0) return rank * 1e3;
9513
+ return [
9514
+ "+",
9515
+ rank * 1e3,
9516
+ ["min", 999, ["max", 0, within]]
9517
+ ];
9518
+ }
9519
+ var NAV_DESIGNATION_STOPWORDS = [
9520
+ "Buoy",
9521
+ "Can",
9522
+ "Nun",
9523
+ "Bell",
9524
+ "Gong",
9525
+ "Mark"
9526
+ ];
9527
+ function extractNavAidDesignation(objnam) {
9528
+ const trimmed = objnam.trim();
9529
+ if (/^[A-Za-z0-9]{1,4}$/.test(trimmed)) {
9530
+ return NAV_DESIGNATION_STOPWORDS.some((w) => w === trimmed) ? null : trimmed;
9531
+ }
9532
+ const m = trimmed.match(/ ([A-Za-z0-9]{1,4})$/);
9533
+ if (!m) return null;
9534
+ const token = m[1];
9535
+ if (NAV_DESIGNATION_STOPWORDS.some((w) => w === token)) return null;
9536
+ return token;
9537
+ }
9538
+ function navAidDesignationTextExpression(objnam, colourPrefix) {
9539
+ const len = ["length", objnam];
9540
+ const tokenBranches = [];
9541
+ for (let k = 1; k <= 4; k++) {
9542
+ tokenBranches.push(
9543
+ [
9544
+ "all",
9545
+ [">", len, k],
9546
+ ["==", ["slice", objnam, ["-", len, k + 1], ["-", len, k]], " "]
9547
+ ],
9548
+ ["slice", objnam, ["-", len, k]]
9549
+ );
9550
+ }
9551
+ return [
9552
+ "let",
9553
+ "desig",
9554
+ ["case", ...tokenBranches, ""],
9555
+ [
9556
+ "case",
9557
+ [
9558
+ "any",
9559
+ ["==", ["var", "desig"], ""],
9560
+ ["match", ["var", "desig"], [...NAV_DESIGNATION_STOPWORDS], true, false]
9561
+ ],
9562
+ ["concat", colourPrefix, objnam],
9563
+ ["concat", colourPrefix, '"', ["var", "desig"], '"']
9564
+ ]
9565
+ ];
9566
+ }
9567
+ function navAidLabelLayout(sortKey) {
9568
+ return {
9569
+ "text-allow-overlap": false,
9570
+ "text-ignore-placement": false,
9571
+ "text-optional": true,
9572
+ "text-padding": 4,
9573
+ "symbol-sort-key": sortKey
9574
+ };
9575
+ }
9576
+ var NAV_AID_SORT_KEY = labelSortKey(200, [
9577
+ "case",
9578
+ ["has", "VALNMR"],
9579
+ ["-", 300, ["min", 299, ["to-number", ["get", "VALNMR"], 0]]],
9580
+ ["+", 400, ["min", 500, ["length", ["coalesce", ["get", "OBJNAM"], ""]]]]
9581
+ ]);
9582
+ var LIGHT_SORT_KEY = labelSortKey(100, [
9583
+ "-",
9584
+ 300,
9585
+ ["min", 299, ["to-number", ["coalesce", ["get", "VALNMR"], 0], 0]]
9586
+ ]);
7025
9587
  function getLabelConfig(layerName, opts = {}) {
7026
9588
  const ct = opts.colorTable ?? "DAY";
7027
9589
  switch (layerName) {
@@ -7089,14 +9651,15 @@ function getLabelConfig(layerName, opts = {}) {
7089
9651
  // sizes — established cartographic typography practice.
7090
9652
  "text-letter-spacing": 0.02,
7091
9653
  // Shallowest-wins: Mapbox GL renders lower sort keys first and
7092
- // gives them collision priority. Using depth directly ensures
7093
- // the shallowest sounding in a cluster is always the one shown,
7094
- // even when labels are hidden due to overlap.
7095
- "symbol-sort-key": [
9654
+ // gives them collision priority. Depth as the within-class key
9655
+ // ensures the shallowest sounding in a cluster is always the
9656
+ // one shown, even when labels are hidden due to overlap. Folded
9657
+ // into the rank-300 band of the LABEL_PRIORITY ladder.
9658
+ "symbol-sort-key": labelSortKey(300, [
7096
9659
  "to-number",
7097
9660
  ["coalesce", ["get", "depth"], ["get", "DEPTH"], ["get", "VALSOU"]],
7098
9661
  999
7099
- ]
9662
+ ])
7100
9663
  }
7101
9664
  };
7102
9665
  }
@@ -7136,8 +9699,14 @@ function getLabelConfig(layerName, opts = {}) {
7136
9699
  textColor: DARK_BG,
7137
9700
  textHaloColor: "#d4c9b0",
7138
9701
  textHaloWidth: 1.5,
7139
- textOffset: [0, 1.2],
7140
- minzoom: 9
9702
+ textOffset: [0, 0],
9703
+ minzoom: 9,
9704
+ layout: {
9705
+ "text-variable-anchor": ["top", "bottom", "right", "left"],
9706
+ "text-radial-offset": 1.2,
9707
+ "text-justify": "auto",
9708
+ "symbol-sort-key": labelSortKey(400, ["length", ["coalesce", ["get", "OBJNAM"], ""]])
9709
+ }
7141
9710
  };
7142
9711
  case "BOYLAT":
7143
9712
  return {
@@ -7149,7 +9718,8 @@ function getLabelConfig(layerName, opts = {}) {
7149
9718
  textHaloColor: navHalo("WHITE", ct),
7150
9719
  textHaloWidth: 1.5,
7151
9720
  textOffset: [0, 1.5],
7152
- minzoom: 10
9721
+ minzoom: 10,
9722
+ layout: navAidLabelLayout(NAV_AID_SORT_KEY)
7153
9723
  };
7154
9724
  case "BCNLAT":
7155
9725
  return {
@@ -7161,7 +9731,8 @@ function getLabelConfig(layerName, opts = {}) {
7161
9731
  textHaloColor: navHalo("WHITE", ct),
7162
9732
  textHaloWidth: 1.5,
7163
9733
  textOffset: [0, 1.5],
7164
- minzoom: 10
9734
+ minzoom: 10,
9735
+ layout: navAidLabelLayout(NAV_AID_SORT_KEY)
7165
9736
  };
7166
9737
  case "BOYCAR":
7167
9738
  case "BCNCAR":
@@ -7174,7 +9745,8 @@ function getLabelConfig(layerName, opts = {}) {
7174
9745
  textHaloColor: navHalo("CREAM", ct),
7175
9746
  textHaloWidth: 1.5,
7176
9747
  textOffset: [0, 1.5],
7177
- minzoom: 10
9748
+ minzoom: 10,
9749
+ layout: navAidLabelLayout(NAV_AID_SORT_KEY)
7178
9750
  };
7179
9751
  case "BOYSPP":
7180
9752
  case "BCNSPP":
@@ -7187,7 +9759,8 @@ function getLabelConfig(layerName, opts = {}) {
7187
9759
  textHaloColor: navHalo("YELLOW", ct),
7188
9760
  textHaloWidth: 1.5,
7189
9761
  textOffset: [0, 1.5],
7190
- minzoom: 10
9762
+ minzoom: 10,
9763
+ layout: navAidLabelLayout(NAV_AID_SORT_KEY)
7191
9764
  };
7192
9765
  case "BOYISD":
7193
9766
  case "BOYSAW":
@@ -7200,7 +9773,8 @@ function getLabelConfig(layerName, opts = {}) {
7200
9773
  textHaloColor: navHalo("WHITE", ct),
7201
9774
  textHaloWidth: 1.5,
7202
9775
  textOffset: [0, 1.5],
7203
- minzoom: 10
9776
+ minzoom: 10,
9777
+ layout: navAidLabelLayout(NAV_AID_SORT_KEY)
7204
9778
  };
7205
9779
  case "LIGHTS":
7206
9780
  return {
@@ -7214,7 +9788,12 @@ function getLabelConfig(layerName, opts = {}) {
7214
9788
  textOffset: [2, 0],
7215
9789
  minzoom: 9,
7216
9790
  layout: {
7217
- "text-anchor": "left"
9791
+ "text-anchor": "left",
9792
+ "text-allow-overlap": false,
9793
+ "text-ignore-placement": false,
9794
+ "text-optional": true,
9795
+ "text-padding": 2,
9796
+ "symbol-sort-key": LIGHT_SORT_KEY
7218
9797
  }
7219
9798
  };
7220
9799
  case "FOGSIG": {
@@ -7252,7 +9831,8 @@ function getLabelConfig(layerName, opts = {}) {
7252
9831
  layout: {
7253
9832
  "text-anchor": "left",
7254
9833
  "text-allow-overlap": false,
7255
- "text-optional": true
9834
+ "text-optional": true,
9835
+ "symbol-sort-key": labelSortKey(250)
7256
9836
  }
7257
9837
  };
7258
9838
  }
@@ -7425,11 +10005,15 @@ function getLabelConfig(layerName, opts = {}) {
7425
10005
  textColor: "#bfa88a",
7426
10006
  textHaloColor: DARK_BG,
7427
10007
  textHaloWidth: 1.5,
7428
- textOffset: [0, 1],
10008
+ textOffset: [0, 0],
7429
10009
  minzoom: 12,
7430
10010
  layout: {
10011
+ "text-variable-anchor": ["top", "bottom", "right", "left"],
10012
+ "text-radial-offset": 1,
10013
+ "text-justify": "auto",
7431
10014
  "text-allow-overlap": false,
7432
- "text-optional": true
10015
+ "text-optional": true,
10016
+ "symbol-sort-key": labelSortKey(400, ["length", ["coalesce", ["get", "OBJNAM"], ""]])
7433
10017
  }
7434
10018
  };
7435
10019
  case "PILPNT":
@@ -7653,17 +10237,26 @@ function getLabelConfig(layerName, opts = {}) {
7653
10237
  textColor: resolveColor("CHBLK", ct2),
7654
10238
  textHaloColor: resolveColor("CHWHT", ct2),
7655
10239
  textHaloWidth: 1.2,
7656
- // Place to the upper-right of the symbol so it doesn't sit on top
7657
- // of the asterisk / cross. Anchor top-left so the offset grows
7658
- // away from the symbol as text-size grows.
7659
- textOffset: [0.8, -0.4],
10240
+ textOffset: [0, 0],
7660
10241
  textFont: ["Open Sans Regular"],
7661
10242
  minzoom: 12,
10243
+ // Variable anchors: the depth callout tries each corner of the
10244
+ // hazard symbol and takes the first free spot, so in a rocky
10245
+ // field the callouts fan out around their symbols instead of
10246
+ // overprinting or vanishing. Safe here (unlike nav-aids) because
10247
+ // UWTROC/OBSTRN/WRECKS are dominance-PARTICIPATING classes —
10248
+ // exactly one band's copy renders at any zoom, so anchor-shifting
10249
+ // can't split a cross-band duplicate pair into two visible labels.
10250
+ // Shallowest (most dangerous) hazard wins within-class contests;
10251
+ // VALSOU-less hazards (awash / unsounded) rank first of all.
7662
10252
  layout: {
7663
- "text-anchor": "top-left",
10253
+ "text-variable-anchor": ["top-left", "bottom-left", "top-right", "bottom-right"],
10254
+ "text-radial-offset": 0.9,
10255
+ "text-justify": "auto",
7664
10256
  "text-allow-overlap": false,
7665
10257
  "text-optional": true,
7666
- "text-padding": 2
10258
+ "text-padding": 2,
10259
+ "symbol-sort-key": labelSortKey(100, ["*", 10, valsou])
7667
10260
  }
7668
10261
  };
7669
10262
  }
@@ -7702,7 +10295,13 @@ function getLabelConfig(layerName, opts = {}) {
7702
10295
  "text-rotation-alignment": "map",
7703
10296
  "text-allow-overlap": false,
7704
10297
  "text-optional": true,
7705
- "text-letter-spacing": 0.02
10298
+ "text-letter-spacing": 0.02,
10299
+ // Lower clearance = more dangerous = places first.
10300
+ "symbol-sort-key": labelSortKey(100, [
10301
+ "*",
10302
+ 5,
10303
+ ["to-number", ["coalesce", ["get", "VERCLR"], 199], 199]
10304
+ ])
7706
10305
  }
7707
10306
  };
7708
10307
  }
@@ -7731,7 +10330,10 @@ function getLabelConfig(layerName, opts = {}) {
7731
10330
  "symbol-spacing": 250,
7732
10331
  "text-rotation-alignment": "map",
7733
10332
  "text-allow-overlap": false,
7734
- "text-optional": true
10333
+ "text-optional": true,
10334
+ // Shallower contours win within-class contests, mirroring the
10335
+ // SOUNDG shallowest-wins rule.
10336
+ "symbol-sort-key": labelSortKey(300, ["to-number", ["get", "VALDCO"], 999])
7735
10337
  }
7736
10338
  };
7737
10339
  default:
@@ -7904,9 +10506,8 @@ function preprocessNavAids(fc) {
7904
10506
  const props = f.properties ?? {};
7905
10507
  const objnam = props.OBJNAM;
7906
10508
  if (!objnam) return f;
7907
- const match = objnam.match(/(\d+[A-Z]?)\s*$/);
7908
- if (!match) return f;
7909
- const designation = match[1];
10509
+ const designation = extractNavAidDesignation(objnam);
10510
+ if (!designation) return f;
7910
10511
  const colourCode = firstListCode(props.COLOUR);
7911
10512
  const prefix = NAV_COLOUR_PREFIX[colourCode];
7912
10513
  const label = prefix ? `${prefix} "${designation}"` : `"${designation}"`;
@@ -9660,7 +12261,7 @@ function getS52SymbolStats() {
9660
12261
  }
9661
12262
 
9662
12263
  // src/atlas/tile-zoom.ts
9663
- var BUILD_MINZOOM = 3;
12264
+ var BUILD_MINZOOM = 0;
9664
12265
  var BUILD_MAXZOOM = 14;
9665
12266
 
9666
12267
  // src/atlas/source-registry.ts
@@ -9760,33 +12361,6 @@ var CHILD_ICON_OFFSETS = {
9760
12361
  LIGHTS: [14, 14]
9761
12362
  // lower-right, off the buoy hull so the flare is visible
9762
12363
  };
9763
- var SAFETY_CRITICAL_LABELS = /* @__PURE__ */ new Set([
9764
- "LIGHTS",
9765
- "SOUNDG",
9766
- "BOYLAT",
9767
- "BCNLAT",
9768
- "BOYCAR",
9769
- "BCNCAR",
9770
- "BOYSPP",
9771
- "BCNSPP",
9772
- "BOYISD",
9773
- "BOYSAW",
9774
- "FOGSIG",
9775
- "RTPBCN"
9776
- ]);
9777
- var HIGH_DENSITY_NAV_AIDS = /* @__PURE__ */ new Set([
9778
- "BOYLAT",
9779
- "BOYCAR",
9780
- "BOYSPP",
9781
- "BOYISD",
9782
- "BOYSAW",
9783
- "BOYINB",
9784
- "BCNLAT",
9785
- "BCNCAR",
9786
- "BCNSPP",
9787
- "BCNISD",
9788
- "BCNSAW"
9789
- ]);
9790
12364
  function buildNavAidLabelExpression() {
9791
12365
  const colourPrefix = colourCodeCase(
9792
12366
  [
@@ -9801,7 +12375,12 @@ function buildNavAidLabelExpression() {
9801
12375
  ["has", "_navAidLabel"],
9802
12376
  ["get", "_navAidLabel"],
9803
12377
  ["has", "OBJNAM"],
9804
- ["concat", colourPrefix, ["get", "OBJNAM"]],
12378
+ // Chart-convention abbreviation: `R "4"` instead of the full NOAA
12379
+ // name ("R Boston North Channel Lighted Buoy 4") — the tile pipeline
12380
+ // doesn't precompute `_navAidLabel`, so before this expression every
12381
+ // production buoy label carried its full 30-40 glyph OBJNAM, the
12382
+ // biggest single driver of Harbor/Approach label overprint (E3).
12383
+ navAidDesignationTextExpression(["get", "OBJNAM"], colourPrefix),
9805
12384
  ""
9806
12385
  ];
9807
12386
  }
@@ -9992,6 +12571,11 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION) {
9992
12571
  layer.paint["icon-color"] = lightFillExpression(ct);
9993
12572
  layer.paint["icon-halo-color"] = resolveColor("CHBLK", ct);
9994
12573
  layer.paint["icon-halo-width"] = 1;
12574
+ layer.layout = {
12575
+ ...layer.layout,
12576
+ "icon-allow-overlap": true,
12577
+ "icon-ignore-placement": true
12578
+ };
9995
12579
  }
9996
12580
  } else if (resolved.layout) {
9997
12581
  layer.layout = resolved.layout;
@@ -10862,7 +13446,7 @@ function buildLabelLayer2(objectClass, ct, sourceId = DEFAULT_REGION, depthUnits
10862
13446
  ];
10863
13447
  }
10864
13448
  }
10865
- const isSafetyCritical = SAFETY_CRITICAL_LABELS.has(objectClass);
13449
+ const labelPriority = getLabelPriority(objectClass);
10866
13450
  const layer = {
10867
13451
  id: `s52-${objectClass}-label`,
10868
13452
  type: "symbol",
@@ -10875,15 +13459,20 @@ function buildLabelLayer2(objectClass, ct, sourceId = DEFAULT_REGION, depthUnits
10875
13459
  "text-font": config.textFont ?? ["Open Sans Regular"],
10876
13460
  "text-offset": config.textOffset,
10877
13461
  "text-anchor": "top",
10878
- "text-allow-overlap": isSafetyCritical,
10879
- "text-optional": !isSafetyCritical,
10880
- "text-padding": isSafetyCritical ? 0 : 4,
13462
+ "text-allow-overlap": false,
13463
+ "text-optional": true,
13464
+ "text-padding": 4,
13465
+ "symbol-sort-key": labelPriority * 1e3,
10881
13466
  ...config.layout ?? {}
10882
13467
  },
10883
13468
  paint: {
10884
13469
  "text-color": config.textColor,
10885
13470
  "text-halo-color": config.textHaloColor,
10886
13471
  "text-halo-width": config.textHaloWidth
13472
+ },
13473
+ metadata: {
13474
+ "s52:objectClass": objectClass,
13475
+ "chrt:labelPriority": labelPriority
10887
13476
  }
10888
13477
  };
10889
13478
  if (config.minzoom) {
@@ -10935,13 +13524,24 @@ function getOrderedClasses() {
10935
13524
 
10936
13525
  // src/atlas/band-priority.ts
10937
13526
  var BAND_PRIORITY_RANGES = {
10938
- "1": { minzoom: 3 },
13527
+ "1": { minzoom: 0 },
10939
13528
  "2": { minzoom: 5 },
10940
13529
  "3": { minzoom: 7 },
10941
13530
  "4": { minzoom: 9 },
10942
13531
  "5": { minzoom: 11 },
10943
13532
  "6": { minzoom: 11 }
10944
13533
  };
13534
+ var BAND_DATA_MINZOOM = {
13535
+ "1": 0,
13536
+ "2": 5,
13537
+ "3": 7,
13538
+ "4": 9,
13539
+ "5": 11,
13540
+ "6": 13
13541
+ };
13542
+ function dominatedVariantMaxzoom(D) {
13543
+ return Math.max(BAND_PRIORITY_RANGES[D].minzoom, BAND_DATA_MINZOOM[D]);
13544
+ }
10945
13545
  var BAND_DRAW_ORDER = ["1", "2", "3", "4", "5", "6"];
10946
13546
  var CHART_BAND_CODES = {
10947
13547
  Overview: ["1"],
@@ -11102,12 +13702,26 @@ var DOMINANCE_EXEMPT_CLASSES = /* @__PURE__ */ new Set([
11102
13702
  "RTPBCN",
11103
13703
  "RDOSTA"
11104
13704
  ]);
13705
+ var DOMINANCE_EXEMPT_FILL_CLASSES = /* @__PURE__ */ new Set([
13706
+ "LNDARE",
13707
+ "DEPARE",
13708
+ "UNSARE",
13709
+ "LAKARE",
13710
+ "DRGARE",
13711
+ "SEAARE"
13712
+ ]);
11105
13713
  function isDominanceExempt(layer) {
11106
13714
  for (const cls of DOMINANCE_EXEMPT_CLASSES) {
11107
13715
  if (layer.id.startsWith(`s52-${cls}-`)) return true;
11108
13716
  }
13717
+ if (layer.type === "fill") {
13718
+ for (const cls of DOMINANCE_EXEMPT_FILL_CLASSES) {
13719
+ if (layer.id.startsWith(`s52-${cls}-`)) return true;
13720
+ }
13721
+ }
11109
13722
  return false;
11110
13723
  }
13724
+ var TOP_STACK_LIGHT_CLASSES = /* @__PURE__ */ new Set(["LIGHTS"]);
11111
13725
  function makeBandVariants(layer, B) {
11112
13726
  const range = BAND_PRIORITY_RANGES[B];
11113
13727
  const classMinzoom = layer.minzoom ?? 0;
@@ -11138,8 +13752,8 @@ function makeBandVariants(layer, B) {
11138
13752
  for (const D of BAND_DRAW_ORDER) {
11139
13753
  const dInt = parseInt(D, 10);
11140
13754
  if (dInt <= bInt) continue;
11141
- const dRange = BAND_PRIORITY_RANGES[D];
11142
- if (dRange.minzoom <= minzoom) continue;
13755
+ const dMaxzoom = dominatedVariantMaxzoom(D);
13756
+ if (dMaxzoom <= minzoom) continue;
11143
13757
  const domVariant = {
11144
13758
  ...layer,
11145
13759
  id: `${layer.id}-band${B}-domBy${D}`,
@@ -11148,7 +13762,7 @@ function makeBandVariants(layer, B) {
11148
13762
  ["all", bandFilterExpression(B), dominatedByMinFilter(D)]
11149
13763
  ),
11150
13764
  minzoom,
11151
- maxzoom: dRange.minzoom
13765
+ maxzoom: dMaxzoom
11152
13766
  };
11153
13767
  variants.push(domVariant);
11154
13768
  }
@@ -11195,46 +13809,26 @@ function fanOrientationLabelByCellBand(label) {
11195
13809
  paint
11196
13810
  });
11197
13811
  for (const D of BAND_DRAW_ORDER) {
11198
- const dom = BAND_PRIORITY_RANGES[D];
11199
- if (dom.minzoom <= minzoom) continue;
13812
+ const dMaxzoom = dominatedVariantMaxzoom(D);
13813
+ if (dMaxzoom <= minzoom) continue;
11200
13814
  variants.push({
11201
13815
  ...label,
11202
13816
  id: `${label.id}-cell${code}-domBy${D}`,
11203
13817
  filter: combineWithBandFilter(cellFiltered, dominatedByMinFilter(D)),
11204
13818
  minzoom,
11205
- maxzoom: dom.minzoom,
13819
+ // Data-aware ceiling: band 6 has no tile data until z13 despite
13820
+ // being style-eligible at z11 (see dominatedVariantMaxzoom).
13821
+ maxzoom: dMaxzoom,
11206
13822
  paint
11207
13823
  });
11208
13824
  }
11209
13825
  }
11210
13826
  return variants;
11211
13827
  }
11212
- function applyHarborLabelCollisionDiscipline(variant, objectClass, code) {
11213
- if (!HIGH_DENSITY_NAV_AIDS.has(objectClass)) return variant;
11214
- if (code !== "5" && code !== "6") return variant;
11215
- const oldLayout = variant.layout ?? {};
11216
- return {
11217
- ...variant,
11218
- layout: {
11219
- ...oldLayout,
11220
- "text-allow-overlap": false,
11221
- "text-optional": true,
11222
- // Wider padding so even allowed labels space visually instead of
11223
- // touching. 6 is enough to read individual buoy designations clearly
11224
- // at z12-14 over Boston Harbor.
11225
- "text-padding": 6,
11226
- // Deterministic collision arbitration: prefer features with
11227
- // longer-range nav-aid significance (VALNMR — visible nautical
11228
- // miles), then shorter OBJNAM (tighter labels win), then FIDN as
11229
- // tiebreaker. Lower sort-key wins in MapLibre.
11230
- "symbol-sort-key": [
11231
- "case",
11232
- ["has", "VALNMR"],
11233
- ["-", 0, ["coalesce", ["to-number", ["get", "VALNMR"]], 0]],
11234
- ["length", ["coalesce", ["get", "OBJNAM"], ""]]
11235
- ]
11236
- }
11237
- };
13828
+ function byLabelPriorityBottomUp(bundles) {
13829
+ return [...bundles].sort(
13830
+ (a, b) => getLabelPriority(b.objectClass) - getLabelPriority(a.objectClass)
13831
+ );
11238
13832
  }
11239
13833
  function chartModeFilterForBand(layer, B, higherCascadeBands, lockedBounds, isRequestedBand) {
11240
13834
  let filter = combineWithBandFilter(layer.filter, bandFilterExpression(B));
@@ -11270,37 +13864,47 @@ function generateStyle(palette = "DAY", opts = {}) {
11270
13864
  })
11271
13865
  });
11272
13866
  }
13867
+ const regularBundles = bundles.filter((b) => !TOP_STACK_LIGHT_CLASSES.has(b.objectClass));
13868
+ const topLightBundles = bundles.filter((b) => TOP_STACK_LIGHT_CLASSES.has(b.objectClass));
11273
13869
  if (chartCascade && chartRequestedCodes) {
11274
13870
  const requestedSet = new Set(chartRequestedCodes);
11275
- for (let i = 0; i < chartCascade.length; i++) {
11276
- const B = chartCascade[i];
11277
- const higher = chartCascade.slice(i + 1);
11278
- const isRequested = requestedSet.has(B);
11279
- for (const { featureLayers } of bundles) {
11280
- for (const base of featureLayers) {
11281
- const filter = chartModeFilterForBand(
11282
- base,
11283
- B,
11284
- higher,
11285
- lockedBounds,
11286
- isRequested
11287
- );
11288
- const variant = {
11289
- ...base,
11290
- id: `${base.id}-band${B}`,
11291
- filter
11292
- };
11293
- delete variant.maxzoom;
11294
- layers.push(variant);
13871
+ const emitChartFeatureVariants = (bundleSet) => {
13872
+ for (let i = 0; i < chartCascade.length; i++) {
13873
+ const B = chartCascade[i];
13874
+ const higher = chartCascade.slice(i + 1);
13875
+ const isRequested = requestedSet.has(B);
13876
+ for (const { featureLayers } of bundleSet) {
13877
+ for (const base of featureLayers) {
13878
+ const filter = chartModeFilterForBand(
13879
+ base,
13880
+ B,
13881
+ higher,
13882
+ lockedBounds,
13883
+ isRequested
13884
+ );
13885
+ const variant = {
13886
+ ...base,
13887
+ id: `${base.id}-band${B}`,
13888
+ filter
13889
+ };
13890
+ delete variant.maxzoom;
13891
+ layers.push(variant);
13892
+ }
11295
13893
  }
11296
13894
  }
11297
- }
11298
- for (let i = 0; i < chartCascade.length; i++) {
11299
- const B = chartCascade[i];
11300
- const higher = chartCascade.slice(i + 1);
11301
- const isRequested = requestedSet.has(B);
11302
- for (const { label } of bundles) {
11303
- if (!label) continue;
13895
+ };
13896
+ emitChartFeatureVariants(regularBundles);
13897
+ emitChartFeatureVariants(topLightBundles);
13898
+ const chartLabelBundles = byLabelPriorityBottomUp(
13899
+ bundles.filter(
13900
+ (b) => b.label !== null
13901
+ )
13902
+ );
13903
+ for (const { label } of chartLabelBundles) {
13904
+ for (let i = 0; i < chartCascade.length; i++) {
13905
+ const B = chartCascade[i];
13906
+ const higher = chartCascade.slice(i + 1);
13907
+ const isRequested = requestedSet.has(B);
11304
13908
  const filter = chartModeFilterForBand(
11305
13909
  label,
11306
13910
  B,
@@ -11320,7 +13924,14 @@ function generateStyle(palette = "DAY", opts = {}) {
11320
13924
  layers.push(...labelLayers);
11321
13925
  } else {
11322
13926
  for (const code of BAND_DRAW_ORDER) {
11323
- for (const { featureLayers } of bundles) {
13927
+ for (const { featureLayers } of regularBundles) {
13928
+ for (const base of featureLayers) {
13929
+ layers.push(...makeBandVariants(base, code));
13930
+ }
13931
+ }
13932
+ }
13933
+ for (const code of BAND_DRAW_ORDER) {
13934
+ for (const { featureLayers } of topLightBundles) {
11324
13935
  for (const base of featureLayers) {
11325
13936
  layers.push(...makeBandVariants(base, code));
11326
13937
  }
@@ -11332,13 +13943,14 @@ function generateStyle(palette = "DAY", opts = {}) {
11332
13943
  labelLayers.push(...fanOrientationLabelByCellBand(label));
11333
13944
  }
11334
13945
  }
11335
- for (const code of BAND_DRAW_ORDER) {
11336
- for (const { objectClass, label } of bundles) {
11337
- if (!label) continue;
11338
- if (getLabelBehavior(objectClass) === "orientation-cross-band") continue;
11339
- for (const variant of makeBandVariants(label, code)) {
11340
- labelLayers.push(applyHarborLabelCollisionDiscipline(variant, objectClass, code));
11341
- }
13946
+ const featureLabelBundles = byLabelPriorityBottomUp(
13947
+ bundles.filter(
13948
+ (b) => b.label !== null && getLabelBehavior(b.objectClass) !== "orientation-cross-band"
13949
+ )
13950
+ );
13951
+ for (const { label } of featureLabelBundles) {
13952
+ for (const code of BAND_DRAW_ORDER) {
13953
+ labelLayers.push(...makeBandVariants(label, code));
11342
13954
  }
11343
13955
  }
11344
13956
  layers.push(...labelLayers);
@@ -11404,6 +14016,363 @@ function getStyleStats(style) {
11404
14016
  };
11405
14017
  }
11406
14018
 
14019
+ // src/atlas/custom-style.ts
14020
+ var DEFAULT_NAV_AID_SETTINGS = {
14021
+ buoysVisible: true,
14022
+ beaconsVisible: true,
14023
+ lightsVisible: true,
14024
+ scaminOverride: 0,
14025
+ iconScale: 0.8,
14026
+ iconSizeLocked: false,
14027
+ allowOverlap: false,
14028
+ showSectorArcs: true,
14029
+ fogSignalsVisible: true,
14030
+ radarVisible: true
14031
+ };
14032
+ var DEFAULT_LABEL_SETTINGS = {
14033
+ navAidLabelsVisible: true,
14034
+ landLabelsVisible: true,
14035
+ structureLabelsVisible: true,
14036
+ depthContoursVisible: true,
14037
+ navAidFullNames: false,
14038
+ minzoomOverride: null,
14039
+ textScale: 1,
14040
+ allowOverlap: false
14041
+ };
14042
+ var DEFAULT_SOUNDING_SETTINGS = {
14043
+ visible: true,
14044
+ minzoomOverride: null,
14045
+ textScale: 1,
14046
+ allowOverlap: false,
14047
+ showUnits: false,
14048
+ safetyContour: 10,
14049
+ fontWeight: "semibold",
14050
+ twoShade: true
14051
+ };
14052
+ var DEFAULT_WATER_BODY_SETTINGS = {
14053
+ seaAreasVisible: true,
14054
+ lakesVisible: true,
14055
+ riversVisible: true,
14056
+ canalsVisible: true,
14057
+ minzoomOverride: null,
14058
+ textScale: 1,
14059
+ allowOverlap: false,
14060
+ uppercaseSeaAreas: true
14061
+ };
14062
+ var NAV_AID_LAYER_GROUPS = {
14063
+ buoys: ["BOYLAT", "BOYCAR", "BOYSPP", "BOYISD", "BOYSAW"],
14064
+ beacons: ["BCNLAT", "BCNCAR", "BCNSPP"],
14065
+ lights: ["LIGHTS"],
14066
+ fogSignals: ["FOGSIG"],
14067
+ radar: ["RTPBCN", "RDOSTA"]
14068
+ };
14069
+ var ALL_NAV_AID_LAYERS = new Set(
14070
+ Object.values(NAV_AID_LAYER_GROUPS).flat()
14071
+ );
14072
+ var LABEL_LAYER_GROUPS = {
14073
+ navAids: ["BOYLAT", "BCNLAT", "BOYCAR", "BOYSPP", "BOYISD", "BOYSAW", "BCNCAR", "BCNSPP", "LIGHTS", "FOGSIG", "RTPBCN", "RDOSTA"],
14074
+ land: ["LNDRGN", "LNDMRK", "LNDARE"],
14075
+ structures: ["BRIDGE", "PILPNT", "MORFAC", "SLCONS", "BUISGL", "AIRARE"],
14076
+ depthContours: ["DEPCNT"]
14077
+ };
14078
+ var SOUNDING_LAYERS = ["SOUNDG"];
14079
+ var WATER_BODY_LAYER_GROUPS = {
14080
+ seaAreas: ["SEAARE"],
14081
+ lakes: ["LAKARE"],
14082
+ rivers: ["RIVERS"],
14083
+ canals: ["CANALS"]
14084
+ };
14085
+ var ALL_WATER_BODY_LAYERS = new Set(
14086
+ Object.values(WATER_BODY_LAYER_GROUPS).flat()
14087
+ );
14088
+ function isNavAidLayerVisible(layerName, settings) {
14089
+ if (NAV_AID_LAYER_GROUPS.buoys.includes(layerName)) return settings.buoysVisible;
14090
+ if (NAV_AID_LAYER_GROUPS.beacons.includes(layerName)) return settings.beaconsVisible;
14091
+ if (NAV_AID_LAYER_GROUPS.lights.includes(layerName)) return settings.lightsVisible;
14092
+ if (NAV_AID_LAYER_GROUPS.fogSignals.includes(layerName)) return settings.fogSignalsVisible;
14093
+ if (NAV_AID_LAYER_GROUPS.radar.includes(layerName)) return settings.radarVisible;
14094
+ return true;
14095
+ }
14096
+ function isLabelLayerVisible(layerName, settings) {
14097
+ if (LABEL_LAYER_GROUPS.navAids.includes(layerName)) return settings.navAidLabelsVisible;
14098
+ if (LABEL_LAYER_GROUPS.land.includes(layerName)) return settings.landLabelsVisible;
14099
+ if (LABEL_LAYER_GROUPS.structures.includes(layerName)) return settings.structureLabelsVisible;
14100
+ if (LABEL_LAYER_GROUPS.depthContours.includes(layerName)) return settings.depthContoursVisible;
14101
+ return true;
14102
+ }
14103
+ function isSoundingLayerVisible(settings) {
14104
+ return settings.visible;
14105
+ }
14106
+ function isWaterBodyLayerVisible(layerName, settings) {
14107
+ if (WATER_BODY_LAYER_GROUPS.seaAreas.includes(layerName)) return settings.seaAreasVisible;
14108
+ if (WATER_BODY_LAYER_GROUPS.lakes.includes(layerName)) return settings.lakesVisible;
14109
+ if (WATER_BODY_LAYER_GROUPS.rivers.includes(layerName)) return settings.riversVisible;
14110
+ if (WATER_BODY_LAYER_GROUPS.canals.includes(layerName)) return settings.canalsVisible;
14111
+ return true;
14112
+ }
14113
+ var DEFAULT_CUSTOM_STYLE_CONFIG = {
14114
+ enabled_color_tables: ["DAY", "DUSK", "NIGHT"],
14115
+ default_color_table: "DAY",
14116
+ display_mode: "STANDARD",
14117
+ nav_aid_settings: DEFAULT_NAV_AID_SETTINGS,
14118
+ label_settings: DEFAULT_LABEL_SETTINGS,
14119
+ sounding_settings: DEFAULT_SOUNDING_SETTINGS,
14120
+ water_body_settings: DEFAULT_WATER_BODY_SETTINGS,
14121
+ visible_layers: null
14122
+ };
14123
+ var COLOR_TABLE_NAMES = /* @__PURE__ */ new Set(["DAY", "DUSK", "NIGHT"]);
14124
+ var DISPLAY_MODES = /* @__PURE__ */ new Set(["BASE", "STANDARD", "ALL"]);
14125
+ var LAYER_NAME_RE = /^[A-Za-z0-9_$]{2,16}$/;
14126
+ var MAX_VISIBLE_LAYERS = 512;
14127
+ function clampNumber(raw, min, max, fallback) {
14128
+ const n = typeof raw === "number" ? raw : NaN;
14129
+ if (!Number.isFinite(n)) return fallback;
14130
+ return Math.min(max, Math.max(min, n));
14131
+ }
14132
+ function intOrNull(raw, min, max, fallback) {
14133
+ if (raw === null) return null;
14134
+ const n = typeof raw === "number" ? raw : NaN;
14135
+ if (!Number.isFinite(n)) return fallback;
14136
+ return Math.round(Math.min(max, Math.max(min, n)));
14137
+ }
14138
+ function bool(raw, fallback) {
14139
+ return typeof raw === "boolean" ? raw : fallback;
14140
+ }
14141
+ function asRecord(raw) {
14142
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
14143
+ }
14144
+ function sanitizeNavAid(raw) {
14145
+ const r = asRecord(raw);
14146
+ const d = DEFAULT_NAV_AID_SETTINGS;
14147
+ return {
14148
+ buoysVisible: bool(r.buoysVisible, d.buoysVisible),
14149
+ beaconsVisible: bool(r.beaconsVisible, d.beaconsVisible),
14150
+ lightsVisible: bool(r.lightsVisible, d.lightsVisible),
14151
+ scaminOverride: intOrNull(r.scaminOverride, 0, 14, d.scaminOverride),
14152
+ iconScale: clampNumber(r.iconScale, 0.2, 2, d.iconScale),
14153
+ iconSizeLocked: bool(r.iconSizeLocked, d.iconSizeLocked),
14154
+ allowOverlap: bool(r.allowOverlap, d.allowOverlap),
14155
+ showSectorArcs: bool(r.showSectorArcs, d.showSectorArcs),
14156
+ fogSignalsVisible: bool(r.fogSignalsVisible, d.fogSignalsVisible),
14157
+ radarVisible: bool(r.radarVisible, d.radarVisible)
14158
+ };
14159
+ }
14160
+ function sanitizeLabels(raw) {
14161
+ const r = asRecord(raw);
14162
+ const d = DEFAULT_LABEL_SETTINGS;
14163
+ return {
14164
+ navAidLabelsVisible: bool(r.navAidLabelsVisible, d.navAidLabelsVisible),
14165
+ landLabelsVisible: bool(r.landLabelsVisible, d.landLabelsVisible),
14166
+ structureLabelsVisible: bool(r.structureLabelsVisible, d.structureLabelsVisible),
14167
+ depthContoursVisible: bool(r.depthContoursVisible, d.depthContoursVisible),
14168
+ navAidFullNames: bool(r.navAidFullNames, d.navAidFullNames),
14169
+ minzoomOverride: intOrNull(r.minzoomOverride, 0, 14, d.minzoomOverride),
14170
+ textScale: clampNumber(r.textScale, 0.5, 2, d.textScale),
14171
+ allowOverlap: bool(r.allowOverlap, d.allowOverlap)
14172
+ };
14173
+ }
14174
+ function sanitizeSoundings(raw) {
14175
+ const r = asRecord(raw);
14176
+ const d = DEFAULT_SOUNDING_SETTINGS;
14177
+ return {
14178
+ visible: bool(r.visible, d.visible),
14179
+ minzoomOverride: intOrNull(r.minzoomOverride, 0, 14, d.minzoomOverride),
14180
+ textScale: clampNumber(r.textScale, 0.5, 2, d.textScale),
14181
+ allowOverlap: bool(r.allowOverlap, d.allowOverlap),
14182
+ showUnits: bool(r.showUnits, d.showUnits),
14183
+ safetyContour: clampNumber(r.safetyContour, 0, 100, d.safetyContour),
14184
+ fontWeight: r.fontWeight === "regular" || r.fontWeight === "semibold" ? r.fontWeight : d.fontWeight,
14185
+ twoShade: bool(r.twoShade, d.twoShade)
14186
+ };
14187
+ }
14188
+ function sanitizeWaterBody(raw) {
14189
+ const r = asRecord(raw);
14190
+ const d = DEFAULT_WATER_BODY_SETTINGS;
14191
+ return {
14192
+ seaAreasVisible: bool(r.seaAreasVisible, d.seaAreasVisible),
14193
+ lakesVisible: bool(r.lakesVisible, d.lakesVisible),
14194
+ riversVisible: bool(r.riversVisible, d.riversVisible),
14195
+ canalsVisible: bool(r.canalsVisible, d.canalsVisible),
14196
+ minzoomOverride: intOrNull(r.minzoomOverride, 0, 14, d.minzoomOverride),
14197
+ textScale: clampNumber(r.textScale, 0.5, 2, d.textScale),
14198
+ allowOverlap: bool(r.allowOverlap, d.allowOverlap),
14199
+ uppercaseSeaAreas: bool(r.uppercaseSeaAreas, d.uppercaseSeaAreas)
14200
+ };
14201
+ }
14202
+ function sanitizeCustomStyleConfig(raw) {
14203
+ const r = asRecord(raw);
14204
+ const enabledRaw = Array.isArray(r.enabled_color_tables) ? r.enabled_color_tables : [];
14205
+ const enabled = [...new Set(
14206
+ enabledRaw.filter((c) => typeof c === "string" && COLOR_TABLE_NAMES.has(c))
14207
+ )];
14208
+ const enabledFinal = enabled.length > 0 ? enabled : ["DAY", "DUSK", "NIGHT"];
14209
+ const defaultRaw = typeof r.default_color_table === "string" ? r.default_color_table : "";
14210
+ const defaultFinal = enabledFinal.includes(defaultRaw) ? defaultRaw : enabledFinal[0];
14211
+ const modeRaw = typeof r.display_mode === "string" ? r.display_mode : "";
14212
+ const mode = DISPLAY_MODES.has(modeRaw) ? modeRaw : "STANDARD";
14213
+ let visibleLayers = null;
14214
+ if (Array.isArray(r.visible_layers)) {
14215
+ visibleLayers = r.visible_layers.filter((l) => typeof l === "string" && LAYER_NAME_RE.test(l)).slice(0, MAX_VISIBLE_LAYERS).map((l) => l.toUpperCase());
14216
+ }
14217
+ return {
14218
+ enabled_color_tables: enabledFinal,
14219
+ default_color_table: defaultFinal,
14220
+ display_mode: mode,
14221
+ nav_aid_settings: sanitizeNavAid(r.nav_aid_settings),
14222
+ label_settings: sanitizeLabels(r.label_settings),
14223
+ sounding_settings: sanitizeSoundings(r.sounding_settings),
14224
+ water_body_settings: sanitizeWaterBody(r.water_body_settings),
14225
+ visible_layers: visibleLayers
14226
+ };
14227
+ }
14228
+ function normalizeTheme(raw) {
14229
+ if (!raw) return null;
14230
+ const upper = raw.trim().toUpperCase();
14231
+ return COLOR_TABLE_NAMES.has(upper) ? upper : null;
14232
+ }
14233
+ function resolveCustomTheme(config, requested) {
14234
+ if (!requested) return { palette: config.default_color_table, themeFellBack: false };
14235
+ if (config.enabled_color_tables.includes(requested)) {
14236
+ return { palette: requested, themeFellBack: false };
14237
+ }
14238
+ return { palette: config.default_color_table, themeFellBack: true };
14239
+ }
14240
+ function layerObjectClass(layer) {
14241
+ const meta = layer.metadata;
14242
+ const fromMeta = meta?.["s52:objectClass"];
14243
+ if (typeof fromMeta === "string" && fromMeta.length > 0) return fromMeta;
14244
+ const m = /^s52-([A-Za-z0-9_$]+)-/.exec(layer.id);
14245
+ return m ? m[1] : null;
14246
+ }
14247
+ function isLabelLayer(layer) {
14248
+ if (layer.type !== "symbol") return false;
14249
+ const layout = layer.layout;
14250
+ return layout?.["text-field"] !== void 0;
14251
+ }
14252
+ function isIconLayer(layer) {
14253
+ if (layer.type !== "symbol") return false;
14254
+ const layout = layer.layout;
14255
+ return layout?.["icon-image"] !== void 0 && layout?.["text-field"] === void 0;
14256
+ }
14257
+ function bandFloor(layerId) {
14258
+ const m = /-band(\d)/.exec(layerId);
14259
+ if (!m) return 0;
14260
+ const range = BAND_PRIORITY_RANGES[m[1]];
14261
+ return range ? range.minzoom : 0;
14262
+ }
14263
+ function scaleSizeValue(value, factor) {
14264
+ if (typeof value === "number") {
14265
+ return Math.round(value * factor * 100) / 100;
14266
+ }
14267
+ if (!Array.isArray(value) || value.length === 0) return value;
14268
+ const op = value[0];
14269
+ if (op === "interpolate") {
14270
+ const out = [...value];
14271
+ for (let i = 4; i < out.length; i += 2) out[i] = scaleSizeValue(out[i], factor);
14272
+ return out;
14273
+ }
14274
+ if (op === "step") {
14275
+ const out = [...value];
14276
+ for (let i = 2; i < out.length; i += 2) out[i] = scaleSizeValue(out[i], factor);
14277
+ return out;
14278
+ }
14279
+ if (op === "case") {
14280
+ const out = [...value];
14281
+ for (let i = 2; i < out.length - 1; i += 2) out[i] = scaleSizeValue(out[i], factor);
14282
+ out[out.length - 1] = scaleSizeValue(out[out.length - 1], factor);
14283
+ return out;
14284
+ }
14285
+ if (op === "match") {
14286
+ const out = [...value];
14287
+ for (let i = 3; i < out.length - 1; i += 2) out[i] = scaleSizeValue(out[i], factor);
14288
+ out[out.length - 1] = scaleSizeValue(out[out.length - 1], factor);
14289
+ return out;
14290
+ }
14291
+ return value;
14292
+ }
14293
+ function setLayout(layer, key, val) {
14294
+ const layout = { ...layer.layout ?? {} };
14295
+ layout[key] = val;
14296
+ layer.layout = layout;
14297
+ }
14298
+ function hideLayer(layer) {
14299
+ setLayout(layer, "visibility", "none");
14300
+ }
14301
+ function applyCustomStyleSettings(style, config) {
14302
+ const nav = config.nav_aid_settings;
14303
+ const labels = config.label_settings;
14304
+ const soundings = config.sounding_settings;
14305
+ const water = config.water_body_settings;
14306
+ const visibleSet = config.visible_layers ? new Set(config.visible_layers) : null;
14307
+ for (const layer of style.layers) {
14308
+ const cls = layerObjectClass(layer);
14309
+ if (!cls) continue;
14310
+ const label = isLabelLayer(layer);
14311
+ if (visibleSet && !visibleSet.has(cls)) {
14312
+ hideLayer(layer);
14313
+ continue;
14314
+ }
14315
+ if (label) {
14316
+ const hidden = SOUNDING_LAYERS.includes(cls) && !isSoundingLayerVisible(soundings) || ALL_WATER_BODY_LAYERS.has(cls) && !isWaterBodyLayerVisible(cls, water) || !SOUNDING_LAYERS.includes(cls) && !ALL_WATER_BODY_LAYERS.has(cls) && !isLabelLayerVisible(cls, labels);
14317
+ if (hidden) {
14318
+ hideLayer(layer);
14319
+ continue;
14320
+ }
14321
+ } else {
14322
+ const hidden = ALL_NAV_AID_LAYERS.has(cls) && !isNavAidLayerVisible(cls, nav) || ALL_WATER_BODY_LAYERS.has(cls) && !isWaterBodyLayerVisible(cls, water) || SOUNDING_LAYERS.includes(cls) && !isSoundingLayerVisible(soundings);
14323
+ if (hidden) {
14324
+ hideLayer(layer);
14325
+ continue;
14326
+ }
14327
+ }
14328
+ if (label) {
14329
+ const bundle = SOUNDING_LAYERS.includes(cls) ? { textScale: soundings.textScale, minzoom: soundings.minzoomOverride, allowOverlap: soundings.allowOverlap } : ALL_WATER_BODY_LAYERS.has(cls) ? { textScale: water.textScale, minzoom: water.minzoomOverride, allowOverlap: water.allowOverlap } : { textScale: labels.textScale, minzoom: labels.minzoomOverride, allowOverlap: labels.allowOverlap };
14330
+ if (bundle.textScale !== 1) {
14331
+ const layout = layer.layout;
14332
+ if (layout?.["text-size"] !== void 0) {
14333
+ setLayout(layer, "text-size", scaleSizeValue(layout["text-size"], bundle.textScale));
14334
+ }
14335
+ }
14336
+ if (bundle.allowOverlap === true) {
14337
+ setLayout(layer, "text-allow-overlap", true);
14338
+ setLayout(layer, "text-optional", false);
14339
+ }
14340
+ if (bundle.minzoom !== null) {
14341
+ layer.minzoom = Math.max(bundle.minzoom, bandFloor(layer.id));
14342
+ }
14343
+ if (cls === "SEAARE") {
14344
+ setLayout(layer, "text-transform", water.uppercaseSeaAreas ? "uppercase" : "none");
14345
+ }
14346
+ }
14347
+ if (!label && ALL_NAV_AID_LAYERS.has(cls) && isIconLayer(layer)) {
14348
+ if (nav.iconScale !== 1) {
14349
+ const layout = layer.layout;
14350
+ const current = layout?.["icon-size"];
14351
+ setLayout(layer, "icon-size", current === void 0 ? Math.round(nav.iconScale * 100) / 100 : scaleSizeValue(current, nav.iconScale));
14352
+ }
14353
+ if (nav.allowOverlap === true) {
14354
+ setLayout(layer, "icon-allow-overlap", true);
14355
+ }
14356
+ if (nav.scaminOverride !== null) {
14357
+ layer.minzoom = Math.max(nav.scaminOverride, bandFloor(layer.id));
14358
+ }
14359
+ }
14360
+ }
14361
+ return style;
14362
+ }
14363
+ function generateCustomStyle(config, opts = {}) {
14364
+ const { theme, ...generatorOpts } = opts;
14365
+ const { palette, themeFellBack } = resolveCustomTheme(config, theme ?? null);
14366
+ const style = generateStyle(palette, {
14367
+ ...generatorOpts,
14368
+ safetyContour: config.sounding_settings.safetyContour,
14369
+ twoShadeSoundings: config.sounding_settings.twoShade,
14370
+ soundingFontWeight: config.sounding_settings.fontWeight
14371
+ });
14372
+ applyCustomStyleSettings(style, config);
14373
+ return { style, palette, themeFellBack };
14374
+ }
14375
+
11407
14376
  // src/pagination.ts
11408
14377
  async function* paginate(fetchPage, getItems, getTotalCount, options) {
11409
14378
  const pageSize = options?.pageSize ?? 100;
@@ -11459,6 +14428,12 @@ var Chrt = class {
11459
14428
  * requires `manifestPath`.
11460
14429
  */
11461
14430
  offline;
14431
+ /**
14432
+ * CHRTinteract — click-to-inspect: `attach(map)` for wired popups/events,
14433
+ * `inspect(feature)` for headless decode. Included with the Dev plan and
14434
+ * above; enforced server-side at attach/inspect time.
14435
+ */
14436
+ interact;
11462
14437
  featuresEp;
11463
14438
  queryEp;
11464
14439
  http;
@@ -11471,13 +14446,14 @@ var Chrt = class {
11471
14446
  this.tileBaseUrl = config.tileBaseUrl ?? DEFAULT_TILE_BASE_URL;
11472
14447
  this.http = new HttpClient(config.apiKey, config.baseUrl, config.retries);
11473
14448
  const tileOpts = { region: this.region, tileBaseUrl: this.tileBaseUrl };
11474
- this.tiles = new TilesEndpoint(this.http, tileOpts);
14449
+ this.tileAuth = new TileAuth(this.http, tileOpts);
14450
+ this.tiles = new TilesEndpoint(this.http, this.tileAuth, tileOpts);
11475
14451
  this.cells = new CellsEndpoint(this.http);
11476
14452
  this.coverage = new CoverageEndpoint(this.http);
11477
14453
  this.featuresEp = new FeaturesEndpoint(this.http);
11478
14454
  this.queryEp = new QueryEndpoint(this.http);
11479
14455
  this.offline = new OfflineEndpoint(this.http, config.deviceId, config.manifestPath);
11480
- this.tileAuth = new TileAuth(this.http, tileOpts);
14456
+ this.interact = new InteractClient(this.http);
11481
14457
  }
11482
14458
  /* ── Convenience shortcuts ───────────────────────── */
11483
14459
  /** Query GeoJSON features by bounding box */
@@ -11571,6 +14547,9 @@ var Chrt = class {
11571
14547
  * });
11572
14548
  *
11573
14549
  * @remarks S-101 portrayal is on the roadmap; today `style()` emits S-52.
14550
+ * Generated locally, so it never applies a key-bound CHRTstyle custom
14551
+ * style — use {@link styleUrl} / {@link fetchStyle} (the served Style API)
14552
+ * for that.
11574
14553
  */
11575
14554
  style(opts = {}) {
11576
14555
  const { palette = "DAY", ...styleOpts } = opts;
@@ -11581,12 +14560,33 @@ var Chrt = class {
11581
14560
  });
11582
14561
  }
11583
14562
  /**
11584
- * @deprecated CHRT serves no remote `style.json`. Use {@link style} to get a
11585
- * MapLibre style object instead. Throws to avoid silently pointing a map at a
11586
- * dead URL.
14563
+ * URL of the served S-52 style — the CHRTstyle Style API at
14564
+ * `tiles.chrt.co/v1/styles/{region}/style.json`. Unlike {@link style}
14565
+ * (generated locally), the served document honors the API key's bound
14566
+ * CHRTstyle custom style: palettes, layer visibility, label/icon scale,
14567
+ * and the rest of the platform's Map Styles settings, applied server-side
14568
+ * and tier-gated (custom styles are Dev+; Free keys get the default
14569
+ * themes). Async because it pre-mints the tile token, so the style
14570
+ * request is guaranteed to carry auth via `transformRequest()`.
14571
+ *
14572
+ * @example
14573
+ * const map = new maplibregl.Map({
14574
+ * container: 'map',
14575
+ * style: await chrt.styleUrl({ theme: 'day' }),
14576
+ * transformRequest: chrt.transformRequest(),
14577
+ * });
14578
+ */
14579
+ async styleUrl(opts) {
14580
+ return this.tiles.styleUrl(opts);
14581
+ }
14582
+ /**
14583
+ * Fetch the served S-52 style (see {@link styleUrl}) as a MapLibre style
14584
+ * object — inspect or mutate it before handing it to the map.
14585
+ * `chrt.style()` remains the local generator that needs no network
14586
+ * round-trip (and therefore applies no custom style).
11587
14587
  */
11588
- styleUrl() {
11589
- return this.tiles.styleUrl();
14588
+ async fetchStyle(opts) {
14589
+ return this.tiles.fetchStyle(opts);
11590
14590
  }
11591
14591
  /**
11592
14592
  * Returns a `transformRequest` for Mapbox GL JS / MapLibre GL JS that mints a
@@ -11615,8 +14615,12 @@ var Chrt = class {
11615
14615
  };
11616
14616
  // Annotate the CommonJS export names for ESM import in node:
11617
14617
  0 && (module.exports = {
14618
+ ALL_NAV_AID_LAYERS,
14619
+ ALL_WATER_BODY_LAYERS,
14620
+ AREA_QUERY_LAYERS,
11618
14621
  BAND_DRAW_ORDER,
11619
14622
  BAND_PRIORITY_RANGES,
14623
+ BRIDGE_QUERY_LAYERS,
11620
14624
  BUILD_MAXZOOM,
11621
14625
  BUILD_MINZOOM,
11622
14626
  CHART_BAND_CODES,
@@ -11624,12 +14628,25 @@ var Chrt = class {
11624
14628
  CHART_SECTIONS,
11625
14629
  Chrt,
11626
14630
  ChrtError,
14631
+ DEFAULT_CUSTOM_STYLE_CONFIG,
14632
+ DEFAULT_LABEL_SETTINGS,
14633
+ DEFAULT_NAV_AID_SETTINGS,
11627
14634
  DEFAULT_REGION,
14635
+ DEFAULT_SOUNDING_SETTINGS,
11628
14636
  DEFAULT_TILE_BASE_URL,
11629
14637
  DEFAULT_TILE_REGION,
14638
+ DEFAULT_WATER_BODY_SETTINGS,
14639
+ InteractClient,
14640
+ InteractError,
11630
14641
  LABELED_LAYERS,
11631
14642
  LABEL_BEHAVIOR,
14643
+ LABEL_LAYER_GROUPS,
14644
+ LABEL_PRIORITY,
11632
14645
  NAUTICAL_SYMBOLS,
14646
+ NAV_AID_LAYER_GROUPS,
14647
+ NAV_AID_QUERY_LAYERS,
14648
+ NON_SOLAS_DISCLAIMER,
14649
+ NON_SOLAS_DISCLAIMER_SHORT,
11633
14650
  OfflineError,
11634
14651
  OfflineStyleError,
11635
14652
  S101_CHART_SECTIONS,
@@ -11642,21 +14659,34 @@ var Chrt = class {
11642
14659
  S52_DUSK,
11643
14660
  S52_LOOKUP,
11644
14661
  S52_NIGHT,
14662
+ SOUNDING_LAYERS,
11645
14663
  SYMBOL_GROUPS,
11646
14664
  SYMBOL_MAP,
11647
14665
  TileAuth,
11648
14666
  TileTokenError,
14667
+ WATER_BODY_LAYER_GROUPS,
11649
14668
  applyColorTheme,
14669
+ applyCustomStyleSettings,
11650
14670
  bandFilterExpression,
14671
+ buildFlashPattern,
11651
14672
  buildLabelLayer,
11652
14673
  buildLayersForClass,
11653
14674
  buildOfflineStyle,
14675
+ buildSectorArcGeoJSON,
14676
+ buildSectorArcsFC,
14677
+ buildSectorsFromFeatures,
11654
14678
  buildSources,
11655
14679
  combineWithBandFilter,
11656
14680
  daymarkColor,
11657
14681
  decimateFeatures,
14682
+ decodeAreaFeatureProps,
14683
+ decodeBridgeProps,
14684
+ decodeNavAidProps,
11658
14685
  depareColor,
11659
14686
  depareFillExpression,
14687
+ deriveMarinerName,
14688
+ extractNavAidDesignation,
14689
+ generateCustomStyle,
11660
14690
  generateSpriteManifest,
11661
14691
  generateSpriteSheet,
11662
14692
  generateStyle,
@@ -11671,6 +14701,7 @@ var Chrt = class {
11671
14701
  getIconExpression,
11672
14702
  getLabelBehavior,
11673
14703
  getLabelConfig,
14704
+ getLabelPriority,
11674
14705
  getLookupEntry,
11675
14706
  getMaxCellsForZoom,
11676
14707
  getOrderedClasses,
@@ -11679,22 +14710,33 @@ var Chrt = class {
11679
14710
  getStyleStats,
11680
14711
  getSymbolSvgString,
11681
14712
  getThemedS52Svg,
14713
+ getTopmarkMeaning,
11682
14714
  hazardColor,
11683
14715
  hazardFillExpression,
14716
+ isLabelLayerVisible,
11684
14717
  isNavAidLayer,
14718
+ isNavAidLayerVisible,
14719
+ isSoundingLayerVisible,
11685
14720
  isVisibleInMode,
14721
+ isWaterBodyLayerVisible,
14722
+ labelSortKey,
11686
14723
  lateralColor,
11687
14724
  lateralFillExpression,
11688
14725
  lightColor,
11689
14726
  lightFillExpression,
14727
+ normalizeTheme,
11690
14728
  paginate,
14729
+ parseListCodes,
11691
14730
  parseWebhook,
11692
14731
  preprocessLights,
11693
14732
  preprocessNavAids,
11694
14733
  preprocessSoundings,
11695
14734
  preprocessWaterBodies,
11696
14735
  resolveColor,
14736
+ resolveCustomTheme,
11697
14737
  resolveStyle,
14738
+ sanitizeCustomStyleConfig,
14739
+ scaleSizeValue,
11698
14740
  scaminToMinZoom,
11699
14741
  withinBboxFilter,
11700
14742
  worldMinusBboxPolygon