diff --git a/SQL/0000-00-05-ElectrophysiologyTables.sql b/SQL/0000-00-05-ElectrophysiologyTables.sql index 3860a11e1ad..3e08c05e2e9 100644 --- a/SQL/0000-00-05-ElectrophysiologyTables.sql +++ b/SQL/0000-00-05-ElectrophysiologyTables.sql @@ -382,105 +382,6 @@ CREATE TABLE `physiological_archive` ( ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; --- SQL tables for BIDS derivative file structure --- Create physiological_annotation_file_type table -CREATE TABLE `physiological_annotation_file_type` ( - `FileType` VARCHAR(20) NOT NULL UNIQUE, - `Description` VARCHAR(255), - PRIMARY KEY (`FileType`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- Create physiological_annotation_file table -CREATE TABLE `physiological_annotation_file` ( - `AnnotationFileID` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, - `PhysiologicalFileID` INT(10) UNSIGNED NOT NULL, - `FileType` VARCHAR(20) NOT NULL, - `FilePath` VARCHAR(255), - `LastUpdate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `LastWritten` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`AnnotationFileID`), - CONSTRAINT `FK_phys_file_ID` - FOREIGN KEY (`PhysiologicalFileID`) - REFERENCES `physiological_file` (`PhysiologicalFileID`), - CONSTRAINT `FK_annotation_file_type` - FOREIGN KEY (`FileType`) - REFERENCES `physiological_annotation_file_type` (`FileType`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- Create annotation_archive which will store archives of all the annotation files for --- Front-end download -CREATE TABLE `physiological_annotation_archive` ( - `AnnotationArchiveID` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, - `PhysiologicalFileID` INT(10) UNSIGNED NOT NULL, - `Blake2bHash` VARCHAR(128) NOT NULL, - `FilePath` VARCHAR(255) NOT NULL, - PRIMARY KEY (`AnnotationArchiveID`), - CONSTRAINT `FK_physiological_file_ID` - FOREIGN KEY (`PhysiologicalFileID`) - REFERENCES `physiological_file` (`PhysiologicalFileID`) - ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- Create annotation_parameter table --- Note: This corresponds with the JSON annotation files -CREATE TABLE `physiological_annotation_parameter` ( - `AnnotationParameterID` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, - `AnnotationFileID` INT(10) UNSIGNED NOT NULL, - `Description` TEXT DEFAULT NULL, - `Sources` VARCHAR(255), - `Author` VARCHAR(255), - PRIMARY KEY (`AnnotationParameterID`), - CONSTRAINT `FK_annotation_file_ID` - FOREIGN KEY (`AnnotationFileID`) - REFERENCES `physiological_annotation_file` (`AnnotationFileID`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- Create an annotation_label_type table -CREATE TABLE `physiological_annotation_label` ( - `AnnotationLabelID` INT(5) UNSIGNED NOT NULL AUTO_INCREMENT, - `AnnotationFileID` INT(10) UNSIGNED DEFAULT NULL, - `LabelName` VARCHAR(255) NOT NULL, - `LabelDescription` TEXT DEFAULT NULL, - PRIMARY KEY (`AnnotationLabelID`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- Create annotation_tsv table --- Note: This corresponds with the .tsv annotation files -CREATE TABLE `physiological_annotation_instance` ( - `AnnotationInstanceID` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, - `AnnotationFileID` INT(10) UNSIGNED NOT NULL, - `AnnotationParameterID` INT(10) UNSIGNED NOT NULL, - `Onset` DECIMAL(10, 4), - `Duration` DECIMAL(10, 4) DEFAULT 0, - `AnnotationLabelID` INT(5) UNSIGNED NOT NULL, - `Channels` TEXT, - `AbsoluteTime` TIMESTAMP, - `Description` VARCHAR(255), - PRIMARY KEY (`AnnotationInstanceID`), - CONSTRAINT `FK_annotation_parameter_ID` - FOREIGN KEY (`AnnotationParameterID`) - REFERENCES `physiological_annotation_parameter` (`AnnotationParameterID`), - CONSTRAINT `FK_annotation_file` - FOREIGN KEY (`AnnotationFileID`) - REFERENCES `physiological_annotation_file` (`AnnotationFileID`), - CONSTRAINT `FK_annotation_label_ID` - FOREIGN KEY (`AnnotationLabelID`) - REFERENCES `physiological_annotation_label` (`AnnotationLabelID`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- Create physiological_annotation_rel table -CREATE TABLE `physiological_annotation_rel` ( - `AnnotationTSV` INT(10) UNSIGNED NOT NULL, - `AnnotationJSON` INT(10) UNSIGNED NOT NULL, - PRIMARY KEY (`AnnotationTSV`, `AnnotationJSON`), - CONSTRAINT `FK_AnnotationTSV` - FOREIGN KEY (`AnnotationTSV`) - REFERENCES `physiological_annotation_file` (`AnnotationFileID`), - CONSTRAINT `FK_AnnotationJSON` - FOREIGN KEY (`AnnotationJSON`) - REFERENCES `physiological_annotation_file` (`AnnotationFileID`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -- Create EEG upload table CREATE TABLE `electrophysiology_uploader` ( `UploadID` int(10) unsigned NOT NULL AUTO_INCREMENT, @@ -634,39 +535,3 @@ INSERT INTO ImagingFileTypes ('edf', 'European data format (EEG)'), ('cnt', 'Neuroscan CNT data format (EEG)'), ('archive', 'Archive file'); - --- Insert into annotation_file_type -INSERT INTO physiological_annotation_file_type - (FileType, Description) - VALUES - ('tsv', 'TSV File Type, contains information about each annotation'), - ('json', 'JSON File Type, metadata for annotations'); - --- Insert into annotation_label_type -INSERT INTO physiological_annotation_label - (AnnotationLabelID, LabelName, LabelDescription) - VALUES - (1, 'artifact', 'artifactual data'), - (2, 'motion', 'motion related artifact'), - (3, 'flux_jump', 'artifactual data due to flux jump'), - (4, 'line_noise', 'artifactual data due to line noise (e.g., 50Hz)'), - (5, 'muscle', 'artifactual data due to muscle activity'), - (6, 'epilepsy_interictal', 'period deemed interictal'), - (7, 'epilepsy_preictal', 'onset of preictal state prior to onset of epilepsy'), - (8, 'epilepsy_seizure', 'onset of epilepsy'), - (9, 'epilepsy_postictal', 'postictal seizure period'), - (10, 'epileptiform', 'unspecified epileptiform activity'), - (11, 'epileptiform_single', 'a single epileptiform graphoelement (including possible slow wave)'), - (12, 'epileptiform_run', 'a run of one or more epileptiform graphoelements'), - (13, 'eye_blink', 'Eye blink'), - (14, 'eye_movement', 'Smooth Pursuit / Saccadic eye movement'), - (15, 'eye_fixation', 'Fixation onset'), - (16, 'sleep_N1', 'sleep stage N1'), - (17, 'sleep_N2', 'sleep stage N2'), - (18, 'sleep_N3', 'sleep stage N3'), - (19, 'sleep_REM', 'REM sleep'), - (20, 'sleep_wake', 'sleep stage awake'), - (21, 'sleep_spindle', 'sleep spindle'), - (22, 'sleep_k-complex', 'sleep K-complex'), - (23, 'scorelabeled', 'a global label indicating that the EEG has been annotated with SCORE.'); - diff --git a/SQL/New_patches/2024-01-29-Physiological-Events-Replace-Annotations.sql b/SQL/New_patches/2024-01-29-Physiological-Events-Replace-Annotations.sql new file mode 100644 index 00000000000..3ca77e98491 --- /dev/null +++ b/SQL/New_patches/2024-01-29-Physiological-Events-Replace-Annotations.sql @@ -0,0 +1,17 @@ +-- Dropping all tables regarding annotations +DROP TABLE physiological_annotation_archive; +DROP TABLE physiological_annotation_rel; +DROP TABLE physiological_annotation_instance; +DROP TABLE physiological_annotation_parameter; +DROP TABLE physiological_annotation_label; +DROP TABLE physiological_annotation_file; +DROP TABLE physiological_annotation_file_type; + +-- Event files are always associated to Projects, sometimes exclusively (dataset-scope events.json files) +-- Add ProjectID and make PhysiologicalFileID DEFAULT NULL (ProjectID should ideally not be NULLable) +ALTER TABLE `physiological_event_file` + CHANGE `PhysiologicalFileID` `PhysiologicalFileID` int(10) unsigned DEFAULT NULL, + ADD COLUMN `ProjectID` int(10) unsigned DEFAULT NULL AFTER `PhysiologicalFileID`, + ADD KEY `FK_physiological_event_file_project_id` (`ProjectID`), + ADD CONSTRAINT `FK_physiological_event_file_project_id` + FOREIGN KEY (`ProjectID`) REFERENCES `Project` (`ProjectID`); diff --git a/modules/electrophysiology_browser/README.md b/modules/electrophysiology_browser/README.md index 25bb1398b44..c1c7d89a618 100644 --- a/modules/electrophysiology_browser/README.md +++ b/modules/electrophysiology_browser/README.md @@ -4,14 +4,14 @@ The Electrophysiology Browser is intended to allow users to view candidate electrophysiology (EEG, MEG...) sessions collected for a study and any associated -annotations for each recording. +events for each recording. ## Intended Users The primary types of users are: 1. Electrophysiology researchers who want to know details about the inserted datasets. 2. Site coordinators or researchers ensuring the uploaded electrophysiology data have -been correctly inserted into LORIS. + been correctly inserted into LORIS. ## Scope @@ -26,22 +26,22 @@ sufficient to provide access to view data in the module. The third permission pr permissions to add or modify annotations for data from the sites the user has access to in this module. electrophysiology_browser_view_allsites - - This permission gives the user access to all electrophysiology datasets present in the database. + - This permission gives the user access to all electrophysiology datasets present in the database. electrophysiology_browser_view_site - - This permission gives the user access to electrophysiology datasets from their own site(s) only. + - This permission gives the user access to electrophysiology datasets from their own site(s) only. electrophysiology_browser_edit_annotations - - This permission allows the user to add, edit, and delete annotations for raw or derived datasets + - This permission allows the user to add, edit, and delete annotations for raw or derived datasets ## Download You can download all the files related to a recording (channel information, -electrode information, task event information, the actual recording) -- as well as its annotations and their related metadata. +electrode information, task event information, the actual recording) -- as well as its events and their related metadata. ## Updating Derivative Files -New annotations or edits to existing annotations made through the browser must also be updated in the derivative files stored in the filesystem, before a user tries to download a derivative file package. To do this automatically, a script is provided under `tools/update_annotation_files.php`, and a cron job should be set up to execute it regularly, e.g. every evening. +New events or edits to existing events made through the browser must also be updated in the derivative files stored in the filesystem, before a user tries to download a derivative file package. To do this automatically, a script is provided under `tools/update_event_files.php`, and a cron job should be set up to execute it regularly, e.g. every evening. ## Installation requirements to use the visualization features diff --git a/modules/electrophysiology_browser/css/electrophysiology_browser.css b/modules/electrophysiology_browser/css/electrophysiology_browser.css index b5516024df9..8a4300a663e 100644 --- a/modules/electrophysiology_browser/css/electrophysiology_browser.css +++ b/modules/electrophysiology_browser/css/electrophysiology_browser.css @@ -3,7 +3,7 @@ } .react-series-data-viewer-scoped .dropdown-menu li { - margin-top: 0; + margin: 0; padding: 0 10px; } @@ -14,6 +14,38 @@ width: 100%; } +.checkbox-flex-label > div > input[type="checkbox"] { + vertical-align: top; +} + +.checkbox-flex-label { + display: flex; + align-items: center; + margin-bottom: 0; + justify-content: flex-end; +} + +.btn-dropdown-toggle { + padding: 5px 10%; +} + +.col-xs-12 > .btn-dropdown-toggle { + padding: 5px; + max-width: fit-content; +} + +.col-xs-12 > .dropdown-menu { + width: max-content; + line-height: 14px; + padding: 0 +} + +.col-xs-12 > .dropdown-menu li { + margin: 0; + padding: 0; +} + + .btn.btn-xs { font-size: 12px; } @@ -46,42 +78,51 @@ svg:not(:root) { overflow: clip; } -.list-group-item { +.annotation.list-group-item { position: relative; display: flex; flex-direction: column; justify-content: space-between; align-items: center; + padding: 0; + width: 100%; } .annotation { background: #fffae6; - border-left: 5px solid #ff6600; + border-left: 5px solid #8eecfa; } .epoch-details { - padding-right: 100px; + display: flex; + width: 100%; + padding: 10px 0; } .epoch-action { display: flex; flex-direction: row; - justify-content: center; + justify-content: end; align-items: center; - position: absolute; - right: 10px; } .epoch-tag { - padding: 5px; + padding: 10px; background: #e7e4e4; - border-left: 5px solid #797878; + word-wrap: break-word; width: 100%; } -.epoch-tag p { - word-wrap: break-word; - width: 95%; +.line-height-14 { + line-height: 14px; +} + +.margin-top-10 { + margin-top: 10px; +} + +.flex-basis-45 { + flex-basis: 45% } .event-list .btn.btn-primary { @@ -111,8 +152,9 @@ svg:not(:root) { .btn-zoom { margin: 0 auto 3px auto; - width: 50px; + width: 55px; text-align: center; + text-wrap: unset; } .col-xs-title { @@ -139,7 +181,7 @@ svg:not(:root) { .electrode:hover circle { stroke: #064785; cursor: pointer; - fill: #E4EBF2 + fill: #E4EBF2; } .electrode:hover text { @@ -181,6 +223,10 @@ svg:not(:root) { width: auto; } +.cursor-default { + cursor: default; +} + /* Custom, iPhone Retina */ @media only screen and (min-width : 320px) { .pagination-nav { diff --git a/modules/electrophysiology_browser/help/sessions.md b/modules/electrophysiology_browser/help/sessions.md index 9a02d6dca8d..7aa591e5f2d 100644 --- a/modules/electrophysiology_browser/help/sessions.md +++ b/modules/electrophysiology_browser/help/sessions.md @@ -10,5 +10,5 @@ Files can be downloaded containing only the recording signal, the events, or oth - EEG: the file containing the session recording data. - Electrode info (tsv): contains electrode locations. - Channels info (tsv): channel status and filter settings. -- Events (tsv): events (both stimuli and responses) recorded during the session. -- Annotations (tsv): annotations (both stimuli and responses) recorded during the session. +- Events (tsv): events (both stimuli and responses) recorded during the session. + diff --git a/modules/electrophysiology_browser/jsx/components/DownloadPanel.js b/modules/electrophysiology_browser/jsx/components/DownloadPanel.js index 027395c1c0b..a254e1c8cbf 100644 --- a/modules/electrophysiology_browser/jsx/components/DownloadPanel.js +++ b/modules/electrophysiology_browser/jsx/components/DownloadPanel.js @@ -21,7 +21,7 @@ class DownloadPanel extends Component { downloads: this.props.downloads, physioFileID: this.props.physioFileID, annotationsAction: loris.BaseURL - + '/electrophysiology_browser/annotations', + + '/electrophysiology_browser/events', outputType: this.props.outputType, }; } @@ -54,27 +54,29 @@ class DownloadPanel extends Component { maxWidth: '250px', margin: '0 auto', } - }> + }> {Object.entries(panel.links).map(([type, download], j) => { const disabled = (download.file === ''); - return ( -
+ // Ignore physiological_coord_system_file + return type !== 'physiological_coord_system_file' + ? (
{download.label}
- {disabled - ? +
{download.label}
+ {disabled + ?
Not Available - : diff --git a/modules/electrophysiology_browser/jsx/electrophysiologySessionView.js b/modules/electrophysiology_browser/jsx/electrophysiologySessionView.js index b7a6bb37cb3..7078bb97543 100644 --- a/modules/electrophysiology_browser/jsx/electrophysiologySessionView.js +++ b/modules/electrophysiology_browser/jsx/electrophysiologySessionView.js @@ -139,10 +139,6 @@ class ElectrophysiologySessionView extends Component { type: 'physiological_task_event_file', file: '', }, - { - type: 'physiological_annotation_files', - file: '', - }, { type: 'all_files', file: '', @@ -152,8 +148,8 @@ class ElectrophysiologySessionView extends Component { chunksURL: null, epochsURL: null, electrodesURL: null, + coordSystemURL: null, events: null, - annotations: null, splitData: null, }, ], @@ -200,68 +196,73 @@ class ElectrophysiologySessionView extends Component { throw Error(resp.statusText); } return resp.json(); - }) - .then((data) => { - const database = data.database.map((dbEntry) => ({ - ...dbEntry, - // EEG Visualization urls - chunksURLs: - dbEntry - && dbEntry.file.chunks_urls.map( - (url) => - loris.BaseURL - + '/electrophysiology_browser/file_reader/?file=' - + url - ), - epochsURL: - dbEntry - && dbEntry.file?.epochsURL - && [loris.BaseURL + }).then((data) => { + const database = data.database.map((dbEntry) => ({ + ...dbEntry, + // EEG Visualization urls + chunksURLs: + dbEntry + && dbEntry.file.chunks_urls.map( + (url) => + loris.BaseURL + + '/electrophysiology_browser/file_reader/?file=' + + url + ), + epochsURL: + dbEntry + && dbEntry.file?.epochsURL + && [loris.BaseURL + + '/electrophysiology_browser/file_reader/?file=' + + dbEntry.file.epochsURL], + electrodesURL: + dbEntry + && dbEntry.file.downloads.map( + (group) => + group.links['physiological_electrode_file']?.file + && loris.BaseURL + '/electrophysiology_browser/file_reader/?file=' - + dbEntry.file.epochsURL], - electrodesURL: - dbEntry - && dbEntry.file.downloads.map( - (group) => - group.links['physiological_electrode_file']?.file - && loris.BaseURL - + '/electrophysiology_browser/file_reader/?file=' - + group.links['physiological_electrode_file'].file - ), - events: - dbEntry - && dbEntry.file.events, - annotations: - dbEntry - && dbEntry.file.annotations, - })); + + group.links['physiological_electrode_file'].file + ), + coordSystemURL: + dbEntry + && dbEntry.file.downloads.map( + (group) => + group.links['physiological_coord_system_file']?.file + && loris.BaseURL + + '/electrophysiology_browser/file_reader/?file=' + + group.links['physiological_coord_system_file'].file + ), + events: + dbEntry + && dbEntry.file.events, + })); - this.setState({ - setup: {data}, - isLoaded: true, - database: database, - patient: { - info: data.patient, - }, - }); + this.setState({ + setup: {data}, + isLoaded: true, + database: database, + patient: { + info: data.patient, + }, + }); - document.getElementById( - 'nav_next' - ).href = dataURL + data.nextSession + outputTypeArg; - document.getElementById( - 'nav_previous' - ).href = dataURL + data.prevSession + outputTypeArg; - if (data.prevSession !== '') { - document.getElementById('nav_previous').style.display = 'block'; - } - if (data.nextSession !== '') { - document.getElementById('nav_next').style.display = 'block'; - } - }) - .catch((error) => { - this.setState({error: true}); - console.error(error); - }); + document.getElementById( + 'nav_next' + ).href = dataURL + data.nextSession + outputTypeArg; + document.getElementById( + 'nav_previous' + ).href = dataURL + data.prevSession + outputTypeArg; + if (data.prevSession !== '') { + document.getElementById('nav_previous').style.display = 'block'; + } + if (data.nextSession !== '') { + document.getElementById('nav_next').style.display = 'block'; + } + }) + .catch((error) => { + this.setState({error: true}); + console.error(error); + }); } /** @@ -333,8 +334,8 @@ class ElectrophysiologySessionView extends Component { chunksURLs, epochsURL, events, - annotations, electrodesURL, + coordSystemURL, } = this.state.database[i]; const file = this.state.database[i].file; const splitPagination = []; @@ -365,8 +366,8 @@ class ElectrophysiologySessionView extends Component { } epochsURL={epochsURL} events={events} - annotations={annotations} electrodesURL={electrodesURL} + coordSystemURL={coordSystemURL} physioFileID={this.state.database[i].file.id} >
diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/eeglab/EEGLabSeriesProvider.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/eeglab/EEGLabSeriesProvider.tsx index 0771738c896..b8acb2e6c70 100644 --- a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/eeglab/EEGLabSeriesProvider.tsx +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/eeglab/EEGLabSeriesProvider.tsx @@ -18,8 +18,11 @@ import { setFilteredEpochs, } from '../series/store/state/dataset'; import {setDomain, setInterval} from '../series/store/state/bounds'; -import {setElectrodes} from '../series/store/state/montage'; -import {AnnotationMetadata, EventMetadata} from '../series/store/types'; +import { + setCoordinateSystem, + setElectrodes, +} from '../series/store/state/montage'; +import {EventMetadata} from '../series/store/types'; declare global { interface Window { @@ -30,8 +33,8 @@ declare global { type CProps = { chunksURL: string, electrodesURL: string, + coordSystemURL: string, events: EventMetadata, - annotations: AnnotationMetadata, physioFileID: number, limit: number, children: React.ReactNode, @@ -63,8 +66,8 @@ class EEGLabSeriesProvider extends Component { const { chunksURL, electrodesURL, + coordSystemURL, events, - annotations, physioFileID, limit, } = props; @@ -114,62 +117,68 @@ class EEGLabSeriesProvider extends Component { }) ); this.store.dispatch(setChannels(emptyChannels( - Math.min(this.props.limit, channelMetadata.length), - 1 + Math.min(this.props.limit, channelMetadata.length), + 1 ))); this.store.dispatch(setDomain(timeInterval)); this.store.dispatch(setInterval(DEFAULT_TIME_INTERVAL)); } }).then(() => { - return events.instances.map((instance) => { - const onset = parseFloat(instance.Onset); - const duration = parseFloat(instance.Duration); - const label = instance.TrialType && instance.TrialType !== 'n/a' ? - instance.TrialType : instance.EventValue; - const hed = instance.AssembledHED; - return { - onset: onset, - duration: duration, - type: 'Event', - label: label, - comment: null, - hed: hed, - channels: 'all', - annotationInstanceID: null, - }; - }); - }).then((events) => { - const epochs = events; - annotations.instances.map((instance) => { - const label = annotations.labels - .find((label) => - label.AnnotationLabelID == instance.AnnotationLabelID - ).LabelName; - epochs.push({ - onset: parseFloat(instance.Onset), - duration: parseFloat(instance.Duration), - type: 'Annotation', - label: label, - comment: instance.Description, - hed: null, - channels: 'all', - annotationInstanceID: instance.AnnotationInstanceID, + const epochs = []; + events.instances.map((instance) => { + const epochIndex = + epochs.findIndex((e) => + e.physiologicalTaskEventID === + instance.PhysiologicalTaskEventID + ); + + const extraColumns = Array.from( + events.extraColumns + ).filter((column) => { + return column.PhysiologicalTaskEventID === + instance.PhysiologicalTaskEventID; }); - }); + if (epochIndex === -1) { + const epochLabel = [null, 'n/a'].includes(instance.TrialType) + ? null + : instance.TrialType; + epochs.push({ + onset: parseFloat(instance.Onset), + duration: parseFloat(instance.Duration), + type: 'Event', + label: epochLabel ?? instance.EventValue, + value: instance.EventValue, + trialType: instance.TrialType, + properties: extraColumns, + hed: null, + channels: 'all', + physiologicalTaskEventID: instance.PhysiologicalTaskEventID, + }); + } else { + console.error('ERROR: EPOCH EXISTS'); + } + }); return epochs; - }).then((epochs) => { - this.store.dispatch( - setEpochs( - epochs - .flat() - .sort(function(a, b) { - return a.onset - b.onset; - }) - ) - ); - this.store.dispatch(setFilteredEpochs(epochs.map((_, index) => index))); - }) - ; + }).then((epochs) => { + const sortedEpochs = epochs + .flat() + .sort(function(a, b) { + return a.onset - b.onset; + }); + + const timeInterval = this.store.getState().dataset.timeInterval; + this.store.dispatch(setEpochs(sortedEpochs)); + this.store.dispatch(setFilteredEpochs({ + plotVisibility: sortedEpochs.reduce((indices, epoch, index) => { + if (!(epoch.onset < 1 && epoch.duration >= timeInterval[1])) { + // Full-recording events not visible by default + indices.push(index); + } + return indices; + }, []), + columnVisibility: [], + })); + }); Promise.race(racers(fetchText, electrodesURL)) .then((text) => { @@ -188,6 +197,27 @@ class EEGLabSeriesProvider extends Component { .catch((error) => { console.error(error); }); + + Promise.race(racers(fetchJSON, coordSystemURL)) + .then( ({json, _}) => { + if (json) { + const { + EEGCoordinateSystem, + EEGCoordinateUnits, + EEGCoordinateSystemDescription, + } = json; + this.store.dispatch( + setCoordinateSystem({ + name: EEGCoordinateSystem ?? 'Other', + units: EEGCoordinateUnits ?? 'm', + description: EEGCoordinateSystemDescription ?? 'n/a', + }) + ); + } + }) + .catch((error) => { + console.error(error); + }); } /** diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/AnnotationForm.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/AnnotationForm.tsx index c07c0b12b25..377f06f5160 100644 --- a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/AnnotationForm.tsx +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/AnnotationForm.tsx @@ -1,6 +1,5 @@ import React, {useEffect, useState} from 'react'; import { - AnnotationMetadata, Epoch as EpochType, RightPanel, } from '../store/types'; @@ -12,7 +11,7 @@ import {toggleEpoch, updateActiveEpoch} from '../store/logic/filterEpochs'; import {RootState} from '../store'; import {setEpochs} from '../store/state/dataset'; import {setCurrentAnnotation} from '../store/state/currentAnnotation'; -import {NumericElement, SelectElement, TextareaElement} from './Form'; +import {NumericElement, SelectElement, TextboxElement} from './Form'; import swal from 'sweetalert2'; type CProps = { @@ -25,11 +24,9 @@ type CProps = { currentAnnotation: EpochType, setCurrentAnnotation: (_: EpochType) => void, physioFileID: number, - annotationMetadata: AnnotationMetadata, toggleEpoch: (_: number) => void, updateActiveEpoch: (_: number) => void, interval: [number, number], - domain: [number, number], }; /** @@ -47,7 +44,6 @@ type CProps = { * @param root0.toggleEpoch, * @param root0.updateActiveEpoch, * @param root0.interval - * @param root0.domain * @param root0.toggleEpoch * @param root0.updateActiveEpoch */ @@ -60,11 +56,9 @@ const AnnotationForm = ({ currentAnnotation, setCurrentAnnotation, physioFileID, - annotationMetadata, toggleEpoch, updateActiveEpoch, interval, - domain, }: CProps) => { const [startEvent = '', endEvent = ''] = timeSelection || []; const [event, setEvent] = useState<(number | string)[]>( @@ -78,11 +72,7 @@ const AnnotationForm = ({ currentAnnotation.label : null ); - const [comment, setComment] = useState( - currentAnnotation ? - currentAnnotation.comment : - '' - ); + const [isSubmitted, setIsSubmitted] = useState(false); const [isDeleted, setIsDeleted] = useState(false); const [annoMessage, setAnnoMessage] = useState(''); @@ -101,8 +91,6 @@ const AnnotationForm = ({ (event[0] || event[0] === 0) && (event[1] || event[1] === 0) && event[0] <= event[1] - && event[0] >= interval[0] && event[0] <= interval[1] - && event[1] >= interval[0] && event[1] <= interval[1] ); /** @@ -161,14 +149,7 @@ const AnnotationForm = ({ const handleLabelChange = (name, value) => { setLabel(value); }; - /** - * - * @param name - * @param value - */ - const handleCommentChange = (name, value) => { - setComment(value); - }; + /** * */ @@ -180,18 +161,14 @@ const AnnotationForm = ({ * */ const handleReset = () => { - // Clear all fields - setEvent(['', '']); - setTimeSelection([null, null]); - setLabel(''); - setComment(''); + // TODO: Clear all fields }; /** * */ const handleDelete = () => { - setIsDeleted(true); + // Not supported }; // Submit @@ -213,7 +190,7 @@ const AnnotationForm = ({ } const url = window.location.origin + - '/electrophysiology_browser/annotations/'; + '/electrophysiology_browser/events/'; // get duration of annotation let startTime = event[0]; @@ -229,9 +206,10 @@ const AnnotationForm = ({ // set body // instance_id = null for new annotations const body = { + request_type: 'event_update', physioFileID: physioFileID, instance_id: currentAnnotation ? - currentAnnotation.annotationInstanceID : + currentAnnotation.physiologicalTaskEventID : null, instance: { onset: startTime, @@ -239,22 +217,9 @@ const AnnotationForm = ({ label_name: label, label_description: label, channels: 'all', - description: comment, }, }; - const newAnnotation : EpochType = { - onset: startTime, - duration: duration, - type: 'Annotation', - label: label, - comment: comment, - channels: 'all', - annotationInstanceID: currentAnnotation ? - currentAnnotation.annotationInstanceID : - null, - }; - fetch(url, { method: 'POST', credentials: 'same-origin', @@ -263,15 +228,31 @@ const AnnotationForm = ({ if (response.ok) { return response.json(); } - }).then((data) => { + }).then((response) => { setIsSubmitted(false); // if in edit mode, remove old annotation instance if (currentAnnotation !== null) { epochs.splice(epochs.indexOf(currentAnnotation), 1); - } else { - newAnnotation.annotationInstanceID = parseInt(data.instance_id); } + + const data = response.instance; + + const epochLabel = [null, 'n/a'].includes(data.instance.TrialType) + ? null + : data.instance.TrialType; + const newAnnotation : EpochType = { + onset: parseFloat(data.instance.Onset), + duration: parseFloat(data.instance.Duration), + type: 'Event', + label: epochLabel ?? data.instance.EventValue, + value: data.instance.EventValue, + trialType: data.instance.TrialType, + properties: data.extraColumns, + channels: 'all', + physiologicalTaskEventID: data.instance.PhysiologicalTaskEventID, + }; + epochs.push(newAnnotation); setEpochs( epochs @@ -285,25 +266,27 @@ const AnnotationForm = ({ // Display success message setAnnoMessage(currentAnnotation ? - 'Annotation Updated!' : - 'Annotation Added!'); + 'Event Updated!' : + 'Event Added!'); setTimeout(() => { setAnnoMessage(''); // Empty string will cause success div to hide - - // If in edit mode, switch back to annotation panel - if (currentAnnotation !== null) { - setCurrentAnnotation(null); - setRightPanel('annotationList'); - } - }, 3000); + }, 2000); }).catch((error) => { console.error(error); // Display error message - swal.fire( - 'Error', - 'Something went wrong!', - 'error' - ); + if (error.status === 401) { + swal.fire( + 'Unauthorized', + 'This action is not permitted.', + 'error' + ); + } else { + swal.fire( + 'Error', + 'Something went wrong!', + 'error' + ); + } }); }, [isSubmitted]); @@ -311,11 +294,11 @@ const AnnotationForm = ({ useEffect(() => { if (isDeleted) { const url = window.location.origin - + '/electrophysiology_browser/annotations/'; + + '/electrophysiology_browser/events/'; const body = { physioFileID: physioFileID, instance_id: currentAnnotation ? - currentAnnotation.annotationInstanceID : + currentAnnotation.physiologicalTaskEventID : null, }; @@ -352,14 +335,14 @@ const AnnotationForm = ({ // Display success message swal.fire( 'Success', - 'Annotation Deleted!', + 'Event Deleted!', 'success' ); // If in edit mode, switch back to annotation panel if (currentAnnotation !== null) { setCurrentAnnotation(null); - setRightPanel('annotationList'); + setRightPanel('eventList'); } } }).catch((error) => { @@ -378,14 +361,6 @@ const AnnotationForm = ({ } }, [isDeleted]); - let labelOptions = {}; - annotationMetadata.labels.map((label) => { - labelOptions = { - ...labelOptions, - [label.LabelName]: label.LabelName, - }; - }); - return (
- {currentAnnotation ? 'Edit' : 'Add'} Annotation + {currentAnnotation ? 'Edit' : 'Add'} Event { - setRightPanel('annotationList'); + setRightPanel('eventList'); setCurrentAnnotation(null); setTimeSelection(null); + updateActiveEpoch(null); }} >
+ Event Name + + { + currentAnnotation.label === currentAnnotation.trialType + ? 'trial_type' + : 'value' + } + + + } + value={currentAnnotation ? currentAnnotation.label : ""} + required={true} + readonly={true} + /> - - +
+ { + currentAnnotation && currentAnnotation.properties.length > 0 && ( + <> + +
+ { + currentAnnotation.properties.map((property) => { + return ( + + ); + }) + } +
+ + ) + } +
+ - {currentAnnotation && - - } {annoMessage && (
({ + physioFileID: state.dataset.physioFileID, timeSelection: state.timeSelection, epochs: state.dataset.epochs, - filteredEpochs: state.dataset.filteredEpochs, + filteredEpochs: state.dataset.filteredEpochs.plotVisibility, currentAnnotation: state.currentAnnotation, interval: state.bounds.interval, domain: state.bounds.domain, diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/EventManager.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/EventManager.tsx index 09f33ff29b8..de51f107835 100644 --- a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/EventManager.tsx +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/EventManager.tsx @@ -6,7 +6,7 @@ import { toggleEpoch, updateActiveEpoch, } from '../store/logic/filterEpochs'; -import {Epoch as EpochType, RightPanel} from '../store/types'; +import {Epoch as EpochType, EpochFilter, RightPanel} from '../store/types'; import {connect} from 'react-redux'; import {setTimeSelection} from '../store/state/timeSelection'; import {setRightPanel} from '../store/state/rightPanel'; @@ -17,14 +17,14 @@ import {setFilteredEpochs} from '../store/state/dataset'; type CProps = { timeSelection?: [number, number], epochs: EpochType[], - filteredEpochs: number[], + filteredEpochs: EpochFilter, rightPanel: RightPanel, setCurrentAnnotation: (_: EpochType) => void, setTimeSelection: (_: [number, number]) => void, setRightPanel: (_: RightPanel) => void, toggleEpoch: (_: number) => void, updateActiveEpoch: (_: number) => void, - setFilteredEpochs: (_: number[]) => void, + setFilteredEpochs: (_: EpochFilter) => void, interval: [number, number], viewerHeight: number, }; @@ -57,59 +57,58 @@ const EventManager = ({ interval, viewerHeight, }: CProps) => { - const [epochType, setEpochType] = useState((rightPanel - && rightPanel !== 'annotationForm' - && rightPanel === 'eventList') ? - 'Event' : 'Annotation'); - - const [activeEpochs, setActiveEpochs] = useState([]); - const [epochsInRange, setEpochsInRange] = useState( - getEpochsInRange(epochs, interval, epochType) - ); + const [epochsInRange, setEpochsInRange] = useState(getEpochsInRange(epochs, interval)); const [allEpochsVisible, setAllEpochsVisibility] = useState(() => { if (epochsInRange.length < MAX_RENDERED_EPOCHS) { return epochsInRange.some((index) => { - return !filteredEpochs.includes(index); - }); + return !filteredEpochs.plotVisibility.includes(index); + }) } return true; }); - const [visibleComments, setVisibleComments] = useState([]); const [allCommentsVisible, setAllCommentsVisible] = useState(false); - const totalEpochs = epochs.filter( - (epoch) => epoch.type === epochType - ).length; // Update window visibility state useEffect(() => { - setEpochsInRange(getEpochsInRange(epochs, interval, epochType)); - if (epochsInRange.length < MAX_RENDERED_EPOCHS) { - setAllEpochsVisibility(!epochsInRange.some((index) => { - return !filteredEpochs.includes(index); - })); // If one or more event isn't visible, set to be able to reveal all + const updatedEpochs = getEpochsInRange(epochs, interval); + + if (updatedEpochs.length > 0 && updatedEpochs.length < MAX_RENDERED_EPOCHS) { + setAllEpochsVisibility(!updatedEpochs.some((index) => { + return !filteredEpochs.plotVisibility.includes(index); + })); // If one or more event isn't visible, set to be able to reveal all } else { setAllEpochsVisibility(false); } - }, [filteredEpochs, interval]); - useEffect(() => { - // Toggle comment section if in range and has a comment / tag - if (!allCommentsVisible) { - setVisibleComments([]); + if (updatedEpochs.length > 0) { + setAllCommentsVisible(!updatedEpochs.some((epochIndex) => { + return epochs[epochIndex].properties.length > 0 + && !filteredEpochs.columnVisibility.includes(epochIndex); + })); } else { - - const commentIndexes = getEpochsInRange(epochs, interval, epochType, true) - .map((index) => index); - setVisibleComments([...commentIndexes]); + setAllCommentsVisible(false); } - }, [allCommentsVisible]); - useEffect(() => { - setEpochType((rightPanel - && rightPanel !== 'annotationForm' - && rightPanel === 'eventList') ? - 'Event' : 'Annotation'); - }, [rightPanel]); + setEpochsInRange(updatedEpochs); + }, [filteredEpochs, interval]); + + + const setCommentsInRangeVisibility = (visible) => { + let commentIndices = [...filteredEpochs.columnVisibility]; + epochsInRange.forEach((epochIndex) => { + if (epochs[epochIndex].properties.length > 0) { + if (visible && !filteredEpochs.columnVisibility.includes(epochIndex)) { + commentIndices.push(epochIndex); + } else if (!visible && filteredEpochs.columnVisibility.includes(epochIndex)) { + commentIndices = commentIndices.filter((value) => value !== epochIndex); + } + } + }); + setFilteredEpochs({ + plotVisibility: filteredEpochs.plotVisibility, + columnVisibility: commentIndices + }); + } /** * @@ -117,17 +116,17 @@ const EventManager = ({ */ const setEpochsInViewVisibility = (visible) => { if (epochsInRange.length < MAX_RENDERED_EPOCHS) { - epochsInRange.map((index) => { - if ((visible && !filteredEpochs.includes(index)) - || (!visible && filteredEpochs.includes(index))) { - toggleEpoch(index); + epochsInRange.forEach((epochIndex) => { + if ((visible && !filteredEpochs.plotVisibility.includes(epochIndex)) + || (!visible && filteredEpochs.plotVisibility.includes(epochIndex))) { + toggleEpoch(epochIndex); } }); } - }; + } const visibleEpochsInRange = epochsInRange.filter( - (epochIndex) => filteredEpochs.includes(epochIndex) + (epochIndex) => filteredEpochs.plotVisibility.includes(epochIndex) ); return ( @@ -142,13 +141,20 @@ const EventManager = ({ >

- {`${epochType}s (${visibleEpochsInRange.length}/${epochsInRange.length})`} + {`Events (${visibleEpochsInRange.length}/${epochsInRange.length})`} -
in timeline view [Total: {totalEpochs}] +
in timeline view [Total: {epochs.length}]

+ setCommentsInRangeVisibility(!allCommentsVisible)} + > setEpochsInViewVisibility(!allEpochsVisible)} - - > - setAllCommentsVisible(!allCommentsVisible)} > } - {epochsInRange.map((index) => { - const epoch = epochs[index]; - const visible = filteredEpochs.includes(index); + {epochsInRange.map((epochIndex) => { + const epoch = epochs[epochIndex]; + const epochVisible = filteredEpochs.plotVisibility.includes(epochIndex); /** * */ const handleCommentVisibilityChange = () => { - if (!visibleComments.includes(index)) { - setVisibleComments([ - ...visibleComments, - index, - ]); - } else { - setVisibleComments(visibleComments.filter( - (value) => value !== index - )); - } + setFilteredEpochs({ + plotVisibility: filteredEpochs.plotVisibility, + columnVisibility: [ + ...filteredEpochs.columnVisibility, + epochIndex, + ] + }); }; /** @@ -231,64 +228,73 @@ const EventManager = ({ return (
updateActiveEpoch(epochIndex)} + onMouseLeave={() => updateActiveEpoch(null)} >
- {epoch.label}
- {Math.round(epoch.onset * 1000) / 1000} - {epoch.duration > 0 - && ' - ' - + (Math.round((epoch.onset + epoch.duration) * 1000) / 1000) - } -
-
- {epoch.type === 'Annotation' && +
+ {epoch.label} +
+ {Math.round(epoch.onset * 1000) / 1000} + {epoch.duration > 0 + && ' - ' + + (Math.round((epoch.onset + epoch.duration) * 1000) / 1000) + } +
+
+ {(epoch.properties.length > 0) && + + } - } - - {(epoch.comment || epoch.hed) && - - } + {epoch.type === 'Event' && + + } +
- {visibleComments.includes(index) && + {epoch.properties.length > 0 &&
- {epoch.type == 'Annotation' && epoch.comment && -

Comment: {epoch.comment}

- } - {epoch.type == 'Event' && epoch.hed && -

HED: {epoch.hed}

+ {epoch.properties.length > 0 && +
Additional Columns: + { + epoch.properties.map((property) => + `${property.PropertyName}: ${property.PropertyValue}` + ).join(', ') + } +
}
} @@ -304,7 +310,10 @@ const EventManager = ({ EventManager.defaultProps = { timeSelection: null, epochs: [], - filteredEpochs: [], + filteredEpochs: { + plotVisibility: [], + columnVisibility: [], + }, }; export default connect( diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/Form.js b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/Form.js index 882a3a2a55a..4ce71e7e8eb 100644 --- a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/Form.js +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/Form.js @@ -327,37 +327,39 @@ export const TextboxElement = (props) => { } props.onUserInput(props.id, value); }; + + const {disabled, required} = props; + let requiredHTML = required ? * : null; + let errorMessage = null; + let elementClass = 'row form-group'; + /** * Renders the React component. * * @return {JSX} - React markup for component. */ return ( - <> +
{props.label && -
); }; TextboxElement.defaultProps = { diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/SeriesCursor.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/SeriesCursor.tsx index 89eb142b90f..9df9c957b39 100644 --- a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/SeriesCursor.tsx +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/SeriesCursor.tsx @@ -180,7 +180,7 @@ const SeriesCursor = ( * */ const EpochMarker = () => { - const visibleEpochs = getEpochsInRange(epochs, interval, 'Event'); + const visibleEpochs = getEpochsInRange(epochs, interval); if (visibleEpochs .filter((index) => { filteredEpochs.includes(index); @@ -310,6 +310,6 @@ export default connect( (state: RootState)=> ({ cursorPosition: state.cursor.cursorPosition, epochs: state.dataset.epochs, - filteredEpochs: state.dataset.filteredEpochs, + filteredEpochs: state.dataset.filteredEpochs.plotVisibility, }) )(SeriesCursor); diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/SeriesRenderer.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/SeriesRenderer.tsx index 73436fb5bd4..fce1b0e1083 100644 --- a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/SeriesRenderer.tsx +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/SeriesRenderer.tsx @@ -17,6 +17,7 @@ import { DEFAULT_MAX_CHANNELS, CHANNEL_DISPLAY_OPTIONS, SIGNAL_UNIT, + Vector2, DEFAULT_TIME_INTERVAL, STATIC_SERIES_RANGE, DEFAULT_VIEWER_HEIGHT, @@ -28,7 +29,7 @@ import LineChunk from './LineChunk'; import Epoch from './Epoch'; import SeriesCursor from './SeriesCursor'; import {setRightPanel} from '../store/state/rightPanel'; -import {setFilteredEpochs, setDatasetMetadata} from '../store/state/dataset'; +import {setDatasetMetadata} from '../store/state/dataset'; import {setOffsetIndex} from '../store/logic/pagination'; import IntervalSelect from './IntervalSelect'; import EventManager from './EventManager'; @@ -61,7 +62,6 @@ import { Channel, Epoch as EpochType, RightPanel, - AnnotationMetadata, } from '../store/types'; import {setCurrentAnnotation} from '../store/state/currentAnnotation'; import {setCursorInteraction} from '../store/logic/cursorInteraction'; @@ -79,6 +79,7 @@ type CProps = { timeSelection?: [number, number], setCursor: (number) => void, setRightPanel: (_: RightPanel) => void, + chunksURL: string, channels: Channel[], channelMetadata: ChannelMetadata[], hidden: number[], @@ -93,7 +94,6 @@ type CProps = { setHighPassFilter: (_: string) => void, setViewerWidth: (_: number) => void, setViewerHeight: (_: number) => void, - setFilteredEpochs: (_: number[]) => void, setDatasetMetadata: (_: { limit: number }) => void, dragStart: (_: number) => void, dragContinue: (_: number) => void, @@ -102,9 +102,8 @@ type CProps = { setInterval: (_: [number, number]) => void, setCurrentAnnotation: (_: EpochType) => void, physioFileID: number, - annotationMetadata: AnnotationMetadata, hoveredChannels: number[], - setHoveredChannels: (_: number[]) => void, + setHoveredChannels: (_: number[]) => void, }; /** @@ -120,6 +119,7 @@ type CProps = { * @param root0.timeSelection * @param root0.setCursor * @param root0.setRightPanel + * @param root0.chunksURL * @param root0.channels * @param root0.channelMetadata * @param root0.hidden @@ -134,7 +134,6 @@ type CProps = { * @param root0.setHighPassFilter * @param root0.setViewerWidth * @param root0.setViewerHeight - * @param root0.setFilteredEpochs * @param root0.setDatasetMetadata * @param root0.dragStart * @param root0.dragContinue @@ -142,47 +141,70 @@ type CProps = { * @param root0.limit * @param root0.setCurrentAnnotation * @param root0.physioFileID - * @param root0.annotationMetadata * @param root0.hoveredChannels * @param root0.setHoveredChannels */ const SeriesRenderer: FunctionComponent = ({ - viewerHeight, - viewerWidth, - interval, - setInterval, - domain, - amplitudeScale, - rightPanel, - timeSelection, - setCursor, - setRightPanel, - channels, - channelMetadata, - hidden, - epochs, - filteredEpochs, - activeEpoch, - offsetIndex, - setOffsetIndex, - setAmplitudesScale, - resetAmplitudesScale, - setLowPassFilter, - setHighPassFilter, - setViewerWidth, - setViewerHeight, - setFilteredEpochs, - setDatasetMetadata, - dragStart, - dragContinue, - dragEnd, - limit, - setCurrentAnnotation, - physioFileID, - annotationMetadata, - hoveredChannels, - setHoveredChannels, + viewerHeight, + viewerWidth, + interval, + setInterval, + domain, + amplitudeScale, + rightPanel, + timeSelection, + setCursor, + setRightPanel, + chunksURL, + channels, + channelMetadata, + hidden, + epochs, + filteredEpochs, + activeEpoch, + offsetIndex, + setOffsetIndex, + setAmplitudesScale, + resetAmplitudesScale, + setLowPassFilter, + setHighPassFilter, + setViewerWidth, + setViewerHeight, + setDatasetMetadata, + dragStart, + dragContinue, + dragEnd, + limit, + setCurrentAnnotation, + physioFileID, + hoveredChannels, + setHoveredChannels, }) => { + if (channels.length === 0) return null; + + const [ + numDisplayedChannels, + setNumDisplayedChannels, + ] = useState(DEFAULT_MAX_CHANNELS); + const [cursorEnabled, setCursorEnabled] = useState(false); + const toggleCursor = () => setCursorEnabled((value) => !value); + const [DCOffsetView, setDCOffsetView] = useState(true); + const toggleDCOffsetView = () => setDCOffsetView((value) => !value); + const [stackedView, setStackedView] = useState(false); + const toggleStackedView = () => setStackedView((value) => !value); + const [singleMode, setSingleMode] = useState(false); + const toggleSingleMode = () => setSingleMode((value) => !value); + const [showOverflow, setShowOverflow] = useState(false); + const toggleShowOverflow = () => setShowOverflow((value) => !value); + const [highPass, setHighPass] = useState('none'); + const [lowPass, setLowPass] = useState('none'); + const [refNode, setRefNode] = useState(null); + const [bounds, setBounds] = useState(null); + const getBounds = useCallback((domNode) => { + if (domNode) { + setRefNode(domNode); + } + }, []); const intervalChange = Math.pow( 10, @@ -259,18 +281,13 @@ const SeriesRenderer: FunctionComponent = ({ const viewerRef = useRef(null); const cursorRef = useRef(null); - // Memoized to singal which vars are to be read from - const memoizedCallback = useCallback(null, [ - offsetIndex, interval, limit, timeSelection, amplitudeScale, - ]); - useEffect(() => { // Keypress handler /** * * @param e */ const keybindHandler = (e) => { - if (cursorRef.current) { // Cursor is on page / focus + if (cursorRef.current) { // Cursor is on plot / focus if ([ 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ].indexOf(e.code) > -1) { @@ -321,46 +338,42 @@ const SeriesRenderer: FunctionComponent = ({ toggleSingleMode(); } break; + case 'KeyC': + setRightPanel(null); + break; + // case 'KeyA': + // setRightPanel('annotationForm'); + // break; + case 'KeyZ': + zoomToSelection(); + break; + case 'KeyX': + zoomReset(); + break; + case 'Minus': + zoomOut(); + break; + case 'Equal': // This key combination is '+' + zoomIn(); + break; + case 'KeyN': // Lower amplitude scale + setAmplitudesScale(1.1); + break; + case 'KeyM': // Increase amplitude scale + setAmplitudesScale(0.9); + break; } } } - - // Generic keybinds that don't require focus - if (e.shiftKey) { - switch (e.code) { - case 'KeyC': - setRightPanel(null); - break; - case 'KeyA': - setRightPanel('annotationForm'); - break; - case 'KeyZ': - zoomToSelection(); - break; - case 'KeyX': - zoomReset(); - break; - case 'Minus': - zoomOut(); - break; - case 'Equal': // This key combination is '+' - zoomIn(); - break; - case 'KeyN': // Lower amplitude scale - setAmplitudesScale(1.1); - break; - case 'KeyM': // Increase amplitude scale - setAmplitudesScale(0.9); - break; - } - } }; window.addEventListener('keydown', keybindHandler); return function cleanUp() { // Prevent multiple listeners window.removeEventListener('keydown', keybindHandler); }; - }, [memoizedCallback]); + }, [ + offsetIndex, interval, limit, timeSelection, amplitudeScale, stackedView + ]); useEffect(() => { setViewerHeight(viewerHeight); @@ -416,30 +429,6 @@ const SeriesRenderer: FunctionComponent = ({ prevHoveredChannels.current = hoveredChannels; }, [hoveredChannels]); - const [ - numDisplayedChannels, - setNumDisplayedChannels, - ] = useState(DEFAULT_MAX_CHANNELS); - const [cursorEnabled, setCursorEnabled] = useState(false); - const toggleCursor = () => setCursorEnabled((value) => !value); - const [DCOffsetView, setDCOffsetView] = useState(true); - const toggleDCOffsetView = () => setDCOffsetView((value) => !value); - const [stackedView, setStackedView] = useState(false); - const toggleStackedView = () => setStackedView((value) => !value); - const [singleMode, setSingleMode] = useState(false); - const toggleSingleMode = () => setSingleMode((value) => !value); - const [showOverflow, setShowOverflow] = useState(false); - const toggleShowOverflow = () => setShowOverflow((value) => !value); - const [highPass, setHighPass] = useState('none'); - const [lowPass, setLowPass] = useState('none'); - const [refNode, setRefNode] = useState(null); - const [bounds, setBounds] = useState(null); - const getBounds = useCallback((domNode) => { - if (domNode) { - setRefNode(domNode); - } - }, []); - const topLeft = vec2.fromValues( -viewerWidth/2, viewerHeight/2 @@ -458,8 +447,8 @@ const SeriesRenderer: FunctionComponent = ({ vec2.scale(center, center, 1 / 2); const scales: [ - ScaleLinear, - ScaleLinear + ScaleLinear, + ScaleLinear ] = [ scaleLinear() .domain(interval) @@ -502,32 +491,24 @@ const SeriesRenderer: FunctionComponent = ({ ); }; - /** - * - */ const EpochsLayer = () => { - const epochType = rightPanel === 'eventList' - ? 'Event' - : rightPanel === 'annotationList' - ? 'Annotation' - : null - ; - const visibleEpochs = getEpochsInRange(epochs, interval, epochType); + const visibleEpochs = rightPanel ? getEpochsInRange(epochs, interval) : []; const minEpochWidth = (interval[1] - interval[0]) * MIN_EPOCH_WIDTH / DEFAULT_TIME_INTERVAL[1]; return ( { - visibleEpochs.length < MAX_RENDERED_EPOCHS && + visibleEpochs.length < MAX_RENDERED_EPOCHS && visibleEpochs.map((index) => { return filteredEpochs.includes(index) && ( = ({ } {timeSelection && = ({ { channelList.map((channel, i) => { - if (!channelMetadata[channel.index]) { - return null; - } - const subTopLeft = vec2.create(); - vec2.add( - subTopLeft, - topLeft, - vec2.fromValues( - 0, - stackedView && !singleMode - ? (numDisplayedChannels - 2) * + if (!channelMetadata[channel.index]) { + return null; + } + const subTopLeft = vec2.create(); + vec2.add( + subTopLeft, + topLeft, + vec2.fromValues( + 0, + stackedView && !singleMode + ? (numDisplayedChannels - 2) * diagonal[1] / (2 * numDisplayedChannels) - : (i * diagonal[1]) / numDisplayedChannels - ) - ); + : (i * diagonal[1]) / numDisplayedChannels + ) + ); - const subBottomRight = vec2.create(); - vec2.add( - subBottomRight, - topLeft, - vec2.fromValues( - diagonal[0], - stackedView && !singleMode - ? (numDisplayedChannels + 2) * + const subBottomRight = vec2.create(); + vec2.add( + subBottomRight, + topLeft, + vec2.fromValues( + diagonal[0], + stackedView && !singleMode + ? (numDisplayedChannels + 2) * diagonal[1] / (2 * numDisplayedChannels) - : ((i + 1) * diagonal[1]) / numDisplayedChannels - ) - ); - - const subDiagonal = vec2.create(); - vec2.sub(subDiagonal, subBottomRight, subTopLeft); + : ((i + 1) * diagonal[1]) / numDisplayedChannels + ) + ); - const axisEnd = vec2.create(); - vec2.add(axisEnd, subTopLeft, vec2.fromValues(0.1, subDiagonal[1])); + const subDiagonal = vec2.create(); + vec2.sub(subDiagonal, subBottomRight, subTopLeft); + + const axisEnd = vec2.create(); + vec2.add(axisEnd, subTopLeft, vec2.fromValues(0.1, subDiagonal[1])); + + return ( + channel.traces.map((trace, j) => { + const numChunks = trace.chunks.filter( + (chunk) => chunk.values.length > 0 + ).length; + + const valuesInView = trace.chunks.map((chunk) => { + let includedIndices = [0, chunk.values.length]; + if (chunk.interval[0] < interval[0]) { + const startIndex = chunk.values.length * + (interval[0] - chunk.interval[0]) / + (chunk.interval[1] - chunk.interval[0]); + includedIndices = [startIndex, includedIndices[1]]; + } + if (chunk.interval[1] > interval[1]) { + const endIndex = chunk.values.length * + (interval[1] - chunk.interval[0]) / + (chunk.interval[1] - chunk.interval[0]); + includedIndices = [includedIndices[0], endIndex]; + } + return chunk.values.slice( + includedIndices[0], includedIndices[1] + ); + }).flat(); - return ( - channel.traces.map((trace, j) => { - const numChunks = trace.chunks.filter( - (chunk) => chunk.values.length > 0 - ).length; - - const valuesInView = trace.chunks.map((chunk) => { - let includedIndices = [0, chunk.values.length]; - if (chunk.interval[0] < interval[0]) { - const startIndex = chunk.values.length * - (interval[0] - chunk.interval[0]) / - (chunk.interval[1] - chunk.interval[0]); - includedIndices = [startIndex, includedIndices[1]]; + if (valuesInView.length === 0) { + return; } - if (chunk.interval[1] > interval[1]) { - const endIndex = chunk.values.length * - (interval[1] - chunk.interval[0]) / - (chunk.interval[1] - chunk.interval[0]); - includedIndices = [includedIndices[0], endIndex]; - } - return chunk.values.slice( - includedIndices[0], includedIndices[1] - ); - }).flat(); - if (valuesInView.length === 0) { - return; - } - - const seriesRange: [number, number] = STATIC_SERIES_RANGE; - - const scales: [ - ScaleLinear, - ScaleLinear - ] = [ - scaleLinear() - .domain(interval) - .range([subTopLeft[0], subBottomRight[0]]), - scaleLinear() - .domain(seriesRange) - .range( - stackedView - ? [ - -viewerHeight / (2 * numDisplayedChannels), - viewerHeight / (2 * numDisplayedChannels), - ] - : [subTopLeft[1], subBottomRight[1]] - ), - ]; - - const scaleByAmplitude = scaleLinear() - .domain(seriesRange.map((x) => x * amplitudeScale)) - .range([-0.5, 0.5]); - - /** - * - * @param values - */ - const getScaledMean = (values) => { - let numValues = values.length; - return values.reduce((a, b) => { + const seriesRange: [number, number] = STATIC_SERIES_RANGE; + + const scales: [ + ScaleLinear, + ScaleLinear + ] = [ + scaleLinear() + .domain(interval) + .range([subTopLeft[0], subBottomRight[0]]), + scaleLinear() + .domain(seriesRange) + .range( + stackedView + ? [ + -viewerHeight / (2 * numDisplayedChannels), + viewerHeight / (2 * numDisplayedChannels), + ] + : [subTopLeft[1], subBottomRight[1]] + ), + ]; + + const scaleByAmplitude = scaleLinear() + .domain(seriesRange.map((x) => x * amplitudeScale)) + .range([-0.5, 0.5]); + + /** + * + * @param values + */ + const getScaledMean = (values) => { + let numValues = values.length; + return values.reduce((a, b) => { if (isNaN(b)) { numValues--; return a; } - return a + scaleByAmplitude(b); - }, 0) / numValues; - }; + return a + scaleByAmplitude(b); + }, 0) / numValues; + }; - const DCOffset = DCOffsetView - ? getScaledMean(valuesInView) - : 0; + const DCOffset = DCOffsetView + ? getScaledMean(valuesInView) + : 0; - return ( - trace.chunks.map((chunk, k, chunks) => ( + return ( + trace.chunks.map((chunk, k, chunks) => ( = ({ : chunks[k - 1].values.slice(-1)[0] } /> - )) - ); - }) - ); - })} + )) + ); + }) + ); + })} ); }; @@ -808,23 +789,6 @@ const SeriesRenderer: FunctionComponent = ({ ); }; - const updateCursorCallback = useCallback((cursor: [number, number]) => { - setCursor({ - cursorPosition: [cursor[0], cursor[1]], - viewerRef: viewerRef, - }); - }, []); - - /** - * - * @param v - */ - const updateTimeSelectionCallback = useCallback((v: vec2) => { - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - R.compose(dragStart, R.nth(0))(v); - }, [bounds]); - /** * * @param channelIndex @@ -892,7 +856,7 @@ const SeriesRenderer: FunctionComponent = ({ className='btn btn-primary btn-xs btn-zoom' onClick={zoomToSelection} disabled={!selectionCanBeZoomedTo} - value='Region' + value='Fit to Window' />
@@ -1002,21 +966,18 @@ const SeriesRenderer: FunctionComponent = ({ )}
- +
= ({
+
+
+
+
= ({ {filteredChannels .slice(0, numDisplayedChannels) .map((channel) => ( -
onChannelHover(channel.index)} - onMouseLeave={() => onChannelHover(-1)} - > - {channelMetadata[channel.index] && - channelMetadata[channel.index].name} -
- ))} + ? 'bold' + : 'normal'}`, + }} + onMouseEnter={() => onChannelHover(channel.index)} + onMouseLeave={() => onChannelHover(-1)} + > + {channelMetadata[channel.index] && + channelMetadata[channel.index].name} +
+ ))}
= ({ { + setCursor({ + cursorPosition: [cursor[0], cursor[1]], + viewerRef: viewerRef + }); + }, [])} + mouseDown={useCallback((v: Vector2) => { + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + R.compose(dragStart, R.nth(0))(v); + }, [bounds])} showOverflow={showOverflow} + chunksURL={chunksURL} > = ({ } } - { - [...Array(epochs.length).keys()].filter((i) => - epochs[i].type === 'Annotation' - ).length > 0 && - - } - { - - }
{rightPanel &&
{rightPanel === 'annotationForm' && - + } - {rightPanel === 'eventList' && } - {rightPanel === 'annotationList' && } + {rightPanel === 'eventList' && }
} @@ -1342,9 +1278,10 @@ export default connect( amplitudeScale: state.bounds.amplitudeScale, rightPanel: state.rightPanel, timeSelection: state.timeSelection, + chunksURL: state.dataset.chunksURL, channels: state.channels, epochs: state.dataset.epochs, - filteredEpochs: state.dataset.filteredEpochs, + filteredEpochs: state.dataset.filteredEpochs.plotVisibility, activeEpoch: state.dataset.activeEpoch, hidden: state.montage.hidden, channelMetadata: state.dataset.channelMetadata, @@ -1395,10 +1332,6 @@ export default connect( dispatch, setViewerHeight ), - setFilteredEpochs: R.compose( - dispatch, - setFilteredEpochs - ), setDatasetMetadata: R.compose( dispatch, setDatasetMetadata diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/logic/filterEpochs.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/logic/filterEpochs.tsx index 55126816403..a71937c0a24 100644 --- a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/logic/filterEpochs.tsx +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/logic/filterEpochs.tsx @@ -70,10 +70,12 @@ export const createToggleEpochEpic = (fromState: (_: any) => any) => ( const index = payload; let newFilteredEpochs; - if (filteredEpochs.includes(index)) { - newFilteredEpochs = filteredEpochs.filter((i) => i !== index); + if (filteredEpochs.plotVisibility.includes(index)) { + newFilteredEpochs = filteredEpochs.plotVisibility.filter( + (i) => i !== index + ); } else if (index >= 0 && index < epochs.length) { - newFilteredEpochs = filteredEpochs.slice(); + newFilteredEpochs = filteredEpochs.plotVisibility.slice(); newFilteredEpochs.push(index); newFilteredEpochs.sort(); } else { @@ -81,7 +83,10 @@ export const createToggleEpochEpic = (fromState: (_: any) => any) => ( } return (dispatch) => { - dispatch(setFilteredEpochs(newFilteredEpochs)); + dispatch(setFilteredEpochs({ + plotVisibility: newFilteredEpochs, + columnVisibility: filteredEpochs.columnVisibility, + })); }; }) ); @@ -121,16 +126,9 @@ export const createActiveEpochEpic = (fromState: (_: any) => any) => ( * * @param {Epoch[]} epochs - Array of epoch * @param {[number, number]} interval - Time interval to search - * @param {string} epochType - Epoch type (Annotation|Event) - * @param {boolean} withComments - Include only if has comments * @returns {Epoch[]} - Epoch[] in interval with epochType */ -export const getEpochsInRange = ( - epochs, - interval, - epochType, - withComments = false, -) => { +export const getEpochsInRange = (epochs, interval) => { return [...Array(epochs.length).keys()].filter((index) => ( (isNaN(epochs[index].onset) && interval[0] === 0) @@ -139,8 +137,6 @@ export const getEpochsInRange = ( epochs[index].onset + epochs[index].duration > interval[0] && epochs[index].onset < interval[1] ) - ) && - epochs[index].type === epochType && - (!withComments || epochs[index].hed || epochs[index].comment) + ) ); }; diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/state/dataset.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/state/dataset.tsx index 399696f0b55..da1dcbcd875 100644 --- a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/state/dataset.tsx +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/state/dataset.tsx @@ -1,6 +1,6 @@ import * as R from 'ramda'; import {createAction} from 'redux-actions'; -import {ChannelMetadata, Epoch} from '../types'; +import {ChannelMetadata, Epoch, EpochFilter} from '../types'; import {DEFAULT_MAX_CHANNELS} from '../../../vector'; export const SET_EPOCHS = 'SET_EPOCHS'; @@ -43,7 +43,7 @@ export type State = { offsetIndex: number, limit: number, epochs: Epoch[], - filteredEpochs: number[], + filteredEpochs: EpochFilter, activeEpoch: number | null, physioFileID: number | null, shapes: number[][], @@ -64,7 +64,10 @@ export const datasetReducer = ( chunksURL: '', channelMetadata: [], epochs: [], - filteredEpochs: [], + filteredEpochs: { + plotVisibility: [], + columnVisibility: [], + }, activeEpoch: null, physioFileID: null, offsetIndex: 1, diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/state/montage.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/state/montage.tsx index 13d2f799486..c6000c69130 100644 --- a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/state/montage.tsx +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/state/montage.tsx @@ -1,6 +1,6 @@ import * as R from 'ramda'; import {createAction} from 'redux-actions'; -import {Electrode} from '../types'; +import {CoordinateSystem, Electrode} from '../types'; export const SET_ELECTRODES = 'SET_ELECTRODES'; export const setElectrodes = createAction(SET_ELECTRODES); @@ -8,13 +8,18 @@ export const setElectrodes = createAction(SET_ELECTRODES); export const SET_HIDDEN = 'SET_HIDDEN'; export const setHidden = createAction(SET_HIDDEN); +export const SET_COORDINATE_SYSTEM = 'SET_COORDINATE_SYSTEM'; +export const setCoordinateSystem = createAction(SET_COORDINATE_SYSTEM); + export type Action = | {type: 'SET_ELECTRODES', payload: Electrode[]} - | {type: 'SET_HIDDEN', payload: number[]}; + | {type: 'SET_HIDDEN', payload: number[]} + | {type: 'SET_COORDINATE_SYSTEM', payload: CoordinateSystem}; export type State = { electrodes: Electrode[], - hidden: number[] + hidden: number[], + coordinateSystem: CoordinateSystem, }; export type Reducer = (state: State, action?: Action) => State; @@ -27,7 +32,7 @@ export type Reducer = (state: State, action?: Action) => State; * @returns {State} - The updated state */ export const montageReducer: Reducer = ( - state = {electrodes: [], hidden: []}, + state = {electrodes: [], hidden: [], coordinateSystem: null}, action ) => { if (!action) { @@ -40,6 +45,9 @@ export const montageReducer: Reducer = ( case SET_HIDDEN: { return R.assoc('hidden', action.payload, state); } + case SET_COORDINATE_SYSTEM: { + return R.assoc('coordinateSystem', action.payload, state); + } default: { return state; } diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/types.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/types.tsx index cd826e781ee..1e5a7829270 100644 --- a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/types.tsx +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/types.tsx @@ -28,30 +28,38 @@ export type Channel = { export type Epoch = { onset: number, duration: number, - type: 'Event' | 'Annotation', + type: 'Event', label: string, - comment?: string, + value: string, + trialType: string, + properties?: any[], hed?: string, channels: number[] | 'all', - annotationInstanceID?: number, + physiologicalTaskEventID?: number, }; -export type EventMetadata = { - instances: any[], +export type EpochFilter = { + plotVisibility: number[], + columnVisibility: number[], } -export type AnnotationMetadata = { +export type EventMetadata = { instances: any[], - labels: any[], - metadata: any[] + extraColumns: any[], } export type RightPanel = 'annotationForm' | 'eventList' - | 'annotationList' | null; + +export type CoordinateSystem = { + name: string | 'Other', + units: string | 'm', + description: string | 'n/a' +}; + export type Electrode = { name: string, channelIndex?: number, diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/vector/index.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/vector/index.tsx index 994fc806fe3..b050a2e9258 100644 --- a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/vector/index.tsx +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/vector/index.tsx @@ -1,5 +1,7 @@ import {vec2, glMatrix} from 'gl-matrix'; +export type Vector2 = typeof glMatrix.ARRAY_TYPE; + /** * Apply transformation f on point p * diff --git a/modules/electrophysiology_browser/php/annotations.class.inc b/modules/electrophysiology_browser/php/events.class.inc similarity index 61% rename from modules/electrophysiology_browser/php/annotations.class.inc rename to modules/electrophysiology_browser/php/events.class.inc index 0b0ea96aac4..a32360dca3c 100644 --- a/modules/electrophysiology_browser/php/annotations.class.inc +++ b/modules/electrophysiology_browser/php/events.class.inc @@ -1,11 +1,13 @@ getMethod()) { case 'GET': + // TODO: Get official server-side solution + Add to documentation + // set_time_limit(300); // Increase request time limit to 5 minutes + // ini_set('memory_limit', '1G'); // Increase memory allocation limit + $parameters = $request->getQueryParams(); $sessionID = $db->pselectOne( 'SELECT SessionID @@ -59,7 +65,7 @@ class Annotations extends \NDB_Page } $physioFileID = intval($parameters['physioFileID']); - (new ElectrophysioAnnotations($physioFileID))->updateFiles(); + (new ElectrophysioEvents($physioFileID))->updateFiles(); $config = \NDB_Factory::singleton()->config(); $downloadpath = \Utility::appendForwardSlash( @@ -73,12 +79,16 @@ class Annotations extends \NDB_Page $downloader = new \LORIS\FilesDownloadHandler( new \SPLFileInfo($downloadpath . $path) ); + return $downloader->handle( $request->withAttribute('filename', $filename) ); case 'DELETE': - $parameters = json_decode((string) $request->getBody(), true); - if (!$user->hasPermission('electrophysiology_browser_edit_annotations') + $parameters = json_decode((string)$request->getBody(), true); + + if (!$user->hasPermission( + 'electrophysiology_browser_edit_annotations' + ) ) { return (new \LORIS\Http\Response\JSON\Unauthorized()); } @@ -89,53 +99,50 @@ class Annotations extends \NDB_Page return (new \LORIS\Http\Response\JSON\BadRequest()); } - (new ElectrophysioAnnotations(intval($parameters['physioFileID']))) - ->delete(intval($parameters['instance_id'])); - return (new \LORIS\Http\Response\JSON\OK()); case 'POST': - $parameters = json_decode((string) $request->getBody(), true); - if (!$user->hasPermission('electrophysiology_browser_edit_annotations') + // TODO: Better failure reporting + $parameters = json_decode((string)$request->getBody(), true); + + if (!$user->hasPermission( + 'electrophysiology_browser_edit_annotations' + ) ) { return (new \LORIS\Http\Response\JSON\Unauthorized()); } - if (!isset($parameters['physioFileID'])) { + if (!isset($parameters['physioFileID']) + || !isset($parameters['request_type']) + ) { return (new \LORIS\Http\Response\JSON\BadRequest()); } - $instance_data = $parameters['instance']; - // $metadata = $parameters['metadata']; - // TODO: Figure out a better description modeled on other derivatives - $metadata = [ - 'description' => 'An annotation', - 'sources' => 'EEGNet LORIS', - 'author' => $user->getFullname() - ]; - - $instance_id = $parameters['instance_id'] ? - intval($parameters['instance_id']) : null; - $parameter_id = $parameters['parameter_id'] ?? null; - - (new ElectrophysioAnnotations(intval($parameters['physioFileID']))) - ->update($instance_data, $metadata, $instance_id, $parameter_id); - - // if new annotation, get instanceID - if (is_null($instance_id)) { - $instance_id = $db->pselectOne( - "SELECT MAX(AnnotationInstanceID) - FROM physiological_annotation_instance ai - JOIN physiological_annotation_file af USING (AnnotationFileID) - WHERE PhysiologicalFileID=:physioFileID - ", - ['physioFileID' => $parameters['physioFileID']] - ); + switch ($parameters['request_type']) { + case 'event_update': + $instance_data = $parameters['instance']; + // $metadata = $parameters['metadata']; + // TODO: Figure out better description modeled on other derivatives + $metadata = [ + 'description' => 'An event', + 'sources' => 'EEGNet LORIS', + 'author' => $user->getFullname() + ]; + + $instance_id = $parameters['instance_id'] ? + intval($parameters['instance_id']) : null; + + $updated_instance = ( + new ElectrophysioEvents(intval($parameters['physioFileID'])) + )->update($instance_data, $metadata, $instance_id); + + if (count($updated_instance) > 0) { + return (new \LORIS\Http\Response\JSON\OK( + ['instance' => $updated_instance] + )); + } + return (new \LORIS\Http\Response\JSON\Unauthorized()); } - - return (new \LORIS\Http\Response\JSON\OK( - ['instance_id' => $instance_id] - )); - default: + default: return (new \LORIS\Http\Response\JSON\MethodNotAllowed( ["GET", "DELETE", "POST"] )); diff --git a/modules/electrophysiology_browser/php/models/electrophysioannotations.class.inc b/modules/electrophysiology_browser/php/models/electrophysioannotations.class.inc deleted file mode 100644 index 82e23adadf4..00000000000 --- a/modules/electrophysiology_browser/php/models/electrophysioannotations.class.inc +++ /dev/null @@ -1,607 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 - * @link https://www.github.com/aces/Loris/ - */ -class ElectrophysioAnnotations -{ - private int $_physioFileID; - private array $_data; - - /** - * Construct an Annotation object - * - * @param integer $physioFileID Electrophysiological file ID - * to collect annotation data from - */ - function __construct(int $physioFileID) - { - $this->_physioFileID = $physioFileID; - $db = \NDB_Factory::singleton()->database(); - - $annotationInstance = $db->pselect( - 'SELECT i.* - FROM physiological_annotation_instance AS i - JOIN physiological_annotation_file AS f - ON f.AnnotationFileID = i.AnnotationFileID - WHERE f.PhysiologicalFileID=:PFID AND f.FileType="tsv"', - ['PFID' => $this->_physioFileID] - ); - - $annotationMetadata = $db->pselect( - 'SELECT p.* - FROM physiological_annotation_parameter AS p - JOIN physiological_annotation_file AS f - ON f.AnnotationFileID = p.AnnotationFileID - WHERE f.PhysiologicalFileID=:PFID AND f.FileType="json"', - ['PFID' => $this->_physioFileID] - ); - - $annotationLabels = $db->pselect( - 'SELECT * FROM physiological_annotation_label', - [] - ); - - $this->_data = [ - 'instances' => $annotationInstance, - 'metadata' => $annotationMetadata, - 'labels' => $annotationLabels, - ]; - } - - /** - * Get data for the Electrophysiological file annotations - * - * @return array The data array - */ - function getData(): array - { - return $this->_data; - } - - /** - * Updates annotation tables when there is a POST request. - * Will add new derivative files if none exist for the given instance. - * Will either add new annotations or update existing ones. - * - * @param array $instance_data Instance data - * @param array $metadata Metadata - * @param int|null $instance_id InstanceID - * @param int|null $parameter_id ParameterID - * - * @return void - */ - function update( - array $instance_data, - array $metadata, - ?int $instance_id, - ?int $parameter_id - ): void { - - $factory = \NDB_Factory::singleton(); - $user = $factory->user(); - $db = $factory->database(); - - if ($user->hasPermission('electrophysiology_browser_edit_annotations')) { - - //If the label is new, add to annotation label table - //and get label ID - $labelID = $db->pselectOne( - // Adding MAX here as a hack fix for now until LORIS-MRI - // bugfix for issue https://github.com/aces/Loris-MRI/issues/763 - // is available and cleanup happens of the annotation_label table - "SELECT MAX(AnnotationLabelID) - FROM physiological_annotation_label - WHERE LabelName=:label", - ['label' => $instance_data['label_name']] - ); - if (empty($labelID)) { - $data = [ - 'LabelName' => $instance_data['label_name'], - 'LabelDescription' => $instance_data['label_description'] - ]; - $db->insert("physiological_annotation_label", $data); - $labelID = $db->pselectOne( - "SELECT AnnotationLabelID - FROM physiological_annotation_label - WHERE LabelName=:label", - ['label' => $instance_data['label_name']] - ); - } - - //If no derivative files exist, must create new files - $annotationFIDs = $db->pselect( - "SELECT AnnotationFileID - FROM physiological_annotation_file - WHERE PhysiologicalFileID=:PFID", - ['PFID' => $this->_physioFileID] - ); - - //Get data from POST request - $metadata = [ - 'Description' => $metadata['description'], - 'Sources' => $metadata['sources'], - 'Author' => $metadata['author'] - ]; - - $instance = [ - 'Onset' => $instance_data['onset'], - 'Duration' => $instance_data['duration'], - 'AnnotationLabelID' => $labelID, - 'Channels' => $instance_data['channels'] === 'all' ? - null : - $instance_data['channels'], - 'Description' => $instance_data['description'] - ]; - - //Insert new files and data into DB - if (empty($annotationFIDs)) { - //Create new annotation files - $this->_createFiles(); - - //Get new annotation file ID - $annotation_tsv_ID = $db->pselectOne( - "SELECT AnnotationFileID - FROM physiological_annotation_file - WHERE PhysiologicalFileID=:PFID - AND FileType='tsv'", - ['PFID' => $this->_physioFileID] - ); - //Get new annotation file ID - $annotation_json_ID = $db->pselectOne( - "SELECT AnnotationFileID - FROM physiological_annotation_file - WHERE PhysiologicalFileID=:PFID - AND FileType='json'", - ['PFID' => $this->_physioFileID] - ); - - $metadata['AnnotationFileID'] = $annotation_json_ID; - $db->insert("physiological_annotation_parameter", $metadata); - - //Get new metadata file ID - $metadata_ID = $db->pselectOne( - "SELECT AnnotationParameterID - FROM physiological_annotation_parameter - WHERE AnnotationFileID=:annotation_ID", - ['annotation_ID' => $annotation_json_ID] - ); - - $instance['AnnotationFileID'] = $annotation_tsv_ID; - $instance['AnnotationParameterID'] = $metadata_ID; - $db->insert("physiological_annotation_instance", $instance); - - } else { - //If the files are not new - //Get annotation file ID for the tsv file - $tsv_ID = $db->pselectOne( - "SELECT AnnotationFileID - FROM physiological_annotation_file - WHERE PhysiologicalFileID=:PFID - AND FileType='tsv'", - ['PFID' => $this->_physioFileID] - ); - //Get annotation file ID for the json file - $json_ID = $db->pselectOne( - "SELECT AnnotationFileID - FROM physiological_annotation_file - WHERE PhysiologicalFileID=:PFID - AND FileType='json'", - ['PFID' => $this->_physioFileID] - ); - - $instance['AnnotationFileID'] = $tsv_ID; - $metadata['AnnotationFileID'] = $json_ID; - - /* If no instance ID is specified, insert new instance - * into instance table and get the parameter file ID - * from the parameter table - */ - if (is_null($instance_id)) { - $parameterID = $db->pselectOne( - "SELECT AnnotationParameterID - FROM physiological_annotation_parameter - WHERE AnnotationFileID=:annotationFID", - ['annotationFID' => $json_ID] - ); - $instance['AnnotationParameterID'] = $parameterID; - - $db->insert('physiological_annotation_instance', $instance); - } else { - $db->update( - 'physiological_annotation_instance', - $instance, - ['AnnotationInstanceID' => $instance_id] - ); - } - //Update parameter table if parameter ID provided - if (!is_null($parameter_id)) { - $db->update( - 'physiological_annotation_parameter', - $metadata, - ['AnnotationParameterID' => $parameter_id] - ); - } - - //In all cases where files are not new, - //set LastUpdate time for all related files - - $db->update( - 'physiological_annotation_file', - ['LastUpdate' => date("Y-m-d H:i:s")], - ['PhysiologicalFileID' => $this->_physioFileID] - ); - } - } - } - - /** - * Deletes one annotation - * - * @param int $annotationID Annotation ID - * - * @return void - */ - function delete(int $annotationID): void - { - // TODO : check that $annotationID belongs to physioFileID - $db = \NDB_Factory::singleton()->database(); - - $physioFileID = $db->pselectone( - 'SELECT PhysiologicalFileID - FROM physiological_annotation_file AS f - INNER JOIN physiological_annotation_instance AS i - ON f.AnnotationFileID=i.AnnotationFileID - AND i.AnnotationInstanceID=:annotationID', - ['annotationID' => $annotationID] - ); - - if ($this->_physioFileID == $physioFileID) { - $db->delete( - "physiological_annotation_instance", - ['AnnotationInstanceID' => $annotationID] - ); - } - } - - /** - * Updates the derivative files associated with the - * physiological file ID - * - * @return void - * @throws SodiumException - */ - function updateFiles(): void - { - $db = \NDB_Factory::singleton()->database(); - - //If no derivative files exist, must create new files - $annotationFIDs = $db->pselect( - "SELECT AnnotationFileID - FROM physiological_annotation_file - WHERE PhysiologicalFileID=:PFID", - ['PFID' => $this->_physioFileID] - ); - //Insert new files and data into DB - if (empty($annotationFIDs)) { - //Create new annotation files - $this->_createFiles(); - } - - //Get data directory base path from Config - $dataDir = $db->pselectOne( - 'SELECT Value - FROM Config AS config - INNER JOIN ConfigSettings AS c - ON c.Name=:name AND config.ConfigID=c.ID', - ['name' => 'dataDirBasepath'] - ); - - $tsv_entries = [ - 'onset', 'duration', 'label', 'channels', 'absolute_time', 'description' - ]; - - $tsv = $db->pselect( - "SELECT - AnnotationFileID AS id, - FilePath AS filePath, - LastUpdate AS lastUpdate, - LastWritten AS lastWritten - FROM physiological_annotation_file - WHERE PhysiologicalFileID=:PFID - AND FileType='tsv'", - ['PFID' => $this->_physioFileID] - ); - - $json = $db->pselect( - "SELECT - AnnotationFileID AS id, - FilePath AS filePath, - LastUpdate AS lastUpdate, - LastWritten AS lastWritten - FROM physiological_annotation_file - WHERE PhysiologicalFileID=:PFID - AND FileType='json'", - ['PFID' => $this->_physioFileID] - ); - - $tsv_path = $dataDir.$tsv[0]['filePath']; - $json_path = $dataDir.$json[0]['filePath']; - - //Update files if files updated before database updated - if ($tsv[0]['lastWritten'] <= $tsv[0]['lastUpdate'] - || $json[0]['lastWritten'] <= $json[0]['lastUpdate'] - ) { - //Update the three files with the given paths - $labels = []; // Label Name => Label Description - $tsv_file = fopen($tsv_path, 'w'); //Will override all file content - - //Get all annotation instances - //Then go thru each and get the label name + description - //add label name to file and also to an array for json file - //change anything null to n/a - $instances = $db->pselect( - "SELECT - p.Onset AS Onset, - p.Duration AS Duration, - l.LabelName AS LabelName, - l.LabelDescription AS LabelDescription, - p.Channels AS Channels, - p.AbsoluteTime AS AbsoluteTime, - p.Description AS Description - FROM physiological_annotation_instance p - LEFT JOIN physiological_annotation_label l - ON (l.AnnotationLabelID=p.AnnotationLabelID) - WHERE p.AnnotationFileID=:AFID", - ['AFID' => $tsv[0]['id']] - ); - - if (count($instances) < 1) { - return; - } - - //Add columns - $columns = implode("\t", $tsv_entries); - fwrite($tsv_file, $columns."\n"); - - foreach ($instances as $instance) { - //Add labels to list for parameter file - $labels[$instance['LabelName']] = $instance['LabelDescription']; - - //Setup each column in correct order - $input_tsv = [ - $instance['Onset'], - $instance['Duration'], - $instance['LabelName'], - $instance['Channels'], - $instance['AbsoluteTime'], - $instance['Description'] - ]; - //Set all null values to 'n/a' - $input_tsv = array_map( - function ($v) { - return (is_null($v)) ? "n/a" : $v; - }, - $input_tsv - ); - //Implode with tabs as delimeter - $input = implode("\t", $input_tsv); - - fwrite($tsv_file, $input."\n"); - } - fclose($tsv_file); - - //Write to metadata (json) file - //Get metadata from database (should only be 1 entry) - $json_desc = $db->pselectOne( - "SELECT Description - FROM physiological_annotation_parameter - WHERE AnnotationFileID=:AFID", - ['AFID' => $json[0]['id']] - ); - $json_source = $db->pselectOne( - "SELECT Sources - FROM physiological_annotation_parameter - WHERE AnnotationFileID=:AFID", - ['AFID' => $json[0]['id']] - ); - $json_author = $db->pselectOne( - "SELECT Author - FROM physiological_annotation_parameter - WHERE AnnotationFileID=:AFID", - ['AFID' => $json[0]['id']] - ); - //Get "IntendedFor" entry: physiological file path - $physioFilePath = $db->pselectOne( - "SELECT FilePath - FROM physiological_file - WHERE PhysiologicalFileID=:PFID", - ['PFID' => $this->_physioFileID] - ); - - $input_json = [ - "Description" => $json_desc, - "IntendedFor" => $physioFilePath, - "Sources" => $json_source, - "Author" => $json_author, - "LabelDescription" => $labels - ]; - $input_encode = json_encode($input_json, JSON_PRETTY_PRINT); - - $json_file = fopen($json_path, 'w'); - fwrite($json_file, $input_encode); - fclose($json_file); - - //Update archives and create new hash - $this->_updateArchives([$tsv_path, $json_path]); - - //Update time that files were written to - $db->update( - 'physiological_annotation_file', - ['LastWritten' => date("Y-m-d H:i:s")], - ['PhysiologicalFileID' => $this->_physioFileID] - ); - } - } - - /** - * Creates new annotation files for the given physiological file - * and inserts their information into database - * - * @return void - * @throws SodiumException - */ - function _createFiles() : void - { - $db = \NDB_Factory::singleton()->database(); - - $physioFilePath = $db->pselectOne( - 'SELECT FilePath - FROM physiological_file - WHERE PhysiologicalFileID=:PFID', - ['PFID' => $this->_physioFileID] - ); - - // Get output type (raw, derivative) - $outputType = $db->pselectOne( - 'SELECT OutputTypeName - FROM physiological_file pf - JOIN physiological_output_type ot USING (PhysiologicalOutputTypeID) - WHERE PhysiologicalFileID=:PFID', - ['PFID' => $this->_physioFileID] - ); - - //Create new filepaths - //Get data directory base path from Config - $dataDir = $db->pselectOne( - 'SELECT Value - FROM Config AS config - INNER JOIN ConfigSettings AS c - ON c.Name=:name AND config.ConfigID=c.ID', - ['name' => 'dataDirBasepath'] - ); - //Create path with correct structure - $subPath = strstr($physioFilePath, "sub"); - - if ($outputType === 'derivative') { - $destinationPath = $dataDir - . "bids_imports/derivatives/loris_annotations/" - . $subPath; - } else { - $destinationPath = $dataDir . $physioFilePath; - } - - //Create directories if they don't exist - $dirname = pathinfo($destinationPath, PATHINFO_DIRNAME); - if (!file_exists($dirname)) { - mkdir($dirname, 0777, true); - } - - //Replace file type with "annotations" - $pathWithoutEDF = substr( - $destinationPath, - 0, - strrpos($destinationPath, "_") - ); - - $tsv_path = $pathWithoutEDF . "_annotations.tsv"; - $json_path = $pathWithoutEDF . "_annotations.json"; - $tgz_path = $pathWithoutEDF . "_annotations.tgz"; - - //Create files - $tsv_file = fopen($tsv_path, 'a+'); - $json_file = fopen($json_path, 'a+'); - - $tgz_file = new \PharData($tgz_path); - $tgz_file->addFile($tsv_path, basename($tsv_path)); - $tgz_file->addFile($json_path, basename($json_path)); - fclose($tsv_file); - fclose($json_file); - - $annotation_f = file_get_contents($tgz_path); - $annotation_hash = sodium_crypto_generichash($annotation_f); - - $params_tsv = [ - 'PhysiologicalFileID' => $this->_physioFileID, - 'FileType' => 'tsv', - 'FilePath' => str_replace($dataDir, '', $tsv_path) - ]; - $params_json = [ - 'PhysiologicalFileID' => $this->_physioFileID, - 'FileType' => 'json', - 'FilePath' => str_replace($dataDir, '', $json_path), - ]; - $params_archive = [ - 'PhysiologicalFileID' => $this->_physioFileID, - 'FilePath' => str_replace($dataDir, '', $tgz_path), - 'Blake2bHash' => bin2hex($annotation_hash) - ]; - $db->insert("physiological_annotation_file", $params_tsv); - $db->insert("physiological_annotation_file", $params_json); - $db->insert("physiological_annotation_archive", $params_archive); - } - - /** - * Updates the annotation and physiological archives for the given - * physiological file ID with the provided paths and updates - * database with new archive file hash - * - * @param array $paths Paths to files to be added to archive - * - * @return void - * @throws SodiumException - */ - function _updateArchives(array $paths) : void - { - $db = \NDB_Factory::singleton()->database(); - - $dataDir = $db->pselectOne( - 'SELECT Value - FROM Config AS config - INNER JOIN ConfigSettings AS c - ON c.Name=:name AND config.ConfigID=c.ID', - ['name' => 'dataDirBasepath'] - ); - $queries = [ - 'physiological_annotation_archive', - 'physiological_archive' - ]; - - foreach ($queries as $query) { - $filepath = $db->pselectone( - "SELECT - DISTINCT(FilePath) - FROM $query - WHERE PhysiologicalFileID=:PFID", - ['PFID' => $this->_physioFileID] - ); - if (!$filepath) { - continue; - } - $filepath = $dataDir.$filepath; - $arch_file = new \PharData($filepath); - foreach ($paths as $path) { - $arch_file->addFile($path, basename($path)); - } - - $f = file_get_contents($filepath); - $hash = sodium_crypto_generichash($f); - - //Update database with hash - $db->update( - $query, - ['Blake2bHash' => bin2hex($hash)], - ['PhysiologicalFileID' => $this->_physioFileID] - ); - } - } -} diff --git a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc index 25161a93ed3..bf1f5a15db4 100644 --- a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc +++ b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc @@ -1,5 +1,6 @@ database(); $taskEvents = $db->pselect( - 'SELECT te.* + 'SELECT te.* FROM physiological_task_event AS te JOIN physiological_event_file AS f - ON f.EventFileID = te.EventFileID + ON f.EventFileID = te.EventFileID WHERE f.PhysiologicalFileID=:PFID AND f.FileType="tsv"', ['PFID' => $this->_physioFileID] ); - /** - * TODO: Get event params and metadata. - * NOT in the scope of current task - **/ + $taskEventIDs = array_map( + function ($taskEvent) { + return $taskEvent['PhysiologicalTaskEventID']; + }, + $taskEvents + ); + + $taskEventIDs = array_combine( + array_map('intval', array_keys($taskEventIDs)), + array_values($taskEventIDs) + ); + + $extraColumns = $db->pselect( + 'SELECT opt.* + FROM physiological_task_event_opt AS opt + WHERE opt.PhysiologicalTaskEventID IN (' + . ( + count($taskEventIDs) > 0 + ? join(',', $taskEventIDs) + : 'null' + ) . ')', + [] + ); $this->_data = [ - 'instances' => $taskEvents, + 'instances' => $taskEvents, + 'extraColumns' => $extraColumns, ]; } /** - * Get data for the Electrophysiological file annotations + * Get data for the Electrophysiological events * * @return array The data array */ @@ -57,7 +78,328 @@ class ElectrophysioEvents } /** - * TODO: Add other features such as add, update, delete - * NOT in the scope of current task - **/ + * Updates event tables when there is a POST request. + * Will add new derivative files if none exist for the given instance. + * Will either add new events or update existing ones. + * + * @param array $instance_data Instance data + * @param array $metadata Metadata + * @param int|null $instance_id InstanceID + * + * @return array + */ + function update( + array $instance_data, + array $metadata, + ?int $instance_id, + ): array { + + $factory = \NDB_Factory::singleton(); + $user = $factory->user(); + $db = $factory->database(); + + if ($user->hasPermission('electrophysiology_browser_edit_annotations')) { + + //If no derivative files exist, must create new files + $eventFileID = $db->pselect( + "SELECT EventFileID + FROM physiological_task_event + WHERE PhysiologicalFileID=:PFID", + ['PFID' => $this->_physioFileID] + ); + + if (is_null($instance_id)) { + // TODO: Support Instance INSERT + return []; + } + + $instance = [ + 'Onset' => $instance_data['onset'], + 'Duration' => $instance_data['duration'], + ]; + + // TODO: Support Event Instance Insert + if (!empty($eventFileID)) { + // Update physiological_task_event + $db->update( + 'physiological_task_event', + $instance, + ['PhysiologicalTaskEventID' => $instance_id] + ); + + $db->update( + 'physiological_event_file', + ['LastUpdate' => date("Y-m-d H:i:s")], + ['PhysiologicalFileID' => $this->_physioFileID] + ); + } + + $taskEvent = $db->pselect( + 'SELECT * FROM physiological_task_event + WHERE PhysiologicalTaskEventID=:PTEID', + ['PTEID' => $instance_id] + ); + + $extraColumns = $db->pselect( + 'SELECT opt.* + FROM physiological_task_event_opt AS opt + WHERE opt.PhysiologicalTaskEventID=:PTEID', + ['PTEID' => $instance_id] + ); + + return [ + 'instance' => $taskEvent[0], + 'extraColumns' => $extraColumns, + ]; + } + return []; + } + + /** + * Deletes one event instance + * + * @param int $physiologicalTaskEventID PhysiologicalTaskEventID + * + * @return void + */ + function deleteEvent(int $physiologicalTaskEventID): void + { + $db = \NDB_Factory::singleton()->database(); + + $physioFileID = $db->pselectone( + 'SELECT PhysiologicalFileID + FROM physiological_task_event + WHERE PhysiologicalTaskEventID=:taskEventID', + ['taskEventID' => $physiologicalTaskEventID] + ); + + // TODO: Check that this cascades properly to rel tables + if ($this->_physioFileID == $physioFileID) { + $db->delete( + "physiological_task_event", + ['PhysiologicalTaskEventID' => $physiologicalTaskEventID] + ); + } + } + + /** + * Updates the event files associated with the given + * physiological file ID + * + * @return void + * @throws SodiumException + */ + function updateFiles(): void + { + $db = \NDB_Factory::singleton()->database(); + + //Get data directory base path from Config + $config = \NDB_Factory::singleton()->config(); + $dataDir = $config->getSetting("dataDirBasepath"); + + $tsv = $db->pselect( + "SELECT + EventFileID AS id, + FilePath AS filePath, + ProjectID AS projectID, + LastUpdate AS lastUpdate, + LastWritten AS lastWritten + FROM physiological_event_file + WHERE PhysiologicalFileID=:PFID + AND FileType='tsv'", + ['PFID' => $this->_physioFileID] + ); + + if (count($tsv) > 0) { + $tsvPath = $dataDir . $tsv[0]['filePath']; + // Update files if files updated before database updated + if ($tsv[0]['lastWritten'] <= $tsv[0]['lastUpdate']) { + // events.tsv + $tsvFile = fopen($tsvPath, 'w'); // Will override all file content + + $extraColumns = $db->pselect( + "SELECT * + FROM physiological_task_event_opt + WHERE PhysiologicalTaskEventID IN ( + SELECT PhysiologicalTaskEventID + FROM physiological_task_event + WHERE PhysiologicalFileID=:PFID + )", + ['PFID' => $this->_physioFileID] + ); + + $columnNames = $db->pselect( + "SELECT DISTINCT PropertyName + FROM physiological_task_event_opt + WHERE PhysiologicalTaskEventID IN ( + SELECT PhysiologicalTaskEventID + FROM physiological_task_event + WHERE PhysiologicalFileID=:PFID + )", + ['PFID' => $this->_physioFileID] + ); + + // TODO: Make columns more dynamic + $tsvEntries = [ + 'onset', 'duration', 'sample', 'trial_type', + 'response_time', 'value' + ]; + foreach ($columnNames as $columnName) { + $tsvEntries[] = $columnName['PropertyName']; + } + // $tsvEntries[] = 'HED'; + + // Add columns names + $columns = implode("\t", $tsvEntries); + fwrite($tsvFile, "$columns\n"); + + $instances = $db->pselect( + "SELECT + PhysiologicalTaskEventID, + Onset, + Duration, + EventSample, + TrialType, + ResponseTime, + EventValue + FROM physiological_task_event + WHERE PhysiologicalFileID=:PFID", + ['PFID' => $this->_physioFileID] + ); + + foreach ($instances as $instance) { + // Setup each column in correct order + $inputTSV = [ + $instance['Onset'], + $instance['Duration'], + $instance['EventSample'], + $instance['TrialType'], + $instance['ResponseTime'], + $instance['EventValue'], + ]; + + $taskEventID = $instance['PhysiologicalTaskEventID']; + + // Get instance's extra columns + $instanceExtraColumns + = array_filter( + array_values($extraColumns), + function ($column) use ($taskEventID) { + return + $column['PhysiologicalTaskEventID'] == + $taskEventID; + } + ); + + foreach ($columnNames as $columnName) { + $column = array_filter( + array_values($instanceExtraColumns), + function ($col) use ($columnName) { + return + $col['PropertyName'] == + $columnName['PropertyName']; + } + ); + + $columnValue = count($column) > 0 + ? array_values($column)[0]['PropertyValue'] + : 'n/a'; + + $inputTSV[] = $columnValue; + } + + // Set all null values to 'n/a' + $inputTSV = array_map( + function ($v) { + return is_null($v) ? "n/a" : $v; + }, + $inputTSV + ); + + // Implode with tabs as delimiter + $input = implode("\t", $inputTSV); + + fwrite($tsvFile, $input . "\n"); + } + fclose($tsvFile); + + //Update archives and create new hash + $this->_updateArchives([$tsvPath]); + + // Update time that files were written to + $db->update( + 'physiological_event_file', + ['LastWritten' => date("Y-m-d H:i:s")], + ['PhysiologicalFileID' => $this->_physioFileID] + ); + } + } + } + + /** + * Convert column name from DB into BIDS-recognized column name + * + * @param string $columnName Column name from DB + * + * @return string + */ + function _getColumnName(string $columnName) : string + { + return match (strtolower($columnName)) { + 'eventvalue', 'event_value', 'value' => 'value', + 'trialtype' => 'trial_type', + default => $columnName, + }; + } + + /** + * Updates the event and physiological archives for the given + * physiological file ID with the provided paths and updates + * database with new archive file hash + * + * @param array $paths Paths to files to be added to archive + * + * @return void + * @throws SodiumException + */ + function _updateArchives(array $paths) : void + { + $db = \NDB_Factory::singleton()->database(); + + //Get data directory base path from Config + $config = \NDB_Factory::singleton()->config(); + $dataDir = $config->getSetting("dataDirBasepath"); + + $archive_table_names = [ + 'physiological_event_archive', + 'physiological_archive' + ]; + + foreach ($archive_table_names as $archive_table_name) { + $filepath = $db->pselectOne( + "SELECT + DISTINCT(FilePath) + FROM $archive_table_name + WHERE PhysiologicalFileID=:PFID", + ['PFID' => $this->_physioFileID] + ); + + $filepath = $dataDir . $filepath; + + $archive_file = new \PharData($filepath); + foreach ($paths as $path) { + $archive_file->addFile($path, basename($path)); + } + + $f = file_get_contents($filepath); + $hash = sodium_crypto_generichash($f); + + //Update database with hash + $db->update( + $archive_table_name, + ['Blake2bHash' => bin2hex($hash)], + ['PhysiologicalFileID' => $this->_physioFileID] + ); + } + } } diff --git a/modules/electrophysiology_browser/php/sessions.class.inc b/modules/electrophysiology_browser/php/sessions.class.inc index ee68788834f..f51b18172c1 100644 --- a/modules/electrophysiology_browser/php/sessions.class.inc +++ b/modules/electrophysiology_browser/php/sessions.class.inc @@ -17,7 +17,6 @@ namespace LORIS\electrophysiology_browser; use \Psr\Http\Message\ServerRequestInterface; use \Psr\Http\Message\ResponseInterface; use LORIS\electrophysiology_browser\Models\ElectrophysioFile; -use LORIS\electrophysiology_browser\Models\ElectrophysioAnnotations; use LORIS\electrophysiology_browser\Models\ElectrophysioEvents; /** @@ -50,7 +49,7 @@ class Sessions extends \NDB_Page { return (($user->hasPermission('electrophysiology_browser_view_allsites') || ($user->hasCenter($this->timepoint->getCenterID()) - && $user->hasPermission('electrophysiology_browser_view_site')) + && $user->hasPermission('electrophysiology_browser_view_site')) ) && $user->hasProject($this->timepoint->getProject()->getId())); } @@ -164,8 +163,8 @@ class Sessions extends \NDB_Page WHERE s.Active = "Y" AND pf.FileType IN ('. - '"bdf", "cnt", "edf", "set", "vhdr", "vsm", "archive"'. - ') ORDER BY pf.SessionID'; + '"bdf", "cnt", "edf", "set", "vhdr", "vsm", "archive"'. + ') ORDER BY pf.SessionID'; $response = []; @@ -292,12 +291,12 @@ class Sessions extends \NDB_Page $fileSummary['summary'], array_map( fn($channel) => - [ - 'name' => $channel.' Channel Count', - 'value' => $physioFileObj->getParameter( - $channel.'ChannelCount' - ), - ], + [ + 'name' => $channel.' Channel Count', + 'value' => $physioFileObj->getParameter( + $channel.'ChannelCount' + ), + ], $channels ) ); @@ -460,13 +459,6 @@ class Sessions extends \NDB_Page $fileSummary['downloads'] = $this->getDownloadLinks($physioFileObj); $fileSummary['chunks_urls'] = $physioFileObj->getChunksURLs(); - $fileSummary['epochsURL'] = $db->pselectOne( - "SELECT FilePath - FROM physiological_event_file - WHERE PhysiologicalFileID=:physioFileID - AND FileType='tsv'", - ['physioFileID' => $physioFileID] - ); $fileOutput = $db->pselectone( 'SELECT pot.OutputTypeName @@ -477,25 +469,20 @@ class Sessions extends \NDB_Page ['PFID' => $physioFileID] ); - // Get the annotation data - $annotations = new ElectrophysioAnnotations( - intval($physioFileID) - ); - $fileSummary['annotations'] = $annotations->getData(); - - // Get the task events data + // Get the task's event data $events = new ElectrophysioEvents( intval($physioFileID) ); $fileSummary['events'] = $events->getData(); - $fileSummary['epochsURL'] = $db->pselectOne( + $fileSummary['epochsURL'] = $db->pselectOne( "SELECT FilePath FROM physiological_event_file WHERE PhysiologicalFileID=:physioFileID AND FileType='tsv'", ['physioFileID' => $physioFileID] ); + $fileSummary['output_type'] = $fileOutput; $fileSummary['splitData'] = $physioFileObj->getSplitData(0); @@ -583,28 +570,37 @@ class Sessions extends \NDB_Page // Metadata $queries = [ - 'physiological_channel' => 'physiological_channel_file', - 'physiological_event_archive' => 'physiological_event_files', - 'physiological_annotation_archive' => 'physiological_annotation_files', - 'physiological_archive' => 'all_files', + 'physiological_electrode' => 'physiological_electrode_file', + 'physiological_coord_system' => 'physiological_coord_system_file', + 'physiological_channel' => 'physiological_channel_file', + 'physiological_event_archive' => 'physiological_event_files', + 'physiological_archive' => 'all_files', ]; $labels = [ - 'physiological_electrode_file' => 'Electrodes', - 'physiological_channel_file' => 'Channels', - 'physiological_event_files' => 'Events', - 'physiological_annotation_files' => 'Annotations', - 'all_files' => 'All Files', + 'physiological_electrode_file' => 'Electrodes', + 'physiological_coord_system_file' => 'Coordinate System', + 'physiological_channel_file' => 'Channels', + 'physiological_event_files' => 'Events', + 'all_files' => 'All Files', ]; foreach ($queries as $query_key => $query_value) { + // TODO: Revisit logic if we plan to support multiple electrode spaces if ($query_key == 'physiological_electrode') { // electrode filepath linked to coordinate system - $query_statement = "SELECT DISTINCT e.FilePath - FROM physiological_coord_system_electrode_rel AS r, - physiological_electrode AS e - WHERE r.PhysiologicalElectrodeID = e.PhysiologicalElectrodeID - AND r.PhysiologicalFileID=:PFID"; + $query_statement = "SELECT DISTINCT (FilePath) + FROM physiological_electrode + JOIN physiological_coord_system_electrode_rel + USING (PhysiologicalElectrodeID) + WHERE PhysiologicalFileID=:PFID"; + } else if ($query_key == 'physiological_coord_system') { + // coordinate system json + $query_statement = "SELECT DISTINCT (FilePath) + FROM physiological_coord_system + JOIN physiological_coord_system_electrode_rel + USING (PhysiologicalCoordSystemID) + WHERE PhysiologicalFileID=:PFID"; } else { // others metadata $query_statement = "SELECT DISTINCT FilePath @@ -628,29 +624,6 @@ class Sessions extends \NDB_Page ]; } } - - // Electrodes - $file_name = 'physiological_electrode_file'; - // TODO: If we plan to support multiple electrode spaces - // the LIMIT logic should be revisited - $query_statement = "SELECT DISTINCT(FilePath) - FROM physiological_electrode - JOIN physiological_coord_system_electrode_rel - USING (PhysiologicalElectrodeID) - WHERE PhysiologicalFileID=:PFID - LIMIT 1"; - $query_statement = $db->pselect( - $query_statement, - ['PFID' => $physioFileID] - ); - - $downloadLinks[$file_name] = [ - 'file' => '', - 'label' => $labels[$file_name], - ]; - if (count($query_statement) > 0) { - $downloadLinks[$file_name]['file'] = $query_statement[0]['FilePath']; - } return $downloadLinks; } diff --git a/modules/electrophysiology_browser/php/split_data.class.inc b/modules/electrophysiology_browser/php/split_data.class.inc index 529fc7d5d66..751cbe1db09 100644 --- a/modules/electrophysiology_browser/php/split_data.class.inc +++ b/modules/electrophysiology_browser/php/split_data.class.inc @@ -5,7 +5,7 @@ use \Psr\Http\Message\ResponseInterface; use LORIS\electrophysiology_browser\Models\ElectrophysioFile; /** - * Contains the Annotations class used for electrophysiological browser + * Contains the Split_Data class used for electrophysiological browser * * PHP Version 7 * @@ -57,4 +57,4 @@ class Split_Data extends \NDB_Page )); } } -} \ No newline at end of file +} diff --git a/modules/electrophysiology_browser/test/TestPlan.md b/modules/electrophysiology_browser/test/TestPlan.md index 034f93803d5..fd42442f0c1 100644 --- a/modules/electrophysiology_browser/test/TestPlan.md +++ b/modules/electrophysiology_browser/test/TestPlan.md @@ -1,45 +1,49 @@ ## Electrophysiology Browser test plan - + ### A. Electrophysiology Browser front page 1. User can load Electrophysiology Browser module front page if and only if user has either permission: - * `electrophysiology_browser_view_site` : _"View all-sites Electrophysiology Browser pages"_ [Automated Testing] - * `electrophysiology_browser_view_allsites` : _"View own site Electrophysiology Browser pages"_ [Automated Testing] -2. User can see other sites Electrophysiology datasets if and only if user has permission `electrophysiology_browser_view_allsites`. User can see only own-site datasets if and only if user has permission `electrophysiology_browser_view_site`. + * `electrophysiology_browser_view_site` : _"View all-sites Electrophysiology Browser pages"_ [Automated Testing] + * `electrophysiology_browser_view_allsites` : _"View own site Electrophysiology Browser pages"_ [Automated Testing] +2. User can see other sites Electrophysiology datasets if and only if user has permission `electrophysiology_browser_view_allsites`. User can see only own-site datasets if and only if user has permission `electrophysiology_browser_view_site`. 3. Test that all Filters work. [Automated Testing] 4. Test Clear Filters button. [Automated Testing] 5. Test column table is sortable by headers. [Automated Testing] 6. Test that Links work and point to correct dataset (raw/derivative). [Manual Testing] -### B. Subpage: Sessions +### B. Subpage: Sessions 7. User can view a session from any site if the user has `electrophysiology_browser_view_allsites` permissions. User can see only own-site session if the user has permission `electrophysiology_browser_view_site`. [Automated Testing] 8. User can view only own-project sessions if they have either `electrophysiology_browser_view_site` or `electrophysiology_browser_view_allsites` permissions. [Automated Testing] 9. Sidebar: Navigation links work. [Automated Testing] 10. Data table display: information displayed looks decently laid out, not garbled. -11. Click each "Download" button (there should be 6). Check: Does the download button work? Does the file that is downloaded have greater than 0kb size? Is a different file downloaded by each button? - * Check that if a session does not have annotation files, the `Annotations` download button is not clickable - * Check that if the session has annotation files, the `Annotations` download button is clickable and downloads the proper files +11. Click each "Download" button (there should be 5). Check: Does the download button work? Does the file that is downloaded have greater than 0kb size? Is a different file downloaded by each button? + * Check that if a session does not have event files, the `Events` download button is not clickable + * Check that if the session has event files, the `Events` download button is clickable and downloads the proper files 12. Test Breadcrumb link back to Electrophysiology Browser. [Automated Testing] +13. Test that if changes have been made to the session's events, the downloaded event files are correctly updated to match [Manual Testing] -### C. Visualization - -13. Follow the [module README extra installation steps](../README.md#installation-requirements-to-use-the-visualization-features) -and make sure the `Signal Viewer panel` displays correctly on the screen. (Documentation: see [react-series-data-viewer README](../jsx/react-series-data-viewer/README.md#user-manual)) -14. Delete `modules/electrophysiology_browser/jsx/react-series-data-viewer/src/protocol-buffers/chunk_pb.js` and set `useEEGBrowserVisualizationComponents` to false to simulate an environment for which the extra installation steps -have not been run yet. -Make sure `make dev` runs without failing, and that except the Signal Viewer panel, all the other components in the page display well. -15. Temporarily deactivate an entry in `physiological_parameter_file` -for a ParameterTypeID IN (SELECT ParameterTypeID from parameter_type WHERE Name = 'electrophysiology_chunked_dataset_path') -and a chosen PhysiologicalFileID to simulate an environment for which the visualization components are not loaded. -Load the corresponding session page and make sure that except the `Signal Viewer panel`, the rest of the page displays well, either with or without the extra installation steps. -16. Test all the buttons on the interface to ensure they perform the action that the [react-series-data-viewer README](../jsx/react-series-data-viewer/README.md#Signal Viewer) states it will perform. -17. Hover over a signal to ensure it responds to being hovered. It should change to a color and its value should be displayed below the signal plot. -18. Ensure that 'Stacked View' and 'Isolate Mode' behave as stateed in the [react-series-data-viewer README](../jsx/react-series-data-viewer/README.md). -19. Ensure that the electrodes on the 'Electrode Map' 2D view are visible and their index can be hovered to reveal their channel name. -20. Ensure that the electrodes on the 'Electrode Map' 3D view are visible and the mesh can be manipulated/rotated with the mouse. +### C. Visualization +14. Follow the [module README extra installation steps](../README.md#installation-requirements-to-use-the-visualization-features) + and make sure the `Signal Viewer panel` displays correctly on the screen. (Documentation: see [react-series-data-viewer README](../jsx/react-series-data-viewer/README.md#user-manual)) +15. Delete `modules/electrophysiology_browser/jsx/react-series-data-viewer/src/protocol-buffers/chunk_pb.js` and set `useEEGBrowserVisualizationComponents` to false to simulate an environment for which the extra installation steps + have not been run yet. + Make sure `make dev` runs without failing, and that except the Signal Viewer panel, all the other components in the page display well. +16. Temporarily deactivate an entry in `physiological_parameter_file` + for a ParameterTypeID IN (SELECT ParameterTypeID from parameter_type WHERE Name = 'electrophysiology_chunked_dataset_path') + and a chosen PhysiologicalFileID to simulate an environment for which the visualization components are not loaded. + Load the corresponding session page and make sure that except the Signal Viewer panel, the rest of the page displays well, either with or without the extra installation steps. +17. Make sure the 'Show Event Panel' opens the 'Event Panel' and it can be closed both via its close button and the 'Hide Event Panel' button. +18. Make sure the text fields can not be modified (support planned in future) . +19. Make sure HED tags belonging to an individual event can be added and deleted from that individual event. The selectable tags should only be SCORE 'Artifact's. +20. Make sure the 'Dataset Tag Viewer' can be opened with the 'Open Dataset Tag Viewer' button and the selectable fields are properly populated. +21. Test all the buttons on the interface to ensure they perform the action that the [react-series-data-viewer README](../jsx/react-series-data-viewer/README.md#Signal Viewer) states it will perform. +22. Hover over a signal to ensure it responds to being hovered. It should change to a color and its value should be displayed below the signal plot. +23. Ensure that 'Stacked View' and 'Isolate Mode' behave as stateed in the [react-series-data-viewer README](../jsx/react-series-data-viewer/README.md). +24. Ensure that the electrodes on the 'Electrode Map' 2D view are visible and their index can be hovered to reveal their channel name. +25. Ensure that the electrodes on the 'Electrode Map' 3D view are visible and the mesh can be manipulated/rotated with the mouse. -_For extra credit: Verify LORIS Menu permissions_ +_For extra credit: Verify LORIS Menu permissions_ User can view the top-level LORIS Menu _Electrophysiology_ and Menu item : _Electrophysiology Browser_ if and only if user has either permission: - * `electrophysiology_browser_view_site` : _"View all-sites Electrophysiology Browser pages"_ - * `electrophysiology_browser_view_allsites` : _"View own site Electrophysiology Browser pages"_ +* `electrophysiology_browser_view_site` : _"View all-sites Electrophysiology Browser pages"_ +* `electrophysiology_browser_view_allsites` : _"View own site Electrophysiology Browser pages"_ diff --git a/raisinbread/RB_files/RB_physiological_annotation_archive.sql b/raisinbread/RB_files/RB_physiological_annotation_archive.sql deleted file mode 100644 index 4b151ea9537..00000000000 --- a/raisinbread/RB_files/RB_physiological_annotation_archive.sql +++ /dev/null @@ -1,5 +0,0 @@ -SET FOREIGN_KEY_CHECKS=0; -TRUNCATE TABLE `physiological_annotation_archive`; -LOCK TABLES `physiological_annotation_archive` WRITE; -UNLOCK TABLES; -SET FOREIGN_KEY_CHECKS=1; diff --git a/raisinbread/RB_files/RB_physiological_annotation_file.sql b/raisinbread/RB_files/RB_physiological_annotation_file.sql deleted file mode 100644 index a80a80a2a37..00000000000 --- a/raisinbread/RB_files/RB_physiological_annotation_file.sql +++ /dev/null @@ -1,5 +0,0 @@ -SET FOREIGN_KEY_CHECKS=0; -TRUNCATE TABLE `physiological_annotation_file`; -LOCK TABLES `physiological_annotation_file` WRITE; -UNLOCK TABLES; -SET FOREIGN_KEY_CHECKS=1; diff --git a/raisinbread/RB_files/RB_physiological_annotation_file_type.sql b/raisinbread/RB_files/RB_physiological_annotation_file_type.sql deleted file mode 100644 index 4a1f2f664c4..00000000000 --- a/raisinbread/RB_files/RB_physiological_annotation_file_type.sql +++ /dev/null @@ -1,7 +0,0 @@ -SET FOREIGN_KEY_CHECKS=0; -TRUNCATE TABLE `physiological_annotation_file_type`; -LOCK TABLES `physiological_annotation_file_type` WRITE; -INSERT INTO `physiological_annotation_file_type` (`FileType`, `Description`) VALUES ('json','JSON File Type, metadata for annotations'); -INSERT INTO `physiological_annotation_file_type` (`FileType`, `Description`) VALUES ('tsv','TSV File Type, contains information about each annotation'); -UNLOCK TABLES; -SET FOREIGN_KEY_CHECKS=1; diff --git a/raisinbread/RB_files/RB_physiological_annotation_instance.sql b/raisinbread/RB_files/RB_physiological_annotation_instance.sql deleted file mode 100644 index 7a5d15a8e08..00000000000 --- a/raisinbread/RB_files/RB_physiological_annotation_instance.sql +++ /dev/null @@ -1,5 +0,0 @@ -SET FOREIGN_KEY_CHECKS=0; -TRUNCATE TABLE `physiological_annotation_instance`; -LOCK TABLES `physiological_annotation_instance` WRITE; -UNLOCK TABLES; -SET FOREIGN_KEY_CHECKS=1; diff --git a/raisinbread/RB_files/RB_physiological_annotation_label.sql b/raisinbread/RB_files/RB_physiological_annotation_label.sql deleted file mode 100644 index 64168d79151..00000000000 --- a/raisinbread/RB_files/RB_physiological_annotation_label.sql +++ /dev/null @@ -1,28 +0,0 @@ -SET FOREIGN_KEY_CHECKS=0; -TRUNCATE TABLE `physiological_annotation_label`; -LOCK TABLES `physiological_annotation_label` WRITE; -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (1,NULL,'artifact','artifactual data'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (2,NULL,'motion','motion related artifact'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (3,NULL,'flux_jump','artifactual data due to flux jump'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (4,NULL,'line_noise','artifactual data due to line noise (e.g., 50Hz)'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (5,NULL,'muscle','artifactual data due to muscle activity'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (6,NULL,'epilepsy_interictal','period deemed interictal'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (7,NULL,'epilepsy_preictal','onset of preictal state prior to onset of epilepsy'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (8,NULL,'epilepsy_seizure','onset of epilepsy'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (9,NULL,'epilepsy_postictal','postictal seizure period'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (10,NULL,'epileptiform','unspecified epileptiform activity'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (11,NULL,'epileptiform_single','a single epileptiform graphoelement (including possible slow wave)'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (12,NULL,'epileptiform_run','a run of one or more epileptiform graphoelements'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (13,NULL,'eye_blink','Eye blink'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (14,NULL,'eye_movement','Smooth Pursuit / Saccadic eye movement'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (15,NULL,'eye_fixation','Fixation onset'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (16,NULL,'sleep_N1','sleep stage N1'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (17,NULL,'sleep_N2','sleep stage N2'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (18,NULL,'sleep_N3','sleep stage N3'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (19,NULL,'sleep_REM','REM sleep'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (20,NULL,'sleep_wake','sleep stage awake'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (21,NULL,'sleep_spindle','sleep spindle'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (22,NULL,'sleep_k-complex','sleep K-complex'); -INSERT INTO `physiological_annotation_label` (`AnnotationLabelID`, `AnnotationFileID`, `LabelName`, `LabelDescription`) VALUES (23,NULL,'scorelabeled','a global label indicating that the EEG has been annotated with SCORE.'); -UNLOCK TABLES; -SET FOREIGN_KEY_CHECKS=1; diff --git a/raisinbread/RB_files/RB_physiological_annotation_parameter.sql b/raisinbread/RB_files/RB_physiological_annotation_parameter.sql deleted file mode 100644 index 812f2463f90..00000000000 --- a/raisinbread/RB_files/RB_physiological_annotation_parameter.sql +++ /dev/null @@ -1,5 +0,0 @@ -SET FOREIGN_KEY_CHECKS=0; -TRUNCATE TABLE `physiological_annotation_parameter`; -LOCK TABLES `physiological_annotation_parameter` WRITE; -UNLOCK TABLES; -SET FOREIGN_KEY_CHECKS=1; diff --git a/raisinbread/RB_files/RB_physiological_annotation_rel.sql b/raisinbread/RB_files/RB_physiological_annotation_rel.sql deleted file mode 100644 index 362e9d22592..00000000000 --- a/raisinbread/RB_files/RB_physiological_annotation_rel.sql +++ /dev/null @@ -1,5 +0,0 @@ -SET FOREIGN_KEY_CHECKS=0; -TRUNCATE TABLE `physiological_annotation_rel`; -LOCK TABLES `physiological_annotation_rel` WRITE; -UNLOCK TABLES; -SET FOREIGN_KEY_CHECKS=1;