@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.js CHANGED
@@ -3143,18 +3143,37 @@ function depareFillExpression(ct = "DAY") {
3143
3143
  resolveColor("DEPDW", ct)
3144
3144
  ];
3145
3145
  }
3146
- function colourCodeCase(cases, fallback) {
3147
- const colourStr = ["to-string", ["coalesce", ["get", "COLOUR"], ["get", "colour"], ""]];
3146
+ function listAttrString(attr) {
3147
+ return ["to-string", ["coalesce", ["get", attr], ["get", attr.toLowerCase()], ""]];
3148
+ }
3149
+ function listAttrCase(attr, cases, fallback) {
3150
+ const attrStr = listAttrString(attr);
3148
3151
  const expr = ["case"];
3149
3152
  for (const [code, value] of cases) {
3150
- expr.push([">=", ["index-of", `"${code}"`, colourStr], 0], value);
3153
+ expr.push([">=", ["index-of", `"${code}"`, attrStr], 0], value);
3154
+ }
3155
+ for (const [code, value] of cases) {
3156
+ expr.push(["==", attrStr, code], value);
3151
3157
  }
3152
3158
  for (const [code, value] of cases) {
3153
- expr.push(["==", colourStr, code], value);
3159
+ expr.push(["==", attrStr, `[${code}]`], value);
3154
3160
  }
3155
3161
  expr.push(fallback);
3156
3162
  return expr;
3157
3163
  }
3164
+ function listAttrHas(attr, codes) {
3165
+ const attrStr = listAttrString(attr);
3166
+ const clauses = [];
3167
+ for (const code of codes) {
3168
+ clauses.push([">=", ["index-of", `"${code}"`, attrStr], 0]);
3169
+ clauses.push(["==", attrStr, code]);
3170
+ clauses.push(["==", attrStr, `[${code}]`]);
3171
+ }
3172
+ return ["any", ...clauses];
3173
+ }
3174
+ function colourCodeCase(cases, fallback) {
3175
+ return listAttrCase("COLOUR", cases, fallback);
3176
+ }
3158
3177
  function lightColor(props, ct = "DAY") {
3159
3178
  const raw = props.COLOUR ?? props.colour;
3160
3179
  const code = Array.isArray(raw) ? Number(raw[0]) : typeof raw === "string" && raw.startsWith("[") ? Number(JSON.parse(raw)[0]) : Number(raw ?? 0);
@@ -5193,7 +5212,7 @@ var NAUTICAL_SYMBOLS = [
5193
5212
  description: "Church spire or steeple, conspicuous from seaward. Useful for visual fixing.",
5194
5213
  s52PlRef: "LNDMRK17",
5195
5214
  attributeKey: "CATLMK",
5196
- attributeValue: 17,
5215
+ attributeValue: 20,
5197
5216
  renderSize: "32x32",
5198
5217
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Spire, building, position circle" }],
5199
5218
  lineStyles: [{ element: "Building outline", width: 0.8, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5210,7 +5229,7 @@ var NAUTICAL_SYMBOLS = [
5210
5229
  description: "Tower conspicuous from seaward. General-purpose landmark symbol.",
5211
5230
  s52PlRef: "LNDMRK20",
5212
5231
  attributeKey: "CATLMK",
5213
- attributeValue: 20,
5232
+ attributeValue: 17,
5214
5233
  renderSize: "32x32",
5215
5234
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Tower body, cap, position circle" }],
5216
5235
  lineStyles: [{ element: "Tower outline", width: 0.8, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5227,7 +5246,7 @@ var NAUTICAL_SYMBOLS = [
5227
5246
  description: "Industrial chimney or smokestack visible from the sea.",
5228
5247
  s52PlRef: "LNDMRK01",
5229
5248
  attributeKey: "CATLMK",
5230
- attributeValue: 1,
5249
+ attributeValue: 3,
5231
5250
  renderSize: "32x32",
5232
5251
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Chimney body, smoke, position circle" }],
5233
5252
  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" }],
@@ -5244,7 +5263,7 @@ var NAUTICAL_SYMBOLS = [
5244
5263
  description: "Flagstaff or flagpole conspicuous from seaward.",
5245
5264
  s52PlRef: "LNDMRK03",
5246
5265
  attributeKey: "CATLMK",
5247
- attributeValue: 3,
5266
+ attributeValue: 5,
5248
5267
  renderSize: "32x32",
5249
5268
  colors: [
5250
5269
  { token: "CHBLK", hex: "#000000", usage: "Pole and position circle" },
@@ -5264,7 +5283,7 @@ var NAUTICAL_SYMBOLS = [
5264
5283
  description: "Monument or memorial conspicuous from the sea.",
5265
5284
  s52PlRef: "LNDMRK05",
5266
5285
  attributeKey: "CATLMK",
5267
- attributeValue: 5,
5286
+ attributeValue: 9,
5268
5287
  renderSize: "32x32",
5269
5288
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Monument body, base, position circle" }],
5270
5289
  lineStyles: [{ element: "Body outline", width: 0.8, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5279,9 +5298,9 @@ var NAUTICAL_SYMBOLS = [
5279
5298
  getSvg: landmarkWindmill,
5280
5299
  group: "landmarks",
5281
5300
  description: "Traditional windmill with rotating vanes. Conspicuous landmark.",
5282
- s52PlRef: "LNDMRK21",
5301
+ s52PlRef: "WNDMIL12",
5283
5302
  attributeKey: "CATLMK",
5284
- attributeValue: 7,
5303
+ attributeValue: 18,
5285
5304
  renderSize: "32x32",
5286
5305
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Tower, vanes, hub, position circle" }],
5287
5306
  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" }],
@@ -5296,9 +5315,9 @@ var NAUTICAL_SYMBOLS = [
5296
5315
  getSvg: landmarkWindTurbine,
5297
5316
  group: "landmarks",
5298
5317
  description: "Modern 3-blade wind turbine. Increasingly common offshore landmark.",
5299
- s52PlRef: "LNDMRK22",
5318
+ s52PlRef: "WIMCON11",
5300
5319
  attributeKey: "CATLMK",
5301
- attributeValue: 26,
5320
+ attributeValue: 19,
5302
5321
  renderSize: "32x32",
5303
5322
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Tower, blades, hub, position circle" }],
5304
5323
  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" }],
@@ -5315,7 +5334,7 @@ var NAUTICAL_SYMBOLS = [
5315
5334
  description: "Dome or cupola (e.g., observatory, mosque) visible from the sea.",
5316
5335
  s52PlRef: "LNDMRK03",
5317
5336
  attributeKey: "CATLMK",
5318
- attributeValue: 6,
5337
+ attributeValue: 15,
5319
5338
  renderSize: "32x32",
5320
5339
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Dome, base, position circle" }],
5321
5340
  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" }],
@@ -5332,7 +5351,7 @@ var NAUTICAL_SYMBOLS = [
5332
5351
  description: "Radar scanner antenna conspicuous from seaward.",
5333
5352
  s52PlRef: "LNDMRK12",
5334
5353
  attributeKey: "CATLMK",
5335
- attributeValue: 9,
5354
+ attributeValue: 16,
5336
5355
  renderSize: "32x32",
5337
5356
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Tower, scanner dish, position circle" }],
5338
5357
  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" }],
@@ -5349,7 +5368,7 @@ var NAUTICAL_SYMBOLS = [
5349
5368
  description: "Satellite dish or parabolic antenna visible from the sea.",
5350
5369
  s52PlRef: "LNDMRK12",
5351
5370
  attributeKey: "CATLMK",
5352
- attributeValue: 12,
5371
+ attributeValue: 4,
5353
5372
  renderSize: "32x32",
5354
5373
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Tower, dish, position circle" }],
5355
5374
  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" }],
@@ -5364,9 +5383,9 @@ var NAUTICAL_SYMBOLS = [
5364
5383
  getSvg: landmarkWaterTower,
5365
5384
  group: "landmarks",
5366
5385
  description: "Water tower. Elevated tank on supporting structure, conspicuous landmark.",
5367
- s52PlRef: "LNDMRK19",
5368
- attributeKey: "CATLMK",
5369
- attributeValue: 15,
5386
+ s52PlRef: "TOWERS12",
5387
+ attributeKey: "CATSIL",
5388
+ attributeValue: 4,
5370
5389
  renderSize: "32x32",
5371
5390
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Tank, legs, position circle" }],
5372
5391
  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" }],
@@ -5383,7 +5402,7 @@ var NAUTICAL_SYMBOLS = [
5383
5402
  description: "Radio or television transmission mast with horizontal cross-arms.",
5384
5403
  s52PlRef: "LNDMRK10",
5385
5404
  attributeKey: "CATLMK",
5386
- attributeValue: 8,
5405
+ attributeValue: 7,
5387
5406
  renderSize: "32x32",
5388
5407
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Mast, cross-arms, top, position circle" }],
5389
5408
  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" }],
@@ -5400,7 +5419,7 @@ var NAUTICAL_SYMBOLS = [
5400
5419
  description: "Religious cross or calvary conspicuous from seaward.",
5401
5420
  s52PlRef: "LNDMRK05",
5402
5421
  attributeKey: "CATLMK",
5403
- attributeValue: 10,
5422
+ attributeValue: 14,
5404
5423
  renderSize: "32x32",
5405
5424
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Cross arms and position circle" }],
5406
5425
  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" }],
@@ -5417,7 +5436,7 @@ var NAUTICAL_SYMBOLS = [
5417
5436
  description: "General mast (not radio/TV). Pole with spherical top.",
5418
5437
  s52PlRef: "LNDMRK10",
5419
5438
  attributeKey: "CATLMK",
5420
- attributeValue: 14,
5439
+ attributeValue: 7,
5421
5440
  renderSize: "32x32",
5422
5441
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Mast pole, top ball, position circle" }],
5423
5442
  lineStyles: [{ element: "Mast pole", width: 2, color: "#000000" }, { element: "Position circle", width: 0.6, pattern: "2, 2", color: "#000000" }],
@@ -5432,9 +5451,9 @@ var NAUTICAL_SYMBOLS = [
5432
5451
  getSvg: landmarkSilo,
5433
5452
  group: "landmarks",
5434
5453
  description: "Silo, grain elevator, or storage tank conspicuous from the sea.",
5435
- s52PlRef: "LNDMRK17",
5436
- attributeKey: "CATLMK",
5437
- attributeValue: 33,
5454
+ s52PlRef: "SILBUI01",
5455
+ attributeKey: "CATSIL",
5456
+ attributeValue: 1,
5438
5457
  renderSize: "32x32",
5439
5458
  colors: [{ token: "CHBLK", hex: "#000000", usage: "Body, dome top, position circle" }],
5440
5459
  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" }],
@@ -5836,27 +5855,18 @@ function getIconExpression(layerName) {
5836
5855
  ];
5837
5856
  }
5838
5857
  if (layerName === "LNDMRK") {
5839
- return [
5840
- "match",
5841
- ["coalesce", ["get", "CATLMK"], ["get", "catlmk"], 0],
5842
- 1,
5843
- "CAIRNS11",
5844
- // Cairn
5845
- 17,
5846
- "BCNTOW01",
5847
- // Tower
5848
- 18,
5849
- "LNDMRK18",
5858
+ return listAttrCase("CATLMK", [
5859
+ ["18", "LNDMRK18"],
5850
5860
  // Windmill (4-vane)
5851
- 19,
5852
- "LNDMRK19",
5861
+ ["19", "LNDMRK19"],
5853
5862
  // Wind motor / turbine (3-blade)
5854
- 20,
5855
- "BCNTOW01",
5863
+ ["1", "CAIRNS11"],
5864
+ // Cairn
5865
+ ["17", "BCNTOW01"],
5866
+ // Tower
5867
+ ["20", "BCNTOW01"]
5856
5868
  // Spire → tower
5857
- "BCNTOW01"
5858
- // Default: tower
5859
- ];
5869
+ ], "BCNTOW01");
5860
5870
  }
5861
5871
  if (layerName === "DAYMAR") {
5862
5872
  return [
@@ -5996,18 +6006,12 @@ function getIconExpression(layerName) {
5996
6006
  ];
5997
6007
  }
5998
6008
  if (layerName === "LIGHTS") {
5999
- return [
6000
- "match",
6001
- ["coalesce", ["get", "CATLIT"], ["get", "catlit"], 0],
6002
- 8,
6003
- "LIGHTS82",
6004
- // Aero light
6005
- 9,
6006
- "LIGHTS82",
6007
- // Air obstruction light
6008
- "LIGHTS11"
6009
- // Default: major light
6010
- ];
6009
+ return listAttrCase("CATLIT", [
6010
+ ["8", "LIGHTS82"],
6011
+ // Floodlight
6012
+ ["9", "LIGHTS82"]
6013
+ // Strip light
6014
+ ], "LIGHTS11");
6011
6015
  }
6012
6016
  return SYMBOL_MAP[layerName] ?? "";
6013
6017
  }
@@ -6358,30 +6362,141 @@ function buildSeabedLabelExpression() {
6358
6362
  const natsurJoined = ["slice", natsurParts, 1];
6359
6363
  return ["concat", natquaPrefix, natsurJoined];
6360
6364
  }
6365
+ function underseaFeatureNameMatch(objnam) {
6366
+ const terms = [
6367
+ "Canyon",
6368
+ "Seamount",
6369
+ "Ridge",
6370
+ "Basin",
6371
+ "Swell",
6372
+ "Knoll",
6373
+ "Shoal",
6374
+ "Ledge",
6375
+ "Bank",
6376
+ "Valley",
6377
+ "Spur",
6378
+ "Escarpment"
6379
+ ];
6380
+ return ["any", ...terms.map((t) => [">=", ["index-of", t, objnam], 0])];
6381
+ }
6382
+ var CATSEA_RELIEF_CODES = [
6383
+ 2,
6384
+ // gat
6385
+ 3,
6386
+ // bank
6387
+ 4,
6388
+ // deep
6389
+ 6,
6390
+ // trench
6391
+ 7,
6392
+ // basin
6393
+ 8,
6394
+ // mud flats
6395
+ 9,
6396
+ // reef
6397
+ 10,
6398
+ // ledge
6399
+ 11,
6400
+ // canyon
6401
+ 13,
6402
+ // shoal
6403
+ 14,
6404
+ // knoll
6405
+ 15,
6406
+ // ridge
6407
+ 16,
6408
+ // seamount
6409
+ 17,
6410
+ // pinnacle
6411
+ 18,
6412
+ // abyssal plain
6413
+ 19,
6414
+ // plateau
6415
+ 20,
6416
+ // spur
6417
+ 21,
6418
+ // shelf
6419
+ 22,
6420
+ // trough
6421
+ 23,
6422
+ // saddle
6423
+ 24,
6424
+ // abyssal hills
6425
+ 25,
6426
+ // apron
6427
+ 26,
6428
+ // archipelagic apron
6429
+ 27,
6430
+ // borderland
6431
+ 28,
6432
+ // continental margin
6433
+ 29,
6434
+ // continental rise
6435
+ 30,
6436
+ // escarpment
6437
+ 31,
6438
+ // fan
6439
+ 32,
6440
+ // fracture zone
6441
+ 33,
6442
+ // gap
6443
+ 34,
6444
+ // guyot
6445
+ 35,
6446
+ // hill
6447
+ 36,
6448
+ // hole
6449
+ 37,
6450
+ // hollow
6451
+ 39,
6452
+ // levee
6453
+ 40,
6454
+ // median valley
6455
+ 41,
6456
+ // moat
6457
+ 42,
6458
+ // mountains
6459
+ 43,
6460
+ // peak
6461
+ 44,
6462
+ // province
6463
+ 45,
6464
+ // rise
6465
+ 47,
6466
+ // seamount chain
6467
+ 48,
6468
+ // shelf-edge
6469
+ 49,
6470
+ // sill
6471
+ 50,
6472
+ // slope
6473
+ 51,
6474
+ // terrace
6475
+ 52
6476
+ // valley
6477
+ ];
6361
6478
  function buildSeaAreaTierExpression() {
6362
- const catsea = ["coalesce", ["get", "CATSEA"], ["get", "catsea"], 0];
6479
+ const catsea = ["to-number", ["coalesce", ["get", "CATSEA"], ["get", "catsea"], 0]];
6363
6480
  const objnam = ["coalesce", ["get", "OBJNAM"], ""];
6364
6481
  return [
6365
6482
  "case",
6366
- // Tier 1 — Ocean (CATSEA=31 OR OBJNAM contains "Ocean", excluding
6367
- // anchorage-area-style compound names which we filter on later).
6368
- ["==", ["to-number", catsea], 31],
6369
- 1,
6370
- ["any", ["==", ["to-number", catsea], 29], ["==", ["to-number", catsea], 30]],
6371
- 2,
6372
- [
6373
- "any",
6374
- ["==", ["to-number", catsea], 4],
6375
- ["==", ["to-number", catsea], 11],
6376
- ["==", ["to-number", catsea], 27],
6377
- ["==", ["to-number", catsea], 28]
6378
- ],
6379
- 3,
6380
- // Tier 4 — Anchorage detail (must be checked before OBJNAM-substring
6381
- // promotion so "Boston Harbor, MA …" anchorages don't bubble up).
6483
+ // Tier 4 — Anchorage detail (before all promotions so "Boston Harbor,
6484
+ // MA …" anchorage compound names don't bubble up).
6382
6485
  [">=", ["index-of", "Anchorage Area", objnam], 0],
6383
6486
  4,
6384
- // OBJNAM-substring promotion shims (run AFTER tier 4 demotion):
6487
+ // Tier 5 — undersea relief, by authoritative CATSEA code first
6488
+ // (Georges Bank=3, Georges Shoal=13, …), then by name for
6489
+ // uncategorised features. Before promotion shims; see
6490
+ // underseaFeatureNameMatch.
6491
+ ["match", catsea, CATSEA_RELIEF_CODES, true, false],
6492
+ 5,
6493
+ underseaFeatureNameMatch(objnam),
6494
+ 5,
6495
+ // Tier 3 by authoritative CATSEA: bay / narrows.
6496
+ ["match", catsea, [5, 12], true, false],
6497
+ 3,
6498
+ // OBJNAM promotion shims (CATSEA has no ocean/sea/gulf/sound/strait
6499
+ // codes, so names are the only signal for the headline tiers):
6385
6500
  [">=", ["index-of", "Atlantic Ocean", objnam], 0],
6386
6501
  1,
6387
6502
  [">=", ["index-of", "Pacific Ocean", objnam], 0],
@@ -6390,9 +6505,6 @@ function buildSeaAreaTierExpression() {
6390
6505
  1,
6391
6506
  [">=", ["index-of", "Gulf of", objnam], 0],
6392
6507
  2,
6393
- // Bay/Sound/Sea/Strait/Channel as a tier-3 shim (matches "Massachusetts
6394
- // Bay", "Buzzards Bay", "Long Island Sound", "Block Island Sound",
6395
- // "Vineyard Sound", "Cape Cod Bay", etc. without needing CATSEA).
6396
6508
  [">=", ["index-of", " Bay", objnam], 0],
6397
6509
  3,
6398
6510
  [">=", ["index-of", " Sound", objnam], 0],
@@ -6426,6 +6538,11 @@ function buildLandRegionTierExpression() {
6426
6538
  const nameLen = ["length", objnam];
6427
6539
  return [
6428
6540
  "case",
6541
+ // NOAA also encodes undersea relief names (e.g. "Block Canyon", 12
6542
+ // chars, no comma) as LNDRGN — without this demotion they'd qualify
6543
+ // as tier-1 "major region" and paint at overview zoom.
6544
+ underseaFeatureNameMatch(objnam),
6545
+ 2,
6429
6546
  ["all", ["<=", nameLen, 12], ["==", ["index-of", ",", objnam], -1]],
6430
6547
  1,
6431
6548
  2
@@ -6507,6 +6624,36 @@ function orientationLabelLayout(extras = {}) {
6507
6624
  ...extras
6508
6625
  };
6509
6626
  }
6627
+ function tieredSizeCurve(tierExpr, stops) {
6628
+ const withHolds = [];
6629
+ for (let i = 0; i < stops.length; i++) {
6630
+ if (i > 0) {
6631
+ const [pz, pv] = stops[i - 1];
6632
+ const [nz, nv] = stops[i];
6633
+ if (pv.some((v, t) => v === 0 && nv[t] > 0)) {
6634
+ const hz = Math.round((nz - 0.2) * 10) / 10;
6635
+ if (hz > pz) {
6636
+ const f = (hz - pz) / (nz - pz);
6637
+ withHolds.push([
6638
+ hz,
6639
+ pv.map(
6640
+ (v, t) => v === 0 && nv[t] > 0 ? 0 : Math.round((v + (nv[t] - v) * f) * 10) / 10
6641
+ )
6642
+ ]);
6643
+ }
6644
+ }
6645
+ }
6646
+ withHolds.push(stops[i]);
6647
+ }
6648
+ const expr = ["interpolate", ["linear"], ["zoom"]];
6649
+ for (const [z, vals] of withHolds) {
6650
+ const m = ["match", tierExpr];
6651
+ for (let t = 0; t < vals.length - 1; t++) m.push(t + 1, vals[t]);
6652
+ m.push(vals[vals.length - 1]);
6653
+ expr.push(z, m);
6654
+ }
6655
+ return expr;
6656
+ }
6510
6657
  var ORIENTATION_HALO_WIDTH = [
6511
6658
  "interpolate",
6512
6659
  ["linear"],
@@ -6556,6 +6703,11 @@ var LABEL_BEHAVIOR = {
6556
6703
  function getLabelBehavior(layerName) {
6557
6704
  return LABEL_BEHAVIOR[layerName] ?? "feature-band-gated";
6558
6705
  }
6706
+ var LABEL_ANCHOR_FILTER = [
6707
+ "!=",
6708
+ ["get", "_label_anchor"],
6709
+ false
6710
+ ];
6559
6711
  function getLabelConfig(layerName, opts = {}) {
6560
6712
  const ct = opts.colorTable ?? "DAY";
6561
6713
  switch (layerName) {
@@ -6636,35 +6788,23 @@ function getLabelConfig(layerName, opts = {}) {
6636
6788
  }
6637
6789
  case "LNDRGN": {
6638
6790
  const tierExpr = buildLandRegionTierExpression();
6639
- const sizeAt = (t1, t2) => [
6640
- "match",
6641
- tierExpr,
6642
- 1,
6643
- t1,
6644
- 2,
6645
- t2,
6646
- t2
6647
- ];
6648
6791
  return {
6649
6792
  type: "symbol",
6650
6793
  textField: ["get", "OBJNAM"],
6651
- textSize: [
6652
- "interpolate",
6653
- ["linear"],
6654
- ["zoom"],
6655
- 5,
6656
- sizeAt(9, 0),
6657
- 7,
6658
- sizeAt(11, 0),
6659
- 9,
6660
- sizeAt(12, 0),
6661
- 11,
6662
- sizeAt(12, 9),
6663
- 13,
6664
- sizeAt(11, 11),
6665
- 16,
6666
- sizeAt(10, 10)
6667
- ],
6794
+ // Tier-1 entry moved from z5 to z7: undersea-relief names without a
6795
+ // generic keyword ("Three Dory", "Outer Fall") can't be demoted by
6796
+ // pattern, so the overview/general bands stay clean by holding ALL
6797
+ // LNDRGN labels until coastal zoom. The Carto basemap already labels
6798
+ // major land regions at z5-z6. Columns: t1 major region, t2 local
6799
+ // point (also the default).
6800
+ textSize: tieredSizeCurve(tierExpr, [
6801
+ [5, [0, 0]],
6802
+ [7, [11, 0]],
6803
+ [9, [12, 0]],
6804
+ [11, [12, 9]],
6805
+ [13, [11, 11]],
6806
+ [16, [10, 10]]
6807
+ ]),
6668
6808
  textColor: DARK_BG,
6669
6809
  textHaloColor: COLOR.LAND_REGION,
6670
6810
  textHaloWidth: ORIENTATION_HALO_WIDTH,
@@ -6804,19 +6944,6 @@ function getLabelConfig(layerName, opts = {}) {
6804
6944
  }
6805
6945
  case "SEAARE": {
6806
6946
  const tierExpr = buildSeaAreaTierExpression();
6807
- const sizeAt = (t1, t2, t3, t4, def) => [
6808
- "match",
6809
- tierExpr,
6810
- 1,
6811
- t1,
6812
- 2,
6813
- t2,
6814
- 3,
6815
- t3,
6816
- 4,
6817
- t4,
6818
- def
6819
- ];
6820
6947
  return {
6821
6948
  type: "symbol",
6822
6949
  textField: ["get", "OBJNAM"],
@@ -6825,26 +6952,17 @@ function getLabelConfig(layerName, opts = {}) {
6825
6952
  // it floods the overview with hundreds of small italic labels that
6826
6953
  // crowd out the headline Ocean/Sea/Gulf names. It becomes visible only
6827
6954
  // at approach scale (z9) when the user has zoomed in enough that
6828
- // individual banks are useful context.
6829
- textSize: [
6830
- "interpolate",
6831
- ["linear"],
6832
- ["zoom"],
6833
- 3,
6834
- sizeAt(14, 8, 0, 0, 0),
6835
- 5,
6836
- sizeAt(16, 13, 9, 0, 0),
6837
- 7,
6838
- sizeAt(14, 14, 12, 0, 0),
6839
- 9,
6840
- sizeAt(12, 13, 14, 0, 9),
6841
- 11,
6842
- sizeAt(10, 12, 13, 0, 11),
6843
- 13,
6844
- sizeAt(9, 11, 12, 10, 11),
6845
- 16,
6846
- sizeAt(9, 10, 11, 11, 10)
6847
- ],
6955
+ // individual banks are useful context. Columns: t1 ocean, t2 sea/gulf,
6956
+ // t3 bay/sound, t4 anchorage, default.
6957
+ textSize: tieredSizeCurve(tierExpr, [
6958
+ [3, [14, 8, 0, 0, 0]],
6959
+ [5, [16, 13, 9, 0, 0]],
6960
+ [7, [14, 14, 12, 0, 0]],
6961
+ [9, [12, 13, 14, 0, 9]],
6962
+ [11, [10, 12, 13, 0, 11]],
6963
+ [13, [9, 11, 12, 10, 11]],
6964
+ [16, [9, 10, 11, 11, 10]]
6965
+ ]),
6848
6966
  textColor: DARK_BG,
6849
6967
  textHaloColor: COLOR.WATER_DEEP,
6850
6968
  textHaloWidth: ORIENTATION_HALO_WIDTH,
@@ -6853,39 +6971,38 @@ function getLabelConfig(layerName, opts = {}) {
6853
6971
  minzoom: 5,
6854
6972
  layout: orientationLabelLayout({
6855
6973
  "text-letter-spacing": 0.15,
6856
- "text-max-width": 9
6974
+ "text-max-width": 9,
6975
+ // SEAARE is always-paint, unlike the other orientation classes.
6976
+ // MapLibre places symbols topmost-layer-first, so with
6977
+ // allow-overlap:false the dense sounding field (drawn above)
6978
+ // collision-starves big-water names — observed on staging at z8:
6979
+ // only 2 SEAARE labels survived viewport-wide, with Cape Cod Bay
6980
+ // and Gulf of Maine dropped entirely. Always-paint is safe for
6981
+ // this class because (a) the dominance composition in
6982
+ // fanOrientationLabelByCellBand suppresses cross-band duplicate
6983
+ // copies, and (b) tier sizing zeroes relief/anchorage names at
6984
+ // the zooms where density is a problem. Rivers/lakes/land keep
6985
+ // collision-dedup — their within-band multi-polygon stacking was
6986
+ // the original complaint, and they lack big-water headline value.
6987
+ "text-allow-overlap": true,
6988
+ "text-ignore-placement": true,
6989
+ "text-optional": false,
6990
+ "text-padding": 2
6857
6991
  })
6858
6992
  };
6859
6993
  }
6860
6994
  case "LAKARE": {
6861
6995
  const tierExpr = buildLakeTierExpression();
6862
- const sizeAt = (t1, t2) => [
6863
- "match",
6864
- tierExpr,
6865
- 1,
6866
- t1,
6867
- 2,
6868
- t2,
6869
- t2
6870
- ];
6871
6996
  return {
6872
6997
  type: "symbol",
6873
6998
  textField: ["get", "OBJNAM"],
6874
- textSize: [
6875
- "interpolate",
6876
- ["linear"],
6877
- ["zoom"],
6878
- 7,
6879
- sizeAt(9, 0),
6880
- 9,
6881
- sizeAt(11, 0),
6882
- 11,
6883
- sizeAt(11, 9),
6884
- 13,
6885
- sizeAt(10, 11),
6886
- 16,
6887
- sizeAt(9, 10)
6888
- ],
6999
+ textSize: tieredSizeCurve(tierExpr, [
7000
+ [7, [9, 0]],
7001
+ [9, [11, 0]],
7002
+ [11, [11, 9]],
7003
+ [13, [10, 11]],
7004
+ [16, [9, 10]]
7005
+ ]),
6889
7006
  textColor: DARK_BG,
6890
7007
  textHaloColor: COLOR.WATER,
6891
7008
  textHaloWidth: ORIENTATION_HALO_WIDTH,
@@ -6897,33 +7014,16 @@ function getLabelConfig(layerName, opts = {}) {
6897
7014
  }
6898
7015
  case "RIVERS": {
6899
7016
  const tierExpr = buildRiverTierExpression();
6900
- const sizeAt = (t1, t2) => [
6901
- "match",
6902
- tierExpr,
6903
- 1,
6904
- t1,
6905
- 2,
6906
- t2,
6907
- t2
6908
- ];
6909
7017
  return {
6910
7018
  type: "symbol",
6911
7019
  textField: ["get", "OBJNAM"],
6912
- textSize: [
6913
- "interpolate",
6914
- ["linear"],
6915
- ["zoom"],
6916
- 7,
6917
- sizeAt(9, 0),
6918
- 9,
6919
- sizeAt(11, 0),
6920
- 11,
6921
- sizeAt(11, 9),
6922
- 13,
6923
- sizeAt(10, 10),
6924
- 16,
6925
- sizeAt(9, 10)
6926
- ],
7020
+ textSize: tieredSizeCurve(tierExpr, [
7021
+ [7, [9, 0]],
7022
+ [9, [11, 0]],
7023
+ [11, [11, 9]],
7024
+ [13, [10, 10]],
7025
+ [16, [9, 10]]
7026
+ ]),
6927
7027
  textColor: DARK_BG,
6928
7028
  textHaloColor: COLOR.WATER,
6929
7029
  textHaloWidth: ORIENTATION_HALO_WIDTH,
@@ -6958,35 +7058,17 @@ function getLabelConfig(layerName, opts = {}) {
6958
7058
  };
6959
7059
  case "LNDARE": {
6960
7060
  const tierExpr = buildLandAreaTierExpression();
6961
- const sizeAt = (t1, t2) => [
6962
- "match",
6963
- tierExpr,
6964
- 1,
6965
- t1,
6966
- 2,
6967
- t2,
6968
- t2
6969
- ];
6970
7061
  return {
6971
7062
  type: "symbol",
6972
7063
  textField: ["get", "OBJNAM"],
6973
- textSize: [
6974
- "interpolate",
6975
- ["linear"],
6976
- ["zoom"],
6977
- 5,
6978
- sizeAt(8, 0),
6979
- 7,
6980
- sizeAt(10, 0),
6981
- 9,
6982
- sizeAt(11, 0),
6983
- 11,
6984
- sizeAt(11, 9),
6985
- 13,
6986
- sizeAt(10, 10),
6987
- 16,
6988
- sizeAt(9, 10)
6989
- ],
7064
+ textSize: tieredSizeCurve(tierExpr, [
7065
+ [5, [8, 0]],
7066
+ [7, [10, 0]],
7067
+ [9, [11, 0]],
7068
+ [11, [11, 9]],
7069
+ [13, [10, 10]],
7070
+ [16, [9, 10]]
7071
+ ]),
6990
7072
  textColor: DARK_BG,
6991
7073
  textHaloColor: COLOR.LAND_NATURAL,
6992
7074
  textHaloWidth: ORIENTATION_HALO_WIDTH,
@@ -6998,39 +7080,18 @@ function getLabelConfig(layerName, opts = {}) {
6998
7080
  }
6999
7081
  case "BUAARE": {
7000
7082
  const tierExpr = buildBuiltUpAreaTierExpression();
7001
- const sizeAt = (t1, t2, t3) => [
7002
- "match",
7003
- tierExpr,
7004
- 1,
7005
- t1,
7006
- 2,
7007
- t2,
7008
- 3,
7009
- t3,
7010
- t3
7011
- ];
7012
7083
  return {
7013
7084
  type: "symbol",
7014
7085
  textField: ["get", "OBJNAM"],
7015
- textSize: [
7016
- "interpolate",
7017
- ["linear"],
7018
- ["zoom"],
7019
- 3,
7020
- sizeAt(11, 0, 0),
7021
- 5,
7022
- sizeAt(13, 0, 0),
7023
- 7,
7024
- sizeAt(13, 9, 0),
7025
- 9,
7026
- sizeAt(12, 12, 0),
7027
- 11,
7028
- sizeAt(11, 12, 10),
7029
- 13,
7030
- sizeAt(10, 11, 12),
7031
- 16,
7032
- sizeAt(10, 10, 11)
7033
- ],
7086
+ textSize: tieredSizeCurve(tierExpr, [
7087
+ [3, [11, 0, 0]],
7088
+ [5, [13, 0, 0]],
7089
+ [7, [13, 9, 0]],
7090
+ [9, [12, 12, 0]],
7091
+ [11, [11, 12, 10]],
7092
+ [13, [10, 11, 12]],
7093
+ [16, [10, 10, 11]]
7094
+ ]),
7034
7095
  textColor: DARK_BG,
7035
7096
  textHaloColor: COLOR.CITY,
7036
7097
  textHaloWidth: ORIENTATION_HALO_WIDTH,
@@ -7111,33 +7172,16 @@ function getLabelConfig(layerName, opts = {}) {
7111
7172
  };
7112
7173
  case "AIRARE": {
7113
7174
  const tierExpr = buildAirportTierExpression();
7114
- const sizeAt = (t1, t2) => [
7115
- "match",
7116
- tierExpr,
7117
- 1,
7118
- t1,
7119
- 2,
7120
- t2,
7121
- t2
7122
- ];
7123
7175
  return {
7124
7176
  type: "symbol",
7125
7177
  textField: ["get", "OBJNAM"],
7126
- textSize: [
7127
- "interpolate",
7128
- ["linear"],
7129
- ["zoom"],
7130
- 7,
7131
- sizeAt(9, 0),
7132
- 9,
7133
- sizeAt(11, 0),
7134
- 11,
7135
- sizeAt(11, 9),
7136
- 13,
7137
- sizeAt(10, 10),
7138
- 16,
7139
- sizeAt(9, 9)
7140
- ],
7178
+ textSize: tieredSizeCurve(tierExpr, [
7179
+ [7, [9, 0]],
7180
+ [9, [11, 0]],
7181
+ [11, [11, 9]],
7182
+ [13, [10, 10]],
7183
+ [16, [9, 9]]
7184
+ ]),
7141
7185
  textColor: DARK_BG,
7142
7186
  textHaloColor: COLOR.AIRPORT,
7143
7187
  textHaloWidth: ORIENTATION_HALO_WIDTH,
@@ -7455,6 +7499,18 @@ function preprocessSoundings(fc) {
7455
7499
  }
7456
7500
  return { type: "FeatureCollection", features };
7457
7501
  }
7502
+ function firstListCode(raw) {
7503
+ if (raw == null) return NaN;
7504
+ if (Array.isArray(raw)) return Number(raw[0]);
7505
+ if (typeof raw === "string" && raw.startsWith("[")) {
7506
+ try {
7507
+ const arr = JSON.parse(raw);
7508
+ if (Array.isArray(arr)) return Number(arr[0]);
7509
+ } catch {
7510
+ }
7511
+ }
7512
+ return Number(raw);
7513
+ }
7458
7514
  function preprocessLights(fc) {
7459
7515
  return {
7460
7516
  type: "FeatureCollection",
@@ -7463,7 +7519,7 @@ function preprocessLights(fc) {
7463
7519
  const parts = [];
7464
7520
  const litchr = Number(props.LITCHR);
7465
7521
  if (litchr && LITCHR_ABBR[litchr]) parts.push(LITCHR_ABBR[litchr]);
7466
- const colour = Number(props.COLOUR);
7522
+ const colour = firstListCode(props.COLOUR);
7467
7523
  if (colour && COLOUR_ABBR[colour]) parts.push(COLOUR_ABBR[colour]);
7468
7524
  const sigper = Number(props.SIGPER);
7469
7525
  if (sigper && sigper > 0) parts.push(sigper + "s");
@@ -7537,7 +7593,7 @@ function preprocessNavAids(fc) {
7537
7593
  const match = objnam.match(/(\d+[A-Z]?)\s*$/);
7538
7594
  if (!match) return f;
7539
7595
  const designation = match[1];
7540
- const colourCode = Number(props.COLOUR);
7596
+ const colourCode = firstListCode(props.COLOUR);
7541
7597
  const prefix = NAV_COLOUR_PREFIX[colourCode];
7542
7598
  const label = prefix ? `${prefix} "${designation}"` : `"${designation}"`;
7543
7599
  return {
@@ -9616,23 +9672,30 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION) {
9616
9672
  }
9617
9673
  if (objectClass === "LNDMRK" && resolved.type === "symbol") {
9618
9674
  const baseFilterParts = layer.filter && layer.filter[0] === "all" ? layer.filter.slice(1) : layer.filter ? [layer.filter] : [];
9619
- const catlmk = ["coalesce", ["get", "CATLMK"], ["get", "catlmk"], 0];
9620
- const isWind = ["any", ["==", catlmk, 18], ["==", catlmk, 19]];
9621
- const isLighthouse = ["==", catlmk, 17];
9675
+ const isWind = listAttrHas("CATLMK", ["18", "19"]);
9676
+ const isLighthouse = listAttrHas("CATLMK", ["17"]);
9622
9677
  const scaminFloor = scaminToMinZoom(entry?.scamin);
9623
9678
  const windLayer = {
9624
9679
  ...layer,
9625
9680
  id: `s52-${objectClass}-wind-symbol`,
9626
- filter: ["all", ...baseFilterParts, isWind]
9681
+ filter: ["all", ...baseFilterParts, isWind],
9682
+ // Explicit, not inherited: turbines share exact coordinates with
9683
+ // their aviation-light LIGHTS feature; without allow-overlap the
9684
+ // turbine icon loses collision to the LIGHTS flare and vanishes.
9685
+ // (The value currently also arrives via the entry-layout
9686
+ // spread-last chain, but that chain is fragile — see s52-lookup
9687
+ // LNDMRK entry — so the intent is pinned here.)
9688
+ layout: { ...layer.layout ?? {}, "icon-allow-overlap": true }
9627
9689
  };
9628
9690
  delete windLayer.minzoom;
9629
9691
  const lighthouseLayer = {
9630
9692
  ...layer,
9631
9693
  id: `s52-${objectClass}-lighthouse-symbol`,
9632
- filter: ["all", ...baseFilterParts, isLighthouse],
9694
+ filter: ["all", ...baseFilterParts, isLighthouse, ["!", isWind]],
9633
9695
  layout: {
9634
9696
  ...layer.layout ?? {},
9635
- "icon-image": "LIGHTHOUSE01"
9697
+ "icon-image": "LIGHTHOUSE01",
9698
+ "icon-allow-overlap": true
9636
9699
  }
9637
9700
  };
9638
9701
  delete lighthouseLayer.minzoom;
@@ -10409,7 +10472,10 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION) {
10409
10472
  }
10410
10473
  if (objectClass === "TUNNEL" && resolved.type === "line") {
10411
10474
  layers.push({
10412
- id: `s52-TUNNEL-line`,
10475
+ // NOT `s52-TUNNEL-line` — that id is already taken by the main layer
10476
+ // (`s52-${objectClass}-${resolved.type}`), and duplicate layer ids
10477
+ // are a Mapbox style-spec violation (caught by style-integrity.test).
10478
+ id: `s52-TUNNEL-linestring`,
10413
10479
  type: "line",
10414
10480
  source: sourceId,
10415
10481
  "source-layer": "TUNNEL",
@@ -10510,7 +10576,7 @@ function buildLabelLayer2(objectClass, ct, sourceId = DEFAULT_REGION, depthUnits
10510
10576
  delete layer.filter;
10511
10577
  }
10512
10578
  if (getLabelBehavior(objectClass) === "orientation-cross-band") {
10513
- delete layer.filter;
10579
+ layer.filter = LABEL_ANCHOR_FILTER;
10514
10580
  }
10515
10581
  return layer;
10516
10582
  }
@@ -10783,24 +10849,37 @@ function fanOrientationLabelByCellBand(label) {
10783
10849
  const curve = CELL_BAND_OPACITY_CURVE[code];
10784
10850
  const opacityExpr = ["interpolate", ["linear"], ["zoom"]];
10785
10851
  for (const [z, o] of curve) opacityExpr.push(z, o);
10786
- const variant = {
10787
- ...label,
10788
- id: `${label.id}-cell${code}`,
10789
- filter: combineWithBandFilter(
10790
- label.filter,
10791
- ["==", cellBandExpr, code]
10792
- ),
10793
- minzoom: Math.max(
10794
- label.minzoom ?? range.minzoom,
10795
- range.minzoom
10796
- ),
10797
- paint: {
10798
- ...label.paint ?? {},
10799
- "text-opacity": opacityExpr
10800
- }
10852
+ const cellFiltered = combineWithBandFilter(
10853
+ label.filter,
10854
+ ["==", cellBandExpr, code]
10855
+ );
10856
+ const minzoom = Math.max(
10857
+ label.minzoom ?? range.minzoom,
10858
+ range.minzoom
10859
+ );
10860
+ const paint = {
10861
+ ...label.paint ?? {},
10862
+ "text-opacity": opacityExpr
10801
10863
  };
10802
- delete variant.maxzoom;
10803
- variants.push(variant);
10864
+ variants.push({
10865
+ ...label,
10866
+ id: `${label.id}-cell${code}-notdom`,
10867
+ filter: combineWithBandFilter(cellFiltered, NOT_DOMINATED_FILTER),
10868
+ minzoom,
10869
+ paint
10870
+ });
10871
+ for (const D of BAND_DRAW_ORDER) {
10872
+ const dom = BAND_PRIORITY_RANGES[D];
10873
+ if (dom.minzoom <= minzoom) continue;
10874
+ variants.push({
10875
+ ...label,
10876
+ id: `${label.id}-cell${code}-domBy${D}`,
10877
+ filter: combineWithBandFilter(cellFiltered, dominatedByMinFilter(D)),
10878
+ minzoom,
10879
+ maxzoom: dom.minzoom,
10880
+ paint
10881
+ });
10882
+ }
10804
10883
  }
10805
10884
  return variants;
10806
10885
  }