diff --git a/front-end/src/App.js b/front-end/src/App.js index beea306..7ae150d 100644 --- a/front-end/src/App.js +++ b/front-end/src/App.js @@ -1,7 +1,8 @@ -import React, { useState, useEffect, useCallback, createContext } from 'react'; // Import useState and useEffect +import React, { useState, useEffect, createContext } from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; + +// Import components import MapPage from './components/MapPage'; -import './css/navBar.css'; import NavBar from './components/NavBar'; import RoutesPage from './components/RoutesPage'; import RoutesSubpage from './components/RoutesSubpage'; @@ -13,86 +14,81 @@ import FeedbackSupportPage from './components/settings/FeedbackSupportPage'; import PrivacyPolicyPage from './components/settings/PrivacyPolicyPage'; import LoadingScreen from './components/LoadingScreen'; import TutorialComponent from './components/TutorialComponent'; + +// Import hooks and utilities import useDarkMode from './hooks/darkMode'; import { registerService } from './utils/serviceRegister'; -import { getUserPos } from './utils/mapUtility'; +import { getUserPos, loadGoogleMapsAPI } from './utils/mapUtility'; + +// Import CSS import './index.css'; +import './css/navBar.css'; import './css/tutorialComponent.css'; -export const TutorialContext = createContext(); // create context for tutorial that is shared between all pages +export const TutorialContext = createContext(); function App() { const [colorTheme, setTheme] = useDarkMode(); const [isLoading, setIsLoading] = useState(true); - const [firstTime, setFirstTime] = useState(localStorage.getItem('isFirst')); // check if it is the first time the user is using the app - const [tutorialIndex, setTutorialIndex] = useState(0); // keep track which pages has been clicked on the tutorial - const [tutorialOn, setTutorialOn] = useState(false); // if tutorial is on - useEffect(() => { - localStorage.getItem('isFirst') == 'false' ? setFirstTime('false') : setFirstTime('true'); - //get a list of all local storage items, for debugging purposes - const localStorageItems = {}; - for (let i = 0; i < localStorage.length; i++) { - let key = localStorage.key(i); - let value = localStorage.getItem(key); - localStorageItems[key] = value; - } - setTimeout(() => { - if (firstTime == 'true' || firstTime == null || firstTime == 'null') { - console.log('<--------First time user detected-------->'); - console.log('Initializing local storage items...'); - localStorage.setItem('isFirst', false); // set first time to false - } else { - console.log('<--------Returning user detected-------->'); - console.log('Local storage items:'); - console.log(localStorageItems); - } - setIsLoading(false); - }, 3000); + const isFirstTimeUser = localStorage.getItem('isFirst') !== 'false'; + const [tutorialIndex, setTutorialIndex] = useState(0); + const [tutorialOn, setTutorialOn] = useState(isFirstTimeUser); + const localStorageItems = {}; + for (let i = 0; i < localStorage.length; i++) { + let key = localStorage.key(i); + let value = localStorage.getItem(key); + localStorageItems[key] = value; + } - window.addEventListener('keydown', devTools); // add button press even listeners for dev tools - - // if first time is null, set it to true + useEffect(() => { + initializeLocalStorage(isFirstTimeUser); + loadGoogleMapsAPI(() => setIsLoading(false)); + window.addEventListener('keydown', devTools); registerService(); getUserPos(); + + return () => window.removeEventListener('keydown', devTools); }, []); - useEffect(() => { - firstTime == 'true' ? setTutorialOn(true) : setTutorialOn(false); - }, [firstTime]); + const initializeLocalStorage = (isFirstTime) => { + if (isFirstTime) { + console.log('<--------First time user detected-------->'); + console.log('Initializing local storage items...'); + localStorage.setItem('isFirst', false); + } else { + console.log('<--------Returning user detected-------->'); + console.log('Local storage items:', localStorageItems); + } + }; const devTools = (e) => { - switch (e.keyCode) { - case 82: //press r to reset local storage - console.log('Resetting local storage...'); - localStorage.clear(); - break; + if (e.keyCode === 82) { + // R key + console.log('Resetting local storage...'); + localStorage.clear(); } }; return ( - - {' '} - {/* Share the context without passing the prop to each page */} +
{!isLoading && tutorialOn && } - {isLoading && }{' '} - {/* Putting the loading component here so that loading screen appears when refreshing as well */} - {!isLoading && } {/* Hides navbar when loading */} - - } /> {/*Goes to loading on app boot*/} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - + {isLoading && } {!isLoading && } {/* Hides navbar when loading */} + {!isLoading && ( + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + )}
diff --git a/front-end/src/components/NavBar.js b/front-end/src/components/NavBar.js index 139dcfe..883e709 100644 --- a/front-end/src/components/NavBar.js +++ b/front-end/src/components/NavBar.js @@ -1,79 +1,90 @@ -import '../css/navBar.css' +import '../css/navBar.css'; import { ReactComponent as MapIcon } from '../images/map.svg'; import { ReactComponent as AlertIcon } from '../images/alert.svg'; import { ReactComponent as RouteIcon } from '../images/route.svg'; import { ReactComponent as SettingIcon } from '../images/gears.svg'; -import { useState, useEffect , useContext } from 'react'; +import { useState, useEffect, useContext } from 'react'; import { useNavigate } from 'react-router-dom'; import { TutorialContext } from '../App'; -import '../css/tutorialComponent.css' +import '../css/tutorialComponent.css'; import TutorialComponent from './TutorialComponent'; function NavBar() { - const [navBarState, setNavBarState] = useState('Map'); - const {tutorialIndex, setTutorialIndex, firstTime, setFirstTime, tutorialOn, setTutorialOn} = useContext(TutorialContext); - const navigate = useNavigate(); - const handleClick = (iconName) => { - setNavBarState(iconName); - } - - useEffect(() => { - let overlay = document.getElementsByClassName('overlay')[0]; - let cur = document.getElementById(navBarState); - if (cur.classList.contains('inactive')) { // when a inactive icon is clicked - document.getElementsByClassName('active')[0].classList.add('inactive'); - document.getElementsByClassName('active')[0].classList.remove('active'); - cur.classList.remove('inactive'); - cur.classList.add('active'); - switch(navBarState) { //change the functionalities of each button here - case 'Map': - overlay.style.left = '-36%'; //shifts the overlay - navigate('/map') - break; - case 'Routes': - overlay.style.left = '-12%'; - navigate('/routes') - break; - case 'Alerts': - overlay.style.left = '12%'; - navigate('/alerts') - break; - case 'Settings': - overlay.style.left = '36%'; - navigate('/settings') - break; - } - } - }, [navBarState]); + const getPath = () => window.location.pathname.split('/')[1] || 'map'; + const [navigationState, setnavigationState] = useState(getPath()); + const { tutorialIndex, setTutorialIndex, firstTime, setFirstTime, tutorialOn, setTutorialOn } = + useContext(TutorialContext); + const navigate = useNavigate(); + const handleClick = (iconName) => { + setnavigationState(iconName); + }; - return ( - <> -
-
-
-
handleClick('Map')}> - -

Map

-
-
handleClick('Routes')}> - -

Routes

-
-
handleClick('Alerts')}> - -

Alerts

-
-
handleClick('Settings')}> - -

Settings

-
-
- - ); -} + const updateNavBarDisplay = (to) => { + const overlay = document.getElementsByClassName('overlay')[0]; + const cur = document.getElementById(to); + if (cur.classList.contains('inactive')) { + // when a inactive icon is clicked + document.getElementsByClassName('active')[0].classList.add('inactive'); + document.getElementsByClassName('active')[0].classList.remove('active'); + cur.classList.remove('inactive'); + cur.classList.add('active'); + //change the functionalities of each button here + switch (to) { + case 'map': + overlay.style.left = '-36%'; //shifts the overlay + break; + case 'routes': + overlay.style.left = '-12%'; + break; + case 'alerts': + overlay.style.left = '12%'; + break; + case 'settings': + overlay.style.left = '36%'; + break; + } + } + }; + useEffect(() => { + updateNavBarDisplay(navigationState); + if (navigationState !== getPath()) { + navigate('/' + navigationState); + } + }, [navigationState]); + useEffect(() => { + // Send first time users to the map page + if (localStorage.getItem('isFirst') !== 'true') { + setnavigationState('map'); + } + }, []); + return ( + <> +
+
+
+
handleClick('map')}> + +

Map

+
+
handleClick('routes')}> + +

Routes

+
+
handleClick('alerts')}> + +

Alerts

+
+
handleClick('settings')}> + +

Settings

+
+
+ + ); +} -export default NavBar; \ No newline at end of file +export default NavBar; diff --git a/front-end/src/utils/mapUtility.js b/front-end/src/utils/mapUtility.js index aa3a760..afd3c9a 100644 --- a/front-end/src/utils/mapUtility.js +++ b/front-end/src/utils/mapUtility.js @@ -43,15 +43,17 @@ const CALLBACK_NAME = 'gmapAPICallback'; const POS_DEFAULT = [40.716503, -73.976077]; export function loadGoogleMapsAPI(callback) { + let c = typeof callback == 'function' ? callback : () => {}; + if (!window.google || !window.google.maps) { - window.gmapAPICallback = () => callback(true); + window.gmapAPICallback = () => c(true); const script = document.createElement('script'); script.src = API_BASE + `?key=${API_KEY}&libraries=${API_LIBRARIES.join(',')}&callback=${CALLBACK_NAME}`; script.async = true; document.head.appendChild(script); } else { - callback(true); + c(true); } } diff --git a/front-end/src/utils/transportMarker.js b/front-end/src/utils/transportMarker.js index baec0ad..809c47e 100644 --- a/front-end/src/utils/transportMarker.js +++ b/front-end/src/utils/transportMarker.js @@ -1,4 +1,4 @@ -const MAX_ANIMATION_DURATION = 7000; +const MAX_ANIMATION_DURATION = 9000; export function updateTransportMarkers(transportData, markerRef, map) { if (!transportData) { @@ -16,20 +16,25 @@ export function updateTransportMarkers(transportData, markerRef, map) { // Process new and existing transport data Object.keys(transportData).forEach((transport) => { const transportInfo = transportData[transport][0]; + const marker = markerRef.current[transport]; const lat = parseFloat(transportInfo.latitude); const lng = parseFloat(transportInfo.longitude); const newPosition = new window.google.maps.LatLng(lat, lng); - const newIcon = generateTransportMarkerIcon(transportInfo.color, transportInfo.calculatedCourse); + const iconUpdate = marker && marker.direction !== transportInfo.calculatedCourse; - if (markerRef.current[transport]) { + if (marker) { // Update the position of the existing marker - const currentPosition = markerRef.current[transport].getPosition(); - animateMarker(markerRef.current[transport], currentPosition, newPosition, MAX_ANIMATION_DURATION); + const currentPosition = marker.getPosition(); + animateMarker(marker, currentPosition, newPosition, MAX_ANIMATION_DURATION); // Update the icon of the existing marker - markerRef.current[transport].setIcon(newIcon); + if (iconUpdate) { + const newIcon = generateTransportMarkerIcon(transportInfo.color, transportInfo.calculatedCourse); + marker.setIcon(newIcon); + } } else { // Create a new marker + const newIcon = generateTransportMarkerIcon(transportInfo.color, transportInfo.calculatedCourse); let transportMarker = createTransportMarker(newPosition, transportInfo, map, newIcon); markerRef.current[transport] = transportMarker; } @@ -48,6 +53,7 @@ function createTransportMarker(position, transportInfo, map) { content: `
No.${transportInfo.busId}
Line: ${transportInfo.route}
`, }); + transportMarker.direction = transportInfo.calculatedCourse; transportMarker.addListener('click', () => { infowindow.open(map, transportMarker); console.log(transportInfo); // tansportation info @@ -56,18 +62,21 @@ function createTransportMarker(position, transportInfo, map) { return transportMarker; } -function animateMarker(marker, startPosition, endPosition, minBusQueryInterval) { +function animateMarker(marker, startPosition, endPosition, duration) { const distanceThreshold = { min: 10, max: 300 }; - const dynamicDuration = minBusQueryInterval + 2000; const distance = window.google.maps.geometry.spherical.computeDistanceBetween(startPosition, endPosition); + if (window.cancelAnimationFrame) { + window.cancelAnimationFrame(marker.animationHandler); + } else { + clearTimeout(marker.animationHandler); + } + if (distance < distanceThreshold.min || distance > distanceThreshold.max) { marker.setPosition(endPosition); } else { - let startTime = null; - - const easeOutQuad = (t) => t * (2 - t); + let startTime; const animate = (currentTime) => { if (!startTime) { @@ -75,13 +84,19 @@ function animateMarker(marker, startPosition, endPosition, minBusQueryInterval) } const elapsedTime = currentTime - startTime; - let progress = elapsedTime / dynamicDuration; - progress = easeOutQuad(Math.min(1, progress)); + let progress = Math.min(1, elapsedTime / duration); if (progress < 1) { - const nextPosition = window.google.maps.geometry.spherical.interpolate(startPosition, endPosition, progress); - marker.setPosition(nextPosition); - window.requestAnimationFrame(animate); + const newPosition_lat = startPosition.lat() + (endPosition.lat() - startPosition.lat()) * progress; + const newPosition_lng = startPosition.lng() + (endPosition.lng() - startPosition.lng()) * progress; + var deltaPosition = new window.google.maps.LatLng(newPosition_lat, newPosition_lng); + marker.setPosition(deltaPosition); + + if (window.requestAnimationFrame) { + marker.animationHandler = window.requestAnimationFrame(animate); + } else { + marker.animationHandler = setTimeout(animate, 17); + } } else { marker.setPosition(endPosition); }