function matches = link(obj, particles, t0)
    % matches = link(obj, prt_ary, t0)
	%
	% link the projected locations of particle centers to observed
	% locations of particle centers at timestep t0.
    %
    % Input:
    %   particles
    %       a structure array as returned from obj.match()
    % 
    % Output:
    %   matches n_match x 2 array representing track->particle correspondences
    %       matches(:,1) is index of track
    %       matches(:,2) is index of particle 
    
    
    %% allocate
    n_tracks            = length(obj.tracks);
    n_cameras           = obj.n_cameras;
    camera              = obj.setup.camera;
    n_active            = obj.count();
    linkopts            = obj.opts.linking;
    
    %% get latest predicted positions from cell array
    re_pred             = obj.get_active_property(1:n_active, t0, 're_pred', true);
	re_pred				= squeeze(re_pred);		% re_pred is 3 x n_active
    re_part             = [particles.re_pos];	% re_pred is 3 x n_active
    
    %% find nearest neighbour match
    % matches(:,1) is index of track
    % matches(:,2) is index of particle 
	if strcmp(linkopts.algorithm, 'link_nearest_neighbour')
		matches         = link_nearest_neighbour(re_pred, re_part, linkopts.R_search_obj);
	elseif strcmp(linkopts.algorithm, 'link_neighbour_topological')
		matches			= link_neighbour_topological(re_pred, re_part, ...
                            linkopts.R_search_obj, linkopts.K_feature);
    else
        error('unrecognised linking algorithm %s', linkopts.algorithm);
    end
    
    %% update tracks
	new_active			= cell(1, n_active);
    for m = matches.'
        % index of track and particle
		trk				= obj.active(m(1));
		% change orientation of particle to be consistent across tracks
		re_length_prev	= trk.re_length(:,t0-1);
		re_length_now	= particles(m(2)).re_length;
		flip_sign		= sign(dot(re_length_prev,re_length_now));
		if isnan(flip_sign)
			flip_sign	= 1;
		end
		particles(m(2)).re_length = flip_sign * particles(m(2)).re_length;
		% assign particle to track
        trk				= ShadowTrack.assign_particle_to_track(trk, particles(m(2)), t0);
        % increment remaining lives
        trk.life		= min(trk.life+1, obj.opts.n_lives_max);
		%obj.active(m(1))= trk;
		new_active{m(1)}= trk;
    end
    
    %% update properties where no match was possible
    i_accept            = matches(:,1).';
    i_reject            = setdiff(1 : n_active, i_accept);
    for n = i_reject
        % use prediction as object/image space position
        trk				= obj.active(n);
        trk.re_pos(:,:,t0) = trk.re_pred(:,:,t0);
        trk.im_pos(:,:,t0) = trk.im_pred(:,:,t0);
		% decrement life and check if still active
        trk.life		= trk.life - 1;
		new_active{n}   = trk;
	end

	%% update
	obj.active			= cell2mat(new_active);
end