Add interactive map support for bike and walk routes
This commit is contained in:
@@ -28,6 +28,7 @@
|
|||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
|
"react-native-maps": "^1.20.0",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-svg": "^15.15.5"
|
"react-native-svg": "^15.15.5"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
|
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome';
|
||||||
import { faClock, faRuler, faMap } from '@fortawesome/free-solid-svg-icons';
|
import { faClock, faRuler } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { formatDuration, formatDistance } from '@timetoleave/core';
|
import { formatDuration, formatDistance } from '@timetoleave/core';
|
||||||
import type { BikeRoute, Station } from '@timetoleave/core';
|
import type { BikeRoute, Station } from '@timetoleave/core';
|
||||||
import type { AppColors } from '../hooks/useColors';
|
import type { AppColors } from '../hooks/useColors';
|
||||||
|
import { RouteMap } from './RouteMap';
|
||||||
|
|
||||||
/** Props for the bike route information section shown on event detail. */
|
/** Props for the bike route information section shown on event detail. */
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -14,7 +15,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays cycling route duration, distance, and a map placeholder for the
|
* Displays cycling route duration, distance, and an interactive map for the
|
||||||
* event's origin → destination leg. Shows contextual empty states when no
|
* event's origin → destination leg. Shows contextual empty states when no
|
||||||
* origin is set or no route is available.
|
* origin is set or no route is available.
|
||||||
*/
|
*/
|
||||||
@@ -44,12 +45,9 @@ export function BikeSection({ bikeRoute, loading, origin, colors }: Props) {
|
|||||||
</View>
|
</View>
|
||||||
<Text style={[styles.value, { color: colors.accent }]}>{formatDistance(bikeRoute.distance)}</Text>
|
<Text style={[styles.value, { color: colors.accent }]}>{formatDistance(bikeRoute.distance)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.mapPlaceholder, { backgroundColor: colors.background, borderColor: colors.border }]}>
|
{bikeRoute.geometry && (
|
||||||
<View style={styles.mapTextRow}>
|
<RouteMap geometry={bikeRoute.geometry} colors={colors} mode="bike" />
|
||||||
<FontAwesomeIcon icon={faMap} size={13} color={colors.subtext} />
|
)}
|
||||||
<Text style={[styles.mapText, { color: colors.subtext }]}>Map (post-MVP)</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<Text style={[styles.empty, { color: colors.subtext }]}>
|
<Text style={[styles.empty, { color: colors.subtext }]}>
|
||||||
@@ -71,14 +69,4 @@ const styles = StyleSheet.create({
|
|||||||
labelRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
labelRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
||||||
label: { fontSize: 15, fontWeight: '500' },
|
label: { fontSize: 15, fontWeight: '500' },
|
||||||
value: { fontSize: 15, fontWeight: '600' },
|
value: { fontSize: 15, fontWeight: '600' },
|
||||||
mapTextRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
|
||||||
mapPlaceholder: {
|
|
||||||
marginTop: 10,
|
|
||||||
height: 100,
|
|
||||||
borderRadius: 8,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
borderWidth: 1,
|
|
||||||
},
|
|
||||||
mapText: { fontSize: 14 },
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'rea
|
|||||||
import { formatDuration, formatDistance, rankJourneys } from '@timetoleave/core';
|
import { formatDuration, formatDistance, rankJourneys } from '@timetoleave/core';
|
||||||
import type { Journey, Station, WalkRoute } from '@timetoleave/core';
|
import type { Journey, Station, WalkRoute } from '@timetoleave/core';
|
||||||
import type { AppColors } from '../hooks/useColors';
|
import type { AppColors } from '../hooks/useColors';
|
||||||
|
import { RouteMap } from './RouteMap';
|
||||||
|
|
||||||
/** Props for the train journey list and optional final walk leg. */
|
/** Props for the train journey list and optional final walk leg. */
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -132,6 +133,9 @@ export function JourneyList({
|
|||||||
<Text style={[styles.walkLabel, { color: colors.text }]}>Distance</Text>
|
<Text style={[styles.walkLabel, { color: colors.text }]}>Distance</Text>
|
||||||
<Text style={[styles.walkValue, { color: colors.accent }]}>{formatDistance(walkRoute.distance)}</Text>
|
<Text style={[styles.walkValue, { color: colors.accent }]}>{formatDistance(walkRoute.distance)}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
{walkRoute.geometry && (
|
||||||
|
<RouteMap geometry={walkRoute.geometry} colors={colors} mode="walk" />
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { StyleSheet, View } from 'react-native';
|
||||||
|
import MapView, { Marker, Polyline } from 'react-native-maps';
|
||||||
|
import { decodePolyline } from '../utils/polyline';
|
||||||
|
import type { AppColors } from '../hooks/useColors';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
geometry: string;
|
||||||
|
colors: AppColors;
|
||||||
|
mode: 'bike' | 'walk';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRegionFromCoordinates(
|
||||||
|
coords: Array<{ latitude: number; longitude: number }>,
|
||||||
|
) {
|
||||||
|
let minLat = Infinity;
|
||||||
|
let maxLat = -Infinity;
|
||||||
|
let minLng = Infinity;
|
||||||
|
let maxLng = -Infinity;
|
||||||
|
|
||||||
|
for (const c of coords) {
|
||||||
|
minLat = Math.min(minLat, c.latitude);
|
||||||
|
maxLat = Math.max(maxLat, c.latitude);
|
||||||
|
minLng = Math.min(minLng, c.longitude);
|
||||||
|
maxLng = Math.max(maxLng, c.longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
const latDelta = (maxLat - minLat) * 1.3; // 30 % padding
|
||||||
|
const lngDelta = (maxLng - minLng) * 1.3;
|
||||||
|
|
||||||
|
return {
|
||||||
|
latitude: (minLat + maxLat) / 2,
|
||||||
|
longitude: (minLng + maxLng) / 2,
|
||||||
|
latitudeDelta: Math.max(latDelta, 0.005),
|
||||||
|
longitudeDelta: Math.max(lngDelta, 0.005),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders an interactive map with the decoded route polyline and start/end
|
||||||
|
* markers. Automatically centres and zooms to fit the entire route.
|
||||||
|
*/
|
||||||
|
export function RouteMap({ geometry, colors, mode }: Props) {
|
||||||
|
const coords = useMemo(() => decodePolyline(geometry), [geometry]);
|
||||||
|
|
||||||
|
const region = useMemo(() => {
|
||||||
|
if (coords.length < 2) return null;
|
||||||
|
return getRegionFromCoordinates(coords);
|
||||||
|
}, [coords]);
|
||||||
|
|
||||||
|
if (!region || coords.length < 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strokeColor = mode === 'bike' ? colors.accent : colors.success;
|
||||||
|
const startCoord = coords[0];
|
||||||
|
const endCoord = coords[coords.length - 1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, { borderColor: colors.border }]}>
|
||||||
|
<MapView style={styles.map} initialRegion={region} scrollEnabled={false} zoomEnabled={false} rotateEnabled={false} pitchEnabled={false}>
|
||||||
|
<Polyline
|
||||||
|
coordinates={coords}
|
||||||
|
strokeColor={strokeColor}
|
||||||
|
strokeWidth={4}
|
||||||
|
/>
|
||||||
|
<Marker coordinate={startCoord} pinColor={colors.accent} />
|
||||||
|
<Marker coordinate={endCoord} pinColor={colors.success} />
|
||||||
|
</MapView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginTop: 10,
|
||||||
|
height: 220,
|
||||||
|
borderRadius: 8,
|
||||||
|
overflow: 'hidden',
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
map: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Decode a Google polyline string into an array of { latitude, longitude } points.
|
||||||
|
*
|
||||||
|
* Based on the polyline algorithm:
|
||||||
|
* https://developers.google.com/maps/documentation/utilities/polylinealgorithm
|
||||||
|
*/
|
||||||
|
export function decodePolyline(encoded: string): Array<{ latitude: number; longitude: number }> {
|
||||||
|
const points: Array<{ latitude: number; longitude: number }> = [];
|
||||||
|
let index = 0;
|
||||||
|
let lat = 0;
|
||||||
|
let lng = 0;
|
||||||
|
|
||||||
|
while (index < encoded.length) {
|
||||||
|
let b;
|
||||||
|
let shift = 0;
|
||||||
|
let result = 0;
|
||||||
|
|
||||||
|
// Decode latitude
|
||||||
|
do {
|
||||||
|
b = encoded.charCodeAt(index++) - 63;
|
||||||
|
result |= (b & 0x1f) << shift;
|
||||||
|
shift += 5;
|
||||||
|
} while (b >= 0x20);
|
||||||
|
const dlat = (result & 1) !== 0 ? ~(result >> 1) : result >> 1;
|
||||||
|
lat += dlat;
|
||||||
|
|
||||||
|
shift = 0;
|
||||||
|
result = 0;
|
||||||
|
|
||||||
|
// Decode longitude
|
||||||
|
do {
|
||||||
|
b = encoded.charCodeAt(index++) - 63;
|
||||||
|
result |= (b & 0x1f) << shift;
|
||||||
|
shift += 5;
|
||||||
|
} while (b >= 0x20);
|
||||||
|
const dlng = (result & 1) !== 0 ? ~(result >> 1) : result >> 1;
|
||||||
|
lng += dlng;
|
||||||
|
|
||||||
|
points.push({
|
||||||
|
latitude: lat / 1e5,
|
||||||
|
longitude: lng / 1e5,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ describe("api/bike-route/route", () => {
|
|||||||
mockGetBikeRoute.mockResolvedValue({
|
mockGetBikeRoute.mockResolvedValue({
|
||||||
distance: 1500,
|
distance: 1500,
|
||||||
duration: 300,
|
duration: 300,
|
||||||
|
geometry: "_p~iF~ps|U_ulLnnqC_mqNvxq`@",
|
||||||
steps: [
|
steps: [
|
||||||
{ name: "Start", distance: 100, duration: 10, instruction: "Go straight" },
|
{ name: "Start", distance: 100, duration: 10, instruction: "Go straight" },
|
||||||
{ name: "Turn left", distance: 200, duration: 20, instruction: "Turn left at the corner" },
|
{ name: "Turn left", distance: 200, duration: 20, instruction: "Turn left at the corner" },
|
||||||
@@ -45,6 +46,7 @@ describe("api/bike-route/route", () => {
|
|||||||
expect(data).toHaveProperty("distance");
|
expect(data).toHaveProperty("distance");
|
||||||
expect(data).toHaveProperty("duration");
|
expect(data).toHaveProperty("duration");
|
||||||
expect(data).toHaveProperty("steps");
|
expect(data).toHaveProperty("steps");
|
||||||
|
expect(data).toHaveProperty("geometry");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle client error", async () => {
|
it("should handle client error", async () => {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ describe("api/walk-route/route", () => {
|
|||||||
mockGetWalkRoute.mockResolvedValue({
|
mockGetWalkRoute.mockResolvedValue({
|
||||||
distance: 800,
|
distance: 800,
|
||||||
duration: 600,
|
duration: 600,
|
||||||
|
geometry: "_p~iF~ps|U_ulLnnqC_mqNvxq`@",
|
||||||
steps: [
|
steps: [
|
||||||
{ name: "Start", distance: 50, duration: 40, instruction: "Head north" },
|
{ name: "Start", distance: 50, duration: 40, instruction: "Head north" },
|
||||||
{ name: "Main St", distance: 150, duration: 120, instruction: "Turn right" },
|
{ name: "Main St", distance: 150, duration: 120, instruction: "Turn right" },
|
||||||
@@ -45,6 +46,7 @@ describe("api/walk-route/route", () => {
|
|||||||
expect(data).toHaveProperty("distance");
|
expect(data).toHaveProperty("distance");
|
||||||
expect(data).toHaveProperty("duration");
|
expect(data).toHaveProperty("duration");
|
||||||
expect(data).toHaveProperty("steps");
|
expect(data).toHaveProperty("steps");
|
||||||
|
expect(data).toHaveProperty("geometry");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle client error", async () => {
|
it("should handle client error", async () => {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ interface OsrmRoute {
|
|||||||
distance: number;
|
distance: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
legs: Array<{ steps: OsrmStep[] }>;
|
legs: Array<{ steps: OsrmStep[] }>;
|
||||||
|
geometry: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OsrmResponse {
|
interface OsrmResponse {
|
||||||
@@ -66,7 +67,7 @@ export class BikeRoutingClient {
|
|||||||
|
|
||||||
const res = await this.client.get<OsrmResponse>(
|
const res = await this.client.get<OsrmResponse>(
|
||||||
path,
|
path,
|
||||||
{ overview: "false", steps: "true" },
|
{ overview: "full", steps: "true", geometries: "polyline" },
|
||||||
{
|
{
|
||||||
cacheKey,
|
cacheKey,
|
||||||
ttl: this.defaultTtlMs,
|
ttl: this.defaultTtlMs,
|
||||||
@@ -87,6 +88,7 @@ export class BikeRoutingClient {
|
|||||||
distance: route.distance,
|
distance: route.distance,
|
||||||
duration: route.duration,
|
duration: route.duration,
|
||||||
steps,
|
steps,
|
||||||
|
geometry: route.geometry,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ interface OsrmRoute {
|
|||||||
distance: number;
|
distance: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
legs: Array<{ steps: OsrmStep[] }>;
|
legs: Array<{ steps: OsrmStep[] }>;
|
||||||
|
geometry: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OsrmResponse {
|
interface OsrmResponse {
|
||||||
@@ -68,7 +69,7 @@ export class WalkRoutingClient {
|
|||||||
|
|
||||||
const res = await this.client.get<OsrmResponse>(
|
const res = await this.client.get<OsrmResponse>(
|
||||||
path,
|
path,
|
||||||
{ overview: "false", steps: "true" },
|
{ overview: "full", steps: "true", geometries: "polyline" },
|
||||||
{
|
{
|
||||||
cacheKey,
|
cacheKey,
|
||||||
ttl: this.defaultTtlMs,
|
ttl: this.defaultTtlMs,
|
||||||
@@ -90,6 +91,7 @@ export class WalkRoutingClient {
|
|||||||
distance: route.distance,
|
distance: route.distance,
|
||||||
duration: Math.max(route.duration, walkingDuration),
|
duration: Math.max(route.duration, walkingDuration),
|
||||||
steps,
|
steps,
|
||||||
|
geometry: route.geometry,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+36
-1
@@ -11,6 +11,11 @@
|
|||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"expo": "~54.0.34",
|
||||||
|
"react": "19.1.0",
|
||||||
|
"react-native": "0.81.5"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^17.0.5"
|
"lint-staged": "^17.0.5"
|
||||||
@@ -35,6 +40,7 @@
|
|||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
|
"react-native-maps": "^1.20.0",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-svg": "^15.15.5"
|
"react-native-svg": "^15.15.5"
|
||||||
@@ -5386,6 +5392,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/geojson": {
|
||||||
|
"version": "7946.0.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||||
|
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/graceful-fs": {
|
"node_modules/@types/graceful-fs": {
|
||||||
"version": "4.1.9",
|
"version": "4.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
||||||
@@ -15764,6 +15776,28 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-maps": {
|
||||||
|
"version": "1.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.27.2.tgz",
|
||||||
|
"integrity": "sha512-VKr+xZ2RZGHHJlY6KhlafvGSmK0dq/tUu5uhfJ7K9rwN5pUdubdugzMKGDU/16lXmQSg7xbClKhRctj3Pm5F5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "^7946.0.13"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.19.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 18.3.1",
|
||||||
|
"react-native": ">= 0.76.0",
|
||||||
|
"react-native-web": ">= 0.11"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-native-web": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-safe-area-context": {
|
"node_modules/react-native-safe-area-context": {
|
||||||
"version": "5.6.2",
|
"version": "5.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
|
||||||
@@ -18913,7 +18947,8 @@
|
|||||||
"@timetoleave/core": "*"
|
"@timetoleave/core": "*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5"
|
"typescript": "^5",
|
||||||
|
"vitest": "^4.1.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
|
|||||||
+8
-1
@@ -15,7 +15,9 @@
|
|||||||
"test": "npm run test -w apps/web && npm run test -w apps/mobile && npm run test -w packages/core && npm run test -w packages/api-client",
|
"test": "npm run test -w apps/web && npm run test -w apps/mobile && npm run test -w packages/core && npm run test -w packages/api-client",
|
||||||
"dev:mobile": "npm run start -w apps/mobile",
|
"dev:mobile": "npm run start -w apps/mobile",
|
||||||
"typecheck:mobile": "npm run typecheck -w apps/mobile",
|
"typecheck:mobile": "npm run typecheck -w apps/mobile",
|
||||||
"prepare": "husky"
|
"prepare": "husky",
|
||||||
|
"android": "expo run:android",
|
||||||
|
"ios": "expo run:ios"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,tsx}": [
|
"*.{ts,tsx}": [
|
||||||
@@ -31,5 +33,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^17.0.5"
|
"lint-staged": "^17.0.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"expo": "~54.0.34",
|
||||||
|
"react": "19.1.0",
|
||||||
|
"react-native": "0.81.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export interface BikeRoute {
|
|||||||
distance: number;
|
distance: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
steps: BikeStep[];
|
steps: BikeStep[];
|
||||||
|
/** Encoded polyline geometry (Google polyline format). */
|
||||||
|
geometry?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Walking route from OSRM with turn-by-turn steps. */
|
/** Walking route from OSRM with turn-by-turn steps. */
|
||||||
@@ -61,6 +63,8 @@ export interface WalkRoute {
|
|||||||
distance: number;
|
distance: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
steps: WalkStep[];
|
steps: WalkStep[];
|
||||||
|
/** Encoded polyline geometry (Google polyline format). */
|
||||||
|
geometry?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Walking step from OSRM route data. */
|
/** Walking step from OSRM route data. */
|
||||||
|
|||||||
Reference in New Issue
Block a user