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}