298 lines
11 KiB
JavaScript
298 lines
11 KiB
JavaScript
import React, {Component} from 'react';
|
|
import PropTypes from 'prop-types';
|
|
|
|
import { StyleSheet, css } from 'aphrodite/no-important';
|
|
|
|
import AddSignals from './AddSignals';
|
|
import CanHistogram from './CanHistogram';
|
|
import CanGraph from './CanGraph';
|
|
import NearestFrame from './NearestFrame';
|
|
import CanLog from './CanLog';
|
|
import RouteSeeker from './RouteSeeker';
|
|
import Entries from '../models/can/entries';
|
|
import debounce from '../utils/debounce';
|
|
|
|
export default class Explorer extends Component {
|
|
static propTypes = {
|
|
selectedMessage: PropTypes.string,
|
|
url: PropTypes.string,
|
|
messages: PropTypes.objectOf(PropTypes.object),
|
|
partsLoaded: PropTypes.number,
|
|
onConfirmedSignalChange: PropTypes.func,
|
|
canFrameOffset: PropTypes.number,
|
|
firstCanTime: PropTypes.number
|
|
};
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
plottedSignals: [],
|
|
segment: [],
|
|
segmentIndices: [],
|
|
shouldShowAddSignal: false,
|
|
userSeekIndex: 0,
|
|
seekIndex: 0,
|
|
seekTime: 0,
|
|
playing: false,
|
|
videoElement: null
|
|
};
|
|
this.onSignalPlotPressed = this.onSignalPlotPressed.bind(this);
|
|
this.onSignalUnplotPressed = this.onSignalUnplotPressed.bind(this);
|
|
this.onSegmentChanged = this.onSegmentChanged.bind(this);
|
|
this.resetSegment = this.resetSegment.bind(this);
|
|
this.showAddSignal = this.showAddSignal.bind(this);
|
|
this.onGraphTimeClick = this.onGraphTimeClick.bind(this);
|
|
this.onUserSeek = this.onUserSeek.bind(this);
|
|
this.onPlaySeek = this.onPlaySeek.bind(this);
|
|
|
|
this.onVideoElementAvailable = this.onVideoElementAvailable.bind(this);
|
|
this.onPlay = this.onPlay.bind(this);
|
|
this.onPause = this.onPause.bind(this);
|
|
this.onVideoClick = this.onVideoClick.bind(this);
|
|
}
|
|
|
|
graphData(msg, signalName) {
|
|
if(!msg) return null;
|
|
|
|
let samples = [];
|
|
let skip = Math.floor(msg.entries.length / CanGraph.MAX_POINTS);
|
|
|
|
if(skip == 0){
|
|
samples = msg.entries;
|
|
} else {
|
|
for(let i = 0; i < msg.entries.length; i += skip) {
|
|
samples.push(msg.entries[i]);
|
|
}
|
|
}
|
|
|
|
return samples.map((entry) => {
|
|
return {x: entry.time,
|
|
xRel: entry.time - this.props.firstCanTime,
|
|
y: entry.signals[signalName],
|
|
unit: msg.signals[signalName].unit}
|
|
});
|
|
}
|
|
|
|
onSignalPlotPressed(messageId, name) {
|
|
const {plottedSignals} = this.state;
|
|
this.setState({plottedSignals: plottedSignals.concat([{messageId, name}])})
|
|
}
|
|
|
|
onSignalUnplotPressed(messageId, name) {
|
|
const {plottedSignals} = this.state;
|
|
const newPlottedSignals = plottedSignals.filter((signal) => !(signal.messageId == messageId && signal.name == name));
|
|
|
|
this.setState({plottedSignals: newPlottedSignals})
|
|
}
|
|
|
|
updateSegment = debounce((segment) => {
|
|
const {entries} = this.props.messages[this.props.selectedMessage];
|
|
const segmentIndices = Entries.findSegmentIndices(entries, segment)
|
|
|
|
this.setState({segment, segmentIndices, userSeekIndex: segmentIndices[0]})
|
|
}, 250);
|
|
|
|
onSegmentChanged(segment) {
|
|
if(Array.isArray(segment)) {
|
|
this.updateSegment(segment);
|
|
}
|
|
}
|
|
|
|
resetSegment() {
|
|
this.setState({segment: [], segmentIndices: [], userSeekIndex: 0})
|
|
}
|
|
|
|
showAddSignal() {
|
|
this.setState({shouldShowAddSignal: true})
|
|
}
|
|
|
|
indexFromSeekRatio(ratio) {
|
|
const {entries} = this.props.messages[this.props.selectedMessage];
|
|
const {segmentIndices} = this.state;
|
|
let segmentLength, offset;
|
|
if(segmentIndices.length === 2) {
|
|
offset = segmentIndices[0];
|
|
segmentLength = segmentIndices[1] - segmentIndices[0];
|
|
} else {
|
|
offset = 0;
|
|
segmentLength = entries.length;
|
|
}
|
|
|
|
return offset + Math.round(ratio * segmentLength);
|
|
}
|
|
|
|
onUserSeek(ratio) {
|
|
const userSeekIndex = this.indexFromSeekRatio(ratio);
|
|
this.setState({seekIndex: userSeekIndex, userSeekIndex});
|
|
}
|
|
|
|
onPlaySeek(ratio) {
|
|
const {entries} = this.props.messages[this.props.selectedMessage];
|
|
|
|
const seekIndex = this.indexFromSeekRatio(ratio);
|
|
this.setState({seekIndex, seekTime: entries[seekIndex].time});
|
|
}
|
|
|
|
onGraphTimeClick(time) {
|
|
const canTime = time + this.props.firstCanTime;
|
|
|
|
const {segmentIndices} = this.state;
|
|
const {entries} = this.props.messages[this.props.selectedMessage];
|
|
const userSeekIndex = Entries.findTimeIndex(entries, canTime);
|
|
|
|
this.setState({userSeekIndex, playing: true});
|
|
}
|
|
|
|
onVideoElementAvailable(videoElement) {
|
|
this.setState({videoElement});
|
|
}
|
|
|
|
onPlay() {
|
|
this.setState({playing: true});
|
|
}
|
|
|
|
onPause() {
|
|
this.setState({playing: false});
|
|
}
|
|
|
|
secondsLoaded() {
|
|
const {entries} = this.props.messages[this.props.selectedMessage];
|
|
const {segmentIndices} = this.state;
|
|
if(segmentIndices.length === 2) {
|
|
const [low, hi] = segmentIndices;
|
|
return entries[hi].time - entries[low].time;
|
|
} else {
|
|
return entries[entries.length - 1].time - entries[0].time;
|
|
}
|
|
}
|
|
|
|
startOffset() {
|
|
const {entries} = this.props.messages[this.props.selectedMessage];
|
|
const {segmentIndices} = this.state;
|
|
const {canFrameOffset, firstCanTime} = this.props;
|
|
if(segmentIndices.length === 2) {
|
|
const [low, _] = segmentIndices;
|
|
const startEntry = entries[low];
|
|
// can frame offset is time from first frame to first can
|
|
// add time from first can to first can in segment
|
|
|
|
return canFrameOffset + (startEntry.time - firstCanTime);
|
|
} else {
|
|
return canFrameOffset;
|
|
}
|
|
}
|
|
|
|
onVideoClick() {
|
|
const playing = !this.state.playing;
|
|
this.setState({playing});
|
|
}
|
|
|
|
seekTime() {
|
|
const {userSeekIndex} = this.state;
|
|
const msg = this.props.messages[this.props.selectedMessage];
|
|
return msg.entries[userSeekIndex].time;
|
|
}
|
|
|
|
relativeSegment() {
|
|
const {segment} = this.state;
|
|
if(segment.length === 2) {
|
|
return [segment[0] - this.props.firstCanTime,
|
|
segment[1] - this.props.firstCanTime];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
render() {
|
|
return (<div className={css(Styles.root)}>
|
|
{this.props.messages[this.props.selectedMessage] !== undefined ?
|
|
<RouteSeeker
|
|
seekIndex={this.state.seekIndex}
|
|
secondsLoaded={this.secondsLoaded()}
|
|
startOffset={this.startOffset()}
|
|
segmentIndices={this.state.segmentIndices}
|
|
onUserSeek={this.onUserSeek}
|
|
onPlaySeek={this.onPlaySeek}
|
|
message={this.props.messages[this.props.selectedMessage]}
|
|
videoElement={this.state.videoElement}
|
|
onPlay={this.onPlay}
|
|
onPause={this.onPause}
|
|
playing={this.state.playing}
|
|
/> : null}
|
|
<CanHistogram
|
|
message={this.props.messages[this.props.selectedMessage]}
|
|
indices={this.state.segmentIndices}
|
|
onSegmentChanged={this.onSegmentChanged}
|
|
onResetClicked={this.resetSegment}
|
|
partsLoaded={this.props.partsLoaded}
|
|
/>
|
|
<div className={css(Styles.dataContainer)}>
|
|
<div className={css(Styles.left)}>
|
|
{this.state.shouldShowAddSignal ?
|
|
<AddSignals
|
|
onConfirmedSignalChange={this.props.onConfirmedSignalChange}
|
|
message={this.props.messages[this.props.selectedMessage]}
|
|
onClose={() => {this.setState({shouldShowAddSignal: false})}}
|
|
messageIndex={this.state.seekIndex}
|
|
/> : null}
|
|
<CanLog message={this.props.messages[this.props.selectedMessage]}
|
|
messageIndex={this.state.seekIndex}
|
|
segmentIndices={this.state.segmentIndices}
|
|
plottedSignals={this.state.plottedSignals}
|
|
onSignalPlotPressed={this.onSignalPlotPressed}
|
|
onSignalUnplotPressed={this.onSignalUnplotPressed}
|
|
showAddSignal={this.showAddSignal} />
|
|
</div>
|
|
<div className={css(Styles.right)}>
|
|
{this.props.messages[this.props.selectedMessage] !== undefined ?
|
|
<NearestFrame message={this.props.messages[this.props.selectedMessage]}
|
|
messageIndex={this.state.userSeekIndex}
|
|
playing={this.state.playing}
|
|
url={this.props.url}
|
|
canFrameOffset={this.props.canFrameOffset}
|
|
onVideoElementAvailable={this.onVideoElementAvailable}
|
|
onVideoClick={this.onVideoClick}
|
|
/> : null}
|
|
|
|
{this.state.plottedSignals.map(({messageId, name}) => {
|
|
const msg = this.props.messages[messageId];
|
|
|
|
return <CanGraph key={messageId + '_' + name}
|
|
unplot={() => {this.onSignalUnplotPressed(messageId, name)}}
|
|
messageName={msg.name}
|
|
signalSpec={msg.signals[name]}
|
|
segment={this.relativeSegment()}
|
|
data={this.graphData(msg, name)}
|
|
onRelativeTimeClick={this.onGraphTimeClick}
|
|
currentTime={this.state.seekTime - this.props.firstCanTime} />;
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>);
|
|
}
|
|
}
|
|
|
|
const Styles = StyleSheet.create({
|
|
root: {
|
|
flexDirection: 'column',
|
|
flex: 4,
|
|
width: '100%',
|
|
},
|
|
dataContainer: {
|
|
paddingTop: '10px',
|
|
paddingLeft: '10px',
|
|
flexDirection: 'row',
|
|
flex: 1,
|
|
display: 'flex',
|
|
width: '100%',
|
|
height: '100vh'
|
|
},
|
|
left: {
|
|
flex: '2 3 auto',
|
|
overflow: 'auto'
|
|
},
|
|
right: {
|
|
flex: '4 1',
|
|
}
|
|
})
|