diff --git a/react-native/src/views/InstructionsView.tsx b/react-native/src/views/InstructionsView.tsx new file mode 100644 index 00000000..459e449e --- /dev/null +++ b/react-native/src/views/InstructionsView.tsx @@ -0,0 +1,132 @@ +import { useMemo, useState } from 'react'; +import type { RouteStep, VisualInstruction } from '../generated/ferrostar'; +import { LocalizedDistanceFormatter, type Formatter } from './_utils'; +import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native'; +import ManeuverImage from './maneuver/ManeuverImage'; + +export type InstructionViewProps = { + instructions?: VisualInstruction; + distanceToNextManeuver?: number; + distanceFormatter?: Formatter; + remainingSteps?: Array; +}; + +/** + * A banner view with sensible defaults. + * + * This banner view includes the default iconography from Mapbox, attempts to use the device's + * locale for formatting distances and determining flow order (this can be overridden by passing a + * customized formatter.) + */ +const InstructionsView = ({ + instructions, + distanceToNextManeuver = 0, + distanceFormatter = LocalizedDistanceFormatter(), + remainingSteps, +}: InstructionViewProps) => { + const [isExpanded, setIsExpanded] = useState(false); + + // These are the steps that will be listed in the dropdown menu + const nextSteps = useMemo(() => { + return remainingSteps?.slice(1) ?? []; + }, [remainingSteps]); + + const upcomingInstructions = useMemo(() => { + return nextSteps.map((step) => step.visualInstructions[0] ?? null); + }, [nextSteps]); + + const handleExpand = () => { + setIsExpanded(!isExpanded); + }; + + if (!instructions) return null; + + return ( + + + + + + + {distanceFormatter.format(distanceToNextManeuver)} + + + {instructions.primaryContent.text} + + + + + {isExpanded && ( + + { + if (!item) return null; + return ( + + + + + {distanceFormatter.format( + item.triggerDistanceBeforeManeuver + )} + + + {item.primaryContent.text} + + + + ); + }} + /> + + )} + + ); +}; + +const defaultStyle = StyleSheet.create({ + container: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + flex: 1, + flexDirection: 'column', + }, + column: { + flex: 1, + flexDirection: 'column', + backgroundColor: '#fff', + borderRadius: 10, + marginTop: 10, + marginRight: 10, + marginLeft: 10, + }, + instructionButton: { + flex: 1, + flexDirection: 'row', + padding: 10, + alignItems: 'center', + }, + instructionText: { + fontSize: 18, + color: '#000', + }, + instructionListItem: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + margin: 10, + paddingVertical: 10, + }, + distanceText: { + fontSize: 16, + color: '#000', + }, +}); + +export default InstructionsView; diff --git a/react-native/src/views/NavigationView.tsx b/react-native/src/views/NavigationView.tsx index 86549cce..5e03731a 100644 --- a/react-native/src/views/NavigationView.tsx +++ b/react-native/src/views/NavigationView.tsx @@ -10,6 +10,7 @@ import BorderedPolyline from './BorderedPolyline'; import NavigationMapViewCamera from './NavigationMapViewCamera'; import TripProgressView from './TripProgressView'; import { View } from 'react-native'; +import InstructionsView from './InstructionsView'; MapLibreGL.setAccessToken(null); @@ -26,8 +27,10 @@ const NavigationView = (props: NavigationViewProps) => { return uiState?.isNavigating() ?? false; }, [uiState]); - // eslint-disable-next-line @typescript-eslint/no-unused-vars // We need to find a way to override the location manager from within maplibre-react-native + // or we need to create a custom puck that can have a custom navigation when navigating. + // But that is only when the snapToRouteLocation is true. + // eslint-disable-next-line @typescript-eslint/no-unused-vars const location = useMemo(() => { if (snapUserLocationToRoute && isNavigating) { return uiState?.snappedLocation; @@ -72,6 +75,11 @@ const NavigationView = (props: NavigationViewProps) => { {children} + core.stopNavigation()} diff --git a/react-native/src/views/_utils.ts b/react-native/src/views/_utils.ts index 3ae89ed3..cead78b8 100644 --- a/react-native/src/views/_utils.ts +++ b/react-native/src/views/_utils.ts @@ -11,7 +11,11 @@ type CalculatedResult = { seconds: number; }; -export const LocalizedDurationFormatter = () => { +export type Formatter = { + format(input: number): string; +}; + +export function LocalizedDurationFormatter(): Formatter { const units: DurationUnit[] = ['days', 'hours', 'minutes', 'seconds']; const unitStyle: UnitStyle = 'short'; @@ -90,8 +94,8 @@ export const LocalizedDurationFormatter = () => { } } - function format(durtionSeconds: number): string { - const durationRecord = calculate(durtionSeconds); + function format(input: number): string { + const durationRecord = calculate(input); return Object.entries(durationRecord) .filter((it) => it[1] > 0) @@ -102,7 +106,7 @@ export const LocalizedDurationFormatter = () => { return { format, }; -}; +} export function getLocale() { let currentLocale = 'en'; @@ -119,11 +123,11 @@ export function getLocale() { return currentLocale; } -export const LocalizedDistanceFormatter = () => { +export function LocalizedDistanceFormatter(): Formatter { const locale = getLocale().replace('_', '-'); - function format(distanceInMeters: number): string { + function format(input: number): string { // We want to the distance in kilometers only if it's greater than 1000 meters - const distanceInKilometers = distanceInMeters / 1000; + const distanceInKilometers = input / 1000; if (distanceInKilometers > 1) { return new Intl.NumberFormat(locale, { style: 'unit', @@ -137,7 +141,7 @@ export const LocalizedDistanceFormatter = () => { unit: 'meter', unitDisplay: 'short', maximumFractionDigits: 0, - }).format(distanceInMeters); + }).format(input); } } @@ -146,4 +150,4 @@ export const LocalizedDistanceFormatter = () => { return { format, }; -}; +}