diff --git a/src/api/network.ts b/src/api/network.ts index 1cc2774..eab2716 100644 --- a/src/api/network.ts +++ b/src/api/network.ts @@ -50,6 +50,48 @@ export async function getSimulationOutput( return await response.json(); } +export function getSimulationAnalytics(simulationAnalytics: SimulationOutput) { + const tripInfo = simulationAnalytics.tripInfo; + const netState = simulationAnalytics.netstate; + // Run time: Measured in simulation seconds + + // Average duration: The average time each vehicle needed to accomplish the route in simulation seconds + // Extract all durations into an array + const durations = tripInfo.map(trip => trip.duration); + + // Calculate the average duration + const totalDuration = durations.reduce((acc, duration) => acc + duration, 0); + const averageDuration = totalDuration / tripInfo.length; + + // Waiting time: The average time in which vehicles speed was below or equal 0.1 m/s in simulation seconds + // Extract all durations into an array + const waitingTime = tripInfo.map(trip => trip.waitingTime); + + // Calculate the average duration + const totalWaiting = waitingTime.reduce((acc, time) => acc + time, 0); + const averageWaiting = totalWaiting / tripInfo.length; + + // Time loss: The time lost due to driving below the ideal speed. (ideal speed includes the individual speedFactor; slowdowns due to intersections etc. will incur timeLoss, scheduled stops do not count) in simulation seconds + // Extract all durations into an array + const timeLoss = tripInfo.map(trip => trip.timeLoss); + + // Calculate the average duration + const totalTimeLoss = timeLoss.reduce((acc, time) => acc + time, 0); + const averageTimeLoss = totalTimeLoss / tripInfo.length; + + // Total number of cars that reached their destination. Can work this out with vaporised variable + const noFinish = tripInfo.filter(trip => trip.vaporized === true).length; + const totalNumberOfCarsThatCompleted = tripInfo.length - noFinish; + + return { + averageDuration, + averageWaiting, + averageTimeLoss, + totalNumberOfCarsThatCompleted, + simulationLength: netState.length, + }; +} + export async function getSimulationInfo( simulationId: string, ): Promise { diff --git a/src/components/Canvas/BidirectionalRoad.tsx b/src/components/Canvas/BidirectionalRoad.tsx index 2f9f171..58215aa 100644 --- a/src/components/Canvas/BidirectionalRoad.tsx +++ b/src/components/Canvas/BidirectionalRoad.tsx @@ -37,4 +37,4 @@ export function BidirectionalRoad({ /> ); -} +} \ No newline at end of file diff --git a/src/components/FloatingPlayPause.tsx b/src/components/FloatingPlayPause.tsx index 55b95ae..268c17e 100644 --- a/src/components/FloatingPlayPause.tsx +++ b/src/components/FloatingPlayPause.tsx @@ -3,7 +3,7 @@ import { CircleLoader } from 'react-spinners'; import { PlayIcon, StopIcon } from '@heroicons/react/24/outline'; -import { getSimulationOutput, uploadNetwork } from '~/api/network'; +import { getSimulationAnalytics, getSimulationOutput, uploadNetwork } from '~/api/network'; import { extractCarsFromSumoMessage } from '~/helpers/sumo'; import { useSimulation } from '~/hooks/useSimulation'; import { @@ -12,9 +12,11 @@ import { BASE_SIMULATION_ERROR_TOPIC, SIMULATION_SOCKET_URL, } from '~/simulation-urls'; +import { SimulationInfo } from '~/types/Simulation'; import { useCarsStore } from '~/zustand/useCarStore'; import { useNetworkStore } from '~/zustand/useNetworkStore'; import { usePlaying } from '~/zustand/usePlaying'; +import { useSimulationHistory } from '~/zustand/useSimulationHistory'; export const FloatingPlayPause = () => { const [loading, setLoading] = useState(false); @@ -25,6 +27,13 @@ export const FloatingPlayPause = () => { brokerURL: SIMULATION_SOCKET_URL, }); + const simulationHistory = useSimulationHistory(); + + const [startTime, setStartTime] = useState(null); + const [simulationInfo, setSimulationInfo] = useState( + null, + ); + // streaming of simulation data useEffect(() => { const SIMULATION_DATA_TOPIC = `${BASE_SIMULATION_DATA_TOPIC}/${player.simulationId}`; @@ -82,6 +91,8 @@ export const FloatingPlayPause = () => { }; const simInfo = await uploadNetwork(requestBody); + setStartTime(new Date().toISOString()); + setSimulationInfo(simInfo); player.changeSimulationId(simInfo.id); player.play(); } catch (error: unknown) { @@ -101,8 +112,21 @@ export const FloatingPlayPause = () => { } const simOutput = await getSimulationOutput(player.simulationId); + const simAnalytics = await getSimulationAnalytics(simOutput); + + if (startTime && simulationInfo) { + simulationHistory.updateHistory({ + startTime, + endTime: new Date().toISOString(), + simulation: { + info: simulationInfo, + output: simOutput, + }, + }); + } console.log({ simOutput }); + console.log(simAnalytics) } catch (error: unknown) { console.error(error); } finally { diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 5636ddf..c78b6e1 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,13 +1,16 @@ import { useState } from 'react'; import { Dialog } from '@headlessui/react'; -import { Bars3Icon, XMarkIcon } from '@heroicons/react/20/solid'; +import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/solid'; import { useNetworkStore } from '~/zustand/useNetworkStore'; +import { useSimulationHistory } from '~/zustand/useSimulationHistory'; import Logo from '../assets/UrbanFloLogoB&W.svg'; import { ProjectDownloadButton } from './ProjectDownloadButton'; import { ProjectUploadButton } from './ProjectUploadButton'; +import { SimulationHistoryButton } from './SimulationHistory/SimulationHistoryButton'; +import { SimulationSummary } from './SimulationHistory/SimulationSummary'; export function classNames(...classes: string[]) { return classes.filter(Boolean).join(' '); @@ -17,6 +20,8 @@ export function Header() { const network = useNetworkStore(); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const simulationHistoryStore = useSimulationHistory(); + return (
-
- - Your Company - - - -
-
-
- - +
+
+ + Urban Flo + + + +
+
+ {simulationHistoryStore.history.map((item, index) => ( + + ))}
diff --git a/src/components/ProjectDownloadButton.tsx b/src/components/ProjectDownloadButton.tsx index 7332031..00d98e7 100644 --- a/src/components/ProjectDownloadButton.tsx +++ b/src/components/ProjectDownloadButton.tsx @@ -14,7 +14,7 @@ export function ProjectDownloadButton() { return ( + ); +} diff --git a/src/components/SimulationHistory/SimulationSummary.tsx b/src/components/SimulationHistory/SimulationSummary.tsx new file mode 100644 index 0000000..a56d2a0 --- /dev/null +++ b/src/components/SimulationHistory/SimulationSummary.tsx @@ -0,0 +1,99 @@ +import { useState } from 'react'; + +import { getSimulationAnalytics } from '~/api/network'; +import { formatISOString } from '~/helpers/date/formatter'; +import { SimulationHistory } from '~/zustand/useSimulationHistory'; + +interface SimulationSummaryProps { + histroyItem: SimulationHistory; + simulationNumber: number; +} + +export function SimulationSummary({ + histroyItem, + simulationNumber, +}: SimulationSummaryProps) { + const [isExpanded, setIsExpanded] = useState(false); + const summary = getSimulationAnalytics(histroyItem.simulation.output); + + const startDateTime = formatISOString(histroyItem.startTime); + const endDateTime = formatISOString(histroyItem.endTime); + + return ( +
+
+

+ Simulation Summary #{simulationNumber} +

+
+
+

Start Time:

+

{startDateTime.date}

+

{startDateTime.time}

+
+
+

End Time:

+

{endDateTime.date}

+

{endDateTime.time}

+
+
+
+ +
+
+ + {isExpanded && ( +
+

+ Detailed Simulation Metrics +

+
+
+ + Simulation Length: + {' '} + {summary.simulationLength} +
+
+ + Average Time Spent Per Car: + {' '} + + {summary.averageDuration} seconds + +
+
+ + Average Waiting Time Per Car: + {' '} + + {summary.averageWaiting} seconds + +
+
+ + Average Time Lost Due to Congestion: + {' '} + + {summary.averageTimeLoss} seconds + +
+
+ + Total Cars Completed Simulation: + {' '} + + {summary.totalNumberOfCarsThatCompleted} + +
+
+
+ )} +
+ ); +} diff --git a/src/helpers/date/formatter.ts b/src/helpers/date/formatter.ts new file mode 100644 index 0000000..b45c76f --- /dev/null +++ b/src/helpers/date/formatter.ts @@ -0,0 +1,16 @@ +export function formatISOString(dateString: string) { + const date = new Date(dateString); + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are zero-based + const year = String(date.getFullYear()).slice(-2); + const time = date.toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + hour12: true, + }); + + return { + date: `${day}/${month}/${year}`, + time, + }; +} diff --git a/src/zustand/useSimulationHistory.ts b/src/zustand/useSimulationHistory.ts new file mode 100644 index 0000000..2b31a48 --- /dev/null +++ b/src/zustand/useSimulationHistory.ts @@ -0,0 +1,33 @@ +import { create } from 'zustand'; + +import { SimulationInfo, SimulationOutput } from '~/types/Simulation'; + +export interface Simulation { + info: SimulationInfo; + output: SimulationOutput; +} + +export interface SimulationHistory { + startTime: string; + endTime: string; + simulation: Simulation; +} + +type SimulationHistoryState = { + history: SimulationHistory[]; + showHistory: boolean; + closeHistory: () => void; + openHistory: () => void; + updateHistory: (s: SimulationHistory) => void; +}; + +export const useSimulationHistory = create(set => ({ + history: [], + showHistory: false, + closeHistory: () => set({ showHistory: false }), + openHistory: () => set({ showHistory: true }), + updateHistory: (s: SimulationHistory) => + set(state => ({ + history: [...state.history, s], + })), +}));