Skip to content

Commit

Permalink
[eeg_modelling] Support import events from JSON
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 245494127
  • Loading branch information
pdpino authored and copybara-github committed Apr 26, 2019
1 parent 4af3c87 commit 0c2e07c
Show file tree
Hide file tree
Showing 10 changed files with 384 additions and 20 deletions.
10 changes: 5 additions & 5 deletions eeg_modelling/eeg_viewer/static/js/annotations.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ class Annotations {
* @param {!Store.StoreData} store Store object with chunk data.
*/
handleChunkNavigation(store) {
const chunk_start = store.absStart + store.chunkStart;
const chunk_end = chunk_start + store.chunkDuration;
const chunkStart = store.absStart + store.chunkStart;
const chunkEnd = chunkStart + store.chunkDuration;
const annotationRows = document.querySelectorAll(
'table.annotation tbody tr');
annotationRows.forEach((row) => {
const annotation_time = Number(row.getAttribute('data-start'));
if (annotation_time < chunk_end && annotation_time >= chunk_start) {
const annotationTime = Number(row.getAttribute('data-start'));
if (annotationTime < chunkEnd && annotationTime >= chunkStart) {
row.classList.add('in-viewport');
} else {
row.classList.remove('in-viewport');
Expand All @@ -59,7 +59,7 @@ class Annotations {
*/
handleAnnotations(store) {
const annotationTable = document.querySelector('table.annotation tbody');
annotationTable.innerHTML = '';
annotationTable.textContent = '';
store.annotations.forEach((annotation) => {
if (annotation.startTime != null
&& annotation.labelText != null) {
Expand Down
1 change: 1 addition & 0 deletions eeg_modelling/eeg_viewer/static/js/dispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const ActionType = {
DELETE_WAVE_EVENT: 'deleteWaveEvent',
DOWNLOAD_DATA: 'downloadData',
ERROR: 'error',
IMPORT_STORE: 'importStore',
MENU_FILE_LOAD: 'menuFileLoad',
NAV_BAR_CHUNK_REQUEST: 'navBarChunkRequest',
NAVIGATE_TO_SPAN: 'navigateToSpan',
Expand Down
1 change: 1 addition & 0 deletions eeg_modelling/eeg_viewer/static/js/downloader.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,4 @@ class Downloader {
goog.addSingletonGetter(Downloader);

exports = Downloader;
exports.DownloadObject = DownloadObject;
13 changes: 13 additions & 0 deletions eeg_modelling/eeg_viewer/static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const NavChart = goog.require('eeg_modelling.eeg_viewer.NavChart');
const Predictions = goog.require('eeg_modelling.eeg_viewer.Predictions');
const Requests = goog.require('eeg_modelling.eeg_viewer.Requests');
const ToolBar = goog.require('eeg_modelling.eeg_viewer.ToolBar');
const Uploader = goog.require('eeg_modelling.eeg_viewer.Uploader');
const WaveEventForm = goog.require('eeg_modelling.eeg_viewer.WaveEventForm');
const WaveEvents = goog.require('eeg_modelling.eeg_viewer.WaveEvents');
const WindowLocation = goog.require('eeg_modelling.eeg_viewer.WindowLocation');
Expand Down Expand Up @@ -92,6 +93,18 @@ goog.exportSymbol('toolBar.nextSec', goog.bind(toolBar.nextSec, toolBar));
goog.exportSymbol('toolBar.prevChunk', goog.bind(toolBar.prevChunk, toolBar));
goog.exportSymbol('toolBar.prevSec', goog.bind(toolBar.prevSec, toolBar));

/** @const {!Uploader} */
const uploader = Uploader.getInstance();
goog.exportSymbol(
'uploader.closeMenu', goog.bind(uploader.closeMenu, uploader));
goog.exportSymbol(
'uploader.openMenu', goog.bind(uploader.openMenu, uploader));
goog.exportSymbol(
'uploader.handleFileChange',
goog.bind(uploader.handleFileChange, uploader));
goog.exportSymbol(
'uploader.upload', goog.bind(uploader.upload, uploader));

/** @const {!WindowLocation} */
const windowLocation = WindowLocation.getInstance();

Expand Down
2 changes: 1 addition & 1 deletion eeg_modelling/eeg_viewer/static/js/predictions.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ class Predictions {
// set with a verified value (i.e. a number that predicts well),
// and possibly there will be support to change it from the UI.
/** @private {number} threshold to classify as positive or negative */
this.threshold_ = 0.5;
this.threshold_ = 0;

/** @private {!Object} Currently active filters */
this.activeFilters_ = {
Expand Down
21 changes: 21 additions & 0 deletions eeg_modelling/eeg_viewer/static/js/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,8 @@ class Store {
this.handleDownloadData);
registerCallback(Dispatcher.ActionType.ERROR,
this.handleError);
registerCallback(Dispatcher.ActionType.IMPORT_STORE,
this.handleImportStore);
registerCallback(Dispatcher.ActionType.MENU_FILE_LOAD,
this.handleMenuFileLoad);
registerCallback(Dispatcher.ActionType.NAVIGATE_TO_SPAN,
Expand Down Expand Up @@ -693,6 +695,25 @@ class Store {
};
}

/**
* Handles data from a IMPORT_STORE action.
* @param {!Object} data The data to import to the store.
* Notice that any object can be passed, but this method will only
* consider existing properties.
* Also, the type of each property is not checked, so the store could be
* broken after importing. It's up to the user to import a correct file.
* @return {!PartialStoreData} store data with changed properties.
*/
handleImportStore(data) {
const /** !PartialStoreData */ newStoreData = {};
Object.values(Property).forEach((propertyName) => {
if (propertyName in data) {
newStoreData[propertyName] = data[propertyName];
}
});
return newStoreData;
}

/**
* Handles data from a REQUEST_RESPONSE_OK action.
* @param {!DataResponse} data The data payload from the action.
Expand Down
151 changes: 151 additions & 0 deletions eeg_modelling/eeg_viewer/static/js/uploader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright 2019 The Google Research Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
* @fileoverview Allows uploading data from a JSON file to the store.
*/

goog.module('eeg_modelling.eeg_viewer.Uploader');

const Dispatcher = goog.require('eeg_modelling.eeg_viewer.Dispatcher');
const Downloader = goog.require('eeg_modelling.eeg_viewer.Downloader');
const HtmlSanitizer = goog.require('goog.html.sanitizer.HtmlSanitizer');
const SafeHtml = goog.require('goog.html.SafeHtml');
const utils = goog.require('eeg_modelling.eeg_viewer.utils');


class Uploader {
constructor() {
/** @private {string} */
this.containerId_ = 'uploader-modal';
/** @private {string} */
this.fileInputId_ = 'uploader-file-input';
/** @private {string} */
this.textDisplayId_ = 'uploader-text-display';
}

/**
* Dispatches an error with a given message.
* @param {string} message Message to send in the error.
* @private
*/
dispatchError_(message) {
Dispatcher.getInstance().sendAction({
actionType: Dispatcher.ActionType.ERROR,
data: {
message,
},
});
}

/**
* Retrieves the file selected and sends an action to upload it.
* @return {!Promise} Promise that resolves once the file was read.
*/
upload() {
const input = utils.getInputElement(this.fileInputId_);
if (input.files.length === 0) {
this.dispatchError_('No file selected');
return Promise.resolve();
}

const file = input.files[0];

const reader = new FileReader();
return new Promise((resolve) => {
reader.onload = () => {
// The file text is sanitized before parsing to prevent code injections.
// Note that the string can't be escaped to prevent injections, since
// that would escape the quotes and break the JSON.

const sanitizer = new HtmlSanitizer();

let fileData;
const text = /** @type {string} */ (reader.result);
const safeText = SafeHtml.unwrap(sanitizer.sanitize(text));

try {
fileData = JSON.parse(safeText);
} catch (err) {
this.dispatchError_('Can\'t parse file as JSON');
resolve();
return;
}

fileData = /** @type {!Downloader.DownloadObject} */ (fileData);

if (!fileData.store || !fileData.timestamp || !fileData.fileParams) {
this.dispatchError_('JSON file has missing attributes');
resolve();
return;
}

Dispatcher.getInstance().sendAction({
actionType: Dispatcher.ActionType.IMPORT_STORE,
data: fileData.store,
});

this.closeMenu();

resolve();
};
reader.readAsText(file);
});
}

/**
* Sets the text to be displayed.
* @param {?string} value Text to be displayed.
* @private
*/
setTextDisplay_(value) {
const textDisplay = utils.getInputElement(this.textDisplayId_);
textDisplay.value = value || 'No file chosen';
}

/**
* Handles a change in the hidden file input, which will update the text
* displayed.
*/
handleFileChange() {
const input = utils.getInputElement(this.fileInputId_);
if (input.files.length === 0) {
this.setTextDisplay_(null);
return;
}
this.setTextDisplay_(input.files[0].name);
}

/**
* Closes the modal menu.
*/
closeMenu() {
const fileInput = utils.getInputElement(this.fileInputId_);
fileInput.value = null;
this.setTextDisplay_(null);

utils.hideElement(this.containerId_);
}

/**
* Opens the modal menu.
*/
openMenu() {
utils.showElement(this.containerId_);
}
}

goog.addSingletonGetter(Uploader);

exports = Uploader;
Loading

0 comments on commit 0c2e07c

Please sign in to comment.