@any-routing/osrm-data-provider 1.0.0-rc.1

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 ADDED
@@ -0,0 +1,31 @@
1
+ # osrm-data-provider
2
+
3
+ `AnyRoutingDataProvider` implementation backed by an [OSRM](https://project-osrm.org/) HTTP server (`route` service, see the [OSRM HTTP API docs](https://project-osrm.org/docs/v5.24.0/api/#requests)).
4
+
5
+ ## Usage
6
+
7
+ ```ts
8
+ import { AnyRouting } from '@any-routing/core';
9
+ import { OsrmProvider } from '@any-routing/osrm-data-provider';
10
+
11
+ const dataProvider = new OsrmProvider({
12
+ baseUrl: 'https://router.project-osrm.org/route/v1/driving',
13
+ alternatives: 2,
14
+ });
15
+
16
+ const routing = new AnyRouting({
17
+ dataProvider,
18
+ waypointsSyncStrategy: 'none',
19
+ });
20
+ ```
21
+
22
+ By default requests run inside a Web Worker (`worker: true`), mirroring `@any-routing/here-data-provider`. Set `worker: false` to run the executor on the main thread instead.
23
+
24
+ ## Building
25
+
26
+ Run `nx build osrm-data-provider` to build the library.
27
+
28
+ ## Running unit tests
29
+
30
+ Run `nx test osrm-data-provider` to execute the unit tests via [Vitest](https://vitest.dev/).
31
+
@@ -0,0 +1,22 @@
1
+ import baseConfig from '../../eslint.config.mjs';
2
+
3
+ export default [
4
+ ...baseConfig,
5
+ {
6
+ files: ['**/*.json'],
7
+ rules: {
8
+ '@nx/dependency-checks': [
9
+ 'error',
10
+ {
11
+ ignoredFiles: [
12
+ '{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
13
+ '{projectRoot}/vite.config.{js,ts,mjs,mts}',
14
+ ],
15
+ },
16
+ ],
17
+ },
18
+ languageOptions: {
19
+ parser: await import('jsonc-eslint-parser'),
20
+ },
21
+ },
22
+ ];
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@any-routing/osrm-data-provider",
3
+ "version": "1.0.0-rc.1",
4
+ "type": "module",
5
+ "main": "./index.js",
6
+ "types": "./index.d.ts",
7
+ "dependencies": {
8
+ "@any-routing/core": "1.0.0-rc.1",
9
+ "comlink": "4.4.2",
10
+ "@turf/bbox": "7.4.0",
11
+ "@turf/helpers": "7.4.0",
12
+ "@types/geojson": "7946.0.16"
13
+ }
14
+ }
package/project.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "osrm-data-provider",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "libs/osrm-data-provider/src",
5
+ "projectType": "library",
6
+ "release": {
7
+ "version": {
8
+ "manifestRootsToUpdate": ["dist/{projectRoot}"],
9
+ "currentVersionResolver": "git-tag",
10
+ "fallbackCurrentVersionResolver": "disk"
11
+ }
12
+ },
13
+ "tags": [],
14
+ "targets": {
15
+ "nx-release-publish": {
16
+ "options": {
17
+ "packageRoot": "dist/{projectRoot}"
18
+ }
19
+ }
20
+ }
21
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './lib/osrm-data-provider';
2
+ export * from './lib/osrm-provider.types';
@@ -0,0 +1,7 @@
1
+ // import { OsrmProvider } from './osrm-data-provider';
2
+
3
+ // describe('OsrmProvider', () => {
4
+ // it('should work', () => {
5
+ // expect(OsrmProvider).toEqual('osrm-data-provider');
6
+ // });
7
+ // });
@@ -0,0 +1,120 @@
1
+ import { wrap, Remote } from 'comlink';
2
+
3
+ import { ExecutorRequestOptions, OsrmExecutor } from './osrm.executor';
4
+ import type {
5
+ AnyRoutingDataProvider,
6
+ RequestOptions,
7
+ Waypoint,
8
+ WaypointPosition,
9
+ } from '@any-routing/core';
10
+ import { OsrmRoutingData, Options } from './osrm-provider.types';
11
+
12
+ const defaultOptions: Partial<Options> = {
13
+ baseUrl: 'https://router.project-osrm.org/route/v1/driving',
14
+ worker: true,
15
+ alternatives: 0,
16
+ steps: true,
17
+ geometries: 'geojson',
18
+ overview: 'full',
19
+ };
20
+
21
+ export class OsrmProvider implements AnyRoutingDataProvider {
22
+ private worker?: Worker;
23
+ // Comlink's `wrap` returns a `Remote<T>` proxy whose methods always
24
+ // resolve to promises, even for originally-synchronous ones — typing
25
+ // this union up front avoids the `@ts-ignore` a plain assignment would
26
+ // otherwise need.
27
+ private executorAPI: OsrmExecutor | Remote<OsrmExecutor>;
28
+ private _options: Options;
29
+
30
+ public get options(): Options {
31
+ return this._options;
32
+ }
33
+
34
+ constructor(options: Options = {}) {
35
+ this._options = { ...defaultOptions, ...options };
36
+
37
+ if (this.options.worker === true) {
38
+ this.worker = new Worker(new URL('./osrm.worker', import.meta.url), {
39
+ type: 'module',
40
+ });
41
+
42
+ this.executorAPI = wrap<OsrmExecutor>(this.worker);
43
+ } else {
44
+ this.executorAPI = new OsrmExecutor();
45
+ }
46
+ }
47
+
48
+ public destroy(): void {
49
+ if (this.worker) {
50
+ this.worker.terminate();
51
+ }
52
+ }
53
+
54
+ public request(waypoints: Waypoint[], opts: RequestOptions): Promise<OsrmRoutingData> {
55
+ const url = this.buildUrl(waypoints, { ...this.options, ...opts });
56
+
57
+ // `buildUrl` is a function and must never be forwarded into
58
+ // `requestParams` below — when running with `worker: true`,
59
+ // `executorAPI` is a Comlink `Remote<OsrmExecutor>` and the call goes
60
+ // through `postMessage`'s structured clone algorithm, which cannot
61
+ // clone functions and throws `DataCloneError` at runtime.
62
+ const requestOptions = { ...this.options, ...opts, url };
63
+ delete requestOptions.buildUrl;
64
+ const requestParams: ExecutorRequestOptions = requestOptions;
65
+
66
+ return this.executorAPI.request(requestParams);
67
+ }
68
+
69
+ public async abortAllRequests(): Promise<void> {
70
+ await this.executorAPI.abortAllRequests();
71
+ }
72
+
73
+ public setOption<T extends keyof Options>(optionKey: T, value: Options[T]): void {
74
+ this.options[optionKey] = value;
75
+ }
76
+
77
+ public async hasPendingRequests(): Promise<boolean> {
78
+ return await this.executorAPI.hasPendingRequests();
79
+ }
80
+
81
+ private buildUrl(waypoints: Waypoint[], opts: Options & RequestOptions): string {
82
+ if (!waypoints[0] || !waypoints[waypoints.length - 1]) {
83
+ throw new Error('At least two waypoints are required');
84
+ }
85
+
86
+ const coordinates = waypoints.map((w) => this.formatWp(w.position)).join(';');
87
+
88
+ const queryParamsObj: Record<string, string | number | boolean | undefined | null> = {
89
+ alternatives: opts.mode === 'default' ? opts.alternatives ?? 0 : 0,
90
+ steps: opts.steps,
91
+ geometries: opts.geometries || 'geojson',
92
+ overview: opts.overview || 'full',
93
+ continue_straight: opts.continueStraight,
94
+ annotations: opts.annotations,
95
+ ...opts.queryParams,
96
+ };
97
+
98
+ // Only truthy/explicitly-set values are sent, matching OSRM's own
99
+ // defaults for anything omitted (mirrors here-data-provider's filter).
100
+ const queryParams: Record<string, string> = Object.entries(queryParamsObj).reduce(
101
+ (acc, [key, value]) => {
102
+ if (value !== undefined && value !== null && value !== '') {
103
+ acc[key] = String(value);
104
+ }
105
+ return acc;
106
+ },
107
+ {} as Record<string, string>,
108
+ );
109
+
110
+ const qp = new URLSearchParams(queryParams).toString();
111
+
112
+ const url = `${this.options.baseUrl}/${coordinates}${qp ? `?${qp}` : ''}`;
113
+
114
+ return this.options.buildUrl ? this.options.buildUrl({ waypoints, options: opts }, url) : url;
115
+ }
116
+
117
+ private formatWp({ lat, lng }: WaypointPosition): string {
118
+ return `${lng},${lat}`;
119
+ }
120
+ }
@@ -0,0 +1,95 @@
1
+ import type { AnyRoutingDataResponse, RequestOptions, RouteSummary, Waypoint } from '@any-routing/core';
2
+ import type { ExecutorRequestOptions } from './osrm.executor';
3
+
4
+ export type OsrmGeometry = 'polyline' | 'polyline6' | 'geojson';
5
+ export type OsrmOverview = 'simplified' | 'full' | 'false';
6
+
7
+ export interface OsrmRouteSummary extends RouteSummary {
8
+ rawRoute: OsrmRawRoute;
9
+ }
10
+
11
+ export interface OsrmRoutingData extends AnyRoutingDataResponse {
12
+ routes: OsrmRouteSummary[];
13
+ requestOptions: ExecutorRequestOptions;
14
+ }
15
+
16
+ export type Options = {
17
+ /**
18
+ * Base URL of the OSRM HTTP server up to and including the profile, e.g.
19
+ * `https://router.project-osrm.org/route/v1/driving`. `{coordinates}` and
20
+ * query options are appended by this provider.
21
+ */
22
+ baseUrl?: string;
23
+ worker?: boolean;
24
+ alternatives?: number;
25
+ steps?: boolean;
26
+ annotations?: boolean | 'nodes' | 'distance' | 'duration' | 'datasources' | 'weight' | 'speed';
27
+ geometries?: OsrmGeometry;
28
+ overview?: OsrmOverview;
29
+ continueStraight?: 'default' | boolean;
30
+ queryParams?: Record<string, unknown>;
31
+ requestParams?: RequestInit;
32
+ buildUrl?: (ctx: { waypoints: Waypoint[]; options: Options & RequestOptions }, url: string) => string;
33
+ };
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // OSRM `route` service raw response types (the subset the executor reads).
37
+ // Modeled after https://project-osrm.org/docs/v5.24.0/api/#route-service and
38
+ // the OSRM HTTP API docs. `geometries=geojson` is always requested by this
39
+ // provider so `geometry` is always a GeoJSON `LineString`.
40
+ // ---------------------------------------------------------------------------
41
+
42
+ export interface OsrmRawLineStringGeometry {
43
+ type: 'LineString';
44
+ coordinates: [number, number][];
45
+ }
46
+
47
+ export interface OsrmRawStepManeuver {
48
+ location: [number, number];
49
+ bearing_before: number;
50
+ bearing_after: number;
51
+ type: string;
52
+ modifier?: string;
53
+ exit?: number;
54
+ }
55
+
56
+ export interface OsrmRawStep {
57
+ distance: number;
58
+ duration: number;
59
+ weight?: number;
60
+ name: string;
61
+ ref?: string;
62
+ geometry: OsrmRawLineStringGeometry;
63
+ maneuver: OsrmRawStepManeuver;
64
+ }
65
+
66
+ export interface OsrmRawLeg {
67
+ distance: number;
68
+ duration: number;
69
+ weight?: number;
70
+ summary?: string;
71
+ steps: OsrmRawStep[];
72
+ }
73
+
74
+ export interface OsrmRawRoute {
75
+ distance: number;
76
+ duration: number;
77
+ weight?: number;
78
+ weight_name?: string;
79
+ geometry: OsrmRawLineStringGeometry;
80
+ legs: OsrmRawLeg[];
81
+ }
82
+
83
+ export interface OsrmRawWaypoint {
84
+ hint?: string;
85
+ distance: number;
86
+ name: string;
87
+ location: [number, number];
88
+ }
89
+
90
+ export interface OsrmApiResponse {
91
+ code: string;
92
+ message?: string;
93
+ waypoints?: OsrmRawWaypoint[];
94
+ routes?: OsrmRawRoute[];
95
+ }
@@ -0,0 +1,156 @@
1
+ import bbox from '@turf/bbox';
2
+ import { featureCollection, lineString } from '@turf/helpers';
3
+ import type { BBox, Feature, LineString } from 'geojson';
4
+
5
+ import { type Mode, Requester, type WaypointPosition } from '@any-routing/core';
6
+
7
+ import type {
8
+ Options,
9
+ OsrmApiResponse,
10
+ OsrmRawLeg,
11
+ OsrmRawRoute,
12
+ OsrmRouteSummary,
13
+ OsrmRoutingData,
14
+ } from './osrm-provider.types';
15
+
16
+ export type ExecutorRequestOptions = { url: string; mode: Mode } & Options;
17
+
18
+ /** Properties attached to each rendered segment of a route's shape. */
19
+ type ShapeFeatureProperties = {
20
+ waypoint: number;
21
+ routeId: number;
22
+ };
23
+
24
+ export class OsrmExecutor {
25
+ private readonly requester = new Requester();
26
+
27
+ async request(opts: ExecutorRequestOptions): Promise<OsrmRoutingData> {
28
+ const data = (await this.requester.request(opts.url, {
29
+ method: 'GET',
30
+ headers: {
31
+ Accept: 'application/json',
32
+ },
33
+ ...opts.requestParams,
34
+ })) as OsrmApiResponse;
35
+
36
+ if (data.code !== 'Ok' || !data.routes?.length) {
37
+ throw new Error(data.message || `OSRM request failed with code "${data.code}"`);
38
+ }
39
+
40
+ const routeSummaries = data.routes.map((route, routeId) =>
41
+ summarizeRoute(route, routeId),
42
+ );
43
+
44
+ const features = routeSummaries.flatMap((routeSummary, fid) =>
45
+ routeSummary.shape.features.map((feature) => ({
46
+ id: fid,
47
+ ...feature,
48
+ properties: {
49
+ ...feature.properties,
50
+ id: fid,
51
+ routeId: routeSummary.id,
52
+ },
53
+ })),
54
+ );
55
+
56
+ const routesShapeGeojson = featureCollection(features);
57
+
58
+ return {
59
+ routesShapeBounds: bbox(routesShapeGeojson) as BBox,
60
+ rawResponse: data,
61
+ routes: routeSummaries,
62
+ selectedRouteId: routeSummaries.length ? 0 : null,
63
+ routesShapeGeojson,
64
+ version: performance.now(),
65
+ latest: !this.requester.hasPendingRequests,
66
+ mode: opts.mode,
67
+ requestOptions: opts,
68
+ };
69
+ }
70
+
71
+ hasPendingRequests() {
72
+ return this.requester.hasPendingRequests;
73
+ }
74
+
75
+ abortAllRequests() {
76
+ this.requester.abortAllRequests();
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Waypoints introduced by a leg: OSRM legs don't carry per-leg
82
+ * departure/arrival coordinates directly, so waypoints are instead derived
83
+ * from the route's own `geometry` boundaries via the accumulated leg
84
+ * distances — simpler and sufficient here: every leg boundary is a waypoint,
85
+ * taken from the first/last coordinate of the leg's own geometry (built up
86
+ * from its steps).
87
+ */
88
+ function extractLegWaypoints(leg: OsrmRawLeg): WaypointPosition[] {
89
+ const firstStep = leg.steps[0];
90
+ const lastStep = leg.steps[leg.steps.length - 1];
91
+
92
+ if (!firstStep || !lastStep) {
93
+ return [];
94
+ }
95
+
96
+ const [lng, lat] = firstStep.maneuver.location;
97
+
98
+ return [{ lat, lng }];
99
+ }
100
+
101
+ function buildLegShape(
102
+ leg: OsrmRawLeg,
103
+ routeId: number,
104
+ waypointIndex: number,
105
+ ): Feature<LineString, ShapeFeatureProperties>[] {
106
+ const legPath = leg.steps.flatMap((step) => step.geometry.coordinates);
107
+
108
+ if (!legPath.length) {
109
+ return [];
110
+ }
111
+
112
+ return [
113
+ lineString(legPath, { waypoint: waypointIndex, routeId }) as Feature<
114
+ LineString,
115
+ ShapeFeatureProperties
116
+ >,
117
+ ];
118
+ }
119
+
120
+ const summarizeRoute = (route: OsrmRawRoute, routeId: number): OsrmRouteSummary => {
121
+ const now = new Date();
122
+
123
+ let waypoints: WaypointPosition[] = [];
124
+ let shapeFeatures: Feature<LineString, ShapeFeatureProperties>[] = [];
125
+ let waypointIndex = 0;
126
+
127
+ route.legs.forEach((leg, index) => {
128
+ waypoints = [...waypoints, ...extractLegWaypoints(leg)];
129
+ shapeFeatures = [...shapeFeatures, ...buildLegShape(leg, routeId, waypointIndex)];
130
+
131
+ const isLastLeg = index === route.legs.length - 1;
132
+ if (isLastLeg) {
133
+ const lastStep = leg.steps[leg.steps.length - 1];
134
+ if (lastStep) {
135
+ const [lng, lat] = lastStep.maneuver.location;
136
+ waypoints = [...waypoints, { lat, lng }];
137
+ }
138
+ }
139
+
140
+ waypointIndex += 1;
141
+ });
142
+
143
+ const path = route.geometry.coordinates.map(([lng, lat]) => [lat, lng]);
144
+
145
+ return {
146
+ id: routeId,
147
+ durationTime: route.duration,
148
+ distance: route.distance,
149
+ path,
150
+ arriveTime: new Date(now.getTime() + route.duration * 1000),
151
+ departureTime: now,
152
+ waypoints,
153
+ shape: featureCollection(shapeFeatures),
154
+ rawRoute: route,
155
+ };
156
+ };
@@ -0,0 +1,6 @@
1
+ import { expose } from 'comlink';
2
+ import { OsrmExecutor } from './osrm.executor';
3
+
4
+ const executor = new OsrmExecutor();
5
+
6
+ expose(executor);
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "es2022",
5
+ "forceConsistentCasingInFileNames": true,
6
+ "strict": true,
7
+ "importHelpers": true,
8
+ "noImplicitOverride": true,
9
+ "noImplicitReturns": true,
10
+ "noFallthroughCasesInSwitch": true,
11
+ "noPropertyAccessFromIndexSignature": true
12
+ },
13
+ "files": [],
14
+ "include": [],
15
+ "references": [
16
+ {
17
+ "path": "./tsconfig.lib.json"
18
+ },
19
+ {
20
+ "path": "./tsconfig.spec.json"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../dist/out-tsc",
5
+ "declaration": true,
6
+ "types": ["node", "vite/client"]
7
+ },
8
+ "include": ["src/**/*.ts"],
9
+ "exclude": [
10
+ "vite.config.ts",
11
+ "vite.config.mts",
12
+ "vitest.config.ts",
13
+ "vitest.config.mts",
14
+ "src/**/*.test.ts",
15
+ "src/**/*.spec.ts",
16
+ "src/**/*.test.tsx",
17
+ "src/**/*.spec.tsx",
18
+ "src/**/*.test.js",
19
+ "src/**/*.spec.js",
20
+ "src/**/*.test.jsx",
21
+ "src/**/*.spec.jsx"
22
+ ]
23
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../dist/out-tsc",
5
+ "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node", "vitest"]
6
+ },
7
+ "include": [
8
+ "vite.config.ts",
9
+ "vite.config.mts",
10
+ "vitest.config.ts",
11
+ "vitest.config.mts",
12
+ "src/**/*.test.ts",
13
+ "src/**/*.spec.ts",
14
+ "src/**/*.test.tsx",
15
+ "src/**/*.spec.tsx",
16
+ "src/**/*.test.js",
17
+ "src/**/*.spec.js",
18
+ "src/**/*.test.jsx",
19
+ "src/**/*.spec.jsx",
20
+ "src/**/*.d.ts"
21
+ ]
22
+ }
@@ -0,0 +1,59 @@
1
+ /// <reference types='vitest' />
2
+ import { defineConfig } from 'vite';
3
+ import dts from 'vite-plugin-dts';
4
+ import * as path from 'path';
5
+ import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
6
+ import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
7
+
8
+ export default defineConfig(() => ({
9
+ root: import.meta.dirname,
10
+ cacheDir: '../../node_modules/.vite/libs/osrm-data-provider',
11
+ plugins: [
12
+ nxViteTsPaths(),
13
+ nxCopyAssetsPlugin(['*.md']),
14
+ dts({
15
+ entryRoot: 'src',
16
+ tsconfigPath: path.join(import.meta.dirname, 'tsconfig.lib.json'),
17
+ pathsToAliases: false,
18
+ }),
19
+ ],
20
+ // Uncomment this if you are using workers.
21
+ // worker: {
22
+ // plugins: () => [ nxViteTsPaths() ],
23
+ // },
24
+ // Configuration for building your library.
25
+ // See: https://vite.dev/guide/build.html#library-mode
26
+ build: {
27
+ outDir: '../../dist/libs/osrm-data-provider',
28
+ emptyOutDir: true,
29
+ reportCompressedSize: true,
30
+ commonjsOptions: {
31
+ transformMixedEsModules: true,
32
+ },
33
+ lib: {
34
+ // Could also be a dictionary or array of multiple entry points.
35
+ entry: 'src/index.ts',
36
+ name: 'osrm-data-provider',
37
+ fileName: 'index',
38
+ // Change this to the formats you want to support.
39
+ // Don't forget to update your package.json as well.
40
+ formats: ['es' as const],
41
+ },
42
+ rolldownOptions: {
43
+ // External packages that should not be bundled into your library.
44
+ external: [],
45
+ },
46
+ },
47
+ test: {
48
+ name: 'osrm-data-provider',
49
+ watch: false,
50
+ globals: true,
51
+ environment: 'node',
52
+ include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
53
+ reporters: ['default'],
54
+ coverage: {
55
+ reportsDirectory: '../../coverage/libs/osrm-data-provider',
56
+ provider: 'v8' as const,
57
+ },
58
+ },
59
+ }));