diff --git a/src/hooks/useLocationBias.ts b/src/hooks/useLocationBias.ts new file mode 100644 index 000000000000..b95ffbb57e9d --- /dev/null +++ b/src/hooks/useLocationBias.ts @@ -0,0 +1,52 @@ +import {useMemo} from 'react'; + +/** + * Construct the rectangular boundary based on user location and waypoints + */ +export default function useLocationBias(allWaypoints: Record, userLocation?: {latitude: number; longitude: number}) { + return useMemo(() => { + const hasFilledWaypointCount = Object.values(allWaypoints).some((waypoint) => Object.keys(waypoint).length > 0); + // If there are no filled wayPoints and if user's current location cannot be retrieved, + // it is futile to arrive at a biased location. Let's return + if (!hasFilledWaypointCount && userLocation === undefined) { + return null; + } + + // Gather the longitudes and latitudes from filled waypoints. + const longitudes: number[] = Object.values(allWaypoints).reduce((accum: number[], waypoint) => { + if (waypoint?.lng) { + accum.push(waypoint.lng); + } + return accum; + }, []); + const latitudes: number[] = Object.values(allWaypoints).reduce((accum: number[], waypoint) => { + if (waypoint?.lat) { + accum.push(waypoint.lat); + } + return accum; + }, []); + + // When no filled waypoints are available but the current location of the user is available, + // let us consider the current user's location to construct a rectangular bound + if (!hasFilledWaypointCount && userLocation !== undefined) { + longitudes.push(userLocation.longitude); + latitudes.push(userLocation.latitude); + } + + // Extend the rectangular bound by 0.5 degree (roughly around 25-30 miles in US) + const minLat = Math.min(...latitudes) - 0.5; + const minLng = Math.min(...longitudes) - 0.5; + const maxLat = Math.max(...latitudes) + 0.5; + const maxLng = Math.max(...longitudes) + 0.5; + + // Ensuring coordinates do not go out of range. + const south = minLat > -90 ? minLat : -90; + const west = minLng > -180 ? minLng : -180; + const north = maxLat < 90 ? maxLat : 90; + const east = maxLng < 180 ? maxLng : 180; + + // Format: rectangle:south,west|north,east + const rectFormat = `rectangle:${south},${west}|${north},${east}`; + return rectFormat; + }, [userLocation, allWaypoints]); +} diff --git a/src/pages/iou/WaypointEditor.js b/src/pages/iou/WaypointEditor.js index e8d3c8520ca8..ab8874091152 100644 --- a/src/pages/iou/WaypointEditor.js +++ b/src/pages/iou/WaypointEditor.js @@ -14,6 +14,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import ScreenWrapper from '@components/ScreenWrapper'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; +import useLocationBias from '@hooks/useLocationBias'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -43,6 +44,15 @@ const propTypes = { }), }), + /* Current location coordinates of the user */ + userLocation: PropTypes.shape({ + /** Latitude of the location */ + latitude: PropTypes.number, + + /** Longitude of the location */ + longitude: PropTypes.number, + }), + recentWaypoints: PropTypes.arrayOf( PropTypes.shape({ /** The name of the location */ @@ -74,9 +84,10 @@ const defaultProps = { route: {}, recentWaypoints: [], transaction: {}, + userLocation: undefined, }; -function WaypointEditor({route: {params: {iouType = '', transactionID = '', waypointIndex = '', threadReportID = 0}} = {}, transaction, recentWaypoints}) { +function WaypointEditor({route: {params: {iouType = '', transactionID = '', waypointIndex = '', threadReportID = 0}} = {}, transaction, recentWaypoints, userLocation}) { const styles = useThemeStyles(); const {windowWidth} = useWindowDimensions(); const [isDeleteStopModalOpen, setIsDeleteStopModalOpen] = useState(false); @@ -91,7 +102,7 @@ function WaypointEditor({route: {params: {iouType = '', transactionID = '', wayp const waypointCount = _.size(allWaypoints); const filledWaypointCount = _.size(_.filter(allWaypoints, (waypoint) => !_.isEmpty(waypoint))); - + const locationBias = useLocationBias(allWaypoints, userLocation); const waypointDescriptionKey = useMemo(() => { switch (parsedWaypointIndex) { case 0: @@ -221,6 +232,7 @@ function WaypointEditor({route: {params: {iouType = '', transactionID = '', wayp > (textInput.current = e)} @@ -254,6 +266,9 @@ WaypointEditor.displayName = 'WaypointEditor'; WaypointEditor.propTypes = propTypes; WaypointEditor.defaultProps = defaultProps; export default withOnyx({ + userLocation: { + key: ONYXKEYS.USER_LOCATION, + }, transaction: { key: ({route}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(route, 'params.transactionID')}`, }, diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.js b/src/pages/iou/request/step/IOURequestStepWaypoint.js index 73d5b37e72ee..09617026576d 100644 --- a/src/pages/iou/request/step/IOURequestStepWaypoint.js +++ b/src/pages/iou/request/step/IOURequestStepWaypoint.js @@ -1,6 +1,5 @@ import {useNavigation} from '@react-navigation/native'; import lodashGet from 'lodash/get'; -import lodashIsNil from 'lodash/isNil'; import PropTypes from 'prop-types'; import React, {useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; @@ -16,6 +15,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import ScreenWrapper from '@components/ScreenWrapper'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; +import useLocationBias from '@hooks/useLocationBias'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -97,7 +97,6 @@ function IOURequestStepWaypoint({ const parsedWaypointIndex = parseInt(pageIndex, 10); const allWaypoints = lodashGet(transaction, 'comment.waypoints', {}); const currentWaypoint = lodashGet(allWaypoints, `waypoint${pageIndex}`, {}); - const waypointCount = _.size(allWaypoints); const filledWaypointCount = _.size(_.filter(allWaypoints, (waypoint) => !_.isEmpty(waypoint))); @@ -112,58 +111,7 @@ function IOURequestStepWaypoint({ } }, [parsedWaypointIndex, waypointCount]); - // Construct the rectangular boundary based on user location and waypoints - const locationBias = useMemo(() => { - // If there are no filled wayPoints and if user's current location cannot be retrieved, - // it is futile to arrive at a biased location. Let's return - if (filledWaypointCount === 0 && _.isEmpty(userLocation)) { - return null; - } - - // Gather the longitudes and latitudes from filled waypoints. - const longitudes = _.filter( - _.map(allWaypoints, (waypoint) => { - if (!waypoint || lodashIsNil(waypoint.lng)) { - return; - } - return waypoint.lng; - }), - (lng) => lng, - ); - const latitudes = _.filter( - _.map(allWaypoints, (waypoint) => { - if (!waypoint || lodashIsNil(waypoint.lat)) { - return; - } - return waypoint.lat; - }), - (lat) => lat, - ); - - // When no filled waypoints are available but the current location of the user is available, - // let us consider the current user's location to construct a rectangular bound - if (filledWaypointCount === 0 && !_.isEmpty(userLocation)) { - longitudes.push(userLocation.longitude); - latitudes.push(userLocation.latitude); - } - - // Extend the rectangular bound by 0.5 degree (roughly around 25-30 miles in US) - const minLat = Math.min(...latitudes) - 0.5; - const minLng = Math.min(...longitudes) - 0.5; - const maxLat = Math.max(...latitudes) + 0.5; - const maxLng = Math.max(...longitudes) + 0.5; - - // Ensuring coordinates do not go out of range. - const south = minLat > -90 ? minLat : -90; - const west = minLng > -180 ? minLng : -180; - const north = maxLat < 90 ? maxLat : 90; - const east = maxLng < 180 ? maxLng : 180; - - // Format: rectangle:south,west|north,east - const rectFormat = `rectangle:${south},${west}|${north},${east}`; - return rectFormat; - }, [userLocation, filledWaypointCount, allWaypoints]); - + const locationBias = useLocationBias(allWaypoints, userLocation); const waypointAddress = lodashGet(currentWaypoint, 'address', ''); // Hide the menu when there is only start and finish waypoint const shouldShowThreeDotsButton = waypointCount > 2;