001package com.hfg.bio.phylogeny; 002 003import java.awt.Color; 004import java.awt.Dimension; 005import java.awt.Font; 006import java.awt.Frame; 007import java.awt.Graphics2D; 008import java.awt.Point; 009import java.awt.Rectangle; 010import java.awt.font.FontRenderContext; 011import java.awt.geom.AffineTransform; 012import java.awt.geom.Point2D; 013import java.awt.geom.Rectangle2D; 014import java.awt.image.BufferedImage; 015import java.io.InputStream; 016import java.io.IOException; 017import java.io.ByteArrayInputStream; 018import java.io.OutputStream; 019import java.util.*; 020import java.util.List; 021 022import com.hfg.css.CSS; 023import com.hfg.css.CSSRule; 024import com.hfg.exception.ProgrammingException; 025import com.hfg.graphics.ColorUtil; 026import com.hfg.graphics.TextUtil; 027import com.hfg.graphics.units.GfxSize; 028import com.hfg.graphics.units.GfxUnits; 029import com.hfg.graphics.units.Pixels; 030import com.hfg.graphics.units.Points; 031import com.hfg.html.Span; 032import com.hfg.html.Pre; 033import com.hfg.html.HTMLTag; 034import com.hfg.html.StyleTag; 035import com.hfg.html.attribute.HTMLColor; 036import com.hfg.image.ImageIO_Util; 037import com.hfg.math.SimpleSampleStats; 038import com.hfg.network.Edge; 039import com.hfg.svg.*; 040import com.hfg.svg.path.SvgPathEllipticalArcCmd; 041import com.hfg.svg.path.SvgPathMoveToCmd; 042import com.hfg.util.AttributeMgr; 043import com.hfg.util.StringBuilderPlus; 044import com.hfg.util.StringUtil; 045import com.hfg.util.collection.CollectionUtil; 046import com.hfg.util.collection.DataColumn; 047import com.hfg.util.collection.DataTable; 048import com.hfg.util.io.StreamUtil; 049import com.hfg.xml.XMLName; 050import com.hfg.xml.XMLNamespace; 051import com.hfg.xml.XMLTag; 052 053//------------------------------------------------------------------------------ 054/** 055 Object representation of a phylogenetic tree. 056 Does not work with negative distance values. 057 <div> 058 @see <a href='http://evolution.genetics.washington.edu/phylip/newicktree.html'>The Newick tree format</a> 059 </div> 060 <div> 061 @author J. Alex Taylor, hairyfatguy.com 062 </div> 063 */ 064//------------------------------------------------------------------------------ 065// com.hfg XML/HTML Coding Library 066// 067// This library is free software; you can redistribute it and/or 068// modify it under the terms of the GNU Lesser General Public 069// License as published by the Free Software Foundation; either 070// version 2.1 of the License, or (at your option) any later version. 071// 072// This library is distributed in the hope that it will be useful, 073// but WITHOUT ANY WARRANTY; without even the implied warranty of 074// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 075// Lesser General Public License for more details. 076// 077// You should have received a copy of the GNU Lesser General Public 078// License along with this library; if not, write to the Free Software 079// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 080// 081// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com 082// jataylor@hairyfatguy.com 083//------------------------------------------------------------------------------ 084// http://artedi.ebc.uu.se/course/X3-2004/Phylogeny/Phylogeny-Fundamentals/Phylogeny-Basics.html 085// 086public class PhylogeneticTree 087{ 088 public static enum Flag 089 { 090 ASSIGN_GROUPS_TO_SINGLETONS 091 } 092 093 /** 094 The ID used for the branch traversal limit ('branchLimit'). 095 */ 096 public static final String BRANCH_TRAVERSAL_LIMIT_BASE_ID = "branchLimit"; 097 private static int branchTraversalLimitIdSrc = 1; 098 099 private PhyloNode mRootNode; 100 private TreeDisplaySettings mSettings; 101 private String mTitle; 102 private DataTable mMetaDataTable; 103 private AttributeMgr mAttributeMgr; 104 105 private int mImageTopPadding = 50; 106 private int mImageLeftPadding = 50; 107 private int mAsciiWidth = sDefaultAsciiWidth; 108 109 // Cached 110 private List<PhyloNode> mAllNodes; 111 private List<PhyloNode> mLeafNodes; 112 113 private static int sDefaultAsciiWidth = 75; 114 115 private static FontRenderContext sFRC = new FontRenderContext(new AffineTransform(), true, true); 116 117 //########################################################################## 118 // CONSTRUCTORS 119 //########################################################################## 120 121 //-------------------------------------------------------------------------- 122 public PhylogeneticTree() 123 { 124 } 125 126 //-------------------------------------------------------------------------- 127 public PhylogeneticTree(String inTree) 128 { 129 parseNewick(new ByteArrayInputStream(inTree.getBytes())); 130 } 131 132 //-------------------------------------------------------------------------- 133 public PhylogeneticTree(InputStream inStream) 134 { 135 parseNewick(inStream); 136 } 137 138 //########################################################################## 139 // PUBLIC METHODS 140 //########################################################################## 141 142 //-------------------------------------------------------------------------- 143 public PhylogeneticTree setTitle(String inValue) 144 { 145 mTitle = inValue; 146 return this; 147 } 148 149 //-------------------------------------------------------------------------- 150 public String getTitle() 151 { 152 return mTitle; 153 } 154 155 156 //-------------------------------------------------------------------------- 157 public static void setDefaultAsciiWidth(int inValue) 158 { 159 sDefaultAsciiWidth = inValue; 160 } 161 162 //-------------------------------------------------------------------------- 163 public PhylogeneticTree setAsciiWidth(int inValue) 164 { 165 mAsciiWidth = inValue; 166 return this; 167 } 168 169 //-------------------------------------------------------------------------- 170 // 2 child nodes indicates a rooted tree. 3 indicates an unrooted tree. 171 public boolean isRooted() 172 { 173 return (mRootNode != null 174 && mRootNode.getLeafFacingEdges().size() == 2 175 && null == mRootNode.getParentNode()); 176 } 177 178 //-------------------------------------------------------------------------- 179 public PhyloNode getRootNode() 180 { 181 return mRootNode; 182 } 183 184 //-------------------------------------------------------------------------- 185 public void setRootNode(PhyloNode inValue) 186 { 187 mRootNode = inValue; 188 inValue.makeRoot(); 189 } 190 191 //-------------------------------------------------------------------------- 192 public List<PhyloNode> getLeafNodes() 193 { 194 if (null == mLeafNodes) 195 { 196 List<PhyloNode> leaves = new ArrayList<>(); 197 198 recursivelyRakeLeaves(leaves, mRootNode); 199 200 mLeafNodes = leaves; 201 } 202 203 return mLeafNodes; 204 } 205 206 //-------------------------------------------------------------------------- 207 public List<PhyloNode> getAllNodes() 208 { 209 if (null == mAllNodes) 210 { 211 List<PhyloNode> nodes = new ArrayList<>(); 212 213 recursivelyAddNodes(nodes, mRootNode); 214 215 mAllNodes = nodes; 216 } 217 218 return mAllNodes; 219 } 220 221 //-------------------------------------------------------------------------- 222 public void orderByNodeCount() 223 { 224 for (PhyloNode node : getAllNodes()) 225 { 226 node.orderEdgesByLeafCount(); 227 } 228 } 229 230 //-------------------------------------------------------------------------- 231 public Float getRootedTreeDistance() 232 { 233 return mRootNode.getMaxDistanceToLeaf(); 234 } 235 236 //-------------------------------------------------------------------------- 237 public void removeNode(PhyloNode inNode) 238 { 239 List<Edge<PhyloNode>> edges = inNode.getEdges(); 240 if (CollectionUtil.hasValues(edges)) 241 { 242 for (int i = 0; i < edges.size(); i++) 243 { 244 Edge<PhyloNode> edge = edges.get(i); 245 246 if (edge.getTo().equals(inNode)) 247 { 248 PhyloNode preceedingNode = edge.getFrom(); 249 preceedingNode.removeEdge(edge); 250 i--; 251 252 if (! CollectionUtil.hasValues(preceedingNode.getLeafFacingEdges())) 253 { 254 removeNode(preceedingNode); 255 } 256 } 257 } 258 } 259 260 // Clear caches 261 mAllNodes = null; 262 mLeafNodes = null; 263 } 264 265 //-------------------------------------------------------------------------- 266 public PhyloNode getClosestLeaf(PhyloNode inNode) 267 { 268 PhyloNode closestLeaf = null; 269 float closestDistance = Float.MAX_VALUE; 270 271 for (PhyloNode leaf : getLeafNodes()) 272 { 273 float distance = leaf.distanceTo(inNode); 274 if (! leaf.equals(inNode) 275 && (null == closestLeaf 276 || distance < closestDistance)) 277 { 278 closestLeaf = leaf; 279 closestDistance = distance; 280 } 281 } 282 283 return closestLeaf; 284 } 285 286 //-------------------------------------------------------------------------- 287 public Edge getClosestLeafPair() 288 { 289 Edge closestLeafPair = null; 290 291 List<PhyloNode> leaves = getLeafNodes(); 292 if (leaves.size() > 1) 293 { 294 for (int i = 0; i < leaves.size() - 1; i++) 295 { 296 PhyloNode leaf1 = leaves.get(i); 297 for (int j = i + 1; j < leaves.size(); j++) 298 { 299 PhyloNode leaf2 = leaves.get(j); 300 float distance = leaf1.distanceTo(leaf2); 301 if (null == closestLeafPair 302 || distance < closestLeafPair.getDistance()) 303 { 304 closestLeafPair = new Edge<>(leaf1, leaf2, distance); 305 } 306 } 307 } 308 } 309 310 return closestLeafPair; 311 } 312 313 //-------------------------------------------------------------------------- 314 public List<Edge<PhyloNode>> getMaxLeaf2LeafEdgeTrail() 315 { 316 List<Edge<PhyloNode>> edgeTrail = null; 317 318 List<PhyloNode> leaves = getLeafNodes(); 319 if (leaves.size() > 1) 320 { 321 Edge<PhyloNode> maxEdge = null; 322 for (int i = 0; i < leaves.size() - 1; i++) 323 { 324 PhyloNode leaf1 = leaves.get(i); 325 for (int j = i + 1; j < leaves.size(); j++) 326 { 327 PhyloNode leaf2 = leaves.get(j); 328 float distance = leaf1.distanceTo(leaf2); 329 if (null == maxEdge 330 || maxEdge.getDistance() < distance) 331 { 332 maxEdge = new Edge<>(leaf1, leaf2, distance); 333 } 334 } 335 } 336 337 edgeTrail = new ArrayList<>(); 338 Edge<PhyloNode> edge = maxEdge.getFrom().getParentEdge(); 339 while(edge != null) 340 { 341 edgeTrail.add(edge); 342 edge = edge.getFrom().getParentEdge(); 343 } 344 345 List<Edge<PhyloNode>> trailingTrail = new ArrayList<>(); 346 edge = maxEdge.getTo().getParentEdge(); 347 while(edge != null) 348 { 349 trailingTrail.add(edge); 350 edge = edge.getFrom().getParentEdge(); 351 } 352 353 for (int i = trailingTrail.size() - 1; i >= 0; i--) 354 { 355 edgeTrail.add(trailingTrail.get(i)); 356 } 357 } 358 359 return edgeTrail; 360 } 361 362 //-------------------------------------------------------------------------- 363 public void rootTreeByMidpointMethod() 364 { 365 List<Edge<PhyloNode>> maxEdgeTrail = getMaxLeaf2LeafEdgeTrail(); 366 if (maxEdgeTrail != null) 367 { 368 float distance = 0.0f; 369 Edge<PhyloNode> prevEdge = null; 370 for (Edge<PhyloNode> edge : maxEdgeTrail) 371 { 372 distance += edge.getDistance() != null ? edge.getDistance() : 0.0f; 373 if (null == prevEdge 374 || prevEdge.getTo() != edge.getFrom()) 375 { 376 edge.switchDirection(); 377 } 378 prevEdge = edge; 379 } 380 381 // Find the midpoint 382 distance /= 2; 383 384 for (Edge<PhyloNode> edge : maxEdgeTrail) 385 { 386 if (edge.getDistance() != null 387 && edge.getDistance() > distance) 388 { 389 // Insert the new root in this edge. 390 PhyloNode newRoot = new PhyloNode(); 391 newRoot.addEdge(edge.getTo(), distance); 392 newRoot.addEdge(edge.getFrom(), edge.getDistance() - distance); 393 // This will remove the edge from BOTH attached nodes. 394 edge.getTo().removeEdge(edge); 395 396 setRootNode(newRoot); 397 398 break; 399 } 400 else if (edge.getDistance() != null 401 && edge.getDistance() == distance) 402 { 403 setRootNode(edge.getTo()); 404 } 405 else 406 { 407 distance -= edge.getDistance(); 408 } 409 } 410 } 411 } 412 413 //-------------------------------------------------------------------------- 414 public List<List<PhyloNode>> groupNodes(float inBranchLengthTraversalLimit, Flag... inFlags) 415 { 416 Set<Flag> flags = new HashSet<>(5); 417 if (inFlags != null) 418 { 419 for (Flag flag : inFlags) 420 { 421 flags.add(flag); 422 } 423 } 424 425 List<List<PhyloNode>> groups = new ArrayList<>(); 426 427 for (PhyloNode leaf : getLeafNodes()) 428 { 429 Float bestFit = null; 430 List<PhyloNode> bestGroup = null; 431 432 for (List<PhyloNode> group : groups) 433 { 434 for (PhyloNode node : group) 435 { 436 float distance = leaf.distanceTo(node); 437 if (distance <= inBranchLengthTraversalLimit) 438 { 439 if (null == bestFit 440 || distance < bestFit) 441 { 442 bestFit = distance; 443 bestGroup = group; 444 } 445 } 446 } 447 } 448 449 if (bestGroup != null) 450 { 451 bestGroup.add(leaf); 452 } 453 else 454 { 455 // Create a new group 456 List<PhyloNode> group = new ArrayList<>(); 457 group.add(leaf); 458 groups.add(group); 459 } 460 } 461 462 // Remove singletons ? 463 if (! flags.contains(Flag.ASSIGN_GROUPS_TO_SINGLETONS)) 464 { 465 for (int i = groups.size() - 1; i >= 0; i--) 466 { 467 if (groups.get(i).size() == 1) 468 { 469 groups.remove(i); 470 } 471 } 472 } 473 474 return groups; 475 } 476 477 //-------------------------------------------------------------------------- 478 /** 479 Note that this DistanceMatrix may not be equivalent to the DistanceMatrix 480 originally used to construct the tree. 481 @return the DistanceMatrix for the tree 482 */ 483 public DistanceMatrix toDistanceMatrix() 484 { 485 DistanceMatrix matrix = new DistanceMatrix(); 486 for (PhyloNode node1 : getLeafNodes()) 487 { 488 for (PhyloNode node2 : getLeafNodes()) 489 { 490 if (node1 != node2) 491 { 492 matrix.setDistance(node1.getLabel(), node2.getLabel(), node1.distanceTo(node2)); 493 } 494 } 495 } 496 497 return matrix; 498 } 499 500 //-------------------------------------------------------------------------- 501 /** 502 Returns a Newick format representation of the tree. 503 @return Newick format string representation of the tree 504 */ 505 @Override 506 public String toString() 507 { 508 return toNewick(); 509 } 510 511 //-------------------------------------------------------------------------- 512 /** 513 Returns a Newick format representation of the tree. 514 @return Newick format string representation of the tree 515 */ 516 public String toNewick() 517 { 518 StringBuilder buffer = new StringBuilder(); 519 if (mRootNode != null) buffer.append(mRootNode); 520 buffer.append(";"); 521 522 return buffer.toString(); 523 } 524 525 //-------------------------------------------------------------------------- 526 /** 527 Constructs a phylogram in ASCII format. 528 @return an ASCII representation of the tree 529 */ 530 public String toASCII() 531 { 532 int numLines = getLeafNodes().size() * 2 - 1; 533 List<StringBuilder> lines = new ArrayList<>(numLines); 534 // Initialize the lines. 535 for (int i = 0; i < numLines; i++) 536 { 537 StringBuilder line = new StringBuilder(); 538 line.append(StringUtil.polyChar(' ', mAsciiWidth + 100)); 539 lines.add(line); 540 } 541 542 int leftPadding = 3; 543 544 // Determine the scale. 545 double scalingFactor = mAsciiWidth / getRootedTreeDistance(); 546 547 assignLineIndexes(); 548 549 550 for (PhyloNode node : getAllNodes()) 551 { 552 Edge<PhyloNode> parentEdge = node.getParentEdge(); 553// if (null == parentEdge || null == parentEdge.getFrom()) continue; 554 555 float distance = (parentEdge != null && parentEdge.getDistance() != null ? parentEdge.getDistance() : 0.0f); 556 int branchStart = leftPadding + (int)((node.getDistanceFromRoot() - distance) * scalingFactor); 557 int branchEnd = leftPadding + (int) (node.getDistanceFromRoot() * scalingFactor); 558 int branchLength = branchEnd - branchStart + 1; 559 560 // Branch 561 lines.get(node.getLineIndex()).replace(branchStart, branchEnd + 1, "+" + StringUtil.polyChar('-', branchLength - 1)); 562 563 if (node.isLeaf()) 564 { 565 if (StringUtil.isSet(node.getLabel())) 566 { 567 lines.get(node.getLineIndex()).replace(branchEnd + 2, branchEnd + 2 + node.getLabel().length(), node.getLabel()); 568 } 569 } 570 else 571 { 572 for (int lineIndex = node.getVericalLineStartIndex() + 1; lineIndex < node.getVericalLineEndIndex(); lineIndex++) 573 { 574 if (branchLength > 1 || lineIndex != node.getLineIndex()) lines.get(lineIndex).replace(branchEnd, branchEnd + 1, "|"); 575 } 576 } 577 } 578 579 StringBuilder output = new StringBuilder(); 580 for (StringBuilder line : lines) 581 { 582 output.append(StringUtil.trimTrailingWhitespace(line)); 583 output.append(System.getProperty("line.separator")); 584 } 585 586 return output.toString(); 587 } 588 589 //-------------------------------------------------------------------------- 590 /** 591 Constructs a phylogram in HTML format. 592 @return an HTML representation of the tree 593 */ 594 public HTMLTag toHTMLTag() 595 { 596 int numLines = getLeafNodes().size() * 2 - 1; 597 List<StringBuilder> lines = new ArrayList<>(numLines); 598 // Initialize the lines. 599 for (int i = 0; i < numLines; i++) 600 { 601 StringBuilder line = new StringBuilder(); 602 line.append(StringUtil.polyChar(' ', mAsciiWidth + 100)); 603 lines.add(line); 604 } 605 606 int leftPadding = 3; 607 608 // Determine the scale. 609 double scalingFactor = mAsciiWidth / getRootedTreeDistance(); 610 611 assignLineIndexes(); 612 613 614 for (PhyloNode node : getAllNodesOrderedByDistance()) 615 { 616 Edge<PhyloNode> parentEdge = node.getParentEdge(); 617 float distance = (parentEdge != null && parentEdge.getDistance() != null ? parentEdge.getDistance() : 0.0f); 618 int branchStart = leftPadding + (int)((node.getDistanceFromRoot() - distance) * scalingFactor); 619 int branchEnd = leftPadding + (int) (node.getDistanceFromRoot() * scalingFactor); 620 int branchLength = branchEnd - branchStart + 1; 621 622 // Branch 623 Span branchSpan = new Span("+"); 624 if (branchLength > 1) branchSpan.addContent(StringUtil.polyChar('-', branchLength - 2) + (node.isLeaf() ? "-" : "|")); 625 if (distance != 0.0f) 626 { 627 branchSpan.setTitle(distance + ""); 628 } 629 630 StringBuilder line = lines.get(node.getLineIndex()); 631 for (int i = branchEnd; i >= branchStart; i--) 632 { 633 if (line.charAt(i) != ' ' && line.charAt(i) != '|') 634 { 635 branchEnd--; 636 branchLength--; 637 branchSpan.setContent(branchSpan.getContent().substring(0, branchSpan.getContent().length() - 1)); 638 } 639 else 640 { 641 break; 642 } 643 } 644 if (branchLength > 0) 645 { 646 line.replace(branchStart, branchEnd + 1, branchSpan.toHTML()); 647 } 648 649 int tagEnd = branchStart + branchSpan.toHTML().length(); 650 651 if (node.isLeaf()) 652 { 653 if (StringUtil.isSet(node.getLabel())) 654 { 655 Span nodeSpan = new Span(node.getLabel()); 656 if (node.getColor() != null) 657 { 658 nodeSpan.setStyle(CSS.bgColor(node.getColor()) + CSS.color(HTMLColor.getContrastingColor(node.getColor()))); 659 } 660 line.replace(tagEnd + 2, tagEnd + 2 + node.getLabel().length(), nodeSpan.toHTML()); 661 } 662 } 663 else 664 { 665 for (int lineIndex = node.getVericalLineStartIndex() + 1; lineIndex < node.getVericalLineEndIndex(); lineIndex++) 666 { 667 if (lineIndex != node.getLineIndex() 668 && lines.get(lineIndex).charAt(branchEnd) == ' ') 669 { 670 lines.get(lineIndex).replace(branchEnd, branchEnd + 1, "|"); 671 } 672 } 673 } 674 } 675 676 StringBuilder output = new StringBuilder(); 677 for (StringBuilder line : lines) 678 { 679 output.append(line); 680 output.append(System.getProperty("line.separator")); 681 } 682 683 //TODO: add the scale to the bottom. 684 685 Pre pre = new Pre(); 686 pre.addContentWithoutEscaping(output.toString()); 687 688 return pre; 689 } 690 691 //-------------------------------------------------------------------------- 692 public void setMetaDataForDisplay(DataTable inMetaData) 693 { 694 mMetaDataTable = inMetaData; 695 } 696 697 //-------------------------------------------------------------------------- 698 public SVG toSVG() 699 { 700 return toSVG(new TreeDisplaySettings()); 701 } 702 703 //-------------------------------------------------------------------------- 704 public SVG toSVG(TreeDisplaySettings inDisplaySettings) 705 { 706 SVG svg; 707 708 switch (inDisplaySettings.getCladogramStyle()) 709 { 710 case Rectangular: 711 svg = toRectangularCladogramSVG(inDisplaySettings); 712 break; 713 case Circular: 714 svg = toCircularCladogramSVG(inDisplaySettings); 715 break; 716 default: 717 throw new ProgrammingException(StringUtil.singleQuote(inDisplaySettings.getCladogramStyle()) + " is not a recognized CladogramStyle!"); 718 } 719 720 return svg; 721 } 722 723 //-------------------------------------------------------------------------- 724 private SVG toRectangularCladogramSVG(TreeDisplaySettings inDisplaySettings) 725 { 726 Font font = inDisplaySettings.getFont(); 727 728 // Determine the scale. 729 int treeWidth = inDisplaySettings.getTreeWidth(); 730 float treeDistance = getRootedTreeDistance(); 731 double scalingFactor = treeWidth / treeDistance; 732 int leafLabelLeftPadding = inDisplaySettings.getLeafLabelLeftPadding().toInt(GfxUnits.pixels); 733 int scalePaddingTop = inDisplaySettings.getScalePaddingTop().toInt(GfxUnits.pixels); 734 735 // Determine a top-to-bottom node order 736 assignLineIndexes(); 737 738 int maxLeafX = 0; 739 740 int lineHeight = (int) TextUtil.getStringRect("A", font).getHeight(); 741 742 SVG svg = new SVG() 743 .setFont(font); 744 745 svg.addSubtag(generateScopedStyleTag()); 746 747 float maxLabelWidth = getMaxLabelWidth(font); 748 749 SvgGroup treeGroup = svg.addGroup().setClass("tree"); 750// treeGroup.setAttribute("transform", "scale(1)"); 751 752 // Add rectangle behind the tree which can be used as a drag handle if panning and zooming 753 treeGroup.addRect(new Rectangle2D.Float(mImageLeftPadding, mImageTopPadding, treeWidth + leafLabelLeftPadding + maxLabelWidth, getLeafNodes().size() * 2 * lineHeight)).setOpacity(0); 754 755 756 for (PhyloNode node : getAllNodes()) 757 { 758 Edge<PhyloNode> parentEdge = node.getParentEdge(); 759 float distance = (parentEdge != null && parentEdge.getDistance() != null ? parentEdge.getDistance() : 0.0f); 760 int branchStart = mImageLeftPadding + (int)((node.getDistanceFromRoot() - distance) * scalingFactor); 761 int branchEnd = mImageLeftPadding + (int) (node.getDistanceFromRoot() * scalingFactor); 762 763 int branchY = mImageTopPadding + ((node.getLineIndex() + 1) * lineHeight) - (lineHeight / 2); 764 765 // Branch 766 SvgLine branch = treeGroup.addLine(new Point(branchStart, branchY), new Point(branchEnd, branchY)); 767 768 if (distance != 0.0f) 769 { 770 branch.setTitle(distance + ""); 771 } 772 773 if (node.isLeaf()) 774 { 775 SvgNode svgNode = treeGroup.addCircle().setCx(branchEnd).setCy(branchY).setR(2).setId(node.getId()).addClass("leaf"); 776 PhyloNode parentNode = node.getParentNode(); 777 if (parentNode != null) 778 { 779 svgNode.setAttribute("parentId", parentNode.getId()); 780// svgNode.setAttribute("dist", node.getParentEdge().getDistance()); 781 } 782 783 if (StringUtil.isSet(node.getLabel())) 784 { 785 int labelX; 786 if (inDisplaySettings.getAlignLeaftNodeLabels()) 787 { 788 // Add a guide line to better match the label to the node 789 int guideStartX = branchEnd + leafLabelLeftPadding; 790 if (guideStartX > mImageLeftPadding + treeWidth) 791 { 792 guideStartX = mImageLeftPadding + treeWidth; 793 } 794 795 int guideEndX = mImageLeftPadding + treeWidth; 796 797 treeGroup.addLine(new Point2D.Double(guideStartX, branchY), new Point2D.Double(guideEndX, branchY)).addStyle("stroke:#" + ColorUtil.colorToHex(HTMLColor.LIGHT_GRAY)); 798 799 labelX = guideEndX + leafLabelLeftPadding; 800 } 801 else 802 { 803 labelX = branchEnd + leafLabelLeftPadding; 804 } 805 806 Rectangle2D textBoundBox = TextUtil.getStringRect(node.getLabel(), font); 807 808 // Create a colored rectangle to go behind the text. 809 SvgRect rect = treeGroup.addRect(new Rectangle(new Point(labelX - 2, 810 branchY - (int) (textBoundBox.getHeight()/2) - 3), 811 new Dimension((int)textBoundBox.getMaxX() + 4, (int)textBoundBox.getHeight() + 4))); 812// rect.setStyle("stroke-width: 0.5; stroke: #000000; fill:#" + ColorUtil.colorToHex(node.getColor()) + ";"); 813 rect.addStyle("border:none; fill:" + (node.getColor() != null ? "#" + ColorUtil.colorToHex(node.getColor()) : "none") + ";"); 814 rect.setId(node.getId() + "_labelBox"); 815 816 // Create the label text 817 SvgText label = treeGroup.addText(node.getLabel(), font, new Point(labelX, branchY + (lineHeight / 2) - 2)) 818 .setId(node.getId() + "_label"); 819 int labelRightX = branchEnd + leafLabelLeftPadding + (int) textBoundBox.getMaxX(); 820 if (labelRightX > maxLeafX) 821 { 822 maxLeafX = labelRightX; 823 } 824 825 if (node.getColor() != null) 826 { 827 label.setFill(new HTMLColor(node.getColor()).getContrastingColor()); 828 } 829 830 if (node.getTooltip() != null) 831 { 832 // For the browser to show a tooltip, add a subtag of title. 833 // Since the label sits on top of the rect, we'll title both. 834 rect.addSubtag(new XMLTag("title").setContent(node.getTooltip())); 835 label.addSubtag(new XMLTag("title").setContent(node.getTooltip())); 836 } 837 } 838 } 839 else 840 { 841 // Add the vertical connector line 842 treeGroup.addLine(new Point(branchEnd, mImageTopPadding + ((node.getVericalLineStartIndex() + 1) * lineHeight) - (lineHeight / 2)), 843 new Point(branchEnd, mImageTopPadding + ((node.getVericalLineEndIndex() + 1) * lineHeight) - (lineHeight / 2))); 844 } 845 } 846 847 if (inDisplaySettings.getShowScale() 848 || inDisplaySettings.getEnableDynamicGrouping()) 849 { 850 // Add the scale to the bottom. 851 svg.addSubtag(generateSvgScale(inDisplaySettings, svg, new Point2D.Double(mImageLeftPadding, treeGroup.getBoundsBox().getHeight() + mImageTopPadding + scalePaddingTop))); 852 } 853 854 if (inDisplaySettings.getShowBranchLengthTraversalLimit()) 855 { 856 // Make sure a BLTL value is set 857 if (null == inDisplaySettings.getBranchLengthTraversalLimit()) 858 { 859 // Default to 25% 860 inDisplaySettings.setBranchLengthTraversalLimit(0.25f * getRootedTreeDistance()); 861 } 862 else 863 { 864 inDisplaySettings.boundsCheckBranchLengthTraversalLimit(this); 865 } 866 867 int bltl_x = treeWidth - (int) ((inDisplaySettings.getBranchLengthTraversalLimit() * scalingFactor) / 2f); 868 treeGroup.addLine(new Point(mImageLeftPadding + bltl_x, mImageTopPadding), 869 new Point(mImageLeftPadding + bltl_x, (getLeafNodes().size() + 1) * 2 * lineHeight + 5)) 870 .setOpacity(30) 871 .addStyle("stroke:red") 872 .setId(BRANCH_TRAVERSAL_LIMIT_BASE_ID); 873 } 874 875 if (mMetaDataTable != null) 876 { 877 SvgGroup dataGroup = treeGroup.addGroup(); 878 879 for (DataColumn col : mMetaDataTable.getDataColumns()) 880 { 881 int leftX = maxLeafX + 40; 882 int y = mImageTopPadding - (2 * lineHeight); 883 884 Rectangle colTitleTextBoundBox = TextUtil.getStringRect(col.getTitle(), font); 885 if (leftX + colTitleTextBoundBox.getMaxX() > maxLeafX) 886 { 887 maxLeafX = leftX + (int) colTitleTextBoundBox.getMaxX(); 888 } 889 890 SvgText label = dataGroup.addText(col.getTitle(), font, new Point(leftX, y)); 891 // Underline the column header 892 dataGroup.addLine(new Point(leftX, y + 2), 893 new Point(leftX + (int) colTitleTextBoundBox.getMaxX(), y + 2)); 894 895 for (PhyloNode leafNode : getLeafNodes()) 896 { 897 Object dataValue = mMetaDataTable.get(leafNode.getId(), col); 898 if (dataValue != null) 899 { 900 String dataValueString = dataValue.toString(); 901 902 Rectangle textBoundBox = TextUtil.getStringRect(dataValueString, font); 903 if (leftX + textBoundBox.getMaxX() > maxLeafX) 904 { 905 maxLeafX = leftX + (int) textBoundBox.getMaxX(); 906 } 907 908 int x = leftX; 909 if (dataValue instanceof Number) 910 { 911 // Right justify 912 x += (int) (colTitleTextBoundBox.getMaxX() - textBoundBox.getMaxX()) - 4; 913 } 914 915 y = mImageTopPadding + (leafNode.getLineIndex() + 1) * lineHeight - 2; 916 917 // Display the data value 918 dataGroup.addText(dataValueString, font, new Point(x, y)); 919 } 920 } 921 } 922 } 923 924 return svg; 925 } 926 927 //-------------------------------------------------------------------------- 928 private StyleTag generateScopedStyleTag() 929 { 930 StyleTag styleTag = new StyleTag().setScoped(); 931 932 styleTag.addRule(new CSSRule(".tree path, .tree line, .scale path, .scale line { stroke:#333333; stroke-width:0.8 !important; stroke-linecap:round }")); 933 styleTag.addRule(new CSSRule(".tree .guide { stroke:#" + ColorUtil.colorToHex(HTMLColor.LIGHT_GRAY) + " }")); 934 935 return styleTag; 936 } 937 938 //-------------------------------------------------------------------------- 939 private SVG toCircularCladogramSVG(TreeDisplaySettings inDisplaySettings) 940 { 941 List<PhyloNode> leafNodes = getLeafNodes(); 942 943 double degreesPerNode = 360f/(leafNodes.size() + 1); 944 945 int minLabelHeight = 2; 946 float minRadius = calculateMinRadius(degreesPerNode, minLabelHeight); 947 int scalePaddingTop = inDisplaySettings.getScalePaddingTop().toInt(GfxUnits.pixels); 948 949 // Determine the scale. 950 float radius = inDisplaySettings.getTreeWidth() / 2; 951 if (radius < minRadius) 952 { 953 radius = minRadius; 954 } 955 956 float treeDistance = getRootedTreeDistance(); 957 if (treeDistance <= 0) 958 { 959 treeDistance = 0.0001f; 960 } 961 double scalingFactor = radius / treeDistance; 962 963 // Scale down the label font if necessary 964 float maxLabelSize = calculateMaxLabelSize(degreesPerNode, radius); 965 Font font = inDisplaySettings.getFont(); 966 if (new Points(font.getSize()).to(GfxUnits.pixels) > maxLabelSize) 967 { 968 font = new Font(font.getName(), font.getStyle(), (int) new Pixels(maxLabelSize).to(GfxUnits.points)); 969 } 970 971 972 float nodeCircleRadius = 0.5f; 973 974 double labelBoxOffsetRadians = -0.0005f; 975// double labelBoxOffsetRadians = -maxLabelSize * 0.01; 976 977 float maxLabelWidth = getMaxLabelWidth(font); 978 979 Point2D centerPoint = new Point2D.Float(mImageLeftPadding + maxLabelWidth + radius, 980 mImageTopPadding + maxLabelWidth + radius); 981 982 983 // Assign angles to the leaves 984 int leafCount = 0; 985 for (PhyloNode leafNode : leafNodes) 986 { 987 leafCount++; 988 double angle = 180 + (-degreesPerNode * leafCount); 989 double angleInRadians = Math.toRadians(angle); 990 leafNode.setAngle(angleInRadians); 991 } 992 993 994 SVG svg = new SVG() 995 .setFont(font); 996 997 svg.addSubtag(generateScopedStyleTag()); 998 999 SvgGroup treeGroup = svg.addGroup().setClass("tree"); 1000// treeGroup.setAttribute("transform", "scale(1)"); 1001 1002 // Add circle behind the tree which can be used as a drag handle if panning and zooming 1003 treeGroup.addCircle().setCenter(centerPoint).setR((int) (maxLabelWidth + radius)).setOpacity(0); 1004 1005 for (PhyloNode node : getAllNodes()) 1006 { 1007 Edge<PhyloNode> parentEdge = node.getParentEdge(); 1008 float distance = (parentEdge != null && parentEdge.getDistance() != null ? parentEdge.getDistance() : 0.0f); 1009 1010 if (node.isLeaf()) 1011 { 1012 double angleInRadians = node.getAngle(); 1013 double startX = centerPoint.getX() + ((node.getDistanceFromRoot() - distance) * scalingFactor) * Math.sin(angleInRadians); 1014 double startY = centerPoint.getY() + ((node.getDistanceFromRoot() - distance) * scalingFactor) * Math.cos(angleInRadians); 1015 double endX = centerPoint.getX() + (node.getDistanceFromRoot() * scalingFactor) * Math.sin(angleInRadians); 1016 double endY = centerPoint.getY() + (node.getDistanceFromRoot() * scalingFactor) * Math.cos(angleInRadians); 1017 1018 treeGroup.addLine(new Point2D.Double(startX, startY), new Point2D.Double(endX, endY)); 1019 SvgNode svgNode = treeGroup.addCircle().setCx(endX).setCy(endY).setR(nodeCircleRadius).setId(node.getId()).addClass("leaf"); 1020 PhyloNode parentNode = node.getParentNode(); 1021 if (parentNode != null) 1022 { 1023 svgNode.setAttribute("parentId", parentNode.getId()); 1024// svgNode.setAttribute("dist", node.getParentEdge().getDistance()); 1025 } 1026 1027 float labelRadius; 1028 if (inDisplaySettings.getAlignLeaftNodeLabels()) 1029 { 1030 // Add a guide line to better match the label to the node 1031 float guideRadius = (float) (node.getDistanceFromRoot() * scalingFactor) + 10; 1032 if (guideRadius < radius) 1033 { 1034 double guideStartX = centerPoint.getX() + guideRadius * Math.sin(angleInRadians); 1035 double guideStartY = centerPoint.getY() + guideRadius * Math.cos(angleInRadians); 1036 double guideEndX = centerPoint.getX() + radius * Math.sin(angleInRadians); 1037 double guideEndY = centerPoint.getY() + radius * Math.cos(angleInRadians); 1038 1039 treeGroup.addLine(new Point2D.Double(guideStartX, guideStartY), new Point2D.Double(guideEndX, guideEndY)).setClass("guide"); 1040 } 1041 1042 labelRadius = radius + 2; 1043 } 1044 else 1045 { 1046 labelRadius = (float) (node.getDistanceFromRoot() * scalingFactor) + 2f; 1047 } 1048 1049 if (StringUtil.isSet(node.getLabel())) 1050 { 1051 Rectangle2D textBoundBox = TextUtil.getStringRect(node.getLabel(), font); 1052 1053 double labelX = centerPoint.getX() + labelRadius * Math.sin(angleInRadians); 1054 double labelY = centerPoint.getY() + labelRadius * Math.cos(angleInRadians); 1055 1056 String labelTransform = "rotate(" + SVG.formatCoordinate(360 - Math.toDegrees(angleInRadians) + 90) + " " + SVG.formatCoordinate(labelX) + " " + SVG.formatCoordinate(labelY) + ")"; 1057 1058 1059 // Create a colored rectangle to go behind the text. 1060 double labelBoxX = centerPoint.getX() + labelRadius * Math.sin(angleInRadians + labelBoxOffsetRadians); 1061 double labelBoxY = centerPoint.getY() + labelRadius * Math.cos(angleInRadians + labelBoxOffsetRadians); 1062 1063 SvgRect rect = treeGroup.addRect(new Rectangle2D.Double(labelBoxX, labelBoxY - textBoundBox.getHeight(), textBoundBox.getWidth(), textBoundBox.getHeight())); 1064 1065 // We always need to create the label box even if the label has no color 1066 // because we need to support dynamic grouping via javascript. 1067 rect.addStyle("stroke-width:" + SVG.formatCoordinate(0.15 * textBoundBox.getHeight()) + "px;"); // Add a 15% border to better frame the text 1068 rect.addStyle("stroke:" + (node.getColor() != null ? "#" + ColorUtil.colorToHex(node.getColor()) : "none") + ";"); 1069 rect.addStyle("fill:" + (node.getColor() != null ? "#" + ColorUtil.colorToHex(node.getColor()) : "none") + ";"); 1070 rect.setTransform("rotate(" + SVG.formatCoordinate(360 - Math.toDegrees(angleInRadians + labelBoxOffsetRadians) + 90) + " " + SVG.formatCoordinate(labelBoxX) + " " + SVG.formatCoordinate(labelBoxY) + ")"); 1071 rect.setId(node.getId() + "_labelBox"); 1072 1073 // Create the label text 1074 SvgText label = treeGroup.addText(node.getLabel(), font, new Point2D.Float((float) labelX, (float) labelY)) 1075 .setTransform(labelTransform) 1076 .setId(node.getId() + "_label"); 1077 1078 if (node.getColor() != null) 1079 { 1080 label.setFill(new HTMLColor(node.getColor()).getContrastingColor()); 1081 } 1082 1083 if (node.getTooltip() != null) 1084 { 1085 // For the browser to show a tooltip, add a subtag of title. 1086 // Since the label sits on top of the rect, we'll title both. 1087 rect.addSubtag(new XMLTag("title").setContent(node.getTooltip())); 1088 label.addSubtag(new XMLTag("title").setContent(node.getTooltip())); 1089 } 1090 } 1091 } 1092 else 1093 { 1094 if (null == node.getAngle()) 1095 { 1096 recursivelyAssignAngles(node); 1097 } 1098 1099 List<PhyloNode> childNodes = node.getChildNodes(); 1100 double startAngleInRadians = childNodes.get(0).getAngle(); 1101 double endAngleInRadians = childNodes.get(childNodes.size() - 1).getAngle(); 1102 double deltaAngleInRadians = childNodes.get(0).getAngle() - childNodes.get(childNodes.size() - 1).getAngle(); 1103 1104 double startX = centerPoint.getX() + (node.getDistanceFromRoot() * scalingFactor) * Math.sin(startAngleInRadians); 1105 double startY = centerPoint.getY() + (node.getDistanceFromRoot() * scalingFactor) * Math.cos(startAngleInRadians); 1106 double endX = centerPoint.getX() + (node.getDistanceFromRoot() * scalingFactor) * Math.sin(endAngleInRadians); 1107 double endY = centerPoint.getY() + (node.getDistanceFromRoot() * scalingFactor) * Math.cos(endAngleInRadians); 1108 1109 // Like Noah, we need to build an arc 1110 SvgPath path = treeGroup.addPath().setFill(null).setStroke(Color.BLACK);//TODO .setStrokeWidth(1); 1111 path.addPathCommand(new SvgPathMoveToCmd().addPoint(new Point2D.Double(startX, startY))); 1112 List<Float> arcNumbers = new ArrayList<>(7); 1113 arcNumbers.add((float) (node.getDistanceFromRoot() * scalingFactor)); // rx 1114 arcNumbers.add((float) (node.getDistanceFromRoot() * scalingFactor)); // ry 1115 arcNumbers.add(0f); // x-rotation 1116 arcNumbers.add(deltaAngleInRadians > Math.PI ? 1f : 0f); // large-arc-sweep-flag 1117 arcNumbers.add(1f); // sweep-flag 1118 arcNumbers.add((float)endX); 1119 arcNumbers.add((float)endY); 1120 path.addPathCommand(new SvgPathEllipticalArcCmd().setRawNumbers(arcNumbers)); 1121 1122 1123 1124 // Now add the connector back to the previous node 1125 double angleInRadians = (startAngleInRadians + endAngleInRadians) / 2f; 1126 node.setAngle(angleInRadians); 1127 startX = centerPoint.getX() + ((node.getDistanceFromRoot() - distance) * scalingFactor) * Math.sin(angleInRadians); 1128 startY = centerPoint.getX() + ((node.getDistanceFromRoot() - distance) * scalingFactor) * Math.cos(angleInRadians); 1129 endX = centerPoint.getX() + (node.getDistanceFromRoot() * scalingFactor) * Math.sin(angleInRadians); 1130 endY = centerPoint.getX() + (node.getDistanceFromRoot() * scalingFactor) * Math.cos(angleInRadians); 1131 treeGroup.addLine(new Point2D.Double(startX, startY), new Point2D.Double(endX, endY)); 1132 1133 SvgNode svgNode = treeGroup.addCircle().setCx(endX).setCy(endY).setR(nodeCircleRadius).setId(node.getId()); 1134 PhyloNode parentNode = node.getParentNode(); 1135 if (parentNode != null) 1136 { 1137 svgNode.setAttribute("parentId", parentNode.getId()); 1138// svgNode.setAttribute("dist", node.getParentEdge().getDistance()); 1139 } 1140 } 1141 } 1142 1143 if (inDisplaySettings.getShowScale()) 1144 { 1145 // Add the scale to the bottom. 1146 1147 Rectangle2D treeBox = treeGroup.getBoundsBox(); 1148 Point2D topLeft = new Point2D.Double(centerPoint.getX() - radius, 1149 treeBox.getHeight() + treeBox.getY() + mImageTopPadding + scalePaddingTop); 1150 svg.addSubtag(generateSvgScale(inDisplaySettings, svg, topLeft)); 1151 } 1152 1153 if (inDisplaySettings.getShowBranchLengthTraversalLimit()) 1154 { 1155 // Make sure a BLTL value is set 1156 if (null == inDisplaySettings.getBranchLengthTraversalLimit()) 1157 { 1158 // Default to 25% 1159 inDisplaySettings.setBranchLengthTraversalLimit(0.25f * getRootedTreeDistance()); 1160 } 1161 else 1162 { 1163 inDisplaySettings.boundsCheckBranchLengthTraversalLimit(this); 1164 } 1165 1166 // Add circle behind the tree which can be used as a drag handle if panning and zooming 1167 treeGroup.addCircle().setCenter(centerPoint).setR((int) ((getRootedTreeDistance() - (inDisplaySettings.getBranchLengthTraversalLimit() / 2)) * scalingFactor)) 1168 .setOpacity(30) 1169 .addStyle("stroke:red") 1170 .setFill(null) 1171 .setId(BRANCH_TRAVERSAL_LIMIT_BASE_ID); 1172 } 1173 1174 1175 Rectangle2D contentRect = svg.getContentBoundsBox(); 1176 if (contentRect.getMinX() < 0 1177 || contentRect.getMinY() < 0) 1178 { 1179 int xOffset = (int) (contentRect.getX() < 0 ? -contentRect.getX() : 0); 1180 int yOffset = (int) (contentRect.getY() < 0 ? -contentRect.getY() : 0); 1181 contentRect.setRect(contentRect.getX() + xOffset, contentRect.getY() + yOffset, contentRect.getWidth(), contentRect.getHeight()); 1182 svg.setViewBox(contentRect); 1183 svg.setTransform("translate(" + xOffset + "," + yOffset + ")"); 1184 } 1185 1186 return svg; 1187 } 1188 1189 //-------------------------------------------------------------------------- 1190 // Calculates the maximum label size (height) given the number of labels on the circle 1191 // and the circle's radius. 1192 private float calculateMaxLabelSize(double inDegreesPerNode, float inLabelRadius) 1193 { 1194 double angle1 = inDegreesPerNode; 1195 double angleInRadians1 = Math.toRadians(angle1); 1196 double angle2 = inDegreesPerNode * 2; 1197 double angleInRadians2 = Math.toRadians(angle2); 1198 1199 double pt1_x = inLabelRadius * Math.sin(angleInRadians1); 1200 double pt1_y = inLabelRadius * Math.cos(angleInRadians1); 1201 1202 double pt2_x = inLabelRadius * Math.sin(angleInRadians2); 1203 double pt2_y = inLabelRadius * Math.cos(angleInRadians2); 1204 1205 double distance = Math.sqrt((pt1_x -= pt2_x) * pt1_x + (pt1_y -= pt2_y) * pt1_y); 1206 1207 return 0.8f * (float) distance; 1208 } 1209 1210 //-------------------------------------------------------------------------- 1211 // Calculates the minimum circle radius to achieve a minimum label size (height) 1212 // given the number of labels on the circle expressed as degrees per node. 1213 private float calculateMinRadius(double inDegreesPerNode, float inMinLabelHeight) 1214 { 1215 double angleInRadians = Math.toRadians(inDegreesPerNode); 1216 1217 return (float) ((inMinLabelHeight / 2) / Math.sin(angleInRadians/2)); 1218 } 1219 1220 //-------------------------------------------------------------------------- 1221 private float getMaxLabelWidth(Font inFont) 1222 { 1223 float maxLabelWidth = 0f; 1224 for (PhyloNode leafNode : getLeafNodes()) 1225 { 1226 if (StringUtil.isSet(leafNode.getLabel())) 1227 { 1228 Rectangle textBoundBox = TextUtil.getStringRect(leafNode.getLabel(), inFont); 1229 if (textBoundBox.getWidth() > maxLabelWidth) 1230 { 1231 maxLabelWidth = (float) textBoundBox.getWidth(); 1232 } 1233 } 1234 } 1235 1236 return maxLabelWidth; 1237 } 1238 1239 //-------------------------------------------------------------------------- 1240 private void recursivelyAssignAngles(PhyloNode inStartingNode) 1241 { 1242 SimpleSampleStats stats = new SimpleSampleStats(); 1243 1244 for (PhyloNode childNode : inStartingNode.getChildNodes()) 1245 { 1246 if (null == childNode.getAngle()) 1247 { 1248 recursivelyAssignAngles(childNode); 1249 } 1250 1251 stats.add(childNode.getAngle()); 1252 } 1253 1254 inStartingNode.setAngle(stats.getMean()); 1255 } 1256 1257 //-------------------------------------------------------------------------- 1258 private void attachDistanceMatrixAsMetadata(SVG inSVG, TreeDisplaySettings inDisplaySettings) 1259 { 1260 XMLNamespace hfgNamespace = XMLNamespace.getNamespace("hfg", "http://hairyfatguy.com"); 1261 inSVG.addXMLNamespaceDeclaration(hfgNamespace); 1262 1263 // Build a distance matrix 1264 XMLTag distMatrixTag = new XMLTag(new XMLName("distancematrix", hfgNamespace)); 1265 1266 List<PhyloNode> leafNodes = getLeafNodes(); 1267 for (int i = 0; i < leafNodes.size(); i++) 1268 { 1269 PhyloNode leaf1 = leafNodes.get(i); 1270 1271 StringBuilderPlus buffer = new StringBuilderPlus().setDelimiter(" "); 1272 1273 for (int j = 0; j <= i; j++) 1274 { 1275 PhyloNode leaf2 = leafNodes.get(j); 1276 1277 buffer.delimitedAppend(String.format("%.5f", leaf1.distanceTo(leaf2))); 1278 } 1279 1280 buffer.insert(0, leaf1.getId() + ":"); 1281 buffer.appendln(); 1282 1283 distMatrixTag.addContent(buffer); 1284 } 1285 1286 inSVG.getMetadata().addSubtag(distMatrixTag); 1287 1288 1289 XMLTag settingsTag = new XMLTag(new XMLName("settings", hfgNamespace)); 1290 1291 XMLTag settingTag = new XMLTag(new XMLName("setting", hfgNamespace)); 1292 settingTag.setAttribute("name", "assignGroupsToSingletons"); 1293 settingTag.setAttribute("value", inDisplaySettings.getAssignGroupsToSingletons()); 1294 settingsTag.addSubtag(settingTag); 1295 1296 if (inDisplaySettings.getBLTL_OnChangeCallback() != null) 1297 { 1298 settingTag = new XMLTag(new XMLName("setting", hfgNamespace)); 1299 settingTag.setAttribute("name", "bltlOnChangeCallback"); 1300 settingTag.setAttribute("value", inDisplaySettings.getBLTL_OnChangeCallback()); 1301 settingsTag.addSubtag(settingTag); 1302 } 1303 1304 if (inDisplaySettings.getBLTL_SelectionCompleteCallback() != null) 1305 { 1306 settingTag = new XMLTag(new XMLName("setting", hfgNamespace)); 1307 settingTag.setAttribute("name", "bltlSelectionCompleteCallback"); 1308 settingTag.setAttribute("value", inDisplaySettings.getBLTL_SelectionCompleteCallback()); 1309 settingsTag.addSubtag(settingTag); 1310 } 1311 1312 inSVG.getMetadata().addSubtag(settingsTag); 1313 } 1314 1315 //-------------------------------------------------------------------------- 1316 public static String generateSvgJavascript() 1317 throws IOException 1318 { 1319 String rsrcName = "rsrc/cladogram_bltl.js"; 1320 InputStream rsrcStream = PhylogeneticTree.class.getResourceAsStream(rsrcName); 1321 if (null == rsrcStream) 1322 { 1323 throw new IOException("The javascript rsrc " + StringUtil.singleQuote(rsrcName) + " couldn't be found!"); 1324 } 1325 1326 String js = StreamUtil.inputStreamToString(rsrcStream); 1327 1328 return js; 1329 } 1330 1331 //-------------------------------------------------------------------------- 1332 public void toJPG(TreeDisplaySettings inDisplaySettings, OutputStream inStream) 1333 throws IOException 1334 { 1335 ImageIO_Util.writeBufferedImageAsJpeg(getBufferedImage(inDisplaySettings), inStream); 1336 } 1337 1338 //-------------------------------------------------------------------------- 1339 public BufferedImage getBufferedImage(TreeDisplaySettings inDisplaySettings) 1340 { 1341 SVG svg = (SVG) toSVG(inDisplaySettings); 1342 // Add padding to the right 1343 svg.setWidth(svg.getWidth() + mImageLeftPadding); 1344 1345 Frame frame = new Frame(); 1346 frame.addNotify(); 1347 BufferedImage bufferedImage = new BufferedImage(svg.getWidth(), 1348 svg.getHeight(), 1349 BufferedImage.TYPE_INT_RGB); 1350 1351 Graphics2D g2 = (Graphics2D) bufferedImage.getGraphics(); 1352 svg.draw(g2); 1353 1354 return bufferedImage; 1355 } 1356 1357 //-------------------------------------------------------------------------- 1358 public BufferedImage getBufferedImageWithMaxWidth(TreeDisplaySettings inDisplaySettings, GfxSize inMaxWidth) 1359 { 1360 SVG svg = (SVG) toSVG(inDisplaySettings); 1361 // Add padding to the right 1362 svg.setWidth(svg.getWidth() + mImageLeftPadding); 1363 1364 if (svg.getWidth() > inMaxWidth.to(GfxUnits.pixels)) 1365 { 1366 float scalingFactor = inMaxWidth.to(GfxUnits.pixels) / svg.getWidth(); 1367 svg.scale(scalingFactor); 1368 } 1369 1370 Frame frame = new Frame(); 1371 frame.addNotify(); 1372 BufferedImage bufferedImage = new BufferedImage(svg.getWidth(), 1373 svg.getHeight(), 1374 BufferedImage.TYPE_INT_RGB); 1375 1376 Graphics2D g2 = (Graphics2D) bufferedImage.getGraphics(); 1377 svg.draw(g2); 1378 1379 return bufferedImage; 1380 } 1381 1382 //-------------------------------------------------------------------------- 1383 public List<PhyloNode> getAllNodesOrderedByDistance() 1384 { 1385 List<PhyloNode> nodes = new ArrayList<>(getAllNodes()); 1386 1387 Collections.sort(nodes, new NodeDistanceComparator()); 1388 1389 return nodes; 1390 } 1391 1392 // Attribute methods 1393 1394 //-------------------------------------------------------------------------- 1395 public void setAttribute(String inName, Object inValue) 1396 { 1397 getOrInitAttributeMgr().setAttribute(inName, inValue); 1398 } 1399 1400 //-------------------------------------------------------------------------- 1401 public boolean hasAttribute(String inName) 1402 { 1403 return mAttributeMgr != null && getOrInitAttributeMgr().hasAttribute(inName); 1404 } 1405 1406 //-------------------------------------------------------------------------- 1407 public Object getAttribute(String inName) 1408 { 1409 return getOrInitAttributeMgr().getAttribute(inName); 1410 } 1411 1412 //-------------------------------------------------------------------------- 1413 public Collection<String> getAttributeNames() 1414 { 1415 return getOrInitAttributeMgr().getAttributeNames(); 1416 } 1417 1418 //-------------------------------------------------------------------------- 1419 public void clearAttributes() 1420 { 1421 if (mAttributeMgr != null) 1422 { 1423 mAttributeMgr.clearAttributes(); 1424 } 1425 } 1426 1427 //-------------------------------------------------------------------------- 1428 public Object removeAttribute(String inName) 1429 { 1430 Object attr = null; 1431 if (mAttributeMgr != null) 1432 { 1433 attr = getOrInitAttributeMgr().removeAttribute(inName); 1434 } 1435 1436 return attr; 1437 } 1438 1439 1440 //########################################################################## 1441 // PRIVATE METHODS 1442 //########################################################################## 1443 1444 1445 //-------------------------------------------------------------------------- 1446 private AttributeMgr getOrInitAttributeMgr() 1447 { 1448 if (null == mAttributeMgr) 1449 { 1450 mAttributeMgr = new AttributeMgr(); 1451 } 1452 1453 return mAttributeMgr; 1454 } 1455 1456 //-------------------------------------------------------------------------- 1457 private String getFormatString(float inValue) 1458 { 1459 String formatString = "%"; 1460 1461 if (inValue > 1000) 1462 { 1463 formatString += ".2e"; 1464 } 1465 else if (inValue < 0.01) 1466 { 1467 formatString += ".2e"; 1468 } 1469 else 1470 { 1471 formatString += ".2f"; 1472 } 1473 1474 return formatString; 1475 } 1476 1477 //-------------------------------------------------------------------------- 1478 private void parseNewick(InputStream inStream) 1479 { 1480 try 1481 { 1482 StringBuilder buffer = new StringBuilder(); 1483 PhyloNode currentNode = null; 1484 int prevChar = 0; 1485 int theChar; 1486 1487 while ((theChar = inStream.read()) != -1 1488 && theChar != ';') 1489 { 1490 if (Character.isWhitespace(theChar)) continue; 1491 1492 if (prevChar == ',' 1493 || prevChar == ')') 1494 { 1495 currentNode = currentNode.getParentNode(); 1496 } 1497 1498 if (theChar == '(') 1499 { 1500 if (null == currentNode) 1501 { 1502 currentNode = new PhyloNode(); 1503 mRootNode = currentNode; 1504 } 1505 else 1506 { 1507 PhyloNode newNode = new PhyloNode(); 1508 currentNode.addEdge(newNode, null); 1509 currentNode = newNode; 1510 } 1511 } 1512 else if (prevChar == ')') 1513 { 1514// currentNode = currentNode.getParentNode(); 1515 1516 // Read the label (if present). 1517 buffer.setLength(0); 1518 if (theChar != ':' 1519 && theChar != ',') 1520 { 1521 do 1522 { 1523 buffer.append((char) theChar); 1524 } 1525 while ((theChar = inStream.read()) != -1 1526 && theChar != ':' 1527 && theChar != ',' 1528 && theChar != ';'); 1529 1530 if (StringUtil.isSet(buffer.toString())) 1531 { 1532 currentNode.setId(buffer.toString()); 1533 currentNode.setLabel(buffer.toString()); 1534 } 1535 } 1536 } 1537 else if (prevChar == '(' 1538 || prevChar == ',') 1539 { 1540 PhyloNode newNode = new PhyloNode(); 1541 currentNode.addEdge(newNode, null); 1542 currentNode = newNode; 1543 1544 // Read the label (if present). 1545 buffer.setLength(0); 1546 do 1547 { 1548// buffer.append(theChar == '_' ? ' ' : (char) theChar); 1549 buffer.append((char) theChar); 1550 } 1551 while ((theChar = inStream.read()) != -1 1552 && theChar != ':' 1553 && theChar != ',' 1554 && theChar != ';'); 1555 1556 if (StringUtil.isSet(buffer.toString())) 1557 { 1558 currentNode.setId(buffer.toString()); 1559 currentNode.setLabel(buffer.toString()); 1560 } 1561 } 1562 else if (prevChar == ':') 1563 { 1564 // Read the distance value (branch length) 1565 buffer.setLength(0); 1566 do 1567 { 1568 buffer.append((char) theChar); 1569 } 1570 while ((theChar = inStream.read()) != -1 1571 && theChar != ')' 1572 && theChar != ',' 1573 && theChar != ';'); 1574 1575 if (StringUtil.isSet(buffer.toString())) 1576 { 1577 Edge<PhyloNode> parentEdge = currentNode.getParentEdge(); 1578 if (null == parentEdge) 1579 { 1580 Edge<PhyloNode> edge = new Edge<PhyloNode>(null, currentNode, Float.parseFloat(buffer.toString())); 1581 currentNode.addEdge(edge); 1582 } 1583 else 1584 { 1585 parentEdge.setDistance(Float.parseFloat(buffer.toString())); 1586 } 1587 } 1588 } 1589 1590 prevChar = theChar; 1591 } 1592 1593 1594 // Assign node ids 1595 int nodeCount = 0; 1596 for (PhyloNode node : getAllNodes()) 1597 { 1598 node.setId(++nodeCount); 1599 } 1600 } 1601 catch (IOException e) 1602 { 1603 throw new RuntimeException(e); 1604 } 1605 } 1606 1607 //-------------------------------------------------------------------------- 1608 private void recursivelyRakeLeaves(List<PhyloNode> inLeaves, PhyloNode inNode) 1609 { 1610 if (inNode != mRootNode 1611 && inNode.isLeaf()) 1612 { 1613 inLeaves.add(inNode); 1614 } 1615 else 1616 { 1617 List<Edge<PhyloNode>> leafFacingEdges = inNode.getLeafFacingEdges(); 1618 for (Edge<PhyloNode> edge : leafFacingEdges) 1619 { 1620 recursivelyRakeLeaves(inLeaves, edge.getTo()); 1621 } 1622 } 1623 } 1624 1625 //-------------------------------------------------------------------------- 1626 private void recursivelyAddNodes(List<PhyloNode> inNodes, PhyloNode inNode) 1627 { 1628 inNodes.add(inNode); 1629 1630 List<Edge<PhyloNode>> leafFacingEdges = inNode.getLeafFacingEdges(); 1631 if (CollectionUtil.hasValues(leafFacingEdges)) 1632 { 1633 for (Edge<PhyloNode> edge : leafFacingEdges) 1634 { 1635 recursivelyAddNodes(inNodes, edge.getTo()); 1636 } 1637 } 1638 } 1639 1640 //-------------------------------------------------------------------------- 1641 private void assignLineIndexes() 1642 { 1643 int leafIndex = 0; 1644 List<PhyloNode> leafNodes = getLeafNodes(); 1645 for (PhyloNode leaf : leafNodes) 1646 { 1647 leaf.setLineIndex(leafIndex++ * 2); 1648 } 1649 1650 for (PhyloNode leaf : leafNodes) 1651 { 1652 PhyloNode node = leaf.getParentNode(); 1653 if (node != null) 1654 { 1655 do 1656 { 1657 if (node.getLineIndex() != null) break; 1658 1659 // Find the line index range of the child nodes 1660 List<Edge<PhyloNode>> leafFacingEdges = node.getLeafFacingEdges(); 1661 Integer lowerBound = leafFacingEdges.get(0).getTo().getLineIndex(); 1662 Integer upperBound = leafFacingEdges.get(leafFacingEdges.size() - 1).getTo().getLineIndex(); 1663 1664 // Can't assign yet if the bounds aren't set. 1665 if (null == lowerBound || null == upperBound) break; 1666 1667 node.setLineIndex(lowerBound + (upperBound - lowerBound) / 2); 1668 node.setVerticalLineStartIndex(lowerBound); 1669 node.setVerticalLineEndIndex(upperBound); 1670 1671 node = node.getParentNode(); 1672 } 1673 while (node != null); 1674 } 1675 } 1676 } 1677 1678 //-------------------------------------------------------------------------- 1679 private SvgGroup generateSvgScale(TreeDisplaySettings inDisplaySettings, SVG inSVG, Point2D inStartPoint) 1680 { 1681 Font font = inDisplaySettings.getFont(); 1682 1683 int treeWidth = inDisplaySettings.getTreeWidth(); 1684 float treeDistance = getRootedTreeDistance(); 1685 double scalingFactor = treeWidth / treeDistance; 1686 int tickHeight = inDisplaySettings.getScaleTickHeight(); 1687 1688 SvgGroup scalegroup = new SvgGroup().setClass("scale"); 1689 1690 // Horizontal axis 1691 SvgLine axis = scalegroup.addLine(inStartPoint, new Point2D.Double(inStartPoint.getX() + treeWidth, inStartPoint.getY())); 1692 1693 // Tick marks 1694 int numTicks = 4; 1695 for (int i = 0; i <= numTicks; i++) 1696 { 1697 scalegroup.addLine(new Point2D.Double(inStartPoint.getX() + (i * treeWidth/numTicks), inStartPoint.getY()), 1698 new Point2D.Double(inStartPoint.getX() + (i * treeWidth/numTicks), (int) inStartPoint.getY() + tickHeight)); 1699 } 1700 1701 // Labels 1702 String formatString = getFormatString(treeDistance); 1703 for (int i = 0; i <= numTicks; i++) 1704 { 1705 String label = String.format(formatString, treeDistance - (treeDistance * i/numTicks)); 1706 Rectangle2D textBoundBox = TextUtil.getStringRect(label, font); 1707 scalegroup.addText(label, font, new Point2D.Double(inStartPoint.getX() + (i * treeWidth/numTicks) - (textBoundBox.getWidth()/2), 1708 inStartPoint.getY() + tickHeight + textBoundBox.getHeight())); 1709 } 1710 1711 if (inDisplaySettings.getEnableDynamicGrouping()) 1712 { 1713 axis.addStyle("stroke-width:4px; stroke-linecap:round"); 1714 1715 if (null == inDisplaySettings.getBranchLengthTraversalLimit()) 1716 { 1717 inDisplaySettings.setBranchLengthTraversalLimit(0.25f * getRootedTreeDistance()); 1718 } 1719 else 1720 { 1721 inDisplaySettings.boundsCheckBranchLengthTraversalLimit(this); 1722 } 1723 1724 attachDistanceMatrixAsMetadata(inSVG, inDisplaySettings); 1725 1726 // The value is divided by 2 because the distance includes out and back 1727 float sliderPosition = treeWidth - ((float) (inDisplaySettings.getBranchLengthTraversalLimit() * scalingFactor) / 2f); 1728 1729 // Handle for adjusting the BLTL 1730 SvgCircle handle = scalegroup.addCircle().setCx((int) (inStartPoint.getX() + sliderPosition)).setCy((int) inStartPoint.getY()).setR(8).setId("bltl_slider_handle"); 1731 handle.addStyle(" fill:#ffffff"); 1732 handle.addStyle(" stroke:#000000"); 1733 handle.addStyle(" stroke-width:4px"); 1734 handle.addStyle(" cursor:grab"); 1735 handle.addStyle(" cursor:-moz-grab"); 1736 handle.addStyle(" cursor:-webkit-grab"); 1737 handle.setAttribute("x", (int) inStartPoint.getX() + sliderPosition); 1738 handle.setAttribute("min_x", (int) inStartPoint.getX()); 1739 handle.setAttribute("max_x", (int) inStartPoint.getX() + treeWidth); 1740 handle.setAttribute("scaling_factor", scalingFactor); 1741 handle.setOnMouseOver("branchTraversalLimitHandleOnMouseOver(this);"); 1742 handle.setOnMouseOut("branchTraversalLimitHandleOnMouseOut(this);"); 1743 handle.setOnMouseDown("branchTraversalLimitHandleDragStart(evt, this)"); 1744 1745 if (inDisplaySettings.getCladogramStyle().equals(CladogramStyle.Circular)) 1746 { 1747 handle.setAttribute("tree_distance", getRootedTreeDistance()); 1748 handle.setAttribute("tree_radius", inDisplaySettings.getTreeWidth() / 2); 1749 } 1750 } 1751 1752 return scalegroup; 1753 } 1754 1755 //########################################################################## 1756 // INNER CLASSES 1757 //########################################################################## 1758 1759 private class NodeDistanceComparator implements Comparator<PhyloNode> 1760 { 1761 public int compare(PhyloNode node1, PhyloNode node2) 1762 { 1763 return - node1.getDistanceFromRoot().compareTo(node2.getDistanceFromRoot()); 1764 } 1765 } 1766 1767}