@mark1russell7/highways 0.8.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.8.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
  }
@@ -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.
package/src/lane-order.ts CHANGED
@@ -31,13 +31,16 @@ function turn(from: Point, base: Point, to: Point): number {
31
31
  return Math.atan2(dx * vy - dy * vx, dx * vx + dy * vy);
32
32
  }
33
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
+
34
37
  /** This function gives the place where the routes `P` and `Q` part after the last point of the run, or nothing. */
35
38
  function partAfter(P: Placed, Q: Placed): Parting | undefined {
36
39
  for (let j = 1; ; j++) {
37
40
  const a = P.p[P.e + j];
38
41
  const b = Q.p[Q.e + j];
39
42
  if (a === undefined || b === undefined) return undefined;
40
- if (dist(a, b) > SAME) {
43
+ if (a !== b && dist(a, b) > SAME) {
41
44
  const from = P.p[P.e + j - 2]!;
42
45
  const at = P.p[P.e + j - 1]!;
43
46
  const side = turn(from, at, a) - turn(from, at, b);
@@ -52,7 +55,7 @@ function partBefore(P: Placed, Q: Placed): Parting | undefined {
52
55
  const a = P.p[P.s - j];
53
56
  const b = Q.p[Q.s - j];
54
57
  if (a === undefined || b === undefined) return undefined;
55
- if (dist(a, b) > SAME) {
58
+ if (a !== b && dist(a, b) > SAME) {
56
59
  const from = P.p[P.s - j + 2]!;
57
60
  const at = P.p[P.s - j + 1]!;
58
61
  // Seen backward, the left side is the right side. Thus the order turns around.
@@ -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]!;
package/src/segments.ts CHANGED
@@ -174,6 +174,8 @@ export function laneRoutes(source: Result | SegmentGraph, open: ReadonlySet<stri
174
174
  /** This function gives the route of each path, with the lanes of each segment at the spacing that `spacingOf` gives. */
175
175
  function lanePaths(graph: SegmentGraph, spacingOf: (s: Segment) => number): Route[] {
176
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>();
177
179
  const routes: Route[] = [];
178
180
  for (const path of graph.paths) {
179
181
  const ends = pathPoints(graph, path);
@@ -181,7 +183,8 @@ function lanePaths(graph: SegmentGraph, spacingOf: (s: Segment) => number): Rout
181
183
  for (const ref of path.segments) {
182
184
  const s = byId.get(ref.segment);
183
185
  if (!s) continue;
184
- const spacing = spacingOf(s);
186
+ let spacing = spacings.get(s);
187
+ if (spacing === undefined) spacings.set(s, (spacing = spacingOf(s)));
185
188
  const moved = offsetPolyline(s.points, ((s.count - 1) / 2 - ref.lane) * spacing);
186
189
  for (const q of ref.reverse ? moved.reverse() : moved) if (out.length === 0 || dist(q, out[out.length - 1]!) > SAME) out.push(q);
187
190
  }