Skip to content

Commit

Permalink
Merge pull request #55517 from truph01/feat/53421
Browse files Browse the repository at this point in the history
feat: Add distance label on map route
  • Loading branch information
Gonals authored Feb 5, 2025
2 parents 81bdb03 + 5632ffd commit 07809b6
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 21 deletions.
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);

// 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>
</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

0 comments on commit 07809b6

Please sign in to comment.