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