[frontend]: Add filter to display only items in a range
This commit is contained in:
@ -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>
|
||||||
}
|
}
|
||||||
|
@ -133,4 +133,4 @@ const MapComponent = ({ markers, clusteringEnabled }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MapComponent;
|
export default MapComponent;
|
@ -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
|
||||||
|
Reference in New Issue
Block a user