@any-routing/ors-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,22 @@
1
+ # ors-data-provider
2
+
3
+ `@any-routing/ors-data-provider` uses the openrouteservice Directions API v2
4
+ GeoJSON endpoint.
5
+
6
+ ```ts
7
+ import { OrsProvider } from '@any-routing/ors-data-provider';
8
+
9
+ const dataProvider = new OrsProvider({
10
+ apiKey: 'YOUR_ORS_API_KEY',
11
+ profile: 'driving-car',
12
+ alternatives: 2,
13
+ });
14
+ ```
15
+
16
+ Requests use `POST /openrouteservice/v2/directions/{profile}/geojson` on the
17
+ documented `api.heigit.org` server, send
18
+ coordinates as `[longitude, latitude]` pairs, and authenticate with the
19
+ `Authorization` header as required by the
20
+ [openrouteservice Directions API](https://docs.openrouteservice.org/all/docs).
21
+ Requests run in a Web Worker by default; set `worker: false` to execute on the
22
+ main thread.
@@ -0,0 +1,3 @@
1
+ import baseConfig from '../../eslint.config.mjs';
2
+
3
+ export default [...baseConfig];
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@any-routing/ors-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,28 @@
1
+ {
2
+ "name": "ors-data-provider",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "libs/ors-data-provider/src",
5
+ "projectType": "library",
6
+ "targets": {
7
+ "build": {
8
+ "executor": "@nx/vite:build",
9
+ "outputs": ["{options.outputPath}"],
10
+ "options": {
11
+ "outputPath": "dist/libs/ors-data-provider",
12
+ "configFile": "libs/ors-data-provider/vite.config.mts"
13
+ }
14
+ },
15
+ "test": {
16
+ "executor": "@nx/vitest:test",
17
+ "options": {
18
+ "configFile": "libs/ors-data-provider/vite.config.mts"
19
+ }
20
+ },
21
+ "lint": {
22
+ "executor": "@nx/eslint:lint",
23
+ "options": {
24
+ "lintFilePatterns": ["libs/ors-data-provider/**/*.ts"]
25
+ }
26
+ }
27
+ }
28
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './lib/ors-data-provider';
2
+ export * from './lib/ors-provider.types';
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { OrsProvider } from './ors-data-provider';
4
+
5
+ describe('OrsProvider', () => {
6
+ it('sends an ORS GeoJSON directions request with longitude/latitude coordinates', async () => {
7
+ const fetchMock = vi.fn().mockResolvedValue(
8
+ new Response(
9
+ JSON.stringify({
10
+ type: 'FeatureCollection',
11
+ features: [
12
+ {
13
+ type: 'Feature',
14
+ geometry: {
15
+ type: 'LineString',
16
+ coordinates: [
17
+ [19, 52],
18
+ [20, 53],
19
+ ],
20
+ },
21
+ properties: {
22
+ summary: { distance: 1000, duration: 120 },
23
+ },
24
+ },
25
+ ],
26
+ }),
27
+ { headers: { 'Content-Type': 'application/geo+json' } },
28
+ ),
29
+ );
30
+ vi.stubGlobal('fetch', fetchMock);
31
+
32
+ const provider = new OrsProvider({
33
+ apiKey: 'test-key',
34
+ baseUrl: 'https://api.openrouteservice.org/v2/directions',
35
+ worker: false,
36
+ });
37
+ const data = await provider.request(
38
+ [
39
+ { position: { lat: 52, lng: 19 } },
40
+ { position: { lat: 53, lng: 20 } },
41
+ ],
42
+ { mode: 'default' },
43
+ );
44
+
45
+ expect(fetchMock).toHaveBeenCalledWith(
46
+ 'https://api.openrouteservice.org/v2/directions/driving-car',
47
+ expect.objectContaining({
48
+ method: 'POST',
49
+ headers: expect.objectContaining({ Authorization: 'test-key' }),
50
+ }),
51
+ );
52
+ expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toMatchObject({
53
+ coordinates: [
54
+ [19, 52],
55
+ [20, 53],
56
+ ],
57
+ });
58
+ expect(data.routes[0]?.distance).toBe(1000);
59
+ expect(data.routes[0]?.path).toEqual([
60
+ [52, 19],
61
+ [53, 20],
62
+ ]);
63
+ });
64
+ });
@@ -0,0 +1,83 @@
1
+ import { Remote, wrap } from 'comlink';
2
+
3
+ import type { AnyRoutingDataProvider, RequestOptions, Waypoint } from '@any-routing/core';
4
+ import { OrsExecutor, type ExecutorRequestOptions } from './ors.executor';
5
+ import type { Options, OrsRoutingData } from './ors-provider.types';
6
+
7
+ const defaultOptions: Partial<Options> = {
8
+ baseUrl: 'https://api.heigit.org/openrouteservice/v2/directions',
9
+ profile: 'driving-car',
10
+ worker: true,
11
+ alternatives: 0,
12
+ preference: 'recommended',
13
+ units: 'm',
14
+ instructions: true,
15
+ instructionsFormat: 'text',
16
+ geometry: true,
17
+ maneuvers: false,
18
+ };
19
+
20
+ export class OrsProvider implements AnyRoutingDataProvider {
21
+ private worker?: Worker;
22
+ private executorAPI: OrsExecutor | Remote<OrsExecutor>;
23
+ private _options: Options;
24
+
25
+ public get options(): Options {
26
+ return this._options;
27
+ }
28
+
29
+ constructor(options: Options) {
30
+ this._options = { ...defaultOptions, ...options };
31
+ if (!this.options.apiKey) {
32
+ throw new Error('An openrouteservice API key is required');
33
+ }
34
+
35
+ if (this.options.worker) {
36
+ this.worker = new Worker(new URL('./ors.worker', import.meta.url), { type: 'module' });
37
+ this.executorAPI = wrap<OrsExecutor>(this.worker);
38
+ } else {
39
+ this.executorAPI = new OrsExecutor();
40
+ }
41
+ }
42
+
43
+ public destroy(): void {
44
+ this.worker?.terminate();
45
+ }
46
+
47
+ public request(waypoints: Waypoint[], opts: RequestOptions): Promise<OrsRoutingData> {
48
+ const url = this.buildUrl(waypoints, { ...this.options, ...opts });
49
+ const requestOptions = {
50
+ ...this.options,
51
+ ...opts,
52
+ url,
53
+ requestCoordinates: waypoints.map(({ position }) => [position.lng, position.lat] as [number, number]),
54
+ };
55
+ delete requestOptions.buildUrl;
56
+ return this.executorAPI.request(requestOptions as ExecutorRequestOptions);
57
+ }
58
+
59
+ public async abortAllRequests(): Promise<void> {
60
+ await this.executorAPI.abortAllRequests();
61
+ }
62
+
63
+ public setOption<T extends keyof Options>(key: T, value: Options[T]): void {
64
+ this.options[key] = value;
65
+ }
66
+
67
+ public async hasPendingRequests(): Promise<boolean> {
68
+ return await this.executorAPI.hasPendingRequests();
69
+ }
70
+
71
+ private buildUrl(waypoints: Waypoint[], opts: Options & RequestOptions): string {
72
+ if (waypoints.length < 2) {
73
+ throw new Error('At least two waypoints are required');
74
+ }
75
+
76
+ const profile = encodeURIComponent(opts.profile ?? 'driving-car');
77
+ const query = new URLSearchParams(
78
+ Object.entries(opts.queryParams ?? {}).map(([key, value]) => [key, String(value)]),
79
+ ).toString();
80
+ const url = `${opts.baseUrl}/${profile}${query ? `?${query}` : ''}/geojson`;
81
+ return opts.buildUrl ? opts.buildUrl({ waypoints, options: opts }, url) : url;
82
+ }
83
+ }
@@ -0,0 +1,95 @@
1
+ import type { AnyRoutingDataResponse, RequestOptions, RouteSummary, Waypoint } from '@any-routing/core';
2
+ import type { ExecutorRequestOptions } from './ors.executor';
3
+
4
+ export type OrsProfile =
5
+ | 'driving-car'
6
+ | 'driving-hgv'
7
+ | 'cycling-regular'
8
+ | 'cycling-road'
9
+ | 'cycling-mountain'
10
+ | 'cycling-electric'
11
+ | 'foot-walking'
12
+ | 'foot-hiking'
13
+ | 'wheelchair'
14
+ | 'public-transport'
15
+ | (string & {});
16
+
17
+ export type OrsPreference = 'fastest' | 'shortest' | 'recommended' | 'custom';
18
+ export type OrsUnits = 'm' | 'km' | 'mi';
19
+
20
+ export interface OrsRouteSummary extends RouteSummary {
21
+ rawRoute: OrsRawFeature;
22
+ }
23
+
24
+ export interface OrsRoutingData extends AnyRoutingDataResponse {
25
+ routes: OrsRouteSummary[];
26
+ requestOptions: ExecutorRequestOptions;
27
+ }
28
+
29
+ export interface OrsRawStep {
30
+ distance: number;
31
+ duration: number;
32
+ instruction?: string;
33
+ name?: string;
34
+ type?: number;
35
+ way_points?: [number, number];
36
+ }
37
+
38
+ export interface OrsRawSegment {
39
+ distance: number;
40
+ duration: number;
41
+ steps?: OrsRawStep[];
42
+ way_points?: [number, number];
43
+ }
44
+
45
+ export interface OrsRawFeatureProperties {
46
+ summary?: { distance?: number; duration?: number; ascent?: number; descent?: number };
47
+ segments?: OrsRawSegment[];
48
+ way_points?: [number, number];
49
+ [key: string]: unknown;
50
+ }
51
+
52
+ export interface OrsRawFeature {
53
+ type: 'Feature';
54
+ geometry: { type: 'LineString'; coordinates: [number, number][] };
55
+ properties: OrsRawFeatureProperties;
56
+ }
57
+
58
+ export interface OrsApiResponse {
59
+ type: 'FeatureCollection';
60
+ features?: OrsRawFeature[];
61
+ bbox?: number[];
62
+ metadata?: Record<string, unknown>;
63
+ }
64
+
65
+ export interface OrsApiErrorResponse {
66
+ error?: {
67
+ code?: number | string;
68
+ message?: string;
69
+ };
70
+ message?: string;
71
+ code?: number | string;
72
+ }
73
+
74
+ export type Options = {
75
+ apiKey: string;
76
+ profile?: OrsProfile;
77
+ baseUrl?: string;
78
+ worker?: boolean;
79
+ alternatives?: number;
80
+ preference?: OrsPreference;
81
+ units?: OrsUnits;
82
+ language?: string;
83
+ instructions?: boolean;
84
+ instructionsFormat?: 'text' | 'html';
85
+ maneuvers?: boolean;
86
+ geometry?: boolean;
87
+ extraInfo?: string[];
88
+ attributes?: string[];
89
+ continueStraight?: boolean;
90
+ elevation?: boolean;
91
+ options?: Record<string, unknown>;
92
+ queryParams?: Record<string, string | number | boolean>;
93
+ requestParams?: RequestInit;
94
+ buildUrl?: (ctx: { waypoints: Waypoint[]; options: Options & RequestOptions }, url: string) => string;
95
+ };
@@ -0,0 +1,136 @@
1
+ import bbox from '@turf/bbox';
2
+ import { featureCollection, lineString } from '@turf/helpers';
3
+ import type { BBox, LineString } from 'geojson';
4
+
5
+ import { Requester } from '@any-routing/core';
6
+ import type {
7
+ Options,
8
+ OrsApiErrorResponse,
9
+ OrsApiResponse,
10
+ OrsRawFeature,
11
+ OrsRouteSummary,
12
+ OrsRoutingData,
13
+ } from './ors-provider.types';
14
+
15
+ export type ExecutorRequestOptions = {
16
+ url: string;
17
+ requestCoordinates: [number, number][];
18
+ } & Options;
19
+
20
+ type ShapeProperties = { waypoint: number; routeId: number };
21
+
22
+ export class OrsExecutor {
23
+ private readonly requester = new Requester();
24
+
25
+ async request(opts: ExecutorRequestOptions): Promise<OrsRoutingData> {
26
+ const body = {
27
+ coordinates: opts.requestCoordinates,
28
+ preference: opts.preference,
29
+ units: opts.units,
30
+ language: opts.language,
31
+ instructions: opts.instructions,
32
+ instructions_format: opts.instructionsFormat,
33
+ maneuvers: opts.maneuvers,
34
+ geometry: opts.geometry,
35
+ extra_info: opts.extraInfo,
36
+ attributes: opts.attributes,
37
+ continue_straight: opts.continueStraight,
38
+ elevation: opts.elevation,
39
+ options: opts.options,
40
+ alternative_routes:
41
+ opts.alternatives
42
+ ? { target_count: opts.alternatives }
43
+ : undefined,
44
+ };
45
+ const response = (await this.requester.request(opts.url, {
46
+ ...opts.requestParams,
47
+ method: 'POST',
48
+ headers: {
49
+ ...opts.requestParams?.headers,
50
+ Accept: 'application/geo+json, application/json',
51
+ 'Content-Type': 'application/json',
52
+ Authorization: opts.apiKey,
53
+ },
54
+ body: JSON.stringify(body),
55
+ })) as OrsApiResponse & OrsApiErrorResponse;
56
+
57
+ if (response.type !== 'FeatureCollection' || !response.features?.length) {
58
+ const error = response.error;
59
+ const message = error?.message ?? response.message;
60
+ const code = error?.code ?? response.code;
61
+ throw new Error(
62
+ message
63
+ ? `openrouteservice request failed${code ? ` (${code})` : ''}: ${message}`
64
+ : 'openrouteservice returned no routes',
65
+ );
66
+ }
67
+
68
+ const routes = response.features.map((route, id) => summarizeRoute(route, id));
69
+
70
+ const features = routes.flatMap((route, fid) =>
71
+ route.shape.features.map((feature) => ({
72
+ id: fid,
73
+ ...feature,
74
+ properties: { ...feature.properties, id: fid, routeId: route.id },
75
+ })),
76
+ );
77
+ const routesShapeGeojson = featureCollection(features);
78
+
79
+ return {
80
+ routesShapeBounds: bbox(routesShapeGeojson) as BBox,
81
+ rawResponse: response,
82
+ routes,
83
+ selectedRouteId: 0,
84
+ routesShapeGeojson,
85
+ version: performance.now(),
86
+ latest: !this.requester.hasPendingRequests,
87
+ requestOptions: opts,
88
+ };
89
+ }
90
+
91
+ hasPendingRequests(): boolean {
92
+ return this.requester.hasPendingRequests;
93
+ }
94
+
95
+ abortAllRequests(): void {
96
+ this.requester.abortAllRequests();
97
+ }
98
+ }
99
+
100
+ function summarizeRoute(route: OrsRawFeature, routeId: number): OrsRouteSummary {
101
+ const now = new Date();
102
+ const summary = route.properties.summary ?? {};
103
+ const path = route.geometry.coordinates.map(([lng, lat]) => [lat, lng]);
104
+ const segments = route.properties.segments ?? [];
105
+ const shapeFeatures = segments.flatMap((segment, waypoint) => {
106
+ const [start, end] = segment.way_points ?? [0, route.geometry.coordinates.length - 1];
107
+ const coordinates = route.geometry.coordinates.slice(start, end + 1);
108
+ return coordinates.length > 1 ? [lineString(coordinates, { waypoint, routeId })] : [];
109
+ });
110
+ const shape = featureCollection<LineString, ShapeProperties>(
111
+ shapeFeatures.length
112
+ ? shapeFeatures
113
+ : [lineString(route.geometry.coordinates, { waypoint: 0, routeId })],
114
+ );
115
+ const waypointIndexes = segments.length
116
+ ? segments.flatMap((segment, index) => {
117
+ const [start, end] = segment.way_points ?? [0, route.geometry.coordinates.length - 1];
118
+ return index === segments.length - 1 ? [start, end] : [start];
119
+ })
120
+ : [0, route.geometry.coordinates.length - 1];
121
+
122
+ return {
123
+ id: routeId,
124
+ durationTime: summary.duration ?? 0,
125
+ distance: summary.distance ?? 0,
126
+ arriveTime: new Date(now.getTime() + (summary.duration ?? 0) * 1000),
127
+ departureTime: now,
128
+ path,
129
+ waypoints: waypointIndexes
130
+ .map((index) => route.geometry.coordinates[index])
131
+ .filter((point): point is [number, number] => Boolean(point))
132
+ .map(([lng, lat]) => ({ lat, lng })),
133
+ shape,
134
+ rawRoute: route,
135
+ };
136
+ }
@@ -0,0 +1,4 @@
1
+ import { expose } from 'comlink';
2
+ import { OrsExecutor } from './ors.executor';
3
+
4
+ expose(new OrsExecutor());
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "esnext",
5
+ "moduleResolution": "bundler",
6
+ "target": "es2022",
7
+ "strict": true
8
+ },
9
+ "include": ["src/**/*.ts"]
10
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "declaration": true,
5
+ "declarationMap": true,
6
+ "outDir": "../../dist/out-tsc"
7
+ },
8
+ "include": ["src/**/*.ts"]
9
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../dist/out-tsc"
5
+ },
6
+ "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
7
+ }
@@ -0,0 +1,45 @@
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/ors-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
+ build: {
21
+ outDir: '../../dist/libs/ors-data-provider',
22
+ emptyOutDir: true,
23
+ lib: {
24
+ entry: 'src/index.ts',
25
+ name: 'ors-data-provider',
26
+ formats: ['es'],
27
+ fileName: 'index',
28
+ },
29
+ rollupOptions: {
30
+ external: [],
31
+ },
32
+ },
33
+ test: {
34
+ name: 'ors-data-provider',
35
+ watch: false,
36
+ globals: true,
37
+ environment: 'node',
38
+ include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
39
+ reporters: ['default'],
40
+ coverage: {
41
+ reportsDirectory: '../../coverage/libs/ors-data-provider',
42
+ provider: 'v8',
43
+ },
44
+ },
45
+ });