prettier the js

main
ChristopherBiscardi 2017-12-12 18:27:20 -08:00
parent 40e7efad4c
commit beb0a7ea01
No known key found for this signature in database
GPG Key ID: 703265E1DE405983
94 changed files with 7250 additions and 6338 deletions

View File

@ -17,10 +17,13 @@
"github-api": "^3.0.0", "github-api": "^3.0.0",
"hls": "0.0.1", "hls": "0.0.1",
"hls.js": "^0.7.9", "hls.js": "^0.7.9",
"husky": "^0.14.3",
"int64-buffer": "^0.1.9", "int64-buffer": "^0.1.9",
"js-cookie": "^2.1.4", "js-cookie": "^2.1.4",
"left-pad": "^1.1.3", "left-pad": "^1.1.3",
"lint-staged": "^6.0.0",
"moment": "^2.18.1", "moment": "^2.18.1",
"prettier": "^1.9.2",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"raven-js": "^3.16.0", "raven-js": "^3.16.0",
"react": "^16.2.0", "react": "^16.2.0",
@ -57,6 +60,13 @@
"build": "react-app-rewired build", "build": "react-app-rewired build",
"test": "react-app-rewired test --env=jsdom", "test": "react-app-rewired test --env=jsdom",
"sass": "sass":
"scss src/index.scss:src/index.css; sass --watch src/index.scss:src/index.css" "scss src/index.scss:src/index.css; sass --watch src/index.scss:src/index.css",
"precommit": "lint-staged"
},
"lint-staged": {
"*.{js,jsx}": ["prettier --parser flow --write", "git add"],
"*.json": ["prettier --parser json --write", "git add"],
"*.{graphql,gql}": ["prettier --parser graphql --write", "git add"],
"*.{md,markdown}": ["prettier --parser markdown --write", "git add"]
} }
} }

View File

@ -1,25 +1,27 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import DBC from '../../models/can/dbc'; import DBC from "../../models/can/dbc";
import Signal from '../../models/can/signal'; import Signal from "../../models/can/signal";
test('setting signals should create a message', () => { test("setting signals should create a message", () => {
const dbc = new DBC(); const dbc = new DBC();
dbc.setSignals(100, {'My Signal': new Signal({name: 'My Signal'})}); dbc.setSignals(100, { "My Signal": new Signal({ name: "My Signal" }) });
expect(dbc.messages.has(100)).toBe(true); expect(dbc.messages.has(100)).toBe(true);
}); });
test('setting signals should update DBC.boardUnits', () => { test("setting signals should update DBC.boardUnits", () => {
const dbc = new DBC(); const dbc = new DBC();
dbc.setSignals(100, {'My Signal': new Signal({name: 'My Signal', receiver: ['NEO']})}); dbc.setSignals(100, {
"My Signal": new Signal({ name: "My Signal", receiver: ["NEO"] })
});
expect(dbc.boardUnits.map((bu) => bu.name).indexOf('NEO')).toBe(0); expect(dbc.boardUnits.map(bu => bu.name).indexOf("NEO")).toBe(0);
}); });
test('adding a signal should update DBC.boardUnits', () => { test("adding a signal should update DBC.boardUnits", () => {
const dbc = new DBC(); const dbc = new DBC();
dbc.createFrame(100); dbc.createFrame(100);
dbc.addSignal(100, new Signal({name: 'My Signal', receiver: ['NEO']})); dbc.addSignal(100, new Signal({ name: "My Signal", receiver: ["NEO"] }));
expect(dbc.boardUnits.map((bu) => bu.name).indexOf('NEO')).toBe(0); expect(dbc.boardUnits.map(bu => bu.name).indexOf("NEO")).toBe(0);
}); });

View File

@ -1,7 +1,7 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import DBC, {swapOrder} from '../../models/can/dbc'; import DBC, { swapOrder } from "../../models/can/dbc";
import Signal from '../../models/can/signal'; import Signal from "../../models/can/signal";
import Bitarray from '../../models/bitarray'; import Bitarray from "../../models/bitarray";
const DBC_MESSAGE_DEF = `BO_ 228 STEERING_CONTROL: 5 ADAS const DBC_MESSAGE_DEF = `BO_ 228 STEERING_CONTROL: 5 ADAS
SG_ STEER_TORQUE : 7|16@0- (1,0) [-3840|3840] "" EPS SG_ STEER_TORQUE : 7|16@0- (1,0) [-3840|3840] "" EPS
@ -63,7 +63,7 @@ BO_ 228 STEERING_CONTROL: 5 ADAS
CM_ SG_ 228 STEER_TORQUE "steer torque is the amount of torque in Nm applied";`; CM_ SG_ 228 STEER_TORQUE "steer torque is the amount of torque in Nm applied";`;
const DBC_SIGNAL_WITH_MULTI_LINE_COMMENT = ` const DBC_SIGNAL_WITH_MULTI_LINE_COMMENT = `
BO_ 228 STEERING_CONTROL: 5 ADAS BO_ 228 STEERING_CONTROL: 5 ADAS
SG_ STEER_TORQUE : 7|16@0- (1,0) [-3840|3840] "" EPS SG_ STEER_TORQUE : 7|16@0- (1,0) [-3840|3840] "" EPS
SG_ STEER_TORQUE_REQUEST : 23|1@0+ (1,0) [0|1] "" EPS SG_ STEER_TORQUE_REQUEST : 23|1@0+ (1,0) [0|1] "" EPS
@ -137,136 +137,148 @@ VAL_TABLE_ DI_speedUnits 1 "DI_SPEED_KPH" 0 "DI_SPEED_MPH" ;
`; `;
const steerTorqueSignal = new Signal({ const steerTorqueSignal = new Signal({
name: 'STEER_TORQUE', name: "STEER_TORQUE",
startBit: 7, startBit: 7,
size: 16, size: 16,
isLittleEndian: false, isLittleEndian: false,
isSigned: true, isSigned: true,
factor: 1, factor: 1,
offset: 0, offset: 0,
min: -3840, min: -3840,
max: 3840, max: 3840,
receiver: ['EPS'], receiver: ["EPS"],
unit: ""}); unit: ""
test('DBC parses steering control message', () => {
const dbcParsed = new DBC(DBC_MESSAGE_DEF);
const {signals} = dbcParsed.messages.get(228);
expect(Object.keys(signals).length).toBe(6);
expect(signals['STEER_TORQUE'].equals(steerTorqueSignal)).toBe(true);
}); });
test('DBC parses signal comment', () => { test("DBC parses steering control message", () => {
const dbcParsed = new DBC(DBC_SIGNAL_WITH_COMMENT); const dbcParsed = new DBC(DBC_MESSAGE_DEF);
const {signals} = dbcParsed.messages.get(228); const { signals } = dbcParsed.messages.get(228);
expect(signals.STEER_TORQUE.comment).toEqual("steer torque is the amount of torque in Nm applied"); expect(Object.keys(signals).length).toBe(6);
expect(signals["STEER_TORQUE"].equals(steerTorqueSignal)).toBe(true);
}); });
test('DBC parses multi-line signal comment', () => { test("DBC parses signal comment", () => {
const dbcParsed = new DBC(DBC_SIGNAL_WITH_MULTI_LINE_COMMENT); const dbcParsed = new DBC(DBC_SIGNAL_WITH_COMMENT);
const {signals} = dbcParsed.messages.get(228); const { signals } = dbcParsed.messages.get(228);
expect(signals.STEER_TORQUE.comment).toEqual("steer torque is the\namount of torque in Nm applied"); expect(signals.STEER_TORQUE.comment).toEqual(
"steer torque is the amount of torque in Nm applied"
);
}); });
test('DBC parses message comment', () => { test("DBC parses multi-line signal comment", () => {
const dbcParsed = new DBC(DBC_MESSAGE_WITH_COMMENT); const dbcParsed = new DBC(DBC_SIGNAL_WITH_MULTI_LINE_COMMENT);
const msg = dbcParsed.messages.get(228); const { signals } = dbcParsed.messages.get(228);
expect(msg.comment).toEqual("this message contains steer torque information"); expect(signals.STEER_TORQUE.comment).toEqual(
"steer torque is the\namount of torque in Nm applied"
);
}); });
test('DBC parses multi-line message comment', () => { test("DBC parses message comment", () => {
const dbcParsed = new DBC(DBC_MESSAGE_WITH_MULTI_LINE_COMMENT); const dbcParsed = new DBC(DBC_MESSAGE_WITH_COMMENT);
const msg = dbcParsed.messages.get(228); const msg = dbcParsed.messages.get(228);
expect(msg.comment).toEqual("this message contains\nsteer torque information"); expect(msg.comment).toEqual("this message contains steer torque information");
}); });
test('DBC parses board unit names', () => { test("DBC parses multi-line message comment", () => {
const dbcParsed = new DBC(DBC_BOARD_UNITS); const dbcParsed = new DBC(DBC_MESSAGE_WITH_MULTI_LINE_COMMENT);
expect(dbcParsed.boardUnits[0].name).toEqual("first_board_unit"); const msg = dbcParsed.messages.get(228);
expect(dbcParsed.boardUnits[1].name).toEqual("second_board_unit");
expect(msg.comment).toEqual(
"this message contains\nsteer torque information"
);
}); });
test('DBC parses board unit comments', () => { test("DBC parses board unit names", () => {
const dbcParsed = new DBC(DBC_BOARD_UNITS_WITH_COMMENT); const dbcParsed = new DBC(DBC_BOARD_UNITS);
expect(dbcParsed.boardUnits[0].comment).toEqual("first board unit comment"); expect(dbcParsed.boardUnits[0].name).toEqual("first_board_unit");
expect(dbcParsed.boardUnits[1].name).toEqual("second_board_unit");
}); });
test('DBC parses multi-line board unit comments', () => { test("DBC parses board unit comments", () => {
const dbcParsed = new DBC(DBC_BOARD_UNITS_WITH_COMMENT_LINES); const dbcParsed = new DBC(DBC_BOARD_UNITS_WITH_COMMENT);
expect(dbcParsed.boardUnits[0].comment).toEqual("first board unit\ncomment"); expect(dbcParsed.boardUnits[0].comment).toEqual("first board unit comment");
}); });
test('DBC parses signal value descriptions', () => { test("DBC parses multi-line board unit comments", () => {
const dbcParsed = new DBC(DBC_SIGNALS_WITH_VAL); const dbcParsed = new DBC(DBC_BOARD_UNITS_WITH_COMMENT_LINES);
const {signals} = dbcParsed.messages.get(228); expect(dbcParsed.boardUnits[0].comment).toEqual("first board unit\ncomment");
const expectedTorqueRequestVals = new Map([['1', 'requesting torque'],
['0', 'not requesting torque']]);
expect(signals.STEER_TORQUE_REQUEST.valueDescriptions).toEqual(expectedTorqueRequestVals);
}); });
test('DBC parses value tables', () => { test("DBC parses signal value descriptions", () => {
const dbcParsed = new DBC(DBC_VALUE_TABLE); const dbcParsed = new DBC(DBC_SIGNALS_WITH_VAL);
const stateTableEntries = [['4', "DI_STATE_ENABLE"], const { signals } = dbcParsed.messages.get(228);
['3', "DI_STATE_FAULT"],
['2', "DI_STATE_CLEAR_FAULT"],
['1', "DI_STATE_STANDBY"],
['0', "DI_STATE_PREAUTH"]]
const stateTable = new Map(stateTableEntries);
const speedUnitsEntries = [['1',"DI_SPEED_KPH"],
['0',"DI_SPEED_MPH"]]
const speedUnitsTable = new Map(speedUnitsEntries);
const valueTableEntries = Array.from(dbcParsed.valueTables.entries()); const expectedTorqueRequestVals = new Map([
expect(valueTableEntries[0]).toEqual(['DI_state', stateTable]); ["1", "requesting torque"],
expect(valueTableEntries[1]).toEqual(['DI_speedUnits', speedUnitsTable]); ["0", "not requesting torque"]
]);
expect(signals.STEER_TORQUE_REQUEST.valueDescriptions).toEqual(
expectedTorqueRequestVals
);
}); });
test('swapOrder properly converts little endian to big endian', () => { test("DBC parses value tables", () => {
const littleEndianHex = 'e2d62a0bd0d3b5e5'; const dbcParsed = new DBC(DBC_VALUE_TABLE);
const bigEndianHex = 'e5b5d3d00b2ad6e2'; const stateTableEntries = [
["4", "DI_STATE_ENABLE"],
["3", "DI_STATE_FAULT"],
["2", "DI_STATE_CLEAR_FAULT"],
["1", "DI_STATE_STANDBY"],
["0", "DI_STATE_PREAUTH"]
];
const stateTable = new Map(stateTableEntries);
const speedUnitsEntries = [["1", "DI_SPEED_KPH"], ["0", "DI_SPEED_MPH"]];
const speedUnitsTable = new Map(speedUnitsEntries);
const littleEndianHexSwapped = swapOrder(littleEndianHex, 16, 2); const valueTableEntries = Array.from(dbcParsed.valueTables.entries());
expect(valueTableEntries[0]).toEqual(["DI_state", stateTable]);
expect(littleEndianHexSwapped == bigEndianHex).toBe(true); expect(valueTableEntries[1]).toEqual(["DI_speedUnits", speedUnitsTable]);
}); });
test('int32 parser produces correct value for steer torque signal', () => { test("swapOrder properly converts little endian to big endian", () => {
const dbc = new DBC(DBC_MESSAGE_DEF); const littleEndianHex = "e2d62a0bd0d3b5e5";
const bigEndianHex = "e5b5d3d00b2ad6e2";
const hex = 'e2d62a0bd0d3b5e5'; const littleEndianHexSwapped = swapOrder(littleEndianHex, 16, 2);
const buffer = Buffer.from(hex, 'hex');
const bufferSwapped = Buffer.from(buffer).swap64();
const bitArr = Bitarray.fromBytes(buffer); expect(littleEndianHexSwapped == bigEndianHex).toBe(true);
const bitsSwapped = Bitarray.fromBytes(bufferSwapped);
const value = dbc.valueForInt32Signal(steerTorqueSignal, bitArr, bitsSwapped);
expect(value).toBe(-7466);
}); });
test('int64 parser produces correct value for steer torque signal', () => { test("int32 parser produces correct value for steer torque signal", () => {
const dbc = new DBC(DBC_MESSAGE_DEF); const dbc = new DBC(DBC_MESSAGE_DEF);
const hex = 'e2d62a0bd0d3b5e5'; const hex = "e2d62a0bd0d3b5e5";
const value = dbc.valueForInt64Signal(steerTorqueSignal, hex); const buffer = Buffer.from(hex, "hex");
const bufferSwapped = Buffer.from(buffer).swap64();
expect(value).toBe(-7466); const bitArr = Bitarray.fromBytes(buffer);
const bitsSwapped = Bitarray.fromBytes(bufferSwapped);
const value = dbc.valueForInt32Signal(steerTorqueSignal, bitArr, bitsSwapped);
expect(value).toBe(-7466);
});
test("int64 parser produces correct value for steer torque signal", () => {
const dbc = new DBC(DBC_MESSAGE_DEF);
const hex = "e2d62a0bd0d3b5e5";
const value = dbc.valueForInt64Signal(steerTorqueSignal, hex);
expect(value).toBe(-7466);
}); });
function dbcInt32SignalValue(dbc, signalSpec, hex) { function dbcInt32SignalValue(dbc, signalSpec, hex) {
const buffer = Buffer.from(hex, 'hex'); const buffer = Buffer.from(hex, "hex");
const bufferSwapped = Buffer.from(buffer).swap64(); const bufferSwapped = Buffer.from(buffer).swap64();
const bits = Bitarray.fromBytes(buffer); const bits = Bitarray.fromBytes(buffer);
const bitsSwapped = Bitarray.fromBytes(bufferSwapped); const bitsSwapped = Bitarray.fromBytes(bufferSwapped);
return dbc.valueForInt32Signal(signalSpec, bits, bitsSwapped); return dbc.valueForInt32Signal(signalSpec, bits, bitsSwapped);
} }
const DBC_BINARY_LE_SIGNAL = ` const DBC_BINARY_LE_SIGNAL = `
@ -274,56 +286,53 @@ BO_ 768 NEW_MSG_1: 8 XXX
SG_ NEW_SIGNAL_1 : 37|1@1+ (1,0) [0|1] "" XXX SG_ NEW_SIGNAL_1 : 37|1@1+ (1,0) [0|1] "" XXX
`; `;
test('int32 parsers produces correct value for binary little endian signal', () => { test("int32 parsers produces correct value for binary little endian signal", () => {
const dbc = new DBC(DBC_BINARY_LE_SIGNAL) const dbc = new DBC(DBC_BINARY_LE_SIGNAL);
const signalSpec = dbc.messages.get(768).signals['NEW_SIGNAL_1']; const signalSpec = dbc.messages.get(768).signals["NEW_SIGNAL_1"];
const hexDataSet = '0000000020000000'; const hexDataSet = "0000000020000000";
const hexDataNotSet = '0000000000000000'; const hexDataNotSet = "0000000000000000";
const setValue = dbcInt32SignalValue(dbc, signalSpec, hexDataSet);
const notSetValue = dbcInt32SignalValue(dbc, signalSpec, hexDataNotSet);
const setValue = dbcInt32SignalValue(dbc, signalSpec, hexDataSet); expect(setValue).toEqual(1);
const notSetValue = dbcInt32SignalValue(dbc, signalSpec, hexDataNotSet); expect(notSetValue).toEqual(0);
expect(setValue).toEqual(1);
expect(notSetValue).toEqual(0);
}); });
const DBC_TWO_BIT_LE_SIGNAL = ` const DBC_TWO_BIT_LE_SIGNAL = `
BO_ 768 NEW_MSG_1: 8 XXX BO_ 768 NEW_MSG_1: 8 XXX
SG_ NEW_SIGNAL_1 : 35|2@1+ (1,0) [0|3] "" XXX SG_ NEW_SIGNAL_1 : 35|2@1+ (1,0) [0|3] "" XXX
`; `;
test('int32 parser produces correct value for 2-bit little endian signal spanning words', () => { test("int32 parser produces correct value for 2-bit little endian signal spanning words", () => {
const dbc = new DBC(DBC_TWO_BIT_LE_SIGNAL); const dbc = new DBC(DBC_TWO_BIT_LE_SIGNAL);
const signalSpec = dbc.messages.get(768).signals['NEW_SIGNAL_1']; const signalSpec = dbc.messages.get(768).signals["NEW_SIGNAL_1"];
const hexData = '00000001f8000000'; const hexData = "00000001f8000000";
const value = dbcInt32SignalValue(dbc, signalSpec, hexData); const value = dbcInt32SignalValue(dbc, signalSpec, hexData);
expect(value).toEqual(3); expect(value).toEqual(3);
}); });
const DBC_FOUR_BIT_LE_SIGNAL = ` const DBC_FOUR_BIT_LE_SIGNAL = `
BO_ 768 NEW_MSG_1: 8 XXX BO_ 768 NEW_MSG_1: 8 XXX
SG_ NEW_SIGNAL_1 : 6|4@1+ (1,0) [0|15] "" XXX SG_ NEW_SIGNAL_1 : 6|4@1+ (1,0) [0|15] "" XXX
`; `;
test('int32 parser produces correct value for 4-bit little endian signal', () => { test("int32 parser produces correct value for 4-bit little endian signal", () => {
const dbc = new DBC(DBC_FOUR_BIT_LE_SIGNAL); const dbc = new DBC(DBC_FOUR_BIT_LE_SIGNAL);
const signalSpec = dbc.messages.get(768).signals['NEW_SIGNAL_1']; const signalSpec = dbc.messages.get(768).signals["NEW_SIGNAL_1"];
// this data is symmetric, the data bits are 1111 // this data is symmetric, the data bits are 1111
const hexDataSymmetric = 'f00f000000000000'; const hexDataSymmetric = "f00f000000000000";
const symValue = dbcInt32SignalValue(dbc, signalSpec, hexDataSymmetric); const symValue = dbcInt32SignalValue(dbc, signalSpec, hexDataSymmetric);
expect(symValue).toEqual(15); expect(symValue).toEqual(15);
// this data is asymmetric, the data bits are 1101 // this data is asymmetric, the data bits are 1101
const hexDataAsymmetric = 'f002000000000000'; const hexDataAsymmetric = "f002000000000000";
const aSymValue = dbcInt32SignalValue(dbc, signalSpec, hexDataAsymmetric); const aSymValue = dbcInt32SignalValue(dbc, signalSpec, hexDataAsymmetric);
expect(aSymValue).toEqual(11); expect(aSymValue).toEqual(11);
}); });
const DBC_CHFFR_METRIC_COMMENT = ` const DBC_CHFFR_METRIC_COMMENT = `
BO_ 37 STEERING_CONTROL: 8 XXX BO_ 37 STEERING_CONTROL: 8 XXX
SG_ STEER_ANGLE : 6|4@1+ (1,0) [0|15] "" XXX SG_ STEER_ANGLE : 6|4@1+ (1,0) [0|15] "" XXX
@ -331,10 +340,12 @@ BO_ 37 STEERING_CONTROL: 8 XXX
CM_ "CHFFR_METRIC 37 STEER_ANGLE STEER_ANGLE 0.36 180"; CM_ "CHFFR_METRIC 37 STEER_ANGLE STEER_ANGLE 0.36 180";
`; `;
test('dbc parser parses top-level comment with chffr metric', () => { test("dbc parser parses top-level comment with chffr metric", () => {
const dbc = new DBC(DBC_CHFFR_METRIC_COMMENT); const dbc = new DBC(DBC_CHFFR_METRIC_COMMENT);
const { comments } = dbc; const { comments } = dbc;
expect(comments.length).toEqual(1); expect(comments.length).toEqual(1);
expect(comments[0]).toEqual("CHFFR_METRIC 37 STEER_ANGLE STEER_ANGLE 0.36 180"); expect(comments[0]).toEqual(
}) "CHFFR_METRIC 37 STEER_ANGLE STEER_ANGLE 0.36 180"
);
});

View File

@ -1,17 +1,17 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import DBC, {swapOrder} from '../../models/can/dbc'; import DBC, { swapOrder } from "../../models/can/dbc";
import { ACURA_DBC } from '../res/acura-dbc'; import { ACURA_DBC } from "../res/acura-dbc";
import { CRV_DBC } from '../res/crv-dbc'; import { CRV_DBC } from "../res/crv-dbc";
import { TESLA_DBC } from '../res/tesla-dbc'; import { TESLA_DBC } from "../res/tesla-dbc";
test('DBC.text() for acura DBC should be equivalent to its original text', () => { test("DBC.text() for acura DBC should be equivalent to its original text", () => {
const dbc = new DBC(ACURA_DBC); const dbc = new DBC(ACURA_DBC);
expect(dbc.text()).toBe(ACURA_DBC); expect(dbc.text()).toBe(ACURA_DBC);
}); });
test('DBC.text() for crv DBC should be equivalent to its original text', () => { test("DBC.text() for crv DBC should be equivalent to its original text", () => {
const dbc = new DBC(CRV_DBC); const dbc = new DBC(CRV_DBC);
expect(dbc.text()).toBe(CRV_DBC); expect(dbc.text()).toBe(CRV_DBC);
}); });

View File

@ -1,9 +1,17 @@
import Entries from '../../models/can/entries'; import Entries from "../../models/can/entries";
test('segment index low is inclusive and index high is exclusive', () => { test("segment index low is inclusive and index high is exclusive", () => {
const entries = [{time: 1.0}, {time: 3.45}, {time: 3.65}, {time: 5.55}]; const entries = [
const [segmentIdxLow, segmentIdxHi] = Entries.findSegmentIndices(entries, [3.45, 5.55]); { time: 1.0 },
{ time: 3.45 },
{ time: 3.65 },
{ time: 5.55 }
];
const [segmentIdxLow, segmentIdxHi] = Entries.findSegmentIndices(entries, [
3.45,
5.55
]);
expect(segmentIdxLow).toBe(1); expect(segmentIdxLow).toBe(1);
expect(segmentIdxHi).toBe(entries.length - 1); expect(segmentIdxHi).toBe(entries.length - 1);
}) });

View File

@ -1,8 +1,13 @@
import Frame from '../../models/can/frame'; import Frame from "../../models/can/frame";
const FRAME_HEADER = "BO_ 255 SOME_FRAME: 5 ADAS"; const FRAME_HEADER = "BO_ 255 SOME_FRAME: 5 ADAS";
test('Frame.header() returns spec compliant representation', () => { test("Frame.header() returns spec compliant representation", () => {
const frame = new Frame({name: 'SOME_FRAME', id: 255, size: 5, transmitters: ['ADAS']}); const frame = new Frame({
expect(frame.header()).toEqual(FRAME_HEADER); name: "SOME_FRAME",
id: 255,
size: 5,
transmitters: ["ADAS"]
});
expect(frame.header()).toEqual(FRAME_HEADER);
}); });

View File

@ -1,77 +1,104 @@
import Signal from '../../models/can/signal'; import Signal from "../../models/can/signal";
const someSignalParams = { const someSignalParams = {
name: 'STEER_TORQUE', name: "STEER_TORQUE",
startBit: 7, startBit: 7,
size: 16, size: 16,
isLittleEndian: false, isLittleEndian: false,
isSigned: true, isSigned: true,
factor: 1, factor: 1,
offset: 0, offset: 0,
min: -3840, min: -3840,
max: 3840, max: 3840,
unit: ""}; unit: ""
};
const someOtherSignalParams = { const someOtherSignalParams = {
name: 'DIFFERENT_NAME', name: "DIFFERENT_NAME",
startBit: 0, startBit: 0,
size: 16,
isLittleEndian: false,
isSigned: true,
factor: 1,
offset: 0,
min: -3840,
max: 3840,
unit: ""
};
test("Signal.equals returns true for signals with identical properties", () => {
const someSignal = new Signal(someSignalParams);
const someEquivalentSignal = new Signal(someSignalParams);
expect(someSignal.equals(someEquivalentSignal)).toBe(true);
});
test("Signal.equals returns false for signals with different properties", () => {
const someSignal = new Signal(someSignalParams);
const differentSignal = new Signal(someOtherSignalParams);
expect(someSignal.equals(differentSignal)).toBe(false);
});
test("Signal.bitDescription returns proper description for a little endian signal", () => {
const littleEndianSignal = new Signal({
name: "little endian signal",
startBit: 20,
size: 4,
isLittleEndian: true
});
expect(littleEndianSignal.bitDescription(20).bitNumber).toBe(0);
expect(littleEndianSignal.bitDescription(21).bitNumber).toBe(1);
expect(littleEndianSignal.bitDescription(22).bitNumber).toBe(2);
expect(littleEndianSignal.bitDescription(23).bitNumber).toBe(3);
});
test("Signal.bitDescription returns proper description for a big endian signal", () => {
const bigEndianSignal = new Signal({
name: "big endian signal",
startBit: 7,
size: 16, size: 16,
isLittleEndian: false, isLittleEndian: false
isSigned: true, });
factor: 1,
offset: 0,
min: -3840,
max: 3840,
unit: ""};
test('Signal.equals returns true for signals with identical properties', () => { const bitFifteenDescription = {
const someSignal = new Signal(someSignalParams); bitNumber: 8,
const someEquivalentSignal = new Signal(someSignalParams); isLsb: false,
expect(someSignal.equals(someEquivalentSignal)).toBe(true); isMsb: false,
range: [0, 15]
};
const bitZeroDescription = {
bitNumber: 7,
isLsb: false,
isMsb: false,
range: [0, 15]
};
const bitEightDescription = {
bitNumber: 15,
isLsb: true,
isMsb: false,
range: [0, 15]
};
const bitSevenDescription = {
bitNumber: 0,
isLsb: false,
isMsb: true,
range: [0, 15]
};
expect(bigEndianSignal.bitDescription(15)).toEqual(bitFifteenDescription);
expect(bigEndianSignal.bitDescription(0)).toEqual(bitZeroDescription);
expect(bigEndianSignal.bitDescription(8)).toEqual(bitEightDescription);
expect(bigEndianSignal.bitDescription(7)).toEqual(bitSevenDescription);
}); });
test('Signal.equals returns false for signals with different properties', () => { test("Signal.bitDescription returns null for bit index that is not in its range", () => {
const someSignal = new Signal(someSignalParams); const someSignal = new Signal({
const differentSignal = new Signal(someOtherSignalParams); name: "some signal",
expect(someSignal.equals(differentSignal)).toBe(false); startBit: 20,
}); size: 4,
isLittleEndian: false
test('Signal.bitDescription returns proper description for a little endian signal', () => { });
const littleEndianSignal = new Signal({name: 'little endian signal',
startBit: 20, expect(someSignal.bitDescription(21)).toBe(null);
size: 4, expect(someSignal.bitDescription(16)).toBe(null);
isLittleEndian: true});
expect(littleEndianSignal.bitDescription(20).bitNumber).toBe(0);
expect(littleEndianSignal.bitDescription(21).bitNumber).toBe(1);
expect(littleEndianSignal.bitDescription(22).bitNumber).toBe(2);
expect(littleEndianSignal.bitDescription(23).bitNumber).toBe(3);
});
test('Signal.bitDescription returns proper description for a big endian signal', () => {
const bigEndianSignal = new Signal({name: 'big endian signal',
startBit: 7,
size: 16,
isLittleEndian: false});
const bitFifteenDescription = {bitNumber: 8, isLsb: false, isMsb: false, range: [0,15]};
const bitZeroDescription = {bitNumber: 7, isLsb: false, isMsb: false, range: [0,15]};
const bitEightDescription = {bitNumber: 15, isLsb: true, isMsb: false, range: [0,15]};
const bitSevenDescription = {bitNumber: 0, isLsb: false, isMsb: true, range: [0,15]};
expect(bigEndianSignal.bitDescription(15)).toEqual(bitFifteenDescription);
expect(bigEndianSignal.bitDescription(0)).toEqual(bitZeroDescription);
expect(bigEndianSignal.bitDescription(8)).toEqual(bitEightDescription);
expect(bigEndianSignal.bitDescription(7)).toEqual(bitSevenDescription);
});
test('Signal.bitDescription returns null for bit index that is not in its range', () => {
const someSignal = new Signal({name: 'some signal',
startBit: 20,
size: 4,
isLittleEndian: false});
expect(someSignal.bitDescription(21)).toBe(null);
expect(someSignal.bitDescription(16)).toBe(null);
}); });

View File

@ -5,10 +5,10 @@ note: 'right' and 'left' in test descriptions
refer to the sides of the bit matrix refer to the sides of the bit matrix
as displayed to the user. as displayed to the user.
*/ */
import AddSignals from '../../components/AddSignals'; import AddSignals from "../../components/AddSignals";
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
import {StyleSheetTestUtils} from 'aphrodite'; import { StyleSheetTestUtils } from "aphrodite";
// Prevents style injection from firing after test finishes // Prevents style injection from firing after test finishes
// and jsdom is torn down. // and jsdom is torn down.
@ -22,286 +22,332 @@ afterEach(() => {
// signal creation // signal creation
function createAddSignals(signals) { function createAddSignals(signals) {
if(signals === undefined) { if (signals === undefined) {
signals = {}; signals = {};
} }
const message = {signals, const message = {
address: 0, signals,
entries: [ address: 0,
{relTime: 0, entries: [
hexData: '0000000000000000'} {
]}; relTime: 0,
hexData: "0000000000000000"
}
]
};
const component = shallow(<AddSignals const component = shallow(
message={message} <AddSignals
messageIndex={0} message={message}
onConfirmedSignalChange={() => {}} />); messageIndex={0}
onConfirmedSignalChange={() => {}}
/>
);
return component; return component;
} }
test('double clicking adds a signal', () => { test("double clicking adds a signal", () => {
const component = createAddSignals(); const component = createAddSignals();
const firstBit = component.find('.bit').first(); const firstBit = component.find(".bit").first();
firstBit.simulate('dblclick'); firstBit.simulate("dblclick");
const newSignal = Object.values(component.state('signals'))[0]; const newSignal = Object.values(component.state("signals"))[0];
expect(newSignal).toBeDefined(); expect(newSignal).toBeDefined();
expect(newSignal.size).toBe(1); expect(newSignal.size).toBe(1);
}); });
test('dragging right to left across a byte creates a little endian signal', () => { test("dragging right to left across a byte creates a little endian signal", () => {
const component = createAddSignals(); const component = createAddSignals();
const leftBitInByte = component.find('.bit').first(); const leftBitInByte = component.find(".bit").first();
const rightBitInByte = component.find('.bit').at(7); const rightBitInByte = component.find(".bit").at(7);
rightBitInByte.simulate('mousedown'); rightBitInByte.simulate("mousedown");
leftBitInByte.simulate('mouseup'); leftBitInByte.simulate("mouseup");
const newSignal = Object.values(component.state('signals'))[0]; const newSignal = Object.values(component.state("signals"))[0];
expect(newSignal).toBeDefined(); expect(newSignal).toBeDefined();
expect(newSignal.size).toBe(8); expect(newSignal.size).toBe(8);
expect(newSignal.isLittleEndian).toBe(true); expect(newSignal.isLittleEndian).toBe(true);
expect(newSignal.startBit).toBe(0); expect(newSignal.startBit).toBe(0);
}); });
test('dragging left to right across a byte creates a big endian signal', () => { test("dragging left to right across a byte creates a big endian signal", () => {
const component = createAddSignals(); const component = createAddSignals();
const leftBitInByte = component.find('.bit').first(); const leftBitInByte = component.find(".bit").first();
const rightBitInByte = component.find('.bit').at(7); const rightBitInByte = component.find(".bit").at(7);
leftBitInByte.simulate('mousedown'); leftBitInByte.simulate("mousedown");
rightBitInByte.simulate('mouseup'); rightBitInByte.simulate("mouseup");
const newSignal = Object.values(component.state('signals'))[0]; const newSignal = Object.values(component.state("signals"))[0];
expect(newSignal).toBeDefined(); expect(newSignal).toBeDefined();
expect(newSignal.size).toBe(8); expect(newSignal.size).toBe(8);
expect(newSignal.isLittleEndian).toBe(false); expect(newSignal.isLittleEndian).toBe(false);
expect(newSignal.startBit).toBe(7); expect(newSignal.startBit).toBe(7);
}); });
test('dragging from the left of byte 0 to right of byte 1 creates a big endian signal spanning both bytes', () => { test("dragging from the left of byte 0 to right of byte 1 creates a big endian signal spanning both bytes", () => {
const component = createAddSignals(); const component = createAddSignals();
const leftBitInByte = component.find('.bit').first(); const leftBitInByte = component.find(".bit").first();
const rightBitInByte = component.find('.bit').at(15); const rightBitInByte = component.find(".bit").at(15);
leftBitInByte.simulate('mousedown'); leftBitInByte.simulate("mousedown");
rightBitInByte.simulate('mouseup'); rightBitInByte.simulate("mouseup");
const newSignal = Object.values(component.state('signals'))[0]; const newSignal = Object.values(component.state("signals"))[0];
expect(newSignal).toBeDefined(); expect(newSignal).toBeDefined();
expect(newSignal.size).toBe(16); expect(newSignal.size).toBe(16);
expect(newSignal.isLittleEndian).toBe(false); expect(newSignal.isLittleEndian).toBe(false);
expect(newSignal.startBit).toBe(7); expect(newSignal.startBit).toBe(7);
}); });
test('dragging from the right of byte 0 to the left of byte 1 creates a little endian signal spanning both bytes', () => { test("dragging from the right of byte 0 to the left of byte 1 creates a little endian signal spanning both bytes", () => {
const component = createAddSignals(); const component = createAddSignals();
const leftBitInByteOne = component.find('.bit').at(8); // left of byte 1 const leftBitInByteOne = component.find(".bit").at(8); // left of byte 1
const rightBitInByteZero = component.find('.bit').at(7); // right of byte 0 const rightBitInByteZero = component.find(".bit").at(7); // right of byte 0
rightBitInByteZero.simulate('mousedown'); rightBitInByteZero.simulate("mousedown");
leftBitInByteOne.simulate('mouseup'); leftBitInByteOne.simulate("mouseup");
const newSignal = Object.values(component.state('signals'))[0]; const newSignal = Object.values(component.state("signals"))[0];
expect(newSignal).toBeDefined(); expect(newSignal).toBeDefined();
expect(newSignal.size).toBe(16); expect(newSignal.size).toBe(16);
expect(newSignal.isLittleEndian).toBe(true); expect(newSignal.isLittleEndian).toBe(true);
expect(newSignal.startBit).toBe(0); expect(newSignal.startBit).toBe(0);
}); });
test('dragging from the left of byte 1 to the right of byte 0 creates a little endian signal spanning both bytes', () => { test("dragging from the left of byte 1 to the right of byte 0 creates a little endian signal spanning both bytes", () => {
const component = createAddSignals(); const component = createAddSignals();
const leftBitInByteOne = component.find('.bit').at(8); const leftBitInByteOne = component.find(".bit").at(8);
const rightBitInByteZero = component.find('.bit').at(7); const rightBitInByteZero = component.find(".bit").at(7);
leftBitInByteOne.simulate('mousedown'); leftBitInByteOne.simulate("mousedown");
rightBitInByteZero.simulate('mouseup'); rightBitInByteZero.simulate("mouseup");
const signal = Object.values(component.state('signals'))[0]; const signal = Object.values(component.state("signals"))[0];
expect(signal).toBeDefined(); expect(signal).toBeDefined();
expect(signal.size).toBe(16); expect(signal.size).toBe(16);
expect(signal.isLittleEndian).toBe(true); expect(signal.isLittleEndian).toBe(true);
expect(signal.startBit).toBe(0); expect(signal.startBit).toBe(0);
}); });
test('dragging from the right of byte 1 to the left of byte 0 creates a big endian signal spanning both bytes', () => { test("dragging from the right of byte 1 to the left of byte 0 creates a big endian signal spanning both bytes", () => {
const component = createAddSignals(); const component = createAddSignals();
const leftBitInByteZero = component.find('.bit').at(0); const leftBitInByteZero = component.find(".bit").at(0);
const rightBitInByteOne = component.find('.bit').at(15); const rightBitInByteOne = component.find(".bit").at(15);
rightBitInByteOne.simulate('mousedown'); rightBitInByteOne.simulate("mousedown");
leftBitInByteZero.simulate('mouseup'); leftBitInByteZero.simulate("mouseup");
const signal = Object.values(component.state('signals'))[0]; const signal = Object.values(component.state("signals"))[0];
expect(signal).toBeDefined(); expect(signal).toBeDefined();
expect(signal.size).toBe(16); expect(signal.size).toBe(16);
expect(signal.isLittleEndian).toBe(false); expect(signal.isLittleEndian).toBe(false);
expect(signal.startBit).toBe(7); expect(signal.startBit).toBe(7);
}); });
// signal mutation // signal mutation
test('dragging a one-bit big-endian signal to the right should extend it to the right of the byte', () => { test("dragging a one-bit big-endian signal to the right should extend it to the right of the byte", () => {
const component = createAddSignals(); const component = createAddSignals();
component.instance().createSignal({startBit: 7, size: 1, isLittleEndian: false}); component
.instance()
.createSignal({ startBit: 7, size: 1, isLittleEndian: false });
const signalBit = component.find('.bit').at(0); const signalBit = component.find(".bit").at(0);
signalBit.simulate('mousedown'); signalBit.simulate("mousedown");
for(let i = 1; i < 8; i++) { for (let i = 1; i < 8; i++) {
component.find('.bit').at(i).simulate('mouseenter'); component
} .find(".bit")
const bitAtRightOfFirstByte = component.find('.bit').at(7); .at(i)
bitAtRightOfFirstByte.simulate('mouseup'); .simulate("mouseenter");
}
const bitAtRightOfFirstByte = component.find(".bit").at(7);
bitAtRightOfFirstByte.simulate("mouseup");
const signal = Object.values(component.state('signals'))[0]; const signal = Object.values(component.state("signals"))[0];
expect(signal).toBeDefined(); expect(signal).toBeDefined();
expect(signal.size).toBe(8); expect(signal.size).toBe(8);
expect(signal.isLittleEndian).toBe(false); expect(signal.isLittleEndian).toBe(false);
expect(signal.startBit).toBe(7); expect(signal.startBit).toBe(7);
}); });
test('dragging a one-bit little-endian signal to the right should extend it to the right of the byte', () => { test("dragging a one-bit little-endian signal to the right should extend it to the right of the byte", () => {
const component = createAddSignals(); const component = createAddSignals();
component.instance().createSignal({startBit: 7, size: 1, isLittleEndian: true}); component
.instance()
.createSignal({ startBit: 7, size: 1, isLittleEndian: true });
const signalBit = component.find('.bit').at(0); const signalBit = component.find(".bit").at(0);
signalBit.simulate('mousedown'); signalBit.simulate("mousedown");
for(let i = 1; i < 8; i++) { for (let i = 1; i < 8; i++) {
component.find('.bit').at(i).simulate('mouseenter'); component
} .find(".bit")
const bitAtRightOfFirstByte = component.find('.bit').at(7); .at(i)
bitAtRightOfFirstByte.simulate('mouseup'); .simulate("mouseenter");
}
const bitAtRightOfFirstByte = component.find(".bit").at(7);
bitAtRightOfFirstByte.simulate("mouseup");
const signal = Object.values(component.state('signals'))[0]; const signal = Object.values(component.state("signals"))[0];
expect(signal).toBeDefined(); expect(signal).toBeDefined();
expect(signal.size).toBe(8); expect(signal.size).toBe(8);
expect(signal.isLittleEndian).toBe(true); expect(signal.isLittleEndian).toBe(true);
expect(signal.startBit).toBe(0); expect(signal.startBit).toBe(0);
}); });
test('dragging a one-bit big-endian signal to the left should extend it to the left of the byte', () => { test("dragging a one-bit big-endian signal to the left should extend it to the left of the byte", () => {
const component = createAddSignals(); const component = createAddSignals();
component.instance().createSignal({startBit: 0, size: 1, isLittleEndian: false}); component
.instance()
.createSignal({ startBit: 0, size: 1, isLittleEndian: false });
const signalBit = component.find('.bit').at(7); const signalBit = component.find(".bit").at(7);
signalBit.simulate('mousedown'); signalBit.simulate("mousedown");
for(let i = 6; i > -1; i--) { for (let i = 6; i > -1; i--) {
component.find('.bit').at(i).simulate('mouseenter'); component
} .find(".bit")
const bitAtRightOfFirstByte = component.find('.bit').at(0); .at(i)
bitAtRightOfFirstByte.simulate('mouseup'); .simulate("mouseenter");
}
const bitAtRightOfFirstByte = component.find(".bit").at(0);
bitAtRightOfFirstByte.simulate("mouseup");
const signal = Object.values(component.state('signals'))[0]; const signal = Object.values(component.state("signals"))[0];
expect(signal).toBeDefined(); expect(signal).toBeDefined();
expect(signal.size).toBe(8); expect(signal.size).toBe(8);
expect(signal.isLittleEndian).toBe(false); expect(signal.isLittleEndian).toBe(false);
expect(signal.startBit).toBe(7); expect(signal.startBit).toBe(7);
}); });
test('extending a two-bit big-endian signal by its LSB should extend it to the right of the byte', () => { test("extending a two-bit big-endian signal by its LSB should extend it to the right of the byte", () => {
const component = createAddSignals(); const component = createAddSignals();
component.instance().createSignal({startBit: 7, size: 2, isLittleEndian: false}); component
.instance()
.createSignal({ startBit: 7, size: 2, isLittleEndian: false });
const lsb = component.find('.bit').at(1); const lsb = component.find(".bit").at(1);
lsb.simulate('mousedown'); lsb.simulate("mousedown");
for(let i = 0; i < 8; i++) { for (let i = 0; i < 8; i++) {
component.find('.bit').at(i).simulate('mouseenter'); component
} .find(".bit")
const bitAtRightOfFirstByte = component.find('.bit').at(7); .at(i)
bitAtRightOfFirstByte.simulate('mouseup'); .simulate("mouseenter");
}
const bitAtRightOfFirstByte = component.find(".bit").at(7);
bitAtRightOfFirstByte.simulate("mouseup");
const signal = Object.values(component.state('signals'))[0]; const signal = Object.values(component.state("signals"))[0];
expect(signal).toBeDefined(); expect(signal).toBeDefined();
expect(signal.size).toBe(8); expect(signal.size).toBe(8);
expect(signal.isLittleEndian).toBe(false); expect(signal.isLittleEndian).toBe(false);
expect(signal.startBit).toBe(7); expect(signal.startBit).toBe(7);
}); });
test('a two-bit little-endian signal should extend by its LSB to the end of the byte', () => { test("a two-bit little-endian signal should extend by its LSB to the end of the byte", () => {
const component = createAddSignals(); const component = createAddSignals();
component.instance().createSignal({startBit: 6, size: 2, isLittleEndian: true}); component
.instance()
.createSignal({ startBit: 6, size: 2, isLittleEndian: true });
const lsb = component.find('.bit').at(1); const lsb = component.find(".bit").at(1);
lsb.simulate('mousedown'); lsb.simulate("mousedown");
for(let i = 0; i < 8; i++) { for (let i = 0; i < 8; i++) {
component.find('.bit').at(i).simulate('mouseenter'); component
} .find(".bit")
const bitAtRightOfFirstByte = component.find('.bit').at(7); .at(i)
bitAtRightOfFirstByte.simulate('mouseup'); .simulate("mouseenter");
}
const bitAtRightOfFirstByte = component.find(".bit").at(7);
bitAtRightOfFirstByte.simulate("mouseup");
const signal = Object.values(component.state('signals'))[0]; const signal = Object.values(component.state("signals"))[0];
expect(signal).toBeDefined(); expect(signal).toBeDefined();
expect(signal.size).toBe(8); expect(signal.size).toBe(8);
expect(signal.isLittleEndian).toBe(true); expect(signal.isLittleEndian).toBe(true);
expect(signal.startBit).toBe(0); expect(signal.startBit).toBe(0);
}); });
test('dragging the lsb of a little-endian signal spanning an entire byte should not be allowed to pass the MSB', () => { test("dragging the lsb of a little-endian signal spanning an entire byte should not be allowed to pass the MSB", () => {
const component = createAddSignals(); const component = createAddSignals();
component.instance().createSignal({startBit: 0, size: 8, isLittleEndian: true}); component
.instance()
.createSignal({ startBit: 0, size: 8, isLittleEndian: true });
const lsb = component.find('.bit').at(7); const lsb = component.find(".bit").at(7);
lsb.simulate('mousedown'); lsb.simulate("mousedown");
const bitPastMsb = component.find('.bit').at(15); const bitPastMsb = component.find(".bit").at(15);
bitPastMsb.simulate('mouseenter'); bitPastMsb.simulate("mouseenter");
bitPastMsb.simulate('mouseup'); bitPastMsb.simulate("mouseup");
const signal = Object.values(component.state('signals'))[0]; const signal = Object.values(component.state("signals"))[0];
expect(signal).toBeDefined(); expect(signal).toBeDefined();
expect(signal.size).toBe(8); expect(signal.size).toBe(8);
expect(signal.isLittleEndian).toBe(true); expect(signal.isLittleEndian).toBe(true);
expect(signal.startBit).toBe(0); expect(signal.startBit).toBe(0);
}); });
test('dragging the lsb of a big-endian signal towards the msb in the same byte should contract the signal', () => { test("dragging the lsb of a big-endian signal towards the msb in the same byte should contract the signal", () => {
const component = createAddSignals(); const component = createAddSignals();
component.instance().createSignal({startBit: 7, size: 8, isLittleEndian: false}); component
.instance()
.createSignal({ startBit: 7, size: 8, isLittleEndian: false });
const lsb = component.find('.bit').at(7); const lsb = component.find(".bit").at(7);
lsb.simulate('mousedown'); lsb.simulate("mousedown");
for(let i = 6; i > 0; i--) { for (let i = 6; i > 0; i--) {
component.find('.bit').at(i).simulate('mouseenter'); component
} .find(".bit")
component.find('.bit').at(1).simulate('mouseup'); .at(i)
.simulate("mouseenter");
}
component
.find(".bit")
.at(1)
.simulate("mouseup");
const signal = Object.values(component.state('signals'))[0]; const signal = Object.values(component.state("signals"))[0];
expect(signal).toBeDefined(); expect(signal).toBeDefined();
expect(signal.size).toBe(2); expect(signal.size).toBe(2);
expect(signal.isLittleEndian).toBe(false); expect(signal.isLittleEndian).toBe(false);
expect(signal.startBit).toBe(7); expect(signal.startBit).toBe(7);
}); });
test('a big endian signal spanning one byte should switch to little endian preserving its bit coverage', () => { test("a big endian signal spanning one byte should switch to little endian preserving its bit coverage", () => {
const component = createAddSignals(); const component = createAddSignals();
component.instance().createSignal({startBit: 0, size: 8, isLittleEndian: true}); component
.instance()
.createSignal({ startBit: 0, size: 8, isLittleEndian: true });
const lsb = component.find('.bit').at(7); const lsb = component.find(".bit").at(7);
lsb.simulate('mousedown'); lsb.simulate("mousedown");
const bitPastMsb = component.find('.bit').at(15); const bitPastMsb = component.find(".bit").at(15);
bitPastMsb.simulate('mouseenter'); bitPastMsb.simulate("mouseenter");
bitPastMsb.simulate('mouseup'); bitPastMsb.simulate("mouseup");
const signal = Object.values(component.state('signals'))[0]; const signal = Object.values(component.state("signals"))[0];
expect(signal).toBeDefined(); expect(signal).toBeDefined();
expect(signal.size).toBe(8); expect(signal.size).toBe(8);
expect(signal.isLittleEndian).toBe(true); expect(signal.isLittleEndian).toBe(true);
expect(signal.startBit).toBe(0); expect(signal.startBit).toBe(0);
}); });
test('dragging the msb of a 2-bit little endian signal to a lower byte should not change the signal', () => { test("dragging the msb of a 2-bit little endian signal to a lower byte should not change the signal", () => {
const component = createAddSignals(); const component = createAddSignals();
component.instance().createSignal({startBit: 14, size: 2, isLittleEndian: true}); component
.instance()
.createSignal({ startBit: 14, size: 2, isLittleEndian: true });
const msb = component.find('.bit').at(8); const msb = component.find(".bit").at(8);
msb.simulate('mousedown'); msb.simulate("mousedown");
const bitOutOfBounds = component.find('.bit').at(0); const bitOutOfBounds = component.find(".bit").at(0);
bitOutOfBounds.simulate('mouseenter'); bitOutOfBounds.simulate("mouseenter");
bitOutOfBounds.simulate('mouseup'); bitOutOfBounds.simulate("mouseup");
const signal = Object.values(component.state('signals'))[0]; const signal = Object.values(component.state("signals"))[0];
expect(signal).toBeDefined(); expect(signal).toBeDefined();
expect(signal.size).toBe(2); expect(signal.size).toBe(2);
expect(signal.isLittleEndian).toBe(true); expect(signal.isLittleEndian).toBe(true);
expect(signal.startBit).toBe(14); expect(signal.startBit).toBe(14);
}); });

View File

@ -1,9 +1,9 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import CanExplorer from '../../CanExplorer'; import CanExplorer from "../../CanExplorer";
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
import {StyleSheetTestUtils} from 'aphrodite'; import { StyleSheetTestUtils } from "aphrodite";
test('CanExplorer renders', () => { test("CanExplorer renders", () => {
const canExplorer = shallow(<CanExplorer />); const canExplorer = shallow(<CanExplorer />);
}); });

View File

@ -1,29 +1,31 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import CanGraph from '../../components/CanGraph'; import CanGraph from "../../components/CanGraph";
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
test('CanGraph successfully mounts with minimal default props', () => { test("CanGraph successfully mounts with minimal default props", () => {
const component = shallow(<CanGraph const component = shallow(
onGraphRefAvailable={() => {}} <CanGraph
unplot={() => {}} onGraphRefAvailable={() => {}}
messages={{}} unplot={() => {}}
messageId={null} messages={{}}
messageName={null} messageId={null}
signalSpec={null} messageName={null}
onSegmentChanged={() => {}} signalSpec={null}
segment={[]} onSegmentChanged={() => {}}
data={[]} segment={[]}
onRelativeTimeClick={() => {}} data={[]}
currentTime={0} onRelativeTimeClick={() => {}}
onDragStart={() => {}} currentTime={0}
onDragEnd={() => {}} onDragStart={() => {}}
container={null} onDragEnd={() => {}}
dragPos={null} container={null}
canReceiveGraphDrop={false} dragPos={null}
plottedSignals={[]} canReceiveGraphDrop={false}
live={true} plottedSignals={[]}
/>); live={true}
expect(component.exists()).toBe(true); />
);
expect(component.exists()).toBe(true);
}); });

View File

@ -1,20 +1,23 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import CanGraphList from '../../components/CanGraphList'; import CanGraphList from "../../components/CanGraphList";
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
test('CanGraphList successfully mounts with minimal default props', () => { test("CanGraphList successfully mounts with minimal default props", () => {
const component = shallow(<CanGraphList const component = shallow(
plottedSignals={[]} <CanGraphList
messages={{}} plottedSignals={[]}
graphData={[]} messages={{}}
onGraphTimeClick={() => {}} graphData={[]}
seekTime={0} onGraphTimeClick={() => {}}
onSegmentChanged={() => {}} seekTime={0}
onSignalUnplotPressed={() => {}} onSegmentChanged={() => {}}
segment={[]} onSignalUnplotPressed={() => {}}
mergePlots={() => {}} segment={[]}
live={true} />); mergePlots={() => {}}
expect(component.exists()).toBe(true); live={true}
/>
);
expect(component.exists()).toBe(true);
}); });

View File

@ -1,18 +1,21 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import CanLog from '../../components/CanLog'; import CanLog from "../../components/CanLog";
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
test('CanLog successfully mounts with minimal default props', () => { test("CanLog successfully mounts with minimal default props", () => {
const component = shallow(<CanLog const component = shallow(
message={null} <CanLog
messageIndex={0} message={null}
segmentIndices={[]} messageIndex={0}
plottedSignals={[]} segmentIndices={[]}
onSignalPlotPressed={() => {}} plottedSignals={[]}
onSignalUnplotPressed={() => {}} onSignalPlotPressed={() => {}}
showAddSignal={() => {}} onSignalUnplotPressed={() => {}}
onMessageExpanded={() => {}} />); showAddSignal={() => {}}
expect(component.exists()).toBe(true); onMessageExpanded={() => {}}
/>
);
expect(component.exists()).toBe(true);
}); });

View File

@ -1,10 +1,10 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import DbcUpload from '../../components/DbcUpload'; import DbcUpload from "../../components/DbcUpload";
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
test('DbcUpload successfully mounts with minimal default props', () => { test("DbcUpload successfully mounts with minimal default props", () => {
const component = shallow(<DbcUpload />); const component = shallow(<DbcUpload />);
expect(component.exists()).toBe(true); expect(component.exists()).toBe(true);
}); });

View File

@ -1,20 +1,22 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import EditMessageModal from '../../components/EditMessageModal'; import EditMessageModal from "../../components/EditMessageModal";
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
import DbcUtils from '../../utils/dbc'; import DbcUtils from "../../utils/dbc";
import DBC from '../../models/can/dbc'; import DBC from "../../models/can/dbc";
test('EditMessageModal successfully mounts with minimal default props', () => { test("EditMessageModal successfully mounts with minimal default props", () => {
const dbc = new DBC(); const dbc = new DBC();
const frame = dbc.createFrame(0); const frame = dbc.createFrame(0);
const message = DbcUtils.createMessageSpec(dbc, 0, '0', 1); const message = DbcUtils.createMessageSpec(dbc, 0, "0", 1);
const component = shallow(<EditMessageModal const component = shallow(
handleClose={() => {}} <EditMessageModal
handleSave={() => {}} handleClose={() => {}}
message={message} handleSave={() => {}}
/>); message={message}
expect(component.exists()).toBe(true); />
);
expect(component.exists()).toBe(true);
}); });

View File

@ -1,30 +1,32 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import Explorer from '../../components/Explorer'; import Explorer from "../../components/Explorer";
import React from 'react'; import React from "react";
import Moment from 'moment'; import Moment from "moment";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
test('Explorer successfully mounts with minimal default props', () => { test("Explorer successfully mounts with minimal default props", () => {
const component = shallow(<Explorer const component = shallow(
url={null} <Explorer
live={true} url={null}
messages={{}} live={true}
selectedMessage={null} messages={{}}
onConfirmedSignalChange={() => {}} selectedMessage={null}
onSeek={() => {}} onConfirmedSignalChange={() => {}}
onUserSeek={() => {}} onSeek={() => {}}
canFrameOffset={0} onUserSeek={() => {}}
firstCanTime={0} canFrameOffset={0}
seekTime={0} firstCanTime={0}
seekIndex={0} seekTime={0}
currentParts={[0,0]} seekIndex={0}
partsLoaded={0} currentParts={[0, 0]}
autoplay={true} partsLoaded={0}
showEditMessageModal={() => {}} autoplay={true}
onPartChange={() => {}} showEditMessageModal={() => {}}
routeStartTime={Moment()} onPartChange={() => {}}
partsCount={0} routeStartTime={Moment()}
/>); partsCount={0}
expect(component.exists()).toBe(true); />
);
expect(component.exists()).toBe(true);
}); });

View File

@ -1,17 +1,20 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import GithubDbcList from '../../components/GithubDbcList'; import GithubDbcList from "../../components/GithubDbcList";
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
import OpenDbc from '../../api/OpenDbc'; import OpenDbc from "../../api/OpenDbc";
test('GithubDbcList successfully mounts with minimal default props', () => { test("GithubDbcList successfully mounts with minimal default props", () => {
const openDbcClient = new OpenDbc(null); const openDbcClient = new OpenDbc(null);
const component = shallow(<GithubDbcList const component = shallow(
onDbcLoaded={ () => {} } <GithubDbcList
repo="commaai/opendbc" onDbcLoaded={() => {}}
openDbcClient={ openDbcClient } />); repo="commaai/opendbc"
expect(component.exists()).toBe(true); openDbcClient={openDbcClient}
/>
);
expect(component.exists()).toBe(true);
}); });

View File

@ -1,23 +1,26 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import HLS from '../../components/HLS'; import HLS from "../../components/HLS";
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
test('HLS successfully mounts with minimal default props', () => { test("HLS successfully mounts with minimal default props", () => {
const component = shallow(<HLS const component = shallow(
source={'http://comma.ai'} <HLS
startTime={0} source={"http://comma.ai"}
playbackSpeed={1} startTime={0}
onVideoElementAvailable={() => {}} playbackSpeed={1}
playing={false} onVideoElementAvailable={() => {}}
onClick={() => {}} playing={false}
onLoadStart={() => {}} onClick={() => {}}
onLoadEnd={() => {}} onLoadStart={() => {}}
onUserSeek={() => {}} onLoadEnd={() => {}}
onPlaySeek={() => {}} onUserSeek={() => {}}
segmentProgress={() => {}} onPlaySeek={() => {}}
shouldRestart={false} segmentProgress={() => {}}
onRestart={() => {}} />); shouldRestart={false}
expect(component.exists()).toBe(true); onRestart={() => {}}
/>
);
expect(component.exists()).toBe(true);
}); });

View File

@ -1,18 +1,20 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
import LoadDbcModal from '../../components/LoadDbcModal'; import LoadDbcModal from "../../components/LoadDbcModal";
import OpenDbc from '../../api/OpenDbc'; import OpenDbc from "../../api/OpenDbc";
test('LoadDbcModal successfully mounts with minimal default props', () => { test("LoadDbcModal successfully mounts with minimal default props", () => {
const openDbcClient = new OpenDbc(null); const openDbcClient = new OpenDbc(null);
const component = shallow(<LoadDbcModal const component = shallow(
onDbcSelected={() => {}} <LoadDbcModal
handleClose={() => {}} onDbcSelected={() => {}}
openDbcClient={openDbcClient} handleClose={() => {}}
loginWithGithub={<p>Login with github</p>} openDbcClient={openDbcClient}
/>); loginWithGithub={<p>Login with github</p>}
expect(component.exists()).toBe(true); />
);
expect(component.exists()).toBe(true);
}); });

View File

@ -1,10 +1,10 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import LoadingBar from '../../components/LoadingBar'; import LoadingBar from "../../components/LoadingBar";
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
test('LoadingBar successfully mounts with minimal default props', () => { test("LoadingBar successfully mounts with minimal default props", () => {
const component = shallow(<LoadingBar />); const component = shallow(<LoadingBar />);
expect(component.exists()).toBe(true); expect(component.exists()).toBe(true);
}); });

View File

@ -1,17 +1,16 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
import MessageBytes from '../../components/MessageBytes'; import MessageBytes from "../../components/MessageBytes";
import DbcUtils from '../../utils/dbc'; import DbcUtils from "../../utils/dbc";
import DBC from '../../models/can/dbc'; import DBC from "../../models/can/dbc";
test('MessageBytes successfully mounts with minimal default props', () => { test("MessageBytes successfully mounts with minimal default props", () => {
const message = DbcUtils.createMessageSpec(new DBC(), 0, '0', 1); const message = DbcUtils.createMessageSpec(new DBC(), 0, "0", 1);
const component = shallow(<MessageBytes const component = shallow(
seekTime={0} <MessageBytes seekTime={0} message={message} live={true} />
message={message} );
live={true} />); expect(component.exists()).toBe(true);
expect(component.exists()).toBe(true);
}); });

View File

@ -1,30 +1,33 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import Meta from '../../components/Meta'; import Meta from "../../components/Meta";
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
test('Meta successfully mounts with minimal default props', () => { test("Meta successfully mounts with minimal default props", () => {
const component = shallow(<Meta url={null} const component = shallow(
messages={{}} <Meta
selectedMessages={[]} url={null}
updateSelectedMessages={() => {}} messages={{}}
showEditMessageModal={() => {}} selectedMessages={[]}
currentParts={[]} updateSelectedMessages={() => {}}
onMessageSelected={() => {}} showEditMessageModal={() => {}}
onMessageUnselected={() => {}} currentParts={[]}
showLoadDbc={() => {}} onMessageSelected={() => {}}
showSaveDbc={() => {}} onMessageUnselected={() => {}}
dbcFilename={null} showLoadDbc={() => {}}
dbcLastSaved={null} showSaveDbc={() => {}}
dongleId={null} dbcFilename={null}
name={null} dbcLastSaved={null}
route={null} dongleId={null}
seekTime={0} name={null}
seekIndex={0} route={null}
maxByteStateChangeCount={0} seekTime={0}
isDemo={false} seekIndex={0}
live={true} maxByteStateChangeCount={0}
/>); isDemo={false}
expect(component.exists()).toBe(true); live={true}
/>
);
expect(component.exists()).toBe(true);
}); });

View File

@ -1,10 +1,10 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import Modal from '../../components/Modal'; import Modal from "../../components/Modal";
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
test('Modal successfully mounts with minimal default props', () => { test("Modal successfully mounts with minimal default props", () => {
const component = shallow(<Modal />); const component = shallow(<Modal />);
expect(component.exists()).toBe(true); expect(component.exists()).toBe(true);
}); });

View File

@ -1,12 +1,12 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import PartSelector from '../../components/PartSelector'; import PartSelector from "../../components/PartSelector";
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
test('PartSelector successfully mounts with minimal default props', () => { test("PartSelector successfully mounts with minimal default props", () => {
const component = shallow(<PartSelector const component = shallow(
onPartChange={ () => {} } <PartSelector onPartChange={() => {}} partsCount={0} />
partsCount={ 0 } />); );
expect(component.exists()).toBe(true); expect(component.exists()).toBe(true);
}); });

View File

@ -1,10 +1,10 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import PlayButton from '../../components/PlayButton'; import PlayButton from "../../components/PlayButton";
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
test('PlayButton successfully mounts with minimal default props', () => { test("PlayButton successfully mounts with minimal default props", () => {
const component = shallow(<PlayButton />); const component = shallow(<PlayButton />);
expect(component.exists()).toBe(true); expect(component.exists()).toBe(true);
}); });

View File

@ -1,21 +1,24 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import RouteSeeker from '../../components/RouteSeeker'; import RouteSeeker from "../../components/RouteSeeker";
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
test('RouteSeeker successfully mounts with minimal default props', () => { test("RouteSeeker successfully mounts with minimal default props", () => {
const component = shallow(<RouteSeeker const component = shallow(
nearestFrameTime={0} <RouteSeeker
segmentProgress={() => {}} nearestFrameTime={0}
secondsLoaded={0} segmentProgress={() => {}}
segmentIndices={[]} secondsLoaded={0}
onUserSeek={() => {}} segmentIndices={[]}
onPlaySeek={() => {}} onUserSeek={() => {}}
videoElement={null} onPlaySeek={() => {}}
onPlay={() => {}} videoElement={null}
onPause={() => {}} onPlay={() => {}}
playing={false} onPause={() => {}}
ratioTime={() => {}} />); playing={false}
expect(component.exists()).toBe(true); ratioTime={() => {}}
/>
);
expect(component.exists()).toBe(true);
}); });

View File

@ -1,9 +1,9 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import RouteVideoSync from '../../components/RouteVideoSync'; import RouteVideoSync from "../../components/RouteVideoSync";
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
import {StyleSheetTestUtils} from 'aphrodite'; import { StyleSheetTestUtils } from "aphrodite";
// Prevents style injection from firing after test finishes // Prevents style injection from firing after test finishes
// and jsdom is torn down. // and jsdom is torn down.
@ -14,22 +14,25 @@ afterEach(() => {
StyleSheetTestUtils.clearBufferAndResumeStyleInjection(); StyleSheetTestUtils.clearBufferAndResumeStyleInjection();
}); });
test('RouteVideoSync successfully mounts with minimal default props', () => { test("RouteVideoSync successfully mounts with minimal default props", () => {
const component = shallow(<RouteVideoSync const component = shallow(
message={null} <RouteVideoSync
secondsLoaded={0} message={null}
startOffset={0} secondsLoaded={0}
seekIndex={0} startOffset={0}
userSeekIndex={0} seekIndex={0}
playing={false} userSeekIndex={0}
url={'http://comma.ai'} playing={false}
canFrameOffset={0} url={"http://comma.ai"}
firstCanTime={0} canFrameOffset={0}
onVideoClick={() => {}} firstCanTime={0}
onPlaySeek={() => {}} onVideoClick={() => {}}
onUserSeek={() => {}} onPlaySeek={() => {}}
onPlay={() => {}} onUserSeek={() => {}}
onPause={() => {}} onPlay={() => {}}
userSeekTime={0} />); onPause={() => {}}
expect(component.exists()).toBe(true); userSeekTime={0}
/>
);
expect(component.exists()).toBe(true);
}); });

View File

@ -1,23 +1,25 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
import SaveDbcModal from '../../components/SaveDbcModal'; import SaveDbcModal from "../../components/SaveDbcModal";
import OpenDbc from '../../api/OpenDbc'; import OpenDbc from "../../api/OpenDbc";
import DBC from '../../models/can/dbc'; import DBC from "../../models/can/dbc";
test('SaveDbcModal successfully mounts with minimal default props', () => { test("SaveDbcModal successfully mounts with minimal default props", () => {
const openDbcClient = new OpenDbc(null); const openDbcClient = new OpenDbc(null);
const dbc = new DBC(); const dbc = new DBC();
const component = shallow(<SaveDbcModal const component = shallow(
dbc={dbc} <SaveDbcModal
sourceDbcFilename={''} dbc={dbc}
onDbcSaved={() => {}} sourceDbcFilename={""}
handleClose={() => {}} onDbcSaved={() => {}}
openDbcClient={openDbcClient} handleClose={() => {}}
hasGithubAuth={false} openDbcClient={openDbcClient}
loginWithGithub={<p>Login with github</p>} hasGithubAuth={false}
/>); loginWithGithub={<p>Login with github</p>}
expect(component.exists()).toBe(true); />
);
expect(component.exists()).toBe(true);
}); });

View File

@ -1,21 +1,23 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import SignalLegend from '../../components/SignalLegend'; import SignalLegend from "../../components/SignalLegend";
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
test('SignalLegend successfully mounts with minimal default props', () => { test("SignalLegend successfully mounts with minimal default props", () => {
const component = shallow(<SignalLegend const component = shallow(
signals={{}} <SignalLegend
signalStyles={{}} signals={{}}
highlightedSignal={null} signalStyles={{}}
onSignalHover={() => {}} highlightedSignal={null}
onSignalHoverEnd={() => {}} onSignalHover={() => {}}
onTentativeSignalChange={() => {}} onSignalHoverEnd={() => {}}
onSignalChange={() => {}} onTentativeSignalChange={() => {}}
onSignalRemove={() => {}} onSignalChange={() => {}}
onSignalPlotChange={() => {}} onSignalRemove={() => {}}
plottedSignals={[]} onSignalPlotChange={() => {}}
/>); plottedSignals={[]}
expect(component.exists()).toBe(true); />
);
expect(component.exists()).toBe(true);
}); });

View File

@ -1,8 +1,8 @@
import SignalLegendEntry from '../../components/SignalLegendEntry'; import SignalLegendEntry from "../../components/SignalLegendEntry";
import Signal from '../../models/can/signal'; import Signal from "../../models/can/signal";
import React from 'react'; import React from "react";
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from "enzyme";
import {StyleSheetTestUtils} from 'aphrodite'; import { StyleSheetTestUtils } from "aphrodite";
// Prevents style injection from firing after test finishes // Prevents style injection from firing after test finishes
// and jsdom is torn down. // and jsdom is torn down.
@ -14,100 +14,122 @@ afterEach(() => {
}); });
function createSignalLegendEntry(props) { function createSignalLegendEntry(props) {
let signal = props.signal, let signal = props.signal,
onSignalChange = props.onSignalChange, onSignalChange = props.onSignalChange,
onTentativeSignalChange = props.onTentativeSignalChange; onTentativeSignalChange = props.onTentativeSignalChange;
if(signal === undefined) { if (signal === undefined) {
signal = new Signal({name: 'NEW_SIGNAL'}); signal = new Signal({ name: "NEW_SIGNAL" });
} }
if(onSignalChange === undefined) { if (onSignalChange === undefined) {
onSignalChange = () => {}; onSignalChange = () => {};
} }
if(onTentativeSignalChange === undefined) { if (onTentativeSignalChange === undefined) {
onTentativeSignalChange = () => {}; onTentativeSignalChange = () => {};
} }
return shallow(<SignalLegendEntry return shallow(
highlightedStyle={null} <SignalLegendEntry
signal={signal} highlightedStyle={null}
onSignalChange={onSignalChange} signal={signal}
onTentativeSignalChange={onTentativeSignalChange} onSignalChange={onSignalChange}
/>); onTentativeSignalChange={onTentativeSignalChange}
/>
);
} }
test('a little endian signal spanning one byte should adjust its startBit switching to big endian, preserving its bit coverage', () => { test("a little endian signal spanning one byte should adjust its startBit switching to big endian, preserving its bit coverage", () => {
const signal = new Signal({name: 'signal', const signal = new Signal({
startBit: 0, name: "signal",
size: 8, startBit: 0,
isLittleEndian: true}); size: 8,
isLittleEndian: true
});
const component = createSignalLegendEntry({signal}); const component = createSignalLegendEntry({ signal });
const endiannessFieldSpec = SignalLegendEntry.fieldSpecForName('isLittleEndian'); const endiannessFieldSpec = SignalLegendEntry.fieldSpecForName(
component.instance().updateField(endiannessFieldSpec, false); "isLittleEndian"
);
component.instance().updateField(endiannessFieldSpec, false);
const signalEdited = component.state('signalEdited'); const signalEdited = component.state("signalEdited");
expect(signalEdited.isLittleEndian).toBe(false); expect(signalEdited.isLittleEndian).toBe(false);
expect(signalEdited.startBit).toBe(7); expect(signalEdited.startBit).toBe(7);
expect(signalEdited.size).toBe(8); expect(signalEdited.size).toBe(8);
}); });
test('a big endian signal spanning two bytes should should adjust its startBit switching to little endian, preserving its bit coverage', () => { test("a big endian signal spanning two bytes should should adjust its startBit switching to little endian, preserving its bit coverage", () => {
const signal = new Signal({name: 'signal', const signal = new Signal({
startBit: 7, name: "signal",
size: 8, startBit: 7,
isLittleEndian: false}); size: 8,
const component = createSignalLegendEntry({signal}); isLittleEndian: false
const endiannessFieldSpec = SignalLegendEntry.fieldSpecForName('isLittleEndian'); });
component.instance().updateField(endiannessFieldSpec, true); const component = createSignalLegendEntry({ signal });
const endiannessFieldSpec = SignalLegendEntry.fieldSpecForName(
"isLittleEndian"
);
component.instance().updateField(endiannessFieldSpec, true);
const signalEdited = component.state('signalEdited'); const signalEdited = component.state("signalEdited");
expect(signalEdited.isLittleEndian).toBe(true); expect(signalEdited.isLittleEndian).toBe(true);
expect(signalEdited.startBit).toBe(0); expect(signalEdited.startBit).toBe(0);
expect(signalEdited.size).toBe(8); expect(signalEdited.size).toBe(8);
}); });
test("a big endian signal spanning one and a half bytes should adjust its startBit switching to little endian, preserving the first byte's coverage", () => { test("a big endian signal spanning one and a half bytes should adjust its startBit switching to little endian, preserving the first byte's coverage", () => {
const signal = new Signal({name: 'signal', const signal = new Signal({
startBit: 7, name: "signal",
size: 12, startBit: 7,
isLittleEndian: false}); size: 12,
isLittleEndian: false
});
const component = createSignalLegendEntry({signal}); const component = createSignalLegendEntry({ signal });
const endiannessFieldSpec = SignalLegendEntry.fieldSpecForName('isLittleEndian'); const endiannessFieldSpec = SignalLegendEntry.fieldSpecForName(
component.instance().updateField(endiannessFieldSpec, true); "isLittleEndian"
);
component.instance().updateField(endiannessFieldSpec, true);
const signalEdited = component.state('signalEdited'); const signalEdited = component.state("signalEdited");
expect(signalEdited.isLittleEndian).toBe(true); expect(signalEdited.isLittleEndian).toBe(true);
expect(signalEdited.startBit).toBe(0); expect(signalEdited.startBit).toBe(0);
expect(signalEdited.size).toBe(12); expect(signalEdited.size).toBe(12);
}); });
test('a little endian signal spanning 3 bits on one byte should adjust its startBit switching to big endian, preserving its bit coverage', () => { test("a little endian signal spanning 3 bits on one byte should adjust its startBit switching to big endian, preserving its bit coverage", () => {
const signal = new Signal({name: 'signal', const signal = new Signal({
startBit: 13, name: "signal",
size: 3, startBit: 13,
isLittleEndian: true}); size: 3,
const component = createSignalLegendEntry({signal}); isLittleEndian: true
const endiannessFieldSpec = SignalLegendEntry.fieldSpecForName('isLittleEndian'); });
component.instance().updateField(endiannessFieldSpec, false); const component = createSignalLegendEntry({ signal });
const endiannessFieldSpec = SignalLegendEntry.fieldSpecForName(
"isLittleEndian"
);
component.instance().updateField(endiannessFieldSpec, false);
const signalEdited = component.state('signalEdited'); const signalEdited = component.state("signalEdited");
expect(signalEdited.isLittleEndian).toBe(false); expect(signalEdited.isLittleEndian).toBe(false);
expect(signalEdited.startBit).toBe(15); expect(signalEdited.startBit).toBe(15);
expect(signalEdited.size).toBe(3); expect(signalEdited.size).toBe(3);
}); });
test('a big endian signal spanning 3 bytes on one byte should adjust its startBit switching to little endian, preserving its bit coverage', () => { test("a big endian signal spanning 3 bytes on one byte should adjust its startBit switching to little endian, preserving its bit coverage", () => {
const signal = new Signal({name: 'signal', const signal = new Signal({
startBit: 15, name: "signal",
size: 3, startBit: 15,
isLittleEndian: false}); size: 3,
const component = createSignalLegendEntry({signal}); isLittleEndian: false
const endiannessFieldSpec = SignalLegendEntry.fieldSpecForName('isLittleEndian'); });
component.instance().updateField(endiannessFieldSpec, true); const component = createSignalLegendEntry({ signal });
const endiannessFieldSpec = SignalLegendEntry.fieldSpecForName(
"isLittleEndian"
);
component.instance().updateField(endiannessFieldSpec, true);
const signalEdited = component.state('signalEdited'); const signalEdited = component.state("signalEdited");
expect(signalEdited.isLittleEndian).toBe(true); expect(signalEdited.isLittleEndian).toBe(true);
expect(signalEdited.startBit).toBe(13); expect(signalEdited.startBit).toBe(13);
expect(signalEdited.size).toBe(3); expect(signalEdited.size).toBe(3);
}); });

View File

@ -1,38 +1,45 @@
// appendNewGraphData(plottedSignals, graphData, messages) { // appendNewGraphData(plottedSignals, graphData, messages) {
global.__JEST__ = 1; global.__JEST__ = 1;
import GraphData from '../../models/graph-data'; import GraphData from "../../models/graph-data";
import Signal from '../../models/can/signal'; import Signal from "../../models/can/signal";
import DBC from '../../models/can/dbc'; import DBC from "../../models/can/dbc";
import DbcUtils from '../../utils/dbc'; import DbcUtils from "../../utils/dbc";
function appendMockGraphData(existingGraphData, entryCount = 1) { function appendMockGraphData(existingGraphData, entryCount = 1) {
const dbc = new DBC(); const dbc = new DBC();
const signal = new Signal({name: 'NEW_SIGNAL_1'}); const signal = new Signal({ name: "NEW_SIGNAL_1" });
dbc.setSignals(0, {[signal.name]: signal}); dbc.setSignals(0, { [signal.name]: signal });
const message = DbcUtils.createMessageSpec(dbc, 0, '0', 0); const message = DbcUtils.createMessageSpec(dbc, 0, "0", 0);
// time, relTime, data, byteStateChangeTimes) { // time, relTime, data, byteStateChangeTimes) {
message.entries = Array(entryCount).fill(DbcUtils.createMessageEntry(dbc, 0, 0, 0, Buffer.alloc(8), [])); message.entries = Array(entryCount).fill(
const messages = {[message.id]: message}; DbcUtils.createMessageEntry(dbc, 0, 0, 0, Buffer.alloc(8), [])
);
const messages = { [message.id]: message };
const plottedSignals = [[{signalUid: signal.uid, messageId: '0'}]]; const plottedSignals = [[{ signalUid: signal.uid, messageId: "0" }]];
return GraphData.appendNewGraphData(plottedSignals, existingGraphData, messages, 0); return GraphData.appendNewGraphData(
plottedSignals,
existingGraphData,
messages,
0
);
} }
test('GraphData.appendNewGraphData adds messages to empty GraphData array', () => { test("GraphData.appendNewGraphData adds messages to empty GraphData array", () => {
const graphData = appendMockGraphData([[]]); const graphData = appendMockGraphData([[]]);
expect(graphData.length).toEqual(1); // 1 plot expect(graphData.length).toEqual(1); // 1 plot
expect(graphData[0].length).toEqual(1); // 1 message entry expect(graphData[0].length).toEqual(1); // 1 message entry
expect(graphData[0][0].x).toEqual(0); // message entry X value corresponds to provided time in createMessageEntry expect(graphData[0][0].x).toEqual(0); // message entry X value corresponds to provided time in createMessageEntry
}); });
test('GraphData.appendNewGraphData does not change graph data when entries are unchanged', () => { test("GraphData.appendNewGraphData does not change graph data when entries are unchanged", () => {
let graphData = [[]]; let graphData = [[]];
for(let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
graphData = appendMockGraphData(graphData); graphData = appendMockGraphData(graphData);
} }
expect(graphData.length).toEqual(1); expect(graphData.length).toEqual(1);
expect(graphData[0].length).toEqual(1); expect(graphData[0].length).toEqual(1);
}); });

View File

@ -1,23 +1,22 @@
import Panda from '../../api/panda'; import Panda from "../../api/panda";
function arrayBufferFromHex(hex) { function arrayBufferFromHex(hex) {
const buffer = Buffer.from(hex, 'hex'); const buffer = Buffer.from(hex, "hex");
const arrayBuffer = new ArrayBuffer(buffer.length); const arrayBuffer = new ArrayBuffer(buffer.length);
const view = new Uint8Array(arrayBuffer); const view = new Uint8Array(arrayBuffer);
for(let i = 0; i < buffer.length; i++) { for (let i = 0; i < buffer.length; i++) {
view[i] = buffer[i]; view[i] = buffer[i];
} }
return arrayBuffer; return arrayBuffer;
} }
test('parseCanBuffer correctly parses a message', () => { test("parseCanBuffer correctly parses a message", () => {
const panda = new Panda(); const panda = new Panda();
// 16 byte buffer // 16 byte buffer
const arrayBuffer = arrayBufferFromHex('abababababababababababababababab'); const arrayBuffer = arrayBufferFromHex("abababababababababababababababab");
const messages = panda.parseCanBuffer(arrayBuffer); const messages = panda.parseCanBuffer(arrayBuffer);
expect(messages.length).toEqual(1) expect(messages.length).toEqual(1);
expect(messages[0]).toEqual([1373, 43947, 'abababababababab', 10]); expect(messages[0]).toEqual([1373, 43947, "abababababababab", 10]);
}); });

View File

@ -1,4 +1,4 @@
export const ACURA_DBC= `VERSION "" export const ACURA_DBC = `VERSION ""
NS_ : NS_ :
@ -318,4 +318,4 @@ VAL_ 506 CHIME 4 "double_chime" 3 "single_chime" 2 "continuous_chime" 1 "repeati
VAL_ 506 FCW 3 "fcw" 2 "fcw" 1 "fcw" 0 "no_fcw" ; VAL_ 506 FCW 3 "fcw" 2 "fcw" 1 "fcw" 0 "no_fcw" ;
VAL_ 780 HUD_LEAD 3 "no_car" 2 "solid_car" 1 "dashed_car" 0 "no_car" ; VAL_ 780 HUD_LEAD 3 "no_car" 2 "solid_car" 1 "dashed_car" 0 "no_car" ;
VAL_ 829 BEEP 3 "single_beep" 2 "triple_beep" 1 "repeated_beep" 0 "no_beep" ; VAL_ 829 BEEP 3 "single_beep" 2 "triple_beep" 1 "repeated_beep" 0 "no_beep" ;
`; `;

View File

@ -404,4 +404,4 @@ VAL_ 904 MCU_clusterReadyForDrive 0 "NO_SNA" 1 "YES" ;
VAL_ 1160 DAS_steeringAngleRequest 16384 "ZERO_ANGLE" ; VAL_ 1160 DAS_steeringAngleRequest 16384 "ZERO_ANGLE" ;
VAL_ 1160 DAS_steeringControlType 1 "ANGLE_CONTROL" 3 "DISABLED" 0 "NONE" 2 "RESERVED" ; VAL_ 1160 DAS_steeringControlType 1 "ANGLE_CONTROL" 3 "DISABLED" 0 "NONE" 2 "RESERVED" ;
VAL_ 1160 DAS_steeringHapticRequest 1 "ACTIVE" 0 "IDLE" ; VAL_ 1160 DAS_steeringHapticRequest 1 "ACTIVE" 0 "IDLE" ;
`; `;

View File

@ -1,8 +1,8 @@
import {shade} from '../../utils/color'; import { shade } from "../../utils/color";
test('Shade darkens rgb white (255,255,255)', () => { test("Shade darkens rgb white (255,255,255)", () => {
const rgb = [255,255,255]; const rgb = [255, 255, 255];
const darkenRgb = shade(rgb, -0.5); const darkenRgb = shade(rgb, -0.5);
expect(darkenRgb).toEqual([128,128,128]); expect(darkenRgb).toEqual([128, 128, 128]);
}); });

View File

@ -1,63 +1,70 @@
global.__JEST__ = 1 global.__JEST__ = 1;
import DbcUtils from '../../utils/dbc'; import DbcUtils from "../../utils/dbc";
import DBC from '../../models/can/dbc'; import DBC from "../../models/can/dbc";
import Signal from '../../models/can/signal'; import Signal from "../../models/can/signal";
// want to mock pandareader and test processStreamedCanMessages // want to mock pandareader and test processStreamedCanMessages
const SAMPLE_MESSAGE = [0x10, 0, Buffer.from('abababababababab', 'hex'), 1]; const SAMPLE_MESSAGE = [0x10, 0, Buffer.from("abababababababab", "hex"), 1];
const SAMPLE_MESSAGE_ID = '1:10'; const SAMPLE_MESSAGE_ID = "1:10";
function expectSampleMessageFieldsPreserved(messages, frame) { function expectSampleMessageFieldsPreserved(messages, frame) {
const [address, busTime, data, source] = SAMPLE_MESSAGE; const [address, busTime, data, source] = SAMPLE_MESSAGE;
expect(messages[SAMPLE_MESSAGE_ID].address).toEqual(address); expect(messages[SAMPLE_MESSAGE_ID].address).toEqual(address);
expect(messages[SAMPLE_MESSAGE_ID].id).toEqual(SAMPLE_MESSAGE_ID); expect(messages[SAMPLE_MESSAGE_ID].id).toEqual(SAMPLE_MESSAGE_ID);
expect(messages[SAMPLE_MESSAGE_ID].bus).toEqual(source); expect(messages[SAMPLE_MESSAGE_ID].bus).toEqual(source);
expect(messages[SAMPLE_MESSAGE_ID].frame).toEqual(frame); expect(messages[SAMPLE_MESSAGE_ID].frame).toEqual(frame);
expect(messages[SAMPLE_MESSAGE_ID].byteStateChangeCounts).toEqual(Array(8).fill(0)); expect(messages[SAMPLE_MESSAGE_ID].byteStateChangeCounts).toEqual(
Array(8).fill(0)
);
} }
// function addCanMessage([address, busTime, data, source], dbc, canStartTime, messages, prevMsgEntries, byteStateChangeCountsByMessage) { // function addCanMessage([address, busTime, data, source], dbc, canStartTime, messages, prevMsgEntries, byteStateChangeCountsByMessage) {
function addMessages(messages, message, dbc, n) { function addMessages(messages, message, dbc, n) {
const firstCanTime = 0; const firstCanTime = 0;
message = [...message]; message = [...message];
let nextMessage = () => {message[1] = message[1] + 1; return message}; let nextMessage = () => {
message[1] = message[1] + 1;
return message;
};
for(let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
DbcUtils.addCanMessage(nextMessage(), dbc, firstCanTime, messages, {}, {}); DbcUtils.addCanMessage(nextMessage(), dbc, firstCanTime, messages, {}, {});
} }
} }
test('addCanMessage should add raw can message with empty dbc', () => { test("addCanMessage should add raw can message with empty dbc", () => {
const messages = {}; const messages = {};
addMessages(messages, SAMPLE_MESSAGE, new DBC(), 1); addMessages(messages, SAMPLE_MESSAGE, new DBC(), 1);
expect(messages[SAMPLE_MESSAGE_ID].entries.length).toEqual(1); expect(messages[SAMPLE_MESSAGE_ID].entries.length).toEqual(1);
expectSampleMessageFieldsPreserved(messages); expectSampleMessageFieldsPreserved(messages);
}); });
test('addCanMessage should add multiple raw can messages with empty dbc', () => { test("addCanMessage should add multiple raw can messages with empty dbc", () => {
const messages = {}; const messages = {};
addMessages(messages, SAMPLE_MESSAGE, new DBC(), 3); addMessages(messages, SAMPLE_MESSAGE, new DBC(), 3);
expect(messages[SAMPLE_MESSAGE_ID].entries.length).toEqual(3); expect(messages[SAMPLE_MESSAGE_ID].entries.length).toEqual(3);
expectSampleMessageFieldsPreserved(messages); expectSampleMessageFieldsPreserved(messages);
}); });
test('addCanMessage should add parsed can message with dbc containing message spec', () => { test("addCanMessage should add parsed can message with dbc containing message spec", () => {
const messages = {}; const messages = {};
// create dbc with message spec and signal for sample_message // create dbc with message spec and signal for sample_message
const dbc = new DBC(); const dbc = new DBC();
dbc.createFrame(SAMPLE_MESSAGE[0]); dbc.createFrame(SAMPLE_MESSAGE[0]);
const signal = new Signal({name: 'NEW_SIGNAL', startBit: 0, size: 8}); const signal = new Signal({ name: "NEW_SIGNAL", startBit: 0, size: 8 });
dbc.addSignal(SAMPLE_MESSAGE[0], signal); dbc.addSignal(SAMPLE_MESSAGE[0], signal);
// add 1 sample_message // add 1 sample_message
addMessages(messages, SAMPLE_MESSAGE, dbc, 1); addMessages(messages, SAMPLE_MESSAGE, dbc, 1);
// verify message and parsed signal added // verify message and parsed signal added
const sampleMessages = messages[SAMPLE_MESSAGE_ID]; const sampleMessages = messages[SAMPLE_MESSAGE_ID];
expect(sampleMessages.entries.length).toEqual(1); expect(sampleMessages.entries.length).toEqual(1);
expect(sampleMessages.entries[0].signals[signal.name]).toEqual(0xab); expect(sampleMessages.entries[0].signals[signal.name]).toEqual(0xab);
expectSampleMessageFieldsPreserved(messages, dbc.messages.get(SAMPLE_MESSAGE[0])); expectSampleMessageFieldsPreserved(
messages,
dbc.messages.get(SAMPLE_MESSAGE[0])
);
}); });

View File

@ -1,4 +1,4 @@
import DBC from './models/can/dbc'; import DBC from "./models/can/dbc";
const AcuraDbc = new DBC(` const AcuraDbc = new DBC(`
VERSION "" VERSION ""

View File

@ -1,12 +1,12 @@
import GitHub from 'github-api'; import GitHub from "github-api";
import {OPENDBC_SOURCE_REPO} from '../config'; import { OPENDBC_SOURCE_REPO } from "../config";
export default class OpenDBC { export default class OpenDBC {
constructor(token) { constructor(token) {
this.token = token; this.token = token;
this.github = new GitHub({token}); this.github = new GitHub({ token });
this.sourceRepo = this.github.getRepo('commaai', 'opendbc'); this.sourceRepo = this.github.getRepo("commaai", "opendbc");
this.githubUsername = null; this.githubUsername = null;
} }
@ -15,11 +15,11 @@ export default class OpenDBC {
} }
async getGithubUsername() { async getGithubUsername() {
if(this.githubUsername) { if (this.githubUsername) {
return this.githubUsername; return this.githubUsername;
} else { } else {
const githubUsername = await this.fetchGithubUsername(); const githubUsername = await this.fetchGithubUsername();
if(githubUsername) { if (githubUsername) {
return githubUsername; return githubUsername;
} }
} }
@ -28,13 +28,13 @@ export default class OpenDBC {
async fetchGithubUsername() { async fetchGithubUsername() {
try { try {
const user = await this.github.getUser(); const user = await this.github.getUser();
if(user) { if (user) {
const profile = await user.getProfile(); const profile = await user.getProfile();
if(profile) { if (profile) {
return profile.data.login; return profile.data.login;
} }
} }
} catch(e) { } catch (e) {
return null; return null;
} }
} }
@ -47,31 +47,31 @@ export default class OpenDBC {
*/ */
let repo; let repo;
if(repoFullName === undefined) { if (repoFullName === undefined) {
repo = this.sourceRepo; repo = this.sourceRepo;
} else { } else {
const [username, repoName] = repoFullName.split('/'); const [username, repoName] = repoFullName.split("/");
repo = this.github.getRepo(username, repoName); repo = this.github.getRepo(username, repoName);
} }
try { try {
const response = await repo.getContents('master', ''); const response = await repo.getContents("master", "");
return response.data.map((content) => content.path); return response.data.map(content => content.path);
} catch(e) { } catch (e) {
return []; return [];
} }
} }
async getDbcContents(dbcPath, repoFullName) { async getDbcContents(dbcPath, repoFullName) {
let repo; let repo;
if(repoFullName === undefined) { if (repoFullName === undefined) {
repo = this.sourceRepo; repo = this.sourceRepo;
} else { } else {
const [username, repoName] = repoFullName.split('/'); const [username, repoName] = repoFullName.split("/");
repo = this.github.getRepo(username, repoName); repo = this.github.getRepo(username, repoName);
} }
const fileContents = await repo.getContents('master', dbcPath); const fileContents = await repo.getContents("master", dbcPath);
const rawContentsUrl = fileContents.data.download_url; const rawContentsUrl = fileContents.data.download_url;
@ -81,72 +81,83 @@ export default class OpenDBC {
} }
repoSourceIsOpenDbc(repoDetails) { repoSourceIsOpenDbc(repoDetails) {
return repoDetails.source return (
&& repoDetails.source.full_name === OPENDBC_SOURCE_REPO; repoDetails.source && repoDetails.source.full_name === OPENDBC_SOURCE_REPO
);
} }
async getUserOpenDbcFork() { async getUserOpenDbcFork() {
const githubUsername = await this.getGithubUsername(); const githubUsername = await this.getGithubUsername();
if(!githubUsername) return null; if (!githubUsername) return null;
const openDbcFork = this.github.getRepo(githubUsername, 'opendbc'); const openDbcFork = this.github.getRepo(githubUsername, "opendbc");
const repoDetailResp = await openDbcFork.getDetails(); const repoDetailResp = await openDbcFork.getDetails();
const repoDetails = repoDetailResp.data; const repoDetails = repoDetailResp.data;
if(this.repoSourceIsOpenDbc(repoDetails)) { if (this.repoSourceIsOpenDbc(repoDetails)) {
return repoDetails.full_name; return repoDetails.full_name;
} else { } else {
return null; return null;
} }
} }
async fork() { async fork() {
const forkResponse = await this.sourceRepo.fork(); const forkResponse = await this.sourceRepo.fork();
if(forkResponse.status === 202) { if (forkResponse.status === 202) {
return true; return true;
} else { } else {
return false; return false;
} }
} }
async commitFile(repoFullName, path, contents) { async commitFile(repoFullName, path, contents) {
/* /*
repo is of format username/reponame repo is of format username/reponame
authenciated user must have write access to repo authenciated user must have write access to repo
*/ */
const [user, repoName] = repoFullName.split('/'); const [user, repoName] = repoFullName.split("/");
const repo = this.github.getRepo(user, repoName); const repo = this.github.getRepo(user, repoName);
// get HEAD reference // get HEAD reference
const refResp = await repo.getRef('heads/master'); const refResp = await repo.getRef("heads/master");
const ref = refResp.data; const ref = refResp.data;
// get HEAD commit sha // get HEAD commit sha
const headCommitResp = await repo.getCommit(ref.object.sha); const headCommitResp = await repo.getCommit(ref.object.sha);
const headCommit = headCommitResp.data; const headCommit = headCommitResp.data;
// get HEAD tree // get HEAD tree
const headTreeResp = await repo.getTree(headCommit.tree.sha); const headTreeResp = await repo.getTree(headCommit.tree.sha);
const headTree = headTreeResp.data; const headTree = headTreeResp.data;
// create new tree // create new tree
const tree = [{ const tree = [
mode: '100644', {
path: path, mode: "100644",
type: 'blob', path: path,
content: contents, type: "blob",
}]; content: contents
}
];
const createTreeResp = await repo.createTree(tree, headTree.sha); const createTreeResp = await repo.createTree(tree, headTree.sha);
const createdTree = createTreeResp.data; const createdTree = createTreeResp.data;
// commit // commit
const commitResp = await repo.commit(headCommit.sha, createdTree.sha, 'OpenDBC updates'); const commitResp = await repo.commit(
const commit = commitResp.data; headCommit.sha,
createdTree.sha,
"OpenDBC updates"
);
const commit = commitResp.data;
// update HEAD // update HEAD
const updateHeadResp = await repo.updateHead('heads/master', commit.sha, false); const updateHeadResp = await repo.updateHead(
"heads/master",
commit.sha,
false
);
return updateHeadResp.status === 200; return updateHeadResp.status === 200;
} }
} }

View File

@ -1,32 +1,38 @@
import NumpyLoader from '../utils/loadnpy'; import NumpyLoader from "../utils/loadnpy";
export async function fetchCanTimes(base, part) { export async function fetchCanTimes(base, part) {
const url = base+"/Log/"+part+"/can/t"; const url = base + "/Log/" + part + "/can/t";
const canData = await NumpyLoader.promise(url); const canData = await NumpyLoader.promise(url);
return canData.data; return canData.data;
} }
export async function fetchCanPart(base, part) { export async function fetchCanPart(base, part) {
var urls = [ base+"/Log/"+part+"/can/t", var urls = [
base+"/Log/"+part+"/can/src", base + "/Log/" + part + "/can/t",
base+"/Log/"+part+"/can/address", base + "/Log/" + part + "/can/src",
base+"/Log/"+part+"/can/data"]; base + "/Log/" + part + "/can/address",
base + "/Log/" + part + "/can/data"
];
var messages = {}; var messages = {};
let canData = null; let canData = null;
try { try {
canData = await Promise.all(urls.map(NumpyLoader.promise)); canData = await Promise.all(urls.map(NumpyLoader.promise));
} catch (e) { } catch (e) {
console.log('this is a 404 workaround that is hacky', e) console.log("this is a 404 workaround that is hacky", e);
return {times: [], return {
sources: [], times: [],
addresses: [], sources: [],
datas: []} addresses: [],
} datas: []
return {times: canData[0].data, };
sources: canData[1].data, }
addresses: canData[2].data, return {
datas: canData[3].data}; times: canData[0].data,
} sources: canData[1].data,
addresses: canData[2].data,
datas: canData[3].data
};
}

View File

@ -1,19 +1,22 @@
import Cookies from 'js-cookie'; import Cookies from "js-cookie";
import {COMMA_ACCESS_TOKEN_COOKIE, COMMA_OAUTH_REDIRECT_COOKIE} from '../config'; import {
COMMA_ACCESS_TOKEN_COOKIE,
COMMA_OAUTH_REDIRECT_COOKIE
} from "../config";
function getCommaAccessToken() { function getCommaAccessToken() {
return Cookies.get(COMMA_ACCESS_TOKEN_COOKIE); return Cookies.get(COMMA_ACCESS_TOKEN_COOKIE);
} }
function isAuthenticated() { function isAuthenticated() {
return getCommaAccessToken() !== undefined; return getCommaAccessToken() !== undefined;
} }
function authUrl() { function authUrl() {
Cookies.set(COMMA_OAUTH_REDIRECT_COOKIE, window.location.href); Cookies.set(COMMA_OAUTH_REDIRECT_COOKIE, window.location.href);
return 'https://community.comma.ai/ucp.php?mode=login&login=external&oauth_service=google'; return "https://community.comma.ai/ucp.php?mode=login&login=external&oauth_service=google";
} }
export default {getCommaAccessToken, isAuthenticated, authUrl}; export default { getCommaAccessToken, isAuthenticated, authUrl };

View File

@ -1,11 +1,13 @@
import {GITHUB_CLIENT_ID, GITHUB_REDIRECT_URL} from '../config'; import { GITHUB_CLIENT_ID, GITHUB_REDIRECT_URL } from "../config";
import {objToQuery} from '../utils/url'; import { objToQuery } from "../utils/url";
export function authorizeUrl(route) { export function authorizeUrl(route) {
const params = {client_id: GITHUB_CLIENT_ID, const params = {
redirect_uri: GITHUB_REDIRECT_URL, client_id: GITHUB_CLIENT_ID,
scope: 'user:email public_repo', redirect_uri: GITHUB_REDIRECT_URL,
state: JSON.stringify({route})}; scope: "user:email public_repo",
state: JSON.stringify({ route })
};
return `http://github.com/login/oauth/authorize?${objToQuery(params)}`; return `http://github.com/login/oauth/authorize?${objToQuery(params)}`;
} }

View File

@ -1,24 +1,26 @@
import DBC from '../models/can/dbc'; import DBC from "../models/can/dbc";
export function fetchPersistedDbc(routeName) { export function fetchPersistedDbc(routeName) {
const maybeDbc = window.localStorage.getItem(routeName); const maybeDbc = window.localStorage.getItem(routeName);
if(maybeDbc !== null) { if (maybeDbc !== null) {
const {dbcFilename, dbcText} = JSON.parse(maybeDbc); const { dbcFilename, dbcText } = JSON.parse(maybeDbc);
const dbc = new DBC(dbcText); const dbc = new DBC(dbcText);
return {dbc, dbcText, dbcFilename}; return { dbc, dbcText, dbcFilename };
} else return null; } else return null;
} }
export function persistDbc(routeName, {dbcFilename, dbc}) { export function persistDbc(routeName, { dbcFilename, dbc }) {
const dbcJson = JSON.stringify({dbcFilename, const dbcJson = JSON.stringify({
dbcText: dbc.text()}); dbcFilename,
dbcText: dbc.text()
});
window.localStorage.setItem(routeName, dbcJson); window.localStorage.setItem(routeName, dbcJson);
} }
const GITHUB_AUTH_TOKEN_LOCALSTORAGE_KEY = 'gh_auth_token'; const GITHUB_AUTH_TOKEN_LOCALSTORAGE_KEY = "gh_auth_token";
export function fetchPersistedGithubAuthToken() { export function fetchPersistedGithubAuthToken() {
return window.localStorage.getItem(GITHUB_AUTH_TOKEN_LOCALSTORAGE_KEY); return window.localStorage.getItem(GITHUB_AUTH_TOKEN_LOCALSTORAGE_KEY);
} }
export function unpersistGithubAuthToken() { export function unpersistGithubAuthToken() {
@ -26,5 +28,5 @@ export function unpersistGithubAuthToken() {
} }
export function persistGithubAuthToken(token) { export function persistGithubAuthToken(token) {
return window.localStorage.setItem(GITHUB_AUTH_TOKEN_LOCALSTORAGE_KEY, token); return window.localStorage.setItem(GITHUB_AUTH_TOKEN_LOCALSTORAGE_KEY, token);
} }

View File

@ -1,66 +1,76 @@
import Panda from './panda'; import Panda from "./panda";
export default class PandaReader { export default class PandaReader {
static ERROR_NO_DEVICE_SELECTED = 8;
static ERROR_NO_DEVICE_SELECTED = 8; constructor() {
this.panda = new Panda();
this.isReading = false;
this.onMessagesReceived = () => {};
this.callbackQueue = [];
this.callbackQueueTimer = null;
constructor() { this.readLoop = this.readLoop.bind(this);
this.panda = new Panda(); this.flushCallbackQueue = this.flushCallbackQueue.bind(this);
this.isReading = false; this._flushCallbackQueue = this._flushCallbackQueue.bind(this);
this.onMessagesReceived = () => {}; }
this.callbackQueue = [];
this.callbackQueueTimer = null;
this.readLoop = this.readLoop.bind(this); connect() {
this.flushCallbackQueue = this.flushCallbackQueue.bind(this); return this.panda.connect();
this._flushCallbackQueue = this._flushCallbackQueue.bind(this); }
setOnMessagesReceivedCallback(callback) {
this.onMessagesReceived = callback;
}
stopReadLoop() {
this.isReading = false;
window.cancelAnimationFrame(this.callbackQueueTimer);
}
_flushCallbackQueue() {
const messages = this.callbackQueue.reduce(
(arr, messages) => arr.concat(messages),
[]
);
this.onMessagesReceived(messages);
this.callbackQueue = [];
}
flushCallbackQueue() {
if (this.callbackQueue.length > 0) {
this._flushCallbackQueue();
} }
connect() { this.callbackQueueTimer = window.requestAnimationFrame(
return this.panda.connect(); this.flushCallbackQueue
);
}
readLoop() {
if (!this.isReading) {
this.isReading = true;
// this.flushCallbackQueueTimer = wi
this.callbackQueueTimer = window.requestAnimationFrame(
this.flushCallbackQueue,
30
);
} }
setOnMessagesReceivedCallback(callback) { this.panda.canRecv().then(
this.onMessagesReceived = callback; messages => {
} if (this.isReading && messages.canMessages.length > 0) {
this.callbackQueue.push(messages);
stopReadLoop() {
this.isReading = false;
window.cancelAnimationFrame(this.callbackQueueTimer);
}
_flushCallbackQueue() {
const messages = this.callbackQueue.reduce((arr, messages) => arr.concat(messages), [])
this.onMessagesReceived(messages);
this.callbackQueue = [];
}
flushCallbackQueue() {
if(this.callbackQueue.length > 0) {
this._flushCallbackQueue();
}
this.callbackQueueTimer = window.requestAnimationFrame(this.flushCallbackQueue);
}
readLoop() {
if(!this.isReading) {
this.isReading = true;
// this.flushCallbackQueueTimer = wi
this.callbackQueueTimer = window.requestAnimationFrame(this.flushCallbackQueue, 30);
} }
this.readLoop();
this.panda.canRecv().then(messages => { },
if(this.isReading && messages.canMessages.length > 0) { error => {
this.callbackQueue.push(messages); if (this.isReading) {
} console.log("canRecv error", error);
this.readLoop(); this.readLoop();
}, error => { }
if(this.isReading) { }
console.log('canRecv error', error); );
this.readLoop(); }
}
});
}
} }

View File

@ -1,5 +1,5 @@
import CloudLog from '../logging/CloudLog'; import CloudLog from "../logging/CloudLog";
require('core-js/fn/string/pad-end'); require("core-js/fn/string/pad-end");
const PANDA_VENDOR_ID = 0xbbaa; const PANDA_VENDOR_ID = 0xbbaa;
const PANDA_PRODUCT_ID = 0xddcc; const PANDA_PRODUCT_ID = 0xddcc;
@ -7,73 +7,93 @@ const PANDA_PRODUCT_ID = 0xddcc;
const BUFFER_SIZE = 0x10 * 256; const BUFFER_SIZE = 0x10 * 256;
export default class Panda { export default class Panda {
constructor() { constructor() {
this.device = null; this.device = null;
}
connect() {
// Must be called via a mouse click handler, per Chrome restrictions.
return navigator.usb
.requestDevice({ filters: [{ vendorId: PANDA_VENDOR_ID }] })
.then(device => {
this.device = device;
return device.open();
})
.then(() => this.device.selectConfiguration(1))
.then(() => this.device.claimInterface(0));
}
async health() {
const controlParams = {
requestType: "vendor",
recipient: "device",
request: 0xd2,
value: 0,
index: 0
};
try {
return await this.device.controlTransferIn(controlParams, 13);
} catch (err) {
CloudLog.error({ event: "Panda.health failed", error: err });
}
}
parseCanBuffer(buffer) {
const messages = [];
for (let i = 0; i < buffer.byteLength; i += 0x10) {
const dat = buffer.slice(i, i + 0x10);
const datView = Buffer.from(dat);
const f1 = datView.readInt32LE(0),
f2 = datView.readInt32LE(4);
const address = f1 >>> 21;
const busTime = f2 >>> 16;
const data = new Buffer(dat.slice(8, 8 + (f2 & 0xf)));
const source = (f2 >> 4) & 0xf & 0xff;
messages.push([
address,
busTime,
data.toString("hex").padEnd(16, "0"),
source
]);
} }
connect() { return messages;
// Must be called via a mouse click handler, per Chrome restrictions. }
return navigator.usb.requestDevice({ filters: [{ vendorId: PANDA_VENDOR_ID }] })
.then(device => { async mockCanRecv() {
this.device = device; const promise = new Promise(resolve =>
return device.open(); setTimeout(
}) () =>
.then(() => this.device.selectConfiguration(1)) resolve({
.then(() => this.device.claimInterface(0)); time: performance.now() / 1000,
canMessages: [[0, Math.random() * 65000, "".padEnd(16, "0"), 0]]
}),
100
)
);
return await promise;
}
async canRecv() {
let result = null,
receiptTime = null;
while (result === null) {
try {
result = await this.device.transferIn(1, BUFFER_SIZE);
receiptTime = performance.now() / 1000;
} catch (err) {
console.warn("can_recv failed, retrying");
}
} }
async health() { return {
const controlParams = { requestType: 'vendor', time: receiptTime,
recipient: 'device', canMessages: this.parseCanBuffer(result.data.buffer)
request: 0xd2, };
value: 0, }
index: 0 };
try {
return await this.device.controlTransferIn(controlParams, 13);
} catch(err) {
CloudLog.error({event: 'Panda.health failed', 'error': err});
}
}
parseCanBuffer(buffer) {
const messages = [];
for(let i = 0; i < buffer.byteLength; i+=0x10) {
const dat = buffer.slice(i, i + 0x10);
const datView = Buffer.from(dat);
const f1 = datView.readInt32LE(0), f2 = datView.readInt32LE(4);
const address = f1 >>> 21;
const busTime = (f2 >>> 16);
const data = new Buffer(dat.slice(8, 8 + (f2 & 0xF)));
const source = ((f2 >> 4) & 0xF) & 0xFF;
messages.push([address, busTime, data.toString('hex').padEnd(16, '0'), source]);
}
return messages;
}
async mockCanRecv() {
const promise = new Promise((resolve) =>
setTimeout(() => resolve({time: performance.now() / 1000,
canMessages: [[0, Math.random() * 65000, ''.padEnd(16, '0'), 0]]}), 100));
return await promise;
}
async canRecv() {
let result = null, receiptTime = null;
while(result === null) {
try {
result = await this.device.transferIn(1, BUFFER_SIZE);
receiptTime = performance.now() / 1000;
} catch(err) {
console.warn('can_recv failed, retrying');
}
}
return {time: receiptTime, canMessages: this.parseCanBuffer(result.data.buffer)};
}
} }

View File

@ -1,11 +1,11 @@
import Cookies from 'js-cookie'; import Cookies from "js-cookie";
import Moment from 'moment'; import Moment from "moment";
import CommaAuth from './comma-auth'; import CommaAuth from "./comma-auth";
const ROUTES_ENDPOINT = 'https://api.commadotai.com/v1/{dongleId}/routes/'; const ROUTES_ENDPOINT = "https://api.commadotai.com/v1/{dongleId}/routes/";
function momentizeTimes(routes) { function momentizeTimes(routes) {
for(let routeName in routes) { for (let routeName in routes) {
routes[routeName].start_time = Moment(routes[routeName].start_time); routes[routeName].start_time = Moment(routes[routeName].start_time);
routes[routeName].end_time = Moment(routes[routeName].end_time); routes[routeName].end_time = Moment(routes[routeName].end_time);
} }
@ -13,35 +13,34 @@ function momentizeTimes(routes) {
} }
export async function fetchRoutes(dongleId) { export async function fetchRoutes(dongleId) {
// will throw errors from fetch() on HTTP failure // will throw errors from fetch() on HTTP failure
if(dongleId === undefined) { if (dongleId === undefined) {
dongleId = 'me'; dongleId = "me";
}
const accessToken = CommaAuth.getCommaAccessToken();
if (accessToken) {
const endpoint = ROUTES_ENDPOINT.replace("{dongleId}", dongleId);
const headers = new Headers();
headers.append("Authorization", `JWT ${accessToken}`);
const request = new Request(endpoint, { headers });
const resp = await fetch(request);
const routes = await resp.json();
if ("routes" in routes) {
return momentizeTimes(routes.routes);
} }
}
const accessToken = CommaAuth.getCommaAccessToken(); return {};
if(accessToken) {
const endpoint = ROUTES_ENDPOINT.replace('{dongleId}', dongleId);
const headers = new Headers();
headers.append('Authorization', `JWT ${accessToken}`);
const request = new Request(endpoint, {headers});
const resp = await fetch(request);
const routes = await resp.json();
if('routes' in routes) {
return momentizeTimes(routes.routes);
}
}
return {};
} }
export function cameraPath(routeUrl, frame) { export function cameraPath(routeUrl, frame) {
return `${routeUrl}/sec${frame}.jpg` return `${routeUrl}/sec${frame}.jpg`;
} }
export function parseRouteName(name) { export function parseRouteName(name) {
const startTime = Moment(name, "YYYY-MM-DD--H-m-s"); const startTime = Moment(name, "YYYY-MM-DD--H-m-s");
return {startTime}; return { startTime };
} }

View File

@ -1,12 +1,12 @@
import SocketIO from 'socket.io-client'; import SocketIO from "socket.io-client";
import {UNLOGGER_HOST} from '../config'; import { UNLOGGER_HOST } from "../config";
export default class UnloggerClient { export default class UnloggerClient {
constructor() { constructor() {
this.socket = SocketIO(UNLOGGER_HOST); this.socket = SocketIO(UNLOGGER_HOST);
} }
seek(dongleId, baseTime, seekSeconds) { seek(dongleId, baseTime, seekSeconds) {
this.socket.emit('seek', dongleId, baseTime, seekSeconds); this.socket.emit("seek", dongleId, baseTime, seekSeconds);
} }
} }

View File

@ -5,16 +5,18 @@ function videoUrl(dongleId, hashedRouteName) {
function videoUrlForRouteUrl(routeUrlString) { function videoUrlForRouteUrl(routeUrlString) {
const url = new URL(routeUrlString); const url = new URL(routeUrlString);
const pathParts = url.pathname.split('/'); const pathParts = url.pathname.split("/");
const [dongleIdPrefixed, hashedRouteName] = pathParts.slice(pathParts.length - 2); const [dongleIdPrefixed, hashedRouteName] = pathParts.slice(
let dongleId = dongleIdPrefixed pathParts.length - 2
if(dongleIdPrefixed.indexOf('comma-') === 0) { );
const [_, dongleIdNoPrefix] = dongleIdPrefixed.split('comma-'); let dongleId = dongleIdPrefixed;
if (dongleIdPrefixed.indexOf("comma-") === 0) {
const [_, dongleIdNoPrefix] = dongleIdPrefixed.split("comma-");
dongleId = dongleIdNoPrefix; dongleId = dongleIdNoPrefix;
} }
return videoUrl(dongleId, hashedRouteName); return videoUrl(dongleId, hashedRouteName);
} }
export default {videoUrl, videoUrlForRouteUrl}; export default { videoUrl, videoUrlForRouteUrl };

View File

@ -1,4 +1,4 @@
import DBC from './models/can/dbc'; import DBC from "./models/can/dbc";
const CivicDbc = new DBC(` const CivicDbc = new DBC(`
VERSION "" VERSION ""
@ -345,4 +345,4 @@ VAL_ 829 BEEP 3 "single_beep" 2 "triple_beep" 1 "repeated_beep" 0 "no_beep" ;
VAL_ 927 ACC_ALERTS 29 "esp_active_acc_canceled" 10 "b_pedal_applied" 9 "speed_too_low" 8 "speed_too_high" 7 "p_brake_applied" 6 "gear_no_d" 5 "seatbelt" 4 "too_steep_downhill" 3 "too_steep_uphill" 2 "too_close" 1 "no_vehicle_ahead" ; VAL_ 927 ACC_ALERTS 29 "esp_active_acc_canceled" 10 "b_pedal_applied" 9 "speed_too_low" 8 "speed_too_high" 7 "p_brake_applied" 6 "gear_no_d" 5 "seatbelt" 4 "too_steep_downhill" 3 "too_steep_uphill" 2 "too_close" 1 "no_vehicle_ahead" ;
`); `);
export default CivicDbc; export default CivicDbc;

File diff suppressed because it is too large Load Diff

View File

@ -1,268 +1,306 @@
import React, {Component} from 'react'; import React, { Component } from "react";
import Measure from 'react-measure'; import Measure from "react-measure";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import cx from 'classnames'; import cx from "classnames";
import Signal from '../models/can/signal'; import Signal from "../models/can/signal";
import CanPlot from '../vega/CanPlot'; import CanPlot from "../vega/CanPlot";
const DefaultPlotInnerStyle = { const DefaultPlotInnerStyle = {
position: 'absolute', position: "absolute",
top: 0, top: 0,
left: 0, left: 0
}; };
export default class CanGraph extends Component { export default class CanGraph extends Component {
static emptyTable = []; static emptyTable = [];
static propTypes = { static propTypes = {
data: PropTypes.object, data: PropTypes.object,
messages: PropTypes.object, messages: PropTypes.object,
messageId: PropTypes.string, messageId: PropTypes.string,
messageName: PropTypes.string, messageName: PropTypes.string,
signalSpec: PropTypes.instanceOf(Signal), signalSpec: PropTypes.instanceOf(Signal),
segment: PropTypes.array, segment: PropTypes.array,
unplot: PropTypes.func, unplot: PropTypes.func,
onRelativeTimeClick: PropTypes.func, onRelativeTimeClick: PropTypes.func,
currentTime: PropTypes.number, currentTime: PropTypes.number,
onSegmentChanged: PropTypes.func, onSegmentChanged: PropTypes.func,
onDragStart: PropTypes.func, onDragStart: PropTypes.func,
onDragEnd: PropTypes.func, onDragEnd: PropTypes.func,
container: PropTypes.node, container: PropTypes.node,
dragPos: PropTypes.object, dragPos: PropTypes.object,
canReceiveGraphDrop: PropTypes.bool, canReceiveGraphDrop: PropTypes.bool,
onGraphRefAvailable: PropTypes.func, onGraphRefAvailable: PropTypes.func,
plottedSignals: PropTypes.array, plottedSignals: PropTypes.array
};
constructor(props) {
super(props);
this.state = {
plotInnerStyle: null,
shiftX: 0,
shiftY: 0,
bounds: null,
isDataInserted: false
}; };
this.onNewView = this.onNewView.bind(this);
this.onSignalClickTime = this.onSignalClickTime.bind(this);
this.onSignalSegment = this.onSignalSegment.bind(this);
this.onDragAnchorMouseDown = this.onDragAnchorMouseDown.bind(this);
this.onDragAnchorMouseUp = this.onDragAnchorMouseUp.bind(this);
this.onDragStart = this.onDragStart.bind(this);
this.onPlotResize = this.onPlotResize.bind(this);
}
constructor(props) { segmentIsNew(newSegment) {
super(props); return (
newSegment.length !== this.props.segment.length ||
!newSegment.every((val, idx) => this.props.segment[idx] === val)
);
}
this.state = { dataChanged(prevProps, nextProps) {
plotInnerStyle: null, return (
shiftX: 0, nextProps.data.series.length !== prevProps.data.series.length ||
shiftY: 0, !prevProps.signalSpec.equals(nextProps.signalSpec) ||
bounds: null, nextProps.data.updated !== this.props.data.updated
isDataInserted: false, );
}; }
this.onNewView = this.onNewView.bind(this);
this.onSignalClickTime = this.onSignalClickTime.bind(this);
this.onSignalSegment = this.onSignalSegment.bind(this);
this.onDragAnchorMouseDown = this.onDragAnchorMouseDown.bind(this);
this.onDragAnchorMouseUp = this.onDragAnchorMouseUp.bind(this);
this.onDragStart = this.onDragStart.bind(this);
this.onPlotResize = this.onPlotResize.bind(this);
}
segmentIsNew(newSegment) { visualChanged(prevProps, nextProps) {
return newSegment.length !== this.props.segment.length return (
|| !(newSegment.every((val, idx) => this.props.segment[idx] === val)); prevProps.canReceiveGraphDrop !== nextProps.canReceiveGraphDrop ||
} JSON.stringify(prevProps.dragPos) !== JSON.stringify(nextProps.dragPos)
);
}
dataChanged(prevProps, nextProps) { onPlotResize({ bounds }) {
return nextProps.data.series.length !== prevProps.data.series.length this.setState({ bounds });
|| !prevProps.signalSpec.equals(nextProps.signalSpec)
|| nextProps.data.updated !== this.props.data.updated;
}
visualChanged(prevProps, nextProps) { this.view.run();
return prevProps.canReceiveGraphDrop !== nextProps.canReceiveGraphDrop this.view.signal("width", bounds.width);
|| JSON.stringify(prevProps.dragPos) !== JSON.stringify(nextProps.dragPos); this.view.signal("height", 0.4 * bounds.width); // 5:2 aspect ratio
} this.view.run();
}
shouldComponentUpdate(nextProps, nextState) {
if (this.view) {
// only update if segment is new
let segmentChanged = false;
if (this.segmentIsNew(nextProps.segment)) {
if (nextProps.segment.length > 0) {
// Set segmented domain
this.view.signal("segment", nextProps.segment);
} else {
// Reset segment to full domain
this.view.signal("segment", 0);
}
segmentChanged = true;
}
onPlotResize({bounds}) { if (!nextProps.live && nextProps.currentTime !== this.props.currentTime) {
this.setState({bounds}); this.view.signal("videoTime", nextProps.currentTime);
segmentChanged = true;
}
if (segmentChanged) {
this.view.run(); this.view.run();
this.view.signal('width', bounds.width); }
this.view.signal('height', 0.4 * bounds.width); // 5:2 aspect ratio
this.view.run();
} }
shouldComponentUpdate(nextProps, nextState) { const dataChanged = this.dataChanged(this.props, nextProps);
if(this.view) {
// only update if segment is new
let segmentChanged = false;
if(this.segmentIsNew(nextProps.segment)) {
if(nextProps.segment.length > 0) {
// Set segmented domain
this.view.signal('segment', nextProps.segment)
} else {
// Reset segment to full domain
this.view.signal('segment', 0);
}
segmentChanged = true;
}
if(!nextProps.live && nextProps.currentTime !== this.props.currentTime) { return (
this.view.signal('videoTime', nextProps.currentTime); dataChanged ||
segmentChanged = true; JSON.stringify(this.state) !== JSON.stringify(nextState) ||
} this.visualChanged(this.props, nextProps)
);
}
if(segmentChanged) { insertData() {
this.view.run(); this.view.remove("table", () => true).run();
} this.view.insert("table", this.props.data.series).run();
} }
const dataChanged = this.dataChanged(this.props, nextProps); componentDidUpdate(prevProps, prevState) {
if (this.dataChanged(prevProps, this.props)) {
this.insertData();
}
}
return dataChanged componentWillReceiveProps(nextProps) {
|| JSON.stringify(this.state) !== JSON.stringify(nextState) if (
|| this.visualChanged(this.props, nextProps); nextProps.dragPos &&
JSON.stringify(nextProps.dragPos) !== JSON.stringify(this.props.dragPos)
) {
this.updateStyleFromDragPos(nextProps.dragPos);
} else if (!nextProps.dragPos && this.state.plotInnerStyle !== null) {
this.setState({ plotInnerStyle: null });
}
}
updateStyleFromDragPos({ left, top }) {
const plotInnerStyle = this.state.plotInnerStyle || {};
plotInnerStyle.left = left;
plotInnerStyle.top = top;
this.setState({ plotInnerStyle });
}
onNewView(view) {
this.view = view;
if (this.state.bounds) {
this.onPlotResize({ bounds: this.state.bounds });
}
if (this.props.segment.length > 0) {
view.signal("segment", this.props.segment);
} }
insertData() { this.insertData();
this.view.remove('table', () => true).run(); }
this.view.insert('table', this.props.data.series).run();
onSignalClickTime(signal, clickTime) {
if (clickTime !== undefined) {
this.props.onRelativeTimeClick(this.props.messageId, clickTime);
}
}
onSignalSegment(signal, segment) {
if (!Array.isArray(segment)) {
return;
} }
componentDidUpdate(prevProps, prevState) { this.props.onSegmentChanged(this.props.messageId, segment);
if(this.dataChanged(prevProps, this.props)) { if (this.view) {
this.insertData(); const state = this.view.getState();
} state.subcontext[0].signals.brush = 0;
this.view = this.view.setState(state);
} }
}
componentWillReceiveProps(nextProps) { plotInnerStyleFromMouseEvent(e) {
if(nextProps.dragPos && JSON.stringify(nextProps.dragPos) !== JSON.stringify(this.props.dragPos)) { const { shiftX, shiftY } = this.state;
this.updateStyleFromDragPos(nextProps.dragPos); const plotInnerStyle = { ...DefaultPlotInnerStyle };
} else if(!nextProps.dragPos && this.state.plotInnerStyle !== null) { const rect = this.props.container.getBoundingClientRect();
this.setState({plotInnerStyle: null});
}
}
updateStyleFromDragPos({left, top}) { const x = e.clientX - rect.left - shiftX;
const plotInnerStyle = this.state.plotInnerStyle || {}; const y = e.clientY - rect.top - shiftY;
plotInnerStyle.left = left; plotInnerStyle.left = x;
plotInnerStyle.top = top; plotInnerStyle.top = y;
this.setState({plotInnerStyle}); return plotInnerStyle;
} }
onNewView(view) { onDragAnchorMouseDown(e) {
this.view = view; e.persist();
if(this.state.bounds) { const shiftX = e.clientX - e.target.getBoundingClientRect().left;
this.onPlotResize({bounds: this.state.bounds}); const shiftY = e.clientY - e.target.getBoundingClientRect().top;
} this.setState({ shiftX, shiftY }, () => {
if(this.props.segment.length > 0) { this.setState({ plotInnerStyle: this.plotInnerStyleFromMouseEvent(e) });
view.signal('segment', this.props.segment); });
} this.props.onDragStart(
this.props.messageId,
this.props.signalSpec.uid,
shiftX,
shiftY
);
}
this.insertData(); onDragAnchorMouseUp(e) {
} this.props.onDragEnd();
this.setState({
plotInnerStyle: null,
shiftX: 0,
shiftY: 0
});
}
onSignalClickTime(signal, clickTime) { onDragStart(e) {
if(clickTime !== undefined) { e.preventDefault();
this.props.onRelativeTimeClick(this.props.messageId, clickTime); return false;
} }
}
onSignalSegment(signal, segment) { render() {
if(!Array.isArray(segment)) { const { plotInnerStyle } = this.state;
return; const canReceiveDropClass = this.props.canReceiveGraphDrop
} ? "is-droppable"
: null;
this.props.onSegmentChanged(this.props.messageId, segment); return (
if(this.view) { <div
const state = this.view.getState(); className="cabana-explorer-visuals-plot"
state.subcontext[0].signals.brush = 0; ref={this.props.onGraphRefAvailable}
this.view = this.view.setState(state); >
} <div
} className={cx(
"cabana-explorer-visuals-plot-inner",
canReceiveDropClass
)}
style={plotInnerStyle || null}
>
<div
className="cabana-explorer-visuals-plot-draganchor"
onMouseDown={this.onDragAnchorMouseDown}
>
<span className="fa fa-bars" />
</div>
{this.props.plottedSignals.map(
({ messageId, signalUid, messageName }) => {
const signal = Object.values(
this.props.messages[messageId].frame.signals
).find(s => s.uid === signalUid);
const { colors } = signal;
plotInnerStyleFromMouseEvent(e) { return (
const {shiftX, shiftY} = this.state; <div
const plotInnerStyle = {...DefaultPlotInnerStyle}; className="cabana-explorer-visuals-plot-header"
const rect = this.props.container.getBoundingClientRect(); key={messageId + "_" + signal.uid}
>
const x = e.clientX - rect.left - shiftX; <div className="cabana-explorer-visuals-plot-header-toggle">
const y = e.clientY - rect.top - shiftY; <button
plotInnerStyle.left = x; className="button--tiny"
plotInnerStyle.top = y; onClick={() => this.props.unplot(messageId, signalUid)}
return plotInnerStyle; >
} <span>Hide Plot</span>
</button>
onDragAnchorMouseDown(e) { </div>
e.persist(); <div className="cabana-explorer-visuals-plot-header-copy">
const shiftX = e.clientX - e.target.getBoundingClientRect().left; <div className="cabana-explorer-visuals-plot-message">
const shiftY = e.clientY - e.target.getBoundingClientRect().top; <span>{messageName}</span>
this.setState({shiftX, shiftY},
() => {
this.setState({plotInnerStyle: this.plotInnerStyleFromMouseEvent(e)});
});
this.props.onDragStart(this.props.messageId, this.props.signalSpec.uid, shiftX, shiftY);
}
onDragAnchorMouseUp(e) {
this.props.onDragEnd();
this.setState({plotInnerStyle: null,
shiftX: 0,
shiftY: 0});
}
onDragStart(e) {
e.preventDefault();
return false;
}
render() {
const {plotInnerStyle} = this.state;
const canReceiveDropClass = this.props.canReceiveGraphDrop ? 'is-droppable' : null;
return (
<div className='cabana-explorer-visuals-plot'
ref={this.props.onGraphRefAvailable}>
<div className={ cx('cabana-explorer-visuals-plot-inner', canReceiveDropClass) }
style={ plotInnerStyle || null }>
<div className='cabana-explorer-visuals-plot-draganchor'
onMouseDown={this.onDragAnchorMouseDown}>
<span className='fa fa-bars'></span>
</div> </div>
{this.props.plottedSignals.map(({ messageId, signalUid, messageName }) => { <div className="cabana-explorer-visuals-plot-signal">
const signal = Object.values(this.props.messages[messageId].frame.signals).find((s) => s.uid === signalUid); <div
const { colors } = signal; className="cabana-explorer-visuals-plot-signal-color"
style={{ background: `rgb(${colors}` }}
return ( />
<div className='cabana-explorer-visuals-plot-header' <strong>{signal.name}</strong>
key={messageId + '_' + signal.uid}> </div>
<div className='cabana-explorer-visuals-plot-header-toggle'> </div>
<button className='button--tiny'
onClick={ () => this.props.unplot(messageId, signalUid)}>
<span>Hide Plot</span>
</button>
</div>
<div className='cabana-explorer-visuals-plot-header-copy'>
<div className='cabana-explorer-visuals-plot-message'>
<span>{messageName}</span>
</div>
<div className='cabana-explorer-visuals-plot-signal'>
<div className='cabana-explorer-visuals-plot-signal-color'
style={{background: `rgb(${colors}`}}></div>
<strong>{signal.name}</strong>
</div>
</div>
</div>
)}
)}
<Measure
bounds
onResize={this.onPlotResize}>
{({measureRef}) => {
return (<div ref={measureRef}
className='cabana-explorer-visuals-plot-container'>
<CanPlot
logLevel={0}
data={{table: CanGraph.emptyTable}}
onNewView={this.onNewView}
onSignalClickTime={this.onSignalClickTime}
onSignalSegment={this.onSignalSegment}
renderer={'canvas'}
/>
</div>);
}
}
</Measure>
</div> </div>
</div> );
); }
} )}
<Measure bounds onResize={this.onPlotResize}>
{({ measureRef }) => {
return (
<div
ref={measureRef}
className="cabana-explorer-visuals-plot-container"
>
<CanPlot
logLevel={0}
data={{ table: CanGraph.emptyTable }}
onNewView={this.onNewView}
onSignalClickTime={this.onSignalClickTime}
onSignalSegment={this.onSignalSegment}
renderer={"canvas"}
/>
</div>
);
}}
</Measure>
</div>
</div>
);
}
} }

View File

@ -1,268 +1,310 @@
import React, {Component} from 'react'; import React, { Component } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import ReactList from 'react-list'; import ReactList from "react-list";
import cx from 'classnames'; import cx from "classnames";
export default class CanLog extends Component { export default class CanLog extends Component {
static ITEMS_PER_PAGE = 50; static ITEMS_PER_PAGE = 50;
static propTypes = { static propTypes = {
plottedSignals: PropTypes.array, plottedSignals: PropTypes.array,
segmentIndices: PropTypes.array, segmentIndices: PropTypes.array,
onSignalUnplotPressed: PropTypes.func, onSignalUnplotPressed: PropTypes.func,
onSignalPlotPressed: PropTypes.func, onSignalPlotPressed: PropTypes.func,
message: PropTypes.object, message: PropTypes.object,
messageIndex: PropTypes.number, messageIndex: PropTypes.number,
onMessageExpanded: PropTypes.func, onMessageExpanded: PropTypes.func
};
constructor(props) {
super(props);
// only want to display up to length elements at a time
// offset, length
this.state = {
length: 0,
expandedMessages: [],
messageHeights: [],
allPacketsExpanded: false
}; };
constructor(props) { this.renderLogListItemMessage = this.renderLogListItemMessage.bind(this);
super(props); this.addDisplayedMessages = this.addDisplayedMessages.bind(this);
// only want to display up to length elements at a time this.renderLogListItem = this.renderLogListItem.bind(this);
// offset, length this.renderLogList = this.renderLogList.bind(this);
this.onExpandAllChanged = this.onExpandAllChanged.bind(this);
this.toggleExpandAllPackets = this.toggleExpandAllPackets.bind(this);
this.toggleSignalPlot = this.toggleSignalPlot.bind(this);
}
this.state = { componentWillReceiveProps(nextProps) {
length: 0, if (nextProps.message && !this.props.message) {
expandedMessages: [], this.addDisplayedMessages();
messageHeights: [],
allPacketsExpanded: false
}
this.renderLogListItemMessage = this.renderLogListItemMessage.bind(this);
this.addDisplayedMessages = this.addDisplayedMessages.bind(this);
this.renderLogListItem = this.renderLogListItem.bind(this);
this.renderLogList = this.renderLogList.bind(this);
this.onExpandAllChanged = this.onExpandAllChanged.bind(this);
this.toggleExpandAllPackets = this.toggleExpandAllPackets.bind(this);
this.toggleSignalPlot = this.toggleSignalPlot.bind(this);
} }
}
componentWillReceiveProps(nextProps) { shouldComponentUpdate(nextProps, nextState) {
if(nextProps.message && !this.props.message) { const curMessageLength = this.props.message
this.addDisplayedMessages(); ? this.props.message.entries.length
} : 0;
} const nextMessageLength = nextProps.message
? nextProps.message.entries.length
: 0;
shouldComponentUpdate(nextProps, nextState) { const shouldUpdate =
const curMessageLength = this.props.message ? this.props.message.entries.length : 0; this.props.message !== nextProps.message ||
const nextMessageLength = nextProps.message ? nextProps.message.entries.length : 0; nextMessageLength !== curMessageLength ||
nextProps.messageIndex !== this.props.messageIndex ||
nextProps.plottedSignals.length !== this.props.plottedSignals.length ||
JSON.stringify(nextProps.segmentIndices) !==
JSON.stringify(this.props.segmentIndices) ||
JSON.stringify(nextState) !== JSON.stringify(this.state) ||
this.props.message !== nextProps.message ||
(this.props.message !== undefined &&
nextProps.message !== undefined &&
this.props.message.frame !== undefined &&
nextProps.message.frame !== undefined &&
JSON.stringify(this.props.message.frame) !==
JSON.stringify(nextProps.message.frame));
const shouldUpdate = this.props.message !== nextProps.message return shouldUpdate;
|| nextMessageLength !== curMessageLength }
|| nextProps.messageIndex !== this.props.messageIndex
|| nextProps.plottedSignals.length !== this.props.plottedSignals.length
|| JSON.stringify(nextProps.segmentIndices) !== JSON.stringify(this.props.segmentIndices)
|| JSON.stringify(nextState) !== JSON.stringify(this.state)
|| this.props.message !== nextProps.message
|| (this.props.message !== undefined
&& nextProps.message !== undefined
&& this.props.message.frame !== undefined
&& nextProps.message.frame !== undefined
&&
(
(JSON.stringify(this.props.message.frame) !== JSON.stringify(nextProps.message.frame))
));
return shouldUpdate; addDisplayedMessages() {
} const { length } = this.state;
const newLength = length + CanLog.ITEMS_PER_PAGE;
addDisplayedMessages() { this.setState({ length: newLength });
const {length} = this.state; }
const newLength = length + CanLog.ITEMS_PER_PAGE;
this.setState({length: newLength}); expandMessage(msg, msgIdx) {
} this.setState({
expandedMessages: this.state.expandedMessages.concat([msg.time])
});
this.props.onMessageExpanded();
}
expandMessage(msg, msgIdx) { collapseMessage(msg, msgIdx) {
this.setState({expandedMessages: this.state.expandedMessages.concat([msg.time])}) this.setState({
this.props.onMessageExpanded(); expandedMessages: this.state.expandedMessages.filter(
} expMsgTime => expMsgTime !== msg.time
collapseMessage(msg, msgIdx) {
this.setState({expandedMessages: this.state.expandedMessages
.filter((expMsgTime) => expMsgTime !== msg.time)})
}
isSignalPlotted(msgId, signalUid) {
const plottedSignal = this.props.plottedSignals.find((plot) =>
plot.some((signal) => signal.messageId === msgId && signal.signalUid === signalUid));
return plottedSignal !== undefined;
}
signalValuePretty(signal, value) {
if(signal.isFloat) {
return value.toFixed(3);
} else return value;
}
isMessageExpanded(msg) {
return this.state.expandedMessages.indexOf(msg.time) !== -1;
}
toggleSignalPlot(msg, signalUid, plotted) {
if (!plotted) {
this.props.onSignalPlotPressed(msg, signalUid);
} else {
this.props.onSignalUnplotPressed(msg, signalUid);
}
}
toggleExpandPacketSignals(msgEntry) {
if(!this.props.message.frame) {
return;
}
const msgIsExpanded = this.state.allPacketsExpanded || this.isMessageExpanded(msgEntry);
const msgHasSignals = Object.keys(this.props.message.frame.signals).length > 0;
if (msgIsExpanded && msgHasSignals) {
this.setState({expandedMessages: this.state.expandedMessages
.filter((expMsgTime) => expMsgTime !== msgEntry.time)})
} else if (msgHasSignals) {
this.setState({expandedMessages: this.state.expandedMessages.concat([msgEntry.time])})
this.props.onMessageExpanded();
} else { return; }
}
renderLogListItemSignals(msgEntry) {
const { message } = this.props;
return (
<div className='signals-log-list-signals'>
{ Object.entries(msgEntry.signals).map(
([name, value]) => {
const signal = message.frame.signals[name];
if (signal === undefined) {
// Signal removed?
return null;
}
const unit = signal.unit.length > 0 ? signal.unit : 'units';
const isPlotted = this.isSignalPlotted(message.id, signal.uid);
const plottedButtonClass = isPlotted ? null : 'button--alpha';
const plottedButtonText = isPlotted ? 'Hide Plot' : 'Show Plot';
return (
<div key={ name } className='signals-log-list-signal'>
<div className='signals-log-list-signal-message'>
<span>{ name }</span>
</div>
<div className='signals-log-list-signal-value'>
<span>
(<strong>{ this.signalValuePretty(signal, value) }</strong> { unit })
</span>
</div>
<div className='signals-log-list-signal-action'
onClick={ () => { this.toggleSignalPlot(message.id, signal.uid, isPlotted) } }>
<button className={ cx('button--tiny', plottedButtonClass) }>
<span>{ plottedButtonText }</span>
</button>
</div>
</div>
);
})}
</div>
) )
});
}
isSignalPlotted(msgId, signalUid) {
const plottedSignal = this.props.plottedSignals.find(plot =>
plot.some(
signal => signal.messageId === msgId && signal.signalUid === signalUid
)
);
return plottedSignal !== undefined;
}
signalValuePretty(signal, value) {
if (signal.isFloat) {
return value.toFixed(3);
} else return value;
}
isMessageExpanded(msg) {
return this.state.expandedMessages.indexOf(msg.time) !== -1;
}
toggleSignalPlot(msg, signalUid, plotted) {
if (!plotted) {
this.props.onSignalPlotPressed(msg, signalUid);
} else {
this.props.onSignalUnplotPressed(msg, signalUid);
} }
}
renderLogListItemMessage(msgEntry, key) { toggleExpandPacketSignals(msgEntry) {
const { message } = this.props; if (!this.props.message.frame) {
const msgIsExpanded = this.state.allPacketsExpanded || this.isMessageExpanded(msgEntry); return;
const msgHasSignals = Object.keys(msgEntry.signals).length > 0;
const hasSignalsClass = msgHasSignals ? 'has-signals' : null;
const expandedClass = msgIsExpanded ? 'is-expanded' : null;
const row = (
<div key={key} className={cx('signals-log-list-item', hasSignalsClass, expandedClass)}>
<div className='signals-log-list-item-header'
onClick={ () => { this.toggleExpandPacketSignals(msgEntry) } }>
<div className='signals-log-list-message'>
<strong>{(message.frame ? message.frame.name : null) || message.id}</strong>
</div>
<div className='signals-log-list-time'>
<span>[{msgEntry.relTime.toFixed(3)}]</span>
</div>
<div className='signals-log-list-bytes'>
<span className='t-mono'>{msgEntry.hexData}</span>
</div>
</div>
<div className='signals-log-list-item-body'>
{ msgIsExpanded ? this.renderLogListItemSignals(msgEntry) : null}
</div>
</div>
);
return row;
} }
const msgIsExpanded =
this.state.allPacketsExpanded || this.isMessageExpanded(msgEntry);
renderLogListItem(index, key) { const msgHasSignals =
let offset = this.props.messageIndex; Object.keys(this.props.message.frame.signals).length > 0;
if(offset === 0 && this.props.segmentIndices.length === 2) { if (msgIsExpanded && msgHasSignals) {
offset = this.props.segmentIndices[0]; this.setState({
} expandedMessages: this.state.expandedMessages.filter(
if((offset + index) < this.props.message.entries.length) { expMsgTime => expMsgTime !== msgEntry.time
return this.renderLogListItemMessage(this.props.message.entries[offset + index], key);
} else {
return null;
}
}
renderLogList(items, ref) {
return (
<div className='signals-log-list'>
<div className='signals-log-list-header'>
<div className='signals-log-list-message'>Message</div>
<div className='signals-log-list-time'>Time</div>
<div className='signals-log-list-bytes'>Bytes</div>
</div>
<div className='signals-log-list-items'
ref={ref}>
{items}
</div>
</div>
) )
});
} else if (msgHasSignals) {
this.setState({
expandedMessages: this.state.expandedMessages.concat([msgEntry.time])
});
this.props.onMessageExpanded();
} else {
return;
} }
}
listLength() { renderLogListItemSignals(msgEntry) {
const {segmentIndices, messageIndex} = this.props; const { message } = this.props;
if(messageIndex > 0) { return (
return this.props.message.entries.length - messageIndex; <div className="signals-log-list-signals">
} else if(segmentIndices.length === 2) { {Object.entries(msgEntry.signals).map(([name, value]) => {
return segmentIndices[1] - segmentIndices[0]; const signal = message.frame.signals[name];
} else if(this.props.message) { if (signal === undefined) {
return this.props.message.entries.length; // Signal removed?
} else { return null;
// no message yet }
return 0; const unit = signal.unit.length > 0 ? signal.unit : "units";
} const isPlotted = this.isSignalPlotted(message.id, signal.uid);
} const plottedButtonClass = isPlotted ? null : "button--alpha";
const plottedButtonText = isPlotted ? "Hide Plot" : "Show Plot";
onExpandAllChanged(e) { return (
this.setState({allPacketsExpanded: e.target.checked}); <div key={name} className="signals-log-list-signal">
} <div className="signals-log-list-signal-message">
<span>{name}</span>
toggleExpandAllPackets() { </div>
this.setState({allPacketsExpanded: !this.state.allPacketsExpanded}); <div className="signals-log-list-signal-value">
} <span>
(<strong>{this.signalValuePretty(signal, value)}</strong>{" "}
render() { {unit})
let expandAllText = this.state.allPacketsExpanded ? 'Collapse All' : 'Expand All'; </span>
let expandAllClass = this.state.allPacketsExpanded ? null : 'button--alpha'; </div>
return ( <div
<div className='cabana-explorer-signals-log'> className="signals-log-list-signal-action"
<div className='cabana-explorer-signals-log-header'> onClick={() => {
<strong>Message Packets</strong> this.toggleSignalPlot(message.id, signal.uid, isPlotted);
<button className={cx('button--tiny', expandAllClass)} }}
onClick={this.toggleExpandAllPackets}> >
{expandAllText} <button className={cx("button--tiny", plottedButtonClass)}>
</button> <span>{plottedButtonText}</span>
</div> </button>
<div className='cabana-explorer-signals-log-body'> </div>
<ReactList
itemRenderer={this.renderLogListItem}
itemsRenderer={this.renderLogList}
length={this.listLength()}
pageSize={50}
updateWhenThisValueChanges={this.props.messageIndex}
type='variable' />
</div> </div>
);
})}
</div>
);
}
renderLogListItemMessage(msgEntry, key) {
const { message } = this.props;
const msgIsExpanded =
this.state.allPacketsExpanded || this.isMessageExpanded(msgEntry);
const msgHasSignals = Object.keys(msgEntry.signals).length > 0;
const hasSignalsClass = msgHasSignals ? "has-signals" : null;
const expandedClass = msgIsExpanded ? "is-expanded" : null;
const row = (
<div
key={key}
className={cx("signals-log-list-item", hasSignalsClass, expandedClass)}
>
<div
className="signals-log-list-item-header"
onClick={() => {
this.toggleExpandPacketSignals(msgEntry);
}}
>
<div className="signals-log-list-message">
<strong>
{(message.frame ? message.frame.name : null) || message.id}
</strong>
</div> </div>
); <div className="signals-log-list-time">
<span>[{msgEntry.relTime.toFixed(3)}]</span>
</div>
<div className="signals-log-list-bytes">
<span className="t-mono">{msgEntry.hexData}</span>
</div>
</div>
<div className="signals-log-list-item-body">
{msgIsExpanded ? this.renderLogListItemSignals(msgEntry) : null}
</div>
</div>
);
return row;
}
renderLogListItem(index, key) {
let offset = this.props.messageIndex;
if (offset === 0 && this.props.segmentIndices.length === 2) {
offset = this.props.segmentIndices[0];
} }
if (offset + index < this.props.message.entries.length) {
return this.renderLogListItemMessage(
this.props.message.entries[offset + index],
key
);
} else {
return null;
}
}
renderLogList(items, ref) {
return (
<div className="signals-log-list">
<div className="signals-log-list-header">
<div className="signals-log-list-message">Message</div>
<div className="signals-log-list-time">Time</div>
<div className="signals-log-list-bytes">Bytes</div>
</div>
<div className="signals-log-list-items" ref={ref}>
{items}
</div>
</div>
);
}
listLength() {
const { segmentIndices, messageIndex } = this.props;
if (messageIndex > 0) {
return this.props.message.entries.length - messageIndex;
} else if (segmentIndices.length === 2) {
return segmentIndices[1] - segmentIndices[0];
} else if (this.props.message) {
return this.props.message.entries.length;
} else {
// no message yet
return 0;
}
}
onExpandAllChanged(e) {
this.setState({ allPacketsExpanded: e.target.checked });
}
toggleExpandAllPackets() {
this.setState({ allPacketsExpanded: !this.state.allPacketsExpanded });
}
render() {
let expandAllText = this.state.allPacketsExpanded
? "Collapse All"
: "Expand All";
let expandAllClass = this.state.allPacketsExpanded ? null : "button--alpha";
return (
<div className="cabana-explorer-signals-log">
<div className="cabana-explorer-signals-log-header">
<strong>Message Packets</strong>
<button
className={cx("button--tiny", expandAllClass)}
onClick={this.toggleExpandAllPackets}
>
{expandAllText}
</button>
</div>
<div className="cabana-explorer-signals-log-body">
<ReactList
itemRenderer={this.renderLogListItem}
itemsRenderer={this.renderLogList}
length={this.listLength()}
pageSize={50}
updateWhenThisValueChanges={this.props.messageIndex}
type="variable"
/>
</div>
</div>
);
}
} }

View File

@ -1,40 +1,42 @@
import React, {Component} from 'react'; import React, { Component } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
export default class DbcUpload extends Component { export default class DbcUpload extends Component {
static propTypes = { static propTypes = {
onDbcLoaded: PropTypes.func onDbcLoaded: PropTypes.func
};
constructor(props) {
super(props);
this.state = {
dbcText: ""
}; };
constructor(props) {
super(props);
this.state = {
dbcText: ''
};
this.onTextChanged = this.onTextChanged.bind(this); this.onTextChanged = this.onTextChanged.bind(this);
} }
onTextChanged(e) { onTextChanged(e) {
const dbcText = e.target.value; const dbcText = e.target.value;
this.setState({dbcText}) this.setState({ dbcText });
this.props.onDbcLoaded('from paste', dbcText); this.props.onDbcLoaded("from paste", dbcText);
} }
render() { render() {
return ( return (
<div className='cabana-dbc-upload-raw'> <div className="cabana-dbc-upload-raw">
<div className='form-field'> <div className="form-field">
<label htmlFor='raw_dbc_upload'> <label htmlFor="raw_dbc_upload">
<span>Raw DBC File:</span> <span>Raw DBC File:</span>
<sup>Paste your DBC text output within this box</sup> <sup>Paste your DBC text output within this box</sup>
</label> </label>
<textarea value={ this.state.dbcText } <textarea
id='raw_dbc_upload' value={this.state.dbcText}
className='t-mono' id="raw_dbc_upload"
placeholder='PASTE DBC FILE HERE' className="t-mono"
onChange={ this.onTextChanged } /> placeholder="PASTE DBC FILE HERE"
</div> onChange={this.onTextChanged}
</div> />
); </div>
} </div>
);
}
} }

View File

@ -1,123 +1,130 @@
import React, { Component } from 'react'; import React, { Component } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import Modal from './Modals/baseModal'; import Modal from "./Modals/baseModal";
export default class EditMessageModal extends Component { export default class EditMessageModal extends Component {
static propTypes = { static propTypes = {
handleClose: PropTypes.func.isRequired, handleClose: PropTypes.func.isRequired,
handleSave: PropTypes.func.isRequired, handleSave: PropTypes.func.isRequired,
message: PropTypes.object.isRequired, message: PropTypes.object.isRequired
};
constructor(props) {
super(props);
this.state = {
messageFrame: props.message.frame.copy()
}; };
this.handleSave = this.handleSave.bind(this);
this.editTransmitter = this.editTransmitter.bind(this);
this.addTransmitter = this.addTransmitter.bind(this);
this.renderActions = this.renderActions.bind(this);
}
constructor(props) { handleSave() {
super(props); this.props.handleSave(this.state.messageFrame);
}
this.state = { addTransmitter() {
messageFrame: props.message.frame.copy() const { messageFrame } = this.state;
} messageFrame.addTransmitter();
this.handleSave = this.handleSave.bind(this); this.setState({ messageFrame });
this.editTransmitter = this.editTransmitter.bind(this); }
this.addTransmitter = this.addTransmitter.bind(this);
this.renderActions = this.renderActions.bind(this);
}
handleSave() { editTransmitter(transmitter) {
this.props.handleSave(this.state.messageFrame); return;
} }
addTransmitter() { renderActions() {
const {messageFrame} = this.state; return (
messageFrame.addTransmitter(); <div>
this.setState({messageFrame}); <button className="button--inverted" onClick={this.props.handleClose}>
} Cancel
</button>
<button className="button--primary" onClick={this.handleSave}>
Save Message
</button>
</div>
);
}
editTransmitter(transmitter) { render() {
return; return (
} <Modal
title={`Edit Message: (${this.props.message.id})`}
renderActions() { subtitle="Make changes and update defaults of this message"
return ( handleClose={this.props.handleClose}
<div> handleSave={this.handleSave}
<button actions={this.renderActions()}
className='button--inverted' >
onClick={ this.props.handleClose }>Cancel</button> <div className="form-field">
<button <label htmlFor="message_name">
className='button--primary' <span>Name</span>
onClick={ this.handleSave }>Save Message</button> <sup>Customize the name of this message</sup>
</label>
<input
type="text"
id="message_name"
value={this.state.messageFrame.name}
onChange={e => {
const { messageFrame } = this.state;
messageFrame.name = e.target.value;
this.setState({ messageFrame });
}}
/>
</div> </div>
) <div className="form-field">
} <label htmlFor="message_size">
<span>Size</span>
render() { <sup>Add a size parameter to this message</sup>
return ( </label>
<Modal <input
title={ `Edit Message: (${ this.props.message.id })`} type="number"
subtitle='Make changes and update defaults of this message' id="message_size"
handleClose={ this.props.handleClose } value={this.state.messageFrame.size}
handleSave={ this.handleSave } onChange={e => {
actions={ this.renderActions() }> const { messageFrame } = this.state;
<div className='form-field'> if (e.target.value > 8) {
<label htmlFor='message_name'> return;
<span>Name</span> }
<sup>Customize the name of this message</sup> messageFrame.size = parseInt(e.target.value, 10);
</label> this.setState({ messageFrame });
<input }}
type='text' />
id='message_name' </div>
value={ this.state.messageFrame.name } <div className="form-field u-hidden">
onChange={ (e) => { <label htmlFor="message_transmitters">
const { messageFrame } = this.state; <span>Transmitters</span>
messageFrame.name = e.target.value; <sup>
this.setState({ messageFrame }); Add the physical ECU units that this message is coming from.
}} /> </sup>
</div> </label>
<div className='form-field'> <div className="form-field-inset">
<label htmlFor='message_size'> <ul className="form-field-inset-list">
<span>Size</span> {this.state.messageFrame.transmitters.map(transmitter => {
<sup>Add a size parameter to this message</sup> return (
</label> <li className="form-field-inset-list-item" key={transmitter}>
<input <div className="form-field-inset-list-item-title">
type='number' <span>{transmitter}</span>
id='message_size'
value={this.state.messageFrame.size}
onChange={(e) => {
const {messageFrame} = this.state;
if(e.target.value > 8) {
return;
}
messageFrame.size = parseInt(e.target.value, 10);
this.setState({ messageFrame });
}} />
</div>
<div className='form-field u-hidden'>
<label htmlFor='message_transmitters'>
<span>Transmitters</span>
<sup>Add the physical ECU units that this message is coming from.</sup>
</label>
<div className='form-field-inset'>
<ul className='form-field-inset-list'>
{ this.state.messageFrame.transmitters.map((transmitter) => {
return (
<li className='form-field-inset-list-item' key={ transmitter }>
<div className='form-field-inset-list-item-title'>
<span>{ transmitter }</span>
</div>
<div className='form-field-inset-list-item-action'>
<button className='button--tiny button--alpha'>
Edit
</button>
</div>
</li>
);
}) }
<button className='button--tiny button--alpha'>
<span><i className='fa fa-plus'></i> Add Transmitter</span>
</button>
</ul>
</div> </div>
</div> <div className="form-field-inset-list-item-action">
</Modal> <button className="button--tiny button--alpha">
); Edit
} </button>
</div>
</li>
);
})}
<button className="button--tiny button--alpha">
<span>
<i className="fa fa-plus" /> Add Transmitter
</span>
</button>
</ul>
</div>
</div>
</Modal>
);
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,84 +1,97 @@
import React, {Component} from 'react'; import React, { Component } from "react";
import cx from 'classnames'; import cx from "classnames";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import OpenDbc from '../api/OpenDbc'; import OpenDbc from "../api/OpenDbc";
export default class GithubDbcList extends Component { export default class GithubDbcList extends Component {
static propTypes = { static propTypes = {
onDbcLoaded: PropTypes.func.isRequired, onDbcLoaded: PropTypes.func.isRequired,
repo: PropTypes.string.isRequired, repo: PropTypes.string.isRequired,
openDbcClient: PropTypes.instanceOf(OpenDbc).isRequired openDbcClient: PropTypes.instanceOf(OpenDbc).isRequired
};
constructor(props) {
super(props);
this.state = {
paths: [],
selectedPath: null,
pathQuery: ""
}; };
constructor(props){ this.updatePathQuery = this.updatePathQuery.bind(this);
super(props); }
this.state = { componentWillReceiveProps(nextProps) {
paths: [], if (nextProps.repo !== this.props.repo) {
selectedPath: null, this.props.openDbcClient.list(nextProps.repo).then(paths => {
pathQuery: '' this.setState({ paths, selectedPath: null });
}; });
this.updatePathQuery = this.updatePathQuery.bind(this);
} }
}
componentWillReceiveProps(nextProps) { componentWillMount() {
if(nextProps.repo !== this.props.repo) { this.props.openDbcClient.list(this.props.repo).then(paths => {
this.props.openDbcClient.list(nextProps.repo).then((paths) => { paths = paths.filter(path => path.indexOf(".dbc") !== -1);
this.setState({paths, selectedPath: null}) this.setState({ paths });
}) });
} }
}
componentWillMount() { updatePathQuery(e) {
this.props.openDbcClient.list(this.props.repo).then((paths) => { this.setState({
paths = paths.filter((path) => path.indexOf(".dbc") !== -1); pathQuery: e.target.value
this.setState({paths}); });
}) }
}
updatePathQuery(e) { selectPath(path) {
this.setState({ this.setState({ selectedPath: path });
pathQuery: e.target.value, this.props.openDbcClient
}) .getDbcContents(path, this.props.repo)
} .then(dbcContents => {
this.props.onDbcLoaded(path, dbcContents);
});
}
selectPath(path) { render() {
this.setState({selectedPath: path}) return (
this.props.openDbcClient.getDbcContents(path, this.props.repo).then((dbcContents) => { <div className="cabana-dbc-list">
this.props.onDbcLoaded(path, dbcContents); <div className="cabana-dbc-list-header">
}) <a href={`https://github.com/${this.props.repo}`} target="_blank">
} <i className="fa fa-github" />
<span>{this.props.repo}</span>
render() { </a>
return ( <div className="form-field form-field--small">
<div className='cabana-dbc-list'> <input
<div className='cabana-dbc-list-header'> type="text"
<a href={ `https://github.com/${ this.props.repo }` } placeholder="Search DBC Files"
target='_blank'> onChange={this.updatePathQuery}
<i className='fa fa-github'></i> />
<span>{ this.props.repo }</span> </div>
</a> </div>
<div className='form-field form-field--small'> <div className="cabana-dbc-list-files">
<input type='text' {this.state.paths
placeholder='Search DBC Files' .filter(
onChange={ this.updatePathQuery }/> p =>
</div> (this.state.pathQuery === "") | p.includes(this.state.pathQuery)
)
.map(path => {
return (
<div
className={cx("cabana-dbc-list-file", {
"is-selected": this.state.selectedPath === path
})}
onClick={() => {
this.selectPath(path);
}}
key={path}
>
<span>{path}</span>
</div> </div>
<div className='cabana-dbc-list-files'> );
{ this.state.paths.filter(p => this.state.pathQuery === '' | p.includes(this.state.pathQuery)) })}
.map((path) => { </div>
return ( </div>
<div className={ cx('cabana-dbc-list-file', {'is-selected': this.state.selectedPath === path})} );
onClick={ () => { this.selectPath(path) } } }
key={path}>
<span>{ path }</span>
</div>
)
})}
</div>
</div>
);
}
} }

View File

@ -1,6 +1,6 @@
import React, {Component} from 'react'; import React, { Component } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import Hls from 'hls.js/lib'; import Hls from "hls.js/lib";
export default class HLS extends Component { export default class HLS extends Component {
static propTypes = { static propTypes = {
@ -19,13 +19,16 @@ export default class HLS extends Component {
}; };
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if( (nextProps.shouldRestart || nextProps.startTime !== this.props.startTime) if (
&& isFinite(nextProps.startTime)) { (nextProps.shouldRestart ||
nextProps.startTime !== this.props.startTime) &&
isFinite(nextProps.startTime)
) {
this.videoElement.currentTime = nextProps.startTime; this.videoElement.currentTime = nextProps.startTime;
this.props.onRestart(); this.props.onRestart();
} }
if(nextProps.playing) { if (nextProps.playing) {
this.videoElement.play(); this.videoElement.play();
} else { } else {
this.videoElement.pause(); this.videoElement.pause();
@ -37,20 +40,20 @@ export default class HLS extends Component {
this.player.loadSource(this.props.source); this.player.loadSource(this.props.source);
this.player.attachMedia(this.videoElement); this.player.attachMedia(this.videoElement);
// these events fire when video is playing // these events fire when video is playing
this.videoElement.addEventListener('waiting', this.props.onLoadStart); this.videoElement.addEventListener("waiting", this.props.onLoadStart);
this.videoElement.addEventListener('playing', this.props.onLoadEnd); this.videoElement.addEventListener("playing", this.props.onLoadEnd);
// these events fire when video is paused & seeked // these events fire when video is paused & seeked
this.videoElement.addEventListener('seeking', () => { this.videoElement.addEventListener("seeking", () => {
if(!this.props.playing) { if (!this.props.playing) {
this.props.onLoadStart(); this.props.onLoadStart();
this.props.onPlaySeek(this.videoElement.currentTime); this.props.onPlaySeek(this.videoElement.currentTime);
} }
}); });
let shouldInitVideoTime = true; let shouldInitVideoTime = true;
this.videoElement.addEventListener('seeked', () => { this.videoElement.addEventListener("seeked", () => {
if(!this.props.playing) { if (!this.props.playing) {
if(shouldInitVideoTime) { if (shouldInitVideoTime) {
this.videoElement.currentTime = this.props.startTime; this.videoElement.currentTime = this.props.startTime;
shouldInitVideoTime = false; shouldInitVideoTime = false;
} }
@ -59,7 +62,7 @@ export default class HLS extends Component {
}); });
this.props.onVideoElementAvailable(this.videoElement); this.props.onVideoElementAvailable(this.videoElement);
if(this.props.playing) { if (this.props.playing) {
this.videoElement.play(); this.videoElement.play();
} }
} }
@ -67,9 +70,14 @@ export default class HLS extends Component {
render() { render() {
return ( return (
<div <div
className='cabana-explorer-visuals-camera-wrapper' className="cabana-explorer-visuals-camera-wrapper"
onClick={this.props.onClick}> onClick={this.props.onClick}
<video ref={ (video) => { this.videoElement = video; } } /> >
<video
ref={video => {
this.videoElement = video;
}}
/>
</div> </div>
); );
} }

View File

@ -1,127 +1,129 @@
import React, { Component } from 'react'; import React, { Component } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import cx from 'classnames'; import cx from "classnames";
import DBC from '../models/can/dbc'; import DBC from "../models/can/dbc";
import OpenDbc from '../api/OpenDbc'; import OpenDbc from "../api/OpenDbc";
import Modal from './Modals/baseModal'; import Modal from "./Modals/baseModal";
import GithubDbcList from './GithubDbcList'; import GithubDbcList from "./GithubDbcList";
import DbcUpload from './DbcUpload'; import DbcUpload from "./DbcUpload";
export default class LoadDbcModal extends Component { export default class LoadDbcModal extends Component {
static propTypes = { static propTypes = {
handleClose: PropTypes.func.isRequired, handleClose: PropTypes.func.isRequired,
onDbcSelected: PropTypes.func.isRequired, onDbcSelected: PropTypes.func.isRequired,
openDbcClient: PropTypes.instanceOf(OpenDbc).isRequired, openDbcClient: PropTypes.instanceOf(OpenDbc).isRequired,
loginWithGithub: PropTypes.element.isRequired, loginWithGithub: PropTypes.element.isRequired
};
constructor(props) {
super(props);
this.state = {
tab: "OpenDBC",
tabs: ["OpenDBC", "GitHub", "Upload"],
dbc: null,
dbcSource: null,
userOpenDbcRepo: null
}; };
constructor(props) { this.onDbcLoaded = this.onDbcLoaded.bind(this);
super(props); this.handleSave = this.handleSave.bind(this);
this.state = { this.renderTabNavigation = this.renderTabNavigation.bind(this);
tab: 'OpenDBC', this.renderTabContent = this.renderTabContent.bind(this);
tabs: ['OpenDBC', 'GitHub', 'Upload'], this.renderActions = this.renderActions.bind(this);
dbc: null, }
dbcSource: null,
userOpenDbcRepo: null,
}
this.onDbcLoaded = this.onDbcLoaded.bind(this); componentWillMount() {
this.handleSave = this.handleSave.bind(this); this.props.openDbcClient.getUserOpenDbcFork().then(userOpenDbcRepo => {
this.renderTabNavigation = this.renderTabNavigation.bind(this); this.setState({ userOpenDbcRepo });
this.renderTabContent = this.renderTabContent.bind(this); });
this.renderActions = this.renderActions.bind(this); }
onDbcLoaded(dbcSource, dbcText) {
const dbc = new DBC(dbcText);
this.setState({ dbcSource, dbc });
}
handleSave() {
const { dbc, dbcSource } = this.state;
if (dbc !== null) {
this.props.onDbcSelected(dbcSource, dbc);
} }
}
componentWillMount() { renderTabNavigation() {
this.props.openDbcClient.getUserOpenDbcFork().then((userOpenDbcRepo) => { return (
this.setState({userOpenDbcRepo}); <div className="cabana-tabs-navigation">
}); {this.state.tabs.map(tab => {
}
onDbcLoaded(dbcSource, dbcText) {
const dbc = new DBC(dbcText);
this.setState({dbcSource, dbc})
}
handleSave() {
const { dbc, dbcSource } = this.state;
if(dbc !== null) {
this.props.onDbcSelected(dbcSource, dbc);
}
}
renderTabNavigation() {
return (
<div className='cabana-tabs-navigation'>
{ this.state.tabs.map((tab) => {
return (
<a className={ cx({'is-active': this.state.tab === tab})}
onClick={ () => { this.setState({ tab }) }}
key={tab}>
<span>{ tab }</span>
</a>
)
})}
</div>
)
}
renderTabContent() {
const { tab } = this.state;
if (tab === 'OpenDBC') {
return (
<GithubDbcList
onDbcLoaded={ this.onDbcLoaded }
repo="commaai/opendbc"
openDbcClient={ this.props.openDbcClient } />
);
} else if (tab === 'GitHub') {
if (!this.props.openDbcClient.hasAuth()) {
return this.props.loginWithGithub;
} else if (this.state.userOpenDbcRepo === null) {
return (<div>Fork it</div>);
} else {
return (
<GithubDbcList
onDbcLoaded={this.onDbcLoaded}
repo={this.state.userOpenDbcRepo}
openDbcClient={this.props.openDbcClient} />
);
}
} else if (tab === 'Upload') {
return ( return (
<DbcUpload <a
onDbcLoaded={this.onDbcLoaded} /> className={cx({ "is-active": this.state.tab === tab })}
onClick={() => {
this.setState({ tab });
}}
key={tab}
>
<span>{tab}</span>
</a>
); );
} })}
} </div>
);
}
renderActions() { renderTabContent() {
const { tab } = this.state;
if (tab === "OpenDBC") {
return (
<GithubDbcList
onDbcLoaded={this.onDbcLoaded}
repo="commaai/opendbc"
openDbcClient={this.props.openDbcClient}
/>
);
} else if (tab === "GitHub") {
if (!this.props.openDbcClient.hasAuth()) {
return this.props.loginWithGithub;
} else if (this.state.userOpenDbcRepo === null) {
return <div>Fork it</div>;
} else {
return ( return (
<div> <GithubDbcList
<button className='button--inverted' onDbcLoaded={this.onDbcLoaded}
onClick={ this.props.handleClose }> repo={this.state.userOpenDbcRepo}
<span>Cancel</span> openDbcClient={this.props.openDbcClient}
</button> />
<button className='button--primary'
onClick={ this.handleSave }>
<span>Load DBC</span>
</button>
</div>
); );
}
} else if (tab === "Upload") {
return <DbcUpload onDbcLoaded={this.onDbcLoaded} />;
} }
}
render() { renderActions() {
return ( return (
<Modal <div>
title='Load DBC File' <button className="button--inverted" onClick={this.props.handleClose}>
subtitle='Modify an existing DBC file with Cabana' <span>Cancel</span>
handleClose={ this.props.handleClose } </button>
navigation={ this.renderTabNavigation() } <button className="button--primary" onClick={this.handleSave}>
actions={ this.renderActions() }> <span>Load DBC</span>
{ this.renderTabContent() } </button>
</Modal> </div>
); );
} }
render() {
return (
<Modal
title="Load DBC File"
subtitle="Modify an existing DBC file with Cabana"
handleClose={this.props.handleClose}
navigation={this.renderTabNavigation()}
actions={this.renderActions()}
>
{this.renderTabContent()}
</Modal>
);
}
} }

View File

@ -1,42 +1,42 @@
import React, { Component } from 'react'; import React, { Component } from "react";
import { css, StyleSheet } from 'aphrodite/no-important'; import { css, StyleSheet } from "aphrodite/no-important";
const keyframes = { const keyframes = {
'0%': { "0%": {
transform: 'translateX(0)' transform: "translateX(0)"
}, },
to: { to: {
transform: 'translateX(-400px)' transform: "translateX(-400px)"
} }
}; };
const animationColor1 = 'RGBA(74, 242, 161, 1.00)'; const animationColor1 = "RGBA(74, 242, 161, 1.00)";
const animationColor2 = 'RGBA(140, 169, 197, 1.00)'; const animationColor2 = "RGBA(140, 169, 197, 1.00)";
const Styles = StyleSheet.create({ const Styles = StyleSheet.create({
loadingBar: { loadingBar: {
display: 'block', display: "block",
animationName: [keyframes], animationName: [keyframes],
animationDuration: '2s', animationDuration: "2s",
animationTimingFunction: 'linear', animationTimingFunction: "linear",
animationIterationCount: 'infinite', animationIterationCount: "infinite",
backgroundColor: animationColor1, backgroundColor: animationColor1,
backgroundImage: `linear-gradient(to right, backgroundImage: `linear-gradient(to right,
${animationColor2} 0, ${animationColor2} 0,
${animationColor2} 50%, ${animationColor2} 50%,
${animationColor1} 50%, ${animationColor1} 50%,
${animationColor1} 100%)`, ${animationColor1} 100%)`,
backgroundRepeat: 'repeat-x', backgroundRepeat: "repeat-x",
backgroundSize: '25pc 25pc', backgroundSize: "25pc 25pc",
width: '200%', width: "200%",
position: 'fixed', position: "fixed",
top: 0, top: 0,
left: 0, left: 0,
height: 2 height: 2
} }
}); });
export default class LoadingBar extends Component { export default class LoadingBar extends Component {
render() { render() {
return (<div className={css(Styles.loadingBar)}></div>) return <div className={css(Styles.loadingBar)} />;
} }
} }

View File

@ -1,113 +1,124 @@
import React, {Component} from 'react'; import React, { Component } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
export default class MessageBytes extends Component { export default class MessageBytes extends Component {
static propTypes = { static propTypes = {
seekTime: PropTypes.number.isRequired, seekTime: PropTypes.number.isRequired,
message: PropTypes.object.isRequired, message: PropTypes.object.isRequired,
seekIndex: PropTypes.number, seekIndex: PropTypes.number,
live: PropTypes.bool.isRequired, live: PropTypes.bool.isRequired
};
constructor(props) {
super(props);
this.state = {
isVisible: true,
lastMessageIndex: 0,
lastSeekTime: 0
}; };
constructor(props) { this.onVisibilityChange = this.onVisibilityChange.bind(this);
super(props); this.onCanvasRefAvailable = this.onCanvasRefAvailable.bind(this);
this.state = { }
isVisible: true,
lastMessageIndex: 0,
lastSeekTime: 0,
};
this.onVisibilityChange = this.onVisibilityChange.bind(this); shouldComponentUpdate(nextProps, nextState) {
this.onCanvasRefAvailable = this.onCanvasRefAvailable.bind(this); if (nextProps.live) {
const nextLastEntry =
nextProps.message.entries[nextProps.message.entries.length - 1];
const curLastEntry = this.props.message.entries[
this.props.message.entries.length - 1
];
return nextLastEntry.hexData !== curLastEntry.hexData;
} else {
return nextProps.seekTime !== this.props.seekTime;
} }
}
shouldComponentUpdate(nextProps, nextState) { componentWillReceiveProps(nextProps) {
if(nextProps.live) { this.updateCanvas(nextProps);
const nextLastEntry = nextProps.message.entries[nextProps.message.entries.length - 1]; }
const curLastEntry = this.props.message.entries[this.props.message.entries.length - 1];
return (nextLastEntry.hexData !== curLastEntry.hexData); findMostRecentMessage(seekTime) {
} else { const { message } = this.props;
return nextProps.seekTime !== this.props.seekTime const { lastMessageIndex, lastSeekTime } = this.state;
let mostRecentMessageIndex = null;
if (seekTime >= lastSeekTime) {
for (let i = lastMessageIndex; i < message.entries.length; i++) {
const msg = message.entries[i];
if (msg && msg.relTime >= seekTime) {
mostRecentMessageIndex = i;
break;
} }
}
} }
componentWillReceiveProps(nextProps) { if (!mostRecentMessageIndex) {
this.updateCanvas(nextProps); // TODO this can be faster with binary search, not currently a bottleneck though.
mostRecentMessageIndex = message.entries.findIndex(
e => e.relTime >= seekTime
);
} }
findMostRecentMessage(seekTime) { if (mostRecentMessageIndex) {
const {message} = this.props; this.setState({
const {lastMessageIndex, lastSeekTime} = this.state; lastMessageIndex: mostRecentMessageIndex,
let mostRecentMessageIndex = null; lastSeekTime: seekTime
if(seekTime >= lastSeekTime) { });
for(let i = lastMessageIndex; i < message.entries.length; i++) { return message.entries[mostRecentMessageIndex];
const msg = message.entries[i]; }
if(msg && msg.relTime >= seekTime) { }
mostRecentMessageIndex = i;
break;
}
}
}
if(!mostRecentMessageIndex) { updateCanvas(props) {
// TODO this can be faster with binary search, not currently a bottleneck though. const { message, live, seekTime } = props;
if (!this.canvas || message.entries.length === 0) return;
mostRecentMessageIndex = message.entries.findIndex((e) => e.relTime >= seekTime); let mostRecentMsg = message.entries[message.entries.length - 1];
} if (!live) {
mostRecentMsg = this.findMostRecentMessage(seekTime);
if(mostRecentMessageIndex) { if (!mostRecentMsg) {
this.setState({lastMessageIndex: mostRecentMessageIndex, lastSeekTime: seekTime}); mostRecentMsg = message.entries[0];
return message.entries[mostRecentMessageIndex]; }
}
} }
updateCanvas(props) { const ctx = this.canvas.getContext("2d");
const {message, live, seekTime} = props; ctx.clearRect(0, 0, 180, 15);
if(!this.canvas || message.entries.length === 0) return; for (let i = 0; i < message.byteStateChangeCounts.length; i++) {
const hexData = mostRecentMsg.hexData.substr(i * 2, 2);
ctx.fillStyle = message.byteColors[i];
let mostRecentMsg = message.entries[message.entries.length - 1]; ctx.fillRect(i * 20, 0, 20, 15);
if(!live) {
mostRecentMsg = this.findMostRecentMessage(seekTime);
if(!mostRecentMsg) { ctx.font = "12px Courier";
mostRecentMsg = message.entries[0]; ctx.fillStyle = "white";
} ctx.fillText(hexData, i * 20 + 2, 12);
}
const ctx = this.canvas.getContext('2d');
ctx.clearRect(0,0,180,15);
for(let i = 0; i < message.byteStateChangeCounts.length; i++) {
const hexData = mostRecentMsg.hexData.substr(i * 2, 2);
ctx.fillStyle = message.byteColors[i];
ctx.fillRect(i * 20, 0, 20, 15);
ctx.font = '12px Courier';
ctx.fillStyle = 'white';
ctx.fillText(hexData, i * 20 + 2, 12);
}
} }
}
onVisibilityChange(isVisible) { onVisibilityChange(isVisible) {
if(isVisible !== this.state.isVisible) { if (isVisible !== this.state.isVisible) {
this.setState({isVisible}); this.setState({ isVisible });
}
} }
}
onCanvasRefAvailable(ref) { onCanvasRefAvailable(ref) {
if(!ref) return; if (!ref) return;
this.canvas = ref; this.canvas = ref;
this.canvas.width = 160 * window.devicePixelRatio; this.canvas.width = 160 * window.devicePixelRatio;
this.canvas.height = 15 * window.devicePixelRatio; this.canvas.height = 15 * window.devicePixelRatio;
const ctx = this.canvas.getContext('2d'); const ctx = this.canvas.getContext("2d");
ctx.scale(window.devicePixelRatio, window.devicePixelRatio); ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
} }
render() { render() {
return (<canvas ref={this.onCanvasRefAvailable} return (
className='cabana-meta-messages-list-item-bytes-canvas'></canvas>); <canvas
} ref={this.onCanvasRefAvailable}
className="cabana-meta-messages-list-item-bytes-canvas"
/>
);
}
} }

View File

@ -1,9 +1,9 @@
import React, {Component} from 'react'; import React, { Component } from "react";
import { StyleSheet, css } from 'aphrodite/no-important'; import { StyleSheet, css } from "aphrodite/no-important";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import GlobalStyles from '../styles/styles'; import GlobalStyles from "../styles/styles";
import Images from '../styles/images'; import Images from "../styles/images";
export default class Modal extends Component { export default class Modal extends Component {
static propTypes = { static propTypes = {
@ -21,112 +21,119 @@ export default class Modal extends Component {
} }
_onKeyDown(e) { _onKeyDown(e) {
if(e.keyCode === 27){ // escape if (e.keyCode === 27) {
this.props.onCancel() // escape
this.props.onCancel();
} }
} }
componentWillMount() { componentWillMount() {
document.addEventListener('keydown', this._onKeyDown); document.addEventListener("keydown", this._onKeyDown);
} }
componentWillUnmount() { componentWillUnmount() {
document.removeEventListener('keydown', this._onKeyDown); document.removeEventListener("keydown", this._onKeyDown);
} }
selectButton() { selectButton() {
const {continueEnabled, continueText, onContinue} = this.props; const { continueEnabled, continueText, onContinue } = this.props;
let style; let style;
if(continueEnabled) { if (continueEnabled) {
style = Styles.selectButtonEnabled; style = Styles.selectButtonEnabled;
} else { } else {
style = Styles.selectButtonDisabled; style = Styles.selectButtonDisabled;
} }
return (<div className={css(GlobalStyles.button, Styles.selectButton, style)} return (
onClick={continueEnabled ? this.props.onContinue : () => {}}> <div
<p>{continueText || 'Continue'}</p> className={css(GlobalStyles.button, Styles.selectButton, style)}
</div>); onClick={continueEnabled ? this.props.onContinue : () => {}}
>
<p>{continueText || "Continue"}</p>
</div>
);
} }
render() { render() {
return (<div className={css(Styles.root)}> return (
<div className={css(Styles.bg)}></div> <div className={css(Styles.root)}>
<div className={css(Styles.box)}> <div className={css(Styles.bg)} />
<div className={css(Styles.header)}> <div className={css(Styles.box)}>
<p className={css(Styles.title)}>{this.props.title}</p> <div className={css(Styles.header)}>
<Images.clear styles={[Styles.closeButton]} <p className={css(Styles.title)}>{this.props.title}</p>
onClick={this.props.onCancel} /> <Images.clear
</div> styles={[Styles.closeButton]}
{this.props.children} onClick={this.props.onCancel}
<div className={css(Styles.select)}> />
{this.selectButton()} </div>
<div className={css(Styles.finishButton, Styles.cancelButton)}> {this.props.children}
<p onClick={this.props.onCancel}>Cancel</p> <div className={css(Styles.select)}>
</div> {this.selectButton()}
</div> <div className={css(Styles.finishButton, Styles.cancelButton)}>
</div> <p onClick={this.props.onCancel}>Cancel</p>
</div>); </div>
</div>
</div>
</div>
);
} }
}; }
const Styles = StyleSheet.create({ const Styles = StyleSheet.create({
bg: { bg: {
position: 'absolute', position: "absolute",
left: 0, left: 0,
top: 0, top: 0,
zIndex: 9, zIndex: 9,
width: '100%', width: "100%",
height: '100%', height: "100%",
backgroundColor: 'white', backgroundColor: "white",
opacity: 0.75 opacity: 0.75
}, },
title: { title: {
fontSize: 20, fontSize: 20,
marginBottom: 10, marginBottom: 10,
marginRight: 'auto' marginRight: "auto"
},
closeButton: {
}, },
closeButton: {},
box: { box: {
position: 'absolute', position: "absolute",
left: '50%', left: "50%",
top: '50%', top: "50%",
transform: 'translate(-50%, -50%)', transform: "translate(-50%, -50%)",
zIndex: 10, zIndex: 10,
backgroundColor: 'white', backgroundColor: "white",
borderRadius: '4px', borderRadius: "4px",
border: '1px solid #000', border: "1px solid #000",
boxShadow: '1px 1px 1px #000', boxShadow: "1px 1px 1px #000",
padding: 20, padding: 20,
minWidth: 480 minWidth: 480
}, },
header: { header: {
display: 'flex', display: "flex",
flexDirection: 'row', flexDirection: "row",
justifyContent: 'flex-end' justifyContent: "flex-end"
}, },
select: { select: {
paddingTop: 20, paddingTop: 20,
display: 'flex', display: "flex",
flexDirection: 'row' flexDirection: "row"
}, },
finishButton: { finishButton: {
borderRadius: 5, borderRadius: 5,
height: 40, height: 40,
width: 80, width: 80,
cursor: 'pointer', cursor: "pointer",
display: 'flex', display: "flex",
justifyContent: 'center', justifyContent: "center",
alignItems: 'center' alignItems: "center"
}, },
selectButton: { selectButton: {
backgroundColor: 'rgb(77,144,254)', backgroundColor: "rgb(77,144,254)",
color: 'white' color: "white"
}, },
selectButtonDisabled: { selectButtonDisabled: {
cursor: 'default', cursor: "default",
opacity: 0.5 opacity: 0.5
} }
}); });

View File

@ -1,295 +1,376 @@
import React, { Component } from 'react'; import React, { Component } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import Moment from 'moment'; import Moment from "moment";
import _ from 'lodash'; import _ from "lodash";
import cx from 'classnames'; import cx from "classnames";
import auth from '../../api/comma-auth'; import auth from "../../api/comma-auth";
import Modal from '../Modals/baseModal'; import Modal from "../Modals/baseModal";
export default class OnboardingModal extends Component { export default class OnboardingModal extends Component {
static propTypes = { static propTypes = {
handlePandaConnect: PropTypes.func, handlePandaConnect: PropTypes.func,
routes: PropTypes.array, routes: PropTypes.array
};
static instructionalImages = {
step2: require("../../images/webusb-enable-experimental-features.png"),
step3: require("../../images/webusb-enable-webusb.png")
};
constructor(props) {
super(props);
this.state = {
webUsbEnabled: !!navigator.usb,
viewingUsbInstructions: false,
pandaConnected: false,
chffrDrivesSearch: "",
chffrDrivesSortBy: "start_time",
chffrDrivesOrderDesc: true
}; };
static instructionalImages = { this.attemptPandaConnection = this.attemptPandaConnection.bind(this);
step2: require("../../images/webusb-enable-experimental-features.png"), this.toggleUsbInstructions = this.toggleUsbInstructions.bind(this);
step3: require("../../images/webusb-enable-webusb.png"), this.handleSortDrives = this.handleSortDrives.bind(this);
this.handleSearchDrives = this.handleSearchDrives.bind(this);
this.navigateToAuth = this.navigateToAuth.bind(this);
this.openChffrDrive = this.openChffrDrive.bind(this);
}
attemptPandaConnection() {
if (!this.state.webUsbEnabled) {
return;
} }
this.props.handlePandaConnect();
}
constructor(props) { toggleUsbInstructions() {
super(props); this.setState({
viewingUsbInstructions: !this.state.viewingUsbInstructions
});
}
this.state = { navigateToAuth() {
webUsbEnabled: !!navigator.usb, const authUrl = auth.authUrl();
viewingUsbInstructions: false, window.location.href = authUrl;
pandaConnected: false, }
chffrDrivesSearch: '',
chffrDrivesSortBy: 'start_time',
chffrDrivesOrderDesc: true,
}
this.attemptPandaConnection = this.attemptPandaConnection.bind(this); filterRoutesWithCan(drive) {
this.toggleUsbInstructions = this.toggleUsbInstructions.bind(this); return drive.can === true;
this.handleSortDrives = this.handleSortDrives.bind(this); }
this.handleSearchDrives = this.handleSearchDrives.bind(this);
this.navigateToAuth = this.navigateToAuth.bind(this); handleSearchDrives(drive) {
this.openChffrDrive = this.openChffrDrive.bind(this); const { chffrDrivesSearch } = this.state;
const searchKeywords = chffrDrivesSearch
.split(" ")
.filter(s => s.length > 0)
.map(s => s.toLowerCase());
return (
searchKeywords.length === 0 ||
searchKeywords.some(
kw =>
drive.end_geocode.toLowerCase().indexOf(kw) !== -1 ||
drive.start_geocode.toLowerCase().indexOf(kw) !== -1 ||
Moment(drive.start_time)
.format("dddd MMMM Do YYYY")
.toLowerCase()
.indexOf(kw) !== -1 ||
Moment(drive.end_time)
.format("dddd MMMM Do YYYY")
.toLowerCase()
.indexOf(kw) !== -1
)
);
}
handleSortDrives(key) {
if (this.state.chffrDrivesSortBy === key) {
this.setState({ chffrDrivesOrderDesc: !this.state.chffrDrivesOrderDesc });
} else {
this.setState({ chffrDrivesOrderDesc: true });
this.setState({ chffrDrivesSortBy: key });
} }
}
attemptPandaConnection() { openChffrDrive(route) {
if (!this.state.webUsbEnabled) { return }; window.location.search = `?route=${route.fullname}`;
this.props.handlePandaConnect(); }
renderPandaEligibility() {
const { webUsbEnabled, pandaConnected } = this.state;
const { attemptingPandaConnection } = this.props;
if (!webUsbEnabled) {
return (
<p>
<i className="fa fa-exclamation-triangle" />
<a onClick={this.toggleUsbInstructions}>
<span>WebUSB is not enabled in your Chrome settings</span>
</a>
</p>
);
} else if (!pandaConnected && attemptingPandaConnection) {
return (
<p>
<i className="fa fa-spinner animate-spin" />
<span className="animate-pulse-opacity">
Waiting for panda USB connection
</span>
</p>
);
} }
}
toggleUsbInstructions() { renderChffrOption() {
this.setState({ viewingUsbInstructions: !this.state.viewingUsbInstructions }); const { routes } = this.props;
} if (routes.length > 0) {
return (
navigateToAuth() { <div className="cabana-onboarding-mode-chffr">
const authUrl = auth.authUrl(); <div className="cabana-onboarding-mode-chffr-search">
window.location.href = authUrl; <div className="form-field--small">
} <input
type="text"
filterRoutesWithCan(drive) { id="chffr_drives_search"
return drive.can === true; placeholder="Search chffr drives"
} value={this.state.chffrDrivesSearch}
onChange={e =>
handleSearchDrives(drive) { this.setState({ chffrDrivesSearch: e.target.value })
const { chffrDrivesSearch } = this.state; }
const searchKeywords = chffrDrivesSearch.split(" ") />
.filter((s) => s.length > 0) <div className="cabana-onboarding-mode-chffr-search-helper">
.map((s) => s.toLowerCase()); <p>(Try: "Drives in San Francisco" or "Drives in June 2017")</p>
</div>
return searchKeywords.length === 0 ||
searchKeywords.some((kw) => (
drive.end_geocode.toLowerCase().indexOf(kw) !== -1
|| drive.start_geocode.toLowerCase().indexOf(kw) !== -1
|| Moment(drive.start_time).format('dddd MMMM Do YYYY').toLowerCase().indexOf(kw) !== -1
|| Moment(drive.end_time).format('dddd MMMM Do YYYY').toLowerCase().indexOf(kw) !== -1));
}
handleSortDrives(key) {
if (this.state.chffrDrivesSortBy === key) {
this.setState({ chffrDrivesOrderDesc: !this.state.chffrDrivesOrderDesc });
} else {
this.setState({ chffrDrivesOrderDesc: true });
this.setState({ chffrDrivesSortBy: key });
}
}
openChffrDrive(route) {
window.location.search = `?route=${ route.fullname }`;
}
renderPandaEligibility() {
const { webUsbEnabled, pandaConnected } = this.state;
const { attemptingPandaConnection } = this.props;
if (!webUsbEnabled) {
return (
<p>
<i className='fa fa-exclamation-triangle'></i>
<a onClick={ this.toggleUsbInstructions }>
<span>WebUSB is not enabled in your Chrome settings</span>
</a>
</p>
)
}
else if (!pandaConnected && attemptingPandaConnection) {
return (
<p>
<i className='fa fa-spinner animate-spin'></i>
<span className='animate-pulse-opacity'>Waiting for panda USB connection</span>
</p>
)
}
}
renderChffrOption() {
const { routes } = this.props;
if (routes.length > 0) {
return (
<div className='cabana-onboarding-mode-chffr'>
<div className='cabana-onboarding-mode-chffr-search'>
<div className='form-field--small'>
<input type='text'
id='chffr_drives_search'
placeholder='Search chffr drives'
value={ this.state.chffrDrivesSearch }
onChange={ (e) =>
this.setState({ chffrDrivesSearch: e.target.value }) } />
<div className='cabana-onboarding-mode-chffr-search-helper'>
<p>(Try: "Drives in San Francisco" or "Drives in June 2017")</p>
</div>
</div>
</div>
<div className={ cx('cabana-onboarding-mode-chffr-header', {
'is-ordered-desc': this.state.chffrDrivesOrderDesc,
'is-ordered-asc': !this.state.chffrDrivesOrderDesc
}) }>
<div className={ cx('cabana-onboarding-mode-chffr-drive-date', {
'is-sorted': this.state.chffrDrivesSortBy === 'start_time',
}) }
onClick={ () => this.handleSortDrives('start_time') }>
<span>Date</span>
</div>
<div className={ cx('cabana-onboarding-mode-chffr-drive-places', {
'is-sorted': this.state.chffrDrivesSortBy === 'end_geocode'
}) }
onClick={ () => this.handleSortDrives('end_geocode') }>
<span>Places</span>
</div>
<div className={ cx('cabana-onboarding-mode-chffr-drive-time') }>
<span>Time</span>
</div>
<div className={ cx('cabana-onboarding-mode-chffr-drive-distance', {
'is-sorted': this.state.chffrDrivesSortBy === 'len'
}) }
onClick={ () => this.handleSortDrives('len') }>
<span>Distance</span>
</div>
<div className='cabana-onboarding-mode-chffr-drive-action'></div>
</div>
<ul className='cabana-onboarding-mode-chffr-drives'>
{ _.orderBy(routes, [this.state.chffrDrivesSortBy], [this.state.chffrDrivesOrderDesc ? 'desc' : 'asc'])
.filter(this.filterRoutesWithCan)
.filter(this.handleSearchDrives)
.map((route) => {
const routeDuration = Moment.duration(route.end_time.diff(route.start_time));
const routeStartClock = Moment(route.start_time).format('LT');
const routeEndClock = Moment(route.end_time).format('LT');
return (
<li key={ route.fullname }
className='cabana-onboarding-mode-chffr-drive'>
<div className='cabana-onboarding-mode-chffr-drive-date'>
<strong>{ Moment(route.start_time._i).format('MMM Do') }</strong>
<span>{ Moment(route.start_time._i).format('dddd') }</span>
</div>
<div className='cabana-onboarding-mode-chffr-drive-places'>
<strong>{ route.end_geocode }</strong>
<span>From { route.start_geocode }</span>
</div>
<div className='cabana-onboarding-mode-chffr-drive-time'>
<strong>
{ routeDuration.hours > 0 ? `${ routeDuration._data.hours } hr ` : null }
{ `${ routeDuration._data.minutes } min ${ routeDuration._data.seconds } sec` }
</strong>
<span>{ `${ routeStartClock } - ${ routeEndClock }`}</span>
</div>
<div className='cabana-onboarding-mode-chffr-drive-distance'>
<strong>{ route.len.toFixed(2) } mi</strong>
<span>{ (route.len * 1.6).toFixed(2) } km</span>
</div>
<div className='cabana-onboarding-mode-chffr-drive-action'>
<button className='button--primary'
onClick={ () => this.openChffrDrive(route) }>
<span>View Drive</span>
</button>
</div>
</li>
)
})
}
</ul>
</div>
)
} else {
return (
<button onClick={ this.navigateToAuth }
className='button--primary button--kiosk'>
<i className='fa fa-video-camera'></i>
<strong>Log in to View Recorded Drives</strong>
<sup>Analyze your car driving data from <em>chffr</em></sup>
</button>
)
}
}
renderOnboardingOptions() {
return (
<div className='cabana-onboarding-modes'>
<div className='cabana-onboarding-mode'>
{ this.renderChffrOption() }
</div>
<div className='cabana-onboarding-mode'>
<button className={ cx('button--secondary button--kiosk', {
'is-disabled': !this.state.webUsbEnabled || this.props.attemptingPandaConnection
}) }
onClick={ this.attemptPandaConnection }>
<i className='fa fa-bolt'></i>
<strong>Launch Realtime Streaming</strong>
<sup>Interactively stream car data over USB with <em>panda</em></sup>
{ this.renderPandaEligibility() }
</button>
</div>
</div> </div>
) </div>
} <div
className={cx("cabana-onboarding-mode-chffr-header", {
renderUsbInstructions() { "is-ordered-desc": this.state.chffrDrivesOrderDesc,
return ( "is-ordered-asc": !this.state.chffrDrivesOrderDesc
<div className='cabana-onboarding-instructions'> })}
<button className='button--small button--inverted' onClick={ this.toggleUsbInstructions }> >
<i className='fa fa-chevron-left'></i> <div
<span> Go back</span> className={cx("cabana-onboarding-mode-chffr-drive-date", {
</button> "is-sorted": this.state.chffrDrivesSortBy === "start_time"
<h3>Follow these directions to enable WebUSB:</h3> })}
<ol className='cabana-onboarding-instructions-list list--bubbled'> onClick={() => this.handleSortDrives("start_time")}
<li> >
<p><strong>Open your Chrome settings:</strong></p> <span>Date</span>
<div className='inset'>
<span>chrome://flags/#enable-experimental-web-platform-features</span>
</div>
</li>
<li>
<p><strong>Enable Experimental Platform features:</strong></p>
<img alt={"Screenshot of Google Chrome Experimental Platform features"}
src={ OnboardingModal.instructionalImages.step2 } />
</li>
<li>
<p><strong>Enable WebUSB:</strong></p>
<img alt={"Screenshot of Google Chrome enable WebUSB"}
src={ OnboardingModal.instructionalImages.step3 } />
</li>
<li>
<p><strong>Relaunch your Chrome browser and try enabling live mode again.</strong></p>
</li>
</ol>
</div> </div>
) <div
className={cx("cabana-onboarding-mode-chffr-drive-places", {
"is-sorted": this.state.chffrDrivesSortBy === "end_geocode"
})}
onClick={() => this.handleSortDrives("end_geocode")}
>
<span>Places</span>
</div>
<div className={cx("cabana-onboarding-mode-chffr-drive-time")}>
<span>Time</span>
</div>
<div
className={cx("cabana-onboarding-mode-chffr-drive-distance", {
"is-sorted": this.state.chffrDrivesSortBy === "len"
})}
onClick={() => this.handleSortDrives("len")}
>
<span>Distance</span>
</div>
<div className="cabana-onboarding-mode-chffr-drive-action" />
</div>
<ul className="cabana-onboarding-mode-chffr-drives">
{_.orderBy(
routes,
[this.state.chffrDrivesSortBy],
[this.state.chffrDrivesOrderDesc ? "desc" : "asc"]
)
.filter(this.filterRoutesWithCan)
.filter(this.handleSearchDrives)
.map(route => {
const routeDuration = Moment.duration(
route.end_time.diff(route.start_time)
);
const routeStartClock = Moment(route.start_time).format("LT");
const routeEndClock = Moment(route.end_time).format("LT");
return (
<li
key={route.fullname}
className="cabana-onboarding-mode-chffr-drive"
>
<div className="cabana-onboarding-mode-chffr-drive-date">
<strong>
{Moment(route.start_time._i).format("MMM Do")}
</strong>
<span>{Moment(route.start_time._i).format("dddd")}</span>
</div>
<div className="cabana-onboarding-mode-chffr-drive-places">
<strong>{route.end_geocode}</strong>
<span>From {route.start_geocode}</span>
</div>
<div className="cabana-onboarding-mode-chffr-drive-time">
<strong>
{routeDuration.hours > 0
? `${routeDuration._data.hours} hr `
: null}
{`${routeDuration._data.minutes} min ${
routeDuration._data.seconds
} sec`}
</strong>
<span>{`${routeStartClock} - ${routeEndClock}`}</span>
</div>
<div className="cabana-onboarding-mode-chffr-drive-distance">
<strong>{route.len.toFixed(2)} mi</strong>
<span>{(route.len * 1.6).toFixed(2)} km</span>
</div>
<div className="cabana-onboarding-mode-chffr-drive-action">
<button
className="button--primary"
onClick={() => this.openChffrDrive(route)}
>
<span>View Drive</span>
</button>
</div>
</li>
);
})}
</ul>
</div>
);
} else {
return (
<button
onClick={this.navigateToAuth}
className="button--primary button--kiosk"
>
<i className="fa fa-video-camera" />
<strong>Log in to View Recorded Drives</strong>
<sup>
Analyze your car driving data from <em>chffr</em>
</sup>
</button>
);
} }
}
renderModalContent() { renderOnboardingOptions() {
if (this.state.viewingUsbInstructions) { return (
return this.renderUsbInstructions(); <div className="cabana-onboarding-modes">
} <div className="cabana-onboarding-mode">{this.renderChffrOption()}</div>
else { <div className="cabana-onboarding-mode">
return this.renderOnboardingOptions(); <button
} className={cx("button--secondary button--kiosk", {
} "is-disabled":
!this.state.webUsbEnabled ||
this.props.attemptingPandaConnection
})}
onClick={this.attemptPandaConnection}
>
<i className="fa fa-bolt" />
<strong>Launch Realtime Streaming</strong>
<sup>
Interactively stream car data over USB with <em>panda</em>
</sup>
{this.renderPandaEligibility()}
</button>
</div>
</div>
);
}
renderModalFooter() { renderUsbInstructions() {
return ( return (
<div className="cabana-onboarding-instructions">
<button
className="button--small button--inverted"
onClick={this.toggleUsbInstructions}
>
<i className="fa fa-chevron-left" />
<span> Go back</span>
</button>
<h3>Follow these directions to enable WebUSB:</h3>
<ol className="cabana-onboarding-instructions-list list--bubbled">
<li>
<p> <p>
<span>Don't have a <a href='https://panda.comma.ai' target='_blank'>panda</a>? </span> <strong>Open your Chrome settings:</strong>
<span><a href='https://panda.comma.ai' target='_blank'>Get one here</a> </span>
<span>or <a href='https://community.comma.ai/cabana/?demo=1'>try the demo</a>.</span>
</p> </p>
) <div className="inset">
} <span>
chrome://flags/#enable-experimental-web-platform-features
</span>
</div>
</li>
<li>
<p>
<strong>Enable Experimental Platform features:</strong>
</p>
<img
alt={"Screenshot of Google Chrome Experimental Platform features"}
src={OnboardingModal.instructionalImages.step2}
/>
</li>
<li>
<p>
<strong>Enable WebUSB:</strong>
</p>
<img
alt={"Screenshot of Google Chrome enable WebUSB"}
src={OnboardingModal.instructionalImages.step3}
/>
</li>
<li>
<p>
<strong>
Relaunch your Chrome browser and try enabling live mode again.
</strong>
</p>
</li>
</ol>
</div>
);
}
render() { renderModalContent() {
return ( if (this.state.viewingUsbInstructions) {
<Modal return this.renderUsbInstructions();
title='Welcome to cabana' } else {
subtitle='Get started by viewing your chffr drives or enabling live mode' return this.renderOnboardingOptions();
footer={ this.renderModalFooter() }
disableClose={ true }
variations={['wide', 'dark']}>
{ this.renderModalContent() }
</Modal>
);
} }
}
renderModalFooter() {
return (
<p>
<span>
Don't have a{" "}
<a href="https://panda.comma.ai" target="_blank">
panda
</a>?{" "}
</span>
<span>
<a href="https://panda.comma.ai" target="_blank">
Get one here
</a>{" "}
</span>
<span>
or{" "}
<a href="https://community.comma.ai/cabana/?demo=1">try the demo</a>.
</span>
</p>
);
}
render() {
return (
<Modal
title="Welcome to cabana"
subtitle="Get started by viewing your chffr drives or enabling live mode"
footer={this.renderModalFooter()}
disableClose={true}
variations={["wide", "dark"]}
>
{this.renderModalContent()}
</Modal>
);
}
} }

View File

@ -1,98 +1,99 @@
import React, { Component } from 'react'; import React, { Component } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import cx from 'classnames'; import cx from "classnames";
import Measure from 'react-measure'; import Measure from "react-measure";
export default class Modal extends Component { export default class Modal extends Component {
static PropTypes = { static PropTypes = {
variations: PropTypes.array, variations: PropTypes.array,
disableClose: PropTypes.bool, disableClose: PropTypes.bool,
handleClose: PropTypes.func, handleClose: PropTypes.func,
handleSave: PropTypes.func, handleSave: PropTypes.func,
title: PropTypes.string, title: PropTypes.string,
subtitle: PropTypes.string, subtitle: PropTypes.string,
navigation: PropTypes.node, navigation: PropTypes.node,
actions: PropTypes.node, actions: PropTypes.node,
footer: PropTypes.node, footer: PropTypes.node
};
constructor(props) {
super(props);
this.state = {
windowHeight: {},
modalHeight: {}
};
this.updateHeights = this.updateHeights.bind(this);
}
updateHeights(contentRect) {
this.setState({ windowHeight: window.innerHeight });
this.setState({ modalHeight: contentRect.bounds.height });
}
readVariationClasses() {
if (this.props.variations) {
const { variations } = this.props;
const classes = variations.reduce(
(classes, variation) => classes + `cabana-modal--${variation} `,
""
);
return classes;
} }
}
constructor(props) { checkClosability() {
super(props); return this.props.disableClose || false;
}
this.state = { checkYScrollability() {
windowHeight: {}, return this.state.modalHeight > this.state.windowHeight;
modalHeight: {}, }
};
this.updateHeights = this.updateHeights.bind(this); render() {
} return (
<div
updateHeights(contentRect) { className={cx("cabana-modal", this.readVariationClasses(), {
this.setState({ windowHeight: window.innerHeight }); "cabana-modal--not-closable": this.checkClosability(),
this.setState({ modalHeight: contentRect.bounds.height }); "cabana-modal--scrollable-y": this.checkYScrollability()
} })}
>
readVariationClasses() { <Measure
if (this.props.variations) { bounds
const { variations } = this.props; onResize={contentRect => {
const classes = variations.reduce((classes, variation) => this.updateHeights(contentRect);
classes + `cabana-modal--${variation} `, }}
''); >
return classes; {({ measureRef }) => (
} <div ref={measureRef} className="cabana-modal-container">
} <div
className="cabana-modal-close-icon"
checkClosability() { onClick={this.props.handleClose}
return (this.props.disableClose || false); />
} <div className="cabana-modal-header">
<h1>{this.props.title}</h1>
checkYScrollability() { <p>{this.props.subtitle}</p>
return (this.state.modalHeight > this.state.windowHeight); </div>
} <div className="cabana-modal-navigation">
{this.props.navigation}
render() { </div>
return ( <div className="cabana-modal-body">
<div className={ cx( <div className="cabana-modal-body-window">
'cabana-modal', {this.props.children}
this.readVariationClasses(), {
'cabana-modal--not-closable': this.checkClosability(),
'cabana-modal--scrollable-y': this.checkYScrollability(),
}) }>
<Measure
bounds
onResize={(contentRect) => {
this.updateHeights(contentRect);
}}>
{({ measureRef }) =>
<div ref={ measureRef } className='cabana-modal-container'>
<div className='cabana-modal-close-icon'
onClick={ this.props.handleClose }></div>
<div className='cabana-modal-header'>
<h1>{ this.props.title }</h1>
<p>{ this.props.subtitle }</p>
</div>
<div className='cabana-modal-navigation'>
{ this.props.navigation }
</div>
<div className='cabana-modal-body'>
<div className='cabana-modal-body-window'>
{ this.props.children }
</div>
<div className='cabana-modal-body-gradient'></div>
</div>
<div className='cabana-modal-actions'>
{ this.props.actions }
</div>
<div className='cabana-modal-footer'>
{ this.props.footer }
</div>
</div>
}
</Measure>
<div className='cabana-modal-backdrop'
onClick={ this.props.handleClose }>
</div> </div>
<div className="cabana-modal-body-gradient" />
</div>
<div className="cabana-modal-actions">{this.props.actions}</div>
<div className="cabana-modal-footer">{this.props.footer}</div>
</div> </div>
) )}
} </Measure>
<div
className="cabana-modal-backdrop"
onClick={this.props.handleClose}
/>
</div>
);
}
} }

View File

@ -1,128 +1,137 @@
import React, {Component} from 'react'; import React, { Component } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import {PART_SEGMENT_LENGTH} from '../config'; import { PART_SEGMENT_LENGTH } from "../config";
export default class PartSelector extends Component { export default class PartSelector extends Component {
static selectorWidth = 150; static selectorWidth = 150;
static propTypes = { static propTypes = {
onPartChange: PropTypes.func.isRequired, onPartChange: PropTypes.func.isRequired,
partsCount: PropTypes.number.isRequired, partsCount: PropTypes.number.isRequired
};
constructor(props) {
super(props);
this.state = {
selectedPartStyle: this.makePartStyle(props.partsCount, 0),
selectedPart: 0,
isDragging: false
}; };
constructor(props) { this.selectNextPart = this.selectNextPart.bind(this);
super(props); this.selectPrevPart = this.selectPrevPart.bind(this);
this.onSelectedPartDragStart = this.onSelectedPartDragStart.bind(this);
this.onSelectedPartMouseMove = this.onSelectedPartMouseMove.bind(this);
this.onSelectedPartDragEnd = this.onSelectedPartDragEnd.bind(this);
this.onClick = this.onClick.bind(this);
}
this.state = { makePartStyle(partsCount, selectedPart) {
selectedPartStyle: this.makePartStyle(props.partsCount, 0), return {
selectedPart: 0, left: selectedPart / partsCount * PartSelector.selectorWidth,
isDragging: false, width: PART_SEGMENT_LENGTH / partsCount * PartSelector.selectorWidth
}; };
}
this.selectNextPart = this.selectNextPart.bind(this); componentWillReceiveProps(nextProps) {
this.selectPrevPart = this.selectPrevPart.bind(this); if (nextProps.partsCount !== this.props.partsCount) {
this.onSelectedPartDragStart = this.onSelectedPartDragStart.bind(this); const selectedPartStyle = this.makePartStyle(
this.onSelectedPartMouseMove = this.onSelectedPartMouseMove.bind(this); nextProps.partsCount,
this.onSelectedPartDragEnd = this.onSelectedPartDragEnd.bind(this); this.state.selectedPart
this.onClick = this.onClick.bind(this); );
this.setState({ selectedPartStyle });
}
}
selectPart(part) {
part = Math.max(
0,
Math.min(this.props.partsCount - PART_SEGMENT_LENGTH, part)
);
if (part === this.state.selectedPart) {
return;
} }
makePartStyle(partsCount, selectedPart) { this.props.onPartChange(part);
return { this.setState({
left: (selectedPart / partsCount) * PartSelector.selectorWidth, selectedPart: part,
width: (PART_SEGMENT_LENGTH / partsCount) * PartSelector.selectorWidth selectedPartStyle: this.makePartStyle(this.props.partsCount, part)
}; });
}
selectNextPart() {
let { selectedPart } = this.state;
selectedPart++;
if (selectedPart + PART_SEGMENT_LENGTH >= this.props.partsCount) {
return;
} }
componentWillReceiveProps(nextProps) { this.selectPart(selectedPart);
if(nextProps.partsCount !== this.props.partsCount) { }
const selectedPartStyle = this.makePartStyle(nextProps.partsCount, this.state.selectedPart);
this.setState({selectedPartStyle}); selectPrevPart() {
} let { selectedPart } = this.state;
selectedPart--;
if (selectedPart < 0) {
return;
} }
selectPart(part) { this.selectPart(selectedPart);
part = Math.max(0, Math.min(this.props.partsCount - PART_SEGMENT_LENGTH, part)); }
if(part === this.state.selectedPart){
return;
}
this.props.onPartChange(part); partAtClientX(clientX) {
this.setState({selectedPart: part, const rect = this.selectorRect.getBoundingClientRect();
selectedPartStyle: this.makePartStyle(this.props.partsCount, const x = clientX - rect.left;
part)}); return Math.floor(x * this.props.partsCount / PartSelector.selectorWidth);
} }
selectNextPart() { onSelectedPartDragStart(e) {
let {selectedPart} = this.state; this.setState({ isDragging: true });
selectedPart++; document.addEventListener("mouseup", this.onSelectedPartDragEnd);
if(selectedPart + PART_SEGMENT_LENGTH >= this.props.partsCount) { }
return;
} onSelectedPartMouseMove(e) {
if (!this.state.isDragging) return;
this.selectPart(selectedPart);
} const part = this.partAtClientX(e.clientX);
this.selectPart(part);
selectPrevPart() { }
let {selectedPart} = this.state;
selectedPart--; onSelectedPartDragEnd(e) {
if(selectedPart < 0) { this.setState({ isDragging: false });
return; document.removeEventListener("mouseup", this.onSelectedPartDragEnd);
} }
this.selectPart(selectedPart); onClick(e) {
} const part = this.partAtClientX(e.clientX);
this.selectPart(part);
partAtClientX(clientX) { }
const rect = this.selectorRect.getBoundingClientRect();
const x = clientX - rect.left; render() {
return Math.floor(x * this.props.partsCount / PartSelector.selectorWidth); const { selectedPartStyle } = this.state;
} if (this.props.partsCount <= PART_SEGMENT_LENGTH) {
// all parts are available so no need to render the partselector
onSelectedPartDragStart(e) {
this.setState({isDragging: true}); return null;
document.addEventListener('mouseup', this.onSelectedPartDragEnd);
}
onSelectedPartMouseMove(e) {
if(!this.state.isDragging) return;
const part = this.partAtClientX(e.clientX);
this.selectPart(part);
}
onSelectedPartDragEnd(e) {
this.setState({isDragging: false});
document.removeEventListener('mouseup', this.onSelectedPartDragEnd);
}
onClick(e) {
const part = this.partAtClientX(e.clientX);
this.selectPart(part);
}
render() {
const {selectedPartStyle} = this.state;
if (this.props.partsCount <= PART_SEGMENT_LENGTH) {
// all parts are available so no need to render the partselector
return null;
}
return (
<div className='cabana-explorer-part-selector'>
<div className='cabana-explorer-part-selector-track'
ref={ (selector) => this.selectorRect = selector }
style={{ width: PartSelector.selectorWidth }}
onMouseMove={ this.onSelectedPartMouseMove }
onClick={ this.onClick }>
<div className='cabana-explorer-part-selector-track-active'
style={ selectedPartStyle }
onMouseDown={ this.onSelectedPartDragStart }
onMouseUp={ this.onSelectedPartDragEnd }>
</div>
</div>
</div>
)
} }
return (
<div className="cabana-explorer-part-selector">
<div
className="cabana-explorer-part-selector-track"
ref={selector => (this.selectorRect = selector)}
style={{ width: PartSelector.selectorWidth }}
onMouseMove={this.onSelectedPartMouseMove}
onClick={this.onClick}
>
<div
className="cabana-explorer-part-selector-track-active"
style={selectedPartStyle}
onMouseDown={this.onSelectedPartDragStart}
onMouseUp={this.onSelectedPartDragEnd}
/>
</div>
</div>
);
}
} }

View File

@ -1,5 +1,5 @@
import React, {Component} from 'react'; import React, { Component } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
export default class PlayButton extends Component { export default class PlayButton extends Component {
static propTypes = { static propTypes = {
@ -11,33 +11,41 @@ export default class PlayButton extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {'hover': false}; this.state = { hover: false };
this.onClick = this.onClick.bind(this); this.onClick = this.onClick.bind(this);
} }
imageSource() { imageSource() {
const {hover} = this.state; const { hover } = this.state;
const {isPlaying} = this.props; const { isPlaying } = this.props;
if(isPlaying) { if (isPlaying) {
if(hover) { if (hover) {
return (process.env.PUBLIC_URL + "/img/ic_pause_circle_filled_white_24px.svg"); return (
process.env.PUBLIC_URL + "/img/ic_pause_circle_filled_white_24px.svg"
);
} else { } else {
return (process.env.PUBLIC_URL + "/img/ic_pause_circle_outline_white_24px.svg"); return (
process.env.PUBLIC_URL + "/img/ic_pause_circle_outline_white_24px.svg"
);
} }
} else { } else {
if(hover) { if (hover) {
return (process.env.PUBLIC_URL + "/img/ic_play_circle_filled_white_24px.svg"); return (
process.env.PUBLIC_URL + "/img/ic_play_circle_filled_white_24px.svg"
);
} else { } else {
return (process.env.PUBLIC_URL + "/img/ic_play_circle_outline_white_24px.svg"); return (
process.env.PUBLIC_URL + "/img/ic_play_circle_outline_white_24px.svg"
);
} }
} }
} }
onClick(e) { onClick(e) {
let {isPlaying} = this.props; let { isPlaying } = this.props;
if(!isPlaying) { if (!isPlaying) {
this.props.onPlay(); this.props.onPlay();
} else { } else {
this.props.onPause(); this.props.onPause();
@ -45,11 +53,15 @@ export default class PlayButton extends Component {
} }
render() { render() {
return <img src={this.imageSource()} return (
alt={this.props.isPlaying ? 'Pause' : 'Play'} <img
className={this.props.className} src={this.imageSource()}
onClick={this.onClick} alt={this.props.isPlaying ? "Pause" : "Play"}
onMouseOver={() => this.setState({hover: true})} className={this.props.className}
onMouseLeave={() => this.setState({hover: false})} />; onClick={this.onClick}
onMouseOver={() => this.setState({ hover: true })}
onMouseLeave={() => this.setState({ hover: false })}
/>
);
} }
} }

View File

@ -1,220 +1,237 @@
import React, {Component} from 'react'; import React, { Component } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import PlayButton from '../PlayButton'; import PlayButton from "../PlayButton";
import debounce from '../../utils/debounce'; import debounce from "../../utils/debounce";
export default class RouteSeeker extends Component { export default class RouteSeeker extends Component {
static propTypes = { static propTypes = {
secondsLoaded: PropTypes.number.isRequired, secondsLoaded: PropTypes.number.isRequired,
segmentIndices: PropTypes.arrayOf(PropTypes.number), segmentIndices: PropTypes.arrayOf(PropTypes.number),
onUserSeek: PropTypes.func, onUserSeek: PropTypes.func,
onPlaySeek: PropTypes.func, onPlaySeek: PropTypes.func,
video: PropTypes.node, video: PropTypes.node,
onPause: PropTypes.func, onPause: PropTypes.func,
onPlay: PropTypes.func, onPlay: PropTypes.func,
playing: PropTypes.bool, playing: PropTypes.bool,
segmentProgress: PropTypes.func, segmentProgress: PropTypes.func,
ratioTime: PropTypes.func, ratioTime: PropTypes.func,
nearestFrameTime: PropTypes.number nearestFrameTime: PropTypes.number
};
static hiddenMarkerStyle = { display: "none", left: 0 };
static zeroSeekedBarStyle = { width: 0 };
static hiddenTooltipStyle = { display: "none", left: 0 };
static markerWidth = 20;
static tooltipWidth = 50;
constructor(props) {
super(props);
this.state = {
seekedBarStyle: RouteSeeker.zeroSeekedBarStyle,
markerStyle: RouteSeeker.hiddenMarkerStyle,
tooltipStyle: RouteSeeker.hiddenTooltipStyle,
ratio: 0,
tooltipTime: "0:00",
isPlaying: false,
isDragging: false
}; };
static hiddenMarkerStyle = {display: 'none', left: 0}; this.onMouseMove = this.onMouseMove.bind(this);
static zeroSeekedBarStyle = {width: 0}; this.onMouseLeave = this.onMouseLeave.bind(this);
static hiddenTooltipStyle = {display: 'none', left: 0}; this.onMouseDown = this.onMouseDown.bind(this);
static markerWidth = 20; this.onMouseUp = this.onMouseUp.bind(this);
static tooltipWidth = 50; this.onClick = this.onClick.bind(this);
this.onPlay = this.onPlay.bind(this);
this.onPause = this.onPause.bind(this);
this.executePlayTimer = this.executePlayTimer.bind(this);
}
constructor(props) { componentWillReceiveProps(nextProps) {
super(props); const { ratio } = this.state;
this.state = {
seekedBarStyle: RouteSeeker.zeroSeekedBarStyle,
markerStyle: RouteSeeker.hiddenMarkerStyle,
tooltipStyle: RouteSeeker.hiddenTooltipStyle,
ratio: 0,
tooltipTime: '0:00',
isPlaying: false,
isDragging: false,
};
this.onMouseMove = this.onMouseMove.bind(this); if (
this.onMouseLeave = this.onMouseLeave.bind(this); JSON.stringify(this.props.segmentIndices) !==
this.onMouseDown = this.onMouseDown.bind(this); JSON.stringify(nextProps.segmentIndices)
this.onMouseUp = this.onMouseUp.bind(this); ) {
this.onClick = this.onClick.bind(this); this.setState({
this.onPlay = this.onPlay.bind(this); seekedBarStyle: RouteSeeker.zeroSeekedBarStyle,
this.onPause = this.onPause.bind(this); markerStyle: RouteSeeker.hiddenMarkerStyle,
this.executePlayTimer = this.executePlayTimer.bind(this); ratio: 0
});
} else if (nextProps.secondsLoaded !== this.props.secondsLoaded) {
// adjust ratio in line with new secondsLoaded
const secondsSeeked = ratio * this.props.secondsLoaded;
const newRatio = secondsSeeked / nextProps.secondsLoaded;
this.updateSeekedBar(newRatio);
} }
componentWillReceiveProps(nextProps) { if (this.props.nearestFrameTime !== nextProps.nearestFrameTime) {
const {ratio} = this.state; const newRatio = this.props.segmentProgress(nextProps.nearestFrameTime);
this.updateSeekedBar(newRatio);
if(JSON.stringify(this.props.segmentIndices)
!== JSON.stringify(nextProps.segmentIndices)) {
this.setState({seekedBarStyle: RouteSeeker.zeroSeekedBarStyle,
markerStyle: RouteSeeker.hiddenMarkerStyle,
ratio: 0});
} else if(nextProps.secondsLoaded !== this.props.secondsLoaded) {
// adjust ratio in line with new secondsLoaded
const secondsSeeked = ratio * this.props.secondsLoaded;
const newRatio = secondsSeeked / nextProps.secondsLoaded;
this.updateSeekedBar(newRatio);
}
if(this.props.nearestFrameTime !== nextProps.nearestFrameTime) {
const newRatio = this.props.segmentProgress(nextProps.nearestFrameTime);
this.updateSeekedBar(newRatio);
}
if(nextProps.playing && !this.state.isPlaying) {
this.onPlay();
} else if(!nextProps.playing && this.state.isPlaying) {
this.onPause();
}
} }
componentWillUnmount() { if (nextProps.playing && !this.state.isPlaying) {
window.cancelAnimationFrame(this.playTimer); this.onPlay();
} else if (!nextProps.playing && this.state.isPlaying) {
this.onPause();
}
}
componentWillUnmount() {
window.cancelAnimationFrame(this.playTimer);
}
mouseEventXOffsetPercent(e) {
const rect = this.progressBar.getBoundingClientRect();
const x = e.clientX - rect.left;
return 100 * (x / this.progressBar.offsetWidth);
}
updateDraggingSeek = debounce(ratio => this.props.onUserSeek(ratio), 250);
onMouseMove(e) {
const markerOffsetPct = this.mouseEventXOffsetPercent(e);
if (markerOffsetPct < 0) {
this.onMouseLeave();
return;
}
const markerWidth = RouteSeeker.markerWidth;
const markerLeft = `calc(${markerOffsetPct + "%"} - ${markerWidth / 2}px)`;
const markerStyle = {
display: "",
left: markerLeft
};
const tooltipWidth = RouteSeeker.tooltipWidth;
const tooltipLeft = `calc(${markerOffsetPct + "%"} - ${tooltipWidth /
2}px)`;
const tooltipStyle = { display: "flex", left: tooltipLeft };
const ratio = Math.max(0, markerOffsetPct / 100);
if (this.state.isDragging) {
this.updateSeekedBar(ratio);
this.updateDraggingSeek(ratio);
} }
mouseEventXOffsetPercent(e) { this.setState({
const rect = this.progressBar.getBoundingClientRect(); markerStyle,
const x = e.clientX - rect.left; tooltipStyle,
tooltipTime: this.props.ratioTime(ratio).toFixed(3)
});
}
return 100 * (x / this.progressBar.offsetWidth); onMouseLeave(e) {
this.setState({
markerStyle: RouteSeeker.hiddenMarkerStyle,
tooltipStyle: RouteSeeker.hiddenTooltipStyle,
isDragging: false
});
}
updateSeekedBar(ratio) {
const seekedBarStyle = { width: 100 * ratio + "%" };
this.setState({ seekedBarStyle, ratio });
}
onClick(e) {
let ratio = this.mouseEventXOffsetPercent(e) / 100;
ratio = Math.min(1, Math.max(0, ratio));
this.updateSeekedBar(ratio);
this.props.onUserSeek(ratio);
}
onPlay() {
this.playTimer = window.requestAnimationFrame(this.executePlayTimer);
let { ratio } = this.state;
if (ratio >= 1) {
ratio = 0;
}
this.setState({ isPlaying: true, ratio });
this.props.onPlay();
}
executePlayTimer() {
const { videoElement } = this.props;
if (videoElement === null) {
this.playTimer = window.requestAnimationFrame(this.executePlayTimer);
return;
} }
updateDraggingSeek = debounce((ratio) => this.props.onUserSeek(ratio), 250); const { currentTime } = videoElement;
let newRatio = this.props.segmentProgress(currentTime);
onMouseMove(e) { if (newRatio === this.state.ratio) {
const markerOffsetPct = this.mouseEventXOffsetPercent(e); this.playTimer = window.requestAnimationFrame(this.executePlayTimer);
if(markerOffsetPct < 0) { return;
this.onMouseLeave();
return;
}
const markerWidth = RouteSeeker.markerWidth;
const markerLeft = `calc(${markerOffsetPct + '%'} - ${markerWidth / 2}px)`;
const markerStyle = {
display: '',
left: markerLeft
};
const tooltipWidth = RouteSeeker.tooltipWidth;
const tooltipLeft = `calc(${markerOffsetPct + '%'} - ${tooltipWidth / 2}px)`;
const tooltipStyle = { display: 'flex', left: tooltipLeft };
const ratio = Math.max(0, markerOffsetPct / 100);
if(this.state.isDragging) {
this.updateSeekedBar(ratio);
this.updateDraggingSeek(ratio);
}
this.setState({markerStyle,
tooltipStyle,
tooltipTime: this.props.ratioTime(ratio).toFixed(3)});
} }
onMouseLeave(e) { if (newRatio >= 1) {
this.setState({markerStyle: RouteSeeker.hiddenMarkerStyle, newRatio = 0;
tooltipStyle: RouteSeeker.hiddenTooltipStyle, this.props.onUserSeek(newRatio);
isDragging: false});
} }
updateSeekedBar(ratio) { if (newRatio >= 0) {
const seekedBarStyle = { width: (100 * ratio) + '%' }; this.updateSeekedBar(newRatio);
this.setState({seekedBarStyle, ratio}) this.props.onPlaySeek(currentTime);
} }
onClick(e) { this.playTimer = window.requestAnimationFrame(this.executePlayTimer);
let ratio = this.mouseEventXOffsetPercent(e) / 100; }
ratio = Math.min(1, Math.max(0, ratio));
this.updateSeekedBar(ratio); onPause() {
this.props.onUserSeek(ratio); window.cancelAnimationFrame(this.playTimer);
this.setState({ isPlaying: false });
this.props.onPause();
}
onMouseDown() {
if (!this.state.isDragging) {
this.setState({ isDragging: true });
} }
}
onMouseUp() {
onPlay() { if (this.state.isDragging) {
this.playTimer = window.requestAnimationFrame(this.executePlayTimer); this.setState({ isDragging: false });
let {ratio} = this.state;
if(ratio >= 1) {
ratio = 0;
}
this.setState({isPlaying: true, ratio});
this.props.onPlay();
} }
}
executePlayTimer() { render() {
const {videoElement} = this.props; const { seekedBarStyle, markerStyle, tooltipStyle } = this.state;
if(videoElement === null) { return (
this.playTimer = window.requestAnimationFrame(this.executePlayTimer); <div className="cabana-explorer-visuals-camera-seeker">
return; <PlayButton
} className={"cabana-explorer-visuals-camera-seeker-playbutton"}
onPlay={this.onPlay}
const {currentTime} = videoElement; onPause={this.onPause}
let newRatio = this.props.segmentProgress(currentTime); isPlaying={this.state.isPlaying}
/>
if(newRatio === this.state.ratio) { <div
this.playTimer = window.requestAnimationFrame(this.executePlayTimer); className={"cabana-explorer-visuals-camera-seeker-progress"}
return; onMouseMove={this.onMouseMove}
} onMouseLeave={this.onMouseLeave}
onMouseDown={this.onMouseDown}
if(newRatio >= 1) { onMouseUp={this.onMouseUp}
newRatio = 0; onClick={this.onClick}
this.props.onUserSeek(newRatio); ref={ref => (this.progressBar = ref)}
} >
<div
if(newRatio >= 0) { className={"cabana-explorer-visuals-camera-seeker-progress-tooltip"}
this.updateSeekedBar(newRatio); style={tooltipStyle}
this.props.onPlaySeek(currentTime); >
} {this.state.tooltipTime}
this.playTimer = window.requestAnimationFrame(this.executePlayTimer);
}
onPause() {
window.cancelAnimationFrame(this.playTimer);
this.setState({isPlaying: false});
this.props.onPause();
}
onMouseDown() {
if(!this.state.isDragging) {
this.setState({isDragging: true});
}
}
onMouseUp() {
if(this.state.isDragging) {
this.setState({isDragging: false});
}
}
render() {
const {seekedBarStyle, markerStyle, tooltipStyle} = this.state;
return (
<div className='cabana-explorer-visuals-camera-seeker'>
<PlayButton
className={'cabana-explorer-visuals-camera-seeker-playbutton'}
onPlay={this.onPlay}
onPause={this.onPause}
isPlaying={this.state.isPlaying} />
<div className={'cabana-explorer-visuals-camera-seeker-progress'}
onMouseMove={this.onMouseMove}
onMouseLeave={this.onMouseLeave}
onMouseDown={this.onMouseDown}
onMouseUp={this.onMouseUp}
onClick={this.onClick}
ref={(ref) => this.progressBar = ref}>
<div className={'cabana-explorer-visuals-camera-seeker-progress-tooltip'}
style={tooltipStyle}>
{this.state.tooltipTime}
</div>
<div className={'cabana-explorer-visuals-camera-seeker-progress-marker'}
style={markerStyle}></div>
<div className={'cabana-explorer-visuals-camera-seeker-progress-inner'}
style={seekedBarStyle}></div>
</div>
</div> </div>
); <div
} className={"cabana-explorer-visuals-camera-seeker-progress-marker"}
style={markerStyle}
/>
<div
className={"cabana-explorer-visuals-camera-seeker-progress-inner"}
style={seekedBarStyle}
/>
</div>
</div>
);
}
} }

View File

@ -1,2 +1,2 @@
import RouteSeeker from './RouteSeeker'; import RouteSeeker from "./RouteSeeker";
export default RouteSeeker; export default RouteSeeker;

View File

@ -1,194 +1,205 @@
import React, {Component} from 'react'; import React, { Component } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import { StyleSheet, css } from 'aphrodite/no-important'; import { StyleSheet, css } from "aphrodite/no-important";
import HLS from './HLS'; import HLS from "./HLS";
import {cameraPath} from '../api/routes'; import { cameraPath } from "../api/routes";
import Video from '../api/video'; import Video from "../api/video";
import RouteSeeker from './RouteSeeker/RouteSeeker'; import RouteSeeker from "./RouteSeeker/RouteSeeker";
const Styles = StyleSheet.create({ const Styles = StyleSheet.create({
loadingOverlay: { loadingOverlay: {
position: 'absolute', position: "absolute",
top: 0, top: 0,
left: 0, left: 0,
width: '100%', width: "100%",
height: '100%', height: "100%",
display: 'flex', display: "flex",
justifyContent: 'center', justifyContent: "center",
alignItems: 'center', alignItems: "center",
zIndex: 3 zIndex: 3
}, },
loadingSpinner: { loadingSpinner: {
width: '25%', width: "25%",
height: '25%', height: "25%",
display: 'block' display: "block"
}, },
img: { img: {
height: 480, height: 480,
display: 'block', display: "block",
position: 'absolute', position: "absolute",
zIndex: 2 zIndex: 2
}, },
hls: { hls: {
zIndex: 1, zIndex: 1,
height: 480, height: 480,
backgroundColor: 'rgba(0,0,0,0.9)' backgroundColor: "rgba(0,0,0,0.9)"
}, },
seekBar: { seekBar: {
position: 'absolute', position: "absolute",
bottom: 0, bottom: 0,
left: 0, left: 0,
width: '100%', width: "100%",
zIndex: 4 zIndex: 4
} }
}); });
export default class RouteVideoSync extends Component { export default class RouteVideoSync extends Component {
static propTypes = { static propTypes = {
userSeekIndex: PropTypes.number.isRequired, userSeekIndex: PropTypes.number.isRequired,
secondsLoaded: PropTypes.number.isRequired, secondsLoaded: PropTypes.number.isRequired,
startOffset: PropTypes.number.isRequired, startOffset: PropTypes.number.isRequired,
message: PropTypes.object, message: PropTypes.object,
firstCanTime: PropTypes.number.isRequired, firstCanTime: PropTypes.number.isRequired,
canFrameOffset: PropTypes.number.isRequired, canFrameOffset: PropTypes.number.isRequired,
url: PropTypes.string.isRequired, url: PropTypes.string.isRequired,
playing: PropTypes.bool.isRequired, playing: PropTypes.bool.isRequired,
onPlaySeek: PropTypes.func.isRequired, onPlaySeek: PropTypes.func.isRequired,
onUserSeek: PropTypes.func.isRequired, onUserSeek: PropTypes.func.isRequired,
onPlay: PropTypes.func.isRequired, onPlay: PropTypes.func.isRequired,
onPause: PropTypes.func.isRequired, onPause: PropTypes.func.isRequired,
userSeekTime: PropTypes.number.isRequired userSeekTime: PropTypes.number.isRequired
};
constructor(props) {
super(props);
this.state = {
shouldShowJpeg: true,
isLoading: true,
videoElement: null,
shouldRestartHls: false
}; };
constructor(props) { this.onLoadStart = this.onLoadStart.bind(this);
super(props); this.onLoadEnd = this.onLoadEnd.bind(this);
this.state = { this.segmentProgress = this.segmentProgress.bind(this);
shouldShowJpeg: true, this.onVideoElementAvailable = this.onVideoElementAvailable.bind(this);
isLoading: true, this.onUserSeek = this.onUserSeek.bind(this);
videoElement: null, this.onHlsRestart = this.onHlsRestart.bind(this);
shouldRestartHls: false, this.ratioTime = this.ratioTime.bind(this);
}; }
this.onLoadStart = this.onLoadStart.bind(this); componentWillReceiveProps(nextProps) {
this.onLoadEnd = this.onLoadEnd.bind(this); if (
this.segmentProgress = this.segmentProgress.bind(this); this.props.userSeekIndex !== nextProps.userSeekIndex ||
this.onVideoElementAvailable = this.onVideoElementAvailable.bind(this); this.props.canFrameOffset !== nextProps.canFrameOffset ||
this.onUserSeek = this.onUserSeek.bind(this); (this.props.message &&
this.onHlsRestart = this.onHlsRestart.bind(this); nextProps.message &&
this.ratioTime = this.ratioTime.bind(this); this.props.message.entries.length !== nextProps.message.entries.length)
) {
this.setState({ shouldRestartHls: true });
}
}
nearestFrameUrl() {
const { url } = this.props;
const sec = Math.round(this.props.userSeekTime);
return cameraPath(url, sec);
}
loadingOverlay() {
return (
<div className={css(Styles.loadingOverlay)}>
<img
className={css(Styles.loadingSpinner)}
src={process.env.PUBLIC_URL + "/img/loading.svg"}
alt={"Loading video"}
/>
</div>
);
}
onLoadStart() {
this.setState({
shouldShowJpeg: true,
isLoading: true
});
}
onLoadEnd() {
this.setState({
shouldShowJpeg: false,
isLoading: false
});
}
segmentProgress(currentTime) {
// returns progress as number in [0,1]
if (currentTime < this.props.startOffset) {
currentTime = this.props.startOffset;
} }
componentWillReceiveProps(nextProps) { const ratio =
if(this.props.userSeekIndex !== nextProps.userSeekIndex (currentTime - this.props.startOffset) / this.props.secondsLoaded;
|| this.props.canFrameOffset !== nextProps.canFrameOffset return Math.max(0, Math.min(1, ratio));
|| (this.props.message }
&& nextProps.message
&& this.props.message.entries.length !== nextProps.message.entries.length)) { ratioTime(ratio) {
this.setState({shouldRestartHls: true}); return ratio * this.props.secondsLoaded + this.props.startOffset;
} }
onVideoElementAvailable(videoElement) {
this.setState({ videoElement });
}
onUserSeek(ratio) {
/* ratio in [0,1] */
const funcSeekToRatio = () => this.props.onUserSeek(this.ratioTime(ratio));
if (ratio === 0) {
this.setState({ shouldRestartHls: true }, funcSeekToRatio);
} else {
funcSeekToRatio();
} }
}
nearestFrameUrl() { onHlsRestart() {
const {url} = this.props; this.setState({ shouldRestartHls: false });
const sec = Math.round(this.props.userSeekTime); }
return cameraPath(url, sec);
}
loadingOverlay() { render() {
return (<div className={css(Styles.loadingOverlay)}> return (
<img className={css(Styles.loadingSpinner)} <div className="cabana-explorer-visuals-camera">
src={process.env.PUBLIC_URL + "/img/loading.svg"} {this.state.isLoading ? this.loadingOverlay() : null}
alt={"Loading video"} {this.state.shouldShowJpeg ? (
/> <img
</div>); src={this.nearestFrameUrl()}
} className={css(Styles.img)}
alt={"Camera preview at t = " + Math.round(this.props.userSeekTime)}
onLoadStart() { />
this.setState({shouldShowJpeg: true, ) : null}
isLoading: true}); <HLS
} className={css(Styles.hls)}
source={Video.videoUrlForRouteUrl(this.props.url)}
onLoadEnd() { startTime={this.props.userSeekTime}
this.setState({shouldShowJpeg: false, playbackSpeed={1}
isLoading: false}); onVideoElementAvailable={this.onVideoElementAvailable}
} playing={this.props.playing}
onClick={this.props.onVideoClick}
segmentProgress(currentTime) { onLoadStart={this.onLoadStart}
// returns progress as number in [0,1] onLoadEnd={this.onLoadEnd}
onUserSeek={this.onUserSeek}
if(currentTime < this.props.startOffset) { onPlaySeek={this.props.onPlaySeek}
currentTime = this.props.startOffset; segmentProgress={this.segmentProgress}
} shouldRestart={this.state.shouldRestartHls}
onRestart={this.onHlsRestart}
const ratio = (currentTime - this.props.startOffset) / this.props.secondsLoaded; />
return Math.max(0, Math.min(1, ratio)); <RouteSeeker
} className={css(Styles.seekBar)}
nearestFrameTime={this.props.userSeekTime}
ratioTime(ratio) { segmentProgress={this.segmentProgress}
return ratio * this.props.secondsLoaded + this.props.startOffset; secondsLoaded={this.props.secondsLoaded}
} segmentIndices={this.props.segmentIndices}
onUserSeek={this.onUserSeek}
onVideoElementAvailable(videoElement) { onPlaySeek={this.props.onPlaySeek}
this.setState({videoElement}); videoElement={this.state.videoElement}
} onPlay={this.props.onPlay}
onPause={this.props.onPause}
onUserSeek(ratio) { playing={this.props.playing}
/* ratio in [0,1] */ ratioTime={this.ratioTime}
/>
const funcSeekToRatio = () => this.props.onUserSeek(this.ratioTime(ratio)); </div>
if(ratio === 0) { );
this.setState({shouldRestartHls: true}, }
funcSeekToRatio);
} else {
funcSeekToRatio();
}
}
onHlsRestart() {
this.setState({shouldRestartHls: false})
}
render() {
return (
<div className='cabana-explorer-visuals-camera'>
{this.state.isLoading ? this.loadingOverlay() : null}
{this.state.shouldShowJpeg ?
<img src={this.nearestFrameUrl()}
className={css(Styles.img)}
alt={"Camera preview at t = " + Math.round(this.props.userSeekTime)} />
: null }
<HLS
className={css(Styles.hls)}
source={Video.videoUrlForRouteUrl(this.props.url)}
startTime={this.props.userSeekTime}
playbackSpeed={1}
onVideoElementAvailable={this.onVideoElementAvailable}
playing={this.props.playing}
onClick={this.props.onVideoClick}
onLoadStart={this.onLoadStart}
onLoadEnd={this.onLoadEnd}
onUserSeek={this.onUserSeek}
onPlaySeek={this.props.onPlaySeek}
segmentProgress={this.segmentProgress}
shouldRestart={this.state.shouldRestartHls}
onRestart={this.onHlsRestart} />
<RouteSeeker
className={css(Styles.seekBar)}
nearestFrameTime={this.props.userSeekTime}
segmentProgress={this.segmentProgress}
secondsLoaded={this.props.secondsLoaded}
segmentIndices={this.props.segmentIndices}
onUserSeek={this.onUserSeek}
onPlaySeek={this.props.onPlaySeek}
videoElement={this.state.videoElement}
onPlay={this.props.onPlay}
onPause={this.props.onPause}
playing={this.props.playing}
ratioTime={this.ratioTime} />
</div>
);
}
} }

View File

@ -1,227 +1,227 @@
import React, {Component} from 'react'; import React, { Component } from "react";
import cx from 'classnames'; import cx from "classnames";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import FileSaver from 'file-saver'; import FileSaver from "file-saver";
import OpenDbc from '../api/OpenDbc'; import OpenDbc from "../api/OpenDbc";
import DBC from '../models/can/dbc'; import DBC from "../models/can/dbc";
import Modal from './Modals/baseModal'; import Modal from "./Modals/baseModal";
// import TabStyles from '../styles/modal-tabs'; // import TabStyles from '../styles/modal-tabs';
export default class SaveDbcModal extends Component { export default class SaveDbcModal extends Component {
static propTypes = { static propTypes = {
dbc: PropTypes.instanceOf(DBC).isRequired, dbc: PropTypes.instanceOf(DBC).isRequired,
sourceDbcFilename: PropTypes.string.isRequired, sourceDbcFilename: PropTypes.string.isRequired,
handleClose: PropTypes.func.isRequired, handleClose: PropTypes.func.isRequired,
onDbcSaved: PropTypes.func.isRequired, onDbcSaved: PropTypes.func.isRequired,
openDbcClient: PropTypes.instanceOf(OpenDbc).isRequired, openDbcClient: PropTypes.instanceOf(OpenDbc).isRequired,
hasGithubAuth: PropTypes.bool.isRequired, hasGithubAuth: PropTypes.bool.isRequired,
loginWithGithub: PropTypes.element.isRequired loginWithGithub: PropTypes.element.isRequired
};
constructor(props) {
super(props);
this.state = {
tab: "GitHub",
openDbcFork: null,
dbcFilename: this.props.sourceDbcFilename,
tabs: ["GitHub", "Download"]
}; };
constructor(props) { this.commitToGitHub = this.commitToGitHub.bind(this);
super(props); this.downloadDbcFile = this.downloadDbcFile.bind(this);
this.state = { this.forkOpenDbcAndWait = this.forkOpenDbcAndWait.bind(this);
tab: 'GitHub', this.renderForkButton = this.renderForkButton.bind(this);
openDbcFork: null, this.renderTabNavigation = this.renderTabNavigation.bind(this);
dbcFilename: this.props.sourceDbcFilename, this.renderActions = this.renderActions.bind(this);
tabs: ['GitHub', 'Download'], }
};
this.commitToGitHub = this.commitToGitHub.bind(this); async componentWillMount() {
this.downloadDbcFile = this.downloadDbcFile.bind(this); const openDbcFork = await this.props.openDbcClient.getUserOpenDbcFork();
this.forkOpenDbcAndWait = this.forkOpenDbcAndWait.bind(this); this.setState({ openDbcFork });
this.renderForkButton = this.renderForkButton.bind(this); }
this.renderTabNavigation = this.renderTabNavigation.bind(this);
this.renderActions = this.renderActions.bind(this); async commitToGitHub() {
const { openDbcFork, dbcFilename } = this.state;
const filename = dbcFilename.replace(/\.dbc/g, "") + ".dbc";
const success = await this.props.openDbcClient.commitFile(
openDbcFork,
filename,
this.props.dbc.text()
);
if (success) {
this.props.onDbcSaved(filename);
} }
}
async componentWillMount() { async downloadDbcFile() {
const openDbcFork = await this.props.openDbcClient.getUserOpenDbcFork(); const blob = new Blob([this.props.dbc.text()], {
this.setState({openDbcFork}) type: "text/plain;charset=utf-8"
} });
const filename = this.state.dbcFilename.replace(/\.dbc/g, "") + ".dbc";
FileSaver.saveAs(blob, filename, true);
}
async commitToGitHub() { async forkOpenDbcAndWait() {
const { openDbcFork, dbcFilename } = this.state; const forkResponseSuccess = await this.props.openDbcClient.fork();
const filename = dbcFilename.replace(/\.dbc/g, '') + '.dbc'; if (forkResponseSuccess) {
const success = await this.props.openDbcClient.commitFile(openDbcFork, let isTimedOut = false;
filename, const timeout = window.setTimeout(() => {
this.props.dbc.text()); isTimedOut = true;
if (success) { }, 30000);
this.props.onDbcSaved(filename);
}
}
async downloadDbcFile() {
const blob = new Blob([this.props.dbc.text()], {type: "text/plain;charset=utf-8"});
const filename = this.state.dbcFilename.replace(/\.dbc/g, '') + '.dbc';
FileSaver.saveAs(blob, filename, true);
}
async forkOpenDbcAndWait() {
const forkResponseSuccess = await this.props.openDbcClient.fork();
if(forkResponseSuccess) {
let isTimedOut = false;
const timeout = window.setTimeout(() => {
isTimedOut = true;
}, 30000);
const interval = window.setInterval(() => {
if(!isTimedOut) {
this.props.openDbcClient.getUserOpenDbcFork().then((openDbcFork) => {
if(openDbcFork !== null) {
this.setState({openDbcFork});
window.clearInterval(interval);
window.clearTimeout(timeout);
}
});
} else {
window.clearInterval(interval);
}
}, 3000);
const interval = window.setInterval(() => {
if (!isTimedOut) {
this.props.openDbcClient.getUserOpenDbcFork().then(openDbcFork => {
if (openDbcFork !== null) {
this.setState({ openDbcFork });
window.clearInterval(interval);
window.clearTimeout(timeout);
}
});
} else { } else {
// fork failed window.clearInterval(interval);
} }
}, 3000);
} else {
// fork failed
} }
}
primaryActionDisabled() { primaryActionDisabled() {
const { tab } = this.state; const { tab } = this.state;
if (tab === 'GitHub') { if (tab === "GitHub") {
return this.state.openDbcFork != null
&& this.state.dbcFilename.length > 0
} else if (tab === 'Download') {
return true;
}
}
renderForkButton() {
return (
<button onClick={ this.forkOpenDbcAndWait }>
<i className='fa fa-code-fork'></i>
<span> Fork OpenDBC</span>
</button>
);
}
renderForkStep() {
const { openDbcFork } = this.state;
let content;
if (openDbcFork !== null) {
content = (
<button disabled>
<i className='fa fa-code-fork'></i>
<span> Forked: { openDbcFork }</span>
</button>
)
} else if (this.props.hasGithubAuth) {
content = this.renderForkButton();
} else {
content = this.props.loginWithGithub;
}
return (
<div>
{ openDbcFork !== null ? this.renderForkButton() : null }
{ content }
<hr />
</div>
);
}
renderFilenameField() {
return (
<div className='form-field' data-extension='.dbc'>
<label htmlFor='filename'>
<span>Choose a filename:</span>
<sup>Pick a unique name for your car DBC file</sup>
</label>
<input type='text'
id='filename'
value={ this.state.dbcFilename.replace(/\.dbc/g, '') }
size={ this.state.dbcFilename.length + 2 }
onChange={ (e) =>
this.setState({dbcFilename: e.target.value}) } />
</div>
);
}
renderTabNavigation() {
return ( return (
<div className='cabana-tabs-navigation'> this.state.openDbcFork != null && this.state.dbcFilename.length > 0
{ this.state.tabs.map((tab) => { );
return ( } else if (tab === "Download") {
<a className={ cx({'is-active': this.state.tab === tab})} return true;
onClick={ () => { this.setState({ tab }) }} }
key={tab}> }
<span>{ tab }</span>
</a> renderForkButton() {
) return (
})} <button onClick={this.forkOpenDbcAndWait}>
<i className="fa fa-code-fork" />
<span> Fork OpenDBC</span>
</button>
);
}
renderForkStep() {
const { openDbcFork } = this.state;
let content;
if (openDbcFork !== null) {
content = (
<button disabled>
<i className="fa fa-code-fork" />
<span> Forked: {openDbcFork}</span>
</button>
);
} else if (this.props.hasGithubAuth) {
content = this.renderForkButton();
} else {
content = this.props.loginWithGithub;
}
return (
<div>
{openDbcFork !== null ? this.renderForkButton() : null}
{content}
<hr />
</div>
);
}
renderFilenameField() {
return (
<div className="form-field" data-extension=".dbc">
<label htmlFor="filename">
<span>Choose a filename:</span>
<sup>Pick a unique name for your car DBC file</sup>
</label>
<input
type="text"
id="filename"
value={this.state.dbcFilename.replace(/\.dbc/g, "")}
size={this.state.dbcFilename.length + 2}
onChange={e => this.setState({ dbcFilename: e.target.value })}
/>
</div>
);
}
renderTabNavigation() {
return (
<div className="cabana-tabs-navigation">
{this.state.tabs.map(tab => {
return (
<a
className={cx({ "is-active": this.state.tab === tab })}
onClick={() => {
this.setState({ tab });
}}
key={tab}
>
<span>{tab}</span>
</a>
);
})}
</div>
);
}
renderTabContent() {
const { tab } = this.state;
if (tab === "GitHub") {
return (
<div>
{this.renderForkStep()}
{this.renderFilenameField()}
</div> </div>
) );
} else if (tab === "Download") {
return <div>{this.renderFilenameField()}</div>;
} }
}
renderTabContent() { renderActions() {
const { tab } = this.state; const { tab } = this.state;
if (tab === 'GitHub') { if (tab === "GitHub") {
return ( return (
<div> <div>
{ this.renderForkStep() } <button className="button--inverted" onClick={this.props.handleClose}>
{ this.renderFilenameField() } <span>Cancel</span>
</div> </button>
); <button className="button--primary" onClick={this.commitToGitHub}>
} <span>Commit to GitHub</span>
else if (tab === 'Download') { </button>
return ( </div>
<div> );
{ this.renderFilenameField() } } else if (tab === "Download") {
</div> return (
); <div>
} <button className="button--inverted" onClick={this.props.handleClose}>
<span>Cancel</span>
</button>
<button className="button--primary" onClick={this.downloadDbcFile}>
<span>Download</span>
</button>
</div>
);
} }
}
renderActions() { render() {
const { tab } = this.state; return (
if (tab === 'GitHub') { <Modal
return ( title="Save DBC File"
<div> subtitle="Save your progress and output to a DBC file"
<button className='button--inverted' handleClose={this.props.handleClose}
onClick={ this.props.handleClose }> navigation={this.renderTabNavigation()}
<span>Cancel</span> actions={this.renderActions()}
</button> >
<button className='button--primary' {this.renderTabContent()}
onClick={ this.commitToGitHub }> </Modal>
<span>Commit to GitHub</span> );
</button> }
</div>
)
}
else if (tab === 'Download') {
return (
<div>
<button className='button--inverted'
onClick={ this.props.handleClose }>
<span>Cancel</span>
</button>
<button className='button--primary'
onClick={ this.downloadDbcFile }>
<span>Download</span>
</button>
</div>
)
}
}
render() {
return (
<Modal
title='Save DBC File'
subtitle='Save your progress and output to a DBC file'
handleClose={ this.props.handleClose }
navigation={ this.renderTabNavigation() }
actions={ this.renderActions() }>
{ this.renderTabContent() }
</Modal>
);
}
} }

View File

@ -1,365 +1,398 @@
// SignalLegendEntry.js // SignalLegendEntry.js
import React, {Component} from 'react'; import React, { Component } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import cx from 'classnames'; import cx from "classnames";
import Signal from '../models/can/signal'; import Signal from "../models/can/signal";
import DbcUtils from '../utils/dbc'; import DbcUtils from "../utils/dbc";
import {swapKeysAndValues} from '../utils/object'; import { swapKeysAndValues } from "../utils/object";
export default class SignalLegendEntry extends Component { export default class SignalLegendEntry extends Component {
static propTypes = { static propTypes = {
signal: PropTypes.instanceOf(Signal).isRequired, signal: PropTypes.instanceOf(Signal).isRequired,
isHighlighted: PropTypes.bool, isHighlighted: PropTypes.bool,
onSignalHover: PropTypes.func, onSignalHover: PropTypes.func,
onSignalHoverEnd: PropTypes.func, onSignalHoverEnd: PropTypes.func,
onTentativeSignalChange: PropTypes.func, onTentativeSignalChange: PropTypes.func,
onSignalChange: PropTypes.func, onSignalChange: PropTypes.func,
onSignalRemove: PropTypes.func, onSignalRemove: PropTypes.func,
onSignalPlotChange: PropTypes.func, onSignalPlotChange: PropTypes.func,
toggleExpandSignal: PropTypes.func, toggleExpandSignal: PropTypes.func,
isPlotted: PropTypes.bool, isPlotted: PropTypes.bool,
isExpanded: PropTypes.bool, isExpanded: PropTypes.bool
};
static unsignedTransformation = field => {
return (value, signal) => {
if (value !== "") {
value = Number(value) || 0;
if (value < 0) {
value = 0;
}
}
signal[field] = value;
return signal;
}; };
};
static unsignedTransformation = (field) => { static fields = [
return (value, signal) => { {
if(value !== '') { field: "name",
value = Number(value) || 0; title: "Name",
type: "string"
},
{
field: "size",
title: "Size",
type: "number",
transform: SignalLegendEntry.unsignedTransformation("size")
},
{
field: "startBit",
title: signal =>
signal.isLittleEndian
? "Least significant bit"
: "Most significant bit",
type: "number",
transform: SignalLegendEntry.unsignedTransformation("startBit")
},
{
field: "isLittleEndian",
title: "Endianness",
type: "option",
options: {
options: ["Little", "Big"],
optionValues: { Little: true, Big: false }
},
transform: (isLittleEndian, signal) => {
if (signal.isLittleEndian !== isLittleEndian) {
const { startBit } = signal;
if(value < 0) { if (isLittleEndian) {
value = 0; // big endian -> little endian
} const startByte = Math.floor(signal.startBit / 8),
endByte = Math.floor((signal.startBit - signal.size + 1) / 8);
if (startByte === endByte) {
signal.startBit = signal.startBit - signal.size + 1;
} else {
signal.startBit = DbcUtils.matrixBitNumber(startBit);
} }
signal[field] = value; } else {
return signal; // little endian -> big endian
}; const startByte = Math.floor(signal.startBit / 8),
}; endByte = Math.floor((signal.startBit + signal.size - 1) / 8);
static fields = [ if (startByte === endByte) {
{ signal.startBit = signal.startBit + signal.size - 1;
field: 'name', } else {
title: 'Name', signal.startBit = DbcUtils.bigEndianBitIndex(startBit);
type: 'string',
},
{
field: 'size',
title: 'Size',
type: 'number',
transform: SignalLegendEntry.unsignedTransformation('size')
},
{
field: 'startBit',
title: (signal) => signal.isLittleEndian ? 'Least significant bit' : 'Most significant bit',
type: 'number',
transform: SignalLegendEntry.unsignedTransformation('startBit')
},
{
field: 'isLittleEndian',
title: 'Endianness',
type: 'option',
options: {
options: ['Little', 'Big'],
optionValues: {Little: true, Big: false}
},
transform: (isLittleEndian, signal) => {
if(signal.isLittleEndian !== isLittleEndian) {
const {startBit} = signal;
if(isLittleEndian) {
// big endian -> little endian
const startByte = Math.floor(signal.startBit / 8),
endByte = Math.floor((signal.startBit - signal.size + 1) / 8);
if(startByte === endByte) {
signal.startBit = signal.startBit - signal.size + 1;
} else {
signal.startBit = DbcUtils.matrixBitNumber(startBit);
}
} else {
// little endian -> big endian
const startByte = Math.floor(signal.startBit / 8),
endByte = Math.floor((signal.startBit + signal.size - 1) / 8);
if(startByte === endByte) {
signal.startBit = signal.startBit + signal.size - 1;
} else {
signal.startBit = DbcUtils.bigEndianBitIndex(startBit);
}
}
signal.isLittleEndian = isLittleEndian;
}
return signal;
} }
}, }
{ signal.isLittleEndian = isLittleEndian;
field: 'isSigned',
title: 'Sign',
type: 'option',
options: {options: ['Signed', 'Unsigned'],
optionValues: {Signed: true, Unsigned: false}}
},
{
field: 'factor',
title: 'Factor',
type: 'number'
},
{
field: 'offset',
title: 'Offset',
type: 'number'
},
{
field: 'unit',
title: 'Unit',
type: 'string'
},
{
field: 'comment',
title: 'Comment',
type: 'string'
},
{
field: 'min',
title: 'Minimum value',
type: 'number'
},
{
field: 'max',
title: 'Maximum value',
type: 'number'
} }
]; return signal;
}
},
{
field: "isSigned",
title: "Sign",
type: "option",
options: {
options: ["Signed", "Unsigned"],
optionValues: { Signed: true, Unsigned: false }
}
},
{
field: "factor",
title: "Factor",
type: "number"
},
{
field: "offset",
title: "Offset",
type: "number"
},
{
field: "unit",
title: "Unit",
type: "string"
},
{
field: "comment",
title: "Comment",
type: "string"
},
{
field: "min",
title: "Minimum value",
type: "number"
},
{
field: "max",
title: "Maximum value",
type: "number"
}
];
static fieldSpecForName = (name) => { static fieldSpecForName = name => {
return SignalLegendEntry.fields.find((field) => return SignalLegendEntry.fields.find(field => field.field === name);
field.field === name) };
constructor(props) {
super(props);
this.state = {
isExpanded: false,
signalEdited: Object.assign(Object.create(props.signal), props.signal)
}; };
constructor(props) { this.toggleEditing = this.toggleEditing.bind(this);
super(props); this.updateField = this.updateField.bind(this);
this.state = { this.toggleSignalPlot = this.toggleSignalPlot.bind(this);
isExpanded: false, }
signalEdited: Object.assign(Object.create(props.signal), props.signal),
};
this.toggleEditing = this.toggleEditing.bind(this); componentWillReceiveProps(nextProps) {
this.updateField = this.updateField.bind(this); if (!nextProps.signal.equals(this.props.signal)) {
this.toggleSignalPlot = this.toggleSignalPlot.bind(this); this.setState({
} signalEdited: Object.assign(
Object.create(nextProps.signal),
componentWillReceiveProps(nextProps) { nextProps.signal
if(!nextProps.signal.equals(this.props.signal)) {
this.setState({signalEdited: Object.assign(Object.create(nextProps.signal),
nextProps.signal)});
}
}
renderField(field, title, valueCol, signal) {
let titleStr;
if(typeof title === 'function') {
titleStr = title(signal);
} else {
titleStr = title;
}
return (
<div key={field} className='form-field form-field--small'>
<label htmlFor={`${signal.name}_${field}`}>{titleStr}</label>
{valueCol}
</div>
) )
});
}
}
renderField(field, title, valueCol, signal) {
let titleStr;
if (typeof title === "function") {
titleStr = title(signal);
} else {
titleStr = title;
} }
updateField(fieldSpec, value) { return (
let {signalEdited} = this.state; <div key={field} className="form-field form-field--small">
const {signal} = this.props; <label htmlFor={`${signal.name}_${field}`}>{titleStr}</label>
{valueCol}
</div>
);
}
if(fieldSpec.transform) { updateField(fieldSpec, value) {
signalEdited = fieldSpec.transform(value, signalEdited); let { signalEdited } = this.state;
} else { const { signal } = this.props;
signalEdited[fieldSpec.field] = value;
}
if (fieldSpec.transform) {
// Save entire signal while editing signalEdited = fieldSpec.transform(value, signalEdited);
this.setState({signalEdited}); } else {
const signalCopy = Object.assign(Object.create(signal), signal); signalEdited[fieldSpec.field] = value;
Object.entries(signalEdited).forEach(([field, value]) => {
signalCopy[field] = value;
});
this.props.onSignalChange(signalCopy, signal);
} }
renderNumberField(fieldSpec, signal) { // Save entire signal while editing
const {field, title} = fieldSpec; this.setState({ signalEdited });
let valueCol; const signalCopy = Object.assign(Object.create(signal), signal);
Object.entries(signalEdited).forEach(([field, value]) => {
signalCopy[field] = value;
});
this.props.onSignalChange(signalCopy, signal);
}
if(this.props.isExpanded) { renderNumberField(fieldSpec, signal) {
let value = this.state.signalEdited[field]; const { field, title } = fieldSpec;
if(value !== '') { let valueCol;
let num = Number(value);
value = (isNaN(num) ? '' : num);
}
valueCol = (
<input id={`${signal.name}_${field}`}
type="number"
value={value}
onChange={(e) => {this.updateField(fieldSpec, e.target.value)}
}/>
);
} else {
let value = this.props.signal[field];
valueCol = <span>{value}</span>;
}
return this.renderField(field, title, valueCol, signal);
}
renderStringField(fieldSpec, signal) { if (this.props.isExpanded) {
const {field, title} = fieldSpec; let value = this.state.signalEdited[field];
let valueCol; if (value !== "") {
if(this.props.isExpanded) { let num = Number(value);
valueCol = ( value = isNaN(num) ? "" : num;
<input id={`${signal.name}_${field}`} }
type="text" valueCol = (
value={this.state.signalEdited[field] || ''} <input
onChange={(e) => { id={`${signal.name}_${field}`}
this.updateField(fieldSpec, e.target.value) type="number"
}} value={value}
/>); onChange={e => {
} else { this.updateField(fieldSpec, e.target.value);
valueCol = <span>{this.props.signal[field]}</span>; }}
} />
return this.renderField(field, title, valueCol, signal);
}
renderOptionField(fieldSpec, signal) {
let valueCol;
const {field, title} = fieldSpec;
const {options, optionValues} = fieldSpec.options;
let valueOptions = swapKeysAndValues(optionValues);
if(this.props.isExpanded) {
const optionEles = options.map((opt) =>
<option key={opt}
value={optionValues[opt]}>{opt}</option>
);
valueCol = (
<select id={`${signal.name}_${field}`}
defaultValue={this.state.signalEdited[field]}
onChange={
(e) => { this.updateField(fieldSpec, e.target.value === "true") }
}>
{optionEles}
</select>
);
} else {
valueCol = <span>{valueOptions[this.props.signal[field]]}</span>;
}
return this.renderField(field, title, valueCol, signal);
}
renderFieldNode(field, signal) {
if(field.type === 'number') {
return this.renderNumberField(field, signal);
} else if(field.type === 'option') {
return this.renderOptionField(field, signal);
} else if(field.type === 'string') {
return this.renderStringField(field, signal);
}
}
toggleEditing(e) {
let { signalEdited } = this.state;
const { signal, isExpanded } = this.props;
const signalCopy = Object.assign(Object.create(signal), signal);
if(isExpanded) {
// Finished editing, save changes & reset intermediate
// signalEdited state.
Object.entries(signalEdited).forEach(([field, value]) => {
const fieldSpec = SignalLegendEntry.fieldSpecForName(field);
if(fieldSpec && fieldSpec.type === 'number' && isNaN(parseInt(value, 10))) {
value = 0;
}
signalCopy[field] = value;
});
this.props.onSignalChange(signalCopy, signal);
} else {
signalEdited = signalCopy;
}
// Expand and enable signal editing
this.setState({
signalEdited
})
this.props.toggleExpandSignal(signal);
e.stopPropagation();
}
renderSignalForm(signal) {
return (
<div className='signals-legend-entry-form'>
{SignalLegendEntry.fields.map((field) => {
return (
<div className='signals-legend-entry-form-field'
key={field.field}>
{this.renderFieldNode(field, signal)}
</div>
)
})}
<div className='signals-legend-entry-form-remove'>
<button className='button--tiny button--alpha'
onClick={ () => this.props.onSignalRemove(signal) }>Remove Signal</button>
</div>
</div>
); );
} else {
let value = this.props.signal[field];
valueCol = <span>{value}</span>;
}
return this.renderField(field, title, valueCol, signal);
}
renderStringField(fieldSpec, signal) {
const { field, title } = fieldSpec;
let valueCol;
if (this.props.isExpanded) {
valueCol = (
<input
id={`${signal.name}_${field}`}
type="text"
value={this.state.signalEdited[field] || ""}
onChange={e => {
this.updateField(fieldSpec, e.target.value);
}}
/>
);
} else {
valueCol = <span>{this.props.signal[field]}</span>;
} }
toggleSignalPlot(e) { return this.renderField(field, title, valueCol, signal);
const {signal, isPlotted} = this.props; }
e.preventDefault();
this.props.onSignalPlotChange(!isPlotted, signal.uid); renderOptionField(fieldSpec, signal) {
let valueCol;
const { field, title } = fieldSpec;
const { options, optionValues } = fieldSpec.options;
let valueOptions = swapKeysAndValues(optionValues);
if (this.props.isExpanded) {
const optionEles = options.map(opt => (
<option key={opt} value={optionValues[opt]}>
{opt}
</option>
));
valueCol = (
<select
id={`${signal.name}_${field}`}
defaultValue={this.state.signalEdited[field]}
onChange={e => {
this.updateField(fieldSpec, e.target.value === "true");
}}
>
{optionEles}
</select>
);
} else {
valueCol = <span>{valueOptions[this.props.signal[field]]}</span>;
} }
render() { return this.renderField(field, title, valueCol, signal);
const {signal} = this.props; }
const expandedEntryClass = this.props.isExpanded ? 'is-expanded' : null;
const highlightedEntryClass = this.props.isHighlighted ? 'is-highlighted' : null; renderFieldNode(field, signal) {
const plottedButtonClass = this.props.isPlotted ? 'button' : 'button--alpha'; if (field.type === "number") {
const plottedButtonText = this.props.isPlotted ? 'Hide Plot' : 'Show Plot'; return this.renderNumberField(field, signal);
return ( } else if (field.type === "option") {
return this.renderOptionField(field, signal);
} else if (field.type === "string") {
return this.renderStringField(field, signal);
}
}
toggleEditing(e) {
let { signalEdited } = this.state;
const { signal, isExpanded } = this.props;
const signalCopy = Object.assign(Object.create(signal), signal);
if (isExpanded) {
// Finished editing, save changes & reset intermediate
// signalEdited state.
Object.entries(signalEdited).forEach(([field, value]) => {
const fieldSpec = SignalLegendEntry.fieldSpecForName(field);
if (
fieldSpec &&
fieldSpec.type === "number" &&
isNaN(parseInt(value, 10))
) {
value = 0;
}
signalCopy[field] = value;
});
this.props.onSignalChange(signalCopy, signal);
} else {
signalEdited = signalCopy;
}
// Expand and enable signal editing
this.setState({
signalEdited
});
this.props.toggleExpandSignal(signal);
e.stopPropagation();
}
renderSignalForm(signal) {
return (
<div className="signals-legend-entry-form">
{SignalLegendEntry.fields.map(field => {
return (
<div className="signals-legend-entry-form-field" key={field.field}>
{this.renderFieldNode(field, signal)}
</div>
);
})}
<div className="signals-legend-entry-form-remove">
<button
className="button--tiny button--alpha"
onClick={() => this.props.onSignalRemove(signal)}
>
Remove Signal
</button>
</div>
</div>
);
}
toggleSignalPlot(e) {
const { signal, isPlotted } = this.props;
e.preventDefault();
this.props.onSignalPlotChange(!isPlotted, signal.uid);
}
render() {
const { signal } = this.props;
const expandedEntryClass = this.props.isExpanded ? "is-expanded" : null;
const highlightedEntryClass = this.props.isHighlighted
? "is-highlighted"
: null;
const plottedButtonClass = this.props.isPlotted
? "button"
: "button--alpha";
const plottedButtonText = this.props.isPlotted ? "Hide Plot" : "Show Plot";
return (
<div
className={cx(
"signals-legend-entry",
expandedEntryClass,
highlightedEntryClass
)}
onMouseEnter={() => this.props.onSignalHover(signal)}
onMouseLeave={() => this.props.onSignalHoverEnd(signal)}
>
<div
className="signals-legend-entry-color"
style={{ backgroundColor: `rgb(${this.props.color}` }}
/>
<div className="signals-legend-entry-header">
<div <div
className={cx('signals-legend-entry', expandedEntryClass, highlightedEntryClass)} className="signals-legend-entry-header-name"
onMouseEnter={() => this.props.onSignalHover(signal)} onClick={this.toggleEditing}
onMouseLeave={() => this.props.onSignalHoverEnd(signal)}> >
<div className='signals-legend-entry-color' <strong>{signal.name}</strong>
style={{backgroundColor: `rgb(${this.props.color}`}}></div>
<div className="signals-legend-entry-header">
<div
className="signals-legend-entry-header-name"
onClick={this.toggleEditing}>
<strong>{signal.name}</strong>
</div>
<div
className="signals-legend-entry-header-action"
onClick={this.toggleSignalPlot}>
<button className={cx('button--tiny', plottedButtonClass)}>
{plottedButtonText}
</button>
</div>
</div>
<div className="signals-legend-entry-body">
{this.props.isExpanded ? this.renderSignalForm(signal) : null}
</div>
</div> </div>
); <div
} className="signals-legend-entry-header-action"
onClick={this.toggleSignalPlot}
>
<button className={cx("button--tiny", plottedButtonClass)}>
{plottedButtonText}
</button>
</div>
</div>
<div className="signals-legend-entry-body">
{this.props.isExpanded ? this.renderSignalForm(signal) : null}
</div>
</div>
);
}
} }

View File

@ -1,22 +1,26 @@
import {getUrlParameter} from './utils/url'; import { getUrlParameter } from "./utils/url";
const ENV = process.env.NODE_ENV === "production" ? "prod" : "debug";
const ENV = process.env.NODE_ENV === 'production' ? 'prod' : 'debug'; const ENV_GITHUB_CLIENT_ID = {
debug: "f1e42d14f45491f9ca34",
const ENV_GITHUB_CLIENT_ID = {debug: 'f1e42d14f45491f9ca34', prod: "4b43250e7499a97d62a5"
prod: '4b43250e7499a97d62a5'} };
export const GITHUB_CLIENT_ID = ENV_GITHUB_CLIENT_ID[ENV]; export const GITHUB_CLIENT_ID = ENV_GITHUB_CLIENT_ID[ENV];
const ENV_GITHUB_REDIRECT_URL = {debug: 'http://127.0.0.1:1235/callback', const ENV_GITHUB_REDIRECT_URL = {
prod: 'https://api.comma.ai/cabana/ghcallback'} debug: "http://127.0.0.1:1235/callback",
prod: "https://api.comma.ai/cabana/ghcallback"
};
export const GITHUB_REDIRECT_URL = ENV_GITHUB_REDIRECT_URL[ENV]; export const GITHUB_REDIRECT_URL = ENV_GITHUB_REDIRECT_URL[ENV];
export const GITHUB_AUTH_TOKEN_KEY = 'gh_access_token'; export const GITHUB_AUTH_TOKEN_KEY = "gh_access_token";
export const OPENDBC_SOURCE_REPO = 'commaai/opendbc'; export const OPENDBC_SOURCE_REPO = "commaai/opendbc";
export const USE_UNLOGGER = (typeof window !== 'undefined' && getUrlParameter('unlogger') !== null); export const USE_UNLOGGER =
export const UNLOGGER_HOST = 'http://localhost:8080/unlogger'; typeof window !== "undefined" && getUrlParameter("unlogger") !== null;
export const UNLOGGER_HOST = "http://localhost:8080/unlogger";
export const LOGENTRIES_TOKEN = '4bc98019-8277-4fe0-867c-ed21ea843cc5'; export const LOGENTRIES_TOKEN = "4bc98019-8277-4fe0-867c-ed21ea843cc5";
export const PART_SEGMENT_LENGTH = 3; export const PART_SEGMENT_LENGTH = 3;
@ -24,5 +28,5 @@ export const CAN_GRAPH_MAX_POINTS = 10000;
export const STREAMING_WINDOW = 60; export const STREAMING_WINDOW = 60;
export const COMMA_ACCESS_TOKEN_COOKIE = 'comma_access_token'; export const COMMA_ACCESS_TOKEN_COOKIE = "comma_access_token";
export const COMMA_OAUTH_REDIRECT_COOKIE = 'wiki_login_redirect'; export const COMMA_OAUTH_REDIRECT_COOKIE = "wiki_login_redirect";

View File

@ -1,49 +1,53 @@
import LogEntries from './LogEntries'; import LogEntries from "./LogEntries";
import {LOGENTRIES_TOKEN} from '../config'; import { LOGENTRIES_TOKEN } from "../config";
class CloudLog { class CloudLog {
constructor() { constructor() {
LogEntries.init({token: LOGENTRIES_TOKEN, LogEntries.init({
no_format: true, token: LOGENTRIES_TOKEN,
catchall: false}); no_format: true,
this.context = {}; catchall: false
});
this.context = {};
}
bind(obj) {
this.context.update(obj);
}
emit(message, level = "log") {
if (typeof global.__JEST__ !== "undefined") {
// Don't log in testing environment
return;
} }
bind(obj) { const entry = {
this.context.update(obj); ctx: this.context,
created: new Date().getTime() / 1000,
msg: message,
src: "JSCloudLog"
};
if (level === "log") {
LogEntries.log(entry);
} else if (level === "warn") {
LogEntries.warn(entry);
} else if (level === "error") {
LogEntries.error(entry);
} }
}
emit(message, level = 'log') { log(message) {
if(typeof global.__JEST__ !== 'undefined') { this.emit(message);
// Don't log in testing environment }
return
}
const entry = {ctx: this.context, warn(message) {
created: new Date().getTime() / 1000, this.emit(message, "warn");
msg: message, }
src: 'JSCloudLog'};
if(level === 'log') { error(message) {
LogEntries.log(entry); this.emit(message, "error");
} else if(level === 'warn') { }
LogEntries.warn(entry);
} else if(level === 'error') {
LogEntries.error(entry);
}
}
log(message) {
this.emit(message);
}
warn(message) {
this.emit(message, 'warn');
}
error(message) {
this.emit(message, 'error');
}
} }
export default (new CloudLog()); export default new CloudLog();

View File

@ -1,382 +1,388 @@
// Vendored from https://github.com/rapid7/le_js, which is broken with webpack. // Vendored from https://github.com/rapid7/le_js, which is broken with webpack.
/* eslint-disable */ /* eslint-disable */
if (typeof window === 'undefined') { // eslint-disable-line no-use-before-define if (typeof window === "undefined") {
// eslint-disable-line no-use-before-define
var window = self; var window = self;
} }
var _indexOf = function (array, obj) { var _indexOf = function(array, obj) {
for (var i = 0; i < array.length; i++) { for (var i = 0; i < array.length; i++) {
if (obj === array[i]) { if (obj === array[i]) {
return i; return i;
} }
} }
return -1; return -1;
}; };
// Obtain a browser-specific XHR object // Obtain a browser-specific XHR object
var _getAjaxObject = function () { var _getAjaxObject = function() {
if (typeof XDomainRequest !== "undefined") { if (typeof XDomainRequest !== "undefined") {
// We're using IE8/9 // We're using IE8/9
return new XDomainRequest(); return new XDomainRequest();
} }
return new XMLHttpRequest(); return new XMLHttpRequest();
}; };
/** /**
* A single log event stream. * A single log event stream.
* @constructor * @constructor
* @param {Object} options * @param {Object} options
*/ */
function LogStream(options) { function LogStream(options) {
/** /**
* @const * @const
* @type {string} */ * @type {string} */
var _traceCode = options.trace ? (Math.random() + Math.PI).toString(36).substring(2, 10) : null; var _traceCode = options.trace
/** @type {string} */ ? (Math.random() + Math.PI).toString(36).substring(2, 10)
var _pageInfo = options.page_info; : null;
/** @type {string} */ /** @type {string} */
var _token = options.token; var _pageInfo = options.page_info;
/** @type {boolean} */ /** @type {string} */
var _print = options.print; var _token = options.token;
/** @type {boolean} */ /** @type {boolean} */
var _noFormat = options.no_format; var _print = options.print;
/** @type {boolean} */ /** @type {boolean} */
var _SSL = function() { var _noFormat = options.no_format;
if (typeof XDomainRequest === "undefined") { /** @type {boolean} */
return options.ssl; var _SSL = (function() {
} if (typeof XDomainRequest === "undefined") {
// If we're relying on XDomainRequest, we return options.ssl;
// must adhere to the page's encryption scheme. }
return window.location.protocol === "https:" ? true : false; // If we're relying on XDomainRequest, we
}(); // must adhere to the page's encryption scheme.
/** @type {string} */ return window.location.protocol === "https:" ? true : false;
var _endpoint; })();
if (window.LEENDPOINT) { /** @type {string} */
_endpoint = window.LEENDPOINT; var _endpoint;
} else if (_noFormat) { if (window.LEENDPOINT) {
_endpoint = "webhook.logentries.com/noformat"; _endpoint = window.LEENDPOINT;
} } else if (_noFormat) {
else { _endpoint = "webhook.logentries.com/noformat";
_endpoint = "js.logentries.com/v1"; } else {
} _endpoint = "js.logentries.com/v1";
_endpoint = (_SSL ? "https://" : "http://") + _endpoint + "/logs/" + _token; }
_endpoint = (_SSL ? "https://" : "http://") + _endpoint + "/logs/" + _token;
/** /**
* Flag to prevent further invocations on network err * Flag to prevent further invocations on network err
** @type {boolean} */ ** @type {boolean} */
var _shouldCall = true; var _shouldCall = true;
/** @type {Array.<string>} */ /** @type {Array.<string>} */
var _backlog = []; var _backlog = [];
/** @type {boolean} */ /** @type {boolean} */
var _active = false; var _active = false;
/** @type {boolean} */ /** @type {boolean} */
var _sentPageInfo = false; var _sentPageInfo = false;
var _apiCall = function(token, data) { var _apiCall = function(token, data) {
_active = true; _active = true;
var request = _getAjaxObject(); var request = _getAjaxObject();
if (_shouldCall) { if (_shouldCall) {
if (request.constructor === XMLHttpRequest) { if (request.constructor === XMLHttpRequest) {
// Currently we don't support fine-grained error // Currently we don't support fine-grained error
// handling in older versions of IE // handling in older versions of IE
request.onreadystatechange = function() { request.onreadystatechange = function() {
if (request.readyState === 4) { if (request.readyState === 4) {
// Handle any errors // Handle any errors
if (request.status >= 400) { if (request.status >= 400) {
console.error("Couldn't submit events."); console.error("Couldn't submit events.");
if (request.status === 410) { if (request.status === 410) {
// This API version has been phased out // This API version has been phased out
console.warn("This version of le_js is no longer supported!"); console.warn("This version of le_js is no longer supported!");
} }
} else { } else {
if (request.status === 301) { if (request.status === 301) {
// Server issued a deprecation warning // Server issued a deprecation warning
console.warn("This version of le_js is deprecated! Consider upgrading."); console.warn(
} "This version of le_js is deprecated! Consider upgrading."
if (_backlog.length > 0) { );
// Submit the next event in the backlog }
_apiCall(token, _backlog.shift()); if (_backlog.length > 0) {
} else { // Submit the next event in the backlog
_active = false; _apiCall(token, _backlog.shift());
} } else {
} _active = false;
} }
}
}
};
} else {
request.onload = function() {
if (_backlog.length > 0) {
// Submit the next event in the backlog
_apiCall(token, _backlog.shift());
} else {
_active = false;
}
};
}
}; request.open("POST", _endpoint, true);
} else { if (request.constructor === XMLHttpRequest) {
request.onload = function() { request.setRequestHeader("X-Requested-With", "XMLHttpRequest");
if (_backlog.length > 0) { request.setRequestHeader("Content-type", "application/json");
// Submit the next event in the backlog }
_apiCall(token, _backlog.shift());
} else {
_active = false;
}
};
}
request.open("POST", _endpoint, true); if (request.overrideMimeType) {
if (request.constructor === XMLHttpRequest) { request.overrideMimeType("text");
request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); }
request.setRequestHeader('Content-type', 'application/json');
}
if (request.overrideMimeType) { request.send(data);
request.overrideMimeType('text'); }
} };
request.send(data); var _getEvent = function() {
} var raw = null;
}; var args = Array.prototype.slice.call(arguments);
if (args.length === 0) {
throw new Error("No arguments!");
} else if (args.length === 1) {
raw = args[0];
} else {
// Handle a variadic overload,
// e.g. _rawLog("some text ", x, " ...", 1);
raw = args;
}
return raw;
};
var _getEvent = function() { var _agentInfo = function() {
var raw = null; var nav = window.navigator || { doNotTrack: undefined };
var args = Array.prototype.slice.call(arguments); var screen = window.screen || {};
if (args.length === 0) { var location = window.location || {};
throw new Error("No arguments!");
} else if (args.length === 1) {
raw = args[0];
} else {
// Handle a variadic overload,
// e.g. _rawLog("some text ", x, " ...", 1);
raw = args;
}
return raw;
};
var _agentInfo = function() { return {
var nav = window.navigator || {doNotTrack: undefined}; url: location.pathname,
var screen = window.screen || {}; referrer: document.referrer,
var location = window.location || {}; screen: {
width: screen.width,
height: screen.height
},
window: {
width: window.innerWidth,
height: window.innerHeight
},
browser: {
name: nav.appName,
version: nav.appVersion,
cookie_enabled: nav.cookieEnabled,
do_not_track: nav.doNotTrack
},
platform: nav.platform
};
};
return { // Single arg stops the compiler arity warning
url: location.pathname, var _rawLog = function(msg) {
referrer: document.referrer, var event = _getEvent.apply(this, arguments);
screen: {
width: screen.width,
height: screen.height
},
window: {
width: window.innerWidth,
height: window.innerHeight
},
browser: {
name: nav.appName,
version: nav.appVersion,
cookie_enabled: nav.cookieEnabled,
do_not_track: nav.doNotTrack
},
platform: nav.platform
};
};
// Single arg stops the compiler arity warning var data = { event: event };
var _rawLog = function(msg) {
var event = _getEvent.apply(this, arguments);
var data = {event: event}; // Add agent info if required
if (_pageInfo !== "never") {
if (!_sentPageInfo || _pageInfo === "per-entry") {
_sentPageInfo = true;
if (
typeof event.screen === "undefined" &&
typeof event.browser === "undefined"
)
_rawLog(_agentInfo())
.level("PAGE")
.send();
}
}
// Add agent info if required if (_traceCode) {
if (_pageInfo !== 'never') { data.trace = _traceCode;
if (!_sentPageInfo || _pageInfo === 'per-entry') { }
_sentPageInfo = true;
if (typeof event.screen === "undefined" &&
typeof event.browser === "undefined")
_rawLog(_agentInfo()).level('PAGE').send();
}
}
if (_traceCode) { return {
data.trace = _traceCode; level: function(l) {
} // Don't log PAGE events to console
// PAGE events are generated for the agentInfo function
if (_print && typeof console !== "undefined" && l !== "PAGE") {
var serialized = null;
if (typeof XDomainRequest !== "undefined") {
// We're using IE8/9
serialized = data.trace + " " + data.event;
}
try {
console[l.toLowerCase()].call(console, serialized || data);
} catch (ex) {
// IE compat fix
console.log(serialized || data);
}
}
data.level = l;
return {level: function(l) { return {
// Don't log PAGE events to console send: function() {
// PAGE events are generated for the agentInfo function var cache = [];
if (_print && typeof console !== "undefined" && l !== 'PAGE') { var serialized = JSON.stringify(data, function(key, value) {
var serialized = null; if (typeof value === "undefined") {
if (typeof XDomainRequest !== "undefined") { return "undefined";
// We're using IE8/9 } else if (typeof value === "object" && value !== null) {
serialized = data.trace + ' ' + data.event; if (_indexOf(cache, value) !== -1) {
} // We've seen this object before;
try { // return a placeholder instead to prevent
console[l.toLowerCase()].call(console, (serialized || data)); // cycles
} catch (ex) { return "<?>";
// IE compat fix }
console.log((serialized || data)); cache.push(value);
} }
} return value;
data.level = l; });
return {send: function() { if (_active) {
var cache = []; _backlog.push(serialized);
var serialized = JSON.stringify(data, function(key, value) { } else {
_apiCall(_token, serialized);
if (typeof value === "undefined") { }
return "undefined"; }
} else if (typeof value === "object" && value !== null) { };
if (_indexOf(cache, value) !== -1) { }
// We've seen this object before; };
// return a placeholder instead to prevent };
// cycles
return "<?>";
}
cache.push(value);
}
return value;
});
if (_active) {
_backlog.push(serialized);
} else {
_apiCall(_token, serialized);
}
}};
}};
};
if (options.catchall) {
var oldHandler = window.onerror;
var newHandler = function(msg, url, line) {
_rawLog({error: msg, line: line, location: url}).level('ERROR').send();
if (oldHandler) {
return oldHandler(msg, url, line);
} else {
return false;
}
};
window.onerror = newHandler;
}
/** @expose */
this.log = _rawLog;
if (options.catchall) {
var oldHandler = window.onerror;
var newHandler = function(msg, url, line) {
_rawLog({ error: msg, line: line, location: url })
.level("ERROR")
.send();
if (oldHandler) {
return oldHandler(msg, url, line);
} else {
return false;
}
};
window.onerror = newHandler;
}
/** @expose */
this.log = _rawLog;
} }
/** /**
* A single log object * A single log object
* @constructor * @constructor
* @param {Object} options * @param {Object} options
*/ */
function Logger(options) { function Logger(options) {
var logger; var logger;
// Default values // Default values
var dict = { var dict = {
ssl: true, ssl: true,
catchall: false, catchall: false,
trace: true, trace: true,
page_info: 'never', page_info: "never",
print: false, print: false,
endpoint: null, endpoint: null,
token: null token: null
}; };
if (typeof options === "object") if (typeof options === "object") for (var k in options) dict[k] = options[k];
for (var k in options) else throw new Error("Invalid parameters for createLogStream()");
dict[k] = options[k];
else
throw new Error("Invalid parameters for createLogStream()");
if (dict.token === null) { if (dict.token === null) {
throw new Error("Token not present."); throw new Error("Token not present.");
} else { } else {
logger = new LogStream(dict); logger = new LogStream(dict);
} }
var _log = function(msg) { var _log = function(msg) {
if (logger) { if (logger) {
return logger.log.apply(this, arguments); return logger.log.apply(this, arguments);
} else } else throw new Error("You must call LE.init(...) first.");
throw new Error("You must call LE.init(...) first."); };
};
// The public interface // The public interface
return { return {
log: function() { log: function() {
_log.apply(this, arguments).level('LOG').send(); _log
}, .apply(this, arguments)
warn: function() { .level("LOG")
_log.apply(this, arguments).level('WARN').send(); .send();
}, },
error: function() { warn: function() {
_log.apply(this, arguments).level('ERROR').send(); _log
}, .apply(this, arguments)
info: function() { .level("WARN")
_log.apply(this, arguments).level('INFO').send(); .send();
} },
}; error: function() {
_log
.apply(this, arguments)
.level("ERROR")
.send();
},
info: function() {
_log
.apply(this, arguments)
.level("INFO")
.send();
}
};
} }
// Array of Logger elements // Array of Logger elements
var loggers = {}; var loggers = {};
var _getLogger = function(name) { var _getLogger = function(name) {
if (!loggers.hasOwnProperty(name)) if (!loggers.hasOwnProperty(name))
throw new Error("Invalid name for logStream"); throw new Error("Invalid name for logStream");
return loggers[name]; return loggers[name];
}; };
var _createLogStream = function(options) { var _createLogStream = function(options) {
if (typeof options.name !== "string") if (typeof options.name !== "string") throw new Error("Name not present.");
throw new Error("Name not present."); else if (loggers.hasOwnProperty(options.name))
else if (loggers.hasOwnProperty(options.name)) throw new Error("A logger with that name already exists!");
throw new Error("A logger with that name already exists!"); loggers[options.name] = new Logger(options);
loggers[options.name] = new Logger(options);
return true; return true;
}; };
var _deprecatedInit = function(options) { var _deprecatedInit = function(options) {
var dict = { var dict = {
name : "default" name: "default"
}; };
if (typeof options === "object") if (typeof options === "object") for (var k in options) dict[k] = options[k];
for (var k in options) else if (typeof options === "string") dict.token = options;
dict[k] = options[k]; else throw new Error("Invalid parameters for init()");
else if (typeof options === "string")
dict.token = options;
else
throw new Error("Invalid parameters for init()");
return _createLogStream(dict); return _createLogStream(dict);
}; };
var _destroyLogStream = function(name) { var _destroyLogStream = function(name) {
if (typeof name === 'undefined'){ if (typeof name === "undefined") {
name = 'default'; name = "default";
} }
delete loggers[name]; delete loggers[name];
}; };
// The public interface // The public interface
export default { export default {
init: _deprecatedInit, init: _deprecatedInit,
createLogStream: _createLogStream, createLogStream: _createLogStream,
to: _getLogger, to: _getLogger,
destroy: _destroyLogStream, destroy: _destroyLogStream,
log: function() { log: function() {
for (var k in loggers) for (var k in loggers) loggers[k].log.apply(this, arguments);
loggers[k].log.apply(this, arguments); },
}, warn: function() {
warn: function() { for (var k in loggers) loggers[k].warn.apply(this, arguments);
for (var k in loggers) },
loggers[k].warn.apply(this, arguments); error: function() {
}, for (var k in loggers) loggers[k].error.apply(this, arguments);
error: function() { },
for (var k in loggers) info: function() {
loggers[k].error.apply(this, arguments); for (var k in loggers) loggers[k].info.apply(this, arguments);
}, }
info: function() {
for (var k in loggers)
loggers[k].info.apply(this, arguments);
}
}; };

View File

@ -1,17 +1,18 @@
import Raven from 'raven-js'; import Raven from "raven-js";
function init() { function init() {
if(process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === "production") {
const opts = {}; const opts = {};
if(typeof __webpack_hash__ !== 'undefined') { if (typeof __webpack_hash__ !== "undefined") {
opts['release'] = __webpack_hash__; // eslint-disable-line no-undef opts["release"] = __webpack_hash__; // eslint-disable-line no-undef
}
Raven
.config('https://50006e5d91894f508dd288bbbf4585a6@sentry.io/185303', opts)
.install();
} }
Raven.config(
"https://50006e5d91894f508dd288bbbf4585a6@sentry.io/185303",
opts
).install();
}
} }
export default {init}; export default { init };

View File

@ -8,9 +8,9 @@ const bitArray = {
* slice until the end of the array. * slice until the end of the array.
* @return {bitArray} The requested slice. * @return {bitArray} The requested slice.
*/ */
bitSlice: function (a, bstart, bend) { bitSlice: function(a, bstart, bend) {
a = bitArray._shiftRight(a.slice(bstart/32), 32 - (bstart & 31)).slice(1); a = bitArray._shiftRight(a.slice(bstart / 32), 32 - (bstart & 31)).slice(1);
return (bend === undefined) ? a : bitArray.clamp(a, bend-bstart); return bend === undefined ? a : bitArray.clamp(a, bend - bstart);
}, },
/** /**
@ -23,15 +23,17 @@ const bitArray = {
extract: function(a, bstart, blength) { extract: function(a, bstart, blength) {
// FIXME: this Math.floor is not necessary at all, but for some reason // FIXME: this Math.floor is not necessary at all, but for some reason
// seems to suppress a bug in the Chromium JIT. // seems to suppress a bug in the Chromium JIT.
var x, sh = Math.floor((-bstart-blength) & 31); var x,
if ((bstart + blength - 1 ^ bstart) & -32) { sh = Math.floor((-bstart - blength) & 31);
if (((bstart + blength - 1) ^ bstart) & -32) {
// it crosses a boundary // it crosses a boundary
x = (a[bstart/32|0] << (32 - sh)) ^ (a[bstart/32+1|0] >>> sh); x =
(a[(bstart / 32) | 0] << (32 - sh)) ^ (a[(bstart / 32 + 1) | 0] >>> sh);
} else { } else {
// within a single word // within a single word
x = a[bstart/32|0] >>> sh; x = a[(bstart / 32) | 0] >>> sh;
} }
return x & ((1<<blength) - 1); return x & ((1 << blength) - 1);
}, },
/** /**
@ -40,16 +42,22 @@ const bitArray = {
* @param {bitArray} a2 The second array. * @param {bitArray} a2 The second array.
* @return {bitArray} The concatenation of a1 and a2. * @return {bitArray} The concatenation of a1 and a2.
*/ */
concat: function (a1, a2) { concat: function(a1, a2) {
if (a1.length === 0 || a2.length === 0) { if (a1.length === 0 || a2.length === 0) {
return a1.concat(a2); return a1.concat(a2);
} }
var last = a1[a1.length-1], shift = bitArray.getPartial(last); var last = a1[a1.length - 1],
shift = bitArray.getPartial(last);
if (shift === 32) { if (shift === 32) {
return a1.concat(a2); return a1.concat(a2);
} else { } else {
return bitArray._shiftRight(a2, shift, last|0, a1.slice(0,a1.length-1)); return bitArray._shiftRight(
a2,
shift,
last | 0,
a1.slice(0, a1.length - 1)
);
} }
}, },
@ -58,11 +66,14 @@ const bitArray = {
* @param {bitArray} a The array. * @param {bitArray} a The array.
* @return {Number} The length of a, in bits. * @return {Number} The length of a, in bits.
*/ */
bitLength: function (a) { bitLength: function(a) {
var l = a.length, x; var l = a.length,
if (l === 0) { return 0; } x;
if (l === 0) {
return 0;
}
x = a[l - 1]; x = a[l - 1];
return (l-1) * 32 + bitArray.getPartial(x); return (l - 1) * 32 + bitArray.getPartial(x);
}, },
/** /**
@ -71,13 +82,15 @@ const bitArray = {
* @param {Number} len The length to truncate to, in bits. * @param {Number} len The length to truncate to, in bits.
* @return {bitArray} A new array, truncated to len bits. * @return {bitArray} A new array, truncated to len bits.
*/ */
clamp: function (a, len) { clamp: function(a, len) {
if (a.length * 32 < len) { return a; } if (a.length * 32 < len) {
return a;
}
a = a.slice(0, Math.ceil(len / 32)); a = a.slice(0, Math.ceil(len / 32));
var l = a.length; var l = a.length;
len &= 31; len &= 31;
if (l > 0 && len) { if (l > 0 && len) {
a[l-1] = bitArray.partial(len, a[l-1] & (0x80000000 >> (len-1)), 1); a[l - 1] = bitArray.partial(len, a[l - 1] & (0x80000000 >> (len - 1)), 1);
} }
return a; return a;
}, },
@ -89,9 +102,11 @@ const bitArray = {
* @param {Number} [_end=0] Pass 1 if x has already been shifted to the high side. * @param {Number} [_end=0] Pass 1 if x has already been shifted to the high side.
* @return {Number} The partial word. * @return {Number} The partial word.
*/ */
partial: function (len, x, _end) { partial: function(len, x, _end) {
if (len === 32) { return x; } if (len === 32) {
return (_end ? x|0 : x << (32-len)) + len * 0x10000000000; return x;
}
return (_end ? x | 0 : x << (32 - len)) + len * 0x10000000000;
}, },
/** /**
@ -99,8 +114,8 @@ const bitArray = {
* @param {Number} x The partial word. * @param {Number} x The partial word.
* @return {Number} The number of bits used by the partial word. * @return {Number} The number of bits used by the partial word.
*/ */
getPartial: function (x) { getPartial: function(x) {
return Math.round(x/0x10000000000) || 32; return Math.round(x / 0x10000000000) || 32;
}, },
/** /**
@ -109,15 +124,16 @@ const bitArray = {
* @param {bitArray} b The second array. * @param {bitArray} b The second array.
* @return {boolean} true if a == b; false otherwise. * @return {boolean} true if a == b; false otherwise.
*/ */
equal: function (a, b) { equal: function(a, b) {
if (bitArray.bitLength(a) !== bitArray.bitLength(b)) { if (bitArray.bitLength(a) !== bitArray.bitLength(b)) {
return false; return false;
} }
var x = 0, i; var x = 0,
for (i=0; i<a.length; i++) { i;
x |= a[i]^b[i]; for (i = 0; i < a.length; i++) {
x |= a[i] ^ b[i];
} }
return (x === 0); return x === 0;
}, },
/** Shift an array right. /** Shift an array right.
@ -127,9 +143,13 @@ const bitArray = {
* @param {bitArray} [out=[]] An array to prepend to the output. * @param {bitArray} [out=[]] An array to prepend to the output.
* @private * @private
*/ */
_shiftRight: function (a, shift, carry, out) { _shiftRight: function(a, shift, carry, out) {
var i, last2=0, shift2; var i,
if (out === undefined) { out = []; } last2 = 0,
shift2;
if (out === undefined) {
out = [];
}
for (; shift >= 32; shift -= 32) { for (; shift >= 32; shift -= 32) {
out.push(carry); out.push(carry);
@ -139,21 +159,27 @@ const bitArray = {
return out.concat(a); return out.concat(a);
} }
for (i=0; i<a.length; i++) { for (i = 0; i < a.length; i++) {
out.push(carry | (a[i]>>>shift)); out.push(carry | (a[i] >>> shift));
carry = a[i] << (32-shift); carry = a[i] << (32 - shift);
} }
last2 = a.length ? a[a.length-1] : 0; last2 = a.length ? a[a.length - 1] : 0;
shift2 = bitArray.getPartial(last2); shift2 = bitArray.getPartial(last2);
out.push(bitArray.partial(shift+shift2 & 31, (shift + shift2 > 32) ? carry : out.pop(),1)); out.push(
bitArray.partial(
(shift + shift2) & 31,
shift + shift2 > 32 ? carry : out.pop(),
1
)
);
return out; return out;
}, },
/** xor a block of 4 words together. /** xor a block of 4 words together.
* @private * @private
*/ */
_xor4: function(x,y) { _xor4: function(x, y) {
return [x[0]^y[0],x[1]^y[1],x[2]^y[2],x[3]^y[3]]; return [x[0] ^ y[0], x[1] ^ y[1], x[2] ^ y[2], x[3] ^ y[3]];
}, },
/** byteswap a word array inplace. /** byteswap a word array inplace.
@ -162,7 +188,9 @@ const bitArray = {
* @return {bitArray} byteswapped array * @return {bitArray} byteswapped array
*/ */
byteswapM: function(a) { byteswapM: function(a) {
var i, v, m = 0xff00; var i,
v,
m = 0xff00;
for (i = 0; i < a.length; ++i) { for (i = 0; i < a.length; ++i) {
v = a[i]; v = a[i];
a[i] = (v >>> 24) | ((v >>> 8) & m) | ((v & m) << 8) | (v << 24); a[i] = (v >>> 24) | ((v >>> 8) & m) | ((v & m) << 8) | (v << 24);
@ -173,18 +201,20 @@ const bitArray = {
export default { export default {
fromBytes: function(bytes) { fromBytes: function(bytes) {
var out = [], i, tmp=0; var out = [],
for (i=0; i<bytes.length; i++) { i,
tmp = 0;
for (i = 0; i < bytes.length; i++) {
tmp = (tmp << 8) | bytes[i]; tmp = (tmp << 8) | bytes[i];
if ((i&3) === 3) { if ((i & 3) === 3) {
out.push(tmp); out.push(tmp);
tmp = 0; tmp = 0;
} }
} }
if (i&3) { if (i & 3) {
out.push(bitArray.partial(8*(i&3), tmp)); out.push(bitArray.partial(8 * (i & 3), tmp));
} }
return out; return out;
}, },
...bitArray ...bitArray
}; };

View File

@ -1,15 +1,15 @@
const Uint64BE = require('int64-buffer').Uint64BE const Uint64BE = require("int64-buffer").Uint64BE;
export function formatForMsg(msg) { export function formatForMsg(msg) {
return {bstart: 0, bend: 15} return { bstart: 0, bend: 15 };
} }
export function formatMsgDec(msg) { export function formatMsgDec(msg) {
const {bstart, bend} = formatForMsg(msg) const { bstart, bend } = formatForMsg(msg);
const uint = Uint64BE(msg[1]) const uint = Uint64BE(msg[1]);
var tt = "0"+uint.toString(2); var tt = "0" + uint.toString(2);
tt = tt.substring(0, tt.length - (63-bend)) tt = tt.substring(0, tt.length - (63 - bend));
tt = tt.substring(tt.length - (bend-bstart) - 1) tt = tt.substring(tt.length - (bend - bstart) - 1);
return [msg[0], parseInt(tt, 2)]; return [msg[0], parseInt(tt, 2)];
} }
@ -20,17 +20,17 @@ export function uint64BEToHex(int64) {
export function int64BufferToPrettyHexStr(buffer) { export function int64BufferToPrettyHexStr(buffer) {
const uint = Uint64BE(buffer); const uint = Uint64BE(buffer);
let hex = uint.toString(16); let hex = uint.toString(16);
if(hex.length === 1) hex = '0' + hex; if (hex.length === 1) hex = "0" + hex;
let hexParts = hex.match(/.{1,2}/g); let hexParts = hex.match(/.{1,2}/g);
return hexParts.join(" ") return hexParts.join(" ");
} }
export function formatMsgHex(msg) { export function formatMsgHex(msg) {
const uint = Uint64BE(msg[1]); const uint = Uint64BE(msg[1]);
let hex = uint.toString(16); let hex = uint.toString(16);
if(hex.length === 1) hex = '0' + hex; if (hex.length === 1) hex = "0" + hex;
let hexParts = hex.match(/.{1,2}/g); let hexParts = hex.match(/.{1,2}/g);
return [msg[0], hexParts.join(" ")] return [msg[0], hexParts.join(" ")];
} }

View File

@ -1,11 +1,11 @@
export default class BoardUnit { export default class BoardUnit {
constructor(name) { constructor(name) {
this.name = name; this.name = name;
this.attributes = {}; this.attributes = {};
this.comment = null this.comment = null;
} }
text() { text() {
return this.name; return this.name;
} }
} }

View File

@ -421,12 +421,9 @@ export default class DBC {
messageId = parseInt(messageId, 10); messageId = parseInt(messageId, 10);
const msg = messages.get(messageId); const msg = messages.get(messageId);
if (msg === undefined) { if (msg === undefined) {
warnings.push(`failed to parse signal comment on line ${i + 1} -- ${ warnings.push(`failed to parse signal comment on line ${i +
line 1} -- ${line}:
}: message id ${messageId} does not exist prior to this line`);
message id ${
messageId
} does not exist prior to this line`);
continue; continue;
} }
const signal = msg.signals[signalName]; const signal = msg.signals[signalName];

View File

@ -1,13 +1,17 @@
function findTimeIndex(entries, time) { function findTimeIndex(entries, time) {
return entries.findIndex((e) => e.time >= time); return entries.findIndex(e => e.time >= time);
} }
function findRelativeTimeIndex(entries, relTime) { function findRelativeTimeIndex(entries, relTime) {
return entries.findIndex((e) => e.relTime >= relTime); return entries.findIndex(e => e.relTime >= relTime);
} }
function findSegmentIndices(entries, [segmentTimeLow, segmentTimeHi], isRelative) { function findSegmentIndices(
/* entries,
[segmentTimeLow, segmentTimeHi],
isRelative
) {
/*
Finds pair of indices (inclusive, exclusive) within entries array Finds pair of indices (inclusive, exclusive) within entries array
whose timestamps match segmentTimeLow and segmentTimeHi. whose timestamps match segmentTimeLow and segmentTimeHi.
if isRelative === true, then the segment times if isRelative === true, then the segment times
@ -16,15 +20,19 @@ function findSegmentIndices(entries, [segmentTimeLow, segmentTimeHi], isRelative
Returns `[segmentIdxLow, segmentIdxHigh]` Returns `[segmentIdxLow, segmentIdxHigh]`
(inclusive, exclusive) (inclusive, exclusive)
*/ */
let timeIndexFunc = (isRelative === true ? findRelativeTimeIndex : findTimeIndex); let timeIndexFunc =
isRelative === true ? findRelativeTimeIndex : findTimeIndex;
const segmentIdxLow = timeIndexFunc(entries, segmentTimeLow); const segmentIdxLow = timeIndexFunc(entries, segmentTimeLow);
const upperSegments = entries.slice(segmentIdxLow); const upperSegments = entries.slice(segmentIdxLow);
let upperSegmentIdxHi = timeIndexFunc(upperSegments, segmentTimeHi); let upperSegmentIdxHi = timeIndexFunc(upperSegments, segmentTimeHi);
const segmentIdxHi = (upperSegmentIdxHi >= 0 ? upperSegmentIdxHi + segmentIdxLow + 1 : entries.length - 1) const segmentIdxHi =
upperSegmentIdxHi >= 0
? upperSegmentIdxHi + segmentIdxLow + 1
: entries.length - 1;
return [segmentIdxLow, Math.min(segmentIdxHi, entries.length - 1)] return [segmentIdxLow, Math.min(segmentIdxHi, entries.length - 1)];
} }
export default {findTimeIndex, findRelativeTimeIndex, findSegmentIndices}; export default { findTimeIndex, findRelativeTimeIndex, findSegmentIndices };

View File

@ -1,56 +1,63 @@
export default class Frame { export default class Frame {
constructor({name, constructor({
id = 0, name,
size = 0, id = 0,
transmitters = [], size = 0,
extended = 0, transmitters = [],
comment = null, extended = 0,
signals = {}}) { comment = null,
Object.assign(this, {name, signals = {}
id, }) {
size, Object.assign(this, {
transmitters, name,
extended, id,
comment, size,
signals}) transmitters,
extended,
comment,
signals
});
}
nextNewTransmitterName() {
let txNum = 1,
txName;
do {
txName = "NEW_TRANSMITTER_" + txNum;
txNum++;
} while (this.transmitters.indexOf(txName) !== -1);
return txName;
}
addTransmitter() {
const txName = this.nextNewTransmitterName();
this.transmitters.push(txName);
return txName;
}
header() {
return (
`BO_ ${this.id} ${this.name}: ${this.size} ` +
`${this.transmitters[0] || "XXX"}`
);
}
text() {
const signals = Object.values(this.signals)
.map(signal => " " + signal.text()) // indent
.join("\n");
if (signals.length > 0) {
return this.header() + "\n" + signals;
} else {
return this.header();
} }
}
nextNewTransmitterName() { copy() {
let txNum = 1, txName; const copy = Object.assign(Object.create(this), this);
do {
txName = 'NEW_TRANSMITTER_' + txNum;
txNum++;
} while(this.transmitters.indexOf(txName) !== -1);
return txName; return copy;
} }
addTransmitter() {
const txName = this.nextNewTransmitterName();
this.transmitters.push(txName);
return txName;
}
header() {
return `BO_ ${this.id} ${this.name}: ${this.size} ` +
`${this.transmitters[0] || 'XXX'}`;
}
text() {
const signals = Object.values(this.signals)
.map((signal) => " " + signal.text()) // indent
.join("\n");
if(signals.length > 0) {
return this.header() + "\n" + signals;
} else {
return this.header();
}
}
copy() {
const copy = Object.assign(Object.create(this), this);
return copy;
}
} }

View File

@ -1,141 +1,173 @@
import ArrayUtils from '../utils/array'; import ArrayUtils from "../utils/array";
import {CAN_GRAPH_MAX_POINTS} from '../config'; import { CAN_GRAPH_MAX_POINTS } from "../config";
function _calcGraphData(msg, signalUid, firstCanTime) { function _calcGraphData(msg, signalUid, firstCanTime) {
if(!msg) return null; if (!msg) return null;
const signal = Object.values(msg.frame.signals).find((s) => s.uid === signalUid); const signal = Object.values(msg.frame.signals).find(
if(!signal) { s => s.uid === signalUid
console.warn('_calcGraphData: no signal', signalUid, msg) );
return null; if (!signal) {
} console.warn("_calcGraphData: no signal", signalUid, msg);
let samples = []; return null;
let skip = Math.floor(msg.entries.length / CAN_GRAPH_MAX_POINTS); }
let samples = [];
let skip = Math.floor(msg.entries.length / CAN_GRAPH_MAX_POINTS);
if(skip === 0){ if (skip === 0) {
samples = msg.entries; samples = msg.entries;
} else { } else {
for(let i = 0; i < msg.entries.length; i += skip) { for (let i = 0; i < msg.entries.length; i += skip) {
samples.push(msg.entries[i]); samples.push(msg.entries[i]);
}
// Always include last message entry, which faciliates graphData comparison
samples.push(msg.entries[msg.entries.length - 1]);
} }
return samples.filter((e) => e.signals[signal.name] !== undefined) // Always include last message entry, which faciliates graphData comparison
.map((entry) => { samples.push(msg.entries[msg.entries.length - 1]);
return {x: entry.time, }
relTime: entry.relTime, return samples
y: entry.signals[signal.name], .filter(e => e.signals[signal.name] !== undefined)
unit: signal.unit, .map(entry => {
color: `rgba(${signal.colors.join(",")}, 0.5)`, return {
signalName: signal.name, x: entry.time,
signalUid} relTime: entry.relTime,
y: entry.signals[signal.name],
unit: signal.unit,
color: `rgba(${signal.colors.join(",")}, 0.5)`,
signalName: signal.name,
signalUid
};
}); });
} }
function appendNewGraphData(plottedSignals, graphData, messages, firstCanTime) { function appendNewGraphData(plottedSignals, graphData, messages, firstCanTime) {
const messagesPerPlot = plottedSignals.map((plottedMessages) => const messagesPerPlot = plottedSignals.map(plottedMessages =>
plottedMessages.reduce((messages, plottedMessages.reduce((messages, { messageId, signalUid }) => {
{messageId, signalUid}) => { messages.push(messageId);
messages.push(messageId); return messages;
return messages; }, [])
}, []) );
const extendedPlots = messagesPerPlot
.map((plottedMessageIds, index) => {
return { plottedMessageIds, index };
}) // preserve index so we can look up graphData
.filter(({ plottedMessageIds, index }) => {
if (index < graphData.length) {
let maxGraphTime = 0;
const { series } = graphData[index];
if (series.length > 0) {
maxGraphTime = series[graphData[index].series.length - 1].relTime;
}
return plottedMessageIds.some(
messageId =>
(messages[messageId].entries.length > 0 && series.length === 0) ||
messages[messageId].entries.some(e => e.relTime > maxGraphTime)
);
} else {
return false;
}
})
.map(({ plottedMessageIds, index }) => {
plottedMessageIds = plottedMessageIds.reduce((arr, messageId) => {
if (arr.indexOf(messageId) === -1) {
arr.push(messageId);
}
return arr;
}, []);
return { plottedMessageIds, index };
});
extendedPlots.forEach(({ plottedMessageIds, index }) => {
const signalUidsByMessageId = plottedSignals[index].reduce(
(obj, { messageId, signalUid }) => {
if (!obj[messageId]) {
obj[messageId] = [];
}
obj[messageId].push(signalUid);
return obj;
},
{}
);
const { series } = graphData[index];
const graphDataMaxMessageTimes = plottedMessageIds.reduce(
(obj, messageId) => {
const signalUids = signalUidsByMessageId[messageId];
const maxIndex = ArrayUtils.findIndexRight(series, entry => {
return signalUids.indexOf(entry.signalUid) !== -1;
});
if (maxIndex) {
obj[messageId] = series[maxIndex].relTime;
} else if (series.length > 0) {
obj[messageId] = series[series.length - 1].relTime;
} else {
// Graph data is empty
obj[messageId] = -1;
}
return obj;
},
{}
);
let newGraphData = [];
plottedMessageIds
.map(messageId => {
return { messageId, entries: messages[messageId].entries };
})
.filter(
(
{ messageId, entries } // Filter to only messages with stale graphData
) =>
entries[entries.length - 1].relTime >
graphDataMaxMessageTimes[messageId]
)
.forEach(({ messageId, entries }) => {
// Compute and append new graphData
let firstNewEntryIdx = entries.findIndex(
entry => entry.relTime > graphDataMaxMessageTimes[messageId]
); );
const extendedPlots = messagesPerPlot const newEntries = entries.slice(firstNewEntryIdx);
.map((plottedMessageIds, index) => {return {plottedMessageIds, index}}) // preserve index so we can look up graphData signalUidsByMessageId[messageId].forEach(signalUid => {
.filter(({plottedMessageIds, index}) => { const signalGraphData = _calcGraphData(
if(index < graphData.length) { {
let maxGraphTime = 0; ...messages[messageId],
const { series } = graphData[index]; entries: newEntries
if(series.length > 0) { },
maxGraphTime = series[graphData[index].series.length - 1].relTime; signalUid,
} firstCanTime
);
return plottedMessageIds.some((messageId) => newGraphData = newGraphData.concat(signalGraphData);
(messages[messageId].entries.length > 0 && series.length === 0)
||
messages[messageId].entries.some((e) => e.relTime > maxGraphTime));
} else {
return false;
}
}).map(({plottedMessageIds, index}) => {
plottedMessageIds = plottedMessageIds.reduce((arr, messageId) => {
if(arr.indexOf(messageId) === -1) {
arr.push(messageId);
}
return arr;
}, []);
return {plottedMessageIds, index};
});
extendedPlots.forEach(({plottedMessageIds, index}) => {
const signalUidsByMessageId = plottedSignals[index].reduce((obj, {messageId, signalUid}) => {
if(!obj[messageId]) {
obj[messageId] = []
}
obj[messageId].push(signalUid);
return obj;
}, {});
const { series } = graphData[index];
const graphDataMaxMessageTimes = plottedMessageIds.reduce((obj, messageId) => {
const signalUids = signalUidsByMessageId[messageId];
const maxIndex = ArrayUtils.findIndexRight(series, (entry) => {
return signalUids.indexOf(entry.signalUid) !== -1
});
if(maxIndex) {
obj[messageId] = series[maxIndex].relTime;
} else if(series.length > 0) {
obj[messageId] = series[series.length - 1].relTime;
} else {
// Graph data is empty
obj[messageId] = -1;
}
return obj;
}, {});
let newGraphData = [];
plottedMessageIds.map((messageId) => {
return { messageId, entries: messages[messageId].entries }
}).filter(({messageId, entries}) => // Filter to only messages with stale graphData
entries[entries.length - 1].relTime > graphDataMaxMessageTimes[messageId])
.forEach(({messageId, entries}) => { // Compute and append new graphData
let firstNewEntryIdx = entries.findIndex((entry) =>
entry.relTime > graphDataMaxMessageTimes[messageId]);
const newEntries = entries.slice(firstNewEntryIdx);
signalUidsByMessageId[messageId].forEach((signalUid) => {
const signalGraphData = _calcGraphData({...messages[messageId],
entries: newEntries},
signalUid,
firstCanTime);
newGraphData = newGraphData.concat(signalGraphData);
});
}); });
});
const messageIdOutOfBounds = ( const messageIdOutOfBounds =
series.length > 0 series.length > 0 &&
&& plottedMessageIds.find((messageId) => plottedMessageIds.find(
messages[messageId].entries.length > 0 messageId =>
&& series[0].relTime < messages[messageId].entries[0].relTime)); messages[messageId].entries.length > 0 &&
graphData[index] = { series[0].relTime < messages[messageId].entries[0].relTime
series: graphData[index].series.concat(newGraphData), );
updated: Date.now() graphData[index] = {
}; series: graphData[index].series.concat(newGraphData),
updated: Date.now()
};
if(messageIdOutOfBounds) { if (messageIdOutOfBounds) {
const graphDataLowerBound = graphData[index].series.findIndex( const graphDataLowerBound = graphData[index].series.findIndex(
(e) => e.relTime > messages[messageIdOutOfBounds].entries[0].relTime); e => e.relTime > messages[messageIdOutOfBounds].entries[0].relTime
);
if(graphDataLowerBound) { if (graphDataLowerBound) {
graphData[index].series = graphData[index].series.slice(graphDataLowerBound); graphData[index].series = graphData[index].series.slice(
} graphDataLowerBound
} );
}); }
}
});
return [...graphData]; return [...graphData];
} }
export default {_calcGraphData, appendNewGraphData}; export default { _calcGraphData, appendNewGraphData };

View File

@ -1,51 +1,67 @@
import React from 'react'; import React from "react";
import {StyleSheet, css} from 'aphrodite/no-important'; import { StyleSheet, css } from "aphrodite/no-important";
import * as ObjectUtils from '../utils/object'; import * as ObjectUtils from "../utils/object";
function createImageComponent(source, alt, styles) { function createImageComponent(source, alt, styles) {
if(styles === undefined) { if (styles === undefined) {
styles = [] styles = [];
} else if(!Array.isArray(styles)) { } else if (!Array.isArray(styles)) {
styles = [styles]; styles = [styles];
}
return props => {
let localStyles = styles.slice();
if (Array.isArray(props.styles)) {
localStyles = localStyles.concat(props.styles);
// filter 'styles' from props, which is passed via spread to <img> tag.
props = ObjectUtils.fromArray(
Object.entries(props).filter(([k, v]) => k !== "styles")
);
} }
return (props) => { return (
let localStyles = styles.slice(); <img src={source} className={css(...localStyles)} alt={alt} {...props} />
if(Array.isArray(props.styles)) { );
localStyles = localStyles.concat(props.styles); };
// filter 'styles' from props, which is passed via spread to <img> tag.
props = ObjectUtils.fromArray(Object.entries(props).filter(([k,v]) => k !== 'styles'));
}
return <img src={source} className={css(...localStyles)} alt={alt} {...props} />
};
} }
const Styles = StyleSheet.create({ const Styles = StyleSheet.create({
materialIcon: { materialIcon: {
width: 24, width: 24,
height: 24, height: 24
}, },
pointer: { pointer: {
cursor: 'pointer' cursor: "pointer"
} }
}); });
const leftArrow = createImageComponent(process.env.PUBLIC_URL + "/img/ic_arrow_left_black_24dp.png", const leftArrow = createImageComponent(
'Left arrow', process.env.PUBLIC_URL + "/img/ic_arrow_left_black_24dp.png",
Styles.materialIcon); "Left arrow",
const rightArrow = createImageComponent(process.env.PUBLIC_URL + "/img/ic_arrow_right_black_24dp.png", Styles.materialIcon
'Right arrow', );
Styles.materialIcon); const rightArrow = createImageComponent(
process.env.PUBLIC_URL + "/img/ic_arrow_right_black_24dp.png",
"Right arrow",
Styles.materialIcon
);
const downArrow = createImageComponent(process.env.PUBLIC_URL + "/img/ic_arrow_drop_down_black_24dp.png", const downArrow = createImageComponent(
'Down arrow', process.env.PUBLIC_URL + "/img/ic_arrow_drop_down_black_24dp.png",
Styles.materialIcon) "Down arrow",
Styles.materialIcon
);
const clear = createImageComponent(process.env.PUBLIC_URL + "/img/ic_clear_black_24dp.png", const clear = createImageComponent(
'Clear', process.env.PUBLIC_URL + "/img/ic_clear_black_24dp.png",
[Styles.materialIcon, Styles.pointer]); "Clear",
[Styles.materialIcon, Styles.pointer]
);
const panda = createImageComponent(process.env.PUBLIC_URL + "/img/panda.png", 'Panda', []); const panda = createImageComponent(
export default {rightArrow, leftArrow, downArrow, clear, panda}; process.env.PUBLIC_URL + "/img/panda.png",
"Panda",
[]
);
export default { rightArrow, leftArrow, downArrow, clear, panda };

View File

@ -1,16 +1,16 @@
import {StyleSheet} from 'aphrodite/no-important'; import { StyleSheet } from "aphrodite/no-important";
export default StyleSheet.create({ export default StyleSheet.create({
tab: { tab: {
display: 'inline', display: "inline",
marginRight: 20, marginRight: 20,
cursor: 'pointer' cursor: "pointer"
}, },
selectedTab: { selectedTab: {
borderBottom: '2px solid #000', borderBottom: "2px solid #000",
fontWeight: 'bold' fontWeight: "bold"
}, },
tabContent: { tabContent: {
paddingTop: 20 paddingTop: 20
} }
}); });

View File

@ -1,22 +1,22 @@
import {StyleSheet} from 'aphrodite/no-important'; import { StyleSheet } from "aphrodite/no-important";
export default StyleSheet.create({ export default StyleSheet.create({
root: { root: {
flexDirection: 'row' flexDirection: "row"
}, },
button: { button: {
cursor: 'pointer', cursor: "pointer",
backgroundColor: 'RGB(63, 135, 255)', backgroundColor: "RGB(63, 135, 255)",
borderRadius: 5, borderRadius: 5,
minWidth: 80, minWidth: 80,
paddingLeft: 20, paddingLeft: 20,
paddingRight: 20, paddingRight: 20,
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
color: 'white', color: "white",
':hover': { ":hover": {
backgroundColor: 'RGBA(63, 135, 255, 0.5)' backgroundColor: "RGBA(63, 135, 255, 0.5)"
}
} }
}); }
});

View File

@ -1,13 +1,15 @@
function elementWiseEquals(arr1, arr2) { function elementWiseEquals(arr1, arr2) {
return arr1.length === arr2.length && arr1.every((ele, idx) => arr2[idx] === ele); return (
arr1.length === arr2.length && arr1.every((ele, idx) => arr2[idx] === ele)
);
} }
function findIndexRight(arr, condition) { function findIndexRight(arr, condition) {
for(let i = arr.length - 1; i >= 0; i--) { for (let i = arr.length - 1; i >= 0; i--) {
if(condition(arr[i])) { if (condition(arr[i])) {
return i; return i;
}
} }
}
} }
export default {elementWiseEquals, findIndexRight}; export default { elementWiseEquals, findIndexRight };

View File

@ -1,15 +1,17 @@
export function shade([r, g, b], opacity) { export function shade([r, g, b], opacity) {
/* /*
@param {Array} rgb @param {Array} rgb
@param {Number} opacity -1.0 -> +1.0 @param {Number} opacity -1.0 -> +1.0
negative opacity will darken image negative opacity will darken image
returns new RGB array returns new RGB array
*/ */
const t = (opacity < 0) ? 0 : 255; const t = opacity < 0 ? 0 : 255;
const opacityPos = Math.abs(opacity); const opacityPos = Math.abs(opacity);
return [Math.round((t - r) * opacityPos) + r, return [
Math.round((t - g) * opacityPos) + g, Math.round((t - r) * opacityPos) + r,
Math.round((t - b) * opacityPos) + b]; Math.round((t - g) * opacityPos) + g,
} Math.round((t - b) * opacityPos) + b
];
}

View File

@ -4,19 +4,17 @@
// Useful for component testing // Useful for component testing
import { css } from 'aphrodite'; import { css } from "aphrodite";
import classNames from 'classnames'; import classNames from "classnames";
export default function() { export default function() {
const styles = []; const styles = [];
const classes = []; const classes = [];
[].forEach.call(arguments, it => { [].forEach.call(arguments, it => {
if (it && it._definition && it._name) if (it && it._definition && it._name) styles.push(it);
styles.push(it); else classes.push(it);
else
classes.push(it);
}); });
return classNames.apply(null, [ ...classes, css(styles) ]); return classNames.apply(null, [...classes, css(styles)]);
} }

View File

@ -1,145 +1,184 @@
require('core-js/fn/array/from'); require("core-js/fn/array/from");
function findMaxByteStateChangeCount(messages) { function findMaxByteStateChangeCount(messages) {
return Object.values(messages).map((m) => m.byteStateChangeCounts) return Object.values(messages)
.reduce((counts, countArr) => counts.concat(countArr), []) // flatten arrays .map(m => m.byteStateChangeCounts)
.reduce((count1, count2) => count1 > count2 ? count1 : count2, 0); // find max .reduce((counts, countArr) => counts.concat(countArr), []) // flatten arrays
.reduce((count1, count2) => (count1 > count2 ? count1 : count2), 0); // find max
} }
function addCanMessage([address, busTime, data, source], dbc, canStartTime, messages, prevMsgEntries, byteStateChangeCountsByMessage) { function addCanMessage(
var id = source + ":" + address.toString(16); [address, busTime, data, source],
dbc,
canStartTime,
messages,
prevMsgEntries,
byteStateChangeCountsByMessage
) {
var id = source + ":" + address.toString(16);
if (messages[id] === undefined) messages[id] = createMessageSpec(dbc, address, id, source); if (messages[id] === undefined)
messages[id] = createMessageSpec(dbc, address, id, source);
const prevMsgEntry = messages[id].entries.length > 0 ? const prevMsgEntry =
messages[id].entries[messages[id].entries.length - 1] messages[id].entries.length > 0
: ? messages[id].entries[messages[id].entries.length - 1]
(prevMsgEntries[id] || null); : prevMsgEntries[id] || null;
if(byteStateChangeCountsByMessage[id] && messages[id].byteStateChangeCounts.every((c) => c === 0)) { if (
messages[id].byteStateChangeCounts = byteStateChangeCountsByMessage[id] byteStateChangeCountsByMessage[id] &&
} messages[id].byteStateChangeCounts.every(c => c === 0)
) {
messages[id].byteStateChangeCounts = byteStateChangeCountsByMessage[id];
}
const {msgEntry, const { msgEntry, byteStateChangeCounts } = parseMessage(
byteStateChangeCounts} = parseMessage(dbc, dbc,
busTime, busTime,
address, address,
data, data,
canStartTime, canStartTime,
prevMsgEntry); prevMsgEntry
);
messages[id].byteStateChangeCounts = byteStateChangeCounts.map((count, idx) => messages[id].byteStateChangeCounts = byteStateChangeCounts.map(
messages[id].byteStateChangeCounts[idx] + count (count, idx) => messages[id].byteStateChangeCounts[idx] + count
); );
messages[id].entries.push(msgEntry); messages[id].entries.push(msgEntry);
return msgEntry; return msgEntry;
} }
function createMessageSpec(dbc, address, id, bus) { function createMessageSpec(dbc, address, id, bus) {
const frame = dbc.messages.get(address); const frame = dbc.messages.get(address);
const size = frame ? frame.size : 8; const size = frame ? frame.size : 8;
return { return {
address: address, address: address,
id: id, id: id,
bus: bus, bus: bus,
entries: [], entries: [],
frame: frame, frame: frame,
byteColors: Array(size).fill(0), byteColors: Array(size).fill(0),
byteStateChangeCounts: Array(size).fill(0) byteStateChangeCounts: Array(size).fill(0)
}; };
} }
function determineByteStateChangeTimes(hexData, time, msgSize, lastParsedMessage) { function determineByteStateChangeTimes(
const byteStateChangeCounts = Array(msgSize).fill(0); hexData,
let byteStateChangeTimes; time,
msgSize,
lastParsedMessage
) {
const byteStateChangeCounts = Array(msgSize).fill(0);
let byteStateChangeTimes;
if(!lastParsedMessage) { if (!lastParsedMessage) {
byteStateChangeTimes = Array(msgSize).fill(time); byteStateChangeTimes = Array(msgSize).fill(time);
} else { } else {
byteStateChangeTimes = Array.from(lastParsedMessage.byteStateChangeTimes); byteStateChangeTimes = Array.from(lastParsedMessage.byteStateChangeTimes);
for(let i = 0; i < byteStateChangeTimes.length; i++) { for (let i = 0; i < byteStateChangeTimes.length; i++) {
const currentData = hexData.substr(i * 2, 2), const currentData = hexData.substr(i * 2, 2),
prevData = lastParsedMessage.hexData.substr(i * 2, 2); prevData = lastParsedMessage.hexData.substr(i * 2, 2);
if(currentData !== prevData) { if (currentData !== prevData) {
byteStateChangeTimes[i] = time; byteStateChangeTimes[i] = time;
byteStateChangeCounts[i] = 1; byteStateChangeCounts[i] = 1;
}
} }
} }
}
return {byteStateChangeTimes, byteStateChangeCounts}; return { byteStateChangeTimes, byteStateChangeCounts };
} }
function createMessageEntry(dbc, address, time, relTime, data, byteStateChangeTimes) { function createMessageEntry(
return { dbc,
signals: dbc.getSignalValues(address, data), address,
time, time,
relTime, relTime,
hexData: Buffer.from(data).toString('hex'), data,
byteStateChangeTimes, byteStateChangeTimes
updated: Date.now(), ) {
}; return {
signals: dbc.getSignalValues(address, data),
time,
relTime,
hexData: Buffer.from(data).toString("hex"),
byteStateChangeTimes,
updated: Date.now()
};
} }
function parseMessage(dbc, time, address, data, timeStart, lastParsedMessage) { function parseMessage(dbc, time, address, data, timeStart, lastParsedMessage) {
let hexData; let hexData;
if(typeof data === 'string') { if (typeof data === "string") {
hexData = data; hexData = data;
data = Buffer.from(data, 'hex'); data = Buffer.from(data, "hex");
} else { } else {
hexData = Buffer.from(data).toString('hex'); hexData = Buffer.from(data).toString("hex");
} }
const msgSpec = dbc.messages.get(address); const msgSpec = dbc.messages.get(address);
const msgSize = msgSpec ? msgSpec.size : 8; const msgSize = msgSpec ? msgSpec.size : 8;
const relTime = time - timeStart; const relTime = time - timeStart;
const {byteStateChangeTimes, const {
byteStateChangeCounts} = determineByteStateChangeTimes(hexData, byteStateChangeTimes,
relTime, byteStateChangeCounts
msgSize, } = determineByteStateChangeTimes(
lastParsedMessage); hexData,
const msgEntry = createMessageEntry(dbc, address, time, relTime, data, byteStateChangeTimes); relTime,
msgSize,
lastParsedMessage
);
const msgEntry = createMessageEntry(
dbc,
address,
time,
relTime,
data,
byteStateChangeTimes
);
return {msgEntry, byteStateChangeCounts}; return { msgEntry, byteStateChangeCounts };
} }
const BIG_ENDIAN_START_BITS = []; const BIG_ENDIAN_START_BITS = [];
for(let i = 0; i < 64; i += 8) { for (let i = 0; i < 64; i += 8) {
for(let j = 7; j > -1; j--) { for (let j = 7; j > -1; j--) {
BIG_ENDIAN_START_BITS.push(i + j); BIG_ENDIAN_START_BITS.push(i + j);
} }
} }
function bigEndianBitIndex(matrixBitIndex) { function bigEndianBitIndex(matrixBitIndex) {
return BIG_ENDIAN_START_BITS.indexOf(matrixBitIndex); return BIG_ENDIAN_START_BITS.indexOf(matrixBitIndex);
} }
function matrixBitNumber(bigEndianIndex) { function matrixBitNumber(bigEndianIndex) {
return BIG_ENDIAN_START_BITS[bigEndianIndex]; return BIG_ENDIAN_START_BITS[bigEndianIndex];
} }
function setMessageByteColors(message, maxByteStateChangeCount) { function setMessageByteColors(message, maxByteStateChangeCount) {
message.byteColors = message.byteStateChangeCounts.map((count) => message.byteColors = message.byteStateChangeCounts
isNaN(count) ? 0 : Math.min(255, 75 + 180 * (count / maxByteStateChangeCount)) .map(
).map((red) => count =>
'rgb(' + Math.round(red) + ',0,0)' isNaN(count)
); ? 0
: Math.min(255, 75 + 180 * (count / maxByteStateChangeCount))
)
.map(red => "rgb(" + Math.round(red) + ",0,0)");
return message; return message;
} }
export default {bigEndianBitIndex, export default {
addCanMessage, bigEndianBitIndex,
createMessageSpec, addCanMessage,
matrixBitNumber, createMessageSpec,
parseMessage, matrixBitNumber,
findMaxByteStateChangeCount, parseMessage,
setMessageByteColors, findMaxByteStateChangeCount,
createMessageEntry}; setMessageByteColors,
createMessageEntry
};

View File

@ -1,14 +1,15 @@
export default function debounce(func, wait, immediate) { export default function debounce(func, wait, immediate) {
var timeout; var timeout;
return function() { return function() {
var context = this, args = arguments; var context = this,
var later = function() { args = arguments;
timeout = null; var later = function() {
if (!immediate) func.apply(context, args); timeout = null;
}; if (!immediate) func.apply(context, args);
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
}; };
}; var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}

View File

@ -1,58 +1,67 @@
const binTimeIntervals = [ const binTimeIntervals = [
{seconds: 1, {
title: 'second'}, seconds: 1,
{seconds: 60, title: "second"
title: 'minute'}, },
{seconds: 300, {
title: '5 minutes'}, seconds: 60,
{seconds: 3600, title: "minute"
title: 'hour'} },
{
seconds: 300,
title: "5 minutes"
},
{
seconds: 3600,
title: "hour"
}
]; ];
function prettyBinDuration(samplesDurationSeconds, maxBinCount = 100) { function prettyBinDuration(samplesDurationSeconds, maxBinCount = 100) {
for(let i = 0; i < binTimeIntervals.length; i++) { for (let i = 0; i < binTimeIntervals.length; i++) {
const interval = binTimeIntervals[i]; const interval = binTimeIntervals[i];
if(samplesDurationSeconds / interval.seconds <= maxBinCount) { if (samplesDurationSeconds / interval.seconds <= maxBinCount) {
return interval; return interval;
}
} }
}
// maxBinCount is exceeded. // maxBinCount is exceeded.
// This means sampleDurationSeconds > 100 hours with default args. Quite a long trip. // This means sampleDurationSeconds > 100 hours with default args. Quite a long trip.
// Just going to use hours. // Just going to use hours.
return binTimeIntervals[3]; return binTimeIntervals[3];
} }
export function binMessages(messageEntries, segmentIndices) { export function binMessages(messageEntries, segmentIndices) {
let startIdx = 0, endIdx = messageEntries.length - 1; let startIdx = 0,
if(segmentIndices && segmentIndices.length === 2) { endIdx = messageEntries.length - 1;
[startIdx, endIdx] = segmentIndices; if (segmentIndices && segmentIndices.length === 2) {
} [startIdx, endIdx] = segmentIndices;
const first = messageEntries[startIdx], last = messageEntries[endIdx]; }
const binDuration = prettyBinDuration(last.time - first.time); const first = messageEntries[startIdx],
last = messageEntries[endIdx];
const binDuration = prettyBinDuration(last.time - first.time);
const bins = []; const bins = [];
const offset = first.time - first.relTime; const offset = first.time - first.relTime;
for(let t = first.time; t < last.time; t += binDuration.seconds) { for (let t = first.time; t < last.time; t += binDuration.seconds) {
const binCutoffTime = t + binDuration.seconds; const binCutoffTime = t + binDuration.seconds;
const previousBinEnd = startIdx; const previousBinEnd = startIdx;
let entry = {time: -1} let entry = { time: -1 };
while(entry !== undefined && entry.time < binCutoffTime) { while (entry !== undefined && entry.time < binCutoffTime) {
entry = messageEntries[startIdx]; entry = messageEntries[startIdx];
++startIdx; ++startIdx;
}
bins.push({
count: startIdx - previousBinEnd,
startTime: t,
endTime: binCutoffTime,
relStartTime: t - offset
});
} }
return {bins: bins, binDuration} bins.push({
count: startIdx - previousBinEnd,
startTime: t,
endTime: binCutoffTime,
relStartTime: t - offset
});
}
return { bins: bins, binDuration };
} }

View File

@ -2,130 +2,133 @@
// See the specification: http://docs.scipy.org/doc/numpy-dev/neps/npy-format.html // See the specification: http://docs.scipy.org/doc/numpy-dev/neps/npy-format.html
const NumpyLoader = (function NumpyLoader() { const NumpyLoader = (function NumpyLoader() {
function asciiDecode(buf) { function asciiDecode(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf)); return String.fromCharCode.apply(null, new Uint8Array(buf));
}
function readUint16LE(buffer) {
var view = new DataView(buffer);
var val = view.getUint8(0);
val |= view.getUint8(1) << 8;
return val;
}
function fromArrayBuffer(buf) {
// Check the magic number
var magic = asciiDecode(buf.slice(0, 6));
if (magic !== "\x93NUMPY") {
throw new Error("Bad magic number");
} }
function readUint16LE(buffer) { var version = new Uint8Array(buf.slice(6, 8)),
var view = new DataView(buffer); headerLength = readUint16LE(buf.slice(8, 10)),
var val = view.getUint8(0); headerStr = asciiDecode(buf.slice(10, 10 + headerLength));
val |= view.getUint8(1) << 8; const offsetBytes = 10 + headerLength;
return val; //rest = buf.slice(10+headerLength); XXX -- This makes a copy!!! https://www.khronos.org/registry/typedarray/specs/latest/#5
}
function fromArrayBuffer(buf) { // Hacky conversion of dict literal string to JS Object
// Check the magic number const info = JSON.parse(
var magic = asciiDecode(buf.slice(0,6)); headerStr
if (magic !== "\x93NUMPY") { .toLowerCase()
throw new Error("Bad magic number"); .replace("(", "[")
} .replace("),", "]")
.replace(/'/g, '"')
.replace(",]", "]")
);
var version = new Uint8Array(buf.slice(6,8)), // Intepret the bytes according to the specified dtype
headerLength = readUint16LE(buf.slice(8,10)), var data;
headerStr = asciiDecode(buf.slice(10, 10+headerLength)); if (info.descr === "|u1") {
const offsetBytes = 10 + headerLength; data = new Uint8Array(buf, offsetBytes);
//rest = buf.slice(10+headerLength); XXX -- This makes a copy!!! https://www.khronos.org/registry/typedarray/specs/latest/#5 } else if (info.descr === "|i1") {
data = new Int8Array(buf, offsetBytes);
// Hacky conversion of dict literal string to JS Object } else if (info.descr === "<u2") {
const info = JSON.parse(headerStr.toLowerCase() data = new Uint16Array(buf, offsetBytes);
.replace('(','[') } else if (info.descr === "<i2") {
.replace('),',']') data = new Int16Array(buf, offsetBytes);
.replace(/'/g, '"') } else if (info.descr === "<u4") {
.replace(',]', ']')); data = new Uint32Array(buf, offsetBytes);
} else if (info.descr === "<i4") {
// Intepret the bytes according to the specified dtype data = new Int32Array(buf, offsetBytes);
var data; } else if (info.descr === "<f4") {
if (info.descr === "|u1") { data = new Float32Array(buf, offsetBytes);
data = new Uint8Array(buf, offsetBytes); } else if (info.descr === "<f8") {
} else if (info.descr === "|i1") { data = new Float64Array(buf, offsetBytes);
data = new Int8Array(buf, offsetBytes); } else if (info.descr === "<u8") {
} else if (info.descr === "<u2") { // 8 byte uint64s
data = new Uint16Array(buf, offsetBytes); data = new Uint8Array(buf, offsetBytes);
} else if (info.descr === "<i2") { } else if (info.descr === "<i8") {
data = new Int16Array(buf, offsetBytes); // 8 byte int64s
} else if (info.descr === "<u4") { data = new Uint8Array(buf, offsetBytes);
data = new Uint32Array(buf, offsetBytes); } else if (info.descr === "|s5") {
} else if (info.descr === "<i4") { // 5 byte string
data = new Int32Array(buf, offsetBytes); data = new Uint8Array(buf, offsetBytes);
} else if (info.descr === "<f4") { } else if (info.descr === "|s8") {
data = new Float32Array(buf, offsetBytes); // 8 byte strings
} else if (info.descr === "<f8") { data = new Uint8Array(buf, offsetBytes);
data = new Float64Array(buf, offsetBytes); } else if (info.descr === "|s15") {
} else if (info.descr === "<u8") { // 15 byte strings?
// 8 byte uint64s data = new Uint8Array(buf, offsetBytes);
data = new Uint8Array(buf, offsetBytes); } else {
} else if (info.descr === "<i8") { throw new Error("unknown numeric dtype " + info.descr);
// 8 byte int64s
data = new Uint8Array(buf, offsetBytes);
} else if (info.descr === "|s5") {
// 5 byte string
data = new Uint8Array(buf, offsetBytes);
} else if (info.descr === "|s8") {
// 8 byte strings
data = new Uint8Array(buf, offsetBytes);
} else if (info.descr === '|s15') {
// 15 byte strings?
data = new Uint8Array(buf, offsetBytes);
} else {
throw new Error('unknown numeric dtype '+info.descr)
}
return {
shape: info.shape,
fortran_order: info.fortran_order,
data: data
};
}
function open(file, callback) {
var reader = new FileReader();
reader.onload = function() {
// the file contents have been read as an array buffer
var buf = reader.result;
var ndarray = fromArrayBuffer(buf);
callback(ndarray);
};
reader.readAsArrayBuffer(file);
}
function promise(url) {
return new Promise(function(resolve, reject) {
// Do the usual XHR stuff
var req = new XMLHttpRequest();
req.onload = function() {
// This is called even on 404 etc
// so check the status
if (req.status == 200) {
var buf = req.response; // not responseText
var ndarray = fromArrayBuffer(buf);
resolve(ndarray);
} else if (req.status == 404) {
console.log('yup')
reject({is404: true})
} else {
// Otherwise reject with the status text
// which will hopefully be a meaningful error
reject(Error(req.statusText));
}
};
// Handle network errors
req.onerror = function() {
reject(Error("Network Error"));
};
// Make the request
req.open('GET', url, true);
req.responseType = "arraybuffer";
req.send(null);
});
} }
return { return {
open: open, shape: info.shape,
promise: promise, fortran_order: info.fortran_order,
fromArrayBuffer: fromArrayBuffer, data: data
}; };
}
function open(file, callback) {
var reader = new FileReader();
reader.onload = function() {
// the file contents have been read as an array buffer
var buf = reader.result;
var ndarray = fromArrayBuffer(buf);
callback(ndarray);
};
reader.readAsArrayBuffer(file);
}
function promise(url) {
return new Promise(function(resolve, reject) {
// Do the usual XHR stuff
var req = new XMLHttpRequest();
req.onload = function() {
// This is called even on 404 etc
// so check the status
if (req.status == 200) {
var buf = req.response; // not responseText
var ndarray = fromArrayBuffer(buf);
resolve(ndarray);
} else if (req.status == 404) {
console.log("yup");
reject({ is404: true });
} else {
// Otherwise reject with the status text
// which will hopefully be a meaningful error
reject(Error(req.statusText));
}
};
// Handle network errors
req.onerror = function() {
reject(Error("Network Error"));
};
// Make the request
req.open("GET", url, true);
req.responseType = "arraybuffer";
req.send(null);
});
}
return {
open: open,
promise: promise,
fromArrayBuffer: fromArrayBuffer
};
})(); })();
module.exports = NumpyLoader; module.exports = NumpyLoader;

View File

@ -1,18 +1,18 @@
export function swapKeysAndValues(obj, f) { export function swapKeysAndValues(obj, f) {
return Object.keys(obj).reduce(function(acc, k) { return Object.keys(obj).reduce(function(acc, k) {
acc[obj[k]] = k; acc[obj[k]] = k;
return acc return acc;
},{}) }, {});
}; }
export function fromArray(arr) { export function fromArray(arr) {
// arr is an array of array key-value pairs // arr is an array of array key-value pairs
// like [['a', 1], ['b', 2]] // like [['a', 1], ['b', 2]]
const pairs = arr.map( ([k,v]) => ({[k]: v})); const pairs = arr.map(([k, v]) => ({ [k]: v }));
if(pairs.length > 0) { if (pairs.length > 0) {
return Object.assign(...pairs); return Object.assign(...pairs);
} else { } else {
return {}; return {};
} }
} }

View File

@ -1,9 +1,8 @@
export function hash(str) { export function hash(str) {
var hash = 5381, var hash = 5381,
i = str.length; i = str.length;
while(i) { while (i) {
hash = (hash * 33) ^ str.charCodeAt(--i); hash = (hash * 33) ^ str.charCodeAt(--i);
} }

View File

@ -1,173 +1,184 @@
import {createClassFromSpec} from 'react-vega'; import { createClassFromSpec } from "react-vega";
const canHistogramSpec = { const canHistogramSpec = {
"$schema": "https://vega.github.io/schema/vega/v3.0.json", $schema: "https://vega.github.io/schema/vega/v3.0.json",
"width": 1000, width: 1000,
"height": 100, height: 100,
"padding": 5, padding: 5,
"signals": [ signals: [
{ {
"name": "segment" name: "segment"
} }
], ],
"data": [ data: [
{ {
"name": "binned" name: "binned"
} }
], ],
"scales": [ scales: [
{ {
"name": "xscale", name: "xscale",
"type": "linear", type: "linear",
"zero": false, zero: false,
"range": "width", range: "width",
"domain": {"data": "binned", "field": "startTime"} domain: { data: "binned", field: "startTime" }
}, },
{ {
"name": "xrelscale", name: "xrelscale",
"type": "linear", type: "linear",
"zero": false, zero: false,
"range": "width", range: "width",
"domain": {"data": "binned", "field": "relStartTime"} domain: { data: "binned", field: "relStartTime" }
}, },
{ {
"name": "yscale", name: "yscale",
"type": "linear", type: "linear",
"range": "height", "round": true, range: "height",
"domain": {"data": "binned", "field": "count"}, round: true,
"zero": true, "nice": true domain: { data: "binned", field: "count" },
zero: true,
nice: true
} }
], ],
"axes": [ axes: [
{"orient": "bottom", "scale": "xrelscale", "zindex": 1, "tickCount": 10}, { orient: "bottom", scale: "xrelscale", zindex: 1, tickCount: 10 },
{"orient": "left", "scale": "yscale", "tickCount": 5, "zindex": 1} { orient: "left", scale: "yscale", tickCount: 5, zindex: 1 }
], ],
"marks": [ marks: [
{"type": "group", {
"name": "histogram", type: "group",
"interactive": true, name: "histogram",
"encode": { interactive: true,
"enter": { encode: {
"height": {"value": 75}, enter: {
"width": {"value": 1000}, height: { value: 75 },
"fill": {"value": "transparent"} width: { value: 1000 },
} fill: { value: "transparent" }
}, }
"signals": [ },
signals: [
{ {
"name": "brush", "value": 0, name: "brush",
"on": [ value: 0,
on: [
{ {
"events": "@bins:mousedown", events: "@bins:mousedown",
"update": "[x(), x()]" update: "[x(), x()]"
}, },
{ {
"events": "[@bins:mousedown, window:mouseup] > window:mousemove!", events: "[@bins:mousedown, window:mouseup] > window:mousemove!",
"update": "[brush[0], clamp(x(), 0, width)]" update: "[brush[0], clamp(x(), 0, width)]"
}, },
{ {
"events": {"signal": "delta"}, events: { signal: "delta" },
"update": "clampRange([anchor[0] + delta, anchor[1] + delta], 0, width)" update:
"clampRange([anchor[0] + delta, anchor[1] + delta], 0, width)"
} }
] ]
}, },
{ {
"name": "anchor", "value": null, name: "anchor",
"on": [{"events": "@brush:mousedown", "update": "slice(brush)"}] value: null,
on: [{ events: "@brush:mousedown", update: "slice(brush)" }]
}, },
{ {
"name": "xdown", "value": 0, name: "xdown",
"on": [{"events": "@brush:mousedown", "update": "x()"}] value: 0,
on: [{ events: "@brush:mousedown", update: "x()" }]
}, },
{ {
"name": "delta", "value": 0, name: "delta",
"on": [ value: 0,
on: [
{ {
"events": "[@brush:mousedown, window:mouseup] > window:mousemove!", events: "[@brush:mousedown, window:mouseup] > window:mousemove!",
"update": "x() - xdown" update: "x() - xdown"
} }
] ]
}, },
{ {
"name": "segment", name: "segment",
"push": "outer", push: "outer",
"on": [ on: [
{ {
"events": {"signal": "brush"}, events: { signal: "brush" },
"update": "span(brush) ? invert('xscale', brush) : null" update: "span(brush) ? invert('xscale', brush) : null"
} }
] ]
} }
], ],
"marks": [ marks: [
{ {
"type": "rect", type: "rect",
"interactive": true, interactive: true,
"from": {"data": "binned"}, from: { data: "binned" },
"name": "bins", name: "bins",
"encode": { encode: {
"update": { update: {
"x": {"scale": "xscale", "field": "startTime"}, x: { scale: "xscale", field: "startTime" },
"x2": {"scale": "xscale", "field": "endTime", x2: {
"offset": 0}, scale: "xscale",
"y": {"scale": "yscale", "field": "count"}, field: "endTime",
"y2": {"scale": "yscale", "value": 0}, offset: 0
"fill": {"value": "steelblue"} },
y: { scale: "yscale", field: "count" },
y2: { scale: "yscale", value: 0 },
fill: { value: "steelblue" }
}, },
"hover": { hover: {
"fill": {"value": "goldenrod"}, fill: { value: "goldenrod" },
"cursor": {"value": "ew-resize"} cursor: { value: "ew-resize" }
} }
} }
}, },
{ {
"type": "rect", type: "rect",
"interactive": true, interactive: true,
"name": "brush", name: "brush",
"encode": { encode: {
"enter": { enter: {
"y": {"value": 0}, y: { value: 0 },
"height": {"value": 100}, height: { value: 100 },
"fill": {"value": "#333"}, fill: { value: "#333" },
"fillOpacity": {"value": 0.2} fillOpacity: { value: 0.2 }
}, },
"update": { update: {
"x": {"signal": "brush[0]"}, x: { signal: "brush[0]" },
"x2": {"signal": "brush[1]"} x2: { signal: "brush[1]" }
} }
} }
}, },
{ {
"type": "rect", type: "rect",
"interactive": false, interactive: false,
"encode": { encode: {
"enter": { enter: {
"y": {"value": 0}, y: { value: 0 },
"height": {"value": 100}, height: { value: 100 },
"width": {"value": 2}, width: { value: 2 },
"fill": {"value": "firebrick"} fill: { value: "firebrick" }
}, },
"update": { update: {
"x": {"signal": "brush[0]"} x: { signal: "brush[0]" }
} }
} }
}, },
{ {
"type": "rect", type: "rect",
"interactive": false, interactive: false,
"encode": { encode: {
"enter": { enter: {
"y": {"value": 0}, y: { value: 0 },
"height": {"value": 100}, height: { value: 100 },
"width": {"value": 2}, width: { value: 2 },
"fill": {"value": "firebrick"} fill: { value: "firebrick" }
}, },
"update": { update: {
"x": {"signal": "brush[1]"} x: { signal: "brush[1]" }
} }
} }
} }

View File

@ -1,306 +1,334 @@
import {createClassFromSpec} from 'react-vega'; import { createClassFromSpec } from "react-vega";
export default createClassFromSpec('CanPlot', { export default createClassFromSpec("CanPlot", {
"$schema": "https://vega.github.io/schema/vega/v3.0.json", $schema: "https://vega.github.io/schema/vega/v3.0.json",
"width": 500, width: 500,
"height": 200, height: 200,
"padding": "auto", padding: "auto",
"autosize": {"type": "fit", "resize": true}, autosize: { type: "fit", resize: true },
"signals": [ signals: [
{ {
"name": "tipTime", name: "tipTime",
"on": [{ on: [
"events": "mousemove",
"update": "invert('xrelscale', x())"
}]
},
{"name": "clickTime",
"on": [{
"events": "mouseup",
"update": "invert('xrelscale', x())"
}]
},
{"name": "videoTime"},
{"name": "segment",
"value": {"data": "table", "field": "relTime"}
}
],
"data": [
{
"name": "table"
},
{
"name": "tooltip",
"source": "table",
"transform": [
{ {
"type": "filter", events: "mousemove",
"expr": "abs(datum.relTime - tipTime) <= 0.01" update: "invert('xrelscale', x())"
},
{
"type": "aggregate",
"fields": ["relTime", "y", "unit"],
"ops": ["min", "argmin", "argmin"],
"as": ["min", "argmin", "argmin"]
} }
] ]
}, },
{ {
"name": "ySegmentScale", name: "clickTime",
"source": "table", on: [
"transform": [
{ {
"type": "filter", events: "mouseup",
"expr": "length(segment) != 2 || (datum.relTime >= segment[0] && datum.relTime <= segment[1])" update: "invert('xrelscale', x())"
}
]
},
{ name: "videoTime" },
{
name: "segment",
value: { data: "table", field: "relTime" }
}
],
data: [
{
name: "table"
},
{
name: "tooltip",
source: "table",
transform: [
{
type: "filter",
expr: "abs(datum.relTime - tipTime) <= 0.01"
}, },
{"type": "extent", "field": "y", "signal": "ySegment"} {
type: "aggregate",
fields: ["relTime", "y", "unit"],
ops: ["min", "argmin", "argmin"],
as: ["min", "argmin", "argmin"]
}
]
},
{
name: "ySegmentScale",
source: "table",
transform: [
{
type: "filter",
expr:
"length(segment) != 2 || (datum.relTime >= segment[0] && datum.relTime <= segment[1])"
},
{ type: "extent", field: "y", signal: "ySegment" }
] ]
} }
], ],
"scales": [ scales: [
{ {
"name": "xscale", name: "xscale",
"type": "linear", type: "linear",
"range": "width", range: "width",
"domain": {"data": "table", "field": "x"}, domain: { data: "table", field: "x" },
"zero": false zero: false
}, },
{ {
"name": "xrelscale", name: "xrelscale",
"type": "linear", type: "linear",
"range": "width", range: "width",
"domain": {"data": "table", "field": "relTime"}, domain: { data: "table", field: "relTime" },
"zero": false, zero: false,
"clamp": true, clamp: true,
"domainRaw": {"signal": "segment"} domainRaw: { signal: "segment" }
}, },
{ {
"name": "yscale", name: "yscale",
"type": "linear", type: "linear",
"range": "height", range: "height",
"clamp": true, clamp: true,
"zero": false, zero: false,
"domain": {"signal": "ySegment"} domain: { signal: "ySegment" }
}, },
{ {
"name": "color", name: "color",
"type": "ordinal", type: "ordinal",
"domain": {"data": "table", "field": "color"}, domain: { data: "table", field: "color" },
"range": {"data": "table", "field": "color"} range: { data: "table", field: "color" }
} }
], ],
"axes": [ axes: [
{"orient": "bottom", "scale": "xrelscale", labelOverlap: true}, { orient: "bottom", scale: "xrelscale", labelOverlap: true },
{"orient": "left", "scale": "yscale"} { orient: "left", scale: "yscale" }
], ],
"marks": [ marks: [
{"type": "group", {
"name": "plot", type: "group",
"interactive": true, name: "plot",
"encode": { interactive: true,
"enter": { encode: {
"width": {"signal": "width"}, enter: {
"height": {"signal": "height"}, width: { signal: "width" },
"fill": {"value": "transparent"} height: { signal: "height" },
} fill: { value: "transparent" }
}, }
"signals": [ },
signals: [
{ {
"name": "brush", "value": 0, name: "brush",
"on": [ value: 0,
on: [
{ {
"events": "@boundingRect:mousedown", events: "@boundingRect:mousedown",
"update": "[x(), x()]" update: "[x(), x()]"
}, },
{ {
"events": "[@boundingRect:mousedown, window:mouseup] > window:mousemove!", events:
"update": "[brush[0], clamp(x(), 0, width)]" "[@boundingRect:mousedown, window:mouseup] > window:mousemove!",
update: "[brush[0], clamp(x(), 0, width)]"
}, },
{ {
"events": {"signal": "delta"}, events: { signal: "delta" },
"update": "clampRange([anchor[0] + delta, anchor[1] + delta], 0, width)" update:
"clampRange([anchor[0] + delta, anchor[1] + delta], 0, width)"
} }
] ]
}, },
{ {
"name": "anchor", "value": null, name: "anchor",
"on": [{"events": "@brush:mousedown", "update": "slice(brush)"}] value: null,
on: [{ events: "@brush:mousedown", update: "slice(brush)" }]
}, },
{ {
"name": "xdown", "value": 0, name: "xdown",
"on": [{"events": "@brush:mousedown", "update": "x()"}] value: 0,
on: [{ events: "@brush:mousedown", update: "x()" }]
}, },
{ {
"name": "delta", "value": 0, name: "delta",
"on": [ value: 0,
on: [
{ {
"events": "[@brush:mousedown, window:mouseup] > window:mousemove!", events: "[@brush:mousedown, window:mouseup] > window:mousemove!",
"update": "x() - xdown" update: "x() - xdown"
} }
] ]
}, },
{ {
"name": "segment", name: "segment",
"push": "outer", push: "outer",
"on": [ on: [
{ {
"events": "window:mouseup", events: "window:mouseup",
"update": "span(brush) && span(brush) > 15 ? invert('xrelscale', brush) : segment" update:
"span(brush) && span(brush) > 15 ? invert('xrelscale', brush) : segment"
} }
] ]
} }
], ],
"marks": [ marks: [
{ {
"type": "group", type: "group",
"from": { from: {
"facet": { facet: {
"name": "series", name: "series",
"data": "table", data: "table",
"groupby": "color" groupby: "color"
}
},
marks: {
type: "line",
name: "lineMark",
from: { data: "series" },
interactive: true,
encode: {
update: {
interpolate: { value: "step" },
x: { scale: "xrelscale", field: "relTime" },
y: { scale: "yscale", field: "y" }
},
hover: {
fillOpacity: { value: 0.5 }
},
enter: {
clip: { value: true },
stroke: { scale: "color", field: "color" },
strokeWidth: { value: 2 }
} }
}
}
},
{
type: "rect",
interactive: true,
name: "brush",
encode: {
enter: {
y: { value: 0 },
height: { signal: "height" },
fill: { value: "#333" },
fillOpacity: { value: 0.2 }
}, },
"marks": { update: {
"type": "line", x: { signal: "brush[0]" },
"name": "lineMark", x2: { signal: "brush[1]" }
"from": {"data": "series"}, }
"interactive": true, }
"encode": { },
"update": { // {
"interpolate": {"value": "step"}, // "type": "rect",
"x": {"scale": "xrelscale", "field": "relTime"}, // "interactive": false,
"y": {"scale": "yscale", "field": "y"} // "encode": {
// "enter": {
// "y": {"value": 0},
// "height": {"value": 200},
// "width": {"value": 2},
// "fill": {"value": "firebrick"}
// },
// "update": {
// "x": {"signal": "brush[0]"}
// }
// }
// },
// {
// "type": "rect",
// "interactive": false,
// "encode": {
// "enter": {
// "y": {"value": 0},
// "height": {"value": 200},
// "width": {"value": 2},
// "fill": {"value": "firebrick"}
// },
// "update": {
// "x": {"signal": "brush[1]"}
// }
// }
// },
{
type: "rule",
encode: {
update: {
y: { value: 0 },
y2: { field: { group: "height" } },
stroke: { value: "#000" },
strokeWidth: { value: 2 },
x: {
scale: "xrelscale",
signal: "videoTime",
offset: 0.5
}
}
}
},
{
type: "symbol",
from: { data: "tooltip" },
encode: {
update: {
x: { scale: "xrelscale", field: "argmin.relTime" },
y: { scale: "yscale", field: "argmin.y" },
size: { value: 50 },
fill: { value: "black" }
}
}
},
{
type: "group",
from: { data: "tooltip" },
interactive: false,
name: "tooltipGroup",
encode: {
update: {
x: [
{
test:
"inrange(datum.argmin.relTime + 80, domain('xrelscale'))",
scale: "xrelscale",
field: "argmin.relTime"
}, },
"hover": { { scale: "xrelscale", field: "argmin.relTime", offset: -80 }
"fillOpacity": {"value": 0.5} ],
}, y: { scale: "yscale", field: "argmin.y" },
"enter": { height: { value: 20 },
"clip": {"value": true}, width: { value: 80 },
"stroke": {"scale": "color", "field": "color"}, fill: { value: "#fff" },
"strokeWidth": {"value": 2} fillOpacity: { value: 0.85 },
stroke: { value: "#aaa" },
strokeWidth: { value: 0.5 }
}
},
marks: [
{
type: "text",
interactive: false,
encode: {
update: {
text: {
signal:
"format(parent.argmin.relTime, ',.2f') + ': ' + format(parent.argmin.y, ',.2f') + ' ' + parent.argmin.unit"
},
fill: { value: "black" },
fontWeight: { value: "bold" },
y: { value: 20 }
} }
} }
}, }
}, ]
{
"type": "rect",
"interactive": true,
"name": "brush",
"encode": {
"enter": {
"y": {"value": 0},
"height": {"signal": "height"},
"fill": {"value": "#333"},
"fillOpacity": {"value": 0.2}
},
"update": {
"x": {"signal": "brush[0]"},
"x2": {"signal": "brush[1]"}
}
}
},
// {
// "type": "rect",
// "interactive": false,
// "encode": {
// "enter": {
// "y": {"value": 0},
// "height": {"value": 200},
// "width": {"value": 2},
// "fill": {"value": "firebrick"}
// },
// "update": {
// "x": {"signal": "brush[0]"}
// }
// }
// },
// {
// "type": "rect",
// "interactive": false,
// "encode": {
// "enter": {
// "y": {"value": 0},
// "height": {"value": 200},
// "width": {"value": 2},
// "fill": {"value": "firebrick"}
// },
// "update": {
// "x": {"signal": "brush[1]"}
// }
// }
// },
{
"type": "rule",
"encode": {
"update": {
"y": {"value": 0},
"y2": {"field": {"group": "height"}},
"stroke": {"value": "#000"},
"strokeWidth": {"value": 2},
"x": {"scale": "xrelscale",
"signal": "videoTime", "offset": 0.5}
}
}
},
{
"type": "symbol",
"from": {"data": "tooltip"},
"encode": {
"update": {
"x": {"scale": "xrelscale", "field": "argmin.relTime"},
"y": {"scale": "yscale", "field": "argmin.y"},
"size": {"value": 50},
"fill": {"value": "black"}
}
}
},
{
"type": "group",
"from": {"data": "tooltip"},
"interactive": false,
"name": "tooltipGroup",
"encode": {
"update": {
"x": [{"test": "inrange(datum.argmin.relTime + 80, domain('xrelscale'))", "scale": "xrelscale", "field": "argmin.relTime"},
{"scale": "xrelscale", "field": "argmin.relTime", "offset": -80}],
"y": {"scale": "yscale", "field": "argmin.y"},
"height": {"value": 20},
"width": {"value": 80},
"fill": {"value": "#fff"},
"fillOpacity": {"value": 0.85},
"stroke": {"value": "#aaa"},
"strokeWidth": {"value": 0.5}
}
}, },
"marks": [ {
{ type: "rect",
"type": "text", name: "boundingRect",
"interactive": false, interactive: true,
"encode": { encode: {
"update": { enter: {
"text": {"signal": "format(parent.argmin.relTime, ',.2f') + ': ' + format(parent.argmin.y, ',.2f') + ' ' + parent.argmin.unit"}, width: { signal: "width" },
"fill": {"value": "black"}, height: { signal: "height" },
"fontWeight": {"value": "bold"}, fill: { value: "transparent" }
"y": {"value": 20}
}
} }
} }
]
},
{
"type": "rect",
"name": "boundingRect",
"interactive": true,
"encode": {
"enter": {
"width": {"signal": "width"},
"height": {"signal": "height"},
"fill": {"value": "transparent"}
}
} }
}, ]
]
} }
] ]
}); });

687
yarn.lock

File diff suppressed because it is too large Load Diff