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