react-native-brouter 0.0.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.
Files changed (48) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +125 -0
  3. package/android/build.gradle +110 -0
  4. package/android/generated/java/com/jhotadhari/reactnative/brouter/NativeBRouterSpec.java +39 -0
  5. package/android/generated/jni/CMakeLists.txt +28 -0
  6. package/android/generated/jni/RNBRouterSpec-generated.cpp +32 -0
  7. package/android/generated/jni/RNBRouterSpec.h +31 -0
  8. package/android/generated/jni/react/renderer/components/RNBRouterSpec/RNBRouterSpecJSI.h +38 -0
  9. package/android/gradle.properties +5 -0
  10. package/android/src/main/AndroidManifest.xml +3 -0
  11. package/android/src/main/AndroidManifestNew.xml +2 -0
  12. package/android/src/main/aidl/btools/routingapp/IBRouterService.aidl +47 -0
  13. package/android/src/main/java/com/jhotadhari/reactnative/brouter/BRouterClient.java +205 -0
  14. package/android/src/main/java/com/jhotadhari/reactnative/brouter/BRouterError.java +38 -0
  15. package/android/src/main/java/com/jhotadhari/reactnative/brouter/BRouterModule.java +135 -0
  16. package/android/src/main/java/com/jhotadhari/reactnative/brouter/BRouterPackage.kt +33 -0
  17. package/android/src/main/java/com/jhotadhari/reactnative/brouter/BRouterServiceConnection.java +54 -0
  18. package/android/src/main/java/com/jhotadhari/reactnative/brouter/ParamMapper.java +174 -0
  19. package/android/src/test/java/com/jhotadhari/reactnative/brouter/BRouterClientTest.java +247 -0
  20. package/android/src/test/java/com/jhotadhari/reactnative/brouter/BRouterErrorTest.java +48 -0
  21. package/android/src/test/java/com/jhotadhari/reactnative/brouter/BRouterModuleTest.java +137 -0
  22. package/android/src/test/java/com/jhotadhari/reactnative/brouter/ParamMapperTest.java +279 -0
  23. package/lib/module/NativeBRouter.js +5 -0
  24. package/lib/module/NativeBRouter.js.map +1 -0
  25. package/lib/module/geojson/index.js +235 -0
  26. package/lib/module/geojson/index.js.map +1 -0
  27. package/lib/module/index.js +297 -0
  28. package/lib/module/index.js.map +1 -0
  29. package/lib/module/package.json +1 -0
  30. package/lib/module/types.js +2 -0
  31. package/lib/module/types.js.map +1 -0
  32. package/lib/typescript/package.json +1 -0
  33. package/lib/typescript/release.config.d.ts +11 -0
  34. package/lib/typescript/release.config.d.ts.map +1 -0
  35. package/lib/typescript/src/NativeBRouter.d.ts +9 -0
  36. package/lib/typescript/src/NativeBRouter.d.ts.map +1 -0
  37. package/lib/typescript/src/geojson/index.d.ts +122 -0
  38. package/lib/typescript/src/geojson/index.d.ts.map +1 -0
  39. package/lib/typescript/src/index.d.ts +13 -0
  40. package/lib/typescript/src/index.d.ts.map +1 -0
  41. package/lib/typescript/src/types.d.ts +93 -0
  42. package/lib/typescript/src/types.d.ts.map +1 -0
  43. package/package.json +159 -0
  44. package/react-native.config.js +12 -0
  45. package/src/NativeBRouter.ts +8 -0
  46. package/src/geojson/index.ts +371 -0
  47. package/src/index.tsx +344 -0
  48. package/src/types.ts +112 -0
@@ -0,0 +1,371 @@
1
+ /**
2
+ * GeoJSON-friendly API for react-native-brouter.
3
+ *
4
+ * This layer accepts GeoJSON {@link Position} arrays for waypoints and
5
+ * nogo areas, and returns results that integrate naturally with
6
+ * `@turf/turf` and other GeoJSON tooling.
7
+ *
8
+ * ## Usage
9
+ *
10
+ * ```ts
11
+ * import { getRoute } from 'react-native-brouter/geojson';
12
+ *
13
+ * const result = await getRoute({
14
+ * waypoints: [[-71.04, -13.95], [-70.90, -13.79]],
15
+ * vehicle: 'bicycle',
16
+ * format: 'json',
17
+ * });
18
+ * // result.parsed.track → GeoJSON FeatureCollection
19
+ * // result.parsed.waypoints → FeatureCollection of waypoint points
20
+ * ```
21
+ *
22
+ * @module geojson
23
+ */
24
+
25
+ import type {
26
+ FeatureCollection,
27
+ LineString,
28
+ MultiPolygon,
29
+ Point,
30
+ Polygon,
31
+ Position,
32
+ } from 'geojson';
33
+
34
+ import { getRoute as coreGetRoute } from '../index';
35
+ import type {
36
+ BRouterError,
37
+ NogoArea,
38
+ RouteRequest,
39
+ RouteResult,
40
+ TrackFormat,
41
+ VehicleMode,
42
+ Waypoint,
43
+ } from '../types';
44
+
45
+ // Re-export GeoJSON Position so consumers don't need their own @types/geojson
46
+ export type { Position };
47
+
48
+ export type { BRouterError, RouteResult, TrackFormat, VehicleMode };
49
+
50
+ // ── GeoJSON-specific types ──────────────────────────────────────────
51
+
52
+ export interface GeoJSONRouteRequest {
53
+ /**
54
+ * Waypoints as GeoJSON positions: `[[lng, lat], [lng, lat], ...]`.
55
+ *
56
+ * Works directly with turf's `lineString()`, `along()`, etc.
57
+ * Minimum 2 waypoints required.
58
+ */
59
+ waypoints: Position[];
60
+
61
+ /** BRouter profile file name without .brf extension. */
62
+ profile?: string;
63
+
64
+ /** Raw profile content (overrides profile + vehicle + fast). */
65
+ remoteProfile?: string;
66
+
67
+ vehicle?: VehicleMode;
68
+ fast?: boolean;
69
+ format?: TrackFormat;
70
+ alternativeIndex?: 0 | 1 | 2 | 3;
71
+
72
+ /**
73
+ * Nogo areas as point+radius circles.
74
+ *
75
+ * For polygon-based nogos, use {@link polygonToNogoAreas} to convert
76
+ * GeoJSON Polygon / MultiPolygon geometries to the nogo format.
77
+ */
78
+ nogos?: NogoArea[];
79
+
80
+ exportWaypoints?: boolean;
81
+ heading?: number;
82
+ direction?: number;
83
+ elevation?: boolean;
84
+ maxRunningTime?: number;
85
+ connectTimeout?: number;
86
+ pathToFileResult?: string;
87
+ acceptCompressedResult?: boolean;
88
+ extraParams?: Record<string, string>;
89
+ }
90
+
91
+ export interface RouteSummary {
92
+ /** Total distance in meters (available when format is 'json'). */
93
+ totalDistanceMeters?: number;
94
+ /** Total duration in seconds (available when format is 'json'). */
95
+ totalDurationSeconds?: number;
96
+ /** Ascent in meters (available when format is 'json'). */
97
+ ascentMeters?: number;
98
+ /** Descent in meters (available when format is 'json'). */
99
+ descentMeters?: number;
100
+ /** Number of track points. */
101
+ trackPointCount?: number;
102
+ }
103
+
104
+ export interface GeoJSONRouteResult {
105
+ /** The raw track string (GPX, KML, or JSON). */
106
+ raw: string;
107
+
108
+ /** The format of the returned track. */
109
+ format: TrackFormat;
110
+
111
+ /**
112
+ * Parsed GeoJSON result (only populated when format is 'json').
113
+ *
114
+ * `track` is a FeatureCollection containing the route geometry as a
115
+ * LineString feature. `waypoints` contains the input waypoints as
116
+ * Point features. `summary` holds extracted statistics.
117
+ */
118
+ parsed?: {
119
+ track: FeatureCollection<LineString>;
120
+ waypoints: FeatureCollection<Point>;
121
+ summary: RouteSummary;
122
+ };
123
+ }
124
+
125
+ // ── Helpers ─────────────────────────────────────────────────────────
126
+
127
+ /**
128
+ * Parse a BRouter JSON track result into a typed GeoJSON structure.
129
+ *
130
+ * BRouter's JSON output format is a plain array of feature objects
131
+ * (not a proper FeatureCollection). This normalizes it.
132
+ */
133
+ function parseJsonTrack(
134
+ raw: string,
135
+ waypoints: Position[]
136
+ ): {
137
+ track: FeatureCollection<LineString>;
138
+ waypoints: FeatureCollection<Point>;
139
+ summary: RouteSummary;
140
+ } {
141
+ const parsed = JSON.parse(raw) as unknown;
142
+
143
+ // BRouter may return a bare array of features or a FeatureCollection
144
+ // wrapper. Handle both shapes.
145
+ let features: Array<unknown>;
146
+ if (Array.isArray(parsed)) {
147
+ features = parsed;
148
+ } else if (
149
+ parsed != null &&
150
+ typeof parsed === 'object' &&
151
+ 'features' in parsed &&
152
+ Array.isArray((parsed as Record<string, unknown>).features)
153
+ ) {
154
+ features = (parsed as Record<string, unknown>)
155
+ .features as Array<unknown>;
156
+ } else {
157
+ throw new Error('Unexpected BRouter JSON shape: ' + typeof parsed);
158
+ }
159
+
160
+ // Separate track features from waypoint features
161
+ const trackFeatures: Array<
162
+ FeatureCollection<LineString>['features'][number]
163
+ > = [];
164
+ let totalDistanceMeters: number | undefined;
165
+ let totalDurationSeconds: number | undefined;
166
+ let ascentMeters: number | undefined;
167
+ let descentMeters: number | undefined;
168
+ let trackPointCount = 0;
169
+
170
+ for (let fi = 0; fi < features.length; fi++) {
171
+ const feature = features[fi] as {
172
+ type: 'Feature';
173
+ geometry: {
174
+ type: string;
175
+ coordinates: unknown;
176
+ };
177
+ properties: Record<string, unknown>;
178
+ };
179
+ if (feature.geometry.type === 'LineString') {
180
+ const coords = feature.geometry.coordinates as Position[];
181
+ trackFeatures.push({
182
+ type: 'Feature',
183
+ geometry: {
184
+ type: 'LineString',
185
+ coordinates: coords,
186
+ },
187
+ properties: feature.properties,
188
+ });
189
+ trackPointCount += coords.length;
190
+
191
+ // Extract summary from feature properties
192
+ if (feature.properties['track-length'] != null) {
193
+ totalDistanceMeters = Number(
194
+ feature.properties['track-length']
195
+ );
196
+ }
197
+ if (feature.properties['total-time'] != null) {
198
+ totalDurationSeconds = Number(feature.properties['total-time']);
199
+ }
200
+ if (feature.properties['filtered ascend'] != null) {
201
+ ascentMeters = Number(feature.properties['filtered ascend']);
202
+ }
203
+ if (feature.properties['plain-descent'] != null) {
204
+ descentMeters = Number(feature.properties['plain-descent']);
205
+ }
206
+ }
207
+ }
208
+
209
+ const waypointFeatures: FeatureCollection<Point>['features'] =
210
+ waypoints.map((pos, i) => ({
211
+ type: 'Feature' as const,
212
+ geometry: {
213
+ type: 'Point' as const,
214
+ coordinates: pos,
215
+ },
216
+ properties: { index: i },
217
+ }));
218
+
219
+ return {
220
+ track: {
221
+ type: 'FeatureCollection',
222
+ features: trackFeatures,
223
+ },
224
+ waypoints: {
225
+ type: 'FeatureCollection',
226
+ features: waypointFeatures,
227
+ },
228
+ summary: {
229
+ totalDistanceMeters,
230
+ totalDurationSeconds,
231
+ ascentMeters,
232
+ descentMeters,
233
+ trackPointCount,
234
+ },
235
+ };
236
+ }
237
+
238
+ // ── Public API ──────────────────────────────────────────────────────
239
+
240
+ /**
241
+ * Request a route from the BRouter Android app using GeoJSON types.
242
+ *
243
+ * Converts GeoJSON {@link Position} arrays to the internal format,
244
+ * calls the native BRouter service, and returns a result that
245
+ * includes parsed GeoJSON structures when `format` is `'json'`.
246
+ *
247
+ * @throws {BRouterError} if the request is invalid or BRouter fails.
248
+ */
249
+ export async function getRoute(
250
+ request: GeoJSONRouteRequest
251
+ ): Promise<GeoJSONRouteResult> {
252
+ // Convert GeoJSON positions to core Waypoint[]
253
+ const coreWaypoints: Waypoint[] = request.waypoints.map((pos) => ({
254
+ position: [pos[0]!, pos[1]!] as [number, number],
255
+ }));
256
+
257
+ const coreRequest: RouteRequest = {
258
+ waypoints: coreWaypoints,
259
+ profile: request.profile,
260
+ remoteProfile: request.remoteProfile,
261
+ vehicle: request.vehicle,
262
+ fast: request.fast,
263
+ format: request.format,
264
+ alternativeIndex: request.alternativeIndex,
265
+ nogos: request.nogos,
266
+ exportWaypoints: request.exportWaypoints,
267
+ heading: request.heading,
268
+ direction: request.direction,
269
+ elevation: request.elevation,
270
+ maxRunningTime: request.maxRunningTime,
271
+ connectTimeout: request.connectTimeout,
272
+ pathToFileResult: request.pathToFileResult,
273
+ acceptCompressedResult: request.acceptCompressedResult,
274
+ extraParams: request.extraParams,
275
+ };
276
+
277
+ const result: RouteResult = await coreGetRoute(coreRequest);
278
+
279
+ const geoResult: GeoJSONRouteResult = {
280
+ raw: result.raw,
281
+ format: result.format,
282
+ };
283
+
284
+ // Parse JSON result into typed GeoJSON
285
+ if (result.format === 'json') {
286
+ try {
287
+ geoResult.parsed = parseJsonTrack(result.raw, request.waypoints);
288
+ } catch (e: unknown) {
289
+ console.warn(
290
+ 'Failed to parse BRouter JSON track:',
291
+ e instanceof Error ? e.message : String(e)
292
+ );
293
+ }
294
+ }
295
+
296
+ return geoResult;
297
+ }
298
+
299
+ /**
300
+ * Convert a GeoJSON {@link Polygon} or {@link MultiPolygon} into an array
301
+ * of {@link NogoArea} entries suitable for use in
302
+ * {@link GeoJSONRouteRequest.nogos}.
303
+ *
304
+ * Uses a bounding-circle approximation: the polygon's centroid becomes
305
+ * the nogo center, and the farthest vertex distance becomes the radius.
306
+ *
307
+ * This is a utility for consumers — it is not called automatically.
308
+ *
309
+ * ## Usage with turf
310
+ *
311
+ * ```ts
312
+ * import { polygonToNogoAreas } from 'react-native-brouter/geojson';
313
+ * import turfBboxPolygon from '@turf/bbox-polygon';
314
+ *
315
+ * const bboxPoly = turfBboxPolygon([lng1, lat1, lng2, lat2]);
316
+ * const nogos = polygonToNogoAreas(bboxPoly.geometry);
317
+ * ```
318
+ */
319
+ export function polygonToNogoAreas(
320
+ geometry: Polygon | MultiPolygon,
321
+ weight?: number
322
+ ): NogoArea[] {
323
+ const result: NogoArea[] = [];
324
+
325
+ // Normalize to an array of polygon rings
326
+ const polygons: Position[][][] =
327
+ geometry.type === 'Polygon'
328
+ ? [geometry.coordinates]
329
+ : geometry.coordinates;
330
+
331
+ for (let pi = 0; pi < polygons.length; pi++) {
332
+ const rings = polygons[pi]!;
333
+ const outerRing = rings[0];
334
+ if (!outerRing || outerRing.length === 0) {
335
+ continue;
336
+ }
337
+
338
+ // Compute centroid (average of all vertices)
339
+ let sumLng = 0;
340
+ let sumLat = 0;
341
+ for (let oi = 0; oi < outerRing.length; oi++) {
342
+ const pos = outerRing[oi]!;
343
+ sumLng += pos[0]!;
344
+ sumLat += pos[1]!;
345
+ }
346
+ const centerLng = sumLng / outerRing.length;
347
+ const centerLat = sumLat / outerRing.length;
348
+
349
+ // Compute max distance from centroid to any vertex as radius
350
+ let maxDist = 0;
351
+ for (let oi = 0; oi < outerRing.length; oi++) {
352
+ const pos = outerRing[oi]!;
353
+ const dLng =
354
+ (pos[0]! - centerLng) * Math.cos((centerLat * Math.PI) / 180);
355
+ const dLat = pos[1]! - centerLat;
356
+ // Approximate distance in meters (1 degree ≈ 111,320 m)
357
+ const distMeters = Math.sqrt(dLng * dLng + dLat * dLat) * 111320;
358
+ if (distMeters > maxDist) {
359
+ maxDist = distMeters;
360
+ }
361
+ }
362
+
363
+ result.push({
364
+ position: [centerLng, centerLat],
365
+ radiusMeters: Math.ceil(maxDist),
366
+ weight,
367
+ });
368
+ }
369
+
370
+ return result;
371
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,344 @@
1
+ import NativeBRouter from './NativeBRouter';
2
+ import type {
3
+ BRouterError,
4
+ RouteRequest,
5
+ RouteResult,
6
+ TrackFormat,
7
+ } from './types';
8
+
9
+ export type {
10
+ Position,
11
+ Waypoint,
12
+ NogoArea,
13
+ Polyline,
14
+ Polygon,
15
+ Poi,
16
+ VehicleMode,
17
+ TrackFormat,
18
+ TurnInstructionFormat,
19
+ TurnInstructionMode,
20
+ RouteRequest,
21
+ RouteResult,
22
+ BRouterError,
23
+ } from './types';
24
+
25
+ const TURN_INSTRUCTION_MODE_MAP: Record<string, number> = {
26
+ none: 0,
27
+ 'auto-choose': 1,
28
+ locus: 2,
29
+ osmand: 3,
30
+ comment: 4,
31
+ gpsies: 5,
32
+ orux: 6,
33
+ 'locus-old': 7,
34
+ };
35
+
36
+ /**
37
+ * Serialize waypoints into the BRouter wire format.
38
+ *
39
+ * Produces three AIDL keys:
40
+ * - `lonlats`: `lng,lat[,name][,d]|...` pipe-delimited string
41
+ * - `lats`: double[] of latitudes
42
+ * - `lons`: double[] of longitudes
43
+ * - `straight`: comma-separated indices of direct-routing waypoints (if any)
44
+ */
45
+ function serializeWaypoints(
46
+ waypoints: RouteRequest['waypoints']
47
+ ): Record<string, unknown> {
48
+ const parts: string[] = [];
49
+ const lats: number[] = [];
50
+ const lons: number[] = [];
51
+ const directIndices: number[] = [];
52
+
53
+ for (let i = 0; i < waypoints.length; i++) {
54
+ const wp = waypoints[i]!;
55
+ const lng = wp.position[0] as number;
56
+ const lat = wp.position[1] as number;
57
+
58
+ let part = `${lng},${lat}`;
59
+ if (wp.name) {
60
+ part += `,${wp.name}`;
61
+ }
62
+ if (wp.direct) {
63
+ part += ',d';
64
+ directIndices.push(i);
65
+ }
66
+
67
+ parts.push(part);
68
+ lats.push(lat);
69
+ lons.push(lng);
70
+ }
71
+
72
+ const result: Record<string, unknown> = {
73
+ lonlats: parts.join('|'),
74
+ lats,
75
+ lons,
76
+ };
77
+
78
+ if (directIndices.length > 0) {
79
+ result.straight = directIndices.join(',');
80
+ }
81
+
82
+ return result;
83
+ }
84
+
85
+ /**
86
+ * Serialize nogo areas into the BRouter wire format.
87
+ *
88
+ * Produces AIDL keys:
89
+ * - `nogos`: `lng,lat,radius[,weight]|...` pipe-delimited string
90
+ * - `nogoLats`: double[] of nogo center latitudes
91
+ * - `nogoLons`: double[] of nogo center longitudes
92
+ * - `nogoRadi`: double[] of nogo radii
93
+ */
94
+ function serializeNogos(nogos: RouteRequest['nogos']): Record<string, unknown> {
95
+ if (!nogos || nogos.length === 0) {
96
+ return {};
97
+ }
98
+
99
+ const parts: string[] = [];
100
+ const nogoLats: number[] = [];
101
+ const nogoLons: number[] = [];
102
+ const nogoRadi: number[] = [];
103
+
104
+ for (let ni = 0; ni < nogos.length; ni++) {
105
+ const nogo = nogos[ni]!;
106
+ const lng = nogo.position[0] as number;
107
+ const lat = nogo.position[1] as number;
108
+ let part = `${lng},${lat},${nogo.radiusMeters}`;
109
+ if (nogo.weight !== undefined) {
110
+ part += `,${nogo.weight}`;
111
+ }
112
+
113
+ parts.push(part);
114
+ nogoLats.push(lat);
115
+ nogoLons.push(lng);
116
+ nogoRadi.push(nogo.radiusMeters);
117
+ }
118
+
119
+ return {
120
+ nogos: parts.join('|'),
121
+ nogoLats,
122
+ nogoLons,
123
+ nogoRadi,
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Serialize polylines into the BRouter wire format.
129
+ *
130
+ * Produces: `lng,lat,lng,lat,...,weight|lng,lat,...|...`
131
+ */
132
+ function serializePolylines(
133
+ polylines: RouteRequest['polylines']
134
+ ): string | undefined {
135
+ if (!polylines || polylines.length === 0) {
136
+ return undefined;
137
+ }
138
+
139
+ return polylines
140
+ .map((pl) => {
141
+ const coords = pl.positions.map((p) => `${p[0]},${p[1]}`).join(',');
142
+ return pl.weight !== undefined ? `${coords},${pl.weight}` : coords;
143
+ })
144
+ .join('|');
145
+ }
146
+
147
+ /**
148
+ * Serialize polygons into the BRouter wire format (same as polylines).
149
+ */
150
+ function serializePolygons(
151
+ polygons: RouteRequest['polygons']
152
+ ): string | undefined {
153
+ if (!polygons || polygons.length === 0) {
154
+ return undefined;
155
+ }
156
+
157
+ return polygons
158
+ .map((pg) => {
159
+ const coords = pg.positions.map((p) => `${p[0]},${p[1]}`).join(',');
160
+ return pg.weight !== undefined ? `${coords},${pg.weight}` : coords;
161
+ })
162
+ .join('|');
163
+ }
164
+
165
+ /**
166
+ * Serialize pois into the BRouter wire format.
167
+ *
168
+ * Produces: `lng,lat,name|lng,lat,name|...`
169
+ */
170
+ function serializePois(pois: RouteRequest['pois']): string | undefined {
171
+ if (!pois || pois.length === 0) {
172
+ return undefined;
173
+ }
174
+
175
+ return pois
176
+ .map((poi) => `${poi.position[0]},${poi.position[1]},${poi.name}`)
177
+ .join('|');
178
+ }
179
+
180
+ /**
181
+ * Serialize a RouteRequest into the flat key-value map expected by the
182
+ * Android native module's ParamMapper.
183
+ */
184
+ function serializeParams(
185
+ request: RouteRequest
186
+ ): Readonly<Record<string, unknown>> {
187
+ const params: Record<string, unknown> = {};
188
+
189
+ // Waypoints (required, already validated)
190
+ Object.assign(params, serializeWaypoints(request.waypoints));
191
+
192
+ // Profile
193
+ if (request.profile) {
194
+ // Strip .brf extension if present
195
+ params.profile = request.profile.replace(/\.brf$/, '');
196
+ }
197
+ if (request.remoteProfile) {
198
+ params.remoteProfile = request.remoteProfile;
199
+ }
200
+
201
+ // Vehicle mode
202
+ if (request.vehicle) {
203
+ params.v = request.vehicle;
204
+ }
205
+
206
+ // Fast mode
207
+ if (request.fast !== undefined) {
208
+ params.fast = request.fast ? 1 : 0;
209
+ }
210
+
211
+ // Track format
212
+ if (request.format) {
213
+ params.trackFormat = request.format;
214
+ }
215
+
216
+ // Alternative index
217
+ if (request.alternativeIndex !== undefined) {
218
+ params.alternativeidx = request.alternativeIndex;
219
+ }
220
+
221
+ // Export waypoints
222
+ if (request.exportWaypoints) {
223
+ params.exportWaypoints = 1;
224
+ }
225
+
226
+ // Turn instruction format
227
+ if (request.turnInstructionFormat) {
228
+ params.turnInstructionFormat = request.turnInstructionFormat;
229
+ }
230
+
231
+ // Turn instruction mode (enum → int)
232
+ if (request.turnInstructionMode) {
233
+ const mode = TURN_INSTRUCTION_MODE_MAP[request.turnInstructionMode];
234
+ if (mode !== undefined) {
235
+ params.timode = mode;
236
+ }
237
+ }
238
+
239
+ // Heading / direction
240
+ if (request.heading !== undefined) {
241
+ params.heading = request.heading;
242
+ }
243
+ if (request.direction !== undefined) {
244
+ params.direction = request.direction;
245
+ }
246
+
247
+ // Elevation (engineMode)
248
+ if (request.elevation) {
249
+ params.engineMode = 2;
250
+ }
251
+
252
+ // Max running time
253
+ if (request.maxRunningTime !== undefined) {
254
+ params.maxRunningTime = request.maxRunningTime;
255
+ }
256
+
257
+ // Connection timeout (not AIDL, used by BRouterClient)
258
+ if (request.connectTimeout !== undefined) {
259
+ params.connectTimeout = request.connectTimeout;
260
+ }
261
+
262
+ // File output
263
+ if (request.pathToFileResult) {
264
+ params.pathToFileResult = request.pathToFileResult;
265
+ }
266
+
267
+ // Compression
268
+ if (request.acceptCompressedResult) {
269
+ params.acceptCompressedResult = true;
270
+ }
271
+
272
+ // Extra params
273
+ if (request.extraParams) {
274
+ params.extraParams = request.extraParams;
275
+ }
276
+
277
+ // Nogo areas
278
+ if (request.nogos && request.nogos.length > 0) {
279
+ Object.assign(params, serializeNogos(request.nogos));
280
+ }
281
+
282
+ // Polylines
283
+ const polylinesStr = serializePolylines(request.polylines);
284
+ if (polylinesStr) {
285
+ params.polylines = polylinesStr;
286
+ }
287
+
288
+ // Polygons
289
+ const polygonsStr = serializePolygons(request.polygons);
290
+ if (polygonsStr) {
291
+ params.polygons = polygonsStr;
292
+ }
293
+
294
+ // POIs
295
+ const poisStr = serializePois(request.pois);
296
+ if (poisStr) {
297
+ params.pois = poisStr;
298
+ }
299
+
300
+ return params;
301
+ }
302
+
303
+ /**
304
+ * Extract a structured error from a native module rejection.
305
+ *
306
+ * Native rejections come through as `{ code: string, message: string }`
307
+ * from {@code Promise.reject(code, message)}.
308
+ */
309
+ function normalizeError(e: unknown): BRouterError {
310
+ const err = e as Record<string, unknown> | undefined;
311
+ return {
312
+ code: (err?.code as string) ?? 'UNKNOWN',
313
+ message: (err?.message as string) ?? String(e),
314
+ };
315
+ }
316
+
317
+ /**
318
+ * Request a route from the BRouter Android app.
319
+ *
320
+ * This is the core API entry point. It validates the request, serializes
321
+ * it into the AIDL wire format, calls the native module, and returns a
322
+ * typed {@link RouteResult}.
323
+ *
324
+ * @throws {BRouterError} if the request is invalid or BRouter fails.
325
+ */
326
+ export async function getRoute(request: RouteRequest): Promise<RouteResult> {
327
+ // Validate
328
+ if (!request.waypoints || request.waypoints.length < 2) {
329
+ throw {
330
+ code: 'INVALID_PARAMS',
331
+ message: 'At least 2 waypoints are required',
332
+ } satisfies BRouterError;
333
+ }
334
+
335
+ const format: TrackFormat = request.format ?? 'gpx';
336
+ const params = serializeParams(request);
337
+
338
+ try {
339
+ const raw = await NativeBRouter.getRoute(params);
340
+ return { raw, format };
341
+ } catch (e: unknown) {
342
+ throw normalizeError(e);
343
+ }
344
+ }