@chrtco/sdk 0.7.4 → 0.7.6

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
@@ -3248,18 +3248,37 @@ function depareFillExpression(ct = "DAY") {
3248
3248
  resolveColor("DEPDW", ct)
3249
3249
  ];
3250
3250
  }
3251
- function colourCodeCase(cases, fallback) {
3252
- const colourStr = ["to-string", ["coalesce", ["get", "COLOUR"], ["get", "colour"], ""]];
3251
+ function listAttrString(attr) {
3252
+ return ["to-string", ["coalesce", ["get", attr], ["get", attr.toLowerCase()], ""]];
3253
+ }
3254
+ function listAttrCase(attr, cases, fallback) {
3255
+ const attrStr = listAttrString(attr);
3253
3256
  const expr = ["case"];
3254
3257
  for (const [code, value] of cases) {
3255
- expr.push([">=", ["index-of", `"${code}"`, colourStr], 0], value);
3258
+ expr.push([">=", ["index-of", `"${code}"`, attrStr], 0], value);
3259
+ }
3260
+ for (const [code, value] of cases) {
3261
+ expr.push(["==", attrStr, code], value);
3256
3262
  }
3257
3263
  for (const [code, value] of cases) {
3258
- expr.push(["==", colourStr, code], value);
3264
+ expr.push(["==", attrStr, `[${code}]`], value);
3259
3265
  }
3260
3266
  expr.push(fallback);
3261
3267
  return expr;
3262
3268
  }
3269
+ function listAttrHas(attr, codes) {
3270
+ const attrStr = listAttrString(attr);
3271
+ const clauses = [];
3272
+ for (const code of codes) {
3273
+ clauses.push([">=", ["index-of", `"${code}"`, attrStr], 0]);
3274
+ clauses.push(["==", attrStr, code]);
3275
+ clauses.push(["==", attrStr, `[${code}]`]);
3276
+ }
3277
+ return ["any", ...clauses];
3278
+ }
3279
+ function colourCodeCase(cases, fallback) {
3280
+ return listAttrCase("COLOUR", cases, fallback);
3281
+ }
3263
3282
  function lightColor(props, ct = "DAY") {
3264
3283
  const raw = props.COLOUR ?? props.colour;
3265
3284
  const code = Array.isArray(raw) ? Number(raw[0]) : typeof raw === "string" && raw.startsWith("[") ? Number(JSON.parse(raw)[0]) : Number(raw ?? 0);
@@ -5298,7 +5317,7 @@ var NAUTICAL_SYMBOLS = [
5298
5317
  description: "Church spire or steeple, conspicuous from seaward. Useful for visual fixing.",
5299
5318
  s52PlRef: "LNDMRK17",
5300
5319
  attributeKey: "CATLMK",
5301
- attributeValue: 17,
5320
+ attributeValue: 20,
5302
5321
  renderSize: "32x32",
5303
5322
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Spire, building, position circle" }],
5304
5323
  lineStyles: [{ element: "Building outline", width: 0.8, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5315,7 +5334,7 @@ var NAUTICAL_SYMBOLS = [
5315
5334
  description: "Tower conspicuous from seaward. General-purpose landmark symbol.",
5316
5335
  s52PlRef: "LNDMRK20",
5317
5336
  attributeKey: "CATLMK",
5318
- attributeValue: 20,
5337
+ attributeValue: 17,
5319
5338
  renderSize: "32x32",
5320
5339
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Tower body, cap, position circle" }],
5321
5340
  lineStyles: [{ element: "Tower outline", width: 0.8, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5332,7 +5351,7 @@ var NAUTICAL_SYMBOLS = [
5332
5351
  description: "Industrial chimney or smokestack visible from the sea.",
5333
5352
  s52PlRef: "LNDMRK01",
5334
5353
  attributeKey: "CATLMK",
5335
- attributeValue: 1,
5354
+ attributeValue: 3,
5336
5355
  renderSize: "32x32",
5337
5356
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Chimney body, smoke, position circle" }],
5338
5357
  lineStyles: [{ element: "Body outline", width: 0.8, color: "#000000" }, { element: "Smoke plume", width: 1, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5349,7 +5368,7 @@ var NAUTICAL_SYMBOLS = [
5349
5368
  description: "Flagstaff or flagpole conspicuous from seaward.",
5350
5369
  s52PlRef: "LNDMRK03",
5351
5370
  attributeKey: "CATLMK",
5352
- attributeValue: 3,
5371
+ attributeValue: 5,
5353
5372
  renderSize: "32x32",
5354
5373
  colors: [
5355
5374
  { token: "CHBLK", hex: "#000000", usage: "Pole and position circle" },
@@ -5369,7 +5388,7 @@ var NAUTICAL_SYMBOLS = [
5369
5388
  description: "Monument or memorial conspicuous from the sea.",
5370
5389
  s52PlRef: "LNDMRK05",
5371
5390
  attributeKey: "CATLMK",
5372
- attributeValue: 5,
5391
+ attributeValue: 9,
5373
5392
  renderSize: "32x32",
5374
5393
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Monument body, base, position circle" }],
5375
5394
  lineStyles: [{ element: "Body outline", width: 0.8, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5384,9 +5403,9 @@ var NAUTICAL_SYMBOLS = [
5384
5403
  getSvg: landmarkWindmill,
5385
5404
  group: "landmarks",
5386
5405
  description: "Traditional windmill with rotating vanes. Conspicuous landmark.",
5387
- s52PlRef: "LNDMRK21",
5406
+ s52PlRef: "WNDMIL12",
5388
5407
  attributeKey: "CATLMK",
5389
- attributeValue: 7,
5408
+ attributeValue: 18,
5390
5409
  renderSize: "32x32",
5391
5410
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Tower, vanes, hub, position circle" }],
5392
5411
  lineStyles: [{ element: "Tower", width: 1.5, color: "#000000" }, { element: "Vane arms", width: 1.2, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5401,9 +5420,9 @@ var NAUTICAL_SYMBOLS = [
5401
5420
  getSvg: landmarkWindTurbine,
5402
5421
  group: "landmarks",
5403
5422
  description: "Modern 3-blade wind turbine. Increasingly common offshore landmark.",
5404
- s52PlRef: "LNDMRK22",
5423
+ s52PlRef: "WIMCON11",
5405
5424
  attributeKey: "CATLMK",
5406
- attributeValue: 26,
5425
+ attributeValue: 19,
5407
5426
  renderSize: "32x32",
5408
5427
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Tower, blades, hub, position circle" }],
5409
5428
  lineStyles: [{ element: "Tower", width: 1.5, color: "#000000" }, { element: "Blades", width: 1.2, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5420,7 +5439,7 @@ var NAUTICAL_SYMBOLS = [
5420
5439
  description: "Dome or cupola (e.g., observatory, mosque) visible from the sea.",
5421
5440
  s52PlRef: "LNDMRK03",
5422
5441
  attributeKey: "CATLMK",
5423
- attributeValue: 6,
5442
+ attributeValue: 15,
5424
5443
  renderSize: "32x32",
5425
5444
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Dome, base, position circle" }],
5426
5445
  lineStyles: [{ element: "Base outline", width: 0.8, color: "#000000" }, { element: "Dome curve", width: 0.8, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5437,7 +5456,7 @@ var NAUTICAL_SYMBOLS = [
5437
5456
  description: "Radar scanner antenna conspicuous from seaward.",
5438
5457
  s52PlRef: "LNDMRK12",
5439
5458
  attributeKey: "CATLMK",
5440
- attributeValue: 9,
5459
+ attributeValue: 16,
5441
5460
  renderSize: "32x32",
5442
5461
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Tower, scanner dish, position circle" }],
5443
5462
  lineStyles: [{ element: "Tower", width: 1.5, color: "#000000" }, { element: "Scanner frame", width: 1.5, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5454,7 +5473,7 @@ var NAUTICAL_SYMBOLS = [
5454
5473
  description: "Satellite dish or parabolic antenna visible from the sea.",
5455
5474
  s52PlRef: "LNDMRK12",
5456
5475
  attributeKey: "CATLMK",
5457
- attributeValue: 12,
5476
+ attributeValue: 4,
5458
5477
  renderSize: "32x32",
5459
5478
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Tower, dish, position circle" }],
5460
5479
  lineStyles: [{ element: "Tower", width: 1.5, color: "#000000" }, { element: "Dish curve", width: 1.5, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5469,9 +5488,9 @@ var NAUTICAL_SYMBOLS = [
5469
5488
  getSvg: landmarkWaterTower,
5470
5489
  group: "landmarks",
5471
5490
  description: "Water tower. Elevated tank on supporting structure, conspicuous landmark.",
5472
- s52PlRef: "LNDMRK19",
5473
- attributeKey: "CATLMK",
5474
- attributeValue: 15,
5491
+ s52PlRef: "TOWERS12",
5492
+ attributeKey: "CATSIL",
5493
+ attributeValue: 4,
5475
5494
  renderSize: "32x32",
5476
5495
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Tank, legs, position circle" }],
5477
5496
  lineStyles: [{ element: "Leg outlines", width: 1.2, color: "#000000" }, { element: "Tank outline", width: 0.8, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5488,7 +5507,7 @@ var NAUTICAL_SYMBOLS = [
5488
5507
  description: "Radio or television transmission mast with horizontal cross-arms.",
5489
5508
  s52PlRef: "LNDMRK10",
5490
5509
  attributeKey: "CATLMK",
5491
- attributeValue: 8,
5510
+ attributeValue: 7,
5492
5511
  renderSize: "32x32",
5493
5512
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Mast, cross-arms, top, position circle" }],
5494
5513
  lineStyles: [{ element: "Main mast", width: 1.5, color: "#000000" }, { element: "Cross-arms", width: 0.8, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5505,7 +5524,7 @@ var NAUTICAL_SYMBOLS = [
5505
5524
  description: "Religious cross or calvary conspicuous from seaward.",
5506
5525
  s52PlRef: "LNDMRK05",
5507
5526
  attributeKey: "CATLMK",
5508
- attributeValue: 10,
5527
+ attributeValue: 14,
5509
5528
  renderSize: "32x32",
5510
5529
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Cross arms and position circle" }],
5511
5530
  lineStyles: [{ element: "Vertical arm", width: 2, color: "#000000" }, { element: "Horizontal arm", width: 2, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5522,7 +5541,7 @@ var NAUTICAL_SYMBOLS = [
5522
5541
  description: "General mast (not radio/TV). Pole with spherical top.",
5523
5542
  s52PlRef: "LNDMRK10",
5524
5543
  attributeKey: "CATLMK",
5525
- attributeValue: 14,
5544
+ attributeValue: 7,
5526
5545
  renderSize: "32x32",
5527
5546
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Mast pole, top ball, position circle" }],
5528
5547
  lineStyles: [{ element: "Mast pole", width: 2, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5537,9 +5556,9 @@ var NAUTICAL_SYMBOLS = [
5537
5556
  getSvg: landmarkSilo,
5538
5557
  group: "landmarks",
5539
5558
  description: "Silo, grain elevator, or storage tank conspicuous from the sea.",
5540
- s52PlRef: "LNDMRK17",
5541
- attributeKey: "CATLMK",
5542
- attributeValue: 33,
5559
+ s52PlRef: "SILBUI01",
5560
+ attributeKey: "CATSIL",
5561
+ attributeValue: 1,
5543
5562
  renderSize: "32x32",
5544
5563
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Body, dome top, position circle" }],
5545
5564
  lineStyles: [{ element: "Body outline", width: 0.8, color: "#000000" }, { element: "Dome curve", width: 0.8, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5941,27 +5960,18 @@ function getIconExpression(layerName) {
5941
5960
  ];
5942
5961
  }
5943
5962
  if (layerName === "LNDMRK") {
5944
- return [
5945
- "match",
5946
- ["coalesce", ["get", "CATLMK"], ["get", "catlmk"], 0],
5947
- 1,
5948
- "CAIRNS11",
5949
- // Cairn
5950
- 17,
5951
- "BCNTOW01",
5952
- // Tower
5953
- 18,
5954
- "LNDMRK18",
5963
+ return listAttrCase("CATLMK", [
5964
+ ["18", "LNDMRK18"],
5955
5965
  // Windmill (4-vane)
5956
- 19,
5957
- "LNDMRK19",
5966
+ ["19", "LNDMRK19"],
5958
5967
  // Wind motor / turbine (3-blade)
5959
- 20,
5960
- "BCNTOW01",
5968
+ ["1", "CAIRNS11"],
5969
+ // Cairn
5970
+ ["17", "BCNTOW01"],
5971
+ // Tower
5972
+ ["20", "BCNTOW01"]
5961
5973
  // Spire → tower
5962
- "BCNTOW01"
5963
- // Default: tower
5964
- ];
5974
+ ], "BCNTOW01");
5965
5975
  }
5966
5976
  if (layerName === "DAYMAR") {
5967
5977
  return [
@@ -6101,18 +6111,12 @@ function getIconExpression(layerName) {
6101
6111
  ];
6102
6112
  }
6103
6113
  if (layerName === "LIGHTS") {
6104
- return [
6105
- "match",
6106
- ["coalesce", ["get", "CATLIT"], ["get", "catlit"], 0],
6107
- 8,
6108
- "LIGHTS82",
6109
- // Aero light
6110
- 9,
6111
- "LIGHTS82",
6112
- // Air obstruction light
6113
- "LIGHTS11"
6114
- // Default: major light
6115
- ];
6114
+ return listAttrCase("CATLIT", [
6115
+ ["8", "LIGHTS82"],
6116
+ // Floodlight
6117
+ ["9", "LIGHTS82"]
6118
+ // Strip light
6119
+ ], "LIGHTS11");
6116
6120
  }
6117
6121
  return SYMBOL_MAP[layerName] ?? "";
6118
6122
  }
@@ -6463,30 +6467,141 @@ function buildSeabedLabelExpression() {
6463
6467
  const natsurJoined = ["slice", natsurParts, 1];
6464
6468
  return ["concat", natquaPrefix, natsurJoined];
6465
6469
  }
6470
+ function underseaFeatureNameMatch(objnam) {
6471
+ const terms = [
6472
+ "Canyon",
6473
+ "Seamount",
6474
+ "Ridge",
6475
+ "Basin",
6476
+ "Swell",
6477
+ "Knoll",
6478
+ "Shoal",
6479
+ "Ledge",
6480
+ "Bank",
6481
+ "Valley",
6482
+ "Spur",
6483
+ "Escarpment"
6484
+ ];
6485
+ return ["any", ...terms.map((t) => [">=", ["index-of", t, objnam], 0])];
6486
+ }
6487
+ var CATSEA_RELIEF_CODES = [
6488
+ 2,
6489
+ // gat
6490
+ 3,
6491
+ // bank
6492
+ 4,
6493
+ // deep
6494
+ 6,
6495
+ // trench
6496
+ 7,
6497
+ // basin
6498
+ 8,
6499
+ // mud flats
6500
+ 9,
6501
+ // reef
6502
+ 10,
6503
+ // ledge
6504
+ 11,
6505
+ // canyon
6506
+ 13,
6507
+ // shoal
6508
+ 14,
6509
+ // knoll
6510
+ 15,
6511
+ // ridge
6512
+ 16,
6513
+ // seamount
6514
+ 17,
6515
+ // pinnacle
6516
+ 18,
6517
+ // abyssal plain
6518
+ 19,
6519
+ // plateau
6520
+ 20,
6521
+ // spur
6522
+ 21,
6523
+ // shelf
6524
+ 22,
6525
+ // trough
6526
+ 23,
6527
+ // saddle
6528
+ 24,
6529
+ // abyssal hills
6530
+ 25,
6531
+ // apron
6532
+ 26,
6533
+ // archipelagic apron
6534
+ 27,
6535
+ // borderland
6536
+ 28,
6537
+ // continental margin
6538
+ 29,
6539
+ // continental rise
6540
+ 30,
6541
+ // escarpment
6542
+ 31,
6543
+ // fan
6544
+ 32,
6545
+ // fracture zone
6546
+ 33,
6547
+ // gap
6548
+ 34,
6549
+ // guyot
6550
+ 35,
6551
+ // hill
6552
+ 36,
6553
+ // hole
6554
+ 37,
6555
+ // hollow
6556
+ 39,
6557
+ // levee
6558
+ 40,
6559
+ // median valley
6560
+ 41,
6561
+ // moat
6562
+ 42,
6563
+ // mountains
6564
+ 43,
6565
+ // peak
6566
+ 44,
6567
+ // province
6568
+ 45,
6569
+ // rise
6570
+ 47,
6571
+ // seamount chain
6572
+ 48,
6573
+ // shelf-edge
6574
+ 49,
6575
+ // sill
6576
+ 50,
6577
+ // slope
6578
+ 51,
6579
+ // terrace
6580
+ 52
6581
+ // valley
6582
+ ];
6466
6583
  function buildSeaAreaTierExpression() {
6467
- const catsea = ["coalesce", ["get", "CATSEA"], ["get", "catsea"], 0];
6584
+ const catsea = ["to-number", ["coalesce", ["get", "CATSEA"], ["get", "catsea"], 0]];
6468
6585
  const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6469
6586
  return [
6470
6587
  "case",
6471
- // Tier 1 — Ocean (CATSEA=31 OR OBJNAM contains "Ocean", excluding
6472
- // anchorage-area-style compound names which we filter on later).
6473
- ["==", ["to-number", catsea], 31],
6474
- 1,
6475
- ["any", ["==", ["to-number", catsea], 29], ["==", ["to-number", catsea], 30]],
6476
- 2,
6477
- [
6478
- "any",
6479
- ["==", ["to-number", catsea], 4],
6480
- ["==", ["to-number", catsea], 11],
6481
- ["==", ["to-number", catsea], 27],
6482
- ["==", ["to-number", catsea], 28]
6483
- ],
6484
- 3,
6485
- // Tier 4 — Anchorage detail (must be checked before OBJNAM-substring
6486
- // promotion so "Boston Harbor, MA …" anchorages don't bubble up).
6588
+ // Tier 4 — Anchorage detail (before all promotions so "Boston Harbor,
6589
+ // MA …" anchorage compound names don't bubble up).
6487
6590
  [">=", ["index-of", "Anchorage Area", objnam], 0],
6488
6591
  4,
6489
- // OBJNAM-substring promotion shims (run AFTER tier 4 demotion):
6592
+ // Tier 5 — undersea relief, by authoritative CATSEA code first
6593
+ // (Georges Bank=3, Georges Shoal=13, …), then by name for
6594
+ // uncategorised features. Before promotion shims; see
6595
+ // underseaFeatureNameMatch.
6596
+ ["match", catsea, CATSEA_RELIEF_CODES, true, false],
6597
+ 5,
6598
+ underseaFeatureNameMatch(objnam),
6599
+ 5,
6600
+ // Tier 3 by authoritative CATSEA: bay / narrows.
6601
+ ["match", catsea, [5, 12], true, false],
6602
+ 3,
6603
+ // OBJNAM promotion shims (CATSEA has no ocean/sea/gulf/sound/strait
6604
+ // codes, so names are the only signal for the headline tiers):
6490
6605
  [">=", ["index-of", "Atlantic Ocean", objnam], 0],
6491
6606
  1,
6492
6607
  [">=", ["index-of", "Pacific Ocean", objnam], 0],
@@ -6495,9 +6610,6 @@ function buildSeaAreaTierExpression() {
6495
6610
  1,
6496
6611
  [">=", ["index-of", "Gulf of", objnam], 0],
6497
6612
  2,
6498
- // Bay/Sound/Sea/Strait/Channel as a tier-3 shim (matches "Massachusetts
6499
- // Bay", "Buzzards Bay", "Long Island Sound", "Block Island Sound",
6500
- // "Vineyard Sound", "Cape Cod Bay", etc. without needing CATSEA).
6501
6613
  [">=", ["index-of", " Bay", objnam], 0],
6502
6614
  3,
6503
6615
  [">=", ["index-of", " Sound", objnam], 0],
@@ -6531,6 +6643,11 @@ function buildLandRegionTierExpression() {
6531
6643
  const nameLen = ["length", objnam];
6532
6644
  return [
6533
6645
  "case",
6646
+ // NOAA also encodes undersea relief names (e.g. "Block Canyon", 12
6647
+ // chars, no comma) as LNDRGN — without this demotion they'd qualify
6648
+ // as tier-1 "major region" and paint at overview zoom.
6649
+ underseaFeatureNameMatch(objnam),
6650
+ 2,
6534
6651
  ["all", ["<=", nameLen, 12], ["==", ["index-of", ",", objnam], -1]],
6535
6652
  1,
6536
6653
  2
@@ -6612,6 +6729,36 @@ function orientationLabelLayout(extras = {}) {
6612
6729
  ...extras
6613
6730
  };
6614
6731
  }
6732
+ function tieredSizeCurve(tierExpr, stops) {
6733
+ const withHolds = [];
6734
+ for (let i = 0; i < stops.length; i++) {
6735
+ if (i > 0) {
6736
+ const [pz, pv] = stops[i - 1];
6737
+ const [nz, nv] = stops[i];
6738
+ if (pv.some((v, t) => v === 0 && nv[t] > 0)) {
6739
+ const hz = Math.round((nz - 0.2) * 10) / 10;
6740
+ if (hz > pz) {
6741
+ const f = (hz - pz) / (nz - pz);
6742
+ withHolds.push([
6743
+ hz,
6744
+ pv.map(
6745
+ (v, t) => v === 0 && nv[t] > 0 ? 0 : Math.round((v + (nv[t] - v) * f) * 10) / 10
6746
+ )
6747
+ ]);
6748
+ }
6749
+ }
6750
+ }
6751
+ withHolds.push(stops[i]);
6752
+ }
6753
+ const expr = ["interpolate", ["linear"], ["zoom"]];
6754
+ for (const [z, vals] of withHolds) {
6755
+ const m = ["match", tierExpr];
6756
+ for (let t = 0; t < vals.length - 1; t++) m.push(t + 1, vals[t]);
6757
+ m.push(vals[vals.length - 1]);
6758
+ expr.push(z, m);
6759
+ }
6760
+ return expr;
6761
+ }
6615
6762
  var ORIENTATION_HALO_WIDTH = [
6616
6763
  "interpolate",
6617
6764
  ["linear"],
@@ -6661,6 +6808,11 @@ var LABEL_BEHAVIOR = {
6661
6808
  function getLabelBehavior(layerName) {
6662
6809
  return LABEL_BEHAVIOR[layerName] ?? "feature-band-gated";
6663
6810
  }
6811
+ var LABEL_ANCHOR_FILTER = [
6812
+ "!=",
6813
+ ["get", "_label_anchor"],
6814
+ false
6815
+ ];
6664
6816
  function getLabelConfig(layerName, opts = {}) {
6665
6817
  const ct = opts.colorTable ?? "DAY";
6666
6818
  switch (layerName) {
@@ -6741,35 +6893,23 @@ function getLabelConfig(layerName, opts = {}) {
6741
6893
  }
6742
6894
  case "LNDRGN": {
6743
6895
  const tierExpr = buildLandRegionTierExpression();
6744
- const sizeAt = (t1, t2) => [
6745
- "match",
6746
- tierExpr,
6747
- 1,
6748
- t1,
6749
- 2,
6750
- t2,
6751
- t2
6752
- ];
6753
6896
  return {
6754
6897
  type: "symbol",
6755
6898
  textField: ["get", "OBJNAM"],
6756
- textSize: [
6757
- "interpolate",
6758
- ["linear"],
6759
- ["zoom"],
6760
- 5,
6761
- sizeAt(9, 0),
6762
- 7,
6763
- sizeAt(11, 0),
6764
- 9,
6765
- sizeAt(12, 0),
6766
- 11,
6767
- sizeAt(12, 9),
6768
- 13,
6769
- sizeAt(11, 11),
6770
- 16,
6771
- sizeAt(10, 10)
6772
- ],
6899
+ // Tier-1 entry moved from z5 to z7: undersea-relief names without a
6900
+ // generic keyword ("Three Dory", "Outer Fall") can't be demoted by
6901
+ // pattern, so the overview/general bands stay clean by holding ALL
6902
+ // LNDRGN labels until coastal zoom. The Carto basemap already labels
6903
+ // major land regions at z5-z6. Columns: t1 major region, t2 local
6904
+ // point (also the default).
6905
+ textSize: tieredSizeCurve(tierExpr, [
6906
+ [5, [0, 0]],
6907
+ [7, [11, 0]],
6908
+ [9, [12, 0]],
6909
+ [11, [12, 9]],
6910
+ [13, [11, 11]],
6911
+ [16, [10, 10]]
6912
+ ]),
6773
6913
  textColor: DARK_BG,
6774
6914
  textHaloColor: COLOR.LAND_REGION,
6775
6915
  textHaloWidth: ORIENTATION_HALO_WIDTH,
@@ -6909,19 +7049,6 @@ function getLabelConfig(layerName, opts = {}) {
6909
7049
  }
6910
7050
  case "SEAARE": {
6911
7051
  const tierExpr = buildSeaAreaTierExpression();
6912
- const sizeAt = (t1, t2, t3, t4, def) => [
6913
- "match",
6914
- tierExpr,
6915
- 1,
6916
- t1,
6917
- 2,
6918
- t2,
6919
- 3,
6920
- t3,
6921
- 4,
6922
- t4,
6923
- def
6924
- ];
6925
7052
  return {
6926
7053
  type: "symbol",
6927
7054
  textField: ["get", "OBJNAM"],
@@ -6930,26 +7057,17 @@ function getLabelConfig(layerName, opts = {}) {
6930
7057
  // it floods the overview with hundreds of small italic labels that
6931
7058
  // crowd out the headline Ocean/Sea/Gulf names. It becomes visible only
6932
7059
  // at approach scale (z9) when the user has zoomed in enough that
6933
- // individual banks are useful context.
6934
- textSize: [
6935
- "interpolate",
6936
- ["linear"],
6937
- ["zoom"],
6938
- 3,
6939
- sizeAt(14, 8, 0, 0, 0),
6940
- 5,
6941
- sizeAt(16, 13, 9, 0, 0),
6942
- 7,
6943
- sizeAt(14, 14, 12, 0, 0),
6944
- 9,
6945
- sizeAt(12, 13, 14, 0, 9),
6946
- 11,
6947
- sizeAt(10, 12, 13, 0, 11),
6948
- 13,
6949
- sizeAt(9, 11, 12, 10, 11),
6950
- 16,
6951
- sizeAt(9, 10, 11, 11, 10)
6952
- ],
7060
+ // individual banks are useful context. Columns: t1 ocean, t2 sea/gulf,
7061
+ // t3 bay/sound, t4 anchorage, default.
7062
+ textSize: tieredSizeCurve(tierExpr, [
7063
+ [3, [14, 8, 0, 0, 0]],
7064
+ [5, [16, 13, 9, 0, 0]],
7065
+ [7, [14, 14, 12, 0, 0]],
7066
+ [9, [12, 13, 14, 0, 9]],
7067
+ [11, [10, 12, 13, 0, 11]],
7068
+ [13, [9, 11, 12, 10, 11]],
7069
+ [16, [9, 10, 11, 11, 10]]
7070
+ ]),
6953
7071
  textColor: DARK_BG,
6954
7072
  textHaloColor: COLOR.WATER_DEEP,
6955
7073
  textHaloWidth: ORIENTATION_HALO_WIDTH,
@@ -6958,39 +7076,38 @@ function getLabelConfig(layerName, opts = {}) {
6958
7076
  minzoom: 5,
6959
7077
  layout: orientationLabelLayout({
6960
7078
  "text-letter-spacing": 0.15,
6961
- "text-max-width": 9
7079
+ "text-max-width": 9,
7080
+ // SEAARE is always-paint, unlike the other orientation classes.
7081
+ // MapLibre places symbols topmost-layer-first, so with
7082
+ // allow-overlap:false the dense sounding field (drawn above)
7083
+ // collision-starves big-water names — observed on staging at z8:
7084
+ // only 2 SEAARE labels survived viewport-wide, with Cape Cod Bay
7085
+ // and Gulf of Maine dropped entirely. Always-paint is safe for
7086
+ // this class because (a) the dominance composition in
7087
+ // fanOrientationLabelByCellBand suppresses cross-band duplicate
7088
+ // copies, and (b) tier sizing zeroes relief/anchorage names at
7089
+ // the zooms where density is a problem. Rivers/lakes/land keep
7090
+ // collision-dedup — their within-band multi-polygon stacking was
7091
+ // the original complaint, and they lack big-water headline value.
7092
+ "text-allow-overlap": true,
7093
+ "text-ignore-placement": true,
7094
+ "text-optional": false,
7095
+ "text-padding": 2
6962
7096
  })
6963
7097
  };
6964
7098
  }
6965
7099
  case "LAKARE": {
6966
7100
  const tierExpr = buildLakeTierExpression();
6967
- const sizeAt = (t1, t2) => [
6968
- "match",
6969
- tierExpr,
6970
- 1,
6971
- t1,
6972
- 2,
6973
- t2,
6974
- t2
6975
- ];
6976
7101
  return {
6977
7102
  type: "symbol",
6978
7103
  textField: ["get", "OBJNAM"],
6979
- textSize: [
6980
- "interpolate",
6981
- ["linear"],
6982
- ["zoom"],
6983
- 7,
6984
- sizeAt(9, 0),
6985
- 9,
6986
- sizeAt(11, 0),
6987
- 11,
6988
- sizeAt(11, 9),
6989
- 13,
6990
- sizeAt(10, 11),
6991
- 16,
6992
- sizeAt(9, 10)
6993
- ],
7104
+ textSize: tieredSizeCurve(tierExpr, [
7105
+ [7, [9, 0]],
7106
+ [9, [11, 0]],
7107
+ [11, [11, 9]],
7108
+ [13, [10, 11]],
7109
+ [16, [9, 10]]
7110
+ ]),
6994
7111
  textColor: DARK_BG,
6995
7112
  textHaloColor: COLOR.WATER,
6996
7113
  textHaloWidth: ORIENTATION_HALO_WIDTH,
@@ -7002,33 +7119,16 @@ function getLabelConfig(layerName, opts = {}) {
7002
7119
  }
7003
7120
  case "RIVERS": {
7004
7121
  const tierExpr = buildRiverTierExpression();
7005
- const sizeAt = (t1, t2) => [
7006
- "match",
7007
- tierExpr,
7008
- 1,
7009
- t1,
7010
- 2,
7011
- t2,
7012
- t2
7013
- ];
7014
7122
  return {
7015
7123
  type: "symbol",
7016
7124
  textField: ["get", "OBJNAM"],
7017
- textSize: [
7018
- "interpolate",
7019
- ["linear"],
7020
- ["zoom"],
7021
- 7,
7022
- sizeAt(9, 0),
7023
- 9,
7024
- sizeAt(11, 0),
7025
- 11,
7026
- sizeAt(11, 9),
7027
- 13,
7028
- sizeAt(10, 10),
7029
- 16,
7030
- sizeAt(9, 10)
7031
- ],
7125
+ textSize: tieredSizeCurve(tierExpr, [
7126
+ [7, [9, 0]],
7127
+ [9, [11, 0]],
7128
+ [11, [11, 9]],
7129
+ [13, [10, 10]],
7130
+ [16, [9, 10]]
7131
+ ]),
7032
7132
  textColor: DARK_BG,
7033
7133
  textHaloColor: COLOR.WATER,
7034
7134
  textHaloWidth: ORIENTATION_HALO_WIDTH,
@@ -7063,35 +7163,17 @@ function getLabelConfig(layerName, opts = {}) {
7063
7163
  };
7064
7164
  case "LNDARE": {
7065
7165
  const tierExpr = buildLandAreaTierExpression();
7066
- const sizeAt = (t1, t2) => [
7067
- "match",
7068
- tierExpr,
7069
- 1,
7070
- t1,
7071
- 2,
7072
- t2,
7073
- t2
7074
- ];
7075
7166
  return {
7076
7167
  type: "symbol",
7077
7168
  textField: ["get", "OBJNAM"],
7078
- textSize: [
7079
- "interpolate",
7080
- ["linear"],
7081
- ["zoom"],
7082
- 5,
7083
- sizeAt(8, 0),
7084
- 7,
7085
- sizeAt(10, 0),
7086
- 9,
7087
- sizeAt(11, 0),
7088
- 11,
7089
- sizeAt(11, 9),
7090
- 13,
7091
- sizeAt(10, 10),
7092
- 16,
7093
- sizeAt(9, 10)
7094
- ],
7169
+ textSize: tieredSizeCurve(tierExpr, [
7170
+ [5, [8, 0]],
7171
+ [7, [10, 0]],
7172
+ [9, [11, 0]],
7173
+ [11, [11, 9]],
7174
+ [13, [10, 10]],
7175
+ [16, [9, 10]]
7176
+ ]),
7095
7177
  textColor: DARK_BG,
7096
7178
  textHaloColor: COLOR.LAND_NATURAL,
7097
7179
  textHaloWidth: ORIENTATION_HALO_WIDTH,
@@ -7103,39 +7185,18 @@ function getLabelConfig(layerName, opts = {}) {
7103
7185
  }
7104
7186
  case "BUAARE": {
7105
7187
  const tierExpr = buildBuiltUpAreaTierExpression();
7106
- const sizeAt = (t1, t2, t3) => [
7107
- "match",
7108
- tierExpr,
7109
- 1,
7110
- t1,
7111
- 2,
7112
- t2,
7113
- 3,
7114
- t3,
7115
- t3
7116
- ];
7117
7188
  return {
7118
7189
  type: "symbol",
7119
7190
  textField: ["get", "OBJNAM"],
7120
- textSize: [
7121
- "interpolate",
7122
- ["linear"],
7123
- ["zoom"],
7124
- 3,
7125
- sizeAt(11, 0, 0),
7126
- 5,
7127
- sizeAt(13, 0, 0),
7128
- 7,
7129
- sizeAt(13, 9, 0),
7130
- 9,
7131
- sizeAt(12, 12, 0),
7132
- 11,
7133
- sizeAt(11, 12, 10),
7134
- 13,
7135
- sizeAt(10, 11, 12),
7136
- 16,
7137
- sizeAt(10, 10, 11)
7138
- ],
7191
+ textSize: tieredSizeCurve(tierExpr, [
7192
+ [3, [11, 0, 0]],
7193
+ [5, [13, 0, 0]],
7194
+ [7, [13, 9, 0]],
7195
+ [9, [12, 12, 0]],
7196
+ [11, [11, 12, 10]],
7197
+ [13, [10, 11, 12]],
7198
+ [16, [10, 10, 11]]
7199
+ ]),
7139
7200
  textColor: DARK_BG,
7140
7201
  textHaloColor: COLOR.CITY,
7141
7202
  textHaloWidth: ORIENTATION_HALO_WIDTH,
@@ -7216,33 +7277,16 @@ function getLabelConfig(layerName, opts = {}) {
7216
7277
  };
7217
7278
  case "AIRARE": {
7218
7279
  const tierExpr = buildAirportTierExpression();
7219
- const sizeAt = (t1, t2) => [
7220
- "match",
7221
- tierExpr,
7222
- 1,
7223
- t1,
7224
- 2,
7225
- t2,
7226
- t2
7227
- ];
7228
7280
  return {
7229
7281
  type: "symbol",
7230
7282
  textField: ["get", "OBJNAM"],
7231
- textSize: [
7232
- "interpolate",
7233
- ["linear"],
7234
- ["zoom"],
7235
- 7,
7236
- sizeAt(9, 0),
7237
- 9,
7238
- sizeAt(11, 0),
7239
- 11,
7240
- sizeAt(11, 9),
7241
- 13,
7242
- sizeAt(10, 10),
7243
- 16,
7244
- sizeAt(9, 9)
7245
- ],
7283
+ textSize: tieredSizeCurve(tierExpr, [
7284
+ [7, [9, 0]],
7285
+ [9, [11, 0]],
7286
+ [11, [11, 9]],
7287
+ [13, [10, 10]],
7288
+ [16, [9, 9]]
7289
+ ]),
7246
7290
  textColor: DARK_BG,
7247
7291
  textHaloColor: COLOR.AIRPORT,
7248
7292
  textHaloWidth: ORIENTATION_HALO_WIDTH,
@@ -7560,6 +7604,18 @@ function preprocessSoundings(fc) {
7560
7604
  }
7561
7605
  return { type: "FeatureCollection", features };
7562
7606
  }
7607
+ function firstListCode(raw) {
7608
+ if (raw == null) return NaN;
7609
+ if (Array.isArray(raw)) return Number(raw[0]);
7610
+ if (typeof raw === "string" && raw.startsWith("[")) {
7611
+ try {
7612
+ const arr = JSON.parse(raw);
7613
+ if (Array.isArray(arr)) return Number(arr[0]);
7614
+ } catch {
7615
+ }
7616
+ }
7617
+ return Number(raw);
7618
+ }
7563
7619
  function preprocessLights(fc) {
7564
7620
  return {
7565
7621
  type: "FeatureCollection",
@@ -7568,7 +7624,7 @@ function preprocessLights(fc) {
7568
7624
  const parts = [];
7569
7625
  const litchr = Number(props.LITCHR);
7570
7626
  if (litchr && LITCHR_ABBR[litchr]) parts.push(LITCHR_ABBR[litchr]);
7571
- const colour = Number(props.COLOUR);
7627
+ const colour = firstListCode(props.COLOUR);
7572
7628
  if (colour && COLOUR_ABBR[colour]) parts.push(COLOUR_ABBR[colour]);
7573
7629
  const sigper = Number(props.SIGPER);
7574
7630
  if (sigper && sigper > 0) parts.push(sigper + "s");
@@ -7642,7 +7698,7 @@ function preprocessNavAids(fc) {
7642
7698
  const match = objnam.match(/(\d+[A-Z]?)\s*$/);
7643
7699
  if (!match) return f;
7644
7700
  const designation = match[1];
7645
- const colourCode = Number(props.COLOUR);
7701
+ const colourCode = firstListCode(props.COLOUR);
7646
7702
  const prefix = NAV_COLOUR_PREFIX[colourCode];
7647
7703
  const label = prefix ? `${prefix} "${designation}"` : `"${designation}"`;
7648
7704
  return {
@@ -9721,23 +9777,30 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION) {
9721
9777
  }
9722
9778
  if (objectClass === "LNDMRK" && resolved.type === "symbol") {
9723
9779
  const baseFilterParts = layer.filter && layer.filter[0] === "all" ? layer.filter.slice(1) : layer.filter ? [layer.filter] : [];
9724
- const catlmk = ["coalesce", ["get", "CATLMK"], ["get", "catlmk"], 0];
9725
- const isWind = ["any", ["==", catlmk, 18], ["==", catlmk, 19]];
9726
- const isLighthouse = ["==", catlmk, 17];
9780
+ const isWind = listAttrHas("CATLMK", ["18", "19"]);
9781
+ const isLighthouse = listAttrHas("CATLMK", ["17"]);
9727
9782
  const scaminFloor = scaminToMinZoom(entry?.scamin);
9728
9783
  const windLayer = {
9729
9784
  ...layer,
9730
9785
  id: `s52-${objectClass}-wind-symbol`,
9731
- filter: ["all", ...baseFilterParts, isWind]
9786
+ filter: ["all", ...baseFilterParts, isWind],
9787
+ // Explicit, not inherited: turbines share exact coordinates with
9788
+ // their aviation-light LIGHTS feature; without allow-overlap the
9789
+ // turbine icon loses collision to the LIGHTS flare and vanishes.
9790
+ // (The value currently also arrives via the entry-layout
9791
+ // spread-last chain, but that chain is fragile — see s52-lookup
9792
+ // LNDMRK entry — so the intent is pinned here.)
9793
+ layout: { ...layer.layout ?? {}, "icon-allow-overlap": true }
9732
9794
  };
9733
9795
  delete windLayer.minzoom;
9734
9796
  const lighthouseLayer = {
9735
9797
  ...layer,
9736
9798
  id: `s52-${objectClass}-lighthouse-symbol`,
9737
- filter: ["all", ...baseFilterParts, isLighthouse],
9799
+ filter: ["all", ...baseFilterParts, isLighthouse, ["!", isWind]],
9738
9800
  layout: {
9739
9801
  ...layer.layout ?? {},
9740
- "icon-image": "LIGHTHOUSE01"
9802
+ "icon-image": "LIGHTHOUSE01",
9803
+ "icon-allow-overlap": true
9741
9804
  }
9742
9805
  };
9743
9806
  delete lighthouseLayer.minzoom;
@@ -10514,7 +10577,10 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION) {
10514
10577
  }
10515
10578
  if (objectClass === "TUNNEL" && resolved.type === "line") {
10516
10579
  layers.push({
10517
- id: `s52-TUNNEL-line`,
10580
+ // NOT `s52-TUNNEL-line` — that id is already taken by the main layer
10581
+ // (`s52-${objectClass}-${resolved.type}`), and duplicate layer ids
10582
+ // are a Mapbox style-spec violation (caught by style-integrity.test).
10583
+ id: `s52-TUNNEL-linestring`,
10518
10584
  type: "line",
10519
10585
  source: sourceId,
10520
10586
  "source-layer": "TUNNEL",
@@ -10615,7 +10681,7 @@ function buildLabelLayer2(objectClass, ct, sourceId = DEFAULT_REGION, depthUnits
10615
10681
  delete layer.filter;
10616
10682
  }
10617
10683
  if (getLabelBehavior(objectClass) === "orientation-cross-band") {
10618
- delete layer.filter;
10684
+ layer.filter = LABEL_ANCHOR_FILTER;
10619
10685
  }
10620
10686
  return layer;
10621
10687
  }
@@ -10888,24 +10954,37 @@ function fanOrientationLabelByCellBand(label) {
10888
10954
  const curve = CELL_BAND_OPACITY_CURVE[code];
10889
10955
  const opacityExpr = ["interpolate", ["linear"], ["zoom"]];
10890
10956
  for (const [z, o] of curve) opacityExpr.push(z, o);
10891
- const variant = {
10892
- ...label,
10893
- id: `${label.id}-cell${code}`,
10894
- filter: combineWithBandFilter(
10895
- label.filter,
10896
- ["==", cellBandExpr, code]
10897
- ),
10898
- minzoom: Math.max(
10899
- label.minzoom ?? range.minzoom,
10900
- range.minzoom
10901
- ),
10902
- paint: {
10903
- ...label.paint ?? {},
10904
- "text-opacity": opacityExpr
10905
- }
10957
+ const cellFiltered = combineWithBandFilter(
10958
+ label.filter,
10959
+ ["==", cellBandExpr, code]
10960
+ );
10961
+ const minzoom = Math.max(
10962
+ label.minzoom ?? range.minzoom,
10963
+ range.minzoom
10964
+ );
10965
+ const paint = {
10966
+ ...label.paint ?? {},
10967
+ "text-opacity": opacityExpr
10906
10968
  };
10907
- delete variant.maxzoom;
10908
- variants.push(variant);
10969
+ variants.push({
10970
+ ...label,
10971
+ id: `${label.id}-cell${code}-notdom`,
10972
+ filter: combineWithBandFilter(cellFiltered, NOT_DOMINATED_FILTER),
10973
+ minzoom,
10974
+ paint
10975
+ });
10976
+ for (const D of BAND_DRAW_ORDER) {
10977
+ const dom = BAND_PRIORITY_RANGES[D];
10978
+ if (dom.minzoom <= minzoom) continue;
10979
+ variants.push({
10980
+ ...label,
10981
+ id: `${label.id}-cell${code}-domBy${D}`,
10982
+ filter: combineWithBandFilter(cellFiltered, dominatedByMinFilter(D)),
10983
+ minzoom,
10984
+ maxzoom: dom.minzoom,
10985
+ paint
10986
+ });
10987
+ }
10909
10988
  }
10910
10989
  return variants;
10911
10990
  }