cosmo meeg layout collection

function db=cosmo_meeg_layout_collection()
% return supported MEEG channel layouts
%
% db=cosmo_meeg_layout_collection()
%
% Output:
%     db            Nx1 cell for N layouts. Each elements has the following
%                   fields:
%                   .pos      Mx2 x and y coordinates for M channels
%                   .width    Mx1 width of channel
%                   .height   Mx1 height of channel
%                   .label    Mx1 cell string with channel labels
%                   .name     string with filename of layout
%
% Example:
%     % (This example requires FieldTrip)
%     cosmo_skip_test_if_no_external('fieldtrip');
%     %
%     % get all layouts
%     layouts=cosmo_meeg_layout_collection();
%     %
%     % find index of neuromag306cmb layout
%     names=cellfun(@(x)x.name,layouts,'UniformOutput',false);
%     i=find(cosmo_match(names,'neuromag306cmb.lay'));
%     %
%     % show neuromag306 combined planar layout (with 102 channels)
%     pl_layout=layouts{i};
%     cosmo_disp(pl_layout.label)
%     %|| { 'MEG0112+0113'
%     %||   'MEG0122+0123'
%     %||   'MEG0132+0133'
%     %||         :
%     %||   'MEG2642+2643'
%     %||   'COMNT'
%     %||   'SCALE'        }@104x1
%     cosmo_disp([pl_layout.pos pl_layout.width pl_layout.height])
%     %|| [ -0.408     0.273    0.0717    0.0791
%     %||   -0.328     0.306    0.0717    0.0791
%     %||   -0.377     0.179    0.0717    0.0791
%     %||      :         :         :         :
%     %||    0.373    -0.104    0.0717    0.0791
%     %||   -0.547      -0.5    0.0717    0.0791
%     %||    0.547      -0.5    0.0717    0.0791 ]@104x4
%     pl_layout.name
%     %|| 'neuromag306cmb.lay'
%
% Note:
%   - this function requires FieldTrip, as it uses its collection of
%     layouts
%   - the output from this function is similar to FieldTrip's
%     ft_prepare_layout.
%   - this function caches previously read layouts, for optimization
%     reasons. run "clear functions" to reset the cache.
%
% #   For CoSMoMVPA's copyright information and license terms,   #
% #   see the COPYING file distributed with CoSMoMVPA.           #

    % cache the layouts, so that after the first call layouts don't have to
    % be read from disc
    persistent cached_layout_db;

    if isempty(cached_layout_db)
        cached_layout_db=read_all_layouts();
    end
    db=cached_layout_db;

function layouts=read_all_layouts()
    cosmo_check_external('fieldtrip');

    % see where fieldtrip is
    ft_dir=fileparts(which('ft_defaults'));
    lay_dir=fullfile(ft_dir,'template','layout');

    % get layout filenames
    lay_fns=dir(fullfile(lay_dir,'*.lay'));
    nlay=numel(lay_fns);

    if nlay==0, error('No layout founds in %s', lay_dir); end

    layout_names={lay_fns.name}';

    % allocate space for output
    layouts=cell(nlay,1);

    % read each layout
    for k=1:nlay
        lay_name=layout_names{k};
        lay_fn=fullfile(lay_dir,lay_name);
        lay=cosmo_meeg_read_layout(lay_fn);
        lay.name=lay_name;
        layouts{k}=lay;
    end

    layouts=apply_fixers(layouts);

function layouts=apply_fixers(layouts)
    fixers={@fix_elec1020_old_fieldtrip,...
            @fix_make_ft_compatible};
    for j=1:numel(fixers)
        fixer=fixers{j};
        layouts=fixer(layouts);
    end


function db=fix_elec1020_old_fieldtrip(db)
    % old fieldtrip had channel locations differently;
    % fix that there so that the unit tests pass
    i=find_layout(db,'elec1020.lay');
    lay=db{i};
    if isequal(lay.pos(1,:),[-0.308949 0.951110]) && ...
                isequal(unique(lay.width),.75)
        suffix=cellfun(@(x)x(end),lay.label)';

        suffix1=suffix=='1';
        suffix2=suffix=='2';

        assert(isequal(lay.label(suffix1),{'Fp1','O1'}'));
        assert(isequal(lay.label(suffix2),{'Fp2','O2'}'));

        lay.pos(suffix1,:)=[-.38 .89111; -.38 -.89111];
        lay.pos(suffix2,:)=[.38  .891004; .38 -.891004];
        lay.width(:)=.35;
        lay.height(:)=.25;
        db{i}=lay;
    end

function i=find_layout(db,name)
    names=cellfun(@(x)x.name,db,'UniformOutput',false);
    i=find(cosmo_match(names,name));
    assert(numel(i)==1);


function layouts=fix_make_ft_compatible(layouts)
    for j=1:numel(layouts)
        layouts{j}=make_single_ft_compatible(layouts{j});
    end

function layout=make_single_ft_compatible(layout)
    processors={@layout_ft_add_outline,...
                @layout_ft_scale,...
                @layout_ft_add_misc_labels};

    for k=1:numel(processors)
        processor=processors{k};
        layout=processor(layout);
    end

function layout=layout_ft_add_outline(layout)
    % add outline and mask to the layout
    alpha=(0:.01:1)'*(2*pi);
    head=[cos(alpha) sin(alpha)]*.5;
    nose=[0.09  0.496;
          0     0.575;
          -0.09 0.496];
    right_ear=[ 0.4970    0.0555
                0.5100    0.0775
                0.5180    0.0783
                0.5299    0.0746
                0.5419    0.0555
                0.5400   -0.0055
                0.5470   -0.0932
                0.5320   -0.1313
                0.5100   -0.1384
                0.4890   -0.1199 ];
    left_ear=bsxfun(@times,[-1 1],right_ear);

    layout.outline={head, nose, right_ear, left_ear};
    layout.mask={head};

function layout=layout_ft_scale(layout)
    % do not consider the COMNT and SCALE channels
    chan_msk=~cosmo_match(layout.label,{'COMNT','SCALE'});

    chan_pos=layout.pos(chan_msk,:);
    extent=(max(chan_pos)-min(chan_pos));

    % put everything in circle around origin with radius .45
    layout.pos=(bsxfun(@rdivide,bsxfun(@minus,...
                    layout.pos,min(chan_pos)),extent))*.9-.45;
    layout.width=layout.width/extent(1);
    layout.height=layout.height/extent(2);

function stacked_layout=stack_channels(layouts)
    % helper function to stack the channels
    % if any element is empty it is just ignored
    keys={'pos','width','height','label'};
    nlayout=numel(layouts);
    nkeys=numel(keys);

    stacked_layout=layouts{1};
    for k=1:nkeys
        key=keys{k};
        values_cell=cell(nlayout,1);
        keep=false(nlayout,1);
        for j=1:nlayout
            layout=layouts{j};
            if ~isempty(layout)
                values_cell{j}=layout.(key);
                keep(j)=true;
            end
        end
        stacked_layout.(key)=cat(1,values_cell{keep});
    end

function layout=layout_ft_add_misc_labels(layout)
    comnt_layout=get_single_label(layout,'COMNT',[-.45 -.45]);
    scale_layout=get_single_label(layout,'SCALE',[ .45 -.45]);
    layout=stack_channels({layout,comnt_layout,scale_layout});

function y=get_single_label(x,label,pos)
    % return a layout
    if cosmo_match({label},x.label);
        y=[];
    else
        y.width=mean(x.width,1);
        y.height=mean(x.height,1);
        y.label={label};
        y.pos=pos;
    end