@chrtco/sdk 0.6.0 → 0.7.0

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.d.cts CHANGED
@@ -18,10 +18,10 @@ import { ExpressionSpecification } from '@maplibre/maplibre-gl-style-spec';
18
18
  * MapLibre clients (iOS/Android/desktop) that don't have the JS atlas
19
19
  * available.
20
20
  *
21
- * 2. Generate the online style themselves (e.g. via the React app's
22
- * `src/lib/atlas/style-generator.ts`) and call `buildOfflineStyle()`
23
- * locally — recommended for JS/web SDK consumers who already use the
24
- * atlas at runtime.
21
+ * 2. Generate the online style themselves (e.g. via this SDK's
22
+ * `generateStyle()` export from `@chrtco/sdk`) and call
23
+ * `buildOfflineStyle()` locally — recommended for JS/web SDK
24
+ * consumers who already use the atlas at runtime.
25
25
  *
26
26
  * Both paths produce equivalent output.
27
27
  */
@@ -672,6 +672,21 @@ interface LookupEntry {
672
672
  viewingGroup?: number;
673
673
  /** Minimum compilation scale for display (SCAMIN) */
674
674
  scamin?: number;
675
+ /**
676
+ * Mark this class as deliberately invisible (label-only or metadata-only)
677
+ * so the layer-coverage audit doesn't flag its geometry-type gaps as bugs.
678
+ *
679
+ * Examples:
680
+ * - SOUNDG: rendered as depth numbers via labels.ts, not as visible dots
681
+ * - LNDRGN: region NAME labels only, no shape
682
+ * - M_NPUB / M_NSYS / M_QUAL / M_SREL / DSID: S-57 metadata classes;
683
+ * present for click-query attributes, never drawn
684
+ *
685
+ * Without this flag, `scripts/audit-layer-coverage.ts` reports the
686
+ * thousands of features in these classes as "gaps" every week even
687
+ * though the invisibility is intentional.
688
+ */
689
+ auditIgnore?: boolean;
675
690
  }
676
691
  /** Resolved style ready for Mapbox GL (tokens → hex) */
677
692
  interface ResolvedStyle {
@@ -743,12 +758,12 @@ interface LabelLayerConfig {
743
758
  textField: ExpressionSpecification | string;
744
759
  /** Text size in pixels */
745
760
  textSize: number | ExpressionSpecification;
746
- /** Text color */
747
- textColor: string;
761
+ /** Text color (hex) or data-driven expression */
762
+ textColor: string | ExpressionSpecification;
748
763
  /** Halo color for contrast */
749
764
  textHaloColor: string;
750
- /** Halo width */
751
- textHaloWidth: number;
765
+ /** Halo width — pixels or zoom-interpolation expression */
766
+ textHaloWidth: number | ExpressionSpecification;
752
767
  /** Text offset [x, y] in ems */
753
768
  textOffset: [number, number];
754
769
  /** Font stack */
@@ -758,8 +773,56 @@ interface LabelLayerConfig {
758
773
  /** Minimum zoom level for this label */
759
774
  minzoom?: number;
760
775
  }
776
+ /**
777
+ * Optional palette-and-data-driven options for label config.
778
+ *
779
+ * Currently used by SOUNDG to pick palette-aware text/halo colors and to
780
+ * drive the S-52 safety-contour two-shade rule. Other classes ignore
781
+ * these options.
782
+ */
783
+ interface LabelConfigOptions {
784
+ /** S-52 colour table — drives palette-aware text and halo colors. */
785
+ colorTable?: ColorTableName;
786
+ /**
787
+ * Safety contour depth in metres. Soundings shoaler than this render in
788
+ * `SNDG2` (high-contrast); deeper soundings render in `SNDG1` (muted).
789
+ * S-52 §SOUNDG02 conditional symbology.
790
+ */
791
+ safetyContour?: number;
792
+ /**
793
+ * When false, all soundings render in `SNDG2` (single-shade). Default
794
+ * true — emit the safety-contour case expression.
795
+ */
796
+ twoShadeSoundings?: boolean;
797
+ /**
798
+ * When "semibold", SOUNDG labels use Open Sans Semibold for crisper
799
+ * numerals at small sizes. Default "semibold".
800
+ */
801
+ soundingFontWeight?: "regular" | "semibold";
802
+ }
803
+ /**
804
+ * Two-tier label rendering strategy for the band-priority style.
805
+ *
806
+ * `feature-band-gated` — emit one variant per source band, each gated to
807
+ * its band's zoom window (the existing `makeBandVariants` behaviour). Right
808
+ * for dense feature labels where seeing the same buoy designation or
809
+ * sounding twice across overlapping bands is visual noise.
810
+ *
811
+ * `orientation-cross-band` — emit a SINGLE label layer with no band
812
+ * filter. The per-class `minzoom` is honoured. Right for orientation
813
+ * "where am I" labels (sea-area names, land regions, cities) — these
814
+ * must persist as the user zooms in, even if the broader-scope cell
815
+ * carrying the OBJNAM is from a coarser band than the one rendering
816
+ * features at the current zoom. Collision arbitration via
817
+ * `ORIENTATION_BAND_SORT_KEY` gives the broader-scope (lower-band) copy
818
+ * priority where the same OBJNAM exists in multiple bands.
819
+ */
820
+ type LabelBehavior = "feature-band-gated" | "orientation-cross-band";
821
+ declare const LABEL_BEHAVIOR: Record<string, LabelBehavior>;
822
+ /** Returns the rendering strategy for an S-57 class. Unknown classes default to band-gated. */
823
+ declare function getLabelBehavior(layerName: string): LabelBehavior;
761
824
  /** Get label config for a given S-57 layer, or null if no labels */
762
- declare function getLabelConfig(layerName: string): LabelLayerConfig | null;
825
+ declare function getLabelConfig(layerName: string, opts?: LabelConfigOptions): LabelLayerConfig | null;
763
826
  /**
764
827
  * Build a Mapbox symbol layer spec from a label config.
765
828
  */
@@ -1171,6 +1234,148 @@ declare function buildLayersForClass(objectClass: string, ct: ColorTableName, so
1171
1234
  */
1172
1235
  declare function getOrderedClasses(): string[];
1173
1236
 
1237
+ /**
1238
+ * Strict scale-band priority for continuous-mode rendering.
1239
+ *
1240
+ * Each ENC feature in the served PMTiles carries `_cell_id` (added at
1241
+ * baremetal NDJSON export time). NOAA's cell-id format encodes the source
1242
+ * scale band as the third character: `US5MA85M` is band 5 (Harbor).
1243
+ *
1244
+ * MapLibre layer filters can't read `["zoom"]`, so to enforce "only one
1245
+ * band renders at any given zoom" we emit one layer copy per band, each
1246
+ * with a static band filter and per-band `minzoom`/`maxzoom`. The ranges
1247
+ * below are non-overlapping (`minzoom` inclusive, `maxzoom` exclusive),
1248
+ * so exactly one band is visible at any continuous zoom.
1249
+ *
1250
+ * Long-term cleanup: baremetal should emit a normalized `_band: 1..6`
1251
+ * integer in NDJSON so this works for non-NOAA chart sources too. The
1252
+ * `["slice", _cell_id, 2, 3]` extraction here is NOAA-specific.
1253
+ */
1254
+ /**
1255
+ * UI-facing scale-band names (matches the app's `ScaleBand` type in
1256
+ * `src/lib/map/constants.ts`). Defined locally so the SDK doesn't have
1257
+ * to depend on app code.
1258
+ */
1259
+ type ScaleBand = "Overview" | "General" | "Coastal" | "Approach" | "Harbor";
1260
+ /** S-57 source bands. NOAA cell ids use these as the third character. */
1261
+ type BandCode = "1" | "2" | "3" | "4" | "5" | "6";
1262
+ interface BandPriorityRange {
1263
+ /** MapLibre `minzoom` (inclusive). */
1264
+ minzoom: number;
1265
+ /** MapLibre `maxzoom` (exclusive). `undefined` means no upper cap. */
1266
+ maxzoom?: number;
1267
+ }
1268
+ /**
1269
+ * Per-band minzoom thresholds for continuous mode. Bands cascade as
1270
+ * fallbacks for one another — every band whose `minzoom` is satisfied
1271
+ * may render. The deeper-numbered band's features render preferentially
1272
+ * (drawn last in the z-stack, so its opaque fills cover lower bands),
1273
+ * and lower bands fill in where the higher band has no coverage.
1274
+ *
1275
+ * Cross-band visual overlap (doubled coastlines, blended fills) is
1276
+ * suppressed by the `_dominated_by_min` per-feature tag added at NDJSON
1277
+ * export time (see `tools/chrt-baremetal/server/src/tilebuild/dominanceIndex.ts`):
1278
+ * every feature whose centroid sits inside a higher-band cell's M_COVR
1279
+ * coverage polygon is tagged with the LOWEST band code (2..6) that
1280
+ * dominates it, and `makeBandVariants()` in the style generator emits
1281
+ * per-(featureBand, dominatorBand) layer variants gated by `maxzoom =
1282
+ * BAND_PRIORITY_RANGES[D].minzoom`. The dominated feature renders until
1283
+ * exactly the zoom where its dominator becomes eligible — solving the
1284
+ * low-zoom "black silhouettes" bug where a band-3 LNDARE dominated by
1285
+ * a band-5 cell was dropped at z7 even though band 5 wasn't yet rendering
1286
+ * at z7. At any pixel, exactly ONE band's data renders — the
1287
+ * most-detailed band whose minzoom has been reached and whose cells
1288
+ * actually cover that pixel.
1289
+ *
1290
+ * Bands 5 and 6 both start at z11; band 6 (Berthing) is drawn AFTER
1291
+ * band 5 (Harbor) so where Berthing exists it visually wins, and
1292
+ * Harbor shows through everywhere else.
1293
+ */
1294
+ declare const BAND_PRIORITY_RANGES: Record<BandCode, BandPriorityRange>;
1295
+ /**
1296
+ * Order in which band layers are emitted (and thus drawn — earlier = lower
1297
+ * in the z-stack). Band 6 last so Berthing draws on top of Harbor.
1298
+ */
1299
+ declare const BAND_DRAW_ORDER: BandCode[];
1300
+ /**
1301
+ * UI scale-band names → cell-id band codes that should render in chart mode
1302
+ * for that selection. The UI doesn't expose Berthing separately, so picking
1303
+ * Harbor includes both `5` and `6` cells.
1304
+ *
1305
+ * `CHART_BAND_CODES` is the *requested* band set (what the user picked);
1306
+ * `CHART_CASCADE_CODES` below adds the coarser bands that fall back where
1307
+ * the requested band has no cells.
1308
+ */
1309
+ declare const CHART_BAND_CODES: Record<ScaleBand, readonly BandCode[]>;
1310
+ /**
1311
+ * UI scale-band names → ordered cascade of cell-id band codes for chart-mode
1312
+ * rendering with graceful coverage fallback. Bands are listed coarsest-first
1313
+ * so the style generator can use array order as draw order: lower bands draw
1314
+ * earlier in the z-stack, the requested band draws last on top.
1315
+ *
1316
+ * The chart-mode implementation emits one filtered layer per cascade band per
1317
+ * S-57 class. Where the requested band's cells provide data, its opaque fills
1318
+ * cover the lower bands underneath; where the requested band has no cells
1319
+ * (e.g. Chart→Harbor over Massachusetts Bay offshore), the next-coarser band
1320
+ * shows through. Different cells in the same viewport may end up rendering at
1321
+ * different effective bands.
1322
+ *
1323
+ * The per-feature `_dominated_by_min` tag baked at baremetal NDJSON-export
1324
+ * time (see `tools/chrt-baremetal/server/src/tilebuild/dominanceIndex.ts`)
1325
+ * suppresses lower-band features inside higher-band cell coverage so cell
1326
+ * boundaries don't render with doubled coastlines or contours.
1327
+ *
1328
+ * Lock scope: when chart-Harbor's viewport-lock greyout is active, the
1329
+ * `["within"]` bbox filter is applied ONLY to the requested band's variants
1330
+ * — cascade fallback bands render across the full viewport. The greyout
1331
+ * overlay still cues the lock boundary visually; the user picks Harbor and
1332
+ * gets Harbor inside the lock plus dimmed Approach/Coastal context outside.
1333
+ *
1334
+ * See `.chrt/ux/chart-mode-band-fallback-2026-05-05.md` for the design
1335
+ * rationale and the geographies (Boston Harbor, Massachusetts Bay offshore)
1336
+ * that motivated this.
1337
+ */
1338
+ declare const CHART_CASCADE_CODES: Record<ScaleBand, readonly BandCode[]>;
1339
+ /**
1340
+ * Build a MapLibre filter expression that matches features in the given
1341
+ * band code(s) by extracting the band character from `_cell_id`.
1342
+ */
1343
+ declare function bandFilterExpression(codes: BandCode | readonly BandCode[]): unknown[];
1344
+ /**
1345
+ * Combine a base filter (may be undefined) with a band filter via `["all"]`.
1346
+ * If the base filter is itself an `["all", ...]`, flatten so we don't end
1347
+ * up with `["all", ["all", ...], ...]` (which is legal but ugly).
1348
+ */
1349
+ declare function combineWithBandFilter(baseFilter: unknown[] | undefined, bandFilter: unknown[]): unknown[];
1350
+ /**
1351
+ * Geographic bounding box used for chart-mode viewport locking.
1352
+ * Coordinates in WGS84 lng/lat. `sw` is south-west corner, `ne` is north-east.
1353
+ */
1354
+ interface BBox {
1355
+ sw: [number, number];
1356
+ ne: [number, number];
1357
+ }
1358
+ /**
1359
+ * Build a MapLibre `["within"]` filter expression that matches features
1360
+ * inside the given bbox. Used by chart mode to spatially limit which
1361
+ * features render — when locked to a Harbor area, only features inside
1362
+ * the locked bbox draw, even though the tile pyramid covers more.
1363
+ *
1364
+ * The polygon is closed (first and last vertex repeated) per the GeoJSON
1365
+ * spec; without that, MapLibre throws on style validation.
1366
+ */
1367
+ declare function withinBboxFilter(bbox: BBox): unknown[];
1368
+ /**
1369
+ * Build a GeoJSON Polygon for the world MINUS a given bbox — i.e. an
1370
+ * outer ring covering the whole world with a single hole punched at
1371
+ * the bbox. Used by the frontend as the source for a translucent grey
1372
+ * overlay so the user sees clearly where the chart is "locked."
1373
+ *
1374
+ * Outer ring uses the standard antimeridian-safe world extent
1375
+ * (lng [-180, 180], lat [-85, 85] — Web Mercator's safe range).
1376
+ */
1377
+ declare function worldMinusBboxPolygon(bbox: BBox): GeoJSON.Polygon;
1378
+
1174
1379
  /**
1175
1380
  * Generates a complete Mapbox GL Style Specification v8 JSON
1176
1381
  * from the S-52 atlas definitions.
@@ -1185,8 +1390,57 @@ interface MapboxStyle {
1185
1390
  glyphs: string;
1186
1391
  layers: StyleLayer[];
1187
1392
  }
1188
- declare function generateStyle(palette?: ColorTableName, opts?: BuildSourcesOptions): MapboxStyle;
1189
- declare function generateStyleJSON(palette?: ColorTableName, opts?: BuildSourcesOptions): string;
1393
+ interface StyleGeneratorOptions extends BuildSourcesOptions {
1394
+ /**
1395
+ * When set, emit a "paper chart" style centered on the selected scale
1396
+ * band, with graceful coverage fallback: the requested band's layers
1397
+ * draw on top where its cells have data; coarser bands (driven by
1398
+ * `CHART_CASCADE_CODES`) fill gaps where the requested band has no
1399
+ * cells. Per-feature SCAMIN minzooms are preserved on every cascade
1400
+ * variant; `maxzoom` is dropped so the user can zoom in indefinitely.
1401
+ *
1402
+ * When omitted (default), continuous mode emits one layer copy per
1403
+ * source band with strict per-band zoom windows so exactly one band
1404
+ * renders at any given continuous zoom.
1405
+ *
1406
+ * `lockedBounds` (optional) restricts chart-mode rendering to a
1407
+ * specific bbox via a `["within"]` filter — applied ONLY to the
1408
+ * requested band's variants. Cascade fallback bands render across the
1409
+ * full viewport so a chart-Harbor user panning offshore still sees
1410
+ * Approach/Coastal data. The frontend pairs this with a translucent
1411
+ * grey overlay outside the bbox so the lock boundary is still visible
1412
+ * to the user (the fallback data is dimmed by the overlay but not
1413
+ * filtered out).
1414
+ */
1415
+ chartMode?: {
1416
+ band: ScaleBand;
1417
+ lockedBounds?: BBox;
1418
+ };
1419
+ /**
1420
+ * Display unit for depth labels (SOUNDG soundings, DEPCNT contour
1421
+ * values). Source data is always meters (S-57 / VALSOU / VALDCO);
1422
+ * "feet" wraps the text-field expression with a unit conversion +
1423
+ * round so the rendered numbers match. Default "meters".
1424
+ */
1425
+ depthUnits?: "meters" | "feet";
1426
+ /**
1427
+ * S-52 §SOUNDG02 — soundings shoaler than this depth (metres) render in
1428
+ * the high-contrast color; deeper soundings render in the muted color.
1429
+ * Default 10 m.
1430
+ */
1431
+ safetyContour?: number;
1432
+ /**
1433
+ * When false, all soundings render in a single shade. Default true.
1434
+ */
1435
+ twoShadeSoundings?: boolean;
1436
+ /**
1437
+ * Font weight for SOUNDG labels. Default "semibold" — crisper numerals
1438
+ * at small sizes on screen.
1439
+ */
1440
+ soundingFontWeight?: "regular" | "semibold";
1441
+ }
1442
+ declare function generateStyle(palette?: ColorTableName, opts?: StyleGeneratorOptions): MapboxStyle;
1443
+ declare function generateStyleJSON(palette?: ColorTableName, opts?: StyleGeneratorOptions): string;
1190
1444
  /** Get summary stats about the generated style */
1191
1445
  declare function getStyleStats(style: MapboxStyle): {
1192
1446
  totalLayers: number;
@@ -1256,4 +1510,4 @@ declare class Chrt {
1256
1510
  transformRequest(): (url: string) => TransformRequest | undefined;
1257
1511
  }
1258
1512
 
1259
- export { type AtlasStyle, BUILD_MAXZOOM, BUILD_MINZOOM, type BuildOfflineStyleOptions, type BuildSourcesOptions, CHART_SECTIONS, type CellDeletedEvent, type CellDetail, type CellEventData, type CellNewEvent, type CellUpdatedData, type CellUpdatedEvent, type CellsOptions, type CellsResponse, type ChartSection, Chrt, type ChrtConfig, ChrtError, ColorTableName, type CoverageOptions, type CoverageResponse, type CoverageSource, DEFAULT_REGION, DEFAULT_TILE_BASE_URL, type DisplayCategory, type DisplayMode, type FeatureCollection, type FeaturesOptions, FileSystemAdapter, type GeoJsonFeature, type GeoJsonGeometry, type GeomType, LABELED_LAYERS, type LabelLayerConfig, type LookupEntry, type MapLibreStyle, type MapboxStyle, type MapboxVectorSource, NAUTICAL_SYMBOLS, type NauticalCategory, type NauticalSymbol, type OfflineBundleResponse, type OfflineDownloadOptions, type OfflineDownloadProgress, type OfflineDownloadResult, OfflineError, type OfflineErrorCode, type OfflineFetchStyleOptions, type OfflineHeartbeatOptions, type OfflineRegionSummary, type OfflineRemoveOptions, OfflineStyleError, type OfflineStylePlatform, type OfflineStyleResponse, type PmtilesVectorSource, type QueryOptions, type ResolvedStyle, type RouteFeatureQueryOptions, type RouteFeatureQueryResponse, type RouteQueryOptions, type RouteQueryResponse, type S101Feature, S101_CHART_SECTIONS, S101_FEATURES, S101_NEW_SYMBOLS, S101_SYMBOL_GROUPS, S52_DRAW_ORDER, S52_LOOKUP, SYMBOL_GROUPS, SYMBOL_MAP, type SourceMode, type StyleLayer, type SymbolGroup, type TileGroup, type TileGroupsResponse, type TransformRequest, type VectorSourceSpec, type WebhookEvent, type WebhookEventType, type WebhookPayload, applyColorTheme, buildLabelLayer, buildLayersForClass, buildOfflineStyle, buildSources, daymarkColor, decimateFeatures, depareColor, depareFillExpression, generateSpriteManifest, generateSpriteSheet, generateStyle, generateStyleJSON, getAvailableLineSymbols, getAvailablePatterns, getAvailablePointSymbols, getCollisionSettings, getDrawPriority, getGridSizeForZoom, getIconExpression, getLabelConfig, getLookupEntry, getMaxCellsForZoom, getOrderedClasses, getS52SvgString, getS52SymbolStats, getStyleStats, getSymbolSvgString, getThemedS52Svg, hazardColor, hazardFillExpression, isNavAidLayer, isVisibleInMode, lateralColor, lateralFillExpression, lightColor, lightFillExpression, paginate, parseWebhook, preprocessLights, preprocessNavAids, preprocessSoundings, preprocessWaterBodies, resolveStyle, scaminToMinZoom };
1513
+ export { type AtlasStyle, BAND_DRAW_ORDER, BAND_PRIORITY_RANGES, type BBox, BUILD_MAXZOOM, BUILD_MINZOOM, type BandCode, type BandPriorityRange, type BuildOfflineStyleOptions, type BuildSourcesOptions, CHART_BAND_CODES, CHART_CASCADE_CODES, CHART_SECTIONS, type CellDeletedEvent, type CellDetail, type CellEventData, type CellNewEvent, type CellUpdatedData, type CellUpdatedEvent, type CellsOptions, type CellsResponse, type ChartSection, Chrt, type ChrtConfig, ChrtError, ColorTableName, type CoverageOptions, type CoverageResponse, type CoverageSource, DEFAULT_REGION, DEFAULT_TILE_BASE_URL, type DisplayCategory, type DisplayMode, type FeatureCollection, type FeaturesOptions, FileSystemAdapter, type GeoJsonFeature, type GeoJsonGeometry, type GeomType, LABELED_LAYERS, LABEL_BEHAVIOR, type LabelBehavior, type LabelConfigOptions, type LabelLayerConfig, type LookupEntry, type MapLibreStyle, type MapboxStyle, type MapboxVectorSource, NAUTICAL_SYMBOLS, type NauticalCategory, type NauticalSymbol, type OfflineBundleResponse, type OfflineDownloadOptions, type OfflineDownloadProgress, type OfflineDownloadResult, OfflineError, type OfflineErrorCode, type OfflineFetchStyleOptions, type OfflineHeartbeatOptions, type OfflineRegionSummary, type OfflineRemoveOptions, OfflineStyleError, type OfflineStylePlatform, type OfflineStyleResponse, type PmtilesVectorSource, type QueryOptions, type ResolvedStyle, type RouteFeatureQueryOptions, type RouteFeatureQueryResponse, type RouteQueryOptions, type RouteQueryResponse, type S101Feature, S101_CHART_SECTIONS, S101_FEATURES, S101_NEW_SYMBOLS, S101_SYMBOL_GROUPS, S52_DRAW_ORDER, S52_LOOKUP, SYMBOL_GROUPS, SYMBOL_MAP, type ScaleBand, type SourceMode, type StyleGeneratorOptions, type StyleLayer, type SymbolGroup, type TileGroup, type TileGroupsResponse, type TransformRequest, type VectorSourceSpec, type WebhookEvent, type WebhookEventType, type WebhookPayload, applyColorTheme, bandFilterExpression, buildLabelLayer, buildLayersForClass, buildOfflineStyle, buildSources, combineWithBandFilter, daymarkColor, decimateFeatures, depareColor, depareFillExpression, generateSpriteManifest, generateSpriteSheet, generateStyle, generateStyleJSON, getAvailableLineSymbols, getAvailablePatterns, getAvailablePointSymbols, getCollisionSettings, getDrawPriority, getGridSizeForZoom, getIconExpression, getLabelBehavior, getLabelConfig, getLookupEntry, getMaxCellsForZoom, getOrderedClasses, getS52SvgString, getS52SymbolStats, getStyleStats, getSymbolSvgString, getThemedS52Svg, hazardColor, hazardFillExpression, isNavAidLayer, isVisibleInMode, lateralColor, lateralFillExpression, lightColor, lightFillExpression, paginate, parseWebhook, preprocessLights, preprocessNavAids, preprocessSoundings, preprocessWaterBodies, resolveStyle, scaminToMinZoom, withinBboxFilter, worldMinusBboxPolygon };
package/dist/index.d.ts CHANGED
@@ -18,10 +18,10 @@ import { ExpressionSpecification } from '@maplibre/maplibre-gl-style-spec';
18
18
  * MapLibre clients (iOS/Android/desktop) that don't have the JS atlas
19
19
  * available.
20
20
  *
21
- * 2. Generate the online style themselves (e.g. via the React app's
22
- * `src/lib/atlas/style-generator.ts`) and call `buildOfflineStyle()`
23
- * locally — recommended for JS/web SDK consumers who already use the
24
- * atlas at runtime.
21
+ * 2. Generate the online style themselves (e.g. via this SDK's
22
+ * `generateStyle()` export from `@chrtco/sdk`) and call
23
+ * `buildOfflineStyle()` locally — recommended for JS/web SDK
24
+ * consumers who already use the atlas at runtime.
25
25
  *
26
26
  * Both paths produce equivalent output.
27
27
  */
@@ -672,6 +672,21 @@ interface LookupEntry {
672
672
  viewingGroup?: number;
673
673
  /** Minimum compilation scale for display (SCAMIN) */
674
674
  scamin?: number;
675
+ /**
676
+ * Mark this class as deliberately invisible (label-only or metadata-only)
677
+ * so the layer-coverage audit doesn't flag its geometry-type gaps as bugs.
678
+ *
679
+ * Examples:
680
+ * - SOUNDG: rendered as depth numbers via labels.ts, not as visible dots
681
+ * - LNDRGN: region NAME labels only, no shape
682
+ * - M_NPUB / M_NSYS / M_QUAL / M_SREL / DSID: S-57 metadata classes;
683
+ * present for click-query attributes, never drawn
684
+ *
685
+ * Without this flag, `scripts/audit-layer-coverage.ts` reports the
686
+ * thousands of features in these classes as "gaps" every week even
687
+ * though the invisibility is intentional.
688
+ */
689
+ auditIgnore?: boolean;
675
690
  }
676
691
  /** Resolved style ready for Mapbox GL (tokens → hex) */
677
692
  interface ResolvedStyle {
@@ -743,12 +758,12 @@ interface LabelLayerConfig {
743
758
  textField: ExpressionSpecification | string;
744
759
  /** Text size in pixels */
745
760
  textSize: number | ExpressionSpecification;
746
- /** Text color */
747
- textColor: string;
761
+ /** Text color (hex) or data-driven expression */
762
+ textColor: string | ExpressionSpecification;
748
763
  /** Halo color for contrast */
749
764
  textHaloColor: string;
750
- /** Halo width */
751
- textHaloWidth: number;
765
+ /** Halo width — pixels or zoom-interpolation expression */
766
+ textHaloWidth: number | ExpressionSpecification;
752
767
  /** Text offset [x, y] in ems */
753
768
  textOffset: [number, number];
754
769
  /** Font stack */
@@ -758,8 +773,56 @@ interface LabelLayerConfig {
758
773
  /** Minimum zoom level for this label */
759
774
  minzoom?: number;
760
775
  }
776
+ /**
777
+ * Optional palette-and-data-driven options for label config.
778
+ *
779
+ * Currently used by SOUNDG to pick palette-aware text/halo colors and to
780
+ * drive the S-52 safety-contour two-shade rule. Other classes ignore
781
+ * these options.
782
+ */
783
+ interface LabelConfigOptions {
784
+ /** S-52 colour table — drives palette-aware text and halo colors. */
785
+ colorTable?: ColorTableName;
786
+ /**
787
+ * Safety contour depth in metres. Soundings shoaler than this render in
788
+ * `SNDG2` (high-contrast); deeper soundings render in `SNDG1` (muted).
789
+ * S-52 §SOUNDG02 conditional symbology.
790
+ */
791
+ safetyContour?: number;
792
+ /**
793
+ * When false, all soundings render in `SNDG2` (single-shade). Default
794
+ * true — emit the safety-contour case expression.
795
+ */
796
+ twoShadeSoundings?: boolean;
797
+ /**
798
+ * When "semibold", SOUNDG labels use Open Sans Semibold for crisper
799
+ * numerals at small sizes. Default "semibold".
800
+ */
801
+ soundingFontWeight?: "regular" | "semibold";
802
+ }
803
+ /**
804
+ * Two-tier label rendering strategy for the band-priority style.
805
+ *
806
+ * `feature-band-gated` — emit one variant per source band, each gated to
807
+ * its band's zoom window (the existing `makeBandVariants` behaviour). Right
808
+ * for dense feature labels where seeing the same buoy designation or
809
+ * sounding twice across overlapping bands is visual noise.
810
+ *
811
+ * `orientation-cross-band` — emit a SINGLE label layer with no band
812
+ * filter. The per-class `minzoom` is honoured. Right for orientation
813
+ * "where am I" labels (sea-area names, land regions, cities) — these
814
+ * must persist as the user zooms in, even if the broader-scope cell
815
+ * carrying the OBJNAM is from a coarser band than the one rendering
816
+ * features at the current zoom. Collision arbitration via
817
+ * `ORIENTATION_BAND_SORT_KEY` gives the broader-scope (lower-band) copy
818
+ * priority where the same OBJNAM exists in multiple bands.
819
+ */
820
+ type LabelBehavior = "feature-band-gated" | "orientation-cross-band";
821
+ declare const LABEL_BEHAVIOR: Record<string, LabelBehavior>;
822
+ /** Returns the rendering strategy for an S-57 class. Unknown classes default to band-gated. */
823
+ declare function getLabelBehavior(layerName: string): LabelBehavior;
761
824
  /** Get label config for a given S-57 layer, or null if no labels */
762
- declare function getLabelConfig(layerName: string): LabelLayerConfig | null;
825
+ declare function getLabelConfig(layerName: string, opts?: LabelConfigOptions): LabelLayerConfig | null;
763
826
  /**
764
827
  * Build a Mapbox symbol layer spec from a label config.
765
828
  */
@@ -1171,6 +1234,148 @@ declare function buildLayersForClass(objectClass: string, ct: ColorTableName, so
1171
1234
  */
1172
1235
  declare function getOrderedClasses(): string[];
1173
1236
 
1237
+ /**
1238
+ * Strict scale-band priority for continuous-mode rendering.
1239
+ *
1240
+ * Each ENC feature in the served PMTiles carries `_cell_id` (added at
1241
+ * baremetal NDJSON export time). NOAA's cell-id format encodes the source
1242
+ * scale band as the third character: `US5MA85M` is band 5 (Harbor).
1243
+ *
1244
+ * MapLibre layer filters can't read `["zoom"]`, so to enforce "only one
1245
+ * band renders at any given zoom" we emit one layer copy per band, each
1246
+ * with a static band filter and per-band `minzoom`/`maxzoom`. The ranges
1247
+ * below are non-overlapping (`minzoom` inclusive, `maxzoom` exclusive),
1248
+ * so exactly one band is visible at any continuous zoom.
1249
+ *
1250
+ * Long-term cleanup: baremetal should emit a normalized `_band: 1..6`
1251
+ * integer in NDJSON so this works for non-NOAA chart sources too. The
1252
+ * `["slice", _cell_id, 2, 3]` extraction here is NOAA-specific.
1253
+ */
1254
+ /**
1255
+ * UI-facing scale-band names (matches the app's `ScaleBand` type in
1256
+ * `src/lib/map/constants.ts`). Defined locally so the SDK doesn't have
1257
+ * to depend on app code.
1258
+ */
1259
+ type ScaleBand = "Overview" | "General" | "Coastal" | "Approach" | "Harbor";
1260
+ /** S-57 source bands. NOAA cell ids use these as the third character. */
1261
+ type BandCode = "1" | "2" | "3" | "4" | "5" | "6";
1262
+ interface BandPriorityRange {
1263
+ /** MapLibre `minzoom` (inclusive). */
1264
+ minzoom: number;
1265
+ /** MapLibre `maxzoom` (exclusive). `undefined` means no upper cap. */
1266
+ maxzoom?: number;
1267
+ }
1268
+ /**
1269
+ * Per-band minzoom thresholds for continuous mode. Bands cascade as
1270
+ * fallbacks for one another — every band whose `minzoom` is satisfied
1271
+ * may render. The deeper-numbered band's features render preferentially
1272
+ * (drawn last in the z-stack, so its opaque fills cover lower bands),
1273
+ * and lower bands fill in where the higher band has no coverage.
1274
+ *
1275
+ * Cross-band visual overlap (doubled coastlines, blended fills) is
1276
+ * suppressed by the `_dominated_by_min` per-feature tag added at NDJSON
1277
+ * export time (see `tools/chrt-baremetal/server/src/tilebuild/dominanceIndex.ts`):
1278
+ * every feature whose centroid sits inside a higher-band cell's M_COVR
1279
+ * coverage polygon is tagged with the LOWEST band code (2..6) that
1280
+ * dominates it, and `makeBandVariants()` in the style generator emits
1281
+ * per-(featureBand, dominatorBand) layer variants gated by `maxzoom =
1282
+ * BAND_PRIORITY_RANGES[D].minzoom`. The dominated feature renders until
1283
+ * exactly the zoom where its dominator becomes eligible — solving the
1284
+ * low-zoom "black silhouettes" bug where a band-3 LNDARE dominated by
1285
+ * a band-5 cell was dropped at z7 even though band 5 wasn't yet rendering
1286
+ * at z7. At any pixel, exactly ONE band's data renders — the
1287
+ * most-detailed band whose minzoom has been reached and whose cells
1288
+ * actually cover that pixel.
1289
+ *
1290
+ * Bands 5 and 6 both start at z11; band 6 (Berthing) is drawn AFTER
1291
+ * band 5 (Harbor) so where Berthing exists it visually wins, and
1292
+ * Harbor shows through everywhere else.
1293
+ */
1294
+ declare const BAND_PRIORITY_RANGES: Record<BandCode, BandPriorityRange>;
1295
+ /**
1296
+ * Order in which band layers are emitted (and thus drawn — earlier = lower
1297
+ * in the z-stack). Band 6 last so Berthing draws on top of Harbor.
1298
+ */
1299
+ declare const BAND_DRAW_ORDER: BandCode[];
1300
+ /**
1301
+ * UI scale-band names → cell-id band codes that should render in chart mode
1302
+ * for that selection. The UI doesn't expose Berthing separately, so picking
1303
+ * Harbor includes both `5` and `6` cells.
1304
+ *
1305
+ * `CHART_BAND_CODES` is the *requested* band set (what the user picked);
1306
+ * `CHART_CASCADE_CODES` below adds the coarser bands that fall back where
1307
+ * the requested band has no cells.
1308
+ */
1309
+ declare const CHART_BAND_CODES: Record<ScaleBand, readonly BandCode[]>;
1310
+ /**
1311
+ * UI scale-band names → ordered cascade of cell-id band codes for chart-mode
1312
+ * rendering with graceful coverage fallback. Bands are listed coarsest-first
1313
+ * so the style generator can use array order as draw order: lower bands draw
1314
+ * earlier in the z-stack, the requested band draws last on top.
1315
+ *
1316
+ * The chart-mode implementation emits one filtered layer per cascade band per
1317
+ * S-57 class. Where the requested band's cells provide data, its opaque fills
1318
+ * cover the lower bands underneath; where the requested band has no cells
1319
+ * (e.g. Chart→Harbor over Massachusetts Bay offshore), the next-coarser band
1320
+ * shows through. Different cells in the same viewport may end up rendering at
1321
+ * different effective bands.
1322
+ *
1323
+ * The per-feature `_dominated_by_min` tag baked at baremetal NDJSON-export
1324
+ * time (see `tools/chrt-baremetal/server/src/tilebuild/dominanceIndex.ts`)
1325
+ * suppresses lower-band features inside higher-band cell coverage so cell
1326
+ * boundaries don't render with doubled coastlines or contours.
1327
+ *
1328
+ * Lock scope: when chart-Harbor's viewport-lock greyout is active, the
1329
+ * `["within"]` bbox filter is applied ONLY to the requested band's variants
1330
+ * — cascade fallback bands render across the full viewport. The greyout
1331
+ * overlay still cues the lock boundary visually; the user picks Harbor and
1332
+ * gets Harbor inside the lock plus dimmed Approach/Coastal context outside.
1333
+ *
1334
+ * See `.chrt/ux/chart-mode-band-fallback-2026-05-05.md` for the design
1335
+ * rationale and the geographies (Boston Harbor, Massachusetts Bay offshore)
1336
+ * that motivated this.
1337
+ */
1338
+ declare const CHART_CASCADE_CODES: Record<ScaleBand, readonly BandCode[]>;
1339
+ /**
1340
+ * Build a MapLibre filter expression that matches features in the given
1341
+ * band code(s) by extracting the band character from `_cell_id`.
1342
+ */
1343
+ declare function bandFilterExpression(codes: BandCode | readonly BandCode[]): unknown[];
1344
+ /**
1345
+ * Combine a base filter (may be undefined) with a band filter via `["all"]`.
1346
+ * If the base filter is itself an `["all", ...]`, flatten so we don't end
1347
+ * up with `["all", ["all", ...], ...]` (which is legal but ugly).
1348
+ */
1349
+ declare function combineWithBandFilter(baseFilter: unknown[] | undefined, bandFilter: unknown[]): unknown[];
1350
+ /**
1351
+ * Geographic bounding box used for chart-mode viewport locking.
1352
+ * Coordinates in WGS84 lng/lat. `sw` is south-west corner, `ne` is north-east.
1353
+ */
1354
+ interface BBox {
1355
+ sw: [number, number];
1356
+ ne: [number, number];
1357
+ }
1358
+ /**
1359
+ * Build a MapLibre `["within"]` filter expression that matches features
1360
+ * inside the given bbox. Used by chart mode to spatially limit which
1361
+ * features render — when locked to a Harbor area, only features inside
1362
+ * the locked bbox draw, even though the tile pyramid covers more.
1363
+ *
1364
+ * The polygon is closed (first and last vertex repeated) per the GeoJSON
1365
+ * spec; without that, MapLibre throws on style validation.
1366
+ */
1367
+ declare function withinBboxFilter(bbox: BBox): unknown[];
1368
+ /**
1369
+ * Build a GeoJSON Polygon for the world MINUS a given bbox — i.e. an
1370
+ * outer ring covering the whole world with a single hole punched at
1371
+ * the bbox. Used by the frontend as the source for a translucent grey
1372
+ * overlay so the user sees clearly where the chart is "locked."
1373
+ *
1374
+ * Outer ring uses the standard antimeridian-safe world extent
1375
+ * (lng [-180, 180], lat [-85, 85] — Web Mercator's safe range).
1376
+ */
1377
+ declare function worldMinusBboxPolygon(bbox: BBox): GeoJSON.Polygon;
1378
+
1174
1379
  /**
1175
1380
  * Generates a complete Mapbox GL Style Specification v8 JSON
1176
1381
  * from the S-52 atlas definitions.
@@ -1185,8 +1390,57 @@ interface MapboxStyle {
1185
1390
  glyphs: string;
1186
1391
  layers: StyleLayer[];
1187
1392
  }
1188
- declare function generateStyle(palette?: ColorTableName, opts?: BuildSourcesOptions): MapboxStyle;
1189
- declare function generateStyleJSON(palette?: ColorTableName, opts?: BuildSourcesOptions): string;
1393
+ interface StyleGeneratorOptions extends BuildSourcesOptions {
1394
+ /**
1395
+ * When set, emit a "paper chart" style centered on the selected scale
1396
+ * band, with graceful coverage fallback: the requested band's layers
1397
+ * draw on top where its cells have data; coarser bands (driven by
1398
+ * `CHART_CASCADE_CODES`) fill gaps where the requested band has no
1399
+ * cells. Per-feature SCAMIN minzooms are preserved on every cascade
1400
+ * variant; `maxzoom` is dropped so the user can zoom in indefinitely.
1401
+ *
1402
+ * When omitted (default), continuous mode emits one layer copy per
1403
+ * source band with strict per-band zoom windows so exactly one band
1404
+ * renders at any given continuous zoom.
1405
+ *
1406
+ * `lockedBounds` (optional) restricts chart-mode rendering to a
1407
+ * specific bbox via a `["within"]` filter — applied ONLY to the
1408
+ * requested band's variants. Cascade fallback bands render across the
1409
+ * full viewport so a chart-Harbor user panning offshore still sees
1410
+ * Approach/Coastal data. The frontend pairs this with a translucent
1411
+ * grey overlay outside the bbox so the lock boundary is still visible
1412
+ * to the user (the fallback data is dimmed by the overlay but not
1413
+ * filtered out).
1414
+ */
1415
+ chartMode?: {
1416
+ band: ScaleBand;
1417
+ lockedBounds?: BBox;
1418
+ };
1419
+ /**
1420
+ * Display unit for depth labels (SOUNDG soundings, DEPCNT contour
1421
+ * values). Source data is always meters (S-57 / VALSOU / VALDCO);
1422
+ * "feet" wraps the text-field expression with a unit conversion +
1423
+ * round so the rendered numbers match. Default "meters".
1424
+ */
1425
+ depthUnits?: "meters" | "feet";
1426
+ /**
1427
+ * S-52 §SOUNDG02 — soundings shoaler than this depth (metres) render in
1428
+ * the high-contrast color; deeper soundings render in the muted color.
1429
+ * Default 10 m.
1430
+ */
1431
+ safetyContour?: number;
1432
+ /**
1433
+ * When false, all soundings render in a single shade. Default true.
1434
+ */
1435
+ twoShadeSoundings?: boolean;
1436
+ /**
1437
+ * Font weight for SOUNDG labels. Default "semibold" — crisper numerals
1438
+ * at small sizes on screen.
1439
+ */
1440
+ soundingFontWeight?: "regular" | "semibold";
1441
+ }
1442
+ declare function generateStyle(palette?: ColorTableName, opts?: StyleGeneratorOptions): MapboxStyle;
1443
+ declare function generateStyleJSON(palette?: ColorTableName, opts?: StyleGeneratorOptions): string;
1190
1444
  /** Get summary stats about the generated style */
1191
1445
  declare function getStyleStats(style: MapboxStyle): {
1192
1446
  totalLayers: number;
@@ -1256,4 +1510,4 @@ declare class Chrt {
1256
1510
  transformRequest(): (url: string) => TransformRequest | undefined;
1257
1511
  }
1258
1512
 
1259
- export { type AtlasStyle, BUILD_MAXZOOM, BUILD_MINZOOM, type BuildOfflineStyleOptions, type BuildSourcesOptions, CHART_SECTIONS, type CellDeletedEvent, type CellDetail, type CellEventData, type CellNewEvent, type CellUpdatedData, type CellUpdatedEvent, type CellsOptions, type CellsResponse, type ChartSection, Chrt, type ChrtConfig, ChrtError, ColorTableName, type CoverageOptions, type CoverageResponse, type CoverageSource, DEFAULT_REGION, DEFAULT_TILE_BASE_URL, type DisplayCategory, type DisplayMode, type FeatureCollection, type FeaturesOptions, FileSystemAdapter, type GeoJsonFeature, type GeoJsonGeometry, type GeomType, LABELED_LAYERS, type LabelLayerConfig, type LookupEntry, type MapLibreStyle, type MapboxStyle, type MapboxVectorSource, NAUTICAL_SYMBOLS, type NauticalCategory, type NauticalSymbol, type OfflineBundleResponse, type OfflineDownloadOptions, type OfflineDownloadProgress, type OfflineDownloadResult, OfflineError, type OfflineErrorCode, type OfflineFetchStyleOptions, type OfflineHeartbeatOptions, type OfflineRegionSummary, type OfflineRemoveOptions, OfflineStyleError, type OfflineStylePlatform, type OfflineStyleResponse, type PmtilesVectorSource, type QueryOptions, type ResolvedStyle, type RouteFeatureQueryOptions, type RouteFeatureQueryResponse, type RouteQueryOptions, type RouteQueryResponse, type S101Feature, S101_CHART_SECTIONS, S101_FEATURES, S101_NEW_SYMBOLS, S101_SYMBOL_GROUPS, S52_DRAW_ORDER, S52_LOOKUP, SYMBOL_GROUPS, SYMBOL_MAP, type SourceMode, type StyleLayer, type SymbolGroup, type TileGroup, type TileGroupsResponse, type TransformRequest, type VectorSourceSpec, type WebhookEvent, type WebhookEventType, type WebhookPayload, applyColorTheme, buildLabelLayer, buildLayersForClass, buildOfflineStyle, buildSources, daymarkColor, decimateFeatures, depareColor, depareFillExpression, generateSpriteManifest, generateSpriteSheet, generateStyle, generateStyleJSON, getAvailableLineSymbols, getAvailablePatterns, getAvailablePointSymbols, getCollisionSettings, getDrawPriority, getGridSizeForZoom, getIconExpression, getLabelConfig, getLookupEntry, getMaxCellsForZoom, getOrderedClasses, getS52SvgString, getS52SymbolStats, getStyleStats, getSymbolSvgString, getThemedS52Svg, hazardColor, hazardFillExpression, isNavAidLayer, isVisibleInMode, lateralColor, lateralFillExpression, lightColor, lightFillExpression, paginate, parseWebhook, preprocessLights, preprocessNavAids, preprocessSoundings, preprocessWaterBodies, resolveStyle, scaminToMinZoom };
1513
+ export { type AtlasStyle, BAND_DRAW_ORDER, BAND_PRIORITY_RANGES, type BBox, BUILD_MAXZOOM, BUILD_MINZOOM, type BandCode, type BandPriorityRange, type BuildOfflineStyleOptions, type BuildSourcesOptions, CHART_BAND_CODES, CHART_CASCADE_CODES, CHART_SECTIONS, type CellDeletedEvent, type CellDetail, type CellEventData, type CellNewEvent, type CellUpdatedData, type CellUpdatedEvent, type CellsOptions, type CellsResponse, type ChartSection, Chrt, type ChrtConfig, ChrtError, ColorTableName, type CoverageOptions, type CoverageResponse, type CoverageSource, DEFAULT_REGION, DEFAULT_TILE_BASE_URL, type DisplayCategory, type DisplayMode, type FeatureCollection, type FeaturesOptions, FileSystemAdapter, type GeoJsonFeature, type GeoJsonGeometry, type GeomType, LABELED_LAYERS, LABEL_BEHAVIOR, type LabelBehavior, type LabelConfigOptions, type LabelLayerConfig, type LookupEntry, type MapLibreStyle, type MapboxStyle, type MapboxVectorSource, NAUTICAL_SYMBOLS, type NauticalCategory, type NauticalSymbol, type OfflineBundleResponse, type OfflineDownloadOptions, type OfflineDownloadProgress, type OfflineDownloadResult, OfflineError, type OfflineErrorCode, type OfflineFetchStyleOptions, type OfflineHeartbeatOptions, type OfflineRegionSummary, type OfflineRemoveOptions, OfflineStyleError, type OfflineStylePlatform, type OfflineStyleResponse, type PmtilesVectorSource, type QueryOptions, type ResolvedStyle, type RouteFeatureQueryOptions, type RouteFeatureQueryResponse, type RouteQueryOptions, type RouteQueryResponse, type S101Feature, S101_CHART_SECTIONS, S101_FEATURES, S101_NEW_SYMBOLS, S101_SYMBOL_GROUPS, S52_DRAW_ORDER, S52_LOOKUP, SYMBOL_GROUPS, SYMBOL_MAP, type ScaleBand, type SourceMode, type StyleGeneratorOptions, type StyleLayer, type SymbolGroup, type TileGroup, type TileGroupsResponse, type TransformRequest, type VectorSourceSpec, type WebhookEvent, type WebhookEventType, type WebhookPayload, applyColorTheme, bandFilterExpression, buildLabelLayer, buildLayersForClass, buildOfflineStyle, buildSources, combineWithBandFilter, daymarkColor, decimateFeatures, depareColor, depareFillExpression, generateSpriteManifest, generateSpriteSheet, generateStyle, generateStyleJSON, getAvailableLineSymbols, getAvailablePatterns, getAvailablePointSymbols, getCollisionSettings, getDrawPriority, getGridSizeForZoom, getIconExpression, getLabelBehavior, getLabelConfig, getLookupEntry, getMaxCellsForZoom, getOrderedClasses, getS52SvgString, getS52SymbolStats, getStyleStats, getSymbolSvgString, getThemedS52Svg, hazardColor, hazardFillExpression, isNavAidLayer, isVisibleInMode, lateralColor, lateralFillExpression, lightColor, lightFillExpression, paginate, parseWebhook, preprocessLights, preprocessNavAids, preprocessSoundings, preprocessWaterBodies, resolveStyle, scaminToMinZoom, withinBboxFilter, worldMinusBboxPolygon };