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