@chrtco/sdk 0.10.25 → 0.10.27

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/CHANGELOG.md CHANGED
@@ -4,6 +4,25 @@ All notable changes to `@chrtco/sdk` will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ### Fixed
8
+ - **Sector-light arcs were drawn on the wrong side of the light.**
9
+ `buildSectorArcGeoJSON()` / `buildSectorArcsFC()` painted each sector at the
10
+ raw `SECTR1`/`SECTR2` bearing measured from the light. S-57 charts those
11
+ limits as bearings FROM SEAWARD (the light as seen from the vessel), so every
12
+ arc — and its limit ticks — landed on the reciprocal side: a Maine light
13
+ charted white 000°–180° / red 180°–000° showed white over the island and red
14
+ over the channel. Both builders now apply the 180° reciprocal (new export
15
+ `sectorLimitToArcBearing()`); bearing labels still print the charted value.
16
+ `SectorInfo.sectorStart`/`sectorEnd` are documented as charted from-seaward
17
+ values and are unchanged.
18
+ - **All-round lights co-located with sector lights were not drawn.**
19
+ `buildSectorsFromFeatures()` skipped any `LIGHTS` feature without
20
+ `SECTR1`/`SECTR2`, so a light like Isle au Haut (`Fl R 4s` all-round plus a
21
+ white 034°–060° sector) showed the white arc and no red at all. Per S-52
22
+ LIGHTS05 the all-round light is now emitted over the arc its sectored
23
+ siblings leave uncovered (new export `uncoveredSectorArcs()`; entries carry
24
+ `SectorInfo.allRound: true`). A lone all-round light still adds no arc.
25
+
7
26
  ### Added
8
27
  - **`StyleGeneratorOptions.labels`** — `"legacy"` (default) or `"placename"`.
9
28
  `"placename"` swaps the 9 orientation classes' 54-layer per-band label fan
package/dist/index.cjs CHANGED
@@ -213,8 +213,10 @@ __export(index_exports, {
213
213
  sanitizeCustomStyleConfig: () => sanitizeCustomStyleConfig,
214
214
  scaleSizeValue: () => scaleSizeValue,
215
215
  scaminToMinZoom: () => scaminToMinZoom,
216
+ sectorLimitToArcBearing: () => sectorLimitToArcBearing,
216
217
  styleModeBand: () => styleModeBand,
217
218
  tileTemplate: () => tileTemplate,
219
+ uncoveredSectorArcs: () => uncoveredSectorArcs,
218
220
  withinBboxFilter: () => withinBboxFilter,
219
221
  worldMinusBboxPolygon: () => worldMinusBboxPolygon
220
222
  });
@@ -3388,13 +3390,54 @@ function buildFlashPattern(decoded) {
3388
3390
  const totalCycle = phases.reduce((sum, p) => sum + p.duration, 0);
3389
3391
  return { phases, totalCycle, notation };
3390
3392
  }
3393
+ function uncoveredSectorArcs(sectors) {
3394
+ const norm = (d) => (d % 360 + 360) % 360;
3395
+ const flat = [];
3396
+ for (const sec of sectors) {
3397
+ const a = norm(sec.sectorStart);
3398
+ let sweep = norm(sec.sectorEnd) - a;
3399
+ if (sweep <= 0) sweep += 360;
3400
+ if (sweep >= 360) return [];
3401
+ const b = a + sweep;
3402
+ if (b <= 360) flat.push([a, b]);
3403
+ else {
3404
+ flat.push([a, 360]);
3405
+ flat.push([0, b - 360]);
3406
+ }
3407
+ }
3408
+ if (flat.length === 0) return [{ start: 0, end: 0 }];
3409
+ flat.sort((x, y) => x[0] - y[0]);
3410
+ const merged = [];
3411
+ for (const [a, b] of flat) {
3412
+ const last = merged[merged.length - 1];
3413
+ if (last && a <= last[1]) last[1] = Math.max(last[1], b);
3414
+ else merged.push([a, b]);
3415
+ }
3416
+ const gaps = [];
3417
+ let cursor = 0;
3418
+ for (const [a, b] of merged) {
3419
+ if (a > cursor) gaps.push([cursor, a]);
3420
+ cursor = Math.max(cursor, b);
3421
+ }
3422
+ if (cursor < 360) gaps.push([cursor, 360]);
3423
+ if (gaps.length >= 2 && gaps[0][0] === 0 && gaps[gaps.length - 1][1] === 360) {
3424
+ const first = gaps.shift();
3425
+ const last = gaps.pop();
3426
+ gaps.push([last[0], first[1]]);
3427
+ }
3428
+ return gaps.filter(([a, b]) => Math.abs(b - a) >= 0.05 || b < a).map(([a, b]) => ({ start: norm(a), end: norm(b) }));
3429
+ }
3391
3430
  function buildSectorsFromFeatures(features) {
3392
3431
  const sectors = [];
3432
+ const allRound = [];
3393
3433
  for (const f of features) {
3394
3434
  const props = f.properties ?? {};
3395
3435
  const s1 = props.SECTR1 != null ? Number(props.SECTR1) : null;
3396
3436
  const s2 = props.SECTR2 != null ? Number(props.SECTR2) : null;
3397
- if (s1 == null || s2 == null || isNaN(s1) || isNaN(s2)) continue;
3437
+ if (s1 == null || s2 == null || isNaN(s1) || isNaN(s2)) {
3438
+ if (props.SECTR1 == null && props.SECTR2 == null && parseCode(props.COLOUR) != null) allRound.push(props);
3439
+ continue;
3440
+ }
3398
3441
  const colourCode = parseCode(props.COLOUR);
3399
3442
  const ce = colourCode ? COLOUR_TABLE[colourCode] : null;
3400
3443
  const litchrCode = parseCode(props.LITCHR);
@@ -3413,15 +3456,44 @@ function buildSectorsFromFeatures(features) {
3413
3456
  nominalRange
3414
3457
  });
3415
3458
  }
3459
+ if (sectors.length > 0 && allRound.length > 0) {
3460
+ const gaps = uncoveredSectorArcs(sectors);
3461
+ for (const props of allRound) {
3462
+ const colourCode = parseCode(props.COLOUR);
3463
+ const ce = colourCode ? COLOUR_TABLE[colourCode] : null;
3464
+ if (!ce) continue;
3465
+ const litchrCode = parseCode(props.LITCHR);
3466
+ const le = litchrCode ? LITCHR_TABLE[litchrCode] : null;
3467
+ const sigper = props.SIGPER != null ? Number(props.SIGPER) : null;
3468
+ const group = parseGroup(props.SIGGRP);
3469
+ const notation = buildNotation(le?.abbr ?? null, [ce.abbr], sigper, group);
3470
+ const valnmr = props.VALNMR != null ? Number(props.VALNMR) : null;
3471
+ const nominalRange = valnmr != null && !isNaN(valnmr) && valnmr > 0 ? valnmr : null;
3472
+ for (const gap of gaps) {
3473
+ sectors.push({
3474
+ sectorStart: gap.start,
3475
+ sectorEnd: gap.end,
3476
+ colour: ce.name,
3477
+ colourHex: ce.hex,
3478
+ notation,
3479
+ nominalRange,
3480
+ allRound: true
3481
+ });
3482
+ }
3483
+ }
3484
+ }
3416
3485
  return sectors;
3417
3486
  }
3487
+ function sectorLimitToArcBearing(limitDegFromSeaward) {
3488
+ return ((limitDegFromSeaward + 180) % 360 + 360) % 360;
3489
+ }
3418
3490
  function buildSectorArcGeoJSON(center, startBearing, endBearing, radiusNm = 0.5, innerRadiusNm = 0, steps = 64) {
3419
3491
  const radiusDeg = radiusNm / 60;
3420
3492
  const innerDeg = innerRadiusNm > 0 ? innerRadiusNm / 60 : 0;
3421
3493
  const [lng, lat] = center;
3422
3494
  const cosLat = Math.cos(lat * Math.PI / 180);
3423
- const s = (startBearing % 360 + 360) % 360;
3424
- const e = (endBearing % 360 + 360) % 360;
3495
+ const s = sectorLimitToArcBearing(startBearing);
3496
+ const e = sectorLimitToArcBearing(endBearing);
3425
3497
  let sweep = e - s;
3426
3498
  if (sweep <= 0) sweep += 360;
3427
3499
  const project = (bearingDeg, rDeg) => {
@@ -3476,7 +3548,7 @@ function buildSectorArcsFC(center, sectors, radiusNm = 0.4, innerRadiusFrac = 0.
3476
3548
  const TICK_INNER_OVERHANG = 0.93;
3477
3549
  for (const sec of sectors) {
3478
3550
  for (const bearing of [sec.sectorStart, sec.sectorEnd]) {
3479
- const rad = bearing * Math.PI / 180;
3551
+ const rad = sectorLimitToArcBearing(bearing) * Math.PI / 180;
3480
3552
  const radiusDeg = radiusNm / 60;
3481
3553
  const innerRadiusDeg = innerNm > 0 ? innerNm / 60 : 0;
3482
3554
  const cosLat = Math.cos(center[1] * Math.PI / 180);
@@ -28637,6 +28709,9 @@ function bandVariantMinzoom(layer, B) {
28637
28709
  if (isExemptOpaqueFill(layer) && FILL_FLOOR_FREE_BANDS.has(B)) {
28638
28710
  return classMinzoom;
28639
28711
  }
28712
+ if (isExemptOpaqueFill(layer)) {
28713
+ return BAND_PRIORITY_RANGES[B].minzoom;
28714
+ }
28640
28715
  return Math.max(classMinzoom, BAND_PRIORITY_RANGES[B].minzoom);
28641
28716
  }
28642
28717
  var GENERALISED_AREA_FILL_CLASSES = /* @__PURE__ */ new Set([
@@ -30037,7 +30112,9 @@ function generateStyle(palette = "DAY", opts = {}) {
30037
30112
  }
30038
30113
  }
30039
30114
  {
30040
- const TERMINAL_FILL_RE = /^s52-(?:LNDARE|DEPARE)-fill-band[456]$/;
30115
+ const TERMINAL_FILL_RE = new RegExp(
30116
+ `^s52-(?:${[...DOMINANCE_EXEMPT_FILL_CLASSES].join("|")})-fill-band[456]$`
30117
+ );
30041
30118
  for (const l of layers) {
30042
30119
  const lay = l;
30043
30120
  if (!TERMINAL_FILL_RE.test(lay.id)) continue;
@@ -30175,7 +30252,7 @@ function generateStyle(palette = "DAY", opts = {}) {
30175
30252
  )) {
30176
30253
  const src = l;
30177
30254
  const landClone = {
30178
- ...l,
30255
+ ...asBaseTan(l),
30179
30256
  id: `${src.id}-overpair`,
30180
30257
  metadata: { ...src.metadata ?? {}, "chrt:role": "charted-land-repaint" }
30181
30258
  };
@@ -30211,6 +30288,21 @@ function generateStyle(palette = "DAY", opts = {}) {
30211
30288
  ...restAbove.slice(pairAt + 1)
30212
30289
  ];
30213
30290
  }
30291
+ {
30292
+ const hsAt = restFinal.findIndex(
30293
+ (l) => l.type === "hillshade"
30294
+ );
30295
+ let lastRepaint = -1;
30296
+ restFinal.forEach((l, i) => {
30297
+ if (/-overpair(-|$)/.test(l.id)) lastRepaint = i;
30298
+ });
30299
+ if (hsAt !== -1 && lastRepaint > hsAt) {
30300
+ const reordered = [...restFinal];
30301
+ const [hs] = reordered.splice(hsAt, 1);
30302
+ reordered.splice(lastRepaint, 0, hs);
30303
+ restFinal = reordered;
30304
+ }
30305
+ }
30214
30306
  return [restFinal[0], ...baseBlock, ...seaare, ...ordered, ...restFinal.slice(1)];
30215
30307
  })() : layers;
30216
30308
  for (const id of [
@@ -31292,8 +31384,10 @@ var Chrt = class {
31292
31384
  sanitizeCustomStyleConfig,
31293
31385
  scaleSizeValue,
31294
31386
  scaminToMinZoom,
31387
+ sectorLimitToArcBearing,
31295
31388
  styleModeBand,
31296
31389
  tileTemplate,
31390
+ uncoveredSectorArcs,
31297
31391
  withinBboxFilter,
31298
31392
  worldMinusBboxPolygon
31299
31393
  });