0012888311c063e1f15029459bef68d3fd70edb2
[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     {
831       @Override
832       public void colourSelected(Color c)
833       {
834         setColor(highlightNode, c);
835         PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
836         repaint();
837       }
838     };
839     JalviewColourChooser.showColourChooser(this, ttl, highlightNode.color,
840             listener);
841   }
842
843   @Override
844   public void mouseMoved(MouseEvent evt)
845   {
846     av.setCurrentTree(tree);
847
848     Object ob = findElement(evt.getX(), evt.getY());
849
850     if (ob instanceof SequenceNode)
851     {
852       highlightNode = (SequenceNode) ob;
853       this.setToolTipText(
854               "<html>" + MessageManager.getString("label.highlightnode"));
855       repaint();
856
857     }
858     else
859     {
860       if (highlightNode != null)
861       {
862         highlightNode = null;
863         setToolTipText(null);
864         repaint();
865       }
866     }
867   }
868
869   @Override
870   public void mouseDragged(MouseEvent ect)
871   {
872   }
873
874   /**
875    * Handles a mouse press on a sequence name or the tree background canvas
876    * (click on a node is handled in mouseClicked). The action is to create
877    * groups by partitioning the tree at the mouse position. Colours for the
878    * groups (and sequence names) are generated randomly.
879    * 
880    * @param e
881    */
882   @Override
883   public void mousePressed(MouseEvent e)
884   {
885     av.setCurrentTree(tree);
886
887     /*
888      * isPopupTrigger is set for mousePressed (Mac)
889      * or mouseReleased (Windows)
890      */
891     if (e.isPopupTrigger())
892     {
893       if (highlightNode != null)
894       {
895         chooseSubtreeColour();
896       }
897       return;
898     }
899
900     /*
901      * defer right-click handling on Windows to
902      * mouseClicked; note isRightMouseButton
903      * also matches Cmd-click on Mac which should do
904      * nothing here
905      */
906     if (SwingUtilities.isRightMouseButton(e))
907     {
908       return;
909     }
910
911     int x = e.getX();
912     int y = e.getY();
913
914     Object ob = findElement(x, y);
915
916     if (ob instanceof SequenceI)
917     {
918       treeSelectionChanged((Sequence) ob);
919       PaintRefresher.Refresh(tp,
920               getAssociatedPanel().av.getSequenceSetId());
921       repaint();
922       av.sendSelection();
923       return;
924     }
925     else if (!(ob instanceof SequenceNode))
926     {
927       // Find threshold
928       if (tree.getMaxHeight() != 0)
929       {
930         threshold = (float) (x - offx)
931                 / (float) (getWidth() - labelLength - (2 * offx));
932
933         List<SequenceNode> groups = tree.groupNodes(threshold);
934         setColor(tree.getTopNode(), Color.black);
935
936         AlignmentPanel[] aps = getAssociatedPanels();
937
938         // TODO push calls below into a single AlignViewportI method?
939         // see also AlignViewController.deleteGroups
940         for (int a = 0; a < aps.length; a++)
941         {
942           aps[a].av.setSelectionGroup(null);
943           aps[a].av.getAlignment().deleteAllGroups();
944           aps[a].av.clearSequenceColours();
945           if (aps[a].av.getCodingComplement() != null)
946           {
947             aps[a].av.getCodingComplement().setSelectionGroup(null);
948             aps[a].av.getCodingComplement().getAlignment()
949                     .deleteAllGroups();
950             aps[a].av.getCodingComplement().clearSequenceColours();
951           }
952           aps[a].av.setUpdateStructures(true);
953         }
954         colourGroups(groups);
955
956         /*
957          * clear partition (don't show vertical line) if
958          * it is to the right of all nodes
959          */
960         if (groups.isEmpty())
961         {
962           threshold = 0f;
963         }
964       }
965
966       PaintRefresher.Refresh(tp,
967               getAssociatedPanel().av.getSequenceSetId());
968       repaint();
969     }
970
971   }
972
973   void colourGroups(List<SequenceNode> groups)
974   {
975     AlignmentPanel[] aps = getAssociatedPanels();
976     for (int i = 0; i < groups.size(); i++)
977     {
978       Color col = new Color((int) (Math.random() * 255),
979               (int) (Math.random() * 255), (int) (Math.random() * 255));
980       setColor(groups.get(i), col.brighter());
981
982       Vector<SequenceNode> l = tree.findLeaves(groups.get(i));
983
984       Vector<SequenceI> sequences = new Vector<>();
985
986       for (int j = 0; j < l.size(); j++)
987       {
988         SequenceI s1 = (SequenceI) l.elementAt(j).element();
989
990         if (!sequences.contains(s1))
991         {
992           sequences.addElement(s1);
993         }
994       }
995
996       ColourSchemeI cs = null;
997       SequenceGroup _sg = new SequenceGroup(sequences, null, cs, true, true,
998               false, 0, av.getAlignment().getWidth() - 1);
999
1000       _sg.setName("JTreeGroup:" + _sg.hashCode());
1001       _sg.setIdColour(col);
1002
1003       for (int a = 0; a < aps.length; a++)
1004       {
1005         SequenceGroup sg = new SequenceGroup(_sg);
1006         AlignViewport viewport = aps[a].av;
1007
1008         // Propagate group colours in each view
1009         if (viewport.getGlobalColourScheme() != null)
1010         {
1011           cs = viewport.getGlobalColourScheme().getInstance(viewport, sg);
1012           sg.setColourScheme(cs);
1013           sg.getGroupColourScheme().setThreshold(
1014                   viewport.getResidueShading().getThreshold(),
1015                   viewport.isIgnoreGapsConsensus());
1016
1017           if (viewport.getResidueShading().conservationApplied())
1018           {
1019             Conservation c = new Conservation("Group",
1020                     sg.getSequences(null), sg.getStartRes(),
1021                     sg.getEndRes());
1022             c.calculate();
1023             c.verdict(false, viewport.getConsPercGaps());
1024             sg.cs.setConservation(c);
1025           }
1026         }
1027         // indicate that associated structure views will need an update
1028         viewport.setUpdateStructures(true);
1029         // propagate structure view update and sequence group to complement view
1030         viewport.addSequenceGroup(sg);
1031       }
1032     }
1033
1034     // notify the panel(s) to redo any group specific stuff
1035     // also updates structure views if necessary
1036     for (int a = 0; a < aps.length; a++)
1037     {
1038       aps[a].updateAnnotation();
1039       final AlignViewportI codingComplement = aps[a].av
1040               .getCodingComplement();
1041       if (codingComplement != null)
1042       {
1043         ((AlignViewport) codingComplement).getAlignPanel()
1044                 .updateAnnotation();
1045       }
1046     }
1047   }
1048
1049   /**
1050    * DOCUMENT ME!
1051    * 
1052    * @param state
1053    *          DOCUMENT ME!
1054    */
1055   public void setShowDistances(boolean state)
1056   {
1057     this.showDistances = state;
1058     repaint();
1059   }
1060
1061   /**
1062    * DOCUMENT ME!
1063    * 
1064    * @param state
1065    *          DOCUMENT ME!
1066    */
1067   public void setShowBootstrap(boolean state)
1068   {
1069     this.showBootstrap = state;
1070     repaint();
1071   }
1072
1073   /**
1074    * DOCUMENT ME!
1075    * 
1076    * @param state
1077    *          DOCUMENT ME!
1078    */
1079   public void setMarkPlaceholders(boolean state)
1080   {
1081     this.markPlaceholders = state;
1082     repaint();
1083   }
1084
1085   AlignmentPanel[] getAssociatedPanels()
1086   {
1087     if (applyToAllViews)
1088     {
1089       return PaintRefresher.getAssociatedPanels(av.getSequenceSetId());
1090     }
1091     else
1092     {
1093       return new AlignmentPanel[] { getAssociatedPanel() };
1094     }
1095   }
1096
1097   public AlignmentPanel getAssociatedPanel()
1098   {
1099     return ap;
1100   }
1101
1102   public void setAssociatedPanel(AlignmentPanel ap)
1103   {
1104     this.ap = ap;
1105   }
1106
1107   public AlignViewport getViewport()
1108   {
1109     return av;
1110   }
1111
1112   public void setViewport(AlignViewport av)
1113   {
1114     this.av = av;
1115   }
1116
1117   public float getThreshold()
1118   {
1119     return threshold;
1120   }
1121
1122   public void setThreshold(float threshold)
1123   {
1124     this.threshold = threshold;
1125   }
1126
1127   public boolean isApplyToAllViews()
1128   {
1129     return this.applyToAllViews;
1130   }
1131
1132   public void setApplyToAllViews(boolean applyToAllViews)
1133   {
1134     this.applyToAllViews = applyToAllViews;
1135   }
1136 }