JAL-2403 JAL-1483 changes to ScoreModelI hierarchy and signatures to
[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 jalview.analysis.Conservation;
24 import jalview.analysis.NJTree;
25 import jalview.api.AlignViewportI;
26 import jalview.datamodel.Sequence;
27 import jalview.datamodel.SequenceGroup;
28 import jalview.datamodel.SequenceI;
29 import jalview.datamodel.SequenceNode;
30 import jalview.schemes.ColourSchemeI;
31 import jalview.schemes.ColourSchemeProperty;
32 import jalview.schemes.UserColourScheme;
33 import jalview.structure.SelectionSource;
34 import jalview.util.Format;
35 import jalview.util.MappingUtils;
36 import jalview.util.MessageManager;
37
38 import java.awt.Color;
39 import java.awt.Dimension;
40 import java.awt.Font;
41 import java.awt.FontMetrics;
42 import java.awt.Graphics;
43 import java.awt.Graphics2D;
44 import java.awt.Point;
45 import java.awt.Rectangle;
46 import java.awt.RenderingHints;
47 import java.awt.event.MouseEvent;
48 import java.awt.event.MouseListener;
49 import java.awt.event.MouseMotionListener;
50 import java.awt.print.PageFormat;
51 import java.awt.print.Printable;
52 import java.awt.print.PrinterException;
53 import java.awt.print.PrinterJob;
54 import java.util.Enumeration;
55 import java.util.Hashtable;
56 import java.util.Vector;
57
58 import javax.swing.JColorChooser;
59 import javax.swing.JPanel;
60 import javax.swing.JScrollPane;
61 import javax.swing.SwingUtilities;
62 import javax.swing.ToolTipManager;
63
64 /**
65  * DOCUMENT ME!
66  * 
67  * @author $author$
68  * @version $Revision$
69  */
70 public class TreeCanvas extends JPanel implements MouseListener, Runnable,
71         Printable, MouseMotionListener, SelectionSource
72 {
73   /** DOCUMENT ME!! */
74   public static final String PLACEHOLDER = " * ";
75
76   NJTree tree;
77
78   JScrollPane scrollPane;
79
80   TreePanel tp;
81
82   AlignViewport av;
83
84   AlignmentPanel ap;
85
86   Font font;
87
88   FontMetrics fm;
89
90   boolean fitToWindow = true;
91
92   boolean showDistances = false;
93
94   boolean showBootstrap = false;
95
96   boolean markPlaceholders = false;
97
98   int offx = 20;
99
100   int offy;
101
102   float threshold;
103
104   String longestName;
105
106   int labelLength = -1;
107
108   Hashtable nameHash = new Hashtable();
109
110   Hashtable nodeHash = new Hashtable();
111
112   SequenceNode highlightNode;
113
114   boolean applyToAllViews = false;
115
116   /**
117    * Creates a new TreeCanvas object.
118    * 
119    * @param av
120    *          DOCUMENT ME!
121    * @param tree
122    *          DOCUMENT ME!
123    * @param scroller
124    *          DOCUMENT ME!
125    * @param label
126    *          DOCUMENT ME!
127    */
128   public TreeCanvas(TreePanel tp, AlignmentPanel ap, JScrollPane scroller)
129   {
130     this.tp = tp;
131     this.av = ap.av;
132     this.ap = ap;
133     font = av.getFont();
134     scrollPane = scroller;
135     addMouseListener(this);
136     addMouseMotionListener(this);
137     ToolTipManager.sharedInstance().registerComponent(this);
138   }
139
140   /**
141    * DOCUMENT ME!
142    * 
143    * @param sequence
144    *          DOCUMENT ME!
145    */
146   public void treeSelectionChanged(SequenceI sequence)
147   {
148     AlignmentPanel[] aps = getAssociatedPanels();
149
150     for (int a = 0; a < aps.length; a++)
151     {
152       SequenceGroup selected = aps[a].av.getSelectionGroup();
153
154       if (selected == null)
155       {
156         selected = new SequenceGroup();
157         aps[a].av.setSelectionGroup(selected);
158       }
159
160       selected.setEndRes(aps[a].av.getAlignment().getWidth() - 1);
161       selected.addOrRemove(sequence, true);
162     }
163   }
164
165   /**
166    * DOCUMENT ME!
167    * 
168    * @param tree
169    *          DOCUMENT ME!
170    */
171   public void setTree(NJTree tree)
172   {
173     this.tree = tree;
174     tree.findHeight(tree.getTopNode());
175
176     // Now have to calculate longest name based on the leaves
177     Vector<SequenceNode> leaves = tree.findLeaves(tree.getTopNode());
178     boolean has_placeholders = false;
179     longestName = "";
180
181     for (int i = 0; i < leaves.size(); i++)
182     {
183       SequenceNode lf = leaves.elementAt(i);
184
185       if (lf.isPlaceholder())
186       {
187         has_placeholders = true;
188       }
189
190       if (longestName.length() < ((Sequence) lf.element()).getName()
191               .length())
192       {
193         longestName = TreeCanvas.PLACEHOLDER
194                 + ((Sequence) lf.element()).getName();
195       }
196     }
197
198     setMarkPlaceholders(has_placeholders);
199   }
200
201   /**
202    * DOCUMENT ME!
203    * 
204    * @param g
205    *          DOCUMENT ME!
206    * @param node
207    *          DOCUMENT ME!
208    * @param chunk
209    *          DOCUMENT ME!
210    * @param wscale
211    *          DOCUMENT ME!
212    * @param width
213    *          DOCUMENT ME!
214    * @param offx
215    *          DOCUMENT ME!
216    * @param offy
217    *          DOCUMENT ME!
218    */
219   public void drawNode(Graphics g, SequenceNode node, float chunk,
220           double wscale, int width, int offx, int offy)
221   {
222     if (node == null)
223     {
224       return;
225     }
226
227     if ((node.left() == null) && (node.right() == null))
228     {
229       // Drawing leaf node
230       double height = node.height;
231       double dist = node.dist;
232
233       int xstart = (int) ((height - dist) * wscale) + offx;
234       int xend = (int) (height * wscale) + offx;
235
236       int ypos = (int) (node.ycount * chunk) + offy;
237
238       if (node.element() instanceof SequenceI)
239       {
240         SequenceI seq = (SequenceI) node.element();
241
242         if (av.getSequenceColour(seq) == Color.white)
243         {
244           g.setColor(Color.black);
245         }
246         else
247         {
248           g.setColor(av.getSequenceColour(seq).darker());
249         }
250       }
251       else
252       {
253         g.setColor(Color.black);
254       }
255
256       // Draw horizontal line
257       g.drawLine(xstart, ypos, xend, ypos);
258
259       String nodeLabel = "";
260
261       if (showDistances && (node.dist > 0))
262       {
263         nodeLabel = new Format("%-.2f").form(node.dist);
264       }
265
266       if (showBootstrap && node.bootstrap > -1)
267       {
268         if (showDistances)
269         {
270           nodeLabel = nodeLabel + " : ";
271         }
272
273         nodeLabel = nodeLabel + String.valueOf(node.bootstrap);
274       }
275
276       if (!nodeLabel.equals(""))
277       {
278         g.drawString(nodeLabel, xstart + 2, ypos - 2);
279       }
280
281       String name = (markPlaceholders && node.isPlaceholder()) ? (PLACEHOLDER + node
282               .getName()) : 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 = (int) (((SequenceNode) node.left()).ycount * chunk)
335               + offy;
336       int yend = (int) (((SequenceNode) node.right()).ycount * chunk)
337               + offy;
338
339       Rectangle pos = new Rectangle(xend - 2, ypos - 2, 5, 5);
340       nodeHash.put(node, pos);
341
342       g.drawLine((int) (height * wscale) + offx, ystart,
343               (int) (height * wscale) + offx, yend);
344
345       String nodeLabel = "";
346
347       if (showDistances && (node.dist > 0))
348       {
349         nodeLabel = new Format("%-.2f").form(node.dist);
350       }
351
352       if (showBootstrap && node.bootstrap > -1)
353       {
354         if (showDistances)
355         {
356           nodeLabel = nodeLabel + " : ";
357         }
358
359         nodeLabel = nodeLabel + String.valueOf(node.bootstrap);
360       }
361
362       if (!nodeLabel.equals(""))
363       {
364         g.drawString(nodeLabel, xstart + 2, ypos - 2);
365       }
366     }
367   }
368
369   /**
370    * DOCUMENT ME!
371    * 
372    * @param x
373    *          DOCUMENT ME!
374    * @param y
375    *          DOCUMENT ME!
376    * 
377    * @return DOCUMENT ME!
378    */
379   public Object findElement(int x, int y)
380   {
381     Enumeration keys = nameHash.keys();
382
383     while (keys.hasMoreElements())
384     {
385       Object ob = keys.nextElement();
386       Rectangle rect = (Rectangle) nameHash.get(ob);
387
388       if ((x >= rect.x) && (x <= (rect.x + rect.width)) && (y >= rect.y)
389               && (y <= (rect.y + rect.height)))
390       {
391         return ob;
392       }
393     }
394
395     keys = nodeHash.keys();
396
397     while (keys.hasMoreElements())
398     {
399       Object ob = keys.nextElement();
400       Rectangle rect = (Rectangle) nodeHash.get(ob);
401
402       if ((x >= rect.x) && (x <= (rect.x + rect.width)) && (y >= rect.y)
403               && (y <= (rect.y + rect.height)))
404       {
405         return ob;
406       }
407     }
408
409     return null;
410   }
411
412   /**
413    * DOCUMENT ME!
414    * 
415    * @param pickBox
416    *          DOCUMENT ME!
417    */
418   public void pickNodes(Rectangle pickBox)
419   {
420     int width = getWidth();
421     int height = getHeight();
422
423     SequenceNode top = tree.getTopNode();
424
425     double wscale = ((width * .8) - (offx * 2))
426             / tree.getMaxHeight();
427
428     if (top.count == 0)
429     {
430       top.count = ((SequenceNode) top.left()).count
431               + ((SequenceNode) top.right()).count;
432     }
433
434     float chunk = (float) (height - (offy)) / top.count;
435
436     pickNode(pickBox, top, chunk, wscale, width, offx, offy);
437   }
438
439   /**
440    * DOCUMENT ME!
441    * 
442    * @param pickBox
443    *          DOCUMENT ME!
444    * @param node
445    *          DOCUMENT ME!
446    * @param chunk
447    *          DOCUMENT ME!
448    * @param wscale
449    *          DOCUMENT ME!
450    * @param width
451    *          DOCUMENT ME!
452    * @param offx
453    *          DOCUMENT ME!
454    * @param offy
455    *          DOCUMENT ME!
456    */
457   public void pickNode(Rectangle pickBox, SequenceNode node, float chunk,
458           double wscale, int width, int offx, int offy)
459   {
460     if (node == null)
461     {
462       return;
463     }
464
465     if ((node.left() == null) && (node.right() == null))
466     {
467       double height = node.height;
468       double dist = node.dist;
469
470       int xstart = (int) ((height - dist) * wscale) + offx;
471       int xend = (int) (height * wscale) + offx;
472
473       int ypos = (int) (node.ycount * chunk) + offy;
474
475       if (pickBox.contains(new Point(xend, ypos)))
476       {
477         if (node.element() instanceof SequenceI)
478         {
479           SequenceI seq = (SequenceI) node.element();
480           SequenceGroup sg = av.getSelectionGroup();
481
482           if (sg != null)
483           {
484             sg.addOrRemove(seq, true);
485           }
486         }
487       }
488     }
489     else
490     {
491       pickNode(pickBox, (SequenceNode) node.left(), chunk, wscale, width,
492               offx, offy);
493       pickNode(pickBox, (SequenceNode) node.right(), chunk, wscale, width,
494               offx, offy);
495     }
496   }
497
498   /**
499    * DOCUMENT ME!
500    * 
501    * @param node
502    *          DOCUMENT ME!
503    * @param c
504    *          DOCUMENT ME!
505    */
506   public void setColor(SequenceNode node, Color c)
507   {
508     if (node == null)
509     {
510       return;
511     }
512
513     if ((node.left() == null) && (node.right() == null)) // TODO: internal node
514     {
515       node.color = c;
516
517       if (node.element() instanceof SequenceI)
518       {
519         AlignmentPanel[] aps = getAssociatedPanels();
520         if (aps != null)
521         {
522           for (int a = 0; a < aps.length; a++)
523           {
524             final SequenceI seq = (SequenceI) node.element();
525             aps[a].av.setSequenceColour(seq, c);
526           }
527         }
528       }
529     }
530     else
531     {
532       node.color = c;
533       setColor((SequenceNode) node.left(), c);
534       setColor((SequenceNode) node.right(), c);
535     }
536   }
537
538   /**
539    * DOCUMENT ME!
540    */
541   void startPrinting()
542   {
543     Thread thread = new Thread(this);
544     thread.start();
545   }
546
547   // put printing in a thread to avoid painting problems
548   @Override
549   public void run()
550   {
551     PrinterJob printJob = PrinterJob.getPrinterJob();
552     PageFormat pf = printJob.pageDialog(printJob.defaultPage());
553
554     printJob.setPrintable(this, pf);
555
556     if (printJob.printDialog())
557     {
558       try
559       {
560         printJob.print();
561       } catch (Exception PrintException)
562       {
563         PrintException.printStackTrace();
564       }
565     }
566   }
567
568   /**
569    * DOCUMENT ME!
570    * 
571    * @param pg
572    *          DOCUMENT ME!
573    * @param pf
574    *          DOCUMENT ME!
575    * @param pi
576    *          DOCUMENT ME!
577    * 
578    * @return DOCUMENT ME!
579    * 
580    * @throws PrinterException
581    *           DOCUMENT ME!
582    */
583   @Override
584   public int print(Graphics pg, PageFormat pf, int pi)
585           throws PrinterException
586   {
587     pg.setFont(font);
588     pg.translate((int) pf.getImageableX(), (int) pf.getImageableY());
589
590     int pwidth = (int) pf.getImageableWidth();
591     int pheight = (int) pf.getImageableHeight();
592
593     int noPages = getHeight() / pheight;
594
595     if (pi > noPages)
596     {
597       return Printable.NO_SUCH_PAGE;
598     }
599
600     if (pwidth > getWidth())
601     {
602       pwidth = getWidth();
603     }
604
605     if (fitToWindow)
606     {
607       if (pheight > getHeight())
608       {
609         pheight = getHeight();
610       }
611
612       noPages = 0;
613     }
614     else
615     {
616       FontMetrics fm = pg.getFontMetrics(font);
617       int height = fm.getHeight() * nameHash.size();
618       pg.translate(0, -pi * pheight);
619       pg.setClip(0, pi * pheight, pwidth, (pi * pheight) + pheight);
620
621       // translate number of pages,
622       // height is screen size as this is the
623       // non overlapping text size
624       pheight = height;
625     }
626
627     draw(pg, pwidth, pheight);
628
629     return Printable.PAGE_EXISTS;
630   }
631
632   /**
633    * DOCUMENT ME!
634    * 
635    * @param g
636    *          DOCUMENT ME!
637    */
638   @Override
639   public void paintComponent(Graphics g)
640   {
641     super.paintComponent(g);
642     g.setFont(font);
643
644     if (tree == null)
645     {
646       g.drawString(MessageManager.getString("label.calculating_tree")
647               + "....", 20, getHeight() / 2);
648     }
649     else
650     {
651       fm = g.getFontMetrics(font);
652
653       if (nameHash.size() == 0)
654       {
655         repaint();
656       }
657
658       if (fitToWindow
659               || (!fitToWindow && (scrollPane.getHeight() > ((fm
660                       .getHeight() * nameHash.size()) + offy))))
661       {
662         draw(g, scrollPane.getWidth(), scrollPane.getHeight());
663         setPreferredSize(null);
664       }
665       else
666       {
667         setPreferredSize(new Dimension(scrollPane.getWidth(),
668                 fm.getHeight() * nameHash.size()));
669         draw(g, scrollPane.getWidth(), fm.getHeight() * nameHash.size());
670       }
671
672       scrollPane.revalidate();
673     }
674   }
675
676   /**
677    * DOCUMENT ME!
678    * 
679    * @param fontSize
680    *          DOCUMENT ME!
681    */
682   @Override
683   public void setFont(Font font)
684   {
685     this.font = font;
686     repaint();
687   }
688
689   /**
690    * DOCUMENT ME!
691    * 
692    * @param g1
693    *          DOCUMENT ME!
694    * @param width
695    *          DOCUMENT ME!
696    * @param height
697    *          DOCUMENT ME!
698    */
699   public void draw(Graphics g1, int width, int height)
700   {
701     Graphics2D g2 = (Graphics2D) g1;
702     g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
703             RenderingHints.VALUE_ANTIALIAS_ON);
704     g2.setColor(Color.white);
705     g2.fillRect(0, 0, width, height);
706     g2.setFont(font);
707
708     if (longestName == null || tree == null)
709     {
710       g2.drawString("Calculating tree.", 20, 20);
711     }
712     offy = font.getSize() + 10;
713
714     fm = g2.getFontMetrics(font);
715
716     labelLength = fm.stringWidth(longestName) + 20; // 20 allows for scrollbar
717
718     double wscale = (width - labelLength - (offx * 2))
719             / tree.getMaxHeight();
720
721     SequenceNode top = tree.getTopNode();
722
723     if (top.count == 0)
724     {
725       top.count = ((SequenceNode) top.left()).count
726               + ((SequenceNode) top.right()).count;
727     }
728
729     float chunk = (float) (height - (offy)) / top.count;
730
731     drawNode(g2, tree.getTopNode(), chunk, wscale, width, offx, offy);
732
733     if (threshold != 0)
734     {
735       if (av.getCurrentTree() == tree)
736       {
737         g2.setColor(Color.red);
738       }
739       else
740       {
741         g2.setColor(Color.gray);
742       }
743
744       int x = (int) ((threshold * (getWidth() - labelLength - (2 * offx))) + offx);
745
746       g2.drawLine(x, 0, x, getHeight());
747     }
748   }
749
750   /**
751    * Empty method to satisfy the MouseListener interface
752    * 
753    * @param e
754    */
755   @Override
756   public void mouseReleased(MouseEvent e)
757   {
758     /*
759      * isPopupTrigger is set on mouseReleased on Windows
760      */
761     if (e.isPopupTrigger())
762     {
763       chooseSubtreeColour();
764       e.consume(); // prevent mouseClicked happening
765     }
766   }
767
768   /**
769    * Empty method to satisfy the MouseListener interface
770    * 
771    * @param e
772    */
773   @Override
774   public void mouseEntered(MouseEvent e)
775   {
776   }
777
778   /**
779    * Empty method to satisfy the MouseListener interface
780    * 
781    * @param e
782    */
783   @Override
784   public void mouseExited(MouseEvent e)
785   {
786   }
787
788   /**
789    * Handles a mouse click on a tree node (clicks elsewhere are handled in
790    * mousePressed). Click selects the sub-tree, double-click swaps leaf nodes
791    * order, right-click opens a dialogue to choose colour for the sub-tree.
792    * 
793    * @param e
794    */
795   @Override
796   public void mouseClicked(MouseEvent evt)
797   {
798     if (highlightNode == null)
799     {
800       return;
801     }
802
803     if (evt.getClickCount() > 1)
804     {
805       tree.swapNodes(highlightNode);
806       tree.reCount(tree.getTopNode());
807       tree.findHeight(tree.getTopNode());
808     }
809     else
810     {
811       Vector<SequenceNode> leaves = tree.findLeaves(highlightNode);
812
813       for (int i = 0; i < leaves.size(); i++)
814       {
815         SequenceI seq = (SequenceI) leaves.elementAt(i).element();
816         treeSelectionChanged(seq);
817       }
818       av.sendSelection();
819     }
820
821     PaintRefresher.Refresh(tp, av.getSequenceSetId());
822     repaint();
823   }
824
825   /**
826    * Offer the user the option to choose a colour for the highlighted node and
827    * its children; this colour is also applied to the corresponding sequence ids
828    * in the alignment
829    */
830   void chooseSubtreeColour()
831   {
832     Color col = JColorChooser.showDialog(this,
833             MessageManager.getString("label.select_subtree_colour"),
834             highlightNode.color);
835     if (col != null)
836     {
837       setColor(highlightNode, col);
838       PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
839       repaint();
840     }
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("<html>"
854               + 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, ap.av.getSequenceSetId());
920       repaint();
921       av.sendSelection();
922       return;
923     }
924     else if (!(ob instanceof SequenceNode))
925     {
926       // Find threshold
927       if (tree.getMaxHeight() != 0)
928       {
929         threshold = (float) (x - offx)
930                 / (float) (getWidth() - labelLength - (2 * offx));
931
932         tree.getGroups().removeAllElements();
933         tree.groupNodes(tree.getTopNode(), 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         }
953         colourGroups();
954       }
955
956       PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
957       repaint();
958     }
959
960   }
961
962   void colourGroups()
963   {
964     AlignmentPanel[] aps = getAssociatedPanels();
965     for (int i = 0; i < tree.getGroups().size(); i++)
966     {
967       Color col = new Color((int) (Math.random() * 255),
968               (int) (Math.random() * 255), (int) (Math.random() * 255));
969       setColor(tree.getGroups().elementAt(i), col.brighter());
970
971       Vector<SequenceNode> l = tree.findLeaves(tree.getGroups()
972               .elementAt(i));
973
974       Vector<SequenceI> sequences = new Vector<SequenceI>();
975
976       for (int j = 0; j < l.size(); j++)
977       {
978         SequenceI s1 = (SequenceI) l.elementAt(j).element();
979
980         if (!sequences.contains(s1))
981         {
982           sequences.addElement(s1);
983         }
984       }
985
986       ColourSchemeI cs = null;
987       SequenceGroup sg = new SequenceGroup(sequences, null, cs, true, true,
988               false, 0, av.getAlignment().getWidth() - 1);
989
990       if (av.getGlobalColourScheme() != null)
991       {
992         if (av.getGlobalColourScheme() instanceof UserColourScheme)
993         {
994           cs = new UserColourScheme(
995                   ((UserColourScheme) av.getGlobalColourScheme())
996                           .getColours());
997
998         }
999         else
1000         {
1001           cs = ColourSchemeProperty.getColourScheme(sg,
1002                   ColourSchemeProperty.getColourName(av
1003                           .getGlobalColourScheme()));
1004         }
1005         // cs is null if shading is an annotationColourGradient
1006         // if (cs != null)
1007         // {
1008         // cs.setThreshold(av.getViewportColourScheme().getThreshold(),
1009         // av.isIgnoreGapsConsensus());
1010         // }
1011       }
1012       sg.setColourScheme(cs);
1013       sg.getGroupColourScheme().setThreshold(
1014               av.getResidueShading().getThreshold(),
1015               av.isIgnoreGapsConsensus());
1016       // sg.recalcConservation();
1017       sg.setName("JTreeGroup:" + sg.hashCode());
1018       sg.setIdColour(col);
1019
1020       for (int a = 0; a < aps.length; a++)
1021       {
1022         if (aps[a].av.getGlobalColourScheme() != null
1023                 && aps[a].av.getResidueShading()
1024                         .conservationApplied())
1025         {
1026           Conservation c = new Conservation("Group", sg.getSequences(null),
1027                   sg.getStartRes(), sg.getEndRes());
1028           c.calculate();
1029           c.verdict(false, aps[a].av.getConsPercGaps());
1030           sg.cs.setConservation(c);
1031         }
1032
1033         aps[a].av.getAlignment().addGroup(new SequenceGroup(sg));
1034         // TODO can we push all of the below into AlignViewportI?
1035         final AlignViewportI codingComplement = aps[a].av
1036                 .getCodingComplement();
1037         if (codingComplement != null)
1038         {
1039           SequenceGroup mappedGroup = MappingUtils.mapSequenceGroup(sg, av,
1040                   codingComplement);
1041           if (mappedGroup.getSequences().size() > 0)
1042           {
1043             codingComplement.getAlignment().addGroup(mappedGroup);
1044             for (SequenceI seq : mappedGroup.getSequences())
1045             {
1046               codingComplement.setSequenceColour(seq, col.brighter());
1047             }
1048           }
1049         }
1050       }
1051     }
1052
1053     // notify the panel(s) to redo any group specific stuff.
1054     for (int a = 0; a < aps.length; a++)
1055     {
1056       aps[a].updateAnnotation();
1057       // TODO: JAL-868 - need to ensure view colour change message is broadcast
1058       // to any Jmols listening in
1059       final AlignViewportI codingComplement = aps[a].av
1060               .getCodingComplement();
1061       if (codingComplement != null)
1062       {
1063         ((AlignViewport) codingComplement).getAlignPanel()
1064                 .updateAnnotation();
1065       }
1066     }
1067   }
1068
1069   /**
1070    * DOCUMENT ME!
1071    * 
1072    * @param state
1073    *          DOCUMENT ME!
1074    */
1075   public void setShowDistances(boolean state)
1076   {
1077     this.showDistances = state;
1078     repaint();
1079   }
1080
1081   /**
1082    * DOCUMENT ME!
1083    * 
1084    * @param state
1085    *          DOCUMENT ME!
1086    */
1087   public void setShowBootstrap(boolean state)
1088   {
1089     this.showBootstrap = state;
1090     repaint();
1091   }
1092
1093   /**
1094    * DOCUMENT ME!
1095    * 
1096    * @param state
1097    *          DOCUMENT ME!
1098    */
1099   public void setMarkPlaceholders(boolean state)
1100   {
1101     this.markPlaceholders = state;
1102     repaint();
1103   }
1104
1105   AlignmentPanel[] getAssociatedPanels()
1106   {
1107     if (applyToAllViews)
1108     {
1109       return PaintRefresher.getAssociatedPanels(av.getSequenceSetId());
1110     }
1111     else
1112     {
1113       return new AlignmentPanel[] { ap };
1114     }
1115   }
1116 }