@mark1russell7/highways 0.4.1 → 0.4.2

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 (36) hide show
  1. package/README.md +27 -1
  2. package/build/index.d.ts +642 -1
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +5189 -2609
  5. package/build/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/bench/measures/highways.ts +93 -0
  8. package/src/bench/measures/index.ts +1 -0
  9. package/src/bench/screen/measures.ts +5 -0
  10. package/src/bench/screen/screen.ts +14 -0
  11. package/src/highways/base.ts +126 -0
  12. package/src/highways/flow-field/index.ts +49 -0
  13. package/src/highways/gates/ambiguity/index.ts +110 -0
  14. package/src/highways/gates/breakeven/index.ts +62 -0
  15. package/src/highways/gates/hysteresis/index.ts +48 -0
  16. package/src/highways/gates/ids/index.ts +59 -0
  17. package/src/highways/gates/index.ts +6 -0
  18. package/src/highways/gates/model.ts +129 -0
  19. package/src/highways/gates/stretch/index.ts +39 -0
  20. package/src/highways/grouped.ts +123 -0
  21. package/src/highways/index.ts +15 -0
  22. package/src/highways/level-links/index.ts +114 -0
  23. package/src/highways/meet/index.ts +84 -0
  24. package/src/highways/model.ts +5 -0
  25. package/src/highways/od-cells/index.ts +50 -0
  26. package/src/highways/quickbundles/index.ts +117 -0
  27. package/src/highways/road-classes/index.ts +74 -0
  28. package/src/highways/router.ts +1 -1
  29. package/src/highways/select/index.ts +162 -0
  30. package/src/highways/stretches.ts +141 -0
  31. package/src/highways/stubs/index.ts +117 -0
  32. package/src/highways/weiszfeld/index.ts +105 -0
  33. package/src/highways/wspd/index.ts +130 -0
  34. package/src/highways/zipper/index.ts +168 -0
  35. package/src/strategies/builtins.ts +149 -7
  36. package/src/strategies/schemas.ts +120 -0
@@ -0,0 +1,129 @@
1
+ import { polylineLength } from "../../geom.ts";
2
+ import type { Graph } from "../../model.ts";
3
+ import { defineStrategy, type Strategy } from "../../strategies/model.ts";
4
+ import type { OptionSchema } from "../../strategies/options.ts";
5
+ import type { WayOptions } from "../../ways.ts";
6
+ import { follow } from "../follow.ts";
7
+ import type { Highway, HighwayInput, HighwayMember, HighwayPlan } from "../model.ts";
8
+ import { wayOptionsOf } from "../router.ts";
9
+
10
+ /**
11
+ * A gate: a rule that keeps, drops or changes the highways of a plan. `gateStrategy` puts a gate after a highway strategy.
12
+ * A gate does not move a spine. It takes members out of a highway, drops a highway or changes the id of a highway.
13
+ */
14
+ export interface Gate<G extends object> {
15
+ /** The name of the gate. The strategy of a gate has the name `<strategy>+<gate>`. */
16
+ readonly name: string;
17
+ /** One or two sentences that tell what the gate does. */
18
+ readonly describe: string;
19
+ /** The values of the options of the gate. Their keys must not be keys of the options of the strategy before the gate. */
20
+ readonly defaults: G;
21
+ readonly schema: OptionSchema<G>;
22
+ /** This function gives the plan after the gate. `way` holds the options of the way finder of the strategy. */
23
+ apply(input: HighwayInput, plan: HighwayPlan, options: Readonly<G>, way: WayOptions): HighwayPlan;
24
+ }
25
+
26
+ /**
27
+ * This function puts a gate after a highway strategy. The new strategy has the name `<strategy>+<gate>`, and the options of both.
28
+ * The strategy gets its own options, and the gate gets the plan of the strategy and the options of the gate.
29
+ * Thus a gate can follow each plan, for example `corridors+breakeven` or `shared-ways+stretch`. Gates can follow gates.
30
+ */
31
+ export function gateStrategy<O extends object, G extends object>(strategy: Strategy<"highway", O>, gate: Gate<G>): Strategy<"highway", O & G> {
32
+ const keys = Object.keys(gate.defaults);
33
+ for (const k of keys) if (k in strategy.defaults) throw new Error(`The gate "${gate.name}" has the option "${k}", and the strategy "${strategy.name}" has it too.`);
34
+ return defineStrategy<"highway", O & G>({
35
+ step: "highway",
36
+ name: `${strategy.name}+${gate.name}`,
37
+ describe: `${strategy.describe} ${gate.describe}`,
38
+ defaults: { ...strategy.defaults, ...gate.defaults } as O & G,
39
+ schema: { ...(strategy.schema ?? {}), ...gate.schema } as OptionSchema<O & G>,
40
+ ...(strategy.maxNodes === undefined ? {} : { maxNodes: strategy.maxNodes }),
41
+ run: (input: HighwayInput, options: Readonly<O & G>): HighwayPlan => {
42
+ const own: Record<string, unknown> = {};
43
+ const mine: Record<string, unknown> = {};
44
+ for (const [k, v] of Object.entries(options)) (keys.includes(k) ? mine : own)[k] = v;
45
+ return gate.apply(input, strategy.run(input, own as Partial<O>), mine as G, wayOptionsOf(options));
46
+ },
47
+ });
48
+ }
49
+
50
+ /**
51
+ * This function gives the plan with only the members that `keep` accepts. A highway with fewer than two members goes away.
52
+ * The lanes keep their order, and each highway keeps its id, its key and its spine.
53
+ */
54
+ export function keepMembers(plan: HighwayPlan, keep: (h: Highway, m: HighwayMember) => boolean): HighwayPlan {
55
+ const highways: Highway[] = [];
56
+ for (const h of plan.highways) {
57
+ const members = h.members.filter((m) => keep(h, m));
58
+ if (members.length < 2) continue;
59
+ if (members.length === h.members.length) {
60
+ highways.push(h);
61
+ continue;
62
+ }
63
+ const stay = new Set(members.map((m) => m.edge));
64
+ highways.push({ ...h, lanes: h.lanes.filter((e) => stay.has(e)), members });
65
+ }
66
+ return { highways };
67
+ }
68
+
69
+ /** The lengths of the route of one edge of a plan. */
70
+ export interface EdgeLengths {
71
+ /** The length of the route along the plan, after `follow`. */
72
+ readonly route: number;
73
+ /** The length of the route without a highway: the route of the router `corners`. */
74
+ readonly alone: number;
75
+ }
76
+
77
+ /**
78
+ * This function gives the length of the route of each edge of a plan, along the plan and alone. `follow` routes each edge of a plan on its own.
79
+ * Thus the route of an edge does not change when another edge leaves the plan.
80
+ */
81
+ export function edgeLengths(graph: Graph, plan: HighwayPlan, way: WayOptions): Map<string, EdgeLengths> {
82
+ const edges = new Set(plan.highways.flatMap((h) => h.members.map((m) => m.edge)));
83
+ const sub: Graph = { nodes: graph.nodes, edges: graph.edges.filter((e) => edges.has(e.id)) };
84
+ const along = follow(sub, plan, way);
85
+ const alone = follow(sub, { highways: [] }, way);
86
+ const lengthOf = new Map(alone.routes.map((r) => [r.edge, polylineLength(r.points)]));
87
+ return new Map(along.routes.map((r) => [r.edge, { route: polylineLength(r.points), alone: lengthOf.get(r.edge) ?? 0 }]));
88
+ }
89
+
90
+ /** This function gives the ids of the edges that both graphs have. */
91
+ export function commonEdges(a: Graph, b: Graph): Set<string> {
92
+ const ids = new Set(b.edges.map((e) => e.id));
93
+ return new Set(a.edges.map((e) => e.id).filter((id) => ids.has(id)));
94
+ }
95
+
96
+ /** This function gives the Jaccard index of two sets of edges: the common edges over all edges. Two empty sets give 0. */
97
+ export function jaccard(a: ReadonlySet<string>, b: ReadonlySet<string>): number {
98
+ let common = 0;
99
+ for (const x of a) if (b.has(x)) common++;
100
+ const all = a.size + b.size - common;
101
+ return all === 0 ? 0 : common / all;
102
+ }
103
+
104
+ /**
105
+ * This function matches the highways of a plan with the highways of an earlier plan. It pairs the highways with the largest Jaccard index of their edges first.
106
+ * A pair needs an index of `min` or more. Each highway is in one pair at most. The result maps the index of a new highway to the index of its old highway.
107
+ * Only the edges of `edges` count, when this set is present: for example the edges of both graphs.
108
+ */
109
+ export function matchHighways(before: HighwayPlan, after: HighwayPlan, min: number, edges?: ReadonlySet<string>): Map<number, number> {
110
+ const setOf = (h: Highway): Set<string> => new Set(h.members.map((m) => m.edge).filter((e) => edges?.has(e) ?? true));
111
+ const old = before.highways.map(setOf);
112
+ const now = after.highways.map(setOf);
113
+ const pairs: [j: number, i: number, i0: number][] = [];
114
+ now.forEach((a, i) =>
115
+ old.forEach((b, i0) => {
116
+ const j = jaccard(a, b);
117
+ if (j >= min && j > 0) pairs.push([j, i, i0]);
118
+ }),
119
+ );
120
+ pairs.sort((p, q) => q[0] - p[0] || p[1] - q[1] || p[2] - q[2]);
121
+ const out = new Map<number, number>();
122
+ const used = new Set<number>();
123
+ for (const [, i, i0] of pairs) {
124
+ if (out.has(i) || used.has(i0)) continue;
125
+ out.set(i, i0);
126
+ used.add(i0);
127
+ }
128
+ return out;
129
+ }
@@ -0,0 +1,39 @@
1
+ import type { WayOptions } from "../../../ways.ts";
2
+ import type { HighwayInput, HighwayPlan } from "../../model.ts";
3
+ import { edgeLengths, keepMembers, type Gate } from "../model.ts";
4
+
5
+ /** The options of the gate `stretch` (HWY-048). */
6
+ export interface StretchOptions {
7
+ /** An edge stays on its highways when its route is at most this number times its route alone, plus `pad`. The default is 1.2. */
8
+ readonly budget: number;
9
+ /** The added length that each edge can use, in units of the board. The default is 0. */
10
+ readonly pad: number;
11
+ }
12
+
13
+ /** The default options of the gate `stretch`. */
14
+ export const STRETCH_DEFAULTS: Readonly<StretchOptions> = Object.freeze({ budget: 1.2, pad: 0 });
15
+
16
+ /**
17
+ * This function applies the stretch budget of HWY-048 to a plan: the join test of each edge.
18
+ * An edge stays on its highways only when its route along the plan is at most `budget` times its route alone, plus `pad`.
19
+ * The route alone is the route of the router `corners`. An edge that fails leaves each of its highways and goes alone, so the highways do not bend for it.
20
+ * `follow` routes each edge on its own, so the test of one edge does not change with the other edges. One pass is enough.
21
+ */
22
+ export function stretchPlan(input: HighwayInput, plan: HighwayPlan, options: Readonly<StretchOptions>, way: WayOptions = {}): HighwayPlan {
23
+ const lengths = edgeLengths(input.graph, plan, way);
24
+ const out = new Set<string>();
25
+ for (const [edge, l] of lengths) if (l.route > options.budget * l.alone + options.pad + 1e-9) out.add(edge);
26
+ return out.size === 0 ? plan : keepMembers(plan, (_, m) => !out.has(m.edge));
27
+ }
28
+
29
+ /** The gate `stretch` (HWY-048): an edge joins a highway only when its route stays in a budget of its route alone. */
30
+ export const stretchGate: Gate<StretchOptions> = {
31
+ name: "stretch",
32
+ describe: "The gate `stretch` (HWY-048) keeps an edge on a highway only when its route is at most `budget` times its route alone, plus `pad`.",
33
+ defaults: STRETCH_DEFAULTS,
34
+ schema: {
35
+ budget: { type: "number", min: 1, max: 2, step: 0.05, marks: [1.1, 1.2, 1.3, 1.5], default: 1.2, describe: "The largest ratio of the route along the plan to the route alone." },
36
+ pad: { type: "number", min: 0, max: 40, step: 5, default: 0, describe: "The added length that each edge can use, in units of the board." },
37
+ },
38
+ apply: stretchPlan,
39
+ };
@@ -0,0 +1,123 @@
1
+ import { dist, polylineLength, type Point } from "../geom.ts";
2
+ import { orderLanes } from "../segments.ts";
3
+ import { WayFinder, type WayOptions } from "../ways.ts";
4
+ import { tidyPlan } from "./base.ts";
5
+ import { memberAt, type Highway, type HighwayInput, type HighwayMember, type HighwayPlan } from "./model.ts";
6
+ import { sharedWaysPlan } from "./shared-ways/index.ts";
7
+ import { edgePlugs } from "./stretches.ts";
8
+
9
+ /**
10
+ * The spine of a group of edges. With `shared`, the edges of each group go through the router `shared` with the group as their key.
11
+ * Each stretch that they share is a highway. With `trunk`, each group gets one trunk between two terminals, with a leg from each plug.
12
+ */
13
+ export type GroupSpine = "shared" | "trunk";
14
+
15
+ /** The values of `GroupSpine`. */
16
+ export const GROUP_SPINES: readonly GroupSpine[] = Object.freeze(["shared", "trunk"]);
17
+
18
+ /** The group of one edge: its key, and `flip` when the edge goes from the second end of the group to the first end. */
19
+ export interface EdgeGroup {
20
+ readonly key: string;
21
+ readonly flip: boolean;
22
+ }
23
+
24
+ /** The options of `groupedPlan`. */
25
+ export interface GroupedOptions extends WayOptions {
26
+ readonly spine: GroupSpine;
27
+ /** A group has this number of edges or more. The value is 2 or more. */
28
+ readonly minSize: number;
29
+ /**
30
+ * With the spine `trunk`, an edge rides the trunk only when each of its legs is this share of the span or shorter.
31
+ * The span is the distance between the two terminals. `Infinity` sets no limit.
32
+ */
33
+ readonly legMax?: number;
34
+ }
35
+
36
+ /**
37
+ * This function gives the geometric median of points (the Weber point), with the iterations of Weiszfeld.
38
+ * The point has the least sum of the distances to the points.
39
+ */
40
+ export function weberPoint(points: readonly Point[], iterations = 32): Point {
41
+ if (points.length === 0) throw new Error("The list has no point.");
42
+ let p = { x: points.reduce((s, q) => s + q.x, 0) / points.length, y: points.reduce((s, q) => s + q.y, 0) / points.length };
43
+ for (let k = 0; k < iterations; k++) {
44
+ let wx = 0;
45
+ let wy = 0;
46
+ let w = 0;
47
+ for (const q of points) {
48
+ const d = dist(p, q);
49
+ if (d < 1e-9) return q;
50
+ wx += q.x / d;
51
+ wy += q.y / d;
52
+ w += 1 / d;
53
+ }
54
+ const next = { x: wx / w, y: wy / w };
55
+ if (dist(next, p) < 1e-6) return next;
56
+ p = next;
57
+ }
58
+ return p;
59
+ }
60
+
61
+ /**
62
+ * This function gives the highways of the groups of edges. Only the edges in `groups` ride a highway, and each group needs `minSize` edges.
63
+ * The spine `shared` gives the shared stretches of the router `shared` with the group as the key.
64
+ * The spine `trunk` gives one trunk for each group, through the corner graph between two terminals.
65
+ * A terminal is the Weber point of the points out of the plugs at one end of the group. A terminal in a box moves to the nearest of those points.
66
+ */
67
+ export function groupedPlan(input: HighwayInput, groups: ReadonlyMap<string, EdgeGroup>, o: Readonly<GroupedOptions>): HighwayPlan {
68
+ const min = Math.max(2, o.minSize);
69
+ const size = new Map<string, number>();
70
+ for (const g of groups.values()) size.set(g.key, (size.get(g.key) ?? 0) + 1);
71
+ const big = (edge: string): EdgeGroup | undefined => {
72
+ const g = groups.get(edge);
73
+ return g && (size.get(g.key) ?? 0) >= min ? g : undefined;
74
+ };
75
+ const way: WayOptions = { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) };
76
+ if (o.spine === "shared") return sharedWaysPlan(input, { ...way, rule: (e) => big(e.id)?.key });
77
+
78
+ const finder = new WayFinder(input.graph.nodes, way);
79
+ const byKey = new Map<string, { edge: string; flip: boolean; a: Point; b: Point; ps: Point; pt: Point }[]>();
80
+ for (const { edge, ps, pt } of edgePlugs(input.graph)) {
81
+ const g = big(edge.id);
82
+ if (!g) continue;
83
+ const os = finder.out(ps);
84
+ const ot = finder.out(pt);
85
+ const item = g.flip ? { edge: edge.id, flip: true, a: ot, b: os, ps: ps.point, pt: pt.point } : { edge: edge.id, flip: false, a: os, b: ot, ps: ps.point, pt: pt.point };
86
+ const list = byKey.get(g.key);
87
+ if (list) list.push(item);
88
+ else byKey.set(g.key, [item]);
89
+ }
90
+ const pad = finder.margin / 2;
91
+ const terminal = (points: readonly Point[]): Point => {
92
+ const p = weberPoint(points);
93
+ if (!finder.obstacles.inside(p, pad)) return p;
94
+ let best = points[0]!;
95
+ for (const q of points) if (dist(q, p) < dist(best, p)) best = q;
96
+ return best;
97
+ };
98
+ const highways: Highway[] = [];
99
+ const legMax = o.legMax ?? Infinity;
100
+ for (const [key, list] of byKey) {
101
+ if (list.length < min) continue;
102
+ const ta = terminal(list.map((x) => x.a));
103
+ const tb = terminal(list.map((x) => x.b));
104
+ const span = dist(ta, tb);
105
+ if (span <= finder.margin) continue;
106
+ const riders = list.filter((x) => dist(x.a, ta) <= legMax * span && dist(x.b, tb) <= legMax * span);
107
+ if (riders.length < min) continue;
108
+ const raw = finder.corners.path(ta, tb);
109
+ if (!raw) continue;
110
+ const spine = finder.corners.pull(raw);
111
+ if (spine.length < 2 || polylineLength(spine) <= 1e-6) continue;
112
+ const last = spine.length - 1;
113
+ const lanes = orderLanes(
114
+ spine,
115
+ riders.map((x) => ({ edge: x.edge, points: x.flip ? [x.ps, x.b, ...[...spine].reverse(), x.a, x.pt] : [x.ps, x.a, ...spine, x.b, x.pt] })),
116
+ "bundle",
117
+ );
118
+ const byEdge = new Map(riders.map((x) => [x.edge, x]));
119
+ const members: HighwayMember[] = lanes.map((edge) => (byEdge.get(edge)!.flip ? memberAt(spine, edge, last, 0) : memberAt(spine, edge, 0, last)));
120
+ highways.push({ id: `h${highways.length}`, key, spine, lanes, members });
121
+ }
122
+ return tidyPlan({ highways });
123
+ }
@@ -5,3 +5,18 @@ export { highwayRouter, highwayRouterStrategy } from "./router.ts";
5
5
  export { keysPlan, type KeysOptions } from "./keys/index.ts";
6
6
  export { sharedWaysPlan, type SharedWaysOptions } from "./shared-ways/index.ts";
7
7
  export { BREAK_EVEN, CORRIDOR_DEFAULTS, corridorPlan, minHash, type CorridorOptions } from "./corridors/index.ts";
8
+ export { basePlan, HIGHWAY_BASES, type HighwayBase } from "./base.ts";
9
+ export { MEET_DEFAULTS, meetMember, meetPlan, type MeetOptions } from "./meet/index.ts";
10
+ export { meetingPoints, WEISZFELD_DEFAULTS, weiszfeldPlan, type WeiszfeldOptions } from "./weiszfeld/index.ts";
11
+ export { STUB_DEFAULTS, stubPlan, type StubEnds, type StubOptions } from "./stubs/index.ts";
12
+ export { ZIPPER_DEFAULTS, zipLegs, zipperPlan, type Zip, type ZipLimits, type ZipperOptions } from "./zipper/index.ts";
13
+ export { LEVEL_HIGHWAY_DEFAULTS, levelHighwayPlan, type LevelHighwayOptions } from "./level-links/index.ts";
14
+ export { edgePlugs, sharedStretches, type EdgePlugs, type KeyedWay, type SharedStretchOptions } from "./stretches.ts";
15
+ export { GROUP_SPINES, groupedPlan, weberPoint, type EdgeGroup, type GroupedOptions, type GroupSpine } from "./grouped.ts";
16
+ export { mdf, QUICKBUNDLES_DEFAULTS, quickBundles, quickBundlesPlan, resample, type BundleLine, type BundleOrder, type QuickBundlesOptions } from "./quickbundles/index.ts";
17
+ export { ROAD_CLASS_DEFAULTS, roadClassPlan, type RoadClassOptions } from "./road-classes/index.ts";
18
+ export { FLOW_FIELD_DEFAULTS, flowFieldPlan, type FieldEnd, type FlowFieldOptions } from "./flow-field/index.ts";
19
+ export { SplitTree, WSPD_DEFAULTS, wspdGroups, wspdPlan, type SplitCell, type WspdOptions } from "./wspd/index.ts";
20
+ export { OD_CELL_DEFAULTS, odCellPlan, odGroups, type OdCellOptions } from "./od-cells/index.ts";
21
+ export { CANDIDATE_SOURCES, candidatePlan, candidateScore, SELECT_DEFAULTS, selectPlan, type CandidateSource, type SelectOptions } from "./select/index.ts";
22
+ export * from "./gates/index.ts";
@@ -0,0 +1,114 @@
1
+ import { dist, pointBoxDistance, polylineLength, type Point } from "../../geom.ts";
2
+ import { levelLinks, type ItemPair } from "../../levels.ts";
3
+ import { nodeIndex } from "../../model.ts";
4
+ import { buildSections, levelCut } from "../../sections.ts";
5
+ import { WayFinder } from "../../ways.ts";
6
+ import { basePlan, firstRoutes, tidyPlan, type HighwayBase } from "../base.ts";
7
+ import { memberAt, type Highway, type HighwayInput, type HighwayMember, type HighwayPlan } from "../model.ts";
8
+
9
+ /** The options of the highway strategy `level-links` (HWY-056 with WILD-034 of the notes). */
10
+ export interface LevelHighwayOptions {
11
+ /** The plan of the edges that do not ride a level link. The default is `shared-ways`. */
12
+ readonly from: HighwayBase;
13
+ /** The depth of the level in the tree of sections. Depth 1 is the level below the root. The default is 1. */
14
+ readonly depth: number;
15
+ /** A level link becomes a highway when this number of edges or more ride it. The default is 2. */
16
+ readonly minSize: number;
17
+ /** An edge rides the link only when its way along the link is this many times its first route, or shorter. The default is 1.5. */
18
+ readonly stretch: number;
19
+ }
20
+
21
+ /** The default options of the strategy `level-links`. */
22
+ export const LEVEL_HIGHWAY_DEFAULTS: Readonly<LevelHighwayOptions> = Object.freeze({ from: "shared-ways", depth: 1, minSize: 2, stretch: 1.5 });
23
+
24
+ /** This function gives the stretch of a polyline from the length `from` to the length `to`, along the polyline. */
25
+ function cut(p: readonly Point[], from: number, to: number): Point[] {
26
+ const out: Point[] = [];
27
+ let walked = 0;
28
+ for (let i = 1; i < p.length; i++) {
29
+ const a = p[i - 1]!;
30
+ const b = p[i]!;
31
+ const len = dist(a, b);
32
+ const s = Math.max(from, walked);
33
+ const e = Math.min(to, walked + len);
34
+ if (e > s && len > 0) {
35
+ const at = (l: number): Point => ({ x: a.x + ((b.x - a.x) * (l - walked)) / len, y: a.y + ((b.y - a.y) * (l - walked)) / len });
36
+ if (out.length === 0) out.push(at(s));
37
+ out.push(at(e));
38
+ }
39
+ walked += len;
40
+ }
41
+ return out;
42
+ }
43
+
44
+ /**
45
+ * This function gives a highway for each link of a zoom level, with the edges between its two items (HWY-056, WILD-034).
46
+ * The level is the cut of the tree of sections at `depth`. The link goes around the boxes of the other items (`levelLinks`).
47
+ * The spine is the link without a margin at each end, so it stays off the boxes of the two items. It must stay out of each box.
48
+ * The edges between the two items, in each direction, ride the spine. The ramps go through the corner graph inside the items.
49
+ *
50
+ * An edge rides only when the ramps (as straight lines) and the spine are `stretch` times its first route, or shorter.
51
+ * A link with fewer than `minSize` such edges is not a highway. The other edges keep their highways of the plan `from`.
52
+ * Thus a zoom from the level to the routes shows the same lines, which open into lanes.
53
+ */
54
+ export function levelHighwayPlan(input: HighwayInput, o: Readonly<LevelHighwayOptions>): HighwayPlan {
55
+ const graph = input.graph;
56
+ const base = basePlan(input, o.from);
57
+ if (graph.nodes.length === 0) return base;
58
+ const byId = nodeIndex(graph);
59
+ const root = buildSections(graph.nodes);
60
+ const { items, owner } = levelCut(graph.nodes, root, Math.max(0, Math.round(o.depth)));
61
+ const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
62
+ // The edges between each pair of items, by the pair in the order of the items.
63
+ const between = new Map<string, { a: number; b: number; edges: { edge: string; forward: boolean }[] }>();
64
+ for (const e of graph.edges) {
65
+ const iu = indexOfNode.get(e.source.node);
66
+ const iv = indexOfNode.get(e.target.node);
67
+ if (iu === undefined || iv === undefined) continue;
68
+ const x = owner[iu]!;
69
+ const y = owner[iv]!;
70
+ if (x === y) continue;
71
+ const [a, b] = x < y ? [x, y] : [y, x];
72
+ const key = `${a}\u0000${b}`;
73
+ const pair = between.get(key) ?? { a, b, edges: [] };
74
+ pair.edges.push({ edge: e.id, forward: x === a });
75
+ between.set(key, pair);
76
+ }
77
+ const pairs = [...between.values()].filter((p) => p.edges.length >= Math.max(2, o.minSize));
78
+ if (pairs.length === 0) return base;
79
+ const itemPairs: ItemPair[] = pairs.map((p) => ({ source: items[p.a]!.id, target: items[p.b]!.id, count: p.edges.length }));
80
+ const links = levelLinks(items, itemPairs);
81
+ const linkOf = new Map(links.map((l) => [`${l.source}\u0000${l.target}`, l.points]));
82
+ const ways = new WayFinder(graph.nodes);
83
+ const first = firstRoutes(input);
84
+ const edgeOf = new Map(graph.edges.map((e) => [e.id, e]));
85
+ const added: Highway[] = [];
86
+ const riders = new Set<string>();
87
+ for (const p of pairs) {
88
+ const points = linkOf.get(`${items[p.a]!.id}\u0000${items[p.b]!.id}`);
89
+ if (!points || points.length < 2) continue;
90
+ const length = polylineLength(points);
91
+ if (length <= 4 * ways.margin) continue;
92
+ const spine = cut(points, ways.margin, length - ways.margin);
93
+ if (spine.length < 2 || !ways.obstacles.clearPath(spine, 1)) continue;
94
+ const head = spine[0]!;
95
+ const tail = spine[spine.length - 1]!;
96
+ const along = polylineLength(spine);
97
+ const members: HighwayMember[] = [];
98
+ for (const { edge, forward } of p.edges) {
99
+ const e = edgeOf.get(edge)!;
100
+ const s = byId.get(e.source.node)!;
101
+ const t = byId.get(e.target.node)!;
102
+ const [enter, leave] = forward ? [head, tail] : [tail, head];
103
+ const way = pointBoxDistance(enter, s.box) + along + pointBoxDistance(leave, t.box);
104
+ const own = polylineLength(first.get(edge) ?? []);
105
+ if (own > 0 && way > o.stretch * own) continue;
106
+ members.push(forward ? memberAt(spine, edge, 0, spine.length - 1) : memberAt(spine, edge, spine.length - 1, 0));
107
+ }
108
+ if (members.length < Math.max(2, o.minSize)) continue;
109
+ for (const m of members) riders.add(m.edge);
110
+ added.push({ id: "", key: `level${o.depth}:${items[p.a]!.id}:${items[p.b]!.id}`, spine, lanes: members.map((m) => m.edge), members });
111
+ }
112
+ const rest = base.highways.map((h) => ({ ...h, members: h.members.filter((m) => !riders.has(m.edge)) }));
113
+ return tidyPlan({ highways: [...rest, ...added] });
114
+ }
@@ -0,0 +1,84 @@
1
+ import { dist, type Point } from "../../geom.ts";
2
+ import { basePlan, firstRoutes, tidyPlan, type HighwayBase } from "../base.ts";
3
+ import { lengthBetween, memberAt, pointAt, type HighwayInput, type HighwayMember, type HighwayPlan } from "../model.ts";
4
+
5
+ /** The options of the highway strategy `meet` (HWY-050 of the note on highways). */
6
+ export interface MeetOptions {
7
+ /** The plan whose merge points and split points the strategy moves. The default is `shared-ways`. */
8
+ readonly from: HighwayBase;
9
+ /** A first route meets the spine where it is this distance from the spine, or nearer. The default is 8: two lanes of 4. */
10
+ readonly near: number;
11
+ /** A first route meets the spine only where it runs at this angle to the spine, or less, in degrees. The default is 45. */
12
+ readonly maxTurn: number;
13
+ /** A member keeps its places when the stretch where its first route meets the spine is shorter than this length. The default is 10. */
14
+ readonly minRun: number;
15
+ }
16
+
17
+ /** The default options of the strategy `meet`. */
18
+ export const MEET_DEFAULTS: Readonly<MeetOptions> = Object.freeze({ from: "shared-ways", near: 8, maxTurn: 45, minRun: 10 });
19
+
20
+ /** This function gives the distance from a point to a polyline, and the unit direction of the nearest leg. */
21
+ function nearestLeg(line: readonly Point[], p: Point): { d: number; ux: number; uy: number } {
22
+ let best = { d: Infinity, ux: 1, uy: 0 };
23
+ for (let i = 1; i < line.length; i++) {
24
+ const a = line[i - 1]!;
25
+ const b = line[i]!;
26
+ const len = dist(a, b);
27
+ if (len <= 1e-9) continue;
28
+ const u = Math.max(0, Math.min(1, ((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / (len * len)));
29
+ const d = dist(p, { x: a.x + (b.x - a.x) * u, y: a.y + (b.y - a.y) * u });
30
+ if (d < best.d) best = { d, ux: (b.x - a.x) / len, uy: (b.y - a.y) / len };
31
+ }
32
+ return best;
33
+ }
34
+
35
+ /**
36
+ * This function gives a member with its merge point and its split point where its first route meets the spine and parts from it (HWY-050).
37
+ * The function samples the stretch of the member on the spine, at steps of `near / 2` or less.
38
+ * A sample meets the route when the route is `near` or nearer, and its nearest leg runs at `maxTurn` or less to the spine.
39
+ * The new merge point is the first sample that meets the route, in the direction of the member. The new split point is the last one.
40
+ * So the edge joins the spine late and leaves it early. The places only move inward.
41
+ * The member keeps its places when no sample meets the route, or when the met stretch is shorter than `minRun`.
42
+ */
43
+ export function meetMember(spine: readonly Point[], member: HighwayMember, route: readonly Point[] | undefined, o: Readonly<MeetOptions>): HighwayMember {
44
+ if (!route || route.length < 2 || spine.length < 2) return member;
45
+ const lo = Math.min(member.on, member.off);
46
+ const hi = Math.max(member.on, member.off);
47
+ const cos = Math.cos((Math.max(0, Math.min(90, o.maxTurn)) * Math.PI) / 180);
48
+ const step = Math.max(1e-3, o.near / 2);
49
+ let first = Infinity;
50
+ let last = -Infinity;
51
+ for (let i = Math.floor(lo); i < Math.min(spine.length - 1, Math.ceil(hi)); i++) {
52
+ const from = Math.max(lo, i);
53
+ const to = Math.min(hi, i + 1);
54
+ if (to <= from) continue;
55
+ const a = spine[i]!;
56
+ const b = spine[i + 1]!;
57
+ const len = dist(a, b);
58
+ if (len <= 1e-9) continue;
59
+ const tx = (b.x - a.x) / len;
60
+ const ty = (b.y - a.y) / len;
61
+ const n = Math.min(256, Math.max(1, Math.ceil(((to - from) * len) / step)));
62
+ for (let j = 0; j <= n; j++) {
63
+ const at = from + ((to - from) * j) / n;
64
+ const leg = nearestLeg(route, pointAt(spine, at));
65
+ if (leg.d > o.near || Math.abs(leg.ux * tx + leg.uy * ty) < cos - 1e-12) continue;
66
+ first = Math.min(first, at);
67
+ last = Math.max(last, at);
68
+ }
69
+ }
70
+ if (!(last > first) || lengthBetween(spine, first, last) < o.minRun) return member;
71
+ return member.on <= member.off ? memberAt(spine, member.edge, first, last, member.seq) : memberAt(spine, member.edge, last, first, member.seq);
72
+ }
73
+
74
+ /**
75
+ * This function gives the highways of the plan `from`, with merge points and split points where the routes meet and part (HWY-050).
76
+ * The first route of each edge is the route of `input.routes`, or its route without highways.
77
+ * Each member gets the places of `meetMember`. The spines and the trips do not change.
78
+ */
79
+ export function meetPlan(input: HighwayInput, o: Readonly<MeetOptions>): HighwayPlan {
80
+ const plan = basePlan(input, o.from);
81
+ if (plan.highways.length === 0) return plan;
82
+ const first = firstRoutes(input);
83
+ return tidyPlan({ highways: plan.highways.map((h) => ({ ...h, members: h.members.map((m) => meetMember(h.spine, m, first.get(m.edge), o)) })) });
84
+ }
@@ -9,6 +9,11 @@ export interface HighwayInput {
9
9
  * The strategy `corridors` makes the ways of the corner graph, as the router `corners` finds them.
10
10
  */
11
11
  readonly routes?: Result;
12
+ /**
13
+ * This value is the graph before an edit and the plan of the same strategy on it. A strategy that keeps its highways stable over edits reads it.
14
+ * The gates `hysteresis` (HWY-046) and `ids` (HWY-054) read it. The other strategies ignore it.
15
+ */
16
+ readonly previous?: { readonly graph: Graph; readonly plan: HighwayPlan };
12
17
  }
13
18
 
14
19
  /**
@@ -0,0 +1,50 @@
1
+ import { buildSections, levelCut } from "../../sections.ts";
2
+ import { groupedPlan, type EdgeGroup, type GroupSpine } from "../grouped.ts";
3
+ import type { HighwayInput, HighwayPlan } from "../model.ts";
4
+
5
+ /** The options of the highway strategy `od-cells` (EDA-033 with WILD-032 of the notes). */
6
+ export interface OdCellOptions {
7
+ /** The depth of the cells in the tree of sections. Depth 1 is the level below the root. The default is 2. */
8
+ readonly level: number;
9
+ /** A pair of cells with this number of edges or more gets a trunk. The default is 3. */
10
+ readonly minGroup: number;
11
+ /** An edge rides the trunk only when each of its legs is this share of the span or shorter. The default is 0.3. */
12
+ readonly legMax: number;
13
+ /** The spine of each pair of cells. The default is `trunk`. */
14
+ readonly spine: GroupSpine;
15
+ }
16
+
17
+ /** The default options of the strategy `od-cells`. */
18
+ export const OD_CELL_DEFAULTS: Readonly<OdCellOptions> = Object.freeze({ level: 2, minGroup: 3, legMax: 0.3, spine: "trunk" });
19
+
20
+ /**
21
+ * This function gives the groups of the edges by the pair of the cells of their two nodes (EDA-033, WILD-032).
22
+ * The cells are the items of the zoom level at the depth `level`. The edges of one pair of cells, in either direction, are one group.
23
+ * An edge inside one cell is in no group.
24
+ */
25
+ export function odGroups(input: HighwayInput, o: Readonly<OdCellOptions>): Map<string, EdgeGroup> {
26
+ const nodes = input.graph.nodes;
27
+ const groups = new Map<string, EdgeGroup>();
28
+ if (nodes.length === 0) return groups;
29
+ const { owner } = levelCut(nodes, buildSections(nodes), Math.max(0, Math.round(o.level)));
30
+ const index = new Map(nodes.map((n, i) => [n.id, i]));
31
+ for (const e of input.graph.edges) {
32
+ const u = index.get(e.source.node);
33
+ const v = index.get(e.target.node);
34
+ if (u === undefined || v === undefined) continue;
35
+ const a = owner[u]!;
36
+ const b = owner[v]!;
37
+ if (a === b) continue;
38
+ groups.set(e.id, a < b ? { key: `od${a}:${b}`, flip: false } : { key: `od${b}:${a}`, flip: true });
39
+ }
40
+ return groups;
41
+ }
42
+
43
+ /**
44
+ * This function gives the highways of the consolidation by origin and destination cells (EDA-033, WILD-032).
45
+ * Carriers merge the loads from one region to another into one truck between two terminals. Thus each pair of cells with `minGroup` edges or more gets one trunk.
46
+ * The terminals are the Weber points of the plugs in each cell. An edge with a leg longer than `legMax` times the span goes alone.
47
+ */
48
+ export function odCellPlan(input: HighwayInput, o: Readonly<OdCellOptions>): HighwayPlan {
49
+ return groupedPlan(input, odGroups(input, o), { spine: o.spine, minSize: o.minGroup, legMax: o.legMax });
50
+ }