594 lines
23 KiB
JavaScript
594 lines
23 KiB
JavaScript
import React, {Component} from 'react';
|
|
import PropTypes from 'prop-types';
|
|
|
|
import cx from 'classnames';
|
|
|
|
import AddSignals from './AddSignals';
|
|
import CanGraphList from './CanGraphList';
|
|
import RouteVideoSync from './RouteVideoSync';
|
|
import CanLog from './CanLog';
|
|
import Entries from '../models/can/entries';
|
|
import debounce from '../utils/debounce';
|
|
import PartSelector from './PartSelector';
|
|
import GraphData from '../models/graph-data';
|
|
|
|
export default class Explorer extends Component {
|
|
static propTypes = {
|
|
selectedMessage: PropTypes.string,
|
|
url: PropTypes.string,
|
|
live: PropTypes.bool.isRequired,
|
|
messages: PropTypes.objectOf(PropTypes.object),
|
|
onConfirmedSignalChange: PropTypes.func.isRequired,
|
|
canFrameOffset: PropTypes.number,
|
|
firstCanTime: PropTypes.number,
|
|
onSeek: PropTypes.func.isRequired,
|
|
autoplay: PropTypes.bool.isRequired,
|
|
onPartChange: PropTypes.func.isRequired,
|
|
partsCount: PropTypes.number,
|
|
};
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
plottedSignals: [],
|
|
graphData: [],
|
|
segment: [],
|
|
segmentIndices: [],
|
|
shouldShowAddSignal: true,
|
|
userSeekIndex: 0,
|
|
userSeekTime: props.currentParts[0] * 60,
|
|
playing: props.autoplay,
|
|
signals: {}
|
|
};
|
|
this.onSignalPlotPressed = this.onSignalPlotPressed.bind(this);
|
|
this.onSignalUnplotPressed = this.onSignalUnplotPressed.bind(this);
|
|
this.onSegmentChanged = this.onSegmentChanged.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.onPlay = this.onPlay.bind(this);
|
|
this.onPause = this.onPause.bind(this);
|
|
this.onVideoClick = this.onVideoClick.bind(this);
|
|
this.onSignalPlotChange = this.onSignalPlotChange.bind(this);
|
|
this._onKeyDown = this._onKeyDown.bind(this);
|
|
this.mergePlots = this.mergePlots.bind(this);
|
|
this.refreshGraphData = this.refreshGraphData.bind(this);
|
|
this.toggleShouldShowAddSignal = this.toggleShouldShowAddSignal.bind(this);
|
|
}
|
|
|
|
_onKeyDown(e) {
|
|
if(e.keyCode === 27){ // escape
|
|
this.resetSegment()
|
|
}
|
|
}
|
|
|
|
componentWillMount() {
|
|
document.addEventListener('keydown', this._onKeyDown);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
document.removeEventListener('keydown', this._onKeyDown);
|
|
}
|
|
|
|
clipSegment(segment, segmentIndices, nextMessage) {
|
|
if(segment.length === 2) {
|
|
const segmentStartIdx = nextMessage.entries.findIndex((e) => e.relTime >= segment[0]);
|
|
let segmentEndIdx = nextMessage.entries.findIndex((e) => e.relTime >= segment[1]);
|
|
if(segmentStartIdx !== -1) {
|
|
if(segmentEndIdx === -1) {
|
|
// previous segment end is past bounds of this message
|
|
segmentEndIdx = nextMessage.entries.length - 1;
|
|
}
|
|
const segmentStartTime = nextMessage.entries[segmentStartIdx].relTime;
|
|
const segmentEndTime = nextMessage.entries[segmentEndIdx].relTime;
|
|
|
|
segment = [segmentStartTime, segmentEndTime];
|
|
segmentIndices = [segmentStartIdx, segmentEndIdx];
|
|
} else {
|
|
// segment times are out of boudns for this message
|
|
segment = [];
|
|
segmentIndices = [];
|
|
}
|
|
}
|
|
|
|
return {segment, segmentIndices};
|
|
}
|
|
|
|
componentWillReceiveProps(nextProps) {
|
|
const nextMessage = nextProps.messages[nextProps.selectedMessage];
|
|
const curMessage = this.props.messages[this.props.selectedMessage];
|
|
let {plottedSignals, graphData} = this.state;
|
|
|
|
if(Object.keys(nextProps.messages).length === 0) {
|
|
this.resetSegment();
|
|
}
|
|
if(nextMessage && nextMessage.frame && nextMessage !== curMessage) {
|
|
const nextSignalNames = Object.keys(nextMessage.frame.signals);
|
|
|
|
if(nextSignalNames.length === 0) {
|
|
this.setState({shouldShowAddSignal: true});
|
|
}
|
|
}
|
|
|
|
// remove plottedSignals that no longer exist
|
|
plottedSignals = plottedSignals.map((plot) => plot.filter(({messageId, signalUid}, index) => {
|
|
const messageExists = Object.keys(nextProps.messages).indexOf(messageId) !== -1;
|
|
let signalExists = true;
|
|
if(!messageExists) {
|
|
graphData.splice(index, 1);
|
|
} else {
|
|
signalExists = Object.values(nextProps.messages[messageId].frame.signals)
|
|
.some((signal) => signal.uid === signalUid);
|
|
|
|
if(!signalExists) {
|
|
graphData[index].series = graphData[index].series.filter((entry) => entry.signalUid !== signalUid);
|
|
}
|
|
}
|
|
|
|
return messageExists && signalExists;
|
|
})).filter((plot) => plot.length > 0);
|
|
this.setState({plottedSignals, graphData});
|
|
|
|
if(nextProps.selectedMessage && nextProps.selectedMessage !== this.props.selectedMessage) {
|
|
// Update segment and seek state
|
|
// by finding a entry indices
|
|
// corresponding to old message segment/seek times.
|
|
|
|
let {segment, segmentIndices} = this.clipSegment(this.state.segment,
|
|
this.state.segmentIndices,
|
|
nextMessage);
|
|
|
|
const nextSeekMsgEntry = nextMessage.entries[nextProps.seekIndex];
|
|
let nextSeekTime;
|
|
if(nextSeekMsgEntry) {
|
|
nextSeekTime = nextSeekMsgEntry.relTime;
|
|
} else if(segment.length === 2) {
|
|
nextSeekTime = segment[0];
|
|
} else {
|
|
nextSeekTime = nextMessage.entries[0];
|
|
}
|
|
|
|
this.setState({segment,
|
|
segmentIndices,
|
|
userSeekIndex: nextProps.seekIndex,
|
|
userSeekTime: nextSeekTime});
|
|
}
|
|
|
|
if(nextMessage && curMessage && nextMessage.entries.length !== curMessage.entries.length) {
|
|
let {segment, segmentIndices} = this.clipSegment(this.state.segment,
|
|
this.state.segmentIndices,
|
|
nextMessage);
|
|
this.setState({segment, segmentIndices});
|
|
}
|
|
|
|
const partsDidChange = JSON.stringify(nextProps.currentParts) !== JSON.stringify(this.props.currentParts);
|
|
|
|
if(plottedSignals.length > 0) {
|
|
if(graphData.length !== plottedSignals.length || partsDidChange) {
|
|
this.refreshGraphData(nextProps.messages, plottedSignals);
|
|
} else if(graphData.length === plottedSignals.length) {
|
|
if(plottedSignals.some((plot) =>
|
|
plot.some(({messageId, signalUid}) => {
|
|
const signalName = Object.values(this.props.messages[messageId].frame.signals)
|
|
.find((s) => s.uid === signalUid);
|
|
|
|
return nextProps.messages[messageId].entries.length > 0
|
|
&& this.props.messages[messageId].entries.length > 0
|
|
&& nextProps.messages[messageId].entries[0].updated !== this.props.messages[messageId].entries[0].updated;
|
|
}))) {
|
|
this.refreshGraphData(nextProps.messages, plottedSignals);
|
|
} else {
|
|
graphData = GraphData.appendNewGraphData(plottedSignals, graphData, nextProps.messages, nextProps.firstCanTime);
|
|
this.setState({graphData});
|
|
}
|
|
}
|
|
}
|
|
|
|
if(partsDidChange) {
|
|
const {userSeekTime} = this.state;
|
|
const nextSeekTime = (userSeekTime - this.props.currentParts[0] * 60) + nextProps.currentParts[0] * 60;
|
|
this.setState({userSeekTime: nextSeekTime});
|
|
}
|
|
}
|
|
|
|
|
|
timeWindow() {
|
|
const {routeStartTime, currentParts} = this.props;
|
|
|
|
if(routeStartTime) {
|
|
const partStartOffset = currentParts[0] * 60,
|
|
partEndOffset = (currentParts[1] + 1) * 60;
|
|
|
|
const windowStartTime = routeStartTime.clone().add(partStartOffset, 's').format('HH:mm:ss');
|
|
const windowEndTime = routeStartTime.clone().add(partEndOffset, 's').format('HH:mm:ss');
|
|
|
|
return `${windowStartTime} - ${windowEndTime}`;
|
|
} else return '';
|
|
}
|
|
|
|
sortGraphData(graphData) {
|
|
return graphData.sort((entry1, entry2) => {
|
|
if(entry1.relTime < entry2.relTime) {
|
|
return -1;
|
|
} else if(entry1.relTime > entry2.relTime) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
});
|
|
}
|
|
|
|
calcGraphData(plottedSignals, messages) {
|
|
const {firstCanTime} = this.props;
|
|
if(typeof messages === 'undefined') {
|
|
messages = this.props.messages;
|
|
}
|
|
|
|
const series = this.sortGraphData(plottedSignals.map(({messageId, signalUid}) =>
|
|
GraphData._calcGraphData(messages[messageId], signalUid, firstCanTime))
|
|
.reduce((combined, signalData) => combined.concat(signalData), []));
|
|
|
|
return { series, updated: Date.now() };
|
|
}
|
|
|
|
onSignalPlotPressed(messageId, signalUid) {
|
|
let {plottedSignals, graphData} = this.state;
|
|
|
|
graphData = [this.calcGraphData([{messageId, signalUid}]), ...graphData];
|
|
plottedSignals = [[{messageId, signalUid}], ...plottedSignals];
|
|
|
|
this.setState({plottedSignals, graphData});
|
|
}
|
|
|
|
refreshGraphData(messages, plottedSignals) {
|
|
if(typeof messages === 'undefined') {
|
|
messages = this.props.messages;
|
|
}
|
|
if(typeof plottedSignals === 'undefined') {
|
|
plottedSignals = this.state.plottedSignals;
|
|
}
|
|
let graphData = plottedSignals.map((plotSignals, index) => this.calcGraphData(plotSignals, messages));
|
|
console.log('refreshGraphData')
|
|
this.setState({graphData});
|
|
}
|
|
|
|
onSignalUnplotPressed(messageId, signalUid) {
|
|
const {plottedSignals} = this.state;
|
|
const newPlottedSignals = plottedSignals.map((plot) =>
|
|
(plot.filter((signal) => !(signal.messageId === messageId && signal.signalUid === signalUid))))
|
|
.filter((plot) => plot.length > 0);
|
|
|
|
this.setState({plottedSignals: newPlottedSignals}, this.refreshGraphData(this.props.messages, newPlottedSignals));
|
|
}
|
|
|
|
updateSegment = debounce((messageId, segment) => {
|
|
const {entries} = this.props.messages[this.props.selectedMessage];
|
|
const segmentIndices = Entries.findSegmentIndices(entries, segment, true);
|
|
|
|
this.setState({segment, segmentIndices, userSeekIndex: segmentIndices[0], userSeekTime: segment[0]})
|
|
}, 250);
|
|
|
|
onSegmentChanged(messageId, segment) {
|
|
if(Array.isArray(segment)) {
|
|
this.updateSegment(messageId, segment);
|
|
}
|
|
}
|
|
|
|
resetSegment() {
|
|
const {segment, segmentIndices} = this.state;
|
|
const {messages, selectedMessage} = this.props;
|
|
if(segment.length > 0 || segmentIndices.length > 0) {
|
|
let userSeekTime = 0;
|
|
if(messages[selectedMessage] && messages[selectedMessage].entries.length > 0) {
|
|
userSeekTime = messages[selectedMessage].entries[0].relTime;
|
|
}
|
|
this.setState({segment: [], segmentIndices: [], userSeekIndex: 0, userSeekTime})
|
|
}
|
|
}
|
|
|
|
showAddSignal() {
|
|
this.setState({shouldShowAddSignal: true})
|
|
}
|
|
|
|
toggleShouldShowAddSignal() {
|
|
this.setState({shouldShowAddSignal: !this.state.shouldShowAddSignal});
|
|
}
|
|
|
|
indexFromSeekTime(time) {
|
|
// returns index guaranteed to be in [0, entries.length - 1]
|
|
|
|
const {entries} = this.props.messages[this.props.selectedMessage];
|
|
if(entries.length === 0) return null;
|
|
|
|
const {segmentIndices} = this.state;
|
|
if(segmentIndices.length === 2) {
|
|
for(let i = segmentIndices[0]; i <= segmentIndices[1]; i++) {
|
|
if(entries[i].relTime >= time) {
|
|
return i;
|
|
}
|
|
}
|
|
return segmentIndices[1];
|
|
} else {
|
|
for(let i = 0; i < entries.length; i++) {
|
|
if(entries[i].relTime >= time) {
|
|
return i;
|
|
}
|
|
}
|
|
return entries.length - 1;
|
|
}
|
|
}
|
|
|
|
onUserSeek(time) {
|
|
this.setState({userSeekTime: time});
|
|
const message = this.props.messages[this.props.selectedMessage];
|
|
if(!message) {
|
|
this.props.onUserSeek(time);
|
|
this.props.onSeek(0, time);
|
|
return;
|
|
}
|
|
|
|
const {entries} = message;
|
|
const userSeekIndex = this.indexFromSeekTime(time);
|
|
if(userSeekIndex) {
|
|
const seekTime = entries[userSeekIndex].relTime;
|
|
|
|
this.setState({userSeekIndex, userSeekTime: seekTime});
|
|
this.props.onSeek(userSeekIndex, seekTime);
|
|
} else {
|
|
this.props.onUserSeek(time);
|
|
this.setState({userSeekTime: time});
|
|
}
|
|
}
|
|
|
|
onPlaySeek(time) {
|
|
const message = this.props.messages[this.props.selectedMessage];
|
|
if(!message || message.entries.length === 0) {
|
|
this.props.onSeek(0, time);
|
|
return;
|
|
}
|
|
|
|
const {entries} = message
|
|
|
|
const seekIndex = this.indexFromSeekTime(time);
|
|
const seekTime = entries[seekIndex].relTime;
|
|
|
|
this.props.onSeek(seekIndex, seekTime);
|
|
}
|
|
|
|
onGraphTimeClick(messageId, time) {
|
|
const canTime = time + this.props.firstCanTime;
|
|
|
|
const {entries} = this.props.messages[messageId];
|
|
if(entries.length) {
|
|
const userSeekIndex = Entries.findTimeIndex(entries, canTime);
|
|
|
|
this.props.onUserSeek(time);
|
|
|
|
this.setState({userSeekIndex,
|
|
userSeekTime: time});
|
|
} else {
|
|
this.setState({userSeekTime: time});
|
|
}
|
|
}
|
|
|
|
onPlay() {
|
|
this.setState({playing: true});
|
|
}
|
|
|
|
onPause() {
|
|
this.setState({playing: false});
|
|
}
|
|
|
|
secondsLoadedRouteRelative(currentParts) {
|
|
return (currentParts[1] - currentParts[0] + 1) * 60;
|
|
}
|
|
|
|
secondsLoaded() {
|
|
const message = this.props.messages[this.props.selectedMessage];
|
|
if(!message || message.entries.length === 0) {
|
|
return this.secondsLoadedRouteRelative(this.props.currentParts);
|
|
}
|
|
|
|
const {entries} = message;
|
|
|
|
const {segment} = this.state;
|
|
if(segment.length === 2) {
|
|
return segment[1] - segment[0];
|
|
} else {
|
|
return entries[entries.length - 1].time - entries[0].time;
|
|
}
|
|
}
|
|
|
|
startOffset() {
|
|
const partOffset = this.props.currentParts[0] * 60;
|
|
const message = this.props.messages[this.props.selectedMessage];
|
|
if(!message || message.entries.length === 0) {
|
|
return partOffset;
|
|
}
|
|
|
|
const {entries} = message;
|
|
const {segment} = this.state;
|
|
let startTime;
|
|
if(segment.length === 2) {
|
|
startTime = segment[0];
|
|
} else {
|
|
startTime = entries[0].relTime;
|
|
}
|
|
|
|
if(startTime > partOffset && startTime < (this.props.currentParts[1] + 1) * 60) {
|
|
// startTime is within bounds of currently selected parts
|
|
return startTime;
|
|
} else {
|
|
return partOffset;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
onSignalPlotChange(shouldPlot, messageId, signalUid) {
|
|
if(shouldPlot) {
|
|
this.onSignalPlotPressed(messageId, signalUid);
|
|
} else {
|
|
this.onSignalUnplotPressed(messageId, signalUid);
|
|
}
|
|
}
|
|
|
|
renderSelectMessagePrompt() {
|
|
return (
|
|
<div className='cabana-explorer-select-prompt'>
|
|
<h1>Select a message</h1>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
selectedMessagePlottedSignalUids() {
|
|
const {plottedSignals} = this.state;
|
|
return plottedSignals
|
|
.map((plot) =>
|
|
plot.filter(({messageId, signalUid}) =>
|
|
messageId === this.props.selectedMessage)
|
|
.map(({signalUid}) => signalUid))
|
|
.reduce((arr, signalUid) => arr.concat(signalUid), []);
|
|
}
|
|
|
|
renderExplorerSignals() {
|
|
const selectedMessageKey = this.props.selectedMessage;
|
|
const selectedMessage = this.props.messages[selectedMessageKey];
|
|
const selectedMessageName = selectedMessage.frame !== undefined ? selectedMessage.frame.name : 'undefined';
|
|
return (
|
|
<div className='cabana-explorer-signals-wrapper'>
|
|
<div className='cabana-explorer-signals-header'>
|
|
<div className='cabana-explorer-signals-header-context'>
|
|
<h5 className='t-capline'>Selected Message:</h5>
|
|
<h3>{selectedMessageName}</h3>
|
|
</div>
|
|
<div className='cabana-explorer-signals-header-action'>
|
|
<button
|
|
className='button--small'
|
|
onClick={() => this.props.showEditMessageModal(selectedMessageKey)}>Edit</button>
|
|
</div>
|
|
</div>
|
|
<div className='cabana-explorer-signals-subheader'
|
|
onClick={ this.toggleShouldShowAddSignal }>
|
|
<strong>Edit Signals</strong>
|
|
</div>
|
|
<div className='cabana-explorer-signals-window'>
|
|
{this.state.shouldShowAddSignal ?
|
|
<AddSignals
|
|
onConfirmedSignalChange={this.props.onConfirmedSignalChange}
|
|
message={this.props.messages[this.props.selectedMessage]}
|
|
onClose={() => {this.setState({shouldShowAddSignal: false})}}
|
|
messageIndex={this.props.seekIndex}
|
|
onSignalPlotChange={this.onSignalPlotChange}
|
|
plottedSignalUids={this.selectedMessagePlottedSignalUids()}
|
|
/> : null}
|
|
<CanLog message={this.props.messages[this.props.selectedMessage]}
|
|
messageIndex={this.props.seekIndex}
|
|
segmentIndices={this.state.segmentIndices}
|
|
plottedSignals={this.state.plottedSignals}
|
|
onSignalPlotPressed={this.onSignalPlotPressed}
|
|
onSignalUnplotPressed={this.onSignalUnplotPressed}
|
|
showAddSignal={this.showAddSignal}
|
|
onMessageExpanded={this.onPause} />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
mergePlots({fromPlot, toPlot}) {
|
|
let {plottedSignals, graphData} = this.state;
|
|
|
|
// remove fromPlot from plottedSignals, graphData
|
|
const fromPlotIdx = plottedSignals.findIndex(
|
|
(plot) =>
|
|
plot.some((signal) =>
|
|
signal.signalUid === fromPlot.signalUid
|
|
&& signal.messageId === fromPlot.messageId));
|
|
plottedSignals.splice(fromPlotIdx, 1);
|
|
graphData.splice(fromPlotIdx, 1);
|
|
|
|
// calc new graph data
|
|
const newGraphData = this.calcGraphData([fromPlot, toPlot]);
|
|
|
|
const toPlotIdx = plottedSignals.findIndex(
|
|
(plot) =>
|
|
plot.some((signal) =>
|
|
signal.signalUid === toPlot.signalUid
|
|
&& signal.messageId === toPlot.messageId));
|
|
graphData[toPlotIdx] = newGraphData;
|
|
plottedSignals[toPlotIdx] = [fromPlot, toPlot];
|
|
|
|
this.setState({graphData, plottedSignals});
|
|
}
|
|
|
|
render() {
|
|
const signalsExpandedClass = this.state.shouldShowAddSignal ? 'is-expanded' : null;
|
|
return (
|
|
<div className='cabana-explorer'>
|
|
<div className={ cx('cabana-explorer-signals', signalsExpandedClass) }>
|
|
{this.props.messages[this.props.selectedMessage] ?
|
|
this.renderExplorerSignals()
|
|
: this.renderSelectMessagePrompt()}
|
|
</div>
|
|
<div className='cabana-explorer-visuals'>
|
|
{ this.props.live === false ?
|
|
<div>
|
|
<div className='cabana-explorer-visuals-header'>
|
|
{this.timeWindow()}
|
|
<PartSelector
|
|
onPartChange={this.props.onPartChange}
|
|
partsCount={this.props.partsCount}
|
|
/>
|
|
</div>
|
|
<RouteVideoSync
|
|
message={this.props.messages[this.props.selectedMessage]}
|
|
secondsLoaded={this.secondsLoaded()}
|
|
startOffset={this.startOffset()}
|
|
seekIndex={this.props.seekIndex}
|
|
userSeekIndex={this.state.userSeekIndex}
|
|
playing={this.state.playing}
|
|
url={this.props.url}
|
|
canFrameOffset={this.props.canFrameOffset}
|
|
firstCanTime={this.props.firstCanTime}
|
|
onVideoClick={this.onVideoClick}
|
|
onPlaySeek={this.onPlaySeek}
|
|
onUserSeek={this.onUserSeek}
|
|
onPlay={this.onPlay}
|
|
onPause={this.onPause}
|
|
userSeekTime={this.state.userSeekTime} />
|
|
</div>
|
|
: null }
|
|
{this.state.segment.length > 0 ?
|
|
<div className={'cabana-explorer-visuals-segmentreset'}
|
|
onClick={() => {this.resetSegment()}}>
|
|
<p>Reset Segment</p>
|
|
</div>
|
|
: null}
|
|
<CanGraphList
|
|
plottedSignals={this.state.plottedSignals}
|
|
messages={this.props.messages}
|
|
graphData={this.state.graphData}
|
|
onGraphTimeClick={this.onGraphTimeClick}
|
|
seekTime={this.props.seekTime}
|
|
onSegmentChanged={this.onSegmentChanged}
|
|
onSignalUnplotPressed={this.onSignalUnplotPressed}
|
|
segment={this.state.segment}
|
|
mergePlots={this.mergePlots}
|
|
live={this.props.live} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}
|