Step 4: Add React components
Intro
A React application is composed of components, each representing some piece of functionality.
React components can contain other components. In fact, the whole application is also
represented by a high-level component, which in our case is defined in the App.js
file (this file
will be updated in the next step).
Now let’s create components that will display more detailed data about the simulation including animation.
The source code for all components created in this step
is stored in *.js
files inside the components
folder.
Sketching the UI
Before we move on, let’s think about how our web application should present its data to the user, what parts there will be in its UI.
If we take the simulation mode in the desktop application as the role model, here is the sketch of our future web UI:
So, here we have some kind of a control panel on the left, and animation and real-time simulation information on the right.
Control panel (left side)
There are three parts that the control panel can be divided into: scenario list, simulation controls, and summary statistics. We will now create a React component for each part.
ScenarioList
component
Create a components
folder in the src
folder.
This is the folder where all custom React component files will be placed.
Inside this new folder, create a new file ScenarioList.js
:
import React from "react";
import Form from "react-bootstrap/Form";
import Dropdown from 'react-bootstrap/Dropdown';
import DropdownButton from 'react-bootstrap/DropdownButton';
import Badge from 'react-bootstrap/Badge';
function ScenarioList({currentScenarioName, currentScenarioType, embeddedScenarioNames, onEmbeddedScenarioSelected, onCustomScenarioFileSelected}) {
return (
<>
To set up a simulation scenario:<br></br>
<DropdownButton id="scenario-list" title="Select an embedded scenario" className="ms-2">
{embeddedScenarioNames &&
embeddedScenarioNames.map((scenarioName) => (
<Dropdown.Item key={scenarioName} onClick={(e) => onEmbeddedScenarioSelected(scenarioName)}>{scenarioName}</Dropdown.Item>
))}
</DropdownButton>
<Form.Group controlId="formFile" className="mb-2 mt-2 ms-2">
<Form.Label>or upload a <i>custom</i> scenario:</Form.Label>
<Form.Control type="file"
onChange={(event) => {
if (event.target.files) {
let file = event.target.files[0];
onCustomScenarioFileSelected(file);
}
}
}/>
</Form.Group>
<b className="mt-3">Scenario:</b>
<br></br>
<label className="mt-1 ms-1 me-2">{currentScenarioName ? currentScenarioName : "not selected"}</label>
<Badge bg="secondary">{currentScenarioType || ""}</Badge>
</>
);
}
export default ScenarioList;
ScenarioList
displays a list of embedded scenario names and fires a message when
the user selects a scenario.
SimulationControls
component
Create a new file SimulationControls.js
:
import React from "react";
import Button from 'react-bootstrap/Button';
import ButtonGroup from 'react-bootstrap/ButtonGroup';
import ProgressBar from 'react-bootstrap/ProgressBar';
import { formatDateTime } from "../utils";
const formatTimeScale = (val) => {
if (val >= 1) {
return val + " x";
} else {
return "1/" + 1 / val + " x";
}
}
function SimulationControls({modelDateTime, simulationProgressPct, timeScale, simulationState, blocking,
onStart, onStop, onResume, onReset, onSlower, onFaster}) {
const stateMapping = {"no_scenario": "Please set a scenario",
"ready_to_run" : "Ready to run",
"running" : "Running",
"finished": "Finished"};
return (
<div>
<h5>Simulation</h5>
<ProgressBar animated={simulationProgressPct < 100} now={simulationProgressPct} label={`${simulationProgressPct}%`}></ProgressBar>
<div className="mb-2 mt-2">
<label style={{width: "6em"}}>Model time:</label>
{modelDateTime ? formatDateTime(modelDateTime) : " <press Start>"}
</div>
<div className="mb-2 mt-2">
<label style={{width: "6em"}}>Status:</label>
{stateMapping[simulationState]}
</div>
<div className="mb-2 mt-2">
<label style={{width: "6em"}}>Speed:</label>
<label style={{width: "4em", textAlign: "left"}}>{formatTimeScale(timeScale)}</label>
<ButtonGroup>
<Button variant="outline-primary" size="sm" onClick={(e) => onSlower()}>
Slower
</Button>
<Button variant="outline-primary" size="sm" onClick={(e) => onFaster()}>
Faster
</Button>
</ButtonGroup>
</div>
<button
className="large-button deep-blue"
disabled={simulationProgressPct > 0 || simulationState !== "ready_to_run" || blocking}
onClick={(e) => onStart()}
>
START
</button>
<button
className="large-button deep-blue"
disabled={simulationProgressPct === 0 || simulationState !== "ready_to_run" || blocking}
onClick={(e) => onResume()}
>
RESUME
</button>
<button
className="large-button deep-blue"
disabled={simulationState !== "running" || blocking}
onClick={(e) => onStop()}
>
STOP
</button>
<button
className="large-button yellow"
disabled={simulationState === "no_scenario" || blocking}
onClick={(e) => onReset()}
>
RESET
</button>
<br></br>
</div>
);
}
export default SimulationControls;
SimulationControls
shows current model time and the buttons to control the simulation (start, stop, etc.).
SummaryStatistics
component
Create a new file SummaryStatistics.js
:
import React from "react";
function SummaryStatistics(props) {
return (
<>
<h5 className="mt-1">Summary statistics</h5>
<table>
<thead>
<tr>
<th>Indicator name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Service level</td>
<td>{props.stats.serviceLevelPct ? props.stats.serviceLevelPct : 0} %</td>
</tr>
<tr>
<td>Expenses</td>
<td>
$ {props.numberWithSpaces((props.stats.expenses||0).toFixed(2))}
</td>
</tr>
</tbody>
</table>
</>
);
}
export default SummaryStatistics;
SummaryStatistics
displays the overall simulation statistics,
as in the Summary statistics part in the desktop application.
These three new components will work together like this:
Main panel (on the right)
The right part contains animation and three tab-controlled tables/charts.
Animation
component
Create a new Animation.js
file:
import React, { Suspense, Fragment } from "react";
import { Canvas } from "@react-three/fiber";
import { OrbitControls, Plane, Line, Html } from "@react-three/drei";
const Truck = ({truck}) => {
return (
<group position={[truck.x, -0.5, truck.y]} rotation-y={truck.heading + Math.PI}>
<mesh key={"truckhead-" + truck.id} position={[0, 0, 0]}>
<boxGeometry args={[0.8, 0.7, 0.4]} />
<meshStandardMaterial color={"green"} />
</mesh>
<mesh key={"truckbody-" + truck.id} position={[0, 0, 1.7]}>
<boxGeometry args={[1, 1, 3]} />
<meshStandardMaterial color={truck.withCargo ? "royalblue" : "gray"} />
</mesh>
<Html center position={[2, 0, 0]}>
<div style={{ background: truck.withCargo ? "rgba(180,200,250,0.5)" : "rgba(220,220,220,0.5)" , whiteSpace: "nowrap"}}>{truck.name}</div>
</Html>
</group>
)
}
const Store = ({store, nodes}) => {
return (
<group key={"store-" + store.name} position={[nodes.get(store.node).x, -0.75, nodes.get(store.node).y]}>
<mesh scale={[2, 1, 2]}>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color={"red"} />
</mesh>
<Html center position={[0, 0, 2.7]}>
<div style={{ background: "rgba(240,200,200,0.5)", whiteSpace: "nowrap"}}>{store.name}</div>
</Html>
</group>
)
}
const Warehouse = ({warehouse, nodes}) => {
return (
<group key={"warehouse-" + warehouse.name} position={[nodes.get(warehouse.node).x, -0.75, nodes.get(warehouse.node).y]}>
<mesh scale={[2, 1, 2]}>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color={"orange"} />
</mesh>
<Html center position={[0, 0, 2.7]}>
<div style={{ background: "rgba(240,240,190,0.5)", whiteSpace: "nowrap"}}>{warehouse.name}</div>
</Html>
</group>
)
}
const MainScene = ({arcs, nodes, stores, warehouses, trucks}) => {
return (
<>
{arcs.map((arc) => (
<Line
key={"arc-" + arc.source + "-" + arc.dest}
points={[
[nodes.get(arc.source).x, -0.6, nodes.get(arc.source).y],
...arc.points.map((point) => [point.x, -0.6, point.y]),
[nodes.get(arc.dest).x, -0.6, nodes.get(arc.dest).y],
]} />
))}
{stores.map((store) => (
<Store store={store} nodes={nodes}/>
))}
{warehouses.map((warehouse) => (
<Warehouse warehouse={warehouse} nodes={nodes} />
))}
{trucks.map((truck) => (
<Truck truck={truck} />
))}
<Plane
scale={[2000, 2000, 0]}
position={[0, -0.75, 0]}
rotation-x={Math.PI * -0.5}
/>
{/* scene setup */}
<ambientLight intensity={0.1} />
<directionalLight intensity={1} color="white" />
<OrbitControls makeDefault />
</>
)
}
function Animation({arcs, nodes, stores, warehouses, trucks}) {
return (
<Suspense fallback={null}>
<Canvas camera={{
position: [0, 400, 0],
fov: 45,
far: 2000,
}}>
<MainScene arcs={arcs} nodes={nodes} stores={stores} warehouses={warehouses} trucks={trucks} />
</Canvas>
</Suspense>
);
}
export default Animation;
The Animation
component shows road graph, assets, and trucks in real time.
TruckStatistics
component
Create a new TruckStatistics.js
file:
import React from "react";
import { numberWithSpaces } from "../utils";
function TruckStatistics({trucks}) {
return (
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Expenses</th>
<th>Distance traveled, km</th>
</tr>
</thead>
{trucks && (
<tbody>
{trucks.map((truck) => (
<tr key={truck.id}>
<td>{truck.id}</td>
<td>{truck.name}</td>
<td>$ {numberWithSpaces(truck.expenses.toFixed(2))}</td>
<td>{truck.distanceTraveled.toFixed(2)}</td>
</tr>
))}
</tbody>
)}
</table>
);
}
export default TruckStatistics;
TruckStatistics
shows a table with one line per each truck that looks similar to the
'Trucks' table in the desktop application.
GanttChart
component
Create a new GanttChart.js
file:
import React from "react";
import { Chart } from "react-google-charts";
function GanttChart({gantts}) {
const columns = [
{ type: "string", id: "Truck" },
{ type: "string", id: "Name" },
{ type: "datetime", id: "Start" },
{ type: "datetime", id: "End" },
];
const data = [columns, ...gantts];
const options = {
enableInteractivity: false,
tooltip: { trigger: "none" },
width: "100%",
height: 400,
groupByRowLabel: false,
timeline: {
colorByRowLabel: true,
singleColor: "0065ad",
},
};
return (
<>
{gantts.length === 0
? "Will be displayed when some transportation task is finished."
: (<Chart chartType="Timeline" data={data} options={options} />)}
</>
);
}
export default GanttChart;
GanttChart
displays a chart for each completed task (like the
'Gantt Chart' part in the desktop application).
TaskStatistics
component
Create a new TaskStatistics.js
file:
import React from "react";
import { formatDateTime } from "../utils";
function TaskStatistics({tasks}) {
return (
<table>
<thead>
<tr>
<th>Id</th>
<th>Truck</th>
<th>Source</th>
<th>Destination</th>
<th>Created</th>
<th>Started</th>
<th>Deadline</th>
<th>Completed</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{tasks &&
tasks.map((task) => (
<tr key={task.id}>
<td>{task.id}</td>
<td>{task.truck}</td>
<td>{task.source}</td>
<td>{task.destination}</td>
<td>{formatDateTime(task.created)}</td>
<td>{formatDateTime(task.started)}</td>
<td>{formatDateTime(task.deadline)}</td>
<td>{formatDateTime(task.completed)}</td>
<td>{task.status.replaceAll('_', ' ')}</td>
</tr>
))}
</tbody>
</table>
);
}
export default TaskStatistics;
TaskStatistics
lists the tasks, both completed and not.
Its contents resembles that of the
'Tasks' table in the desktop application.
MainPanel
component
The MainPanel
component will group the four previous components,
also adding a split between the animation and tables/chart and grouping
the tasks, trucks, and gantt chart into a tab container.
Create a new MainPanel.js
file:
import React from "react";
import { Tab, Row, Col, Nav } from "react-bootstrap";
import Split from 'react-split'
import TaskStatistics from "./TaskStatistics";
import TruckStatistics from "./TruckStatistics";
import Animation from "./Animation";
import GanttChart from "./GanttChart";
function MainPanel({arcs, nodes, warehouses, stores, trucks, tasks, gantts}) {
return (
<Split
direction="vertical"
style={{ height: "96vh"}}>
<div className="animation-panel">
<Animation arcs={arcs}
nodes={nodes}
warehouses={warehouses}
stores={stores}
trucks={trucks}/>
</div>
<div>
<Tab.Container id="top-tabs" defaultActiveKey="trucks">
<Row className="gx-0" style={{height: "100%"}}>
<Col style={{height: "100%"}}>
<Nav variant="underline" className="flex-row">
<Nav.Item>
<Nav.Link eventKey="trucks">Trucks</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="gantt">Gantt chart</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="tasks">Tasks</Nav.Link>
</Nav.Item>
</Nav>
<Tab.Content className="m-1" style={{ overflowY: "scroll", height: `calc(100% - 2rem)`}}>
<Tab.Pane eventKey="trucks">
<TruckStatistics trucks={trucks} />
</Tab.Pane>
<Tab.Pane eventKey="gantt">
<GanttChart gantts={gantts} />
</Tab.Pane>
<Tab.Pane eventKey="tasks">
<TaskStatistics tasks={tasks} />
</Tab.Pane>
</Tab.Content>
</Col>
</Row>
</Tab.Container>
</div>
</Split>
);
}
export default MainPanel;