Files
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

864 lines
28 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { IcBike, IcBus, IcCar, IcWalk } from '../res/icons';
import { useMapGestures } from './useMapGestures';
import { useMapStore, selectGoogle } from '../state';
import { useLocale } from '../locale';
import {
formatRouteDistanceMeters,
getMapStrings,
googleLangCode,
parseDistanceTextToMeters,
toJudgeTravelMode,
} from '../utils/placeUtils';
import {
cacheRouteByKey,
getOfflineRoutePayload,
makePointPairRouteCacheKey,
} from '../utils/offlineRouteStore';
import { hasGoogleMapsApiKey } from '../utils/googleMapsConfig';
export type RoutePoint = {
lat: number;
lng: number;
name: string;
address?: string;
isCurrentLocation?: boolean;
};
type TransportModeKey = 'driving' | 'transit' | 'walking' | 'cycling';
type RouteTravelModeValue =
| google.maps.TravelMode
| 'DRIVING'
| 'TRANSIT'
| 'WALKING'
| 'BICYCLING';
export function isCurrentLocationRoutePoint(point: RoutePoint | null | undefined): boolean {
return point?.isCurrentLocation === true;
}
function normalizeTransportMode(mode: unknown): TransportModeKey {
return mode === 'transit' || mode === 'walking' || mode === 'cycling' || mode === 'driving'
? mode
: 'driving';
}
function resolveTravelMode(
uiMode: TransportModeKey,
maps: typeof google.maps | undefined,
): RouteTravelModeValue {
if (uiMode === 'transit') return maps?.TravelMode?.TRANSIT ?? 'TRANSIT';
if (uiMode === 'walking') return maps?.TravelMode?.WALKING ?? 'WALKING';
if (uiMode === 'cycling') return maps?.TravelMode?.BICYCLING ?? 'BICYCLING';
return maps?.TravelMode?.DRIVING ?? 'DRIVING';
}
function modeStorageKey(mode: RouteTravelModeValue): string {
return String(mode).toUpperCase();
}
/** JS Routes API 的 `Route.computeRoutes` 在 @types/google.maps 的 RoutesLibrary 中尚未补齐 */
type RouteComputeApi = {
computeRoutes: (request: {
origin: { lat: number; lng: number };
destination: { lat: number; lng: number };
travelMode: google.maps.TravelMode;
computeAlternativeRoutes?: boolean;
fields?: string[];
}) => Promise<{
routes?: Array<{
path?: google.maps.LatLng[];
localizedValues?: { duration?: string };
legs?: Array<{
durationMillis?: number;
localizedValues?: { duration?: string };
distanceMeters?: number;
steps?: Array<{
instructions?: string;
distanceMeters?: number;
maneuver?: string;
}>;
endLocation?: { lat: number; lng: number };
}>;
routeLabels?: string[];
}>;
}>;
};
async function importRoutesLibrary(
maps: typeof google.maps,
): Promise<google.maps.RoutesLibrary & { Route: RouteComputeApi }> {
return (await maps.importLibrary('routes')) as google.maps.RoutesLibrary & { Route: RouteComputeApi };
}
export interface RouteStep {
instructions: string;
distanceText: string;
distanceMeters: number;
maneuver?: string;
}
export interface RouteInfo {
duration: string;
distance: string;
distanceMeters?: number;
routeLabels?: string[];
}
export function useRouting(options: {
mapInstance: google.maps.Map | null;
currentLocation: { latitude: number; longitude: number };
}) {
const { mapInstance, currentLocation } = options;
const { replaceState } = useMapGestures();
const location = useLocation();
const google = useMapStore(selectGoogle);
const hasRealGoogleMapsKey = hasGoogleMapsApiKey();
const locale = useLocale();
const s = useMemo(() => getMapStrings(locale), [locale]);
const setActiveRoute = useMapStore((s) => s.setActiveRoute);
const setRouteModes = useMapStore((s) => s.setRouteModes);
const destination = location.state?.destination as RoutePoint | undefined;
const routePolylineRef = useRef<google.maps.Polyline | null>(null);
const clearRoutePolyline = useCallback(() => {
if (routePolylineRef.current) {
routePolylineRef.current.setMap(null);
routePolylineRef.current = null;
}
}, []);
const [routeInfo, setRouteInfo] = useState<RouteInfo | null>(null);
const [routeError, setRouteError] = useState<string | null>(null);
const [routeEndLocation, setRouteEndLocation] = useState<{ lat: number; lng: number } | null>(null);
const [routeSteps, setRouteSteps] = useState<RouteStep[]>([]);
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const [travelMode, setTravelMode] = useState<google.maps.TravelMode>('DRIVING' as google.maps.TravelMode);
const [selectedTransportMode, setSelectedTransportMode] = useState<TransportModeKey>(
normalizeTransportMode((location.state as { mode?: unknown } | null)?.mode),
);
const [isNavigating, setIsNavigating] = useState(false);
const [origin, setOrigin] = useState<RoutePoint | null>(location.state?.origin || null);
const [selectionMode, setSelectionMode] = useState<'origin' | 'destination' | null>(null);
const [isPinPicking, setIsPinPicking] = useState(false);
const [anchorSide, setAnchorSide] = useState<'origin' | 'destination'>(
location.state?.anchorSide || 'origin',
);
const [modeDurations, setModeDurations] = useState<Record<string, string>>({});
useEffect(() => {
if (currentLocation.latitude && !origin) {
const defaultOrigin = {
lat: currentLocation.latitude,
lng: currentLocation.longitude,
name: s.your_location,
isCurrentLocation: true,
};
setOrigin(defaultOrigin);
}
}, [currentLocation, origin, s]);
useEffect(() => {
return () => {
clearRoutePolyline();
};
}, [mapInstance, clearRoutePolyline]);
useEffect(() => {
if (!destination) {
clearRoutePolyline();
}
}, [destination, clearRoutePolyline]);
useEffect(() => {
let isCancelled = false;
setRouteError(null);
if (destination && origin) {
const latDiff = Math.abs(origin.lat - destination.lat);
const lngDiff = Math.abs(origin.lng - destination.lng);
if (latDiff < 0.0001 && lngDiff < 0.0001) {
console.log('Origin and destination too close, skipping route calculation');
setRouteError(s.route_origin_dest_too_close);
return;
}
const mode = resolveTravelMode(selectedTransportMode, google?.maps);
const clearAndResetError = (msg: string) => {
clearRoutePolyline();
setRouteInfo(null);
setRouteSteps([]);
setCurrentStepIndex(0);
setRouteEndLocation(null);
setRouteError(msg);
};
void (async () => {
try {
const offlinePayload = await getOfflineRoutePayload({
origin: { lat: origin.lat, lng: origin.lng },
destination: { lat: destination.lat, lng: destination.lng },
travelMode: mode as google.maps.TravelMode,
locale,
});
if (offlinePayload) {
console.log(`[Map][离线] 路线: ${mode} -> ${offlinePayload.duration} / ${offlinePayload.distance}`);
if (isCancelled) return;
clearRoutePolyline();
if (offlinePayload.encodedPolyline && google && mapInstance) {
const geometryLib = (await google.maps.importLibrary(
'geometry',
)) as google.maps.GeometryLibrary;
const path = geometryLib.encoding.decodePath(offlinePayload.encodedPolyline);
if (path?.length) {
routePolylineRef.current = new google.maps.Polyline({
path,
strokeColor: '#6200EE',
strokeWeight: 8,
map: mapInstance,
geodesic: true,
});
}
}
setRouteInfo({
duration: offlinePayload.duration,
distance: offlinePayload.distance,
distanceMeters: offlinePayload.distance_meters,
routeLabels: offlinePayload.routeLabels,
});
const steps: RouteStep[] = (offlinePayload.steps || []).map((s) => ({
instructions: s.instruction,
distanceText: formatRouteDistanceMeters(s.distanceMeters, locale),
distanceMeters: s.distanceMeters,
maneuver: s.maneuver,
}));
setRouteSteps(steps);
setCurrentStepIndex(0);
setRouteEndLocation(offlinePayload.endLocation);
setRouteError(null);
return;
}
if (!google || !hasRealGoogleMapsKey) {
const reason = google ? '未配置真实 key' : 'Google SDK 不可用';
console.log(`[Map][离线未命中] 路线: ${mode} 未命中离线路线,${reason}`);
clearAndResetError(s.route_not_found_detail);
return;
}
console.log(`[Map][在线] 路线: ${mode} 离线未命中,调用 Google Routes`);
const { Route } = await importRoutesLibrary(google.maps);
const { routes } = await Route.computeRoutes({
origin: { lat: origin.lat, lng: origin.lng },
destination: { lat: destination.lat, lng: destination.lng },
travelMode: mode as google.maps.TravelMode,
computeAlternativeRoutes: true,
fields: ['path', 'legs', 'distanceMeters', 'localizedValues', 'routeLabels'],
});
if (isCancelled) return;
const route = routes?.[0];
if (!route || !mapInstance) {
clearAndResetError(s.route_not_found_detail);
return;
}
clearRoutePolyline();
const path = route.path;
if (path && path.length) {
routePolylineRef.current = new google.maps.Polyline({
path,
strokeColor: '#6200EE',
strokeWeight: 8,
map: mapInstance,
geodesic: true,
});
}
const leg = route.legs?.[0];
if (leg) {
const durationMs = leg.durationMillis ?? 0;
const durationText = leg.localizedValues?.duration
|| `${Math.round(durationMs / 60000)} ${s.route_minute_unit}`;
const distanceText = formatRouteDistanceMeters(leg.distanceMeters, locale);
setRouteInfo({
duration: durationText,
distance: distanceText,
distanceMeters: leg.distanceMeters,
routeLabels: route.routeLabels,
});
console.log(`[Map][在线] 路线: ${mode} -> ${durationText} / ${distanceText}`);
const steps: RouteStep[] = (leg.steps || []).map(
(s: { instructions?: string; distanceMeters?: number; maneuver?: string }) => {
const dm = s.distanceMeters ?? 0;
return {
instructions: s.instructions || '',
distanceText: formatRouteDistanceMeters(dm, locale),
distanceMeters: dm,
maneuver: s.maneuver || undefined,
};
},
);
setRouteSteps(steps);
setCurrentStepIndex(0);
const endLoc = leg.endLocation;
setRouteEndLocation(
endLoc ? { lat: endLoc.lat, lng: endLoc.lng } : null,
);
setRouteError(null);
const ck = makePointPairRouteCacheKey(
{ lat: origin.lat, lng: origin.lng },
{ lat: destination.lat, lng: destination.lng },
mode,
);
const routeModeKey = modeStorageKey(mode);
if (
ck &&
leg.distanceMeters != null &&
(routeModeKey === 'WALKING' || routeModeKey === 'DRIVING')
) {
cacheRouteByKey(ck, {
mode: routeModeKey === 'WALKING' ? 'WALKING' : 'DRIVING',
duration: durationText,
distance: distanceText,
distance_meters: leg.distanceMeters,
duration_seconds: Math.round(durationMs / 1000),
encodedPolyline: null,
endLocation: endLoc ? { lat: endLoc.lat, lng: endLoc.lng } : null,
routeLabels: route.routeLabels,
steps: (leg.steps || []).map(
(s: { instructions?: string; distanceMeters?: number; maneuver?: string }) => {
const dm = s.distanceMeters ?? 0;
return {
instruction: s.instructions || '',
distance: formatRouteDistanceMeters(dm, locale),
distanceMeters: dm,
maneuver: s.maneuver || undefined,
};
},
),
});
}
}
} catch (e) {
if (isCancelled) return;
console.warn('computeRoutes failed:', e);
clearAndResetError(s.route_failed);
}
})();
}
return () => {
isCancelled = true;
};
}, [
destination,
origin,
google,
hasRealGoogleMapsKey,
selectedTransportMode,
mapInstance,
clearRoutePolyline,
locale,
s,
]);
const handleSwapRoute = useCallback(() => {
if (destination && origin) {
const oldOrigin = origin;
const oldDest = destination;
setOrigin({
lat: oldDest.lat,
lng: oldDest.lng,
name: oldDest.name,
address: oldDest.address,
isCurrentLocation: oldDest.isCurrentLocation,
});
const newAnchorSide = anchorSide === 'origin' ? 'destination' : 'origin';
setAnchorSide(newAnchorSide);
replaceState({
...location.state,
anchorSide: newAnchorSide,
origin: {
lat: oldDest.lat,
lng: oldDest.lng,
name: oldDest.name,
address: oldDest.address,
isCurrentLocation: oldDest.isCurrentLocation,
},
destination: {
lat: oldOrigin.lat,
lng: oldOrigin.lng,
name: oldOrigin.name,
address: oldOrigin.address,
isCurrentLocation: oldOrigin.isCurrentLocation,
},
});
}
}, [destination, origin, anchorSide, location.state, replaceState]);
useEffect(() => {
if (!mapInstance || !google || !routeEndLocation || !destination) return;
let isActive = true;
const cleanupFunctions: (() => void)[] = [];
const renderWalkingPath = async () => {
let geometry = google.maps.geometry;
if (!geometry) {
try {
geometry = (await google.maps.importLibrary('geometry')) as google.maps.GeometryLibrary;
} catch (e) {
console.error('Failed to load geometry library', e);
return;
}
}
const destLatLng = new google.maps.LatLng(destination.lat, destination.lng);
const endLatLng = new google.maps.LatLng(routeEndLocation.lat, routeEndLocation.lng);
const dist = geometry.spherical.computeDistanceBetween(endLatLng, destLatLng);
if (dist > 20 && isActive) {
const lineSymbol = {
path: 'M 0,-1 0,1',
strokeOpacity: 1,
scale: 3,
strokeColor: '#4285F4',
};
const walkingPolyline = new google.maps.Polyline({
path: [endLatLng, destLatLng],
strokeOpacity: 0,
icons: [
{
icon: lineSymbol,
offset: '0',
repeat: '12px',
},
],
map: mapInstance,
zIndex: 10,
});
cleanupFunctions.push(() => walkingPolyline.setMap(null));
}
};
void renderWalkingPath();
return () => {
isActive = false;
cleanupFunctions.forEach((fn) => fn());
};
}, [mapInstance, google, destination, routeEndLocation]);
useEffect(() => {
if (!destination || !origin) return;
const latDiff = Math.abs(origin.lat - destination.lat);
const lngDiff = Math.abs(origin.lng - destination.lng);
if (latDiff < 0.0001 && lngDiff < 0.0001) {
return;
}
const modesToFetch: RouteTravelModeValue[] = [
google?.maps?.TravelMode?.DRIVING ?? 'DRIVING',
google?.maps?.TravelMode?.TRANSIT ?? 'TRANSIT',
google?.maps?.TravelMode?.WALKING ?? 'WALKING',
google?.maps?.TravelMode?.BICYCLING ?? 'BICYCLING',
];
setModeDurations({});
void (async () => {
const routeApi = google && hasRealGoogleMapsKey
? await importRoutesLibrary(google.maps).catch(() => null)
: null;
await Promise.all(
modesToFetch.map(async (mode) => {
const key = modeStorageKey(mode);
try {
const offlineDur = await getOfflineRoutePayload({
origin: { lat: origin.lat, lng: origin.lng },
destination: { lat: destination.lat, lng: destination.lng },
travelMode: mode,
locale,
});
if (offlineDur) {
console.log(`[Map][离线] 路线时长: ${mode} -> ${offlineDur.duration}`);
setModeDurations((prev) => ({
...prev,
[key]: offlineDur.duration,
}));
return;
}
if (!routeApi) {
const reason = google ? '未配置真实 key' : 'Google Routes 不可用';
console.log(`[Map][离线未命中] 路线时长: ${mode} 未命中离线时长,${reason}`);
setModeDurations((prev) => ({
...prev,
[key]: 'N/A',
}));
return;
}
const { routes } = await routeApi.Route.computeRoutes({
origin: { lat: origin.lat, lng: origin.lng },
destination: { lat: destination.lat, lng: destination.lng },
travelMode: mode as google.maps.TravelMode,
fields: ['localizedValues'],
});
const durationText = routes?.[0]?.localizedValues?.duration;
console.log(`[Map][在线] 路线时长: ${mode} -> ${durationText || 'N/A'}`);
setModeDurations((prev) => ({
...prev,
[key]: durationText || 'N/A',
}));
} catch {
setModeDurations((prev) => ({
...prev,
[key]: 'N/A',
}));
}
}),
);
})();
}, [destination, origin, google, hasRealGoogleMapsKey, locale]);
useEffect(() => {
if (!destination || !origin) {
setRouteModes({});
return;
}
const modes: Record<string, Record<string, unknown>> = {};
for (const [mode, duration] of Object.entries(modeDurations)) {
modes[mode] = { duration };
}
const activeMode = toJudgeTravelMode(selectedTransportMode);
if (routeInfo) {
modes[activeMode] = {
...(modes[activeMode] || {}),
duration: routeInfo.duration,
distance: routeInfo.distance,
distance_meters: routeInfo.distanceMeters ?? parseDistanceTextToMeters(routeInfo.distance),
};
}
setRouteModes(
Object.keys(modes).length > 0
? {
origin: {
lat: origin.lat,
lng: origin.lng,
name: origin.name,
},
destination: {
lat: destination.lat,
lng: destination.lng,
name: destination.name,
},
modes,
}
: {},
);
}, [destination, origin, modeDurations, routeInfo, selectedTransportMode, setRouteModes]);
useEffect(() => {
if (!destination || !origin || !routeInfo || routeError) {
setActiveRoute(null);
return;
}
setActiveRoute({
mode: toJudgeTravelMode(selectedTransportMode),
origin: {
lat: origin.lat,
lng: origin.lng,
name: origin.name,
},
destination: {
lat: destination.lat,
lng: destination.lng,
name: destination.name,
},
duration: routeInfo.duration,
distance: routeInfo.distance,
distance_meters: routeInfo.distanceMeters ?? parseDistanceTextToMeters(routeInfo.distance),
steps: routeSteps.map((step) => ({
instruction: step.instructions,
distance: step.distanceText,
})),
});
}, [destination, origin, routeInfo, routeError, routeSteps, selectedTransportMode, setActiveRoute]);
const modes = useMemo(
() => [
{ key: 'driving', mode: (google?.maps?.TravelMode?.DRIVING ?? 'DRIVING') as google.maps.TravelMode, icon: IcCar, label: '', subLabel: s.route_fastest },
{ key: 'transit', mode: (google?.maps?.TravelMode?.TRANSIT ?? 'TRANSIT') as google.maps.TravelMode, icon: IcBus, label: '', subLabel: '' },
{ key: 'walking', mode: (google?.maps?.TravelMode?.WALKING ?? 'WALKING') as google.maps.TravelMode, icon: IcWalk, label: '', subLabel: '' },
{ key: 'cycling', mode: (google?.maps?.TravelMode?.BICYCLING ?? 'BICYCLING') as google.maps.TravelMode, icon: IcBike, label: '', subLabel: '' },
],
[google, s],
);
const handlePinPickerConfirm = useCallback(async () => {
if (!mapInstance || !google) return;
const center = mapInstance.getCenter();
if (!center) return;
const lat = center.lat();
const lng = center.lng();
const applyPickedPoint = (name: string, address?: string) => {
if (selectionMode === 'origin') {
const newOrigin = { lat, lng, name, address, isCurrentLocation: false };
setOrigin(newOrigin);
const dest =
destination ||
({
lat: currentLocation.latitude || 39.9042,
lng: currentLocation.longitude || 116.4074,
name: s.your_location,
isCurrentLocation: true,
} as RoutePoint);
replaceState({
origin: newOrigin,
destination: dest,
mode: selectedTransportMode,
});
} else if (selectionMode === 'destination') {
replaceState({
destination: { lat, lng, name, address, isCurrentLocation: false },
mode: selectedTransportMode,
});
}
};
const geocoder = new google.maps.Geocoder();
const placesLib = (await google.maps.importLibrary('places')) as google.maps.PlacesLibrary;
const PlaceCtor = placesLib.Place;
try {
const nearbySearchPromise = PlaceCtor.searchNearby({
fields: ['id', 'displayName', 'location', 'rating', 'userRatingCount', 'types'],
locationRestriction: { center: { lat, lng }, radius: 50 },
maxResultCount: 30,
language: googleLangCode(locale),
})
.then((r) => r.places)
.catch(() => [] as google.maps.places.Place[]);
const [geoPromise, nearbyResults] = await Promise.all([
geocoder.geocode({ location: { lat, lng } }).catch(() => ({ results: [] as google.maps.GeocoderResult[] })),
nearbySearchPromise,
]);
let name: string = s.selected_location;
const geoResult = geoPromise.results[0] || null;
const address = geoResult?.formatted_address?.replace(/^中国/, '').trim() || undefined;
const visibleTypes = [
'restaurant',
'lodging',
'tourist_attraction',
'museum',
'park',
'cafe',
'bar',
'shopping_mall',
'subway_station',
'transit_station',
'department_store',
'supermarket',
'bakery',
'library',
'university',
'school',
'hospital',
];
let foundExactPOI = false;
if (nearbyResults.length > 0) {
const candidates = nearbyResults.filter((p) => p.types?.some((t) => visibleTypes.includes(t)) && p.location);
if (candidates.length > 0) {
const scoredCandidates = candidates.map((p) => {
const latDiff = Math.abs(p.location!.lat() - lat);
const lngDiff = Math.abs(p.location!.lng() - lng);
const distSq = latDiff * latDiff + lngDiff * lngDiff;
const distPenalty = distSq * 10000000;
const ratingCount = p.userRatingCount || 0;
const rating = p.rating || 0;
let prominenceScore = Math.log(ratingCount + 1) * rating;
if (p.types?.includes('tourist_attraction')) prominenceScore *= 1.5;
if (p.types?.includes('lodging')) prominenceScore *= 1.2;
return { place: p, score: prominenceScore - distPenalty, distSq };
});
scoredCandidates.sort((a, b) => b.score - a.score);
const winner = scoredCandidates[0];
if (winner && winner.distSq < 0.00005) {
name = winner.place.displayName!;
foundExactPOI = true;
}
}
}
if (!foundExactPOI && geoResult?.place_id) {
const isGeocoderPrimaryVisible = geoResult.types?.some((t) => visibleTypes.includes(t));
if (isGeocoderPrimaryVisible) {
try {
const geoPlace = new PlaceCtor({ id: geoResult.place_id });
await geoPlace.fetchFields({ fields: ['displayName'] });
if (geoPlace.displayName) {
name = geoPlace.displayName;
foundExactPOI = true;
}
} catch {
/* ignore */
}
}
}
if (!foundExactPOI) {
const majorTypes = [
'lodging',
'shopping_mall',
'transit_station',
'subway_station',
'tourist_attraction',
'museum',
'park',
'university',
'hospital',
'airport',
'library',
'train_station',
'restaurant',
'cafe',
'bar',
];
try {
const { places: widerResults } = await PlaceCtor.searchNearby({
fields: ['displayName', 'location', 'types'],
locationRestriction: { center: { lat, lng }, radius: 100 },
maxResultCount: 30,
language: googleLangCode(locale),
});
const landmark = widerResults.find((p) => p.types?.some((t) => majorTypes.includes(t)) && p.location);
if (landmark?.location) {
const dLat = landmark.location.lat() - lat;
const dLng = landmark.location.lng() - lng;
if (dLat * dLat + dLng * dLng < 0.000005) {
name = landmark.displayName!;
foundExactPOI = true;
}
}
} catch {
/* ignore */
}
}
if (!foundExactPOI && geoResult) {
const result = geoResult;
const getComp = (type: string) => result.address_components.find((c) => c.types.includes(type))?.long_name;
const poi = getComp('point_of_interest') || getComp('establishment') || getComp('premise');
const route = getComp('route');
const streetNum = getComp('street_number');
const sublocality = getComp('sublocality') || getComp('sublocality_level_1');
if (poi) {
name = poi;
} else if (route) {
name = `${route}${streetNum || ''}`;
} else if (sublocality) {
name = sublocality;
} else {
name = result.formatted_address.replace(/^中国/, '').trim();
}
}
applyPickedPoint(name, address);
} catch (e) {
console.error('Geocoding failed', e);
applyPickedPoint(`${lat.toFixed(4)},${lng.toFixed(4)}`);
}
setIsPinPicking(false);
setSelectionMode(null);
}, [
mapInstance,
google,
locale,
selectionMode,
setOrigin,
destination,
currentLocation.latitude,
currentLocation.longitude,
replaceState,
location.state,
selectedTransportMode,
s,
]);
return {
destination,
location,
origin,
setOrigin,
anchorSide,
setAnchorSide,
selectionMode,
setSelectionMode,
isPinPicking,
setIsPinPicking,
routeInfo,
setRouteInfo,
routeError,
setRouteError,
routeSteps,
currentStepIndex,
setCurrentStepIndex,
routeEndLocation,
setRouteEndLocation,
travelMode,
setTravelMode,
selectedTransportMode,
setSelectedTransportMode,
modeDurations,
isNavigating,
setIsNavigating,
clearRoutePolyline,
handleSwapRoute,
handlePinPickerConfirm,
modes,
};
}