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.
- package/LICENSE +20 -0
- package/README.md +125 -0
- package/android/build.gradle +110 -0
- package/android/generated/java/com/jhotadhari/reactnative/brouter/NativeBRouterSpec.java +39 -0
- package/android/generated/jni/CMakeLists.txt +28 -0
- package/android/generated/jni/RNBRouterSpec-generated.cpp +32 -0
- package/android/generated/jni/RNBRouterSpec.h +31 -0
- package/android/generated/jni/react/renderer/components/RNBRouterSpec/RNBRouterSpecJSI.h +38 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/aidl/btools/routingapp/IBRouterService.aidl +47 -0
- package/android/src/main/java/com/jhotadhari/reactnative/brouter/BRouterClient.java +205 -0
- package/android/src/main/java/com/jhotadhari/reactnative/brouter/BRouterError.java +38 -0
- package/android/src/main/java/com/jhotadhari/reactnative/brouter/BRouterModule.java +135 -0
- package/android/src/main/java/com/jhotadhari/reactnative/brouter/BRouterPackage.kt +33 -0
- package/android/src/main/java/com/jhotadhari/reactnative/brouter/BRouterServiceConnection.java +54 -0
- package/android/src/main/java/com/jhotadhari/reactnative/brouter/ParamMapper.java +174 -0
- package/android/src/test/java/com/jhotadhari/reactnative/brouter/BRouterClientTest.java +247 -0
- package/android/src/test/java/com/jhotadhari/reactnative/brouter/BRouterErrorTest.java +48 -0
- package/android/src/test/java/com/jhotadhari/reactnative/brouter/BRouterModuleTest.java +137 -0
- package/android/src/test/java/com/jhotadhari/reactnative/brouter/ParamMapperTest.java +279 -0
- package/lib/module/NativeBRouter.js +5 -0
- package/lib/module/NativeBRouter.js.map +1 -0
- package/lib/module/geojson/index.js +235 -0
- package/lib/module/geojson/index.js.map +1 -0
- package/lib/module/index.js +297 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/release.config.d.ts +11 -0
- package/lib/typescript/release.config.d.ts.map +1 -0
- package/lib/typescript/src/NativeBRouter.d.ts +9 -0
- package/lib/typescript/src/NativeBRouter.d.ts.map +1 -0
- package/lib/typescript/src/geojson/index.d.ts +122 -0
- package/lib/typescript/src/geojson/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +13 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +93 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +159 -0
- package/react-native.config.js +12 -0
- package/src/NativeBRouter.ts +8 -0
- package/src/geojson/index.ts +371 -0
- package/src/index.tsx +344 -0
- 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
|
+
}
|