Repository: Aymane-Arfaoui/FindMyClass
Pull request #303: Fix Time Estimation for Google Maps API
Author: login: Domat99
Labels: name: backend
Created: 2025-03-24T21:35:20Z | Merged: 2025-03-28T20:25:24Z

This is the fix for the issue #275  to be able to get the estimated time and destination address when getting directions to a location.

![image](https://github.com/user-attachments/assets/51469482-24aa-4116-8489-9c3b99024a53)

 


Diff:
diff --git a/app/homemap.jsx b/app/homemap.jsx
index 0cebfe4b..b6910be8 100644
--- a/app/homemap.jsx
+++ b/app/homemap.jsx
@@ -30,13 +30,11 @@ export default function Homemap() {
     const [centerCoordinate, setCenterCoordinate] = useState([-73.5789, 45.4960]);
     const cameraRef = useRef(null);
     const [isDirectionsView, setIsDirectionsView] = useState(false);
-    const [routeDetails, setRouteDetails] = useState(null);
     const [modeSelected, setModeSelected] = useState('walking');
     const panelY = useRef(new Animated.Value(500)).current;
     const [currentOrigin, setCurrentOrigin] = useState(null);
     const [currentDestination, setCurrentDestination] = useState(null);
     const [places, setPlaces] = useState([]);
-    const [selectedCategory, setSelectedCategory] = useState(null);
 
     useEffect(() => {
         let isMounted = true;
@@ -81,48 +79,90 @@ export default function Homemap() {
     }, [selectedLocation, modeSelected]);
 
     async function fetchAllModesData(originStr, destinationStr) {
-        const modes = ['driving', 'transit', 'walking', 'bicycling'];
+        const modes = ['DRIVE', 'TRANSIT', 'WALK', 'BICYCLE'];
         const updatedTravelTimes = {
-            driving: 'N/A',
-            transit: 'N/A',
-            walking: 'N/A',
-            bicycling: 'N/A',
+            DRIVE: 'N/A',
+            TRANSIT: 'N/A',
+            WALK: 'N/A',
+            BICYCLE: 'N/A',
         };
 
+        const API_URL = "https://routes.googleapis.com/directions/v2:computeRoutes";
+
+        // Convert string coordinates to numbers
+        const [originLat, originLng] = originStr.split(",").map(Number);
+        const [destinationLat, destinationLng] = destinationStr.split(",").map(Number);
+
         await Promise.all(
             modes.map(async (mode) => {
-                const url = `https://maps.googleapis.com/maps/api/directions/json
-        ?origin=${encodeURIComponent(originStr)}
-        &destination=${encodeURIComponent(destinationStr)}
-        &mode=${mode}
-        &alternatives=true
-        &key=${GOOGLE_PLACES_API_KEY}`
-                    .replace(/\s+/g, '');
+                const requestBody = {
+                    origin: {
+                        location: {
+                            latLng: { latitude: originLat, longitude: originLng }
+                        }
+                    },
+                    destination: {
+                        location: {
+                            latLng: { latitude: destinationLat, longitude: destinationLng }
+                        }
+                    },
+                    travelMode: mode, // DRIVE, TRANSIT, WALK, BICYCLE
+                    computeAlternativeRoutes: true,
+                    languageCode: "en-US",
+                    units: "METRIC"
+                };
+
+                // Only add routingPreference for DRIVE mode
+                if (mode === "DRIVE") {
+                    requestBody.routingPreference = "TRAFFIC_AWARE";
+                }
 
                 try {
-                    const resp = await fetch(url);
+                    const resp = await fetch(API_URL, {
+                        method: "POST",
+                        headers: {
+                            "Content-Type": "application/json",
+                            "X-Goog-Api-Key": GOOGLE_PLACES_API_KEY,
+                            "X-Goog-FieldMask": "routes.legs.duration"
+                        },
+                        body: JSON.stringify(requestBody)
+                    });
+
+                    if (!resp.ok) {
+                        const errorText = await resp.text();
+                        console.error(`HTTP Error: ${resp.status} - ${errorText}`);
+                        return;
+                    }
+
                     const data = await resp.json();
-                    if (data.routes && data.routes.length > 0) {
-                        const bestRoute = data.routes.reduce((shortest, cur) =>
-                            cur.legs[0].duration.value < shortest.legs[0].duration.value
-                                ? cur
-                                : shortest
-                        );
-                        const durSec = bestRoute.legs[0].duration.value;
-                        const hours = Math.floor(durSec / 3600);
-                        const minutes = Math.ceil((durSec % 3600) / 60);
-
-                        let label;
-                        if (hours > 0) {
-                            label = `${hours}h ${minutes} min`;
-                        } else {
-                            label = `${minutes} min`;
-                        }
 
-                        updatedTravelTimes[mode] = label;
+                    if (!data.routes || data.routes.length === 0) {
+                        console.warn(`No valid routes returned for mode: ${mode}`);
+                        return;
+                    }
+
+                    // Find the shortest route based on duration
+                    const bestRoute = data.routes.reduce((shortest, cur) => {
+                        const shortestDuration = parseInt(shortest.legs[0].duration.replace("s", ""), 10);
+                        const currentDuration = parseInt(cur.legs[0].duration.replace("s", ""), 10);
+                        return currentDuration < shortestDuration ? cur : shortest;
+                    });
+
+                    if (!bestRoute.legs || bestRoute.legs.length === 0) {
+                        console.warn(`No duration data available for mode: ${mode}`);
+                        return;
                     }
+
+                    // Extract numeric duration value
+                    const durSec = parseInt(bestRoute.legs[0].duration.replace("s", ""), 10);
+
+                    const hours = Math.floor(durSec / 3600);
+                    const minutes = Math.ceil((durSec % 3600) / 60);
+
+                    updatedTravelTimes[mode] = hours > 0 ? `${hours}h ${minutes} min` : `${minutes} min`;
+
                 } catch (err) {
-                    console.error(`Error fetching ${mode}`, err);
+                    console.error(`Failed to fetch ${mode}: ${err.message}`);
                 }
             })
         );
@@ -148,20 +188,13 @@ export default function Homemap() {
                 setRoutes(routes);
                 setFastestRoute(routes[0]);
 
-                setRouteDetails({
-                    distance: routes[0].distance,
-                    duration: routes[0].duration
-                });
-
             } else {
                 setRoutes([]);
                 setFastestRoute(null);
-                setRouteDetails(null);
             }
         } catch (error) {
             setRoutes([]);
             setFastestRoute(null);
-            setRouteDetails(null);
         } finally {
             setLoading(false);
         }
@@ -502,7 +535,6 @@ export default function Homemap() {
                     {/* Filter Button (Now Functional) */}
                     <PlaceFilterButtons
                         onSelectCategory={(category) => {
-                            setSelectedCategory(category);
                             if (category) {
                                 fetchPlacesOfInterest(category);
                             } else {
@@ -513,23 +545,6 @@ export default function Homemap() {
                 </View>
             )}
 
-
-            {/*{!isDirectionsView && (*/}
-            {/*    <View style={styles.filterButtonsContainer}>*/}
-            {/*        <PlaceFilterButtons*/}
-            {/*            onSelectCategory={(category) => {*/}
-            {/*                setSelectedCategory(category);*/}
-            {/*                if (category) {*/}
-            {/*                    fetchPlacesOfInterest(category);*/}
-            {/*                } else {*/}
-            {/*                    setPlaces([]);*/}
-            {/*                }*/}
-            {/*            }}*/}
-            {/*        />*/}
-            {/*    </View>*/}
-
-            {/*)}*/}
-
             {!isDirectionsView && (
                 <View style={styles.mapButtonsContainer}>
                     <MapButtons
@@ -573,17 +588,17 @@ export default function Homemap() {
                 <>
                     <SearchBars
                         currentLocation={currentLocation}
-                        destination={buildingDetails?.formatted_address}
+                        destination={buildingDetails?.formattedAddress}
                         onBackPress={() => switchToRegularMapView(false)}
                         modeSelected={modeSelected}
                         setModeSelected={setModeSelected}
                         travelTimes={travelTimes}
-                        setTravelTimes={setTravelTimes}
                     />
                     <BottomPanel
                         transportMode={modeSelected}
                         routeDetails={fastestRoute}
                         routes={routes}
+                        travelTimes={travelTimes}
                     />
                 </>
             )}
diff --git a/components/BottomPanel.jsx b/components/BottomPanel.jsx
index f4c667f4..cddc2f77 100644
--- a/components/BottomPanel.jsx
+++ b/components/BottomPanel.jsx
@@ -4,7 +4,7 @@ import {FontAwesome} from "@expo/vector-icons";
 import {theme} from "@/constants/theme";
 import {useRouter} from "expo-router";
 import PropTypes from "prop-types";
-const BottomPanel = ({transportMode, routeDetails, routes}) => {
+const BottomPanel = ({transportMode, routeDetails, routes, travelTimes}) => {
     const [expanded, setExpanded] = useState(false);
     const animatedHeight = useState(new Animated.Value(100))[0];
     const [selectedRoute, setSelectedRoute] = useState(null);
@@ -35,6 +35,15 @@ const BottomPanel = ({transportMode, routeDetails, routes}) => {
     const shuttleRoutes = routes?.filter((r) => r.mode === "shuttle");
     const otherTransitRoutes = routes?.filter((r) => r.mode !== "shuttle");
 
+    const modeMapping = {
+        driving: "DRIVE",
+        transit: "TRANSIT",
+        walking: "WALK",
+        bicycling: "BICYCLE",
+    };
+
+    const matchedTime = selectedRoute ? travelTimes[modeMapping[selectedRoute.mode]] : null;
+
 
     return (
         <Animated.View style={[styles.container, {height: animatedHeight}]} testID={'bottom-panel'}>
@@ -43,7 +52,7 @@ const BottomPanel = ({transportMode, routeDetails, routes}) => {
                 {routeDetails ? (
                     <View testID={'route-details'}>
                         <Text style={styles.text}>
-                            {selectedRoute ? `Duration: ${selectedRoute.duration}` : `Duration: ${routeDetails?.duration || 'N/A'}`}
+                            Duration: {matchedTime || selectedRoute?.duration || routeDetails?.duration || "N/A"}
                         </Text>
                         <Text style={styles.subText}>
                             {selectedRoute ? `Distance: ${selectedRoute.distance}` : `Distance: ${routeDetails?.distance || 'N/A'}`}
@@ -170,7 +179,8 @@ const BottomPanel = ({transportMode, routeDetails, routes}) => {
 BottomPanel.propTypes={
     transportMode:PropTypes.string,
     routeDetails:PropTypes.object,
-    routes:PropTypes.array
+    routes:PropTypes.array,
+    travelTimes: PropTypes.object
 }
 
 
@@ -369,5 +379,4 @@ const styles = StyleSheet.create({
 
 });
 
-export default BottomPanel;
-
+export default BottomPanel;
\ No newline at end of file
diff --git a/components/SearchBars.jsx b/components/SearchBars.jsx
index 51baf29a..3056d76f 100644
--- a/components/SearchBars.jsx
+++ b/components/SearchBars.jsx
@@ -15,8 +15,7 @@ const SearchBars = ({
                         onBackPress,
                         modeSelected,
                         setModeSelected,
-                        travelTimes,
-                        setTravelTimes
+                        travelTimes
 
                     }) => {
 
@@ -43,70 +42,8 @@ const SearchBars = ({
                 });
         }
     }, [currentLocation]);
-    const fetchRoutesData = async (originAddress, destinationAddress) => {
-        const modes = ['driving', 'transit', 'walking', 'bicycling'];
-        const updatedTravelTimes = {};
-
-        await Promise.all(
-            modes.map(async (mode) => {
-                try {
-                    const url = `https://maps.googleapis.com/maps/api/directions/json
-          ?origin=${encodeURIComponent(originAddress)}
-          &destination=${encodeURIComponent(destinationAddress)}
-          &mode=${mode}
-          &alternatives=true
-          &key=${GOOGLE_API_KEY}`.replace(/\s+/g, '');
-
-                    const response = await fetch(url);
-                    const data = await response.json();
-
-                    if (data.routes && data.routes.length > 0) {
-                        const bestRoute = data.routes.reduce((shortest, cur) => {
-                            return (cur.legs[0].duration.value < shortest.legs[0].duration.value)
-                                ? cur
-                                : shortest;
-                        });
-
-                        const durationSec = bestRoute.legs[0].duration.value;
-                        const hours = Math.floor(durationSec / 3600);
-                        const minutes = Math.ceil((durationSec % 3600) / 60);
-
-                        let label;
-                        if (hours > 0) {
-                            label = `${hours}h ${minutes} min`;
-                        } else {
-                            label = `${minutes} min`;
-                        }
-
-                        updatedTravelTimes[mode] = label;
-                    } else {
-                        updatedTravelTimes[mode] = 'N/A';
-                    }
-                } catch (error) {
-                    console.error(`Error fetching ${mode} route:`, error);
-                    updatedTravelTimes[mode] = 'Error';
-                }
-            })
-        );
-
-        return updatedTravelTimes;
-    };
 
 
-    useEffect(() => {
-        if (
-            startLocation &&
-            endLocation &&
-            startLocation !== 'Fetching current location...' &&
-            endLocation !== 'Destination'
-        ) {
-            (async () => {
-                const times = await fetchRoutesData(startLocation, endLocation);
-                setTravelTimes(times);
-            })();
-        }
-    }, [startLocation, endLocation]);
-
     const [suggestions, setSuggestions] = useState([]);
     const handleAddressChange = (text, isStart) => {
         if (text.length > 2) {
@@ -126,10 +63,6 @@ const SearchBars = ({
         isStart ? setStartLocation(text) : setEndLocation(text);
     };
 
-    const handleSuggestionSelect = (suggestion, isStart) => {
-        isStart ? setStartLocation(suggestion.description) : setEndLocation(suggestion.description);
-        setSuggestions([]);
-    };
 
     return (
         <View style={styles.container} testID={'search-bars'}>
@@ -228,7 +161,6 @@ SearchBars.propTypes={
     onBackPress:PropTypes.func,
     modeSelected:PropTypes.string,
     setModeSelected:PropTypes.func,
-    travelTimes:PropTypes.object,
-    setTravelTimes:PropTypes.func
+    travelTimes:PropTypes.object
 }
-export default SearchBars;
+export default SearchBars;
\ No newline at end of file
diff --git a/components/TransportOptions.jsx b/components/TransportOptions.jsx
index 1bd03474..8b53f414 100644
--- a/components/TransportOptions.jsx
+++ b/components/TransportOptions.jsx
@@ -1,16 +1,16 @@
 import {Pressable, StyleSheet, Text, View} from 'react-native';
 import React from 'react';
 import Icon from 'react-native-vector-icons/FontAwesome5';
-import {theme} from '../constants/theme';
-import {hp, wp} from '../helpers/common';
+import {theme} from '@/constants/theme';
+import {hp, wp} from '@/helpers/common';
 import PropTypes from "prop-types";
 
-const TransportOptions = ({modeSelected, setModeSelected, travelTimes}) => {
+const TransportOptions = ({ modeSelected, setModeSelected, travelTimes }) => {
     const transportModes = [
-        {mode: 'driving', icon: 'car', time: travelTimes.driving},
-        {mode: 'transit', icon: 'bus', time: travelTimes.transit},
-        {mode: 'walking', icon: 'walking', time: travelTimes.walking},
-        {mode: 'bicycling', icon: 'bicycle', time: travelTimes.bicycling},
+        { mode: 'driving', icon: 'car', time: travelTimes.DRIVE },
+        { mode: 'transit', icon: 'bus', time: travelTimes.TRANSIT },
+        { mode: 'walking', icon: 'walking', time: travelTimes.WALK },
+        { mode: 'bicycling', icon: 'bicycle', time: travelTimes.BICYCLE },
     ];
     return (
         <View style={styles.container} testID={'transport-options'}>
@@ -93,8 +93,4 @@ const styles = StyleSheet.create({
         color: theme.colors.primary,
         fontWeight: 'bold',
     },
-});
-
-
-
-
+});
\ No newline at end of file


Reviews:
user: login: RawdaWaez
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T19:39:13Z
user: login: RawdaWaez
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T19:40:48Z
user: login: RawdaWaez
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T19:44:40Z
user: login: Domat99
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T19:46:51Z
user: login: Domat99
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T19:47:34Z
user: login: Domat99
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T19:48:23Z
user: login: RawdaWaez
body: Looks good! The errors has been resolved!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-28T20:24:56Z

Review comments:
diff_hunk: @@ -4,7 +4,7 @@ import {FontAwesome} from "@expo/vector-icons";
 import {theme} from "@/constants/theme";
 import {useRouter} from "expo-router";
 import PropTypes from "prop-types";
-const BottomPanel = ({transportMode, routeDetails, routes}) => {
+const BottomPanel = ({transportMode, routeDetails, routes, travelTimes}) => {
path: components/BottomPanel.jsx
user: login: RawdaWaez
body: travelTimes is missing in the props validation. Please consider adding it.
created_at: 2025-03-28T19:39:13Z
author_association: COLLABORATOR
diff_hunk: @@ -43,7 +52,7 @@ const BottomPanel = ({transportMode, routeDetails, routes}) => {
                 {routeDetails ? (
                     <View testID={'route-details'}>
                         <Text style={styles.text}>
-                            {selectedRoute ? `Duration: ${selectedRoute.duration}` : `Duration: ${routeDetails?.duration || 'N/A'}`}
+                            Duration: {matchedTime ? matchedTime : (selectedRoute?.duration || routeDetails?.duration || "N/A")}
path: components/BottomPanel.jsx
user: login: RawdaWaez
body: It is unnecessary to add a condition for the default assignment. Please consider removing it.
created_at: 2025-03-28T19:40:48Z
author_association: COLLABORATOR
diff_hunk: @@ -231,4 +164,4 @@ SearchBars.propTypes={
     travelTimes:PropTypes.object,
     setTravelTimes:PropTypes.func
path: components/SearchBars.jsx
user: login: RawdaWaez
body: Please consider removing setTravelTimes from props since you are not using it.
created_at: 2025-03-28T19:44:40Z
author_association: COLLABORATOR
diff_hunk: @@ -4,7 +4,7 @@ import {FontAwesome} from "@expo/vector-icons";
 import {theme} from "@/constants/theme";
 import {useRouter} from "expo-router";
 import PropTypes from "prop-types";
-const BottomPanel = ({transportMode, routeDetails, routes}) => {
+const BottomPanel = ({transportMode, routeDetails, routes, travelTimes}) => {
path: components/BottomPanel.jsx
user: login: Domat99
body: Good note, I will add it.
created_at: 2025-03-28T19:46:51Z
author_association: COLLABORATOR
diff_hunk: @@ -43,7 +52,7 @@ const BottomPanel = ({transportMode, routeDetails, routes}) => {
                 {routeDetails ? (
                     <View testID={'route-details'}>
                         <Text style={styles.text}>
-                            {selectedRoute ? `Duration: ${selectedRoute.duration}` : `Duration: ${routeDetails?.duration || 'N/A'}`}
+                            Duration: {matchedTime ? matchedTime : (selectedRoute?.duration || routeDetails?.duration || "N/A")}
path: components/BottomPanel.jsx
user: login: Domat99
body: Yes that makes sense. I will fix it.
created_at: 2025-03-28T19:47:34Z
author_association: COLLABORATOR
diff_hunk: @@ -231,4 +164,4 @@ SearchBars.propTypes={
     travelTimes:PropTypes.object,
     setTravelTimes:PropTypes.func
path: components/SearchBars.jsx
user: login: Domat99
body: Will do. Thanks for the note.
created_at: 2025-03-28T19:48:23Z
author_association: COLLABORATOR

PR comments:
user: login: Pluhs
created_at: 2025-03-25T05:04:03Z
body: Looks good, I have nothing to say about the pr as of now.
author_association: COLLABORATOR
user: login: sonarqubecloud[bot]
created_at: 2025-03-28T20:22:15Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=Aymane-Arfaoui_FindMyClass&pullRequest=303) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [35.8% Coverage on New Code](https://sonarcloud.io/component_measures?id=Aymane-Arfaoui_FindMyClass&pullRequest=303&metric=new_coverage&view=list) (required ≥ 80%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=Aymane-Arfaoui_FindMyClass&pullRequest=303)


author_association: NONE