@mark1russell7/highways 0.8.1 → 0.9.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.
@@ -26,6 +26,11 @@ export interface NumberOption extends OptionBase {
26
26
  readonly special?: readonly number[];
27
27
  /** These values are the notable values of the option: the default and the values of the sweeps of the benchmark. */
28
28
  readonly marks?: readonly number[];
29
+ /**
30
+ * These values are the only valid values of the option, in the order of a control. A control shows them as a list, and a sweep gives them.
31
+ * Without them, each value on the range is valid.
32
+ */
33
+ readonly values?: readonly number[];
29
34
  /** The strategy uses this value when the caller and `defaults` give no value. */
30
35
  readonly default?: number;
31
36
  }
@@ -42,6 +47,11 @@ export interface EnumOption<V extends string = string> extends OptionBase {
42
47
  readonly type: "enum";
43
48
  /** The values of the option, in the order of a control. */
44
49
  readonly values: readonly V[];
50
+ /**
51
+ * With `function`, the option also takes a rule of the caller in place of a name. A control sets only the names.
52
+ * Each name is a known rule of the strategy.
53
+ */
54
+ readonly custom?: "function";
45
55
  /** The strategy uses this value when the caller and `defaults` give no value. */
46
56
  readonly default?: V;
47
57
  }
@@ -61,8 +71,19 @@ export interface ValueOption<V = unknown> extends OptionBase {
61
71
  /** The declaration of one option of any type. */
62
72
  export type AnyOption = NumberOption | BooleanOption | EnumOption | ValueOption;
63
73
 
64
- /** The declaration that fits the type of an option value: numbers, booleans, string unions or other values. */
65
- export type OptionDeclaration<V> = [V] extends [number] ? NumberOption : [V] extends [boolean] ? BooleanOption : [V] extends [string] ? EnumOption<V> : ValueOption<V>;
74
+ /**
75
+ * The declaration that fits the type of an option value: numbers, booleans, string unions or other values.
76
+ * A union of strings and other values takes an enum of the strings or a value declaration. An example is a rule or the name of a rule.
77
+ */
78
+ export type OptionDeclaration<V> = [V] extends [number]
79
+ ? NumberOption
80
+ : [V] extends [boolean]
81
+ ? BooleanOption
82
+ : [V] extends [string]
83
+ ? EnumOption<V>
84
+ : [Extract<V, string>] extends [never]
85
+ ? ValueOption<V>
86
+ : EnumOption<Extract<V, string>> | ValueOption<V>;
66
87
 
67
88
  /** The declarations of the options of a strategy. The schema has one declaration for each key of the options type, and no other key. */
68
89
  export type OptionSchema<O> = { readonly [K in keyof O]-?: OptionDeclaration<NonNullable<O[K]>> };
@@ -139,6 +160,7 @@ function issueOf(key: string, value: unknown, d: AnyOption): OptionIssue | undef
139
160
  case "integer": {
140
161
  if (typeof value !== "number" || Number.isNaN(value)) return issue("type", `The option "${key}" takes a number.`);
141
162
  if (isSpecial(d, value)) return undefined;
163
+ if (d.values !== undefined && !d.values.includes(value)) return issue("value", `The option "${key}" takes ${d.values.join(", ")}, not ${value}.`);
142
164
  if (value < d.min || value > d.max) return issue("range", `The option "${key}" is ${value}, out of the range ${d.min} to ${d.max}.`);
143
165
  if (d.type === "integer" && !Number.isInteger(value)) return issue("integer", `The option "${key}" takes a whole number, not ${value}.`);
144
166
  return undefined;
@@ -146,6 +168,7 @@ function issueOf(key: string, value: unknown, d: AnyOption): OptionIssue | undef
146
168
  case "boolean":
147
169
  return typeof value === "boolean" ? undefined : issue("type", `The option "${key}" takes true or false.`);
148
170
  case "enum":
171
+ if (d.custom === "function" && typeof value === "function") return undefined;
149
172
  return d.values.includes(value as string) ? undefined : issue("value", `The option "${key}" takes ${d.values.join(", ")}, not ${String(value)}.`);
150
173
  case "function":
151
174
  return typeof value === "function" ? undefined : issue("type", `The option "${key}" takes a function.`);
@@ -176,7 +199,7 @@ export function checkOptions<S extends Step, O extends object>(strategy: Strateg
176
199
 
177
200
  /**
178
201
  * This function moves each number option into the range of its declaration, for a control. An integer option gets the nearest whole number.
179
- * A special value stays. The function keeps each other value as it is. `checkOptions` reports the values that this function cannot correct.
202
+ * An option with a list of `values` gets the nearest value of the list. A special value stays. The function keeps each other value as it is. `checkOptions` reports the values that this function cannot correct.
180
203
  */
181
204
  export function clampOptions<S extends Step, O extends object>(strategy: Strategy<S, O>, options: Partial<O>): Partial<O> {
182
205
  const schema = schemaOf(strategy);
@@ -184,6 +207,10 @@ export function clampOptions<S extends Step, O extends object>(strategy: Strateg
184
207
  for (const [key, value] of Object.entries(options)) {
185
208
  const d = schema[key];
186
209
  if (!d || (d.type !== "number" && d.type !== "integer") || typeof value !== "number" || Number.isNaN(value) || isSpecial(d, value)) continue;
210
+ if (d.values !== undefined && d.values.length > 0) {
211
+ out[key] = d.values.reduce((best, v) => (Math.abs(v - value) < Math.abs(best - value) ? v : best));
212
+ continue;
213
+ }
187
214
  const v = Math.min(d.max, Math.max(d.min, value));
188
215
  out[key] = d.type === "integer" ? Math.min(d.max, Math.max(d.min, Math.round(v))) : v;
189
216
  }
@@ -211,7 +238,7 @@ function rangeValues(d: NumberOption, count: number): number[] {
211
238
 
212
239
  /**
213
240
  * This function gives the values of a sweep of one option declaration.
214
- * A number option gives its `marks`, or `count` values on its range and its scale. A boolean gives `false` and `true`. An enum gives its values.
241
+ * A number option gives its `marks`, or its list of `values`, or `count` values on its range and its scale. A boolean gives `false` and `true`. An enum gives its values.
215
242
  * An option of another type has no sweep, so the function throws an error.
216
243
  */
217
244
  export function sweepValues(declaration: AnyOption, options: SweepOptions = {}): unknown[] {
@@ -219,6 +246,7 @@ export function sweepValues(declaration: AnyOption, options: SweepOptions = {}):
219
246
  case "number":
220
247
  case "integer": {
221
248
  if (options.count === undefined && declaration.marks !== undefined) return [...declaration.marks];
249
+ if (declaration.values !== undefined) return [...declaration.values];
222
250
  const values = rangeValues(declaration, options.count ?? SWEEP_COUNT);
223
251
  return options.special ? [...values, ...(declaration.special ?? []).filter((s) => !values.includes(s))] : values;
224
252
  }
@@ -268,6 +296,10 @@ function declarationIssues(key: string, d: AnyOption): string[] {
268
296
  if (d.type === "integer" && (!Number.isInteger(d.min) || !Number.isInteger(d.max))) out.push(`The integer option "${key}" has a range that is not whole.`);
269
297
  if (d.step !== undefined && !(d.step > 0)) out.push(`The option "${key}" has the step ${d.step}. A step is more than 0.`);
270
298
  if (d.scale === "log" && !(d.min > 0)) out.push(`The option "${key}" has the scale log and the min ${d.min}. The scale log needs a min above 0.`);
299
+ if (d.values !== undefined && d.values.length === 0) out.push(`The number option "${key}" has an empty list of values.`);
300
+ for (const v of d.values ?? []) {
301
+ if (v < d.min || v > d.max || (d.type === "integer" && !Number.isInteger(v))) out.push(`The value ${v} of the option "${key}" is not on its range ${d.min} to ${d.max}.`);
302
+ }
271
303
  for (const v of [...(d.marks ?? []), ...(d.default === undefined ? [] : [d.default])]) {
272
304
  const issue = issueOf(key, v, d);
273
305
  if (issue) out.push(issue.message);
@@ -49,12 +49,15 @@ import type { TraclusOptions } from "../highways/traclus/index.ts";
49
49
  import type { WeiszfeldOptions } from "../highways/weiszfeld/index.ts";
50
50
  import type { WspdOptions } from "../highways/wspd/index.ts";
51
51
  import type { ZipperOptions } from "../highways/zipper/index.ts";
52
+ import type { LaneStretchOptions } from "../lane-orders/stretch/index.ts";
53
+ import type { DepthLevelOptions } from "../levels/depths/index.ts";
52
54
  import type { CornersOptions } from "../routers/corners.ts";
53
55
  import type { SectionsOptions } from "../routers/sections.ts";
54
56
  import type { SharedOptions } from "../routers/shared.ts";
55
- import type { TrunksOptions } from "../routers/trunks.ts";
57
+ import { GROUP_RULE_NAMES, type GroupRuleName, type TrunksOptions } from "../routers/trunks.ts";
56
58
  import type { SectionOptions } from "../sections.ts";
57
59
  import type { WayOptions } from "../ways.ts";
60
+ import type { PipelineOptions } from "../whole/pipeline/index.ts";
58
61
  import type { RunOrder } from "../segments.ts";
59
62
  import type { ClothoidStrategyOptions, HighwayStrategyOptions, LevelRoomPreset, RadiusOptions, RoomPreset, RouterPreset, SmoothStrategyOptions, TensionOptions } from "./builtins.ts";
60
63
  import type { EnumOption, NumberOption, OptionSchema } from "./options.ts";
@@ -69,6 +72,24 @@ const WAY_SCHEMA: OptionSchema<WayOptions> = {
69
72
  smooth: { type: "boolean", default: false, describe: "With `true`, a way in clear view uses the inflection rule, and the other ways get round corners." },
70
73
  };
71
74
 
75
+ /** The declarations of the options of the stretch order (LANE-STRETCH): the method and the passes of `sifting`. A generator can list each value. */
76
+ export const LANE_STRETCH_SCHEMA: OptionSchema<LaneStretchOptions> = {
77
+ method: {
78
+ type: "enum",
79
+ values: ["none", "consensus", "periphery", "sifting"],
80
+ default: "sifting",
81
+ describe: "The order of each stretch: the order of each run alone, the order of the pair relation, the order of the largest run, or the order of the pair relation after sifting.",
82
+ },
83
+ passes: { type: "integer", min: 1, max: 4, step: 1, default: 2, describe: "The number of passes of the method `sifting`. In each pass, each edge moves to its best place." },
84
+ };
85
+
86
+ /**
87
+ * The declaration of the grouping rule of the edges: a name of `GROUP_RULE_NAMES`. A new name of the list needs no change here.
88
+ * The option also takes a rule of the caller (`custom`), so the old value form stays valid.
89
+ * The `defaults` of a strategy keep the rule `byCollection`, and the declaration gives its name `collection` as its `default`.
90
+ */
91
+ const EDGE_RULE: EnumOption<GroupRuleName> = { type: "enum", values: GROUP_RULE_NAMES, custom: "function", describe: "The grouping rule." };
92
+
72
93
  /** The declaration of the lane order of the shared ways of the routers `trunks` and `shared`. */
73
94
  const LANE_ORDER: EnumOption<RunOrder> = {
74
95
  type: "enum",
@@ -97,12 +118,14 @@ export const CORNERS_SCHEMA: OptionSchema<RouterPreset<CornersOptions>> = {
97
118
  default: 0,
98
119
  describe: "With a gap above 0, the free plugs of a side keep their order and at least this gap (WILD-002). 0 keeps each plug where it is.",
99
120
  },
121
+ plug: { type: "strategy", step: "plugs", describe: "The plug rule: a strategy of the step `plugs`. Without it, the option `plugs` places the plugs." },
122
+ plugOptions: { type: "object", describe: "The options of the strategy `plug`." },
100
123
  };
101
124
 
102
125
  /** The declarations of the options of the router `trunks`. */
103
126
  export const TRUNKS_SCHEMA: OptionSchema<RouterPreset<TrunksOptions>> = {
104
127
  ...WAY_SCHEMA,
105
- rule: { type: "function", describe: "The grouping rule. The edges from one plug with one key share a trunk." },
128
+ rule: { ...EDGE_RULE, default: "collection", describe: "The grouping rule. The edges from one plug with one key share a trunk. With `all`, each edge has one key." },
106
129
  split: { type: "number", min: 0.1, max: 0.9, step: 0.05, default: 0.5, describe: "The split point is at this share of the way to the center of the targets." },
107
130
  minSize: { type: "integer", min: 1, max: 8, step: 1, default: 2, describe: "A group with fewer edges stays unbundled." },
108
131
  laneOrder: LANE_ORDER,
@@ -128,10 +151,10 @@ export const TRUNKS_SCHEMA: OptionSchema<RouterPreset<TrunksOptions>> = {
128
151
  bf: { type: "number", min: 0, max: 1, step: 0.05, default: 0.4, describe: "The balancing factor of the fan `tree`. 0 gives the least ink, and a larger factor gives more direct routes." },
129
152
  };
130
153
 
131
- /** The declarations of the options of the routers `shared` and `shared-all`. */
154
+ /** The declarations of the options of the router `shared` and of the highway strategy `shared-ways`. */
132
155
  export const SHARED_SCHEMA: OptionSchema<RouterPreset<SharedOptions>> = {
133
156
  ...WAY_SCHEMA,
134
- rule: { type: "function", describe: "The edges with one key share their ways. Without a rule, all edges share." },
157
+ rule: { ...EDGE_RULE, default: "collection", describe: "The edges with one key share their ways. With `collection`, the key is `data.collection`. With `all`, all edges share." },
135
158
  discount: {
136
159
  type: "number",
137
160
  min: 0.05,
@@ -179,6 +202,18 @@ export const SHARED_SCHEMA: OptionSchema<RouterPreset<SharedOptions>> = {
179
202
  default: "corner",
180
203
  describe: "With `corner`, a route joins a run at a corner of the graph. With `snell`, it joins at the angle `acos(d)` (NAT-035).",
181
204
  },
205
+ stretchOrder: {
206
+ ...LANE_STRETCH_SCHEMA.method,
207
+ default: "none",
208
+ describe: "With `none`, each run gets its lane order alone. With the other values, the runs that follow each other share one order (LANE-STRETCH).",
209
+ },
210
+ siftPasses: LANE_STRETCH_SCHEMA.passes,
211
+ };
212
+
213
+ /** The declarations of the options of the router `shared-all`. It sets no rule, so all edges share. Thus its rule has no `default`. */
214
+ export const SHARED_ALL_SCHEMA: OptionSchema<RouterPreset<SharedOptions>> = {
215
+ ...SHARED_SCHEMA,
216
+ rule: { ...EDGE_RULE, describe: "The edges with one key share their ways. Without a rule, or with `all`, all edges share. With `collection`, the key is `data.collection`." },
182
217
  };
183
218
 
184
219
  /** The declarations of the options of the highway strategy `corridors` (HWY-031). */
@@ -248,6 +283,8 @@ export const SECTIONS_SCHEMA: OptionSchema<RouterPreset<SectionsOptions>> = {
248
283
  group: { type: "strategy", step: "group", describe: "The grouping rule: a strategy of the step `group`. Without it, the router uses the quadtree." },
249
284
  groupOptions: { type: "object", describe: "The options of the strategy `group`." },
250
285
  levels: { type: "list", describe: "The depths of the tree that get a level with groups. Without it, each depth gets a level." },
286
+ level: { type: "strategy", step: "levels", describe: "The level rule: a strategy of the step `levels`. Without it, the router uses the strategy `depths`." },
287
+ levelOptions: { type: "object", describe: "The options of the strategy `level`. They go over the level options of the router." },
251
288
  laneOrder: RUN_LANE_ORDER,
252
289
  };
253
290
 
@@ -553,7 +590,7 @@ export const EDGE_PATH_SCHEMA: OptionSchema<EdgePathOptions> = {
553
590
 
554
591
  /** The declarations of the options of the highway strategy `soft-keys` (HWY-019). */
555
592
  export const SOFT_KEY_SCHEMA: OptionSchema<SoftKeyOptions> = {
556
- rule: { type: "function", describe: "The rule that gives the key of each edge. Two different keys add the distance `lambda`." },
593
+ rule: { ...EDGE_RULE, default: "collection", describe: "The rule that gives the key of each edge. Two different keys add the distance `lambda`. With `all`, the keys add no distance." },
557
594
  lambda: { type: "number", min: 0, max: 2, step: 0.05, marks: [0, 0.25, 1], default: 0.25, describe: "The distance that two different keys add. A very large value keeps the keys apart." },
558
595
  tau: { type: "number", min: 0.1, max: 1.5, step: 0.05, marks: [0.3, 0.5, 1], default: 0.5, describe: "Two clusters join while their distance is this value or less." },
559
596
  attributes: { type: "list", default: [], describe: "The names of number fields of `data` that add a distance." },
@@ -862,3 +899,29 @@ export const SCALE_SPACE_SCHEMA: OptionSchema<ScaleSpaceOptions> = {
862
899
  minSize: { type: "integer", min: 2, max: 16, step: 1, default: 2, describe: "A cluster with this number of edges or more gets highways." },
863
900
  spine: GROUP_SPINE,
864
901
  };
902
+
903
+ /** The declarations of the options of the level strategy `depths`: the limits of the level links, and the depths that get a level. */
904
+ export const DEPTHS_SCHEMA: OptionSchema<DepthLevelOptions> = {
905
+ margin: WAY_SCHEMA.margin,
906
+ k: WAY_SCHEMA.k,
907
+ tightMargin: SECTIONS_SCHEMA.tightMargin,
908
+ plugInset: SECTIONS_SCHEMA.plugInset,
909
+ throughCost: SECTIONS_SCHEMA.throughCost,
910
+ levels: SECTIONS_SCHEMA.levels,
911
+ };
912
+
913
+ /** The declarations of the options of the whole strategy `pipeline`. */
914
+ export const PIPELINE_SCHEMA: OptionSchema<PipelineOptions> = {
915
+ router: { type: "strategy", step: "router", describe: "The router. A highway plan takes its place." },
916
+ routerOptions: { type: "object", describe: "The options of the router." },
917
+ highway: { type: "strategy", step: "highway", describe: "The highway strategy. With it, the plan and `follow` give the routes. Without it, the router routes." },
918
+ highwayOptions: { type: "object", describe: "The options of the highway strategy." },
919
+ group: { type: "strategy", step: "group", describe: "The grouping rule of a router that reads one, for example `sections`." },
920
+ groupOptions: { type: "object", describe: "The options of the grouping rule." },
921
+ plugs: { type: "strategy", step: "plugs", describe: "The plug rule of a router that reads one, for example `corners`." },
922
+ plugOptions: { type: "object", describe: "The options of the plug rule." },
923
+ levels: { type: "strategy", step: "levels", describe: "The level rule of a router that reads one, for example `sections`." },
924
+ levelOptions: { type: "object", describe: "The options of the level rule." },
925
+ room: { type: "strategy", step: "room", describe: "The room strategy. With it, the gaps of the board open before the routes. Without it, the board stays." },
926
+ roomOptions: { type: "object", describe: "The options of the room strategy." },
927
+ };
@@ -0,0 +1,66 @@
1
+ import { highwayRouterStrategy } from "../../highways/router.ts";
2
+ import type { Graph } from "../../model.ts";
3
+ import { openGaps, roomLinks } from "../../room/index.ts";
4
+ import { asRouter, type Step, type Strategy, type WholeOutput } from "../../strategies/model.ts";
5
+ import { listOptions } from "../../strategies/options.ts";
6
+
7
+ /**
8
+ * The options of the whole strategy `pipeline`: one strategy of each step, and the options of each one.
9
+ * A rule of the steps `group`, `plugs` and `levels` acts only on a router that reads a rule of that step (`strategyKey`).
10
+ */
11
+ export interface PipelineOptions {
12
+ /** The router. A highway plan takes its place. */
13
+ readonly router: Strategy<"router">;
14
+ /** The options of the router. */
15
+ readonly routerOptions?: object;
16
+ /** The highway strategy. With it, the plan of the strategy and `follow` give the routes, not the router. */
17
+ readonly highway?: Strategy<"highway">;
18
+ /** The options of the highway strategy. */
19
+ readonly highwayOptions?: object;
20
+ /** The grouping rule of a router that reads one, for example `sections`. */
21
+ readonly group?: Strategy<"group">;
22
+ /** The options of the grouping rule. */
23
+ readonly groupOptions?: object;
24
+ /** The plug rule of a router that reads one, for example `corners`. */
25
+ readonly plugs?: Strategy<"plugs">;
26
+ /** The options of the plug rule. */
27
+ readonly plugOptions?: object;
28
+ /** The level rule of a router that reads one, for example `sections`. */
29
+ readonly levels?: Strategy<"levels">;
30
+ /** The options of the level rule. */
31
+ readonly levelOptions?: object;
32
+ /** The room strategy. With it, the pipeline opens the gaps of the board before the routes (`openGaps`). */
33
+ readonly room?: Strategy<"room">;
34
+ /** The options of the room strategy. */
35
+ readonly roomOptions?: object;
36
+ }
37
+
38
+ /**
39
+ * This function gives the key of the option of a router that takes a strategy of one step.
40
+ * It gives `undefined` when the router has no such option.
41
+ * The schema of the router declares the option with the type `strategy` and the step. The options of that strategy have the key `<key>Options`.
42
+ */
43
+ export function strategyKey(router: Strategy<"router">, step: Step): string | undefined {
44
+ return listOptions(router).find((o) => o.declaration.type === "strategy" && o.declaration.step === step)?.key;
45
+ }
46
+
47
+ /**
48
+ * This function runs the whole strategy `pipeline` on one graph. The room strategy opens the gaps of the board first.
49
+ * A highway strategy makes its plan, and `follow` routes the plan. Otherwise the router routes, with each rule of the steps `group`, `plugs` and `levels` that it reads.
50
+ * The output has the board and the routes. The function does not examine the `maxNodes` of the router.
51
+ */
52
+ export function runPipeline(graph: Graph, options: PipelineOptions): WholeOutput {
53
+ const board = options.room ? { nodes: openGaps(graph.nodes, options.room.run({ items: graph.nodes, links: roomLinks(graph) }, options.roomOptions)), edges: graph.edges } : graph;
54
+ if (options.highway) return { graph: board, result: asRouter(highwayRouterStrategy(options.highway) as Strategy<"router">, options.highwayOptions).route(board) };
55
+ let routerOptions = options.routerOptions;
56
+ const rules: readonly [Step, Strategy<Step> | undefined, object | undefined][] = [
57
+ ["group", options.group, options.groupOptions],
58
+ ["plugs", options.plugs, options.plugOptions],
59
+ ["levels", options.levels, options.levelOptions],
60
+ ];
61
+ for (const [step, rule, own] of rules) {
62
+ const key = rule && strategyKey(options.router, step);
63
+ if (key) routerOptions = { ...routerOptions, [key]: rule, [`${key}Options`]: own };
64
+ }
65
+ return { graph: board, result: asRouter(options.router, routerOptions).route(board) };
66
+ }