@any-routing/core 0.1.0 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/eslint.config.mjs +24 -0
- package/package.json +7 -28
- package/project.json +21 -0
- package/src/index.ts +4 -0
- package/src/lib/core.model.ts +209 -0
- package/src/lib/core.ts +572 -0
- package/src/lib/data-providers/errors/unauthorized.ts +6 -0
- package/src/lib/data-providers/index.ts +39 -0
- package/src/lib/utils/dispatcher.ts +37 -0
- package/src/lib/utils/{random.js → random.ts} +0 -1
- package/src/lib/utils/requester.ts +41 -0
- package/tsconfig.json +23 -0
- package/tsconfig.lib.json +28 -0
- package/tsconfig.spec.json +28 -0
- package/vite.config.mts +59 -0
- package/src/index.d.ts +0 -4
- package/src/index.js +0 -5
- package/src/index.js.map +0 -1
- package/src/lib/data-providers/errors/index.js +0 -2
- package/src/lib/data-providers/errors/index.js.map +0 -1
- package/src/lib/data-providers/errors/unauthorized.d.ts +0 -6
- package/src/lib/data-providers/errors/unauthorized.js +0 -8
- package/src/lib/data-providers/errors/unauthorized.js.map +0 -1
- package/src/lib/data-providers/index.d.ts +0 -44
- package/src/lib/data-providers/index.js +0 -2
- package/src/lib/data-providers/index.js.map +0 -1
- package/src/lib/libre-routing.d.ts +0 -43
- package/src/lib/libre-routing.js +0 -235
- package/src/lib/libre-routing.js.map +0 -1
- package/src/lib/libre-routing.model.d.ts +0 -108
- package/src/lib/libre-routing.model.js +0 -15
- package/src/lib/libre-routing.model.js.map +0 -1
- package/src/lib/utils/dispatcher.d.ts +0 -7
- package/src/lib/utils/dispatcher.js +0 -25
- package/src/lib/utils/dispatcher.js.map +0 -1
- package/src/lib/utils/index.js +0 -4
- package/src/lib/utils/index.js.map +0 -1
- package/src/lib/utils/random.d.ts +0 -1
- package/src/lib/utils/random.js.map +0 -1
- package/src/lib/utils/requester.d.ts +0 -11
- package/src/lib/utils/requester.js +0 -44
- package/src/lib/utils/requester.js.map +0 -1
- /package/src/lib/data-providers/errors/{index.d.ts → index.ts} +0 -0
- /package/src/lib/utils/{index.d.ts → index.ts} +0 -0
package/src/lib/core.ts
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import { AnyRoutingDataResponse, RouteSummary } from './data-providers';
|
|
2
|
+
import { Dispatcher } from './utils/dispatcher';
|
|
3
|
+
import { randomId } from './utils/random';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
AnyRoutingOptions,
|
|
7
|
+
AnyRoutingState,
|
|
8
|
+
PluginFactory,
|
|
9
|
+
Waypoint as InputWaypoint,
|
|
10
|
+
AnyRoutingPlugin,
|
|
11
|
+
AnyRoutingProjector,
|
|
12
|
+
InternalWaypoint,
|
|
13
|
+
InternalWaypointC,
|
|
14
|
+
AnyRoutingGeocoder,
|
|
15
|
+
RoutingEvents,
|
|
16
|
+
} from './core.model';
|
|
17
|
+
|
|
18
|
+
export type SetStateOptions<R extends AnyRoutingDataResponse> = {
|
|
19
|
+
waypoints?: InputWaypoint[];
|
|
20
|
+
data?: R;
|
|
21
|
+
routesShapeGeojson?: R['routesShapeGeojson'];
|
|
22
|
+
selectedRouteId?: number | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export class AnyRouting<R extends AnyRoutingDataResponse = AnyRoutingDataResponse, P extends AnyRoutingProjector = AnyRoutingProjector> {
|
|
26
|
+
private readonly dispatcher = new Dispatcher<RoutingEvents<R>>();
|
|
27
|
+
private readonly geocoder?: AnyRoutingGeocoder;
|
|
28
|
+
|
|
29
|
+
private readonly _options: AnyRoutingOptions<P>;
|
|
30
|
+
private readonly _plugins: AnyRoutingPlugin[] = [];
|
|
31
|
+
private _projector?: P;
|
|
32
|
+
|
|
33
|
+
private initialized = false;
|
|
34
|
+
private removed = false;
|
|
35
|
+
private calculationId = 0;
|
|
36
|
+
private latestAppliedCalculationId = 0;
|
|
37
|
+
private _state: AnyRoutingState<R>;
|
|
38
|
+
|
|
39
|
+
public constructor(options: AnyRoutingOptions<P>) {
|
|
40
|
+
this._options = {
|
|
41
|
+
uniqueKey: randomId(),
|
|
42
|
+
...options,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
this._state = this.createInitialState();
|
|
46
|
+
|
|
47
|
+
this._plugins = (options.plugins ?? []).map((plugin) => this.resolvePlugin(plugin));
|
|
48
|
+
this._projector = options.projector;
|
|
49
|
+
|
|
50
|
+
if (this.options.waypointsSyncStrategy === 'geocodeFirst' && !this.options.geocoder) {
|
|
51
|
+
throw new Error('Geocoder is required when waypointsSyncStrategy is `geocodeFirst`');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.geocoder =
|
|
55
|
+
this.options.geocoder && typeof this.options.geocoder === 'function'
|
|
56
|
+
? { geocode: this.options.geocoder }
|
|
57
|
+
: (this.options.geocoder as AnyRoutingGeocoder | undefined);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Public API
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
public get options(): AnyRoutingOptions {
|
|
65
|
+
return this._options;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public get state(): AnyRoutingState<R> {
|
|
69
|
+
return this._state;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public get data(): R | undefined {
|
|
73
|
+
return this._state.data;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public get dataProvider() {
|
|
77
|
+
return this.options.dataProvider;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public get projector(): P | undefined {
|
|
81
|
+
return this._projector;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public get selectedRouteId(): number | null | undefined {
|
|
85
|
+
return this._state.selectedRouteId;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public initialize(): void {
|
|
89
|
+
if (this.initialized || this.removed) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.initialized = true;
|
|
94
|
+
|
|
95
|
+
this._plugins.forEach((plugin) => {
|
|
96
|
+
plugin.onAdd(this);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
this._projector?.onAdd(this);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public setProjector(projector?: P): void {
|
|
103
|
+
if (this._projector === projector) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (this.initialized && !this.removed) {
|
|
108
|
+
this._projector?.onRemove(this);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this._projector = projector;
|
|
112
|
+
this._options.projector = projector;
|
|
113
|
+
|
|
114
|
+
if (this.initialized && !this.removed) {
|
|
115
|
+
this._projector?.onAdd(this);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public addPlugin<P extends PluginFactory>(
|
|
120
|
+
plugin: P,
|
|
121
|
+
): P extends new (...args: any[]) => infer I ? I : P {
|
|
122
|
+
const resolved = this.resolvePlugin(plugin);
|
|
123
|
+
this._plugins.push(resolved);
|
|
124
|
+
|
|
125
|
+
if (this.initialized && !this.removed) {
|
|
126
|
+
resolved.onAdd(this);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return resolved as P extends new (...args: any[]) => infer I ? I : P;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public removePlugin(plugin: AnyRoutingPlugin): void {
|
|
133
|
+
const index = this._plugins.indexOf(plugin);
|
|
134
|
+
|
|
135
|
+
if (index === -1) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this._plugins.splice(index, 1);
|
|
140
|
+
|
|
141
|
+
if (this.initialized && !this.removed) {
|
|
142
|
+
plugin.onRemove(this);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public setWaypointsSyncStrategy(strategy: AnyRoutingOptions['waypointsSyncStrategy']): void {
|
|
147
|
+
if (strategy === 'geocodeFirst' && !this.geocoder) {
|
|
148
|
+
throw new Error('Geocoder is required when waypointsSyncStrategy is `geocodeFirst`');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this._options.waypointsSyncStrategy = strategy;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public onRemove(): void {
|
|
155
|
+
if (this.removed) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.removed = true;
|
|
160
|
+
|
|
161
|
+
if (this.initialized) {
|
|
162
|
+
this._plugins.forEach((plugin) => {
|
|
163
|
+
plugin.onRemove(this);
|
|
164
|
+
});
|
|
165
|
+
this._projector?.onRemove(this);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.options.dataProvider?.destroy();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public on<E extends keyof RoutingEvents<R>>(
|
|
172
|
+
event: E,
|
|
173
|
+
callback: (event: RoutingEvents<R>[E]) => void,
|
|
174
|
+
): void {
|
|
175
|
+
this.dispatcher.on(event, callback);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public off<E extends keyof RoutingEvents<R>>(
|
|
179
|
+
event: E,
|
|
180
|
+
callback: (event: RoutingEvents<R>[E]) => void,
|
|
181
|
+
): void {
|
|
182
|
+
this.dispatcher.off(event, callback);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public setWaypoints(waypoints: InputWaypoint[]): void {
|
|
186
|
+
this.updateState(
|
|
187
|
+
{
|
|
188
|
+
waypoints: this.transformToInternalWaypoints(waypoints),
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public getWaypoint(index: number): InternalWaypoint | undefined {
|
|
194
|
+
return this._state.waypoints[index];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public setState(patch: SetStateOptions<R>): void {
|
|
198
|
+
const statePatch: Partial<AnyRoutingState<R>> = {};
|
|
199
|
+
|
|
200
|
+
if (patch.waypoints !== undefined) {
|
|
201
|
+
statePatch.waypoints = this.transformToInternalWaypoints(patch.waypoints);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (patch.data !== undefined) {
|
|
205
|
+
statePatch.data = patch.data;
|
|
206
|
+
statePatch.routesShapeGeojson = patch.data.routesShapeGeojson;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (patch.routesShapeGeojson !== undefined) {
|
|
210
|
+
statePatch.routesShapeGeojson = patch.routesShapeGeojson;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (patch.selectedRouteId !== undefined) {
|
|
214
|
+
statePatch.selectedRouteId = patch.selectedRouteId;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.updateState(statePatch);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
public applyCalculationResult(data: R): void {
|
|
221
|
+
const patchState: Partial<AnyRoutingState<R>> = {
|
|
222
|
+
data,
|
|
223
|
+
selectedRouteId: data.selectedRouteId,
|
|
224
|
+
routesShapeGeojson: data.routesShapeGeojson,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
if (this.options.waypointsSyncStrategy === 'toPath') {
|
|
228
|
+
patchState.waypoints = this.syncWaypointsPositions(data.routes[0].waypoints);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.updateState(patchState);
|
|
232
|
+
|
|
233
|
+
this.fire('routesFound', {
|
|
234
|
+
waypoints: this.state.waypoints,
|
|
235
|
+
data,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
public reset(): void {
|
|
240
|
+
const previousState = this._state;
|
|
241
|
+
|
|
242
|
+
this._state = this.createInitialState();
|
|
243
|
+
|
|
244
|
+
const updatedProperties = Object.keys(previousState) as Array<keyof AnyRoutingState<R>>;
|
|
245
|
+
|
|
246
|
+
this.fire('stateUpdated', {
|
|
247
|
+
updatedProperties,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
this.fire('waypointsChanged', {
|
|
251
|
+
waypoints: this._state.waypoints,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (previousState.loading !== this._state.loading) {
|
|
255
|
+
this.fire('loadingChanged', {
|
|
256
|
+
loading: this._state.loading,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
public getUniqueName(name: string): string {
|
|
262
|
+
return `${name}-${this.options.uniqueKey}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
public async recalculateRoute(): Promise<R | undefined> {
|
|
266
|
+
if (this.waypoints.length < 2) {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const dataProvider = this.options.dataProvider;
|
|
271
|
+
|
|
272
|
+
if (!dataProvider) {
|
|
273
|
+
throw new Error('No data provider');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const calculationId = ++this.calculationId;
|
|
277
|
+
|
|
278
|
+
this.setLoading(true);
|
|
279
|
+
this.fire('calculationStarted', { waypoints: this.waypoints });
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const waypointsSynced = await this.syncWaypointsBeforeCalculation(calculationId);
|
|
283
|
+
if (!waypointsSynced || calculationId !== this.calculationId) {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const data = (await dataProvider.request(this._state.waypoints, {
|
|
288
|
+
mode: 'default',
|
|
289
|
+
})) as R;
|
|
290
|
+
|
|
291
|
+
if (calculationId <= this.latestAppliedCalculationId) {
|
|
292
|
+
return data;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.latestAppliedCalculationId = calculationId;
|
|
296
|
+
|
|
297
|
+
const patchState: Partial<AnyRoutingState<R>> = {
|
|
298
|
+
data,
|
|
299
|
+
selectedRouteId: data.selectedRouteId,
|
|
300
|
+
routesShapeGeojson: data.routesShapeGeojson,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
if (this.options.waypointsSyncStrategy === 'toPath') {
|
|
304
|
+
patchState.waypoints = this.syncWaypointsPositions(data.routes[0].waypoints);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.updateState(patchState);
|
|
308
|
+
|
|
309
|
+
// Tylko ostatni rozpoczęty request może zakończyć loading.
|
|
310
|
+
if (calculationId === this.calculationId) {
|
|
311
|
+
this.setLoading(false);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this.fire('routesFound', {
|
|
315
|
+
waypoints: this.state.waypoints,
|
|
316
|
+
data,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
return data;
|
|
320
|
+
} catch (error: unknown) {
|
|
321
|
+
if (calculationId !== this.calculationId) {
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
this.updateState({
|
|
326
|
+
data: undefined,
|
|
327
|
+
selectedRouteId: null,
|
|
328
|
+
routesShapeGeojson: undefined,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
if (calculationId === this.calculationId) {
|
|
332
|
+
this.setLoading(false);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this.fire('calculationError', {
|
|
336
|
+
error: this.toError(error),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
public selectRoute(routeId: number): void {
|
|
344
|
+
const data = this.data;
|
|
345
|
+
|
|
346
|
+
if (!data) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const route = data.routes[routeId];
|
|
351
|
+
|
|
352
|
+
if (!route) {
|
|
353
|
+
throw new Error(`No route with id: ${routeId}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
this.updateState({
|
|
357
|
+
selectedRouteId: routeId,
|
|
358
|
+
data: {
|
|
359
|
+
...data,
|
|
360
|
+
selectedRouteId: routeId,
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
this.fire('routeSelected', {
|
|
365
|
+
route,
|
|
366
|
+
routeId,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
public syncWaypointsPositions(waypoints: RouteSummary['waypoints']): InternalWaypoint[] {
|
|
371
|
+
return waypoints.map((position, index) => {
|
|
372
|
+
const currentWaypoint = this._state.waypoints[index];
|
|
373
|
+
|
|
374
|
+
if (!currentWaypoint) {
|
|
375
|
+
return {
|
|
376
|
+
position,
|
|
377
|
+
originalPosition: position,
|
|
378
|
+
properties: {
|
|
379
|
+
index,
|
|
380
|
+
isFirst: index === 0,
|
|
381
|
+
isLast: index === waypoints.length - 1,
|
|
382
|
+
},
|
|
383
|
+
geocoded: false,
|
|
384
|
+
index,
|
|
385
|
+
isFirst: index === 0,
|
|
386
|
+
isLast: index === waypoints.length - 1,
|
|
387
|
+
} as InternalWaypoint;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
...currentWaypoint,
|
|
392
|
+
position,
|
|
393
|
+
originalPosition: currentWaypoint.originalPosition ?? currentWaypoint.position,
|
|
394
|
+
properties: currentWaypoint.properties,
|
|
395
|
+
};
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
// State
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
private createInitialState(): AnyRoutingState<R> {
|
|
404
|
+
return {
|
|
405
|
+
waypoints: [],
|
|
406
|
+
data: undefined,
|
|
407
|
+
loading: false,
|
|
408
|
+
selectedRouteId: null,
|
|
409
|
+
routesShapeGeojson: undefined,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private updateState(
|
|
414
|
+
patch: Partial<AnyRoutingState<R>>,
|
|
415
|
+
): void {
|
|
416
|
+
const updatedProperties = Object.keys(patch) as Array<keyof AnyRoutingState<R>>;
|
|
417
|
+
|
|
418
|
+
if (updatedProperties.length === 0) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const previousState = { ...this._state };
|
|
423
|
+
|
|
424
|
+
this._state = {
|
|
425
|
+
...previousState,
|
|
426
|
+
...patch,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
this.fire('stateUpdated', {
|
|
430
|
+
updatedProperties,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
if ('waypoints' in patch) {
|
|
434
|
+
this.fire('waypointsChanged', {
|
|
435
|
+
waypoints: this._state.waypoints,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if ('loading' in patch) {
|
|
440
|
+
this.fire('loadingChanged', {
|
|
441
|
+
loading: this._state.loading,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private setLoading(loading: boolean): void {
|
|
447
|
+
if (this._state.loading === loading) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
this.updateState({
|
|
452
|
+
loading,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
// Calculation
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
|
|
460
|
+
private async syncWaypointsBeforeCalculation(
|
|
461
|
+
calculationId: number,
|
|
462
|
+
): Promise<boolean> {
|
|
463
|
+
if (this.options.waypointsSyncStrategy !== 'geocodeFirst') {
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const waypointsToGeocode = this._state.waypoints.filter((waypoint) => !waypoint.geocoded);
|
|
468
|
+
|
|
469
|
+
if (waypointsToGeocode.length === 0) {
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const previousWaypoints = this._state.waypoints;
|
|
474
|
+
|
|
475
|
+
const waypoints = await this.geocodeWaypoints(previousWaypoints);
|
|
476
|
+
|
|
477
|
+
if (calculationId !== this.calculationId) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
this.updateState({
|
|
482
|
+
waypoints,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
waypoints.forEach((waypoint, index) => {
|
|
486
|
+
if (previousWaypoints[index] && !previousWaypoints[index].geocoded && waypoint.geocoded) {
|
|
487
|
+
this.fire('waypointGeocoded', {
|
|
488
|
+
waypoint,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
// Plugins
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
|
|
499
|
+
private resolvePlugin(plugin: PluginFactory): AnyRoutingPlugin {
|
|
500
|
+
if (typeof plugin === 'function') {
|
|
501
|
+
return new plugin();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return plugin;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
// Events
|
|
509
|
+
// ---------------------------------------------------------------------------
|
|
510
|
+
|
|
511
|
+
private fire<E extends keyof RoutingEvents<R>>(
|
|
512
|
+
event: E,
|
|
513
|
+
data: Omit<RoutingEvents<R>[E], 'state'>,
|
|
514
|
+
): void {
|
|
515
|
+
this.dispatcher.fire(event, {
|
|
516
|
+
...data,
|
|
517
|
+
state: this._state,
|
|
518
|
+
} as RoutingEvents<R>[E]);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
// Waypoints
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
|
|
525
|
+
private transformToInternalWaypoints(waypoints: InputWaypoint[]): InternalWaypoint[] {
|
|
526
|
+
return waypoints.map((waypoint, index) =>
|
|
527
|
+
InternalWaypointC.fromWaypoint(waypoint, {
|
|
528
|
+
index,
|
|
529
|
+
isFirst: index === 0,
|
|
530
|
+
isLast: index === waypoints.length - 1,
|
|
531
|
+
}),
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private geocodeWaypoints(waypoints: InternalWaypoint[]): Promise<InternalWaypoint[]> {
|
|
536
|
+
const geocoder = this.geocoder;
|
|
537
|
+
if (!geocoder) {
|
|
538
|
+
return Promise.resolve(waypoints);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return Promise.all(
|
|
542
|
+
waypoints.map(async (waypoint) => {
|
|
543
|
+
if (waypoint.geocoded) {
|
|
544
|
+
return waypoint;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const geocodedWaypoint = await geocoder.geocode(waypoint);
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
...geocodedWaypoint,
|
|
551
|
+
geocoded: true,
|
|
552
|
+
};
|
|
553
|
+
}),
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
// Utils
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
|
|
561
|
+
private toError(error: unknown): Error {
|
|
562
|
+
if (error instanceof Error) {
|
|
563
|
+
return error;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return new Error(String(error));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private get waypoints(): InternalWaypoint[] {
|
|
570
|
+
return this._state.waypoints;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { BBox, FeatureCollection, Geometry, LineString } from 'geojson';
|
|
2
|
+
import { Waypoint, LngLatPosition } from '../core.model';
|
|
3
|
+
|
|
4
|
+
export interface AnyRoutingDataProvider {
|
|
5
|
+
request: (waypoints: Waypoint[], opts: RequestOptions) => Promise<AnyRoutingDataResponse>;
|
|
6
|
+
|
|
7
|
+
destroy(): void;
|
|
8
|
+
hasPendingRequests(): Promise<boolean>;
|
|
9
|
+
abortAllRequests(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type RequestOptions = Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
export interface AnyRoutingDataResponse {
|
|
15
|
+
rawResponse: unknown;
|
|
16
|
+
routesShapeGeojson: FeatureCollection<Geometry, { routeId: number; waypoint: number }>;
|
|
17
|
+
routes: RouteSummary[];
|
|
18
|
+
selectedRouteId?: number | null;
|
|
19
|
+
routesShapeBounds?: BBox;
|
|
20
|
+
version: number;
|
|
21
|
+
latest: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type RoutePath = LngLatPosition[];
|
|
25
|
+
|
|
26
|
+
export interface RouteSummary {
|
|
27
|
+
id: number;
|
|
28
|
+
label?: string;
|
|
29
|
+
path: RoutePath;
|
|
30
|
+
durationTime: number;
|
|
31
|
+
arriveTime: Date;
|
|
32
|
+
departureTime: Date;
|
|
33
|
+
distance: number;
|
|
34
|
+
cost?: number;
|
|
35
|
+
waypoints: { lat: number; lng: number }[];
|
|
36
|
+
shape: FeatureCollection<LineString, { routeId: number; waypoint: number }>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export * from './errors';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export class Dispatcher<EventMap extends object> {
|
|
2
|
+
private callbacks: {
|
|
3
|
+
[E in keyof EventMap]?: Array<(event: EventMap[E]) => void>;
|
|
4
|
+
} = {};
|
|
5
|
+
|
|
6
|
+
public fire<E extends keyof EventMap>(
|
|
7
|
+
event: E,
|
|
8
|
+
data: EventMap[E],
|
|
9
|
+
): void {
|
|
10
|
+
for (const callback of this.callbacks[event] ?? []) {
|
|
11
|
+
try {
|
|
12
|
+
callback(data);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error(error);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public on<E extends keyof EventMap>(
|
|
20
|
+
event: E,
|
|
21
|
+
callback: (event: EventMap[E]) => void,
|
|
22
|
+
): void {
|
|
23
|
+
this.callbacks[event] = [
|
|
24
|
+
...(this.callbacks[event] ?? []),
|
|
25
|
+
callback,
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public off<E extends keyof EventMap>(
|
|
30
|
+
event: E,
|
|
31
|
+
callback: (event: EventMap[E]) => void,
|
|
32
|
+
): void {
|
|
33
|
+
this.callbacks[event] = (
|
|
34
|
+
this.callbacks[event] ?? []
|
|
35
|
+
).filter((item) => item !== callback);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export class Requester {
|
|
2
|
+
private buffer: AbortController[] = [];
|
|
3
|
+
private readonly maxBuffer = 4;
|
|
4
|
+
public get hasPendingRequests(): boolean {
|
|
5
|
+
return this.buffer.length > 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
public async request(url: string, params?: RequestInit) {
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
|
|
11
|
+
if (this.buffer.length >= this.maxBuffer) {
|
|
12
|
+
this.buffer.shift()?.abort();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
this.buffer.push(controller);
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(url, {
|
|
19
|
+
...params,
|
|
20
|
+
signal: controller.signal,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw response;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return await response.json();
|
|
28
|
+
} finally {
|
|
29
|
+
this.cleanBuffer(controller);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public abortAllRequests(): void {
|
|
34
|
+
this.buffer.forEach((controller) => controller.abort());
|
|
35
|
+
this.buffer = [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private cleanBuffer(controller: AbortController): void {
|
|
39
|
+
this.buffer = this.buffer.filter((ctrl) => ctrl !== controller);
|
|
40
|
+
}
|
|
41
|
+
}
|