@chrtco/sdk 0.7.2 → 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",
@@ -5967,6 +6405,15 @@ var NATQUA_ABBR = [
5967
6405
  // hard
5968
6406
  ];
5969
6407
  var DARK_BG = "#1a1d23";
6408
+ var NAV_HALO = {
6409
+ WHITE: { bright: "#ffffff", night: "#b8b0a0" },
6410
+ CREAM: { bright: "#fdf0b0", night: "#a89868" },
6411
+ YELLOW: { bright: "#eab308", night: "#806020" },
6412
+ BLUE: { bright: "#b0c8e8", night: "#7088a0" }
6413
+ };
6414
+ function navHalo(palette, ct) {
6415
+ return ct === "NIGHT" ? NAV_HALO[palette].night : NAV_HALO[palette].bright;
6416
+ }
5970
6417
  var NAV_AID_LABEL_TEXT_SIZE = [
5971
6418
  "interpolate",
5972
6419
  ["linear"],
@@ -6016,6 +6463,124 @@ function buildSeabedLabelExpression() {
6016
6463
  const natsurJoined = ["slice", natsurParts, 1];
6017
6464
  return ["concat", natquaPrefix, natsurJoined];
6018
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
+ }
6019
6584
  var COLOR = {
6020
6585
  WATER: "#9ec1d8",
6021
6586
  WATER_DEEP: "#8fb8d0",
@@ -6036,6 +6601,30 @@ var ORIENTATION_BAND_SORT_KEY = [
6036
6601
  ["slice", ["coalesce", ["get", "_cell_id"], "??9???"], 2, 3],
6037
6602
  9
6038
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
+ ];
6039
6628
  var LABEL_BEHAVIOR = {
6040
6629
  // Orientation labels — render once across all bands.
6041
6630
  SEAARE: "orientation-cross-band",
@@ -6073,9 +6662,9 @@ function getLabelBehavior(layerName) {
6073
6662
  return LABEL_BEHAVIOR[layerName] ?? "feature-band-gated";
6074
6663
  }
6075
6664
  function getLabelConfig(layerName, opts = {}) {
6665
+ const ct = opts.colorTable ?? "DAY";
6076
6666
  switch (layerName) {
6077
6667
  case "SOUNDG": {
6078
- const ct = opts.colorTable ?? "DAY";
6079
6668
  const safetyContour = opts.safetyContour ?? 10;
6080
6669
  const twoShade = opts.twoShadeSoundings ?? true;
6081
6670
  const weight = opts.soundingFontWeight ?? "semibold";
@@ -6150,7 +6739,17 @@ function getLabelConfig(layerName, opts = {}) {
6150
6739
  }
6151
6740
  };
6152
6741
  }
6153
- 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
+ ];
6154
6753
  return {
6155
6754
  type: "symbol",
6156
6755
  textField: ["get", "OBJNAM"],
@@ -6158,27 +6757,28 @@ function getLabelConfig(layerName, opts = {}) {
6158
6757
  "interpolate",
6159
6758
  ["linear"],
6160
6759
  ["zoom"],
6161
- 8,
6162
- 10,
6163
- 12,
6164
- 12,
6165
- 15,
6166
- 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)
6167
6772
  ],
6168
6773
  textColor: DARK_BG,
6169
6774
  textHaloColor: COLOR.LAND_REGION,
6170
- textHaloWidth: 1.4,
6775
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6171
6776
  textOffset: [0, 0],
6172
6777
  textFont: ["Open Sans Regular"],
6173
- minzoom: 8,
6174
- layout: {
6175
- "text-anchor": "center",
6176
- "text-allow-overlap": false,
6177
- "text-padding": 6,
6178
- "text-optional": true,
6179
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6180
- }
6778
+ minzoom: 5,
6779
+ layout: orientationLabelLayout()
6181
6780
  };
6781
+ }
6182
6782
  case "LNDMRK":
6183
6783
  return {
6184
6784
  type: "symbol",
@@ -6197,7 +6797,7 @@ function getLabelConfig(layerName, opts = {}) {
6197
6797
  textSize: NAV_AID_LABEL_TEXT_SIZE,
6198
6798
  textFont: NAV_AID_LABEL_FONT,
6199
6799
  textColor: DARK_BG,
6200
- textHaloColor: "#ffffff",
6800
+ textHaloColor: navHalo("WHITE", ct),
6201
6801
  textHaloWidth: 1.5,
6202
6802
  textOffset: [0, 1.5],
6203
6803
  minzoom: 10
@@ -6209,7 +6809,7 @@ function getLabelConfig(layerName, opts = {}) {
6209
6809
  textSize: NAV_AID_LABEL_TEXT_SIZE,
6210
6810
  textFont: NAV_AID_LABEL_FONT,
6211
6811
  textColor: DARK_BG,
6212
- textHaloColor: "#ffffff",
6812
+ textHaloColor: navHalo("WHITE", ct),
6213
6813
  textHaloWidth: 1.5,
6214
6814
  textOffset: [0, 1.5],
6215
6815
  minzoom: 10
@@ -6222,7 +6822,7 @@ function getLabelConfig(layerName, opts = {}) {
6222
6822
  textSize: NAV_AID_LABEL_TEXT_SIZE,
6223
6823
  textFont: NAV_AID_LABEL_FONT,
6224
6824
  textColor: DARK_BG,
6225
- textHaloColor: "#fdf0b0",
6825
+ textHaloColor: navHalo("CREAM", ct),
6226
6826
  textHaloWidth: 1.5,
6227
6827
  textOffset: [0, 1.5],
6228
6828
  minzoom: 10
@@ -6235,7 +6835,7 @@ function getLabelConfig(layerName, opts = {}) {
6235
6835
  textSize: NAV_AID_LABEL_TEXT_SIZE,
6236
6836
  textFont: NAV_AID_LABEL_FONT,
6237
6837
  textColor: DARK_BG,
6238
- textHaloColor: "#eab308",
6838
+ textHaloColor: navHalo("YELLOW", ct),
6239
6839
  textHaloWidth: 1.5,
6240
6840
  textOffset: [0, 1.5],
6241
6841
  minzoom: 10
@@ -6248,7 +6848,7 @@ function getLabelConfig(layerName, opts = {}) {
6248
6848
  textSize: NAV_AID_LABEL_TEXT_SIZE,
6249
6849
  textFont: NAV_AID_LABEL_FONT,
6250
6850
  textColor: DARK_BG,
6251
- textHaloColor: "#ffffff",
6851
+ textHaloColor: navHalo("WHITE", ct),
6252
6852
  textHaloWidth: 1.5,
6253
6853
  textOffset: [0, 1.5],
6254
6854
  minzoom: 10
@@ -6260,7 +6860,7 @@ function getLabelConfig(layerName, opts = {}) {
6260
6860
  textSize: NAV_AID_LABEL_TEXT_SIZE,
6261
6861
  textFont: NAV_AID_LABEL_FONT,
6262
6862
  textColor: DARK_BG,
6263
- textHaloColor: "#fdf0b0",
6863
+ textHaloColor: navHalo("CREAM", ct),
6264
6864
  textHaloWidth: 1.5,
6265
6865
  textOffset: [2, 0],
6266
6866
  minzoom: 9,
@@ -6296,7 +6896,7 @@ function getLabelConfig(layerName, opts = {}) {
6296
6896
  textSize: NAV_AID_LABEL_TEXT_SIZE,
6297
6897
  textFont: NAV_AID_LABEL_FONT,
6298
6898
  textColor: DARK_BG,
6299
- textHaloColor: "#b0c8e8",
6899
+ textHaloColor: navHalo("BLUE", ct),
6300
6900
  textHaloWidth: 1.5,
6301
6901
  textOffset: [2, 1.2],
6302
6902
  minzoom: 11,
@@ -6307,40 +6907,72 @@ function getLabelConfig(layerName, opts = {}) {
6307
6907
  }
6308
6908
  };
6309
6909
  }
6310
- 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
+ ];
6311
6925
  return {
6312
6926
  type: "symbol",
6313
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.
6314
6934
  textSize: [
6315
6935
  "interpolate",
6316
6936
  ["linear"],
6317
6937
  ["zoom"],
6938
+ 3,
6939
+ sizeAt(14, 8, 0, 0, 0),
6318
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),
6319
6946
  11,
6320
- 8,
6947
+ sizeAt(10, 12, 13, 0, 11),
6321
6948
  13,
6322
- 11,
6323
- 15,
6324
- 14,
6325
- 17
6949
+ sizeAt(9, 11, 12, 10, 11),
6950
+ 16,
6951
+ sizeAt(9, 10, 11, 11, 10)
6326
6952
  ],
6327
6953
  textColor: DARK_BG,
6328
6954
  textHaloColor: COLOR.WATER_DEEP,
6329
- textHaloWidth: 1.8,
6955
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6330
6956
  textOffset: [0, 0],
6331
6957
  textFont: ["Open Sans Italic"],
6332
6958
  minzoom: 5,
6333
- layout: {
6334
- "text-anchor": "center",
6335
- "text-allow-overlap": false,
6336
- "text-padding": 10,
6337
- "text-optional": true,
6959
+ layout: orientationLabelLayout({
6338
6960
  "text-letter-spacing": 0.15,
6339
- "text-max-width": 9,
6340
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6341
- }
6961
+ "text-max-width": 9
6962
+ })
6342
6963
  };
6343
- 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
+ ];
6344
6976
  return {
6345
6977
  type: "symbol",
6346
6978
  textField: ["get", "OBJNAM"],
@@ -6348,28 +6980,37 @@ function getLabelConfig(layerName, opts = {}) {
6348
6980
  "interpolate",
6349
6981
  ["linear"],
6350
6982
  ["zoom"],
6351
- 8,
6352
- 10,
6353
- 12,
6354
- 12,
6355
- 15,
6356
- 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)
6357
6993
  ],
6358
6994
  textColor: DARK_BG,
6359
6995
  textHaloColor: COLOR.WATER,
6360
- textHaloWidth: 1.5,
6996
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6361
6997
  textOffset: [0, 0],
6362
6998
  textFont: ["Open Sans Italic"],
6363
- minzoom: 8,
6364
- layout: {
6365
- "text-anchor": "center",
6366
- "text-allow-overlap": false,
6367
- "text-padding": 6,
6368
- "text-optional": true,
6369
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6370
- }
6999
+ minzoom: 7,
7000
+ layout: orientationLabelLayout()
6371
7001
  };
6372
- 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
+ ];
6373
7014
  return {
6374
7015
  type: "symbol",
6375
7016
  textField: ["get", "OBJNAM"],
@@ -6377,27 +7018,26 @@ function getLabelConfig(layerName, opts = {}) {
6377
7018
  "interpolate",
6378
7019
  ["linear"],
6379
7020
  ["zoom"],
7021
+ 7,
7022
+ sizeAt(9, 0),
6380
7023
  9,
6381
- 9,
6382
- 12,
7024
+ sizeAt(11, 0),
6383
7025
  11,
6384
- 15,
6385
- 13
7026
+ sizeAt(11, 9),
7027
+ 13,
7028
+ sizeAt(10, 10),
7029
+ 16,
7030
+ sizeAt(9, 10)
6386
7031
  ],
6387
7032
  textColor: DARK_BG,
6388
7033
  textHaloColor: COLOR.WATER,
6389
- textHaloWidth: 1.4,
7034
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6390
7035
  textOffset: [0, 0],
6391
7036
  textFont: ["Open Sans Italic"],
6392
- minzoom: 9,
6393
- layout: {
6394
- "text-anchor": "center",
6395
- "text-allow-overlap": false,
6396
- "text-padding": 4,
6397
- "text-optional": true,
6398
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6399
- }
7037
+ minzoom: 7,
7038
+ layout: orientationLabelLayout()
6400
7039
  };
7040
+ }
6401
7041
  case "CANALS":
6402
7042
  return {
6403
7043
  type: "symbol",
@@ -6407,25 +7047,31 @@ function getLabelConfig(layerName, opts = {}) {
6407
7047
  ["linear"],
6408
7048
  ["zoom"],
6409
7049
  11,
7050
+ 10,
7051
+ 13,
6410
7052
  9,
6411
- 14,
6412
- 11
7053
+ 16,
7054
+ 9
6413
7055
  ],
6414
7056
  textColor: DARK_BG,
6415
7057
  textHaloColor: COLOR.WATER,
6416
- textHaloWidth: 1.3,
7058
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6417
7059
  textOffset: [0, 0],
6418
7060
  textFont: ["Open Sans Italic"],
6419
7061
  minzoom: 11,
6420
- layout: {
6421
- "text-anchor": "center",
6422
- "text-allow-overlap": false,
6423
- "text-padding": 4,
6424
- "text-optional": true,
6425
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6426
- }
7062
+ layout: orientationLabelLayout()
6427
7063
  };
6428
- 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
+ ];
6429
7075
  return {
6430
7076
  type: "symbol",
6431
7077
  textField: ["get", "OBJNAM"],
@@ -6433,28 +7079,41 @@ function getLabelConfig(layerName, opts = {}) {
6433
7079
  "interpolate",
6434
7080
  ["linear"],
6435
7081
  ["zoom"],
6436
- 8,
7082
+ 5,
7083
+ sizeAt(8, 0),
7084
+ 7,
7085
+ sizeAt(10, 0),
6437
7086
  9,
6438
- 12,
7087
+ sizeAt(11, 0),
6439
7088
  11,
6440
- 15,
6441
- 13
7089
+ sizeAt(11, 9),
7090
+ 13,
7091
+ sizeAt(10, 10),
7092
+ 16,
7093
+ sizeAt(9, 10)
6442
7094
  ],
6443
7095
  textColor: DARK_BG,
6444
7096
  textHaloColor: COLOR.LAND_NATURAL,
6445
- textHaloWidth: 1.4,
7097
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6446
7098
  textOffset: [0, 0],
6447
7099
  textFont: ["Open Sans Regular"],
6448
- minzoom: 8,
6449
- layout: {
6450
- "text-anchor": "center",
6451
- "text-allow-overlap": false,
6452
- "text-padding": 6,
6453
- "text-optional": true,
6454
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6455
- }
7100
+ minzoom: 5,
7101
+ layout: orientationLabelLayout()
6456
7102
  };
6457
- 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
+ ];
6458
7117
  return {
6459
7118
  type: "symbol",
6460
7119
  textField: ["get", "OBJNAM"],
@@ -6462,28 +7121,32 @@ function getLabelConfig(layerName, opts = {}) {
6462
7121
  "interpolate",
6463
7122
  ["linear"],
6464
7123
  ["zoom"],
7124
+ 3,
7125
+ sizeAt(11, 0, 0),
7126
+ 5,
7127
+ sizeAt(13, 0, 0),
6465
7128
  7,
7129
+ sizeAt(13, 9, 0),
7130
+ 9,
7131
+ sizeAt(12, 12, 0),
6466
7132
  11,
6467
- 10,
6468
- 13,
7133
+ sizeAt(11, 12, 10),
6469
7134
  13,
6470
- 15
7135
+ sizeAt(10, 11, 12),
7136
+ 16,
7137
+ sizeAt(10, 10, 11)
6471
7138
  ],
6472
7139
  textColor: DARK_BG,
6473
7140
  textHaloColor: COLOR.CITY,
6474
- textHaloWidth: 1.6,
7141
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6475
7142
  textOffset: [0, 0],
6476
7143
  textFont: ["Open Sans Regular"],
6477
- minzoom: 7,
6478
- layout: {
6479
- "text-anchor": "center",
6480
- "text-allow-overlap": false,
6481
- "text-padding": 8,
6482
- "text-optional": true,
6483
- "text-letter-spacing": 0.05,
6484
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6485
- }
7144
+ minzoom: 3,
7145
+ layout: orientationLabelLayout({
7146
+ "text-letter-spacing": 0.05
7147
+ })
6486
7148
  };
7149
+ }
6487
7150
  case "BRIDGE":
6488
7151
  return {
6489
7152
  type: "symbol",
@@ -6551,7 +7214,17 @@ function getLabelConfig(layerName, opts = {}) {
6551
7214
  "text-optional": true
6552
7215
  }
6553
7216
  };
6554
- 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
+ ];
6555
7228
  return {
6556
7229
  type: "symbol",
6557
7230
  textField: ["get", "OBJNAM"],
@@ -6559,25 +7232,26 @@ function getLabelConfig(layerName, opts = {}) {
6559
7232
  "interpolate",
6560
7233
  ["linear"],
6561
7234
  ["zoom"],
7235
+ 7,
7236
+ sizeAt(9, 0),
6562
7237
  9,
6563
- 9,
7238
+ sizeAt(11, 0),
7239
+ 11,
7240
+ sizeAt(11, 9),
6564
7241
  13,
6565
- 11
7242
+ sizeAt(10, 10),
7243
+ 16,
7244
+ sizeAt(9, 9)
6566
7245
  ],
6567
7246
  textColor: DARK_BG,
6568
7247
  textHaloColor: COLOR.AIRPORT,
6569
- textHaloWidth: 1.4,
7248
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6570
7249
  textOffset: [0, 0],
6571
7250
  textFont: ["Open Sans Regular"],
6572
- minzoom: 9,
6573
- layout: {
6574
- "text-anchor": "center",
6575
- "text-allow-overlap": false,
6576
- "text-padding": 5,
6577
- "text-optional": true,
6578
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6579
- }
7251
+ minzoom: 7,
7252
+ layout: orientationLabelLayout()
6580
7253
  };
7254
+ }
6581
7255
  case "SBDARE":
6582
7256
  return {
6583
7257
  type: "symbol",
@@ -6650,26 +7324,20 @@ function getLabelConfig(layerName, opts = {}) {
6650
7324
  "interpolate",
6651
7325
  ["linear"],
6652
7326
  ["zoom"],
7327
+ 11,
7328
+ 9,
7329
+ 13,
6653
7330
  8,
6654
- 10,
6655
- 12,
6656
- 12,
6657
- 15,
6658
- 14
7331
+ 16,
7332
+ 8
6659
7333
  ],
6660
7334
  textColor: "#c545c3",
6661
7335
  textHaloColor: DARK_BG,
6662
- textHaloWidth: 1.4,
7336
+ textHaloWidth: ORIENTATION_HALO_WIDTH,
6663
7337
  textOffset: [0, 0],
6664
7338
  textFont: ["Open Sans Italic"],
6665
7339
  minzoom: 8,
6666
- layout: {
6667
- "text-anchor": "center",
6668
- "text-allow-overlap": false,
6669
- "text-padding": 6,
6670
- "text-optional": true,
6671
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY
6672
- }
7340
+ layout: orientationLabelLayout()
6673
7341
  };
6674
7342
  case "WEDKLP":
6675
7343
  return {
@@ -6702,7 +7370,7 @@ function getLabelConfig(layerName, opts = {}) {
6702
7370
  case "UWTROC":
6703
7371
  case "OBSTRN":
6704
7372
  case "WRECKS": {
6705
- const ct = opts.colorTable ?? "DAY";
7373
+ const ct2 = opts.colorTable ?? "DAY";
6706
7374
  const valsou = [
6707
7375
  "to-number",
6708
7376
  ["coalesce", ["get", "VALSOU"], ["get", "valsou"], 0],
@@ -6729,8 +7397,8 @@ function getLabelConfig(layerName, opts = {}) {
6729
7397
  16,
6730
7398
  11
6731
7399
  ],
6732
- textColor: resolveColor("CHBLK", ct),
6733
- textHaloColor: resolveColor("CHWHT", ct),
7400
+ textColor: resolveColor("CHBLK", ct2),
7401
+ textHaloColor: resolveColor("CHWHT", ct2),
6734
7402
  textHaloWidth: 1.2,
6735
7403
  // Place to the upper-right of the symbol so it doesn't sit on top
6736
7404
  // of the asterisk / cross. Anchor top-left so the offset grows
@@ -8776,7 +9444,21 @@ var LINE_FROM_POLYGON_LAYERS = /* @__PURE__ */ new Set([
8776
9444
  "MIPARE",
8777
9445
  "TESARE",
8778
9446
  "MAGVAR",
8779
- "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"
8780
9462
  ]);
8781
9463
  var ZOOM_SCALED_LINES2 = {
8782
9464
  DEPCNT: [8, 0.3, 14, 0.8],
@@ -9764,6 +10446,87 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION) {
9764
10446
  metadata: { "s52:objectClass": "RECTRC", "s52:role": "polygon" }
9765
10447
  });
9766
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
+ }
9767
10530
  }
9768
10531
  return layers;
9769
10532
  }
@@ -9999,9 +10762,21 @@ var META_LAYERS = /* @__PURE__ */ new Set([
9999
10762
  "M_CSCL",
10000
10763
  "TS_FEB",
10001
10764
  "TS_TIS",
10002
- "COSARE",
10003
- "NEWOBJ",
10004
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.
10005
10780
  ]);
10006
10781
  var PALETTE_NAMES = {
10007
10782
  DAY: "DAY_BRIGHT",
@@ -10092,21 +10867,45 @@ function makeBandVariants(layer, B) {
10092
10867
  }
10093
10868
  return variants;
10094
10869
  }
10095
- 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
+ ];
10096
10885
  const variants = [];
10097
- variants.push({
10098
- ...label,
10099
- id: `${label.id}-notdom`,
10100
- filter: combineWithBandFilter(label.filter, NOT_DOMINATED_FILTER)
10101
- });
10102
- for (const D of BAND_DRAW_ORDER) {
10103
- if (D === "1") continue;
10104
- 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 = {
10105
10892
  ...label,
10106
- id: `${label.id}-domBy${D}`,
10107
- filter: combineWithBandFilter(label.filter, dominatedByMinFilter(D)),
10108
- maxzoom: BAND_PRIORITY_RANGES[D].minzoom
10109
- });
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);
10110
10909
  }
10111
10910
  return variants;
10112
10911
  }
@@ -10230,7 +11029,7 @@ function generateStyle(palette = "DAY", opts = {}) {
10230
11029
  for (const { objectClass, label } of bundles) {
10231
11030
  if (!label) continue;
10232
11031
  if (getLabelBehavior(objectClass) === "orientation-cross-band") {
10233
- labelLayers.push(...fanOrientationLabelByDominator(label));
11032
+ labelLayers.push(...fanOrientationLabelByCellBand(label));
10234
11033
  }
10235
11034
  }
10236
11035
  for (const code of BAND_DRAW_ORDER) {