@chrtco/sdk 0.10.46 → 0.10.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -72,6 +72,11 @@ __export(index_exports, {
72
72
  DEPTH_LABEL_CLASSES: () => DEPTH_LABEL_CLASSES,
73
73
  DEPTH_LABEL_SIZES: () => DEPTH_LABEL_SIZES,
74
74
  DRAPEABLE_LAYER_TYPES: () => DRAPEABLE_LAYER_TYPES,
75
+ EINK_COLOR_TABLES: () => EINK_COLOR_TABLES,
76
+ EINK_DANGER_PATTERN: () => EINK_DANGER_PATTERN,
77
+ EINK_DOTS_PATTERN: () => EINK_DOTS_PATTERN,
78
+ EINK_HATCH_PATTERN: () => EINK_HATCH_PATTERN,
79
+ EINK_MONO_GREYS: () => EINK_MONO_GREYS,
75
80
  INSPECT_CLASS_RANK: () => INSPECT_CLASS_RANK,
76
81
  INSPECT_RANK_DEVIATIONS: () => INSPECT_RANK_DEVIATIONS,
77
82
  InteractClient: () => InteractClient,
@@ -124,6 +129,9 @@ __export(index_exports, {
124
129
  S52_DAY: () => S52_DAY,
125
130
  S52_DRAW_ORDER: () => S52_DRAW_ORDER,
126
131
  S52_DUSK: () => S52_DUSK,
132
+ S52_EINK_COLOR: () => S52_EINK_COLOR,
133
+ S52_EINK_MONO: () => S52_EINK_MONO,
134
+ S52_EINK_NIGHT: () => S52_EINK_NIGHT,
127
135
  S52_LOOKUP: () => S52_LOOKUP,
128
136
  S52_NIGHT: () => S52_NIGHT,
129
137
  S52_PATTERN_SPRITE_PREFIX: () => S52_PATTERN_SPRITE_PREFIX,
@@ -143,6 +151,7 @@ __export(index_exports, {
143
151
  applyColorTheme: () => applyColorTheme,
144
152
  applyCustomStyleSettings: () => applyCustomStyleSettings,
145
153
  applyDepthLabelSize: () => applyDepthLabelSize,
154
+ applyEinkPortrayal: () => applyEinkPortrayal,
146
155
  applyRasterSymbolSet: () => applyRasterSymbolSet,
147
156
  applyS101SymbolSet: () => applyS101SymbolSet,
148
157
  atonIconExpression: () => atonIconExpression,
@@ -222,6 +231,8 @@ __export(index_exports, {
222
231
  iconExpressionForFamily: () => iconExpressionForFamily,
223
232
  isDepthAreaClassLayerId: () => isDepthAreaClassLayerId,
224
233
  isDrapeable: () => isDrapeable,
234
+ isEinkColorTable: () => isEinkColorTable,
235
+ isEinkGreyTable: () => isEinkGreyTable,
225
236
  isLabelLayerVisible: () => isLabelLayerVisible,
226
237
  isMapLive: () => isMapLive,
227
238
  isNavAidLayer: () => isNavAidLayer,
@@ -261,6 +272,7 @@ __export(index_exports, {
261
272
  resolveTargetClass: () => resolveTargetClass,
262
273
  rhythmProperties: () => rhythmProperties,
263
274
  s101ArtworkProvenance: () => s101ArtworkProvenance,
275
+ safetyContourValues: () => safetyContourValues,
264
276
  sanitizeCustomStyleConfig: () => sanitizeCustomStyleConfig,
265
277
  scaleLinearLight: () => scaleLinearLight,
266
278
  scaleSizeValue: () => scaleSizeValue,
@@ -2336,6 +2348,19 @@ function s57AbbrTable(acronym) {
2336
2348
  }
2337
2349
 
2338
2350
  // src/atlas/s52-colors.ts
2351
+ var EINK_COLOR_TABLES = { color: "EINK_COLOR", mono: "EINK_MONO", night: "EINK_NIGHT" };
2352
+ function isEinkColorTable(ct) {
2353
+ return ct === "EINK_COLOR" || ct === "EINK_MONO" || ct === "EINK_NIGHT";
2354
+ }
2355
+ function isEinkGreyTable(ct) {
2356
+ return ct === "EINK_MONO" || ct === "EINK_NIGHT";
2357
+ }
2358
+ var EINK_MONO_GREYS = {
2359
+ INK: "#000000",
2360
+ DARK: "#555555",
2361
+ MID: "#aaaaaa",
2362
+ PAPER: "#ffffff"
2363
+ };
2339
2364
  var S52_DAY = {
2340
2365
  // Depth
2341
2366
  NODTA: "#a4a4a4",
@@ -2616,11 +2641,235 @@ var S52_NIGHT = {
2616
2641
  RES02: "#383028",
2617
2642
  RES03: "#383028"
2618
2643
  };
2644
+ var S52_EINK_COLOR = {
2645
+ NODTA: "#b4b4b4",
2646
+ DEPIT: "#4f8f3a",
2647
+ DEPVS: "#74ade0",
2648
+ DEPMS: "#bad6ef",
2649
+ DEPMD: "#ffffff",
2650
+ DEPDW: "#ffffff",
2651
+ LANDA: "#f2e2b0",
2652
+ LANDF: "#c49a4a",
2653
+ CHBRN: "#8b5a2b",
2654
+ CHGRF: "#9a9a9a",
2655
+ CHGRD: "#444444",
2656
+ CHBLK: "#000000",
2657
+ CSTLN: "#000000",
2658
+ OUTLW: "#000000",
2659
+ BKAJ1: "#333333",
2660
+ TRFCD: "#c800c8",
2661
+ TRFCF: "#c800c8",
2662
+ RESBL: "#0050c8",
2663
+ APTS1: "#c800c8",
2664
+ DNGHL: "#e00000",
2665
+ ISHDG: "#e00000",
2666
+ LITRD: "#e00000",
2667
+ LITGN: "#00a000",
2668
+ LITYW: "#f0c000",
2669
+ LITWT: "#ffffff",
2670
+ LITBL: "#0050c8",
2671
+ LITOR: "#f07000",
2672
+ RADHI: "#00a000",
2673
+ RADLO: "#006000",
2674
+ PLRTE: "#e00000",
2675
+ ARPAT: "#008040",
2676
+ ISDNG: "#c800c8",
2677
+ CHRED: "#e00000",
2678
+ CHGRN: "#00a000",
2679
+ CHYLW: "#f0c000",
2680
+ CHMGD: "#c800c8",
2681
+ CHMGF: "#c800c8",
2682
+ CHWHT: "#ffffff",
2683
+ CHCOR: "#f07000",
2684
+ // Deeper-than-safety soundings: dark grey, the one secondary ink. Shoal
2685
+ // soundings are black — the louder numeral, as in every other table.
2686
+ SNDG1: "#555555",
2687
+ SNDG2: "#000000",
2688
+ DEPSC: "#000000",
2689
+ DEPCN: "#555555",
2690
+ UINFD: "#000000",
2691
+ UINFF: "#555555",
2692
+ UINFR: "#e00000",
2693
+ UINFG: "#00a000",
2694
+ UINFO: "#f07000",
2695
+ UINFB: "#0050c8",
2696
+ UINFM: "#c800c8",
2697
+ UIBCK: "#ffffff",
2698
+ UIBDR: "#555555",
2699
+ UIAFD: "#74ade0",
2700
+ UIAFF: "#c49a4a",
2701
+ PIPLN: "#008000",
2702
+ OUTLL: "#c49a4a",
2703
+ SCLBR: "#f07000",
2704
+ NINFO: "#f07000",
2705
+ ADINF: "#b08800",
2706
+ RESGR: "#555555",
2707
+ SHIPS: "#000000",
2708
+ PSTRK: "#000000",
2709
+ SYTRK: "#555555",
2710
+ APLRT: "#f07000",
2711
+ CURSR: "#f07000",
2712
+ BKAJ2: "#000000",
2713
+ RES01: "#808080",
2714
+ RES02: "#808080",
2715
+ RES03: "#808080"
2716
+ };
2717
+ var M = EINK_MONO_GREYS;
2718
+ var S52_EINK_MONO = {
2719
+ // No data is DARK, never the land tone: a no-data edge in the land tone
2720
+ // reads as a coastline (seen on the Boston render, 2026-10-05).
2721
+ NODTA: M.DARK,
2722
+ DEPIT: M.PAPER,
2723
+ DEPVS: M.PAPER,
2724
+ DEPMS: M.PAPER,
2725
+ DEPMD: M.PAPER,
2726
+ DEPDW: M.PAPER,
2727
+ LANDA: M.MID,
2728
+ LANDF: M.DARK,
2729
+ CHBRN: M.DARK,
2730
+ CHGRF: M.MID,
2731
+ CHGRD: M.DARK,
2732
+ CHBLK: M.INK,
2733
+ CSTLN: M.INK,
2734
+ OUTLW: M.INK,
2735
+ BKAJ1: M.DARK,
2736
+ TRFCD: M.INK,
2737
+ TRFCF: M.INK,
2738
+ RESBL: M.INK,
2739
+ APTS1: M.INK,
2740
+ DNGHL: M.INK,
2741
+ ISHDG: M.INK,
2742
+ LITRD: M.INK,
2743
+ LITGN: M.INK,
2744
+ LITYW: M.INK,
2745
+ LITWT: M.PAPER,
2746
+ LITBL: M.INK,
2747
+ LITOR: M.INK,
2748
+ RADHI: M.INK,
2749
+ RADLO: M.DARK,
2750
+ PLRTE: M.INK,
2751
+ ARPAT: M.INK,
2752
+ ISDNG: M.INK,
2753
+ CHRED: M.DARK,
2754
+ CHGRN: M.MID,
2755
+ CHYLW: M.MID,
2756
+ CHMGD: M.INK,
2757
+ CHMGF: M.INK,
2758
+ CHWHT: M.PAPER,
2759
+ CHCOR: M.MID,
2760
+ SNDG1: M.DARK,
2761
+ SNDG2: M.INK,
2762
+ DEPSC: M.INK,
2763
+ DEPCN: M.DARK,
2764
+ UINFD: M.INK,
2765
+ UINFF: M.DARK,
2766
+ UINFR: M.INK,
2767
+ UINFG: M.INK,
2768
+ UINFO: M.INK,
2769
+ UINFB: M.INK,
2770
+ UINFM: M.INK,
2771
+ UIBCK: M.PAPER,
2772
+ UIBDR: M.DARK,
2773
+ UIAFD: M.MID,
2774
+ UIAFF: M.MID,
2775
+ PIPLN: M.INK,
2776
+ OUTLL: M.DARK,
2777
+ SCLBR: M.INK,
2778
+ NINFO: M.INK,
2779
+ ADINF: M.DARK,
2780
+ RESGR: M.DARK,
2781
+ SHIPS: M.INK,
2782
+ PSTRK: M.INK,
2783
+ SYTRK: M.DARK,
2784
+ APLRT: M.INK,
2785
+ CURSR: M.INK,
2786
+ BKAJ2: M.INK,
2787
+ RES01: M.DARK,
2788
+ RES02: M.DARK,
2789
+ RES03: M.DARK
2790
+ };
2791
+ var N = { GROUND: M.INK, DIM: M.DARK, LINE: M.MID, BRIGHT: M.PAPER };
2792
+ var S52_EINK_NIGHT = {
2793
+ NODTA: N.GROUND,
2794
+ DEPIT: N.GROUND,
2795
+ DEPVS: N.GROUND,
2796
+ DEPMS: N.GROUND,
2797
+ DEPMD: N.GROUND,
2798
+ DEPDW: N.GROUND,
2799
+ LANDA: N.DIM,
2800
+ LANDF: N.LINE,
2801
+ CHBRN: N.LINE,
2802
+ CHGRF: N.DIM,
2803
+ CHGRD: N.LINE,
2804
+ CHBLK: N.LINE,
2805
+ CSTLN: N.LINE,
2806
+ OUTLW: N.LINE,
2807
+ BKAJ1: N.DIM,
2808
+ TRFCD: N.LINE,
2809
+ TRFCF: N.LINE,
2810
+ RESBL: N.LINE,
2811
+ APTS1: N.LINE,
2812
+ DNGHL: N.BRIGHT,
2813
+ ISHDG: N.BRIGHT,
2814
+ LITRD: N.BRIGHT,
2815
+ LITGN: N.BRIGHT,
2816
+ LITYW: N.BRIGHT,
2817
+ LITWT: N.BRIGHT,
2818
+ LITBL: N.BRIGHT,
2819
+ LITOR: N.BRIGHT,
2820
+ RADHI: N.LINE,
2821
+ RADLO: N.DIM,
2822
+ PLRTE: N.LINE,
2823
+ ARPAT: N.LINE,
2824
+ ISDNG: N.BRIGHT,
2825
+ // Mark bodies by tone, as in mono, so they stay apart from each other and
2826
+ // from the black water; shape still says which side.
2827
+ CHRED: N.DIM,
2828
+ CHGRN: N.LINE,
2829
+ CHYLW: N.LINE,
2830
+ CHMGD: N.LINE,
2831
+ CHMGF: N.LINE,
2832
+ CHWHT: N.BRIGHT,
2833
+ CHCOR: N.LINE,
2834
+ SNDG1: N.LINE,
2835
+ SNDG2: N.BRIGHT,
2836
+ DEPSC: N.BRIGHT,
2837
+ DEPCN: N.LINE,
2838
+ UINFD: N.LINE,
2839
+ UINFF: N.DIM,
2840
+ UINFR: N.BRIGHT,
2841
+ UINFG: N.LINE,
2842
+ UINFO: N.LINE,
2843
+ UINFB: N.LINE,
2844
+ UINFM: N.LINE,
2845
+ UIBCK: N.GROUND,
2846
+ UIBDR: N.DIM,
2847
+ UIAFD: N.DIM,
2848
+ UIAFF: N.DIM,
2849
+ PIPLN: N.LINE,
2850
+ OUTLL: N.DIM,
2851
+ SCLBR: N.LINE,
2852
+ NINFO: N.LINE,
2853
+ ADINF: N.DIM,
2854
+ RESGR: N.DIM,
2855
+ SHIPS: N.BRIGHT,
2856
+ PSTRK: N.LINE,
2857
+ SYTRK: N.DIM,
2858
+ APLRT: N.BRIGHT,
2859
+ CURSR: N.BRIGHT,
2860
+ BKAJ2: N.GROUND,
2861
+ RES01: N.DIM,
2862
+ RES02: N.DIM,
2863
+ RES03: N.DIM
2864
+ };
2619
2865
  var S52_COLORS = S52_DAY;
2620
2866
  var COLOR_TABLES = {
2621
2867
  DAY: S52_DAY,
2622
2868
  DUSK: S52_DUSK,
2623
- NIGHT: S52_NIGHT
2869
+ NIGHT: S52_NIGHT,
2870
+ EINK_COLOR: S52_EINK_COLOR,
2871
+ EINK_MONO: S52_EINK_MONO,
2872
+ EINK_NIGHT: S52_EINK_NIGHT
2624
2873
  };
2625
2874
  function getColorTable(name) {
2626
2875
  return COLOR_TABLES[name];
@@ -5267,6 +5516,254 @@ var InteractClient = class {
5267
5516
  }
5268
5517
  };
5269
5518
 
5519
+ // src/atlas/chrt-svgs-generated.ts
5520
+ var CHRT_GLYPH_SVGS = {
5521
+ "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>',
5522
+ "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>',
5523
+ "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>',
5524
+ "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>',
5525
+ "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>',
5526
+ "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>',
5527
+ "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>',
5528
+ "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>',
5529
+ "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>',
5530
+ "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>',
5531
+ "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>',
5532
+ "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>',
5533
+ "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>',
5534
+ "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>',
5535
+ "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>',
5536
+ "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>',
5537
+ "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>',
5538
+ "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>',
5539
+ "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>',
5540
+ "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>',
5541
+ "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>',
5542
+ "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>',
5543
+ "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>',
5544
+ "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>',
5545
+ "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>',
5546
+ "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>',
5547
+ "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>',
5548
+ "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>',
5549
+ "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>'
5550
+ };
5551
+
5552
+ // src/atlas/s52-pattern.ts
5553
+ var S52_PATTERN_SPRITE_PREFIX = "AP:";
5554
+ function patternSpriteId(patternName) {
5555
+ return `${S52_PATTERN_SPRITE_PREFIX}${patternName}`;
5556
+ }
5557
+
5558
+ // src/atlas/s52-conditional.ts
5559
+ var DEFAULT_SAFETY_CONTOUR_M = 10;
5560
+ var DEPARE_DEEP_CUT_M = 18.2;
5561
+ var DEPARE_SHALLOW_CUT_M = 2;
5562
+ function effectiveSafetyContour(safetyContourM) {
5563
+ if (!Number.isFinite(safetyContourM)) return DEFAULT_SAFETY_CONTOUR_M;
5564
+ return Math.min(Math.max(safetyContourM, DEPARE_SHALLOW_CUT_M), DEPARE_DEEP_CUT_M);
5565
+ }
5566
+ function depareColor(props, ct = "DAY", safetyContourM = DEFAULT_SAFETY_CONTOUR_M) {
5567
+ const drval1 = Number(props.DRVAL1 ?? props.drval1 ?? -1);
5568
+ const drval2 = Number(props.DRVAL2 ?? props.drval2 ?? drval1);
5569
+ const safety = effectiveSafetyContour(safetyContourM);
5570
+ if (drval1 < 0 && drval2 <= 0) return resolveColor("DEPIT", ct);
5571
+ if (drval1 < DEPARE_SHALLOW_CUT_M) return resolveColor("DEPVS", ct);
5572
+ if (drval1 < safety) return resolveColor("DEPMS", ct);
5573
+ if (drval1 < DEPARE_DEEP_CUT_M) return resolveColor("DEPMD", ct);
5574
+ return resolveColor("DEPDW", ct);
5575
+ }
5576
+ var DEPARE_FILL_BRANCH_TOKENS = ["DEPIT", "DEPVS", "DEPMS", "DEPMD"];
5577
+ var DEPARE_FILL_FALLBACK_TOKEN = "DEPDW";
5578
+ function depareFillExpression(ct = "DAY", safetyContourM = DEFAULT_SAFETY_CONTOUR_M) {
5579
+ const safety = effectiveSafetyContour(safetyContourM);
5580
+ const [it, vs, ms, md] = DEPARE_FILL_BRANCH_TOKENS;
5581
+ return [
5582
+ "case",
5583
+ [
5584
+ "all",
5585
+ ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], 0],
5586
+ ["<=", ["coalesce", ["get", "DRVAL2"], ["get", "drval2"], 0], 0]
5587
+ ],
5588
+ resolveColor(it, ct),
5589
+ ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], DEPARE_SHALLOW_CUT_M],
5590
+ resolveColor(vs, ct),
5591
+ ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], safety],
5592
+ resolveColor(ms, ct),
5593
+ ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], DEPARE_DEEP_CUT_M],
5594
+ resolveColor(md, ct),
5595
+ resolveColor(DEPARE_FILL_FALLBACK_TOKEN, ct)
5596
+ ];
5597
+ }
5598
+ function listAttrString(attr) {
5599
+ return ["to-string", ["coalesce", ["get", attr], ["get", attr.toLowerCase()], ""]];
5600
+ }
5601
+ function listAttrCase(attr, cases, fallback) {
5602
+ const attrStr = listAttrString(attr);
5603
+ const expr = ["case"];
5604
+ for (const [code, value] of cases) {
5605
+ expr.push([">=", ["index-of", `"${code}"`, attrStr], 0], value);
5606
+ }
5607
+ for (const [code, value] of cases) {
5608
+ expr.push(["==", attrStr, code], value);
5609
+ }
5610
+ for (const [code, value] of cases) {
5611
+ expr.push(["==", attrStr, `[${code}]`], value);
5612
+ }
5613
+ expr.push(fallback);
5614
+ return expr;
5615
+ }
5616
+ function listAttrHas(attr, codes) {
5617
+ const attrStr = listAttrString(attr);
5618
+ const clauses = [];
5619
+ for (const code of codes) {
5620
+ clauses.push([">=", ["index-of", `"${code}"`, attrStr], 0]);
5621
+ clauses.push(["==", attrStr, code]);
5622
+ clauses.push(["==", attrStr, `[${code}]`]);
5623
+ }
5624
+ return ["any", ...clauses];
5625
+ }
5626
+ function colourCodeCase(cases, fallback) {
5627
+ return listAttrCase("COLOUR", cases, fallback);
5628
+ }
5629
+ function lightColor(props, ct = "DAY") {
5630
+ const raw = props.COLOUR ?? props.colour;
5631
+ const code = Array.isArray(raw) ? Number(raw[0]) : typeof raw === "string" && raw.startsWith("[") ? Number(JSON.parse(raw)[0]) : Number(raw ?? 0);
5632
+ switch (code) {
5633
+ case 1:
5634
+ return resolveColor("LITWT", ct);
5635
+ case 3:
5636
+ return resolveColor("LITRD", ct);
5637
+ case 4:
5638
+ return resolveColor("LITGN", ct);
5639
+ case 5:
5640
+ return resolveColor("LITBL", ct);
5641
+ case 6:
5642
+ return resolveColor("LITYW", ct);
5643
+ case 11:
5644
+ return resolveColor("LITOR", ct);
5645
+ default:
5646
+ return resolveColor("LITYW", ct);
5647
+ }
5648
+ }
5649
+ function lightFillExpression(ct = "DAY") {
5650
+ return colourCodeCase(
5651
+ [
5652
+ ["3", resolveColor("LITRD", ct)],
5653
+ ["4", resolveColor("LITGN", ct)],
5654
+ ["6", resolveColor("LITYW", ct)],
5655
+ ["11", resolveColor("LITOR", ct)],
5656
+ ["5", resolveColor("LITBL", ct)],
5657
+ ["1", resolveColor("LITWT", ct)]
5658
+ ],
5659
+ resolveColor("LITYW", ct)
5660
+ );
5661
+ }
5662
+ function lateralColor(props, ct = "DAY") {
5663
+ const catlam = Number(props.CATLAM ?? props.catlam ?? 0);
5664
+ if (catlam === 1) return resolveColor("LITRD", ct);
5665
+ if (catlam === 2) return resolveColor("LITGN", ct);
5666
+ return resolveColor("LITYW", ct);
5667
+ }
5668
+ function lateralFillExpression(ct = "DAY") {
5669
+ return [
5670
+ "match",
5671
+ ["coalesce", ["get", "CATLAM"], ["get", "catlam"], 0],
5672
+ 1,
5673
+ resolveColor("LITRD", ct),
5674
+ 2,
5675
+ resolveColor("LITGN", ct),
5676
+ resolveColor("LITYW", ct)
5677
+ ];
5678
+ }
5679
+ function hazardColor(props, ct = "DAY") {
5680
+ const watlev = Number(props.WATLEV ?? props.watlev ?? 0);
5681
+ const valsou = props.VALSOU ?? props.valsou;
5682
+ if (watlev === 1 || watlev === 2) return resolveColor("DNGHL", ct);
5683
+ if (valsou != null && Number(valsou) < 2) return resolveColor("DNGHL", ct);
5684
+ return resolveColor("RESBL", ct);
5685
+ }
5686
+ function hazardFillExpression(ct = "DAY") {
5687
+ return [
5688
+ "case",
5689
+ [
5690
+ "any",
5691
+ ["==", ["coalesce", ["get", "WATLEV"], ["get", "watlev"], 0], 1],
5692
+ ["==", ["coalesce", ["get", "WATLEV"], ["get", "watlev"], 0], 2]
5693
+ ],
5694
+ resolveColor("DNGHL", ct),
5695
+ [
5696
+ "all",
5697
+ ["has", "VALSOU"],
5698
+ ["<", ["get", "VALSOU"], 2]
5699
+ ],
5700
+ resolveColor("DNGHL", ct),
5701
+ resolveColor("RESBL", ct)
5702
+ ];
5703
+ }
5704
+ function vegatnPatternExpression() {
5705
+ return [
5706
+ "match",
5707
+ ["coalesce", ["get", "CATVEG"], ["get", "catveg"], 0],
5708
+ 1,
5709
+ patternSpriteId("VEGATN03"),
5710
+ // grass
5711
+ 3,
5712
+ patternSpriteId("VEGATN03"),
5713
+ // bush
5714
+ 4,
5715
+ patternSpriteId("VEGATN04"),
5716
+ // deciduous wood
5717
+ 5,
5718
+ patternSpriteId("VEGATN04"),
5719
+ // coniferous wood
5720
+ 6,
5721
+ patternSpriteId("VEGATN04"),
5722
+ // wood (mixed)
5723
+ 7,
5724
+ patternSpriteId("MARSHES1"),
5725
+ // mangroves
5726
+ 10,
5727
+ patternSpriteId("VEGATN03"),
5728
+ // scrub
5729
+ 11,
5730
+ patternSpriteId("VEGATN04"),
5731
+ // tree
5732
+ 12,
5733
+ patternSpriteId("VEGATN04"),
5734
+ // evergreen
5735
+ 16,
5736
+ patternSpriteId("MARSHES1"),
5737
+ // swamp
5738
+ 17,
5739
+ patternSpriteId("MARSHES1"),
5740
+ // marsh
5741
+ 21,
5742
+ patternSpriteId("VEGATN04"),
5743
+ // savannah
5744
+ patternSpriteId("VEGATN03")
5745
+ // default: grass pattern
5746
+ ];
5747
+ }
5748
+ function daymarkColor(props, ct = "DAY") {
5749
+ const raw = props.COLOUR ?? props.colour;
5750
+ const code = Array.isArray(raw) ? Number(raw[0]) : typeof raw === "string" && raw.startsWith("[") ? Number(JSON.parse(raw)[0]) : Number(raw ?? 0);
5751
+ switch (code) {
5752
+ case 1:
5753
+ return resolveColor("LITWT", ct);
5754
+ case 3:
5755
+ return resolveColor("LITRD", ct);
5756
+ case 4:
5757
+ return resolveColor("LITGN", ct);
5758
+ case 6:
5759
+ return resolveColor("LITYW", ct);
5760
+ case 11:
5761
+ return resolveColor("LITOR", ct);
5762
+ default:
5763
+ return resolveColor("LITGN", ct);
5764
+ }
5765
+ }
5766
+
5270
5767
  // src/atlas/palette-light.ts
5271
5768
  var HEX6 = /^#?([0-9a-f]{6})$/i;
5272
5769
  function channels(hex) {
@@ -5305,8 +5802,966 @@ function capLuminance(hex, ceiling) {
5305
5802
  }
5306
5803
  var NIGHT_INK = S52_NIGHT.CHBLK;
5307
5804
  var NIGHT_MUTED_INK = scaleLinearLight(S52_NIGHT.CHBLK, 0.43);
5308
-
5309
- // src/atlas/map-live.ts
5805
+ function einkMonoGrey(hex) {
5806
+ const l = relativeLuminance(hex);
5807
+ const g2 = EINK_MONO_GREYS;
5808
+ const steps = [g2.INK, g2.DARK, g2.MID, g2.PAPER].map((c) => [c, relativeLuminance(c)]);
5809
+ let best = steps[0];
5810
+ for (const step of steps) if (Math.abs(step[1] - l) < Math.abs(best[1] - l)) best = step;
5811
+ return best[0];
5812
+ }
5813
+ function einkNightGrey(hex) {
5814
+ const l = relativeLuminance(hex);
5815
+ const g2 = EINK_MONO_GREYS;
5816
+ const steps = [g2.DARK, g2.MID, g2.PAPER].map((c) => [c, relativeLuminance(c)]);
5817
+ let best = steps[0];
5818
+ for (const step of steps) if (Math.abs(step[1] - l) < Math.abs(best[1] - l)) best = step;
5819
+ return best[0];
5820
+ }
5821
+
5822
+ // src/atlas/chrt-style.ts
5823
+ var CHRT_ICON_PREFIX = "chrt:";
5824
+ var CHRT_SDF_GLYPHS = /* @__PURE__ */ new Set([
5825
+ "buoy-can",
5826
+ "buoy-conical",
5827
+ "buoy-spherical",
5828
+ "buoy-pillar",
5829
+ "buoy-spar",
5830
+ "buoy-barrel",
5831
+ "buoy-super",
5832
+ "beacon-generic",
5833
+ "beacon-tower",
5834
+ "beacon-lattice",
5835
+ "beacon-stake",
5836
+ "beacon-cairn",
5837
+ "light-flare",
5838
+ "lightship",
5839
+ "daymark-triangle",
5840
+ "daymark-square"
5841
+ ]);
5842
+ var CHRT_GLYPH_NAMES = new Set(Object.keys(CHRT_GLYPH_SVGS));
5843
+ var CHRT_ATON_COLORS = {
5844
+ "1": "#F8FAFC",
5845
+ // white
5846
+ "2": "#101418",
5847
+ // black
5848
+ "3": "#E5484D",
5849
+ // red
5850
+ "4": "#30A46C",
5851
+ // green
5852
+ "5": "#3E63DD",
5853
+ // blue
5854
+ "6": "#FFC805",
5855
+ // yellow
5856
+ "7": "#8B9298",
5857
+ // grey
5858
+ "8": "#A66B3A",
5859
+ // brown
5860
+ "9": "#FFB224",
5861
+ // amber
5862
+ "10": "#7E4CCB",
5863
+ // violet
5864
+ "11": "#F76B15",
5865
+ // orange
5866
+ "12": "#D671C1"
5867
+ // pink (magenta-ish)
5868
+ };
5869
+ var CHRT_UNKNOWN_TINT = "#8B9298";
5870
+ var HOUSE_BLACK = "#101418";
5871
+ function chrtHouseColor(dayHex, palette = "DAY") {
5872
+ if (palette === "DAY" || palette === "EINK_COLOR") return dayHex;
5873
+ if (palette === "EINK_MONO") return einkMonoGrey(dayHex);
5874
+ if (palette === "EINK_NIGHT") return dayHex.toLowerCase() === HOUSE_BLACK.toLowerCase() || dayHex.toLowerCase() === "#000000" ? "#aaaaaa" : einkNightGrey(dayHex);
5875
+ const isBlack = dayHex.toLowerCase() === HOUSE_BLACK.toLowerCase();
5876
+ if (palette === "DUSK") {
5877
+ return isBlack ? dayHex : capLuminance(dayHex, relativeLuminance(resolveColor("CHBLK", "DUSK")));
5878
+ }
5879
+ return isBlack ? NIGHT_INK : capLuminance(dayHex, relativeLuminance(NIGHT_MUTED_INK));
5880
+ }
5881
+ function themeChrtGlyph(svg3, palette = "DAY") {
5882
+ if (palette === "DAY" || palette === "EINK_COLOR") return svg3;
5883
+ return svg3.replace(
5884
+ /(fill|stroke)="(#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6})"/g,
5885
+ (_m, attr, hex) => {
5886
+ const six = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
5887
+ return `${attr}="${chrtHouseColor(six, palette)}"`;
5888
+ }
5889
+ );
5890
+ }
5891
+ function chrtAtonTintExpression(palette = "DAY") {
5892
+ const c = (code) => chrtHouseColor(CHRT_ATON_COLORS[code], palette);
5893
+ return colourCodeCase(
5894
+ [
5895
+ ["3", c("3")],
5896
+ ["4", c("4")],
5897
+ ["6", c("6")],
5898
+ ["11", c("11")],
5899
+ ["9", c("9")],
5900
+ ["5", c("5")],
5901
+ ["1", c("1")],
5902
+ ["2", c("2")],
5903
+ ["8", c("8")],
5904
+ ["10", c("10")],
5905
+ ["12", c("12")],
5906
+ ["7", c("7")]
5907
+ ],
5908
+ chrtHouseColor(CHRT_UNKNOWN_TINT, palette)
5909
+ );
5910
+ }
5911
+ function chrtLightTintExpression(palette = "DAY") {
5912
+ const c = (code) => chrtHouseColor(CHRT_ATON_COLORS[code], palette);
5913
+ return colourCodeCase(
5914
+ [
5915
+ ["3", c("3")],
5916
+ ["4", c("4")],
5917
+ ["6", c("6")],
5918
+ ["11", c("11")],
5919
+ ["9", c("9")],
5920
+ ["5", c("5")],
5921
+ ["1", c("1")]
5922
+ ],
5923
+ c("1")
5924
+ );
5925
+ }
5926
+ function scalar(attr) {
5927
+ return ["coalesce", ["get", attr], ["get", attr.toLowerCase()], 0];
5928
+ }
5929
+ var g = (name) => `${CHRT_ICON_PREFIX}${name}`;
5930
+ function buoyBodyExpression() {
5931
+ return [
5932
+ "match",
5933
+ scalar("BOYSHP"),
5934
+ 1,
5935
+ g("buoy-conical"),
5936
+ 2,
5937
+ g("buoy-can"),
5938
+ 3,
5939
+ g("buoy-spherical"),
5940
+ 4,
5941
+ g("buoy-pillar"),
5942
+ 5,
5943
+ g("buoy-spar"),
5944
+ 6,
5945
+ g("buoy-barrel"),
5946
+ 7,
5947
+ g("buoy-super"),
5948
+ 8,
5949
+ g("buoy-super"),
5950
+ g("buoy-pillar")
5951
+ ];
5952
+ }
5953
+ function beaconBodyExpression() {
5954
+ return [
5955
+ "match",
5956
+ scalar("BCNSHP"),
5957
+ 1,
5958
+ g("beacon-stake"),
5959
+ 2,
5960
+ g("beacon-stake"),
5961
+ 3,
5962
+ g("beacon-tower"),
5963
+ 4,
5964
+ g("beacon-lattice"),
5965
+ 5,
5966
+ g("beacon-stake"),
5967
+ 6,
5968
+ g("beacon-cairn"),
5969
+ g("beacon-generic")
5970
+ ];
5971
+ }
5972
+ function cardinalExpression() {
5973
+ return [
5974
+ "match",
5975
+ scalar("CATCAM"),
5976
+ 1,
5977
+ g("cardinal-n"),
5978
+ 2,
5979
+ g("cardinal-e"),
5980
+ 3,
5981
+ g("cardinal-s"),
5982
+ 4,
5983
+ g("cardinal-w"),
5984
+ g("cardinal-n")
5985
+ ];
5986
+ }
5987
+ var CHRT_ICON_EXPRESSIONS = {
5988
+ BOYLAT: buoyBodyExpression,
5989
+ BOYSPP: buoyBodyExpression,
5990
+ BOYINB: buoyBodyExpression,
5991
+ BOYCAR: cardinalExpression,
5992
+ BOYISD: () => g("isolated-danger"),
5993
+ BOYSAW: () => g("safe-water"),
5994
+ BCNLAT: beaconBodyExpression,
5995
+ BCNSPP: beaconBodyExpression,
5996
+ BCNCAR: cardinalExpression,
5997
+ BCNISD: () => g("isolated-danger"),
5998
+ BCNSAW: () => g("safe-water"),
5999
+ LIGHTS: () => g("light-flare"),
6000
+ LITFLT: () => g("lightship"),
6001
+ LITVES: () => g("lightship"),
6002
+ FOGSIG: () => g("badge-sound"),
6003
+ RTPBCN: () => g("badge-radar"),
6004
+ RDOSTA: () => g("badge-radar"),
6005
+ DAYMAR: () => [
6006
+ "match",
6007
+ scalar("TOPSHP"),
6008
+ 1,
6009
+ g("daymark-triangle"),
6010
+ 2,
6011
+ g("daymark-triangle"),
6012
+ 5,
6013
+ g("daymark-triangle"),
6014
+ g("daymark-square")
6015
+ ]
6016
+ };
6017
+ var TINTED_CLASSES = /* @__PURE__ */ new Set([
6018
+ "BOYLAT",
6019
+ "BOYSPP",
6020
+ "BOYINB",
6021
+ "BCNLAT",
6022
+ "BCNSPP",
6023
+ "DAYMAR"
6024
+ ]);
6025
+ var CHRT_HALO = { color: HOUSE_BLACK, width: 1 };
6026
+ var CHRT_LINE_DASH = {
6027
+ CBLSUB: { color: "CHMGD", width: 0.8, dash: [6, 3] },
6028
+ PIPSOL: { color: "PIPLN", width: 0.8, dash: [6, 3] }
6029
+ };
6030
+ function applyChrtSymbolSet(style, palette = "DAY") {
6031
+ for (const layer of style.layers) {
6032
+ if (layer.type === "line") {
6033
+ const cls = layer.metadata?.["s52:objectClass"];
6034
+ const dash = cls ? CHRT_LINE_DASH[cls] : void 0;
6035
+ const paint2 = layer.paint ?? {};
6036
+ if (dash && paint2["line-pattern"] != null) {
6037
+ delete paint2["line-pattern"];
6038
+ paint2["line-color"] = resolveColor(dash.color, palette);
6039
+ paint2["line-width"] = dash.width;
6040
+ paint2["line-dasharray"] = dash.dash;
6041
+ layer.paint = paint2;
6042
+ }
6043
+ continue;
6044
+ }
6045
+ if (layer.type !== "symbol") continue;
6046
+ const objectClass2 = layer.metadata?.["s52:objectClass"];
6047
+ if (!objectClass2 || !CHRT_ICON_EXPRESSIONS[objectClass2]) continue;
6048
+ const layout = layer.layout;
6049
+ if (!layout || layout["icon-image"] == null) continue;
6050
+ layout["icon-image"] = CHRT_ICON_EXPRESSIONS[objectClass2]();
6051
+ const paint = layer.paint ?? {};
6052
+ const haloColor = palette === "EINK_MONO" ? chrtHouseColor(HOUSE_BLACK, palette) : palette === "EINK_NIGHT" ? "#000000" : CHRT_HALO.color;
6053
+ if (TINTED_CLASSES.has(objectClass2)) {
6054
+ paint["icon-color"] = chrtAtonTintExpression(palette);
6055
+ paint["icon-halo-color"] = haloColor;
6056
+ paint["icon-halo-width"] = CHRT_HALO.width;
6057
+ } else if (objectClass2 === "LIGHTS") {
6058
+ paint["icon-color"] = chrtLightTintExpression(palette);
6059
+ paint["icon-halo-color"] = haloColor;
6060
+ paint["icon-halo-width"] = CHRT_HALO.width;
6061
+ }
6062
+ layer.paint = paint;
6063
+ }
6064
+ const metadata = style.metadata;
6065
+ metadata["chrt:symbol-set"] = "chrt";
6066
+ return style;
6067
+ }
6068
+
6069
+ // src/atlas/base-layers.ts
6070
+ var NOCOVERAGE_PATTERN = "chrt:hatch-nocoverage";
6071
+ var UNCHARTED_PATTERN = "chrt:hatch-uncharted";
6072
+ function inlandWaterColor(palette) {
6073
+ return (BASE_THEMES[palette] ?? BASE_THEMES.DAY).inlandWater;
6074
+ }
6075
+ var BASE_THEMES = {
6076
+ DAY: {
6077
+ // ── `earth` is the jurisdiction tan DIMMED (~0.55), not a blue-grey ──
6078
+ // Outside the jurisdiction the map still owes the reader "what is land".
6079
+ // State 3 in the header is "the geography, under a crosshatch", and until
6080
+ // 2026-08-31 that geography was unreadable: earth #2A3439 against sea
6081
+ // #1B2328 measured 1.25:1, so Hispaniola showed charted boxes with tan
6082
+ // land inside and nothing recognisable around them (owner report: "no
6083
+ // land painting down near the DR at some zooms"). Land is warm and sea is
6084
+ // cool because that is what land and sea are; dimming the JURISDICTION
6085
+ // tan rather than inventing a colour says the true thing — this is the
6086
+ // same land, we simply do not carry charts for it. Held against BOTH
6087
+ // neighbours: legible over sea, and never mistakable for the full tan of
6088
+ // a country we do carry. DAY 3.59:1 vs sea, 3.17:1 vs tan.
6089
+ earth: "#807761",
6090
+ water: "#1B2328",
6091
+ river: "#24313A",
6092
+ boundary: "#4A575E",
6093
+ jurisdiction: "#D4C69D",
6094
+ // CHRTland's own tan — one land colour
6095
+ inlandWater: "#B9CFDD",
6096
+ // muted, unmistakably not a charted blue
6097
+ // The uncharted hatch's stripe, held to 2.3:1 against `inlandWater` in
6098
+ // every palette (`inland-hatch-contrast.test.ts`) — the hatch says "we have
6099
+ // not charted this water", and stripes the eye cannot find say nothing.
6100
+ // DAY 3.08:1, darker than its ground. Also the colour the artwork file
6101
+ // itself is drawn in.
6102
+ inlandHatch: "#5B7383",
6103
+ // State furniture. These three were referenced by the layers but missing
6104
+ // from this table (and its type — the long-standing TS2339 trio), so the
6105
+ // lines shipped on MapLibre's DEFAULT black. Now deliberate: a hairline
6106
+ // a shade deeper than the tan, quiet caps, tan halo.
6107
+ stateLine: "#B1A077",
6108
+ stateLabel: "#8E8468",
6109
+ stateHalo: "#D4C69D",
6110
+ // Rough-lake furniture (owner ask 2026-08-28: "the lakes need labels,
6111
+ // and shorelines drawn"): a shoreline a shade deeper than the rough
6112
+ // water, and a name in the water's own voice, haloed by the water.
6113
+ inlandShore: "#8FA9BA",
6114
+ inlandLabel: "#54707F",
6115
+ inlandHalo: "#B9CFDD"
6116
+ },
6117
+ DUSK: {
6118
+ // ── Re-derived 2026-09-01: the basemap sits UNDER the chart ──────────
6119
+ // These were the DAY tones scaled by a constant, never measured against
6120
+ // the DUSK chart — so `jurisdiction` came out at L=0.506 against a chart
6121
+ // whose own land (LANDA #8a7d5e) is L=0.209, i.e. the context layer was
6122
+ // 2.4x brighter than the data layer. Now capped at the chart's own land.
6123
+ // The rest of the block rides the same 0.666 channel factor so every
6124
+ // relationship the palette already chose is preserved, only moved down.
6125
+ // `water` is deliberately NOT scaled: it is the reference the land/sea
6126
+ // floor is measured against.
6127
+ earth: "#544E3E",
6128
+ water: "#161D21",
6129
+ river: "#141B20",
6130
+ boundary: "#293135",
6131
+ jurisdiction: "#867D64",
6132
+ inlandWater: "#62717A",
6133
+ // Owner's choice, 2026-10-04: dark stripes, 2.37:1. The DAY stripe here
6134
+ // was 1.02:1 — invisible, so an uncharted lake read as plain water.
6135
+ inlandHatch: "#2E383F",
6136
+ // Label stays DARK here — the halo is still mid-toned, so 3:1 is reachable
6137
+ // downward and a bright label would be a new light source at dusk.
6138
+ stateLine: "#B5A987",
6139
+ stateLabel: "#38342A",
6140
+ stateHalo: "#867D64",
6141
+ inlandShore: "#4D5B67",
6142
+ inlandLabel: "#212629",
6143
+ inlandHalo: "#62717A"
6144
+ },
6145
+ NIGHT: {
6146
+ // ── Re-derived 2026-09-01 — the owner-reported night defect ──────────
6147
+ // `jurisdiction` was L=0.240 against a chart whose own land (LANDA
6148
+ // #3a3020) is L=0.031: the world basemap was 7.7x BRIGHTER than the chart
6149
+ // it sits under, so at z<11 the uncharted interior read as a pale
6150
+ // continent-sized slab on a night chart. Capped at the chart's own land.
6151
+ //
6152
+ // ⚠ `earth` AND `jurisdiction` CONVERGE HERE, and that is forced, not
6153
+ // sloppy. Holding earth 2:1 above the ocean AND 2:1 below the jurisdiction
6154
+ // tan requires jurisdiction >= L 0.174 — 5.6x the chart's own land. At
6155
+ // night the range between the ocean (L 0.006) and the cap (L 0.031) is too
6156
+ // compressed to carry two land tones. COVERAGE IS STILL SAID, by the
6157
+ // `chrt:hatch-nocoverage` crosshatch, which is a stronger signal than tone
6158
+ // and is the one thing that cannot be lost to palette compression.
6159
+ //
6160
+ // Labels invert to light-on-dark: 3:1 downward is unreachable off a halo
6161
+ // this close to black, and CHRTland's own night town labels already sit at
6162
+ // L=0.177 for exactly this reason.
6163
+ earth: "#333026",
6164
+ water: "#10161A",
6165
+ river: "#090D0F",
6166
+ boundary: "#131719",
6167
+ jurisdiction: "#353128",
6168
+ inlandWater: "#232A30",
6169
+ // The DAY stripe, lighter than this ground: 2.92:1, and L 0.16, under the
6170
+ // NIGHT muted-ink ceiling (`night-luminance.test.ts`). Red night draws the
6171
+ // NIGHT chart, so this is its stripe too.
6172
+ inlandHatch: "#5B7383",
6173
+ stateLine: "#484236",
6174
+ stateLabel: "#837963",
6175
+ stateHalo: "#353128",
6176
+ inlandShore: "#1C2227",
6177
+ inlandLabel: "#617484",
6178
+ inlandHalo: "#232A30"
6179
+ },
6180
+ // ── E-ink: uncharted is DARK, never paper ─────────────────────────────
6181
+ // On an e-ink chart, charted safe water is paper. Water outside coverage
6182
+ // must never read as that, so it is the darkest fill on the page — the
6183
+ // e-ink twin of DAY's dark carto — with the no-coverage crosshatch over it
6184
+ // (invariant 5: the coverage portrayal survives). Land outside coverage
6185
+ // keeps the chart's own land tone.
6186
+ EINK_COLOR: {
6187
+ earth: "#d8ccb0",
6188
+ water: "#4f5a62",
6189
+ river: "#4f5a62",
6190
+ boundary: "#000000",
6191
+ jurisdiction: "#f2e2b0",
6192
+ inlandWater: "#c8d6e0",
6193
+ // The house stripe, 3.4:1 on this rough water — strong enough for the
6194
+ // 150 ppi colour layer.
6195
+ inlandHatch: "#5B7383",
6196
+ stateLine: "#555555",
6197
+ stateLabel: "#000000",
6198
+ stateHalo: "#ffffff",
6199
+ inlandShore: "#555555",
6200
+ inlandLabel: "#000000",
6201
+ inlandHalo: "#ffffff"
6202
+ },
6203
+ // Mono: the same rule in the four greys. Uncharted water DARK under a MID
6204
+ // crosshatch; land MID; rough inland water PAPER under the uncharted hatch.
6205
+ EINK_MONO: {
6206
+ earth: "#aaaaaa",
6207
+ water: "#555555",
6208
+ river: "#555555",
6209
+ boundary: "#000000",
6210
+ jurisdiction: "#aaaaaa",
6211
+ inlandWater: "#ffffff",
6212
+ inlandHatch: "#555555",
6213
+ stateLine: "#555555",
6214
+ stateLabel: "#000000",
6215
+ stateHalo: "#ffffff",
6216
+ inlandShore: "#555555",
6217
+ inlandLabel: "#000000",
6218
+ inlandHalo: "#ffffff"
6219
+ },
6220
+ // Night: the inverse. Water outside coverage is the black ground UNDER the
6221
+ // light crosshatch — charted safe water is plain black, so the hatch is what
6222
+ // tells them apart, here as everywhere. Land the DIM grey; labels the line
6223
+ // grey on black.
6224
+ EINK_NIGHT: {
6225
+ earth: "#555555",
6226
+ water: "#000000",
6227
+ river: "#000000",
6228
+ boundary: "#aaaaaa",
6229
+ jurisdiction: "#555555",
6230
+ inlandWater: "#000000",
6231
+ inlandHatch: "#aaaaaa",
6232
+ stateLine: "#aaaaaa",
6233
+ stateLabel: "#aaaaaa",
6234
+ stateHalo: "#000000",
6235
+ inlandShore: "#aaaaaa",
6236
+ inlandLabel: "#aaaaaa",
6237
+ inlandHalo: "#000000"
6238
+ }
6239
+ };
6240
+ function baseLayers(opts = {}) {
6241
+ const palette = opts.palette ?? "DAY";
6242
+ const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
6243
+ const src = opts.sourceId ?? BASE_REGION;
6244
+ const L = (o) => o;
6245
+ return [
6246
+ // 1. Geography — the world as it is, in the quiet dark voice. The OCEAN
6247
+ // is a background, not a polygon: the archive ships land and lakes,
6248
+ // and "everything else is sea" is exactly what a background expresses
6249
+ // — geometry-free, gap-free at every antimeridian and pole. Without
6250
+ // it the sea outside coverage was bare S-52 NODTA, a light grey that
6251
+ // read as "charted ground" with a hatch on it.
6252
+ L({
6253
+ id: "chrtbase-ocean",
6254
+ type: "background",
6255
+ paint: { "background-color": T.water }
6256
+ }),
6257
+ L({
6258
+ id: "chrtbase-earth",
6259
+ type: "fill",
6260
+ source: src,
6261
+ "source-layer": "base_earth",
6262
+ paint: { "fill-color": T.earth }
6263
+ }),
6264
+ L({
6265
+ id: "chrtbase-water",
6266
+ type: "fill",
6267
+ source: src,
6268
+ "source-layer": "base_water",
6269
+ paint: { "fill-color": T.water }
6270
+ }),
6271
+ L({
6272
+ id: "chrtbase-river",
6273
+ type: "line",
6274
+ source: src,
6275
+ "source-layer": "base_river",
6276
+ minzoom: 9,
6277
+ paint: {
6278
+ "line-color": T.river,
6279
+ "line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.6, 14, 1.6]
6280
+ }
6281
+ }),
6282
+ L({
6283
+ id: "chrtbase-boundary",
6284
+ type: "line",
6285
+ source: src,
6286
+ "source-layer": "base_boundary",
6287
+ paint: { "line-color": T.boundary, "line-width": 0.6, "line-opacity": 0.8 }
6288
+ }),
6289
+ // 2. THE DEFAULT: no coverage, everywhere, as a geometry-free background.
6290
+ // Constant opacity by design — see the header. What kills it is being
6291
+ // painted OVER, never fading.
6292
+ L({
6293
+ id: "chrtbase-nocoverage",
6294
+ type: "background",
6295
+ paint: { "background-pattern": NOCOVERAGE_PATTERN, "background-opacity": 0.55 }
6296
+ }),
6297
+ // 3. The jurisdiction: countries whose charts CHRT carries go tan — the
6298
+ // same tan as CHRTland, so "land in our world" is ONE colour. Opaque,
6299
+ // so it takes the crosshatch off its ground entirely.
6300
+ L({
6301
+ id: "chrtbase-jurisdiction",
6302
+ type: "fill",
6303
+ source: src,
6304
+ "source-layer": "jurisdiction",
6305
+ paint: { "fill-color": T.jurisdiction }
6306
+ }),
6307
+ // 4. States — internal boundaries and names, inside the jurisdiction
6308
+ // only (owner decision 2026-08-28). A hairline a shade deeper than the
6309
+ // tan, and quiet letter-spaced caps that fade as harbour detail takes
6310
+ // over. The one deliberate exception to no-names-on-the-basemap:
6311
+ // ADMIN furniture, nothing else — see the fence test.
6312
+ L({
6313
+ id: "chrtbase-state-boundary",
6314
+ type: "line",
6315
+ source: src,
6316
+ "source-layer": "state_boundary",
6317
+ minzoom: 4,
6318
+ paint: {
6319
+ "line-color": T.stateLine,
6320
+ "line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1],
6321
+ "line-dasharray": [3, 2],
6322
+ "line-opacity": 0.8
6323
+ }
6324
+ }),
6325
+ // 5. Country borders, drawn to be HOISTED above the whole chart by the
6326
+ // generator (owner, 2026-08-31: "where there is charted land, can we
6327
+ // add country borders on top of everything? I want it clear where
6328
+ // there is a boarder."). Sourced from NE boundary_lines_land — land
6329
+ // borders only, never coastline — so riding on top cannot stroke a
6330
+ // coast. Dashed so it reads as an administrative line, not chart
6331
+ // linework.
6332
+ L({
6333
+ id: "chrtbase-country-boundary",
6334
+ type: "line",
6335
+ source: src,
6336
+ "source-layer": "country_boundary",
6337
+ paint: {
6338
+ "line-color": T.boundary,
6339
+ "line-width": ["interpolate", ["linear"], ["zoom"], 3, 0.8, 8, 1.4, 12, 2.2],
6340
+ "line-dasharray": [4, 2],
6341
+ "line-opacity": 0.9
6342
+ }
6343
+ }),
6344
+ L({
6345
+ id: "chrtbase-state-label",
6346
+ type: "symbol",
6347
+ source: src,
6348
+ "source-layer": "state_label",
6349
+ minzoom: 4,
6350
+ maxzoom: 10,
6351
+ layout: {
6352
+ "text-field": ["upcase", ["get", "name"]],
6353
+ "text-font": ["Open Sans Regular"],
6354
+ "text-size": ["interpolate", ["linear"], ["zoom"], 4, 9, 8, 12],
6355
+ "text-letter-spacing": 0.25,
6356
+ "text-allow-overlap": false,
6357
+ "text-padding": 8
6358
+ },
6359
+ paint: {
6360
+ "text-color": T.stateLabel,
6361
+ "text-halo-color": T.stateHalo,
6362
+ "text-halo-width": 1.2,
6363
+ "text-opacity": ["interpolate", ["linear"], ["zoom"], 4, 0.7, 8, 0.85, 10, 0.4]
6364
+ }
6365
+ })
6366
+ ];
6367
+ }
6368
+ function baseInlandWaterLayers(opts = {}) {
6369
+ const palette = opts.palette ?? "DAY";
6370
+ const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
6371
+ const src = opts.sourceId ?? BASE_REGION;
6372
+ const L = (o) => o;
6373
+ const NE_TIER = ["!", ["has", "kind"]];
6374
+ const PLANET_TIER = ["has", "kind"];
6375
+ const HANDOVER = 9;
6376
+ return [
6377
+ // ── The fill pair, SPLIT BY TIER at the handover ────────────────────────
6378
+ //
6379
+ // Both tiers share `inland_water` (tile-join concatenates the same layer
6380
+ // name), and an unsplit fill draws BOTH — which means the NE polygon is
6381
+ // painted over the planet one at every zoom. That is not a cosmetic
6382
+ // double-draw: NE 10m carries no interior rings, so its blob has no
6383
+ // ISLANDS in it, and painting it over the planet's holed polygons buries
6384
+ // every island in the lake under "uncharted water". Owner-reported at
6385
+ // Winnipesaukee z11.7 ("there is land (and islands) being covered by the
6386
+ // 'no chart here' boundary"), and measured: rendering the planet tier
6387
+ // alone brings back the whole archipelago — Rattlesnake, Bear, Long
6388
+ // Island, Diamond, the Barndoors — while the NE tier alone is a faceted
6389
+ // blob whose edges bear no relation to the shoreline drawn beneath it.
6390
+ //
6391
+ // So each tier owns its own zoom range, the same handover the shoreline
6392
+ // below uses. Known residue, measured over the whole archive: of the 141
6393
+ // NE lakes we still carry, 138 have planet water beneath them; the
6394
+ // exceptions are two unnamed western-Alaska tundra lakes and a 2e-5 deg²
6395
+ // Lake Michigan sliver, which lose their rough fill above z9 because the
6396
+ // planet has no `water` polygon there at all (checked against the raw
6397
+ // pre-clip NDJSON — not a clip artefact).
6398
+ L({
6399
+ id: "chrtbase-inland-water",
6400
+ type: "fill",
6401
+ source: src,
6402
+ "source-layer": "inland_water",
6403
+ filter: NE_TIER,
6404
+ maxzoom: HANDOVER,
6405
+ paint: { "fill-color": T.inlandWater }
6406
+ }),
6407
+ L({
6408
+ id: "chrtbase-inland-hatch",
6409
+ type: "fill",
6410
+ source: src,
6411
+ "source-layer": "inland_water",
6412
+ filter: NE_TIER,
6413
+ maxzoom: HANDOVER,
6414
+ paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
6415
+ }),
6416
+ L({
6417
+ id: "chrtbase-inland-water-detail",
6418
+ type: "fill",
6419
+ source: src,
6420
+ "source-layer": "inland_water",
6421
+ filter: PLANET_TIER,
6422
+ minzoom: HANDOVER,
6423
+ paint: { "fill-color": T.inlandWater }
6424
+ }),
6425
+ L({
6426
+ id: "chrtbase-inland-hatch-detail",
6427
+ type: "fill",
6428
+ source: src,
6429
+ "source-layer": "inland_water",
6430
+ filter: PLANET_TIER,
6431
+ minzoom: HANDOVER,
6432
+ paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
6433
+ }),
6434
+ // The rough shoreline (owner ask 2026-08-28). Drawn ABOVE the hatch so
6435
+ // the edge stays crisp through the texture, and BELOW the charted-water
6436
+ // repaint clones the generator splices in after it — so wherever the
6437
+ // chart repaints its own water, the rough NE10m line yields to the
6438
+ // chart's COALNE instead of double-striking it.
6439
+ //
6440
+ // NE-tier geometry ONLY (`kind` absent): the planet detail tier joins
6441
+ // the same source-layer from z9 as MANY polygons whose chunk-clip seams
6442
+ // rejoin invisibly as FILLS — but a line layer strokes every seam, and
6443
+ // Winnipesaukee rendered as a mesh of internal edges. Planet features
6444
+ // carry `kind` (the INLAND_KINDS filter's own field); NE's carry only
6445
+ // `name`, so the absence of `kind` IS the tier discriminator.
6446
+ L({
6447
+ id: "chrtbase-inland-shore",
6448
+ type: "line",
6449
+ source: src,
6450
+ "source-layer": "inland_water",
6451
+ filter: NE_TIER,
6452
+ // Hands the shoreline over to the planet tier at ITS floor. Both
6453
+ // tiers live in `inland_water` from z9 up (tile-join concatenates the
6454
+ // same layer name), and their FILLS merge invisibly — but two
6455
+ // outlines do not: NE 10m generalises Winnipesaukee's shape, so its
6456
+ // outline ran straight across the middle of the lake the planet had
6457
+ // drawn correctly around. The coarse line is right only while it is
6458
+ // the only shape we have.
6459
+ maxzoom: HANDOVER,
6460
+ paint: {
6461
+ "line-color": T.inlandShore,
6462
+ "line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1.1, 14, 1.6],
6463
+ "line-opacity": 0.9
6464
+ }
6465
+ }),
6466
+ // The PLANET tier's shoreline (owner ask 2026-08-31: "uncharted small
6467
+ // lakes render with no outline"). It reads its own `inland_shore` layer,
6468
+ // NOT `inland_water`, and that indirection is the whole fix: the mesh
6469
+ // that forced the `!has kind` fence above is a CLIP artefact — an MVT
6470
+ // feature arrives already cut to its own z10 tile, so stroking the
6471
+ // polygon strokes every tile boundary it crosses. The build now walks
6472
+ // those rings, drops the axis-aligned runs that sit on the read zoom's
6473
+ // tile grid, and emits what is left as lines (measured on one New
6474
+ // England chunk: 61 long axis-aligned mesh edges → 5, and the 5 survivors
6475
+ // are 0.02–0.5 tiles off the grid, i.e. real straight water edges).
6476
+ //
6477
+ // Thinner and quieter than the NE line: this is finer geometry describing
6478
+ // water nobody has surveyed, and approximate data should look
6479
+ // approximate.
6480
+ L({
6481
+ id: "chrtbase-inland-shore-detail",
6482
+ type: "line",
6483
+ source: src,
6484
+ "source-layer": "inland_shore",
6485
+ minzoom: HANDOVER,
6486
+ paint: {
6487
+ "line-color": T.inlandShore,
6488
+ "line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.5, 12, 0.9, 15, 1.2],
6489
+ "line-opacity": 0.85
6490
+ }
6491
+ }),
6492
+ // The lake's name (owner ask 2026-08-28) — the second scoped exception
6493
+ // to no-names-on-the-basemap, after the states: WATER-BODY names only.
6494
+ // No maxzoom: an uncharted lake keeps its name at every zoom; over
6495
+ // charted lakes the chart's own SEAARE label takes over at detail zooms
6496
+ // and collision culls the echo.
6497
+ //
6498
+ // The archive's `inland_water_label` anchors (v3+). v3 shipped ONE
6499
+ // PointOnSurface anchor per lake, and a big lake's single anchor pans
6500
+ // off-viewport — Winnipesaukee at z11.7 read nameless with its name two
6501
+ // tiles west of the screen (owner-reported). Labelling the polygons
6502
+ // instead is NOT the fix: tippecanoe's coalescing drops the NE polygon
6503
+ // from some z11 tiles (measured: 11/619/747 carries only planet pieces),
6504
+ // so a polygon-sourced label vanishes tile by tile. v4 therefore bakes a
6505
+ // deterministic anchor GRID into big lakes — the paper-chart convention
6506
+ // of repeating a name along a large water body — and this one layer
6507
+ // renders however many anchors are in view; collision culls the rest.
6508
+ // Planet-tier water-body NAMES (owner ask 2026-08-31: NE 10m names only
6509
+ // 113 US lakes). Emitted BELOW the NE layer on purpose — MapLibre places
6510
+ // symbols topmost-layer-first, so the big NE lake wins collision against
6511
+ // the pond beside it, and both lose to the chart's own labels (every ENC
6512
+ // class is emitted after this pair).
6513
+ //
6514
+ // The size threshold is OSM's own `min_zoom`, carried through the build:
6515
+ // Lake Winnipesaukee reads 10, Squam Lake 12, so a pond simply is not in
6516
+ // the filter until the user is close enough for it to be the answer to
6517
+ // "what is that water". `["zoom"]` in a filter evaluates at integer zoom
6518
+ // levels and removes the feature from placement entirely — verified
6519
+ // against maplibre-gl-native rather than assumed, because a text-size-0
6520
+ // gate would still claim a collision slot.
6521
+ //
6522
+ // The tiers are separate LAYERS, not a filtered split of one — see the
6523
+ // builder for why: an archive that a not-yet-deployed style would
6524
+ // mis-render cannot be promoted first, and this one has to be.
6525
+ L({
6526
+ id: "chrtbase-inland-label-detail",
6527
+ type: "symbol",
6528
+ source: src,
6529
+ "source-layer": "inland_water_label_detail",
6530
+ minzoom: HANDOVER,
6531
+ filter: [">=", ["zoom"], ["get", "min_zoom"]],
6532
+ layout: {
6533
+ "text-field": ["get", "name"],
6534
+ "text-font": ["Open Sans Regular"],
6535
+ "text-size": ["interpolate", ["linear"], ["zoom"], 9, 9, 12, 10.5, 15, 11],
6536
+ "text-max-width": 8,
6537
+ "text-allow-overlap": false,
6538
+ "text-padding": 6,
6539
+ // Bigger water first within the tier — min_zoom IS the size rank.
6540
+ "symbol-sort-key": ["get", "min_zoom"]
6541
+ },
6542
+ paint: {
6543
+ "text-color": T.inlandLabel,
6544
+ "text-halo-color": T.inlandHalo,
6545
+ "text-halo-width": 1,
6546
+ "text-opacity": 0.9
6547
+ }
6548
+ }),
6549
+ L({
6550
+ id: "chrtbase-inland-label",
6551
+ type: "symbol",
6552
+ source: src,
6553
+ "source-layer": "inland_water_label",
6554
+ minzoom: 5,
6555
+ layout: {
6556
+ "text-field": ["get", "name"],
6557
+ "text-font": ["Open Sans Regular"],
6558
+ "text-size": ["interpolate", ["linear"], ["zoom"], 5, 9.5, 10, 12],
6559
+ "text-max-width": 7,
6560
+ "text-allow-overlap": false,
6561
+ // Zoom-scaled spacing for the anchor GRID: at z8.9 the 0.08-deg
6562
+ // anchors sit ~54 px apart and a flat padding let six copies of
6563
+ // "Lake Ontario" crowd one screen (owner-reported 2026-08-30).
6564
+ // Wide collision boxes at low zoom thin the grid to the repeat-
6565
+ // by-convention cadence; by z12 the anchors are ~850 px apart and
6566
+ // the padding relaxes to normal.
6567
+ "text-padding": ["interpolate", ["linear"], ["zoom"], 5, 80, 9, 64, 12, 16]
6568
+ },
6569
+ paint: {
6570
+ "text-color": T.inlandLabel,
6571
+ "text-halo-color": T.inlandHalo,
6572
+ "text-halo-width": 1.1
6573
+ }
6574
+ })
6575
+ ];
6576
+ }
6577
+
6578
+ // src/atlas/eink-portrayal.ts
6579
+ var EINK_DOTS_PATTERN = `${CHRT_ICON_PREFIX}eink-dots`;
6580
+ var EINK_HATCH_PATTERN = `${CHRT_ICON_PREFIX}eink-hatch`;
6581
+ var EINK_DANGER_PATTERN = `${CHRT_ICON_PREFIX}eink-danger`;
6582
+ var EINK_LINE_SCALE = 1.5;
6583
+ var EINK_LINE_MIN_PX = 1;
6584
+ var EINK_TEXT_SCALE = 1.15;
6585
+ var EINK_HALO_MIN_PX = 1.5;
6586
+ var EINK_DANGER_ICON_SCALE = 1.35;
6587
+ var EINK_WASH_CUTOFF = 0.3;
6588
+ var EINK_SAFETY_CONTOUR_WIDTH = [2, 3.5];
6589
+ var EINK_DANGER_CLASSES = /* @__PURE__ */ new Set(["OBSTRN", "WRECKS", "UWTROC"]);
6590
+ var METRIC_CONTOUR_LADDER_M = [0, 2, 5, 10, 15, 20, 30, 50, 100, 200, 500, 1e3];
6591
+ var IMPERIAL_CONTOUR_LADDER_M = [
6592
+ 1.8,
6593
+ 3.6,
6594
+ 5.4,
6595
+ 9.1,
6596
+ 18.2,
6597
+ 27.4,
6598
+ 36.5,
6599
+ 54.8,
6600
+ 91.4,
6601
+ 182.8,
6602
+ 365.7,
6603
+ 914.4
6604
+ ];
6605
+ function snapToLadder(safetyM, ladder) {
6606
+ for (const rung of ladder) if (rung >= safetyM - 1e-6) return rung;
6607
+ return null;
6608
+ }
6609
+ function safetyContourValues(safetyContourM) {
6610
+ const s = effectiveSafetyContour(safetyContourM);
6611
+ const out = [snapToLadder(s, METRIC_CONTOUR_LADDER_M), snapToLadder(s, IMPERIAL_CONTOUR_LADDER_M)];
6612
+ return [...new Set(out.filter((v) => v !== null))];
6613
+ }
6614
+ function isExpr(v) {
6615
+ return Array.isArray(v) && typeof v[0] === "string";
6616
+ }
6617
+ function scaleNumeric(v, k, min = 0) {
6618
+ if (typeof v === "number") return Math.max(v * k, v > 0 ? min : 0);
6619
+ if (!isExpr(v)) return v;
6620
+ const op = v[0];
6621
+ if (op === "interpolate" || op === "interpolate-hcl" || op === "interpolate-lab") {
6622
+ const out = v.slice(0, 3);
6623
+ for (let i = 3; i < v.length; i += 2) out.push(v[i], scaleNumeric(v[i + 1], k, min));
6624
+ return out;
6625
+ }
6626
+ if (op === "step") {
6627
+ const out = [op, v[1], scaleNumeric(v[2], k, min)];
6628
+ for (let i = 3; i < v.length; i += 2) out.push(v[i], scaleNumeric(v[i + 1], k, min));
6629
+ return out;
6630
+ }
6631
+ if (op === "literal") return v;
6632
+ return ["max", ["*", v, k], min];
6633
+ }
6634
+ function objectClass(layer) {
6635
+ const md = layer.metadata;
6636
+ const c = md?.["s52:objectClass"];
6637
+ return typeof c === "string" ? c : null;
6638
+ }
6639
+ function scaffoldId(prefix, baseId) {
6640
+ return `${prefix}${baseId.replace(/-band(\d)/g, "-b$1")}`;
6641
+ }
6642
+ var DRVAL1 = ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1];
6643
+ var DRVAL2 = ["coalesce", ["get", "DRVAL2"], ["get", "drval2"], 0];
6644
+ var VALDCO = ["to-number", ["coalesce", ["get", "VALDCO"], ["get", "valdco"], -1], -1];
6645
+ function withFilter(base, extra) {
6646
+ return base === void 0 ? extra : ["all", base, extra];
6647
+ }
6648
+ function applyEinkPortrayal(style, palette, opts = {}) {
6649
+ if (palette !== "EINK_COLOR" && palette !== "EINK_MONO" && palette !== "EINK_NIGHT") return style;
6650
+ const mono = isEinkGreyTable(palette);
6651
+ const night = palette === "EINK_NIGHT";
6652
+ const safetyInk = resolveColor("DEPSC", palette);
6653
+ const groundInk = resolveColor("UIBCK", palette);
6654
+ const safety = effectiveSafetyContour(opts.safetyContour ?? DEFAULT_SAFETY_CONTOUR_M);
6655
+ const safetyValues = safetyContourValues(safety);
6656
+ const out = [];
6657
+ for (const original of style.layers) {
6658
+ const id = original.id;
6659
+ const type = original.type;
6660
+ if (type === "hillshade" || id.startsWith("chrtland-fade-")) continue;
6661
+ const layer = structuredClone(original);
6662
+ const paint = layer.paint ??= {};
6663
+ const layout = layer.layout;
6664
+ const cls = objectClass(layer);
6665
+ if (type === "line") {
6666
+ if (paint["line-width"] !== void 0) {
6667
+ paint["line-width"] = scaleNumeric(paint["line-width"], EINK_LINE_SCALE, EINK_LINE_MIN_PX);
6668
+ } else {
6669
+ paint["line-width"] = EINK_LINE_SCALE;
6670
+ }
6671
+ const o = paint["line-opacity"];
6672
+ if (typeof o === "number" && o > 0 && o < 1) paint["line-opacity"] = 1;
6673
+ if (paint["line-blur"] !== void 0) paint["line-blur"] = 0;
6674
+ }
6675
+ if (type === "fill") {
6676
+ const o = paint["fill-opacity"];
6677
+ if (typeof o === "number" && o > 0 && o < 1) {
6678
+ if (cls && EINK_DANGER_CLASSES.has(cls)) {
6679
+ paint["fill-pattern"] = EINK_DANGER_PATTERN;
6680
+ paint["fill-opacity"] = 1;
6681
+ } else {
6682
+ paint["fill-opacity"] = o < EINK_WASH_CUTOFF ? 0 : 1;
6683
+ }
6684
+ }
6685
+ if (paint["fill-antialias"] === void 0) paint["fill-antialias"] = true;
6686
+ }
6687
+ if (type === "background" && night && id === "chrt-background") {
6688
+ paint["background-pattern"] = NOCOVERAGE_PATTERN;
6689
+ }
6690
+ if (type === "background") {
6691
+ const o = paint["background-opacity"];
6692
+ if (typeof o === "number" && o > 0 && o < 1) paint["background-opacity"] = 1;
6693
+ }
6694
+ if (type === "circle") {
6695
+ const blur = paint["circle-blur"];
6696
+ if (typeof blur === "number" && blur >= 0.5) {
6697
+ paint["circle-opacity"] = 0;
6698
+ } else {
6699
+ if (blur !== void 0) paint["circle-blur"] = 0;
6700
+ const o = paint["circle-opacity"];
6701
+ if (typeof o === "number" && o > 0 && o < 1) paint["circle-opacity"] = 1;
6702
+ }
6703
+ }
6704
+ if (type === "symbol") {
6705
+ if (layout?.["text-field"] !== void 0) {
6706
+ if (layout["text-size"] !== void 0) {
6707
+ layout["text-size"] = scaleNumeric(layout["text-size"], EINK_TEXT_SCALE);
6708
+ }
6709
+ if (paint["text-opacity"] !== void 0) paint["text-opacity"] = 1;
6710
+ if (typeof paint["text-halo-color"] === "string") paint["text-halo-color"] = groundInk;
6711
+ const hw = paint["text-halo-width"];
6712
+ paint["text-halo-width"] = typeof hw === "number" ? Math.max(hw, EINK_HALO_MIN_PX) : hw ?? EINK_HALO_MIN_PX;
6713
+ if (paint["text-halo-blur"] !== void 0) paint["text-halo-blur"] = 0;
6714
+ }
6715
+ const io = paint["icon-opacity"];
6716
+ if (typeof io === "number" && io > 0 && io < 1) paint["icon-opacity"] = 1;
6717
+ if (cls && EINK_DANGER_CLASSES.has(cls) && layout?.["icon-image"] !== void 0) {
6718
+ layout["icon-size"] = scaleNumeric(layout["icon-size"] ?? 1, EINK_DANGER_ICON_SCALE);
6719
+ }
6720
+ }
6721
+ out.push(layer);
6722
+ if (type === "fill" && cls === "DEPARE" && mono) {
6723
+ const drying = ["all", ["<", DRVAL1, 0], ["<=", DRVAL2, 0]];
6724
+ out.push({
6725
+ ...structuredClone(original),
6726
+ id: scaffoldId("chrt-eink-depth-of-", id),
6727
+ filter: withFilter(original.filter, ["<", DRVAL1, safety]),
6728
+ paint: {
6729
+ "fill-pattern": ["case", drying, EINK_HATCH_PATTERN, EINK_DOTS_PATTERN]
6730
+ }
6731
+ });
6732
+ }
6733
+ if (type === "line" && cls === "DEPCNT") {
6734
+ const isSafety = ["any", ...safetyValues.map((v) => ["<", ["abs", ["-", VALDCO, v]], 0.05])];
6735
+ out.push({
6736
+ ...structuredClone(original),
6737
+ id: scaffoldId("chrt-eink-safety-of-", id),
6738
+ filter: withFilter(original.filter, isSafety),
6739
+ paint: {
6740
+ "line-color": safetyInk,
6741
+ "line-width": [
6742
+ "interpolate",
6743
+ ["linear"],
6744
+ ["zoom"],
6745
+ 8,
6746
+ EINK_SAFETY_CONTOUR_WIDTH[0],
6747
+ 14,
6748
+ EINK_SAFETY_CONTOUR_WIDTH[1]
6749
+ ]
6750
+ },
6751
+ layout: { "line-join": "round", "line-cap": "round" }
6752
+ });
6753
+ }
6754
+ }
6755
+ return {
6756
+ ...style,
6757
+ // No transitions: a property change is one repaint, never an animation.
6758
+ ...{ transition: { duration: 0, delay: 0 } },
6759
+ metadata: { ...style.metadata, "chrt:eink": night ? "night" : mono ? "mono" : "color" },
6760
+ layers: out
6761
+ };
6762
+ }
6763
+
6764
+ // src/atlas/map-live.ts
5310
6765
  function isMapLive(map) {
5311
6766
  return map._removed !== true;
5312
6767
  }
@@ -5941,248 +7396,33 @@ function lineSpriteId(lineStyleName) {
5941
7396
  }
5942
7397
  function lineStylePaint(name) {
5943
7398
  const meta = S52_LINESTYLES[name];
5944
- if (!meta) {
5945
- throw new Error(
5946
- `Unknown S-52 linestyle "${name}". Known: ${Object.keys(S52_LINESTYLES).length} styles (regenerate with: bun run scripts/extract-opencpn-linestyles.ts)`
5947
- );
5948
- }
5949
- return {
5950
- "line-pattern": lineSpriteId(meta.name),
5951
- "line-width": houseWidth(meta)
5952
- };
5953
- }
5954
- function lineStylePaintCase(cases, fallback) {
5955
- const resolve = (name) => {
5956
- const meta = S52_LINESTYLES[name];
5957
- if (!meta) throw new Error(`Unknown S-52 linestyle "${name}"`);
5958
- return meta;
5959
- };
5960
- const fallbackMeta = resolve(fallback);
5961
- const pattern = ["case"];
5962
- const width = ["case"];
5963
- for (const [condition, name] of cases) {
5964
- const meta = resolve(name);
5965
- pattern.push(condition, lineSpriteId(meta.name));
5966
- width.push(condition, houseWidth(meta));
5967
- }
5968
- pattern.push(lineSpriteId(fallbackMeta.name));
5969
- width.push(houseWidth(fallbackMeta));
5970
- return { "line-pattern": pattern, "line-width": width };
5971
- }
5972
-
5973
- // src/atlas/s52-pattern.ts
5974
- var S52_PATTERN_SPRITE_PREFIX = "AP:";
5975
- function patternSpriteId(patternName) {
5976
- return `${S52_PATTERN_SPRITE_PREFIX}${patternName}`;
5977
- }
5978
-
5979
- // src/atlas/s52-conditional.ts
5980
- var DEFAULT_SAFETY_CONTOUR_M = 10;
5981
- var DEPARE_DEEP_CUT_M = 18.2;
5982
- var DEPARE_SHALLOW_CUT_M = 2;
5983
- function effectiveSafetyContour(safetyContourM) {
5984
- if (!Number.isFinite(safetyContourM)) return DEFAULT_SAFETY_CONTOUR_M;
5985
- return Math.min(Math.max(safetyContourM, DEPARE_SHALLOW_CUT_M), DEPARE_DEEP_CUT_M);
5986
- }
5987
- function depareColor(props, ct = "DAY", safetyContourM = DEFAULT_SAFETY_CONTOUR_M) {
5988
- const drval1 = Number(props.DRVAL1 ?? props.drval1 ?? -1);
5989
- const drval2 = Number(props.DRVAL2 ?? props.drval2 ?? drval1);
5990
- const safety = effectiveSafetyContour(safetyContourM);
5991
- if (drval1 < 0 && drval2 <= 0) return resolveColor("DEPIT", ct);
5992
- if (drval1 < DEPARE_SHALLOW_CUT_M) return resolveColor("DEPVS", ct);
5993
- if (drval1 < safety) return resolveColor("DEPMS", ct);
5994
- if (drval1 < DEPARE_DEEP_CUT_M) return resolveColor("DEPMD", ct);
5995
- return resolveColor("DEPDW", ct);
5996
- }
5997
- var DEPARE_FILL_BRANCH_TOKENS = ["DEPIT", "DEPVS", "DEPMS", "DEPMD"];
5998
- var DEPARE_FILL_FALLBACK_TOKEN = "DEPDW";
5999
- function depareFillExpression(ct = "DAY", safetyContourM = DEFAULT_SAFETY_CONTOUR_M) {
6000
- const safety = effectiveSafetyContour(safetyContourM);
6001
- const [it, vs, ms, md] = DEPARE_FILL_BRANCH_TOKENS;
6002
- return [
6003
- "case",
6004
- [
6005
- "all",
6006
- ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], 0],
6007
- ["<=", ["coalesce", ["get", "DRVAL2"], ["get", "drval2"], 0], 0]
6008
- ],
6009
- resolveColor(it, ct),
6010
- ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], DEPARE_SHALLOW_CUT_M],
6011
- resolveColor(vs, ct),
6012
- ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], safety],
6013
- resolveColor(ms, ct),
6014
- ["<", ["coalesce", ["get", "DRVAL1"], ["get", "drval1"], -1], DEPARE_DEEP_CUT_M],
6015
- resolveColor(md, ct),
6016
- resolveColor(DEPARE_FILL_FALLBACK_TOKEN, ct)
6017
- ];
6018
- }
6019
- function listAttrString(attr) {
6020
- return ["to-string", ["coalesce", ["get", attr], ["get", attr.toLowerCase()], ""]];
6021
- }
6022
- function listAttrCase(attr, cases, fallback) {
6023
- const attrStr = listAttrString(attr);
6024
- const expr = ["case"];
6025
- for (const [code, value] of cases) {
6026
- expr.push([">=", ["index-of", `"${code}"`, attrStr], 0], value);
6027
- }
6028
- for (const [code, value] of cases) {
6029
- expr.push(["==", attrStr, code], value);
6030
- }
6031
- for (const [code, value] of cases) {
6032
- expr.push(["==", attrStr, `[${code}]`], value);
6033
- }
6034
- expr.push(fallback);
6035
- return expr;
6036
- }
6037
- function listAttrHas(attr, codes) {
6038
- const attrStr = listAttrString(attr);
6039
- const clauses = [];
6040
- for (const code of codes) {
6041
- clauses.push([">=", ["index-of", `"${code}"`, attrStr], 0]);
6042
- clauses.push(["==", attrStr, code]);
6043
- clauses.push(["==", attrStr, `[${code}]`]);
6044
- }
6045
- return ["any", ...clauses];
6046
- }
6047
- function colourCodeCase(cases, fallback) {
6048
- return listAttrCase("COLOUR", cases, fallback);
6049
- }
6050
- function lightColor(props, ct = "DAY") {
6051
- const raw = props.COLOUR ?? props.colour;
6052
- const code = Array.isArray(raw) ? Number(raw[0]) : typeof raw === "string" && raw.startsWith("[") ? Number(JSON.parse(raw)[0]) : Number(raw ?? 0);
6053
- switch (code) {
6054
- case 1:
6055
- return resolveColor("LITWT", ct);
6056
- case 3:
6057
- return resolveColor("LITRD", ct);
6058
- case 4:
6059
- return resolveColor("LITGN", ct);
6060
- case 5:
6061
- return resolveColor("LITBL", ct);
6062
- case 6:
6063
- return resolveColor("LITYW", ct);
6064
- case 11:
6065
- return resolveColor("LITOR", ct);
6066
- default:
6067
- return resolveColor("LITYW", ct);
6068
- }
6069
- }
6070
- function lightFillExpression(ct = "DAY") {
6071
- return colourCodeCase(
6072
- [
6073
- ["3", resolveColor("LITRD", ct)],
6074
- ["4", resolveColor("LITGN", ct)],
6075
- ["6", resolveColor("LITYW", ct)],
6076
- ["11", resolveColor("LITOR", ct)],
6077
- ["5", resolveColor("LITBL", ct)],
6078
- ["1", resolveColor("LITWT", ct)]
6079
- ],
6080
- resolveColor("LITYW", ct)
6081
- );
6082
- }
6083
- function lateralColor(props, ct = "DAY") {
6084
- const catlam = Number(props.CATLAM ?? props.catlam ?? 0);
6085
- if (catlam === 1) return resolveColor("LITRD", ct);
6086
- if (catlam === 2) return resolveColor("LITGN", ct);
6087
- return resolveColor("LITYW", ct);
6088
- }
6089
- function lateralFillExpression(ct = "DAY") {
6090
- return [
6091
- "match",
6092
- ["coalesce", ["get", "CATLAM"], ["get", "catlam"], 0],
6093
- 1,
6094
- resolveColor("LITRD", ct),
6095
- 2,
6096
- resolveColor("LITGN", ct),
6097
- resolveColor("LITYW", ct)
6098
- ];
6099
- }
6100
- function hazardColor(props, ct = "DAY") {
6101
- const watlev = Number(props.WATLEV ?? props.watlev ?? 0);
6102
- const valsou = props.VALSOU ?? props.valsou;
6103
- if (watlev === 1 || watlev === 2) return resolveColor("DNGHL", ct);
6104
- if (valsou != null && Number(valsou) < 2) return resolveColor("DNGHL", ct);
6105
- return resolveColor("RESBL", ct);
6106
- }
6107
- function hazardFillExpression(ct = "DAY") {
6108
- return [
6109
- "case",
6110
- [
6111
- "any",
6112
- ["==", ["coalesce", ["get", "WATLEV"], ["get", "watlev"], 0], 1],
6113
- ["==", ["coalesce", ["get", "WATLEV"], ["get", "watlev"], 0], 2]
6114
- ],
6115
- resolveColor("DNGHL", ct),
6116
- [
6117
- "all",
6118
- ["has", "VALSOU"],
6119
- ["<", ["get", "VALSOU"], 2]
6120
- ],
6121
- resolveColor("DNGHL", ct),
6122
- resolveColor("RESBL", ct)
6123
- ];
6124
- }
6125
- function vegatnPatternExpression() {
6126
- return [
6127
- "match",
6128
- ["coalesce", ["get", "CATVEG"], ["get", "catveg"], 0],
6129
- 1,
6130
- patternSpriteId("VEGATN03"),
6131
- // grass
6132
- 3,
6133
- patternSpriteId("VEGATN03"),
6134
- // bush
6135
- 4,
6136
- patternSpriteId("VEGATN04"),
6137
- // deciduous wood
6138
- 5,
6139
- patternSpriteId("VEGATN04"),
6140
- // coniferous wood
6141
- 6,
6142
- patternSpriteId("VEGATN04"),
6143
- // wood (mixed)
6144
- 7,
6145
- patternSpriteId("MARSHES1"),
6146
- // mangroves
6147
- 10,
6148
- patternSpriteId("VEGATN03"),
6149
- // scrub
6150
- 11,
6151
- patternSpriteId("VEGATN04"),
6152
- // tree
6153
- 12,
6154
- patternSpriteId("VEGATN04"),
6155
- // evergreen
6156
- 16,
6157
- patternSpriteId("MARSHES1"),
6158
- // swamp
6159
- 17,
6160
- patternSpriteId("MARSHES1"),
6161
- // marsh
6162
- 21,
6163
- patternSpriteId("VEGATN04"),
6164
- // savannah
6165
- patternSpriteId("VEGATN03")
6166
- // default: grass pattern
6167
- ];
6168
- }
6169
- function daymarkColor(props, ct = "DAY") {
6170
- const raw = props.COLOUR ?? props.colour;
6171
- const code = Array.isArray(raw) ? Number(raw[0]) : typeof raw === "string" && raw.startsWith("[") ? Number(JSON.parse(raw)[0]) : Number(raw ?? 0);
6172
- switch (code) {
6173
- case 1:
6174
- return resolveColor("LITWT", ct);
6175
- case 3:
6176
- return resolveColor("LITRD", ct);
6177
- case 4:
6178
- return resolveColor("LITGN", ct);
6179
- case 6:
6180
- return resolveColor("LITYW", ct);
6181
- case 11:
6182
- return resolveColor("LITOR", ct);
6183
- default:
6184
- return resolveColor("LITGN", ct);
7399
+ if (!meta) {
7400
+ throw new Error(
7401
+ `Unknown S-52 linestyle "${name}". Known: ${Object.keys(S52_LINESTYLES).length} styles (regenerate with: bun run scripts/extract-opencpn-linestyles.ts)`
7402
+ );
7403
+ }
7404
+ return {
7405
+ "line-pattern": lineSpriteId(meta.name),
7406
+ "line-width": houseWidth(meta)
7407
+ };
7408
+ }
7409
+ function lineStylePaintCase(cases, fallback) {
7410
+ const resolve = (name) => {
7411
+ const meta = S52_LINESTYLES[name];
7412
+ if (!meta) throw new Error(`Unknown S-52 linestyle "${name}"`);
7413
+ return meta;
7414
+ };
7415
+ const fallbackMeta = resolve(fallback);
7416
+ const pattern = ["case"];
7417
+ const width = ["case"];
7418
+ for (const [condition, name] of cases) {
7419
+ const meta = resolve(name);
7420
+ pattern.push(condition, lineSpriteId(meta.name));
7421
+ width.push(condition, houseWidth(meta));
6185
7422
  }
7423
+ pattern.push(lineSpriteId(fallbackMeta.name));
7424
+ width.push(houseWidth(fallbackMeta));
7425
+ return { "line-pattern": pattern, "line-width": width };
6186
7426
  }
6187
7427
 
6188
7428
  // src/atlas/s52-lookup.ts
@@ -20075,9 +21315,9 @@ function compileAtonExpression(lookup, mapSymbol = (name) => name) {
20075
21315
  expr.push(fallback);
20076
21316
  return expr;
20077
21317
  }
20078
- function atonIconExpression(objectClass) {
20079
- const lookup = S52_ATON_LOOKUPS[objectClass];
20080
- if (!lookup) throw new Error(`s52-aton-expressions: no generated lookup for ${objectClass}`);
21318
+ function atonIconExpression(objectClass2) {
21319
+ const lookup = S52_ATON_LOOKUPS[objectClass2];
21320
+ if (!lookup) throw new Error(`s52-aton-expressions: no generated lookup for ${objectClass2}`);
20081
21321
  return compileAtonExpression(lookup);
20082
21322
  }
20083
21323
  function lightsIconExpression() {
@@ -23162,7 +24402,9 @@ var LABEL_TINT = {
23162
24402
  };
23163
24403
  function tintFor(tint, ct) {
23164
24404
  const day = LABEL_TINT[tint].day;
23165
- if (ct === "DAY") return day;
24405
+ if (ct === "DAY" || ct === "EINK_COLOR") return day;
24406
+ if (ct === "EINK_MONO") return einkMonoGrey(day);
24407
+ if (ct === "EINK_NIGHT") return einkNightGrey(day);
23166
24408
  return capLuminance(day, relativeLuminance(resolveColor("CHBLK", ct)));
23167
24409
  }
23168
24410
  function nightText(tint) {
@@ -23173,10 +24415,14 @@ function nightText(tint) {
23173
24415
  }
23174
24416
  function onHalo(tint, ct) {
23175
24417
  if (ct === "NIGHT") return { textColor: nightText(tint), textHaloColor: NIGHT_KNOCKOUT };
24418
+ if (ct === "EINK_NIGHT") return { textColor: EINK_MONO_GREYS.MID, textHaloColor: EINK_MONO_GREYS.INK };
24419
+ if (isEinkColorTable(ct)) return { textColor: EINK_MONO_GREYS.INK, textHaloColor: EINK_MONO_GREYS.PAPER };
23176
24420
  return { textColor: DARK_BG, textHaloColor: tintFor(tint, ct) };
23177
24421
  }
23178
24422
  function onKnockout(tint, ct) {
23179
24423
  if (ct === "NIGHT") return { textColor: nightText(tint), textHaloColor: NIGHT_KNOCKOUT };
24424
+ if (ct === "EINK_NIGHT") return { textColor: EINK_MONO_GREYS.MID, textHaloColor: EINK_MONO_GREYS.INK };
24425
+ if (isEinkColorTable(ct)) return { textColor: EINK_MONO_GREYS.INK, textHaloColor: EINK_MONO_GREYS.PAPER };
23180
24426
  return { textColor: tintFor(tint, ct), textHaloColor: DARK_BG };
23181
24427
  }
23182
24428
  var NAV_AID_LABEL_TEXT_SIZE = [
@@ -27393,10 +28639,20 @@ var S101_NO_OFFICIAL_COUNTERPART = /* @__PURE__ */ new Set([
27393
28639
  "SISTAT02",
27394
28640
  "WATTUR01"
27395
28641
  ]);
28642
+ function s101EinkTable(einkS52, fallback) {
28643
+ const out = {};
28644
+ for (const [token, dayHex] of Object.entries(S101_DAY)) {
28645
+ out[token] = einkS52[token] ?? fallback(dayHex);
28646
+ }
28647
+ return out;
28648
+ }
27396
28649
  var S101_TABLES = {
27397
28650
  DAY: S101_DAY,
27398
28651
  DUSK: S101_DUSK,
27399
- NIGHT: S101_NIGHT
28652
+ NIGHT: S101_NIGHT,
28653
+ EINK_COLOR: s101EinkTable(S52_EINK_COLOR, (hex) => hex),
28654
+ EINK_MONO: s101EinkTable(S52_EINK_MONO, einkMonoGrey),
28655
+ EINK_NIGHT: s101EinkTable(S52_EINK_NIGHT, einkNightGrey)
27400
28656
  };
27401
28657
  var warnedTokens = /* @__PURE__ */ new Set();
27402
28658
  function reportUnresolvedToken(token, table) {
@@ -27443,328 +28699,54 @@ function rewritePatternToS101(node, available) {
27443
28699
  if (typeof node === "string") {
27444
28700
  return available.has(node) ? `${S101_ICON_PREFIX}${node}` : node;
27445
28701
  }
27446
- if (Array.isArray(node)) return node.map((n) => rewritePatternToS101(n, available));
27447
- return node;
27448
- }
27449
- function collectLiterals(node, out) {
27450
- if (typeof node === "string") {
27451
- out.add(node);
27452
- return;
27453
- }
27454
- if (Array.isArray(node)) for (const n of node) collectLiterals(n, out);
27455
- }
27456
- function applyS101SymbolSet(style) {
27457
- const seen = { icon: /* @__PURE__ */ new Set(), fill: /* @__PURE__ */ new Set(), line: /* @__PURE__ */ new Set() };
27458
- for (const layer of style.layers) {
27459
- const layout = layer.layout;
27460
- const paint = layer.paint;
27461
- if (layer.type === "symbol" && layout && layout["icon-image"] != null) {
27462
- layout["icon-image"] = rewriteIconToS101(layout["icon-image"]);
27463
- collectLiterals(layout["icon-image"], seen.icon);
27464
- }
27465
- for (const bag of [layout, paint]) {
27466
- if (!bag) continue;
27467
- if (bag["fill-pattern"] != null) {
27468
- bag["fill-pattern"] = rewritePatternToS101(bag["fill-pattern"], S101_AREA_FILL_NAMES);
27469
- collectLiterals(bag["fill-pattern"], seen.fill);
27470
- }
27471
- if (bag["line-pattern"] != null) {
27472
- bag["line-pattern"] = rewritePatternToS101(bag["line-pattern"], S101_LINE_STYLE_NAMES);
27473
- collectLiterals(bag["line-pattern"], seen.line);
27474
- }
27475
- }
27476
- }
27477
- const official = (names) => [...names].filter((n) => n.startsWith(S101_ICON_PREFIX)).length;
27478
- const iconSubs = [...seen.icon].filter((n) => S101_NO_OFFICIAL_COUNTERPART.has(n)).length;
27479
- const patternSubs = (names) => [...names].filter((n) => /^(AP|LC):/.test(n)).length;
27480
- const coverage = {
27481
- pointSymbols: { official: official(seen.icon), s52Substitute: iconSubs },
27482
- areaFills: { official: official(seen.fill), s52Substitute: patternSubs(seen.fill) },
27483
- lineStyles: { official: official(seen.line), s52Substitute: patternSubs(seen.line) }
27484
- };
27485
- const metadata = style.metadata;
27486
- metadata["chrt:symbol-set"] = "s101";
27487
- metadata["chrt:s101-source-commit"] = S101_SOURCE_COMMIT;
27488
- metadata["chrt:s101-coverage"] = {
27489
- ...coverage,
27490
- /** True only when NOTHING in this document is an S-52 substitute. A
27491
- * consumer that wants "S-101 or nothing" branches on this one boolean. */
27492
- complete: Object.values(coverage).every((c) => c.s52Substitute === 0)
27493
- };
27494
- return style;
27495
- }
27496
-
27497
- // src/atlas/chrt-svgs-generated.ts
27498
- var CHRT_GLYPH_SVGS = {
27499
- "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>',
27500
- "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>',
27501
- "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>',
27502
- "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>',
27503
- "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>',
27504
- "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>',
27505
- "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>',
27506
- "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>',
27507
- "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>',
27508
- "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>',
27509
- "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>',
27510
- "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>',
27511
- "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>',
27512
- "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>',
27513
- "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>',
27514
- "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>',
27515
- "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>',
27516
- "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>',
27517
- "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>',
27518
- "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>',
27519
- "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>',
27520
- "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>',
27521
- "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>',
27522
- "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>',
27523
- "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>',
27524
- "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>'
27525
- };
27526
-
27527
- // src/atlas/chrt-style.ts
27528
- var CHRT_ICON_PREFIX = "chrt:";
27529
- var CHRT_SDF_GLYPHS = /* @__PURE__ */ new Set([
27530
- "buoy-can",
27531
- "buoy-conical",
27532
- "buoy-spherical",
27533
- "buoy-pillar",
27534
- "buoy-spar",
27535
- "buoy-barrel",
27536
- "buoy-super",
27537
- "beacon-generic",
27538
- "beacon-tower",
27539
- "beacon-lattice",
27540
- "beacon-stake",
27541
- "beacon-cairn",
27542
- "light-flare",
27543
- "lightship",
27544
- "daymark-triangle",
27545
- "daymark-square"
27546
- ]);
27547
- var CHRT_GLYPH_NAMES = new Set(Object.keys(CHRT_GLYPH_SVGS));
27548
- var CHRT_ATON_COLORS = {
27549
- "1": "#F8FAFC",
27550
- // white
27551
- "2": "#101418",
27552
- // black
27553
- "3": "#E5484D",
27554
- // red
27555
- "4": "#30A46C",
27556
- // green
27557
- "5": "#3E63DD",
27558
- // blue
27559
- "6": "#FFC805",
27560
- // yellow
27561
- "7": "#8B9298",
27562
- // grey
27563
- "8": "#A66B3A",
27564
- // brown
27565
- "9": "#FFB224",
27566
- // amber
27567
- "10": "#7E4CCB",
27568
- // violet
27569
- "11": "#F76B15",
27570
- // orange
27571
- "12": "#D671C1"
27572
- // pink (magenta-ish)
27573
- };
27574
- var CHRT_UNKNOWN_TINT = "#8B9298";
27575
- var HOUSE_BLACK = "#101418";
27576
- function chrtHouseColor(dayHex, palette = "DAY") {
27577
- if (palette === "DAY") return dayHex;
27578
- const isBlack = dayHex.toLowerCase() === HOUSE_BLACK.toLowerCase();
27579
- if (palette === "DUSK") {
27580
- return isBlack ? dayHex : capLuminance(dayHex, relativeLuminance(resolveColor("CHBLK", "DUSK")));
27581
- }
27582
- return isBlack ? NIGHT_INK : capLuminance(dayHex, relativeLuminance(NIGHT_MUTED_INK));
27583
- }
27584
- function themeChrtGlyph(svg3, palette = "DAY") {
27585
- if (palette === "DAY") return svg3;
27586
- return svg3.replace(
27587
- /(fill|stroke)="(#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6})"/g,
27588
- (_m, attr, hex) => {
27589
- const six = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
27590
- return `${attr}="${chrtHouseColor(six, palette)}"`;
27591
- }
27592
- );
27593
- }
27594
- function chrtAtonTintExpression(palette = "DAY") {
27595
- const c = (code) => chrtHouseColor(CHRT_ATON_COLORS[code], palette);
27596
- return colourCodeCase(
27597
- [
27598
- ["3", c("3")],
27599
- ["4", c("4")],
27600
- ["6", c("6")],
27601
- ["11", c("11")],
27602
- ["9", c("9")],
27603
- ["5", c("5")],
27604
- ["1", c("1")],
27605
- ["2", c("2")],
27606
- ["8", c("8")],
27607
- ["10", c("10")],
27608
- ["12", c("12")],
27609
- ["7", c("7")]
27610
- ],
27611
- chrtHouseColor(CHRT_UNKNOWN_TINT, palette)
27612
- );
27613
- }
27614
- function chrtLightTintExpression(palette = "DAY") {
27615
- const c = (code) => chrtHouseColor(CHRT_ATON_COLORS[code], palette);
27616
- return colourCodeCase(
27617
- [
27618
- ["3", c("3")],
27619
- ["4", c("4")],
27620
- ["6", c("6")],
27621
- ["11", c("11")],
27622
- ["9", c("9")],
27623
- ["5", c("5")],
27624
- ["1", c("1")]
27625
- ],
27626
- c("1")
27627
- );
27628
- }
27629
- function scalar(attr) {
27630
- return ["coalesce", ["get", attr], ["get", attr.toLowerCase()], 0];
27631
- }
27632
- var g = (name) => `${CHRT_ICON_PREFIX}${name}`;
27633
- function buoyBodyExpression() {
27634
- return [
27635
- "match",
27636
- scalar("BOYSHP"),
27637
- 1,
27638
- g("buoy-conical"),
27639
- 2,
27640
- g("buoy-can"),
27641
- 3,
27642
- g("buoy-spherical"),
27643
- 4,
27644
- g("buoy-pillar"),
27645
- 5,
27646
- g("buoy-spar"),
27647
- 6,
27648
- g("buoy-barrel"),
27649
- 7,
27650
- g("buoy-super"),
27651
- 8,
27652
- g("buoy-super"),
27653
- g("buoy-pillar")
27654
- ];
27655
- }
27656
- function beaconBodyExpression() {
27657
- return [
27658
- "match",
27659
- scalar("BCNSHP"),
27660
- 1,
27661
- g("beacon-stake"),
27662
- 2,
27663
- g("beacon-stake"),
27664
- 3,
27665
- g("beacon-tower"),
27666
- 4,
27667
- g("beacon-lattice"),
27668
- 5,
27669
- g("beacon-stake"),
27670
- 6,
27671
- g("beacon-cairn"),
27672
- g("beacon-generic")
27673
- ];
28702
+ if (Array.isArray(node)) return node.map((n) => rewritePatternToS101(n, available));
28703
+ return node;
27674
28704
  }
27675
- function cardinalExpression() {
27676
- return [
27677
- "match",
27678
- scalar("CATCAM"),
27679
- 1,
27680
- g("cardinal-n"),
27681
- 2,
27682
- g("cardinal-e"),
27683
- 3,
27684
- g("cardinal-s"),
27685
- 4,
27686
- g("cardinal-w"),
27687
- g("cardinal-n")
27688
- ];
28705
+ function collectLiterals(node, out) {
28706
+ if (typeof node === "string") {
28707
+ out.add(node);
28708
+ return;
28709
+ }
28710
+ if (Array.isArray(node)) for (const n of node) collectLiterals(n, out);
27689
28711
  }
27690
- var CHRT_ICON_EXPRESSIONS = {
27691
- BOYLAT: buoyBodyExpression,
27692
- BOYSPP: buoyBodyExpression,
27693
- BOYINB: buoyBodyExpression,
27694
- BOYCAR: cardinalExpression,
27695
- BOYISD: () => g("isolated-danger"),
27696
- BOYSAW: () => g("safe-water"),
27697
- BCNLAT: beaconBodyExpression,
27698
- BCNSPP: beaconBodyExpression,
27699
- BCNCAR: cardinalExpression,
27700
- BCNISD: () => g("isolated-danger"),
27701
- BCNSAW: () => g("safe-water"),
27702
- LIGHTS: () => g("light-flare"),
27703
- LITFLT: () => g("lightship"),
27704
- LITVES: () => g("lightship"),
27705
- FOGSIG: () => g("badge-sound"),
27706
- RTPBCN: () => g("badge-radar"),
27707
- RDOSTA: () => g("badge-radar"),
27708
- DAYMAR: () => [
27709
- "match",
27710
- scalar("TOPSHP"),
27711
- 1,
27712
- g("daymark-triangle"),
27713
- 2,
27714
- g("daymark-triangle"),
27715
- 5,
27716
- g("daymark-triangle"),
27717
- g("daymark-square")
27718
- ]
27719
- };
27720
- var TINTED_CLASSES = /* @__PURE__ */ new Set([
27721
- "BOYLAT",
27722
- "BOYSPP",
27723
- "BOYINB",
27724
- "BCNLAT",
27725
- "BCNSPP",
27726
- "DAYMAR"
27727
- ]);
27728
- var CHRT_HALO = { color: HOUSE_BLACK, width: 1 };
27729
- var CHRT_LINE_DASH = {
27730
- CBLSUB: { color: "CHMGD", width: 0.8, dash: [6, 3] },
27731
- PIPSOL: { color: "PIPLN", width: 0.8, dash: [6, 3] }
27732
- };
27733
- function applyChrtSymbolSet(style, palette = "DAY") {
28712
+ function applyS101SymbolSet(style) {
28713
+ const seen = { icon: /* @__PURE__ */ new Set(), fill: /* @__PURE__ */ new Set(), line: /* @__PURE__ */ new Set() };
27734
28714
  for (const layer of style.layers) {
27735
- if (layer.type === "line") {
27736
- const cls = layer.metadata?.["s52:objectClass"];
27737
- const dash = cls ? CHRT_LINE_DASH[cls] : void 0;
27738
- const paint2 = layer.paint ?? {};
27739
- if (dash && paint2["line-pattern"] != null) {
27740
- delete paint2["line-pattern"];
27741
- paint2["line-color"] = resolveColor(dash.color, palette);
27742
- paint2["line-width"] = dash.width;
27743
- paint2["line-dasharray"] = dash.dash;
27744
- layer.paint = paint2;
27745
- }
27746
- continue;
27747
- }
27748
- if (layer.type !== "symbol") continue;
27749
- const objectClass = layer.metadata?.["s52:objectClass"];
27750
- if (!objectClass || !CHRT_ICON_EXPRESSIONS[objectClass]) continue;
27751
28715
  const layout = layer.layout;
27752
- if (!layout || layout["icon-image"] == null) continue;
27753
- layout["icon-image"] = CHRT_ICON_EXPRESSIONS[objectClass]();
27754
- const paint = layer.paint ?? {};
27755
- if (TINTED_CLASSES.has(objectClass)) {
27756
- paint["icon-color"] = chrtAtonTintExpression(palette);
27757
- paint["icon-halo-color"] = CHRT_HALO.color;
27758
- paint["icon-halo-width"] = CHRT_HALO.width;
27759
- } else if (objectClass === "LIGHTS") {
27760
- paint["icon-color"] = chrtLightTintExpression(palette);
27761
- paint["icon-halo-color"] = CHRT_HALO.color;
27762
- paint["icon-halo-width"] = CHRT_HALO.width;
28716
+ const paint = layer.paint;
28717
+ if (layer.type === "symbol" && layout && layout["icon-image"] != null) {
28718
+ layout["icon-image"] = rewriteIconToS101(layout["icon-image"]);
28719
+ collectLiterals(layout["icon-image"], seen.icon);
28720
+ }
28721
+ for (const bag of [layout, paint]) {
28722
+ if (!bag) continue;
28723
+ if (bag["fill-pattern"] != null) {
28724
+ bag["fill-pattern"] = rewritePatternToS101(bag["fill-pattern"], S101_AREA_FILL_NAMES);
28725
+ collectLiterals(bag["fill-pattern"], seen.fill);
28726
+ }
28727
+ if (bag["line-pattern"] != null) {
28728
+ bag["line-pattern"] = rewritePatternToS101(bag["line-pattern"], S101_LINE_STYLE_NAMES);
28729
+ collectLiterals(bag["line-pattern"], seen.line);
28730
+ }
27763
28731
  }
27764
- layer.paint = paint;
27765
28732
  }
28733
+ const official = (names) => [...names].filter((n) => n.startsWith(S101_ICON_PREFIX)).length;
28734
+ const iconSubs = [...seen.icon].filter((n) => S101_NO_OFFICIAL_COUNTERPART.has(n)).length;
28735
+ const patternSubs = (names) => [...names].filter((n) => /^(AP|LC):/.test(n)).length;
28736
+ const coverage = {
28737
+ pointSymbols: { official: official(seen.icon), s52Substitute: iconSubs },
28738
+ areaFills: { official: official(seen.fill), s52Substitute: patternSubs(seen.fill) },
28739
+ lineStyles: { official: official(seen.line), s52Substitute: patternSubs(seen.line) }
28740
+ };
27766
28741
  const metadata = style.metadata;
27767
- metadata["chrt:symbol-set"] = "chrt";
28742
+ metadata["chrt:symbol-set"] = "s101";
28743
+ metadata["chrt:s101-source-commit"] = S101_SOURCE_COMMIT;
28744
+ metadata["chrt:s101-coverage"] = {
28745
+ ...coverage,
28746
+ /** True only when NOTHING in this document is an S-52 substitute. A
28747
+ * consumer that wants "S-101 or nothing" branches on this one boolean. */
28748
+ complete: Object.values(coverage).every((c) => c.s52Substitute === 0)
28749
+ };
27768
28750
  return style;
27769
28751
  }
27770
28752
 
@@ -27921,9 +28903,9 @@ var RASTER_ATON_CLASSES = Object.keys(
27921
28903
  if (!s52) return true;
27922
28904
  return JSON.stringify({ r: paper.rows, d: paper.defaultSymbol }) !== JSON.stringify({ r: s52.rows, d: s52.defaultSymbol });
27923
28905
  });
27924
- function rasterAtonIconExpression(objectClass) {
27925
- const lookup = S52_ATON_LOOKUPS_PAPER[objectClass];
27926
- if (!lookup) throw new Error(`raster-style: no paper lookup for ${objectClass}`);
28906
+ function rasterAtonIconExpression(objectClass2) {
28907
+ const lookup = S52_ATON_LOOKUPS_PAPER[objectClass2];
28908
+ if (!lookup) throw new Error(`raster-style: no paper lookup for ${objectClass2}`);
27927
28909
  return compileAtonExpression(lookup, resolveRasterImage);
27928
28910
  }
27929
28911
  function getRasterSvg(name, table = "DAY") {
@@ -27937,9 +28919,9 @@ function applyRasterSymbolSet(style) {
27937
28919
  const layout = layer.layout;
27938
28920
  if (!layout || layout["icon-image"] == null) continue;
27939
28921
  const metadata2 = layer.metadata;
27940
- const objectClass = metadata2?.["s52:objectClass"];
27941
- if (typeof objectClass !== "string" || !classes.has(objectClass)) continue;
27942
- layout["icon-image"] = rasterAtonIconExpression(objectClass);
28922
+ const objectClass2 = metadata2?.["s52:objectClass"];
28923
+ if (typeof objectClass2 !== "string" || !classes.has(objectClass2)) continue;
28924
+ layout["icon-image"] = rasterAtonIconExpression(objectClass2);
27943
28925
  }
27944
28926
  const metadata = style.metadata;
27945
28927
  metadata["chrt:symbol-set"] = "raster";
@@ -27971,19 +28953,19 @@ function parseSymbolFamily(raw) {
27971
28953
  const lower = raw.toLowerCase();
27972
28954
  return SYMBOL_FAMILIES.find((f) => f === lower) ?? null;
27973
28955
  }
27974
- function iconExpressionForFamily(objectClass, family) {
27975
- const s52 = getIconExpression(objectClass);
28956
+ function iconExpressionForFamily(objectClass2, family) {
28957
+ const s52 = getIconExpression(objectClass2);
27976
28958
  switch (family) {
27977
28959
  case "s52":
27978
28960
  return s52;
27979
28961
  case "s101":
27980
28962
  return rewriteIconToS101(s52);
27981
28963
  case "chrt": {
27982
- const build = CHRT_ICON_EXPRESSIONS[objectClass];
28964
+ const build = CHRT_ICON_EXPRESSIONS[objectClass2];
27983
28965
  return build ? build() : s52;
27984
28966
  }
27985
28967
  case "raster":
27986
- return RASTER_ATON_CLASSES.includes(objectClass) ? rasterAtonIconExpression(objectClass) : s52;
28968
+ return RASTER_ATON_CLASSES.includes(objectClass2) ? rasterAtonIconExpression(objectClass2) : s52;
27987
28969
  }
27988
28970
  }
27989
28971
 
@@ -28031,16 +29013,16 @@ var LARGE = {
28031
29013
  WRECKS: HAZARD_DEPTH
28032
29014
  };
28033
29015
  var LARGE_SOUNDING_PADDING_PX = 3.5;
28034
- function applyDepthLabelSize(objectClass, layer, size) {
29016
+ function applyDepthLabelSize(objectClass2, layer, size) {
28035
29017
  if (size !== "large") return layer;
28036
- const large = LARGE[objectClass];
29018
+ const large = LARGE[objectClass2];
28037
29019
  if (large === void 0) return layer;
28038
29020
  const layout = {
28039
29021
  ...layer.layout ?? {},
28040
29022
  "text-size": large.textSize,
28041
29023
  "text-font": [...large.font]
28042
29024
  };
28043
- if (objectClass === "SOUNDG") layout["text-padding"] = LARGE_SOUNDING_PADDING_PX;
29025
+ if (objectClass2 === "SOUNDG") layout["text-padding"] = LARGE_SOUNDING_PADDING_PX;
28044
29026
  return {
28045
29027
  ...layer,
28046
29028
  layout,
@@ -28237,47 +29219,47 @@ function buildLightLabelExpression() {
28237
29219
  ];
28238
29220
  return ["concat", litchrExpr, siggrpExpr, colourExpr, sigperExpr, valnmrExpr];
28239
29221
  }
28240
- function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts = {}) {
28241
- if (!S52_LOOKUP[objectClass]) return [];
28242
- const entry = S52_LOOKUP[objectClass];
29222
+ function buildLayersForClass(objectClass2, ct, sourceId = DEFAULT_REGION, opts = {}) {
29223
+ if (!S52_LOOKUP[objectClass2]) return [];
29224
+ const entry = S52_LOOKUP[objectClass2];
28243
29225
  const layers = [];
28244
- const geomTypes = getGeomTypesForClass(objectClass, entry?.style.type);
29226
+ const geomTypes = getGeomTypesForClass(objectClass2, entry?.style.type);
28245
29227
  for (const geomType of geomTypes) {
28246
- const resolved = resolveStyle(objectClass, geomType, ct, { safetyContour: opts.safetyContour });
28247
- const layerId = `s52-${objectClass}-${resolved.type}`;
29228
+ const resolved = resolveStyle(objectClass2, geomType, ct, { safetyContour: opts.safetyContour });
29229
+ const layerId = `s52-${objectClass2}-${resolved.type}`;
28248
29230
  const layer = {
28249
29231
  id: layerId,
28250
29232
  type: resolved.type,
28251
29233
  source: sourceId,
28252
- "source-layer": objectClass,
29234
+ "source-layer": objectClass2,
28253
29235
  paint: resolved.paint,
28254
29236
  metadata: {
28255
- "s52:objectClass": objectClass,
29237
+ "s52:objectClass": objectClass2,
28256
29238
  "s52:displayCategory": entry?.displayCategory ?? "OTHER",
28257
29239
  "s52:category": entry?.category ?? "Meta"
28258
29240
  }
28259
29241
  };
28260
- if (resolved.type === "line" && LINE_FROM_POLYGON_LAYERS.has(objectClass)) {
29242
+ if (resolved.type === "line" && LINE_FROM_POLYGON_LAYERS.has(objectClass2)) {
28261
29243
  layer.filter = GEOM_FILTERS.fill;
28262
29244
  } else if (GEOM_FILTERS[resolved.type]) {
28263
29245
  layer.filter = GEOM_FILTERS[resolved.type];
28264
29246
  }
28265
- if (SCAMIN_DECLUTTER_CLASSES.has(objectClass)) {
29247
+ if (SCAMIN_DECLUTTER_CLASSES.has(objectClass2)) {
28266
29248
  layer.filter = layer.filter ? ["all", layer.filter, scaminZoomGateFilter()] : ["all", scaminZoomGateFilter()];
28267
29249
  }
28268
29250
  if (resolved.minzoom) {
28269
29251
  layer.minzoom = resolved.minzoom;
28270
29252
  }
28271
- if (resolved.type === "fill" && OPAQUE_FILLS.has(objectClass)) {
29253
+ if (resolved.type === "fill" && OPAQUE_FILLS.has(objectClass2)) {
28272
29254
  layer.paint["fill-opacity"] = 1;
28273
29255
  }
28274
- if (resolved.type === "line" && ZOOM_SCALED_LINES2[objectClass]) {
28275
- const [z1, w1, z2, w2] = ZOOM_SCALED_LINES2[objectClass];
29256
+ if (resolved.type === "line" && ZOOM_SCALED_LINES2[objectClass2]) {
29257
+ const [z1, w1, z2, w2] = ZOOM_SCALED_LINES2[objectClass2];
28276
29258
  layer.paint["line-width"] = ["interpolate", ["linear"], ["zoom"], z1, w1, z2, w2];
28277
29259
  }
28278
29260
  if (resolved.type === "symbol") {
28279
- const iconExpr = getIconExpression(objectClass);
28280
- const childOffset = CHILD_ICON_OFFSETS[objectClass];
29261
+ const iconExpr = getIconExpression(objectClass2);
29262
+ const childOffset = CHILD_ICON_OFFSETS[objectClass2];
28281
29263
  const isSafetyCritical = SAFETY_GROUPS.has(entry?.viewingGroup ?? 0);
28282
29264
  const safetyCriticalIconSize = [
28283
29265
  "interpolate",
@@ -28400,7 +29382,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28400
29382
  ]
28401
29383
  };
28402
29384
  }
28403
- if (objectClass === "LIGHTS") {
29385
+ if (objectClass2 === "LIGHTS") {
28404
29386
  layer.layout = {
28405
29387
  ...layer.layout,
28406
29388
  "icon-allow-overlap": true,
@@ -28442,14 +29424,14 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28442
29424
  } else if (resolved.layout) {
28443
29425
  layer.layout = resolved.layout;
28444
29426
  }
28445
- if (objectClass === "LNDMRK" && resolved.type === "symbol") {
29427
+ if (objectClass2 === "LNDMRK" && resolved.type === "symbol") {
28446
29428
  const baseFilterParts = layer.filter && layer.filter[0] === "all" ? layer.filter.slice(1) : layer.filter ? [layer.filter] : [];
28447
29429
  const isWind = listAttrHas("CATLMK", ["18", "19"]);
28448
29430
  const isLighthouse = listAttrHas("CATLMK", ["17"]);
28449
29431
  const scaminFloor = scaminToMinZoom(entry?.scamin);
28450
29432
  const windLayer = {
28451
29433
  ...layer,
28452
- id: `s52-${objectClass}-wind-symbol`,
29434
+ id: `s52-${objectClass2}-wind-symbol`,
28453
29435
  filter: ["all", ...baseFilterParts, isWind],
28454
29436
  // Explicit, not inherited: turbines share exact coordinates with
28455
29437
  // their aviation-light LIGHTS feature; without allow-overlap the
@@ -28462,7 +29444,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28462
29444
  delete windLayer.minzoom;
28463
29445
  const lighthouseLayer = {
28464
29446
  ...layer,
28465
- id: `s52-${objectClass}-lighthouse-symbol`,
29447
+ id: `s52-${objectClass2}-lighthouse-symbol`,
28466
29448
  filter: ["all", ...baseFilterParts, isLighthouse, ["!", isWind]],
28467
29449
  layout: {
28468
29450
  ...layer.layout ?? {},
@@ -28473,7 +29455,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28473
29455
  delete lighthouseLayer.minzoom;
28474
29456
  const otherLayer = {
28475
29457
  ...layer,
28476
- id: `s52-${objectClass}-other-symbol`,
29458
+ id: `s52-${objectClass2}-other-symbol`,
28477
29459
  filter: ["all", ...baseFilterParts, ["!", isWind], ["!", isLighthouse]]
28478
29460
  };
28479
29461
  if (scaminFloor !== void 0) {
@@ -28488,10 +29470,10 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28488
29470
  const valsouNum = ["to-number", ["coalesce", ["get", "VALSOU"], ["get", "valsou"], 999], 999];
28489
29471
  const watlevNum = ["to-number", ["coalesce", ["get", "WATLEV"], ["get", "watlev"], 0], 0];
28490
29472
  layers.push({
28491
- id: `s52-${objectClass}-danger-ring`,
29473
+ id: `s52-${objectClass2}-danger-ring`,
28492
29474
  type: "symbol",
28493
29475
  source: sourceId,
28494
- "source-layer": objectClass,
29476
+ "source-layer": objectClass2,
28495
29477
  filter: [
28496
29478
  "all",
28497
29479
  GEOM_FILTERS.symbol,
@@ -28524,48 +29506,48 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28524
29506
  },
28525
29507
  paint: {},
28526
29508
  minzoom: 13,
28527
- metadata: { "s52:objectClass": objectClass, "s52:role": "isolated-danger-ring" }
29509
+ metadata: { "s52:objectClass": objectClass2, "s52:role": "isolated-danger-ring" }
28528
29510
  });
28529
29511
  }
28530
- if (resolved.type === "fill" && FILL_OUTLINE_LAYERS.has(objectClass)) {
28531
- const outlineColor = objectClass === "DEPARE" ? resolveColor("CHGRD", ct) : resolveColor("CHGRF", ct);
29512
+ if (resolved.type === "fill" && FILL_OUTLINE_LAYERS.has(objectClass2)) {
29513
+ const outlineColor = objectClass2 === "DEPARE" ? resolveColor("CHGRD", ct) : resolveColor("CHGRF", ct);
28532
29514
  layers.push({
28533
- id: `s52-${objectClass}-outline`,
29515
+ id: `s52-${objectClass2}-outline`,
28534
29516
  type: "line",
28535
29517
  source: sourceId,
28536
- "source-layer": objectClass,
29518
+ "source-layer": objectClass2,
28537
29519
  filter: GEOM_FILTERS.fill,
28538
29520
  paint: {
28539
29521
  "line-color": outlineColor,
28540
29522
  "line-width": 0.3,
28541
29523
  "line-opacity": 0.4
28542
29524
  },
28543
- metadata: { "s52:objectClass": objectClass, "s52:role": "outline" }
29525
+ metadata: { "s52:objectClass": objectClass2, "s52:role": "outline" }
28544
29526
  });
28545
29527
  }
28546
- if (resolved.type === "fill" && REGULATED_AREA_BOUNDARY_LAYERS[objectClass]) {
28547
- const lineStyle = REGULATED_AREA_BOUNDARY_LINESTYLES[objectClass];
29528
+ if (resolved.type === "fill" && REGULATED_AREA_BOUNDARY_LAYERS[objectClass2]) {
29529
+ const lineStyle = REGULATED_AREA_BOUNDARY_LINESTYLES[objectClass2];
28548
29530
  const boundaryPaint = lineStyle ? lineStylePaint(lineStyle) : {
28549
- "line-color": resolveColor(REGULATED_AREA_BOUNDARY_LAYERS[objectClass], ct),
29531
+ "line-color": resolveColor(REGULATED_AREA_BOUNDARY_LAYERS[objectClass2], ct),
28550
29532
  "line-width": 1,
28551
29533
  "line-dasharray": [4, 2],
28552
29534
  "line-opacity": 0.85
28553
29535
  };
28554
29536
  layers.push({
28555
- id: `s52-${objectClass}-boundary`,
29537
+ id: `s52-${objectClass2}-boundary`,
28556
29538
  type: "line",
28557
29539
  source: sourceId,
28558
- "source-layer": objectClass,
29540
+ "source-layer": objectClass2,
28559
29541
  filter: GEOM_FILTERS.fill,
28560
29542
  paint: boundaryPaint,
28561
29543
  metadata: {
28562
- "s52:objectClass": objectClass,
29544
+ "s52:objectClass": objectClass2,
28563
29545
  "s52:role": "boundary",
28564
29546
  ...lineStyle ? { "s52:lineStyle": lineStyle } : {}
28565
29547
  }
28566
29548
  });
28567
29549
  }
28568
- if (objectClass === "ACHARE" && resolved.type === "fill") {
29550
+ if (objectClass2 === "ACHARE" && resolved.type === "fill") {
28569
29551
  layers.push({
28570
29552
  id: "s52-ACHARE-boundary",
28571
29553
  type: "line",
@@ -28581,7 +29563,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28581
29563
  metadata: { "s52:objectClass": "ACHARE", "s52:role": "boundary" }
28582
29564
  });
28583
29565
  }
28584
- if (objectClass === "DRGARE" && resolved.type === "fill") {
29566
+ if (objectClass2 === "DRGARE" && resolved.type === "fill") {
28585
29567
  layers.push({
28586
29568
  id: "s52-DRGARE-boundary",
28587
29569
  type: "line",
@@ -28597,7 +29579,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28597
29579
  metadata: { "s52:objectClass": "DRGARE", "s52:role": "boundary" }
28598
29580
  });
28599
29581
  }
28600
- if (objectClass === "ACHARE" && resolved.type === "fill") {
29582
+ if (objectClass2 === "ACHARE" && resolved.type === "fill") {
28601
29583
  layers.push({
28602
29584
  id: "s52-ACHARE-anchor-icon",
28603
29585
  type: "symbol",
@@ -28626,7 +29608,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28626
29608
  metadata: { "s52:objectClass": "ACHARE", "s52:role": "centroid-icon" }
28627
29609
  });
28628
29610
  }
28629
- if (objectClass === "LNDARE" && resolved.type === "fill") {
29611
+ if (objectClass2 === "LNDARE" && resolved.type === "fill") {
28630
29612
  layers.push({
28631
29613
  id: `s52-LNDARE-point`,
28632
29614
  type: "circle",
@@ -28642,7 +29624,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28642
29624
  metadata: { "s52:objectClass": "LNDARE", "s52:role": "point" }
28643
29625
  });
28644
29626
  }
28645
- if (objectClass === "LNDARE" && resolved.type === "fill") {
29627
+ if (objectClass2 === "LNDARE" && resolved.type === "fill") {
28646
29628
  layers.push({
28647
29629
  id: `s52-LNDARE-line`,
28648
29630
  type: "line",
@@ -28656,7 +29638,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28656
29638
  metadata: { "s52:objectClass": "LNDARE", "s52:role": "line" }
28657
29639
  });
28658
29640
  }
28659
- if (objectClass === "BUAARE" && resolved.type === "fill") {
29641
+ if (objectClass2 === "BUAARE" && resolved.type === "fill") {
28660
29642
  layers.push({
28661
29643
  id: `s52-BUAARE-point`,
28662
29644
  type: "circle",
@@ -28672,7 +29654,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28672
29654
  metadata: { "s52:objectClass": "BUAARE", "s52:role": "point" }
28673
29655
  });
28674
29656
  }
28675
- if (objectClass === "RIVERS" && resolved.type === "fill") {
29657
+ if (objectClass2 === "RIVERS" && resolved.type === "fill") {
28676
29658
  layers.push({
28677
29659
  id: `s52-RIVERS-line`,
28678
29660
  type: "line",
@@ -28686,7 +29668,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28686
29668
  metadata: { "s52:objectClass": "RIVERS", "s52:role": "line" }
28687
29669
  });
28688
29670
  }
28689
- if (objectClass === "BUISGL" && resolved.type === "circle") {
29671
+ if (objectClass2 === "BUISGL" && resolved.type === "circle") {
28690
29672
  layers.push({
28691
29673
  id: `s52-BUISGL-polygon`,
28692
29674
  type: "fill",
@@ -28712,7 +29694,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28712
29694
  metadata: { "s52:objectClass": "BUISGL", "s52:role": "polygon-outline" }
28713
29695
  });
28714
29696
  }
28715
- if (objectClass === "BRIDGE" && resolved.type === "line") {
29697
+ if (objectClass2 === "BRIDGE" && resolved.type === "line") {
28716
29698
  layers.push({
28717
29699
  id: `s52-BRIDGE-polygon`,
28718
29700
  type: "fill",
@@ -28738,7 +29720,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28738
29720
  metadata: { "s52:objectClass": "BRIDGE", "s52:role": "polygon-outline" }
28739
29721
  });
28740
29722
  }
28741
- if (objectClass === "PYLONS" && resolved.type === "fill") {
29723
+ if (objectClass2 === "PYLONS" && resolved.type === "fill") {
28742
29724
  layers.push({
28743
29725
  id: `s52-PYLONS-point`,
28744
29726
  type: "circle",
@@ -28754,7 +29736,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28754
29736
  metadata: { "s52:objectClass": "PYLONS", "s52:role": "point" }
28755
29737
  });
28756
29738
  }
28757
- if (objectClass === "WEDKLP" && resolved.type === "fill") {
29739
+ if (objectClass2 === "WEDKLP" && resolved.type === "fill") {
28758
29740
  layers.push({
28759
29741
  id: `s52-WEDKLP-point`,
28760
29742
  type: "circle",
@@ -28771,7 +29753,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28771
29753
  metadata: { "s52:objectClass": "WEDKLP", "s52:role": "point" }
28772
29754
  });
28773
29755
  }
28774
- if (objectClass === "SLOGRD" && resolved.type === "fill") {
29756
+ if (objectClass2 === "SLOGRD" && resolved.type === "fill") {
28775
29757
  layers.push({
28776
29758
  id: `s52-SLOGRD-point`,
28777
29759
  type: "circle",
@@ -28788,7 +29770,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28788
29770
  metadata: { "s52:objectClass": "SLOGRD", "s52:role": "point" }
28789
29771
  });
28790
29772
  }
28791
- if (objectClass === "RUNWAY" && resolved.type === "fill") {
29773
+ if (objectClass2 === "RUNWAY" && resolved.type === "fill") {
28792
29774
  layers.push({
28793
29775
  id: `s52-RUNWAY-line`,
28794
29776
  type: "line",
@@ -28802,7 +29784,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28802
29784
  metadata: { "s52:objectClass": "RUNWAY", "s52:role": "line" }
28803
29785
  });
28804
29786
  }
28805
- if (objectClass === "FORSTC" && resolved.type === "fill") {
29787
+ if (objectClass2 === "FORSTC" && resolved.type === "fill") {
28806
29788
  layers.push({
28807
29789
  id: `s52-FORSTC-line`,
28808
29790
  type: "line",
@@ -28816,7 +29798,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28816
29798
  metadata: { "s52:objectClass": "FORSTC", "s52:role": "line" }
28817
29799
  });
28818
29800
  }
28819
- if (objectClass === "FSHFAC" && resolved.type === "fill") {
29801
+ if (objectClass2 === "FSHFAC" && resolved.type === "fill") {
28820
29802
  layers.push({
28821
29803
  id: `s52-FSHFAC-point`,
28822
29804
  type: "circle",
@@ -28832,7 +29814,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28832
29814
  metadata: { "s52:objectClass": "FSHFAC", "s52:role": "point" }
28833
29815
  });
28834
29816
  }
28835
- if (objectClass === "OBSTRN" && resolved.type === "symbol") {
29817
+ if (objectClass2 === "OBSTRN" && resolved.type === "symbol") {
28836
29818
  layers.push({
28837
29819
  id: `s52-OBSTRN-polygon`,
28838
29820
  type: "fill",
@@ -28872,7 +29854,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28872
29854
  metadata: { "s52:objectClass": "OBSTRN", "s52:role": "line" }
28873
29855
  });
28874
29856
  }
28875
- if (objectClass === "SLCONS" && resolved.type === "line") {
29857
+ if (objectClass2 === "SLCONS" && resolved.type === "line") {
28876
29858
  layers.push({
28877
29859
  id: `s52-SLCONS-polygon`,
28878
29860
  type: "fill",
@@ -28912,7 +29894,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28912
29894
  metadata: { "s52:objectClass": "SLCONS", "s52:role": "point" }
28913
29895
  });
28914
29896
  }
28915
- if (objectClass === "PRDARE" && resolved.type === "line") {
29897
+ if (objectClass2 === "PRDARE" && resolved.type === "line") {
28916
29898
  layers.push({
28917
29899
  id: `s52-PRDARE-point`,
28918
29900
  type: "circle",
@@ -28929,7 +29911,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28929
29911
  metadata: { "s52:objectClass": "PRDARE", "s52:role": "point" }
28930
29912
  });
28931
29913
  }
28932
- if (objectClass === "CTNARE" && resolved.type === "line") {
29914
+ if (objectClass2 === "CTNARE" && resolved.type === "line") {
28933
29915
  layers.push({
28934
29916
  id: `s52-CTNARE-point`,
28935
29917
  type: "circle",
@@ -28945,7 +29927,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28945
29927
  metadata: { "s52:objectClass": "CTNARE", "s52:role": "point" }
28946
29928
  });
28947
29929
  }
28948
- if (objectClass === "PONTON" && resolved.type === "line") {
29930
+ if (objectClass2 === "PONTON" && resolved.type === "line") {
28949
29931
  layers.push({
28950
29932
  id: `s52-PONTON-polygon`,
28951
29933
  type: "fill",
@@ -28971,7 +29953,7 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28971
29953
  metadata: { "s52:objectClass": "PONTON", "s52:role": "polygon-outline" }
28972
29954
  });
28973
29955
  }
28974
- if (objectClass === "WRECKS" && resolved.type === "symbol") {
29956
+ if (objectClass2 === "WRECKS" && resolved.type === "symbol") {
28975
29957
  layers.push({
28976
29958
  id: `s52-WRECKS-polygon`,
28977
29959
  type: "fill",
@@ -28985,1179 +29967,729 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
28985
29967
  metadata: { "s52:objectClass": "WRECKS", "s52:role": "polygon" }
28986
29968
  });
28987
29969
  layers.push({
28988
- id: `s52-WRECKS-polygon-outline`,
28989
- type: "line",
28990
- source: sourceId,
28991
- "source-layer": "WRECKS",
28992
- filter: GEOM_FILTERS.fill,
28993
- paint: {
28994
- "line-color": resolveColor("CHBLK", ct),
28995
- "line-width": 1,
28996
- "line-dasharray": [3, 2]
28997
- },
28998
- metadata: { "s52:objectClass": "WRECKS", "s52:role": "polygon-outline" }
28999
- });
29000
- }
29001
- if (objectClass === "WATTUR" && resolved.type === "symbol") {
29002
- layers.push({
29003
- id: `s52-WATTUR-polygon`,
29004
- type: "fill",
29005
- source: sourceId,
29006
- "source-layer": "WATTUR",
29007
- filter: GEOM_FILTERS.fill,
29008
- paint: {
29009
- "fill-color": resolveColor("CHBLK", ct),
29010
- "fill-opacity": 0.05
29011
- },
29012
- metadata: { "s52:objectClass": "WATTUR", "s52:role": "polygon" }
29013
- });
29014
- }
29015
- if (objectClass === "MAGVAR" && resolved.type === "line") {
29016
- layers.push({
29017
- id: "s52-MAGVAR-point",
29018
- type: "circle",
29019
- source: sourceId,
29020
- "source-layer": "MAGVAR",
29021
- filter: GEOM_FILTERS.circle,
29022
- paint: {
29023
- "circle-color": resolveColor("CHMGD", ct),
29024
- "circle-radius": ["interpolate", ["linear"], ["zoom"], 6, 1, 12, 2],
29025
- "circle-stroke-color": resolveColor("CHBLK", ct),
29026
- "circle-stroke-width": 0.4,
29027
- "circle-opacity": 0.4
29028
- },
29029
- metadata: { "s52:objectClass": "MAGVAR", "s52:role": "point" }
29030
- });
29031
- }
29032
- if (objectClass === "DMPGRD" && resolved.type === "fill") {
29033
- layers.push({
29034
- id: `s52-DMPGRD-point`,
29035
- type: "circle",
29036
- source: sourceId,
29037
- "source-layer": "DMPGRD",
29038
- filter: GEOM_FILTERS.circle,
29039
- paint: {
29040
- "circle-color": resolveColor("LITYW", ct),
29041
- "circle-radius": ["interpolate", ["linear"], ["zoom"], 10, 1.5, 14, 2.5, 17, 3.6],
29042
- "circle-stroke-color": resolveColor("CHBLK", ct),
29043
- "circle-stroke-width": 0.5,
29044
- "circle-opacity": 0.7
29045
- },
29046
- metadata: { "s52:objectClass": "DMPGRD", "s52:role": "point" }
29047
- });
29048
- }
29049
- if (objectClass === "DAMCON" && resolved.type === "line") {
29050
- layers.push({
29051
- id: `s52-DAMCON-polygon`,
29052
- type: "fill",
29053
- source: sourceId,
29054
- "source-layer": "DAMCON",
29055
- filter: GEOM_FILTERS.fill,
29056
- paint: {
29057
- "fill-color": resolveColor("CHBRN", ct),
29058
- "fill-opacity": 0.4
29059
- },
29060
- metadata: { "s52:objectClass": "DAMCON", "s52:role": "polygon" }
29061
- });
29062
- layers.push({
29063
- id: `s52-DAMCON-polygon-outline`,
29064
- type: "line",
29065
- source: sourceId,
29066
- "source-layer": "DAMCON",
29067
- filter: GEOM_FILTERS.fill,
29068
- paint: {
29069
- "line-color": resolveColor("CHBLK", ct),
29070
- "line-width": 0.6
29071
- },
29072
- metadata: { "s52:objectClass": "DAMCON", "s52:role": "outline" }
29073
- });
29074
- }
29075
- if (objectClass === "GATCON" && resolved.type === "line") {
29076
- layers.push({
29077
- id: `s52-GATCON-polygon`,
29078
- type: "fill",
29079
- source: sourceId,
29080
- "source-layer": "GATCON",
29081
- filter: GEOM_FILTERS.fill,
29082
- paint: {
29083
- "fill-color": resolveColor("CHBRN", ct),
29084
- "fill-opacity": 0.4
29085
- },
29086
- metadata: { "s52:objectClass": "GATCON", "s52:role": "polygon" }
29087
- });
29088
- layers.push({
29089
- id: `s52-GATCON-polygon-outline`,
29090
- type: "line",
29091
- source: sourceId,
29092
- "source-layer": "GATCON",
29093
- filter: GEOM_FILTERS.fill,
29094
- paint: {
29095
- "line-color": resolveColor("CHBLK", ct),
29096
- "line-width": 0.6
29097
- },
29098
- metadata: { "s52:objectClass": "GATCON", "s52:role": "outline" }
29099
- });
29100
- }
29101
- if (objectClass === "CANALS" && resolved.type === "line") {
29102
- layers.push({
29103
- id: `s52-CANALS-polygon`,
29104
- type: "fill",
29105
- source: sourceId,
29106
- "source-layer": "CANALS",
29107
- filter: GEOM_FILTERS.fill,
29108
- paint: {
29109
- "fill-color": resolveColor("DEPMS", ct),
29110
- "fill-opacity": 0.5
29111
- },
29112
- metadata: { "s52:objectClass": "CANALS", "s52:role": "polygon" }
29113
- });
29114
- }
29115
- if (objectClass === "PILBOP" && resolved.type === "symbol") {
29116
- layers.push({
29117
- id: `s52-PILBOP-polygon`,
29118
- type: "fill",
29119
- source: sourceId,
29120
- "source-layer": "PILBOP",
29121
- filter: GEOM_FILTERS.fill,
29122
- paint: {
29123
- "fill-color": resolveColor("CHMGD", ct),
29124
- "fill-opacity": 0.1
29125
- },
29126
- metadata: { "s52:objectClass": "PILBOP", "s52:role": "polygon" }
29127
- });
29128
- layers.push({
29129
- id: `s52-PILBOP-polygon-outline`,
29970
+ id: `s52-WRECKS-polygon-outline`,
29130
29971
  type: "line",
29131
29972
  source: sourceId,
29132
- "source-layer": "PILBOP",
29973
+ "source-layer": "WRECKS",
29133
29974
  filter: GEOM_FILTERS.fill,
29134
29975
  paint: {
29135
- "line-color": resolveColor("CHMGD", ct),
29136
- "line-width": 0.8,
29137
- "line-dasharray": [4, 2]
29976
+ "line-color": resolveColor("CHBLK", ct),
29977
+ "line-width": 1,
29978
+ "line-dasharray": [3, 2]
29138
29979
  },
29139
- metadata: { "s52:objectClass": "PILBOP", "s52:role": "outline" }
29980
+ metadata: { "s52:objectClass": "WRECKS", "s52:role": "polygon-outline" }
29140
29981
  });
29141
29982
  }
29142
- if (objectClass === "HRBFAC" && resolved.type === "circle") {
29983
+ if (objectClass2 === "WATTUR" && resolved.type === "symbol") {
29143
29984
  layers.push({
29144
- id: `s52-HRBFAC-polygon`,
29985
+ id: `s52-WATTUR-polygon`,
29145
29986
  type: "fill",
29146
29987
  source: sourceId,
29147
- "source-layer": "HRBFAC",
29988
+ "source-layer": "WATTUR",
29148
29989
  filter: GEOM_FILTERS.fill,
29149
29990
  paint: {
29150
- "fill-color": resolveColor("TRFCD", ct),
29151
- "fill-opacity": 0.15
29991
+ "fill-color": resolveColor("CHBLK", ct),
29992
+ "fill-opacity": 0.05
29152
29993
  },
29153
- metadata: { "s52:objectClass": "HRBFAC", "s52:role": "polygon" }
29994
+ metadata: { "s52:objectClass": "WATTUR", "s52:role": "polygon" }
29154
29995
  });
29155
29996
  }
29156
- if (objectClass === "MARCUL" && resolved.type === "fill") {
29997
+ if (objectClass2 === "MAGVAR" && resolved.type === "line") {
29157
29998
  layers.push({
29158
- id: `s52-MARCUL-point`,
29999
+ id: "s52-MAGVAR-point",
29159
30000
  type: "circle",
29160
30001
  source: sourceId,
29161
- "source-layer": "MARCUL",
30002
+ "source-layer": "MAGVAR",
29162
30003
  filter: GEOM_FILTERS.circle,
29163
30004
  paint: {
29164
- "circle-color": resolveColor("CHBRN", ct),
29165
- "circle-radius": ["interpolate", ["linear"], ["zoom"], 12, 1.2, 14, 1.8, 17, 2.6],
30005
+ "circle-color": resolveColor("CHMGD", ct),
30006
+ "circle-radius": ["interpolate", ["linear"], ["zoom"], 6, 1, 12, 2],
29166
30007
  "circle-stroke-color": resolveColor("CHBLK", ct),
29167
30008
  "circle-stroke-width": 0.4,
29168
- "circle-opacity": 0.6
30009
+ "circle-opacity": 0.4
29169
30010
  },
29170
- metadata: { "s52:objectClass": "MARCUL", "s52:role": "point" }
30011
+ metadata: { "s52:objectClass": "MAGVAR", "s52:role": "point" }
29171
30012
  });
30013
+ }
30014
+ if (objectClass2 === "DMPGRD" && resolved.type === "fill") {
29172
30015
  layers.push({
29173
- id: `s52-MARCUL-line`,
29174
- type: "line",
30016
+ id: `s52-DMPGRD-point`,
30017
+ type: "circle",
29175
30018
  source: sourceId,
29176
- "source-layer": "MARCUL",
29177
- filter: GEOM_FILTERS.line,
30019
+ "source-layer": "DMPGRD",
30020
+ filter: GEOM_FILTERS.circle,
29178
30021
  paint: {
29179
- "line-color": resolveColor("CHBLK", ct),
29180
- "line-width": 0.5,
29181
- "line-dasharray": [2, 2]
30022
+ "circle-color": resolveColor("LITYW", ct),
30023
+ "circle-radius": ["interpolate", ["linear"], ["zoom"], 10, 1.5, 14, 2.5, 17, 3.6],
30024
+ "circle-stroke-color": resolveColor("CHBLK", ct),
30025
+ "circle-stroke-width": 0.5,
30026
+ "circle-opacity": 0.7
29182
30027
  },
29183
- metadata: { "s52:objectClass": "MARCUL", "s52:role": "line" }
30028
+ metadata: { "s52:objectClass": "DMPGRD", "s52:role": "point" }
29184
30029
  });
29185
30030
  }
29186
- if (objectClass === "RECTRC" && resolved.type === "line") {
30031
+ if (objectClass2 === "DAMCON" && resolved.type === "line") {
29187
30032
  layers.push({
29188
- id: `s52-RECTRC-polygon`,
30033
+ id: `s52-DAMCON-polygon`,
29189
30034
  type: "fill",
29190
30035
  source: sourceId,
29191
- "source-layer": "RECTRC",
30036
+ "source-layer": "DAMCON",
29192
30037
  filter: GEOM_FILTERS.fill,
29193
30038
  paint: {
29194
- "fill-color": resolveColor("TRFCD", ct),
29195
- "fill-opacity": 0.05
30039
+ "fill-color": resolveColor("CHBRN", ct),
30040
+ "fill-opacity": 0.4
29196
30041
  },
29197
- metadata: { "s52:objectClass": "RECTRC", "s52:role": "polygon" }
30042
+ metadata: { "s52:objectClass": "DAMCON", "s52:role": "polygon" }
29198
30043
  });
29199
- }
29200
- if (objectClass === "PONTON" && resolved.type === "fill") {
29201
30044
  layers.push({
29202
- id: `s52-PONTON-line`,
30045
+ id: `s52-DAMCON-polygon-outline`,
29203
30046
  type: "line",
29204
30047
  source: sourceId,
29205
- "source-layer": "PONTON",
29206
- filter: GEOM_FILTERS.line,
30048
+ "source-layer": "DAMCON",
30049
+ filter: GEOM_FILTERS.fill,
29207
30050
  paint: {
29208
- "line-color": resolveColor("CHGRF", ct),
29209
- "line-width": 1.5
30051
+ "line-color": resolveColor("CHBLK", ct),
30052
+ "line-width": 0.6
29210
30053
  },
29211
- metadata: { "s52:objectClass": "PONTON", "s52:role": "line" }
30054
+ metadata: { "s52:objectClass": "DAMCON", "s52:role": "outline" }
29212
30055
  });
29213
30056
  }
29214
- if (objectClass === "SILTNK" && resolved.type === "symbol") {
30057
+ if (objectClass2 === "GATCON" && resolved.type === "line") {
29215
30058
  layers.push({
29216
- id: `s52-SILTNK-symbol-polygon`,
29217
- type: "symbol",
30059
+ id: `s52-GATCON-polygon`,
30060
+ type: "fill",
29218
30061
  source: sourceId,
29219
- "source-layer": "SILTNK",
30062
+ "source-layer": "GATCON",
29220
30063
  filter: GEOM_FILTERS.fill,
29221
- layout: {
29222
- "icon-image": "TNKCON02",
29223
- "icon-size": 0.6,
29224
- "icon-allow-overlap": true,
29225
- "icon-ignore-placement": false,
29226
- "symbol-placement": "point"
30064
+ paint: {
30065
+ "fill-color": resolveColor("CHBRN", ct),
30066
+ "fill-opacity": 0.4
29227
30067
  },
29228
- paint: {},
29229
- metadata: { "s52:objectClass": "SILTNK", "s52:role": "symbol-polygon" }
30068
+ metadata: { "s52:objectClass": "GATCON", "s52:role": "polygon" }
29230
30069
  });
29231
- }
29232
- if (objectClass === "ACHBRT" && resolved.type === "symbol") {
29233
30070
  layers.push({
29234
- id: `s52-ACHBRT-symbol-polygon`,
29235
- type: "symbol",
30071
+ id: `s52-GATCON-polygon-outline`,
30072
+ type: "line",
29236
30073
  source: sourceId,
29237
- "source-layer": "ACHBRT",
30074
+ "source-layer": "GATCON",
29238
30075
  filter: GEOM_FILTERS.fill,
29239
- layout: {
29240
- "icon-image": "ACHARE02",
29241
- "icon-size": 0.5,
29242
- "icon-allow-overlap": true,
29243
- "icon-ignore-placement": false,
29244
- "symbol-placement": "point"
29245
- },
29246
- paint: {},
29247
- metadata: { "s52:objectClass": "ACHBRT", "s52:role": "symbol-polygon" }
29248
- });
29249
- }
29250
- if (objectClass === "BERTHS" && resolved.type === "line") {
29251
- layers.push({
29252
- id: `s52-BERTHS-circle`,
29253
- type: "circle",
29254
- source: sourceId,
29255
- "source-layer": "BERTHS",
29256
- filter: GEOM_FILTERS.circle,
29257
30076
  paint: {
29258
- "circle-color": resolveColor("CHBLK", ct),
29259
- "circle-radius": ["interpolate", ["linear"], ["zoom"], 12, 1.5, 14, 2.2, 17, 3],
29260
- "circle-stroke-color": resolveColor("CHBLK", ct),
29261
- "circle-stroke-width": 0.5
30077
+ "line-color": resolveColor("CHBLK", ct),
30078
+ "line-width": 0.6
29262
30079
  },
29263
- metadata: { "s52:objectClass": "BERTHS", "s52:role": "point" }
30080
+ metadata: { "s52:objectClass": "GATCON", "s52:role": "outline" }
29264
30081
  });
29265
30082
  }
29266
- if (objectClass === "TUNNEL" && resolved.type === "line") {
30083
+ if (objectClass2 === "CANALS" && resolved.type === "line") {
29267
30084
  layers.push({
29268
- // NOT `s52-TUNNEL-line` — that id is already taken by the main layer
29269
- // (`s52-${objectClass}-${resolved.type}`), and duplicate layer ids
29270
- // are a Mapbox style-spec violation (caught by style-integrity.test).
29271
- id: `s52-TUNNEL-linestring`,
29272
- type: "line",
30085
+ id: `s52-CANALS-polygon`,
30086
+ type: "fill",
29273
30087
  source: sourceId,
29274
- "source-layer": "TUNNEL",
29275
- filter: GEOM_FILTERS.line,
30088
+ "source-layer": "CANALS",
30089
+ filter: GEOM_FILTERS.fill,
29276
30090
  paint: {
29277
- "line-color": resolveColor("CHGRD", ct),
29278
- "line-width": 1,
29279
- "line-dasharray": [2, 2]
30091
+ "fill-color": resolveColor("DEPMS", ct),
30092
+ "fill-opacity": 0.5
29280
30093
  },
29281
- metadata: { "s52:objectClass": "TUNNEL", "s52:role": "line" }
29282
- });
29283
- }
29284
- }
29285
- return layers;
29286
- }
29287
- function buildLabelLayer2(objectClass, ct, sourceId = DEFAULT_REGION, depthUnits = "meters", labelOpts = {}) {
29288
- if (!LABELED_LAYERS.has(objectClass)) return null;
29289
- const config = getLabelConfig(objectClass, {
29290
- colorTable: ct,
29291
- safetyContour: labelOpts.safetyContour,
29292
- twoShadeSoundings: labelOpts.twoShadeSoundings,
29293
- soundingFontWeight: labelOpts.soundingFontWeight
29294
- });
29295
- if (!config) return null;
29296
- let textField = config.textField;
29297
- if (objectClass === "LIGHTS") {
29298
- textField = buildLightLabelExpression();
29299
- }
29300
- if (typeof textField !== "string" && Array.isArray(textField)) {
29301
- const flat = JSON.stringify(textField);
29302
- if (flat.includes("_navAidLabel")) {
29303
- textField = buildNavAidLabelExpression();
29304
- }
29305
- }
29306
- if (depthUnits === "feet") {
29307
- if (objectClass === "SOUNDG") {
29308
- textField = [
29309
- "concat",
29310
- [
29311
- "round",
29312
- [
29313
- "*",
29314
- [
29315
- "to-number",
29316
- ["coalesce", ["get", "depth"], ["get", "DEPTH"], ["get", "VALSOU"]],
29317
- 0
29318
- ],
29319
- 3.28084
29320
- ]
29321
- ],
29322
- ""
29323
- ];
29324
- } else if (objectClass === "DEPCNT") {
29325
- textField = [
29326
- "concat",
29327
- ["round", ["*", ["to-number", ["get", "VALDCO"]], 3.28084]],
29328
- "ft"
29329
- ];
29330
- } else if (objectClass === "UWTROC" || objectClass === "OBSTRN" || objectClass === "WRECKS") {
29331
- const valsouFt = [
29332
- "to-number",
29333
- ["coalesce", ["get", "VALSOU"], ["get", "valsou"], 0],
29334
- 0
29335
- ];
29336
- textField = [
29337
- "case",
29338
- [">", valsouFt, 0],
29339
- ["concat", ["to-string", ["round", ["*", valsouFt, 3.28084]]], "ft"],
29340
- ""
29341
- ];
29342
- }
29343
- }
29344
- const labelPriority = getLabelPriority(objectClass);
29345
- const layer = {
29346
- id: `s52-${objectClass}-label`,
29347
- type: "symbol",
29348
- source: sourceId,
29349
- "source-layer": objectClass,
29350
- filter: GEOM_FILTERS.symbol,
29351
- layout: {
29352
- "text-field": textField,
29353
- "text-size": config.textSize,
29354
- "text-font": config.textFont ?? ["Open Sans Regular"],
29355
- "text-offset": config.textOffset,
29356
- "text-anchor": "top",
29357
- "text-allow-overlap": false,
29358
- "text-optional": true,
29359
- "text-padding": 4,
29360
- "symbol-sort-key": labelPriority * 1e3,
29361
- ...config.layout ?? {}
29362
- },
29363
- paint: {
29364
- "text-color": config.textColor,
29365
- "text-halo-color": config.textHaloColor,
29366
- "text-halo-width": config.textHaloWidth
29367
- },
29368
- metadata: {
29369
- "s52:objectClass": objectClass,
29370
- "chrt:labelPriority": labelPriority
29371
- }
29372
- };
29373
- if (config.minzoom) {
29374
- layer.minzoom = config.minzoom;
29375
- }
29376
- if (objectClass === "DEPCNT") {
29377
- layer.filter = GEOM_FILTERS.line;
29378
- }
29379
- if (objectClass === "CBLOHD" || objectClass === "PIPOHD") {
29380
- layer.filter = GEOM_FILTERS.line;
29381
- }
29382
- if (objectClass === "SBDARE") {
29383
- delete layer.filter;
29384
- }
29385
- if (objectClass === "MAGVAR") {
29386
- delete layer.filter;
29387
- }
29388
- if (getLabelBehavior(objectClass) === "orientation-cross-band") {
29389
- layer.filter = LABEL_ANCHOR_FILTER;
29390
- }
29391
- return applyDepthLabelSize(objectClass, layer, labelOpts.depthLabelSize);
29392
- }
29393
- function getGeomTypesForClass(objectClass, defaultType) {
29394
- if (SYMBOL_MAP[objectClass] && defaultType === "symbol") {
29395
- return ["symbol"];
29396
- }
29397
- if (objectClass === "SOUNDG") {
29398
- return ["circle"];
29399
- }
29400
- if (defaultType) {
29401
- return [defaultType];
29402
- }
29403
- return ["circle"];
29404
- }
29405
- function getOrderedClasses() {
29406
- const ordered = [...S52_DRAW_ORDER];
29407
- for (const key of Object.keys(S52_LOOKUP)) {
29408
- if (!ordered.includes(key)) {
29409
- const insertIdx = ordered.indexOf("SOUNDG");
29410
- if (insertIdx >= 0) {
29411
- ordered.splice(insertIdx, 0, key);
29412
- } else {
29413
- ordered.push(key);
29414
- }
30094
+ metadata: { "s52:objectClass": "CANALS", "s52:role": "polygon" }
30095
+ });
29415
30096
  }
29416
- }
29417
- return ordered;
29418
- }
29419
-
29420
- // src/atlas/terrain-order.ts
29421
- var DRAPEABLE_LAYER_TYPES = /* @__PURE__ */ new Set([
29422
- "background",
29423
- "fill",
29424
- "line",
29425
- "raster",
29426
- "hillshade",
29427
- "color-relief"
29428
- ]);
29429
- function isDrapeable(layer) {
29430
- return DRAPEABLE_LAYER_TYPES.has(layer.type);
29431
- }
29432
- function drapedFirst(layers) {
29433
- const draped = [];
29434
- const direct = [];
29435
- for (const l of layers) (isDrapeable(l) ? draped : direct).push(l);
29436
- return [...draped, ...direct];
29437
- }
29438
- function terrainStackCount(layers) {
29439
- let stacks = 0;
29440
- let prevDraped = false;
29441
- for (const l of layers) {
29442
- const d = isDrapeable(l);
29443
- if (d && !prevDraped) stacks++;
29444
- prevDraped = d;
29445
- }
29446
- return stacks;
29447
- }
29448
- function drapeHalves(layers) {
29449
- const draped = [];
29450
- const direct = [];
29451
- for (const l of layers) (isDrapeable(l) ? draped : direct).push(l);
29452
- return { draped, direct };
29453
- }
29454
-
29455
- // src/atlas/lamps.ts
29456
- var LAMP_GLOW_LAYER_ID = "s52-LIGHTS-lamp-glow";
29457
- var LAMP_CORE_LAYER_ID = "s52-LIGHTS-lamp-core";
29458
- var LAMP_LAYER_IDS = [LAMP_GLOW_LAYER_ID, LAMP_CORE_LAYER_ID];
29459
- var RANGE_NM = ["to-number", ["coalesce", ["get", "VALNMR"], ["get", "valnmr"]], 6];
29460
- var UNSECTORED_POINTS = [
29461
- "all",
29462
- ["any", ["==", ["geometry-type"], "Point"], ["==", ["geometry-type"], "MultiPoint"]],
29463
- ["!", ["has", "SECTR1"]],
29464
- // …and not lights with a charted rhythm either: those flash, and the
29465
- // flashing is computed by the app from `SIGSEQ` (light-sigseq.ts). What is
29466
- // left here is exactly the set that is steady from every bearing.
29467
- ["!", ["has", "SIGSEQ"]]
29468
- ];
29469
- function buildLampLayers(sourceId, ct) {
29470
- const colour = lightFillExpression(ct);
29471
- const common = {
29472
- type: "circle",
29473
- source: sourceId,
29474
- "source-layer": "LIGHTS",
29475
- filter: UNSECTORED_POINTS,
29476
- layout: { visibility: "none" }
29477
- };
29478
- const metadata = { "chrt:role": "lamp", "s52:objectClass": "LIGHTS" };
29479
- return [
29480
- {
29481
- ...common,
29482
- id: LAMP_GLOW_LAYER_ID,
29483
- paint: {
29484
- "circle-color": colour,
29485
- // 4 nm → 7 px, 12 nm → 12 px, 25 nm → 20 px; capped, because a
29486
- // primary seacoast light is not a searchlight.
29487
- "circle-radius": ["interpolate", ["linear"], RANGE_NM, 0, 5, 4, 7, 12, 12, 25, 20],
29488
- "circle-blur": 1,
29489
- "circle-opacity": 0.55,
29490
- // A lamp is a point of light: it does not grow as it comes near, and
29491
- // it does not lie down with the chart.
29492
- "circle-pitch-alignment": "viewport",
29493
- "circle-pitch-scale": "viewport"
29494
- },
29495
- metadata: { ...metadata, "chrt:lamp": "glow" }
29496
- },
29497
- {
29498
- ...common,
29499
- id: LAMP_CORE_LAYER_ID,
29500
- paint: {
29501
- "circle-color": colour,
29502
- "circle-radius": ["interpolate", ["linear"], RANGE_NM, 0, 1.5, 12, 2.5, 25, 3.5],
29503
- "circle-blur": 0.25,
29504
- "circle-opacity": 1,
29505
- "circle-pitch-alignment": "viewport",
29506
- "circle-pitch-scale": "viewport"
29507
- },
29508
- metadata: { ...metadata, "chrt:lamp": "core" }
30097
+ if (objectClass2 === "PILBOP" && resolved.type === "symbol") {
30098
+ layers.push({
30099
+ id: `s52-PILBOP-polygon`,
30100
+ type: "fill",
30101
+ source: sourceId,
30102
+ "source-layer": "PILBOP",
30103
+ filter: GEOM_FILTERS.fill,
30104
+ paint: {
30105
+ "fill-color": resolveColor("CHMGD", ct),
30106
+ "fill-opacity": 0.1
30107
+ },
30108
+ metadata: { "s52:objectClass": "PILBOP", "s52:role": "polygon" }
30109
+ });
30110
+ layers.push({
30111
+ id: `s52-PILBOP-polygon-outline`,
30112
+ type: "line",
30113
+ source: sourceId,
30114
+ "source-layer": "PILBOP",
30115
+ filter: GEOM_FILTERS.fill,
30116
+ paint: {
30117
+ "line-color": resolveColor("CHMGD", ct),
30118
+ "line-width": 0.8,
30119
+ "line-dasharray": [4, 2]
30120
+ },
30121
+ metadata: { "s52:objectClass": "PILBOP", "s52:role": "outline" }
30122
+ });
30123
+ }
30124
+ if (objectClass2 === "HRBFAC" && resolved.type === "circle") {
30125
+ layers.push({
30126
+ id: `s52-HRBFAC-polygon`,
30127
+ type: "fill",
30128
+ source: sourceId,
30129
+ "source-layer": "HRBFAC",
30130
+ filter: GEOM_FILTERS.fill,
30131
+ paint: {
30132
+ "fill-color": resolveColor("TRFCD", ct),
30133
+ "fill-opacity": 0.15
30134
+ },
30135
+ metadata: { "s52:objectClass": "HRBFAC", "s52:role": "polygon" }
30136
+ });
30137
+ }
30138
+ if (objectClass2 === "MARCUL" && resolved.type === "fill") {
30139
+ layers.push({
30140
+ id: `s52-MARCUL-point`,
30141
+ type: "circle",
30142
+ source: sourceId,
30143
+ "source-layer": "MARCUL",
30144
+ filter: GEOM_FILTERS.circle,
30145
+ paint: {
30146
+ "circle-color": resolveColor("CHBRN", ct),
30147
+ "circle-radius": ["interpolate", ["linear"], ["zoom"], 12, 1.2, 14, 1.8, 17, 2.6],
30148
+ "circle-stroke-color": resolveColor("CHBLK", ct),
30149
+ "circle-stroke-width": 0.4,
30150
+ "circle-opacity": 0.6
30151
+ },
30152
+ metadata: { "s52:objectClass": "MARCUL", "s52:role": "point" }
30153
+ });
30154
+ layers.push({
30155
+ id: `s52-MARCUL-line`,
30156
+ type: "line",
30157
+ source: sourceId,
30158
+ "source-layer": "MARCUL",
30159
+ filter: GEOM_FILTERS.line,
30160
+ paint: {
30161
+ "line-color": resolveColor("CHBLK", ct),
30162
+ "line-width": 0.5,
30163
+ "line-dasharray": [2, 2]
30164
+ },
30165
+ metadata: { "s52:objectClass": "MARCUL", "s52:role": "line" }
30166
+ });
30167
+ }
30168
+ if (objectClass2 === "RECTRC" && resolved.type === "line") {
30169
+ layers.push({
30170
+ id: `s52-RECTRC-polygon`,
30171
+ type: "fill",
30172
+ source: sourceId,
30173
+ "source-layer": "RECTRC",
30174
+ filter: GEOM_FILTERS.fill,
30175
+ paint: {
30176
+ "fill-color": resolveColor("TRFCD", ct),
30177
+ "fill-opacity": 0.05
30178
+ },
30179
+ metadata: { "s52:objectClass": "RECTRC", "s52:role": "polygon" }
30180
+ });
30181
+ }
30182
+ if (objectClass2 === "PONTON" && resolved.type === "fill") {
30183
+ layers.push({
30184
+ id: `s52-PONTON-line`,
30185
+ type: "line",
30186
+ source: sourceId,
30187
+ "source-layer": "PONTON",
30188
+ filter: GEOM_FILTERS.line,
30189
+ paint: {
30190
+ "line-color": resolveColor("CHGRF", ct),
30191
+ "line-width": 1.5
30192
+ },
30193
+ metadata: { "s52:objectClass": "PONTON", "s52:role": "line" }
30194
+ });
30195
+ }
30196
+ if (objectClass2 === "SILTNK" && resolved.type === "symbol") {
30197
+ layers.push({
30198
+ id: `s52-SILTNK-symbol-polygon`,
30199
+ type: "symbol",
30200
+ source: sourceId,
30201
+ "source-layer": "SILTNK",
30202
+ filter: GEOM_FILTERS.fill,
30203
+ layout: {
30204
+ "icon-image": "TNKCON02",
30205
+ "icon-size": 0.6,
30206
+ "icon-allow-overlap": true,
30207
+ "icon-ignore-placement": false,
30208
+ "symbol-placement": "point"
30209
+ },
30210
+ paint: {},
30211
+ metadata: { "s52:objectClass": "SILTNK", "s52:role": "symbol-polygon" }
30212
+ });
30213
+ }
30214
+ if (objectClass2 === "ACHBRT" && resolved.type === "symbol") {
30215
+ layers.push({
30216
+ id: `s52-ACHBRT-symbol-polygon`,
30217
+ type: "symbol",
30218
+ source: sourceId,
30219
+ "source-layer": "ACHBRT",
30220
+ filter: GEOM_FILTERS.fill,
30221
+ layout: {
30222
+ "icon-image": "ACHARE02",
30223
+ "icon-size": 0.5,
30224
+ "icon-allow-overlap": true,
30225
+ "icon-ignore-placement": false,
30226
+ "symbol-placement": "point"
30227
+ },
30228
+ paint: {},
30229
+ metadata: { "s52:objectClass": "ACHBRT", "s52:role": "symbol-polygon" }
30230
+ });
30231
+ }
30232
+ if (objectClass2 === "BERTHS" && resolved.type === "line") {
30233
+ layers.push({
30234
+ id: `s52-BERTHS-circle`,
30235
+ type: "circle",
30236
+ source: sourceId,
30237
+ "source-layer": "BERTHS",
30238
+ filter: GEOM_FILTERS.circle,
30239
+ paint: {
30240
+ "circle-color": resolveColor("CHBLK", ct),
30241
+ "circle-radius": ["interpolate", ["linear"], ["zoom"], 12, 1.5, 14, 2.2, 17, 3],
30242
+ "circle-stroke-color": resolveColor("CHBLK", ct),
30243
+ "circle-stroke-width": 0.5
30244
+ },
30245
+ metadata: { "s52:objectClass": "BERTHS", "s52:role": "point" }
30246
+ });
30247
+ }
30248
+ if (objectClass2 === "TUNNEL" && resolved.type === "line") {
30249
+ layers.push({
30250
+ // NOT `s52-TUNNEL-line` — that id is already taken by the main layer
30251
+ // (`s52-${objectClass}-${resolved.type}`), and duplicate layer ids
30252
+ // are a Mapbox style-spec violation (caught by style-integrity.test).
30253
+ id: `s52-TUNNEL-linestring`,
30254
+ type: "line",
30255
+ source: sourceId,
30256
+ "source-layer": "TUNNEL",
30257
+ filter: GEOM_FILTERS.line,
30258
+ paint: {
30259
+ "line-color": resolveColor("CHGRD", ct),
30260
+ "line-width": 1,
30261
+ "line-dasharray": [2, 2]
30262
+ },
30263
+ metadata: { "s52:objectClass": "TUNNEL", "s52:role": "line" }
30264
+ });
29509
30265
  }
29510
- ];
29511
- }
29512
-
29513
- // src/atlas/band-priority.ts
29514
- var BAND_PRIORITY_RANGES = {
29515
- "1": { minzoom: 0 },
29516
- "2": { minzoom: 5 },
29517
- "3": { minzoom: 7 },
29518
- "4": { minzoom: 9 },
29519
- "5": { minzoom: 11 },
29520
- "6": { minzoom: 11 }
29521
- };
29522
- var BAND_DATA_MINZOOM = {
29523
- "1": 0,
29524
- "2": 5,
29525
- "3": 7,
29526
- "4": 9,
29527
- "5": 11,
29528
- "6": 13
29529
- };
29530
- function dominatedVariantMaxzoom(D) {
29531
- return Math.max(BAND_PRIORITY_RANGES[D].minzoom, BAND_DATA_MINZOOM[D]);
29532
- }
29533
- var DOMINANCE_EXEMPT_FILL_CLASSES = /* @__PURE__ */ new Set([
29534
- "LNDARE",
29535
- "DEPARE",
29536
- "UNSARE",
29537
- "LAKARE",
29538
- "DRGARE",
29539
- "SEAARE"
29540
- ]);
29541
- function isExemptOpaqueFill(layer) {
29542
- if (layer.type !== "fill") return false;
29543
- for (const cls of DOMINANCE_EXEMPT_FILL_CLASSES) {
29544
- if (layer.id.startsWith(`s52-${cls}-`)) return true;
29545
- }
29546
- return false;
29547
- }
29548
- var GROUND_FILL_CLASSES = /* @__PURE__ */ new Set([
29549
- ...DOMINANCE_EXEMPT_FILL_CLASSES,
29550
- "RIVERS"
29551
- ]);
29552
- function isGroundFill(layer) {
29553
- if (layer.type !== "fill") return false;
29554
- for (const cls of GROUND_FILL_CLASSES) {
29555
- if (layer.id.startsWith(`s52-${cls}-`)) return true;
29556
- }
29557
- return false;
29558
- }
29559
- var FILL_FLOOR_FREE_BANDS = /* @__PURE__ */ new Set([
29560
- "1",
29561
- "2",
29562
- "3"
29563
- ]);
29564
- function bandVariantMinzoom(layer, B) {
29565
- const classMinzoom = layer.minzoom ?? 0;
29566
- if (isGroundFill(layer) && FILL_FLOOR_FREE_BANDS.has(B)) {
29567
- return 0;
29568
- }
29569
- if (isGroundFill(layer)) {
29570
- return BAND_PRIORITY_RANGES[B].minzoom;
29571
- }
29572
- return Math.max(classMinzoom, BAND_PRIORITY_RANGES[B].minzoom);
29573
- }
29574
- var GENERALISED_AREA_FILL_CLASSES = /* @__PURE__ */ new Set([
29575
- "BUAARE",
29576
- // 2026-08-28, New York Harbor: the "New York Marine Inspection Zone and
29577
- // Captain of the Port Zone" RESARE exists at bands 2/3 as a generalized
29578
- // shore-hugging polygon whose point-sampled dominance tag MISSED (the
29579
- // Providence-BUAARE failure mode), so its magenta dashed boundary drew
29580
- // OFFSET over the band-5 detail at z11.5 — read in the field as "a
29581
- // magenta shoreline bleeding down from a less coarse band". The same
29582
- // tile carried untagged band-2 ADMARE and CTNARE giants in the same
29583
- // state. All three are area classes whose coarse-band boundaries hug
29584
- // shorelines and whose fine-band copies are authoritative.
29585
- "RESARE",
29586
- "CTNARE",
29587
- "ADMARE"
29588
- ]);
29589
- var GENERALISED_AREA_MAX_BAND = 3;
29590
- function generalisedAreaCeiling(layer, B) {
29591
- if (layer.type !== "fill" && layer.type !== "line") return null;
29592
- if (parseInt(B, 10) > GENERALISED_AREA_MAX_BAND) return null;
29593
- for (const cls of GENERALISED_AREA_FILL_CLASSES) {
29594
- if (layer.id.startsWith(`s52-${cls}-`)) return BAND_PRIORITY_RANGES["5"].minzoom;
29595
- }
29596
- return null;
29597
- }
29598
- var BAND_DRAW_ORDER = ["1", "2", "3", "4", "5", "6"];
29599
- var CHART_BAND_CODES = {
29600
- Overview: ["1"],
29601
- General: ["2"],
29602
- Coastal: ["3"],
29603
- Approach: ["4"],
29604
- Harbor: ["5", "6"]
29605
- };
29606
- var CHART_CASCADE_CODES = {
29607
- Overview: ["1"],
29608
- General: ["1", "2"],
29609
- Coastal: ["1", "2", "3"],
29610
- Approach: ["1", "2", "3", "4"],
29611
- Harbor: ["1", "2", "3", "4", "5", "6"]
29612
- };
29613
- var CHRT_STYLE_MODES = [
29614
- "continuous",
29615
- ...Object.keys(CHART_BAND_CODES).map(
29616
- (band) => `chart:${band}`
29617
- )
29618
- ];
29619
- function normalizeStyleMode(raw) {
29620
- if (raw === null || raw === void 0 || raw === "") return "continuous";
29621
- const lower = raw.toLowerCase();
29622
- return CHRT_STYLE_MODES.find((mode) => mode.toLowerCase() === lower) ?? null;
29623
- }
29624
- function styleModeBand(mode) {
29625
- return mode === "continuous" ? null : mode.slice("chart:".length);
29626
- }
29627
- function bandFilterExpression(codes) {
29628
- return cellBandMatch(["slice", ["get", "_cell_id"], 2, 3], codes);
29629
- }
29630
- var BAND_CODE_ALIASES = {
29631
- "5": ["7"]
29632
- };
29633
- function cellBandCharacters(code) {
29634
- return [code, ...BAND_CODE_ALIASES[code] ?? []];
29635
- }
29636
- function cellBandMatch(cellBandExpr, codes) {
29637
- const list = Array.isArray(codes) ? codes : [codes];
29638
- const chars = [...new Set(list.flatMap((c) => cellBandCharacters(c)))];
29639
- if (chars.length === 1) {
29640
- return ["==", cellBandExpr, chars[0]];
29641
30266
  }
29642
- return ["in", cellBandExpr, ["literal", chars]];
30267
+ return layers;
29643
30268
  }
29644
- function combineWithBandFilter(baseFilter, bandFilter) {
29645
- if (!baseFilter) return bandFilter;
29646
- if (baseFilter[0] === "all") {
29647
- return ["all", ...baseFilter.slice(1), bandFilter];
30269
+ function buildLabelLayer2(objectClass2, ct, sourceId = DEFAULT_REGION, depthUnits = "meters", labelOpts = {}) {
30270
+ if (!LABELED_LAYERS.has(objectClass2)) return null;
30271
+ const config = getLabelConfig(objectClass2, {
30272
+ colorTable: ct,
30273
+ safetyContour: labelOpts.safetyContour,
30274
+ twoShadeSoundings: labelOpts.twoShadeSoundings,
30275
+ soundingFontWeight: labelOpts.soundingFontWeight
30276
+ });
30277
+ if (!config) return null;
30278
+ let textField = config.textField;
30279
+ if (objectClass2 === "LIGHTS") {
30280
+ textField = buildLightLabelExpression();
29648
30281
  }
29649
- return ["all", baseFilter, bandFilter];
29650
- }
29651
- var NOT_DOMINATED_FILTER = [
29652
- "all",
29653
- ["!", ["has", "_dominated_by_min"]],
29654
- ["!=", ["get", "_dominated"], true]
29655
- ];
29656
- function dominatedByMinFilter(D) {
29657
- const dInt = parseInt(D, 10);
29658
- if (D === "6") {
29659
- return [
29660
- "any",
29661
- ["==", ["get", "_dominated_by_min"], dInt],
29662
- ["==", ["get", "_dominated"], true]
29663
- ];
30282
+ if (typeof textField !== "string" && Array.isArray(textField)) {
30283
+ const flat = JSON.stringify(textField);
30284
+ if (flat.includes("_navAidLabel")) {
30285
+ textField = buildNavAidLabelExpression();
30286
+ }
29664
30287
  }
29665
- return ["==", ["get", "_dominated_by_min"], dInt];
29666
- }
29667
- function withinBboxFilter(bbox) {
29668
- const [w, s] = bbox.sw;
29669
- const [e, n] = bbox.ne;
29670
- return [
29671
- "within",
29672
- {
29673
- type: "Polygon",
29674
- coordinates: [
30288
+ if (depthUnits === "feet") {
30289
+ if (objectClass2 === "SOUNDG") {
30290
+ textField = [
30291
+ "concat",
29675
30292
  [
29676
- [w, s],
29677
- [e, s],
29678
- [e, n],
29679
- [w, n],
29680
- [w, s]
29681
- ]
29682
- ]
30293
+ "round",
30294
+ [
30295
+ "*",
30296
+ [
30297
+ "to-number",
30298
+ ["coalesce", ["get", "depth"], ["get", "DEPTH"], ["get", "VALSOU"]],
30299
+ 0
30300
+ ],
30301
+ 3.28084
30302
+ ]
30303
+ ],
30304
+ ""
30305
+ ];
30306
+ } else if (objectClass2 === "DEPCNT") {
30307
+ textField = [
30308
+ "concat",
30309
+ ["round", ["*", ["to-number", ["get", "VALDCO"]], 3.28084]],
30310
+ "ft"
30311
+ ];
30312
+ } else if (objectClass2 === "UWTROC" || objectClass2 === "OBSTRN" || objectClass2 === "WRECKS") {
30313
+ const valsouFt = [
30314
+ "to-number",
30315
+ ["coalesce", ["get", "VALSOU"], ["get", "valsou"], 0],
30316
+ 0
30317
+ ];
30318
+ textField = [
30319
+ "case",
30320
+ [">", valsouFt, 0],
30321
+ ["concat", ["to-string", ["round", ["*", valsouFt, 3.28084]]], "ft"],
30322
+ ""
30323
+ ];
30324
+ }
30325
+ }
30326
+ const labelPriority = getLabelPriority(objectClass2);
30327
+ const layer = {
30328
+ id: `s52-${objectClass2}-label`,
30329
+ type: "symbol",
30330
+ source: sourceId,
30331
+ "source-layer": objectClass2,
30332
+ filter: GEOM_FILTERS.symbol,
30333
+ layout: {
30334
+ "text-field": textField,
30335
+ "text-size": config.textSize,
30336
+ "text-font": config.textFont ?? ["Open Sans Regular"],
30337
+ "text-offset": config.textOffset,
30338
+ "text-anchor": "top",
30339
+ "text-allow-overlap": false,
30340
+ "text-optional": true,
30341
+ "text-padding": 4,
30342
+ "symbol-sort-key": labelPriority * 1e3,
30343
+ ...config.layout ?? {}
30344
+ },
30345
+ paint: {
30346
+ "text-color": config.textColor,
30347
+ "text-halo-color": config.textHaloColor,
30348
+ "text-halo-width": config.textHaloWidth
30349
+ },
30350
+ metadata: {
30351
+ "s52:objectClass": objectClass2,
30352
+ "chrt:labelPriority": labelPriority
29683
30353
  }
29684
- ];
29685
- }
29686
- function worldMinusBboxPolygon(bbox) {
29687
- const [w, s] = bbox.sw;
29688
- const [e, n] = bbox.ne;
29689
- return {
29690
- type: "Polygon",
29691
- coordinates: [
29692
- [
29693
- [-180, -85],
29694
- [180, -85],
29695
- [180, 85],
29696
- [-180, 85],
29697
- [-180, -85]
29698
- ],
29699
- // Inner ring (hole) — winding opposite to outer ring per GeoJSON
29700
- // spec. MapLibre is permissive but we follow strict winding.
29701
- [
29702
- [w, s],
29703
- [w, n],
29704
- [e, n],
29705
- [e, s],
29706
- [w, s]
29707
- ]
29708
- ]
29709
30354
  };
30355
+ if (config.minzoom) {
30356
+ layer.minzoom = config.minzoom;
30357
+ }
30358
+ if (objectClass2 === "DEPCNT") {
30359
+ layer.filter = GEOM_FILTERS.line;
30360
+ }
30361
+ if (objectClass2 === "CBLOHD" || objectClass2 === "PIPOHD") {
30362
+ layer.filter = GEOM_FILTERS.line;
30363
+ }
30364
+ if (objectClass2 === "SBDARE") {
30365
+ delete layer.filter;
30366
+ }
30367
+ if (objectClass2 === "MAGVAR") {
30368
+ delete layer.filter;
30369
+ }
30370
+ if (getLabelBehavior(objectClass2) === "orientation-cross-band") {
30371
+ layer.filter = LABEL_ANCHOR_FILTER;
30372
+ }
30373
+ return applyDepthLabelSize(objectClass2, layer, labelOpts.depthLabelSize);
29710
30374
  }
29711
-
29712
- // src/atlas/base-layers.ts
29713
- var NOCOVERAGE_PATTERN = "chrt:hatch-nocoverage";
29714
- var UNCHARTED_PATTERN = "chrt:hatch-uncharted";
29715
- function inlandWaterColor(palette) {
29716
- return (BASE_THEMES[palette] ?? BASE_THEMES.DAY).inlandWater;
29717
- }
29718
- var BASE_THEMES = {
29719
- DAY: {
29720
- // ── `earth` is the jurisdiction tan DIMMED (~0.55), not a blue-grey ──
29721
- // Outside the jurisdiction the map still owes the reader "what is land".
29722
- // State 3 in the header is "the geography, under a crosshatch", and until
29723
- // 2026-08-31 that geography was unreadable: earth #2A3439 against sea
29724
- // #1B2328 measured 1.25:1, so Hispaniola showed charted boxes with tan
29725
- // land inside and nothing recognisable around them (owner report: "no
29726
- // land painting down near the DR at some zooms"). Land is warm and sea is
29727
- // cool because that is what land and sea are; dimming the JURISDICTION
29728
- // tan rather than inventing a colour says the true thing — this is the
29729
- // same land, we simply do not carry charts for it. Held against BOTH
29730
- // neighbours: legible over sea, and never mistakable for the full tan of
29731
- // a country we do carry. DAY 3.59:1 vs sea, 3.17:1 vs tan.
29732
- earth: "#807761",
29733
- water: "#1B2328",
29734
- river: "#24313A",
29735
- boundary: "#4A575E",
29736
- jurisdiction: "#D4C69D",
29737
- // CHRTland's own tan — one land colour
29738
- inlandWater: "#B9CFDD",
29739
- // muted, unmistakably not a charted blue
29740
- // The uncharted hatch's stripe, held to 2.3:1 against `inlandWater` in
29741
- // every palette (`inland-hatch-contrast.test.ts`) — the hatch says "we have
29742
- // not charted this water", and stripes the eye cannot find say nothing.
29743
- // DAY 3.08:1, darker than its ground. Also the colour the artwork file
29744
- // itself is drawn in.
29745
- inlandHatch: "#5B7383",
29746
- // State furniture. These three were referenced by the layers but missing
29747
- // from this table (and its type — the long-standing TS2339 trio), so the
29748
- // lines shipped on MapLibre's DEFAULT black. Now deliberate: a hairline
29749
- // a shade deeper than the tan, quiet caps, tan halo.
29750
- stateLine: "#B1A077",
29751
- stateLabel: "#8E8468",
29752
- stateHalo: "#D4C69D",
29753
- // Rough-lake furniture (owner ask 2026-08-28: "the lakes need labels,
29754
- // and shorelines drawn"): a shoreline a shade deeper than the rough
29755
- // water, and a name in the water's own voice, haloed by the water.
29756
- inlandShore: "#8FA9BA",
29757
- inlandLabel: "#54707F",
29758
- inlandHalo: "#B9CFDD"
29759
- },
29760
- DUSK: {
29761
- // ── Re-derived 2026-09-01: the basemap sits UNDER the chart ──────────
29762
- // These were the DAY tones scaled by a constant, never measured against
29763
- // the DUSK chart — so `jurisdiction` came out at L=0.506 against a chart
29764
- // whose own land (LANDA #8a7d5e) is L=0.209, i.e. the context layer was
29765
- // 2.4x brighter than the data layer. Now capped at the chart's own land.
29766
- // The rest of the block rides the same 0.666 channel factor so every
29767
- // relationship the palette already chose is preserved, only moved down.
29768
- // `water` is deliberately NOT scaled: it is the reference the land/sea
29769
- // floor is measured against.
29770
- earth: "#544E3E",
29771
- water: "#161D21",
29772
- river: "#141B20",
29773
- boundary: "#293135",
29774
- jurisdiction: "#867D64",
29775
- inlandWater: "#62717A",
29776
- // Owner's choice, 2026-10-04: dark stripes, 2.37:1. The DAY stripe here
29777
- // was 1.02:1 — invisible, so an uncharted lake read as plain water.
29778
- inlandHatch: "#2E383F",
29779
- // Label stays DARK here — the halo is still mid-toned, so 3:1 is reachable
29780
- // downward and a bright label would be a new light source at dusk.
29781
- stateLine: "#B5A987",
29782
- stateLabel: "#38342A",
29783
- stateHalo: "#867D64",
29784
- inlandShore: "#4D5B67",
29785
- inlandLabel: "#212629",
29786
- inlandHalo: "#62717A"
29787
- },
29788
- NIGHT: {
29789
- // ── Re-derived 2026-09-01 — the owner-reported night defect ──────────
29790
- // `jurisdiction` was L=0.240 against a chart whose own land (LANDA
29791
- // #3a3020) is L=0.031: the world basemap was 7.7x BRIGHTER than the chart
29792
- // it sits under, so at z<11 the uncharted interior read as a pale
29793
- // continent-sized slab on a night chart. Capped at the chart's own land.
29794
- //
29795
- // ⚠ `earth` AND `jurisdiction` CONVERGE HERE, and that is forced, not
29796
- // sloppy. Holding earth 2:1 above the ocean AND 2:1 below the jurisdiction
29797
- // tan requires jurisdiction >= L 0.174 — 5.6x the chart's own land. At
29798
- // night the range between the ocean (L 0.006) and the cap (L 0.031) is too
29799
- // compressed to carry two land tones. COVERAGE IS STILL SAID, by the
29800
- // `chrt:hatch-nocoverage` crosshatch, which is a stronger signal than tone
29801
- // and is the one thing that cannot be lost to palette compression.
29802
- //
29803
- // Labels invert to light-on-dark: 3:1 downward is unreachable off a halo
29804
- // this close to black, and CHRTland's own night town labels already sit at
29805
- // L=0.177 for exactly this reason.
29806
- earth: "#333026",
29807
- water: "#10161A",
29808
- river: "#090D0F",
29809
- boundary: "#131719",
29810
- jurisdiction: "#353128",
29811
- inlandWater: "#232A30",
29812
- // The DAY stripe, lighter than this ground: 2.92:1, and L 0.16, under the
29813
- // NIGHT muted-ink ceiling (`night-luminance.test.ts`). Red night draws the
29814
- // NIGHT chart, so this is its stripe too.
29815
- inlandHatch: "#5B7383",
29816
- stateLine: "#484236",
29817
- stateLabel: "#837963",
29818
- stateHalo: "#353128",
29819
- inlandShore: "#1C2227",
29820
- inlandLabel: "#617484",
29821
- inlandHalo: "#232A30"
30375
+ function getGeomTypesForClass(objectClass2, defaultType) {
30376
+ if (SYMBOL_MAP[objectClass2] && defaultType === "symbol") {
30377
+ return ["symbol"];
29822
30378
  }
29823
- };
29824
- function baseLayers(opts = {}) {
29825
- const palette = opts.palette ?? "DAY";
29826
- const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
29827
- const src = opts.sourceId ?? BASE_REGION;
29828
- const L = (o) => o;
29829
- return [
29830
- // 1. Geography — the world as it is, in the quiet dark voice. The OCEAN
29831
- // is a background, not a polygon: the archive ships land and lakes,
29832
- // and "everything else is sea" is exactly what a background expresses
29833
- // — geometry-free, gap-free at every antimeridian and pole. Without
29834
- // it the sea outside coverage was bare S-52 NODTA, a light grey that
29835
- // read as "charted ground" with a hatch on it.
29836
- L({
29837
- id: "chrtbase-ocean",
29838
- type: "background",
29839
- paint: { "background-color": T.water }
29840
- }),
29841
- L({
29842
- id: "chrtbase-earth",
29843
- type: "fill",
29844
- source: src,
29845
- "source-layer": "base_earth",
29846
- paint: { "fill-color": T.earth }
29847
- }),
29848
- L({
29849
- id: "chrtbase-water",
29850
- type: "fill",
29851
- source: src,
29852
- "source-layer": "base_water",
29853
- paint: { "fill-color": T.water }
29854
- }),
29855
- L({
29856
- id: "chrtbase-river",
29857
- type: "line",
29858
- source: src,
29859
- "source-layer": "base_river",
29860
- minzoom: 9,
29861
- paint: {
29862
- "line-color": T.river,
29863
- "line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.6, 14, 1.6]
29864
- }
29865
- }),
29866
- L({
29867
- id: "chrtbase-boundary",
29868
- type: "line",
29869
- source: src,
29870
- "source-layer": "base_boundary",
29871
- paint: { "line-color": T.boundary, "line-width": 0.6, "line-opacity": 0.8 }
29872
- }),
29873
- // 2. THE DEFAULT: no coverage, everywhere, as a geometry-free background.
29874
- // Constant opacity by design — see the header. What kills it is being
29875
- // painted OVER, never fading.
29876
- L({
29877
- id: "chrtbase-nocoverage",
29878
- type: "background",
29879
- paint: { "background-pattern": NOCOVERAGE_PATTERN, "background-opacity": 0.55 }
29880
- }),
29881
- // 3. The jurisdiction: countries whose charts CHRT carries go tan — the
29882
- // same tan as CHRTland, so "land in our world" is ONE colour. Opaque,
29883
- // so it takes the crosshatch off its ground entirely.
29884
- L({
29885
- id: "chrtbase-jurisdiction",
29886
- type: "fill",
29887
- source: src,
29888
- "source-layer": "jurisdiction",
29889
- paint: { "fill-color": T.jurisdiction }
29890
- }),
29891
- // 4. States — internal boundaries and names, inside the jurisdiction
29892
- // only (owner decision 2026-08-28). A hairline a shade deeper than the
29893
- // tan, and quiet letter-spaced caps that fade as harbour detail takes
29894
- // over. The one deliberate exception to no-names-on-the-basemap:
29895
- // ADMIN furniture, nothing else — see the fence test.
29896
- L({
29897
- id: "chrtbase-state-boundary",
29898
- type: "line",
29899
- source: src,
29900
- "source-layer": "state_boundary",
29901
- minzoom: 4,
29902
- paint: {
29903
- "line-color": T.stateLine,
29904
- "line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1],
29905
- "line-dasharray": [3, 2],
29906
- "line-opacity": 0.8
29907
- }
29908
- }),
29909
- // 5. Country borders, drawn to be HOISTED above the whole chart by the
29910
- // generator (owner, 2026-08-31: "where there is charted land, can we
29911
- // add country borders on top of everything? I want it clear where
29912
- // there is a boarder."). Sourced from NE boundary_lines_land — land
29913
- // borders only, never coastline — so riding on top cannot stroke a
29914
- // coast. Dashed so it reads as an administrative line, not chart
29915
- // linework.
29916
- L({
29917
- id: "chrtbase-country-boundary",
29918
- type: "line",
29919
- source: src,
29920
- "source-layer": "country_boundary",
29921
- paint: {
29922
- "line-color": T.boundary,
29923
- "line-width": ["interpolate", ["linear"], ["zoom"], 3, 0.8, 8, 1.4, 12, 2.2],
29924
- "line-dasharray": [4, 2],
29925
- "line-opacity": 0.9
29926
- }
29927
- }),
29928
- L({
29929
- id: "chrtbase-state-label",
29930
- type: "symbol",
29931
- source: src,
29932
- "source-layer": "state_label",
29933
- minzoom: 4,
29934
- maxzoom: 10,
29935
- layout: {
29936
- "text-field": ["upcase", ["get", "name"]],
29937
- "text-font": ["Open Sans Regular"],
29938
- "text-size": ["interpolate", ["linear"], ["zoom"], 4, 9, 8, 12],
29939
- "text-letter-spacing": 0.25,
29940
- "text-allow-overlap": false,
29941
- "text-padding": 8
29942
- },
29943
- paint: {
29944
- "text-color": T.stateLabel,
29945
- "text-halo-color": T.stateHalo,
29946
- "text-halo-width": 1.2,
29947
- "text-opacity": ["interpolate", ["linear"], ["zoom"], 4, 0.7, 8, 0.85, 10, 0.4]
30379
+ if (objectClass2 === "SOUNDG") {
30380
+ return ["circle"];
30381
+ }
30382
+ if (defaultType) {
30383
+ return [defaultType];
30384
+ }
30385
+ return ["circle"];
30386
+ }
30387
+ function getOrderedClasses() {
30388
+ const ordered = [...S52_DRAW_ORDER];
30389
+ for (const key of Object.keys(S52_LOOKUP)) {
30390
+ if (!ordered.includes(key)) {
30391
+ const insertIdx = ordered.indexOf("SOUNDG");
30392
+ if (insertIdx >= 0) {
30393
+ ordered.splice(insertIdx, 0, key);
30394
+ } else {
30395
+ ordered.push(key);
29948
30396
  }
29949
- })
29950
- ];
30397
+ }
30398
+ }
30399
+ return ordered;
29951
30400
  }
29952
- function baseInlandWaterLayers(opts = {}) {
29953
- const palette = opts.palette ?? "DAY";
29954
- const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
29955
- const src = opts.sourceId ?? BASE_REGION;
29956
- const L = (o) => o;
29957
- const NE_TIER = ["!", ["has", "kind"]];
29958
- const PLANET_TIER = ["has", "kind"];
29959
- const HANDOVER = 9;
30401
+
30402
+ // src/atlas/terrain-order.ts
30403
+ var DRAPEABLE_LAYER_TYPES = /* @__PURE__ */ new Set([
30404
+ "background",
30405
+ "fill",
30406
+ "line",
30407
+ "raster",
30408
+ "hillshade",
30409
+ "color-relief"
30410
+ ]);
30411
+ function isDrapeable(layer) {
30412
+ return DRAPEABLE_LAYER_TYPES.has(layer.type);
30413
+ }
30414
+ function drapedFirst(layers) {
30415
+ const draped = [];
30416
+ const direct = [];
30417
+ for (const l of layers) (isDrapeable(l) ? draped : direct).push(l);
30418
+ return [...draped, ...direct];
30419
+ }
30420
+ function terrainStackCount(layers) {
30421
+ let stacks = 0;
30422
+ let prevDraped = false;
30423
+ for (const l of layers) {
30424
+ const d = isDrapeable(l);
30425
+ if (d && !prevDraped) stacks++;
30426
+ prevDraped = d;
30427
+ }
30428
+ return stacks;
30429
+ }
30430
+ function drapeHalves(layers) {
30431
+ const draped = [];
30432
+ const direct = [];
30433
+ for (const l of layers) (isDrapeable(l) ? draped : direct).push(l);
30434
+ return { draped, direct };
30435
+ }
30436
+
30437
+ // src/atlas/lamps.ts
30438
+ var LAMP_GLOW_LAYER_ID = "s52-LIGHTS-lamp-glow";
30439
+ var LAMP_CORE_LAYER_ID = "s52-LIGHTS-lamp-core";
30440
+ var LAMP_LAYER_IDS = [LAMP_GLOW_LAYER_ID, LAMP_CORE_LAYER_ID];
30441
+ var RANGE_NM = ["to-number", ["coalesce", ["get", "VALNMR"], ["get", "valnmr"]], 6];
30442
+ var UNSECTORED_POINTS = [
30443
+ "all",
30444
+ ["any", ["==", ["geometry-type"], "Point"], ["==", ["geometry-type"], "MultiPoint"]],
30445
+ ["!", ["has", "SECTR1"]],
30446
+ // …and not lights with a charted rhythm either: those flash, and the
30447
+ // flashing is computed by the app from `SIGSEQ` (light-sigseq.ts). What is
30448
+ // left here is exactly the set that is steady from every bearing.
30449
+ ["!", ["has", "SIGSEQ"]]
30450
+ ];
30451
+ function buildLampLayers(sourceId, ct) {
30452
+ const colour = lightFillExpression(ct);
30453
+ const common = {
30454
+ type: "circle",
30455
+ source: sourceId,
30456
+ "source-layer": "LIGHTS",
30457
+ filter: UNSECTORED_POINTS,
30458
+ layout: { visibility: "none" }
30459
+ };
30460
+ const metadata = { "chrt:role": "lamp", "s52:objectClass": "LIGHTS" };
29960
30461
  return [
29961
- // ── The fill pair, SPLIT BY TIER at the handover ────────────────────────
29962
- //
29963
- // Both tiers share `inland_water` (tile-join concatenates the same layer
29964
- // name), and an unsplit fill draws BOTH — which means the NE polygon is
29965
- // painted over the planet one at every zoom. That is not a cosmetic
29966
- // double-draw: NE 10m carries no interior rings, so its blob has no
29967
- // ISLANDS in it, and painting it over the planet's holed polygons buries
29968
- // every island in the lake under "uncharted water". Owner-reported at
29969
- // Winnipesaukee z11.7 ("there is land (and islands) being covered by the
29970
- // 'no chart here' boundary"), and measured: rendering the planet tier
29971
- // alone brings back the whole archipelago — Rattlesnake, Bear, Long
29972
- // Island, Diamond, the Barndoors — while the NE tier alone is a faceted
29973
- // blob whose edges bear no relation to the shoreline drawn beneath it.
29974
- //
29975
- // So each tier owns its own zoom range, the same handover the shoreline
29976
- // below uses. Known residue, measured over the whole archive: of the 141
29977
- // NE lakes we still carry, 138 have planet water beneath them; the
29978
- // exceptions are two unnamed western-Alaska tundra lakes and a 2e-5 deg²
29979
- // Lake Michigan sliver, which lose their rough fill above z9 because the
29980
- // planet has no `water` polygon there at all (checked against the raw
29981
- // pre-clip NDJSON — not a clip artefact).
29982
- L({
29983
- id: "chrtbase-inland-water",
29984
- type: "fill",
29985
- source: src,
29986
- "source-layer": "inland_water",
29987
- filter: NE_TIER,
29988
- maxzoom: HANDOVER,
29989
- paint: { "fill-color": T.inlandWater }
29990
- }),
29991
- L({
29992
- id: "chrtbase-inland-hatch",
29993
- type: "fill",
29994
- source: src,
29995
- "source-layer": "inland_water",
29996
- filter: NE_TIER,
29997
- maxzoom: HANDOVER,
29998
- paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
29999
- }),
30000
- L({
30001
- id: "chrtbase-inland-water-detail",
30002
- type: "fill",
30003
- source: src,
30004
- "source-layer": "inland_water",
30005
- filter: PLANET_TIER,
30006
- minzoom: HANDOVER,
30007
- paint: { "fill-color": T.inlandWater }
30008
- }),
30009
- L({
30010
- id: "chrtbase-inland-hatch-detail",
30011
- type: "fill",
30012
- source: src,
30013
- "source-layer": "inland_water",
30014
- filter: PLANET_TIER,
30015
- minzoom: HANDOVER,
30016
- paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
30017
- }),
30018
- // The rough shoreline (owner ask 2026-08-28). Drawn ABOVE the hatch so
30019
- // the edge stays crisp through the texture, and BELOW the charted-water
30020
- // repaint clones the generator splices in after it — so wherever the
30021
- // chart repaints its own water, the rough NE10m line yields to the
30022
- // chart's COALNE instead of double-striking it.
30023
- //
30024
- // NE-tier geometry ONLY (`kind` absent): the planet detail tier joins
30025
- // the same source-layer from z9 as MANY polygons whose chunk-clip seams
30026
- // rejoin invisibly as FILLS — but a line layer strokes every seam, and
30027
- // Winnipesaukee rendered as a mesh of internal edges. Planet features
30028
- // carry `kind` (the INLAND_KINDS filter's own field); NE's carry only
30029
- // `name`, so the absence of `kind` IS the tier discriminator.
30030
- L({
30031
- id: "chrtbase-inland-shore",
30032
- type: "line",
30033
- source: src,
30034
- "source-layer": "inland_water",
30035
- filter: NE_TIER,
30036
- // Hands the shoreline over to the planet tier at ITS floor. Both
30037
- // tiers live in `inland_water` from z9 up (tile-join concatenates the
30038
- // same layer name), and their FILLS merge invisibly — but two
30039
- // outlines do not: NE 10m generalises Winnipesaukee's shape, so its
30040
- // outline ran straight across the middle of the lake the planet had
30041
- // drawn correctly around. The coarse line is right only while it is
30042
- // the only shape we have.
30043
- maxzoom: HANDOVER,
30044
- paint: {
30045
- "line-color": T.inlandShore,
30046
- "line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1.1, 14, 1.6],
30047
- "line-opacity": 0.9
30048
- }
30049
- }),
30050
- // The PLANET tier's shoreline (owner ask 2026-08-31: "uncharted small
30051
- // lakes render with no outline"). It reads its own `inland_shore` layer,
30052
- // NOT `inland_water`, and that indirection is the whole fix: the mesh
30053
- // that forced the `!has kind` fence above is a CLIP artefact — an MVT
30054
- // feature arrives already cut to its own z10 tile, so stroking the
30055
- // polygon strokes every tile boundary it crosses. The build now walks
30056
- // those rings, drops the axis-aligned runs that sit on the read zoom's
30057
- // tile grid, and emits what is left as lines (measured on one New
30058
- // England chunk: 61 long axis-aligned mesh edges → 5, and the 5 survivors
30059
- // are 0.02–0.5 tiles off the grid, i.e. real straight water edges).
30060
- //
30061
- // Thinner and quieter than the NE line: this is finer geometry describing
30062
- // water nobody has surveyed, and approximate data should look
30063
- // approximate.
30064
- L({
30065
- id: "chrtbase-inland-shore-detail",
30066
- type: "line",
30067
- source: src,
30068
- "source-layer": "inland_shore",
30069
- minzoom: HANDOVER,
30070
- paint: {
30071
- "line-color": T.inlandShore,
30072
- "line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.5, 12, 0.9, 15, 1.2],
30073
- "line-opacity": 0.85
30074
- }
30075
- }),
30076
- // The lake's name (owner ask 2026-08-28) — the second scoped exception
30077
- // to no-names-on-the-basemap, after the states: WATER-BODY names only.
30078
- // No maxzoom: an uncharted lake keeps its name at every zoom; over
30079
- // charted lakes the chart's own SEAARE label takes over at detail zooms
30080
- // and collision culls the echo.
30081
- //
30082
- // The archive's `inland_water_label` anchors (v3+). v3 shipped ONE
30083
- // PointOnSurface anchor per lake, and a big lake's single anchor pans
30084
- // off-viewport — Winnipesaukee at z11.7 read nameless with its name two
30085
- // tiles west of the screen (owner-reported). Labelling the polygons
30086
- // instead is NOT the fix: tippecanoe's coalescing drops the NE polygon
30087
- // from some z11 tiles (measured: 11/619/747 carries only planet pieces),
30088
- // so a polygon-sourced label vanishes tile by tile. v4 therefore bakes a
30089
- // deterministic anchor GRID into big lakes — the paper-chart convention
30090
- // of repeating a name along a large water body — and this one layer
30091
- // renders however many anchors are in view; collision culls the rest.
30092
- // Planet-tier water-body NAMES (owner ask 2026-08-31: NE 10m names only
30093
- // 113 US lakes). Emitted BELOW the NE layer on purpose — MapLibre places
30094
- // symbols topmost-layer-first, so the big NE lake wins collision against
30095
- // the pond beside it, and both lose to the chart's own labels (every ENC
30096
- // class is emitted after this pair).
30097
- //
30098
- // The size threshold is OSM's own `min_zoom`, carried through the build:
30099
- // Lake Winnipesaukee reads 10, Squam Lake 12, so a pond simply is not in
30100
- // the filter until the user is close enough for it to be the answer to
30101
- // "what is that water". `["zoom"]` in a filter evaluates at integer zoom
30102
- // levels and removes the feature from placement entirely — verified
30103
- // against maplibre-gl-native rather than assumed, because a text-size-0
30104
- // gate would still claim a collision slot.
30105
- //
30106
- // The tiers are separate LAYERS, not a filtered split of one — see the
30107
- // builder for why: an archive that a not-yet-deployed style would
30108
- // mis-render cannot be promoted first, and this one has to be.
30109
- L({
30110
- id: "chrtbase-inland-label-detail",
30111
- type: "symbol",
30112
- source: src,
30113
- "source-layer": "inland_water_label_detail",
30114
- minzoom: HANDOVER,
30115
- filter: [">=", ["zoom"], ["get", "min_zoom"]],
30116
- layout: {
30117
- "text-field": ["get", "name"],
30118
- "text-font": ["Open Sans Regular"],
30119
- "text-size": ["interpolate", ["linear"], ["zoom"], 9, 9, 12, 10.5, 15, 11],
30120
- "text-max-width": 8,
30121
- "text-allow-overlap": false,
30122
- "text-padding": 6,
30123
- // Bigger water first within the tier — min_zoom IS the size rank.
30124
- "symbol-sort-key": ["get", "min_zoom"]
30125
- },
30462
+ {
30463
+ ...common,
30464
+ id: LAMP_GLOW_LAYER_ID,
30126
30465
  paint: {
30127
- "text-color": T.inlandLabel,
30128
- "text-halo-color": T.inlandHalo,
30129
- "text-halo-width": 1,
30130
- "text-opacity": 0.9
30131
- }
30132
- }),
30133
- L({
30134
- id: "chrtbase-inland-label",
30135
- type: "symbol",
30136
- source: src,
30137
- "source-layer": "inland_water_label",
30138
- minzoom: 5,
30139
- layout: {
30140
- "text-field": ["get", "name"],
30141
- "text-font": ["Open Sans Regular"],
30142
- "text-size": ["interpolate", ["linear"], ["zoom"], 5, 9.5, 10, 12],
30143
- "text-max-width": 7,
30144
- "text-allow-overlap": false,
30145
- // Zoom-scaled spacing for the anchor GRID: at z8.9 the 0.08-deg
30146
- // anchors sit ~54 px apart and a flat padding let six copies of
30147
- // "Lake Ontario" crowd one screen (owner-reported 2026-08-30).
30148
- // Wide collision boxes at low zoom thin the grid to the repeat-
30149
- // by-convention cadence; by z12 the anchors are ~850 px apart and
30150
- // the padding relaxes to normal.
30151
- "text-padding": ["interpolate", ["linear"], ["zoom"], 5, 80, 9, 64, 12, 16]
30466
+ "circle-color": colour,
30467
+ // 4 nm → 7 px, 12 nm → 12 px, 25 nm → 20 px; capped, because a
30468
+ // primary seacoast light is not a searchlight.
30469
+ "circle-radius": ["interpolate", ["linear"], RANGE_NM, 0, 5, 4, 7, 12, 12, 25, 20],
30470
+ "circle-blur": 1,
30471
+ "circle-opacity": 0.55,
30472
+ // A lamp is a point of light: it does not grow as it comes near, and
30473
+ // it does not lie down with the chart.
30474
+ "circle-pitch-alignment": "viewport",
30475
+ "circle-pitch-scale": "viewport"
30152
30476
  },
30477
+ metadata: { ...metadata, "chrt:lamp": "glow" }
30478
+ },
30479
+ {
30480
+ ...common,
30481
+ id: LAMP_CORE_LAYER_ID,
30153
30482
  paint: {
30154
- "text-color": T.inlandLabel,
30155
- "text-halo-color": T.inlandHalo,
30156
- "text-halo-width": 1.1
30157
- }
30158
- })
30483
+ "circle-color": colour,
30484
+ "circle-radius": ["interpolate", ["linear"], RANGE_NM, 0, 1.5, 12, 2.5, 25, 3.5],
30485
+ "circle-blur": 0.25,
30486
+ "circle-opacity": 1,
30487
+ "circle-pitch-alignment": "viewport",
30488
+ "circle-pitch-scale": "viewport"
30489
+ },
30490
+ metadata: { ...metadata, "chrt:lamp": "core" }
30491
+ }
30492
+ ];
30493
+ }
30494
+
30495
+ // src/atlas/band-priority.ts
30496
+ var BAND_PRIORITY_RANGES = {
30497
+ "1": { minzoom: 0 },
30498
+ "2": { minzoom: 5 },
30499
+ "3": { minzoom: 7 },
30500
+ "4": { minzoom: 9 },
30501
+ "5": { minzoom: 11 },
30502
+ "6": { minzoom: 11 }
30503
+ };
30504
+ var BAND_DATA_MINZOOM = {
30505
+ "1": 0,
30506
+ "2": 5,
30507
+ "3": 7,
30508
+ "4": 9,
30509
+ "5": 11,
30510
+ "6": 13
30511
+ };
30512
+ function dominatedVariantMaxzoom(D) {
30513
+ return Math.max(BAND_PRIORITY_RANGES[D].minzoom, BAND_DATA_MINZOOM[D]);
30514
+ }
30515
+ var DOMINANCE_EXEMPT_FILL_CLASSES = /* @__PURE__ */ new Set([
30516
+ "LNDARE",
30517
+ "DEPARE",
30518
+ "UNSARE",
30519
+ "LAKARE",
30520
+ "DRGARE",
30521
+ "SEAARE"
30522
+ ]);
30523
+ function isExemptOpaqueFill(layer) {
30524
+ if (layer.type !== "fill") return false;
30525
+ for (const cls of DOMINANCE_EXEMPT_FILL_CLASSES) {
30526
+ if (layer.id.startsWith(`s52-${cls}-`)) return true;
30527
+ }
30528
+ return false;
30529
+ }
30530
+ var GROUND_FILL_CLASSES = /* @__PURE__ */ new Set([
30531
+ ...DOMINANCE_EXEMPT_FILL_CLASSES,
30532
+ "RIVERS"
30533
+ ]);
30534
+ function isGroundFill(layer) {
30535
+ if (layer.type !== "fill") return false;
30536
+ for (const cls of GROUND_FILL_CLASSES) {
30537
+ if (layer.id.startsWith(`s52-${cls}-`)) return true;
30538
+ }
30539
+ return false;
30540
+ }
30541
+ var FILL_FLOOR_FREE_BANDS = /* @__PURE__ */ new Set([
30542
+ "1",
30543
+ "2",
30544
+ "3"
30545
+ ]);
30546
+ function bandVariantMinzoom(layer, B) {
30547
+ const classMinzoom = layer.minzoom ?? 0;
30548
+ if (isGroundFill(layer) && FILL_FLOOR_FREE_BANDS.has(B)) {
30549
+ return 0;
30550
+ }
30551
+ if (isGroundFill(layer)) {
30552
+ return BAND_PRIORITY_RANGES[B].minzoom;
30553
+ }
30554
+ return Math.max(classMinzoom, BAND_PRIORITY_RANGES[B].minzoom);
30555
+ }
30556
+ var GENERALISED_AREA_FILL_CLASSES = /* @__PURE__ */ new Set([
30557
+ "BUAARE",
30558
+ // 2026-08-28, New York Harbor: the "New York Marine Inspection Zone and
30559
+ // Captain of the Port Zone" RESARE exists at bands 2/3 as a generalized
30560
+ // shore-hugging polygon whose point-sampled dominance tag MISSED (the
30561
+ // Providence-BUAARE failure mode), so its magenta dashed boundary drew
30562
+ // OFFSET over the band-5 detail at z11.5 — read in the field as "a
30563
+ // magenta shoreline bleeding down from a less coarse band". The same
30564
+ // tile carried untagged band-2 ADMARE and CTNARE giants in the same
30565
+ // state. All three are area classes whose coarse-band boundaries hug
30566
+ // shorelines and whose fine-band copies are authoritative.
30567
+ "RESARE",
30568
+ "CTNARE",
30569
+ "ADMARE"
30570
+ ]);
30571
+ var GENERALISED_AREA_MAX_BAND = 3;
30572
+ function generalisedAreaCeiling(layer, B) {
30573
+ if (layer.type !== "fill" && layer.type !== "line") return null;
30574
+ if (parseInt(B, 10) > GENERALISED_AREA_MAX_BAND) return null;
30575
+ for (const cls of GENERALISED_AREA_FILL_CLASSES) {
30576
+ if (layer.id.startsWith(`s52-${cls}-`)) return BAND_PRIORITY_RANGES["5"].minzoom;
30577
+ }
30578
+ return null;
30579
+ }
30580
+ var BAND_DRAW_ORDER = ["1", "2", "3", "4", "5", "6"];
30581
+ var CHART_BAND_CODES = {
30582
+ Overview: ["1"],
30583
+ General: ["2"],
30584
+ Coastal: ["3"],
30585
+ Approach: ["4"],
30586
+ Harbor: ["5", "6"]
30587
+ };
30588
+ var CHART_CASCADE_CODES = {
30589
+ Overview: ["1"],
30590
+ General: ["1", "2"],
30591
+ Coastal: ["1", "2", "3"],
30592
+ Approach: ["1", "2", "3", "4"],
30593
+ Harbor: ["1", "2", "3", "4", "5", "6"]
30594
+ };
30595
+ var CHRT_STYLE_MODES = [
30596
+ "continuous",
30597
+ ...Object.keys(CHART_BAND_CODES).map(
30598
+ (band) => `chart:${band}`
30599
+ )
30600
+ ];
30601
+ function normalizeStyleMode(raw) {
30602
+ if (raw === null || raw === void 0 || raw === "") return "continuous";
30603
+ const lower = raw.toLowerCase();
30604
+ return CHRT_STYLE_MODES.find((mode) => mode.toLowerCase() === lower) ?? null;
30605
+ }
30606
+ function styleModeBand(mode) {
30607
+ return mode === "continuous" ? null : mode.slice("chart:".length);
30608
+ }
30609
+ function bandFilterExpression(codes) {
30610
+ return cellBandMatch(["slice", ["get", "_cell_id"], 2, 3], codes);
30611
+ }
30612
+ var BAND_CODE_ALIASES = {
30613
+ "5": ["7"]
30614
+ };
30615
+ function cellBandCharacters(code) {
30616
+ return [code, ...BAND_CODE_ALIASES[code] ?? []];
30617
+ }
30618
+ function cellBandMatch(cellBandExpr, codes) {
30619
+ const list = Array.isArray(codes) ? codes : [codes];
30620
+ const chars = [...new Set(list.flatMap((c) => cellBandCharacters(c)))];
30621
+ if (chars.length === 1) {
30622
+ return ["==", cellBandExpr, chars[0]];
30623
+ }
30624
+ return ["in", cellBandExpr, ["literal", chars]];
30625
+ }
30626
+ function combineWithBandFilter(baseFilter, bandFilter) {
30627
+ if (!baseFilter) return bandFilter;
30628
+ if (baseFilter[0] === "all") {
30629
+ return ["all", ...baseFilter.slice(1), bandFilter];
30630
+ }
30631
+ return ["all", baseFilter, bandFilter];
30632
+ }
30633
+ var NOT_DOMINATED_FILTER = [
30634
+ "all",
30635
+ ["!", ["has", "_dominated_by_min"]],
30636
+ ["!=", ["get", "_dominated"], true]
30637
+ ];
30638
+ function dominatedByMinFilter(D) {
30639
+ const dInt = parseInt(D, 10);
30640
+ if (D === "6") {
30641
+ return [
30642
+ "any",
30643
+ ["==", ["get", "_dominated_by_min"], dInt],
30644
+ ["==", ["get", "_dominated"], true]
30645
+ ];
30646
+ }
30647
+ return ["==", ["get", "_dominated_by_min"], dInt];
30648
+ }
30649
+ function withinBboxFilter(bbox) {
30650
+ const [w, s] = bbox.sw;
30651
+ const [e, n] = bbox.ne;
30652
+ return [
30653
+ "within",
30654
+ {
30655
+ type: "Polygon",
30656
+ coordinates: [
30657
+ [
30658
+ [w, s],
30659
+ [e, s],
30660
+ [e, n],
30661
+ [w, n],
30662
+ [w, s]
30663
+ ]
30664
+ ]
30665
+ }
30159
30666
  ];
30160
30667
  }
30668
+ function worldMinusBboxPolygon(bbox) {
30669
+ const [w, s] = bbox.sw;
30670
+ const [e, n] = bbox.ne;
30671
+ return {
30672
+ type: "Polygon",
30673
+ coordinates: [
30674
+ [
30675
+ [-180, -85],
30676
+ [180, -85],
30677
+ [180, 85],
30678
+ [-180, 85],
30679
+ [-180, -85]
30680
+ ],
30681
+ // Inner ring (hole) — winding opposite to outer ring per GeoJSON
30682
+ // spec. MapLibre is permissive but we follow strict winding.
30683
+ [
30684
+ [w, s],
30685
+ [w, n],
30686
+ [e, n],
30687
+ [e, s],
30688
+ [w, s]
30689
+ ]
30690
+ ]
30691
+ };
30692
+ }
30161
30693
 
30162
30694
  // src/atlas/tile-number.ts
30163
30695
  function tileNumber(property, fallback) {
@@ -30228,6 +30760,57 @@ var LAND_THEMES = {
30228
30760
  // skipper is navigating by. Left at the shipped value rather than
30229
30761
  // lowered, because lowering it is a separate decision from this one.
30230
30762
  exaggeration: 0.45
30763
+ },
30764
+ // ── E-ink: paper land, no relief ──────────────────────────────────────
30765
+ // Hillshade is a continuous grey ramp — exactly what an e-ink panel cannot
30766
+ // show (and a moving one costs a refresh), so both e-ink palettes switch it
30767
+ // off (`exaggeration: 0`). Land is the chart's own LANDA in that table, so
30768
+ // charted and CHRTland land are one tone; labels are black on paper.
30769
+ EINK_COLOR: {
30770
+ base: "#f2e2b0",
30771
+ cover: { wood: "#d6e0b0", wetland: "#d0dccc", urban: "#e0dacb", sand: "#f2e2b0" },
30772
+ contour: "#a08a5a",
30773
+ contourIndex: "#6b5a3e",
30774
+ road: "#d8c08a",
30775
+ roadMajor: "#c49a4a",
30776
+ label: "#000000",
30777
+ halo: "#ffffff",
30778
+ shadow: "#6b5a3e",
30779
+ highlight: "#ffffff",
30780
+ fade: "#ffffff",
30781
+ exaggeration: 0
30782
+ },
30783
+ // Mono: one land tone (MID). Land cover is not drawn as tone — four greys
30784
+ // cannot spare one for "wood" — so every cover class is the land tone.
30785
+ EINK_MONO: {
30786
+ base: "#aaaaaa",
30787
+ cover: { wood: "#aaaaaa", wetland: "#aaaaaa", urban: "#aaaaaa", sand: "#aaaaaa" },
30788
+ contour: "#555555",
30789
+ contourIndex: "#555555",
30790
+ road: "#ffffff",
30791
+ roadMajor: "#ffffff",
30792
+ label: "#000000",
30793
+ halo: "#ffffff",
30794
+ shadow: "#555555",
30795
+ highlight: "#ffffff",
30796
+ fade: "#ffffff",
30797
+ exaggeration: 0
30798
+ },
30799
+ // Night: land is the DIM grey (the chart's EINK_NIGHT LANDA), drawn on in
30800
+ // black so the land adds no lit lines of its own; labels the night line grey.
30801
+ EINK_NIGHT: {
30802
+ base: "#555555",
30803
+ cover: { wood: "#555555", wetland: "#555555", urban: "#555555", sand: "#555555" },
30804
+ contour: "#000000",
30805
+ contourIndex: "#000000",
30806
+ road: "#000000",
30807
+ roadMajor: "#000000",
30808
+ label: "#aaaaaa",
30809
+ halo: "#000000",
30810
+ shadow: "#000000",
30811
+ highlight: "#555555",
30812
+ fade: "#000000",
30813
+ exaggeration: 0
30231
30814
  }
30232
30815
  };
30233
30816
  var FITS = ["<=", tileNumber("min_z", 0), ["zoom"]];
@@ -30610,7 +31193,10 @@ var META_LAYERS = /* @__PURE__ */ new Set([
30610
31193
  var PALETTE_NAMES = {
30611
31194
  DAY: "DAY_BRIGHT",
30612
31195
  DUSK: "DUSK",
30613
- NIGHT: "NIGHT"
31196
+ NIGHT: "NIGHT",
31197
+ EINK_COLOR: "EINK_COLOR",
31198
+ EINK_MONO: "EINK_MONO",
31199
+ EINK_NIGHT: "EINK_NIGHT"
30614
31200
  };
30615
31201
  var SYMBOL_SET_LABELS = {
30616
31202
  s52: "S-52",
@@ -30722,7 +31308,7 @@ var ORIENTATION_OPACITY_SCALE = {
30722
31308
  };
30723
31309
  var ORIENTATION_OPACITY_MAX = 0.92;
30724
31310
  var ORIENTATION_BOOST_MINZOOM = 11;
30725
- function fanOrientationLabelByCellBand(label, bandFan, objectClass) {
31311
+ function fanOrientationLabelByCellBand(label, bandFan, objectClass2) {
30726
31312
  const cellBandExpr = [
30727
31313
  "slice",
30728
31314
  ["coalesce", ["get", "_cell_id"], "??9???"],
@@ -30730,7 +31316,7 @@ function fanOrientationLabelByCellBand(label, bandFan, objectClass) {
30730
31316
  3
30731
31317
  ];
30732
31318
  const variants = [];
30733
- const scale = objectClass ? ORIENTATION_OPACITY_SCALE[objectClass] ?? 1 : 1;
31319
+ const scale = objectClass2 ? ORIENTATION_OPACITY_SCALE[objectClass2] ?? 1 : 1;
30734
31320
  for (const code of BAND_DRAW_ORDER) {
30735
31321
  const range = BAND_PRIORITY_RANGES[code];
30736
31322
  const curve = CELL_BAND_OPACITY_CURVE[code];
@@ -30929,14 +31515,14 @@ function generateStyle(palette = "DAY", opts = {}) {
30929
31515
  const bandFan = opts.bandFan ?? "collapsed";
30930
31516
  const lockedBounds = opts.chartMode?.lockedBounds;
30931
31517
  const bundles = [];
30932
- for (const objectClass of orderedClasses) {
30933
- if (META_LAYERS.has(objectClass)) continue;
31518
+ for (const objectClass2 of orderedClasses) {
31519
+ if (META_LAYERS.has(objectClass2)) continue;
30934
31520
  bundles.push({
30935
- objectClass,
30936
- featureLayers: buildLayersForClass(objectClass, palette, sourceId, {
31521
+ objectClass: objectClass2,
31522
+ featureLayers: buildLayersForClass(objectClass2, palette, sourceId, {
30937
31523
  safetyContour: opts.safetyContour
30938
31524
  }),
30939
- label: buildLabelLayer2(objectClass, palette, sourceId, opts.depthUnits ?? "meters", {
31525
+ label: buildLabelLayer2(objectClass2, palette, sourceId, opts.depthUnits ?? "meters", {
30940
31526
  safetyContour: opts.safetyContour,
30941
31527
  twoShadeSoundings: opts.twoShadeSoundings,
30942
31528
  soundingFontWeight: opts.soundingFontWeight,
@@ -31081,17 +31667,17 @@ function generateStyle(palette = "DAY", opts = {}) {
31081
31667
  if (opts.labels === "placename") {
31082
31668
  labelLayers.push(...buildPlaceNameLayers(sourceId, palette));
31083
31669
  } else {
31084
- for (const { objectClass, label } of bundles) {
31670
+ for (const { objectClass: objectClass2, label } of bundles) {
31085
31671
  if (!label) continue;
31086
- if (getLabelBehavior(objectClass) === "orientation-cross-band") {
31087
- if (objectClass === "LNDARE" || objectClass === "SEAARE") {
31672
+ if (getLabelBehavior(objectClass2) === "orientation-cross-band") {
31673
+ if (objectClass2 === "LNDARE" || objectClass2 === "SEAARE") {
31088
31674
  const Z = ISLAND_LABEL_HOIST_MINZOOM;
31089
- const { hoist, watermark } = objectClass === "LNDARE" ? splitOrientationFan(
31090
- fanOrientationLabelByCellBand(label, bandFan, objectClass),
31675
+ const { hoist, watermark } = objectClass2 === "LNDARE" ? splitOrientationFan(
31676
+ fanOrientationLabelByCellBand(label, bandFan, objectClass2),
31091
31677
  [">=", ["zoom"], Z],
31092
31678
  ["<", ["zoom"], Z]
31093
31679
  ) : splitOrientationFan(
31094
- fanOrientationLabelByCellBand(label, bandFan, objectClass),
31680
+ fanOrientationLabelByCellBand(label, bandFan, objectClass2),
31095
31681
  ["<=", buildSeaAreaTierExpression(), MAJOR_WATER_TIER_MAX],
31096
31682
  [">", buildSeaAreaTierExpression(), MAJOR_WATER_TIER_MAX],
31097
31683
  { "text-padding": MAJOR_WATER_TEXT_PADDING }
@@ -31100,7 +31686,7 @@ function generateStyle(palette = "DAY", opts = {}) {
31100
31686
  labelLayers.push(...watermark);
31101
31687
  continue;
31102
31688
  }
31103
- labelLayers.push(...fanOrientationLabelByCellBand(label, bandFan, objectClass));
31689
+ labelLayers.push(...fanOrientationLabelByCellBand(label, bandFan, objectClass2));
31104
31690
  }
31105
31691
  }
31106
31692
  }
@@ -31364,10 +31950,8 @@ function generateStyle(palette = "DAY", opts = {}) {
31364
31950
  glyphs: `${glyphsBase}/{fontstack}/{range}.pbf`,
31365
31951
  layers: finalLayers
31366
31952
  };
31367
- if (symbolSet === "s101") return applyS101SymbolSet(style);
31368
- if (symbolSet === "chrt") return applyChrtSymbolSet(style, palette);
31369
- if (symbolSet === "raster") return applyRasterSymbolSet(style);
31370
- return style;
31953
+ const familyStyle = symbolSet === "s101" ? applyS101SymbolSet(style) : symbolSet === "chrt" ? applyChrtSymbolSet(style, palette) : symbolSet === "raster" ? applyRasterSymbolSet(style) : style;
31954
+ return applyEinkPortrayal(familyStyle, palette, { safetyContour: opts.safetyContour });
31371
31955
  }
31372
31956
  function generateStyleJSON(palette = "DAY", opts = {}) {
31373
31957
  return JSON.stringify(generateStyle(palette, opts), null, 2);
@@ -31511,7 +32095,7 @@ var DEFAULT_CUSTOM_STYLE_CONFIG = {
31511
32095
  water_body_settings: DEFAULT_WATER_BODY_SETTINGS,
31512
32096
  visible_layers: null
31513
32097
  };
31514
- var COLOR_TABLE_NAMES = /* @__PURE__ */ new Set(["DAY", "DUSK", "NIGHT"]);
32098
+ var COLOR_TABLE_NAMES = /* @__PURE__ */ new Set(["DAY", "DUSK", "NIGHT", "EINK_COLOR", "EINK_MONO", "EINK_NIGHT"]);
31515
32099
  var DISPLAY_MODES = /* @__PURE__ */ new Set(["BASE", "STANDARD", "ALL"]);
31516
32100
  var LAYER_NAME_RE = /^[A-Za-z0-9_$]{2,16}$/;
31517
32101
  var MAX_VISIBLE_LAYERS = 512;
@@ -31618,12 +32202,12 @@ function sanitizeCustomStyleConfig(raw) {
31618
32202
  }
31619
32203
  function normalizeTheme(raw) {
31620
32204
  if (!raw) return null;
31621
- const upper = raw.trim().toUpperCase();
32205
+ const upper = raw.trim().toUpperCase().replace(/-/g, "_");
31622
32206
  return COLOR_TABLE_NAMES.has(upper) ? upper : null;
31623
32207
  }
31624
32208
  function resolveCustomTheme(config, requested) {
31625
32209
  if (!requested) return { palette: config.default_color_table, themeFellBack: false };
31626
- if (config.enabled_color_tables.includes(requested)) {
32210
+ if (isEinkColorTable(requested) || config.enabled_color_tables.includes(requested)) {
31627
32211
  return { palette: requested, themeFellBack: false };
31628
32212
  }
31629
32213
  return { palette: config.default_color_table, themeFellBack: true };
@@ -32302,6 +32886,11 @@ var Chrt = class {
32302
32886
  DEPTH_LABEL_CLASSES,
32303
32887
  DEPTH_LABEL_SIZES,
32304
32888
  DRAPEABLE_LAYER_TYPES,
32889
+ EINK_COLOR_TABLES,
32890
+ EINK_DANGER_PATTERN,
32891
+ EINK_DOTS_PATTERN,
32892
+ EINK_HATCH_PATTERN,
32893
+ EINK_MONO_GREYS,
32305
32894
  INSPECT_CLASS_RANK,
32306
32895
  INSPECT_RANK_DEVIATIONS,
32307
32896
  InteractClient,
@@ -32354,6 +32943,9 @@ var Chrt = class {
32354
32943
  S52_DAY,
32355
32944
  S52_DRAW_ORDER,
32356
32945
  S52_DUSK,
32946
+ S52_EINK_COLOR,
32947
+ S52_EINK_MONO,
32948
+ S52_EINK_NIGHT,
32357
32949
  S52_LOOKUP,
32358
32950
  S52_NIGHT,
32359
32951
  S52_PATTERN_SPRITE_PREFIX,
@@ -32373,6 +32965,7 @@ var Chrt = class {
32373
32965
  applyColorTheme,
32374
32966
  applyCustomStyleSettings,
32375
32967
  applyDepthLabelSize,
32968
+ applyEinkPortrayal,
32376
32969
  applyRasterSymbolSet,
32377
32970
  applyS101SymbolSet,
32378
32971
  atonIconExpression,
@@ -32452,6 +33045,8 @@ var Chrt = class {
32452
33045
  iconExpressionForFamily,
32453
33046
  isDepthAreaClassLayerId,
32454
33047
  isDrapeable,
33048
+ isEinkColorTable,
33049
+ isEinkGreyTable,
32455
33050
  isLabelLayerVisible,
32456
33051
  isMapLive,
32457
33052
  isNavAidLayer,
@@ -32491,6 +33086,7 @@ var Chrt = class {
32491
33086
  resolveTargetClass,
32492
33087
  rhythmProperties,
32493
33088
  s101ArtworkProvenance,
33089
+ safetyContourValues,
32494
33090
  sanitizeCustomStyleConfig,
32495
33091
  scaleLinearLight,
32496
33092
  scaleSizeValue,