[frontend]: Add filter to display only items in a range

This commit is contained in:
2025-03-12 19:17:56 +00:00
parent f0467b7fdd
commit 2ca4a8f3d0
3 changed files with 119 additions and 70 deletions

View File

@ -34,22 +34,66 @@ function App() {
const [filteredMarkers, setFilteredMarkers] = useState([]); const [filteredMarkers, setFilteredMarkers] = useState([]);
const [numMarkers, setNumMarkers] = useState(0); const [numMarkers, setNumMarkers] = useState(0);
const [userLocation, setUserLocation] = useState(null);
const [userLocationAvailable, setUserLocationAvailable] = useState(false);
useEffect(() => {
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition(
(position) => {
setUserLocation([position.coords.latitude, position.coords.longitude]);
setUserLocationAvailable(true);
},
(error) => {
console.error("Error getting location:", error);
setUserLocation([53.4494762, -7.5029786]);
setUserLocationAvailable(false);
},
{
enableHighAccuracy: true,
timeout: 2000,
maximumAge: 0
}
);
} else {
setUserLocation([53.4494762, -7.5029786]);
setUserLocationAvailable(false);
}
}, []);
const handleSearchChange = (e) => { const handleSearchChange = (e) => {
const value = e.target.value; const value = e.target.value;
// Clear any existing timeout to reset the debounce timer
if (debounceTimeout.current) { if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current); clearTimeout(debounceTimeout.current);
} }
// Set a new timeout to update the state only after 300ms of inactivity
debounceTimeout.current = setTimeout(() => { debounceTimeout.current = setTimeout(() => {
setSearchTerm(value); // Only update state after delay setSearchTerm(value);
}, 300); }, 300);
}; };
// calculate distance between 2 points
function haversineDistance(coord1, coord2) {
const R = 6371; // Radius of the Earth in km
const toRad = (angle) => angle * (Math.PI / 180);
const fetchData = async (enabledSources) => { const [lat1, lon1] = coord1;
const [lat2, lon2] = coord2;
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // Distance in km
}
const fetchData = async (enabledSources, numberInputValue) => {
setLoading(true); setLoading(true);
try { try {
const transientTypes = dataSources.filter(({ id, api }) => enabledSources.includes(id) && api === "transient").map(({ objectType }) => objectType); const transientTypes = dataSources.filter(({ id, api }) => enabledSources.includes(id) && api === "transient").map(({ objectType }) => objectType);
@ -141,7 +185,6 @@ function App() {
punctualityStr = "N/A"; punctualityStr = "N/A";
} }
// set icon depending on lateness of train and type
if (punctualityStr === "early") { if (punctualityStr === "early") {
latenessMessage = -punctuality + " minute" + (punctuality === -1 ? "" : "s") + " early"; latenessMessage = -punctuality + " minute" + (punctuality === -1 ? "" : "s") + " early";
icon += "OnTime"; icon += "OnTime";
@ -181,11 +224,12 @@ function App() {
markerText = item.trainPublicMessage + " " + item.trainDirection; markerText = item.trainPublicMessage + " " + item.trainDirection;
display = display =
((item.latitude !== "0" && item.longitude !== "0") && // filter out trains with no location data ((item.latitude !== "0" && item.longitude !== "0") &&
((showMainline && trainType == "Mainline") || (showSuburban && trainType == "Suburban") || (showDart && trainType == "DART")) && ((showMainline && trainType == "Mainline") || (showSuburban && trainType == "Suburban") || (showDart && trainType == "DART")) &&
((showRunning && trainStatus == "Running") || (showNotYetRunning && trainStatus == "Not yet running") || (showTerminated && trainStatus == "Terminated")) && ((showRunning && trainStatus == "Running") || (showNotYetRunning && trainStatus == "Not yet running") || (showTerminated && trainStatus == "Terminated")) &&
((trainStatus == "Running" && showEarly && punctualityStr == "early") || (trainStatus == "Running" && showOnTime && punctualityStr == "On time") || (trainStatus == "Running" && showLate && punctualityStr == "late") ((trainStatus == "Running" && showEarly && punctualityStr == "early") || (trainStatus == "Running" && showOnTime && punctualityStr == "On time") || (trainStatus == "Running" && showLate && punctualityStr == "late")
|| (trainStatus == "Not yet running" && showNotYetRunning) || (trainStatus == "Terminated" && showTerminated))); || (trainStatus == "Not yet running" && showNotYetRunning) || (trainStatus == "Terminated" && showTerminated))) &&
(userLocationAvailable ? haversineDistance(userLocation, [item.latitude, item.longitude]) < numberInputValue : true);
break; break;
@ -196,7 +240,8 @@ function App() {
); );
markerText = item.trainStationCode + " " + item.trainStationDesc; markerText = item.trainStationCode + " " + item.trainStationDesc;
display = (item.latitude !== "0" && item.longitude !== "0"); display = (item.latitude !== "0" && item.longitude !== "0") &&
(userLocationAvailable ? haversineDistance(userLocation, [item.latitude, item.longitude]) < numberInputValue : true);
break; break;
@ -216,7 +261,8 @@ function App() {
); );
markerText = item.busRouteAgencyName + " " + item.busRouteShortName + " " + item.busRouteLongName; markerText = item.busRouteAgencyName + " " + item.busRouteShortName + " " + item.busRouteLongName;
display = (item.latitude !== "0" && item.longitude !== "0"); display = (item.latitude !== "0" && item.longitude !== "0") &&
(userLocationAvailable ? haversineDistance(userLocation, [item.latitude, item.longitude]) < numberInputValue : true);
break; break;
@ -234,7 +280,8 @@ function App() {
); );
markerText = item.busStopName; markerText = item.busStopName;
display = (item.latitude !== "0" && item.longitude !== "0"); display = (item.latitude !== "0" && item.longitude !== "0") &&
(userLocationAvailable ? haversineDistance(userLocation, [item.latitude, item.longitude]) < numberInputValue : true);
break; break;
@ -264,7 +311,8 @@ function App() {
(showGreenLine && luasLine === "Green Line" || showRedLine && luasLine === "Red Line") && (showGreenLine && luasLine === "Green Line" || showRedLine && luasLine === "Red Line") &&
(showEnabled && item.luasStopIsEnabled === "1" || showDisabled && item.luasStopIsEnabled === "0") && (showEnabled && item.luasStopIsEnabled === "1" || showDisabled && item.luasStopIsEnabled === "0") &&
(!showCycleAndRide || (showCycleAndRide && item.luasStopIsCycleAndRide === "1")) && (!showCycleAndRide || (showCycleAndRide && item.luasStopIsCycleAndRide === "1")) &&
(!showParkAndRide || (showParkAndRide && item.luasStopIsParkAndRide === "1")) (!showParkAndRide || (showParkAndRide && item.luasStopIsParkAndRide === "1")) &&
(userLocationAvailable ? haversineDistance(userLocation, [item.latitude, item.longitude]) < numberInputValue : true)
); );
break; break;
@ -296,25 +344,6 @@ function App() {
setLoading(false); setLoading(false);
}; };
// 2. Memoize the filtered markers so it recalculates only if `searchTerm` or `markers` changes
// const filteredMarkers = useMemo(() => {
// setLoading(true);
// console.log("set loading true");
//
// if (!searchTerm.trim()) {
// setLoading(false);
// return markers;
// }
// const newMarkers = markers.filter((marker) =>
// marker.markerText.includes(searchTerm.toLowerCase())
// );
//
// setLoading(false);
// console.log("set loading false");
// return newMarkers;
//
// }, [searchTerm, markers]);
const memoizedFilteredMarkers = useMemo(() => { const memoizedFilteredMarkers = useMemo(() => {
return markers.filter(marker => return markers.filter(marker =>
marker.markerText.includes(searchTerm.toLowerCase()) marker.markerText.includes(searchTerm.toLowerCase())
@ -323,24 +352,23 @@ function App() {
useEffect(() => { useEffect(() => {
if (numMarkers > 500) { if (numMarkers > 500) {
setLoading(true); // Start loading immediately setLoading(true);
} }
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
setFilteredMarkers(memoizedFilteredMarkers); // Update markers setFilteredMarkers(memoizedFilteredMarkers);
// Now wait 10 seconds before setting loading to false
if (numMarkers > 500) { if (numMarkers > 500) {
const loadingTimeout = setTimeout(() => { const loadingTimeout = setTimeout(() => {
setLoading(false); // Stop loading after 10 seconds setLoading(false);
}, 5000); }, 3000);
} }
return () => clearTimeout(loadingTimeout); // Cleanup loading timeout return () => clearTimeout(loadingTimeout);
}, 0); // Small debounce before filtering }, 0);
return () => clearTimeout(timeout); // Cleanup initial debounce timeout return () => clearTimeout(timeout);
}, [memoizedFilteredMarkers]); }, [memoizedFilteredMarkers]);
return ( return (
@ -351,30 +379,29 @@ function App() {
path="/" path="/"
element={ element={
<div style={{ height: "100vh", width: "100vw", display: "flex", position: "relative", paddingTop: "5vh" }}> <div style={{ height: "100vh", width: "100vw", display: "flex", position: "relative", paddingTop: "5vh" }}>
{loading && <LoadingOverlay message={"Loading data..."} />} <div {loading && <LoadingOverlay message={"Loading data..."} />} <div
style={{
position: "absolute",
top: "1vh",
height: "5vh",
width: "250px", minWidth: "50px",
left: "50%",
transform: "translateX(-50%)",
zIndex: 1000
}}
>
<input
type="text"
onChange={(e) => handleSearchChange(e)}
placeholder="Search..."
style={{ style={{
position: "absolute", width: "250px", fontSize: "16px",
top: "1vh", top: "6vh", marginTop: "5vh",
height: "5vh", padding: "10px", background: "rgba(255, 255, 255, 0.9)", color: "black",
width: "250px", minWidth: "50px", borderRadius: "10px", overflow: "hidden"
left: "50%",
transform: "translateX(-50%)",
zIndex: 1000
}} }}
> />
<input </div>
type="text"
// value={searchInput}
onChange={(e) => handleSearchChange(e)}
placeholder="Search..."
style={{
width: "250px", fontSize: "16px",
top: "6vh", marginTop: "5vh",
padding: "10px", background: "rgba(255, 255, 255, 0.9)", color: "black",
borderRadius: "10px", overflow: "hidden"
}}
/>
</div>
<Sidebar <Sidebar
selectedSources={selectedSources} selectedSources={selectedSources}
@ -382,9 +409,10 @@ function App() {
clusteringEnabled={clusteringEnabled} clusteringEnabled={clusteringEnabled}
setClusteringEnabled={setClusteringEnabled} setClusteringEnabled={setClusteringEnabled}
fetchData={fetchData} fetchData={fetchData}
userLocationAvailable={userLocationAvailable}
/> />
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<MapComponent markers={filteredMarkers} clusteringEnabled={clusteringEnabled} /> <MapComponent markers={filteredMarkers} clusteringEnabled={clusteringEnabled} userLocationAvailable={userLocationAvailable} />
</div> </div>
</div> </div>
} }

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useRef } from "react";
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Cookies from "js-cookie"; import Cookies from "js-cookie";
@ -131,9 +131,10 @@ const CheckboxItem = ({ item, selectedSources, setSelectedSources, enabledSource
); );
}; };
const Sidebar = ({ selectedSources, setSelectedSources, clusteringEnabled, setClusteringEnabled, fetchData }) => { const Sidebar = ({ selectedSources, setSelectedSources, clusteringEnabled, setClusteringEnabled, fetchData, userLocationAvailable }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [enabledSources, setEnabledSources] = useState([]); // New state to track enabled sources const [enabledSources, setEnabledSources] = useState([]); // New state to track enabled sources
const [numberInputValue, setNumberInputValue] = useState(""); // State to manage number input value
// Load selected sources from cookies or set all as default checked // Load selected sources from cookies or set all as default checked
useEffect(() => { useEffect(() => {
@ -144,11 +145,18 @@ const Sidebar = ({ selectedSources, setSelectedSources, clusteringEnabled, setCl
const allDefaultChecked = getAllDefaultCheckedIds(menuData); const allDefaultChecked = getAllDefaultCheckedIds(menuData);
setSelectedSources(allDefaultChecked); setSelectedSources(allDefaultChecked);
} }
// Load numberInputValue from cookie
const savedNumberInputValue = Cookies.get("numberInputValue");
if (savedNumberInputValue) {
setNumberInputValue(savedNumberInputValue);
}
}, [setSelectedSources]); }, [setSelectedSources]);
const handleSubmit = () => { const handleSubmit = () => {
Cookies.set("selectedSources", JSON.stringify(selectedSources), { expires: 7 }); Cookies.set("selectedSources", JSON.stringify(selectedSources));
fetchData(enabledSources); // Use enabledSources for data fetching Cookies.set("numberInputValue", numberInputValue); // Save numberInputValue to cookie
fetchData(enabledSources, numberInputValue); // Use enabledSources for data fetching
}; };
return ( return (
@ -184,6 +192,18 @@ const Sidebar = ({ selectedSources, setSelectedSources, clusteringEnabled, setCl
/> />
<label htmlFor="toggleClustering">Cluster overlapping icons</label> <label htmlFor="toggleClustering">Cluster overlapping icons</label>
</div> </div>
{userLocationAvailable && (
<div style={{marginTop: "10px", display: "flex", alignItems: "center", gap: "8px"}}>
<label htmlFor="numberInput" style={{maxWidth: "40%"}}>Within KM:</label>
<input
type="number"
id="numberInput"
value={numberInputValue}
onChange={(e) => setNumberInputValue(e.target.value)}
style={{maxWidth: "40%"}}
/>
</div>
)}
<button onClick={handleSubmit} style={{marginTop: "10px", color: "white"}}>Submit</button> <button onClick={handleSubmit} style={{marginTop: "10px", color: "white"}}>Submit</button>
</div> </div>
)} )}
@ -197,6 +217,7 @@ Sidebar.propTypes = {
clusteringEnabled: PropTypes.bool.isRequired, clusteringEnabled: PropTypes.bool.isRequired,
setClusteringEnabled: PropTypes.func.isRequired, setClusteringEnabled: PropTypes.func.isRequired,
fetchData: PropTypes.func.isRequired, fetchData: PropTypes.func.isRequired,
userLocationAvailable: PropTypes.bool.isRequired,
}; };
export default Sidebar; export default Sidebar