

function Local_Chromatic_Integration_Stimulus(varargin)
%
%===================================================================================
% Parameter             Default                     Usage
%===================================================================================
%
% selecttextfile        false           Browse prompt to select the parameters text file
% stimduration          600             Stimulus presentation duration in refresh rate unit (600 = 10 sec).
% maxcontrast           0.2             Maximum Weber contrast value (ranges from -1 to 1)
% mincontrast           -0.2            Minimum Weber contrast value (ranges from -1 to 1)
% contrastdiff          0.02            Contrast steps from mincontrast to maxcontrast e.g. -20 : 2 : 20
% stixelwidth           20              Width of each local stimulus in pixels
% stixelheight          20              Height of each local stimulus in pixels
% gapwidth              2               Number of locations to be avoided around each locally selected stimulus area
% gapheight             2               Same as gapwidth but for the y-axis of each selected location
% seedlocation          -1000           Seed for random number generator for locations’ list (should be negative)
% seedonoroffpixels     -10000          Seed for random number generator of binary contrasts (not used here)
% seedcolorcontrast     -2000           Seed for random number generator for color contrast list (should be negative)
% redmeanintensity      0               Mean intensity for red gun of the screen
% greenmeanintensity	0.5             Mean intensity for green gun of the screen
% bluemeanintensity     0.5             Mean intensity for blue gun of the screen
% screensize            864 x 480       Screen resolution, default value is the resolution of the lightcrafter projector
% refreshrate           60              Screen refresh rate in Hz
% drawcircle            true            Option to display stimulus as circle or squares
% drawannulus           false           Option to display the stimulus as annulus (not available for this stimulus)
% fullscreen            false           Option to display the stimulus in full-screen mode
% help                  false           Option to check the list of available parameters
% lmargin               0               Margins from left side of the screen (not available for this stimulus)
% rmagin                0               Margins from right side of the screen (not available for this stimulus)
% tmargin               0               Margins from top of the screen (not available for this stimulus)
% bmargin               0               Margins from bottom of the screen (not available for this stimulus)
% coneisolating         false           Option to activate opsin-isolation (excluded for simplicity)
%
%===================================================================================


para            =   read_stimulus_parameters(varargin{:});
if para.help, return; end


offcontrasts    =   fliplr(0:-para.contrastdiff:para.mincontrast);
oncontrasts     =   0:para.contrastdiff:para.maxcontrast;

greencontrasts  =   [offcontrasts,oncontrasts];
bluecontrasts   =   [oncontrasts,offcontrasts];

% convert to weber contrast (mean+(contrast*mean))
greencontrasts  =   para.greenmeanintensity + (greencontrasts * para.greenmeanintensity);
bluecontrasts   =   para.bluemeanintensity + (bluecontrasts * para.bluemeanintensity);

% first get the corrdinates of all the locations
[f, v]          =   draw_locations(para);

% The darwing of the screen goes here
monitorsize =   get(0,'ScreenSize');
scpos       =   [monitorsize(3)/2-(para.screensize(1)/2), ...
    monitorsize(4)/2-(para.screensize(2)/2), para.screensize];
% make an screen like figure
fh          =   figure('Menu','none','ToolBar','none','Position',scpos,'Color',0.5*[1 1 1]);
fh.Name     =   'Example of Local Chromatic Integration Stimulus, from Khani and Gollisch (2021)';
fh.Colormap =   gray;
ah          =   axes('Units','Normalize','Position',[0 0 1 1]);
                axis(ah,[0, para.screensize(1),0, para.screensize(2)]);
                axis(ah,'off');

% inital variables
framecounter=   0;
seedlocation    =   para.seedlocation;      % to randomize locations
seedcolor       =   para.seedcolorcontrast; % to randomize contrast order
stimdur         =   ceil((para.stimduration ./ para.refreshrate)/ 0.5); % start by an estimate of one cycle

while ishandle(fh)
    
    if framecounter == 2,   tic;    end     % start the timer on the second frame
    frameMod        =   mod(framecounter,stimdur);
    
    if frameMod     ==  0
        [contrastlist, seedlocation, seedcolor] = select_sparse_locations(seedlocation, seedcolor, greencontrasts, bluecontrasts, para);
    end
    if framecounter ==  0
        p   =     patch('Faces',f,'Vertices',v,'FaceVertexcData',contrastlist,'Facecolor','Flat','edgecolor','none');
    else
        p.FaceVertexCData            =          contrastlist;
    end
     
    % this is to draw the frames relatively accurately.
    drawnow;
    java.lang.Thread.sleep(1/para.refreshrate*1e3);
    %pause(1/para.refreshrate);
    if framecounter == 2
       t            =  toc; % take the time of one trial and adjust the time based on that.
       stimdur      =  ceil((para.stimduration ./ para.refreshrate)/ t); 
    end
    
    framecounter    =   framecounter+1;
end

end

%--------------------------------------------------------------------------------------------------%
%----------                               sub-functions                                  ----------%
%--------------------------------------------------------------------------------------------------%

function varargout = fisher_Yates_shuffle_order(seed,inputVec,varargin)
%
%%% fisher_Yates_shuffle_order %%%
%
%
% This function generate psudorandom permution similar to randperm in MATLAB
% but works with psudorandom number generator ran1. It also gives back the
% most recent seed value to continue the permuation in case of repeated trials.
% note that the direction of permutation is along x-axix or for columns of
% MATALB not for the rows.
%
%
% ===============================Inputs====================================
%
%   seed : seed value for random number generation.
%   inputVec : input vector used for permutation.
%
%================================Output====================================
%
%   testOrder : vector of permuted indices for the inputVec.
%   newSeed : the recent seed that used in the ran1 function.
%   outputVec : the permuted input vector along x-axis
%
% Note that the permution algorithem is based on Fisher-Yates shuffle
% algorithem identical to what is used in the stimulus program.
% for more info check :
% https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
%
% written by Mohammad, 01.02.2016

newSeed         =   seed;
testOrder       =   zeros(1,size(inputVec,2));
testOrder(1)    =   1;

for i   =   2:length(inputVec)-1
    
    [randVal,newSeed] = ran1(newSeed);
    
    j               =   ceil(i*randVal);    % based on Fischer-Yates algorithem
    testOrder(i)    =   testOrder(j);
    testOrder(j)    =   i;
    
end

testOrder           =   [testOrder(end),testOrder(1:end-1)]+1;   % to match MATLAB indexing
varargout{1}        =   testOrder;
varargout{2}        =   newSeed;
for j   =   1:size(inputVec,1)
    varargout{3}(j,:)   =   inputVec(j,testOrder);
end

end

%--------------------------------------------------------------------------------------------------%


function [f, v] = draw_locations(para)

Nx      =   ceil(para.screensize(1)/para.stixelwidth);
Ny      =   ceil(para.screensize(2)/para.stixelheight);

if para.drawcircle,     numpoints = 100;        else,    numpoints = 5;     end

locs    =   nan(Nx*Ny,numpoints,2);
iter    =   1;
for ii  =   1 : para.stixelheight : para.screensize(2)
    for jj  =   1 : para.stixelwidth : para.screensize(1)
        if para.drawcircle
            locs(iter,1:numpoints,1)    =   jj + ((para.stixelwidth/2).*cos(linspace(0, 2*pi,numpoints))) - para.stixelwidth/2;
            locs(iter,1:numpoints,2)    =   ii + ((para.stixelheight/2).*sin(linspace(0, 2*pi,numpoints))) - para.stixelheight/2;
        else % little hack to draw squares
            locs(iter,1:numpoints,1)    =   [jj, jj+para.stixelwidth, jj+para.stixelwidth, jj, jj]-para.stixelwidth;
            locs(iter,1:numpoints,2)    =   [ii, ii, ii+para.stixelheight, ii+para.stixelheight,ii]-para.stixelheight;
        end
        iter                            =   iter+1;
    end
end
x       =   squeeze(locs(:,:,1))';
y       =   squeeze(locs(:,:,2))';

f       =   reshape(1:Nx*Ny*numpoints,numpoints,Nx*Ny)';    % faces for patch fuction
v       =   [x(:),y(:)];                                    % vertices for patch function
%c      =   repmat(0.5*[1 1 1],Nx*Ny,1);                    % colors for patch function
end

%--------------------------------------------------------------------------------------------------%

function [stimMat, seedlocationout, seedcolorout] = select_sparse_locations(seedlocation, seedcolor, greencontrasts, bluecontrasts, para)

numcontrasts    =   size(greencontrasts,2);
Nx              =   ceil(para.screensize(1)/para.stixelwidth);
Ny              =   ceil(para.screensize(2)/para.stixelheight);

stimMat         =   zeros(Ny*Nx,3)+0.5;         % set all locations to gray
locationlist    =   (0:Nx*Ny-1)';
neighbourlist   =   get_neighbourlist(Nx, Ny, para.gapwidth, para.gapheight);

% make an estimate of all possible locations
possibleXYlocs  =   zeros(floor(numel(locationlist)/min(cellfun(@numel,neighbourlist))),1);

%outside loop is faster
[randvals, seedlocationout]   =   ran1(seedlocation, numel(possibleXYlocs));
randIdx         =   1;

availablelist   =   locationlist;
currXYlocs      =   possibleXYlocs;
idx             =   1;

while numel(availablelist)>0
    
    randval     =   randvals(randIdx);
    randIdx     =   randIdx + 1;
    stixelInd   =   floor(randval * size(availablelist,1)) + 1;
    toadd       =   availablelist(stixelInd);
    toremove    =   neighbourlist{Ny * mod(toadd,Nx) + floor(toadd/Nx)+1};
    %using the mex function for speed
    % availablelist(builtin('_ismemberoneoutput',availablelist,sort(toremove)))=[];
    % this is for older matlab, for new matlab use the line below
    availablelist(ismembc(availablelist,sort(toremove)))    =   [];
    
    currXYlocs(idx)     =   toadd+1;
    idx         =   idx + 1;
end

contlist        =   cell(1,ceil((idx-1)/length(1:numcontrasts)));
for jj  =    1:ceil((idx-1) / length(1 : numcontrasts))
    [contlist{jj}, seedcolor] = fisher_Yates_shuffle_order(seedcolor, 1:numcontrasts);
end
seedcolorout    =   seedcolor;
contindices     =   cell2mat(contlist)';
%contperframe = contindices(1:idx-1);
stimMat(currXYlocs(1:idx-1),1)      =   0;
stimMat(currXYlocs(1:idx-1),2)      =   greencontrasts (contindices(1:idx-1));
stimMat(currXYlocs(1:idx-1),3)      =   bluecontrasts (contindices(1:idx-1));

end

%--------------------------------------------------------------------------------------------------%

function neighbourlist = get_neighbourlist(nx, ny, gapx, gapy)

neighbourlist       =       cell(nx*ny,1);
idx                 =       1;
for x   =   0: nx-1
    for y   =   0: ny-1
        localneigbour       =       [];
        for currX           = x-gapx : x+gapx
            for currY       = y-gapy : y+gapy
                if (currX  >= 0 && currY >= 0 )
                    localneigbour   =   [localneigbour, nx * currY + currX];    %#ok
                end
            end
        end
        neighbourlist{idx}      =    localneigbour;
        idx                     =    idx+1;
    end
end

end

%--------------------------------------------------------------------------------------------------%

function paraout = read_stimulus_parameters(varargin)

% first parse the user inputs
p = inputParser();      % check the user options.
p.addParameter('selecttextfile', false,  @(x) islogical(x) || (isnumeric(x) && ismember(x,[0,1])));
p.addParameter('stimduration', 30, @isnumeric);
p.addParameter('contrastdiff', 0.02, @isnumeric);
p.addParameter('mincontrast',-0.2, @isnumeric);
p.addParameter('maxcontrast', 0.2, @isnumeric);

p.addParameter('stixelwidth', 20, @isnumeric);
p.addParameter('stixelheight', 20, @isnumeric);
p.addParameter('gapwidth', 2, @isnumeric);
p.addParameter('gapheight', 2, @isnumeric);

p.addParameter('seedlocation', -1000, @isnumeric);
p.addParameter('seedonoroffpixels', -10000, @isnumeric);
p.addParameter('seedcolorcontrast', -2000, @isnumeric);

p.addParameter('redmeanintensity', 0, @isnumeric);
p.addParameter('greenmeanintensity', 0.5, @isnumeric);
p.addParameter('bluemeanintensity', 0.5, @isnumeric);
p.addParameter('lmargin',0, @isnumeric);
p.addParameter('rmargin', 0, @isnumeric);
p.addParameter('bmargin', 0, @isnumeric);
p.addParameter('tmargin', 0, @isnumeric);
p.addParameter('coneisolating', true,  @(x) islogical(x) || (isnumeric(x) && ismember(x,[0,1])));
p.addParameter('drawcircle', true, @(x) islogical(x) || (isnumeric(x) && ismember(x,[0,1])));
p.addParameter('drawannulus', false, @(x) islogical(x) || (isnumeric(x) && ismember(x,[0,1])));
p.addParameter('screensize', [864 480], @isnumeric);
p.addParameter('refreshrate', 60, @isnumeric);
p.addParameter('fullscreen', false,  @(x) islogical(x) || (isnumeric(x) && ismember(x,[0,1])));
p.addParameter('help', false,  @(x) islogical(x) || (isnumeric(x) && ismember(x,[0,1])));

p.parse(varargin{:});
% defualt parameters
defpara     =    p.Results;

if defpara.help
    help_info_for_parameters(defpara);
    paraout     =   defpara;
    return;
end

if defpara.selecttextfile
    % now read the text files
    [stimfile, stimpath] = uigetfile('*.txt','Select a chromatic integration stimulus parameter file','chromatic_integration.txt');
    
    fid     =   fopen([stimpath,filesep,stimfile]);
    tline   =   fgetl(fid);
    while ischar(tline)
        tline   =   fgetl(fid);
        if tline == -1, break; end
        fn      =   extractBefore(tline,' = ');
        if isempty(fn), continue; end
        val     =   extractAfter(tline,' = ');
        % to convert to double
        if ~isnan(str2double(val))
            val =   str2double(val);
        end
        % to convert to logical
        if strcmp(val,'true'),        val = true;       end
        if strcmp(val,'false'),       val = false;       end
        % store the values in a structure
        stimpara.(fn)   =   val;
    end
    fclose(fid);
    
    % compare the text file to the defualt values and fill the missing parameters
    fn      =       fieldnames(defpara);
    for ii  =   1:numel(fn)
        if isfield(stimpara,fn{ii})
            paraout.(fn{ii})    =       stimpara.(fn{ii});
        else
            paraout.(fn{ii})    =       defpara.(fn{ii});
        end
    end
else
    paraout     =       defpara;
end

if paraout.fullscreen
    monitorsize         =   get(0,'ScreenSize');
    paraout.screensize  =   monitorsize(3:4) ;
end

end

%--------------------------------------------------------------------------------------------------%

function help_info_for_parameters(defpara)

fn          =   fieldnames(defpara);
fprintf(['\n\n',repmat('==',1,50),'\r\n']);
fprintf([repmat(' ',1,35),'List of Stimulus Parameters\r\n']);
fprintf([repmat('==',1,50),'\r\n']);
maxtxtlen   =   max(cellfun(@numel,fn))+10;
for ii      =  1:numel(fn)
    g       =  repmat(' ',1,maxtxtlen - numel(fn{ii}));
   fprintf(['\t-- \t%s',g,':',repmat(' ',1,10),'%s\n'],fn{ii},num2str(defpara.(fn{ii}))); 
end
fprintf(['\n',repmat('==',1,50),'\r\n']);

end 
