@diagrammo/dgmo 0.20.0 → 0.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advanced.cjs +106 -74
- package/dist/advanced.js +104 -72
- package/dist/auto.cjs +104 -72
- package/dist/auto.js +111 -111
- package/dist/auto.mjs +103 -71
- package/dist/cli.cjs +153 -153
- package/dist/index.cjs +103 -71
- package/dist/index.js +102 -70
- package/dist/internal.cjs +106 -74
- package/dist/internal.js +104 -72
- package/docs/language-reference.md +4 -4
- package/package.json +1 -1
- package/src/advanced.ts +1 -4
- package/src/map/layout.ts +134 -72
- package/src/map/load-data.ts +48 -19
- package/src/map/resolver.ts +8 -1
|
@@ -74,7 +74,8 @@ tag GroupName as <alias>
|
|
|
74
74
|
- Alias: optional postfix `as <alias>` per §2A (universal alias syntax — `[A-Za-z][A-Za-z0-9_]{0,11}`)
|
|
75
75
|
- Inline values also supported: `tag Priority as p Low green, High red`
|
|
76
76
|
- Color follows the value as a bare trailing token (see §1.5). Capitalize the color word (`Red`, `Yellow`) to keep it as a literal value with no color.
|
|
77
|
-
- First entry is the default — reorder to change
|
|
77
|
+
- First entry is the default value — reorder to change
|
|
78
|
+
- The first declared group is active by default (colors nodes immediately); `active-tag <GroupName>` only matters with ≥2 groups to pick a non-first group, and `active-tag none` suppresses all coloring
|
|
78
79
|
- Must appear before diagram content
|
|
79
80
|
- Legacy bare shorthand (`tag Priority p`) and `alias` keyword (`tag Priority alias p`) emit `E_TAG_SHORTHAND_REMOVED` per TD-18
|
|
80
81
|
|
|
@@ -1362,7 +1363,7 @@ recruit crew 1 2 4 c: Quartermaster
|
|
|
1362
1363
|
load powder 0.5 1 2 c: Bosun
|
|
1363
1364
|
```
|
|
1364
1365
|
|
|
1365
|
-
|
|
1366
|
+
The first declared tag group is active by default and colors nodes; `active-tag <GroupName>` only matters with two or more groups when you want a non-first group active, and `active-tag none` suppresses coloring entirely. When a group is active, the activity card's middle (name) band picks up the tag color while the border continues to communicate criticality. Milestone diamonds adopt the tag color across the full pill.
|
|
1366
1367
|
|
|
1367
1368
|
### Date anchoring
|
|
1368
1369
|
|
|
@@ -2672,7 +2673,7 @@ Florida score: 51
|
|
|
2672
2673
|
|
|
2673
2674
|
### Region fill — categorical (tags)
|
|
2674
2675
|
|
|
2675
|
-
Uses the universal tag model (§1.3): declare a `tag` group
|
|
2676
|
+
Uses the universal tag model (§1.3): declare a `tag` group and apply its alias as a key. The first declared group colors regions by default; `active-tag` only selects a different dimension (another group, or the `score` ramp).
|
|
2676
2677
|
|
|
2677
2678
|
```
|
|
2678
2679
|
map Global Presence
|
|
@@ -2682,7 +2683,6 @@ tag Market as m
|
|
|
2682
2683
|
HQ blue
|
|
2683
2684
|
Region teal
|
|
2684
2685
|
Prospect orange
|
|
2685
|
-
active-tag Market
|
|
2686
2686
|
|
|
2687
2687
|
United States m: HQ
|
|
2688
2688
|
Germany m: Region
|
package/package.json
CHANGED
package/src/advanced.ts
CHANGED
|
@@ -631,10 +631,7 @@ export {
|
|
|
631
631
|
} from './echarts';
|
|
632
632
|
export type { ScatterLabelPoint } from './echarts';
|
|
633
633
|
export { ScaleContext } from './utils/scaling';
|
|
634
|
-
export {
|
|
635
|
-
renderLegendSvg,
|
|
636
|
-
renderLegendSvgFromConfig,
|
|
637
|
-
} from './utils/legend-svg';
|
|
634
|
+
export { renderLegendSvg, renderLegendSvgFromConfig } from './utils/legend-svg';
|
|
638
635
|
export type { LegendGroupData } from './utils/legend-types';
|
|
639
636
|
export { LEGEND_HEIGHT, LEGEND_GEAR_PILL_W } from './utils/legend-constants';
|
|
640
637
|
export { renderLegendD3 } from './utils/legend-d3';
|
package/src/map/layout.ts
CHANGED
|
@@ -313,12 +313,35 @@ export function layoutMap(
|
|
|
313
313
|
// Lakes match the states' resolution. Falls back to the world tiers otherwise.
|
|
314
314
|
const usCrisp =
|
|
315
315
|
resolved.projection === 'albers-usa' && wantsUsStates && !!data.naLand;
|
|
316
|
+
// Base world layer. In a US view use the DETAIL tier (full global coverage) so
|
|
317
|
+
// distant context — South America, northern Canada, etc. — is present and can
|
|
318
|
+
// draw when it falls inside the frame. (`naLand` alone is bbox-clipped to lon
|
|
319
|
+
// -140..-52 / lat 10..66, so it has no S. America and a truncated Canada; using
|
|
320
|
+
// it as the base would leave ocean where that land belongs.)
|
|
316
321
|
const worldTopo = usCrisp
|
|
317
|
-
? data.
|
|
322
|
+
? data.worldDetail
|
|
318
323
|
: resolved.basemaps.world === 'detail'
|
|
319
324
|
? data.worldDetail
|
|
320
325
|
: data.worldCoarse;
|
|
321
326
|
const worldLayer = decodeLayer(worldTopo);
|
|
327
|
+
// Crisp upgrade: `naLand` is 10m country land (vs the base's 50m) but clipped to
|
|
328
|
+
// a North-America bbox. Swap a country's geometry to the crisp version ONLY when
|
|
329
|
+
// its full (base) bounds lie inside that clip box — so contained neighbours
|
|
330
|
+
// (Mexico, Central America, the Caribbean) sharpen to match the 10m states,
|
|
331
|
+
// while countries the clip would truncate (Canada, Greenland) keep their full
|
|
332
|
+
// base shape. Coast off-frame still bleeds; nothing is lost.
|
|
333
|
+
if (usCrisp && data.naLand) {
|
|
334
|
+
// NA clip bbox from the data build (scripts/build-map-data.mjs NA_BBOX).
|
|
335
|
+
const [nbW, nbS, nbE, nbN] = [-140, 10, -52, 66];
|
|
336
|
+
const crisp = decodeLayer(data.naLand);
|
|
337
|
+
for (const [iso, cf] of crisp) {
|
|
338
|
+
const base = worldLayer.get(iso);
|
|
339
|
+
if (!base) continue; // crisp-only id with no base → skip (avoid orphans)
|
|
340
|
+
const [[bw, bs], [be, bn]] = geoBounds(base as never);
|
|
341
|
+
if (bw >= nbW && be <= nbE && bs >= nbS && bn <= nbN)
|
|
342
|
+
worldLayer.set(iso, cf);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
322
345
|
const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
|
|
323
346
|
|
|
324
347
|
// Land is a muted green; the ocean/backdrop is blue. Scored/tagged regions
|
|
@@ -571,6 +594,19 @@ export function layoutMap(
|
|
|
571
594
|
return p ? stretch(p[0], p[1]) : null;
|
|
572
595
|
};
|
|
573
596
|
} else {
|
|
597
|
+
// Clip the projected geometry to the canvas. fitExtent frames the focus
|
|
598
|
+
// region, but the rest of the world mesh still projects to coordinates far
|
|
599
|
+
// off-canvas — invisible under our viewBox, but they bloat the SVG and,
|
|
600
|
+
// critically, blow up any downstream getBBox()/bbox recompute (remark-dgmo
|
|
601
|
+
// embeddings tighten the viewBox to real content bounds, which would
|
|
602
|
+
// otherwise shrink the map to a dot). clipExtent trims the path `d` data to
|
|
603
|
+
// the viewport so drawn content == frame. Point projection (POIs/edges,
|
|
604
|
+
// albers-usa coast sampling) ignores clipExtent so positions are unaffected,
|
|
605
|
+
// and the AK/HI insets use their own dedicated projection — both safe.
|
|
606
|
+
projection.clipExtent([
|
|
607
|
+
[0, 0],
|
|
608
|
+
[width, height],
|
|
609
|
+
]);
|
|
574
610
|
path = geoPath(projection);
|
|
575
611
|
project = (lon, lat) => projection([lon, lat]) ?? null;
|
|
576
612
|
}
|
|
@@ -777,67 +813,72 @@ export function layoutMap(
|
|
|
777
813
|
// unclipped conic doesn't paint frame-filling garbage; the us-states layer
|
|
778
814
|
// itself is never culled (every conus state is in frame by construction).
|
|
779
815
|
const conusFit = resolved.projection === 'albers-usa' && !!usLayer;
|
|
780
|
-
|
|
816
|
+
// Extent used only to classify a near-global view (draw everything) vs a
|
|
817
|
+
// regional one (cull to the canvas). For an albers fit that's the CONUS bounds;
|
|
818
|
+
// else the resolved data extent.
|
|
819
|
+
const classifyExtent = conusFit
|
|
781
820
|
? (geoBounds(fitTarget as never) as [[number, number], [number, number]])
|
|
782
821
|
: resolved.extent;
|
|
783
|
-
const [[
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
//
|
|
787
|
-
//
|
|
788
|
-
|
|
789
|
-
//
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
const
|
|
793
|
-
const vW = exW - padLon;
|
|
794
|
-
const vE = exE + padLon;
|
|
795
|
-
const vS = exS - padLat;
|
|
796
|
-
const vN = exN + padLat;
|
|
797
|
-
// Pacific-crossing extents use extended longitudes (e.g. 247 = 113°W), but
|
|
798
|
-
// ring vertices are in [-180,180]. Shift each vertex into the extent's frame
|
|
799
|
-
// so the overlap test compares like-for-like.
|
|
800
|
-
const vLonCenter = (exW + exE) / 2;
|
|
822
|
+
const dLonSpan = classifyExtent[1][0] - classifyExtent[0][0];
|
|
823
|
+
const dLatSpan = classifyExtent[1][1] - classifyExtent[0][1];
|
|
824
|
+
// A near-global view draws everything; a regional one culls each ring to what
|
|
825
|
+
// actually projects onto the canvas (see ringOverlapsView — projection-based,
|
|
826
|
+
// so it's correct for the US conic and for route maps alike).
|
|
827
|
+
const isGlobalView = dLonSpan >= 270 || dLatSpan >= 130;
|
|
828
|
+
// Pacific-crossing extents use extended longitudes (e.g. 247 = 113°W), but ring
|
|
829
|
+
// vertices are in [-180,180]. Normalize each ring lon into the view's frame so
|
|
830
|
+
// the circumpolar / antimeridian-sliver guards compare like-for-like.
|
|
831
|
+
const vLonCenter = (classifyExtent[0][0] + classifyExtent[1][0]) / 2;
|
|
801
832
|
const normLon = (lon: number): number => {
|
|
802
833
|
let L = lon;
|
|
803
834
|
while (L < vLonCenter - 180) L += 360;
|
|
804
835
|
while (L > vLonCenter + 180) L -= 360;
|
|
805
836
|
return L;
|
|
806
837
|
};
|
|
807
|
-
// True if an outer ring
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
//
|
|
838
|
+
// True if an outer ring should be drawn in a regional view. Visibility is
|
|
839
|
+
// decided by the PROJECTION, not a lat/lon box: the ring is kept iff its
|
|
840
|
+
// projected screen bbox intersects the canvas (the projection's clipExtent then
|
|
841
|
+
// trims it to the viewport). A lat/lon box can't model what a conic actually
|
|
842
|
+
// shows — under the US Albers conic, Panama/Colombia land on-canvas at a tall
|
|
843
|
+
// aspect yet sit outside any tidy CONUS-ish box. Two geometry guards still drop
|
|
844
|
+
// the antimeridian/circumpolar rings that would otherwise project to a
|
|
845
|
+
// frame-filling garbage fill (their projected bbox spuriously covers the
|
|
846
|
+
// canvas): a near-circumpolar ring (>270° span) and an antimeridian sliver
|
|
847
|
+
// (raw span >180° but a small normalized arc, e.g. Fiji).
|
|
811
848
|
type Ring = ReadonlyArray<readonly [number, number]>;
|
|
812
849
|
const ringOverlapsView = (ring: Ring): boolean => {
|
|
813
|
-
let anyIn = false;
|
|
814
850
|
let loMin = Infinity,
|
|
815
851
|
loMax = -Infinity,
|
|
816
|
-
laMin = Infinity,
|
|
817
|
-
laMax = -Infinity,
|
|
818
852
|
rawMin = Infinity,
|
|
819
853
|
rawMax = -Infinity;
|
|
820
|
-
for (const [rawLon
|
|
854
|
+
for (const [rawLon] of ring) {
|
|
821
855
|
const lon = normLon(rawLon);
|
|
822
|
-
if (lon >= vW && lon <= vE && lat >= vS && lat <= vN) anyIn = true;
|
|
823
856
|
if (lon < loMin) loMin = lon;
|
|
824
857
|
if (lon > loMax) loMax = lon;
|
|
825
858
|
if (rawLon < rawMin) rawMin = rawLon;
|
|
826
859
|
if (rawLon > rawMax) rawMax = rawLon;
|
|
827
|
-
if (lat < laMin) laMin = lat;
|
|
828
|
-
if (lat > laMax) laMax = lat;
|
|
829
860
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
861
|
+
if (loMax - loMin > 270) return false; // circumpolar/polar-wrap garbage
|
|
862
|
+
if (rawMax - rawMin > 180 && loMax - loMin < 90) return false; // seam sliver
|
|
863
|
+
// Projected-bbox ∩ canvas. project() honours the active projection (and
|
|
864
|
+
// ignores clipExtent, so positions are true), so this is exactly "does any
|
|
865
|
+
// of this ring fall on the canvas".
|
|
866
|
+
let px0 = Infinity,
|
|
867
|
+
py0 = Infinity,
|
|
868
|
+
px1 = -Infinity,
|
|
869
|
+
py1 = -Infinity,
|
|
870
|
+
anyFinite = false;
|
|
871
|
+
for (const [lon, lat] of ring) {
|
|
872
|
+
const p = project(lon, lat);
|
|
873
|
+
if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
|
|
874
|
+
anyFinite = true;
|
|
875
|
+
if (p[0] < px0) px0 = p[0];
|
|
876
|
+
if (p[0] > px1) px1 = p[0];
|
|
877
|
+
if (p[1] < py0) py0 = p[1];
|
|
878
|
+
if (p[1] > py1) py1 = p[1];
|
|
879
|
+
}
|
|
880
|
+
if (!anyFinite) return false;
|
|
881
|
+
return !(px1 < 0 || px0 > width || py1 < 0 || py0 > height);
|
|
841
882
|
};
|
|
842
883
|
// Drop a feature's sub-polygons that don't touch the view (e.g. Alaska's
|
|
843
884
|
// Aleutians on a US feature framed over the Caribbean). Returns null if the
|
|
@@ -1364,24 +1405,50 @@ export function layoutMap(
|
|
|
1364
1405
|
const text = labelText(p);
|
|
1365
1406
|
return { text, w: measureLegendText(text, FONT) };
|
|
1366
1407
|
};
|
|
1408
|
+
// Candidate inline placements around a marker, in escalation order: the two
|
|
1409
|
+
// horizontal sides first (most legible), then above/below for a hub whose
|
|
1410
|
+
// edges all leave sideways and block both flanks (e.g. a POI fed by routes
|
|
1411
|
+
// from the east AND west — Boulder in the route-cluster gauntlet).
|
|
1412
|
+
type Side = 'right' | 'left' | 'above' | 'below';
|
|
1413
|
+
const GAP = 3;
|
|
1414
|
+
const inlineRect = (p: MapLayoutPoi, w: number, side: Side): LabelRect => {
|
|
1415
|
+
switch (side) {
|
|
1416
|
+
case 'right':
|
|
1417
|
+
return { x: p.cx + p.r + GAP, y: p.cy - poiLabH / 2, w, h: poiLabH };
|
|
1418
|
+
case 'left':
|
|
1419
|
+
return {
|
|
1420
|
+
x: p.cx - p.r - GAP - w,
|
|
1421
|
+
y: p.cy - poiLabH / 2,
|
|
1422
|
+
w,
|
|
1423
|
+
h: poiLabH,
|
|
1424
|
+
};
|
|
1425
|
+
case 'above':
|
|
1426
|
+
return {
|
|
1427
|
+
x: p.cx - w / 2,
|
|
1428
|
+
y: p.cy - p.r - GAP - poiLabH,
|
|
1429
|
+
w,
|
|
1430
|
+
h: poiLabH,
|
|
1431
|
+
};
|
|
1432
|
+
case 'below':
|
|
1433
|
+
return { x: p.cx - w / 2, y: p.cy + p.r + GAP, w, h: poiLabH };
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1367
1436
|
const pushInline = (
|
|
1368
1437
|
p: MapLayoutPoi,
|
|
1369
1438
|
text: string,
|
|
1370
1439
|
w: number,
|
|
1371
|
-
side:
|
|
1440
|
+
side: Side
|
|
1372
1441
|
): void => {
|
|
1373
|
-
const
|
|
1374
|
-
obstacles.push(
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
h: poiLabH,
|
|
1379
|
-
});
|
|
1442
|
+
const rect = inlineRect(p, w, side);
|
|
1443
|
+
obstacles.push(rect);
|
|
1444
|
+
const anchor =
|
|
1445
|
+
side === 'right' ? 'start' : side === 'left' ? 'end' : 'middle';
|
|
1446
|
+
const x = side === 'right' ? rect.x : side === 'left' ? rect.x + w : p.cx;
|
|
1380
1447
|
labels.push({
|
|
1381
|
-
x
|
|
1382
|
-
y:
|
|
1448
|
+
x,
|
|
1449
|
+
y: rect.y + poiLabH / 2 + FONT / 3,
|
|
1383
1450
|
text,
|
|
1384
|
-
anchor
|
|
1451
|
+
anchor,
|
|
1385
1452
|
color: palette.text,
|
|
1386
1453
|
halo: true,
|
|
1387
1454
|
haloColor: palette.bg,
|
|
@@ -1389,19 +1456,15 @@ export function layoutMap(
|
|
|
1389
1456
|
lineNumber: p.lineNumber,
|
|
1390
1457
|
});
|
|
1391
1458
|
};
|
|
1392
|
-
const inlineFits = (
|
|
1393
|
-
p
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
w,
|
|
1402
|
-
h: poiLabH,
|
|
1403
|
-
};
|
|
1404
|
-
return rect.x >= 0 && rect.x + rect.w <= width && !collides(rect);
|
|
1459
|
+
const inlineFits = (p: MapLayoutPoi, w: number, side: Side): boolean => {
|
|
1460
|
+
const rect = inlineRect(p, w, side);
|
|
1461
|
+
return (
|
|
1462
|
+
rect.x >= 0 &&
|
|
1463
|
+
rect.x + rect.w <= width &&
|
|
1464
|
+
rect.y >= 0 &&
|
|
1465
|
+
rect.y + rect.h <= height &&
|
|
1466
|
+
!collides(rect)
|
|
1467
|
+
);
|
|
1405
1468
|
};
|
|
1406
1469
|
|
|
1407
1470
|
// Pre-group POIs by proximity. A tight cluster (offshore platforms, a metro
|
|
@@ -1477,12 +1540,11 @@ export function layoutMap(
|
|
|
1477
1540
|
if (g.length === 1) {
|
|
1478
1541
|
const p = g[0]!;
|
|
1479
1542
|
const { text, w } = labelInfo(p);
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
pushInline(p, text, w, 'left');
|
|
1543
|
+
const side = (['right', 'left', 'above', 'below'] as const).find((s) =>
|
|
1544
|
+
inlineFits(p, w, s)
|
|
1545
|
+
);
|
|
1546
|
+
if (side) {
|
|
1547
|
+
pushInline(p, text, w, side);
|
|
1486
1548
|
continue;
|
|
1487
1549
|
}
|
|
1488
1550
|
}
|
package/src/map/load-data.ts
CHANGED
|
@@ -10,12 +10,33 @@
|
|
|
10
10
|
// build must inject `MapData` (or supply a fetch/bundle loader) — `resolveMap`
|
|
11
11
|
// takes `MapData` by DI precisely so the browser path can differ. Do NOT assume
|
|
12
12
|
// a green Node smoke test proves the browser load.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
//
|
|
14
|
+
// Node builtins (`fs/promises`, `url`, `path`) are imported LAZILY inside
|
|
15
|
+
// `loadMapData` — never at module top level — so this file can be pulled into a
|
|
16
|
+
// browser bundle (Obsidian's esbuild `platform: browser`, the app's Vite/Rollup
|
|
17
|
+
// web build) without the bundler trying to resolve `node:*`. Mirrors the
|
|
18
|
+
// `await import('jsdom')` seam in render.ts. The web build injects `MapData` via
|
|
19
|
+
// DI and never calls `loadMapData`, so the dynamic import only runs in Node.
|
|
16
20
|
import type { MapData } from './resolved-types';
|
|
17
21
|
import type { BoundaryTopology, Gazetteer } from './data/types';
|
|
18
22
|
|
|
23
|
+
type NodeBuiltins = {
|
|
24
|
+
readFile: typeof import('node:fs/promises').readFile;
|
|
25
|
+
fileURLToPath: typeof import('node:url').fileURLToPath;
|
|
26
|
+
dirname: typeof import('node:path').dirname;
|
|
27
|
+
resolve: typeof import('node:path').resolve;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
async function loadNodeBuiltins(): Promise<NodeBuiltins> {
|
|
31
|
+
const [{ readFile }, { fileURLToPath }, { dirname, resolve }] =
|
|
32
|
+
await Promise.all([
|
|
33
|
+
import('node:fs/promises'),
|
|
34
|
+
import('node:url'),
|
|
35
|
+
import('node:path'),
|
|
36
|
+
]);
|
|
37
|
+
return { readFile, fileURLToPath, dirname, resolve };
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
const FILES = {
|
|
20
41
|
worldCoarse: 'world-coarse.json',
|
|
21
42
|
worldDetail: 'world-detail.json',
|
|
@@ -37,15 +58,22 @@ const CANDIDATE_DIRS = [
|
|
|
37
58
|
|
|
38
59
|
let cache: Promise<MapData> | undefined;
|
|
39
60
|
|
|
40
|
-
async function readJson<T>(
|
|
41
|
-
|
|
61
|
+
async function readJson<T>(
|
|
62
|
+
nb: NodeBuiltins,
|
|
63
|
+
dir: string,
|
|
64
|
+
name: string
|
|
65
|
+
): Promise<T> {
|
|
66
|
+
return JSON.parse(await nb.readFile(nb.resolve(dir, name), 'utf8')) as T;
|
|
42
67
|
}
|
|
43
68
|
|
|
44
|
-
async function firstExistingDir(
|
|
69
|
+
async function firstExistingDir(
|
|
70
|
+
nb: NodeBuiltins,
|
|
71
|
+
baseDir: string
|
|
72
|
+
): Promise<string> {
|
|
45
73
|
for (const rel of CANDIDATE_DIRS) {
|
|
46
|
-
const dir = resolve(baseDir, rel);
|
|
74
|
+
const dir = nb.resolve(baseDir, rel);
|
|
47
75
|
try {
|
|
48
|
-
await readFile(resolve(dir, FILES.gazetteer), 'utf8');
|
|
76
|
+
await nb.readFile(nb.resolve(dir, FILES.gazetteer), 'utf8');
|
|
49
77
|
return dir;
|
|
50
78
|
} catch {
|
|
51
79
|
/* try next candidate */
|
|
@@ -78,10 +106,10 @@ function validate(data: MapData): MapData {
|
|
|
78
106
|
* (dist/cli.cjs, where `import.meta.url` is `undefined` → `fileURLToPath`
|
|
79
107
|
* throws). The `typeof __dirname` guard is safe in ESM (evaluates to
|
|
80
108
|
* 'undefined' without a ReferenceError). */
|
|
81
|
-
function moduleBaseDir(): string {
|
|
109
|
+
function moduleBaseDir(nb: NodeBuiltins): string {
|
|
82
110
|
try {
|
|
83
111
|
const url = import.meta.url;
|
|
84
|
-
if (url) return dirname(fileURLToPath(url));
|
|
112
|
+
if (url) return nb.dirname(nb.fileURLToPath(url));
|
|
85
113
|
} catch {
|
|
86
114
|
/* CJS: import.meta unavailable — fall through */
|
|
87
115
|
}
|
|
@@ -95,7 +123,8 @@ function moduleBaseDir(): string {
|
|
|
95
123
|
* call can retry rather than inheriting a poisoned promise. */
|
|
96
124
|
export function loadMapData(): Promise<MapData> {
|
|
97
125
|
cache ??= (async (): Promise<MapData> => {
|
|
98
|
-
const
|
|
126
|
+
const nb = await loadNodeBuiltins();
|
|
127
|
+
const dir = await firstExistingDir(nb, moduleBaseDir(nb));
|
|
99
128
|
const [
|
|
100
129
|
worldCoarse,
|
|
101
130
|
worldDetail,
|
|
@@ -106,15 +135,15 @@ export function loadMapData(): Promise<MapData> {
|
|
|
106
135
|
naLakes,
|
|
107
136
|
gazetteer,
|
|
108
137
|
] = await Promise.all([
|
|
109
|
-
readJson<BoundaryTopology>(dir, FILES.worldCoarse),
|
|
110
|
-
readJson<BoundaryTopology>(dir, FILES.worldDetail),
|
|
111
|
-
readJson<BoundaryTopology>(dir, FILES.usStates),
|
|
138
|
+
readJson<BoundaryTopology>(nb, dir, FILES.worldCoarse),
|
|
139
|
+
readJson<BoundaryTopology>(nb, dir, FILES.worldDetail),
|
|
140
|
+
readJson<BoundaryTopology>(nb, dir, FILES.usStates),
|
|
112
141
|
// Lakes/rivers/NA assets are optional — older bundles may predate them.
|
|
113
|
-
readJson<BoundaryTopology>(dir, FILES.lakes).catch(() => undefined),
|
|
114
|
-
readJson<BoundaryTopology>(dir, FILES.rivers).catch(() => undefined),
|
|
115
|
-
readJson<BoundaryTopology>(dir, FILES.naLand).catch(() => undefined),
|
|
116
|
-
readJson<BoundaryTopology>(dir, FILES.naLakes).catch(() => undefined),
|
|
117
|
-
readJson<Gazetteer>(dir, FILES.gazetteer),
|
|
142
|
+
readJson<BoundaryTopology>(nb, dir, FILES.lakes).catch(() => undefined),
|
|
143
|
+
readJson<BoundaryTopology>(nb, dir, FILES.rivers).catch(() => undefined),
|
|
144
|
+
readJson<BoundaryTopology>(nb, dir, FILES.naLand).catch(() => undefined),
|
|
145
|
+
readJson<BoundaryTopology>(nb, dir, FILES.naLakes).catch(() => undefined),
|
|
146
|
+
readJson<Gazetteer>(nb, dir, FILES.gazetteer),
|
|
118
147
|
]);
|
|
119
148
|
return validate({
|
|
120
149
|
worldCoarse,
|
package/src/map/resolver.ts
CHANGED
|
@@ -519,8 +519,15 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
519
519
|
// albers-usa only covers US territory: choose it only when the map is truly
|
|
520
520
|
// US-only — no non-US country region AND no POI outside the US (#13). Without
|
|
521
521
|
// the POI guard a `default-country US` + Tokyo map projected to garbage.
|
|
522
|
+
// albers-usa is the US-only composite projection — it insets AK/HI and clips
|
|
523
|
+
// out all non-US land. Use it only when the map actually renders US STATES (an
|
|
524
|
+
// explicit `region us-states` or US-state region fills), NOT merely because the
|
|
525
|
+
// POIs happen to be US: a pure POI/route map across the US should stay on a
|
|
526
|
+
// geographic projection so neighbour land (Mexico, Central America, the
|
|
527
|
+
// Caribbean, Canada) still draws.
|
|
522
528
|
const usDominant =
|
|
523
|
-
(
|
|
529
|
+
(subdivisions.includes('us-states') ||
|
|
530
|
+
regions.some((r) => r.layer === 'us-state')) &&
|
|
524
531
|
!regions.some((r) => r.layer === 'country' && r.iso !== 'US') &&
|
|
525
532
|
!anyNonUsPoi;
|
|
526
533
|
|