cosmo surface dataset

function ds=cosmo_surface_dataset(fn, varargin)
% Returns a dataset structure based on surface mesh data
%
% ds=cosmo_surface_dataset(filename, varargin)
%
% Inputs:
%   filename          filename of surface data to be loaded. Currently
%                     supported are '.niml.dset' (AFNI/SUMA NIML),
%                     'smp' (BrainVoyager surface maps), and 'gii' (GIFTI).
%                     Also supported are structs as provided by
%                     afni_niml_readsimple, or objects provided by xff or
%                     the MATLAB/Octave GIfTI Library.
%   'targets', t      Px1 targets for P samples; these will be stored in
%                     the output as ds.sa.targets
%   'chunks', c       Px1 chunks for P samples; these will be stored in the
%                     the output as ds.sa.chunks
% Output:
%   ds                dataset struct
%
% Examples:
%     % (this example requires the surfing toolbox)
%     cosmo_skip_test_if_no_external('afni');
%     %
%     % construct AFNI NIML dataset struct
%     cosmo_check_external('afni');
%     niml=struct();
%     niml.data=[1 2; 3 4; 5 6];
%     niml.node_indices=[1 20 201];
%     niml.stats={'Ttest(10)','Zscore()'};
%     %
%     % make surface dataset
%     % (a filename of a NIML dataset in ASCII format is supported as well)
%     ds=cosmo_surface_dataset(niml);
%     cosmo_disp(ds)
%     %|| .samples
%     %||   [ 1         3         5
%     %||     2         4         6 ]
%     %|| .sa
%     %||   .stats
%     %||     { 'Ttest(10)'
%     %||       'Zscore()'  }
%     %|| .fa
%     %||   .node_indices
%     %||     [ 1         2         3 ]
%     %|| .a
%     %||   .fdim
%     %||     .labels
%     %||       { 'node_indices' }
%     %||     .values
%     %||       { [ 2 21 202 ] }
%
%
% Notes:
%   - this function is intended for datasets with surface data, i.e. with
%     one or more values associated with each surface node. It does not
%     support anatomical surface meshes that contain node coordinates and
%     faces. To read and write such anatomical meshes, consider the surfing
%     toolbox, github.com/nno/surfing
%   - data can be mapped back to a surface file format using
%     cosmo_map2surface
%
% Dependencies:
%   - for Brainvoyager files (.smp), it requires the NeuroElf
%     toolbox, available from: http://neuroelf.net
%   - for AFNI/SUMA NIML files (.niml.dset) it requires the AFNI
%     Matlab toolbox, available from: http://afni.nimh.nih.gov/afni/matlab/
%   - for GIFTI files (.gii) it requires the  MATLAB/Octave GIfTI Library,
%     available from https://github.com/gllmflndn/gifti
%
% See also: cosmo_map2surface
%
% #   For CoSMoMVPA's copyright information and license terms,   #
% #   see the COPYING file distributed with CoSMoMVPA.           #

    defaults=struct();
    defaults.targets=[];
    defaults.chunks=[];

    params = cosmo_structjoin(defaults, varargin);

    ds=get_dataset(fn);

     % set targets and chunks
    ds=set_vec_sa(ds,'targets',params.targets);
    ds=set_vec_sa(ds,'chunks',params.chunks);

    % check consistency
    cosmo_check_dataset(ds,'surface');

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% general helper functions
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function ds=get_dataset(fn)
    obj_to_clean=[];
    % try to load from .mat file
    obj_from_mat=try_dataset_from_mat(fn);
    if isempty(obj_from_mat)
        obj=fn;
    else
        obj=obj_from_mat;
        obj_to_clean=obj;
    end

    % try to load from file
    if ischar(obj)
        obj=try_obj_from_file(obj);
        obj_to_clean=obj;
    end

    [ds,fmt]=try_dataset_from_obj(obj);

    if ~isempty(obj_to_clean)
        apply_cleaner(obj_to_clean,fmt);
    end

function obj=try_dataset_from_mat(fn)
    obj=[];
    if ischar(fn) && isempty(cosmo_strsplit(fn,'.mat',-1))
        obj=importdata(fn);
    end

function [obj,fmt]=try_obj_from_file(fn)
    assert(ischar(fn));
    [exts_cell,fmts]=get_format_attribute('exts');

    endswith=@(s,e) isempty(cosmo_strsplit(s,e,-1));
    endswith_any=@(s,ce) any(cellfun(@(x)endswith(s,x),ce));

    match=cellfun(@(x)endswith_any(fn,x), exts_cell);

    i=find_single_or_error(match, ...
                            @()sprintf('unknown extension for file %s',...
                                                fn));
    fmt=fmts{i};
    reader=fmt.reader;
    obj=reader(fn);


function [ds,fmt]=try_dataset_from_obj(obj)
    if cosmo_check_dataset(obj,'surface',false)
        % it is already a valid dataset
        ds=obj;
        fmt='';
        return;
    end

    [matcher_cell,fmts]=get_format_attribute('matcher');

    match=cellfun(@(f) f(obj),matcher_cell);
    i=find_single_or_error(match, ...
                            @()sprintf('unknown object of type %s',...
                                                class(obj)));

    fmt=fmts{i};
    ds=build_dataset(obj,fmt);


function ds=build_dataset(obj,fmt)
    builder=fmt.builder;
    [data,node_indices,sa]=builder(obj);
    nfeatures=size(data,2);
    if nfeatures~=numel(node_indices)
        error(['The number of features (%d) does not match the number '...
                'of node indices (%d)'],nfeatures,numel(node_indices));
    end

    ds=struct();
    ds.samples=data;

    % set sample attributes
    ds.sa=sa;

    % set feature attributes
    ds.fa.node_indices=1:nfeatures;

    % set dataset attributes
    fdim=struct();
    fdim.labels={'node_indices'};
    fdim.values={node_indices(:)'};
    ds.a.fdim=fdim;

function apply_cleaner(obj,fmt)
    if ~isempty(fmt) && isfield(fmt,'cleaner')
        cleaner=fmt.cleaner;
        cleaner(obj);
    end

function i=find_single_or_error(m, error_text_func)
    i=find(m);
    switch numel(i)
        case 0
            error(error_text_func());

        case 1
            % ok

        otherwise
            assert(false,'this should not happen');
    end

function [elems,fmts]=get_format_attribute(fmt_key)
    img_formats=get_img_formats();
    keys=fieldnames(img_formats);
    n_fmts=numel(keys);

    elems=cell(n_fmts,1);
    fmts=cell(n_fmts,1);

    for k=1:n_fmts
        key=keys{k};

        fmt=img_formats.(key);
        elems{k}=fmt.(fmt_key);
        fmts{k}=fmt;
    end



function ds=read_surface_dataset(fn)
    [data,node_indices,sa]=read(fn);
    nfeatures=size(data,2);
    if nfeatures~=numel(node_indices)
        error(['The number of features (%d) does not match the number '...
                'of node indices (%d)'],nfeatures,numel(node_indices));
    end

    ds=struct();
    ds.samples=data;

    % set sample attributes
    ds.sa=sa;

    % set feature attributes
    ds.fa.node_indices=1:nfeatures;

    % set dataset attributes
    fdim=struct();
    fdim.labels={'node_indices'};
    fdim.values={node_indices(:)'};
    ds.a.fdim=fdim;


function ds=set_vec_sa(ds, label, values)
    if isempty(values)
        return;
    end
    if numel(values)==1
        nsamples=size(ds.samples,1);
        values=repmat(values,nsamples,1);
    end
    ds.sa.(label)=values(:);



function [data,node_indices,sa]=read(fn)
    img_formats=get_img_formats();

    endswith=@(s,e) isempty(cosmo_strsplit(s,e,-1));

    names=fieldnames(img_formats);
    n=numel(names);

    for k=1:n
        name=names{k};
        img_format=img_formats.(name);

        matcher=img_format.matcher;
        builder=img_format.builder;

        if matcher(fn)
            [data,node_indices,sa]=builder(fn);
            return
        elseif ischar(fn)
            exts=img_format.exts;
            reader=img_format.reader;
            for j=1:numel(exts)
                if endswith(fn,exts{j})
                    cosmo_check_external(img_format.externals);
                    s=reader(fn);

                    if ~matcher(s)
                        error(['Could read file ''%s'', but the data '...
                                'is not matching the expected contents '...
                                'for a dataset of type %s'],...
                                fn, name);
                    end

                    [data,node_indices,sa]=builder(s);
                    return
                end
            end
        end
    end

    if ischar(fn)
        error('Unsupported extension in filename %s', fn);
    else
        error('Unsupported input of type %s', class(fn));
    end


function img_formats=get_img_formats()

    % define which formats are supports
    % .exts indicates the extensions
    % .matcher says whether a struct is of the type
    % .reader should read a filaname and return a struct
    % .externals are fed to cosmo_check_externals
    img_formats=struct();

    img_formats.niml_dset.exts={'.niml.dset'};
    img_formats.niml_dset.matcher=@isa_niml_dset;
    img_formats.niml_dset.reader=@read_niml_dset;
    img_formats.niml_dset.builder=@build_niml_dset;
    img_formats.niml_dset.externals={'afni'};

    img_formats.bv_smp.exts={'.smp'};
    img_formats.bv_smp.matcher=@isa_bv_smp;
    img_formats.bv_smp.reader=@read_bv_smp;
    img_formats.bv_smp.builder=@build_bv_smp;
    img_formats.bv_smp.cleaner=@(x)x.ClearObject();
    img_formats.bv_smp.externals={'neuroelf'};

    img_formats.gii.exts={'.gii'};
    img_formats.gii.matcher=@isa_gii;
    img_formats.gii.reader=@read_gii;
    img_formats.gii.builder=@build_gii;
    img_formats.gii.externals={'gifti'};

    img_formats.pymvpa.exts={};
    img_formats.pymvpa.matcher=@isa_pymvpa;
    img_formats.pymvpa.reader=@read_pymvpa;
    img_formats.pymvpa.builder=@build_pymvpa;
    img_formats.pymvpa.externals={};


function b=isa_pymvpa(x)
    b=isstruct(x) && ...
            isfield(x,'samples') && ...
            isfield(x,'fa') && ...
            any(cosmo_isfield(x.fa, pymvpa_get_node_indices_fields()));

function b=read_pymvpa(fn)
    assert(false,'this function should not have been called');

function [data,node_indices,sa]=build_pymvpa(s)
    data=s.samples;
    index_fields=pymvpa_get_node_indices_fields();
    common_fields=intersect(index_fields,fieldnames(s.fa));

    assert(~isempty(common_fields));
    first_field=common_fields{1};
    node_indice_base0=s.fa.(first_field);
    node_indices=double(node_indice_base0)+1;

    sa=struct();
    if isfield(s,'sa')
        sa=convert_struct_with_3d_string_array_to_cellstr(s.sa,1);
    end


function c=convert_3d_string_array_to_cellstr(arr,dim)
    sz=size(arr);
    assert(numel(sz)==3);
    if dim==1
        new_sz_idxs=[1 3];
    else
        new_sz_idxs=[2 3];
    end

    arr_2d=reshape(arr,sz(new_sz_idxs));
    c=cellstr(arr_2d);

    if dim==2
        c=c';
    end

function s=convert_struct_with_3d_string_array_to_cellstr(s,dim)
    % deal with scipy's character arrays that represent 2d cell strings
    fns=fieldnames(s);
    for k=1:numel(fns)
        fn=fns{k};
        value=s.(fn);
        if ischar(value) && numel(size(value))==3
            s.(fn)=convert_3d_string_array_to_cellstr(value,dim);
        end
    end


function fields=pymvpa_get_node_indices_fields()
    fields={'node_indices','center_ids'};

function b=isa_gii(x)
    b=isa(x,'gifti') && isfield(x,'cdata');

function s=read_gii(fn)
    s=gifti(fn);

function [data,node_indices,sa]=build_gii(s)
    data=s.cdata';

    if ~isa(data,'double')
        data=double(data);
    end

    if isfield(s,'indices')
        node_indices=double(s.indices);
    else
        nv=size(data,2);
        node_indices=1:nv;
    end

    sa=struct();


function b=isa_niml_dset(x)
    b=isstruct(x) && isfield(x,'data');

function s=read_niml_dset(fn)
    s=afni_niml_readsimple(fn);

function [data,node_indices,sa]=build_niml_dset(s)
    data=s.data';

    if ~isa(data,'double')
        data=double(data);
    end

    if isfield(s,'node_indices')
        node_indices=s.node_indices+1; % base0 -> base1
    else
        nv=size(data,2);
        node_indices=1:nv;
    end

    sa=struct();
    if isfield(s,'stats')
        sa.stats=cosmo_statcode(s.stats);
    end
    if isfield(s,'labels')
        sa.labels=s.labels(:);
    end

function b=isa_bv_smp(x)
    b=isa(x,'xff') && isfield(x,'NrOfVertices') && isfield(x,'Map');

function s=read_bv_smp(fn)
    s=xff(fn);
    neuroelf_bless_wrapper(s);

function [data,node_indices,sa]=build_bv_smp(s)
    nsamples=s.NrOfMaps;
    nfeatures=s.NrOfVertices;

    data=zeros(nsamples,nfeatures);
    node_indices=1:nfeatures;
    labels=cell(nsamples,1);

    for k=1:nsamples
        map=s.Map(k);
        data(k,:)=map.SMPData(:);
        labels{k}=map.Name;
    end

    sa=struct();
    sa.labels=labels;
    sa.stats=cosmo_statcode(s);


function result=neuroelf_bless_wrapper(arg)
    % deals with recent neuroelf (>v1.1), where bless is deprecated
    s=warning('off','neuroelf:xff:deprecated');
    resetter=onCleanup(@()warning(s));

    result=bless(arg);