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