From 6146f72526bc215b3c33eb170d84d24cc981d614 Mon Sep 17 00:00:00 2001 From: Jefferson Casimir Date: Tue, 30 Jan 2024 17:00:21 -0500 Subject: [PATCH 01/23] Remove Annotations in favor of Events --- SQL/0000-00-05-ElectrophysiologyTables.sql | 99 --- ...ysiological-Events-Replace-Annotations.sql | 9 + modules/electrophysiology_browser/README.md | 14 +- .../css/electrophysiology_browser.css | 74 ++- .../help/sessions.md | 4 +- .../jsx/components/DownloadPanel.js | 57 +- .../jsx/electrophysiologySessionView.js | 134 ++-- .../src/eeglab/EEGLabSeriesProvider.tsx | 121 ++-- .../src/series/components/AnnotationForm.tsx | 237 +++---- .../src/series/components/EventManager.tsx | 233 +++---- .../src/series/components/Form.js | 46 +- .../src/series/components/SeriesCursor.tsx | 4 +- .../src/series/components/SeriesRenderer.tsx | 601 ++++++++--------- .../src/series/store/logic/filterEpochs.tsx | 28 +- .../src/series/store/state/dataset.tsx | 9 +- .../src/series/store/state/montage.tsx | 16 +- .../src/series/store/types.tsx | 26 +- .../src/vector/index.tsx | 2 + .../php/annotations.class.inc | 144 ----- .../php/events.class.inc | 165 +++++ .../models/electrophysioannotations.class.inc | 607 ------------------ .../php/models/electrophysioevents.class.inc | 388 ++++++++++- .../php/sessions.class.inc | 91 +-- .../php/split_data.class.inc | 4 +- .../test/TestPlan.md | 58 +- 25 files changed, 1449 insertions(+), 1722 deletions(-) create mode 100644 SQL/New_patches/2024-01-29-Physiological-Events-Replace-Annotations.sql delete mode 100644 modules/electrophysiology_browser/php/annotations.class.inc create mode 100644 modules/electrophysiology_browser/php/events.class.inc delete mode 100644 modules/electrophysiology_browser/php/models/electrophysioannotations.class.inc diff --git a/SQL/0000-00-05-ElectrophysiologyTables.sql b/SQL/0000-00-05-ElectrophysiologyTables.sql index 3860a11e1ad..fca3cf01fc5 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, 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..d48bbc8d686 --- /dev/null +++ b/SQL/New_patches/2024-01-29-Physiological-Events-Replace-Annotations.sql @@ -0,0 +1,9 @@ +-- 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; + 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..a8fcafeb692 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,8 @@ 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 +30,8 @@ declare global { type CProps = { chunksURL: string, electrodesURL: string, + coordSystemURL: string, events: EventMetadata, - annotations: AnnotationMetadata, physioFileID: number, limit: number, children: React.ReactNode, @@ -63,8 +63,8 @@ class EEGLabSeriesProvider extends Component { const { chunksURL, electrodesURL, + coordSystemURL, events, - annotations, physioFileID, limit, } = props; @@ -114,62 +114,60 @@ 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.extra_columns).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, + trial_type: 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])) { + indices.push(index); // Full-recording events not visible by default + } + return indices; + }, []), + columnVisibility: [], + })); + }); Promise.race(racers(fetchText, electrodesURL)) .then((text) => { @@ -188,6 +186,23 @@ 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..ba2651673ab 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); - }; + /** * */ @@ -181,17 +162,16 @@ const AnnotationForm = ({ */ const handleReset = () => { // Clear all fields - setEvent(['', '']); - setTimeSelection([null, null]); - setLabel(''); - setComment(''); + // setEvent(['', '']); + // setTimeSelection([null, null]); + // setLabel(''); }; /** * */ const handleDelete = () => { - setIsDeleted(true); + // setIsDeleted(true); }; // Submit @@ -213,7 +193,7 @@ const AnnotationForm = ({ } const url = window.location.origin + - '/electrophysiology_browser/annotations/'; + '/electrophysiology_browser/events/'; // get duration of annotation let startTime = event[0]; @@ -229,9 +209,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 +220,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 +231,28 @@ 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 newAnnotation : EpochType = { + onset: parseFloat(data.instance.Onset), + duration: parseFloat(data.instance.Duration), + type: 'Event', + label: data.instance.EventValue, // Unused + value: data.instance.EventValue, + trial_type: data.instance.TrialType, + properties: data.extra_columns, + 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.trial_type + ? 'trial_type' + : 'value' + } + + + } + value={currentAnnotation ? currentAnnotation.label : ""} + required={true} + readonly={true} + /> - - +
+ { + currentAnnotation && currentAnnotation.properties.length > 0 && ( + <> + +
+ { + currentAnnotation.properties.map((property) => { + return ( + + ); + }) + } +
+ + ) + } +
+ - {currentAnnotation && - - } + {/*{currentAnnotation &&*/} + {/* */} + {/* Delete*/} + {/* */} + {/*}*/} {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..5d038790f89 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 === 'annotationForm'*/} + {/* ? setRightPanel(null)*/} + {/* : setRightPanel('annotationForm');*/} + {/* setCurrentAnnotation(null);*/} + {/* }}*/} + {/* >*/} + {/* {rightPanel === 'annotationForm'*/} + {/* ? 'Close Annotation Form'*/} + {/* : 'Add Annotation'*/} + {/* }*/} + {/* */} + {/*}*/}
{rightPanel &&
{rightPanel === 'annotationForm' && - + } - {rightPanel === 'eventList' && } - {rightPanel === 'annotationList' && } + {rightPanel === 'eventList' && }
} @@ -1342,9 +1296,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 +1350,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..e53a556674e 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..34e0f043b6d 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, + trial_type: 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[] + extra_columns: 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/annotations.class.inc deleted file mode 100644 index 0b0ea96aac4..00000000000 --- a/modules/electrophysiology_browser/php/annotations.class.inc +++ /dev/null @@ -1,144 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 - * @link https://www.github.com/aces/Loris/ - */ -class Annotations extends \NDB_Page -{ - /** - * Handle how to operate all the files. - * GET method gets a file. - * - * @param ServerRequestInterface $request The incoming PSR7 request - * - * @return ResponseInterface The outgoing PSR7 response - */ - public function handle(ServerRequestInterface $request): ResponseInterface - { - $user = $request->getAttribute('user'); - $db = $request->getAttribute('loris')->getDatabaseConnection(); - - switch ($request->getMethod()) { - case 'GET': - $parameters = $request->getQueryParams(); - $sessionID = $db->pselectOne( - 'SELECT SessionID - FROM physiological_file - WHERE PhysiologicalFileID=:PFID', - ['PFID' => $parameters['physioFileID']] - ); - - $timepoint = \NDB_Factory::singleton()->timepoint( - new \SessionID(strval($sessionID)) - ); - - if (!(($user->hasPermission('electrophysiology_browser_view_allsites') - || ($user->hasCenter($timepoint->getCenterID()) - && $user->hasPermission('electrophysiology_browser_view_site'))) - && $user->hasProject($timepoint->getProject()->getId())) - ) { - return (new \LORIS\Http\Response\JSON\Unauthorized()); - } - - if (!isset($parameters['physioFileID']) - || !isset($parameters['filePath']) - ) { - return (new \LORIS\Http\Response\JSON\BadRequest()); - } - - $physioFileID = intval($parameters['physioFileID']); - (new ElectrophysioAnnotations($physioFileID))->updateFiles(); - - $config = \NDB_Factory::singleton()->config(); - $downloadpath = \Utility::appendForwardSlash( - $config->getSetting("dataDirBasepath") - ); - - $file = $parameters['filePath'] ?? null; - $filename = urldecode(basename($file)); - $path = dirname($file); - - $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') - ) { - return (new \LORIS\Http\Response\JSON\Unauthorized()); - } - - if (!isset($parameters['physioFileID']) - || !isset($parameters['instance_id']) - ) { - 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') - ) { - return (new \LORIS\Http\Response\JSON\Unauthorized()); - } - - if (!isset($parameters['physioFileID'])) { - 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']] - ); - } - - return (new \LORIS\Http\Response\JSON\OK( - ['instance_id' => $instance_id] - )); - default: - return (new \LORIS\Http\Response\JSON\MethodNotAllowed( - ["GET", "DELETE", "POST"] - )); - } - } -} diff --git a/modules/electrophysiology_browser/php/events.class.inc b/modules/electrophysiology_browser/php/events.class.inc new file mode 100644 index 00000000000..fe9cdf6c702 --- /dev/null +++ b/modules/electrophysiology_browser/php/events.class.inc @@ -0,0 +1,165 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Events extends \NDB_Page +{ + /** + * Handle how to operate all the files. + * GET method gets a file. + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface The outgoing PSR7 response + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $user = $request->getAttribute('user'); + $db = $request->getAttribute('loris')->getDatabaseConnection(); + + switch ($request->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 to 1G + + $parameters = $request->getQueryParams(); + $sessionID = $db->pselectOne( + 'SELECT SessionID + FROM physiological_file + WHERE PhysiologicalFileID=:PFID', + ['PFID' => $parameters['physioFileID']] + ); + + $timepoint = \NDB_Factory::singleton()->timepoint( + new \SessionID(strval($sessionID)) + ); + + if (!(($user->hasPermission('electrophysiology_browser_view_allsites') + || ($user->hasCenter($timepoint->getCenterID()) + && $user->hasPermission('electrophysiology_browser_view_site'))) + && $user->hasProject($timepoint->getProject()->getId())) + ) { + return (new \LORIS\Http\Response\JSON\Unauthorized()); + } + + if (!isset($parameters['physioFileID']) + || !isset($parameters['filePath']) + ) { + return (new \LORIS\Http\Response\JSON\BadRequest()); + } + + $physioFileID = intval($parameters['physioFileID']); + (new ElectrophysioEvents($physioFileID))->updateFiles(); + + $config = \NDB_Factory::singleton()->config(); + $downloadpath = \Utility::appendForwardSlash( + $config->getSetting("dataDirBasepath") + ); + + $file = $parameters['filePath'] ?? null; + $filename = urldecode(basename($file)); + $path = dirname($file); + + $downloader = new \LORIS\FilesDownloadHandler( + new \SPLFileInfo($downloadpath . $path) + ); + + return $downloader->handle( + $request->withAttribute('filename', $filename) + ); + case 'DELETE': + $parameters = json_decode((string)$request->getBody(), true); + $sessionID = $db->pselectOne( + 'SELECT SessionID + FROM physiological_file + WHERE PhysiologicalFileID=:PFID', + ['PFID' => $parameters['physioFileID']] + ); + $timepoint = \NDB_Factory::singleton()->timepoint( + new \SessionID(strval($sessionID)) + ); + $projectName = $timepoint->getProject()->getName(); + + if (!$user->hasPermission('electrophysiology_browser_edit_annotations')) { + return (new \LORIS\Http\Response\JSON\Unauthorized()); + } + + if (!isset($parameters['physioFileID']) + || !isset($parameters['instance_id']) + ) { + return (new \LORIS\Http\Response\JSON\BadRequest()); + } + +// (new ElectrophysioEvents(intval($parameters['physioFileID']))) +// ->deleteEvent(intval($parameters['instance_id'])); + + return (new \LORIS\Http\Response\JSON\OK()); + case 'POST': + // TODO: Better failure reporting + $parameters = json_decode((string)$request->getBody(), true); + $sessionID = $db->pselectOne( + 'SELECT SessionID + FROM physiological_file + WHERE PhysiologicalFileID=:PFID', + ['PFID' => $parameters['physioFileID']] + ); + $timepoint = \NDB_Factory::singleton()->timepoint( + new \SessionID(strval($sessionID)) + ); + $projectName = $timepoint->getProject()->getName(); + + if (!$user->hasPermission('electrophysiology_browser_edit_annotations')) { + return (new \LORIS\Http\Response\JSON\Unauthorized()); + } + + if (!isset($parameters['physioFileID']) || !isset($parameters['request_type'])) { + return (new \LORIS\Http\Response\JSON\BadRequest()); + } + + switch ($parameters['request_type']) { + case 'event_update': + $instance_data = $parameters['instance']; + // $metadata = $parameters['metadata']; + // TODO: Figure out a 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()); + } + 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..d953745b53b 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, + 'extra_columns' => $extraColumns, ]; } /** - * Get data for the Electrophysiological file annotations + * Get data for the Electrophysiological events * * @return array The data array */ @@ -57,7 +74,356 @@ 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(); + + $sessionID = $db->pselectOne( + 'SELECT SessionID + FROM physiological_file + WHERE PhysiologicalFileID=:PFID', + ['PFID' => $this->_physioFileID] + ); + $timepoint = \NDB_Factory::singleton()->timepoint( + new \SessionID(strval($sessionID)) + ); + + 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] + ); + + //Get data from POST request + $metadata = [ + 'Description' => $metadata['description'], + 'Sources' => $metadata['sources'], + 'Author' => $metadata['author'] + ]; + + if (is_null($instance_id)) { + // TODO: Support Instance INSERT + $instance_id = 1; + } + + $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], + 'extra_columns' => $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 + $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 = $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] + ); + + $projectID = intval($db->pselectOne( + 'SELECT ProjectID FROM session AS s WHERE s.ID = ( + SELECT SessionID FROM physiological_file + WHERE PhysiologicalFileID=:PFID + )', + ['PFID' => $this->_physioFileID] + )); + + + $tsvPath = count($tsv) > 0 ? $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(); + + $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'] + ); + $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..ab44efe09dd 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_electrode' => 'physiological_electrode_file', + 'physiological_coord_system' => 'physiological_coord_system_file', 'physiological_channel' => 'physiological_channel_file', 'physiological_event_archive' => 'physiological_event_files', - 'physiological_annotation_archive' => 'physiological_annotation_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"_ From e64d2ef4172959a3f3a81ae0f7d7fc18319056d0 Mon Sep 17 00:00:00 2001 From: Jefferson Casimir Date: Tue, 30 Jan 2024 18:43:35 -0500 Subject: [PATCH 02/23] Removed annotation inserts --- SQL/0000-00-05-ElectrophysiologyTables.sql | 36 ---------------------- 1 file changed, 36 deletions(-) diff --git a/SQL/0000-00-05-ElectrophysiologyTables.sql b/SQL/0000-00-05-ElectrophysiologyTables.sql index fca3cf01fc5..3e08c05e2e9 100644 --- a/SQL/0000-00-05-ElectrophysiologyTables.sql +++ b/SQL/0000-00-05-ElectrophysiologyTables.sql @@ -535,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.'); - From 8932371e4658e215d6896d59320951b98a4ec81c Mon Sep 17 00:00:00 2001 From: Jefferson Casimir Date: Wed, 31 Jan 2024 09:50:46 -0500 Subject: [PATCH 03/23] Satisfy linter --- .../php/events.class.inc | 240 +++++++++--------- .../php/models/electrophysioevents.class.inc | 37 +-- .../php/sessions.class.inc | 10 +- 3 files changed, 146 insertions(+), 141 deletions(-) diff --git a/modules/electrophysiology_browser/php/events.class.inc b/modules/electrophysiology_browser/php/events.class.inc index fe9cdf6c702..8528ef39bf9 100644 --- a/modules/electrophysiology_browser/php/events.class.inc +++ b/modules/electrophysiology_browser/php/events.class.inc @@ -30,136 +30,136 @@ class Events extends \NDB_Page public function handle(ServerRequestInterface $request): ResponseInterface { $user = $request->getAttribute('user'); - $db = $request->getAttribute('loris')->getDatabaseConnection(); + $db = $request->getAttribute('loris')->getDatabaseConnection(); switch ($request->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 to 1G - - $parameters = $request->getQueryParams(); - $sessionID = $db->pselectOne( - 'SELECT SessionID + 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 to 1G + + $parameters = $request->getQueryParams(); + $sessionID = $db->pselectOne( + 'SELECT SessionID FROM physiological_file WHERE PhysiologicalFileID=:PFID', - ['PFID' => $parameters['physioFileID']] - ); - - $timepoint = \NDB_Factory::singleton()->timepoint( - new \SessionID(strval($sessionID)) - ); - - if (!(($user->hasPermission('electrophysiology_browser_view_allsites') - || ($user->hasCenter($timepoint->getCenterID()) - && $user->hasPermission('electrophysiology_browser_view_site'))) - && $user->hasProject($timepoint->getProject()->getId())) - ) { - return (new \LORIS\Http\Response\JSON\Unauthorized()); - } - - if (!isset($parameters['physioFileID']) - || !isset($parameters['filePath']) - ) { - return (new \LORIS\Http\Response\JSON\BadRequest()); - } - - $physioFileID = intval($parameters['physioFileID']); - (new ElectrophysioEvents($physioFileID))->updateFiles(); - - $config = \NDB_Factory::singleton()->config(); - $downloadpath = \Utility::appendForwardSlash( - $config->getSetting("dataDirBasepath") - ); - - $file = $parameters['filePath'] ?? null; - $filename = urldecode(basename($file)); - $path = dirname($file); - - $downloader = new \LORIS\FilesDownloadHandler( - new \SPLFileInfo($downloadpath . $path) - ); - - return $downloader->handle( - $request->withAttribute('filename', $filename) - ); - case 'DELETE': - $parameters = json_decode((string)$request->getBody(), true); - $sessionID = $db->pselectOne( - 'SELECT SessionID + ['PFID' => $parameters['physioFileID']] + ); + + $timepoint = \NDB_Factory::singleton()->timepoint( + new \SessionID(strval($sessionID)) + ); + + if (!(($user->hasPermission('electrophysiology_browser_view_allsites') + || ($user->hasCenter($timepoint->getCenterID()) + && $user->hasPermission('electrophysiology_browser_view_site'))) + && $user->hasProject($timepoint->getProject()->getId())) + ) { + return (new \LORIS\Http\Response\JSON\Unauthorized()); + } + + if (!isset($parameters['physioFileID']) + || !isset($parameters['filePath']) + ) { + return (new \LORIS\Http\Response\JSON\BadRequest()); + } + + $physioFileID = intval($parameters['physioFileID']); + (new ElectrophysioEvents($physioFileID))->updateFiles(); + + $config = \NDB_Factory::singleton()->config(); + $downloadpath = \Utility::appendForwardSlash( + $config->getSetting("dataDirBasepath") + ); + + $file = $parameters['filePath'] ?? null; + $filename = urldecode(basename($file)); + $path = dirname($file); + + $downloader = new \LORIS\FilesDownloadHandler( + new \SPLFileInfo($downloadpath . $path) + ); + + return $downloader->handle( + $request->withAttribute('filename', $filename) + ); + case 'DELETE': + $parameters = json_decode((string)$request->getBody(), true); + $sessionID = $db->pselectOne( + 'SELECT SessionID FROM physiological_file WHERE PhysiologicalFileID=:PFID', - ['PFID' => $parameters['physioFileID']] - ); - $timepoint = \NDB_Factory::singleton()->timepoint( - new \SessionID(strval($sessionID)) - ); - $projectName = $timepoint->getProject()->getName(); - - if (!$user->hasPermission('electrophysiology_browser_edit_annotations')) { - return (new \LORIS\Http\Response\JSON\Unauthorized()); - } - - if (!isset($parameters['physioFileID']) - || !isset($parameters['instance_id']) - ) { - return (new \LORIS\Http\Response\JSON\BadRequest()); - } - -// (new ElectrophysioEvents(intval($parameters['physioFileID']))) -// ->deleteEvent(intval($parameters['instance_id'])); - - return (new \LORIS\Http\Response\JSON\OK()); - case 'POST': - // TODO: Better failure reporting - $parameters = json_decode((string)$request->getBody(), true); - $sessionID = $db->pselectOne( - 'SELECT SessionID + ['PFID' => $parameters['physioFileID']] + ); + $timepoint = \NDB_Factory::singleton()->timepoint( + new \SessionID(strval($sessionID)) + ); + $projectName = $timepoint->getProject()->getName(); + + if (!$user->hasPermission('electrophysiology_browser_edit_annotations')) { + return (new \LORIS\Http\Response\JSON\Unauthorized()); + } + + if (!isset($parameters['physioFileID']) + || !isset($parameters['instance_id']) + ) { + return (new \LORIS\Http\Response\JSON\BadRequest()); + } + + // (new ElectrophysioEvents(intval($parameters['physioFileID']))) + // ->deleteEvent(intval($parameters['instance_id'])); + + return (new \LORIS\Http\Response\JSON\OK()); + case 'POST': + // TODO: Better failure reporting + $parameters = json_decode((string)$request->getBody(), true); + $sessionID = $db->pselectOne( + 'SELECT SessionID FROM physiological_file WHERE PhysiologicalFileID=:PFID', - ['PFID' => $parameters['physioFileID']] - ); - $timepoint = \NDB_Factory::singleton()->timepoint( - new \SessionID(strval($sessionID)) - ); - $projectName = $timepoint->getProject()->getName(); - - if (!$user->hasPermission('electrophysiology_browser_edit_annotations')) { - return (new \LORIS\Http\Response\JSON\Unauthorized()); - } - - if (!isset($parameters['physioFileID']) || !isset($parameters['request_type'])) { - return (new \LORIS\Http\Response\JSON\BadRequest()); - } - - switch ($parameters['request_type']) { - case 'event_update': - $instance_data = $parameters['instance']; - // $metadata = $parameters['metadata']; - // TODO: Figure out a 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()); + ['PFID' => $parameters['physioFileID']] + ); + $timepoint = \NDB_Factory::singleton()->timepoint( + new \SessionID(strval($sessionID)) + ); + $projectName = $timepoint->getProject()->getName(); + + if (!$user->hasPermission('electrophysiology_browser_edit_annotations')) { + return (new \LORIS\Http\Response\JSON\Unauthorized()); + } + + if (!isset($parameters['physioFileID']) || !isset($parameters['request_type'])) { + return (new \LORIS\Http\Response\JSON\BadRequest()); + } + + switch ($parameters['request_type']) { + case 'event_update': + $instance_data = $parameters['instance']; + // $metadata = $parameters['metadata']; + // TODO: Figure out a 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()); + } default: - return (new \LORIS\Http\Response\JSON\MethodNotAllowed( - ["GET", "DELETE", "POST"] - )); + return (new \LORIS\Http\Response\JSON\MethodNotAllowed( + ["GET", "DELETE", "POST"] + )); } } } diff --git a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc index d953745b53b..e6b3d8b0d76 100644 --- a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc +++ b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc @@ -37,9 +37,12 @@ class ElectrophysioEvents ['PFID' => $this->_physioFileID] ); - $taskEventIDs = array_map(function($taskEvent) { - return $taskEvent['PhysiologicalTaskEventID']; - }, $taskEvents); + $taskEventIDs = array_map( + function ($taskEvent) { + return $taskEvent['PhysiologicalTaskEventID']; + }, + $taskEvents + ); $taskEventIDs = array_combine( array_map('intval', array_keys($taskEventIDs)), @@ -54,11 +57,12 @@ class ElectrophysioEvents count($taskEventIDs) > 0 ? join(',', $taskEventIDs) : 'null' - ) . ')', [] + ) . ')', + [] ); $this->_data = [ - 'instances' => $taskEvents, + 'instances' => $taskEvents, 'extra_columns' => $extraColumns, ]; } @@ -127,8 +131,8 @@ class ElectrophysioEvents } $instance = [ - 'Onset' => $instance_data['onset'], - 'Duration' => $instance_data['duration'], + 'Onset' => $instance_data['onset'], + 'Duration' => $instance_data['duration'], ]; // TODO: Support Event Instance Insert @@ -161,7 +165,7 @@ class ElectrophysioEvents ); return [ - 'instance' => $taskEvent[0], + 'instance' => $taskEvent[0], 'extra_columns' => $extraColumns, ]; } @@ -228,16 +232,17 @@ class ElectrophysioEvents ['PFID' => $this->_physioFileID] ); - $projectID = intval($db->pselectOne( - 'SELECT ProjectID FROM session AS s WHERE s.ID = ( + $projectID = intval( + $db->pselectOne( + 'SELECT ProjectID FROM session AS s WHERE s.ID = ( SELECT SessionID FROM physiological_file WHERE PhysiologicalFileID=:PFID )', - ['PFID' => $this->_physioFileID] - )); - + ['PFID' => $this->_physioFileID] + ) + ); - $tsvPath = count($tsv) > 0 ? $dataDir . $tsv[0]['filePath'] : ''; + $tsvPath = count($tsv) > 0 ? $dataDir . $tsv[0]['filePath'] : ''; // Update files if files updated before database updated if ($tsv[0]['lastWritten'] <= $tsv[0]['lastUpdate']) { @@ -266,14 +271,14 @@ class ElectrophysioEvents ['PFID' => $this->_physioFileID] ); - # TODO: Make columns more dynamic + // TODO: Make columns more dynamic $tsvEntries = [ 'onset', 'duration', 'sample', 'trial_type', 'response_time', 'value' ]; foreach ($columnNames as $columnName) { $tsvEntries[] = $columnName['PropertyName']; } -// $tsvEntries[] = 'HED'; + // $tsvEntries[] = 'HED'; // Add columns names $columns = implode("\t", $tsvEntries); diff --git a/modules/electrophysiology_browser/php/sessions.class.inc b/modules/electrophysiology_browser/php/sessions.class.inc index ab44efe09dd..f51b18172c1 100644 --- a/modules/electrophysiology_browser/php/sessions.class.inc +++ b/modules/electrophysiology_browser/php/sessions.class.inc @@ -570,11 +570,11 @@ class Sessions extends \NDB_Page // Metadata $queries = [ - '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', + '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 = [ From ff95e62ffc9b32659a5464e58bfb9fa96d8f4627 Mon Sep 17 00:00:00 2001 From: Jefferson Casimir Date: Wed, 31 Jan 2024 10:16:02 -0500 Subject: [PATCH 04/23] Satisfy linter --- .../php/events.class.inc | 29 ++++++++++++------- .../php/models/electrophysioevents.class.inc | 10 ++++--- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/modules/electrophysiology_browser/php/events.class.inc b/modules/electrophysiology_browser/php/events.class.inc index 8528ef39bf9..4571f453f1e 100644 --- a/modules/electrophysiology_browser/php/events.class.inc +++ b/modules/electrophysiology_browser/php/events.class.inc @@ -35,8 +35,8 @@ class Events extends \NDB_Page switch ($request->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 to 1G + // 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( @@ -96,7 +96,10 @@ class Events extends \NDB_Page ); $projectName = $timepoint->getProject()->getName(); - if (!$user->hasPermission('electrophysiology_browser_edit_annotations')) { + if (!$user->hasPermission( + 'electrophysiology_browser_edit_annotations' + ) + ) { return (new \LORIS\Http\Response\JSON\Unauthorized()); } @@ -106,8 +109,8 @@ class Events extends \NDB_Page return (new \LORIS\Http\Response\JSON\BadRequest()); } - // (new ElectrophysioEvents(intval($parameters['physioFileID']))) - // ->deleteEvent(intval($parameters['instance_id'])); + // (new ElectrophysioEvents(intval($parameters['physioFileID']))) + // ->deleteEvent(intval($parameters['instance_id'])); return (new \LORIS\Http\Response\JSON\OK()); case 'POST': @@ -124,11 +127,16 @@ class Events extends \NDB_Page ); $projectName = $timepoint->getProject()->getName(); - if (!$user->hasPermission('electrophysiology_browser_edit_annotations')) { + if (!$user->hasPermission( + 'electrophysiology_browser_edit_annotations' + ) + ) { return (new \LORIS\Http\Response\JSON\Unauthorized()); } - if (!isset($parameters['physioFileID']) || !isset($parameters['request_type'])) { + if (!isset($parameters['physioFileID']) + || !isset($parameters['request_type']) + ) { return (new \LORIS\Http\Response\JSON\BadRequest()); } @@ -136,7 +144,7 @@ class Events extends \NDB_Page case 'event_update': $instance_data = $parameters['instance']; // $metadata = $parameters['metadata']; - // TODO: Figure out a better description modeled on other derivatives + // TODO: Figure out better description modeled on other derivatives $metadata = [ 'description' => 'An event', 'sources' => 'EEGNet LORIS', @@ -146,8 +154,9 @@ class Events extends \NDB_Page $instance_id = $parameters['instance_id'] ? intval($parameters['instance_id']) : null; - $updated_instance = (new ElectrophysioEvents(intval($parameters['physioFileID']))) - ->update($instance_data, $metadata, $instance_id); + $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( diff --git a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc index e6b3d8b0d76..1f268ca4eee 100644 --- a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc +++ b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc @@ -312,11 +312,12 @@ class ElectrophysioEvents $taskEventID = $instance['PhysiologicalTaskEventID']; // Get instance's extra columns - $instanceExtraColumns = - array_filter( + $instanceExtraColumns + = array_filter( array_values($extraColumns), function ($column) use ($taskEventID) { - return $column['PhysiologicalTaskEventID'] == $taskEventID; + return + $column['PhysiologicalTaskEventID'] == $taskEventID; } ); @@ -324,7 +325,8 @@ class ElectrophysioEvents $column = array_filter( array_values($instanceExtraColumns), function ($col) use ($columnName) { - return $col['PropertyName'] == $columnName['PropertyName']; + return + $col['PropertyName'] == $columnName['PropertyName']; } ); From 94c3b3e68235d24e19bb6304c23f6a006c3b3079 Mon Sep 17 00:00:00 2001 From: Jefferson Casimir Date: Wed, 31 Jan 2024 10:48:23 -0500 Subject: [PATCH 05/23] Satisfy phan --- .../php/events.class.inc | 20 ---------------- .../php/models/electrophysioevents.class.inc | 23 ++----------------- 2 files changed, 2 insertions(+), 41 deletions(-) diff --git a/modules/electrophysiology_browser/php/events.class.inc b/modules/electrophysiology_browser/php/events.class.inc index 4571f453f1e..9deda04c8de 100644 --- a/modules/electrophysiology_browser/php/events.class.inc +++ b/modules/electrophysiology_browser/php/events.class.inc @@ -85,16 +85,6 @@ class Events extends \NDB_Page ); case 'DELETE': $parameters = json_decode((string)$request->getBody(), true); - $sessionID = $db->pselectOne( - 'SELECT SessionID - FROM physiological_file - WHERE PhysiologicalFileID=:PFID', - ['PFID' => $parameters['physioFileID']] - ); - $timepoint = \NDB_Factory::singleton()->timepoint( - new \SessionID(strval($sessionID)) - ); - $projectName = $timepoint->getProject()->getName(); if (!$user->hasPermission( 'electrophysiology_browser_edit_annotations' @@ -116,16 +106,6 @@ class Events extends \NDB_Page case 'POST': // TODO: Better failure reporting $parameters = json_decode((string)$request->getBody(), true); - $sessionID = $db->pselectOne( - 'SELECT SessionID - FROM physiological_file - WHERE PhysiologicalFileID=:PFID', - ['PFID' => $parameters['physioFileID']] - ); - $timepoint = \NDB_Factory::singleton()->timepoint( - new \SessionID(strval($sessionID)) - ); - $projectName = $timepoint->getProject()->getName(); if (!$user->hasPermission( 'electrophysiology_browser_edit_annotations' diff --git a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc index 1f268ca4eee..bae699e8f41 100644 --- a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc +++ b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc @@ -98,16 +98,6 @@ class ElectrophysioEvents $user = $factory->user(); $db = $factory->database(); - $sessionID = $db->pselectOne( - 'SELECT SessionID - FROM physiological_file - WHERE PhysiologicalFileID=:PFID', - ['PFID' => $this->_physioFileID] - ); - $timepoint = \NDB_Factory::singleton()->timepoint( - new \SessionID(strval($sessionID)) - ); - if ($user->hasPermission('electrophysiology_browser_edit_annotations')) { //If no derivative files exist, must create new files @@ -124,6 +114,7 @@ class ElectrophysioEvents 'Sources' => $metadata['sources'], 'Author' => $metadata['author'] ]; + error_log(json_encode($metadata)); if (is_null($instance_id)) { // TODO: Support Instance INSERT @@ -231,17 +222,7 @@ class ElectrophysioEvents AND FileType='tsv'", ['PFID' => $this->_physioFileID] ); - - $projectID = intval( - $db->pselectOne( - 'SELECT ProjectID FROM session AS s WHERE s.ID = ( - SELECT SessionID FROM physiological_file - WHERE PhysiologicalFileID=:PFID - )', - ['PFID' => $this->_physioFileID] - ) - ); - + $tsvPath = count($tsv) > 0 ? $dataDir . $tsv[0]['filePath'] : ''; // Update files if files updated before database updated From 415a009e9ece4304cc230182a986acac8ddc289e Mon Sep 17 00:00:00 2001 From: Jefferson Casimir Date: Wed, 31 Jan 2024 10:57:05 -0500 Subject: [PATCH 06/23] Satisfy linter --- modules/electrophysiology_browser/php/events.class.inc | 4 ++-- .../php/models/electrophysioevents.class.inc | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/electrophysiology_browser/php/events.class.inc b/modules/electrophysiology_browser/php/events.class.inc index 9deda04c8de..8faebac0831 100644 --- a/modules/electrophysiology_browser/php/events.class.inc +++ b/modules/electrophysiology_browser/php/events.class.inc @@ -84,7 +84,7 @@ class Events extends \NDB_Page $request->withAttribute('filename', $filename) ); case 'DELETE': - $parameters = json_decode((string)$request->getBody(), true); + $parameters = json_decode((string)$request->getBody(), true); if (!$user->hasPermission( 'electrophysiology_browser_edit_annotations' @@ -105,7 +105,7 @@ class Events extends \NDB_Page return (new \LORIS\Http\Response\JSON\OK()); case 'POST': // TODO: Better failure reporting - $parameters = json_decode((string)$request->getBody(), true); + $parameters = json_decode((string)$request->getBody(), true); if (!$user->hasPermission( 'electrophysiology_browser_edit_annotations' diff --git a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc index bae699e8f41..c1a87c0a0d9 100644 --- a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc +++ b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc @@ -222,7 +222,7 @@ class ElectrophysioEvents AND FileType='tsv'", ['PFID' => $this->_physioFileID] ); - + $tsvPath = count($tsv) > 0 ? $dataDir . $tsv[0]['filePath'] : ''; // Update files if files updated before database updated From 9897a4d751531fe604fe72db8408226b9b474f5c Mon Sep 17 00:00:00 2001 From: Jefferson Casimir Date: Wed, 31 Jan 2024 12:28:55 -0500 Subject: [PATCH 07/23] Satisfy js linter --- .../src/eeglab/EEGLabSeriesProvider.tsx | 39 +++++++++++++------ .../src/series/components/AnnotationForm.tsx | 6 +-- .../src/series/store/logic/filterEpochs.tsx | 4 +- .../src/series/store/types.tsx | 4 +- 4 files changed, 34 insertions(+), 19 deletions(-) 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 a8fcafeb692..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,7 +18,10 @@ import { setFilteredEpochs, } from '../series/store/state/dataset'; import {setDomain, setInterval} from '../series/store/state/bounds'; -import {setCoordinateSystem, setElectrodes} from '../series/store/state/montage'; +import { + setCoordinateSystem, + setElectrodes, +} from '../series/store/state/montage'; import {EventMetadata} from '../series/store/types'; declare global { @@ -123,10 +126,17 @@ class EEGLabSeriesProvider extends Component { }).then(() => { const epochs = []; events.instances.map((instance) => { - const epochIndex = epochs.findIndex((e) => e.physiologicalTaskEventID === instance.PhysiologicalTaskEventID); - - const extraColumns = Array.from(events.extra_columns).filter((column) => { - return column.PhysiologicalTaskEventID === instance.PhysiologicalTaskEventID + 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) @@ -137,8 +147,8 @@ class EEGLabSeriesProvider extends Component { duration: parseFloat(instance.Duration), type: 'Event', label: epochLabel ?? instance.EventValue, - value: instance.EventValue, - trial_type: instance.TrialType, + value: instance.EventValue, + trialType: instance.TrialType, properties: extraColumns, hed: null, channels: 'all', @@ -152,7 +162,7 @@ class EEGLabSeriesProvider extends Component { }).then((epochs) => { const sortedEpochs = epochs .flat() - .sort(function (a, b) { + .sort(function(a, b) { return a.onset - b.onset; }); @@ -161,7 +171,8 @@ class EEGLabSeriesProvider extends Component { this.store.dispatch(setFilteredEpochs({ plotVisibility: sortedEpochs.reduce((indices, epoch, index) => { if (!(epoch.onset < 1 && epoch.duration >= timeInterval[1])) { - indices.push(index); // Full-recording events not visible by default + // Full-recording events not visible by default + indices.push(index); } return indices; }, []), @@ -190,12 +201,16 @@ class EEGLabSeriesProvider extends Component { Promise.race(racers(fetchJSON, coordSystemURL)) .then( ({json, _}) => { if (json) { - const {EEGCoordinateSystem, EEGCoordinateUnits, EEGCoordinateSystemDescription} = json; + const { + EEGCoordinateSystem, + EEGCoordinateUnits, + EEGCoordinateSystemDescription, + } = json; this.store.dispatch( setCoordinateSystem({ - name: EEGCoordinateSystem ?? 'Other', + name: EEGCoordinateSystem ?? 'Other', units: EEGCoordinateUnits ?? 'm', - description: EEGCoordinateSystemDescription ?? 'n/a' + description: EEGCoordinateSystemDescription ?? 'n/a', }) ); } 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 ba2651673ab..6c33bd20551 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 @@ -247,8 +247,8 @@ const AnnotationForm = ({ type: 'Event', label: data.instance.EventValue, // Unused value: data.instance.EventValue, - trial_type: data.instance.TrialType, - properties: data.extra_columns, + trialType: data.instance.TrialType, + properties: data.extraColumns, channels: 'all', physiologicalTaskEventID: data.instance.PhysiologicalTaskEventID, }; @@ -402,7 +402,7 @@ const AnnotationForm = ({ marginLeft: '10px', }}> { - currentAnnotation.label === currentAnnotation.trial_type + currentAnnotation.label === currentAnnotation.trialType ? 'trial_type' : 'value' } 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 e53a556674e..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 @@ -85,7 +85,7 @@ export const createToggleEpochEpic = (fromState: (_: any) => any) => ( return (dispatch) => { dispatch(setFilteredEpochs({ plotVisibility: newFilteredEpochs, - columnVisibility: filteredEpochs.columnVisibility + columnVisibility: filteredEpochs.columnVisibility, })); }; }) @@ -139,4 +139,4 @@ export const getEpochsInRange = (epochs, interval) => { ) ) ); -} +}; 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 34e0f043b6d..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 @@ -31,7 +31,7 @@ export type Epoch = { type: 'Event', label: string, value: string, - trial_type: string, + trialType: string, properties?: any[], hed?: string, channels: number[] | 'all', @@ -45,7 +45,7 @@ export type EpochFilter = { export type EventMetadata = { instances: any[], - extra_columns: any[], + extraColumns: any[], } export type RightPanel = From 2d6ce0848e16d7e031ea207c5e70e85e80d17450 Mon Sep 17 00:00:00 2001 From: Jefferson Casimir Date: Wed, 31 Jan 2024 15:13:10 -0500 Subject: [PATCH 08/23] Remove RB tables --- .../RB_physiological_annotation_archive.sql | 5 ---- .../RB_physiological_annotation_file.sql | 5 ---- .../RB_physiological_annotation_file_type.sql | 7 ----- .../RB_physiological_annotation_instance.sql | 5 ---- .../RB_physiological_annotation_label.sql | 28 ------------------- .../RB_physiological_annotation_parameter.sql | 5 ---- .../RB_physiological_annotation_rel.sql | 5 ---- 7 files changed, 60 deletions(-) delete mode 100644 raisinbread/RB_files/RB_physiological_annotation_archive.sql delete mode 100644 raisinbread/RB_files/RB_physiological_annotation_file.sql delete mode 100644 raisinbread/RB_files/RB_physiological_annotation_file_type.sql delete mode 100644 raisinbread/RB_files/RB_physiological_annotation_instance.sql delete mode 100644 raisinbread/RB_files/RB_physiological_annotation_label.sql delete mode 100644 raisinbread/RB_files/RB_physiological_annotation_parameter.sql delete mode 100644 raisinbread/RB_files/RB_physiological_annotation_rel.sql 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; From 3a7c6e923f7a61b7372032cc29f0bd0d5040957d Mon Sep 17 00:00:00 2001 From: jeffersoncasimir <15801528+jeffersoncasimir@users.noreply.github.com> Date: Tue, 13 Feb 2024 10:51:57 -0500 Subject: [PATCH 09/23] [Review] Removed commented-out lines --- modules/electrophysiology_browser/php/events.class.inc | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/electrophysiology_browser/php/events.class.inc b/modules/electrophysiology_browser/php/events.class.inc index 8faebac0831..a32360dca3c 100644 --- a/modules/electrophysiology_browser/php/events.class.inc +++ b/modules/electrophysiology_browser/php/events.class.inc @@ -99,9 +99,6 @@ class Events extends \NDB_Page return (new \LORIS\Http\Response\JSON\BadRequest()); } - // (new ElectrophysioEvents(intval($parameters['physioFileID']))) - // ->deleteEvent(intval($parameters['instance_id'])); - return (new \LORIS\Http\Response\JSON\OK()); case 'POST': // TODO: Better failure reporting From a8c5208e1db9250f4dcfc01f87b3d91e6415beda Mon Sep 17 00:00:00 2001 From: jeffersoncasimir <15801528+jeffersoncasimir@users.noreply.github.com> Date: Tue, 13 Feb 2024 11:31:01 -0500 Subject: [PATCH 10/23] Junk removal in unsupported event update --- .../php/models/electrophysioevents.class.inc | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc index c1a87c0a0d9..8523a5868fc 100644 --- a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc +++ b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc @@ -108,17 +108,9 @@ class ElectrophysioEvents ['PFID' => $this->_physioFileID] ); - //Get data from POST request - $metadata = [ - 'Description' => $metadata['description'], - 'Sources' => $metadata['sources'], - 'Author' => $metadata['author'] - ]; - error_log(json_encode($metadata)); - if (is_null($instance_id)) { // TODO: Support Instance INSERT - $instance_id = 1; + return []; } $instance = [ From 2bcbe850d7909d554d0ed68eba625b931aacaf73 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir <15801528+jeffersoncasimir@users.noreply.github.com> Date: Tue, 13 Feb 2024 11:42:35 -0500 Subject: [PATCH 11/23] Remove commented code --- .../src/series/components/AnnotationForm.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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 6c33bd20551..8cae1ce1551 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 @@ -161,17 +161,14 @@ const AnnotationForm = ({ * */ const handleReset = () => { - // Clear all fields - // setEvent(['', '']); - // setTimeSelection([null, null]); - // setLabel(''); + // TODO: Clear all fields }; /** * */ const handleDelete = () => { - // setIsDeleted(true); + // Not supported }; // Submit From 12062e081d11d938aa9cde3add94c28aa53aaad3 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir <15801528+jeffersoncasimir@users.noreply.github.com> Date: Tue, 13 Feb 2024 11:44:44 -0500 Subject: [PATCH 12/23] Removed commented out html --- .../src/series/components/AnnotationForm.tsx | 9 --------- 1 file changed, 9 deletions(-) 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 8cae1ce1551..0b59d96acd9 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 @@ -485,15 +485,6 @@ const AnnotationForm = ({ > Reset - {/*{currentAnnotation &&*/} - {/* */} - {/* Delete*/} - {/* */} - {/*}*/} {annoMessage && (
Date: Tue, 13 Feb 2024 11:46:02 -0500 Subject: [PATCH 13/23] Removed commented out html --- .../src/series/components/SeriesRenderer.tsx | 18 ------------------ 1 file changed, 18 deletions(-) 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 5d038790f89..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 @@ -1237,24 +1237,6 @@ const SeriesRenderer: FunctionComponent = ({ } } - {/*{*/} - {/* {*/} - {/* rightPanel === 'annotationForm'*/} - {/* ? setRightPanel(null)*/} - {/* : setRightPanel('annotationForm');*/} - {/* setCurrentAnnotation(null);*/} - {/* }}*/} - {/* >*/} - {/* {rightPanel === 'annotationForm'*/} - {/* ? 'Close Annotation Form'*/} - {/* : 'Add Annotation'*/} - {/* }*/} - {/* */} - {/*}*/}
From abf4c53648f06426cc016423d25c102eebc3a9f2 Mon Sep 17 00:00:00 2001 From: Jefferson Casimir Date: Tue, 13 Feb 2024 13:50:57 -0500 Subject: [PATCH 14/23] Add conditioanl check for file existence --- .../php/models/electrophysioevents.class.inc | 159 +++++++++--------- 1 file changed, 80 insertions(+), 79 deletions(-) diff --git a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc index 8523a5868fc..50d8055784f 100644 --- a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc +++ b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc @@ -215,50 +215,50 @@ class ElectrophysioEvents ['PFID' => $this->_physioFileID] ); - $tsvPath = count($tsv) > 0 ? $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 * + 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] - ); + ['PFID' => $this->_physioFileID] + ); - $columnNames = $db->pselect( - "SELECT DISTINCT PropertyName + $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] - ); + ['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'; + // 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"); + // Add columns names + $columns = implode("\t", $tsvEntries); + fwrite($tsvFile, "$columns\n"); - $instances = $db->pselect( - "SELECT + $instances = $db->pselect( + "SELECT PhysiologicalTaskEventID, Onset, Duration, @@ -268,25 +268,25 @@ class ElectrophysioEvents 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']; + ['PFID' => $this->_physioFileID] + ); - // Get instance's extra columns - $instanceExtraColumns - = array_filter( + 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 @@ -294,46 +294,47 @@ class ElectrophysioEvents } ); - foreach ($columnNames as $columnName) { - $column = array_filter( - array_values($instanceExtraColumns), - function ($col) use ($columnName) { - return - $col['PropertyName'] == $columnName['PropertyName']; - } + 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 ); - $columnValue = count($column) > 0 - ? array_values($column)[0]['PropertyValue'] - : 'n/a'; + // Implode with tabs as delimiter + $input = implode("\t", $inputTSV); - $inputTSV[] = $columnValue; + fwrite($tsvFile, $input . "\n"); } + fclose($tsvFile); - // 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); + //Update archives and create new hash + $this->_updateArchives([$tsvPath]); - fwrite($tsvFile, $input."\n"); + // Update time that files were written to + $db->update( + 'physiological_event_file', + ['LastWritten' => date("Y-m-d H:i:s")], + ['PhysiologicalFileID' => $this->_physioFileID] + ); } - 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] - ); } } From 2b2d0b6344fd60b803bd45401403f27806aaa4e5 Mon Sep 17 00:00:00 2001 From: Jefferson Casimir Date: Tue, 13 Feb 2024 13:55:50 -0500 Subject: [PATCH 15/23] Satisfy linter --- .../php/models/electrophysioevents.class.inc | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc index 50d8055784f..c18e807512b 100644 --- a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc +++ b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc @@ -246,7 +246,8 @@ class ElectrophysioEvents // TODO: Make columns more dynamic $tsvEntries = [ - 'onset', 'duration', 'sample', 'trial_type', 'response_time', 'value' + 'onset', 'duration', 'sample', 'trial_type', + 'response_time', 'value' ]; foreach ($columnNames as $columnName) { $tsvEntries[] = $columnName['PropertyName']; @@ -287,12 +288,12 @@ class ElectrophysioEvents // Get instance's extra columns $instanceExtraColumns = array_filter( - array_values($extraColumns), - function ($column) use ($taskEventID) { - return - $column['PhysiologicalTaskEventID'] == $taskEventID; - } - ); + array_values($extraColumns), + function ($column) use ($taskEventID) { + return + $column['PhysiologicalTaskEventID'] == $taskEventID; + } + ); foreach ($columnNames as $columnName) { $column = array_filter( From 8c6894736d0260a3e41d40bb7d078fb921a55925 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir <15801528+jeffersoncasimir@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:28:34 -0500 Subject: [PATCH 16/23] Satisfy linter --- .../php/models/electrophysioevents.class.inc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc index c18e807512b..b134cf8d3aa 100644 --- a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc +++ b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc @@ -291,7 +291,8 @@ class ElectrophysioEvents array_values($extraColumns), function ($column) use ($taskEventID) { return - $column['PhysiologicalTaskEventID'] == $taskEventID; + $column['PhysiologicalTaskEventID'] == + $taskEventID; } ); @@ -300,7 +301,8 @@ class ElectrophysioEvents array_values($instanceExtraColumns), function ($col) use ($columnName) { return - $col['PropertyName'] == $columnName['PropertyName']; + $col['PropertyName'] == + $columnName['PropertyName']; } ); From 5554a7a900c0b6ae73643f806589c0c7bc14310a Mon Sep 17 00:00:00 2001 From: jeffersoncasimir <15801528+jeffersoncasimir@users.noreply.github.com> Date: Tue, 13 Feb 2024 18:41:46 -0500 Subject: [PATCH 17/23] Add patch from next PR --- ...024-01-29-Physiological-Events-Replace-Annotations.sql | 8 ++++++++ 1 file changed, 8 insertions(+) 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 index d48bbc8d686..3ca77e98491 100644 --- a/SQL/New_patches/2024-01-29-Physiological-Events-Replace-Annotations.sql +++ b/SQL/New_patches/2024-01-29-Physiological-Events-Replace-Annotations.sql @@ -7,3 +7,11 @@ 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`); From 5436917671d939dce3beebc52b4be09155a9eea1 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir <15801528+jeffersoncasimir@users.noreply.github.com> Date: Tue, 13 Feb 2024 18:48:19 -0500 Subject: [PATCH 18/23] Change variable name for compliance (artifact) --- .../php/models/electrophysioevents.class.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc index b134cf8d3aa..845ee27b9b5 100644 --- a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc +++ b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc @@ -63,7 +63,7 @@ class ElectrophysioEvents $this->_data = [ 'instances' => $taskEvents, - 'extra_columns' => $extraColumns, + 'extraColumns' => $extraColumns, ]; } From 46e6dce5a011dd9b3d848b2ecf80841d19b08e17 Mon Sep 17 00:00:00 2001 From: Laetitia Fesselier Date: Wed, 14 Feb 2024 11:35:17 -0500 Subject: [PATCH 19/23] Fixed event update --- .../src/series/components/AnnotationForm.tsx | 5 ++++- .../php/models/electrophysioevents.class.inc | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) 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 0b59d96acd9..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 @@ -238,11 +238,14 @@ const AnnotationForm = ({ 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: data.instance.EventValue, // Unused + label: epochLabel ?? data.instance.EventValue, value: data.instance.EventValue, trialType: data.instance.TrialType, properties: data.extraColumns, diff --git a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc index 845ee27b9b5..7739f08c630 100644 --- a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc +++ b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc @@ -62,8 +62,8 @@ class ElectrophysioEvents ); $this->_data = [ - 'instances' => $taskEvents, - 'extraColumns' => $extraColumns, + 'instances' => $taskEvents, + 'extraColumns' => $extraColumns, ]; } @@ -148,8 +148,8 @@ class ElectrophysioEvents ); return [ - 'instance' => $taskEvent[0], - 'extra_columns' => $extraColumns, + 'instance' => $taskEvent[0], + 'extraColumns' => $extraColumns, ]; } return []; From a76f045fae5905b83ce105eeea08a51315e6a589 Mon Sep 17 00:00:00 2001 From: Jefferson Casimir Date: Wed, 14 Feb 2024 12:07:42 -0500 Subject: [PATCH 20/23] Revert "Fixed event update" (wrong author) This reverts commit 46e6dce5a011dd9b3d848b2ecf80841d19b08e17. --- .../src/series/components/AnnotationForm.tsx | 5 +---- .../php/models/electrophysioevents.class.inc | 8 ++++---- 2 files changed, 5 insertions(+), 8 deletions(-) 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 377f06f5160..0b59d96acd9 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 @@ -238,14 +238,11 @@ const AnnotationForm = ({ 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, + label: data.instance.EventValue, // Unused value: data.instance.EventValue, trialType: data.instance.TrialType, properties: data.extraColumns, diff --git a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc index 7739f08c630..845ee27b9b5 100644 --- a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc +++ b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc @@ -62,8 +62,8 @@ class ElectrophysioEvents ); $this->_data = [ - 'instances' => $taskEvents, - 'extraColumns' => $extraColumns, + 'instances' => $taskEvents, + 'extraColumns' => $extraColumns, ]; } @@ -148,8 +148,8 @@ class ElectrophysioEvents ); return [ - 'instance' => $taskEvent[0], - 'extraColumns' => $extraColumns, + 'instance' => $taskEvent[0], + 'extra_columns' => $extraColumns, ]; } return []; From 812e412f90723766935c5c5839a080a2bf6aff87 Mon Sep 17 00:00:00 2001 From: Jefferson Casimir Date: Wed, 14 Feb 2024 12:09:45 -0500 Subject: [PATCH 21/23] Fixed event update --- .../src/series/components/AnnotationForm.tsx | 5 ++++- .../php/models/electrophysioevents.class.inc | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) 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 0b59d96acd9..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 @@ -238,11 +238,14 @@ const AnnotationForm = ({ 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: data.instance.EventValue, // Unused + label: epochLabel ?? data.instance.EventValue, value: data.instance.EventValue, trialType: data.instance.TrialType, properties: data.extraColumns, diff --git a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc index 845ee27b9b5..7739f08c630 100644 --- a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc +++ b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc @@ -62,8 +62,8 @@ class ElectrophysioEvents ); $this->_data = [ - 'instances' => $taskEvents, - 'extraColumns' => $extraColumns, + 'instances' => $taskEvents, + 'extraColumns' => $extraColumns, ]; } @@ -148,8 +148,8 @@ class ElectrophysioEvents ); return [ - 'instance' => $taskEvent[0], - 'extra_columns' => $extraColumns, + 'instance' => $taskEvent[0], + 'extraColumns' => $extraColumns, ]; } return []; From 851106f7edfa0b78f3bf8b8151d9570cdde2a209 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir <15801528+jeffersoncasimir@users.noreply.github.com> Date: Wed, 14 Feb 2024 16:31:22 -0500 Subject: [PATCH 22/23] Get directory from config factory --- .../php/models/electrophysioevents.class.inc | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc index 7739f08c630..61eeeae78e6 100644 --- a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc +++ b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc @@ -191,16 +191,11 @@ class ElectrophysioEvents */ function updateFiles(): void { - $db = \NDB_Factory::singleton()->database(); + $db = \NDB_Factory::singleton()->database(); //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'] - ); + $config = \NDB_Factory::singleton()->config(); + $dataDir = $config->getSetting("dataDirBasepath"); $tsv = $db->pselect( "SELECT @@ -369,15 +364,12 @@ class ElectrophysioEvents */ function _updateArchives(array $paths) : void { - $db = \NDB_Factory::singleton()->database(); + $db = \NDB_Factory::singleton()->database(); + + //Get data directory base path from Config + $config = \NDB_Factory::singleton()->config(); + $dataDir = $config->getSetting("dataDirBasepath"); - $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'] - ); $archive_table_names = [ 'physiological_event_archive', 'physiological_archive' From cd13eb9060809eb1d286891fb978d2d4ce4eb0ac Mon Sep 17 00:00:00 2001 From: jeffersoncasimir <15801528+jeffersoncasimir@users.noreply.github.com> Date: Wed, 14 Feb 2024 17:15:42 -0500 Subject: [PATCH 23/23] Satisfy linter --- .../php/models/electrophysioevents.class.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc index 61eeeae78e6..bf1f5a15db4 100644 --- a/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc +++ b/modules/electrophysiology_browser/php/models/electrophysioevents.class.inc @@ -191,7 +191,7 @@ class ElectrophysioEvents */ function updateFiles(): void { - $db = \NDB_Factory::singleton()->database(); + $db = \NDB_Factory::singleton()->database(); //Get data directory base path from Config $config = \NDB_Factory::singleton()->config(); @@ -364,7 +364,7 @@ class ElectrophysioEvents */ function _updateArchives(array $paths) : void { - $db = \NDB_Factory::singleton()->database(); + $db = \NDB_Factory::singleton()->database(); //Get data directory base path from Config $config = \NDB_Factory::singleton()->config();