Skip to content

Commit

Permalink
Merge pull request #59 from igloo-4002/feat/sim-history
Browse files Browse the repository at this point in the history
  • Loading branch information
vishaljak committed Sep 18, 2023
2 parents 65f0588 + d42df99 commit d7d489b
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 51 deletions.
42 changes: 42 additions & 0 deletions src/api/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SimulationInfo> {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Canvas/BidirectionalRoad.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ export function BidirectionalRoad({
/>
</>
);
}
}
26 changes: 25 additions & 1 deletion src/components/FloatingPlayPause.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
Expand All @@ -25,6 +27,13 @@ export const FloatingPlayPause = () => {
brokerURL: SIMULATION_SOCKET_URL,
});

const simulationHistory = useSimulationHistory();

const [startTime, setStartTime] = useState<string | null>(null);
const [simulationInfo, setSimulationInfo] = useState<SimulationInfo | null>(
null,
);

// streaming of simulation data
useEffect(() => {
const SIMULATION_DATA_TOPIC = `${BASE_SIMULATION_DATA_TOPIC}/${player.simulationId}`;
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down
80 changes: 32 additions & 48 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -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(' ');
Expand All @@ -17,6 +20,8 @@ export function Header() {
const network = useNetworkStore();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);

const simulationHistoryStore = useSimulationHistory();

return (
<header className="bg-white drop-shadow-lg">
<nav
Expand Down Expand Up @@ -51,60 +56,39 @@ export function Header() {
<div className="hidden lg:flex lg:flex-1 lg:justify-end space-x-4">
<ProjectDownloadButton />
<ProjectUploadButton />
<SimulationHistoryButton />
</div>
</nav>
<Dialog
as="div"
className="lg:hidden"
open={mobileMenuOpen}
onClose={setMobileMenuOpen}
open={simulationHistoryStore.showHistory}
onClose={simulationHistoryStore.closeHistory}
>
<div className="fixed inset-0 z-10" />
<Dialog.Panel className="fixed inset-y-0 right-0 z-10 w-full overflow-y-auto bg-white px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10">
<div className="flex items-center justify-between">
<a href="#" className="-m-1.5 p-1.5">
<span className="sr-only">Your Company</span>
<img className="h-8 w-auto" src={Logo} alt="" />
</a>
<button
type="button"
className="-m-2.5 rounded-md p-2.5 text-gray-700"
onClick={() => setMobileMenuOpen(false)}
>
<span className="sr-only">Close menu</span>
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
<div className="mt-6 flow-root">
<div className="-my-6 divide-y divide-gray-500/10">
<div className="space-y-2 py-6">
<a
href="#"
className="-mx-3 block rounded-lg px-3 py-2 text-base font-semibold leading-7 text-gray-900 hover:bg-gray-50"
>
Features
</a>
<a
href="#"
className="-mx-3 block rounded-lg px-3 py-2 text-base font-semibold leading-7 text-gray-900 hover:bg-gray-50"
>
Marketplace
</a>
<a
href="#"
className="-mx-3 block rounded-lg px-3 py-2 text-base font-semibold leading-7 text-gray-900 hover:bg-gray-50"
>
Company
</a>
</div>
<div className="py-6">
<a
href="#"
className="-mx-3 block rounded-lg px-3 py-2.5 text-base font-semibold leading-7 text-gray-900 hover:bg-gray-50"
>
Log in
</a>
</div>
<div className="flex flex-col gap-5">
<div className="flex items-center justify-between">
<a href="#" className="-m-1.5 p-1.5">
<span className="sr-only">Urban Flo</span>
<img className="h-8 w-auto" src={Logo} alt="" />
</a>
<button
type="button"
className="-m-2.5 rounded-md p-2.5 text-gray-700"
onClick={simulationHistoryStore.closeHistory}
>
<span className="sr-only">Close menu</span>
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
<div className="flex flex-col justify-between gap-2">
{simulationHistoryStore.history.map((item, index) => (
<SimulationSummary
key={index}
histroyItem={item}
simulationNumber={index + 1}
/>
))}
</div>
</div>
</Dialog.Panel>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ProjectDownloadButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function ProjectDownloadButton() {

return (
<button
className="text-sm font-semibold text-white bg-amber-400 leading-6 items-center rounded-full flex py-2 px-4 mt-4"
className="text-sm font-semibold bg-amber-400 leading-6 items-center rounded-full flex py-2 px-4 mt-4"
onClick={handleDownloadClick}
>
Download
Expand Down
21 changes: 21 additions & 0 deletions src/components/SimulationHistory/SimulationHistoryButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useSimulationHistory } from '~/zustand/useSimulationHistory';

export function SimulationHistoryButton() {
const simulationHistoryStore = useSimulationHistory();

function handleClick() {
simulationHistoryStore.openHistory();
}

const disabled = simulationHistoryStore.history.length === 0;

return (
<button
disabled={disabled}
onClick={handleClick}
className="text-sm font-semibold bg-amber-400 leading-6 items-center rounded-full flex py-2 px-4 mt-4 disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-700"
>
Simulation History
</button>
);
}
99 changes: 99 additions & 0 deletions src/components/SimulationHistory/SimulationSummary.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="bg-white p-4 rounded-lg shadow-md w-full">
<div className="flex flex-col justify-between gap-2">
<h2 className="text-xl font-semibold">
Simulation Summary #{simulationNumber}
</h2>
<div className="grid grid-cols-2 gap-4">
<div>
<h3 className="text-lg font-medium">Start Time:</h3>
<p>{startDateTime.date}</p>
<p>{startDateTime.time}</p>
</div>
<div>
<h3 className="text-lg font-medium">End Time:</h3>
<p>{endDateTime.date}</p>
<p>{endDateTime.time}</p>
</div>
</div>
<div>
<button
onClick={() => setIsExpanded(!isExpanded)}
className="text-blue-500"
>
{isExpanded ? 'Hide Details' : 'Show Details'}
</button>
</div>
</div>

{isExpanded && (
<div className="flex flex-col gap-2 mt-6 bg-gray-100 p-3 rounded-lg">
<h3 className="text-lg font-semibold text-gray-700">
Detailed Simulation Metrics
</h3>
<div className="flex flex-col gap-2">
<div>
<span className="font-medium text-gray-600">
Simulation Length:
</span>{' '}
<span className="text-gray-800">{summary.simulationLength}</span>
</div>
<div>
<span className="font-medium text-gray-600">
Average Time Spent Per Car:
</span>{' '}
<span className="text-gray-800">
{summary.averageDuration} seconds
</span>
</div>
<div>
<span className="font-medium text-gray-600">
Average Waiting Time Per Car:
</span>{' '}
<span className="text-gray-800">
{summary.averageWaiting} seconds
</span>
</div>
<div>
<span className="font-medium text-gray-600">
Average Time Lost Due to Congestion:
</span>{' '}
<span className="text-gray-800">
{summary.averageTimeLoss} seconds
</span>
</div>
<div>
<span className="font-medium text-gray-600">
Total Cars Completed Simulation:
</span>{' '}
<span className="text-gray-800">
{summary.totalNumberOfCarsThatCompleted}
</span>
</div>
</div>
</div>
)}
</div>
);
}
16 changes: 16 additions & 0 deletions src/helpers/date/formatter.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
Loading

0 comments on commit d7d489b

Please sign in to comment.