@unmap/routing 0.9.1 → 0.12.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/README.md CHANGED
@@ -23,7 +23,11 @@ bands.features // one GeoJSON polygon per minute value
23
23
  ```
24
24
 
25
25
  Coordinates are always `[lng, lat]`. `mode` is `'auto' | 'car' | 'bicycle' | 'pedestrian' | 'truck'` and defaults
26
- to `'auto'`; `minutes` is required and takes up to four values of at most 120 minutes. Both methods accept an
26
+ to `'auto'`; `minutes` is required and takes up to four values of at most 120 minutes. Driving modes accept
27
+ `useHighways` (`0` avoid motorways and trunks, `1` prefer); cycling and walking accept `useHills`
28
+ (`0` avoid climbs even for a longer route, `1` indifferent) and already account for terrain
29
+ without it. Driving times do not vary with grade at all, which is Valhalla's model, so
30
+ `useHills` on a driving mode is a `400` rather than a no-op. Both methods accept an
27
31
  `AbortSignal` as `signal`. A non-2xx response throws a `RouterError` carrying the HTTP `status`.
28
32
 
29
33
  Parameters, response shapes, and error bodies are documented at
package/dist/index.d.ts CHANGED
@@ -34,8 +34,44 @@ interface RouteOptions {
34
34
  mode?: TravelMode;
35
35
  /** Only meaningful with `mode: "truck"`; the gateway rejects it on any other mode. */
36
36
  truck?: TruckProfile;
37
+ /**
38
+ * Willingness to take motorways and trunks, 0 (avoid) to 1 (prefer). Driving
39
+ * modes only (`auto`, `car`, `truck`). This is Valhalla's existing costing
40
+ * option on the Canada graph, not a second router and not resource/winter/field routing.
41
+ */
42
+ useHighways?: number;
43
+ /**
44
+ * Willingness to climb, 0 (avoid hills even for a longer route) to 1 (indifferent).
45
+ * `bicycle` and `pedestrian` only; the gateway rejects it on the driving modes, whose
46
+ * costing has no grade term at all. Grades come from the national 30 m elevation model,
47
+ * baked into the Canada graph at build time.
48
+ */
49
+ useHills?: number;
37
50
  signal?: AbortSignal;
38
51
  }
52
+ /**
53
+ * The state of the routing graph behind every other call here.
54
+ *
55
+ * `graph` is absent when the engine did not report a build time. That is a real state, not an
56
+ * error: the deployed Canada graph was built before the service was configured to report one.
57
+ * Treat its absence as "unknown", never as "fresh".
58
+ */
59
+ interface RoutingStatus {
60
+ /** The only graph there is. Canada-only is an architectural constraint, not a rollout stage. */
61
+ region: "canada";
62
+ /** `unavailable` means the routing container did not answer, not that this call failed. */
63
+ status: "ready" | "unavailable";
64
+ engine: {
65
+ name: "valhalla";
66
+ version?: string;
67
+ };
68
+ graph?: {
69
+ /** When the tileset was last written, ISO 8601. */
70
+ builtAt: string;
71
+ /** Whole hours since `builtAt`. */
72
+ ageHours: number;
73
+ };
74
+ }
39
75
  interface RouteResult {
40
76
  distanceMeters: number;
41
77
  durationSeconds: number;
@@ -44,6 +80,31 @@ interface RouteResult {
44
80
  coordinates: LngLat[];
45
81
  };
46
82
  }
83
+ /**
84
+ * Travel times and distances between every origin and every destination.
85
+ *
86
+ * If any pair is unreachable, or beyond the engine's own road-distance limit, `matrix()`
87
+ * rejects with a `RouterError` (status 422, code `no_route`) instead of resolving with a
88
+ * `null` cell for that pair: `null` never appears in a resolved `MatrixResult` in practice. It
89
+ * stays in the type as a defensive case, costing nothing and remaining correct if a future
90
+ * engine version starts nulling cells instead of failing the request.
91
+ */
92
+ interface MatrixResult {
93
+ /** Seconds. Rows are origins, columns are destinations. */
94
+ durations: (number | null)[][];
95
+ /** Metres, same indexing. */
96
+ distances: (number | null)[][];
97
+ }
98
+ /**
99
+ * A route through a set of stops, plus the order to visit them in.
100
+ *
101
+ * A route with one extra field rather than a shape of its own, the same reasoning that makes
102
+ * `match()` return `RouteResult` outright: an optimized route is a route.
103
+ */
104
+ interface OptimizedRouteResult extends RouteResult {
105
+ /** Indices into the request's `stops`, in visiting order. Always a permutation of them. */
106
+ order: number[];
107
+ }
47
108
  /** Minimal GeoJSON FeatureCollection (isochrone bands). */
48
109
  interface FeatureCollection {
49
110
  type: "FeatureCollection";
@@ -60,7 +121,7 @@ declare class RouterError extends Error {
60
121
  /** The gateway's machine-readable `code`, when the response carried one (e.g. `unauthorized`). */
61
122
  code?: string | undefined);
62
123
  }
63
- /** Typed client for the unmap routing API (gateway `/route`, `/isochrone`). */
124
+ /** Typed client for the unmap routing API (gateway `/route`, `/isochrone`, `/matrix`, `/match`, `/optimized-route`). */
64
125
  declare class Router {
65
126
  private readonly key;
66
127
  private readonly base;
@@ -68,7 +129,47 @@ declare class Router {
68
129
  constructor(opts: RouterOptions);
69
130
  route(from: LngLat, to: LngLat, opts?: RouteOptions): Promise<RouteResult>;
70
131
  isochrone(center: LngLat, opts: IsochroneOptions): Promise<FeatureCollection>;
132
+ /**
133
+ * The routing graph's state: whether the engine is answering, which version it is, and how
134
+ * old the Canada graph is.
135
+ *
136
+ * Resolves rather than rejects when routing is down: it answers `status: "unavailable"`,
137
+ * because a status call that throws when the subject is unhealthy tells you nothing a failed
138
+ * `route()` had not already told you.
139
+ */
140
+ status(opts?: {
141
+ signal?: AbortSignal;
142
+ }): Promise<RoutingStatus>;
143
+ /**
144
+ * Travel times and distances between every origin and every destination.
145
+ *
146
+ * At most 10 origins and 10 destinations. A `0` is a real answer, which is what you get
147
+ * where an origin is also a destination. If any pair is unreachable, or beyond the engine's
148
+ * own road-distance limit, the whole call rejects with a `RouterError` (422, `no_route`)
149
+ * rather than resolving with a `null` cell for that pair.
150
+ */
151
+ matrix(origins: LngLat[], destinations: LngLat[], opts?: RouteOptions): Promise<MatrixResult>;
152
+ /**
153
+ * Snap a GPS trace to the road network. At least 2 and at most 100 coordinates.
154
+ *
155
+ * Returns the same `RouteResult` `route()` returns: a matched trace is a route.
156
+ */
157
+ match(coordinates: LngLat[], opts?: RouteOptions): Promise<RouteResult>;
158
+ /**
159
+ * Order a set of stops and return the route through them. At least 3 and at most 10 stops.
160
+ *
161
+ * The first stop is the fixed start and the last is the fixed end; only the stops between
162
+ * them are reordered. There is no round-trip or open-route toggle: that is a real constraint
163
+ * of the underlying engine, not an omission here.
164
+ *
165
+ * Optimizing solves a cost matrix over every stop, so it is expected to fail the same way `matrix()` does:
166
+ * if any stop is unreachable, or the set is spread further apart than the engine's own
167
+ * road-distance limit allows, the whole call rejects with a `RouterError` (422, `no_route`)
168
+ * rather than resolving with a partial tour.
169
+ */
170
+ optimize(stops: LngLat[], opts?: RouteOptions): Promise<OptimizedRouteResult>;
171
+ private post;
71
172
  private get;
72
173
  }
73
174
 
74
- export { type FeatureCollection, type IsochroneOptions, type LngLat, type RouteOptions, type RouteResult, Router, RouterError, type RouterOptions, type TravelMode, type TruckProfile };
175
+ export { type FeatureCollection, type IsochroneOptions, type LngLat, type MatrixResult, type OptimizedRouteResult, type RouteOptions, type RouteResult, Router, RouterError, type RouterOptions, type RoutingStatus, type TravelMode, type TruckProfile };
package/dist/index.js CHANGED
@@ -19,6 +19,23 @@ function truckParams(truck) {
19
19
  }
20
20
  return out;
21
21
  }
22
+ function truckBody(truck) {
23
+ const out = {};
24
+ if (!truck) return out;
25
+ for (const [field, param] of Object.entries(TRUCK_PARAMS)) {
26
+ const v = truck[field];
27
+ if (v != null) out[param] = v;
28
+ }
29
+ return out;
30
+ }
31
+ function bodyOptions(opts) {
32
+ return {
33
+ ...opts.mode ? { mode: opts.mode } : {},
34
+ ...opts.useHighways != null ? { use_highways: opts.useHighways } : {},
35
+ ...opts.useHills != null ? { use_hills: opts.useHills } : {},
36
+ ...truckBody(opts.truck)
37
+ };
38
+ }
22
39
  var RouterError = class extends Error {
23
40
  constructor(status, message, code) {
24
41
  super(message);
@@ -53,7 +70,14 @@ var Router = class {
53
70
  route(from, to, opts = {}) {
54
71
  return this.get(
55
72
  "/route",
56
- { from: from.join(","), to: to.join(","), mode: opts.mode, ...truckParams(opts.truck) },
73
+ {
74
+ from: from.join(","),
75
+ to: to.join(","),
76
+ mode: opts.mode,
77
+ use_highways: opts.useHighways != null ? String(opts.useHighways) : void 0,
78
+ use_hills: opts.useHills != null ? String(opts.useHills) : void 0,
79
+ ...truckParams(opts.truck)
80
+ },
57
81
  opts.signal
58
82
  );
59
83
  }
@@ -65,11 +89,69 @@ var Router = class {
65
89
  lat: String(center[1]),
66
90
  minutes: opts.minutes.join(","),
67
91
  mode: opts.mode,
92
+ use_highways: opts.useHighways != null ? String(opts.useHighways) : void 0,
93
+ use_hills: opts.useHills != null ? String(opts.useHills) : void 0,
68
94
  ...truckParams(opts.truck)
69
95
  },
70
96
  opts.signal
71
97
  );
72
98
  }
99
+ /**
100
+ * The routing graph's state: whether the engine is answering, which version it is, and how
101
+ * old the Canada graph is.
102
+ *
103
+ * Resolves rather than rejects when routing is down: it answers `status: "unavailable"`,
104
+ * because a status call that throws when the subject is unhealthy tells you nothing a failed
105
+ * `route()` had not already told you.
106
+ */
107
+ status(opts = {}) {
108
+ return this.get("/routing/status", {}, opts.signal);
109
+ }
110
+ /**
111
+ * Travel times and distances between every origin and every destination.
112
+ *
113
+ * At most 10 origins and 10 destinations. A `0` is a real answer, which is what you get
114
+ * where an origin is also a destination. If any pair is unreachable, or beyond the engine's
115
+ * own road-distance limit, the whole call rejects with a `RouterError` (422, `no_route`)
116
+ * rather than resolving with a `null` cell for that pair.
117
+ */
118
+ matrix(origins, destinations, opts = {}) {
119
+ return this.post("/matrix", { origins, destinations, ...bodyOptions(opts) }, opts.signal);
120
+ }
121
+ /**
122
+ * Snap a GPS trace to the road network. At least 2 and at most 100 coordinates.
123
+ *
124
+ * Returns the same `RouteResult` `route()` returns: a matched trace is a route.
125
+ */
126
+ match(coordinates, opts = {}) {
127
+ return this.post("/match", { coordinates, ...bodyOptions(opts) }, opts.signal);
128
+ }
129
+ /**
130
+ * Order a set of stops and return the route through them. At least 3 and at most 10 stops.
131
+ *
132
+ * The first stop is the fixed start and the last is the fixed end; only the stops between
133
+ * them are reordered. There is no round-trip or open-route toggle: that is a real constraint
134
+ * of the underlying engine, not an omission here.
135
+ *
136
+ * Optimizing solves a cost matrix over every stop, so it is expected to fail the same way `matrix()` does:
137
+ * if any stop is unreachable, or the set is spread further apart than the engine's own
138
+ * road-distance limit allows, the whole call rejects with a `RouterError` (422, `no_route`)
139
+ * rather than resolving with a partial tour.
140
+ */
141
+ optimize(stops, opts = {}) {
142
+ return this.post("/optimized-route", { stops, ...bodyOptions(opts) }, opts.signal);
143
+ }
144
+ async post(path, body, signal) {
145
+ const q = new URLSearchParams({ key: this.key });
146
+ const res = await this.fetchImpl(`${this.base}${path}?${q}`, {
147
+ method: "POST",
148
+ signal,
149
+ headers: { accept: "application/json", "content-type": "application/json" },
150
+ body: JSON.stringify(body)
151
+ });
152
+ if (!res.ok) throw await errorFrom(res, path);
153
+ return res.json();
154
+ }
73
155
  async get(path, params, signal) {
74
156
  const q = new URLSearchParams({ key: this.key });
75
157
  for (const [k, v] of Object.entries(params)) if (v != null && v !== "") q.set(k, v);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { DEFAULT_GATEWAY } from \"@unmap/core\";\n\nexport interface RouterOptions {\n key: string;\n gateway?: string;\n fetch?: typeof fetch;\n}\n\n/** `car` is the same costing as `auto`, spelled for people who do not think in Valhalla terms. */\nexport type TravelMode = \"auto\" | \"car\" | \"bicycle\" | \"pedestrian\" | \"truck\";\n/** [longitude, latitude]. */\nexport type LngLat = [number, number];\n\n/**\n * Vehicle profile for `mode: \"truck\"`. Metric throughout: metres and tonnes. Every field is\n * optional; the gateway falls back to Valhalla's defaults (4.11 m high, 2.6 m wide, 21.64 m\n * long, 21.77 t, 9.07 t axle load, 5 axles, no hazmat) for anything omitted.\n */\nexport interface TruckProfile {\n /** Vehicle height in metres. */\n height?: number;\n /** Vehicle width in metres. */\n width?: number;\n /** Vehicle length in metres. */\n length?: number;\n /** Gross vehicle weight in tonnes. */\n weight?: number;\n /** Load per axle in tonnes (0–40). */\n axleLoad?: number;\n /** Number of axles (2–20). */\n axleCount?: number;\n /** Carrying hazardous materials: avoids roads tagged `hazmat=no`. */\n hazmat?: boolean;\n /** Preference for designated truck routes, 0 (none) to 1 (strong). */\n useTruckRoute?: number;\n}\n\nexport interface RouteOptions {\n mode?: TravelMode;\n /** Only meaningful with `mode: \"truck\"`; the gateway rejects it on any other mode. */\n truck?: TruckProfile;\n signal?: AbortSignal;\n}\n\n/** `TruckProfile` field → the gateway's snake_case query parameter. */\nconst TRUCK_PARAMS: Record<keyof TruckProfile, string> = {\n height: \"height\",\n width: \"width\",\n length: \"length\",\n weight: \"weight\",\n axleLoad: \"axle_load\",\n axleCount: \"axle_count\",\n hazmat: \"hazmat\",\n useTruckRoute: \"use_truck_route\",\n};\n\nfunction truckParams(truck?: TruckProfile): Record<string, string | undefined> {\n const out: Record<string, string | undefined> = {};\n if (!truck) return out;\n for (const [field, param] of Object.entries(TRUCK_PARAMS) as [keyof TruckProfile, string][]) {\n const v = truck[field];\n if (v != null) out[param] = String(v);\n }\n return out;\n}\n\nexport interface RouteResult {\n distanceMeters: number;\n durationSeconds: number;\n geometry: { type: \"LineString\"; coordinates: LngLat[] };\n}\n\n/** Minimal GeoJSON FeatureCollection (isochrone bands). */\nexport interface FeatureCollection {\n type: \"FeatureCollection\";\n features: unknown[];\n}\n\nexport interface IsochroneOptions extends RouteOptions {\n minutes: number[];\n}\n\nexport class RouterError extends Error {\n constructor(\n public readonly status: number,\n message: string,\n /** The gateway's machine-readable `code`, when the response carried one (e.g. `unauthorized`). */\n public readonly code?: string,\n ) {\n super(message);\n this.name = \"RouterError\";\n }\n}\n\n/**\n * The gateway answers every error with a JSON body whose `error` says what went wrong\n * (\"mode must be one of auto, car, bicycle, pedestrian, truck\", \"q required\"). Put that in the\n * message so a caller who logs the thrown error reads the reason, not just the status.\n */\nasync function errorFrom(res: { status: number; json(): Promise<unknown> }, path: string): Promise<RouterError> {\n const fallback = `route ${res.status} on ${path}`;\n let body: { error?: unknown; code?: unknown } | undefined;\n try {\n body = (await res.json()) as { error?: unknown; code?: unknown };\n } catch {\n body = undefined;\n }\n const reason = typeof body?.error === \"string\" ? body.error : undefined;\n const code = typeof body?.code === \"string\" ? body.code : undefined;\n return new RouterError(res.status, reason ? `${reason} (${fallback})` : fallback, code);\n}\n\n/** Typed client for the unmap routing API (gateway `/route`, `/isochrone`). */\nexport class Router {\n private readonly key: string;\n private readonly base: string;\n private readonly fetchImpl: typeof fetch;\n\n constructor(opts: RouterOptions) {\n this.key = opts.key;\n this.base = (opts.gateway ?? DEFAULT_GATEWAY).replace(/\\/+$/, \"\");\n // Bound to globalThis: stored on an instance and called as `this.fetchImpl(...)`, a bare\n // `fetch` reference is invoked with the Router as its receiver, and browsers reject that\n // with \"Illegal invocation\". Node is lenient, so this only ever surfaces in the browser.\n this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);\n }\n\n route(from: LngLat, to: LngLat, opts: RouteOptions = {}): Promise<RouteResult> {\n return this.get<RouteResult>(\n \"/route\",\n { from: from.join(\",\"), to: to.join(\",\"), mode: opts.mode, ...truckParams(opts.truck) },\n opts.signal,\n );\n }\n\n isochrone(center: LngLat, opts: IsochroneOptions): Promise<FeatureCollection> {\n return this.get<FeatureCollection>(\n \"/isochrone\",\n {\n lon: String(center[0]),\n lat: String(center[1]),\n minutes: opts.minutes.join(\",\"),\n mode: opts.mode,\n ...truckParams(opts.truck),\n },\n opts.signal,\n );\n }\n\n private async get<T>(\n path: string,\n params: Record<string, string | undefined>,\n signal?: AbortSignal,\n ): Promise<T> {\n const q = new URLSearchParams({ key: this.key });\n for (const [k, v] of Object.entries(params)) if (v != null && v !== \"\") q.set(k, v);\n const res = await this.fetchImpl(`${this.base}${path}?${q}`, {\n signal,\n headers: { accept: \"application/json\" },\n });\n if (!res.ok) throw await errorFrom(res, path);\n return res.json() as Promise<T>;\n }\n}\n"],"mappings":";AAAA,SAAS,uBAAuB;AA6ChC,IAAM,eAAmD;AAAA,EACvD,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,eAAe;AACjB;AAEA,SAAS,YAAY,OAA0D;AAC7E,QAAM,MAA0C,CAAC;AACjD,MAAI,CAAC,MAAO,QAAO;AACnB,aAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,YAAY,GAAqC;AAC3F,UAAM,IAAI,MAAM,KAAK;AACrB,QAAI,KAAK,KAAM,KAAI,KAAK,IAAI,OAAO,CAAC;AAAA,EACtC;AACA,SAAO;AACT;AAkBO,IAAM,cAAN,cAA0B,MAAM;AAAA,EACrC,YACkB,QAChB,SAEgB,MAChB;AACA,UAAM,OAAO;AALG;AAGA;AAGhB,SAAK,OAAO;AAAA,EACd;AAAA,EAPkB;AAAA,EAGA;AAKpB;AAOA,eAAe,UAAU,KAAmD,MAAoC;AAC9G,QAAM,WAAW,SAAS,IAAI,MAAM,OAAO,IAAI;AAC/C,MAAI;AACJ,MAAI;AACF,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,SAAS,OAAO,MAAM,UAAU,WAAW,KAAK,QAAQ;AAC9D,QAAM,OAAO,OAAO,MAAM,SAAS,WAAW,KAAK,OAAO;AAC1D,SAAO,IAAI,YAAY,IAAI,QAAQ,SAAS,GAAG,MAAM,KAAK,QAAQ,MAAM,UAAU,IAAI;AACxF;AAGO,IAAM,SAAN,MAAa;AAAA,EACD;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,MAAqB;AAC/B,SAAK,MAAM,KAAK;AAChB,SAAK,QAAQ,KAAK,WAAW,iBAAiB,QAAQ,QAAQ,EAAE;AAIhE,SAAK,YAAY,KAAK,SAAS,WAAW,MAAM,KAAK,UAAU;AAAA,EACjE;AAAA,EAEA,MAAM,MAAc,IAAY,OAAqB,CAAC,GAAyB;AAC7E,WAAO,KAAK;AAAA,MACV;AAAA,MACA,EAAE,MAAM,KAAK,KAAK,GAAG,GAAG,IAAI,GAAG,KAAK,GAAG,GAAG,MAAM,KAAK,MAAM,GAAG,YAAY,KAAK,KAAK,EAAE;AAAA,MACtF,KAAK;AAAA,IACP;AAAA,EACF;AAAA,EAEA,UAAU,QAAgB,MAAoD;AAC5E,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,QACE,KAAK,OAAO,OAAO,CAAC,CAAC;AAAA,QACrB,KAAK,OAAO,OAAO,CAAC,CAAC;AAAA,QACrB,SAAS,KAAK,QAAQ,KAAK,GAAG;AAAA,QAC9B,MAAM,KAAK;AAAA,QACX,GAAG,YAAY,KAAK,KAAK;AAAA,MAC3B;AAAA,MACA,KAAK;AAAA,IACP;AAAA,EACF;AAAA,EAEA,MAAc,IACZ,MACA,QACA,QACY;AACZ,UAAM,IAAI,IAAI,gBAAgB,EAAE,KAAK,KAAK,IAAI,CAAC;AAC/C,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,EAAG,KAAI,KAAK,QAAQ,MAAM,GAAI,GAAE,IAAI,GAAG,CAAC;AAClF,UAAM,MAAM,MAAM,KAAK,UAAU,GAAG,KAAK,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI;AAAA,MAC3D;AAAA,MACA,SAAS,EAAE,QAAQ,mBAAmB;AAAA,IACxC,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,OAAM,MAAM,UAAU,KAAK,IAAI;AAC5C,WAAO,IAAI,KAAK;AAAA,EAClB;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { DEFAULT_GATEWAY } from \"@unmap/core\";\n\nexport interface RouterOptions {\n key: string;\n gateway?: string;\n fetch?: typeof fetch;\n}\n\n/** `car` is the same costing as `auto`, spelled for people who do not think in Valhalla terms. */\nexport type TravelMode = \"auto\" | \"car\" | \"bicycle\" | \"pedestrian\" | \"truck\";\n/** [longitude, latitude]. */\nexport type LngLat = [number, number];\n\n/**\n * Vehicle profile for `mode: \"truck\"`. Metric throughout: metres and tonnes. Every field is\n * optional; the gateway falls back to Valhalla's defaults (4.11 m high, 2.6 m wide, 21.64 m\n * long, 21.77 t, 9.07 t axle load, 5 axles, no hazmat) for anything omitted.\n */\nexport interface TruckProfile {\n /** Vehicle height in metres. */\n height?: number;\n /** Vehicle width in metres. */\n width?: number;\n /** Vehicle length in metres. */\n length?: number;\n /** Gross vehicle weight in tonnes. */\n weight?: number;\n /** Load per axle in tonnes (0–40). */\n axleLoad?: number;\n /** Number of axles (2–20). */\n axleCount?: number;\n /** Carrying hazardous materials: avoids roads tagged `hazmat=no`. */\n hazmat?: boolean;\n /** Preference for designated truck routes, 0 (none) to 1 (strong). */\n useTruckRoute?: number;\n}\n\nexport interface RouteOptions {\n mode?: TravelMode;\n /** Only meaningful with `mode: \"truck\"`; the gateway rejects it on any other mode. */\n truck?: TruckProfile;\n /**\n * Willingness to take motorways and trunks, 0 (avoid) to 1 (prefer). Driving\n * modes only (`auto`, `car`, `truck`). This is Valhalla's existing costing\n * option on the Canada graph, not a second router and not resource/winter/field routing.\n */\n useHighways?: number;\n /**\n * Willingness to climb, 0 (avoid hills even for a longer route) to 1 (indifferent).\n * `bicycle` and `pedestrian` only; the gateway rejects it on the driving modes, whose\n * costing has no grade term at all. Grades come from the national 30 m elevation model,\n * baked into the Canada graph at build time.\n */\n useHills?: number;\n signal?: AbortSignal;\n}\n\n/** `TruckProfile` field → the gateway's snake_case query parameter. */\nconst TRUCK_PARAMS: Record<keyof TruckProfile, string> = {\n height: \"height\",\n width: \"width\",\n length: \"length\",\n weight: \"weight\",\n axleLoad: \"axle_load\",\n axleCount: \"axle_count\",\n hazmat: \"hazmat\",\n useTruckRoute: \"use_truck_route\",\n};\n\nfunction truckParams(truck?: TruckProfile): Record<string, string | undefined> {\n const out: Record<string, string | undefined> = {};\n if (!truck) return out;\n for (const [field, param] of Object.entries(TRUCK_PARAMS) as [keyof TruckProfile, string][]) {\n const v = truck[field];\n if (v != null) out[param] = String(v);\n }\n return out;\n}\n\n/** `TruckProfile` field to the gateway's snake_case body field, values left as numbers. */\nfunction truckBody(truck?: TruckProfile): Record<string, number | boolean> {\n const out: Record<string, number | boolean> = {};\n if (!truck) return out;\n for (const [field, param] of Object.entries(TRUCK_PARAMS) as [keyof TruckProfile, string][]) {\n const v = truck[field];\n if (v != null) out[param] = v;\n }\n return out;\n}\n\n/**\n * The option fields both POST endpoints accept, shaped for a JSON body.\n *\n * Every option `RouteOptions` declares has to appear here. One left out still typechecks at the\n * call site and is then dropped on the floor, so the caller asks for something and silently does\n * not get it.\n */\nfunction bodyOptions(opts: RouteOptions): Record<string, unknown> {\n return {\n ...(opts.mode ? { mode: opts.mode } : {}),\n ...(opts.useHighways != null ? { use_highways: opts.useHighways } : {}),\n ...(opts.useHills != null ? { use_hills: opts.useHills } : {}),\n ...truckBody(opts.truck),\n };\n}\n\n/**\n * The state of the routing graph behind every other call here.\n *\n * `graph` is absent when the engine did not report a build time. That is a real state, not an\n * error: the deployed Canada graph was built before the service was configured to report one.\n * Treat its absence as \"unknown\", never as \"fresh\".\n */\nexport interface RoutingStatus {\n /** The only graph there is. Canada-only is an architectural constraint, not a rollout stage. */\n region: \"canada\";\n /** `unavailable` means the routing container did not answer, not that this call failed. */\n status: \"ready\" | \"unavailable\";\n engine: { name: \"valhalla\"; version?: string };\n graph?: {\n /** When the tileset was last written, ISO 8601. */\n builtAt: string;\n /** Whole hours since `builtAt`. */\n ageHours: number;\n };\n}\n\nexport interface RouteResult {\n distanceMeters: number;\n durationSeconds: number;\n geometry: { type: \"LineString\"; coordinates: LngLat[] };\n}\n\n/**\n * Travel times and distances between every origin and every destination.\n *\n * If any pair is unreachable, or beyond the engine's own road-distance limit, `matrix()`\n * rejects with a `RouterError` (status 422, code `no_route`) instead of resolving with a\n * `null` cell for that pair: `null` never appears in a resolved `MatrixResult` in practice. It\n * stays in the type as a defensive case, costing nothing and remaining correct if a future\n * engine version starts nulling cells instead of failing the request.\n */\nexport interface MatrixResult {\n /** Seconds. Rows are origins, columns are destinations. */\n durations: (number | null)[][];\n /** Metres, same indexing. */\n distances: (number | null)[][];\n}\n\n/**\n * A route through a set of stops, plus the order to visit them in.\n *\n * A route with one extra field rather than a shape of its own, the same reasoning that makes\n * `match()` return `RouteResult` outright: an optimized route is a route.\n */\nexport interface OptimizedRouteResult extends RouteResult {\n /** Indices into the request's `stops`, in visiting order. Always a permutation of them. */\n order: number[];\n}\n\n/** Minimal GeoJSON FeatureCollection (isochrone bands). */\nexport interface FeatureCollection {\n type: \"FeatureCollection\";\n features: unknown[];\n}\n\nexport interface IsochroneOptions extends RouteOptions {\n minutes: number[];\n}\n\nexport class RouterError extends Error {\n constructor(\n public readonly status: number,\n message: string,\n /** The gateway's machine-readable `code`, when the response carried one (e.g. `unauthorized`). */\n public readonly code?: string,\n ) {\n super(message);\n this.name = \"RouterError\";\n }\n}\n\n/**\n * The gateway answers every error with a JSON body whose `error` says what went wrong\n * (\"mode must be one of auto, car, bicycle, pedestrian, truck\", \"q required\"). Put that in the\n * message so a caller who logs the thrown error reads the reason, not just the status.\n */\nasync function errorFrom(res: { status: number; json(): Promise<unknown> }, path: string): Promise<RouterError> {\n const fallback = `route ${res.status} on ${path}`;\n let body: { error?: unknown; code?: unknown } | undefined;\n try {\n body = (await res.json()) as { error?: unknown; code?: unknown };\n } catch {\n body = undefined;\n }\n const reason = typeof body?.error === \"string\" ? body.error : undefined;\n const code = typeof body?.code === \"string\" ? body.code : undefined;\n return new RouterError(res.status, reason ? `${reason} (${fallback})` : fallback, code);\n}\n\n/** Typed client for the unmap routing API (gateway `/route`, `/isochrone`, `/matrix`, `/match`, `/optimized-route`). */\nexport class Router {\n private readonly key: string;\n private readonly base: string;\n private readonly fetchImpl: typeof fetch;\n\n constructor(opts: RouterOptions) {\n this.key = opts.key;\n this.base = (opts.gateway ?? DEFAULT_GATEWAY).replace(/\\/+$/, \"\");\n // Bound to globalThis: stored on an instance and called as `this.fetchImpl(...)`, a bare\n // `fetch` reference is invoked with the Router as its receiver, and browsers reject that\n // with \"Illegal invocation\". Node is lenient, so this only ever surfaces in the browser.\n this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);\n }\n\n route(from: LngLat, to: LngLat, opts: RouteOptions = {}): Promise<RouteResult> {\n return this.get<RouteResult>(\n \"/route\",\n {\n from: from.join(\",\"),\n to: to.join(\",\"),\n mode: opts.mode,\n use_highways: opts.useHighways != null ? String(opts.useHighways) : undefined,\n use_hills: opts.useHills != null ? String(opts.useHills) : undefined,\n ...truckParams(opts.truck),\n },\n opts.signal,\n );\n }\n\n isochrone(center: LngLat, opts: IsochroneOptions): Promise<FeatureCollection> {\n return this.get<FeatureCollection>(\n \"/isochrone\",\n {\n lon: String(center[0]),\n lat: String(center[1]),\n minutes: opts.minutes.join(\",\"),\n mode: opts.mode,\n use_highways: opts.useHighways != null ? String(opts.useHighways) : undefined,\n use_hills: opts.useHills != null ? String(opts.useHills) : undefined,\n ...truckParams(opts.truck),\n },\n opts.signal,\n );\n }\n\n /**\n * The routing graph's state: whether the engine is answering, which version it is, and how\n * old the Canada graph is.\n *\n * Resolves rather than rejects when routing is down: it answers `status: \"unavailable\"`,\n * because a status call that throws when the subject is unhealthy tells you nothing a failed\n * `route()` had not already told you.\n */\n status(opts: { signal?: AbortSignal } = {}): Promise<RoutingStatus> {\n return this.get<RoutingStatus>(\"/routing/status\", {}, opts.signal);\n }\n\n /**\n * Travel times and distances between every origin and every destination.\n *\n * At most 10 origins and 10 destinations. A `0` is a real answer, which is what you get\n * where an origin is also a destination. If any pair is unreachable, or beyond the engine's\n * own road-distance limit, the whole call rejects with a `RouterError` (422, `no_route`)\n * rather than resolving with a `null` cell for that pair.\n */\n matrix(origins: LngLat[], destinations: LngLat[], opts: RouteOptions = {}): Promise<MatrixResult> {\n return this.post<MatrixResult>(\"/matrix\", { origins, destinations, ...bodyOptions(opts) }, opts.signal);\n }\n\n /**\n * Snap a GPS trace to the road network. At least 2 and at most 100 coordinates.\n *\n * Returns the same `RouteResult` `route()` returns: a matched trace is a route.\n */\n match(coordinates: LngLat[], opts: RouteOptions = {}): Promise<RouteResult> {\n return this.post<RouteResult>(\"/match\", { coordinates, ...bodyOptions(opts) }, opts.signal);\n }\n\n /**\n * Order a set of stops and return the route through them. At least 3 and at most 10 stops.\n *\n * The first stop is the fixed start and the last is the fixed end; only the stops between\n * them are reordered. There is no round-trip or open-route toggle: that is a real constraint\n * of the underlying engine, not an omission here.\n *\n * Optimizing solves a cost matrix over every stop, so it is expected to fail the same way `matrix()` does:\n * if any stop is unreachable, or the set is spread further apart than the engine's own\n * road-distance limit allows, the whole call rejects with a `RouterError` (422, `no_route`)\n * rather than resolving with a partial tour.\n */\n optimize(stops: LngLat[], opts: RouteOptions = {}): Promise<OptimizedRouteResult> {\n return this.post<OptimizedRouteResult>(\"/optimized-route\", { stops, ...bodyOptions(opts) }, opts.signal);\n }\n\n private async post<T>(path: string, body: unknown, signal?: AbortSignal): Promise<T> {\n const q = new URLSearchParams({ key: this.key });\n const res = await this.fetchImpl(`${this.base}${path}?${q}`, {\n method: \"POST\",\n signal,\n headers: { accept: \"application/json\", \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n if (!res.ok) throw await errorFrom(res, path);\n return res.json() as Promise<T>;\n }\n\n private async get<T>(\n path: string,\n params: Record<string, string | undefined>,\n signal?: AbortSignal,\n ): Promise<T> {\n const q = new URLSearchParams({ key: this.key });\n for (const [k, v] of Object.entries(params)) if (v != null && v !== \"\") q.set(k, v);\n const res = await this.fetchImpl(`${this.base}${path}?${q}`, {\n signal,\n headers: { accept: \"application/json\" },\n });\n if (!res.ok) throw await errorFrom(res, path);\n return res.json() as Promise<T>;\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unmap/routing",
3
- "version": "0.9.1",
3
+ "version": "0.12.0",
4
4
  "description": "Typed client for the unmap routing API (route + isochrone)",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -38,7 +38,7 @@
38
38
  "access": "public"
39
39
  },
40
40
  "dependencies": {
41
- "@unmap/core": "0.9.1"
41
+ "@unmap/core": "0.12.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "tsup": "^8.3.0",