diff --git a/README.md b/README.md index 499455d..ac4cff0 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,28 @@ README ====== +Requires NodeJS +----------------------------------- +Windows: +- node.js 4.3.2 +Download from [https://nodejs.org]. Node.js installs npm, the node package manager + +OS X: +- node.js +homebrew install node (got version 5.0.0) --- this installs node and +npm (node package manager) + +Linux: +- node.js +apt-get node + + +Installation +----------------------------------- To initialize the project and install node packages, run `npm install` -To build the project, run `npm run build`; you can also build individual parts only: +To build the project, run `npm run build`; -- Web worker: `npm run build:worker` -- TypeScript code: `npm run build:ts` -- Less CSS code: `npm run build:less` +To debug the project, run `npm run debug`; -To start the project, run `npm start` - -To create a package of the project, run `npm run package` \ No newline at end of file +To run the project, run `npm start` diff --git a/app/less/common.less b/app/less/common.less index 67655aa..46747d9 100644 --- a/app/less/common.less +++ b/app/less/common.less @@ -78,6 +78,15 @@ .tbtn-style-mixin(#d9534f, white); } +.disabled { + cursor: not-allowed; + opacity: .5; +} + +.flipped-icon { + transform: scaleX(-1); + -webkit-transform: scaleX(-1); +} // Button groups. .tbtn-group { diff --git a/app/less/labeling.less b/app/less/labeling.less index 830b881..23198f4 100644 --- a/app/less/labeling.less +++ b/app/less/labeling.less @@ -86,7 +86,7 @@ .labeling-detailed-view { position: absolute; top: 50px; left: 0; - .time-cursor { // FIXME: seems to only be used in referenceTrackOverview (why is this a class of labeling-detailed-view? + .time-cursor { line { stroke: black; fill: none; @@ -257,7 +257,6 @@ } } -// FIXME (remove when we remove slider) .confidence-slider { rect.histogram-bin { fill: #CCC; diff --git a/app/ts/components/App.tsx b/app/ts/components/App.tsx index bdba10e..c9fc0ec 100644 --- a/app/ts/components/App.tsx +++ b/app/ts/components/App.tsx @@ -1,20 +1,13 @@ -// The main view for the app. - import { TabID } from '../stores/dataStructures/types'; import * as stores from '../stores/stores'; import { NavigationColumn, NavigationColumnItem } from './common/NavigationColumn'; +import { SharedAlignmentLabelingPane } from './common/SharedAlignmentLabelingPane'; import { DeploymentPanel } from './deployment/DeploymentPanel'; -import { HomeMenu } from './menus/HomeMenu'; -import { TabPane } from './TabPane'; +import { HomeMenu } from './home/HomeMenu'; import { remote } from 'electron'; import { observer } from 'mobx-react'; -// tslint:disable-next-line:import-name -import DevTools from 'mobx-react-devtools'; import * as React from 'react'; - - -// Labeling app has some configuration code, then it calls LabelingView. @observer export class App extends React.Component<{}, {}> { constructor(props: {}, context: any) { @@ -64,11 +57,17 @@ export class App extends React.Component<{}, {}> { } } - // FIXME: TabPane just refers to alignment/labeling relocate or split like other tabs public render(): JSX.Element { + const debugging = remote.getGlobal('debugging'); return (
- + + {debugging ? + // The following weird construct is to ensure that the devtools + // module is only loaded when the debugging flag is true. + // tslint:disable-next-line:no-require-imports + (DevTools => )(require('mobx-react-devtools').default) : null} + { stores.projectUiStore.currentTab = tab as TabID; @@ -78,13 +77,13 @@ export class App extends React.Component<{}, {}> { - + - + - +
diff --git a/app/ts/components/alignment/AlignmentToolbar.tsx b/app/ts/components/alignment/AlignmentToolbar.tsx index d9b28e8..0a0c56f 100644 --- a/app/ts/components/alignment/AlignmentToolbar.tsx +++ b/app/ts/components/alignment/AlignmentToolbar.tsx @@ -1,6 +1,4 @@ -// Alignment toolbar view. -// - Toolbar buttons for alignment. -import { alignmentStore, projectStore } from '../../stores/stores'; +import { projectStore } from '../../stores/stores'; import { OptionsToolbar } from '../common/OptionsToolbar'; import { remote } from 'electron'; import { observer } from 'mobx-react'; @@ -29,7 +27,7 @@ export class AlignmentToolbar extends React.Component { properties: ['openFile'], filters: [ - { name: 'Videos', extensions: ['mov', 'mp4', 'webm'] }, + { name: 'Videos', extensions: ['mov', 'mp4', 'webm'] } ] }, (fileNames: string[]) => { @@ -76,9 +74,9 @@ export class AlignmentToolbar extends React.Component Load Reference Video... diff --git a/app/ts/components/alignment/AlignmentView.tsx b/app/ts/components/alignment/AlignmentView.tsx index 7d7e67b..fe81ba2 100644 --- a/app/ts/components/alignment/AlignmentView.tsx +++ b/app/ts/components/alignment/AlignmentView.tsx @@ -72,12 +72,6 @@ export class AlignmentView extends React.Component(); let trackYCurrent = 50; - const trackMinimizedHeight = 40; // FIXME: I don't think minimized is every used + const trackMinimizedHeight = 40; const referenceTrack = stores.projectStore.referenceTrack; if (referenceTrack) { diff --git a/app/ts/components/common/OptionsToolbar.tsx b/app/ts/components/common/OptionsToolbar.tsx index 2c4e447..04031da 100644 --- a/app/ts/components/common/OptionsToolbar.tsx +++ b/app/ts/components/common/OptionsToolbar.tsx @@ -1,4 +1,5 @@ import { SignalsViewMode } from '../../stores/dataStructures/labeling'; +import { KeyCode } from '../../stores/dataStructures/types'; import * as stores from '../../stores/stores'; import { projectStore, projectUiStore } from '../../stores/stores'; import { observer } from 'mobx-react'; @@ -7,22 +8,48 @@ import * as React from 'react'; @observer export class OptionsToolbar extends React.Component<{}, {}> { - private setViewModeThunk: { [viewMode: number]: () => void } = {}; - private changeFadeVideo(): void { - projectStore.fadeBackground(!projectStore.shouldFadeVideoBackground); - } - constructor(props: {}, context: any) { super(props, context); - + this.onKeyDown = this.onKeyDown.bind(this); Object.keys(SignalsViewMode).forEach(name => { const val = SignalsViewMode[name]; this.setViewModeThunk[val] = this.setViewMode.bind(this, val); }); } + private setViewModeThunk: { [viewMode: number]: () => void } = {}; + private changeFadeVideo(): void { + projectStore.fadeBackground(!projectStore.shouldFadeVideoBackground); + } + + private changeTimeSeriesColor(): void { + projectUiStore.timeSeriesGrayscale = !projectUiStore.timeSeriesGrayscale; + } + private onKeyDown(event: KeyboardEvent): void { + if (event.keyCode === KeyCode.LEFT) { + stores.projectUiStore.zoomReferenceTrackByPercentage(-0.6); + } + if (event.keyCode === KeyCode.RIGHT) { + stores.projectUiStore.zoomReferenceTrackByPercentage(+0.6); + } + if (event.ctrlKey && event.keyCode === 'Z'.charCodeAt(0)) { // Ctrl-Z + stores.projectStore.undo(); + } + if (event.ctrlKey && event.keyCode === 'Y'.charCodeAt(0)) { // Ctrl-Y + stores.projectStore.redo(); + } + } + + public componentDidMount(): void { + window.addEventListener('keydown', this.onKeyDown); + } + + public componentWillUnmount(): void { + window.removeEventListener('keydown', this.onKeyDown); + } + private setViewMode(viewMode: SignalsViewMode): void { - stores.labelingUiStore.setSignalsViewMode(viewMode); + stores.labelingUiStore.signalsViewMode = viewMode; } public render(): JSX.Element { @@ -31,17 +58,35 @@ export class OptionsToolbar extends React.Component<{}, {}> { }; return (
- + + {projectStore.statusMessage}
    -
  • Signals Display
  • +
  • Time Series Color
  • +
  • + + + Grayscale +
  • +
  • +
  • Signals Display Type
  • diff --git a/app/ts/components/common/ReferenceTrackOverview.tsx b/app/ts/components/common/ReferenceTrackOverview.tsx index 4103c44..a686404 100644 --- a/app/ts/components/common/ReferenceTrackOverview.tsx +++ b/app/ts/components/common/ReferenceTrackOverview.tsx @@ -1,8 +1,5 @@ -// The 'Overview' view that is shared by both alignment and labeling. - import { getLabelKey } from '../../stores/dataStructures/labeling'; import { PanZoomParameters } from '../../stores/dataStructures/PanZoomParameters'; -import { KeyCode } from '../../stores/dataStructures/types'; import * as stores from '../../stores/stores'; import { makePathDFromPoints, startDragging } from '../../stores/utils'; import { LabelType, LabelView } from '../labeling/LabelView'; @@ -20,7 +17,7 @@ export interface ReferenceTrackOverviewProps { downReach: number; } - +// The 'Overview' view that is shared by both alignment and labeling. @observer export class ReferenceTrackOverview extends React.Component { public refs: { @@ -30,18 +27,7 @@ export class ReferenceTrackOverview extends React.Component): void { @@ -58,14 +44,6 @@ export class ReferenceTrackOverview extends React.Component - {labels.map(label => - - )} - + {labels.map(label => + + )} + ); } @@ -222,10 +200,11 @@ export class ReferenceTrackOverview extends React.Component - - - - + {!isNaN(cursorX) ? + + + + : null} ; // Colors to use, if null, use d3.category10 or 20. + grayscale: boolean; + //colorScale?: d3.ScaleOrdinal; // Colors to use, if null, use d3.category10 or 20. pixelsPerSecond: number; // Scaling factor (assume the dataset's timeunit is seconds). plotHeight: number; // The height of the plot. yDomain?: number[]; // The y domain: [ min, max ], similar to D3's scale.domain([min, max]). @@ -28,7 +29,7 @@ export class SensorPlot extends React.Component { // We consider the timeSeries object and colorScale constant, so any change INSIDE these objects will not trigger an update. // To change the timeSeries, replace it with another object, don't update it directly. return nextProps.timeSeries !== this.props.timeSeries || - nextProps.colorScale !== this.props.colorScale || + nextProps.grayscale !== this.props.grayscale || nextProps.pixelsPerSecond !== this.props.pixelsPerSecond || nextProps.plotHeight !== this.props.plotHeight || nextProps.alternateDimensions !== this.props.alternateDimensions || @@ -61,8 +62,7 @@ export class SensorPlot extends React.Component { const yScale = 1 / (y0 - y1) * this.props.plotHeight; // Determine color scale. - const colors = this.props.colorScale || - (numSeries <= 10 ? + const colors = (numSeries <= 10 ? d3.scaleOrdinal(d3.schemeCategory10) : d3.scaleOrdinal(d3.schemeCategory20)); @@ -87,16 +87,19 @@ export class SensorPlot extends React.Component { } } const d: string = 'M' + positions.join('L'); - const style = { - fill: 'none', - stroke: colors(dimIndex) - }; - // const grayscale = .6 - dimIndex * ((.6 - .2) / dimensions.length); //grayscale range from .6-.2 (dark to light) - // const style = { - // fill: 'none', - // stroke: 'rgba(0, 0, 0, ' + grayscale + ')' - // }; + const grayscale = .6 - dimIndex * ((.6 - .2) / dimensions.length); //grayscale range from .6-.2 (dark to light) + let style = { + fill: 'none', + stroke: 'rgba(0, 0, 0, ' + grayscale + ')' + }; + if ( !this.props.grayscale) { + style = { + fill: 'none', + stroke: colors(dimIndex) + }; + } + return ( { export interface SensorRangePlotProps { timeSeries: SensorTimeSeries; // The timeseries object to show, replace it with a NEW object if you need to update its contents. dimensionVisibility?: boolean[]; // boolean array to show/hide individual dimensions. - colorScale?: any; // Colors to use, if null, use d3.category10 or 20. + grayscale: boolean; // Colors to use, if null, use d3.category10 or 20. pixelsPerSecond: number; // Scaling factor (assume the dataset's timeunit is seconds). plotWidth: number; // The width of the plot. plotHeight: number; // The height of the plot. @@ -135,7 +138,7 @@ export class SensorRangePlot extends React.Component { public shouldComponentUpdate(nextProps: SensorRangePlotProps): boolean { return nextProps.timeSeries !== this.props.timeSeries || - nextProps.colorScale !== this.props.colorScale || + nextProps.grayscale !== this.props.grayscale || nextProps.pixelsPerSecond !== this.props.pixelsPerSecond || nextProps.plotHeight !== this.props.plotHeight || !isSameArray(nextProps.yDomain, this.props.yDomain) || @@ -225,7 +228,7 @@ export class SensorRangePlot extends React.Component { { +export class SharedAlignmentLabelingPane extends React.Component { public refs: { [key: string]: Element, container: Element, @@ -106,7 +104,6 @@ export class TabPane extends React.Component diff --git a/app/ts/components/common/TrackView.tsx b/app/ts/components/common/TrackView.tsx index 5c9a87a..144d20a 100644 --- a/app/ts/components/common/TrackView.tsx +++ b/app/ts/components/common/TrackView.tsx @@ -20,7 +20,6 @@ export interface TrackViewProps { viewHeight: number; zoomTransform: PanZoomParameters; signalsViewMode?: SignalsViewMode; - colorScale?: any; useMipmap?: boolean; videoDetail?: boolean; @@ -70,7 +69,6 @@ export class TrackView extends React.Component { startX={startX} endX={endX} height={seriesHeight} startTime={startTime} endTime={endTime} - colorScale={this.props.colorScale} useMipmap={this.props.useMipmap} videoDetail={this.props.videoDetail} onMouseDown={this.props.onMouseDown} @@ -97,7 +95,6 @@ export interface TimeSeriesViewProps { height: number; startTime: number; endTime: number; - colorScale?: any; useMipmap?: boolean; videoDetail?: boolean; signalsViewMode?: SignalsViewMode; @@ -231,7 +228,7 @@ export class TimeSeriesView extends React.Component { rangeStart={this.props.startTime} pixelsPerSecond={this.pixelsPerSecond} plotWidth={this.width} plotHeight={this.props.height} useMipmap={this.props.useMipmap} - colorScale={this.props.colorScale} + grayscale={stores.projectUiStore.timeSeriesGrayscale} /> ); } @@ -281,7 +278,7 @@ export class TimeSeriesView extends React.Component { onWheel={this.onWheel} /> { - timeCursor != null ? ( + timeCursor != null && !isNaN(timeCursor) ? ( { diff --git a/app/ts/components/labeling/LabelView.tsx b/app/ts/components/labeling/LabelView.tsx index 49cc9b8..20d54f5 100644 --- a/app/ts/components/labeling/LabelView.tsx +++ b/app/ts/components/labeling/LabelView.tsx @@ -123,7 +123,6 @@ export class LabelView extends React.Component { this.props.label.state === LabelConfirmationState.CONFIRMED_BOTH; } - // FIXME: what does this do? private getSuggestionConfidenceOrOne(): number { const suggestionConfidence = this.props.label.suggestionConfidence; if (suggestionConfidence && this.props.label.state !== LabelConfirmationState.CONFIRMED_BOTH) { diff --git a/app/ts/components/labeling/LabelingView.tsx b/app/ts/components/labeling/LabelingView.tsx index a63098c..682861a 100644 --- a/app/ts/components/labeling/LabelingView.tsx +++ b/app/ts/components/labeling/LabelingView.tsx @@ -46,12 +46,6 @@ export class LabelingView extends React.Component= 0) { this.markers.splice(index, 1); @@ -50,7 +50,7 @@ export class AlignmentStore { } @action public addMarkerCorrespondence(marker1: Marker, marker2: Marker): MarkerCorrespondence { - projectStore.alignmentHistoryRecord(); + projectStore.recordAlignmentSnapshot(); const newCorr = new MarkerCorrespondence(marker1, marker2); // Remove all conflicting correspondence. this.correspondences = this.correspondences.filter(c => c.compatibleWith(newCorr)); @@ -60,7 +60,7 @@ export class AlignmentStore { } @action public deleteMarkerCorrespondence(correspondence: MarkerCorrespondence): void { - projectStore.alignmentHistoryRecord(); + projectStore.recordAlignmentSnapshot(); const index = this.correspondences.indexOf(correspondence); if (index >= 0) { this.correspondences.splice(index, 1); @@ -113,7 +113,6 @@ export class AlignmentStore { return blocks; } - public alignAllTracks(animate: boolean = false): void { if (this.correspondences.length === 0) { return; } projectStore.tracks.forEach(track => { @@ -172,7 +171,7 @@ export class AlignmentStore { track.referenceStart = tsState.referenceStart; track.referenceEnd = tsState.referenceEnd; }); - this.alignAllTracks(false); + //this.alignAllTracks(false); } public reset(): void { diff --git a/app/ts/stores/LabelingStore.ts b/app/ts/stores/LabelingStore.ts index 6afe431..f43f6cf 100644 --- a/app/ts/stores/LabelingStore.ts +++ b/app/ts/stores/LabelingStore.ts @@ -53,10 +53,6 @@ export class LabelingStore { return this._labelsIndex.items; } - @computed public get suggestions(): Label[] { - return this._suggestedLabelsIndex.items; - } - public getLabelsInRange(timeRange: TimeRange): Label[] { return mergeTimeRangeArrays( this._labelsIndex.getRangesInRange(timeRange), @@ -65,12 +61,12 @@ export class LabelingStore { @action public addLabel(label: Label): void { + projectStore.recordLabelingSnapshot(); this._labelsIndex.add(label); - projectStore.labelingHistoryRecord(); } @action public removeLabel(label: Label): void { - projectStore.labelingHistoryRecord(); + projectStore.recordLabelingSnapshot(); if (this._labelsIndex.has(label)) { this._labelsIndex.remove(label); } @@ -80,7 +76,7 @@ export class LabelingStore { } @action public updateLabel(label: Label, newLabel: PartialLabel): void { - projectStore.labelingHistoryRecord(); + projectStore.recordLabelingSnapshot(); // Update the label info. if (newLabel.timestampStart !== undefined) { label.timestampStart = newLabel.timestampStart; } if (newLabel.timestampEnd !== undefined) { label.timestampEnd = newLabel.timestampEnd; } @@ -169,13 +165,13 @@ export class LabelingStore { } @action public removeAllLabels(): void { - projectStore.labelingHistoryRecord(); + projectStore.recordLabelingSnapshot(); this._labelsIndex.clear(); this._suggestedLabelsIndex.clear(); } @action public addClass(className: string): void { - projectStore.labelingHistoryRecord(); + projectStore.recordLabelingSnapshot(); if (this.classes.indexOf(className) < 0) { this.classes.push(className); this.updateColors(); @@ -183,7 +179,7 @@ export class LabelingStore { } @action public removeClass(className: string): void { - projectStore.labelingHistoryRecord(); + projectStore.recordLabelingSnapshot(); // Remove the labels of that class. const toRemove = []; this._labelsIndex.forEach(label => { @@ -204,7 +200,7 @@ export class LabelingStore { } @action public renameClass(oldClassName: string, newClassName: string): void { - projectStore.labelingHistoryRecord(); + projectStore.recordLabelingSnapshot(); if (this.classes.indexOf(newClassName) < 0) { let renamed = false; this._labelsIndex.forEach(label => { @@ -230,7 +226,7 @@ export class LabelingStore { } @action public confirmVisibleSuggestions(): void { - projectStore.labelingHistoryRecord(); + projectStore.recordLabelingSnapshot(); // Get visible suggestions. let visibleSuggestions = this._suggestedLabelsIndex.getRangesInRange( projectUiStore.referenceTrackTimeRange); diff --git a/app/ts/stores/LabelingUiStore.ts b/app/ts/stores/LabelingUiStore.ts index d055878..954bb27 100644 --- a/app/ts/stores/LabelingUiStore.ts +++ b/app/ts/stores/LabelingUiStore.ts @@ -3,7 +3,7 @@ import { Label, SignalsViewMode, TimeRange } from './dataStructures/labeling'; import { ObservableSet } from './dataStructures/ObservableSet'; import { LabelingStore } from './LabelingStore'; import { labelingStore } from './stores'; -import { action, computed, observable } from 'mobx'; +import { action, observable } from 'mobx'; export class LabelingUiStore { @@ -90,10 +90,6 @@ export class LabelingUiStore { this.suggestionLogic = getLabelingSuggestionLogic(logic); } - @action public setSignalsViewMode(mode: SignalsViewMode): void { - this.signalsViewMode = mode; - } - public getLabelsInRange(timeRange: TimeRange): Label[] { const labels = labelingStore.getLabelsInRange(timeRange); return labels.filter(l => !this.selectedLabels.has(l)).concat( diff --git a/app/ts/stores/ProjectStore.ts b/app/ts/stores/ProjectStore.ts index 222ba6f..ada52d5 100644 --- a/app/ts/stores/ProjectStore.ts +++ b/app/ts/stores/ProjectStore.ts @@ -5,10 +5,10 @@ import { Track } from './dataStructures/alignment'; import { loadMultipleSensorTimeSeriesFromFile, loadRawSensorTimeSeriesFromFile, loadVideoTimeSeriesFromFile } from './dataStructures/dataset'; import { DeferredCallbacks } from './dataStructures/DeferredCallbacks'; -import { HistoryTracker } from './dataStructures/HistoryTracker'; import { PanZoomParameters } from './dataStructures/PanZoomParameters'; import { SavedAlignmentSnapshot, SavedLabelingSnapshot, SavedProject, SavedTrack } from './dataStructures/project'; +import { UndoRedoHistory } from './dataStructures/UndoRedoHistory'; import { convertToWebm, fadeBackground, isWebm } from './dataStructures/video'; import { alignmentStore, labelingStore, projectUiStore } from './stores'; import * as fs from 'fs'; @@ -49,16 +49,19 @@ export class ProjectStore { // Stores alignment and labeling history (undo is implemented separately, you can't undo alignment from labeling or vice versa). - private _alignmentHistory: HistoryTracker; - private _labelingHistory: HistoryTracker; + private _alignmentUndoRedoHistory: UndoRedoHistory; + private _labelingUndoRedoHistory: UndoRedoHistory; constructor() { - this._alignmentHistory = new HistoryTracker(); - this._labelingHistory = new HistoryTracker(); + this._alignmentUndoRedoHistory = new UndoRedoHistory(); + this._labelingUndoRedoHistory = new UndoRedoHistory(); this.referenceTrack = null; this.tracks = []; this.projectFileLocation = null; this.statusMessage = ''; + + this.undo = this.undo.bind(this); + this.redo = this.redo.bind(this); } @@ -84,15 +87,28 @@ export class ProjectStore { return this.referenceTimestampEnd - this.referenceTimestampStart; } + @computed public get canUndo(): boolean { + const tab = projectUiStore.currentTab; + const canUndoAlignment = this._alignmentUndoRedoHistory.canUndo; + const canUndoLabeling = this._labelingUndoRedoHistory.canUndo; + return tab === 'alignment' && canUndoAlignment || tab === 'labeling' && canUndoLabeling; + } + + @computed public get canRedo(): boolean { + const tab = projectUiStore.currentTab; + const canRedoAlignment = this._alignmentUndoRedoHistory.canRedo; + const canRedoLabeling = this._labelingUndoRedoHistory.canRedo; + return tab === 'alignment' && canRedoAlignment || tab === 'labeling' && canRedoLabeling; + } @action public loadReferenceTrack(path: string): void { - this.alignmentHistoryRecord(); + this.recordAlignmentSnapshot(); loadVideoTimeSeriesFromFile(path, video => { if (!isWebm(path)) { - const newPath = convertToWebm( + convertToWebm( path, video.videoDuration, pctDone => { - this.statusMessage = 'converting video: ' + (pctDone * 100).toFixed(0) + '%'; + this.statusMessage = 'converting video: ' + (pctDone * 100).toFixed(0) + '%'; }, webmVideo => { this.referenceTrack = Track.fromFile(webmVideo.filename, [webmVideo]); @@ -108,19 +124,19 @@ export class ProjectStore { } @action public loadVideoTrack(fileName: string): void { - this.alignmentHistoryRecord(); + this.recordAlignmentSnapshot(); loadVideoTimeSeriesFromFile(fileName, video => { this.tracks.push(Track.fromFile(fileName, [video])); }); } @action public loadSensorTrack(fileName: string): void { - this.alignmentHistoryRecord(); + this.recordAlignmentSnapshot(); const sensors = loadMultipleSensorTimeSeriesFromFile(fileName); this.tracks.push(Track.fromFile(fileName, sensors)); } - @action public fadeBackground(userChoice: boolean): void { + @action public fadeBackground(userChoice: boolean): void { this.shouldFadeVideoBackground = userChoice; if (this.shouldFadeVideoBackground) { this.originalReferenceTrackFilename = this.referenceTrack.source; @@ -142,7 +158,7 @@ export class ProjectStore { } @action public deleteTrack(track: Track): void { - this.alignmentHistoryRecord(); + this.recordAlignmentSnapshot(); const index = this.tracks.map(t => t.id).indexOf(track.id); this.tracks.splice(index, 1); } @@ -169,8 +185,8 @@ export class ProjectStore { const json = fs.readFileSync(fileName, 'utf-8'); const project = JSON.parse(json); this.projectFileLocation = null; - this.alignmentHistoryReset(); - this.labelingHistoryReset(); + this.resetAlignmentUndoRedoHistory(); + this.resetLabelingUndoRedoHistory(); this.loadProjectHelper(project as SavedProject, () => { this.projectFileLocation = fileName; this.addToRecentProjects(fileName); @@ -354,6 +370,14 @@ export class ProjectStore { alignmentStore.loadState(snapshot.alignment); } + @action public recordAlignmentSnapshot(): void { + this._alignmentUndoRedoHistory.add(this.getAlignmentSnapshot()); + } + + @action private resetAlignmentUndoRedoHistory(): void { + this._alignmentUndoRedoHistory.reset(); + } + private getLabelingSnapshot(): SavedLabelingSnapshot { return { labeling: deepClone(labelingStore.saveState()) }; } @@ -362,47 +386,39 @@ export class ProjectStore { labelingStore.loadState(snapshot.labeling); } - public alignmentHistoryRecord(): void { - this._alignmentHistory.add(this.getAlignmentSnapshot()); + @action public recordLabelingSnapshot(): void { + this._labelingUndoRedoHistory.add(this.getLabelingSnapshot()); } - private alignmentHistoryReset(): void { - this._alignmentHistory.reset(); + @action private resetLabelingUndoRedoHistory(): void { + this._labelingUndoRedoHistory.reset(); } - public labelingHistoryRecord(): void { - this._labelingHistory.add(this.getLabelingSnapshot()); - } - - private labelingHistoryReset(): void { - this._labelingHistory.reset(); - } - - public alignmentUndo(): void { - const snapshot = this._alignmentHistory.undo(this.getAlignmentSnapshot()); - if (snapshot) { - this.loadAlignmentSnapshot(snapshot); + @action public undo(): void { + if (projectUiStore.currentTab === 'alignment') { + const snapshot = this._alignmentUndoRedoHistory.undo(this.getAlignmentSnapshot()); + if (snapshot) { + this.loadAlignmentSnapshot(snapshot); + } + } else { + const snapshot = this._labelingUndoRedoHistory.undo(this.getLabelingSnapshot()); + if (snapshot) { + this.loadLabelingSnapshot(snapshot); + } } } - public alignmentRedo(): void { - const snapshot = this._alignmentHistory.redo(this.getAlignmentSnapshot()); - if (snapshot) { - this.loadAlignmentSnapshot(snapshot); - } - } - - public labelingUndo(): void { - const snapshot = this._labelingHistory.undo(this.getLabelingSnapshot()); - if (snapshot) { - this.loadLabelingSnapshot(snapshot); - } - } - - public labelingRedo(): void { - const snapshot = this._labelingHistory.redo(this.getLabelingSnapshot()); - if (snapshot) { - this.loadLabelingSnapshot(snapshot); + @action public redo(): void { + if (projectUiStore.currentTab === 'alignment') { + const snapshot = this._alignmentUndoRedoHistory.redo(this.getAlignmentSnapshot()); + if (snapshot) { + this.loadAlignmentSnapshot(snapshot); + } + } else { + const snapshot = this._labelingUndoRedoHistory.redo(this.getLabelingSnapshot()); + if (snapshot) { + this.loadLabelingSnapshot(snapshot); + } } } diff --git a/app/ts/stores/ProjectUiStore.ts b/app/ts/stores/ProjectUiStore.ts index c333818..7a9e8de 100644 --- a/app/ts/stores/ProjectUiStore.ts +++ b/app/ts/stores/ProjectUiStore.ts @@ -15,6 +15,8 @@ export class ProjectUiStore { @observable public viewWidth: number; @observable public currentTab: TabID; + @observable public timeSeriesGrayscale: boolean; + // Current transition. private _referenceViewTransition: TransitionController; @@ -37,8 +39,8 @@ export class ProjectUiStore { this._panZoomParameterMap = observable.map(); this.selectedMarker = null; this.selectedCorrespondence = null; - this.getTimeCursor = this.getTimeCursor.bind(this); + this.timeSeriesGrayscale = false; autorun('ProjectUiStore.onTracksChanged', () => this.onTracksChanged()); reaction( diff --git a/app/ts/stores/dataStructures/ObservableSet.ts b/app/ts/stores/dataStructures/ObservableSet.ts index 6e0ce98..c346ac9 100644 --- a/app/ts/stores/dataStructures/ObservableSet.ts +++ b/app/ts/stores/dataStructures/ObservableSet.ts @@ -1,9 +1,9 @@ -import { action, computed, observable, ObservableMap } from 'mobx'; import * as Map from 'es6-map'; +import { action, computed, observable, ObservableMap } from 'mobx'; -// FIXME: this class requires the es6-map polyfill for Map. Consider changing the implementation to avoid this. +// This class requires the es6-map polyfill for Map. export class ObservableSet { - private map: ObservableMap; //Observable + private map: ObservableMap; private keyMap: Map; constructor(private getKey: (item: T) => string) { diff --git a/app/ts/stores/dataStructures/HistoryTracker.ts b/app/ts/stores/dataStructures/UndoRedoHistory.ts similarity index 81% rename from app/ts/stores/dataStructures/HistoryTracker.ts rename to app/ts/stores/dataStructures/UndoRedoHistory.ts index 77151d9..4be6eb4 100644 --- a/app/ts/stores/dataStructures/HistoryTracker.ts +++ b/app/ts/stores/dataStructures/UndoRedoHistory.ts @@ -22,21 +22,23 @@ // Snapshots need to be decoupled // - They shouldn't reference to the same object which can be updated by the app. // - Referencing to the same object is okay (and save space) if the object never changes. +import { action, computed, observable } from 'mobx'; + +export class UndoRedoHistory { + @observable private _undoHistory: TSnapshot[]; + @observable private _redoHistory: TSnapshot[]; -export class HistoryTracker { - private _undoHistory: Snapshot[]; - private _redoHistory: Snapshot[]; constructor() { this._undoHistory = []; this._redoHistory = []; } - public add(item: Snapshot): void { + @action public add(item: TSnapshot): void { this._undoHistory.push(item); this._redoHistory = []; } - public undo(current: Snapshot): Snapshot { + @action public undo(current: TSnapshot): TSnapshot { const lastIndex = this._undoHistory.length - 1; if (lastIndex >= 0) { const [lastItem] = this._undoHistory.splice(lastIndex, 1); @@ -47,7 +49,7 @@ export class HistoryTracker { } } - public redo(current: Snapshot): Snapshot { + @action public redo(current: TSnapshot): TSnapshot { const lastIndex = this._redoHistory.length - 1; if (lastIndex >= 0) { const [lastItem] = this._redoHistory.splice(lastIndex, 1); @@ -58,15 +60,15 @@ export class HistoryTracker { } } - public canUndo(): boolean { + @computed public get canUndo(): boolean { return this._undoHistory.length > 0; } - public canRndo(): boolean { + @computed public get canRedo(): boolean { return this._redoHistory.length > 0; } - public reset(): void { + @action public reset(): void { this._undoHistory = []; this._redoHistory = []; } diff --git a/app/ts/stores/dataStructures/alignment.ts b/app/ts/stores/dataStructures/alignment.ts index f172bed..3ee681c 100644 --- a/app/ts/stores/dataStructures/alignment.ts +++ b/app/ts/stores/dataStructures/alignment.ts @@ -130,6 +130,7 @@ export class Track { }); // Find the translation and scale for correspondences. + if (tCorrespondences.length === 0) { return; } // The correspondences don't involve this track. let [k, b] = leastSquares(tCorrespondences); if (isNaN(k) || isNaN(b)) { k = 1; b = 0; } // Is this the right thing to do? const project = x => k * x + b; @@ -140,10 +141,6 @@ export class Track { } - - - - // leastSquares([[yi, xi], ... ]) => [ k, b ] such that sum(k xi + b - yi)^2 is minimized. function leastSquares(correspondences: [number, number][]): [number, number] { if (correspondences.length === 0) { throw 'leastSquares empty array'; } diff --git a/app/ts/stores/utils.ts b/app/ts/stores/utils.ts index 4a0b68a..794165c 100644 --- a/app/ts/stores/utils.ts +++ b/app/ts/stores/utils.ts @@ -1,10 +1,3 @@ -import { autocorrelogram } from '../suggestion/algorithms/Autocorrelation'; -import { Dataset, SensorTimeSeries, TimeSeriesKind } from './dataStructures/dataset'; -import { Label, LabelConfirmationState } from './dataStructures/labeling'; -import * as d3 from 'd3'; - - - export function startDragging( move?: (e: MouseEvent) => void, up?: (e: MouseEvent) => void, @@ -24,8 +17,6 @@ export function startDragging( window.addEventListener('mouseup', handler_up, useCapture); } - - export function isSameArray(arr1?: T[], arr2?: T[]): boolean { return arr1 === arr2 || arr1 && arr2 && arr1.length === arr2.length && arr1.every((d, i) => d === arr2[i]); } @@ -35,31 +26,6 @@ export function makePathDFromPoints(points: number[][]): string { return 'M' + points.map(([x, y]) => x + ',' + y).join('L'); } - -export function updateLabelConfirmationState(label: Label, endpoint: string): LabelConfirmationState { - let newState = label.state; - if (endpoint === 'start') { - if (label.state === LabelConfirmationState.UNCONFIRMED) { - newState = LabelConfirmationState.CONFIRMED_START; - } else if (label.state === LabelConfirmationState.CONFIRMED_END) { - newState = LabelConfirmationState.CONFIRMED_BOTH; - } - } - if (endpoint === 'end') { - if (label.state === LabelConfirmationState.UNCONFIRMED) { - newState = LabelConfirmationState.CONFIRMED_END; - } else if (label.state === LabelConfirmationState.CONFIRMED_START) { - newState = LabelConfirmationState.CONFIRMED_BOTH; - } - } - if (endpoint === 'both') { - newState = LabelConfirmationState.CONFIRMED_BOTH; - } - return newState; -} - - - export class TransitionController { private _timer: number; private _onProgress: (t: number, finish?: boolean) => void; @@ -92,9 +58,6 @@ export class TransitionController { } } - - - export class ArrayThrottler { private _minInterval: number; private _callback: (items: ItemType[], stationary: StationaryType) => void; @@ -146,82 +109,3 @@ export class ArrayThrottler { } } -export interface DatasetMetadata { - name: string; - sensors: { - name: string; - path: string; - timestampStart?: number; - timestampEnd?: number; - alignmentFix?: [number, number]; - }[]; - videos: { - name: string; - path: string; - timestampStart?: number; - timestampEnd?: number; - alignmentFix?: [number, number]; - }[]; -} - - -const autocorrelogramCache = new WeakMap(); - -export function computeSensorTimeSeriesAutocorrelogram(timeSeries: SensorTimeSeries): SensorTimeSeries { - if (autocorrelogramCache.has(timeSeries)) { return autocorrelogramCache.get(timeSeries); } - - const sampleRate = (timeSeries.dimensions[0].length - 1) / (timeSeries.timestampEnd - timeSeries.timestampStart); - const windowSize = Math.ceil(sampleRate * 4); - const sliceSize = Math.ceil(windowSize / 4); - const dimension = new Float32Array(timeSeries.dimensions[0].length); - for (let i = 0; i < timeSeries.dimensions[0].length; i++) { - dimension[i] = 0; - for (let j = 0; j < timeSeries.dimensions.length; j++) { - if (timeSeries.dimensions[j][i] === timeSeries.dimensions[j][i]) { - dimension[i] += timeSeries.dimensions[j][i]; - } - } - } - const result = autocorrelogram(dimension, windowSize, sliceSize); - const sliceCount = result.length / windowSize; - const dimensions: Float32Array[] = []; - const samplesScale = d3.scaleLinear() // sample index <> sample timestamp - .domain([0, dimension.length - 1]) - .range([timeSeries.timestampStart, timeSeries.timestampEnd]); - const sliceScale = d3.scaleLinear() // slice index <> slice timestamp - .domain([0, sliceCount - 1]) - .range([samplesScale(0 + windowSize / 2), samplesScale(sliceSize * (sliceCount - 1) + windowSize / 2)]); - for (let i = 0; i < windowSize; i++) { - const t = dimensions[i] = new Float32Array(sliceCount); - for (let j = 0; j < sliceCount; j++) { - t[j] = result[j * windowSize + i]; - if (t[j] !== t[j]) { t[j] = 0; } - } - } - const r = { - name: timeSeries.name + '.autocorrelogram', - kind: timeSeries.kind, - timestampStart: sliceScale.range()[0], - timestampEnd: sliceScale.range()[1], - dimensions: dimensions, - sampleRate: (sliceScale.range()[1] - sliceScale.range()[0]) / (dimension.length - 1), - scales: [[-1, 1]] - }; - autocorrelogramCache.set(timeSeries, r); - return r; -} - -export function computeDatasetAutocorrelogram(dataset: Dataset): Dataset { - const datasetOut = new Dataset(); - datasetOut.name = dataset.name; - datasetOut.timestampStart = dataset.timestampStart; - datasetOut.timestampEnd = dataset.timestampEnd; - for (const series of dataset.timeSeries) { - if (series.kind === TimeSeriesKind.VIDEO) { - datasetOut.timeSeries.push(series); - } else { - datasetOut.timeSeries.push(computeSensorTimeSeriesAutocorrelogram(series as SensorTimeSeries)); - } - } - return datasetOut; -} diff --git a/docs/INSTALL.md b/docs/INSTALL.md deleted file mode 100644 index a351cfa..0000000 --- a/docs/INSTALL.md +++ /dev/null @@ -1,88 +0,0 @@ -Prerequisites for using Element with native modules -=================================================== - -- SWIG -- node.js -- node-gyp -- electron -- electron-rebuild -- d3 node.js module -- serial node.js module (modified for electron) -- the FeatureExtraction library - -How to install requirements on Windows --------------------------------------- - -- SWIG -Download the Windows zip file from swig.org's [download section](http://www.swig.org/download.html). Unzip this directory -and put it somewhere convenient (I used C:\swigwin-3.0.8). Make sure you set your PATH environment -variable to include the swigwin directory. - -- node.js 4.3.2 -Download from [https://nodejs.org]. Node.js installs npm, the node package manager - -- node-gyp -npm install -g node-gyp - -- electron -npm install electron-prebuilt -g - -- electron-rebuild -npm install --save-dev electron-rebuild (note to self: what is save-dev?) - -- d3 -npm install d3 - -- serial -??? - - -How to install requirements on OS X ------------------------------------ - -- SWIG -homebrew install swig - -- node.js -homebrew install node (got version 5.0.0) --- this installs node and -npm (node package manager) - -- node-gyp -npm install -g node-gyp - -- electron -npm install electron-prebuilt -g - -- electron-rebuild -npm install --save-dev electron-rebuild (what is save-dev?) - -- d3 -npm install d3 - -- serial -??? - - -How to install requirements on Linux ------------------------------------- - -- SWIG -apt-get swig - -- node.js -apt-get node - -- node-gyp -npm install -g node-gyp - -- electron -npm install electron-prebuilt -g - -- electron-rebuild -npm install --save-dev electron-rebuild (what is save-dev?) - -- d3 -npm install d3 - -- serial -??? diff --git a/docs/StyleGuide.md b/docs/StyleGuide.md deleted file mode 100644 index cff79ba..0000000 --- a/docs/StyleGuide.md +++ /dev/null @@ -1,125 +0,0 @@ -Intelligent Devices UX style guide -==================================================== - -This is the coding style guide for UX projects. - -Directory hierarchy --------------- - -TODO: after setting up intern project spaces. Include build scripts? - - -File structure --------------- -Use one file per logical component (one main class per file plus any helpers). - -Files should be organized in the following way: -* import statements -* props interface -* state interface -* class definition - * refs - * private fields - * public fields - * constructors - * getters/setters - * lifecycle methods (componentsWillMount, componentsDidMount...) - * event handlers - * other methods - * render - -Naming --------------- -All names should be as descriptive as possible. Prefer whole words to abbreviations, especially if those abbreviations could be ambiguous, e.g., prefer `CurrentMousePosition` to `CurrMousePos` - -Filenames: -Use camelCase, e.g., `dataStore.ts` -Component filenames should match the main component class, e.g., the `dataView.tsx` file should contain a `class DataView`. - -Classes, interfaces, and enums: -Use PascalCase, e.g., `class DataView`. -Don't use "I" as a prefix for interface names. - -Component props and state interfaces: -Use the same name as the corresponding component followed by "Props" or "State", e.g., `DataViewProps` and `DataViewState` for the `DataView` component. - -Methods: -Use camelCase, e.g., `onDragStart()`, `zoomLevel()` - -Member variables: -Use `_` (underscore) followed by camelCase, e.g., `_zoomLevel` - -Getter and setter methods: -Use the same name as the corresponding member variable, minus the underscore, e.g., `public get zoomLevel()` - -Enum values (TODO: double check this): -Use all caps with `_` (underscore) between words, e.g., `INPUT_DATA_CHANGED` - -Event emitters, addListeners and removeListeners: -Use `private emit`, `public addListener`, `public removeListener` - -Import statements: -Only import what you need. If you only need to import one class from a file, use `import {ClassName} from 'path'`. If you need to import more than one class, use `import * as className from 'path'`. Do not import using requires because these will not pick up the interface description from typings. - - -Types and modifiers --------------- -All methods and member variables should have access modifiers. Always opt for `private` where possible and `public` when necessary. - -Always specify types for member variables, e.g., prefer `private _dataFileName : string = "";` to `private _dataFileName = "";`. - -There is no need to specify types for local variables. - - `any` types should be avoided. - -`refs` should always start with `[key: string]: React.ReactInstance`. All additional refs should also be of type `React.ReactInstance`. For example: - - refs: { - [key: string]: React.ReactInstance, - dataView: React.ReactInstance, - } - -Always do type checks and set default values on state variables. e.g., TODO - - -Miscellaneous styling -------- - -Keep lambdas simple e.g., prefer `x => x + x` to `(x) => x + x`. - -Open curly braces always go on the same line as whatever necessitates them. - -`else` and `else if` goes on a separate line from the closing curly brace. - -Use fat arrow syntax `() => {` over `function() {`. The word function shouldn't appear in your code. - -Use `let` not `var` for local variables. - -Do not export types/functions unless you need to share them across components. - -For anything not documented here, consult this guide: https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines - - -Comments -------- - -Only add comments for anything that is not obvious. - -TODO statements can be used before checking in to master, e.g., for communicating to code reviewers. No TODOs should be checked in to master. - - -Interfacing with EMLL -------- - -TODO - - -HTML and CSS --------- - -Use bootstrap css as much as possible: http://getbootstrap.com/css/ - -Add custom styles to `styles.css` - -Note that Typescript html code uses `className` not `class` whereas .html files use `class` - diff --git a/index.html b/index.html index 569d3e1..06c8ec8 100644 --- a/index.html +++ b/index.html @@ -21,7 +21,7 @@ - Gesture Builder - Intelligent Device Project + Embedded Learning Toolkit