package statalign.postprocess.plugins.contree;
import java.util.ArrayList;
/**
* Nodes in the consensus network
* Also includes a substantial amount of code that draws the edges from the node.
*
* @author wood
*
*/
public class CNetworkNode {
// List of edges that join the node to other nodes
public ArrayList<CNetworkEdge> joins;
// if it is a taxon... need a name.
public String Taxaname = null;
// if printing the output, stores a number for the nexus file format
public int outputNumber = 0;
// node that this node was copied to when adding a certain split
public CNetworkNode CopiedToNode;
// paths this node is involved in during adding a split. Used to find other paths to link up to.
public ArrayList<CNetworkPath> paths;
// flag for if considered in a function to find the place to add a compatible split
public boolean consideration;
// flag to show if drawn or not (i.e. assigned a position)
public boolean drawn;
// direction of centre of angle available to add edges in
public double angleDirection;
// width of angle available to add edges in
public double angleWidth;
// position of node in graph
public double xPos,yPos;
/**
* Initialise the node
*/
public CNetworkNode(){
joins = new ArrayList<CNetworkEdge>();
paths = new ArrayList<CNetworkPath>();
consideration = false;
CopiedToNode = null;
outputNumber = 0;
drawn = false;
angleWidth = 0.0;
// for testing...
xPos = -1.0;
yPos = -1.0;
}
/**
* Add a path to the node path list..
*
* @param pathToAdd Path to add to this node's list
* @param nodesToClearUp List of nodes to remove paths from at end of adding split
*/
public void AddToPaths(CNetworkPath pathToAdd,ArrayList<CNetworkNode> nodesToClearUp){
if(paths.isEmpty()==true){
nodesToClearUp.add(this);
}
paths.add(pathToAdd);
}
/**
* Recursive function used to find node at which we can add a split compatible with the network
* Goes up from first node and finds where edges joining are no longer subset of specified side of split we want to add
*
* @param addSplit Split that we are adding
* @param consideredNodes Nodes that we have so far considered
* @param zeroSide True if looking on zero side of split, otherwise false. Try to choose side with fewer taxa to improve efficiency.
* @param noOfTaxa Number of taxa
*/
public CNetworkNode findFirstNonSubset(CNetworkSplit addSplit, ArrayList<CNetworkNode> consideredNodes,boolean zeroSide, int noOfTaxa){
// new method to check if is subset or not...
// note down that we are now using this node...
consideredNodes.add(this);
consideration = true;
// Look at all joining edges
for(CNetworkEdge currentEdge :joins){
// keep looking if still a subset...
if(addSplit.isSubset(currentEdge.split,zeroSide,noOfTaxa)==true){
//split is not final one, then continue and find the next targets....
// Temp array to store the joining nodes
CNetworkNode[] joiningEdges = new CNetworkNode[]{currentEdge.networkNodeA,currentEdge.networkNodeB};
for(CNetworkNode currentNode: joiningEdges){
// Only go to a joining node if we haven't been there before
if(currentNode.consideration==false){
//Keep going...
CNetworkNode testNode = currentNode.findFirstNonSubset(addSplit,consideredNodes,zeroSide, noOfTaxa);
if(testNode!=null){
return testNode;
}
}
}
}
else{
for(CNetworkNode consideredNode : consideredNodes){
consideredNode.consideration = false;
}
consideredNodes.clear();
return this;
}
}
//return a null if we have included all the nodes joining this (i.e. it is a taxon and maybe other cases...)
return null;
}
/**
* Function to draw the network i.e. find x and y co-ordinates for nodes.
*
* @param noOfTaxa Number of taxa
*/
public ArrayList<CNetworkNode> DrawEdges(int noOfTaxa){
// Array list to store the nodes that we return...
ArrayList<CNetworkNode> nodesToReturn = new ArrayList<CNetworkNode>();
// create a 2D arraylist of the joins...
ArrayList<ArrayList<CNetworkEdge>> orderedJoins = new ArrayList<ArrayList<CNetworkEdge>>();
// first get the weightings and the order...
double totalWeighting = 0.0;
// create a list of the drawn edges in the order they occur...
// THIS ORDER WILL BE REVERSED TO THEIR DIRECTIONS GIVEN!!
orderedJoins.add(new ArrayList<CNetworkEdge>());
orderedJoins.add(new ArrayList<CNetworkEdge>());
for(CNetworkEdge currentEdge : joins){
// if already drawn we want to add it to a list that we can add the others to...
if(currentEdge.drawn == true || currentEdge.split.direction >=0.0){
// form first entry in box list based on the directions that have been stored in the splits
// may not contain anything...and that should be fine!
// Could be optimised as a do while...?
// list 0 is for those less than this node's angle direction, list 1 is for those above it.
double currentDirection = (currentEdge.drawn==true)?returnAngle(currentEdge.split.direction+Math.PI):currentEdge.split.direction;
int j = (currentDirection<angleDirection)?0:1;
int storedPos = 0;
for(int i=0;i<orderedJoins.get(j).size();i++){
if(currentDirection>returnAngle(orderedJoins.get(j).get(i).split.direction+((orderedJoins.get(j).get(i).drawn==true)?Math.PI:0.0))){
storedPos++;
}
}
orderedJoins.get(j).add(storedPos,currentEdge);
currentEdge.considered = true;
}
}
orderedJoins.get(1).addAll(orderedJoins.get(0));
orderedJoins.get(0).clear();
// ONLY draw an edge if it hasn't already been drawn...
// populate the neighbours list...
for(CNetworkEdge currentEdge : joins){
if(currentEdge.drawn == false){
// renew list:
currentEdge.neighbours = new ArrayList<CNetworkEdge>();
// The zero element is the current edge
// Get the other node from the edge
CNetworkNode linkedNode = (currentEdge.networkNodeA == this)?currentEdge.networkNodeB:currentEdge.networkNodeA;
// work out it's weighting by looking at splits
currentEdge.weighting = linkedNode.FindWeighting(currentEdge, noOfTaxa);
totalWeighting += currentEdge.weighting;
// see if it is a box or not... i.e. do we need to add it to a list that already exists...?
// go through all joined nodes if might need to state is a box...
if(currentEdge.considered == false){
for(CNetworkEdge secondOrderLink : linkedNode.joins){
// if the corresponding split occurs
for(CNetworkEdge currentCompEdge : joins){
if(secondOrderLink.split == currentCompEdge.split && secondOrderLink !=currentCompEdge){
// If a second order edge matches a first order edge then we will soon have a box
// Therefore we need to ensure that these two edges occur next to each other in the drawing of nodes
currentEdge.neighbours.add(currentCompEdge);
currentEdge.weighting = linkedNode.FindWeighting(currentEdge, noOfTaxa)/2.0;
totalWeighting -= (currentEdge.weighting);
}
// end 2nd loop through current joins
}
// end loop of 2nd degree neighbours
}
// end if not considered 2nd degree neighbours
}
// end if not drawn
}
// end going through joins
}
// Let's now try and add to any list that exists already...
// add to the below one by looking for ends
// false if nothing found in this run...
boolean found = true;
// A ridiculous amount of nesting....
if(orderedJoins.get(1).size()>0){
do{
found = false;
// probably speeded up a little bit with Do loop...?!
for(CNetworkEdge currentEdge : joins){
if(currentEdge.considered == false && currentEdge.drawn == false){
for(CNetworkEdge neighbourLink : currentEdge.neighbours){
// case is higher end, then add it to this side
if(neighbourLink == orderedJoins.get(1).get(0)){
orderedJoins.get(1).add(0,currentEdge);
currentEdge.considered = true;
found = true;
}
// case is lower end, then add it to position "zero"
else if(neighbourLink == orderedJoins.get(1).get(orderedJoins.get(1).size()-1)){
orderedJoins.get(1).add(currentEdge);
currentEdge.considered = true;
found = true;
}
// end for loop through neighbours to the join we are considering (ARGH!)
}
// end checking is not drawn or added as a neighbour yet...
}
// end loop through joins
}
// end DO WHILE...
}
while(found == true);
// end condition of only bothering to add to the drawn nodes if nodes are already drawn...
}
// Now let's go through the rest and link them up...
for(CNetworkEdge neighbourLink:joins){
if(neighbourLink.considered == false && neighbourLink.drawn == false){
ArrayList<CNetworkEdge> buildList = new ArrayList<CNetworkEdge>();
neighbourLink.considered = true;
// add on start side...
ListToAdd(neighbourLink,true,buildList);
// add this node...
buildList.add(neighbourLink);
// add on other end...
ListToAdd(neighbourLink,false,buildList);
orderedJoins.add(buildList);
}
}
// now let's create ordered lists of the remaining ones from the neighbour array...
// remove all the considered flags..
for(CNetworkEdge currentEdge : joins){
currentEdge.considered = false;
}
// Now we have all the joins then let's divi up the space we have and assign directions...
double weightMultiplier = angleWidth/totalWeighting;
// Now we rearrange the lists so that the final one is for all above the direction and "1" is for all below...
// Temporary arrays to store above and below directed assigned area.
ArrayList<CNetworkEdge> lessThanEdges = new ArrayList<CNetworkEdge>();
ArrayList<CNetworkEdge> moreThanEdges = new ArrayList<CNetworkEdge>();
// REMINDER: list 0 is for those less than direction, list 1 for those above it.
boolean found2 = false;
for(int k=0;k<orderedJoins.get(1).size();k++){
if(orderedJoins.get(1).get(k).drawn == true){
found2 = true;
}
else{
if(found2 == true){
lessThanEdges.add(orderedJoins.get(1).get(k));
}
else{
moreThanEdges.add(orderedJoins.get(1).get(k));
}
}
}
// now position these correctly in the orderedlist...
orderedJoins.add(moreThanEdges);
orderedJoins.get(1).clear();
orderedJoins.set(1,lessThanEdges);
// Need to consider case there is nothing in the first list (first node)
// once again go through the edges but this time in order of the lists that we created...
// assign the beginning angle:
double currentMinAngle = returnAngle(angleDirection - angleWidth/2);
for(int i=0;i<orderedJoins.size();i++){
for(int j=0;j<orderedJoins.get(i).size();j++){
// no we add in order
CNetworkEdge currentJoin = orderedJoins.get(i).get(j);
// only draw those that need to be drawn
if(currentJoin.drawn == false){
// case does not have direction means we need to calculate direction...
double widthToAdd = weightMultiplier*currentJoin.weighting;
double directionToUse = returnAngle(currentMinAngle+widthToAdd/2.0);
if(currentJoin.split.direction<0){
currentJoin.split.direction = directionToUse;
// the following hackery is used to make sure it doesn't draw boxes that are too big...
double maxAngle = 2.5;
// if it has a neighbour, make sure it is within the limits
if(j>0){
// second angle is ALWAYS further round in +ve direction than latter....
// case new angle is bigger than last.... simply subtract to find difference
double boxAngleWidth = currentJoin.split.direction - orderedJoins.get(i).get(j-1).split.direction;
// otherwise difference is 2PI subtract this...
if(currentJoin.split.direction < orderedJoins.get(i).get(j-1).split.direction){
boxAngleWidth = 2*Math.PI-boxAngleWidth;
}
// now if this is > 3 then limit it...
if(boxAngleWidth > maxAngle){
currentJoin.split.direction = returnAngle(orderedJoins.get(i).get(j-1).split.direction+maxAngle);
}
}
// maybe there is a neighbour above...
else if(j==0 && orderedJoins.get(i).size() > 1){
if(orderedJoins.get(i).get(1).split.direction>=0.0){
// next angle is ALWAYS further round in +ve direction than latter....
// case new angle is bigger than last.... simply subtract to find difference
double boxAngleWidth = orderedJoins.get(i).get(1).split.direction - currentJoin.split.direction;
// otherwise difference is 2PI subtract this...
if(currentJoin.split.direction > orderedJoins.get(i).get(1).split.direction){
boxAngleWidth = 2*Math.PI-boxAngleWidth;
}
// now if this is > 3 then limit it...
if(boxAngleWidth > maxAngle){
currentJoin.split.direction = returnAngle(orderedJoins.get(i).get(1).split.direction-maxAngle);
}
}
}
}
// Now draw in the edge & node given the direction and assign the correct amount of angle to the node.
//Draw the edge...
currentJoin.xPosA = xPos;
currentJoin.yPosA = yPos;
currentJoin.xPosB = xPos+currentJoin.split.edgelength*Math.sin(currentJoin.split.direction);
currentJoin.yPosB = yPos+currentJoin.split.edgelength*Math.cos(currentJoin.split.direction);
currentJoin.drawn = true;
// Draw the node...
CNetworkNode linkedNode = (currentJoin.networkNodeA == this)?currentJoin.networkNodeB:currentJoin.networkNodeA;
linkedNode.xPos = currentJoin.xPosB;
linkedNode.yPos = currentJoin.yPosB;
if (linkedNode.drawn == true){
//find smallest angle beginning, should be mutually exclusive...:
double drawnMax = returnAngle(linkedNode.angleDirection+(linkedNode.angleWidth/2.0));
double drawnMin = returnAngle(linkedNode.angleDirection-(linkedNode.angleWidth/2.0));
double toAddMax = returnAngle(directionToUse+(widthToAdd/2.0));
double toAddMin = returnAngle(directionToUse-(widthToAdd/2.0));
if(Math.pow((drawnMax-toAddMin),2)<0.0001){
linkedNode.angleDirection = returnAngle(linkedNode.angleDirection+(widthToAdd/2.0));
//add half the mean angle width to the smaller one...
}
else{
linkedNode.angleDirection = returnAngle(directionToUse+(linkedNode.angleWidth/2.0));
}
}
else{
linkedNode.angleDirection = directionToUse;
}
linkedNode.angleWidth += widthToAdd;
/*System.out.println("*** This Node:"+linkedNode.Taxaname);
System.out.println("*** Direction:"+linkedNode.angleDirection);
System.out.println("*** Width: "+linkedNode.angleWidth);
System.out.println("*** Weight: "+currentJoin.weighting);
System.out.println("*** Total Weight: "+totalWeighting);
*/
linkedNode.drawn = true;
// make sure that we run the algorithm on this too!
nodesToReturn.add(linkedNode);
// Add to the current min angle...
currentMinAngle += widthToAdd;
}
}
}
return nodesToReturn;
}
/**
* Finds the weighting of this this node using the joins that join to it. Based on:
* a) returning "1" if it is a taxon.
* b) return the number of linked taxon in this direction by finding which node is a subset of this split
*
* @param compEdge Edge that we want to find the weighting for
* @param noOfTaxa Number of taxa
*/
// Function to work out the weighting of this node by:
public double FindWeighting(CNetworkEdge compEdge,int noOfTaxa){
// case is a taxon
if (joins.size() ==1) return 1;
else{
// go through all the edges and look for one that hasn't been drawn...
for(CNetworkEdge currentEdge : joins){
// Don't consider edges that have already been drawn...
if(currentEdge.drawn == false && currentEdge != compEdge){
// So for this edge that hasn't been drawn yet, see if it is compatible or not...
if(currentEdge.split.isCompatible(compEdge.split,noOfTaxa)== true){
//Find the side that one is a subset of and return the number with this side as the weighting.
return compEdge.split.getSubsetJoins(currentEdge,noOfTaxa);
}
}
}
// if still haven't returned anything, then everything has been drawn and so return something very small (otherwise later division by zero)!...
return 0.05;
}
}
/**
* Function returns joined up ArrayList if must be joined in a certain order...
* Used for stitching together neighbours
* Runs recursively!
*
* @param neighbourLink
* @param start begin from start if true, begin from end if false
* @param ListToAddTo
*/
// Join across from start or end...
public void ListToAdd(CNetworkEdge neighbourLink, boolean start,ArrayList<CNetworkEdge> ListToAddTo){
// create a new list to add to and return...
// only work on the nodes we can...
for(CNetworkEdge neighbour : neighbourLink.neighbours){
// if not drawn yet (shouldn't be anyway...) and not considered (not previous in chain)... then add
if(neighbour.considered == false && neighbour.drawn == false){
// Add to the list in the correct place
ListToAddTo.add((start==true)?0:ListToAddTo.size()-1,neighbour);
// mark as considered
neighbour.considered = true;
// now look next to it...
ListToAdd(neighbour, start, ListToAddTo);
// and head straight back up ignoring any subsequent neighbours as they shouldn't exist (hopefully)...
return;
}
}
// No neighbours..........
return;
}
/**
* Return an angle that is in the range 0 to 2*pi....
*
* @param inputAngle Any input angle
*/
public double returnAngle(double inputAngle){
inputAngle %= 2*Math.PI;
if(inputAngle < 0)
inputAngle += 2*Math.PI;
return inputAngle;
}
}