@mark1russell7/highways 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.0",
13
13
  "type": "module",
14
14
  "main": "./build/index.js",
15
15
  "types": "./build/index.d.ts",
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
+ }
@@ -0,0 +1,149 @@
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
+ /** This function gives the place where the routes `P` and `Q` part after the last point of the run, or nothing. */
35
+ function partAfter(P: Placed, Q: Placed): Parting | undefined {
36
+ for (let j = 1; ; j++) {
37
+ const a = P.p[P.e + j];
38
+ const b = Q.p[Q.e + j];
39
+ if (a === undefined || b === undefined) return undefined;
40
+ if (dist(a, b) > SAME) {
41
+ const from = P.p[P.e + j - 2]!;
42
+ const at = P.p[P.e + j - 1]!;
43
+ const side = turn(from, at, a) - turn(from, at, b);
44
+ return side === 0 ? undefined : { at, side };
45
+ }
46
+ }
47
+ }
48
+
49
+ /** This function gives the place where the routes `P` and `Q` part before the first point of the run, or nothing. */
50
+ function partBefore(P: Placed, Q: Placed): Parting | undefined {
51
+ for (let j = 1; ; j++) {
52
+ const a = P.p[P.s - j];
53
+ const b = Q.p[Q.s - j];
54
+ if (a === undefined || b === undefined) return undefined;
55
+ if (dist(a, b) > SAME) {
56
+ const from = P.p[P.s - j + 2]!;
57
+ const at = P.p[P.s - j + 1]!;
58
+ // Seen backward, the left side is the right side. Thus the order turns around.
59
+ const side = turn(from, at, b) - turn(from, at, a);
60
+ return side === 0 ? undefined : { at, side };
61
+ }
62
+ }
63
+ }
64
+
65
+ /** This function compares two placed routes by their edge ids. */
66
+ export const byEdge = (P: Placed, Q: Placed): number => byText(P.edge, Q.edge);
67
+
68
+ /** This function puts the point `a` before the point `b`: the smaller x first, then the smaller y. */
69
+ const firstPoint = (a: Point, b: Point): boolean => a.x < b.x - SAME || (Math.abs(a.x - b.x) <= SAME && a.y < b.y);
70
+
71
+ /**
72
+ * 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.
73
+ * A value below 0 puts `P` before `Q`. The relation is not transitive: three lanes can make a cycle.
74
+ */
75
+ export const byRun = (P: Placed, Q: Placed): number => partAfter(P, Q)?.side ?? partBefore(P, Q)?.side ?? byEdge(P, Q);
76
+
77
+ /**
78
+ * 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.
79
+ * A value below 0 puts `P` before `Q`. The relation is not transitive: three lanes can make a cycle.
80
+ */
81
+ export const byBundle = (P: Placed, Q: Placed): number => {
82
+ const after = partAfter(P, Q);
83
+ const before = partBefore(P, Q);
84
+ if (after && before && Math.sign(after.side) !== Math.sign(before.side)) return firstPoint(after.at, before.at) ? after.side : before.side;
85
+ return after?.side ?? before?.side ?? byEdge(P, Q);
86
+ };
87
+
88
+ /**
89
+ * This function sorts a list with a stable merge sort of the package, not with `Array.prototype.sort`.
90
+ * The pairs that it gives to `compare` depend only on the list and on the results of `compare`.
91
+ * Thus the result is the same on each JavaScript engine, also when `compare` is not a total order.
92
+ * A value of `compare(a, b)` above 0 puts `b` before `a`. Any other value (below 0, 0 or NaN) keeps `a` first.
93
+ */
94
+ export function mergeSort<T>(list: readonly T[], compare: (a: T, b: T) => number): T[] {
95
+ if (list.length < 2) return [...list];
96
+ const mid = list.length >> 1;
97
+ const left = mergeSort(list.slice(0, mid), compare);
98
+ const right = mergeSort(list.slice(mid), compare);
99
+ const out: T[] = [];
100
+ let i = 0;
101
+ let j = 0;
102
+ while (i < left.length && j < right.length) {
103
+ if (compare(left[i]!, right[j]!) > 0) out.push(right[j++]!);
104
+ else out.push(left[i++]!);
105
+ }
106
+ while (i < left.length) out.push(left[i++]!);
107
+ while (j < right.length) out.push(right[j++]!);
108
+ return out;
109
+ }
110
+
111
+ /**
112
+ * 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`.
113
+ * 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.
114
+ */
115
+ export function hasCycle(n: number, before: (i: number, j: number) => number): boolean {
116
+ const wins = new Int32Array(n);
117
+ for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) wins[before(i, j) > 0 ? j : i]!++;
118
+ const seen = new Uint8Array(n);
119
+ for (const w of wins) seen[w] = 1;
120
+ return seen.includes(0);
121
+ }
122
+
123
+ /**
124
+ * The function that `observeLanes` installs. It gets the lanes of each run in their start order, the relation, and the lane order.
125
+ * The measure scripts use it to compare the lane order with the sort of the engine.
126
+ */
127
+ export type LaneObserver = <T>(start: readonly T[], compare: (a: T, b: T) => number, lanes: readonly T[]) => void;
128
+
129
+ let observer: LaneObserver | undefined;
130
+
131
+ /** This function installs an observer of the lane orders, or removes it with `undefined`. The measure scripts use it. The routes do not change. */
132
+ export function observeLanes(fn: LaneObserver | undefined): void {
133
+ observer = fn;
134
+ }
135
+
136
+ /** This function gives the lane order of a run: the merge sort of `start` with the relation `compare`. It tells the observer of `observeLanes`. */
137
+ export function orderRun<T>(start: readonly T[], compare: (a: T, b: T) => number): T[] {
138
+ const lanes = mergeSort(start, compare);
139
+ observer?.(start, compare, lanes);
140
+ return lanes;
141
+ }
142
+
143
+ /**
144
+ * This function gives the lane order of the placed routes of a run. `order` selects the relation.
145
+ * 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.
146
+ */
147
+ export function orderPlaced(placed: readonly Placed[], order: "run" | "bundle"): Placed[] {
148
+ return order === "run" ? orderRun(placed, byRun) : orderRun([...placed].sort(byEdge), byBundle);
149
+ }
@@ -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;
@@ -376,5 +376,6 @@ function laneOrderOfRun(run: Run): string[] {
376
376
  }
377
377
  return byText(p.edge.id, q.edge.id);
378
378
  };
379
- return [...run.ways].sort(compare).map((w) => w.edge.id);
379
+ // This relation is not transitive, so the merge sort of the package gives the same order on each engine (LANE-ORDER).
380
+ return orderRun(run.ways, compare).map((w) => w.edge.id);
380
381
  }
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. */
@@ -193,15 +194,6 @@ function lanePaths(graph: SegmentGraph, spacingOf: (s: Segment) => number): Rout
193
194
  return routes;
194
195
  }
195
196
 
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
197
  /**
206
198
  * The lane order of the shared ways of a router.
207
199
  * With `bundle`, two edges keep one order on all the shared ways of their common stretch.
@@ -209,58 +201,6 @@ function turn(from: Point, base: Point, to: Point): number {
209
201
  */
210
202
  export type RunOrder = "bundle" | "run";
211
203
 
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
204
  /**
265
205
  * This function gives the lane order of the edges on a shared way `run` (metro-line order).
266
206
  * 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 +211,11 @@ const firstPoint = (a: Point, b: Point): boolean => a.x < b.x - SAME || (Math.ab
271
211
  * With the order `bundle`, two routes that part on one side at both ends keep that side.
272
212
  * When the two ends disagree, the two routes must cross. The end with the smaller x, then the smaller y, tells the side.
273
213
  * Thus each shared way of a common stretch gives two routes the same order, and the crossing is where they part.
214
+ *
215
+ * 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.
216
+ * Thus the function sorts with a merge sort of the package (LANE-ORDER). The order is the same on each JavaScript engine.
217
+ * On a run with no cycle, the order is the order of each correct sort, for each order of `routes`.
218
+ * On a run with a cycle, the order depends on the order of `routes`. The item LANE-STRETCH is to remove the cycles.
274
219
  */
275
220
  export function orderLanes(run: readonly Point[], routes: readonly Route[], order: RunOrder = "run"): string[] {
276
221
  const placed: Placed[] = [];
@@ -285,16 +230,6 @@ export function orderLanes(run: readonly Point[], routes: readonly Route[], orde
285
230
  else lost.push(r.edge);
286
231
  }
287
232
  }
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()];
233
+ const lanes = orderPlaced(placed, order);
234
+ return [...lanes.map((x) => x.edge), ...lost.sort()];
300
235
  }