Skip to content

Commit

Permalink
Added InstructionView
Browse files Browse the repository at this point in the history
  • Loading branch information
bjtrounson committed Dec 25, 2024
1 parent 4217567 commit 6d99b4c
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 10 deletions.
132 changes: 132 additions & 0 deletions react-native/src/views/InstructionsView.tsx
Original file line number Diff line number Diff line change
@@ -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<RouteStep>;
};

/**
* 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 (
<View style={defaultStyle.container}>
<View style={defaultStyle.column}>
<Pressable
style={defaultStyle.instructionButton}
onPress={handleExpand}
>
<ManeuverImage content={instructions.primaryContent} />
<View>
<Text style={defaultStyle.distanceText}>
{distanceFormatter.format(distanceToNextManeuver)}
</Text>
<Text style={defaultStyle.instructionText}>
{instructions.primaryContent.text}
</Text>
</View>
</Pressable>
</View>
{isExpanded && (
<View style={defaultStyle.column}>
<FlatList
data={upcomingInstructions}
renderItem={({ item, index }) => {
if (!item) return null;
return (
<View key={index} style={defaultStyle.instructionListItem}>
<ManeuverImage content={item.primaryContent} />
<View>
<Text style={defaultStyle.distanceText}>
{distanceFormatter.format(
item.triggerDistanceBeforeManeuver
)}
</Text>
<Text style={defaultStyle.instructionText}>
{item.primaryContent.text}
</Text>
</View>
</View>
);
}}
/>
</View>
)}
</View>
);
};

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;
10 changes: 9 additions & 1 deletion react-native/src/views/NavigationView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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;
Expand Down Expand Up @@ -72,6 +75,11 @@ const NavigationView = (props: NavigationViewProps) => {
<BorderedPolyline points={uiState?.routeGeometry ?? []} zIndex={0} />
{children}
</MapView>
<InstructionsView
instructions={uiState?.visualInstruction}
remainingSteps={uiState?.remainingSteps}
distanceToNextManeuver={uiState?.progress?.distanceToNextManeuver ?? 0}
/>
<TripProgressView
progress={uiState?.progress}
onTapExit={() => core.stopNavigation()}
Expand Down
22 changes: 13 additions & 9 deletions react-native/src/views/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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)
Expand All @@ -102,7 +106,7 @@ export const LocalizedDurationFormatter = () => {
return {
format,
};
};
}

export function getLocale() {
let currentLocale = 'en';
Expand All @@ -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',
Expand All @@ -137,7 +141,7 @@ export const LocalizedDistanceFormatter = () => {
unit: 'meter',
unitDisplay: 'short',
maximumFractionDigits: 0,
}).format(distanceInMeters);
}).format(input);
}
}

Expand All @@ -146,4 +150,4 @@ export const LocalizedDistanceFormatter = () => {
return {
format,
};
};
}

0 comments on commit 6d99b4c

Please sign in to comment.