-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f419c1f
commit 9e74c58
Showing
7 changed files
with
511 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import { | ||
TripState, | ||
type GeographicCoordinate, | ||
type RouteDeviation, | ||
type RouteStep, | ||
type SpokenInstruction, | ||
type TripProgress, | ||
type UserLocation, | ||
type VisualInstruction, | ||
} from '../generated/ferrostar'; | ||
import type { NavigationState } from './FerrostarCore'; | ||
|
||
export class NavigationUiState { | ||
/** The user's location as reported by the location provider. */ | ||
location?: UserLocation; | ||
/** The user's location snapped to the route shape. */ | ||
snappedLocation?: UserLocation; | ||
/** | ||
* The last known heading of the user. | ||
* | ||
* NOTE: This is distinct from the course over ground (direction of travel), which is included | ||
* in the `location` and `snappedLocation` properties. | ||
*/ | ||
heading?: number; | ||
/** The geometry of the full route. */ | ||
routeGeometry?: Array<GeographicCoordinate>; | ||
/** Visual instructions which should be displayed based on the user's current progress. */ | ||
visualInstruction?: VisualInstruction; | ||
/** | ||
* Instructions which should be spoken via speech synthesis based on the user's current | ||
* progress. | ||
*/ | ||
spokenInstruction?: SpokenInstruction; | ||
/** The user's progress through the current trip. */ | ||
progress?: TripProgress; | ||
/** If true, the core is currently calculating a new route. */ | ||
isCalculatingNewRoute?: boolean; | ||
/** Describes whether the user is believed to be off the correct route. */ | ||
routeDeviation?: RouteDeviation; | ||
/** If true, spoken instructions will not be synthesized. */ | ||
isMuted?: boolean; | ||
/** The name of the road which the current route step is traversing. */ | ||
currentStepRoadName?: string; | ||
/** The remaining steps in the trip (including the current step). */ | ||
remainingSteps?: Array<RouteStep>; | ||
/** The route annotation object at the current location. */ | ||
// TODO: Annotation implementation | ||
//currentAnnotation: AnnotationWrapper<*> | ||
|
||
constructor( | ||
location?: UserLocation, | ||
snappedLocation?: UserLocation, | ||
heading?: number, | ||
routeGeometry?: Array<GeographicCoordinate>, | ||
visualInstruction?: VisualInstruction, | ||
spokenInstruction?: SpokenInstruction, | ||
progress?: TripProgress, | ||
isCalculatingNewRoute?: boolean, | ||
routeDeviation?: RouteDeviation, | ||
isMuted?: boolean, | ||
currentStepRoadName?: string, | ||
remainingSteps?: Array<RouteStep> | ||
) { | ||
this.location = location; | ||
this.snappedLocation = snappedLocation; | ||
this.heading = heading; | ||
this.routeGeometry = routeGeometry; | ||
this.visualInstruction = visualInstruction; | ||
this.spokenInstruction = spokenInstruction; | ||
this.progress = progress; | ||
this.isCalculatingNewRoute = isCalculatingNewRoute; | ||
this.routeDeviation = routeDeviation; | ||
this.isMuted = isMuted; | ||
this.currentStepRoadName = currentStepRoadName; | ||
this.remainingSteps = remainingSteps; | ||
} | ||
|
||
static fromFerrostar( | ||
coreState: NavigationState, | ||
isMuted?: boolean, | ||
location?: UserLocation, | ||
snappedLocation?: UserLocation | ||
): NavigationUiState { | ||
let tripState; | ||
if (TripState.Navigating.instanceOf(coreState.tripState)) { | ||
tripState = coreState.tripState; | ||
} | ||
return new NavigationUiState( | ||
location, | ||
snappedLocation, | ||
undefined, | ||
coreState.routeGeometry, | ||
tripState?.inner.visualInstruction, | ||
undefined, | ||
tripState?.inner.progress, | ||
coreState.isCalculatingNewRoute, | ||
tripState?.inner.deviation, | ||
isMuted, | ||
undefined, // TODO: Android seems to have this type but it's not in the TS definition | ||
tripState?.inner.remainingSteps | ||
); | ||
} | ||
|
||
isNavigating(): boolean { | ||
return this.progress !== undefined; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { LineLayer, ShapeSource } from '@maplibre/maplibre-react-native'; | ||
|
||
type BorderedPolylineProps = { | ||
points: Array<{ lat: number; lng: number }>; | ||
zIndex?: number; | ||
color?: string; | ||
borderColor?: string; | ||
lineWidth?: number; | ||
borderWidth?: number; | ||
}; | ||
|
||
const BorderedPolyline = ({ | ||
points, | ||
zIndex = 1, | ||
color = '#3583dd', | ||
borderColor = '#ffffff', | ||
lineWidth = 10.0, | ||
borderWidth = 3.0, | ||
}: BorderedPolylineProps) => { | ||
if (points.length < 2) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<ShapeSource | ||
id="border-polyline" | ||
shape={{ | ||
type: 'Feature', | ||
geometry: { | ||
type: 'LineString', | ||
coordinates: points.map((p) => [p.lng, p.lat]) as number[][], | ||
}, | ||
properties: {}, | ||
}} | ||
> | ||
<LineLayer | ||
id="line-border" | ||
style={{ | ||
lineCap: 'round', | ||
lineWidth: lineWidth + borderWidth * 2.0, | ||
lineColor: borderColor, | ||
lineSortKey: zIndex, | ||
}} | ||
/> | ||
<LineLayer | ||
id="line" | ||
style={{ | ||
lineCap: 'round', | ||
lineWidth, | ||
lineColor: color, | ||
lineSortKey: zIndex, | ||
}} | ||
/> | ||
</ShapeSource> | ||
); | ||
}; | ||
|
||
export default BorderedPolyline; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import { | ||
Camera, | ||
UserTrackingMode, | ||
type CameraPadding, | ||
} from '@maplibre/maplibre-react-native'; | ||
import { useMemo } from 'react'; | ||
import { Dimensions, PixelRatio, useWindowDimensions } from 'react-native'; | ||
|
||
export class NavigationActivity { | ||
zoom: number; | ||
pitch: number; | ||
|
||
constructor(zoom: number, pitch: number) { | ||
this.zoom = zoom; | ||
this.pitch = pitch; | ||
} | ||
|
||
/* The recommended camera configuration for automotive navigation. */ | ||
static Automotive = new NavigationActivity(16.0, 45.0); | ||
|
||
/* The recommended camera configuration for bicycle navigation. */ | ||
static Bicycle = new NavigationActivity(18.0, 45.0); | ||
|
||
/* The recommended camera configuration for pedestrian navigation. */ | ||
static Pedestrian = new NavigationActivity(20.0, 10.0); | ||
} | ||
|
||
type NavigationMapViewCameraProps = { | ||
activity?: NavigationActivity; | ||
}; | ||
|
||
/** | ||
* The camera configuration for navigation. This configuration sets the camera to track the user, | ||
* with a high zoom level and moderate pitch for a 2.5D isometric view. It automatically adjusts the | ||
* padding based on the screen size and orientation. | ||
* | ||
* @param activity The type of activity the camera is being used for. | ||
* @return The recommended navigation MapViewCamera | ||
*/ | ||
const NavigationMapViewCamera = ({ | ||
activity = NavigationActivity.Automotive, | ||
}: NavigationMapViewCameraProps) => { | ||
const { width, height } = useWindowDimensions(); | ||
const orientation = useMemo(() => { | ||
return height > width ? 'portrait' : 'landscape'; | ||
}, [height, width]); | ||
|
||
const start = useMemo(() => { | ||
if (orientation === 'landscape') return 0.5; | ||
return 0.0; | ||
}, [orientation]); | ||
|
||
const padding: CameraPadding = useMemo(() => { | ||
const { height: screenHeight, width: screenWidth } = | ||
Dimensions.get('screen'); | ||
|
||
const screenWidthPx = PixelRatio.getPixelSizeForLayoutSize(screenWidth); | ||
const screenHeightPx = PixelRatio.getPixelSizeForLayoutSize(screenHeight); | ||
// TODO: A way to calculate RTL and LTR padding. | ||
const left = start * screenWidthPx; | ||
const top = 0.5 * screenHeightPx; | ||
const right = 0.0 * screenWidthPx; | ||
const bottom = 0.0 * screenHeightPx; | ||
|
||
return { | ||
paddingLeft: left, | ||
paddingTop: top, | ||
paddingRight: right, | ||
paddingBottom: bottom, | ||
}; | ||
}, [start]); | ||
|
||
return ( | ||
<Camera | ||
defaultSettings={{ | ||
padding, | ||
}} | ||
zoomLevel={activity.zoom} | ||
pitch={activity.pitch} | ||
followUserLocation | ||
followUserMode={UserTrackingMode.FollowWithHeading} | ||
/> | ||
); | ||
}; | ||
|
||
export default NavigationMapViewCamera; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,75 @@ | ||
import MapLibreGL, { MapView } from '@maplibre/maplibre-react-native'; | ||
import { useEffect, useState, type ComponentProps } from 'react'; | ||
import { FerrostarCore, type NavigationState } from '../core/FerrostarCore'; | ||
import MapLibreGL, { | ||
Camera, | ||
MapView, | ||
UserLocation, | ||
} from '@maplibre/maplibre-react-native'; | ||
import { useEffect, useMemo, useState, type ComponentProps } from 'react'; | ||
import { FerrostarCore } from '../core/FerrostarCore'; | ||
import { NavigationUiState } from '../core/NavigationUiState'; | ||
import BorderedPolyline from './BorderedPolyline'; | ||
import NavigationMapViewCamera from './NavigationMapViewCamera'; | ||
import TripProgressView from './TripProgressView'; | ||
import { View } from 'react-native'; | ||
|
||
MapLibreGL.setAccessToken(null); | ||
|
||
type NavigationViewProps = ComponentProps<typeof MapView> & { | ||
core: FerrostarCore; | ||
snapUserLocationToRoute?: boolean; | ||
}; | ||
|
||
const NavigationView = (props: NavigationViewProps) => { | ||
const { core } = props; | ||
const [navigationState, setNavigationState] = useState<NavigationState>(); | ||
const { core, children, snapUserLocationToRoute = true } = props; | ||
const [uiState, setUiState] = useState<NavigationUiState>(); | ||
|
||
const isNavigating = useMemo(() => { | ||
return uiState?.isNavigating() ?? false; | ||
}, [uiState]); | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
const location = useMemo(() => { | ||
if (snapUserLocationToRoute && isNavigating) { | ||
return uiState?.snappedLocation; | ||
} | ||
|
||
return uiState?.location; | ||
}, [ | ||
isNavigating, | ||
snapUserLocationToRoute, | ||
uiState?.location, | ||
uiState?.snappedLocation, | ||
]); | ||
|
||
useEffect(() => { | ||
const watchId = core.addStateListener((state) => { | ||
setNavigationState(state); | ||
setUiState(NavigationUiState.fromFerrostar(state)); | ||
}); | ||
|
||
return () => { | ||
core.removeStateListener(watchId); | ||
}; | ||
}, [core]); | ||
|
||
return <MapView {...props} />; | ||
return ( | ||
<View style={{ flex: 1, position: 'relative' }}> | ||
<MapView compassEnabled={false} {...props}> | ||
{isNavigating ? ( | ||
<NavigationMapViewCamera /> | ||
) : ( | ||
<> | ||
<Camera followUserLocation /> | ||
<UserLocation /> | ||
</> | ||
)} | ||
<BorderedPolyline points={uiState?.routeGeometry ?? []} zIndex={0} /> | ||
{children} | ||
</MapView> | ||
<TripProgressView | ||
progress={uiState?.progress} | ||
onTapExit={() => core.stopNavigation()} | ||
/> | ||
</View> | ||
); | ||
}; | ||
|
||
export default NavigationView; |
Oops, something went wrong.