@chrtco/sdk 0.7.3 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1307,6 +1307,32 @@ var S52_LOOKUP = {
1307
1307
  viewingGroup: 13050,
1308
1308
  scamin: 45e3
1309
1309
  },
1310
+ // Unsurveyed area — a polygon where no reliable hydrographic survey
1311
+ // exists. The IHO S-52 PresLib portrayal is the hatched pattern
1312
+ // UNSURV03 (diagonal lines over a faint grey wash); we don't carry
1313
+ // that sprite, so we lean on the existing OPAQUE_FILLS treatment in
1314
+ // `layer-builder.ts` to force a fully-opaque grey fill that visibly
1315
+ // suppresses anything underneath. The "no data here — don't assume
1316
+ // safe water" signal is the entire point; UNSARE rendering is a BASE
1317
+ // display-category obligation, not an optional cue.
1318
+ //
1319
+ // Note: `UNSARE` is already a member of `OPAQUE_FILLS` and
1320
+ // `OPAQUE_FILL_LAYERS` in `layer-builder.ts` and `style-resolver.ts`
1321
+ // — half-wired before this entry landed. Adding the lookup entry +
1322
+ // draw-order slot completes the wire-up. Surfaced by the
1323
+ // `Audit S-57 Layer Coverage (Weekly)` workflow (issue #575); 5
1324
+ // features in the NOAA corpus, all in remote-Alaska coverage gaps
1325
+ // where survey is genuinely absent.
1326
+ UNSARE: {
1327
+ objectClass: "UNSARE",
1328
+ name: "Unsurveyed Area",
1329
+ category: "Depths",
1330
+ displayCategory: "BASE",
1331
+ int1Ref: "I27",
1332
+ style: { type: "fill", paint: { "fill-color": "CHGRF", "fill-opacity": 1 } },
1333
+ viewingGroup: 13050,
1334
+ scamin: 0
1335
+ },
1310
1336
  // ── Land & Shoreline (INT1 Section C) ───────────────────
1311
1337
  LNDARE: {
1312
1338
  objectClass: "LNDARE",
@@ -1459,6 +1485,51 @@ var S52_LOOKUP = {
1459
1485
  viewingGroup: 12050,
1460
1486
  scamin: 45e3
1461
1487
  },
1488
+ // Rapids — direct navigation hazard in narrows / river constrictions. S-52
1489
+ // INT1 C24 portrayal: a stylised wavy line representing turbulent water.
1490
+ // We use a blue dashed line (DEPVS) so the feature reads as a water
1491
+ // disturbance — same colour family as the depth tints, distinct from the
1492
+ // black coastal-construction line family. Surfaced by the weekly layer-
1493
+ // coverage audit (issue #575); 4 features in the NOAA corpus, all in
1494
+ // freshwater approaches where a small craft could be swept onto rocks.
1495
+ RAPIDS: {
1496
+ objectClass: "RAPIDS",
1497
+ name: "Rapids",
1498
+ category: "Hazards",
1499
+ displayCategory: "STANDARD",
1500
+ int1Ref: "C24",
1501
+ style: { type: "line", paint: { "line-color": "DEPVS", "line-width": 1, "line-dasharray": [3, 2] } },
1502
+ viewingGroup: 24020,
1503
+ scamin: 45e3
1504
+ },
1505
+ // Dyke — physical earth/stone embankment that confines water. Same
1506
+ // black-line family as SLCONS / FNCLNE / CAUSWY: solid CHBLK so the
1507
+ // structure reads as a hard man-made shoreline modification. INT1 F8.
1508
+ // Surfaced by the weekly layer-coverage audit (issue #575); 13 features
1509
+ // in the NOAA corpus.
1510
+ DYKCON: {
1511
+ objectClass: "DYKCON",
1512
+ name: "Dyke",
1513
+ category: "Land",
1514
+ displayCategory: "STANDARD",
1515
+ int1Ref: "F8",
1516
+ style: { type: "line", paint: { "line-color": "CHBLK", "line-width": 1 } },
1517
+ viewingGroup: 12410,
1518
+ scamin: 22e3
1519
+ },
1520
+ // Causeway — raised road or rail bed across tidal flats or shallow water
1521
+ // (Cape Cod, the Florida Keys, etc.). Black-line family alongside DYKCON.
1522
+ // INT1 F3. Surfaced by the weekly layer-coverage audit (issue #575).
1523
+ CAUSWY: {
1524
+ objectClass: "CAUSWY",
1525
+ name: "Causeway",
1526
+ category: "Infrastructure",
1527
+ displayCategory: "STANDARD",
1528
+ int1Ref: "F3",
1529
+ style: { type: "line", paint: { "line-color": "CHBLK", "line-width": 1 } },
1530
+ viewingGroup: 12410,
1531
+ scamin: 22e3
1532
+ },
1462
1533
  TUNNEL: {
1463
1534
  objectClass: "TUNNEL",
1464
1535
  name: "Tunnel",
@@ -1656,6 +1727,13 @@ var S52_LOOKUP = {
1656
1727
  scamin: 9e4
1657
1728
  },
1658
1729
  LNDELV: {
1730
+ // LineString variant (land contours) renders normally with the line
1731
+ // style below. Point variant ≈ spot elevation ("12m") on land — per
1732
+ // S-52 PresLib convention, marine displays suppress land spot heights
1733
+ // so they don't compete with safety-critical hydrography. The
1734
+ // `auditIgnore` flag silences the weekly layer-coverage audit for the
1735
+ // Point geom-type only; it does not disable the line rendering. See
1736
+ // #575 triage matrix for the full rationale.
1659
1737
  objectClass: "LNDELV",
1660
1738
  name: "Land Elevation",
1661
1739
  category: "Land",
@@ -1663,7 +1741,8 @@ var S52_LOOKUP = {
1663
1741
  int1Ref: "C10",
1664
1742
  style: { type: "line", paint: { "line-color": "CHBRN", "line-width": 0.5 } },
1665
1743
  viewingGroup: 12070,
1666
- scamin: 22e3
1744
+ scamin: 22e3,
1745
+ auditIgnore: true
1667
1746
  },
1668
1747
  // ── Hazards (INT1 Section K) ────────────────────────────
1669
1748
  OBSTRN: {
@@ -1749,7 +1828,15 @@ var S52_LOOKUP = {
1749
1828
  int1Ref: "J1",
1750
1829
  style: { type: "fill", paint: { "fill-color": "CHGRF", "fill-opacity": 0.15 } },
1751
1830
  viewingGroup: 14010,
1752
- scamin: 22e3
1831
+ scamin: 22e3,
1832
+ // Point variant (NOAA encodes 2,709 SBDARE Points) is rendered as a
1833
+ // seabed-quality text label ("S"/"M"/"R" per S-52 INT1 J) via the
1834
+ // labels.ts path — see layer-builder.ts SBDARE special-case routing.
1835
+ // The layer-coverage audit script only inspects MapLibre layer filters
1836
+ // from buildLayersForClass(), so it can't see that the Point variant
1837
+ // is in fact rendered. Matches the SOUNDG precedent (auditIgnore +
1838
+ // text-via-labels.ts rendering — see s52-lookup.ts SOUNDG entry).
1839
+ auditIgnore: true
1753
1840
  },
1754
1841
  WEDKLP: {
1755
1842
  // Translucent green fill suggests kelp/vegetation; Point variant (the
@@ -1766,6 +1853,23 @@ var S52_LOOKUP = {
1766
1853
  viewingGroup: 14010,
1767
1854
  scamin: 22e3
1768
1855
  },
1856
+ // Oil barrier — a deployed containment boom corralling spilled oil or
1857
+ // marking the perimeter of a spill-response operation. Operational
1858
+ // hazard: small craft must avoid crossing the line (and the polluted
1859
+ // water it contains). S-52 has no formal INT1 entry, but PresLib
1860
+ // convention for spill-response gear is magenta — matches the rest of
1861
+ // the regulated/operational-area portrayal family. Dashed line so it
1862
+ // reads as a temporary/operational marker rather than a permanent
1863
+ // structure. Surfaced by the weekly layer-coverage audit (issue #575).
1864
+ OILBAR: {
1865
+ objectClass: "OILBAR",
1866
+ name: "Oil Barrier",
1867
+ category: "Hazards",
1868
+ displayCategory: "STANDARD",
1869
+ style: { type: "line", paint: { "line-color": "CHMGD", "line-width": 1, "line-dasharray": [4, 2] } },
1870
+ viewingGroup: 24020,
1871
+ scamin: 45e3
1872
+ },
1769
1873
  // ── Regulated Areas (INT1 Section N) ────────────────────
1770
1874
  ACHARE: {
1771
1875
  // Anchorages are conventionally rendered as a subtle filled region in
@@ -1939,6 +2043,196 @@ var S52_LOOKUP = {
1939
2043
  viewingGroup: 26050,
1940
2044
  scamin: 9e4
1941
2045
  },
2046
+ // ── Jurisdictional zones (INT1 Section N) ───────────────
2047
+ // ADMARE / COSARE / CONZNE / EXEZNE / TESARE share an S-52 portrayal
2048
+ // family: magenta dashed boundary, no fill. These are vast offshore
2049
+ // polygons (EEZ extends 200nm, contiguous zone 24nm) where a flood fill
2050
+ // — even at 0.05 opacity — would visibly tint half the chart and stack
2051
+ // with overlapping jurisdictions into a purple wash. Same boundary-only
2052
+ // pattern as TESARE / RESARE / CTNARE via LINE_FROM_POLYGON_LAYERS.
2053
+ // Surfaced by the weekly layer-coverage audit (issue #575 rows 8–15).
2054
+ ADMARE: {
2055
+ // Administration area — state/national waters, the broadest
2056
+ // jurisdictional polygon class. INT1 N40 maps it into the same
2057
+ // magenta-dashed family as territorial-sea / EEZ / contiguous-zone.
2058
+ objectClass: "ADMARE",
2059
+ name: "Administration Area",
2060
+ category: "Regulated Areas",
2061
+ displayCategory: "STANDARD",
2062
+ int1Ref: "N40",
2063
+ style: {
2064
+ type: "line",
2065
+ paint: {
2066
+ "line-color": "CHMGD",
2067
+ "line-width": 1,
2068
+ "line-dasharray": [6, 4],
2069
+ "line-opacity": 0.6
2070
+ }
2071
+ },
2072
+ viewingGroup: 26010,
2073
+ scamin: 9e4
2074
+ },
2075
+ COSARE: {
2076
+ // Coastal area — jurisdictional polygon delineating a coastal-state
2077
+ // administrative region. Previously suppressed via META_LAYERS in
2078
+ // style-generator.ts; promoted here to render as a magenta dashed
2079
+ // boundary alongside the rest of the N-section jurisdiction family.
2080
+ objectClass: "COSARE",
2081
+ name: "Coastal Area",
2082
+ category: "Regulated Areas",
2083
+ displayCategory: "STANDARD",
2084
+ int1Ref: "N40",
2085
+ style: {
2086
+ type: "line",
2087
+ paint: {
2088
+ "line-color": "CHMGD",
2089
+ "line-width": 1,
2090
+ "line-dasharray": [6, 4],
2091
+ "line-opacity": 0.6
2092
+ }
2093
+ },
2094
+ viewingGroup: 26010,
2095
+ scamin: 9e4
2096
+ },
2097
+ CONZNE: {
2098
+ // Contiguous Zone — 24nm offshore boundary. Same magenta dashed
2099
+ // portrayal as territorial sea; boundary-only because the polygon is
2100
+ // vast and a flood fill would obscure huge swathes of chart data.
2101
+ objectClass: "CONZNE",
2102
+ name: "Contiguous Zone",
2103
+ category: "Regulated Areas",
2104
+ displayCategory: "STANDARD",
2105
+ int1Ref: "N44",
2106
+ style: {
2107
+ type: "line",
2108
+ paint: {
2109
+ "line-color": "CHMGD",
2110
+ "line-width": 1,
2111
+ "line-dasharray": [6, 4],
2112
+ "line-opacity": 0.6
2113
+ }
2114
+ },
2115
+ viewingGroup: 26010,
2116
+ scamin: 9e4
2117
+ },
2118
+ EXEZNE: {
2119
+ // Exclusive Economic Zone — 200nm offshore boundary. Largest of the
2120
+ // jurisdictional polygons; flood-filling even at 0.05 opacity would
2121
+ // tint half the chart at coastal zooms. Boundary-only is the only
2122
+ // sensible portrayal at scale.
2123
+ objectClass: "EXEZNE",
2124
+ name: "Exclusive Economic Zone",
2125
+ category: "Regulated Areas",
2126
+ displayCategory: "STANDARD",
2127
+ int1Ref: "N47",
2128
+ style: {
2129
+ type: "line",
2130
+ paint: {
2131
+ "line-color": "CHMGD",
2132
+ "line-width": 1,
2133
+ "line-dasharray": [6, 4],
2134
+ "line-opacity": 0.6
2135
+ }
2136
+ },
2137
+ viewingGroup: 26010,
2138
+ scamin: 9e4
2139
+ },
2140
+ // ── IMO routing measures (INT1 Section M) ───────────────
2141
+ // TSEZNE / DWRTPT / DWRTCL / TWRTPT are IMO-adopted routing measures
2142
+ // (Traffic Separation Scheme zones, deepwater routes, two-way routes).
2143
+ // S-52 portrayal: magenta dashed boundary for polygons, dashed centerline
2144
+ // for the LineString. The INT1 M-section spec calls for additional
2145
+ // diagonal hatching (TSSCRS01 sprite) on TSEZNE and "DW" centroid text
2146
+ // on DWRTPT — both are deferred follow-ups (TSSCRS01 not in the atlas
2147
+ // sprite set; centroid text belongs in labels.ts). Boundary visibility
2148
+ // is the priority cue here; routing-text augmentation comes later.
2149
+ TSEZNE: {
2150
+ // Traffic Separation Zone — the buffer band between opposing lanes of
2151
+ // a TSS. S-52 INT1 M14 calls for T-pattern diagonal hatching
2152
+ // (TSSCRS01) but that sprite isn't in our generated atlas; fall back
2153
+ // to a magenta dashed boundary so the zone is at least visible.
2154
+ objectClass: "TSEZNE",
2155
+ name: "Traffic Separation Zone",
2156
+ category: "Regulated Areas",
2157
+ displayCategory: "STANDARD",
2158
+ int1Ref: "M14",
2159
+ style: {
2160
+ type: "line",
2161
+ paint: {
2162
+ "line-color": "CHMGD",
2163
+ "line-width": 1.2,
2164
+ "line-dasharray": [4, 3],
2165
+ "line-opacity": 0.7
2166
+ }
2167
+ },
2168
+ viewingGroup: 26010,
2169
+ scamin: 9e4
2170
+ },
2171
+ DWRTPT: {
2172
+ // Deepwater Route Part — polygon segment of an IMO-adopted deepwater
2173
+ // route. S-52 INT1 M27 portrayal includes "DW" centroid text; that's
2174
+ // a labels.ts addition deferred to a follow-up. Boundary-only here.
2175
+ objectClass: "DWRTPT",
2176
+ name: "Deepwater Route Part",
2177
+ category: "Regulated Areas",
2178
+ displayCategory: "STANDARD",
2179
+ int1Ref: "M27",
2180
+ style: {
2181
+ type: "line",
2182
+ paint: {
2183
+ "line-color": "CHMGD",
2184
+ "line-width": 1.2,
2185
+ "line-dasharray": [4, 3],
2186
+ "line-opacity": 0.7
2187
+ }
2188
+ },
2189
+ viewingGroup: 26010,
2190
+ scamin: 9e4
2191
+ },
2192
+ DWRTCL: {
2193
+ // Deepwater Route Centerline — LineString feature (not a polygon
2194
+ // boundary) running down the middle of a DWRTPT. Same magenta dashed
2195
+ // style as MAGVAR isogonic lines but slightly heavier so the
2196
+ // route-axis cue reads against busy water.
2197
+ objectClass: "DWRTCL",
2198
+ name: "Deepwater Route Centerline",
2199
+ category: "Regulated Areas",
2200
+ displayCategory: "STANDARD",
2201
+ int1Ref: "M27",
2202
+ style: {
2203
+ type: "line",
2204
+ paint: {
2205
+ "line-color": "CHMGD",
2206
+ "line-width": 0.8,
2207
+ "line-dasharray": [6, 3],
2208
+ "line-opacity": 0.7
2209
+ }
2210
+ },
2211
+ viewingGroup: 26010,
2212
+ scamin: 9e4
2213
+ },
2214
+ TWRTPT: {
2215
+ // Two-Way Route Part — polygon segment of an IMO-adopted two-way
2216
+ // route. S-52 INT1 M28 portrayal includes paired arrow centroid
2217
+ // symbols (TWRTPT52 / TWRTPT53 sprites exist in the atlas but are a
2218
+ // labels.ts follow-up). Boundary-only here.
2219
+ objectClass: "TWRTPT",
2220
+ name: "Two-Way Route Part",
2221
+ category: "Regulated Areas",
2222
+ displayCategory: "STANDARD",
2223
+ int1Ref: "M28",
2224
+ style: {
2225
+ type: "line",
2226
+ paint: {
2227
+ "line-color": "CHMGD",
2228
+ "line-width": 1.2,
2229
+ "line-dasharray": [4, 3],
2230
+ "line-opacity": 0.7
2231
+ }
2232
+ },
2233
+ viewingGroup: 26010,
2234
+ scamin: 9e4
2235
+ },
1942
2236
  // ── Infrastructure (INT1 Sections D, F, L) ──────────────
1943
2237
  BRIDGE: {
1944
2238
  objectClass: "BRIDGE",
@@ -2247,6 +2541,56 @@ var S52_LOOKUP = {
2247
2541
  viewingGroup: 12410,
2248
2542
  scamin: 22e3
2249
2543
  },
2544
+ // Coast Guard station — emergency-response context. Mariners look for
2545
+ // these when navigating unfamiliar coastline or planning a distress-call
2546
+ // contingency. INT1 F62. Symbolised with the same generic tower glyph
2547
+ // (BCNTOW01) used by BUISGL and the CATLMK=17 LNDMRK variant — a
2548
+ // dedicated CGUSTA SVG is a reasonable follow-up but not required for
2549
+ // the structural-presence cue. Surfaced by the weekly layer-coverage
2550
+ // audit (issue #575); 17 Point features in the NOAA corpus.
2551
+ CGUSTA: {
2552
+ objectClass: "CGUSTA",
2553
+ name: "Coast Guard Station",
2554
+ category: "Infrastructure",
2555
+ displayCategory: "OTHER",
2556
+ int1Ref: "F62",
2557
+ style: { type: "symbol", paint: {}, layout: { "icon-size": 0.6, "icon-allow-overlap": true }, iconImage: "BCNTOW01" },
2558
+ viewingGroup: 12410,
2559
+ scamin: 22e3
2560
+ },
2561
+ // "New Object" — S-57 placeholder class for features the issuing HO has
2562
+ // identified but not yet classified into a more specific class. S-52
2563
+ // PresLib does NOT formally define a portrayal because the class is by
2564
+ // construction not-yet-portrayable, so any distinctive symbol is
2565
+ // permissible — we use a vivid magenta-filled circle with a bold black
2566
+ // stroke so a mariner sees clearly that "there is something unclassified
2567
+ // here." Hiding this class would amount to hiding the HO's own
2568
+ // uncertainty marker, which is the exact opposite of what a chart user
2569
+ // needs. displayCategory: STANDARD (not OTHER) so a default-display
2570
+ // mariner cannot accidentally suppress it; scamin: 0 so the marker
2571
+ // stays visible at every scale band. Surfaced by the weekly layer-
2572
+ // coverage audit (issue #575); 8 Point features in the NOAA corpus.
2573
+ // SAFETY CONTRACT: any future "clean up unused magenta point classes"
2574
+ // pass MUST NOT delete or hide this entry — read the comment block on
2575
+ // issue #575 row 22 before touching.
2576
+ NEWOBJ: {
2577
+ objectClass: "NEWOBJ",
2578
+ name: "New Object (Unclassified)",
2579
+ category: "Hazards",
2580
+ displayCategory: "STANDARD",
2581
+ style: {
2582
+ type: "circle",
2583
+ paint: {
2584
+ "circle-color": "CHMGD",
2585
+ "circle-radius": 5,
2586
+ "circle-stroke-color": "CHBLK",
2587
+ "circle-stroke-width": 1.2,
2588
+ "circle-opacity": 0.85
2589
+ }
2590
+ },
2591
+ viewingGroup: 24020,
2592
+ scamin: 0
2593
+ },
2250
2594
  // ── Overhead & Underground (INT1 Section L) ─────────────
2251
2595
  // Overhead cable — S-52 INT1 L31 portrayal: solid thin magenta line
2252
2596
  // (not dashed) because overhead cables are an in-air navigation hazard
@@ -2298,6 +2642,11 @@ var S52_LOOKUP = {
2298
2642
  // when the sprite image is missing; `fill-pattern` here is the default
2299
2643
  // used if the conditional expression isn't applied for some reason
2300
2644
  // (e.g. a consumer that bypasses `resolvePaint`).
2645
+ //
2646
+ // Polygon (vegetation fill) is the primary variant. The Point variant
2647
+ // ≈ a single marker tree; sub-marine-relevance, so `auditIgnore`
2648
+ // silences the audit for the Point geom-type without touching the
2649
+ // Polygon rendering. See #575 triage matrix.
2301
2650
  style: {
2302
2651
  type: "fill",
2303
2652
  paint: { "fill-color": "LANDF", "fill-opacity": 0.6, "fill-pattern": "VEGATN03" }
@@ -2305,7 +2654,8 @@ var S52_LOOKUP = {
2305
2654
  hasConditional: true,
2306
2655
  conditionalAttributes: ["CATVEG"],
2307
2656
  viewingGroup: 12050,
2308
- scamin: 22e3
2657
+ scamin: 22e3,
2658
+ auditIgnore: true
2309
2659
  },
2310
2660
  SEAARE: {
2311
2661
  // Cross-band water base. SEAARE polygons tile the sea below the
@@ -2484,6 +2834,42 @@ var S52_LOOKUP = {
2484
2834
  viewingGroup: 23010,
2485
2835
  scamin: 0
2486
2836
  },
2837
+ // ── Audit-ignored stubs (non-metadata) ──────────────────
2838
+ // Classes whose non-rendering is a deliberate S-52 portrayal choice
2839
+ // rather than a real gap, but which aren't S-57 metadata. Each is
2840
+ // present here only so the weekly layer-coverage audit (#575) stops
2841
+ // flagging it as missing. Paint is a transparent fill — no visible
2842
+ // output. Swapping `auditIgnore: true` for a real style is a one-line
2843
+ // change if the call ever needs to be reversed.
2844
+ SLOTOP: {
2845
+ // Top of slope — land terrain feature, low marine relevance.
2846
+ objectClass: "SLOTOP",
2847
+ name: "Top of Slope",
2848
+ category: "Land",
2849
+ displayCategory: "OTHER",
2850
+ int1Ref: "C12",
2851
+ style: { type: "fill", paint: { "fill-color": "CHGRF", "fill-opacity": 0 } },
2852
+ auditIgnore: true
2853
+ },
2854
+ SNDWAV: {
2855
+ // Sand waves — niche bathymetric texture feature, not a hazard.
2856
+ objectClass: "SNDWAV",
2857
+ name: "Sand Waves",
2858
+ category: "Depths",
2859
+ displayCategory: "OTHER",
2860
+ int1Ref: "J14",
2861
+ style: { type: "fill", paint: { "fill-color": "CHGRF", "fill-opacity": 0 } },
2862
+ auditIgnore: true
2863
+ },
2864
+ CTRPNT: {
2865
+ // Centerline point — geometric helper, not a feature on the chart.
2866
+ objectClass: "CTRPNT",
2867
+ name: "Centerline Point",
2868
+ category: "Meta",
2869
+ displayCategory: "OTHER",
2870
+ style: { type: "fill", paint: { "fill-color": "CHGRF", "fill-opacity": 0 } },
2871
+ auditIgnore: true
2872
+ },
2487
2873
  // ── Meta (hidden by default) ────────────────────────────
2488
2874
  // M_* and DSID are S-57 metadata classes — they describe the dataset
2489
2875
  // (coverage extent, survey reliability, source publication, etc.) and
@@ -2522,6 +2908,17 @@ var S52_LOOKUP = {
2522
2908
  style: { type: "fill", paint: { "fill-color": "CHGRF", "fill-opacity": 0 } },
2523
2909
  auditIgnore: true
2524
2910
  },
2911
+ M_SDAT: {
2912
+ // Sounding Datum — S-57 metadata describing the vertical datum used
2913
+ // for soundings (typically LAT). Click-query attribute carrier; no
2914
+ // visible shape on its own.
2915
+ objectClass: "M_SDAT",
2916
+ name: "Sounding Datum",
2917
+ category: "Meta",
2918
+ displayCategory: "META",
2919
+ style: { type: "fill", paint: { "fill-color": "CHGRF", "fill-opacity": 0 } },
2920
+ auditIgnore: true
2921
+ },
2525
2922
  M_SREL: {
2526
2923
  objectClass: "M_SREL",
2527
2924
  name: "Survey Reliability",
@@ -2561,6 +2958,16 @@ var S52_DRAW_ORDER = [
2561
2958
  // paired paint-spec change (fill-opacity 0 → 1, color → DEPDW).
2562
2959
  "SEAARE",
2563
2960
  "DEPARE",
2961
+ // 2a. Unsurveyed area — opaque grey wash painted ON TOP of any DEPARE
2962
+ // tile within its extent so depth-tier colours can never imply the area
2963
+ // is surveyed-and-safe. If both DEPARE and UNSARE cover the same pixel,
2964
+ // UNSARE wins on purpose: the hydrographic office classified the polygon
2965
+ // as unsurveyed, so any depth data within it is suspect at best. Sits
2966
+ // below dredged / swept (which represent active survey work) so those
2967
+ // appear if they overlap UNSARE — but in practice they don't, and the
2968
+ // ordering is the safer fallback. Surfaced by the weekly layer-coverage
2969
+ // audit (issue #575); see s52-lookup.ts UNSARE entry for context.
2970
+ "UNSARE",
2564
2971
  // 3. Dredged / swept areas (fill)
2565
2972
  "DRGARE",
2566
2973
  "SWPARE",
@@ -2598,6 +3005,17 @@ var S52_DRAW_ORDER = [
2598
3005
  "LOGPON",
2599
3006
  "CBLARE",
2600
3007
  "PIPARE",
3008
+ // 6a. Jurisdictional zones + IMO routing measures (boundary-only via
3009
+ // LINE_FROM_POLYGON_LAYERS). Slotted at the end of the regulated-area
3010
+ // band so the dashed magenta boundaries paint above the fill family
3011
+ // but below shoreline / depth-contour lines. Closes #575 rows 8–15.
3012
+ "ADMARE",
3013
+ "COSARE",
3014
+ "CONZNE",
3015
+ "EXEZNE",
3016
+ "TSEZNE",
3017
+ "DWRTPT",
3018
+ "TWRTPT",
2601
3019
  // 7. Infrastructure areas (fill)
2602
3020
  "PONTON",
2603
3021
  "HULKES",
@@ -2607,8 +3025,13 @@ var S52_DRAW_ORDER = [
2607
3025
  "PYLONS",
2608
3026
  "FORSTC",
2609
3027
  // 8. Coastline / shore (line)
3028
+ // DYKCON (dyke) and CAUSWY (causeway) join the SLCONS family — solid
3029
+ // black lines representing man-made shoreline modifications. Drawn in
3030
+ // the same band so they layer consistently with adjacent SLCONS edges.
2610
3031
  "COALNE",
2611
3032
  "SLCONS",
3033
+ "DYKCON",
3034
+ "CAUSWY",
2612
3035
  "FNCLNE",
2613
3036
  // 9. Depth contours (line)
2614
3037
  "DEPCNT",
@@ -2619,13 +3042,20 @@ var S52_DRAW_ORDER = [
2619
3042
  // routing and aids-to-nav win the visual hierarchy).
2620
3043
  "MAGVAR",
2621
3044
  // 10. Navigation lines (line)
3045
+ // RAPIDS and OILBAR are direct/operational water hazards. RAPIDS uses
3046
+ // the depth-blue palette (DEPVS dashed) so the feature reads as a
3047
+ // water-surface disturbance; OILBAR uses CHMGD to fit the operational-
3048
+ // hazard family alongside cables / pipelines.
2622
3049
  "NAVLNE",
2623
3050
  "TSSBND",
3051
+ "DWRTCL",
2624
3052
  "ROADWY",
2625
3053
  "RAILWY",
2626
3054
  "TUNNEL",
2627
3055
  "FERYRT",
2628
3056
  "RECTRC",
3057
+ "RAPIDS",
3058
+ "OILBAR",
2629
3059
  "PIPSOL",
2630
3060
  "PIPOHD",
2631
3061
  "CBLSUB",
@@ -2634,6 +3064,11 @@ var S52_DRAW_ORDER = [
2634
3064
  "DAMCON",
2635
3065
  "BERTHS",
2636
3066
  // 11. Point features
3067
+ // CGUSTA (coast guard station, emergency-context) sits with the building
3068
+ // / landmark family — a structural-presence cue rather than a routing
3069
+ // signal. NEWOBJ ("something here we haven't classified yet") goes at
3070
+ // the END of the point list so the distinctive magenta marker stays on
3071
+ // top of neighbouring point features and cannot be visually swallowed.
2637
3072
  "SOUNDG",
2638
3073
  "MORFAC",
2639
3074
  "PILPNT",
@@ -2647,6 +3082,8 @@ var S52_DRAW_ORDER = [
2647
3082
  "CRANES",
2648
3083
  "DISMAR",
2649
3084
  "ACHBRT",
3085
+ "CGUSTA",
3086
+ "NEWOBJ",
2650
3087
  // 12. Nav aids (on top)
2651
3088
  "FOGSIG",
2652
3089
  "RTPBCN",
@@ -5264,6 +5701,7 @@ var SYMBOL_MAP = {
5264
5701
  PILBOP: "PILBOP02",
5265
5702
  SISTAT: "SISTAT02",
5266
5703
  SMCFAC: "ACHARE02",
5704
+ CGUSTA: "BCNTOW01",
5267
5705
  // Land infrastructure
5268
5706
  SILTNK: "TNKCON02",
5269
5707
  BUISGL: "BCNTOW01",
@@ -5920,6 +6358,237 @@ function buildSeabedLabelExpression() {
5920
6358
  const natsurJoined = ["slice", natsurParts, 1];
5921
6359
  return ["concat", natquaPrefix, natsurJoined];
5922
6360
  }
6361
+ function underseaFeatureNameMatch(objnam) {
6362
+ const terms = [
6363
+ "Canyon",
6364
+ "Seamount",
6365
+ "Ridge",
6366
+ "Basin",
6367
+ "Swell",
6368
+ "Knoll",
6369
+ "Shoal",
6370
+ "Ledge",
6371
+ "Bank",
6372
+ "Valley",
6373
+ "Spur",
6374
+ "Escarpment"
6375
+ ];
6376
+ return ["any", ...terms.map((t) => [">=", ["index-of", t, objnam], 0])];
6377
+ }
6378
+ var CATSEA_RELIEF_CODES = [
6379
+ 2,
6380
+ // gat
6381
+ 3,
6382
+ // bank
6383
+ 4,
6384
+ // deep
6385
+ 6,
6386
+ // trench
6387
+ 7,
6388
+ // basin
6389
+ 8,
6390
+ // mud flats
6391
+ 9,
6392
+ // reef
6393
+ 10,
6394
+ // ledge
6395
+ 11,
6396
+ // canyon
6397
+ 13,
6398
+ // shoal
6399
+ 14,
6400
+ // knoll
6401
+ 15,
6402
+ // ridge
6403
+ 16,
6404
+ // seamount
6405
+ 17,
6406
+ // pinnacle
6407
+ 18,
6408
+ // abyssal plain
6409
+ 19,
6410
+ // plateau
6411
+ 20,
6412
+ // spur
6413
+ 21,
6414
+ // shelf
6415
+ 22,
6416
+ // trough
6417
+ 23,
6418
+ // saddle
6419
+ 24,
6420
+ // abyssal hills
6421
+ 25,
6422
+ // apron
6423
+ 26,
6424
+ // archipelagic apron
6425
+ 27,
6426
+ // borderland
6427
+ 28,
6428
+ // continental margin
6429
+ 29,
6430
+ // continental rise
6431
+ 30,
6432
+ // escarpment
6433
+ 31,
6434
+ // fan
6435
+ 32,
6436
+ // fracture zone
6437
+ 33,
6438
+ // gap
6439
+ 34,
6440
+ // guyot
6441
+ 35,
6442
+ // hill
6443
+ 36,
6444
+ // hole
6445
+ 37,
6446
+ // hollow
6447
+ 39,
6448
+ // levee
6449
+ 40,
6450
+ // median valley
6451
+ 41,
6452
+ // moat
6453
+ 42,
6454
+ // mountains
6455
+ 43,
6456
+ // peak
6457
+ 44,
6458
+ // province
6459
+ 45,
6460
+ // rise
6461
+ 47,
6462
+ // seamount chain
6463
+ 48,
6464
+ // shelf-edge
6465
+ 49,
6466
+ // sill
6467
+ 50,
6468
+ // slope
6469
+ 51,
6470
+ // terrace
6471
+ 52
6472
+ // valley
6473
+ ];
6474
+ function buildSeaAreaTierExpression() {
6475
+ const catsea = ["to-number", ["coalesce", ["get", "CATSEA"], ["get", "catsea"], 0]];
6476
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6477
+ return [
6478
+ "case",
6479
+ // Tier 4 — Anchorage detail (before all promotions so "Boston Harbor,
6480
+ // MA …" anchorage compound names don't bubble up).
6481
+ [">=", ["index-of", "Anchorage Area", objnam], 0],
6482
+ 4,
6483
+ // Tier 5 — undersea relief, by authoritative CATSEA code first
6484
+ // (Georges Bank=3, Georges Shoal=13, …), then by name for
6485
+ // uncategorised features. Before promotion shims; see
6486
+ // underseaFeatureNameMatch.
6487
+ ["match", catsea, CATSEA_RELIEF_CODES, true, false],
6488
+ 5,
6489
+ underseaFeatureNameMatch(objnam),
6490
+ 5,
6491
+ // Tier 3 by authoritative CATSEA: bay / narrows.
6492
+ ["match", catsea, [5, 12], true, false],
6493
+ 3,
6494
+ // OBJNAM promotion shims (CATSEA has no ocean/sea/gulf/sound/strait
6495
+ // codes, so names are the only signal for the headline tiers):
6496
+ [">=", ["index-of", "Atlantic Ocean", objnam], 0],
6497
+ 1,
6498
+ [">=", ["index-of", "Pacific Ocean", objnam], 0],
6499
+ 1,
6500
+ [">=", ["index-of", "Arctic Ocean", objnam], 0],
6501
+ 1,
6502
+ [">=", ["index-of", "Gulf of", objnam], 0],
6503
+ 2,
6504
+ [">=", ["index-of", " Bay", objnam], 0],
6505
+ 3,
6506
+ [">=", ["index-of", " Sound", objnam], 0],
6507
+ 3,
6508
+ [">=", ["index-of", " Strait", objnam], 0],
6509
+ 3,
6510
+ [">=", ["index-of", " Sea", objnam], 0],
6511
+ 2,
6512
+ 5
6513
+ ];
6514
+ }
6515
+ function buildBuiltUpAreaTierExpression() {
6516
+ const catbua = ["coalesce", ["get", "CATBUA"], ["get", "catbua"], 0];
6517
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6518
+ const nameLen = ["length", objnam];
6519
+ return [
6520
+ "case",
6521
+ ["==", ["to-number", catbua], 1],
6522
+ 1,
6523
+ ["==", ["to-number", catbua], 5],
6524
+ 2,
6525
+ ["<=", nameLen, 8],
6526
+ 1,
6527
+ ["<=", nameLen, 14],
6528
+ 2,
6529
+ 3
6530
+ ];
6531
+ }
6532
+ function buildLandRegionTierExpression() {
6533
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6534
+ const nameLen = ["length", objnam];
6535
+ return [
6536
+ "case",
6537
+ // NOAA also encodes undersea relief names (e.g. "Block Canyon", 12
6538
+ // chars, no comma) as LNDRGN — without this demotion they'd qualify
6539
+ // as tier-1 "major region" and paint at overview zoom.
6540
+ underseaFeatureNameMatch(objnam),
6541
+ 2,
6542
+ ["all", ["<=", nameLen, 12], ["==", ["index-of", ",", objnam], -1]],
6543
+ 1,
6544
+ 2
6545
+ ];
6546
+ }
6547
+ function buildLandAreaTierExpression() {
6548
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6549
+ return [
6550
+ "case",
6551
+ ["<=", ["length", objnam], 12],
6552
+ 1,
6553
+ 2
6554
+ ];
6555
+ }
6556
+ function buildLakeTierExpression() {
6557
+ const catlak = ["coalesce", ["get", "CATLAK"], ["get", "catlak"], 0];
6558
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6559
+ return [
6560
+ "case",
6561
+ ["==", ["to-number", catlak], 1],
6562
+ 1,
6563
+ ["any", ["==", ["to-number", catlak], 2], ["==", ["to-number", catlak], 3]],
6564
+ 2,
6565
+ ["<=", ["length", objnam], 12],
6566
+ 1,
6567
+ 2
6568
+ ];
6569
+ }
6570
+ function buildRiverTierExpression() {
6571
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6572
+ return [
6573
+ "case",
6574
+ [
6575
+ "all",
6576
+ ["<=", ["length", objnam], 15],
6577
+ ["==", ["index-of", ",", objnam], -1]
6578
+ ],
6579
+ 1,
6580
+ 2
6581
+ ];
6582
+ }
6583
+ function buildAirportTierExpression() {
6584
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6585
+ return [
6586
+ "case",
6587
+ ["<=", ["length", objnam], 25],
6588
+ 1,
6589
+ 2
6590
+ ];
6591
+ }
5923
6592
  var COLOR = {
5924
6593
  WATER: "#9ec1d8",
5925
6594
  WATER_DEEP: "#8fb8d0",
@@ -5940,6 +6609,60 @@ var ORIENTATION_BAND_SORT_KEY = [
5940
6609
  ["slice", ["coalesce", ["get", "_cell_id"], "??9???"], 2, 3],
5941
6610
  9
5942
6611
  ];
6612
+ function orientationLabelLayout(extras = {}) {
6613
+ return {
6614
+ "text-anchor": "center",
6615
+ "text-allow-overlap": false,
6616
+ "text-ignore-placement": false,
6617
+ "text-optional": true,
6618
+ "text-padding": 15,
6619
+ "symbol-sort-key": ORIENTATION_BAND_SORT_KEY,
6620
+ ...extras
6621
+ };
6622
+ }
6623
+ function tieredSizeCurve(tierExpr, stops) {
6624
+ const withHolds = [];
6625
+ for (let i = 0; i < stops.length; i++) {
6626
+ if (i > 0) {
6627
+ const [pz, pv] = stops[i - 1];
6628
+ const [nz, nv] = stops[i];
6629
+ if (pv.some((v, t) => v === 0 && nv[t] > 0)) {
6630
+ const hz = Math.round((nz - 0.2) * 10) / 10;
6631
+ if (hz > pz) {
6632
+ const f = (hz - pz) / (nz - pz);
6633
+ withHolds.push([
6634
+ hz,
6635
+ pv.map(
6636
+ (v, t) => v === 0 && nv[t] > 0 ? 0 : Math.round((v + (nv[t] - v) * f) * 10) / 10
6637
+ )
6638
+ ]);
6639
+ }
6640
+ }
6641
+ }
6642
+ withHolds.push(stops[i]);
6643
+ }
6644
+ const expr = ["interpolate", ["linear"], ["zoom"]];
6645
+ for (const [z, vals] of withHolds) {
6646
+ const m = ["match", tierExpr];
6647
+ for (let t = 0; t < vals.length - 1; t++) m.push(t + 1, vals[t]);
6648
+ m.push(vals[vals.length - 1]);
6649
+ expr.push(z, m);
6650
+ }
6651
+ return expr;
6652
+ }
6653
+ var ORIENTATION_HALO_WIDTH = [
6654
+ "interpolate",
6655
+ ["linear"],
6656
+ ["zoom"],
6657
+ 8,
6658
+ 1.6,
6659
+ 11,
6660
+ 1.5,
6661
+ 14,
6662
+ 1,
6663
+ 16,
6664
+ 0.7
6665
+ ];
5943
6666
  var LABEL_BEHAVIOR = {
5944
6667
  // Orientation labels — render once across all bands.
5945
6668
  SEAARE: "orientation-cross-band",
@@ -6054,35 +6777,34 @@ function getLabelConfig(layerName, opts = {}) {
6054
6777
  }
6055
6778
  };
6056
6779
  }
6057
- case "LNDRGN":
6780
+ case "LNDRGN": {
6781
+ const tierExpr = buildLandRegionTierExpression();
6058
6782
  return {
6059
6783
  type: "symbol",
6060
6784
  textField: ["get", "OBJNAM"],
6061
- textSize: [
6062
- "interpolate",
6063
- ["linear"],
6064
- ["zoom"],
6065
- 8,
6066
- 10,
6067
- 12,
6068
- 12,
6069
- 15,
6070
- 14
6071
- ],
6785
+ // Tier-1 entry moved from z5 to z7: undersea-relief names without a
6786
+ // generic keyword ("Three Dory", "Outer Fall") can't be demoted by
6787
+ // pattern, so the overview/general bands stay clean by holding ALL
6788
+ // LNDRGN labels until coastal zoom. The Carto basemap already labels
6789
+ // major land regions at z5-z6. Columns: t1 major region, t2 local
6790
+ // point (also the default).
6791
+ textSize: tieredSizeCurve(tierExpr, [
6792
+ [5, [0, 0]],
6793
+ [7, [11, 0]],
6794
+ [9, [12, 0]],
6795
+ [11, [12, 9]],
6796
+ [13, [11, 11]],
6797
+ [16, [10, 10]]
6798
+ ]),
6072
6799
  textColor: DARK_BG,
6073
6800
  textHaloColor: COLOR.LAND_REGION,
6074
- textHaloWidth: 1.4,
6801
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6075
6802
  textOffset: [0, 0],
6076
6803
  textFont: ["Open Sans Regular"],
6077
- minzoom: 8,
6078
- layout: {
6079
- "text-anchor": "center",
6080
- "text-allow-overlap": false,
6081
- "text-padding": 6,
6082
- "text-optional": true,
6083
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6084
- }
6804
+ minzoom: 5,
6805
+ layout: orientationLabelLayout()
6085
6806
  };
6807
+ }
6086
6808
  case "LNDMRK":
6087
6809
  return {
6088
6810
  type: "symbol",
@@ -6211,97 +6933,97 @@ function getLabelConfig(layerName, opts = {}) {
6211
6933
  }
6212
6934
  };
6213
6935
  }
6214
- case "SEAARE":
6936
+ case "SEAARE": {
6937
+ const tierExpr = buildSeaAreaTierExpression();
6215
6938
  return {
6216
6939
  type: "symbol",
6217
6940
  textField: ["get", "OBJNAM"],
6218
- textSize: [
6219
- "interpolate",
6220
- ["linear"],
6221
- ["zoom"],
6222
- 5,
6223
- 11,
6224
- 8,
6225
- 13,
6226
- 11,
6227
- 15,
6228
- 14,
6229
- 17
6230
- ],
6941
+ // Default tier (CATSEA=0, uncategorised: canyons, seamounts, fishing
6942
+ // banks etc.) is suppressed at overview/general/coastal scales because
6943
+ // it floods the overview with hundreds of small italic labels that
6944
+ // crowd out the headline Ocean/Sea/Gulf names. It becomes visible only
6945
+ // at approach scale (z9) when the user has zoomed in enough that
6946
+ // individual banks are useful context. Columns: t1 ocean, t2 sea/gulf,
6947
+ // t3 bay/sound, t4 anchorage, default.
6948
+ textSize: tieredSizeCurve(tierExpr, [
6949
+ [3, [14, 8, 0, 0, 0]],
6950
+ [5, [16, 13, 9, 0, 0]],
6951
+ [7, [14, 14, 12, 0, 0]],
6952
+ [9, [12, 13, 14, 0, 9]],
6953
+ [11, [10, 12, 13, 0, 11]],
6954
+ [13, [9, 11, 12, 10, 11]],
6955
+ [16, [9, 10, 11, 11, 10]]
6956
+ ]),
6231
6957
  textColor: DARK_BG,
6232
6958
  textHaloColor: COLOR.WATER_DEEP,
6233
- textHaloWidth: 1.8,
6959
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6234
6960
  textOffset: [0, 0],
6235
6961
  textFont: ["Open Sans Italic"],
6236
6962
  minzoom: 5,
6237
- layout: {
6238
- "text-anchor": "center",
6239
- "text-allow-overlap": false,
6240
- "text-padding": 10,
6241
- "text-optional": true,
6963
+ layout: orientationLabelLayout({
6242
6964
  "text-letter-spacing": 0.15,
6243
6965
  "text-max-width": 9,
6244
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6245
- }
6966
+ // SEAARE is always-paint, unlike the other orientation classes.
6967
+ // MapLibre places symbols topmost-layer-first, so with
6968
+ // allow-overlap:false the dense sounding field (drawn above)
6969
+ // collision-starves big-water names — observed on staging at z8:
6970
+ // only 2 SEAARE labels survived viewport-wide, with Cape Cod Bay
6971
+ // and Gulf of Maine dropped entirely. Always-paint is safe for
6972
+ // this class because (a) the dominance composition in
6973
+ // fanOrientationLabelByCellBand suppresses cross-band duplicate
6974
+ // copies, and (b) tier sizing zeroes relief/anchorage names at
6975
+ // the zooms where density is a problem. Rivers/lakes/land keep
6976
+ // collision-dedup — their within-band multi-polygon stacking was
6977
+ // the original complaint, and they lack big-water headline value.
6978
+ "text-allow-overlap": true,
6979
+ "text-ignore-placement": true,
6980
+ "text-optional": false,
6981
+ "text-padding": 2
6982
+ })
6246
6983
  };
6247
- case "LAKARE":
6984
+ }
6985
+ case "LAKARE": {
6986
+ const tierExpr = buildLakeTierExpression();
6248
6987
  return {
6249
6988
  type: "symbol",
6250
6989
  textField: ["get", "OBJNAM"],
6251
- textSize: [
6252
- "interpolate",
6253
- ["linear"],
6254
- ["zoom"],
6255
- 8,
6256
- 10,
6257
- 12,
6258
- 12,
6259
- 15,
6260
- 14
6261
- ],
6990
+ textSize: tieredSizeCurve(tierExpr, [
6991
+ [7, [9, 0]],
6992
+ [9, [11, 0]],
6993
+ [11, [11, 9]],
6994
+ [13, [10, 11]],
6995
+ [16, [9, 10]]
6996
+ ]),
6262
6997
  textColor: DARK_BG,
6263
6998
  textHaloColor: COLOR.WATER,
6264
- textHaloWidth: 1.5,
6999
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6265
7000
  textOffset: [0, 0],
6266
7001
  textFont: ["Open Sans Italic"],
6267
- minzoom: 8,
6268
- layout: {
6269
- "text-anchor": "center",
6270
- "text-allow-overlap": false,
6271
- "text-padding": 6,
6272
- "text-optional": true,
6273
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6274
- }
7002
+ minzoom: 7,
7003
+ layout: orientationLabelLayout()
6275
7004
  };
6276
- case "RIVERS":
7005
+ }
7006
+ case "RIVERS": {
7007
+ const tierExpr = buildRiverTierExpression();
6277
7008
  return {
6278
7009
  type: "symbol",
6279
7010
  textField: ["get", "OBJNAM"],
6280
- textSize: [
6281
- "interpolate",
6282
- ["linear"],
6283
- ["zoom"],
6284
- 9,
6285
- 9,
6286
- 12,
6287
- 11,
6288
- 15,
6289
- 13
6290
- ],
7011
+ textSize: tieredSizeCurve(tierExpr, [
7012
+ [7, [9, 0]],
7013
+ [9, [11, 0]],
7014
+ [11, [11, 9]],
7015
+ [13, [10, 10]],
7016
+ [16, [9, 10]]
7017
+ ]),
6291
7018
  textColor: DARK_BG,
6292
7019
  textHaloColor: COLOR.WATER,
6293
- textHaloWidth: 1.4,
7020
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6294
7021
  textOffset: [0, 0],
6295
7022
  textFont: ["Open Sans Italic"],
6296
- minzoom: 9,
6297
- layout: {
6298
- "text-anchor": "center",
6299
- "text-allow-overlap": false,
6300
- "text-padding": 4,
6301
- "text-optional": true,
6302
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6303
- }
7023
+ minzoom: 7,
7024
+ layout: orientationLabelLayout()
6304
7025
  };
7026
+ }
6305
7027
  case "CANALS":
6306
7028
  return {
6307
7029
  type: "symbol",
@@ -6311,83 +7033,67 @@ function getLabelConfig(layerName, opts = {}) {
6311
7033
  ["linear"],
6312
7034
  ["zoom"],
6313
7035
  11,
7036
+ 10,
7037
+ 13,
6314
7038
  9,
6315
- 14,
6316
- 11
7039
+ 16,
7040
+ 9
6317
7041
  ],
6318
7042
  textColor: DARK_BG,
6319
7043
  textHaloColor: COLOR.WATER,
6320
- textHaloWidth: 1.3,
7044
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6321
7045
  textOffset: [0, 0],
6322
7046
  textFont: ["Open Sans Italic"],
6323
7047
  minzoom: 11,
6324
- layout: {
6325
- "text-anchor": "center",
6326
- "text-allow-overlap": false,
6327
- "text-padding": 4,
6328
- "text-optional": true,
6329
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6330
- }
7048
+ layout: orientationLabelLayout()
6331
7049
  };
6332
- case "LNDARE":
7050
+ case "LNDARE": {
7051
+ const tierExpr = buildLandAreaTierExpression();
6333
7052
  return {
6334
7053
  type: "symbol",
6335
7054
  textField: ["get", "OBJNAM"],
6336
- textSize: [
6337
- "interpolate",
6338
- ["linear"],
6339
- ["zoom"],
6340
- 8,
6341
- 9,
6342
- 12,
6343
- 11,
6344
- 15,
6345
- 13
6346
- ],
7055
+ textSize: tieredSizeCurve(tierExpr, [
7056
+ [5, [8, 0]],
7057
+ [7, [10, 0]],
7058
+ [9, [11, 0]],
7059
+ [11, [11, 9]],
7060
+ [13, [10, 10]],
7061
+ [16, [9, 10]]
7062
+ ]),
6347
7063
  textColor: DARK_BG,
6348
7064
  textHaloColor: COLOR.LAND_NATURAL,
6349
- textHaloWidth: 1.4,
7065
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6350
7066
  textOffset: [0, 0],
6351
7067
  textFont: ["Open Sans Regular"],
6352
- minzoom: 8,
6353
- layout: {
6354
- "text-anchor": "center",
6355
- "text-allow-overlap": false,
6356
- "text-padding": 6,
6357
- "text-optional": true,
6358
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6359
- }
7068
+ minzoom: 5,
7069
+ layout: orientationLabelLayout()
6360
7070
  };
6361
- case "BUAARE":
7071
+ }
7072
+ case "BUAARE": {
7073
+ const tierExpr = buildBuiltUpAreaTierExpression();
6362
7074
  return {
6363
7075
  type: "symbol",
6364
7076
  textField: ["get", "OBJNAM"],
6365
- textSize: [
6366
- "interpolate",
6367
- ["linear"],
6368
- ["zoom"],
6369
- 7,
6370
- 11,
6371
- 10,
6372
- 13,
6373
- 13,
6374
- 15
6375
- ],
7077
+ textSize: tieredSizeCurve(tierExpr, [
7078
+ [3, [11, 0, 0]],
7079
+ [5, [13, 0, 0]],
7080
+ [7, [13, 9, 0]],
7081
+ [9, [12, 12, 0]],
7082
+ [11, [11, 12, 10]],
7083
+ [13, [10, 11, 12]],
7084
+ [16, [10, 10, 11]]
7085
+ ]),
6376
7086
  textColor: DARK_BG,
6377
7087
  textHaloColor: COLOR.CITY,
6378
- textHaloWidth: 1.6,
7088
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6379
7089
  textOffset: [0, 0],
6380
7090
  textFont: ["Open Sans Regular"],
6381
- minzoom: 7,
6382
- layout: {
6383
- "text-anchor": "center",
6384
- "text-allow-overlap": false,
6385
- "text-padding": 8,
6386
- "text-optional": true,
6387
- "text-letter-spacing": 0.05,
6388
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6389
- }
7091
+ minzoom: 3,
7092
+ layout: orientationLabelLayout({
7093
+ "text-letter-spacing": 0.05
7094
+ })
6390
7095
  };
7096
+ }
6391
7097
  case "BRIDGE":
6392
7098
  return {
6393
7099
  type: "symbol",
@@ -6455,33 +7161,27 @@ function getLabelConfig(layerName, opts = {}) {
6455
7161
  "text-optional": true
6456
7162
  }
6457
7163
  };
6458
- case "AIRARE":
7164
+ case "AIRARE": {
7165
+ const tierExpr = buildAirportTierExpression();
6459
7166
  return {
6460
7167
  type: "symbol",
6461
7168
  textField: ["get", "OBJNAM"],
6462
- textSize: [
6463
- "interpolate",
6464
- ["linear"],
6465
- ["zoom"],
6466
- 9,
6467
- 9,
6468
- 13,
6469
- 11
6470
- ],
7169
+ textSize: tieredSizeCurve(tierExpr, [
7170
+ [7, [9, 0]],
7171
+ [9, [11, 0]],
7172
+ [11, [11, 9]],
7173
+ [13, [10, 10]],
7174
+ [16, [9, 9]]
7175
+ ]),
6471
7176
  textColor: DARK_BG,
6472
7177
  textHaloColor: COLOR.AIRPORT,
6473
- textHaloWidth: 1.4,
7178
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6474
7179
  textOffset: [0, 0],
6475
7180
  textFont: ["Open Sans Regular"],
6476
- minzoom: 9,
6477
- layout: {
6478
- "text-anchor": "center",
6479
- "text-allow-overlap": false,
6480
- "text-padding": 5,
6481
- "text-optional": true,
6482
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6483
- }
7181
+ minzoom: 7,
7182
+ layout: orientationLabelLayout()
6484
7183
  };
7184
+ }
6485
7185
  case "SBDARE":
6486
7186
  return {
6487
7187
  type: "symbol",
@@ -6554,26 +7254,20 @@ function getLabelConfig(layerName, opts = {}) {
6554
7254
  "interpolate",
6555
7255
  ["linear"],
6556
7256
  ["zoom"],
7257
+ 11,
7258
+ 9,
7259
+ 13,
6557
7260
  8,
6558
- 10,
6559
- 12,
6560
- 12,
6561
- 15,
6562
- 14
7261
+ 16,
7262
+ 8
6563
7263
  ],
6564
7264
  textColor: "#c545c3",
6565
7265
  textHaloColor: DARK_BG,
6566
- textHaloWidth: 1.4,
7266
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6567
7267
  textOffset: [0, 0],
6568
7268
  textFont: ["Open Sans Italic"],
6569
7269
  minzoom: 8,
6570
- layout: {
6571
- "text-anchor": "center",
6572
- "text-allow-overlap": false,
6573
- "text-padding": 6,
6574
- "text-optional": true,
6575
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6576
- }
7270
+ layout: orientationLabelLayout()
6577
7271
  };
6578
7272
  case "WEDKLP":
6579
7273
  return {
@@ -8680,7 +9374,21 @@ var LINE_FROM_POLYGON_LAYERS = /* @__PURE__ */ new Set([
8680
9374
  "MIPARE",
8681
9375
  "TESARE",
8682
9376
  "MAGVAR",
8683
- "PRDARE"
9377
+ "PRDARE",
9378
+ "TUNNEL",
9379
+ // Jurisdictional zones (INT1 N40–N47) and IMO routing measures
9380
+ // (INT1 M14, M27, M28) — magenta dashed boundary, no fill. Vast
9381
+ // offshore polygons would tint half the chart if flood-filled, so
9382
+ // every member here renders as boundary-only. DWRTCL is not listed
9383
+ // because it's a native LineString class, not a polygon-as-line.
9384
+ // Closes issue #575 rows 8–15.
9385
+ "ADMARE",
9386
+ "COSARE",
9387
+ "CONZNE",
9388
+ "EXEZNE",
9389
+ "TSEZNE",
9390
+ "DWRTPT",
9391
+ "TWRTPT"
8684
9392
  ]);
8685
9393
  var ZOOM_SCALED_LINES2 = {
8686
9394
  DEPCNT: [8, 0.3, 14, 0.8],
@@ -9668,6 +10376,90 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION) {
9668
10376
  metadata: { "s52:objectClass": "RECTRC", "s52:role": "polygon" }
9669
10377
  });
9670
10378
  }
10379
+ if (objectClass === "PONTON" && resolved.type === "fill") {
10380
+ layers.push({
10381
+ id: `s52-PONTON-line`,
10382
+ type: "line",
10383
+ source: sourceId,
10384
+ "source-layer": "PONTON",
10385
+ filter: GEOM_FILTERS.line,
10386
+ paint: {
10387
+ "line-color": resolveColor("CHGRF", ct),
10388
+ "line-width": 1.5
10389
+ },
10390
+ metadata: { "s52:objectClass": "PONTON", "s52:role": "line" }
10391
+ });
10392
+ }
10393
+ if (objectClass === "SILTNK" && resolved.type === "symbol") {
10394
+ layers.push({
10395
+ id: `s52-SILTNK-symbol-polygon`,
10396
+ type: "symbol",
10397
+ source: sourceId,
10398
+ "source-layer": "SILTNK",
10399
+ filter: GEOM_FILTERS.fill,
10400
+ layout: {
10401
+ "icon-image": "TNKCON02",
10402
+ "icon-size": 0.6,
10403
+ "icon-allow-overlap": true,
10404
+ "icon-ignore-placement": false,
10405
+ "symbol-placement": "point"
10406
+ },
10407
+ paint: {},
10408
+ metadata: { "s52:objectClass": "SILTNK", "s52:role": "symbol-polygon" }
10409
+ });
10410
+ }
10411
+ if (objectClass === "ACHBRT" && resolved.type === "symbol") {
10412
+ layers.push({
10413
+ id: `s52-ACHBRT-symbol-polygon`,
10414
+ type: "symbol",
10415
+ source: sourceId,
10416
+ "source-layer": "ACHBRT",
10417
+ filter: GEOM_FILTERS.fill,
10418
+ layout: {
10419
+ "icon-image": "ACHARE02",
10420
+ "icon-size": 0.5,
10421
+ "icon-allow-overlap": true,
10422
+ "icon-ignore-placement": false,
10423
+ "symbol-placement": "point"
10424
+ },
10425
+ paint: {},
10426
+ metadata: { "s52:objectClass": "ACHBRT", "s52:role": "symbol-polygon" }
10427
+ });
10428
+ }
10429
+ if (objectClass === "BERTHS" && resolved.type === "line") {
10430
+ layers.push({
10431
+ id: `s52-BERTHS-circle`,
10432
+ type: "circle",
10433
+ source: sourceId,
10434
+ "source-layer": "BERTHS",
10435
+ filter: GEOM_FILTERS.circle,
10436
+ paint: {
10437
+ "circle-color": resolveColor("CHBLK", ct),
10438
+ "circle-radius": ["interpolate", ["linear"], ["zoom"], 12, 1.5, 14, 2.2, 17, 3],
10439
+ "circle-stroke-color": resolveColor("CHBLK", ct),
10440
+ "circle-stroke-width": 0.5
10441
+ },
10442
+ metadata: { "s52:objectClass": "BERTHS", "s52:role": "point" }
10443
+ });
10444
+ }
10445
+ if (objectClass === "TUNNEL" && resolved.type === "line") {
10446
+ layers.push({
10447
+ // NOT `s52-TUNNEL-line` — that id is already taken by the main layer
10448
+ // (`s52-${objectClass}-${resolved.type}`), and duplicate layer ids
10449
+ // are a Mapbox style-spec violation (caught by style-integrity.test).
10450
+ id: `s52-TUNNEL-linestring`,
10451
+ type: "line",
10452
+ source: sourceId,
10453
+ "source-layer": "TUNNEL",
10454
+ filter: GEOM_FILTERS.line,
10455
+ paint: {
10456
+ "line-color": resolveColor("CHGRD", ct),
10457
+ "line-width": 1,
10458
+ "line-dasharray": [2, 2]
10459
+ },
10460
+ metadata: { "s52:objectClass": "TUNNEL", "s52:role": "line" }
10461
+ });
10462
+ }
9671
10463
  }
9672
10464
  return layers;
9673
10465
  }
@@ -9903,9 +10695,21 @@ var META_LAYERS = /* @__PURE__ */ new Set([
9903
10695
  "M_CSCL",
9904
10696
  "TS_FEB",
9905
10697
  "TS_TIS",
9906
- "COSARE",
9907
- "NEWOBJ",
9908
10698
  "LOCMAG"
10699
+ // COSARE was previously suppressed here as meta. The weekly layer-
10700
+ // coverage audit (issue #575) classified it as a jurisdictional polygon
10701
+ // (S-52 INT1 N40, same family as ADMARE / TESARE). It now has an
10702
+ // S52_LOOKUP entry and renders as a magenta dashed boundary; suppressing
10703
+ // it as meta would defeat that wire-up.
10704
+ //
10705
+ // NEWOBJ was likewise previously suppressed here. That was the source
10706
+ // of audit gap row 22: NEWOBJ is the S-57 placeholder for features the
10707
+ // HO has identified but not yet classified. Skipping it silently hid
10708
+ // the HO's own "we don't know what this is, but it is here" signal —
10709
+ // the opposite of what mariners need. The class now renders via its
10710
+ // S52_LOOKUP entry as a vivid magenta circle, displayCategory STANDARD,
10711
+ // scamin 0. Do not put it back here — see the lookup entry for the
10712
+ // safety contract.
9909
10713
  ]);
9910
10714
  var PALETTE_NAMES = {
9911
10715
  DAY: "DAY_BRIGHT",
@@ -9996,21 +10800,58 @@ function makeBandVariants(layer, B) {
9996
10800
  }
9997
10801
  return variants;
9998
10802
  }
9999
- function fanOrientationLabelByDominator(label) {
10803
+ var CELL_BAND_OPACITY_CURVE = {
10804
+ "1": [[3, 0.5], [5, 0.3], [16, 0.15]],
10805
+ "2": [[5, 0.5], [7, 0.3], [16, 0.15]],
10806
+ "3": [[7, 0.55], [9, 0.35], [16, 0.18]],
10807
+ "4": [[9, 0.55], [11, 0.35], [16, 0.2]],
10808
+ "5": [[11, 0.6], [13, 0.35], [18, 0.25]],
10809
+ "6": [[13, 0.75], [16, 0.55], [22, 0.45]]
10810
+ };
10811
+ function fanOrientationLabelByCellBand(label) {
10812
+ const cellBandExpr = [
10813
+ "slice",
10814
+ ["coalesce", ["get", "_cell_id"], "??9???"],
10815
+ 2,
10816
+ 3
10817
+ ];
10000
10818
  const variants = [];
10001
- variants.push({
10002
- ...label,
10003
- id: `${label.id}-notdom`,
10004
- filter: combineWithBandFilter(label.filter, NOT_DOMINATED_FILTER)
10005
- });
10006
- for (const D of BAND_DRAW_ORDER) {
10007
- if (D === "1") continue;
10819
+ for (const code of BAND_DRAW_ORDER) {
10820
+ const range = BAND_PRIORITY_RANGES[code];
10821
+ const curve = CELL_BAND_OPACITY_CURVE[code];
10822
+ const opacityExpr = ["interpolate", ["linear"], ["zoom"]];
10823
+ for (const [z, o] of curve) opacityExpr.push(z, o);
10824
+ const cellFiltered = combineWithBandFilter(
10825
+ label.filter,
10826
+ ["==", cellBandExpr, code]
10827
+ );
10828
+ const minzoom = Math.max(
10829
+ label.minzoom ?? range.minzoom,
10830
+ range.minzoom
10831
+ );
10832
+ const paint = {
10833
+ ...label.paint ?? {},
10834
+ "text-opacity": opacityExpr
10835
+ };
10008
10836
  variants.push({
10009
10837
  ...label,
10010
- id: `${label.id}-domBy${D}`,
10011
- filter: combineWithBandFilter(label.filter, dominatedByMinFilter(D)),
10012
- maxzoom: BAND_PRIORITY_RANGES[D].minzoom
10838
+ id: `${label.id}-cell${code}-notdom`,
10839
+ filter: combineWithBandFilter(cellFiltered, NOT_DOMINATED_FILTER),
10840
+ minzoom,
10841
+ paint
10013
10842
  });
10843
+ for (const D of BAND_DRAW_ORDER) {
10844
+ const dom = BAND_PRIORITY_RANGES[D];
10845
+ if (dom.minzoom <= minzoom) continue;
10846
+ variants.push({
10847
+ ...label,
10848
+ id: `${label.id}-cell${code}-domBy${D}`,
10849
+ filter: combineWithBandFilter(cellFiltered, dominatedByMinFilter(D)),
10850
+ minzoom,
10851
+ maxzoom: dom.minzoom,
10852
+ paint
10853
+ });
10854
+ }
10014
10855
  }
10015
10856
  return variants;
10016
10857
  }
@@ -10134,7 +10975,7 @@ function generateStyle(palette = "DAY", opts = {}) {
10134
10975
  for (const { objectClass, label } of bundles) {
10135
10976
  if (!label) continue;
10136
10977
  if (getLabelBehavior(objectClass) === "orientation-cross-band") {
10137
- labelLayers.push(...fanOrientationLabelByDominator(label));
10978
+ labelLayers.push(...fanOrientationLabelByCellBand(label));
10138
10979
  }
10139
10980
  }
10140
10981
  for (const code of BAND_DRAW_ORDER) {