Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add distance label on map route #55517

Merged
merged 19 commits into from
Feb 5, 2025
38 changes: 22 additions & 16 deletions src/components/DistanceRequest/DistanceRequestFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
import React, {useCallback, useMemo} from 'react';
import type {ReactNode} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import Button from '@components/Button';
import DistanceMapView from '@components/DistanceMapView';
import * as Expensicons from '@components/Icon/Expensicons';
import ImageSVG from '@components/ImageSVG';
import type {WayPoint} from '@components/MapView/MapViewTypes';
import useLocalize from '@hooks/useLocalize';
import usePolicy from '@hooks/usePolicy';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as TransactionUtils from '@libs/TransactionUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import {getPersonalPolicy} from '@libs/PolicyUtils';
import {getDistanceInMeters, getWaypointIndex, isCustomUnitRateIDForP2P} from '@libs/TransactionUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {MapboxAccessToken} from '@src/types/onyx';
import type {Policy} from '@src/types/onyx';
import type {WaypointCollection} from '@src/types/onyx/Transaction';
import type Transaction from '@src/types/onyx/Transaction';
import type IconAsset from '@src/types/utils/IconAsset';

const MAX_WAYPOINTS = 25;

type DistanceRequestFooterOnyxProps = {
/** Data about Mapbox token for calling Mapbox API */
mapboxAccessToken: OnyxEntry<MapboxAccessToken>;
};

type DistanceRequestFooterProps = DistanceRequestFooterOnyxProps & {
type DistanceRequestFooterProps = {
/** The waypoints for the distance expense */
waypoints?: WaypointCollection;

Expand All @@ -35,16 +33,26 @@ type DistanceRequestFooterProps = DistanceRequestFooterOnyxProps & {

/** The transaction being interacted with */
transaction: OnyxEntry<Transaction>;

/** The policy */
policy: OnyxEntry<Policy>;
};

function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navigateToWaypointEditPage}: DistanceRequestFooterProps) {
function DistanceRequestFooter({waypoints, transaction, navigateToWaypointEditPage, policy}: DistanceRequestFooterProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
const activePolicy = usePolicy(activePolicyID);
const [mapboxAccessToken] = useOnyx(ONYXKEYS.MAPBOX_ACCESS_TOKEN);

const numberOfWaypoints = Object.keys(waypoints ?? {}).length;
const numberOfFilledWaypoints = Object.values(waypoints ?? {}).filter((waypoint) => waypoint?.address).length;
const lastWaypointIndex = numberOfWaypoints - 1;
const defaultMileageRate = DistanceRequestUtils.getDefaultMileageRate(policy ?? activePolicy);
const policyCurrency = (policy ?? activePolicy)?.outputCurrency ?? getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD;
const mileageRate = isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(policyCurrency, transaction) : defaultMileageRate;
const {unit} = mileageRate ?? {};

const getMarkerComponent = useCallback(
(icon: IconAsset): ReactNode => (
Expand All @@ -66,7 +74,7 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig
return;
}

const index = TransactionUtils.getWaypointIndex(key);
const index = getWaypointIndex(key);
let MarkerComponent: IconAsset;
if (index === 0) {
MarkerComponent = Expensicons.DotIndicatorUnfilled;
Expand Down Expand Up @@ -114,6 +122,8 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig
waypoints={waypointMarkers}
styleURL={CONST.MAPBOX.STYLE_URL}
overlayStyle={styles.mapEditView}
distanceInMeters={getDistanceInMeters(transaction, undefined)}
unit={unit}
/>
</View>
</>
Expand All @@ -122,8 +132,4 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig

DistanceRequestFooter.displayName = 'DistanceRequestFooter';

export default withOnyx<DistanceRequestFooterProps, DistanceRequestFooterOnyxProps>({
mapboxAccessToken: {
key: ONYXKEYS.MAPBOX_ACCESS_TOKEN,
},
})(DistanceRequestFooter);
export default DistanceRequestFooter;
64 changes: 59 additions & 5 deletions src/components/MapView/MapView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import * as Expensicons from '@components/Icon/Expensicons';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import Text from '@components/Text';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as UserLocation from '@libs/actions/UserLocation';
import {clearUserLocation, setUserLocation} from '@libs/actions/UserLocation';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import getCurrentPosition from '@libs/getCurrentPosition';
import type {GeolocationErrorCallback} from '@libs/getCurrentPosition/getCurrentPosition.types';
import {GeolocationErrorCode} from '@libs/getCurrentPosition/getCurrentPosition.types';
Expand All @@ -24,7 +27,7 @@ import responder from './responder';
import utils from './utils';

const MapView = forwardRef<MapViewHandle, MapViewProps>(
({accessToken, style, mapPadding, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady, interactive = true}, ref) => {
({accessToken, style, mapPadding, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady, interactive = true, distanceInMeters, unit}, ref) => {
const [userLocation] = useOnyx(ONYXKEYS.USER_LOCATION);
const navigation = useNavigation();
const {isOffline} = useNetwork();
Expand All @@ -39,6 +42,25 @@ const MapView = forwardRef<MapViewHandle, MapViewProps>(
const shouldInitializeCurrentPosition = useRef(true);
const [isAccessTokenSet, setIsAccessTokenSet] = useState(false);

const [distanceUnit, setDistanceUnit] = useState(unit);
useEffect(() => {
if (!unit || distanceUnit) {
return;
}
setDistanceUnit(unit);
}, [unit, distanceUnit]);

const toggleDistanceUnit = useCallback(() => {
setDistanceUnit((currentUnit) =>
currentUnit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS ? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES : CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS,
);
}, []);

const distanceLabelText = useMemo(
() => DistanceRequestUtils.getDistanceForDisplayLabel(distanceInMeters ?? 0, distanceUnit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS),
[distanceInMeters, distanceUnit],
);

// Determines if map can be panned to user's detected
// location without bothering the user. It will return
// false if user has already started dragging the map or
Expand All @@ -50,7 +72,7 @@ const MapView = forwardRef<MapViewHandle, MapViewProps>(
if (error?.code !== GeolocationErrorCode.PERMISSION_DENIED || !initialLocation) {
return;
}
UserLocation.clearUserLocation();
clearUserLocation();
},
[initialLocation],
);
Expand All @@ -74,7 +96,7 @@ const MapView = forwardRef<MapViewHandle, MapViewProps>(

getCurrentPosition((params) => {
const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude};
UserLocation.setUserLocation(currentCoords);
setUserLocation(currentCoords);
}, setCurrentPositionToInitialState);
}, [isOffline, shouldPanMapToCurrentPosition, setCurrentPositionToInitialState]),
);
Expand Down Expand Up @@ -205,6 +227,20 @@ const MapView = forwardRef<MapViewHandle, MapViewProps>(
const initCenterCoordinate = useMemo(() => (interactive ? centerCoordinate : undefined), [interactive, centerCoordinate]);
const initBounds = useMemo(() => (interactive ? undefined : waypointsBounds), [interactive, waypointsBounds]);

const distanceSymbolCoorinate = useMemo(() => {
const length = directionCoordinates?.length;
// If the array is empty, return undefined
if (!length) {
return undefined;
}

// Find the index of the middle element
const middleIndex = Math.floor(length / 2);

// Return the middle element
return directionCoordinates.at(middleIndex);
}, [directionCoordinates]);

return !isOffline && isAccessTokenSet && !!defaultSettings ? (
<View style={[style, !interactive ? styles.pointerEventsNone : {}]}>
<Mapbox.MapView
Expand Down Expand Up @@ -258,7 +294,6 @@ const MapView = forwardRef<MapViewHandle, MapViewProps>(
/>
</Mapbox.ShapeSource>
)}

{waypoints?.map(({coordinate, markerComponent, id}) => {
const MarkerComponent = markerComponent;
if (utils.areSameCoordinate([coordinate[0], coordinate[1]], [currentPosition?.longitude ?? 0, currentPosition?.latitude ?? 0]) && interactive) {
Expand All @@ -276,6 +311,25 @@ const MapView = forwardRef<MapViewHandle, MapViewProps>(
})}

{!!directionCoordinates && <Direction coordinates={directionCoordinates} />}
{!!distanceSymbolCoorinate && !!distanceInMeters && !!distanceUnit && (
<MarkerView
coordinate={distanceSymbolCoorinate}
id="distance-label"
key="distance-label"
>
<View style={{zIndex: 1}}>
<PressableWithoutFeedback
accessibilityRole={CONST.ROLE.BUTTON}
accessibilityLabel="distance-label"
onPress={toggleDistanceUnit}
>
<View style={[styles.distanceLabelWrapper]}>
<Text style={styles.distanceLabelText}> {distanceLabelText}</Text>
</View>
</PressableWithoutFeedback>
</View>
</MarkerView>
)}
</Mapbox.MapView>
{interactive && (
<View style={[styles.pAbsolute, styles.p5, styles.t0, styles.r0, {zIndex: 1}]}>
Expand Down
49 changes: 49 additions & 0 deletions src/components/MapView/MapViewImpl.website.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import * as Expensicons from '@components/Icon/Expensicons';
import {PressableWithoutFeedback} from '@components/Pressable';
import usePrevious from '@hooks/usePrevious';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import type {GeolocationErrorCallback} from '@libs/getCurrentPosition/getCurrentPosition.types';
import {GeolocationErrorCode} from '@libs/getCurrentPosition/getCurrentPosition.types';
import {clearUserLocation, setUserLocation} from '@userActions/UserLocation';
Expand All @@ -42,13 +44,28 @@ const MapViewImpl = forwardRef<MapViewHandle, MapViewProps>(
directionCoordinates,
initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM},
interactive = true,
distanceInMeters,
unit,
},
ref,
) => {
const [userLocation] = useOnyx(ONYXKEYS.USER_LOCATION);

const {isOffline} = useNetwork();
const {translate} = useLocalize();
const [distanceUnit, setDistanceUnit] = useState(unit);
useEffect(() => {
if (!unit || distanceUnit) {
return;
}
setDistanceUnit(unit);
}, [unit, distanceUnit]);

const toggleDistanceUnit = useCallback(() => {
setDistanceUnit((currentUnit) =>
currentUnit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS ? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES : CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS,
);
}, []);

const theme = useTheme();
const styles = useThemeStyles();
Expand Down Expand Up @@ -232,6 +249,20 @@ const MapViewImpl = forwardRef<MapViewHandle, MapViewProps>(
};
}, [waypoints, directionCoordinates, interactive, currentPosition, initialState.zoom]);

const distanceSymbolCoorinate = useMemo(() => {
const length = directionCoordinates?.length;
// If the array is empty, return undefined
if (!length) {
return undefined;
}

// Find the index of the middle element
const middleIndex = Math.floor(length / 2);
Comment on lines +259 to +260
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming from #56531 checklist: We can enhance the logic by finding the closest point to the center of the waypoints instead of using the middle index, which may hide the distance label in some cases.


// Return the middle element
return directionCoordinates.at(middleIndex);
}, [directionCoordinates]);

return !isOffline && !!accessToken && !!initialViewState ? (
<View
style={style}
Expand All @@ -257,6 +288,24 @@ const MapViewImpl = forwardRef<MapViewHandle, MapViewProps>(
<View style={styles.currentPositionDot} />
</Marker>
)}
{!!distanceSymbolCoorinate && !!distanceInMeters && !!distanceUnit && (
<Marker
key="distance"
longitude={distanceSymbolCoorinate.at(0) ?? 0}
latitude={distanceSymbolCoorinate.at(1) ?? 0}
>
<PressableWithoutFeedback
accessibilityLabel={CONST.ROLE.BUTTON}
role={CONST.ROLE.BUTTON}
onPress={toggleDistanceUnit}
style={{marginRight: 100}}
>
<View style={styles.distanceLabelWrapper}>
<View style={styles.distanceLabelText}> {DistanceRequestUtils.getDistanceForDisplayLabel(distanceInMeters, distanceUnit)}</View>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was supposed to display a text value, wrapping it in a view caused #56603

</View>
</PressableWithoutFeedback>
</Marker>
)}
{waypoints?.map(({coordinate, markerComponent, id}) => {
const MarkerComponent = markerComponent;
if (utils.areSameCoordinate([coordinate[0], coordinate[1]], [currentPosition?.longitude ?? 0, currentPosition?.latitude ?? 0]) && interactive) {
Expand Down
7 changes: 7 additions & 0 deletions src/components/MapView/MapViewTypes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {ReactNode} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import type {Unit} from '@src/types/onyx/Policy';

type MapViewProps = {
// Public access token to be used to fetch map data from Mapbox.
Expand All @@ -22,6 +23,12 @@ type MapViewProps = {
onMapReady?: () => void;
// Whether the map is interactable or not
interactive?: boolean;

// Distance displayed on the map in meters.
distanceInMeters?: number;

// Unit of measurement for distance
unit?: Unit;
};

type DirectionProps = {
Expand Down
6 changes: 6 additions & 0 deletions src/libs/DistanceRequestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,11 @@ function getDistanceForDisplay(
return `${distanceInUnits} ${unitString}`;
}

function getDistanceForDisplayLabel(distanceInMeters: number, unit: Unit): string {
const distanceInUnits = getRoundedDistanceInUnits(distanceInMeters, unit);
return `${distanceInUnits} ${unit}`;
}

/**
* @param hasRoute Whether the route exists for the distance expense
* @param distanceInMeters Distance traveled
Expand Down Expand Up @@ -407,6 +412,7 @@ export default {
getUpdatedDistanceUnit,
getRate,
getRateByCustomUnitRateID,
getDistanceForDisplayLabel,
};

export type {MileageRate};
1 change: 1 addition & 0 deletions src/pages/iou/request/step/IOURequestStepDistance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ function IOURequestStepDistance({
waypoints={waypoints}
navigateToWaypointEditPage={navigateToWaypointEditPage}
transaction={transaction}
policy={policy}
/>
}
/>
Expand Down
13 changes: 13 additions & 0 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4001,6 +4001,19 @@ const styles = (theme: ThemeColors) =>
...wordBreak.breakWord,
},

distanceLabelWrapper: {
backgroundColor: colors.green500,
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 4,
textAlign: 'center',
},
distanceLabelText: {
fontSize: 13,
fontWeight: FontUtils.fontWeight.bold,
color: colors.productLight100,
},

productTrainingTooltipWrapper: {
backgroundColor: theme.tooltipHighlightBG,
borderRadius: variables.componentBorderRadiusNormal,
Expand Down