@chrtco/sdk 0.10.26 → 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);
@@ -31312,8 +31384,10 @@ var Chrt = class {
31312
31384
  sanitizeCustomStyleConfig,
31313
31385
  scaleSizeValue,
31314
31386
  scaminToMinZoom,
31387
+ sectorLimitToArcBearing,
31315
31388
  styleModeBand,
31316
31389
  tileTemplate,
31390
+ uncoveredSectorArcs,
31317
31391
  withinBboxFilter,
31318
31392
  worldMinusBboxPolygon
31319
31393
  });