@chrtco/sdk 0.10.32 → 0.10.33

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
@@ -24574,6 +24574,197 @@ function buildPlaceNameLayers(sourceId) {
24574
24574
  make("land", PLACENAME_LAND_CLASSES, ["Open Sans Regular"], COLOR.LAND_REGION, 0.02, PLACENAME_LAND_SIZE_STOPS)
24575
24575
  ];
24576
24576
  }
24577
+ var DRGNOTE_SOURCE_LAYER = "DRGNOTE";
24578
+ var DRGNOTE_LABEL_PRIORITY = 300;
24579
+ var DRGNOTE_MINZOOM = 13;
24580
+ var DRGNOTE_FULL_ZOOM = 16;
24581
+ var DRGNOTE_MIN_TEXT_PX = 1;
24582
+ var DRGNOTE_MAX_TEXT_PX = 64;
24583
+ function legacyNoteSizePx(zoom) {
24584
+ const z = Math.min(16, Math.max(13, zoom));
24585
+ return 10.5 + (13 - 10.5) * (z - 13) / 3;
24586
+ }
24587
+ var DRGNOTE_FIT_ZOOM = 15;
24588
+ function dredgeNoteSize() {
24589
+ const stops = [];
24590
+ for (let z = 12; z <= 20; z++) {
24591
+ const prop = z < DRGNOTE_FULL_ZOOM ? "size_s" : "size_f";
24592
+ const fitted = ["to-number", ["get", prop], 0];
24593
+ stops.push(z, [
24594
+ "case",
24595
+ [">", fitted, 0],
24596
+ [
24597
+ "min",
24598
+ DRGNOTE_MAX_TEXT_PX,
24599
+ ["max", DRGNOTE_MIN_TEXT_PX, ["*", fitted, 2 ** (z - DRGNOTE_FIT_ZOOM)]]
24600
+ ],
24601
+ legacyNoteSizePx(z)
24602
+ ]);
24603
+ }
24604
+ return ["interpolate", ["exponential", 2], ["zoom"], ...stops];
24605
+ }
24606
+ function noteDepthText(prop, depthUnits) {
24607
+ return depthUnits === "feet" ? ["concat", ["to-string", ["round", ["*", ["to-number", ["get", prop]], 3.28084]]], " FT"] : ["concat", ["to-string", ["get", prop]], " M"];
24608
+ }
24609
+ function buildDredgeNoteLayers(sourceId, palette = "DAY", depthUnits = "meters") {
24610
+ const widthProp = depthUnits === "feet" ? "width_ft" : "width_m";
24611
+ const widthUnit = depthUnits === "feet" ? " FT" : " M";
24612
+ const titleText = [
24613
+ "to-string",
24614
+ ["coalesce", ["get", "designation"], ["get", "name"], ""]
24615
+ ];
24616
+ const placeText = ["to-string", ["coalesce", ["get", "place"], ""]];
24617
+ const sep = (prop) => [
24618
+ "to-string",
24619
+ ["coalesce", ["get", prop], "\n"]
24620
+ ];
24621
+ const legendText = [
24622
+ "concat",
24623
+ noteDepthText("depth_m", depthUnits),
24624
+ [
24625
+ "case",
24626
+ [">", ["to-number", ["get", "qual_m"], -1], 0],
24627
+ [
24628
+ "concat",
24629
+ " ",
24630
+ noteDepthText("qual_m", depthUnits),
24631
+ " ",
24632
+ ["to-string", ["get", "qual_part"]]
24633
+ ],
24634
+ ""
24635
+ ],
24636
+ [
24637
+ "case",
24638
+ [">", ["to-number", ["get", widthProp], -1], 0],
24639
+ ["concat", " FOR A WIDTH OF ", ["to-string", ["get", widthProp]], widthUnit],
24640
+ ""
24641
+ ],
24642
+ [
24643
+ "case",
24644
+ ["!=", ["to-string", ["get", "date"]], ""],
24645
+ ["concat", " ", ["to-string", ["get", "date"]]],
24646
+ ""
24647
+ ],
24648
+ ["case", ["==", ["get", "unmaintained"], true], " NOT MAINTAINED", ""]
24649
+ ];
24650
+ const shortField = [
24651
+ "case",
24652
+ ["!=", titleText, ""],
24653
+ ["concat", titleText, sep("sep_s"), legendText],
24654
+ legendText
24655
+ ];
24656
+ const fullField = [
24657
+ "case",
24658
+ ["!=", placeText, ""],
24659
+ ["concat", placeText, sep("sep_fp"), titleText, sep("sep_ft"), legendText],
24660
+ ["case", ["!=", titleText, ""], ["concat", titleText, sep("sep_ft"), legendText], legendText]
24661
+ ];
24662
+ const noteField = [
24663
+ "step",
24664
+ ["zoom"],
24665
+ shortField,
24666
+ DRGNOTE_FULL_ZOOM,
24667
+ fullField
24668
+ ];
24669
+ const sortKey = [
24670
+ "case",
24671
+ [">=", ["to-number", ["get", "rank"], -1], 0],
24672
+ ["to-number", ["get", "rank"]],
24673
+ ["*", 1e3, ["to-number", ["get", "depth_m"], 99]]
24674
+ ];
24675
+ return [
24676
+ {
24677
+ // The `-label` suffix is how this repo marks a label layer, and
24678
+ // z-order guards partition on it (sector-light-zorder.test.ts).
24679
+ id: "chrt-dredge-note-label",
24680
+ type: "symbol",
24681
+ source: sourceId,
24682
+ "source-layer": DRGNOTE_SOURCE_LAYER,
24683
+ minzoom: DRGNOTE_MINZOOM,
24684
+ layout: {
24685
+ "text-field": noteField,
24686
+ "text-font": ["Open Sans Regular"],
24687
+ // Ink on the chart, not type on the screen.
24688
+ //
24689
+ // The size is fitted to THIS polygon at build time (px at z15) and
24690
+ // doubled per zoom here, so the note holds a fixed GEOGRAPHIC size
24691
+ // the way a raster chart's note is fixed to its paper. Set at one
24692
+ // screen size instead, a note fits its channel at one zoom and
24693
+ // spills at every other — measured at Gloucester z15, only 37% of
24694
+ // North Channel's note lay inside North Channel.
24695
+ //
24696
+ // `interpolate` with base 2 and stops ONE zoom apart is exactly
24697
+ // geometric: t = (2^(z-z1)-1)/(2^1-1) and v2 = 2*v1 collapse to
24698
+ // v1*2^(z-z1). Writing it the obvious way instead —
24699
+ // ["*", size, ["^", 2, ["-", ["zoom"], 15]]] — is INVALID, because
24700
+ // ["zoom"] may only be the direct input of an outer interpolate or
24701
+ // step. MapLibre rejects it silently, keeps the previous value and
24702
+ // renders on as if nothing happened.
24703
+ //
24704
+ // It is NOT clamped to a legible band. That was tried and it
24705
+ // defeated the point: measured across z13-z18 a clamp bound at
24706
+ // five of six zooms for every Gloucester note, pinning the note to
24707
+ // a fixed screen size while its channel grew. A note that keeps
24708
+ // its proportion is illegibly small when its channel is small,
24709
+ // and that is correct — it is how the eye tells which channel a
24710
+ // note belongs to, and how a raster chart behaves. Zoom in and it
24711
+ // grows with the water. A tile built before the fit carries no
24712
+ // size and keeps the fixed size it was designed for.
24713
+ "text-size": dredgeNoteSize(),
24714
+ "text-transform": "uppercase",
24715
+ "text-letter-spacing": 0.08,
24716
+ // ONE line, and wide enough never to wrap into a second.
24717
+ //
24718
+ // Must equal noteFit.ts#LINE_HEIGHT_EM — the build side measures
24719
+ // the note's height with it before deciding how large it can set.
24720
+ "text-line-height": 1.2,
24721
+ // Wide enough that MapLibre NEVER wraps. Every break in this note
24722
+ // is an explicit "\n" the build side chose by fitting the result
24723
+ // against the polygon; an automatic wrap on top of that would
24724
+ // re-flow the label into lines nothing measured, and MapLibre's
24725
+ // line breaker balances lines rather than filling greedily, so the
24726
+ // build side cannot predict where it would land.
24727
+ "text-max-width": 512,
24728
+ "text-rotate": ["get", "rotate"],
24729
+ "text-rotation-alignment": "map",
24730
+ "text-padding": 3,
24731
+ "text-allow-overlap": false,
24732
+ "text-ignore-placement": false,
24733
+ "text-optional": true,
24734
+ // Placement rank from the build side (dredgeNote.ts#stationRank):
24735
+ // shoaler water always survives a collision, bigger channel first
24736
+ // among equal depths.
24737
+ //
24738
+ // Deliberately NOT labelSortKey. That helper clamps its second
24739
+ // argument to [0, 999] — fine for a VALDCO contour value, fatal
24740
+ // here: every rank in the millions collapsed to 999 and both notes
24741
+ // evaluated to the identical key 300999, making the whole ranking
24742
+ // inert. The priority prefix it adds is redundant anyway, because
24743
+ // symbol-sort-key only orders WITHIN a layer; cross-layer order is
24744
+ // the layer stack's job (spliceByLabelPriority). PLACENAME reads
24745
+ // its own `rank` bare for exactly this reason.
24746
+ //
24747
+ // Tiles built before `rank` existed fall back to depth ordering,
24748
+ // so they degrade to shoal-first rather than to one flat value.
24749
+ "symbol-sort-key": sortKey
24750
+ },
24751
+ paint: {
24752
+ "text-color": resolveColor("CHBLK", palette),
24753
+ // Halo in the dredged-area fill itself, so the note reads as set
24754
+ // INTO the channel rather than floating over it. Kept thin: the
24755
+ // note crosses its own dashed limit and whatever soundings sit in
24756
+ // the channel, so it needs SOME separation — but a wide halo on
24757
+ // ~11px type erodes the glyph strokes and turns black type grey.
24758
+ "text-halo-color": resolveColor("DEPMD", palette),
24759
+ "text-halo-width": 1
24760
+ },
24761
+ metadata: {
24762
+ "chrt:dredgeNote": true,
24763
+ "chrt:labelPriority": DRGNOTE_LABEL_PRIORITY
24764
+ }
24765
+ }
24766
+ ];
24767
+ }
24577
24768
 
24578
24769
  // src/atlas/feature-decimation.ts
24579
24770
  function getGridSizeForZoom(zoom) {
@@ -27844,6 +28035,22 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
27844
28035
  metadata: { "s52:objectClass": "ACHARE", "s52:role": "boundary" }
27845
28036
  });
27846
28037
  }
28038
+ if (objectClass === "DRGARE" && resolved.type === "fill") {
28039
+ layers.push({
28040
+ id: "s52-DRGARE-boundary",
28041
+ type: "line",
28042
+ source: sourceId,
28043
+ "source-layer": "DRGARE",
28044
+ filter: GEOM_FILTERS.fill,
28045
+ paint: {
28046
+ "line-color": resolveColor("CHGRD", ct),
28047
+ "line-width": 0.9,
28048
+ "line-dasharray": [4, 3],
28049
+ "line-opacity": 0.75
28050
+ },
28051
+ metadata: { "s52:objectClass": "DRGARE", "s52:role": "boundary" }
28052
+ });
28053
+ }
27847
28054
  if (objectClass === "ACHARE" && resolved.type === "fill") {
27848
28055
  layers.push({
27849
28056
  id: "s52-ACHARE-anchor-icon",
@@ -29919,6 +30126,17 @@ function byLabelPriorityBottomUp(bundles) {
29919
30126
  (a, b) => getLabelPriority(b.objectClass) - getLabelPriority(a.objectClass)
29920
30127
  );
29921
30128
  }
30129
+ function spliceByLabelPriority(labelLayers, incoming, priority) {
30130
+ if (incoming.length === 0) return;
30131
+ const rankOf = (l) => {
30132
+ const meta = l.metadata;
30133
+ const p = meta?.["chrt:labelPriority"];
30134
+ return typeof p === "number" ? p : DEFAULT_LABEL_PRIORITY;
30135
+ };
30136
+ const at = labelLayers.findIndex((l) => rankOf(l) < priority);
30137
+ if (at === -1) labelLayers.push(...incoming);
30138
+ else labelLayers.splice(at, 0, ...incoming);
30139
+ }
29922
30140
  function chartModeBandClauses(layer, B, higherCascadeBands, lockedBounds, isRequestedBand) {
29923
30141
  const parts = [bandFilterExpression(B)];
29924
30142
  if (!isDominanceExempt(layer)) {
@@ -30105,6 +30323,15 @@ function generateStyle(palette = "DAY", opts = {}) {
30105
30323
  labelLayers.push(variant);
30106
30324
  }
30107
30325
  }
30326
+ spliceByLabelPriority(
30327
+ labelLayers,
30328
+ buildDredgeNoteLayers(
30329
+ sourceId,
30330
+ palette,
30331
+ opts.depthUnits ?? "meters"
30332
+ ),
30333
+ DRGNOTE_LABEL_PRIORITY
30334
+ );
30108
30335
  layers.push(...labelLayers);
30109
30336
  } else {
30110
30337
  const emitBand = (base, code) => bandFan === "legacy" ? makeBandVariants(base, code) : makeCollapsedBandVariant(base, code, isDominanceExempt(base));
@@ -30192,6 +30419,15 @@ function generateStyle(palette = "DAY", opts = {}) {
30192
30419
  }
30193
30420
  }
30194
30421
  if (!majorsEmitted) labelLayers.push(...majorWaterLayers);
30422
+ spliceByLabelPriority(
30423
+ labelLayers,
30424
+ buildDredgeNoteLayers(
30425
+ sourceId,
30426
+ palette,
30427
+ opts.depthUnits ?? "meters"
30428
+ ),
30429
+ DRGNOTE_LABEL_PRIORITY
30430
+ );
30195
30431
  layers.push(...labelLayers);
30196
30432
  }
30197
30433
  const symbolSet = opts.symbolSet ?? "s52";