@diagrammo/dgmo 0.20.1 → 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 +73 -47
- package/dist/advanced.js +73 -47
- package/dist/auto.cjs +74 -48
- package/dist/auto.js +102 -102
- package/dist/auto.mjs +74 -48
- package/dist/cli.cjs +134 -134
- package/dist/index.cjs +73 -47
- package/dist/index.js +73 -47
- package/dist/internal.cjs +73 -47
- package/dist/internal.js +73 -47
- package/docs/language-reference.md +4 -4
- package/package.json +1 -1
- package/src/map/layout.ts +134 -72
- package/src/map/resolver.ts +8 -1
package/dist/internal.js
CHANGED
|
@@ -46213,7 +46213,7 @@ function resolveMap(parsed, data) {
|
|
|
46213
46213
|
const lonSpan = extent2[1][0] - extent2[0][0];
|
|
46214
46214
|
const latSpan = extent2[1][1] - extent2[0][1];
|
|
46215
46215
|
const span = Math.max(lonSpan, latSpan);
|
|
46216
|
-
const usDominant = (
|
|
46216
|
+
const usDominant = (subdivisions.includes("us-states") || regions.some((r) => r.layer === "us-state")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
|
|
46217
46217
|
let projection;
|
|
46218
46218
|
const override = parsed.directives.projection;
|
|
46219
46219
|
if (override === "equirectangular" || override === "natural-earth" || override === "albers-usa" || override === "mercator") {
|
|
@@ -46473,8 +46473,19 @@ function layoutMap(resolved, data, size, opts) {
|
|
|
46473
46473
|
const { width, height } = size;
|
|
46474
46474
|
const wantsUsStates = resolved.basemaps.subdivisions.includes("us-states");
|
|
46475
46475
|
const usCrisp = resolved.projection === "albers-usa" && wantsUsStates && !!data.naLand;
|
|
46476
|
-
const worldTopo = usCrisp ? data.
|
|
46476
|
+
const worldTopo = usCrisp ? data.worldDetail : resolved.basemaps.world === "detail" ? data.worldDetail : data.worldCoarse;
|
|
46477
46477
|
const worldLayer = decodeLayer(worldTopo);
|
|
46478
|
+
if (usCrisp && data.naLand) {
|
|
46479
|
+
const [nbW, nbS, nbE, nbN] = [-140, 10, -52, 66];
|
|
46480
|
+
const crisp = decodeLayer(data.naLand);
|
|
46481
|
+
for (const [iso, cf] of crisp) {
|
|
46482
|
+
const base = worldLayer.get(iso);
|
|
46483
|
+
if (!base) continue;
|
|
46484
|
+
const [[bw, bs], [be, bn]] = geoBounds2(base);
|
|
46485
|
+
if (bw >= nbW && be <= nbE && bs >= nbS && bn <= nbN)
|
|
46486
|
+
worldLayer.set(iso, cf);
|
|
46487
|
+
}
|
|
46488
|
+
}
|
|
46478
46489
|
const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
|
|
46479
46490
|
const landTint = isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT;
|
|
46480
46491
|
const neutralFill = mix(palette.colors.green, palette.bg, landTint);
|
|
@@ -46620,6 +46631,10 @@ function layoutMap(resolved, data, size, opts) {
|
|
|
46620
46631
|
return p ? stretch(p[0], p[1]) : null;
|
|
46621
46632
|
};
|
|
46622
46633
|
} else {
|
|
46634
|
+
projection.clipExtent([
|
|
46635
|
+
[0, 0],
|
|
46636
|
+
[width, height]
|
|
46637
|
+
]);
|
|
46623
46638
|
path = geoPath(projection);
|
|
46624
46639
|
project = (lon, lat) => projection([lon, lat]) ?? null;
|
|
46625
46640
|
}
|
|
@@ -46764,18 +46779,11 @@ function layoutMap(resolved, data, size, opts) {
|
|
|
46764
46779
|
placeInset("US-HI", hawaiiProjection(), akRight + 24, width * 0.1);
|
|
46765
46780
|
}
|
|
46766
46781
|
const conusFit = resolved.projection === "albers-usa" && !!usLayer;
|
|
46767
|
-
const
|
|
46768
|
-
const [[
|
|
46769
|
-
const
|
|
46770
|
-
const
|
|
46771
|
-
const
|
|
46772
|
-
const padLon = Math.max(8, lonSpan * 0.35);
|
|
46773
|
-
const padLat = Math.max(8, latSpan * 0.35);
|
|
46774
|
-
const vW = exW - padLon;
|
|
46775
|
-
const vE = exE + padLon;
|
|
46776
|
-
const vS = exS - padLat;
|
|
46777
|
-
const vN = exN + padLat;
|
|
46778
|
-
const vLonCenter = (exW + exE) / 2;
|
|
46782
|
+
const classifyExtent = conusFit ? geoBounds2(fitTarget) : resolved.extent;
|
|
46783
|
+
const dLonSpan = classifyExtent[1][0] - classifyExtent[0][0];
|
|
46784
|
+
const dLatSpan = classifyExtent[1][1] - classifyExtent[0][1];
|
|
46785
|
+
const isGlobalView = dLonSpan >= 270 || dLatSpan >= 130;
|
|
46786
|
+
const vLonCenter = (classifyExtent[0][0] + classifyExtent[1][0]) / 2;
|
|
46779
46787
|
const normLon = (lon) => {
|
|
46780
46788
|
let L = lon;
|
|
46781
46789
|
while (L < vLonCenter - 180) L += 360;
|
|
@@ -46783,23 +46791,28 @@ function layoutMap(resolved, data, size, opts) {
|
|
|
46783
46791
|
return L;
|
|
46784
46792
|
};
|
|
46785
46793
|
const ringOverlapsView = (ring) => {
|
|
46786
|
-
let
|
|
46787
|
-
|
|
46788
|
-
for (const [rawLon, lat] of ring) {
|
|
46794
|
+
let loMin = Infinity, loMax = -Infinity, rawMin = Infinity, rawMax = -Infinity;
|
|
46795
|
+
for (const [rawLon] of ring) {
|
|
46789
46796
|
const lon = normLon(rawLon);
|
|
46790
|
-
if (lon >= vW && lon <= vE && lat >= vS && lat <= vN) anyIn = true;
|
|
46791
46797
|
if (lon < loMin) loMin = lon;
|
|
46792
46798
|
if (lon > loMax) loMax = lon;
|
|
46793
46799
|
if (rawLon < rawMin) rawMin = rawLon;
|
|
46794
46800
|
if (rawLon > rawMax) rawMax = rawLon;
|
|
46795
|
-
if (lat < laMin) laMin = lat;
|
|
46796
|
-
if (lat > laMax) laMax = lat;
|
|
46797
46801
|
}
|
|
46798
46802
|
if (loMax - loMin > 270) return false;
|
|
46799
46803
|
if (rawMax - rawMin > 180 && loMax - loMin < 90) return false;
|
|
46800
|
-
|
|
46801
|
-
|
|
46802
|
-
|
|
46804
|
+
let px0 = Infinity, py0 = Infinity, px1 = -Infinity, py1 = -Infinity, anyFinite = false;
|
|
46805
|
+
for (const [lon, lat] of ring) {
|
|
46806
|
+
const p = project(lon, lat);
|
|
46807
|
+
if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
|
|
46808
|
+
anyFinite = true;
|
|
46809
|
+
if (p[0] < px0) px0 = p[0];
|
|
46810
|
+
if (p[0] > px1) px1 = p[0];
|
|
46811
|
+
if (p[1] < py0) py0 = p[1];
|
|
46812
|
+
if (p[1] > py1) py1 = p[1];
|
|
46813
|
+
}
|
|
46814
|
+
if (!anyFinite) return false;
|
|
46815
|
+
return !(px1 < 0 || px0 > width || py1 < 0 || py0 > height);
|
|
46803
46816
|
};
|
|
46804
46817
|
const cullFeatureToView = (f) => {
|
|
46805
46818
|
if (isGlobalView) return f;
|
|
@@ -47190,19 +47203,39 @@ function layoutMap(resolved, data, size, opts) {
|
|
|
47190
47203
|
const text = labelText(p);
|
|
47191
47204
|
return { text, w: measureLegendText(text, FONT) };
|
|
47192
47205
|
};
|
|
47206
|
+
const GAP = 3;
|
|
47207
|
+
const inlineRect = (p, w, side) => {
|
|
47208
|
+
switch (side) {
|
|
47209
|
+
case "right":
|
|
47210
|
+
return { x: p.cx + p.r + GAP, y: p.cy - poiLabH / 2, w, h: poiLabH };
|
|
47211
|
+
case "left":
|
|
47212
|
+
return {
|
|
47213
|
+
x: p.cx - p.r - GAP - w,
|
|
47214
|
+
y: p.cy - poiLabH / 2,
|
|
47215
|
+
w,
|
|
47216
|
+
h: poiLabH
|
|
47217
|
+
};
|
|
47218
|
+
case "above":
|
|
47219
|
+
return {
|
|
47220
|
+
x: p.cx - w / 2,
|
|
47221
|
+
y: p.cy - p.r - GAP - poiLabH,
|
|
47222
|
+
w,
|
|
47223
|
+
h: poiLabH
|
|
47224
|
+
};
|
|
47225
|
+
case "below":
|
|
47226
|
+
return { x: p.cx - w / 2, y: p.cy + p.r + GAP, w, h: poiLabH };
|
|
47227
|
+
}
|
|
47228
|
+
};
|
|
47193
47229
|
const pushInline = (p, text, w, side) => {
|
|
47194
|
-
const
|
|
47195
|
-
obstacles.push(
|
|
47196
|
-
|
|
47197
|
-
|
|
47198
|
-
w,
|
|
47199
|
-
h: poiLabH
|
|
47200
|
-
});
|
|
47230
|
+
const rect = inlineRect(p, w, side);
|
|
47231
|
+
obstacles.push(rect);
|
|
47232
|
+
const anchor = side === "right" ? "start" : side === "left" ? "end" : "middle";
|
|
47233
|
+
const x = side === "right" ? rect.x : side === "left" ? rect.x + w : p.cx;
|
|
47201
47234
|
labels.push({
|
|
47202
|
-
x
|
|
47203
|
-
y:
|
|
47235
|
+
x,
|
|
47236
|
+
y: rect.y + poiLabH / 2 + FONT / 3,
|
|
47204
47237
|
text,
|
|
47205
|
-
anchor
|
|
47238
|
+
anchor,
|
|
47206
47239
|
color: palette.text,
|
|
47207
47240
|
halo: true,
|
|
47208
47241
|
haloColor: palette.bg,
|
|
@@ -47211,14 +47244,8 @@ function layoutMap(resolved, data, size, opts) {
|
|
|
47211
47244
|
});
|
|
47212
47245
|
};
|
|
47213
47246
|
const inlineFits = (p, w, side) => {
|
|
47214
|
-
const
|
|
47215
|
-
|
|
47216
|
-
x: side === "right" ? tx : tx - w,
|
|
47217
|
-
y: p.cy - poiLabH / 2,
|
|
47218
|
-
w,
|
|
47219
|
-
h: poiLabH
|
|
47220
|
-
};
|
|
47221
|
-
return rect.x >= 0 && rect.x + rect.w <= width && !collides(rect);
|
|
47247
|
+
const rect = inlineRect(p, w, side);
|
|
47248
|
+
return rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect);
|
|
47222
47249
|
};
|
|
47223
47250
|
const GROUP_R = 30;
|
|
47224
47251
|
const groups = [];
|
|
@@ -47275,12 +47302,11 @@ function layoutMap(resolved, data, size, opts) {
|
|
|
47275
47302
|
if (g.length === 1) {
|
|
47276
47303
|
const p = g[0];
|
|
47277
47304
|
const { text, w } = labelInfo(p);
|
|
47278
|
-
|
|
47279
|
-
|
|
47280
|
-
|
|
47281
|
-
|
|
47282
|
-
|
|
47283
|
-
pushInline(p, text, w, "left");
|
|
47305
|
+
const side = ["right", "left", "above", "below"].find(
|
|
47306
|
+
(s) => inlineFits(p, w, s)
|
|
47307
|
+
);
|
|
47308
|
+
if (side) {
|
|
47309
|
+
pushInline(p, text, w, side);
|
|
47284
47310
|
continue;
|
|
47285
47311
|
}
|
|
47286
47312
|
}
|
|
@@ -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/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/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
|
|