@mark1russell7/highways 0.9.0 → 0.9.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/README.md +69 -1
- package/build/index.d.ts +420 -9
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4361 -2002
- package/build/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bench/compare.ts +13 -1
- package/src/bench/measures/curves.ts +55 -1
- package/src/bench/measures/groups.ts +71 -0
- package/src/bench/measures/index.ts +1 -0
- package/src/bench/measures/levels.ts +24 -0
- package/src/bench/measures/room.ts +106 -1
- package/src/bench/measures/symmetry.ts +114 -0
- package/src/bench/screen/exercise.ts +5 -0
- package/src/bench/screen/forms.ts +14 -3
- package/src/bench/screen/measures.ts +40 -3
- package/src/bench/screen/screen.ts +40 -5
- package/src/bench/suite.ts +10 -2
- package/src/highways/fixtures.test-util.ts +13 -0
- package/src/highways/follow.ts +3 -2
- package/src/highways/free-lines.ts +141 -0
- package/src/highways/gaps/index.ts +85 -0
- package/src/highways/index.ts +6 -0
- package/src/highways/interchanges/index.ts +231 -0
- package/src/highways/search/index.ts +170 -0
- package/src/highways/snap.ts +4 -2
- package/src/highways/steiner/index.ts +215 -0
- package/src/highways/streets/index.ts +78 -0
- package/src/model.ts +127 -4
- package/src/routers/attraction/index.ts +61 -0
- package/src/routers/corners.ts +15 -8
- package/src/routers/frames.ts +136 -0
- package/src/routers/index.ts +3 -1
- package/src/routers/plugs.ts +3 -2
- package/src/routers/sections.ts +3 -2
- package/src/routers/shared.ts +70 -7
- package/src/routers/straight.ts +3 -2
- package/src/routers/trunks.ts +3 -2
- package/src/strategies/builtins.ts +56 -1
- package/src/strategies/schemas.ts +69 -0
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import type { Graph, Result, Route } from "../../model.ts";
|
|
1
|
+
import type { Graph, Level, Result, Route } from "../../model.ts";
|
|
2
2
|
import { stableOrderStrategy } from "../../order/index.ts";
|
|
3
3
|
import { repairStrategy } from "../../repair/index.ts";
|
|
4
4
|
import { STRATEGIES } from "../../strategies/builtins.ts";
|
|
5
5
|
import { asRouter, createRegistry, getStrategy, listStrategies, register, STEPS, type AnyStrategy, type Registry, type Strategy } from "../../strategies/model.ts";
|
|
6
|
-
import { compareStep, highwayRouters, isRoutedStep, stepRouters, type CompareRow, type CompareStep, type RoutedStep, type StepComparison } from "../compare.ts";
|
|
6
|
+
import { compareStep, highwayRouters, isRoutedStep, stepRouters, wholeBoard, type CompareRow, type CompareStep, type RoutedStep, type StepComparison } from "../compare.ts";
|
|
7
7
|
import { bundledShare, bundleStretch, mergeAngle } from "../measures/bundles.ts";
|
|
8
8
|
import { highwayEdits } from "../measures/highways.ts";
|
|
9
9
|
import { laneArea, laneCrossings, laneFidelity, lanesThrough, occupiedArea, openLanes, orthogonality, routeSeparation, runProfile } from "../measures/lanes.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { farLevel, groupFill, itemOverlaps, silhouette } from "../measures/groups.ts";
|
|
11
|
+
import { levelRatio, linkAgreement } from "../measures/levels.ts";
|
|
11
12
|
import { orderFragility } from "../measures/order.ts";
|
|
12
13
|
import { paretoFront, type Objective } from "../measures/pareto.ts";
|
|
13
14
|
import { boardUnit } from "../measures/routes.ts";
|
|
15
|
+
import { boxDisplacement, distanceDistortion, roomOrderFaults } from "../measures/room.ts";
|
|
14
16
|
import { routeStability } from "../measures/stability.ts";
|
|
17
|
+
import { routeSymmetry } from "../measures/symmetry.ts";
|
|
15
18
|
import { quantile } from "../measures/stats.ts";
|
|
16
19
|
import { checkLevels, checkProperties, TOLERANCE } from "../properties.ts";
|
|
17
20
|
import { measureQuality } from "../quality.ts";
|
|
@@ -227,11 +230,30 @@ function screenRegistry(step: ScreenStep, strategies: readonly (Strategy<ScreenS
|
|
|
227
230
|
/** This function tells if a step of the screen gives routes. The step `router` gives routes, and each step of `RoutedStep` gives routes as a router. */
|
|
228
231
|
const givesRoutes = (step: ScreenStep): step is "router" | RoutedStep => step === "router" || isRoutedStep(step);
|
|
229
232
|
|
|
233
|
+
/** This function gives the measures of the shape of the groups and of the levels: G4, G5, G7 and Z5. A value without its data is not in the result. */
|
|
234
|
+
function groupShapeValues(graph: Graph, levels: readonly Level[]): Record<string, number> {
|
|
235
|
+
const values: Record<string, number> = {};
|
|
236
|
+
if (levels.length === 0) return values;
|
|
237
|
+
values["G5.overlaps"] = itemOverlaps(levels);
|
|
238
|
+
const far = farLevel(levels);
|
|
239
|
+
const fill = far && groupFill(graph, far);
|
|
240
|
+
if (fill !== undefined) values["G4.fill"] = fill;
|
|
241
|
+
const sil = far && silhouette(graph, far);
|
|
242
|
+
if (sil !== undefined) values["G7.silhouette"] = sil;
|
|
243
|
+
const ratio = levelRatio(levels);
|
|
244
|
+
if (ratio) {
|
|
245
|
+
values["Z5.max"] = ratio.max;
|
|
246
|
+
values["Z5.spread"] = ratio.spread;
|
|
247
|
+
}
|
|
248
|
+
return values;
|
|
249
|
+
}
|
|
250
|
+
|
|
230
251
|
/**
|
|
231
252
|
* This function gives the values of a row of `compareStep`, with the measures of `measureQuality` for a router.
|
|
232
253
|
* A router also gets the bundled share (B2), the merge angle (B6) and the bundle stretch (B3) against the routes `base` of `corners`.
|
|
233
254
|
* It also gets the measures of MEAS3A: R12, R14, B5, B7, B8, B9, M1 and M4. The lane measures open each shared segment at `MEASURE_LANE_WIDTH`.
|
|
234
|
-
* A result with zoom levels also gets the link agreement (Z9)
|
|
255
|
+
* A result with zoom levels also gets the link agreement (Z9) and `groupShapeValues`, for a router and for a grouping rule.
|
|
256
|
+
* A router also gets the symmetry of its routes (H3). A whole strategy also gets M2, M3 and M5 of the board of its output.
|
|
235
257
|
*/
|
|
236
258
|
function rowValues(step: ScreenStep, graph: Graph, row: CompareRow, base: () => readonly Route[]): Record<string, number> {
|
|
237
259
|
const values: Record<string, number> = { ...row.values };
|
|
@@ -265,10 +287,21 @@ function rowValues(step: ScreenStep, graph: Graph, row: CompareRow, base: () =>
|
|
|
265
287
|
}
|
|
266
288
|
values["M1.area"] = laneArea(graph, open);
|
|
267
289
|
values["M4.laneThrough"] = lanesThrough(graph, open);
|
|
290
|
+
const h3 = routeSymmetry(graph, routes);
|
|
291
|
+
if (h3 !== undefined) values["H3.share"] = h3;
|
|
268
292
|
}
|
|
269
293
|
if ((givesRoutes(step) || step === "group") && row.result?.levels) {
|
|
270
|
-
const
|
|
294
|
+
const levels = row.result.levels;
|
|
295
|
+
const la = linkAgreement(graph, levels, row.result.routes, boardUnit(graph));
|
|
271
296
|
if (la !== undefined) values["Z9.mean"] = la;
|
|
297
|
+
Object.assign(values, groupShapeValues(graph, levels));
|
|
298
|
+
}
|
|
299
|
+
// The step `whole` can move the boxes with a room strategy. M2, M3 and M5 compare the board of the output with the input board.
|
|
300
|
+
if (step === "whole" && row.result) {
|
|
301
|
+
const board = wholeBoard(row.result) ?? graph;
|
|
302
|
+
values["M2.order"] = roomOrderFaults(graph.nodes, board.nodes);
|
|
303
|
+
values["M3.displacement"] = boxDisplacement(graph.nodes, board.nodes, boardUnit(graph));
|
|
304
|
+
values["M5.stress"] = distanceDistortion(graph.nodes, board.nodes);
|
|
272
305
|
}
|
|
273
306
|
return values;
|
|
274
307
|
}
|
|
@@ -426,6 +459,8 @@ export function screen(step: ScreenStep, strategies: readonly (Strategy<ScreenSt
|
|
|
426
459
|
const values = rowValues(step, graph, row, cornersBase);
|
|
427
460
|
// Z2 is the count of the hard rule `link-ends`, as a measure. The steps `router`, `highway` and `group` check the rule.
|
|
428
461
|
if (hard["link-ends"] !== undefined) values["Z2.linkEnds"] = hard["link-ends"];
|
|
462
|
+
// Z1 is the count of the hard rule `counts-sum`, as a measure, the same way.
|
|
463
|
+
if (hard["counts-sum"] !== undefined) values["Z1.countsSum"] = hard["counts-sum"];
|
|
429
464
|
if (optima && withOptima.has(input.id) && row.result) Object.assign(values, gapValues(input, graph, row.result, cornersBase(), gapBase(), optima));
|
|
430
465
|
if (shuffles > 0 && row.result) {
|
|
431
466
|
const router = asRouter(strategy as Strategy<"router">, own[row.strategy]);
|
package/src/bench/suite.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { asRouter, listStrategies, STEPS, type LaneBoard, type Registry, type St
|
|
|
15
15
|
import { measureLevelFaults, sumFaults } from "./links.ts";
|
|
16
16
|
import { checkLevels } from "./properties.ts";
|
|
17
17
|
import { ambiguity, inkSaved } from "./measures/bundles.ts";
|
|
18
|
-
import { bendingEnergy, curveFaults, g1Defect, g2Defect } from "./measures/curves.ts";
|
|
18
|
+
import { bendingEnergy, curvatureVariation, curveDeviation, curveFaults, g1Defect, g2Defect } from "./measures/curves.ts";
|
|
19
19
|
import { balance, cutShare, farLevel, groupStability, levelGrouping, modularity, treeCost } from "./measures/groups.ts";
|
|
20
20
|
import { nestingFaults, orderStability, positionStability } from "./measures/levels.ts";
|
|
21
21
|
import { paretoFront, type Objective } from "./measures/pareto.ts";
|
|
@@ -328,7 +328,7 @@ export interface CurveRun {
|
|
|
328
328
|
readonly edges: readonly string[];
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
/** This function draws the routes of a result with each curve strategy and measures the curves (C1, C2, C3, C6 and the curve rules). */
|
|
331
|
+
/** This function draws the routes of a result with each curve strategy and measures the curves (C1, C2, C3, C4, C6, C8 and the curve rules). */
|
|
332
332
|
export function curveRuns(graph: Graph, result: Result, curves: readonly Strategy<"curve">[], options: CurveRunOptions): CurveRun[] {
|
|
333
333
|
const byId = nodeIndex(graph);
|
|
334
334
|
const edges = new Map(graph.edges.map((e) => [e.id, e]));
|
|
@@ -363,6 +363,8 @@ export function curveRuns(graph: Graph, result: Result, curves: readonly Strateg
|
|
|
363
363
|
const g1: number[] = [];
|
|
364
364
|
const g2: number[] = [];
|
|
365
365
|
const energy: number[] = [];
|
|
366
|
+
const variation: number[] = [];
|
|
367
|
+
const deviation: number[] = [];
|
|
366
368
|
const rules = { "curve-clear": 0, "curve-ends": 0, "curve-g1": 0, "curve-simple": 0 };
|
|
367
369
|
let length = 0;
|
|
368
370
|
drawn.forEach((segments, i) => {
|
|
@@ -371,6 +373,8 @@ export function curveRuns(graph: Graph, result: Result, curves: readonly Strateg
|
|
|
371
373
|
g1.push(g1Defect(segments));
|
|
372
374
|
g2.push(g2Defect(segments, u));
|
|
373
375
|
energy.push(bendingEnergy(s, 0.25));
|
|
376
|
+
variation.push(curvatureVariation(s, 0.25));
|
|
377
|
+
deviation.push(curveDeviation(s, r.points, u / 4) / u);
|
|
374
378
|
length += polylineLength(s);
|
|
375
379
|
const normals = options.normals.has(curve.name);
|
|
376
380
|
const f = curveFaults({
|
|
@@ -394,6 +398,10 @@ export function curveRuns(graph: Graph, result: Result, curves: readonly Strateg
|
|
|
394
398
|
"C2.max": g2.reduce((m, x) => Math.max(m, x), 0),
|
|
395
399
|
"C3.median": quantile(energy, 0.5) || 0,
|
|
396
400
|
"C3.q95": quantile(energy, 0.95) || 0,
|
|
401
|
+
"C4.median": quantile(variation, 0.5) || 0,
|
|
402
|
+
"C4.q95": quantile(variation, 0.95) || 0,
|
|
403
|
+
"C8.median": quantile(deviation, 0.5) || 0,
|
|
404
|
+
"C8.max": deviation.reduce((m, x) => Math.max(m, x), 0),
|
|
397
405
|
"C6.length": ink > 0 ? length / ink : 1,
|
|
398
406
|
...rules,
|
|
399
407
|
};
|
|
@@ -49,6 +49,19 @@ export const parallel: Graph = {
|
|
|
49
49
|
edges: [0, 1, 2].map((i) => ({ id: `e${i}`, source: { node: `l${i}` }, target: { node: `r${i}` } })),
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
/** Three rows of nodes on the left and on the right, with three edges between the rows. The gaps between the rows are 40 wide. */
|
|
53
|
+
export const rows: Graph = {
|
|
54
|
+
nodes: [0, 1, 2].flatMap((i) => [
|
|
55
|
+
{ id: `l${i}`, box: box(0, 60 * i) },
|
|
56
|
+
{ id: `r${i}`, box: box(600, 60 * i) },
|
|
57
|
+
]),
|
|
58
|
+
edges: [
|
|
59
|
+
{ id: "a", source: { node: "l0" }, target: { node: "r2" } },
|
|
60
|
+
{ id: "b", source: { node: "l1" }, target: { node: "r1" } },
|
|
61
|
+
{ id: "c", source: { node: "l2" }, target: { node: "r0" } },
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
52
65
|
/**
|
|
53
66
|
* This function adds the common tests of a highway strategy. The strategy is in the registry, and it gives a correct plan the same each time.
|
|
54
67
|
* Its routes break no hard rule after `follow`.
|
package/src/highways/follow.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { center, dist, pointBoxDistance, type Point } from "../geom.ts";
|
|
2
|
-
import { fixedPortPoint, nodeIndex, type End, type Graph, type Node, type Result, type Route, type SharedRun } from "../model.ts";
|
|
2
|
+
import { fixedPortPoint, nodeIndex, outerGraph, type End, type Graph, type Node, type Result, type Route, type SharedRun } from "../model.ts";
|
|
3
3
|
import { placePlug } from "../plugs.ts";
|
|
4
4
|
import { orderLanes, type RunOrder } from "../segments.ts";
|
|
5
5
|
import { WayFinder, type WayOptions } from "../ways.ts";
|
|
@@ -68,7 +68,8 @@ function slice(stops: Stops, on: number, off: number): Point[] {
|
|
|
68
68
|
* The result has a shared run for each stretch of a spine where the same two edges or more go. Each run holds the points of its routes in a row.
|
|
69
69
|
* The same graph, plan and options give the same result.
|
|
70
70
|
*/
|
|
71
|
-
export function follow(
|
|
71
|
+
export function follow(input: Graph, plan: HighwayPlan, options: FollowOptions = {}): Result {
|
|
72
|
+
const graph = outerGraph(input);
|
|
72
73
|
const byId = nodeIndex(graph);
|
|
73
74
|
const ways = new WayFinder(graph.nodes, options);
|
|
74
75
|
const base = new Map((options.routes?.routes ?? []).map((r) => [r.edge, r.points]));
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { dist, polylineLength, type Point } from "../geom.ts";
|
|
2
|
+
import type { Graph } from "../model.ts";
|
|
3
|
+
import { boardGaps } from "../room/index.ts";
|
|
4
|
+
import type { WayFinder } from "../ways.ts";
|
|
5
|
+
import { firstRoutes } from "./base.ts";
|
|
6
|
+
import type { HighwayInput } from "./model.ts";
|
|
7
|
+
import { edgePlugs } from "./stretches.ts";
|
|
8
|
+
|
|
9
|
+
/** The source of the candidate streets: the middle lines of the gaps of the room step, or the lines through the corners. */
|
|
10
|
+
export type StreetSource = "room" | "corners";
|
|
11
|
+
|
|
12
|
+
/** The values of `StreetSource`. */
|
|
13
|
+
export const STREET_SOURCES: readonly StreetSource[] = Object.freeze(["room", "corners"]);
|
|
14
|
+
|
|
15
|
+
/** One axis-parallel street in the free space, from `a` to `b`. */
|
|
16
|
+
export interface FreeLine {
|
|
17
|
+
readonly a: Point;
|
|
18
|
+
readonly b: Point;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** One edge for the streets: its two points out of its plugs, and the length of its first route. */
|
|
22
|
+
export interface StreetEdge {
|
|
23
|
+
readonly edge: string;
|
|
24
|
+
readonly s: Point;
|
|
25
|
+
readonly t: Point;
|
|
26
|
+
readonly base: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The ride of one edge on one street. It has the points where the edge joins and leaves the street, and two lengths. */
|
|
30
|
+
export interface StreetRide {
|
|
31
|
+
readonly on: Point;
|
|
32
|
+
readonly off: Point;
|
|
33
|
+
/** The length on the street. */
|
|
34
|
+
readonly ride: number;
|
|
35
|
+
/** The length of the two ramps, to the street and from it. */
|
|
36
|
+
readonly ramps: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const round = (v: number): number => Math.round(v * 2) / 2;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* This function gives the free pieces of the line `x = c` (axis `x`) or `y = c` (axis `y`). A box closer than `pad` to the line blocks it.
|
|
43
|
+
* A piece shorter than `min` is not in the list. The pieces run over the board and `2 pad` past it.
|
|
44
|
+
*/
|
|
45
|
+
function freePieces(finder: WayFinder, axis: "x" | "y", c: number, pad: number, min: number, lo: number, hi: number): FreeLine[] {
|
|
46
|
+
const blocks: [number, number][] = [];
|
|
47
|
+
const strip = axis === "x" ? { x: c - pad, y: lo, w: 2 * pad, h: hi - lo } : { x: lo, y: c - pad, w: hi - lo, h: 2 * pad };
|
|
48
|
+
finder.obstacles.query(strip, (n) => {
|
|
49
|
+
const a = axis === "x" ? n.box.x : n.box.y;
|
|
50
|
+
const w = axis === "x" ? n.box.w : n.box.h;
|
|
51
|
+
if (c <= a - pad || c >= a + w + pad) return;
|
|
52
|
+
const b = axis === "x" ? n.box.y : n.box.x;
|
|
53
|
+
const h = axis === "x" ? n.box.h : n.box.w;
|
|
54
|
+
blocks.push([b - pad, b + h + pad]);
|
|
55
|
+
});
|
|
56
|
+
blocks.sort((p, q) => p[0] - q[0] || p[1] - q[1]);
|
|
57
|
+
const out: FreeLine[] = [];
|
|
58
|
+
const at = (u: number): Point => (axis === "x" ? { x: c, y: u } : { x: u, y: c });
|
|
59
|
+
let from = lo;
|
|
60
|
+
for (const [s, e] of blocks) {
|
|
61
|
+
if (s - from >= min) out.push({ a: at(from), b: at(s) });
|
|
62
|
+
from = Math.max(from, e);
|
|
63
|
+
}
|
|
64
|
+
if (hi - from >= min) out.push({ a: at(from), b: at(hi) });
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* This function gives the candidate streets of a board. Each street is an axis-parallel piece of free space.
|
|
70
|
+
* The source `room` takes the middle line of each gap of `boardGaps` that is two margins wide or more.
|
|
71
|
+
* The source `corners` takes the lines one margin out from each side of each box.
|
|
72
|
+
* A street is two margins long or more. The list has no copies, and its order is the same each time.
|
|
73
|
+
*/
|
|
74
|
+
export function freeLines(graph: Graph, finder: WayFinder, source: StreetSource): FreeLine[] {
|
|
75
|
+
const nodes = graph.nodes;
|
|
76
|
+
if (nodes.length === 0) return [];
|
|
77
|
+
const m = finder.margin;
|
|
78
|
+
const pad = m / 2;
|
|
79
|
+
let x0 = Infinity;
|
|
80
|
+
let y0 = Infinity;
|
|
81
|
+
let x1 = -Infinity;
|
|
82
|
+
let y1 = -Infinity;
|
|
83
|
+
for (const n of nodes) {
|
|
84
|
+
x0 = Math.min(x0, n.box.x);
|
|
85
|
+
y0 = Math.min(y0, n.box.y);
|
|
86
|
+
x1 = Math.max(x1, n.box.x + n.box.w);
|
|
87
|
+
y1 = Math.max(y1, n.box.y + n.box.h);
|
|
88
|
+
}
|
|
89
|
+
const lines: { axis: "x" | "y"; c: number }[] = [];
|
|
90
|
+
if (source === "room") {
|
|
91
|
+
for (const g of boardGaps(nodes)) if (g.width >= 2 * m) lines.push({ axis: g.axis, c: round(g.at - g.width / 2) });
|
|
92
|
+
} else {
|
|
93
|
+
for (const n of nodes) {
|
|
94
|
+
lines.push({ axis: "x", c: round(n.box.x - m) }, { axis: "x", c: round(n.box.x + n.box.w + m) });
|
|
95
|
+
lines.push({ axis: "y", c: round(n.box.y - m) }, { axis: "y", c: round(n.box.y + n.box.h + m) });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const seen = new Set<string>();
|
|
99
|
+
const out: FreeLine[] = [];
|
|
100
|
+
const sorted = [...lines].sort((p, q) => (p.axis === q.axis ? p.c - q.c : p.axis < q.axis ? -1 : 1));
|
|
101
|
+
for (const l of sorted) {
|
|
102
|
+
const k = `${l.axis}${l.c}`;
|
|
103
|
+
if (seen.has(k)) continue;
|
|
104
|
+
seen.add(k);
|
|
105
|
+
const [lo, hi] = l.axis === "x" ? [y0 - 2 * m, y1 + 2 * m] : [x0 - 2 * m, x1 + 2 * m];
|
|
106
|
+
out.push(...freePieces(finder, l.axis, l.c, pad, 2 * m, lo, hi));
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** This function gives the edges of the input for the streets, with the points out of their plugs and the lengths of their first routes. */
|
|
112
|
+
export function streetEdges(input: HighwayInput, finder: WayFinder): StreetEdge[] {
|
|
113
|
+
const first = firstRoutes(input);
|
|
114
|
+
const out: StreetEdge[] = [];
|
|
115
|
+
for (const p of edgePlugs(input.graph)) {
|
|
116
|
+
const route = first.get(p.edge.id);
|
|
117
|
+
const s = finder.out(p.ps);
|
|
118
|
+
const t = finder.out(p.pt);
|
|
119
|
+
out.push({ edge: p.edge.id, s, t, base: route ? polylineLength(route) : dist(s, t) });
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** This function gives the point of the street nearest to `p`. */
|
|
125
|
+
export function streetPoint(line: FreeLine, p: Point): Point {
|
|
126
|
+
if (line.a.x === line.b.x) {
|
|
127
|
+
const lo = Math.min(line.a.y, line.b.y);
|
|
128
|
+
const hi = Math.max(line.a.y, line.b.y);
|
|
129
|
+
return { x: line.a.x, y: Math.min(hi, Math.max(lo, p.y)) };
|
|
130
|
+
}
|
|
131
|
+
const lo = Math.min(line.a.x, line.b.x);
|
|
132
|
+
const hi = Math.max(line.a.x, line.b.x);
|
|
133
|
+
return { x: Math.min(hi, Math.max(lo, p.x)), y: line.a.y };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** This function gives the ride of an edge on a street. The edge joins the street at the point nearest to `s`, and leaves it at the point nearest to `t`. */
|
|
137
|
+
export function streetRide(line: FreeLine, e: StreetEdge): StreetRide {
|
|
138
|
+
const on = streetPoint(line, e.s);
|
|
139
|
+
const off = streetPoint(line, e.t);
|
|
140
|
+
return { on, off, ride: dist(on, off), ramps: dist(e.s, on) + dist(off, e.t) };
|
|
141
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { WayFinder } from "../../ways.ts";
|
|
2
|
+
import { freeLines, streetEdges, streetRide, type FreeLine, type StreetEdge, type StreetSource } from "../free-lines.ts";
|
|
3
|
+
import type { HighwayInput, HighwayPlan } from "../model.ts";
|
|
4
|
+
import { snapPlan, type SnapLine } from "../snap.ts";
|
|
5
|
+
|
|
6
|
+
/** The options of the highway strategy `gaps` (HWY-057 of the note on highways). */
|
|
7
|
+
export interface GapOptions {
|
|
8
|
+
/** A street with fewer edges than this number (the demand `k`) is not a highway. The default is 3. */
|
|
9
|
+
readonly minSize: number;
|
|
10
|
+
/** The cost of a length on a street against a length off the streets, from 0.1 to 0.8. The default is 0.5. */
|
|
11
|
+
readonly discount: number;
|
|
12
|
+
/** An edge rides a street only when its way along the street is this many times its first route or less. The default is 1.3. */
|
|
13
|
+
readonly detour: number;
|
|
14
|
+
/** The source of the streets: the gaps of the room step, or the lines through the corners. The default is `room`. */
|
|
15
|
+
readonly source: StreetSource;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The default options of the strategy `gaps`. */
|
|
19
|
+
export const GAP_DEFAULTS: Readonly<GapOptions> = Object.freeze({ minSize: 3, discount: 0.5, detour: 1.3, source: "room" });
|
|
20
|
+
|
|
21
|
+
/** The best street of an edge, from the streets in `open`. The ride costs `discount` times its length. The way is at most `detour` times the first route. */
|
|
22
|
+
function bestStreet(lines: readonly FreeLine[], open: ReadonlySet<number>, e: StreetEdge, o: Readonly<GapOptions>, min: number): number {
|
|
23
|
+
let best = -1;
|
|
24
|
+
let bestSaving = 1e-9;
|
|
25
|
+
for (const i of open) {
|
|
26
|
+
const r = streetRide(lines[i]!, e);
|
|
27
|
+
if (r.ride < min) continue;
|
|
28
|
+
if (r.ramps + r.ride > o.detour * e.base) continue;
|
|
29
|
+
const saving = e.base - (r.ramps + o.discount * r.ride);
|
|
30
|
+
if (saving > bestSaving || (saving === bestSaving && i < best)) [best, bestSaving] = [i, saving];
|
|
31
|
+
}
|
|
32
|
+
return best;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* This function gives the highways on the gaps of the board (HWY-057). The streets are the free middle lines of the gaps.
|
|
37
|
+
* Each edge takes the street that saves it the most cost, when a length on a street costs `discount` times its length.
|
|
38
|
+
* A street with `minSize` edges or more becomes a highway. The edges of the other streets then choose again among the highways, two times.
|
|
39
|
+
*/
|
|
40
|
+
export function gapPlan(input: HighwayInput, o: Readonly<GapOptions>): HighwayPlan {
|
|
41
|
+
const finder = new WayFinder(input.graph.nodes);
|
|
42
|
+
const lines = freeLines(input.graph, finder, o.source);
|
|
43
|
+
const edges = streetEdges(input, finder);
|
|
44
|
+
const min = 2 * finder.margin;
|
|
45
|
+
const size = Math.max(2, Math.round(o.minSize));
|
|
46
|
+
let open = new Set(lines.map((_, i) => i));
|
|
47
|
+
let choice = new Map<string, number>();
|
|
48
|
+
for (let round = 0; round < 3; round++) {
|
|
49
|
+
choice = new Map();
|
|
50
|
+
const count = new Map<number, number>();
|
|
51
|
+
for (const e of edges) {
|
|
52
|
+
const i = bestStreet(lines, open, e, o, min);
|
|
53
|
+
if (i < 0) continue;
|
|
54
|
+
choice.set(e.edge, i);
|
|
55
|
+
count.set(i, (count.get(i) ?? 0) + 1);
|
|
56
|
+
}
|
|
57
|
+
const kept = new Set([...open].filter((i) => (count.get(i) ?? 0) >= size));
|
|
58
|
+
if (kept.size === open.size) break;
|
|
59
|
+
open = kept;
|
|
60
|
+
}
|
|
61
|
+
const byLine = new Map<number, SnapLine["members"][number][]>();
|
|
62
|
+
for (const e of edges) {
|
|
63
|
+
const i = choice.get(e.edge);
|
|
64
|
+
if (i === undefined || !open.has(i)) continue;
|
|
65
|
+
const r = streetRide(lines[i]!, e);
|
|
66
|
+
const list = byLine.get(i) ?? [];
|
|
67
|
+
list.push({ edge: e.edge, from: r.on, to: r.off, at: 0 });
|
|
68
|
+
byLine.set(i, list);
|
|
69
|
+
}
|
|
70
|
+
const snaps: SnapLine[] = [...byLine.keys()]
|
|
71
|
+
.sort((p, q) => p - q)
|
|
72
|
+
.map((i) => {
|
|
73
|
+
const members = byLine.get(i)!;
|
|
74
|
+
// The spine runs only over the part of the street that its members ride.
|
|
75
|
+
const line = lines[i]!;
|
|
76
|
+
const vertical = line.a.x === line.b.x;
|
|
77
|
+
const us = members.flatMap((m) => (vertical ? [m.from.y, m.to.y] : [m.from.x, m.to.x]));
|
|
78
|
+
const lo = Math.min(...us);
|
|
79
|
+
const hi = Math.max(...us);
|
|
80
|
+
const a = vertical ? { x: line.a.x, y: lo } : { x: lo, y: line.a.y };
|
|
81
|
+
const b = vertical ? { x: line.a.x, y: hi } : { x: hi, y: line.a.y };
|
|
82
|
+
return { key: `gap${i}`, a, b, members };
|
|
83
|
+
});
|
|
84
|
+
return snapPlan(input, snaps, { minSize: size, direct: true });
|
|
85
|
+
}
|
package/src/highways/index.ts
CHANGED
|
@@ -97,5 +97,11 @@ export { LINK_COMMUNITY_DEFAULTS, linkCommunities, linkCommunityGroups, linkComm
|
|
|
97
97
|
export { cellSteps, nearestCentre, pointGroups, TERRITORY_DEFAULTS, TERRITORY_POINTS, territoryPlan, type CellStep, type TerritoryOptions, type TerritoryPoints } from "./territories/index.ts";
|
|
98
98
|
export { coAssociation, CONSENSUS_DEFAULTS, CONSENSUS_LINKAGES, consensusGroups, consensusLabels, consensusPlan, type ConsensusLinkage, type ConsensusOptions } from "./consensus/index.ts";
|
|
99
99
|
export { HUB_ALLOCATIONS, HUB_CANDIDATES, hubCandidates, P_HUB_DEFAULTS, pHubPlan, pMedian, type HubAllocation, type HubCandidates, type PHubOptions } from "./p-hub/index.ts";
|
|
100
|
+
export { freeLines, STREET_SOURCES, streetEdges, streetPoint, streetRide, type FreeLine, type StreetEdge, type StreetRide, type StreetSource } from "./free-lines.ts";
|
|
101
|
+
export { oneSteiner, spanningTree, STEINER_DEFAULTS, STEINER_GROUPS, STEINER_METHODS, STEINER_METRICS, steinerPlan, type SteinerGroup, type SteinerMethod, type SteinerMetric, type SteinerOptions } from "./steiner/index.ts";
|
|
102
|
+
export { GAP_DEFAULTS, gapPlan, type GapOptions } from "./gaps/index.ts";
|
|
103
|
+
export { STREET_DEFAULTS, streetPlan, type StreetOptions } from "./streets/index.ts";
|
|
104
|
+
export { SEARCH_DEFAULTS, SEARCH_MOVES, searchPlan, searchScore, type SearchMove, type SearchOptions } from "./search/index.ts";
|
|
105
|
+
export { INTERCHANGE_BASES, INTERCHANGE_DEFAULTS, interchangeCandidates, interchangePlan, type Interchange, type InterchangeBase, type InterchangeOptions } from "./interchanges/index.ts";
|
|
100
106
|
export { SCALE_SPACE_DEFAULTS, scaleClusters, scaleSpaceGroups, scaleSpacePlan, type ScaleCluster, type ScaleSpaceOptions } from "./scale-space/index.ts";
|
|
101
107
|
export * from "./gates/index.ts";
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { boardUnit } from "../../bench/measures/routes.ts";
|
|
2
|
+
import { dist, polylineLength, type Point } from "../../geom.ts";
|
|
3
|
+
import { orderLanes } from "../../segments.ts";
|
|
4
|
+
import { WayFinder } from "../../ways.ts";
|
|
5
|
+
import { basePlan, firstRoutes, nearestPlace, tidyPlan } from "../base.ts";
|
|
6
|
+
import { lengthBetween, memberAt, pointAt, type Highway, type HighwayInput, type HighwayMember, type HighwayPlan } from "../model.ts";
|
|
7
|
+
import { spinePart } from "../snap.ts";
|
|
8
|
+
import { edgePlugs } from "../stretches.ts";
|
|
9
|
+
|
|
10
|
+
/** The plan that gives the highways of the network: the plan of `corridors` or of `shared-ways`, with their defaults. */
|
|
11
|
+
export type InterchangeBase = "corridors" | "shared-ways";
|
|
12
|
+
|
|
13
|
+
/** The values of `InterchangeBase`. */
|
|
14
|
+
export const INTERCHANGE_BASES: readonly InterchangeBase[] = Object.freeze(["corridors", "shared-ways"]);
|
|
15
|
+
|
|
16
|
+
/** The options of the highway strategy `interchanges` (HWY-060 of the note on highways). */
|
|
17
|
+
export interface InterchangeOptions {
|
|
18
|
+
/** The plan that gives the highways. The default is `corridors`. */
|
|
19
|
+
readonly from: InterchangeBase;
|
|
20
|
+
/** The cost of a change between two highways, in units of the board, from 0 to 8. The default is 2. */
|
|
21
|
+
readonly changeCost: number;
|
|
22
|
+
/** The largest number of interchanges, from 1 to 32. The default is 16. */
|
|
23
|
+
readonly interchanges: number;
|
|
24
|
+
/** The largest number of highways on the way of one edge, from 1 to 3. The default is 2. */
|
|
25
|
+
readonly perEdge: number;
|
|
26
|
+
/** Two highways get an interchange where they come this near, in units of the board. The default is 2. */
|
|
27
|
+
readonly radius: number;
|
|
28
|
+
/** The cost of a length on a highway against a length off the highways, from 0.1 to 1. The default is 0.5. */
|
|
29
|
+
readonly discount: number;
|
|
30
|
+
/** An edge rides the network only when its way is this many times its first route or less. The default is 1.5. */
|
|
31
|
+
readonly detour: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The default options of the strategy `interchanges`. */
|
|
35
|
+
export const INTERCHANGE_DEFAULTS: Readonly<InterchangeOptions> = Object.freeze({ from: "corridors", changeCost: 2, interchanges: 16, perEdge: 2, radius: 2, discount: 0.5, detour: 1.5 });
|
|
36
|
+
|
|
37
|
+
/** The largest number of candidate interchanges of the first pass. */
|
|
38
|
+
const MAX_CANDIDATES = 64;
|
|
39
|
+
|
|
40
|
+
/** One interchange: a place on the highway `h`, a place on the highway `g`, and the length of the link between the two places. */
|
|
41
|
+
export interface Interchange {
|
|
42
|
+
readonly h: number;
|
|
43
|
+
readonly hAt: number;
|
|
44
|
+
readonly g: number;
|
|
45
|
+
readonly gAt: number;
|
|
46
|
+
readonly gap: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The nearest points of the segments `a b` and `c d`, as the parameters `s` and `t` from 0 to 1, with their distance. */
|
|
50
|
+
function nearestOfSegments(a: Point, b: Point, c: Point, d: Point): { s: number; t: number; d: number } {
|
|
51
|
+
const ux = b.x - a.x;
|
|
52
|
+
const uy = b.y - a.y;
|
|
53
|
+
const vx = d.x - c.x;
|
|
54
|
+
const vy = d.y - c.y;
|
|
55
|
+
const at = (s: number, t: number): number => {
|
|
56
|
+
const dx = a.x + s * ux - (c.x + t * vx);
|
|
57
|
+
const dy = a.y + s * uy - (c.y + t * vy);
|
|
58
|
+
// `Math.sqrt` is exact on each platform. `Math.hypot` is not.
|
|
59
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
60
|
+
};
|
|
61
|
+
const clamp = (v: number): number => Math.min(1, Math.max(0, v));
|
|
62
|
+
const uu = ux * ux + uy * uy;
|
|
63
|
+
const vv = vx * vx + vy * vy;
|
|
64
|
+
const project = (p: Point, q: Point, x: number, y: number, len: number): number => (len > 0 ? clamp(((p.x - q.x) * x + (p.y - q.y) * y) / len) : 0);
|
|
65
|
+
// The four ends against the other segment, and the crossing point, give the nearest pair.
|
|
66
|
+
const tries: [number, number][] = [
|
|
67
|
+
[0, project(a, c, vx, vy, vv)],
|
|
68
|
+
[1, project(b, c, vx, vy, vv)],
|
|
69
|
+
[project(c, a, ux, uy, uu), 0],
|
|
70
|
+
[project(d, a, ux, uy, uu), 1],
|
|
71
|
+
];
|
|
72
|
+
const den = ux * vy - uy * vx;
|
|
73
|
+
if (Math.abs(den) > 1e-12) {
|
|
74
|
+
const s = ((c.x - a.x) * vy - (c.y - a.y) * vx) / den;
|
|
75
|
+
const t = ((c.x - a.x) * uy - (c.y - a.y) * ux) / den;
|
|
76
|
+
if (s >= 0 && s <= 1 && t >= 0 && t <= 1) tries.push([s, t]);
|
|
77
|
+
}
|
|
78
|
+
let best = { s: 0, t: 0, d: Infinity };
|
|
79
|
+
for (const [s, t] of tries) {
|
|
80
|
+
const v = at(s, t);
|
|
81
|
+
if (v < best.d - 1e-12) best = { s, t, d: v };
|
|
82
|
+
}
|
|
83
|
+
return best;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** This function gives the candidate interchanges: the nearest places of each pair of spines that come within `radius`, with a link in clear view. */
|
|
87
|
+
export function interchangeCandidates(spines: readonly (readonly Point[])[], radius: number, clear: (a: Point, b: Point) => boolean): Interchange[] {
|
|
88
|
+
const out: Interchange[] = [];
|
|
89
|
+
for (let h = 0; h < spines.length; h++) {
|
|
90
|
+
for (let g = h + 1; g < spines.length; g++) {
|
|
91
|
+
const p = spines[h]!;
|
|
92
|
+
const q = spines[g]!;
|
|
93
|
+
let best: Interchange | undefined;
|
|
94
|
+
for (let i = 0; i + 1 < p.length; i++) {
|
|
95
|
+
for (let j = 0; j + 1 < q.length; j++) {
|
|
96
|
+
const n = nearestOfSegments(p[i]!, p[i + 1]!, q[j]!, q[j + 1]!);
|
|
97
|
+
if (n.d <= radius && (!best || n.d < best.gap - 1e-12)) best = { h, hAt: i + n.s, g, gAt: j + n.t, gap: n.d };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (best && (best.gap <= 1e-9 || clear(pointAt(p, best.hAt), pointAt(q, best.gAt)))) out.push(best);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out.sort((x, y) => x.gap - y.gap || x.h - y.h || x.g - y.g).slice(0, MAX_CANDIDATES);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** One leg of the way of an edge: the highway and its two places. */
|
|
107
|
+
interface Leg {
|
|
108
|
+
readonly h: number;
|
|
109
|
+
readonly on: number;
|
|
110
|
+
readonly off: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The best way of one edge over the network, with the interchanges that it uses. */
|
|
114
|
+
interface Trip {
|
|
115
|
+
readonly legs: readonly Leg[];
|
|
116
|
+
readonly used: readonly number[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* This function gives the highways of a network with interchanges (HWY-060). The highways come from a plan. Two highways that come near get an interchange.
|
|
121
|
+
* Each edge takes its cheapest way: a ride on one highway, or on up to `perEdge` highways with a change at an interchange.
|
|
122
|
+
* A change costs `changeCost` units. The interchanges that the most edges use stay, up to `interchanges`.
|
|
123
|
+
*/
|
|
124
|
+
export function interchangePlan(input: HighwayInput, o: Readonly<InterchangeOptions>): HighwayPlan {
|
|
125
|
+
const graph = input.graph;
|
|
126
|
+
const base = basePlan(input, o.from);
|
|
127
|
+
const spines = base.highways.map((h) => h.spine);
|
|
128
|
+
if (spines.length === 0) return { highways: [] };
|
|
129
|
+
const finder = new WayFinder(graph.nodes);
|
|
130
|
+
const pad = finder.margin / 2;
|
|
131
|
+
const unit = boardUnit(graph);
|
|
132
|
+
const first = firstRoutes(input);
|
|
133
|
+
const change = Math.max(0, o.changeCost) * unit;
|
|
134
|
+
const perEdge = Math.min(3, Math.max(1, Math.round(o.perEdge)));
|
|
135
|
+
const disc = Math.min(1, Math.max(0.05, o.discount));
|
|
136
|
+
const ride = (h: number, a: number, b: number): number => lengthBetween(spines[h]!, Math.min(a, b), Math.max(a, b));
|
|
137
|
+
const edges = edgePlugs(graph).map((p) => {
|
|
138
|
+
const s = finder.out(p.ps);
|
|
139
|
+
const t = finder.out(p.pt);
|
|
140
|
+
const route = first.get(p.edge.id);
|
|
141
|
+
const ends = spines.map((spine) => {
|
|
142
|
+
const on = nearestPlace(spine, s);
|
|
143
|
+
const off = nearestPlace(spine, t);
|
|
144
|
+
return { on, off, rampIn: dist(s, pointAt(spine, on)), rampOut: dist(pointAt(spine, off), t) };
|
|
145
|
+
});
|
|
146
|
+
return { edge: p.edge.id, s, t, base: route ? polylineLength(route) : dist(s, t), ends };
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// The cheapest way of one edge over the interchanges `list`. The cost counts a ride at `disc` times its length. Each leg is one margin long or more.
|
|
150
|
+
const tripOf = (e: (typeof edges)[number], list: readonly Interchange[]): Trip | undefined => {
|
|
151
|
+
const found: { cost: number; trip: Trip | undefined } = { cost: Infinity, trip: undefined };
|
|
152
|
+
const offer = (cost: number, length: number, trip: Trip): void => {
|
|
153
|
+
if (trip.legs.some((l) => ride(l.h, l.on, l.off) < finder.margin)) return;
|
|
154
|
+
if (cost >= e.base || length > o.detour * e.base) return;
|
|
155
|
+
if (cost < found.cost - 1e-9) [found.cost, found.trip] = [cost, trip];
|
|
156
|
+
};
|
|
157
|
+
spines.forEach((_, h) => {
|
|
158
|
+
const x = e.ends[h]!;
|
|
159
|
+
const r = ride(h, x.on, x.off);
|
|
160
|
+
offer(x.rampIn + disc * r + x.rampOut, x.rampIn + r + x.rampOut, { legs: [{ h, on: x.on, off: x.off }], used: [] });
|
|
161
|
+
});
|
|
162
|
+
if (perEdge < 2) return found.trip;
|
|
163
|
+
// Each interchange, in both directions: on `p` at its place `pAt`, then off at the place `qAt` of `q`.
|
|
164
|
+
const sides = list.flatMap((c, i) => [
|
|
165
|
+
{ i, p: c.h, pAt: c.hAt, q: c.g, qAt: c.gAt, gap: c.gap },
|
|
166
|
+
{ i, p: c.g, pAt: c.gAt, q: c.h, qAt: c.hAt, gap: c.gap },
|
|
167
|
+
]);
|
|
168
|
+
for (const a of sides) {
|
|
169
|
+
const x = e.ends[a.p]!;
|
|
170
|
+
const y = e.ends[a.q]!;
|
|
171
|
+
const r1 = ride(a.p, x.on, a.pAt);
|
|
172
|
+
const r2 = ride(a.q, a.qAt, y.off);
|
|
173
|
+
offer(x.rampIn + disc * (r1 + r2) + a.gap + change + y.rampOut, x.rampIn + r1 + r2 + a.gap + y.rampOut, {
|
|
174
|
+
legs: [
|
|
175
|
+
{ h: a.p, on: x.on, off: a.pAt },
|
|
176
|
+
{ h: a.q, on: a.qAt, off: y.off },
|
|
177
|
+
],
|
|
178
|
+
used: [a.i],
|
|
179
|
+
});
|
|
180
|
+
if (perEdge < 3) continue;
|
|
181
|
+
for (const b of sides) {
|
|
182
|
+
if (b.p !== a.q || b.q === a.p || b.i === a.i) continue;
|
|
183
|
+
const z = e.ends[b.q]!;
|
|
184
|
+
const r3 = ride(a.q, a.qAt, b.pAt);
|
|
185
|
+
const r4 = ride(b.q, b.qAt, z.off);
|
|
186
|
+
offer(x.rampIn + disc * (r1 + r3 + r4) + a.gap + b.gap + 2 * change + z.rampOut, x.rampIn + r1 + r3 + r4 + a.gap + b.gap + z.rampOut, {
|
|
187
|
+
legs: [
|
|
188
|
+
{ h: a.p, on: x.on, off: a.pAt },
|
|
189
|
+
{ h: a.q, on: a.qAt, off: b.pAt },
|
|
190
|
+
{ h: b.q, on: b.qAt, off: z.off },
|
|
191
|
+
],
|
|
192
|
+
used: [a.i, b.i],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return found.trip;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const clear = (a: Point, b: Point): boolean => finder.obstacles.clear(a, b, pad);
|
|
200
|
+
const candidates = interchangeCandidates(spines, Math.max(0, o.radius) * unit, clear);
|
|
201
|
+
// The first pass counts the edges at each candidate. The busiest candidates stay for the second pass.
|
|
202
|
+
const use = new Array<number>(candidates.length).fill(0);
|
|
203
|
+
for (const e of edges) for (const i of tripOf(e, candidates)?.used ?? []) use[i] = use[i]! + 1;
|
|
204
|
+
const kept = candidates
|
|
205
|
+
.map((c, i) => ({ c, i, n: use[i]! }))
|
|
206
|
+
.filter((x) => x.n > 0)
|
|
207
|
+
.sort((x, y) => y.n - x.n || x.i - y.i)
|
|
208
|
+
.slice(0, Math.max(1, Math.round(o.interchanges)))
|
|
209
|
+
.sort((x, y) => x.i - y.i)
|
|
210
|
+
.map((x) => x.c);
|
|
211
|
+
|
|
212
|
+
const riders = spines.map(() => [] as { edge: string; on: number; off: number; seq: number; points: Point[] }[]);
|
|
213
|
+
for (const e of edges) {
|
|
214
|
+
const trip = tripOf(e, kept);
|
|
215
|
+
if (!trip) continue;
|
|
216
|
+
trip.legs.forEach((l, seq) => riders[l.h]!.push({ edge: e.edge, on: l.on, off: l.off, seq, points: [e.s, ...spinePart(spines[l.h]!, l.on, l.off), e.t] }));
|
|
217
|
+
}
|
|
218
|
+
const highways: Highway[] = [];
|
|
219
|
+
spines.forEach((spine, h) => {
|
|
220
|
+
const list = riders[h]!;
|
|
221
|
+
if (list.length < 2) return;
|
|
222
|
+
const lanes = orderLanes(spine, list.map((r) => ({ edge: r.edge, points: r.points })), "bundle");
|
|
223
|
+
const byEdge = new Map(list.map((r) => [r.edge, r]));
|
|
224
|
+
const members: HighwayMember[] = lanes.map((edge) => {
|
|
225
|
+
const r = byEdge.get(edge)!;
|
|
226
|
+
return memberAt(spine, edge, r.on, r.off, r.seq);
|
|
227
|
+
});
|
|
228
|
+
highways.push({ id: `h${h}`, key: base.highways[h]!.key, spine, lanes, members });
|
|
229
|
+
});
|
|
230
|
+
return tidyPlan({ highways });
|
|
231
|
+
}
|