diff --git a/bids_export.m b/bids_export.m index 36a2f89..dec8f5f 100644 --- a/bids_export.m +++ b/bids_export.m @@ -265,6 +265,7 @@ function bids_export(files, varargin) 'chanlookup' 'string' {} ''; 'defaced' 'string' {'on' 'off'} 'on'; 'createids' 'string' {'on' 'off'} 'on'; + 'singleEventsJson' 'string' {'on' 'off'} 'on'; 'exportext' 'string' { 'edf' 'eeglab' } 'eeglab'; 'README' 'string' {} ''; 'CHANGES' 'string' {} '' ; @@ -277,8 +278,20 @@ function bids_export(files, varargin) % deleting folder fprintf('Exporting data to %s...\n', opt.targetdir); if exist(opt.targetdir,'dir') - disp('Deleting folder...') - rmdir(opt.targetdir, 's'); + uilist = { ... + { 'Style', 'text', 'string', 'Output directory exists and all current files will be deleted if continue', 'fontweight', 'bold' }, ... + { 'Style', 'text', 'string', 'Would you want to proceed?'}, ... + }; + geometry = { [1] [1]}; + geomvert = [1 1 ]; + [results,userdata,isOk,restag] = inputgui( 'geometry', geometry, 'geomvert', geomvert, 'uilist', uilist, 'title', 'Warning'); + if isempty(isOk) + disp('BIDS export cancelled...') + return + else + disp('Deleting folder...') + rmdir(opt.targetdir, 's'); + end end disp('Creating sub-directories...') @@ -445,11 +458,20 @@ function bids_export(files, varargin) % prepare event file information (_events.json) % ---------------------------- +eInfoDescFields = { 'LongName' 'optional' 'char' ''; + 'Levels' 'optional' 'struct' struct([]); + 'Description' 'optional' 'char' ''; + 'Units' 'optional' 'char' ''; + 'TermURL' 'optional' 'char' ''; + 'HED' 'optional' 'struct' struct([])}; fields = fieldnames(opt.eInfoDesc); for iField = 1:length(fields) descFields{1,4} = fields{iField}; if ~isfield(opt.eInfoDesc, fields{iField}), opt.eInfoDesc(1).(fields{iField}) = struct([]); end - opt.eInfoDesc.(fields{iField}) = checkfields(opt.eInfoDesc.(fields{iField}), descFields, 'eInfoDesc'); + opt.eInfoDesc.(fields{iField}) = checkfields(opt.eInfoDesc.(fields{iField}), eInfoDescFields, 'eInfoDesc'); +end +if strcmpi(opt.singleEventsJson, 'on') + jsonwrite(fullfile(opt.targetdir, ['task-' opt.taskName '_events.json' ]), opt.eInfoDesc,struct('indent',' ')); end % Write README files (README) @@ -739,8 +761,9 @@ function copy_data_bids(fileIn, fileOut, notes, opt, chanlocs, copydata, exportE [folderOut,fileOut,~] = fileparts(fileOut); fileOut = fullfile(folderOut,fileOut); if ~isempty(EEG.event) - jsonwrite([ fileOut(1:end-3) 'events.json' ], opt.eInfoDesc,struct('indent',' ')); - + if strcmpi(opt.singleEventsJson,'off') + jsonwrite([ fileOut(1:end-3) 'events.json' ], opt.eInfoDesc,struct('indent',' ')); + end % --- _events.tsv fid = fopen( [ fileOut(1:end-3) 'events.tsv' ], 'w'); @@ -757,7 +780,7 @@ function copy_data_bids(fileIn, fileOut, notes, opt, chanlocs, copydata, exportE else opt.eInfo(end+1,:) = { 'value' 'type' }; end if isfield(EEG.event, 'response_time'), opt.eInfo(end+1,:) = { 'response_time' 'response_time' }; end if isfield(EEG.event, 'stim_file'), opt.eInfo(end+1,:) = { 'stim_file' 'stim_file' }; end - if isfield(EEG.event, 'usertags'), opt.eInfo(end+1,:) = { 'HED' 'usertags' }; end + if isfield(EEG.event, 'HED'), opt.eInfo(end+1,:) = { 'HED' 'HED' }; end else bids_fields = opt.eInfo(:,1); if ~any(strcmp(bids_fields,'onset')) @@ -783,7 +806,7 @@ function copy_data_bids(fileIn, fileOut, notes, opt, chanlocs, copydata, exportE end % reorder fields so it matches BIDS - fieldOrder = { 'onset' 'duration' 'sample' 'trial_type' 'response_time' 'stim_file' 'value' 'HED' }; + fieldOrder = { 'onset' 'duration' 'sample' 'trial_type' 'response_time' 'stim_file' 'value'}; % 'HED' }; % remove HED from default column in events.tsv as HED tags should be put in events.json instead newOrder = []; for iField = 1:length(fieldOrder) ind = strmatch(fieldOrder{iField}, opt.eInfo(:,1)', 'exact'); @@ -926,21 +949,21 @@ function copy_data_bids(fileIn, fileOut, notes, opt, chanlocs, copydata, exportE end str{end+1} = eventValue; - case 'HED' - hed = 'n/a'; - if isfield(EEG.event, tmpField) && ~isempty(EEG.event(iEvent).(tmpField)) - hed = EEG.event(iEvent).(tmpField); - else - if isfield(EEG.event, 'usertags') && ~isempty(EEG.event(iEvent).usertags) - hed = EEG.event(iEvent).usertags; - if isfield(EEG.event, 'hedtags') && ~isempty(EEG.event(iEvent).hedtags) - hed = [hed ',' EEG.event(iEvent).hedtags]; - end - elseif isfield(EEG.event, 'hedtags') && ~isempty(EEG.event(iEvent).hedtags) - hed = EEG.event(iEvent).hedtags; - end - end - str{end+1} = hed; +% case 'HED' +% hed = 'n/a'; +% if isfield(EEG.event, tmpField) && ~isempty(EEG.event(iEvent).(tmpField)) +% hed = EEG.event(iEvent).(tmpField); +% else +% if isfield(EEG.event, 'HED') && ~isempty(EEG.event(iEvent).usertags) +% hed = EEG.event(iEvent).usertags; +% if isfield(EEG.event, 'hedtags') && ~isempty(EEG.event(iEvent).hedtags) +% hed = [hed ',' EEG.event(iEvent).hedtags]; +% end +% elseif isfield(EEG.event, 'hedtags') && ~isempty(EEG.event(iEvent).hedtags) +% hed = EEG.event(iEvent).hedtags; +% end +% end +% str{end+1} = hed; otherwise if isfield(EEG.event, opt.eInfo{iField,2}) @@ -1161,6 +1184,10 @@ function copy_data_bids(fileIn, fileOut, notes, opt, chanlocs, copydata, exportE s = setfield(s, {1}, f{iRow,1}, f{iRow,4}); end elseif ~isempty(f{iRow,3}) && ~isa(s.(f{iRow,1}), f{iRow,3}) && ~strcmpi(s.(f{iRow,1}), 'n/a') + % if it's HED in eInfoDesc, allow string also + if strcmp(structName,'eInfoDesc') && strcmp(f{iRow,1}, 'HED') && isa(s.(f{iRow,1}), 'char') + return + end error(sprintf('Parameter %s.%s must be a %s', structName, f{iRow,1}, f{iRow,3})); end end diff --git a/eegplugin_bids.m b/eegplugin_bids.m index 9653919..e31996d 100644 --- a/eegplugin_bids.m +++ b/eegplugin_bids.m @@ -44,8 +44,8 @@ % create BIDS menus % ----------------- comtaskinfo = [trystrs.no_check '[EEG,LASTCOM] = pop_taskinfo(EEG);' catchstrs.store_and_hist ]; - comsubjinfo = [trystrs.no_check '[EEG,LASTCOM] = pop_participantinfo(EEG,STUDY);' catchstrs.store_and_hist ]; - comeventinfo = [trystrs.no_check '[EEG,LASTCOM] = pop_eventinfo(EEG);' catchstrs.store_and_hist ]; + comsubjinfo = [trystrs.no_check '[EEG,STUDY,LASTCOM] = pop_participantinfo(EEG,STUDY);' catchstrs.store_and_hist ]; + comeventinfo = [trystrs.no_check '[EEG,STUDY,LASTCOM] = pop_eventinfo(EEG,STUDY);' catchstrs.store_and_hist ]; % comvalidatebids = [ trystrs.no_check 'if plugin_askinstall(''bids-validator'',''pop_validatebids'') == 1 pop_validatebids() end' catchstrs.add_to_hist ]; bids = findobj(fig, 'label', 'BIDS tools'); if isempty(bids) diff --git a/pop_eventinfo.m b/pop_eventinfo.m index 8cef76f..006361f 100644 --- a/pop_eventinfo.m +++ b/pop_eventinfo.m @@ -14,29 +14,36 @@ % 'EEG' - [struct] Updated EEG structure containing event BIDS information % in each EEG structure at EEG.BIDS.eInfoDesc and EEG.BIDS.eInfo % -% 'eInfoDesc' - [struct] structure describing BIDS event fields as you specified. +% 'eInfoDesc' - [struct] structure describing BIDS event fields as you specified. % See BIDS specification for all suggested fields. % -% 'eInfo' - [cell] BIDS event fields and their corresponding +% 'eInfo' - [cell] BIDS event fields and their corresponding % event fields in the EEGLAB event structure. Note that % EEGLAB event latency, duration, and type are inserted % automatically as columns "onset" (latency in sec), "duration" % (duration in sec), "value" (EEGLAB event type) % % Author: Dung Truong, Arnaud Delorme -function [EEG, command] = pop_eventinfo(EEG, varargin) +function [EEG, STUDY, command] = pop_eventinfo(EEG, STUDY, varargin) %% check if there's already an opened window if ~isempty(findobj('Tag','eventBidsTable')) errordlg2('A window is already openened for pop_eventinfo'); return end - command = '[EEG, command] = pop_eventinfo(EEG)'; + command = '[EEG, [], command] = pop_eventinfo(EEG)'; + %% if STUDY is provided, check for consistency + hasSTUDY = false; + if exist('STUDY','var') && ~isempty(STUDY) + [STUDY, EEG] = pop_checkdatasetinfo(STUDY, EEG); + command = '[EEG, STUDY, command] = pop_eventinfo(EEG, STUDY);'; + hasSTUDY = true; + end + % perform check to make sure EEG.event is consistent across EEG if isempty(EEG(1).event) errordlg2('EEG.event is empty for first dataset'); - return - + return end try eventFields = fieldnames([EEG.event]); @@ -48,7 +55,7 @@ warning('There is mismatch in number of fields in EEG.event structures. Using fields of EEG(%d) which has the highest number of fields (%d).', index, num); end end - bidsFields = {'onset', 'duration', 'trial_type','value','stim_file','sample','response_time'};%,'HED'}; + bidsFields = {'onset', 'duration', 'trial_type','value','stim_file','sample','response_time'}; eventFields = setdiff(eventFields, 'latency'); % define global variables % ----------------------- @@ -63,7 +70,7 @@ fontSize = 12; % Use GUI - if nargin < 2 + if nargin < 3 % create UI f = figure('MenuBar', 'None', 'ToolBar', 'None', 'Name', 'Edit BIDS event info - pop_eventinfo', 'Color', bg); f.Position(3) = appWidth; @@ -110,7 +117,7 @@ if isfield(eventBIDS.(field), 'Levels') && ~isempty(eventBIDS.(field).Levels) data{i,strcmp(tbl.ColumnName, 'Levels')} = strjoin(fieldnames(eventBIDS.(field).Levels),','); else - if strcmp(field, 'onset') || strcmp(field, "sample") || strcmp(field, "duration") || strcmp(field, "HED") + if strcmp(field, 'onset') || strcmp(field, "sample") || strcmp(field, "duration") data{i,strcmp(tbl.ColumnName, 'Levels')} = 'n/a'; else data{i,strcmp(tbl.ColumnName, 'Levels')} = 'Click to specify below'; @@ -122,7 +129,7 @@ tbl.Data = data; waitfor(f); % Use default value - pop_eventinfo(EEG,'default') called - elseif nargin < 3 && ischar(varargin{1}) && strcmp(varargin{1}, 'default') + elseif nargin < 4 && ischar(varargin{1}) && strcmp(varargin{1}, 'default') done(); end @@ -201,8 +208,12 @@ function done() % prepare return struct fields = fieldnames(eventBIDS); - if isfield(EEG.etc,'tags') - hedTags = EEG.etc.tags; + fMap = []; + if hasSTUDY && isfield(STUDY.etc, 'tags') + hedTags = STUDY.etc.tags; + fMap = fieldMap.createfMapFromStruct(hedTags); + elseif isfield(EEG(1).etc,'tags') + hedTags = EEG(1).etc.tags; fMap = fieldMap.createfMapFromStruct(hedTags); end for k=1:length(fields) @@ -229,18 +240,23 @@ function done() if isfield(eventBIDS.(bidsField),'Levels') && ~isempty(eventBIDS.(bidsField).Levels) && ~strcmp(eventBIDS.(bidsField).Levels,'n/a') eInfoDesc.(bidsField).Levels = eventBIDS.(bidsField).Levels; end - % parse HED - if isfield(EEG.etc,'tags') + % parse HED if exists + if ~isempty(fMap) tMap = fMap.getMap(eegField); - if ~isempty(tMap) + if ~isempty(tMap) && tMap.hasAnnotation() codes = tMap.getCodes(); if numel(codes) == 1 && strcmp(codes{1},'HED') tList = tMap.getValue('HED'); - eInfoDesc.(bidsField).HED = tagList.stringify(tList.getTags()); + if tList.hasAnnotation() + eInfoDesc.(bidsField).HED = tagList.stringify(tList.getTags()); + end else for c=1:numel(codes) tList = tMap.getValue(codes{c}); - eInfoDesc.(bidsField).HED.(codes{c}) = tagList.stringify(tList.getTags()); + % only add HED tags for the ones that + if tList.hasAnnotation() + eInfoDesc.(bidsField).HED.(codes{c}) = tagList.stringify(tList.getTags()); + end end end end @@ -250,12 +266,12 @@ function done() end end end - if numel(EEG) == 1 - command = '[EEG, eInfoDesc, eInfo] = pop_eventinfo(EEG);'; - else - command = '[EEG, eInfoDesc, eInfo] = pop_eventinfo(EEG);'; - end + % add info to STUDY if exists + if hasSTUDY + STUDY.BIDS.eInfoDesc = eInfoDesc; + STUDY.BIDS.eInfo = eInfo; + end % add info to EEG structs for e=1:numel(EEG) EEG(e).BIDS.eInfoDesc = eInfoDesc; @@ -380,9 +396,7 @@ function createLevelUI(~,~,table,field) removeLevelUI(); matchedRow = strcmp(table.Source.Data(:, strcmp(table.Source.ColumnName, 'BIDS Field')), field); levelCellText = table.Source.Data{matchedRow, strcmp(table.Source.ColumnName, 'Levels')}; % text @ (field, Levels) cell. if 'n/a' then no action, 'Click to..' then conditional action, ',...' then get levels - if strcmp(field, 'HED') - uicontrol(f, 'Style', 'text', 'String', 'Levels editing not applied for HED. Use ''pop_tageeg(EEG)'' of HEDTools plug-in to edit event HED tags', 'Units', 'normalized', 'Position', [0.01 0.45 1 0.05],'ForegroundColor', fg,'BackgroundColor', bg, 'Tag', 'levelEditMsg'); - elseif strcmp(field, 'onset') || strcmp(field, 'sample') || strcmp(field, 'duration') + if strcmp(field, 'onset') || strcmp(field, 'sample') || strcmp(field, 'duration') uicontrol(f, 'Style', 'text', 'String', 'Levels editing not applied for field with continuous values.', 'Units', 'normalized', 'Position', [0.01 0.45 1 0.05],'ForegroundColor', fg,'BackgroundColor', bg, 'Tag', 'levelEditMsg'); else % retrieve all unique values from EEG.event.(field). @@ -619,23 +633,7 @@ function removeLevelUI() event.value.Units = ''; event.value.Levels = []; event.value.TermURL = ''; -% elseif strcmp(fields{idx}, 'HED') && any(strcmp(eventFields, 'usertags')) -% if isfield(EEG(1).event, 'usertags') -% event.HED.EEGField = 'usertags'; -% else -% event.HED.EEGField = ''; -% end -% event.HED.LongName = 'Hierarchical Event Descriptor'; -% event.HED.Description = 'Tags describing the nature of the event'; -% event.HED.Levels = []; -% event.HED.Units = ''; -% event.HED.TermURL = ''; elseif strcmp(fields{idx}, 'duration') -% if isfield(EEG(1).event, 'duration') -% event.HED.EEGField = 'duration'; -% else -% event.HED.EEGField = ''; -% end event.duration.LongName = 'Event duration'; event.duration.Description = 'Duration of the event (measured from onset) in seconds. Must always be either zero or positive. A "duration" value of zero implies that the delta function or event is so short as to be effectively modeled as an impulse.'; event.duration.Units = 'second'; diff --git a/pop_exportbids.m b/pop_exportbids.m index b912090..9062a42 100644 --- a/pop_exportbids.m +++ b/pop_exportbids.m @@ -59,10 +59,11 @@ { 'Style', 'pushbutton', 'string', 'Edit participants' 'tag' 'participants' 'callback' cb_participants }, ... { 'Style', 'pushbutton', 'string', 'Edit event info' 'tag' 'events' 'callback' cb_events }, ... { 'Style', 'checkbox', 'string', 'Do not use participants ID and create anonymized participant ID instead' 'tag' 'newids' }, ... + { 'Style', 'checkbox', 'string', 'Use single top-level events.json' 'tag' 'eventsJson' }, ... }; relSize = 0.7; - geometry = { [1] [1] [1-relSize relSize*0.8 relSize*0.2] [1-relSize relSize] [1] [1] [1 1 1] [1] }; - geomvert = [1 0.2 1 1 1 3 1 1 ]; + geometry = { [1] [1] [1-relSize relSize*0.8 relSize*0.2] [1-relSize relSize] [1] [1] [1 1 1] [1] [1]}; + geomvert = [1 0.2 1 1 1 3 1 1 1]; userdata.EEG = EEG; userdata.STUDY = STUDY; [results,userdata,~,restag] = inputgui( 'geometry', geometry, 'geomvert', geomvert, 'uilist', uilist, 'helpcom', 'pophelp(''pop_exportbids'');', 'title', 'Export EEGLAB STUDY to BIDS -- pop_exportbids()', 'userdata', userdata ); @@ -80,10 +81,10 @@ % end % options - options = { 'targetdir' restag.outputfolder 'License' restag.license 'CHANGES' restag.changes 'createids' fastif(restag.newids, 'on', 'off') }; + options = { 'targetdir' restag.outputfolder 'License' restag.license 'CHANGES' restag.changes 'createids' fastif(restag.newids, 'on', 'off') 'singleEventsJson' fastif(restag.eventsJson, 'on', 'off')}; if ~isfield(EEG(1), 'BIDS') % none of the edit button was clicked - EEG = pop_eventinfo(EEG, 'default'); + EEG = pop_eventinfo(EEG, STUDY, 'default'); EEG = pop_participantinfo(EEG, STUDY, 'default'); EEG = pop_taskinfo(EEG, 'default'); end diff --git a/pop_participantinfo.m b/pop_participantinfo.m index 3a73a16..da2ffee 100644 --- a/pop_participantinfo.m +++ b/pop_participantinfo.m @@ -45,8 +45,8 @@ % CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) % ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF % THE POSSIBILITY OF SUCH DAMAGE. -function [EEG, command] = pop_participantinfo(EEG,STUDY, varargin) - command = '[EEG, command] = pop_participantinfo(EEG);'; +function [EEG, STUDY, command] = pop_participantinfo(EEG,STUDY, varargin) + command = '[EEG, [], command] = pop_participantinfo(EEG);'; %% check if there's already an opened window if ~isempty(findobj('Tag','pInfoTable')) @@ -55,9 +55,11 @@ end %% if STUDY is provided, check for consistency + hasSTUDY = false; if exist('STUDY','var') && ~isempty(STUDY) [STUDY, EEG] = pop_checkdatasetinfo(STUDY, EEG); - command = '[EEG, command] = pop_participantinfo(EEG, STUDY);'; + command = '[EEG, STUDY, command] = pop_participantinfo(EEG, STUDY);'; + hasSTUDY = true; end %% default settings @@ -79,13 +81,13 @@ % ------------------------- if isfield(EEG(1), 'subject') && ~isempty(EEG(1).subject) allSubjects = { EEG.subject }; - elseif isfield(STUDY,'datasetinfo') && isfield(STUDY.datasetinfo(1), 'subject') && ~isempty(STUDY.datasetinfo(1).subject) + elseif hasSTUDY && isfield(STUDY,'datasetinfo') && isfield(STUDY.datasetinfo(1), 'subject') && ~isempty(STUDY.datasetinfo(1).subject) allSubjects = { STUDY.datasetinfo.subject }; else if numel(EEG) == 1 errordlg2('No subject ID found. Please fill in "Subject code" in the next window, then resume.'); EEG = pop_editset(EEG); - else + elseif hasSTUDY errordlg2('No subject info found in STUDY. Please add using "Study > Edit study info", then resume.'); end return @@ -399,7 +401,7 @@ function okCB(~, ~) end if ~isempty(EEG(e).subject) rowIdx = strcmp(EEG(e).subject, pTable.Data(:, strcmp('participant_id', pTable.ColumnName))); - elseif ~isempty(STUDY.datasetinfo(1).subject) % assuming order of STUDY.datasetinfo matches with order of EEG in the EEG array + elseif hasSTUDY && ~isempty(STUDY.datasetinfo(1).subject) % assuming order of STUDY.datasetinfo matches with order of EEG in the EEG array rowIdx = strcmp(STUDY.datasetinfo(e).subject, pTable.Data(:, strcmp('participant_id', pTable.ColumnName))); end if isempty(pTable.Data{rowIdx,strcmp('HeadCircumference',pTable.ColumnName)})