@mark1russell7/highways 0.3.0 → 0.4.1

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.
Files changed (43) hide show
  1. package/README.md +66 -2
  2. package/build/index.d.ts +1009 -12
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +4408 -195
  5. package/build/index.js.map +1 -1
  6. package/package.json +4 -1
  7. package/src/bench/compare.ts +41 -6
  8. package/src/bench/measures/bundles.ts +101 -2
  9. package/src/bench/measures/index.ts +1 -0
  10. package/src/bench/measures/order.ts +97 -0
  11. package/src/bench/room.ts +126 -9
  12. package/src/bench/screen/forms.ts +372 -0
  13. package/src/bench/screen/generators.ts +54 -0
  14. package/src/bench/screen/hash.ts +129 -0
  15. package/src/bench/screen/index.ts +6 -0
  16. package/src/bench/screen/measures.ts +126 -0
  17. package/src/bench/screen/screen.ts +454 -0
  18. package/src/bench/screen/set.ts +136 -0
  19. package/src/bench/suite.ts +71 -13
  20. package/src/highways/corridors/index.ts +305 -0
  21. package/src/highways/follow.ts +155 -0
  22. package/src/highways/index.ts +7 -0
  23. package/src/highways/keys/index.ts +15 -0
  24. package/src/highways/model.ts +111 -0
  25. package/src/highways/result.ts +34 -0
  26. package/src/highways/router.ts +40 -0
  27. package/src/highways/shared-ways/index.ts +15 -0
  28. package/src/index.ts +4 -0
  29. package/src/order/index.ts +65 -0
  30. package/src/plugs/justify.ts +48 -0
  31. package/src/plugs/side.ts +138 -0
  32. package/src/repair/index.ts +377 -0
  33. package/src/repair/walk.ts +220 -0
  34. package/src/room/lanes.ts +109 -10
  35. package/src/routers/corners.ts +73 -6
  36. package/src/routers/fan.ts +145 -0
  37. package/src/routers/index.ts +2 -2
  38. package/src/routers/shared.ts +107 -8
  39. package/src/routers/snell.ts +140 -0
  40. package/src/routers/trunks.ts +43 -2
  41. package/src/strategies/builtins.ts +82 -6
  42. package/src/strategies/model.ts +36 -6
  43. package/src/strategies/schemas.ts +66 -2
@@ -0,0 +1,155 @@
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";
3
+ import { placePlug } from "../plugs.ts";
4
+ import { orderLanes, type RunOrder } from "../segments.ts";
5
+ import { WayFinder, type WayOptions } from "../ways.ts";
6
+ import { pointAt, type HighwayMember, type HighwayPlan } from "./model.ts";
7
+
8
+ /** The options of `follow`. */
9
+ export interface FollowOptions extends WayOptions {
10
+ /**
11
+ * These routes are the routes of the base router. Each edge outside the plan keeps its route from them.
12
+ * Without them, or for an edge without a route in them, `follow` routes the edge as the router `corners` does.
13
+ */
14
+ readonly routes?: Result;
15
+ /** The lane order of the shared runs. The default is `bundle`. */
16
+ readonly laneOrder?: RunOrder;
17
+ }
18
+
19
+ const SAME = 1e-6;
20
+
21
+ const dedupe = (p: readonly Point[]): Point[] => p.filter((q, i) => i === 0 || dist(q, p[i - 1]!) > SAME);
22
+
23
+ /** This function tells if a polyline holds the points of `part` in a row, in this order. */
24
+ function holdsInOrder(p: readonly Point[], part: readonly Point[]): boolean {
25
+ for (let i = 0; i + part.length <= p.length; i++) {
26
+ let ok = true;
27
+ for (let j = 0; ok && j < part.length; j++) ok = dist(p[i + j]!, part[j]!) <= SAME;
28
+ if (ok) return true;
29
+ }
30
+ return false;
31
+ }
32
+
33
+ /** This function tells if a polyline holds the points of `part` in a row, in either direction. */
34
+ const holds = (p: readonly Point[], part: readonly Point[]): boolean => holdsInOrder(p, part) || holdsInOrder(p, [...part].reverse());
35
+
36
+ /**
37
+ * This function tells if a point of a spine can be the plug of an end. The point is on the box of the node.
38
+ * When the end has a fixed plug, the point is at that plug.
39
+ */
40
+ function plugAt(node: Node, end: End, p: Point): boolean {
41
+ if (pointBoxDistance(p, node.box) > SAME) return false;
42
+ const fixed = fixedPortPoint(node, end);
43
+ return fixed === undefined || dist(fixed, p) <= SAME;
44
+ }
45
+
46
+ /** The stops of one spine: each point of the spine and each merge point and split point, by their place, in order. */
47
+ type Stops = readonly (readonly [at: number, point: Point])[];
48
+
49
+ /** This function gives the points of the stops from the place `on` to the place `off`. When `on` is larger than `off`, the points go backward. */
50
+ function slice(stops: Stops, on: number, off: number): Point[] {
51
+ const lo = Math.min(on, off);
52
+ const hi = Math.max(on, off);
53
+ const out = stops.filter(([at]) => at >= lo && at <= hi).map(([, p]) => p);
54
+ return on <= off ? out : out.reverse();
55
+ }
56
+
57
+ /**
58
+ * This function routes the edges along a highway plan.
59
+ *
60
+ * An edge of a highway goes from its source plug to its merge point. Then it goes along the spine to its split point, and to its target plug.
61
+ * The ramps go around the boxes through the corner graph (`WayFinder.fromPlug` and `WayFinder.toPlug`).
62
+ * A spine point on the box of the node is the plug itself. When the end has a fixed plug, the point is at that plug.
63
+ *
64
+ * An edge on several highways takes them in the order of `seq`.
65
+ * A way through the corner graph goes from each split point to the next merge point.
66
+ * An edge outside the plan keeps its route from `options.routes`, or goes as in the router `corners`.
67
+ *
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
+ * The same graph, plan and options give the same result.
70
+ */
71
+ export function follow(graph: Graph, plan: HighwayPlan, options: FollowOptions = {}): Result {
72
+ const byId = nodeIndex(graph);
73
+ const ways = new WayFinder(graph.nodes, options);
74
+ const base = new Map((options.routes?.routes ?? []).map((r) => [r.edge, r.points]));
75
+ const lines: Stops[] = plan.highways.map((h) => {
76
+ const stops = new Map<number, Point>(h.spine.map((p, i) => [i, p]));
77
+ for (const m of h.members) for (const at of [m.on, m.off]) if (!stops.has(at)) stops.set(at, pointAt(h.spine, at));
78
+ return [...stops].sort((a, b) => a[0] - b[0]);
79
+ });
80
+ const trips = new Map<string, { h: number; m: HighwayMember }[]>();
81
+ plan.highways.forEach((h, hi) => {
82
+ for (const m of h.members) {
83
+ const list = trips.get(m.edge);
84
+ if (list) list.push({ h: hi, m });
85
+ else trips.set(m.edge, [{ h: hi, m }]);
86
+ }
87
+ });
88
+ for (const list of trips.values()) list.sort((a, b) => a.m.seq - b.m.seq || a.h - b.h);
89
+
90
+ const routes: Route[] = [];
91
+ for (const e of graph.edges) {
92
+ const s = byId.get(e.source.node);
93
+ const t = byId.get(e.target.node);
94
+ if (!s || !t) continue;
95
+ const trip = trips.get(e.id);
96
+ if (!trip) {
97
+ const known = base.get(e.id);
98
+ if (known) {
99
+ routes.push({ edge: e.id, points: known });
100
+ continue;
101
+ }
102
+ const ps = placePlug(s, e.source, center(t.box));
103
+ const pt = placePlug(t, e.target, center(s.box));
104
+ routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
105
+ continue;
106
+ }
107
+ const out: Point[] = [];
108
+ let at: Point | undefined;
109
+ for (const { h, m } of trip) {
110
+ const piece = slice(lines[h]!, m.on, m.off);
111
+ const first = piece[0]!;
112
+ if (at === undefined) {
113
+ if (!plugAt(s, e.source, first)) {
114
+ const ps = placePlug(s, e.source, first);
115
+ out.push(...(ways.fromPlug(ps, first) ?? [ps.point, first]));
116
+ }
117
+ } else if (dist(at, first) > SAME) {
118
+ const way = ways.corners.path(at, first);
119
+ out.push(...(way ? ways.corners.pull(way) : [at, first]));
120
+ }
121
+ out.push(...piece);
122
+ at = piece[piece.length - 1]!;
123
+ }
124
+ if (at !== undefined && !plugAt(t, e.target, at)) {
125
+ const pt = placePlug(t, e.target, at);
126
+ out.push(...(ways.toPlug(at, pt) ?? [at, pt.point]));
127
+ }
128
+ routes.push({ edge: e.id, points: dedupe(out) });
129
+ }
130
+
131
+ // The runs: each stretch between two stops of a spine, with the same two edges or more on it, joined with the next stretch of the same edges.
132
+ const routeOf = new Map(routes.map((r) => [r.edge, r]));
133
+ const runs: SharedRun[] = [];
134
+ plan.highways.forEach((h, hi) => {
135
+ const stops = lines[hi]!;
136
+ const usersOf = (k: number): string[] =>
137
+ h.members
138
+ .filter((m) => routeOf.has(m.edge) && Math.min(m.on, m.off) <= stops[k]![0] && Math.max(m.on, m.off) >= stops[k + 1]![0])
139
+ .map((m) => m.edge)
140
+ .sort();
141
+ let k = 0;
142
+ while (k < stops.length - 1) {
143
+ const users = usersOf(k);
144
+ let end = k + 1;
145
+ while (end < stops.length - 1 && usersOf(end).join("\u0000") === users.join("\u0000")) end++;
146
+ const points = dedupe(stops.slice(k, end + 1).map(([, p]) => p));
147
+ k = end;
148
+ if (users.length < 2 || points.length < 2) continue;
149
+ const mine = users.map((id) => routeOf.get(id)!).filter((r) => holds(r.points, points));
150
+ if (mine.length < 2) continue;
151
+ runs.push({ id: `r${runs.length}`, key: h.key, points, edges: orderLanes(points, mine, options.laneOrder ?? "bundle") });
152
+ }
153
+ });
154
+ return runs.length > 0 ? { routes, runs } : { routes };
155
+ }
@@ -0,0 +1,7 @@
1
+ export { checkPlan, lengthBetween, memberAt, pointAt, type Highway, type HighwayInput, type HighwayMember, type HighwayPlan } from "./model.ts";
2
+ export { follow, type FollowOptions } from "./follow.ts";
3
+ export { planOfResult } from "./result.ts";
4
+ export { highwayRouter, highwayRouterStrategy } from "./router.ts";
5
+ export { keysPlan, type KeysOptions } from "./keys/index.ts";
6
+ export { sharedWaysPlan, type SharedWaysOptions } from "./shared-ways/index.ts";
7
+ export { BREAK_EVEN, CORRIDOR_DEFAULTS, corridorPlan, minHash, type CorridorOptions } from "./corridors/index.ts";
@@ -0,0 +1,15 @@
1
+ import { trunksRouter, type TrunksOptions } from "../../routers/trunks.ts";
2
+ import { planOfResult } from "../result.ts";
3
+ import type { HighwayInput, HighwayPlan } from "../model.ts";
4
+
5
+ /** The options of the highway strategy `keys`: the options of the router `trunks`, without the name and the limit of the benchmark. */
6
+ export type KeysOptions = Omit<TrunksOptions, "name" | "maxNodes">;
7
+
8
+ /**
9
+ * This function gives the highways that the router `trunks` decides. A rule gives each edge a key.
10
+ * The edges from one plug with one key share a trunk to a split point. The edges that go on together share a run past it.
11
+ * Each trunk and each run becomes one highway. The function does not change the router: it reads its bundles and its runs.
12
+ */
13
+ export function keysPlan(input: HighwayInput, options: KeysOptions): HighwayPlan {
14
+ return planOfResult(trunksRouter({ ...options, name: "keys" }).route(input.graph));
15
+ }
@@ -0,0 +1,111 @@
1
+ import { dist, type Point } from "../geom.ts";
2
+ import type { Graph, Result } from "../model.ts";
3
+
4
+ /** The input of a highway strategy: the boxes and the edges, and optionally the first routes of a router. */
5
+ export interface HighwayInput {
6
+ readonly graph: Graph;
7
+ /**
8
+ * These routes are the first routes, from a router. A strategy that reads first routes makes them itself when this value is absent.
9
+ * The strategy `corridors` makes the ways of the corner graph, as the router `corners` finds them.
10
+ */
11
+ readonly routes?: Result;
12
+ }
13
+
14
+ /**
15
+ * One edge of a highway. The edge joins the spine at the merge point and leaves it at the split point.
16
+ * A place on the spine is the index of a point of the spine plus the share of the next leg. Point `i` of the spine is at `i`.
17
+ * When `on` is larger than `off`, the edge goes along the spine from its last point to its first point.
18
+ */
19
+ export interface HighwayMember {
20
+ readonly edge: string;
21
+ /** The place of the merge point on the spine. */
22
+ readonly on: number;
23
+ /** The place of the split point on the spine. */
24
+ readonly off: number;
25
+ /** The point where the edge joins the spine. */
26
+ readonly merge: Point;
27
+ /** The point where the edge leaves the spine. */
28
+ readonly split: Point;
29
+ /**
30
+ * The place of this highway in the trip of the edge, from 0. An edge can go along several highways, one after the other.
31
+ * `follow` takes the highways of an edge in the order of this value.
32
+ */
33
+ readonly seq: number;
34
+ }
35
+
36
+ /** A highway: a shared way that a bundle of edges follows, from their merge points to their split points. */
37
+ export interface Highway {
38
+ readonly id: string;
39
+ /** The key of the highway, for example the key of the grouping rule or the index of a cluster. */
40
+ readonly key: string;
41
+ /** The spine: the polyline that the edges follow, in free space. */
42
+ readonly spine: readonly Point[];
43
+ /** The edges in lane order. Lane 0 is on the left of the spine, from its first point to its last point. The y axis points down. */
44
+ readonly lanes: readonly string[];
45
+ /** The edges with their merge points and their split points, in the order of `lanes`. */
46
+ readonly members: readonly HighwayMember[];
47
+ }
48
+
49
+ /** The output of a highway strategy: the highways, as data. `follow` routes the edges along them. */
50
+ export interface HighwayPlan {
51
+ readonly highways: readonly Highway[];
52
+ }
53
+
54
+ /** This function gives the point at a place on a polyline. A place outside the polyline gives its nearest end. */
55
+ export function pointAt(line: readonly Point[], at: number): Point {
56
+ if (line.length === 0) throw new Error("The polyline has no point.");
57
+ if (at <= 0) return line[0]!;
58
+ if (at >= line.length - 1) return line[line.length - 1]!;
59
+ const i = Math.floor(at);
60
+ const u = at - i;
61
+ const a = line[i]!;
62
+ if (u === 0) return a;
63
+ const b = line[i + 1]!;
64
+ return { x: a.x + (b.x - a.x) * u, y: a.y + (b.y - a.y) * u };
65
+ }
66
+
67
+ /** This function gives the length of a polyline from the place `from` to the place `to`, with `from` not larger than `to`. */
68
+ export function lengthBetween(line: readonly Point[], from: number, to: number): number {
69
+ if (to <= from) return 0;
70
+ let sum = 0;
71
+ let prev = pointAt(line, from);
72
+ for (let i = Math.floor(from) + 1; i < to; i++) {
73
+ const p = line[i]!;
74
+ sum += dist(prev, p);
75
+ prev = p;
76
+ }
77
+ return sum + dist(prev, pointAt(line, to));
78
+ }
79
+
80
+ /** This function gives a member of a highway with its merge point and its split point at the places `on` and `off` of the spine. */
81
+ export function memberAt(spine: readonly Point[], edge: string, on: number, off: number, seq = 0): HighwayMember {
82
+ return { edge, on, off, merge: pointAt(spine, on), split: pointAt(spine, off), seq };
83
+ }
84
+
85
+ /**
86
+ * This function lists the problems of a plan against its graph. A good plan gives an empty list.
87
+ * Each highway has a spine of two points or more, and two members or more. Each member is an edge of the graph, with `on` and `off` on the spine and apart.
88
+ * An edge is a member of a highway one time at most.
89
+ */
90
+ export function checkPlan(graph: Graph, plan: HighwayPlan): string[] {
91
+ const edges = new Set(graph.edges.map((e) => e.id));
92
+ const out: string[] = [];
93
+ const ids = new Set<string>();
94
+ for (const h of plan.highways) {
95
+ if (ids.has(h.id)) out.push(`Two highways have the id "${h.id}".`);
96
+ ids.add(h.id);
97
+ const last = h.spine.length - 1;
98
+ if (h.spine.length < 2) out.push(`The highway "${h.id}" has a spine with fewer than two points.`);
99
+ if (h.members.length < 2) out.push(`The highway "${h.id}" has fewer than two members.`);
100
+ const seen = new Set<string>();
101
+ for (const m of h.members) {
102
+ if (!edges.has(m.edge)) out.push(`The highway "${h.id}" has the member "${m.edge}", which is not an edge of the graph.`);
103
+ if (seen.has(m.edge)) out.push(`The highway "${h.id}" has the member "${m.edge}" two times.`);
104
+ seen.add(m.edge);
105
+ if (!(m.on >= 0 && m.on <= last && m.off >= 0 && m.off <= last)) out.push(`The member "${m.edge}" of the highway "${h.id}" has a place off the spine.`);
106
+ if (m.on === m.off) out.push(`The member "${m.edge}" of the highway "${h.id}" joins and leaves the spine at one place.`);
107
+ }
108
+ if (h.lanes.length !== h.members.length || h.lanes.some((e) => !seen.has(e))) out.push(`The lanes of the highway "${h.id}" are not its members.`);
109
+ }
110
+ return out;
111
+ }
@@ -0,0 +1,34 @@
1
+ import type { Result } from "../model.ts";
2
+ import { segmentsOf } from "../segments.ts";
3
+ import { memberAt, type Highway, type HighwayMember, type HighwayPlan } from "./model.ts";
4
+
5
+ /**
6
+ * This function gives the highways of a result of a router: one highway for each shared run and for each trunk of a bundle.
7
+ * The spine is the points of the run or of the trunk. Each edge of the run is a member, from one end of the spine to the other end, in the way of its route.
8
+ * The member `seq` is the place of the shared stretch in the route of the edge. The lanes keep the lane order of the router.
9
+ *
10
+ * Thus `follow` routes each edge along the shared stretches of the router. A route that does not hold its run is not a member.
11
+ */
12
+ export function planOfResult(result: Result): HighwayPlan {
13
+ const graph = segmentsOf(result);
14
+ const keys = new Map<string, string>([...(result.runs ?? []).map((r) => [r.id, r.key] as const), ...(result.bundles ?? []).map((b) => [b.id, b.key] as const)]);
15
+ const shared = graph.segments.filter((s) => s.count > 1);
16
+ const ids = new Set(shared.map((s) => s.id));
17
+ const place = new Map<string, { seq: number; reverse: boolean }>();
18
+ for (const path of graph.paths) {
19
+ let seq = 0;
20
+ for (const ref of path.segments) if (ids.has(ref.segment)) place.set(`${ref.segment}\u0000${path.edge}`, { seq: seq++, reverse: ref.reverse });
21
+ }
22
+ const highways: Highway[] = [];
23
+ for (const s of shared) {
24
+ const last = s.points.length - 1;
25
+ const members: HighwayMember[] = [];
26
+ for (const edge of s.edges) {
27
+ const at = place.get(`${s.id}\u0000${edge}`);
28
+ if (at) members.push(at.reverse ? memberAt(s.points, edge, last, 0, at.seq) : memberAt(s.points, edge, 0, last, at.seq));
29
+ }
30
+ if (members.length < 2) continue;
31
+ highways.push({ id: `h${highways.length}`, key: keys.get(s.id) ?? s.id, spine: s.points, lanes: members.map((m) => m.edge), members });
32
+ }
33
+ return { highways };
34
+ }
@@ -0,0 +1,40 @@
1
+ import type { Graph, Result, Router } from "../model.ts";
2
+ import { defineStrategy, type Strategy } from "../strategies/model.ts";
3
+ import type { WayOptions } from "../ways.ts";
4
+ import { follow, type FollowOptions } from "./follow.ts";
5
+
6
+ /** This function gives the options of the way finder in the options of a strategy: `margin` and `k`, when they are present. */
7
+ function wayOptionsOf(options: object): WayOptions {
8
+ const o = options as { margin?: unknown; k?: unknown };
9
+ return { ...(typeof o.margin === "number" ? { margin: o.margin } : {}), ...(typeof o.k === "number" ? { k: o.k } : {}) };
10
+ }
11
+
12
+ /**
13
+ * This function gives a router that runs a highway strategy and then `follow`. The strategy gets the graph without first routes.
14
+ * The way finder of `follow` takes `margin` and `k` from the options of the strategy, and then from `followOptions`.
15
+ * The router keeps the name and the `maxNodes` of the strategy.
16
+ */
17
+ export function highwayRouter<O extends object>(strategy: Strategy<"highway", O>, options?: Partial<O>, followOptions: FollowOptions = {}): Router {
18
+ return {
19
+ name: strategy.name,
20
+ ...(strategy.maxNodes === undefined ? {} : { maxNodes: strategy.maxNodes }),
21
+ route: (graph: Graph): Result => follow(graph, strategy.run({ graph }, options), { ...followOptions, ...wayOptionsOf({ ...strategy.defaults, ...options }) }),
22
+ };
23
+ }
24
+
25
+ /**
26
+ * This function gives a router strategy that runs a highway strategy and then `follow`.
27
+ * The router strategy has the name, the options and the schema of the highway strategy.
28
+ * The benchmark and the screen race such a router with the other routers.
29
+ */
30
+ export function highwayRouterStrategy<O extends object>(strategy: Strategy<"highway", O>): Strategy<"router", O> {
31
+ return defineStrategy<"router", O>({
32
+ step: "router",
33
+ name: strategy.name,
34
+ describe: strategy.describe,
35
+ defaults: strategy.defaults as O,
36
+ ...(strategy.schema === undefined ? {} : { schema: strategy.schema }),
37
+ ...(strategy.maxNodes === undefined ? {} : { maxNodes: strategy.maxNodes }),
38
+ run: (graph: Graph, options: Readonly<O>): Result => follow(graph, strategy.run({ graph }, options), wayOptionsOf(options)),
39
+ });
40
+ }
@@ -0,0 +1,15 @@
1
+ import { sharedRouter, type SharedOptions } from "../../routers/shared.ts";
2
+ import { planOfResult } from "../result.ts";
3
+ import type { HighwayInput, HighwayPlan } from "../model.ts";
4
+
5
+ /** The options of the highway strategy `shared-ways`: the options of the router `shared`, without the name and the limit of the benchmark. */
6
+ export type SharedWaysOptions = Omit<SharedOptions, "name" | "maxNodes">;
7
+
8
+ /**
9
+ * This function gives the highways that the router `shared` decides. The edges of one key go one by one through the corner graph.
10
+ * A link that the key uses already costs less, so the ways of a key come together. Each stretch that two edges or more share becomes one highway.
11
+ * The function does not change the router: it reads its runs.
12
+ */
13
+ export function sharedWaysPlan(input: HighwayInput, options: SharedWaysOptions): HighwayPlan {
14
+ return planOfResult(sharedRouter({ ...options, name: "shared-ways" }).route(input.graph));
15
+ }
package/src/index.ts CHANGED
@@ -27,3 +27,7 @@ export * from "./bench/measures/index.ts";
27
27
  export * from "./bench/suite.ts";
28
28
  export * from "./strategies/index.ts";
29
29
  export * from "./bench/compare.ts";
30
+ export * from "./bench/screen/index.ts";
31
+ export * from "./highways/index.ts";
32
+ export * from "./order/index.ts";
33
+ export * from "./repair/index.ts";
@@ -0,0 +1,65 @@
1
+ import { fnv1a32 } from "../bench/screen/hash.ts";
2
+ import { rng } from "../fixtures.ts";
3
+ import type { Edge, Graph, Result, Router } from "../model.ts";
4
+ import { defineStrategy, type Strategy } from "../strategies/model.ts";
5
+
6
+ /** This function compares two edges by the hash of their ids, and then by their ids. Thus the order of two edges does not depend on the input. */
7
+ function byHash(a: { readonly h: number; readonly e: Edge }, b: { readonly h: number; readonly e: Edge }): number {
8
+ if (a.h !== b.h) return a.h - b.h;
9
+ return a.e.id < b.e.id ? -1 : a.e.id > b.e.id ? 1 : 0;
10
+ }
11
+
12
+ /**
13
+ * This function gives the graph with its edges in the stable order of WILD-053: by the FNV-1a hash of the id, then by the id.
14
+ * The nodes and the edges do not change. Two orders of the same edges give the same graph.
15
+ * The hash spreads the edges, so the stable order does not favor the edges of one node or one collection.
16
+ */
17
+ export function stableOrder(graph: Graph): Graph {
18
+ const edges = graph.edges.map((e) => ({ h: fnv1a32(e.id), e })).sort(byHash).map((x) => x.e);
19
+ return { ...graph, edges };
20
+ }
21
+
22
+ /** This function gives the graph with its edges in a random order of the seed. The same graph and seed always give the same order. */
23
+ export function shuffleEdges(graph: Graph, seed: number): Graph {
24
+ const edges = [...graph.edges];
25
+ const r = rng(seed);
26
+ for (let i = edges.length - 1; i > 0; i--) {
27
+ const j = Math.floor(r() * (i + 1));
28
+ [edges[i], edges[j]] = [edges[j]!, edges[i]!];
29
+ }
30
+ return { ...graph, edges };
31
+ }
32
+
33
+ /**
34
+ * This function routes the graph in the stable order of `stableOrder`, with the function `route`.
35
+ * The routes come back in the order of the input edges, so the caller sees no change of order.
36
+ * The bundles, the runs and the levels come from the stable order. Thus each order of the input gives the same data.
37
+ */
38
+ export function routeInStableOrder(graph: Graph, route: (graph: Graph) => Result): Result {
39
+ const result = route(stableOrder(graph));
40
+ const index = new Map(graph.edges.map((e, i) => [e.id, i] as const));
41
+ const at = (edge: string): number => index.get(edge) ?? Number.MAX_SAFE_INTEGER;
42
+ const routes = [...result.routes].sort((a, b) => at(a.edge) - at(b.edge));
43
+ return { ...result, routes };
44
+ }
45
+
46
+ /** This function gives a router that routes each graph in the stable order of WILD-053. It keeps the name and the `maxNodes` of the router. */
47
+ export function withStableOrder(router: Router): Router {
48
+ return { ...router, route: (graph: Graph): Result => routeInStableOrder(graph, (g) => router.route(g)) };
49
+ }
50
+
51
+ /**
52
+ * This function gives a router strategy that runs `strategy` in the stable order of WILD-053. The output does not depend on the order of the input edges.
53
+ * The new strategy has the name `<name>+stable`, and the options and the schema of `strategy`. The strategy `strategy` does not change.
54
+ */
55
+ export function stableOrderStrategy<O extends object>(strategy: Strategy<"router", O>): Strategy<"router", O> {
56
+ return defineStrategy<"router", O>({
57
+ step: "router",
58
+ name: `${strategy.name}+stable`,
59
+ describe: `${strategy.describe} The edges go in a stable order: by a hash of their ids.`,
60
+ defaults: { ...strategy.defaults } as O,
61
+ ...(strategy.schema === undefined ? {} : { schema: strategy.schema }),
62
+ ...(strategy.maxNodes === undefined ? {} : { maxNodes: strategy.maxNodes }),
63
+ run: (graph: Graph, options: Readonly<O>): Result => routeInStableOrder(graph, (g) => strategy.run(g, options)),
64
+ });
65
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Plug justification (WILD-002, with NAT-031): the plugs of one side keep their order and a minimum gap.
3
+ * They move the least distance from their ideal places.
4
+ */
5
+
6
+ /**
7
+ * This function places `ideal.length` points on the segment from `lo` to `hi` of one side.
8
+ * The points keep the order of their ideal places. Two neighbours are at least `gap` apart, or the side divided evenly when it is too short.
9
+ * The sum of the squares of the moves is the least. The answer is exact: a fit by pool adjacent violators.
10
+ *
11
+ * The function `tie` orders two points with the same ideal place. The result is in the order of `ideal`.
12
+ */
13
+ export function justifyPlugs(ideal: readonly number[], lo: number, hi: number, gap: number, tie?: (i: number) => number): number[] {
14
+ const k = ideal.length;
15
+ if (k === 0) return [];
16
+ const order = ideal.map((_, i) => i).sort((a, b) => ideal[a]! - ideal[b]! || (tie ? tie(a) - tie(b) : 0) || a - b);
17
+ const s = k > 1 ? Math.max(0, Math.min(gap, (hi - lo) / (k - 1))) : 0;
18
+ // With y_i = x_i - i·s, the gap becomes the rule y_i <= y_(i+1): an isotonic fit of a_i - i·s.
19
+ const y = isotonic(order.map((j, i) => ideal[j]! - i * s));
20
+ const top = Math.max(lo, hi - (k - 1) * s);
21
+ const out = new Array<number>(k);
22
+ order.forEach((j, i) => {
23
+ out[j] = Math.max(lo, Math.min(top, y[i]!)) + i * s;
24
+ });
25
+ return out;
26
+ }
27
+
28
+ /** This function gives the non-decreasing sequence nearest to `v` in the sum of squares (pool adjacent violators). */
29
+ export function isotonic(v: readonly number[]): number[] {
30
+ const mean: number[] = [];
31
+ const size: number[] = [];
32
+ for (const x of v) {
33
+ mean.push(x);
34
+ size.push(1);
35
+ while (mean.length > 1 && mean[mean.length - 2]! > mean[mean.length - 1]!) {
36
+ const m = mean.pop()!;
37
+ const n = size.pop()!;
38
+ const i = mean.length - 1;
39
+ mean[i] = (mean[i]! * size[i]! + m * n) / (size[i]! + n);
40
+ size[i]! += n;
41
+ }
42
+ }
43
+ const out: number[] = [];
44
+ mean.forEach((m, i) => {
45
+ for (let j = 0; j < size[i]!; j++) out.push(m);
46
+ });
47
+ return out;
48
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Free plugs on a side (WILD-001): the search of a route starts from the whole free side, not from one point of it.
3
+ * Thus the first leg goes straight out to the first corner of the way, and the plug is the foot of that leg.
4
+ */
5
+ import { center, dist, type Box, type Point } from "../geom.ts";
6
+ import { fixedPortPoint, type End, type Node, type Side } from "../model.ts";
7
+ import { placePlug, pointOnSide, SIDE_NORMAL, sideToward, type Plug } from "../plugs.ts";
8
+ import type { Terminal } from "../visibility.ts";
9
+ import type { WayFinder } from "../ways.ts";
10
+
11
+ /** The options of `sidePlugs`. */
12
+ export interface SidePlugOptions {
13
+ /** The cost of a slant first leg: the length of the leg grows by `bend` × its angle to the normal, in radians. The default is 1. */
14
+ readonly bend?: number;
15
+ /** The distance from a plug to a corner of its box. The default is 6, as in `pointOnSide`. */
16
+ readonly inset?: number;
17
+ /** The largest number of places that the search tries on each side. The default is 16. */
18
+ readonly most?: number;
19
+ }
20
+
21
+ /** A free side of one end: its node, its side and its range on the axis along the side. It also has the plug that faces the other end. */
22
+ interface FreeSide {
23
+ readonly node: Node;
24
+ readonly side: Side;
25
+ readonly normal: Point;
26
+ /** The axis along the side. */
27
+ readonly along: "x" | "y";
28
+ /** The place of the side on the other axis. */
29
+ readonly at: number;
30
+ readonly lo: number;
31
+ readonly hi: number;
32
+ readonly facing: Plug;
33
+ }
34
+
35
+ /** A place that the search tries: the plug, and the free point where the search starts from it, with its cost. */
36
+ interface Start extends Terminal {
37
+ readonly plug: Plug;
38
+ }
39
+
40
+ /** This function gives the free side of an end, or `undefined` when the plug of the end is fixed. */
41
+ function freeSide(node: Node, end: End, toward: Point, inset: number): FreeSide | undefined {
42
+ if (fixedPortPoint(node, end)) return undefined;
43
+ const port = end.port === undefined ? undefined : node.ports?.find((p) => p.id === end.port);
44
+ const side = port?.side ?? sideToward(node.box, toward);
45
+ const b: Box = node.box;
46
+ const along = side === "east" || side === "west" ? "y" : "x";
47
+ const start = along === "y" ? b.y : b.x;
48
+ const len = along === "y" ? b.h : b.w;
49
+ const m = Math.min(inset, len / 4);
50
+ const at = side === "east" ? b.x + b.w : side === "west" ? b.x : side === "south" ? b.y + b.h : b.y;
51
+ const point = pointOnSide(b, side, toward, inset);
52
+ return { node, side, normal: SIDE_NORMAL[side], along, at, lo: start + m, hi: start + len - m, facing: { point, normal: SIDE_NORMAL[side] } };
53
+ }
54
+
55
+ /** This function gives the point of the side at `v` on the axis along the side. */
56
+ function footAt(f: FreeSide, v: number): Point {
57
+ return f.along === "y" ? { x: f.at, y: v } : { x: v, y: f.at };
58
+ }
59
+
60
+ /**
61
+ * This function gives the places that the search tries on one free side. A place is the out point of a plug on the side.
62
+ * A place is also a corner in front of the side. The plug of a corner is the foot of the leg to it.
63
+ */
64
+ function startsOf(ways: WayFinder, f: FreeSide, others: readonly number[], bend: number, most: number): Start[] {
65
+ const margin = ways.margin;
66
+ const pad = margin / 2;
67
+ const own = new Set<Node>([f.node]);
68
+ const starts: Start[] = [];
69
+ const out = (p: Point): Point => ({ x: p.x + f.normal.x * margin, y: p.y + f.normal.y * margin });
70
+ const plugAt = (v: number): Plug => ({ point: footAt(f, v), normal: f.normal });
71
+ // The plug that faces the other end is always a place of the search, so the search is never worse than the default.
72
+ starts.push({ plug: f.facing, point: out(f.facing.point), cost: margin });
73
+ const mid = (f.lo + f.hi) / 2;
74
+ for (const v of [f.lo, mid, f.hi, ...others]) {
75
+ if (v < f.lo - 1e-9 || v > f.hi + 1e-9) continue;
76
+ const p = out(footAt(f, v));
77
+ if (!ways.obstacles.inside(p, 0, f.node)) starts.push({ plug: plugAt(v), point: p, cost: margin });
78
+ }
79
+ // The corners in front of the side that its out points see.
80
+ const seen = new Set<number>();
81
+ for (const v of [f.lo, mid, f.hi, f.facing.point[f.along]]) for (const c of ways.corners.nearestVisible(out(footAt(f, v)), 16)) seen.add(c);
82
+ for (const c of seen) {
83
+ const q = ways.corners.points[c]!;
84
+ const across = f.along === "y" ? "x" : "y";
85
+ const depth = (q[across] - f.at) * f.normal[across];
86
+ if (depth < margin - 1e-9) continue;
87
+ const v = Math.max(f.lo, Math.min(f.hi, q[f.along]));
88
+ const foot = footAt(f, v);
89
+ const angle = Math.atan2(Math.abs(q[f.along] - v), depth);
90
+ if (angle > Math.PI / 4 + 1e-9) continue;
91
+ if (!ways.obstacles.clear(foot, q, pad, own)) continue;
92
+ starts.push({ plug: plugAt(v), point: q, cost: dist(foot, q) * (1 + bend * angle) });
93
+ }
94
+ // One place for each foot: the cheapest. The places nearest to the facing plug come first.
95
+ const best = new Map<number, Start>();
96
+ for (const s of starts) {
97
+ const key = Math.round(s.plug.point[f.along] * 2);
98
+ const old = best.get(key);
99
+ if (!old || s.cost < old.cost) best.set(key, s);
100
+ }
101
+ const facing = f.facing.point[f.along];
102
+ const list = [...best.values()].sort((a, b) => Math.abs(a.plug.point[f.along] - facing) - Math.abs(b.plug.point[f.along] - facing));
103
+ return list.slice(0, Math.max(1, most));
104
+ }
105
+
106
+ /**
107
+ * This function places the two plugs of an edge with a free side. A fixed plug stays at its point.
108
+ * The search (A*) starts from each place of the free side of the source. It ends at each place of the free side of the target.
109
+ * The plugs are the places of the shortest way. Without a way, the plugs face the other end, as `placePlug` places them.
110
+ */
111
+ export function sidePlugs(ways: WayFinder, s: Node, es: End, t: Node, et: End, options: SidePlugOptions = {}): [Plug, Plug] {
112
+ const inset = options.inset ?? 6;
113
+ const ps = placePlug(s, es, center(t.box));
114
+ const pt = placePlug(t, et, center(s.box));
115
+ if (s === t) return [ps, pt];
116
+ const fs = freeSide(s, es, center(t.box), inset);
117
+ const ft = freeSide(t, et, center(s.box), inset);
118
+ if (!fs && !ft) return [ps, pt];
119
+ const bend = options.bend ?? 1;
120
+ const most = options.most ?? 16;
121
+ const fixed = (p: Plug): Start[] => [{ plug: p, point: ways.out(p), cost: ways.margin }];
122
+ // Two sides that face each other on one axis share the places of a straight line.
123
+ const facingPair = fs && ft && fs.along === ft.along && fs.normal.x === -ft.normal.x && fs.normal.y === -ft.normal.y;
124
+ const shared = (a: FreeSide | undefined, b: FreeSide | undefined, p: Plug): number[] => {
125
+ if (!a) return [];
126
+ if (facingPair && b) {
127
+ const lo = Math.max(a.lo, b.lo);
128
+ const hi = Math.min(a.hi, b.hi);
129
+ return lo <= hi ? [lo, (lo + hi) / 2, hi] : [];
130
+ }
131
+ return [p.point[a.along]];
132
+ };
133
+ const from = fs ? startsOf(ways, fs, shared(fs, ft, pt), bend, most) : fixed(ps);
134
+ const to = ft ? startsOf(ways, ft, shared(ft, fs, ps), bend, most) : fixed(pt);
135
+ const way = ways.corners.pathAmong(from, to);
136
+ if (!way) return [ps, pt];
137
+ return [from[way.from]!.plug, to[way.to]!.plug];
138
+ }