@mark1russell7/highways 0.4.1 → 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 +43 -1
- package/build/index.d.ts +1377 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +8202 -1515
- package/build/index.js.map +1 -1
- package/package.json +3 -1
- package/src/bench/exact/index.ts +197 -0
- package/src/bench/measures/highways.ts +93 -0
- package/src/bench/measures/index.ts +2 -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 +23 -0
- package/src/bench/screen/screen.ts +42 -0
- package/src/channels.ts +56 -0
- package/src/highways/base.ts +126 -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/flow-field/index.ts +49 -0
- package/src/highways/gates/ambiguity/index.ts +110 -0
- package/src/highways/gates/breakeven/index.ts +62 -0
- package/src/highways/gates/hysteresis/index.ts +48 -0
- package/src/highways/gates/ids/index.ts +59 -0
- package/src/highways/gates/index.ts +6 -0
- package/src/highways/gates/model.ts +129 -0
- package/src/highways/gates/stretch/index.ts +39 -0
- package/src/highways/grouped.ts +123 -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 +30 -0
- package/src/highways/lanes/index.ts +189 -0
- package/src/highways/level-links/index.ts +114 -0
- package/src/highways/meet/index.ts +84 -0
- package/src/highways/mingle/index.ts +171 -0
- package/src/highways/model.ts +5 -0
- package/src/highways/nested/index.ts +143 -0
- package/src/highways/od-cells/index.ts +50 -0
- package/src/highways/quickbundles/index.ts +117 -0
- package/src/highways/ridges/index.ts +328 -0
- package/src/highways/road-classes/index.ts +74 -0
- package/src/highways/router.ts +1 -1
- package/src/highways/select/index.ts +162 -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/stretches.ts +141 -0
- package/src/highways/stubs/index.ts +117 -0
- package/src/highways/traclus/index.ts +296 -0
- package/src/highways/weiszfeld/index.ts +105 -0
- package/src/highways/wspd/index.ts +130 -0
- package/src/highways/zipper/index.ts +168 -0
- package/src/index.ts +1 -0
- package/src/routers/shared.ts +56 -3
- package/src/strategies/builtins.ts +303 -7
- package/src/strategies/schemas.ts +300 -0
- package/src/visibility.ts +5 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Highway, HighwayInput, HighwayPlan } from "../../model.ts";
|
|
2
|
+
import { commonEdges, matchHighways, type Gate } from "../model.ts";
|
|
3
|
+
|
|
4
|
+
/** The options of the gate `ids` (HWY-054). */
|
|
5
|
+
export interface StableIdOptions {
|
|
6
|
+
/** A new highway takes the id of an old highway when the Jaccard index of their edges is this value or more. The default is 0.5. */
|
|
7
|
+
readonly minJaccard: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** The default options of the gate `ids`. */
|
|
11
|
+
export const STABLE_ID_DEFAULTS: Readonly<StableIdOptions> = Object.freeze({ minJaccard: 0.5 });
|
|
12
|
+
|
|
13
|
+
/** This function gives the number at the end of an id, or -1 for an id without a number at its end. */
|
|
14
|
+
function numberOf(id: string): number {
|
|
15
|
+
let start = id.length;
|
|
16
|
+
while (start > 0 && id.charCodeAt(start - 1) >= 48 && id.charCodeAt(start - 1) <= 57) start--;
|
|
17
|
+
return start === id.length ? -1 : Number(id.slice(start));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* This function gives stable ids to the highways of a plan (HWY-054). The earlier plan is `input.previous`.
|
|
22
|
+
* Each new highway takes the id of the old highway with the most common edges.
|
|
23
|
+
* The pairs with the largest Jaccard index of their edges come first.
|
|
24
|
+
*
|
|
25
|
+
* A pair needs an index of `minJaccard` or more, and only the edges of both graphs count.
|
|
26
|
+
* A new highway without a match gets a new id `h<n>` that no old highway and no new highway has. Thus the old highway of a new id ends.
|
|
27
|
+
* The gate changes only the ids, so the routes do not change. Without an earlier plan, the plan does not change.
|
|
28
|
+
*/
|
|
29
|
+
export function stableIdPlan(input: HighwayInput, plan: HighwayPlan, options: Readonly<StableIdOptions>): HighwayPlan {
|
|
30
|
+
if (!input.previous) return plan;
|
|
31
|
+
const before = input.previous.plan;
|
|
32
|
+
const matched = matchHighways(before, plan, options.minJaccard, commonEdges(input.previous.graph, input.graph));
|
|
33
|
+
const taken = new Set<string>([...before.highways.map((h) => h.id)]);
|
|
34
|
+
for (const [, i0] of matched) taken.add(before.highways[i0]!.id);
|
|
35
|
+
let next = 1 + Math.max(-1, ...before.highways.map((h) => numberOf(h.id)), ...plan.highways.map((h) => numberOf(h.id)));
|
|
36
|
+
const fresh = (): string => {
|
|
37
|
+
while (taken.has(`h${next}`)) next++;
|
|
38
|
+
const id = `h${next++}`;
|
|
39
|
+
taken.add(id);
|
|
40
|
+
return id;
|
|
41
|
+
};
|
|
42
|
+
const highways = plan.highways.map((h, i): Highway => {
|
|
43
|
+
const i0 = matched.get(i);
|
|
44
|
+
const id = i0 === undefined ? fresh() : before.highways[i0]!.id;
|
|
45
|
+
return id === h.id ? h : { ...h, id };
|
|
46
|
+
});
|
|
47
|
+
return { highways };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The gate `ids` (HWY-054): stable highway ids across edits. A highway keeps the id of its match in the earlier plan. */
|
|
51
|
+
export const stableIdGate: Gate<StableIdOptions> = {
|
|
52
|
+
name: "ids",
|
|
53
|
+
describe: "The gate `ids` (HWY-054) gives each highway the id of the highway of the earlier plan with the most common edges. The routes do not change.",
|
|
54
|
+
defaults: STABLE_ID_DEFAULTS,
|
|
55
|
+
schema: {
|
|
56
|
+
minJaccard: { type: "number", min: 0.3, max: 0.8, step: 0.05, default: 0.5, describe: "A new highway takes the id of an old highway when the Jaccard index of their edges is this value or more." },
|
|
57
|
+
},
|
|
58
|
+
apply: (input, plan, options) => stableIdPlan(input, plan, options),
|
|
59
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { commonEdges, edgeLengths, gateStrategy, jaccard, keepMembers, matchHighways, type EdgeLengths, type Gate } from "./model.ts";
|
|
2
|
+
export { BREAK_EVEN_DEFAULTS, breakEvenGate, breakEvenPlan, type BreakEvenOptions } from "./breakeven/index.ts";
|
|
3
|
+
export { STRETCH_DEFAULTS, stretchGate, stretchPlan, type StretchOptions } from "./stretch/index.ts";
|
|
4
|
+
export { AMBIGUITY_GUARD_DEFAULTS, ambiguityGate, ambiguityPlan, nodeAmbiguity, type AmbiguityGuardOptions } from "./ambiguity/index.ts";
|
|
5
|
+
export { HYSTERESIS_DEFAULTS, hysteresisGate, hysteresisPlan, SAME_HIGHWAY, type HysteresisOptions } from "./hysteresis/index.ts";
|
|
6
|
+
export { STABLE_ID_DEFAULTS, stableIdGate, stableIdPlan, type StableIdOptions } from "./ids/index.ts";
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { polylineLength } from "../../geom.ts";
|
|
2
|
+
import type { Graph } from "../../model.ts";
|
|
3
|
+
import { defineStrategy, type Strategy } from "../../strategies/model.ts";
|
|
4
|
+
import type { OptionSchema } from "../../strategies/options.ts";
|
|
5
|
+
import type { WayOptions } from "../../ways.ts";
|
|
6
|
+
import { follow } from "../follow.ts";
|
|
7
|
+
import type { Highway, HighwayInput, HighwayMember, HighwayPlan } from "../model.ts";
|
|
8
|
+
import { wayOptionsOf } from "../router.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A gate: a rule that keeps, drops or changes the highways of a plan. `gateStrategy` puts a gate after a highway strategy.
|
|
12
|
+
* A gate does not move a spine. It takes members out of a highway, drops a highway or changes the id of a highway.
|
|
13
|
+
*/
|
|
14
|
+
export interface Gate<G extends object> {
|
|
15
|
+
/** The name of the gate. The strategy of a gate has the name `<strategy>+<gate>`. */
|
|
16
|
+
readonly name: string;
|
|
17
|
+
/** One or two sentences that tell what the gate does. */
|
|
18
|
+
readonly describe: string;
|
|
19
|
+
/** The values of the options of the gate. Their keys must not be keys of the options of the strategy before the gate. */
|
|
20
|
+
readonly defaults: G;
|
|
21
|
+
readonly schema: OptionSchema<G>;
|
|
22
|
+
/** This function gives the plan after the gate. `way` holds the options of the way finder of the strategy. */
|
|
23
|
+
apply(input: HighwayInput, plan: HighwayPlan, options: Readonly<G>, way: WayOptions): HighwayPlan;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* This function puts a gate after a highway strategy. The new strategy has the name `<strategy>+<gate>`, and the options of both.
|
|
28
|
+
* The strategy gets its own options, and the gate gets the plan of the strategy and the options of the gate.
|
|
29
|
+
* Thus a gate can follow each plan, for example `corridors+breakeven` or `shared-ways+stretch`. Gates can follow gates.
|
|
30
|
+
*/
|
|
31
|
+
export function gateStrategy<O extends object, G extends object>(strategy: Strategy<"highway", O>, gate: Gate<G>): Strategy<"highway", O & G> {
|
|
32
|
+
const keys = Object.keys(gate.defaults);
|
|
33
|
+
for (const k of keys) if (k in strategy.defaults) throw new Error(`The gate "${gate.name}" has the option "${k}", and the strategy "${strategy.name}" has it too.`);
|
|
34
|
+
return defineStrategy<"highway", O & G>({
|
|
35
|
+
step: "highway",
|
|
36
|
+
name: `${strategy.name}+${gate.name}`,
|
|
37
|
+
describe: `${strategy.describe} ${gate.describe}`,
|
|
38
|
+
defaults: { ...strategy.defaults, ...gate.defaults } as O & G,
|
|
39
|
+
schema: { ...(strategy.schema ?? {}), ...gate.schema } as OptionSchema<O & G>,
|
|
40
|
+
...(strategy.maxNodes === undefined ? {} : { maxNodes: strategy.maxNodes }),
|
|
41
|
+
run: (input: HighwayInput, options: Readonly<O & G>): HighwayPlan => {
|
|
42
|
+
const own: Record<string, unknown> = {};
|
|
43
|
+
const mine: Record<string, unknown> = {};
|
|
44
|
+
for (const [k, v] of Object.entries(options)) (keys.includes(k) ? mine : own)[k] = v;
|
|
45
|
+
return gate.apply(input, strategy.run(input, own as Partial<O>), mine as G, wayOptionsOf(options));
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* This function gives the plan with only the members that `keep` accepts. A highway with fewer than two members goes away.
|
|
52
|
+
* The lanes keep their order, and each highway keeps its id, its key and its spine.
|
|
53
|
+
*/
|
|
54
|
+
export function keepMembers(plan: HighwayPlan, keep: (h: Highway, m: HighwayMember) => boolean): HighwayPlan {
|
|
55
|
+
const highways: Highway[] = [];
|
|
56
|
+
for (const h of plan.highways) {
|
|
57
|
+
const members = h.members.filter((m) => keep(h, m));
|
|
58
|
+
if (members.length < 2) continue;
|
|
59
|
+
if (members.length === h.members.length) {
|
|
60
|
+
highways.push(h);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const stay = new Set(members.map((m) => m.edge));
|
|
64
|
+
highways.push({ ...h, lanes: h.lanes.filter((e) => stay.has(e)), members });
|
|
65
|
+
}
|
|
66
|
+
return { highways };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The lengths of the route of one edge of a plan. */
|
|
70
|
+
export interface EdgeLengths {
|
|
71
|
+
/** The length of the route along the plan, after `follow`. */
|
|
72
|
+
readonly route: number;
|
|
73
|
+
/** The length of the route without a highway: the route of the router `corners`. */
|
|
74
|
+
readonly alone: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* This function gives the length of the route of each edge of a plan, along the plan and alone. `follow` routes each edge of a plan on its own.
|
|
79
|
+
* Thus the route of an edge does not change when another edge leaves the plan.
|
|
80
|
+
*/
|
|
81
|
+
export function edgeLengths(graph: Graph, plan: HighwayPlan, way: WayOptions): Map<string, EdgeLengths> {
|
|
82
|
+
const edges = new Set(plan.highways.flatMap((h) => h.members.map((m) => m.edge)));
|
|
83
|
+
const sub: Graph = { nodes: graph.nodes, edges: graph.edges.filter((e) => edges.has(e.id)) };
|
|
84
|
+
const along = follow(sub, plan, way);
|
|
85
|
+
const alone = follow(sub, { highways: [] }, way);
|
|
86
|
+
const lengthOf = new Map(alone.routes.map((r) => [r.edge, polylineLength(r.points)]));
|
|
87
|
+
return new Map(along.routes.map((r) => [r.edge, { route: polylineLength(r.points), alone: lengthOf.get(r.edge) ?? 0 }]));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** This function gives the ids of the edges that both graphs have. */
|
|
91
|
+
export function commonEdges(a: Graph, b: Graph): Set<string> {
|
|
92
|
+
const ids = new Set(b.edges.map((e) => e.id));
|
|
93
|
+
return new Set(a.edges.map((e) => e.id).filter((id) => ids.has(id)));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** This function gives the Jaccard index of two sets of edges: the common edges over all edges. Two empty sets give 0. */
|
|
97
|
+
export function jaccard(a: ReadonlySet<string>, b: ReadonlySet<string>): number {
|
|
98
|
+
let common = 0;
|
|
99
|
+
for (const x of a) if (b.has(x)) common++;
|
|
100
|
+
const all = a.size + b.size - common;
|
|
101
|
+
return all === 0 ? 0 : common / all;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* This function matches the highways of a plan with the highways of an earlier plan. It pairs the highways with the largest Jaccard index of their edges first.
|
|
106
|
+
* A pair needs an index of `min` or more. Each highway is in one pair at most. The result maps the index of a new highway to the index of its old highway.
|
|
107
|
+
* Only the edges of `edges` count, when this set is present: for example the edges of both graphs.
|
|
108
|
+
*/
|
|
109
|
+
export function matchHighways(before: HighwayPlan, after: HighwayPlan, min: number, edges?: ReadonlySet<string>): Map<number, number> {
|
|
110
|
+
const setOf = (h: Highway): Set<string> => new Set(h.members.map((m) => m.edge).filter((e) => edges?.has(e) ?? true));
|
|
111
|
+
const old = before.highways.map(setOf);
|
|
112
|
+
const now = after.highways.map(setOf);
|
|
113
|
+
const pairs: [j: number, i: number, i0: number][] = [];
|
|
114
|
+
now.forEach((a, i) =>
|
|
115
|
+
old.forEach((b, i0) => {
|
|
116
|
+
const j = jaccard(a, b);
|
|
117
|
+
if (j >= min && j > 0) pairs.push([j, i, i0]);
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
pairs.sort((p, q) => q[0] - p[0] || p[1] - q[1] || p[2] - q[2]);
|
|
121
|
+
const out = new Map<number, number>();
|
|
122
|
+
const used = new Set<number>();
|
|
123
|
+
for (const [, i, i0] of pairs) {
|
|
124
|
+
if (out.has(i) || used.has(i0)) continue;
|
|
125
|
+
out.set(i, i0);
|
|
126
|
+
used.add(i0);
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { WayOptions } from "../../../ways.ts";
|
|
2
|
+
import type { HighwayInput, HighwayPlan } from "../../model.ts";
|
|
3
|
+
import { edgeLengths, keepMembers, type Gate } from "../model.ts";
|
|
4
|
+
|
|
5
|
+
/** The options of the gate `stretch` (HWY-048). */
|
|
6
|
+
export interface StretchOptions {
|
|
7
|
+
/** An edge stays on its highways when its route is at most this number times its route alone, plus `pad`. The default is 1.2. */
|
|
8
|
+
readonly budget: number;
|
|
9
|
+
/** The added length that each edge can use, in units of the board. The default is 0. */
|
|
10
|
+
readonly pad: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The default options of the gate `stretch`. */
|
|
14
|
+
export const STRETCH_DEFAULTS: Readonly<StretchOptions> = Object.freeze({ budget: 1.2, pad: 0 });
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* This function applies the stretch budget of HWY-048 to a plan: the join test of each edge.
|
|
18
|
+
* An edge stays on its highways only when its route along the plan is at most `budget` times its route alone, plus `pad`.
|
|
19
|
+
* The route alone is the route of the router `corners`. An edge that fails leaves each of its highways and goes alone, so the highways do not bend for it.
|
|
20
|
+
* `follow` routes each edge on its own, so the test of one edge does not change with the other edges. One pass is enough.
|
|
21
|
+
*/
|
|
22
|
+
export function stretchPlan(input: HighwayInput, plan: HighwayPlan, options: Readonly<StretchOptions>, way: WayOptions = {}): HighwayPlan {
|
|
23
|
+
const lengths = edgeLengths(input.graph, plan, way);
|
|
24
|
+
const out = new Set<string>();
|
|
25
|
+
for (const [edge, l] of lengths) if (l.route > options.budget * l.alone + options.pad + 1e-9) out.add(edge);
|
|
26
|
+
return out.size === 0 ? plan : keepMembers(plan, (_, m) => !out.has(m.edge));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The gate `stretch` (HWY-048): an edge joins a highway only when its route stays in a budget of its route alone. */
|
|
30
|
+
export const stretchGate: Gate<StretchOptions> = {
|
|
31
|
+
name: "stretch",
|
|
32
|
+
describe: "The gate `stretch` (HWY-048) keeps an edge on a highway only when its route is at most `budget` times its route alone, plus `pad`.",
|
|
33
|
+
defaults: STRETCH_DEFAULTS,
|
|
34
|
+
schema: {
|
|
35
|
+
budget: { type: "number", min: 1, max: 2, step: 0.05, marks: [1.1, 1.2, 1.3, 1.5], default: 1.2, describe: "The largest ratio of the route along the plan to the route alone." },
|
|
36
|
+
pad: { type: "number", min: 0, max: 40, step: 5, default: 0, describe: "The added length that each edge can use, in units of the board." },
|
|
37
|
+
},
|
|
38
|
+
apply: stretchPlan,
|
|
39
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { dist, polylineLength, type Point } from "../geom.ts";
|
|
2
|
+
import { orderLanes } from "../segments.ts";
|
|
3
|
+
import { WayFinder, type WayOptions } from "../ways.ts";
|
|
4
|
+
import { tidyPlan } from "./base.ts";
|
|
5
|
+
import { memberAt, type Highway, type HighwayInput, type HighwayMember, type HighwayPlan } from "./model.ts";
|
|
6
|
+
import { sharedWaysPlan } from "./shared-ways/index.ts";
|
|
7
|
+
import { edgePlugs } from "./stretches.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The spine of a group of edges. With `shared`, the edges of each group go through the router `shared` with the group as their key.
|
|
11
|
+
* Each stretch that they share is a highway. With `trunk`, each group gets one trunk between two terminals, with a leg from each plug.
|
|
12
|
+
*/
|
|
13
|
+
export type GroupSpine = "shared" | "trunk";
|
|
14
|
+
|
|
15
|
+
/** The values of `GroupSpine`. */
|
|
16
|
+
export const GROUP_SPINES: readonly GroupSpine[] = Object.freeze(["shared", "trunk"]);
|
|
17
|
+
|
|
18
|
+
/** The group of one edge: its key, and `flip` when the edge goes from the second end of the group to the first end. */
|
|
19
|
+
export interface EdgeGroup {
|
|
20
|
+
readonly key: string;
|
|
21
|
+
readonly flip: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The options of `groupedPlan`. */
|
|
25
|
+
export interface GroupedOptions extends WayOptions {
|
|
26
|
+
readonly spine: GroupSpine;
|
|
27
|
+
/** A group has this number of edges or more. The value is 2 or more. */
|
|
28
|
+
readonly minSize: number;
|
|
29
|
+
/**
|
|
30
|
+
* With the spine `trunk`, an edge rides the trunk only when each of its legs is this share of the span or shorter.
|
|
31
|
+
* The span is the distance between the two terminals. `Infinity` sets no limit.
|
|
32
|
+
*/
|
|
33
|
+
readonly legMax?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* This function gives the geometric median of points (the Weber point), with the iterations of Weiszfeld.
|
|
38
|
+
* The point has the least sum of the distances to the points.
|
|
39
|
+
*/
|
|
40
|
+
export function weberPoint(points: readonly Point[], iterations = 32): Point {
|
|
41
|
+
if (points.length === 0) throw new Error("The list has no point.");
|
|
42
|
+
let p = { x: points.reduce((s, q) => s + q.x, 0) / points.length, y: points.reduce((s, q) => s + q.y, 0) / points.length };
|
|
43
|
+
for (let k = 0; k < iterations; k++) {
|
|
44
|
+
let wx = 0;
|
|
45
|
+
let wy = 0;
|
|
46
|
+
let w = 0;
|
|
47
|
+
for (const q of points) {
|
|
48
|
+
const d = dist(p, q);
|
|
49
|
+
if (d < 1e-9) return q;
|
|
50
|
+
wx += q.x / d;
|
|
51
|
+
wy += q.y / d;
|
|
52
|
+
w += 1 / d;
|
|
53
|
+
}
|
|
54
|
+
const next = { x: wx / w, y: wy / w };
|
|
55
|
+
if (dist(next, p) < 1e-6) return next;
|
|
56
|
+
p = next;
|
|
57
|
+
}
|
|
58
|
+
return p;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* This function gives the highways of the groups of edges. Only the edges in `groups` ride a highway, and each group needs `minSize` edges.
|
|
63
|
+
* The spine `shared` gives the shared stretches of the router `shared` with the group as the key.
|
|
64
|
+
* The spine `trunk` gives one trunk for each group, through the corner graph between two terminals.
|
|
65
|
+
* A terminal is the Weber point of the points out of the plugs at one end of the group. A terminal in a box moves to the nearest of those points.
|
|
66
|
+
*/
|
|
67
|
+
export function groupedPlan(input: HighwayInput, groups: ReadonlyMap<string, EdgeGroup>, o: Readonly<GroupedOptions>): HighwayPlan {
|
|
68
|
+
const min = Math.max(2, o.minSize);
|
|
69
|
+
const size = new Map<string, number>();
|
|
70
|
+
for (const g of groups.values()) size.set(g.key, (size.get(g.key) ?? 0) + 1);
|
|
71
|
+
const big = (edge: string): EdgeGroup | undefined => {
|
|
72
|
+
const g = groups.get(edge);
|
|
73
|
+
return g && (size.get(g.key) ?? 0) >= min ? g : undefined;
|
|
74
|
+
};
|
|
75
|
+
const way: WayOptions = { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) };
|
|
76
|
+
if (o.spine === "shared") return sharedWaysPlan(input, { ...way, rule: (e) => big(e.id)?.key });
|
|
77
|
+
|
|
78
|
+
const finder = new WayFinder(input.graph.nodes, way);
|
|
79
|
+
const byKey = new Map<string, { edge: string; flip: boolean; a: Point; b: Point; ps: Point; pt: Point }[]>();
|
|
80
|
+
for (const { edge, ps, pt } of edgePlugs(input.graph)) {
|
|
81
|
+
const g = big(edge.id);
|
|
82
|
+
if (!g) continue;
|
|
83
|
+
const os = finder.out(ps);
|
|
84
|
+
const ot = finder.out(pt);
|
|
85
|
+
const item = g.flip ? { edge: edge.id, flip: true, a: ot, b: os, ps: ps.point, pt: pt.point } : { edge: edge.id, flip: false, a: os, b: ot, ps: ps.point, pt: pt.point };
|
|
86
|
+
const list = byKey.get(g.key);
|
|
87
|
+
if (list) list.push(item);
|
|
88
|
+
else byKey.set(g.key, [item]);
|
|
89
|
+
}
|
|
90
|
+
const pad = finder.margin / 2;
|
|
91
|
+
const terminal = (points: readonly Point[]): Point => {
|
|
92
|
+
const p = weberPoint(points);
|
|
93
|
+
if (!finder.obstacles.inside(p, pad)) return p;
|
|
94
|
+
let best = points[0]!;
|
|
95
|
+
for (const q of points) if (dist(q, p) < dist(best, p)) best = q;
|
|
96
|
+
return best;
|
|
97
|
+
};
|
|
98
|
+
const highways: Highway[] = [];
|
|
99
|
+
const legMax = o.legMax ?? Infinity;
|
|
100
|
+
for (const [key, list] of byKey) {
|
|
101
|
+
if (list.length < min) continue;
|
|
102
|
+
const ta = terminal(list.map((x) => x.a));
|
|
103
|
+
const tb = terminal(list.map((x) => x.b));
|
|
104
|
+
const span = dist(ta, tb);
|
|
105
|
+
if (span <= finder.margin) continue;
|
|
106
|
+
const riders = list.filter((x) => dist(x.a, ta) <= legMax * span && dist(x.b, tb) <= legMax * span);
|
|
107
|
+
if (riders.length < min) continue;
|
|
108
|
+
const raw = finder.corners.path(ta, tb);
|
|
109
|
+
if (!raw) continue;
|
|
110
|
+
const spine = finder.corners.pull(raw);
|
|
111
|
+
if (spine.length < 2 || polylineLength(spine) <= 1e-6) continue;
|
|
112
|
+
const last = spine.length - 1;
|
|
113
|
+
const lanes = orderLanes(
|
|
114
|
+
spine,
|
|
115
|
+
riders.map((x) => ({ edge: x.edge, points: x.flip ? [x.ps, x.b, ...[...spine].reverse(), x.a, x.pt] : [x.ps, x.a, ...spine, x.b, x.pt] })),
|
|
116
|
+
"bundle",
|
|
117
|
+
);
|
|
118
|
+
const byEdge = new Map(riders.map((x) => [x.edge, x]));
|
|
119
|
+
const members: HighwayMember[] = lanes.map((edge) => (byEdge.get(edge)!.flip ? memberAt(spine, edge, last, 0) : memberAt(spine, edge, 0, last)));
|
|
120
|
+
highways.push({ id: `h${highways.length}`, key, spine, lanes, members });
|
|
121
|
+
}
|
|
122
|
+
return tidyPlan({ highways });
|
|
123
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
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 { firstRoutes, nearestPlace, tidyPlan } from "../base.ts";
|
|
6
|
+
import { lengthBetween, memberAt, pointAt, type Highway, type HighwayInput, type HighwayMember, type HighwayPlan } from "../model.ts";
|
|
7
|
+
import { edgePlugs } from "../stretches.ts";
|
|
8
|
+
|
|
9
|
+
/** The order of the edges in the growth: the longest first route first, or the order of the graph. */
|
|
10
|
+
export type GrowthOrder = "length" | "input";
|
|
11
|
+
|
|
12
|
+
/** The options of the highway strategy `growth` (HWY-055 of the note on highways). */
|
|
13
|
+
export interface GrowthOptions {
|
|
14
|
+
/** The distance from a box to the corners of the graph. The default is 5. */
|
|
15
|
+
readonly margin?: number;
|
|
16
|
+
/** The number of visible neighbours of each corner. The default is 8. */
|
|
17
|
+
readonly k?: number;
|
|
18
|
+
/** The order of the edges. The default is `length`. */
|
|
19
|
+
readonly order: GrowthOrder;
|
|
20
|
+
/** The cost of the part on a spine, as a share of its length. The ramps cost their full length. The default is 0.5. */
|
|
21
|
+
readonly share: number;
|
|
22
|
+
/** An edge joins a highway only when its way along it is this value times its first route or shorter. The default is 1.3. */
|
|
23
|
+
readonly stretch: number;
|
|
24
|
+
/** A spine end extends when this number of its lanes or more go on together. The default is 3. */
|
|
25
|
+
readonly extendLanes: number;
|
|
26
|
+
/** The lanes go on together when their ends are this distance from their center or nearer, in units of the board. The default is 2. */
|
|
27
|
+
readonly extendRadius: number;
|
|
28
|
+
/** A highway with this number of edges or more stays in the plan. The default is 2. */
|
|
29
|
+
readonly minSize: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The default options of the strategy `growth`. */
|
|
33
|
+
export const GROWTH_DEFAULTS: Readonly<GrowthOptions> = Object.freeze({ order: "length", share: 0.5, stretch: 1.3, extendLanes: 3, extendRadius: 2, minSize: 2 });
|
|
34
|
+
|
|
35
|
+
interface Grown {
|
|
36
|
+
spine: Point[];
|
|
37
|
+
members: { edge: string; on: number; off: number }[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface Rider {
|
|
41
|
+
readonly edge: string;
|
|
42
|
+
readonly ps: Point;
|
|
43
|
+
readonly pt: Point;
|
|
44
|
+
readonly a: Point;
|
|
45
|
+
readonly b: Point;
|
|
46
|
+
readonly len: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* This function gives the highways of local growth (HWY-055). The edges come one by one, by default the longest first route first.
|
|
51
|
+
* Each edge joins the highway with the largest saving: its first route less its ramps and `share` of its part on the spine. Without a saving, the edge starts a highway on its own way.
|
|
52
|
+
* After the growth, a spine end can go on to the center of the far ends of its lanes.
|
|
53
|
+
* This occurs when `extendLanes` of them or more are near that center and the line is clear.
|
|
54
|
+
*/
|
|
55
|
+
export function growthPlan(input: HighwayInput, o: Readonly<GrowthOptions>): HighwayPlan {
|
|
56
|
+
const graph = input.graph;
|
|
57
|
+
const finder = new WayFinder(graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
|
|
58
|
+
const pad = finder.margin / 2;
|
|
59
|
+
const routes = firstRoutes(input);
|
|
60
|
+
const riders: Rider[] = edgePlugs(graph).map(({ edge, ps, pt }) => {
|
|
61
|
+
const route = routes.get(edge.id);
|
|
62
|
+
const a = finder.out(ps);
|
|
63
|
+
const b = finder.out(pt);
|
|
64
|
+
return { edge: edge.id, ps: ps.point, pt: pt.point, a, b, len: route ? polylineLength(route) : dist(ps.point, pt.point) };
|
|
65
|
+
});
|
|
66
|
+
if (o.order === "length") riders.sort((p, q) => q.len - p.len || (p.edge < q.edge ? -1 : p.edge > q.edge ? 1 : 0));
|
|
67
|
+
const grown: Grown[] = [];
|
|
68
|
+
const short = finder.margin;
|
|
69
|
+
for (const r of riders) {
|
|
70
|
+
let best = -1;
|
|
71
|
+
let bestSave = 0;
|
|
72
|
+
let bestOn = 0;
|
|
73
|
+
let bestOff = 0;
|
|
74
|
+
grown.forEach((h, hi) => {
|
|
75
|
+
const on = nearestPlace(h.spine, r.a);
|
|
76
|
+
const off = nearestPlace(h.spine, r.b);
|
|
77
|
+
const part = lengthBetween(h.spine, Math.min(on, off), Math.max(on, off));
|
|
78
|
+
if (part <= short) return;
|
|
79
|
+
const ramps = dist(r.a, pointAt(h.spine, on)) + dist(pointAt(h.spine, off), r.b);
|
|
80
|
+
if (ramps + part > o.stretch * r.len) return;
|
|
81
|
+
const save = r.len - (ramps + o.share * part);
|
|
82
|
+
if (save > bestSave) [best, bestSave, bestOn, bestOff] = [hi, save, on, off];
|
|
83
|
+
});
|
|
84
|
+
if (best >= 0) {
|
|
85
|
+
grown[best]!.members.push({ edge: r.edge, on: bestOn, off: bestOff });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const raw = finder.corners.path(r.a, r.b);
|
|
89
|
+
if (!raw) continue;
|
|
90
|
+
const spine = finder.corners.pull(raw);
|
|
91
|
+
if (spine.length < 2 || polylineLength(spine) <= short) continue;
|
|
92
|
+
grown.push({ spine, members: [{ edge: r.edge, on: 0, off: spine.length - 1 }] });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A spine end goes on while enough of its lanes go on together.
|
|
96
|
+
const byEdge = new Map(riders.map((r) => [r.edge, r]));
|
|
97
|
+
const radius = o.extendRadius * boardUnit(graph);
|
|
98
|
+
const need = Math.max(2, Math.round(o.extendLanes));
|
|
99
|
+
for (const h of grown) {
|
|
100
|
+
for (const end of ["first", "last"] as const) {
|
|
101
|
+
const last = h.spine.length - 1;
|
|
102
|
+
const at = end === "first" ? 0 : last;
|
|
103
|
+
// The far point of each lane at this end: the point out of the plug that the lane goes to or comes from.
|
|
104
|
+
const far: { m: Grown["members"][number]; side: "on" | "off"; p: Point }[] = [];
|
|
105
|
+
for (const m of h.members) {
|
|
106
|
+
const r = byEdge.get(m.edge)!;
|
|
107
|
+
if (m.on === at) far.push({ m, side: "on", p: r.a });
|
|
108
|
+
else if (m.off === at) far.push({ m, side: "off", p: r.b });
|
|
109
|
+
}
|
|
110
|
+
if (far.length < need) continue;
|
|
111
|
+
const c = { x: far.reduce((s, f) => s + f.p.x, 0) / far.length, y: far.reduce((s, f) => s + f.p.y, 0) / far.length };
|
|
112
|
+
const near = far.filter((f) => dist(f.p, c) <= radius);
|
|
113
|
+
const tip = h.spine[at]!;
|
|
114
|
+
if (near.length < need || dist(tip, c) <= short) continue;
|
|
115
|
+
if (finder.obstacles.inside(c, pad) || !finder.obstacles.clear(tip, c, pad)) continue;
|
|
116
|
+
if (end === "first") {
|
|
117
|
+
h.spine.unshift(c);
|
|
118
|
+
for (const m of h.members) [m.on, m.off] = [m.on + 1, m.off + 1];
|
|
119
|
+
for (const f of near) f.m[f.side] = 0;
|
|
120
|
+
} else {
|
|
121
|
+
h.spine.push(c);
|
|
122
|
+
for (const f of near) f.m[f.side] = last + 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const highways: Highway[] = [];
|
|
128
|
+
grown.forEach((h, hi) => {
|
|
129
|
+
if (h.members.length < Math.max(2, o.minSize)) return;
|
|
130
|
+
const spine = h.spine;
|
|
131
|
+
const back = [...spine].reverse();
|
|
132
|
+
const lanes = orderLanes(
|
|
133
|
+
spine,
|
|
134
|
+
h.members.map((m) => {
|
|
135
|
+
const r = byEdge.get(m.edge)!;
|
|
136
|
+
return { edge: m.edge, points: m.on <= m.off ? [r.ps, r.a, ...spine, r.b, r.pt] : [r.ps, r.a, ...back, r.b, r.pt] };
|
|
137
|
+
}),
|
|
138
|
+
"bundle",
|
|
139
|
+
);
|
|
140
|
+
const byLane = new Map(h.members.map((m) => [m.edge, m]));
|
|
141
|
+
const members: HighwayMember[] = lanes.map((edge) => {
|
|
142
|
+
const m = byLane.get(edge)!;
|
|
143
|
+
return memberAt(spine, edge, m.on, m.off);
|
|
144
|
+
});
|
|
145
|
+
highways.push({ id: `h${hi}`, key: `g${hi}`, spine, lanes, members });
|
|
146
|
+
});
|
|
147
|
+
return tidyPlan({ highways });
|
|
148
|
+
}
|