diff --git a/client/App.js b/client/App.js
index 94e25a6..b369e5c 100644
--- a/client/App.js
+++ b/client/App.js
@@ -21,7 +21,21 @@ export default function App() {
retrieveData('username') === null ? 'AccountScreen' : 'Map';
return (
-
+ {
+ switch (route.name) {
+ case 'FindRouteScreen':
+ return { animation: 'fade' }; // or 'slide_from_bottom' if supported
+ case 'Dashboard':
+ return { animation: 'slide_from_left' };
+ case 'FriendsScreen':
+ return { animation: 'slide_from_right' };
+ default:
+ return { animation: 'fade' };
+ }
+ }}
+ >
-
+
-
+
-
+
);
diff --git a/client/package-lock.json b/client/package-lock.json
index 70ae650..1a861dd 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -2641,9 +2641,6 @@
}
},
"node_modules/@expo/config": {
- "version": "10.0.11",
- "resolved": "https://registry.npmjs.org/@expo/config/-/config-10.0.11.tgz",
- "integrity": "sha512-nociJ4zr/NmbVfMNe9j/+zRlt7wz/siISu7PjdWE4WE+elEGxWWxsGzltdJG0llzrM+khx8qUiFK5aiVcdMBww==",
"version": "10.0.11",
"resolved": "https://registry.npmjs.org/@expo/config/-/config-10.0.11.tgz",
"integrity": "sha512-nociJ4zr/NmbVfMNe9j/+zRlt7wz/siISu7PjdWE4WE+elEGxWWxsGzltdJG0llzrM+khx8qUiFK5aiVcdMBww==",
@@ -2652,8 +2649,6 @@
"@babel/code-frame": "~7.10.4",
"@expo/config-plugins": "~9.0.17",
"@expo/config-types": "^52.0.5",
- "@expo/config-plugins": "~9.0.17",
- "@expo/config-types": "^52.0.5",
"@expo/json-file": "^9.0.2",
"deepmerge": "^4.3.1",
"getenv": "^1.0.0",
@@ -2745,9 +2740,6 @@
}
},
"node_modules/@expo/config-types": {
- "version": "52.0.5",
- "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-52.0.5.tgz",
- "integrity": "sha512-AMDeuDLHXXqd8W+0zSjIt7f37vUd/BP8p43k68NHpyAvQO+z8mbQZm3cNQVAMySeayK2XoPigAFB1JF2NFajaA==",
"version": "52.0.5",
"resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-52.0.5.tgz",
"integrity": "sha512-AMDeuDLHXXqd8W+0zSjIt7f37vUd/BP8p43k68NHpyAvQO+z8mbQZm3cNQVAMySeayK2XoPigAFB1JF2NFajaA==",
@@ -2784,28 +2776,6 @@
"xml2js": "0.6.0"
}
},
- "node_modules/@expo/config/node_modules/@expo/config-plugins": {
- "version": "9.0.17",
- "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-9.0.17.tgz",
- "integrity": "sha512-m24F1COquwOm7PBl5wRbkT9P9DviCXe0D7S7nQsolfbhdCWuvMkfXeoWmgjtdhy7sDlOyIgBrAdnB6MfsWKqIg==",
- "license": "MIT",
- "dependencies": {
- "@expo/config-types": "^52.0.5",
- "@expo/json-file": "~9.0.2",
- "@expo/plist": "^0.2.2",
- "@expo/sdk-runtime-versions": "^1.0.0",
- "chalk": "^4.1.2",
- "debug": "^4.3.5",
- "getenv": "^1.0.0",
- "glob": "^10.4.2",
- "resolve-from": "^5.0.0",
- "semver": "^7.5.4",
- "slash": "^3.0.0",
- "slugify": "^1.6.6",
- "xcode": "^3.0.1",
- "xml2js": "0.6.0"
- }
- },
"node_modules/@expo/config/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -4252,7 +4222,6 @@
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.1.2.tgz",
"integrity": "sha512-dvlNq4AlGWC+ehtH12p65+17V0Dx7IecOWl6WanF2ja38O1Dcjjvn7jVzkUHJ5oWkQBlyASurTPlTHgKXyYiow==",
"dev": true,
- "dev": true,
"license": "MIT",
"dependencies": {
"merge-options": "^3.0.4"
@@ -6671,9 +6640,6 @@
"license": "ISC"
},
"node_modules/@urql/core": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/@urql/core/-/core-5.1.1.tgz",
- "integrity": "sha512-aGh024z5v2oINGD/In6rAtVKTm4VmQ2TxKQBAtk2ZSME5dunZFcjltw4p5ENQg+5CBhZ3FHMzl0Oa+rwqiWqlg==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@urql/core/-/core-5.1.1.tgz",
"integrity": "sha512-aGh024z5v2oINGD/In6rAtVKTm4VmQ2TxKQBAtk2ZSME5dunZFcjltw4p5ENQg+5CBhZ3FHMzl0Oa+rwqiWqlg==",
@@ -7778,99 +7744,6 @@
"@babel/preset-env": "^7.1.6"
}
},
- "node_modules/babel-preset-expo/node_modules/@react-native/babel-plugin-codegen": {
- "version": "0.76.7",
- "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.76.7.tgz",
- "integrity": "sha512-+8H4DXJREM4l/pwLF/wSVMRzVhzhGDix5jLezNrMD9J1U1AMfV2aSkWA1XuqR7pjPs/Vqf6TaPL7vJMZ4LU05Q==",
- "license": "MIT",
- "dependencies": {
- "@react-native/codegen": "0.76.7"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/babel-preset-expo/node_modules/@react-native/babel-preset": {
- "version": "0.76.7",
- "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.76.7.tgz",
- "integrity": "sha512-/c5DYZ6y8tyg+g8tgXKndDT7mWnGmkZ9F+T3qNDfoE3Qh7ucrNeC2XWvU9h5pk8eRtj9l4SzF4aO1phzwoibyg==",
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.25.2",
- "@babel/plugin-proposal-export-default-from": "^7.24.7",
- "@babel/plugin-syntax-dynamic-import": "^7.8.3",
- "@babel/plugin-syntax-export-default-from": "^7.24.7",
- "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
- "@babel/plugin-syntax-optional-chaining": "^7.8.3",
- "@babel/plugin-transform-arrow-functions": "^7.24.7",
- "@babel/plugin-transform-async-generator-functions": "^7.25.4",
- "@babel/plugin-transform-async-to-generator": "^7.24.7",
- "@babel/plugin-transform-block-scoping": "^7.25.0",
- "@babel/plugin-transform-class-properties": "^7.25.4",
- "@babel/plugin-transform-classes": "^7.25.4",
- "@babel/plugin-transform-computed-properties": "^7.24.7",
- "@babel/plugin-transform-destructuring": "^7.24.8",
- "@babel/plugin-transform-flow-strip-types": "^7.25.2",
- "@babel/plugin-transform-for-of": "^7.24.7",
- "@babel/plugin-transform-function-name": "^7.25.1",
- "@babel/plugin-transform-literals": "^7.25.2",
- "@babel/plugin-transform-logical-assignment-operators": "^7.24.7",
- "@babel/plugin-transform-modules-commonjs": "^7.24.8",
- "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7",
- "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7",
- "@babel/plugin-transform-numeric-separator": "^7.24.7",
- "@babel/plugin-transform-object-rest-spread": "^7.24.7",
- "@babel/plugin-transform-optional-catch-binding": "^7.24.7",
- "@babel/plugin-transform-optional-chaining": "^7.24.8",
- "@babel/plugin-transform-parameters": "^7.24.7",
- "@babel/plugin-transform-private-methods": "^7.24.7",
- "@babel/plugin-transform-private-property-in-object": "^7.24.7",
- "@babel/plugin-transform-react-display-name": "^7.24.7",
- "@babel/plugin-transform-react-jsx": "^7.25.2",
- "@babel/plugin-transform-react-jsx-self": "^7.24.7",
- "@babel/plugin-transform-react-jsx-source": "^7.24.7",
- "@babel/plugin-transform-regenerator": "^7.24.7",
- "@babel/plugin-transform-runtime": "^7.24.7",
- "@babel/plugin-transform-shorthand-properties": "^7.24.7",
- "@babel/plugin-transform-spread": "^7.24.7",
- "@babel/plugin-transform-sticky-regex": "^7.24.7",
- "@babel/plugin-transform-typescript": "^7.25.2",
- "@babel/plugin-transform-unicode-regex": "^7.24.7",
- "@babel/template": "^7.25.0",
- "@react-native/babel-plugin-codegen": "0.76.7",
- "babel-plugin-syntax-hermes-parser": "^0.25.1",
- "babel-plugin-transform-flow-enums": "^0.0.2",
- "react-refresh": "^0.14.0"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@babel/core": "*"
- }
- },
- "node_modules/babel-preset-expo/node_modules/@react-native/codegen": {
- "version": "0.76.7",
- "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.76.7.tgz",
- "integrity": "sha512-FAn585Ll65YvkSrKDyAcsdjHhhAGiMlSTUpHh0x7J5ntudUns+voYms0xMP+pEPt0XuLdjhD7zLIIlAWP407+g==",
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.25.3",
- "glob": "^7.1.1",
- "hermes-parser": "0.23.1",
- "invariant": "^2.2.4",
- "jscodeshift": "^0.14.0",
- "mkdirp": "^0.5.1",
- "nullthrows": "^1.1.1",
- "yargs": "^17.6.2"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@babel/preset-env": "^7.1.6"
- }
- },
"node_modules/babel-preset-jest": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz",
@@ -8883,9 +8756,6 @@
}
},
"node_modules/cross-spawn": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
@@ -11883,15 +11753,12 @@
"devOptional": true,
"license": "MIT",
"dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
- "es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
- "get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
@@ -12353,9 +12220,9 @@
}
},
"node_modules/image-size": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz",
- "integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
+ "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
"license": "MIT",
"dependencies": {
"queue": "6.0.2"
@@ -15426,15 +15293,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/metro/node_modules/source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
- "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/metro/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -16637,15 +16495,6 @@
"node": ">= 0.4"
}
},
- "node_modules/possible-typed-array-names": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
- "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -17814,13 +17663,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/safe-array-concat/node_modules/isarray": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
- "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -17865,13 +17707,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/safe-push-apply/node_modules/isarray": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
- "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/safe-regex-test": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
@@ -20277,13 +20112,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/which-builtin-type/node_modules/isarray": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
- "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/which-collection": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
diff --git a/client/src/AccountScreen.js b/client/src/AccountScreen.js
index b026264..9edd05b 100644
--- a/client/src/AccountScreen.js
+++ b/client/src/AccountScreen.js
@@ -65,6 +65,14 @@ export default function AccountScreen({ navigation }) {
Log Out
You are logged in
+
+ {/* todo: Need to make new changes to the preferences logic */}
+ navigation.navigate('PreferencesScreen')}
+ >
+ User Preferences
+
>
)}
diff --git a/client/src/DisplayRoute.js b/client/src/DisplayRoute.js
index c9e2246..60d89f0 100644
--- a/client/src/DisplayRoute.js
+++ b/client/src/DisplayRoute.js
@@ -1,13 +1,38 @@
/* eslint-disable no-undef, no-use-before-define, react-hooks/exhaustive-deps, array-callback-return */
import React, { useEffect, useState, useCallback } from 'react';
-import { View, Text, Alert, TouchableOpacity, Switch } from 'react-native';
+import {
+ View,
+ Text,
+ Alert,
+ TouchableOpacity,
+ Switch,
+ ImageBackground,
+ Platform,
+ Image,
+} from 'react-native';
import * as Speech from 'expo-speech';
import MapView, { Polyline, Marker } from 'react-native-maps';
import { haversine, startLocationTracking } from './utils/mapUtils';
import displayRouteStyles from './components/styles/DisplayRoute.styles';
-
import locationCircleIcon from './assets/location-circle.png';
+import IncidentReporter from './IncidentReporter';
+import { postIncident } from './utils/incidentReporterUtils';
+import accident from './assets/Crowdsource/TrafficAccident.png';
+import roadClosure from './assets/Crowdsource/RoadClosure.png';
+import roadHazard from './assets/Crowdsource/hazard.png';
+import police from './assets/Crowdsource/Speeding.png';
+import trafficJam from './assets/Crowdsource/TrafficSlow.png';
+import construction from './assets/Crowdsource/construction.png';
+
+const iconMap = {
+ Accident: accident,
+ Closure: roadClosure,
+ Hazard: roadHazard,
+ Police: police,
+ Traffic: trafficJam,
+ Construction: construction,
+};
export default function DisplayRouteScreen({ navigation, route }) {
// route is a prop passed by the navigator, hence why that is used instead of other variable names
@@ -17,9 +42,22 @@ export default function DisplayRouteScreen({ navigation, route }) {
const [remainingPolyline, setRemainingPolyline] =
useState(polylineCoordinates);
const [devMode, setDevMode] = useState(false);
- const [audioOn, setAudioOn] = useState(false);
- const [detailedStepData, setDetailedStepData] = useState([]);
+ const [audioOn, setAudioOn] = useState(true);
const [currentInstruction, setCurrentInstruction] = useState(null);
+ const [incidentInfo, setIncidentInfo] = useState([]);
+ const [detailedStepData, setDetailedStepData] = useState([]);
+
+ const handleIncidentSubmit = async (incidentData) => {
+ // Here you would process the incident data
+ console.log('Incident reported:', incidentData);
+
+ // Example: Send to your API
+ const data = await postIncident(incidentData);
+ console.log('Response:', data);
+
+ // Example: Update local state to show on map
+ // setMapIncidents(prev => [...prev, incidentData]);
+ };
useEffect(() => {
setDetailedStepData([]);
@@ -102,6 +140,45 @@ export default function DisplayRouteScreen({ navigation, route }) {
});
};
+ const isGpsCoordinates = (str) => {
+ const gpsRegex = /^-?\d+(\.\d+)?,-?\d+(\.\d+)?$/; // Regex to match "latitude,longitude"
+ return gpsRegex.test(str);
+ };
+
+ const pollIncident = async () => {
+ try {
+ const baseUrl =
+ Platform.OS === 'web'
+ ? 'http://localhost:8000'
+ : process.env.EXPO_PUBLIC_API_URL;
+ console.log(`Sending request to ${baseUrl}/check_incidents`);
+
+ const response = await fetch(`${baseUrl}/check_incidents`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ // Check if the response is ok
+ if (response.ok) {
+ // Parse the response as JSON
+ const serverMessage = await response.json();
+ console.log('Response from Server: ', serverMessage.message);
+
+ // process the return incidents object, which contains multiple incidents
+ setIncidentInfo(serverMessage.message);
+ } else {
+ // Log the raw response text for debugging
+ const responseText = await response.text();
+ console.error('Failed to get crowd sourced incidents:', responseText);
+ alert('Server Error: ', responseText);
+ }
+ } catch (error) {
+ console.error('Error getting crowd sourced incidents:', error);
+ }
+ };
+
const checkProximityAndUpdate = useCallback(
(currentLocation) => {
for (let i = 0; i < remainingPolyline.length; i += 1) {
@@ -120,7 +197,6 @@ export default function DisplayRouteScreen({ navigation, route }) {
'Destination reached',
'You have reached your destination.',
);
- // TODO: maybe navigate to sustainability
navigation.navigate('Map');
} else {
// Update instruction if previous instruction complete
@@ -146,6 +222,16 @@ export default function DisplayRouteScreen({ navigation, route }) {
[remainingPolyline, navigation, detailedStepData],
);
+ // Poll for incidents every 5 seconds
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ pollIncident();
+ }, 30000); // Poll every 5 seconds
+ pollIncident();
+ // Cleanup function to clear the interval when the component unmounts
+ return () => clearInterval(intervalId);
+ }, []);
+
// Call checkProximityAndUpdate and update setlocation on latitude / longitude button click
const devMove = ({ delLat = 0, delLng = 0 }) => {
const newLocation = {
@@ -169,39 +255,6 @@ export default function DisplayRouteScreen({ navigation, route }) {
{location && routeData ? (
<>
-
-
- {currentInstruction != null ? (
-
- {currentInstruction}
-
- ) : (
- <>
-
- Route from {origin} to {destination}
-
-
- Distance: {routeData.legs[0].distance.text}
-
-
- Duration: {routeData.legs[0].duration.text}
-
- >
- )}
-
-
- Dev Mode
- toggleDevMode(value)}
- />
- Audio
- setAudioOn(value)}
- />
-
-
+ {Array.isArray(incidentInfo) &&
+ incidentInfo.map((incident) => {
+ const type = incident[2]; // Adjust this if the incident type is in a different index
+ const icon = iconMap[type] || accident; // default fallback icon
+
+ return (
+
+
+
+ );
+ })}
+
)}
+
+
+
+ {currentInstruction != null ? (
+
+
+ Route from{' '}
+ {isGpsCoordinates(origin) ? 'current location' : origin} to{' '}
+ {destination}
+
+
+ Distance: {routeData.legs[0].distance.text} Duration:{' '}
+ {routeData.legs[0].duration.text} {'\n \n'}
+ {currentInstruction}
+
+
+ ) : (
+ <>
+
+ Route from{' '}
+ {isGpsCoordinates(origin) ? 'current location' : origin} to{' '}
+ {destination}
+
+
+ Distance: {routeData.legs[0].distance.text} Duration:{' '}
+ {routeData.legs[0].duration.text}
+
+ >
+ )}
+
+
+
+ Dev Mode
+ toggleDevMode(value)}
+ />
+ Audio
+ setAudioOn(value)}
+ />
+
+
{devMode && (
devMove({ delLat: 0.0003 })}
style={displayRouteStyles.dpadButton}
>
- lat +
+ lat +
devMove({ delLng: -0.0004 })}
style={displayRouteStyles.dpadButton}
>
- long -
+ long -
devMove({ delLng: 0.0004 })}
style={displayRouteStyles.dpadButton}
>
- long +
+ long +
devMove({ delLat: -0.0003 })}
style={displayRouteStyles.dpadButton}
>
- lat -
+ lat -
)}
@@ -274,6 +400,8 @@ export default function DisplayRouteScreen({ navigation, route }) {
Fetching your location...
)}
+
+
);
}
diff --git a/client/src/FindRoute.js b/client/src/FindRoute.js
index 1150068..61c4ca2 100644
--- a/client/src/FindRoute.js
+++ b/client/src/FindRoute.js
@@ -6,6 +6,8 @@ import {
TextInput,
TouchableOpacity,
Text,
+ ImageBackground,
+ View,
} from 'react-native';
import Picker from 'react-native-picker-select';
import ActionSheet from 'react-native-actionsheet';
@@ -13,7 +15,9 @@ import {
findRouteStyles,
pickerSelectStyles,
} from './components/styles/FindRoute.styles';
+import bgImage from './assets/FindRouteScreen/Navigationbackground.png';
import { retrieveData } from './caching';
+import { getCurrentLocation } from './utils/mapUtils';
export default function FindRouteScreen({ navigation }) {
const pickerRef = useRef();
@@ -24,7 +28,8 @@ export default function FindRouteScreen({ navigation }) {
const [selectedMode, setSelectedMode] = useState(null);
- const isFormValid = start.trim() !== '' && destination.trim() !== '';
+ const isFormValid =
+ start.trim() !== '' && destination.trim() !== '' && selectedMode !== null;
const fetchRoutes = async () => {
let username = await retrieveData('username');
@@ -33,8 +38,27 @@ export default function FindRouteScreen({ navigation }) {
}
try {
+ let originToSend = start.trim();
+
+ // Check for "current location" variations
+ const normalizedStart = originToSend.toLowerCase();
+ const isCurrentLocation =
+ normalizedStart === 'current location' ||
+ normalizedStart === 'Current Location' ||
+ normalizedStart === 'my location';
+
+ if (isCurrentLocation) {
+ const initialLocation = await getCurrentLocation();
+ if (!initialLocation) {
+ alert('Unable to fetch current location.');
+ return;
+ }
+
+ originToSend = `${initialLocation.latitude},${initialLocation.longitude}`;
+ }
+
const payload = {
- origin: start,
+ origin: originToSend,
destination,
mode: selectedMode,
alternatives: true,
@@ -66,9 +90,12 @@ export default function FindRouteScreen({ navigation }) {
const data = await response.json();
- // Navigate to SelectRouteScreen with the response data
+ if (data.error) {
+ alert(`Error finding route: ${response.message || response.status}`);
+ return;
+ }
navigation.navigate('SelectRouteScreen', {
- origin: start,
+ origin: originToSend,
destination,
routeData: data,
});
@@ -88,66 +115,97 @@ export default function FindRouteScreen({ navigation }) {
};
return (
-
- setStartPoint(text)}
- onSubmitEditing={() => ref2.current.focus()}
- />
- setDestinationPoint(text)}
- onSubmitEditing={() => alert(`Route Entered`)}
- />
-
- Select an option:
- {Platform.OS === 'android' ? (
- pickerRef.current.togglePicker()}>
-
+
+
+ ref2.current.focus()}
+ />
+
+ setStartPoint('Current Location')}
+ >
+
+ 📍 Use Current Location as Start
+
- ) : (
- <>
- actionSheetRef.current.show()}>
- Choose an option...
+
+ alert(`Route Entered`)}
+ />
+
+ Mode of Transport:
+ {Platform.OS === 'android' ? (
+ pickerRef.current.togglePicker()}>
+
+
+
- item.label)
- .concat('Cancel')}
- cancelButtonIndex={modeDropdownData.length}
- onPress={(index) => {
- if (index !== modeDropdownData.length) {
- handlePickerSelect(modeDropdownData[index].value);
- }
- }}
- />
- >
- )}
- {selectedMode && (
+ ) : (
+ <>
+ actionSheetRef.current.show()}
+ activeOpacity={0.8}
+ >
+
+ {selectedMode
+ ? `Selected: ${selectedMode}`
+ : 'Choose an option ▼'}
+
+
+
+ item.label)
+ .concat('Cancel')}
+ cancelButtonIndex={modeDropdownData.length}
+ onPress={(index) => {
+ if (index !== modeDropdownData.length) {
+ handlePickerSelect(modeDropdownData[index].value);
+ }
+ }}
+ />
+ >
+ )}
+ {/* {selectedMode && (
Selected: {selectedMode}
- )}
-
-
- {/* activeOpacity={isFormValid ? 0.7 : 1} */}
- Search
-
-
+ )} */}
+
+
+ Search
+
+
+
);
}
diff --git a/client/src/FriendsScreen.js b/client/src/FriendsScreen.js
index ed947af..e5a9f4b 100644
--- a/client/src/FriendsScreen.js
+++ b/client/src/FriendsScreen.js
@@ -8,9 +8,11 @@ import {
FlatList,
TouchableOpacity,
Platform,
+ ImageBackground,
} from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import styles from './components/styles/FriendsScreen.styles';
+import beachBackground from './assets/FriendsUI/BeachBackground.png'; // Import your background image
import { retrieveData } from './caching';
// Not logged in message component
@@ -329,38 +331,47 @@ export default function FriendsScreen() {
// Otherwise, show the normal friends dashboard
return (
-
- {/* Top Section: Input & Button */}
-
-
-
-
-
- {/* Sent Friend Requests */}
-
- Sent Friend Requests
- index.toString()}
- renderItem={({ item }) => (
-
- {item}
- {/* Cancel friend request */}
- cancelFriendRequest(item)}
- style={styles.cancelButton}
- >
- Cancel
-
-
- )}
- />
-
+ {/* Background Image */}
+
+
+ {/* Top Section: Input & Button */}
+
+
+
+
+
+ {/* Sent Friend Requests */}
+
+ Sent Friend Requests
+ index.toString()}
+ renderItem={({ item }) => (
+
+ {item}
+ cancelFriendRequest(item)}
+ style={styles.cancelButton}
+ >
+ Cancel
+
+
+ )}
+ />
+
{/* Pending Friends List */}
@@ -388,27 +399,27 @@ export default function FriendsScreen() {
/>
- {/* Current Friends List */}
-
- Current Friends
- index.toString()}
- renderItem={({ item }) => (
-
- {item}
- {/* Remove friend */}
- removeFriend(item)}
- style={styles.removeButton}
- >
- Remove
-
-
- )}
- />
+ {/* Current Friends List */}
+
+ Current Friends
+ index.toString()}
+ renderItem={({ item }) => (
+
+ {item}
+ removeFriend(item)}
+ style={styles.removeButton}
+ >
+ Remove
+
+
+ )}
+ />
+
-
+
);
}
diff --git a/client/src/IncidentReporter.js b/client/src/IncidentReporter.js
new file mode 100644
index 0000000..f7554dc
--- /dev/null
+++ b/client/src/IncidentReporter.js
@@ -0,0 +1,237 @@
+import React, { useState, useRef } from 'react';
+import {
+ View,
+ Text,
+ TouchableOpacity,
+ Animated,
+ TextInput,
+ ScrollView,
+ Dimensions,
+ Image,
+ TouchableWithoutFeedback,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import styles from './components/styles/IncidentReporter.styles';
+import { getCurrentLocation } from './utils/mapUtils';
+
+// Incident icons
+import accidentIcon from './assets/accident.png';
+import policeIcon from './assets/police.png';
+import hazardIcon from './assets/hazard.png';
+import trafficIcon from './assets/traffic.png';
+import constructionIcon from './assets/construction.png';
+import closureIcon from './assets/closure.png';
+
+const ICONS = {
+ accident: accidentIcon,
+ police: policeIcon,
+ hazard: hazardIcon,
+ traffic: trafficIcon,
+ construction: constructionIcon,
+ closure: closureIcon,
+};
+
+// Incident types with titles and descriptions
+const INCIDENT_TYPES = [
+ { id: 'accident', title: 'Accident', description: 'Report a crash' },
+ { id: 'police', title: 'Police', description: 'Report police presence' },
+ { id: 'hazard', title: 'Hazard', description: 'Report a road hazard' },
+ { id: 'traffic', title: 'Traffic', description: 'Report heavy traffic' },
+ { id: 'construction', title: 'Construction', description: 'Report works' },
+ { id: 'closure', title: 'Closure', description: 'Report road closure' },
+];
+
+const { width, height } = Dimensions.get('window');
+
+// Modal dimensions (partial screen size)
+// eslint-disable-next-line no-unused-vars
+const MODAL_WIDTH = width * 0.7;
+const MODAL_HEIGHT = height * 0.6;
+
+function IncidentReporter({ onSubmitIncident }) {
+ // State management
+ const [isVisible, setIsVisible] = useState(false);
+ const [selectedIncident, setSelectedIncident] = useState(null);
+ const [comment, setComment] = useState('');
+ const [isCommentView, setIsCommentView] = useState(false);
+
+ // Animation values
+ const slideAnim = useRef(new Animated.Value(MODAL_HEIGHT + 50)).current;
+ const fadeAnim = useRef(new Animated.Value(0)).current;
+
+ // Open the incident reporter
+ const openReporter = () => {
+ setIsVisible(true);
+ setSelectedIncident(null);
+ setComment('');
+ setIsCommentView(false);
+
+ Animated.parallel([
+ Animated.timing(slideAnim, {
+ toValue: 0,
+ duration: 300,
+ useNativeDriver: true,
+ }),
+ Animated.timing(fadeAnim, {
+ toValue: 0.5,
+ duration: 300,
+ useNativeDriver: true,
+ }),
+ ]).start();
+ };
+
+ // Close the incident reporter
+ const closeReporter = () => {
+ Animated.parallel([
+ Animated.timing(slideAnim, {
+ toValue: MODAL_HEIGHT + 50,
+ duration: 300,
+ useNativeDriver: true,
+ }),
+ Animated.timing(fadeAnim, {
+ toValue: 0,
+ duration: 300,
+ useNativeDriver: true,
+ }),
+ ]).start(() => {
+ setIsVisible(false);
+ });
+ };
+
+ // Handle incident selection
+ const selectIncident = (incident) => {
+ setSelectedIncident(incident);
+ setIsCommentView(true);
+ };
+
+ // Go back to incident selection
+ const backToIncidents = () => {
+ setIsCommentView(false);
+ };
+
+ // Submit the incident report
+ const submitIncident = async () => {
+ const location = await getCurrentLocation();
+ if (selectedIncident) {
+ onSubmitIncident({
+ type: selectedIncident.id,
+ title: selectedIncident.title,
+ comment: comment.trim(),
+ timestamp: new Date().toISOString(),
+ location: {
+ latitude: location.latitude,
+ longitude: location.longitude,
+ },
+ });
+
+ closeReporter();
+ }
+ };
+
+ // Prevent rendering if not visible
+ if (!isVisible) {
+ return (
+
+ Report
+
+ );
+ }
+
+ return (
+ <>
+ {/* Backdrop/overlay - now positioned only behind the modal */}
+
+
+
+
+ {/* Sliding panel */}
+
+
+ {/* Header */}
+
+
+
+ {isCommentView ? 'Back' : 'Cancel'}
+
+
+
+
+ {isCommentView ? selectedIncident?.title : 'Report Incident'}
+
+
+ {isCommentView && (
+
+ Submit
+
+ )}
+
+
+ {/* Content */}
+ {!isCommentView ? (
+ // Incident type selection
+
+ {INCIDENT_TYPES.map((incident) => (
+ selectIncident(incident)}
+ >
+
+
+
+
+ {incident.title}
+
+ {incident.description}
+
+
+
+ ))}
+
+ ) : (
+ // Comment input view
+
+ Add details (optional):
+
+
+ )}
+
+
+ >
+ );
+}
+
+export default IncidentReporter;
diff --git a/client/src/Map.js b/client/src/Map.js
index 6c5e4d3..5225edc 100644
--- a/client/src/Map.js
+++ b/client/src/Map.js
@@ -1,50 +1,168 @@
import React, { useState, useEffect, useRef } from 'react';
-import { View, Text, TouchableOpacity, Alert } from 'react-native';
+import { View, Text, TouchableOpacity, Image, Platform } from 'react-native';
import MapView, { Marker } from 'react-native-maps';
import { getCurrentLocation, startLocationTracking } from './utils/mapUtils';
-import locationCircleIcon from './assets/location-circle.png';
import MapStyles from './components/styles/Map.styles';
+import Sunny from './assets/MapDashboard/SunIcon.png';
+import Rain from './assets/MapDashboard/rainIcon.png';
+import Cloud from './assets/MapDashboard/CloudIcon.png';
+import Thunder from './assets/MapDashboard/lightingIcon.png';
+import accident from './assets/Crowdsource/TrafficAccident.png';
+import roadClosure from './assets/Crowdsource/RoadClosure.png';
+import roadHazard from './assets/Crowdsource/hazard.png';
+import police from './assets/Crowdsource/Speeding.png';
+import trafficJam from './assets/Crowdsource/TrafficSlow.png';
+import construction from './assets/Crowdsource/construction.png';
+
+const iconMap = {
+ Accident: accident,
+ Closure: roadClosure,
+ Hazard: roadHazard,
+ Police: police,
+ Traffic: trafficJam,
+ Construction: construction,
+};
+
export default function MapScreen({ navigation }) {
- const [webSocket, setWebSocket] = useState(null);
+ const [bikeStations, setBikeStations] = useState([]);
+ const [incidentInfo, setIncidentInfo] = useState([]);
const [location, setLocation] = useState(null);
+ const [weather, setWeather] = useState(null);
+ const [temperature, setTemperature] = useState(null);
const [errorMessage, setErrorMessage] = useState('');
const mapRef = useRef(null);
- // WebSocket Setup
- useEffect(() => {
- const wsUrl = `${process.env.EXPO_PUBLIC_API_URL.replace(/^http/, 'ws')}/ws/location`;
- console.log('Connecting to WebSocket:', wsUrl);
+ const pollIncident = async () => {
+ try {
+ const baseUrl =
+ Platform.OS === 'web'
+ ? 'http://localhost:8000'
+ : process.env.EXPO_PUBLIC_API_URL;
+ console.log(`Sending request to ${baseUrl}/check_incidents`);
- const socket = new WebSocket(wsUrl);
+ const response = await fetch(`${baseUrl}/check_incidents`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
- socket.onopen = () => {
- console.log('WebSocket connection opened');
- setWebSocket(socket);
- };
+ // Check if the response is ok
+ if (response.ok) {
+ // Parse the response as JSON
+ const serverMessage = await response.json();
+ console.log('Response from Server: ', serverMessage.message);
- socket.onmessage = (event) =>
- console.log('Message from server:', event.data); // check if required
- socket.onerror = (error) => console.error('WebSocket error:', error);
- socket.onclose = () => console.log('WebSocket connection closed');
+ // process the return incidents object, which contains multiple incidents
+ setIncidentInfo(serverMessage.message);
+ } else {
+ // Log the raw response text for debugging
+ const responseText = await response.text();
+ console.error('Failed to get crowd sourced incidents:', responseText);
+ alert('Server Error: ', responseText);
+ }
+ } catch (error) {
+ console.error('Error getting crowd sourced incidents:', error);
+ }
+ };
- return () => socket.close();
- }, []);
+ // choose weather Icon
+ const getWeatherIcon = (weatherCondition) => {
+ switch (weatherCondition) {
+ case 'sun':
+ return Sunny;
+ case 'cloud':
+ return Cloud;
+ case 'rain':
+ return Rain;
+ case 'thunder':
+ return Thunder;
+ default:
+ return null;
+ }
+ };
+
+ const fetchWeather = async () => {
+ try {
+ const baseUrl =
+ Platform.OS === 'web'
+ ? 'http://localhost:8000'
+ : process.env.EXPO_PUBLIC_API_URL;
+
+ console.log(
+ `Sending request to ${baseUrl}/weather?longitude=${location.longitude}&latitude=${location.latitude}`,
+ );
+
+ const response = await fetch(
+ `${baseUrl}/weather?longitude=${location.longitude}&latitude=${location.latitude}`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
- // useEffect(() => {
- // (async () => {
- // try {
- // const initialLocation = await getCurrentLocation();
- // setLocation(initialLocation);
+ if (response.ok) {
+ const serverMessage = await response.json();
+ console.log('Response from Server: ', serverMessage.weather);
+ console.log('Response from Server: ', serverMessage.temperature);
+ setWeather(serverMessage.weather);
+ setTemperature(serverMessage.temperature);
+ } else {
+ const responseText = await response.text();
+ console.error('Failed to get weather:', responseText);
+ setWeather('cloud'); // fallback
+ setTemperature('10');
+ }
+ } catch (error) {
+ console.error('Error getting real time weather', error);
+ setWeather('cloud'); // fallback on network error
+ setTemperature('10');
+ }
+ };
+
+ const fetchBikeApi = async () => {
+ try {
+ const baseUrl =
+ Platform.OS === 'web'
+ ? 'http://localhost:8000'
+ : process.env.EXPO_PUBLIC_API_URL;
+
+ console.log(
+ `Sending request to ${baseUrl}/BikeStand?longitude=${location.longitude}&latitude=${location.latitude}`,
+ );
+
+ const response = await fetch(
+ `${baseUrl}/BikeStand?longitude=${location.longitude}&latitude=${location.latitude}`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
- // const locationSubscription = await startLocationTracking(setLocation);
+ const raw = await response.text();
+ console.log('Raw Bike API Response:', raw);
- // return () => locationSubscription.remove();
- // } catch (error) {
- // setErrorMessage(error.message);
- // }
- // })();
- // }, []);
+ const serverMessage = raw ? JSON.parse(raw) : null;
+
+ if (serverMessage && Array.isArray(serverMessage.BikeInfo)) {
+ setBikeStations(serverMessage.BikeInfo);
+ } else if (Array.isArray(serverMessage)) {
+ // if it's just a raw array
+ setBikeStations(serverMessage);
+ } else {
+ console.warn('Unexpected or null response, setting empty bikeStations');
+ setBikeStations([]);
+ }
+ } catch (error) {
+ console.error('Error fetching bike station data:', error);
+ setBikeStations([]); // fallback
+ }
+ };
useEffect(() => {
const fetchLocation = async () => {
@@ -63,18 +181,18 @@ export default function MapScreen({ navigation }) {
fetchLocation();
}, []);
- // Send Location to WebSocket
- const sendLocation = () => {
- if (webSocket && location) {
- webSocket.send(JSON.stringify(location));
- console.log('Sent location:', location);
- } else {
- Alert.alert(
- 'Location/WebSocket Issue',
- !location ? 'Fetching GPS location...' : 'WebSocket not connected.',
- );
- }
- };
+ useEffect(() => {
+ const pollOnce = async () => {
+ if (location) {
+ fetchWeather();
+ fetchBikeApi();
+ pollIncident();
+ }
+ };
+
+ pollOnce();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [location]); // Empty dependency array ensures this runs only once when the component mounts
const renderContent = () => {
if (errorMessage) {
@@ -82,8 +200,20 @@ export default function MapScreen({ navigation }) {
}
if (location) {
+ const weatherIcon = getWeatherIcon(weather);
return (
<>
+ {weatherIcon && (
+
+
+ {temperature}°C
+
+ )}
+
+ {Array.isArray(bikeStations) &&
+ bikeStations.map((station) => (
+
+
+
+ ))}
+
+ {Array.isArray(incidentInfo) &&
+ incidentInfo.map((incident) => {
+ const type = incident[2]; // Adjust this if the incident type is in a different index
+ const icon = iconMap[type] || accident; // default fallback icon
+
+ return (
+
+
+
+ );
+ })}
+
+ >
+
+
-
- Send Location
-
navigation.navigate('AccountScreen')}
>
Account
- navigation.navigate('FindRouteScreen')}
- >
- Find Route
-
- navigation.navigate('FindRouteScreen')}
- >
- Find Route
-
- navigation.navigate('FriendsScreen')}
- >
- Friends UI
-
- navigation.navigate('Dashboard')}
- >
- Sustainability Dashboard
-
- navigation.navigate('WeatherScreen')}
- >
- Weather
-
+
+
+ navigation.navigate('Dashboard')}
+ style={MapStyles.button}
+ >
+
+ Dashboard
+
- {/* todo: Need to make new changes to the preferences logic */}
- navigation.navigate('PreferencesScreen')}
- >
- User Preferences
-
+ navigation.navigate('FindRouteScreen')}
+ style={[MapStyles.button, MapStyles.centerButton]}
+ >
+
+ Find Route
+
+
+ navigation.navigate('FriendsScreen')}
+ style={MapStyles.button}
+ >
+
+ Friends
+
+
>
);
}
diff --git a/client/src/assets/Crowdsource/RoadClosure.png b/client/src/assets/Crowdsource/RoadClosure.png
new file mode 100644
index 0000000..3583e5d
Binary files /dev/null and b/client/src/assets/Crowdsource/RoadClosure.png differ
diff --git a/client/src/assets/Crowdsource/Speeding.png b/client/src/assets/Crowdsource/Speeding.png
new file mode 100644
index 0000000..f5d2316
Binary files /dev/null and b/client/src/assets/Crowdsource/Speeding.png differ
diff --git a/client/src/assets/Crowdsource/TrafficAccident.png b/client/src/assets/Crowdsource/TrafficAccident.png
new file mode 100644
index 0000000..3d90806
Binary files /dev/null and b/client/src/assets/Crowdsource/TrafficAccident.png differ
diff --git a/client/src/assets/Crowdsource/TrafficSlow.png b/client/src/assets/Crowdsource/TrafficSlow.png
new file mode 100644
index 0000000..92c3928
Binary files /dev/null and b/client/src/assets/Crowdsource/TrafficSlow.png differ
diff --git a/client/src/assets/Crowdsource/construction.png b/client/src/assets/Crowdsource/construction.png
new file mode 100644
index 0000000..8bbc472
Binary files /dev/null and b/client/src/assets/Crowdsource/construction.png differ
diff --git a/client/src/assets/Crowdsource/hazard.png b/client/src/assets/Crowdsource/hazard.png
new file mode 100644
index 0000000..0c4905f
Binary files /dev/null and b/client/src/assets/Crowdsource/hazard.png differ
diff --git a/client/src/assets/FindRouteScreen/Navigationbackground.png b/client/src/assets/FindRouteScreen/Navigationbackground.png
new file mode 100644
index 0000000..050d1d5
Binary files /dev/null and b/client/src/assets/FindRouteScreen/Navigationbackground.png differ
diff --git a/client/src/assets/FriendsUI/BeachBackground.png b/client/src/assets/FriendsUI/BeachBackground.png
new file mode 100644
index 0000000..4f632e3
Binary files /dev/null and b/client/src/assets/FriendsUI/BeachBackground.png differ
diff --git a/client/src/assets/MapDashboard/1Asset 1bikeicon.png b/client/src/assets/MapDashboard/1Asset 1bikeicon.png
new file mode 100644
index 0000000..a462a09
Binary files /dev/null and b/client/src/assets/MapDashboard/1Asset 1bikeicon.png differ
diff --git a/client/src/assets/MapDashboard/CloudIcon.png b/client/src/assets/MapDashboard/CloudIcon.png
new file mode 100644
index 0000000..b3a208a
Binary files /dev/null and b/client/src/assets/MapDashboard/CloudIcon.png differ
diff --git a/client/src/assets/MapDashboard/SunIcon.png b/client/src/assets/MapDashboard/SunIcon.png
new file mode 100644
index 0000000..65c4c80
Binary files /dev/null and b/client/src/assets/MapDashboard/SunIcon.png differ
diff --git a/client/src/assets/MapDashboard/bikeicon.png b/client/src/assets/MapDashboard/bikeicon.png
new file mode 100644
index 0000000..ddfd041
Binary files /dev/null and b/client/src/assets/MapDashboard/bikeicon.png differ
diff --git a/client/src/assets/MapDashboard/friendsicon.png b/client/src/assets/MapDashboard/friendsicon.png
new file mode 100644
index 0000000..c47aaff
Binary files /dev/null and b/client/src/assets/MapDashboard/friendsicon.png differ
diff --git a/client/src/assets/MapDashboard/leaficon.png b/client/src/assets/MapDashboard/leaficon.png
new file mode 100644
index 0000000..cf9fb46
Binary files /dev/null and b/client/src/assets/MapDashboard/leaficon.png differ
diff --git a/client/src/assets/MapDashboard/lightingIcon.png b/client/src/assets/MapDashboard/lightingIcon.png
new file mode 100644
index 0000000..5eafe5e
Binary files /dev/null and b/client/src/assets/MapDashboard/lightingIcon.png differ
diff --git a/client/src/assets/MapDashboard/movementbar.png b/client/src/assets/MapDashboard/movementbar.png
new file mode 100644
index 0000000..1a63e31
Binary files /dev/null and b/client/src/assets/MapDashboard/movementbar.png differ
diff --git a/client/src/assets/MapDashboard/rainIcon.png b/client/src/assets/MapDashboard/rainIcon.png
new file mode 100644
index 0000000..8ae419a
Binary files /dev/null and b/client/src/assets/MapDashboard/rainIcon.png differ
diff --git a/client/src/assets/MapDashboard/routeicon.png b/client/src/assets/MapDashboard/routeicon.png
new file mode 100644
index 0000000..0175036
Binary files /dev/null and b/client/src/assets/MapDashboard/routeicon.png differ
diff --git a/client/src/assets/accident.png b/client/src/assets/accident.png
new file mode 100644
index 0000000..00c00df
Binary files /dev/null and b/client/src/assets/accident.png differ
diff --git a/client/src/assets/closure.png b/client/src/assets/closure.png
new file mode 100644
index 0000000..39a6fdc
Binary files /dev/null and b/client/src/assets/closure.png differ
diff --git a/client/src/assets/construction.png b/client/src/assets/construction.png
new file mode 100644
index 0000000..b65f203
Binary files /dev/null and b/client/src/assets/construction.png differ
diff --git a/client/src/assets/hazard.png b/client/src/assets/hazard.png
new file mode 100644
index 0000000..9e3dc2a
Binary files /dev/null and b/client/src/assets/hazard.png differ
diff --git a/client/src/assets/police.png b/client/src/assets/police.png
new file mode 100644
index 0000000..48c9e8e
Binary files /dev/null and b/client/src/assets/police.png differ
diff --git a/client/src/assets/traffic.png b/client/src/assets/traffic.png
new file mode 100644
index 0000000..c7a1e5e
Binary files /dev/null and b/client/src/assets/traffic.png differ
diff --git a/client/src/components/styles/DisplayRoute.styles.js b/client/src/components/styles/DisplayRoute.styles.js
index 8ed192e..8b36331 100644
--- a/client/src/components/styles/DisplayRoute.styles.js
+++ b/client/src/components/styles/DisplayRoute.styles.js
@@ -1,15 +1,23 @@
-import { StyleSheet } from 'react-native';
+import { StyleSheet, Dimensions } from 'react-native';
+
+const screenWidth = Dimensions.get('window').width;
const displayRouteStyles = StyleSheet.create({
container: { flex: 1 },
map: { flex: 1 },
- routeInfo: { fontSize: 16 },
+ routeInfo: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ color: 'white',
+ textAlign: 'center',
+ top: 60,
+ },
error: { color: 'red', textAlign: 'center', margin: 10 },
loadingText: { textAlign: 'center', margin: 10 },
dpadContainer: {
position: 'absolute',
- bottom: 20,
- left: '50%',
+ top: '50%',
+ left: '15%',
transform: [{ translateX: -50 }],
alignItems: 'center',
},
@@ -18,26 +26,56 @@ const displayRouteStyles = StyleSheet.create({
},
dpadButton: {
padding: 10,
- backgroundColor: '#ADD8E6',
+ backgroundColor: '#33CC66',
+ colour: 'white',
borderRadius: 5,
margin: 5,
},
- infoContainer: {
+ dpadtext: {
+ color: 'white',
+ fontWeight: 'bold',
+ },
+ barContainer: {
+ position: 'absolute', // Position it relative to the parent container
+ bottom: 0, // Align it to the bottom
+ left: 0, // Align it to the left edge
+ right: 0, // Align it to the right edge
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
- margin: 10,
+ backgroundColor: 'white', // Optional: Add a background color for better visibility
+ padding: 10, // Optional: Add padding for spacing
+ borderTopWidth: 1, // Optional: Add a border at the top
+ borderColor: '#ccc', // Optional: Border color
+ width: screenWidth,
+ height: 10,
},
routeInfoContainer: {
flex: 1,
},
devModeContainer: {
+ left: 20,
+ top: 120,
+ position: 'absolute',
alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: 'white',
+ padding: 10,
+ borderRadius: 10, // Add this to curve the corners
},
devModeText: {
fontSize: 16,
marginBottom: 5,
},
+ barImage: {
+ position: 'absolute',
+ width: screenWidth,
+ height: 190,
+ left: 0,
+ bottom: 0,
+ zIndex: 0, // put image behind buttons
+ resizeMode: 'stretch', // or 'cover' if stretch distorts
+ },
});
export default displayRouteStyles;
diff --git a/client/src/components/styles/FindRoute.styles.js b/client/src/components/styles/FindRoute.styles.js
index 9ed6e3a..92f18a8 100644
--- a/client/src/components/styles/FindRoute.styles.js
+++ b/client/src/components/styles/FindRoute.styles.js
@@ -3,50 +3,118 @@ import { StyleSheet } from 'react-native';
const findRouteStyles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: '#fff',
- alignItems: 'center',
+ paddingHorizontal: 20,
justifyContent: 'center',
},
input: {
width: '100%',
- padding: 10,
+ paddingVertical: 12,
+ paddingHorizontal: 15,
borderWidth: 1,
- borderColor: 'gray',
- borderRadius: 4,
- marginBottom: 10,
+ borderColor: '#ccc',
+ borderRadius: 8,
+ backgroundColor: '#fafafa',
+ marginBottom: 1,
+ fontSize: 16,
},
label: {
- marginBottom: 10,
+ fontSize: 16,
+ fontWeight: '500',
+ color: '#333',
+ marginBottom: 8,
},
selected: {
+ fontSize: 14,
marginTop: 10,
- marginBottom: 10,
+ color: '#444',
},
- TouchableOpacity: {
- padding: 10,
+ button: {
+ paddingVertical: 12,
+ paddingHorizontal: 20,
backgroundColor: '#841584',
- borderRadius: 5,
+ borderRadius: 6,
+ alignItems: 'center',
+ marginTop: 24,
},
disabledButton: {
backgroundColor: '#ccc',
},
+ buttonText: {
+ color: '#fff',
+ fontWeight: 'bold',
+ fontSize: 16,
+ },
+ pickerWrapper: {
+ width: '100%',
+ borderWidth: 1,
+ borderColor: '#ccc',
+ borderRadius: 8,
+ paddingVertical: 12,
+ paddingHorizontal: 15,
+ backgroundColor: '#fff',
+ justifyContent: 'center',
+ marginBottom: 16,
+ },
+ dropdownButton: {
+ width: '100%',
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ borderWidth: 1,
+ borderColor: '#ccc',
+ borderRadius: 8,
+ backgroundColor: '#fff',
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+
+ dropdownButtonText: {
+ fontSize: 16,
+ color: '#333',
+ textAlign: 'center',
+ },
+ useLocationButton: {
+ marginTop: 6,
+ alignSelf: 'flex-start',
+ backgroundColor: 'white',
+ paddingVertical: 6,
+ paddingHorizontal: 10,
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: '#ccc',
+ shadowColor: '#000',
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ shadowOffset: { width: 0, height: 2 },
+ marginBottom: 16,
+ },
+
+ useLocationButtonText: {
+ color: '#333',
+ fontSize: 14,
+ },
});
const pickerSelectStyles = StyleSheet.create({
inputIOS: {
- color: 'black',
- paddingTop: 13,
- paddingHorizontal: 10,
- paddingBottom: 12,
+ fontSize: 16,
+ paddingVertical: 12,
+ paddingHorizontal: 15,
borderWidth: 1,
- borderColor: 'gray',
- borderRadius: 4,
- backgroundColor: 'white',
- width: '100%',
+ borderColor: '#ccc',
+ borderRadius: 8,
+ backgroundColor: '#fff',
+ marginBottom: 16,
},
inputAndroid: {
- color: 'black',
- width: '80%',
+ fontSize: 16,
+ paddingVertical: 10,
+ paddingHorizontal: 15,
+ borderWidth: 1,
+ borderColor: '#ccc',
+ borderRadius: 8,
+ backgroundColor: '#fff',
+ marginBottom: 16,
},
});
diff --git a/client/src/components/styles/FriendsScreen.styles.js b/client/src/components/styles/FriendsScreen.styles.js
index a52ced8..23a0a7e 100644
--- a/client/src/components/styles/FriendsScreen.styles.js
+++ b/client/src/components/styles/FriendsScreen.styles.js
@@ -4,7 +4,11 @@ const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
- backgroundColor: '#f5f5f5',
+ },
+ backgroundImage: {
+ flex: 1, // Ensure the image stretches to fill the screen
+ width: '100%', // Stretch horizontally
+ height: '100%', // Stretch vertically
},
inputContainer: {
flexDirection: 'row',
@@ -17,13 +21,18 @@ const styles = StyleSheet.create({
padding: 10,
borderRadius: 5,
marginRight: 10,
+ backgroundColor: 'rgba(255, 255, 255, 0.5)',
},
listContainer: {
+ backgroundColor: 'rgba(255, 255, 255, 0.5)',
+ padding: 10,
+ borderRadius: 5,
flex: 1,
marginBottom: 20,
},
sectionTitle: {
fontSize: 18,
+ opacity: 1,
fontWeight: 'bold',
marginBottom: 10,
},
@@ -36,8 +45,15 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'space-between',
},
+ requestButton: {
+ backgroundColor: '#4CAF50',
+ padding: 8,
+ borderRadius: 5,
+ marginLeft: 5,
+ width: 150,
+ },
friendItem: {
- backgroundColor: '#d1f0d1',
+ backgroundColor: '#fff',
padding: 10,
borderRadius: 5,
marginBottom: 5,
@@ -66,12 +82,15 @@ const styles = StyleSheet.create({
buttonText: {
color: '#fff',
fontWeight: 'bold',
+ textAlign: 'center',
},
removeButton: {
backgroundColor: '#E74C3C',
padding: 8,
borderRadius: 5,
- marginLeft: 5,
+ marginLeft: 10,
+ width: 100,
+ alignItems: '',
},
});
diff --git a/client/src/components/styles/IncidentReporter.styles.js b/client/src/components/styles/IncidentReporter.styles.js
new file mode 100644
index 0000000..ccf4021
--- /dev/null
+++ b/client/src/components/styles/IncidentReporter.styles.js
@@ -0,0 +1,156 @@
+import { StyleSheet, Dimensions } from 'react-native';
+
+const { width, height } = Dimensions.get('window');
+
+// Modal dimensions (partial screen size)
+const MODAL_WIDTH = width * 0.7;
+const MODAL_HEIGHT = height * 0.6;
+
+const styles = StyleSheet.create({
+ reportButton: {
+ position: 'absolute',
+ left: 20,
+ top: 40,
+ backgroundColor: '#FF4500',
+ borderRadius: 30,
+ padding: 16, // Slightly increased padding
+ paddingHorizontal: 20, // Wider button
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 3 },
+ shadowOpacity: 0.4, // Increased shadow opacity
+ shadowRadius: 5, // Increased shadow radius
+ elevation: 8, // Increased elevation for Android
+ zIndex: 10,
+ },
+ reportButtonText: {
+ color: 'white',
+ fontWeight: 'bold',
+ fontSize: 18, // Increased font size
+ },
+ backdrop: {
+ // Changed from absoluteFillObject to only cover part of the screen
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ width: MODAL_WIDTH + 15, // Just enough to cover the modal with padding
+ height: MODAL_HEIGHT + 15,
+ // backgroundColor: 'rgba(0, 0, 0, 0.3)', // More transparent
+ zIndex: 100,
+ borderRadius: 20, // Round the corners of the backdrop
+ },
+ container: {
+ position: 'absolute',
+ bottom: 200,
+ left: 60,
+ width: MODAL_WIDTH,
+ height: MODAL_HEIGHT,
+ backgroundColor: 'white',
+ borderRadius: 20,
+ zIndex: 101,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 3 },
+ shadowOpacity: 0.3,
+ shadowRadius: 6,
+ elevation: 10,
+ overflow: 'hidden', // Ensure content doesn't overflow the container
+ },
+ safeArea: {
+ flex: 1,
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: '#E0E0E0',
+ },
+ headerTitle: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ color: '#333',
+ textAlign: 'center',
+ flex: 1,
+ },
+ closeButton: {
+ padding: 8,
+ minWidth: 60, // Ensure button has enough width
+ },
+ closeButtonText: {
+ fontSize: 16,
+ color: '#007AFF',
+ },
+ submitButton: {
+ padding: 8,
+ minWidth: 60, // Ensure button has enough width
+ },
+ submitButtonText: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ color: '#007AFF',
+ },
+ incidentList: {
+ flex: 1,
+ paddingHorizontal: 8, // Add horizontal padding to prevent items from touching the edges
+ },
+ incidentItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 12, // Slightly reduced padding to fit better
+ borderBottomWidth: 1,
+ borderBottomColor: '#E0E0E0',
+ width: '100%', // Ensure full width within container
+ },
+ incidentIconContainer: {
+ width: 46, // Slightly reduced to fit better
+ height: 46, // Slightly reduced to fit better
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 12, // Reduced margin
+ borderRadius: 23,
+ backgroundColor: '#F5F5F5',
+ },
+ incidentIcon: {
+ width: 26, // Slightly reduced
+ height: 26, // Slightly reduced
+ },
+ incidentTextContainer: {
+ flex: 1,
+ paddingRight: 4, // Ensure text doesn't touch the edge
+ },
+ incidentTitle: {
+ fontSize: 15, // Slightly reduced
+ fontWeight: 'bold',
+ color: '#333',
+ marginBottom: 2, // Reduced spacing
+ },
+ incidentDescription: {
+ fontSize: 13, // Slightly reduced
+ color: '#666',
+ },
+ commentContainer: {
+ flex: 1,
+ padding: 16,
+ },
+ commentLabel: {
+ fontSize: 14,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ color: '#333',
+ },
+ commentInput: {
+ flex: 0,
+ height: 100,
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ borderWidth: 1,
+ borderColor: '#E0E0E0',
+ borderRadius: 8,
+ fontSize: 16,
+ textAlignVertical: 'top',
+ backgroundColor: '#F9F9F9',
+ },
+});
+
+export default styles;
diff --git a/client/src/components/styles/Map.styles.js b/client/src/components/styles/Map.styles.js
index d83dcef..d66fc87 100644
--- a/client/src/components/styles/Map.styles.js
+++ b/client/src/components/styles/Map.styles.js
@@ -1,4 +1,6 @@
-import { StyleSheet, Platform } from 'react-native';
+import { StyleSheet, Platform, Dimensions } from 'react-native';
+
+const screenWidth = Dimensions.get('window').width;
const MapStyles = StyleSheet.create({
container: {
@@ -27,9 +29,9 @@ const MapStyles = StyleSheet.create({
loginButton: {
position: 'absolute',
alignItems: 'center',
- left: '70%',
+ left: '34%',
top: '0%',
- backgroundColor: '#007bff',
+ backgroundColor: '#33CC66',
paddingVertical: 10,
paddingHorizontal: 40,
borderRadius: 10,
@@ -128,6 +130,73 @@ const MapStyles = StyleSheet.create({
textAlignVertical: 'center',
fontSize: 18,
},
+ bar: {
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ width: screenWidth,
+ height: 120,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-around',
+ },
+ barImage: {
+ position: 'absolute',
+ width: screenWidth,
+ height: 140,
+ left: 0,
+ bottom: 0,
+ zIndex: 0, // put image behind buttons
+ resizeMode: 'stretch', // or 'cover' if stretch distorts
+ },
+ button: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ flex: 1,
+ },
+ centerButton: {
+ marginBottom: 10, // visually pop it up if needed
+ },
+ icon: {
+ width: 36,
+ height: 36,
+ },
+ centerIcon: {
+ width: 44,
+ height: 44,
+ },
+ label: {
+ fontSize: 12,
+ color: '#fff',
+ marginTop: 4,
+ },
+ weatherContainer: {
+ position: 'absolute',
+ top: 10,
+ left: 10,
+ flexDirection: 'column', // 🔁 stack vertically
+ alignItems: 'center',
+ backgroundColor: 'rgba(255,255,255,0.8)',
+ paddingHorizontal: 10,
+ paddingVertical: 6,
+ borderRadius: 8,
+ shadowColor: '#000',
+ shadowOpacity: 0.2,
+ shadowRadius: 4,
+ shadowOffset: { width: 0, height: 2 },
+ zIndex: 100,
+ },
+
+ weatherIcon: {
+ width: 50,
+ height: 50,
+ marginBottom: 4, // space between icon and text
+ },
+
+ weatherText: {
+ fontSize: 14,
+ color: '#333',
+ },
});
export default MapStyles;
diff --git a/client/src/utils/incidentReporterUtils.js b/client/src/utils/incidentReporterUtils.js
new file mode 100644
index 0000000..d68183a
--- /dev/null
+++ b/client/src/utils/incidentReporterUtils.js
@@ -0,0 +1,35 @@
+import { Platform } from 'react-native';
+
+export const postIncident = async (incidentData, customBaseUrl = null) => {
+ console.log('Sending data:', incidentData);
+ try {
+ const baseUrl =
+ customBaseUrl ||
+ (Platform.OS === 'web'
+ ? 'http://localhost:8000'
+ : process.env.EXPO_PUBLIC_API_URL);
+ console.log(`Sending request to ${baseUrl}/report_incident}`);
+
+ const response = await fetch(`${baseUrl}/report_incident`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(incidentData),
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ alert(data.message);
+
+ return data;
+ } catch (error) {
+ console.error('Error details:', error);
+ throw error;
+ }
+};
+
+export default postIncident;
diff --git a/client/tests/incidentReporter.test.js b/client/tests/incidentReporter.test.js
new file mode 100644
index 0000000..b8443a9
--- /dev/null
+++ b/client/tests/incidentReporter.test.js
@@ -0,0 +1,76 @@
+import { postIncident } from '../src/utils/incidentReporterUtils';
+
+global.fetch = jest.fn();
+global.alert = jest.fn();
+
+jest.mock('react-native', () => ({
+ Platform: { OS: 'android' }, // Mock Platform to avoid runtime issues
+}));
+
+describe('postIncident', () => {
+ const mockIncidentData = {
+ type: 'accident',
+ title: 'Accident',
+ comment: 'Test comment',
+ timestamp: '2025-03-28T12:00:00Z',
+ location: {
+ latitude: 40.7128,
+ longitude: -74.006,
+ },
+ };
+ const mockBaseUrl = 'http://mockserver.local';
+
+ afterEach(() => {
+ jest.clearAllMocks(); // Clear mocks after each test
+ });
+
+ it('should return data on successful response', async () => {
+ const mockResponse = { message: 'Incident reported successfully' };
+ fetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => mockResponse,
+ });
+
+ const result = await postIncident(mockIncidentData, mockBaseUrl);
+
+ expect(fetch).toHaveBeenCalledWith(`${mockBaseUrl}/report_incident`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(mockIncidentData),
+ });
+ expect(result).toEqual(mockResponse);
+ expect(global.alert).toHaveBeenCalledWith(mockResponse.message);
+ });
+
+ it('should throw an error on failed response', async () => {
+ fetch.mockResolvedValueOnce({
+ ok: false,
+ status: 500,
+ });
+
+ await expect(postIncident(mockIncidentData, mockBaseUrl)).rejects.toThrow(
+ 'HTTP error! status: 500',
+ );
+
+ expect(fetch).toHaveBeenCalledWith(`${mockBaseUrl}/report_incident`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(mockIncidentData),
+ });
+ });
+
+ it('should log an error on exception', async () => {
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
+ fetch.mockRejectedValueOnce(new Error('Network error'));
+
+ await expect(postIncident(mockIncidentData, mockBaseUrl)).rejects.toThrow(
+ 'Network error',
+ );
+
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ 'Error details:',
+ expect.any(Error),
+ );
+ consoleErrorSpy.mockRestore();
+ });
+});
diff --git a/server/src/Database_class.py b/server/src/Database_class.py
index 515844e..4db5213 100644
--- a/server/src/Database_class.py
+++ b/server/src/Database_class.py
@@ -266,7 +266,7 @@ def print_table(self, tablename: str):
records = cursor.fetchall()
# Print the results
- print(f"{tablename}:")
+ print(f"{tablename}: {records}")
for record in records:
print(record)
@@ -276,6 +276,29 @@ def print_table(self, tablename: str):
finally:
cursor.close()
+ def search_table(self, tablename: str):
+ """Search and return all entries in the current selected table
+
+ Args:
+ tablename (str): Name of table to be returned
+ """
+ try:
+ cursor = self.connection.cursor()
+
+ query = f"SELECT * FROM {tablename};"
+
+ cursor.execute(query)
+ records = cursor.fetchall()
+
+ self.connection.commit()
+
+ except Exception as e:
+ print("An error occurred:", e)
+ self.connection.rollback()
+ finally:
+ cursor.close()
+ return records
+
def close_con(self):
"""Close the connection"""
self.connection.close()
diff --git a/server/src/dublin_bike_api.py b/server/src/dublin_bike_api.py
index c32232f..79381a8 100644
--- a/server/src/dublin_bike_api.py
+++ b/server/src/dublin_bike_api.py
@@ -1,38 +1,84 @@
-from base_api import BaseAPI
import requests
+import math
+import os
+from dotenv import load_dotenv
-# URL of Open Data
-APIKEY = "3386ce10aca77dde762ab5c2de0177f7405cb6b3"
+load_dotenv()
-class bikeAPI(BaseAPI):
+class bikeAPI:
def __init__(self):
super().__init__()
- def get(self, apiKey):
+ def get(self, lat=53.349562, lng=-6.278198):
# URL of Open Data
- self.apiKey = apiKey
+ self.api_key = os.getenv("DUBLIN_BIKE_API_KEY")
+
self.url = (
"https://api.jcdecaux.com/vls/v1/stations?contract=dublin&apiKey="
- + APIKEY
+ + self.api_key
)
self.response = requests.get(self.url)
-
+ # count = 0
+ loadedBikestops = []
# Check if the request was successful
if self.response.status_code == 200:
for item in self.response.json():
- print(
- item["name"],
- " ",
- item["available_bikes"],
- (int(item["last_update"])),
+ # print(item['position']['lat'])
+ within_walking = self.are_coordinates_within_distance(
+ float(item["position"]["lat"]),
+ float(item["position"]["lng"]),
+ lat,
+ lng,
+ 0.5,
)
+ if within_walking:
+ loadedBikestops.append(item)
+ # print(loadedBikestops)
+ return loadedBikestops
else:
print("Failed to retrieve data:", self.response.status_code)
+ def are_coordinates_within_distance(
+ self, lat1, lon1, lat2, lon2, max_distance_km=5
+ ):
+ """
+ Determine if two GPS coordinates are within
+ a specified distance (default 5 km).
+
+ Uses the Haversine formula to calculate
+ the great-circle distance between two points.
+
+ Parameters:
+ lat1, lon1: Latitude and longitude of first point in decimal degrees
+ lat2, lon2: Latitude and longitude of second point in decimal degrees
+ max_distance_km: Maximum distance in kilometers (default 5)
+
+ Returns:
+ Boolean: True if points are within the specified distance,
+ False otherwise
+ """
+ # Convert decimal degrees to radians
+ lat1_rad = math.radians(lat1)
+ lon1_rad = math.radians(lon1)
+ lat2_rad = math.radians(float(lat2))
+ lon2_rad = math.radians(float(lon2))
+
+ # Radius of the Earth in kilometers
+ earth_radius = 6371.0
+
+ # Haversine formula
+ dlon = lon2_rad - lon1_rad
+ dlat = lat2_rad - lat1_rad
+
+ a = (
+ math.sin(dlat / 2) ** 2
+ + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2) ** 2
+ )
+ c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
+ distance = earth_radius * c
-bike = bikeAPI()
-bike.get(APIKEY)
+ return distance <= max_distance_km
diff --git a/server/src/incident_reporter.py b/server/src/incident_reporter.py
new file mode 100644
index 0000000..81253de
--- /dev/null
+++ b/server/src/incident_reporter.py
@@ -0,0 +1,99 @@
+from pydantic import BaseModel
+from dotenv import load_dotenv
+import logging
+from src.Database_class import DataBase
+
+
+class Location(BaseModel):
+ latitude: float
+ longitude: float
+
+
+class IncidentReport(BaseModel):
+ type: str
+ title: str
+ comment: str
+ timestamp: str
+ location: Location
+
+
+class IncidentReporter:
+ def __init__(self, api, logger: logging.Logger):
+ self.app = api
+ self.logger = logger
+
+ # Load environment vars
+ load_dotenv()
+
+ self.api_report_incident()
+ self.api_check_incident()
+
+ def api_report_incident(self):
+ @self.app.post("/report_incident")
+ async def report_incident(request: IncidentReport):
+
+ self.logger.info(f"Incident report for {request.type}")
+
+ response = self.db_handle_incident_report(request)
+
+ return {"message": response}
+
+ def db_handle_incident_report(self, request: IncidentReport):
+ try:
+ db = DataBase()
+ db.connect_db()
+ table_name = "incident_table"
+ type_column = "type"
+ title_column = "title"
+ comment_column = "comment"
+ timestamp_column = "timestamp"
+ latitude_column = "latitude"
+ longitude_column = "longitude"
+
+ incident_dict = {
+ type_column: request.type,
+ title_column: request.title,
+ comment_column: request.comment,
+ timestamp_column: request.timestamp,
+ latitude_column: request.location.latitude,
+ longitude_column: request.location.longitude,
+ }
+
+ db.add_entry(table_name, incident_dict)
+
+ return "Incident report sent successfully"
+ except Exception as e:
+ self.logger.error(f"Error reporting incident: {e}")
+ return "Error reporting incident"
+ finally:
+ db.close_con()
+
+ def api_check_incident(self):
+ @self.app.post("/check_incidents")
+ async def check_incident():
+ self.logger.info("Checking for incident reports in database")
+
+ response = self.db_handle_checking_incidents()
+
+ print(response)
+
+ return {"message": response}
+
+ def db_handle_checking_incidents(self):
+ try:
+ db = DataBase()
+ db.connect_db()
+ table_name = "incident_table"
+
+ data = db.search_table(table_name)
+
+ return data
+ except Exception as e:
+ self.logger.error(f"Error checking incidents: {e}")
+ return "Error checking incident"
+ finally:
+ db.close_con()
+
+
+if __name__ == "__main__":
+ pass
diff --git a/server/src/main.py b/server/src/main.py
index ebd30cb..43a77dd 100644
--- a/server/src/main.py
+++ b/server/src/main.py
@@ -1,4 +1,4 @@
-from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
+from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect, Query
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
@@ -11,8 +11,10 @@
from src.weatherApi import weatherAPI
from src.preferences import Preferences
from src.networking import Networking
+from src.incident_reporter import IncidentReporter
from src.wayfinding import wayfinding_router_setup
from src.sustainability import Sustainability
+from src.dublin_bike_api import bikeAPI
class Server:
@@ -29,6 +31,7 @@ def __init__(self):
self.login_logic = Login(self.app, self.logger)
self.connection_manager = ConnectionManager()
self.weather_api = weatherAPI()
+ self.bike_api = bikeAPI()
self.networking = Networking(self.app, self.logger)
self.preferences_logic = Preferences(self.app, self.logger)
self.sustainability = Sustainability(self.app, self.logger)
@@ -43,6 +46,7 @@ def __init__(self):
self.app.include_router(wayfinding_router, prefix="/wayfinding")
self.app.include_router(preferences_router, prefix="/preferences")
+ self.incident_reporter = IncidentReporter(self.app, self.logger)
# Configure CORS
self.configure_cors()
@@ -112,15 +116,43 @@ async def echo_message(message: Message):
raise
@self.app.get("/weather")
- async def get_weather():
- self.logger.info(f'{"Received weather API request"}')
+ async def get_weather(
+ longitude: str = Query(...), latitude: str = Query(...)
+ ):
+ self.logger.info(
+ f"Received weather API request: lat={latitude}, lng={longitude}" # noqa: E501
+ )
try:
- # Example coordinates for Dublin
- return self.weather_api.get(lat="-6.266155", lng="53.350140")
+ realtimeweatherdata, temperature = self.weather_api.get(
+ lat=latitude, lng=longitude
+ )
+ print(f"Temperature: {temperature}")
+ return {
+ "weather": realtimeweatherdata,
+ "temperature": temperature,
+ }
except Exception as e:
self.logger.error(f"Error hitting weather endpoint: {e}")
raise
+ @self.app.get("/BikeStand")
+ async def get_bikeStand(
+ longitude: str = Query(...), latitude: str = Query(...)
+ ):
+ self.logger.info(
+ f"Received bike API request: lat={latitude}, lng={longitude}"
+ )
+ try:
+ realtimeBikeInfo = self.bike_api.get(
+ lat=latitude, lng=longitude
+ )
+ print(f"real time bike info: {realtimeBikeInfo}")
+
+ return {"BikeInfo": realtimeBikeInfo}
+ except Exception as e:
+ self.logger.error(f"Error hitting BikeApi endpoint: {e}")
+ raise
+
@self.app.websocket("/ws/location")
async def websocket_endpoint(websocket: WebSocket):
await self.connection_manager.connect(websocket)
diff --git a/server/src/wayfinding.py b/server/src/wayfinding.py
index 93e67f7..017b3ca 100644
--- a/server/src/wayfinding.py
+++ b/server/src/wayfinding.py
@@ -30,7 +30,18 @@ def get_routes(
"key": GOOGLE_MAPS_API_KEY,
}
response = requests.get(url, params=parameters)
- return response.json()
+ data = response.json()
+
+ if data.get("status") != "OK":
+ return {
+ "error": True,
+ "status": data.get("status"),
+ "message": data.get(
+ "error_message", "Could not retrieve directions."
+ ),
+ }
+
+ return data # Return full successful response
def wayfinding_router_setup(preferences_logic: Preferences, logger):
@@ -74,10 +85,21 @@ def get_routes_with_preferences(
"alternatives": str(alternatives).lower(),
"key": GOOGLE_MAPS_API_KEY,
"username": username,
- "avoid": toAvoid,
+ "avoid": toAvoid
}
response = requests.get(url, params=parameters)
- return response.json()
+ data = response.json()
+
+ if data.get("status") != "OK":
+ return {
+ "error": True,
+ "status": data.get("status"),
+ "message": data.get(
+ "error_message", "Could not retrieve directions."
+ ),
+ }
+
+ return data # Return full successful response
return router
diff --git a/server/src/weatherApi.py b/server/src/weatherApi.py
index dd05869..01ef61e 100644
--- a/server/src/weatherApi.py
+++ b/server/src/weatherApi.py
@@ -1,5 +1,4 @@
import requests
-import numpy as np
# from base_api import BaseAPI
@@ -10,35 +9,58 @@ class weatherAPI: # (BaseAPI):
def __init__(self):
super().__init__()
+ self.who_table = {
+ 0: "sun",
+ 1: "sun",
+ 2: "sun",
+ 3: "sun",
+ 45: "cloud",
+ 48: "cloud",
+ 51: "rain",
+ 53: "rain",
+ 55: "rain",
+ 56: "rain",
+ 57: "rain",
+ 61: "rain",
+ 63: "rain",
+ 65: "rain",
+ 66: "rain",
+ 67: "rain",
+ 71: "rain",
+ 73: "rain",
+ 75: "rain",
+ 77: "rain",
+ 80: "rain",
+ 81: "rain",
+ 82: "rain",
+ 85: "rain",
+ 86: "rain",
+ 95: "thunder",
+ 96: "thunder",
+ 99: "thunder",
+ }
+
def get(self, lat, lng):
self.lat = lat
self.lng = lng
- self.url = f"https://api.open-meteo.com/v1/forecast?latitude={self.lat}&longitude={self.lng}&hourly=temperature_2m,apparent_temperature,precipitation,rain,snowfall,cloud_cover,wind_speed_10m&forecast_days=1&models=ecmwf_ifs025" # noqa: E501
+ self.url = f"https://api.open-meteo.com/v1/forecast?latitude={self.lat}&longitude={self.lng}¤t=temperature_2m,weather_code" # noqa: E501
# Fetch the geoJSON data from the URL
response = requests.get(self.url)
# Check if the request was successful
if response.status_code == 200:
- data = response.json()["hourly"]
- weather = np.column_stack(
- (
- data["time"],
- data["temperature_2m"],
- data["apparent_temperature"],
- data["precipitation"],
- data["rain"],
- data["snowfall"],
- data["cloud_cover"],
- data["wind_speed_10m"],
- )
- )
- print(weather)
+ data = response.json()["current"]
+ # weather = data['weather_code']
+
+ # print(data['temperature_2m'])
+
+ return self.who_table[data["weather_code"]], data["temperature_2m"]
else:
print("Failed to retrieve data:", response.status_code)
# To test
-# w = weatherAPI()
-# w.get('53.350140', '-6.266155')
+w = weatherAPI()
+w.get("53.350140", "-6.266155")
diff --git a/server/tests/test_database_connection.py b/server/tests/test_database_connection.py
index c7dec15..c01e6ce 100644
--- a/server/tests/test_database_connection.py
+++ b/server/tests/test_database_connection.py
@@ -175,6 +175,40 @@ def test_search_user_not_found(db):
assert result is False
+def test_search_table(db):
+ """Test search_table method when entry is found"""
+ db.connection = MagicMock()
+ cursor_mock = db.connection.cursor.return_value
+ cursor_mock.fetchall.return_value = [
+ ["Accident", "58.049", "-6.412"],
+ ["Police", "32.211", "10.909"],
+ ["Roadworks", "86.924", "-3.123"],
+ ]
+ table_name = "test_table"
+
+ result = db.search_table(table_name)
+ cursor_mock.execute.assert_called_once_with("SELECT * FROM test_table;")
+ cursor_mock.close.assert_called_once()
+ assert result == [
+ ["Accident", "58.049", "-6.412"],
+ ["Police", "32.211", "10.909"],
+ ["Roadworks", "86.924", "-3.123"],
+ ]
+
+
+def test_search_table_empty(db):
+ """Test search_table method when entry is found"""
+ db.connection = MagicMock()
+ cursor_mock = db.connection.cursor.return_value
+ cursor_mock.fetchall.return_value = []
+ table_name = "test_table"
+
+ result = db.search_table(table_name)
+ cursor_mock.execute.assert_called_once_with("SELECT * FROM test_table;")
+ cursor_mock.close.assert_called_once()
+ assert result == []
+
+
def test_close_con(db):
"""Test close_con method"""
db.connection = MagicMock()
diff --git a/server/tests/test_incident_reporter.py b/server/tests/test_incident_reporter.py
new file mode 100644
index 0000000..66aa4aa
--- /dev/null
+++ b/server/tests/test_incident_reporter.py
@@ -0,0 +1,89 @@
+import pytest
+from fastapi.testclient import TestClient
+from unittest.mock import patch, MagicMock
+from fastapi import FastAPI
+import logging
+from src.incident_reporter import IncidentReporter
+
+
+@pytest.fixture
+def test_app():
+ """
+ Create a FastAPI instance and attach the IncidentReporter routes
+ for testing.
+ """
+ app = FastAPI()
+ logger = logging.getLogger("test_logger")
+ incident_reporter = IncidentReporter(api=app, logger=logger) # noqa: F841
+ return TestClient(app)
+
+
+def test_report_incident_success(test_app):
+ """
+ Test for /report_incident endpoint where the incident is
+ reported successfully.
+ """
+ with patch("src.incident_reporter.DataBase") as mock_db_class:
+ mock_db = MagicMock()
+ mock_db.add_entry.return_value = None # Simulate successful DB entry
+ mock_db_class.return_value = mock_db
+
+ incident_data = {
+ "type": "accident",
+ "title": "Accident",
+ "comment": "Test comment",
+ "timestamp": "2025-03-28T12:00:00Z",
+ "location": {
+ "latitude": 40.7128,
+ "longitude": -74.006,
+ },
+ }
+
+ response = test_app.post("/report_incident", json=incident_data)
+
+ assert response.status_code == 200
+ assert response.json()["message"] == "Incident report sent successfully"
+
+
+def test_report_incident_invalid_data(test_app):
+ """
+ Test for /report_incident endpoint where invalid data is sent.
+ """
+ invalid_data = {
+ "type": "accident",
+ "title": "Accident",
+ # Missing required fields like "comment", "timestamp", and "location"
+ }
+
+ response = test_app.post("/report_incident", json=invalid_data)
+
+ assert response.status_code == 422 # Unprocessable Entity
+ assert "detail" in response.json()
+
+
+def test_report_incident_internal_server_error(test_app):
+ """
+ Test for /report_incident endpoint where an internal server error occurs.
+ """
+ with patch("src.incident_reporter.DataBase") as mock_db_class:
+ mock_db = MagicMock()
+ mock_db.add_entry.side_effect = Exception(
+ "Database error"
+ ) # Simulate DB error
+ mock_db_class.return_value = mock_db
+
+ incident_data = {
+ "type": "accident",
+ "title": "Accident",
+ "comment": "Test comment",
+ "timestamp": "2025-03-28T12:00:00Z",
+ "location": {
+ "latitude": 40.7128,
+ "longitude": -74.006,
+ },
+ }
+
+ response = test_app.post("/report_incident", json=incident_data)
+
+ assert response.status_code == 200
+ assert response.json()["message"] == "Error reporting incident"