@mark1russell7/highways 0.4.2 → 0.5.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 +18 -2
- package/build/index.d.ts +799 -68
- package/build/index.d.ts.map +1 -1
- package/build/index.js +6235 -2128
- package/build/index.js.map +1 -1
- package/package.json +3 -1
- package/src/bench/exact/index.ts +197 -0
- package/src/bench/measures/index.ts +1 -0
- package/src/bench/measures/lanes.ts +304 -0
- package/src/bench/measures/levels.ts +86 -2
- package/src/bench/screen/forms.ts +11 -5
- package/src/bench/screen/measures.ts +18 -0
- package/src/bench/screen/screen.ts +28 -0
- package/src/channels.ts +56 -0
- package/src/highways/diffusion/index.ts +103 -0
- package/src/highways/edge-paths/index.ts +155 -0
- package/src/highways/fixtures.test-util.ts +71 -0
- package/src/highways/flow-clusters/index.ts +143 -0
- package/src/highways/growth/index.ts +148 -0
- package/src/highways/hdbscan/index.ts +250 -0
- package/src/highways/hough/index.ts +182 -0
- package/src/highways/hub-arcs/index.ts +181 -0
- package/src/highways/index.ts +15 -0
- package/src/highways/lanes/index.ts +189 -0
- package/src/highways/mingle/index.ts +171 -0
- package/src/highways/nested/index.ts +143 -0
- package/src/highways/ridges/index.ts +328 -0
- package/src/highways/snap.ts +110 -0
- package/src/highways/soft-keys/index.ts +114 -0
- package/src/highways/spiral/index.ts +205 -0
- package/src/highways/traclus/index.ts +296 -0
- package/src/index.ts +1 -0
- package/src/routers/shared.ts +56 -3
- package/src/strategies/builtins.ts +155 -1
- package/src/strategies/schemas.ts +180 -0
- package/src/visibility.ts +5 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { dist, polylineLength, type Point } from "../../geom.ts";
|
|
2
|
+
import { orderLanes } from "../../segments.ts";
|
|
3
|
+
import { WayFinder } from "../../ways.ts";
|
|
4
|
+
import { firstRoutes, nearestPlace, tidyPlan } from "../base.ts";
|
|
5
|
+
import { lengthBetween, memberAt, pointAt, type Highway, type HighwayInput, type HighwayPlan } from "../model.ts";
|
|
6
|
+
import { candidatePlan, type CandidateSource } from "../select/index.ts";
|
|
7
|
+
import { spinePart } from "../snap.ts";
|
|
8
|
+
import { edgePlugs, sharedStretches } from "../stretches.ts";
|
|
9
|
+
|
|
10
|
+
/** A generator of candidate arcs: `runs` (the shared stretches of the first ways of all edges) or a generator of the strategy `select`. */
|
|
11
|
+
export type ArcSource = "runs" | CandidateSource;
|
|
12
|
+
|
|
13
|
+
/** The values of `ArcSource`. */
|
|
14
|
+
export const ARC_SOURCES: readonly ArcSource[] = Object.freeze(["runs", "corridors", "quickbundles", "wspd", "road-classes", "flow-field", "od-cells", "shared-ways"]);
|
|
15
|
+
|
|
16
|
+
/** The options of the highway strategy `hub-arcs` (HWY-042 of the note on highways). */
|
|
17
|
+
export interface HubArcOptions {
|
|
18
|
+
/** The largest number of arcs (highways). The default is 12. */
|
|
19
|
+
readonly q: number;
|
|
20
|
+
/** The cost of a length on an arc against a length off the arcs, from 0 to 1. The default is 0.5. */
|
|
21
|
+
readonly alpha: number;
|
|
22
|
+
/** An edge rides an arc only when its legs plus its part of the arc are this many times its first route or less. The default is 1.25. */
|
|
23
|
+
readonly stretch: number;
|
|
24
|
+
/** The number of passes of the local search with one swap. The default is 1. */
|
|
25
|
+
readonly swaps: number;
|
|
26
|
+
/** An arc with fewer edges than this number is not a highway. The default is 2. */
|
|
27
|
+
readonly minSize: number;
|
|
28
|
+
/** The generators of the candidate arcs. The default is `runs`, `corridors` and `road-classes`. */
|
|
29
|
+
readonly generators: readonly ArcSource[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The default options of the strategy `hub-arcs`. */
|
|
33
|
+
export const HUB_ARC_DEFAULTS: Readonly<HubArcOptions> = Object.freeze({
|
|
34
|
+
q: 12,
|
|
35
|
+
alpha: 0.5,
|
|
36
|
+
stretch: 1.25,
|
|
37
|
+
swaps: 1,
|
|
38
|
+
minSize: 2,
|
|
39
|
+
generators: Object.freeze(["runs", "corridors", "road-classes"] as ArcSource[]),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** The largest number of candidate arcs. The longest arcs stay. */
|
|
43
|
+
const MAX_ARCS = 400;
|
|
44
|
+
|
|
45
|
+
/** The place of one edge on one arc: where it joins and leaves, and the cost it saves against its first route. */
|
|
46
|
+
interface Ride {
|
|
47
|
+
readonly on: number;
|
|
48
|
+
readonly off: number;
|
|
49
|
+
readonly save: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** This function gives the candidate arcs: the spines of the plans of the generators, without the copies, with the longest first. */
|
|
53
|
+
export function candidateArcs(input: HighwayInput, generators: readonly ArcSource[]): Point[][] {
|
|
54
|
+
const spines: Point[][] = [];
|
|
55
|
+
for (const g of generators) {
|
|
56
|
+
const plan = g === "runs" ? runsPlan(input) : candidatePlan(input, g);
|
|
57
|
+
for (const h of plan.highways) spines.push([...h.spine]);
|
|
58
|
+
}
|
|
59
|
+
const seen = new Set<string>();
|
|
60
|
+
const out: Point[][] = [];
|
|
61
|
+
for (const s of spines) {
|
|
62
|
+
if (s.length < 2 || polylineLength(s) <= 1e-6) continue;
|
|
63
|
+
const key = s.map((p) => `${Math.round(p.x)},${Math.round(p.y)}`).join(";");
|
|
64
|
+
const back = [...s].reverse().map((p) => `${Math.round(p.x)},${Math.round(p.y)}`).join(";");
|
|
65
|
+
if (seen.has(key) || seen.has(back)) continue;
|
|
66
|
+
seen.add(key);
|
|
67
|
+
out.push(s);
|
|
68
|
+
}
|
|
69
|
+
out.sort((a, b) => polylineLength(b) - polylineLength(a));
|
|
70
|
+
return out.slice(0, MAX_ARCS);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** This function gives the shared stretches of the first ways of all edges, with no keys. */
|
|
74
|
+
function runsPlan(input: HighwayInput): HighwayPlan {
|
|
75
|
+
const finder = new WayFinder(input.graph.nodes);
|
|
76
|
+
const ways = edgePlugs(input.graph).map(({ edge, ps, pt }) => {
|
|
77
|
+
const a = finder.out(ps);
|
|
78
|
+
const b = finder.out(pt);
|
|
79
|
+
return { edge: edge.id, key: "", points: finder.corners.path(a, b) ?? [a, b] };
|
|
80
|
+
});
|
|
81
|
+
return sharedStretches(ways, { minSize: 2, pull: (p) => finder.corners.pull(p) });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* This function gives the highways of the hub arc location (Campbell, Ernst and Krishnamoorthy 2005). It chooses at most `q` arcs from the candidates.
|
|
86
|
+
* An edge on an arc costs its two legs plus `alpha` times its part of the arc. An edge off the arcs costs its first route.
|
|
87
|
+
* The greedy step adds the arc that saves the most cost. Then each swap pass changes one chosen arc for one other arc while the saving grows.
|
|
88
|
+
* Each edge rides the arc that saves the most for it.
|
|
89
|
+
*/
|
|
90
|
+
export function hubArcPlan(input: HighwayInput, o: Readonly<HubArcOptions>): HighwayPlan {
|
|
91
|
+
const margin = 5;
|
|
92
|
+
const routes = firstRoutes(input);
|
|
93
|
+
const plugs = edgePlugs(input.graph);
|
|
94
|
+
const arcs = candidateArcs(input, o.generators);
|
|
95
|
+
const q = Math.max(0, Math.round(o.q));
|
|
96
|
+
if (q === 0 || arcs.length === 0) return { highways: [] };
|
|
97
|
+
// The saving of each edge on each arc. A saving of 0 means that the edge does not ride the arc.
|
|
98
|
+
const rides: (Ride | undefined)[][] = arcs.map((spine) =>
|
|
99
|
+
plugs.map(({ edge, ps, pt }) => {
|
|
100
|
+
const first = routes.get(edge.id);
|
|
101
|
+
const base = first ? polylineLength(first) : dist(ps.point, pt.point);
|
|
102
|
+
const on = nearestPlace(spine, ps.point);
|
|
103
|
+
const off = nearestPlace(spine, pt.point);
|
|
104
|
+
const along = lengthBetween(spine, Math.min(on, off), Math.max(on, off));
|
|
105
|
+
if (along < margin) return undefined;
|
|
106
|
+
const legs = dist(ps.point, pointAt(spine, on)) + dist(pointAt(spine, off), pt.point);
|
|
107
|
+
if (legs + along > o.stretch * base) return undefined;
|
|
108
|
+
const save = base - (legs + o.alpha * along);
|
|
109
|
+
return save > 0 ? { on, off, save } : undefined;
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
const n = plugs.length;
|
|
113
|
+
const total = (chosen: readonly number[]): number => {
|
|
114
|
+
let sum = 0;
|
|
115
|
+
for (let e = 0; e < n; e++) {
|
|
116
|
+
let best = 0;
|
|
117
|
+
for (const a of chosen) best = Math.max(best, rides[a]![e]?.save ?? 0);
|
|
118
|
+
sum += best;
|
|
119
|
+
}
|
|
120
|
+
return sum;
|
|
121
|
+
};
|
|
122
|
+
const chosen: number[] = [];
|
|
123
|
+
while (chosen.length < q) {
|
|
124
|
+
const now = total(chosen);
|
|
125
|
+
let pick = -1;
|
|
126
|
+
let gain = 1e-9;
|
|
127
|
+
for (let a = 0; a < arcs.length; a++) {
|
|
128
|
+
if (chosen.includes(a)) continue;
|
|
129
|
+
const g = total([...chosen, a]) - now;
|
|
130
|
+
if (g > gain) [gain, pick] = [g, a];
|
|
131
|
+
}
|
|
132
|
+
if (pick < 0) break;
|
|
133
|
+
chosen.push(pick);
|
|
134
|
+
}
|
|
135
|
+
for (let pass = 0; pass < Math.round(o.swaps); pass++) {
|
|
136
|
+
let better = false;
|
|
137
|
+
for (let i = 0; i < chosen.length; i++) {
|
|
138
|
+
const now = total(chosen);
|
|
139
|
+
let pick = -1;
|
|
140
|
+
let gain = 1e-9;
|
|
141
|
+
for (let a = 0; a < arcs.length; a++) {
|
|
142
|
+
if (chosen.includes(a)) continue;
|
|
143
|
+
const next = chosen.map((x, j) => (j === i ? a : x));
|
|
144
|
+
const g = total(next) - now;
|
|
145
|
+
if (g > gain) [gain, pick] = [g, a];
|
|
146
|
+
}
|
|
147
|
+
if (pick >= 0) {
|
|
148
|
+
chosen[i] = pick;
|
|
149
|
+
better = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!better) break;
|
|
153
|
+
}
|
|
154
|
+
// Each edge rides its best arc.
|
|
155
|
+
const riders = new Map<number, { edge: string; ride: Ride; points: Point[] }[]>();
|
|
156
|
+
plugs.forEach(({ edge, ps, pt }, e) => {
|
|
157
|
+
let best = -1;
|
|
158
|
+
for (const a of chosen) if ((rides[a]![e]?.save ?? 0) > (best < 0 ? 0 : rides[best]![e]!.save)) best = a;
|
|
159
|
+
if (best < 0) return;
|
|
160
|
+
const ride = rides[best]![e]!;
|
|
161
|
+
const list = riders.get(best) ?? [];
|
|
162
|
+
list.push({ edge: edge.id, ride, points: [ps.point, ...spinePart(arcs[best]!, ride.on, ride.off), pt.point] });
|
|
163
|
+
riders.set(best, list);
|
|
164
|
+
});
|
|
165
|
+
const highways: Highway[] = [];
|
|
166
|
+
for (const a of [...chosen].sort((x, y) => x - y)) {
|
|
167
|
+
const list = riders.get(a) ?? [];
|
|
168
|
+
if (list.length < Math.max(2, o.minSize)) continue;
|
|
169
|
+
const spine = arcs[a]!;
|
|
170
|
+
const lanes = orderLanes(spine, list.map((r) => ({ edge: r.edge, points: r.points })), "bundle");
|
|
171
|
+
const byEdge = new Map(list.map((r) => [r.edge, r.ride]));
|
|
172
|
+
highways.push({
|
|
173
|
+
id: `h${highways.length}`,
|
|
174
|
+
key: `a${a}`,
|
|
175
|
+
spine,
|
|
176
|
+
lanes,
|
|
177
|
+
members: lanes.map((edge) => memberAt(spine, edge, byEdge.get(edge)!.on, byEdge.get(edge)!.off)),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return tidyPlan({ highways });
|
|
181
|
+
}
|
package/src/highways/index.ts
CHANGED
|
@@ -19,4 +19,19 @@ export { FLOW_FIELD_DEFAULTS, flowFieldPlan, type FieldEnd, type FlowFieldOption
|
|
|
19
19
|
export { SplitTree, WSPD_DEFAULTS, wspdGroups, wspdPlan, type SplitCell, type WspdOptions } from "./wspd/index.ts";
|
|
20
20
|
export { OD_CELL_DEFAULTS, odCellPlan, odGroups, type OdCellOptions } from "./od-cells/index.ts";
|
|
21
21
|
export { CANDIDATE_SOURCES, candidatePlan, candidateScore, SELECT_DEFAULTS, selectPlan, type CandidateSource, type SelectOptions } from "./select/index.ts";
|
|
22
|
+
export { MINGLE_DEFAULTS, mingleBundles, minglePlan, type MingleOptions } from "./mingle/index.ts";
|
|
23
|
+
export { SPIRAL_DEFAULTS, spiralJoin, spiralPlan, spiralTree, type SpiralNode, type SpiralOptions } from "./spiral/index.ts";
|
|
24
|
+
export { FLOW_CLUSTER_DEFAULTS, flowClusterPlan, flowClusters, flowDistance, chainClusters, LINKAGES, type FlowClusterOptions, type Linkage } from "./flow-clusters/index.ts";
|
|
25
|
+
export { characteristicPoints, representative, segmentClusters, segmentDistance, TRACLUS_DEFAULTS, traclusPlan, type RouteSegment, type TraclusOptions } from "./traclus/index.ts";
|
|
26
|
+
export { DIFFUSION_DEFAULTS, diffusionPlan, linkDensity, type DiffusionOptions } from "./diffusion/index.ts";
|
|
27
|
+
export { NESTED_DEFAULTS, nestedGroups, nestedPlan, nestingViolations, type NestedOptions } from "./nested/index.ts";
|
|
28
|
+
export { channelOverflows, LANES_DEFAULTS, lanesPlan, type LanesOptions } from "./lanes/index.ts";
|
|
29
|
+
export { GROWTH_DEFAULTS, growthPlan, type GrowthOptions, type GrowthOrder } from "./growth/index.ts";
|
|
30
|
+
export { EDGE_PATH_DEFAULTS, edgePathPlan, edgePaths, type EdgePathOptions, type PathStep } from "./edge-paths/index.ts";
|
|
31
|
+
export { SOFT_KEY_DEFAULTS, softDistance, softKeyClusters, softKeyPlan, type SoftKeyOptions } from "./soft-keys/index.ts";
|
|
32
|
+
export { linePoint, snapPlan, spinePart, type SnapLine, type SnapMember, type SnapOptions } from "./snap.ts";
|
|
33
|
+
export { HOUGH_DEFAULTS, houghLines, houghPeaks, houghPlan, type HoughOptions, type HoughPeak } from "./hough/index.ts";
|
|
34
|
+
export { densityRidges, lineDensity, RIDGE_DEFAULTS, ridgeLines, ridgePlan, type DensityImage, type Ridge, type RidgeOptions, type RidgeSource } from "./ridges/index.ts";
|
|
35
|
+
export { compatibility, EDGE_DISTANCES, HDBSCAN_DEFAULTS, hdbscanClusters, hdbscanLabels, hdbscanPlan, type EdgeDistance, type HdbscanOptions } from "./hdbscan/index.ts";
|
|
36
|
+
export { ARC_SOURCES, candidateArcs, HUB_ARC_DEFAULTS, hubArcPlan, type ArcSource, type HubArcOptions } from "./hub-arcs/index.ts";
|
|
22
37
|
export * from "./gates/index.ts";
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { channelWidth } from "../../channels.ts";
|
|
2
|
+
import { dist, polylineLength, type Point } from "../../geom.ts";
|
|
3
|
+
import type { Graph } from "../../model.ts";
|
|
4
|
+
import { WayFinder } from "../../ways.ts";
|
|
5
|
+
import { basePlan, nearestPlace, tidyPlan, type HighwayBase } from "../base.ts";
|
|
6
|
+
import { memberAt, pointAt, type Highway, type HighwayInput, type HighwayMember, type HighwayPlan } from "../model.ts";
|
|
7
|
+
|
|
8
|
+
/** The options of the highway strategy `lanes` (HWY-051 of the note on highways). */
|
|
9
|
+
export interface LanesOptions {
|
|
10
|
+
/** The distance from a box to the corners of the graph. The default is 5. */
|
|
11
|
+
readonly margin?: number;
|
|
12
|
+
/** The number of visible neighbours of each corner. The default is 8. */
|
|
13
|
+
readonly k?: number;
|
|
14
|
+
/** The plan that the strategy changes. The default is `shared-ways`. */
|
|
15
|
+
readonly from: HighwayBase;
|
|
16
|
+
/** The gap between two lanes. The default is 4. */
|
|
17
|
+
readonly laneGap: number;
|
|
18
|
+
/** The largest width of a highway. The default is 32. */
|
|
19
|
+
readonly maxWidth: number;
|
|
20
|
+
/** The number of times that a highway can split, one half after the other. The default is 3. */
|
|
21
|
+
readonly depth: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The default options of the strategy `lanes`. */
|
|
25
|
+
export const LANES_DEFAULTS: Readonly<LanesOptions> = Object.freeze({ from: "shared-ways", laneGap: 4, maxWidth: 32, depth: 3 });
|
|
26
|
+
|
|
27
|
+
/** The cost factor of a link that is too narrow for the lanes, for the way of the second half. */
|
|
28
|
+
const NARROW_COST = 8;
|
|
29
|
+
|
|
30
|
+
/** This function gives the number of lanes of a highway on each leg of its spine. A lane is a member whose part on the spine holds the leg. */
|
|
31
|
+
function lanesOnLegs(h: Highway): number[] {
|
|
32
|
+
const legs = new Array<number>(Math.max(0, h.spine.length - 1)).fill(0);
|
|
33
|
+
for (const m of h.members) {
|
|
34
|
+
const lo = Math.min(m.on, m.off);
|
|
35
|
+
const hi = Math.max(m.on, m.off);
|
|
36
|
+
for (let i = 0; i < legs.length; i++) if (lo < i + 1 && hi > i) legs[i]!++;
|
|
37
|
+
}
|
|
38
|
+
return legs;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** This function gives the width of `n` lanes. */
|
|
42
|
+
const widthOf = (n: number, o: Readonly<LanesOptions>): number => Math.min(o.maxWidth, n * o.laneGap);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* This function gives the number of legs of the spines of a plan where the lanes do not fit in the channel.
|
|
46
|
+
* A leg with two lanes or more needs `min(maxWidth, lanes × laneGap)`. The channel is the free space across the leg (`channelWidth`).
|
|
47
|
+
*/
|
|
48
|
+
export function channelOverflows(graph: Graph, plan: HighwayPlan, o: Readonly<Pick<LanesOptions, "laneGap" | "maxWidth" | "margin" | "k">>): number {
|
|
49
|
+
const finder = new WayFinder(graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
|
|
50
|
+
const full = { from: "none" as const, depth: 0, ...o };
|
|
51
|
+
let n = 0;
|
|
52
|
+
for (const h of plan.highways) {
|
|
53
|
+
const legs = lanesOnLegs(h);
|
|
54
|
+
legs.forEach((count, i) => {
|
|
55
|
+
if (count > 1 && channelWidth(finder.obstacles, h.spine[i]!, h.spine[i + 1]!) < widthOf(count, full)) n++;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return n;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* This function gives the plan of `from`, with each highway that does not fit in its channels split in two (HWY-051).
|
|
63
|
+
* The lanes split into two halves by lane order. The first half keeps the spine. The second half gets a new spine through the corners, where the narrow links cost more.
|
|
64
|
+
* A highway with fewer than four lanes moves as one to the new spine.
|
|
65
|
+
* A half that still does not fit splits again, up to `depth` times. A new spine needs a shorter length of narrow legs than the old spine.
|
|
66
|
+
*/
|
|
67
|
+
export function lanesPlan(input: HighwayInput, o: Readonly<LanesOptions>): HighwayPlan {
|
|
68
|
+
const plan = basePlan(input, o.from);
|
|
69
|
+
const finder = new WayFinder(input.graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
|
|
70
|
+
const corners = finder.corners;
|
|
71
|
+
const widths = new Map<string, number>();
|
|
72
|
+
const channel = (a: Point, b: Point): number => {
|
|
73
|
+
const key = a.x < b.x || (a.x === b.x && a.y <= b.y) ? `${a.x},${a.y},${b.x},${b.y}` : `${b.x},${b.y},${a.x},${a.y}`;
|
|
74
|
+
let w = widths.get(key);
|
|
75
|
+
if (w === undefined) widths.set(key, (w = channelWidth(finder.obstacles, a, b)));
|
|
76
|
+
return w;
|
|
77
|
+
};
|
|
78
|
+
const narrowLegs = (h: Highway): number[] => {
|
|
79
|
+
const out: number[] = [];
|
|
80
|
+
lanesOnLegs(h).forEach((count, i) => {
|
|
81
|
+
if (count > 1 && channel(h.spine[i]!, h.spine[i + 1]!) < widthOf(count, o)) out.push(i);
|
|
82
|
+
});
|
|
83
|
+
return out;
|
|
84
|
+
};
|
|
85
|
+
// The length of the legs of a spine that are too narrow for `need`.
|
|
86
|
+
const tight = (spine: readonly Point[], need: number): number => spine.reduce((n, p, i) => (i > 0 && channel(spine[i - 1]!, p) < need ? n + dist(spine[i - 1]!, p) : n), 0);
|
|
87
|
+
|
|
88
|
+
const pad = finder.margin / 2;
|
|
89
|
+
const reach = 2 * (o.k ?? 8);
|
|
90
|
+
/**
|
|
91
|
+
* This function gives the cheapest way from `from` to `to` through the corners (Dijkstra). A link narrower than `need` costs `NARROW_COST` times its length.
|
|
92
|
+
* Unlike `corners.path`, it also prices the first link and the last link, and it does not take the straight line without a price.
|
|
93
|
+
*/
|
|
94
|
+
const detour = (from: Point, to: Point, need: number): Point[] | undefined => {
|
|
95
|
+
const pts: Point[] = [from, to];
|
|
96
|
+
const id = (j: number): number => j + 2;
|
|
97
|
+
const pointOf = (n: number): Point => (n < 2 ? pts[n]! : corners.points[n - 2]!);
|
|
98
|
+
const best = new Map<number, number>([[0, 0]]);
|
|
99
|
+
const parent = new Map<number, number>();
|
|
100
|
+
const done = new Set<number>();
|
|
101
|
+
const heap: [number, number][] = [[0, 0]];
|
|
102
|
+
const push = (c: number, n: number): void => {
|
|
103
|
+
heap.push([c, n]);
|
|
104
|
+
for (let i = heap.length - 1; i > 0; ) {
|
|
105
|
+
const up = (i - 1) >> 1;
|
|
106
|
+
if (heap[up]![0] <= heap[i]![0]) break;
|
|
107
|
+
[heap[up], heap[i]] = [heap[i]!, heap[up]!];
|
|
108
|
+
i = up;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const pop = (): [number, number] => {
|
|
112
|
+
const top = heap[0]!;
|
|
113
|
+
const last = heap.pop()!;
|
|
114
|
+
if (heap.length > 0) {
|
|
115
|
+
heap[0] = last;
|
|
116
|
+
for (let i = 0; ; ) {
|
|
117
|
+
const l = 2 * i + 1;
|
|
118
|
+
const r = l + 1;
|
|
119
|
+
let m = i;
|
|
120
|
+
if (l < heap.length && heap[l]![0] < heap[m]![0]) m = l;
|
|
121
|
+
if (r < heap.length && heap[r]![0] < heap[m]![0]) m = r;
|
|
122
|
+
if (m === i) break;
|
|
123
|
+
[heap[m], heap[i]] = [heap[i]!, heap[m]!];
|
|
124
|
+
i = m;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return top;
|
|
128
|
+
};
|
|
129
|
+
while (heap.length > 0 && done.size < 20_000) {
|
|
130
|
+
const [c, n] = pop();
|
|
131
|
+
if (done.has(n)) continue;
|
|
132
|
+
done.add(n);
|
|
133
|
+
if (n === 1) break;
|
|
134
|
+
const p = pointOf(n);
|
|
135
|
+
const next = corners.nearestVisible(p, reach, n >= 2 ? n - 2 : -1).map(id);
|
|
136
|
+
if (finder.obstacles.clear(p, to, pad)) next.push(1);
|
|
137
|
+
for (const m of next) {
|
|
138
|
+
if (done.has(m)) continue;
|
|
139
|
+
const q = pointOf(m);
|
|
140
|
+
const d = dist(p, q);
|
|
141
|
+
const g = c + (channel(p, q) < need ? d * NARROW_COST : d);
|
|
142
|
+
if (g < (best.get(m) ?? Infinity)) {
|
|
143
|
+
best.set(m, g);
|
|
144
|
+
parent.set(m, n);
|
|
145
|
+
push(g, m);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!done.has(1)) return undefined;
|
|
150
|
+
const out: Point[] = [];
|
|
151
|
+
for (let n: number | undefined = 1; n !== undefined; n = parent.get(n)) out.push(pointOf(n));
|
|
152
|
+
return out.reverse();
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// This function moves the members to a new spine. A member whose two places meet on the new spine leaves the highway.
|
|
156
|
+
const move = (h: Highway, spine: readonly Point[], edges: readonly string[]): Highway => {
|
|
157
|
+
const byEdge = new Map(h.members.map((m) => [m.edge, m]));
|
|
158
|
+
const members: HighwayMember[] = [];
|
|
159
|
+
for (const edge of edges) {
|
|
160
|
+
const m = byEdge.get(edge)!;
|
|
161
|
+
const on = nearestPlace(spine, pointAt(h.spine, m.on));
|
|
162
|
+
const off = nearestPlace(spine, pointAt(h.spine, m.off));
|
|
163
|
+
if (Math.abs(on - off) < 1e-6) continue;
|
|
164
|
+
members.push({ ...memberAt(spine, edge, on, off), seq: m.seq });
|
|
165
|
+
}
|
|
166
|
+
return { ...h, spine, lanes: members.map((m) => m.edge), members };
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const split = (h: Highway, depth: number): Highway[] => {
|
|
170
|
+
if (depth <= 0 || narrowLegs(h).length === 0) return [h];
|
|
171
|
+
// A highway with fewer than four lanes cannot give two halves of two lanes. All its lanes move to the new spine.
|
|
172
|
+
const half = h.lanes.length < 4 ? 0 : Math.ceil(h.lanes.length / 2);
|
|
173
|
+
const first = h.lanes.slice(0, half);
|
|
174
|
+
const second = h.lanes.slice(half);
|
|
175
|
+
const need = widthOf(second.length, o);
|
|
176
|
+
const from = h.spine[0]!;
|
|
177
|
+
const to = h.spine[h.spine.length - 1]!;
|
|
178
|
+
const raw = detour(from, to, need);
|
|
179
|
+
if (!raw) return [h];
|
|
180
|
+
const pulled = corners.pull(raw);
|
|
181
|
+
const spine = tight(pulled, need) <= tight(raw, need) ? pulled : raw;
|
|
182
|
+
if (spine.length < 2 || polylineLength(spine) <= finder.margin || tight(spine, need) >= tight(h.spine, need)) return [h];
|
|
183
|
+
const moved = move(h, spine, second);
|
|
184
|
+
if (first.length === 0) return split(moved, depth - 1);
|
|
185
|
+
return [...split(move(h, h.spine, first), depth - 1), ...split(moved, depth - 1)];
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
return tidyPlan({ highways: plan.highways.flatMap((h) => split(h, Math.max(0, Math.round(o.depth)))) });
|
|
189
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { dist, type Point } from "../../geom.ts";
|
|
2
|
+
import { groupedPlan, type EdgeGroup, type GroupSpine } from "../grouped.ts";
|
|
3
|
+
import type { HighwayInput, HighwayPlan } from "../model.ts";
|
|
4
|
+
import { edgePlugs } from "../stretches.ts";
|
|
5
|
+
import { meetingPoints } from "../weiszfeld/index.ts";
|
|
6
|
+
|
|
7
|
+
/** The options of the highway strategy `mingle` (HWY-003 of the note on highways). */
|
|
8
|
+
export interface MingleOptions {
|
|
9
|
+
/** Each bundle looks for a merge among this number of nearest bundles in 4D. The default is 8. */
|
|
10
|
+
readonly k: number;
|
|
11
|
+
/** The largest turn of an edge at a meeting point, in degrees. A larger turn moves the meeting point along the trunk. The default is 40. */
|
|
12
|
+
readonly maxTurn: number;
|
|
13
|
+
/** The number of levels of merges. The default is 6. */
|
|
14
|
+
readonly levels: number;
|
|
15
|
+
/** The weight of the trunk in the ink. With 1, the trunk costs its length one time. The default is 1. */
|
|
16
|
+
readonly weight: number;
|
|
17
|
+
/** A bundle with this number of edges or more gets highways. The default is 2. */
|
|
18
|
+
readonly minSize: number;
|
|
19
|
+
/** The spine of each bundle. The default is `trunk`: one trunk between the two meeting points. */
|
|
20
|
+
readonly spine: GroupSpine;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The default options of the strategy `mingle`. */
|
|
24
|
+
export const MINGLE_DEFAULTS: Readonly<MingleOptions> = Object.freeze({ k: 8, maxTurn: 40, levels: 6, weight: 1, minSize: 2, spine: "trunk" });
|
|
25
|
+
|
|
26
|
+
/** One bundle of MINGLE: its edges with their direction, the ends on each side, the two meeting points and the ink. */
|
|
27
|
+
interface Bundle {
|
|
28
|
+
readonly edges: readonly { readonly edge: string; readonly flip: boolean }[];
|
|
29
|
+
readonly a: readonly Point[];
|
|
30
|
+
readonly b: readonly Point[];
|
|
31
|
+
readonly m1: Point;
|
|
32
|
+
readonly m2: Point;
|
|
33
|
+
readonly ink: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The number of steps of Weiszfeld for the meeting points of a bundle. */
|
|
37
|
+
const STEPS = 12;
|
|
38
|
+
|
|
39
|
+
/** This function gives the turn in degrees from the direction `p` to `q` to the direction `q` to `r`. A leg of length 0 gives no turn. */
|
|
40
|
+
function turnAt(p: Point, q: Point, r: Point): number {
|
|
41
|
+
const ux = q.x - p.x;
|
|
42
|
+
const uy = q.y - p.y;
|
|
43
|
+
const vx = r.x - q.x;
|
|
44
|
+
const vy = r.y - q.y;
|
|
45
|
+
if (Math.hypot(ux, uy) < 1e-9 || Math.hypot(vx, vy) < 1e-9) return 0;
|
|
46
|
+
return (Math.abs(Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy)) * 180) / Math.PI;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The number of steps of the bisection that moves a meeting point to the turn limit. */
|
|
50
|
+
const SLIDE_STEPS = 24;
|
|
51
|
+
|
|
52
|
+
/** This function gives the point at the share `t` of the way from `p` to `q`. */
|
|
53
|
+
function lerp(p: Point, q: Point, t: number): Point {
|
|
54
|
+
return { x: p.x + t * (q.x - p.x), y: p.y + t * (q.y - p.y) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* This function moves the meeting point `m` along the trunk to the meeting point `far`. It stops when each end of `ends` turns at most `maxTurn` degrees at `m`.
|
|
59
|
+
* The turn of each end gets smaller when `m` moves to `far`, so a bisection finds the smallest move. The point stays at most at the middle of the trunk.
|
|
60
|
+
* The function gives `undefined` when the middle of the trunk does not obey the limit.
|
|
61
|
+
*/
|
|
62
|
+
function slide(m: Point, far: Point, ends: readonly Point[], maxTurn: number): Point | undefined {
|
|
63
|
+
const worst = (p: Point): number => Math.max(...ends.map((e) => turnAt(e, p, far)));
|
|
64
|
+
if (worst(m) <= maxTurn) return m;
|
|
65
|
+
if (worst(lerp(m, far, 0.5)) > maxTurn) return undefined;
|
|
66
|
+
let lo = 0;
|
|
67
|
+
let hi = 0.5;
|
|
68
|
+
for (let s = 0; s < SLIDE_STEPS; s++) {
|
|
69
|
+
const mid = (lo + hi) / 2;
|
|
70
|
+
if (worst(lerp(m, far, mid)) <= maxTurn) hi = mid;
|
|
71
|
+
else lo = mid;
|
|
72
|
+
}
|
|
73
|
+
return lerp(m, far, hi);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* This function gives the bundle of the ends `a` and `b`, with its meeting points and its ink. A turn that is too large gives `undefined`.
|
|
78
|
+
* The ink is `Σ |a_i - M1| + w |M1 - M2| + Σ |M2 - b_i|`. Each edge turns at most `maxTurn` degrees at `M1` and at `M2`.
|
|
79
|
+
* As in MINGLE, a meeting point with a larger turn moves along the trunk to the other meeting point, until the turns obey the limit.
|
|
80
|
+
*/
|
|
81
|
+
function bundleOf(edges: Bundle["edges"], a: readonly Point[], b: readonly Point[], o: Readonly<MingleOptions>): Bundle | undefined {
|
|
82
|
+
const [w1, w2] = meetingPoints(a, b, o.weight, STEPS);
|
|
83
|
+
if (dist(w1, w2) < 1e-6) return undefined;
|
|
84
|
+
const m1 = slide(w1, w2, a, o.maxTurn);
|
|
85
|
+
const m2 = slide(w2, w1, b, o.maxTurn);
|
|
86
|
+
if (!m1 || !m2) return undefined;
|
|
87
|
+
let ink = o.weight * dist(m1, m2);
|
|
88
|
+
for (let i = 0; i < a.length; i++) ink += dist(a[i]!, m1) + dist(m2, b[i]!);
|
|
89
|
+
return { edges, a, b, m1, m2, ink };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** This function gives the bundle of `p` and `q`, with `q` turned around when `flip` is `true`. */
|
|
93
|
+
function merged(p: Bundle, q: Bundle, flip: boolean, o: Readonly<MingleOptions>): Bundle | undefined {
|
|
94
|
+
const edges = [...p.edges, ...q.edges.map((e) => ({ edge: e.edge, flip: e.flip !== flip }))];
|
|
95
|
+
return bundleOf(edges, [...p.a, ...(flip ? q.b : q.a)], [...p.b, ...(flip ? q.a : q.b)], o);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** This function gives the 4D distance of two bundles by their meeting points, and `flip` when the opposite direction is nearer. */
|
|
99
|
+
function distance4(p: Bundle, q: Bundle): { d: number; flip: boolean } {
|
|
100
|
+
const direct = Math.hypot(dist(p.m1, q.m1), dist(p.m2, q.m2));
|
|
101
|
+
const back = Math.hypot(dist(p.m1, q.m2), dist(p.m2, q.m1));
|
|
102
|
+
return back < direct ? { d: back, flip: true } : { d: direct, flip: false };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* This function gives the bundles of MINGLE (Gansner, Hu, North and Scheidegger 2011) as groups of edges.
|
|
107
|
+
* Each edge is a point in 4D: its two plugs. At each level, each bundle examines its `k` nearest bundles in 4D.
|
|
108
|
+
* A merge saves ink when the ink of the joint bundle is less than the sum of the two inks. The merges with the largest gain go first.
|
|
109
|
+
* A bundle merges one time at most in a level. The levels stop after `levels` levels, or when no merge saves ink.
|
|
110
|
+
*/
|
|
111
|
+
export function mingleBundles(input: HighwayInput, o: Readonly<MingleOptions>): Map<string, EdgeGroup> {
|
|
112
|
+
let bundles: Bundle[] = edgePlugs(input.graph).map(({ edge, ps, pt }) => ({
|
|
113
|
+
edges: [{ edge: edge.id, flip: false }],
|
|
114
|
+
a: [ps.point],
|
|
115
|
+
b: [pt.point],
|
|
116
|
+
m1: ps.point,
|
|
117
|
+
m2: pt.point,
|
|
118
|
+
ink: dist(ps.point, pt.point),
|
|
119
|
+
}));
|
|
120
|
+
const k = Math.max(1, Math.round(o.k));
|
|
121
|
+
for (let level = 0; level < Math.round(o.levels); level++) {
|
|
122
|
+
const offers: { i: number; j: number; gain: number; joint: Bundle }[] = [];
|
|
123
|
+
const tried = new Set<number>();
|
|
124
|
+
const n = bundles.length;
|
|
125
|
+
bundles.forEach((p, i) => {
|
|
126
|
+
const near = bundles
|
|
127
|
+
.map((q, j) => ({ j, ...distance4(p, q) }))
|
|
128
|
+
.filter((x) => x.j !== i)
|
|
129
|
+
.sort((x, y) => x.d - y.d || x.j - y.j)
|
|
130
|
+
.slice(0, k);
|
|
131
|
+
for (const { j, flip } of near) {
|
|
132
|
+
const lo = Math.min(i, j);
|
|
133
|
+
const hi = Math.max(i, j);
|
|
134
|
+
if (tried.has(lo * n + hi)) continue;
|
|
135
|
+
tried.add(lo * n + hi);
|
|
136
|
+
const joint = merged(bundles[lo]!, bundles[hi]!, flip, o);
|
|
137
|
+
if (!joint) continue;
|
|
138
|
+
const gain = bundles[lo]!.ink + bundles[hi]!.ink - joint.ink;
|
|
139
|
+
if (gain > 1e-9) offers.push({ i: lo, j: hi, gain, joint });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
if (offers.length === 0) break;
|
|
143
|
+
offers.sort((x, y) => y.gain - x.gain || x.i - y.i || x.j - y.j);
|
|
144
|
+
const used = new Set<number>();
|
|
145
|
+
const next: Bundle[] = [];
|
|
146
|
+
for (const x of offers) {
|
|
147
|
+
if (used.has(x.i) || used.has(x.j)) continue;
|
|
148
|
+
used.add(x.i);
|
|
149
|
+
used.add(x.j);
|
|
150
|
+
next.push(x.joint);
|
|
151
|
+
}
|
|
152
|
+
bundles.forEach((p, i) => {
|
|
153
|
+
if (!used.has(i)) next.push(p);
|
|
154
|
+
});
|
|
155
|
+
bundles = next;
|
|
156
|
+
}
|
|
157
|
+
const groups = new Map<string, EdgeGroup>();
|
|
158
|
+
bundles.forEach((p, i) => {
|
|
159
|
+
for (const e of p.edges) groups.set(e.edge, { key: `m${i}`, flip: e.flip });
|
|
160
|
+
});
|
|
161
|
+
return groups;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* This function gives the highways of MINGLE (HWY-003). The bundles of `mingleBundles` with `minSize` edges or more get highways.
|
|
166
|
+
* With the spine `trunk`, each bundle gets one trunk between its two meeting points, through the corner graph.
|
|
167
|
+
* With `shared`, the edges of each bundle share their ways through the corner graph.
|
|
168
|
+
*/
|
|
169
|
+
export function minglePlan(input: HighwayInput, o: Readonly<MingleOptions>): HighwayPlan {
|
|
170
|
+
return groupedPlan(input, mingleBundles(input, o), { spine: o.spine, minSize: o.minSize });
|
|
171
|
+
}
|