@mark1russell7/highways 0.7.0 → 0.8.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.
- package/README.md +11 -0
- package/build/index.d.ts +5 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +173 -87
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bench/exact/index.ts +4 -0
- package/src/bench/properties.ts +8 -2
- package/src/compare.ts +17 -0
- package/src/highways/stretches.ts +28 -7
- package/src/lane-order.ts +152 -0
- package/src/order/index.ts +2 -7
- package/src/routers/shared.ts +29 -15
- package/src/segments.ts +12 -74
package/package.json
CHANGED
package/src/bench/exact/index.ts
CHANGED
|
@@ -283,9 +283,13 @@ function distancesTo(points: readonly Point[], adj: readonly (readonly { to: num
|
|
|
283
283
|
const done = new Uint8Array(adj.length);
|
|
284
284
|
const goal = new Set(targets);
|
|
285
285
|
const ends = targets.map((t) => points[t]!);
|
|
286
|
+
// A node can go in the heap more than one time. Each node gets its guide one time (ROUTE-SCALE), so a search to many targets stays fast.
|
|
287
|
+
const guides = new Float64Array(adj.length).fill(-1);
|
|
286
288
|
const guide = (v: number): number => {
|
|
289
|
+
if (guides[v]! >= 0) return guides[v]!;
|
|
287
290
|
let h = Infinity;
|
|
288
291
|
for (const t of ends) h = Math.min(h, dist(points[v]!, t));
|
|
292
|
+
guides[v] = h;
|
|
289
293
|
return h;
|
|
290
294
|
};
|
|
291
295
|
const heap = new NodeHeap();
|
package/src/bench/properties.ts
CHANGED
|
@@ -83,9 +83,11 @@ export function checkProperties(graph: Graph, result: Result, limit = 100): Viol
|
|
|
83
83
|
for (const run of result.runs ?? []) {
|
|
84
84
|
if (run.edges.length < 2)
|
|
85
85
|
if (add({ rule: "run-shared", detail: `run ${run.id} has fewer than two edges` })) return out;
|
|
86
|
+
// The run gets its reverse one time, not one time for each edge (ROUTE-SCALE).
|
|
87
|
+
let back: readonly Point[] | undefined;
|
|
86
88
|
for (const id of run.edges) {
|
|
87
89
|
const r = routes.get(id);
|
|
88
|
-
if (!r || !(holds(r.points, run.points) || holds(r.points, [...run.points].reverse())))
|
|
90
|
+
if (!r || !(holds(r.points, run.points) || holds(r.points, (back ??= [...run.points].reverse()))))
|
|
89
91
|
if (add({ rule: "run-shared", edge: id, detail: `run ${run.id}: the route does not hold the points of the run in a row` })) return out;
|
|
90
92
|
}
|
|
91
93
|
}
|
|
@@ -139,6 +141,10 @@ function onBoundary(p: Point, b: Box): boolean {
|
|
|
139
141
|
/** This function tells if the polyline `p` holds the points of `part` in a row. */
|
|
140
142
|
function holds(p: readonly Point[], part: readonly Point[]): boolean {
|
|
141
143
|
if (part.length === 0) return true;
|
|
142
|
-
for (let i = 0; i + part.length <= p.length; i++)
|
|
144
|
+
for (let i = 0; i + part.length <= p.length; i++) {
|
|
145
|
+
let j = 0;
|
|
146
|
+
while (j < part.length && dist(part[j]!, p[i + j]!) <= TOLERANCE) j++;
|
|
147
|
+
if (j === part.length) return true;
|
|
148
|
+
}
|
|
143
149
|
return false;
|
|
144
150
|
}
|
package/src/compare.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The named comparators of the core (LANE-ORDER). `comparators.test.ts` checks that each one is a total order.
|
|
2
|
+
// The package index does not export this module.
|
|
3
|
+
|
|
4
|
+
/** This function compares two texts by their UTF-16 code units. It gives 0 only for equal texts. */
|
|
5
|
+
export const byText = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
|
|
6
|
+
|
|
7
|
+
/** An edge id with its hash. */
|
|
8
|
+
export interface Hashed {
|
|
9
|
+
readonly h: number;
|
|
10
|
+
readonly e: { readonly id: string };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 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. */
|
|
14
|
+
export function byHash(a: Hashed, b: Hashed): number {
|
|
15
|
+
if (a.h !== b.h) return a.h - b.h;
|
|
16
|
+
return byText(a.e.id, b.e.id);
|
|
17
|
+
}
|
|
@@ -77,17 +77,35 @@ export function sharedStretches(ways: readonly KeyedWay[], o: Readonly<SharedStr
|
|
|
77
77
|
readonly key: string;
|
|
78
78
|
readonly points: readonly Point[];
|
|
79
79
|
readonly members: { edge: string; forward: boolean; at: number }[];
|
|
80
|
+
/** The edges of `members`, so that the check of a member does not scan the list. */
|
|
81
|
+
readonly seen: Set<string>;
|
|
80
82
|
}
|
|
81
83
|
const stretches = new Map<string, Stretch>();
|
|
82
84
|
const wayOf = new Map<string, readonly Point[]>();
|
|
85
|
+
// The signature of each link (ROUTE-SCALE): 0 for a link with fewer than `min` ways, else the number of the sorted list of its ways.
|
|
86
|
+
// Each link gets its signature one time. Two links get the same number only when they have the same sorted list, so the stretches do not change.
|
|
87
|
+
const texts = new Map<string, number>();
|
|
88
|
+
const sigs = new Map<string, number>();
|
|
89
|
+
const sigOf = (k: string): number => {
|
|
90
|
+
let n = sigs.get(k);
|
|
91
|
+
if (n !== undefined) return n;
|
|
92
|
+
const list = users.get(k) ?? [];
|
|
93
|
+
if (list.length < min) n = 0;
|
|
94
|
+
else {
|
|
95
|
+
const text = [...list].sort().join("\u0001");
|
|
96
|
+
n = texts.get(text);
|
|
97
|
+
if (n === undefined) texts.set(text, (n = texts.size + 1));
|
|
98
|
+
}
|
|
99
|
+
sigs.set(k, n);
|
|
100
|
+
return n;
|
|
101
|
+
};
|
|
83
102
|
for (const w of clean) {
|
|
84
103
|
wayOf.set(w.edge, w.points);
|
|
85
|
-
const sig = (i: number):
|
|
104
|
+
const sig = (i: number): number => {
|
|
86
105
|
const a = w.points[i]!;
|
|
87
106
|
const b = w.points[i + 1]!;
|
|
88
|
-
if (!usable(a, b)) return
|
|
89
|
-
|
|
90
|
-
return list.length < min ? "" : [...list].sort().join("\u0001");
|
|
107
|
+
if (!usable(a, b)) return 0;
|
|
108
|
+
return sigOf(`${w.key}\u0000${linkKey(pointKey(a), pointKey(b))}`);
|
|
91
109
|
};
|
|
92
110
|
let start = 0;
|
|
93
111
|
for (let i = 0; i < w.points.length - 1; i++) {
|
|
@@ -95,15 +113,18 @@ export function sharedStretches(ways: readonly KeyedWay[], o: Readonly<SharedStr
|
|
|
95
113
|
if (i + 1 < w.points.length - 1 && sig(i + 1) === s) continue;
|
|
96
114
|
const from = start;
|
|
97
115
|
start = i + 1;
|
|
98
|
-
if (s ===
|
|
116
|
+
if (s === 0) continue;
|
|
99
117
|
const a = pointKey(w.points[from]!);
|
|
100
118
|
const b = pointKey(w.points[i + 1]!);
|
|
101
119
|
if (a === b) continue;
|
|
102
120
|
const id = `${w.key}\u0000${s}\u0000${linkKey(a, b)}`;
|
|
103
121
|
const piece = w.points.slice(from, i + 2);
|
|
104
122
|
let found = stretches.get(id);
|
|
105
|
-
if (!found) stretches.set(id, (found = { key: w.key, points: a < b ? piece : [...piece].reverse(), members: [] }));
|
|
106
|
-
if (!found.
|
|
123
|
+
if (!found) stretches.set(id, (found = { key: w.key, points: a < b ? piece : [...piece].reverse(), members: [], seen: new Set() }));
|
|
124
|
+
if (!found.seen.has(w.edge)) {
|
|
125
|
+
found.seen.add(w.edge);
|
|
126
|
+
found.members.push({ edge: w.edge, forward: a < b, at: from });
|
|
127
|
+
}
|
|
107
128
|
}
|
|
108
129
|
}
|
|
109
130
|
// The order of the stretches of each way gives the `seq` of its members.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// The lane order of a run (LANE-ORDER): the relation of two lanes, and a total order from it. `orderLanes` of `segments.ts` uses this module.
|
|
2
|
+
// The package index does not export this module.
|
|
3
|
+
import { byText } from "./compare.ts";
|
|
4
|
+
import { dist, type Point } from "./geom.ts";
|
|
5
|
+
|
|
6
|
+
/** Two points nearer than this distance are the same point. The value is the same as in `segments.ts`. */
|
|
7
|
+
const SAME = 1e-6;
|
|
8
|
+
|
|
9
|
+
/** The place of a route on a run. The points go in the direction of the run. `s` and `e` are the indexes of the ends of the run. */
|
|
10
|
+
export interface Placed {
|
|
11
|
+
readonly edge: string;
|
|
12
|
+
readonly p: readonly Point[];
|
|
13
|
+
readonly s: number;
|
|
14
|
+
readonly e: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The place where two routes part past one end of their common stretch, and the side of the first route there. */
|
|
18
|
+
interface Parting {
|
|
19
|
+
/** The last common point. */
|
|
20
|
+
readonly at: Point;
|
|
21
|
+
/** This value is below 0 when the first route goes to the left of the second route. It is above 0 when it goes to the right. */
|
|
22
|
+
readonly side: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The angle, from -π to π, of the turn at `base` from the direction `from` → `base` to the point `to`. */
|
|
26
|
+
function turn(from: Point, base: Point, to: Point): number {
|
|
27
|
+
const dx = base.x - from.x;
|
|
28
|
+
const dy = base.y - from.y;
|
|
29
|
+
const vx = to.x - base.x;
|
|
30
|
+
const vy = to.y - base.y;
|
|
31
|
+
return Math.atan2(dx * vy - dy * vx, dx * vx + dy * vy);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The routes of one run often hold the same point objects past the run. A point is at the distance 0 from itself,
|
|
35
|
+
// so the check `a !== b` skips the distance and keeps each result (ROUTE-SCALE).
|
|
36
|
+
|
|
37
|
+
/** This function gives the place where the routes `P` and `Q` part after the last point of the run, or nothing. */
|
|
38
|
+
function partAfter(P: Placed, Q: Placed): Parting | undefined {
|
|
39
|
+
for (let j = 1; ; j++) {
|
|
40
|
+
const a = P.p[P.e + j];
|
|
41
|
+
const b = Q.p[Q.e + j];
|
|
42
|
+
if (a === undefined || b === undefined) return undefined;
|
|
43
|
+
if (a !== b && dist(a, b) > SAME) {
|
|
44
|
+
const from = P.p[P.e + j - 2]!;
|
|
45
|
+
const at = P.p[P.e + j - 1]!;
|
|
46
|
+
const side = turn(from, at, a) - turn(from, at, b);
|
|
47
|
+
return side === 0 ? undefined : { at, side };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** This function gives the place where the routes `P` and `Q` part before the first point of the run, or nothing. */
|
|
53
|
+
function partBefore(P: Placed, Q: Placed): Parting | undefined {
|
|
54
|
+
for (let j = 1; ; j++) {
|
|
55
|
+
const a = P.p[P.s - j];
|
|
56
|
+
const b = Q.p[Q.s - j];
|
|
57
|
+
if (a === undefined || b === undefined) return undefined;
|
|
58
|
+
if (a !== b && dist(a, b) > SAME) {
|
|
59
|
+
const from = P.p[P.s - j + 2]!;
|
|
60
|
+
const at = P.p[P.s - j + 1]!;
|
|
61
|
+
// Seen backward, the left side is the right side. Thus the order turns around.
|
|
62
|
+
const side = turn(from, at, b) - turn(from, at, a);
|
|
63
|
+
return side === 0 ? undefined : { at, side };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** This function compares two placed routes by their edge ids. */
|
|
69
|
+
export const byEdge = (P: Placed, Q: Placed): number => byText(P.edge, Q.edge);
|
|
70
|
+
|
|
71
|
+
/** This function puts the point `a` before the point `b`: the smaller x first, then the smaller y. */
|
|
72
|
+
const firstPoint = (a: Point, b: Point): boolean => a.x < b.x - SAME || (Math.abs(a.x - b.x) <= SAME && a.y < b.y);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* This function is the relation of the order `run`. The side after the run tells the order first, then the side before the run, then the edges.
|
|
76
|
+
* A value below 0 puts `P` before `Q`. The relation is not transitive: three lanes can make a cycle.
|
|
77
|
+
*/
|
|
78
|
+
export const byRun = (P: Placed, Q: Placed): number => partAfter(P, Q)?.side ?? partBefore(P, Q)?.side ?? byEdge(P, Q);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* This function is the relation of the order `bundle`. When the two ends disagree, the end with the smaller x, then the smaller y, tells the side.
|
|
82
|
+
* A value below 0 puts `P` before `Q`. The relation is not transitive: three lanes can make a cycle.
|
|
83
|
+
*/
|
|
84
|
+
export const byBundle = (P: Placed, Q: Placed): number => {
|
|
85
|
+
const after = partAfter(P, Q);
|
|
86
|
+
const before = partBefore(P, Q);
|
|
87
|
+
if (after && before && Math.sign(after.side) !== Math.sign(before.side)) return firstPoint(after.at, before.at) ? after.side : before.side;
|
|
88
|
+
return after?.side ?? before?.side ?? byEdge(P, Q);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* This function sorts a list with a stable merge sort of the package, not with `Array.prototype.sort`.
|
|
93
|
+
* The pairs that it gives to `compare` depend only on the list and on the results of `compare`.
|
|
94
|
+
* Thus the result is the same on each JavaScript engine, also when `compare` is not a total order.
|
|
95
|
+
* A value of `compare(a, b)` above 0 puts `b` before `a`. Any other value (below 0, 0 or NaN) keeps `a` first.
|
|
96
|
+
*/
|
|
97
|
+
export function mergeSort<T>(list: readonly T[], compare: (a: T, b: T) => number): T[] {
|
|
98
|
+
if (list.length < 2) return [...list];
|
|
99
|
+
const mid = list.length >> 1;
|
|
100
|
+
const left = mergeSort(list.slice(0, mid), compare);
|
|
101
|
+
const right = mergeSort(list.slice(mid), compare);
|
|
102
|
+
const out: T[] = [];
|
|
103
|
+
let i = 0;
|
|
104
|
+
let j = 0;
|
|
105
|
+
while (i < left.length && j < right.length) {
|
|
106
|
+
if (compare(left[i]!, right[j]!) > 0) out.push(right[j++]!);
|
|
107
|
+
else out.push(left[i++]!);
|
|
108
|
+
}
|
|
109
|
+
while (i < left.length) out.push(left[i++]!);
|
|
110
|
+
while (j < right.length) out.push(right[j++]!);
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* This function gives true when the relation of `n` items has a cycle. For `i < j`, a value of `before(i, j)` above 0 puts `j` before `i`.
|
|
116
|
+
* Any other value puts `i` before `j`. The relation has no cycle when the counts of the items that each item comes before are all different.
|
|
117
|
+
*/
|
|
118
|
+
export function hasCycle(n: number, before: (i: number, j: number) => number): boolean {
|
|
119
|
+
const wins = new Int32Array(n);
|
|
120
|
+
for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) wins[before(i, j) > 0 ? j : i]!++;
|
|
121
|
+
const seen = new Uint8Array(n);
|
|
122
|
+
for (const w of wins) seen[w] = 1;
|
|
123
|
+
return seen.includes(0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The function that `observeLanes` installs. It gets the lanes of each run in their start order, the relation, and the lane order.
|
|
128
|
+
* The measure scripts use it to compare the lane order with the sort of the engine.
|
|
129
|
+
*/
|
|
130
|
+
export type LaneObserver = <T>(start: readonly T[], compare: (a: T, b: T) => number, lanes: readonly T[]) => void;
|
|
131
|
+
|
|
132
|
+
let observer: LaneObserver | undefined;
|
|
133
|
+
|
|
134
|
+
/** This function installs an observer of the lane orders, or removes it with `undefined`. The measure scripts use it. The routes do not change. */
|
|
135
|
+
export function observeLanes(fn: LaneObserver | undefined): void {
|
|
136
|
+
observer = fn;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** This function gives the lane order of a run: the merge sort of `start` with the relation `compare`. It tells the observer of `observeLanes`. */
|
|
140
|
+
export function orderRun<T>(start: readonly T[], compare: (a: T, b: T) => number): T[] {
|
|
141
|
+
const lanes = mergeSort(start, compare);
|
|
142
|
+
observer?.(start, compare, lanes);
|
|
143
|
+
return lanes;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* This function gives the lane order of the placed routes of a run. `order` selects the relation.
|
|
148
|
+
* With `run`, the sort starts from the order of `placed`. With `bundle`, it starts from the order of the edges, so that each run sorts the same edges the same way.
|
|
149
|
+
*/
|
|
150
|
+
export function orderPlaced(placed: readonly Placed[], order: "run" | "bundle"): Placed[] {
|
|
151
|
+
return order === "run" ? orderRun(placed, byRun) : orderRun([...placed].sort(byEdge), byBundle);
|
|
152
|
+
}
|
package/src/order/index.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { fnv1a32 } from "../bench/screen/hash.ts";
|
|
2
|
+
import { byHash } from "../compare.ts";
|
|
2
3
|
import { rng } from "../fixtures.ts";
|
|
3
|
-
import type {
|
|
4
|
+
import type { Graph, Result, Router } from "../model.ts";
|
|
4
5
|
import { defineStrategy, type Strategy } from "../strategies/model.ts";
|
|
5
6
|
|
|
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
7
|
/**
|
|
13
8
|
* 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
9
|
* The nodes and the edges do not change. Two orders of the same edges give the same graph.
|
package/src/routers/shared.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { center, dist, type Point } from "../geom.ts";
|
|
2
2
|
import { nodeIndex, type Edge, type Graph, type GroupRule, type Result, type Route, type Router, type SharedRun } from "../model.ts";
|
|
3
3
|
import { channelWidth } from "../channels.ts";
|
|
4
|
+
import { orderRun } from "../lane-order.ts";
|
|
5
|
+
import { byText } from "../compare.ts";
|
|
4
6
|
import { placePlug, type Plug } from "../plugs.ts";
|
|
5
7
|
import { buildSections, type Section } from "../sections.ts";
|
|
6
8
|
import { orderLanes, type RunOrder } from "../segments.ts";
|
|
@@ -85,8 +87,6 @@ interface Run {
|
|
|
85
87
|
readonly ways: Way[];
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
const byText = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
|
|
89
|
-
|
|
90
90
|
/** The angle, from -π to π, of the turn at `base` from the direction `from` → `base` to the point `to`. */
|
|
91
91
|
function turn(from: Point, base: Point, to: Point): number {
|
|
92
92
|
const dx = base.x - from.x;
|
|
@@ -238,20 +238,38 @@ export function sharedRouter(options: SharedOptions): Router {
|
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
// The users of each link, with the links to the ends. Two edges from one plug share the link from it.
|
|
241
|
-
const
|
|
241
|
+
const N = corners.points.length + ends.size + 1;
|
|
242
|
+
const pairOf = (a: number, b: number): number => (a < b ? a * N + b : b * N + a);
|
|
243
|
+
const users = new Map<string, Map<number, string[]>>();
|
|
242
244
|
for (const w of order) {
|
|
243
245
|
if (!w.found) continue;
|
|
244
246
|
let mine = users.get(w.key);
|
|
245
247
|
if (!mine) users.set(w.key, (mine = new Map()));
|
|
246
248
|
for (let i = 1; i < w.ids.length; i++) {
|
|
247
|
-
const
|
|
248
|
-
const b = w.ids[i]!;
|
|
249
|
-
const k = a < b ? `${a}:${b}` : `${b}:${a}`;
|
|
249
|
+
const k = pairOf(w.ids[i - 1]!, w.ids[i]!);
|
|
250
250
|
const list = mine.get(k);
|
|
251
251
|
if (list) list.push(w.edge.id);
|
|
252
252
|
else mine.set(k, [w.edge.id]);
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
|
+
// The signature of each link (ROUTE-SCALE): 0 for a link with fewer than 2 users, else the number of the sorted list of its users.
|
|
256
|
+
// Each link gets its signature one time. Two links get the same number only when they have the same sorted list, so the runs do not change.
|
|
257
|
+
const texts = new Map<string, number>();
|
|
258
|
+
const sigs = new Map<string, Map<number, number>>();
|
|
259
|
+
for (const [key, mine] of users) {
|
|
260
|
+
const own = new Map<number, number>();
|
|
261
|
+
for (const [k, list] of mine) {
|
|
262
|
+
if (list.length < 2) {
|
|
263
|
+
own.set(k, 0);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const text = [...list].sort(byText).join("\u0001");
|
|
267
|
+
let n = texts.get(text);
|
|
268
|
+
if (n === undefined) texts.set(text, (n = texts.size + 1));
|
|
269
|
+
own.set(k, n);
|
|
270
|
+
}
|
|
271
|
+
sigs.set(key, own);
|
|
272
|
+
}
|
|
255
273
|
|
|
256
274
|
// Each way breaks into stretches where the set of edges on the links stays the same.
|
|
257
275
|
// A shared stretch loses its needless corners one time, so it has the same points in each of its routes.
|
|
@@ -259,13 +277,8 @@ export function sharedRouter(options: SharedOptions): Router {
|
|
|
259
277
|
const points = new Map<string, Point[]>();
|
|
260
278
|
for (const w of all) {
|
|
261
279
|
if (!w.found) continue;
|
|
262
|
-
const
|
|
263
|
-
const sig = (i: number):
|
|
264
|
-
const a = w.ids[i]!;
|
|
265
|
-
const b = w.ids[i + 1]!;
|
|
266
|
-
const list = used.get(a < b ? `${a}:${b}` : `${b}:${a}`)!;
|
|
267
|
-
return list.length < 2 ? "" : [...list].sort(byText).join("\u0001");
|
|
268
|
-
};
|
|
280
|
+
const own = sigs.get(w.key)!;
|
|
281
|
+
const sig = (i: number): number => own.get(pairOf(w.ids[i]!, w.ids[i + 1]!))!;
|
|
269
282
|
const out: Point[] = [w.ps.point];
|
|
270
283
|
const append = (piece: readonly Point[]): void => {
|
|
271
284
|
for (const p of piece) if (dist(p, out[out.length - 1]!) > 1e-9) out.push(p);
|
|
@@ -275,7 +288,7 @@ export function sharedRouter(options: SharedOptions): Router {
|
|
|
275
288
|
const s = sig(i);
|
|
276
289
|
if (i + 1 < w.seq.length - 1 && sig(i + 1) === s) continue;
|
|
277
290
|
const raw = w.seq.slice(start, i + 2);
|
|
278
|
-
if (s ===
|
|
291
|
+
if (s === 0) append(corners.pull(raw));
|
|
279
292
|
else {
|
|
280
293
|
const a = w.ids[start]!;
|
|
281
294
|
const b = w.ids[i + 1]!;
|
|
@@ -376,5 +389,6 @@ function laneOrderOfRun(run: Run): string[] {
|
|
|
376
389
|
}
|
|
377
390
|
return byText(p.edge.id, q.edge.id);
|
|
378
391
|
};
|
|
379
|
-
|
|
392
|
+
// This relation is not transitive, so the merge sort of the package gives the same order on each engine (LANE-ORDER).
|
|
393
|
+
return orderRun(run.ways, compare).map((w) => w.edge.id);
|
|
380
394
|
}
|
package/src/segments.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dist, type Point } from "./geom.ts";
|
|
2
|
+
import { orderPlaced, type Placed } from "./lane-order.ts";
|
|
2
3
|
import type { Result, Route } from "./model.ts";
|
|
3
4
|
|
|
4
5
|
/** A piece of drawn way. A shared segment carries several edges, in lane order. A private segment carries one edge. */
|
|
@@ -173,6 +174,8 @@ export function laneRoutes(source: Result | SegmentGraph, open: ReadonlySet<stri
|
|
|
173
174
|
/** This function gives the route of each path, with the lanes of each segment at the spacing that `spacingOf` gives. */
|
|
174
175
|
function lanePaths(graph: SegmentGraph, spacingOf: (s: Segment) => number): Route[] {
|
|
175
176
|
const byId = new Map(graph.segments.map((s) => [s.id, s]));
|
|
177
|
+
// Each segment gets its spacing one time (ROUTE-SCALE). A key of `runKey` sorts the edges of the segment, and a busy segment is on many paths.
|
|
178
|
+
const spacings = new Map<Segment, number>();
|
|
176
179
|
const routes: Route[] = [];
|
|
177
180
|
for (const path of graph.paths) {
|
|
178
181
|
const ends = pathPoints(graph, path);
|
|
@@ -180,7 +183,8 @@ function lanePaths(graph: SegmentGraph, spacingOf: (s: Segment) => number): Rout
|
|
|
180
183
|
for (const ref of path.segments) {
|
|
181
184
|
const s = byId.get(ref.segment);
|
|
182
185
|
if (!s) continue;
|
|
183
|
-
|
|
186
|
+
let spacing = spacings.get(s);
|
|
187
|
+
if (spacing === undefined) spacings.set(s, (spacing = spacingOf(s)));
|
|
184
188
|
const moved = offsetPolyline(s.points, ((s.count - 1) / 2 - ref.lane) * spacing);
|
|
185
189
|
for (const q of ref.reverse ? moved.reverse() : moved) if (out.length === 0 || dist(q, out[out.length - 1]!) > SAME) out.push(q);
|
|
186
190
|
}
|
|
@@ -193,15 +197,6 @@ function lanePaths(graph: SegmentGraph, spacingOf: (s: Segment) => number): Rout
|
|
|
193
197
|
return routes;
|
|
194
198
|
}
|
|
195
199
|
|
|
196
|
-
/** The angle, from -π to π, of the turn at `base` from the direction `from` → `base` to the point `to`. */
|
|
197
|
-
function turn(from: Point, base: Point, to: Point): number {
|
|
198
|
-
const dx = base.x - from.x;
|
|
199
|
-
const dy = base.y - from.y;
|
|
200
|
-
const vx = to.x - base.x;
|
|
201
|
-
const vy = to.y - base.y;
|
|
202
|
-
return Math.atan2(dx * vy - dy * vx, dx * vx + dy * vy);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
200
|
/**
|
|
206
201
|
* The lane order of the shared ways of a router.
|
|
207
202
|
* With `bundle`, two edges keep one order on all the shared ways of their common stretch.
|
|
@@ -209,58 +204,6 @@ function turn(from: Point, base: Point, to: Point): number {
|
|
|
209
204
|
*/
|
|
210
205
|
export type RunOrder = "bundle" | "run";
|
|
211
206
|
|
|
212
|
-
/** The place of a route on a run. The points go in the direction of the run. `s` and `e` are the indexes of the ends of the run. */
|
|
213
|
-
interface Placed {
|
|
214
|
-
readonly edge: string;
|
|
215
|
-
readonly p: readonly Point[];
|
|
216
|
-
readonly s: number;
|
|
217
|
-
readonly e: number;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** The place where two routes part past one end of their common stretch, and the side of the first route there. */
|
|
221
|
-
interface Parting {
|
|
222
|
-
/** The last common point. */
|
|
223
|
-
readonly at: Point;
|
|
224
|
-
/** This value is below 0 when the first route goes to the left of the second route. It is above 0 when it goes to the right. */
|
|
225
|
-
readonly side: number;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** This function gives the place where the routes `P` and `Q` part after the last point of the run, or nothing. */
|
|
229
|
-
function partAfter(P: Placed, Q: Placed): Parting | undefined {
|
|
230
|
-
for (let j = 1; ; j++) {
|
|
231
|
-
const a = P.p[P.e + j];
|
|
232
|
-
const b = Q.p[Q.e + j];
|
|
233
|
-
if (a === undefined || b === undefined) return undefined;
|
|
234
|
-
if (dist(a, b) > SAME) {
|
|
235
|
-
const from = P.p[P.e + j - 2]!;
|
|
236
|
-
const at = P.p[P.e + j - 1]!;
|
|
237
|
-
const side = turn(from, at, a) - turn(from, at, b);
|
|
238
|
-
return side === 0 ? undefined : { at, side };
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/** This function gives the place where the routes `P` and `Q` part before the first point of the run, or nothing. */
|
|
244
|
-
function partBefore(P: Placed, Q: Placed): Parting | undefined {
|
|
245
|
-
for (let j = 1; ; j++) {
|
|
246
|
-
const a = P.p[P.s - j];
|
|
247
|
-
const b = Q.p[Q.s - j];
|
|
248
|
-
if (a === undefined || b === undefined) return undefined;
|
|
249
|
-
if (dist(a, b) > SAME) {
|
|
250
|
-
const from = P.p[P.s - j + 2]!;
|
|
251
|
-
const at = P.p[P.s - j + 1]!;
|
|
252
|
-
// Seen backward, the left side is the right side. Thus the order turns around.
|
|
253
|
-
const side = turn(from, at, b) - turn(from, at, a);
|
|
254
|
-
return side === 0 ? undefined : { at, side };
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const byEdge = (P: Placed, Q: Placed): number => (P.edge < Q.edge ? -1 : P.edge > Q.edge ? 1 : 0);
|
|
260
|
-
|
|
261
|
-
/** This function puts the point `a` before the point `b`: the smaller x first, then the smaller y. */
|
|
262
|
-
const firstPoint = (a: Point, b: Point): boolean => a.x < b.x - SAME || (Math.abs(a.x - b.x) <= SAME && a.y < b.y);
|
|
263
|
-
|
|
264
207
|
/**
|
|
265
208
|
* This function gives the lane order of the edges on a shared way `run` (metro-line order).
|
|
266
209
|
* The routes are polylines that hold the points of the run in a row, in either direction. A route that does not hold the run goes last.
|
|
@@ -271,6 +214,11 @@ const firstPoint = (a: Point, b: Point): boolean => a.x < b.x - SAME || (Math.ab
|
|
|
271
214
|
* With the order `bundle`, two routes that part on one side at both ends keep that side.
|
|
272
215
|
* When the two ends disagree, the two routes must cross. The end with the smaller x, then the smaller y, tells the side.
|
|
273
216
|
* Thus each shared way of a common stretch gives two routes the same order, and the crossing is where they part.
|
|
217
|
+
*
|
|
218
|
+
* This relation of two lanes is not transitive, so three lanes can make a cycle. The sort of the engine then gives another order on another engine.
|
|
219
|
+
* Thus the function sorts with a merge sort of the package (LANE-ORDER). The order is the same on each JavaScript engine.
|
|
220
|
+
* On a run with no cycle, the order is the order of each correct sort, for each order of `routes`.
|
|
221
|
+
* On a run with a cycle, the order depends on the order of `routes`. The item LANE-STRETCH is to remove the cycles.
|
|
274
222
|
*/
|
|
275
223
|
export function orderLanes(run: readonly Point[], routes: readonly Route[], order: RunOrder = "run"): string[] {
|
|
276
224
|
const placed: Placed[] = [];
|
|
@@ -285,16 +233,6 @@ export function orderLanes(run: readonly Point[], routes: readonly Route[], orde
|
|
|
285
233
|
else lost.push(r.edge);
|
|
286
234
|
}
|
|
287
235
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
// The sort starts from the order of the edges, so that each run sorts the same edges the same way.
|
|
291
|
-
placed.sort(byEdge);
|
|
292
|
-
placed.sort((P, Q) => {
|
|
293
|
-
const after = partAfter(P, Q);
|
|
294
|
-
const before = partBefore(P, Q);
|
|
295
|
-
if (after && before && Math.sign(after.side) !== Math.sign(before.side)) return firstPoint(after.at, before.at) ? after.side : before.side;
|
|
296
|
-
return after?.side ?? before?.side ?? byEdge(P, Q);
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
return [...placed.map((x) => x.edge), ...lost.sort()];
|
|
236
|
+
const lanes = orderPlaced(placed, order);
|
|
237
|
+
return [...lanes.map((x) => x.edge), ...lost.sort()];
|
|
300
238
|
}
|