@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/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "directory": "packages/core"
10
10
  },
11
11
  "homepage": "https://mark1russell7.github.io/highways/",
12
- "version": "0.7.0",
12
+ "version": "0.8.1",
13
13
  "type": "module",
14
14
  "main": "./build/index.js",
15
15
  "types": "./build/index.d.ts",
@@ -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();
@@ -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++) if (part.every((q, j) => dist(q, p[i + j]!) <= TOLERANCE)) return true;
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): string => {
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
- const list = users.get(`${w.key}\u0000${linkKey(pointKey(a), pointKey(b))}`) ?? [];
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 === "") continue;
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.members.some((m) => m.edge === w.edge)) found.members.push({ edge: w.edge, forward: a < b, at: from });
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
+ }
@@ -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 { Edge, Graph, Result, Router } from "../model.ts";
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.
@@ -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 users = new Map<string, Map<string, string[]>>();
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 a = w.ids[i - 1]!;
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 used = users.get(w.key)!;
263
- const sig = (i: number): string => {
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 === "") append(corners.pull(raw));
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
- return [...run.ways].sort(compare).map((w) => w.edge.id);
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
- const spacing = spacingOf(s);
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
- if (order === "run") placed.sort((P, Q) => partAfter(P, Q)?.side ?? partBefore(P, Q)?.side ?? byEdge(P, Q));
289
- else {
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
  }