@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.cjs CHANGED
@@ -1412,6 +1412,32 @@ var S52_LOOKUP = {
1412
1412
  viewingGroup: 13050,
1413
1413
  scamin: 45e3
1414
1414
  },
1415
+ // Unsurveyed area — a polygon where no reliable hydrographic survey
1416
+ // exists. The IHO S-52 PresLib portrayal is the hatched pattern
1417
+ // UNSURV03 (diagonal lines over a faint grey wash); we don't carry
1418
+ // that sprite, so we lean on the existing OPAQUE_FILLS treatment in
1419
+ // `layer-builder.ts` to force a fully-opaque grey fill that visibly
1420
+ // suppresses anything underneath. The "no data here — don't assume
1421
+ // safe water" signal is the entire point; UNSARE rendering is a BASE
1422
+ // display-category obligation, not an optional cue.
1423
+ //
1424
+ // Note: `UNSARE` is already a member of `OPAQUE_FILLS` and
1425
+ // `OPAQUE_FILL_LAYERS` in `layer-builder.ts` and `style-resolver.ts`
1426
+ // — half-wired before this entry landed. Adding the lookup entry +
1427
+ // draw-order slot completes the wire-up. Surfaced by the
1428
+ // `Audit S-57 Layer Coverage (Weekly)` workflow (issue #575); 5
1429
+ // features in the NOAA corpus, all in remote-Alaska coverage gaps
1430
+ // where survey is genuinely absent.
1431
+ UNSARE: {
1432
+ objectClass: "UNSARE",
1433
+ name: "Unsurveyed Area",
1434
+ category: "Depths",
1435
+ displayCategory: "BASE",
1436
+ int1Ref: "I27",
1437
+ style: { type: "fill", paint: { "fill-color": "CHGRF", "fill-opacity": 1 } },
1438
+ viewingGroup: 13050,
1439
+ scamin: 0
1440
+ },
1415
1441
  // ── Land & Shoreline (INT1 Section C) ───────────────────
1416
1442
  LNDARE: {
1417
1443
  objectClass: "LNDARE",
@@ -1564,6 +1590,51 @@ var S52_LOOKUP = {
1564
1590
  viewingGroup: 12050,
1565
1591
  scamin: 45e3
1566
1592
  },
1593
+ // Rapids — direct navigation hazard in narrows / river constrictions. S-52
1594
+ // INT1 C24 portrayal: a stylised wavy line representing turbulent water.
1595
+ // We use a blue dashed line (DEPVS) so the feature reads as a water
1596
+ // disturbance — same colour family as the depth tints, distinct from the
1597
+ // black coastal-construction line family. Surfaced by the weekly layer-
1598
+ // coverage audit (issue #575); 4 features in the NOAA corpus, all in
1599
+ // freshwater approaches where a small craft could be swept onto rocks.
1600
+ RAPIDS: {
1601
+ objectClass: "RAPIDS",
1602
+ name: "Rapids",
1603
+ category: "Hazards",
1604
+ displayCategory: "STANDARD",
1605
+ int1Ref: "C24",
1606
+ style: { type: "line", paint: { "line-color": "DEPVS", "line-width": 1, "line-dasharray": [3, 2] } },
1607
+ viewingGroup: 24020,
1608
+ scamin: 45e3
1609
+ },
1610
+ // Dyke — physical earth/stone embankment that confines water. Same
1611
+ // black-line family as SLCONS / FNCLNE / CAUSWY: solid CHBLK so the
1612
+ // structure reads as a hard man-made shoreline modification. INT1 F8.
1613
+ // Surfaced by the weekly layer-coverage audit (issue #575); 13 features
1614
+ // in the NOAA corpus.
1615
+ DYKCON: {
1616
+ objectClass: "DYKCON",
1617
+ name: "Dyke",
1618
+ category: "Land",
1619
+ displayCategory: "STANDARD",
1620
+ int1Ref: "F8",
1621
+ style: { type: "line", paint: { "line-color": "CHBLK", "line-width": 1 } },
1622
+ viewingGroup: 12410,
1623
+ scamin: 22e3
1624
+ },
1625
+ // Causeway — raised road or rail bed across tidal flats or shallow water
1626
+ // (Cape Cod, the Florida Keys, etc.). Black-line family alongside DYKCON.
1627
+ // INT1 F3. Surfaced by the weekly layer-coverage audit (issue #575).
1628
+ CAUSWY: {
1629
+ objectClass: "CAUSWY",
1630
+ name: "Causeway",
1631
+ category: "Infrastructure",
1632
+ displayCategory: "STANDARD",
1633
+ int1Ref: "F3",
1634
+ style: { type: "line", paint: { "line-color": "CHBLK", "line-width": 1 } },
1635
+ viewingGroup: 12410,
1636
+ scamin: 22e3
1637
+ },
1567
1638
  TUNNEL: {
1568
1639
  objectClass: "TUNNEL",
1569
1640
  name: "Tunnel",
@@ -1761,6 +1832,13 @@ var S52_LOOKUP = {
1761
1832
  scamin: 9e4
1762
1833
  },
1763
1834
  LNDELV: {
1835
+ // LineString variant (land contours) renders normally with the line
1836
+ // style below. Point variant ≈ spot elevation ("12m") on land — per
1837
+ // S-52 PresLib convention, marine displays suppress land spot heights
1838
+ // so they don't compete with safety-critical hydrography. The
1839
+ // `auditIgnore` flag silences the weekly layer-coverage audit for the
1840
+ // Point geom-type only; it does not disable the line rendering. See
1841
+ // #575 triage matrix for the full rationale.
1764
1842
  objectClass: "LNDELV",
1765
1843
  name: "Land Elevation",
1766
1844
  category: "Land",
@@ -1768,7 +1846,8 @@ var S52_LOOKUP = {
1768
1846
  int1Ref: "C10",
1769
1847
  style: { type: "line", paint: { "line-color": "CHBRN", "line-width": 0.5 } },
1770
1848
  viewingGroup: 12070,
1771
- scamin: 22e3
1849
+ scamin: 22e3,
1850
+ auditIgnore: true
1772
1851
  },
1773
1852
  // ── Hazards (INT1 Section K) ────────────────────────────
1774
1853
  OBSTRN: {
@@ -1854,7 +1933,15 @@ var S52_LOOKUP = {
1854
1933
  int1Ref: "J1",
1855
1934
  style: { type: "fill", paint: { "fill-color": "CHGRF", "fill-opacity": 0.15 } },
1856
1935
  viewingGroup: 14010,
1857
- scamin: 22e3
1936
+ scamin: 22e3,
1937
+ // Point variant (NOAA encodes 2,709 SBDARE Points) is rendered as a
1938
+ // seabed-quality text label ("S"/"M"/"R" per S-52 INT1 J) via the
1939
+ // labels.ts path — see layer-builder.ts SBDARE special-case routing.
1940
+ // The layer-coverage audit script only inspects MapLibre layer filters
1941
+ // from buildLayersForClass(), so it can't see that the Point variant
1942
+ // is in fact rendered. Matches the SOUNDG precedent (auditIgnore +
1943
+ // text-via-labels.ts rendering — see s52-lookup.ts SOUNDG entry).
1944
+ auditIgnore: true
1858
1945
  },
1859
1946
  WEDKLP: {
1860
1947
  // Translucent green fill suggests kelp/vegetation; Point variant (the
@@ -1871,6 +1958,23 @@ var S52_LOOKUP = {
1871
1958
  viewingGroup: 14010,
1872
1959
  scamin: 22e3
1873
1960
  },
1961
+ // Oil barrier — a deployed containment boom corralling spilled oil or
1962
+ // marking the perimeter of a spill-response operation. Operational
1963
+ // hazard: small craft must avoid crossing the line (and the polluted
1964
+ // water it contains). S-52 has no formal INT1 entry, but PresLib
1965
+ // convention for spill-response gear is magenta — matches the rest of
1966
+ // the regulated/operational-area portrayal family. Dashed line so it
1967
+ // reads as a temporary/operational marker rather than a permanent
1968
+ // structure. Surfaced by the weekly layer-coverage audit (issue #575).
1969
+ OILBAR: {
1970
+ objectClass: "OILBAR",
1971
+ name: "Oil Barrier",
1972
+ category: "Hazards",
1973
+ displayCategory: "STANDARD",
1974
+ style: { type: "line", paint: { "line-color": "CHMGD", "line-width": 1, "line-dasharray": [4, 2] } },
1975
+ viewingGroup: 24020,
1976
+ scamin: 45e3
1977
+ },
1874
1978
  // ── Regulated Areas (INT1 Section N) ────────────────────
1875
1979
  ACHARE: {
1876
1980
  // Anchorages are conventionally rendered as a subtle filled region in
@@ -2044,6 +2148,196 @@ var S52_LOOKUP = {
2044
2148
  viewingGroup: 26050,
2045
2149
  scamin: 9e4
2046
2150
  },
2151
+ // ── Jurisdictional zones (INT1 Section N) ───────────────
2152
+ // ADMARE / COSARE / CONZNE / EXEZNE / TESARE share an S-52 portrayal
2153
+ // family: magenta dashed boundary, no fill. These are vast offshore
2154
+ // polygons (EEZ extends 200nm, contiguous zone 24nm) where a flood fill
2155
+ // — even at 0.05 opacity — would visibly tint half the chart and stack
2156
+ // with overlapping jurisdictions into a purple wash. Same boundary-only
2157
+ // pattern as TESARE / RESARE / CTNARE via LINE_FROM_POLYGON_LAYERS.
2158
+ // Surfaced by the weekly layer-coverage audit (issue #575 rows 8–15).
2159
+ ADMARE: {
2160
+ // Administration area — state/national waters, the broadest
2161
+ // jurisdictional polygon class. INT1 N40 maps it into the same
2162
+ // magenta-dashed family as territorial-sea / EEZ / contiguous-zone.
2163
+ objectClass: "ADMARE",
2164
+ name: "Administration Area",
2165
+ category: "Regulated Areas",
2166
+ displayCategory: "STANDARD",
2167
+ int1Ref: "N40",
2168
+ style: {
2169
+ type: "line",
2170
+ paint: {
2171
+ "line-color": "CHMGD",
2172
+ "line-width": 1,
2173
+ "line-dasharray": [6, 4],
2174
+ "line-opacity": 0.6
2175
+ }
2176
+ },
2177
+ viewingGroup: 26010,
2178
+ scamin: 9e4
2179
+ },
2180
+ COSARE: {
2181
+ // Coastal area — jurisdictional polygon delineating a coastal-state
2182
+ // administrative region. Previously suppressed via META_LAYERS in
2183
+ // style-generator.ts; promoted here to render as a magenta dashed
2184
+ // boundary alongside the rest of the N-section jurisdiction family.
2185
+ objectClass: "COSARE",
2186
+ name: "Coastal Area",
2187
+ category: "Regulated Areas",
2188
+ displayCategory: "STANDARD",
2189
+ int1Ref: "N40",
2190
+ style: {
2191
+ type: "line",
2192
+ paint: {
2193
+ "line-color": "CHMGD",
2194
+ "line-width": 1,
2195
+ "line-dasharray": [6, 4],
2196
+ "line-opacity": 0.6
2197
+ }
2198
+ },
2199
+ viewingGroup: 26010,
2200
+ scamin: 9e4
2201
+ },
2202
+ CONZNE: {
2203
+ // Contiguous Zone — 24nm offshore boundary. Same magenta dashed
2204
+ // portrayal as territorial sea; boundary-only because the polygon is
2205
+ // vast and a flood fill would obscure huge swathes of chart data.
2206
+ objectClass: "CONZNE",
2207
+ name: "Contiguous Zone",
2208
+ category: "Regulated Areas",
2209
+ displayCategory: "STANDARD",
2210
+ int1Ref: "N44",
2211
+ style: {
2212
+ type: "line",
2213
+ paint: {
2214
+ "line-color": "CHMGD",
2215
+ "line-width": 1,
2216
+ "line-dasharray": [6, 4],
2217
+ "line-opacity": 0.6
2218
+ }
2219
+ },
2220
+ viewingGroup: 26010,
2221
+ scamin: 9e4
2222
+ },
2223
+ EXEZNE: {
2224
+ // Exclusive Economic Zone — 200nm offshore boundary. Largest of the
2225
+ // jurisdictional polygons; flood-filling even at 0.05 opacity would
2226
+ // tint half the chart at coastal zooms. Boundary-only is the only
2227
+ // sensible portrayal at scale.
2228
+ objectClass: "EXEZNE",
2229
+ name: "Exclusive Economic Zone",
2230
+ category: "Regulated Areas",
2231
+ displayCategory: "STANDARD",
2232
+ int1Ref: "N47",
2233
+ style: {
2234
+ type: "line",
2235
+ paint: {
2236
+ "line-color": "CHMGD",
2237
+ "line-width": 1,
2238
+ "line-dasharray": [6, 4],
2239
+ "line-opacity": 0.6
2240
+ }
2241
+ },
2242
+ viewingGroup: 26010,
2243
+ scamin: 9e4
2244
+ },
2245
+ // ── IMO routing measures (INT1 Section M) ───────────────
2246
+ // TSEZNE / DWRTPT / DWRTCL / TWRTPT are IMO-adopted routing measures
2247
+ // (Traffic Separation Scheme zones, deepwater routes, two-way routes).
2248
+ // S-52 portrayal: magenta dashed boundary for polygons, dashed centerline
2249
+ // for the LineString. The INT1 M-section spec calls for additional
2250
+ // diagonal hatching (TSSCRS01 sprite) on TSEZNE and "DW" centroid text
2251
+ // on DWRTPT — both are deferred follow-ups (TSSCRS01 not in the atlas
2252
+ // sprite set; centroid text belongs in labels.ts). Boundary visibility
2253
+ // is the priority cue here; routing-text augmentation comes later.
2254
+ TSEZNE: {
2255
+ // Traffic Separation Zone — the buffer band between opposing lanes of
2256
+ // a TSS. S-52 INT1 M14 calls for T-pattern diagonal hatching
2257
+ // (TSSCRS01) but that sprite isn't in our generated atlas; fall back
2258
+ // to a magenta dashed boundary so the zone is at least visible.
2259
+ objectClass: "TSEZNE",
2260
+ name: "Traffic Separation Zone",
2261
+ category: "Regulated Areas",
2262
+ displayCategory: "STANDARD",
2263
+ int1Ref: "M14",
2264
+ style: {
2265
+ type: "line",
2266
+ paint: {
2267
+ "line-color": "CHMGD",
2268
+ "line-width": 1.2,
2269
+ "line-dasharray": [4, 3],
2270
+ "line-opacity": 0.7
2271
+ }
2272
+ },
2273
+ viewingGroup: 26010,
2274
+ scamin: 9e4
2275
+ },
2276
+ DWRTPT: {
2277
+ // Deepwater Route Part — polygon segment of an IMO-adopted deepwater
2278
+ // route. S-52 INT1 M27 portrayal includes "DW" centroid text; that's
2279
+ // a labels.ts addition deferred to a follow-up. Boundary-only here.
2280
+ objectClass: "DWRTPT",
2281
+ name: "Deepwater Route Part",
2282
+ category: "Regulated Areas",
2283
+ displayCategory: "STANDARD",
2284
+ int1Ref: "M27",
2285
+ style: {
2286
+ type: "line",
2287
+ paint: {
2288
+ "line-color": "CHMGD",
2289
+ "line-width": 1.2,
2290
+ "line-dasharray": [4, 3],
2291
+ "line-opacity": 0.7
2292
+ }
2293
+ },
2294
+ viewingGroup: 26010,
2295
+ scamin: 9e4
2296
+ },
2297
+ DWRTCL: {
2298
+ // Deepwater Route Centerline — LineString feature (not a polygon
2299
+ // boundary) running down the middle of a DWRTPT. Same magenta dashed
2300
+ // style as MAGVAR isogonic lines but slightly heavier so the
2301
+ // route-axis cue reads against busy water.
2302
+ objectClass: "DWRTCL",
2303
+ name: "Deepwater Route Centerline",
2304
+ category: "Regulated Areas",
2305
+ displayCategory: "STANDARD",
2306
+ int1Ref: "M27",
2307
+ style: {
2308
+ type: "line",
2309
+ paint: {
2310
+ "line-color": "CHMGD",
2311
+ "line-width": 0.8,
2312
+ "line-dasharray": [6, 3],
2313
+ "line-opacity": 0.7
2314
+ }
2315
+ },
2316
+ viewingGroup: 26010,
2317
+ scamin: 9e4
2318
+ },
2319
+ TWRTPT: {
2320
+ // Two-Way Route Part — polygon segment of an IMO-adopted two-way
2321
+ // route. S-52 INT1 M28 portrayal includes paired arrow centroid
2322
+ // symbols (TWRTPT52 / TWRTPT53 sprites exist in the atlas but are a
2323
+ // labels.ts follow-up). Boundary-only here.
2324
+ objectClass: "TWRTPT",
2325
+ name: "Two-Way Route Part",
2326
+ category: "Regulated Areas",
2327
+ displayCategory: "STANDARD",
2328
+ int1Ref: "M28",
2329
+ style: {
2330
+ type: "line",
2331
+ paint: {
2332
+ "line-color": "CHMGD",
2333
+ "line-width": 1.2,
2334
+ "line-dasharray": [4, 3],
2335
+ "line-opacity": 0.7
2336
+ }
2337
+ },
2338
+ viewingGroup: 26010,
2339
+ scamin: 9e4
2340
+ },
2047
2341
  // ── Infrastructure (INT1 Sections D, F, L) ──────────────
2048
2342
  BRIDGE: {
2049
2343
  objectClass: "BRIDGE",
@@ -2352,6 +2646,56 @@ var S52_LOOKUP = {
2352
2646
  viewingGroup: 12410,
2353
2647
  scamin: 22e3
2354
2648
  },
2649
+ // Coast Guard station — emergency-response context. Mariners look for
2650
+ // these when navigating unfamiliar coastline or planning a distress-call
2651
+ // contingency. INT1 F62. Symbolised with the same generic tower glyph
2652
+ // (BCNTOW01) used by BUISGL and the CATLMK=17 LNDMRK variant — a
2653
+ // dedicated CGUSTA SVG is a reasonable follow-up but not required for
2654
+ // the structural-presence cue. Surfaced by the weekly layer-coverage
2655
+ // audit (issue #575); 17 Point features in the NOAA corpus.
2656
+ CGUSTA: {
2657
+ objectClass: "CGUSTA",
2658
+ name: "Coast Guard Station",
2659
+ category: "Infrastructure",
2660
+ displayCategory: "OTHER",
2661
+ int1Ref: "F62",
2662
+ style: { type: "symbol", paint: {}, layout: { "icon-size": 0.6, "icon-allow-overlap": true }, iconImage: "BCNTOW01" },
2663
+ viewingGroup: 12410,
2664
+ scamin: 22e3
2665
+ },
2666
+ // "New Object" — S-57 placeholder class for features the issuing HO has
2667
+ // identified but not yet classified into a more specific class. S-52
2668
+ // PresLib does NOT formally define a portrayal because the class is by
2669
+ // construction not-yet-portrayable, so any distinctive symbol is
2670
+ // permissible — we use a vivid magenta-filled circle with a bold black
2671
+ // stroke so a mariner sees clearly that "there is something unclassified
2672
+ // here." Hiding this class would amount to hiding the HO's own
2673
+ // uncertainty marker, which is the exact opposite of what a chart user
2674
+ // needs. displayCategory: STANDARD (not OTHER) so a default-display
2675
+ // mariner cannot accidentally suppress it; scamin: 0 so the marker
2676
+ // stays visible at every scale band. Surfaced by the weekly layer-
2677
+ // coverage audit (issue #575); 8 Point features in the NOAA corpus.
2678
+ // SAFETY CONTRACT: any future "clean up unused magenta point classes"
2679
+ // pass MUST NOT delete or hide this entry — read the comment block on
2680
+ // issue #575 row 22 before touching.
2681
+ NEWOBJ: {
2682
+ objectClass: "NEWOBJ",
2683
+ name: "New Object (Unclassified)",
2684
+ category: "Hazards",
2685
+ displayCategory: "STANDARD",
2686
+ style: {
2687
+ type: "circle",
2688
+ paint: {
2689
+ "circle-color": "CHMGD",
2690
+ "circle-radius": 5,
2691
+ "circle-stroke-color": "CHBLK",
2692
+ "circle-stroke-width": 1.2,
2693
+ "circle-opacity": 0.85
2694
+ }
2695
+ },
2696
+ viewingGroup: 24020,
2697
+ scamin: 0
2698
+ },
2355
2699
  // ── Overhead & Underground (INT1 Section L) ─────────────
2356
2700
  // Overhead cable — S-52 INT1 L31 portrayal: solid thin magenta line
2357
2701
  // (not dashed) because overhead cables are an in-air navigation hazard
@@ -2403,6 +2747,11 @@ var S52_LOOKUP = {
2403
2747
  // when the sprite image is missing; `fill-pattern` here is the default
2404
2748
  // used if the conditional expression isn't applied for some reason
2405
2749
  // (e.g. a consumer that bypasses `resolvePaint`).
2750
+ //
2751
+ // Polygon (vegetation fill) is the primary variant. The Point variant
2752
+ // ≈ a single marker tree; sub-marine-relevance, so `auditIgnore`
2753
+ // silences the audit for the Point geom-type without touching the
2754
+ // Polygon rendering. See #575 triage matrix.
2406
2755
  style: {
2407
2756
  type: "fill",
2408
2757
  paint: { "fill-color": "LANDF", "fill-opacity": 0.6, "fill-pattern": "VEGATN03" }
@@ -2410,7 +2759,8 @@ var S52_LOOKUP = {
2410
2759
  hasConditional: true,
2411
2760
  conditionalAttributes: ["CATVEG"],
2412
2761
  viewingGroup: 12050,
2413
- scamin: 22e3
2762
+ scamin: 22e3,
2763
+ auditIgnore: true
2414
2764
  },
2415
2765
  SEAARE: {
2416
2766
  // Cross-band water base. SEAARE polygons tile the sea below the
@@ -2589,6 +2939,42 @@ var S52_LOOKUP = {
2589
2939
  viewingGroup: 23010,
2590
2940
  scamin: 0
2591
2941
  },
2942
+ // ── Audit-ignored stubs (non-metadata) ──────────────────
2943
+ // Classes whose non-rendering is a deliberate S-52 portrayal choice
2944
+ // rather than a real gap, but which aren't S-57 metadata. Each is
2945
+ // present here only so the weekly layer-coverage audit (#575) stops
2946
+ // flagging it as missing. Paint is a transparent fill — no visible
2947
+ // output. Swapping `auditIgnore: true` for a real style is a one-line
2948
+ // change if the call ever needs to be reversed.
2949
+ SLOTOP: {
2950
+ // Top of slope — land terrain feature, low marine relevance.
2951
+ objectClass: "SLOTOP",
2952
+ name: "Top of Slope",
2953
+ category: "Land",
2954
+ displayCategory: "OTHER",
2955
+ int1Ref: "C12",
2956
+ style: { type: "fill", paint: { "fill-color": "CHGRF", "fill-opacity": 0 } },
2957
+ auditIgnore: true
2958
+ },
2959
+ SNDWAV: {
2960
+ // Sand waves — niche bathymetric texture feature, not a hazard.
2961
+ objectClass: "SNDWAV",
2962
+ name: "Sand Waves",
2963
+ category: "Depths",
2964
+ displayCategory: "OTHER",
2965
+ int1Ref: "J14",
2966
+ style: { type: "fill", paint: { "fill-color": "CHGRF", "fill-opacity": 0 } },
2967
+ auditIgnore: true
2968
+ },
2969
+ CTRPNT: {
2970
+ // Centerline point — geometric helper, not a feature on the chart.
2971
+ objectClass: "CTRPNT",
2972
+ name: "Centerline Point",
2973
+ category: "Meta",
2974
+ displayCategory: "OTHER",
2975
+ style: { type: "fill", paint: { "fill-color": "CHGRF", "fill-opacity": 0 } },
2976
+ auditIgnore: true
2977
+ },
2592
2978
  // ── Meta (hidden by default) ────────────────────────────
2593
2979
  // M_* and DSID are S-57 metadata classes — they describe the dataset
2594
2980
  // (coverage extent, survey reliability, source publication, etc.) and
@@ -2627,6 +3013,17 @@ var S52_LOOKUP = {
2627
3013
  style: { type: "fill", paint: { "fill-color": "CHGRF", "fill-opacity": 0 } },
2628
3014
  auditIgnore: true
2629
3015
  },
3016
+ M_SDAT: {
3017
+ // Sounding Datum — S-57 metadata describing the vertical datum used
3018
+ // for soundings (typically LAT). Click-query attribute carrier; no
3019
+ // visible shape on its own.
3020
+ objectClass: "M_SDAT",
3021
+ name: "Sounding Datum",
3022
+ category: "Meta",
3023
+ displayCategory: "META",
3024
+ style: { type: "fill", paint: { "fill-color": "CHGRF", "fill-opacity": 0 } },
3025
+ auditIgnore: true
3026
+ },
2630
3027
  M_SREL: {
2631
3028
  objectClass: "M_SREL",
2632
3029
  name: "Survey Reliability",
@@ -2666,6 +3063,16 @@ var S52_DRAW_ORDER = [
2666
3063
  // paired paint-spec change (fill-opacity 0 → 1, color → DEPDW).
2667
3064
  "SEAARE",
2668
3065
  "DEPARE",
3066
+ // 2a. Unsurveyed area — opaque grey wash painted ON TOP of any DEPARE
3067
+ // tile within its extent so depth-tier colours can never imply the area
3068
+ // is surveyed-and-safe. If both DEPARE and UNSARE cover the same pixel,
3069
+ // UNSARE wins on purpose: the hydrographic office classified the polygon
3070
+ // as unsurveyed, so any depth data within it is suspect at best. Sits
3071
+ // below dredged / swept (which represent active survey work) so those
3072
+ // appear if they overlap UNSARE — but in practice they don't, and the
3073
+ // ordering is the safer fallback. Surfaced by the weekly layer-coverage
3074
+ // audit (issue #575); see s52-lookup.ts UNSARE entry for context.
3075
+ "UNSARE",
2669
3076
  // 3. Dredged / swept areas (fill)
2670
3077
  "DRGARE",
2671
3078
  "SWPARE",
@@ -2703,6 +3110,17 @@ var S52_DRAW_ORDER = [
2703
3110
  "LOGPON",
2704
3111
  "CBLARE",
2705
3112
  "PIPARE",
3113
+ // 6a. Jurisdictional zones + IMO routing measures (boundary-only via
3114
+ // LINE_FROM_POLYGON_LAYERS). Slotted at the end of the regulated-area
3115
+ // band so the dashed magenta boundaries paint above the fill family
3116
+ // but below shoreline / depth-contour lines. Closes #575 rows 8–15.
3117
+ "ADMARE",
3118
+ "COSARE",
3119
+ "CONZNE",
3120
+ "EXEZNE",
3121
+ "TSEZNE",
3122
+ "DWRTPT",
3123
+ "TWRTPT",
2706
3124
  // 7. Infrastructure areas (fill)
2707
3125
  "PONTON",
2708
3126
  "HULKES",
@@ -2712,8 +3130,13 @@ var S52_DRAW_ORDER = [
2712
3130
  "PYLONS",
2713
3131
  "FORSTC",
2714
3132
  // 8. Coastline / shore (line)
3133
+ // DYKCON (dyke) and CAUSWY (causeway) join the SLCONS family — solid
3134
+ // black lines representing man-made shoreline modifications. Drawn in
3135
+ // the same band so they layer consistently with adjacent SLCONS edges.
2715
3136
  "COALNE",
2716
3137
  "SLCONS",
3138
+ "DYKCON",
3139
+ "CAUSWY",
2717
3140
  "FNCLNE",
2718
3141
  // 9. Depth contours (line)
2719
3142
  "DEPCNT",
@@ -2724,13 +3147,20 @@ var S52_DRAW_ORDER = [
2724
3147
  // routing and aids-to-nav win the visual hierarchy).
2725
3148
  "MAGVAR",
2726
3149
  // 10. Navigation lines (line)
3150
+ // RAPIDS and OILBAR are direct/operational water hazards. RAPIDS uses
3151
+ // the depth-blue palette (DEPVS dashed) so the feature reads as a
3152
+ // water-surface disturbance; OILBAR uses CHMGD to fit the operational-
3153
+ // hazard family alongside cables / pipelines.
2727
3154
  "NAVLNE",
2728
3155
  "TSSBND",
3156
+ "DWRTCL",
2729
3157
  "ROADWY",
2730
3158
  "RAILWY",
2731
3159
  "TUNNEL",
2732
3160
  "FERYRT",
2733
3161
  "RECTRC",
3162
+ "RAPIDS",
3163
+ "OILBAR",
2734
3164
  "PIPSOL",
2735
3165
  "PIPOHD",
2736
3166
  "CBLSUB",
@@ -2739,6 +3169,11 @@ var S52_DRAW_ORDER = [
2739
3169
  "DAMCON",
2740
3170
  "BERTHS",
2741
3171
  // 11. Point features
3172
+ // CGUSTA (coast guard station, emergency-context) sits with the building
3173
+ // / landmark family — a structural-presence cue rather than a routing
3174
+ // signal. NEWOBJ ("something here we haven't classified yet") goes at
3175
+ // the END of the point list so the distinctive magenta marker stays on
3176
+ // top of neighbouring point features and cannot be visually swallowed.
2742
3177
  "SOUNDG",
2743
3178
  "MORFAC",
2744
3179
  "PILPNT",
@@ -2752,6 +3187,8 @@ var S52_DRAW_ORDER = [
2752
3187
  "CRANES",
2753
3188
  "DISMAR",
2754
3189
  "ACHBRT",
3190
+ "CGUSTA",
3191
+ "NEWOBJ",
2755
3192
  // 12. Nav aids (on top)
2756
3193
  "FOGSIG",
2757
3194
  "RTPBCN",
@@ -5369,6 +5806,7 @@ var SYMBOL_MAP = {
5369
5806
  PILBOP: "PILBOP02",
5370
5807
  SISTAT: "SISTAT02",
5371
5808
  SMCFAC: "ACHARE02",
5809
+ CGUSTA: "BCNTOW01",
5372
5810
  // Land infrastructure
5373
5811
  SILTNK: "TNKCON02",
5374
5812
  BUISGL: "BCNTOW01",
@@ -6025,6 +6463,237 @@ function buildSeabedLabelExpression() {
6025
6463
  const natsurJoined = ["slice", natsurParts, 1];
6026
6464
  return ["concat", natquaPrefix, natsurJoined];
6027
6465
  }
6466
+ function underseaFeatureNameMatch(objnam) {
6467
+ const terms = [
6468
+ "Canyon",
6469
+ "Seamount",
6470
+ "Ridge",
6471
+ "Basin",
6472
+ "Swell",
6473
+ "Knoll",
6474
+ "Shoal",
6475
+ "Ledge",
6476
+ "Bank",
6477
+ "Valley",
6478
+ "Spur",
6479
+ "Escarpment"
6480
+ ];
6481
+ return ["any", ...terms.map((t) => [">=", ["index-of", t, objnam], 0])];
6482
+ }
6483
+ var CATSEA_RELIEF_CODES = [
6484
+ 2,
6485
+ // gat
6486
+ 3,
6487
+ // bank
6488
+ 4,
6489
+ // deep
6490
+ 6,
6491
+ // trench
6492
+ 7,
6493
+ // basin
6494
+ 8,
6495
+ // mud flats
6496
+ 9,
6497
+ // reef
6498
+ 10,
6499
+ // ledge
6500
+ 11,
6501
+ // canyon
6502
+ 13,
6503
+ // shoal
6504
+ 14,
6505
+ // knoll
6506
+ 15,
6507
+ // ridge
6508
+ 16,
6509
+ // seamount
6510
+ 17,
6511
+ // pinnacle
6512
+ 18,
6513
+ // abyssal plain
6514
+ 19,
6515
+ // plateau
6516
+ 20,
6517
+ // spur
6518
+ 21,
6519
+ // shelf
6520
+ 22,
6521
+ // trough
6522
+ 23,
6523
+ // saddle
6524
+ 24,
6525
+ // abyssal hills
6526
+ 25,
6527
+ // apron
6528
+ 26,
6529
+ // archipelagic apron
6530
+ 27,
6531
+ // borderland
6532
+ 28,
6533
+ // continental margin
6534
+ 29,
6535
+ // continental rise
6536
+ 30,
6537
+ // escarpment
6538
+ 31,
6539
+ // fan
6540
+ 32,
6541
+ // fracture zone
6542
+ 33,
6543
+ // gap
6544
+ 34,
6545
+ // guyot
6546
+ 35,
6547
+ // hill
6548
+ 36,
6549
+ // hole
6550
+ 37,
6551
+ // hollow
6552
+ 39,
6553
+ // levee
6554
+ 40,
6555
+ // median valley
6556
+ 41,
6557
+ // moat
6558
+ 42,
6559
+ // mountains
6560
+ 43,
6561
+ // peak
6562
+ 44,
6563
+ // province
6564
+ 45,
6565
+ // rise
6566
+ 47,
6567
+ // seamount chain
6568
+ 48,
6569
+ // shelf-edge
6570
+ 49,
6571
+ // sill
6572
+ 50,
6573
+ // slope
6574
+ 51,
6575
+ // terrace
6576
+ 52
6577
+ // valley
6578
+ ];
6579
+ function buildSeaAreaTierExpression() {
6580
+ const catsea = ["to-number", ["coalesce", ["get", "CATSEA"], ["get", "catsea"], 0]];
6581
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6582
+ return [
6583
+ "case",
6584
+ // Tier 4 — Anchorage detail (before all promotions so "Boston Harbor,
6585
+ // MA …" anchorage compound names don't bubble up).
6586
+ [">=", ["index-of", "Anchorage Area", objnam], 0],
6587
+ 4,
6588
+ // Tier 5 — undersea relief, by authoritative CATSEA code first
6589
+ // (Georges Bank=3, Georges Shoal=13, …), then by name for
6590
+ // uncategorised features. Before promotion shims; see
6591
+ // underseaFeatureNameMatch.
6592
+ ["match", catsea, CATSEA_RELIEF_CODES, true, false],
6593
+ 5,
6594
+ underseaFeatureNameMatch(objnam),
6595
+ 5,
6596
+ // Tier 3 by authoritative CATSEA: bay / narrows.
6597
+ ["match", catsea, [5, 12], true, false],
6598
+ 3,
6599
+ // OBJNAM promotion shims (CATSEA has no ocean/sea/gulf/sound/strait
6600
+ // codes, so names are the only signal for the headline tiers):
6601
+ [">=", ["index-of", "Atlantic Ocean", objnam], 0],
6602
+ 1,
6603
+ [">=", ["index-of", "Pacific Ocean", objnam], 0],
6604
+ 1,
6605
+ [">=", ["index-of", "Arctic Ocean", objnam], 0],
6606
+ 1,
6607
+ [">=", ["index-of", "Gulf of", objnam], 0],
6608
+ 2,
6609
+ [">=", ["index-of", " Bay", objnam], 0],
6610
+ 3,
6611
+ [">=", ["index-of", " Sound", objnam], 0],
6612
+ 3,
6613
+ [">=", ["index-of", " Strait", objnam], 0],
6614
+ 3,
6615
+ [">=", ["index-of", " Sea", objnam], 0],
6616
+ 2,
6617
+ 5
6618
+ ];
6619
+ }
6620
+ function buildBuiltUpAreaTierExpression() {
6621
+ const catbua = ["coalesce", ["get", "CATBUA"], ["get", "catbua"], 0];
6622
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6623
+ const nameLen = ["length", objnam];
6624
+ return [
6625
+ "case",
6626
+ ["==", ["to-number", catbua], 1],
6627
+ 1,
6628
+ ["==", ["to-number", catbua], 5],
6629
+ 2,
6630
+ ["<=", nameLen, 8],
6631
+ 1,
6632
+ ["<=", nameLen, 14],
6633
+ 2,
6634
+ 3
6635
+ ];
6636
+ }
6637
+ function buildLandRegionTierExpression() {
6638
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6639
+ const nameLen = ["length", objnam];
6640
+ return [
6641
+ "case",
6642
+ // NOAA also encodes undersea relief names (e.g. "Block Canyon", 12
6643
+ // chars, no comma) as LNDRGN — without this demotion they'd qualify
6644
+ // as tier-1 "major region" and paint at overview zoom.
6645
+ underseaFeatureNameMatch(objnam),
6646
+ 2,
6647
+ ["all", ["<=", nameLen, 12], ["==", ["index-of", ",", objnam], -1]],
6648
+ 1,
6649
+ 2
6650
+ ];
6651
+ }
6652
+ function buildLandAreaTierExpression() {
6653
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6654
+ return [
6655
+ "case",
6656
+ ["<=", ["length", objnam], 12],
6657
+ 1,
6658
+ 2
6659
+ ];
6660
+ }
6661
+ function buildLakeTierExpression() {
6662
+ const catlak = ["coalesce", ["get", "CATLAK"], ["get", "catlak"], 0];
6663
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6664
+ return [
6665
+ "case",
6666
+ ["==", ["to-number", catlak], 1],
6667
+ 1,
6668
+ ["any", ["==", ["to-number", catlak], 2], ["==", ["to-number", catlak], 3]],
6669
+ 2,
6670
+ ["<=", ["length", objnam], 12],
6671
+ 1,
6672
+ 2
6673
+ ];
6674
+ }
6675
+ function buildRiverTierExpression() {
6676
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6677
+ return [
6678
+ "case",
6679
+ [
6680
+ "all",
6681
+ ["<=", ["length", objnam], 15],
6682
+ ["==", ["index-of", ",", objnam], -1]
6683
+ ],
6684
+ 1,
6685
+ 2
6686
+ ];
6687
+ }
6688
+ function buildAirportTierExpression() {
6689
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6690
+ return [
6691
+ "case",
6692
+ ["<=", ["length", objnam], 25],
6693
+ 1,
6694
+ 2
6695
+ ];
6696
+ }
6028
6697
  var COLOR = {
6029
6698
  WATER: "#9ec1d8",
6030
6699
  WATER_DEEP: "#8fb8d0",
@@ -6045,6 +6714,60 @@ var ORIENTATION_BAND_SORT_KEY = [
6045
6714
  ["slice", ["coalesce", ["get", "_cell_id"], "??9???"], 2, 3],
6046
6715
  9
6047
6716
  ];
6717
+ function orientationLabelLayout(extras = {}) {
6718
+ return {
6719
+ "text-anchor": "center",
6720
+ "text-allow-overlap": false,
6721
+ "text-ignore-placement": false,
6722
+ "text-optional": true,
6723
+ "text-padding": 15,
6724
+ "symbol-sort-key": ORIENTATION_BAND_SORT_KEY,
6725
+ ...extras
6726
+ };
6727
+ }
6728
+ function tieredSizeCurve(tierExpr, stops) {
6729
+ const withHolds = [];
6730
+ for (let i = 0; i < stops.length; i++) {
6731
+ if (i > 0) {
6732
+ const [pz, pv] = stops[i - 1];
6733
+ const [nz, nv] = stops[i];
6734
+ if (pv.some((v, t) => v === 0 && nv[t] > 0)) {
6735
+ const hz = Math.round((nz - 0.2) * 10) / 10;
6736
+ if (hz > pz) {
6737
+ const f = (hz - pz) / (nz - pz);
6738
+ withHolds.push([
6739
+ hz,
6740
+ pv.map(
6741
+ (v, t) => v === 0 && nv[t] > 0 ? 0 : Math.round((v + (nv[t] - v) * f) * 10) / 10
6742
+ )
6743
+ ]);
6744
+ }
6745
+ }
6746
+ }
6747
+ withHolds.push(stops[i]);
6748
+ }
6749
+ const expr = ["interpolate", ["linear"], ["zoom"]];
6750
+ for (const [z, vals] of withHolds) {
6751
+ const m = ["match", tierExpr];
6752
+ for (let t = 0; t < vals.length - 1; t++) m.push(t + 1, vals[t]);
6753
+ m.push(vals[vals.length - 1]);
6754
+ expr.push(z, m);
6755
+ }
6756
+ return expr;
6757
+ }
6758
+ var ORIENTATION_HALO_WIDTH = [
6759
+ "interpolate",
6760
+ ["linear"],
6761
+ ["zoom"],
6762
+ 8,
6763
+ 1.6,
6764
+ 11,
6765
+ 1.5,
6766
+ 14,
6767
+ 1,
6768
+ 16,
6769
+ 0.7
6770
+ ];
6048
6771
  var LABEL_BEHAVIOR = {
6049
6772
  // Orientation labels — render once across all bands.
6050
6773
  SEAARE: "orientation-cross-band",
@@ -6159,35 +6882,34 @@ function getLabelConfig(layerName, opts = {}) {
6159
6882
  }
6160
6883
  };
6161
6884
  }
6162
- case "LNDRGN":
6885
+ case "LNDRGN": {
6886
+ const tierExpr = buildLandRegionTierExpression();
6163
6887
  return {
6164
6888
  type: "symbol",
6165
6889
  textField: ["get", "OBJNAM"],
6166
- textSize: [
6167
- "interpolate",
6168
- ["linear"],
6169
- ["zoom"],
6170
- 8,
6171
- 10,
6172
- 12,
6173
- 12,
6174
- 15,
6175
- 14
6176
- ],
6890
+ // Tier-1 entry moved from z5 to z7: undersea-relief names without a
6891
+ // generic keyword ("Three Dory", "Outer Fall") can't be demoted by
6892
+ // pattern, so the overview/general bands stay clean by holding ALL
6893
+ // LNDRGN labels until coastal zoom. The Carto basemap already labels
6894
+ // major land regions at z5-z6. Columns: t1 major region, t2 local
6895
+ // point (also the default).
6896
+ textSize: tieredSizeCurve(tierExpr, [
6897
+ [5, [0, 0]],
6898
+ [7, [11, 0]],
6899
+ [9, [12, 0]],
6900
+ [11, [12, 9]],
6901
+ [13, [11, 11]],
6902
+ [16, [10, 10]]
6903
+ ]),
6177
6904
  textColor: DARK_BG,
6178
6905
  textHaloColor: COLOR.LAND_REGION,
6179
- textHaloWidth: 1.4,
6906
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6180
6907
  textOffset: [0, 0],
6181
6908
  textFont: ["Open Sans Regular"],
6182
- minzoom: 8,
6183
- layout: {
6184
- "text-anchor": "center",
6185
- "text-allow-overlap": false,
6186
- "text-padding": 6,
6187
- "text-optional": true,
6188
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6189
- }
6909
+ minzoom: 5,
6910
+ layout: orientationLabelLayout()
6190
6911
  };
6912
+ }
6191
6913
  case "LNDMRK":
6192
6914
  return {
6193
6915
  type: "symbol",
@@ -6316,97 +7038,97 @@ function getLabelConfig(layerName, opts = {}) {
6316
7038
  }
6317
7039
  };
6318
7040
  }
6319
- case "SEAARE":
7041
+ case "SEAARE": {
7042
+ const tierExpr = buildSeaAreaTierExpression();
6320
7043
  return {
6321
7044
  type: "symbol",
6322
7045
  textField: ["get", "OBJNAM"],
6323
- textSize: [
6324
- "interpolate",
6325
- ["linear"],
6326
- ["zoom"],
6327
- 5,
6328
- 11,
6329
- 8,
6330
- 13,
6331
- 11,
6332
- 15,
6333
- 14,
6334
- 17
6335
- ],
7046
+ // Default tier (CATSEA=0, uncategorised: canyons, seamounts, fishing
7047
+ // banks etc.) is suppressed at overview/general/coastal scales because
7048
+ // it floods the overview with hundreds of small italic labels that
7049
+ // crowd out the headline Ocean/Sea/Gulf names. It becomes visible only
7050
+ // at approach scale (z9) when the user has zoomed in enough that
7051
+ // individual banks are useful context. Columns: t1 ocean, t2 sea/gulf,
7052
+ // t3 bay/sound, t4 anchorage, default.
7053
+ textSize: tieredSizeCurve(tierExpr, [
7054
+ [3, [14, 8, 0, 0, 0]],
7055
+ [5, [16, 13, 9, 0, 0]],
7056
+ [7, [14, 14, 12, 0, 0]],
7057
+ [9, [12, 13, 14, 0, 9]],
7058
+ [11, [10, 12, 13, 0, 11]],
7059
+ [13, [9, 11, 12, 10, 11]],
7060
+ [16, [9, 10, 11, 11, 10]]
7061
+ ]),
6336
7062
  textColor: DARK_BG,
6337
7063
  textHaloColor: COLOR.WATER_DEEP,
6338
- textHaloWidth: 1.8,
7064
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6339
7065
  textOffset: [0, 0],
6340
7066
  textFont: ["Open Sans Italic"],
6341
7067
  minzoom: 5,
6342
- layout: {
6343
- "text-anchor": "center",
6344
- "text-allow-overlap": false,
6345
- "text-padding": 10,
6346
- "text-optional": true,
7068
+ layout: orientationLabelLayout({
6347
7069
  "text-letter-spacing": 0.15,
6348
7070
  "text-max-width": 9,
6349
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6350
- }
7071
+ // SEAARE is always-paint, unlike the other orientation classes.
7072
+ // MapLibre places symbols topmost-layer-first, so with
7073
+ // allow-overlap:false the dense sounding field (drawn above)
7074
+ // collision-starves big-water names — observed on staging at z8:
7075
+ // only 2 SEAARE labels survived viewport-wide, with Cape Cod Bay
7076
+ // and Gulf of Maine dropped entirely. Always-paint is safe for
7077
+ // this class because (a) the dominance composition in
7078
+ // fanOrientationLabelByCellBand suppresses cross-band duplicate
7079
+ // copies, and (b) tier sizing zeroes relief/anchorage names at
7080
+ // the zooms where density is a problem. Rivers/lakes/land keep
7081
+ // collision-dedup — their within-band multi-polygon stacking was
7082
+ // the original complaint, and they lack big-water headline value.
7083
+ "text-allow-overlap": true,
7084
+ "text-ignore-placement": true,
7085
+ "text-optional": false,
7086
+ "text-padding": 2
7087
+ })
6351
7088
  };
6352
- case "LAKARE":
7089
+ }
7090
+ case "LAKARE": {
7091
+ const tierExpr = buildLakeTierExpression();
6353
7092
  return {
6354
7093
  type: "symbol",
6355
7094
  textField: ["get", "OBJNAM"],
6356
- textSize: [
6357
- "interpolate",
6358
- ["linear"],
6359
- ["zoom"],
6360
- 8,
6361
- 10,
6362
- 12,
6363
- 12,
6364
- 15,
6365
- 14
6366
- ],
7095
+ textSize: tieredSizeCurve(tierExpr, [
7096
+ [7, [9, 0]],
7097
+ [9, [11, 0]],
7098
+ [11, [11, 9]],
7099
+ [13, [10, 11]],
7100
+ [16, [9, 10]]
7101
+ ]),
6367
7102
  textColor: DARK_BG,
6368
7103
  textHaloColor: COLOR.WATER,
6369
- textHaloWidth: 1.5,
7104
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6370
7105
  textOffset: [0, 0],
6371
7106
  textFont: ["Open Sans Italic"],
6372
- minzoom: 8,
6373
- layout: {
6374
- "text-anchor": "center",
6375
- "text-allow-overlap": false,
6376
- "text-padding": 6,
6377
- "text-optional": true,
6378
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6379
- }
7107
+ minzoom: 7,
7108
+ layout: orientationLabelLayout()
6380
7109
  };
6381
- case "RIVERS":
7110
+ }
7111
+ case "RIVERS": {
7112
+ const tierExpr = buildRiverTierExpression();
6382
7113
  return {
6383
7114
  type: "symbol",
6384
7115
  textField: ["get", "OBJNAM"],
6385
- textSize: [
6386
- "interpolate",
6387
- ["linear"],
6388
- ["zoom"],
6389
- 9,
6390
- 9,
6391
- 12,
6392
- 11,
6393
- 15,
6394
- 13
6395
- ],
7116
+ textSize: tieredSizeCurve(tierExpr, [
7117
+ [7, [9, 0]],
7118
+ [9, [11, 0]],
7119
+ [11, [11, 9]],
7120
+ [13, [10, 10]],
7121
+ [16, [9, 10]]
7122
+ ]),
6396
7123
  textColor: DARK_BG,
6397
7124
  textHaloColor: COLOR.WATER,
6398
- textHaloWidth: 1.4,
7125
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6399
7126
  textOffset: [0, 0],
6400
7127
  textFont: ["Open Sans Italic"],
6401
- minzoom: 9,
6402
- layout: {
6403
- "text-anchor": "center",
6404
- "text-allow-overlap": false,
6405
- "text-padding": 4,
6406
- "text-optional": true,
6407
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6408
- }
7128
+ minzoom: 7,
7129
+ layout: orientationLabelLayout()
6409
7130
  };
7131
+ }
6410
7132
  case "CANALS":
6411
7133
  return {
6412
7134
  type: "symbol",
@@ -6416,83 +7138,67 @@ function getLabelConfig(layerName, opts = {}) {
6416
7138
  ["linear"],
6417
7139
  ["zoom"],
6418
7140
  11,
7141
+ 10,
7142
+ 13,
6419
7143
  9,
6420
- 14,
6421
- 11
7144
+ 16,
7145
+ 9
6422
7146
  ],
6423
7147
  textColor: DARK_BG,
6424
7148
  textHaloColor: COLOR.WATER,
6425
- textHaloWidth: 1.3,
7149
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6426
7150
  textOffset: [0, 0],
6427
7151
  textFont: ["Open Sans Italic"],
6428
7152
  minzoom: 11,
6429
- layout: {
6430
- "text-anchor": "center",
6431
- "text-allow-overlap": false,
6432
- "text-padding": 4,
6433
- "text-optional": true,
6434
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6435
- }
7153
+ layout: orientationLabelLayout()
6436
7154
  };
6437
- case "LNDARE":
7155
+ case "LNDARE": {
7156
+ const tierExpr = buildLandAreaTierExpression();
6438
7157
  return {
6439
7158
  type: "symbol",
6440
7159
  textField: ["get", "OBJNAM"],
6441
- textSize: [
6442
- "interpolate",
6443
- ["linear"],
6444
- ["zoom"],
6445
- 8,
6446
- 9,
6447
- 12,
6448
- 11,
6449
- 15,
6450
- 13
6451
- ],
7160
+ textSize: tieredSizeCurve(tierExpr, [
7161
+ [5, [8, 0]],
7162
+ [7, [10, 0]],
7163
+ [9, [11, 0]],
7164
+ [11, [11, 9]],
7165
+ [13, [10, 10]],
7166
+ [16, [9, 10]]
7167
+ ]),
6452
7168
  textColor: DARK_BG,
6453
7169
  textHaloColor: COLOR.LAND_NATURAL,
6454
- textHaloWidth: 1.4,
7170
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6455
7171
  textOffset: [0, 0],
6456
7172
  textFont: ["Open Sans Regular"],
6457
- minzoom: 8,
6458
- layout: {
6459
- "text-anchor": "center",
6460
- "text-allow-overlap": false,
6461
- "text-padding": 6,
6462
- "text-optional": true,
6463
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6464
- }
7173
+ minzoom: 5,
7174
+ layout: orientationLabelLayout()
6465
7175
  };
6466
- case "BUAARE":
7176
+ }
7177
+ case "BUAARE": {
7178
+ const tierExpr = buildBuiltUpAreaTierExpression();
6467
7179
  return {
6468
7180
  type: "symbol",
6469
7181
  textField: ["get", "OBJNAM"],
6470
- textSize: [
6471
- "interpolate",
6472
- ["linear"],
6473
- ["zoom"],
6474
- 7,
6475
- 11,
6476
- 10,
6477
- 13,
6478
- 13,
6479
- 15
6480
- ],
7182
+ textSize: tieredSizeCurve(tierExpr, [
7183
+ [3, [11, 0, 0]],
7184
+ [5, [13, 0, 0]],
7185
+ [7, [13, 9, 0]],
7186
+ [9, [12, 12, 0]],
7187
+ [11, [11, 12, 10]],
7188
+ [13, [10, 11, 12]],
7189
+ [16, [10, 10, 11]]
7190
+ ]),
6481
7191
  textColor: DARK_BG,
6482
7192
  textHaloColor: COLOR.CITY,
6483
- textHaloWidth: 1.6,
7193
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6484
7194
  textOffset: [0, 0],
6485
7195
  textFont: ["Open Sans Regular"],
6486
- minzoom: 7,
6487
- layout: {
6488
- "text-anchor": "center",
6489
- "text-allow-overlap": false,
6490
- "text-padding": 8,
6491
- "text-optional": true,
6492
- "text-letter-spacing": 0.05,
6493
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6494
- }
7196
+ minzoom: 3,
7197
+ layout: orientationLabelLayout({
7198
+ "text-letter-spacing": 0.05
7199
+ })
6495
7200
  };
7201
+ }
6496
7202
  case "BRIDGE":
6497
7203
  return {
6498
7204
  type: "symbol",
@@ -6560,33 +7266,27 @@ function getLabelConfig(layerName, opts = {}) {
6560
7266
  "text-optional": true
6561
7267
  }
6562
7268
  };
6563
- case "AIRARE":
7269
+ case "AIRARE": {
7270
+ const tierExpr = buildAirportTierExpression();
6564
7271
  return {
6565
7272
  type: "symbol",
6566
7273
  textField: ["get", "OBJNAM"],
6567
- textSize: [
6568
- "interpolate",
6569
- ["linear"],
6570
- ["zoom"],
6571
- 9,
6572
- 9,
6573
- 13,
6574
- 11
6575
- ],
7274
+ textSize: tieredSizeCurve(tierExpr, [
7275
+ [7, [9, 0]],
7276
+ [9, [11, 0]],
7277
+ [11, [11, 9]],
7278
+ [13, [10, 10]],
7279
+ [16, [9, 9]]
7280
+ ]),
6576
7281
  textColor: DARK_BG,
6577
7282
  textHaloColor: COLOR.AIRPORT,
6578
- textHaloWidth: 1.4,
7283
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6579
7284
  textOffset: [0, 0],
6580
7285
  textFont: ["Open Sans Regular"],
6581
- minzoom: 9,
6582
- layout: {
6583
- "text-anchor": "center",
6584
- "text-allow-overlap": false,
6585
- "text-padding": 5,
6586
- "text-optional": true,
6587
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6588
- }
7286
+ minzoom: 7,
7287
+ layout: orientationLabelLayout()
6589
7288
  };
7289
+ }
6590
7290
  case "SBDARE":
6591
7291
  return {
6592
7292
  type: "symbol",
@@ -6659,26 +7359,20 @@ function getLabelConfig(layerName, opts = {}) {
6659
7359
  "interpolate",
6660
7360
  ["linear"],
6661
7361
  ["zoom"],
7362
+ 11,
7363
+ 9,
7364
+ 13,
6662
7365
  8,
6663
- 10,
6664
- 12,
6665
- 12,
6666
- 15,
6667
- 14
7366
+ 16,
7367
+ 8
6668
7368
  ],
6669
7369
  textColor: "#c545c3",
6670
7370
  textHaloColor: DARK_BG,
6671
- textHaloWidth: 1.4,
7371
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6672
7372
  textOffset: [0, 0],
6673
7373
  textFont: ["Open Sans Italic"],
6674
7374
  minzoom: 8,
6675
- layout: {
6676
- "text-anchor": "center",
6677
- "text-allow-overlap": false,
6678
- "text-padding": 6,
6679
- "text-optional": true,
6680
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6681
- }
7375
+ layout: orientationLabelLayout()
6682
7376
  };
6683
7377
  case "WEDKLP":
6684
7378
  return {
@@ -8785,7 +9479,21 @@ var LINE_FROM_POLYGON_LAYERS = /* @__PURE__ */ new Set([
8785
9479
  "MIPARE",
8786
9480
  "TESARE",
8787
9481
  "MAGVAR",
8788
- "PRDARE"
9482
+ "PRDARE",
9483
+ "TUNNEL",
9484
+ // Jurisdictional zones (INT1 N40–N47) and IMO routing measures
9485
+ // (INT1 M14, M27, M28) — magenta dashed boundary, no fill. Vast
9486
+ // offshore polygons would tint half the chart if flood-filled, so
9487
+ // every member here renders as boundary-only. DWRTCL is not listed
9488
+ // because it's a native LineString class, not a polygon-as-line.
9489
+ // Closes issue #575 rows 8–15.
9490
+ "ADMARE",
9491
+ "COSARE",
9492
+ "CONZNE",
9493
+ "EXEZNE",
9494
+ "TSEZNE",
9495
+ "DWRTPT",
9496
+ "TWRTPT"
8789
9497
  ]);
8790
9498
  var ZOOM_SCALED_LINES2 = {
8791
9499
  DEPCNT: [8, 0.3, 14, 0.8],
@@ -9773,6 +10481,90 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION) {
9773
10481
  metadata: { "s52:objectClass": "RECTRC", "s52:role": "polygon" }
9774
10482
  });
9775
10483
  }
10484
+ if (objectClass === "PONTON" && resolved.type === "fill") {
10485
+ layers.push({
10486
+ id: `s52-PONTON-line`,
10487
+ type: "line",
10488
+ source: sourceId,
10489
+ "source-layer": "PONTON",
10490
+ filter: GEOM_FILTERS.line,
10491
+ paint: {
10492
+ "line-color": resolveColor("CHGRF", ct),
10493
+ "line-width": 1.5
10494
+ },
10495
+ metadata: { "s52:objectClass": "PONTON", "s52:role": "line" }
10496
+ });
10497
+ }
10498
+ if (objectClass === "SILTNK" && resolved.type === "symbol") {
10499
+ layers.push({
10500
+ id: `s52-SILTNK-symbol-polygon`,
10501
+ type: "symbol",
10502
+ source: sourceId,
10503
+ "source-layer": "SILTNK",
10504
+ filter: GEOM_FILTERS.fill,
10505
+ layout: {
10506
+ "icon-image": "TNKCON02",
10507
+ "icon-size": 0.6,
10508
+ "icon-allow-overlap": true,
10509
+ "icon-ignore-placement": false,
10510
+ "symbol-placement": "point"
10511
+ },
10512
+ paint: {},
10513
+ metadata: { "s52:objectClass": "SILTNK", "s52:role": "symbol-polygon" }
10514
+ });
10515
+ }
10516
+ if (objectClass === "ACHBRT" && resolved.type === "symbol") {
10517
+ layers.push({
10518
+ id: `s52-ACHBRT-symbol-polygon`,
10519
+ type: "symbol",
10520
+ source: sourceId,
10521
+ "source-layer": "ACHBRT",
10522
+ filter: GEOM_FILTERS.fill,
10523
+ layout: {
10524
+ "icon-image": "ACHARE02",
10525
+ "icon-size": 0.5,
10526
+ "icon-allow-overlap": true,
10527
+ "icon-ignore-placement": false,
10528
+ "symbol-placement": "point"
10529
+ },
10530
+ paint: {},
10531
+ metadata: { "s52:objectClass": "ACHBRT", "s52:role": "symbol-polygon" }
10532
+ });
10533
+ }
10534
+ if (objectClass === "BERTHS" && resolved.type === "line") {
10535
+ layers.push({
10536
+ id: `s52-BERTHS-circle`,
10537
+ type: "circle",
10538
+ source: sourceId,
10539
+ "source-layer": "BERTHS",
10540
+ filter: GEOM_FILTERS.circle,
10541
+ paint: {
10542
+ "circle-color": resolveColor("CHBLK", ct),
10543
+ "circle-radius": ["interpolate", ["linear"], ["zoom"], 12, 1.5, 14, 2.2, 17, 3],
10544
+ "circle-stroke-color": resolveColor("CHBLK", ct),
10545
+ "circle-stroke-width": 0.5
10546
+ },
10547
+ metadata: { "s52:objectClass": "BERTHS", "s52:role": "point" }
10548
+ });
10549
+ }
10550
+ if (objectClass === "TUNNEL" && resolved.type === "line") {
10551
+ layers.push({
10552
+ // NOT `s52-TUNNEL-line` — that id is already taken by the main layer
10553
+ // (`s52-${objectClass}-${resolved.type}`), and duplicate layer ids
10554
+ // are a Mapbox style-spec violation (caught by style-integrity.test).
10555
+ id: `s52-TUNNEL-linestring`,
10556
+ type: "line",
10557
+ source: sourceId,
10558
+ "source-layer": "TUNNEL",
10559
+ filter: GEOM_FILTERS.line,
10560
+ paint: {
10561
+ "line-color": resolveColor("CHGRD", ct),
10562
+ "line-width": 1,
10563
+ "line-dasharray": [2, 2]
10564
+ },
10565
+ metadata: { "s52:objectClass": "TUNNEL", "s52:role": "line" }
10566
+ });
10567
+ }
9776
10568
  }
9777
10569
  return layers;
9778
10570
  }
@@ -10008,9 +10800,21 @@ var META_LAYERS = /* @__PURE__ */ new Set([
10008
10800
  "M_CSCL",
10009
10801
  "TS_FEB",
10010
10802
  "TS_TIS",
10011
- "COSARE",
10012
- "NEWOBJ",
10013
10803
  "LOCMAG"
10804
+ // COSARE was previously suppressed here as meta. The weekly layer-
10805
+ // coverage audit (issue #575) classified it as a jurisdictional polygon
10806
+ // (S-52 INT1 N40, same family as ADMARE / TESARE). It now has an
10807
+ // S52_LOOKUP entry and renders as a magenta dashed boundary; suppressing
10808
+ // it as meta would defeat that wire-up.
10809
+ //
10810
+ // NEWOBJ was likewise previously suppressed here. That was the source
10811
+ // of audit gap row 22: NEWOBJ is the S-57 placeholder for features the
10812
+ // HO has identified but not yet classified. Skipping it silently hid
10813
+ // the HO's own "we don't know what this is, but it is here" signal —
10814
+ // the opposite of what mariners need. The class now renders via its
10815
+ // S52_LOOKUP entry as a vivid magenta circle, displayCategory STANDARD,
10816
+ // scamin 0. Do not put it back here — see the lookup entry for the
10817
+ // safety contract.
10014
10818
  ]);
10015
10819
  var PALETTE_NAMES = {
10016
10820
  DAY: "DAY_BRIGHT",
@@ -10101,21 +10905,58 @@ function makeBandVariants(layer, B) {
10101
10905
  }
10102
10906
  return variants;
10103
10907
  }
10104
- function fanOrientationLabelByDominator(label) {
10908
+ var CELL_BAND_OPACITY_CURVE = {
10909
+ "1": [[3, 0.5], [5, 0.3], [16, 0.15]],
10910
+ "2": [[5, 0.5], [7, 0.3], [16, 0.15]],
10911
+ "3": [[7, 0.55], [9, 0.35], [16, 0.18]],
10912
+ "4": [[9, 0.55], [11, 0.35], [16, 0.2]],
10913
+ "5": [[11, 0.6], [13, 0.35], [18, 0.25]],
10914
+ "6": [[13, 0.75], [16, 0.55], [22, 0.45]]
10915
+ };
10916
+ function fanOrientationLabelByCellBand(label) {
10917
+ const cellBandExpr = [
10918
+ "slice",
10919
+ ["coalesce", ["get", "_cell_id"], "??9???"],
10920
+ 2,
10921
+ 3
10922
+ ];
10105
10923
  const variants = [];
10106
- variants.push({
10107
- ...label,
10108
- id: `${label.id}-notdom`,
10109
- filter: combineWithBandFilter(label.filter, NOT_DOMINATED_FILTER)
10110
- });
10111
- for (const D of BAND_DRAW_ORDER) {
10112
- if (D === "1") continue;
10924
+ for (const code of BAND_DRAW_ORDER) {
10925
+ const range = BAND_PRIORITY_RANGES[code];
10926
+ const curve = CELL_BAND_OPACITY_CURVE[code];
10927
+ const opacityExpr = ["interpolate", ["linear"], ["zoom"]];
10928
+ for (const [z, o] of curve) opacityExpr.push(z, o);
10929
+ const cellFiltered = combineWithBandFilter(
10930
+ label.filter,
10931
+ ["==", cellBandExpr, code]
10932
+ );
10933
+ const minzoom = Math.max(
10934
+ label.minzoom ?? range.minzoom,
10935
+ range.minzoom
10936
+ );
10937
+ const paint = {
10938
+ ...label.paint ?? {},
10939
+ "text-opacity": opacityExpr
10940
+ };
10113
10941
  variants.push({
10114
10942
  ...label,
10115
- id: `${label.id}-domBy${D}`,
10116
- filter: combineWithBandFilter(label.filter, dominatedByMinFilter(D)),
10117
- maxzoom: BAND_PRIORITY_RANGES[D].minzoom
10943
+ id: `${label.id}-cell${code}-notdom`,
10944
+ filter: combineWithBandFilter(cellFiltered, NOT_DOMINATED_FILTER),
10945
+ minzoom,
10946
+ paint
10118
10947
  });
10948
+ for (const D of BAND_DRAW_ORDER) {
10949
+ const dom = BAND_PRIORITY_RANGES[D];
10950
+ if (dom.minzoom <= minzoom) continue;
10951
+ variants.push({
10952
+ ...label,
10953
+ id: `${label.id}-cell${code}-domBy${D}`,
10954
+ filter: combineWithBandFilter(cellFiltered, dominatedByMinFilter(D)),
10955
+ minzoom,
10956
+ maxzoom: dom.minzoom,
10957
+ paint
10958
+ });
10959
+ }
10119
10960
  }
10120
10961
  return variants;
10121
10962
  }
@@ -10239,7 +11080,7 @@ function generateStyle(palette = "DAY", opts = {}) {
10239
11080
  for (const { objectClass, label } of bundles) {
10240
11081
  if (!label) continue;
10241
11082
  if (getLabelBehavior(objectClass) === "orientation-cross-band") {
10242
- labelLayers.push(...fanOrientationLabelByDominator(label));
11083
+ labelLayers.push(...fanOrientationLabelByCellBand(label));
10243
11084
  }
10244
11085
  }
10245
11086
  for (const code of BAND_DRAW_ORDER) {