cabana/src/components/AddSignals.js

566 lines
16 KiB
JavaScript

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { StyleSheet } from 'aphrodite/no-important';
import css from '../utils/css';
import SignalLegend from './SignalLegend';
import Signal from '../models/can/signal';
import { shade } from '../utils/color';
import DbcUtils from '../utils/dbc';
/*
AddSignals component draws an 8x8 matrix
representing the bytes in a CAN message, alongside
a signal legend. Dragging on the matrix
either extends or creates a signal, which is
configurable in the legend.
*/
const Styles = StyleSheet.create({
bit: {
margin: 0,
padding: 12,
userSelect: 'none',
cursor: 'pointer',
textAlign: 'center',
position: 'relative'
},
bitSelectedStyle: {
backgroundColor: 'rgba(0,119,158,0.5)'
},
bitSignificance: {
fontSize: 12,
display: 'block',
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
margin: '0 auto'
},
highlightedSignalTitle: {
backgroundColor: 'rgba(0,0,0,0.2)'
}
});
export default class AddSignals extends Component {
static propTypes = {
message: PropTypes.object,
onConfirmedSignalChange: PropTypes.func,
messageIndex: PropTypes.number,
onSignalPlotChange: PropTypes.func,
plottedSignalUids: PropTypes.array
};
constructor(props) {
super(props);
let signals = {};
if (props.message && props.message.frame && props.message.frame.signals) {
signals = this.copySignals(props.message.frame.signals);
}
this.state = {
bits: [],
signals,
signalStyles: this.calcSignalStyles(signals),
highlightedSignal: null,
dragStartBit: null,
dragSignal: null,
dragCurrentBit: null
};
}
copySignals(signals) {
return Object.entries(signals).reduce(
(signalsCopy, [signalName, signal]) => {
signalsCopy[signalName] = Object.assign(Object.create(signal), signal);
return signalsCopy;
},
{}
);
}
shouldComponentUpdate(nextProps, nextState) {
return (
nextProps.message.hexData !== this.props.message.hexData
|| nextProps.messageIndex !== this.props.messageIndex
|| JSON.stringify(nextProps.plottedSignalUids)
!== JSON.stringify(this.props.plottedSignalUids)
|| JSON.stringify(this.state) !== JSON.stringify(nextState)
);
}
signalColorStyle(signal) {
const { colors } = signal;
let colorRgbStr;
let backgroundColor;
if (this.state && this.state.highlightedSignal === signal.name) {
// when signal highlighted,
// darkened background and lightened text.
const darkenedColors = shade(colors, -0.5);
const lightenedColors = shade(colors, 0.9);
colorRgbStr = `rgb(${lightenedColors.join(',')})`;
backgroundColor = `rgba(${darkenedColors.join(',')},0.5)`;
} else {
const colorsCommaSep = colors.join(',');
colorRgbStr = `rgb(${colorsCommaSep})`;
backgroundColor = `rgba(${colorsCommaSep},0.2)`;
}
const style = StyleSheet.create({
signal: { color: colorRgbStr, backgroundColor }
}).signal;
return style;
}
updateSignalStyles = () => {
const signalStyles = this.calcSignalStyles(this.state.signals);
this.setState({ signalStyles });
};
calcSignalStyles(signals) {
const signalStyles = {};
Object.values(signals).forEach((signal) => {
signalStyles[signal.name] = this.signalColorStyle(signal);
});
return signalStyles;
}
componentWillReceiveProps({ message }) {
const isNewMessage = message.address !== this.props.message.address;
if (isNewMessage) {
const signals = message.frame ? message.frame.signals : {};
this.setState(
{ signals: this.copySignals(signals) },
this.updateSignalStyles
);
}
}
signalForBit(bitIdx) {
// bitIdx in [0,64)
// returns instance of Signal
return Object.values(this.state.signals).filter(
(signal) => signal.bitDescription(bitIdx) !== null
)[0];
}
onSignalHover = (signal) => {
if (!signal) return;
this.setState({ highlightedSignal: signal.name }, this.updateSignalStyles);
};
signalBitIndex(bitIdx, signal) {
// todo does this work for both big and little endian?
let { startBit } = signal;
if (!signal.isLittleEndian) {
startBit = DbcUtils.bigEndianBitIndex(startBit);
}
return bitIdx - startBit;
}
onBitHover = (bitIdx, signal) => {
let { dragStartBit, signals, dragSignal } = this.state;
if (dragStartBit !== null) {
if (dragSignal !== null) {
signals = this.copySignals(signals);
dragSignal = Object.assign(Object.create(dragSignal), dragSignal);
if (dragStartBit === dragSignal.startBit && dragSignal.size > 1) {
if (!dragSignal.isLittleEndian) {
// should not be able to drag the msb past the lsb
const hoveredBigEndian = DbcUtils.bigEndianBitIndex(bitIdx);
const lsbBigEndian = dragSignal.lsbBitNumber();
if (hoveredBigEndian > lsbBigEndian) {
return;
}
} else {
// should not be able to drag the lsb past the msb
if (bitIdx > dragSignal.msbBitIndex()) {
return;
}
}
const diff = bitIdx - dragStartBit;
if (dragSignal.isLittleEndian) {
dragSignal.size -= diff;
} else if (dragSignal.bitDescription(bitIdx) === null) {
dragSignal.size += Math.abs(diff);
} else {
dragSignal.size -= Math.abs(diff);
}
dragSignal.startBit += diff;
signals[dragSignal.name] = dragSignal;
dragStartBit = dragSignal.startBit;
} else if (dragSignal.size === 1) {
// 1-bit signals can be dragged in either direction
if (Math.floor(bitIdx / 8) === Math.floor(dragStartBit / 8)) {
if (bitIdx > dragStartBit) {
if (dragSignal.isLittleEndian) {
dragSignal.size = bitIdx - dragSignal.startBit;
} else {
dragSignal.startBit = bitIdx;
dragSignal.size = bitIdx - dragStartBit + 1;
dragStartBit = bitIdx;
}
} else if (dragSignal.isLittleEndian) {
dragSignal.startBit = bitIdx;
dragSignal.size = dragStartBit - bitIdx + 1;
dragStartBit = bitIdx;
} else {
dragSignal.size = dragStartBit - bitIdx + 1;
dragStartBit = bitIdx;
}
}
signals[dragSignal.name] = dragSignal;
} else if (
dragSignal.isLittleEndian
&& dragStartBit === dragSignal.msbBitIndex()
) {
if (bitIdx < dragSignal.startBit) {
// should not be able to drag the MSB past the LSB
return;
}
const diff = bitIdx - dragStartBit;
if (dragSignal.bitDescription(bitIdx) === null) {
dragSignal.size += Math.abs(diff);
} else {
dragSignal.size -= Math.abs(diff);
}
signals[dragSignal.name] = dragSignal;
dragStartBit = dragSignal.msbBitIndex();
} else if (
!dragSignal.isLittleEndian
&& dragStartBit === dragSignal.lsbBitIndex()
) {
const diff = bitIdx - dragStartBit;
if (dragSignal.bitDescription(bitIdx) === null) {
dragSignal.size += Math.abs(diff);
} else {
dragSignal.size -= Math.abs(diff);
}
signals[dragSignal.name] = dragSignal;
dragStartBit = dragSignal.lsbBitIndex();
}
this.setState({
signals,
dragSignal,
dragCurrentBit: bitIdx,
dragStartBit
});
} else {
this.setState({ dragCurrentBit: bitIdx });
}
}
if (signal) {
this.onSignalHover(signal);
}
};
onSignalHoverEnd = (signal) => {
if (!signal) return;
this.setState({ highlightedSignal: null }, this.updateSignalStyles);
};
nextNewSignalName() {
const existingNames = Object.keys(this.state.signals);
let signalNum = 1;
let signalName;
do {
signalName = `NEW_SIGNAL_${signalNum}`;
signalNum++;
} while (existingNames.indexOf(signalName) !== -1);
return signalName;
}
onBitMouseDown(dragStartBit, dragSignal) {
this.setState({
dragStartBit,
dragSignal: dragSignal || null
});
}
createSignal({ startBit, size, isLittleEndian }) {
const signal = new Signal({
name: this.nextNewSignalName(),
startBit,
size,
isLittleEndian
});
let { signals } = this.state;
signals = { ...signals };
signals[signal.name] = signal;
this.setState({ signals }, this.propagateUpSignalChange);
}
createSignalIfNotExtendingOne(dragStartBit, dragEndBit) {
if (this.state.dragSignal === null) {
// check for overlapping bits
for (let i = dragStartBit; i <= dragEndBit; i++) {
if (this.signalForBit(i) !== undefined) {
// Don't create signal if a signal is already defined in the selected range.
return;
}
}
const isDragAcrossSingleByte = Math.floor(dragEndBit / 8) === Math.floor(dragStartBit / 8);
const isDragDirectionUp = !isDragAcrossSingleByte && dragEndBit < dragStartBit;
let isLittleEndian;
if (isDragAcrossSingleByte || !isDragDirectionUp) {
isLittleEndian = dragStartBit % 8 < 4;
} else {
isLittleEndian = dragStartBit % 8 >= 4;
}
let size;
let startBit = dragStartBit;
if (isDragAcrossSingleByte) {
size = Math.abs(dragEndBit - dragStartBit) + 1;
} else if (isLittleEndian) {
if (dragEndBit > dragStartBit) {
startBit = dragStartBit;
size = dragEndBit - dragStartBit + 1;
} else {
startBit = dragEndBit;
size = dragStartBit - dragEndBit + 1;
}
} else {
if (dragEndBit < dragStartBit) {
startBit = dragEndBit;
}
size = Math.abs(
DbcUtils.bigEndianBitIndex(dragEndBit)
- DbcUtils.bigEndianBitIndex(dragStartBit)
) + 1;
}
this.createSignal({ startBit, size, isLittleEndian });
}
}
onBitMouseUp(dragEndBit, signal) {
if (this.state.dragStartBit !== null) {
const { dragStartBit } = this.state;
if (dragEndBit !== dragStartBit) {
// one-bit signal requires double click
// see onBitDoubleClick
this.createSignalIfNotExtendingOne(dragStartBit, dragEndBit);
}
this.propagateUpSignalChange();
this.resetDragState();
}
}
byteValueHex(byteIdx) {
const { entries } = this.props.message;
if (this.props.messageIndex < entries.length) {
const entry = entries[this.props.messageIndex];
return entry.hexData.substr(byteIdx * 2, 2);
}
return '--';
}
bitValue(byteIdx, byteBitIdx) {
const { entries } = this.props.message;
if (this.props.messageIndex < entries.length) {
const entry = entries[this.props.messageIndex];
const data = Buffer.from(entry.hexData, 'hex');
if (byteIdx >= data.length) {
return '-';
}
const byte = data.readInt8(byteIdx);
return (byte >> byteBitIdx) & 1;
}
return '-';
}
bitIsContainedInSelection(bitIdx, isLittleEndian = false) {
const { dragStartBit, dragCurrentBit } = this.state;
if (isLittleEndian || dragStartBit % 8 < 4) {
return (
dragStartBit !== null
&& dragCurrentBit !== null
&& bitIdx >= dragStartBit
&& bitIdx <= dragCurrentBit
);
}
const bigEndianStartBit = DbcUtils.bigEndianBitIndex(dragStartBit);
const bigEndianCurrentBit = DbcUtils.bigEndianBitIndex(dragCurrentBit);
const bigEndianBitIdx = DbcUtils.bigEndianBitIndex(bitIdx);
return (
dragStartBit !== null
&& dragCurrentBit !== null
&& bigEndianBitIdx >= bigEndianStartBit
&& bigEndianBitIdx <= bigEndianCurrentBit
);
}
onBitDoubleClick(startBit, signal) {
if (signal === undefined) {
this.createSignal({ startBit, size: 1, isLittleEndian: false });
}
}
renderBitMatrix() {
const { message } = this.props;
const rows = [];
let rowCount;
if (message.frame && message.frame.size) {
rowCount = Math.floor((message.frame.size * 8) / 8);
} else {
rowCount = 8;
}
for (let i = 0; i < rowCount; i++) {
const rowBits = [];
for (let j = 7; j >= 0; j--) {
const bitIdx = i * 8 + j;
const signal = this.signalForBit(bitIdx);
let bitStyle = null;
let bitSignificance = '';
if (signal) {
bitStyle = this.state.signalStyles[signal.name] || null;
const bitDesc = signal.bitDescription(bitIdx);
bitSignificance = bitDesc.isMsb ? 'msb' : bitDesc.isLsb ? 'lsb' : '';
} else if (this.bitIsContainedInSelection(bitIdx)) {
bitStyle = Styles.bitSelectedStyle;
}
const className = css('bit', Styles.bit, bitStyle);
const bitValue = this.bitValue(i, j);
rowBits.push(
<td
key={j.toString()}
className={className}
onMouseEnter={() => this.onBitHover(bitIdx, signal)}
onMouseLeave={() => this.onSignalHoverEnd(signal)}
onMouseDown={this.onBitMouseDown.bind(this, bitIdx, signal)}
onMouseUp={this.onBitMouseUp.bind(this, bitIdx, signal)}
onDoubleClick={() => this.onBitDoubleClick(bitIdx, signal)}
>
<span>{bitValue}</span>
<span className={css(Styles.bitSignificance)}>
{bitSignificance}
</span>
</td>
);
}
rowBits.push(<td key="hex-repr">{this.byteValueHex(i)}</td>);
rows.push(<tr key={i.toString()}>{rowBits}</tr>);
}
return (
<div className="cabana-explorer-signals-matrix">
<table cellSpacing={0} onMouseLeave={this.resetDragState}>
<tbody>{rows}</tbody>
</table>
</div>
);
}
resetDragState = () => {
this.setState({
dragStartBit: null,
dragSignal: null,
dragCurrentBit: null
});
};
onTentativeSignalChange = (signal) => {
// Tentative signal changes are not propagated up
// but their effects are displayed in the bitmatrix
const { signals } = this.state;
signals[signal.name] = signal;
this.setState({ signals });
};
onSignalChange = (signal, oldSignal) => {
const { signals } = this.state;
for (const signalName in signals) {
if (signals[signalName].uid === signal.uid) {
delete signals[signalName];
}
}
signals[signal.name] = signal;
this.setState({ signals }, this.propagateUpSignalChange);
};
onSignalRemove = (signal) => {
const { signals } = this.state;
delete signals[signal.name];
this.setState({ signals }, this.propagateUpSignalChange);
};
propagateUpSignalChange() {
const { signals } = this.state;
this.props.onConfirmedSignalChange(
this.props.message,
this.copySignals(signals)
);
}
onSignalPlotChange = (shouldPlot, signalUid) => {
const { message } = this.props;
this.props.onSignalPlotChange(shouldPlot, message.id, signalUid);
};
render() {
return (
<div className="cabana-explorer-signals-controller">
{Object.keys(this.state.signals).length === 0 ? (
<p>Double click or drag to add a signal</p>
) : null}
{this.props.message.entries[this.props.messageIndex] ? (
<div className="cabana-explorer-signals-time">
<p>
time:
{' '}
{this.props.message.entries[
this.props.messageIndex
].relTime.toFixed(3)}
</p>
</div>
) : null}
{this.props.message.isLogEvent || this.renderBitMatrix()}
<SignalLegend
isLogEvent={!!this.props.message.isLogEvent}
signals={this.state.signals}
signalStyles={this.state.signalStyles}
highlightedSignal={this.state.highlightedSignal}
onSignalHover={this.onSignalHover}
onSignalHoverEnd={this.onSignalHoverEnd}
onTentativeSignalChange={this.onTentativeSignalChange}
onSignalChange={this.onSignalChange}
onSignalRemove={this.onSignalRemove}
onSignalPlotChange={this.onSignalPlotChange}
plottedSignalUids={this.props.plottedSignalUids}
/>
</div>
);
}
}