Repository: AlimuratDinch/minicap
Pull request #132: feat: determine start building based on location
Author: login: Azmi-21
Created: 2025-04-03T18:46:07Z | Merged: 2025-04-06T02:37:21Z

# Objectives of the Pull Request ? 
> This PR implements a feature that allows users to set their current building as the start location in the campus navigation system. This enhances user experience by automatically detecting which Concordia building the user is in, eliminating the need for manual building selection.

# What is left out of the Pull Request ? 
> Handling of edge cases where buildings overlap

# How to run the pull request ?
> Clone the branch and install dependencies:
```bash
git clone -b feature/determine_start_building_based_on_location 
cd minicap/application
npm install
```bash
# Assuming you are in a folder containing minicap_concordia_campus_guide_app/application folder.
# Update the code
git checkout feature/determine_start_building_based_on_location
git pull

# Run npx
npx expo run:android
```

# Checklist
> Please, read carefully each item before checking it. Your PR's review might be delayed otherwise.

* **Code** :
  * [x] I have added all necessary unit tests to demonstrate that my code functions as expected and is testable.
  * [x] The code I'm asking a review for is working. **I understand that my PR will be rejected as-is otherwise.**
  * [x] My code follows the conventions described in the `contribution guide`.
 
* **Documentation** : 
  * [ ]  I have updated the documentation (README) accordingly to my changes.
 
* **Pull-request** : 
  * [x]  My merge message is prefixed with a `commitizen` key word so it looks something like `<feat|fix|chore|doc|refactor|perf|style>: foo bar`**
  * [x]  The code *provided to run the pull request* is working. **I understand that my PR will be rejected as-is otherwise.**
  * [x]  My pull request is documented. I have explained the needs for the PR, what was left out of the it and why.
  * [x]  I have carefully reviewd each changes made to a file and made sure the files included on the PR were actually added on purpose.
  * [x]  I have assignee at least **1** people to review my PR.



Diff:
diff --git a/application/app/CampusMap.tsx b/application/app/CampusMap.tsx
index 1b9084ec..badff304 100644
--- a/application/app/CampusMap.tsx
+++ b/application/app/CampusMap.tsx
@@ -4,7 +4,7 @@ import MapView, { Polygon, Marker, PROVIDER_GOOGLE } from "react-native-maps";
 import * as Location from "expo-location";
 import SearchBar from './components/SearchBar';
 import BuildingPopup from "./components/BuildingPopup";
-import { parseCoordinates, getCenterFromCoordinates, mapCoordinates, Coordinate } from "./utils/coordinateHelpers";
+import { parseCoordinates, getCenterFromCoordinates, mapCoordinates, Coordinate, findBuildingAtLocation } from "./utils/coordinateHelpers";
 import { useBuildings } from './utils/useBuildings';
 
 interface Building {
@@ -21,12 +21,11 @@ export default function CampusMap() {
   const [selectedBuilding, setSelectedBuilding] = useState<Building | null>(null);
   const [showPopup, setShowPopup] = useState(false);
   const [showUserLocation, setShowUserLocation] = useState<boolean>(true);
+  const [userLocation, setUserLocation] = useState<Coordinate | null>(null);
 
   // Start & Destination
   const [startBuilding, setStartBuilding] = useState<Building | null>(null);
-  const [destinationBuilding, setDestinationBuilding] = useState<Building | null>(
-    null
-  );
+  const [destinationBuilding, setDestinationBuilding] = useState<Building | null>(null);
 
   // function to retreive the building
   const buildings = useBuildings();
@@ -105,9 +104,45 @@ export default function CampusMap() {
 
   }, [startBuilding]);
 
+  useEffect(() => {
+    getLocationPermission();
+    const getLocation = async () => {
+      try {
+        const location = await Location.getCurrentPositionAsync({});
+        setUserLocation({
+          latitude: location.coords.latitude,
+          longitude: location.coords.longitude
+        });
+      } catch (error) {
+        console.error("Error getting location:", error);
+      }
+    };
+
+    // Update location every 10 seconds
+    const locationInterval = setInterval(getLocation, 10000);
+    getLocation(); // Initial location fetch
+
+    return () => clearInterval(locationInterval);
+  }, []);
+
+  const handleSetCurrentLocationAsStart = async () => {
+    if (!userLocation) {
+      Alert.alert("Error", "Unable to get your current location. Please make sure location services are enabled.");
+      return;
+    }
 
+    const currentBuilding = findBuildingAtLocation(userLocation, buildings);
+    
+    if (!currentBuilding) {
+      Alert.alert("Not in a Building", "You must be inside a Concordia building to set it as your start location.");
+      return;
+    }
 
-const getLocationPermission = async () => {
+    setStartBuilding(currentBuilding);
+    Alert.alert("Success", `Set ${currentBuilding.BuildingName} as your start location.`);
+  };
+
+  const getLocationPermission = async () => {
     try {
         const permission = await Location.requestForegroundPermissionsAsync();
         if (!permission || !permission.status) {
@@ -121,8 +156,7 @@ const getLocationPermission = async () => {
     } catch (error) {
         console.error("Error requesting location permission:", error);
     }
-};
-
+  };
 
   // Toggle user location on the map
   const toggleUserLocation = () => {
@@ -219,6 +253,15 @@ const getLocationPermission = async () => {
         )}
       </MapView>
 
+      <View style={styles.buttonContainer}>
+        <TouchableOpacity
+          style={styles.button}
+          onPress={handleSetCurrentLocationAsStart}
+        >
+          <Text style={styles.buttonText}>Set Current Building as Start</Text>
+        </TouchableOpacity>
+      </View>
+
       <View style={styles.searchContainer}>
         <SearchBar
           data={buildingNames}
@@ -267,8 +310,6 @@ const getLocationPermission = async () => {
           </TouchableOpacity>
         </View>
       </View>
-
-
     </View>
   );
 }
@@ -293,6 +334,29 @@ const styles = StyleSheet.create({
     elevation: 5,
     borderWidth: 1,
   },
+  buttonContainer: {
+    position: 'absolute',
+    bottom: 60,
+    left: 20,
+    right: 20,
+    alignItems: 'center',
+  },
+  button: {
+    backgroundColor: '#900',
+    paddingHorizontal: 20,
+    paddingVertical: 10,
+    borderRadius: 5,
+    elevation: 3,
+    shadowColor: '#000',
+    shadowOffset: { width: 0, height: 2 },
+    shadowOpacity: 0.25,
+    shadowRadius: 3.84,
+  },
+  buttonText: {
+    color: 'white',
+    fontSize: 16,
+    fontWeight: 'bold',
+  },
 });
 
 const stylesButtons = StyleSheet.create({
@@ -357,4 +421,3 @@ const stylesButtons = StyleSheet.create({
     color: "#fff",
   },
 });
-
diff --git a/application/app/utils/coordinateHelpers.tsx b/application/app/utils/coordinateHelpers.tsx
index 3f3bf50e..e2070ba9 100644
--- a/application/app/utils/coordinateHelpers.tsx
+++ b/application/app/utils/coordinateHelpers.tsx
@@ -30,7 +30,6 @@ export const parseCoordinates = (coordinateString: string): Coordinate[] => {
 export const mapCoordinates = (coordinateString: string): Coordinate[] =>
   parseCoordinates(coordinateString);
 
-
 /**
  * Computes the center point from an array of coordinates.
  */
@@ -43,3 +42,35 @@ export const getCenterFromCoordinates = (coords: Coordinate[]): Coordinate | nul
     longitude: lngSum / coords.length,
   };
 };
+
+/**
+ * Checks if a point is inside a polygon using ray casting algorithm
+ */
+export const isPointInPolygon = (point: Coordinate, polygon: Coordinate[]): boolean => {
+  let inside = false;
+  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
+    const xi = polygon[i].latitude;
+    const yi = polygon[i].longitude;
+    const xj = polygon[j].latitude;
+    const yj = polygon[j].longitude;
+
+    const intersect = ((yi > point.longitude) !== (yj > point.longitude)) &&
+      (point.latitude < (xj - xi) * (point.longitude - yi) / (yj - yi) + xi);
+    
+    if (intersect) inside = !inside;
+  }
+  return inside;
+};
+
+/**
+ * Finds which building contains the given coordinate
+ */
+export const findBuildingAtLocation = (location: Coordinate, buildings: Building[]): Building | null => {
+  for (const building of buildings) {
+    const coordinates = parseCoordinates(building.Latitude_Longitude_Points);
+    if (isPointInPolygon(location, coordinates)) {
+      return building;
+    }
+  }
+  return null;
+};
diff --git a/application/components/__tests__/CurrentLocationAsStart.test.tsx b/application/components/__tests__/CurrentLocationAsStart.test.tsx
new file mode 100644
index 00000000..63014d69
--- /dev/null
+++ b/application/components/__tests__/CurrentLocationAsStart.test.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { render } from '@testing-library/react-native';
+import CampusMap from '../../app/CampusMap';
+
+// Mock react-native-maps
+jest.mock('react-native-maps', () => {
+  const React = require('react');
+  const MockMapView = (props: any) => React.createElement('MockMapView', { testID: 'map', ...props }, props.children);
+  const MockMarker = (props: any) => React.createElement('MockMarker', { testID: 'marker', ...props });
+  const MockPolygon = (props: any) => React.createElement('MockPolygon', { testID: 'polygon', ...props });
+  
+  return {
+    __esModule: true,
+    default: MockMapView,
+    Marker: MockMarker,
+    Polygon: MockPolygon,
+    PROVIDER_GOOGLE: 'PROVIDER_GOOGLE',
+  };
+});
+
+// Mock building data
+jest.mock('../../app/utils/useBuildings', () => ({
+  useBuildings: jest.fn(() => [
+    {
+      BuildingName: 'Hall Building',
+      'Building Long Name': 'Henry F. Hall Building',
+      Address: '1455 De Maisonneuve Blvd. W.',
+      Latitude_Longitude_Points: '45.497,-73.578;45.498,-73.578;45.498,-73.579;45.497,-73.579',
+      color: 'red',
+      strokeColor: 'black',
+    }
+  ]),
+}));
+
+// Mock expo-location
+jest.mock('expo-location', () => ({
+  requestForegroundPermissionsAsync: jest.fn(() => Promise.resolve({ status: 'granted' })),
+  getCurrentPositionAsync: jest.fn(),
+  LocationObject: {},
+  LocationAccuracy: {
+    Balanced: 3
+  },
+}));
+
+describe('Set Current Building as Start Feature', () => {
+  it('renders the "Set Current Building as Start" button', () => {
+    const { getByText } = render(<CampusMap />);
+    const button = getByText('Set Current Building as Start');
+    expect(button).toBeTruthy();
+  });
+});


Reviews:
user: login: AbdullahT01
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-05T17:15:09Z
user: login: AbdullahT01
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-05T17:20:08Z
user: login: AbdullahT01
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-05T17:25:54Z
user: login: Sawanoza
body: Great job! This feature enhances user experience by auto-detecting the current building as the start location, eliminating manual selection and streamlining navigation.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-06T02:33:00Z
user: login: JosephAladas
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-06T02:33:42Z
user: login: AlimuratDinch
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-07T01:31:09Z
user: login: AlimuratDinch
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-07T01:31:17Z
user: login: AlimuratDinch
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-07T01:31:23Z
user: login: AlimuratDinch
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-07T01:31:52Z

Review comments:
diff_hunk: @@ -293,6 +334,29 @@ const styles = StyleSheet.create({
     elevation: 5,
     borderWidth: 1,
   },
+  buttonContainer: {
+    position: 'absolute',
+    bottom: 60,
+    left: 20,
+    right: 20,
+    alignItems: 'center',
+  },
+  button: {
+    backgroundColor: '#900',
+    paddingHorizontal: 20,
+    paddingVertical: 10,
+    borderRadius: 5,
+    elevation: 3,
+    shadowColor: '#000',
+    shadowOffset: { width: 0, height: 2 },
+    shadowOpacity: 0.25,
+    shadowRadius: 3.84,
+  },
+  buttonText: {
+    color: 'white',
+    fontSize: 16,
+    fontWeight: 'bold',
+  },
path: application/app/CampusMap.tsx
user: login: AbdullahT01
body: This is just a small suggestion for future, if we decide to grow the application. It could be a good idea to group map-related styles (like button, buttonContainer) into a mapControls object to better organize styles as the screen grows.

for example: mapControls: {
  button: {...},
  container: {...}
}

created_at: 2025-04-05T17:15:08Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,51 @@
+import React from 'react';
+import { render } from '@testing-library/react-native';
+import CampusMap from '../../app/CampusMap';
+
path: application/components/__tests__/CurrentLocationAsStart.test.tsx
user: login: AbdullahT01
body: we could maybe add a quick check to make sure the mocked <MapView /> is actually rendering with getByTestId('map')

for example something like this: const { getByTestId } = render(<CampusMap />);
expect(getByTestId('map')).toBeTruthy();


created_at: 2025-04-05T17:20:07Z
author_association: COLLABORATOR
diff_hunk: @@ -105,9 +104,45 @@ export default function CampusMap() {
 
   }, [startBuilding]);
 
+  useEffect(() => {
+    getLocationPermission();
+    const getLocation = async () => {
+      try {
+        const location = await Location.getCurrentPositionAsync({});
+        setUserLocation({
+          latitude: location.coords.latitude,
+          longitude: location.coords.longitude
+        });
+      } catch (error) {
+        console.error("Error getting location:", error);
+      }
+    };
+
+    // Update location every 10 seconds
+    const locationInterval = setInterval(getLocation, 10000);
+    getLocation(); // Initial location fetch
+
+    return () => clearInterval(locationInterval);
+  }, []);
+
+  const handleSetCurrentLocationAsStart = async () => {
path: application/app/CampusMap.tsx
user: login: AbdullahT01
body: handleSetCurrentLocationAsStart, the variable seems to be a bit long. could be a good idea to rename it eventually during refactoring.

for example: setStartLocation


created_at: 2025-04-05T17:25:54Z
author_association: COLLABORATOR
diff_hunk: @@ -43,3 +42,35 @@ export const getCenterFromCoordinates = (coords: Coordinate[]): Coordinate | nul
     longitude: lngSum / coords.length,
   };
 };
+
+/**
+ * Checks if a point is inside a polygon using ray casting algorithm
+ */
+export const isPointInPolygon = (point: Coordinate, polygon: Coordinate[]): boolean => {
+  let inside = false;
+  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
+    const xi = polygon[i].latitude;
+    const yi = polygon[i].longitude;
+    const xj = polygon[j].latitude;
+    const yj = polygon[j].longitude;
+
+    const intersect = ((yi > point.longitude) !== (yj > point.longitude)) &&
path: application/app/utils/coordinateHelpers.tsx
user: login: JosephAladas
body: latitude typically represents y and longitude represents x in geographic systems. Add a comment clarifying the coordinate system or adjust the variable names if it is a mistake.
created_at: 2025-04-05T23:33:40Z
author_association: COLLABORATOR
diff_hunk: @@ -105,9 +104,45 @@ export default function CampusMap() {
 
   }, [startBuilding]);
 
+  useEffect(() => {
+    getLocationPermission();
+    const getLocation = async () => {
+      try {
+        const location = await Location.getCurrentPositionAsync({});
+        setUserLocation({
+          latitude: location.coords.latitude,
+          longitude: location.coords.longitude
+        });
+      } catch (error) {
+        console.error("Error getting location:", error);
+      }
+    };
+
+    // Update location every 10 seconds
+    const locationInterval = setInterval(getLocation, 10000);
path: application/app/CampusMap.tsx
user: login: JosephAladas
body: Great job cleaning up the interval! If u agree, you can add a comment explaining why 10 seconds was chosen as the interval, or make it a configurable constant (e.g., const LOCATION_UPDATE_INTERVAL = 10000) for easier tuning later.
created_at: 2025-04-05T23:38:59Z
author_association: COLLABORATOR
diff_hunk: @@ -293,6 +334,29 @@ const styles = StyleSheet.create({
     elevation: 5,
     borderWidth: 1,
   },
+  buttonContainer: {
+    position: 'absolute',
+    bottom: 60,
+    left: 20,
+    right: 20,
+    alignItems: 'center',
+  },
+  button: {
+    backgroundColor: '#900',
+    paddingHorizontal: 20,
+    paddingVertical: 10,
+    borderRadius: 5,
+    elevation: 3,
+    shadowColor: '#000',
+    shadowOffset: { width: 0, height: 2 },
+    shadowOpacity: 0.25,
+    shadowRadius: 3.84,
+  },
+  buttonText: {
+    color: 'white',
+    fontSize: 16,
+    fontWeight: 'bold',
+  },
path: application/app/CampusMap.tsx
user: login: AlimuratDinch
body: Because of the time restriction, i will put this issue as `WON'T FIX`
created_at: 2025-04-07T01:31:09Z
author_association: OWNER
diff_hunk: @@ -105,9 +104,45 @@ export default function CampusMap() {
 
   }, [startBuilding]);
 
+  useEffect(() => {
+    getLocationPermission();
+    const getLocation = async () => {
+      try {
+        const location = await Location.getCurrentPositionAsync({});
+        setUserLocation({
+          latitude: location.coords.latitude,
+          longitude: location.coords.longitude
+        });
+      } catch (error) {
+        console.error("Error getting location:", error);
+      }
+    };
+
+    // Update location every 10 seconds
+    const locationInterval = setInterval(getLocation, 10000);
+    getLocation(); // Initial location fetch
+
+    return () => clearInterval(locationInterval);
+  }, []);
+
+  const handleSetCurrentLocationAsStart = async () => {
path: application/app/CampusMap.tsx
user: login: AlimuratDinch
body: Because of the time restriction, i will put this issue as `WON'T FIX`
created_at: 2025-04-07T01:31:17Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,51 @@
+import React from 'react';
+import { render } from '@testing-library/react-native';
+import CampusMap from '../../app/CampusMap';
+
path: application/components/__tests__/CurrentLocationAsStart.test.tsx
user: login: AlimuratDinch
body: Because of the time restriction, i will put this issue as `WON'T FIX`
created_at: 2025-04-07T01:31:23Z
author_association: OWNER
diff_hunk: @@ -43,3 +42,35 @@ export const getCenterFromCoordinates = (coords: Coordinate[]): Coordinate | nul
     longitude: lngSum / coords.length,
   };
 };
+
+/**
+ * Checks if a point is inside a polygon using ray casting algorithm
+ */
+export const isPointInPolygon = (point: Coordinate, polygon: Coordinate[]): boolean => {
+  let inside = false;
+  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
+    const xi = polygon[i].latitude;
+    const yi = polygon[i].longitude;
+    const xj = polygon[j].latitude;
+    const yj = polygon[j].longitude;
+
+    const intersect = ((yi > point.longitude) !== (yj > point.longitude)) &&
path: application/app/utils/coordinateHelpers.tsx
user: login: AlimuratDinch
body: Because of the time restriction, i will put this issue as `WON'T FIX`
created_at: 2025-04-07T01:31:52Z
author_association: OWNER

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-03T18:46:32Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=AlimuratDinch_minicap_concordia_campus_guide_app&pullRequest=132) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=AlimuratDinch_minicap_concordia_campus_guide_app&pullRequest=132&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=AlimuratDinch_minicap_concordia_campus_guide_app&pullRequest=132&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=AlimuratDinch_minicap_concordia_campus_guide_app&pullRequest=132&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=AlimuratDinch_minicap_concordia_campus_guide_app&pullRequest=132&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=AlimuratDinch_minicap_concordia_campus_guide_app&pullRequest=132&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=AlimuratDinch_minicap_concordia_campus_guide_app&pullRequest=132)


author_association: NONE
user: login: AlimuratDinch
created_at: 2025-04-03T19:10:35Z
body: @Azmi-21 The name of the PR should be `feat: determine start building based on location`. I wll review it once i get home
author_association: OWNER
user: login: ryad-all
created_at: 2025-04-03T20:27:09Z
body: Looks like one or more tests in this PR are failing because Location.getCurrentPositionAsync is undefined during test execution:
```
TypeError: Location.getCurrentPositionAsync is not a function
```
I suggest using a mock to simulate the return value of location.

author_association: COLLABORATOR