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