@chrtco/sdk 0.10.45 → 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,971 @@ 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/s52-linestyles-generated.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
6474
+ function isMapLive(map) {
6475
+ return map._removed !== true;
6476
+ }
6477
+
6478
+ // src/atlas/s52-linestyles-generated.ts
5031
6479
  var S52_LINESTYLES = {
5032
6480
  ACHARE51: {
5033
6481
  name: "ACHARE51",
@@ -5653,252 +7101,37 @@ function houseWidth(meta) {
5653
7101
  }
5654
7102
  var S52_LINE_SPRITE_PREFIX = "LC:";
5655
7103
  function lineSpriteId(lineStyleName) {
5656
- return `${S52_LINE_SPRITE_PREFIX}${lineStyleName}`;
5657
- }
5658
- function lineStylePaint(name) {
5659
- const meta = S52_LINESTYLES[name];
5660
- if (!meta) {
5661
- throw new Error(
5662
- `Unknown S-52 linestyle "${name}". Known: ${Object.keys(S52_LINESTYLES).length} styles (regenerate with: bun run scripts/extract-opencpn-linestyles.ts)`
5663
- );
5664
- }
5665
- return {
5666
- "line-pattern": lineSpriteId(meta.name),
5667
- "line-width": houseWidth(meta)
5668
- };
5669
- }
5670
- function lineStylePaintCase(cases, fallback) {
5671
- const resolve = (name) => {
5672
- const meta = S52_LINESTYLES[name];
5673
- if (!meta) throw new Error(`Unknown S-52 linestyle "${name}"`);
5674
- return meta;
5675
- };
5676
- const fallbackMeta = resolve(fallback);
5677
- const pattern = ["case"];
5678
- const width = ["case"];
5679
- for (const [condition, name] of cases) {
5680
- const meta = resolve(name);
5681
- pattern.push(condition, lineSpriteId(meta.name));
5682
- width.push(condition, houseWidth(meta));
5683
- }
5684
- pattern.push(lineSpriteId(fallbackMeta.name));
5685
- width.push(houseWidth(fallbackMeta));
5686
- return { "line-pattern": pattern, "line-width": width };
5687
- }
5688
-
5689
- // src/atlas/s52-pattern.ts
5690
- var S52_PATTERN_SPRITE_PREFIX = "AP:";
5691
- function patternSpriteId(patternName) {
5692
- return `${S52_PATTERN_SPRITE_PREFIX}${patternName}`;
5693
- }
5694
-
5695
- // src/atlas/s52-conditional.ts
5696
- var DEFAULT_SAFETY_CONTOUR_M = 10;
5697
- var DEPARE_DEEP_CUT_M = 18.2;
5698
- var DEPARE_SHALLOW_CUT_M = 2;
5699
- function effectiveSafetyContour(safetyContourM) {
5700
- if (!Number.isFinite(safetyContourM)) return DEFAULT_SAFETY_CONTOUR_M;
5701
- return Math.min(Math.max(safetyContourM, DEPARE_SHALLOW_CUT_M), DEPARE_DEEP_CUT_M);
5702
- }
5703
- function depareColor(props, ct = "DAY", safetyContourM = DEFAULT_SAFETY_CONTOUR_M) {
5704
- const drval1 = Number(props.DRVAL1 ?? props.drval1 ?? -1);
5705
- const drval2 = Number(props.DRVAL2 ?? props.drval2 ?? drval1);
5706
- const safety = effectiveSafetyContour(safetyContourM);
5707
- if (drval1 < 0 && drval2 <= 0) return resolveColor("DEPIT", ct);
5708
- if (drval1 < DEPARE_SHALLOW_CUT_M) return resolveColor("DEPVS", ct);
5709
- if (drval1 < safety) return resolveColor("DEPMS", ct);
5710
- if (drval1 < DEPARE_DEEP_CUT_M) return resolveColor("DEPMD", ct);
5711
- return resolveColor("DEPDW", ct);
5712
- }
5713
- var DEPARE_FILL_BRANCH_TOKENS = ["DEPIT", "DEPVS", "DEPMS", "DEPMD"];
5714
- var DEPARE_FILL_FALLBACK_TOKEN = "DEPDW";
5715
- function depareFillExpression(ct = "DAY", safetyContourM = DEFAULT_SAFETY_CONTOUR_M) {
5716
- const safety = effectiveSafetyContour(safetyContourM);
5717
- const [it, vs, ms, md] = DEPARE_FILL_BRANCH_TOKENS;
5718
- return [
5719
- "case",
5720
- [
5721
- "all",
5722
- ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], 0],
5723
- ["<=", ["coalesce", ["get", "DRVAL2"], ["get", "drval2"], 0], 0]
5724
- ],
5725
- resolveColor(it, ct),
5726
- ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], DEPARE_SHALLOW_CUT_M],
5727
- resolveColor(vs, ct),
5728
- ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], safety],
5729
- resolveColor(ms, ct),
5730
- ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], DEPARE_DEEP_CUT_M],
5731
- resolveColor(md, ct),
5732
- resolveColor(DEPARE_FILL_FALLBACK_TOKEN, ct)
5733
- ];
5734
- }
5735
- function listAttrString(attr) {
5736
- return ["to-string", ["coalesce", ["get", attr], ["get", attr.toLowerCase()], ""]];
5737
- }
5738
- function listAttrCase(attr, cases, fallback) {
5739
- const attrStr = listAttrString(attr);
5740
- const expr = ["case"];
5741
- for (const [code, value] of cases) {
5742
- expr.push([">=", ["index-of", `"${code}"`, attrStr], 0], value);
5743
- }
5744
- for (const [code, value] of cases) {
5745
- expr.push(["==", attrStr, code], value);
5746
- }
5747
- for (const [code, value] of cases) {
5748
- expr.push(["==", attrStr, `[${code}]`], value);
5749
- }
5750
- expr.push(fallback);
5751
- return expr;
5752
- }
5753
- function listAttrHas(attr, codes) {
5754
- const attrStr = listAttrString(attr);
5755
- const clauses = [];
5756
- for (const code of codes) {
5757
- clauses.push([">=", ["index-of", `"${code}"`, attrStr], 0]);
5758
- clauses.push(["==", attrStr, code]);
5759
- clauses.push(["==", attrStr, `[${code}]`]);
5760
- }
5761
- return ["any", ...clauses];
5762
- }
5763
- function colourCodeCase(cases, fallback) {
5764
- return listAttrCase("COLOUR", cases, fallback);
5765
- }
5766
- function lightColor(props, ct = "DAY") {
5767
- const raw = props.COLOUR ?? props.colour;
5768
- const code = Array.isArray(raw) ? Number(raw[0]) : typeof raw === "string" && raw.startsWith("[") ? Number(JSON.parse(raw)[0]) : Number(raw ?? 0);
5769
- switch (code) {
5770
- case 1:
5771
- return resolveColor("LITWT", ct);
5772
- case 3:
5773
- return resolveColor("LITRD", ct);
5774
- case 4:
5775
- return resolveColor("LITGN", ct);
5776
- case 5:
5777
- return resolveColor("LITBL", ct);
5778
- case 6:
5779
- return resolveColor("LITYW", ct);
5780
- case 11:
5781
- return resolveColor("LITOR", ct);
5782
- default:
5783
- return resolveColor("LITYW", ct);
5784
- }
5785
- }
5786
- function lightFillExpression(ct = "DAY") {
5787
- return colourCodeCase(
5788
- [
5789
- ["3", resolveColor("LITRD", ct)],
5790
- ["4", resolveColor("LITGN", ct)],
5791
- ["6", resolveColor("LITYW", ct)],
5792
- ["11", resolveColor("LITOR", ct)],
5793
- ["5", resolveColor("LITBL", ct)],
5794
- ["1", resolveColor("LITWT", ct)]
5795
- ],
5796
- resolveColor("LITYW", ct)
5797
- );
5798
- }
5799
- function lateralColor(props, ct = "DAY") {
5800
- const catlam = Number(props.CATLAM ?? props.catlam ?? 0);
5801
- if (catlam === 1) return resolveColor("LITRD", ct);
5802
- if (catlam === 2) return resolveColor("LITGN", ct);
5803
- return resolveColor("LITYW", ct);
5804
- }
5805
- function lateralFillExpression(ct = "DAY") {
5806
- return [
5807
- "match",
5808
- ["coalesce", ["get", "CATLAM"], ["get", "catlam"], 0],
5809
- 1,
5810
- resolveColor("LITRD", ct),
5811
- 2,
5812
- resolveColor("LITGN", ct),
5813
- resolveColor("LITYW", ct)
5814
- ];
5815
- }
5816
- function hazardColor(props, ct = "DAY") {
5817
- const watlev = Number(props.WATLEV ?? props.watlev ?? 0);
5818
- const valsou = props.VALSOU ?? props.valsou;
5819
- if (watlev === 1 || watlev === 2) return resolveColor("DNGHL", ct);
5820
- if (valsou != null && Number(valsou) < 2) return resolveColor("DNGHL", ct);
5821
- return resolveColor("RESBL", ct);
5822
- }
5823
- function hazardFillExpression(ct = "DAY") {
5824
- return [
5825
- "case",
5826
- [
5827
- "any",
5828
- ["==", ["coalesce", ["get", "WATLEV"], ["get", "watlev"], 0], 1],
5829
- ["==", ["coalesce", ["get", "WATLEV"], ["get", "watlev"], 0], 2]
5830
- ],
5831
- resolveColor("DNGHL", ct),
5832
- [
5833
- "all",
5834
- ["has", "VALSOU"],
5835
- ["<", ["get", "VALSOU"], 2]
5836
- ],
5837
- resolveColor("DNGHL", ct),
5838
- resolveColor("RESBL", ct)
5839
- ];
5840
- }
5841
- function vegatnPatternExpression() {
5842
- return [
5843
- "match",
5844
- ["coalesce", ["get", "CATVEG"], ["get", "catveg"], 0],
5845
- 1,
5846
- patternSpriteId("VEGATN03"),
5847
- // grass
5848
- 3,
5849
- patternSpriteId("VEGATN03"),
5850
- // bush
5851
- 4,
5852
- patternSpriteId("VEGATN04"),
5853
- // deciduous wood
5854
- 5,
5855
- patternSpriteId("VEGATN04"),
5856
- // coniferous wood
5857
- 6,
5858
- patternSpriteId("VEGATN04"),
5859
- // wood (mixed)
5860
- 7,
5861
- patternSpriteId("MARSHES1"),
5862
- // mangroves
5863
- 10,
5864
- patternSpriteId("VEGATN03"),
5865
- // scrub
5866
- 11,
5867
- patternSpriteId("VEGATN04"),
5868
- // tree
5869
- 12,
5870
- patternSpriteId("VEGATN04"),
5871
- // evergreen
5872
- 16,
5873
- patternSpriteId("MARSHES1"),
5874
- // swamp
5875
- 17,
5876
- patternSpriteId("MARSHES1"),
5877
- // marsh
5878
- 21,
5879
- patternSpriteId("VEGATN04"),
5880
- // savannah
5881
- patternSpriteId("VEGATN03")
5882
- // default: grass pattern
5883
- ];
5884
- }
5885
- function daymarkColor(props, ct = "DAY") {
5886
- const raw = props.COLOUR ?? props.colour;
5887
- const code = Array.isArray(raw) ? Number(raw[0]) : typeof raw === "string" && raw.startsWith("[") ? Number(JSON.parse(raw)[0]) : Number(raw ?? 0);
5888
- switch (code) {
5889
- case 1:
5890
- return resolveColor("LITWT", ct);
5891
- case 3:
5892
- return resolveColor("LITRD", ct);
5893
- case 4:
5894
- return resolveColor("LITGN", ct);
5895
- case 6:
5896
- return resolveColor("LITYW", ct);
5897
- case 11:
5898
- return resolveColor("LITOR", ct);
5899
- default:
5900
- return resolveColor("LITGN", ct);
7104
+ return `${S52_LINE_SPRITE_PREFIX}${lineStyleName}`;
7105
+ }
7106
+ function lineStylePaint(name) {
7107
+ const meta = S52_LINESTYLES[name];
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));
5901
7131
  }
7132
+ pattern.push(lineSpriteId(fallbackMeta.name));
7133
+ width.push(houseWidth(fallbackMeta));
7134
+ return { "line-pattern": pattern, "line-width": width };
5902
7135
  }
5903
7136
 
5904
7137
  // src/atlas/s52-lookup.ts
@@ -19791,9 +21024,9 @@ function compileAtonExpression(lookup, mapSymbol = (name) => name) {
19791
21024
  expr.push(fallback);
19792
21025
  return expr;
19793
21026
  }
19794
- function atonIconExpression(objectClass) {
19795
- const lookup = S52_ATON_LOOKUPS[objectClass];
19796
- 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}`);
19797
21030
  return compileAtonExpression(lookup);
19798
21031
  }
19799
21032
  function lightsIconExpression() {
@@ -22878,7 +24111,9 @@ var LABEL_TINT = {
22878
24111
  };
22879
24112
  function tintFor(tint, ct) {
22880
24113
  const day = LABEL_TINT[tint].day;
22881
- 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);
22882
24117
  return capLuminance(day, relativeLuminance(resolveColor("CHBLK", ct)));
22883
24118
  }
22884
24119
  function nightText(tint) {
@@ -22889,10 +24124,14 @@ function nightText(tint) {
22889
24124
  }
22890
24125
  function onHalo(tint, ct) {
22891
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 };
22892
24129
  return { textColor: DARK_BG, textHaloColor: tintFor(tint, ct) };
22893
24130
  }
22894
24131
  function onKnockout(tint, ct) {
22895
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 };
22896
24135
  return { textColor: tintFor(tint, ct), textHaloColor: DARK_BG };
22897
24136
  }
22898
24137
  var NAV_AID_LABEL_TEXT_SIZE = [
@@ -27078,409 +28317,145 @@ function applyColorTheme(svgString, colorTable = "DAY") {
27078
28317
  }
27079
28318
  );
27080
28319
  }
27081
- function getAvailablePointSymbols() {
27082
- return Object.keys(S52_POINT_SVGS);
27083
- }
27084
- function getAvailablePatterns() {
27085
- return Object.keys(S52_PATTERN_SVGS);
27086
- }
27087
- function getAvailableLineSymbols() {
27088
- return Object.keys(S52_LINE_SVGS);
27089
- }
27090
- function getS52SymbolStats() {
27091
- return {
27092
- pointCount: Object.keys(S52_POINT_SVGS).length,
27093
- patternCount: Object.keys(S52_PATTERN_SVGS).length,
27094
- lineCount: Object.keys(S52_LINE_SVGS).length,
27095
- totalCount: Object.keys(ALL_S52_SVGS).length
27096
- };
27097
- }
27098
-
27099
- // src/atlas/s101-style.ts
27100
- var S101_ICON_PREFIX = "s101:";
27101
- var S101_SYMBOL_NAMES = new Set(Object.keys(S101_POINT_SVGS));
27102
- var S101_NO_OFFICIAL_COUNTERPART = /* @__PURE__ */ new Set([
27103
- "DNGCIR01",
27104
- "RADRFL03",
27105
- "UWTROC01",
27106
- "LNDMRK18",
27107
- "LNDMRK19",
27108
- "LIGHTHOUSE01",
27109
- "SISTAT02",
27110
- "WATTUR01"
27111
- ]);
27112
- var S101_TABLES = {
27113
- DAY: S101_DAY,
27114
- DUSK: S101_DUSK,
27115
- NIGHT: S101_NIGHT
27116
- };
27117
- var warnedTokens = /* @__PURE__ */ new Set();
27118
- function reportUnresolvedToken(token, table) {
27119
- const key = `${table}:${token}`;
27120
- if (warnedTokens.has(key)) return;
27121
- warnedTokens.add(key);
27122
- console.warn(
27123
- `[chrt/s101] colour token "${token}" is not defined in the official S-101 ${table} profile. The symbol using it will draw with a substituted colour, which on a chart changes what the symbol MEANS. Re-check the vendored assets/s101/css/${table.toLowerCase()}SvgStyle.css against the pinned catalogue commit.`
27124
- );
27125
- }
27126
- function resolveS101Color(token, table = "DAY") {
27127
- const hex = S101_TABLES[table][token];
27128
- if (hex) return hex;
27129
- reportUnresolvedToken(token, table);
27130
- return "#000000";
27131
- }
27132
- function getThemedS101Svg(symbolName, table = "DAY") {
27133
- const raw = S101_POINT_SVGS[symbolName];
27134
- if (!raw) return null;
27135
- return raw.replace(/(fill|stroke)="([A-Z][A-Z0-9]{3,5})"/g, (match, attr, token) => {
27136
- if (token === "none") return match;
27137
- const hex = S101_TABLES[table][token];
27138
- if (hex) return `${attr}="${hex}"`;
27139
- reportUnresolvedToken(token, table);
27140
- return match;
27141
- });
27142
- }
27143
- function s101ArtworkProvenance(symbolName) {
27144
- if (!symbolName) return "chrt-drawn";
27145
- if (S101_SYMBOL_NAMES.has(symbolName)) return "official";
27146
- if (getS52SvgString(symbolName) !== null) return "s52-substitute";
27147
- return "chrt-drawn";
27148
- }
27149
- function rewriteIconToS101(node) {
27150
- if (typeof node === "string") {
27151
- return S101_SYMBOL_NAMES.has(node) ? `${S101_ICON_PREFIX}${node}` : node;
27152
- }
27153
- if (Array.isArray(node)) return node.map(rewriteIconToS101);
27154
- return node;
27155
- }
27156
- var S101_AREA_FILL_NAMES = /* @__PURE__ */ new Set();
27157
- var S101_LINE_STYLE_NAMES = /* @__PURE__ */ new Set();
27158
- function rewritePatternToS101(node, available) {
27159
- if (typeof node === "string") {
27160
- return available.has(node) ? `${S101_ICON_PREFIX}${node}` : node;
27161
- }
27162
- if (Array.isArray(node)) return node.map((n) => rewritePatternToS101(n, available));
27163
- return node;
27164
- }
27165
- function collectLiterals(node, out) {
27166
- if (typeof node === "string") {
27167
- out.add(node);
27168
- return;
27169
- }
27170
- if (Array.isArray(node)) for (const n of node) collectLiterals(n, out);
27171
- }
27172
- function applyS101SymbolSet(style) {
27173
- const seen = { icon: /* @__PURE__ */ new Set(), fill: /* @__PURE__ */ new Set(), line: /* @__PURE__ */ new Set() };
27174
- for (const layer of style.layers) {
27175
- const layout = layer.layout;
27176
- const paint = layer.paint;
27177
- if (layer.type === "symbol" && layout && layout["icon-image"] != null) {
27178
- layout["icon-image"] = rewriteIconToS101(layout["icon-image"]);
27179
- collectLiterals(layout["icon-image"], seen.icon);
27180
- }
27181
- for (const bag of [layout, paint]) {
27182
- if (!bag) continue;
27183
- if (bag["fill-pattern"] != null) {
27184
- bag["fill-pattern"] = rewritePatternToS101(bag["fill-pattern"], S101_AREA_FILL_NAMES);
27185
- collectLiterals(bag["fill-pattern"], seen.fill);
27186
- }
27187
- if (bag["line-pattern"] != null) {
27188
- bag["line-pattern"] = rewritePatternToS101(bag["line-pattern"], S101_LINE_STYLE_NAMES);
27189
- collectLiterals(bag["line-pattern"], seen.line);
27190
- }
27191
- }
27192
- }
27193
- const official = (names) => [...names].filter((n) => n.startsWith(S101_ICON_PREFIX)).length;
27194
- const iconSubs = [...seen.icon].filter((n) => S101_NO_OFFICIAL_COUNTERPART.has(n)).length;
27195
- const patternSubs = (names) => [...names].filter((n) => /^(AP|LC):/.test(n)).length;
27196
- const coverage = {
27197
- pointSymbols: { official: official(seen.icon), s52Substitute: iconSubs },
27198
- areaFills: { official: official(seen.fill), s52Substitute: patternSubs(seen.fill) },
27199
- lineStyles: { official: official(seen.line), s52Substitute: patternSubs(seen.line) }
27200
- };
27201
- const metadata = style.metadata;
27202
- metadata["chrt:symbol-set"] = "s101";
27203
- metadata["chrt:s101-source-commit"] = S101_SOURCE_COMMIT;
27204
- metadata["chrt:s101-coverage"] = {
27205
- ...coverage,
27206
- /** True only when NOTHING in this document is an S-52 substitute. A
27207
- * consumer that wants "S-101 or nothing" branches on this one boolean. */
27208
- complete: Object.values(coverage).every((c) => c.s52Substitute === 0)
27209
- };
27210
- return style;
27211
- }
27212
-
27213
- // src/atlas/chrt-svgs-generated.ts
27214
- var CHRT_GLYPH_SVGS = {
27215
- "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>',
27216
- "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>',
27217
- "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>',
27218
- "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>',
27219
- "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>',
27220
- "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>',
27221
- "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>',
27222
- "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>',
27223
- "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>',
27224
- "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>',
27225
- "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>',
27226
- "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>',
27227
- "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>',
27228
- "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>',
27229
- "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>',
27230
- "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>',
27231
- "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>',
27232
- "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>',
27233
- "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>',
27234
- "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>',
27235
- "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>',
27236
- "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>',
27237
- "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>',
27238
- "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>',
27239
- "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>',
27240
- "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>'
27241
- };
27242
-
27243
- // src/atlas/chrt-style.ts
27244
- var CHRT_ICON_PREFIX = "chrt:";
27245
- var CHRT_SDF_GLYPHS = /* @__PURE__ */ new Set([
27246
- "buoy-can",
27247
- "buoy-conical",
27248
- "buoy-spherical",
27249
- "buoy-pillar",
27250
- "buoy-spar",
27251
- "buoy-barrel",
27252
- "buoy-super",
27253
- "beacon-generic",
27254
- "beacon-tower",
27255
- "beacon-lattice",
27256
- "beacon-stake",
27257
- "beacon-cairn",
27258
- "light-flare",
27259
- "lightship",
27260
- "daymark-triangle",
27261
- "daymark-square"
27262
- ]);
27263
- var CHRT_GLYPH_NAMES = new Set(Object.keys(CHRT_GLYPH_SVGS));
27264
- var CHRT_ATON_COLORS = {
27265
- "1": "#F8FAFC",
27266
- // white
27267
- "2": "#101418",
27268
- // black
27269
- "3": "#E5484D",
27270
- // red
27271
- "4": "#30A46C",
27272
- // green
27273
- "5": "#3E63DD",
27274
- // blue
27275
- "6": "#FFC805",
27276
- // yellow
27277
- "7": "#8B9298",
27278
- // grey
27279
- "8": "#A66B3A",
27280
- // brown
27281
- "9": "#FFB224",
27282
- // amber
27283
- "10": "#7E4CCB",
27284
- // violet
27285
- "11": "#F76B15",
27286
- // orange
27287
- "12": "#D671C1"
27288
- // pink (magenta-ish)
27289
- };
27290
- var CHRT_UNKNOWN_TINT = "#8B9298";
27291
- var HOUSE_BLACK = "#101418";
27292
- function chrtHouseColor(dayHex, palette = "DAY") {
27293
- if (palette === "DAY") return dayHex;
27294
- const isBlack = dayHex.toLowerCase() === HOUSE_BLACK.toLowerCase();
27295
- if (palette === "DUSK") {
27296
- return isBlack ? dayHex : capLuminance(dayHex, relativeLuminance(resolveColor("CHBLK", "DUSK")));
27297
- }
27298
- return isBlack ? NIGHT_INK : capLuminance(dayHex, relativeLuminance(NIGHT_MUTED_INK));
27299
- }
27300
- function themeChrtGlyph(svg3, palette = "DAY") {
27301
- if (palette === "DAY") return svg3;
27302
- return svg3.replace(
27303
- /(fill|stroke)="(#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6})"/g,
27304
- (_m, attr, hex) => {
27305
- const six = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
27306
- return `${attr}="${chrtHouseColor(six, palette)}"`;
27307
- }
27308
- );
27309
- }
27310
- function chrtAtonTintExpression(palette = "DAY") {
27311
- const c = (code) => chrtHouseColor(CHRT_ATON_COLORS[code], palette);
27312
- return colourCodeCase(
27313
- [
27314
- ["3", c("3")],
27315
- ["4", c("4")],
27316
- ["6", c("6")],
27317
- ["11", c("11")],
27318
- ["9", c("9")],
27319
- ["5", c("5")],
27320
- ["1", c("1")],
27321
- ["2", c("2")],
27322
- ["8", c("8")],
27323
- ["10", c("10")],
27324
- ["12", c("12")],
27325
- ["7", c("7")]
27326
- ],
27327
- chrtHouseColor(CHRT_UNKNOWN_TINT, palette)
27328
- );
28320
+ function getAvailablePointSymbols() {
28321
+ return Object.keys(S52_POINT_SVGS);
27329
28322
  }
27330
- function chrtLightTintExpression(palette = "DAY") {
27331
- const c = (code) => chrtHouseColor(CHRT_ATON_COLORS[code], palette);
27332
- return colourCodeCase(
27333
- [
27334
- ["3", c("3")],
27335
- ["4", c("4")],
27336
- ["6", c("6")],
27337
- ["11", c("11")],
27338
- ["9", c("9")],
27339
- ["5", c("5")],
27340
- ["1", c("1")]
27341
- ],
27342
- c("1")
28323
+ function getAvailablePatterns() {
28324
+ return Object.keys(S52_PATTERN_SVGS);
28325
+ }
28326
+ function getAvailableLineSymbols() {
28327
+ return Object.keys(S52_LINE_SVGS);
28328
+ }
28329
+ function getS52SymbolStats() {
28330
+ return {
28331
+ pointCount: Object.keys(S52_POINT_SVGS).length,
28332
+ patternCount: Object.keys(S52_PATTERN_SVGS).length,
28333
+ lineCount: Object.keys(S52_LINE_SVGS).length,
28334
+ totalCount: Object.keys(ALL_S52_SVGS).length
28335
+ };
28336
+ }
28337
+
28338
+ // src/atlas/s101-style.ts
28339
+ var S101_ICON_PREFIX = "s101:";
28340
+ var S101_SYMBOL_NAMES = new Set(Object.keys(S101_POINT_SVGS));
28341
+ var S101_NO_OFFICIAL_COUNTERPART = /* @__PURE__ */ new Set([
28342
+ "DNGCIR01",
28343
+ "RADRFL03",
28344
+ "UWTROC01",
28345
+ "LNDMRK18",
28346
+ "LNDMRK19",
28347
+ "LIGHTHOUSE01",
28348
+ "SISTAT02",
28349
+ "WATTUR01"
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
+ }
28358
+ var S101_TABLES = {
28359
+ DAY: S101_DAY,
28360
+ DUSK: S101_DUSK,
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)
28365
+ };
28366
+ var warnedTokens = /* @__PURE__ */ new Set();
28367
+ function reportUnresolvedToken(token, table) {
28368
+ const key = `${table}:${token}`;
28369
+ if (warnedTokens.has(key)) return;
28370
+ warnedTokens.add(key);
28371
+ console.warn(
28372
+ `[chrt/s101] colour token "${token}" is not defined in the official S-101 ${table} profile. The symbol using it will draw with a substituted colour, which on a chart changes what the symbol MEANS. Re-check the vendored assets/s101/css/${table.toLowerCase()}SvgStyle.css against the pinned catalogue commit.`
27343
28373
  );
27344
28374
  }
27345
- function scalar(attr) {
27346
- return ["coalesce", ["get", attr], ["get", attr.toLowerCase()], 0];
28375
+ function resolveS101Color(token, table = "DAY") {
28376
+ const hex = S101_TABLES[table][token];
28377
+ if (hex) return hex;
28378
+ reportUnresolvedToken(token, table);
28379
+ return "#000000";
27347
28380
  }
27348
- var g = (name) => `${CHRT_ICON_PREFIX}${name}`;
27349
- function buoyBodyExpression() {
27350
- return [
27351
- "match",
27352
- scalar("BOYSHP"),
27353
- 1,
27354
- g("buoy-conical"),
27355
- 2,
27356
- g("buoy-can"),
27357
- 3,
27358
- g("buoy-spherical"),
27359
- 4,
27360
- g("buoy-pillar"),
27361
- 5,
27362
- g("buoy-spar"),
27363
- 6,
27364
- g("buoy-barrel"),
27365
- 7,
27366
- g("buoy-super"),
27367
- 8,
27368
- g("buoy-super"),
27369
- g("buoy-pillar")
27370
- ];
28381
+ function getThemedS101Svg(symbolName, table = "DAY") {
28382
+ const raw = S101_POINT_SVGS[symbolName];
28383
+ if (!raw) return null;
28384
+ return raw.replace(/(fill|stroke)="([A-Z][A-Z0-9]{3,5})"/g, (match, attr, token) => {
28385
+ if (token === "none") return match;
28386
+ const hex = S101_TABLES[table][token];
28387
+ if (hex) return `${attr}="${hex}"`;
28388
+ reportUnresolvedToken(token, table);
28389
+ return match;
28390
+ });
27371
28391
  }
27372
- function beaconBodyExpression() {
27373
- return [
27374
- "match",
27375
- scalar("BCNSHP"),
27376
- 1,
27377
- g("beacon-stake"),
27378
- 2,
27379
- g("beacon-stake"),
27380
- 3,
27381
- g("beacon-tower"),
27382
- 4,
27383
- g("beacon-lattice"),
27384
- 5,
27385
- g("beacon-stake"),
27386
- 6,
27387
- g("beacon-cairn"),
27388
- g("beacon-generic")
27389
- ];
28392
+ function s101ArtworkProvenance(symbolName) {
28393
+ if (!symbolName) return "chrt-drawn";
28394
+ if (S101_SYMBOL_NAMES.has(symbolName)) return "official";
28395
+ if (getS52SvgString(symbolName) !== null) return "s52-substitute";
28396
+ return "chrt-drawn";
27390
28397
  }
27391
- function cardinalExpression() {
27392
- return [
27393
- "match",
27394
- scalar("CATCAM"),
27395
- 1,
27396
- g("cardinal-n"),
27397
- 2,
27398
- g("cardinal-e"),
27399
- 3,
27400
- g("cardinal-s"),
27401
- 4,
27402
- g("cardinal-w"),
27403
- g("cardinal-n")
27404
- ];
28398
+ function rewriteIconToS101(node) {
28399
+ if (typeof node === "string") {
28400
+ return S101_SYMBOL_NAMES.has(node) ? `${S101_ICON_PREFIX}${node}` : node;
28401
+ }
28402
+ if (Array.isArray(node)) return node.map(rewriteIconToS101);
28403
+ return node;
27405
28404
  }
27406
- var CHRT_ICON_EXPRESSIONS = {
27407
- BOYLAT: buoyBodyExpression,
27408
- BOYSPP: buoyBodyExpression,
27409
- BOYINB: buoyBodyExpression,
27410
- BOYCAR: cardinalExpression,
27411
- BOYISD: () => g("isolated-danger"),
27412
- BOYSAW: () => g("safe-water"),
27413
- BCNLAT: beaconBodyExpression,
27414
- BCNSPP: beaconBodyExpression,
27415
- BCNCAR: cardinalExpression,
27416
- BCNISD: () => g("isolated-danger"),
27417
- BCNSAW: () => g("safe-water"),
27418
- LIGHTS: () => g("light-flare"),
27419
- LITFLT: () => g("lightship"),
27420
- LITVES: () => g("lightship"),
27421
- FOGSIG: () => g("badge-sound"),
27422
- RTPBCN: () => g("badge-radar"),
27423
- RDOSTA: () => g("badge-radar"),
27424
- DAYMAR: () => [
27425
- "match",
27426
- scalar("TOPSHP"),
27427
- 1,
27428
- g("daymark-triangle"),
27429
- 2,
27430
- g("daymark-triangle"),
27431
- 5,
27432
- g("daymark-triangle"),
27433
- g("daymark-square")
27434
- ]
27435
- };
27436
- var TINTED_CLASSES = /* @__PURE__ */ new Set([
27437
- "BOYLAT",
27438
- "BOYSPP",
27439
- "BOYINB",
27440
- "BCNLAT",
27441
- "BCNSPP",
27442
- "DAYMAR"
27443
- ]);
27444
- var CHRT_HALO = { color: HOUSE_BLACK, width: 1 };
27445
- var CHRT_LINE_DASH = {
27446
- CBLSUB: { color: "CHMGD", width: 0.8, dash: [6, 3] },
27447
- PIPSOL: { color: "PIPLN", width: 0.8, dash: [6, 3] }
27448
- };
27449
- function applyChrtSymbolSet(style, palette = "DAY") {
28405
+ var S101_AREA_FILL_NAMES = /* @__PURE__ */ new Set();
28406
+ var S101_LINE_STYLE_NAMES = /* @__PURE__ */ new Set();
28407
+ function rewritePatternToS101(node, available) {
28408
+ if (typeof node === "string") {
28409
+ return available.has(node) ? `${S101_ICON_PREFIX}${node}` : node;
28410
+ }
28411
+ if (Array.isArray(node)) return node.map((n) => rewritePatternToS101(n, available));
28412
+ return node;
28413
+ }
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);
28420
+ }
28421
+ function applyS101SymbolSet(style) {
28422
+ const seen = { icon: /* @__PURE__ */ new Set(), fill: /* @__PURE__ */ new Set(), line: /* @__PURE__ */ new Set() };
27450
28423
  for (const layer of style.layers) {
27451
- if (layer.type === "line") {
27452
- const cls = layer.metadata?.["s52:objectClass"];
27453
- const dash = cls ? CHRT_LINE_DASH[cls] : void 0;
27454
- const paint2 = layer.paint ?? {};
27455
- if (dash && paint2["line-pattern"] != null) {
27456
- delete paint2["line-pattern"];
27457
- paint2["line-color"] = resolveColor(dash.color, palette);
27458
- paint2["line-width"] = dash.width;
27459
- paint2["line-dasharray"] = dash.dash;
27460
- layer.paint = paint2;
27461
- }
27462
- continue;
27463
- }
27464
- if (layer.type !== "symbol") continue;
27465
- const objectClass = layer.metadata?.["s52:objectClass"];
27466
- if (!objectClass || !CHRT_ICON_EXPRESSIONS[objectClass]) continue;
27467
28424
  const layout = layer.layout;
27468
- if (!layout || layout["icon-image"] == null) continue;
27469
- layout["icon-image"] = CHRT_ICON_EXPRESSIONS[objectClass]();
27470
- const paint = layer.paint ?? {};
27471
- if (TINTED_CLASSES.has(objectClass)) {
27472
- paint["icon-color"] = chrtAtonTintExpression(palette);
27473
- paint["icon-halo-color"] = CHRT_HALO.color;
27474
- paint["icon-halo-width"] = CHRT_HALO.width;
27475
- } else if (objectClass === "LIGHTS") {
27476
- paint["icon-color"] = chrtLightTintExpression(palette);
27477
- paint["icon-halo-color"] = CHRT_HALO.color;
27478
- 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
+ }
27479
28440
  }
27480
- layer.paint = paint;
27481
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
+ };
27482
28450
  const metadata = style.metadata;
27483
- 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
+ };
27484
28459
  return style;
27485
28460
  }
27486
28461
 
@@ -27637,9 +28612,9 @@ var RASTER_ATON_CLASSES = Object.keys(
27637
28612
  if (!s52) return true;
27638
28613
  return JSON.stringify({ r: paper.rows, d: paper.defaultSymbol }) !== JSON.stringify({ r: s52.rows, d: s52.defaultSymbol });
27639
28614
  });
27640
- function rasterAtonIconExpression(objectClass) {
27641
- const lookup = S52_ATON_LOOKUPS_PAPER[objectClass];
27642
- 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}`);
27643
28618
  return compileAtonExpression(lookup, resolveRasterImage);
27644
28619
  }
27645
28620
  function getRasterSvg(name, table = "DAY") {
@@ -27653,9 +28628,9 @@ function applyRasterSymbolSet(style) {
27653
28628
  const layout = layer.layout;
27654
28629
  if (!layout || layout["icon-image"] == null) continue;
27655
28630
  const metadata2 = layer.metadata;
27656
- const objectClass = metadata2?.["s52:objectClass"];
27657
- if (typeof objectClass !== "string" || !classes.has(objectClass)) continue;
27658
- 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);
27659
28634
  }
27660
28635
  const metadata = style.metadata;
27661
28636
  metadata["chrt:symbol-set"] = "raster";
@@ -27687,22 +28662,83 @@ function parseSymbolFamily(raw) {
27687
28662
  const lower = raw.toLowerCase();
27688
28663
  return SYMBOL_FAMILIES.find((f) => f === lower) ?? null;
27689
28664
  }
27690
- function iconExpressionForFamily(objectClass, family) {
27691
- const s52 = getIconExpression(objectClass);
28665
+ function iconExpressionForFamily(objectClass2, family) {
28666
+ const s52 = getIconExpression(objectClass2);
27692
28667
  switch (family) {
27693
28668
  case "s52":
27694
28669
  return s52;
27695
28670
  case "s101":
27696
28671
  return rewriteIconToS101(s52);
27697
28672
  case "chrt": {
27698
- const build = CHRT_ICON_EXPRESSIONS[objectClass];
28673
+ const build = CHRT_ICON_EXPRESSIONS[objectClass2];
27699
28674
  return build ? build() : s52;
27700
28675
  }
27701
28676
  case "raster":
27702
- return RASTER_ATON_CLASSES.includes(objectClass) ? rasterAtonIconExpression(objectClass) : s52;
28677
+ return RASTER_ATON_CLASSES.includes(objectClass2) ? rasterAtonIconExpression(objectClass2) : s52;
27703
28678
  }
27704
28679
  }
27705
28680
 
28681
+ // src/atlas/depth-label-size.ts
28682
+ var DEPTH_LABEL_SIZES = ["standard", "large"];
28683
+ var DEFAULT_DEPTH_LABEL_SIZE = "standard";
28684
+ function parseDepthLabelSize(raw) {
28685
+ if (!raw) return null;
28686
+ const lower = raw.toLowerCase();
28687
+ return DEPTH_LABEL_SIZES.find((s) => s === lower) ?? null;
28688
+ }
28689
+ var DEPTH_LABEL_CLASSES = /* @__PURE__ */ new Set([
28690
+ "SOUNDG",
28691
+ "DEPCNT",
28692
+ "UWTROC",
28693
+ "OBSTRN",
28694
+ "WRECKS"
28695
+ ]);
28696
+ var SEMIBOLD = ["Open Sans Semibold"];
28697
+ var HAZARD_DEPTH = {
28698
+ textSize: ["interpolate", ["linear"], ["zoom"], 12, 12, 16, 15],
28699
+ haloWidth: 1.4,
28700
+ font: SEMIBOLD
28701
+ };
28702
+ var LARGE = {
28703
+ // Standard: 7 / 8 / 10 / 12 at z8 / 11 / 14 / 16, halo 1.0, Semibold.
28704
+ SOUNDG: {
28705
+ textSize: ["interpolate", ["linear"], ["zoom"], 8, 10, 11, 11, 14, 14, 16, 17],
28706
+ haloWidth: 1.2,
28707
+ font: SEMIBOLD
28708
+ },
28709
+ // Standard: 8 / 10 at z10 / 14, halo 1.2, Regular.
28710
+ //
28711
+ // ⚠ ~1.2x, NOT 1.4x, AND SEMIBOLD CARRIES THE REST. A contour value rides
28712
+ // its line (`symbol-placement: line`) and needs a straight-enough run to fit;
28713
+ // it also shares rank 300 with the soundings, which place first. At 1.4x the
28714
+ // Boston harbour view kept 15 of Standard's 20 contour labels; at 1.2x, 18.
28715
+ DEPCNT: {
28716
+ textSize: ["interpolate", ["linear"], ["zoom"], 10, 10, 14, 12],
28717
+ haloWidth: 1.2,
28718
+ font: SEMIBOLD
28719
+ },
28720
+ UWTROC: HAZARD_DEPTH,
28721
+ OBSTRN: HAZARD_DEPTH,
28722
+ WRECKS: HAZARD_DEPTH
28723
+ };
28724
+ var LARGE_SOUNDING_PADDING_PX = 3.5;
28725
+ function applyDepthLabelSize(objectClass2, layer, size) {
28726
+ if (size !== "large") return layer;
28727
+ const large = LARGE[objectClass2];
28728
+ if (large === void 0) return layer;
28729
+ const layout = {
28730
+ ...layer.layout ?? {},
28731
+ "text-size": large.textSize,
28732
+ "text-font": [...large.font]
28733
+ };
28734
+ if (objectClass2 === "SOUNDG") layout["text-padding"] = LARGE_SOUNDING_PADDING_PX;
28735
+ return {
28736
+ ...layer,
28737
+ layout,
28738
+ paint: { ...layer.paint, "text-halo-width": large.haloWidth }
28739
+ };
28740
+ }
28741
+
27706
28742
  // src/atlas/base-bundle.ts
27707
28743
  var BASE_BUNDLE_ID = BASE_REGION;
27708
28744
  var BASE_BUNDLE_SCHEMA = "chrt.basemap-bundle/1";
@@ -27892,47 +28928,47 @@ function buildLightLabelExpression() {
27892
28928
  ];
27893
28929
  return ["concat", litchrExpr, siggrpExpr, colourExpr, sigperExpr, valnmrExpr];
27894
28930
  }
27895
- function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts = {}) {
27896
- if (!S52_LOOKUP[objectClass]) return [];
27897
- 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];
27898
28934
  const layers = [];
27899
- const geomTypes = getGeomTypesForClass(objectClass, entry?.style.type);
28935
+ const geomTypes = getGeomTypesForClass(objectClass2, entry?.style.type);
27900
28936
  for (const geomType of geomTypes) {
27901
- const resolved = resolveStyle(objectClass, geomType, ct, { safetyContour: opts.safetyContour });
27902
- const layerId = `s52-${objectClass}-${resolved.type}`;
28937
+ const resolved = resolveStyle(objectClass2, geomType, ct, { safetyContour: opts.safetyContour });
28938
+ const layerId = `s52-${objectClass2}-${resolved.type}`;
27903
28939
  const layer = {
27904
28940
  id: layerId,
27905
28941
  type: resolved.type,
27906
28942
  source: sourceId,
27907
- "source-layer": objectClass,
28943
+ "source-layer": objectClass2,
27908
28944
  paint: resolved.paint,
27909
28945
  metadata: {
27910
- "s52:objectClass": objectClass,
28946
+ "s52:objectClass": objectClass2,
27911
28947
  "s52:displayCategory": entry?.displayCategory ?? "OTHER",
27912
28948
  "s52:category": entry?.category ?? "Meta"
27913
28949
  }
27914
28950
  };
27915
- if (resolved.type === "line" && LINE_FROM_POLYGON_LAYERS.has(objectClass)) {
28951
+ if (resolved.type === "line" && LINE_FROM_POLYGON_LAYERS.has(objectClass2)) {
27916
28952
  layer.filter = GEOM_FILTERS.fill;
27917
28953
  } else if (GEOM_FILTERS[resolved.type]) {
27918
28954
  layer.filter = GEOM_FILTERS[resolved.type];
27919
28955
  }
27920
- if (SCAMIN_DECLUTTER_CLASSES.has(objectClass)) {
28956
+ if (SCAMIN_DECLUTTER_CLASSES.has(objectClass2)) {
27921
28957
  layer.filter = layer.filter ? ["all", layer.filter, scaminZoomGateFilter()] : ["all", scaminZoomGateFilter()];
27922
28958
  }
27923
28959
  if (resolved.minzoom) {
27924
28960
  layer.minzoom = resolved.minzoom;
27925
28961
  }
27926
- if (resolved.type === "fill" && OPAQUE_FILLS.has(objectClass)) {
28962
+ if (resolved.type === "fill" && OPAQUE_FILLS.has(objectClass2)) {
27927
28963
  layer.paint["fill-opacity"] = 1;
27928
28964
  }
27929
- if (resolved.type === "line" && ZOOM_SCALED_LINES2[objectClass]) {
27930
- 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];
27931
28967
  layer.paint["line-width"] = ["interpolate", ["linear"], ["zoom"], z1, w1, z2, w2];
27932
28968
  }
27933
28969
  if (resolved.type === "symbol") {
27934
- const iconExpr = getIconExpression(objectClass);
27935
- const childOffset = CHILD_ICON_OFFSETS[objectClass];
28970
+ const iconExpr = getIconExpression(objectClass2);
28971
+ const childOffset = CHILD_ICON_OFFSETS[objectClass2];
27936
28972
  const isSafetyCritical = SAFETY_GROUPS.has(entry?.viewingGroup ?? 0);
27937
28973
  const safetyCriticalIconSize = [
27938
28974
  "interpolate",
@@ -28055,7 +29091,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28055
29091
  ]
28056
29092
  };
28057
29093
  }
28058
- if (objectClass === "LIGHTS") {
29094
+ if (objectClass2 === "LIGHTS") {
28059
29095
  layer.layout = {
28060
29096
  ...layer.layout,
28061
29097
  "icon-allow-overlap": true,
@@ -28097,14 +29133,14 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28097
29133
  } else if (resolved.layout) {
28098
29134
  layer.layout = resolved.layout;
28099
29135
  }
28100
- if (objectClass === "LNDMRK" && resolved.type === "symbol") {
29136
+ if (objectClass2 === "LNDMRK" && resolved.type === "symbol") {
28101
29137
  const baseFilterParts = layer.filter && layer.filter[0] === "all" ? layer.filter.slice(1) : layer.filter ? [layer.filter] : [];
28102
29138
  const isWind = listAttrHas("CATLMK", ["18", "19"]);
28103
29139
  const isLighthouse = listAttrHas("CATLMK", ["17"]);
28104
29140
  const scaminFloor = scaminToMinZoom(entry?.scamin);
28105
29141
  const windLayer = {
28106
29142
  ...layer,
28107
- id: `s52-${objectClass}-wind-symbol`,
29143
+ id: `s52-${objectClass2}-wind-symbol`,
28108
29144
  filter: ["all", ...baseFilterParts, isWind],
28109
29145
  // Explicit, not inherited: turbines share exact coordinates with
28110
29146
  // their aviation-light LIGHTS feature; without allow-overlap the
@@ -28117,7 +29153,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28117
29153
  delete windLayer.minzoom;
28118
29154
  const lighthouseLayer = {
28119
29155
  ...layer,
28120
- id: `s52-${objectClass}-lighthouse-symbol`,
29156
+ id: `s52-${objectClass2}-lighthouse-symbol`,
28121
29157
  filter: ["all", ...baseFilterParts, isLighthouse, ["!", isWind]],
28122
29158
  layout: {
28123
29159
  ...layer.layout ?? {},
@@ -28128,7 +29164,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28128
29164
  delete lighthouseLayer.minzoom;
28129
29165
  const otherLayer = {
28130
29166
  ...layer,
28131
- id: `s52-${objectClass}-other-symbol`,
29167
+ id: `s52-${objectClass2}-other-symbol`,
28132
29168
  filter: ["all", ...baseFilterParts, ["!", isWind], ["!", isLighthouse]]
28133
29169
  };
28134
29170
  if (scaminFloor !== void 0) {
@@ -28143,10 +29179,10 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28143
29179
  const valsouNum = ["to-number", ["coalesce", ["get", "VALSOU"], ["get", "valsou"], 999], 999];
28144
29180
  const watlevNum = ["to-number", ["coalesce", ["get", "WATLEV"], ["get", "watlev"], 0], 0];
28145
29181
  layers.push({
28146
- id: `s52-${objectClass}-danger-ring`,
29182
+ id: `s52-${objectClass2}-danger-ring`,
28147
29183
  type: "symbol",
28148
29184
  source: sourceId,
28149
- "source-layer": objectClass,
29185
+ "source-layer": objectClass2,
28150
29186
  filter: [
28151
29187
  "all",
28152
29188
  GEOM_FILTERS.symbol,
@@ -28179,48 +29215,48 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28179
29215
  },
28180
29216
  paint: {},
28181
29217
  minzoom: 13,
28182
- metadata: { "s52:objectClass": objectClass, "s52:role": "isolated-danger-ring" }
29218
+ metadata: { "s52:objectClass": objectClass2, "s52:role": "isolated-danger-ring" }
28183
29219
  });
28184
29220
  }
28185
- if (resolved.type === "fill" && FILL_OUTLINE_LAYERS.has(objectClass)) {
28186
- 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);
28187
29223
  layers.push({
28188
- id: `s52-${objectClass}-outline`,
29224
+ id: `s52-${objectClass2}-outline`,
28189
29225
  type: "line",
28190
29226
  source: sourceId,
28191
- "source-layer": objectClass,
29227
+ "source-layer": objectClass2,
28192
29228
  filter: GEOM_FILTERS.fill,
28193
29229
  paint: {
28194
29230
  "line-color": outlineColor,
28195
29231
  "line-width": 0.3,
28196
29232
  "line-opacity": 0.4
28197
29233
  },
28198
- metadata: { "s52:objectClass": objectClass, "s52:role": "outline" }
29234
+ metadata: { "s52:objectClass": objectClass2, "s52:role": "outline" }
28199
29235
  });
28200
29236
  }
28201
- if (resolved.type === "fill" && REGULATED_AREA_BOUNDARY_LAYERS[objectClass]) {
28202
- 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];
28203
29239
  const boundaryPaint = lineStyle ? lineStylePaint(lineStyle) : {
28204
- "line-color": resolveColor(REGULATED_AREA_BOUNDARY_LAYERS[objectClass], ct),
29240
+ "line-color": resolveColor(REGULATED_AREA_BOUNDARY_LAYERS[objectClass2], ct),
28205
29241
  "line-width": 1,
28206
29242
  "line-dasharray": [4, 2],
28207
29243
  "line-opacity": 0.85
28208
29244
  };
28209
29245
  layers.push({
28210
- id: `s52-${objectClass}-boundary`,
29246
+ id: `s52-${objectClass2}-boundary`,
28211
29247
  type: "line",
28212
29248
  source: sourceId,
28213
- "source-layer": objectClass,
29249
+ "source-layer": objectClass2,
28214
29250
  filter: GEOM_FILTERS.fill,
28215
29251
  paint: boundaryPaint,
28216
29252
  metadata: {
28217
- "s52:objectClass": objectClass,
29253
+ "s52:objectClass": objectClass2,
28218
29254
  "s52:role": "boundary",
28219
29255
  ...lineStyle ? { "s52:lineStyle": lineStyle } : {}
28220
29256
  }
28221
29257
  });
28222
29258
  }
28223
- if (objectClass === "ACHARE" && resolved.type === "fill") {
29259
+ if (objectClass2 === "ACHARE" && resolved.type === "fill") {
28224
29260
  layers.push({
28225
29261
  id: "s52-ACHARE-boundary",
28226
29262
  type: "line",
@@ -28236,7 +29272,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28236
29272
  metadata: { "s52:objectClass": "ACHARE", "s52:role": "boundary" }
28237
29273
  });
28238
29274
  }
28239
- if (objectClass === "DRGARE" && resolved.type === "fill") {
29275
+ if (objectClass2 === "DRGARE" && resolved.type === "fill") {
28240
29276
  layers.push({
28241
29277
  id: "s52-DRGARE-boundary",
28242
29278
  type: "line",
@@ -28252,7 +29288,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28252
29288
  metadata: { "s52:objectClass": "DRGARE", "s52:role": "boundary" }
28253
29289
  });
28254
29290
  }
28255
- if (objectClass === "ACHARE" && resolved.type === "fill") {
29291
+ if (objectClass2 === "ACHARE" && resolved.type === "fill") {
28256
29292
  layers.push({
28257
29293
  id: "s52-ACHARE-anchor-icon",
28258
29294
  type: "symbol",
@@ -28281,7 +29317,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28281
29317
  metadata: { "s52:objectClass": "ACHARE", "s52:role": "centroid-icon" }
28282
29318
  });
28283
29319
  }
28284
- if (objectClass === "LNDARE" && resolved.type === "fill") {
29320
+ if (objectClass2 === "LNDARE" && resolved.type === "fill") {
28285
29321
  layers.push({
28286
29322
  id: `s52-LNDARE-point`,
28287
29323
  type: "circle",
@@ -28297,7 +29333,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28297
29333
  metadata: { "s52:objectClass": "LNDARE", "s52:role": "point" }
28298
29334
  });
28299
29335
  }
28300
- if (objectClass === "LNDARE" && resolved.type === "fill") {
29336
+ if (objectClass2 === "LNDARE" && resolved.type === "fill") {
28301
29337
  layers.push({
28302
29338
  id: `s52-LNDARE-line`,
28303
29339
  type: "line",
@@ -28311,7 +29347,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28311
29347
  metadata: { "s52:objectClass": "LNDARE", "s52:role": "line" }
28312
29348
  });
28313
29349
  }
28314
- if (objectClass === "BUAARE" && resolved.type === "fill") {
29350
+ if (objectClass2 === "BUAARE" && resolved.type === "fill") {
28315
29351
  layers.push({
28316
29352
  id: `s52-BUAARE-point`,
28317
29353
  type: "circle",
@@ -28327,7 +29363,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28327
29363
  metadata: { "s52:objectClass": "BUAARE", "s52:role": "point" }
28328
29364
  });
28329
29365
  }
28330
- if (objectClass === "RIVERS" && resolved.type === "fill") {
29366
+ if (objectClass2 === "RIVERS" && resolved.type === "fill") {
28331
29367
  layers.push({
28332
29368
  id: `s52-RIVERS-line`,
28333
29369
  type: "line",
@@ -28341,7 +29377,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28341
29377
  metadata: { "s52:objectClass": "RIVERS", "s52:role": "line" }
28342
29378
  });
28343
29379
  }
28344
- if (objectClass === "BUISGL" && resolved.type === "circle") {
29380
+ if (objectClass2 === "BUISGL" && resolved.type === "circle") {
28345
29381
  layers.push({
28346
29382
  id: `s52-BUISGL-polygon`,
28347
29383
  type: "fill",
@@ -28367,7 +29403,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28367
29403
  metadata: { "s52:objectClass": "BUISGL", "s52:role": "polygon-outline" }
28368
29404
  });
28369
29405
  }
28370
- if (objectClass === "BRIDGE" && resolved.type === "line") {
29406
+ if (objectClass2 === "BRIDGE" && resolved.type === "line") {
28371
29407
  layers.push({
28372
29408
  id: `s52-BRIDGE-polygon`,
28373
29409
  type: "fill",
@@ -28393,7 +29429,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28393
29429
  metadata: { "s52:objectClass": "BRIDGE", "s52:role": "polygon-outline" }
28394
29430
  });
28395
29431
  }
28396
- if (objectClass === "PYLONS" && resolved.type === "fill") {
29432
+ if (objectClass2 === "PYLONS" && resolved.type === "fill") {
28397
29433
  layers.push({
28398
29434
  id: `s52-PYLONS-point`,
28399
29435
  type: "circle",
@@ -28409,7 +29445,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28409
29445
  metadata: { "s52:objectClass": "PYLONS", "s52:role": "point" }
28410
29446
  });
28411
29447
  }
28412
- if (objectClass === "WEDKLP" && resolved.type === "fill") {
29448
+ if (objectClass2 === "WEDKLP" && resolved.type === "fill") {
28413
29449
  layers.push({
28414
29450
  id: `s52-WEDKLP-point`,
28415
29451
  type: "circle",
@@ -28426,7 +29462,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28426
29462
  metadata: { "s52:objectClass": "WEDKLP", "s52:role": "point" }
28427
29463
  });
28428
29464
  }
28429
- if (objectClass === "SLOGRD" && resolved.type === "fill") {
29465
+ if (objectClass2 === "SLOGRD" && resolved.type === "fill") {
28430
29466
  layers.push({
28431
29467
  id: `s52-SLOGRD-point`,
28432
29468
  type: "circle",
@@ -28443,7 +29479,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28443
29479
  metadata: { "s52:objectClass": "SLOGRD", "s52:role": "point" }
28444
29480
  });
28445
29481
  }
28446
- if (objectClass === "RUNWAY" && resolved.type === "fill") {
29482
+ if (objectClass2 === "RUNWAY" && resolved.type === "fill") {
28447
29483
  layers.push({
28448
29484
  id: `s52-RUNWAY-line`,
28449
29485
  type: "line",
@@ -28457,7 +29493,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28457
29493
  metadata: { "s52:objectClass": "RUNWAY", "s52:role": "line" }
28458
29494
  });
28459
29495
  }
28460
- if (objectClass === "FORSTC" && resolved.type === "fill") {
29496
+ if (objectClass2 === "FORSTC" && resolved.type === "fill") {
28461
29497
  layers.push({
28462
29498
  id: `s52-FORSTC-line`,
28463
29499
  type: "line",
@@ -28471,7 +29507,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28471
29507
  metadata: { "s52:objectClass": "FORSTC", "s52:role": "line" }
28472
29508
  });
28473
29509
  }
28474
- if (objectClass === "FSHFAC" && resolved.type === "fill") {
29510
+ if (objectClass2 === "FSHFAC" && resolved.type === "fill") {
28475
29511
  layers.push({
28476
29512
  id: `s52-FSHFAC-point`,
28477
29513
  type: "circle",
@@ -28487,7 +29523,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28487
29523
  metadata: { "s52:objectClass": "FSHFAC", "s52:role": "point" }
28488
29524
  });
28489
29525
  }
28490
- if (objectClass === "OBSTRN" && resolved.type === "symbol") {
29526
+ if (objectClass2 === "OBSTRN" && resolved.type === "symbol") {
28491
29527
  layers.push({
28492
29528
  id: `s52-OBSTRN-polygon`,
28493
29529
  type: "fill",
@@ -28527,7 +29563,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28527
29563
  metadata: { "s52:objectClass": "OBSTRN", "s52:role": "line" }
28528
29564
  });
28529
29565
  }
28530
- if (objectClass === "SLCONS" && resolved.type === "line") {
29566
+ if (objectClass2 === "SLCONS" && resolved.type === "line") {
28531
29567
  layers.push({
28532
29568
  id: `s52-SLCONS-polygon`,
28533
29569
  type: "fill",
@@ -28567,7 +29603,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28567
29603
  metadata: { "s52:objectClass": "SLCONS", "s52:role": "point" }
28568
29604
  });
28569
29605
  }
28570
- if (objectClass === "PRDARE" && resolved.type === "line") {
29606
+ if (objectClass2 === "PRDARE" && resolved.type === "line") {
28571
29607
  layers.push({
28572
29608
  id: `s52-PRDARE-point`,
28573
29609
  type: "circle",
@@ -28584,7 +29620,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28584
29620
  metadata: { "s52:objectClass": "PRDARE", "s52:role": "point" }
28585
29621
  });
28586
29622
  }
28587
- if (objectClass === "CTNARE" && resolved.type === "line") {
29623
+ if (objectClass2 === "CTNARE" && resolved.type === "line") {
28588
29624
  layers.push({
28589
29625
  id: `s52-CTNARE-point`,
28590
29626
  type: "circle",
@@ -28600,7 +29636,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28600
29636
  metadata: { "s52:objectClass": "CTNARE", "s52:role": "point" }
28601
29637
  });
28602
29638
  }
28603
- if (objectClass === "PONTON" && resolved.type === "line") {
29639
+ if (objectClass2 === "PONTON" && resolved.type === "line") {
28604
29640
  layers.push({
28605
29641
  id: `s52-PONTON-polygon`,
28606
29642
  type: "fill",
@@ -28626,7 +29662,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28626
29662
  metadata: { "s52:objectClass": "PONTON", "s52:role": "polygon-outline" }
28627
29663
  });
28628
29664
  }
28629
- if (objectClass === "WRECKS" && resolved.type === "symbol") {
29665
+ if (objectClass2 === "WRECKS" && resolved.type === "symbol") {
28630
29666
  layers.push({
28631
29667
  id: `s52-WRECKS-polygon`,
28632
29668
  type: "fill",
@@ -28653,7 +29689,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28653
29689
  metadata: { "s52:objectClass": "WRECKS", "s52:role": "polygon-outline" }
28654
29690
  });
28655
29691
  }
28656
- if (objectClass === "WATTUR" && resolved.type === "symbol") {
29692
+ if (objectClass2 === "WATTUR" && resolved.type === "symbol") {
28657
29693
  layers.push({
28658
29694
  id: `s52-WATTUR-polygon`,
28659
29695
  type: "fill",
@@ -28667,7 +29703,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28667
29703
  metadata: { "s52:objectClass": "WATTUR", "s52:role": "polygon" }
28668
29704
  });
28669
29705
  }
28670
- if (objectClass === "MAGVAR" && resolved.type === "line") {
29706
+ if (objectClass2 === "MAGVAR" && resolved.type === "line") {
28671
29707
  layers.push({
28672
29708
  id: "s52-MAGVAR-point",
28673
29709
  type: "circle",
@@ -28684,7 +29720,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28684
29720
  metadata: { "s52:objectClass": "MAGVAR", "s52:role": "point" }
28685
29721
  });
28686
29722
  }
28687
- if (objectClass === "DMPGRD" && resolved.type === "fill") {
29723
+ if (objectClass2 === "DMPGRD" && resolved.type === "fill") {
28688
29724
  layers.push({
28689
29725
  id: `s52-DMPGRD-point`,
28690
29726
  type: "circle",
@@ -28701,7 +29737,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28701
29737
  metadata: { "s52:objectClass": "DMPGRD", "s52:role": "point" }
28702
29738
  });
28703
29739
  }
28704
- if (objectClass === "DAMCON" && resolved.type === "line") {
29740
+ if (objectClass2 === "DAMCON" && resolved.type === "line") {
28705
29741
  layers.push({
28706
29742
  id: `s52-DAMCON-polygon`,
28707
29743
  type: "fill",
@@ -28727,7 +29763,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28727
29763
  metadata: { "s52:objectClass": "DAMCON", "s52:role": "outline" }
28728
29764
  });
28729
29765
  }
28730
- if (objectClass === "GATCON" && resolved.type === "line") {
29766
+ if (objectClass2 === "GATCON" && resolved.type === "line") {
28731
29767
  layers.push({
28732
29768
  id: `s52-GATCON-polygon`,
28733
29769
  type: "fill",
@@ -28753,7 +29789,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28753
29789
  metadata: { "s52:objectClass": "GATCON", "s52:role": "outline" }
28754
29790
  });
28755
29791
  }
28756
- if (objectClass === "CANALS" && resolved.type === "line") {
29792
+ if (objectClass2 === "CANALS" && resolved.type === "line") {
28757
29793
  layers.push({
28758
29794
  id: `s52-CANALS-polygon`,
28759
29795
  type: "fill",
@@ -28767,7 +29803,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28767
29803
  metadata: { "s52:objectClass": "CANALS", "s52:role": "polygon" }
28768
29804
  });
28769
29805
  }
28770
- if (objectClass === "PILBOP" && resolved.type === "symbol") {
29806
+ if (objectClass2 === "PILBOP" && resolved.type === "symbol") {
28771
29807
  layers.push({
28772
29808
  id: `s52-PILBOP-polygon`,
28773
29809
  type: "fill",
@@ -28794,7 +29830,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28794
29830
  metadata: { "s52:objectClass": "PILBOP", "s52:role": "outline" }
28795
29831
  });
28796
29832
  }
28797
- if (objectClass === "HRBFAC" && resolved.type === "circle") {
29833
+ if (objectClass2 === "HRBFAC" && resolved.type === "circle") {
28798
29834
  layers.push({
28799
29835
  id: `s52-HRBFAC-polygon`,
28800
29836
  type: "fill",
@@ -28808,7 +29844,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28808
29844
  metadata: { "s52:objectClass": "HRBFAC", "s52:role": "polygon" }
28809
29845
  });
28810
29846
  }
28811
- if (objectClass === "MARCUL" && resolved.type === "fill") {
29847
+ if (objectClass2 === "MARCUL" && resolved.type === "fill") {
28812
29848
  layers.push({
28813
29849
  id: `s52-MARCUL-point`,
28814
29850
  type: "circle",
@@ -28838,7 +29874,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28838
29874
  metadata: { "s52:objectClass": "MARCUL", "s52:role": "line" }
28839
29875
  });
28840
29876
  }
28841
- if (objectClass === "RECTRC" && resolved.type === "line") {
29877
+ if (objectClass2 === "RECTRC" && resolved.type === "line") {
28842
29878
  layers.push({
28843
29879
  id: `s52-RECTRC-polygon`,
28844
29880
  type: "fill",
@@ -28852,7 +29888,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28852
29888
  metadata: { "s52:objectClass": "RECTRC", "s52:role": "polygon" }
28853
29889
  });
28854
29890
  }
28855
- if (objectClass === "PONTON" && resolved.type === "fill") {
29891
+ if (objectClass2 === "PONTON" && resolved.type === "fill") {
28856
29892
  layers.push({
28857
29893
  id: `s52-PONTON-line`,
28858
29894
  type: "line",
@@ -28866,7 +29902,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28866
29902
  metadata: { "s52:objectClass": "PONTON", "s52:role": "line" }
28867
29903
  });
28868
29904
  }
28869
- if (objectClass === "SILTNK" && resolved.type === "symbol") {
29905
+ if (objectClass2 === "SILTNK" && resolved.type === "symbol") {
28870
29906
  layers.push({
28871
29907
  id: `s52-SILTNK-symbol-polygon`,
28872
29908
  type: "symbol",
@@ -28884,7 +29920,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28884
29920
  metadata: { "s52:objectClass": "SILTNK", "s52:role": "symbol-polygon" }
28885
29921
  });
28886
29922
  }
28887
- if (objectClass === "ACHBRT" && resolved.type === "symbol") {
29923
+ if (objectClass2 === "ACHBRT" && resolved.type === "symbol") {
28888
29924
  layers.push({
28889
29925
  id: `s52-ACHBRT-symbol-polygon`,
28890
29926
  type: "symbol",
@@ -28902,7 +29938,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28902
29938
  metadata: { "s52:objectClass": "ACHBRT", "s52:role": "symbol-polygon" }
28903
29939
  });
28904
29940
  }
28905
- if (objectClass === "BERTHS" && resolved.type === "line") {
29941
+ if (objectClass2 === "BERTHS" && resolved.type === "line") {
28906
29942
  layers.push({
28907
29943
  id: `s52-BERTHS-circle`,
28908
29944
  type: "circle",
@@ -28918,888 +29954,451 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28918
29954
  metadata: { "s52:objectClass": "BERTHS", "s52:role": "point" }
28919
29955
  });
28920
29956
  }
28921
- if (objectClass === "TUNNEL" && resolved.type === "line") {
29957
+ if (objectClass2 === "TUNNEL" && resolved.type === "line") {
28922
29958
  layers.push({
28923
29959
  // NOT `s52-TUNNEL-line` — that id is already taken by the main layer
28924
29960
  // (`s52-${objectClass}-${resolved.type}`), and duplicate layer ids
28925
29961
  // are a Mapbox style-spec violation (caught by style-integrity.test).
28926
29962
  id: `s52-TUNNEL-linestring`,
28927
29963
  type: "line",
28928
- source: sourceId,
28929
- "source-layer": "TUNNEL",
28930
- filter: GEOM_FILTERS.line,
28931
- paint: {
28932
- "line-color": resolveColor("CHGRD", ct),
28933
- "line-width": 1,
28934
- "line-dasharray": [2, 2]
28935
- },
28936
- metadata: { "s52:objectClass": "TUNNEL", "s52:role": "line" }
28937
- });
28938
- }
28939
- }
28940
- return layers;
28941
- }
28942
- function buildLabelLayer2(objectClass, ct, sourceId = DEFAULT_REGION, depthUnits = "meters", labelOpts = {}) {
28943
- if (!LABELED_LAYERS.has(objectClass)) return null;
28944
- const config = getLabelConfig(objectClass, {
28945
- colorTable: ct,
28946
- safetyContour: labelOpts.safetyContour,
28947
- twoShadeSoundings: labelOpts.twoShadeSoundings,
28948
- soundingFontWeight: labelOpts.soundingFontWeight
28949
- });
28950
- if (!config) return null;
28951
- let textField = config.textField;
28952
- if (objectClass === "LIGHTS") {
28953
- textField = buildLightLabelExpression();
28954
- }
28955
- if (typeof textField !== "string" && Array.isArray(textField)) {
28956
- const flat = JSON.stringify(textField);
28957
- if (flat.includes("_navAidLabel")) {
28958
- textField = buildNavAidLabelExpression();
28959
- }
28960
- }
28961
- if (depthUnits === "feet") {
28962
- if (objectClass === "SOUNDG") {
28963
- textField = [
28964
- "concat",
28965
- [
28966
- "round",
28967
- [
28968
- "*",
28969
- [
28970
- "to-number",
28971
- ["coalesce", ["get", "depth"], ["get", "DEPTH"], ["get", "VALSOU"]],
28972
- 0
28973
- ],
28974
- 3.28084
28975
- ]
28976
- ],
28977
- ""
28978
- ];
28979
- } else if (objectClass === "DEPCNT") {
28980
- textField = [
28981
- "concat",
28982
- ["round", ["*", ["to-number", ["get", "VALDCO"]], 3.28084]],
28983
- "ft"
28984
- ];
28985
- } else if (objectClass === "UWTROC" || objectClass === "OBSTRN" || objectClass === "WRECKS") {
28986
- const valsouFt = [
28987
- "to-number",
28988
- ["coalesce", ["get", "VALSOU"], ["get", "valsou"], 0],
28989
- 0
28990
- ];
28991
- textField = [
28992
- "case",
28993
- [">", valsouFt, 0],
28994
- ["concat", ["to-string", ["round", ["*", valsouFt, 3.28084]]], "ft"],
28995
- ""
28996
- ];
28997
- }
28998
- }
28999
- const labelPriority = getLabelPriority(objectClass);
29000
- const layer = {
29001
- id: `s52-${objectClass}-label`,
29002
- type: "symbol",
29003
- source: sourceId,
29004
- "source-layer": objectClass,
29005
- filter: GEOM_FILTERS.symbol,
29006
- layout: {
29007
- "text-field": textField,
29008
- "text-size": config.textSize,
29009
- "text-font": config.textFont ?? ["Open Sans Regular"],
29010
- "text-offset": config.textOffset,
29011
- "text-anchor": "top",
29012
- "text-allow-overlap": false,
29013
- "text-optional": true,
29014
- "text-padding": 4,
29015
- "symbol-sort-key": labelPriority * 1e3,
29016
- ...config.layout ?? {}
29017
- },
29018
- paint: {
29019
- "text-color": config.textColor,
29020
- "text-halo-color": config.textHaloColor,
29021
- "text-halo-width": config.textHaloWidth
29022
- },
29023
- metadata: {
29024
- "s52:objectClass": objectClass,
29025
- "chrt:labelPriority": labelPriority
29026
- }
29027
- };
29028
- if (config.minzoom) {
29029
- layer.minzoom = config.minzoom;
29030
- }
29031
- if (objectClass === "DEPCNT") {
29032
- layer.filter = GEOM_FILTERS.line;
29033
- }
29034
- if (objectClass === "CBLOHD" || objectClass === "PIPOHD") {
29035
- layer.filter = GEOM_FILTERS.line;
29036
- }
29037
- if (objectClass === "SBDARE") {
29038
- delete layer.filter;
29039
- }
29040
- if (objectClass === "MAGVAR") {
29041
- delete layer.filter;
29042
- }
29043
- if (getLabelBehavior(objectClass) === "orientation-cross-band") {
29044
- layer.filter = LABEL_ANCHOR_FILTER;
29045
- }
29046
- return layer;
29047
- }
29048
- function getGeomTypesForClass(objectClass, defaultType) {
29049
- if (SYMBOL_MAP[objectClass] && defaultType === "symbol") {
29050
- return ["symbol"];
29051
- }
29052
- if (objectClass === "SOUNDG") {
29053
- return ["circle"];
29054
- }
29055
- if (defaultType) {
29056
- return [defaultType];
29057
- }
29058
- return ["circle"];
29059
- }
29060
- function getOrderedClasses() {
29061
- const ordered = [...S52_DRAW_ORDER];
29062
- for (const key of Object.keys(S52_LOOKUP)) {
29063
- if (!ordered.includes(key)) {
29064
- const insertIdx = ordered.indexOf("SOUNDG");
29065
- if (insertIdx >= 0) {
29066
- ordered.splice(insertIdx, 0, key);
29067
- } else {
29068
- ordered.push(key);
29069
- }
29070
- }
29071
- }
29072
- return ordered;
29073
- }
29074
-
29075
- // src/atlas/terrain-order.ts
29076
- var DRAPEABLE_LAYER_TYPES = /* @__PURE__ */ new Set([
29077
- "background",
29078
- "fill",
29079
- "line",
29080
- "raster",
29081
- "hillshade",
29082
- "color-relief"
29083
- ]);
29084
- function isDrapeable(layer) {
29085
- return DRAPEABLE_LAYER_TYPES.has(layer.type);
29086
- }
29087
- function drapedFirst(layers) {
29088
- const draped = [];
29089
- const direct = [];
29090
- for (const l of layers) (isDrapeable(l) ? draped : direct).push(l);
29091
- return [...draped, ...direct];
29092
- }
29093
- function terrainStackCount(layers) {
29094
- let stacks = 0;
29095
- let prevDraped = false;
29096
- for (const l of layers) {
29097
- const d = isDrapeable(l);
29098
- if (d && !prevDraped) stacks++;
29099
- prevDraped = d;
29100
- }
29101
- return stacks;
29102
- }
29103
- function drapeHalves(layers) {
29104
- const draped = [];
29105
- const direct = [];
29106
- for (const l of layers) (isDrapeable(l) ? draped : direct).push(l);
29107
- return { draped, direct };
29108
- }
29109
-
29110
- // src/atlas/lamps.ts
29111
- var LAMP_GLOW_LAYER_ID = "s52-LIGHTS-lamp-glow";
29112
- var LAMP_CORE_LAYER_ID = "s52-LIGHTS-lamp-core";
29113
- var LAMP_LAYER_IDS = [LAMP_GLOW_LAYER_ID, LAMP_CORE_LAYER_ID];
29114
- var RANGE_NM = ["to-number", ["coalesce", ["get", "VALNMR"], ["get", "valnmr"]], 6];
29115
- var UNSECTORED_POINTS = [
29116
- "all",
29117
- ["any", ["==", ["geometry-type"], "Point"], ["==", ["geometry-type"], "MultiPoint"]],
29118
- ["!", ["has", "SECTR1"]],
29119
- // …and not lights with a charted rhythm either: those flash, and the
29120
- // flashing is computed by the app from `SIGSEQ` (light-sigseq.ts). What is
29121
- // left here is exactly the set that is steady from every bearing.
29122
- ["!", ["has", "SIGSEQ"]]
29123
- ];
29124
- function buildLampLayers(sourceId, ct) {
29125
- const colour = lightFillExpression(ct);
29126
- const common = {
29127
- type: "circle",
29128
- source: sourceId,
29129
- "source-layer": "LIGHTS",
29130
- filter: UNSECTORED_POINTS,
29131
- layout: { visibility: "none" }
29132
- };
29133
- const metadata = { "chrt:role": "lamp", "s52:objectClass": "LIGHTS" };
29134
- return [
29135
- {
29136
- ...common,
29137
- id: LAMP_GLOW_LAYER_ID,
29138
- paint: {
29139
- "circle-color": colour,
29140
- // 4 nm → 7 px, 12 nm → 12 px, 25 nm → 20 px; capped, because a
29141
- // primary seacoast light is not a searchlight.
29142
- "circle-radius": ["interpolate", ["linear"], RANGE_NM, 0, 5, 4, 7, 12, 12, 25, 20],
29143
- "circle-blur": 1,
29144
- "circle-opacity": 0.55,
29145
- // A lamp is a point of light: it does not grow as it comes near, and
29146
- // it does not lie down with the chart.
29147
- "circle-pitch-alignment": "viewport",
29148
- "circle-pitch-scale": "viewport"
29149
- },
29150
- metadata: { ...metadata, "chrt:lamp": "glow" }
29151
- },
29152
- {
29153
- ...common,
29154
- id: LAMP_CORE_LAYER_ID,
29155
- paint: {
29156
- "circle-color": colour,
29157
- "circle-radius": ["interpolate", ["linear"], RANGE_NM, 0, 1.5, 12, 2.5, 25, 3.5],
29158
- "circle-blur": 0.25,
29159
- "circle-opacity": 1,
29160
- "circle-pitch-alignment": "viewport",
29161
- "circle-pitch-scale": "viewport"
29162
- },
29163
- metadata: { ...metadata, "chrt:lamp": "core" }
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
+ });
29164
29974
  }
29165
- ];
29166
- }
29167
-
29168
- // src/atlas/band-priority.ts
29169
- var BAND_PRIORITY_RANGES = {
29170
- "1": { minzoom: 0 },
29171
- "2": { minzoom: 5 },
29172
- "3": { minzoom: 7 },
29173
- "4": { minzoom: 9 },
29174
- "5": { minzoom: 11 },
29175
- "6": { minzoom: 11 }
29176
- };
29177
- var BAND_DATA_MINZOOM = {
29178
- "1": 0,
29179
- "2": 5,
29180
- "3": 7,
29181
- "4": 9,
29182
- "5": 11,
29183
- "6": 13
29184
- };
29185
- function dominatedVariantMaxzoom(D) {
29186
- return Math.max(BAND_PRIORITY_RANGES[D].minzoom, BAND_DATA_MINZOOM[D]);
29187
- }
29188
- var DOMINANCE_EXEMPT_FILL_CLASSES = /* @__PURE__ */ new Set([
29189
- "LNDARE",
29190
- "DEPARE",
29191
- "UNSARE",
29192
- "LAKARE",
29193
- "DRGARE",
29194
- "SEAARE"
29195
- ]);
29196
- function isExemptOpaqueFill(layer) {
29197
- if (layer.type !== "fill") return false;
29198
- for (const cls of DOMINANCE_EXEMPT_FILL_CLASSES) {
29199
- if (layer.id.startsWith(`s52-${cls}-`)) return true;
29200
- }
29201
- return false;
29202
- }
29203
- var GROUND_FILL_CLASSES = /* @__PURE__ */ new Set([
29204
- ...DOMINANCE_EXEMPT_FILL_CLASSES,
29205
- "RIVERS"
29206
- ]);
29207
- function isGroundFill(layer) {
29208
- if (layer.type !== "fill") return false;
29209
- for (const cls of GROUND_FILL_CLASSES) {
29210
- if (layer.id.startsWith(`s52-${cls}-`)) return true;
29211
- }
29212
- return false;
29213
- }
29214
- var FILL_FLOOR_FREE_BANDS = /* @__PURE__ */ new Set([
29215
- "1",
29216
- "2",
29217
- "3"
29218
- ]);
29219
- function bandVariantMinzoom(layer, B) {
29220
- const classMinzoom = layer.minzoom ?? 0;
29221
- if (isGroundFill(layer) && FILL_FLOOR_FREE_BANDS.has(B)) {
29222
- return 0;
29223
- }
29224
- if (isGroundFill(layer)) {
29225
- return BAND_PRIORITY_RANGES[B].minzoom;
29226
- }
29227
- return Math.max(classMinzoom, BAND_PRIORITY_RANGES[B].minzoom);
29228
- }
29229
- var GENERALISED_AREA_FILL_CLASSES = /* @__PURE__ */ new Set([
29230
- "BUAARE",
29231
- // 2026-08-28, New York Harbor: the "New York Marine Inspection Zone and
29232
- // Captain of the Port Zone" RESARE exists at bands 2/3 as a generalized
29233
- // shore-hugging polygon whose point-sampled dominance tag MISSED (the
29234
- // Providence-BUAARE failure mode), so its magenta dashed boundary drew
29235
- // OFFSET over the band-5 detail at z11.5 — read in the field as "a
29236
- // magenta shoreline bleeding down from a less coarse band". The same
29237
- // tile carried untagged band-2 ADMARE and CTNARE giants in the same
29238
- // state. All three are area classes whose coarse-band boundaries hug
29239
- // shorelines and whose fine-band copies are authoritative.
29240
- "RESARE",
29241
- "CTNARE",
29242
- "ADMARE"
29243
- ]);
29244
- var GENERALISED_AREA_MAX_BAND = 3;
29245
- function generalisedAreaCeiling(layer, B) {
29246
- if (layer.type !== "fill" && layer.type !== "line") return null;
29247
- if (parseInt(B, 10) > GENERALISED_AREA_MAX_BAND) return null;
29248
- for (const cls of GENERALISED_AREA_FILL_CLASSES) {
29249
- if (layer.id.startsWith(`s52-${cls}-`)) return BAND_PRIORITY_RANGES["5"].minzoom;
29250
- }
29251
- return null;
29252
- }
29253
- var BAND_DRAW_ORDER = ["1", "2", "3", "4", "5", "6"];
29254
- var CHART_BAND_CODES = {
29255
- Overview: ["1"],
29256
- General: ["2"],
29257
- Coastal: ["3"],
29258
- Approach: ["4"],
29259
- Harbor: ["5", "6"]
29260
- };
29261
- var CHART_CASCADE_CODES = {
29262
- Overview: ["1"],
29263
- General: ["1", "2"],
29264
- Coastal: ["1", "2", "3"],
29265
- Approach: ["1", "2", "3", "4"],
29266
- Harbor: ["1", "2", "3", "4", "5", "6"]
29267
- };
29268
- var CHRT_STYLE_MODES = [
29269
- "continuous",
29270
- ...Object.keys(CHART_BAND_CODES).map(
29271
- (band) => `chart:${band}`
29272
- )
29273
- ];
29274
- function normalizeStyleMode(raw) {
29275
- if (raw === null || raw === void 0 || raw === "") return "continuous";
29276
- const lower = raw.toLowerCase();
29277
- return CHRT_STYLE_MODES.find((mode) => mode.toLowerCase() === lower) ?? null;
29278
- }
29279
- function styleModeBand(mode) {
29280
- return mode === "continuous" ? null : mode.slice("chart:".length);
29281
- }
29282
- function bandFilterExpression(codes) {
29283
- return cellBandMatch(["slice", ["get", "_cell_id"], 2, 3], codes);
29284
- }
29285
- var BAND_CODE_ALIASES = {
29286
- "5": ["7"]
29287
- };
29288
- function cellBandCharacters(code) {
29289
- return [code, ...BAND_CODE_ALIASES[code] ?? []];
29290
- }
29291
- function cellBandMatch(cellBandExpr, codes) {
29292
- const list = Array.isArray(codes) ? codes : [codes];
29293
- const chars = [...new Set(list.flatMap((c) => cellBandCharacters(c)))];
29294
- if (chars.length === 1) {
29295
- return ["==", cellBandExpr, chars[0]];
29296
29975
  }
29297
- return ["in", cellBandExpr, ["literal", chars]];
29976
+ return layers;
29298
29977
  }
29299
- function combineWithBandFilter(baseFilter, bandFilter) {
29300
- if (!baseFilter) return bandFilter;
29301
- if (baseFilter[0] === "all") {
29302
- 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();
29303
29990
  }
29304
- return ["all", baseFilter, bandFilter];
29305
- }
29306
- var NOT_DOMINATED_FILTER = [
29307
- "all",
29308
- ["!", ["has", "_dominated_by_min"]],
29309
- ["!=", ["get", "_dominated"], true]
29310
- ];
29311
- function dominatedByMinFilter(D) {
29312
- const dInt = parseInt(D, 10);
29313
- if (D === "6") {
29314
- return [
29315
- "any",
29316
- ["==", ["get", "_dominated_by_min"], dInt],
29317
- ["==", ["get", "_dominated"], true]
29318
- ];
29991
+ if (typeof textField !== "string" && Array.isArray(textField)) {
29992
+ const flat = JSON.stringify(textField);
29993
+ if (flat.includes("_navAidLabel")) {
29994
+ textField = buildNavAidLabelExpression();
29995
+ }
29319
29996
  }
29320
- return ["==", ["get", "_dominated_by_min"], dInt];
29321
- }
29322
- function withinBboxFilter(bbox) {
29323
- const [w, s] = bbox.sw;
29324
- const [e, n] = bbox.ne;
29325
- return [
29326
- "within",
29327
- {
29328
- type: "Polygon",
29329
- coordinates: [
29997
+ if (depthUnits === "feet") {
29998
+ if (objectClass2 === "SOUNDG") {
29999
+ textField = [
30000
+ "concat",
29330
30001
  [
29331
- [w, s],
29332
- [e, s],
29333
- [e, n],
29334
- [w, n],
29335
- [w, s]
29336
- ]
29337
- ]
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
29338
30062
  }
29339
- ];
29340
- }
29341
- function worldMinusBboxPolygon(bbox) {
29342
- const [w, s] = bbox.sw;
29343
- const [e, n] = bbox.ne;
29344
- return {
29345
- type: "Polygon",
29346
- coordinates: [
29347
- [
29348
- [-180, -85],
29349
- [180, -85],
29350
- [180, 85],
29351
- [-180, 85],
29352
- [-180, -85]
29353
- ],
29354
- // Inner ring (hole) — winding opposite to outer ring per GeoJSON
29355
- // spec. MapLibre is permissive but we follow strict winding.
29356
- [
29357
- [w, s],
29358
- [w, n],
29359
- [e, n],
29360
- [e, s],
29361
- [w, s]
29362
- ]
29363
- ]
29364
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);
29365
30083
  }
29366
-
29367
- // src/atlas/base-layers.ts
29368
- var NOCOVERAGE_PATTERN = "chrt:hatch-nocoverage";
29369
- var UNCHARTED_PATTERN = "chrt:hatch-uncharted";
29370
- function inlandWaterColor(palette) {
29371
- return (BASE_THEMES[palette] ?? BASE_THEMES.DAY).inlandWater;
29372
- }
29373
- var BASE_THEMES = {
29374
- DAY: {
29375
- // ── `earth` is the jurisdiction tan DIMMED (~0.55), not a blue-grey ──
29376
- // Outside the jurisdiction the map still owes the reader "what is land".
29377
- // State 3 in the header is "the geography, under a crosshatch", and until
29378
- // 2026-08-31 that geography was unreadable: earth #2A3439 against sea
29379
- // #1B2328 measured 1.25:1, so Hispaniola showed charted boxes with tan
29380
- // land inside and nothing recognisable around them (owner report: "no
29381
- // land painting down near the DR at some zooms"). Land is warm and sea is
29382
- // cool because that is what land and sea are; dimming the JURISDICTION
29383
- // tan rather than inventing a colour says the true thing — this is the
29384
- // same land, we simply do not carry charts for it. Held against BOTH
29385
- // neighbours: legible over sea, and never mistakable for the full tan of
29386
- // a country we do carry. DAY 3.59:1 vs sea, 3.17:1 vs tan.
29387
- earth: "#807761",
29388
- water: "#1B2328",
29389
- river: "#24313A",
29390
- boundary: "#4A575E",
29391
- jurisdiction: "#D4C69D",
29392
- // CHRTland's own tan — one land colour
29393
- inlandWater: "#B9CFDD",
29394
- // muted, unmistakably not a charted blue
29395
- // State furniture. These three were referenced by the layers but missing
29396
- // from this table (and its type — the long-standing TS2339 trio), so the
29397
- // lines shipped on MapLibre's DEFAULT black. Now deliberate: a hairline
29398
- // a shade deeper than the tan, quiet caps, tan halo.
29399
- stateLine: "#B1A077",
29400
- stateLabel: "#8E8468",
29401
- stateHalo: "#D4C69D",
29402
- // Rough-lake furniture (owner ask 2026-08-28: "the lakes need labels,
29403
- // and shorelines drawn"): a shoreline a shade deeper than the rough
29404
- // water, and a name in the water's own voice, haloed by the water.
29405
- inlandShore: "#8FA9BA",
29406
- inlandLabel: "#54707F",
29407
- inlandHalo: "#B9CFDD"
29408
- },
29409
- DUSK: {
29410
- // ── Re-derived 2026-09-01: the basemap sits UNDER the chart ──────────
29411
- // These were the DAY tones scaled by a constant, never measured against
29412
- // the DUSK chart — so `jurisdiction` came out at L=0.506 against a chart
29413
- // whose own land (LANDA #8a7d5e) is L=0.209, i.e. the context layer was
29414
- // 2.4x brighter than the data layer. Now capped at the chart's own land.
29415
- // The rest of the block rides the same 0.666 channel factor so every
29416
- // relationship the palette already chose is preserved, only moved down.
29417
- // `water` is deliberately NOT scaled: it is the reference the land/sea
29418
- // floor is measured against.
29419
- earth: "#544E3E",
29420
- water: "#161D21",
29421
- river: "#141B20",
29422
- boundary: "#293135",
29423
- jurisdiction: "#867D64",
29424
- inlandWater: "#62717A",
29425
- // Label stays DARK here — the halo is still mid-toned, so 3:1 is reachable
29426
- // downward and a bright label would be a new light source at dusk.
29427
- stateLine: "#B5A987",
29428
- stateLabel: "#38342A",
29429
- stateHalo: "#867D64",
29430
- inlandShore: "#4D5B67",
29431
- inlandLabel: "#212629",
29432
- inlandHalo: "#62717A"
29433
- },
29434
- NIGHT: {
29435
- // ── Re-derived 2026-09-01 — the owner-reported night defect ──────────
29436
- // `jurisdiction` was L=0.240 against a chart whose own land (LANDA
29437
- // #3a3020) is L=0.031: the world basemap was 7.7x BRIGHTER than the chart
29438
- // it sits under, so at z<11 the uncharted interior read as a pale
29439
- // continent-sized slab on a night chart. Capped at the chart's own land.
29440
- //
29441
- // ⚠ `earth` AND `jurisdiction` CONVERGE HERE, and that is forced, not
29442
- // sloppy. Holding earth 2:1 above the ocean AND 2:1 below the jurisdiction
29443
- // tan requires jurisdiction >= L 0.174 — 5.6x the chart's own land. At
29444
- // night the range between the ocean (L 0.006) and the cap (L 0.031) is too
29445
- // compressed to carry two land tones. COVERAGE IS STILL SAID, by the
29446
- // `chrt:hatch-nocoverage` crosshatch, which is a stronger signal than tone
29447
- // and is the one thing that cannot be lost to palette compression.
29448
- //
29449
- // Labels invert to light-on-dark: 3:1 downward is unreachable off a halo
29450
- // this close to black, and CHRTland's own night town labels already sit at
29451
- // L=0.177 for exactly this reason.
29452
- earth: "#333026",
29453
- water: "#10161A",
29454
- river: "#090D0F",
29455
- boundary: "#131719",
29456
- jurisdiction: "#353128",
29457
- inlandWater: "#232A30",
29458
- stateLine: "#484236",
29459
- stateLabel: "#837963",
29460
- stateHalo: "#353128",
29461
- inlandShore: "#1C2227",
29462
- inlandLabel: "#617484",
29463
- inlandHalo: "#232A30"
30084
+ function getGeomTypesForClass(objectClass2, defaultType) {
30085
+ if (SYMBOL_MAP[objectClass2] && defaultType === "symbol") {
30086
+ return ["symbol"];
29464
30087
  }
29465
- };
29466
- function baseLayers(opts = {}) {
29467
- const palette = opts.palette ?? "DAY";
29468
- const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
29469
- const src = opts.sourceId ?? BASE_REGION;
29470
- const L = (o) => o;
29471
- return [
29472
- // 1. Geography — the world as it is, in the quiet dark voice. The OCEAN
29473
- // is a background, not a polygon: the archive ships land and lakes,
29474
- // and "everything else is sea" is exactly what a background expresses
29475
- // — geometry-free, gap-free at every antimeridian and pole. Without
29476
- // it the sea outside coverage was bare S-52 NODTA, a light grey that
29477
- // read as "charted ground" with a hatch on it.
29478
- L({
29479
- id: "chrtbase-ocean",
29480
- type: "background",
29481
- paint: { "background-color": T.water }
29482
- }),
29483
- L({
29484
- id: "chrtbase-earth",
29485
- type: "fill",
29486
- source: src,
29487
- "source-layer": "base_earth",
29488
- paint: { "fill-color": T.earth }
29489
- }),
29490
- L({
29491
- id: "chrtbase-water",
29492
- type: "fill",
29493
- source: src,
29494
- "source-layer": "base_water",
29495
- paint: { "fill-color": T.water }
29496
- }),
29497
- L({
29498
- id: "chrtbase-river",
29499
- type: "line",
29500
- source: src,
29501
- "source-layer": "base_river",
29502
- minzoom: 9,
29503
- paint: {
29504
- "line-color": T.river,
29505
- "line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.6, 14, 1.6]
29506
- }
29507
- }),
29508
- L({
29509
- id: "chrtbase-boundary",
29510
- type: "line",
29511
- source: src,
29512
- "source-layer": "base_boundary",
29513
- paint: { "line-color": T.boundary, "line-width": 0.6, "line-opacity": 0.8 }
29514
- }),
29515
- // 2. THE DEFAULT: no coverage, everywhere, as a geometry-free background.
29516
- // Constant opacity by design — see the header. What kills it is being
29517
- // painted OVER, never fading.
29518
- L({
29519
- id: "chrtbase-nocoverage",
29520
- type: "background",
29521
- paint: { "background-pattern": NOCOVERAGE_PATTERN, "background-opacity": 0.55 }
29522
- }),
29523
- // 3. The jurisdiction: countries whose charts CHRT carries go tan — the
29524
- // same tan as CHRTland, so "land in our world" is ONE colour. Opaque,
29525
- // so it takes the crosshatch off its ground entirely.
29526
- L({
29527
- id: "chrtbase-jurisdiction",
29528
- type: "fill",
29529
- source: src,
29530
- "source-layer": "jurisdiction",
29531
- paint: { "fill-color": T.jurisdiction }
29532
- }),
29533
- // 4. States — internal boundaries and names, inside the jurisdiction
29534
- // only (owner decision 2026-08-28). A hairline a shade deeper than the
29535
- // tan, and quiet letter-spaced caps that fade as harbour detail takes
29536
- // over. The one deliberate exception to no-names-on-the-basemap:
29537
- // ADMIN furniture, nothing else — see the fence test.
29538
- L({
29539
- id: "chrtbase-state-boundary",
29540
- type: "line",
29541
- source: src,
29542
- "source-layer": "state_boundary",
29543
- minzoom: 4,
29544
- paint: {
29545
- "line-color": T.stateLine,
29546
- "line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1],
29547
- "line-dasharray": [3, 2],
29548
- "line-opacity": 0.8
29549
- }
29550
- }),
29551
- // 5. Country borders, drawn to be HOISTED above the whole chart by the
29552
- // generator (owner, 2026-08-31: "where there is charted land, can we
29553
- // add country borders on top of everything? I want it clear where
29554
- // there is a boarder."). Sourced from NE boundary_lines_land — land
29555
- // borders only, never coastline — so riding on top cannot stroke a
29556
- // coast. Dashed so it reads as an administrative line, not chart
29557
- // linework.
29558
- L({
29559
- id: "chrtbase-country-boundary",
29560
- type: "line",
29561
- source: src,
29562
- "source-layer": "country_boundary",
29563
- paint: {
29564
- "line-color": T.boundary,
29565
- "line-width": ["interpolate", ["linear"], ["zoom"], 3, 0.8, 8, 1.4, 12, 2.2],
29566
- "line-dasharray": [4, 2],
29567
- "line-opacity": 0.9
29568
- }
29569
- }),
29570
- L({
29571
- id: "chrtbase-state-label",
29572
- type: "symbol",
29573
- source: src,
29574
- "source-layer": "state_label",
29575
- minzoom: 4,
29576
- maxzoom: 10,
29577
- layout: {
29578
- "text-field": ["upcase", ["get", "name"]],
29579
- "text-font": ["Open Sans Regular"],
29580
- "text-size": ["interpolate", ["linear"], ["zoom"], 4, 9, 8, 12],
29581
- "text-letter-spacing": 0.25,
29582
- "text-allow-overlap": false,
29583
- "text-padding": 8
29584
- },
29585
- paint: {
29586
- "text-color": T.stateLabel,
29587
- "text-halo-color": T.stateHalo,
29588
- "text-halo-width": 1.2,
29589
- "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);
29590
30105
  }
29591
- })
29592
- ];
30106
+ }
30107
+ }
30108
+ return ordered;
29593
30109
  }
29594
- function baseInlandWaterLayers(opts = {}) {
29595
- const palette = opts.palette ?? "DAY";
29596
- const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
29597
- const src = opts.sourceId ?? BASE_REGION;
29598
- const L = (o) => o;
29599
- const NE_TIER = ["!", ["has", "kind"]];
29600
- const PLANET_TIER = ["has", "kind"];
29601
- 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" };
29602
30170
  return [
29603
- // ── The fill pair, SPLIT BY TIER at the handover ────────────────────────
29604
- //
29605
- // Both tiers share `inland_water` (tile-join concatenates the same layer
29606
- // name), and an unsplit fill draws BOTH — which means the NE polygon is
29607
- // painted over the planet one at every zoom. That is not a cosmetic
29608
- // double-draw: NE 10m carries no interior rings, so its blob has no
29609
- // ISLANDS in it, and painting it over the planet's holed polygons buries
29610
- // every island in the lake under "uncharted water". Owner-reported at
29611
- // Winnipesaukee z11.7 ("there is land (and islands) being covered by the
29612
- // 'no chart here' boundary"), and measured: rendering the planet tier
29613
- // alone brings back the whole archipelago — Rattlesnake, Bear, Long
29614
- // Island, Diamond, the Barndoors — while the NE tier alone is a faceted
29615
- // blob whose edges bear no relation to the shoreline drawn beneath it.
29616
- //
29617
- // So each tier owns its own zoom range, the same handover the shoreline
29618
- // below uses. Known residue, measured over the whole archive: of the 141
29619
- // NE lakes we still carry, 138 have planet water beneath them; the
29620
- // exceptions are two unnamed western-Alaska tundra lakes and a 2e-5 deg²
29621
- // Lake Michigan sliver, which lose their rough fill above z9 because the
29622
- // planet has no `water` polygon there at all (checked against the raw
29623
- // pre-clip NDJSON — not a clip artefact).
29624
- L({
29625
- id: "chrtbase-inland-water",
29626
- type: "fill",
29627
- source: src,
29628
- "source-layer": "inland_water",
29629
- filter: NE_TIER,
29630
- maxzoom: HANDOVER,
29631
- paint: { "fill-color": T.inlandWater }
29632
- }),
29633
- L({
29634
- id: "chrtbase-inland-hatch",
29635
- type: "fill",
29636
- source: src,
29637
- "source-layer": "inland_water",
29638
- filter: NE_TIER,
29639
- maxzoom: HANDOVER,
29640
- paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
29641
- }),
29642
- L({
29643
- id: "chrtbase-inland-water-detail",
29644
- type: "fill",
29645
- source: src,
29646
- "source-layer": "inland_water",
29647
- filter: PLANET_TIER,
29648
- minzoom: HANDOVER,
29649
- paint: { "fill-color": T.inlandWater }
29650
- }),
29651
- L({
29652
- id: "chrtbase-inland-hatch-detail",
29653
- type: "fill",
29654
- source: src,
29655
- "source-layer": "inland_water",
29656
- filter: PLANET_TIER,
29657
- minzoom: HANDOVER,
29658
- paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
29659
- }),
29660
- // The rough shoreline (owner ask 2026-08-28). Drawn ABOVE the hatch so
29661
- // the edge stays crisp through the texture, and BELOW the charted-water
29662
- // repaint clones the generator splices in after it — so wherever the
29663
- // chart repaints its own water, the rough NE10m line yields to the
29664
- // chart's COALNE instead of double-striking it.
29665
- //
29666
- // NE-tier geometry ONLY (`kind` absent): the planet detail tier joins
29667
- // the same source-layer from z9 as MANY polygons whose chunk-clip seams
29668
- // rejoin invisibly as FILLS — but a line layer strokes every seam, and
29669
- // Winnipesaukee rendered as a mesh of internal edges. Planet features
29670
- // carry `kind` (the INLAND_KINDS filter's own field); NE's carry only
29671
- // `name`, so the absence of `kind` IS the tier discriminator.
29672
- L({
29673
- id: "chrtbase-inland-shore",
29674
- type: "line",
29675
- source: src,
29676
- "source-layer": "inland_water",
29677
- filter: NE_TIER,
29678
- // Hands the shoreline over to the planet tier at ITS floor. Both
29679
- // tiers live in `inland_water` from z9 up (tile-join concatenates the
29680
- // same layer name), and their FILLS merge invisibly — but two
29681
- // outlines do not: NE 10m generalises Winnipesaukee's shape, so its
29682
- // outline ran straight across the middle of the lake the planet had
29683
- // drawn correctly around. The coarse line is right only while it is
29684
- // the only shape we have.
29685
- maxzoom: HANDOVER,
29686
- paint: {
29687
- "line-color": T.inlandShore,
29688
- "line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1.1, 14, 1.6],
29689
- "line-opacity": 0.9
29690
- }
29691
- }),
29692
- // The PLANET tier's shoreline (owner ask 2026-08-31: "uncharted small
29693
- // lakes render with no outline"). It reads its own `inland_shore` layer,
29694
- // NOT `inland_water`, and that indirection is the whole fix: the mesh
29695
- // that forced the `!has kind` fence above is a CLIP artefact — an MVT
29696
- // feature arrives already cut to its own z10 tile, so stroking the
29697
- // polygon strokes every tile boundary it crosses. The build now walks
29698
- // those rings, drops the axis-aligned runs that sit on the read zoom's
29699
- // tile grid, and emits what is left as lines (measured on one New
29700
- // England chunk: 61 long axis-aligned mesh edges → 5, and the 5 survivors
29701
- // are 0.02–0.5 tiles off the grid, i.e. real straight water edges).
29702
- //
29703
- // Thinner and quieter than the NE line: this is finer geometry describing
29704
- // water nobody has surveyed, and approximate data should look
29705
- // approximate.
29706
- L({
29707
- id: "chrtbase-inland-shore-detail",
29708
- type: "line",
29709
- source: src,
29710
- "source-layer": "inland_shore",
29711
- minzoom: HANDOVER,
29712
- paint: {
29713
- "line-color": T.inlandShore,
29714
- "line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.5, 12, 0.9, 15, 1.2],
29715
- "line-opacity": 0.85
29716
- }
29717
- }),
29718
- // The lake's name (owner ask 2026-08-28) — the second scoped exception
29719
- // to no-names-on-the-basemap, after the states: WATER-BODY names only.
29720
- // No maxzoom: an uncharted lake keeps its name at every zoom; over
29721
- // charted lakes the chart's own SEAARE label takes over at detail zooms
29722
- // and collision culls the echo.
29723
- //
29724
- // The archive's `inland_water_label` anchors (v3+). v3 shipped ONE
29725
- // PointOnSurface anchor per lake, and a big lake's single anchor pans
29726
- // off-viewport — Winnipesaukee at z11.7 read nameless with its name two
29727
- // tiles west of the screen (owner-reported). Labelling the polygons
29728
- // instead is NOT the fix: tippecanoe's coalescing drops the NE polygon
29729
- // from some z11 tiles (measured: 11/619/747 carries only planet pieces),
29730
- // so a polygon-sourced label vanishes tile by tile. v4 therefore bakes a
29731
- // deterministic anchor GRID into big lakes — the paper-chart convention
29732
- // of repeating a name along a large water body — and this one layer
29733
- // renders however many anchors are in view; collision culls the rest.
29734
- // Planet-tier water-body NAMES (owner ask 2026-08-31: NE 10m names only
29735
- // 113 US lakes). Emitted BELOW the NE layer on purpose — MapLibre places
29736
- // symbols topmost-layer-first, so the big NE lake wins collision against
29737
- // the pond beside it, and both lose to the chart's own labels (every ENC
29738
- // class is emitted after this pair).
29739
- //
29740
- // The size threshold is OSM's own `min_zoom`, carried through the build:
29741
- // Lake Winnipesaukee reads 10, Squam Lake 12, so a pond simply is not in
29742
- // the filter until the user is close enough for it to be the answer to
29743
- // "what is that water". `["zoom"]` in a filter evaluates at integer zoom
29744
- // levels and removes the feature from placement entirely — verified
29745
- // against maplibre-gl-native rather than assumed, because a text-size-0
29746
- // gate would still claim a collision slot.
29747
- //
29748
- // The tiers are separate LAYERS, not a filtered split of one — see the
29749
- // builder for why: an archive that a not-yet-deployed style would
29750
- // mis-render cannot be promoted first, and this one has to be.
29751
- L({
29752
- id: "chrtbase-inland-label-detail",
29753
- type: "symbol",
29754
- source: src,
29755
- "source-layer": "inland_water_label_detail",
29756
- minzoom: HANDOVER,
29757
- filter: [">=", ["zoom"], ["get", "min_zoom"]],
29758
- layout: {
29759
- "text-field": ["get", "name"],
29760
- "text-font": ["Open Sans Regular"],
29761
- "text-size": ["interpolate", ["linear"], ["zoom"], 9, 9, 12, 10.5, 15, 11],
29762
- "text-max-width": 8,
29763
- "text-allow-overlap": false,
29764
- "text-padding": 6,
29765
- // Bigger water first within the tier — min_zoom IS the size rank.
29766
- "symbol-sort-key": ["get", "min_zoom"]
29767
- },
30171
+ {
30172
+ ...common,
30173
+ id: LAMP_GLOW_LAYER_ID,
29768
30174
  paint: {
29769
- "text-color": T.inlandLabel,
29770
- "text-halo-color": T.inlandHalo,
29771
- "text-halo-width": 1,
29772
- "text-opacity": 0.9
29773
- }
29774
- }),
29775
- L({
29776
- id: "chrtbase-inland-label",
29777
- type: "symbol",
29778
- source: src,
29779
- "source-layer": "inland_water_label",
29780
- minzoom: 5,
29781
- layout: {
29782
- "text-field": ["get", "name"],
29783
- "text-font": ["Open Sans Regular"],
29784
- "text-size": ["interpolate", ["linear"], ["zoom"], 5, 9.5, 10, 12],
29785
- "text-max-width": 7,
29786
- "text-allow-overlap": false,
29787
- // Zoom-scaled spacing for the anchor GRID: at z8.9 the 0.08-deg
29788
- // anchors sit ~54 px apart and a flat padding let six copies of
29789
- // "Lake Ontario" crowd one screen (owner-reported 2026-08-30).
29790
- // Wide collision boxes at low zoom thin the grid to the repeat-
29791
- // by-convention cadence; by z12 the anchors are ~850 px apart and
29792
- // the padding relaxes to normal.
29793
- "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"
29794
30185
  },
30186
+ metadata: { ...metadata, "chrt:lamp": "glow" }
30187
+ },
30188
+ {
30189
+ ...common,
30190
+ id: LAMP_CORE_LAYER_ID,
29795
30191
  paint: {
29796
- "text-color": T.inlandLabel,
29797
- "text-halo-color": T.inlandHalo,
29798
- "text-halo-width": 1.1
29799
- }
29800
- })
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
+ }
29801
30375
  ];
29802
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
+ }
29803
30402
 
29804
30403
  // src/atlas/tile-number.ts
29805
30404
  function tileNumber(property, fallback) {
@@ -29870,6 +30469,57 @@ var LAND_THEMES = {
29870
30469
  // skipper is navigating by. Left at the shipped value rather than
29871
30470
  // lowered, because lowering it is a separate decision from this one.
29872
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
29873
30523
  }
29874
30524
  };
29875
30525
  var FITS = ["<=", tileNumber("min_z", 0), ["zoom"]];
@@ -30252,7 +30902,10 @@ var META_LAYERS = /* @__PURE__ */ new Set([
30252
30902
  var PALETTE_NAMES = {
30253
30903
  DAY: "DAY_BRIGHT",
30254
30904
  DUSK: "DUSK",
30255
- NIGHT: "NIGHT"
30905
+ NIGHT: "NIGHT",
30906
+ EINK_COLOR: "EINK_COLOR",
30907
+ EINK_MONO: "EINK_MONO",
30908
+ EINK_NIGHT: "EINK_NIGHT"
30256
30909
  };
30257
30910
  var SYMBOL_SET_LABELS = {
30258
30911
  s52: "S-52",
@@ -30364,7 +31017,7 @@ var ORIENTATION_OPACITY_SCALE = {
30364
31017
  };
30365
31018
  var ORIENTATION_OPACITY_MAX = 0.92;
30366
31019
  var ORIENTATION_BOOST_MINZOOM = 11;
30367
- function fanOrientationLabelByCellBand(label, bandFan, objectClass) {
31020
+ function fanOrientationLabelByCellBand(label, bandFan, objectClass2) {
30368
31021
  const cellBandExpr = [
30369
31022
  "slice",
30370
31023
  ["coalesce", ["get", "_cell_id"], "??9???"],
@@ -30372,7 +31025,7 @@ function fanOrientationLabelByCellBand(label, bandFan, objectClass) {
30372
31025
  3
30373
31026
  ];
30374
31027
  const variants = [];
30375
- const scale = objectClass ? ORIENTATION_OPACITY_SCALE[objectClass] ?? 1 : 1;
31028
+ const scale = objectClass2 ? ORIENTATION_OPACITY_SCALE[objectClass2] ?? 1 : 1;
30376
31029
  for (const code of BAND_DRAW_ORDER) {
30377
31030
  const range = BAND_PRIORITY_RANGES[code];
30378
31031
  const curve = CELL_BAND_OPACITY_CURVE[code];
@@ -30571,17 +31224,18 @@ function generateStyle(palette = "DAY", opts = {}) {
30571
31224
  const bandFan = opts.bandFan ?? "collapsed";
30572
31225
  const lockedBounds = opts.chartMode?.lockedBounds;
30573
31226
  const bundles = [];
30574
- for (const objectClass of orderedClasses) {
30575
- if (META_LAYERS.has(objectClass)) continue;
31227
+ for (const objectClass2 of orderedClasses) {
31228
+ if (META_LAYERS.has(objectClass2)) continue;
30576
31229
  bundles.push({
30577
- objectClass,
30578
- featureLayers: buildLayersForClass(objectClass, palette, sourceId, {
31230
+ objectClass: objectClass2,
31231
+ featureLayers: buildLayersForClass(objectClass2, palette, sourceId, {
30579
31232
  safetyContour: opts.safetyContour
30580
31233
  }),
30581
- label: buildLabelLayer2(objectClass, palette, sourceId, opts.depthUnits ?? "meters", {
31234
+ label: buildLabelLayer2(objectClass2, palette, sourceId, opts.depthUnits ?? "meters", {
30582
31235
  safetyContour: opts.safetyContour,
30583
31236
  twoShadeSoundings: opts.twoShadeSoundings,
30584
- soundingFontWeight: opts.soundingFontWeight
31237
+ soundingFontWeight: opts.soundingFontWeight,
31238
+ depthLabelSize: opts.depthLabelSize
30585
31239
  })
30586
31240
  });
30587
31241
  }
@@ -30722,17 +31376,17 @@ function generateStyle(palette = "DAY", opts = {}) {
30722
31376
  if (opts.labels === "placename") {
30723
31377
  labelLayers.push(...buildPlaceNameLayers(sourceId, palette));
30724
31378
  } else {
30725
- for (const { objectClass, label } of bundles) {
31379
+ for (const { objectClass: objectClass2, label } of bundles) {
30726
31380
  if (!label) continue;
30727
- if (getLabelBehavior(objectClass) === "orientation-cross-band") {
30728
- if (objectClass === "LNDARE" || objectClass === "SEAARE") {
31381
+ if (getLabelBehavior(objectClass2) === "orientation-cross-band") {
31382
+ if (objectClass2 === "LNDARE" || objectClass2 === "SEAARE") {
30729
31383
  const Z = ISLAND_LABEL_HOIST_MINZOOM;
30730
- const { hoist, watermark } = objectClass === "LNDARE" ? splitOrientationFan(
30731
- fanOrientationLabelByCellBand(label, bandFan, objectClass),
31384
+ const { hoist, watermark } = objectClass2 === "LNDARE" ? splitOrientationFan(
31385
+ fanOrientationLabelByCellBand(label, bandFan, objectClass2),
30732
31386
  [">=", ["zoom"], Z],
30733
31387
  ["<", ["zoom"], Z]
30734
31388
  ) : splitOrientationFan(
30735
- fanOrientationLabelByCellBand(label, bandFan, objectClass),
31389
+ fanOrientationLabelByCellBand(label, bandFan, objectClass2),
30736
31390
  ["<=", buildSeaAreaTierExpression(), MAJOR_WATER_TIER_MAX],
30737
31391
  [">", buildSeaAreaTierExpression(), MAJOR_WATER_TIER_MAX],
30738
31392
  { "text-padding": MAJOR_WATER_TEXT_PADDING }
@@ -30741,7 +31395,7 @@ function generateStyle(palette = "DAY", opts = {}) {
30741
31395
  labelLayers.push(...watermark);
30742
31396
  continue;
30743
31397
  }
30744
- labelLayers.push(...fanOrientationLabelByCellBand(label, bandFan, objectClass));
31398
+ labelLayers.push(...fanOrientationLabelByCellBand(label, bandFan, objectClass2));
30745
31399
  }
30746
31400
  }
30747
31401
  }
@@ -31005,10 +31659,8 @@ function generateStyle(palette = "DAY", opts = {}) {
31005
31659
  glyphs: `${glyphsBase}/{fontstack}/{range}.pbf`,
31006
31660
  layers: finalLayers
31007
31661
  };
31008
- if (symbolSet === "s101") return applyS101SymbolSet(style);
31009
- if (symbolSet === "chrt") return applyChrtSymbolSet(style, palette);
31010
- if (symbolSet === "raster") return applyRasterSymbolSet(style);
31011
- 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 });
31012
31664
  }
31013
31665
  function generateStyleJSON(palette = "DAY", opts = {}) {
31014
31666
  return JSON.stringify(generateStyle(palette, opts), null, 2);
@@ -31152,7 +31804,7 @@ var DEFAULT_CUSTOM_STYLE_CONFIG = {
31152
31804
  water_body_settings: DEFAULT_WATER_BODY_SETTINGS,
31153
31805
  visible_layers: null
31154
31806
  };
31155
- 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"]);
31156
31808
  var DISPLAY_MODES = /* @__PURE__ */ new Set(["BASE", "STANDARD", "ALL"]);
31157
31809
  var LAYER_NAME_RE = /^[A-Za-z0-9_$]{2,16}$/;
31158
31810
  var MAX_VISIBLE_LAYERS = 512;
@@ -31259,12 +31911,12 @@ function sanitizeCustomStyleConfig(raw) {
31259
31911
  }
31260
31912
  function normalizeTheme(raw) {
31261
31913
  if (!raw) return null;
31262
- const upper = raw.trim().toUpperCase();
31914
+ const upper = raw.trim().toUpperCase().replace(/-/g, "_");
31263
31915
  return COLOR_TABLE_NAMES.has(upper) ? upper : null;
31264
31916
  }
31265
31917
  function resolveCustomTheme(config, requested) {
31266
31918
  if (!requested) return { palette: config.default_color_table, themeFellBack: false };
31267
- if (config.enabled_color_tables.includes(requested)) {
31919
+ if (isEinkColorTable(requested) || config.enabled_color_tables.includes(requested)) {
31268
31920
  return { palette: requested, themeFellBack: false };
31269
31921
  }
31270
31922
  return { palette: config.default_color_table, themeFellBack: true };
@@ -31927,6 +32579,7 @@ export {
31927
32579
  ChrtError,
31928
32580
  DEFAULT_CLICK_TOLERANCE,
31929
32581
  DEFAULT_CUSTOM_STYLE_CONFIG,
32582
+ DEFAULT_DEPTH_LABEL_SIZE,
31930
32583
  DEFAULT_INSPECT_CLASS_RANK,
31931
32584
  DEFAULT_LABEL_SETTINGS,
31932
32585
  DEFAULT_NAV_AID_SETTINGS,
@@ -31938,7 +32591,14 @@ export {
31938
32591
  DEFAULT_WATER_BODY_SETTINGS,
31939
32592
  DEPARE_FILL_BRANCH_TOKENS,
31940
32593
  DEPARE_FILL_FALLBACK_TOKEN,
32594
+ DEPTH_LABEL_CLASSES,
32595
+ DEPTH_LABEL_SIZES,
31941
32596
  DRAPEABLE_LAYER_TYPES,
32597
+ EINK_COLOR_TABLES,
32598
+ EINK_DANGER_PATTERN,
32599
+ EINK_DOTS_PATTERN,
32600
+ EINK_HATCH_PATTERN,
32601
+ EINK_MONO_GREYS,
31942
32602
  INSPECT_CLASS_RANK,
31943
32603
  INSPECT_RANK_DEVIATIONS,
31944
32604
  InteractClient,
@@ -31954,6 +32614,7 @@ export {
31954
32614
  LAND_ATTRIBUTION,
31955
32615
  LAND_BUILD_MAXZOOM,
31956
32616
  LAND_REGION,
32617
+ LARGE_SOUNDING_PADDING_PX,
31957
32618
  NAUTICAL_SYMBOLS,
31958
32619
  NAV_AID_LAYER_GROUPS,
31959
32620
  NAV_AID_QUERY_LAYERS,
@@ -31990,6 +32651,9 @@ export {
31990
32651
  S52_DAY,
31991
32652
  S52_DRAW_ORDER,
31992
32653
  S52_DUSK,
32654
+ S52_EINK_COLOR,
32655
+ S52_EINK_MONO,
32656
+ S52_EINK_NIGHT,
31993
32657
  S52_LOOKUP,
31994
32658
  S52_NIGHT,
31995
32659
  S52_PATTERN_SPRITE_PREFIX,
@@ -32008,6 +32672,8 @@ export {
32008
32672
  applyChrtSymbolSet,
32009
32673
  applyColorTheme,
32010
32674
  applyCustomStyleSettings,
32675
+ applyDepthLabelSize,
32676
+ applyEinkPortrayal,
32011
32677
  applyRasterSymbolSet,
32012
32678
  applyS101SymbolSet,
32013
32679
  atonIconExpression,
@@ -32087,7 +32753,10 @@ export {
32087
32753
  iconExpressionForFamily,
32088
32754
  isDepthAreaClassLayerId,
32089
32755
  isDrapeable,
32756
+ isEinkColorTable,
32757
+ isEinkGreyTable,
32090
32758
  isLabelLayerVisible,
32759
+ isMapLive,
32091
32760
  isNavAidLayer,
32092
32761
  isNavAidLayerVisible,
32093
32762
  isSoundingLayerVisible,
@@ -32105,6 +32774,7 @@ export {
32105
32774
  normalizeTheme,
32106
32775
  opaqueSplitBaseOf,
32107
32776
  paginate,
32777
+ parseDepthLabelSize,
32108
32778
  parseListCodes,
32109
32779
  parseSigseqTerms,
32110
32780
  parseSymbolFamily,
@@ -32124,6 +32794,7 @@ export {
32124
32794
  resolveTargetClass,
32125
32795
  rhythmProperties,
32126
32796
  s101ArtworkProvenance,
32797
+ safetyContourValues,
32127
32798
  sanitizeCustomStyleConfig,
32128
32799
  scaleLinearLight,
32129
32800
  scaleSizeValue,