341 lines
16 KiB
JavaScript
341 lines
16 KiB
JavaScript
import React, { useState, useEffect, useMemo } from "react";
|
|
import Sidebar from "./components/Sidebar";
|
|
import MapComponent from "./components/MapComponent";
|
|
import LoadingOverlay from "./components/LoadingOverlay";
|
|
import LuasPopup from "./components/LuasPopup";
|
|
|
|
const TRANSIENT_DATA_API = "https://281bc6mcm5.execute-api.us-east-1.amazonaws.com/transient_data";
|
|
const PERMANENT_DATA_API = "https://a6y312dpuj.execute-api.us-east-1.amazonaws.com/permanent_data";
|
|
|
|
const dataSources = [
|
|
{ id: "irish-rail-trains", name: "Irish Rail Trains", url: `${TRANSIENT_DATA_API}?objectType=IrishRailTrain` },
|
|
{ id: "irish-rail-stations", name: "Irish Rail Stations", url: `${PERMANENT_DATA_API}?objectType=IrishRailStation` },
|
|
{ id: "luas-stops", name: "Luas Stops", url: `${PERMANENT_DATA_API}?objectType=LuasStop` },
|
|
{ id: "bus-stops", name: "Bus Stops", url: `${PERMANENT_DATA_API}?objectType=BusStop` },
|
|
{ id: "buses", name: "Buses", url: `${TRANSIENT_DATA_API}?objectType=Bus` },
|
|
];
|
|
|
|
function App() {
|
|
const [selectedSources, setSelectedSources] = useState([]);
|
|
const [markers, setMarkers] = useState([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [clusteringEnabled, setClusteringEnabled] = useState(true);
|
|
|
|
// Search states: one is the raw user input, the other is the actual term we filter on
|
|
const [searchInput, setSearchInput] = useState("");
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
|
|
// Debounce effect
|
|
useEffect(() => {
|
|
const handler = setTimeout(() => {
|
|
setSearchTerm(searchInput);
|
|
}, 300); // Adjust this delay as desired
|
|
|
|
return () => {
|
|
clearTimeout(handler);
|
|
};
|
|
}, [searchInput]);
|
|
|
|
const fetchData = async (enabledSources) => {
|
|
setLoading(true);
|
|
try {
|
|
const newMarkers = (await Promise.all(
|
|
dataSources
|
|
.filter(({ id }) => enabledSources.includes(id))
|
|
.map(({ url }) => fetch(url).then((res) => res.json()))
|
|
))
|
|
.flat()
|
|
.map((item) => {
|
|
const showMainline = enabledSources.includes("mainline");
|
|
const showSuburban = enabledSources.includes("suburban");
|
|
const showDart = enabledSources.includes("dart");
|
|
const showRunning = enabledSources.includes("running");
|
|
const showNotYetRunning = enabledSources.includes("not-yet-running");
|
|
const showTerminated = enabledSources.includes("terminated");
|
|
const showEarly = enabledSources.includes("early");
|
|
const showOnTime = enabledSources.includes("on-time");
|
|
const showLate = enabledSources.includes("late");
|
|
|
|
const showRedLine = enabledSources.includes("red-line");
|
|
const showGreenLine = enabledSources.includes("green-line");
|
|
const showParkAndRide = enabledSources.includes("park-and-ride");
|
|
const showCycleAndRide = enabledSources.includes("cycle-and-ride");
|
|
const showEnabled = enabledSources.includes("enabled");
|
|
const showDisabled = enabledSources.includes("disabled");
|
|
|
|
let icon = item.objectType;
|
|
let popupContent;
|
|
let objectTitle;
|
|
let display = false;
|
|
let markerText = "";
|
|
|
|
switch (item.objectType) {
|
|
case "IrishRailTrain":
|
|
objectTitle = "Irish Rail Train: " + item.trainCode;
|
|
let trainType;
|
|
switch (item.trainType) {
|
|
case "M":
|
|
trainType = "Mainline";
|
|
icon = "mainline";
|
|
break;
|
|
case "S":
|
|
trainType = "Suburban";
|
|
icon = "suburban";
|
|
break;
|
|
case "D":
|
|
trainType = "DART";
|
|
icon = "dart";
|
|
break;
|
|
default:
|
|
trainType = "Unknown";
|
|
}
|
|
|
|
let trainStatus;
|
|
switch (item.trainStatus) {
|
|
case "R":
|
|
trainStatus = "Running";
|
|
break;
|
|
|
|
case "T":
|
|
trainStatus = "Terminated";
|
|
break;
|
|
|
|
case "N":
|
|
trainStatus = "Not yet running";
|
|
break;
|
|
|
|
default:
|
|
trainStatus = "Unknown";
|
|
}
|
|
|
|
const splitMessage = item.trainPublicMessage.split("\\n");
|
|
const match = splitMessage[1].match(/(-?\d+)\s+mins\s+late/);
|
|
const punctuality = match ? parseInt(match[1], 10) : NaN;
|
|
let latenessMessage;
|
|
let punctualityStr;
|
|
|
|
if (punctuality < 0) {
|
|
punctualityStr = "early";
|
|
} else if (punctuality === 0) {
|
|
punctualityStr = "On time";
|
|
} else if (punctuality > 0) {
|
|
punctualityStr = "late";
|
|
} else {
|
|
punctualityStr = "N/A";
|
|
}
|
|
|
|
// set icon depending on lateness of train and type
|
|
if (punctualityStr === "early") {
|
|
latenessMessage = -punctuality + " minute" + (punctuality === 1 ? "" : "s") + " early";
|
|
icon += "OnTime";
|
|
}
|
|
else if (punctualityStr === "On time") {
|
|
latenessMessage = punctualityStr;
|
|
icon += "OnTime";
|
|
}
|
|
else if (punctualityStr === "late") {
|
|
latenessMessage = punctuality + " minute" + (punctuality === 1 ? "" : "s") + " late";
|
|
|
|
if (trainStatus === "Running") {
|
|
icon += "Late";
|
|
}
|
|
else {
|
|
icon += "NotRunning";
|
|
}
|
|
}
|
|
else {
|
|
latenessMessage = "On time";
|
|
icon += "NotRunning";
|
|
}
|
|
|
|
popupContent = (
|
|
<div>
|
|
<h3>{objectTitle}</h3>
|
|
<ul>
|
|
<li><b>Train Details:</b> {splitMessage[1].split("(")[0]}</li>
|
|
<li><b>Train Type:</b> {trainType}</li>
|
|
<li><b>Status:</b> {trainStatus}</li>
|
|
<li><b>Direction:</b> {item.trainDirection}</li>
|
|
<li><b>Update:</b> {splitMessage[2]}</li>
|
|
<li><b>Punctuality:</b> {latenessMessage}</li>
|
|
</ul>
|
|
</div>
|
|
);
|
|
|
|
markerText = item.trainPublicMessage + " " + item.trainDirection;
|
|
display =
|
|
((item.latitude !== "0" && item.longitude !== "0") && // filter out trains with no location data
|
|
((showMainline && trainType == "Mainline") || (showSuburban && trainType == "Suburban") || (showDart && trainType == "DART")) &&
|
|
((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 == "Not yet running" && showNotYetRunning) || (trainStatus == "Terminated" && showTerminated)));
|
|
|
|
break;
|
|
|
|
case "IrishRailStation":
|
|
objectTitle = item.trainStationDesc + " Train Station";
|
|
popupContent = (
|
|
<div>
|
|
<h3>{objectTitle}</h3>
|
|
<ul>
|
|
<li><b>Train Station Name:</b> {item.trainStationDesc}</li>
|
|
<li><b>Train Station ID:</b> {item.trainStationID}</li>
|
|
<li><b>Train Station Code:</b> {item.trainStationCode}</li>
|
|
</ul>
|
|
</div>
|
|
);
|
|
|
|
markerText = item.trainStationCode + " " + item.trainStationDesc;
|
|
display = (item.latitude !== "0" && item.longitude !== "0");
|
|
|
|
break;
|
|
|
|
case "Bus":
|
|
objectTitle = item.busRouteAgencyName + ": " + item.busRouteShortName;
|
|
popupContent = (
|
|
<div>
|
|
<h3>{objectTitle}</h3>
|
|
<ul>
|
|
<li><b>Bus ID:</b> {item.busID}</li>
|
|
<li><b>Bus Route ID:</b> {item.busRoute}</li>
|
|
<li><b>Bus Route Short Name:</b> {item.busRouteShortName}</li>
|
|
<li><b>Bus Route Long Name:</b> {item.busRouteLongName}</li>
|
|
<li><b>Agency: </b> {item.busRouteAgencyName}</li>
|
|
</ul>
|
|
</div>
|
|
);
|
|
|
|
markerText = item.busRouteAgencyName + " " + item.busRouteShortName + " " + item.busRouteLongName;
|
|
display = (item.latitude !== "0" && item.longitude !== "0");
|
|
|
|
break;
|
|
|
|
case "BusStop":
|
|
objectTitle = item.busStopName + " Bus Stop";
|
|
popupContent = (
|
|
<div>
|
|
<h3>{objectTitle}</h3>
|
|
<ul>
|
|
<li><b>Bus Stop ID:</b> {item.busStopID}</li>
|
|
<li><b>Bus Stop Name:</b> {item.busStopName}</li>
|
|
<li><b>Bus Stop Code:</b> {item.busStopCode || "N/A"}</li>
|
|
</ul>
|
|
</div>
|
|
);
|
|
|
|
markerText = item.busStopName;
|
|
display = (item.latitude !== "0" && item.longitude !== "0");
|
|
|
|
break;
|
|
|
|
case "LuasStop":
|
|
objectTitle = item.luasStopName + " Luas Stop";
|
|
|
|
let luasLine;
|
|
switch (item.luasStopLineID) {
|
|
case "1":
|
|
luasLine = "Green Line";
|
|
icon += "Green";
|
|
break;
|
|
case "2":
|
|
luasLine = "Red Line";
|
|
icon += "Red";
|
|
break;
|
|
default:
|
|
luasLine = "N/A";
|
|
}
|
|
popupContent = (
|
|
<LuasPopup item={item} objectTitle={objectTitle} luasLine={luasLine} />
|
|
);
|
|
|
|
markerText = item.luasStopIrishName + " " + item.luasStopName + " " + luasLine;
|
|
display = (
|
|
(item.latitude !== "0" && item.longitude !== "0") &&
|
|
(showGreenLine && luasLine === "Green Line" || showRedLine && luasLine === "Red Line") &&
|
|
(showEnabled && item.luasStopIsEnabled === "1" || showDisabled && item.luasStopIsEnabled === "0") &&
|
|
(!showCycleAndRide || (showCycleAndRide && item.luasStopIsCycleAndRide === "1")) &&
|
|
(!showParkAndRide || (showParkAndRide && item.luasStopIsParkAndRide === "1"))
|
|
);
|
|
|
|
break;
|
|
|
|
default:
|
|
popupContent = (
|
|
<div>
|
|
<h3>{item.objectType}</h3>
|
|
</div>
|
|
);
|
|
markerText = `Unknown Object Type: ${item.objectType}`;
|
|
}
|
|
|
|
return {
|
|
coords: [item.latitude, item.longitude],
|
|
popup: popupContent,
|
|
icon: icon,
|
|
markerText: markerText.toLowerCase(),
|
|
display: display
|
|
};
|
|
})
|
|
.filter((marker) => marker.display);
|
|
|
|
setMarkers(newMarkers);
|
|
} catch (error) {
|
|
console.error("Error fetching data:", error);
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
// 2. Memoize the filtered markers so it recalculates only if `searchTerm` or `markers` changes
|
|
const filteredMarkers = useMemo(() => {
|
|
if (!searchTerm.trim()) {
|
|
return markers;
|
|
}
|
|
return markers.filter((marker) =>
|
|
marker.markerText.includes(searchTerm.toLowerCase())
|
|
);
|
|
}, [searchTerm, markers]);
|
|
|
|
return (
|
|
<div style={{ height: "100vh", width: "100vw", display: "flex", position: "relative" }}>
|
|
{loading && <LoadingOverlay message={"Loading data..."}/>}
|
|
|
|
{/* SEARCH BOX */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: "1vh",
|
|
height: "5vh",
|
|
width: "250px", minWidth: "50px",
|
|
left: "50%",
|
|
transform: "translateX(-50%)",
|
|
zIndex: 1000
|
|
}}
|
|
>
|
|
<input
|
|
type="text"
|
|
value={searchInput}
|
|
onChange={(e) => setSearchInput(e.target.value)}
|
|
placeholder="Search..."
|
|
style={{
|
|
width: "250px", fontSize: "16px",
|
|
padding: "10px", background: "rgba(255, 255, 255, 0.9)", color: "black",
|
|
borderRadius: "10px", overflow: "hidden"
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<Sidebar
|
|
selectedSources={selectedSources}
|
|
setSelectedSources={setSelectedSources}
|
|
clusteringEnabled={clusteringEnabled}
|
|
setClusteringEnabled={setClusteringEnabled}
|
|
fetchData={fetchData}
|
|
/>
|
|
<div style={{ flex: 1 }}>
|
|
<MapComponent markers={filteredMarkers} clusteringEnabled={clusteringEnabled} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App; |