@chrtco/sdk 0.7.3 → 0.7.4

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,124 @@ function buildSeabedLabelExpression() {
6025
6463
  const natsurJoined = ["slice", natsurParts, 1];
6026
6464
  return ["concat", natquaPrefix, natsurJoined];
6027
6465
  }
6466
+ function buildSeaAreaTierExpression() {
6467
+ const catsea = ["coalesce", ["get", "CATSEA"], ["get", "catsea"], 0];
6468
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6469
+ return [
6470
+ "case",
6471
+ // Tier 1 — Ocean (CATSEA=31 OR OBJNAM contains "Ocean", excluding
6472
+ // anchorage-area-style compound names which we filter on later).
6473
+ ["==", ["to-number", catsea], 31],
6474
+ 1,
6475
+ ["any", ["==", ["to-number", catsea], 29], ["==", ["to-number", catsea], 30]],
6476
+ 2,
6477
+ [
6478
+ "any",
6479
+ ["==", ["to-number", catsea], 4],
6480
+ ["==", ["to-number", catsea], 11],
6481
+ ["==", ["to-number", catsea], 27],
6482
+ ["==", ["to-number", catsea], 28]
6483
+ ],
6484
+ 3,
6485
+ // Tier 4 — Anchorage detail (must be checked before OBJNAM-substring
6486
+ // promotion so "Boston Harbor, MA …" anchorages don't bubble up).
6487
+ [">=", ["index-of", "Anchorage Area", objnam], 0],
6488
+ 4,
6489
+ // OBJNAM-substring promotion shims (run AFTER tier 4 demotion):
6490
+ [">=", ["index-of", "Atlantic Ocean", objnam], 0],
6491
+ 1,
6492
+ [">=", ["index-of", "Pacific Ocean", objnam], 0],
6493
+ 1,
6494
+ [">=", ["index-of", "Arctic Ocean", objnam], 0],
6495
+ 1,
6496
+ [">=", ["index-of", "Gulf of", objnam], 0],
6497
+ 2,
6498
+ // Bay/Sound/Sea/Strait/Channel as a tier-3 shim (matches "Massachusetts
6499
+ // Bay", "Buzzards Bay", "Long Island Sound", "Block Island Sound",
6500
+ // "Vineyard Sound", "Cape Cod Bay", etc. without needing CATSEA).
6501
+ [">=", ["index-of", " Bay", objnam], 0],
6502
+ 3,
6503
+ [">=", ["index-of", " Sound", objnam], 0],
6504
+ 3,
6505
+ [">=", ["index-of", " Strait", objnam], 0],
6506
+ 3,
6507
+ [">=", ["index-of", " Sea", objnam], 0],
6508
+ 2,
6509
+ 5
6510
+ ];
6511
+ }
6512
+ function buildBuiltUpAreaTierExpression() {
6513
+ const catbua = ["coalesce", ["get", "CATBUA"], ["get", "catbua"], 0];
6514
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6515
+ const nameLen = ["length", objnam];
6516
+ return [
6517
+ "case",
6518
+ ["==", ["to-number", catbua], 1],
6519
+ 1,
6520
+ ["==", ["to-number", catbua], 5],
6521
+ 2,
6522
+ ["<=", nameLen, 8],
6523
+ 1,
6524
+ ["<=", nameLen, 14],
6525
+ 2,
6526
+ 3
6527
+ ];
6528
+ }
6529
+ function buildLandRegionTierExpression() {
6530
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6531
+ const nameLen = ["length", objnam];
6532
+ return [
6533
+ "case",
6534
+ ["all", ["<=", nameLen, 12], ["==", ["index-of", ",", objnam], -1]],
6535
+ 1,
6536
+ 2
6537
+ ];
6538
+ }
6539
+ function buildLandAreaTierExpression() {
6540
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6541
+ return [
6542
+ "case",
6543
+ ["<=", ["length", objnam], 12],
6544
+ 1,
6545
+ 2
6546
+ ];
6547
+ }
6548
+ function buildLakeTierExpression() {
6549
+ const catlak = ["coalesce", ["get", "CATLAK"], ["get", "catlak"], 0];
6550
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6551
+ return [
6552
+ "case",
6553
+ ["==", ["to-number", catlak], 1],
6554
+ 1,
6555
+ ["any", ["==", ["to-number", catlak], 2], ["==", ["to-number", catlak], 3]],
6556
+ 2,
6557
+ ["<=", ["length", objnam], 12],
6558
+ 1,
6559
+ 2
6560
+ ];
6561
+ }
6562
+ function buildRiverTierExpression() {
6563
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6564
+ return [
6565
+ "case",
6566
+ [
6567
+ "all",
6568
+ ["<=", ["length", objnam], 15],
6569
+ ["==", ["index-of", ",", objnam], -1]
6570
+ ],
6571
+ 1,
6572
+ 2
6573
+ ];
6574
+ }
6575
+ function buildAirportTierExpression() {
6576
+ const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6577
+ return [
6578
+ "case",
6579
+ ["<=", ["length", objnam], 25],
6580
+ 1,
6581
+ 2
6582
+ ];
6583
+ }
6028
6584
  var COLOR = {
6029
6585
  WATER: "#9ec1d8",
6030
6586
  WATER_DEEP: "#8fb8d0",
@@ -6045,6 +6601,30 @@ var ORIENTATION_BAND_SORT_KEY = [
6045
6601
  ["slice", ["coalesce", ["get", "_cell_id"], "??9???"], 2, 3],
6046
6602
  9
6047
6603
  ];
6604
+ function orientationLabelLayout(extras = {}) {
6605
+ return {
6606
+ "text-anchor": "center",
6607
+ "text-allow-overlap": false,
6608
+ "text-ignore-placement": false,
6609
+ "text-optional": true,
6610
+ "text-padding": 15,
6611
+ "symbol-sort-key": ORIENTATION_BAND_SORT_KEY,
6612
+ ...extras
6613
+ };
6614
+ }
6615
+ var ORIENTATION_HALO_WIDTH = [
6616
+ "interpolate",
6617
+ ["linear"],
6618
+ ["zoom"],
6619
+ 8,
6620
+ 1.6,
6621
+ 11,
6622
+ 1.5,
6623
+ 14,
6624
+ 1,
6625
+ 16,
6626
+ 0.7
6627
+ ];
6048
6628
  var LABEL_BEHAVIOR = {
6049
6629
  // Orientation labels — render once across all bands.
6050
6630
  SEAARE: "orientation-cross-band",
@@ -6159,7 +6739,17 @@ function getLabelConfig(layerName, opts = {}) {
6159
6739
  }
6160
6740
  };
6161
6741
  }
6162
- case "LNDRGN":
6742
+ case "LNDRGN": {
6743
+ const tierExpr = buildLandRegionTierExpression();
6744
+ const sizeAt = (t1, t2) => [
6745
+ "match",
6746
+ tierExpr,
6747
+ 1,
6748
+ t1,
6749
+ 2,
6750
+ t2,
6751
+ t2
6752
+ ];
6163
6753
  return {
6164
6754
  type: "symbol",
6165
6755
  textField: ["get", "OBJNAM"],
@@ -6167,27 +6757,28 @@ function getLabelConfig(layerName, opts = {}) {
6167
6757
  "interpolate",
6168
6758
  ["linear"],
6169
6759
  ["zoom"],
6170
- 8,
6171
- 10,
6172
- 12,
6173
- 12,
6174
- 15,
6175
- 14
6760
+ 5,
6761
+ sizeAt(9, 0),
6762
+ 7,
6763
+ sizeAt(11, 0),
6764
+ 9,
6765
+ sizeAt(12, 0),
6766
+ 11,
6767
+ sizeAt(12, 9),
6768
+ 13,
6769
+ sizeAt(11, 11),
6770
+ 16,
6771
+ sizeAt(10, 10)
6176
6772
  ],
6177
6773
  textColor: DARK_BG,
6178
6774
  textHaloColor: COLOR.LAND_REGION,
6179
- textHaloWidth: 1.4,
6775
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6180
6776
  textOffset: [0, 0],
6181
6777
  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
- }
6778
+ minzoom: 5,
6779
+ layout: orientationLabelLayout()
6190
6780
  };
6781
+ }
6191
6782
  case "LNDMRK":
6192
6783
  return {
6193
6784
  type: "symbol",
@@ -6316,40 +6907,72 @@ function getLabelConfig(layerName, opts = {}) {
6316
6907
  }
6317
6908
  };
6318
6909
  }
6319
- case "SEAARE":
6910
+ case "SEAARE": {
6911
+ const tierExpr = buildSeaAreaTierExpression();
6912
+ const sizeAt = (t1, t2, t3, t4, def) => [
6913
+ "match",
6914
+ tierExpr,
6915
+ 1,
6916
+ t1,
6917
+ 2,
6918
+ t2,
6919
+ 3,
6920
+ t3,
6921
+ 4,
6922
+ t4,
6923
+ def
6924
+ ];
6320
6925
  return {
6321
6926
  type: "symbol",
6322
6927
  textField: ["get", "OBJNAM"],
6928
+ // Default tier (CATSEA=0, uncategorised: canyons, seamounts, fishing
6929
+ // banks etc.) is suppressed at overview/general/coastal scales because
6930
+ // it floods the overview with hundreds of small italic labels that
6931
+ // crowd out the headline Ocean/Sea/Gulf names. It becomes visible only
6932
+ // at approach scale (z9) when the user has zoomed in enough that
6933
+ // individual banks are useful context.
6323
6934
  textSize: [
6324
6935
  "interpolate",
6325
6936
  ["linear"],
6326
6937
  ["zoom"],
6938
+ 3,
6939
+ sizeAt(14, 8, 0, 0, 0),
6327
6940
  5,
6941
+ sizeAt(16, 13, 9, 0, 0),
6942
+ 7,
6943
+ sizeAt(14, 14, 12, 0, 0),
6944
+ 9,
6945
+ sizeAt(12, 13, 14, 0, 9),
6328
6946
  11,
6329
- 8,
6947
+ sizeAt(10, 12, 13, 0, 11),
6330
6948
  13,
6331
- 11,
6332
- 15,
6333
- 14,
6334
- 17
6949
+ sizeAt(9, 11, 12, 10, 11),
6950
+ 16,
6951
+ sizeAt(9, 10, 11, 11, 10)
6335
6952
  ],
6336
6953
  textColor: DARK_BG,
6337
6954
  textHaloColor: COLOR.WATER_DEEP,
6338
- textHaloWidth: 1.8,
6955
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6339
6956
  textOffset: [0, 0],
6340
6957
  textFont: ["Open Sans Italic"],
6341
6958
  minzoom: 5,
6342
- layout: {
6343
- "text-anchor": "center",
6344
- "text-allow-overlap": false,
6345
- "text-padding": 10,
6346
- "text-optional": true,
6959
+ layout: orientationLabelLayout({
6347
6960
  "text-letter-spacing": 0.15,
6348
- "text-max-width": 9,
6349
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6350
- }
6961
+ "text-max-width": 9
6962
+ })
6351
6963
  };
6352
- case "LAKARE":
6964
+ }
6965
+ case "LAKARE": {
6966
+ const tierExpr = buildLakeTierExpression();
6967
+ const sizeAt = (t1, t2) => [
6968
+ "match",
6969
+ tierExpr,
6970
+ 1,
6971
+ t1,
6972
+ 2,
6973
+ t2,
6974
+ t2
6975
+ ];
6353
6976
  return {
6354
6977
  type: "symbol",
6355
6978
  textField: ["get", "OBJNAM"],
@@ -6357,28 +6980,37 @@ function getLabelConfig(layerName, opts = {}) {
6357
6980
  "interpolate",
6358
6981
  ["linear"],
6359
6982
  ["zoom"],
6360
- 8,
6361
- 10,
6362
- 12,
6363
- 12,
6364
- 15,
6365
- 14
6983
+ 7,
6984
+ sizeAt(9, 0),
6985
+ 9,
6986
+ sizeAt(11, 0),
6987
+ 11,
6988
+ sizeAt(11, 9),
6989
+ 13,
6990
+ sizeAt(10, 11),
6991
+ 16,
6992
+ sizeAt(9, 10)
6366
6993
  ],
6367
6994
  textColor: DARK_BG,
6368
6995
  textHaloColor: COLOR.WATER,
6369
- textHaloWidth: 1.5,
6996
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6370
6997
  textOffset: [0, 0],
6371
6998
  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
- }
6999
+ minzoom: 7,
7000
+ layout: orientationLabelLayout()
6380
7001
  };
6381
- case "RIVERS":
7002
+ }
7003
+ case "RIVERS": {
7004
+ const tierExpr = buildRiverTierExpression();
7005
+ const sizeAt = (t1, t2) => [
7006
+ "match",
7007
+ tierExpr,
7008
+ 1,
7009
+ t1,
7010
+ 2,
7011
+ t2,
7012
+ t2
7013
+ ];
6382
7014
  return {
6383
7015
  type: "symbol",
6384
7016
  textField: ["get", "OBJNAM"],
@@ -6386,27 +7018,26 @@ function getLabelConfig(layerName, opts = {}) {
6386
7018
  "interpolate",
6387
7019
  ["linear"],
6388
7020
  ["zoom"],
7021
+ 7,
7022
+ sizeAt(9, 0),
6389
7023
  9,
6390
- 9,
6391
- 12,
7024
+ sizeAt(11, 0),
6392
7025
  11,
6393
- 15,
6394
- 13
7026
+ sizeAt(11, 9),
7027
+ 13,
7028
+ sizeAt(10, 10),
7029
+ 16,
7030
+ sizeAt(9, 10)
6395
7031
  ],
6396
7032
  textColor: DARK_BG,
6397
7033
  textHaloColor: COLOR.WATER,
6398
- textHaloWidth: 1.4,
7034
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6399
7035
  textOffset: [0, 0],
6400
7036
  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
- }
7037
+ minzoom: 7,
7038
+ layout: orientationLabelLayout()
6409
7039
  };
7040
+ }
6410
7041
  case "CANALS":
6411
7042
  return {
6412
7043
  type: "symbol",
@@ -6416,25 +7047,31 @@ function getLabelConfig(layerName, opts = {}) {
6416
7047
  ["linear"],
6417
7048
  ["zoom"],
6418
7049
  11,
7050
+ 10,
7051
+ 13,
6419
7052
  9,
6420
- 14,
6421
- 11
7053
+ 16,
7054
+ 9
6422
7055
  ],
6423
7056
  textColor: DARK_BG,
6424
7057
  textHaloColor: COLOR.WATER,
6425
- textHaloWidth: 1.3,
7058
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6426
7059
  textOffset: [0, 0],
6427
7060
  textFont: ["Open Sans Italic"],
6428
7061
  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
- }
7062
+ layout: orientationLabelLayout()
6436
7063
  };
6437
- case "LNDARE":
7064
+ case "LNDARE": {
7065
+ const tierExpr = buildLandAreaTierExpression();
7066
+ const sizeAt = (t1, t2) => [
7067
+ "match",
7068
+ tierExpr,
7069
+ 1,
7070
+ t1,
7071
+ 2,
7072
+ t2,
7073
+ t2
7074
+ ];
6438
7075
  return {
6439
7076
  type: "symbol",
6440
7077
  textField: ["get", "OBJNAM"],
@@ -6442,28 +7079,41 @@ function getLabelConfig(layerName, opts = {}) {
6442
7079
  "interpolate",
6443
7080
  ["linear"],
6444
7081
  ["zoom"],
6445
- 8,
7082
+ 5,
7083
+ sizeAt(8, 0),
7084
+ 7,
7085
+ sizeAt(10, 0),
6446
7086
  9,
6447
- 12,
7087
+ sizeAt(11, 0),
6448
7088
  11,
6449
- 15,
6450
- 13
7089
+ sizeAt(11, 9),
7090
+ 13,
7091
+ sizeAt(10, 10),
7092
+ 16,
7093
+ sizeAt(9, 10)
6451
7094
  ],
6452
7095
  textColor: DARK_BG,
6453
7096
  textHaloColor: COLOR.LAND_NATURAL,
6454
- textHaloWidth: 1.4,
7097
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6455
7098
  textOffset: [0, 0],
6456
7099
  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
- }
7100
+ minzoom: 5,
7101
+ layout: orientationLabelLayout()
6465
7102
  };
6466
- case "BUAARE":
7103
+ }
7104
+ case "BUAARE": {
7105
+ const tierExpr = buildBuiltUpAreaTierExpression();
7106
+ const sizeAt = (t1, t2, t3) => [
7107
+ "match",
7108
+ tierExpr,
7109
+ 1,
7110
+ t1,
7111
+ 2,
7112
+ t2,
7113
+ 3,
7114
+ t3,
7115
+ t3
7116
+ ];
6467
7117
  return {
6468
7118
  type: "symbol",
6469
7119
  textField: ["get", "OBJNAM"],
@@ -6471,28 +7121,32 @@ function getLabelConfig(layerName, opts = {}) {
6471
7121
  "interpolate",
6472
7122
  ["linear"],
6473
7123
  ["zoom"],
7124
+ 3,
7125
+ sizeAt(11, 0, 0),
7126
+ 5,
7127
+ sizeAt(13, 0, 0),
6474
7128
  7,
7129
+ sizeAt(13, 9, 0),
7130
+ 9,
7131
+ sizeAt(12, 12, 0),
6475
7132
  11,
6476
- 10,
7133
+ sizeAt(11, 12, 10),
6477
7134
  13,
6478
- 13,
6479
- 15
7135
+ sizeAt(10, 11, 12),
7136
+ 16,
7137
+ sizeAt(10, 10, 11)
6480
7138
  ],
6481
7139
  textColor: DARK_BG,
6482
7140
  textHaloColor: COLOR.CITY,
6483
- textHaloWidth: 1.6,
7141
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6484
7142
  textOffset: [0, 0],
6485
7143
  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
- }
7144
+ minzoom: 3,
7145
+ layout: orientationLabelLayout({
7146
+ "text-letter-spacing": 0.05
7147
+ })
6495
7148
  };
7149
+ }
6496
7150
  case "BRIDGE":
6497
7151
  return {
6498
7152
  type: "symbol",
@@ -6560,7 +7214,17 @@ function getLabelConfig(layerName, opts = {}) {
6560
7214
  "text-optional": true
6561
7215
  }
6562
7216
  };
6563
- case "AIRARE":
7217
+ case "AIRARE": {
7218
+ const tierExpr = buildAirportTierExpression();
7219
+ const sizeAt = (t1, t2) => [
7220
+ "match",
7221
+ tierExpr,
7222
+ 1,
7223
+ t1,
7224
+ 2,
7225
+ t2,
7226
+ t2
7227
+ ];
6564
7228
  return {
6565
7229
  type: "symbol",
6566
7230
  textField: ["get", "OBJNAM"],
@@ -6568,25 +7232,26 @@ function getLabelConfig(layerName, opts = {}) {
6568
7232
  "interpolate",
6569
7233
  ["linear"],
6570
7234
  ["zoom"],
7235
+ 7,
7236
+ sizeAt(9, 0),
6571
7237
  9,
6572
- 9,
7238
+ sizeAt(11, 0),
7239
+ 11,
7240
+ sizeAt(11, 9),
6573
7241
  13,
6574
- 11
7242
+ sizeAt(10, 10),
7243
+ 16,
7244
+ sizeAt(9, 9)
6575
7245
  ],
6576
7246
  textColor: DARK_BG,
6577
7247
  textHaloColor: COLOR.AIRPORT,
6578
- textHaloWidth: 1.4,
7248
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6579
7249
  textOffset: [0, 0],
6580
7250
  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
- }
7251
+ minzoom: 7,
7252
+ layout: orientationLabelLayout()
6589
7253
  };
7254
+ }
6590
7255
  case "SBDARE":
6591
7256
  return {
6592
7257
  type: "symbol",
@@ -6659,26 +7324,20 @@ function getLabelConfig(layerName, opts = {}) {
6659
7324
  "interpolate",
6660
7325
  ["linear"],
6661
7326
  ["zoom"],
7327
+ 11,
7328
+ 9,
7329
+ 13,
6662
7330
  8,
6663
- 10,
6664
- 12,
6665
- 12,
6666
- 15,
6667
- 14
7331
+ 16,
7332
+ 8
6668
7333
  ],
6669
7334
  textColor: "#c545c3",
6670
7335
  textHaloColor: DARK_BG,
6671
- textHaloWidth: 1.4,
7336
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6672
7337
  textOffset: [0, 0],
6673
7338
  textFont: ["Open Sans Italic"],
6674
7339
  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
- }
7340
+ layout: orientationLabelLayout()
6682
7341
  };
6683
7342
  case "WEDKLP":
6684
7343
  return {
@@ -8785,7 +9444,21 @@ var LINE_FROM_POLYGON_LAYERS = /* @__PURE__ */ new Set([
8785
9444
  "MIPARE",
8786
9445
  "TESARE",
8787
9446
  "MAGVAR",
8788
- "PRDARE"
9447
+ "PRDARE",
9448
+ "TUNNEL",
9449
+ // Jurisdictional zones (INT1 N40–N47) and IMO routing measures
9450
+ // (INT1 M14, M27, M28) — magenta dashed boundary, no fill. Vast
9451
+ // offshore polygons would tint half the chart if flood-filled, so
9452
+ // every member here renders as boundary-only. DWRTCL is not listed
9453
+ // because it's a native LineString class, not a polygon-as-line.
9454
+ // Closes issue #575 rows 8–15.
9455
+ "ADMARE",
9456
+ "COSARE",
9457
+ "CONZNE",
9458
+ "EXEZNE",
9459
+ "TSEZNE",
9460
+ "DWRTPT",
9461
+ "TWRTPT"
8789
9462
  ]);
8790
9463
  var ZOOM_SCALED_LINES2 = {
8791
9464
  DEPCNT: [8, 0.3, 14, 0.8],
@@ -9773,6 +10446,87 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION) {
9773
10446
  metadata: { "s52:objectClass": "RECTRC", "s52:role": "polygon" }
9774
10447
  });
9775
10448
  }
10449
+ if (objectClass === "PONTON" && resolved.type === "fill") {
10450
+ layers.push({
10451
+ id: `s52-PONTON-line`,
10452
+ type: "line",
10453
+ source: sourceId,
10454
+ "source-layer": "PONTON",
10455
+ filter: GEOM_FILTERS.line,
10456
+ paint: {
10457
+ "line-color": resolveColor("CHGRF", ct),
10458
+ "line-width": 1.5
10459
+ },
10460
+ metadata: { "s52:objectClass": "PONTON", "s52:role": "line" }
10461
+ });
10462
+ }
10463
+ if (objectClass === "SILTNK" && resolved.type === "symbol") {
10464
+ layers.push({
10465
+ id: `s52-SILTNK-symbol-polygon`,
10466
+ type: "symbol",
10467
+ source: sourceId,
10468
+ "source-layer": "SILTNK",
10469
+ filter: GEOM_FILTERS.fill,
10470
+ layout: {
10471
+ "icon-image": "TNKCON02",
10472
+ "icon-size": 0.6,
10473
+ "icon-allow-overlap": true,
10474
+ "icon-ignore-placement": false,
10475
+ "symbol-placement": "point"
10476
+ },
10477
+ paint: {},
10478
+ metadata: { "s52:objectClass": "SILTNK", "s52:role": "symbol-polygon" }
10479
+ });
10480
+ }
10481
+ if (objectClass === "ACHBRT" && resolved.type === "symbol") {
10482
+ layers.push({
10483
+ id: `s52-ACHBRT-symbol-polygon`,
10484
+ type: "symbol",
10485
+ source: sourceId,
10486
+ "source-layer": "ACHBRT",
10487
+ filter: GEOM_FILTERS.fill,
10488
+ layout: {
10489
+ "icon-image": "ACHARE02",
10490
+ "icon-size": 0.5,
10491
+ "icon-allow-overlap": true,
10492
+ "icon-ignore-placement": false,
10493
+ "symbol-placement": "point"
10494
+ },
10495
+ paint: {},
10496
+ metadata: { "s52:objectClass": "ACHBRT", "s52:role": "symbol-polygon" }
10497
+ });
10498
+ }
10499
+ if (objectClass === "BERTHS" && resolved.type === "line") {
10500
+ layers.push({
10501
+ id: `s52-BERTHS-circle`,
10502
+ type: "circle",
10503
+ source: sourceId,
10504
+ "source-layer": "BERTHS",
10505
+ filter: GEOM_FILTERS.circle,
10506
+ paint: {
10507
+ "circle-color": resolveColor("CHBLK", ct),
10508
+ "circle-radius": ["interpolate", ["linear"], ["zoom"], 12, 1.5, 14, 2.2, 17, 3],
10509
+ "circle-stroke-color": resolveColor("CHBLK", ct),
10510
+ "circle-stroke-width": 0.5
10511
+ },
10512
+ metadata: { "s52:objectClass": "BERTHS", "s52:role": "point" }
10513
+ });
10514
+ }
10515
+ if (objectClass === "TUNNEL" && resolved.type === "line") {
10516
+ layers.push({
10517
+ id: `s52-TUNNEL-line`,
10518
+ type: "line",
10519
+ source: sourceId,
10520
+ "source-layer": "TUNNEL",
10521
+ filter: GEOM_FILTERS.line,
10522
+ paint: {
10523
+ "line-color": resolveColor("CHGRD", ct),
10524
+ "line-width": 1,
10525
+ "line-dasharray": [2, 2]
10526
+ },
10527
+ metadata: { "s52:objectClass": "TUNNEL", "s52:role": "line" }
10528
+ });
10529
+ }
9776
10530
  }
9777
10531
  return layers;
9778
10532
  }
@@ -10008,9 +10762,21 @@ var META_LAYERS = /* @__PURE__ */ new Set([
10008
10762
  "M_CSCL",
10009
10763
  "TS_FEB",
10010
10764
  "TS_TIS",
10011
- "COSARE",
10012
- "NEWOBJ",
10013
10765
  "LOCMAG"
10766
+ // COSARE was previously suppressed here as meta. The weekly layer-
10767
+ // coverage audit (issue #575) classified it as a jurisdictional polygon
10768
+ // (S-52 INT1 N40, same family as ADMARE / TESARE). It now has an
10769
+ // S52_LOOKUP entry and renders as a magenta dashed boundary; suppressing
10770
+ // it as meta would defeat that wire-up.
10771
+ //
10772
+ // NEWOBJ was likewise previously suppressed here. That was the source
10773
+ // of audit gap row 22: NEWOBJ is the S-57 placeholder for features the
10774
+ // HO has identified but not yet classified. Skipping it silently hid
10775
+ // the HO's own "we don't know what this is, but it is here" signal —
10776
+ // the opposite of what mariners need. The class now renders via its
10777
+ // S52_LOOKUP entry as a vivid magenta circle, displayCategory STANDARD,
10778
+ // scamin 0. Do not put it back here — see the lookup entry for the
10779
+ // safety contract.
10014
10780
  ]);
10015
10781
  var PALETTE_NAMES = {
10016
10782
  DAY: "DAY_BRIGHT",
@@ -10101,21 +10867,45 @@ function makeBandVariants(layer, B) {
10101
10867
  }
10102
10868
  return variants;
10103
10869
  }
10104
- function fanOrientationLabelByDominator(label) {
10870
+ var CELL_BAND_OPACITY_CURVE = {
10871
+ "1": [[3, 0.5], [5, 0.3], [16, 0.15]],
10872
+ "2": [[5, 0.5], [7, 0.3], [16, 0.15]],
10873
+ "3": [[7, 0.55], [9, 0.35], [16, 0.18]],
10874
+ "4": [[9, 0.55], [11, 0.35], [16, 0.2]],
10875
+ "5": [[11, 0.6], [13, 0.35], [18, 0.25]],
10876
+ "6": [[13, 0.75], [16, 0.55], [22, 0.45]]
10877
+ };
10878
+ function fanOrientationLabelByCellBand(label) {
10879
+ const cellBandExpr = [
10880
+ "slice",
10881
+ ["coalesce", ["get", "_cell_id"], "??9???"],
10882
+ 2,
10883
+ 3
10884
+ ];
10105
10885
  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;
10113
- variants.push({
10886
+ for (const code of BAND_DRAW_ORDER) {
10887
+ const range = BAND_PRIORITY_RANGES[code];
10888
+ const curve = CELL_BAND_OPACITY_CURVE[code];
10889
+ const opacityExpr = ["interpolate", ["linear"], ["zoom"]];
10890
+ for (const [z, o] of curve) opacityExpr.push(z, o);
10891
+ const variant = {
10114
10892
  ...label,
10115
- id: `${label.id}-domBy${D}`,
10116
- filter: combineWithBandFilter(label.filter, dominatedByMinFilter(D)),
10117
- maxzoom: BAND_PRIORITY_RANGES[D].minzoom
10118
- });
10893
+ id: `${label.id}-cell${code}`,
10894
+ filter: combineWithBandFilter(
10895
+ label.filter,
10896
+ ["==", cellBandExpr, code]
10897
+ ),
10898
+ minzoom: Math.max(
10899
+ label.minzoom ?? range.minzoom,
10900
+ range.minzoom
10901
+ ),
10902
+ paint: {
10903
+ ...label.paint ?? {},
10904
+ "text-opacity": opacityExpr
10905
+ }
10906
+ };
10907
+ delete variant.maxzoom;
10908
+ variants.push(variant);
10119
10909
  }
10120
10910
  return variants;
10121
10911
  }
@@ -10239,7 +11029,7 @@ function generateStyle(palette = "DAY", opts = {}) {
10239
11029
  for (const { objectClass, label } of bundles) {
10240
11030
  if (!label) continue;
10241
11031
  if (getLabelBehavior(objectClass) === "orientation-cross-band") {
10242
- labelLayers.push(...fanOrientationLabelByDominator(label));
11032
+ labelLayers.push(...fanOrientationLabelByCellBand(label));
10243
11033
  }
10244
11034
  }
10245
11035
  for (const code of BAND_DRAW_ORDER) {