JAL-1421 last change tidied up properly
[jalview.git] / src / jalview / gui / TreeCanvas.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import java.awt.Color;
24 import java.awt.Dimension;
25 import java.awt.Font;
26 import java.awt.FontMetrics;
27 import java.awt.Graphics;
28 import java.awt.Graphics2D;
29 import java.awt.Point;
30 import java.awt.Rectangle;
31 import java.awt.RenderingHints;
32 import java.awt.event.MouseEvent;
33 import java.awt.event.MouseListener;
34 import java.awt.event.MouseMotionListener;
35 import java.awt.print.PageFormat;
36 import java.awt.print.Printable;
37 import java.awt.print.PrinterException;
38 import java.awt.print.PrinterJob;
39 import java.util.Hashtable;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Map.Entry;
43 import java.util.Vector;
44
45 import javax.swing.JPanel;
46 import javax.swing.JScrollPane;
47 import javax.swing.SwingUtilities;
48 import javax.swing.ToolTipManager;
49
50 import jalview.analysis.Conservation;
51 import jalview.analysis.TreeModel;
52 import jalview.api.AlignViewportI;
53 import jalview.datamodel.Sequence;
54 import jalview.datamodel.SequenceGroup;
55 import jalview.datamodel.SequenceI;
56 import jalview.datamodel.SequenceNode;
57 import jalview.gui.JalviewColourChooser.ColourChooserListener;
58 import jalview.schemes.ColourSchemeI;
59 import jalview.structure.SelectionSource;
60 import jalview.util.Format;
61 import jalview.util.MessageManager;
62
63 /**
64  * DOCUMENT ME!
65  * 
66  * @author $author$
67  * @version $Revision$
68  */
69 public class TreeCanvas extends JPanel implements MouseListener, Runnable,
70         Printable, MouseMotionListener, SelectionSource
71 {
72   /** DOCUMENT ME!! */
73   public static final String PLACEHOLDER = " * ";
74
75   TreeModel tree;
76
77   JScrollPane scrollPane;
78
79   TreePanel tp;
80
81   private AlignViewport av;
82
83   private AlignmentPanel ap;
84
85   Font font;
86
87   FontMetrics fm;
88
89   boolean fitToWindow = true;
90
91   boolean showDistances = false;
92
93   boolean showBootstrap = false;
94
95   boolean markPlaceholders = false;
96
97   int offx = 20;
98
99   int offy;
100
101   private float threshold;
102
103   String longestName;
104
105   int labelLength = -1;
106
107   Map<Object, Rectangle> nameHash = new Hashtable<>();
108
109   Map<SequenceNode, Rectangle> nodeHash = new Hashtable<>();
110
111   SequenceNode highlightNode;
112
113   boolean applyToAllViews = false;
114
115   /**
116    * Creates a new TreeCanvas object.
117    * 
118    * @param av
119    *          DOCUMENT ME!
120    * @param tree
121    *          DOCUMENT ME!
122    * @param scroller
123    *          DOCUMENT ME!
124    * @param label
125    *          DOCUMENT ME!
126    */
127   public TreeCanvas(TreePanel tp, AlignmentPanel ap, JScrollPane scroller)
128   {
129     this.tp = tp;
130     this.av = ap.av;
131     this.setAssociatedPanel(ap);
132     font = av.getFont();
133     scrollPane = scroller;
134     addMouseListener(this);
135     addMouseMotionListener(this);
136     ToolTipManager.sharedInstance().registerComponent(this);
137   }
138
139   /**
140    * DOCUMENT ME!
141    * 
142    * @param sequence
143    *          DOCUMENT ME!
144    */
145   public void treeSelectionChanged(SequenceI sequence)
146   {
147     AlignmentPanel[] aps = getAssociatedPanels();
148
149     for (int a = 0; a < aps.length; a++)
150     {
151       SequenceGroup selected = aps[a].av.getSelectionGroup();
152
153       if (selected == null)
154       {
155         selected = new SequenceGroup();
156         aps[a].av.setSelectionGroup(selected);
157       }
158
159       selected.setEndRes(aps[a].av.getAlignment().getWidth() - 1);
160       selected.addOrRemove(sequence, true);
161     }
162   }
163
164   /**
165    * DOCUMENT ME!
166    * 
167    * @param tree
168    *          DOCUMENT ME!
169    */
170   public void setTree(TreeModel tree)
171   {
172     this.tree = tree;
173     tree.findHeight(tree.getTopNode());
174
175     // Now have to calculate longest name based on the leaves
176     Vector<SequenceNode> leaves = tree.findLeaves(tree.getTopNode());
177     boolean has_placeholders = false;
178     longestName = "";
179
180     for (int i = 0; i < leaves.size(); i++)
181     {
182       SequenceNode lf = leaves.elementAt(i);
183
184       if (lf.isPlaceholder())
185       {
186         has_placeholders = true;
187       }
188
189       if (longestName.length() < ((Sequence) lf.element()).getName()
190               .length())
191       {
192         longestName = TreeCanvas.PLACEHOLDER
193                 + ((Sequence) lf.element()).getName();
194       }
195     }
196
197     setMarkPlaceholders(has_placeholders);
198   }
199
200   /**
201    * DOCUMENT ME!
202    * 
203    * @param g
204    *          DOCUMENT ME!
205    * @param node
206    *          DOCUMENT ME!
207    * @param chunk
208    *          DOCUMENT ME!
209    * @param wscale
210    *          DOCUMENT ME!
211    * @param width
212    *          DOCUMENT ME!
213    * @param offx
214    *          DOCUMENT ME!
215    * @param offy
216    *          DOCUMENT ME!
217    */
218   public void drawNode(Graphics g, SequenceNode node, float chunk,
219           double wscale, int width, int offx, int offy)
220   {
221     if (node == null)
222     {
223       return;
224     }
225
226     if ((node.left() == null) && (node.right() == null))
227     {
228       // Drawing leaf node
229       double height = node.height;
230       double dist = node.dist;
231
232       int xstart = (int) ((height - dist) * wscale) + offx;
233       int xend = (int) (height * wscale) + offx;
234
235       int ypos = (int) (node.ycount * chunk) + offy;
236
237       if (node.element() instanceof SequenceI)
238       {
239         SequenceI seq = (SequenceI) node.element();
240
241         if (av.getSequenceColour(seq) == Color.white)
242         {
243           g.setColor(Color.black);
244         }
245         else
246         {
247           g.setColor(av.getSequenceColour(seq).darker());
248         }
249       }
250       else
251       {
252         g.setColor(Color.black);
253       }
254
255       // Draw horizontal line
256       g.drawLine(xstart, ypos, xend, ypos);
257
258       String nodeLabel = "";
259
260       if (showDistances && (node.dist > 0))
261       {
262         nodeLabel = new Format("%-.2f").form(node.dist);
263       }
264
265       if (showBootstrap && node.bootstrap > -1)
266       {
267         if (showDistances)
268         {
269           nodeLabel = nodeLabel + " : ";
270         }
271
272         nodeLabel = nodeLabel + String.valueOf(node.bootstrap);
273       }
274
275       if (!nodeLabel.equals(""))
276       {
277         g.drawString(nodeLabel, xstart + 2, ypos - 2);
278       }
279
280       String name = (markPlaceholders && node.isPlaceholder())
281               ? (PLACEHOLDER + node.getName())
282               : node.getName();
283
284       int charWidth = fm.stringWidth(name) + 3;
285       int charHeight = font.getSize();
286
287       Rectangle rect = new Rectangle(xend + 10, ypos - charHeight / 2,
288               charWidth, charHeight);
289
290       nameHash.put(node.element(), rect);
291
292       // Colour selected leaves differently
293       SequenceGroup selected = av.getSelectionGroup();
294
295       if ((selected != null)
296               && selected.getSequences(null).contains(node.element()))
297       {
298         g.setColor(Color.gray);
299
300         g.fillRect(xend + 10, ypos - charHeight / 2, charWidth, charHeight);
301         g.setColor(Color.white);
302       }
303
304       g.drawString(name, xend + 10, ypos + fm.getDescent());
305       g.setColor(Color.black);
306     }
307     else
308     {
309       drawNode(g, (SequenceNode) node.left(), chunk, wscale, width, offx,
310               offy);
311       drawNode(g, (SequenceNode) node.right(), chunk, wscale, width, offx,
312               offy);
313
314       double height = node.height;
315       double dist = node.dist;
316
317       int xstart = (int) ((height - dist) * wscale) + offx;
318       int xend = (int) (height * wscale) + offx;
319       int ypos = (int) (node.ycount * chunk) + offy;
320
321       g.setColor(node.color.darker());
322
323       // Draw horizontal line
324       g.drawLine(xstart, ypos, xend, ypos);
325       if (node == highlightNode)
326       {
327         g.fillRect(xend - 3, ypos - 3, 6, 6);
328       }
329       else
330       {
331         g.fillRect(xend - 2, ypos - 2, 4, 4);
332       }
333
334       int ystart = (node.left() == null ? 0
335               : (int) (((SequenceNode) node.left()).ycount * chunk)) + offy;
336       int yend = (node.right() == null ? 0
337               : (int) (((SequenceNode) node.right()).ycount * chunk))
338               + offy;
339
340       Rectangle pos = new Rectangle(xend - 2, ypos - 2, 5, 5);
341       nodeHash.put(node, pos);
342
343       g.drawLine((int) (height * wscale) + offx, ystart,
344               (int) (height * wscale) + offx, yend);
345
346       String nodeLabel = "";
347
348       if (showDistances && (node.dist > 0))
349       {
350         nodeLabel = new Format("%-.2f").form(node.dist);
351       }
352
353       if (showBootstrap && node.bootstrap > -1)
354       {
355         if (showDistances)
356         {
357           nodeLabel = nodeLabel + " : ";
358         }
359
360         nodeLabel = nodeLabel + String.valueOf(node.bootstrap);
361       }
362
363       if (!nodeLabel.equals(""))
364       {
365         g.drawString(nodeLabel, xstart + 2, ypos - 2);
366       }
367     }
368   }
369
370   /**
371    * DOCUMENT ME!
372    * 
373    * @param x
374    *          DOCUMENT ME!
375    * @param y
376    *          DOCUMENT ME!
377    * 
378    * @return DOCUMENT ME!
379    */
380   public Object findElement(int x, int y)
381   {
382     for (Entry<Object, Rectangle> entry : nameHash.entrySet())
383     {
384       Rectangle rect = entry.getValue();
385
386       if ((x >= rect.x) && (x <= (rect.x + rect.width)) && (y >= rect.y)
387               && (y <= (rect.y + rect.height)))
388       {
389         return entry.getKey();
390       }
391     }
392
393     for (Entry<SequenceNode, Rectangle> entry : nodeHash.entrySet())
394     {
395       Rectangle rect = entry.getValue();
396
397       if ((x >= rect.x) && (x <= (rect.x + rect.width)) && (y >= rect.y)
398               && (y <= (rect.y + rect.height)))
399       {
400         return entry.getKey();
401       }
402     }
403
404     return null;
405   }
406
407   /**
408    * DOCUMENT ME!
409    * 
410    * @param pickBox
411    *          DOCUMENT ME!
412    */
413   public void pickNodes(Rectangle pickBox)
414   {
415     int width = getWidth();
416     int height = getHeight();
417
418     SequenceNode top = tree.getTopNode();
419
420     double wscale = ((width * .8) - (offx * 2)) / tree.getMaxHeight();
421
422     if (top.count == 0)
423     {
424       top.count = ((SequenceNode) top.left()).count
425               + ((SequenceNode) top.right()).count;
426     }
427
428     float chunk = (float) (height - (offy)) / top.count;
429
430     pickNode(pickBox, top, chunk, wscale, width, offx, offy);
431   }
432
433   /**
434    * DOCUMENT ME!
435    * 
436    * @param pickBox
437    *          DOCUMENT ME!
438    * @param node
439    *          DOCUMENT ME!
440    * @param chunk
441    *          DOCUMENT ME!
442    * @param wscale
443    *          DOCUMENT ME!
444    * @param width
445    *          DOCUMENT ME!
446    * @param offx
447    *          DOCUMENT ME!
448    * @param offy
449    *          DOCUMENT ME!
450    */
451   public void pickNode(Rectangle pickBox, SequenceNode node, float chunk,
452           double wscale, int width, int offx, int offy)
453   {
454     if (node == null)
455     {
456       return;
457     }
458
459     if ((node.left() == null) && (node.right() == null))
460     {
461       double height = node.height;
462 //      double dist = node.dist;
463 //      int xstart = (int) ((height - dist) * wscale) + offx;
464       int xend = (int) (height * wscale) + offx;
465
466       int ypos = (int) (node.ycount * chunk) + offy;
467
468       if (pickBox.contains(new Point(xend, ypos)))
469       {
470         if (node.element() instanceof SequenceI)
471         {
472           SequenceI seq = (SequenceI) node.element();
473           SequenceGroup sg = av.getSelectionGroup();
474
475           if (sg != null)
476           {
477             sg.addOrRemove(seq, true);
478           }
479         }
480       }
481     }
482     else
483     {
484       pickNode(pickBox, (SequenceNode) node.left(), chunk, wscale, width,
485               offx, offy);
486       pickNode(pickBox, (SequenceNode) node.right(), chunk, wscale, width,
487               offx, offy);
488     }
489   }
490
491   /**
492    * DOCUMENT ME!
493    * 
494    * @param node
495    *          DOCUMENT ME!
496    * @param c
497    *          DOCUMENT ME!
498    */
499   public void setColor(SequenceNode node, Color c)
500   {
501     if (node == null)
502     {
503       return;
504     }
505
506     node.color = c;
507     if (node.element() instanceof SequenceI)
508     {
509       final SequenceI seq = (SequenceI) node.element();
510       AlignmentPanel[] aps = getAssociatedPanels();
511       if (aps != null)
512       {
513         for (int a = 0; a < aps.length; a++)
514         {
515           aps[a].av.setSequenceColour(seq, c);
516         }
517       }
518     }
519     setColor((SequenceNode) node.left(), c);
520     setColor((SequenceNode) node.right(), c);
521   }
522
523   /**
524    * DOCUMENT ME!
525    */
526   void startPrinting()
527   {
528     Thread thread = new Thread(this);
529     thread.start();
530   }
531
532   // put printing in a thread to avoid painting problems
533   @Override
534   public void run()
535   {
536     PrinterJob printJob = PrinterJob.getPrinterJob();
537     PageFormat defaultPage = printJob.defaultPage();
538     PageFormat pf = printJob.pageDialog(defaultPage);
539
540     if (defaultPage == pf)
541     {
542       /*
543        * user cancelled
544        */
545       return;
546     }
547
548     printJob.setPrintable(this, pf);
549
550     if (printJob.printDialog())
551     {
552       try
553       {
554         printJob.print();
555       } catch (Exception PrintException)
556       {
557         PrintException.printStackTrace();
558       }
559     }
560   }
561
562   /**
563    * DOCUMENT ME!
564    * 
565    * @param pg
566    *          DOCUMENT ME!
567    * @param pf
568    *          DOCUMENT ME!
569    * @param pi
570    *          DOCUMENT ME!
571    * 
572    * @return DOCUMENT ME!
573    * 
574    * @throws PrinterException
575    *           DOCUMENT ME!
576    */
577   @Override
578   public int print(Graphics pg, PageFormat pf, int pi)
579           throws PrinterException
580   {
581     pg.setFont(font);
582     pg.translate((int) pf.getImageableX(), (int) pf.getImageableY());
583
584     int pwidth = (int) pf.getImageableWidth();
585     int pheight = (int) pf.getImageableHeight();
586
587     int noPages = getHeight() / pheight;
588
589     if (pi > noPages)
590     {
591       return Printable.NO_SUCH_PAGE;
592     }
593
594     if (pwidth > getWidth())
595     {
596       pwidth = getWidth();
597     }
598
599     if (fitToWindow)
600     {
601       if (pheight > getHeight())
602       {
603         pheight = getHeight();
604       }
605
606       noPages = 0;
607     }
608     else
609     {
610       FontMetrics fm = pg.getFontMetrics(font);
611       int height = fm.getHeight() * nameHash.size();
612       pg.translate(0, -pi * pheight);
613       pg.setClip(0, pi * pheight, pwidth, (pi * pheight) + pheight);
614
615       // translate number of pages,
616       // height is screen size as this is the
617       // non overlapping text size
618       pheight = height;
619     }
620
621     draw(pg, pwidth, pheight);
622
623     return Printable.PAGE_EXISTS;
624   }
625
626   /**
627    * DOCUMENT ME!
628    * 
629    * @param g
630    *          DOCUMENT ME!
631    */
632   @Override
633   public void paintComponent(Graphics g)
634   {
635     super.paintComponent(g);
636     g.setFont(font);
637
638     if (tree == null)
639     {
640       g.drawString(
641               MessageManager.getString("label.calculating_tree") + "....",
642               20, getHeight() / 2);
643     }
644     else
645     {
646       fm = g.getFontMetrics(font);
647
648       int nameCount = nameHash.size();
649       if (nameCount == 0)
650       {
651         repaint();
652       }
653
654       if (fitToWindow || (!fitToWindow && (scrollPane
655               .getHeight() > ((fm.getHeight() * nameCount) + offy))))
656       {
657         draw(g, scrollPane.getWidth(), scrollPane.getHeight());
658         setPreferredSize(null);
659       }
660       else
661       {
662         setPreferredSize(new Dimension(scrollPane.getWidth(),
663                 fm.getHeight() * nameCount));
664         draw(g, scrollPane.getWidth(), fm.getHeight() * nameCount);
665       }
666
667       scrollPane.revalidate();
668     }
669   }
670
671   /**
672    * DOCUMENT ME!
673    * 
674    * @param fontSize
675    *          DOCUMENT ME!
676    */
677   @Override
678   public void setFont(Font font)
679   {
680     this.font = font;
681     repaint();
682   }
683
684   /**
685    * DOCUMENT ME!
686    * 
687    * @param g1
688    *          DOCUMENT ME!
689    * @param width
690    *          DOCUMENT ME!
691    * @param height
692    *          DOCUMENT ME!
693    */
694   public void draw(Graphics g1, int width, int height)
695   {
696     Graphics2D g2 = (Graphics2D) g1;
697     g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
698             RenderingHints.VALUE_ANTIALIAS_ON);
699     g2.setColor(Color.white);
700     g2.fillRect(0, 0, width, height);
701     g2.setFont(font);
702
703     if (longestName == null || tree == null)
704     {
705       g2.drawString("Calculating tree.", 20, 20);
706     }
707     offy = font.getSize() + 10;
708
709     fm = g2.getFontMetrics(font);
710
711     labelLength = fm.stringWidth(longestName) + 20; // 20 allows for scrollbar
712
713     double wscale = (width - labelLength - (offx * 2))
714             / tree.getMaxHeight();
715
716     SequenceNode top = tree.getTopNode();
717
718     if (top.count == 0)
719     {
720       top.count = ((SequenceNode) top.left()).count
721               + ((SequenceNode) top.right()).count;
722     }
723
724     float chunk = (float) (height - (offy)) / top.count;
725
726     drawNode(g2, tree.getTopNode(), chunk, wscale, width, offx, offy);
727
728     if (threshold != 0)
729     {
730       if (av.getCurrentTree() == tree)
731       {
732         g2.setColor(Color.red);
733       }
734       else
735       {
736         g2.setColor(Color.gray);
737       }
738
739       int x = (int) ((threshold * (getWidth() - labelLength - (2 * offx)))
740               + offx);
741
742       g2.drawLine(x, 0, x, getHeight());
743     }
744   }
745
746   /**
747    * Empty method to satisfy the MouseListener interface
748    * 
749    * @param e
750    */
751   @Override
752   public void mouseReleased(MouseEvent e)
753   {
754     /*
755      * isPopupTrigger is set on mouseReleased on Windows
756      */
757     if (e.isPopupTrigger())
758     {
759       chooseSubtreeColour();
760       e.consume(); // prevent mouseClicked happening
761     }
762   }
763
764   /**
765    * Empty method to satisfy the MouseListener interface
766    * 
767    * @param e
768    */
769   @Override
770   public void mouseEntered(MouseEvent e)
771   {
772   }
773
774   /**
775    * Empty method to satisfy the MouseListener interface
776    * 
777    * @param e
778    */
779   @Override
780   public void mouseExited(MouseEvent e)
781   {
782   }
783
784   /**
785    * Handles a mouse click on a tree node (clicks elsewhere are handled in
786    * mousePressed). Click selects the sub-tree, double-click swaps leaf nodes
787    * order, right-click opens a dialogue to choose colour for the sub-tree.
788    * 
789    * @param e
790    */
791   @Override
792   public void mouseClicked(MouseEvent evt)
793   {
794     if (highlightNode == null)
795     {
796       return;
797     }
798
799     if (evt.getClickCount() > 1)
800     {
801       tree.swapNodes(highlightNode);
802       tree.reCount(tree.getTopNode());
803       tree.findHeight(tree.getTopNode());
804     }
805     else
806     {
807       Vector<SequenceNode> leaves = tree.findLeaves(highlightNode);
808
809       for (int i = 0; i < leaves.size(); i++)
810       {
811         SequenceI seq = (SequenceI) leaves.elementAt(i).element();
812         treeSelectionChanged(seq);
813       }
814       av.sendSelection();
815     }
816
817     PaintRefresher.Refresh(tp, av.getSequenceSetId());
818     repaint();
819   }
820
821   /**
822    * Offer the user the option to choose a colour for the highlighted node and
823    * its children; this colour is also applied to the corresponding sequence ids
824    * in the alignment
825    */
826   void chooseSubtreeColour()
827   {
828     String ttl = MessageManager.getString("label.select_subtree_colour");
829     ColourChooserListener listener = new ColourChooserListener() {
830       @Override
831       public void colourSelected(Color c)
832       {
833         setColor(highlightNode, c);
834         PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
835         repaint();
836       }
837     };
838     JalviewColourChooser.showColourChooser(this, ttl,  highlightNode.color, listener);
839   }
840
841   @Override
842   public void mouseMoved(MouseEvent evt)
843   {
844     av.setCurrentTree(tree);
845
846     Object ob = findElement(evt.getX(), evt.getY());
847
848     if (ob instanceof SequenceNode)
849     {
850       highlightNode = (SequenceNode) ob;
851       this.setToolTipText(
852               "<html>" + MessageManager.getString("label.highlightnode"));
853       repaint();
854
855     }
856     else
857     {
858       if (highlightNode != null)
859       {
860         highlightNode = null;
861         setToolTipText(null);
862         repaint();
863       }
864     }
865   }
866
867   @Override
868   public void mouseDragged(MouseEvent ect)
869   {
870   }
871
872   /**
873    * Handles a mouse press on a sequence name or the tree background canvas
874    * (click on a node is handled in mouseClicked). The action is to create
875    * groups by partitioning the tree at the mouse position. Colours for the
876    * groups (and sequence names) are generated randomly.
877    * 
878    * @param e
879    */
880   @Override
881   public void mousePressed(MouseEvent e)
882   {
883     av.setCurrentTree(tree);
884
885     /*
886      * isPopupTrigger is set for mousePressed (Mac)
887      * or mouseReleased (Windows)
888      */
889     if (e.isPopupTrigger())
890     {
891       if (highlightNode != null)
892       {
893         chooseSubtreeColour();
894       }
895       return;
896     }
897
898     /*
899      * defer right-click handling on Windows to
900      * mouseClicked; note isRightMouseButton
901      * also matches Cmd-click on Mac which should do
902      * nothing here
903      */
904     if (SwingUtilities.isRightMouseButton(e))
905     {
906       return;
907     }
908
909     int x = e.getX();
910     int y = e.getY();
911
912     Object ob = findElement(x, y);
913
914     if (ob instanceof SequenceI)
915     {
916       treeSelectionChanged((Sequence) ob);
917       PaintRefresher.Refresh(tp, getAssociatedPanel().av.getSequenceSetId());
918       repaint();
919       av.sendSelection();
920       return;
921     }
922     else if (!(ob instanceof SequenceNode))
923     {
924       // Find threshold
925       if (tree.getMaxHeight() != 0)
926       {
927         threshold = (float) (x - offx)
928                 / (float) (getWidth() - labelLength - (2 * offx));
929
930         List<SequenceNode> groups = tree.groupNodes(threshold);
931         setColor(tree.getTopNode(), Color.black);
932
933         AlignmentPanel[] aps = getAssociatedPanels();
934
935         // TODO push calls below into a single AlignViewportI method?
936         // see also AlignViewController.deleteGroups
937         for (int a = 0; a < aps.length; a++)
938         {
939           aps[a].av.setSelectionGroup(null);
940           aps[a].av.getAlignment().deleteAllGroups();
941           aps[a].av.clearSequenceColours();
942           if (aps[a].av.getCodingComplement() != null)
943           {
944             aps[a].av.getCodingComplement().setSelectionGroup(null);
945             aps[a].av.getCodingComplement().getAlignment()
946                     .deleteAllGroups();
947             aps[a].av.getCodingComplement().clearSequenceColours();
948           }
949           aps[a].av.setUpdateStructures(true);
950         }
951         colourGroups(groups);
952
953         /*
954          * clear partition (don't show vertical line) if
955          * it is to the right of all nodes
956          */
957         if (groups.isEmpty())
958         {
959           threshold = 0f;
960         }
961       }
962
963       PaintRefresher.Refresh(tp, getAssociatedPanel().av.getSequenceSetId());
964       repaint();
965     }
966
967   }
968
969   void colourGroups(List<SequenceNode> groups)
970   {
971     AlignmentPanel[] aps = getAssociatedPanels();
972     for (int i = 0; i < groups.size(); i++)
973     {
974       Color col = new Color((int) (Math.random() * 255),
975               (int) (Math.random() * 255), (int) (Math.random() * 255));
976       setColor(groups.get(i), col.brighter());
977
978       Vector<SequenceNode> l = tree.findLeaves(groups.get(i));
979
980       Vector<SequenceI> sequences = new Vector<>();
981
982       for (int j = 0; j < l.size(); j++)
983       {
984         SequenceI s1 = (SequenceI) l.elementAt(j).element();
985
986         if (!sequences.contains(s1))
987         {
988           sequences.addElement(s1);
989         }
990       }
991
992       ColourSchemeI cs = null;
993       SequenceGroup _sg = new SequenceGroup(sequences, null, cs, true, true,
994               false, 0, av.getAlignment().getWidth() - 1);
995
996       _sg.setName("JTreeGroup:" + _sg.hashCode());
997       _sg.setIdColour(col);
998
999       for (int a = 0; a < aps.length; a++)
1000       {
1001         SequenceGroup sg = new SequenceGroup(_sg);
1002         AlignViewport viewport = aps[a].av;
1003
1004         // Propagate group colours in each view
1005         if (viewport.getGlobalColourScheme() != null)
1006         {
1007           cs = viewport.getGlobalColourScheme().getInstance(viewport, sg);
1008           sg.setColourScheme(cs);
1009           sg.getGroupColourScheme().setThreshold(
1010                   viewport.getResidueShading().getThreshold(),
1011                   viewport.isIgnoreGapsConsensus());
1012
1013           if (viewport.getResidueShading().conservationApplied())
1014           {
1015             Conservation c = new Conservation("Group",
1016                     sg.getSequences(null), sg.getStartRes(),
1017                     sg.getEndRes());
1018             c.calculate();
1019             c.verdict(false, viewport.getConsPercGaps());
1020             sg.cs.setConservation(c);
1021           }
1022         }
1023         // indicate that associated structure views will need an update
1024         viewport.setUpdateStructures(true);
1025         // propagate structure view update and sequence group to complement view
1026         viewport.addSequenceGroup(sg);
1027       }
1028     }
1029
1030     // notify the panel(s) to redo any group specific stuff
1031     // also updates structure views if necessary
1032     for (int a = 0; a < aps.length; a++)
1033     {
1034       aps[a].updateAnnotation();
1035       final AlignViewportI codingComplement = aps[a].av
1036               .getCodingComplement();
1037       if (codingComplement != null)
1038       {
1039         ((AlignViewport) codingComplement).getAlignPanel()
1040                 .updateAnnotation();
1041       }
1042     }
1043   }
1044
1045   /**
1046    * DOCUMENT ME!
1047    * 
1048    * @param state
1049    *          DOCUMENT ME!
1050    */
1051   public void setShowDistances(boolean state)
1052   {
1053     this.showDistances = state;
1054     repaint();
1055   }
1056
1057   /**
1058    * DOCUMENT ME!
1059    * 
1060    * @param state
1061    *          DOCUMENT ME!
1062    */
1063   public void setShowBootstrap(boolean state)
1064   {
1065     this.showBootstrap = state;
1066     repaint();
1067   }
1068
1069   /**
1070    * DOCUMENT ME!
1071    * 
1072    * @param state
1073    *          DOCUMENT ME!
1074    */
1075   public void setMarkPlaceholders(boolean state)
1076   {
1077     this.markPlaceholders = state;
1078     repaint();
1079   }
1080
1081   AlignmentPanel[] getAssociatedPanels()
1082   {
1083     if (applyToAllViews)
1084     {
1085       return PaintRefresher.getAssociatedPanels(av.getSequenceSetId());
1086     }
1087     else
1088     {
1089       return new AlignmentPanel[] { getAssociatedPanel() };
1090     }
1091   }
1092
1093   public AlignmentPanel getAssociatedPanel()
1094   {
1095     return ap;
1096   }
1097
1098   public void setAssociatedPanel(AlignmentPanel ap)
1099   {
1100     this.ap = ap;
1101   }
1102
1103   public AlignViewport getViewport()
1104   {
1105     return av;
1106   }
1107
1108   public void setViewport(AlignViewport av)
1109   {
1110     this.av = av;
1111   }
1112
1113   public float getThreshold()
1114   {
1115     return threshold;
1116   }
1117
1118   public void setThreshold(float threshold)
1119   {
1120     this.threshold = threshold;
1121   }
1122
1123   public boolean isApplyToAllViews()
1124   {
1125     return this.applyToAllViews;
1126   }
1127
1128   public void setApplyToAllViews(boolean applyToAllViews)
1129   {
1130     this.applyToAllViews = applyToAllViews;
1131   }
1132 }