% CLEANEDGELIST - remove short edges from a set of edgelists
%
% Function to clean up a set of edge lists generated by EDGELINK so that
% isolated edges and spurs that are shorter that a minimum length are removed.
% This code can also be use with a set of line segments generated by LINESEG.
%
% Usage: nedgelist = cleanedgelist(edgelist, minlength)
%
% Arguments:
%                edgelist - a cell array of edge lists in row,column coords in
%                           the form
%                           { [r1 c1   [r1 c1   etc }
%                              r2 c2    ...
%                                       ...
%                              rN cN]   ....]   
%                minlength - minimum edge length of interest
%
% Returns:
%                nedgelist - the new, cleaned up set of edgelists
%
% See also:  EDGELINK, DRAWEDGELIST, LINESEG

% Copyright (c) 2006-2007 Peter Kovesi
% www.peterkovesi.com
% 
% Permission is hereby granted, free of charge, to any person obtaining a copy
% of this software and associated documentation files (the "Software"), to deal
% in the Software without restriction, subject to the following conditions:
% 
% The above copyright notice and this permission notice shall be included in 
% all copies or substantial portions of the Software.
%
% The Software is provided "as is", without warranty of any kind.
%
% December 2006  Original version
% February 2007  A major rework to fix several problems, hope they really are
%                fixed!


function nedgelist = cleanedgelist(edgelist, minlength)
    
    Nedges = length(edgelist);
    Nnodes = 2*Nedges;

    % Each edgelist has two end nodes - the starting point and the ending point.
    % We build up an adjacency/connection matrix for each node so that we can
    % determine which, if any, edgelists are connected to a node. We also
    % maintain an adjacency matrix for the edges themselves.
    % 
    % It is tricky maintaining all this information but it does allow the
    % code to run much faster.

    % First extract the end nodes from each edgelist.  The nodes are numbered
    % so that the start node has number 2*edgenumber-1 and the end node has
    % number 2*edgenumber
    node = zeros(Nnodes, 2);
    for n = 1:Nedges
        node(2*n-1,:) = edgelist{n}(1,:);
        node(2*n  ,:) = edgelist{n}(end,:);     
    end
    
    % Now build the adjacency/connection matrices. 
    A = zeros(Nnodes);   % Adjacency matrix for nodes
    B = zeros(Nedges);   % Adjacency matrix for edges
    
    for n = 1:Nnodes-1
        for m = n+1:Nnodes
            % If nodes m & n are connected
            A(n,m) =  node(n,1)==node(m,1) && node(n,2)==node(m,2);
            A(m,n) = A(n,m);
            
            if A(n,m)
                edgen = fix((n+1)/2);
                edgem = fix((m+1)/2);           
                B(edgen, edgem) = 1;
                B(edgem, edgen) = 1;            
            end
        end
    end

    % If we sum the columns of the adjacency matrix we get the number of
    % other edgelists that are connected to an edgelist
    Nconnections = sum(A);   % Connection count array for nodes
    Econnections = sum(B);   % Connection count array for edges
    

    % Check every edge to see if any of its ends are connected to just one edge.
    % This should not happen, but occasionally does due to a problem in
    % EDGELINK.  Here we simply merge it with the edge it is connected to.
    % Ultimately I want to be able to remove this block of code.
    % I think there are also some cases that are (still) not properly handled
    % by CLEANEDGELIST and there may be a case for repeating this block of
    % code at the end for another final cleanup pass
    for n = 1:Nedges
        if ~B(n,n) && ~isempty(edgelist{n}) % if edge is not connected to itself
            [spurdegree, spurnode, startnode, sconns, endnode, econns] = connectioninfo(n);
            if sconns == 1
                node2merge = find(A(startnode,:));
                mergenodes(node2merge,startnode);
            end
            
            if ~isempty(edgelist{n})   % If we have not removed this edge in
                                       % the code above check the other end.
                if econns == 1
                    node2merge = find(A(endnode,:));
                    mergenodes(node2merge,endnode);
                end         
            end
        end
    end
    
    
    % Now check every edgelist, if the edgelength is below the minimum length
    % check if we should remove it.

    if minlength > 0
        
    for n = 1:Nedges
        
        [spurdegree, spurnode] = connectioninfo(n);
        
        if ~isempty(edgelist{n}) && edgelistlength(edgelist{n}) < minlength  

            % Remove unconnected lists, or lists that are only connected to
            % themselves. 
            if ~Econnections(n) || (Econnections(n)==1 && B(n,n) == 1)
                removeedge(n);
            
            % Process edges that are spurs coming from a 3-way junction.
            elseif spurdegree == 2
                %fprintf('%d is a spur\n',n)  %%debug

                linkingedges = find(B(n,:));
                
                if length(linkingedges) == 1 % We have a loop with a spur
                                             % coming from the join in the
                                             % loop
                   % Just remove the spur, leaving the loop intact.
                   removeedge(n);  
                   
                else   % Check the other edges coming from this point. If any
                       % are also spurs make sure we remove the shortest one
                   spurs = n;
                   len = edgelistlength(edgelist{n});
                   for i = 1:length(linkingedges)
                       spurdegree = connectioninfo(linkingedges(i));
                       if spurdegree
                           spurs = [spurs linkingedges(i)];  
                           len = [len edgelistlength(edgelist{linkingedges(i)})];  
                       end
                   end

                   linkingedges = [linkingedges n];
                   
                   [mn,i] = min(len);
                   edge2delete = spurs(i);
                   [spurdegree, spurnode] = connectioninfo(edge2delete);

                   nodes2merge = find(A(spurnode,:));
                   
                   if length(nodes2merge) ~= 2
                       error('attempt to merge other than 2 nodes');
                   end
                   
                   removeedge(edge2delete);             
                   mergenodes(nodes2merge(1),nodes2merge(2))               
                   
                end 

            % Look for spurs coming from 4-way junctions that are below the minimum length
            elseif spurdegree == 3
                removeedge(n);    % Just remove it, no subsequent merging needed.
            end
        end
    end
    
    % Final cleanup of any new isolated edges that might have been created by
    % removing spurs.  An edge is isolated if it has no connections to other
    % edges, or is only connected to itself (in a loop).
    
    for n = 1:Nedges
        if ~isempty(edgelist{n}) && edgelistlength(edgelist{n}) < minlength  
            if ~Econnections(n) || (Econnections(n)==1 && B(n,n) == 1)
                removeedge(n);          
            end
        end
    end
    
    end % if minlength > 0
    
    % Run through the edgelist and extract out the non-empty lists
    m = 0;
    for n = 1:Nedges
       if ~isempty(edgelist{n})
           m = m+1;
           nedgelist{m} = edgelist{n};
       end
    end
        
    
%---------------------------------------------------------------------- 
% Internal function to merge 2 edgelists together at the specified nodes and
% perform the necessary updates to the edge adjacency and node adjacency
% matrices and the connection count arrays

function mergenodes(n1,n2)
    
    edge1 = fix((n1+1)/2);   % Indices of the edges associated with the nodes
    edge2 = fix((n2+1)/2);    

    % Get indices of nodes at each end of the two edges
    s1 = 2*edge1-1; e1 = 2*edge1;
    s2 = 2*edge2-1; e2 = 2*edge2;    
    
    if edge1==edge2
        % We should not get here, but somehow we occasionally do
        % fprintf('Nodes %d %d\n',n1,n2)    %% debug
        % warning('Attempt to merge an edge with itself')
        return
    end
    
    if ~A(n1,n2)
        error('Attempt to merge nodes that are not connected');
    end
    
    if mod(n1,2)  % node n1 is the start of edge1
        flipedge1 = 1;  % edge1 will need to be reversed in order to join edge2
    else 
        flipedge1 = 0; 
    end
    
    if mod(n2,2)  % node n2 is the start of edge2       
        flipedge2 = 0;
    else
        flipedge2 = 1;
    end
    
    % Join edgelists together - with appropriate reordering depending on which
    % end is connected to which.  The result is stored in edge1
    
    if  ~flipedge1 && ~flipedge2 
        edgelist{edge1} = [edgelist{edge1}; edgelist{edge2}];

        A(e1,:) = A(e2,:);      A(:,e1) = A(:,e2);
        Nconnections(e1) = Nconnections(e2);
        
    elseif  ~flipedge1 && flipedge2
        edgelist{edge1} = [edgelist{edge1}; flipud(edgelist{edge2})];   
        
        A(e1,:) = A(s2,:);      A(:,e1) = A(:,s2);
        Nconnections(e1) = Nconnections(s2);

    elseif  flipedge1 && ~flipedge2
        edgelist{edge1} = [flipud(edgelist{edge1}); edgelist{edge2}]; 
        
        A(s1,:) = A(e1,:);      A(:,s1) = A(:,e1);
        A(e1,:) = A(e2,:);      A(:,e1) = A(:,e2);      
        Nconnections(s1) = Nconnections(e1);
        Nconnections(e1) = Nconnections(e2);    

    elseif  flipedge1 && flipedge2
        edgelist{edge1} = [flipud(edgelist{edge1}); flipud(edgelist{edge2})];

        A(s1,:) = A(e1,:);      A(:,s1) = A(:,e1);          
        A(e1,:) = A(s2,:);      A(:,e1) = A(:,s2);
        Nconnections(s1) = Nconnections(e1);
        Nconnections(e1) = Nconnections(s2);
        
    else
        fprintf('merging edges %d and %d\n',edge1, edge2); %%debug      
        error('We should not have got here - edgelists cannot be merged');
    end
    
    % Now correct the edge adjacency matrix to reflect the new arrangement
    % The edges that the new edge1 is connected to is all the edges that
    % edge1 and edge2 were connected to
    B(edge1,:) = B(edge1,:) | B(edge2,:);
    B(:,edge1) = B(:,edge1) | B(:,edge2);    
    B(edge1, edge1) = 0;

    % Recompute connection counts because we have shuffled the adjacency matrices
    Econnections = sum(B);
    Nconnections = sum(A);
  
    removeedge(edge2);  % Finally discard edge2
    
end  % end of mergenodes

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

% Function to provide information about the connections at each end of an
% edgelist 
%
%   [spurdegree, spurnode, startnode, sconns, endnode, econns] = connectioninfo(n)
%
%  spurdegree - If this is non-zero it indicates this edgelist is a spur, the
%               value is the number of edges this spur is connected to.
%  spurnode   - If this is a spur spurnode is the index of the node that is
%               connected to other edges, 0 otherwise.
%  startnode  - index of starting node of edgelist.
%  endnode    - index of end node of edgelist.
%  sconns     - number of connections to start node.
%  econns     - number of connections to end node.
    
function [spurdegree, spurnode, startnode, sconns, endnode, econns] = connectioninfo(n)

    if isempty(edgelist{n})
        spurdegree = 0; spurnode = 0;
        startnode = 0; sconns = 0; endnode = 0; econns = 0;
        return
    end
    
    startnode = 2*n-1;
    endnode   = 2*n;
    sconns = Nconnections(startnode);  % No of connections to start node
    econns = Nconnections(endnode);    % No of connections to end node    
    
    if sconns == 0 && econns >= 1
        spurdegree = econns;
        spurnode = endnode;
    elseif sconns >= 1 && econns == 0
        spurdegree = sconns;
        spurnode = startnode;   
    else
        spurdegree = 0;
        spurnode = 0;
    end
    
end

%--------------------------------------------------------------------
% Function to remove an edgelist and perform the necessary updates to the edge
% adjacency and node adjacency matrices and the connection count arrays

function removeedge(n)
    
    edgelist{n} = [];
    Econnections = Econnections - B(n,:); 
    Econnections(n) = 0; 
    B(n,:) = 0;
    B(:,n) = 0;
    
    nodes2delete = [2*n-1, 2*n];
    
    Nconnections = Nconnections - A(nodes2delete(1),:);    
    Nconnections = Nconnections - A(nodes2delete(2),:);        
    
    A(nodes2delete, :) = 0;
    A(:, nodes2delete) = 0;                 
    
end

%--------------------------------------------------------------------
% Function to compute the path length of an edgelist

function l = edgelistlength(edgelist)
    l = sum(sqrt(sum((edgelist(1:end-1,:)-edgelist(2:end,:)).^2, 2)));
end

%--------------------------------------------------------------------
end % End of cleanedgelists
    
