@heroiclands/package-build 22.4.5 → 22.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/CONTENT.md +109 -0
- package/bin/content-build.mjs +36 -8
- package/content-config.mjs +8 -3
- package/docs/api.md +86 -46
- package/docs/commands.md +64 -24
- package/docs/configuration.md +20 -1
- package/docs/content-format.md +71 -0
- package/engine/frontmatter-lint.mjs +2 -0
- package/engine/holdings.mjs +391 -0
- package/engine/index.mjs +9 -0
- package/engine/map-build.mjs +41 -11
- package/engine/map-dot.mjs +84 -24
- package/engine/map-graphviz.mjs +12 -3
- package/engine/map-layout.mjs +283 -101
- package/engine/map-places.mjs +33 -3
- package/engine/market-class.mjs +178 -0
- package/engine/metadata-index.mjs +7 -4
- package/engine/note-vocabulary.mjs +19 -0
- package/engine/site-build.mjs +88 -10
- package/engine/site-maps.mjs +165 -0
- package/package.json +1 -1
- package/types/engine/holdings.d.mts +146 -0
- package/types/engine/index.d.mts +3 -0
- package/types/engine/map-build.d.mts +13 -7
- package/types/engine/map-dot.d.mts +22 -9
- package/types/engine/map-graphviz.d.mts +8 -1
- package/types/engine/map-layout.d.mts +117 -49
- package/types/engine/map-places.d.mts +26 -0
- package/types/engine/market-class.d.mts +62 -0
- package/types/engine/note-vocabulary.d.mts +10 -0
- package/types/engine/site-build.d.mts +37 -6
- package/types/engine/site-maps.d.mts +81 -0
package/engine/map-layout.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* The geometry of the map from a place.
|
|
15
|
+
* The geometry of the map from a place, and of the chart from a place.
|
|
16
16
|
*
|
|
17
17
|
* A pre-modern map was an itinerary: centred on its maker, exact about the
|
|
18
18
|
* next stage and vague about the tenth. A place note's `borders` and `routes`
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
* right**, the angle from the bearing and the radius from the days marker.
|
|
22
22
|
* One ring per marker, spaced logarithmically so a day's ride and a month's
|
|
23
23
|
* voyage both fit on one page; a border with no route on the innermost ring; a
|
|
24
|
-
* place whose days nobody stated on the rim, marked unknown.
|
|
24
|
+
* place whose days nobody stated on the rim, marked unknown. The chart is the
|
|
25
|
+
* same drawing from the same place at a larger horizon: the hops chain on as
|
|
26
|
+
* long as none turns back, and a year's travel fits on the page.
|
|
25
27
|
*
|
|
26
28
|
* **Positions are computed here, not solved by a layout engine.** GraphViz
|
|
27
29
|
* receives every node pinned and only draws, so the picture states what the
|
|
@@ -119,35 +121,46 @@ const RADIUS_PER_LOG_DAY = (HORIZON_RADIUS - INNER_RADIUS) / Math.log(HORIZON_DA
|
|
|
119
121
|
* days nobody stated, is named. One log step beyond the horizon, so the rim
|
|
120
122
|
* reads as the next ring rather than a margin.
|
|
121
123
|
*
|
|
124
|
+
* @param {number} [horizon] - The horizon in days; the map from a place's
|
|
125
|
+
* by default.
|
|
122
126
|
* @returns {number} Points.
|
|
123
127
|
*/
|
|
124
|
-
export function rimRadius() {
|
|
125
|
-
return INNER_RADIUS + RADIUS_PER_LOG_DAY * Math.log(
|
|
128
|
+
export function rimRadius(horizon = HORIZON_DAYS) {
|
|
129
|
+
return INNER_RADIUS + RADIUS_PER_LOG_DAY * Math.log(horizon * 2);
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
/**
|
|
129
133
|
* The radius of the ring a days value sits on.
|
|
130
134
|
*
|
|
131
135
|
* Logarithmic in days, so equal ratios of days are equal steps of radius: a
|
|
132
|
-
* day to two days is the same step as five to ten
|
|
133
|
-
* horizon, and no value at all, is the rim.
|
|
136
|
+
* day to two days is the same step as five to ten, whatever the horizon. Any
|
|
137
|
+
* value beyond the horizon, and no value at all, is the rim.
|
|
134
138
|
*
|
|
135
139
|
* @param {number|undefined} days - A days value, on the scale or not.
|
|
140
|
+
* @param {number} [horizon] - The horizon in days; the map from a place's
|
|
141
|
+
* by default.
|
|
136
142
|
* @returns {number} Points.
|
|
137
143
|
*/
|
|
138
|
-
export function ringRadius(days) {
|
|
139
|
-
if (typeof days !== "number" || !Number.isFinite(days) || days <= 0)
|
|
140
|
-
|
|
144
|
+
export function ringRadius(days, horizon = HORIZON_DAYS) {
|
|
145
|
+
if (typeof days !== "number" || !Number.isFinite(days) || days <= 0) {
|
|
146
|
+
return rimRadius(horizon);
|
|
147
|
+
}
|
|
148
|
+
if (days > horizon) return rimRadius(horizon);
|
|
141
149
|
return INNER_RADIUS + RADIUS_PER_LOG_DAY * Math.log(days);
|
|
142
150
|
}
|
|
143
151
|
|
|
144
152
|
/**
|
|
145
153
|
* The rings, inside out — one per marker up to the horizon.
|
|
146
154
|
*
|
|
155
|
+
* @param {number} [horizon] - The horizon in days; the map from a place's
|
|
156
|
+
* by default.
|
|
147
157
|
* @returns {Array<{days: number, radius: number}>} Each marker and its radius.
|
|
148
158
|
*/
|
|
149
|
-
export function rings() {
|
|
150
|
-
return
|
|
159
|
+
export function rings(horizon = HORIZON_DAYS) {
|
|
160
|
+
return TRAVEL_DAYS.filter((d) => d <= horizon).map((days) => ({
|
|
161
|
+
days,
|
|
162
|
+
radius: ringRadius(days, horizon),
|
|
163
|
+
}));
|
|
151
164
|
}
|
|
152
165
|
|
|
153
166
|
/**
|
|
@@ -191,18 +204,67 @@ export function edgeLength(days) {
|
|
|
191
204
|
* @property {number} [days] - The route's marker; absent for a border.
|
|
192
205
|
*/
|
|
193
206
|
|
|
207
|
+
/**
|
|
208
|
+
* How far, in degrees, a hop's bearing may lie from the running direction of
|
|
209
|
+
* the journey so far and still compose: one octant either way. A path may
|
|
210
|
+
* bend, hop by hop, but never turn back.
|
|
211
|
+
*/
|
|
212
|
+
const TURN_DEGREES = 45;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* A chain of hops composed into one bearing and one total, or nothing where
|
|
216
|
+
* a hop turns back.
|
|
217
|
+
*
|
|
218
|
+
* The composed direction is that of the hops added as vectors weighted by
|
|
219
|
+
* their days, and the total is their sum. Each hop after the first is
|
|
220
|
+
* checked against the running direction of the chain before it — not
|
|
221
|
+
* against the previous hop — and composes only where it lies within one
|
|
222
|
+
* octant of that direction, so a path may bend a little at every hop and
|
|
223
|
+
* drift past an octant of where it began, but a hop that turns back ends
|
|
224
|
+
* the chain: a place east and then north of here is not in any one
|
|
225
|
+
* direction, and the drawing omits it rather than guess. A hop with no days
|
|
226
|
+
* — a border — makes the total unknown, because a frontier states no
|
|
227
|
+
* distance, and then every hop weighs the same, because nothing says which
|
|
228
|
+
* is the longer.
|
|
229
|
+
*
|
|
230
|
+
* @param {Hop[]} hops - The hops, from the centre outward.
|
|
231
|
+
* @returns {{angle: number, days: number|undefined}|undefined} The composed
|
|
232
|
+
* bearing in degrees and the total days, or `undefined` where a hop turns
|
|
233
|
+
* back, a bearing is not one, or there are no hops.
|
|
234
|
+
*/
|
|
235
|
+
export function composeChain(hops) {
|
|
236
|
+
if (!Array.isArray(hops) || hops.length === 0) return undefined;
|
|
237
|
+
const known = (d) => typeof d === "number" && Number.isFinite(d) && d > 0;
|
|
238
|
+
const allKnown = hops.every((hop) => known(hop?.days));
|
|
239
|
+
let x = 0;
|
|
240
|
+
let y = 0;
|
|
241
|
+
for (const hop of hops) {
|
|
242
|
+
const u = bearingVector(hop?.bearing);
|
|
243
|
+
if (!u) return undefined;
|
|
244
|
+
if (x !== 0 || y !== 0) {
|
|
245
|
+
const running = (Math.atan2(y, x) * 180) / Math.PI;
|
|
246
|
+
const turn =
|
|
247
|
+
Math.abs(/** @type {number} */ (bearingAngle(hop.bearing)) - running) % 360;
|
|
248
|
+
if (Math.min(turn, 360 - turn) > TURN_DEGREES + 1e-9) return undefined;
|
|
249
|
+
}
|
|
250
|
+
const w = allKnown ? /** @type {number} */ (hop.days) : 1;
|
|
251
|
+
x += w * u.x;
|
|
252
|
+
y += w * u.y;
|
|
253
|
+
}
|
|
254
|
+
const angle = (Math.atan2(y, x) * 180) / Math.PI;
|
|
255
|
+
const days =
|
|
256
|
+
allKnown ? hops.reduce((sum, hop) => sum + /** @type {number} */ (hop.days), 0) : undefined;
|
|
257
|
+
// -0 from atan2 on an exactly-east result reads as 0.
|
|
258
|
+
return { angle: angle === 0 ? 0 : angle, days };
|
|
259
|
+
}
|
|
260
|
+
|
|
194
261
|
/**
|
|
195
262
|
* Two hops composed into one bearing and one total, or nothing where they
|
|
196
263
|
* disagree.
|
|
197
264
|
*
|
|
198
|
-
* Two hops agree when their bearings are the same or adjacent
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* makes the total unknown, because a frontier states no distance, and then
|
|
202
|
-
* the two hops weigh the same, because nothing says which is the longer.
|
|
203
|
-
* Hops two or more steps apart do not compose:
|
|
204
|
-
* a place east and then north of here is not in any one direction, and the
|
|
205
|
-
* map omits it rather than guess.
|
|
265
|
+
* Two hops agree when their bearings are the same or adjacent — the
|
|
266
|
+
* two-hop case of {@link composeChain}: the second hop is checked against
|
|
267
|
+
* the first, which is the whole running direction at that point.
|
|
206
268
|
*
|
|
207
269
|
* @param {Hop} first - The hop from the centre.
|
|
208
270
|
* @param {Hop} second - The hop onward.
|
|
@@ -211,21 +273,7 @@ export function edgeLength(days) {
|
|
|
211
273
|
* disagree or either bearing is not one.
|
|
212
274
|
*/
|
|
213
275
|
export function composeHops(first, second) {
|
|
214
|
-
|
|
215
|
-
if (steps === undefined || steps > 1) return undefined;
|
|
216
|
-
const u = /** @type {{x: number, y: number}} */ (bearingVector(first.bearing));
|
|
217
|
-
const v = /** @type {{x: number, y: number}} */ (bearingVector(second.bearing));
|
|
218
|
-
const known = (d) => typeof d === "number" && Number.isFinite(d) && d > 0;
|
|
219
|
-
const both = known(first.days) && known(second.days);
|
|
220
|
-
const w1 = both ? /** @type {number} */ (first.days) : 1;
|
|
221
|
-
const w2 = both ? /** @type {number} */ (second.days) : 1;
|
|
222
|
-
const x = w1 * u.x + w2 * v.x;
|
|
223
|
-
const y = w1 * u.y + w2 * v.y;
|
|
224
|
-
const angle = (Math.atan2(y, x) * 180) / Math.PI;
|
|
225
|
-
const days =
|
|
226
|
-
both ? /** @type {number} */ (first.days) + /** @type {number} */ (second.days) : undefined;
|
|
227
|
-
// -0 from atan2 on an exactly-east result reads as 0.
|
|
228
|
-
return { angle: angle === 0 ? 0 : angle, days };
|
|
276
|
+
return composeChain([first, second]);
|
|
229
277
|
}
|
|
230
278
|
|
|
231
279
|
/* --------------------------------------------------------------------- */
|
|
@@ -383,7 +431,8 @@ export function relationsOf(places, shortcode) {
|
|
|
383
431
|
*
|
|
384
432
|
* @typedef {object} FromNode
|
|
385
433
|
* @property {string} shortcode
|
|
386
|
-
* @property {number} hop - 0 for the centre, 1 for a neighbour, 2
|
|
434
|
+
* @property {number} hop - 0 for the centre, 1 for a neighbour, 2 and up
|
|
435
|
+
* beyond — the map from a place stops at 2, the chart goes on.
|
|
387
436
|
* @property {number} x - Points, east positive.
|
|
388
437
|
* @property {number} y - Points, north positive.
|
|
389
438
|
* @property {number} angle - Degrees, counter-clockwise from east.
|
|
@@ -391,7 +440,8 @@ export function relationsOf(places, shortcode) {
|
|
|
391
440
|
* @property {number} [days] - The days the ring was chosen by, or none.
|
|
392
441
|
* @property {boolean} border - Reached by a border alone.
|
|
393
442
|
* @property {boolean} unknown - On the rim because nobody stated its days.
|
|
394
|
-
* @property {string} [via] - The
|
|
443
|
+
* @property {string} [via] - The place a hop beyond the neighbours is reached
|
|
444
|
+
* through.
|
|
395
445
|
*/
|
|
396
446
|
|
|
397
447
|
/**
|
|
@@ -406,7 +456,7 @@ export function relationsOf(places, shortcode) {
|
|
|
406
456
|
* @property {string} [mode] - The shortest route's.
|
|
407
457
|
* @property {Array<{mode: string, days: number|undefined}>} routes - Every
|
|
408
458
|
* route that reaches `to`, for the label.
|
|
409
|
-
* @property {number} hop - 1 from the centre, 2 onward.
|
|
459
|
+
* @property {number} hop - 1 from the centre, 2 and up onward.
|
|
410
460
|
*/
|
|
411
461
|
|
|
412
462
|
/**
|
|
@@ -451,26 +501,50 @@ function fanApart(nodes, places) {
|
|
|
451
501
|
}
|
|
452
502
|
|
|
453
503
|
/**
|
|
454
|
-
*
|
|
504
|
+
* The layout of the map from a place or of the chart from one.
|
|
505
|
+
*
|
|
506
|
+
* @typedef {object} FromLayout
|
|
507
|
+
* @property {string} centre
|
|
508
|
+
* @property {FromNode[]} nodes
|
|
509
|
+
* @property {FromEdge[]} edges
|
|
510
|
+
* @property {Array<{days: number, radius: number}>} rings
|
|
511
|
+
* @property {number} rim - The rim's radius, in points.
|
|
512
|
+
* @property {number} horizon - The horizon, in days.
|
|
513
|
+
* @property {boolean} beyond - Whether a place past the horizon is named at
|
|
514
|
+
* the rim; false, it is omitted and the rim holds only the unknown.
|
|
515
|
+
* @property {Array<{from: string, to: string}>} unresolved
|
|
516
|
+
*/
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Lay out the itinerary from a place: the map from it, or the chart.
|
|
455
520
|
*
|
|
456
521
|
* The centre at the origin. Every neighbour it states, or that states it, at
|
|
457
522
|
* the angle of its bearing and on the ring of its days — the innermost ring
|
|
458
523
|
* for a border with no route, the shortest route where several modes reach
|
|
459
|
-
* one neighbour. Beyond
|
|
460
|
-
* the composed bearing where
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
* the one with known days, then the
|
|
524
|
+
* one neighbour. Beyond the neighbours, every place a chain of hops reaches
|
|
525
|
+
* within `maxHops`, at the composed bearing where no hop turns back and on
|
|
526
|
+
* the ring of the summed days; on the rim, marked unknown, where a hop
|
|
527
|
+
* beyond the first states no days; omitted where a hop turns back. A place
|
|
528
|
+
* reachable by several chains takes the one with known days, then the
|
|
529
|
+
* fewest, then the fewest hops, then the first `via` by name. The chain goes
|
|
530
|
+
* on from a neighbour and from any place whose total is known and within
|
|
531
|
+
* the horizon; it does not go on from a place at the rim, because the
|
|
532
|
+
* drawing states nothing beyond a distance it does not know. A place whose
|
|
533
|
+
* total lies past the horizon is named at the rim with its days where
|
|
534
|
+
* `beyond` is set, and left out otherwise. Nodes sharing a ring and a
|
|
464
535
|
* bearing are fanned apart.
|
|
465
536
|
*
|
|
466
537
|
* @param {Map<string, MapPlace>} places - Every place, by shortcode.
|
|
467
|
-
* @param {string} centre - The place the
|
|
468
|
-
* @
|
|
469
|
-
*
|
|
470
|
-
*
|
|
538
|
+
* @param {string} centre - The place the drawing is from.
|
|
539
|
+
* @param {object} opts
|
|
540
|
+
* @param {number} opts.maxHops - How many hops out the chain may reach.
|
|
541
|
+
* @param {number} opts.horizon - The horizon, in days, a marker of the scale.
|
|
542
|
+
* @param {boolean} opts.beyond - Whether a place past the horizon is named at
|
|
543
|
+
* the rim rather than omitted.
|
|
544
|
+
* @returns {FromLayout} The layout, in points.
|
|
471
545
|
* @throws {Error} When `centre` is not a place.
|
|
472
546
|
*/
|
|
473
|
-
|
|
547
|
+
function layoutItinerary(places, centre, { maxHops, horizon, beyond }) {
|
|
474
548
|
if (!places.has(centre)) {
|
|
475
549
|
throw new Error(`"${centre}" is not a place in this package or a fetched index`);
|
|
476
550
|
}
|
|
@@ -492,8 +566,8 @@ export function layoutFrom(places, centre) {
|
|
|
492
566
|
const edges = [];
|
|
493
567
|
/** @type {Map<string, FromNode>} */
|
|
494
568
|
const byShortcode = new Map([[centre, nodes[0]]]);
|
|
495
|
-
/** @type {Map<string,
|
|
496
|
-
const
|
|
569
|
+
/** @type {Map<string, Hop[]>} The hops each drawn place is reached by. */
|
|
570
|
+
const paths = new Map([[centre, []]]);
|
|
497
571
|
|
|
498
572
|
const { relations, unresolved } = relationsOf(places, centre);
|
|
499
573
|
|
|
@@ -511,7 +585,7 @@ export function layoutFrom(places, centre) {
|
|
|
511
585
|
x: 0,
|
|
512
586
|
y: 0,
|
|
513
587
|
angle: /** @type {number} */ (bearingAngle(relation.bearing)),
|
|
514
|
-
radius: days === undefined ? ringRadius(1) : ringRadius(days),
|
|
588
|
+
radius: days === undefined ? ringRadius(1, horizon) : ringRadius(days, horizon),
|
|
515
589
|
days,
|
|
516
590
|
border: relation.kind === "border",
|
|
517
591
|
// A route whose days are off the scale sits on the rim, and
|
|
@@ -520,7 +594,7 @@ export function layoutFrom(places, centre) {
|
|
|
520
594
|
};
|
|
521
595
|
nodes.push(node);
|
|
522
596
|
byShortcode.set(relation.to, node);
|
|
523
|
-
|
|
597
|
+
paths.set(relation.to, [{ bearing: relation.bearing, days }]);
|
|
524
598
|
firstEdges.set(relation.to, {
|
|
525
599
|
from: centre,
|
|
526
600
|
to: relation.to,
|
|
@@ -545,67 +619,104 @@ export function layoutFrom(places, centre) {
|
|
|
545
619
|
edge.mode = String(relation.mode ?? "");
|
|
546
620
|
node.days = days;
|
|
547
621
|
node.border = false;
|
|
548
|
-
node.radius = ringRadius(days);
|
|
622
|
+
node.radius = ringRadius(days, horizon);
|
|
549
623
|
node.unknown = days === undefined;
|
|
550
|
-
|
|
624
|
+
paths.set(relation.to, [{ bearing: relation.bearing, days }]);
|
|
551
625
|
}
|
|
552
626
|
}
|
|
553
627
|
|
|
554
|
-
//
|
|
555
|
-
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
628
|
+
// Without a rim for the beyond, a neighbour past the horizon is out like
|
|
629
|
+
// any other place.
|
|
630
|
+
const within = (days) => days === undefined || days <= horizon;
|
|
631
|
+
if (!beyond) {
|
|
632
|
+
for (const node of [...nodes]) {
|
|
633
|
+
if (node.hop !== 1 || within(node.days)) continue;
|
|
634
|
+
nodes.splice(nodes.indexOf(node), 1);
|
|
635
|
+
edges.splice(
|
|
636
|
+
edges.indexOf(/** @type {FromEdge} */ (firstEdges.get(node.shortcode))),
|
|
637
|
+
1,
|
|
638
|
+
);
|
|
639
|
+
byShortcode.delete(node.shortcode);
|
|
640
|
+
paths.delete(node.shortcode);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Beyond the neighbours: the best chain per place, settled in order of
|
|
645
|
+
// total days — known before unknown, fewer before more, then fewer hops,
|
|
646
|
+
// then by the name of the place it is reached through — and each place
|
|
647
|
+
// extended once, from the chain it is drawn by.
|
|
648
|
+
/** @type {Array<{node: FromNode, edge: FromEdge, hops: Hop[]}>} */
|
|
649
|
+
const queue = [];
|
|
650
|
+
const rank = (a, b) => {
|
|
559
651
|
const aKnown = a.node.days !== undefined;
|
|
560
652
|
const bKnown = b.node.days !== undefined;
|
|
561
|
-
if (aKnown !== bKnown) return aKnown;
|
|
562
|
-
if (aKnown && a.node.days !== b.node.days)
|
|
563
|
-
|
|
653
|
+
if (aKnown !== bKnown) return aKnown ? -1 : 1;
|
|
654
|
+
if (aKnown && a.node.days !== b.node.days) {
|
|
655
|
+
return /** @type {number} */ (a.node.days) - /** @type {number} */ (b.node.days);
|
|
656
|
+
}
|
|
657
|
+
if (a.node.hop !== b.node.hop) return a.node.hop - b.node.hop;
|
|
658
|
+
return (
|
|
659
|
+
String(a.node.via).localeCompare(String(b.node.via)) ||
|
|
660
|
+
a.node.shortcode.localeCompare(b.node.shortcode)
|
|
661
|
+
);
|
|
564
662
|
};
|
|
565
|
-
|
|
566
|
-
const
|
|
567
|
-
|
|
663
|
+
const extend = (via) => {
|
|
664
|
+
const sofar = /** @type {Hop[]} */ (paths.get(via));
|
|
665
|
+
if (sofar.length >= maxHops) return;
|
|
666
|
+
for (const relation of relationsOf(places, via).relations) {
|
|
568
667
|
if (byShortcode.has(relation.to)) continue;
|
|
569
|
-
const
|
|
570
|
-
const composed =
|
|
668
|
+
const hops = [...sofar, { bearing: relation.bearing, days: relation.days }];
|
|
669
|
+
const composed = composeChain(hops);
|
|
571
670
|
if (!composed) continue;
|
|
671
|
+
if (!beyond && !within(composed.days)) continue;
|
|
572
672
|
const marker = daysMarker(composed.days);
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
673
|
+
queue.push({
|
|
674
|
+
node: {
|
|
675
|
+
shortcode: relation.to,
|
|
676
|
+
hop: hops.length,
|
|
677
|
+
x: 0,
|
|
678
|
+
y: 0,
|
|
679
|
+
angle: composed.angle,
|
|
680
|
+
radius: ringRadius(marker, horizon),
|
|
681
|
+
days: composed.days,
|
|
682
|
+
border: relation.kind === "border" && hops.every((h) => h.days === undefined),
|
|
683
|
+
unknown: composed.days === undefined,
|
|
684
|
+
via,
|
|
685
|
+
},
|
|
686
|
+
edge: {
|
|
687
|
+
from: via,
|
|
688
|
+
to: relation.to,
|
|
689
|
+
kind: relation.kind,
|
|
690
|
+
bearing: relation.bearing,
|
|
691
|
+
hop: hops.length,
|
|
692
|
+
routes:
|
|
693
|
+
relation.kind === "route" ?
|
|
694
|
+
[{ mode: String(relation.mode ?? ""), days: relation.days }]
|
|
695
|
+
: [],
|
|
696
|
+
...(relation.kind === "route" ?
|
|
697
|
+
{ days: relation.days, mode: relation.mode }
|
|
698
|
+
: {}),
|
|
699
|
+
},
|
|
700
|
+
hops,
|
|
701
|
+
});
|
|
601
702
|
}
|
|
602
|
-
}
|
|
603
|
-
for (const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
703
|
+
};
|
|
704
|
+
for (const node of nodes) if (node.hop === 1) extend(node.shortcode);
|
|
705
|
+
/** @type {FromNode[]} */
|
|
706
|
+
const beyondNeighbours = [];
|
|
707
|
+
while (queue.length) {
|
|
708
|
+
queue.sort(rank);
|
|
709
|
+
const { node, edge, hops } = /** @type {NonNullable<typeof queue[0]>} */ (queue.shift());
|
|
710
|
+
if (byShortcode.has(node.shortcode)) continue;
|
|
711
|
+
byShortcode.set(node.shortcode, node);
|
|
712
|
+
paths.set(node.shortcode, hops);
|
|
713
|
+
beyondNeighbours.push(node);
|
|
607
714
|
edges.push(edge);
|
|
715
|
+
if (node.days !== undefined && within(node.days)) extend(node.shortcode);
|
|
608
716
|
}
|
|
717
|
+
beyondNeighbours.sort((a, b) => a.hop - b.hop || a.shortcode.localeCompare(b.shortcode));
|
|
718
|
+
nodes.push(...beyondNeighbours);
|
|
719
|
+
edges.sort((a, b) => a.hop - b.hop || (a.hop === 1 ? 0 : a.to.localeCompare(b.to)));
|
|
609
720
|
|
|
610
721
|
fanApart(nodes, places);
|
|
611
722
|
for (const node of nodes) {
|
|
@@ -615,7 +726,78 @@ export function layoutFrom(places, centre) {
|
|
|
615
726
|
node.y = exact(node.radius * Math.sin(rad));
|
|
616
727
|
}
|
|
617
728
|
|
|
618
|
-
return {
|
|
729
|
+
return {
|
|
730
|
+
centre,
|
|
731
|
+
nodes,
|
|
732
|
+
edges,
|
|
733
|
+
rings: rings(horizon),
|
|
734
|
+
rim: rimRadius(horizon),
|
|
735
|
+
horizon,
|
|
736
|
+
beyond,
|
|
737
|
+
unresolved,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Lay out the map from a place: two hops, a horizon of 90 days, and a rim
|
|
743
|
+
* naming what lies beyond it.
|
|
744
|
+
*
|
|
745
|
+
* The centre at the origin. Every neighbour it states, or that states it, at
|
|
746
|
+
* the angle of its bearing and on the ring of its days — the innermost ring
|
|
747
|
+
* for a border with no route, the shortest route where several modes reach
|
|
748
|
+
* one neighbour. Beyond each neighbour, every place a second hop reaches, at
|
|
749
|
+
* the composed bearing where the two hops agree and on the ring of their
|
|
750
|
+
* total; on the rim, marked unknown, where either hop states no days; omitted
|
|
751
|
+
* where the hops disagree. A place reachable by several second hops takes
|
|
752
|
+
* the one with known days, then the shortest. Nodes sharing a ring and a
|
|
753
|
+
* bearing are fanned apart.
|
|
754
|
+
*
|
|
755
|
+
* @param {Map<string, MapPlace>} places - Every place, by shortcode.
|
|
756
|
+
* @param {string} centre - The place the map is from.
|
|
757
|
+
* @returns {FromLayout} The layout, in points.
|
|
758
|
+
* @throws {Error} When `centre` is not a place.
|
|
759
|
+
*/
|
|
760
|
+
export function layoutFrom(places, centre) {
|
|
761
|
+
return layoutItinerary(places, centre, { maxHops: 2, horizon: HORIZON_DAYS, beyond: true });
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* The horizon of the chart from a place when none is asked for, in days: the
|
|
766
|
+
* end of the scale, so the whole of what the notes state fits on the page.
|
|
767
|
+
*
|
|
768
|
+
* @type {number}
|
|
769
|
+
*/
|
|
770
|
+
export const CHART_HORIZON_DAYS = 360;
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Lay out the chart from a place: the map from it at a larger horizon.
|
|
774
|
+
*
|
|
775
|
+
* The neighbours as on the map from a place. Beyond them the hops chain on,
|
|
776
|
+
* each checked against the running direction of the chain so far and
|
|
777
|
+
* composing only within one octant of it, so a path bends but never turns
|
|
778
|
+
* back; a place is drawn at the composed bearing on the ring of the summed
|
|
779
|
+
* days, and where several chains reach it, by the one with the fewest. A
|
|
780
|
+
* border beyond the first hop makes the total unknown and sends the place to
|
|
781
|
+
* the rim, and nothing is drawn beyond it. A place whose total exceeds the
|
|
782
|
+
* horizon is left out — the chart is already the larger view — so the rim
|
|
783
|
+
* holds the unknown alone.
|
|
784
|
+
*
|
|
785
|
+
* @param {Map<string, MapPlace>} places - Every place, by shortcode.
|
|
786
|
+
* @param {string} centre - The place the chart is from.
|
|
787
|
+
* @param {object} [opts]
|
|
788
|
+
* @param {number} [opts.horizon] - The horizon in days, a marker of the
|
|
789
|
+
* scale; {@link CHART_HORIZON_DAYS} by default.
|
|
790
|
+
* @returns {FromLayout} The layout, in points.
|
|
791
|
+
* @throws {Error} When `centre` is not a place, or `horizon` is not a marker
|
|
792
|
+
* of the scale.
|
|
793
|
+
*/
|
|
794
|
+
export function layoutChart(places, centre, { horizon = CHART_HORIZON_DAYS } = {}) {
|
|
795
|
+
if (!TRAVEL_DAYS.includes(horizon)) {
|
|
796
|
+
throw new Error(
|
|
797
|
+
`a chart's horizon must be a marker of the days scale — one of ${TRAVEL_DAYS.join(", ")} — and ${horizon} is not`,
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
return layoutItinerary(places, centre, { maxHops: Infinity, horizon, beyond: false });
|
|
619
801
|
}
|
|
620
802
|
|
|
621
803
|
/**
|
package/engine/map-places.mjs
CHANGED
|
@@ -251,14 +251,44 @@ export function loadMapWorld({ config, contentBase, base, problems }) {
|
|
|
251
251
|
skipDirectories: config.skipDirectories,
|
|
252
252
|
problems,
|
|
253
253
|
});
|
|
254
|
-
const world = placesFromRecords(records, { contentBase: tree, base });
|
|
255
|
-
world.config = config;
|
|
256
254
|
const foreign = loadForeignIndexes(config, [config.contentPackage]);
|
|
257
|
-
|
|
255
|
+
const world = mapWorld({
|
|
256
|
+
records,
|
|
257
|
+
foreignIndex: foreign.index,
|
|
258
|
+
contentBase: tree,
|
|
259
|
+
base,
|
|
260
|
+
config,
|
|
261
|
+
});
|
|
258
262
|
world.stale = foreign.stale;
|
|
259
263
|
return world;
|
|
260
264
|
}
|
|
261
265
|
|
|
266
|
+
/**
|
|
267
|
+
* The world a map is drawn from, out of records and indexes a caller already
|
|
268
|
+
* holds: this package's places from its index records, and every
|
|
269
|
+
* dependency's from the fetched indexes. The site build reads its corpus and
|
|
270
|
+
* its foreign indexes once for every surface it publishes, and hands them
|
|
271
|
+
* here rather than walking the tree a second time.
|
|
272
|
+
*
|
|
273
|
+
* @param {object} opts
|
|
274
|
+
* @param {Array<Record<string, any>>} opts.records - Index records, as
|
|
275
|
+
* {@link module:engine/content-index.indexRecordsFor} returns them.
|
|
276
|
+
* @param {Map<string, object>} opts.foreignIndex - The fetched indexes, as
|
|
277
|
+
* {@link module:engine/metadata-index.loadForeignIndexes} returns `index`.
|
|
278
|
+
* @param {string} opts.contentBase - The content root the records were read from.
|
|
279
|
+
* @param {string} [opts.base] - The site base this package's pages are served
|
|
280
|
+
* under. Absent, no local place carries a URL.
|
|
281
|
+
* @param {object} opts.config - The resolved configuration.
|
|
282
|
+
* @returns {MapWorld} The world, with nothing stale: the caller has already
|
|
283
|
+
* judged its indexes usable.
|
|
284
|
+
*/
|
|
285
|
+
export function mapWorld({ records, foreignIndex, contentBase, base, config }) {
|
|
286
|
+
const world = placesFromRecords(records, { contentBase, base });
|
|
287
|
+
world.config = config;
|
|
288
|
+
addForeignPlaces(world, foreignIndex);
|
|
289
|
+
return world;
|
|
290
|
+
}
|
|
291
|
+
|
|
262
292
|
/* --------------------------------------------------------------------- */
|
|
263
293
|
/* Containment */
|
|
264
294
|
/* --------------------------------------------------------------------- */
|