@chrtco/sdk 0.8.1 → 0.9.0
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 +21 -0
- package/dist/index.cjs +3231 -189
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +931 -112
- package/dist/index.d.ts +931 -112
- package/dist/index.js +3189 -189
- package/dist/index.js.map +1 -1
- package/package.json +3 -20
package/dist/index.js
CHANGED
|
@@ -135,6 +135,7 @@ var TileTokenError = class extends Error {
|
|
|
135
135
|
}
|
|
136
136
|
};
|
|
137
137
|
var TILE_URL_PATH = /\/v1\/tiles\/[A-Za-z0-9_]+(?:\/[a-z][a-z0-9-]*)?\/\d+\/\d+\/\d+\.mvt(?:\?.*)?$/i;
|
|
138
|
+
var STYLE_URL_PATH = /\/v1\/styles\/[A-Za-z0-9_]+\/style\.json(?:\?.*)?$/i;
|
|
138
139
|
var REFRESH_MARGIN_SECONDS = 30;
|
|
139
140
|
var STALE_GRACE_SECONDS = 1;
|
|
140
141
|
var MINT_RETRY_DELAYS_MS = [500, 1e3, 2e3];
|
|
@@ -278,17 +279,22 @@ var TileAuth = class {
|
|
|
278
279
|
}
|
|
279
280
|
/**
|
|
280
281
|
* Produce a MapLibre / Mapbox GL `transformRequest`. Attaches
|
|
281
|
-
* `Authorization: Bearer <jwt>` only to CHRT tile URLs
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
282
|
+
* `Authorization: Bearer <jwt>` only to CHRT tile URLs and the CHRT
|
|
283
|
+
* served-style URL (`/v1/styles/{region}/style.json`); sprite, glyph, and
|
|
284
|
+
* third-party URLs pass through untouched (never leak the token to
|
|
285
|
+
* third-party CDNs). Kicks off a background mint immediately so the cache
|
|
286
|
+
* warms within one round-trip; the first tile or two may go
|
|
287
|
+
* unauthenticated and be retried by the renderer. When pointing a map's
|
|
288
|
+
* `style` at the served style URL, obtain it via `chrt.styleUrl()` — it
|
|
289
|
+
* awaits the mint, so the style request (which MapLibre does NOT retry)
|
|
290
|
+
* is guaranteed to carry auth.
|
|
286
291
|
*/
|
|
287
292
|
transformRequest(region) {
|
|
288
293
|
const r = region ?? this.defaultRegion;
|
|
289
294
|
void this.getToken(r).catch(() => void 0);
|
|
290
295
|
return (url) => {
|
|
291
|
-
if (!url.startsWith(this.tileBaseUrl)
|
|
296
|
+
if (!url.startsWith(this.tileBaseUrl)) return void 0;
|
|
297
|
+
if (!TILE_URL_PATH.test(url) && !STYLE_URL_PATH.test(url)) return void 0;
|
|
292
298
|
const token = this.peek(r);
|
|
293
299
|
if (!token) return { url };
|
|
294
300
|
return { url, headers: { Authorization: `Bearer ${token}` } };
|
|
@@ -326,23 +332,67 @@ var TileAuth = class {
|
|
|
326
332
|
|
|
327
333
|
// src/endpoints/tiles.ts
|
|
328
334
|
var TilesEndpoint = class {
|
|
329
|
-
constructor(http, opts) {
|
|
335
|
+
constructor(http, tileAuth, opts) {
|
|
330
336
|
this.http = http;
|
|
337
|
+
this.tileAuth = tileAuth;
|
|
331
338
|
this.region = opts?.region ?? DEFAULT_TILE_REGION;
|
|
332
339
|
this.tileBaseUrl = (opts?.tileBaseUrl ?? DEFAULT_TILE_BASE_URL).replace(/\/+$/, "");
|
|
333
340
|
}
|
|
334
341
|
region;
|
|
335
342
|
tileBaseUrl;
|
|
343
|
+
styleEndpoint(theme) {
|
|
344
|
+
const query2 = theme ? `?theme=${theme}` : "";
|
|
345
|
+
return `${this.tileBaseUrl}/v1/styles/${this.region}/style.json${query2}`;
|
|
346
|
+
}
|
|
336
347
|
/**
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
348
|
+
* URL of the served S-52 style for the configured region — the CHRTstyle
|
|
349
|
+
* Style API (`GET /v1/styles/{region}/style.json`). The served document
|
|
350
|
+
* honors the API key's bound custom style (a Dev+ feature configured at
|
|
351
|
+
* platform → Map Styles); Free keys and unbound keys get the default
|
|
352
|
+
* themes, identical to the locally generated `chrt.style()`.
|
|
353
|
+
*
|
|
354
|
+
* The endpoint is JWT-gated, so pair the URL with
|
|
355
|
+
* `chrt.transformRequest()`, which attaches the Bearer token to both the
|
|
356
|
+
* style request and its tile requests. This method is async because it
|
|
357
|
+
* pre-mints the tile token, guaranteeing the auth cache is warm before
|
|
358
|
+
* MapLibre fetches the URL.
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* const map = new maplibregl.Map({
|
|
362
|
+
* container: 'map',
|
|
363
|
+
* style: await chrt.styleUrl({ theme: 'dusk' }),
|
|
364
|
+
* transformRequest: chrt.transformRequest(),
|
|
365
|
+
* });
|
|
341
366
|
*/
|
|
342
|
-
styleUrl() {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
367
|
+
async styleUrl(opts = {}) {
|
|
368
|
+
await this.tileAuth.getToken(this.region);
|
|
369
|
+
return this.styleEndpoint(opts.theme);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Fetch the served S-52 style as a MapLibre style object (see
|
|
373
|
+
* {@link styleUrl} for what "served" means). Prefer this over `styleUrl()`
|
|
374
|
+
* when you want to inspect or mutate the document, or when you can't use
|
|
375
|
+
* `transformRequest` for the style request itself.
|
|
376
|
+
*
|
|
377
|
+
* Falls back nowhere: a network or auth failure throws (`ChrtError` /
|
|
378
|
+
* `TileTokenError`). For an offline-capable local fallback, use
|
|
379
|
+
* `chrt.style()` — the client-side generator.
|
|
380
|
+
*/
|
|
381
|
+
async fetchStyle(opts = {}) {
|
|
382
|
+
const token = await this.tileAuth.getToken(this.region);
|
|
383
|
+
const res = await fetch(this.styleEndpoint(opts.theme), {
|
|
384
|
+
headers: { authorization: `Bearer ${token.token}` },
|
|
385
|
+
signal: opts.signal
|
|
386
|
+
});
|
|
387
|
+
if (!res.ok) {
|
|
388
|
+
let body;
|
|
389
|
+
try {
|
|
390
|
+
body = await res.json();
|
|
391
|
+
} catch {
|
|
392
|
+
}
|
|
393
|
+
throw new ChrtError(res.status, body);
|
|
394
|
+
}
|
|
395
|
+
return await res.json();
|
|
346
396
|
}
|
|
347
397
|
/**
|
|
348
398
|
* Vector tile URL template for the configured region, served by the
|
|
@@ -1146,51 +1196,2409 @@ var OfflineEndpoint = class {
|
|
|
1146
1196
|
{ resumable: true, bytes_written: bytesDownloaded }
|
|
1147
1197
|
);
|
|
1148
1198
|
}
|
|
1149
|
-
emit(true);
|
|
1150
|
-
const computedSha = await fs.sha256(partialPath);
|
|
1151
|
-
if (computedSha.toLowerCase() !== bundle.sha256) {
|
|
1152
|
-
await fs.unlink(partialPath);
|
|
1153
|
-
throw new OfflineError(
|
|
1154
|
-
"download_corrupt",
|
|
1155
|
-
`SHA-256 mismatch for ${region}: expected ${bundle.sha256}, got ${computedSha}`,
|
|
1156
|
-
void 0,
|
|
1157
|
-
{ expected: bundle.sha256, actual: computedSha }
|
|
1199
|
+
emit(true);
|
|
1200
|
+
const computedSha = await fs.sha256(partialPath);
|
|
1201
|
+
if (computedSha.toLowerCase() !== bundle.sha256) {
|
|
1202
|
+
await fs.unlink(partialPath);
|
|
1203
|
+
throw new OfflineError(
|
|
1204
|
+
"download_corrupt",
|
|
1205
|
+
`SHA-256 mismatch for ${region}: expected ${bundle.sha256}, got ${computedSha}`,
|
|
1206
|
+
void 0,
|
|
1207
|
+
{ expected: bundle.sha256, actual: computedSha }
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
await fs.rename(partialPath, path);
|
|
1211
|
+
await this.persistManifestEntry(region, bundle, path, fs);
|
|
1212
|
+
return {
|
|
1213
|
+
path,
|
|
1214
|
+
bytes: bundle.bytes,
|
|
1215
|
+
sha256: bundle.sha256,
|
|
1216
|
+
build_id: bundle.build_id,
|
|
1217
|
+
build_tag: bundle.build_tag,
|
|
1218
|
+
resumed
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
async persistManifestEntry(region, bundle, path, fs) {
|
|
1222
|
+
if (!this.manifestPath) return;
|
|
1223
|
+
const manifest = await readManifest(this.manifestPath, fs);
|
|
1224
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
1225
|
+
manifest.regions[region] = {
|
|
1226
|
+
path,
|
|
1227
|
+
bytes: bundle.bytes,
|
|
1228
|
+
sha256: bundle.sha256,
|
|
1229
|
+
build_id: bundle.build_id,
|
|
1230
|
+
build_tag: bundle.build_tag,
|
|
1231
|
+
downloaded_at: nowSec,
|
|
1232
|
+
last_accessed_at: nowSec
|
|
1233
|
+
};
|
|
1234
|
+
await writeManifest(this.manifestPath, fs, manifest);
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
function dirname(p) {
|
|
1238
|
+
const i = p.lastIndexOf("/");
|
|
1239
|
+
const j = p.lastIndexOf("\\");
|
|
1240
|
+
const k = Math.max(i, j);
|
|
1241
|
+
if (k <= 0) return ".";
|
|
1242
|
+
return p.slice(0, k);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// src/interact/decode.ts
|
|
1246
|
+
var LITCHR_TABLE = {
|
|
1247
|
+
1: { name: "Fixed", abbr: "F" },
|
|
1248
|
+
2: { name: "Flashing", abbr: "Fl" },
|
|
1249
|
+
3: { name: "Long Flashing", abbr: "LFl" },
|
|
1250
|
+
4: { name: "Occulting", abbr: "Oc" },
|
|
1251
|
+
5: { name: "Isophase", abbr: "Iso" },
|
|
1252
|
+
6: { name: "Short Flashing", abbr: "S.Fl" },
|
|
1253
|
+
7: { name: "Short Long Flashing", abbr: "S.LFl" },
|
|
1254
|
+
8: { name: "Alternating Occulting", abbr: "Al.Oc" },
|
|
1255
|
+
9: { name: "Alternating Long Flashing", abbr: "Al.LFl" },
|
|
1256
|
+
10: { name: "Alternating Flashing", abbr: "Al.Fl" },
|
|
1257
|
+
11: { name: "Alternating Group", abbr: "Al.Gp" },
|
|
1258
|
+
12: { name: "Morse", abbr: "Mo" },
|
|
1259
|
+
13: { name: "Quick", abbr: "Q" },
|
|
1260
|
+
14: { name: "Very Quick", abbr: "VQ" },
|
|
1261
|
+
15: { name: "Ultra Quick", abbr: "UQ" },
|
|
1262
|
+
16: { name: "Alternating", abbr: "Al" },
|
|
1263
|
+
17: { name: "Alternating Fixed/Flash", abbr: "Al.F.Fl" },
|
|
1264
|
+
25: { name: "Quick + Long Flash", abbr: "Q+LFl" },
|
|
1265
|
+
26: { name: "Very Quick + Long Flash", abbr: "VQ+LFl" },
|
|
1266
|
+
27: { name: "Ultra Quick + Long Flash", abbr: "UQ+LFl" },
|
|
1267
|
+
28: { name: "Alternating", abbr: "Al" },
|
|
1268
|
+
29: { name: "Fixed + Long Flash", abbr: "F.LFl" }
|
|
1269
|
+
};
|
|
1270
|
+
var COLOUR_TABLE = {
|
|
1271
|
+
1: { name: "White", abbr: "W", hex: "#ffffff" },
|
|
1272
|
+
2: { name: "Black", abbr: "B", hex: "#333333" },
|
|
1273
|
+
3: { name: "Red", abbr: "R", hex: "#ef4444" },
|
|
1274
|
+
4: { name: "Green", abbr: "G", hex: "#22c55e" },
|
|
1275
|
+
5: { name: "Blue", abbr: "Bu", hex: "#3b82f6" },
|
|
1276
|
+
6: { name: "Yellow", abbr: "Y", hex: "#eab308" },
|
|
1277
|
+
7: { name: "Grey", abbr: "Gr", hex: "#9ca3af" },
|
|
1278
|
+
8: { name: "Brown", abbr: "Bn", hex: "#92400e" },
|
|
1279
|
+
9: { name: "Amber", abbr: "Am", hex: "#f59e0b" },
|
|
1280
|
+
10: { name: "Violet", abbr: "Vi", hex: "#8b5cf6" },
|
|
1281
|
+
11: { name: "Orange", abbr: "Or", hex: "#f97316" },
|
|
1282
|
+
12: { name: "Magenta", abbr: "Mg", hex: "#d946ef" },
|
|
1283
|
+
13: { name: "Pink", abbr: "Pk", hex: "#f472b6" }
|
|
1284
|
+
};
|
|
1285
|
+
var BOYSHP_TABLE = {
|
|
1286
|
+
1: "Conical (nun)",
|
|
1287
|
+
2: "Can (cylindrical)",
|
|
1288
|
+
3: "Spherical",
|
|
1289
|
+
4: "Pillar",
|
|
1290
|
+
5: "Spar",
|
|
1291
|
+
6: "Barrel",
|
|
1292
|
+
7: "Super buoy",
|
|
1293
|
+
8: "Ice buoy"
|
|
1294
|
+
};
|
|
1295
|
+
var CATLAM_TABLE = {
|
|
1296
|
+
1: "Port",
|
|
1297
|
+
2: "Starboard",
|
|
1298
|
+
3: "Preferred channel to starboard",
|
|
1299
|
+
4: "Preferred channel to port"
|
|
1300
|
+
};
|
|
1301
|
+
var CATCAM_TABLE = {
|
|
1302
|
+
1: "North cardinal",
|
|
1303
|
+
2: "East cardinal",
|
|
1304
|
+
3: "South cardinal",
|
|
1305
|
+
4: "West cardinal"
|
|
1306
|
+
};
|
|
1307
|
+
var CATLMK_TABLE = {
|
|
1308
|
+
1: "Cairn",
|
|
1309
|
+
2: "Cemetery",
|
|
1310
|
+
3: "Chimney",
|
|
1311
|
+
4: "Dish aerial",
|
|
1312
|
+
5: "Flagstaff",
|
|
1313
|
+
6: "Flare stack",
|
|
1314
|
+
7: "Mast",
|
|
1315
|
+
8: "Windsock",
|
|
1316
|
+
9: "Monument",
|
|
1317
|
+
10: "Column / pillar",
|
|
1318
|
+
11: "Memorial plaque",
|
|
1319
|
+
12: "Obelisk",
|
|
1320
|
+
13: "Statue",
|
|
1321
|
+
14: "Cross",
|
|
1322
|
+
15: "Dome",
|
|
1323
|
+
16: "Radar scanner",
|
|
1324
|
+
17: "Tower",
|
|
1325
|
+
18: "Windmill",
|
|
1326
|
+
19: "Wind turbine",
|
|
1327
|
+
20: "Spire"
|
|
1328
|
+
};
|
|
1329
|
+
var CATFOG_TABLE = {
|
|
1330
|
+
1: "Explosive",
|
|
1331
|
+
2: "Diaphone",
|
|
1332
|
+
3: "Siren",
|
|
1333
|
+
4: "Nautophone",
|
|
1334
|
+
5: "Bell",
|
|
1335
|
+
6: "Whistle",
|
|
1336
|
+
7: "Reed",
|
|
1337
|
+
8: "Gong",
|
|
1338
|
+
9: "Horn"
|
|
1339
|
+
};
|
|
1340
|
+
var CATFOG_ABBR_TABLE = {
|
|
1341
|
+
1: "EXPLOS",
|
|
1342
|
+
2: "DIA",
|
|
1343
|
+
3: "SIREN",
|
|
1344
|
+
4: "NAUTO",
|
|
1345
|
+
5: "BELL",
|
|
1346
|
+
6: "WHIS",
|
|
1347
|
+
7: "REED",
|
|
1348
|
+
8: "GONG",
|
|
1349
|
+
9: "HORN"
|
|
1350
|
+
};
|
|
1351
|
+
var TOPSHP_TABLE = {
|
|
1352
|
+
1: "Cone, point up",
|
|
1353
|
+
2: "Cone, point down",
|
|
1354
|
+
3: "Sphere",
|
|
1355
|
+
4: "2 spheres",
|
|
1356
|
+
5: "Cylinder",
|
|
1357
|
+
6: "Board",
|
|
1358
|
+
7: "X-shape",
|
|
1359
|
+
8: "Upright cross",
|
|
1360
|
+
9: "Cube, point up",
|
|
1361
|
+
10: "2 cones, point to point",
|
|
1362
|
+
11: "2 cones, base to base",
|
|
1363
|
+
12: "Rhombus",
|
|
1364
|
+
13: "2 cones, up",
|
|
1365
|
+
14: "2 cones, down",
|
|
1366
|
+
15: "Besom, point up",
|
|
1367
|
+
16: "Besom, point down",
|
|
1368
|
+
17: "Flag",
|
|
1369
|
+
18: "Sphere over rhombus",
|
|
1370
|
+
19: "Square",
|
|
1371
|
+
33: "T-shape"
|
|
1372
|
+
};
|
|
1373
|
+
var STATUS_TABLE = {
|
|
1374
|
+
1: "Permanent",
|
|
1375
|
+
2: "Occasional",
|
|
1376
|
+
3: "Recommended",
|
|
1377
|
+
4: "Not in use",
|
|
1378
|
+
5: "Periodic/intermittent",
|
|
1379
|
+
6: "Reserved",
|
|
1380
|
+
7: "Temporary",
|
|
1381
|
+
8: "Private",
|
|
1382
|
+
9: "Mandatory",
|
|
1383
|
+
11: "Extinguished",
|
|
1384
|
+
12: "Illuminated",
|
|
1385
|
+
13: "Historic",
|
|
1386
|
+
14: "Public",
|
|
1387
|
+
15: "Synchronized",
|
|
1388
|
+
16: "Watched",
|
|
1389
|
+
17: "Un-watched",
|
|
1390
|
+
18: "Existence doubtful"
|
|
1391
|
+
};
|
|
1392
|
+
var MARSYS_TABLE = {
|
|
1393
|
+
1: "IALA-A",
|
|
1394
|
+
2: "IALA-B"
|
|
1395
|
+
};
|
|
1396
|
+
var LAYER_TYPE_MAP = {
|
|
1397
|
+
BOYLAT: "Lateral Buoy",
|
|
1398
|
+
BOYCAR: "Cardinal Buoy",
|
|
1399
|
+
BOYSPP: "Special Purpose Buoy",
|
|
1400
|
+
BOYISD: "Isolated Danger Buoy",
|
|
1401
|
+
BOYSAW: "Safe Water Buoy",
|
|
1402
|
+
BCNLAT: "Lateral Beacon",
|
|
1403
|
+
BCNCAR: "Cardinal Beacon",
|
|
1404
|
+
BCNSPP: "Special Purpose Beacon",
|
|
1405
|
+
BCNISD: "Isolated Danger Beacon",
|
|
1406
|
+
BCNSAW: "Safe Water Beacon",
|
|
1407
|
+
LIGHTS: "Light",
|
|
1408
|
+
DAYMAR: "Daymark",
|
|
1409
|
+
FOGSIG: "Fog Signal",
|
|
1410
|
+
RTPBCN: "Radar Transponder Beacon",
|
|
1411
|
+
RDOSTA: "Radio/Radar Station",
|
|
1412
|
+
TOPMAR: "Topmark",
|
|
1413
|
+
MORFAC: "Mooring Facility",
|
|
1414
|
+
LNDMRK: "Landmark",
|
|
1415
|
+
SISTAT: "Signal Station",
|
|
1416
|
+
PILBOP: "Pilot Boarding Place",
|
|
1417
|
+
HRBFAC: "Harbour Facility",
|
|
1418
|
+
SMCFAC: "Small Craft Facility",
|
|
1419
|
+
CRANES: "Crane",
|
|
1420
|
+
OFSPLF: "Offshore Platform",
|
|
1421
|
+
ACHBRT: "Anchor Berth",
|
|
1422
|
+
DISMAR: "Distance Mark",
|
|
1423
|
+
LITVES: "Light Vessel",
|
|
1424
|
+
LITFLT: "Light Float"
|
|
1425
|
+
};
|
|
1426
|
+
var NAV_AID_QUERY_LAYERS = [
|
|
1427
|
+
"enc-BOYLAT",
|
|
1428
|
+
"enc-BOYCAR",
|
|
1429
|
+
"enc-BOYSPP",
|
|
1430
|
+
"enc-BOYISD",
|
|
1431
|
+
"enc-BOYSAW",
|
|
1432
|
+
"enc-BCNLAT",
|
|
1433
|
+
"enc-BCNCAR",
|
|
1434
|
+
"enc-BCNSPP",
|
|
1435
|
+
"enc-BCNISD",
|
|
1436
|
+
"enc-BCNSAW",
|
|
1437
|
+
"enc-LIGHTS",
|
|
1438
|
+
"enc-LIGHTS-point",
|
|
1439
|
+
"enc-DAYMAR",
|
|
1440
|
+
"enc-FOGSIG",
|
|
1441
|
+
"enc-RTPBCN",
|
|
1442
|
+
"enc-RDOSTA",
|
|
1443
|
+
"enc-TOPMAR",
|
|
1444
|
+
"enc-MORFAC",
|
|
1445
|
+
"enc-MORFAC-point",
|
|
1446
|
+
"enc-LNDMRK",
|
|
1447
|
+
"enc-LNDMRK-point",
|
|
1448
|
+
"enc-SISTAT",
|
|
1449
|
+
"enc-SISTAT-point",
|
|
1450
|
+
"enc-PILBOP",
|
|
1451
|
+
"enc-PILBOP-point",
|
|
1452
|
+
"enc-HRBFAC",
|
|
1453
|
+
"enc-SMCFAC",
|
|
1454
|
+
"enc-CRANES",
|
|
1455
|
+
"enc-CRANES-point",
|
|
1456
|
+
"enc-OFSPLF",
|
|
1457
|
+
"enc-OFSPLF-point",
|
|
1458
|
+
"enc-ACHBRT",
|
|
1459
|
+
"enc-DISMAR",
|
|
1460
|
+
"enc-LITVES",
|
|
1461
|
+
"enc-LITFLT"
|
|
1462
|
+
];
|
|
1463
|
+
var AREA_QUERY_LAYERS = [
|
|
1464
|
+
"enc-DEPARE",
|
|
1465
|
+
"enc-DEPARE-polygon",
|
|
1466
|
+
"enc-OBSTRN",
|
|
1467
|
+
"enc-OBSTRN-polygon",
|
|
1468
|
+
"enc-OBSTRN-point",
|
|
1469
|
+
"enc-WRECKS",
|
|
1470
|
+
"enc-WRECKS-polygon",
|
|
1471
|
+
"enc-WRECKS-point",
|
|
1472
|
+
"enc-RESARE",
|
|
1473
|
+
"enc-RESARE-polygon",
|
|
1474
|
+
"enc-ACHARE",
|
|
1475
|
+
"enc-ACHARE-polygon",
|
|
1476
|
+
"enc-CTNARE",
|
|
1477
|
+
"enc-CTNARE-polygon",
|
|
1478
|
+
"enc-DRGARE",
|
|
1479
|
+
"enc-DRGARE-polygon",
|
|
1480
|
+
"enc-FAIRWY",
|
|
1481
|
+
"enc-FAIRWY-polygon",
|
|
1482
|
+
"enc-DMPGRD",
|
|
1483
|
+
"enc-DMPGRD-polygon",
|
|
1484
|
+
"enc-MIPARE",
|
|
1485
|
+
"enc-MIPARE-polygon",
|
|
1486
|
+
"enc-PRCARE",
|
|
1487
|
+
"enc-PRCARE-polygon",
|
|
1488
|
+
"enc-TSSLPT",
|
|
1489
|
+
"enc-TSSLPT-polygon",
|
|
1490
|
+
"enc-TSELNE",
|
|
1491
|
+
"enc-TSELNE-line",
|
|
1492
|
+
"enc-ISTZNE",
|
|
1493
|
+
"enc-ISTZNE-polygon",
|
|
1494
|
+
"enc-OSPARE",
|
|
1495
|
+
"enc-OSPARE-polygon",
|
|
1496
|
+
"enc-MARCUL",
|
|
1497
|
+
"enc-MARCUL-polygon",
|
|
1498
|
+
"enc-CBLARE",
|
|
1499
|
+
"enc-CBLARE-polygon",
|
|
1500
|
+
"enc-PIPARE",
|
|
1501
|
+
"enc-PIPARE-polygon",
|
|
1502
|
+
// Tier 3: infrastructure lines (BRIDGE handled by BRIDGE_QUERY_LAYERS)
|
|
1503
|
+
"enc-CBLSUB",
|
|
1504
|
+
"enc-CBLSUB-line",
|
|
1505
|
+
"enc-CBLOHD",
|
|
1506
|
+
"enc-CBLOHD-line",
|
|
1507
|
+
"enc-PIPSOL",
|
|
1508
|
+
"enc-PIPSOL-line",
|
|
1509
|
+
"enc-PIPOHD",
|
|
1510
|
+
"enc-PIPOHD-line",
|
|
1511
|
+
"enc-BERTHS",
|
|
1512
|
+
"enc-BERTHS-line",
|
|
1513
|
+
"enc-DAMCON",
|
|
1514
|
+
"enc-DAMCON-line",
|
|
1515
|
+
"enc-FERYRT",
|
|
1516
|
+
"enc-FERYRT-line",
|
|
1517
|
+
"enc-RECTRC",
|
|
1518
|
+
"enc-RECTRC-line",
|
|
1519
|
+
// Tier 4: land/context features
|
|
1520
|
+
"enc-BUISGL",
|
|
1521
|
+
"enc-BUISGL-polygon",
|
|
1522
|
+
"enc-BUISGL-point",
|
|
1523
|
+
"enc-SILTNK",
|
|
1524
|
+
"enc-SILTNK-polygon",
|
|
1525
|
+
"enc-SILTNK-point",
|
|
1526
|
+
"enc-HULKES",
|
|
1527
|
+
"enc-HULKES-polygon",
|
|
1528
|
+
"enc-HULKES-point",
|
|
1529
|
+
"enc-DRYDOC",
|
|
1530
|
+
"enc-DRYDOC-polygon",
|
|
1531
|
+
"enc-FLODOC",
|
|
1532
|
+
"enc-FLODOC-polygon",
|
|
1533
|
+
"enc-PONTON",
|
|
1534
|
+
"enc-PONTON-polygon",
|
|
1535
|
+
"enc-GATCON",
|
|
1536
|
+
"enc-GATCON-polygon",
|
|
1537
|
+
"enc-GATCON-point",
|
|
1538
|
+
"enc-LOGPON",
|
|
1539
|
+
"enc-LOGPON-polygon",
|
|
1540
|
+
"enc-AIRARE",
|
|
1541
|
+
"enc-AIRARE-polygon",
|
|
1542
|
+
"enc-SLCONS",
|
|
1543
|
+
"enc-SLCONS-polygon",
|
|
1544
|
+
"enc-SLCONS-line",
|
|
1545
|
+
"enc-SLCONS-point",
|
|
1546
|
+
// Tier 5: hydrographic context
|
|
1547
|
+
"enc-COALNE",
|
|
1548
|
+
"enc-COALNE-line",
|
|
1549
|
+
"enc-DEPCNT",
|
|
1550
|
+
"enc-DEPCNT-line",
|
|
1551
|
+
"enc-RIVERS",
|
|
1552
|
+
"enc-RIVERS-polygon",
|
|
1553
|
+
"enc-RIVERS-line",
|
|
1554
|
+
"enc-CANALS",
|
|
1555
|
+
"enc-CANALS-polygon",
|
|
1556
|
+
"enc-CANALS-line",
|
|
1557
|
+
"enc-LAKARE",
|
|
1558
|
+
"enc-LAKARE-polygon",
|
|
1559
|
+
"enc-RAPIDS",
|
|
1560
|
+
"enc-RAPIDS-polygon",
|
|
1561
|
+
"enc-RAPIDS-point",
|
|
1562
|
+
"enc-WATFAL",
|
|
1563
|
+
"enc-WATFAL-polygon",
|
|
1564
|
+
"enc-WATFAL-point",
|
|
1565
|
+
"enc-LNDARE",
|
|
1566
|
+
"enc-LNDARE-polygon"
|
|
1567
|
+
];
|
|
1568
|
+
var BRIDGE_QUERY_LAYERS = [
|
|
1569
|
+
"enc-BRIDGE-midpoint",
|
|
1570
|
+
"enc-BRIDGE",
|
|
1571
|
+
"enc-BRIDGE-line",
|
|
1572
|
+
"s52-BRIDGE-polygon",
|
|
1573
|
+
"s52-BRIDGE-polygon-outline"
|
|
1574
|
+
];
|
|
1575
|
+
var CATBRG_LABELS = {
|
|
1576
|
+
1: "Fixed bridge",
|
|
1577
|
+
2: "Opening bridge",
|
|
1578
|
+
3: "Swing bridge",
|
|
1579
|
+
4: "Lifting bridge",
|
|
1580
|
+
5: "Bascule bridge",
|
|
1581
|
+
6: "Pontoon bridge",
|
|
1582
|
+
7: "Drawbridge",
|
|
1583
|
+
8: "Transporter bridge",
|
|
1584
|
+
9: "Footbridge",
|
|
1585
|
+
10: "Viaduct",
|
|
1586
|
+
11: "Aqueduct",
|
|
1587
|
+
12: "Suspension bridge"
|
|
1588
|
+
};
|
|
1589
|
+
var CONDTN_TABLE = {
|
|
1590
|
+
1: "Under construction",
|
|
1591
|
+
2: "Ruined",
|
|
1592
|
+
3: "Under reclamation",
|
|
1593
|
+
5: "Planned"
|
|
1594
|
+
};
|
|
1595
|
+
function parseFirstNum(val) {
|
|
1596
|
+
if (val == null) return null;
|
|
1597
|
+
const s = String(val);
|
|
1598
|
+
if (s.startsWith("[")) {
|
|
1599
|
+
try {
|
|
1600
|
+
const arr = JSON.parse(s);
|
|
1601
|
+
if (Array.isArray(arr)) {
|
|
1602
|
+
const n2 = Number(arr[0]);
|
|
1603
|
+
return Number.isFinite(n2) ? n2 : null;
|
|
1604
|
+
}
|
|
1605
|
+
} catch {
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
const n = Number(s.split(",")[0]);
|
|
1609
|
+
return Number.isFinite(n) ? n : null;
|
|
1610
|
+
}
|
|
1611
|
+
function asNumber(val) {
|
|
1612
|
+
if (val == null || val === "") return null;
|
|
1613
|
+
const n = Number(val);
|
|
1614
|
+
return Number.isFinite(n) ? n : null;
|
|
1615
|
+
}
|
|
1616
|
+
function decodeBridgeProps(props) {
|
|
1617
|
+
const catCode = parseFirstNum(props.CATBRG);
|
|
1618
|
+
const statusCode = parseFirstNum(props.STATUS);
|
|
1619
|
+
const condCode = parseFirstNum(props.CONDTN);
|
|
1620
|
+
return {
|
|
1621
|
+
name: props.OBJNAM ? String(props.OBJNAM) : null,
|
|
1622
|
+
categoryCode: catCode,
|
|
1623
|
+
categoryLabel: catCode ? CATBRG_LABELS[catCode] ?? null : null,
|
|
1624
|
+
verticalClearance: asNumber(props.VERCLR),
|
|
1625
|
+
verticalClearanceClosed: asNumber(props.VERCCL),
|
|
1626
|
+
verticalClearanceOpen: asNumber(props.VERCOP),
|
|
1627
|
+
horizontalClearance: asNumber(props.HORCLR),
|
|
1628
|
+
status: statusCode ? STATUS_TABLE[statusCode] ?? null : null,
|
|
1629
|
+
condition: condCode ? CONDTN_TABLE[condCode] ?? null : null,
|
|
1630
|
+
information: props.INFORM ? String(props.INFORM) : null,
|
|
1631
|
+
nationalName: props.NOBJNM ? String(props.NOBJNM) : null,
|
|
1632
|
+
sourceIndication: props.SORIND ? String(props.SORIND) : null,
|
|
1633
|
+
sourceDate: props.SORDAT ? String(props.SORDAT) : null
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
var CATOBS_TABLE = {
|
|
1637
|
+
1: "Snag / stump",
|
|
1638
|
+
2: "Wellhead",
|
|
1639
|
+
3: "Diffuser",
|
|
1640
|
+
4: "Crib",
|
|
1641
|
+
5: "Fish haven",
|
|
1642
|
+
6: "Foul area",
|
|
1643
|
+
7: "Foul ground",
|
|
1644
|
+
8: "Ice boom",
|
|
1645
|
+
9: "Ground tackle",
|
|
1646
|
+
10: "Boom",
|
|
1647
|
+
11: "Platform"
|
|
1648
|
+
};
|
|
1649
|
+
var CATWRK_TABLE = {
|
|
1650
|
+
1: "Non-dangerous",
|
|
1651
|
+
2: "Dangerous",
|
|
1652
|
+
3: "Distributed remains",
|
|
1653
|
+
4: "Mast showing",
|
|
1654
|
+
5: "Hull showing"
|
|
1655
|
+
};
|
|
1656
|
+
var WATLEV_TABLE = {
|
|
1657
|
+
1: "Partly submerged at high water",
|
|
1658
|
+
2: "Always dry",
|
|
1659
|
+
3: "Always submerged",
|
|
1660
|
+
4: "Covers and uncovers",
|
|
1661
|
+
5: "Awash",
|
|
1662
|
+
6: "Subject to inundation / flooding",
|
|
1663
|
+
7: "Floating"
|
|
1664
|
+
};
|
|
1665
|
+
var CATREA_TABLE = {
|
|
1666
|
+
1: "Offshore safety zone",
|
|
1667
|
+
2: "Anchoring prohibition",
|
|
1668
|
+
3: "Fishing prohibition",
|
|
1669
|
+
4: "Nature reserve",
|
|
1670
|
+
5: "Bird sanctuary",
|
|
1671
|
+
6: "Game preserve",
|
|
1672
|
+
7: "Seal sanctuary",
|
|
1673
|
+
8: "Degaussing range",
|
|
1674
|
+
9: "Military area",
|
|
1675
|
+
10: "Historic wreck",
|
|
1676
|
+
12: "Navigational aid safety zone",
|
|
1677
|
+
14: "No wake zone",
|
|
1678
|
+
18: "Swimming area",
|
|
1679
|
+
19: "Waiting area",
|
|
1680
|
+
20: "Research area",
|
|
1681
|
+
21: "Dredging area",
|
|
1682
|
+
22: "Fish sanctuary",
|
|
1683
|
+
23: "Ecological reserve",
|
|
1684
|
+
24: "No discharge zone",
|
|
1685
|
+
25: "No anchoring area",
|
|
1686
|
+
26: "No fishing area",
|
|
1687
|
+
27: "Speed restriction zone",
|
|
1688
|
+
28: "To be avoided area"
|
|
1689
|
+
};
|
|
1690
|
+
var RESTRN_TABLE = {
|
|
1691
|
+
1: "Anchoring prohibited",
|
|
1692
|
+
2: "Anchoring restricted",
|
|
1693
|
+
3: "Fishing prohibited",
|
|
1694
|
+
4: "Fishing restricted",
|
|
1695
|
+
5: "Trawling prohibited",
|
|
1696
|
+
6: "Trawling restricted",
|
|
1697
|
+
7: "Entry prohibited",
|
|
1698
|
+
8: "Entry restricted",
|
|
1699
|
+
9: "Dredging prohibited",
|
|
1700
|
+
10: "Dredging restricted",
|
|
1701
|
+
11: "Diving prohibited",
|
|
1702
|
+
12: "Diving restricted",
|
|
1703
|
+
13: "No wake",
|
|
1704
|
+
14: "To be avoided",
|
|
1705
|
+
15: "Construction prohibited",
|
|
1706
|
+
16: "Discharging prohibited",
|
|
1707
|
+
17: "Discharging restricted",
|
|
1708
|
+
18: "Industrial prohibited",
|
|
1709
|
+
19: "Metallic hull vessels prohibited",
|
|
1710
|
+
20: "Navigating prohibited",
|
|
1711
|
+
21: "Speed restricted",
|
|
1712
|
+
24: "Stopping prohibited",
|
|
1713
|
+
25: "Landing prohibited"
|
|
1714
|
+
};
|
|
1715
|
+
var CATHAF_TABLE = {
|
|
1716
|
+
1: "RoRo terminal",
|
|
1717
|
+
3: "Ferry terminal",
|
|
1718
|
+
4: "Fishing harbour",
|
|
1719
|
+
5: "Yacht harbour / marina",
|
|
1720
|
+
6: "Naval base",
|
|
1721
|
+
7: "Tanker terminal",
|
|
1722
|
+
8: "Passenger terminal",
|
|
1723
|
+
9: "Shipyard",
|
|
1724
|
+
10: "Container terminal",
|
|
1725
|
+
11: "Bulk terminal",
|
|
1726
|
+
12: "Synchrolift",
|
|
1727
|
+
13: "Straddle carrier",
|
|
1728
|
+
14: "Port terminal"
|
|
1729
|
+
};
|
|
1730
|
+
var CATSCF_TABLE = {
|
|
1731
|
+
1: "Visitors' berth",
|
|
1732
|
+
2: "Nautical club",
|
|
1733
|
+
3: "Boat hoist",
|
|
1734
|
+
4: "Sailmaker",
|
|
1735
|
+
5: "Boatyard",
|
|
1736
|
+
6: "Public inn",
|
|
1737
|
+
7: "Restaurant",
|
|
1738
|
+
8: "Chandler",
|
|
1739
|
+
9: "Provisions",
|
|
1740
|
+
10: "Doctor",
|
|
1741
|
+
11: "Pharmacy",
|
|
1742
|
+
12: "Water tap",
|
|
1743
|
+
13: "Fuel station",
|
|
1744
|
+
14: "Electricity",
|
|
1745
|
+
15: "Bottle gas",
|
|
1746
|
+
16: "Showers",
|
|
1747
|
+
17: "Launderette",
|
|
1748
|
+
18: "Public toilets",
|
|
1749
|
+
19: "Post box"
|
|
1750
|
+
};
|
|
1751
|
+
var CATCRN_TABLE = {
|
|
1752
|
+
2: "Container crane / gantry",
|
|
1753
|
+
3: "Sheerlegs",
|
|
1754
|
+
4: "Travelling crane"
|
|
1755
|
+
};
|
|
1756
|
+
var CATACH_TABLE = {
|
|
1757
|
+
1: "Unrestricted anchorage",
|
|
1758
|
+
2: "Deep water anchorage",
|
|
1759
|
+
3: "Tanker anchorage",
|
|
1760
|
+
4: "Explosives anchorage",
|
|
1761
|
+
5: "Quarantine anchorage",
|
|
1762
|
+
6: "Sea-plane anchorage",
|
|
1763
|
+
7: "Small craft anchorage",
|
|
1764
|
+
8: "Small craft mooring area",
|
|
1765
|
+
9: "Anchorage for 24 hours or less"
|
|
1766
|
+
};
|
|
1767
|
+
var CATDPG_TABLE = {
|
|
1768
|
+
1: "General dumping ground",
|
|
1769
|
+
2: "Chemical waste",
|
|
1770
|
+
3: "Nuclear waste",
|
|
1771
|
+
4: "Explosives",
|
|
1772
|
+
5: "Spoil ground",
|
|
1773
|
+
6: "Vessel dumping ground"
|
|
1774
|
+
};
|
|
1775
|
+
var CATMFA_TABLE = {
|
|
1776
|
+
1: "Crustaceans",
|
|
1777
|
+
2: "Oysters / mussels",
|
|
1778
|
+
3: "Fish",
|
|
1779
|
+
4: "Seaweed",
|
|
1780
|
+
5: "Pearl culture"
|
|
1781
|
+
};
|
|
1782
|
+
var CATCBL_TABLE = {
|
|
1783
|
+
1: "Submarine power cable",
|
|
1784
|
+
2: "Submarine transmission cable",
|
|
1785
|
+
3: "Submarine telegraph cable",
|
|
1786
|
+
4: "Submarine telephone cable",
|
|
1787
|
+
5: "Mooring cable",
|
|
1788
|
+
6: "Ferry cable"
|
|
1789
|
+
};
|
|
1790
|
+
var CATPIP_TABLE = {
|
|
1791
|
+
1: "Pipeline",
|
|
1792
|
+
2: "Outfall pipe",
|
|
1793
|
+
3: "Intake pipe",
|
|
1794
|
+
4: "Sewer",
|
|
1795
|
+
5: "Bubbler system",
|
|
1796
|
+
6: "Supply pipe"
|
|
1797
|
+
};
|
|
1798
|
+
var TRAFIC_TABLE = {
|
|
1799
|
+
1: "Inbound",
|
|
1800
|
+
2: "Outbound",
|
|
1801
|
+
3: "One-way",
|
|
1802
|
+
4: "Two-way"
|
|
1803
|
+
};
|
|
1804
|
+
var QUASOU_TABLE = {
|
|
1805
|
+
1: "Depth known",
|
|
1806
|
+
2: "Depth unknown",
|
|
1807
|
+
3: "Doubtful sounding",
|
|
1808
|
+
4: "Unreliable sounding",
|
|
1809
|
+
6: "Least depth known",
|
|
1810
|
+
7: "Least depth unknown",
|
|
1811
|
+
9: "Not regularly maintained",
|
|
1812
|
+
10: "Maintained depth",
|
|
1813
|
+
11: "Not surveyed"
|
|
1814
|
+
};
|
|
1815
|
+
function decodeAreaFeatureProps(layerName, props) {
|
|
1816
|
+
const rawLayer = layerName.replace(/^enc-/, "").replace(/-(point|line|polygon|midpoint)$/, "");
|
|
1817
|
+
const parseCodeLocal = (val) => {
|
|
1818
|
+
if (val == null) return null;
|
|
1819
|
+
const s = String(val);
|
|
1820
|
+
try {
|
|
1821
|
+
const arr = JSON.parse(s);
|
|
1822
|
+
if (Array.isArray(arr)) return Number(arr[0]) || null;
|
|
1823
|
+
} catch {
|
|
1824
|
+
}
|
|
1825
|
+
return Number(s.split(",")[0]) || null;
|
|
1826
|
+
};
|
|
1827
|
+
const entries = [];
|
|
1828
|
+
let type = rawLayer;
|
|
1829
|
+
let title = props.OBJNAM ? String(props.OBJNAM) : "";
|
|
1830
|
+
if (rawLayer === "DEPARE") {
|
|
1831
|
+
type = "Depth Area";
|
|
1832
|
+
const drval1 = props.DRVAL1 != null ? Number(props.DRVAL1) : null;
|
|
1833
|
+
const drval2 = props.DRVAL2 != null ? Number(props.DRVAL2) : null;
|
|
1834
|
+
if (drval1 != null && drval2 != null) {
|
|
1835
|
+
title = title || `${drval1}m \u2013 ${drval2}m`;
|
|
1836
|
+
entries.push({ label: "Depth Range", value: `${drval1}m \u2013 ${drval2}m` });
|
|
1837
|
+
} else if (drval1 != null) {
|
|
1838
|
+
entries.push({ label: "Min Depth", value: `${drval1}m` });
|
|
1839
|
+
}
|
|
1840
|
+
const qc = parseCodeLocal(props.QUASOU);
|
|
1841
|
+
if (qc && QUASOU_TABLE[qc]) entries.push({ label: "Quality", value: QUASOU_TABLE[qc] });
|
|
1842
|
+
} else if (rawLayer === "OBSTRN") {
|
|
1843
|
+
type = "Obstruction";
|
|
1844
|
+
const cat = parseCodeLocal(props.CATOBS);
|
|
1845
|
+
if (cat && CATOBS_TABLE[cat]) {
|
|
1846
|
+
entries.push({ label: "Category", value: CATOBS_TABLE[cat] });
|
|
1847
|
+
if (!title) title = CATOBS_TABLE[cat];
|
|
1848
|
+
}
|
|
1849
|
+
if (props.VALSOU != null) entries.push({ label: "Depth", value: `${props.VALSOU}m` });
|
|
1850
|
+
const wl = parseCodeLocal(props.WATLEV);
|
|
1851
|
+
if (wl && WATLEV_TABLE[wl]) entries.push({ label: "Water Level", value: WATLEV_TABLE[wl] });
|
|
1852
|
+
const qc = parseCodeLocal(props.QUASOU);
|
|
1853
|
+
if (qc && QUASOU_TABLE[qc]) entries.push({ label: "Sounding Quality", value: QUASOU_TABLE[qc] });
|
|
1854
|
+
} else if (rawLayer === "WRECKS") {
|
|
1855
|
+
type = "Wreck";
|
|
1856
|
+
const cat = parseCodeLocal(props.CATWRK);
|
|
1857
|
+
if (cat && CATWRK_TABLE[cat]) {
|
|
1858
|
+
entries.push({ label: "Category", value: CATWRK_TABLE[cat] });
|
|
1859
|
+
if (!title) title = CATWRK_TABLE[cat];
|
|
1860
|
+
}
|
|
1861
|
+
if (props.VALSOU != null) entries.push({ label: "Depth", value: `${props.VALSOU}m` });
|
|
1862
|
+
const wl = parseCodeLocal(props.WATLEV);
|
|
1863
|
+
if (wl && WATLEV_TABLE[wl]) entries.push({ label: "Water Level", value: WATLEV_TABLE[wl] });
|
|
1864
|
+
const qc = parseCodeLocal(props.QUASOU);
|
|
1865
|
+
if (qc && QUASOU_TABLE[qc]) entries.push({ label: "Sounding Quality", value: QUASOU_TABLE[qc] });
|
|
1866
|
+
} else if (rawLayer === "RESARE") {
|
|
1867
|
+
type = "Restricted Area";
|
|
1868
|
+
const cat = parseCodeLocal(props.CATREA);
|
|
1869
|
+
if (cat && CATREA_TABLE[cat]) {
|
|
1870
|
+
entries.push({ label: "Category", value: CATREA_TABLE[cat] });
|
|
1871
|
+
if (!title) title = CATREA_TABLE[cat];
|
|
1872
|
+
}
|
|
1873
|
+
if (props.RESTRN) {
|
|
1874
|
+
const codes = parseListCodes(props.RESTRN).filter((n) => RESTRN_TABLE[n]);
|
|
1875
|
+
if (codes.length > 0) {
|
|
1876
|
+
entries.push({ label: "Restrictions", value: codes.map((c) => RESTRN_TABLE[c]).join("; ") });
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
} else if (rawLayer === "ACHARE") {
|
|
1880
|
+
type = "Anchorage Area";
|
|
1881
|
+
const cat = parseCodeLocal(props.CATACH);
|
|
1882
|
+
if (cat && CATACH_TABLE[cat]) {
|
|
1883
|
+
entries.push({ label: "Category", value: CATACH_TABLE[cat] });
|
|
1884
|
+
if (!title) title = CATACH_TABLE[cat];
|
|
1885
|
+
}
|
|
1886
|
+
if (props.RESTRN) {
|
|
1887
|
+
const codes = parseListCodes(props.RESTRN).filter((n) => RESTRN_TABLE[n]);
|
|
1888
|
+
if (codes.length > 0) entries.push({ label: "Restrictions", value: codes.map((c) => RESTRN_TABLE[c]).join("; ") });
|
|
1889
|
+
}
|
|
1890
|
+
const st2 = parseCodeLocal(props.STATUS);
|
|
1891
|
+
if (st2 && STATUS_TABLE[st2]) entries.push({ label: "Status", value: STATUS_TABLE[st2] });
|
|
1892
|
+
} else if (rawLayer === "CTNARE") {
|
|
1893
|
+
type = "Caution Area";
|
|
1894
|
+
} else if (rawLayer === "DRGARE") {
|
|
1895
|
+
type = "Dredged Area";
|
|
1896
|
+
const drval1 = props.DRVAL1 != null ? Number(props.DRVAL1) : null;
|
|
1897
|
+
const drval2 = props.DRVAL2 != null ? Number(props.DRVAL2) : null;
|
|
1898
|
+
if (drval1 != null && drval2 != null) {
|
|
1899
|
+
entries.push({ label: "Maintained Depth", value: `${drval1}m \u2013 ${drval2}m` });
|
|
1900
|
+
} else if (drval1 != null) {
|
|
1901
|
+
entries.push({ label: "Maintained Depth", value: `${drval1}m` });
|
|
1902
|
+
}
|
|
1903
|
+
const qc = parseCodeLocal(props.QUASOU);
|
|
1904
|
+
if (qc && QUASOU_TABLE[qc]) entries.push({ label: "Quality", value: QUASOU_TABLE[qc] });
|
|
1905
|
+
} else if (rawLayer === "FAIRWY") {
|
|
1906
|
+
type = "Fairway";
|
|
1907
|
+
const trafic = parseCodeLocal(props.TRAFIC);
|
|
1908
|
+
if (trafic && TRAFIC_TABLE[trafic]) entries.push({ label: "Traffic", value: TRAFIC_TABLE[trafic] });
|
|
1909
|
+
if (props.ORIENT != null) entries.push({ label: "Orientation", value: `${props.ORIENT}\xB0` });
|
|
1910
|
+
} else if (rawLayer === "DMPGRD") {
|
|
1911
|
+
type = "Dumping Ground";
|
|
1912
|
+
const cat = parseCodeLocal(props.CATDPG);
|
|
1913
|
+
if (cat && CATDPG_TABLE[cat]) {
|
|
1914
|
+
entries.push({ label: "Category", value: CATDPG_TABLE[cat] });
|
|
1915
|
+
if (!title) title = CATDPG_TABLE[cat];
|
|
1916
|
+
}
|
|
1917
|
+
} else if (rawLayer === "MIPARE") {
|
|
1918
|
+
type = "Military Practice Area";
|
|
1919
|
+
if (props.RESTRN) {
|
|
1920
|
+
const codes = parseListCodes(props.RESTRN).filter((n) => RESTRN_TABLE[n]);
|
|
1921
|
+
if (codes.length > 0) entries.push({ label: "Restrictions", value: codes.map((c) => RESTRN_TABLE[c]).join("; ") });
|
|
1922
|
+
}
|
|
1923
|
+
} else if (rawLayer === "PRCARE") {
|
|
1924
|
+
type = "Precautionary Area";
|
|
1925
|
+
const trafic = parseCodeLocal(props.TRAFIC);
|
|
1926
|
+
if (trafic && TRAFIC_TABLE[trafic]) entries.push({ label: "Traffic", value: TRAFIC_TABLE[trafic] });
|
|
1927
|
+
if (props.ORIENT != null) entries.push({ label: "Orientation", value: `${props.ORIENT}\xB0` });
|
|
1928
|
+
} else if (rawLayer === "TSSLPT") {
|
|
1929
|
+
type = "TSS Lane";
|
|
1930
|
+
const trafic = parseCodeLocal(props.TRAFIC);
|
|
1931
|
+
if (trafic && TRAFIC_TABLE[trafic]) entries.push({ label: "Traffic", value: TRAFIC_TABLE[trafic] });
|
|
1932
|
+
if (props.ORIENT != null) entries.push({ label: "Orientation", value: `${props.ORIENT}\xB0` });
|
|
1933
|
+
} else if (rawLayer === "TSELNE") {
|
|
1934
|
+
type = "TSS Boundary";
|
|
1935
|
+
} else if (rawLayer === "ISTZNE") {
|
|
1936
|
+
type = "Inshore Traffic Zone";
|
|
1937
|
+
} else if (rawLayer === "OSPARE") {
|
|
1938
|
+
type = "Offshore Production Area";
|
|
1939
|
+
if (props.RESTRN) {
|
|
1940
|
+
const codes = parseListCodes(props.RESTRN).filter((n) => RESTRN_TABLE[n]);
|
|
1941
|
+
if (codes.length > 0) entries.push({ label: "Restrictions", value: codes.map((c) => RESTRN_TABLE[c]).join("; ") });
|
|
1942
|
+
}
|
|
1943
|
+
} else if (rawLayer === "MARCUL") {
|
|
1944
|
+
type = "Marine Farm / Culture";
|
|
1945
|
+
const cat = parseCodeLocal(props.CATMFA);
|
|
1946
|
+
if (cat && CATMFA_TABLE[cat]) {
|
|
1947
|
+
entries.push({ label: "Category", value: CATMFA_TABLE[cat] });
|
|
1948
|
+
if (!title) title = CATMFA_TABLE[cat];
|
|
1949
|
+
}
|
|
1950
|
+
if (props.RESTRN) {
|
|
1951
|
+
const codes = parseListCodes(props.RESTRN).filter((n) => RESTRN_TABLE[n]);
|
|
1952
|
+
if (codes.length > 0) entries.push({ label: "Restrictions", value: codes.map((c) => RESTRN_TABLE[c]).join("; ") });
|
|
1953
|
+
}
|
|
1954
|
+
} else if (rawLayer === "CBLARE") {
|
|
1955
|
+
type = "Cable Area";
|
|
1956
|
+
const cat = parseCodeLocal(props.CATCBL);
|
|
1957
|
+
if (cat && CATCBL_TABLE[cat]) {
|
|
1958
|
+
entries.push({ label: "Category", value: CATCBL_TABLE[cat] });
|
|
1959
|
+
if (!title) title = `${CATCBL_TABLE[cat]} area`;
|
|
1960
|
+
}
|
|
1961
|
+
const st2 = parseCodeLocal(props.STATUS);
|
|
1962
|
+
if (st2 && STATUS_TABLE[st2]) entries.push({ label: "Status", value: STATUS_TABLE[st2] });
|
|
1963
|
+
if (props.RESTRN) {
|
|
1964
|
+
const codes = parseListCodes(props.RESTRN).filter((n) => RESTRN_TABLE[n]);
|
|
1965
|
+
if (codes.length > 0) entries.push({ label: "Restrictions", value: codes.map((c) => RESTRN_TABLE[c]).join("; ") });
|
|
1966
|
+
}
|
|
1967
|
+
} else if (rawLayer === "PIPARE") {
|
|
1968
|
+
type = "Pipeline Area";
|
|
1969
|
+
const cat = parseCodeLocal(props.CATPIP);
|
|
1970
|
+
if (cat && CATPIP_TABLE[cat]) {
|
|
1971
|
+
entries.push({ label: "Category", value: CATPIP_TABLE[cat] });
|
|
1972
|
+
if (!title) title = `${CATPIP_TABLE[cat]} area`;
|
|
1973
|
+
}
|
|
1974
|
+
const st2 = parseCodeLocal(props.STATUS);
|
|
1975
|
+
if (st2 && STATUS_TABLE[st2]) entries.push({ label: "Status", value: STATUS_TABLE[st2] });
|
|
1976
|
+
if (props.RESTRN) {
|
|
1977
|
+
const codes = parseListCodes(props.RESTRN).filter((n) => RESTRN_TABLE[n]);
|
|
1978
|
+
if (codes.length > 0) entries.push({ label: "Restrictions", value: codes.map((c) => RESTRN_TABLE[c]).join("; ") });
|
|
1979
|
+
}
|
|
1980
|
+
} else if (rawLayer === "BRIDGE") {
|
|
1981
|
+
const CATBRG_SHORT = {
|
|
1982
|
+
1: "Fixed",
|
|
1983
|
+
2: "Opening",
|
|
1984
|
+
3: "Swing",
|
|
1985
|
+
4: "Lifting",
|
|
1986
|
+
5: "Bascule",
|
|
1987
|
+
6: "Pontoon",
|
|
1988
|
+
7: "Draw",
|
|
1989
|
+
8: "Transporter",
|
|
1990
|
+
9: "Footbridge",
|
|
1991
|
+
10: "Viaduct",
|
|
1992
|
+
11: "Aqueduct",
|
|
1993
|
+
12: "Suspension"
|
|
1994
|
+
};
|
|
1995
|
+
const NATCON_TABLE = {
|
|
1996
|
+
1: "Masonry",
|
|
1997
|
+
2: "Concrete",
|
|
1998
|
+
3: "Loose boulders",
|
|
1999
|
+
4: "Hard-surfaced",
|
|
2000
|
+
5: "Unsurfaced",
|
|
2001
|
+
6: "Wooden",
|
|
2002
|
+
7: "Metal",
|
|
2003
|
+
8: "GRP"
|
|
2004
|
+
};
|
|
2005
|
+
const CONDTN_SHORT = {
|
|
2006
|
+
1: "Under construction",
|
|
2007
|
+
2: "Ruined",
|
|
2008
|
+
3: "Under reclamation",
|
|
2009
|
+
5: "Planned"
|
|
2010
|
+
};
|
|
2011
|
+
const catCode = parseCodeLocal(props.CATBRG);
|
|
2012
|
+
const catName = catCode ? CATBRG_SHORT[catCode] : null;
|
|
2013
|
+
type = catName ? `${catName} Bridge` : "Bridge";
|
|
2014
|
+
if (catName) entries.push({ label: "Category", value: catName });
|
|
2015
|
+
if (props.VERCLR != null) entries.push({ label: "Vertical Clearance", value: `${props.VERCLR} m` });
|
|
2016
|
+
if (props.VERCCL != null) entries.push({ label: "Clearance (closed)", value: `${props.VERCCL} m` });
|
|
2017
|
+
if (props.VERCOP != null) entries.push({ label: "Clearance (open)", value: `${props.VERCOP} m` });
|
|
2018
|
+
if (props.HORCLR != null) entries.push({ label: "Horizontal Clearance", value: `${props.HORCLR} m` });
|
|
2019
|
+
const natCode = parseCodeLocal(props.NATCON);
|
|
2020
|
+
if (natCode && NATCON_TABLE[natCode]) entries.push({ label: "Construction", value: NATCON_TABLE[natCode] });
|
|
2021
|
+
const condCode = parseCodeLocal(props.CONDTN);
|
|
2022
|
+
if (condCode && CONDTN_SHORT[condCode]) entries.push({ label: "Condition", value: CONDTN_SHORT[condCode] });
|
|
2023
|
+
const st2 = parseCodeLocal(props.STATUS);
|
|
2024
|
+
if (st2 && STATUS_TABLE[st2]) entries.push({ label: "Status", value: STATUS_TABLE[st2] });
|
|
2025
|
+
if (props.NOBJNM) entries.push({ label: "National Name", value: String(props.NOBJNM) });
|
|
2026
|
+
} else if (rawLayer === "CBLSUB" || rawLayer === "CBLOHD") {
|
|
2027
|
+
const CATCBL_LINE = {
|
|
2028
|
+
1: "Power line",
|
|
2029
|
+
2: "Transmission line",
|
|
2030
|
+
3: "Telephone",
|
|
2031
|
+
4: "Telegraph",
|
|
2032
|
+
5: "Mooring cable/chain",
|
|
2033
|
+
6: "Ferry cable"
|
|
2034
|
+
};
|
|
2035
|
+
type = rawLayer === "CBLSUB" ? "Submarine Cable" : "Overhead Cable";
|
|
2036
|
+
const cat = parseCodeLocal(props.CATCBL);
|
|
2037
|
+
if (cat && CATCBL_LINE[cat]) {
|
|
2038
|
+
entries.push({ label: "Category", value: CATCBL_LINE[cat] });
|
|
2039
|
+
if (!title) title = `${CATCBL_LINE[cat]} (${rawLayer === "CBLSUB" ? "submarine" : "overhead"})`;
|
|
2040
|
+
}
|
|
2041
|
+
if (props.VERCLR != null) entries.push({ label: "Vertical Clearance", value: `${props.VERCLR}m` });
|
|
2042
|
+
if (props.VERCSA != null) entries.push({ label: "Clearance (safe)", value: `${props.VERCSA}m` });
|
|
2043
|
+
if (props.RESTRN) {
|
|
2044
|
+
const codes = parseListCodes(props.RESTRN).filter((n) => RESTRN_TABLE[n]);
|
|
2045
|
+
if (codes.length > 0) entries.push({ label: "Restrictions", value: codes.map((c) => RESTRN_TABLE[c]).join("; ") });
|
|
2046
|
+
}
|
|
2047
|
+
} else if (rawLayer === "PIPSOL" || rawLayer === "PIPOHD") {
|
|
2048
|
+
const CATPIP_LINE = {
|
|
2049
|
+
2: "Outfall pipe",
|
|
2050
|
+
3: "Intake pipe",
|
|
2051
|
+
4: "Sewer",
|
|
2052
|
+
5: "Bubbler system",
|
|
2053
|
+
6: "Supply pipe"
|
|
2054
|
+
};
|
|
2055
|
+
type = rawLayer === "PIPSOL" ? "Submarine Pipeline" : "Overhead Pipeline";
|
|
2056
|
+
const cat = parseCodeLocal(props.CATPIP);
|
|
2057
|
+
if (cat && CATPIP_LINE[cat]) {
|
|
2058
|
+
entries.push({ label: "Category", value: CATPIP_LINE[cat] });
|
|
2059
|
+
if (!title) title = `${CATPIP_LINE[cat]} (${rawLayer === "PIPSOL" ? "submarine" : "overhead"})`;
|
|
2060
|
+
}
|
|
2061
|
+
if (props.VERCLR != null) entries.push({ label: "Vertical Clearance", value: `${props.VERCLR}m` });
|
|
2062
|
+
if (props.RESTRN) {
|
|
2063
|
+
const codes = parseListCodes(props.RESTRN).filter((n) => RESTRN_TABLE[n]);
|
|
2064
|
+
if (codes.length > 0) entries.push({ label: "Restrictions", value: codes.map((c) => RESTRN_TABLE[c]).join("; ") });
|
|
2065
|
+
}
|
|
2066
|
+
} else if (rawLayer === "BERTHS") {
|
|
2067
|
+
type = "Berth";
|
|
2068
|
+
} else if (rawLayer === "DAMCON") {
|
|
2069
|
+
type = "Dam";
|
|
2070
|
+
if (props.VERCLR != null) entries.push({ label: "Vertical Clearance", value: `${props.VERCLR}m` });
|
|
2071
|
+
} else if (rawLayer === "FERYRT") {
|
|
2072
|
+
type = "Ferry Route";
|
|
2073
|
+
} else if (rawLayer === "RECTRC") {
|
|
2074
|
+
type = "Recommended Track";
|
|
2075
|
+
const trafic = parseCodeLocal(props.TRAFIC);
|
|
2076
|
+
if (trafic && TRAFIC_TABLE[trafic]) entries.push({ label: "Traffic", value: TRAFIC_TABLE[trafic] });
|
|
2077
|
+
if (props.ORIENT != null) entries.push({ label: "Orientation", value: `${props.ORIENT}\xB0` });
|
|
2078
|
+
} else if (rawLayer === "BUISGL") {
|
|
2079
|
+
type = "Building";
|
|
2080
|
+
const cat = parseCodeLocal(props.CATBUA);
|
|
2081
|
+
if (cat) {
|
|
2082
|
+
const CATBUA = {
|
|
2083
|
+
1: "Urban area",
|
|
2084
|
+
5: "Settlement",
|
|
2085
|
+
6: "Town",
|
|
2086
|
+
7: "Village",
|
|
2087
|
+
8: "City"
|
|
2088
|
+
};
|
|
2089
|
+
if (CATBUA[cat]) {
|
|
2090
|
+
entries.push({ label: "Category", value: CATBUA[cat] });
|
|
2091
|
+
if (!title) title = CATBUA[cat];
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
const func = parseCodeLocal(props.FUNCTN);
|
|
2095
|
+
if (func) {
|
|
2096
|
+
const FUNCTN = {
|
|
2097
|
+
2: "Harbour master's office",
|
|
2098
|
+
3: "Customs office",
|
|
2099
|
+
4: "Health office",
|
|
2100
|
+
5: "Hospital",
|
|
2101
|
+
6: "Post office",
|
|
2102
|
+
7: "Hotel",
|
|
2103
|
+
8: "Railway station",
|
|
2104
|
+
9: "Police station",
|
|
2105
|
+
10: "Water-police station",
|
|
2106
|
+
11: "Pilot office",
|
|
2107
|
+
13: "Bank",
|
|
2108
|
+
14: "Headquarters",
|
|
2109
|
+
15: "Transit shed/warehouse",
|
|
2110
|
+
18: "Church",
|
|
2111
|
+
19: "Chapel",
|
|
2112
|
+
20: "Temple",
|
|
2113
|
+
21: "Synagogue",
|
|
2114
|
+
22: "Mosque",
|
|
2115
|
+
24: "Lookout",
|
|
2116
|
+
25: "Communication",
|
|
2117
|
+
28: "Power station",
|
|
2118
|
+
33: "Factory",
|
|
2119
|
+
35: "Refinery",
|
|
2120
|
+
36: "Religious order",
|
|
2121
|
+
37: "School",
|
|
2122
|
+
38: "University"
|
|
2123
|
+
};
|
|
2124
|
+
if (FUNCTN[func]) entries.push({ label: "Function", value: FUNCTN[func] });
|
|
2125
|
+
}
|
|
2126
|
+
if (props.VERLEN != null) entries.push({ label: "Height", value: `${props.VERLEN}m` });
|
|
2127
|
+
} else if (rawLayer === "SILTNK") {
|
|
2128
|
+
type = "Silo / Tank";
|
|
2129
|
+
const cat = parseCodeLocal(props.CATSIL);
|
|
2130
|
+
if (cat) {
|
|
2131
|
+
const CATSIL = { 1: "Silo", 2: "Tank", 3: "Grain elevator", 4: "Water tower" };
|
|
2132
|
+
if (CATSIL[cat]) {
|
|
2133
|
+
type = CATSIL[cat];
|
|
2134
|
+
if (!title) title = CATSIL[cat];
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
if (parseCodeLocal(props.CONVIS) === 1) entries.push({ label: "Visibility", value: "Conspicuous from seaward" });
|
|
2138
|
+
if (props.VERLEN != null) entries.push({ label: "Height", value: `${props.VERLEN}m` });
|
|
2139
|
+
} else if (rawLayer === "HULKES") {
|
|
2140
|
+
type = "Hulk";
|
|
2141
|
+
if (props.CATHLK != null) {
|
|
2142
|
+
const cat = parseCodeLocal(props.CATHLK);
|
|
2143
|
+
const CATHLK = { 1: "Floating restaurant", 2: "Historic ship", 3: "Museum", 4: "Accommodation", 5: "Floating breakwater" };
|
|
2144
|
+
if (cat && CATHLK[cat]) {
|
|
2145
|
+
entries.push({ label: "Use", value: CATHLK[cat] });
|
|
2146
|
+
if (!title) title = CATHLK[cat];
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
} else if (rawLayer === "DRYDOC") {
|
|
2150
|
+
type = "Dry Dock";
|
|
2151
|
+
} else if (rawLayer === "FLODOC") {
|
|
2152
|
+
type = "Floating Dock";
|
|
2153
|
+
if (props.VERLEN != null) entries.push({ label: "Length", value: `${props.VERLEN}m` });
|
|
2154
|
+
if (props.HORLEN != null) entries.push({ label: "Horiz. Length", value: `${props.HORLEN}m` });
|
|
2155
|
+
} else if (rawLayer === "PONTON") {
|
|
2156
|
+
type = "Pontoon";
|
|
2157
|
+
} else if (rawLayer === "PYLONS") {
|
|
2158
|
+
type = "Pylon / Bridge Support";
|
|
2159
|
+
const cat = parseCodeLocal(props.CATPYL);
|
|
2160
|
+
if (cat) {
|
|
2161
|
+
const CATPYL = {
|
|
2162
|
+
1: "Power transmission pylon",
|
|
2163
|
+
2: "Telecom pylon",
|
|
2164
|
+
3: "Aerial cableway pylon",
|
|
2165
|
+
4: "Bridge pylon",
|
|
2166
|
+
5: "Bridge pier",
|
|
2167
|
+
6: "Chimney",
|
|
2168
|
+
7: "Flare stack"
|
|
2169
|
+
};
|
|
2170
|
+
if (CATPYL[cat]) {
|
|
2171
|
+
entries.push({ label: "Type", value: CATPYL[cat] });
|
|
2172
|
+
if (!title) title = CATPYL[cat];
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
if (props.VERLEN != null) entries.push({ label: "Height", value: `${props.VERLEN}m` });
|
|
2176
|
+
} else if (rawLayer === "GATCON") {
|
|
2177
|
+
type = "Gate";
|
|
2178
|
+
const cat = parseCodeLocal(props.CATGAT);
|
|
2179
|
+
if (cat) {
|
|
2180
|
+
const CATGAT = { 1: "General gate", 2: "Flood barrage gate", 3: "Caisson", 4: "Lock gate", 5: "Dyke gate" };
|
|
2181
|
+
if (CATGAT[cat]) {
|
|
2182
|
+
entries.push({ label: "Type", value: CATGAT[cat] });
|
|
2183
|
+
if (!title) title = CATGAT[cat];
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
} else if (rawLayer === "LOGPON") {
|
|
2187
|
+
type = "Log Pond";
|
|
2188
|
+
} else if (rawLayer === "AIRARE") {
|
|
2189
|
+
type = "Airport / Airfield";
|
|
2190
|
+
const cat = parseCodeLocal(props.CATAIR);
|
|
2191
|
+
if (cat) {
|
|
2192
|
+
const CATAIR = { 1: "Airport", 2: "Heliport", 3: "Helipad", 4: "Emergency helipad" };
|
|
2193
|
+
if (CATAIR[cat]) {
|
|
2194
|
+
entries.push({ label: "Type", value: CATAIR[cat] });
|
|
2195
|
+
if (!title) title = CATAIR[cat];
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
} else if (rawLayer === "SLCONS") {
|
|
2199
|
+
type = "Shoreline Construction";
|
|
2200
|
+
const cat = parseCodeLocal(props.CATSLC);
|
|
2201
|
+
if (cat) {
|
|
2202
|
+
const CATSLC = {
|
|
2203
|
+
1: "Breakwater",
|
|
2204
|
+
2: "Groyne",
|
|
2205
|
+
3: "Mole",
|
|
2206
|
+
4: "Pier",
|
|
2207
|
+
5: "Promenade pier",
|
|
2208
|
+
6: "Wharf",
|
|
2209
|
+
7: "Training wall",
|
|
2210
|
+
8: "Rip rap",
|
|
2211
|
+
9: "Revetment",
|
|
2212
|
+
10: "Sea wall",
|
|
2213
|
+
11: "Landing stairs",
|
|
2214
|
+
12: "Landing place",
|
|
2215
|
+
13: "Fender",
|
|
2216
|
+
14: "Lock",
|
|
2217
|
+
15: "Flood gate",
|
|
2218
|
+
16: "Slipway",
|
|
2219
|
+
17: "Log ramp"
|
|
2220
|
+
};
|
|
2221
|
+
if (CATSLC[cat]) {
|
|
2222
|
+
entries.push({ label: "Type", value: CATSLC[cat] });
|
|
2223
|
+
if (!title) title = CATSLC[cat];
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
const wl = parseCodeLocal(props.WATLEV);
|
|
2227
|
+
if (wl && WATLEV_TABLE[wl]) entries.push({ label: "Water Level", value: WATLEV_TABLE[wl] });
|
|
2228
|
+
} else if (rawLayer === "COALNE") {
|
|
2229
|
+
type = "Coastline";
|
|
2230
|
+
const cat = parseCodeLocal(props.CATCOA);
|
|
2231
|
+
if (cat) {
|
|
2232
|
+
const CATCOA = {
|
|
2233
|
+
1: "Steep coast",
|
|
2234
|
+
2: "Flat coast",
|
|
2235
|
+
3: "Sandy shore",
|
|
2236
|
+
4: "Stony shore",
|
|
2237
|
+
5: "Shingly shore",
|
|
2238
|
+
6: "Glacier",
|
|
2239
|
+
7: "Mangrove",
|
|
2240
|
+
8: "Marshy shore",
|
|
2241
|
+
9: "Coral reef",
|
|
2242
|
+
10: "Ice coast",
|
|
2243
|
+
11: "Rocky shore"
|
|
2244
|
+
};
|
|
2245
|
+
if (CATCOA[cat]) {
|
|
2246
|
+
entries.push({ label: "Type", value: CATCOA[cat] });
|
|
2247
|
+
if (!title) title = CATCOA[cat];
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
} else if (rawLayer === "DEPCNT") {
|
|
2251
|
+
type = "Depth Contour";
|
|
2252
|
+
if (props.VALDCO != null) {
|
|
2253
|
+
title = title || `${props.VALDCO}m contour`;
|
|
2254
|
+
entries.push({ label: "Depth", value: `${props.VALDCO}m` });
|
|
2255
|
+
}
|
|
2256
|
+
} else if (rawLayer === "RIVERS") {
|
|
2257
|
+
type = "River";
|
|
2258
|
+
} else if (rawLayer === "CANALS") {
|
|
2259
|
+
type = "Canal";
|
|
2260
|
+
if (props.HORWID != null) entries.push({ label: "Width", value: `${props.HORWID}m` });
|
|
2261
|
+
} else if (rawLayer === "LAKARE") {
|
|
2262
|
+
type = "Lake";
|
|
2263
|
+
} else if (rawLayer === "RAPIDS") {
|
|
2264
|
+
type = "Rapids";
|
|
2265
|
+
} else if (rawLayer === "WATFAL") {
|
|
2266
|
+
type = "Waterfall";
|
|
2267
|
+
if (props.HEIGHT != null) entries.push({ label: "Height", value: `${props.HEIGHT}m` });
|
|
2268
|
+
} else if (rawLayer === "LNDARE") {
|
|
2269
|
+
type = "Land Area";
|
|
2270
|
+
}
|
|
2271
|
+
if (props.INFORM) entries.push({ label: "Information", value: String(props.INFORM) });
|
|
2272
|
+
if (props.TXTDSC) entries.push({ label: "Text Description", value: String(props.TXTDSC) });
|
|
2273
|
+
const st = parseCodeLocal(props.STATUS);
|
|
2274
|
+
if (st && STATUS_TABLE[st]) entries.push({ label: "Status", value: STATUS_TABLE[st] });
|
|
2275
|
+
return { type, title: title || type, properties: entries };
|
|
2276
|
+
}
|
|
2277
|
+
function parseCode(val) {
|
|
2278
|
+
if (val == null) return null;
|
|
2279
|
+
let s = String(val).trim();
|
|
2280
|
+
if (s.startsWith("[")) {
|
|
2281
|
+
try {
|
|
2282
|
+
const arr = JSON.parse(s);
|
|
2283
|
+
if (Array.isArray(arr) && arr.length > 0) s = String(arr[0]);
|
|
2284
|
+
else return null;
|
|
2285
|
+
} catch {
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
s = s.split(/[,;]/)[0].trim();
|
|
2289
|
+
const n = Number(s);
|
|
2290
|
+
return isNaN(n) ? null : n;
|
|
2291
|
+
}
|
|
2292
|
+
function parseListCodes(val) {
|
|
2293
|
+
if (val == null) return [];
|
|
2294
|
+
if (Array.isArray(val)) {
|
|
2295
|
+
return val.map(Number).filter((n) => !isNaN(n) && n > 0);
|
|
2296
|
+
}
|
|
2297
|
+
const s = String(val).trim();
|
|
2298
|
+
if (s.startsWith("[")) {
|
|
2299
|
+
try {
|
|
2300
|
+
const arr = JSON.parse(s);
|
|
2301
|
+
if (Array.isArray(arr)) return arr.map(Number).filter((n) => !isNaN(n) && n > 0);
|
|
2302
|
+
} catch {
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
return s.split(/[,;]/).map((v) => Number(v.trim())).filter((n) => !isNaN(n) && n > 0);
|
|
2306
|
+
}
|
|
2307
|
+
function parseGroup(val) {
|
|
2308
|
+
if (val == null) return null;
|
|
2309
|
+
const s = String(val).trim();
|
|
2310
|
+
const parenMatch = s.match(/^\(?([\d+]+)\)?$/);
|
|
2311
|
+
if (parenMatch) {
|
|
2312
|
+
const parts = parenMatch[1].split("+").map(Number);
|
|
2313
|
+
if (parts.every((n2) => !isNaN(n2) && n2 > 0)) return parts;
|
|
2314
|
+
}
|
|
2315
|
+
const n = Number(s);
|
|
2316
|
+
return isNaN(n) || n <= 0 ? null : [n];
|
|
2317
|
+
}
|
|
2318
|
+
var CHILD_LAYERS = /* @__PURE__ */ new Set(["LIGHTS", "FOGSIG", "RTPBCN", "DAYMAR", "TOPMAR"]);
|
|
2319
|
+
var PARENT_LAYERS = [
|
|
2320
|
+
"enc-BOYLAT",
|
|
2321
|
+
"enc-BOYCAR",
|
|
2322
|
+
"enc-BOYSPP",
|
|
2323
|
+
"enc-BOYISD",
|
|
2324
|
+
"enc-BOYSAW",
|
|
2325
|
+
"enc-BCNLAT",
|
|
2326
|
+
"enc-BCNCAR",
|
|
2327
|
+
"enc-BCNSPP",
|
|
2328
|
+
"enc-BCNISD",
|
|
2329
|
+
"enc-BCNSAW"
|
|
2330
|
+
];
|
|
2331
|
+
function deriveMarinerName(objnam, colourCode) {
|
|
2332
|
+
if (!objnam) return "";
|
|
2333
|
+
const m = objnam.match(/(\d+[A-Z]?)\s*$/);
|
|
2334
|
+
if (!m) return objnam;
|
|
2335
|
+
const num = m[1];
|
|
2336
|
+
const colourAbbr = colourCode ? COLOUR_TABLE[colourCode]?.abbr : null;
|
|
2337
|
+
if (colourAbbr && colourAbbr !== "W") {
|
|
2338
|
+
return `${colourAbbr} "${num}"`;
|
|
2339
|
+
}
|
|
2340
|
+
return `"${num}"`;
|
|
2341
|
+
}
|
|
2342
|
+
function getTopmarkMeaning(topshpCode, rawLayer) {
|
|
2343
|
+
if (topshpCode == null) return null;
|
|
2344
|
+
switch (topshpCode) {
|
|
2345
|
+
case 10:
|
|
2346
|
+
return "West cardinal \u2014 pass to the west";
|
|
2347
|
+
// 2 cones, point to point
|
|
2348
|
+
case 11:
|
|
2349
|
+
return "East cardinal \u2014 pass to the east";
|
|
2350
|
+
// 2 cones, base to base
|
|
2351
|
+
case 13:
|
|
2352
|
+
return "North cardinal \u2014 pass to the north";
|
|
2353
|
+
// 2 cones, points up
|
|
2354
|
+
case 14:
|
|
2355
|
+
return "South cardinal \u2014 pass to the south";
|
|
2356
|
+
}
|
|
2357
|
+
const parent = rawLayer.replace(/^(BOY|BCN)/, "");
|
|
2358
|
+
if (parent === "LAT") {
|
|
2359
|
+
if (topshpCode === 1) return "Starboard-hand mark";
|
|
2360
|
+
if (topshpCode === 5) return "Port-hand mark";
|
|
2361
|
+
}
|
|
2362
|
+
if (parent === "SAW" && topshpCode === 3) return "Safe water \u2014 navigable water all around";
|
|
2363
|
+
if (parent === "ISD" && topshpCode === 4) return "Isolated danger \u2014 danger of limited extent";
|
|
2364
|
+
if (parent === "SPP" && topshpCode === 7) return "Special-purpose mark";
|
|
2365
|
+
return null;
|
|
2366
|
+
}
|
|
2367
|
+
function decodeNavAidProps(layerName, props, coords) {
|
|
2368
|
+
const rawLayer = layerName.replace(/^enc-/, "").replace(/-(point|line|polygon|midpoint)$/, "");
|
|
2369
|
+
const fullName = props.OBJNAM || props.NOBJNM || "";
|
|
2370
|
+
const colourCode = parseCode(props.COLOUR);
|
|
2371
|
+
const marinerName = deriveMarinerName(fullName, colourCode);
|
|
2372
|
+
const name = marinerName || fullName;
|
|
2373
|
+
const type = LAYER_TYPE_MAP[rawLayer] ?? rawLayer;
|
|
2374
|
+
const lightColourSource = props.LIGHT_COLOUR ?? props.COLOUR;
|
|
2375
|
+
const lightColourCodes = parseListCodes(lightColourSource);
|
|
2376
|
+
const colourEntry = colourCode ? COLOUR_TABLE[colourCode] : null;
|
|
2377
|
+
const lightColourHexList = lightColourCodes.map((c) => COLOUR_TABLE[c]?.hex ?? "#ffffff");
|
|
2378
|
+
const boyshpCode = parseCode(props.BOYSHP);
|
|
2379
|
+
const shape = boyshpCode ? BOYSHP_TABLE[boyshpCode] ?? null : null;
|
|
2380
|
+
const topshpCode = parseCode(props.TOPSHP);
|
|
2381
|
+
const topmark = topshpCode ? TOPSHP_TABLE[topshpCode] ?? null : null;
|
|
2382
|
+
const topmarkMeaning = getTopmarkMeaning(topshpCode, rawLayer);
|
|
2383
|
+
const litchrCode = parseCode(props.LITCHR);
|
|
2384
|
+
const litchrEntry = litchrCode ? LITCHR_TABLE[litchrCode] : null;
|
|
2385
|
+
const sigper = props.SIGPER != null ? Number(props.SIGPER) : null;
|
|
2386
|
+
const group = parseGroup(props.SIGGRP);
|
|
2387
|
+
const nameForFog = String(props.OBJNAM ?? "").toLowerCase();
|
|
2388
|
+
let catfogCode = null;
|
|
2389
|
+
if (/\bwhistle\b/.test(nameForFog)) catfogCode = 6;
|
|
2390
|
+
else if (/\bgong\b/.test(nameForFog)) catfogCode = 8;
|
|
2391
|
+
else if (/\bbell\b/.test(nameForFog)) catfogCode = 5;
|
|
2392
|
+
else if (/\bhorn\b/.test(nameForFog)) catfogCode = 9;
|
|
2393
|
+
else if (/\bsiren\b/.test(nameForFog)) catfogCode = 3;
|
|
2394
|
+
else if (/diaphone/.test(nameForFog)) catfogCode = 2;
|
|
2395
|
+
else if (/nautophone|nauto/.test(nameForFog)) catfogCode = 4;
|
|
2396
|
+
if (!catfogCode) catfogCode = parseCode(props.CATFOG);
|
|
2397
|
+
const fogSignal2 = catfogCode ? CATFOG_TABLE[catfogCode] ?? null : null;
|
|
2398
|
+
const fogSignalAbbr = catfogCode ? CATFOG_ABBR_TABLE[catfogCode] ?? null : null;
|
|
2399
|
+
const radarReflector = props.RETRFL != null && String(props.RETRFL) !== "0" && String(props.RETRFL) !== "";
|
|
2400
|
+
const valsou = props.VALSOU != null ? Number(props.VALSOU) : null;
|
|
2401
|
+
const depth = valsou != null && !isNaN(valsou) ? valsou : null;
|
|
2402
|
+
const statusCode = parseCode(props.STATUS);
|
|
2403
|
+
const status = statusCode ? STATUS_TABLE[statusCode] ?? null : null;
|
|
2404
|
+
const marsysCode = parseCode(props.MARSYS);
|
|
2405
|
+
const markingSystem = marsysCode ? MARSYS_TABLE[marsysCode] ?? null : null;
|
|
2406
|
+
const catlamCode = parseCode(props.CATLAM);
|
|
2407
|
+
const catcamCode = parseCode(props.CATCAM);
|
|
2408
|
+
const cathafCode = parseCode(props.CATHAF);
|
|
2409
|
+
const catscfCode = parseCode(props.CATSCF);
|
|
2410
|
+
const catcrnCode = parseCode(props.CATCRN);
|
|
2411
|
+
const catlmkNames = parseListCodes(props.CATLMK).map((c) => CATLMK_TABLE[c]).filter((n) => Boolean(n));
|
|
2412
|
+
const category = catlamCode ? CATLAM_TABLE[catlamCode] ?? null : catcamCode ? CATCAM_TABLE[catcamCode] ?? null : cathafCode ? CATHAF_TABLE[cathafCode] ?? null : catscfCode ? CATSCF_TABLE[catscfCode] ?? null : catcrnCode ? CATCRN_TABLE[catcrnCode] ?? null : catlmkNames.length > 0 ? catlmkNames.join(" / ") : null;
|
|
2413
|
+
const information = props.INFORM || null;
|
|
2414
|
+
const lightColourAbbrs = lightColourCodes.map((c) => COLOUR_TABLE[c]?.abbr).filter(Boolean);
|
|
2415
|
+
const notation = buildNotation(litchrEntry?.abbr ?? null, lightColourAbbrs, sigper, group);
|
|
2416
|
+
const sectr1 = props.SECTR1 != null ? Number(props.SECTR1) : null;
|
|
2417
|
+
const sectr2 = props.SECTR2 != null ? Number(props.SECTR2) : null;
|
|
2418
|
+
const orient = props.ORIENT != null ? Number(props.ORIENT) : null;
|
|
2419
|
+
const catlit = parseCode(props.CATLIT);
|
|
2420
|
+
const isDirectional = catlit === 1;
|
|
2421
|
+
const sectors = [];
|
|
2422
|
+
const isSectorLight = sectr1 != null && sectr2 != null && !isNaN(sectr1) && !isNaN(sectr2);
|
|
2423
|
+
const valnmr = props.VALNMR != null ? Number(props.VALNMR) : null;
|
|
2424
|
+
const nominalRange = valnmr != null && !isNaN(valnmr) && valnmr > 0 ? valnmr : null;
|
|
2425
|
+
if (isSectorLight) {
|
|
2426
|
+
sectors.push({
|
|
2427
|
+
sectorStart: sectr1,
|
|
2428
|
+
sectorEnd: sectr2,
|
|
2429
|
+
colour: colourEntry?.name ?? "White",
|
|
2430
|
+
colourHex: colourEntry?.hex ?? "#ffffff",
|
|
2431
|
+
notation,
|
|
2432
|
+
nominalRange
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
return {
|
|
2436
|
+
name,
|
|
2437
|
+
fullName: fullName && fullName !== name ? fullName : null,
|
|
2438
|
+
type,
|
|
2439
|
+
layerName: rawLayer,
|
|
2440
|
+
colour: colourEntry?.name ?? null,
|
|
2441
|
+
colourHex: colourEntry?.hex ?? "#ffffff",
|
|
2442
|
+
lightColourHex: lightColourHexList[0] ?? colourEntry?.hex ?? "#ffffff",
|
|
2443
|
+
colourHexList: lightColourHexList.length > 0 ? lightColourHexList : [colourEntry?.hex ?? "#ffffff"],
|
|
2444
|
+
shape,
|
|
2445
|
+
topmark,
|
|
2446
|
+
topmarkMeaning,
|
|
2447
|
+
lightCharacter: litchrEntry?.abbr ?? null,
|
|
2448
|
+
lightCharacterFull: litchrEntry?.name ?? null,
|
|
2449
|
+
period: sigper,
|
|
2450
|
+
group,
|
|
2451
|
+
fogSignal: fogSignal2,
|
|
2452
|
+
fogSignalAbbr,
|
|
2453
|
+
radarReflector,
|
|
2454
|
+
depth,
|
|
2455
|
+
status,
|
|
2456
|
+
markingSystem,
|
|
2457
|
+
category,
|
|
2458
|
+
information,
|
|
2459
|
+
notation,
|
|
2460
|
+
coordinates: coords ?? null,
|
|
2461
|
+
sectors,
|
|
2462
|
+
orientation: orient != null && !isNaN(orient) ? orient : null,
|
|
2463
|
+
isDirectional,
|
|
2464
|
+
isSectorLight
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
function buildNotation(litchrAbbr, colourAbbrs, sigper, group) {
|
|
2468
|
+
if (!litchrAbbr) return "";
|
|
2469
|
+
const parts = [];
|
|
2470
|
+
let charPart = litchrAbbr;
|
|
2471
|
+
const totalFlashes = group ? group.reduce((a, b) => a + b, 0) : 0;
|
|
2472
|
+
if (totalFlashes > 1) charPart += `(${group.join("+")})`;
|
|
2473
|
+
parts.push(charPart);
|
|
2474
|
+
if (colourAbbrs.length > 0) parts.push(colourAbbrs.join("."));
|
|
2475
|
+
if (sigper && sigper > 0) parts.push(`${sigper}s`);
|
|
2476
|
+
return parts.join(" ");
|
|
2477
|
+
}
|
|
2478
|
+
function buildFlashPattern(decoded) {
|
|
2479
|
+
const litchr = decoded.lightCharacter;
|
|
2480
|
+
if (!litchr) return null;
|
|
2481
|
+
const period = decoded.period && decoded.period > 0 ? decoded.period * 1e3 : 6e3;
|
|
2482
|
+
const groups = decoded.group ?? [1];
|
|
2483
|
+
const totalFlashes = groups.reduce((a, b) => a + b, 0);
|
|
2484
|
+
const hex = decoded.lightColourHex;
|
|
2485
|
+
const colours = decoded.colourHexList;
|
|
2486
|
+
const notation = decoded.notation;
|
|
2487
|
+
const phases = [];
|
|
2488
|
+
switch (litchr) {
|
|
2489
|
+
case "F": {
|
|
2490
|
+
phases.push({ on: true, duration: period, color: hex });
|
|
2491
|
+
break;
|
|
2492
|
+
}
|
|
2493
|
+
case "Fl":
|
|
2494
|
+
case "S.Fl": {
|
|
2495
|
+
const flashDur = 400;
|
|
2496
|
+
const interFlashGap = 300;
|
|
2497
|
+
const interGroupGap = 800;
|
|
2498
|
+
const activeDur = groups.reduce((sum, g) => sum + g * flashDur + (g - 1) * interFlashGap, 0) + (groups.length - 1) * interGroupGap;
|
|
2499
|
+
const eclipseDur = Math.max(period - activeDur, 500);
|
|
2500
|
+
for (let gi = 0; gi < groups.length; gi++) {
|
|
2501
|
+
const g = groups[gi];
|
|
2502
|
+
for (let i = 0; i < g; i++) {
|
|
2503
|
+
phases.push({ on: true, duration: flashDur, color: hex });
|
|
2504
|
+
if (i < g - 1) phases.push({ on: false, duration: interFlashGap });
|
|
2505
|
+
}
|
|
2506
|
+
if (gi < groups.length - 1) phases.push({ on: false, duration: interGroupGap });
|
|
2507
|
+
}
|
|
2508
|
+
phases.push({ on: false, duration: eclipseDur });
|
|
2509
|
+
break;
|
|
2510
|
+
}
|
|
2511
|
+
case "LFl":
|
|
2512
|
+
case "S.LFl": {
|
|
2513
|
+
const onDur = 2e3;
|
|
2514
|
+
phases.push({ on: true, duration: onDur, color: hex });
|
|
2515
|
+
phases.push({ on: false, duration: Math.max(period - onDur, 1e3) });
|
|
2516
|
+
break;
|
|
2517
|
+
}
|
|
2518
|
+
case "Oc": {
|
|
2519
|
+
const offDur = 400;
|
|
2520
|
+
const interGroupOn = 300;
|
|
2521
|
+
const interGroupGap = 800;
|
|
2522
|
+
const totalOcclusions = totalFlashes;
|
|
2523
|
+
const activeDur = totalOcclusions * offDur + (totalOcclusions - 1) * interGroupOn + (groups.length > 1 ? (groups.length - 1) * (interGroupGap - interGroupOn) : 0);
|
|
2524
|
+
const onDur = Math.max(period - activeDur, 500);
|
|
2525
|
+
phases.push({ on: true, duration: onDur, color: hex });
|
|
2526
|
+
for (let gi = 0; gi < groups.length; gi++) {
|
|
2527
|
+
const g = groups[gi];
|
|
2528
|
+
for (let i = 0; i < g; i++) {
|
|
2529
|
+
phases.push({ on: false, duration: offDur });
|
|
2530
|
+
if (i < g - 1) phases.push({ on: true, duration: interGroupOn, color: hex });
|
|
2531
|
+
}
|
|
2532
|
+
if (gi < groups.length - 1) phases.push({ on: true, duration: interGroupGap, color: hex });
|
|
2533
|
+
}
|
|
2534
|
+
break;
|
|
2535
|
+
}
|
|
2536
|
+
case "Iso": {
|
|
2537
|
+
const half = period / 2;
|
|
2538
|
+
phases.push({ on: true, duration: half, color: hex });
|
|
2539
|
+
phases.push({ on: false, duration: half });
|
|
2540
|
+
break;
|
|
2541
|
+
}
|
|
2542
|
+
case "Q": {
|
|
2543
|
+
const cycleDur = 1e3;
|
|
2544
|
+
const count = Math.max(totalFlashes, 1);
|
|
2545
|
+
if (count === 1) {
|
|
2546
|
+
phases.push({ on: true, duration: cycleDur / 2, color: hex });
|
|
2547
|
+
phases.push({ on: false, duration: cycleDur / 2 });
|
|
2548
|
+
} else {
|
|
2549
|
+
const flashDur = 250;
|
|
2550
|
+
const gap = 250;
|
|
2551
|
+
for (let i = 0; i < count; i++) {
|
|
2552
|
+
phases.push({ on: true, duration: flashDur, color: hex });
|
|
2553
|
+
if (i < count - 1) phases.push({ on: false, duration: gap });
|
|
2554
|
+
}
|
|
2555
|
+
phases.push({ on: false, duration: Math.max(period - count * (flashDur + gap), 500) });
|
|
2556
|
+
}
|
|
2557
|
+
break;
|
|
2558
|
+
}
|
|
2559
|
+
case "VQ": {
|
|
2560
|
+
const flashDur = 150;
|
|
2561
|
+
const gap = 150;
|
|
2562
|
+
const count = Math.max(totalFlashes, 1);
|
|
2563
|
+
if (count === 1) {
|
|
2564
|
+
phases.push({ on: true, duration: flashDur, color: hex });
|
|
2565
|
+
phases.push({ on: false, duration: gap });
|
|
2566
|
+
} else {
|
|
2567
|
+
for (let i = 0; i < count; i++) {
|
|
2568
|
+
phases.push({ on: true, duration: flashDur, color: hex });
|
|
2569
|
+
if (i < count - 1) phases.push({ on: false, duration: gap });
|
|
2570
|
+
}
|
|
2571
|
+
phases.push({ on: false, duration: Math.max(period - count * (flashDur + gap), 500) });
|
|
2572
|
+
}
|
|
2573
|
+
break;
|
|
2574
|
+
}
|
|
2575
|
+
case "UQ": {
|
|
2576
|
+
const flashDur = 80;
|
|
2577
|
+
const gap = 80;
|
|
2578
|
+
phases.push({ on: true, duration: flashDur, color: hex });
|
|
2579
|
+
phases.push({ on: false, duration: gap });
|
|
2580
|
+
break;
|
|
2581
|
+
}
|
|
2582
|
+
case "Mo": {
|
|
2583
|
+
const dot = 300;
|
|
2584
|
+
const dash = 900;
|
|
2585
|
+
const gap = 300;
|
|
2586
|
+
phases.push({ on: true, duration: dot, color: hex });
|
|
2587
|
+
phases.push({ on: false, duration: gap });
|
|
2588
|
+
phases.push({ on: true, duration: dash, color: hex });
|
|
2589
|
+
phases.push({ on: false, duration: gap });
|
|
2590
|
+
phases.push({ on: true, duration: dot, color: hex });
|
|
2591
|
+
phases.push({ on: false, duration: Math.max(period - 3 * gap - dot * 2 - dash, 1e3) });
|
|
2592
|
+
break;
|
|
2593
|
+
}
|
|
2594
|
+
case "Al":
|
|
2595
|
+
case "Al.Gp":
|
|
2596
|
+
case "Al.Fl":
|
|
2597
|
+
case "Al.LFl":
|
|
2598
|
+
case "Al.Oc":
|
|
2599
|
+
case "Al.F.Fl": {
|
|
2600
|
+
const isFlashing = litchr.includes("Fl") || litchr === "Al.Gp";
|
|
2601
|
+
const isOcculting = litchr.includes("Oc");
|
|
2602
|
+
const isFixed = litchr === "Al" || litchr === "Al.F.Fl";
|
|
2603
|
+
if (isFlashing && totalFlashes > 1) {
|
|
2604
|
+
const flashDur = 400;
|
|
2605
|
+
const interFlashGap = 300;
|
|
2606
|
+
const interGroupGap = 800;
|
|
2607
|
+
const activeDur = groups.reduce((sum, g) => sum + g * flashDur + (g - 1) * interFlashGap, 0) + (groups.length - 1) * interGroupGap;
|
|
2608
|
+
const eclipseDur = Math.max(period - activeDur, 500);
|
|
2609
|
+
for (let gi = 0; gi < groups.length; gi++) {
|
|
2610
|
+
const g = groups[gi];
|
|
2611
|
+
const groupColour = colours[gi % colours.length];
|
|
2612
|
+
for (let i = 0; i < g; i++) {
|
|
2613
|
+
phases.push({ on: true, duration: flashDur, color: groupColour });
|
|
2614
|
+
if (i < g - 1) phases.push({ on: false, duration: interFlashGap });
|
|
2615
|
+
}
|
|
2616
|
+
if (gi < groups.length - 1) phases.push({ on: false, duration: interGroupGap });
|
|
2617
|
+
}
|
|
2618
|
+
phases.push({ on: false, duration: eclipseDur });
|
|
2619
|
+
} else if (isOcculting) {
|
|
2620
|
+
const numColours = Math.max(colours.length, 2);
|
|
2621
|
+
const offDur = 400;
|
|
2622
|
+
const onDur = (period - numColours * offDur) / numColours;
|
|
2623
|
+
for (let i = 0; i < numColours; i++) {
|
|
2624
|
+
phases.push({ on: true, duration: Math.max(onDur, 300), color: colours[i % colours.length] });
|
|
2625
|
+
phases.push({ on: false, duration: offDur });
|
|
2626
|
+
}
|
|
2627
|
+
} else if (isFixed) {
|
|
2628
|
+
const numColours = Math.max(colours.length, 2);
|
|
2629
|
+
const phaseDur = period / numColours;
|
|
2630
|
+
for (let i = 0; i < numColours; i++) {
|
|
2631
|
+
phases.push({ on: true, duration: phaseDur, color: colours[i % colours.length] });
|
|
2632
|
+
}
|
|
2633
|
+
} else {
|
|
2634
|
+
const numColours = Math.max(colours.length, 2);
|
|
2635
|
+
const phaseDur = period / numColours;
|
|
2636
|
+
for (let i = 0; i < numColours; i++) {
|
|
2637
|
+
phases.push({ on: true, duration: phaseDur, color: colours[i % colours.length] });
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
break;
|
|
2641
|
+
}
|
|
2642
|
+
default: {
|
|
2643
|
+
phases.push({ on: true, duration: 500, color: hex });
|
|
2644
|
+
phases.push({ on: false, duration: Math.max(period - 500, 500) });
|
|
2645
|
+
break;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
const totalCycle = phases.reduce((sum, p) => sum + p.duration, 0);
|
|
2649
|
+
return { phases, totalCycle, notation };
|
|
2650
|
+
}
|
|
2651
|
+
function buildSectorsFromFeatures(features) {
|
|
2652
|
+
const sectors = [];
|
|
2653
|
+
for (const f of features) {
|
|
2654
|
+
const props = f.properties ?? {};
|
|
2655
|
+
const s1 = props.SECTR1 != null ? Number(props.SECTR1) : null;
|
|
2656
|
+
const s2 = props.SECTR2 != null ? Number(props.SECTR2) : null;
|
|
2657
|
+
if (s1 == null || s2 == null || isNaN(s1) || isNaN(s2)) continue;
|
|
2658
|
+
const colourCode = parseCode(props.COLOUR);
|
|
2659
|
+
const ce = colourCode ? COLOUR_TABLE[colourCode] : null;
|
|
2660
|
+
const litchrCode = parseCode(props.LITCHR);
|
|
2661
|
+
const le = litchrCode ? LITCHR_TABLE[litchrCode] : null;
|
|
2662
|
+
const sigper = props.SIGPER != null ? Number(props.SIGPER) : null;
|
|
2663
|
+
const group = parseGroup(props.SIGGRP);
|
|
2664
|
+
const notation = buildNotation(le?.abbr ?? null, ce ? [ce.abbr] : [], sigper, group);
|
|
2665
|
+
const valnmr = props.VALNMR != null ? Number(props.VALNMR) : null;
|
|
2666
|
+
const nominalRange = valnmr != null && !isNaN(valnmr) && valnmr > 0 ? valnmr : null;
|
|
2667
|
+
sectors.push({
|
|
2668
|
+
sectorStart: s1,
|
|
2669
|
+
sectorEnd: s2,
|
|
2670
|
+
colour: ce?.name ?? "White",
|
|
2671
|
+
colourHex: ce?.hex ?? "#ffffff",
|
|
2672
|
+
notation,
|
|
2673
|
+
nominalRange
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
return sectors;
|
|
2677
|
+
}
|
|
2678
|
+
function buildSectorArcGeoJSON(center, startBearing, endBearing, radiusNm = 0.5, innerRadiusNm = 0, steps = 64) {
|
|
2679
|
+
const radiusDeg = radiusNm / 60;
|
|
2680
|
+
const innerDeg = innerRadiusNm > 0 ? innerRadiusNm / 60 : 0;
|
|
2681
|
+
const [lng, lat] = center;
|
|
2682
|
+
const cosLat = Math.cos(lat * Math.PI / 180);
|
|
2683
|
+
const s = (startBearing % 360 + 360) % 360;
|
|
2684
|
+
const e = (endBearing % 360 + 360) % 360;
|
|
2685
|
+
let sweep = e - s;
|
|
2686
|
+
if (sweep <= 0) sweep += 360;
|
|
2687
|
+
const project = (bearingDeg, rDeg) => {
|
|
2688
|
+
const rad = bearingDeg * Math.PI / 180;
|
|
2689
|
+
return [
|
|
2690
|
+
lng + rDeg * Math.sin(rad) / cosLat,
|
|
2691
|
+
lat + rDeg * Math.cos(rad)
|
|
2692
|
+
];
|
|
2693
|
+
};
|
|
2694
|
+
const outerCoords = [];
|
|
2695
|
+
for (let i = 0; i <= steps; i++) {
|
|
2696
|
+
outerCoords.push(project(s + sweep * i / steps, radiusDeg));
|
|
2697
|
+
}
|
|
2698
|
+
let ring;
|
|
2699
|
+
if (innerDeg > 0) {
|
|
2700
|
+
const innerCoords = [];
|
|
2701
|
+
for (let i = steps; i >= 0; i--) {
|
|
2702
|
+
innerCoords.push(project(s + sweep * i / steps, innerDeg));
|
|
2703
|
+
}
|
|
2704
|
+
ring = [...outerCoords, ...innerCoords, outerCoords[0]];
|
|
2705
|
+
} else {
|
|
2706
|
+
ring = [[lng, lat], ...outerCoords, [lng, lat]];
|
|
2707
|
+
}
|
|
2708
|
+
return {
|
|
2709
|
+
type: "Feature",
|
|
2710
|
+
properties: {},
|
|
2711
|
+
geometry: {
|
|
2712
|
+
type: "Polygon",
|
|
2713
|
+
coordinates: [ring]
|
|
2714
|
+
}
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
function buildSectorArcsFC(center, sectors, radiusNm = 0.4, innerRadiusFrac = 0.92) {
|
|
2718
|
+
const innerNm = innerRadiusFrac > 0 ? radiusNm * innerRadiusFrac : 0;
|
|
2719
|
+
const features = sectors.map((sec, i) => {
|
|
2720
|
+
const arc = buildSectorArcGeoJSON(center, sec.sectorStart, sec.sectorEnd, radiusNm, innerNm);
|
|
2721
|
+
arc.properties = {
|
|
2722
|
+
sectorIndex: i,
|
|
2723
|
+
colour: sec.colour,
|
|
2724
|
+
colourHex: sec.colourHex,
|
|
2725
|
+
notation: sec.notation,
|
|
2726
|
+
sectorStart: sec.sectorStart,
|
|
2727
|
+
sectorEnd: sec.sectorEnd,
|
|
2728
|
+
nominalRange: sec.nominalRange ?? null
|
|
2729
|
+
};
|
|
2730
|
+
return arc;
|
|
2731
|
+
});
|
|
2732
|
+
const lineFeatures = [];
|
|
2733
|
+
const labelFeatures = [];
|
|
2734
|
+
const seenBearings = /* @__PURE__ */ new Set();
|
|
2735
|
+
const TICK_OUTER_OVERHANG = 1.05;
|
|
2736
|
+
const TICK_INNER_OVERHANG = 0.93;
|
|
2737
|
+
for (const sec of sectors) {
|
|
2738
|
+
for (const bearing of [sec.sectorStart, sec.sectorEnd]) {
|
|
2739
|
+
const rad = bearing * Math.PI / 180;
|
|
2740
|
+
const radiusDeg = radiusNm / 60;
|
|
2741
|
+
const innerRadiusDeg = innerNm > 0 ? innerNm / 60 : 0;
|
|
2742
|
+
const cosLat = Math.cos(center[1] * Math.PI / 180);
|
|
2743
|
+
const sinB = Math.sin(rad);
|
|
2744
|
+
const cosB = Math.cos(rad);
|
|
2745
|
+
const tickOuterDeg = radiusDeg * TICK_OUTER_OVERHANG;
|
|
2746
|
+
const outerPt = [
|
|
2747
|
+
center[0] + tickOuterDeg * sinB / cosLat,
|
|
2748
|
+
center[1] + tickOuterDeg * cosB
|
|
2749
|
+
];
|
|
2750
|
+
const tickInnerDeg = innerRadiusDeg > 0 ? Math.min(innerRadiusDeg * TICK_INNER_OVERHANG, innerRadiusDeg) : 0;
|
|
2751
|
+
const innerPt = tickInnerDeg > 0 ? [center[0] + tickInnerDeg * sinB / cosLat, center[1] + tickInnerDeg * cosB] : center;
|
|
2752
|
+
lineFeatures.push({
|
|
2753
|
+
type: "Feature",
|
|
2754
|
+
properties: { type: "sector-boundary" },
|
|
2755
|
+
geometry: {
|
|
2756
|
+
type: "LineString",
|
|
2757
|
+
coordinates: [innerPt, outerPt]
|
|
2758
|
+
}
|
|
2759
|
+
});
|
|
2760
|
+
const roundedBearing = Math.round(bearing * 10) / 10;
|
|
2761
|
+
if (!seenBearings.has(roundedBearing)) {
|
|
2762
|
+
seenBearings.add(roundedBearing);
|
|
2763
|
+
const labelRadius = radiusDeg * 1.12;
|
|
2764
|
+
const ldlat = labelRadius * Math.cos(rad);
|
|
2765
|
+
const ldlng = labelRadius * Math.sin(rad) / cosLat;
|
|
2766
|
+
labelFeatures.push({
|
|
2767
|
+
type: "Feature",
|
|
2768
|
+
properties: {
|
|
2769
|
+
type: "bearing-label",
|
|
2770
|
+
bearing: `${roundedBearing}\xB0`,
|
|
2771
|
+
bearingDeg: roundedBearing
|
|
2772
|
+
},
|
|
2773
|
+
geometry: {
|
|
2774
|
+
type: "Point",
|
|
2775
|
+
coordinates: [center[0] + ldlng, center[1] + ldlat]
|
|
2776
|
+
}
|
|
2777
|
+
});
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
const rangeRingFeatures = [];
|
|
2782
|
+
const seenRanges = /* @__PURE__ */ new Set();
|
|
2783
|
+
for (const sec of sectors) {
|
|
2784
|
+
if (sec.nominalRange != null && sec.nominalRange > 0 && !seenRanges.has(sec.nominalRange)) {
|
|
2785
|
+
seenRanges.add(sec.nominalRange);
|
|
2786
|
+
const rangeDeg = sec.nominalRange / 60;
|
|
2787
|
+
const cosLat = Math.cos(center[1] * Math.PI / 180);
|
|
2788
|
+
const ringCoords = [];
|
|
2789
|
+
const ringSteps = 72;
|
|
2790
|
+
for (let i = 0; i <= ringSteps; i++) {
|
|
2791
|
+
const angle = 360 * i / ringSteps;
|
|
2792
|
+
const rad = angle * Math.PI / 180;
|
|
2793
|
+
const dlat = rangeDeg * Math.cos(rad);
|
|
2794
|
+
const dlng = rangeDeg * Math.sin(rad) / cosLat;
|
|
2795
|
+
ringCoords.push([center[0] + dlng, center[1] + dlat]);
|
|
2796
|
+
}
|
|
2797
|
+
rangeRingFeatures.push({
|
|
2798
|
+
type: "Feature",
|
|
2799
|
+
properties: { type: "range-ring", nominalRange: sec.nominalRange },
|
|
2800
|
+
geometry: { type: "LineString", coordinates: ringCoords }
|
|
2801
|
+
});
|
|
2802
|
+
rangeRingFeatures.push({
|
|
2803
|
+
type: "Feature",
|
|
2804
|
+
properties: { type: "range-label", rangeLabel: `${sec.nominalRange} NM` },
|
|
2805
|
+
geometry: { type: "Point", coordinates: [center[0], center[1] + rangeDeg] }
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
return {
|
|
2810
|
+
type: "FeatureCollection",
|
|
2811
|
+
features: [...features, ...lineFeatures, ...labelFeatures, ...rangeRingFeatures]
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
// src/interact/notices.ts
|
|
2816
|
+
var NON_SOLAS_DISCLAIMER = "\u26A0\uFE0F CHRT data is derived from official electronic navigational charts but is NOT FOR PRIMARY NAVIGATION and is NOT SOLAS-compliant. Always use official, type-approved ECDIS or paper charts for navigation safety. CHRT provides situational awareness data only.";
|
|
2817
|
+
var NON_SOLAS_DISCLAIMER_SHORT = "Not for primary navigation";
|
|
2818
|
+
function baseNotice() {
|
|
2819
|
+
return {
|
|
2820
|
+
disclaimer: NON_SOLAS_DISCLAIMER,
|
|
2821
|
+
attribution: "Derived from official Electronic Navigational Charts issued by national hydrographic offices (e.g. NOAA).",
|
|
2822
|
+
dataCurrency: "Tile attributes carry no per-feature edition/update metadata; call inspect({ enrich: true }) to resolve the source cell's edition, update_number and issue_date."
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
function enrichedNotice(cell) {
|
|
2826
|
+
const title = cell.title ? ` \u2014 ${cell.title}` : "";
|
|
2827
|
+
return {
|
|
2828
|
+
disclaimer: NON_SOLAS_DISCLAIMER,
|
|
2829
|
+
attribution: `Official ENC data \xA9 ${cell.chart_source} (cell ${cell.cell_id}${title}).`,
|
|
2830
|
+
dataCurrency: `Edition ${cell.edition ?? "unknown"}, update ${cell.update_number ?? "unknown"}, issued ${cell.issue_date ?? "unknown"}.`
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
// src/interact/errors.ts
|
|
2835
|
+
var InteractError = class extends Error {
|
|
2836
|
+
code;
|
|
2837
|
+
status;
|
|
2838
|
+
/** The team's current plan id, when the server included it (denials). */
|
|
2839
|
+
plan;
|
|
2840
|
+
constructor(code, message, opts) {
|
|
2841
|
+
super(message);
|
|
2842
|
+
this.name = "InteractError";
|
|
2843
|
+
this.code = code;
|
|
2844
|
+
this.status = opts?.status;
|
|
2845
|
+
this.plan = opts?.plan;
|
|
2846
|
+
}
|
|
2847
|
+
};
|
|
2848
|
+
|
|
2849
|
+
// src/interact/inspect.ts
|
|
2850
|
+
var NAV_AID_RAW_LAYERS = new Set(
|
|
2851
|
+
NAV_AID_QUERY_LAYERS.map((id) => stripLayerId(id))
|
|
2852
|
+
);
|
|
2853
|
+
var BRIDGE_RAW_LAYERS = new Set(BRIDGE_QUERY_LAYERS.map((id) => stripLayerId(id)));
|
|
2854
|
+
function stripLayerId(layerId) {
|
|
2855
|
+
return layerId.replace(/^(enc|s52)-/, "").replace(/-(point|line|polygon|midpoint|outline)$/, "").replace(/-(point|line|polygon|midpoint)$/, "");
|
|
2856
|
+
}
|
|
2857
|
+
function resolveLayerId(feature, opts) {
|
|
2858
|
+
if (opts?.layer) {
|
|
2859
|
+
return opts.layer.includes("-") || opts.layer.startsWith("enc") ? opts.layer : `enc-${opts.layer}`;
|
|
2860
|
+
}
|
|
2861
|
+
const layer = feature.layer;
|
|
2862
|
+
if (typeof layer === "string" && layer) {
|
|
2863
|
+
return layer.includes("-") ? layer : `enc-${layer}`;
|
|
2864
|
+
}
|
|
2865
|
+
if (layer && typeof layer === "object" && typeof layer.id === "string" && layer.id) {
|
|
2866
|
+
return layer.id;
|
|
2867
|
+
}
|
|
2868
|
+
if (feature.sourceLayer) return `enc-${feature.sourceLayer}`;
|
|
2869
|
+
throw new InteractError(
|
|
2870
|
+
"unknown_layer",
|
|
2871
|
+
"inspect(): cannot determine the feature's chart layer \u2014 pass { layer } or a feature from queryRenderedFeatures()"
|
|
2872
|
+
);
|
|
2873
|
+
}
|
|
2874
|
+
function resolveCoordinates(feature, opts) {
|
|
2875
|
+
if (opts?.coordinates) return opts.coordinates;
|
|
2876
|
+
const geom = feature.geometry;
|
|
2877
|
+
if (geom && geom.type === "Point" && Array.isArray(geom.coordinates)) {
|
|
2878
|
+
const [lng, lat] = geom.coordinates;
|
|
2879
|
+
if (typeof lng === "number" && typeof lat === "number") return [lng, lat];
|
|
2880
|
+
}
|
|
2881
|
+
return null;
|
|
2882
|
+
}
|
|
2883
|
+
function navAidPropertyRows(decoded) {
|
|
2884
|
+
const rows = [];
|
|
2885
|
+
const push = (label, value) => {
|
|
2886
|
+
if (value == null || value === "") return;
|
|
2887
|
+
rows.push({ label, value: String(value) });
|
|
2888
|
+
};
|
|
2889
|
+
push("Colour", decoded.colour);
|
|
2890
|
+
push("Shape", decoded.shape);
|
|
2891
|
+
push("Topmark", decoded.topmark);
|
|
2892
|
+
push("Topmark meaning", decoded.topmarkMeaning);
|
|
2893
|
+
push("Light", decoded.lightCharacterFull);
|
|
2894
|
+
push("Characteristic", decoded.notation || null);
|
|
2895
|
+
if (decoded.orientation != null) push("Orientation", `${decoded.orientation}\xB0`);
|
|
2896
|
+
push("Fog Signal", decoded.fogSignal);
|
|
2897
|
+
if (decoded.radarReflector) push("Radar", "Reflector");
|
|
2898
|
+
if (decoded.depth != null) push("Depth", `${decoded.depth}m`);
|
|
2899
|
+
push("Status", decoded.status);
|
|
2900
|
+
push("System", decoded.markingSystem);
|
|
2901
|
+
push("Category", decoded.category);
|
|
2902
|
+
return rows;
|
|
2903
|
+
}
|
|
2904
|
+
function bridgePropertyRows(decoded) {
|
|
2905
|
+
const rows = [];
|
|
2906
|
+
const push = (label, value) => {
|
|
2907
|
+
if (value == null || value === "") return;
|
|
2908
|
+
rows.push({ label, value });
|
|
2909
|
+
};
|
|
2910
|
+
push("Category", decoded.categoryLabel);
|
|
2911
|
+
if (decoded.verticalClearanceClosed != null && decoded.verticalClearanceOpen != null) {
|
|
2912
|
+
push(
|
|
2913
|
+
"Vertical clearance",
|
|
2914
|
+
`Closed: ${decoded.verticalClearanceClosed}m / Open: ${decoded.verticalClearanceOpen}m`
|
|
2915
|
+
);
|
|
2916
|
+
} else if (decoded.verticalClearance != null) {
|
|
2917
|
+
push("Vertical clearance", `${decoded.verticalClearance}m`);
|
|
2918
|
+
} else if (decoded.verticalClearanceClosed != null) {
|
|
2919
|
+
push("Vertical clearance", `${decoded.verticalClearanceClosed}m (closed)`);
|
|
2920
|
+
} else if (decoded.verticalClearanceOpen != null) {
|
|
2921
|
+
push("Vertical clearance", `${decoded.verticalClearanceOpen}m (open)`);
|
|
2922
|
+
}
|
|
2923
|
+
if (decoded.horizontalClearance != null) {
|
|
2924
|
+
push("Horizontal clearance", `${decoded.horizontalClearance}m`);
|
|
2925
|
+
}
|
|
2926
|
+
push("Status", decoded.status);
|
|
2927
|
+
push("Condition", decoded.condition);
|
|
2928
|
+
push("National name", decoded.nationalName);
|
|
2929
|
+
push("Source", decoded.sourceIndication);
|
|
2930
|
+
push("Source date", decoded.sourceDate);
|
|
2931
|
+
return rows;
|
|
2932
|
+
}
|
|
2933
|
+
function buildInspectResult(feature, opts) {
|
|
2934
|
+
const layerId = resolveLayerId(feature, opts);
|
|
2935
|
+
const rawLayer = stripLayerId(layerId);
|
|
2936
|
+
const props = { ...feature.properties ?? {} };
|
|
2937
|
+
const coords = resolveCoordinates(feature, opts);
|
|
2938
|
+
const notice = baseNotice();
|
|
2939
|
+
if (BRIDGE_RAW_LAYERS.has(rawLayer)) {
|
|
2940
|
+
const bridge = decodeBridgeProps(props);
|
|
2941
|
+
const result2 = {
|
|
2942
|
+
kind: "bridge",
|
|
2943
|
+
layer: "BRIDGE",
|
|
2944
|
+
layerId,
|
|
2945
|
+
objectClass: bridge.categoryLabel ?? "Bridge",
|
|
2946
|
+
title: bridge.name || bridge.categoryLabel || "Bridge",
|
|
2947
|
+
coordinates: coords,
|
|
2948
|
+
properties: bridgePropertyRows(bridge),
|
|
2949
|
+
raw: props,
|
|
2950
|
+
notice,
|
|
2951
|
+
enrichment: null,
|
|
2952
|
+
bridge
|
|
2953
|
+
};
|
|
2954
|
+
return result2;
|
|
2955
|
+
}
|
|
2956
|
+
if (NAV_AID_RAW_LAYERS.has(rawLayer)) {
|
|
2957
|
+
const navAid = decodeNavAidProps(layerId, props, coords ?? void 0);
|
|
2958
|
+
const flashPattern = buildFlashPattern(navAid);
|
|
2959
|
+
const result2 = {
|
|
2960
|
+
kind: "navaid",
|
|
2961
|
+
layer: navAid.layerName,
|
|
2962
|
+
layerId,
|
|
2963
|
+
objectClass: navAid.type,
|
|
2964
|
+
title: navAid.fullName || navAid.name || navAid.type,
|
|
2965
|
+
coordinates: navAid.coordinates,
|
|
2966
|
+
properties: navAidPropertyRows(navAid),
|
|
2967
|
+
raw: props,
|
|
2968
|
+
notice,
|
|
2969
|
+
enrichment: null,
|
|
2970
|
+
navAid,
|
|
2971
|
+
characteristic: navAid.notation || null,
|
|
2972
|
+
flashPattern
|
|
2973
|
+
};
|
|
2974
|
+
return result2;
|
|
2975
|
+
}
|
|
2976
|
+
const area = decodeAreaFeatureProps(layerId, props);
|
|
2977
|
+
const result = {
|
|
2978
|
+
kind: "area",
|
|
2979
|
+
layer: rawLayer,
|
|
2980
|
+
layerId,
|
|
2981
|
+
objectClass: area.type,
|
|
2982
|
+
title: area.title,
|
|
2983
|
+
coordinates: coords,
|
|
2984
|
+
properties: area.properties,
|
|
2985
|
+
raw: props,
|
|
2986
|
+
notice,
|
|
2987
|
+
enrichment: null,
|
|
2988
|
+
area
|
|
2989
|
+
};
|
|
2990
|
+
return result;
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
// src/interact/attach.ts
|
|
2994
|
+
function bboxAround(point, pad) {
|
|
2995
|
+
return [
|
|
2996
|
+
[point.x - pad, point.y - pad],
|
|
2997
|
+
[point.x + pad, point.y + pad]
|
|
2998
|
+
];
|
|
2999
|
+
}
|
|
3000
|
+
function existingLayers(map, ids) {
|
|
3001
|
+
return ids.filter((id) => {
|
|
3002
|
+
try {
|
|
3003
|
+
return Boolean(map.getLayer(id));
|
|
3004
|
+
} catch {
|
|
3005
|
+
return false;
|
|
3006
|
+
}
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
function query(map, geometry, layers) {
|
|
3010
|
+
if (layers.length === 0) return [];
|
|
3011
|
+
try {
|
|
3012
|
+
return map.queryRenderedFeatures(geometry, { layers }) ?? [];
|
|
3013
|
+
} catch {
|
|
3014
|
+
return [];
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
function featureLayerId(f) {
|
|
3018
|
+
const layer = f.layer;
|
|
3019
|
+
if (layer && typeof layer === "object" && typeof layer.id === "string") return layer.id;
|
|
3020
|
+
if (typeof layer === "string") return layer;
|
|
3021
|
+
return f.sourceLayer ? `enc-${f.sourceLayer}` : "";
|
|
3022
|
+
}
|
|
3023
|
+
function decodeClickedNavAid(map, feature, e, navAidLayers) {
|
|
3024
|
+
const layerId = featureLayerId(feature);
|
|
3025
|
+
const props = { ...feature.properties ?? {} };
|
|
3026
|
+
const geom = feature.geometry;
|
|
3027
|
+
const coords = geom?.type === "Point" && Array.isArray(geom.coordinates) ? [geom.coordinates[0], geom.coordinates[1]] : [e.lngLat.lng, e.lngLat.lat];
|
|
3028
|
+
const rawLayer = stripLayerId(layerId);
|
|
3029
|
+
if (!props.OBJNAM && CHILD_LAYERS.has(rawLayer)) {
|
|
3030
|
+
const parentLayers = existingLayers(map, PARENT_LAYERS);
|
|
3031
|
+
const parents = query(map, bboxAround(e.point, 8), parentLayers);
|
|
3032
|
+
for (const p of parents) {
|
|
3033
|
+
const pName = p.properties?.OBJNAM;
|
|
3034
|
+
if (pName) {
|
|
3035
|
+
props.OBJNAM = pName;
|
|
3036
|
+
if (!props.COLOUR && p.properties?.COLOUR) {
|
|
3037
|
+
props.COLOUR = p.properties.COLOUR;
|
|
3038
|
+
}
|
|
3039
|
+
break;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
if (!props.CATFOG) {
|
|
3044
|
+
const fogFeats = query(map, bboxAround(e.point, 10), existingLayers(map, ["enc-FOGSIG"]));
|
|
3045
|
+
for (const f of fogFeats) {
|
|
3046
|
+
if (f.properties?.CATFOG) {
|
|
3047
|
+
props.CATFOG = f.properties.CATFOG;
|
|
3048
|
+
break;
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
if (!props.RETRFL) {
|
|
3053
|
+
const radarFeats = query(map, bboxAround(e.point, 10), existingLayers(map, ["enc-RTPBCN"]));
|
|
3054
|
+
if (radarFeats.length > 0) props.RETRFL = "1";
|
|
3055
|
+
}
|
|
3056
|
+
if (!props.TOPSHP) {
|
|
3057
|
+
const topmarFeats = query(map, bboxAround(e.point, 10), existingLayers(map, ["enc-TOPMAR"]));
|
|
3058
|
+
for (const f of topmarFeats) {
|
|
3059
|
+
if (f.properties?.TOPSHP) {
|
|
3060
|
+
props.TOPSHP = f.properties.TOPSHP;
|
|
3061
|
+
break;
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
const lightLayerIds = existingLayers(
|
|
3066
|
+
map,
|
|
3067
|
+
navAidLayers.filter((id) => id === "enc-LIGHTS" || id === "enc-LIGHTS-point")
|
|
3068
|
+
);
|
|
3069
|
+
let coLocatedLightFeats = null;
|
|
3070
|
+
const isLightsLayer = layerId.includes("LIGHTS");
|
|
3071
|
+
if (!props.LITCHR) {
|
|
3072
|
+
const lightFeats = query(map, bboxAround(e.point, 10), lightLayerIds);
|
|
3073
|
+
if (lightFeats.length > 0) {
|
|
3074
|
+
coLocatedLightFeats = lightFeats;
|
|
3075
|
+
let primaryLight = null;
|
|
3076
|
+
for (const lf of lightFeats) {
|
|
3077
|
+
if (lf.properties?.LITCHR) {
|
|
3078
|
+
if (!lf.properties?.SECTR1 && !lf.properties?.SECTR2) {
|
|
3079
|
+
primaryLight = lf;
|
|
3080
|
+
break;
|
|
3081
|
+
}
|
|
3082
|
+
if (!primaryLight) primaryLight = lf;
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
if (primaryLight?.properties) {
|
|
3086
|
+
props.LITCHR = primaryLight.properties.LITCHR;
|
|
3087
|
+
if (!props.SIGPER && primaryLight.properties.SIGPER) props.SIGPER = primaryLight.properties.SIGPER;
|
|
3088
|
+
if (!props.SIGGRP && primaryLight.properties.SIGGRP) props.SIGGRP = primaryLight.properties.SIGGRP;
|
|
3089
|
+
if (primaryLight.properties.COLOUR) props.LIGHT_COLOUR = primaryLight.properties.COLOUR;
|
|
3090
|
+
if (primaryLight.properties.CATLIT) props.CATLIT = primaryLight.properties.CATLIT;
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
const result = buildInspectResult(
|
|
3095
|
+
{ ...feature, properties: props },
|
|
3096
|
+
{ layer: layerId, coordinates: coords }
|
|
3097
|
+
);
|
|
3098
|
+
if (result.kind === "navaid") {
|
|
3099
|
+
let sectorSources = null;
|
|
3100
|
+
if (isLightsLayer) {
|
|
3101
|
+
const nearby = query(map, bboxAround(e.point, 12), lightLayerIds);
|
|
3102
|
+
if (nearby.length > 1) sectorSources = nearby;
|
|
3103
|
+
} else if (coLocatedLightFeats && coLocatedLightFeats.length > 0) {
|
|
3104
|
+
sectorSources = coLocatedLightFeats;
|
|
3105
|
+
}
|
|
3106
|
+
if (sectorSources) {
|
|
3107
|
+
const sectors = buildSectorsFromFeatures(
|
|
3108
|
+
sectorSources.map((f) => ({ properties: f.properties ?? {} }))
|
|
3109
|
+
);
|
|
3110
|
+
if (sectors.length > 0) {
|
|
3111
|
+
result.navAid.sectors = sectors;
|
|
3112
|
+
result.navAid.isSectorLight = true;
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
return result;
|
|
3117
|
+
}
|
|
3118
|
+
function decodeClick(map, e, opts) {
|
|
3119
|
+
const navAidLayers = opts.navAidLayers ?? NAV_AID_QUERY_LAYERS;
|
|
3120
|
+
const bridgeLayers = opts.bridgeLayers ?? BRIDGE_QUERY_LAYERS;
|
|
3121
|
+
const areaLayers = opts.areaLayers ?? AREA_QUERY_LAYERS;
|
|
3122
|
+
const navFeatures = query(map, e.point, existingLayers(map, navAidLayers));
|
|
3123
|
+
if (navFeatures.length > 0) {
|
|
3124
|
+
const feature = navFeatures[0];
|
|
3125
|
+
return { result: decodeClickedNavAid(map, feature, e, navAidLayers), feature };
|
|
3126
|
+
}
|
|
3127
|
+
const bridgeFeatures = query(map, e.point, existingLayers(map, bridgeLayers));
|
|
3128
|
+
if (bridgeFeatures.length > 0) {
|
|
3129
|
+
const feature = bridgeFeatures[0];
|
|
3130
|
+
const result = buildInspectResult(feature, {
|
|
3131
|
+
layer: featureLayerId(feature) || "enc-BRIDGE",
|
|
3132
|
+
coordinates: [e.lngLat.lng, e.lngLat.lat]
|
|
3133
|
+
});
|
|
3134
|
+
return { result, feature };
|
|
3135
|
+
}
|
|
3136
|
+
const areaFeatures = query(map, e.point, existingLayers(map, areaLayers));
|
|
3137
|
+
if (areaFeatures.length > 0) {
|
|
3138
|
+
const feature = areaFeatures[0];
|
|
3139
|
+
const result = buildInspectResult(feature, {
|
|
3140
|
+
layer: featureLayerId(feature),
|
|
3141
|
+
coordinates: [e.lngLat.lng, e.lngLat.lat]
|
|
3142
|
+
});
|
|
3143
|
+
return { result, feature };
|
|
3144
|
+
}
|
|
3145
|
+
return null;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
// src/interact/entitlement.ts
|
|
3149
|
+
function mapDenialCode(raw, status) {
|
|
3150
|
+
if (raw === "interact_not_enabled") return "interact_not_enabled";
|
|
3151
|
+
if (status === 401) return "invalid_api_key";
|
|
3152
|
+
if (status === 403) return "missing_scope";
|
|
3153
|
+
return "unexpected_response";
|
|
3154
|
+
}
|
|
3155
|
+
var EntitlementClient = class {
|
|
3156
|
+
constructor(http) {
|
|
3157
|
+
this.http = http;
|
|
3158
|
+
}
|
|
3159
|
+
cached = null;
|
|
3160
|
+
pending = null;
|
|
3161
|
+
/**
|
|
3162
|
+
* Check (and cache) the interact entitlement for this client's API key.
|
|
3163
|
+
* Concurrent callers share one in-flight request; a denial is NOT cached so
|
|
3164
|
+
* a plan upgrade takes effect on the next call.
|
|
3165
|
+
*/
|
|
3166
|
+
async check() {
|
|
3167
|
+
if (this.cached) return this.cached;
|
|
3168
|
+
if (this.pending) return this.pending;
|
|
3169
|
+
this.pending = (async () => {
|
|
3170
|
+
try {
|
|
3171
|
+
const result = await this.checkOnce();
|
|
3172
|
+
this.cached = result;
|
|
3173
|
+
return result;
|
|
3174
|
+
} finally {
|
|
3175
|
+
this.pending = null;
|
|
3176
|
+
}
|
|
3177
|
+
})();
|
|
3178
|
+
return this.pending;
|
|
3179
|
+
}
|
|
3180
|
+
async checkOnce() {
|
|
3181
|
+
let res;
|
|
3182
|
+
try {
|
|
3183
|
+
res = await this.http.postJson("/interact/entitlement", {});
|
|
3184
|
+
} catch (err) {
|
|
3185
|
+
throw new InteractError(
|
|
3186
|
+
"network_error",
|
|
3187
|
+
err instanceof Error ? err.message : "entitlement request failed"
|
|
3188
|
+
);
|
|
3189
|
+
}
|
|
3190
|
+
if (!res.ok) {
|
|
3191
|
+
const code = mapDenialCode(res.body?.error, res.status);
|
|
3192
|
+
const message = typeof res.body?.message === "string" ? res.body.message : code === "interact_not_enabled" ? "CHRTinteract requires the Dev plan or above. Upgrade at https://chrt.co/pricing." : `entitlement check failed (HTTP ${res.status})`;
|
|
3193
|
+
const plan = typeof res.body?.plan === "string" ? res.body.plan : void 0;
|
|
3194
|
+
throw new InteractError(code, message, { status: res.status, plan });
|
|
3195
|
+
}
|
|
3196
|
+
if (res.body?.enabled !== true) {
|
|
3197
|
+
throw new InteractError(
|
|
3198
|
+
"unexpected_response",
|
|
3199
|
+
"entitlement endpoint returned a malformed body",
|
|
3200
|
+
{ status: res.status }
|
|
3201
|
+
);
|
|
3202
|
+
}
|
|
3203
|
+
return {
|
|
3204
|
+
enabled: true,
|
|
3205
|
+
...typeof res.body.plan === "string" ? { plan: res.body.plan } : {}
|
|
3206
|
+
};
|
|
3207
|
+
}
|
|
3208
|
+
/** Drop the cached grant (e.g. after an API-key swap in tests). */
|
|
3209
|
+
reset() {
|
|
3210
|
+
this.cached = null;
|
|
3211
|
+
this.pending = null;
|
|
3212
|
+
}
|
|
3213
|
+
};
|
|
3214
|
+
|
|
3215
|
+
// src/interact/enrich.ts
|
|
3216
|
+
var ENRICH_BBOX_PAD_DEG = 1e-3;
|
|
3217
|
+
var ENRICH_LIMIT = 5;
|
|
3218
|
+
function featurePoint(f) {
|
|
3219
|
+
if (f.geometry?.type === "Point" && Array.isArray(f.geometry.coordinates)) {
|
|
3220
|
+
const [lng, lat] = f.geometry.coordinates;
|
|
3221
|
+
if (typeof lng === "number" && typeof lat === "number") return [lng, lat];
|
|
3222
|
+
}
|
|
3223
|
+
return null;
|
|
3224
|
+
}
|
|
3225
|
+
function distSq(a, b) {
|
|
3226
|
+
return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2;
|
|
3227
|
+
}
|
|
3228
|
+
function matchEnrichedFeature(candidates, raw, coords) {
|
|
3229
|
+
if (candidates.length === 0) return null;
|
|
3230
|
+
const objnam = typeof raw.OBJNAM === "string" && raw.OBJNAM ? raw.OBJNAM : null;
|
|
3231
|
+
if (objnam) {
|
|
3232
|
+
const byName = candidates.find(
|
|
3233
|
+
(f) => f.properties?.OBJNAM === objnam
|
|
3234
|
+
);
|
|
3235
|
+
if (byName) return byName;
|
|
3236
|
+
}
|
|
3237
|
+
if (coords) {
|
|
3238
|
+
let best = null;
|
|
3239
|
+
let bestD = Infinity;
|
|
3240
|
+
for (const f of candidates) {
|
|
3241
|
+
const pt = featurePoint(f);
|
|
3242
|
+
if (!pt) continue;
|
|
3243
|
+
const d = distSq(pt, coords);
|
|
3244
|
+
if (d < bestD) {
|
|
3245
|
+
bestD = d;
|
|
3246
|
+
best = f;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
if (best) return best;
|
|
3250
|
+
}
|
|
3251
|
+
return candidates[0];
|
|
3252
|
+
}
|
|
3253
|
+
async function enrichResult(http, result, signal) {
|
|
3254
|
+
const coords = result.coordinates;
|
|
3255
|
+
if (!coords) {
|
|
3256
|
+
throw new InteractError(
|
|
3257
|
+
"enrich_failed",
|
|
3258
|
+
"enrich: the feature has no coordinates to look up \u2014 pass { coordinates } to inspect()"
|
|
3259
|
+
);
|
|
3260
|
+
}
|
|
3261
|
+
const [lng, lat] = coords;
|
|
3262
|
+
const bbox = [
|
|
3263
|
+
lng - ENRICH_BBOX_PAD_DEG,
|
|
3264
|
+
lat - ENRICH_BBOX_PAD_DEG,
|
|
3265
|
+
lng + ENRICH_BBOX_PAD_DEG,
|
|
3266
|
+
lat + ENRICH_BBOX_PAD_DEG
|
|
3267
|
+
].join(",");
|
|
3268
|
+
let fc;
|
|
3269
|
+
try {
|
|
3270
|
+
fc = await http.request(
|
|
3271
|
+
"/features",
|
|
3272
|
+
{ bbox, layer: result.layer, limit: ENRICH_LIMIT },
|
|
3273
|
+
signal
|
|
3274
|
+
);
|
|
3275
|
+
} catch (err) {
|
|
3276
|
+
if (err instanceof ChrtError) {
|
|
3277
|
+
throw new InteractError("enrich_failed", `features lookup failed: ${err.message}`, {
|
|
3278
|
+
status: err.status
|
|
3279
|
+
});
|
|
3280
|
+
}
|
|
3281
|
+
throw new InteractError(
|
|
3282
|
+
"network_error",
|
|
3283
|
+
err instanceof Error ? err.message : "features lookup failed"
|
|
3284
|
+
);
|
|
3285
|
+
}
|
|
3286
|
+
const matched = matchEnrichedFeature(fc.features ?? [], result.raw, coords);
|
|
3287
|
+
const enrichment = {
|
|
3288
|
+
matched: matched != null,
|
|
3289
|
+
properties: matched ? matched.properties : null,
|
|
3290
|
+
cellId: null,
|
|
3291
|
+
cell: null
|
|
3292
|
+
};
|
|
3293
|
+
if (matched) {
|
|
3294
|
+
const cellId = matched.properties?.cell_id;
|
|
3295
|
+
if (typeof cellId === "string" && cellId) {
|
|
3296
|
+
enrichment.cellId = cellId;
|
|
3297
|
+
try {
|
|
3298
|
+
const cell = await http.request(
|
|
3299
|
+
`/cells/${encodeURIComponent(cellId)}`,
|
|
3300
|
+
void 0,
|
|
3301
|
+
signal
|
|
3302
|
+
);
|
|
3303
|
+
enrichment.cell = cell;
|
|
3304
|
+
result.notice = enrichedNotice(cell);
|
|
3305
|
+
} catch {
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
result.enrichment = enrichment;
|
|
3310
|
+
return result;
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
// src/interact/popup.ts
|
|
3314
|
+
var POPUP_CLASS = "chrt-interact-popup";
|
|
3315
|
+
var POPUP_CSS = `
|
|
3316
|
+
.${POPUP_CLASS} {
|
|
3317
|
+
position: absolute;
|
|
3318
|
+
z-index: 40;
|
|
3319
|
+
min-width: 220px;
|
|
3320
|
+
max-width: 320px;
|
|
3321
|
+
transform: translate(-50%, calc(-100% - 12px));
|
|
3322
|
+
background: var(--chrt-interact-bg, #0b1524);
|
|
3323
|
+
color: var(--chrt-interact-fg, #e2e8f0);
|
|
3324
|
+
border: 1px solid var(--chrt-interact-border, rgba(148, 163, 184, 0.25));
|
|
3325
|
+
border-radius: 8px;
|
|
3326
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
|
3327
|
+
font: 12px/1.45 var(--chrt-interact-font, ui-sans-serif, system-ui, sans-serif);
|
|
3328
|
+
pointer-events: auto;
|
|
3329
|
+
}
|
|
3330
|
+
.${POPUP_CLASS} * { box-sizing: border-box; }
|
|
3331
|
+
.${POPUP_CLASS}__inner { padding: 10px 12px 8px; }
|
|
3332
|
+
.${POPUP_CLASS}__close {
|
|
3333
|
+
position: absolute; top: 4px; right: 6px;
|
|
3334
|
+
border: 0; background: none; color: inherit; opacity: 0.55;
|
|
3335
|
+
font-size: 14px; line-height: 1; cursor: pointer; padding: 4px;
|
|
3336
|
+
}
|
|
3337
|
+
.${POPUP_CLASS}__close:hover { opacity: 1; }
|
|
3338
|
+
.${POPUP_CLASS}__title { font-weight: 600; font-size: 13px; padding-right: 16px; }
|
|
3339
|
+
.${POPUP_CLASS}__subtitle { color: var(--chrt-interact-muted, #94a3b8); margin-top: 1px; }
|
|
3340
|
+
.${POPUP_CLASS}__badge {
|
|
3341
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
3342
|
+
margin-top: 6px; padding: 2px 8px;
|
|
3343
|
+
border: 1px solid var(--chrt-interact-border, rgba(148, 163, 184, 0.25));
|
|
3344
|
+
border-radius: 4px;
|
|
3345
|
+
font-family: var(--chrt-interact-mono, ui-monospace, monospace);
|
|
3346
|
+
font-weight: 600; font-size: 11px;
|
|
3347
|
+
}
|
|
3348
|
+
.${POPUP_CLASS}__dot {
|
|
3349
|
+
width: 10px; height: 10px; border-radius: 50%;
|
|
3350
|
+
background: #555; opacity: 0.15;
|
|
3351
|
+
}
|
|
3352
|
+
.${POPUP_CLASS}__rows { margin-top: 8px; border-top: 1px solid var(--chrt-interact-border, rgba(148,163,184,0.18)); padding-top: 6px; }
|
|
3353
|
+
.${POPUP_CLASS}__row { display: flex; justify-content: space-between; gap: 12px; padding: 1px 0; }
|
|
3354
|
+
.${POPUP_CLASS}__label { color: var(--chrt-interact-muted, #94a3b8); flex-shrink: 0; }
|
|
3355
|
+
.${POPUP_CLASS}__value { text-align: right; font-weight: 500; }
|
|
3356
|
+
.${POPUP_CLASS}__info { margin-top: 4px; font-style: italic; color: var(--chrt-interact-muted, #94a3b8); font-size: 11px; }
|
|
3357
|
+
.${POPUP_CLASS}__footer {
|
|
3358
|
+
margin-top: 8px; padding: 6px 12px 8px;
|
|
3359
|
+
border-top: 1px solid var(--chrt-interact-border, rgba(148,163,184,0.18));
|
|
3360
|
+
color: var(--chrt-interact-muted, #94a3b8);
|
|
3361
|
+
font-size: 10px;
|
|
3362
|
+
}
|
|
3363
|
+
.${POPUP_CLASS}__footer strong { color: var(--chrt-interact-warn, #fbbf24); font-weight: 600; }
|
|
3364
|
+
`;
|
|
3365
|
+
var styleInjected = false;
|
|
3366
|
+
function ensureStyles(doc) {
|
|
3367
|
+
if (styleInjected && doc.getElementById(`${POPUP_CLASS}-style`)) return;
|
|
3368
|
+
const style = doc.createElement("style");
|
|
3369
|
+
style.id = `${POPUP_CLASS}-style`;
|
|
3370
|
+
style.textContent = POPUP_CSS;
|
|
3371
|
+
doc.head.appendChild(style);
|
|
3372
|
+
styleInjected = true;
|
|
3373
|
+
}
|
|
3374
|
+
function el(doc, tag, className, text) {
|
|
3375
|
+
const node = doc.createElement(tag);
|
|
3376
|
+
node.className = className;
|
|
3377
|
+
if (text != null) node.textContent = text;
|
|
3378
|
+
return node;
|
|
3379
|
+
}
|
|
3380
|
+
function animateDot(dot, pattern) {
|
|
3381
|
+
if (pattern.totalCycle <= 0 || typeof requestAnimationFrame === "undefined") {
|
|
3382
|
+
return () => void 0;
|
|
3383
|
+
}
|
|
3384
|
+
let raf = 0;
|
|
3385
|
+
const start = performance.now();
|
|
3386
|
+
const tick = (now2) => {
|
|
3387
|
+
const elapsed = (now2 - start) % pattern.totalCycle;
|
|
3388
|
+
let acc = 0;
|
|
3389
|
+
let phase = pattern.phases[0];
|
|
3390
|
+
for (const p of pattern.phases) {
|
|
3391
|
+
acc += p.duration;
|
|
3392
|
+
if (elapsed < acc) {
|
|
3393
|
+
phase = p;
|
|
3394
|
+
break;
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
if (phase.on) {
|
|
3398
|
+
const color = phase.color || "#ffffff";
|
|
3399
|
+
dot.style.opacity = "1";
|
|
3400
|
+
dot.style.backgroundColor = color;
|
|
3401
|
+
dot.style.boxShadow = `0 0 8px 2px ${color}88`;
|
|
3402
|
+
} else {
|
|
3403
|
+
dot.style.opacity = "0.15";
|
|
3404
|
+
dot.style.backgroundColor = "#555";
|
|
3405
|
+
dot.style.boxShadow = "none";
|
|
3406
|
+
}
|
|
3407
|
+
raf = requestAnimationFrame(tick);
|
|
3408
|
+
};
|
|
3409
|
+
raf = requestAnimationFrame(tick);
|
|
3410
|
+
return () => cancelAnimationFrame(raf);
|
|
3411
|
+
}
|
|
3412
|
+
var InteractPopup = class {
|
|
3413
|
+
constructor(map) {
|
|
3414
|
+
this.map = map;
|
|
3415
|
+
}
|
|
3416
|
+
root = null;
|
|
3417
|
+
stopAnimation = null;
|
|
3418
|
+
lngLat = null;
|
|
3419
|
+
reposition = () => this.place();
|
|
3420
|
+
show(result, lngLat) {
|
|
3421
|
+
const container = this.map.getContainer();
|
|
3422
|
+
const doc = container.ownerDocument;
|
|
3423
|
+
ensureStyles(doc);
|
|
3424
|
+
this.close();
|
|
3425
|
+
const root = el(doc, "div", POPUP_CLASS);
|
|
3426
|
+
const inner = el(doc, "div", `${POPUP_CLASS}__inner`);
|
|
3427
|
+
const close = el(doc, "button", `${POPUP_CLASS}__close`, "\u2715");
|
|
3428
|
+
close.setAttribute("aria-label", "Close");
|
|
3429
|
+
close.addEventListener("click", () => this.close());
|
|
3430
|
+
root.appendChild(close);
|
|
3431
|
+
inner.appendChild(el(doc, "div", `${POPUP_CLASS}__title`, result.title));
|
|
3432
|
+
const subtitleParts = [result.objectClass];
|
|
3433
|
+
if (result.kind === "navaid") {
|
|
3434
|
+
if (result.navAid.shape) subtitleParts.push(result.navAid.shape);
|
|
3435
|
+
if (result.navAid.isSectorLight) subtitleParts.push("Sector");
|
|
3436
|
+
if (result.navAid.isDirectional) subtitleParts.push("Directional");
|
|
3437
|
+
}
|
|
3438
|
+
inner.appendChild(
|
|
3439
|
+
el(doc, "div", `${POPUP_CLASS}__subtitle`, subtitleParts.filter(Boolean).join(" \xB7 "))
|
|
3440
|
+
);
|
|
3441
|
+
if (result.kind === "navaid" && result.characteristic) {
|
|
3442
|
+
const badge = el(doc, "div", `${POPUP_CLASS}__badge`);
|
|
3443
|
+
const dot = el(doc, "span", `${POPUP_CLASS}__dot`);
|
|
3444
|
+
badge.appendChild(dot);
|
|
3445
|
+
badge.appendChild(doc.createTextNode(result.characteristic));
|
|
3446
|
+
if (result.navAid.lightColourHex && result.navAid.lightColourHex !== "#ffffff") {
|
|
3447
|
+
badge.style.color = result.navAid.lightColourHex;
|
|
3448
|
+
}
|
|
3449
|
+
inner.appendChild(badge);
|
|
3450
|
+
if (result.flashPattern) {
|
|
3451
|
+
this.stopAnimation = animateDot(dot, result.flashPattern);
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
if (result.properties.length > 0) {
|
|
3455
|
+
const rows = el(doc, "div", `${POPUP_CLASS}__rows`);
|
|
3456
|
+
for (const p of result.properties) {
|
|
3457
|
+
const row = el(doc, "div", `${POPUP_CLASS}__row`);
|
|
3458
|
+
row.appendChild(el(doc, "span", `${POPUP_CLASS}__label`, p.label));
|
|
3459
|
+
row.appendChild(el(doc, "span", `${POPUP_CLASS}__value`, p.value));
|
|
3460
|
+
rows.appendChild(row);
|
|
3461
|
+
}
|
|
3462
|
+
inner.appendChild(rows);
|
|
3463
|
+
}
|
|
3464
|
+
if (result.kind === "navaid" && result.navAid.information) {
|
|
3465
|
+
inner.appendChild(el(doc, "div", `${POPUP_CLASS}__info`, result.navAid.information));
|
|
3466
|
+
}
|
|
3467
|
+
root.appendChild(inner);
|
|
3468
|
+
const footer = el(doc, "div", `${POPUP_CLASS}__footer`);
|
|
3469
|
+
footer.title = `${NON_SOLAS_DISCLAIMER}
|
|
3470
|
+
|
|
3471
|
+
${result.notice.attribution}
|
|
3472
|
+
${result.notice.dataCurrency}`;
|
|
3473
|
+
const warn = el(doc, "strong", "", `\u26A0 ${NON_SOLAS_DISCLAIMER_SHORT}`);
|
|
3474
|
+
footer.appendChild(warn);
|
|
3475
|
+
footer.appendChild(doc.createElement("br"));
|
|
3476
|
+
footer.appendChild(doc.createTextNode(result.notice.attribution));
|
|
3477
|
+
const currency = result.notice.dataCurrency;
|
|
3478
|
+
if (currency.startsWith("Edition")) {
|
|
3479
|
+
footer.appendChild(doc.createElement("br"));
|
|
3480
|
+
footer.appendChild(doc.createTextNode(currency));
|
|
3481
|
+
}
|
|
3482
|
+
root.appendChild(footer);
|
|
3483
|
+
const position = typeof getComputedStyle !== "undefined" ? getComputedStyle(container).position : container.style.position;
|
|
3484
|
+
if (position === "static" || position === "") {
|
|
3485
|
+
container.style.position = "relative";
|
|
3486
|
+
}
|
|
3487
|
+
container.appendChild(root);
|
|
3488
|
+
this.root = root;
|
|
3489
|
+
this.lngLat = lngLat;
|
|
3490
|
+
this.place();
|
|
3491
|
+
this.map.on("move", this.reposition);
|
|
3492
|
+
}
|
|
3493
|
+
place() {
|
|
3494
|
+
if (!this.root || !this.lngLat) return;
|
|
3495
|
+
const { x, y } = this.map.project(this.lngLat);
|
|
3496
|
+
this.root.style.left = `${x}px`;
|
|
3497
|
+
this.root.style.top = `${y}px`;
|
|
3498
|
+
}
|
|
3499
|
+
close() {
|
|
3500
|
+
if (this.stopAnimation) {
|
|
3501
|
+
this.stopAnimation();
|
|
3502
|
+
this.stopAnimation = null;
|
|
3503
|
+
}
|
|
3504
|
+
if (this.root) {
|
|
3505
|
+
this.map.off("move", this.reposition);
|
|
3506
|
+
this.root.remove();
|
|
3507
|
+
this.root = null;
|
|
3508
|
+
}
|
|
3509
|
+
this.lngLat = null;
|
|
3510
|
+
}
|
|
3511
|
+
};
|
|
3512
|
+
|
|
3513
|
+
// src/interact/index.ts
|
|
3514
|
+
var InteractClient = class {
|
|
3515
|
+
constructor(http) {
|
|
3516
|
+
this.http = http;
|
|
3517
|
+
this.entitlementClient = new EntitlementClient(http);
|
|
3518
|
+
}
|
|
3519
|
+
entitlementClient;
|
|
3520
|
+
/**
|
|
3521
|
+
* Check the CHRTinteract entitlement for this API key (server-side, plan
|
|
3522
|
+
* derived, cached per client). Throws `InteractError` with code
|
|
3523
|
+
* `interact_not_enabled` for Free-plan teams.
|
|
3524
|
+
*/
|
|
3525
|
+
async entitlement() {
|
|
3526
|
+
return this.entitlementClient.check();
|
|
3527
|
+
}
|
|
3528
|
+
/**
|
|
3529
|
+
* Decode a chart feature (from `queryRenderedFeatures`, the features API,
|
|
3530
|
+
* or plain `{ layer, properties }`) into a typed {@link InspectResult}.
|
|
3531
|
+
*
|
|
3532
|
+
* The first call performs the server-side entitlement check. With
|
|
3533
|
+
* `{ enrich: true }` the result is upgraded with full S-57 properties and
|
|
3534
|
+
* source-cell provenance (edition / update / issue date) via the features
|
|
3535
|
+
* API — metered against the key's `featureCalls` quota.
|
|
3536
|
+
*
|
|
3537
|
+
* @example
|
|
3538
|
+
* const result = await chrt.interact.inspect(feature);
|
|
3539
|
+
* if (result.kind === "navaid") console.log(result.characteristic); // "Fl R 4s"
|
|
3540
|
+
*/
|
|
3541
|
+
async inspect(feature, opts) {
|
|
3542
|
+
await this.entitlementClient.check();
|
|
3543
|
+
const result = buildInspectResult(feature, opts);
|
|
3544
|
+
if (opts?.enrich) {
|
|
3545
|
+
return enrichResult(this.http, result, opts.signal);
|
|
3546
|
+
}
|
|
3547
|
+
return result;
|
|
3548
|
+
}
|
|
3549
|
+
/**
|
|
3550
|
+
* Attach click-to-inspect to a MapLibre / Mapbox GL map rendering CHRT
|
|
3551
|
+
* tiles. Resolves once the server-side entitlement check passes; rejects
|
|
3552
|
+
* with `InteractError` (`interact_not_enabled` on Free plans) otherwise —
|
|
3553
|
+
* no listeners are wired on denial.
|
|
3554
|
+
*
|
|
3555
|
+
* Headless-first: pass `onInspect` to receive every decoded
|
|
3556
|
+
* {@link InspectResult} and render your own UI. Without `onInspect`, the
|
|
3557
|
+
* built-in minimal popup is shown (opt back in explicitly with
|
|
3558
|
+
* `popup: true` to have both).
|
|
3559
|
+
*
|
|
3560
|
+
* @example
|
|
3561
|
+
* const handle = await chrt.interact.attach(map, {
|
|
3562
|
+
* onInspect: (result) => console.log(result.title, result.characteristic),
|
|
3563
|
+
* });
|
|
3564
|
+
* // later: handle.detach();
|
|
3565
|
+
*/
|
|
3566
|
+
async attach(map, opts = {}) {
|
|
3567
|
+
if (!map || typeof map.queryRenderedFeatures !== "function" || typeof map.on !== "function") {
|
|
3568
|
+
throw new InteractError(
|
|
3569
|
+
"unexpected_response",
|
|
3570
|
+
"attach(): expected a MapLibre / Mapbox GL map instance"
|
|
1158
3571
|
);
|
|
1159
3572
|
}
|
|
1160
|
-
await
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
3573
|
+
await this.entitlementClient.check();
|
|
3574
|
+
const showPopup = opts.popup ?? !opts.onInspect;
|
|
3575
|
+
const popup = showPopup ? new InteractPopup(map) : null;
|
|
3576
|
+
const clickHandler = (e) => {
|
|
3577
|
+
const hit = decodeClick(map, e, opts);
|
|
3578
|
+
if (!hit) {
|
|
3579
|
+
popup?.close();
|
|
3580
|
+
opts.onMiss?.({ lngLat: e.lngLat });
|
|
3581
|
+
return;
|
|
3582
|
+
}
|
|
3583
|
+
const finish = (result) => {
|
|
3584
|
+
popup?.show(result, result.coordinates ?? [e.lngLat.lng, e.lngLat.lat]);
|
|
3585
|
+
opts.onInspect?.(result, { lngLat: e.lngLat, point: e.point, feature: hit.feature });
|
|
3586
|
+
};
|
|
3587
|
+
if (opts.enrich) {
|
|
3588
|
+
enrichResult(this.http, hit.result).then(finish).catch(() => finish(hit.result));
|
|
3589
|
+
} else {
|
|
3590
|
+
finish(hit.result);
|
|
3591
|
+
}
|
|
1169
3592
|
};
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
path,
|
|
1177
|
-
bytes: bundle.bytes,
|
|
1178
|
-
sha256: bundle.sha256,
|
|
1179
|
-
build_id: bundle.build_id,
|
|
1180
|
-
build_tag: bundle.build_tag,
|
|
1181
|
-
downloaded_at: nowSec,
|
|
1182
|
-
last_accessed_at: nowSec
|
|
3593
|
+
map.on("click", clickHandler);
|
|
3594
|
+
return {
|
|
3595
|
+
detach: () => {
|
|
3596
|
+
map.off("click", clickHandler);
|
|
3597
|
+
popup?.close();
|
|
3598
|
+
}
|
|
1183
3599
|
};
|
|
1184
|
-
await writeManifest(this.manifestPath, fs, manifest);
|
|
1185
3600
|
}
|
|
1186
3601
|
};
|
|
1187
|
-
function dirname(p) {
|
|
1188
|
-
const i = p.lastIndexOf("/");
|
|
1189
|
-
const j = p.lastIndexOf("\\");
|
|
1190
|
-
const k = Math.max(i, j);
|
|
1191
|
-
if (k <= 0) return ".";
|
|
1192
|
-
return p.slice(0, k);
|
|
1193
|
-
}
|
|
1194
3602
|
|
|
1195
3603
|
// src/atlas/s52-colors.ts
|
|
1196
3604
|
var S52_DAY = {
|
|
@@ -6914,6 +9322,118 @@ var LABEL_ANCHOR_FILTER = [
|
|
|
6914
9322
|
["get", "_label_anchor"],
|
|
6915
9323
|
false
|
|
6916
9324
|
];
|
|
9325
|
+
var LABEL_PRIORITY = {
|
|
9326
|
+
LIGHTS: 100,
|
|
9327
|
+
UWTROC: 100,
|
|
9328
|
+
OBSTRN: 100,
|
|
9329
|
+
WRECKS: 100,
|
|
9330
|
+
CBLOHD: 100,
|
|
9331
|
+
PIPOHD: 100,
|
|
9332
|
+
BOYLAT: 200,
|
|
9333
|
+
BOYCAR: 200,
|
|
9334
|
+
BOYSPP: 200,
|
|
9335
|
+
BOYISD: 200,
|
|
9336
|
+
BOYSAW: 200,
|
|
9337
|
+
BCNLAT: 200,
|
|
9338
|
+
BCNCAR: 200,
|
|
9339
|
+
BCNSPP: 200,
|
|
9340
|
+
FOGSIG: 250,
|
|
9341
|
+
SOUNDG: 300,
|
|
9342
|
+
DEPCNT: 300,
|
|
9343
|
+
BRIDGE: 400,
|
|
9344
|
+
LNDMRK: 400,
|
|
9345
|
+
PILPNT: 400,
|
|
9346
|
+
MORFAC: 400,
|
|
9347
|
+
SLCONS: 400,
|
|
9348
|
+
BUISGL: 400,
|
|
9349
|
+
SBDARE: 500,
|
|
9350
|
+
MAGVAR: 500,
|
|
9351
|
+
WEDKLP: 500
|
|
9352
|
+
};
|
|
9353
|
+
var DEFAULT_LABEL_PRIORITY = 450;
|
|
9354
|
+
var ORIENTATION_LABEL_PRIORITY = 900;
|
|
9355
|
+
function getLabelPriority(layerName) {
|
|
9356
|
+
if (getLabelBehavior(layerName) === "orientation-cross-band") {
|
|
9357
|
+
return ORIENTATION_LABEL_PRIORITY;
|
|
9358
|
+
}
|
|
9359
|
+
return LABEL_PRIORITY[layerName] ?? DEFAULT_LABEL_PRIORITY;
|
|
9360
|
+
}
|
|
9361
|
+
function labelSortKey(rank, within) {
|
|
9362
|
+
if (within === void 0) return rank * 1e3;
|
|
9363
|
+
return [
|
|
9364
|
+
"+",
|
|
9365
|
+
rank * 1e3,
|
|
9366
|
+
["min", 999, ["max", 0, within]]
|
|
9367
|
+
];
|
|
9368
|
+
}
|
|
9369
|
+
var NAV_DESIGNATION_STOPWORDS = [
|
|
9370
|
+
"Buoy",
|
|
9371
|
+
"Can",
|
|
9372
|
+
"Nun",
|
|
9373
|
+
"Bell",
|
|
9374
|
+
"Gong",
|
|
9375
|
+
"Mark"
|
|
9376
|
+
];
|
|
9377
|
+
function extractNavAidDesignation(objnam) {
|
|
9378
|
+
const trimmed = objnam.trim();
|
|
9379
|
+
if (/^[A-Za-z0-9]{1,4}$/.test(trimmed)) {
|
|
9380
|
+
return NAV_DESIGNATION_STOPWORDS.some((w) => w === trimmed) ? null : trimmed;
|
|
9381
|
+
}
|
|
9382
|
+
const m = trimmed.match(/ ([A-Za-z0-9]{1,4})$/);
|
|
9383
|
+
if (!m) return null;
|
|
9384
|
+
const token = m[1];
|
|
9385
|
+
if (NAV_DESIGNATION_STOPWORDS.some((w) => w === token)) return null;
|
|
9386
|
+
return token;
|
|
9387
|
+
}
|
|
9388
|
+
function navAidDesignationTextExpression(objnam, colourPrefix) {
|
|
9389
|
+
const len = ["length", objnam];
|
|
9390
|
+
const tokenBranches = [];
|
|
9391
|
+
for (let k = 1; k <= 4; k++) {
|
|
9392
|
+
tokenBranches.push(
|
|
9393
|
+
[
|
|
9394
|
+
"all",
|
|
9395
|
+
[">", len, k],
|
|
9396
|
+
["==", ["slice", objnam, ["-", len, k + 1], ["-", len, k]], " "]
|
|
9397
|
+
],
|
|
9398
|
+
["slice", objnam, ["-", len, k]]
|
|
9399
|
+
);
|
|
9400
|
+
}
|
|
9401
|
+
return [
|
|
9402
|
+
"let",
|
|
9403
|
+
"desig",
|
|
9404
|
+
["case", ...tokenBranches, ""],
|
|
9405
|
+
[
|
|
9406
|
+
"case",
|
|
9407
|
+
[
|
|
9408
|
+
"any",
|
|
9409
|
+
["==", ["var", "desig"], ""],
|
|
9410
|
+
["match", ["var", "desig"], [...NAV_DESIGNATION_STOPWORDS], true, false]
|
|
9411
|
+
],
|
|
9412
|
+
["concat", colourPrefix, objnam],
|
|
9413
|
+
["concat", colourPrefix, '"', ["var", "desig"], '"']
|
|
9414
|
+
]
|
|
9415
|
+
];
|
|
9416
|
+
}
|
|
9417
|
+
function navAidLabelLayout(sortKey) {
|
|
9418
|
+
return {
|
|
9419
|
+
"text-allow-overlap": false,
|
|
9420
|
+
"text-ignore-placement": false,
|
|
9421
|
+
"text-optional": true,
|
|
9422
|
+
"text-padding": 4,
|
|
9423
|
+
"symbol-sort-key": sortKey
|
|
9424
|
+
};
|
|
9425
|
+
}
|
|
9426
|
+
var NAV_AID_SORT_KEY = labelSortKey(200, [
|
|
9427
|
+
"case",
|
|
9428
|
+
["has", "VALNMR"],
|
|
9429
|
+
["-", 300, ["min", 299, ["to-number", ["get", "VALNMR"], 0]]],
|
|
9430
|
+
["+", 400, ["min", 500, ["length", ["coalesce", ["get", "OBJNAM"], ""]]]]
|
|
9431
|
+
]);
|
|
9432
|
+
var LIGHT_SORT_KEY = labelSortKey(100, [
|
|
9433
|
+
"-",
|
|
9434
|
+
300,
|
|
9435
|
+
["min", 299, ["to-number", ["coalesce", ["get", "VALNMR"], 0], 0]]
|
|
9436
|
+
]);
|
|
6917
9437
|
function getLabelConfig(layerName, opts = {}) {
|
|
6918
9438
|
const ct = opts.colorTable ?? "DAY";
|
|
6919
9439
|
switch (layerName) {
|
|
@@ -6981,14 +9501,15 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
6981
9501
|
// sizes — established cartographic typography practice.
|
|
6982
9502
|
"text-letter-spacing": 0.02,
|
|
6983
9503
|
// Shallowest-wins: Mapbox GL renders lower sort keys first and
|
|
6984
|
-
// gives them collision priority.
|
|
6985
|
-
// the shallowest sounding in a cluster is always the
|
|
6986
|
-
// even when labels are hidden due to overlap.
|
|
6987
|
-
|
|
9504
|
+
// gives them collision priority. Depth as the within-class key
|
|
9505
|
+
// ensures the shallowest sounding in a cluster is always the
|
|
9506
|
+
// one shown, even when labels are hidden due to overlap. Folded
|
|
9507
|
+
// into the rank-300 band of the LABEL_PRIORITY ladder.
|
|
9508
|
+
"symbol-sort-key": labelSortKey(300, [
|
|
6988
9509
|
"to-number",
|
|
6989
9510
|
["coalesce", ["get", "depth"], ["get", "DEPTH"], ["get", "VALSOU"]],
|
|
6990
9511
|
999
|
|
6991
|
-
]
|
|
9512
|
+
])
|
|
6992
9513
|
}
|
|
6993
9514
|
};
|
|
6994
9515
|
}
|
|
@@ -7028,8 +9549,14 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
7028
9549
|
textColor: DARK_BG,
|
|
7029
9550
|
textHaloColor: "#d4c9b0",
|
|
7030
9551
|
textHaloWidth: 1.5,
|
|
7031
|
-
textOffset: [0,
|
|
7032
|
-
minzoom: 9
|
|
9552
|
+
textOffset: [0, 0],
|
|
9553
|
+
minzoom: 9,
|
|
9554
|
+
layout: {
|
|
9555
|
+
"text-variable-anchor": ["top", "bottom", "right", "left"],
|
|
9556
|
+
"text-radial-offset": 1.2,
|
|
9557
|
+
"text-justify": "auto",
|
|
9558
|
+
"symbol-sort-key": labelSortKey(400, ["length", ["coalesce", ["get", "OBJNAM"], ""]])
|
|
9559
|
+
}
|
|
7033
9560
|
};
|
|
7034
9561
|
case "BOYLAT":
|
|
7035
9562
|
return {
|
|
@@ -7041,7 +9568,8 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
7041
9568
|
textHaloColor: navHalo("WHITE", ct),
|
|
7042
9569
|
textHaloWidth: 1.5,
|
|
7043
9570
|
textOffset: [0, 1.5],
|
|
7044
|
-
minzoom: 10
|
|
9571
|
+
minzoom: 10,
|
|
9572
|
+
layout: navAidLabelLayout(NAV_AID_SORT_KEY)
|
|
7045
9573
|
};
|
|
7046
9574
|
case "BCNLAT":
|
|
7047
9575
|
return {
|
|
@@ -7053,7 +9581,8 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
7053
9581
|
textHaloColor: navHalo("WHITE", ct),
|
|
7054
9582
|
textHaloWidth: 1.5,
|
|
7055
9583
|
textOffset: [0, 1.5],
|
|
7056
|
-
minzoom: 10
|
|
9584
|
+
minzoom: 10,
|
|
9585
|
+
layout: navAidLabelLayout(NAV_AID_SORT_KEY)
|
|
7057
9586
|
};
|
|
7058
9587
|
case "BOYCAR":
|
|
7059
9588
|
case "BCNCAR":
|
|
@@ -7066,7 +9595,8 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
7066
9595
|
textHaloColor: navHalo("CREAM", ct),
|
|
7067
9596
|
textHaloWidth: 1.5,
|
|
7068
9597
|
textOffset: [0, 1.5],
|
|
7069
|
-
minzoom: 10
|
|
9598
|
+
minzoom: 10,
|
|
9599
|
+
layout: navAidLabelLayout(NAV_AID_SORT_KEY)
|
|
7070
9600
|
};
|
|
7071
9601
|
case "BOYSPP":
|
|
7072
9602
|
case "BCNSPP":
|
|
@@ -7079,7 +9609,8 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
7079
9609
|
textHaloColor: navHalo("YELLOW", ct),
|
|
7080
9610
|
textHaloWidth: 1.5,
|
|
7081
9611
|
textOffset: [0, 1.5],
|
|
7082
|
-
minzoom: 10
|
|
9612
|
+
minzoom: 10,
|
|
9613
|
+
layout: navAidLabelLayout(NAV_AID_SORT_KEY)
|
|
7083
9614
|
};
|
|
7084
9615
|
case "BOYISD":
|
|
7085
9616
|
case "BOYSAW":
|
|
@@ -7092,7 +9623,8 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
7092
9623
|
textHaloColor: navHalo("WHITE", ct),
|
|
7093
9624
|
textHaloWidth: 1.5,
|
|
7094
9625
|
textOffset: [0, 1.5],
|
|
7095
|
-
minzoom: 10
|
|
9626
|
+
minzoom: 10,
|
|
9627
|
+
layout: navAidLabelLayout(NAV_AID_SORT_KEY)
|
|
7096
9628
|
};
|
|
7097
9629
|
case "LIGHTS":
|
|
7098
9630
|
return {
|
|
@@ -7106,7 +9638,12 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
7106
9638
|
textOffset: [2, 0],
|
|
7107
9639
|
minzoom: 9,
|
|
7108
9640
|
layout: {
|
|
7109
|
-
"text-anchor": "left"
|
|
9641
|
+
"text-anchor": "left",
|
|
9642
|
+
"text-allow-overlap": false,
|
|
9643
|
+
"text-ignore-placement": false,
|
|
9644
|
+
"text-optional": true,
|
|
9645
|
+
"text-padding": 2,
|
|
9646
|
+
"symbol-sort-key": LIGHT_SORT_KEY
|
|
7110
9647
|
}
|
|
7111
9648
|
};
|
|
7112
9649
|
case "FOGSIG": {
|
|
@@ -7144,7 +9681,8 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
7144
9681
|
layout: {
|
|
7145
9682
|
"text-anchor": "left",
|
|
7146
9683
|
"text-allow-overlap": false,
|
|
7147
|
-
"text-optional": true
|
|
9684
|
+
"text-optional": true,
|
|
9685
|
+
"symbol-sort-key": labelSortKey(250)
|
|
7148
9686
|
}
|
|
7149
9687
|
};
|
|
7150
9688
|
}
|
|
@@ -7317,11 +9855,15 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
7317
9855
|
textColor: "#bfa88a",
|
|
7318
9856
|
textHaloColor: DARK_BG,
|
|
7319
9857
|
textHaloWidth: 1.5,
|
|
7320
|
-
textOffset: [0,
|
|
9858
|
+
textOffset: [0, 0],
|
|
7321
9859
|
minzoom: 12,
|
|
7322
9860
|
layout: {
|
|
9861
|
+
"text-variable-anchor": ["top", "bottom", "right", "left"],
|
|
9862
|
+
"text-radial-offset": 1,
|
|
9863
|
+
"text-justify": "auto",
|
|
7323
9864
|
"text-allow-overlap": false,
|
|
7324
|
-
"text-optional": true
|
|
9865
|
+
"text-optional": true,
|
|
9866
|
+
"symbol-sort-key": labelSortKey(400, ["length", ["coalesce", ["get", "OBJNAM"], ""]])
|
|
7325
9867
|
}
|
|
7326
9868
|
};
|
|
7327
9869
|
case "PILPNT":
|
|
@@ -7545,17 +10087,26 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
7545
10087
|
textColor: resolveColor("CHBLK", ct2),
|
|
7546
10088
|
textHaloColor: resolveColor("CHWHT", ct2),
|
|
7547
10089
|
textHaloWidth: 1.2,
|
|
7548
|
-
|
|
7549
|
-
// of the asterisk / cross. Anchor top-left so the offset grows
|
|
7550
|
-
// away from the symbol as text-size grows.
|
|
7551
|
-
textOffset: [0.8, -0.4],
|
|
10090
|
+
textOffset: [0, 0],
|
|
7552
10091
|
textFont: ["Open Sans Regular"],
|
|
7553
10092
|
minzoom: 12,
|
|
10093
|
+
// Variable anchors: the depth callout tries each corner of the
|
|
10094
|
+
// hazard symbol and takes the first free spot, so in a rocky
|
|
10095
|
+
// field the callouts fan out around their symbols instead of
|
|
10096
|
+
// overprinting or vanishing. Safe here (unlike nav-aids) because
|
|
10097
|
+
// UWTROC/OBSTRN/WRECKS are dominance-PARTICIPATING classes —
|
|
10098
|
+
// exactly one band's copy renders at any zoom, so anchor-shifting
|
|
10099
|
+
// can't split a cross-band duplicate pair into two visible labels.
|
|
10100
|
+
// Shallowest (most dangerous) hazard wins within-class contests;
|
|
10101
|
+
// VALSOU-less hazards (awash / unsounded) rank first of all.
|
|
7554
10102
|
layout: {
|
|
7555
|
-
"text-anchor": "top-left",
|
|
10103
|
+
"text-variable-anchor": ["top-left", "bottom-left", "top-right", "bottom-right"],
|
|
10104
|
+
"text-radial-offset": 0.9,
|
|
10105
|
+
"text-justify": "auto",
|
|
7556
10106
|
"text-allow-overlap": false,
|
|
7557
10107
|
"text-optional": true,
|
|
7558
|
-
"text-padding": 2
|
|
10108
|
+
"text-padding": 2,
|
|
10109
|
+
"symbol-sort-key": labelSortKey(100, ["*", 10, valsou])
|
|
7559
10110
|
}
|
|
7560
10111
|
};
|
|
7561
10112
|
}
|
|
@@ -7594,7 +10145,13 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
7594
10145
|
"text-rotation-alignment": "map",
|
|
7595
10146
|
"text-allow-overlap": false,
|
|
7596
10147
|
"text-optional": true,
|
|
7597
|
-
"text-letter-spacing": 0.02
|
|
10148
|
+
"text-letter-spacing": 0.02,
|
|
10149
|
+
// Lower clearance = more dangerous = places first.
|
|
10150
|
+
"symbol-sort-key": labelSortKey(100, [
|
|
10151
|
+
"*",
|
|
10152
|
+
5,
|
|
10153
|
+
["to-number", ["coalesce", ["get", "VERCLR"], 199], 199]
|
|
10154
|
+
])
|
|
7598
10155
|
}
|
|
7599
10156
|
};
|
|
7600
10157
|
}
|
|
@@ -7623,7 +10180,10 @@ function getLabelConfig(layerName, opts = {}) {
|
|
|
7623
10180
|
"symbol-spacing": 250,
|
|
7624
10181
|
"text-rotation-alignment": "map",
|
|
7625
10182
|
"text-allow-overlap": false,
|
|
7626
|
-
"text-optional": true
|
|
10183
|
+
"text-optional": true,
|
|
10184
|
+
// Shallower contours win within-class contests, mirroring the
|
|
10185
|
+
// SOUNDG shallowest-wins rule.
|
|
10186
|
+
"symbol-sort-key": labelSortKey(300, ["to-number", ["get", "VALDCO"], 999])
|
|
7627
10187
|
}
|
|
7628
10188
|
};
|
|
7629
10189
|
default:
|
|
@@ -7796,9 +10356,8 @@ function preprocessNavAids(fc) {
|
|
|
7796
10356
|
const props = f.properties ?? {};
|
|
7797
10357
|
const objnam = props.OBJNAM;
|
|
7798
10358
|
if (!objnam) return f;
|
|
7799
|
-
const
|
|
7800
|
-
if (!
|
|
7801
|
-
const designation = match[1];
|
|
10359
|
+
const designation = extractNavAidDesignation(objnam);
|
|
10360
|
+
if (!designation) return f;
|
|
7802
10361
|
const colourCode = firstListCode(props.COLOUR);
|
|
7803
10362
|
const prefix = NAV_COLOUR_PREFIX[colourCode];
|
|
7804
10363
|
const label = prefix ? `${prefix} "${designation}"` : `"${designation}"`;
|
|
@@ -9552,7 +12111,7 @@ function getS52SymbolStats() {
|
|
|
9552
12111
|
}
|
|
9553
12112
|
|
|
9554
12113
|
// src/atlas/tile-zoom.ts
|
|
9555
|
-
var BUILD_MINZOOM =
|
|
12114
|
+
var BUILD_MINZOOM = 0;
|
|
9556
12115
|
var BUILD_MAXZOOM = 14;
|
|
9557
12116
|
|
|
9558
12117
|
// src/atlas/source-registry.ts
|
|
@@ -9652,33 +12211,6 @@ var CHILD_ICON_OFFSETS = {
|
|
|
9652
12211
|
LIGHTS: [14, 14]
|
|
9653
12212
|
// lower-right, off the buoy hull so the flare is visible
|
|
9654
12213
|
};
|
|
9655
|
-
var SAFETY_CRITICAL_LABELS = /* @__PURE__ */ new Set([
|
|
9656
|
-
"LIGHTS",
|
|
9657
|
-
"SOUNDG",
|
|
9658
|
-
"BOYLAT",
|
|
9659
|
-
"BCNLAT",
|
|
9660
|
-
"BOYCAR",
|
|
9661
|
-
"BCNCAR",
|
|
9662
|
-
"BOYSPP",
|
|
9663
|
-
"BCNSPP",
|
|
9664
|
-
"BOYISD",
|
|
9665
|
-
"BOYSAW",
|
|
9666
|
-
"FOGSIG",
|
|
9667
|
-
"RTPBCN"
|
|
9668
|
-
]);
|
|
9669
|
-
var HIGH_DENSITY_NAV_AIDS = /* @__PURE__ */ new Set([
|
|
9670
|
-
"BOYLAT",
|
|
9671
|
-
"BOYCAR",
|
|
9672
|
-
"BOYSPP",
|
|
9673
|
-
"BOYISD",
|
|
9674
|
-
"BOYSAW",
|
|
9675
|
-
"BOYINB",
|
|
9676
|
-
"BCNLAT",
|
|
9677
|
-
"BCNCAR",
|
|
9678
|
-
"BCNSPP",
|
|
9679
|
-
"BCNISD",
|
|
9680
|
-
"BCNSAW"
|
|
9681
|
-
]);
|
|
9682
12214
|
function buildNavAidLabelExpression() {
|
|
9683
12215
|
const colourPrefix = colourCodeCase(
|
|
9684
12216
|
[
|
|
@@ -9693,7 +12225,12 @@ function buildNavAidLabelExpression() {
|
|
|
9693
12225
|
["has", "_navAidLabel"],
|
|
9694
12226
|
["get", "_navAidLabel"],
|
|
9695
12227
|
["has", "OBJNAM"],
|
|
9696
|
-
|
|
12228
|
+
// Chart-convention abbreviation: `R "4"` instead of the full NOAA
|
|
12229
|
+
// name ("R Boston North Channel Lighted Buoy 4") — the tile pipeline
|
|
12230
|
+
// doesn't precompute `_navAidLabel`, so before this expression every
|
|
12231
|
+
// production buoy label carried its full 30-40 glyph OBJNAM, the
|
|
12232
|
+
// biggest single driver of Harbor/Approach label overprint (E3).
|
|
12233
|
+
navAidDesignationTextExpression(["get", "OBJNAM"], colourPrefix),
|
|
9697
12234
|
""
|
|
9698
12235
|
];
|
|
9699
12236
|
}
|
|
@@ -9884,6 +12421,11 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION) {
|
|
|
9884
12421
|
layer.paint["icon-color"] = lightFillExpression(ct);
|
|
9885
12422
|
layer.paint["icon-halo-color"] = resolveColor("CHBLK", ct);
|
|
9886
12423
|
layer.paint["icon-halo-width"] = 1;
|
|
12424
|
+
layer.layout = {
|
|
12425
|
+
...layer.layout,
|
|
12426
|
+
"icon-allow-overlap": true,
|
|
12427
|
+
"icon-ignore-placement": true
|
|
12428
|
+
};
|
|
9887
12429
|
}
|
|
9888
12430
|
} else if (resolved.layout) {
|
|
9889
12431
|
layer.layout = resolved.layout;
|
|
@@ -10754,7 +13296,7 @@ function buildLabelLayer2(objectClass, ct, sourceId = DEFAULT_REGION, depthUnits
|
|
|
10754
13296
|
];
|
|
10755
13297
|
}
|
|
10756
13298
|
}
|
|
10757
|
-
const
|
|
13299
|
+
const labelPriority = getLabelPriority(objectClass);
|
|
10758
13300
|
const layer = {
|
|
10759
13301
|
id: `s52-${objectClass}-label`,
|
|
10760
13302
|
type: "symbol",
|
|
@@ -10767,15 +13309,20 @@ function buildLabelLayer2(objectClass, ct, sourceId = DEFAULT_REGION, depthUnits
|
|
|
10767
13309
|
"text-font": config.textFont ?? ["Open Sans Regular"],
|
|
10768
13310
|
"text-offset": config.textOffset,
|
|
10769
13311
|
"text-anchor": "top",
|
|
10770
|
-
"text-allow-overlap":
|
|
10771
|
-
"text-optional":
|
|
10772
|
-
"text-padding":
|
|
13312
|
+
"text-allow-overlap": false,
|
|
13313
|
+
"text-optional": true,
|
|
13314
|
+
"text-padding": 4,
|
|
13315
|
+
"symbol-sort-key": labelPriority * 1e3,
|
|
10773
13316
|
...config.layout ?? {}
|
|
10774
13317
|
},
|
|
10775
13318
|
paint: {
|
|
10776
13319
|
"text-color": config.textColor,
|
|
10777
13320
|
"text-halo-color": config.textHaloColor,
|
|
10778
13321
|
"text-halo-width": config.textHaloWidth
|
|
13322
|
+
},
|
|
13323
|
+
metadata: {
|
|
13324
|
+
"s52:objectClass": objectClass,
|
|
13325
|
+
"chrt:labelPriority": labelPriority
|
|
10779
13326
|
}
|
|
10780
13327
|
};
|
|
10781
13328
|
if (config.minzoom) {
|
|
@@ -10827,13 +13374,24 @@ function getOrderedClasses() {
|
|
|
10827
13374
|
|
|
10828
13375
|
// src/atlas/band-priority.ts
|
|
10829
13376
|
var BAND_PRIORITY_RANGES = {
|
|
10830
|
-
"1": { minzoom:
|
|
13377
|
+
"1": { minzoom: 0 },
|
|
10831
13378
|
"2": { minzoom: 5 },
|
|
10832
13379
|
"3": { minzoom: 7 },
|
|
10833
13380
|
"4": { minzoom: 9 },
|
|
10834
13381
|
"5": { minzoom: 11 },
|
|
10835
13382
|
"6": { minzoom: 11 }
|
|
10836
13383
|
};
|
|
13384
|
+
var BAND_DATA_MINZOOM = {
|
|
13385
|
+
"1": 0,
|
|
13386
|
+
"2": 5,
|
|
13387
|
+
"3": 7,
|
|
13388
|
+
"4": 9,
|
|
13389
|
+
"5": 11,
|
|
13390
|
+
"6": 13
|
|
13391
|
+
};
|
|
13392
|
+
function dominatedVariantMaxzoom(D) {
|
|
13393
|
+
return Math.max(BAND_PRIORITY_RANGES[D].minzoom, BAND_DATA_MINZOOM[D]);
|
|
13394
|
+
}
|
|
10837
13395
|
var BAND_DRAW_ORDER = ["1", "2", "3", "4", "5", "6"];
|
|
10838
13396
|
var CHART_BAND_CODES = {
|
|
10839
13397
|
Overview: ["1"],
|
|
@@ -10994,12 +13552,26 @@ var DOMINANCE_EXEMPT_CLASSES = /* @__PURE__ */ new Set([
|
|
|
10994
13552
|
"RTPBCN",
|
|
10995
13553
|
"RDOSTA"
|
|
10996
13554
|
]);
|
|
13555
|
+
var DOMINANCE_EXEMPT_FILL_CLASSES = /* @__PURE__ */ new Set([
|
|
13556
|
+
"LNDARE",
|
|
13557
|
+
"DEPARE",
|
|
13558
|
+
"UNSARE",
|
|
13559
|
+
"LAKARE",
|
|
13560
|
+
"DRGARE",
|
|
13561
|
+
"SEAARE"
|
|
13562
|
+
]);
|
|
10997
13563
|
function isDominanceExempt(layer) {
|
|
10998
13564
|
for (const cls of DOMINANCE_EXEMPT_CLASSES) {
|
|
10999
13565
|
if (layer.id.startsWith(`s52-${cls}-`)) return true;
|
|
11000
13566
|
}
|
|
13567
|
+
if (layer.type === "fill") {
|
|
13568
|
+
for (const cls of DOMINANCE_EXEMPT_FILL_CLASSES) {
|
|
13569
|
+
if (layer.id.startsWith(`s52-${cls}-`)) return true;
|
|
13570
|
+
}
|
|
13571
|
+
}
|
|
11001
13572
|
return false;
|
|
11002
13573
|
}
|
|
13574
|
+
var TOP_STACK_LIGHT_CLASSES = /* @__PURE__ */ new Set(["LIGHTS"]);
|
|
11003
13575
|
function makeBandVariants(layer, B) {
|
|
11004
13576
|
const range = BAND_PRIORITY_RANGES[B];
|
|
11005
13577
|
const classMinzoom = layer.minzoom ?? 0;
|
|
@@ -11030,8 +13602,8 @@ function makeBandVariants(layer, B) {
|
|
|
11030
13602
|
for (const D of BAND_DRAW_ORDER) {
|
|
11031
13603
|
const dInt = parseInt(D, 10);
|
|
11032
13604
|
if (dInt <= bInt) continue;
|
|
11033
|
-
const
|
|
11034
|
-
if (
|
|
13605
|
+
const dMaxzoom = dominatedVariantMaxzoom(D);
|
|
13606
|
+
if (dMaxzoom <= minzoom) continue;
|
|
11035
13607
|
const domVariant = {
|
|
11036
13608
|
...layer,
|
|
11037
13609
|
id: `${layer.id}-band${B}-domBy${D}`,
|
|
@@ -11040,7 +13612,7 @@ function makeBandVariants(layer, B) {
|
|
|
11040
13612
|
["all", bandFilterExpression(B), dominatedByMinFilter(D)]
|
|
11041
13613
|
),
|
|
11042
13614
|
minzoom,
|
|
11043
|
-
maxzoom:
|
|
13615
|
+
maxzoom: dMaxzoom
|
|
11044
13616
|
};
|
|
11045
13617
|
variants.push(domVariant);
|
|
11046
13618
|
}
|
|
@@ -11087,46 +13659,26 @@ function fanOrientationLabelByCellBand(label) {
|
|
|
11087
13659
|
paint
|
|
11088
13660
|
});
|
|
11089
13661
|
for (const D of BAND_DRAW_ORDER) {
|
|
11090
|
-
const
|
|
11091
|
-
if (
|
|
13662
|
+
const dMaxzoom = dominatedVariantMaxzoom(D);
|
|
13663
|
+
if (dMaxzoom <= minzoom) continue;
|
|
11092
13664
|
variants.push({
|
|
11093
13665
|
...label,
|
|
11094
13666
|
id: `${label.id}-cell${code}-domBy${D}`,
|
|
11095
13667
|
filter: combineWithBandFilter(cellFiltered, dominatedByMinFilter(D)),
|
|
11096
13668
|
minzoom,
|
|
11097
|
-
|
|
13669
|
+
// Data-aware ceiling: band 6 has no tile data until z13 despite
|
|
13670
|
+
// being style-eligible at z11 (see dominatedVariantMaxzoom).
|
|
13671
|
+
maxzoom: dMaxzoom,
|
|
11098
13672
|
paint
|
|
11099
13673
|
});
|
|
11100
13674
|
}
|
|
11101
13675
|
}
|
|
11102
13676
|
return variants;
|
|
11103
13677
|
}
|
|
11104
|
-
function
|
|
11105
|
-
|
|
11106
|
-
|
|
11107
|
-
|
|
11108
|
-
return {
|
|
11109
|
-
...variant,
|
|
11110
|
-
layout: {
|
|
11111
|
-
...oldLayout,
|
|
11112
|
-
"text-allow-overlap": false,
|
|
11113
|
-
"text-optional": true,
|
|
11114
|
-
// Wider padding so even allowed labels space visually instead of
|
|
11115
|
-
// touching. 6 is enough to read individual buoy designations clearly
|
|
11116
|
-
// at z12-14 over Boston Harbor.
|
|
11117
|
-
"text-padding": 6,
|
|
11118
|
-
// Deterministic collision arbitration: prefer features with
|
|
11119
|
-
// longer-range nav-aid significance (VALNMR — visible nautical
|
|
11120
|
-
// miles), then shorter OBJNAM (tighter labels win), then FIDN as
|
|
11121
|
-
// tiebreaker. Lower sort-key wins in MapLibre.
|
|
11122
|
-
"symbol-sort-key": [
|
|
11123
|
-
"case",
|
|
11124
|
-
["has", "VALNMR"],
|
|
11125
|
-
["-", 0, ["coalesce", ["to-number", ["get", "VALNMR"]], 0]],
|
|
11126
|
-
["length", ["coalesce", ["get", "OBJNAM"], ""]]
|
|
11127
|
-
]
|
|
11128
|
-
}
|
|
11129
|
-
};
|
|
13678
|
+
function byLabelPriorityBottomUp(bundles) {
|
|
13679
|
+
return [...bundles].sort(
|
|
13680
|
+
(a, b) => getLabelPriority(b.objectClass) - getLabelPriority(a.objectClass)
|
|
13681
|
+
);
|
|
11130
13682
|
}
|
|
11131
13683
|
function chartModeFilterForBand(layer, B, higherCascadeBands, lockedBounds, isRequestedBand) {
|
|
11132
13684
|
let filter = combineWithBandFilter(layer.filter, bandFilterExpression(B));
|
|
@@ -11162,37 +13714,47 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
11162
13714
|
})
|
|
11163
13715
|
});
|
|
11164
13716
|
}
|
|
13717
|
+
const regularBundles = bundles.filter((b) => !TOP_STACK_LIGHT_CLASSES.has(b.objectClass));
|
|
13718
|
+
const topLightBundles = bundles.filter((b) => TOP_STACK_LIGHT_CLASSES.has(b.objectClass));
|
|
11165
13719
|
if (chartCascade && chartRequestedCodes) {
|
|
11166
13720
|
const requestedSet = new Set(chartRequestedCodes);
|
|
11167
|
-
|
|
11168
|
-
|
|
11169
|
-
|
|
11170
|
-
|
|
11171
|
-
|
|
11172
|
-
for (const
|
|
11173
|
-
const
|
|
11174
|
-
|
|
11175
|
-
|
|
11176
|
-
|
|
11177
|
-
|
|
11178
|
-
|
|
11179
|
-
|
|
11180
|
-
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
|
|
11186
|
-
|
|
13721
|
+
const emitChartFeatureVariants = (bundleSet) => {
|
|
13722
|
+
for (let i = 0; i < chartCascade.length; i++) {
|
|
13723
|
+
const B = chartCascade[i];
|
|
13724
|
+
const higher = chartCascade.slice(i + 1);
|
|
13725
|
+
const isRequested = requestedSet.has(B);
|
|
13726
|
+
for (const { featureLayers } of bundleSet) {
|
|
13727
|
+
for (const base of featureLayers) {
|
|
13728
|
+
const filter = chartModeFilterForBand(
|
|
13729
|
+
base,
|
|
13730
|
+
B,
|
|
13731
|
+
higher,
|
|
13732
|
+
lockedBounds,
|
|
13733
|
+
isRequested
|
|
13734
|
+
);
|
|
13735
|
+
const variant = {
|
|
13736
|
+
...base,
|
|
13737
|
+
id: `${base.id}-band${B}`,
|
|
13738
|
+
filter
|
|
13739
|
+
};
|
|
13740
|
+
delete variant.maxzoom;
|
|
13741
|
+
layers.push(variant);
|
|
13742
|
+
}
|
|
11187
13743
|
}
|
|
11188
13744
|
}
|
|
11189
|
-
}
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
|
|
11194
|
-
|
|
11195
|
-
|
|
13745
|
+
};
|
|
13746
|
+
emitChartFeatureVariants(regularBundles);
|
|
13747
|
+
emitChartFeatureVariants(topLightBundles);
|
|
13748
|
+
const chartLabelBundles = byLabelPriorityBottomUp(
|
|
13749
|
+
bundles.filter(
|
|
13750
|
+
(b) => b.label !== null
|
|
13751
|
+
)
|
|
13752
|
+
);
|
|
13753
|
+
for (const { label } of chartLabelBundles) {
|
|
13754
|
+
for (let i = 0; i < chartCascade.length; i++) {
|
|
13755
|
+
const B = chartCascade[i];
|
|
13756
|
+
const higher = chartCascade.slice(i + 1);
|
|
13757
|
+
const isRequested = requestedSet.has(B);
|
|
11196
13758
|
const filter = chartModeFilterForBand(
|
|
11197
13759
|
label,
|
|
11198
13760
|
B,
|
|
@@ -11212,7 +13774,14 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
11212
13774
|
layers.push(...labelLayers);
|
|
11213
13775
|
} else {
|
|
11214
13776
|
for (const code of BAND_DRAW_ORDER) {
|
|
11215
|
-
for (const { featureLayers } of
|
|
13777
|
+
for (const { featureLayers } of regularBundles) {
|
|
13778
|
+
for (const base of featureLayers) {
|
|
13779
|
+
layers.push(...makeBandVariants(base, code));
|
|
13780
|
+
}
|
|
13781
|
+
}
|
|
13782
|
+
}
|
|
13783
|
+
for (const code of BAND_DRAW_ORDER) {
|
|
13784
|
+
for (const { featureLayers } of topLightBundles) {
|
|
11216
13785
|
for (const base of featureLayers) {
|
|
11217
13786
|
layers.push(...makeBandVariants(base, code));
|
|
11218
13787
|
}
|
|
@@ -11224,13 +13793,14 @@ function generateStyle(palette = "DAY", opts = {}) {
|
|
|
11224
13793
|
labelLayers.push(...fanOrientationLabelByCellBand(label));
|
|
11225
13794
|
}
|
|
11226
13795
|
}
|
|
11227
|
-
|
|
11228
|
-
|
|
11229
|
-
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11233
|
-
|
|
13796
|
+
const featureLabelBundles = byLabelPriorityBottomUp(
|
|
13797
|
+
bundles.filter(
|
|
13798
|
+
(b) => b.label !== null && getLabelBehavior(b.objectClass) !== "orientation-cross-band"
|
|
13799
|
+
)
|
|
13800
|
+
);
|
|
13801
|
+
for (const { label } of featureLabelBundles) {
|
|
13802
|
+
for (const code of BAND_DRAW_ORDER) {
|
|
13803
|
+
labelLayers.push(...makeBandVariants(label, code));
|
|
11234
13804
|
}
|
|
11235
13805
|
}
|
|
11236
13806
|
layers.push(...labelLayers);
|
|
@@ -11296,6 +13866,363 @@ function getStyleStats(style) {
|
|
|
11296
13866
|
};
|
|
11297
13867
|
}
|
|
11298
13868
|
|
|
13869
|
+
// src/atlas/custom-style.ts
|
|
13870
|
+
var DEFAULT_NAV_AID_SETTINGS = {
|
|
13871
|
+
buoysVisible: true,
|
|
13872
|
+
beaconsVisible: true,
|
|
13873
|
+
lightsVisible: true,
|
|
13874
|
+
scaminOverride: 0,
|
|
13875
|
+
iconScale: 0.8,
|
|
13876
|
+
iconSizeLocked: false,
|
|
13877
|
+
allowOverlap: false,
|
|
13878
|
+
showSectorArcs: true,
|
|
13879
|
+
fogSignalsVisible: true,
|
|
13880
|
+
radarVisible: true
|
|
13881
|
+
};
|
|
13882
|
+
var DEFAULT_LABEL_SETTINGS = {
|
|
13883
|
+
navAidLabelsVisible: true,
|
|
13884
|
+
landLabelsVisible: true,
|
|
13885
|
+
structureLabelsVisible: true,
|
|
13886
|
+
depthContoursVisible: true,
|
|
13887
|
+
navAidFullNames: false,
|
|
13888
|
+
minzoomOverride: null,
|
|
13889
|
+
textScale: 1,
|
|
13890
|
+
allowOverlap: false
|
|
13891
|
+
};
|
|
13892
|
+
var DEFAULT_SOUNDING_SETTINGS = {
|
|
13893
|
+
visible: true,
|
|
13894
|
+
minzoomOverride: null,
|
|
13895
|
+
textScale: 1,
|
|
13896
|
+
allowOverlap: false,
|
|
13897
|
+
showUnits: false,
|
|
13898
|
+
safetyContour: 10,
|
|
13899
|
+
fontWeight: "semibold",
|
|
13900
|
+
twoShade: true
|
|
13901
|
+
};
|
|
13902
|
+
var DEFAULT_WATER_BODY_SETTINGS = {
|
|
13903
|
+
seaAreasVisible: true,
|
|
13904
|
+
lakesVisible: true,
|
|
13905
|
+
riversVisible: true,
|
|
13906
|
+
canalsVisible: true,
|
|
13907
|
+
minzoomOverride: null,
|
|
13908
|
+
textScale: 1,
|
|
13909
|
+
allowOverlap: false,
|
|
13910
|
+
uppercaseSeaAreas: true
|
|
13911
|
+
};
|
|
13912
|
+
var NAV_AID_LAYER_GROUPS = {
|
|
13913
|
+
buoys: ["BOYLAT", "BOYCAR", "BOYSPP", "BOYISD", "BOYSAW"],
|
|
13914
|
+
beacons: ["BCNLAT", "BCNCAR", "BCNSPP"],
|
|
13915
|
+
lights: ["LIGHTS"],
|
|
13916
|
+
fogSignals: ["FOGSIG"],
|
|
13917
|
+
radar: ["RTPBCN", "RDOSTA"]
|
|
13918
|
+
};
|
|
13919
|
+
var ALL_NAV_AID_LAYERS = new Set(
|
|
13920
|
+
Object.values(NAV_AID_LAYER_GROUPS).flat()
|
|
13921
|
+
);
|
|
13922
|
+
var LABEL_LAYER_GROUPS = {
|
|
13923
|
+
navAids: ["BOYLAT", "BCNLAT", "BOYCAR", "BOYSPP", "BOYISD", "BOYSAW", "BCNCAR", "BCNSPP", "LIGHTS", "FOGSIG", "RTPBCN", "RDOSTA"],
|
|
13924
|
+
land: ["LNDRGN", "LNDMRK", "LNDARE"],
|
|
13925
|
+
structures: ["BRIDGE", "PILPNT", "MORFAC", "SLCONS", "BUISGL", "AIRARE"],
|
|
13926
|
+
depthContours: ["DEPCNT"]
|
|
13927
|
+
};
|
|
13928
|
+
var SOUNDING_LAYERS = ["SOUNDG"];
|
|
13929
|
+
var WATER_BODY_LAYER_GROUPS = {
|
|
13930
|
+
seaAreas: ["SEAARE"],
|
|
13931
|
+
lakes: ["LAKARE"],
|
|
13932
|
+
rivers: ["RIVERS"],
|
|
13933
|
+
canals: ["CANALS"]
|
|
13934
|
+
};
|
|
13935
|
+
var ALL_WATER_BODY_LAYERS = new Set(
|
|
13936
|
+
Object.values(WATER_BODY_LAYER_GROUPS).flat()
|
|
13937
|
+
);
|
|
13938
|
+
function isNavAidLayerVisible(layerName, settings) {
|
|
13939
|
+
if (NAV_AID_LAYER_GROUPS.buoys.includes(layerName)) return settings.buoysVisible;
|
|
13940
|
+
if (NAV_AID_LAYER_GROUPS.beacons.includes(layerName)) return settings.beaconsVisible;
|
|
13941
|
+
if (NAV_AID_LAYER_GROUPS.lights.includes(layerName)) return settings.lightsVisible;
|
|
13942
|
+
if (NAV_AID_LAYER_GROUPS.fogSignals.includes(layerName)) return settings.fogSignalsVisible;
|
|
13943
|
+
if (NAV_AID_LAYER_GROUPS.radar.includes(layerName)) return settings.radarVisible;
|
|
13944
|
+
return true;
|
|
13945
|
+
}
|
|
13946
|
+
function isLabelLayerVisible(layerName, settings) {
|
|
13947
|
+
if (LABEL_LAYER_GROUPS.navAids.includes(layerName)) return settings.navAidLabelsVisible;
|
|
13948
|
+
if (LABEL_LAYER_GROUPS.land.includes(layerName)) return settings.landLabelsVisible;
|
|
13949
|
+
if (LABEL_LAYER_GROUPS.structures.includes(layerName)) return settings.structureLabelsVisible;
|
|
13950
|
+
if (LABEL_LAYER_GROUPS.depthContours.includes(layerName)) return settings.depthContoursVisible;
|
|
13951
|
+
return true;
|
|
13952
|
+
}
|
|
13953
|
+
function isSoundingLayerVisible(settings) {
|
|
13954
|
+
return settings.visible;
|
|
13955
|
+
}
|
|
13956
|
+
function isWaterBodyLayerVisible(layerName, settings) {
|
|
13957
|
+
if (WATER_BODY_LAYER_GROUPS.seaAreas.includes(layerName)) return settings.seaAreasVisible;
|
|
13958
|
+
if (WATER_BODY_LAYER_GROUPS.lakes.includes(layerName)) return settings.lakesVisible;
|
|
13959
|
+
if (WATER_BODY_LAYER_GROUPS.rivers.includes(layerName)) return settings.riversVisible;
|
|
13960
|
+
if (WATER_BODY_LAYER_GROUPS.canals.includes(layerName)) return settings.canalsVisible;
|
|
13961
|
+
return true;
|
|
13962
|
+
}
|
|
13963
|
+
var DEFAULT_CUSTOM_STYLE_CONFIG = {
|
|
13964
|
+
enabled_color_tables: ["DAY", "DUSK", "NIGHT"],
|
|
13965
|
+
default_color_table: "DAY",
|
|
13966
|
+
display_mode: "STANDARD",
|
|
13967
|
+
nav_aid_settings: DEFAULT_NAV_AID_SETTINGS,
|
|
13968
|
+
label_settings: DEFAULT_LABEL_SETTINGS,
|
|
13969
|
+
sounding_settings: DEFAULT_SOUNDING_SETTINGS,
|
|
13970
|
+
water_body_settings: DEFAULT_WATER_BODY_SETTINGS,
|
|
13971
|
+
visible_layers: null
|
|
13972
|
+
};
|
|
13973
|
+
var COLOR_TABLE_NAMES = /* @__PURE__ */ new Set(["DAY", "DUSK", "NIGHT"]);
|
|
13974
|
+
var DISPLAY_MODES = /* @__PURE__ */ new Set(["BASE", "STANDARD", "ALL"]);
|
|
13975
|
+
var LAYER_NAME_RE = /^[A-Za-z0-9_$]{2,16}$/;
|
|
13976
|
+
var MAX_VISIBLE_LAYERS = 512;
|
|
13977
|
+
function clampNumber(raw, min, max, fallback) {
|
|
13978
|
+
const n = typeof raw === "number" ? raw : NaN;
|
|
13979
|
+
if (!Number.isFinite(n)) return fallback;
|
|
13980
|
+
return Math.min(max, Math.max(min, n));
|
|
13981
|
+
}
|
|
13982
|
+
function intOrNull(raw, min, max, fallback) {
|
|
13983
|
+
if (raw === null) return null;
|
|
13984
|
+
const n = typeof raw === "number" ? raw : NaN;
|
|
13985
|
+
if (!Number.isFinite(n)) return fallback;
|
|
13986
|
+
return Math.round(Math.min(max, Math.max(min, n)));
|
|
13987
|
+
}
|
|
13988
|
+
function bool(raw, fallback) {
|
|
13989
|
+
return typeof raw === "boolean" ? raw : fallback;
|
|
13990
|
+
}
|
|
13991
|
+
function asRecord(raw) {
|
|
13992
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
13993
|
+
}
|
|
13994
|
+
function sanitizeNavAid(raw) {
|
|
13995
|
+
const r = asRecord(raw);
|
|
13996
|
+
const d = DEFAULT_NAV_AID_SETTINGS;
|
|
13997
|
+
return {
|
|
13998
|
+
buoysVisible: bool(r.buoysVisible, d.buoysVisible),
|
|
13999
|
+
beaconsVisible: bool(r.beaconsVisible, d.beaconsVisible),
|
|
14000
|
+
lightsVisible: bool(r.lightsVisible, d.lightsVisible),
|
|
14001
|
+
scaminOverride: intOrNull(r.scaminOverride, 0, 14, d.scaminOverride),
|
|
14002
|
+
iconScale: clampNumber(r.iconScale, 0.2, 2, d.iconScale),
|
|
14003
|
+
iconSizeLocked: bool(r.iconSizeLocked, d.iconSizeLocked),
|
|
14004
|
+
allowOverlap: bool(r.allowOverlap, d.allowOverlap),
|
|
14005
|
+
showSectorArcs: bool(r.showSectorArcs, d.showSectorArcs),
|
|
14006
|
+
fogSignalsVisible: bool(r.fogSignalsVisible, d.fogSignalsVisible),
|
|
14007
|
+
radarVisible: bool(r.radarVisible, d.radarVisible)
|
|
14008
|
+
};
|
|
14009
|
+
}
|
|
14010
|
+
function sanitizeLabels(raw) {
|
|
14011
|
+
const r = asRecord(raw);
|
|
14012
|
+
const d = DEFAULT_LABEL_SETTINGS;
|
|
14013
|
+
return {
|
|
14014
|
+
navAidLabelsVisible: bool(r.navAidLabelsVisible, d.navAidLabelsVisible),
|
|
14015
|
+
landLabelsVisible: bool(r.landLabelsVisible, d.landLabelsVisible),
|
|
14016
|
+
structureLabelsVisible: bool(r.structureLabelsVisible, d.structureLabelsVisible),
|
|
14017
|
+
depthContoursVisible: bool(r.depthContoursVisible, d.depthContoursVisible),
|
|
14018
|
+
navAidFullNames: bool(r.navAidFullNames, d.navAidFullNames),
|
|
14019
|
+
minzoomOverride: intOrNull(r.minzoomOverride, 0, 14, d.minzoomOverride),
|
|
14020
|
+
textScale: clampNumber(r.textScale, 0.5, 2, d.textScale),
|
|
14021
|
+
allowOverlap: bool(r.allowOverlap, d.allowOverlap)
|
|
14022
|
+
};
|
|
14023
|
+
}
|
|
14024
|
+
function sanitizeSoundings(raw) {
|
|
14025
|
+
const r = asRecord(raw);
|
|
14026
|
+
const d = DEFAULT_SOUNDING_SETTINGS;
|
|
14027
|
+
return {
|
|
14028
|
+
visible: bool(r.visible, d.visible),
|
|
14029
|
+
minzoomOverride: intOrNull(r.minzoomOverride, 0, 14, d.minzoomOverride),
|
|
14030
|
+
textScale: clampNumber(r.textScale, 0.5, 2, d.textScale),
|
|
14031
|
+
allowOverlap: bool(r.allowOverlap, d.allowOverlap),
|
|
14032
|
+
showUnits: bool(r.showUnits, d.showUnits),
|
|
14033
|
+
safetyContour: clampNumber(r.safetyContour, 0, 100, d.safetyContour),
|
|
14034
|
+
fontWeight: r.fontWeight === "regular" || r.fontWeight === "semibold" ? r.fontWeight : d.fontWeight,
|
|
14035
|
+
twoShade: bool(r.twoShade, d.twoShade)
|
|
14036
|
+
};
|
|
14037
|
+
}
|
|
14038
|
+
function sanitizeWaterBody(raw) {
|
|
14039
|
+
const r = asRecord(raw);
|
|
14040
|
+
const d = DEFAULT_WATER_BODY_SETTINGS;
|
|
14041
|
+
return {
|
|
14042
|
+
seaAreasVisible: bool(r.seaAreasVisible, d.seaAreasVisible),
|
|
14043
|
+
lakesVisible: bool(r.lakesVisible, d.lakesVisible),
|
|
14044
|
+
riversVisible: bool(r.riversVisible, d.riversVisible),
|
|
14045
|
+
canalsVisible: bool(r.canalsVisible, d.canalsVisible),
|
|
14046
|
+
minzoomOverride: intOrNull(r.minzoomOverride, 0, 14, d.minzoomOverride),
|
|
14047
|
+
textScale: clampNumber(r.textScale, 0.5, 2, d.textScale),
|
|
14048
|
+
allowOverlap: bool(r.allowOverlap, d.allowOverlap),
|
|
14049
|
+
uppercaseSeaAreas: bool(r.uppercaseSeaAreas, d.uppercaseSeaAreas)
|
|
14050
|
+
};
|
|
14051
|
+
}
|
|
14052
|
+
function sanitizeCustomStyleConfig(raw) {
|
|
14053
|
+
const r = asRecord(raw);
|
|
14054
|
+
const enabledRaw = Array.isArray(r.enabled_color_tables) ? r.enabled_color_tables : [];
|
|
14055
|
+
const enabled = [...new Set(
|
|
14056
|
+
enabledRaw.filter((c) => typeof c === "string" && COLOR_TABLE_NAMES.has(c))
|
|
14057
|
+
)];
|
|
14058
|
+
const enabledFinal = enabled.length > 0 ? enabled : ["DAY", "DUSK", "NIGHT"];
|
|
14059
|
+
const defaultRaw = typeof r.default_color_table === "string" ? r.default_color_table : "";
|
|
14060
|
+
const defaultFinal = enabledFinal.includes(defaultRaw) ? defaultRaw : enabledFinal[0];
|
|
14061
|
+
const modeRaw = typeof r.display_mode === "string" ? r.display_mode : "";
|
|
14062
|
+
const mode = DISPLAY_MODES.has(modeRaw) ? modeRaw : "STANDARD";
|
|
14063
|
+
let visibleLayers = null;
|
|
14064
|
+
if (Array.isArray(r.visible_layers)) {
|
|
14065
|
+
visibleLayers = r.visible_layers.filter((l) => typeof l === "string" && LAYER_NAME_RE.test(l)).slice(0, MAX_VISIBLE_LAYERS).map((l) => l.toUpperCase());
|
|
14066
|
+
}
|
|
14067
|
+
return {
|
|
14068
|
+
enabled_color_tables: enabledFinal,
|
|
14069
|
+
default_color_table: defaultFinal,
|
|
14070
|
+
display_mode: mode,
|
|
14071
|
+
nav_aid_settings: sanitizeNavAid(r.nav_aid_settings),
|
|
14072
|
+
label_settings: sanitizeLabels(r.label_settings),
|
|
14073
|
+
sounding_settings: sanitizeSoundings(r.sounding_settings),
|
|
14074
|
+
water_body_settings: sanitizeWaterBody(r.water_body_settings),
|
|
14075
|
+
visible_layers: visibleLayers
|
|
14076
|
+
};
|
|
14077
|
+
}
|
|
14078
|
+
function normalizeTheme(raw) {
|
|
14079
|
+
if (!raw) return null;
|
|
14080
|
+
const upper = raw.trim().toUpperCase();
|
|
14081
|
+
return COLOR_TABLE_NAMES.has(upper) ? upper : null;
|
|
14082
|
+
}
|
|
14083
|
+
function resolveCustomTheme(config, requested) {
|
|
14084
|
+
if (!requested) return { palette: config.default_color_table, themeFellBack: false };
|
|
14085
|
+
if (config.enabled_color_tables.includes(requested)) {
|
|
14086
|
+
return { palette: requested, themeFellBack: false };
|
|
14087
|
+
}
|
|
14088
|
+
return { palette: config.default_color_table, themeFellBack: true };
|
|
14089
|
+
}
|
|
14090
|
+
function layerObjectClass(layer) {
|
|
14091
|
+
const meta = layer.metadata;
|
|
14092
|
+
const fromMeta = meta?.["s52:objectClass"];
|
|
14093
|
+
if (typeof fromMeta === "string" && fromMeta.length > 0) return fromMeta;
|
|
14094
|
+
const m = /^s52-([A-Za-z0-9_$]+)-/.exec(layer.id);
|
|
14095
|
+
return m ? m[1] : null;
|
|
14096
|
+
}
|
|
14097
|
+
function isLabelLayer(layer) {
|
|
14098
|
+
if (layer.type !== "symbol") return false;
|
|
14099
|
+
const layout = layer.layout;
|
|
14100
|
+
return layout?.["text-field"] !== void 0;
|
|
14101
|
+
}
|
|
14102
|
+
function isIconLayer(layer) {
|
|
14103
|
+
if (layer.type !== "symbol") return false;
|
|
14104
|
+
const layout = layer.layout;
|
|
14105
|
+
return layout?.["icon-image"] !== void 0 && layout?.["text-field"] === void 0;
|
|
14106
|
+
}
|
|
14107
|
+
function bandFloor(layerId) {
|
|
14108
|
+
const m = /-band(\d)/.exec(layerId);
|
|
14109
|
+
if (!m) return 0;
|
|
14110
|
+
const range = BAND_PRIORITY_RANGES[m[1]];
|
|
14111
|
+
return range ? range.minzoom : 0;
|
|
14112
|
+
}
|
|
14113
|
+
function scaleSizeValue(value, factor) {
|
|
14114
|
+
if (typeof value === "number") {
|
|
14115
|
+
return Math.round(value * factor * 100) / 100;
|
|
14116
|
+
}
|
|
14117
|
+
if (!Array.isArray(value) || value.length === 0) return value;
|
|
14118
|
+
const op = value[0];
|
|
14119
|
+
if (op === "interpolate") {
|
|
14120
|
+
const out = [...value];
|
|
14121
|
+
for (let i = 4; i < out.length; i += 2) out[i] = scaleSizeValue(out[i], factor);
|
|
14122
|
+
return out;
|
|
14123
|
+
}
|
|
14124
|
+
if (op === "step") {
|
|
14125
|
+
const out = [...value];
|
|
14126
|
+
for (let i = 2; i < out.length; i += 2) out[i] = scaleSizeValue(out[i], factor);
|
|
14127
|
+
return out;
|
|
14128
|
+
}
|
|
14129
|
+
if (op === "case") {
|
|
14130
|
+
const out = [...value];
|
|
14131
|
+
for (let i = 2; i < out.length - 1; i += 2) out[i] = scaleSizeValue(out[i], factor);
|
|
14132
|
+
out[out.length - 1] = scaleSizeValue(out[out.length - 1], factor);
|
|
14133
|
+
return out;
|
|
14134
|
+
}
|
|
14135
|
+
if (op === "match") {
|
|
14136
|
+
const out = [...value];
|
|
14137
|
+
for (let i = 3; i < out.length - 1; i += 2) out[i] = scaleSizeValue(out[i], factor);
|
|
14138
|
+
out[out.length - 1] = scaleSizeValue(out[out.length - 1], factor);
|
|
14139
|
+
return out;
|
|
14140
|
+
}
|
|
14141
|
+
return value;
|
|
14142
|
+
}
|
|
14143
|
+
function setLayout(layer, key, val) {
|
|
14144
|
+
const layout = { ...layer.layout ?? {} };
|
|
14145
|
+
layout[key] = val;
|
|
14146
|
+
layer.layout = layout;
|
|
14147
|
+
}
|
|
14148
|
+
function hideLayer(layer) {
|
|
14149
|
+
setLayout(layer, "visibility", "none");
|
|
14150
|
+
}
|
|
14151
|
+
function applyCustomStyleSettings(style, config) {
|
|
14152
|
+
const nav = config.nav_aid_settings;
|
|
14153
|
+
const labels = config.label_settings;
|
|
14154
|
+
const soundings = config.sounding_settings;
|
|
14155
|
+
const water = config.water_body_settings;
|
|
14156
|
+
const visibleSet = config.visible_layers ? new Set(config.visible_layers) : null;
|
|
14157
|
+
for (const layer of style.layers) {
|
|
14158
|
+
const cls = layerObjectClass(layer);
|
|
14159
|
+
if (!cls) continue;
|
|
14160
|
+
const label = isLabelLayer(layer);
|
|
14161
|
+
if (visibleSet && !visibleSet.has(cls)) {
|
|
14162
|
+
hideLayer(layer);
|
|
14163
|
+
continue;
|
|
14164
|
+
}
|
|
14165
|
+
if (label) {
|
|
14166
|
+
const hidden = SOUNDING_LAYERS.includes(cls) && !isSoundingLayerVisible(soundings) || ALL_WATER_BODY_LAYERS.has(cls) && !isWaterBodyLayerVisible(cls, water) || !SOUNDING_LAYERS.includes(cls) && !ALL_WATER_BODY_LAYERS.has(cls) && !isLabelLayerVisible(cls, labels);
|
|
14167
|
+
if (hidden) {
|
|
14168
|
+
hideLayer(layer);
|
|
14169
|
+
continue;
|
|
14170
|
+
}
|
|
14171
|
+
} else {
|
|
14172
|
+
const hidden = ALL_NAV_AID_LAYERS.has(cls) && !isNavAidLayerVisible(cls, nav) || ALL_WATER_BODY_LAYERS.has(cls) && !isWaterBodyLayerVisible(cls, water) || SOUNDING_LAYERS.includes(cls) && !isSoundingLayerVisible(soundings);
|
|
14173
|
+
if (hidden) {
|
|
14174
|
+
hideLayer(layer);
|
|
14175
|
+
continue;
|
|
14176
|
+
}
|
|
14177
|
+
}
|
|
14178
|
+
if (label) {
|
|
14179
|
+
const bundle = SOUNDING_LAYERS.includes(cls) ? { textScale: soundings.textScale, minzoom: soundings.minzoomOverride, allowOverlap: soundings.allowOverlap } : ALL_WATER_BODY_LAYERS.has(cls) ? { textScale: water.textScale, minzoom: water.minzoomOverride, allowOverlap: water.allowOverlap } : { textScale: labels.textScale, minzoom: labels.minzoomOverride, allowOverlap: labels.allowOverlap };
|
|
14180
|
+
if (bundle.textScale !== 1) {
|
|
14181
|
+
const layout = layer.layout;
|
|
14182
|
+
if (layout?.["text-size"] !== void 0) {
|
|
14183
|
+
setLayout(layer, "text-size", scaleSizeValue(layout["text-size"], bundle.textScale));
|
|
14184
|
+
}
|
|
14185
|
+
}
|
|
14186
|
+
if (bundle.allowOverlap === true) {
|
|
14187
|
+
setLayout(layer, "text-allow-overlap", true);
|
|
14188
|
+
setLayout(layer, "text-optional", false);
|
|
14189
|
+
}
|
|
14190
|
+
if (bundle.minzoom !== null) {
|
|
14191
|
+
layer.minzoom = Math.max(bundle.minzoom, bandFloor(layer.id));
|
|
14192
|
+
}
|
|
14193
|
+
if (cls === "SEAARE") {
|
|
14194
|
+
setLayout(layer, "text-transform", water.uppercaseSeaAreas ? "uppercase" : "none");
|
|
14195
|
+
}
|
|
14196
|
+
}
|
|
14197
|
+
if (!label && ALL_NAV_AID_LAYERS.has(cls) && isIconLayer(layer)) {
|
|
14198
|
+
if (nav.iconScale !== 1) {
|
|
14199
|
+
const layout = layer.layout;
|
|
14200
|
+
const current = layout?.["icon-size"];
|
|
14201
|
+
setLayout(layer, "icon-size", current === void 0 ? Math.round(nav.iconScale * 100) / 100 : scaleSizeValue(current, nav.iconScale));
|
|
14202
|
+
}
|
|
14203
|
+
if (nav.allowOverlap === true) {
|
|
14204
|
+
setLayout(layer, "icon-allow-overlap", true);
|
|
14205
|
+
}
|
|
14206
|
+
if (nav.scaminOverride !== null) {
|
|
14207
|
+
layer.minzoom = Math.max(nav.scaminOverride, bandFloor(layer.id));
|
|
14208
|
+
}
|
|
14209
|
+
}
|
|
14210
|
+
}
|
|
14211
|
+
return style;
|
|
14212
|
+
}
|
|
14213
|
+
function generateCustomStyle(config, opts = {}) {
|
|
14214
|
+
const { theme, ...generatorOpts } = opts;
|
|
14215
|
+
const { palette, themeFellBack } = resolveCustomTheme(config, theme ?? null);
|
|
14216
|
+
const style = generateStyle(palette, {
|
|
14217
|
+
...generatorOpts,
|
|
14218
|
+
safetyContour: config.sounding_settings.safetyContour,
|
|
14219
|
+
twoShadeSoundings: config.sounding_settings.twoShade,
|
|
14220
|
+
soundingFontWeight: config.sounding_settings.fontWeight
|
|
14221
|
+
});
|
|
14222
|
+
applyCustomStyleSettings(style, config);
|
|
14223
|
+
return { style, palette, themeFellBack };
|
|
14224
|
+
}
|
|
14225
|
+
|
|
11299
14226
|
// src/pagination.ts
|
|
11300
14227
|
async function* paginate(fetchPage, getItems, getTotalCount, options) {
|
|
11301
14228
|
const pageSize = options?.pageSize ?? 100;
|
|
@@ -11351,6 +14278,12 @@ var Chrt = class {
|
|
|
11351
14278
|
* requires `manifestPath`.
|
|
11352
14279
|
*/
|
|
11353
14280
|
offline;
|
|
14281
|
+
/**
|
|
14282
|
+
* CHRTinteract — click-to-inspect: `attach(map)` for wired popups/events,
|
|
14283
|
+
* `inspect(feature)` for headless decode. Included with the Dev plan and
|
|
14284
|
+
* above; enforced server-side at attach/inspect time.
|
|
14285
|
+
*/
|
|
14286
|
+
interact;
|
|
11354
14287
|
featuresEp;
|
|
11355
14288
|
queryEp;
|
|
11356
14289
|
http;
|
|
@@ -11363,13 +14296,14 @@ var Chrt = class {
|
|
|
11363
14296
|
this.tileBaseUrl = config.tileBaseUrl ?? DEFAULT_TILE_BASE_URL;
|
|
11364
14297
|
this.http = new HttpClient(config.apiKey, config.baseUrl, config.retries);
|
|
11365
14298
|
const tileOpts = { region: this.region, tileBaseUrl: this.tileBaseUrl };
|
|
11366
|
-
this.
|
|
14299
|
+
this.tileAuth = new TileAuth(this.http, tileOpts);
|
|
14300
|
+
this.tiles = new TilesEndpoint(this.http, this.tileAuth, tileOpts);
|
|
11367
14301
|
this.cells = new CellsEndpoint(this.http);
|
|
11368
14302
|
this.coverage = new CoverageEndpoint(this.http);
|
|
11369
14303
|
this.featuresEp = new FeaturesEndpoint(this.http);
|
|
11370
14304
|
this.queryEp = new QueryEndpoint(this.http);
|
|
11371
14305
|
this.offline = new OfflineEndpoint(this.http, config.deviceId, config.manifestPath);
|
|
11372
|
-
this.
|
|
14306
|
+
this.interact = new InteractClient(this.http);
|
|
11373
14307
|
}
|
|
11374
14308
|
/* ── Convenience shortcuts ───────────────────────── */
|
|
11375
14309
|
/** Query GeoJSON features by bounding box */
|
|
@@ -11463,6 +14397,9 @@ var Chrt = class {
|
|
|
11463
14397
|
* });
|
|
11464
14398
|
*
|
|
11465
14399
|
* @remarks S-101 portrayal is on the roadmap; today `style()` emits S-52.
|
|
14400
|
+
* Generated locally, so it never applies a key-bound CHRTstyle custom
|
|
14401
|
+
* style — use {@link styleUrl} / {@link fetchStyle} (the served Style API)
|
|
14402
|
+
* for that.
|
|
11466
14403
|
*/
|
|
11467
14404
|
style(opts = {}) {
|
|
11468
14405
|
const { palette = "DAY", ...styleOpts } = opts;
|
|
@@ -11473,12 +14410,33 @@ var Chrt = class {
|
|
|
11473
14410
|
});
|
|
11474
14411
|
}
|
|
11475
14412
|
/**
|
|
11476
|
-
*
|
|
11477
|
-
*
|
|
11478
|
-
*
|
|
14413
|
+
* URL of the served S-52 style — the CHRTstyle Style API at
|
|
14414
|
+
* `tiles.chrt.co/v1/styles/{region}/style.json`. Unlike {@link style}
|
|
14415
|
+
* (generated locally), the served document honors the API key's bound
|
|
14416
|
+
* CHRTstyle custom style: palettes, layer visibility, label/icon scale,
|
|
14417
|
+
* and the rest of the platform's Map Styles settings, applied server-side
|
|
14418
|
+
* and tier-gated (custom styles are Dev+; Free keys get the default
|
|
14419
|
+
* themes). Async because it pre-mints the tile token, so the style
|
|
14420
|
+
* request is guaranteed to carry auth via `transformRequest()`.
|
|
14421
|
+
*
|
|
14422
|
+
* @example
|
|
14423
|
+
* const map = new maplibregl.Map({
|
|
14424
|
+
* container: 'map',
|
|
14425
|
+
* style: await chrt.styleUrl({ theme: 'day' }),
|
|
14426
|
+
* transformRequest: chrt.transformRequest(),
|
|
14427
|
+
* });
|
|
14428
|
+
*/
|
|
14429
|
+
async styleUrl(opts) {
|
|
14430
|
+
return this.tiles.styleUrl(opts);
|
|
14431
|
+
}
|
|
14432
|
+
/**
|
|
14433
|
+
* Fetch the served S-52 style (see {@link styleUrl}) as a MapLibre style
|
|
14434
|
+
* object — inspect or mutate it before handing it to the map.
|
|
14435
|
+
* `chrt.style()` remains the local generator that needs no network
|
|
14436
|
+
* round-trip (and therefore applies no custom style).
|
|
11479
14437
|
*/
|
|
11480
|
-
|
|
11481
|
-
return this.tiles.
|
|
14438
|
+
async fetchStyle(opts) {
|
|
14439
|
+
return this.tiles.fetchStyle(opts);
|
|
11482
14440
|
}
|
|
11483
14441
|
/**
|
|
11484
14442
|
* Returns a `transformRequest` for Mapbox GL JS / MapLibre GL JS that mints a
|
|
@@ -11506,8 +14464,12 @@ var Chrt = class {
|
|
|
11506
14464
|
}
|
|
11507
14465
|
};
|
|
11508
14466
|
export {
|
|
14467
|
+
ALL_NAV_AID_LAYERS,
|
|
14468
|
+
ALL_WATER_BODY_LAYERS,
|
|
14469
|
+
AREA_QUERY_LAYERS,
|
|
11509
14470
|
BAND_DRAW_ORDER,
|
|
11510
14471
|
BAND_PRIORITY_RANGES,
|
|
14472
|
+
BRIDGE_QUERY_LAYERS,
|
|
11511
14473
|
BUILD_MAXZOOM,
|
|
11512
14474
|
BUILD_MINZOOM,
|
|
11513
14475
|
CHART_BAND_CODES,
|
|
@@ -11515,12 +14477,25 @@ export {
|
|
|
11515
14477
|
CHART_SECTIONS,
|
|
11516
14478
|
Chrt,
|
|
11517
14479
|
ChrtError,
|
|
14480
|
+
DEFAULT_CUSTOM_STYLE_CONFIG,
|
|
14481
|
+
DEFAULT_LABEL_SETTINGS,
|
|
14482
|
+
DEFAULT_NAV_AID_SETTINGS,
|
|
11518
14483
|
DEFAULT_REGION,
|
|
14484
|
+
DEFAULT_SOUNDING_SETTINGS,
|
|
11519
14485
|
DEFAULT_TILE_BASE_URL,
|
|
11520
14486
|
DEFAULT_TILE_REGION,
|
|
14487
|
+
DEFAULT_WATER_BODY_SETTINGS,
|
|
14488
|
+
InteractClient,
|
|
14489
|
+
InteractError,
|
|
11521
14490
|
LABELED_LAYERS,
|
|
11522
14491
|
LABEL_BEHAVIOR,
|
|
14492
|
+
LABEL_LAYER_GROUPS,
|
|
14493
|
+
LABEL_PRIORITY,
|
|
11523
14494
|
NAUTICAL_SYMBOLS,
|
|
14495
|
+
NAV_AID_LAYER_GROUPS,
|
|
14496
|
+
NAV_AID_QUERY_LAYERS,
|
|
14497
|
+
NON_SOLAS_DISCLAIMER,
|
|
14498
|
+
NON_SOLAS_DISCLAIMER_SHORT,
|
|
11524
14499
|
OfflineError,
|
|
11525
14500
|
OfflineStyleError,
|
|
11526
14501
|
S101_CHART_SECTIONS,
|
|
@@ -11533,21 +14508,34 @@ export {
|
|
|
11533
14508
|
S52_DUSK,
|
|
11534
14509
|
S52_LOOKUP,
|
|
11535
14510
|
S52_NIGHT,
|
|
14511
|
+
SOUNDING_LAYERS,
|
|
11536
14512
|
SYMBOL_GROUPS,
|
|
11537
14513
|
SYMBOL_MAP,
|
|
11538
14514
|
TileAuth,
|
|
11539
14515
|
TileTokenError,
|
|
14516
|
+
WATER_BODY_LAYER_GROUPS,
|
|
11540
14517
|
applyColorTheme,
|
|
14518
|
+
applyCustomStyleSettings,
|
|
11541
14519
|
bandFilterExpression,
|
|
14520
|
+
buildFlashPattern,
|
|
11542
14521
|
buildLabelLayer,
|
|
11543
14522
|
buildLayersForClass,
|
|
11544
14523
|
buildOfflineStyle,
|
|
14524
|
+
buildSectorArcGeoJSON,
|
|
14525
|
+
buildSectorArcsFC,
|
|
14526
|
+
buildSectorsFromFeatures,
|
|
11545
14527
|
buildSources,
|
|
11546
14528
|
combineWithBandFilter,
|
|
11547
14529
|
daymarkColor,
|
|
11548
14530
|
decimateFeatures,
|
|
14531
|
+
decodeAreaFeatureProps,
|
|
14532
|
+
decodeBridgeProps,
|
|
14533
|
+
decodeNavAidProps,
|
|
11549
14534
|
depareColor,
|
|
11550
14535
|
depareFillExpression,
|
|
14536
|
+
deriveMarinerName,
|
|
14537
|
+
extractNavAidDesignation,
|
|
14538
|
+
generateCustomStyle,
|
|
11551
14539
|
generateSpriteManifest,
|
|
11552
14540
|
generateSpriteSheet,
|
|
11553
14541
|
generateStyle,
|
|
@@ -11562,6 +14550,7 @@ export {
|
|
|
11562
14550
|
getIconExpression,
|
|
11563
14551
|
getLabelBehavior,
|
|
11564
14552
|
getLabelConfig,
|
|
14553
|
+
getLabelPriority,
|
|
11565
14554
|
getLookupEntry,
|
|
11566
14555
|
getMaxCellsForZoom,
|
|
11567
14556
|
getOrderedClasses,
|
|
@@ -11570,22 +14559,33 @@ export {
|
|
|
11570
14559
|
getStyleStats,
|
|
11571
14560
|
getSymbolSvgString,
|
|
11572
14561
|
getThemedS52Svg,
|
|
14562
|
+
getTopmarkMeaning,
|
|
11573
14563
|
hazardColor,
|
|
11574
14564
|
hazardFillExpression,
|
|
14565
|
+
isLabelLayerVisible,
|
|
11575
14566
|
isNavAidLayer,
|
|
14567
|
+
isNavAidLayerVisible,
|
|
14568
|
+
isSoundingLayerVisible,
|
|
11576
14569
|
isVisibleInMode,
|
|
14570
|
+
isWaterBodyLayerVisible,
|
|
14571
|
+
labelSortKey,
|
|
11577
14572
|
lateralColor,
|
|
11578
14573
|
lateralFillExpression,
|
|
11579
14574
|
lightColor,
|
|
11580
14575
|
lightFillExpression,
|
|
14576
|
+
normalizeTheme,
|
|
11581
14577
|
paginate,
|
|
14578
|
+
parseListCodes,
|
|
11582
14579
|
parseWebhook,
|
|
11583
14580
|
preprocessLights,
|
|
11584
14581
|
preprocessNavAids,
|
|
11585
14582
|
preprocessSoundings,
|
|
11586
14583
|
preprocessWaterBodies,
|
|
11587
14584
|
resolveColor,
|
|
14585
|
+
resolveCustomTheme,
|
|
11588
14586
|
resolveStyle,
|
|
14587
|
+
sanitizeCustomStyleConfig,
|
|
14588
|
+
scaleSizeValue,
|
|
11589
14589
|
scaminToMinZoom,
|
|
11590
14590
|
withinBboxFilter,
|
|
11591
14591
|
worldMinusBboxPolygon
|