diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d271a25..5396588 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -8,11 +8,11 @@ const TRANSIENT_DATA_API = "https://281bc6mcm5.execute-api.us-east-1.amazonaws.c const PERMANENT_DATA_API = "https://a6y312dpuj.execute-api.us-east-1.amazonaws.com/permanent_data"; const dataSources = [ - { id: "IrishRailTrains", name: "Irish Rail Trains", url: `${TRANSIENT_DATA_API}?objectType=IrishRailTrain` }, - { id: "IrishRailStations", name: "Irish Rail Stations", url: `${PERMANENT_DATA_API}?objectType=IrishRailStation` }, - { id: "LuasStops", name: "Luas Stops", url: `${PERMANENT_DATA_API}?objectType=LuasStop` }, - { id: "BusStops", name: "Bus Stops", url: `${PERMANENT_DATA_API}?objectType=BusStop` }, - { id: "Buses", name: "Buses", url: `${TRANSIENT_DATA_API}?objectType=Bus` }, + { 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() { @@ -36,19 +36,37 @@ function App() { }; }, [searchInput]); - const fetchData = async () => { + const fetchData = async (enabledSources) => { setLoading(true); try { const newMarkers = (await Promise.all( dataSources - .filter(({ id }) => selectedSources.includes(id)) + .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) { @@ -77,21 +95,57 @@ function App() { case "R": trainStatus = "Running"; break; + + case "T": + trainStatus = "Terminated"; + break; + + case "N": + trainStatus = "Not yet running"; + break; + default: - trainStatus = "Not running"; + trainStatus = "Unknown"; } const splitMessage = item.trainPublicMessage.split("\\n"); - const match = splitMessage[1].match(/\((.*?)\)/); - const punctuality = match ? match[1] : "N/A"; + 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 (trainStatus === "Not running") { - icon += "NotRunning"; - } else if (punctuality.charAt(0) === "-" || punctuality.charAt(0) === "0") { + if (punctualityStr === "early") { + latenessMessage = -punctuality + " minute" + (punctuality === 1 ? "" : "s") + " early"; icon += "OnTime"; - } else { - icon += "Late"; + } + 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 = ( @@ -103,12 +157,19 @@ function App() {
  • Status: {trainStatus}
  • Direction: {item.trainDirection}
  • Update: {splitMessage[2]}
  • -
  • Punctuality: {punctuality}
  • +
  • Punctuality: {latenessMessage}
  • ); 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": @@ -123,7 +184,10 @@ function App() { ); + markerText = item.trainStationCode + " " + item.trainStationDesc; + display = (item.latitude !== "0" && item.longitude !== "0"); + break; case "Bus": @@ -140,7 +204,10 @@ function App() { ); + markerText = item.busRouteAgencyName + " " + item.busRouteShortName + " " + item.busRouteLongName; + display = (item.latitude !== "0" && item.longitude !== "0"); + break; case "BusStop": @@ -155,7 +222,10 @@ function App() { ); + markerText = item.busStopName; + display = (item.latitude !== "0" && item.longitude !== "0"); + break; case "LuasStop": @@ -165,9 +235,11 @@ function App() { switch (item.luasStopLineID) { case "1": luasLine = "Green Line"; + icon += "Green"; break; case "2": luasLine = "Red Line"; + icon += "Red"; break; default: luasLine = "N/A"; @@ -175,7 +247,16 @@ function App() { popupContent = ( ); - markerText = item.luasStopIrishName + " " + item.luasStopName; + + 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: @@ -192,8 +273,10 @@ function App() { popup: popupContent, icon: icon, markerText: markerText.toLowerCase(), + display: display }; - }); + }) + .filter((marker) => marker.display); setMarkers(newMarkers); } catch (error) { diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index ceb7b05..ad14e9b 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -2,27 +2,153 @@ import React, { useState, useEffect } from "react"; import PropTypes from 'prop-types'; import Cookies from "js-cookie"; +const menuData = [ + { + id: "irish-rail", + name: "Irish Rail", + children: [ + { + id: "irish-rail-trains", + name: "Irish Rail Trains", + children: [ + { id: "mainline", name: "Mainline" }, + { id: "suburban", name: "Suburban" }, + { id: "dart", name: "DART" }, + { id: "running", name: "Running" }, + { id: "not-yet-running", name: "Not yet running" }, + { id: "terminated", name: "Terminated" }, + { id: "early", name: "Early" }, + { id: "on-time", name: "On-time" }, + { id: "late", name: "Late" }, + ], + }, + { id: "irish-rail-stations", name: "Irish Rail Stations" }, + ], + }, + { + id: "bus", + name: "Bus", + children: [ + { id: "buses", name: "Buses" }, + { id: "bus-stops", name: "Bus Stops" }, + ], + }, + { + id: "luas-stops", + name: "Luas Stops", + children: [ + { id: "red-line", name: "Red Line" }, + { id: "green-line", name: "Green Line" }, + { id: "enabled", name: "Enabled" }, + { id: "disabled", name: "Disabled" }, + { id: "park-and-ride", name: "Must be Park & Ride" }, + { id: "cycle-and-ride", name: "Must be Cycle & Ride" }, + ], + }, +]; + +const getAllDefaultCheckedIds = (data) => { + const ids = []; + const traverse = (items, isTopLevel = true) => { + items.forEach((item) => { + if (!isTopLevel && item.id !== "cycle-and-ride" && item.id !== "park-and-ride") { + ids.push(item.id); // Check non-top-level items by default + } + if (item.children) { + traverse(item.children, false); // Child items are not top-level + } + }); + }; + traverse(data); + return ids; +}; + +const CheckboxItem = ({ item, selectedSources, setSelectedSources, enabledSources, setEnabledSources, level = 0, parentChecked = true }) => { + const isChecked = selectedSources.includes(item.id); + const isDisabled = !parentChecked; // Disable if any parent is not checked + const isEnabled = isChecked && parentChecked; // Only enabled if checked and parent is checked + + const handleCheckboxChange = () => { + setSelectedSources((prev) => + isChecked + ? prev.filter((id) => id !== item.id) + : [...prev, item.id] + ); + }; + + // Track enabled sources based on parent and own state + useEffect(() => { + setEnabledSources((prev) => { + const newEnabledSources = new Set(prev); + if (isEnabled) newEnabledSources.add(item.id); + else newEnabledSources.delete(item.id); + return Array.from(newEnabledSources); + }); + }, [isEnabled, item.id, setEnabledSources]); + + const hasChildren = item.children && item.children.length > 0; + const isTopLevel = level === 0; + + return ( +
    +
    + + +
    + {hasChildren && ( +
    + {item.children.map((child) => ( + + ))} +
    + )} +
    + ); +}; + const Sidebar = ({ selectedSources, setSelectedSources, clusteringEnabled, setClusteringEnabled, fetchData }) => { const [isOpen, setIsOpen] = useState(true); - const dataSources = [ - { id: "IrishRailTrains", name: "Irish Rail Trains" }, - { id: "IrishRailStations", name: "Irish Rail Stations" }, - { id: "LuasStops", name: "Luas Stops" }, - { id: "BusStops", name: "Bus Stops" }, - { id: "Buses", name: "Buses" }, - ]; + const [enabledSources, setEnabledSources] = useState([]); // New state to track enabled sources - // Load selected sources from cookies on component mount + // Load selected sources from cookies or set all as default checked useEffect(() => { const savedSources = Cookies.get("selectedSources"); if (savedSources) { setSelectedSources(JSON.parse(savedSources)); + } else { + const allDefaultChecked = getAllDefaultCheckedIds(menuData); + setSelectedSources(allDefaultChecked); } }, [setSelectedSources]); const handleSubmit = () => { Cookies.set("selectedSources", JSON.stringify(selectedSources), { expires: 7 }); - fetchData(); + fetchData(enabledSources); // Use enabledSources for data fetching }; return ( @@ -34,39 +160,22 @@ const Sidebar = ({ selectedSources, setSelectedSources, clusteringEnabled, setCl height: isOpen ? "auto" : "40px", display: "flex", flexDirection: "column", alignItems: "center", zIndex: 1000, overflow: "hidden", justifyContent: "center" }}> - {isOpen && (

    Select Data Sources

    - {dataSources.map(({ id, name }) => ( -
    - - setSelectedSources((prev) => prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id]) - } - /> - -
    - ))} -
    - setClusteringEnabled(!clusteringEnabled)} + {menuData.map((item) => ( + - -
    + ))}
    )} @@ -82,4 +191,4 @@ Sidebar.propTypes = { fetchData: PropTypes.func.isRequired, }; -export default Sidebar; \ No newline at end of file +export default Sidebar;