Skip to content

Commit f88eba5

Browse files
Patrick Schustertobifalk
Patrick Schuster
authored andcommitted
ui: Add replay functionality
Signed-off-by: Tobias Falkenstein <[email protected]>
1 parent b6c2095 commit f88eba5

9 files changed

+620
-42
lines changed

ui/src/App.js

+251-37
Large diffs are not rendered by default.

ui/src/components/controller.jsx

+24-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import hash from "object-hash";
2525
import jp from "jsonpath";
2626
import { Icon as LegacyIcon } from "@ant-design/compatible";
2727
import * as mathjs from "mathjs";
28+
import { STREAMINGTYPES } from "../enums";
2829

2930
class Controller extends Component {
3031
constructor(props) {
@@ -36,6 +37,7 @@ class Controller extends Component {
3637
this.sources = {};
3738
this.groupsVisibility = props.layout;
3839
this.uiEnabled = true;
40+
this.streamingType = props.streamingType;
3941
}
4042

4143
render() {
@@ -84,6 +86,17 @@ class Controller extends Component {
8486
this.uiEnabled = false;
8587
});
8688
}
89+
// In replay mode the parent component provides the uiConfigData.
90+
// Check if the provided uiConfigData has entries (should be done once).
91+
else if (Object.keys(this.props.uiConfigData).length > 0) {
92+
let data = this.props.uiConfigData.controller;
93+
this.paths = data.paths;
94+
this.layout = data.layout;
95+
this.title = data.title;
96+
this.action = data.action;
97+
this.elements = this.generateElements(data.elements);
98+
this.uiSpecificationLoaded = true;
99+
}
87100
};
88101

89102
generateElements = (specifiedElements) => {
@@ -107,6 +120,15 @@ class Controller extends Component {
107120
}
108121
});
109122
}
123+
} else if (this.streamingType === STREAMINGTYPES.REPLAY) {
124+
for (const endpoint in this.sources) {
125+
for (const path in this.sources[endpoint]) {
126+
const fetchEndpoint = this.resolveEndpoint(endpoint).replace("/api", "");
127+
if (Object.keys(this.props.cloeData).length > 0) {
128+
this.sources[endpoint][path] = jp.value(this.props.cloeData[fetchEndpoint], path);
129+
}
130+
}
131+
}
110132
}
111133
};
112134

@@ -291,7 +313,7 @@ class Controller extends Component {
291313
<Timebar
292314
key={timebarElement.id}
293315
controllerState={value}
294-
newSimTime={this.props.simTime}
316+
newSimTime={this.props.cloeData.simTime}
295317
simulationID={this.props.simulationID}
296318
colorTrue={timebarElement.params.color_true}
297319
colorFalse={timebarElement.params.color_false}
@@ -317,7 +339,7 @@ class Controller extends Component {
317339
key={linechartElement.id}
318340
sourcePaths={sourcePaths}
319341
nextYValues={values}
320-
nextXValue={this.props.simTime / 1000}
342+
nextXValue={this.props.cloeData.simTime / 1000}
321343
simulationID={this.props.simulationID}
322344
colors={linechartElement.params.colors}
323345
/>

ui/src/components/errorMessage.jsx

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2021 Robert Bosch GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import React, { Component } from "react";
17+
18+
// This component shows different error messages depending on content.
19+
class ErrorMessage extends Component {
20+
constructor(props) {
21+
super(props);
22+
this.fileType = props.fileType || null;
23+
this.errorMessage = props.errorMessage;
24+
this.state = {
25+
showErrorMessage: true
26+
};
27+
}
28+
29+
removeErrorMessage = () => {
30+
this.setState({ showErrorMessage: false });
31+
};
32+
33+
render() {
34+
if (this.state.showErrorMessage) {
35+
return (
36+
<div
37+
className="alert alert-warning alert-dismissible fade show"
38+
style={{ position: "absolute", right: "10px", top: "150px", zIndex: "9999" }}
39+
role="alert"
40+
>
41+
<strong>{this.fileType}</strong>
42+
{this.errorMessage}
43+
<button
44+
type="button"
45+
className="close"
46+
onClick={(e) => {
47+
this.removeErrorMessage();
48+
}}
49+
aria-label="Close"
50+
>
51+
<span aria-hidden="true">&times;</span>
52+
</button>
53+
</div>
54+
);
55+
} else {
56+
return <React.Fragment></React.Fragment>;
57+
}
58+
}
59+
}
60+
61+
export default ErrorMessage;

ui/src/components/fileUploader.jsx

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright 2021 Robert Bosch GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import React, { useState } from "react";
17+
import { UploadOutlined } from "@ant-design/icons";
18+
import pako from "pako";
19+
import ErrorMessage from "./errorMessage";
20+
21+
const FileUploader = (props) => {
22+
const { connected } = props;
23+
const hiddenFileInput = React.useRef(null);
24+
const [state, setState] = useState({ showErrorMessage: false, fileType: "" });
25+
// This method triggers the handleChange() event.
26+
const handleClick = (event) => {
27+
hiddenFileInput.current.click();
28+
};
29+
// This method is processing the file with the FileReader API
30+
// and sends the content to the parent method.
31+
const handleChange = (event) => {
32+
const fileUploaded = event.target.files[0];
33+
if (fileUploaded === undefined) {
34+
return;
35+
}
36+
let reader = new FileReader();
37+
if (fileUploaded.type === "application/json") {
38+
reader.onload = function(e) {
39+
props.handleFileUpload(fileUploaded, e.target.result);
40+
};
41+
reader.readAsText(fileUploaded);
42+
} else if (
43+
fileUploaded.type === "application/x-gzip" ||
44+
fileUploaded.type === "application/gzip"
45+
) {
46+
reader.onload = function(e) {
47+
let result = pako.inflate(e.target.result, { to: "string" });
48+
props.handleFileUpload(fileUploaded, result);
49+
};
50+
reader.readAsArrayBuffer(fileUploaded);
51+
} else {
52+
// Show error message for 5 seconds.
53+
setState({
54+
...state,
55+
showErrorMessage: true,
56+
fileType: fileUploaded.type
57+
});
58+
setTimeout(() => {
59+
setState({
60+
...state,
61+
showErrorMessage: false
62+
});
63+
}, 5000);
64+
}
65+
event.target.value = "";
66+
};
67+
return (
68+
<>
69+
{state.showErrorMessage && (
70+
<ErrorMessage
71+
errorMessage={" not supported. Try .json or .json.gz files"}
72+
fileType={state.fileType}
73+
></ErrorMessage>
74+
)}
75+
<input
76+
type="file"
77+
ref={hiddenFileInput}
78+
onChange={handleChange}
79+
style={{ display: "none" }}
80+
accept=".json, .json.gz"
81+
/>
82+
<div className="navbar-nav ml-auto mr-2">
83+
<div className="float-left my-lg-0 m-auto">
84+
<UploadOutlined
85+
className={connected ? "icon" : "icon-white"}
86+
style={{ fontSize: "26px" }}
87+
onClick={(e) => {
88+
handleClick(e);
89+
}}
90+
/>
91+
</div>
92+
</div>
93+
</>
94+
);
95+
};
96+
export default FileUploader;

ui/src/components/navbar.jsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,16 @@ import React from "react";
1717
import { MoreOutlined } from "@ant-design/icons";
1818
import { Popover } from "antd";
1919
import { copyToClipboard } from "../helpers";
20+
import FileUploader from "./fileUploader";
2021

2122
function NavBar(props) {
2223
const { connected, version } = props;
2324
const versionString = version.package_version;
25+
// Propagate the file to parent app.js.
26+
const handleFileUpload = (file, content) => {
27+
props.getSimulationDataFromJSON(file, content);
28+
};
29+
2430
return (
2531
<nav
2632
className={`navbar navbar-expand-lg navbar-light shadow-sm ${
@@ -54,7 +60,8 @@ function NavBar(props) {
5460
</small>
5561
</Popover>
5662
</div>
57-
<div className="navbar-nav ml-auto">
63+
<FileUploader handleFileUpload={handleFileUpload} connected={connected}></FileUploader>
64+
<div className="navbar-nav mx-1">
5865
<div className="float-left my-lg-0 m-auto">
5966
<MoreOutlined
6067
className={connected ? "icon" : "icon-white"}

ui/src/components/rendering.jsx

+22-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import Slider from "react-rangeslider";
2121
import { Select, Tag } from "antd";
2222
import { SettingFilled, UndoOutlined } from "@ant-design/icons";
2323
import SpriteText from "three-spritetext";
24+
import ReplayControlls from "./replayControls";
2425

2526
const OrbitControls = require("three-orbit-controls")(THREE);
2627
var randomColor = require("randomcolor");
@@ -105,7 +106,7 @@ class Rendering extends Component {
105106

106107
render() {
107108
// Make sensors available for input fields.
108-
const { sensors } = this.props;
109+
const { sensors, replayState, streamingType } = this.props;
109110
const selectObjSensors = this.getAvailSensors(sensors, this.sensorType.Object);
110111
const selectLaneSensors = this.getAvailSensors(sensors, this.sensorType.Lane);
111112

@@ -178,6 +179,14 @@ class Rendering extends Component {
178179
</div>
179180
</div>
180181
</div>
182+
<ReplayControlls
183+
replayState={replayState}
184+
streamingType={streamingType}
185+
isRecording={isRecording}
186+
toggleReplay={this.toggleReplay}
187+
fastForward={this.fastForward}
188+
rewind={this.rewind}
189+
></ReplayControlls>
181190
<div
182191
className="m-0 mt-2 text-light"
183192
style={{
@@ -937,6 +946,18 @@ class Rendering extends Component {
937946
this.setState({ optionsVisible: !this.state.optionsVisible });
938947
};
939948

949+
toggleReplay = (isRunning) => {
950+
this.props.toggleReplay(isRunning);
951+
};
952+
953+
fastForward = () => {
954+
this.props.fastForward();
955+
};
956+
957+
rewind = () => {
958+
this.props.rewind();
959+
};
960+
940961
resetRenderOptions = () => {
941962
if (this.state.helperAxesVisible) {
942963
this.scene.remove(this.axes);

0 commit comments

Comments
 (0)