From baf60f6085eff7c84d744a2875d79609717d911e Mon Sep 17 00:00:00 2001 From: Laetitia Fesselier Date: Fri, 18 Jun 2021 18:31:24 -0400 Subject: [PATCH] Phase 2 - additional features et layout changes --- .eslintrc.json | 10 +- package.json | 2 + public/preload.js | 8 + python/eeg2bids.py | 67 ++- python/libs/BIDS.py | 9 + python/libs/{TSV.py => Modifier.py} | 178 ++++-- python/libs/iEEG.py | 64 +- python/libs/loris_api.py | 147 +++++ requirements.txt | 8 +- src/css/Configuration.css | 20 +- src/css/Converter.css | 19 +- src/css/Menu.css | 7 +- src/css/Modal.css | 4 +- src/css/Validator.css | 8 +- src/css/Welcome.css | 1 - src/css/index.css | 48 +- src/jsx/Configuration.js | 870 +++++++++++++++++++++++----- src/jsx/Converter.js | 552 +++++++++--------- src/jsx/SplashScreen.js | 4 +- src/jsx/Validator.js | 40 +- src/jsx/Welcome.js | 52 +- src/jsx/elements/inputs.js | 248 +++++++- src/jsx/elements/menu.js | 8 +- src/jsx/elements/modal.js | 14 +- wiki/macOS/README.md | 4 +- 25 files changed, 1762 insertions(+), 630 deletions(-) rename python/libs/{TSV.py => Modifier.py} (50%) create mode 100644 python/libs/loris_api.py diff --git a/.eslintrc.json b/.eslintrc.json index ab39341..0d1b34b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -20,7 +20,11 @@ "globalReturn": false } }, - "extends": ["eslint:recommended", "plugin:react/recommended", "google"], + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "google" + ], "rules": { "max-len": ["error", { "code": 80, @@ -35,7 +39,9 @@ "require-jsdoc": "error", "no-console": ["warn", { "allow": ["info", "warn", "error"] - }] + }], + "react/jsx-curly-brace-presence": "warn", + "react/jsx-key": "warn" }, "globals": { "React": true, diff --git a/package.json b/package.json index 28f0439..d00bc5f 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-react": "^7.24.0", "lint-staged": "^11.0.0", + "react-datepicker": "^4.1.1", + "react-switch": "^6.0.0", "wait-on": "^5.3.0" }, "scripts": { diff --git a/public/preload.js b/public/preload.js index a5cfc46..7b717d8 100644 --- a/public/preload.js +++ b/public/preload.js @@ -5,10 +5,18 @@ contextBridge.exposeInMainWorld('myAPI', { const {dialog} = require('@electron/remote'); return dialog; }, + visitBIDS: () => { + const {shell} = require('electron'); + shell.openExternal('https://bids.neuroimaging.io'); + }, visitGitHub: () => { const {shell} = require('electron'); shell.openExternal('https://github.com/aces/eeg2bids'); }, + visitIssues: () => { + const {shell} = require('electron'); + shell.openExternal('https://github.com/aces/eeg2bids/issues'); + }, visitMNE: () => { const {shell} = require('electron'); shell.openExternal('https://mne.tools/mne-bids/'); diff --git a/python/eeg2bids.py b/python/eeg2bids.py index 3842954..9852b9f 100644 --- a/python/eeg2bids.py +++ b/python/eeg2bids.py @@ -3,12 +3,15 @@ from eventlet import tpool import socketio from python.libs import iEEG +from python.libs.Modifier import Modifier from python.libs import BIDS - +from python.libs.loris_api import LorisAPI +import csv # Create socket listener. sio = socketio.Server(async_mode='eventlet', cors_allowed_origins=[]) app = socketio.WSGIApp(sio) +loris_api = LorisAPI() # EEG2BIDS Wizard version appVersion = '1.0.0' @@ -44,6 +47,22 @@ def tarfile_bids(sid, data): sio.emit('response', send) +@sio.event +def get_loris_sites(sid): + sio.emit('loris_sites', loris_api.get_sites()) + +@sio.event +def get_loris_projects(sid): + sio.emit('loris_projects', loris_api.get_projects()) + +@sio.event +def get_loris_subprojects(sid, project): + sio.emit('loris_subprojects', loris_api.get_subprojects(project)) + +@sio.event +def get_loris_visits(sid, project): + sio.emit('loris_visits', loris_api.get_visits(project)) + @sio.event def ieeg_get_header(sid, data): # data = { file_path: 'path to iEEG file' } @@ -59,24 +78,42 @@ def ieeg_get_header(sid, data): response = { 'error': 'Failed to retrieve EDF header information', } - sio.emit('response', response) + sio.emit('edf_header', response) +@sio.event +def get_metadata(sid, data): + # data = { file_path: 'path to metadata file' } + print('metadata file:', data) + + if not data['file_path']: + print('No file path found.') + response = { + 'error': 'No file path found.', + } + else : + try: + with open(data['file_path']) as fd: + reader = csv.DictReader(fd, delimiter="\t", quotechar='"') + response = { + 'metadata': {rows['Field']:rows['Value'] for rows in reader} + } + except IOError: + print("Could not read the metadata file.") + response = { + 'error': 'No file path found.', + } + + sio.emit('metadata', response) def edf_to_bids_thread(data): print('data is ') print(data) error_messages = [] - if not data['file_path']: - error_messages.append('The file.edf to convert is missing.') + if not data['file_paths']: + error_messages.append('No .edf file(s) to convert.') if not data['bids_directory']: error_messages.append('The BIDS output directory is missing.') - if not data['site_id']: - error_messages.append('The LORIS SiteID is missing.') - if not data['project_id']: - error_messages.append('The LORIS ProjectID is missing.') - if not data['sub_project_id']: - error_messages.append('The LORIS SubProjectID is missing.') - if not data['visit_label']: + if not data['session']: error_messages.append('The LORIS Visit Label is missing.') if not error_messages: @@ -84,10 +121,10 @@ def edf_to_bids_thread(data): data['output_time'] = 'output-' + time.latest_output iEEG.Converter(data) # EDF to BIDS format. - # store subject_id for iEEG.Modifier + # store subject_id for Modifier data['subject_id'] = iEEG.Converter.m_info['subject_id'] data['appVersion'] = appVersion - iEEG.Modifier(data, sio) # Modifies data of BIDS format + Modifier(data) # Modifies data of BIDS format response = { 'output_time': data['output_time'] } @@ -100,9 +137,9 @@ def edf_to_bids_thread(data): @sio.event def edf_to_bids(sid, data): - # data = { file_path: '', bids_directory: '', read_only: false, + # data = { file_paths: [], bids_directory: '', read_only: false, # events_tsv: '', line_freq: '', site_id: '', project_id: '', - # sub_project_id: '', visit_label: '', subject_id: ''} + # sub_project_id: '', session: '', subject_id: ''} print('edf_to_bids: ', data) response = eventlet.tpool.execute(edf_to_bids_thread, data) print(response) diff --git a/python/libs/BIDS.py b/python/libs/BIDS.py index be8f73b..b6f6f0b 100644 --- a/python/libs/BIDS.py +++ b/python/libs/BIDS.py @@ -15,6 +15,15 @@ def __init__(self, data): validator = BIDSValidator() for path, dirs, files in os.walk(start_path): for filename in files: + if filename == '.bidsignore': + continue + + if filename.endswith('_annotations.tsv'): + continue + + if filename.endswith('_annotations.json'): + continue + temp = os.path.join(path, filename) file_paths.append(temp[len(start_path):len(temp)]) result.append(validator.is_bids(temp[len(start_path):len(temp)])) diff --git a/python/libs/TSV.py b/python/libs/Modifier.py similarity index 50% rename from python/libs/TSV.py rename to python/libs/Modifier.py index b46519a..267faf6 100644 --- a/python/libs/TSV.py +++ b/python/libs/Modifier.py @@ -2,14 +2,60 @@ import csv import json import re +import shutil -# Writer - writes to tsv file -class Writer: - def __init__(self, data, sio): - print('- Writer: init started.') +class Modifier: + def __init__(self, data): + self.data = data + print(self.data) + + print('- Modifier: init started.') + + self.modify_dataset_description_json() + self.modify_participants_tsv() + self.modify_participants_json() + self.copy_events_tsv() + self.copy_annotations_files() + self.modify_eeg_json() + + + def get_bids_root_path(self): + return os.path.join( + self.data['bids_directory'], + self.data['output_time'] + ) + + def get_eeg_path(self): + directory_path = 'sub-' + self.data['participantID'].replace('_', '').replace('-', '').replace(' ', '') + + return os.path.join( + self.get_bids_root_path(), + directory_path, + 'ses-' + self.data['session'], + self.data['modality'] + ) + + def modify_dataset_description_json(self): + file_path = os.path.join( + self.get_bids_root_path(), + 'dataset_description.json' + ) + + try: + with open(file_path, "r") as fp: + file_data = json.load(fp) + file_data['PreparedBy'] = self.data['preparedBy'] + + with open(file_path, "w") as fp: + json.dump(file_data, fp, indent=4) + + except IOError: + print("Could not read or write dataset_description.json file") + + + def modify_participants_tsv(self): file_path = os.path.join( - data['bids_directory'], - data['output_time'], + self.get_bids_root_path(), 'participants.tsv' ) @@ -27,12 +73,12 @@ def __init__(self, data, sio): output.append( [ participant_id, - age, - sex, - hand, - data['site_id'], - data['project_id'], - data['sub_project_id'] + self.data['age'], + self.data['sex'], + self.data['hand'], + self.data['site_id'], + self.data['sub_project_id'], + self.data['project_id'] ] ) except ValueError: @@ -41,45 +87,45 @@ def __init__(self, data, sio): output.append( [ participant_id, - age, - sex, - hand, - data['site_id'], - data['project_id'], - data['sub_project_id'] + self.data['age'], + self.data['sex'], + self.data['hand'], + self.data['site_id'], + self.data['sub_project_id'], + self.data['project_id'] ] ) except ValueError: print('error: ValueError') with open(file_path, mode='w', newline='') as tsv_file: - headers = ['participant_id', 'age', 'sex', 'hand', 'site', 'project', 'subproject'] + headers = ['participant_id', 'age', 'sex', 'hand', 'site', 'subproject', 'project'] writer = csv.writer(tsv_file, delimiter='\t') writer.writerow(headers) writer.writerows(output) tsv_file.close() - # modify the participants.json file - # and include siteID, projectID, subprojectID + + def modify_participants_json(self): file_path = os.path.join( - data['bids_directory'], - data['output_time'], + self.get_bids_root_path(), 'participants.json' ) + with open(file_path, mode='r+', encoding='utf-8') as json_file: json_data = json.load(json_file) user_data = { 'site': { - 'Description': data['site_id'] - }, - 'project': { - 'Description': data['project_id'] + 'Description': "Site of the testing" }, 'subproject': { - 'Description': data['sub_project_id'] + 'Description': "Subproject of the participant" + }, + 'project': { + 'Description': "Project of the participant" }, 'debug': { - 'Version': data['appVersion'] + 'Version': self.data['appVersion'] } } json_data.update(user_data) @@ -88,18 +134,45 @@ def __init__(self, data, sio): json_file.close() -class Copy: - def __init__(self, data, sio): - print(data) - directory_path = 'sub-' + data['subject_id'].replace( - '_', '' - ).replace('-', '').replace(' ', '') + def copy_annotations_files(self): + if not self.data['annotations_tsv'] and not self.data['annotations_json']: + return + + file = os.path.join( + self.get_bids_root_path(), + '.bidsignore' + ) + + with open(file, mode='w', newline='') as bidsignore: + bidsignore.write('*_annotations.json\n') + bidsignore.write('*_annotations.tsv\n') + bidsignore.close() + + edf_file = [f for f in os.listdir(self.get_eeg_path()) if f.endswith('.edf')] + filename = os.path.join(self.get_eeg_path(), re.sub(r"_i?eeg.edf", '_annotations', edf_file[0])) + + if self.data['annotations_tsv']: + shutil.copyfile( + self.data['annotations_tsv'], + os.path.join(self.get_eeg_path(), filename + '.tsv') + ) + if self.data['annotations_json']: + shutil.copyfile( + self.data['annotations_json'], + os.path.join(self.get_eeg_path(), filename + '.json') + ) + + + def copy_events_tsv(self): + if not self.data['events_tsv']: + return + # events.tsv data collected: output = [] # Open user supplied events.tsv and grab data. - with open(data['events_tsv'], mode='r', newline='') as tsv_file: + with open(self.data['events_tsv'], mode='r', newline='') as tsv_file: tsv_file.readline() reader = csv.reader(tsv_file, delimiter='\t') rows = list(reader) @@ -117,13 +190,7 @@ def __init__(self, data, sio): print('error: ValueError') # The BIDS events.tsv location: - start_path = os.path.join( - data['bids_directory'], - data['output_time'], - directory_path, - 'ses-' + data['visit_label'], - (data['modality'].lower() or 'ieeg') - ) + start_path = self.get_eeg_path() path_events_tsv = '' eeg_edf = '' @@ -172,3 +239,28 @@ def __init__(self, data, sio): writer.writerows(output) tsv_file.close() + + def modify_eeg_json(self): + eeg_json = [f for f in os.listdir(self.get_eeg_path()) if f.endswith('eeg.json')] + if len(eeg_json) != 1: + raise ValueError('Found more than one eeg.json file') + + file_path = os.path.join(self.get_eeg_path(), eeg_json[0]) + + try: + with open(file_path, "r") as fp: + file_data = json.load(fp) + file_data["SoftwareFilters"] = self.data['software_filters'] + file_data["RecordingType"] = self.data['recording_type'] + + if (self.data["modality"] == "ieeg"): + file_data["iEEGReference"] = self.data['reference'] + else: + file_data["EEGReference"] = self.data['reference'] + + with open(file_path, "w") as fp: + json.dump(file_data, fp, indent=4) + + except IOError as e: + print(e) + print("Could not read or write eeg.json file") diff --git a/python/libs/iEEG.py b/python/libs/iEEG.py index 7412e0d..cafbde0 100644 --- a/python/libs/iEEG.py +++ b/python/libs/iEEG.py @@ -1,9 +1,8 @@ import os import mne from python.libs import EDF -from python.libs import TSV from mne_bids import write_raw_bids, BIDSPath - +import json # TarFile - tarfile the BIDS data. class TarFile: @@ -68,23 +67,26 @@ class Converter: # data = { file_path: '', bids_directory: '', read_only: false, # events_tsv: '', line_freq: '', site_id: '', project_id: '', - # sub_project_id: '', visit_label: '', subject_id: ''} + # sub_project_id: '', session: '', subject_id: ''} def __init__(self, data): print('- Converter: init started.') modality = 'seeg' - if data['modality'] == 'EEG': + if data['modality'] == 'eeg': modality = 'eeg' - self.to_bids( - file=data['file_path'], - ch_type=modality, - bids_directory=data['bids_directory'], - subject_id=data['subject_id'], - visit_label=data['visit_label'], - output_time=data['output_time'], - read_only=data['read_only'], - line_freq=data['line_freq'] - ) + for i, file_path in enumerate(data['file_paths']): + self.to_bids( + file=file_path, + ch_type=modality, + task=data['taskName'], + bids_directory=data['bids_directory'], + subject_id=data['participantID'], + session=data['session'], + split=((i+1) if len(data['file_paths']) > 1 else None), + output_time=data['output_time'], + read_only=data['read_only'], + line_freq=data['line_freq'] + ) @staticmethod def validate(path): @@ -102,12 +104,13 @@ def to_bids(self, file, bids_directory, subject_id, - visit_label, + session, output_time, task='test', + split=None, ch_type='seeg', read_only=False, - line_freq=60): + line_freq='n/a'): if self.validate(file): reader = EDF.EDFReader(fname=file) m_info, c_info = reader.open(fname=file) @@ -123,20 +126,16 @@ def to_bids(self, os.makedirs(bids_directory + os.path.sep + output_time, exist_ok=True) bids_directory = bids_directory + os.path.sep + output_time bids_root = bids_directory - m_info['subject_id'] = subject_id # 'alizee' + + m_info['subject_id'] = subject_id subject = m_info['subject_id'].replace('_', '').replace('-', '').replace(' ', '') - bids_basename = BIDSPath(subject=subject, task=task, root=bids_root, acquisition="seeg") - session = visit_label + bids_basename = BIDSPath(subject=subject, task=task, root=bids_root, acquisition=ch_type, split=split) + session = session bids_basename.update(session=session) raw.info['line_freq'] = line_freq - raw.info['subject_info'] = { - # 'his_id': "test", - # 'birthday': (1993, 1, 26), - # 'sex': 1, - # 'hand': 2, - } + raw._init_kwargs = { 'input_fname': file, 'eog': None, @@ -147,7 +146,11 @@ def to_bids(self, 'verbose': None } try: - write_raw_bids(raw, bids_basename, anonymize=dict(daysback=33630), overwrite=False, verbose=False) + write_raw_bids(raw, bids_basename, overwrite=False, verbose=False) + with open(bids_basename, 'r+b') as f: + f.seek(8) # id_info field starts 8 bytes in + f.write(bytes("X X X X".ljust(80), 'ascii')) + except Exception as ex: print(ex) print('finished') @@ -162,12 +165,3 @@ def __init__(self): from datetime import datetime now = datetime.now() self.latest_output = now.strftime("%Y-%m-%d-%Hh%Mm%Ss") - - -# Modifier - 1) used for SiteID to participants.tsv -# 2) used for user's events.tsv to BIDS output events.tsv -class Modifier: - def __init__(self, data, sio): - print('- Modifier: init started.') - TSV.Writer(data, sio) # includes SiteID to participants.tsv - TSV.Copy(data, sio) # copies events.tsv to ieeg directory. diff --git a/python/libs/loris_api.py b/python/libs/loris_api.py new file mode 100644 index 0000000..2d646d4 --- /dev/null +++ b/python/libs/loris_api.py @@ -0,0 +1,147 @@ +import json +import requests +import urllib + +class LorisAPI: + url = 'https://localhost/api/v0.0.4-dev/' + username = '' + password = '' + token = '' + + def __init__(self): + self.login() + + def login(self): + resp = json.loads(requests.post( + url = self.url + 'login', + json = { + 'username': self.username, + 'password': self.password + }, + verify = False + ).content.decode('ascii')) + + if resp.get('error'): + raise RuntimeError(resp.get('error')) + + self.token = resp.get('token') + print(self.token) + + def get_projects(self): + resp = requests.get( + url = self.url + 'projects', + headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + verify = False + ) + + json_resp = json.loads(resp.content.decode('ascii')) + return json_resp.get('Projects') + + def get_subprojects(self, project): + project = self.get_project(project) + return project.get('Subprojects') + + def get_visits(self, project): + project = self.get_project(project) + return project.get('Visits') + + def get_sites(self): + resp = requests.get( + url = self.url + 'sites', + headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + verify = False + ) + + json_resp = json.loads(resp.content.decode('ascii')) + sites = json_resp.get('Sites') + return sites + + def get_project(self, project): + resp = requests.get( + url = self.url + 'projects/' + urllib.parse.quote(project), + headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + verify = False + ) + + json_resp = json.loads(resp.content.decode('ascii')) + return json_resp + + def save_instrument(self): + resp = requests.put( + url = self.url + '/candidates/661630/V1/instruments/pet_mri_scans', + headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + data = json.dumps({ + "Meta" : { + "Instrument" : 'PET/MRI scans', + "Visit" : 'V1', + "Candidate" : 661630, + "DDE" : False + }, + "PET/MRI scans" : { + "Date_taken" : "2021-06-07", + "Examiner" : "Rida", + "completion-date" : "2021-06-07", + } + }), + verify = False + ) + + print(resp) + json_resp = json.loads(resp.content.decode('ascii')) + print(json_resp) + + def get_visit(self, candid, visit, site, subproject, project): + resp = requests.get( + url = self.url + '/candidates/' + str(candid) + '/' + urllib.parse.quote(visit), + headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + data = json.dumps({ + "Meta" : { + "CandID" : candid, + "Visit" : visit, + "Site" : site, + "Battery" : subproject, + "Project" : project + } + }), + verify = False + ) + + json_resp = json.loads(resp.content.decode('ascii')) + return json_resp + + def start_next_stage(self, candid, visit, site, subproject, project, date): + resp = requests.patch( + url = self.url + '/candidates/' + str(candid) + '/' + urllib.parse.quote(visit), + headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + data = json.dumps({ + "CandID" : candid, + "Visit" : visit, + "Site" : site, + "Battery" : subproject, + "Project" : project, + "NextStageDate" : date + }), + verify = False + ) + + print (resp.status_code) + print(resp.text) + + def create_candidate(self): + resp = requests.post( + url = self.url + '/candidates/', + headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + data = json.dumps({ + "Candidate" : { + "Project" : 'Pumpernickel', + "DoB" : "1985-12-22", + "Sex" : "Female", + "Site" : 'Montreal', + } + }), + verify = False + ) + + print(resp) + json_resp = json.loads(resp.content.decode('ascii')) + print(json_resp) diff --git a/requirements.txt b/requirements.txt index bd16f75..85f997b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,12 @@ eventlet>=0.31.0 -mne~=0.22.0 -mne-bids~=0.6 +mne~=0.23.0 +# MNE-BIDS 0.8 will support the .EDF extension +# Before its release we need to use the dev version +git+git://github.com/mne-tools/mne-bids@main#egg=mne-bids mne-features~=0.1 numpy~=1.19.5 python-socketio~=5.0.4 python-engineio~=4.0.0 bids-validator~=1.6.0 +pybv>=0.4 +requests>=2.25.0 \ No newline at end of file diff --git a/src/css/Configuration.css b/src/css/Configuration.css index 1dcace5..1e7cecf 100644 --- a/src/css/Configuration.css +++ b/src/css/Configuration.css @@ -1,10 +1,5 @@ -.anonymize { - margin-top: 12px; - max-height: 30px; -} .readonly { cursor: default; - text-align: center; background: transparent; border: 1px none #3276b1; border-bottom-style: solid; @@ -12,3 +7,18 @@ .readonly:focus, .readonly:focus{ outline: none; } +input[type="button"].primary-btn { + color: #fff; + background-color: #064785; + border: none; + border-radius: .25rem; + padding: .375rem .75rem; +} +input[type="button"].primary-btn:disabled { + color: #000; + background-color: #ddd; + border: 1px solid #333; +} +textarea { + vertical-align: top; +} \ No newline at end of file diff --git a/src/css/Converter.css b/src/css/Converter.css index d61ec90..54c6678 100644 --- a/src/css/Converter.css +++ b/src/css/Converter.css @@ -1,15 +1,20 @@ .checkmark { - cursor: default; - font-size: 24px; - color: transparent; - text-shadow: 0 0 0 #43a243; + font-size: 18px; + padding-right: 10px; + color: #43a243; font-family: Segoe UI Symbol, serif; } .warning { - font-size: 24px; + font-size: 20px; + padding-right: 10px; + color: #FF8C00; font-family: Segoe UI Symbol, serif; } - +.error { + font-size: 18px; + padding-right: 10px; + color: red; +} .tooltip { cursor: default; position: relative; @@ -41,5 +46,5 @@ } .convert-bids-row { padding-top: 0; - margin-top: -5px; + margin-top: 10px; } diff --git a/src/css/Menu.css b/src/css/Menu.css index f4c391c..6fffc4b 100644 --- a/src/css/Menu.css +++ b/src/css/Menu.css @@ -6,15 +6,12 @@ border-bottom: 5px solid #252326; } .menu { - width: 100%; - margin: 0 auto; - display: table; + display: flex; + justify-content: space-around; } .menuTab { user-select: none; - position: relative; - display: table-cell; -webkit-user-select: none; } diff --git a/src/css/Modal.css b/src/css/Modal.css index cc2345e..c2af293 100644 --- a/src/css/Modal.css +++ b/src/css/Modal.css @@ -53,7 +53,7 @@ .modalHeader { height: 30px; display: flex; - padding: 30px; + padding: 30px 30px 0 30px; font-size: 18pt; align-items: center; flex-direction: row; @@ -69,7 +69,7 @@ .modalContent { font-size: 14pt; min-height: 80px; - padding: 10px 15px 5px 15px; + padding: 30px; -webkit-overflow-scrolling: touch; } .bids-errors { diff --git a/src/css/Validator.css b/src/css/Validator.css index d40df93..5ddec55 100644 --- a/src/css/Validator.css +++ b/src/css/Validator.css @@ -1,11 +1,8 @@ -.red-font-bold { +.red { color: #ff0000; - font-weight: bold; } -.green-font-italic { +.green { color: #02a902; - font-weight: bold; - font-style: italic; } .key-terminal { margin: 20px; @@ -15,6 +12,7 @@ background-color: #f4f4f4; } .terminal { + color: white; margin: 20px; overflow: auto; max-height: 600px; diff --git a/src/css/Welcome.css b/src/css/Welcome.css index 8006cc2..ea714ca 100644 --- a/src/css/Welcome.css +++ b/src/css/Welcome.css @@ -19,7 +19,6 @@ .footer { bottom: 0; width: 100%; - height: 40px; padding: 10px; color: #c8c8c8; position: fixed; diff --git a/src/css/index.css b/src/css/index.css index e0681d4..1291204 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -25,11 +25,17 @@ body { text-align: center; vertical-align: middle; } +.container { + display: flex; +} .info { padding: 14px; background-color: #f3f4f8; /*background-color: #039b83;*/ } +.half { + width: 50%; +} .small-pad { padding: 10px; } @@ -38,10 +44,50 @@ body { padding: 14px; flex-wrap: wrap; background-color: #f3f4f8; - /*background-color: #039b83;*/ +} +.info-flex-container input { + width: 100%; } .small-pad-flex { flex: 1; padding: 10px; max-width: 175px; } +.label { + width: 210px; + display: inline-block; + max-width: 100%; + padding-right: 20px; +} +.report .label { + width: auto; +} +input { + max-width: 100%; +} +.react-switch { + vertical-align: middle; + margin-right: 15px; +} +.comboField { + display: inline-block; + max-width: calc(100% - 230px); + vertical-align: top; +} +.alert { + position: relative; + padding: .75rem 1.25rem; + margin: 1rem 0 2rem 0; + border: 1px solid transparent; + border-radius: .25rem; +} +.alert-danger { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; +} +.alert-success { + color: #155724; + background-color: #d4edda; + border-color: #c3e6cb; +} \ No newline at end of file diff --git a/src/jsx/Configuration.js b/src/jsx/Configuration.js index 81d10b1..6b08db0 100644 --- a/src/jsx/Configuration.js +++ b/src/jsx/Configuration.js @@ -2,17 +2,23 @@ import React, {useContext, useState, useEffect} from 'react'; import {AppContext} from '../context'; import PropTypes from 'prop-types'; import '../css/Configuration.css'; +import Switch from 'react-switch'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; // Components import { DirectoryInput, FileInput, - NumberInput, RadioInput, + NumberInput, + RadioInput, TextInput, + SelectInput, + TextareaInput, } from './elements/inputs'; // Socket.io -import {Event, SocketContext} from './socket.io'; +import {SocketContext} from './socket.io'; /** * Configuration - the Data Configuration component. @@ -25,103 +31,280 @@ const Configuration = (props) => { const socketContext = useContext(SocketContext); // React State - const [edfFile, setEdfFile] = useState({}); - const [edfType, setEdfType] = useState('iEEG'); - const [eventsTSV, setEventsTSV] = useState({}); - const [bidsDirectory, setBidsDirectory] = useState(null); - const [lineFreq, setLineFreq] = useState(''); - const [siteID, setSiteID] = useState(''); - const [projectID, setProjectID] = useState(''); - const [subProjectID, setSubProjectID] = useState(''); - const [visitLabel, setVisitLabel] = useState(''); - const [headerFields, setHeaderFields] = useState(null); - const [edfHeader, setHeader] = useState({ + const state = {}; + state.edfFiles = {}; + [state.edfFiles.get, state.edfFiles.set] = useState([]); + state.edfType = {}; + [state.edfType.get, state.edfType.set] = useState('ieeg'); + state.eventsTSV = {}; + [state.eventsTSV.get, state.eventsTSV.set] = useState([]); + state.annotationsTSV = {}; + [state.annotationsTSV.get, state.annotationsTSV.set] = useState([]); + state.annotationsJSON = {}; + [state.annotationsJSON.get, state.annotationsJSON.set] = useState([]); + state.bidsDirectory = {}; + [state.bidsDirectory.get, state.bidsDirectory.set] = useState(null); + state.LORIScompliant = {}; + [state.LORIScompliant.get, state.LORIScompliant.set] = useState(true); + state.siteID = {}; + [state.siteID.get, state.siteID.set] = useState('n/a'); + state.siteOptions = {}; + [state.siteOptions.get, state.siteOptions.set] = useState([]); + state.siteUseAPI = {}; + [state.siteUseAPI.get, state.siteUseAPI.set] = useState(false); + state.projectID = {}; + [state.projectID.get, state.projectID.set] = useState('n/a'); + state.projectOptions = {}; + [state.projectOptions.get, state.projectOptions.set] = useState([]); + state.projectUseAPI = {}; + [state.projectUseAPI.get, state.projectUseAPI.set] = useState(false); + state.subprojectID = {}; + [state.subprojectID.get, state.subprojectID.set] = useState('n/a'); + state.recordingDate = {}; + [state.recordingDate.get, state.recordingDate.set] = useState(null); + state.subprojectOptions = {}; + [state.subprojectOptions.get, state.subprojectOptions.set] = useState([]); + state.subprojectUseAPI = {}; + [state.subprojectUseAPI.get, state.subprojectUseAPI.set] = useState(false); + state.session = {}; + [state.session.get, state.session.set] = useState(''); + state.sessionOptions = {}; + state.eegMetadataFile = {}; + [state.eegMetadataFile.get, state.eegMetadataFile.set] = useState([]); + state.eegMetadata = {}; + [state.eegMetadata.get, state.eegMetadata.set] = useState([]); + state.lineFreq = {}; + [state.lineFreq.get, state.lineFreq.set] = useState(''); + state.taskName = {}; + [state.taskName.get, state.taskName.set] = useState(''); + state.reference = {}; + [state.reference.get, state.reference.set] = useState('n/a'); + state.recordingType = {}; + [state.recordingType.get, state.recordingType.set] = useState('n/a'); + state.softwareFilters = {}; + [state.softwareFilters.get, state.softwareFilters.set] = useState('n/a'); + [state.sessionOptions.get, state.sessionOptions.set] = useState([]); + state.sessionUseAPI = {}; + [state.sessionUseAPI.get, state.sessionUseAPI.set] = useState(false); + state.participantEntryMode = {}; + [ + state.participantEntryMode.get, + state.participantEntryMode.set, + ] = useState('loris'); + state.participantID = {}; + [state.participantID.get, state.participantID.set] = useState(''); + state.participantDOB = {}; + [state.participantDOB.get, state.participantDOB.set] = useState(''); + state.participantAge = {}; + [state.participantAge.get, state.participantAge.set] = useState('n/a'); + state.participantSex = {}; + [state.participantSex.get, state.participantSex.set] = useState('n/a'); + state.participantHand = {}; + [state.participantHand.get, state.participantHand.set] = useState('n/a'); + state.anonymize = {}; + [state.anonymize.get, state.anonymize.set] = useState(false); + state.subjectID = {}; + [state.subjectID.get, state.subjectID.set] = useState(''); + state.edfHeader = {}; + [state.edfHeader.get, state.edfHeader.set] = useState({ subject_id: '', recording_id: '', day: '', month: '', year: '', hour: '', minute: '', second: '', subtype: '', }); + useEffect(() => { + Object.keys(state).map((key) => appContext.setTask(key, state[key].get)); + }, []); + /** * Similar to componentDidMount and componentDidUpdate. */ useEffect(() => { - const keys = [ - 'subject_id', 'recording_id', - 'day', 'month', 'year', - 'hour', 'minute', 'second', - 'subtype', - ]; - const renderFields = []; - for (const key of keys) { - renderFields.push( -
- -
, + Object.keys(state.edfHeader.get).map((key) => { + appContext.setTask(key, state.edfHeader.get[key]); + }); + + if (!isNaN(parseInt(state.edfHeader.get['year']))) { + const date = new Date( + state.edfHeader.get['year'] < 85 ? + '20' + state.edfHeader.get['year'] : + '19' + state.edfHeader.get['year'], + (state.edfHeader.get['month']-1), + state.edfHeader.get['day'], + state.edfHeader.get['hour'], + state.edfHeader.get['minute'], + state.edfHeader.get['second'], ); - appContext.setTask(key, edfHeader[key]); + state.recordingDate.set(date); + appContext.setTask('recordingDate', date); } - setHeaderFields(renderFields); - }, [edfHeader]); + }, [state.edfHeader.get]); + + /** + * Similar to componentDidMount and componentDidUpdate. + */ + useEffect(() => { + if (socketContext) { + socketContext.emit('get_loris_sites'); + socketContext.on('loris_sites', (sites) => { + const siteOpts = []; + sites.map((site) => { + siteOpts.push(site.Name); + }); + state.siteOptions.set(siteOpts); + }); + + socketContext.emit('get_loris_projects'); + socketContext.on('loris_projects', (projects) => { + const projectOpts = []; + Object.keys(projects).map((project) => { + projectOpts.push(project); + }); + state.projectOptions.set(projectOpts); + }); + + socketContext.on('loris_subprojects', (subprojects) => { + const subprojectOpts = []; + subprojects.map((subproject) => { + subprojectOpts.push(subproject); + }); + state.subprojectOptions.set(subprojectOpts); + }); + + socketContext.on('loris_visits', (visits) => { + const visitOpts = []; + visits.map((visit) => { + visitOpts.push(visit); + }); + state.sessionOptions.set(visitOpts); + }); + + socketContext.on('edf_header', (message) => { + if (message['error']) { + console.error(message['error']); + } + + if (message['header']) { + state.edfHeader.set(message['header']); + state.subjectID.set(message['header']['subject_id']); + } + }); + + socketContext.on('metadata', (message) => { + if (message['error']) { + console.error(message['error']); + } + + if (message['metadata']) { + state.eegMetadata.set(message['metadata']); + } + }); + } + }, [socketContext]); + /** * onUserInput - input change by user. * @param {string} name - element name - * @param {object|string} value - element value + * @param {object|string|boolean} value - element value */ const onUserInput = (name, value) => { // Update the state of Configuration. switch (name) { - case 'edfFile': { - setEdfFile(value); - createHeaderFields(value['path']); + case 'edfFiles': + state.edfFiles.set(value); + createHeaderFields(value[0]['path']); break; - } - case 'edfType': { - setEdfType(value); + case 'eegMetadataFile': + state.eegMetadataFile.set(value); + createMetadataFields(value[0]['path']); break; - } - case 'eventsTSV': { - setEventsTSV(value); + case 'LORIScompliant': + if (value === 'yes') { + value = true; + state.participantEntryMode.set('loris'); + } else { + value = false; + state.participantEntryMode.set('manual'); + } + state.LORIScompliant.set(value); break; - } - case 'bidsDirectory': { - setBidsDirectory(value); + case 'siteID_API': + if (value == 'Manual entry') { + value = ''; + state.siteUseAPI.set(false); + } else { + state.siteUseAPI.set(true); + } + state.siteID.set(value); + name = 'siteID'; break; - } - case 'lineFreq': { - setLineFreq(value); + case 'siteID_Manual': + state.siteID.set(value); + name = 'siteID'; break; - } - case 'siteID': { - setSiteID(value); + case 'projectID_API': + if (value == 'Manual entry') { + state.projectUseAPI.set(false); + value = ''; + } else { + state.projectUseAPI.set(true); + socketContext.emit('get_loris_subprojects', value); + socketContext.emit('get_loris_visits', value); + } + state.projectID.set(value); + name = 'projectID'; break; - } - case 'projectID': { - setProjectID(value); + case 'projectID_Manual': + state.projectID.set(value); + name = 'projectID'; break; - } - case 'subProjectID': { - setSubProjectID(value); + case 'subprojectID_API': + if (value == 'Manual entry') { + state.subprojectUseAPI.set(false); + value = ''; + } else { + state.subprojectUseAPI.set(true); + } + state.subprojectID.set(value); + name = 'subprojectID'; break; - } - case 'visitLabel': { - setVisitLabel(value); + case 'subprojectID_Manual': + state.subprojectID.set(value); + name = 'subprojectID'; break; - } - default: { - return; - } + case 'session_API': + if (value == 'Manual entry') { + state.sessionUseAPI.set(false); + value = ''; + } else { + state.sessionUseAPI.set(true); + } + state.session.set(value); + name = 'session'; + break; + case 'session_Manual': + state.session.set(value); + name = 'session'; + break; + case 'anonymize': + if (value) { + anonymizeHeaderValues(); + } else { + onUserHeaderFieldInput('subject_id', state.subjectID.get); + } + state.anonymize.set(value); + break; + default: + if (name in state) { + state[name].set(value); + } + } + if (name in state) { + // Update the 'task' of app context. + appContext.setTask(name, value); } - // Update the 'task' of app context. - appContext.setTask(name, value); }; /** @@ -130,7 +313,7 @@ const Configuration = (props) => { * @param {object|string} value - element value */ const onUserHeaderFieldInput = (name, value) => { - setHeader((prevState) => { + state.edfHeader.set((prevState) => { return {...prevState, [name]: value}; }); // Update the 'task' of app context. @@ -149,31 +332,38 @@ const Configuration = (props) => { }; /** - * onMessage - received message from python. - * @param {object} message - response + * createMetadataFields - Metadata file given from user. + * @param {string} path - metadata file path */ - const onMessage = (message) => { - console.info(message); - if (message['header']) { - setHeader(message['header']); - } + const createMetadataFields = (path) => { + socketContext.emit('get_metadata', { + file_path: path, + }); + }; + + /** + * arrayToObject - Convert an array to an object + * { value: value } + * + * @param {array} array + * + * @return {Object} + */ + const arrayToObject = (array) => { + return array.reduce((obj, item) => { + return { + ...obj, + [item]: item, + }; + }, {}); }; /** * anonymizeHeaderValues - anonymize iEEG header values. */ const anonymizeHeaderValues = () => { - const keys = [ - 'subject_id', 'recording_id', - 'day', 'month', 'year', - ]; - const anonymize = { - subject_id: '0 X X X', - recording_id: 'Startdate 31-DEC-1924 X mne-bids_anonymize X', - day: 31, - month: 12, - year: 85, - }; + const keys = ['subject_id']; + const anonymize = {subject_id: 'X X X X'}; for (const key of keys) { onUserHeaderFieldInput(key, anonymize[key]); appContext.setTask(key, anonymize[key]); @@ -181,113 +371,485 @@ const Configuration = (props) => { }; /** - * Renders the React component. - * @return {JSX.Element} - React markup for component. + * createCandidate */ + const createCandidate = () => { + }; + return props.visible ? ( <> - + Data Configuration -
-
- +
+ edfFile['name']).join(', ') + } + label='EDF Recording to convert' + required={true} onUserInput={onUserInput} /> +
Can be split into multiple EDF files
-
+
-
+
-
- +
-
- +
-
- - LORIS metadata - -
-
- + eegMetadataFile['name'], + ).join(', ') + } + label='Parameter metadata (tsv)' onUserInput={onUserInput} />
-
- +
-
- +
-
- +
+ + Recording details + +
+
+
+ +
+ {state.LORIScompliant.get && + <> +
+ +
+ + {!state.siteUseAPI.get && + + } +
+
+
+ +
+ + {!state.projectUseAPI.get && + + } +
+
+
+ +
+ + {!state.subprojectUseAPI.get && + + } +
+
+ + } +
+ +
+ {state.LORIScompliant.get && + + } + {!state.sessionUseAPI.get && + + } +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
- - EDF header data + + Participant Data -
- {headerFields} - +
+ {state.LORIScompliant.get && +
+ +
+ } + {state.participantEntryMode.get == 'loris' && + <> +
+ + onUserInput('participantDOB', date)} + /> +
+
+ +
+
+ +
+ + + } + {state.participantEntryMode.get == 'manual' && + <> +
+ + {state.LORIScompliant.get && +
Use the LORIS PSCID
+ } +
+
+ +
+
+ +
+
+ +
+ + } +
+ + EDF Header Data + + +
+
+
+ { + state.subjectID.set(value); + onUserHeaderFieldInput(name, value); + }} + placeholder={state.edfHeader.get['subject_id']} + /> +
+ Recommended EDF anonymization: "X X X X"
+ (EDF spec: patientID patientSex patientBirthdate patientName) +
+
+
+
+ +
+ (EDF spec: Startdate dd-MMM-yyyy + administrationCode investigatorCode equipmentCode) + +
+
+
+ +
+
- ) : null; }; diff --git a/src/jsx/Converter.js b/src/jsx/Converter.js index 876ee9f..0e277d8 100644 --- a/src/jsx/Converter.js +++ b/src/jsx/Converter.js @@ -5,6 +5,7 @@ import '../css/Converter.css'; // Display Loading, Success, Error import Modal from './elements/modal'; +import {TextInput} from './elements/inputs'; // Socket.io import {Event, SocketContext} from './socket.io'; @@ -15,6 +16,8 @@ import {Event, SocketContext} from './socket.io'; * @return {JSX.Element} */ const Converter = (props) => { + const [preparedBy, setPreparedBy] = useState(''); + // React Context const appContext = useContext(AppContext); const socketContext = useContext(SocketContext); @@ -32,14 +35,14 @@ const Converter = (props) => { }, message: { loading: - + BIDS creation in progress... 😴 , success: - - Success creating BIDS! + + Success creating BIDS! , error: '', }, @@ -55,18 +58,32 @@ const Converter = (props) => { }); setModalVisible(true); socketContext.emit('edf_to_bids', { - file_path: appContext.getFromTask('edfFile') ? - appContext.getFromTask('edfFile').path : '', + file_paths: appContext.getFromTask('edfFiles') ? + appContext.getFromTask('edfFiles') + .map((edfFile) => edfFile['path']) : [], modality: appContext.getFromTask('edfType') ?? 'ieeg', bids_directory: appContext.getFromTask('bidsDirectory') ?? '', read_only: false, - events_tsv: appContext.getFromTask('eventsTSV') ? - appContext.getFromTask('eventsTSV').path : '', - line_freq: appContext.getFromTask('lineFreq') ?? 'n/a', + events_tsv: appContext.getFromTask('eventsTSV').length > 0 ? + appContext.getFromTask('eventsTSV')[0]['path'] : '', + annotations_tsv: appContext.getFromTask('annotationsTSV').length > 0 ? + appContext.getFromTask('annotationsTSV')[0]['path'] : '', + annotations_json: appContext.getFromTask('annotationsJSON').length > 0 ? + appContext.getFromTask('annotationsJSON')[0]['path'] : '', site_id: appContext.getFromTask('siteID') ?? '', project_id: appContext.getFromTask('projectID') ?? '', - sub_project_id: appContext.getFromTask('subProjectID') ?? '', - visit_label: appContext.getFromTask('visitLabel') ?? '', + sub_project_id: appContext.getFromTask('subprojectID') ?? '', + session: appContext.getFromTask('session') ?? '', + participantID: appContext.getFromTask('participantID') ?? '', + age: appContext.getFromTask('participantAge') ?? '', + hand: appContext.getFromTask('participantHand') ?? '', + sex: appContext.getFromTask('participantSex') ?? '', + preparedBy: appContext.getFromTask('preparedBy') ?? '', + line_freq: appContext.getFromTask('lineFreq') || 'n/a', + software_filters: appContext.getFromTask('softwareFilters') ?? 'n/a', + recording_type: appContext.getFromTask('recordingType') ?? 'n/a', + taskName: appContext.getFromTask('taskName') ?? '', + reference: appContext.getFromTask('reference') ?? '', subject_id: appContext.getFromTask('subject_id') ?? '', }); }; @@ -81,7 +98,7 @@ const Converter = (props) => { time = time.slice(0, time.lastIndexOf('-')) + ' ' + time.slice(time.lastIndexOf('-')+1); setSuccessMessage(<> - Last created at: {time} + Last created at: {time} ); } }, [outputTime]); @@ -109,8 +126,9 @@ const Converter = (props) => { } else { setModalText((prevState) => { prevState.message['error'] = ( -
- {message['error'].map((error, i) =>

{error}

)} +
+ {message['error'].map((error, i) => + {error}
)}
); return {...prevState, ['mode']: 'error'}; @@ -118,287 +136,271 @@ const Converter = (props) => { } }; + let error = false; + /** * Renders the React component. + * + * @param {string} _ + * @param {string} value + * * @return {JSX.Element} - React markup for component. */ return props.visible ? ( <> - + EDF to BIDS format -
-
- Review your Configuration selections: -
    -
  • - {appContext.getFromTask('edfFile') ? - (<> - EDF data file:  - {appContext.getFromTask('edfFile').name} - - ) : - (<> - No EDF file selected. - ❌ - - Please correct. - - - ) - } -
  • -
  • - {appContext.getFromTask('eventsTSV') ? - (<> - Including:  - {appContext.getFromTask('eventsTSV').name} - - ) : - (<> - No events.tsv selected. - ❌ - - Please correct. - - - ) - } -
  • -
  • - {appContext.getFromTask('bidsDirectory') ? - (<> - BIDS output folder:  - {appContext.getFromTask('bidsDirectory')} - - ) : - (<> - The BIDS output directory hasn't been set in configuration. - ❌ - - Please correct. - - - ) - } -
  • -
  • - {appContext.getFromTask('lineFreq') ? - (<> - Line frequency::  - {appContext.getFromTask('lineFreq')} - - ) : - (<> - The Line frequency hasn't been set in configuration. - ❌ - - Please correct. - - - ) - } -
  • -
-
-
- Review your LORIS metadata: -
    -
  • - {appContext.getFromTask('siteID') ? - (<> - The Site:  - {appContext.getFromTask('siteID')} - - ) : - (<> - The Site hasn't been set in configuration. - ❌ - - Please correct. - - - ) - } -
  • -
  • - {appContext.getFromTask('projectID') ? - (<> - The Project:  - {appContext.getFromTask('projectID')} - - ) : - (<> - The Project hasn't been set in configuration. - ❌ - - Please correct. - - - ) - } -
  • -
  • - {appContext.getFromTask('subProjectID') ? - (<> - The SubProject:  - {appContext.getFromTask('subProjectID')} - - ) : - (<> - The SubProject hasn't been set in configuration. - ❌ - - Please correct. - - - ) - } -
  • -
  • - {appContext.getFromTask('visitLabel') ? - (<> - No Visit Label set:  - {appContext.getFromTask('visitLabel')} - - ) : - (<> - Please enter a value in the Configuration tab. - ❌ - - Please correct. - - - ) - } -
  • -
+
+
+ Review your data configuration: +
+ {(appContext.getFromTask('edfFiles') && + appContext.getFromTask('edfFiles').length > 0) ? + <> + + EDF data file(s):  + {appContext.getFromTask('edfFiles') + .map((edfFile) => edfFile['name']).join(', ') + } + : + <> + {error = true} + No EDF file selected. + + } +
+
+ {(appContext.getFromTask('eventsTSV') && + Object.keys(appContext.getFromTask('eventsTSV')) + .length > 0) ? + <> + Including: + {appContext.getFromTask('eventsTSV').name} + : + <> + + No events.tsv selected. + + } +
+
+ {appContext.getFromTask('bidsDirectory') ? + <> + + BIDS output directory: + {appContext.getFromTask('bidsDirectory')} + : + <> + {error = true} + + No BIDS output directory selected. + + } +
-
- Verify anonymization of EDF header data: - +
+
+
+ Review your participant data: +
+ {appContext.getFromTask('participantID') ? + <> + + Participant ID: {appContext.getFromTask('participantID')} + : + <> + {error = true} + <> + + Participant ID is not specified. + + + } +
+
+
+ Verify anonymization of EDF header data: +
+ {appContext.getFromTask('subject_id') ? + <> + Subject ID:  + {appContext.getFromTask('subject_id')} + : + <> + + Subject ID is not modified. + + } +
+ {appContext.getFromTask('recording_id') && +
+ Recording ID:  + {appContext.getFromTask('recording_id')} +
+ } + {appContext.getFromTask('recordingDate') && +
+ Recording Date:  + {new Intl.DateTimeFormat( + 'en-US', + { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + }, + ).format(appContext.getFromTask('recordingDate'))} +
+ } +
+ + {error ? +
+ ❌ Please correct the above errors. +
: +
+ ✔ Ready to proceed +
+ } + +
+ +
+ setPreparedBy(value)} + /> + {!preparedBy && +
+ + Please enter your name for verification tracking purposes. +
+ }
-
- - Click to convert data:  - - + {successMessage}
@@ -407,7 +409,7 @@ const Converter = (props) => { title={modalText.title[modalText.mode]} show={modalVisible} close={hideModal} - width={'500px'} + width='500px' > {modalText.message[modalText.mode]} diff --git a/src/jsx/SplashScreen.js b/src/jsx/SplashScreen.js index 31aa660..021b359 100644 --- a/src/jsx/SplashScreen.js +++ b/src/jsx/SplashScreen.js @@ -18,11 +18,11 @@ const SplashScreen = (props) => { */ return props.visible ? ( <> -

+

EEG2BIDS Wizard is loading ...

diff --git a/src/jsx/Validator.js b/src/jsx/Validator.js index d635d46..f448c61 100644 --- a/src/jsx/Validator.js +++ b/src/jsx/Validator.js @@ -32,15 +32,15 @@ const Validator = (props) => { }, message: { loading: - + BIDS compression in progress ... 😴 , success: - - Success compressing BIDS! + + Success compressing BIDS! , error: '', }, @@ -92,37 +92,33 @@ const Validator = (props) => { validator['file_paths'].forEach((value, index) => { if (validator['result'][index]) { renderFields.push( -
- {value} +
+ {value}
, ); } else { renderFields.push( -
- {value} +
+ {value}
, ); } }); renderPackageBIDS.push( -
-
+
+
Package BIDS output folder into a compressed file:  + type='button' + value='Compress BIDS'/>
, ); } setValidPaths(<> -
- Valid is green. - Invalid is red. -
-
+
{renderFields}
{renderPackageBIDS} @@ -150,17 +146,17 @@ const Validator = (props) => { */ return props.visible ? ( <> - + Validation confirmation -
-
+
+
Run BIDS Validator:  + type='button' + value='Validate BIDS'/>
{validPath} @@ -168,7 +164,7 @@ const Validator = (props) => { title={modalText.title[modalText.mode]} show={modalVisible} close={hideModal} - width={'500px'} + width='500px' > {modalText.message[modalText.mode]} diff --git a/src/jsx/Welcome.js b/src/jsx/Welcome.js index 850d41e..14f301a 100644 --- a/src/jsx/Welcome.js +++ b/src/jsx/Welcome.js @@ -8,6 +8,13 @@ import '../css/Welcome.css'; * @return {JSX.Element} */ const Welcome = (props) => { + /** + * openGitHub - Navigate browser to EEG2BIDS Wizard. + */ + const openBIDS = () => { + const myAPI = window['myAPI']; + myAPI.visitBIDS(); + }; /** * openGitHub - Navigate browser to EEG2BIDS Wizard. */ @@ -15,6 +22,13 @@ const Welcome = (props) => { const myAPI = window['myAPI']; myAPI.visitGitHub(); }; + /** + * openGitHub - Navigate browser to EEG2BIDS Wizard. + */ + const openIssues = () => { + const myAPI = window['myAPI']; + myAPI.visitIssues(); + }; /** * openMCIN - Navigate browser to MCIN. */ @@ -43,22 +57,25 @@ const Welcome = (props) => { */ return props.visible ? ( <> - + Welcome to EEG2BIDS Wizard -
-

EEG2BIDS Wizard  - is a tool for de-identification of EDF data and conversion to BIDS -  format for data sharing. +

+

+ EEG2BIDS Wizard is a tool for de-identification of EDF data + and conversion to BIDS format for data sharing.

This software is designed to run on EDF files (EEG or iEEG) for -  one subject at a time. Events and metadata such as a -  LORIS ProjectID and Visit Label can be included. + one recording from one subject at a time. Events and metadata such as + a LORIS ProjectID and Visit Label can be included.

@@ -68,8 +85,10 @@ const Welcome = (props) => { Configuration tab:

    -
  • Select the data file, events file (events.tsv), and output  - folder +
  • + Select the data file(s) (multiple files allowed for a + single recording split into multiple files), + events file (events.tsv), and output folder
  • Set metadata values
  • Anonymize the EDF header data
  • @@ -96,15 +115,16 @@ const Welcome = (props) => {
    {/**/}
    -
    - Powered by  - +
    + Powered by open source software - and  - + and MNE-BIDS .
    - Copyright © 2021 + + Contact us + with your feedback.
    + Copyright © 2021 MCIN.
    diff --git a/src/jsx/elements/inputs.js b/src/jsx/elements/inputs.js index ff45c5b..ad7bf00 100644 --- a/src/jsx/elements/inputs.js +++ b/src/jsx/elements/inputs.js @@ -13,8 +13,8 @@ export const FileInput = (props) => { */ const handleChange = (event) => { // Send current file to parent component - const file = event.target.files[0] ? event.target.files[0] : ''; - props.onUserInput(props.id, file); + const files = event.target.files ? Array.from(event.target.files) : []; + props.onUserInput(props.id, files); }; /** * Renders the React component. @@ -22,26 +22,42 @@ export const FileInput = (props) => { */ return ( <> - + - -  {props.placeholder ?? 'No file chosen'} - + /> {props.placeholder ?? 'No file chosen'} ); }; +FileInput.defaultProps = { + multiple: false, +}; FileInput.propTypes = { id: PropTypes.string, + multiple: PropTypes.bool, name: PropTypes.string, label: PropTypes.string, accept: PropTypes.string, @@ -73,7 +89,14 @@ export const DirectoryInput = (props) => { */ return ( <> - + { */ return ( <> - + {props.label && + + } { }; TextInput.defaultProps = { readonly: false, + required: false, }; TextInput.propTypes = { id: PropTypes.string, name: PropTypes.string, + required: PropTypes.bool, label: PropTypes.string, value: PropTypes.oneOfType([ PropTypes.string, @@ -167,8 +201,10 @@ export const RadioInput = (props) => { * @param {object} event - input event */ const handleChange = (event) => { - const value = event.target.value; - props.onUserInput(props.id, value); + props.onUserInput( + props.id, + event.target.value, + ); }; /** * generateRadioLayout - creates the radio input layout. @@ -221,9 +257,13 @@ export const RadioInput = (props) => {
    , ); } - return
    - + return
    + {content}
    ; }; @@ -237,15 +277,117 @@ export const RadioInput = (props) => { ); }; +RadioInput.defaultProps = { + required: false, +}; RadioInput.propTypes = { id: PropTypes.string, name: PropTypes.string, + required: PropTypes.bool, label: PropTypes.string, options: PropTypes.object, checked: PropTypes.string, onUserInput: PropTypes.func, }; +/** + * SelectInput - the select component. + * @param {object} props + * @return {JSX.Element} + */ +export const SelectInput = (props) => { + /** + * handleChange - input change by user. + * @param {object} event - input event + */ + const handleChange = (event) => { + props.onUserInput( + props.id, + event.target.value, + ); + }; + /** + * generateRadioLayout - creates the radio input layout. + * @return {JSX.Element} + */ + const generateSelectLayout = () => { + const styleRow = { + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + width: '100%', + }; + const styleColumn = { + display: 'flex', + flexDirection: 'column', + alignSelf: 'flex-start', + marginRight: '10px', + }; + const styleInput = { + display: 'inline-block', + margin: '0 10px 10px 0', + cursor: 'pointer', + }; + + let emptyOptionHTML = null; + // Add empty option + if (props.emptyOption) { + emptyOptionHTML = ; + } + + const optionList = Object.keys(props.options).map((key, index) => { + return ( + + ); + }); + + return ( + <> + {props.label && + + } + + + ); + }; + /** + * Renders the React component. + * @return {JSX.Element} - React markup for component. + */ + return ( + <> + {generateSelectLayout()} + + ); +}; +SelectInput.propTypes = { + id: PropTypes.string, + name: PropTypes.string, + label: PropTypes.string, + value: PropTypes.string, + emptyOption: PropTypes.string, + options: PropTypes.object, + onUserInput: PropTypes.func, +}; + /** * NumberInput - the input type='number' component. * @param {object} props @@ -257,8 +399,10 @@ export const NumberInput = (props) => { * @param {object} event - input event */ const handleChange = (event) => { - const value = event.target.value; - props.onUserInput(props.id, value); + props.onUserInput( + props.id, + event.target.value, + ); }; /** * Renders the React component. @@ -266,7 +410,9 @@ export const NumberInput = (props) => { */ return ( <> - + { + /** + * handleChange - input change by user. + * @param {object} event - input event + */ + const handleChange = (event) => { + props.onUserInput( + props.name, + event.target.value, + ); + }; + + /** + * Renders the React component. + * @return {JSX.Element} - React markup for the component + */ + return ( + <> + + + + ); +}; + +TextareaInput.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string, + value: PropTypes.string, + id: PropTypes.string, + required: PropTypes.bool, + rows: PropTypes.number, + cols: PropTypes.number, + onUserInput: PropTypes.func, +}; + +TextareaInput.defaultProps = { + required: false, + rows: 4, + cols: 25, }; diff --git a/src/jsx/elements/menu.js b/src/jsx/elements/menu.js index 0f6d0e7..b04df2b 100644 --- a/src/jsx/elements/menu.js +++ b/src/jsx/elements/menu.js @@ -9,7 +9,6 @@ import '../../css/Menu.css'; */ const MenuTab = (props) => { // css styling. - const menuTabWidth = {width: props.width}; const classesTitleText = props.active ? 'menu-title menu-active' : 'menu-title'; /** @@ -17,7 +16,7 @@ const MenuTab = (props) => { * @return {JSX.Element} - React markup for component. */ return ( -
    +
    {props.title} @@ -43,14 +42,13 @@ const Menu = (props) => { * @return {JSX.Element} - React markup for component. */ return props.visible ? ( -
    -
    +
    +
    { props.tabs.map((tab, index) => ( { transform: props.show ? 'translateY(0)' : 'translateY(-25%)', }; return ( -
    -
    -
    -
    - + {props.title} - × -
    +
    {props.children}
    diff --git a/wiki/macOS/README.md b/wiki/macOS/README.md index f451ba9..d7a6bf3 100644 --- a/wiki/macOS/README.md +++ b/wiki/macOS/README.md @@ -20,10 +20,10 @@ npm run start python -m venv . source bin/activate pip install -r requirements.txt -python -m python.pycat +python -m python.eeg2bids ``` -**Note:** Both the "python-service" & the "electron-app" need to be running simultaneously for pyCat to successfully function in development! +**Note:** Both the "python-service" & the "electron-app" need to be running simultaneously for EEG2BIDS Wizard to successfully function in development! ## Production