@diagrammo/dgmo 0.26.0 → 0.28.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/README.md +3 -3
- package/dist/advanced.cjs +5651 -3193
- package/dist/advanced.d.cts +272 -58
- package/dist/advanced.d.ts +272 -58
- package/dist/advanced.js +5650 -3186
- package/dist/auto.cjs +5511 -3070
- package/dist/auto.js +116 -137
- package/dist/auto.mjs +5510 -3069
- package/dist/cli.cjs +168 -189
- package/dist/editor.cjs +4 -0
- package/dist/editor.js +4 -0
- package/dist/highlight.cjs +4 -0
- package/dist/highlight.js +4 -0
- package/dist/index.cjs +5536 -3072
- package/dist/index.d.cts +33 -8
- package/dist/index.d.ts +33 -8
- package/dist/index.js +5535 -3071
- package/dist/internal.cjs +5651 -3193
- package/dist/internal.d.cts +272 -58
- package/dist/internal.d.ts +272 -58
- package/dist/internal.js +5650 -3186
- package/dist/map-data/PROVENANCE.json +1 -1
- package/dist/map-data/airport-collisions.json +1 -0
- package/dist/map-data/airports.json +1 -0
- package/docs/language-reference.md +68 -18
- package/gallery/fixtures/boxes-and-lines-diverging.dgmo +15 -0
- package/gallery/fixtures/map-choropleth-diverging.dgmo +9 -0
- package/gallery/fixtures/map-region-values.dgmo +13 -0
- package/gallery/fixtures/map-subnational-zoom.dgmo +12 -0
- package/gallery/fixtures/map-tagged-legs.dgmo +16 -0
- package/gallery/fixtures/map-undirected-edges.dgmo +12 -0
- package/package.json +7 -3
- package/src/advanced.ts +1 -6
- package/src/auto/index.ts +1 -1
- package/src/boxes-and-lines/layout-layered.ts +722 -0
- package/src/boxes-and-lines/layout-search.ts +1200 -0
- package/src/boxes-and-lines/layout.ts +202 -571
- package/src/boxes-and-lines/parser.ts +43 -8
- package/src/boxes-and-lines/renderer.ts +223 -96
- package/src/boxes-and-lines/types.ts +9 -2
- package/src/c4/layout.ts +14 -32
- package/src/c4/parser.ts +9 -5
- package/src/c4/renderer.ts +34 -39
- package/src/class/layout.ts +118 -18
- package/src/class/parser.ts +35 -0
- package/src/class/renderer.ts +58 -2
- package/src/class/types.ts +3 -0
- package/src/cli.ts +4 -4
- package/src/completion.ts +26 -12
- package/src/cycle/layout.ts +55 -72
- package/src/cycle/renderer.ts +11 -6
- package/src/d3.ts +78 -117
- package/src/diagnostics.ts +16 -0
- package/src/echarts.ts +46 -33
- package/src/editor/keywords.ts +4 -0
- package/src/er/layout.ts +114 -22
- package/src/er/parser.ts +28 -0
- package/src/er/renderer.ts +55 -2
- package/src/er/types.ts +3 -0
- package/src/gantt/renderer.ts +46 -38
- package/src/gantt/resolver.ts +9 -2
- package/src/graph/edge-spline.ts +29 -0
- package/src/graph/flowchart-parser.ts +34 -1
- package/src/graph/flowchart-renderer.ts +78 -64
- package/src/graph/layout.ts +206 -23
- package/src/graph/notes.ts +21 -0
- package/src/graph/state-parser.ts +26 -1
- package/src/graph/state-renderer.ts +78 -64
- package/src/graph/types.ts +13 -0
- package/src/index.ts +1 -1
- package/src/infra/layout.ts +46 -26
- package/src/infra/renderer.ts +16 -7
- package/src/journey-map/layout.ts +38 -49
- package/src/journey-map/renderer.ts +22 -45
- package/src/kanban/renderer.ts +15 -6
- package/src/label-layout.ts +3 -3
- package/src/map/completion.ts +77 -22
- package/src/map/context-labels.ts +101 -25
- package/src/map/data/PROVENANCE.json +1 -1
- package/src/map/data/airport-collisions.json +1 -0
- package/src/map/data/airports.json +1 -0
- package/src/map/data/types.ts +19 -0
- package/src/map/layout.ts +1212 -96
- package/src/map/legend-band.ts +2 -2
- package/src/map/load-data.ts +10 -1
- package/src/map/parser.ts +61 -32
- package/src/map/renderer.ts +284 -12
- package/src/map/resolved-types.ts +15 -1
- package/src/map/resolver.ts +132 -12
- package/src/map/types.ts +28 -8
- package/src/migrate/embedded.ts +9 -7
- package/src/mindmap/text-wrap.ts +13 -14
- package/src/org/layout.ts +19 -17
- package/src/org/renderer.ts +11 -4
- package/src/palettes/color-utils.ts +82 -21
- package/src/palettes/index.ts +0 -19
- package/src/palettes/registry.ts +1 -1
- package/src/palettes/types.ts +2 -2
- package/src/pert/layout.ts +48 -40
- package/src/pert/renderer.ts +30 -43
- package/src/pyramid/renderer.ts +4 -5
- package/src/raci/renderer.ts +34 -68
- package/src/render.ts +1 -1
- package/src/ring/renderer.ts +1 -2
- package/src/sequence/parser.ts +100 -22
- package/src/sequence/renderer.ts +75 -50
- package/src/sitemap/layout.ts +27 -19
- package/src/sitemap/renderer.ts +12 -5
- package/src/tech-radar/renderer.ts +11 -35
- package/src/utils/arrow-markers.ts +51 -0
- package/src/utils/fit-canvas.ts +64 -0
- package/src/utils/legend-constants.ts +8 -54
- package/src/utils/legend-d3.ts +10 -7
- package/src/utils/legend-layout.ts +7 -4
- package/src/utils/legend-types.ts +10 -4
- package/src/utils/note-box/constants.ts +25 -0
- package/src/utils/note-box/index.ts +11 -0
- package/src/utils/note-box/metrics.ts +90 -0
- package/src/utils/note-box/svg.ts +331 -0
- package/src/utils/notes/bounds.ts +30 -0
- package/src/utils/notes/build.ts +131 -0
- package/src/utils/notes/index.ts +18 -0
- package/src/utils/notes/model.ts +19 -0
- package/src/utils/notes/parse.ts +131 -0
- package/src/utils/notes/place.ts +177 -0
- package/src/utils/notes/resolve.ts +88 -0
- package/src/utils/number-format.ts +36 -0
- package/src/utils/parsing.ts +41 -0
- package/src/utils/reserved-key-registry.ts +4 -0
- package/src/utils/text-measure.ts +122 -0
- package/src/wireframe/layout.ts +4 -2
- package/src/wireframe/renderer.ts +8 -6
- package/src/palettes/dracula.ts +0 -68
- package/src/palettes/gruvbox.ts +0 -85
- package/src/palettes/monokai.ts +0 -68
- package/src/palettes/one-dark.ts +0 -70
- package/src/palettes/rose-pine.ts +0 -84
- package/src/palettes/solarized.ts +0 -77
|
@@ -7,6 +7,7 @@ import { parseJourneyMap } from './parser';
|
|
|
7
7
|
import {
|
|
8
8
|
layoutJourneyMap,
|
|
9
9
|
scoreToColor,
|
|
10
|
+
scoreToCurveY,
|
|
10
11
|
TAG_STRIP_HEIGHT,
|
|
11
12
|
type CurvePoint,
|
|
12
13
|
type StepLayout,
|
|
@@ -21,6 +22,11 @@ import { renderLegendD3 } from '../utils/legend-d3';
|
|
|
21
22
|
import type { LegendConfig, LegendState } from '../utils/legend-types';
|
|
22
23
|
import { resolveActiveTagGroup } from '../utils/tag-groups';
|
|
23
24
|
import { ScaleContext } from '../utils/scaling';
|
|
25
|
+
import {
|
|
26
|
+
measureText,
|
|
27
|
+
wrapTextToWidth,
|
|
28
|
+
truncateText as truncateToWidth,
|
|
29
|
+
} from '../utils/text-measure';
|
|
24
30
|
|
|
25
31
|
// ============================================================
|
|
26
32
|
// Interactive Options
|
|
@@ -66,7 +72,6 @@ const CURVE_STROKE_WIDTH = 2.5;
|
|
|
66
72
|
const FACE_RADIUS = 14;
|
|
67
73
|
const DIM_HOVER = 0.25;
|
|
68
74
|
const TITLE_LINE_HEIGHT = 16;
|
|
69
|
-
const TITLE_CHAR_WIDTH = 6.5;
|
|
70
75
|
|
|
71
76
|
// ============================================================
|
|
72
77
|
// Renderer
|
|
@@ -415,10 +420,7 @@ export function renderJourneyMap(
|
|
|
415
420
|
|
|
416
421
|
// Grid lines at score levels 1-5
|
|
417
422
|
for (let score = 1; score <= 5; score++) {
|
|
418
|
-
const y =
|
|
419
|
-
layout.curveAreaBottom -
|
|
420
|
-
((score - 1) / 4) * (layout.curveAreaBottom - layout.curveAreaTop - 120) -
|
|
421
|
-
10;
|
|
423
|
+
const y = scoreToCurveY(score, layout.curveAreaBottom);
|
|
422
424
|
|
|
423
425
|
curveG
|
|
424
426
|
.append('line')
|
|
@@ -621,7 +623,11 @@ export function renderJourneyMap(
|
|
|
621
623
|
.attr('fill', onHeaderText)
|
|
622
624
|
.text(
|
|
623
625
|
isCollapsed
|
|
624
|
-
? truncateText(
|
|
626
|
+
? truncateText(
|
|
627
|
+
pl.phase.name,
|
|
628
|
+
pl.width - COLUMN_PADDING * 2,
|
|
629
|
+
FONT_SIZE_PHASE
|
|
630
|
+
)
|
|
625
631
|
: pl.phase.name
|
|
626
632
|
);
|
|
627
633
|
|
|
@@ -728,7 +734,7 @@ export function renderJourneyMap(
|
|
|
728
734
|
.attr('y', itemY + COLLAPSED_CARD_H / 2 + FONT_SIZE_META / 2 - 1)
|
|
729
735
|
.attr('font-size', FONT_SIZE_META)
|
|
730
736
|
.attr('fill', palette.text)
|
|
731
|
-
.text(truncateText(step.title, maxTextW));
|
|
737
|
+
.text(truncateText(step.title, maxTextW, FONT_SIZE_META));
|
|
732
738
|
|
|
733
739
|
if (onNavigateToLine) {
|
|
734
740
|
itemG.style('cursor', 'pointer').on('click', (event: Event) => {
|
|
@@ -893,7 +899,7 @@ export function renderJourneyMap(
|
|
|
893
899
|
const lines = wrapText(thoughtText, THOUGHT_MAX_W, THOUGHT_FONT);
|
|
894
900
|
const textW = Math.min(
|
|
895
901
|
THOUGHT_MAX_W,
|
|
896
|
-
Math.max(...lines.map((l) => l
|
|
902
|
+
Math.max(...lines.map((l) => measureText(l, THOUGHT_FONT)))
|
|
897
903
|
);
|
|
898
904
|
const bw = textW + THOUGHT_PAD_X * 2;
|
|
899
905
|
const bh = lines.length * THOUGHT_LINE_H + THOUGHT_PAD_Y * 2;
|
|
@@ -1170,20 +1176,7 @@ function renderStepCard(
|
|
|
1170
1176
|
|
|
1171
1177
|
// Title (wrapped)
|
|
1172
1178
|
const titleMaxW = sl.width - CARD_PADDING_X * 2;
|
|
1173
|
-
const
|
|
1174
|
-
const titleWords = sl.step.title.split(/\s+/);
|
|
1175
|
-
const titleLines: string[] = [];
|
|
1176
|
-
let titleCur = '';
|
|
1177
|
-
for (const w of titleWords) {
|
|
1178
|
-
const candidate = titleCur ? `${titleCur} ${w}` : w;
|
|
1179
|
-
if (candidate.length > titleMaxChars && titleCur) {
|
|
1180
|
-
titleLines.push(titleCur);
|
|
1181
|
-
titleCur = w;
|
|
1182
|
-
} else {
|
|
1183
|
-
titleCur = candidate;
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
if (titleCur) titleLines.push(titleCur);
|
|
1179
|
+
const titleLines = wrapTextToWidth(sl.step.title, FONT_SIZE_STEP, titleMaxW);
|
|
1187
1180
|
|
|
1188
1181
|
for (let i = 0; i < titleLines.length; i++) {
|
|
1189
1182
|
stepG
|
|
@@ -1459,31 +1452,15 @@ function renderScoreFace(
|
|
|
1459
1452
|
}
|
|
1460
1453
|
|
|
1461
1454
|
function wrapText(text: string, maxWidth: number, fontSize: number): string[] {
|
|
1462
|
-
|
|
1463
|
-
const maxChars = Math.floor(maxWidth / charWidth);
|
|
1464
|
-
if (maxChars <= 0) return [text];
|
|
1465
|
-
|
|
1466
|
-
const words = text.split(/\s+/);
|
|
1467
|
-
const lines: string[] = [];
|
|
1468
|
-
let current = '';
|
|
1469
|
-
|
|
1470
|
-
for (const word of words) {
|
|
1471
|
-
const candidate = current ? `${current} ${word}` : word;
|
|
1472
|
-
if (candidate.length > maxChars && current) {
|
|
1473
|
-
lines.push(current);
|
|
1474
|
-
current = word;
|
|
1475
|
-
} else {
|
|
1476
|
-
current = candidate;
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
if (current) lines.push(current);
|
|
1480
|
-
return lines;
|
|
1455
|
+
return wrapTextToWidth(text, fontSize, maxWidth);
|
|
1481
1456
|
}
|
|
1482
1457
|
|
|
1483
|
-
function truncateText(
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1458
|
+
function truncateText(
|
|
1459
|
+
text: string,
|
|
1460
|
+
maxWidth: number,
|
|
1461
|
+
fontSize: number
|
|
1462
|
+
): string {
|
|
1463
|
+
return truncateToWidth(text, fontSize, maxWidth);
|
|
1487
1464
|
}
|
|
1488
1465
|
|
|
1489
1466
|
function annotationColor(
|
package/src/kanban/renderer.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
D3Sel,
|
|
26
26
|
} from '../utils/legend-types';
|
|
27
27
|
import { ScaleContext } from '../utils/scaling';
|
|
28
|
+
import { measureText } from '../utils/text-measure';
|
|
28
29
|
|
|
29
30
|
// ============================================================
|
|
30
31
|
// Public options object
|
|
@@ -163,7 +164,6 @@ function computeLayout(
|
|
|
163
164
|
const headerHeight = showTitle ? sTitleHeight + 8 : 0;
|
|
164
165
|
const startY = sDiagramPadding + headerHeight;
|
|
165
166
|
|
|
166
|
-
const charWidth = sCardTitleFontSize * 0.6;
|
|
167
167
|
const columnLayouts: ColumnLayout[] = [];
|
|
168
168
|
|
|
169
169
|
let maxColumnHeight = 0;
|
|
@@ -185,13 +185,13 @@ function computeLayout(
|
|
|
185
185
|
continue;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
let maxCardTextWidth = col.name
|
|
188
|
+
let maxCardTextWidth = measureText(col.name, sColumnHeaderFontSize);
|
|
189
189
|
|
|
190
190
|
const cardLayouts: CardLayout[] = [];
|
|
191
191
|
let cardY = sColumnHeaderHeight + sColumnPadding;
|
|
192
192
|
|
|
193
193
|
for (const card of col.cards) {
|
|
194
|
-
const titleWidth = card.title
|
|
194
|
+
const titleWidth = measureText(card.title, sCardTitleFontSize);
|
|
195
195
|
maxCardTextWidth = Math.max(
|
|
196
196
|
maxCardTextWidth,
|
|
197
197
|
titleWidth + sCardPaddingX * 2
|
|
@@ -214,7 +214,7 @@ function computeLayout(
|
|
|
214
214
|
|
|
215
215
|
for (const m of tagMeta) {
|
|
216
216
|
const metaW =
|
|
217
|
-
(m.label
|
|
217
|
+
measureText(`${m.label}: ${m.value}`, sCardMetaFontSize) +
|
|
218
218
|
sCardPaddingX * 2;
|
|
219
219
|
maxCardTextWidth = Math.max(maxCardTextWidth, metaW);
|
|
220
220
|
}
|
|
@@ -697,7 +697,7 @@ export function renderKanban(
|
|
|
697
697
|
.attr('fill', onCardText)
|
|
698
698
|
.text(`${meta.label}: `);
|
|
699
699
|
|
|
700
|
-
const labelWidth = (meta.label
|
|
700
|
+
const labelWidth = measureText(`${meta.label}: `, sCardMetaFontSize);
|
|
701
701
|
cg.append('text')
|
|
702
702
|
.attr('x', cx + sCardPaddingX + labelWidth)
|
|
703
703
|
.attr('y', metaY)
|
|
@@ -768,6 +768,15 @@ function drawSwimlaneIcon(
|
|
|
768
768
|
.attr('class', 'kanban-swimlane-icon')
|
|
769
769
|
.attr('transform', `translate(${x}, ${y})`);
|
|
770
770
|
|
|
771
|
+
// Transparent hit area so the whole icon (not just the 2px bars) is clickable
|
|
772
|
+
iconG
|
|
773
|
+
.append('rect')
|
|
774
|
+
.attr('x', -5)
|
|
775
|
+
.attr('y', -5)
|
|
776
|
+
.attr('width', 22)
|
|
777
|
+
.attr('height', 18)
|
|
778
|
+
.attr('fill', 'transparent');
|
|
779
|
+
|
|
771
780
|
const color = isActive ? palette.primary : palette.textMuted;
|
|
772
781
|
const opacity = isActive ? 1 : 0.35;
|
|
773
782
|
const barWidths = [8, 12, 6];
|
|
@@ -1413,7 +1422,7 @@ function renderSwimlaneCard(
|
|
|
1413
1422
|
.attr('font-size', sCardMetaFontSize)
|
|
1414
1423
|
.attr('fill', palette.textMuted)
|
|
1415
1424
|
.text(`${meta.label}: `);
|
|
1416
|
-
const labelWidth = (meta.label
|
|
1425
|
+
const labelWidth = measureText(`${meta.label}: `, sCardMetaFontSize);
|
|
1417
1426
|
cg.append('text')
|
|
1418
1427
|
.attr('x', cx + sCardPaddingX + labelWidth)
|
|
1419
1428
|
.attr('y', metaY)
|
package/src/label-layout.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Shared label collision detection and placement utilities
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
|
|
5
|
+
import { measureText } from './utils/text-measure';
|
|
6
|
+
|
|
5
7
|
export interface LabelRect {
|
|
6
8
|
x: number;
|
|
7
9
|
y: number;
|
|
@@ -74,8 +76,6 @@ export function segmentRectOverlap(
|
|
|
74
76
|
// Quadrant chart point label placement
|
|
75
77
|
// ---------------------------------------------------------------------------
|
|
76
78
|
|
|
77
|
-
const CHAR_WIDTH_RATIO = 0.6;
|
|
78
|
-
|
|
79
79
|
export interface QuadrantLabelPoint {
|
|
80
80
|
label: string;
|
|
81
81
|
cx: number; // pixel x
|
|
@@ -120,7 +120,7 @@ export function computeQuadrantPointLabels(
|
|
|
120
120
|
|
|
121
121
|
for (let i = 0; i < points.length; i++) {
|
|
122
122
|
const pt = points[i]!; // In-bounds by loop guard.
|
|
123
|
-
const labelWidth = pt.label
|
|
123
|
+
const labelWidth = measureText(pt.label, fontSize) + 8;
|
|
124
124
|
|
|
125
125
|
// Try 4 directions: above, below, left, right
|
|
126
126
|
// Each direction generates candidate (labelX, labelY, anchor)
|
package/src/map/completion.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// ./geo) so this module — exported from the main index — stays free of the
|
|
9
9
|
// d3-geo / topojson imports that geo.ts pulls in. Keep it byte-identical to the
|
|
10
10
|
// resolver/geo folding so matches agree.
|
|
11
|
-
import type { Gazetteer, RegionName } from './data/types';
|
|
11
|
+
import type { Gazetteer, RegionName, AirportData } from './data/types';
|
|
12
12
|
|
|
13
13
|
const fold = (s: string): string =>
|
|
14
14
|
s
|
|
@@ -34,11 +34,24 @@ export interface MapPlaceCompletion {
|
|
|
34
34
|
readonly iso: string;
|
|
35
35
|
readonly sub?: string;
|
|
36
36
|
readonly pop: number;
|
|
37
|
+
/** `'airport'` for IATA-code entries (icon/grouping affordance); absent or
|
|
38
|
+
* `'city'` for gazetteer cities. Cities rank above airports for a shared
|
|
39
|
+
* prefix so ~1500 codes never bury city names (ADR-5). */
|
|
40
|
+
readonly kind?: 'city' | 'airport';
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export interface MapCompletionOptions {
|
|
40
44
|
/** Max results (default 12). */
|
|
41
45
|
readonly limit?: number;
|
|
46
|
+
/** IATA-coded airports (`airports.json`). When supplied, airport codes
|
|
47
|
+
* matching the prefix are offered as a second (post-city) group. Optional —
|
|
48
|
+
* absent (old DI bundles / no asset) just yields city-only completions. */
|
|
49
|
+
readonly airports?: AirportData;
|
|
50
|
+
/** Resolver-inferred map scope (country `US` or subdivision `US-CA`). Biases
|
|
51
|
+
* airport ranking so in-region airports sort above out-of-region same-prefix
|
|
52
|
+
* ones (ADR-6). Pure rank, never a filter — cross-region airports still
|
|
53
|
+
* appear. App passes the document's inferred scope in (Slice 2). */
|
|
54
|
+
readonly scopeISO?: string;
|
|
42
55
|
}
|
|
43
56
|
|
|
44
57
|
/**
|
|
@@ -65,35 +78,77 @@ export function completeMapPlaces(
|
|
|
65
78
|
for (const [key, idx] of Object.entries(gazetteer.alt)) {
|
|
66
79
|
if (key.startsWith(q)) matched.add(idx);
|
|
67
80
|
}
|
|
68
|
-
if (matched.size === 0) return [];
|
|
69
81
|
|
|
70
|
-
|
|
82
|
+
// Airport prefix matches (optional asset; guarded for absent bundles, F5/#8).
|
|
83
|
+
// Folded IATA code → index into `airports`. Scope-match (current map's country)
|
|
84
|
+
// then code-alpha — NOT pop (always 0) and NOT type (the tuple has no type).
|
|
85
|
+
const airports = opts?.airports;
|
|
86
|
+
const scopeCountry = opts?.scopeISO ? opts.scopeISO.slice(0, 2) : undefined;
|
|
87
|
+
const airportHits: Array<{ code: string; name: string; iso: string }> = [];
|
|
88
|
+
if (airports?.airportIata) {
|
|
89
|
+
for (const [key, idx] of Object.entries(airports.airportIata)) {
|
|
90
|
+
if (!key.startsWith(q)) continue;
|
|
91
|
+
const a = airports.airports[idx];
|
|
92
|
+
if (a) airportHits.push({ code: key, name: a[4], iso: a[2] });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Relaxed early-return: only when BOTH groups are empty (an airport-only prefix
|
|
97
|
+
// must still complete — the old city-only `matched.size === 0` blocked it).
|
|
98
|
+
if (matched.size === 0 && airportHits.length === 0) return [];
|
|
99
|
+
|
|
100
|
+
// Cities first (pop desc), then airports — cities fill the limit slots first.
|
|
101
|
+
// Slice the sorted index list to `limit` BEFORE building items (a short prefix
|
|
102
|
+
// matches thousands of cities; mapping them all per keystroke is wasted work —
|
|
103
|
+
// airports are appended after, so they never starve cities).
|
|
104
|
+
const cityItems: MapPlaceCompletion[] = [...matched]
|
|
71
105
|
.filter((i) => gazetteer.cities[i] !== undefined)
|
|
72
106
|
.sort((a, b) => {
|
|
73
107
|
const pa = gazetteer.cities[a]![3];
|
|
74
108
|
const pb = gazetteer.cities[b]![3];
|
|
75
109
|
return pb - pa || a - b; // pop desc, then deterministic index
|
|
76
110
|
})
|
|
77
|
-
.slice(0, limit)
|
|
111
|
+
.slice(0, limit)
|
|
112
|
+
.map((i) => {
|
|
113
|
+
const c = gazetteer.cities[i]!;
|
|
114
|
+
const [, , iso, pop, name, sub] = c;
|
|
115
|
+
const ambiguous = (gazetteer.byName[fold(name)]?.length ?? 0) > 1;
|
|
116
|
+
const qualifier = sub ?? iso;
|
|
117
|
+
const insert = ambiguous ? `${name} ${qualifier}` : name;
|
|
118
|
+
const label = ambiguous ? `${name} — ${qualifier}` : name;
|
|
119
|
+
const detail = `${qualifier} · ${groupThousands(pop)}`;
|
|
120
|
+
return {
|
|
121
|
+
name,
|
|
122
|
+
insert,
|
|
123
|
+
label,
|
|
124
|
+
detail,
|
|
125
|
+
iso,
|
|
126
|
+
pop,
|
|
127
|
+
kind: 'city' as const,
|
|
128
|
+
...(sub !== undefined && { sub }),
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const airportItems: MapPlaceCompletion[] = airportHits
|
|
133
|
+
.sort((a, b) => {
|
|
134
|
+
const sa = scopeCountry && a.iso === scopeCountry ? 0 : 1;
|
|
135
|
+
const sb = scopeCountry && b.iso === scopeCountry ? 0 : 1;
|
|
136
|
+
return sa - sb || (a.code < b.code ? -1 : a.code > b.code ? 1 : 0);
|
|
137
|
+
})
|
|
138
|
+
.map(({ code, name, iso }) => {
|
|
139
|
+
const upper = code.toUpperCase();
|
|
140
|
+
return {
|
|
141
|
+
name,
|
|
142
|
+
insert: upper,
|
|
143
|
+
label: upper,
|
|
144
|
+
detail: `Airport · ${name}`,
|
|
145
|
+
iso,
|
|
146
|
+
pop: 0,
|
|
147
|
+
kind: 'airport' as const,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
78
150
|
|
|
79
|
-
return
|
|
80
|
-
const c = gazetteer.cities[i]!;
|
|
81
|
-
const [, , iso, pop, name, sub] = c;
|
|
82
|
-
const ambiguous = (gazetteer.byName[fold(name)]?.length ?? 0) > 1;
|
|
83
|
-
const qualifier = sub ?? iso;
|
|
84
|
-
const insert = ambiguous ? `${name} ${qualifier}` : name;
|
|
85
|
-
const label = ambiguous ? `${name} — ${qualifier}` : name;
|
|
86
|
-
const detail = `${qualifier} · ${groupThousands(pop)}`;
|
|
87
|
-
return {
|
|
88
|
-
name,
|
|
89
|
-
insert,
|
|
90
|
-
label,
|
|
91
|
-
detail,
|
|
92
|
-
iso,
|
|
93
|
-
pop,
|
|
94
|
-
...(sub !== undefined && { sub }),
|
|
95
|
-
};
|
|
96
|
-
});
|
|
151
|
+
return [...cityItems, ...airportItems].slice(0, limit);
|
|
97
152
|
}
|
|
98
153
|
|
|
99
154
|
export interface MapRegionCompletion {
|
|
@@ -29,6 +29,11 @@ export interface CountryCandidate {
|
|
|
29
29
|
/** Projected screen anchor `[x, y]` (mainland anchor or `path.centroid`), or
|
|
30
30
|
* null when the feature doesn't project to a finite point. */
|
|
31
31
|
readonly anchor: readonly [number, number] | null;
|
|
32
|
+
/** True when `anchor` came from a curated WORLD_LABEL_ANCHORS entry (a trusted
|
|
33
|
+
* mainland point) rather than the area-weighted centroid. Such a country
|
|
34
|
+
* bypasses the both-axes smear gate (its full-canvas bbox is an antimeridian
|
|
35
|
+
* artifact, not a real footprint, so the unreliable-centroid concern is moot). */
|
|
36
|
+
readonly curatedAnchor?: boolean;
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
export interface ContextLabelArgs {
|
|
@@ -60,6 +65,17 @@ const CONTEXT_PAD = 4; // extra gap enforced between two context labels
|
|
|
60
65
|
const EDGE_CLAMP_MARGIN = 8; // px inset for edge-clamped ocean labels
|
|
61
66
|
const EDGE_CLAMP_OVERSHOOT = 0.35; // max off-frame overshoot (× dim) to still clamp
|
|
62
67
|
|
|
68
|
+
// Country labels scale with their projected footprint: a big landmass (Canada,
|
|
69
|
+
// Mexico on a US view) reads as a large, FADED backdrop name; a small one stays
|
|
70
|
+
// at the base font, fully muted. Size metric is the footprint's linear extent
|
|
71
|
+
// (√bbox-area) as a fraction of the canvas's linear extent (√canvas-area), so
|
|
72
|
+
// the ramp is resolution-independent. Below MIN ⇒ base font / no extra fade;
|
|
73
|
+
// at/above MAX ⇒ max font / max fade; linear between.
|
|
74
|
+
const COUNTRY_FONT_MAX = 22; // px ceiling for the largest footprint
|
|
75
|
+
const COUNTRY_SIZE_FRAC_MIN = 0.06; // footprint linear-frac at base font
|
|
76
|
+
const COUNTRY_SIZE_FRAC_MAX = 0.32; // footprint linear-frac at max font
|
|
77
|
+
const COUNTRY_FADE_MAX = 45; // % blend toward bg at max font (subdue big names)
|
|
78
|
+
|
|
63
79
|
// Water-kind priority within a tier (oceans first, then seas, then the rest) so
|
|
64
80
|
// a thin budget always spends on the highest-orientation-value names.
|
|
65
81
|
const KIND_ORDER: Record<WaterKind, number> = {
|
|
@@ -91,10 +107,10 @@ export function labelBudget(
|
|
|
91
107
|
band: TierBand
|
|
92
108
|
): number {
|
|
93
109
|
const bandCap: Record<TierBand, number> = {
|
|
94
|
-
world:
|
|
95
|
-
continental:
|
|
96
|
-
regional:
|
|
97
|
-
local:
|
|
110
|
+
world: 7,
|
|
111
|
+
continental: 6,
|
|
112
|
+
regional: 5,
|
|
113
|
+
local: 4,
|
|
98
114
|
};
|
|
99
115
|
const area = Math.floor(Math.sqrt(Math.max(0, width * height)) / 150);
|
|
100
116
|
return Math.max(0, Math.min(area, bandCap[band]));
|
|
@@ -136,10 +152,14 @@ function insideViewport(
|
|
|
136
152
|
* the per-gap `letter-spacing` the renderer applies to water names, so without
|
|
137
153
|
* this the fit/clamp math under-measures by ~`(len-1)*spacing` and the label
|
|
138
154
|
* clips at the canvas edge. */
|
|
139
|
-
export function labelWidth(
|
|
155
|
+
export function labelWidth(
|
|
156
|
+
text: string,
|
|
157
|
+
letterSpacing: number,
|
|
158
|
+
font: number = FONT
|
|
159
|
+
): number {
|
|
140
160
|
const spacing =
|
|
141
161
|
letterSpacing > 0 ? Math.max(0, text.length - 1) * letterSpacing : 0;
|
|
142
|
-
return measureLegendText(text,
|
|
162
|
+
return measureLegendText(text, font) + spacing + 2 * PADX;
|
|
143
163
|
}
|
|
144
164
|
|
|
145
165
|
/** Wrap a multi-word name into balanced lines, biased to wrap READILY — water
|
|
@@ -188,10 +208,12 @@ function rectAround(
|
|
|
188
208
|
cx: number,
|
|
189
209
|
cy: number,
|
|
190
210
|
lines: readonly string[],
|
|
191
|
-
letterSpacing: number
|
|
211
|
+
letterSpacing: number,
|
|
212
|
+
font: number = FONT
|
|
192
213
|
): LabelRect {
|
|
193
|
-
const
|
|
194
|
-
const
|
|
214
|
+
const lineHeight = font + 2; // MUST match the renderer's per-line stride
|
|
215
|
+
const w = Math.max(...lines.map((l) => labelWidth(l, letterSpacing, font)));
|
|
216
|
+
const h = (lines.length - 1) * lineHeight + font + 2 * PADY;
|
|
195
217
|
return { x: cx - w / 2, y: cy - h / 2, w, h };
|
|
196
218
|
}
|
|
197
219
|
|
|
@@ -252,6 +274,7 @@ export function placeContextLabels(args: ContextLabelArgs): PlacedLabel[] {
|
|
|
252
274
|
italic: boolean;
|
|
253
275
|
letterSpacing: number;
|
|
254
276
|
color: string;
|
|
277
|
+
fontSize: number;
|
|
255
278
|
sort: number; // priority key (lower first)
|
|
256
279
|
};
|
|
257
280
|
const candidates: Candidate[] = [];
|
|
@@ -328,8 +351,12 @@ export function placeContextLabels(args: ContextLabelArgs): PlacedLabel[] {
|
|
|
328
351
|
italic: true,
|
|
329
352
|
letterSpacing: WATER_LETTER_SPACING,
|
|
330
353
|
color: waterColor,
|
|
331
|
-
//
|
|
332
|
-
|
|
354
|
+
fontSize: FONT, // water names keep the base font (no footprint to scale on)
|
|
355
|
+
// Orientation-value bands (lower = earlier): MAJOR water (oceans + major
|
|
356
|
+
// seas, tier ≤ 1) leads at `tier*10+kind` (0..~16); MINOR water (tier ≥ 2:
|
|
357
|
+
// smaller seas, bays, gulfs, straits) drops to band 2 (2000+) — BELOW the
|
|
358
|
+
// country band (1000+), so a big country outranks a minor basin.
|
|
359
|
+
sort: (tier <= 1 ? 0 : 2000) + tier * 10 + KIND_ORDER[kind],
|
|
333
360
|
});
|
|
334
361
|
}
|
|
335
362
|
|
|
@@ -345,22 +372,49 @@ export function placeContextLabels(args: ContextLabelArgs): PlacedLabel[] {
|
|
|
345
372
|
})
|
|
346
373
|
.filter((r) => Number.isFinite(r.area) && r.area > 0)
|
|
347
374
|
.sort((a, b) => b.area - a.area);
|
|
375
|
+
// Canvas linear extent — the denominator for the footprint size ramp below.
|
|
376
|
+
const canvasLinear = Math.sqrt(Math.max(1, width * height));
|
|
348
377
|
let ci = 0;
|
|
349
378
|
for (const r of ranked) {
|
|
350
|
-
const { c, w, h } = r;
|
|
351
|
-
// F2: an antimeridian-crossing / global-smear country
|
|
352
|
-
//
|
|
353
|
-
// is then unreliable (mid-map, wrong basin).
|
|
354
|
-
//
|
|
355
|
-
|
|
379
|
+
const { c, w, h, area } = r;
|
|
380
|
+
// F2: an antimeridian-crossing / global-smear country fills the canvas in
|
|
381
|
+
// BOTH dimensions while its real landmass is split — the `path.centroid`
|
|
382
|
+
// anchor is then unreliable (mid-map, wrong basin). Reject only that
|
|
383
|
+
// both-axes canvas-smear: a country that is merely wide-but-short (Canada
|
|
384
|
+
// hugging the top of a US frame) or tall-but-narrow (Chile) is a legitimate
|
|
385
|
+
// landmass whose clipExtent-clipped centroid still lands over its own ground,
|
|
386
|
+
// so it must NOT be dropped for footprint shape alone. A `curatedAnchor`
|
|
387
|
+
// (WORLD_LABEL_ANCHORS, e.g. Russia → European Russia) is trustworthy by
|
|
388
|
+
// construction — the smear concern is moot, so it bypasses this gate (the
|
|
389
|
+
// `insideViewport` check below still drops an anchor that projects off-frame).
|
|
390
|
+
if (!c.curatedAnchor && w > width * 0.66 && h > height * 0.66) continue;
|
|
356
391
|
if (!insideViewport(c.anchor, width, height)) continue;
|
|
392
|
+
// Footprint-driven scale (Decision: big landmass = large, faded backdrop
|
|
393
|
+
// name). t∈[0,1] over the [MIN,MAX] linear-fraction band; font ramps up and
|
|
394
|
+
// ink rises in lockstep, so a bigger name reads as a large, softly-inked
|
|
395
|
+
// backdrop. A curated-anchor giant (Russia) keeps the standard big-country
|
|
396
|
+
// styling — its full-canvas bbox lands at the top of the ramp (font/ink like
|
|
397
|
+
// any large in-frame country), which is the intended subdued-backdrop look.
|
|
398
|
+
const sizeFrac = Math.sqrt(area) / canvasLinear;
|
|
399
|
+
const t = Math.min(
|
|
400
|
+
1,
|
|
401
|
+
Math.max(
|
|
402
|
+
0,
|
|
403
|
+
(sizeFrac - COUNTRY_SIZE_FRAC_MIN) /
|
|
404
|
+
(COUNTRY_SIZE_FRAC_MAX - COUNTRY_SIZE_FRAC_MIN)
|
|
405
|
+
)
|
|
406
|
+
);
|
|
407
|
+
const fontSize = Math.round(FONT + t * (COUNTRY_FONT_MAX - FONT));
|
|
408
|
+
const fade = Math.round(t * COUNTRY_FADE_MAX);
|
|
409
|
+
const color = fade > 0 ? mix(countryColor, palette.bg, fade) : countryColor;
|
|
357
410
|
// Always the full country name — never an ISO abbreviation. If the name
|
|
358
411
|
// doesn't fit the footprint, drop the label rather than abbreviate.
|
|
359
412
|
const text = c.name;
|
|
360
|
-
const tw = labelWidth(text, 0);
|
|
361
|
-
// Approximate fit (Decision 4): name fits inside the footprint
|
|
362
|
-
// true point-in-polygon — cartographic labels routinely overrun
|
|
363
|
-
|
|
413
|
+
const tw = labelWidth(text, 0, fontSize);
|
|
414
|
+
// Approximate fit (Decision 4): the (scaled) name fits inside the footprint
|
|
415
|
+
// bbox. NOT true point-in-polygon — cartographic labels routinely overrun
|
|
416
|
+
// coastlines. The bigger the font, the bigger the box it must clear.
|
|
417
|
+
if (tw > w || fontSize + 2 * PADY > h) continue;
|
|
364
418
|
candidates.push({
|
|
365
419
|
text,
|
|
366
420
|
lines: [text],
|
|
@@ -368,9 +422,14 @@ export function placeContextLabels(args: ContextLabelArgs): PlacedLabel[] {
|
|
|
368
422
|
cy: c.anchor[1],
|
|
369
423
|
italic: false,
|
|
370
424
|
letterSpacing: 0,
|
|
371
|
-
color
|
|
372
|
-
|
|
373
|
-
|
|
425
|
+
color,
|
|
426
|
+
fontSize,
|
|
427
|
+
// Band 1 (orientation-value ranking): above MINOR water (band 2, 2000+) but
|
|
428
|
+
// below MAJOR water — oceans + major seas (band 0, ≤~16). So a big country
|
|
429
|
+
// (US, Canada, Russia) outranks a minor sea/bay (Sargasso, Bahía de
|
|
430
|
+
// Campeche) yet never displaces an ocean. Larger area = earlier within the
|
|
431
|
+
// band (`ci` is the area-desc rank index).
|
|
432
|
+
sort: 1000 + ci++,
|
|
374
433
|
});
|
|
375
434
|
}
|
|
376
435
|
|
|
@@ -378,9 +437,24 @@ export function placeContextLabels(args: ContextLabelArgs): PlacedLabel[] {
|
|
|
378
437
|
candidates.sort((a, b) => a.sort - b.sort);
|
|
379
438
|
const placed: PlacedLabel[] = [];
|
|
380
439
|
const placedRects: LabelRect[] = [];
|
|
440
|
+
// Guarantee country/state room: water can otherwise monopolise a small budget
|
|
441
|
+
// (a coastal view borders many oceans/seas), so reserve up to 2 slots for
|
|
442
|
+
// countries whenever any country candidate exists. No effect on pure-water
|
|
443
|
+
// views (`countryCount` 0 ⇒ cap = budget). Major water still leads by sort, so
|
|
444
|
+
// this only trims the LAST water bodies that would have crowded out a country.
|
|
445
|
+
const countryCount = candidates.reduce((n, c) => n + (c.italic ? 0 : 1), 0);
|
|
446
|
+
const waterCap = budget - Math.min(2, countryCount);
|
|
447
|
+
let waterPlaced = 0;
|
|
381
448
|
for (const cand of candidates) {
|
|
382
449
|
if (placed.length >= budget) break;
|
|
383
|
-
|
|
450
|
+
if (cand.italic && waterPlaced >= waterCap) continue;
|
|
451
|
+
const rect = rectAround(
|
|
452
|
+
cand.cx,
|
|
453
|
+
cand.cy,
|
|
454
|
+
cand.lines,
|
|
455
|
+
cand.letterSpacing,
|
|
456
|
+
cand.fontSize
|
|
457
|
+
);
|
|
384
458
|
if (!rectFits(rect, width, height)) continue;
|
|
385
459
|
// Water labels must sit over OPEN WATER and NEVER touch land — sample a grid
|
|
386
460
|
// over every wrapped line (each line's own horizontal extent at five points);
|
|
@@ -408,6 +482,7 @@ export function placeContextLabels(args: ContextLabelArgs): PlacedLabel[] {
|
|
|
408
482
|
if (collides(rect)) continue;
|
|
409
483
|
if (placedRects.some((r) => overlapsPadded(rect, r, CONTEXT_PAD))) continue;
|
|
410
484
|
placedRects.push(rect);
|
|
485
|
+
if (cand.italic) waterPlaced++;
|
|
411
486
|
placed.push({
|
|
412
487
|
x: cand.cx,
|
|
413
488
|
y: cand.cy,
|
|
@@ -419,6 +494,7 @@ export function placeContextLabels(args: ContextLabelArgs): PlacedLabel[] {
|
|
|
419
494
|
// cleanly on the basemap without one.
|
|
420
495
|
halo: false,
|
|
421
496
|
haloColor,
|
|
497
|
+
fontSize: cand.fontSize,
|
|
422
498
|
italic: cand.italic,
|
|
423
499
|
letterSpacing: cand.letterSpacing,
|
|
424
500
|
...(cand.lines.length > 1 ? { lines: cand.lines } : {}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"assets":{"gazetteer.json":{"bytes":130767,"gzBytes":56261,"sha256":"5ad56e5ba0b3a4f9a6dc8bd3bf8b0fda0e7b86cbe4d85d231114f5dd967d65f7"},"lakes.json":{"bytes":6315,"gzBytes":1487,"sha256":"5840ffd49b8dbf30183a9534a72adf80b6e77ceec224665393fa94e956220323"},"mountain-ranges.json":{"bytes":90845,"gzBytes":26493,"sha256":"a698b3f296e61712fb39b3d8d42ec7c4699f8aadecb549367feb7d09f7785580"},"na-lakes.json":{"bytes":39387,"gzBytes":11281,"sha256":"2a41c04969209380d544a09efe354277e12d704458af95955201eb4f698d16c6"},"na-land.json":{"bytes":114082,"gzBytes":32375,"sha256":"7b94c9bb4e809c22813da5ae939e1ff6a781fd77a04d9c1585a9a82d2a195388"},"region-names.json":{"bytes":11667,"gzBytes":2235,"sha256":"059662d30b6ee8572c5943096905e05218e5f337e6973a9d43d6b41b7313a9ac"},"rivers.json":{"bytes":6707,"gzBytes":2158,"sha256":"3912508469099b1c37360c5505ea033c4ffa30ce95f7428e668e9d824cb81407"},"us-states.json":{"bytes":23313,"gzBytes":7413,"sha256":"0fe3a8937bc7566192662439f29a7866e8823d687290bcb003433ad5edd86567"},"water-bodies.json":{"bytes":4854,"gzBytes":2123,"sha256":"6d1a407a376c63518329c52189e2887053c4b61062af0597e060050ae8469635"},"world-coarse.json":{"bytes":55436,"gzBytes":18397,"sha256":"5cb42e3c8975dde56504ca5c68ece0a1e71d0929680b5fc8cdab758c8666dbf8"},"world-detail.json":{"bytes":163562,"gzBytes":46767,"sha256":"39f1736eaabe9e21190972be3157822be22ee84fdc41751237f2b516f09a7586"}},"counts":{"countries":175,"gazetteerAliases":8,"gazetteerCities":2119,"mountainRanges":205,"usStates":56,"waterBodies":113},"generatedBy":"scripts/build-map-data.mjs","sourceHashes":{"https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson@master/10m/physical/ne_10m_lakes.json":{"bytes":6648697,"sha256":"93c8fdf0e591e113f449d0d466e15c7a9841b9b6571c7afe41f95ba51b322452"},"https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson@master/110m/physical/ne_110m_lakes.json":{"bytes":27711,"sha256":"6f315b60488e0cf5da9c360e3ce593babf64c2f44cc21e2820c536f7a2aff606"},"https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson@master/110m/physical/ne_110m_rivers_lake_centerlines.json":{"bytes":54146,"sha256":"959e13128e4eb5a6ee530b8270c5017bcee9149ce48a97f6fe7fee1fce600b5d"},"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_10m_admin_0_countries.geojson":{"bytes":13287234,"sha256":"239eec57ac17f100a11e2536cffc56752c318b50ae765b0918ff7aab4ce8f255"},"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_10m_geography_regions_polys.geojson":{"bytes":5583870,"sha256":"b7b26e50ea917d3696aec87f932def2bf5f890f5770e441d59c162c6f4c92a77"},"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_110m_geography_marine_polys.geojson":{"bytes":534055,"sha256":"b9c3f7f557d0ff5217906adc82b66ecdac14aa7438df7e518cf6675d037bceb8"},"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_50m_geography_marine_polys.geojson":{"bytes":1163418,"sha256":"6fe58083e0cc5c7fad9e396970e28a8580bbd8770cfa4d1d7b5a34423e912f97"},"https://cdn.jsdelivr.net/npm/us-atlas@3.0.1/states-10m.json":{"bytes":114554,"sha256":"d76b391ccfa8bff601d51e3e3da5d43a89fa46cd5caca72ce731b383be5596d0"},"https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json":{"bytes":107761,"sha256":"2516c915867c7baf18ddec727aec46c315541a07cfb3d79a6559b05d5e94eee8"},"https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-50m.json":{"bytes":756420,"sha256":"04342cdc1e3016bcd7db1630de95684d67b79fe3c8c460321e87aef469502394"},"https://download.geonames.org/export/dump/cities5000.zip":{"bytes":5549002,"sha256":"d20e28b2f610da34c21fd82ff6a8e4d24ebe67eba2dccf65bd2c4332ff0f380a"}},"sources":{"geonames":{"citiesUrl":"https://download.geonames.org/export/dump/cities5000.zip","license":"CC BY 4.0 — https://creativecommons.org/licenses/by/4.0/","modificationDateRange":"2006-01-17..2026-06-02 (filtered subset)"},"lakes":{"url":"https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson@master/110m/physical/ne_110m_lakes.json","version":"natural-earth 110m (martynafford snapshot)"},"marineCoarse":{"url":"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_110m_geography_marine_polys.geojson","version":"natural-earth 110m (nvkelso vector snapshot)"},"marineDetail":{"url":"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_50m_geography_marine_polys.geojson","version":"natural-earth 50m (nvkelso vector snapshot)"},"mountainRanges":{"url":"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_10m_geography_regions_polys.geojson","version":"natural-earth 10m (nvkelso vector snapshot)"},"naLakes":{"url":"https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson@master/10m/physical/ne_10m_lakes.json","version":"natural-earth 10m (martynafford snapshot)"},"naLand":{"url":"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_10m_admin_0_countries.geojson","version":"natural-earth 10m (nvkelso vector snapshot)"},"rivers":{"url":"https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson@master/110m/physical/ne_110m_rivers_lake_centerlines.json","version":"natural-earth 110m (martynafford snapshot)"},"usAtlas":{"url":"https://cdn.jsdelivr.net/npm/us-atlas@3.0.1/states-10m.json","version":"3.0.1"},"worldCoarse":{"url":"https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json","version":"2.0.2"},"worldDetail":{"url":"https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-50m.json","version":"2.0.2"}},"tooling":{"mapshaper":"0.7.22"}}
|
|
1
|
+
{"assets":{"airport-collisions.json":{"bytes":29,"gzBytes":49,"sha256":"a7d0107c5fcc6d787e010ea100ce1946bb724f7ae1a79eb1b862a8cf822ed1d8"},"airports.json":{"bytes":104083,"gzBytes":38159,"sha256":"c17294d4e3fd259e02c63c49c36c02d9ff34f4462103ff54516d29e93667cd76"},"gazetteer.json":{"bytes":130767,"gzBytes":56261,"sha256":"5ad56e5ba0b3a4f9a6dc8bd3bf8b0fda0e7b86cbe4d85d231114f5dd967d65f7"},"lakes.json":{"bytes":6315,"gzBytes":1487,"sha256":"5840ffd49b8dbf30183a9534a72adf80b6e77ceec224665393fa94e956220323"},"mountain-ranges.json":{"bytes":90845,"gzBytes":26493,"sha256":"a698b3f296e61712fb39b3d8d42ec7c4699f8aadecb549367feb7d09f7785580"},"na-lakes.json":{"bytes":39387,"gzBytes":11281,"sha256":"2a41c04969209380d544a09efe354277e12d704458af95955201eb4f698d16c6"},"na-land.json":{"bytes":114082,"gzBytes":32375,"sha256":"7b94c9bb4e809c22813da5ae939e1ff6a781fd77a04d9c1585a9a82d2a195388"},"region-names.json":{"bytes":11667,"gzBytes":2235,"sha256":"059662d30b6ee8572c5943096905e05218e5f337e6973a9d43d6b41b7313a9ac"},"rivers.json":{"bytes":6707,"gzBytes":2158,"sha256":"3912508469099b1c37360c5505ea033c4ffa30ce95f7428e668e9d824cb81407"},"us-states.json":{"bytes":23313,"gzBytes":7413,"sha256":"0fe3a8937bc7566192662439f29a7866e8823d687290bcb003433ad5edd86567"},"water-bodies.json":{"bytes":4854,"gzBytes":2123,"sha256":"6d1a407a376c63518329c52189e2887053c4b61062af0597e060050ae8469635"},"world-coarse.json":{"bytes":55436,"gzBytes":18397,"sha256":"5cb42e3c8975dde56504ca5c68ece0a1e71d0929680b5fc8cdab758c8666dbf8"},"world-detail.json":{"bytes":163562,"gzBytes":46767,"sha256":"39f1736eaabe9e21190972be3157822be22ee84fdc41751237f2b516f09a7586"}},"counts":{"airportCollisions":2,"airports":1565,"countries":175,"gazetteerAliases":8,"gazetteerCities":2119,"mountainRanges":205,"usStates":56,"waterBodies":113},"generatedBy":"scripts/build-map-data.mjs","sourceHashes":{"file:scripts/airports-snapshot.csv":{"bytes":308536,"sha256":"58b6e45bb91045c2897f1e7e14430aecc3b5eca911bcb389855495ff66ab9599"},"https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson@master/10m/physical/ne_10m_lakes.json":{"bytes":6648697,"sha256":"93c8fdf0e591e113f449d0d466e15c7a9841b9b6571c7afe41f95ba51b322452"},"https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson@master/110m/physical/ne_110m_lakes.json":{"bytes":27711,"sha256":"6f315b60488e0cf5da9c360e3ce593babf64c2f44cc21e2820c536f7a2aff606"},"https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson@master/110m/physical/ne_110m_rivers_lake_centerlines.json":{"bytes":54146,"sha256":"959e13128e4eb5a6ee530b8270c5017bcee9149ce48a97f6fe7fee1fce600b5d"},"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_10m_admin_0_countries.geojson":{"bytes":13287234,"sha256":"239eec57ac17f100a11e2536cffc56752c318b50ae765b0918ff7aab4ce8f255"},"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_10m_geography_regions_polys.geojson":{"bytes":5583870,"sha256":"b7b26e50ea917d3696aec87f932def2bf5f890f5770e441d59c162c6f4c92a77"},"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_110m_geography_marine_polys.geojson":{"bytes":534055,"sha256":"b9c3f7f557d0ff5217906adc82b66ecdac14aa7438df7e518cf6675d037bceb8"},"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_50m_geography_marine_polys.geojson":{"bytes":1163418,"sha256":"6fe58083e0cc5c7fad9e396970e28a8580bbd8770cfa4d1d7b5a34423e912f97"},"https://cdn.jsdelivr.net/npm/us-atlas@3.0.1/states-10m.json":{"bytes":114554,"sha256":"d76b391ccfa8bff601d51e3e3da5d43a89fa46cd5caca72ce731b383be5596d0"},"https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json":{"bytes":107761,"sha256":"2516c915867c7baf18ddec727aec46c315541a07cfb3d79a6559b05d5e94eee8"},"https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-50m.json":{"bytes":756420,"sha256":"04342cdc1e3016bcd7db1630de95684d67b79fe3c8c460321e87aef469502394"},"https://download.geonames.org/export/dump/cities5000.zip":{"bytes":5549002,"sha256":"d20e28b2f610da34c21fd82ff6a8e4d24ebe67eba2dccf65bd2c4332ff0f380a"}},"sources":{"geonames":{"citiesUrl":"https://download.geonames.org/export/dump/cities5000.zip","license":"CC BY 4.0 — https://creativecommons.org/licenses/by/4.0/","modificationDateRange":"2006-01-17..2026-06-02 (filtered subset)"},"lakes":{"url":"https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson@master/110m/physical/ne_110m_lakes.json","version":"natural-earth 110m (martynafford snapshot)"},"marineCoarse":{"url":"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_110m_geography_marine_polys.geojson","version":"natural-earth 110m (nvkelso vector snapshot)"},"marineDetail":{"url":"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_50m_geography_marine_polys.geojson","version":"natural-earth 50m (nvkelso vector snapshot)"},"mountainRanges":{"url":"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_10m_geography_regions_polys.geojson","version":"natural-earth 10m (nvkelso vector snapshot)"},"naLakes":{"url":"https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson@master/10m/physical/ne_10m_lakes.json","version":"natural-earth 10m (martynafford snapshot)"},"naLand":{"url":"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_10m_admin_0_countries.geojson","version":"natural-earth 10m (nvkelso vector snapshot)"},"ourairports":{"license":"Public Domain — https://ourairports.com/data/","note":"Lean committed slice (scheduled_service=yes + 3-letter iata_code). Regenerate is a deliberate, reviewed bump (ADR-4).","snapshot":"scripts/airports-snapshot.csv","source":"https://davidmegginson.github.io/ourairports-data/airports.csv"},"rivers":{"url":"https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson@master/110m/physical/ne_110m_rivers_lake_centerlines.json","version":"natural-earth 110m (martynafford snapshot)"},"usAtlas":{"url":"https://cdn.jsdelivr.net/npm/us-atlas@3.0.1/states-10m.json","version":"3.0.1"},"worldCoarse":{"url":"https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json","version":"2.0.2"},"worldDetail":{"url":"https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-50m.json","version":"2.0.2"}},"tooling":{"mapshaper":"0.7.22"}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"collisions":["aba","ufa"]}
|