@chrtco/sdk 0.10.32 → 0.10.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -24350,6 +24350,197 @@ function buildPlaceNameLayers(sourceId) {
24350
24350
  make("land", PLACENAME_LAND_CLASSES, ["Open Sans Regular"], COLOR.LAND_REGION, 0.02, PLACENAME_LAND_SIZE_STOPS)
24351
24351
  ];
24352
24352
  }
24353
+ var DRGNOTE_SOURCE_LAYER = "DRGNOTE";
24354
+ var DRGNOTE_LABEL_PRIORITY = 300;
24355
+ var DRGNOTE_MINZOOM = 13;
24356
+ var DRGNOTE_FULL_ZOOM = 16;
24357
+ var DRGNOTE_MIN_TEXT_PX = 1;
24358
+ var DRGNOTE_MAX_TEXT_PX = 64;
24359
+ function legacyNoteSizePx(zoom) {
24360
+ const z = Math.min(16, Math.max(13, zoom));
24361
+ return 10.5 + (13 - 10.5) * (z - 13) / 3;
24362
+ }
24363
+ var DRGNOTE_FIT_ZOOM = 15;
24364
+ function dredgeNoteSize() {
24365
+ const stops = [];
24366
+ for (let z = 12; z <= 20; z++) {
24367
+ const prop = z < DRGNOTE_FULL_ZOOM ? "size_s" : "size_f";
24368
+ const fitted = ["to-number", ["get", prop], 0];
24369
+ stops.push(z, [
24370
+ "case",
24371
+ [">", fitted, 0],
24372
+ [
24373
+ "min",
24374
+ DRGNOTE_MAX_TEXT_PX,
24375
+ ["max", DRGNOTE_MIN_TEXT_PX, ["*", fitted, 2 ** (z - DRGNOTE_FIT_ZOOM)]]
24376
+ ],
24377
+ legacyNoteSizePx(z)
24378
+ ]);
24379
+ }
24380
+ return ["interpolate", ["exponential", 2], ["zoom"], ...stops];
24381
+ }
24382
+ function noteDepthText(prop, depthUnits) {
24383
+ return depthUnits === "feet" ? ["concat", ["to-string", ["round", ["*", ["to-number", ["get", prop]], 3.28084]]], " FT"] : ["concat", ["to-string", ["get", prop]], " M"];
24384
+ }
24385
+ function buildDredgeNoteLayers(sourceId, palette = "DAY", depthUnits = "meters") {
24386
+ const widthProp = depthUnits === "feet" ? "width_ft" : "width_m";
24387
+ const widthUnit = depthUnits === "feet" ? " FT" : " M";
24388
+ const titleText = [
24389
+ "to-string",
24390
+ ["coalesce", ["get", "designation"], ["get", "name"], ""]
24391
+ ];
24392
+ const placeText = ["to-string", ["coalesce", ["get", "place"], ""]];
24393
+ const sep = (prop) => [
24394
+ "to-string",
24395
+ ["coalesce", ["get", prop], "\n"]
24396
+ ];
24397
+ const legendText = [
24398
+ "concat",
24399
+ noteDepthText("depth_m", depthUnits),
24400
+ [
24401
+ "case",
24402
+ [">", ["to-number", ["get", "qual_m"], -1], 0],
24403
+ [
24404
+ "concat",
24405
+ " ",
24406
+ noteDepthText("qual_m", depthUnits),
24407
+ " ",
24408
+ ["to-string", ["get", "qual_part"]]
24409
+ ],
24410
+ ""
24411
+ ],
24412
+ [
24413
+ "case",
24414
+ [">", ["to-number", ["get", widthProp], -1], 0],
24415
+ ["concat", " FOR A WIDTH OF ", ["to-string", ["get", widthProp]], widthUnit],
24416
+ ""
24417
+ ],
24418
+ [
24419
+ "case",
24420
+ ["!=", ["to-string", ["get", "date"]], ""],
24421
+ ["concat", " ", ["to-string", ["get", "date"]]],
24422
+ ""
24423
+ ],
24424
+ ["case", ["==", ["get", "unmaintained"], true], " NOT MAINTAINED", ""]
24425
+ ];
24426
+ const shortField = [
24427
+ "case",
24428
+ ["!=", titleText, ""],
24429
+ ["concat", titleText, sep("sep_s"), legendText],
24430
+ legendText
24431
+ ];
24432
+ const fullField = [
24433
+ "case",
24434
+ ["!=", placeText, ""],
24435
+ ["concat", placeText, sep("sep_fp"), titleText, sep("sep_ft"), legendText],
24436
+ ["case", ["!=", titleText, ""], ["concat", titleText, sep("sep_ft"), legendText], legendText]
24437
+ ];
24438
+ const noteField = [
24439
+ "step",
24440
+ ["zoom"],
24441
+ shortField,
24442
+ DRGNOTE_FULL_ZOOM,
24443
+ fullField
24444
+ ];
24445
+ const sortKey = [
24446
+ "case",
24447
+ [">=", ["to-number", ["get", "rank"], -1], 0],
24448
+ ["to-number", ["get", "rank"]],
24449
+ ["*", 1e3, ["to-number", ["get", "depth_m"], 99]]
24450
+ ];
24451
+ return [
24452
+ {
24453
+ // The `-label` suffix is how this repo marks a label layer, and
24454
+ // z-order guards partition on it (sector-light-zorder.test.ts).
24455
+ id: "chrt-dredge-note-label",
24456
+ type: "symbol",
24457
+ source: sourceId,
24458
+ "source-layer": DRGNOTE_SOURCE_LAYER,
24459
+ minzoom: DRGNOTE_MINZOOM,
24460
+ layout: {
24461
+ "text-field": noteField,
24462
+ "text-font": ["Open Sans Regular"],
24463
+ // Ink on the chart, not type on the screen.
24464
+ //
24465
+ // The size is fitted to THIS polygon at build time (px at z15) and
24466
+ // doubled per zoom here, so the note holds a fixed GEOGRAPHIC size
24467
+ // the way a raster chart's note is fixed to its paper. Set at one
24468
+ // screen size instead, a note fits its channel at one zoom and
24469
+ // spills at every other — measured at Gloucester z15, only 37% of
24470
+ // North Channel's note lay inside North Channel.
24471
+ //
24472
+ // `interpolate` with base 2 and stops ONE zoom apart is exactly
24473
+ // geometric: t = (2^(z-z1)-1)/(2^1-1) and v2 = 2*v1 collapse to
24474
+ // v1*2^(z-z1). Writing it the obvious way instead —
24475
+ // ["*", size, ["^", 2, ["-", ["zoom"], 15]]] — is INVALID, because
24476
+ // ["zoom"] may only be the direct input of an outer interpolate or
24477
+ // step. MapLibre rejects it silently, keeps the previous value and
24478
+ // renders on as if nothing happened.
24479
+ //
24480
+ // It is NOT clamped to a legible band. That was tried and it
24481
+ // defeated the point: measured across z13-z18 a clamp bound at
24482
+ // five of six zooms for every Gloucester note, pinning the note to
24483
+ // a fixed screen size while its channel grew. A note that keeps
24484
+ // its proportion is illegibly small when its channel is small,
24485
+ // and that is correct — it is how the eye tells which channel a
24486
+ // note belongs to, and how a raster chart behaves. Zoom in and it
24487
+ // grows with the water. A tile built before the fit carries no
24488
+ // size and keeps the fixed size it was designed for.
24489
+ "text-size": dredgeNoteSize(),
24490
+ "text-transform": "uppercase",
24491
+ "text-letter-spacing": 0.08,
24492
+ // ONE line, and wide enough never to wrap into a second.
24493
+ //
24494
+ // Must equal noteFit.ts#LINE_HEIGHT_EM — the build side measures
24495
+ // the note's height with it before deciding how large it can set.
24496
+ "text-line-height": 1.2,
24497
+ // Wide enough that MapLibre NEVER wraps. Every break in this note
24498
+ // is an explicit "\n" the build side chose by fitting the result
24499
+ // against the polygon; an automatic wrap on top of that would
24500
+ // re-flow the label into lines nothing measured, and MapLibre's
24501
+ // line breaker balances lines rather than filling greedily, so the
24502
+ // build side cannot predict where it would land.
24503
+ "text-max-width": 512,
24504
+ "text-rotate": ["get", "rotate"],
24505
+ "text-rotation-alignment": "map",
24506
+ "text-padding": 3,
24507
+ "text-allow-overlap": false,
24508
+ "text-ignore-placement": false,
24509
+ "text-optional": true,
24510
+ // Placement rank from the build side (dredgeNote.ts#stationRank):
24511
+ // shoaler water always survives a collision, bigger channel first
24512
+ // among equal depths.
24513
+ //
24514
+ // Deliberately NOT labelSortKey. That helper clamps its second
24515
+ // argument to [0, 999] — fine for a VALDCO contour value, fatal
24516
+ // here: every rank in the millions collapsed to 999 and both notes
24517
+ // evaluated to the identical key 300999, making the whole ranking
24518
+ // inert. The priority prefix it adds is redundant anyway, because
24519
+ // symbol-sort-key only orders WITHIN a layer; cross-layer order is
24520
+ // the layer stack's job (spliceByLabelPriority). PLACENAME reads
24521
+ // its own `rank` bare for exactly this reason.
24522
+ //
24523
+ // Tiles built before `rank` existed fall back to depth ordering,
24524
+ // so they degrade to shoal-first rather than to one flat value.
24525
+ "symbol-sort-key": sortKey
24526
+ },
24527
+ paint: {
24528
+ "text-color": resolveColor("CHBLK", palette),
24529
+ // Halo in the dredged-area fill itself, so the note reads as set
24530
+ // INTO the channel rather than floating over it. Kept thin: the
24531
+ // note crosses its own dashed limit and whatever soundings sit in
24532
+ // the channel, so it needs SOME separation — but a wide halo on
24533
+ // ~11px type erodes the glyph strokes and turns black type grey.
24534
+ "text-halo-color": resolveColor("DEPMD", palette),
24535
+ "text-halo-width": 1
24536
+ },
24537
+ metadata: {
24538
+ "chrt:dredgeNote": true,
24539
+ "chrt:labelPriority": DRGNOTE_LABEL_PRIORITY
24540
+ }
24541
+ }
24542
+ ];
24543
+ }
24353
24544
 
24354
24545
  // src/atlas/feature-decimation.ts
24355
24546
  function getGridSizeForZoom(zoom) {
@@ -27620,6 +27811,22 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
27620
27811
  metadata: { "s52:objectClass": "ACHARE", "s52:role": "boundary" }
27621
27812
  });
27622
27813
  }
27814
+ if (objectClass === "DRGARE" && resolved.type === "fill") {
27815
+ layers.push({
27816
+ id: "s52-DRGARE-boundary",
27817
+ type: "line",
27818
+ source: sourceId,
27819
+ "source-layer": "DRGARE",
27820
+ filter: GEOM_FILTERS.fill,
27821
+ paint: {
27822
+ "line-color": resolveColor("CHGRD", ct),
27823
+ "line-width": 0.9,
27824
+ "line-dasharray": [4, 3],
27825
+ "line-opacity": 0.75
27826
+ },
27827
+ metadata: { "s52:objectClass": "DRGARE", "s52:role": "boundary" }
27828
+ });
27829
+ }
27623
27830
  if (objectClass === "ACHARE" && resolved.type === "fill") {
27624
27831
  layers.push({
27625
27832
  id: "s52-ACHARE-anchor-icon",
@@ -29695,6 +29902,17 @@ function byLabelPriorityBottomUp(bundles) {
29695
29902
  (a, b) => getLabelPriority(b.objectClass) - getLabelPriority(a.objectClass)
29696
29903
  );
29697
29904
  }
29905
+ function spliceByLabelPriority(labelLayers, incoming, priority) {
29906
+ if (incoming.length === 0) return;
29907
+ const rankOf = (l) => {
29908
+ const meta = l.metadata;
29909
+ const p = meta?.["chrt:labelPriority"];
29910
+ return typeof p === "number" ? p : DEFAULT_LABEL_PRIORITY;
29911
+ };
29912
+ const at = labelLayers.findIndex((l) => rankOf(l) < priority);
29913
+ if (at === -1) labelLayers.push(...incoming);
29914
+ else labelLayers.splice(at, 0, ...incoming);
29915
+ }
29698
29916
  function chartModeBandClauses(layer, B, higherCascadeBands, lockedBounds, isRequestedBand) {
29699
29917
  const parts = [bandFilterExpression(B)];
29700
29918
  if (!isDominanceExempt(layer)) {
@@ -29881,6 +30099,15 @@ function generateStyle(palette = "DAY", opts = {}) {
29881
30099
  labelLayers.push(variant);
29882
30100
  }
29883
30101
  }
30102
+ spliceByLabelPriority(
30103
+ labelLayers,
30104
+ buildDredgeNoteLayers(
30105
+ sourceId,
30106
+ palette,
30107
+ opts.depthUnits ?? "meters"
30108
+ ),
30109
+ DRGNOTE_LABEL_PRIORITY
30110
+ );
29884
30111
  layers.push(...labelLayers);
29885
30112
  } else {
29886
30113
  const emitBand = (base, code) => bandFan === "legacy" ? makeBandVariants(base, code) : makeCollapsedBandVariant(base, code, isDominanceExempt(base));
@@ -29968,6 +30195,15 @@ function generateStyle(palette = "DAY", opts = {}) {
29968
30195
  }
29969
30196
  }
29970
30197
  if (!majorsEmitted) labelLayers.push(...majorWaterLayers);
30198
+ spliceByLabelPriority(
30199
+ labelLayers,
30200
+ buildDredgeNoteLayers(
30201
+ sourceId,
30202
+ palette,
30203
+ opts.depthUnits ?? "meters"
30204
+ ),
30205
+ DRGNOTE_LABEL_PRIORITY
30206
+ );
29971
30207
  layers.push(...labelLayers);
29972
30208
  }
29973
30209
  const symbolSet = opts.symbolSet ?? "s52";