@unmap/routing 0.20.0 → 0.21.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 +52 -0
- package/dist/index.d.ts +492 -4
- package/dist/index.js +115 -8
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -38,5 +38,57 @@ Transit requires `departAt` and works on `route()` and `isochrone()`; a transit
|
|
|
38
38
|
`outside_coverage`. Production routes the agencies `/coverage.json` lists as routing-ready. There is
|
|
39
39
|
no `arriveBy`, transit matrix, or realtime claim.
|
|
40
40
|
|
|
41
|
+
## Optimize tours
|
|
42
|
+
|
|
43
|
+
`optimizeTours()` is Unmap Optimize (`POST /optimize`): it assigns shipments to vehicles and orders
|
|
44
|
+
their visits under time windows, capacities, breaks and route limits. The request is Google Route
|
|
45
|
+
Optimization's `OptimizeToursRequest`, camelCase or snake_case, and `response` is its
|
|
46
|
+
`OptimizeToursResponse`, unchanged. It is not `router.optimize()`, which orders the stops of one
|
|
47
|
+
vehicle.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { optimizeTours, UnmapOptimizeError } from '@unmap/routing'
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const { response, warnings, billableShipments } = await optimizeTours(
|
|
54
|
+
{
|
|
55
|
+
model: {
|
|
56
|
+
shipments: [{ deliveries: [{ arrivalLocation: { latitude: 51.05, longitude: -114.07 } }] }],
|
|
57
|
+
vehicles: [{ startLocation: { latitude: 51.04, longitude: -114.06 } }],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{ key: 'um_live_...' },
|
|
61
|
+
)
|
|
62
|
+
response.routes // one ShipmentRoute per vehicle that was used
|
|
63
|
+
response.skippedShipments
|
|
64
|
+
} catch (e) {
|
|
65
|
+
if (e instanceof UnmapOptimizeError) console.error(e.googleStatus, e.details)
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The same call is `router.optimizeTours(request)` on a `Router`, and `unmap.router.optimizeTours(request)`
|
|
70
|
+
from `@unmap/sdk`. The key is sent in the `X-API-Key` header, not in the URL.
|
|
71
|
+
|
|
72
|
+
What Unmap knows beyond Google's response comes back beside it, never inside it:
|
|
73
|
+
|
|
74
|
+
- `warnings`: the parsed `X-Unmap-Optimize-Warnings` header, an array of
|
|
75
|
+
`{ code, field?, path?, count?, message }` for fields Unmap accepts but cannot honour, road travel
|
|
76
|
+
the routing budget decided, and route polylines that could not be drawn. Empty when there are none.
|
|
77
|
+
- `billableShipments`: the shipments this request billed (the `X-Unmap-Billable-Shipments` header).
|
|
78
|
+
`0` for `solvingMode: 'VALIDATE_ONLY'`, for a shipment re-solved within 24 hours, and when the
|
|
79
|
+
header is absent.
|
|
80
|
+
- `timeoutClamped` and `clampedTimeoutSeconds`: whether your `timeout` was over the plan's cap, and
|
|
81
|
+
the seconds the solve ran with instead.
|
|
82
|
+
|
|
83
|
+
A failure throws `UnmapOptimizeError`, which extends `UnmapError`. `status` is the HTTP status,
|
|
84
|
+
`code` the gateway's string code (`bad_request`, `rate_limited`, `quota_exceeded`),
|
|
85
|
+
`retryAfterSeconds` comes from `Retry-After`, `googleStatus` is Google's status name
|
|
86
|
+
(`INVALID_ARGUMENT`, `FAILED_PRECONDITION`, `RESOURCE_EXHAUSTED`) and `details` is Google's
|
|
87
|
+
`error.details`: a `google.rpc.BadRequest` lists each field violation, and a `PreconditionFailure`
|
|
88
|
+
lists the mandatory shipments no vehicle can serve. Retries follow the `retry` option, on by default
|
|
89
|
+
for `429`, `502`, `503` and transport failures. A failed request is never billed. Until Unmap
|
|
90
|
+
Optimize is enabled on the gateway, every call is a `404` with `code: 'not_found'` and no
|
|
91
|
+
`googleStatus`.
|
|
92
|
+
|
|
41
93
|
Parameters, response shapes, and error bodies are documented at
|
|
42
94
|
[unmap.dev/docs/apis/routing](https://unmap.dev/docs/apis/routing).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,401 @@
|
|
|
1
1
|
import { RetryPolicy, UnmapError } from '@unmap/core';
|
|
2
2
|
export { RetryPolicy, UnmapError, parseRetryAfter } from '@unmap/core';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Wire types for the GMPRO (Google Maps Platform Route Optimization) `optimizeTours` surface that
|
|
6
|
+
* Unmap Optimize accepts and returns. Field names are the camelCase JSON names; the validator turns
|
|
7
|
+
* snake_case input into these before anything reads it.
|
|
8
|
+
*
|
|
9
|
+
* Every field the field-status table classifies is typed here, including the ones Unmap refuses,
|
|
10
|
+
* so a GMPRO request type-checks against these types. Whether a field is honoured is the table's
|
|
11
|
+
* business (`fields.ts`), not the type's.
|
|
12
|
+
*
|
|
13
|
+
* Scalars follow proto3 JSON: a {@link Duration} is a string such as "300s" or "1.5s", a
|
|
14
|
+
* {@link Timestamp} is an RFC 3339 string or `{ seconds, nanos }`, and an {@link Int64} is a
|
|
15
|
+
* decimal string or a safe JSON integer.
|
|
16
|
+
*/
|
|
17
|
+
/** Seconds with an "s" suffix: "300s", "1.5s". */
|
|
18
|
+
type Duration = string;
|
|
19
|
+
/** RFC 3339 ("2026-10-12T08:00:00Z") or the object form with int64 seconds. */
|
|
20
|
+
type Timestamp = string | {
|
|
21
|
+
seconds?: Int64;
|
|
22
|
+
nanos?: number;
|
|
23
|
+
};
|
|
24
|
+
/** A 64-bit integer as a decimal string or a safe JSON integer. */
|
|
25
|
+
type Int64 = string | number;
|
|
26
|
+
type SolvingMode = "DEFAULT_SOLVE" | "VALIDATE_ONLY" | "DETECT_SOME_INFEASIBLE_SHIPMENTS" | "TRANSFORM_AND_RETURN_REQUEST";
|
|
27
|
+
type SearchMode = "SEARCH_MODE_UNSPECIFIED" | "RETURN_FAST" | "CONSUME_ALL_AVAILABLE_TIME";
|
|
28
|
+
type TravelMode$1 = "TRAVEL_MODE_UNSPECIFIED" | "DRIVING" | "WALKING";
|
|
29
|
+
type UnloadingPolicy = "UNLOADING_POLICY_UNSPECIFIED" | "LAST_IN_FIRST_OUT" | "FIRST_IN_FIRST_OUT";
|
|
30
|
+
/** Unmap extension: the road network profile a vehicle travels on. Only "auto" in V1. */
|
|
31
|
+
type UnmapProfile = "auto" | "truck" | "pedestrian";
|
|
32
|
+
interface LatLng {
|
|
33
|
+
latitude?: number;
|
|
34
|
+
longitude?: number;
|
|
35
|
+
}
|
|
36
|
+
interface Location {
|
|
37
|
+
latLng?: LatLng;
|
|
38
|
+
heading?: number;
|
|
39
|
+
}
|
|
40
|
+
interface Waypoint {
|
|
41
|
+
location?: Location;
|
|
42
|
+
placeId?: string;
|
|
43
|
+
sideOfRoad?: boolean;
|
|
44
|
+
vehicleStopover?: boolean;
|
|
45
|
+
}
|
|
46
|
+
interface TimeWindow {
|
|
47
|
+
startTime?: Timestamp;
|
|
48
|
+
endTime?: Timestamp;
|
|
49
|
+
softStartTime?: Timestamp;
|
|
50
|
+
softEndTime?: Timestamp;
|
|
51
|
+
costPerHourBeforeSoftStartTime?: number;
|
|
52
|
+
costPerHourAfterSoftEndTime?: number;
|
|
53
|
+
}
|
|
54
|
+
interface Load {
|
|
55
|
+
amount?: Int64;
|
|
56
|
+
}
|
|
57
|
+
interface VisitRequest {
|
|
58
|
+
arrivalLocation?: LatLng;
|
|
59
|
+
arrivalWaypoint?: Waypoint;
|
|
60
|
+
departureLocation?: LatLng;
|
|
61
|
+
departureWaypoint?: Waypoint;
|
|
62
|
+
tags?: string[];
|
|
63
|
+
timeWindows?: TimeWindow[];
|
|
64
|
+
duration?: Duration;
|
|
65
|
+
cost?: number;
|
|
66
|
+
loadDemands?: Record<string, Load>;
|
|
67
|
+
visitTypes?: string[];
|
|
68
|
+
label?: string;
|
|
69
|
+
avoidUTurns?: boolean;
|
|
70
|
+
}
|
|
71
|
+
interface Shipment {
|
|
72
|
+
displayName?: string;
|
|
73
|
+
pickups?: VisitRequest[];
|
|
74
|
+
deliveries?: VisitRequest[];
|
|
75
|
+
loadDemands?: Record<string, Load>;
|
|
76
|
+
penaltyCost?: number;
|
|
77
|
+
allowedVehicleIndices?: number[];
|
|
78
|
+
costsPerVehicle?: number[];
|
|
79
|
+
costsPerVehicleIndices?: number[];
|
|
80
|
+
pickupToDeliveryRelativeDetourLimit?: number;
|
|
81
|
+
pickupToDeliveryAbsoluteDetourLimit?: Duration;
|
|
82
|
+
pickupToDeliveryTimeLimit?: Duration;
|
|
83
|
+
shipmentType?: string;
|
|
84
|
+
label?: string;
|
|
85
|
+
ignore?: boolean;
|
|
86
|
+
}
|
|
87
|
+
interface Interval {
|
|
88
|
+
min?: Int64;
|
|
89
|
+
max?: Int64;
|
|
90
|
+
}
|
|
91
|
+
interface LoadCost {
|
|
92
|
+
loadThreshold?: Int64;
|
|
93
|
+
costPerUnitBelowThreshold?: number;
|
|
94
|
+
costPerUnitAboveThreshold?: number;
|
|
95
|
+
}
|
|
96
|
+
interface LoadLimit {
|
|
97
|
+
maxLoad?: Int64;
|
|
98
|
+
softMaxLoad?: Int64;
|
|
99
|
+
costPerUnitAboveSoftMax?: number;
|
|
100
|
+
startLoadInterval?: Interval;
|
|
101
|
+
endLoadInterval?: Interval;
|
|
102
|
+
costPerKilometer?: LoadCost;
|
|
103
|
+
costPerTraveledHour?: LoadCost;
|
|
104
|
+
}
|
|
105
|
+
interface DurationLimit {
|
|
106
|
+
maxDuration?: Duration;
|
|
107
|
+
softMaxDuration?: Duration;
|
|
108
|
+
quadraticSoftMaxDuration?: Duration;
|
|
109
|
+
costPerHourAfterSoftMax?: number;
|
|
110
|
+
costPerSquareHourAfterQuadraticSoftMax?: number;
|
|
111
|
+
}
|
|
112
|
+
interface DistanceLimit {
|
|
113
|
+
maxMeters?: Int64;
|
|
114
|
+
softMaxMeters?: Int64;
|
|
115
|
+
costPerKilometerBelowSoftMax?: number;
|
|
116
|
+
costPerKilometerAboveSoftMax?: number;
|
|
117
|
+
}
|
|
118
|
+
interface BreakRequest {
|
|
119
|
+
earliestStartTime?: Timestamp;
|
|
120
|
+
latestStartTime?: Timestamp;
|
|
121
|
+
minDuration?: Duration;
|
|
122
|
+
}
|
|
123
|
+
interface FrequencyConstraint {
|
|
124
|
+
minBreakDuration?: Duration;
|
|
125
|
+
maxInterBreakDuration?: Duration;
|
|
126
|
+
}
|
|
127
|
+
interface BreakRule {
|
|
128
|
+
breakRequests?: BreakRequest[];
|
|
129
|
+
frequencyConstraints?: FrequencyConstraint[];
|
|
130
|
+
}
|
|
131
|
+
interface RouteModifiers {
|
|
132
|
+
avoidTolls?: boolean;
|
|
133
|
+
avoidHighways?: boolean;
|
|
134
|
+
avoidFerries?: boolean;
|
|
135
|
+
avoidIndoor?: boolean;
|
|
136
|
+
}
|
|
137
|
+
interface Vehicle {
|
|
138
|
+
displayName?: string;
|
|
139
|
+
travelMode?: TravelMode$1;
|
|
140
|
+
routeModifiers?: RouteModifiers;
|
|
141
|
+
startLocation?: LatLng;
|
|
142
|
+
startWaypoint?: Waypoint;
|
|
143
|
+
endLocation?: LatLng;
|
|
144
|
+
endWaypoint?: Waypoint;
|
|
145
|
+
startTags?: string[];
|
|
146
|
+
endTags?: string[];
|
|
147
|
+
startTimeWindows?: TimeWindow[];
|
|
148
|
+
endTimeWindows?: TimeWindow[];
|
|
149
|
+
unloadingPolicy?: UnloadingPolicy;
|
|
150
|
+
loadLimits?: Record<string, LoadLimit>;
|
|
151
|
+
costPerHour?: number;
|
|
152
|
+
costPerTraveledHour?: number;
|
|
153
|
+
costPerKilometer?: number;
|
|
154
|
+
fixedCost?: number;
|
|
155
|
+
usedIfRouteIsEmpty?: boolean;
|
|
156
|
+
routeDurationLimit?: DurationLimit;
|
|
157
|
+
travelDurationLimit?: DurationLimit;
|
|
158
|
+
routeDistanceLimit?: DistanceLimit;
|
|
159
|
+
extraVisitDurationForVisitType?: Record<string, Duration>;
|
|
160
|
+
breakRule?: BreakRule;
|
|
161
|
+
label?: string;
|
|
162
|
+
ignore?: boolean;
|
|
163
|
+
travelDurationMultiple?: number;
|
|
164
|
+
/** Unmap extension. Never a GMPRO field name, so it cannot collide with a future one. */
|
|
165
|
+
unmapProfile?: UnmapProfile;
|
|
166
|
+
}
|
|
167
|
+
interface DurationDistanceMatrixRow {
|
|
168
|
+
durations?: Duration[];
|
|
169
|
+
meters?: number[];
|
|
170
|
+
}
|
|
171
|
+
interface DurationDistanceMatrix {
|
|
172
|
+
rows?: DurationDistanceMatrixRow[];
|
|
173
|
+
vehicleStartTag?: string;
|
|
174
|
+
}
|
|
175
|
+
interface ShipmentModel {
|
|
176
|
+
shipments?: Shipment[];
|
|
177
|
+
vehicles?: Vehicle[];
|
|
178
|
+
objectives?: unknown[];
|
|
179
|
+
globalStartTime?: Timestamp;
|
|
180
|
+
globalEndTime?: Timestamp;
|
|
181
|
+
globalDurationCostPerHour?: number;
|
|
182
|
+
durationDistanceMatrices?: DurationDistanceMatrix[];
|
|
183
|
+
durationDistanceMatrixSrcTags?: string[];
|
|
184
|
+
durationDistanceMatrixDstTags?: string[];
|
|
185
|
+
transitionAttributes?: unknown[];
|
|
186
|
+
shipmentTypeIncompatibilities?: unknown[];
|
|
187
|
+
shipmentTypeRequirements?: unknown[];
|
|
188
|
+
precedenceRules?: unknown[];
|
|
189
|
+
maxActiveVehicles?: number;
|
|
190
|
+
}
|
|
191
|
+
interface OptimizeToursRequest {
|
|
192
|
+
parent?: string;
|
|
193
|
+
timeout?: Duration;
|
|
194
|
+
model?: ShipmentModel;
|
|
195
|
+
solvingMode?: SolvingMode;
|
|
196
|
+
searchMode?: SearchMode;
|
|
197
|
+
injectedFirstSolutionRoutes?: ShipmentRoute[];
|
|
198
|
+
injectedSolutionConstraint?: unknown;
|
|
199
|
+
refreshDetailsRoutes?: ShipmentRoute[];
|
|
200
|
+
interpretInjectedSolutionsUsingLabels?: boolean;
|
|
201
|
+
considerRoadTraffic?: boolean;
|
|
202
|
+
populatePolylines?: boolean;
|
|
203
|
+
populateTransitionPolylines?: boolean;
|
|
204
|
+
allowLargeDeadlineDespiteInterruptionRisk?: boolean;
|
|
205
|
+
useGeodesicDistances?: boolean;
|
|
206
|
+
label?: string;
|
|
207
|
+
geodesicMetersPerSecond?: number;
|
|
208
|
+
maxValidationErrors?: number;
|
|
209
|
+
}
|
|
210
|
+
interface EncodedPolyline {
|
|
211
|
+
points?: string;
|
|
212
|
+
}
|
|
213
|
+
interface VehicleLoad {
|
|
214
|
+
amount?: Int64;
|
|
215
|
+
}
|
|
216
|
+
interface Visit {
|
|
217
|
+
shipmentIndex?: number;
|
|
218
|
+
isPickup?: boolean;
|
|
219
|
+
visitRequestIndex?: number;
|
|
220
|
+
startTime?: Timestamp;
|
|
221
|
+
loadDemands?: Record<string, Load>;
|
|
222
|
+
detour?: Duration;
|
|
223
|
+
shipmentLabel?: string;
|
|
224
|
+
visitLabel?: string;
|
|
225
|
+
}
|
|
226
|
+
interface Transition {
|
|
227
|
+
travelDuration?: Duration;
|
|
228
|
+
travelDistanceMeters?: number;
|
|
229
|
+
trafficInfoUnavailable?: boolean;
|
|
230
|
+
delayDuration?: Duration;
|
|
231
|
+
breakDuration?: Duration;
|
|
232
|
+
waitDuration?: Duration;
|
|
233
|
+
totalDuration?: Duration;
|
|
234
|
+
startTime?: Timestamp;
|
|
235
|
+
routePolyline?: EncodedPolyline;
|
|
236
|
+
routeToken?: string;
|
|
237
|
+
vehicleLoads?: Record<string, VehicleLoad>;
|
|
238
|
+
}
|
|
239
|
+
interface Break {
|
|
240
|
+
startTime?: Timestamp;
|
|
241
|
+
duration?: Duration;
|
|
242
|
+
}
|
|
243
|
+
interface AggregatedMetrics {
|
|
244
|
+
performedShipmentCount?: number;
|
|
245
|
+
travelDuration?: Duration;
|
|
246
|
+
waitDuration?: Duration;
|
|
247
|
+
delayDuration?: Duration;
|
|
248
|
+
breakDuration?: Duration;
|
|
249
|
+
visitDuration?: Duration;
|
|
250
|
+
totalDuration?: Duration;
|
|
251
|
+
travelDistanceMeters?: number;
|
|
252
|
+
maxLoads?: Record<string, VehicleLoad>;
|
|
253
|
+
performedMandatoryShipmentCount?: number;
|
|
254
|
+
performedShipmentPenaltyCostSum?: number;
|
|
255
|
+
}
|
|
256
|
+
interface ShipmentRoute {
|
|
257
|
+
vehicleIndex?: number;
|
|
258
|
+
vehicleLabel?: string;
|
|
259
|
+
vehicleStartTime?: Timestamp;
|
|
260
|
+
vehicleEndTime?: Timestamp;
|
|
261
|
+
visits?: Visit[];
|
|
262
|
+
transitions?: Transition[];
|
|
263
|
+
hasTrafficInfeasibilities?: boolean;
|
|
264
|
+
routePolyline?: EncodedPolyline;
|
|
265
|
+
breaks?: Break[];
|
|
266
|
+
metrics?: AggregatedMetrics;
|
|
267
|
+
routeCosts?: Record<string, number>;
|
|
268
|
+
routeTotalCost?: number;
|
|
269
|
+
}
|
|
270
|
+
type SkippedShipmentReasonCode = "CODE_UNSPECIFIED" | "NO_VEHICLE" | "DEMAND_EXCEEDS_VEHICLE_CAPACITY" | "CANNOT_BE_PERFORMED_WITHIN_VEHICLE_DISTANCE_LIMIT" | "CANNOT_BE_PERFORMED_WITHIN_VEHICLE_DURATION_LIMIT" | "CANNOT_BE_PERFORMED_WITHIN_VEHICLE_TRAVEL_DURATION_LIMIT" | "CANNOT_BE_PERFORMED_WITHIN_VEHICLE_TIME_WINDOWS" | "VEHICLE_NOT_ALLOWED" | "VEHICLE_IGNORED" | "SHIPMENT_IGNORED" | "SKIPPED_IN_INJECTED_SOLUTION_CONSTRAINT" | "VEHICLE_ROUTE_IS_FULLY_SEQUENCE_CONSTRAINED" | "ZERO_PENALTY_COST";
|
|
271
|
+
interface SkippedShipmentReason {
|
|
272
|
+
code?: SkippedShipmentReasonCode;
|
|
273
|
+
exampleVehicleIndices?: number[];
|
|
274
|
+
exampleExceededCapacityType?: string;
|
|
275
|
+
exampleVehicleIndex?: number;
|
|
276
|
+
}
|
|
277
|
+
interface SkippedShipment {
|
|
278
|
+
index?: number;
|
|
279
|
+
label?: string;
|
|
280
|
+
reasons?: SkippedShipmentReason[];
|
|
281
|
+
penaltyCost?: number;
|
|
282
|
+
estimatedIncompatibleVehicleRatio?: number;
|
|
283
|
+
}
|
|
284
|
+
interface Metrics {
|
|
285
|
+
aggregatedRouteMetrics?: AggregatedMetrics;
|
|
286
|
+
skippedMandatoryShipmentCount?: number;
|
|
287
|
+
usedVehicleCount?: number;
|
|
288
|
+
earliestVehicleStartTime?: Timestamp;
|
|
289
|
+
latestVehicleEndTime?: Timestamp;
|
|
290
|
+
costs?: Record<string, number>;
|
|
291
|
+
totalCost?: number;
|
|
292
|
+
}
|
|
293
|
+
/** GMPRO FieldReference: a path into the request as a chain of names, indices and map keys. */
|
|
294
|
+
interface FieldReference {
|
|
295
|
+
name: string;
|
|
296
|
+
index?: number;
|
|
297
|
+
key?: string;
|
|
298
|
+
subField?: FieldReference;
|
|
299
|
+
}
|
|
300
|
+
/** GMPRO OptimizeToursValidationError as it appears in a response body: `code` is an integer. */
|
|
301
|
+
interface OptimizeToursValidationError {
|
|
302
|
+
code: number;
|
|
303
|
+
displayName: string;
|
|
304
|
+
fields: FieldReference[];
|
|
305
|
+
errorMessage: string;
|
|
306
|
+
offendingValues?: string;
|
|
307
|
+
}
|
|
308
|
+
interface OptimizeToursResponse {
|
|
309
|
+
routes?: ShipmentRoute[];
|
|
310
|
+
requestLabel?: string;
|
|
311
|
+
skippedShipments?: SkippedShipment[];
|
|
312
|
+
validationErrors?: OptimizeToursValidationError[];
|
|
313
|
+
metrics?: Metrics;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
interface OptimizeToursOptions {
|
|
317
|
+
key: string;
|
|
318
|
+
gateway?: string;
|
|
319
|
+
fetch?: typeof fetch;
|
|
320
|
+
/**
|
|
321
|
+
* Retry policy for `429`, `502`, `503` and transport failures. `false` disables it. A failed
|
|
322
|
+
* solve is never billed, so a retry costs nothing, but a solve that is still running when the
|
|
323
|
+
* connection drops is not cancelled: it holds one of your plan's concurrent solves until it ends.
|
|
324
|
+
* See `RetryPolicy` in `@unmap/core`.
|
|
325
|
+
*/
|
|
326
|
+
retry?: RetryPolicy | false;
|
|
327
|
+
signal?: AbortSignal;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* One entry of the `X-Unmap-Optimize-Warnings` header: a field Unmap accepted but cannot honour,
|
|
331
|
+
* a clamped timeout, road travel the routing budget decided, or a route polyline that could not be
|
|
332
|
+
* drawn. A final `WARNINGS_TRUNCATED` entry counts any that did not fit the header.
|
|
333
|
+
*/
|
|
334
|
+
interface OptimizeToursWarning {
|
|
335
|
+
code: string;
|
|
336
|
+
field?: string;
|
|
337
|
+
path?: string;
|
|
338
|
+
count?: number;
|
|
339
|
+
message: string;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* What `optimizeTours()` resolves with.
|
|
343
|
+
*
|
|
344
|
+
* `response` is Google's `OptimizeToursResponse`, exactly as the gateway sent it: Unmap never adds
|
|
345
|
+
* a field to it, so code written against Google's response shape reads `response` unchanged. What
|
|
346
|
+
* Unmap knows beyond that arrives in the response headers and is parsed here, beside it.
|
|
347
|
+
*/
|
|
348
|
+
interface OptimizeToursResult {
|
|
349
|
+
response: OptimizeToursResponse;
|
|
350
|
+
/** Parsed `X-Unmap-Optimize-Warnings`. Empty when there were none or the header was unreadable. */
|
|
351
|
+
warnings: OptimizeToursWarning[];
|
|
352
|
+
/**
|
|
353
|
+
* Parsed `X-Unmap-Billable-Shipments`: the shipments this request billed. `0` for
|
|
354
|
+
* `VALIDATE_ONLY`, a free re-solve within 24 hours, and when the header is absent. A dev origin
|
|
355
|
+
* is billed like any other.
|
|
356
|
+
*/
|
|
357
|
+
billableShipments: number;
|
|
358
|
+
/** True when your `timeout` was over the plan's cap and the gateway shortened it. */
|
|
359
|
+
timeoutClamped: boolean;
|
|
360
|
+
/** The timeout the solve ran with, in seconds, when `timeoutClamped`; otherwise `null`. */
|
|
361
|
+
clampedTimeoutSeconds: number | null;
|
|
362
|
+
}
|
|
363
|
+
/** One Google `Status.details` entry (`google.rpc.BadRequest`, `PreconditionFailure`, `RetryInfo`, ...). */
|
|
364
|
+
interface OptimizeToursErrorDetail {
|
|
365
|
+
"@type"?: string;
|
|
366
|
+
[key: string]: unknown;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* An Unmap Optimize failure.
|
|
370
|
+
*
|
|
371
|
+
* Extends `UnmapError`, so `instanceof UnmapError` catches it with every other unmap client's
|
|
372
|
+
* errors. `status` is the HTTP status, `code` the gateway's string code (`bad_request`,
|
|
373
|
+
* `rate_limited`, `quota_exceeded`, ...), and `retryAfterSeconds` comes from `Retry-After`.
|
|
374
|
+
* `googleStatus` is Google's RPC status name (`INVALID_ARGUMENT`, `FAILED_PRECONDITION`, ...) and
|
|
375
|
+
* `details` Google's `error.details`: a `google.rpc.BadRequest` lists each field violation, and a
|
|
376
|
+
* `PreconditionFailure` lists the mandatory shipments no vehicle can serve. A response that is
|
|
377
|
+
* not Google-shaped (the gateway's plain `404` when Optimize is not enabled) has no
|
|
378
|
+
* `googleStatus` and an empty `details`.
|
|
379
|
+
*/
|
|
380
|
+
declare class UnmapOptimizeError extends UnmapError {
|
|
381
|
+
readonly googleStatus?: string | undefined;
|
|
382
|
+
readonly details: OptimizeToursErrorDetail[];
|
|
383
|
+
constructor(status: number, message: string, code?: string, retryAfterSeconds?: number, googleStatus?: string | undefined, details?: OptimizeToursErrorDetail[]);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Assign shipments to vehicles and order their visits (Unmap Optimize, `POST /optimize`).
|
|
387
|
+
*
|
|
388
|
+
* The request and the `response` are Google Route Optimization's `OptimizeToursRequest` and
|
|
389
|
+
* `OptimizeToursResponse`. Unmap's own findings (warnings, the shipments billed) are returned
|
|
390
|
+
* beside the response, never inside it.
|
|
391
|
+
*
|
|
392
|
+
* const { response } = await optimizeTours(request, { key: "um_live_..." });
|
|
393
|
+
*
|
|
394
|
+
* The key is sent in `X-API-Key`, not in the URL. Billed per shipment performed; see
|
|
395
|
+
* `billableShipments`. A failure throws `UnmapOptimizeError`.
|
|
396
|
+
*/
|
|
397
|
+
declare function optimizeTours(request: OptimizeToursRequest, opts: OptimizeToursOptions): Promise<OptimizeToursResult>;
|
|
398
|
+
|
|
4
399
|
interface RouterOptions {
|
|
5
400
|
key: string;
|
|
6
401
|
gateway?: string;
|
|
@@ -142,6 +537,21 @@ interface RoadRouteOptions extends SharedRouteOptions {
|
|
|
142
537
|
}
|
|
143
538
|
/** Transit options are a separate branch so `departAt` cannot be omitted by accident. */
|
|
144
539
|
type RouteOptions = RoadRouteOptions | TransitRouteOptions;
|
|
540
|
+
/** Turn-by-turn options, taken by `route()`, `match()` and `optimize()`. */
|
|
541
|
+
interface StepOptions {
|
|
542
|
+
/**
|
|
543
|
+
* Return `steps`: written turn-by-turn directions for display. Off by default, so a route you
|
|
544
|
+
* only draw stays small.
|
|
545
|
+
*/
|
|
546
|
+
steps?: boolean;
|
|
547
|
+
/**
|
|
548
|
+
* Language of each step's `instruction`: `en` or `fr`. A regional tag resolves by its language
|
|
549
|
+
* (`fr-CA` is French) and any other language is English. Only with `steps: true`; the gateway
|
|
550
|
+
* rejects it otherwise. Without it the gateway reads the request's `Accept-Language` header,
|
|
551
|
+
* which a browser sends for you.
|
|
552
|
+
*/
|
|
553
|
+
lang?: string;
|
|
554
|
+
}
|
|
145
555
|
/**
|
|
146
556
|
* How well a class of truck restriction is covered for a route. `osm_only` is provenance, not a
|
|
147
557
|
* grade: Canada publishes no national bridge inventory, so OSM-derived is the honest description
|
|
@@ -408,6 +818,11 @@ interface RouteResult {
|
|
|
408
818
|
scheduleType?: "scheduled" | "realtime";
|
|
409
819
|
/** Walking access/egress and transit rides, in travel order. Present only for transit routes. */
|
|
410
820
|
legs?: RouteLeg[];
|
|
821
|
+
/**
|
|
822
|
+
* Turn-by-turn directions, in travel order. Present only when you pass `steps: true`. On a
|
|
823
|
+
* transit route they include the walk to and from each stop as well as the rides.
|
|
824
|
+
*/
|
|
825
|
+
steps?: RouteStep[];
|
|
411
826
|
}
|
|
412
827
|
interface TransitAgencySummary {
|
|
413
828
|
id: string;
|
|
@@ -447,6 +862,70 @@ interface TransitRouteLeg {
|
|
|
447
862
|
stops: TransitStopTime[];
|
|
448
863
|
}
|
|
449
864
|
type RouteLeg = PedestrianRouteLeg | TransitRouteLeg;
|
|
865
|
+
/**
|
|
866
|
+
* What a step asks the traveller to do. unmap's own names, so your code does not depend on the
|
|
867
|
+
* routing engine's internal numbering. Switch on this to choose an icon; show `instruction` as text.
|
|
868
|
+
*
|
|
869
|
+
* - `depart` / `arrive`: the first step of a route, and every stop it reaches (`/optimized-route`
|
|
870
|
+
* arrives at each stop and departs again).
|
|
871
|
+
* - `turn`, `uturn`, `continue`, `keep` (stay on a side at a split), `merge`, `name_change` (the
|
|
872
|
+
* road you are on changes name).
|
|
873
|
+
* - `ramp` (take a ramp) and `exit` (leave by an exit), both on limited-access roads.
|
|
874
|
+
* - `roundabout` (enter; `roundaboutExit` says which exit) and `roundabout_exit`.
|
|
875
|
+
* - `ferry` (board) and `ferry_exit`.
|
|
876
|
+
* - Transit: `transit` (board), `transit_transfer`, `transit_stay_on`, `station_enter`,
|
|
877
|
+
* `station_transfer`, `station_exit`.
|
|
878
|
+
* - Inside buildings on a walk: `elevator`, `stairs`, `escalator`, `building_enter`, `building_exit`.
|
|
879
|
+
* - `other`: a step the engine produced that unmap does not name yet. Show its instruction.
|
|
880
|
+
*/
|
|
881
|
+
type RouteStepType = "depart" | "arrive" | "turn" | "uturn" | "continue" | "keep" | "merge" | "name_change" | "ramp" | "exit" | "roundabout" | "roundabout_exit" | "ferry" | "ferry_exit" | "transit" | "transit_transfer" | "transit_stay_on" | "station_enter" | "station_transfer" | "station_exit" | "elevator" | "stairs" | "escalator" | "building_enter" | "building_exit" | "other";
|
|
882
|
+
/**
|
|
883
|
+
* Which way a step goes. Absent when the step has no direction (a roundabout, a transfer). On
|
|
884
|
+
* `depart` and `arrive` it is the side of the road (`left`/`right`); on `uturn` the direction of
|
|
885
|
+
* the turn.
|
|
886
|
+
*/
|
|
887
|
+
type RouteStepModifier = "sharp_left" | "left" | "slight_left" | "straight" | "slight_right" | "right" | "sharp_right";
|
|
888
|
+
/** The guide sign at a highway junction, as posted. Every list is absent when the sign has none. */
|
|
889
|
+
interface RouteStepSign {
|
|
890
|
+
/** Exit numbers, e.g. `["91B"]`. */
|
|
891
|
+
exitNumbers?: string[];
|
|
892
|
+
/** The road or route the exit leads onto, e.g. `["Highway 2 North"]`. */
|
|
893
|
+
branches?: string[];
|
|
894
|
+
/** Where the exit goes, e.g. `["Red Deer"]`. */
|
|
895
|
+
toward?: string[];
|
|
896
|
+
/** The interchange's own name. Rare in Canada. */
|
|
897
|
+
names?: string[];
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* One turn-by-turn step: written directions, for display. There is no voice narration.
|
|
901
|
+
*
|
|
902
|
+
* Present on a route only when you ask for `steps`. Steps cover the whole route in travel order, so
|
|
903
|
+
* their distances and durations add up to the route's, give or take rounding.
|
|
904
|
+
*/
|
|
905
|
+
interface RouteStep {
|
|
906
|
+
type: RouteStepType;
|
|
907
|
+
modifier?: RouteStepModifier;
|
|
908
|
+
/**
|
|
909
|
+
* The step as a sentence, e.g. "Turn left onto 17 Avenue Southwest.", in the language you asked
|
|
910
|
+
* for (`en` or `fr`). French is written by the engine's France French locale, not a Canadian
|
|
911
|
+
* one, so a few phrasings read as European French.
|
|
912
|
+
*/
|
|
913
|
+
instruction: string;
|
|
914
|
+
/** Names of the road the step travels along. Absent on an unnamed road. */
|
|
915
|
+
streetNames?: string[];
|
|
916
|
+
distanceMeters: number;
|
|
917
|
+
durationSeconds: number;
|
|
918
|
+
/** Where the step begins, `[lng, lat]`. The same point as `geometry.coordinates[geometryRange[0]]`. */
|
|
919
|
+
location: LngLat;
|
|
920
|
+
/**
|
|
921
|
+
* First and last index of this step's stretch in the route's `geometry.coordinates`, inclusive.
|
|
922
|
+
* `coordinates.slice(start, end + 1)` is the line to highlight for the step.
|
|
923
|
+
*/
|
|
924
|
+
geometryRange: [number, number];
|
|
925
|
+
/** On `type: "roundabout"`, which exit to take, counting from where you enter. */
|
|
926
|
+
roundaboutExit?: number;
|
|
927
|
+
sign?: RouteStepSign;
|
|
928
|
+
}
|
|
450
929
|
/**
|
|
451
930
|
* Travel times and distances between every origin and every destination.
|
|
452
931
|
*
|
|
@@ -515,7 +994,7 @@ declare class Router {
|
|
|
515
994
|
private readonly fetchImpl;
|
|
516
995
|
private readonly retry;
|
|
517
996
|
constructor(opts: RouterOptions);
|
|
518
|
-
route(from: LngLat, to: LngLat, opts?: RouteOptions): Promise<RouteResult>;
|
|
997
|
+
route(from: LngLat, to: LngLat, opts?: RouteOptions & StepOptions): Promise<RouteResult>;
|
|
519
998
|
isochrone(near: LngLat, opts: IsochroneOptions): Promise<FeatureCollection>;
|
|
520
999
|
/**
|
|
521
1000
|
* The routing graph's state: whether the engine is answering, which version it is, and how
|
|
@@ -548,7 +1027,7 @@ declare class Router {
|
|
|
548
1027
|
* outside WGS84 rejects with a `RouterError` (400, `invalid_location`). A point outside Canada
|
|
549
1028
|
* rejects with a `RouterError` (422, `outside_coverage`) before the graph is searched.
|
|
550
1029
|
*/
|
|
551
|
-
match(coordinates: LngLat[], opts?: RoadRouteOptions): Promise<RouteResult>;
|
|
1030
|
+
match(coordinates: LngLat[], opts?: RoadRouteOptions & StepOptions): Promise<RouteResult>;
|
|
552
1031
|
/**
|
|
553
1032
|
* Order a set of stops and return the route through them. At least 3 and at most 10 stops.
|
|
554
1033
|
*
|
|
@@ -564,9 +1043,18 @@ declare class Router {
|
|
|
564
1043
|
* connects, or two are further apart than the engine's straight-line matrix limit. There is no
|
|
565
1044
|
* partial tour either way, because a tour needs every leg.
|
|
566
1045
|
*/
|
|
567
|
-
optimize(stops: LngLat[], opts?: RoadRouteOptions): Promise<OptimizedRouteResult>;
|
|
1046
|
+
optimize(stops: LngLat[], opts?: RoadRouteOptions & StepOptions): Promise<OptimizedRouteResult>;
|
|
1047
|
+
/**
|
|
1048
|
+
* Assign shipments to vehicles and order their visits: Google Route Optimization's
|
|
1049
|
+
* `optimizeTours`, served by Unmap Optimize. See the standalone `optimizeTours()` for what it
|
|
1050
|
+
* resolves with and throws. This is `/optimized-route`'s bigger sibling: that one orders stops
|
|
1051
|
+
* for a single vehicle, this one plans a fleet.
|
|
1052
|
+
*/
|
|
1053
|
+
optimizeTours(request: OptimizeToursRequest, opts?: {
|
|
1054
|
+
signal?: AbortSignal;
|
|
1055
|
+
}): Promise<OptimizeToursResult>;
|
|
568
1056
|
private post;
|
|
569
1057
|
private get;
|
|
570
1058
|
}
|
|
571
1059
|
|
|
572
|
-
export { type AvoidAreaFeature, type AvoidAreaFeatureCollection, type AvoidAreaMultiPolygon, type AvoidAreaPolygon, type AvoidAreaPosition, type AvoidAreaRing, type AvoidAreas, type AvoidToken, type FeatureCollection, type IsochroneOptions, type LngLat, type MatrixResult, type OptimizedRouteResult, type PedestrianRouteLeg, type RoadRouteOptions, type RoadTravelMode, type RouteLeg, type RouteOptions, type RouteResult, type RouteWarning, Router, RouterError, type RouterOptions, type RoutingStatus, type TransitAgencySummary, type TransitRouteLeg, type TransitRouteOptions, type TransitRouteSummary, type TransitStopTime, type TravelMode, type TruckCoverage, type TruckMeta, type TruckProfile, type TruckValidation, type TruckValidationFinding, type TruckValidationStatus, type TruckVerification, type TruckWarning };
|
|
1060
|
+
export { type AvoidAreaFeature, type AvoidAreaFeatureCollection, type AvoidAreaMultiPolygon, type AvoidAreaPolygon, type AvoidAreaPosition, type AvoidAreaRing, type AvoidAreas, type AvoidToken, type FeatureCollection, type IsochroneOptions, type LngLat, type MatrixResult, type Shipment as OptimizeShipment, type ShipmentModel as OptimizeShipmentModel, type ShipmentRoute as OptimizeShipmentRoute, type SkippedShipment as OptimizeSkippedShipment, type OptimizeToursErrorDetail, type OptimizeToursOptions, type OptimizeToursRequest, type OptimizeToursResponse, type OptimizeToursResult, type OptimizeToursWarning, type Vehicle as OptimizeVehicle, type OptimizedRouteResult, type PedestrianRouteLeg, type RoadRouteOptions, type RoadTravelMode, type RouteLeg, type RouteOptions, type RouteResult, type RouteStep, type RouteStepModifier, type RouteStepSign, type RouteStepType, type RouteWarning, Router, RouterError, type RouterOptions, type RoutingStatus, type StepOptions, type TransitAgencySummary, type TransitRouteLeg, type TransitRouteOptions, type TransitRouteSummary, type TransitStopTime, type TravelMode, type TruckCoverage, type TruckMeta, type TruckProfile, type TruckValidation, type TruckValidationFinding, type TruckValidationStatus, type TruckVerification, type TruckWarning, UnmapOptimizeError, optimizeTours };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,98 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
import { DEFAULT_GATEWAY as DEFAULT_GATEWAY2, UnmapError as UnmapError2, fetchWithRetry as fetchWithRetry2, parseRetryAfter as parseRetryAfter2 } from "@unmap/core";
|
|
3
|
+
|
|
4
|
+
// src/optimize-tours.ts
|
|
2
5
|
import { DEFAULT_GATEWAY, UnmapError, fetchWithRetry, parseRetryAfter } from "@unmap/core";
|
|
3
|
-
|
|
6
|
+
var UnmapOptimizeError = class extends UnmapError {
|
|
7
|
+
constructor(status, message, code, retryAfterSeconds, googleStatus, details = []) {
|
|
8
|
+
super(status, message, code, retryAfterSeconds);
|
|
9
|
+
this.googleStatus = googleStatus;
|
|
10
|
+
this.details = details;
|
|
11
|
+
this.name = "UnmapOptimizeError";
|
|
12
|
+
}
|
|
13
|
+
googleStatus;
|
|
14
|
+
details;
|
|
15
|
+
};
|
|
16
|
+
async function optimizeErrorFrom(res) {
|
|
17
|
+
const fallback = `optimize ${res.status} on /optimize`;
|
|
18
|
+
let body;
|
|
19
|
+
try {
|
|
20
|
+
body = await res.json();
|
|
21
|
+
} catch {
|
|
22
|
+
body = void 0;
|
|
23
|
+
}
|
|
24
|
+
const top = typeof body === "object" && body !== null ? body : {};
|
|
25
|
+
const code = typeof top.code === "string" ? top.code : void 0;
|
|
26
|
+
const retryAfter = parseRetryAfter(res.headers?.get("retry-after") ?? null) ?? void 0;
|
|
27
|
+
let reason;
|
|
28
|
+
let googleStatus;
|
|
29
|
+
let details = [];
|
|
30
|
+
if (typeof top.error === "string") {
|
|
31
|
+
reason = top.error;
|
|
32
|
+
} else if (typeof top.error === "object" && top.error !== null) {
|
|
33
|
+
const g = top.error;
|
|
34
|
+
if (typeof g.message === "string") reason = g.message;
|
|
35
|
+
if (typeof g.status === "string") googleStatus = g.status;
|
|
36
|
+
if (Array.isArray(g.details)) {
|
|
37
|
+
details = g.details.filter((d) => typeof d === "object" && d !== null);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return new UnmapOptimizeError(res.status, reason ? `${reason} (${fallback})` : fallback, code, retryAfter, googleStatus, details);
|
|
41
|
+
}
|
|
42
|
+
function parseWarnings(header) {
|
|
43
|
+
if (!header) return [];
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(header);
|
|
46
|
+
if (!Array.isArray(parsed)) return [];
|
|
47
|
+
return parsed.filter(
|
|
48
|
+
(w) => typeof w === "object" && w !== null && typeof w.code === "string" && typeof w.message === "string"
|
|
49
|
+
);
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function parseBillable(header) {
|
|
55
|
+
if (!header || !/^[0-9]+$/.test(header.trim())) return 0;
|
|
56
|
+
return Number(header.trim());
|
|
57
|
+
}
|
|
58
|
+
function parseClamp(header) {
|
|
59
|
+
const m = header ? /timeout clamped to (\d+)s/.exec(header) : null;
|
|
60
|
+
return m ? Number(m[1]) : null;
|
|
61
|
+
}
|
|
62
|
+
async function optimizeTours(request, opts) {
|
|
63
|
+
const base = (opts.gateway ?? DEFAULT_GATEWAY).replace(/\/+$/, "");
|
|
64
|
+
const path = "/optimize";
|
|
65
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
66
|
+
const res = await fetchWithRetry(
|
|
67
|
+
fetchImpl,
|
|
68
|
+
`${base}${path}`,
|
|
69
|
+
{
|
|
70
|
+
method: "POST",
|
|
71
|
+
signal: opts.signal,
|
|
72
|
+
headers: { accept: "application/json", "content-type": "application/json", "x-api-key": opts.key },
|
|
73
|
+
body: JSON.stringify(request)
|
|
74
|
+
},
|
|
75
|
+
opts.retry
|
|
76
|
+
);
|
|
77
|
+
if (!res.ok) throw await optimizeErrorFrom(res);
|
|
78
|
+
let response;
|
|
79
|
+
try {
|
|
80
|
+
response = await res.json();
|
|
81
|
+
} catch {
|
|
82
|
+
throw new UnmapOptimizeError(res.status, `optimize ${res.status} on /optimize: the response body is not JSON`);
|
|
83
|
+
}
|
|
84
|
+
const clamped = parseClamp(res.headers?.get("warning") ?? null);
|
|
85
|
+
return {
|
|
86
|
+
response,
|
|
87
|
+
warnings: parseWarnings(res.headers?.get("x-unmap-optimize-warnings") ?? null),
|
|
88
|
+
billableShipments: parseBillable(res.headers?.get("x-unmap-billable-shipments") ?? null),
|
|
89
|
+
timeoutClamped: clamped !== null,
|
|
90
|
+
clampedTimeoutSeconds: clamped
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/index.ts
|
|
95
|
+
import { UnmapError as UnmapError3, parseRetryAfter as parseRetryAfter3 } from "@unmap/core";
|
|
4
96
|
var TRUCK_PARAMS = {
|
|
5
97
|
height: "height",
|
|
6
98
|
width: "width",
|
|
@@ -31,6 +123,8 @@ function truckBody(truck) {
|
|
|
31
123
|
}
|
|
32
124
|
function bodyOptions(opts) {
|
|
33
125
|
return {
|
|
126
|
+
...opts.steps != null ? { steps: opts.steps } : {},
|
|
127
|
+
...opts.lang != null ? { lang: opts.lang } : {},
|
|
34
128
|
...opts.mode ? { mode: opts.mode } : {},
|
|
35
129
|
...opts.departAt != null ? { depart_at: departureValue(opts.departAt) } : {},
|
|
36
130
|
...opts.avoid?.length ? { avoid: opts.avoid } : {},
|
|
@@ -43,7 +137,7 @@ function bodyOptions(opts) {
|
|
|
43
137
|
function departureValue(value) {
|
|
44
138
|
return value instanceof Date ? value.toISOString() : value;
|
|
45
139
|
}
|
|
46
|
-
var RouterError = class extends
|
|
140
|
+
var RouterError = class extends UnmapError2 {
|
|
47
141
|
constructor(status, message, code, retryAfterSeconds) {
|
|
48
142
|
super(status, message, code, retryAfterSeconds);
|
|
49
143
|
this.name = "RouterError";
|
|
@@ -59,7 +153,7 @@ async function errorFrom(res, path) {
|
|
|
59
153
|
}
|
|
60
154
|
const reason = typeof body?.error === "string" ? body.error : void 0;
|
|
61
155
|
const code = typeof body?.code === "string" ? body.code : void 0;
|
|
62
|
-
const retryAfter =
|
|
156
|
+
const retryAfter = parseRetryAfter2(res.headers?.get("retry-after") ?? null) ?? void 0;
|
|
63
157
|
return new RouterError(res.status, reason ? `${reason} (${fallback})` : fallback, code, retryAfter);
|
|
64
158
|
}
|
|
65
159
|
var Router = class {
|
|
@@ -70,7 +164,7 @@ var Router = class {
|
|
|
70
164
|
constructor(opts) {
|
|
71
165
|
this.retry = opts.retry;
|
|
72
166
|
this.key = opts.key;
|
|
73
|
-
this.base = (opts.gateway ??
|
|
167
|
+
this.base = (opts.gateway ?? DEFAULT_GATEWAY2).replace(/\/+$/, "");
|
|
74
168
|
this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
75
169
|
}
|
|
76
170
|
route(from, to, opts = {}) {
|
|
@@ -87,6 +181,8 @@ var Router = class {
|
|
|
87
181
|
avoid: opts.avoid?.length ? opts.avoid.join(",") : void 0,
|
|
88
182
|
use_highways: opts.useHighways != null ? String(opts.useHighways) : void 0,
|
|
89
183
|
use_hills: opts.useHills != null ? String(opts.useHills) : void 0,
|
|
184
|
+
steps: opts.steps != null ? String(opts.steps) : void 0,
|
|
185
|
+
lang: opts.lang,
|
|
90
186
|
...truckParams(opts.truck)
|
|
91
187
|
},
|
|
92
188
|
opts.signal
|
|
@@ -169,9 +265,18 @@ var Router = class {
|
|
|
169
265
|
optimize(stops, opts = {}) {
|
|
170
266
|
return this.post("/optimized-route", { stops, ...bodyOptions(opts) }, opts.signal);
|
|
171
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Assign shipments to vehicles and order their visits: Google Route Optimization's
|
|
270
|
+
* `optimizeTours`, served by Unmap Optimize. See the standalone `optimizeTours()` for what it
|
|
271
|
+
* resolves with and throws. This is `/optimized-route`'s bigger sibling: that one orders stops
|
|
272
|
+
* for a single vehicle, this one plans a fleet.
|
|
273
|
+
*/
|
|
274
|
+
optimizeTours(request, opts = {}) {
|
|
275
|
+
return optimizeTours(request, { key: this.key, gateway: this.base, fetch: this.fetchImpl, retry: this.retry, signal: opts.signal });
|
|
276
|
+
}
|
|
172
277
|
async post(path, body, signal) {
|
|
173
278
|
const q = new URLSearchParams({ key: this.key });
|
|
174
|
-
const res = await
|
|
279
|
+
const res = await fetchWithRetry2(
|
|
175
280
|
this.fetchImpl,
|
|
176
281
|
`${this.base}${path}?${q}`,
|
|
177
282
|
{
|
|
@@ -188,7 +293,7 @@ var Router = class {
|
|
|
188
293
|
async get(path, params, signal) {
|
|
189
294
|
const q = new URLSearchParams({ key: this.key });
|
|
190
295
|
for (const [k, v] of Object.entries(params)) if (v != null && v !== "") q.set(k, v);
|
|
191
|
-
const res = await
|
|
296
|
+
const res = await fetchWithRetry2(
|
|
192
297
|
this.fetchImpl,
|
|
193
298
|
`${this.base}${path}?${q}`,
|
|
194
299
|
{ signal, headers: { accept: "application/json" } },
|
|
@@ -201,7 +306,9 @@ var Router = class {
|
|
|
201
306
|
export {
|
|
202
307
|
Router,
|
|
203
308
|
RouterError,
|
|
204
|
-
|
|
205
|
-
|
|
309
|
+
UnmapError3 as UnmapError,
|
|
310
|
+
UnmapOptimizeError,
|
|
311
|
+
optimizeTours,
|
|
312
|
+
parseRetryAfter3 as parseRetryAfter
|
|
206
313
|
};
|
|
207
314
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { DEFAULT_GATEWAY, UnmapError, fetchWithRetry, parseRetryAfter, type RetryPolicy } from \"@unmap/core\";\n\nexport interface RouterOptions {\n key: string;\n gateway?: string;\n fetch?: typeof fetch;\n /**\n * Retry policy for `429`, `502`, `503` and transport failures. `false` disables it.\n *\n * On by default. `Retry-After` is on a `429` other than `billing_required`, and on a container\n * `503`. A `502` has none: routing and `/elevation` both use\n * `upstream_error`. With no header the client backs off itself. A wait longer than\n * `maxDelayMs` (10 s) is not slept through; the error carries `retryAfterSeconds` instead.\n * See `RetryPolicy` in `@unmap/core`.\n */\n retry?: RetryPolicy | false;\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\" | \"transit\";\nexport type RoadTravelMode = Exclude<TravelMode, \"transit\">;\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\n/**\n * A road feature to weight against. `tolls` and `highways` are driving modes only; `ferries`\n * applies to every road mode. `unpaved` is deliberately absent: Valhalla has no surface term in its\n * driving costing, so it could only ever have meant two different things by mode.\n */\nexport type AvoidToken = \"tolls\" | \"highways\" | \"ferries\";\n\n/** A GeoJSON position. The gateway requires exactly `[longitude, latitude]` at runtime. */\nexport type AvoidAreaPosition = number[];\n/** A closed GeoJSON linear ring. */\nexport type AvoidAreaRing = AvoidAreaPosition[];\n\n/** Polygon with one exterior ring. Interior rings are rejected by the gateway. */\nexport interface AvoidAreaPolygon {\n type: \"Polygon\";\n coordinates: AvoidAreaRing[];\n}\n\n/** Multiple polygons, each with one exterior ring. */\nexport interface AvoidAreaMultiPolygon {\n type: \"MultiPolygon\";\n coordinates: AvoidAreaRing[][];\n}\n\nexport interface AvoidAreaFeature {\n type: \"Feature\";\n geometry: AvoidAreaPolygon | AvoidAreaMultiPolygon;\n properties?: Record<string, unknown> | null;\n}\n\nexport interface AvoidAreaFeatureCollection {\n type: \"FeatureCollection\";\n features: AvoidAreaFeature[];\n}\n\nexport type AvoidAreas =\n | AvoidAreaPolygon\n | AvoidAreaMultiPolygon\n | AvoidAreaFeature\n | AvoidAreaFeatureCollection;\n\ninterface SharedRouteOptions {\n signal?: AbortSignal;\n}\n\nexport interface TransitRouteOptions extends SharedRouteOptions {\n mode: \"transit\";\n /**\n * Scheduled departure. A Date is serialized as an ISO instant; a\n * string is sent unchanged so callers can provide an ISO value with an explicit local offset.\n * Transit supports departure time only. There is no `arriveBy` option.\n */\n departAt: Date | string;\n avoid?: never;\n avoidAreas?: never;\n truck?: never;\n useHighways?: never;\n useHills?: never;\n}\n\nexport interface RoadRouteOptions extends SharedRouteOptions {\n mode?: RoadTravelMode;\n /** A scheduled departure applies only to `mode: \"transit\"`. */\n departAt?: never;\n /**\n * Road features to weight against.\n *\n * **A preference, not an exclusion.** These set Valhalla costing weights to 0, which\n * penalises the feature heavily but does not forbid it: a route whose only path crosses a\n * toll bridge still crosses it, and nothing in the result says that it did. If the\n * distinction matters to you, check the returned geometry. Avoid areas have a separate,\n * feature-gated API because they use Valhalla's exclusion mechanism rather than a weight.\n *\n * `tolls` and `highways` are driving modes only (`auto`, `car`, `truck`); `ferries` applies\n * to every road mode. `avoid: [\"highways\"]` and `useHighways` are the same knob at different\n * resolutions and the gateway rejects both together.\n */\n avoid?: AvoidToken[];\n /**\n * Areas to keep the route out of, as a GeoJSON `Polygon`, `MultiPolygon`, or\n * `FeatureCollection` of either.\n *\n * Valhalla defines these as exclusions rather than preferences. Server support remains behind\n * a feature flag while a defect in the pinned engine can snap a location across the polygon;\n * do not rely on this option until the service documentation says the flag is enabled.\n * Interior rings are rejected because the engine cannot preserve holes.\n *\n * At most 8 areas, 100 vertices and 10 km of perimeter in total.\n *\n * Sending this makes `route()` and `isochrone()` use POST instead of GET, because a polygon\n * cannot ride a query string. The response is identical either way.\n */\n avoidAreas?: AvoidAreas;\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}\n\n/** Transit options are a separate branch so `departAt` cannot be omitted by accident. */\nexport type RouteOptions = RoadRouteOptions | TransitRouteOptions;\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.departAt != null ? { depart_at: departureValue(opts.departAt) } : {}),\n ...(opts.avoid?.length ? { avoid: opts.avoid } : {}),\n ...(opts.avoidAreas != null ? { avoid_areas: opts.avoidAreas } : {}),\n ...(opts.useHighways != null ? { use_highways: opts.useHighways } : {}),\n ...(opts.useHills != null ? { use_hills: opts.useHills } : {}),\n ...truckBody(opts.truck),\n };\n}\n\nfunction departureValue(value: Date | string): string {\n return value instanceof Date ? value.toISOString() : value;\n}\n\n/**\n * How well a class of truck restriction is covered for a route. `osm_only` is provenance, not a\n * grade: Canada publishes no national bridge inventory, so OSM-derived is the honest description\n * of most of it. `not_modelled` means the routing engine cannot express the rule at all (a\n * conditional restriction); `not_checked` means nothing looked.\n */\nexport type TruckCoverage =\n | \"authoritative\"\n | \"osm_plus_authoritative\"\n | \"osm_only\"\n | \"not_modelled\"\n | \"not_checked\";\n\n/** How much of a truck route's restriction picture is trustworthy, overall. */\nexport type TruckVerification = \"authoritative\" | \"partial\" | \"osm_only\" | \"unknown\" | \"candidate_permit_route\";\n\nexport interface TruckWarning {\n /** Stable machine-readable code, e.g. `INCOMPLETE_VEHICLE_PROFILE`. Switch on this, not the text. */\n code: string;\n message: string;\n}\n\n/** Outcome of the endpoint check. `clear` means clear against what was checked, nothing more. */\nexport type TruckValidationStatus = \"clear\" | \"conflict\" | \"unknown\";\n\nexport interface TruckValidationFinding {\n /**\n * Where the finding applies. The two endpoints are the segments the routing engine does not\n * evaluate; `route` is anywhere along the path, which is where a time-limited restriction lands.\n */\n where: \"origin\" | \"destination\" | \"route\";\n code: string;\n message: string;\n /** The OSM way the finding concerns, when the engine named one. */\n wayId?: number;\n /** For a time-limited finding: in force now, later, or unreadable from the published schedule. */\n when?: \"now\" | \"later\" | \"unknown\";\n /** Epoch seconds, as the upstream authority published them. */\n effectiveFrom?: number;\n effectiveTo?: number;\n}\n\n/**\n * What was independently checked after the route was produced.\n *\n * The routing engine does not apply restrictions to a route's first or last segment, because a\n * vehicle has to be able to leave where it is parked. This is the check that looks at those two\n * segments anyway.\n *\n * `conflicts` carries findings that are decided: a posted limit your declared vehicle exceeds, and\n * a full closure in force -- the road is shut whatever the vehicle. `unknowns` carries what could\n * not be decided, which includes a restriction whose kind cannot be compared against dimensions\n * at all (a time window, destination-only access), one whose parameter you did not declare, and\n * an endpoint the engine moved or one that sits on a road closed to trucks.\n *\n * An earlier release of this type said a dimension restriction could only ever be an unknown\n * because the engine did not expose the limit. That was wrong, and comparison against your\n * declared vehicle now happens; send a complete profile and you get real answers rather than\n * \"something is posted here\".\n *\n * `clear` means clear against `checkedRestrictionClasses`; it never means the route is legally\n * compliant.\n *\n * Codes worth branching on, beyond the endpoint ones:\n *\n * - `TEMPORARY_LIMIT_EXCEEDED` (conflict) a time-limited posted limit, in force now, that your\n * declared vehicle exceeds.\n * - `TEMPORARY_LIMIT_SCHEDULED` (unknown) the same for a window that has not started.\n * - `TEMPORARY_LIMIT_DEPENDS_ON_CONFIGURATION` (unknown) a structure posting different weights by\n * vehicle configuration, where your declared mass is over the strictest and within the most\n * permissive. Alberta posts 25 t for a single truck and 63.5 t for a B-train at one bridge, so a\n * declared 40 t is legal as one and illegal as the other. This service does not collect your\n * configuration, so it reports the ambiguity rather than picking one.\n * - `TEMPORARY_LIMIT_UNDECLARED` (unknown) a limit is posted and the parameter it constrains was\n * not declared.\n * - `ENDPOINT_ON_TRUCK_PROHIBITED_ROAD` (unknown) the road at an endpoint is closed to trucks, so\n * the route begins or ends on a different one.\n *\n * Read the message as well as the code on a temporary limit. Some are published by the authority\n * in a structured field and some are read out of its own written description; the message says\n * which, because they do not deserve equal confidence.\n *\n * Check `checkedRestrictionClasses` before trusting a `clear`. When the time-limited layer is\n * unavailable you get `TEMPORAL_RESTRICTIONS_NOT_CHECKED` in `unknowns`, which is a different\n * answer from \"no closures found\".\n */\nexport interface TruckValidation {\n status: TruckValidationStatus;\n /** Exactly what was examined, so you can tell what \"clear\" covered. */\n checkedRestrictionClasses: string[];\n conflicts: TruckValidationFinding[];\n unknowns: TruckValidationFinding[];\n}\n\n/**\n * What a truck route considered, and what it did not. Present on `mode: \"truck\"` results only.\n *\n * Read `warnings` before relying on a route. In particular `ENDPOINT_RESTRICTIONS_UNCHECKED`\n * appears whenever the endpoint check did not clear the route's first and last road segment,\n * which includes every route where the check could not run: restrictions are not applied to\n * those two segments, so clearance and load at the origin and destination must be verified\n * separately. Nothing here ever reports a road as unrestricted; the absence of a restriction\n * means none is known.\n */\nexport interface TruckMeta {\n /** The profile as sent, in the units sent. Never back-filled with the engine's defaults. */\n profile: TruckProfile;\n /** True only when height, width, length, weight and axle load were all supplied. */\n profileComplete: boolean;\n /**\n * Every comparable restriction posted on a road this route actually traversed, whether or not\n * your declared vehicle exceeds it.\n *\n * A statement about the route rather than a list of problems: a 20 t bridge on the path is\n * listed even for a 10 t vehicle, because knowing the route crosses it is worth having. The\n * ones your vehicle does exceed also appear in `validation.conflicts`.\n *\n * `value` is in the engine's unit for that type: metres for `max_height`, `max_width` and\n * `max_length`, tonnes for `max_weight` and `max_axle_load`.\n *\n * Absent rather than `[]` whenever the along-route check did not complete — a route with more\n * distinct roads than one pass can sample, a multi-leg trip, or an engine call that failed.\n * An empty array would say \"nothing is posted on this route\", which is a claim; absent says\n * nothing was established. Check `validation.checkedRestrictionClasses` for\n * `route_static_restrictions` to tell the two apart.\n */\n restrictions_applied?: { type: string; value: number; wayId: number }[];\n coverage: Record<string, TruckCoverage>;\n verification: TruckVerification;\n warnings: TruckWarning[];\n /** The endpoint check. Absent when it did not run. */\n validation?: TruckValidation;\n /**\n * Subdivision codes whose authoritative restrictions are in the deployed graph. A statement\n * about the graph, not about this route: no jurisdiction lookup is performed.\n */\n enrichedJurisdictions?: string[];\n}\n\n/**\n * Something about the route itself that you should know before using it. Every mode.\n *\n * Two things today, both worth reading.\n *\n * The engine moves a requested point to the nearest road it can use, and if there is no road near\n * your point it moves it a long way and still reports success. Asked for a route between two\n * places with no road between them, it answered `Ok` with a confident 10.1 km route having\n * relocated the destination 292.3 km. A route is still returned, because it is usually a good\n * route that merely ends short, but `ENDPOINT_RELOCATED` tells you it does not reach the place you\n * named. This matters most where it is least visible: a well pad, a remote lease, a mine, a new\n * subdivision.\n *\n * And a provincial authority may have a road on your path shut. The graph is rebuilt weekly and\n * cannot carry a closure, so `CLOSURE_IN_FORCE_NEAR_ROUTE` is how you learn that the route may go\n * through one; `CLOSURE_SCHEDULED_NEAR_ROUTE` is one that starts later, with `effectiveFrom`\n * saying when. Reported on every mode, because a shut road is shut to a car too.\n *\n * Branch on `code`, which is stable; the text may be reworded.\n */\nexport interface RouteWarning {\n /** Stable machine-readable code, e.g. `ENDPOINT_RELOCATED`. Switch on this, not the text. */\n code: string;\n message: string;\n /**\n * What it concerns: one of the two endpoints, or `\"route\"` for anything along the path, which\n * is where a closure lands.\n */\n where?: \"origin\" | \"destination\" | \"route\";\n /** For a time-limited warning: in force now, later, or unreadable from the published schedule. */\n when?: \"now\" | \"later\" | \"unknown\";\n /** Epoch seconds, as the authority published them. Either end may be absent. */\n effectiveFrom?: number;\n effectiveTo?: number;\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\". `graph.osmTimestamp` is absent until a\n * graph built by the current pipeline is deployed; treat that the same way.\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 * When the source OSM extract was current, ISO 8601. Taken from the PBF header\n * (`osmosis_replication_timestamp`) at graph build time. Absent means unknown.\n */\n osmTimestamp?: string;\n };\n /**\n * How fresh each live restriction feed is. The other half of \"is routing current\".\n *\n * The graph is rebuilt weekly and cannot carry a closure, so closures and temporary limits come\n * from feeds refreshed every few minutes instead. A stale feed is why a truck route reports the\n * temporal layer unchecked, and this is where you can see it without inspecting a route.\n *\n * `fresh` means a route would trust it right now. `stale` means it exists but is too old to be\n * trusted, so routes report the layer unchecked. `absent` means no snapshot has ever been\n * written — for a keyed feed that usually means the key is not configured, which is a different\n * thing from decay. `ageSeconds` is omitted for `absent`, because there is nothing to age.\n *\n * Absent from the response entirely when the deployment has no feed store bound.\n */\n restrictionFeeds?: {\n /** Subdivision code, e.g. `CA-ON`. */\n jurisdiction: string;\n state: \"fresh\" | \"stale\" | \"absent\";\n ageSeconds?: number;\n }[];\n}\n\nexport interface RouteResult {\n distanceMeters: number;\n durationSeconds: number;\n geometry: { type: \"LineString\"; coordinates: LngLat[] };\n /**\n * Route-level warnings, on every mode. Absent when there are none, so a plain route body is\n * unchanged. See `RouteWarning`. Two kinds exist today:\n *\n * - the route does not actually begin or end where you asked (`ENDPOINT_RELOCATED`);\n * - a road closure published by a provincial authority lies within 250 m of the path\n * (`CLOSURE_IN_FORCE_NEAR_ROUTE`, `CLOSURE_SCHEDULED_NEAR_ROUTE`).\n *\n * Closures are reported on every mode, not only on trucks: a washed-out highway is shut to a\n * car as well, and the graph is rebuilt weekly so it cannot carry one. Coverage is Ontario,\n * Alberta and British Columbia, which is where authoritative feeds exist; elsewhere no closure\n * warning means nothing was checked rather than nothing is closed.\n *\n * On `mode: \"truck\"` the same findings also appear inside `truck.validation`, which is a\n * self-contained compliance report and is meant to be readable on its own. The duplication is\n * deliberate: `warnings` is the answer to \"is anything wrong with this route\" for every caller\n * regardless of mode, and putting it somewhere different for trucks would be worse.\n */\n warnings?: RouteWarning[];\n /**\n * Truck restriction coverage and warnings. Present on `mode: \"truck\"` only, and a sibling of\n * the fields above rather than a wrapper around them, so nothing that reads a route today\n * changes. The gateway sends it already; this type ships with the next release.\n *\n * `mode: \"truck\"` itself is included with normal routing on every plan, and applies Valhalla\n * truck costing, the truck restrictions carried in the road network, and the vehicle profile\n * you send. The **Truck Intelligence** add-on gates the layer on top: authoritative Canadian\n * government restriction data, validation against your declared vehicle, and the coverage and\n * provenance reporting.\n *\n * Without the add-on this object is still here and says so. Every `coverage` class reads\n * `not_checked`, `verification` is `unknown`, `validation` is absent rather than an empty\n * report, and `warnings` carries `TRUCK_INTELLIGENCE_NOT_ENABLED`. `not_checked` rather than\n * `osm_only` is deliberate: `osm_only` is a claim about provenance, and without the add-on\n * nothing looked at all.\n *\n * `/route` never refuses over this. Closure warnings in `warnings` above are not gated either,\n * on any mode.\n */\n truck?: TruckMeta;\n /** Present on a multimodal result. Static GTFS is `scheduled`; realtime is a later phase. */\n scheduleType?: \"scheduled\" | \"realtime\";\n /** Walking access/egress and transit rides, in travel order. Present only for transit routes. */\n legs?: RouteLeg[];\n}\n\nexport interface TransitAgencySummary {\n id: string;\n name: string;\n url?: string;\n}\n\nexport interface TransitRouteSummary {\n id: string;\n shortName?: string;\n longName?: string;\n /** CSS hex colour, including `#`. */\n color?: string;\n /** CSS hex text colour, including `#`. */\n textColor?: string;\n}\n\nexport interface TransitStopTime {\n id: string;\n name: string;\n coordinates?: LngLat;\n arrival?: string;\n departure?: string;\n}\n\nexport interface PedestrianRouteLeg {\n mode: \"pedestrian\";\n durationSeconds: number;\n distanceMeters: number;\n}\n\nexport interface TransitRouteLeg {\n mode: \"transit\";\n durationSeconds: number;\n distanceMeters: number;\n agency?: TransitAgencySummary;\n route?: TransitRouteSummary;\n headsign?: string;\n departure?: string;\n arrival?: string;\n stops: TransitStopTime[];\n}\n\nexport type RouteLeg = PedestrianRouteLeg | TransitRouteLeg;\n\n/**\n * Travel times and distances between every origin and every destination.\n *\n * **`null` is a real value here, so handle it on every cell.** A pair the engine could not\n * connect is nulled in place and the rest of the grid still answers; the null is never dropped\n * and never shortens a row, so `durations[i][j]` is always `origins[i]` to `destinations[j]`.\n * A `0` is a real answer, which is what the diagonal returns.\n *\n * Treat a `null` as \"no answer for this pair\", never as proof that no road connects the two\n * points. Across 40 pairs probed on the deployed graph, every null was a pair `/route` answers.\n * An earlier version of this comment claimed `null` never appears; that was retracted\n * on 2026-09-20.\n *\n * `matrix()` rejects with a `RouterError` (status 400, code `invalid_location`) when a coordinate\n * is outside WGS84, and with status 422, code `outside_coverage`, when a location is outside\n * Canada, before the engine runs. It rejects with code `no_route` only for failures the\n * engine raises before computing anything: a location with no road near it, a set of locations\n * the road network does not connect (a property of the set, so no one coordinate is at fault),\n * or two locations further apart in a straight line than its matrix limit.\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\n/**\n * Travel-time bands around a point. Transit bands ride scheduled transit from `departAt`, and a\n * band can be a `MultiPolygon`: the area around the stop a ride drops you at is often its own\n * island. A centre with no scheduled transit nearby is a `RouterError` with code\n * `outside_coverage`, not a walking-only polygon.\n */\nexport type IsochroneOptions = (RoadRouteOptions | TransitRouteOptions) & {\n /** Travel-time bands in minutes. At most four, each 120 or less. */\n minutes: number[];\n};\n\n/**\n * A routing failure.\n *\n * Extends `UnmapError`, so `catch (e) { if (e instanceof UnmapError) }` catches this alongside\n * every other unmap client's errors. `status` and `code` carry the HTTP status and the\n * gateway's error code, and `retryAfterSeconds` comes from the `Retry-After` header.\n */\nexport class RouterError extends UnmapError {\n constructor(status: number, message: string, code?: string, retryAfterSeconds?: number) {\n super(status, message, code, retryAfterSeconds);\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, transit\", \"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; headers?: Headers; 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 const retryAfter = parseRetryAfter(res.headers?.get(\"retry-after\") ?? null) ?? undefined;\n return new RouterError(res.status, reason ? `${reason} (${fallback})` : fallback, code, retryAfter);\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 private readonly retry: RetryPolicy | false | undefined;\n\n constructor(opts: RouterOptions) {\n this.retry = opts.retry;\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 // A polygon cannot ride a query string, so an `avoidAreas` call switches verb. Not a\n // behaviour change the caller has to think about: same parameters, same response, and the\n // GET stays the default so a plain route keeps its cacheable URL.\n if (opts.avoidAreas != null) {\n return this.post<RouteResult>(\"/route\", { from, to, ...bodyOptions(opts) }, opts.signal);\n }\n return this.get<RouteResult>(\n \"/route\",\n {\n from: from.join(\",\"),\n to: to.join(\",\"),\n mode: opts.mode,\n depart_at: opts.departAt != null ? departureValue(opts.departAt) : undefined,\n avoid: opts.avoid?.length ? opts.avoid.join(\",\") : undefined,\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(near: LngLat, opts: IsochroneOptions): Promise<FeatureCollection> {\n if (opts.avoidAreas != null) {\n return this.post<FeatureCollection>(\n \"/isochrone\",\n { near, minutes: opts.minutes, ...bodyOptions(opts) },\n opts.signal,\n );\n }\n return this.get<FeatureCollection>(\n \"/isochrone\",\n {\n near: near.join(\",\"),\n minutes: opts.minutes.join(\",\"),\n mode: opts.mode,\n depart_at: opts.departAt != null ? departureValue(opts.departAt) : undefined,\n avoid: opts.avoid?.length ? opts.avoid.join(\",\") : undefined,\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, and a `null` is a real answer too: a pair the engine\n * could not connect is nulled in place while the rest of the grid answers. A coordinate outside\n * WGS84 rejects first with a `RouterError` (400, `invalid_location`). A location outside\n * Canada rejects first with a `RouterError` (422, `outside_coverage`). The whole call otherwise\n * rejects with a `RouterError` (422, `no_route`) only for a location with no road near it, a\n * set of locations the road network does not connect, or two locations past its straight-line\n * matrix limit.\n */\n matrix(origins: LngLat[], destinations: LngLat[], opts: RoadRouteOptions = {}): 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. A coordinate\n * outside WGS84 rejects with a `RouterError` (400, `invalid_location`). A point outside Canada\n * rejects with a `RouterError` (422, `outside_coverage`) before the graph is searched.\n */\n match(coordinates: LngLat[], opts: RoadRouteOptions = {}): 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, but it does not inherit `matrix()`'s per-cell\n * nulls: a stop set whose own matrix returns nulls still optimizes. A coordinate outside WGS84\n * rejects first with a `RouterError` (400, `invalid_location`). A stop outside Canada rejects\n * first with a `RouterError` (422, `outside_coverage`). The whole call otherwise rejects with\n * a `RouterError` (422, `no_route`) when the stops are not all in one region the road network\n * connects, or two are further apart than the engine's straight-line matrix limit. There is no\n * partial tour either way, because a tour needs every leg.\n */\n optimize(stops: LngLat[], opts: RoadRouteOptions = {}): 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 fetchWithRetry(\n this.fetchImpl,\n `${this.base}${path}?${q}`,\n {\n method: \"POST\",\n signal,\n headers: { accept: \"application/json\", \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n },\n this.retry,\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 fetchWithRetry(\n this.fetchImpl,\n `${this.base}${path}?${q}`,\n { signal, headers: { accept: \"application/json\" } },\n this.retry,\n );\n if (!res.ok) throw await errorFrom(res, path);\n return res.json() as Promise<T>;\n }\n}\n\n// The shared error type and retry policy, re-exported so a caller who installed only this\n// package can `catch (e) { if (e instanceof UnmapError) }` without adding @unmap/core.\nexport { UnmapError, parseRetryAfter, type RetryPolicy } from \"@unmap/core\";\n"],"mappings":"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","names":["UnmapError","parseRetryAfter"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/optimize-tours.ts"],"sourcesContent":["import { DEFAULT_GATEWAY, UnmapError, fetchWithRetry, parseRetryAfter, type RetryPolicy } from \"@unmap/core\";\nimport { optimizeTours, type OptimizeToursRequest, type OptimizeToursResult } from \"./optimize-tours\";\n\nexport * from \"./optimize-tours\";\n\nexport interface RouterOptions {\n key: string;\n gateway?: string;\n fetch?: typeof fetch;\n /**\n * Retry policy for `429`, `502`, `503` and transport failures. `false` disables it.\n *\n * On by default. `Retry-After` is on a `429` other than `billing_required`, and on a container\n * `503`. A `502` has none: routing and `/elevation` both use\n * `upstream_error`. With no header the client backs off itself. A wait longer than\n * `maxDelayMs` (10 s) is not slept through; the error carries `retryAfterSeconds` instead.\n * See `RetryPolicy` in `@unmap/core`.\n */\n retry?: RetryPolicy | false;\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\" | \"transit\";\nexport type RoadTravelMode = Exclude<TravelMode, \"transit\">;\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\n/**\n * A road feature to weight against. `tolls` and `highways` are driving modes only; `ferries`\n * applies to every road mode. `unpaved` is deliberately absent: Valhalla has no surface term in its\n * driving costing, so it could only ever have meant two different things by mode.\n */\nexport type AvoidToken = \"tolls\" | \"highways\" | \"ferries\";\n\n/** A GeoJSON position. The gateway requires exactly `[longitude, latitude]` at runtime. */\nexport type AvoidAreaPosition = number[];\n/** A closed GeoJSON linear ring. */\nexport type AvoidAreaRing = AvoidAreaPosition[];\n\n/** Polygon with one exterior ring. Interior rings are rejected by the gateway. */\nexport interface AvoidAreaPolygon {\n type: \"Polygon\";\n coordinates: AvoidAreaRing[];\n}\n\n/** Multiple polygons, each with one exterior ring. */\nexport interface AvoidAreaMultiPolygon {\n type: \"MultiPolygon\";\n coordinates: AvoidAreaRing[][];\n}\n\nexport interface AvoidAreaFeature {\n type: \"Feature\";\n geometry: AvoidAreaPolygon | AvoidAreaMultiPolygon;\n properties?: Record<string, unknown> | null;\n}\n\nexport interface AvoidAreaFeatureCollection {\n type: \"FeatureCollection\";\n features: AvoidAreaFeature[];\n}\n\nexport type AvoidAreas =\n | AvoidAreaPolygon\n | AvoidAreaMultiPolygon\n | AvoidAreaFeature\n | AvoidAreaFeatureCollection;\n\ninterface SharedRouteOptions {\n signal?: AbortSignal;\n}\n\nexport interface TransitRouteOptions extends SharedRouteOptions {\n mode: \"transit\";\n /**\n * Scheduled departure. A Date is serialized as an ISO instant; a\n * string is sent unchanged so callers can provide an ISO value with an explicit local offset.\n * Transit supports departure time only. There is no `arriveBy` option.\n */\n departAt: Date | string;\n avoid?: never;\n avoidAreas?: never;\n truck?: never;\n useHighways?: never;\n useHills?: never;\n}\n\nexport interface RoadRouteOptions extends SharedRouteOptions {\n mode?: RoadTravelMode;\n /** A scheduled departure applies only to `mode: \"transit\"`. */\n departAt?: never;\n /**\n * Road features to weight against.\n *\n * **A preference, not an exclusion.** These set Valhalla costing weights to 0, which\n * penalises the feature heavily but does not forbid it: a route whose only path crosses a\n * toll bridge still crosses it, and nothing in the result says that it did. If the\n * distinction matters to you, check the returned geometry. Avoid areas have a separate,\n * feature-gated API because they use Valhalla's exclusion mechanism rather than a weight.\n *\n * `tolls` and `highways` are driving modes only (`auto`, `car`, `truck`); `ferries` applies\n * to every road mode. `avoid: [\"highways\"]` and `useHighways` are the same knob at different\n * resolutions and the gateway rejects both together.\n */\n avoid?: AvoidToken[];\n /**\n * Areas to keep the route out of, as a GeoJSON `Polygon`, `MultiPolygon`, or\n * `FeatureCollection` of either.\n *\n * Valhalla defines these as exclusions rather than preferences. Server support remains behind\n * a feature flag while a defect in the pinned engine can snap a location across the polygon;\n * do not rely on this option until the service documentation says the flag is enabled.\n * Interior rings are rejected because the engine cannot preserve holes.\n *\n * At most 8 areas, 100 vertices and 10 km of perimeter in total.\n *\n * Sending this makes `route()` and `isochrone()` use POST instead of GET, because a polygon\n * cannot ride a query string. The response is identical either way.\n */\n avoidAreas?: AvoidAreas;\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}\n\n/** Transit options are a separate branch so `departAt` cannot be omitted by accident. */\nexport type RouteOptions = RoadRouteOptions | TransitRouteOptions;\n\n/** Turn-by-turn options, taken by `route()`, `match()` and `optimize()`. */\nexport interface StepOptions {\n /**\n * Return `steps`: written turn-by-turn directions for display. Off by default, so a route you\n * only draw stays small.\n */\n steps?: boolean;\n /**\n * Language of each step's `instruction`: `en` or `fr`. A regional tag resolves by its language\n * (`fr-CA` is French) and any other language is English. Only with `steps: true`; the gateway\n * rejects it otherwise. Without it the gateway reads the request's `Accept-Language` header,\n * which a browser sends for you.\n */\n lang?: string;\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 & StepOptions): Record<string, unknown> {\n return {\n ...(opts.steps != null ? { steps: opts.steps } : {}),\n ...(opts.lang != null ? { lang: opts.lang } : {}),\n ...(opts.mode ? { mode: opts.mode } : {}),\n ...(opts.departAt != null ? { depart_at: departureValue(opts.departAt) } : {}),\n ...(opts.avoid?.length ? { avoid: opts.avoid } : {}),\n ...(opts.avoidAreas != null ? { avoid_areas: opts.avoidAreas } : {}),\n ...(opts.useHighways != null ? { use_highways: opts.useHighways } : {}),\n ...(opts.useHills != null ? { use_hills: opts.useHills } : {}),\n ...truckBody(opts.truck),\n };\n}\n\nfunction departureValue(value: Date | string): string {\n return value instanceof Date ? value.toISOString() : value;\n}\n\n/**\n * How well a class of truck restriction is covered for a route. `osm_only` is provenance, not a\n * grade: Canada publishes no national bridge inventory, so OSM-derived is the honest description\n * of most of it. `not_modelled` means the routing engine cannot express the rule at all (a\n * conditional restriction); `not_checked` means nothing looked.\n */\nexport type TruckCoverage =\n | \"authoritative\"\n | \"osm_plus_authoritative\"\n | \"osm_only\"\n | \"not_modelled\"\n | \"not_checked\";\n\n/** How much of a truck route's restriction picture is trustworthy, overall. */\nexport type TruckVerification = \"authoritative\" | \"partial\" | \"osm_only\" | \"unknown\" | \"candidate_permit_route\";\n\nexport interface TruckWarning {\n /** Stable machine-readable code, e.g. `INCOMPLETE_VEHICLE_PROFILE`. Switch on this, not the text. */\n code: string;\n message: string;\n}\n\n/** Outcome of the endpoint check. `clear` means clear against what was checked, nothing more. */\nexport type TruckValidationStatus = \"clear\" | \"conflict\" | \"unknown\";\n\nexport interface TruckValidationFinding {\n /**\n * Where the finding applies. The two endpoints are the segments the routing engine does not\n * evaluate; `route` is anywhere along the path, which is where a time-limited restriction lands.\n */\n where: \"origin\" | \"destination\" | \"route\";\n code: string;\n message: string;\n /** The OSM way the finding concerns, when the engine named one. */\n wayId?: number;\n /** For a time-limited finding: in force now, later, or unreadable from the published schedule. */\n when?: \"now\" | \"later\" | \"unknown\";\n /** Epoch seconds, as the upstream authority published them. */\n effectiveFrom?: number;\n effectiveTo?: number;\n}\n\n/**\n * What was independently checked after the route was produced.\n *\n * The routing engine does not apply restrictions to a route's first or last segment, because a\n * vehicle has to be able to leave where it is parked. This is the check that looks at those two\n * segments anyway.\n *\n * `conflicts` carries findings that are decided: a posted limit your declared vehicle exceeds, and\n * a full closure in force -- the road is shut whatever the vehicle. `unknowns` carries what could\n * not be decided, which includes a restriction whose kind cannot be compared against dimensions\n * at all (a time window, destination-only access), one whose parameter you did not declare, and\n * an endpoint the engine moved or one that sits on a road closed to trucks.\n *\n * An earlier release of this type said a dimension restriction could only ever be an unknown\n * because the engine did not expose the limit. That was wrong, and comparison against your\n * declared vehicle now happens; send a complete profile and you get real answers rather than\n * \"something is posted here\".\n *\n * `clear` means clear against `checkedRestrictionClasses`; it never means the route is legally\n * compliant.\n *\n * Codes worth branching on, beyond the endpoint ones:\n *\n * - `TEMPORARY_LIMIT_EXCEEDED` (conflict) a time-limited posted limit, in force now, that your\n * declared vehicle exceeds.\n * - `TEMPORARY_LIMIT_SCHEDULED` (unknown) the same for a window that has not started.\n * - `TEMPORARY_LIMIT_DEPENDS_ON_CONFIGURATION` (unknown) a structure posting different weights by\n * vehicle configuration, where your declared mass is over the strictest and within the most\n * permissive. Alberta posts 25 t for a single truck and 63.5 t for a B-train at one bridge, so a\n * declared 40 t is legal as one and illegal as the other. This service does not collect your\n * configuration, so it reports the ambiguity rather than picking one.\n * - `TEMPORARY_LIMIT_UNDECLARED` (unknown) a limit is posted and the parameter it constrains was\n * not declared.\n * - `ENDPOINT_ON_TRUCK_PROHIBITED_ROAD` (unknown) the road at an endpoint is closed to trucks, so\n * the route begins or ends on a different one.\n *\n * Read the message as well as the code on a temporary limit. Some are published by the authority\n * in a structured field and some are read out of its own written description; the message says\n * which, because they do not deserve equal confidence.\n *\n * Check `checkedRestrictionClasses` before trusting a `clear`. When the time-limited layer is\n * unavailable you get `TEMPORAL_RESTRICTIONS_NOT_CHECKED` in `unknowns`, which is a different\n * answer from \"no closures found\".\n */\nexport interface TruckValidation {\n status: TruckValidationStatus;\n /** Exactly what was examined, so you can tell what \"clear\" covered. */\n checkedRestrictionClasses: string[];\n conflicts: TruckValidationFinding[];\n unknowns: TruckValidationFinding[];\n}\n\n/**\n * What a truck route considered, and what it did not. Present on `mode: \"truck\"` results only.\n *\n * Read `warnings` before relying on a route. In particular `ENDPOINT_RESTRICTIONS_UNCHECKED`\n * appears whenever the endpoint check did not clear the route's first and last road segment,\n * which includes every route where the check could not run: restrictions are not applied to\n * those two segments, so clearance and load at the origin and destination must be verified\n * separately. Nothing here ever reports a road as unrestricted; the absence of a restriction\n * means none is known.\n */\nexport interface TruckMeta {\n /** The profile as sent, in the units sent. Never back-filled with the engine's defaults. */\n profile: TruckProfile;\n /** True only when height, width, length, weight and axle load were all supplied. */\n profileComplete: boolean;\n /**\n * Every comparable restriction posted on a road this route actually traversed, whether or not\n * your declared vehicle exceeds it.\n *\n * A statement about the route rather than a list of problems: a 20 t bridge on the path is\n * listed even for a 10 t vehicle, because knowing the route crosses it is worth having. The\n * ones your vehicle does exceed also appear in `validation.conflicts`.\n *\n * `value` is in the engine's unit for that type: metres for `max_height`, `max_width` and\n * `max_length`, tonnes for `max_weight` and `max_axle_load`.\n *\n * Absent rather than `[]` whenever the along-route check did not complete — a route with more\n * distinct roads than one pass can sample, a multi-leg trip, or an engine call that failed.\n * An empty array would say \"nothing is posted on this route\", which is a claim; absent says\n * nothing was established. Check `validation.checkedRestrictionClasses` for\n * `route_static_restrictions` to tell the two apart.\n */\n restrictions_applied?: { type: string; value: number; wayId: number }[];\n coverage: Record<string, TruckCoverage>;\n verification: TruckVerification;\n warnings: TruckWarning[];\n /** The endpoint check. Absent when it did not run. */\n validation?: TruckValidation;\n /**\n * Subdivision codes whose authoritative restrictions are in the deployed graph. A statement\n * about the graph, not about this route: no jurisdiction lookup is performed.\n */\n enrichedJurisdictions?: string[];\n}\n\n/**\n * Something about the route itself that you should know before using it. Every mode.\n *\n * Two things today, both worth reading.\n *\n * The engine moves a requested point to the nearest road it can use, and if there is no road near\n * your point it moves it a long way and still reports success. Asked for a route between two\n * places with no road between them, it answered `Ok` with a confident 10.1 km route having\n * relocated the destination 292.3 km. A route is still returned, because it is usually a good\n * route that merely ends short, but `ENDPOINT_RELOCATED` tells you it does not reach the place you\n * named. This matters most where it is least visible: a well pad, a remote lease, a mine, a new\n * subdivision.\n *\n * And a provincial authority may have a road on your path shut. The graph is rebuilt weekly and\n * cannot carry a closure, so `CLOSURE_IN_FORCE_NEAR_ROUTE` is how you learn that the route may go\n * through one; `CLOSURE_SCHEDULED_NEAR_ROUTE` is one that starts later, with `effectiveFrom`\n * saying when. Reported on every mode, because a shut road is shut to a car too.\n *\n * Branch on `code`, which is stable; the text may be reworded.\n */\nexport interface RouteWarning {\n /** Stable machine-readable code, e.g. `ENDPOINT_RELOCATED`. Switch on this, not the text. */\n code: string;\n message: string;\n /**\n * What it concerns: one of the two endpoints, or `\"route\"` for anything along the path, which\n * is where a closure lands.\n */\n where?: \"origin\" | \"destination\" | \"route\";\n /** For a time-limited warning: in force now, later, or unreadable from the published schedule. */\n when?: \"now\" | \"later\" | \"unknown\";\n /** Epoch seconds, as the authority published them. Either end may be absent. */\n effectiveFrom?: number;\n effectiveTo?: number;\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\". `graph.osmTimestamp` is absent until a\n * graph built by the current pipeline is deployed; treat that the same way.\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 * When the source OSM extract was current, ISO 8601. Taken from the PBF header\n * (`osmosis_replication_timestamp`) at graph build time. Absent means unknown.\n */\n osmTimestamp?: string;\n };\n /**\n * How fresh each live restriction feed is. The other half of \"is routing current\".\n *\n * The graph is rebuilt weekly and cannot carry a closure, so closures and temporary limits come\n * from feeds refreshed every few minutes instead. A stale feed is why a truck route reports the\n * temporal layer unchecked, and this is where you can see it without inspecting a route.\n *\n * `fresh` means a route would trust it right now. `stale` means it exists but is too old to be\n * trusted, so routes report the layer unchecked. `absent` means no snapshot has ever been\n * written — for a keyed feed that usually means the key is not configured, which is a different\n * thing from decay. `ageSeconds` is omitted for `absent`, because there is nothing to age.\n *\n * Absent from the response entirely when the deployment has no feed store bound.\n */\n restrictionFeeds?: {\n /** Subdivision code, e.g. `CA-ON`. */\n jurisdiction: string;\n state: \"fresh\" | \"stale\" | \"absent\";\n ageSeconds?: number;\n }[];\n}\n\nexport interface RouteResult {\n distanceMeters: number;\n durationSeconds: number;\n geometry: { type: \"LineString\"; coordinates: LngLat[] };\n /**\n * Route-level warnings, on every mode. Absent when there are none, so a plain route body is\n * unchanged. See `RouteWarning`. Two kinds exist today:\n *\n * - the route does not actually begin or end where you asked (`ENDPOINT_RELOCATED`);\n * - a road closure published by a provincial authority lies within 250 m of the path\n * (`CLOSURE_IN_FORCE_NEAR_ROUTE`, `CLOSURE_SCHEDULED_NEAR_ROUTE`).\n *\n * Closures are reported on every mode, not only on trucks: a washed-out highway is shut to a\n * car as well, and the graph is rebuilt weekly so it cannot carry one. Coverage is Ontario,\n * Alberta and British Columbia, which is where authoritative feeds exist; elsewhere no closure\n * warning means nothing was checked rather than nothing is closed.\n *\n * On `mode: \"truck\"` the same findings also appear inside `truck.validation`, which is a\n * self-contained compliance report and is meant to be readable on its own. The duplication is\n * deliberate: `warnings` is the answer to \"is anything wrong with this route\" for every caller\n * regardless of mode, and putting it somewhere different for trucks would be worse.\n */\n warnings?: RouteWarning[];\n /**\n * Truck restriction coverage and warnings. Present on `mode: \"truck\"` only, and a sibling of\n * the fields above rather than a wrapper around them, so nothing that reads a route today\n * changes. The gateway sends it already; this type ships with the next release.\n *\n * `mode: \"truck\"` itself is included with normal routing on every plan, and applies Valhalla\n * truck costing, the truck restrictions carried in the road network, and the vehicle profile\n * you send. The **Truck Intelligence** add-on gates the layer on top: authoritative Canadian\n * government restriction data, validation against your declared vehicle, and the coverage and\n * provenance reporting.\n *\n * Without the add-on this object is still here and says so. Every `coverage` class reads\n * `not_checked`, `verification` is `unknown`, `validation` is absent rather than an empty\n * report, and `warnings` carries `TRUCK_INTELLIGENCE_NOT_ENABLED`. `not_checked` rather than\n * `osm_only` is deliberate: `osm_only` is a claim about provenance, and without the add-on\n * nothing looked at all.\n *\n * `/route` never refuses over this. Closure warnings in `warnings` above are not gated either,\n * on any mode.\n */\n truck?: TruckMeta;\n /** Present on a multimodal result. Static GTFS is `scheduled`; realtime is a later phase. */\n scheduleType?: \"scheduled\" | \"realtime\";\n /** Walking access/egress and transit rides, in travel order. Present only for transit routes. */\n legs?: RouteLeg[];\n /**\n * Turn-by-turn directions, in travel order. Present only when you pass `steps: true`. On a\n * transit route they include the walk to and from each stop as well as the rides.\n */\n steps?: RouteStep[];\n}\n\nexport interface TransitAgencySummary {\n id: string;\n name: string;\n url?: string;\n}\n\nexport interface TransitRouteSummary {\n id: string;\n shortName?: string;\n longName?: string;\n /** CSS hex colour, including `#`. */\n color?: string;\n /** CSS hex text colour, including `#`. */\n textColor?: string;\n}\n\nexport interface TransitStopTime {\n id: string;\n name: string;\n coordinates?: LngLat;\n arrival?: string;\n departure?: string;\n}\n\nexport interface PedestrianRouteLeg {\n mode: \"pedestrian\";\n durationSeconds: number;\n distanceMeters: number;\n}\n\nexport interface TransitRouteLeg {\n mode: \"transit\";\n durationSeconds: number;\n distanceMeters: number;\n agency?: TransitAgencySummary;\n route?: TransitRouteSummary;\n headsign?: string;\n departure?: string;\n arrival?: string;\n stops: TransitStopTime[];\n}\n\nexport type RouteLeg = PedestrianRouteLeg | TransitRouteLeg;\n\n/**\n * What a step asks the traveller to do. unmap's own names, so your code does not depend on the\n * routing engine's internal numbering. Switch on this to choose an icon; show `instruction` as text.\n *\n * - `depart` / `arrive`: the first step of a route, and every stop it reaches (`/optimized-route`\n * arrives at each stop and departs again).\n * - `turn`, `uturn`, `continue`, `keep` (stay on a side at a split), `merge`, `name_change` (the\n * road you are on changes name).\n * - `ramp` (take a ramp) and `exit` (leave by an exit), both on limited-access roads.\n * - `roundabout` (enter; `roundaboutExit` says which exit) and `roundabout_exit`.\n * - `ferry` (board) and `ferry_exit`.\n * - Transit: `transit` (board), `transit_transfer`, `transit_stay_on`, `station_enter`,\n * `station_transfer`, `station_exit`.\n * - Inside buildings on a walk: `elevator`, `stairs`, `escalator`, `building_enter`, `building_exit`.\n * - `other`: a step the engine produced that unmap does not name yet. Show its instruction.\n */\nexport type RouteStepType =\n | \"depart\"\n | \"arrive\"\n | \"turn\"\n | \"uturn\"\n | \"continue\"\n | \"keep\"\n | \"merge\"\n | \"name_change\"\n | \"ramp\"\n | \"exit\"\n | \"roundabout\"\n | \"roundabout_exit\"\n | \"ferry\"\n | \"ferry_exit\"\n | \"transit\"\n | \"transit_transfer\"\n | \"transit_stay_on\"\n | \"station_enter\"\n | \"station_transfer\"\n | \"station_exit\"\n | \"elevator\"\n | \"stairs\"\n | \"escalator\"\n | \"building_enter\"\n | \"building_exit\"\n | \"other\";\n\n/**\n * Which way a step goes. Absent when the step has no direction (a roundabout, a transfer). On\n * `depart` and `arrive` it is the side of the road (`left`/`right`); on `uturn` the direction of\n * the turn.\n */\nexport type RouteStepModifier =\n | \"sharp_left\"\n | \"left\"\n | \"slight_left\"\n | \"straight\"\n | \"slight_right\"\n | \"right\"\n | \"sharp_right\";\n\n/** The guide sign at a highway junction, as posted. Every list is absent when the sign has none. */\nexport interface RouteStepSign {\n /** Exit numbers, e.g. `[\"91B\"]`. */\n exitNumbers?: string[];\n /** The road or route the exit leads onto, e.g. `[\"Highway 2 North\"]`. */\n branches?: string[];\n /** Where the exit goes, e.g. `[\"Red Deer\"]`. */\n toward?: string[];\n /** The interchange's own name. Rare in Canada. */\n names?: string[];\n}\n\n/**\n * One turn-by-turn step: written directions, for display. There is no voice narration.\n *\n * Present on a route only when you ask for `steps`. Steps cover the whole route in travel order, so\n * their distances and durations add up to the route's, give or take rounding.\n */\nexport interface RouteStep {\n type: RouteStepType;\n modifier?: RouteStepModifier;\n /**\n * The step as a sentence, e.g. \"Turn left onto 17 Avenue Southwest.\", in the language you asked\n * for (`en` or `fr`). French is written by the engine's France French locale, not a Canadian\n * one, so a few phrasings read as European French.\n */\n instruction: string;\n /** Names of the road the step travels along. Absent on an unnamed road. */\n streetNames?: string[];\n distanceMeters: number;\n durationSeconds: number;\n /** Where the step begins, `[lng, lat]`. The same point as `geometry.coordinates[geometryRange[0]]`. */\n location: LngLat;\n /**\n * First and last index of this step's stretch in the route's `geometry.coordinates`, inclusive.\n * `coordinates.slice(start, end + 1)` is the line to highlight for the step.\n */\n geometryRange: [number, number];\n /** On `type: \"roundabout\"`, which exit to take, counting from where you enter. */\n roundaboutExit?: number;\n sign?: RouteStepSign;\n}\n\n/**\n * Travel times and distances between every origin and every destination.\n *\n * **`null` is a real value here, so handle it on every cell.** A pair the engine could not\n * connect is nulled in place and the rest of the grid still answers; the null is never dropped\n * and never shortens a row, so `durations[i][j]` is always `origins[i]` to `destinations[j]`.\n * A `0` is a real answer, which is what the diagonal returns.\n *\n * Treat a `null` as \"no answer for this pair\", never as proof that no road connects the two\n * points. Across 40 pairs probed on the deployed graph, every null was a pair `/route` answers.\n * An earlier version of this comment claimed `null` never appears; that was retracted\n * on 2026-09-20.\n *\n * `matrix()` rejects with a `RouterError` (status 400, code `invalid_location`) when a coordinate\n * is outside WGS84, and with status 422, code `outside_coverage`, when a location is outside\n * Canada, before the engine runs. It rejects with code `no_route` only for failures the\n * engine raises before computing anything: a location with no road near it, a set of locations\n * the road network does not connect (a property of the set, so no one coordinate is at fault),\n * or two locations further apart in a straight line than its matrix limit.\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\n/**\n * Travel-time bands around a point. Transit bands ride scheduled transit from `departAt`, and a\n * band can be a `MultiPolygon`: the area around the stop a ride drops you at is often its own\n * island. A centre with no scheduled transit nearby is a `RouterError` with code\n * `outside_coverage`, not a walking-only polygon.\n */\nexport type IsochroneOptions = (RoadRouteOptions | TransitRouteOptions) & {\n /** Travel-time bands in minutes. At most four, each 120 or less. */\n minutes: number[];\n};\n\n/**\n * A routing failure.\n *\n * Extends `UnmapError`, so `catch (e) { if (e instanceof UnmapError) }` catches this alongside\n * every other unmap client's errors. `status` and `code` carry the HTTP status and the\n * gateway's error code, and `retryAfterSeconds` comes from the `Retry-After` header.\n */\nexport class RouterError extends UnmapError {\n constructor(status: number, message: string, code?: string, retryAfterSeconds?: number) {\n super(status, message, code, retryAfterSeconds);\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, transit\", \"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; headers?: Headers; 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 const retryAfter = parseRetryAfter(res.headers?.get(\"retry-after\") ?? null) ?? undefined;\n return new RouterError(res.status, reason ? `${reason} (${fallback})` : fallback, code, retryAfter);\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 private readonly retry: RetryPolicy | false | undefined;\n\n constructor(opts: RouterOptions) {\n this.retry = opts.retry;\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 & StepOptions = {}): Promise<RouteResult> {\n // A polygon cannot ride a query string, so an `avoidAreas` call switches verb. Not a\n // behaviour change the caller has to think about: same parameters, same response, and the\n // GET stays the default so a plain route keeps its cacheable URL.\n if (opts.avoidAreas != null) {\n return this.post<RouteResult>(\"/route\", { from, to, ...bodyOptions(opts) }, opts.signal);\n }\n return this.get<RouteResult>(\n \"/route\",\n {\n from: from.join(\",\"),\n to: to.join(\",\"),\n mode: opts.mode,\n depart_at: opts.departAt != null ? departureValue(opts.departAt) : undefined,\n avoid: opts.avoid?.length ? opts.avoid.join(\",\") : undefined,\n use_highways: opts.useHighways != null ? String(opts.useHighways) : undefined,\n use_hills: opts.useHills != null ? String(opts.useHills) : undefined,\n steps: opts.steps != null ? String(opts.steps) : undefined,\n lang: opts.lang,\n ...truckParams(opts.truck),\n },\n opts.signal,\n );\n }\n\n isochrone(near: LngLat, opts: IsochroneOptions): Promise<FeatureCollection> {\n if (opts.avoidAreas != null) {\n return this.post<FeatureCollection>(\n \"/isochrone\",\n { near, minutes: opts.minutes, ...bodyOptions(opts) },\n opts.signal,\n );\n }\n return this.get<FeatureCollection>(\n \"/isochrone\",\n {\n near: near.join(\",\"),\n minutes: opts.minutes.join(\",\"),\n mode: opts.mode,\n depart_at: opts.departAt != null ? departureValue(opts.departAt) : undefined,\n avoid: opts.avoid?.length ? opts.avoid.join(\",\") : undefined,\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, and a `null` is a real answer too: a pair the engine\n * could not connect is nulled in place while the rest of the grid answers. A coordinate outside\n * WGS84 rejects first with a `RouterError` (400, `invalid_location`). A location outside\n * Canada rejects first with a `RouterError` (422, `outside_coverage`). The whole call otherwise\n * rejects with a `RouterError` (422, `no_route`) only for a location with no road near it, a\n * set of locations the road network does not connect, or two locations past its straight-line\n * matrix limit.\n */\n matrix(origins: LngLat[], destinations: LngLat[], opts: RoadRouteOptions = {}): 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. A coordinate\n * outside WGS84 rejects with a `RouterError` (400, `invalid_location`). A point outside Canada\n * rejects with a `RouterError` (422, `outside_coverage`) before the graph is searched.\n */\n match(coordinates: LngLat[], opts: RoadRouteOptions & StepOptions = {}): 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, but it does not inherit `matrix()`'s per-cell\n * nulls: a stop set whose own matrix returns nulls still optimizes. A coordinate outside WGS84\n * rejects first with a `RouterError` (400, `invalid_location`). A stop outside Canada rejects\n * first with a `RouterError` (422, `outside_coverage`). The whole call otherwise rejects with\n * a `RouterError` (422, `no_route`) when the stops are not all in one region the road network\n * connects, or two are further apart than the engine's straight-line matrix limit. There is no\n * partial tour either way, because a tour needs every leg.\n */\n optimize(stops: LngLat[], opts: RoadRouteOptions & StepOptions = {}): Promise<OptimizedRouteResult> {\n return this.post<OptimizedRouteResult>(\"/optimized-route\", { stops, ...bodyOptions(opts) }, opts.signal);\n }\n\n /**\n * Assign shipments to vehicles and order their visits: Google Route Optimization's\n * `optimizeTours`, served by Unmap Optimize. See the standalone `optimizeTours()` for what it\n * resolves with and throws. This is `/optimized-route`'s bigger sibling: that one orders stops\n * for a single vehicle, this one plans a fleet.\n */\n optimizeTours(request: OptimizeToursRequest, opts: { signal?: AbortSignal } = {}): Promise<OptimizeToursResult> {\n return optimizeTours(request, { key: this.key, gateway: this.base, fetch: this.fetchImpl, retry: this.retry, signal: 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 fetchWithRetry(\n this.fetchImpl,\n `${this.base}${path}?${q}`,\n {\n method: \"POST\",\n signal,\n headers: { accept: \"application/json\", \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n },\n this.retry,\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 fetchWithRetry(\n this.fetchImpl,\n `${this.base}${path}?${q}`,\n { signal, headers: { accept: \"application/json\" } },\n this.retry,\n );\n if (!res.ok) throw await errorFrom(res, path);\n return res.json() as Promise<T>;\n }\n}\n\n// The shared error type and retry policy, re-exported so a caller who installed only this\n// package can `catch (e) { if (e instanceof UnmapError) }` without adding @unmap/core.\nexport { UnmapError, parseRetryAfter, type RetryPolicy } from \"@unmap/core\";\n","import { DEFAULT_GATEWAY, UnmapError, fetchWithRetry, parseRetryAfter, type RetryPolicy } from \"@unmap/core\";\nimport type { OptimizeToursRequest, OptimizeToursResponse } from \"@unmap/optimize-schema\";\n\nexport type {\n OptimizeToursRequest,\n OptimizeToursResponse,\n ShipmentModel as OptimizeShipmentModel,\n Shipment as OptimizeShipment,\n Vehicle as OptimizeVehicle,\n ShipmentRoute as OptimizeShipmentRoute,\n SkippedShipment as OptimizeSkippedShipment,\n} from \"@unmap/optimize-schema\";\n\nexport interface OptimizeToursOptions {\n key: string;\n gateway?: string;\n fetch?: typeof fetch;\n /**\n * Retry policy for `429`, `502`, `503` and transport failures. `false` disables it. A failed\n * solve is never billed, so a retry costs nothing, but a solve that is still running when the\n * connection drops is not cancelled: it holds one of your plan's concurrent solves until it ends.\n * See `RetryPolicy` in `@unmap/core`.\n */\n retry?: RetryPolicy | false;\n signal?: AbortSignal;\n}\n\n/**\n * One entry of the `X-Unmap-Optimize-Warnings` header: a field Unmap accepted but cannot honour,\n * a clamped timeout, road travel the routing budget decided, or a route polyline that could not be\n * drawn. A final `WARNINGS_TRUNCATED` entry counts any that did not fit the header.\n */\nexport interface OptimizeToursWarning {\n code: string;\n field?: string;\n path?: string;\n count?: number;\n message: string;\n}\n\n/**\n * What `optimizeTours()` resolves with.\n *\n * `response` is Google's `OptimizeToursResponse`, exactly as the gateway sent it: Unmap never adds\n * a field to it, so code written against Google's response shape reads `response` unchanged. What\n * Unmap knows beyond that arrives in the response headers and is parsed here, beside it.\n */\nexport interface OptimizeToursResult {\n response: OptimizeToursResponse;\n /** Parsed `X-Unmap-Optimize-Warnings`. Empty when there were none or the header was unreadable. */\n warnings: OptimizeToursWarning[];\n /**\n * Parsed `X-Unmap-Billable-Shipments`: the shipments this request billed. `0` for\n * `VALIDATE_ONLY`, a free re-solve within 24 hours, and when the header is absent. A dev origin\n * is billed like any other.\n */\n billableShipments: number;\n /** True when your `timeout` was over the plan's cap and the gateway shortened it. */\n timeoutClamped: boolean;\n /** The timeout the solve ran with, in seconds, when `timeoutClamped`; otherwise `null`. */\n clampedTimeoutSeconds: number | null;\n}\n\n/** One Google `Status.details` entry (`google.rpc.BadRequest`, `PreconditionFailure`, `RetryInfo`, ...). */\nexport interface OptimizeToursErrorDetail {\n \"@type\"?: string;\n [key: string]: unknown;\n}\n\n/**\n * An Unmap Optimize failure.\n *\n * Extends `UnmapError`, so `instanceof UnmapError` catches it with every other unmap client's\n * errors. `status` is the HTTP status, `code` the gateway's string code (`bad_request`,\n * `rate_limited`, `quota_exceeded`, ...), and `retryAfterSeconds` comes from `Retry-After`.\n * `googleStatus` is Google's RPC status name (`INVALID_ARGUMENT`, `FAILED_PRECONDITION`, ...) and\n * `details` Google's `error.details`: a `google.rpc.BadRequest` lists each field violation, and a\n * `PreconditionFailure` lists the mandatory shipments no vehicle can serve. A response that is\n * not Google-shaped (the gateway's plain `404` when Optimize is not enabled) has no\n * `googleStatus` and an empty `details`.\n */\nexport class UnmapOptimizeError extends UnmapError {\n constructor(\n status: number,\n message: string,\n code?: string,\n retryAfterSeconds?: number,\n readonly googleStatus?: string,\n readonly details: OptimizeToursErrorDetail[] = [],\n ) {\n super(status, message, code, retryAfterSeconds);\n this.name = \"UnmapOptimizeError\";\n }\n}\n\nasync function optimizeErrorFrom(res: { status: number; headers?: Headers; json(): Promise<unknown> }): Promise<UnmapOptimizeError> {\n const fallback = `optimize ${res.status} on /optimize`;\n let body: unknown;\n try {\n body = await res.json();\n } catch {\n body = undefined;\n }\n const top = (typeof body === \"object\" && body !== null ? body : {}) as { error?: unknown; code?: unknown };\n const code = typeof top.code === \"string\" ? top.code : undefined;\n const retryAfter = parseRetryAfter(res.headers?.get(\"retry-after\") ?? null) ?? undefined;\n\n let reason: string | undefined;\n let googleStatus: string | undefined;\n let details: OptimizeToursErrorDetail[] = [];\n if (typeof top.error === \"string\") {\n reason = top.error;\n } else if (typeof top.error === \"object\" && top.error !== null) {\n const g = top.error as { message?: unknown; status?: unknown; details?: unknown };\n if (typeof g.message === \"string\") reason = g.message;\n if (typeof g.status === \"string\") googleStatus = g.status;\n if (Array.isArray(g.details)) {\n details = g.details.filter((d): d is OptimizeToursErrorDetail => typeof d === \"object\" && d !== null);\n }\n }\n return new UnmapOptimizeError(res.status, reason ? `${reason} (${fallback})` : fallback, code, retryAfter, googleStatus, details);\n}\n\nfunction parseWarnings(header: string | null): OptimizeToursWarning[] {\n if (!header) return [];\n try {\n const parsed: unknown = JSON.parse(header);\n if (!Array.isArray(parsed)) return [];\n return parsed.filter(\n (w): w is OptimizeToursWarning =>\n typeof w === \"object\" && w !== null && typeof (w as { code?: unknown }).code === \"string\" && typeof (w as { message?: unknown }).message === \"string\",\n );\n } catch {\n return [];\n }\n}\n\nfunction parseBillable(header: string | null): number {\n if (!header || !/^[0-9]+$/.test(header.trim())) return 0;\n return Number(header.trim());\n}\n\nfunction parseClamp(header: string | null): number | null {\n const m = header ? /timeout clamped to (\\d+)s/.exec(header) : null;\n return m ? Number(m[1]) : null;\n}\n\n/**\n * Assign shipments to vehicles and order their visits (Unmap Optimize, `POST /optimize`).\n *\n * The request and the `response` are Google Route Optimization's `OptimizeToursRequest` and\n * `OptimizeToursResponse`. Unmap's own findings (warnings, the shipments billed) are returned\n * beside the response, never inside it.\n *\n * const { response } = await optimizeTours(request, { key: \"um_live_...\" });\n *\n * The key is sent in `X-API-Key`, not in the URL. Billed per shipment performed; see\n * `billableShipments`. A failure throws `UnmapOptimizeError`.\n */\nexport async function optimizeTours(request: OptimizeToursRequest, opts: OptimizeToursOptions): Promise<OptimizeToursResult> {\n const base = (opts.gateway ?? DEFAULT_GATEWAY).replace(/\\/+$/, \"\");\n const path = \"/optimize\";\n // Bound to globalThis: a bare `fetch` stored on an object is rejected by browsers with\n // \"Illegal invocation\" when called with that object as the receiver.\n const fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);\n const res = await fetchWithRetry(\n fetchImpl,\n `${base}${path}`,\n {\n method: \"POST\",\n signal: opts.signal,\n headers: { accept: \"application/json\", \"content-type\": \"application/json\", \"x-api-key\": opts.key },\n body: JSON.stringify(request),\n },\n opts.retry,\n );\n if (!res.ok) throw await optimizeErrorFrom(res);\n let response: OptimizeToursResponse;\n try {\n response = (await res.json()) as OptimizeToursResponse;\n } catch {\n // A proxy or a captive portal can answer 200 with HTML; callers catch one error type.\n throw new UnmapOptimizeError(res.status, `optimize ${res.status} on /optimize: the response body is not JSON`);\n }\n const clamped = parseClamp(res.headers?.get(\"warning\") ?? null);\n return {\n response,\n warnings: parseWarnings(res.headers?.get(\"x-unmap-optimize-warnings\") ?? null),\n billableShipments: parseBillable(res.headers?.get(\"x-unmap-billable-shipments\") ?? null),\n timeoutClamped: clamped !== null,\n clampedTimeoutSeconds: clamped,\n };\n}\n"],"mappings":"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","names":["DEFAULT_GATEWAY","UnmapError","fetchWithRetry","parseRetryAfter","UnmapError","parseRetryAfter","DEFAULT_GATEWAY","fetchWithRetry"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unmap/routing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Typed client for the unmap routing API (route + isochrone)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
"homepage": "https://unmap.dev/docs/apis/routing",
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
|
-
"url": "git+https://github.com/unmap-dev/
|
|
17
|
+
"url": "git+https://github.com/unmap-dev/sdk-js.git",
|
|
18
18
|
"directory": "packages/routing"
|
|
19
19
|
},
|
|
20
20
|
"bugs": {
|
|
21
|
-
"url": "https://github.com/unmap-dev/
|
|
21
|
+
"url": "https://github.com/unmap-dev/sdk-js/issues"
|
|
22
22
|
},
|
|
23
23
|
"type": "module",
|
|
24
24
|
"main": "./dist/index.js",
|
|
@@ -38,12 +38,13 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@unmap/core": "0.
|
|
41
|
+
"@unmap/core": "0.21.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"tsup": "^8.3.0",
|
|
45
45
|
"typescript": "^5.6.0",
|
|
46
|
-
"vitest": "^2.1.0"
|
|
46
|
+
"vitest": "^2.1.0",
|
|
47
|
+
"@unmap/optimize-schema": "0.21.0"
|
|
47
48
|
},
|
|
48
49
|
"scripts": {
|
|
49
50
|
"build": "tsup",
|