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