function [p] = gui_erp_open(p,command,parent)

% gui_erp_open - Load averaged ERP data into matlab workspace
%
% Usage: [p] = gui_erp_open(p,[command],[parent])
%
% p is a structure, generated by 'eeg_toolbox_defaults'
% command is either 'init' or 'load'
% parent is a handle to the gui that calls this gui, useful
% for updating the UserData field of the parent from this gui.
% The p structure is returned to the parent when the parent
% handle is given.
%

% $Revision: 1.1 $ $Date: 2009-04-28 22:13:56 $

% Licence:  GNU GPL, no express or implied warranties
% History:  01/2002, Darren.Weber_at_radiology.ucsf.edu
%           08/2002, Darren.Weber_at_radiology.ucsf.edu
%                    added EMSE erp handling
%                    added interpolation of zero point option
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

if ~exist('p','var'),
 [p] = eeg_toolbox_defaults;
elseif isempty(p),
 [p] = eeg_toolbox_defaults;
end

if ~exist('command','var'),
  command = 'init';
elseif isempty(command),
  command = 'init';
end

command = lower(command);

switch command,
  case 'init',
    if exist('parent','var'),
      ERPOpen = INIT(p,parent);
    else
      ERPOpen = INIT(p,'');
    end
  otherwise,
    ERPOpen = get(gcbf,'Userdata');
    set(ERPOpen.gui,'Pointer','watch');
end

switch command,

  case 'plot',

    ERPOpen.p = erp_open(ERPOpen.p,ERPOpen.gui);

    plotfig = figure('Name',ERPOpen.p.volt.file,...
      'NumberTitle','off',...
      'UserData',ERPOpen.p);
    movegui(plotfig,'center');

    plot(ERPOpen.p.volt.timeArray,ERPOpen.p.volt.data); axis tight;
    eeg_plot_metric;

    if isequal(get(ERPOpen.handles.Bhold,'Value'),1),
      parent = ERPOpen.gui;
    else
      if isfield(ERPOpen,'parent'),
        parent = ERPOpen.parent.gui;
      else
        parent = [];
      end
    end

    if isempty(parent), [Xpoint,Ypoint] = eeg_crosshair('init',ERPOpen.p);
    else                [Xpoint,Ypoint] = eeg_crosshair('init',ERPOpen.p,parent);
    end

  case 'return',

    ERPOpen.p = erp_open(ERPOpen.p);

  case 'save',

    fprintf('\nGUI_ERP_OPEN: Save As not implemented yet.\n');

  case 'cancel',

    GUI.parent = ERPOpen.parent;
    gui_updateparent(GUI);
    close gcbf;

  otherwise,

end

switch command,
  case {'init','cancel'},
  otherwise,
    set(ERPOpen.gui,'Pointer','arrow');
    set(ERPOpen.gui,'Userdata',ERPOpen);
    if isequal(get(ERPOpen.handles.Bhold,'Value'),1),
     [p] = gui_updateparent(ERPOpen,0);
    else
     [p] = gui_updateparent(ERPOpen);
      close gcbf;
    end
end

return

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function [ERPOpen] = INIT(p,parent)
% GUI General Parameters

GUIwidth  = 500;
GUIheight = 120;

version = '$Revision: 1.1 $';
name = sprintf('ERP File Open [v %s]\n',version(11:15));

GUI = figure('Name',name,'Tag','ERP_OPEN',...
  'NumberTitle','off',...
  'MenuBar','none','Position',[1 1 GUIwidth GUIheight]);
movegui(GUI,'center');

Font.FontName   = 'Helvetica';
Font.FontUnits  = 'Pixels';
Font.FontSize   = 12;
Font.FontWeight = 'normal';
Font.FontAngle  = 'normal';

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Voltage Data Selection and Parameters

G.Title_data = uicontrol('Parent',GUI,'Style','text','Units','Normalized',Font, ...
  'Position',[.01 .75 .17 .2],...
  'String','Data Type:','HorizontalAlignment','left');

switch lower(p.volt.type),
  case 'ascii',   voltType = 1;
  case 'emse',    voltType = 2;
  case 'scan4x',  voltType = 3;
  case 'scan3x',  voltType = 4;
  case 'matlab',  voltType = 5;
  otherwise,      voltType = 1;
end
G.PvoltType = uicontrol('Tag','PvoltType','Parent',GUI,'Style','popupmenu',...
  'Units','Normalized',Font,  ...
  'Position',[.20 .75 .2 .2],...
  'String',{'ASCII' 'EMSE' 'Scan4x' 'Scan3x' 'Matlab'},'Value',voltType,...
  'Callback',strcat('ERPOpen = get(gcbf,''Userdata'');',...
  'ERPOpen.p.volt.type = popupstr(ERPOpen.handles.PvoltType);',...
  'set(gcbf,''Userdata'',ERPOpen); clear ERPOpen;'));

% ERP Parameters
G.Bascii = uicontrol('Parent',GUI,'Style','pushbutton','Units','Normalized', Font, ...
  'Position',[.45 .75 .2 .2],...
  'String','ERP Parameters','BusyAction','queue',...
  'BackgroundColor',[0.0 0.0 0.75],...
  'ForegroundColor',[1 1 1], 'HorizontalAlignment', 'center',...
  'Callback','tempgui = gui_eeg_ascii_parameters(gcbf); clear tempgui;');

% Interpolate Zero
G.BinterpZero = uicontrol('Parent',GUI,'Style','checkbox','Units','Normalized', Font, ...
  'Position',[.80 .75 .18 .2],'String','Interp 0','BusyAction','queue',...
  'TooltipString','Interpolate zero, mainly for ascii, scan3x, scan4x files.',...
  'Value',p.volt.interpZero,'HorizontalAlignment', 'center',...
  'Callback',strcat('ERPOpen = get(gcbf,''Userdata'');',...
  'ERPOpen.p.volt.interpZero = get(ERPOpen.handles.BinterpZero,''Value''); ',...
  'set(gcbf,''Userdata'',ERPOpen); clear ERPOpen;'));

G.Title_path = uicontrol('Parent',GUI,'Style','text','Units','Normalized',Font, ...
  'Position',[.01 .50 .17 .2],...
  'String','Path','HorizontalAlignment','left');
G.EvoltPath = uicontrol('Parent',GUI,'Style','edit','Units','Normalized',Font,  ...
  'Position',[.20 .50 .58 .2], 'String',p.volt.path,...
  'Callback',strcat('ERPOpen = get(gcbf,''Userdata'');',...
  'ERPOpen.p.volt.path = get(ERPOpen.handles.EvoltPath,''String'');',...
  'set(gcbf,''Userdata'',ERPOpen); clear ERPOpen;'));

G.Title_file = uicontrol('Parent',GUI,'Style','text','Units','Normalized',Font, ...
  'Position',[.01 .25 .17 .2],...
  'String','File','HorizontalAlignment','left');
G.EvoltFile = uicontrol('Parent',GUI,'Style','edit','Units','Normalized',Font,  ...
  'Position',[.20 .25 .58 .2], 'String',p.volt.file,...
  'Callback',strcat('ERPOpen = get(gcbf,''Userdata'');',...
  'ERPOpen.p.volt.file = get(ERPOpen.handles.EvoltFile,''String'');',...
  'set(gcbf,''Userdata'',ERPOpen); clear ERPOpen;'));

Font.FontWeight = 'bold';

% BROWSE: Look for the data
browsecommand = strcat('ERPOpen = get(gcbf,''Userdata'');',...
  'cd(ERPOpen.p.volt.path);',...
  '[file, path] = uigetfile(',...
  '{''*.dat;*.asc;*.txt'', ''ASCII Data Files (*.dat,*.asc,*.txt)'';', ...
  ' ''*.erp'', ''NeuroScan Average (*.erp)'';', ...
  ' ''*.erp'', ''EMSE Average (*.erp)'';', ...
  ' ''*.mat'', ''MATLAB Data Files (*.mat)'';', ...
  ' ''*.*'',   ''All Files (*.*)''},', ...
  '''Select ERP File'');',...
  'if ~isequal(path,0), ERPOpen.p.volt.path = path; end;',...
  'if ~isequal(file,0), ERPOpen.p.volt.file = file; end;',...
  'set(ERPOpen.handles.EvoltPath,''String'',ERPOpen.p.volt.path);',...
  'set(ERPOpen.handles.EvoltFile,''String'',ERPOpen.p.volt.file);',...
  'if ~isempty(findstr(file,''.dat'')) | ~isempty(findstr(file,''.txt'')) | ~isempty(findstr(file,''.asc'')), ',...
  '  set(ERPOpen.handles.PvoltType,''Value'',1); ',...
  '  ERPOpen.p.volt.type = popupstr(ERPOpen.handles.PvoltType);',...
  'elseif ~isempty(findstr(file,''.erp'')), ',...
  '  set(ERPOpen.handles.PvoltType,''Value'',3); ',...
  '  ERPOpen.p.volt.type = popupstr(ERPOpen.handles.PvoltType);',...
  'else, ',...
  '  set(ERPOpen.handles.PvoltType,''Value'',5); ',...
  '  ERPOpen.p.volt.type = popupstr(ERPOpen.handles.PvoltType);',...
  'end;',...
  'set(gcbf,''Userdata'',ERPOpen); clear ERPOpen file path;');
G.BvoltFile = uicontrol('Parent',GUI,'Style','pushbutton','Units','Normalized',Font, ...
  'Position',[.01 .01 .17 .2], 'String','BROWSE',...
  'BackgroundColor',[0.8 0.8 0.0],...
  'ForegroundColor', [1 1 1], 'HorizontalAlignment', 'center',...
  'Callback', browsecommand );

% PLOT: Load & plot the data!
G.Bplot = uicontrol('Parent',GUI,'Style','pushbutton','Units','Normalized', Font, ...
  'Position',[.20 .01 .18 .2],...
  'String','PLOT','BusyAction','queue',...
  'TooltipString','Plot the ERP data and return p struct.',...
  'BackgroundColor',[0.0 0.5 0.0],...
  'ForegroundColor', [1 1 1], 'HorizontalAlignment', 'center',...
  'Callback',strcat('ERPOpen = get(gcbf,''Userdata'');',...
  'p = gui_erp_open(ERPOpen.p,''plot'');',...
  'clear ERPOpen;'));

% Save As
G.Bsave = uicontrol('Parent',GUI,'Style','pushbutton','Units','Normalized', Font, ...
  'Position',[.40 .01 .18 .2],'HorizontalAlignment', 'center',...
  'String','SAVE AS','TooltipString','ERP File Conversion Tool (not implemented yet)',...
  'BusyAction','queue',...
  'Visible','on',...
  'BackgroundColor',[0.0 0.0 0.75],...
  'ForegroundColor', [1 1 1], 'HorizontalAlignment', 'center',...
  'Callback',strcat('ERPOpen = get(gcbf,''Userdata'');',...
  'p = gui_erp_open(ERPOpen.p,''save'');',...
  'clear ERPOpen;'));

% Quit, return file parameters
G.Breturn = uicontrol('Parent',GUI,'Style','pushbutton','Units','Normalized', Font, ...
  'Position',[.60 .01 .18 .2],...
  'String','RETURN','BusyAction','queue',...
  'TooltipString','Return p struct to workspace and parent GUI.',...
  'BackgroundColor',[0.75 0.0 0.0],...
  'ForegroundColor', [1 1 1], 'HorizontalAlignment', 'center',...
  'Callback',strcat('ERPOpen = get(gcbf,''Userdata'');',...
  'p = gui_erp_open(ERPOpen.p,''return'');',...
  'clear ERPOpen;'));

% Cancel
G.Bcancel = uicontrol('Parent',GUI,'Style','pushbutton','Units','Normalized', Font, ...
  'Position',[.80 .01 .18 .2],...
  'String','CANCEL','BusyAction','queue',...
  'TooltipString','Close, do not return parameters.',...
  'BackgroundColor',[0.75 0.0 0.0],...
  'ForegroundColor', [1 1 1], 'HorizontalAlignment', 'center',...
  'Callback',strcat('ERPOpen = get(gcbf,''Userdata'');',...
  'p = gui_erp_open(ERPOpen.p,''cancel'');',...
  'clear ERPOpen;'));

% Help
G.Bhelp = uicontrol('Parent',GUI,'Style','pushbutton','Units','Normalized', Font, ...
  'Position',[.80 .25 .18 .2],'String','Help','BusyAction','queue',...
  'BackgroundColor',[1 1 0],...
  'ForegroundColor',[0 0 0], 'HorizontalAlignment', 'center',...
  'Callback','doc erp_open;');

G.Bhold = uicontrol('Parent',GUI,'Style','checkbox','Units','Normalized', Font, ...
  'Position',[.80 .50 .18 .2],'String','Hold GUI','BusyAction','queue',...
  'TooltipString','ERP File Load GUI remains open after ''Plot'' or ''Return'' commands.',...
  'Value',p.hold,'HorizontalAlignment', 'center');

% Store userdata
if exist('parent','var'), ERPOpen.parent.gui = parent; end
ERPOpen.gui = GUI;
ERPOpen.handles = G;
ERPOpen.p = p;
set(GUI,'Userdata',ERPOpen);
set(GUI,'HandleVisibility','callback');

return


function c = commandlines(d)
%tstoolbox/@description/commandlines
%
% Copyright 1997-2001 DPI Goettingen, License http://www.physik3.gwdg.de/tstool/gpl.txt

% C.Merkwirth,U.Parlitz,W.Lauterborn  DPI Goettingen 1998

narginchk(1,1);

c = d.commandlines;


%{
    Copyright (c) 2014, Philipp Krähenbühl
    All rights reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:
        * Redistributions of source code must retain the above copyright
        notice, this list of conditions and the following disclaimer.
        * Redistributions in binary form must reproduce the above copyright
        notice, this list of conditions and the following disclaimer in the
        documentation and/or other materials provided with the distribution.
        * Neither the name of the Stanford University nor the
        names of its contributors may be used to endorse or promote products
        derived from this software without specific prior written permission.

    THIS SOFTWARE IS PROVIDED BY Philipp Krähenbühl ''AS IS'' AND ANY
    EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    DISCLAIMED. IN NO EVENT SHALL Philipp Krähenbühl BE LIABLE FOR ANY
    DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
	 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
	 ON ANY THEORY OF LIABILITY, WHETHER IN 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.
%}
% Usage example:
% I = imread('peppers.png');
% os = OverSegmentation( I );
% p = Proposal('max_iou', 0.8,...
%              'unary', 130, 5, 'seedUnary()', 'backgroundUnary({0,15})',...
%              'unary', 0, 5, 'zeroUnary()', 'backgroundUnary({0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15})'
%              );
% props = p.propose( os );

classdef GopProposal
    properties (SetAccess = private, GetAccess = private)
        c_p
    end

    methods
        function obj = GopProposal( varargin )
            obj.c_p = gop_mex( 'newProposal', varargin{:} );
        end
        function r = propose(this,os)
            r = gop_mex( 'Proposal_propose', this.c_p, os.c_s );
        end
        function sobj = saveobj(this)
            error( 'You cannot load/save a Proposal object!' );
        end
        function loadobj(this, sobj)
            error( 'You cannot load/save a Proposal object!' );
        end
        function delete(this)
            gop_mex( 'freeProposal', this.c_p );
        end
    end
end


classdef Browser < handle
%Browser Interactively view Olympiad data
%   Given a data set, the Browser lets you choose variables of interest,
%   plot those variables in various ways under various groupings, perform
%   various analyses, etc. You can select experiments of interest and dig
%   into the details for that experiment.
%
%   The Browser is extensible with respect to new plots/analyses. To create
%   a new plot/analysis, subclass OlyDat.Plot.
%
%   See also OlyDat.Browser/Browser, OlyDat.Plot, OlyDat.BrowserStat,
%   OlyDat.ExperimentDetailHandler, OlyDat.DataSelector,
%   ExperimentHighlighter.

    properties (Constant)
        %FO_FILE_SYSTEM_ROOT_NFS = '/groups/sciserv/flyolympiad/';
        CURATION_FIELD_NOT_IN_DATA_WID = 'OlyDat:Browser:curationFieldNotInData';
        ACCEPTED_PV_CTOR_ARGS = {'ExperimentFileSystemFolder';'PassAllSelectedExpsToDetailHandler'};
    end

    properties (Constant)
        %FO_FILE_SYSTEM_ROOT_DEFAULT = zlclInitFO_FILE_SYSTEM_ROOT_DEFAULT();
    end

	properties (Hidden,SetAccess=private)
        fUsername; % username supplied at login

		fBrowserH; % handle to BrowserGUI
		fBrowserGData; % handle to Browser guidata
		fAxes; % handle to axes in BrowserGUI

        fAssayName; % char. name of assay, eg 'box', 'bowl', etc
        fData; % Nx1 col struct array. dataset.
        fDataTF; % Nx1 logical array. Indicators for whether an experiment is currently in consideration (true) or excluded (false)
        fEid2Idx; % map from experiment_id -> idx into fData.
        fExpDateFormat; % string; date format for exp_datetime field

        fPlots; % col array of OlyDat.Plot objects
        fSelectedPlot; % idx into fPlots for currently selected plot
        fExpCoordinator; % scalar ExperimentCoordinator object
        fComparePlots; % col array of handles to currently open "compare plots"

        fBStats; % col vec of BrowserStats
        fGroupStats; % col vec of BrowserStats
        fAuxStats; % col vec of BrowserStats
        fExpDetailStats; % col vec of BrowserStats

        fSelectedExpIdxs; % idx into fData for the currently selected experiment.
        fSelectedExpIdxsIdx; % idx into fSelectedExpIdxs for currently viewed experiment in expDetail pane.

        fCustomGroupInfo = struct('Name',cell(0,1),'MemberIdx',cell(0,1)); % struct array with fields 'Name' and 'MemberIdx'.

        fCurationFieldObjs; % col vec of OlyDat.CurationFields
        fCurationFieldObjsTblOrder; % col cellstr, names of fCurationFieldObjs to be used in Curation Manager Table
        fCurationFieldObjsTSVOrder; % col cellstr, names of fCurationFieldObjs to be used in Curation TSV file
        fCurationInfo; % map from eid to curation info struct
        fCurationMarkedThisSession; % map from eid to logical, curated this session or not
        fCurationUnsavedCurationInfo; % scalar logical; if true, there are edits to fCurationInfo that have not been writen to a curation file.
        fCurationFile; % char. Currently loaded curation filename.
        fCurationWriter; % scalar handle to CurationFileWriter object
        fCurationExpName2ExpIDMap; % map from experiment_name to experiment_id; used for reading/writing curation files
        fCurationH; % handle to BrowserCurationGUI
        fCurationGData; % handle to BrowserCuration guidata
        fCurationInfoEntryGUIName; % guiname for curation info entry dialog

        fExpDetailHandler; % scalar OlyDat.ExperimentDetailHandler object
        fPassAllSelectedExpsToDetailHandler = false; % scalar logical. If true, pass all selected exps to detailHandler, not just selected+viewed experiment.

        fExperimentFileSystemFolder = []; % File system location containing experiment folders
    end

    properties (Dependent)
        fDataIsLoaded; % scalar logical. Is there currently data loaded
        fNumExps; % scalar int
        fNumCurrentExps; % scalar int
        fNumExpDetailStats; % scalar int
        fCurrentData; % col struct array. Current data set (full data set minus exclusions)
        fAllSelectedData; % struct array. Data for currently selected experiments.
        fAllSelectedEids; % col vec of selected experimentIDs
        fSelectedDisplayedData; % scalar struct. Currently selected, displayed, data.
        fSelectedDisplayedIdx; % idx into fData for currently selected, displayed, exp.
        fBStatUIControl; % scalar handle to uicontrol. Either fBrowserGData.lbBStats or fBrowserGData.pumBStats, depending on which is visible.
        fAllCustomGroupNames; % cell array of current custom group names
        fCurrentPlot; % currently selected OlyDat.Plot object
        fCurrentAuxStat; % currently selected aux BrowserStat object. If fCurrentPlot.UsesAuxVar=='custom', this returns [].
        % TODO The handling of fCurrentAuxStat for 'custom' places an
        % unnecessary burden on OlyDat.Plot. In the case of 'custom' we can
        % still pass the current auxvar string selection to doPlot(),
        % auxVarChanged(), etc, rather than passing [].
    end

    properties (Hidden,Dependent)
        fCurationFields; % col cellstr of all curation field names
        fCurationWritableFields; % col cellstr of writable curation fields
        fCurationReadonlyFields; % col cellstr of readonly curation fields
    end

    % Dependent property accessors
    methods
        function tf = get.fDataIsLoaded(obj)
            tf = ~isempty(obj.fData);
        end
        function v = get.fNumExps(obj)
            v = numel(obj.fData);
        end
        function v = get.fNumCurrentExps(obj)
            v = nnz(obj.fDataTF);
        end
        function v = get.fNumExpDetailStats(obj)
            v = numel(obj.fExpDetailStats);
        end
        function v = get.fCurrentData(obj)
            v = obj.fData(obj.fDataTF);
        end
        function v = get.fAllSelectedData(obj)
            if isempty(obj.fSelectedExpIdxs)
                v = [];
            else
                v = obj.fData(obj.fSelectedExpIdxs);
            end
        end
        function v = get.fAllSelectedEids(obj)
            d = obj.fAllSelectedData;
            if isempty(d)
                v = zeros(0,1);
            else
                v = [d.experiment_id]';
            end
        end
        function v = get.fSelectedDisplayedData(obj)
            if isempty(obj.fSelectedExpIdxs)
                v = [];
            else
                v = obj.fData(obj.fSelectedExpIdxs(obj.fSelectedExpIdxsIdx));
            end
        end
        function v = get.fSelectedDisplayedIdx(obj)
            if isempty(obj.fSelectedExpIdxs)
                v  = [];
            else
                v = obj.fSelectedExpIdxs(obj.fSelectedExpIdxsIdx);
            end
        end
        function v = get.fAllCustomGroupNames(obj)
            v = {obj.fCustomGroupInfo.Name}';
        end
        function h = get.fBStatUIControl(obj)
            if strcmp(get(obj.fBrowserGData.lbBStats,'Visible'),'on')
                h = obj.fBrowserGData.lbBStats;
            else
                assert(strcmp(get(obj.fBrowserGData.pumBStats,'Visible'),'on'));
                h = obj.fBrowserGData.pumBStats;
            end
        end
        function v = get.fCurationFields(obj)
            v = {obj.fCurationFieldObjs.Name}';
        end
        function v = get.fCurationWritableFields(obj)
            tf = [obj.fCurationFieldObjs.TFEditable]';
            flds = obj.fCurationFieldObjs(tf);
            v = {flds.Name}';
        end
        function v = get.fCurationReadonlyFields(obj)
            tf = [obj.fCurationFieldObjs.TFEditable]';
            flds = obj.fCurationFieldObjs(~tf);
            v = {flds.Name}';
        end
        function v = get.fCurrentPlot(obj)
            v = obj.fPlots{obj.fSelectedPlot};
        end
        function v = get.fCurrentAuxStat(obj)
            crntPlot = obj.fCurrentPlot;
            if isequal(crntPlot.UsesAuxVar,'custom')
                v = [];
            else
                idx = get(obj.fBrowserGData.pumAuxVar,'Value');
                v = obj.fAuxStats(idx);
            end
        end
    end

    methods (Access=private)
        function delete(obj)
            obj.fExpDetailHandler.close();
            delete(obj.fCurationH);
            delete(obj.fComparePlots); % maybe not strictly necessary
            delete(obj.fBrowserH);
        end
    end

    methods (Static)

        % data: struct data array
        % fldname: name of field to extract
        % bstat: numel(data)-by-dataWidth array with nans filled in
        % appropriately when data(idx).fldname is empty.
        function bstat = extractSingleBStat(data,dataFieldName)
            bStatData = {data.(dataFieldName)}';
            tfMissingData = cellfun(@isempty,bStatData);
            if all(tfMissingData)
                % All data fields are empty; assume unit data width
                bstat = nan(numel(data),1);
            else
                nonEmptyData = cat(1,bStatData{~tfMissingData}); % will throw if data have different widths
                assert(size(nonEmptyData,1)==nnz(~tfMissingData));
                dataWidth = size(nonEmptyData,2);
                bstat = nan(numel(data),dataWidth);
                bstat(~tfMissingData,:) = nonEmptyData;
            end
        end

    end

	methods

        function setUsername(obj,uname)
            assert(ischar(uname));
            obj.fUsername = uname;
        end

		function obj = Browser(varargin)
        %Browser Construct a Browser and bring up the Browser GUI.
        %   obj = Browser(assayName,data,plots,bstats,groupstats,auxstats,
        %   expdetailstats,expdetailhandler,curationfldinfo,curationinfoentryguiname,varargin)
        %   constructs a browser object.
        %   assayName is eg 'box' or 'bowl' or 'gap', etc. data is a data
        %   set, typically pulled from SAGE using the OlyDat.DataSelector.
        %   Plots is a cell array of OlyDat.Plot objects. bstats,
        %   groupstats, auxstats, expdetailstats are arrays of
        %   OlyDat.BrowserStat objects. Expdetailhandler is an
        %   OlyDat.ExperimentDetailHandler object. Curationfldinfo is a
        %   struct with three fields: .objs, .tblOrder, and .tsvOrder:
        %       * curationfldinfo.objs is an array of OlyDat.CurationField
        %       objects.
        %       * curationfldinfo.tblOrder is a cellstr of the names of
        %       curationfldinfo.objs to be used in the curation manager
        %       table.
        %       * curationfldinfo.tsvOrder is a cellstr of the names of the
        %       curationfldinfo.objs to be used in the curation TSV file.
        %   Curationinfoentryguiname is the gui to use for the
        %   curationInfoEntry dialog.
        %
        %  varargin accepted PV pairs:
        %  'ExperimentFileSystemFolder': (local) path containing experiment folders
        %  'PassAllSelectedExpsToDetailHandler': scalar logical

            error(nargchk(10,inf,nargin,'struct'));

            assayName = varargin{1};
            data = varargin{2};
            plots = varargin{3};
            bstats = varargin{4};
            groupstats = varargin{5};
            auxstats = varargin{6};
            expdetailstats = varargin{7};
            expdetailhandler = varargin{8};
            curationfldinfo = varargin{9};
            curationinfoentryguiname = varargin{10};
            varargin = varargin(11:end);

            assert(ischar(assayName),'''assayName'' argument must be a string.');
            assert(isstruct(data),'''data'' argument must be a struct array.');
            assert(isstruct(curationfldinfo) && ...
                   all(isfield(curationfldinfo,{'objs';'tblOrder';'tsvOrder'})) && ...
                   isa(curationfldinfo.objs,'OlyDat.CurationField') && ...
                   iscellstr(curationfldinfo.tblOrder) && ...
                   iscellstr(curationfldinfo.tsvOrder));

%             LoginGUI(@(x)obj.setUsername(x));
%             if isempty(obj.fUsername)
%                 % for now, continue with empty username
%             end

            obj.fBrowserH = BrowserGUI(obj);
            obj.fBrowserGData = guidata(obj.fBrowserH);
			obj.fAxes = obj.fBrowserGData.ax;

            obj.fAssayName = assayName;
            obj.fPlots = plots;
            plotNames = cellfun(@(x)x.Name,obj.fPlots,'UniformOutput',false);
			set(obj.fBrowserGData.pumPlotType,'String',plotNames);
            set(obj.fBrowserGData.pumPlotType,'Value',1);
            obj.fSelectedPlot = 1;
            obj.fExpCoordinator = OlyDat.ExperimentCoordinator();
            obj.fExpCoordinator.registerObject(obj,'handleExpSelectionLimited');
            obj.fComparePlots = zeros(0,1);

            obj.fBStats = bstats(:);
            obj.fGroupStats = groupstats(:); %[OlyDat.BrowserStat('','<no group>');groupstats(:)];
            obj.fAuxStats = auxstats(:);
            obj.fExpDetailStats = expdetailstats(:);

            % BStat lb/pum
            set(obj.fBrowserGData.lbBStats,'Visible','off'); % cosmetic, minimize time of overlapping uicontrols
			set(obj.fBrowserGData.lbBStats,'String',{obj.fBStats.PrettyName}');
            set(obj.fBrowserGData.pumBStats,'String',{obj.fBStats.PrettyName}');
            set(obj.fBrowserGData.lbBStats,'Value',1);
            set(obj.fBrowserGData.pumBStats,'Value',1);
            set(obj.fBrowserGData.lbBStats,'Max',2); % enable multi-select

            set(obj.fBrowserGData.pumGroup,'String',{obj.fGroupStats.PrettyName}');
            set(obj.fBrowserGData.pumGroup,'Value',1);
            set(obj.fBrowserGData.pumGroup2,'String',{obj.fGroupStats.PrettyName}');
            set(obj.fBrowserGData.pumGroup2,'Value',1);
            obj.refreshGroupsList();

            % aux vars
            obj.fAuxStats = auxstats;
            set(obj.fBrowserGData.pumAuxVar,'String',{obj.fAuxStats.PrettyName}');

			% expfields
			obj.fExpDetailStats = expdetailstats;

            expDetailData = {expdetailstats.PrettyName}';
            expDetailData(:,2) = {''};
            set(obj.fBrowserGData.tblExpDetail,'Data',expDetailData);

            obj.fExpDetailHandler = expdetailhandler;

            obj.loadData(data);

            obj.handleExpSelectionLimited(zeros(0,1)); % start with no selection

            % Curation: browser curation state
            obj.fCurationFieldObjs = curationfldinfo.objs;
            obj.fCurationFieldObjsTblOrder = curationfldinfo.tblOrder;
            obj.fCurationFieldObjsTSVOrder = curationfldinfo.tsvOrder;
            obj.initCurationInfoAndMarkedThisSessionAndUnsavedCurationInfo();

            % Curation: curation file/writer
            obj.fCurationFile = '';
            obj.initCurationWriter();
            obj.initCurationExpName2ExpIDMap();

            % Curation: Browser Curation guis
            obj.initCurationTable();
            obj.fCurationInfoEntryGUIName = curationinfoentryguiname;

            % optional PVs
            assert(mod(numel(varargin),2)==0);
            for c = 1:2:numel(varargin)
                pname = varargin{c};
                val = varargin{c+1};
                assert(ischar(pname) && ismember(pname,obj.ACCEPTED_PV_CTOR_ARGS));
                objpropname = ['f' pname];
                obj.(objpropname) = val;
            end

            % FOR ADAM
            set(obj.fBrowserGData.pbEnterCurationInfo,'Enable','off');
            set(obj.fBrowserGData.pbCreateCustomGroup,'Enable','off');
            set(obj.fBrowserGData.pbLineReport,'Enable','off');
            set(obj.fBrowserGData.pbFileSystem,'Enable','off');

            obj.newPlot(1);
        end

        % Check that data has the necessary fields, etc to work with the
        % Browser. This may throw.
        function data = checkDataForBrowserRequiredFields(obj,data)

            % Note: Browser hardcodes against experiment_id,
            % experiment_name, exp_datetime, and fields in
            % fCurationExperimentIdentifyingFields

            % experiment_id-- used in curation, and in experiment-selection
            % signaling. mandatory field
            assert(isfield(data,'experiment_id'),'Data must have field ''experiment_id''.');
            cellEids = {data.experiment_id}';
            assert(all(cellfun(@(x)isnumeric(x)&&isscalar(x),cellEids)),'''experiment_id'' must have scalar numeric value.');
            matEids = cellfun(@double,cellEids);
            assert(numel(unique(matEids))==numel(matEids),'Data has duplicate values of ''experiment_id''.');

            % experiment_name. mandatory field
            assert(isfield(data,'experiment_name'),'Data must have field ''experiment_name''.');
            eNames = {data.experiment_name}';
            assert(iscellstr(eNames),'''experiment_name'' must have a string value.');
            assert(numel(unique(eNames))==numel(eNames),'Data has duplicate values of ''experiment_name''.');

            % exp_datetime. optional field
            obj.fExpDateFormat = '';
            if isfield(data,'exp_datetime')
                dateStrs = {data.exp_datetime}';
                assert(iscellstr(dateStrs),'''exp_datetime'' must take string values.');
                numChars = cellfun(@numel,dateStrs);
                uniqueNumChars = unique(numChars);
                assert(numel(uniqueNumChars)==1,'Invalid format for ''exp_datetime'' field.');
                switch uniqueNumChars
                    case 8
                        obj.fExpDateFormat = 'yyyymmdd';
                    case 15
                        obj.fExpDateFormat = 'yyyymmddTHHMMSS';
                    otherwise
                        error('Invalid format for ''exp_datetime'' field.');
                end
                try
                    datenum(dateStrs,obj.fExpDateFormat);
                catch %#ok<CTCH>
                    error('Invalid format for ''exp_datetime'' field.');
                end
            end

            % line_name. optional field
            if isfield(data,'line_name')
                lNames = {data.line_name}';
                assert(iscellstr(lNames),'''line_name'' must take string values.');
            else
                warning('OlyDat:Browser:noLineName','Data has no ''line_name'' field.');
                [data.line_name] = deal('Unknown line');
            end

            %%% BrowserStats

            % BStats, GroupStats, AuxStats: check fields for existence and
            % warn. If one of these are missing nothing bad will happen
            % unless the user actually chooses to use the missing stat.
            warnst = warning('backtrace','off');
            zlclThrowWarningForMissingFields(data,obj.fBStats);
            zlclThrowWarningForMissingFields(data,obj.fGroupStats(2:end)); % XXX hardcoded, first element of fGroupStats is the "no group" stat BUT NOT FOR AGGRESSION.
            %zlclThrowWarningForMissingFields(data,obj.fAuxStats);
            warning(warnst);

            % ExpDetailStats: check fields for existence and add fields if
            % one is missing. If one of these is missing,
            % experiment-selection will break because selection updates the
            % expdetail pane.
            data = zlclAddMissingFields(data,obj.fExpDetailStats);
        end

        function exportPlot(obj)
            % make tmp plot
            h = figure;
            ax = axes;
            obj.plotInAxes(ax,false);

            % get a location/file
            exportpath = ExpPP.loadConfigVal('exportpath');
            if isempty(exportpath)
              exportpath = pwd;
            end
            filterspec = fullfile(exportpath,'*.fig');
            [fname,pth] = uiputfile(filterspec,'Export figure');

            if isequal(fname,0) || isequal(pth,0)
              % none
            else
              figname = fullfile(pth,fname);
              hgsave(h,figname);
              ExpPP.saveConfigVal('exportpath',pth);
            end
            delete(h);
        end

        % TODO loadData button
        function loadData(obj,data)
        %loadData(obj,data)
        %   Load a dataset into the Browser.

            if isempty(data)
                error('OlyDat:Browser:emptyData','Data is empty.');
            end

            data = data(:);

            % Data integrity check for Browser. This will throw if
            % something is wrong with data.
            data = obj.checkDataForBrowserRequiredFields(data);

            obj.fData = data;
            obj.fDataTF = true(numel(data),1);
            eid = [data.experiment_id]';
            obj.fEid2Idx = containers.Map(eid,1:numel(eid));

            numExpStr = sprintf('%d',obj.fNumExps);
            set(obj.fBrowserGData.txNumExp,'String',numExpStr);

            if isfield(obj.fData,'exp_datetime')
                allDates = sort({obj.fData.exp_datetime});
                startDate = datenum(allDates{1},obj.fExpDateFormat);
                endDate = datenum(allDates{end},obj.fExpDateFormat);
                fromToStr = sprintf('%s to %s',datestr(startDate,2),datestr(endDate,2));
                set(obj.fBrowserGData.txFromTo,'String',fromToStr);
            else
                set(obj.fBrowserGData.txFromTo,'String','Unknown dates');
            end

            obj.refreshNumCurrentExps();
        end

        function plot(obj)
        % plot(obj)
        %   Apply the currently selected plot to the currently selected
        %   data in the main browser axis, using the currently selected
        %   statistic/grouping var/etc.

            ax = obj.fAxes;

            % Whatever plot used to be in this axis is going away, so we
            % have to call the axis DeleteFcn per the agreement with
            % OlyDat.Plot.
            fcn = get(ax,'DeleteFcn');
            if ~isempty(fcn)
                feval(fcn,ax,[]); % no eventdata
            end

            hold(ax,'off');
            cla(ax,'reset');
            axis(ax,'auto');

            obj.plotInAxes(ax,true);

            % re-select currently selected experiments to update selection
            % in main window plot
            obj.selectExperiment(obj.fAllSelectedEids);

            obj.plotRefreshed();
        end

        % The load operation a) reset the Browser curation info and then b)
        % loads curation info from the specified file for the currently
        % loaded experiments. There will almost always be experiments
        % loaded in the Browser that are not in the file; there may be
        % experiments in the file that are not loaded in the Browser.
        function curationLoadCurationFile(obj,fname)
            if nargin < 2 || isempty(fname)
                fname = obj.curationCurationFileHelper(@uigetfile);
                if isempty(fname)
                    return;
                end
            end
            assert(ischar(fname),'Filename must be a string.');
            if exist(fname,'file')~=2
                error('OlyDat:Browser:fileNotFound',...
                    'File ''%s'' not found.',fname);
            end

            tfOkToContinue = obj.curationCheckForUnsavedCurationInfo();
            if ~tfOkToContinue
                return;
            end

            % When loading a curation file, we do not want to throw the
            % warning that says "Curation field 'blah' not present in data.
            warnst = warning('off',obj.CURATION_FIELD_NOT_IN_DATA_WID);
            obj.initCurationInfoAndMarkedThisSessionAndUnsavedCurationInfo();
            warning(warnst);

            expName2CurationInfo = obj.fCurationWriter.read(fname);

            % incorporate expName2CurationInfo into fCurationInfo
            fileExpNames = expName2CurationInfo.keys;
            for c = 1:numel(fileExpNames)
                expName = fileExpNames{c};
                if obj.fCurationExpName2ExpIDMap.isKey(expName)
                    % expName is in the current dataSet
                    eid = obj.fCurationExpName2ExpIDMap(expName);
                    existingInfo = obj.fCurationInfo(eid);
                    fileInfo = expName2CurationInfo(expName);
                    existingInfo = zlclStructMerge(existingInfo,fileInfo);
                    obj.fCurationInfo(eid) = existingInfo;
                end
            end

            obj.curationRefreshCurationTableFromInfo;
            obj.fCurationFile = fname;
        end

        % Take all curation info *marked this session* and append to an
        % existing curation file. A warning is thrown if the file already
        % contains an experiment_id that is to be written. There may be
        % unmarked experiments in the Browser for which no data is written
        % to the file; there may be curation data in the file for
        % experiments not currently loaded in the Browser.
        function curationAppendCurationDataToFile(obj,fname)
            if nargin < 2 || isempty(fname)
                fname = obj.curationCurationFileHelper(@uigetfile);
                if isempty(fname)
                    return;
                end
            end
            assert(ischar(fname),'Filename must be a string.');
            if exist(fname,'file')~=2
                error('OlyDat:Browser:fileNotFound',...
                    'File ''%s'' not found.',fname);
            end

            infoToSave = obj.curationGetInfoMarkedThisSession();
            if infoToSave.isempty()
                msgbox('No curation marks have been made in this session. No action will be taken.',...
                    'No action taken');
                assert(~obj.fCurationUnsavedCurationInfo);
                obj.fCurationFile = fname;
            else
                [tfSuccess rowsWritten] = obj.fCurationWriter.append(fname,infoToSave,true);
                if tfSuccess
                    msg = sprintf('%d row(s) appended to TSV file, %d row(s) overwritten.',rowsWritten.append,rowsWritten.overwrite);
                    msgbox(msg,'Curation Marks Written');
                    obj.fCurationUnsavedCurationInfo = false;
                    obj.fCurationFile = fname;
                end
            end
        end

        % Save all "marked" curation info to a new curation file.
        function curationWriteCurationDataToNewFile(obj,fname)
            if nargin < 2 || isempty(fname)
                fname = obj.curationCurationFileHelper(@uiputfile);
                if isempty(fname)
                    return;
                end
            end
            assert(ischar(fname),'Filename must be a string.');
            pathstr = fileparts(fname);
            if exist(pathstr,'dir')~=7
                error('OlyDat:Browser:dirNotFound',...
                    'Directory ''%s'' not found.',pathstr);
            end

            infoToSave = obj.curationGetInfoMarkedThisSession();
            [tfSuccess rowsWritten] = obj.fCurationWriter.writeNewFile(fname,infoToSave);
            if tfSuccess
                msg = sprintf('%d row(s) written to TSV file.',rowsWritten);
                msgbox(msg,'Curation Marks Written');
                obj.fCurationUnsavedCurationInfo = false;
                obj.fCurationFile = fname;
            end
        end

    end

    methods (Hidden)

        function addComparePlot(obj)
            obj.fComparePlots(end+1,1) = figure('CloseRequestFcn',@(src,evt)obj.rmComparePlot(src));
            obj.fComparePlots(end,2) = axes;
            obj.plotInAxes(obj.fComparePlots(end,2),false);
        end

        function rmComparePlot(obj,hFig)
            [tf loc] = ismember(hFig,obj.fComparePlots(:,1));
            assert(tf);
            obj.fComparePlots(loc,:) = [];
            delete(hFig);
        end

        function clearComparePlots(obj)
            while ~isempty(obj.fComparePlots)
                obj.rmComparePlot(obj.fComparePlots(1,1));
            end
        end

        % cfg.stat: Nx1 array of BrowserStat objs
        % cfg.grp: scalar BrowserStat obj
        % cfg.grpVars: col int array or cellstr of values for grouping var
        % cfg.aux: scalar BrowserStat obj
        function cfg = getPlotConfig(obj)
            gdata = obj.fBrowserGData;

            % stat
            idx = get(obj.fBStatUIControl,'Value');
            cfg.stat = obj.fBStats(idx);

            % grp
            idx = get(gdata.pumGroup,'Value');
            cfg.grp = obj.fGroupStats(idx);
            cfg.grpVars = obj.hlpGroupVar(gdata.lbGroupVars,cfg.grp.ValidValues);
            % grp2
            if strcmp(get(gdata.pumGroup2,'Enable'),'on')
                idx = get(gdata.pumGroup2,'Value');
                cfg.grp2 = obj.fGroupStats(idx);
                cfg.grpVars2 = obj.hlpGroupVar(gdata.lbGroupVars2,cfg.grp2.ValidValues);
            else
                cfg.grp2 = [];
                cfg.grpVars2 = [];
            end

            cfg.aux = obj.fCurrentAuxStat;
        end
    end
    methods (Static)
        function grpvars = hlpGroupVar(lb,validvals)
            strs = get(lb,'String');
            idx = get(lb,'Value');
            if ~isempty(strs)
                grpvars = strs(idx);
                assert(~isempty(validvals));
                if isnumeric(validvals)
                    grpvars = cellfun(@str2double,grpvars);
                end
            else
                grpvars = [];
            end
        end
    end
    methods
        function plotInAxes(obj,ax,tfUpdatePlotDesc)
            if ~obj.fDataIsLoaded
                return;
            end

            plotCfg = obj.getPlotConfig();
            plt = obj.fCurrentPlot;

            % if necessary, do custom data flattening
            data = plt.preprocessData(obj.fCurrentData,plotCfg);
            assert(numel(data)==numel(obj.fCurrentData));

            % collect stats of interest in bstat matrix; this is Nexps x
            % numStats
            if plt.AllowsMultiStats
                % multi-stat data handling: for now, each stat must have a
                % scalar value. bstat is a Nexp x Nstat matrix.
                Nstat = numel(plotCfg.stat);
                bstat = nan(numel(data),Nstat);
                for c = 1:Nstat
                    bstObj = plotCfg.stat(c);
                    tmpfld = bstObj.getDataFieldName();
                    tmpdata = {data.(tmpfld)};
                    ismissingdata = cellfun(@isempty,tmpdata);
                    assert(all(cellfun(@(x)isscalar(x)&&(isnumeric(x)||islogical(x)),tmpdata(~ismissingdata))),...
                      'Found nonscalar/nonnumeric data in field %s.',tmpfld);
                    bstat(~ismissingdata,c) = cell2mat(tmpdata(~ismissingdata))';
                end
            else
                % single-stat data handling: each stat may be a row value
                % of arbitrary width. bstat is a Nexp x dataWidth matrix.
                assert(numel(plotCfg.stat)==1);

                dataFieldName = plotCfg.stat.getDataFieldName();
                bstat = OlyDat.Browser.extractSingleBStat(data,dataFieldName);
            end

            % get rid of rows that are all nans
            tfNonNanData = ~all(isnan(bstat),2);

            % grp, grpVars
            [tfGrp1,grp,tfInGroups] = obj.hlpGroupCfg(data,plotCfg,'grp','grpVars');
            [tfGrp2,grp2,tfInGroups2] = obj.hlpGroupCfg(data,plotCfg,'grp2','grpVars2');
            assert(isequal(size(data),size(grp),size(grp2),size(tfInGroups),size(tfInGroups2)));

            tf = tfNonNanData & tfInGroups & tfInGroups2;
            if plt.UsesGroupVar2
              grpArg = {grp(tf) grp2(tf)};
            else
              grpArg = grp(tf);
            end

            % ViewDetail
            vd = struct();
            vd.nstat = size(bstat,2);
            vd.nobsshown = nnz(tf);
            vd.nallnanrows = nnz(~tfNonNanData);
            [vd.tbl,~,~,lbl] = crosstab(grp(tfNonNanData),grp2(tfNonNanData));
            vd.lbl1 = lbl(1:size(vd.tbl,1),1);
            if ~tfGrp1
              assert(isscalar(vd.lbl1)&&strcmp(vd.lbl1{1},'1'));
              vd.lbl1{1} = '<all>';
            end
            vd.lbl2 = lbl(1:size(vd.tbl,2),2);
            if ~tfGrp2
              assert(isscalar(vd.lbl2)&&strcmp(vd.lbl2{1},'1'));
              vd.lbl2{1} = '<all>';
            end
            % Determine which entries in tbl are currently included in plot
            [tblshown,~,~,lblshown] = crosstab(grp(tf),grp2(tf));
            lbl1shown = lblshown(1:size(tblshown,1),1);
            lbl2shown = lblshown(1:size(tblshown,2),2);
            vd.lbl1tfshown = ismember(vd.lbl1,lbl1shown);
            vd.lbl2tfshown = ismember(vd.lbl2,lbl2shown);

            if any(tf)
%                 try
                    str = plt.doPlot(ax,data(tf),bstat(tf,:),grpArg,plotCfg,obj.fExpCoordinator);
%                 catch ME
%                     str = '';
%                     warning('OlyDat:Browser:errDuringPlot',...
%                         'Error caught during plot: %s',ME.message);
%                 end
            else
                text(0.5,0.5,'No Data','HorizontalAlignment','center','Parent',ax);
                str = '';
            end

            if tfUpdatePlotDesc
                obj.handlePlotDescription(str);
                obj.handleViewDetail(vd);
            end

        end
    end
    methods (Static)
        function [tfGroup,grp,tfInGroups] = hlpGroupCfg(data,plotCfg,fldGrp,fldGrpVars)

          % get/verify the grouping var
          % output: grp, tfGroup
          grpStat = plotCfg.(fldGrp);
          if ~isempty(grpStat) && ~isempty(grpStat.Name)
            tfGroup = true;
            grp = {data.(grpStat.Name)}';
            if iscellstr(grp)
              % none
            else
              assert(all(cellfun(@(x)isscalar(x)&&(isnumeric(x)||islogical(x)),grp)));
              grp = cell2mat(grp);
              if ~isempty(grpStat.ValidValues)
                assert(isnumeric(grpStat.ValidValues));
              end
            end
          else
            % hardcoded special case: an empty plotCfg.grp.Name
            % indicates "no group"
            tfGroup = false;
            grp = ones(size(data));
          end

          % get inclusion in selected vars (if applicable)
          grpVars = plotCfg.(fldGrpVars);
          if ~isempty(grpVars)
            assert(tfGroup);
            tfInGroups = ismemberwithequalnans(grp,grpVars);
          else
            tfInGroups = true(size(grp));
          end

          assert(isequal(size(grp),size(tfInGroups)));
        end
    end

    %% Experiment selection
    methods

        % Select the given experiment ids.
        function selectExperiment(obj,eids)
            obj.fExpCoordinator.sendSignal(obj,eids);
        end

        % Browser-only response to experiment selection (expDetail,
        % expDetailPlots). To trigger all experiment-selection-related
        % callbacks, call selectExperiment.
        function handleExpSelectionLimited(obj,eids)
            idxs = nan(size(eids));
            for c = 1:numel(eids)
                idxs(c) = obj.fEid2Idx(eids(c));
            end
            obj.handleExpSelectionLimitedIdx(idxs);
        end

        function handleExpSelectionLimitedIdx(obj,idxs)
            tfNoSelection = isempty(idxs);

            % Sset fSelectedExpIdxs, fSelectedExpIdxsIdx
            obj.fSelectedExpIdxs = idxs;
            if tfNoSelection
                obj.fSelectedExpIdxsIdx = 0;
            else
                obj.fSelectedExpIdxsIdx = 1; % By default, start by displayed first in selected group
            end

            % Enable/disable controls on Experiment Detail panel
            if tfNoSelection
                enableEnum = 'off';
            else
                enableEnum = 'on';
            end
            set(obj.fBrowserGData.cbRemoveExp,'Enable',enableEnum);
            %%% FOR ADAM
%             set(obj.fBrowserGData.pbEnterCurationInfo,'Enable',enableEnum);
%             set(obj.fBrowserGData.pbCreateCustomGroup,'Enable',enableEnum);
%             set(obj.fBrowserGData.pbLineReport,'Enable',enableEnum);
            set(obj.fBrowserGData.pbExpDetail,'Enable',enableEnum);
            set(obj.fBrowserGData.pbPrevExpDetail,'Enable',enableEnum);
            set(obj.fBrowserGData.pbNextExpDetail,'Enable',enableEnum);
%             set(obj.fBrowserGData.pbFileSystem,'Enable',enableEnum);

            obj.refreshExpDetailViewingStr();
            obj.refreshExpDetailTable();
            obj.refreshExperimentDetailPlots();
        end

    end

    %% Experiment Detail Pane

    % Detail table, detail plots, viewstr, next, prev
    methods

        function refreshExpDetailViewingStr(obj)
            set(obj.fBrowserGData.edExpDetailViewDisplayIdx,'String',num2str(obj.fSelectedExpIdxsIdx));
            str = sprintf('out of %d selected',numel(obj.fSelectedExpIdxs));
            set(obj.fBrowserGData.txExpDetailViewing,'String',str);
        end

        function refreshExpDetailTable(obj)
            detailData = get(obj.fBrowserGData.tblExpDetail,'Data');
            if ~isempty(obj.fSelectedExpIdxs)
                validateattributes(obj.fSelectedExpIdxsIdx,{'numeric'},...
                    {'integer' 'positive' '<=' numel(obj.fSelectedExpIdxs)});

                displayIdx = obj.fSelectedDisplayedIdx;
                assert(~isempty(displayIdx));
                dat = obj.fData(displayIdx);
                for c = 1:obj.fNumExpDetailStats
                    fname = obj.fExpDetailStats(c).Name;
                    detailData{c,2} = dat.(fname);
                end
                set(obj.fBrowserGData.tblExpDetail,'Data',detailData);
                set(obj.fBrowserGData.cbRemoveExp,'Value',~obj.fDataTF(displayIdx));
            else
                detailData(:,2) = {''};
                set(obj.fBrowserGData.tblExpDetail,'Data',detailData);
            end
        end

        function openLineReport(obj)
            if isempty(obj.fAssayName)
                warndlg('Assay name is unspecified; cannot open line report.',...
                    'Unknown Assay Name');
                return;
            end

            if isempty(obj.fAllSelectedData)
                selectedLines = cell(0,1);
            else
                selectedLines = {obj.fAllSelectedData.line_name}';
                % selectedLines guaranteed to be cellstr
            end

            tfOkLineName = cellfun(@(x)~strcmp(x,'Unknown line'),selectedLines);
            selectedLines = selectedLines(tfOkLineName);
            if isempty(selectedLines)
                warndlg('No (valid) lines in current selection.','No Lines Selected.');
                return;
            end

            url = OlyDat.lineReportURL(obj.fAssayName,selectedLines);
            web(url,'-browser');
        end

        function openExperimentDetailPlots(obj)
            if obj.fPassAllSelectedExpsToDetailHandler
                obj.fExpDetailHandler.open(obj.fAllSelectedData);
            else
                obj.fExpDetailHandler.open(obj.fSelectedDisplayedData);
            end
        end

        function refreshExperimentDetailPlots(obj)
            if obj.fPassAllSelectedExpsToDetailHandler
                obj.fExpDetailHandler.refresh(obj.fAllSelectedData);
            else
                obj.fExpDetailHandler.refresh(obj.fSelectedDisplayedData);
            end
        end

        function prevExpDetail(obj)
            if obj.fSelectedExpIdxsIdx==1
                % none
            else
                obj.fSelectedExpIdxsIdx = obj.fSelectedExpIdxsIdx - 1;
                obj.refreshExpDetailViewingStr();
                obj.refreshExpDetailTable();
                obj.refreshExperimentDetailPlots();
            end
        end

        function nextExpDetail(obj)
            if obj.fSelectedExpIdxsIdx==numel(obj.fSelectedExpIdxs)
                % none
            else
                obj.fSelectedExpIdxsIdx = obj.fSelectedExpIdxsIdx + 1;
                obj.refreshExpDetailViewingStr();
                obj.refreshExpDetailTable();
                obj.refreshExperimentDetailPlots();
            end
        end

        % Create a custom group out of the currently selected experiments.
        function createCustomGroup(obj,grpname)

            if ~isvarname(grpname)
                newGrpName = genvarname(grpname);
                warning('Browser:nonVarNameGrpName',...
                    'Group name ''%s'' is not a valid MATLAB variable name. Using modified name ''%s''.',...
                    grpname,newGrpName);
                grpname = newGrpName;
            end

            % Validate fieldname for data struct
            allCustomGroupNames = obj.fAllCustomGroupNames;
            if ismember(grpname,allCustomGroupNames)
                error('Browser:cantCreateCustomGroup',...
                    'A custom group with name ''%s'' already exists.',grpname);
            end
            customGroupFieldName = obj.mangleGroupName(grpname);
            if isfield(obj.fData,customGroupFieldName)
                % one in a kazillion
                error('Browser:cantCreateCustomGroup',...
                    'The field ''%s'' already exists in the data.',grpname);
            end

            % Add new field to data
            tfInGroup = false(numel(obj.fData),1);
            tfInGroup(obj.fSelectedExpIdxs) = true;
            [obj.fData(tfInGroup).(customGroupFieldName)] = deal('in group');
            [obj.fData(~tfInGroup).(customGroupFieldName)] = deal('out of group');

            % Add BrowserStat
            groupStatClass = class(obj.fGroupStats);

            % assume accepted constructor sig: ctor(name,prettyName)
            newGroupBrowserStat = feval(groupStatClass,customGroupFieldName,...
                sprintf('Custom group: %s',grpname));
            newGroupBrowserStat.ValidValues = {'in group';'out of group'};
            obj.fGroupStats(end+1,1) = newGroupBrowserStat;
            set(obj.fBrowserGData.pumGroup,'String',{obj.fGroupStats.PrettyName}');
            obj.refreshGroupsList();
        end

        % Open filesystem for currently selected experiment.
        function openExperimentFileSystem(obj)
            dat = obj.fSelectedDisplayedData;
            if ~isempty(dat) && isfield(dat,'file_system_path') && ~isempty(dat.file_system_path)

                if isempty(obj.fExperimentFileSystemFolder)
                    expPath = uigetdir([],'Specify folder containing experiments:');
                    if isequal(expPath,0)
                        return;
                    end
                    obj.fExperimentFileSystemFolder = expPath;
                end

                % Strip off exp folder and add to ExperimentFileSysFolder
                fsp = dat.file_system_path;
                [~,expFlder] = fileparts(fsp);
                if isempty(expFlder)
                    warndlg(sprintf('Corrupt file system path ''%s''. Aborting.',fsp));
                    return;
                end
                fullfsp = fullfile(obj.fExperimentFileSystemFolder,expFlder);

                while exist(fullfsp,'dir')==0
                    queststr = sprintf('Cannot find location ''%s''. Do you want to specify a new experiment folder?',...
                        fullfsp);
                    resp = questdlg(queststr,'Experiment folder inaccessible','Yes','Cancel','Yes');
                    switch resp
                        case 'Cancel'
                            return;
                    end
                    expPath = uigetdir([],'Specify folder containing experiments');
                    if isequal(expPath,0)
                        return;
                    end
                    obj.fExperimentFileSystemFolder = expPath;
                    fullfsp = fullfile(obj.fExperimentFileSystemFolder,expFlder);
                end

                if ispc
                    winopen(fullfsp);
                elseif ismac
                    system(['open ' fullfsp]);
                elseif isunix
                    web(fullfsp);
                end
            else
                warndlg('No experiment selected, or missing file_system_path.',...
                    'Can''t open experiment folder');
            end
        end

    end

    % Include/exclude
    methods

        % Exclude/include all currently selected experiments if tf is
        % true/false.
        function setRemoveExp(obj,tf,tfUpdateCurationMgr)
            if nargin < 3
                tfUpdateCurationMgr = true;
            end
            assert(~isempty(obj.fSelectedExpIdxs));

            % exclude state
            obj.fDataTF(obj.fSelectedExpIdxs) = ~tf;

            % DataSet panel
            obj.refreshNumCurrentExps;

            % Experiment Detail checkbox
            set(obj.fBrowserGData.cbRemoveExp,'Value',tf);

            % Curation Manager
            if tfUpdateCurationMgr
                eids = obj.fAllSelectedEids;
                tblData = get(obj.fCurationGData.tblCuration,'Data');
                tblEids = cell2mat(tblData(:,obj.fCurationGData.tableMetadataCurationField2Col.experiment_id));

                tfSelected = ismember(tblEids,eids);
                tblData(tfSelected,obj.fCurationGData.tableMetadataExcludedColIdx) = {logical(tf)};
                set(obj.fCurationGData.tblCuration,'Data',tblData);
            end

            % plot
            obj.plot();

            % resend experiment selection signal as appropriate
            % (not sure why I have to do this)
            if ~isempty(obj.fSelectedExpIdxs)
                eids = obj.fAllSelectedEids;
                obj.selectExperiment(eids);
            end
        end

        function unexcludeAllExps(obj)
            obj.fDataTF = true(obj.fNumExps,1);
            obj.refreshNumCurrentExps;
            obj.refreshExpDetailTable();
            curationData = get(obj.fCurationGData.tblCuration,'Data');
            curationData(:,obj.fCurationGData.tableMetadataExcludedColIdx) = {false};
            set(obj.fCurationGData.tblCuration,'Data',curationData);
            obj.plot;
        end

    end

    %% Data Set Pane
    methods

        function refreshNumCurrentExps(obj)
            str = sprintf('%d',obj.fNumCurrentExps);
            set(obj.fBrowserGData.txNumCurrentExps,'String',str);
        end

    end

    %% Misc
    methods

        function handlePlotDescription(obj,str)
          % Do some Yair voodoo to enable horizontal scrollbar. Note:
          % resizing the table (eg when figure is resized) will cause
          % horizontal scrollbar to go away. Refreshing plot will bring it
          % back.
          pause(0.01);
          drawnow; % on occassion, getting "no method setHorizontalScrollBarPolicy on class handle.handle"
          try
            jScrollPane = findjobj(obj.fBrowserGData.etPlotDescription);
            % Modify the scroll-pane's scrollbar policies
            %set(jScrollPane,'VerticalScrollBarPolicy',20);  % or: jScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED
            %jScrollPane.setHorizontalScrollBarPolicy(30);  % or: jScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED
            jViewPort = jScrollPane.getViewport;
            jEditbox = jViewPort.getComponent(0);
            jEditbox.setWrapping(false); % do *NOT* use set(...)!!!
            newPolicy = 30;% jScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED;
            set(jScrollPane,'HorizontalScrollBarPolicy',newPolicy);
          catch ME
            warning('Browser:warning','Plot description horizontal scrollbar issues: %s.',ME.message);
          end

          set(obj.fBrowserGData.etPlotDescription,'String',str);
        end

        function handleViewDetail(obj,vdinfo)
            T = vdinfo.tbl;
            T(:,end+1) = sum(T,2);
            T(end+1,:) = sum(T,1);
            T = num2str(T);

            lbl1str = vdinfo.lbl1;
            for i = 1:numel(lbl1str)
              if vdinfo.lbl1tfshown(i), lbl1str{i} = [lbl1str{i} '*']; end
            end
            lbl1str = [lbl1str;{'TOT'}];
            lbl1str = strcat(lbl1str,{' '});
            T = [char(lbl1str) T];

            lbl2str = vdinfo.lbl2;
            for i = 1:numel(lbl2str)
              if vdinfo.lbl2tfshown(i), lbl2str{i} = [lbl2str{i} '*']; end
            end
            lbl2str{end+1} = 'TOT';
            lbl2str = sprintf('%s,',lbl2str{:});
            lbl2str = ['  ' lbl2str(1:end-1)];

            str = cell(0,1);
            str{end+1,1} = sprintf('Nstat: %d. Nallnan: %d. Nshown: %d.',...
              vdinfo.nstat,vdinfo.nallnanrows,vdinfo.nobsshown);
            str{end+1,1} = 'Contingency Table';
            str{end+1,1} = 'Columns:';
            str{end+1,1} = lbl2str;
            str = [str;cellstr(T)];
            str{end+1,1} = '(*) Group currently included';

            % See comments in handlePlotDescription
            pause(0.01);
            drawnow;
            try
              jScrollPane = findjobj(obj.fBrowserGData.etViewDetail);
              jViewPort = jScrollPane.getViewport;
              jEditbox = jViewPort.getComponent(0);
              jEditbox.setWrapping(false);
              newPolicy = 30;
              set(jScrollPane,'HorizontalScrollBarPolicy',newPolicy);
            catch ME
              warning('Browser:warning','View detail horizontal scrollbar issues: %s.',ME.message);
            end

            set(obj.fBrowserGData.etViewDetail,'String',str);
        end

        function statChanged(obj)
            plt = obj.fCurrentPlot;
            if plt.AutoReplotAfterStatChange
              obj.plot();
            else
              obj.plotNeedsRefresh();
            end
        end

        function groupChanged(obj)
            obj.refreshGroupsList();
            obj.callPlotGroupChanged();
            plt = obj.fCurrentPlot;
            if plt.AutoReplotAfterGroupChange
              obj.plot();
            else
              obj.plotNeedsRefresh();
            end
        end
        function group2Changed(obj)
            obj.refreshGroupsList();
            plt = obj.fCurrentPlot;
            if plt.AutoReplotAfterGroupChange
              obj.plot();
            else
              obj.plotNeedsRefresh();
            end
        end
        function groupOrGroup2SelectionChanged(obj)
            plt = obj.fCurrentPlot;
            if plt.AutoReplotAfterGroupChange
              obj.plot();
            else
              obj.plotNeedsRefresh();
            end
        end

        function plotNeedsRefresh(obj)
          set(obj.fBrowserGData.pbPlot,'foregroundcolor',[1 0 0],'fontweight','bold');
        end
        function plotRefreshed(obj)
          set(obj.fBrowserGData.pbPlot,'foregroundcolor',[0 0 0],'fontweight','normal');
        end

        function callPlotGroupChanged(obj)
            currentPlot = obj.fCurrentPlot;
            grpIdx = get(obj.fBrowserGData.pumGroup,'Value');
            grpStat = obj.fGroupStats(grpIdx);
            hCtrls = struct();
            hCtrls.pumAuxVar = obj.fBrowserGData.pumAuxVar;
            currentPlot.groupChanged(grpStat,obj.fData,hCtrls);
        end

        function refreshGroupsList(obj)
          gSets = struct('pum',{'pumGroup' 'pumGroup2'},'lb',{'lbGroupVars' 'lbGroupVars2'});
          for gIdx = [1 2]
            pumTag = gSets(gIdx).pum;
            lbTag = gSets(gIdx).lb;

            grpIdx = get(obj.fBrowserGData.(pumTag),'Value');
            grp = obj.fGroupStats(grpIdx);
            groupVals = grp.ValidValues;
            if ~isempty(groupVals) && isnumeric(groupVals);
                groupVals = num2cell(groupVals);
                groupVals = cellfun(@num2str,groupVals,'UniformOutput',false);
            end

            previousGroupVals = get(obj.fBrowserGData.(lbTag),'String');
            if isequal(previousGroupVals,groupVals)
                % none
            else
                set(obj.fBrowserGData.(lbTag),'String',groupVals,'Value',1:numel(groupVals));
            end
          end
        end

        function newPlot(obj,idx)
            if nargin < 2
                idx = get(obj.fBrowserGData.pumPlotType,'Value');
            end
            plot = obj.fPlots{idx};

            %% Manage BStat uicontrol
            tfLastPlotAllowedMultiStats = strcmp(get(obj.fBrowserGData.lbBStats,'Visible'),'on');
            if plot.AllowsMultiStats==tfLastPlotAllowedMultiStats
                % previous plot and current plot agree on allowing
                % multistats
            elseif plot.AllowsMultiStats
                % previous plot didn't allow multistat, new one does
                currentBStat = get(obj.fBrowserGData.pumBStats,'Value');
                set(obj.fBrowserGData.lbBStats,'Value',currentBStat);

                set(obj.fBrowserGData.lbBStats,'Visible','on');
                set(obj.fBrowserGData.pumBStats,'Visible','off');
            else
                % previous plot allowed multistat, new one doesn't
                currentBStat = get(obj.fBrowserGData.lbBStats,'Value');
                set(obj.fBrowserGData.pumBStats,'Value',currentBStat(1));

                set(obj.fBrowserGData.pumBStats,'Visible','on');
                set(obj.fBrowserGData.lbBStats,'Visible','off');
            end

            if isequal(plot.UsesAuxVar,true) || isequal(plot.UsesAuxVar,'custom')
                set(obj.fBrowserGData.pumAuxVar,'Enable','on');
            elseif isequal(plot.UsesAuxVar,false)
                set(obj.fBrowserGData.pumAuxVar,'Enable','off');
            else
                assert(false,'Invalid value of UsesAuxVar.');
            end

            if plot.UsesGroupVar2==true
                tmpEnable = 'on';
            else
                tmpEnable = 'off';
            end
            set(obj.fBrowserGData.pumGroup2,'Enable',tmpEnable);
            set(obj.fBrowserGData.lbGroupVars2,'Enable',tmpEnable);
            set(obj.fBrowserGData.pbUseAllGroupVals2,'Enable',tmpEnable);

            set(obj.fBrowserGData.txGroupVar,'String',plot.GroupVarLabel);
            set(obj.fBrowserGData.txGroupVar2,'String',plot.GroupVar2Label);

            obj.fSelectedPlot = idx;

            obj.callPlotGroupChanged();
            obj.callPlotAuxVarChanged();

            obj.plot();
        end

        function callPlotAuxVarChanged(obj)
            auxStat = obj.fCurrentAuxStat;
            crntPlot = obj.fCurrentPlot;
            hCtrls = struct();
            hCtrls.pumAuxVar = obj.fBrowserGData.pumAuxVar;
            crntPlot.auxVarChanged(auxStat,obj.fData,hCtrls);
        end

        function auxVarChanged(obj)
            obj.callPlotAuxVarChanged();
            obj.plot();
        end

        function browserGUIWantsToClose(obj)
            tfOkToContinue = obj.curationCheckForUnsavedCurationInfo();
            if tfOkToContinue
                delete(obj);
            end
        end

    end

    %% Curation
    methods

        % Initialize fCurationInfo prop based on fData. Both read-only (eg
        % experiment-identifying) and writeable fields (eg curation marks)
        % are read from fData.
        function initCurationInfoAndMarkedThisSessionAndUnsavedCurationInfo(obj)

            % init fCurationInfo to contain empty structs with the right fields
            fields = obj.fCurationFields;
            Nfields = numel(fields);
            data = cell(numel(obj.fData),Nfields);
            curationInfoStructs = cell2struct(data,fields,2);
            curationInfoStructs = num2cell(curationInfoStructs);
            eids = {obj.fData.experiment_id}';
            obj.fCurationInfo = containers.Map(eids,curationInfoStructs);

            % init fCurationInfo to match what is in fData
            for c = 1:Nfields
                fld = fields{c};
                if ~isfield(obj.fData,fld)
                  % AL20150127, Don't bother with this warning
%                     warnst = warning('backtrace','off');
%                     warning(obj.CURATION_FIELD_NOT_IN_DATA_WID,...
%                         'Curation field ''%s'' not present in dataset. Initializing this field to empty value.',fld);
%                     warning(warnst);
                end
            end
            obj.curationResetCurationInfoToData(cell2mat(eids));

            obj.fCurationMarkedThisSession = containers.Map(eids,repmat({false},size(eids)));
            obj.fCurationUnsavedCurationInfo = false;
        end

        function initCurationWriter(obj)
            [tf loc] = ismember(obj.fCurationFieldObjsTSVOrder,obj.fCurationFields);
            assert(all(tf));
            tsvObjs = obj.fCurationFieldObjs(loc);
            tsvNames = {tsvObjs.TSVName}';
            assert(all(cellfun(@(x)~isempty(x),tsvNames)));
            obj.fCurationWriter = OlyDat.CurationFileWriter(tsvObjs);
        end

        function initCurationExpName2ExpIDMap(obj)
            m = containers.Map('KeyType','char','ValueType','double');
            Nexps = obj.fNumExps;
            fdata = obj.fData;
            assert(Nexps>0);
            assert(Nexps==numel(fdata));
            for c = 1:Nexps
                eid = fdata(c).experiment_id;
                expname = fdata(c).experiment_name;
                assert(~m.isKey(eid));
                m(expname) = eid;
            end
            obj.fCurationExpName2ExpIDMap = m;
        end

        function initCurationTable(obj)

            % construct/open BrowserCurationGUI
            [tf loc] = ismember(obj.fCurationFieldObjsTblOrder,obj.fCurationFields);
            assert(all(tf));
            tblObjs = obj.fCurationFieldObjs(loc);
            tblNames = {tblObjs.TableName}';
            assert(all(cellfun(@(x)~isempty(x),tblNames)));
            obj.fCurationH = BrowserCurationGUI(obj,tblObjs);
            set(obj.fCurationH,'CloseRequestFcn',@(src,evt)set(src,'Visible','off'));
            obj.fCurationGData = guidata(obj.fCurationH);

            % initialize table contents in order of fCurationInfo
            assert(~isempty(obj.fCurationInfo));
            curationInfoStructs = cell2mat(obj.fCurationInfo.values);
            assert(numel(curationInfoStructs)==obj.fNumExps);

            % init table data
            tblData = cell(numel(curationInfoStructs),numel(obj.fCurationGData.tableMetadataCol2Field));

            % EID
            eids = {curationInfoStructs.experiment_id}';
            colIdx = obj.fCurationGData.tableMetadataCurationField2Col.experiment_id;
            tblData(:,colIdx) = eids;

            % initialize excluded col
            assert(all(obj.fDataTF),'Expect all experiments to be unexcluded at this time.');
            tblData(:,obj.fCurationGData.tableMetadataExcludedColIdx) = {false};

            set(obj.fCurationGData.tblCuration,'Data',tblData);

            % set up rest of table
            obj.curationRefreshCurationTableFromInfo();
        end

        function fname = curationCurationFileHelper(obj,uiFileFcn)
            if ~isempty(obj.fCurationFile)
                hintFile = obj.fCurationFile;
            else
                hintFile = pwd;
            end
            [fname pname] = uiFileFcn(OlyDat.CurationFileWriter.fileExtension,...
                'Select curation file',hintFile);
            if isnumeric(fname)
                % user cancelled
                fname  = [];
            else
                fname = fullfile(pname,fname);
            end
        end

        function curationOpenCurationManager(obj)
            set(obj.fCurationH,'Visible','on');
        end

        % Refresh curation table information using fCurationInfo (exclude
        % column unaffected). This is done by looking up info for each
        % experiment_id, in table order. Row-order of table is unchanged.
        function curationRefreshCurationTableFromInfo(obj)
            tblData = get(obj.fCurationGData.tblCuration,'Data');
            info = obj.fCurationInfo;
            Nrows = size(tblData,1);
            assert(Nrows==info.size(1)); % maybe unnecessary in future when tbl shows restricted view

            fldnames = fieldnames(obj.fCurationGData.tableMetadataCurationField2Col);

            for c = 1:Nrows
                row = tblData(c,:);
                eid = row{obj.fCurationGData.tableMetadataCurationField2Col.experiment_id};
                assert(info.isKey(eid));
                s = info(eid);
                for d = 1:numel(fldnames)
                    % custom conversion for table
                    fld = fldnames{d};
                    val = s.(fld);
%                     switch fld
%                         case 'manual_pf'
%                             if isempty(val)
%                                 val = ' '; % for empty option in popupmenu. Is there a workaround for this?
%                             else
%                                 assert(any(strcmp(val,{'U';'P';'F'})));
%                             end
%                         case {'flag_redo' 'flag_review'} % xxx other flag fields?
%                             if isempty(val)
%                                 val = ' ';
%                             else
%                                 assert(any(strcmp(val,{'0';'1'})));
%                             end
%                         otherwise
%                             %none
%                     end
                    row{obj.fCurationGData.tableMetadataCurationField2Col.(fld)} = val;
                end
                tblData(c,:) = row;
            end

            set(obj.fCurationGData.tblCuration,'Data',tblData);
        end

        % Methods that enable editing of fCurationInfo:
        % * curationLoadCurationFile
        % * curationUpdateCurationData
        % * curationResetCurationInfoToData
        % * curationEditCurationInfoForCurrentExp
        % * curationClearCurationInfoForCurrentExp
        % * curationMarkAllUncuratedAsPass

        % Update fCurationInfo based on infoStruct.
        function curationUpdateCurationData(obj,infoStruct)
            disp('CURRENTLY UNUSED METHOD');
            eid = infoStruct.experiment_id;
            assert(obj.fCurationInfo.isKey(eid));
            existingInfo = obj.fCurationInfo(eid);
            existingInfo = zlclStructMerge(existingInfo,infoStruct);
            obj.fCurationInfo(eid) = existingInfo;
            obj.fCurationMarkedThisSession(eid) = true;
            obj.fCurationUnsavedCurationInfo = true;
        end

        % reset the curation info for the experiment ID eids to what is
        % present in fData.
        function curationResetCurationInfoToData(obj,eids)

            % ML pre-memory mgr enhancement, this propget is amazingly slow
            % for large fData. So get this once, outside loop.
            allData = obj.fData;

            for id = eids(:)'
                info = obj.fCurationInfo(id);
                idx = obj.fEid2Idx(id);
                dat = allData(idx);

                flds = fieldnames(info);
                for c = 1:numel(flds)
                    fld = flds{c};
                    if isfield(dat,fld)
                        info.(fld) = dat.(fld);
                    else
                        info.(fld) = '';
                    end
                end

                obj.fCurationInfo(id) = info;
            end
        end

        % edit curation fields for all currently selected exps.
        % s: struct with curation fields
        function curationEditCurationInfoForCurrentExp(obj,s)
            assert(isstruct(s));
            assert(~isempty(obj.fSelectedExpIdxs));

            assert(~any(isfield(s,obj.fCurationReadonlyFields)));

            eids = obj.fAllSelectedEids;
            for c = 1:numel(eids)
                id = eids(c);
                info = obj.fCurationInfo(id);
                info = zlclStructMerge(info,s);
                obj.fCurationInfo(id) = info;
                obj.fCurationMarkedThisSession(id) = true;
            end

            obj.fCurationUnsavedCurationInfo = true;
            obj.curationRefreshCurationTableFromInfo();
        end

        % Warning: this method also resets fCurationMarkedThisSession for
        % the current experiment(s) to false. It is as if the experiment was
        % never touched. However, note that if a curation file was loaded
        % at some point, this method does NOT restore the loaded info; the
        % info is initialized based on fData. This may be a little weird but it's
        % too edgy to worry about in the absence of real usage information.
        function curationClearCurationInfoForCurrentExp(obj)
            assert(~isempty(obj.fSelectedExpIdxs));

            eids = obj.fAllSelectedEids;
            obj.curationResetCurationInfoToData(eids);
            for id = eids(:)'
                obj.fCurationMarkedThisSession(id) = false; % take note
            end

            %obj.fCurationUnsavedCurationInfo = true; Don't touch
            %fCurationUnsavedCurationInfo.
            obj.curationRefreshCurationTableFromInfo();
        end

        % Open curation info entry modal dialog, prepopulated with
        % currently selected exps.
        function curationOpenCurationInfoEntry(obj)
            eids = obj.fAllSelectedEids;
            for c = numel(eids):-1:1
                info(c) = obj.fCurationInfo(eids(c));
            end
            cbkFcn = @(s)obj.curationEditCurationInfoForCurrentExp(s);
            feval(obj.fCurationInfoEntryGUIName,info,obj.fUsername,cbkFcn);
        end

        % If there is unsaved curation info, ask user if it is okay to
        % continue (with an operation which will cause them to lose that
        % info).
        function tfOkToContinue = curationCheckForUnsavedCurationInfo(obj)
            tfOkToContinue = true;
            if obj.fCurationUnsavedCurationInfo
                questStr = 'You have unsaved manual curation edits. You will lose those edits if you continue. Would you like to continue anyway?';
                titleStr = 'Unsaved curation data';
                btn = questdlg(questStr,titleStr,'Continue anyway','No','No');
                switch btn
                    case {'' 'No'}
                        tfOkToContinue = false;
                        return;
                    case 'Continue anyway'
                        % none
                end
            end
        end

        % Get curation info marked this session.
        function expName2CurationInfo = curationGetInfoMarkedThisSession(obj)
            allEids = cell2mat(obj.fCurationMarkedThisSession.keys);
            tfMarked = cell2mat(obj.fCurationMarkedThisSession.values);
            markedEids = allEids(tfMarked);

            expName2CurationInfo = containers.Map('KeyType','char','ValueType','any');
            for eid = markedEids(:)'
                info = obj.fCurationInfo(eid);
                expname = info.experiment_name;
                assert(~expName2CurationInfo.isKey(expname));
                expName2CurationInfo(expname) = info;
            end
        end

% CURRENTLY UNUSED
%         function expName2CurationInfo = curationGetAllMarkedInfo(obj)
%             expName2CurationInfo = containers.Map('KeyType','char','ValueType','any');
%             allEids = cell2mat(obj.fCurationInfo.keys);
%             for eid = allEids(:)'
%                 info = obj.fCurationInfo(eid);
%                 expname = info.experiment_name;
%                 if obj.curationInfoIsManualMarked(info)
%                     assert(~expName2CurationInfo.isKey(expname));
%                     expName2CurationInfo(expname) = info;
%                 end
%             end
%         end

        function curationMarkAllUncuratedAsPass(obj)
            curationInfo = obj.fCurationInfo;
            eids = cell2mat(curationInfo.keys);
            for id = eids(:)'
                info = curationInfo(id);
                if ~obj.curationInfoIsManualMarked(info)
                    info = obj.curationInfoMarkAsManualPass(info);
                    curationInfo(id) = info;
                    obj.fCurationMarkedThisSession(id) = true;
                end
            end
            obj.curationRefreshCurationTableFromInfo();
            obj.fCurationUnsavedCurationInfo = true;
        end

        % For the moment this hardcodes against fields.
        function tf = curationInfoIsManualMarked(obj,info) %#ok<MANU>
            tf = ~isempty(info.manual_curator) || ...
                 ~isempty(info.manual_curation_date);
        end

        % For the moment this hardcodes against fields.
        function info = curationInfoMarkAsManualPass(obj,info)
           info.manual_pf = 'P';
           info.manual_curator = obj.fUsername;
           info.manual_curation_date = datestr(now,30);
        end

    end

    methods (Static)

        function name = mangleGroupName(grpname)
            name = sprintf('Custom_Group_%s',grpname);
        end

    end

end

function s = zlclStructMerge(s,news)
newfields = fieldnames(news);
for c = 1:numel(newfields)
    fld = newfields{c};
    s.(fld) = news.(fld);
end
end

function zlclThrowWarningForMissingFields(data,browserStatObjs)
tfBuried = [browserStatObjs.tfBuriedStat]';
fldnames = {browserStatObjs.Name}';
fldnames = fldnames(~tfBuried); % don't bother checking score stats
for c = 1:numel(fldnames)
    if ~isfield(data,fldnames{c})
        warning('OlyDat:Browser:missingField',...
            'Specified Browser stat ''%s'' does not have a corresponding field in the data. Selection of this statistic may cause an error.',...
            fldnames{c});
    end
end
end

function data = zlclAddMissingFields(data,browserStatObjs)
tfBuried = [browserStatObjs.tfBuriedStat]';
fldnames = {browserStatObjs.Name}';
fldnames = fldnames(~tfBuried); % don't bother checking score stats
for c = 1:numel(fldnames)
    if ~isfield(data,fldnames{c})
        warning('OlyDat:Browser:missingField',...
            'Specified experiment detail statistic ''%s'' is not a field in the data. Adding field with values ''<unknown>''.',...
            fldnames{c});
        [data.(fldnames{c})] = deal('<unknown>');
    end
end
end

% function v = zlclInitFO_FILE_SYSTEM_ROOT_DEFAULT
% if ispc
%     v = '\\dm10\flyolympiad\';
% elseif ismac
%     v = '/Volumes/flyolympiad/'; % typical mount location (?)
%     %v = 'smb://dm10/flyolympiad';
% elseif isunix
%     % typical mount location?
%     v = '/groups/sciserv/flyolympiad/';
% else
%     assert(false);
% end
% end


function identifiersDBs = getRegisteredDatabases()
% Get all databases registered with identifiers.org along with their
% pattern and and prefix data.
%
% USAGE:
%    identifiersDBs = getRegisteredDatabases()
%
% OUPUTS:
%    identifiersDBs:        A Struct with data on each database registered
%                           with identifiers.org (needs web access).
%                           The struct will have the following fields:
%                            * .name:       The name of the database
%                            * .pattern:    A regexp pattern allowed with the db.
%                            * .prefix:     The prefix of the db on identifiers.org

persistent databases

if isempty(databases)
    % load from external site
    try
        dbs = webread('https://registry.api.identifiers.org/resolutionApi/getResolverDataset');
    catch
        error('Could not load the databases registered with identifiers.org.\nThis is likely due to a missing internet connection.\nPlease try this again later');
    end
    dbs = dbs.payload.namespaces;
    % extract relevant information
    dbnames = {dbs.name};
    dbpatterns = {dbs.pattern};
    dbprefix = {dbs.prefix};
    % collate as struct
    databases = struct('name',dbnames,'pattern',dbpatterns,'prefix',dbprefix);
end

identifiersDBs = databases;


function setPreferences()
%SETPREFERENCES Sets MATLAB preferences
%
% Author: Jonathan Karr, jkarr@stanford.edu
% Author: Derek Macklin, macklin@stanford.edu
% Affilitation: Covert Lab, Department of Bioengineering, Stanford University
% Last updated: 9/6/2011

%prevent MATLAB from using OpenGL to avoid segmentation faults
set(0, 'DefaultFigureRenderer', 'painters')
opengl('neverselect')

%set character encoding to UTF-8
if ~isdeployed
    slCharacterEncoding('UTF-8');
end

%default font
set(0, 'defaultAxesFontName', 'Arial')
set(0, 'defaultTextFontName', 'Arial')


function test_bug731

% MEM 2gb
% WALLTIME 00:10:00
% DEPENDENCY

% this function checks problems encountered with the neuromag digital trigger reading

cfg.dataset = dccnpath('/home/common/matlab/fieldtrip/data/test/bug731/test_bug731.fif');
event       = ft_read_event(cfg.dataset);

% issue confirmed -> updated bug but will wait for Alex' input before proceeding


classdef Segment < Data

    properties (Access = public)
        file;
        data;
        label;
        eventIdx;
        startSample;
        endSample;
    end

    methods (Access = public)
        function obj = Segment(file,data,label,eventIdx)
            if nargin >= 1
                obj.file = file;
                if nargin >= 2
                    obj.data = data;
                    if nargin >= 3
                        obj.label = label;
                        if nargin >= 4
                            obj.eventIdx = eventIdx;
                        end
                    end
                end
            end
            obj.type = DataType.kSegment;
        end
    end

    methods (Static)
        %copies a segment (does not copy the data)
        function newSegment = CreateSegmentWithSegment(segment)
            newSegment = Segment(segment.file,[],segment.label,segment.eventIdx);
            newSegment.startSample = segment.startSample;
            newSegment.endSample = segment.endSample;
        end
    end
end


function tapas_BLT_runStimAnswer(params)
% tapas_BLT_runStimAnswer is showing the stimulus question / answer screen

% Show stimulus question
Screen('DrawTexture', params.screen.window, params.text.imgstimanswer, [], params.screen.fit, 0);

% Flip and log
Screen('Flip', params.screen.window);
tapas_BLT_logEvent('stim_ans_on',params);

tic()
ellapsed = 0;
responded = 0;

while ellapsed < params.dur.stimAnsTimeout
    ellapsed = toc();
    keyCode = detectkey(params);

    if keyCode == params.keys.one
        responded = 1;
        rt      = ellapsed;

        % Code the answer
        if params.answertype == 1
            answer  = 1; % Yes
        elseif params.answertype == 2
            answer  = 0; % No
        end

        % Draw and flip the screen
        Screen('DrawTexture', params.screen.window, params.text.imgstimanswerSL, [], params.screen.fit, 0);
        Screen('Flip', params.screen.window);

    elseif keyCode == params.keys.two
        responded = 1;
        rt      = ellapsed;

        % Code the answer
        if params.answertype == 1
            answer  = 0; % No
        elseif params.answertype == 2
            answer  = 1; % Yes
        end

        % Draw and flip the screen
        Screen('DrawTexture', params.screen.window, params.text.imgstimanswerSR, [], params.screen.fit, 0);
        Screen('Flip', params.screen.window);
    end
end

% Display warning if too slow
if responded == 0
    Screen('DrawTexture', params.screen.window, params.text.imgpredtimeout , [], params.screen.fit, 0);
    Screen('Flip', params.screen.window);
    wait2_escapeOption(params.dur.showpredTimeoutScreen, params);
    answer = NaN;
    rt = NaN;
end

% Log the data
tapas_BLT_logData('stim_answer_rt', params, rt);
tapas_BLT_logData('stim_answer', params, answer);
tapas_BLT_logEvent('stim_ans_off',params);

end