@chrtco/sdk 0.10.46 → 0.10.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2057,6 +2057,19 @@ function s57AbbrTable(acronym) {
2057
2057
  }
2058
2058
 
2059
2059
  // src/atlas/s52-colors.ts
2060
+ var EINK_COLOR_TABLES = { color: "EINK_COLOR", mono: "EINK_MONO", night: "EINK_NIGHT" };
2061
+ function isEinkColorTable(ct) {
2062
+ return ct === "EINK_COLOR" || ct === "EINK_MONO" || ct === "EINK_NIGHT";
2063
+ }
2064
+ function isEinkGreyTable(ct) {
2065
+ return ct === "EINK_MONO" || ct === "EINK_NIGHT";
2066
+ }
2067
+ var EINK_MONO_GREYS = {
2068
+ INK: "#000000",
2069
+ DARK: "#555555",
2070
+ MID: "#aaaaaa",
2071
+ PAPER: "#ffffff"
2072
+ };
2060
2073
  var S52_DAY = {
2061
2074
  // Depth
2062
2075
  NODTA: "#a4a4a4",
@@ -2337,11 +2350,235 @@ var S52_NIGHT = {
2337
2350
  RES02: "#383028",
2338
2351
  RES03: "#383028"
2339
2352
  };
2353
+ var S52_EINK_COLOR = {
2354
+ NODTA: "#b4b4b4",
2355
+ DEPIT: "#4f8f3a",
2356
+ DEPVS: "#74ade0",
2357
+ DEPMS: "#bad6ef",
2358
+ DEPMD: "#ffffff",
2359
+ DEPDW: "#ffffff",
2360
+ LANDA: "#f2e2b0",
2361
+ LANDF: "#c49a4a",
2362
+ CHBRN: "#8b5a2b",
2363
+ CHGRF: "#9a9a9a",
2364
+ CHGRD: "#444444",
2365
+ CHBLK: "#000000",
2366
+ CSTLN: "#000000",
2367
+ OUTLW: "#000000",
2368
+ BKAJ1: "#333333",
2369
+ TRFCD: "#c800c8",
2370
+ TRFCF: "#c800c8",
2371
+ RESBL: "#0050c8",
2372
+ APTS1: "#c800c8",
2373
+ DNGHL: "#e00000",
2374
+ ISHDG: "#e00000",
2375
+ LITRD: "#e00000",
2376
+ LITGN: "#00a000",
2377
+ LITYW: "#f0c000",
2378
+ LITWT: "#ffffff",
2379
+ LITBL: "#0050c8",
2380
+ LITOR: "#f07000",
2381
+ RADHI: "#00a000",
2382
+ RADLO: "#006000",
2383
+ PLRTE: "#e00000",
2384
+ ARPAT: "#008040",
2385
+ ISDNG: "#c800c8",
2386
+ CHRED: "#e00000",
2387
+ CHGRN: "#00a000",
2388
+ CHYLW: "#f0c000",
2389
+ CHMGD: "#c800c8",
2390
+ CHMGF: "#c800c8",
2391
+ CHWHT: "#ffffff",
2392
+ CHCOR: "#f07000",
2393
+ // Deeper-than-safety soundings: dark grey, the one secondary ink. Shoal
2394
+ // soundings are black — the louder numeral, as in every other table.
2395
+ SNDG1: "#555555",
2396
+ SNDG2: "#000000",
2397
+ DEPSC: "#000000",
2398
+ DEPCN: "#555555",
2399
+ UINFD: "#000000",
2400
+ UINFF: "#555555",
2401
+ UINFR: "#e00000",
2402
+ UINFG: "#00a000",
2403
+ UINFO: "#f07000",
2404
+ UINFB: "#0050c8",
2405
+ UINFM: "#c800c8",
2406
+ UIBCK: "#ffffff",
2407
+ UIBDR: "#555555",
2408
+ UIAFD: "#74ade0",
2409
+ UIAFF: "#c49a4a",
2410
+ PIPLN: "#008000",
2411
+ OUTLL: "#c49a4a",
2412
+ SCLBR: "#f07000",
2413
+ NINFO: "#f07000",
2414
+ ADINF: "#b08800",
2415
+ RESGR: "#555555",
2416
+ SHIPS: "#000000",
2417
+ PSTRK: "#000000",
2418
+ SYTRK: "#555555",
2419
+ APLRT: "#f07000",
2420
+ CURSR: "#f07000",
2421
+ BKAJ2: "#000000",
2422
+ RES01: "#808080",
2423
+ RES02: "#808080",
2424
+ RES03: "#808080"
2425
+ };
2426
+ var M = EINK_MONO_GREYS;
2427
+ var S52_EINK_MONO = {
2428
+ // No data is DARK, never the land tone: a no-data edge in the land tone
2429
+ // reads as a coastline (seen on the Boston render, 2026-10-05).
2430
+ NODTA: M.DARK,
2431
+ DEPIT: M.PAPER,
2432
+ DEPVS: M.PAPER,
2433
+ DEPMS: M.PAPER,
2434
+ DEPMD: M.PAPER,
2435
+ DEPDW: M.PAPER,
2436
+ LANDA: M.MID,
2437
+ LANDF: M.DARK,
2438
+ CHBRN: M.DARK,
2439
+ CHGRF: M.MID,
2440
+ CHGRD: M.DARK,
2441
+ CHBLK: M.INK,
2442
+ CSTLN: M.INK,
2443
+ OUTLW: M.INK,
2444
+ BKAJ1: M.DARK,
2445
+ TRFCD: M.INK,
2446
+ TRFCF: M.INK,
2447
+ RESBL: M.INK,
2448
+ APTS1: M.INK,
2449
+ DNGHL: M.INK,
2450
+ ISHDG: M.INK,
2451
+ LITRD: M.INK,
2452
+ LITGN: M.INK,
2453
+ LITYW: M.INK,
2454
+ LITWT: M.PAPER,
2455
+ LITBL: M.INK,
2456
+ LITOR: M.INK,
2457
+ RADHI: M.INK,
2458
+ RADLO: M.DARK,
2459
+ PLRTE: M.INK,
2460
+ ARPAT: M.INK,
2461
+ ISDNG: M.INK,
2462
+ CHRED: M.DARK,
2463
+ CHGRN: M.MID,
2464
+ CHYLW: M.MID,
2465
+ CHMGD: M.INK,
2466
+ CHMGF: M.INK,
2467
+ CHWHT: M.PAPER,
2468
+ CHCOR: M.MID,
2469
+ SNDG1: M.DARK,
2470
+ SNDG2: M.INK,
2471
+ DEPSC: M.INK,
2472
+ DEPCN: M.DARK,
2473
+ UINFD: M.INK,
2474
+ UINFF: M.DARK,
2475
+ UINFR: M.INK,
2476
+ UINFG: M.INK,
2477
+ UINFO: M.INK,
2478
+ UINFB: M.INK,
2479
+ UINFM: M.INK,
2480
+ UIBCK: M.PAPER,
2481
+ UIBDR: M.DARK,
2482
+ UIAFD: M.MID,
2483
+ UIAFF: M.MID,
2484
+ PIPLN: M.INK,
2485
+ OUTLL: M.DARK,
2486
+ SCLBR: M.INK,
2487
+ NINFO: M.INK,
2488
+ ADINF: M.DARK,
2489
+ RESGR: M.DARK,
2490
+ SHIPS: M.INK,
2491
+ PSTRK: M.INK,
2492
+ SYTRK: M.DARK,
2493
+ APLRT: M.INK,
2494
+ CURSR: M.INK,
2495
+ BKAJ2: M.INK,
2496
+ RES01: M.DARK,
2497
+ RES02: M.DARK,
2498
+ RES03: M.DARK
2499
+ };
2500
+ var N = { GROUND: M.INK, DIM: M.DARK, LINE: M.MID, BRIGHT: M.PAPER };
2501
+ var S52_EINK_NIGHT = {
2502
+ NODTA: N.GROUND,
2503
+ DEPIT: N.GROUND,
2504
+ DEPVS: N.GROUND,
2505
+ DEPMS: N.GROUND,
2506
+ DEPMD: N.GROUND,
2507
+ DEPDW: N.GROUND,
2508
+ LANDA: N.DIM,
2509
+ LANDF: N.LINE,
2510
+ CHBRN: N.LINE,
2511
+ CHGRF: N.DIM,
2512
+ CHGRD: N.LINE,
2513
+ CHBLK: N.LINE,
2514
+ CSTLN: N.LINE,
2515
+ OUTLW: N.LINE,
2516
+ BKAJ1: N.DIM,
2517
+ TRFCD: N.LINE,
2518
+ TRFCF: N.LINE,
2519
+ RESBL: N.LINE,
2520
+ APTS1: N.LINE,
2521
+ DNGHL: N.BRIGHT,
2522
+ ISHDG: N.BRIGHT,
2523
+ LITRD: N.BRIGHT,
2524
+ LITGN: N.BRIGHT,
2525
+ LITYW: N.BRIGHT,
2526
+ LITWT: N.BRIGHT,
2527
+ LITBL: N.BRIGHT,
2528
+ LITOR: N.BRIGHT,
2529
+ RADHI: N.LINE,
2530
+ RADLO: N.DIM,
2531
+ PLRTE: N.LINE,
2532
+ ARPAT: N.LINE,
2533
+ ISDNG: N.BRIGHT,
2534
+ // Mark bodies by tone, as in mono, so they stay apart from each other and
2535
+ // from the black water; shape still says which side.
2536
+ CHRED: N.DIM,
2537
+ CHGRN: N.LINE,
2538
+ CHYLW: N.LINE,
2539
+ CHMGD: N.LINE,
2540
+ CHMGF: N.LINE,
2541
+ CHWHT: N.BRIGHT,
2542
+ CHCOR: N.LINE,
2543
+ SNDG1: N.LINE,
2544
+ SNDG2: N.BRIGHT,
2545
+ DEPSC: N.BRIGHT,
2546
+ DEPCN: N.LINE,
2547
+ UINFD: N.LINE,
2548
+ UINFF: N.DIM,
2549
+ UINFR: N.BRIGHT,
2550
+ UINFG: N.LINE,
2551
+ UINFO: N.LINE,
2552
+ UINFB: N.LINE,
2553
+ UINFM: N.LINE,
2554
+ UIBCK: N.GROUND,
2555
+ UIBDR: N.DIM,
2556
+ UIAFD: N.DIM,
2557
+ UIAFF: N.DIM,
2558
+ PIPLN: N.LINE,
2559
+ OUTLL: N.DIM,
2560
+ SCLBR: N.LINE,
2561
+ NINFO: N.LINE,
2562
+ ADINF: N.DIM,
2563
+ RESGR: N.DIM,
2564
+ SHIPS: N.BRIGHT,
2565
+ PSTRK: N.LINE,
2566
+ SYTRK: N.DIM,
2567
+ APLRT: N.BRIGHT,
2568
+ CURSR: N.BRIGHT,
2569
+ BKAJ2: N.GROUND,
2570
+ RES01: N.DIM,
2571
+ RES02: N.DIM,
2572
+ RES03: N.DIM
2573
+ };
2340
2574
  var S52_COLORS = S52_DAY;
2341
2575
  var COLOR_TABLES = {
2342
2576
  DAY: S52_DAY,
2343
2577
  DUSK: S52_DUSK,
2344
- NIGHT: S52_NIGHT
2578
+ NIGHT: S52_NIGHT,
2579
+ EINK_COLOR: S52_EINK_COLOR,
2580
+ EINK_MONO: S52_EINK_MONO,
2581
+ EINK_NIGHT: S52_EINK_NIGHT
2345
2582
  };
2346
2583
  function getColorTable(name) {
2347
2584
  return COLOR_TABLES[name];
@@ -4988,6 +5225,254 @@ var InteractClient = class {
4988
5225
  }
4989
5226
  };
4990
5227
 
5228
+ // src/atlas/chrt-svgs-generated.ts
5229
+ var CHRT_GLYPH_SVGS = {
5230
+ "badge-radar": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="19" r="2.6" fill="#D200D2"/><path d="M9 19a7 7 0 0 1 14 0M5 19a11 11 0 0 1 22 0" stroke="#D200D2" stroke-width="2.4" fill="none" stroke-linecap="round" stroke-dasharray="4 3"/></svg>',
5231
+ "badge-sound": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="7" cy="16" r="2.2" fill="#D200D2"/><path d="M12 10a9 9 0 0 1 0 12M16.5 6.5a15 15 0 0 1 0 19M21 3a21 21 0 0 1 0 26" stroke="#D200D2" stroke-width="2.4" fill="none" stroke-linecap="round"/></svg>',
5232
+ "beacon-cairn": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="11" r="4" fill="#fff"/><circle cx="11.5" cy="19" r="4.6" fill="#fff"/><circle cx="20.5" cy="19" r="4.6" fill="#fff"/><path d="M7 28h18" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
5233
+ "beacon-generic": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 25V12" stroke="#fff" stroke-width="2.6" stroke-linecap="round"/><path d="M16 4l6 8H10Z" fill="#fff"/><path d="M8 28h16M11 25h10" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
5234
+ "beacon-lattice": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M11 25 13 6h6l2 19M12 20h8M12.6 13h6.8M11 25l9-12M21 25l-9-12" stroke="#fff" stroke-width="1.8" stroke-linecap="round" fill="none"/><path d="M8 28h16" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
5235
+ "beacon-stake": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 25V6" stroke="#fff" stroke-width="2.8" stroke-linecap="round"/><path d="M9 28h14" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
5236
+ "beacon-tower": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M12 6h8l1.6 19H10.4Z" fill="#fff"/><path d="M10 6h12" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/><path d="M8 28h16" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
5237
+ "buoy-barrel": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect x="7" y="13" width="18" height="11" rx="5.5" fill="#fff"/><path d="M6 28h20" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
5238
+ "buoy-can": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M9 12h14v11a2 2 0 0 1-2 2H11a2 2 0 0 1-2-2Z" fill="#fff"/><path d="M6 28h20" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
5239
+ "buoy-conical": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 8 24.5 25H7.5Z" fill="#fff"/><path d="M6 28h20" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
5240
+ "buoy-pillar": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M13 6h6l1.4 19H11.6Z" fill="#fff"/><path d="M6 28h20" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
5241
+ "buoy-spar": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M14.5 5h3l1 20h-5Z" fill="#fff"/><path d="M6 28h20" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
5242
+ "buoy-spherical": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="17.5" r="8.5" fill="#fff"/><path d="M6 28h20" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
5243
+ "buoy-super": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M7 17h18l-2.5 8h-13Z" fill="#fff"/><path d="M16 17V8" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/><circle cx="16" cy="6.5" r="2" fill="#fff"/><path d="M5 28h22" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
5244
+ "cardinal-e": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 1.5 20 6h-8Z" fill="#101418"/><path d="M16 11 12 6.5h8Z" fill="#101418"/><path d="M13 12h6l1 13H12Z" fill="#FFC805"/><path d="M6 28h20" stroke="#101418" stroke-width="2.4" stroke-linecap="round"/></svg>',
5245
+ "cardinal-n": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 1.5 20 6h-8Z" fill="#101418"/><path d="M16 6.5 20 11h-8Z" fill="#101418"/><path d="M13 12h6l1 13H12Z" fill="#FFC805"/><path d="M6 28h20" stroke="#101418" stroke-width="2.4" stroke-linecap="round"/></svg>',
5246
+ "cardinal-s": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 11 12 6.5h8Z" fill="#101418"/><path d="M16 6 12 1.5h8Z" fill="#101418"/><path d="M13 12h6l1 13H12Z" fill="#FFC805"/><path d="M6 28h20" stroke="#101418" stroke-width="2.4" stroke-linecap="round"/></svg>',
5247
+ "cardinal-w": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 6 12 1.5h8Z" fill="#101418"/><path d="M16 6.5 20 11h-8Z" fill="#101418"/><path d="M13 12h6l1 13H12Z" fill="#FFC805"/><path d="M6 28h20" stroke="#101418" stroke-width="2.4" stroke-linecap="round"/></svg>',
5248
+ "daymark-square": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect x="6" y="6" width="20" height="20" rx="1.5" fill="#fff"/></svg>',
5249
+ "daymark-triangle": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 5 28 26H4Z" fill="#fff"/></svg>',
5250
+ "eink-danger": '<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 8 8"><path d="M-2 2 L2 -2 M0 8 L8 0 M6 10 L10 6" stroke="#000000" stroke-width="1" fill="none"/><path d="M-2 6 L2 10 M0 0 L8 8 M6 -2 L10 2" stroke="#000000" stroke-width="1" fill="none"/></svg>',
5251
+ "eink-dots": '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10"><circle cx="2.5" cy="2.5" r="0.9" fill="#000000"/><circle cx="7.5" cy="7.5" r="0.9" fill="#000000"/></svg>',
5252
+ "eink-hatch": '<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 8 8"><path d="M-2 2 L2 -2 M0 8 L8 0 M6 10 L10 6" stroke="#000000" stroke-width="0.9" fill="none"/></svg>',
5253
+ "hatch-nocoverage": '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M-3 9 L9 -3 M-3 21 L21 -3 M3 27 L27 3 M15 27 L27 15" stroke="#8A97A0" stroke-width="0.8" fill="none"/><path d="M-3 15 L15 33 M-3 3 L21 27 M3 -3 L27 21 M15 -3 L27 9" stroke="#8A97A0" stroke-width="0.8" fill="none"/></svg>',
5254
+ "hatch-uncharted": '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M-3 9 L9 -3 M-3 21 L21 -3 M3 27 L27 3 M15 27 L27 15" stroke="#5B7383" stroke-width="0.65" fill="none"/></svg>',
5255
+ "isolated-danger": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="3.5" r="2.4" fill="#101418"/><circle cx="16" cy="9.5" r="2.4" fill="#101418"/><path d="M13 13h6l1 12H12Z" fill="#101418"/><rect x="12.4" y="17.5" width="7.2" height="3.4" fill="#E5484D"/><path d="M6 28h20" stroke="#101418" stroke-width="2.4" stroke-linecap="round"/></svg>',
5256
+ "light-flare": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="11" cy="11" r="3.2" fill="#fff"/><path d="M13.4 13.4 24 19.5a2.6 2.6 0 0 1-4.5 4.5L13.4 13.4Z" fill="#fff"/></svg>',
5257
+ "lightship": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M6 20l3-5h14l3 5-3.5 4h-13Z" fill="#fff"/><path d="M16 15V8" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/><circle cx="16" cy="6" r="2.2" fill="#fff"/><path d="M5 28h22" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
5258
+ "safe-water": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="6" r="2.4" fill="#E5484D"/><circle cx="16" cy="17.5" r="8" fill="#F8FAFC"/><path d="M12.2 10.5a8 8 0 0 0 0 14M19.8 10.5a8 8 0 0 1 0 14" stroke="#E5484D" stroke-width="3" fill="none"/><path d="M6 28h20" stroke="#101418" stroke-width="2.4" stroke-linecap="round"/></svg>'
5259
+ };
5260
+
5261
+ // src/atlas/s52-pattern.ts
5262
+ var S52_PATTERN_SPRITE_PREFIX = "AP:";
5263
+ function patternSpriteId(patternName) {
5264
+ return `${S52_PATTERN_SPRITE_PREFIX}${patternName}`;
5265
+ }
5266
+
5267
+ // src/atlas/s52-conditional.ts
5268
+ var DEFAULT_SAFETY_CONTOUR_M = 10;
5269
+ var DEPARE_DEEP_CUT_M = 18.2;
5270
+ var DEPARE_SHALLOW_CUT_M = 2;
5271
+ function effectiveSafetyContour(safetyContourM) {
5272
+ if (!Number.isFinite(safetyContourM)) return DEFAULT_SAFETY_CONTOUR_M;
5273
+ return Math.min(Math.max(safetyContourM, DEPARE_SHALLOW_CUT_M), DEPARE_DEEP_CUT_M);
5274
+ }
5275
+ function depareColor(props, ct = "DAY", safetyContourM = DEFAULT_SAFETY_CONTOUR_M) {
5276
+ const drval1 = Number(props.DRVAL1 ?? props.drval1 ?? -1);
5277
+ const drval2 = Number(props.DRVAL2 ?? props.drval2 ?? drval1);
5278
+ const safety = effectiveSafetyContour(safetyContourM);
5279
+ if (drval1 < 0 && drval2 <= 0) return resolveColor("DEPIT", ct);
5280
+ if (drval1 < DEPARE_SHALLOW_CUT_M) return resolveColor("DEPVS", ct);
5281
+ if (drval1 < safety) return resolveColor("DEPMS", ct);
5282
+ if (drval1 < DEPARE_DEEP_CUT_M) return resolveColor("DEPMD", ct);
5283
+ return resolveColor("DEPDW", ct);
5284
+ }
5285
+ var DEPARE_FILL_BRANCH_TOKENS = ["DEPIT", "DEPVS", "DEPMS", "DEPMD"];
5286
+ var DEPARE_FILL_FALLBACK_TOKEN = "DEPDW";
5287
+ function depareFillExpression(ct = "DAY", safetyContourM = DEFAULT_SAFETY_CONTOUR_M) {
5288
+ const safety = effectiveSafetyContour(safetyContourM);
5289
+ const [it, vs, ms, md] = DEPARE_FILL_BRANCH_TOKENS;
5290
+ return [
5291
+ "case",
5292
+ [
5293
+ "all",
5294
+ ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], 0],
5295
+ ["<=", ["coalesce", ["get", "DRVAL2"], ["get", "drval2"], 0], 0]
5296
+ ],
5297
+ resolveColor(it, ct),
5298
+ ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], DEPARE_SHALLOW_CUT_M],
5299
+ resolveColor(vs, ct),
5300
+ ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], safety],
5301
+ resolveColor(ms, ct),
5302
+ ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], DEPARE_DEEP_CUT_M],
5303
+ resolveColor(md, ct),
5304
+ resolveColor(DEPARE_FILL_FALLBACK_TOKEN, ct)
5305
+ ];
5306
+ }
5307
+ function listAttrString(attr) {
5308
+ return ["to-string", ["coalesce", ["get", attr], ["get", attr.toLowerCase()], ""]];
5309
+ }
5310
+ function listAttrCase(attr, cases, fallback) {
5311
+ const attrStr = listAttrString(attr);
5312
+ const expr = ["case"];
5313
+ for (const [code, value] of cases) {
5314
+ expr.push([">=", ["index-of", `"${code}"`, attrStr], 0], value);
5315
+ }
5316
+ for (const [code, value] of cases) {
5317
+ expr.push(["==", attrStr, code], value);
5318
+ }
5319
+ for (const [code, value] of cases) {
5320
+ expr.push(["==", attrStr, `[${code}]`], value);
5321
+ }
5322
+ expr.push(fallback);
5323
+ return expr;
5324
+ }
5325
+ function listAttrHas(attr, codes) {
5326
+ const attrStr = listAttrString(attr);
5327
+ const clauses = [];
5328
+ for (const code of codes) {
5329
+ clauses.push([">=", ["index-of", `"${code}"`, attrStr], 0]);
5330
+ clauses.push(["==", attrStr, code]);
5331
+ clauses.push(["==", attrStr, `[${code}]`]);
5332
+ }
5333
+ return ["any", ...clauses];
5334
+ }
5335
+ function colourCodeCase(cases, fallback) {
5336
+ return listAttrCase("COLOUR", cases, fallback);
5337
+ }
5338
+ function lightColor(props, ct = "DAY") {
5339
+ const raw = props.COLOUR ?? props.colour;
5340
+ const code = Array.isArray(raw) ? Number(raw[0]) : typeof raw === "string" && raw.startsWith("[") ? Number(JSON.parse(raw)[0]) : Number(raw ?? 0);
5341
+ switch (code) {
5342
+ case 1:
5343
+ return resolveColor("LITWT", ct);
5344
+ case 3:
5345
+ return resolveColor("LITRD", ct);
5346
+ case 4:
5347
+ return resolveColor("LITGN", ct);
5348
+ case 5:
5349
+ return resolveColor("LITBL", ct);
5350
+ case 6:
5351
+ return resolveColor("LITYW", ct);
5352
+ case 11:
5353
+ return resolveColor("LITOR", ct);
5354
+ default:
5355
+ return resolveColor("LITYW", ct);
5356
+ }
5357
+ }
5358
+ function lightFillExpression(ct = "DAY") {
5359
+ return colourCodeCase(
5360
+ [
5361
+ ["3", resolveColor("LITRD", ct)],
5362
+ ["4", resolveColor("LITGN", ct)],
5363
+ ["6", resolveColor("LITYW", ct)],
5364
+ ["11", resolveColor("LITOR", ct)],
5365
+ ["5", resolveColor("LITBL", ct)],
5366
+ ["1", resolveColor("LITWT", ct)]
5367
+ ],
5368
+ resolveColor("LITYW", ct)
5369
+ );
5370
+ }
5371
+ function lateralColor(props, ct = "DAY") {
5372
+ const catlam = Number(props.CATLAM ?? props.catlam ?? 0);
5373
+ if (catlam === 1) return resolveColor("LITRD", ct);
5374
+ if (catlam === 2) return resolveColor("LITGN", ct);
5375
+ return resolveColor("LITYW", ct);
5376
+ }
5377
+ function lateralFillExpression(ct = "DAY") {
5378
+ return [
5379
+ "match",
5380
+ ["coalesce", ["get", "CATLAM"], ["get", "catlam"], 0],
5381
+ 1,
5382
+ resolveColor("LITRD", ct),
5383
+ 2,
5384
+ resolveColor("LITGN", ct),
5385
+ resolveColor("LITYW", ct)
5386
+ ];
5387
+ }
5388
+ function hazardColor(props, ct = "DAY") {
5389
+ const watlev = Number(props.WATLEV ?? props.watlev ?? 0);
5390
+ const valsou = props.VALSOU ?? props.valsou;
5391
+ if (watlev === 1 || watlev === 2) return resolveColor("DNGHL", ct);
5392
+ if (valsou != null && Number(valsou) < 2) return resolveColor("DNGHL", ct);
5393
+ return resolveColor("RESBL", ct);
5394
+ }
5395
+ function hazardFillExpression(ct = "DAY") {
5396
+ return [
5397
+ "case",
5398
+ [
5399
+ "any",
5400
+ ["==", ["coalesce", ["get", "WATLEV"], ["get", "watlev"], 0], 1],
5401
+ ["==", ["coalesce", ["get", "WATLEV"], ["get", "watlev"], 0], 2]
5402
+ ],
5403
+ resolveColor("DNGHL", ct),
5404
+ [
5405
+ "all",
5406
+ ["has", "VALSOU"],
5407
+ ["<", ["get", "VALSOU"], 2]
5408
+ ],
5409
+ resolveColor("DNGHL", ct),
5410
+ resolveColor("RESBL", ct)
5411
+ ];
5412
+ }
5413
+ function vegatnPatternExpression() {
5414
+ return [
5415
+ "match",
5416
+ ["coalesce", ["get", "CATVEG"], ["get", "catveg"], 0],
5417
+ 1,
5418
+ patternSpriteId("VEGATN03"),
5419
+ // grass
5420
+ 3,
5421
+ patternSpriteId("VEGATN03"),
5422
+ // bush
5423
+ 4,
5424
+ patternSpriteId("VEGATN04"),
5425
+ // deciduous wood
5426
+ 5,
5427
+ patternSpriteId("VEGATN04"),
5428
+ // coniferous wood
5429
+ 6,
5430
+ patternSpriteId("VEGATN04"),
5431
+ // wood (mixed)
5432
+ 7,
5433
+ patternSpriteId("MARSHES1"),
5434
+ // mangroves
5435
+ 10,
5436
+ patternSpriteId("VEGATN03"),
5437
+ // scrub
5438
+ 11,
5439
+ patternSpriteId("VEGATN04"),
5440
+ // tree
5441
+ 12,
5442
+ patternSpriteId("VEGATN04"),
5443
+ // evergreen
5444
+ 16,
5445
+ patternSpriteId("MARSHES1"),
5446
+ // swamp
5447
+ 17,
5448
+ patternSpriteId("MARSHES1"),
5449
+ // marsh
5450
+ 21,
5451
+ patternSpriteId("VEGATN04"),
5452
+ // savannah
5453
+ patternSpriteId("VEGATN03")
5454
+ // default: grass pattern
5455
+ ];
5456
+ }
5457
+ function daymarkColor(props, ct = "DAY") {
5458
+ const raw = props.COLOUR ?? props.colour;
5459
+ const code = Array.isArray(raw) ? Number(raw[0]) : typeof raw === "string" && raw.startsWith("[") ? Number(JSON.parse(raw)[0]) : Number(raw ?? 0);
5460
+ switch (code) {
5461
+ case 1:
5462
+ return resolveColor("LITWT", ct);
5463
+ case 3:
5464
+ return resolveColor("LITRD", ct);
5465
+ case 4:
5466
+ return resolveColor("LITGN", ct);
5467
+ case 6:
5468
+ return resolveColor("LITYW", ct);
5469
+ case 11:
5470
+ return resolveColor("LITOR", ct);
5471
+ default:
5472
+ return resolveColor("LITGN", ct);
5473
+ }
5474
+ }
5475
+
4991
5476
  // src/atlas/palette-light.ts
4992
5477
  var HEX6 = /^#?([0-9a-f]{6})$/i;
4993
5478
  function channels(hex) {
@@ -5026,8 +5511,966 @@ function capLuminance(hex, ceiling) {
5026
5511
  }
5027
5512
  var NIGHT_INK = S52_NIGHT.CHBLK;
5028
5513
  var NIGHT_MUTED_INK = scaleLinearLight(S52_NIGHT.CHBLK, 0.43);
5029
-
5030
- // src/atlas/map-live.ts
5514
+ function einkMonoGrey(hex) {
5515
+ const l = relativeLuminance(hex);
5516
+ const g2 = EINK_MONO_GREYS;
5517
+ const steps = [g2.INK, g2.DARK, g2.MID, g2.PAPER].map((c) => [c, relativeLuminance(c)]);
5518
+ let best = steps[0];
5519
+ for (const step of steps) if (Math.abs(step[1] - l) < Math.abs(best[1] - l)) best = step;
5520
+ return best[0];
5521
+ }
5522
+ function einkNightGrey(hex) {
5523
+ const l = relativeLuminance(hex);
5524
+ const g2 = EINK_MONO_GREYS;
5525
+ const steps = [g2.DARK, g2.MID, g2.PAPER].map((c) => [c, relativeLuminance(c)]);
5526
+ let best = steps[0];
5527
+ for (const step of steps) if (Math.abs(step[1] - l) < Math.abs(best[1] - l)) best = step;
5528
+ return best[0];
5529
+ }
5530
+
5531
+ // src/atlas/chrt-style.ts
5532
+ var CHRT_ICON_PREFIX = "chrt:";
5533
+ var CHRT_SDF_GLYPHS = /* @__PURE__ */ new Set([
5534
+ "buoy-can",
5535
+ "buoy-conical",
5536
+ "buoy-spherical",
5537
+ "buoy-pillar",
5538
+ "buoy-spar",
5539
+ "buoy-barrel",
5540
+ "buoy-super",
5541
+ "beacon-generic",
5542
+ "beacon-tower",
5543
+ "beacon-lattice",
5544
+ "beacon-stake",
5545
+ "beacon-cairn",
5546
+ "light-flare",
5547
+ "lightship",
5548
+ "daymark-triangle",
5549
+ "daymark-square"
5550
+ ]);
5551
+ var CHRT_GLYPH_NAMES = new Set(Object.keys(CHRT_GLYPH_SVGS));
5552
+ var CHRT_ATON_COLORS = {
5553
+ "1": "#F8FAFC",
5554
+ // white
5555
+ "2": "#101418",
5556
+ // black
5557
+ "3": "#E5484D",
5558
+ // red
5559
+ "4": "#30A46C",
5560
+ // green
5561
+ "5": "#3E63DD",
5562
+ // blue
5563
+ "6": "#FFC805",
5564
+ // yellow
5565
+ "7": "#8B9298",
5566
+ // grey
5567
+ "8": "#A66B3A",
5568
+ // brown
5569
+ "9": "#FFB224",
5570
+ // amber
5571
+ "10": "#7E4CCB",
5572
+ // violet
5573
+ "11": "#F76B15",
5574
+ // orange
5575
+ "12": "#D671C1"
5576
+ // pink (magenta-ish)
5577
+ };
5578
+ var CHRT_UNKNOWN_TINT = "#8B9298";
5579
+ var HOUSE_BLACK = "#101418";
5580
+ function chrtHouseColor(dayHex, palette = "DAY") {
5581
+ if (palette === "DAY" || palette === "EINK_COLOR") return dayHex;
5582
+ if (palette === "EINK_MONO") return einkMonoGrey(dayHex);
5583
+ if (palette === "EINK_NIGHT") return dayHex.toLowerCase() === HOUSE_BLACK.toLowerCase() || dayHex.toLowerCase() === "#000000" ? "#aaaaaa" : einkNightGrey(dayHex);
5584
+ const isBlack = dayHex.toLowerCase() === HOUSE_BLACK.toLowerCase();
5585
+ if (palette === "DUSK") {
5586
+ return isBlack ? dayHex : capLuminance(dayHex, relativeLuminance(resolveColor("CHBLK", "DUSK")));
5587
+ }
5588
+ return isBlack ? NIGHT_INK : capLuminance(dayHex, relativeLuminance(NIGHT_MUTED_INK));
5589
+ }
5590
+ function themeChrtGlyph(svg3, palette = "DAY") {
5591
+ if (palette === "DAY" || palette === "EINK_COLOR") return svg3;
5592
+ return svg3.replace(
5593
+ /(fill|stroke)="(#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6})"/g,
5594
+ (_m, attr, hex) => {
5595
+ const six = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
5596
+ return `${attr}="${chrtHouseColor(six, palette)}"`;
5597
+ }
5598
+ );
5599
+ }
5600
+ function chrtAtonTintExpression(palette = "DAY") {
5601
+ const c = (code) => chrtHouseColor(CHRT_ATON_COLORS[code], palette);
5602
+ return colourCodeCase(
5603
+ [
5604
+ ["3", c("3")],
5605
+ ["4", c("4")],
5606
+ ["6", c("6")],
5607
+ ["11", c("11")],
5608
+ ["9", c("9")],
5609
+ ["5", c("5")],
5610
+ ["1", c("1")],
5611
+ ["2", c("2")],
5612
+ ["8", c("8")],
5613
+ ["10", c("10")],
5614
+ ["12", c("12")],
5615
+ ["7", c("7")]
5616
+ ],
5617
+ chrtHouseColor(CHRT_UNKNOWN_TINT, palette)
5618
+ );
5619
+ }
5620
+ function chrtLightTintExpression(palette = "DAY") {
5621
+ const c = (code) => chrtHouseColor(CHRT_ATON_COLORS[code], palette);
5622
+ return colourCodeCase(
5623
+ [
5624
+ ["3", c("3")],
5625
+ ["4", c("4")],
5626
+ ["6", c("6")],
5627
+ ["11", c("11")],
5628
+ ["9", c("9")],
5629
+ ["5", c("5")],
5630
+ ["1", c("1")]
5631
+ ],
5632
+ c("1")
5633
+ );
5634
+ }
5635
+ function scalar(attr) {
5636
+ return ["coalesce", ["get", attr], ["get", attr.toLowerCase()], 0];
5637
+ }
5638
+ var g = (name) => `${CHRT_ICON_PREFIX}${name}`;
5639
+ function buoyBodyExpression() {
5640
+ return [
5641
+ "match",
5642
+ scalar("BOYSHP"),
5643
+ 1,
5644
+ g("buoy-conical"),
5645
+ 2,
5646
+ g("buoy-can"),
5647
+ 3,
5648
+ g("buoy-spherical"),
5649
+ 4,
5650
+ g("buoy-pillar"),
5651
+ 5,
5652
+ g("buoy-spar"),
5653
+ 6,
5654
+ g("buoy-barrel"),
5655
+ 7,
5656
+ g("buoy-super"),
5657
+ 8,
5658
+ g("buoy-super"),
5659
+ g("buoy-pillar")
5660
+ ];
5661
+ }
5662
+ function beaconBodyExpression() {
5663
+ return [
5664
+ "match",
5665
+ scalar("BCNSHP"),
5666
+ 1,
5667
+ g("beacon-stake"),
5668
+ 2,
5669
+ g("beacon-stake"),
5670
+ 3,
5671
+ g("beacon-tower"),
5672
+ 4,
5673
+ g("beacon-lattice"),
5674
+ 5,
5675
+ g("beacon-stake"),
5676
+ 6,
5677
+ g("beacon-cairn"),
5678
+ g("beacon-generic")
5679
+ ];
5680
+ }
5681
+ function cardinalExpression() {
5682
+ return [
5683
+ "match",
5684
+ scalar("CATCAM"),
5685
+ 1,
5686
+ g("cardinal-n"),
5687
+ 2,
5688
+ g("cardinal-e"),
5689
+ 3,
5690
+ g("cardinal-s"),
5691
+ 4,
5692
+ g("cardinal-w"),
5693
+ g("cardinal-n")
5694
+ ];
5695
+ }
5696
+ var CHRT_ICON_EXPRESSIONS = {
5697
+ BOYLAT: buoyBodyExpression,
5698
+ BOYSPP: buoyBodyExpression,
5699
+ BOYINB: buoyBodyExpression,
5700
+ BOYCAR: cardinalExpression,
5701
+ BOYISD: () => g("isolated-danger"),
5702
+ BOYSAW: () => g("safe-water"),
5703
+ BCNLAT: beaconBodyExpression,
5704
+ BCNSPP: beaconBodyExpression,
5705
+ BCNCAR: cardinalExpression,
5706
+ BCNISD: () => g("isolated-danger"),
5707
+ BCNSAW: () => g("safe-water"),
5708
+ LIGHTS: () => g("light-flare"),
5709
+ LITFLT: () => g("lightship"),
5710
+ LITVES: () => g("lightship"),
5711
+ FOGSIG: () => g("badge-sound"),
5712
+ RTPBCN: () => g("badge-radar"),
5713
+ RDOSTA: () => g("badge-radar"),
5714
+ DAYMAR: () => [
5715
+ "match",
5716
+ scalar("TOPSHP"),
5717
+ 1,
5718
+ g("daymark-triangle"),
5719
+ 2,
5720
+ g("daymark-triangle"),
5721
+ 5,
5722
+ g("daymark-triangle"),
5723
+ g("daymark-square")
5724
+ ]
5725
+ };
5726
+ var TINTED_CLASSES = /* @__PURE__ */ new Set([
5727
+ "BOYLAT",
5728
+ "BOYSPP",
5729
+ "BOYINB",
5730
+ "BCNLAT",
5731
+ "BCNSPP",
5732
+ "DAYMAR"
5733
+ ]);
5734
+ var CHRT_HALO = { color: HOUSE_BLACK, width: 1 };
5735
+ var CHRT_LINE_DASH = {
5736
+ CBLSUB: { color: "CHMGD", width: 0.8, dash: [6, 3] },
5737
+ PIPSOL: { color: "PIPLN", width: 0.8, dash: [6, 3] }
5738
+ };
5739
+ function applyChrtSymbolSet(style, palette = "DAY") {
5740
+ for (const layer of style.layers) {
5741
+ if (layer.type === "line") {
5742
+ const cls = layer.metadata?.["s52:objectClass"];
5743
+ const dash = cls ? CHRT_LINE_DASH[cls] : void 0;
5744
+ const paint2 = layer.paint ?? {};
5745
+ if (dash && paint2["line-pattern"] != null) {
5746
+ delete paint2["line-pattern"];
5747
+ paint2["line-color"] = resolveColor(dash.color, palette);
5748
+ paint2["line-width"] = dash.width;
5749
+ paint2["line-dasharray"] = dash.dash;
5750
+ layer.paint = paint2;
5751
+ }
5752
+ continue;
5753
+ }
5754
+ if (layer.type !== "symbol") continue;
5755
+ const objectClass2 = layer.metadata?.["s52:objectClass"];
5756
+ if (!objectClass2 || !CHRT_ICON_EXPRESSIONS[objectClass2]) continue;
5757
+ const layout = layer.layout;
5758
+ if (!layout || layout["icon-image"] == null) continue;
5759
+ layout["icon-image"] = CHRT_ICON_EXPRESSIONS[objectClass2]();
5760
+ const paint = layer.paint ?? {};
5761
+ const haloColor = palette === "EINK_MONO" ? chrtHouseColor(HOUSE_BLACK, palette) : palette === "EINK_NIGHT" ? "#000000" : CHRT_HALO.color;
5762
+ if (TINTED_CLASSES.has(objectClass2)) {
5763
+ paint["icon-color"] = chrtAtonTintExpression(palette);
5764
+ paint["icon-halo-color"] = haloColor;
5765
+ paint["icon-halo-width"] = CHRT_HALO.width;
5766
+ } else if (objectClass2 === "LIGHTS") {
5767
+ paint["icon-color"] = chrtLightTintExpression(palette);
5768
+ paint["icon-halo-color"] = haloColor;
5769
+ paint["icon-halo-width"] = CHRT_HALO.width;
5770
+ }
5771
+ layer.paint = paint;
5772
+ }
5773
+ const metadata = style.metadata;
5774
+ metadata["chrt:symbol-set"] = "chrt";
5775
+ return style;
5776
+ }
5777
+
5778
+ // src/atlas/base-layers.ts
5779
+ var NOCOVERAGE_PATTERN = "chrt:hatch-nocoverage";
5780
+ var UNCHARTED_PATTERN = "chrt:hatch-uncharted";
5781
+ function inlandWaterColor(palette) {
5782
+ return (BASE_THEMES[palette] ?? BASE_THEMES.DAY).inlandWater;
5783
+ }
5784
+ var BASE_THEMES = {
5785
+ DAY: {
5786
+ // ── `earth` is the jurisdiction tan DIMMED (~0.55), not a blue-grey ──
5787
+ // Outside the jurisdiction the map still owes the reader "what is land".
5788
+ // State 3 in the header is "the geography, under a crosshatch", and until
5789
+ // 2026-08-31 that geography was unreadable: earth #2A3439 against sea
5790
+ // #1B2328 measured 1.25:1, so Hispaniola showed charted boxes with tan
5791
+ // land inside and nothing recognisable around them (owner report: "no
5792
+ // land painting down near the DR at some zooms"). Land is warm and sea is
5793
+ // cool because that is what land and sea are; dimming the JURISDICTION
5794
+ // tan rather than inventing a colour says the true thing — this is the
5795
+ // same land, we simply do not carry charts for it. Held against BOTH
5796
+ // neighbours: legible over sea, and never mistakable for the full tan of
5797
+ // a country we do carry. DAY 3.59:1 vs sea, 3.17:1 vs tan.
5798
+ earth: "#807761",
5799
+ water: "#1B2328",
5800
+ river: "#24313A",
5801
+ boundary: "#4A575E",
5802
+ jurisdiction: "#D4C69D",
5803
+ // CHRTland's own tan — one land colour
5804
+ inlandWater: "#B9CFDD",
5805
+ // muted, unmistakably not a charted blue
5806
+ // The uncharted hatch's stripe, held to 2.3:1 against `inlandWater` in
5807
+ // every palette (`inland-hatch-contrast.test.ts`) — the hatch says "we have
5808
+ // not charted this water", and stripes the eye cannot find say nothing.
5809
+ // DAY 3.08:1, darker than its ground. Also the colour the artwork file
5810
+ // itself is drawn in.
5811
+ inlandHatch: "#5B7383",
5812
+ // State furniture. These three were referenced by the layers but missing
5813
+ // from this table (and its type — the long-standing TS2339 trio), so the
5814
+ // lines shipped on MapLibre's DEFAULT black. Now deliberate: a hairline
5815
+ // a shade deeper than the tan, quiet caps, tan halo.
5816
+ stateLine: "#B1A077",
5817
+ stateLabel: "#8E8468",
5818
+ stateHalo: "#D4C69D",
5819
+ // Rough-lake furniture (owner ask 2026-08-28: "the lakes need labels,
5820
+ // and shorelines drawn"): a shoreline a shade deeper than the rough
5821
+ // water, and a name in the water's own voice, haloed by the water.
5822
+ inlandShore: "#8FA9BA",
5823
+ inlandLabel: "#54707F",
5824
+ inlandHalo: "#B9CFDD"
5825
+ },
5826
+ DUSK: {
5827
+ // ── Re-derived 2026-09-01: the basemap sits UNDER the chart ──────────
5828
+ // These were the DAY tones scaled by a constant, never measured against
5829
+ // the DUSK chart — so `jurisdiction` came out at L=0.506 against a chart
5830
+ // whose own land (LANDA #8a7d5e) is L=0.209, i.e. the context layer was
5831
+ // 2.4x brighter than the data layer. Now capped at the chart's own land.
5832
+ // The rest of the block rides the same 0.666 channel factor so every
5833
+ // relationship the palette already chose is preserved, only moved down.
5834
+ // `water` is deliberately NOT scaled: it is the reference the land/sea
5835
+ // floor is measured against.
5836
+ earth: "#544E3E",
5837
+ water: "#161D21",
5838
+ river: "#141B20",
5839
+ boundary: "#293135",
5840
+ jurisdiction: "#867D64",
5841
+ inlandWater: "#62717A",
5842
+ // Owner's choice, 2026-10-04: dark stripes, 2.37:1. The DAY stripe here
5843
+ // was 1.02:1 — invisible, so an uncharted lake read as plain water.
5844
+ inlandHatch: "#2E383F",
5845
+ // Label stays DARK here — the halo is still mid-toned, so 3:1 is reachable
5846
+ // downward and a bright label would be a new light source at dusk.
5847
+ stateLine: "#B5A987",
5848
+ stateLabel: "#38342A",
5849
+ stateHalo: "#867D64",
5850
+ inlandShore: "#4D5B67",
5851
+ inlandLabel: "#212629",
5852
+ inlandHalo: "#62717A"
5853
+ },
5854
+ NIGHT: {
5855
+ // ── Re-derived 2026-09-01 — the owner-reported night defect ──────────
5856
+ // `jurisdiction` was L=0.240 against a chart whose own land (LANDA
5857
+ // #3a3020) is L=0.031: the world basemap was 7.7x BRIGHTER than the chart
5858
+ // it sits under, so at z<11 the uncharted interior read as a pale
5859
+ // continent-sized slab on a night chart. Capped at the chart's own land.
5860
+ //
5861
+ // ⚠ `earth` AND `jurisdiction` CONVERGE HERE, and that is forced, not
5862
+ // sloppy. Holding earth 2:1 above the ocean AND 2:1 below the jurisdiction
5863
+ // tan requires jurisdiction >= L 0.174 — 5.6x the chart's own land. At
5864
+ // night the range between the ocean (L 0.006) and the cap (L 0.031) is too
5865
+ // compressed to carry two land tones. COVERAGE IS STILL SAID, by the
5866
+ // `chrt:hatch-nocoverage` crosshatch, which is a stronger signal than tone
5867
+ // and is the one thing that cannot be lost to palette compression.
5868
+ //
5869
+ // Labels invert to light-on-dark: 3:1 downward is unreachable off a halo
5870
+ // this close to black, and CHRTland's own night town labels already sit at
5871
+ // L=0.177 for exactly this reason.
5872
+ earth: "#333026",
5873
+ water: "#10161A",
5874
+ river: "#090D0F",
5875
+ boundary: "#131719",
5876
+ jurisdiction: "#353128",
5877
+ inlandWater: "#232A30",
5878
+ // The DAY stripe, lighter than this ground: 2.92:1, and L 0.16, under the
5879
+ // NIGHT muted-ink ceiling (`night-luminance.test.ts`). Red night draws the
5880
+ // NIGHT chart, so this is its stripe too.
5881
+ inlandHatch: "#5B7383",
5882
+ stateLine: "#484236",
5883
+ stateLabel: "#837963",
5884
+ stateHalo: "#353128",
5885
+ inlandShore: "#1C2227",
5886
+ inlandLabel: "#617484",
5887
+ inlandHalo: "#232A30"
5888
+ },
5889
+ // ── E-ink: uncharted is DARK, never paper ─────────────────────────────
5890
+ // On an e-ink chart, charted safe water is paper. Water outside coverage
5891
+ // must never read as that, so it is the darkest fill on the page — the
5892
+ // e-ink twin of DAY's dark carto — with the no-coverage crosshatch over it
5893
+ // (invariant 5: the coverage portrayal survives). Land outside coverage
5894
+ // keeps the chart's own land tone.
5895
+ EINK_COLOR: {
5896
+ earth: "#d8ccb0",
5897
+ water: "#4f5a62",
5898
+ river: "#4f5a62",
5899
+ boundary: "#000000",
5900
+ jurisdiction: "#f2e2b0",
5901
+ inlandWater: "#c8d6e0",
5902
+ // The house stripe, 3.4:1 on this rough water — strong enough for the
5903
+ // 150 ppi colour layer.
5904
+ inlandHatch: "#5B7383",
5905
+ stateLine: "#555555",
5906
+ stateLabel: "#000000",
5907
+ stateHalo: "#ffffff",
5908
+ inlandShore: "#555555",
5909
+ inlandLabel: "#000000",
5910
+ inlandHalo: "#ffffff"
5911
+ },
5912
+ // Mono: the same rule in the four greys. Uncharted water DARK under a MID
5913
+ // crosshatch; land MID; rough inland water PAPER under the uncharted hatch.
5914
+ EINK_MONO: {
5915
+ earth: "#aaaaaa",
5916
+ water: "#555555",
5917
+ river: "#555555",
5918
+ boundary: "#000000",
5919
+ jurisdiction: "#aaaaaa",
5920
+ inlandWater: "#ffffff",
5921
+ inlandHatch: "#555555",
5922
+ stateLine: "#555555",
5923
+ stateLabel: "#000000",
5924
+ stateHalo: "#ffffff",
5925
+ inlandShore: "#555555",
5926
+ inlandLabel: "#000000",
5927
+ inlandHalo: "#ffffff"
5928
+ },
5929
+ // Night: the inverse. Water outside coverage is the black ground UNDER the
5930
+ // light crosshatch — charted safe water is plain black, so the hatch is what
5931
+ // tells them apart, here as everywhere. Land the DIM grey; labels the line
5932
+ // grey on black.
5933
+ EINK_NIGHT: {
5934
+ earth: "#555555",
5935
+ water: "#000000",
5936
+ river: "#000000",
5937
+ boundary: "#aaaaaa",
5938
+ jurisdiction: "#555555",
5939
+ inlandWater: "#000000",
5940
+ inlandHatch: "#aaaaaa",
5941
+ stateLine: "#aaaaaa",
5942
+ stateLabel: "#aaaaaa",
5943
+ stateHalo: "#000000",
5944
+ inlandShore: "#aaaaaa",
5945
+ inlandLabel: "#aaaaaa",
5946
+ inlandHalo: "#000000"
5947
+ }
5948
+ };
5949
+ function baseLayers(opts = {}) {
5950
+ const palette = opts.palette ?? "DAY";
5951
+ const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
5952
+ const src = opts.sourceId ?? BASE_REGION;
5953
+ const L = (o) => o;
5954
+ return [
5955
+ // 1. Geography — the world as it is, in the quiet dark voice. The OCEAN
5956
+ // is a background, not a polygon: the archive ships land and lakes,
5957
+ // and "everything else is sea" is exactly what a background expresses
5958
+ // — geometry-free, gap-free at every antimeridian and pole. Without
5959
+ // it the sea outside coverage was bare S-52 NODTA, a light grey that
5960
+ // read as "charted ground" with a hatch on it.
5961
+ L({
5962
+ id: "chrtbase-ocean",
5963
+ type: "background",
5964
+ paint: { "background-color": T.water }
5965
+ }),
5966
+ L({
5967
+ id: "chrtbase-earth",
5968
+ type: "fill",
5969
+ source: src,
5970
+ "source-layer": "base_earth",
5971
+ paint: { "fill-color": T.earth }
5972
+ }),
5973
+ L({
5974
+ id: "chrtbase-water",
5975
+ type: "fill",
5976
+ source: src,
5977
+ "source-layer": "base_water",
5978
+ paint: { "fill-color": T.water }
5979
+ }),
5980
+ L({
5981
+ id: "chrtbase-river",
5982
+ type: "line",
5983
+ source: src,
5984
+ "source-layer": "base_river",
5985
+ minzoom: 9,
5986
+ paint: {
5987
+ "line-color": T.river,
5988
+ "line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.6, 14, 1.6]
5989
+ }
5990
+ }),
5991
+ L({
5992
+ id: "chrtbase-boundary",
5993
+ type: "line",
5994
+ source: src,
5995
+ "source-layer": "base_boundary",
5996
+ paint: { "line-color": T.boundary, "line-width": 0.6, "line-opacity": 0.8 }
5997
+ }),
5998
+ // 2. THE DEFAULT: no coverage, everywhere, as a geometry-free background.
5999
+ // Constant opacity by design — see the header. What kills it is being
6000
+ // painted OVER, never fading.
6001
+ L({
6002
+ id: "chrtbase-nocoverage",
6003
+ type: "background",
6004
+ paint: { "background-pattern": NOCOVERAGE_PATTERN, "background-opacity": 0.55 }
6005
+ }),
6006
+ // 3. The jurisdiction: countries whose charts CHRT carries go tan — the
6007
+ // same tan as CHRTland, so "land in our world" is ONE colour. Opaque,
6008
+ // so it takes the crosshatch off its ground entirely.
6009
+ L({
6010
+ id: "chrtbase-jurisdiction",
6011
+ type: "fill",
6012
+ source: src,
6013
+ "source-layer": "jurisdiction",
6014
+ paint: { "fill-color": T.jurisdiction }
6015
+ }),
6016
+ // 4. States — internal boundaries and names, inside the jurisdiction
6017
+ // only (owner decision 2026-08-28). A hairline a shade deeper than the
6018
+ // tan, and quiet letter-spaced caps that fade as harbour detail takes
6019
+ // over. The one deliberate exception to no-names-on-the-basemap:
6020
+ // ADMIN furniture, nothing else — see the fence test.
6021
+ L({
6022
+ id: "chrtbase-state-boundary",
6023
+ type: "line",
6024
+ source: src,
6025
+ "source-layer": "state_boundary",
6026
+ minzoom: 4,
6027
+ paint: {
6028
+ "line-color": T.stateLine,
6029
+ "line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1],
6030
+ "line-dasharray": [3, 2],
6031
+ "line-opacity": 0.8
6032
+ }
6033
+ }),
6034
+ // 5. Country borders, drawn to be HOISTED above the whole chart by the
6035
+ // generator (owner, 2026-08-31: "where there is charted land, can we
6036
+ // add country borders on top of everything? I want it clear where
6037
+ // there is a boarder."). Sourced from NE boundary_lines_land — land
6038
+ // borders only, never coastline — so riding on top cannot stroke a
6039
+ // coast. Dashed so it reads as an administrative line, not chart
6040
+ // linework.
6041
+ L({
6042
+ id: "chrtbase-country-boundary",
6043
+ type: "line",
6044
+ source: src,
6045
+ "source-layer": "country_boundary",
6046
+ paint: {
6047
+ "line-color": T.boundary,
6048
+ "line-width": ["interpolate", ["linear"], ["zoom"], 3, 0.8, 8, 1.4, 12, 2.2],
6049
+ "line-dasharray": [4, 2],
6050
+ "line-opacity": 0.9
6051
+ }
6052
+ }),
6053
+ L({
6054
+ id: "chrtbase-state-label",
6055
+ type: "symbol",
6056
+ source: src,
6057
+ "source-layer": "state_label",
6058
+ minzoom: 4,
6059
+ maxzoom: 10,
6060
+ layout: {
6061
+ "text-field": ["upcase", ["get", "name"]],
6062
+ "text-font": ["Open Sans Regular"],
6063
+ "text-size": ["interpolate", ["linear"], ["zoom"], 4, 9, 8, 12],
6064
+ "text-letter-spacing": 0.25,
6065
+ "text-allow-overlap": false,
6066
+ "text-padding": 8
6067
+ },
6068
+ paint: {
6069
+ "text-color": T.stateLabel,
6070
+ "text-halo-color": T.stateHalo,
6071
+ "text-halo-width": 1.2,
6072
+ "text-opacity": ["interpolate", ["linear"], ["zoom"], 4, 0.7, 8, 0.85, 10, 0.4]
6073
+ }
6074
+ })
6075
+ ];
6076
+ }
6077
+ function baseInlandWaterLayers(opts = {}) {
6078
+ const palette = opts.palette ?? "DAY";
6079
+ const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
6080
+ const src = opts.sourceId ?? BASE_REGION;
6081
+ const L = (o) => o;
6082
+ const NE_TIER = ["!", ["has", "kind"]];
6083
+ const PLANET_TIER = ["has", "kind"];
6084
+ const HANDOVER = 9;
6085
+ return [
6086
+ // ── The fill pair, SPLIT BY TIER at the handover ────────────────────────
6087
+ //
6088
+ // Both tiers share `inland_water` (tile-join concatenates the same layer
6089
+ // name), and an unsplit fill draws BOTH — which means the NE polygon is
6090
+ // painted over the planet one at every zoom. That is not a cosmetic
6091
+ // double-draw: NE 10m carries no interior rings, so its blob has no
6092
+ // ISLANDS in it, and painting it over the planet's holed polygons buries
6093
+ // every island in the lake under "uncharted water". Owner-reported at
6094
+ // Winnipesaukee z11.7 ("there is land (and islands) being covered by the
6095
+ // 'no chart here' boundary"), and measured: rendering the planet tier
6096
+ // alone brings back the whole archipelago — Rattlesnake, Bear, Long
6097
+ // Island, Diamond, the Barndoors — while the NE tier alone is a faceted
6098
+ // blob whose edges bear no relation to the shoreline drawn beneath it.
6099
+ //
6100
+ // So each tier owns its own zoom range, the same handover the shoreline
6101
+ // below uses. Known residue, measured over the whole archive: of the 141
6102
+ // NE lakes we still carry, 138 have planet water beneath them; the
6103
+ // exceptions are two unnamed western-Alaska tundra lakes and a 2e-5 deg²
6104
+ // Lake Michigan sliver, which lose their rough fill above z9 because the
6105
+ // planet has no `water` polygon there at all (checked against the raw
6106
+ // pre-clip NDJSON — not a clip artefact).
6107
+ L({
6108
+ id: "chrtbase-inland-water",
6109
+ type: "fill",
6110
+ source: src,
6111
+ "source-layer": "inland_water",
6112
+ filter: NE_TIER,
6113
+ maxzoom: HANDOVER,
6114
+ paint: { "fill-color": T.inlandWater }
6115
+ }),
6116
+ L({
6117
+ id: "chrtbase-inland-hatch",
6118
+ type: "fill",
6119
+ source: src,
6120
+ "source-layer": "inland_water",
6121
+ filter: NE_TIER,
6122
+ maxzoom: HANDOVER,
6123
+ paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
6124
+ }),
6125
+ L({
6126
+ id: "chrtbase-inland-water-detail",
6127
+ type: "fill",
6128
+ source: src,
6129
+ "source-layer": "inland_water",
6130
+ filter: PLANET_TIER,
6131
+ minzoom: HANDOVER,
6132
+ paint: { "fill-color": T.inlandWater }
6133
+ }),
6134
+ L({
6135
+ id: "chrtbase-inland-hatch-detail",
6136
+ type: "fill",
6137
+ source: src,
6138
+ "source-layer": "inland_water",
6139
+ filter: PLANET_TIER,
6140
+ minzoom: HANDOVER,
6141
+ paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
6142
+ }),
6143
+ // The rough shoreline (owner ask 2026-08-28). Drawn ABOVE the hatch so
6144
+ // the edge stays crisp through the texture, and BELOW the charted-water
6145
+ // repaint clones the generator splices in after it — so wherever the
6146
+ // chart repaints its own water, the rough NE10m line yields to the
6147
+ // chart's COALNE instead of double-striking it.
6148
+ //
6149
+ // NE-tier geometry ONLY (`kind` absent): the planet detail tier joins
6150
+ // the same source-layer from z9 as MANY polygons whose chunk-clip seams
6151
+ // rejoin invisibly as FILLS — but a line layer strokes every seam, and
6152
+ // Winnipesaukee rendered as a mesh of internal edges. Planet features
6153
+ // carry `kind` (the INLAND_KINDS filter's own field); NE's carry only
6154
+ // `name`, so the absence of `kind` IS the tier discriminator.
6155
+ L({
6156
+ id: "chrtbase-inland-shore",
6157
+ type: "line",
6158
+ source: src,
6159
+ "source-layer": "inland_water",
6160
+ filter: NE_TIER,
6161
+ // Hands the shoreline over to the planet tier at ITS floor. Both
6162
+ // tiers live in `inland_water` from z9 up (tile-join concatenates the
6163
+ // same layer name), and their FILLS merge invisibly — but two
6164
+ // outlines do not: NE 10m generalises Winnipesaukee's shape, so its
6165
+ // outline ran straight across the middle of the lake the planet had
6166
+ // drawn correctly around. The coarse line is right only while it is
6167
+ // the only shape we have.
6168
+ maxzoom: HANDOVER,
6169
+ paint: {
6170
+ "line-color": T.inlandShore,
6171
+ "line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1.1, 14, 1.6],
6172
+ "line-opacity": 0.9
6173
+ }
6174
+ }),
6175
+ // The PLANET tier's shoreline (owner ask 2026-08-31: "uncharted small
6176
+ // lakes render with no outline"). It reads its own `inland_shore` layer,
6177
+ // NOT `inland_water`, and that indirection is the whole fix: the mesh
6178
+ // that forced the `!has kind` fence above is a CLIP artefact — an MVT
6179
+ // feature arrives already cut to its own z10 tile, so stroking the
6180
+ // polygon strokes every tile boundary it crosses. The build now walks
6181
+ // those rings, drops the axis-aligned runs that sit on the read zoom's
6182
+ // tile grid, and emits what is left as lines (measured on one New
6183
+ // England chunk: 61 long axis-aligned mesh edges → 5, and the 5 survivors
6184
+ // are 0.02–0.5 tiles off the grid, i.e. real straight water edges).
6185
+ //
6186
+ // Thinner and quieter than the NE line: this is finer geometry describing
6187
+ // water nobody has surveyed, and approximate data should look
6188
+ // approximate.
6189
+ L({
6190
+ id: "chrtbase-inland-shore-detail",
6191
+ type: "line",
6192
+ source: src,
6193
+ "source-layer": "inland_shore",
6194
+ minzoom: HANDOVER,
6195
+ paint: {
6196
+ "line-color": T.inlandShore,
6197
+ "line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.5, 12, 0.9, 15, 1.2],
6198
+ "line-opacity": 0.85
6199
+ }
6200
+ }),
6201
+ // The lake's name (owner ask 2026-08-28) — the second scoped exception
6202
+ // to no-names-on-the-basemap, after the states: WATER-BODY names only.
6203
+ // No maxzoom: an uncharted lake keeps its name at every zoom; over
6204
+ // charted lakes the chart's own SEAARE label takes over at detail zooms
6205
+ // and collision culls the echo.
6206
+ //
6207
+ // The archive's `inland_water_label` anchors (v3+). v3 shipped ONE
6208
+ // PointOnSurface anchor per lake, and a big lake's single anchor pans
6209
+ // off-viewport — Winnipesaukee at z11.7 read nameless with its name two
6210
+ // tiles west of the screen (owner-reported). Labelling the polygons
6211
+ // instead is NOT the fix: tippecanoe's coalescing drops the NE polygon
6212
+ // from some z11 tiles (measured: 11/619/747 carries only planet pieces),
6213
+ // so a polygon-sourced label vanishes tile by tile. v4 therefore bakes a
6214
+ // deterministic anchor GRID into big lakes — the paper-chart convention
6215
+ // of repeating a name along a large water body — and this one layer
6216
+ // renders however many anchors are in view; collision culls the rest.
6217
+ // Planet-tier water-body NAMES (owner ask 2026-08-31: NE 10m names only
6218
+ // 113 US lakes). Emitted BELOW the NE layer on purpose — MapLibre places
6219
+ // symbols topmost-layer-first, so the big NE lake wins collision against
6220
+ // the pond beside it, and both lose to the chart's own labels (every ENC
6221
+ // class is emitted after this pair).
6222
+ //
6223
+ // The size threshold is OSM's own `min_zoom`, carried through the build:
6224
+ // Lake Winnipesaukee reads 10, Squam Lake 12, so a pond simply is not in
6225
+ // the filter until the user is close enough for it to be the answer to
6226
+ // "what is that water". `["zoom"]` in a filter evaluates at integer zoom
6227
+ // levels and removes the feature from placement entirely — verified
6228
+ // against maplibre-gl-native rather than assumed, because a text-size-0
6229
+ // gate would still claim a collision slot.
6230
+ //
6231
+ // The tiers are separate LAYERS, not a filtered split of one — see the
6232
+ // builder for why: an archive that a not-yet-deployed style would
6233
+ // mis-render cannot be promoted first, and this one has to be.
6234
+ L({
6235
+ id: "chrtbase-inland-label-detail",
6236
+ type: "symbol",
6237
+ source: src,
6238
+ "source-layer": "inland_water_label_detail",
6239
+ minzoom: HANDOVER,
6240
+ filter: [">=", ["zoom"], ["get", "min_zoom"]],
6241
+ layout: {
6242
+ "text-field": ["get", "name"],
6243
+ "text-font": ["Open Sans Regular"],
6244
+ "text-size": ["interpolate", ["linear"], ["zoom"], 9, 9, 12, 10.5, 15, 11],
6245
+ "text-max-width": 8,
6246
+ "text-allow-overlap": false,
6247
+ "text-padding": 6,
6248
+ // Bigger water first within the tier — min_zoom IS the size rank.
6249
+ "symbol-sort-key": ["get", "min_zoom"]
6250
+ },
6251
+ paint: {
6252
+ "text-color": T.inlandLabel,
6253
+ "text-halo-color": T.inlandHalo,
6254
+ "text-halo-width": 1,
6255
+ "text-opacity": 0.9
6256
+ }
6257
+ }),
6258
+ L({
6259
+ id: "chrtbase-inland-label",
6260
+ type: "symbol",
6261
+ source: src,
6262
+ "source-layer": "inland_water_label",
6263
+ minzoom: 5,
6264
+ layout: {
6265
+ "text-field": ["get", "name"],
6266
+ "text-font": ["Open Sans Regular"],
6267
+ "text-size": ["interpolate", ["linear"], ["zoom"], 5, 9.5, 10, 12],
6268
+ "text-max-width": 7,
6269
+ "text-allow-overlap": false,
6270
+ // Zoom-scaled spacing for the anchor GRID: at z8.9 the 0.08-deg
6271
+ // anchors sit ~54 px apart and a flat padding let six copies of
6272
+ // "Lake Ontario" crowd one screen (owner-reported 2026-08-30).
6273
+ // Wide collision boxes at low zoom thin the grid to the repeat-
6274
+ // by-convention cadence; by z12 the anchors are ~850 px apart and
6275
+ // the padding relaxes to normal.
6276
+ "text-padding": ["interpolate", ["linear"], ["zoom"], 5, 80, 9, 64, 12, 16]
6277
+ },
6278
+ paint: {
6279
+ "text-color": T.inlandLabel,
6280
+ "text-halo-color": T.inlandHalo,
6281
+ "text-halo-width": 1.1
6282
+ }
6283
+ })
6284
+ ];
6285
+ }
6286
+
6287
+ // src/atlas/eink-portrayal.ts
6288
+ var EINK_DOTS_PATTERN = `${CHRT_ICON_PREFIX}eink-dots`;
6289
+ var EINK_HATCH_PATTERN = `${CHRT_ICON_PREFIX}eink-hatch`;
6290
+ var EINK_DANGER_PATTERN = `${CHRT_ICON_PREFIX}eink-danger`;
6291
+ var EINK_LINE_SCALE = 1.5;
6292
+ var EINK_LINE_MIN_PX = 1;
6293
+ var EINK_TEXT_SCALE = 1.15;
6294
+ var EINK_HALO_MIN_PX = 1.5;
6295
+ var EINK_DANGER_ICON_SCALE = 1.35;
6296
+ var EINK_WASH_CUTOFF = 0.3;
6297
+ var EINK_SAFETY_CONTOUR_WIDTH = [2, 3.5];
6298
+ var EINK_DANGER_CLASSES = /* @__PURE__ */ new Set(["OBSTRN", "WRECKS", "UWTROC"]);
6299
+ var METRIC_CONTOUR_LADDER_M = [0, 2, 5, 10, 15, 20, 30, 50, 100, 200, 500, 1e3];
6300
+ var IMPERIAL_CONTOUR_LADDER_M = [
6301
+ 1.8,
6302
+ 3.6,
6303
+ 5.4,
6304
+ 9.1,
6305
+ 18.2,
6306
+ 27.4,
6307
+ 36.5,
6308
+ 54.8,
6309
+ 91.4,
6310
+ 182.8,
6311
+ 365.7,
6312
+ 914.4
6313
+ ];
6314
+ function snapToLadder(safetyM, ladder) {
6315
+ for (const rung of ladder) if (rung >= safetyM - 1e-6) return rung;
6316
+ return null;
6317
+ }
6318
+ function safetyContourValues(safetyContourM) {
6319
+ const s = effectiveSafetyContour(safetyContourM);
6320
+ const out = [snapToLadder(s, METRIC_CONTOUR_LADDER_M), snapToLadder(s, IMPERIAL_CONTOUR_LADDER_M)];
6321
+ return [...new Set(out.filter((v) => v !== null))];
6322
+ }
6323
+ function isExpr(v) {
6324
+ return Array.isArray(v) && typeof v[0] === "string";
6325
+ }
6326
+ function scaleNumeric(v, k, min = 0) {
6327
+ if (typeof v === "number") return Math.max(v * k, v > 0 ? min : 0);
6328
+ if (!isExpr(v)) return v;
6329
+ const op = v[0];
6330
+ if (op === "interpolate" || op === "interpolate-hcl" || op === "interpolate-lab") {
6331
+ const out = v.slice(0, 3);
6332
+ for (let i = 3; i < v.length; i += 2) out.push(v[i], scaleNumeric(v[i + 1], k, min));
6333
+ return out;
6334
+ }
6335
+ if (op === "step") {
6336
+ const out = [op, v[1], scaleNumeric(v[2], k, min)];
6337
+ for (let i = 3; i < v.length; i += 2) out.push(v[i], scaleNumeric(v[i + 1], k, min));
6338
+ return out;
6339
+ }
6340
+ if (op === "literal") return v;
6341
+ return ["max", ["*", v, k], min];
6342
+ }
6343
+ function objectClass(layer) {
6344
+ const md = layer.metadata;
6345
+ const c = md?.["s52:objectClass"];
6346
+ return typeof c === "string" ? c : null;
6347
+ }
6348
+ function scaffoldId(prefix, baseId) {
6349
+ return `${prefix}${baseId.replace(/-band(\d)/g, "-b$1")}`;
6350
+ }
6351
+ var DRVAL1 = ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1];
6352
+ var DRVAL2 = ["coalesce", ["get", "DRVAL2"], ["get", "drval2"], 0];
6353
+ var VALDCO = ["to-number", ["coalesce", ["get", "VALDCO"], ["get", "valdco"], -1], -1];
6354
+ function withFilter(base, extra) {
6355
+ return base === void 0 ? extra : ["all", base, extra];
6356
+ }
6357
+ function applyEinkPortrayal(style, palette, opts = {}) {
6358
+ if (palette !== "EINK_COLOR" && palette !== "EINK_MONO" && palette !== "EINK_NIGHT") return style;
6359
+ const mono = isEinkGreyTable(palette);
6360
+ const night = palette === "EINK_NIGHT";
6361
+ const safetyInk = resolveColor("DEPSC", palette);
6362
+ const groundInk = resolveColor("UIBCK", palette);
6363
+ const safety = effectiveSafetyContour(opts.safetyContour ?? DEFAULT_SAFETY_CONTOUR_M);
6364
+ const safetyValues = safetyContourValues(safety);
6365
+ const out = [];
6366
+ for (const original of style.layers) {
6367
+ const id = original.id;
6368
+ const type = original.type;
6369
+ if (type === "hillshade" || id.startsWith("chrtland-fade-")) continue;
6370
+ const layer = structuredClone(original);
6371
+ const paint = layer.paint ??= {};
6372
+ const layout = layer.layout;
6373
+ const cls = objectClass(layer);
6374
+ if (type === "line") {
6375
+ if (paint["line-width"] !== void 0) {
6376
+ paint["line-width"] = scaleNumeric(paint["line-width"], EINK_LINE_SCALE, EINK_LINE_MIN_PX);
6377
+ } else {
6378
+ paint["line-width"] = EINK_LINE_SCALE;
6379
+ }
6380
+ const o = paint["line-opacity"];
6381
+ if (typeof o === "number" && o > 0 && o < 1) paint["line-opacity"] = 1;
6382
+ if (paint["line-blur"] !== void 0) paint["line-blur"] = 0;
6383
+ }
6384
+ if (type === "fill") {
6385
+ const o = paint["fill-opacity"];
6386
+ if (typeof o === "number" && o > 0 && o < 1) {
6387
+ if (cls && EINK_DANGER_CLASSES.has(cls)) {
6388
+ paint["fill-pattern"] = EINK_DANGER_PATTERN;
6389
+ paint["fill-opacity"] = 1;
6390
+ } else {
6391
+ paint["fill-opacity"] = o < EINK_WASH_CUTOFF ? 0 : 1;
6392
+ }
6393
+ }
6394
+ if (paint["fill-antialias"] === void 0) paint["fill-antialias"] = true;
6395
+ }
6396
+ if (type === "background" && night && id === "chrt-background") {
6397
+ paint["background-pattern"] = NOCOVERAGE_PATTERN;
6398
+ }
6399
+ if (type === "background") {
6400
+ const o = paint["background-opacity"];
6401
+ if (typeof o === "number" && o > 0 && o < 1) paint["background-opacity"] = 1;
6402
+ }
6403
+ if (type === "circle") {
6404
+ const blur = paint["circle-blur"];
6405
+ if (typeof blur === "number" && blur >= 0.5) {
6406
+ paint["circle-opacity"] = 0;
6407
+ } else {
6408
+ if (blur !== void 0) paint["circle-blur"] = 0;
6409
+ const o = paint["circle-opacity"];
6410
+ if (typeof o === "number" && o > 0 && o < 1) paint["circle-opacity"] = 1;
6411
+ }
6412
+ }
6413
+ if (type === "symbol") {
6414
+ if (layout?.["text-field"] !== void 0) {
6415
+ if (layout["text-size"] !== void 0) {
6416
+ layout["text-size"] = scaleNumeric(layout["text-size"], EINK_TEXT_SCALE);
6417
+ }
6418
+ if (paint["text-opacity"] !== void 0) paint["text-opacity"] = 1;
6419
+ if (typeof paint["text-halo-color"] === "string") paint["text-halo-color"] = groundInk;
6420
+ const hw = paint["text-halo-width"];
6421
+ paint["text-halo-width"] = typeof hw === "number" ? Math.max(hw, EINK_HALO_MIN_PX) : hw ?? EINK_HALO_MIN_PX;
6422
+ if (paint["text-halo-blur"] !== void 0) paint["text-halo-blur"] = 0;
6423
+ }
6424
+ const io = paint["icon-opacity"];
6425
+ if (typeof io === "number" && io > 0 && io < 1) paint["icon-opacity"] = 1;
6426
+ if (cls && EINK_DANGER_CLASSES.has(cls) && layout?.["icon-image"] !== void 0) {
6427
+ layout["icon-size"] = scaleNumeric(layout["icon-size"] ?? 1, EINK_DANGER_ICON_SCALE);
6428
+ }
6429
+ }
6430
+ out.push(layer);
6431
+ if (type === "fill" && cls === "DEPARE" && mono) {
6432
+ const drying = ["all", ["<", DRVAL1, 0], ["<=", DRVAL2, 0]];
6433
+ out.push({
6434
+ ...structuredClone(original),
6435
+ id: scaffoldId("chrt-eink-depth-of-", id),
6436
+ filter: withFilter(original.filter, ["<", DRVAL1, safety]),
6437
+ paint: {
6438
+ "fill-pattern": ["case", drying, EINK_HATCH_PATTERN, EINK_DOTS_PATTERN]
6439
+ }
6440
+ });
6441
+ }
6442
+ if (type === "line" && cls === "DEPCNT") {
6443
+ const isSafety = ["any", ...safetyValues.map((v) => ["<", ["abs", ["-", VALDCO, v]], 0.05])];
6444
+ out.push({
6445
+ ...structuredClone(original),
6446
+ id: scaffoldId("chrt-eink-safety-of-", id),
6447
+ filter: withFilter(original.filter, isSafety),
6448
+ paint: {
6449
+ "line-color": safetyInk,
6450
+ "line-width": [
6451
+ "interpolate",
6452
+ ["linear"],
6453
+ ["zoom"],
6454
+ 8,
6455
+ EINK_SAFETY_CONTOUR_WIDTH[0],
6456
+ 14,
6457
+ EINK_SAFETY_CONTOUR_WIDTH[1]
6458
+ ]
6459
+ },
6460
+ layout: { "line-join": "round", "line-cap": "round" }
6461
+ });
6462
+ }
6463
+ }
6464
+ return {
6465
+ ...style,
6466
+ // No transitions: a property change is one repaint, never an animation.
6467
+ ...{ transition: { duration: 0, delay: 0 } },
6468
+ metadata: { ...style.metadata, "chrt:eink": night ? "night" : mono ? "mono" : "color" },
6469
+ layers: out
6470
+ };
6471
+ }
6472
+
6473
+ // src/atlas/map-live.ts
5031
6474
  function isMapLive(map) {
5032
6475
  return map._removed !== true;
5033
6476
  }
@@ -5662,248 +7105,33 @@ function lineSpriteId(lineStyleName) {
5662
7105
  }
5663
7106
  function lineStylePaint(name) {
5664
7107
  const meta = S52_LINESTYLES[name];
5665
- if (!meta) {
5666
- throw new Error(
5667
- `Unknown S-52 linestyle "${name}". Known: ${Object.keys(S52_LINESTYLES).length} styles (regenerate with: bun run scripts/extract-opencpn-linestyles.ts)`
5668
- );
5669
- }
5670
- return {
5671
- "line-pattern": lineSpriteId(meta.name),
5672
- "line-width": houseWidth(meta)
5673
- };
5674
- }
5675
- function lineStylePaintCase(cases, fallback) {
5676
- const resolve = (name) => {
5677
- const meta = S52_LINESTYLES[name];
5678
- if (!meta) throw new Error(`Unknown S-52 linestyle "${name}"`);
5679
- return meta;
5680
- };
5681
- const fallbackMeta = resolve(fallback);
5682
- const pattern = ["case"];
5683
- const width = ["case"];
5684
- for (const [condition, name] of cases) {
5685
- const meta = resolve(name);
5686
- pattern.push(condition, lineSpriteId(meta.name));
5687
- width.push(condition, houseWidth(meta));
5688
- }
5689
- pattern.push(lineSpriteId(fallbackMeta.name));
5690
- width.push(houseWidth(fallbackMeta));
5691
- return { "line-pattern": pattern, "line-width": width };
5692
- }
5693
-
5694
- // src/atlas/s52-pattern.ts
5695
- var S52_PATTERN_SPRITE_PREFIX = "AP:";
5696
- function patternSpriteId(patternName) {
5697
- return `${S52_PATTERN_SPRITE_PREFIX}${patternName}`;
5698
- }
5699
-
5700
- // src/atlas/s52-conditional.ts
5701
- var DEFAULT_SAFETY_CONTOUR_M = 10;
5702
- var DEPARE_DEEP_CUT_M = 18.2;
5703
- var DEPARE_SHALLOW_CUT_M = 2;
5704
- function effectiveSafetyContour(safetyContourM) {
5705
- if (!Number.isFinite(safetyContourM)) return DEFAULT_SAFETY_CONTOUR_M;
5706
- return Math.min(Math.max(safetyContourM, DEPARE_SHALLOW_CUT_M), DEPARE_DEEP_CUT_M);
5707
- }
5708
- function depareColor(props, ct = "DAY", safetyContourM = DEFAULT_SAFETY_CONTOUR_M) {
5709
- const drval1 = Number(props.DRVAL1 ?? props.drval1 ?? -1);
5710
- const drval2 = Number(props.DRVAL2 ?? props.drval2 ?? drval1);
5711
- const safety = effectiveSafetyContour(safetyContourM);
5712
- if (drval1 < 0 && drval2 <= 0) return resolveColor("DEPIT", ct);
5713
- if (drval1 < DEPARE_SHALLOW_CUT_M) return resolveColor("DEPVS", ct);
5714
- if (drval1 < safety) return resolveColor("DEPMS", ct);
5715
- if (drval1 < DEPARE_DEEP_CUT_M) return resolveColor("DEPMD", ct);
5716
- return resolveColor("DEPDW", ct);
5717
- }
5718
- var DEPARE_FILL_BRANCH_TOKENS = ["DEPIT", "DEPVS", "DEPMS", "DEPMD"];
5719
- var DEPARE_FILL_FALLBACK_TOKEN = "DEPDW";
5720
- function depareFillExpression(ct = "DAY", safetyContourM = DEFAULT_SAFETY_CONTOUR_M) {
5721
- const safety = effectiveSafetyContour(safetyContourM);
5722
- const [it, vs, ms, md] = DEPARE_FILL_BRANCH_TOKENS;
5723
- return [
5724
- "case",
5725
- [
5726
- "all",
5727
- ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], 0],
5728
- ["<=", ["coalesce", ["get", "DRVAL2"], ["get", "drval2"], 0], 0]
5729
- ],
5730
- resolveColor(it, ct),
5731
- ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], DEPARE_SHALLOW_CUT_M],
5732
- resolveColor(vs, ct),
5733
- ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], safety],
5734
- resolveColor(ms, ct),
5735
- ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], DEPARE_DEEP_CUT_M],
5736
- resolveColor(md, ct),
5737
- resolveColor(DEPARE_FILL_FALLBACK_TOKEN, ct)
5738
- ];
5739
- }
5740
- function listAttrString(attr) {
5741
- return ["to-string", ["coalesce", ["get", attr], ["get", attr.toLowerCase()], ""]];
5742
- }
5743
- function listAttrCase(attr, cases, fallback) {
5744
- const attrStr = listAttrString(attr);
5745
- const expr = ["case"];
5746
- for (const [code, value] of cases) {
5747
- expr.push([">=", ["index-of", `"${code}"`, attrStr], 0], value);
5748
- }
5749
- for (const [code, value] of cases) {
5750
- expr.push(["==", attrStr, code], value);
5751
- }
5752
- for (const [code, value] of cases) {
5753
- expr.push(["==", attrStr, `[${code}]`], value);
5754
- }
5755
- expr.push(fallback);
5756
- return expr;
5757
- }
5758
- function listAttrHas(attr, codes) {
5759
- const attrStr = listAttrString(attr);
5760
- const clauses = [];
5761
- for (const code of codes) {
5762
- clauses.push([">=", ["index-of", `"${code}"`, attrStr], 0]);
5763
- clauses.push(["==", attrStr, code]);
5764
- clauses.push(["==", attrStr, `[${code}]`]);
5765
- }
5766
- return ["any", ...clauses];
5767
- }
5768
- function colourCodeCase(cases, fallback) {
5769
- return listAttrCase("COLOUR", cases, fallback);
5770
- }
5771
- function lightColor(props, ct = "DAY") {
5772
- const raw = props.COLOUR ?? props.colour;
5773
- const code = Array.isArray(raw) ? Number(raw[0]) : typeof raw === "string" && raw.startsWith("[") ? Number(JSON.parse(raw)[0]) : Number(raw ?? 0);
5774
- switch (code) {
5775
- case 1:
5776
- return resolveColor("LITWT", ct);
5777
- case 3:
5778
- return resolveColor("LITRD", ct);
5779
- case 4:
5780
- return resolveColor("LITGN", ct);
5781
- case 5:
5782
- return resolveColor("LITBL", ct);
5783
- case 6:
5784
- return resolveColor("LITYW", ct);
5785
- case 11:
5786
- return resolveColor("LITOR", ct);
5787
- default:
5788
- return resolveColor("LITYW", ct);
5789
- }
5790
- }
5791
- function lightFillExpression(ct = "DAY") {
5792
- return colourCodeCase(
5793
- [
5794
- ["3", resolveColor("LITRD", ct)],
5795
- ["4", resolveColor("LITGN", ct)],
5796
- ["6", resolveColor("LITYW", ct)],
5797
- ["11", resolveColor("LITOR", ct)],
5798
- ["5", resolveColor("LITBL", ct)],
5799
- ["1", resolveColor("LITWT", ct)]
5800
- ],
5801
- resolveColor("LITYW", ct)
5802
- );
5803
- }
5804
- function lateralColor(props, ct = "DAY") {
5805
- const catlam = Number(props.CATLAM ?? props.catlam ?? 0);
5806
- if (catlam === 1) return resolveColor("LITRD", ct);
5807
- if (catlam === 2) return resolveColor("LITGN", ct);
5808
- return resolveColor("LITYW", ct);
5809
- }
5810
- function lateralFillExpression(ct = "DAY") {
5811
- return [
5812
- "match",
5813
- ["coalesce", ["get", "CATLAM"], ["get", "catlam"], 0],
5814
- 1,
5815
- resolveColor("LITRD", ct),
5816
- 2,
5817
- resolveColor("LITGN", ct),
5818
- resolveColor("LITYW", ct)
5819
- ];
5820
- }
5821
- function hazardColor(props, ct = "DAY") {
5822
- const watlev = Number(props.WATLEV ?? props.watlev ?? 0);
5823
- const valsou = props.VALSOU ?? props.valsou;
5824
- if (watlev === 1 || watlev === 2) return resolveColor("DNGHL", ct);
5825
- if (valsou != null && Number(valsou) < 2) return resolveColor("DNGHL", ct);
5826
- return resolveColor("RESBL", ct);
5827
- }
5828
- function hazardFillExpression(ct = "DAY") {
5829
- return [
5830
- "case",
5831
- [
5832
- "any",
5833
- ["==", ["coalesce", ["get", "WATLEV"], ["get", "watlev"], 0], 1],
5834
- ["==", ["coalesce", ["get", "WATLEV"], ["get", "watlev"], 0], 2]
5835
- ],
5836
- resolveColor("DNGHL", ct),
5837
- [
5838
- "all",
5839
- ["has", "VALSOU"],
5840
- ["<", ["get", "VALSOU"], 2]
5841
- ],
5842
- resolveColor("DNGHL", ct),
5843
- resolveColor("RESBL", ct)
5844
- ];
5845
- }
5846
- function vegatnPatternExpression() {
5847
- return [
5848
- "match",
5849
- ["coalesce", ["get", "CATVEG"], ["get", "catveg"], 0],
5850
- 1,
5851
- patternSpriteId("VEGATN03"),
5852
- // grass
5853
- 3,
5854
- patternSpriteId("VEGATN03"),
5855
- // bush
5856
- 4,
5857
- patternSpriteId("VEGATN04"),
5858
- // deciduous wood
5859
- 5,
5860
- patternSpriteId("VEGATN04"),
5861
- // coniferous wood
5862
- 6,
5863
- patternSpriteId("VEGATN04"),
5864
- // wood (mixed)
5865
- 7,
5866
- patternSpriteId("MARSHES1"),
5867
- // mangroves
5868
- 10,
5869
- patternSpriteId("VEGATN03"),
5870
- // scrub
5871
- 11,
5872
- patternSpriteId("VEGATN04"),
5873
- // tree
5874
- 12,
5875
- patternSpriteId("VEGATN04"),
5876
- // evergreen
5877
- 16,
5878
- patternSpriteId("MARSHES1"),
5879
- // swamp
5880
- 17,
5881
- patternSpriteId("MARSHES1"),
5882
- // marsh
5883
- 21,
5884
- patternSpriteId("VEGATN04"),
5885
- // savannah
5886
- patternSpriteId("VEGATN03")
5887
- // default: grass pattern
5888
- ];
5889
- }
5890
- function daymarkColor(props, ct = "DAY") {
5891
- const raw = props.COLOUR ?? props.colour;
5892
- const code = Array.isArray(raw) ? Number(raw[0]) : typeof raw === "string" && raw.startsWith("[") ? Number(JSON.parse(raw)[0]) : Number(raw ?? 0);
5893
- switch (code) {
5894
- case 1:
5895
- return resolveColor("LITWT", ct);
5896
- case 3:
5897
- return resolveColor("LITRD", ct);
5898
- case 4:
5899
- return resolveColor("LITGN", ct);
5900
- case 6:
5901
- return resolveColor("LITYW", ct);
5902
- case 11:
5903
- return resolveColor("LITOR", ct);
5904
- default:
5905
- return resolveColor("LITGN", ct);
7108
+ if (!meta) {
7109
+ throw new Error(
7110
+ `Unknown S-52 linestyle "${name}". Known: ${Object.keys(S52_LINESTYLES).length} styles (regenerate with: bun run scripts/extract-opencpn-linestyles.ts)`
7111
+ );
7112
+ }
7113
+ return {
7114
+ "line-pattern": lineSpriteId(meta.name),
7115
+ "line-width": houseWidth(meta)
7116
+ };
7117
+ }
7118
+ function lineStylePaintCase(cases, fallback) {
7119
+ const resolve = (name) => {
7120
+ const meta = S52_LINESTYLES[name];
7121
+ if (!meta) throw new Error(`Unknown S-52 linestyle "${name}"`);
7122
+ return meta;
7123
+ };
7124
+ const fallbackMeta = resolve(fallback);
7125
+ const pattern = ["case"];
7126
+ const width = ["case"];
7127
+ for (const [condition, name] of cases) {
7128
+ const meta = resolve(name);
7129
+ pattern.push(condition, lineSpriteId(meta.name));
7130
+ width.push(condition, houseWidth(meta));
5906
7131
  }
7132
+ pattern.push(lineSpriteId(fallbackMeta.name));
7133
+ width.push(houseWidth(fallbackMeta));
7134
+ return { "line-pattern": pattern, "line-width": width };
5907
7135
  }
5908
7136
 
5909
7137
  // src/atlas/s52-lookup.ts
@@ -19796,9 +21024,9 @@ function compileAtonExpression(lookup, mapSymbol = (name) => name) {
19796
21024
  expr.push(fallback);
19797
21025
  return expr;
19798
21026
  }
19799
- function atonIconExpression(objectClass) {
19800
- const lookup = S52_ATON_LOOKUPS[objectClass];
19801
- if (!lookup) throw new Error(`s52-aton-expressions: no generated lookup for ${objectClass}`);
21027
+ function atonIconExpression(objectClass2) {
21028
+ const lookup = S52_ATON_LOOKUPS[objectClass2];
21029
+ if (!lookup) throw new Error(`s52-aton-expressions: no generated lookup for ${objectClass2}`);
19802
21030
  return compileAtonExpression(lookup);
19803
21031
  }
19804
21032
  function lightsIconExpression() {
@@ -22883,7 +24111,9 @@ var LABEL_TINT = {
22883
24111
  };
22884
24112
  function tintFor(tint, ct) {
22885
24113
  const day = LABEL_TINT[tint].day;
22886
- if (ct === "DAY") return day;
24114
+ if (ct === "DAY" || ct === "EINK_COLOR") return day;
24115
+ if (ct === "EINK_MONO") return einkMonoGrey(day);
24116
+ if (ct === "EINK_NIGHT") return einkNightGrey(day);
22887
24117
  return capLuminance(day, relativeLuminance(resolveColor("CHBLK", ct)));
22888
24118
  }
22889
24119
  function nightText(tint) {
@@ -22894,10 +24124,14 @@ function nightText(tint) {
22894
24124
  }
22895
24125
  function onHalo(tint, ct) {
22896
24126
  if (ct === "NIGHT") return { textColor: nightText(tint), textHaloColor: NIGHT_KNOCKOUT };
24127
+ if (ct === "EINK_NIGHT") return { textColor: EINK_MONO_GREYS.MID, textHaloColor: EINK_MONO_GREYS.INK };
24128
+ if (isEinkColorTable(ct)) return { textColor: EINK_MONO_GREYS.INK, textHaloColor: EINK_MONO_GREYS.PAPER };
22897
24129
  return { textColor: DARK_BG, textHaloColor: tintFor(tint, ct) };
22898
24130
  }
22899
24131
  function onKnockout(tint, ct) {
22900
24132
  if (ct === "NIGHT") return { textColor: nightText(tint), textHaloColor: NIGHT_KNOCKOUT };
24133
+ if (ct === "EINK_NIGHT") return { textColor: EINK_MONO_GREYS.MID, textHaloColor: EINK_MONO_GREYS.INK };
24134
+ if (isEinkColorTable(ct)) return { textColor: EINK_MONO_GREYS.INK, textHaloColor: EINK_MONO_GREYS.PAPER };
22901
24135
  return { textColor: tintFor(tint, ct), textHaloColor: DARK_BG };
22902
24136
  }
22903
24137
  var NAV_AID_LABEL_TEXT_SIZE = [
@@ -27114,10 +28348,20 @@ var S101_NO_OFFICIAL_COUNTERPART = /* @__PURE__ */ new Set([
27114
28348
  "SISTAT02",
27115
28349
  "WATTUR01"
27116
28350
  ]);
28351
+ function s101EinkTable(einkS52, fallback) {
28352
+ const out = {};
28353
+ for (const [token, dayHex] of Object.entries(S101_DAY)) {
28354
+ out[token] = einkS52[token] ?? fallback(dayHex);
28355
+ }
28356
+ return out;
28357
+ }
27117
28358
  var S101_TABLES = {
27118
28359
  DAY: S101_DAY,
27119
28360
  DUSK: S101_DUSK,
27120
- NIGHT: S101_NIGHT
28361
+ NIGHT: S101_NIGHT,
28362
+ EINK_COLOR: s101EinkTable(S52_EINK_COLOR, (hex) => hex),
28363
+ EINK_MONO: s101EinkTable(S52_EINK_MONO, einkMonoGrey),
28364
+ EINK_NIGHT: s101EinkTable(S52_EINK_NIGHT, einkNightGrey)
27121
28365
  };
27122
28366
  var warnedTokens = /* @__PURE__ */ new Set();
27123
28367
  function reportUnresolvedToken(token, table) {
@@ -27164,328 +28408,54 @@ function rewritePatternToS101(node, available) {
27164
28408
  if (typeof node === "string") {
27165
28409
  return available.has(node) ? `${S101_ICON_PREFIX}${node}` : node;
27166
28410
  }
27167
- if (Array.isArray(node)) return node.map((n) => rewritePatternToS101(n, available));
27168
- return node;
27169
- }
27170
- function collectLiterals(node, out) {
27171
- if (typeof node === "string") {
27172
- out.add(node);
27173
- return;
27174
- }
27175
- if (Array.isArray(node)) for (const n of node) collectLiterals(n, out);
27176
- }
27177
- function applyS101SymbolSet(style) {
27178
- const seen = { icon: /* @__PURE__ */ new Set(), fill: /* @__PURE__ */ new Set(), line: /* @__PURE__ */ new Set() };
27179
- for (const layer of style.layers) {
27180
- const layout = layer.layout;
27181
- const paint = layer.paint;
27182
- if (layer.type === "symbol" && layout && layout["icon-image"] != null) {
27183
- layout["icon-image"] = rewriteIconToS101(layout["icon-image"]);
27184
- collectLiterals(layout["icon-image"], seen.icon);
27185
- }
27186
- for (const bag of [layout, paint]) {
27187
- if (!bag) continue;
27188
- if (bag["fill-pattern"] != null) {
27189
- bag["fill-pattern"] = rewritePatternToS101(bag["fill-pattern"], S101_AREA_FILL_NAMES);
27190
- collectLiterals(bag["fill-pattern"], seen.fill);
27191
- }
27192
- if (bag["line-pattern"] != null) {
27193
- bag["line-pattern"] = rewritePatternToS101(bag["line-pattern"], S101_LINE_STYLE_NAMES);
27194
- collectLiterals(bag["line-pattern"], seen.line);
27195
- }
27196
- }
27197
- }
27198
- const official = (names) => [...names].filter((n) => n.startsWith(S101_ICON_PREFIX)).length;
27199
- const iconSubs = [...seen.icon].filter((n) => S101_NO_OFFICIAL_COUNTERPART.has(n)).length;
27200
- const patternSubs = (names) => [...names].filter((n) => /^(AP|LC):/.test(n)).length;
27201
- const coverage = {
27202
- pointSymbols: { official: official(seen.icon), s52Substitute: iconSubs },
27203
- areaFills: { official: official(seen.fill), s52Substitute: patternSubs(seen.fill) },
27204
- lineStyles: { official: official(seen.line), s52Substitute: patternSubs(seen.line) }
27205
- };
27206
- const metadata = style.metadata;
27207
- metadata["chrt:symbol-set"] = "s101";
27208
- metadata["chrt:s101-source-commit"] = S101_SOURCE_COMMIT;
27209
- metadata["chrt:s101-coverage"] = {
27210
- ...coverage,
27211
- /** True only when NOTHING in this document is an S-52 substitute. A
27212
- * consumer that wants "S-101 or nothing" branches on this one boolean. */
27213
- complete: Object.values(coverage).every((c) => c.s52Substitute === 0)
27214
- };
27215
- return style;
27216
- }
27217
-
27218
- // src/atlas/chrt-svgs-generated.ts
27219
- var CHRT_GLYPH_SVGS = {
27220
- "badge-radar": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="19" r="2.6" fill="#D200D2"/><path d="M9 19a7 7 0 0 1 14 0M5 19a11 11 0 0 1 22 0" stroke="#D200D2" stroke-width="2.4" fill="none" stroke-linecap="round" stroke-dasharray="4 3"/></svg>',
27221
- "badge-sound": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="7" cy="16" r="2.2" fill="#D200D2"/><path d="M12 10a9 9 0 0 1 0 12M16.5 6.5a15 15 0 0 1 0 19M21 3a21 21 0 0 1 0 26" stroke="#D200D2" stroke-width="2.4" fill="none" stroke-linecap="round"/></svg>',
27222
- "beacon-cairn": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="11" r="4" fill="#fff"/><circle cx="11.5" cy="19" r="4.6" fill="#fff"/><circle cx="20.5" cy="19" r="4.6" fill="#fff"/><path d="M7 28h18" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
27223
- "beacon-generic": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 25V12" stroke="#fff" stroke-width="2.6" stroke-linecap="round"/><path d="M16 4l6 8H10Z" fill="#fff"/><path d="M8 28h16M11 25h10" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
27224
- "beacon-lattice": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M11 25 13 6h6l2 19M12 20h8M12.6 13h6.8M11 25l9-12M21 25l-9-12" stroke="#fff" stroke-width="1.8" stroke-linecap="round" fill="none"/><path d="M8 28h16" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
27225
- "beacon-stake": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 25V6" stroke="#fff" stroke-width="2.8" stroke-linecap="round"/><path d="M9 28h14" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
27226
- "beacon-tower": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M12 6h8l1.6 19H10.4Z" fill="#fff"/><path d="M10 6h12" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/><path d="M8 28h16" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
27227
- "buoy-barrel": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect x="7" y="13" width="18" height="11" rx="5.5" fill="#fff"/><path d="M6 28h20" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
27228
- "buoy-can": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M9 12h14v11a2 2 0 0 1-2 2H11a2 2 0 0 1-2-2Z" fill="#fff"/><path d="M6 28h20" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
27229
- "buoy-conical": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 8 24.5 25H7.5Z" fill="#fff"/><path d="M6 28h20" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
27230
- "buoy-pillar": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M13 6h6l1.4 19H11.6Z" fill="#fff"/><path d="M6 28h20" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
27231
- "buoy-spar": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M14.5 5h3l1 20h-5Z" fill="#fff"/><path d="M6 28h20" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
27232
- "buoy-spherical": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="17.5" r="8.5" fill="#fff"/><path d="M6 28h20" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
27233
- "buoy-super": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M7 17h18l-2.5 8h-13Z" fill="#fff"/><path d="M16 17V8" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/><circle cx="16" cy="6.5" r="2" fill="#fff"/><path d="M5 28h22" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
27234
- "cardinal-e": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 1.5 20 6h-8Z" fill="#101418"/><path d="M16 11 12 6.5h8Z" fill="#101418"/><path d="M13 12h6l1 13H12Z" fill="#FFC805"/><path d="M6 28h20" stroke="#101418" stroke-width="2.4" stroke-linecap="round"/></svg>',
27235
- "cardinal-n": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 1.5 20 6h-8Z" fill="#101418"/><path d="M16 6.5 20 11h-8Z" fill="#101418"/><path d="M13 12h6l1 13H12Z" fill="#FFC805"/><path d="M6 28h20" stroke="#101418" stroke-width="2.4" stroke-linecap="round"/></svg>',
27236
- "cardinal-s": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 11 12 6.5h8Z" fill="#101418"/><path d="M16 6 12 1.5h8Z" fill="#101418"/><path d="M13 12h6l1 13H12Z" fill="#FFC805"/><path d="M6 28h20" stroke="#101418" stroke-width="2.4" stroke-linecap="round"/></svg>',
27237
- "cardinal-w": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 6 12 1.5h8Z" fill="#101418"/><path d="M16 6.5 20 11h-8Z" fill="#101418"/><path d="M13 12h6l1 13H12Z" fill="#FFC805"/><path d="M6 28h20" stroke="#101418" stroke-width="2.4" stroke-linecap="round"/></svg>',
27238
- "daymark-square": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect x="6" y="6" width="20" height="20" rx="1.5" fill="#fff"/></svg>',
27239
- "daymark-triangle": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 5 28 26H4Z" fill="#fff"/></svg>',
27240
- "hatch-nocoverage": '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M-3 9 L9 -3 M-3 21 L21 -3 M3 27 L27 3 M15 27 L27 15" stroke="#8A97A0" stroke-width="0.8" fill="none"/><path d="M-3 15 L15 33 M-3 3 L21 27 M3 -3 L27 21 M15 -3 L27 9" stroke="#8A97A0" stroke-width="0.8" fill="none"/></svg>',
27241
- "hatch-uncharted": '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M-3 9 L9 -3 M-3 21 L21 -3 M3 27 L27 3 M15 27 L27 15" stroke="#5B7383" stroke-width="0.65" fill="none"/></svg>',
27242
- "isolated-danger": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="3.5" r="2.4" fill="#101418"/><circle cx="16" cy="9.5" r="2.4" fill="#101418"/><path d="M13 13h6l1 12H12Z" fill="#101418"/><rect x="12.4" y="17.5" width="7.2" height="3.4" fill="#E5484D"/><path d="M6 28h20" stroke="#101418" stroke-width="2.4" stroke-linecap="round"/></svg>',
27243
- "light-flare": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="11" cy="11" r="3.2" fill="#fff"/><path d="M13.4 13.4 24 19.5a2.6 2.6 0 0 1-4.5 4.5L13.4 13.4Z" fill="#fff"/></svg>',
27244
- "lightship": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M6 20l3-5h14l3 5-3.5 4h-13Z" fill="#fff"/><path d="M16 15V8" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/><circle cx="16" cy="6" r="2.2" fill="#fff"/><path d="M5 28h22" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
27245
- "safe-water": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="6" r="2.4" fill="#E5484D"/><circle cx="16" cy="17.5" r="8" fill="#F8FAFC"/><path d="M12.2 10.5a8 8 0 0 0 0 14M19.8 10.5a8 8 0 0 1 0 14" stroke="#E5484D" stroke-width="3" fill="none"/><path d="M6 28h20" stroke="#101418" stroke-width="2.4" stroke-linecap="round"/></svg>'
27246
- };
27247
-
27248
- // src/atlas/chrt-style.ts
27249
- var CHRT_ICON_PREFIX = "chrt:";
27250
- var CHRT_SDF_GLYPHS = /* @__PURE__ */ new Set([
27251
- "buoy-can",
27252
- "buoy-conical",
27253
- "buoy-spherical",
27254
- "buoy-pillar",
27255
- "buoy-spar",
27256
- "buoy-barrel",
27257
- "buoy-super",
27258
- "beacon-generic",
27259
- "beacon-tower",
27260
- "beacon-lattice",
27261
- "beacon-stake",
27262
- "beacon-cairn",
27263
- "light-flare",
27264
- "lightship",
27265
- "daymark-triangle",
27266
- "daymark-square"
27267
- ]);
27268
- var CHRT_GLYPH_NAMES = new Set(Object.keys(CHRT_GLYPH_SVGS));
27269
- var CHRT_ATON_COLORS = {
27270
- "1": "#F8FAFC",
27271
- // white
27272
- "2": "#101418",
27273
- // black
27274
- "3": "#E5484D",
27275
- // red
27276
- "4": "#30A46C",
27277
- // green
27278
- "5": "#3E63DD",
27279
- // blue
27280
- "6": "#FFC805",
27281
- // yellow
27282
- "7": "#8B9298",
27283
- // grey
27284
- "8": "#A66B3A",
27285
- // brown
27286
- "9": "#FFB224",
27287
- // amber
27288
- "10": "#7E4CCB",
27289
- // violet
27290
- "11": "#F76B15",
27291
- // orange
27292
- "12": "#D671C1"
27293
- // pink (magenta-ish)
27294
- };
27295
- var CHRT_UNKNOWN_TINT = "#8B9298";
27296
- var HOUSE_BLACK = "#101418";
27297
- function chrtHouseColor(dayHex, palette = "DAY") {
27298
- if (palette === "DAY") return dayHex;
27299
- const isBlack = dayHex.toLowerCase() === HOUSE_BLACK.toLowerCase();
27300
- if (palette === "DUSK") {
27301
- return isBlack ? dayHex : capLuminance(dayHex, relativeLuminance(resolveColor("CHBLK", "DUSK")));
27302
- }
27303
- return isBlack ? NIGHT_INK : capLuminance(dayHex, relativeLuminance(NIGHT_MUTED_INK));
27304
- }
27305
- function themeChrtGlyph(svg3, palette = "DAY") {
27306
- if (palette === "DAY") return svg3;
27307
- return svg3.replace(
27308
- /(fill|stroke)="(#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6})"/g,
27309
- (_m, attr, hex) => {
27310
- const six = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
27311
- return `${attr}="${chrtHouseColor(six, palette)}"`;
27312
- }
27313
- );
27314
- }
27315
- function chrtAtonTintExpression(palette = "DAY") {
27316
- const c = (code) => chrtHouseColor(CHRT_ATON_COLORS[code], palette);
27317
- return colourCodeCase(
27318
- [
27319
- ["3", c("3")],
27320
- ["4", c("4")],
27321
- ["6", c("6")],
27322
- ["11", c("11")],
27323
- ["9", c("9")],
27324
- ["5", c("5")],
27325
- ["1", c("1")],
27326
- ["2", c("2")],
27327
- ["8", c("8")],
27328
- ["10", c("10")],
27329
- ["12", c("12")],
27330
- ["7", c("7")]
27331
- ],
27332
- chrtHouseColor(CHRT_UNKNOWN_TINT, palette)
27333
- );
27334
- }
27335
- function chrtLightTintExpression(palette = "DAY") {
27336
- const c = (code) => chrtHouseColor(CHRT_ATON_COLORS[code], palette);
27337
- return colourCodeCase(
27338
- [
27339
- ["3", c("3")],
27340
- ["4", c("4")],
27341
- ["6", c("6")],
27342
- ["11", c("11")],
27343
- ["9", c("9")],
27344
- ["5", c("5")],
27345
- ["1", c("1")]
27346
- ],
27347
- c("1")
27348
- );
27349
- }
27350
- function scalar(attr) {
27351
- return ["coalesce", ["get", attr], ["get", attr.toLowerCase()], 0];
27352
- }
27353
- var g = (name) => `${CHRT_ICON_PREFIX}${name}`;
27354
- function buoyBodyExpression() {
27355
- return [
27356
- "match",
27357
- scalar("BOYSHP"),
27358
- 1,
27359
- g("buoy-conical"),
27360
- 2,
27361
- g("buoy-can"),
27362
- 3,
27363
- g("buoy-spherical"),
27364
- 4,
27365
- g("buoy-pillar"),
27366
- 5,
27367
- g("buoy-spar"),
27368
- 6,
27369
- g("buoy-barrel"),
27370
- 7,
27371
- g("buoy-super"),
27372
- 8,
27373
- g("buoy-super"),
27374
- g("buoy-pillar")
27375
- ];
27376
- }
27377
- function beaconBodyExpression() {
27378
- return [
27379
- "match",
27380
- scalar("BCNSHP"),
27381
- 1,
27382
- g("beacon-stake"),
27383
- 2,
27384
- g("beacon-stake"),
27385
- 3,
27386
- g("beacon-tower"),
27387
- 4,
27388
- g("beacon-lattice"),
27389
- 5,
27390
- g("beacon-stake"),
27391
- 6,
27392
- g("beacon-cairn"),
27393
- g("beacon-generic")
27394
- ];
28411
+ if (Array.isArray(node)) return node.map((n) => rewritePatternToS101(n, available));
28412
+ return node;
27395
28413
  }
27396
- function cardinalExpression() {
27397
- return [
27398
- "match",
27399
- scalar("CATCAM"),
27400
- 1,
27401
- g("cardinal-n"),
27402
- 2,
27403
- g("cardinal-e"),
27404
- 3,
27405
- g("cardinal-s"),
27406
- 4,
27407
- g("cardinal-w"),
27408
- g("cardinal-n")
27409
- ];
28414
+ function collectLiterals(node, out) {
28415
+ if (typeof node === "string") {
28416
+ out.add(node);
28417
+ return;
28418
+ }
28419
+ if (Array.isArray(node)) for (const n of node) collectLiterals(n, out);
27410
28420
  }
27411
- var CHRT_ICON_EXPRESSIONS = {
27412
- BOYLAT: buoyBodyExpression,
27413
- BOYSPP: buoyBodyExpression,
27414
- BOYINB: buoyBodyExpression,
27415
- BOYCAR: cardinalExpression,
27416
- BOYISD: () => g("isolated-danger"),
27417
- BOYSAW: () => g("safe-water"),
27418
- BCNLAT: beaconBodyExpression,
27419
- BCNSPP: beaconBodyExpression,
27420
- BCNCAR: cardinalExpression,
27421
- BCNISD: () => g("isolated-danger"),
27422
- BCNSAW: () => g("safe-water"),
27423
- LIGHTS: () => g("light-flare"),
27424
- LITFLT: () => g("lightship"),
27425
- LITVES: () => g("lightship"),
27426
- FOGSIG: () => g("badge-sound"),
27427
- RTPBCN: () => g("badge-radar"),
27428
- RDOSTA: () => g("badge-radar"),
27429
- DAYMAR: () => [
27430
- "match",
27431
- scalar("TOPSHP"),
27432
- 1,
27433
- g("daymark-triangle"),
27434
- 2,
27435
- g("daymark-triangle"),
27436
- 5,
27437
- g("daymark-triangle"),
27438
- g("daymark-square")
27439
- ]
27440
- };
27441
- var TINTED_CLASSES = /* @__PURE__ */ new Set([
27442
- "BOYLAT",
27443
- "BOYSPP",
27444
- "BOYINB",
27445
- "BCNLAT",
27446
- "BCNSPP",
27447
- "DAYMAR"
27448
- ]);
27449
- var CHRT_HALO = { color: HOUSE_BLACK, width: 1 };
27450
- var CHRT_LINE_DASH = {
27451
- CBLSUB: { color: "CHMGD", width: 0.8, dash: [6, 3] },
27452
- PIPSOL: { color: "PIPLN", width: 0.8, dash: [6, 3] }
27453
- };
27454
- function applyChrtSymbolSet(style, palette = "DAY") {
28421
+ function applyS101SymbolSet(style) {
28422
+ const seen = { icon: /* @__PURE__ */ new Set(), fill: /* @__PURE__ */ new Set(), line: /* @__PURE__ */ new Set() };
27455
28423
  for (const layer of style.layers) {
27456
- if (layer.type === "line") {
27457
- const cls = layer.metadata?.["s52:objectClass"];
27458
- const dash = cls ? CHRT_LINE_DASH[cls] : void 0;
27459
- const paint2 = layer.paint ?? {};
27460
- if (dash && paint2["line-pattern"] != null) {
27461
- delete paint2["line-pattern"];
27462
- paint2["line-color"] = resolveColor(dash.color, palette);
27463
- paint2["line-width"] = dash.width;
27464
- paint2["line-dasharray"] = dash.dash;
27465
- layer.paint = paint2;
27466
- }
27467
- continue;
27468
- }
27469
- if (layer.type !== "symbol") continue;
27470
- const objectClass = layer.metadata?.["s52:objectClass"];
27471
- if (!objectClass || !CHRT_ICON_EXPRESSIONS[objectClass]) continue;
27472
28424
  const layout = layer.layout;
27473
- if (!layout || layout["icon-image"] == null) continue;
27474
- layout["icon-image"] = CHRT_ICON_EXPRESSIONS[objectClass]();
27475
- const paint = layer.paint ?? {};
27476
- if (TINTED_CLASSES.has(objectClass)) {
27477
- paint["icon-color"] = chrtAtonTintExpression(palette);
27478
- paint["icon-halo-color"] = CHRT_HALO.color;
27479
- paint["icon-halo-width"] = CHRT_HALO.width;
27480
- } else if (objectClass === "LIGHTS") {
27481
- paint["icon-color"] = chrtLightTintExpression(palette);
27482
- paint["icon-halo-color"] = CHRT_HALO.color;
27483
- paint["icon-halo-width"] = CHRT_HALO.width;
28425
+ const paint = layer.paint;
28426
+ if (layer.type === "symbol" && layout && layout["icon-image"] != null) {
28427
+ layout["icon-image"] = rewriteIconToS101(layout["icon-image"]);
28428
+ collectLiterals(layout["icon-image"], seen.icon);
28429
+ }
28430
+ for (const bag of [layout, paint]) {
28431
+ if (!bag) continue;
28432
+ if (bag["fill-pattern"] != null) {
28433
+ bag["fill-pattern"] = rewritePatternToS101(bag["fill-pattern"], S101_AREA_FILL_NAMES);
28434
+ collectLiterals(bag["fill-pattern"], seen.fill);
28435
+ }
28436
+ if (bag["line-pattern"] != null) {
28437
+ bag["line-pattern"] = rewritePatternToS101(bag["line-pattern"], S101_LINE_STYLE_NAMES);
28438
+ collectLiterals(bag["line-pattern"], seen.line);
28439
+ }
27484
28440
  }
27485
- layer.paint = paint;
27486
28441
  }
28442
+ const official = (names) => [...names].filter((n) => n.startsWith(S101_ICON_PREFIX)).length;
28443
+ const iconSubs = [...seen.icon].filter((n) => S101_NO_OFFICIAL_COUNTERPART.has(n)).length;
28444
+ const patternSubs = (names) => [...names].filter((n) => /^(AP|LC):/.test(n)).length;
28445
+ const coverage = {
28446
+ pointSymbols: { official: official(seen.icon), s52Substitute: iconSubs },
28447
+ areaFills: { official: official(seen.fill), s52Substitute: patternSubs(seen.fill) },
28448
+ lineStyles: { official: official(seen.line), s52Substitute: patternSubs(seen.line) }
28449
+ };
27487
28450
  const metadata = style.metadata;
27488
- metadata["chrt:symbol-set"] = "chrt";
28451
+ metadata["chrt:symbol-set"] = "s101";
28452
+ metadata["chrt:s101-source-commit"] = S101_SOURCE_COMMIT;
28453
+ metadata["chrt:s101-coverage"] = {
28454
+ ...coverage,
28455
+ /** True only when NOTHING in this document is an S-52 substitute. A
28456
+ * consumer that wants "S-101 or nothing" branches on this one boolean. */
28457
+ complete: Object.values(coverage).every((c) => c.s52Substitute === 0)
28458
+ };
27489
28459
  return style;
27490
28460
  }
27491
28461
 
@@ -27642,9 +28612,9 @@ var RASTER_ATON_CLASSES = Object.keys(
27642
28612
  if (!s52) return true;
27643
28613
  return JSON.stringify({ r: paper.rows, d: paper.defaultSymbol }) !== JSON.stringify({ r: s52.rows, d: s52.defaultSymbol });
27644
28614
  });
27645
- function rasterAtonIconExpression(objectClass) {
27646
- const lookup = S52_ATON_LOOKUPS_PAPER[objectClass];
27647
- if (!lookup) throw new Error(`raster-style: no paper lookup for ${objectClass}`);
28615
+ function rasterAtonIconExpression(objectClass2) {
28616
+ const lookup = S52_ATON_LOOKUPS_PAPER[objectClass2];
28617
+ if (!lookup) throw new Error(`raster-style: no paper lookup for ${objectClass2}`);
27648
28618
  return compileAtonExpression(lookup, resolveRasterImage);
27649
28619
  }
27650
28620
  function getRasterSvg(name, table = "DAY") {
@@ -27658,9 +28628,9 @@ function applyRasterSymbolSet(style) {
27658
28628
  const layout = layer.layout;
27659
28629
  if (!layout || layout["icon-image"] == null) continue;
27660
28630
  const metadata2 = layer.metadata;
27661
- const objectClass = metadata2?.["s52:objectClass"];
27662
- if (typeof objectClass !== "string" || !classes.has(objectClass)) continue;
27663
- layout["icon-image"] = rasterAtonIconExpression(objectClass);
28631
+ const objectClass2 = metadata2?.["s52:objectClass"];
28632
+ if (typeof objectClass2 !== "string" || !classes.has(objectClass2)) continue;
28633
+ layout["icon-image"] = rasterAtonIconExpression(objectClass2);
27664
28634
  }
27665
28635
  const metadata = style.metadata;
27666
28636
  metadata["chrt:symbol-set"] = "raster";
@@ -27692,19 +28662,19 @@ function parseSymbolFamily(raw) {
27692
28662
  const lower = raw.toLowerCase();
27693
28663
  return SYMBOL_FAMILIES.find((f) => f === lower) ?? null;
27694
28664
  }
27695
- function iconExpressionForFamily(objectClass, family) {
27696
- const s52 = getIconExpression(objectClass);
28665
+ function iconExpressionForFamily(objectClass2, family) {
28666
+ const s52 = getIconExpression(objectClass2);
27697
28667
  switch (family) {
27698
28668
  case "s52":
27699
28669
  return s52;
27700
28670
  case "s101":
27701
28671
  return rewriteIconToS101(s52);
27702
28672
  case "chrt": {
27703
- const build = CHRT_ICON_EXPRESSIONS[objectClass];
28673
+ const build = CHRT_ICON_EXPRESSIONS[objectClass2];
27704
28674
  return build ? build() : s52;
27705
28675
  }
27706
28676
  case "raster":
27707
- return RASTER_ATON_CLASSES.includes(objectClass) ? rasterAtonIconExpression(objectClass) : s52;
28677
+ return RASTER_ATON_CLASSES.includes(objectClass2) ? rasterAtonIconExpression(objectClass2) : s52;
27708
28678
  }
27709
28679
  }
27710
28680
 
@@ -27752,16 +28722,16 @@ var LARGE = {
27752
28722
  WRECKS: HAZARD_DEPTH
27753
28723
  };
27754
28724
  var LARGE_SOUNDING_PADDING_PX = 3.5;
27755
- function applyDepthLabelSize(objectClass, layer, size) {
28725
+ function applyDepthLabelSize(objectClass2, layer, size) {
27756
28726
  if (size !== "large") return layer;
27757
- const large = LARGE[objectClass];
28727
+ const large = LARGE[objectClass2];
27758
28728
  if (large === void 0) return layer;
27759
28729
  const layout = {
27760
28730
  ...layer.layout ?? {},
27761
28731
  "text-size": large.textSize,
27762
28732
  "text-font": [...large.font]
27763
28733
  };
27764
- if (objectClass === "SOUNDG") layout["text-padding"] = LARGE_SOUNDING_PADDING_PX;
28734
+ if (objectClass2 === "SOUNDG") layout["text-padding"] = LARGE_SOUNDING_PADDING_PX;
27765
28735
  return {
27766
28736
  ...layer,
27767
28737
  layout,
@@ -27958,47 +28928,47 @@ function buildLightLabelExpression() {
27958
28928
  ];
27959
28929
  return ["concat", litchrExpr, siggrpExpr, colourExpr, sigperExpr, valnmrExpr];
27960
28930
  }
27961
- function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts = {}) {
27962
- if (!S52_LOOKUP[objectClass]) return [];
27963
- const entry = S52_LOOKUP[objectClass];
28931
+ function buildLayersForClass(objectClass2, ct, sourceId = DEFAULT_REGION, opts = {}) {
28932
+ if (!S52_LOOKUP[objectClass2]) return [];
28933
+ const entry = S52_LOOKUP[objectClass2];
27964
28934
  const layers = [];
27965
- const geomTypes = getGeomTypesForClass(objectClass, entry?.style.type);
28935
+ const geomTypes = getGeomTypesForClass(objectClass2, entry?.style.type);
27966
28936
  for (const geomType of geomTypes) {
27967
- const resolved = resolveStyle(objectClass, geomType, ct, { safetyContour: opts.safetyContour });
27968
- const layerId = `s52-${objectClass}-${resolved.type}`;
28937
+ const resolved = resolveStyle(objectClass2, geomType, ct, { safetyContour: opts.safetyContour });
28938
+ const layerId = `s52-${objectClass2}-${resolved.type}`;
27969
28939
  const layer = {
27970
28940
  id: layerId,
27971
28941
  type: resolved.type,
27972
28942
  source: sourceId,
27973
- "source-layer": objectClass,
28943
+ "source-layer": objectClass2,
27974
28944
  paint: resolved.paint,
27975
28945
  metadata: {
27976
- "s52:objectClass": objectClass,
28946
+ "s52:objectClass": objectClass2,
27977
28947
  "s52:displayCategory": entry?.displayCategory ?? "OTHER",
27978
28948
  "s52:category": entry?.category ?? "Meta"
27979
28949
  }
27980
28950
  };
27981
- if (resolved.type === "line" && LINE_FROM_POLYGON_LAYERS.has(objectClass)) {
28951
+ if (resolved.type === "line" && LINE_FROM_POLYGON_LAYERS.has(objectClass2)) {
27982
28952
  layer.filter = GEOM_FILTERS.fill;
27983
28953
  } else if (GEOM_FILTERS[resolved.type]) {
27984
28954
  layer.filter = GEOM_FILTERS[resolved.type];
27985
28955
  }
27986
- if (SCAMIN_DECLUTTER_CLASSES.has(objectClass)) {
28956
+ if (SCAMIN_DECLUTTER_CLASSES.has(objectClass2)) {
27987
28957
  layer.filter = layer.filter ? ["all", layer.filter, scaminZoomGateFilter()] : ["all", scaminZoomGateFilter()];
27988
28958
  }
27989
28959
  if (resolved.minzoom) {
27990
28960
  layer.minzoom = resolved.minzoom;
27991
28961
  }
27992
- if (resolved.type === "fill" && OPAQUE_FILLS.has(objectClass)) {
28962
+ if (resolved.type === "fill" && OPAQUE_FILLS.has(objectClass2)) {
27993
28963
  layer.paint["fill-opacity"] = 1;
27994
28964
  }
27995
- if (resolved.type === "line" && ZOOM_SCALED_LINES2[objectClass]) {
27996
- const [z1, w1, z2, w2] = ZOOM_SCALED_LINES2[objectClass];
28965
+ if (resolved.type === "line" && ZOOM_SCALED_LINES2[objectClass2]) {
28966
+ const [z1, w1, z2, w2] = ZOOM_SCALED_LINES2[objectClass2];
27997
28967
  layer.paint["line-width"] = ["interpolate", ["linear"], ["zoom"], z1, w1, z2, w2];
27998
28968
  }
27999
28969
  if (resolved.type === "symbol") {
28000
- const iconExpr = getIconExpression(objectClass);
28001
- const childOffset = CHILD_ICON_OFFSETS[objectClass];
28970
+ const iconExpr = getIconExpression(objectClass2);
28971
+ const childOffset = CHILD_ICON_OFFSETS[objectClass2];
28002
28972
  const isSafetyCritical = SAFETY_GROUPS.has(entry?.viewingGroup ?? 0);
28003
28973
  const safetyCriticalIconSize = [
28004
28974
  "interpolate",
@@ -28121,7 +29091,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28121
29091
  ]
28122
29092
  };
28123
29093
  }
28124
- if (objectClass === "LIGHTS") {
29094
+ if (objectClass2 === "LIGHTS") {
28125
29095
  layer.layout = {
28126
29096
  ...layer.layout,
28127
29097
  "icon-allow-overlap": true,
@@ -28163,14 +29133,14 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28163
29133
  } else if (resolved.layout) {
28164
29134
  layer.layout = resolved.layout;
28165
29135
  }
28166
- if (objectClass === "LNDMRK" && resolved.type === "symbol") {
29136
+ if (objectClass2 === "LNDMRK" && resolved.type === "symbol") {
28167
29137
  const baseFilterParts = layer.filter && layer.filter[0] === "all" ? layer.filter.slice(1) : layer.filter ? [layer.filter] : [];
28168
29138
  const isWind = listAttrHas("CATLMK", ["18", "19"]);
28169
29139
  const isLighthouse = listAttrHas("CATLMK", ["17"]);
28170
29140
  const scaminFloor = scaminToMinZoom(entry?.scamin);
28171
29141
  const windLayer = {
28172
29142
  ...layer,
28173
- id: `s52-${objectClass}-wind-symbol`,
29143
+ id: `s52-${objectClass2}-wind-symbol`,
28174
29144
  filter: ["all", ...baseFilterParts, isWind],
28175
29145
  // Explicit, not inherited: turbines share exact coordinates with
28176
29146
  // their aviation-light LIGHTS feature; without allow-overlap the
@@ -28183,7 +29153,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28183
29153
  delete windLayer.minzoom;
28184
29154
  const lighthouseLayer = {
28185
29155
  ...layer,
28186
- id: `s52-${objectClass}-lighthouse-symbol`,
29156
+ id: `s52-${objectClass2}-lighthouse-symbol`,
28187
29157
  filter: ["all", ...baseFilterParts, isLighthouse, ["!", isWind]],
28188
29158
  layout: {
28189
29159
  ...layer.layout ?? {},
@@ -28194,7 +29164,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28194
29164
  delete lighthouseLayer.minzoom;
28195
29165
  const otherLayer = {
28196
29166
  ...layer,
28197
- id: `s52-${objectClass}-other-symbol`,
29167
+ id: `s52-${objectClass2}-other-symbol`,
28198
29168
  filter: ["all", ...baseFilterParts, ["!", isWind], ["!", isLighthouse]]
28199
29169
  };
28200
29170
  if (scaminFloor !== void 0) {
@@ -28209,10 +29179,10 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28209
29179
  const valsouNum = ["to-number", ["coalesce", ["get", "VALSOU"], ["get", "valsou"], 999], 999];
28210
29180
  const watlevNum = ["to-number", ["coalesce", ["get", "WATLEV"], ["get", "watlev"], 0], 0];
28211
29181
  layers.push({
28212
- id: `s52-${objectClass}-danger-ring`,
29182
+ id: `s52-${objectClass2}-danger-ring`,
28213
29183
  type: "symbol",
28214
29184
  source: sourceId,
28215
- "source-layer": objectClass,
29185
+ "source-layer": objectClass2,
28216
29186
  filter: [
28217
29187
  "all",
28218
29188
  GEOM_FILTERS.symbol,
@@ -28245,48 +29215,48 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28245
29215
  },
28246
29216
  paint: {},
28247
29217
  minzoom: 13,
28248
- metadata: { "s52:objectClass": objectClass, "s52:role": "isolated-danger-ring" }
29218
+ metadata: { "s52:objectClass": objectClass2, "s52:role": "isolated-danger-ring" }
28249
29219
  });
28250
29220
  }
28251
- if (resolved.type === "fill" && FILL_OUTLINE_LAYERS.has(objectClass)) {
28252
- const outlineColor = objectClass === "DEPARE" ? resolveColor("CHGRD", ct) : resolveColor("CHGRF", ct);
29221
+ if (resolved.type === "fill" && FILL_OUTLINE_LAYERS.has(objectClass2)) {
29222
+ const outlineColor = objectClass2 === "DEPARE" ? resolveColor("CHGRD", ct) : resolveColor("CHGRF", ct);
28253
29223
  layers.push({
28254
- id: `s52-${objectClass}-outline`,
29224
+ id: `s52-${objectClass2}-outline`,
28255
29225
  type: "line",
28256
29226
  source: sourceId,
28257
- "source-layer": objectClass,
29227
+ "source-layer": objectClass2,
28258
29228
  filter: GEOM_FILTERS.fill,
28259
29229
  paint: {
28260
29230
  "line-color": outlineColor,
28261
29231
  "line-width": 0.3,
28262
29232
  "line-opacity": 0.4
28263
29233
  },
28264
- metadata: { "s52:objectClass": objectClass, "s52:role": "outline" }
29234
+ metadata: { "s52:objectClass": objectClass2, "s52:role": "outline" }
28265
29235
  });
28266
29236
  }
28267
- if (resolved.type === "fill" && REGULATED_AREA_BOUNDARY_LAYERS[objectClass]) {
28268
- const lineStyle = REGULATED_AREA_BOUNDARY_LINESTYLES[objectClass];
29237
+ if (resolved.type === "fill" && REGULATED_AREA_BOUNDARY_LAYERS[objectClass2]) {
29238
+ const lineStyle = REGULATED_AREA_BOUNDARY_LINESTYLES[objectClass2];
28269
29239
  const boundaryPaint = lineStyle ? lineStylePaint(lineStyle) : {
28270
- "line-color": resolveColor(REGULATED_AREA_BOUNDARY_LAYERS[objectClass], ct),
29240
+ "line-color": resolveColor(REGULATED_AREA_BOUNDARY_LAYERS[objectClass2], ct),
28271
29241
  "line-width": 1,
28272
29242
  "line-dasharray": [4, 2],
28273
29243
  "line-opacity": 0.85
28274
29244
  };
28275
29245
  layers.push({
28276
- id: `s52-${objectClass}-boundary`,
29246
+ id: `s52-${objectClass2}-boundary`,
28277
29247
  type: "line",
28278
29248
  source: sourceId,
28279
- "source-layer": objectClass,
29249
+ "source-layer": objectClass2,
28280
29250
  filter: GEOM_FILTERS.fill,
28281
29251
  paint: boundaryPaint,
28282
29252
  metadata: {
28283
- "s52:objectClass": objectClass,
29253
+ "s52:objectClass": objectClass2,
28284
29254
  "s52:role": "boundary",
28285
29255
  ...lineStyle ? { "s52:lineStyle": lineStyle } : {}
28286
29256
  }
28287
29257
  });
28288
29258
  }
28289
- if (objectClass === "ACHARE" && resolved.type === "fill") {
29259
+ if (objectClass2 === "ACHARE" && resolved.type === "fill") {
28290
29260
  layers.push({
28291
29261
  id: "s52-ACHARE-boundary",
28292
29262
  type: "line",
@@ -28302,7 +29272,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28302
29272
  metadata: { "s52:objectClass": "ACHARE", "s52:role": "boundary" }
28303
29273
  });
28304
29274
  }
28305
- if (objectClass === "DRGARE" && resolved.type === "fill") {
29275
+ if (objectClass2 === "DRGARE" && resolved.type === "fill") {
28306
29276
  layers.push({
28307
29277
  id: "s52-DRGARE-boundary",
28308
29278
  type: "line",
@@ -28318,7 +29288,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28318
29288
  metadata: { "s52:objectClass": "DRGARE", "s52:role": "boundary" }
28319
29289
  });
28320
29290
  }
28321
- if (objectClass === "ACHARE" && resolved.type === "fill") {
29291
+ if (objectClass2 === "ACHARE" && resolved.type === "fill") {
28322
29292
  layers.push({
28323
29293
  id: "s52-ACHARE-anchor-icon",
28324
29294
  type: "symbol",
@@ -28347,7 +29317,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28347
29317
  metadata: { "s52:objectClass": "ACHARE", "s52:role": "centroid-icon" }
28348
29318
  });
28349
29319
  }
28350
- if (objectClass === "LNDARE" && resolved.type === "fill") {
29320
+ if (objectClass2 === "LNDARE" && resolved.type === "fill") {
28351
29321
  layers.push({
28352
29322
  id: `s52-LNDARE-point`,
28353
29323
  type: "circle",
@@ -28363,7 +29333,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28363
29333
  metadata: { "s52:objectClass": "LNDARE", "s52:role": "point" }
28364
29334
  });
28365
29335
  }
28366
- if (objectClass === "LNDARE" && resolved.type === "fill") {
29336
+ if (objectClass2 === "LNDARE" && resolved.type === "fill") {
28367
29337
  layers.push({
28368
29338
  id: `s52-LNDARE-line`,
28369
29339
  type: "line",
@@ -28377,7 +29347,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28377
29347
  metadata: { "s52:objectClass": "LNDARE", "s52:role": "line" }
28378
29348
  });
28379
29349
  }
28380
- if (objectClass === "BUAARE" && resolved.type === "fill") {
29350
+ if (objectClass2 === "BUAARE" && resolved.type === "fill") {
28381
29351
  layers.push({
28382
29352
  id: `s52-BUAARE-point`,
28383
29353
  type: "circle",
@@ -28393,7 +29363,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28393
29363
  metadata: { "s52:objectClass": "BUAARE", "s52:role": "point" }
28394
29364
  });
28395
29365
  }
28396
- if (objectClass === "RIVERS" && resolved.type === "fill") {
29366
+ if (objectClass2 === "RIVERS" && resolved.type === "fill") {
28397
29367
  layers.push({
28398
29368
  id: `s52-RIVERS-line`,
28399
29369
  type: "line",
@@ -28407,7 +29377,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28407
29377
  metadata: { "s52:objectClass": "RIVERS", "s52:role": "line" }
28408
29378
  });
28409
29379
  }
28410
- if (objectClass === "BUISGL" && resolved.type === "circle") {
29380
+ if (objectClass2 === "BUISGL" && resolved.type === "circle") {
28411
29381
  layers.push({
28412
29382
  id: `s52-BUISGL-polygon`,
28413
29383
  type: "fill",
@@ -28433,7 +29403,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28433
29403
  metadata: { "s52:objectClass": "BUISGL", "s52:role": "polygon-outline" }
28434
29404
  });
28435
29405
  }
28436
- if (objectClass === "BRIDGE" && resolved.type === "line") {
29406
+ if (objectClass2 === "BRIDGE" && resolved.type === "line") {
28437
29407
  layers.push({
28438
29408
  id: `s52-BRIDGE-polygon`,
28439
29409
  type: "fill",
@@ -28459,7 +29429,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28459
29429
  metadata: { "s52:objectClass": "BRIDGE", "s52:role": "polygon-outline" }
28460
29430
  });
28461
29431
  }
28462
- if (objectClass === "PYLONS" && resolved.type === "fill") {
29432
+ if (objectClass2 === "PYLONS" && resolved.type === "fill") {
28463
29433
  layers.push({
28464
29434
  id: `s52-PYLONS-point`,
28465
29435
  type: "circle",
@@ -28475,7 +29445,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28475
29445
  metadata: { "s52:objectClass": "PYLONS", "s52:role": "point" }
28476
29446
  });
28477
29447
  }
28478
- if (objectClass === "WEDKLP" && resolved.type === "fill") {
29448
+ if (objectClass2 === "WEDKLP" && resolved.type === "fill") {
28479
29449
  layers.push({
28480
29450
  id: `s52-WEDKLP-point`,
28481
29451
  type: "circle",
@@ -28492,7 +29462,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28492
29462
  metadata: { "s52:objectClass": "WEDKLP", "s52:role": "point" }
28493
29463
  });
28494
29464
  }
28495
- if (objectClass === "SLOGRD" && resolved.type === "fill") {
29465
+ if (objectClass2 === "SLOGRD" && resolved.type === "fill") {
28496
29466
  layers.push({
28497
29467
  id: `s52-SLOGRD-point`,
28498
29468
  type: "circle",
@@ -28509,7 +29479,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28509
29479
  metadata: { "s52:objectClass": "SLOGRD", "s52:role": "point" }
28510
29480
  });
28511
29481
  }
28512
- if (objectClass === "RUNWAY" && resolved.type === "fill") {
29482
+ if (objectClass2 === "RUNWAY" && resolved.type === "fill") {
28513
29483
  layers.push({
28514
29484
  id: `s52-RUNWAY-line`,
28515
29485
  type: "line",
@@ -28523,7 +29493,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28523
29493
  metadata: { "s52:objectClass": "RUNWAY", "s52:role": "line" }
28524
29494
  });
28525
29495
  }
28526
- if (objectClass === "FORSTC" && resolved.type === "fill") {
29496
+ if (objectClass2 === "FORSTC" && resolved.type === "fill") {
28527
29497
  layers.push({
28528
29498
  id: `s52-FORSTC-line`,
28529
29499
  type: "line",
@@ -28537,7 +29507,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28537
29507
  metadata: { "s52:objectClass": "FORSTC", "s52:role": "line" }
28538
29508
  });
28539
29509
  }
28540
- if (objectClass === "FSHFAC" && resolved.type === "fill") {
29510
+ if (objectClass2 === "FSHFAC" && resolved.type === "fill") {
28541
29511
  layers.push({
28542
29512
  id: `s52-FSHFAC-point`,
28543
29513
  type: "circle",
@@ -28553,7 +29523,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28553
29523
  metadata: { "s52:objectClass": "FSHFAC", "s52:role": "point" }
28554
29524
  });
28555
29525
  }
28556
- if (objectClass === "OBSTRN" && resolved.type === "symbol") {
29526
+ if (objectClass2 === "OBSTRN" && resolved.type === "symbol") {
28557
29527
  layers.push({
28558
29528
  id: `s52-OBSTRN-polygon`,
28559
29529
  type: "fill",
@@ -28593,7 +29563,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28593
29563
  metadata: { "s52:objectClass": "OBSTRN", "s52:role": "line" }
28594
29564
  });
28595
29565
  }
28596
- if (objectClass === "SLCONS" && resolved.type === "line") {
29566
+ if (objectClass2 === "SLCONS" && resolved.type === "line") {
28597
29567
  layers.push({
28598
29568
  id: `s52-SLCONS-polygon`,
28599
29569
  type: "fill",
@@ -28633,7 +29603,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28633
29603
  metadata: { "s52:objectClass": "SLCONS", "s52:role": "point" }
28634
29604
  });
28635
29605
  }
28636
- if (objectClass === "PRDARE" && resolved.type === "line") {
29606
+ if (objectClass2 === "PRDARE" && resolved.type === "line") {
28637
29607
  layers.push({
28638
29608
  id: `s52-PRDARE-point`,
28639
29609
  type: "circle",
@@ -28650,7 +29620,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28650
29620
  metadata: { "s52:objectClass": "PRDARE", "s52:role": "point" }
28651
29621
  });
28652
29622
  }
28653
- if (objectClass === "CTNARE" && resolved.type === "line") {
29623
+ if (objectClass2 === "CTNARE" && resolved.type === "line") {
28654
29624
  layers.push({
28655
29625
  id: `s52-CTNARE-point`,
28656
29626
  type: "circle",
@@ -28666,7 +29636,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28666
29636
  metadata: { "s52:objectClass": "CTNARE", "s52:role": "point" }
28667
29637
  });
28668
29638
  }
28669
- if (objectClass === "PONTON" && resolved.type === "line") {
29639
+ if (objectClass2 === "PONTON" && resolved.type === "line") {
28670
29640
  layers.push({
28671
29641
  id: `s52-PONTON-polygon`,
28672
29642
  type: "fill",
@@ -28692,7 +29662,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28692
29662
  metadata: { "s52:objectClass": "PONTON", "s52:role": "polygon-outline" }
28693
29663
  });
28694
29664
  }
28695
- if (objectClass === "WRECKS" && resolved.type === "symbol") {
29665
+ if (objectClass2 === "WRECKS" && resolved.type === "symbol") {
28696
29666
  layers.push({
28697
29667
  id: `s52-WRECKS-polygon`,
28698
29668
  type: "fill",
@@ -28706,1179 +29676,729 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28706
29676
  metadata: { "s52:objectClass": "WRECKS", "s52:role": "polygon" }
28707
29677
  });
28708
29678
  layers.push({
28709
- id: `s52-WRECKS-polygon-outline`,
28710
- type: "line",
28711
- source: sourceId,
28712
- "source-layer": "WRECKS",
28713
- filter: GEOM_FILTERS.fill,
28714
- paint: {
28715
- "line-color": resolveColor("CHBLK", ct),
28716
- "line-width": 1,
28717
- "line-dasharray": [3, 2]
28718
- },
28719
- metadata: { "s52:objectClass": "WRECKS", "s52:role": "polygon-outline" }
28720
- });
28721
- }
28722
- if (objectClass === "WATTUR" && resolved.type === "symbol") {
28723
- layers.push({
28724
- id: `s52-WATTUR-polygon`,
28725
- type: "fill",
28726
- source: sourceId,
28727
- "source-layer": "WATTUR",
28728
- filter: GEOM_FILTERS.fill,
28729
- paint: {
28730
- "fill-color": resolveColor("CHBLK", ct),
28731
- "fill-opacity": 0.05
28732
- },
28733
- metadata: { "s52:objectClass": "WATTUR", "s52:role": "polygon" }
28734
- });
28735
- }
28736
- if (objectClass === "MAGVAR" && resolved.type === "line") {
28737
- layers.push({
28738
- id: "s52-MAGVAR-point",
28739
- type: "circle",
28740
- source: sourceId,
28741
- "source-layer": "MAGVAR",
28742
- filter: GEOM_FILTERS.circle,
28743
- paint: {
28744
- "circle-color": resolveColor("CHMGD", ct),
28745
- "circle-radius": ["interpolate", ["linear"], ["zoom"], 6, 1, 12, 2],
28746
- "circle-stroke-color": resolveColor("CHBLK", ct),
28747
- "circle-stroke-width": 0.4,
28748
- "circle-opacity": 0.4
28749
- },
28750
- metadata: { "s52:objectClass": "MAGVAR", "s52:role": "point" }
28751
- });
28752
- }
28753
- if (objectClass === "DMPGRD" && resolved.type === "fill") {
28754
- layers.push({
28755
- id: `s52-DMPGRD-point`,
28756
- type: "circle",
28757
- source: sourceId,
28758
- "source-layer": "DMPGRD",
28759
- filter: GEOM_FILTERS.circle,
28760
- paint: {
28761
- "circle-color": resolveColor("LITYW", ct),
28762
- "circle-radius": ["interpolate", ["linear"], ["zoom"], 10, 1.5, 14, 2.5, 17, 3.6],
28763
- "circle-stroke-color": resolveColor("CHBLK", ct),
28764
- "circle-stroke-width": 0.5,
28765
- "circle-opacity": 0.7
28766
- },
28767
- metadata: { "s52:objectClass": "DMPGRD", "s52:role": "point" }
28768
- });
28769
- }
28770
- if (objectClass === "DAMCON" && resolved.type === "line") {
28771
- layers.push({
28772
- id: `s52-DAMCON-polygon`,
28773
- type: "fill",
28774
- source: sourceId,
28775
- "source-layer": "DAMCON",
28776
- filter: GEOM_FILTERS.fill,
28777
- paint: {
28778
- "fill-color": resolveColor("CHBRN", ct),
28779
- "fill-opacity": 0.4
28780
- },
28781
- metadata: { "s52:objectClass": "DAMCON", "s52:role": "polygon" }
28782
- });
28783
- layers.push({
28784
- id: `s52-DAMCON-polygon-outline`,
28785
- type: "line",
28786
- source: sourceId,
28787
- "source-layer": "DAMCON",
28788
- filter: GEOM_FILTERS.fill,
28789
- paint: {
28790
- "line-color": resolveColor("CHBLK", ct),
28791
- "line-width": 0.6
28792
- },
28793
- metadata: { "s52:objectClass": "DAMCON", "s52:role": "outline" }
28794
- });
28795
- }
28796
- if (objectClass === "GATCON" && resolved.type === "line") {
28797
- layers.push({
28798
- id: `s52-GATCON-polygon`,
28799
- type: "fill",
28800
- source: sourceId,
28801
- "source-layer": "GATCON",
28802
- filter: GEOM_FILTERS.fill,
28803
- paint: {
28804
- "fill-color": resolveColor("CHBRN", ct),
28805
- "fill-opacity": 0.4
28806
- },
28807
- metadata: { "s52:objectClass": "GATCON", "s52:role": "polygon" }
28808
- });
28809
- layers.push({
28810
- id: `s52-GATCON-polygon-outline`,
28811
- type: "line",
28812
- source: sourceId,
28813
- "source-layer": "GATCON",
28814
- filter: GEOM_FILTERS.fill,
28815
- paint: {
28816
- "line-color": resolveColor("CHBLK", ct),
28817
- "line-width": 0.6
28818
- },
28819
- metadata: { "s52:objectClass": "GATCON", "s52:role": "outline" }
28820
- });
28821
- }
28822
- if (objectClass === "CANALS" && resolved.type === "line") {
28823
- layers.push({
28824
- id: `s52-CANALS-polygon`,
28825
- type: "fill",
28826
- source: sourceId,
28827
- "source-layer": "CANALS",
28828
- filter: GEOM_FILTERS.fill,
28829
- paint: {
28830
- "fill-color": resolveColor("DEPMS", ct),
28831
- "fill-opacity": 0.5
28832
- },
28833
- metadata: { "s52:objectClass": "CANALS", "s52:role": "polygon" }
28834
- });
28835
- }
28836
- if (objectClass === "PILBOP" && resolved.type === "symbol") {
28837
- layers.push({
28838
- id: `s52-PILBOP-polygon`,
28839
- type: "fill",
28840
- source: sourceId,
28841
- "source-layer": "PILBOP",
28842
- filter: GEOM_FILTERS.fill,
28843
- paint: {
28844
- "fill-color": resolveColor("CHMGD", ct),
28845
- "fill-opacity": 0.1
28846
- },
28847
- metadata: { "s52:objectClass": "PILBOP", "s52:role": "polygon" }
28848
- });
28849
- layers.push({
28850
- id: `s52-PILBOP-polygon-outline`,
29679
+ id: `s52-WRECKS-polygon-outline`,
28851
29680
  type: "line",
28852
29681
  source: sourceId,
28853
- "source-layer": "PILBOP",
29682
+ "source-layer": "WRECKS",
28854
29683
  filter: GEOM_FILTERS.fill,
28855
29684
  paint: {
28856
- "line-color": resolveColor("CHMGD", ct),
28857
- "line-width": 0.8,
28858
- "line-dasharray": [4, 2]
29685
+ "line-color": resolveColor("CHBLK", ct),
29686
+ "line-width": 1,
29687
+ "line-dasharray": [3, 2]
28859
29688
  },
28860
- metadata: { "s52:objectClass": "PILBOP", "s52:role": "outline" }
29689
+ metadata: { "s52:objectClass": "WRECKS", "s52:role": "polygon-outline" }
28861
29690
  });
28862
29691
  }
28863
- if (objectClass === "HRBFAC" && resolved.type === "circle") {
29692
+ if (objectClass2 === "WATTUR" && resolved.type === "symbol") {
28864
29693
  layers.push({
28865
- id: `s52-HRBFAC-polygon`,
29694
+ id: `s52-WATTUR-polygon`,
28866
29695
  type: "fill",
28867
29696
  source: sourceId,
28868
- "source-layer": "HRBFAC",
29697
+ "source-layer": "WATTUR",
28869
29698
  filter: GEOM_FILTERS.fill,
28870
29699
  paint: {
28871
- "fill-color": resolveColor("TRFCD", ct),
28872
- "fill-opacity": 0.15
29700
+ "fill-color": resolveColor("CHBLK", ct),
29701
+ "fill-opacity": 0.05
28873
29702
  },
28874
- metadata: { "s52:objectClass": "HRBFAC", "s52:role": "polygon" }
29703
+ metadata: { "s52:objectClass": "WATTUR", "s52:role": "polygon" }
28875
29704
  });
28876
29705
  }
28877
- if (objectClass === "MARCUL" && resolved.type === "fill") {
29706
+ if (objectClass2 === "MAGVAR" && resolved.type === "line") {
28878
29707
  layers.push({
28879
- id: `s52-MARCUL-point`,
29708
+ id: "s52-MAGVAR-point",
28880
29709
  type: "circle",
28881
29710
  source: sourceId,
28882
- "source-layer": "MARCUL",
29711
+ "source-layer": "MAGVAR",
28883
29712
  filter: GEOM_FILTERS.circle,
28884
29713
  paint: {
28885
- "circle-color": resolveColor("CHBRN", ct),
28886
- "circle-radius": ["interpolate", ["linear"], ["zoom"], 12, 1.2, 14, 1.8, 17, 2.6],
29714
+ "circle-color": resolveColor("CHMGD", ct),
29715
+ "circle-radius": ["interpolate", ["linear"], ["zoom"], 6, 1, 12, 2],
28887
29716
  "circle-stroke-color": resolveColor("CHBLK", ct),
28888
29717
  "circle-stroke-width": 0.4,
28889
- "circle-opacity": 0.6
29718
+ "circle-opacity": 0.4
28890
29719
  },
28891
- metadata: { "s52:objectClass": "MARCUL", "s52:role": "point" }
29720
+ metadata: { "s52:objectClass": "MAGVAR", "s52:role": "point" }
28892
29721
  });
29722
+ }
29723
+ if (objectClass2 === "DMPGRD" && resolved.type === "fill") {
28893
29724
  layers.push({
28894
- id: `s52-MARCUL-line`,
28895
- type: "line",
29725
+ id: `s52-DMPGRD-point`,
29726
+ type: "circle",
28896
29727
  source: sourceId,
28897
- "source-layer": "MARCUL",
28898
- filter: GEOM_FILTERS.line,
29728
+ "source-layer": "DMPGRD",
29729
+ filter: GEOM_FILTERS.circle,
28899
29730
  paint: {
28900
- "line-color": resolveColor("CHBLK", ct),
28901
- "line-width": 0.5,
28902
- "line-dasharray": [2, 2]
29731
+ "circle-color": resolveColor("LITYW", ct),
29732
+ "circle-radius": ["interpolate", ["linear"], ["zoom"], 10, 1.5, 14, 2.5, 17, 3.6],
29733
+ "circle-stroke-color": resolveColor("CHBLK", ct),
29734
+ "circle-stroke-width": 0.5,
29735
+ "circle-opacity": 0.7
28903
29736
  },
28904
- metadata: { "s52:objectClass": "MARCUL", "s52:role": "line" }
29737
+ metadata: { "s52:objectClass": "DMPGRD", "s52:role": "point" }
28905
29738
  });
28906
29739
  }
28907
- if (objectClass === "RECTRC" && resolved.type === "line") {
29740
+ if (objectClass2 === "DAMCON" && resolved.type === "line") {
28908
29741
  layers.push({
28909
- id: `s52-RECTRC-polygon`,
29742
+ id: `s52-DAMCON-polygon`,
28910
29743
  type: "fill",
28911
29744
  source: sourceId,
28912
- "source-layer": "RECTRC",
29745
+ "source-layer": "DAMCON",
28913
29746
  filter: GEOM_FILTERS.fill,
28914
29747
  paint: {
28915
- "fill-color": resolveColor("TRFCD", ct),
28916
- "fill-opacity": 0.05
29748
+ "fill-color": resolveColor("CHBRN", ct),
29749
+ "fill-opacity": 0.4
28917
29750
  },
28918
- metadata: { "s52:objectClass": "RECTRC", "s52:role": "polygon" }
29751
+ metadata: { "s52:objectClass": "DAMCON", "s52:role": "polygon" }
28919
29752
  });
28920
- }
28921
- if (objectClass === "PONTON" && resolved.type === "fill") {
28922
29753
  layers.push({
28923
- id: `s52-PONTON-line`,
29754
+ id: `s52-DAMCON-polygon-outline`,
28924
29755
  type: "line",
28925
29756
  source: sourceId,
28926
- "source-layer": "PONTON",
28927
- filter: GEOM_FILTERS.line,
29757
+ "source-layer": "DAMCON",
29758
+ filter: GEOM_FILTERS.fill,
28928
29759
  paint: {
28929
- "line-color": resolveColor("CHGRF", ct),
28930
- "line-width": 1.5
29760
+ "line-color": resolveColor("CHBLK", ct),
29761
+ "line-width": 0.6
28931
29762
  },
28932
- metadata: { "s52:objectClass": "PONTON", "s52:role": "line" }
29763
+ metadata: { "s52:objectClass": "DAMCON", "s52:role": "outline" }
28933
29764
  });
28934
29765
  }
28935
- if (objectClass === "SILTNK" && resolved.type === "symbol") {
29766
+ if (objectClass2 === "GATCON" && resolved.type === "line") {
28936
29767
  layers.push({
28937
- id: `s52-SILTNK-symbol-polygon`,
28938
- type: "symbol",
29768
+ id: `s52-GATCON-polygon`,
29769
+ type: "fill",
28939
29770
  source: sourceId,
28940
- "source-layer": "SILTNK",
29771
+ "source-layer": "GATCON",
28941
29772
  filter: GEOM_FILTERS.fill,
28942
- layout: {
28943
- "icon-image": "TNKCON02",
28944
- "icon-size": 0.6,
28945
- "icon-allow-overlap": true,
28946
- "icon-ignore-placement": false,
28947
- "symbol-placement": "point"
29773
+ paint: {
29774
+ "fill-color": resolveColor("CHBRN", ct),
29775
+ "fill-opacity": 0.4
28948
29776
  },
28949
- paint: {},
28950
- metadata: { "s52:objectClass": "SILTNK", "s52:role": "symbol-polygon" }
29777
+ metadata: { "s52:objectClass": "GATCON", "s52:role": "polygon" }
28951
29778
  });
28952
- }
28953
- if (objectClass === "ACHBRT" && resolved.type === "symbol") {
28954
29779
  layers.push({
28955
- id: `s52-ACHBRT-symbol-polygon`,
28956
- type: "symbol",
29780
+ id: `s52-GATCON-polygon-outline`,
29781
+ type: "line",
28957
29782
  source: sourceId,
28958
- "source-layer": "ACHBRT",
29783
+ "source-layer": "GATCON",
28959
29784
  filter: GEOM_FILTERS.fill,
28960
- layout: {
28961
- "icon-image": "ACHARE02",
28962
- "icon-size": 0.5,
28963
- "icon-allow-overlap": true,
28964
- "icon-ignore-placement": false,
28965
- "symbol-placement": "point"
28966
- },
28967
- paint: {},
28968
- metadata: { "s52:objectClass": "ACHBRT", "s52:role": "symbol-polygon" }
28969
- });
28970
- }
28971
- if (objectClass === "BERTHS" && resolved.type === "line") {
28972
- layers.push({
28973
- id: `s52-BERTHS-circle`,
28974
- type: "circle",
28975
- source: sourceId,
28976
- "source-layer": "BERTHS",
28977
- filter: GEOM_FILTERS.circle,
28978
29785
  paint: {
28979
- "circle-color": resolveColor("CHBLK", ct),
28980
- "circle-radius": ["interpolate", ["linear"], ["zoom"], 12, 1.5, 14, 2.2, 17, 3],
28981
- "circle-stroke-color": resolveColor("CHBLK", ct),
28982
- "circle-stroke-width": 0.5
29786
+ "line-color": resolveColor("CHBLK", ct),
29787
+ "line-width": 0.6
28983
29788
  },
28984
- metadata: { "s52:objectClass": "BERTHS", "s52:role": "point" }
29789
+ metadata: { "s52:objectClass": "GATCON", "s52:role": "outline" }
28985
29790
  });
28986
29791
  }
28987
- if (objectClass === "TUNNEL" && resolved.type === "line") {
29792
+ if (objectClass2 === "CANALS" && resolved.type === "line") {
28988
29793
  layers.push({
28989
- // NOT `s52-TUNNEL-line` — that id is already taken by the main layer
28990
- // (`s52-${objectClass}-${resolved.type}`), and duplicate layer ids
28991
- // are a Mapbox style-spec violation (caught by style-integrity.test).
28992
- id: `s52-TUNNEL-linestring`,
28993
- type: "line",
29794
+ id: `s52-CANALS-polygon`,
29795
+ type: "fill",
28994
29796
  source: sourceId,
28995
- "source-layer": "TUNNEL",
28996
- filter: GEOM_FILTERS.line,
29797
+ "source-layer": "CANALS",
29798
+ filter: GEOM_FILTERS.fill,
28997
29799
  paint: {
28998
- "line-color": resolveColor("CHGRD", ct),
28999
- "line-width": 1,
29000
- "line-dasharray": [2, 2]
29800
+ "fill-color": resolveColor("DEPMS", ct),
29801
+ "fill-opacity": 0.5
29001
29802
  },
29002
- metadata: { "s52:objectClass": "TUNNEL", "s52:role": "line" }
29003
- });
29004
- }
29005
- }
29006
- return layers;
29007
- }
29008
- function buildLabelLayer2(objectClass, ct, sourceId = DEFAULT_REGION, depthUnits = "meters", labelOpts = {}) {
29009
- if (!LABELED_LAYERS.has(objectClass)) return null;
29010
- const config = getLabelConfig(objectClass, {
29011
- colorTable: ct,
29012
- safetyContour: labelOpts.safetyContour,
29013
- twoShadeSoundings: labelOpts.twoShadeSoundings,
29014
- soundingFontWeight: labelOpts.soundingFontWeight
29015
- });
29016
- if (!config) return null;
29017
- let textField = config.textField;
29018
- if (objectClass === "LIGHTS") {
29019
- textField = buildLightLabelExpression();
29020
- }
29021
- if (typeof textField !== "string" && Array.isArray(textField)) {
29022
- const flat = JSON.stringify(textField);
29023
- if (flat.includes("_navAidLabel")) {
29024
- textField = buildNavAidLabelExpression();
29025
- }
29026
- }
29027
- if (depthUnits === "feet") {
29028
- if (objectClass === "SOUNDG") {
29029
- textField = [
29030
- "concat",
29031
- [
29032
- "round",
29033
- [
29034
- "*",
29035
- [
29036
- "to-number",
29037
- ["coalesce", ["get", "depth"], ["get", "DEPTH"], ["get", "VALSOU"]],
29038
- 0
29039
- ],
29040
- 3.28084
29041
- ]
29042
- ],
29043
- ""
29044
- ];
29045
- } else if (objectClass === "DEPCNT") {
29046
- textField = [
29047
- "concat",
29048
- ["round", ["*", ["to-number", ["get", "VALDCO"]], 3.28084]],
29049
- "ft"
29050
- ];
29051
- } else if (objectClass === "UWTROC" || objectClass === "OBSTRN" || objectClass === "WRECKS") {
29052
- const valsouFt = [
29053
- "to-number",
29054
- ["coalesce", ["get", "VALSOU"], ["get", "valsou"], 0],
29055
- 0
29056
- ];
29057
- textField = [
29058
- "case",
29059
- [">", valsouFt, 0],
29060
- ["concat", ["to-string", ["round", ["*", valsouFt, 3.28084]]], "ft"],
29061
- ""
29062
- ];
29063
- }
29064
- }
29065
- const labelPriority = getLabelPriority(objectClass);
29066
- const layer = {
29067
- id: `s52-${objectClass}-label`,
29068
- type: "symbol",
29069
- source: sourceId,
29070
- "source-layer": objectClass,
29071
- filter: GEOM_FILTERS.symbol,
29072
- layout: {
29073
- "text-field": textField,
29074
- "text-size": config.textSize,
29075
- "text-font": config.textFont ?? ["Open Sans Regular"],
29076
- "text-offset": config.textOffset,
29077
- "text-anchor": "top",
29078
- "text-allow-overlap": false,
29079
- "text-optional": true,
29080
- "text-padding": 4,
29081
- "symbol-sort-key": labelPriority * 1e3,
29082
- ...config.layout ?? {}
29083
- },
29084
- paint: {
29085
- "text-color": config.textColor,
29086
- "text-halo-color": config.textHaloColor,
29087
- "text-halo-width": config.textHaloWidth
29088
- },
29089
- metadata: {
29090
- "s52:objectClass": objectClass,
29091
- "chrt:labelPriority": labelPriority
29092
- }
29093
- };
29094
- if (config.minzoom) {
29095
- layer.minzoom = config.minzoom;
29096
- }
29097
- if (objectClass === "DEPCNT") {
29098
- layer.filter = GEOM_FILTERS.line;
29099
- }
29100
- if (objectClass === "CBLOHD" || objectClass === "PIPOHD") {
29101
- layer.filter = GEOM_FILTERS.line;
29102
- }
29103
- if (objectClass === "SBDARE") {
29104
- delete layer.filter;
29105
- }
29106
- if (objectClass === "MAGVAR") {
29107
- delete layer.filter;
29108
- }
29109
- if (getLabelBehavior(objectClass) === "orientation-cross-band") {
29110
- layer.filter = LABEL_ANCHOR_FILTER;
29111
- }
29112
- return applyDepthLabelSize(objectClass, layer, labelOpts.depthLabelSize);
29113
- }
29114
- function getGeomTypesForClass(objectClass, defaultType) {
29115
- if (SYMBOL_MAP[objectClass] && defaultType === "symbol") {
29116
- return ["symbol"];
29117
- }
29118
- if (objectClass === "SOUNDG") {
29119
- return ["circle"];
29120
- }
29121
- if (defaultType) {
29122
- return [defaultType];
29123
- }
29124
- return ["circle"];
29125
- }
29126
- function getOrderedClasses() {
29127
- const ordered = [...S52_DRAW_ORDER];
29128
- for (const key of Object.keys(S52_LOOKUP)) {
29129
- if (!ordered.includes(key)) {
29130
- const insertIdx = ordered.indexOf("SOUNDG");
29131
- if (insertIdx >= 0) {
29132
- ordered.splice(insertIdx, 0, key);
29133
- } else {
29134
- ordered.push(key);
29135
- }
29803
+ metadata: { "s52:objectClass": "CANALS", "s52:role": "polygon" }
29804
+ });
29136
29805
  }
29137
- }
29138
- return ordered;
29139
- }
29140
-
29141
- // src/atlas/terrain-order.ts
29142
- var DRAPEABLE_LAYER_TYPES = /* @__PURE__ */ new Set([
29143
- "background",
29144
- "fill",
29145
- "line",
29146
- "raster",
29147
- "hillshade",
29148
- "color-relief"
29149
- ]);
29150
- function isDrapeable(layer) {
29151
- return DRAPEABLE_LAYER_TYPES.has(layer.type);
29152
- }
29153
- function drapedFirst(layers) {
29154
- const draped = [];
29155
- const direct = [];
29156
- for (const l of layers) (isDrapeable(l) ? draped : direct).push(l);
29157
- return [...draped, ...direct];
29158
- }
29159
- function terrainStackCount(layers) {
29160
- let stacks = 0;
29161
- let prevDraped = false;
29162
- for (const l of layers) {
29163
- const d = isDrapeable(l);
29164
- if (d && !prevDraped) stacks++;
29165
- prevDraped = d;
29166
- }
29167
- return stacks;
29168
- }
29169
- function drapeHalves(layers) {
29170
- const draped = [];
29171
- const direct = [];
29172
- for (const l of layers) (isDrapeable(l) ? draped : direct).push(l);
29173
- return { draped, direct };
29174
- }
29175
-
29176
- // src/atlas/lamps.ts
29177
- var LAMP_GLOW_LAYER_ID = "s52-LIGHTS-lamp-glow";
29178
- var LAMP_CORE_LAYER_ID = "s52-LIGHTS-lamp-core";
29179
- var LAMP_LAYER_IDS = [LAMP_GLOW_LAYER_ID, LAMP_CORE_LAYER_ID];
29180
- var RANGE_NM = ["to-number", ["coalesce", ["get", "VALNMR"], ["get", "valnmr"]], 6];
29181
- var UNSECTORED_POINTS = [
29182
- "all",
29183
- ["any", ["==", ["geometry-type"], "Point"], ["==", ["geometry-type"], "MultiPoint"]],
29184
- ["!", ["has", "SECTR1"]],
29185
- // …and not lights with a charted rhythm either: those flash, and the
29186
- // flashing is computed by the app from `SIGSEQ` (light-sigseq.ts). What is
29187
- // left here is exactly the set that is steady from every bearing.
29188
- ["!", ["has", "SIGSEQ"]]
29189
- ];
29190
- function buildLampLayers(sourceId, ct) {
29191
- const colour = lightFillExpression(ct);
29192
- const common = {
29193
- type: "circle",
29194
- source: sourceId,
29195
- "source-layer": "LIGHTS",
29196
- filter: UNSECTORED_POINTS,
29197
- layout: { visibility: "none" }
29198
- };
29199
- const metadata = { "chrt:role": "lamp", "s52:objectClass": "LIGHTS" };
29200
- return [
29201
- {
29202
- ...common,
29203
- id: LAMP_GLOW_LAYER_ID,
29204
- paint: {
29205
- "circle-color": colour,
29206
- // 4 nm → 7 px, 12 nm → 12 px, 25 nm → 20 px; capped, because a
29207
- // primary seacoast light is not a searchlight.
29208
- "circle-radius": ["interpolate", ["linear"], RANGE_NM, 0, 5, 4, 7, 12, 12, 25, 20],
29209
- "circle-blur": 1,
29210
- "circle-opacity": 0.55,
29211
- // A lamp is a point of light: it does not grow as it comes near, and
29212
- // it does not lie down with the chart.
29213
- "circle-pitch-alignment": "viewport",
29214
- "circle-pitch-scale": "viewport"
29215
- },
29216
- metadata: { ...metadata, "chrt:lamp": "glow" }
29217
- },
29218
- {
29219
- ...common,
29220
- id: LAMP_CORE_LAYER_ID,
29221
- paint: {
29222
- "circle-color": colour,
29223
- "circle-radius": ["interpolate", ["linear"], RANGE_NM, 0, 1.5, 12, 2.5, 25, 3.5],
29224
- "circle-blur": 0.25,
29225
- "circle-opacity": 1,
29226
- "circle-pitch-alignment": "viewport",
29227
- "circle-pitch-scale": "viewport"
29228
- },
29229
- metadata: { ...metadata, "chrt:lamp": "core" }
29806
+ if (objectClass2 === "PILBOP" && resolved.type === "symbol") {
29807
+ layers.push({
29808
+ id: `s52-PILBOP-polygon`,
29809
+ type: "fill",
29810
+ source: sourceId,
29811
+ "source-layer": "PILBOP",
29812
+ filter: GEOM_FILTERS.fill,
29813
+ paint: {
29814
+ "fill-color": resolveColor("CHMGD", ct),
29815
+ "fill-opacity": 0.1
29816
+ },
29817
+ metadata: { "s52:objectClass": "PILBOP", "s52:role": "polygon" }
29818
+ });
29819
+ layers.push({
29820
+ id: `s52-PILBOP-polygon-outline`,
29821
+ type: "line",
29822
+ source: sourceId,
29823
+ "source-layer": "PILBOP",
29824
+ filter: GEOM_FILTERS.fill,
29825
+ paint: {
29826
+ "line-color": resolveColor("CHMGD", ct),
29827
+ "line-width": 0.8,
29828
+ "line-dasharray": [4, 2]
29829
+ },
29830
+ metadata: { "s52:objectClass": "PILBOP", "s52:role": "outline" }
29831
+ });
29832
+ }
29833
+ if (objectClass2 === "HRBFAC" && resolved.type === "circle") {
29834
+ layers.push({
29835
+ id: `s52-HRBFAC-polygon`,
29836
+ type: "fill",
29837
+ source: sourceId,
29838
+ "source-layer": "HRBFAC",
29839
+ filter: GEOM_FILTERS.fill,
29840
+ paint: {
29841
+ "fill-color": resolveColor("TRFCD", ct),
29842
+ "fill-opacity": 0.15
29843
+ },
29844
+ metadata: { "s52:objectClass": "HRBFAC", "s52:role": "polygon" }
29845
+ });
29846
+ }
29847
+ if (objectClass2 === "MARCUL" && resolved.type === "fill") {
29848
+ layers.push({
29849
+ id: `s52-MARCUL-point`,
29850
+ type: "circle",
29851
+ source: sourceId,
29852
+ "source-layer": "MARCUL",
29853
+ filter: GEOM_FILTERS.circle,
29854
+ paint: {
29855
+ "circle-color": resolveColor("CHBRN", ct),
29856
+ "circle-radius": ["interpolate", ["linear"], ["zoom"], 12, 1.2, 14, 1.8, 17, 2.6],
29857
+ "circle-stroke-color": resolveColor("CHBLK", ct),
29858
+ "circle-stroke-width": 0.4,
29859
+ "circle-opacity": 0.6
29860
+ },
29861
+ metadata: { "s52:objectClass": "MARCUL", "s52:role": "point" }
29862
+ });
29863
+ layers.push({
29864
+ id: `s52-MARCUL-line`,
29865
+ type: "line",
29866
+ source: sourceId,
29867
+ "source-layer": "MARCUL",
29868
+ filter: GEOM_FILTERS.line,
29869
+ paint: {
29870
+ "line-color": resolveColor("CHBLK", ct),
29871
+ "line-width": 0.5,
29872
+ "line-dasharray": [2, 2]
29873
+ },
29874
+ metadata: { "s52:objectClass": "MARCUL", "s52:role": "line" }
29875
+ });
29876
+ }
29877
+ if (objectClass2 === "RECTRC" && resolved.type === "line") {
29878
+ layers.push({
29879
+ id: `s52-RECTRC-polygon`,
29880
+ type: "fill",
29881
+ source: sourceId,
29882
+ "source-layer": "RECTRC",
29883
+ filter: GEOM_FILTERS.fill,
29884
+ paint: {
29885
+ "fill-color": resolveColor("TRFCD", ct),
29886
+ "fill-opacity": 0.05
29887
+ },
29888
+ metadata: { "s52:objectClass": "RECTRC", "s52:role": "polygon" }
29889
+ });
29890
+ }
29891
+ if (objectClass2 === "PONTON" && resolved.type === "fill") {
29892
+ layers.push({
29893
+ id: `s52-PONTON-line`,
29894
+ type: "line",
29895
+ source: sourceId,
29896
+ "source-layer": "PONTON",
29897
+ filter: GEOM_FILTERS.line,
29898
+ paint: {
29899
+ "line-color": resolveColor("CHGRF", ct),
29900
+ "line-width": 1.5
29901
+ },
29902
+ metadata: { "s52:objectClass": "PONTON", "s52:role": "line" }
29903
+ });
29904
+ }
29905
+ if (objectClass2 === "SILTNK" && resolved.type === "symbol") {
29906
+ layers.push({
29907
+ id: `s52-SILTNK-symbol-polygon`,
29908
+ type: "symbol",
29909
+ source: sourceId,
29910
+ "source-layer": "SILTNK",
29911
+ filter: GEOM_FILTERS.fill,
29912
+ layout: {
29913
+ "icon-image": "TNKCON02",
29914
+ "icon-size": 0.6,
29915
+ "icon-allow-overlap": true,
29916
+ "icon-ignore-placement": false,
29917
+ "symbol-placement": "point"
29918
+ },
29919
+ paint: {},
29920
+ metadata: { "s52:objectClass": "SILTNK", "s52:role": "symbol-polygon" }
29921
+ });
29922
+ }
29923
+ if (objectClass2 === "ACHBRT" && resolved.type === "symbol") {
29924
+ layers.push({
29925
+ id: `s52-ACHBRT-symbol-polygon`,
29926
+ type: "symbol",
29927
+ source: sourceId,
29928
+ "source-layer": "ACHBRT",
29929
+ filter: GEOM_FILTERS.fill,
29930
+ layout: {
29931
+ "icon-image": "ACHARE02",
29932
+ "icon-size": 0.5,
29933
+ "icon-allow-overlap": true,
29934
+ "icon-ignore-placement": false,
29935
+ "symbol-placement": "point"
29936
+ },
29937
+ paint: {},
29938
+ metadata: { "s52:objectClass": "ACHBRT", "s52:role": "symbol-polygon" }
29939
+ });
29940
+ }
29941
+ if (objectClass2 === "BERTHS" && resolved.type === "line") {
29942
+ layers.push({
29943
+ id: `s52-BERTHS-circle`,
29944
+ type: "circle",
29945
+ source: sourceId,
29946
+ "source-layer": "BERTHS",
29947
+ filter: GEOM_FILTERS.circle,
29948
+ paint: {
29949
+ "circle-color": resolveColor("CHBLK", ct),
29950
+ "circle-radius": ["interpolate", ["linear"], ["zoom"], 12, 1.5, 14, 2.2, 17, 3],
29951
+ "circle-stroke-color": resolveColor("CHBLK", ct),
29952
+ "circle-stroke-width": 0.5
29953
+ },
29954
+ metadata: { "s52:objectClass": "BERTHS", "s52:role": "point" }
29955
+ });
29956
+ }
29957
+ if (objectClass2 === "TUNNEL" && resolved.type === "line") {
29958
+ layers.push({
29959
+ // NOT `s52-TUNNEL-line` — that id is already taken by the main layer
29960
+ // (`s52-${objectClass}-${resolved.type}`), and duplicate layer ids
29961
+ // are a Mapbox style-spec violation (caught by style-integrity.test).
29962
+ id: `s52-TUNNEL-linestring`,
29963
+ type: "line",
29964
+ source: sourceId,
29965
+ "source-layer": "TUNNEL",
29966
+ filter: GEOM_FILTERS.line,
29967
+ paint: {
29968
+ "line-color": resolveColor("CHGRD", ct),
29969
+ "line-width": 1,
29970
+ "line-dasharray": [2, 2]
29971
+ },
29972
+ metadata: { "s52:objectClass": "TUNNEL", "s52:role": "line" }
29973
+ });
29230
29974
  }
29231
- ];
29232
- }
29233
-
29234
- // src/atlas/band-priority.ts
29235
- var BAND_PRIORITY_RANGES = {
29236
- "1": { minzoom: 0 },
29237
- "2": { minzoom: 5 },
29238
- "3": { minzoom: 7 },
29239
- "4": { minzoom: 9 },
29240
- "5": { minzoom: 11 },
29241
- "6": { minzoom: 11 }
29242
- };
29243
- var BAND_DATA_MINZOOM = {
29244
- "1": 0,
29245
- "2": 5,
29246
- "3": 7,
29247
- "4": 9,
29248
- "5": 11,
29249
- "6": 13
29250
- };
29251
- function dominatedVariantMaxzoom(D) {
29252
- return Math.max(BAND_PRIORITY_RANGES[D].minzoom, BAND_DATA_MINZOOM[D]);
29253
- }
29254
- var DOMINANCE_EXEMPT_FILL_CLASSES = /* @__PURE__ */ new Set([
29255
- "LNDARE",
29256
- "DEPARE",
29257
- "UNSARE",
29258
- "LAKARE",
29259
- "DRGARE",
29260
- "SEAARE"
29261
- ]);
29262
- function isExemptOpaqueFill(layer) {
29263
- if (layer.type !== "fill") return false;
29264
- for (const cls of DOMINANCE_EXEMPT_FILL_CLASSES) {
29265
- if (layer.id.startsWith(`s52-${cls}-`)) return true;
29266
- }
29267
- return false;
29268
- }
29269
- var GROUND_FILL_CLASSES = /* @__PURE__ */ new Set([
29270
- ...DOMINANCE_EXEMPT_FILL_CLASSES,
29271
- "RIVERS"
29272
- ]);
29273
- function isGroundFill(layer) {
29274
- if (layer.type !== "fill") return false;
29275
- for (const cls of GROUND_FILL_CLASSES) {
29276
- if (layer.id.startsWith(`s52-${cls}-`)) return true;
29277
- }
29278
- return false;
29279
- }
29280
- var FILL_FLOOR_FREE_BANDS = /* @__PURE__ */ new Set([
29281
- "1",
29282
- "2",
29283
- "3"
29284
- ]);
29285
- function bandVariantMinzoom(layer, B) {
29286
- const classMinzoom = layer.minzoom ?? 0;
29287
- if (isGroundFill(layer) && FILL_FLOOR_FREE_BANDS.has(B)) {
29288
- return 0;
29289
- }
29290
- if (isGroundFill(layer)) {
29291
- return BAND_PRIORITY_RANGES[B].minzoom;
29292
- }
29293
- return Math.max(classMinzoom, BAND_PRIORITY_RANGES[B].minzoom);
29294
- }
29295
- var GENERALISED_AREA_FILL_CLASSES = /* @__PURE__ */ new Set([
29296
- "BUAARE",
29297
- // 2026-08-28, New York Harbor: the "New York Marine Inspection Zone and
29298
- // Captain of the Port Zone" RESARE exists at bands 2/3 as a generalized
29299
- // shore-hugging polygon whose point-sampled dominance tag MISSED (the
29300
- // Providence-BUAARE failure mode), so its magenta dashed boundary drew
29301
- // OFFSET over the band-5 detail at z11.5 — read in the field as "a
29302
- // magenta shoreline bleeding down from a less coarse band". The same
29303
- // tile carried untagged band-2 ADMARE and CTNARE giants in the same
29304
- // state. All three are area classes whose coarse-band boundaries hug
29305
- // shorelines and whose fine-band copies are authoritative.
29306
- "RESARE",
29307
- "CTNARE",
29308
- "ADMARE"
29309
- ]);
29310
- var GENERALISED_AREA_MAX_BAND = 3;
29311
- function generalisedAreaCeiling(layer, B) {
29312
- if (layer.type !== "fill" && layer.type !== "line") return null;
29313
- if (parseInt(B, 10) > GENERALISED_AREA_MAX_BAND) return null;
29314
- for (const cls of GENERALISED_AREA_FILL_CLASSES) {
29315
- if (layer.id.startsWith(`s52-${cls}-`)) return BAND_PRIORITY_RANGES["5"].minzoom;
29316
- }
29317
- return null;
29318
- }
29319
- var BAND_DRAW_ORDER = ["1", "2", "3", "4", "5", "6"];
29320
- var CHART_BAND_CODES = {
29321
- Overview: ["1"],
29322
- General: ["2"],
29323
- Coastal: ["3"],
29324
- Approach: ["4"],
29325
- Harbor: ["5", "6"]
29326
- };
29327
- var CHART_CASCADE_CODES = {
29328
- Overview: ["1"],
29329
- General: ["1", "2"],
29330
- Coastal: ["1", "2", "3"],
29331
- Approach: ["1", "2", "3", "4"],
29332
- Harbor: ["1", "2", "3", "4", "5", "6"]
29333
- };
29334
- var CHRT_STYLE_MODES = [
29335
- "continuous",
29336
- ...Object.keys(CHART_BAND_CODES).map(
29337
- (band) => `chart:${band}`
29338
- )
29339
- ];
29340
- function normalizeStyleMode(raw) {
29341
- if (raw === null || raw === void 0 || raw === "") return "continuous";
29342
- const lower = raw.toLowerCase();
29343
- return CHRT_STYLE_MODES.find((mode) => mode.toLowerCase() === lower) ?? null;
29344
- }
29345
- function styleModeBand(mode) {
29346
- return mode === "continuous" ? null : mode.slice("chart:".length);
29347
- }
29348
- function bandFilterExpression(codes) {
29349
- return cellBandMatch(["slice", ["get", "_cell_id"], 2, 3], codes);
29350
- }
29351
- var BAND_CODE_ALIASES = {
29352
- "5": ["7"]
29353
- };
29354
- function cellBandCharacters(code) {
29355
- return [code, ...BAND_CODE_ALIASES[code] ?? []];
29356
- }
29357
- function cellBandMatch(cellBandExpr, codes) {
29358
- const list = Array.isArray(codes) ? codes : [codes];
29359
- const chars = [...new Set(list.flatMap((c) => cellBandCharacters(c)))];
29360
- if (chars.length === 1) {
29361
- return ["==", cellBandExpr, chars[0]];
29362
29975
  }
29363
- return ["in", cellBandExpr, ["literal", chars]];
29976
+ return layers;
29364
29977
  }
29365
- function combineWithBandFilter(baseFilter, bandFilter) {
29366
- if (!baseFilter) return bandFilter;
29367
- if (baseFilter[0] === "all") {
29368
- return ["all", ...baseFilter.slice(1), bandFilter];
29978
+ function buildLabelLayer2(objectClass2, ct, sourceId = DEFAULT_REGION, depthUnits = "meters", labelOpts = {}) {
29979
+ if (!LABELED_LAYERS.has(objectClass2)) return null;
29980
+ const config = getLabelConfig(objectClass2, {
29981
+ colorTable: ct,
29982
+ safetyContour: labelOpts.safetyContour,
29983
+ twoShadeSoundings: labelOpts.twoShadeSoundings,
29984
+ soundingFontWeight: labelOpts.soundingFontWeight
29985
+ });
29986
+ if (!config) return null;
29987
+ let textField = config.textField;
29988
+ if (objectClass2 === "LIGHTS") {
29989
+ textField = buildLightLabelExpression();
29369
29990
  }
29370
- return ["all", baseFilter, bandFilter];
29371
- }
29372
- var NOT_DOMINATED_FILTER = [
29373
- "all",
29374
- ["!", ["has", "_dominated_by_min"]],
29375
- ["!=", ["get", "_dominated"], true]
29376
- ];
29377
- function dominatedByMinFilter(D) {
29378
- const dInt = parseInt(D, 10);
29379
- if (D === "6") {
29380
- return [
29381
- "any",
29382
- ["==", ["get", "_dominated_by_min"], dInt],
29383
- ["==", ["get", "_dominated"], true]
29384
- ];
29991
+ if (typeof textField !== "string" && Array.isArray(textField)) {
29992
+ const flat = JSON.stringify(textField);
29993
+ if (flat.includes("_navAidLabel")) {
29994
+ textField = buildNavAidLabelExpression();
29995
+ }
29385
29996
  }
29386
- return ["==", ["get", "_dominated_by_min"], dInt];
29387
- }
29388
- function withinBboxFilter(bbox) {
29389
- const [w, s] = bbox.sw;
29390
- const [e, n] = bbox.ne;
29391
- return [
29392
- "within",
29393
- {
29394
- type: "Polygon",
29395
- coordinates: [
29997
+ if (depthUnits === "feet") {
29998
+ if (objectClass2 === "SOUNDG") {
29999
+ textField = [
30000
+ "concat",
29396
30001
  [
29397
- [w, s],
29398
- [e, s],
29399
- [e, n],
29400
- [w, n],
29401
- [w, s]
29402
- ]
29403
- ]
30002
+ "round",
30003
+ [
30004
+ "*",
30005
+ [
30006
+ "to-number",
30007
+ ["coalesce", ["get", "depth"], ["get", "DEPTH"], ["get", "VALSOU"]],
30008
+ 0
30009
+ ],
30010
+ 3.28084
30011
+ ]
30012
+ ],
30013
+ ""
30014
+ ];
30015
+ } else if (objectClass2 === "DEPCNT") {
30016
+ textField = [
30017
+ "concat",
30018
+ ["round", ["*", ["to-number", ["get", "VALDCO"]], 3.28084]],
30019
+ "ft"
30020
+ ];
30021
+ } else if (objectClass2 === "UWTROC" || objectClass2 === "OBSTRN" || objectClass2 === "WRECKS") {
30022
+ const valsouFt = [
30023
+ "to-number",
30024
+ ["coalesce", ["get", "VALSOU"], ["get", "valsou"], 0],
30025
+ 0
30026
+ ];
30027
+ textField = [
30028
+ "case",
30029
+ [">", valsouFt, 0],
30030
+ ["concat", ["to-string", ["round", ["*", valsouFt, 3.28084]]], "ft"],
30031
+ ""
30032
+ ];
30033
+ }
30034
+ }
30035
+ const labelPriority = getLabelPriority(objectClass2);
30036
+ const layer = {
30037
+ id: `s52-${objectClass2}-label`,
30038
+ type: "symbol",
30039
+ source: sourceId,
30040
+ "source-layer": objectClass2,
30041
+ filter: GEOM_FILTERS.symbol,
30042
+ layout: {
30043
+ "text-field": textField,
30044
+ "text-size": config.textSize,
30045
+ "text-font": config.textFont ?? ["Open Sans Regular"],
30046
+ "text-offset": config.textOffset,
30047
+ "text-anchor": "top",
30048
+ "text-allow-overlap": false,
30049
+ "text-optional": true,
30050
+ "text-padding": 4,
30051
+ "symbol-sort-key": labelPriority * 1e3,
30052
+ ...config.layout ?? {}
30053
+ },
30054
+ paint: {
30055
+ "text-color": config.textColor,
30056
+ "text-halo-color": config.textHaloColor,
30057
+ "text-halo-width": config.textHaloWidth
30058
+ },
30059
+ metadata: {
30060
+ "s52:objectClass": objectClass2,
30061
+ "chrt:labelPriority": labelPriority
29404
30062
  }
29405
- ];
29406
- }
29407
- function worldMinusBboxPolygon(bbox) {
29408
- const [w, s] = bbox.sw;
29409
- const [e, n] = bbox.ne;
29410
- return {
29411
- type: "Polygon",
29412
- coordinates: [
29413
- [
29414
- [-180, -85],
29415
- [180, -85],
29416
- [180, 85],
29417
- [-180, 85],
29418
- [-180, -85]
29419
- ],
29420
- // Inner ring (hole) — winding opposite to outer ring per GeoJSON
29421
- // spec. MapLibre is permissive but we follow strict winding.
29422
- [
29423
- [w, s],
29424
- [w, n],
29425
- [e, n],
29426
- [e, s],
29427
- [w, s]
29428
- ]
29429
- ]
29430
30063
  };
30064
+ if (config.minzoom) {
30065
+ layer.minzoom = config.minzoom;
30066
+ }
30067
+ if (objectClass2 === "DEPCNT") {
30068
+ layer.filter = GEOM_FILTERS.line;
30069
+ }
30070
+ if (objectClass2 === "CBLOHD" || objectClass2 === "PIPOHD") {
30071
+ layer.filter = GEOM_FILTERS.line;
30072
+ }
30073
+ if (objectClass2 === "SBDARE") {
30074
+ delete layer.filter;
30075
+ }
30076
+ if (objectClass2 === "MAGVAR") {
30077
+ delete layer.filter;
30078
+ }
30079
+ if (getLabelBehavior(objectClass2) === "orientation-cross-band") {
30080
+ layer.filter = LABEL_ANCHOR_FILTER;
30081
+ }
30082
+ return applyDepthLabelSize(objectClass2, layer, labelOpts.depthLabelSize);
29431
30083
  }
29432
-
29433
- // src/atlas/base-layers.ts
29434
- var NOCOVERAGE_PATTERN = "chrt:hatch-nocoverage";
29435
- var UNCHARTED_PATTERN = "chrt:hatch-uncharted";
29436
- function inlandWaterColor(palette) {
29437
- return (BASE_THEMES[palette] ?? BASE_THEMES.DAY).inlandWater;
29438
- }
29439
- var BASE_THEMES = {
29440
- DAY: {
29441
- // ── `earth` is the jurisdiction tan DIMMED (~0.55), not a blue-grey ──
29442
- // Outside the jurisdiction the map still owes the reader "what is land".
29443
- // State 3 in the header is "the geography, under a crosshatch", and until
29444
- // 2026-08-31 that geography was unreadable: earth #2A3439 against sea
29445
- // #1B2328 measured 1.25:1, so Hispaniola showed charted boxes with tan
29446
- // land inside and nothing recognisable around them (owner report: "no
29447
- // land painting down near the DR at some zooms"). Land is warm and sea is
29448
- // cool because that is what land and sea are; dimming the JURISDICTION
29449
- // tan rather than inventing a colour says the true thing — this is the
29450
- // same land, we simply do not carry charts for it. Held against BOTH
29451
- // neighbours: legible over sea, and never mistakable for the full tan of
29452
- // a country we do carry. DAY 3.59:1 vs sea, 3.17:1 vs tan.
29453
- earth: "#807761",
29454
- water: "#1B2328",
29455
- river: "#24313A",
29456
- boundary: "#4A575E",
29457
- jurisdiction: "#D4C69D",
29458
- // CHRTland's own tan — one land colour
29459
- inlandWater: "#B9CFDD",
29460
- // muted, unmistakably not a charted blue
29461
- // The uncharted hatch's stripe, held to 2.3:1 against `inlandWater` in
29462
- // every palette (`inland-hatch-contrast.test.ts`) — the hatch says "we have
29463
- // not charted this water", and stripes the eye cannot find say nothing.
29464
- // DAY 3.08:1, darker than its ground. Also the colour the artwork file
29465
- // itself is drawn in.
29466
- inlandHatch: "#5B7383",
29467
- // State furniture. These three were referenced by the layers but missing
29468
- // from this table (and its type — the long-standing TS2339 trio), so the
29469
- // lines shipped on MapLibre's DEFAULT black. Now deliberate: a hairline
29470
- // a shade deeper than the tan, quiet caps, tan halo.
29471
- stateLine: "#B1A077",
29472
- stateLabel: "#8E8468",
29473
- stateHalo: "#D4C69D",
29474
- // Rough-lake furniture (owner ask 2026-08-28: "the lakes need labels,
29475
- // and shorelines drawn"): a shoreline a shade deeper than the rough
29476
- // water, and a name in the water's own voice, haloed by the water.
29477
- inlandShore: "#8FA9BA",
29478
- inlandLabel: "#54707F",
29479
- inlandHalo: "#B9CFDD"
29480
- },
29481
- DUSK: {
29482
- // ── Re-derived 2026-09-01: the basemap sits UNDER the chart ──────────
29483
- // These were the DAY tones scaled by a constant, never measured against
29484
- // the DUSK chart — so `jurisdiction` came out at L=0.506 against a chart
29485
- // whose own land (LANDA #8a7d5e) is L=0.209, i.e. the context layer was
29486
- // 2.4x brighter than the data layer. Now capped at the chart's own land.
29487
- // The rest of the block rides the same 0.666 channel factor so every
29488
- // relationship the palette already chose is preserved, only moved down.
29489
- // `water` is deliberately NOT scaled: it is the reference the land/sea
29490
- // floor is measured against.
29491
- earth: "#544E3E",
29492
- water: "#161D21",
29493
- river: "#141B20",
29494
- boundary: "#293135",
29495
- jurisdiction: "#867D64",
29496
- inlandWater: "#62717A",
29497
- // Owner's choice, 2026-10-04: dark stripes, 2.37:1. The DAY stripe here
29498
- // was 1.02:1 — invisible, so an uncharted lake read as plain water.
29499
- inlandHatch: "#2E383F",
29500
- // Label stays DARK here — the halo is still mid-toned, so 3:1 is reachable
29501
- // downward and a bright label would be a new light source at dusk.
29502
- stateLine: "#B5A987",
29503
- stateLabel: "#38342A",
29504
- stateHalo: "#867D64",
29505
- inlandShore: "#4D5B67",
29506
- inlandLabel: "#212629",
29507
- inlandHalo: "#62717A"
29508
- },
29509
- NIGHT: {
29510
- // ── Re-derived 2026-09-01 — the owner-reported night defect ──────────
29511
- // `jurisdiction` was L=0.240 against a chart whose own land (LANDA
29512
- // #3a3020) is L=0.031: the world basemap was 7.7x BRIGHTER than the chart
29513
- // it sits under, so at z<11 the uncharted interior read as a pale
29514
- // continent-sized slab on a night chart. Capped at the chart's own land.
29515
- //
29516
- // ⚠ `earth` AND `jurisdiction` CONVERGE HERE, and that is forced, not
29517
- // sloppy. Holding earth 2:1 above the ocean AND 2:1 below the jurisdiction
29518
- // tan requires jurisdiction >= L 0.174 — 5.6x the chart's own land. At
29519
- // night the range between the ocean (L 0.006) and the cap (L 0.031) is too
29520
- // compressed to carry two land tones. COVERAGE IS STILL SAID, by the
29521
- // `chrt:hatch-nocoverage` crosshatch, which is a stronger signal than tone
29522
- // and is the one thing that cannot be lost to palette compression.
29523
- //
29524
- // Labels invert to light-on-dark: 3:1 downward is unreachable off a halo
29525
- // this close to black, and CHRTland's own night town labels already sit at
29526
- // L=0.177 for exactly this reason.
29527
- earth: "#333026",
29528
- water: "#10161A",
29529
- river: "#090D0F",
29530
- boundary: "#131719",
29531
- jurisdiction: "#353128",
29532
- inlandWater: "#232A30",
29533
- // The DAY stripe, lighter than this ground: 2.92:1, and L 0.16, under the
29534
- // NIGHT muted-ink ceiling (`night-luminance.test.ts`). Red night draws the
29535
- // NIGHT chart, so this is its stripe too.
29536
- inlandHatch: "#5B7383",
29537
- stateLine: "#484236",
29538
- stateLabel: "#837963",
29539
- stateHalo: "#353128",
29540
- inlandShore: "#1C2227",
29541
- inlandLabel: "#617484",
29542
- inlandHalo: "#232A30"
30084
+ function getGeomTypesForClass(objectClass2, defaultType) {
30085
+ if (SYMBOL_MAP[objectClass2] && defaultType === "symbol") {
30086
+ return ["symbol"];
29543
30087
  }
29544
- };
29545
- function baseLayers(opts = {}) {
29546
- const palette = opts.palette ?? "DAY";
29547
- const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
29548
- const src = opts.sourceId ?? BASE_REGION;
29549
- const L = (o) => o;
29550
- return [
29551
- // 1. Geography — the world as it is, in the quiet dark voice. The OCEAN
29552
- // is a background, not a polygon: the archive ships land and lakes,
29553
- // and "everything else is sea" is exactly what a background expresses
29554
- // — geometry-free, gap-free at every antimeridian and pole. Without
29555
- // it the sea outside coverage was bare S-52 NODTA, a light grey that
29556
- // read as "charted ground" with a hatch on it.
29557
- L({
29558
- id: "chrtbase-ocean",
29559
- type: "background",
29560
- paint: { "background-color": T.water }
29561
- }),
29562
- L({
29563
- id: "chrtbase-earth",
29564
- type: "fill",
29565
- source: src,
29566
- "source-layer": "base_earth",
29567
- paint: { "fill-color": T.earth }
29568
- }),
29569
- L({
29570
- id: "chrtbase-water",
29571
- type: "fill",
29572
- source: src,
29573
- "source-layer": "base_water",
29574
- paint: { "fill-color": T.water }
29575
- }),
29576
- L({
29577
- id: "chrtbase-river",
29578
- type: "line",
29579
- source: src,
29580
- "source-layer": "base_river",
29581
- minzoom: 9,
29582
- paint: {
29583
- "line-color": T.river,
29584
- "line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.6, 14, 1.6]
29585
- }
29586
- }),
29587
- L({
29588
- id: "chrtbase-boundary",
29589
- type: "line",
29590
- source: src,
29591
- "source-layer": "base_boundary",
29592
- paint: { "line-color": T.boundary, "line-width": 0.6, "line-opacity": 0.8 }
29593
- }),
29594
- // 2. THE DEFAULT: no coverage, everywhere, as a geometry-free background.
29595
- // Constant opacity by design — see the header. What kills it is being
29596
- // painted OVER, never fading.
29597
- L({
29598
- id: "chrtbase-nocoverage",
29599
- type: "background",
29600
- paint: { "background-pattern": NOCOVERAGE_PATTERN, "background-opacity": 0.55 }
29601
- }),
29602
- // 3. The jurisdiction: countries whose charts CHRT carries go tan — the
29603
- // same tan as CHRTland, so "land in our world" is ONE colour. Opaque,
29604
- // so it takes the crosshatch off its ground entirely.
29605
- L({
29606
- id: "chrtbase-jurisdiction",
29607
- type: "fill",
29608
- source: src,
29609
- "source-layer": "jurisdiction",
29610
- paint: { "fill-color": T.jurisdiction }
29611
- }),
29612
- // 4. States — internal boundaries and names, inside the jurisdiction
29613
- // only (owner decision 2026-08-28). A hairline a shade deeper than the
29614
- // tan, and quiet letter-spaced caps that fade as harbour detail takes
29615
- // over. The one deliberate exception to no-names-on-the-basemap:
29616
- // ADMIN furniture, nothing else — see the fence test.
29617
- L({
29618
- id: "chrtbase-state-boundary",
29619
- type: "line",
29620
- source: src,
29621
- "source-layer": "state_boundary",
29622
- minzoom: 4,
29623
- paint: {
29624
- "line-color": T.stateLine,
29625
- "line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1],
29626
- "line-dasharray": [3, 2],
29627
- "line-opacity": 0.8
29628
- }
29629
- }),
29630
- // 5. Country borders, drawn to be HOISTED above the whole chart by the
29631
- // generator (owner, 2026-08-31: "where there is charted land, can we
29632
- // add country borders on top of everything? I want it clear where
29633
- // there is a boarder."). Sourced from NE boundary_lines_land — land
29634
- // borders only, never coastline — so riding on top cannot stroke a
29635
- // coast. Dashed so it reads as an administrative line, not chart
29636
- // linework.
29637
- L({
29638
- id: "chrtbase-country-boundary",
29639
- type: "line",
29640
- source: src,
29641
- "source-layer": "country_boundary",
29642
- paint: {
29643
- "line-color": T.boundary,
29644
- "line-width": ["interpolate", ["linear"], ["zoom"], 3, 0.8, 8, 1.4, 12, 2.2],
29645
- "line-dasharray": [4, 2],
29646
- "line-opacity": 0.9
29647
- }
29648
- }),
29649
- L({
29650
- id: "chrtbase-state-label",
29651
- type: "symbol",
29652
- source: src,
29653
- "source-layer": "state_label",
29654
- minzoom: 4,
29655
- maxzoom: 10,
29656
- layout: {
29657
- "text-field": ["upcase", ["get", "name"]],
29658
- "text-font": ["Open Sans Regular"],
29659
- "text-size": ["interpolate", ["linear"], ["zoom"], 4, 9, 8, 12],
29660
- "text-letter-spacing": 0.25,
29661
- "text-allow-overlap": false,
29662
- "text-padding": 8
29663
- },
29664
- paint: {
29665
- "text-color": T.stateLabel,
29666
- "text-halo-color": T.stateHalo,
29667
- "text-halo-width": 1.2,
29668
- "text-opacity": ["interpolate", ["linear"], ["zoom"], 4, 0.7, 8, 0.85, 10, 0.4]
30088
+ if (objectClass2 === "SOUNDG") {
30089
+ return ["circle"];
30090
+ }
30091
+ if (defaultType) {
30092
+ return [defaultType];
30093
+ }
30094
+ return ["circle"];
30095
+ }
30096
+ function getOrderedClasses() {
30097
+ const ordered = [...S52_DRAW_ORDER];
30098
+ for (const key of Object.keys(S52_LOOKUP)) {
30099
+ if (!ordered.includes(key)) {
30100
+ const insertIdx = ordered.indexOf("SOUNDG");
30101
+ if (insertIdx >= 0) {
30102
+ ordered.splice(insertIdx, 0, key);
30103
+ } else {
30104
+ ordered.push(key);
29669
30105
  }
29670
- })
29671
- ];
30106
+ }
30107
+ }
30108
+ return ordered;
29672
30109
  }
29673
- function baseInlandWaterLayers(opts = {}) {
29674
- const palette = opts.palette ?? "DAY";
29675
- const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
29676
- const src = opts.sourceId ?? BASE_REGION;
29677
- const L = (o) => o;
29678
- const NE_TIER = ["!", ["has", "kind"]];
29679
- const PLANET_TIER = ["has", "kind"];
29680
- const HANDOVER = 9;
30110
+
30111
+ // src/atlas/terrain-order.ts
30112
+ var DRAPEABLE_LAYER_TYPES = /* @__PURE__ */ new Set([
30113
+ "background",
30114
+ "fill",
30115
+ "line",
30116
+ "raster",
30117
+ "hillshade",
30118
+ "color-relief"
30119
+ ]);
30120
+ function isDrapeable(layer) {
30121
+ return DRAPEABLE_LAYER_TYPES.has(layer.type);
30122
+ }
30123
+ function drapedFirst(layers) {
30124
+ const draped = [];
30125
+ const direct = [];
30126
+ for (const l of layers) (isDrapeable(l) ? draped : direct).push(l);
30127
+ return [...draped, ...direct];
30128
+ }
30129
+ function terrainStackCount(layers) {
30130
+ let stacks = 0;
30131
+ let prevDraped = false;
30132
+ for (const l of layers) {
30133
+ const d = isDrapeable(l);
30134
+ if (d && !prevDraped) stacks++;
30135
+ prevDraped = d;
30136
+ }
30137
+ return stacks;
30138
+ }
30139
+ function drapeHalves(layers) {
30140
+ const draped = [];
30141
+ const direct = [];
30142
+ for (const l of layers) (isDrapeable(l) ? draped : direct).push(l);
30143
+ return { draped, direct };
30144
+ }
30145
+
30146
+ // src/atlas/lamps.ts
30147
+ var LAMP_GLOW_LAYER_ID = "s52-LIGHTS-lamp-glow";
30148
+ var LAMP_CORE_LAYER_ID = "s52-LIGHTS-lamp-core";
30149
+ var LAMP_LAYER_IDS = [LAMP_GLOW_LAYER_ID, LAMP_CORE_LAYER_ID];
30150
+ var RANGE_NM = ["to-number", ["coalesce", ["get", "VALNMR"], ["get", "valnmr"]], 6];
30151
+ var UNSECTORED_POINTS = [
30152
+ "all",
30153
+ ["any", ["==", ["geometry-type"], "Point"], ["==", ["geometry-type"], "MultiPoint"]],
30154
+ ["!", ["has", "SECTR1"]],
30155
+ // …and not lights with a charted rhythm either: those flash, and the
30156
+ // flashing is computed by the app from `SIGSEQ` (light-sigseq.ts). What is
30157
+ // left here is exactly the set that is steady from every bearing.
30158
+ ["!", ["has", "SIGSEQ"]]
30159
+ ];
30160
+ function buildLampLayers(sourceId, ct) {
30161
+ const colour = lightFillExpression(ct);
30162
+ const common = {
30163
+ type: "circle",
30164
+ source: sourceId,
30165
+ "source-layer": "LIGHTS",
30166
+ filter: UNSECTORED_POINTS,
30167
+ layout: { visibility: "none" }
30168
+ };
30169
+ const metadata = { "chrt:role": "lamp", "s52:objectClass": "LIGHTS" };
29681
30170
  return [
29682
- // ── The fill pair, SPLIT BY TIER at the handover ────────────────────────
29683
- //
29684
- // Both tiers share `inland_water` (tile-join concatenates the same layer
29685
- // name), and an unsplit fill draws BOTH — which means the NE polygon is
29686
- // painted over the planet one at every zoom. That is not a cosmetic
29687
- // double-draw: NE 10m carries no interior rings, so its blob has no
29688
- // ISLANDS in it, and painting it over the planet's holed polygons buries
29689
- // every island in the lake under "uncharted water". Owner-reported at
29690
- // Winnipesaukee z11.7 ("there is land (and islands) being covered by the
29691
- // 'no chart here' boundary"), and measured: rendering the planet tier
29692
- // alone brings back the whole archipelago — Rattlesnake, Bear, Long
29693
- // Island, Diamond, the Barndoors — while the NE tier alone is a faceted
29694
- // blob whose edges bear no relation to the shoreline drawn beneath it.
29695
- //
29696
- // So each tier owns its own zoom range, the same handover the shoreline
29697
- // below uses. Known residue, measured over the whole archive: of the 141
29698
- // NE lakes we still carry, 138 have planet water beneath them; the
29699
- // exceptions are two unnamed western-Alaska tundra lakes and a 2e-5 deg²
29700
- // Lake Michigan sliver, which lose their rough fill above z9 because the
29701
- // planet has no `water` polygon there at all (checked against the raw
29702
- // pre-clip NDJSON — not a clip artefact).
29703
- L({
29704
- id: "chrtbase-inland-water",
29705
- type: "fill",
29706
- source: src,
29707
- "source-layer": "inland_water",
29708
- filter: NE_TIER,
29709
- maxzoom: HANDOVER,
29710
- paint: { "fill-color": T.inlandWater }
29711
- }),
29712
- L({
29713
- id: "chrtbase-inland-hatch",
29714
- type: "fill",
29715
- source: src,
29716
- "source-layer": "inland_water",
29717
- filter: NE_TIER,
29718
- maxzoom: HANDOVER,
29719
- paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
29720
- }),
29721
- L({
29722
- id: "chrtbase-inland-water-detail",
29723
- type: "fill",
29724
- source: src,
29725
- "source-layer": "inland_water",
29726
- filter: PLANET_TIER,
29727
- minzoom: HANDOVER,
29728
- paint: { "fill-color": T.inlandWater }
29729
- }),
29730
- L({
29731
- id: "chrtbase-inland-hatch-detail",
29732
- type: "fill",
29733
- source: src,
29734
- "source-layer": "inland_water",
29735
- filter: PLANET_TIER,
29736
- minzoom: HANDOVER,
29737
- paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
29738
- }),
29739
- // The rough shoreline (owner ask 2026-08-28). Drawn ABOVE the hatch so
29740
- // the edge stays crisp through the texture, and BELOW the charted-water
29741
- // repaint clones the generator splices in after it — so wherever the
29742
- // chart repaints its own water, the rough NE10m line yields to the
29743
- // chart's COALNE instead of double-striking it.
29744
- //
29745
- // NE-tier geometry ONLY (`kind` absent): the planet detail tier joins
29746
- // the same source-layer from z9 as MANY polygons whose chunk-clip seams
29747
- // rejoin invisibly as FILLS — but a line layer strokes every seam, and
29748
- // Winnipesaukee rendered as a mesh of internal edges. Planet features
29749
- // carry `kind` (the INLAND_KINDS filter's own field); NE's carry only
29750
- // `name`, so the absence of `kind` IS the tier discriminator.
29751
- L({
29752
- id: "chrtbase-inland-shore",
29753
- type: "line",
29754
- source: src,
29755
- "source-layer": "inland_water",
29756
- filter: NE_TIER,
29757
- // Hands the shoreline over to the planet tier at ITS floor. Both
29758
- // tiers live in `inland_water` from z9 up (tile-join concatenates the
29759
- // same layer name), and their FILLS merge invisibly — but two
29760
- // outlines do not: NE 10m generalises Winnipesaukee's shape, so its
29761
- // outline ran straight across the middle of the lake the planet had
29762
- // drawn correctly around. The coarse line is right only while it is
29763
- // the only shape we have.
29764
- maxzoom: HANDOVER,
29765
- paint: {
29766
- "line-color": T.inlandShore,
29767
- "line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1.1, 14, 1.6],
29768
- "line-opacity": 0.9
29769
- }
29770
- }),
29771
- // The PLANET tier's shoreline (owner ask 2026-08-31: "uncharted small
29772
- // lakes render with no outline"). It reads its own `inland_shore` layer,
29773
- // NOT `inland_water`, and that indirection is the whole fix: the mesh
29774
- // that forced the `!has kind` fence above is a CLIP artefact — an MVT
29775
- // feature arrives already cut to its own z10 tile, so stroking the
29776
- // polygon strokes every tile boundary it crosses. The build now walks
29777
- // those rings, drops the axis-aligned runs that sit on the read zoom's
29778
- // tile grid, and emits what is left as lines (measured on one New
29779
- // England chunk: 61 long axis-aligned mesh edges → 5, and the 5 survivors
29780
- // are 0.02–0.5 tiles off the grid, i.e. real straight water edges).
29781
- //
29782
- // Thinner and quieter than the NE line: this is finer geometry describing
29783
- // water nobody has surveyed, and approximate data should look
29784
- // approximate.
29785
- L({
29786
- id: "chrtbase-inland-shore-detail",
29787
- type: "line",
29788
- source: src,
29789
- "source-layer": "inland_shore",
29790
- minzoom: HANDOVER,
29791
- paint: {
29792
- "line-color": T.inlandShore,
29793
- "line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.5, 12, 0.9, 15, 1.2],
29794
- "line-opacity": 0.85
29795
- }
29796
- }),
29797
- // The lake's name (owner ask 2026-08-28) — the second scoped exception
29798
- // to no-names-on-the-basemap, after the states: WATER-BODY names only.
29799
- // No maxzoom: an uncharted lake keeps its name at every zoom; over
29800
- // charted lakes the chart's own SEAARE label takes over at detail zooms
29801
- // and collision culls the echo.
29802
- //
29803
- // The archive's `inland_water_label` anchors (v3+). v3 shipped ONE
29804
- // PointOnSurface anchor per lake, and a big lake's single anchor pans
29805
- // off-viewport — Winnipesaukee at z11.7 read nameless with its name two
29806
- // tiles west of the screen (owner-reported). Labelling the polygons
29807
- // instead is NOT the fix: tippecanoe's coalescing drops the NE polygon
29808
- // from some z11 tiles (measured: 11/619/747 carries only planet pieces),
29809
- // so a polygon-sourced label vanishes tile by tile. v4 therefore bakes a
29810
- // deterministic anchor GRID into big lakes — the paper-chart convention
29811
- // of repeating a name along a large water body — and this one layer
29812
- // renders however many anchors are in view; collision culls the rest.
29813
- // Planet-tier water-body NAMES (owner ask 2026-08-31: NE 10m names only
29814
- // 113 US lakes). Emitted BELOW the NE layer on purpose — MapLibre places
29815
- // symbols topmost-layer-first, so the big NE lake wins collision against
29816
- // the pond beside it, and both lose to the chart's own labels (every ENC
29817
- // class is emitted after this pair).
29818
- //
29819
- // The size threshold is OSM's own `min_zoom`, carried through the build:
29820
- // Lake Winnipesaukee reads 10, Squam Lake 12, so a pond simply is not in
29821
- // the filter until the user is close enough for it to be the answer to
29822
- // "what is that water". `["zoom"]` in a filter evaluates at integer zoom
29823
- // levels and removes the feature from placement entirely — verified
29824
- // against maplibre-gl-native rather than assumed, because a text-size-0
29825
- // gate would still claim a collision slot.
29826
- //
29827
- // The tiers are separate LAYERS, not a filtered split of one — see the
29828
- // builder for why: an archive that a not-yet-deployed style would
29829
- // mis-render cannot be promoted first, and this one has to be.
29830
- L({
29831
- id: "chrtbase-inland-label-detail",
29832
- type: "symbol",
29833
- source: src,
29834
- "source-layer": "inland_water_label_detail",
29835
- minzoom: HANDOVER,
29836
- filter: [">=", ["zoom"], ["get", "min_zoom"]],
29837
- layout: {
29838
- "text-field": ["get", "name"],
29839
- "text-font": ["Open Sans Regular"],
29840
- "text-size": ["interpolate", ["linear"], ["zoom"], 9, 9, 12, 10.5, 15, 11],
29841
- "text-max-width": 8,
29842
- "text-allow-overlap": false,
29843
- "text-padding": 6,
29844
- // Bigger water first within the tier — min_zoom IS the size rank.
29845
- "symbol-sort-key": ["get", "min_zoom"]
29846
- },
30171
+ {
30172
+ ...common,
30173
+ id: LAMP_GLOW_LAYER_ID,
29847
30174
  paint: {
29848
- "text-color": T.inlandLabel,
29849
- "text-halo-color": T.inlandHalo,
29850
- "text-halo-width": 1,
29851
- "text-opacity": 0.9
29852
- }
29853
- }),
29854
- L({
29855
- id: "chrtbase-inland-label",
29856
- type: "symbol",
29857
- source: src,
29858
- "source-layer": "inland_water_label",
29859
- minzoom: 5,
29860
- layout: {
29861
- "text-field": ["get", "name"],
29862
- "text-font": ["Open Sans Regular"],
29863
- "text-size": ["interpolate", ["linear"], ["zoom"], 5, 9.5, 10, 12],
29864
- "text-max-width": 7,
29865
- "text-allow-overlap": false,
29866
- // Zoom-scaled spacing for the anchor GRID: at z8.9 the 0.08-deg
29867
- // anchors sit ~54 px apart and a flat padding let six copies of
29868
- // "Lake Ontario" crowd one screen (owner-reported 2026-08-30).
29869
- // Wide collision boxes at low zoom thin the grid to the repeat-
29870
- // by-convention cadence; by z12 the anchors are ~850 px apart and
29871
- // the padding relaxes to normal.
29872
- "text-padding": ["interpolate", ["linear"], ["zoom"], 5, 80, 9, 64, 12, 16]
30175
+ "circle-color": colour,
30176
+ // 4 nm → 7 px, 12 nm → 12 px, 25 nm → 20 px; capped, because a
30177
+ // primary seacoast light is not a searchlight.
30178
+ "circle-radius": ["interpolate", ["linear"], RANGE_NM, 0, 5, 4, 7, 12, 12, 25, 20],
30179
+ "circle-blur": 1,
30180
+ "circle-opacity": 0.55,
30181
+ // A lamp is a point of light: it does not grow as it comes near, and
30182
+ // it does not lie down with the chart.
30183
+ "circle-pitch-alignment": "viewport",
30184
+ "circle-pitch-scale": "viewport"
29873
30185
  },
30186
+ metadata: { ...metadata, "chrt:lamp": "glow" }
30187
+ },
30188
+ {
30189
+ ...common,
30190
+ id: LAMP_CORE_LAYER_ID,
29874
30191
  paint: {
29875
- "text-color": T.inlandLabel,
29876
- "text-halo-color": T.inlandHalo,
29877
- "text-halo-width": 1.1
29878
- }
29879
- })
30192
+ "circle-color": colour,
30193
+ "circle-radius": ["interpolate", ["linear"], RANGE_NM, 0, 1.5, 12, 2.5, 25, 3.5],
30194
+ "circle-blur": 0.25,
30195
+ "circle-opacity": 1,
30196
+ "circle-pitch-alignment": "viewport",
30197
+ "circle-pitch-scale": "viewport"
30198
+ },
30199
+ metadata: { ...metadata, "chrt:lamp": "core" }
30200
+ }
30201
+ ];
30202
+ }
30203
+
30204
+ // src/atlas/band-priority.ts
30205
+ var BAND_PRIORITY_RANGES = {
30206
+ "1": { minzoom: 0 },
30207
+ "2": { minzoom: 5 },
30208
+ "3": { minzoom: 7 },
30209
+ "4": { minzoom: 9 },
30210
+ "5": { minzoom: 11 },
30211
+ "6": { minzoom: 11 }
30212
+ };
30213
+ var BAND_DATA_MINZOOM = {
30214
+ "1": 0,
30215
+ "2": 5,
30216
+ "3": 7,
30217
+ "4": 9,
30218
+ "5": 11,
30219
+ "6": 13
30220
+ };
30221
+ function dominatedVariantMaxzoom(D) {
30222
+ return Math.max(BAND_PRIORITY_RANGES[D].minzoom, BAND_DATA_MINZOOM[D]);
30223
+ }
30224
+ var DOMINANCE_EXEMPT_FILL_CLASSES = /* @__PURE__ */ new Set([
30225
+ "LNDARE",
30226
+ "DEPARE",
30227
+ "UNSARE",
30228
+ "LAKARE",
30229
+ "DRGARE",
30230
+ "SEAARE"
30231
+ ]);
30232
+ function isExemptOpaqueFill(layer) {
30233
+ if (layer.type !== "fill") return false;
30234
+ for (const cls of DOMINANCE_EXEMPT_FILL_CLASSES) {
30235
+ if (layer.id.startsWith(`s52-${cls}-`)) return true;
30236
+ }
30237
+ return false;
30238
+ }
30239
+ var GROUND_FILL_CLASSES = /* @__PURE__ */ new Set([
30240
+ ...DOMINANCE_EXEMPT_FILL_CLASSES,
30241
+ "RIVERS"
30242
+ ]);
30243
+ function isGroundFill(layer) {
30244
+ if (layer.type !== "fill") return false;
30245
+ for (const cls of GROUND_FILL_CLASSES) {
30246
+ if (layer.id.startsWith(`s52-${cls}-`)) return true;
30247
+ }
30248
+ return false;
30249
+ }
30250
+ var FILL_FLOOR_FREE_BANDS = /* @__PURE__ */ new Set([
30251
+ "1",
30252
+ "2",
30253
+ "3"
30254
+ ]);
30255
+ function bandVariantMinzoom(layer, B) {
30256
+ const classMinzoom = layer.minzoom ?? 0;
30257
+ if (isGroundFill(layer) && FILL_FLOOR_FREE_BANDS.has(B)) {
30258
+ return 0;
30259
+ }
30260
+ if (isGroundFill(layer)) {
30261
+ return BAND_PRIORITY_RANGES[B].minzoom;
30262
+ }
30263
+ return Math.max(classMinzoom, BAND_PRIORITY_RANGES[B].minzoom);
30264
+ }
30265
+ var GENERALISED_AREA_FILL_CLASSES = /* @__PURE__ */ new Set([
30266
+ "BUAARE",
30267
+ // 2026-08-28, New York Harbor: the "New York Marine Inspection Zone and
30268
+ // Captain of the Port Zone" RESARE exists at bands 2/3 as a generalized
30269
+ // shore-hugging polygon whose point-sampled dominance tag MISSED (the
30270
+ // Providence-BUAARE failure mode), so its magenta dashed boundary drew
30271
+ // OFFSET over the band-5 detail at z11.5 — read in the field as "a
30272
+ // magenta shoreline bleeding down from a less coarse band". The same
30273
+ // tile carried untagged band-2 ADMARE and CTNARE giants in the same
30274
+ // state. All three are area classes whose coarse-band boundaries hug
30275
+ // shorelines and whose fine-band copies are authoritative.
30276
+ "RESARE",
30277
+ "CTNARE",
30278
+ "ADMARE"
30279
+ ]);
30280
+ var GENERALISED_AREA_MAX_BAND = 3;
30281
+ function generalisedAreaCeiling(layer, B) {
30282
+ if (layer.type !== "fill" && layer.type !== "line") return null;
30283
+ if (parseInt(B, 10) > GENERALISED_AREA_MAX_BAND) return null;
30284
+ for (const cls of GENERALISED_AREA_FILL_CLASSES) {
30285
+ if (layer.id.startsWith(`s52-${cls}-`)) return BAND_PRIORITY_RANGES["5"].minzoom;
30286
+ }
30287
+ return null;
30288
+ }
30289
+ var BAND_DRAW_ORDER = ["1", "2", "3", "4", "5", "6"];
30290
+ var CHART_BAND_CODES = {
30291
+ Overview: ["1"],
30292
+ General: ["2"],
30293
+ Coastal: ["3"],
30294
+ Approach: ["4"],
30295
+ Harbor: ["5", "6"]
30296
+ };
30297
+ var CHART_CASCADE_CODES = {
30298
+ Overview: ["1"],
30299
+ General: ["1", "2"],
30300
+ Coastal: ["1", "2", "3"],
30301
+ Approach: ["1", "2", "3", "4"],
30302
+ Harbor: ["1", "2", "3", "4", "5", "6"]
30303
+ };
30304
+ var CHRT_STYLE_MODES = [
30305
+ "continuous",
30306
+ ...Object.keys(CHART_BAND_CODES).map(
30307
+ (band) => `chart:${band}`
30308
+ )
30309
+ ];
30310
+ function normalizeStyleMode(raw) {
30311
+ if (raw === null || raw === void 0 || raw === "") return "continuous";
30312
+ const lower = raw.toLowerCase();
30313
+ return CHRT_STYLE_MODES.find((mode) => mode.toLowerCase() === lower) ?? null;
30314
+ }
30315
+ function styleModeBand(mode) {
30316
+ return mode === "continuous" ? null : mode.slice("chart:".length);
30317
+ }
30318
+ function bandFilterExpression(codes) {
30319
+ return cellBandMatch(["slice", ["get", "_cell_id"], 2, 3], codes);
30320
+ }
30321
+ var BAND_CODE_ALIASES = {
30322
+ "5": ["7"]
30323
+ };
30324
+ function cellBandCharacters(code) {
30325
+ return [code, ...BAND_CODE_ALIASES[code] ?? []];
30326
+ }
30327
+ function cellBandMatch(cellBandExpr, codes) {
30328
+ const list = Array.isArray(codes) ? codes : [codes];
30329
+ const chars = [...new Set(list.flatMap((c) => cellBandCharacters(c)))];
30330
+ if (chars.length === 1) {
30331
+ return ["==", cellBandExpr, chars[0]];
30332
+ }
30333
+ return ["in", cellBandExpr, ["literal", chars]];
30334
+ }
30335
+ function combineWithBandFilter(baseFilter, bandFilter) {
30336
+ if (!baseFilter) return bandFilter;
30337
+ if (baseFilter[0] === "all") {
30338
+ return ["all", ...baseFilter.slice(1), bandFilter];
30339
+ }
30340
+ return ["all", baseFilter, bandFilter];
30341
+ }
30342
+ var NOT_DOMINATED_FILTER = [
30343
+ "all",
30344
+ ["!", ["has", "_dominated_by_min"]],
30345
+ ["!=", ["get", "_dominated"], true]
30346
+ ];
30347
+ function dominatedByMinFilter(D) {
30348
+ const dInt = parseInt(D, 10);
30349
+ if (D === "6") {
30350
+ return [
30351
+ "any",
30352
+ ["==", ["get", "_dominated_by_min"], dInt],
30353
+ ["==", ["get", "_dominated"], true]
30354
+ ];
30355
+ }
30356
+ return ["==", ["get", "_dominated_by_min"], dInt];
30357
+ }
30358
+ function withinBboxFilter(bbox) {
30359
+ const [w, s] = bbox.sw;
30360
+ const [e, n] = bbox.ne;
30361
+ return [
30362
+ "within",
30363
+ {
30364
+ type: "Polygon",
30365
+ coordinates: [
30366
+ [
30367
+ [w, s],
30368
+ [e, s],
30369
+ [e, n],
30370
+ [w, n],
30371
+ [w, s]
30372
+ ]
30373
+ ]
30374
+ }
29880
30375
  ];
29881
30376
  }
30377
+ function worldMinusBboxPolygon(bbox) {
30378
+ const [w, s] = bbox.sw;
30379
+ const [e, n] = bbox.ne;
30380
+ return {
30381
+ type: "Polygon",
30382
+ coordinates: [
30383
+ [
30384
+ [-180, -85],
30385
+ [180, -85],
30386
+ [180, 85],
30387
+ [-180, 85],
30388
+ [-180, -85]
30389
+ ],
30390
+ // Inner ring (hole) — winding opposite to outer ring per GeoJSON
30391
+ // spec. MapLibre is permissive but we follow strict winding.
30392
+ [
30393
+ [w, s],
30394
+ [w, n],
30395
+ [e, n],
30396
+ [e, s],
30397
+ [w, s]
30398
+ ]
30399
+ ]
30400
+ };
30401
+ }
29882
30402
 
29883
30403
  // src/atlas/tile-number.ts
29884
30404
  function tileNumber(property, fallback) {
@@ -29949,6 +30469,57 @@ var LAND_THEMES = {
29949
30469
  // skipper is navigating by. Left at the shipped value rather than
29950
30470
  // lowered, because lowering it is a separate decision from this one.
29951
30471
  exaggeration: 0.45
30472
+ },
30473
+ // ── E-ink: paper land, no relief ──────────────────────────────────────
30474
+ // Hillshade is a continuous grey ramp — exactly what an e-ink panel cannot
30475
+ // show (and a moving one costs a refresh), so both e-ink palettes switch it
30476
+ // off (`exaggeration: 0`). Land is the chart's own LANDA in that table, so
30477
+ // charted and CHRTland land are one tone; labels are black on paper.
30478
+ EINK_COLOR: {
30479
+ base: "#f2e2b0",
30480
+ cover: { wood: "#d6e0b0", wetland: "#d0dccc", urban: "#e0dacb", sand: "#f2e2b0" },
30481
+ contour: "#a08a5a",
30482
+ contourIndex: "#6b5a3e",
30483
+ road: "#d8c08a",
30484
+ roadMajor: "#c49a4a",
30485
+ label: "#000000",
30486
+ halo: "#ffffff",
30487
+ shadow: "#6b5a3e",
30488
+ highlight: "#ffffff",
30489
+ fade: "#ffffff",
30490
+ exaggeration: 0
30491
+ },
30492
+ // Mono: one land tone (MID). Land cover is not drawn as tone — four greys
30493
+ // cannot spare one for "wood" — so every cover class is the land tone.
30494
+ EINK_MONO: {
30495
+ base: "#aaaaaa",
30496
+ cover: { wood: "#aaaaaa", wetland: "#aaaaaa", urban: "#aaaaaa", sand: "#aaaaaa" },
30497
+ contour: "#555555",
30498
+ contourIndex: "#555555",
30499
+ road: "#ffffff",
30500
+ roadMajor: "#ffffff",
30501
+ label: "#000000",
30502
+ halo: "#ffffff",
30503
+ shadow: "#555555",
30504
+ highlight: "#ffffff",
30505
+ fade: "#ffffff",
30506
+ exaggeration: 0
30507
+ },
30508
+ // Night: land is the DIM grey (the chart's EINK_NIGHT LANDA), drawn on in
30509
+ // black so the land adds no lit lines of its own; labels the night line grey.
30510
+ EINK_NIGHT: {
30511
+ base: "#555555",
30512
+ cover: { wood: "#555555", wetland: "#555555", urban: "#555555", sand: "#555555" },
30513
+ contour: "#000000",
30514
+ contourIndex: "#000000",
30515
+ road: "#000000",
30516
+ roadMajor: "#000000",
30517
+ label: "#aaaaaa",
30518
+ halo: "#000000",
30519
+ shadow: "#000000",
30520
+ highlight: "#555555",
30521
+ fade: "#000000",
30522
+ exaggeration: 0
29952
30523
  }
29953
30524
  };
29954
30525
  var FITS = ["<=", tileNumber("min_z", 0), ["zoom"]];
@@ -30331,7 +30902,10 @@ var META_LAYERS = /* @__PURE__ */ new Set([
30331
30902
  var PALETTE_NAMES = {
30332
30903
  DAY: "DAY_BRIGHT",
30333
30904
  DUSK: "DUSK",
30334
- NIGHT: "NIGHT"
30905
+ NIGHT: "NIGHT",
30906
+ EINK_COLOR: "EINK_COLOR",
30907
+ EINK_MONO: "EINK_MONO",
30908
+ EINK_NIGHT: "EINK_NIGHT"
30335
30909
  };
30336
30910
  var SYMBOL_SET_LABELS = {
30337
30911
  s52: "S-52",
@@ -30443,7 +31017,7 @@ var ORIENTATION_OPACITY_SCALE = {
30443
31017
  };
30444
31018
  var ORIENTATION_OPACITY_MAX = 0.92;
30445
31019
  var ORIENTATION_BOOST_MINZOOM = 11;
30446
- function fanOrientationLabelByCellBand(label, bandFan, objectClass) {
31020
+ function fanOrientationLabelByCellBand(label, bandFan, objectClass2) {
30447
31021
  const cellBandExpr = [
30448
31022
  "slice",
30449
31023
  ["coalesce", ["get", "_cell_id"], "??9???"],
@@ -30451,7 +31025,7 @@ function fanOrientationLabelByCellBand(label, bandFan, objectClass) {
30451
31025
  3
30452
31026
  ];
30453
31027
  const variants = [];
30454
- const scale = objectClass ? ORIENTATION_OPACITY_SCALE[objectClass] ?? 1 : 1;
31028
+ const scale = objectClass2 ? ORIENTATION_OPACITY_SCALE[objectClass2] ?? 1 : 1;
30455
31029
  for (const code of BAND_DRAW_ORDER) {
30456
31030
  const range = BAND_PRIORITY_RANGES[code];
30457
31031
  const curve = CELL_BAND_OPACITY_CURVE[code];
@@ -30650,14 +31224,14 @@ function generateStyle(palette = "DAY", opts = {}) {
30650
31224
  const bandFan = opts.bandFan ?? "collapsed";
30651
31225
  const lockedBounds = opts.chartMode?.lockedBounds;
30652
31226
  const bundles = [];
30653
- for (const objectClass of orderedClasses) {
30654
- if (META_LAYERS.has(objectClass)) continue;
31227
+ for (const objectClass2 of orderedClasses) {
31228
+ if (META_LAYERS.has(objectClass2)) continue;
30655
31229
  bundles.push({
30656
- objectClass,
30657
- featureLayers: buildLayersForClass(objectClass, palette, sourceId, {
31230
+ objectClass: objectClass2,
31231
+ featureLayers: buildLayersForClass(objectClass2, palette, sourceId, {
30658
31232
  safetyContour: opts.safetyContour
30659
31233
  }),
30660
- label: buildLabelLayer2(objectClass, palette, sourceId, opts.depthUnits ?? "meters", {
31234
+ label: buildLabelLayer2(objectClass2, palette, sourceId, opts.depthUnits ?? "meters", {
30661
31235
  safetyContour: opts.safetyContour,
30662
31236
  twoShadeSoundings: opts.twoShadeSoundings,
30663
31237
  soundingFontWeight: opts.soundingFontWeight,
@@ -30802,17 +31376,17 @@ function generateStyle(palette = "DAY", opts = {}) {
30802
31376
  if (opts.labels === "placename") {
30803
31377
  labelLayers.push(...buildPlaceNameLayers(sourceId, palette));
30804
31378
  } else {
30805
- for (const { objectClass, label } of bundles) {
31379
+ for (const { objectClass: objectClass2, label } of bundles) {
30806
31380
  if (!label) continue;
30807
- if (getLabelBehavior(objectClass) === "orientation-cross-band") {
30808
- if (objectClass === "LNDARE" || objectClass === "SEAARE") {
31381
+ if (getLabelBehavior(objectClass2) === "orientation-cross-band") {
31382
+ if (objectClass2 === "LNDARE" || objectClass2 === "SEAARE") {
30809
31383
  const Z = ISLAND_LABEL_HOIST_MINZOOM;
30810
- const { hoist, watermark } = objectClass === "LNDARE" ? splitOrientationFan(
30811
- fanOrientationLabelByCellBand(label, bandFan, objectClass),
31384
+ const { hoist, watermark } = objectClass2 === "LNDARE" ? splitOrientationFan(
31385
+ fanOrientationLabelByCellBand(label, bandFan, objectClass2),
30812
31386
  [">=", ["zoom"], Z],
30813
31387
  ["<", ["zoom"], Z]
30814
31388
  ) : splitOrientationFan(
30815
- fanOrientationLabelByCellBand(label, bandFan, objectClass),
31389
+ fanOrientationLabelByCellBand(label, bandFan, objectClass2),
30816
31390
  ["<=", buildSeaAreaTierExpression(), MAJOR_WATER_TIER_MAX],
30817
31391
  [">", buildSeaAreaTierExpression(), MAJOR_WATER_TIER_MAX],
30818
31392
  { "text-padding": MAJOR_WATER_TEXT_PADDING }
@@ -30821,7 +31395,7 @@ function generateStyle(palette = "DAY", opts = {}) {
30821
31395
  labelLayers.push(...watermark);
30822
31396
  continue;
30823
31397
  }
30824
- labelLayers.push(...fanOrientationLabelByCellBand(label, bandFan, objectClass));
31398
+ labelLayers.push(...fanOrientationLabelByCellBand(label, bandFan, objectClass2));
30825
31399
  }
30826
31400
  }
30827
31401
  }
@@ -31085,10 +31659,8 @@ function generateStyle(palette = "DAY", opts = {}) {
31085
31659
  glyphs: `${glyphsBase}/{fontstack}/{range}.pbf`,
31086
31660
  layers: finalLayers
31087
31661
  };
31088
- if (symbolSet === "s101") return applyS101SymbolSet(style);
31089
- if (symbolSet === "chrt") return applyChrtSymbolSet(style, palette);
31090
- if (symbolSet === "raster") return applyRasterSymbolSet(style);
31091
- return style;
31662
+ const familyStyle = symbolSet === "s101" ? applyS101SymbolSet(style) : symbolSet === "chrt" ? applyChrtSymbolSet(style, palette) : symbolSet === "raster" ? applyRasterSymbolSet(style) : style;
31663
+ return applyEinkPortrayal(familyStyle, palette, { safetyContour: opts.safetyContour });
31092
31664
  }
31093
31665
  function generateStyleJSON(palette = "DAY", opts = {}) {
31094
31666
  return JSON.stringify(generateStyle(palette, opts), null, 2);
@@ -31232,7 +31804,7 @@ var DEFAULT_CUSTOM_STYLE_CONFIG = {
31232
31804
  water_body_settings: DEFAULT_WATER_BODY_SETTINGS,
31233
31805
  visible_layers: null
31234
31806
  };
31235
- var COLOR_TABLE_NAMES = /* @__PURE__ */ new Set(["DAY", "DUSK", "NIGHT"]);
31807
+ var COLOR_TABLE_NAMES = /* @__PURE__ */ new Set(["DAY", "DUSK", "NIGHT", "EINK_COLOR", "EINK_MONO", "EINK_NIGHT"]);
31236
31808
  var DISPLAY_MODES = /* @__PURE__ */ new Set(["BASE", "STANDARD", "ALL"]);
31237
31809
  var LAYER_NAME_RE = /^[A-Za-z0-9_$]{2,16}$/;
31238
31810
  var MAX_VISIBLE_LAYERS = 512;
@@ -31339,12 +31911,12 @@ function sanitizeCustomStyleConfig(raw) {
31339
31911
  }
31340
31912
  function normalizeTheme(raw) {
31341
31913
  if (!raw) return null;
31342
- const upper = raw.trim().toUpperCase();
31914
+ const upper = raw.trim().toUpperCase().replace(/-/g, "_");
31343
31915
  return COLOR_TABLE_NAMES.has(upper) ? upper : null;
31344
31916
  }
31345
31917
  function resolveCustomTheme(config, requested) {
31346
31918
  if (!requested) return { palette: config.default_color_table, themeFellBack: false };
31347
- if (config.enabled_color_tables.includes(requested)) {
31919
+ if (isEinkColorTable(requested) || config.enabled_color_tables.includes(requested)) {
31348
31920
  return { palette: requested, themeFellBack: false };
31349
31921
  }
31350
31922
  return { palette: config.default_color_table, themeFellBack: true };
@@ -32022,6 +32594,11 @@ export {
32022
32594
  DEPTH_LABEL_CLASSES,
32023
32595
  DEPTH_LABEL_SIZES,
32024
32596
  DRAPEABLE_LAYER_TYPES,
32597
+ EINK_COLOR_TABLES,
32598
+ EINK_DANGER_PATTERN,
32599
+ EINK_DOTS_PATTERN,
32600
+ EINK_HATCH_PATTERN,
32601
+ EINK_MONO_GREYS,
32025
32602
  INSPECT_CLASS_RANK,
32026
32603
  INSPECT_RANK_DEVIATIONS,
32027
32604
  InteractClient,
@@ -32074,6 +32651,9 @@ export {
32074
32651
  S52_DAY,
32075
32652
  S52_DRAW_ORDER,
32076
32653
  S52_DUSK,
32654
+ S52_EINK_COLOR,
32655
+ S52_EINK_MONO,
32656
+ S52_EINK_NIGHT,
32077
32657
  S52_LOOKUP,
32078
32658
  S52_NIGHT,
32079
32659
  S52_PATTERN_SPRITE_PREFIX,
@@ -32093,6 +32673,7 @@ export {
32093
32673
  applyColorTheme,
32094
32674
  applyCustomStyleSettings,
32095
32675
  applyDepthLabelSize,
32676
+ applyEinkPortrayal,
32096
32677
  applyRasterSymbolSet,
32097
32678
  applyS101SymbolSet,
32098
32679
  atonIconExpression,
@@ -32172,6 +32753,8 @@ export {
32172
32753
  iconExpressionForFamily,
32173
32754
  isDepthAreaClassLayerId,
32174
32755
  isDrapeable,
32756
+ isEinkColorTable,
32757
+ isEinkGreyTable,
32175
32758
  isLabelLayerVisible,
32176
32759
  isMapLive,
32177
32760
  isNavAidLayer,
@@ -32211,6 +32794,7 @@ export {
32211
32794
  resolveTargetClass,
32212
32795
  rhythmProperties,
32213
32796
  s101ArtworkProvenance,
32797
+ safetyContourValues,
32214
32798
  sanitizeCustomStyleConfig,
32215
32799
  scaleLinearLight,
32216
32800
  scaleSizeValue,