Clover icon

Coverage Report

  1. Project Clover database Thu Nov 27 2025 17:07:57 GMT
  2. Package jalview.gui

File TreeCanvas.java

 

Coverage histogram

../../img/srcFileCovDistChart2.png
54% of files have more coverage

Code metrics

324
642
53
1
2,109
1,427
273
0.43
12.11
53
5.15

Classes

Class Line # Actions
TreeCanvas 86 642 273
0.184494618.4%
 

Contributing tests

This file is covered by 10 tests. .

Source view

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.BasicStroke;
24    import java.awt.Color;
25    import java.awt.Dimension;
26    import java.awt.Font;
27    import java.awt.FontMetrics;
28    import java.awt.Graphics;
29    import java.awt.Graphics2D;
30    import java.awt.Point;
31    import java.awt.Rectangle;
32    import java.awt.RenderingHints;
33    import java.awt.event.MouseEvent;
34    import java.awt.event.MouseListener;
35    import java.awt.event.MouseMotionListener;
36    import java.awt.print.PageFormat;
37    import java.awt.print.Printable;
38    import java.awt.print.PrinterException;
39    import java.awt.print.PrinterJob;
40    import java.util.ArrayList;
41    import java.util.Arrays;
42    import java.util.BitSet;
43    import java.util.HashMap;
44    import java.util.HashSet;
45    import java.util.Hashtable;
46    import java.util.LinkedHashSet;
47    import java.util.List;
48    import java.util.Map;
49    import java.util.Map.Entry;
50    import java.util.Set;
51    import java.util.Vector;
52   
53    import javax.swing.JPanel;
54    import javax.swing.JScrollPane;
55    import javax.swing.SwingUtilities;
56    import javax.swing.ToolTipManager;
57   
58    import jalview.analysis.AlignmentUtils;
59    import jalview.analysis.Conservation;
60    import jalview.analysis.TreeModel;
61    import jalview.api.AlignViewportI;
62    import jalview.bin.Console;
63    import jalview.datamodel.AlignmentAnnotation;
64    import jalview.datamodel.BinaryNode;
65    import jalview.datamodel.ColumnSelection;
66    import jalview.datamodel.ContactMatrixI;
67    import jalview.datamodel.HiddenColumns;
68    import jalview.datamodel.Sequence;
69    import jalview.datamodel.SequenceGroup;
70    import jalview.datamodel.SequenceI;
71    import jalview.datamodel.SequenceNode;
72    import jalview.gui.JalviewColourChooser.ColourChooserListener;
73    import jalview.schemes.ColourSchemeI;
74    import jalview.structure.SelectionSource;
75    import jalview.util.ColorUtils;
76    import jalview.util.Format;
77    import jalview.util.MessageManager;
78    import jalview.ws.datamodel.MappableContactMatrixI;
79   
80    /**
81    * DOCUMENT ME!
82    *
83    * @author $author$
84    * @version $Revision$
85    */
 
86    public class TreeCanvas extends JPanel implements MouseListener, Runnable,
87    Printable, MouseMotionListener, SelectionSource
88    {
89    /** DOCUMENT ME!! */
90    public static final String PLACEHOLDER = " * ";
91   
92    private static final int DASHED_LINE_Y_OFFSET = 6;
93   
94    TreeModel tree;
95   
96    JScrollPane scrollPane;
97   
98    TreePanel tp;
99   
100    private AlignViewport av;
101   
102    private AlignmentPanel ap;
103   
104    Font font;
105   
106    FontMetrics fm;
107   
108    boolean fitToWindow = true;
109   
110    boolean showDistances = false;
111   
112    boolean showStructureProviderLabels = false;
113   
114    boolean showStructureProviderColouredLines = false;
115   
116    Map<String, Boolean> structureProviderColouredLineToggleState = new HashMap<String, Boolean>();
117   
118    boolean showBootstrap = false;
119   
120    boolean markPlaceholders = false;
121   
122    int offx = 20;
123   
124    int offy;
125   
126    private float threshold;
127   
128    int labelLengthThreshold = 4;
129   
130    String longestName;
131   
132    int labelLength = -1;
133   
134    private static final int COLOR_TRANSPARENCY_FOR_GROUP = 198; // approx 25% transparency
135    private static final int COLOR_TRANSPARENCY_FOR_COLOURED_LINES = 80;
136   
137    // Variable to store index for tree group box dimensions
138    private static final int TREE_GROUP_DIM_MIN_X_INDEX = 0;
139    private static final int TREE_GROUP_DIM_MIN_Y_INDEX = 1;
140    private static final int TREE_GROUP_DIM_MAX_X_INDEX = 2;
141    private static final int TREE_GROUP_DIM_MAX_Y_INDEX = 3;
142   
143    /**
144    * TODO - these rectangle-hash lookups should be optimised for big trees...
145    */
146    Map<BinaryNode, Rectangle> nameHash = new Hashtable<>();
147   
148    Map<BinaryNode, Rectangle> nodeHash = new Hashtable<>();
149   
150    BinaryNode highlightNode;
151   
152    boolean applyToAllViews = false;
153   
154   
155    //Map to store label positions (BinaryNode -> List of bounding rectangles for the label)
156    //private Map<BinaryNode, List<Rectangle>> labelBoundsMap = new HashMap<>();
157   
158   
159   
160    /**
161    * Creates a new TreeCanvas object.
162    *
163    * @param av
164    * DOCUMENT ME!
165    * @param tree
166    * DOCUMENT ME!
167    * @param scroller
168    * DOCUMENT ME!
169    * @param label
170    * DOCUMENT ME!
171    */
 
172  18 toggle public TreeCanvas(TreePanel tp, AlignmentPanel ap, JScrollPane scroller)
173    {
174  18 this.tp = tp;
175  18 this.av = ap.av;
176  18 this.setAssociatedPanel(ap);
177  18 font = av.getFont();
178  18 scrollPane = scroller;
179  18 addMouseListener(this);
180  18 addMouseMotionListener(this);
181   
182  18 ToolTipManager.sharedInstance().registerComponent(this);
183    }
184   
 
185  0 toggle public void clearSelectedLeaves()
186    {
187  0 Vector<BinaryNode> leaves = tp.getTree()
188    .findLeaves(tp.getTree().getTopNode());
189  0 if (tp.isColumnWise())
190    {
191  0 markColumnsFor(getAssociatedPanels(), leaves, Color.white, true);
192    }
193    else
194    {
195  0 for (AlignmentPanel ap : getAssociatedPanels())
196    {
197  0 SequenceGroup selected = ap.av.getSelectionGroup();
198  0 if (selected != null)
199    {
200    {
201  0 for (int i = 0; i < leaves.size(); i++)
202    {
203  0 SequenceI seq = (SequenceI) leaves.elementAt(i).element();
204  0 if (selected.contains(seq))
205    {
206  0 selected.addOrRemove(seq, false);
207    }
208   
209   
210  0 if (leaves.elementAt(i).hasAlignmentAnnotation())
211    {
212  0 AlignmentAnnotation annot = leaves.elementAt(i).getAlignmentAnnotation();
213   
214  0 if (selected.containsAnnotation(annot))
215    {
216  0 selected.addOrRemoveAnnotation(annot);
217    }
218    }
219    }
220  0 selected.recalcConservation();
221    }
222    }
223  0 ap.av.sendSelection();
224    }
225    }
226  0 PaintRefresher.Refresh(tp, av.getSequenceSetId());
227  0 repaint();
228    }
229   
230    /**
231    * DOCUMENT ME!
232    *
233    * @param sequence
234    * DOCUMENT ME!
235    */
 
236  0 toggle public void treeSelectionChanged(SequenceI sequence, AlignmentAnnotation alignmentAnnotation)
237    {
238  0 boolean annotationSelected = false;
239  0 AlignmentPanel[] aps = getAssociatedPanels();
240   
241  0 for (int a = 0; a < aps.length; a++)
242    {
243  0 SequenceGroup selected = aps[a].av.getSelectionGroup();
244   
245  0 if (selected == null)
246    {
247  0 selected = new SequenceGroup();
248  0 aps[a].av.setSelectionGroup(selected);
249    }
250   
251  0 if(alignmentAnnotation!=null) {
252  0 annotationSelected = selected.addOrRemoveAnnotation(alignmentAnnotation);
253    }
254   
255  0 selected.setEndRes(aps[a].av.getAlignment().getWidth() - 1);
256   
257  0 if(annotationSelected) {
258  0 selected.addSequence(sequence, true);
259    }
260    else {
261  0 selected.addOrRemove(sequence, true);
262    }
263    }
264    }
265   
266    /**
267    * DOCUMENT ME!
268    *
269    * @param tree
270    * DOCUMENT ME!
271    */
 
272  18 toggle public void setTree(TreeModel tree)
273    {
274  18 this.tree = tree;
275  18 tree.findHeight(tree.getTopNode());
276   
277    // Now have to calculate longest name based on the leaves
278  18 Vector<BinaryNode> leaves = tree.findLeaves(tree.getTopNode());
279  18 boolean has_placeholders = false;
280  18 longestName = "";
281   
282  18 AlignmentAnnotation aa = tp.getAssocAnnotation();
283  18 ContactMatrixI cm = (aa != null) ? av.getContactMatrix(aa) : null;
284  18 if (cm != null && cm.hasCutHeight())
285    {
286  0 threshold = (float) cm.getCutHeight();
287    }
288   
289  270 for (int i = 0; i < leaves.size(); i++)
290    {
291  252 BinaryNode lf = leaves.elementAt(i);
292   
293  252 if (lf instanceof SequenceNode && ((SequenceNode) lf).isPlaceholder())
294    {
295  36 has_placeholders = true;
296    }
297   
298  252 if (longestName.length() < lf.getDisplayName().length())
299    {
300  29 longestName = TreeCanvas.PLACEHOLDER + lf.getDisplayName();
301    }
302  252 if (tp.isColumnWise() && cm != null)
303    {
304    // get color from group colours, if they are set for the matrix
305  0 try
306    {
307  0 Color col = cm.getGroupColorForPosition(parseColumnNode(lf));
308  0 setColor(lf, col.brighter());
309    } catch (NumberFormatException ex)
310    {
311    }
312  0 ;
313    }
314    }
315   
316  18 setMarkPlaceholders(has_placeholders);
317    }
318   
319    /**
320    * DOCUMENT ME!
321    *
322    * @param g
323    * DOCUMENT ME!
324    * @param node
325    * DOCUMENT ME!
326    * @param chunk
327    * DOCUMENT ME!
328    * @param wscale
329    * DOCUMENT ME!
330    * @param width
331    * DOCUMENT ME!
332    * @param offx
333    * DOCUMENT ME!
334    * @param offy
335    * DOCUMENT ME!
336    */
 
337  0 toggle public void drawNode(Graphics g, BinaryNode node, double chunk,
338    double wscale, int width, int offx, int offy)
339    {
340   
341  0 if (node == null)
342    {
343  0 return;
344    }
345   
346  0 SequenceGroup colourGroup = null;
347  0 Vector<BinaryNode> leaves = tree.findLeaves(node);
348  0 gatherLabelsTo(node, leaves);
349   
350  0 if ((node.left() == null) && (node.right() == null))
351    {
352  0 Color annotationColor = Color.WHITE;
353    // Drawing leaf node
354  0 double height = node.height;
355  0 double dist = node.dist;
356   
357  0 int xstart = (int) ((height - dist) * wscale) + offx;
358  0 int xend = (int) (height * wscale) + offx;
359   
360  0 int ypos = (int) (node.ycount * chunk) + offy;
361   
362  0 if (node.element() instanceof SequenceI)
363    {
364   
365  0 SequenceI seq = (SequenceI) node.element();
366   
367  0 SequenceGroup[] groupsWithSequence = null;
368   
369  0 if(ap.getAlignment() != null)
370    {
371  0 groupsWithSequence = ap.getAlignment()
372    .findAllGroups(seq);
373    }
374   
375    // JAL-4537
376  0 if (av.getSequenceColour(seq).equals(Color.white))
377    {
378  0 g.setColor(Color.black);
379    }
380  0 else if (node.hasLabel() && node.hasAlignmentAnnotation())
381    {
382  0 if(groupsWithSequence != null)
383    {
384  0 for (SequenceGroup group : groupsWithSequence)
385    {
386   
387  0 if (group.getAnnotationsFromTree()
388    .contains(node.getAlignmentAnnotation())
389    && group.getName().startsWith("JTreeGroup:"))
390    {
391   
392  0 colourGroup = group;
393   
394    }
395   
396    }
397    }
398   
399  0 annotationColor = av
400    .getAnnotationColour(node.getAlignmentAnnotation());
401  0 if (annotationColor == Color.white)
402    {
403  0 annotationColor = av.getSequenceColour(seq);
404    }
405  0 g.setColor(annotationColor.darker());
406   
407  0 if (showStructureProviderColouredLines)
408    {
409  0 g.setColor(Color.black);
410    }
411    }
412    else
413    {
414  0 if (node.hasLabel())
415    {
416  0 if(groupsWithSequence != null)
417    {
418  0 for (SequenceGroup group : groupsWithSequence)
419    {
420   
421  0 if (group.getName().startsWith("JTreeGroup:"))
422    {
423   
424  0 colourGroup = group;
425   
426    }
427   
428    }
429    }
430    }
431  0 g.setColor(av.getSequenceColour(seq).darker());
432    }
433    }
434   
435    else
436    {
437  0 g.setColor(Color.black);
438    }
439   
440  0 if (!(node.hasLabel() && showStructureProviderColouredLines))
441    {
442    // horizontal line
443  0 g.drawLine(xstart, ypos, xend, ypos);
444    }
445   
446  0 String nodeLabel = "";
447   
448  0 if (showDistances && (node.dist > 0))
449    {
450  0 nodeLabel = new Format("%g").form(node.dist);
451    }
452   
453  0 if (showBootstrap && node.bootstrap > -1)
454    {
455  0 if (showDistances)
456    {
457  0 nodeLabel = nodeLabel + " : ";
458    }
459   
460  0 nodeLabel = nodeLabel + String.valueOf(node.bootstrap);
461    }
462   
463  0 if (node.hasLabel() && showStructureProviderColouredLines)
464    {
465   
466  0 drawLinesAndLabelsForSecondaryStructureProvider(g, node, xstart,
467    xend, ypos, nodeLabel);
468   
469    }
470   
471  0 else if (!nodeLabel.equals(""))
472    {
473  0 g.drawString(nodeLabel, xstart + 2, ypos - 2);
474    }
475   
476  0 String name = (markPlaceholders && ((node instanceof SequenceNode
477    && ((SequenceNode) node).isPlaceholder())))
478    ? (PLACEHOLDER + node.getDisplayName())
479    : node.getDisplayName();
480   
481  0 int charWidth = fm.stringWidth(name) + 3;
482  0 int charHeight = font.getSize();
483   
484  0 Rectangle rect = new Rectangle(xend + 10, ypos - charHeight / 2,
485    charWidth, charHeight);
486   
487  0 nameHash.put(node, rect);
488   
489  0 if (node.hasLabel() && showStructureProviderColouredLines)
490    {
491  0 g.setColor(Color.black);
492    }
493   
494    // Colour selected leaves differently
495  0 boolean isSelected = false;
496  0 if (tp.isColumnWise())
497    {
498  0 isSelected = isColumnForNodeSelected(node);
499    }
500    else
501    {
502  0 SequenceGroup selected = av.getSelectionGroup();
503   
504  0 if ((selected != null)
505    && selected.getSequences(null).contains(node.element())
506    && (node.getAlignmentAnnotation() == null
507    || selected.getAnnotationsFromTree() == null
508    || selected.getAnnotationsFromTree().isEmpty()
509    || selected.getAnnotationsFromTree()
510    .contains(node.getAlignmentAnnotation())))
511    {
512  0 isSelected = true;
513    }
514    }
515  0 if (isSelected)
516    {
517  0 g.setColor(Color.gray);
518   
519  0 g.fillRect(xend + 10, ypos - charHeight / 2, charWidth, charHeight);
520  0 g.setColor(Color.white);
521    }
522   
523  0 g.drawString(name, xend + 10, ypos + fm.getDescent());
524  0 g.setColor(Color.black);
525    }
526    else
527    {
528  0 drawNode(g, (BinaryNode) node.left(), chunk, wscale, width, offx,
529    offy);
530  0 drawNode(g, (BinaryNode) node.right(), chunk, wscale, width, offx,
531    offy);
532   
533  0 double height = node.height;
534  0 double dist = node.dist;
535   
536  0 int xstart = (int) ((height - dist) * wscale) + offx;
537  0 int xend = (int) (height * wscale) + offx;
538  0 int ypos = (int) (node.ycount * chunk) + offy;
539   
540    // g.setColor(node.color.darker());
541   
542  0 if (node.hasLabel() && showStructureProviderColouredLines)
543    {
544  0 g.setColor(Color.black);
545    }
546    else
547    {
548    // horizontal line
549  0 g.setColor(node.color.darker());
550  0 g.drawLine(xstart, ypos, xend, ypos);
551    }
552   
553  0 int ystart = (node.left() == null ? 0
554    : (int) (node.left().ycount * chunk)) + offy;
555  0 int yend = (node.right() == null ? 0
556    : (int) (node.right().ycount * chunk)) + offy;
557   
558  0 Rectangle pos = new Rectangle(xend - 2, ypos - 2, 5, 5);
559  0 nodeHash.put(node, pos);
560   
561  0 g.drawLine((int) (height * wscale) + offx, ystart,
562    (int) (height * wscale) + offx, yend);
563   
564  0 String nodeLabel = "";
565   
566  0 if (showDistances && (node.dist > 0))
567    {
568  0 nodeLabel = new Format("%g").form(node.dist);
569    }
570   
571  0 if (showBootstrap && node.bootstrap > -1)
572    {
573  0 if (showDistances)
574    {
575  0 nodeLabel = nodeLabel + " : ";
576    }
577   
578  0 nodeLabel = nodeLabel + String.valueOf(node.bootstrap);
579    }
580   
581    // Display secondary structure providers only if:
582    // ~ node has Label assigned (Secondary structure providers)
583    // ~ either label or coloured lines option is selected by the user
584  0 boolean labelHandled = false;
585  0 if (node.hasLabel())
586    {
587   
588  0 if (showStructureProviderColouredLines)
589    {
590  0 drawLinesAndLabelsForSecondaryStructureProvider(g, node, xstart,
591    xend, ypos, nodeLabel);
592   
593  0 labelHandled = true;
594   
595    }
596   
597  0 if (showStructureProviderLabels && node.count > 3
598    && node.parent() != null && node.left() != null
599    && node.right() != null && node.dist > 0)
600    {
601   
602  0 String label = node.getLabel();
603   
604  0 if (label.length() > labelLengthThreshold)
605    {
606   
607    // label = AlignmentUtils.reduceLabelLength(label);
608    }
609   
610  0 nodeLabel = label + " | " + nodeLabel;
611   
612    // Split the nodeLabel by "|"
613  0 String[] lines = nodeLabel.split("\\|");
614   
615    // Draw each string separately
616  0 String longestLabelString = "";
617  0 int i = 0;
618  0 for (i = 0; i < lines.length; i++)
619    {
620  0 g.drawString(lines[i].trim(), xstart + 2,
621    ypos - 2 - (i * fm.getHeight()));
622  0 if (longestLabelString.length() < lines[i].trim().length())
623    {
624  0 longestLabelString = lines[i].trim();
625    }
626    }
627   
628  0 labelHandled = true;
629    }
630    }
631   
632  0 if (!nodeLabel.equals("") && !labelHandled)
633    {
634  0 g.drawString(nodeLabel, xstart + 2, ypos - 2);
635    }
636   
637  0 if (node == highlightNode)
638    {
639  0 g.fillRect(xend - 3, ypos - 3, 6, 6);
640    }
641    else
642    {
643  0 g.fillRect(xend - 2, ypos - 2, 4, 4);
644    }
645    }
646    }
647   
648    /**
649    * Updates the bounding box dimensions for sequence groups in a tree.
650    *
651    * @param g graphics object
652    * @param node tree node
653    * @param chunk chunk
654    * @param wscale width
655    * @param offx x offset
656    * @param offy y offset
657    * @param currentTreeGroups set of current tree groups
658    * @param boxGroupColouringDim map from group hash to bounding box dimensions [minX, minY, maxX, maxY]
659    */
 
660  0 toggle private void updateGroupBoxDimensions(Graphics g, BinaryNode node, double chunk,
661    double wscale, int offx, int offy, Set<SequenceGroup> currentTreeGroups, HashMap<Integer, ArrayList<Integer>> boxGroupColouringDim)
662    {
663   
664  0 if (node == null)
665    {
666  0 return;
667    }
668   
669    // Check if it is a leaf node
670  0 if (node.left() == null && node.right() == null)
671    {
672    // Get the group with the node
673  0 SequenceGroup colourGroup = findTreeGroupForNode(node);
674   
675  0 if (colourGroup != null)
676    {
677   
678    // Calculate dimensions for the node
679   
680  0 double height = node.height;
681   
682  0 int xend = (int) (height * wscale) + offx;
683   
684  0 int ypos = (int) (node.ycount * chunk) + offy;
685   
686  0 String name = (markPlaceholders && ((node instanceof SequenceNode
687    && ((SequenceNode) node).isPlaceholder())))
688    ? (PLACEHOLDER + node.getDisplayName())
689    : node.getDisplayName();
690   
691  0 int charWidth = fm.stringWidth(name) + 3;
692  0 int charHeight = font.getSize();
693   
694  0 int newBoxMaxY = ypos - charHeight / 2 + charHeight;
695  0 int newBoxMaxX = xend + 10 + charWidth;
696  0 int newBoxMinX = xend + 10;
697  0 int newBoxMinY = ypos - charHeight / 2;
698   
699    // Initialise the dimension for the group with default values
700    // if not already set
701  0 if (boxGroupColouringDim.get(colourGroup.hashCode()) == null)
702    {
703  0 ArrayList<Integer> boxDims = new ArrayList<>();
704  0 for (int i = 0; i < 4; i++)
705    {
706  0 boxDims.add(-1);
707    }
708  0 boxGroupColouringDim.put(colourGroup.hashCode(), boxDims);
709    }
710   
711    // Get the dimensions for the group bounding box
712  0 ArrayList<Integer> boxDims = boxGroupColouringDim
713    .get(colourGroup.hashCode());
714   
715    // Assign the box minX value to the least of all minX values of
716    // nodes in the group
717  0 boxDims.set(TREE_GROUP_DIM_MIN_X_INDEX,
718  0 boxDims.get(TREE_GROUP_DIM_MIN_X_INDEX) == -1 ? xend + 1
719    : Math.min(boxDims.get(TREE_GROUP_DIM_MIN_X_INDEX),
720    newBoxMinX));
721    // Assign the box minY value to the least of all minY values of
722    // nodes in the group
723  0 boxDims.set(TREE_GROUP_DIM_MIN_Y_INDEX,
724  0 boxDims.get(TREE_GROUP_DIM_MIN_Y_INDEX) == -1 ? newBoxMinY
725    : Math.min(boxDims.get(TREE_GROUP_DIM_MIN_Y_INDEX),
726    newBoxMinY));
727    // Assign the box maxX value to the max of all maxX values of
728    // nodes in the group
729  0 boxDims.set(TREE_GROUP_DIM_MAX_X_INDEX,
730  0 boxDims.get(TREE_GROUP_DIM_MAX_X_INDEX) == -1 ? newBoxMaxX
731    : Math.max(boxDims.get(TREE_GROUP_DIM_MAX_X_INDEX),
732    newBoxMaxX));
733    // Assign the box maxY value to the max of all maxY values of
734    // nodes in the group
735  0 boxDims.set(TREE_GROUP_DIM_MAX_Y_INDEX,
736  0 boxDims.get(TREE_GROUP_DIM_MAX_Y_INDEX) == -1 ? newBoxMaxY
737    : Math.max(boxDims.get(TREE_GROUP_DIM_MAX_Y_INDEX),
738    newBoxMaxY));
739   
740  0 currentTreeGroups.add(colourGroup);
741    }
742    }
743   
744    else
745    {
746    // Recurse on both child nodes
747  0 updateGroupBoxDimensions(g, (BinaryNode) node.left(), chunk, wscale,
748    offx, offy, currentTreeGroups, boxGroupColouringDim);
749  0 updateGroupBoxDimensions(g, (BinaryNode) node.right(), chunk, wscale,
750    offx, offy, currentTreeGroups, boxGroupColouringDim);
751    }
752    }
753   
754   
755    /**
756    * Returns the tree group to be coloured for the node (in annotation based tree)
757    * @param node current node
758    */
 
759  0 toggle private SequenceGroup findTreeGroupForNode(BinaryNode node)
760    {
761   
762  0 if (!(node.element() instanceof SequenceI))
763    {
764  0 return null;
765    }
766   
767  0 SequenceI seq = (SequenceI) node.element();
768   
769    // Get all groups of the sequence
770  0 SequenceGroup[] groupsWithSequence = (ap.getAlignment() != null)
771    ? ap.getAlignment().findAllGroups(seq)
772    : null;
773   
774    // Identify the group
775  0 if (groupsWithSequence != null && node.hasLabel())
776    {
777    // For nodes with annotation
778  0 if (!av.getSequenceColour(seq).equals(Color.white)
779    && node.hasAlignmentAnnotation())
780    {
781  0 for (SequenceGroup group : groupsWithSequence)
782    {
783  0 if (group.getAnnotationsFromTree()
784    .contains(node.getAlignmentAnnotation())
785    && group.getName().startsWith("JTreeGroup:"))
786    {
787  0 return group;
788    }
789    }
790    }
791    // For nodes without annotation
792    else
793    {
794  0 for (SequenceGroup group : groupsWithSequence)
795    {
796  0 if (group.getName().startsWith("JTreeGroup:"))
797    {
798  0 return group;
799    }
800    }
801    }
802    }
803  0 return null;
804    }
805   
806   
 
807  0 toggle private void drawLinesAndLabelsForSecondaryStructureProvider(Graphics g, BinaryNode node,
808    int xstart, int xend, int ypos, String nodeLabel) {
809   
810  0 Graphics2D g2d = (Graphics2D) g.create();
811   
812  0 g2d.setStroke(new BasicStroke(
813    4.0f,
814    BasicStroke.CAP_BUTT,
815    BasicStroke.JOIN_ROUND
816    ));
817   
818  0 String label = node.getLabel();
819    //String[] lines = label.split("\\|");
820  0 String[] lines = Arrays.stream(label.split("\\|"))
821    .map(String::trim) // Remove spaces at the start and end
822    .toArray(String[]::new);
823  0 Arrays.sort(lines);
824   
825  0 int mid = lines.length / 2;
826   
827    // Reverse the order of elements from position 0 to mid - 1
828  0 for (int i = 0; i < mid / 2; i++) {
829  0 String temp = lines[i];
830  0 lines[i] = lines[mid - 1 - i];
831  0 lines[mid - 1 - i] = temp;
832    }
833   
834    // Draw lines for the first half
835  0 int firstHalfLinesCount = mid;
836   
837  0 drawSecondaryStructureProviderLinesSection(g2d,
838    lines, 0, mid, xstart, xend,
839    ypos - DASHED_LINE_Y_OFFSET,
840    true);
841  0 drawVerticalLineAndLabel(g, xstart, ypos, firstHalfLinesCount, true, nodeLabel);
842   
843    // Draw lines for the second half
844  0 int secondHalfLinesCount = lines.length - mid;
845  0 drawSecondaryStructureProviderLinesSection(g2d,
846    lines, mid, lines.length, xstart, xend,
847    ypos,
848    false);
849  0 drawVerticalLineAndLabel(g, xstart, ypos, secondHalfLinesCount, false, nodeLabel);
850   
851  0 g2d.dispose();
852    }
853   
 
854  0 toggle private void drawSecondaryStructureProviderLinesSection(Graphics2D g2d, String[] lines, int start, int end, int xstart, int xend, int baseY, boolean above) {
855   
856  0 for (int i = start, j=0; i < end; i++, j++) {
857  0 int adjustedY = above
858    ? baseY - ((i - start) * DASHED_LINE_Y_OFFSET)
859    : baseY +((i - start) * DASHED_LINE_Y_OFFSET);
860    //Color providerColor = AlignmentUtils.getSecondaryStructureProviderColor(lines[i]);
861  0 Boolean greyOutStateObj = structureProviderColouredLineToggleState.get(
862    lines[i].toUpperCase().trim()
863    );
864   
865  0 boolean colourState = greyOutStateObj == null || !greyOutStateObj; // Coloured by default
866   
867  0 if(colourState) {
868  0 Map<String, Color> secondaryStructureProviderColorMap = tp.getSecondaryStructureProviderColorMap();
869  0 Color providerColor = secondaryStructureProviderColorMap.getOrDefault(lines[i].toUpperCase().trim(), Color.BLACK);
870  0 g2d.setColor(providerColor);
871    }
872    else{
873  0 g2d.setColor(new Color(Color.LIGHT_GRAY.getRed(),
874    Color.LIGHT_GRAY.getGreen(), Color.LIGHT_GRAY.getBlue(), COLOR_TRANSPARENCY_FOR_COLOURED_LINES));
875    }
876  0 g2d.drawLine(xstart, adjustedY, xend, adjustedY);
877    }
878    }
879   
 
880  0 toggle private void drawVerticalLineAndLabel(Graphics g, int xstart, int ypos,
881    int linesCount, boolean above, String nodeLabel) {
882   
883  0 int adjustment = (linesCount * DASHED_LINE_Y_OFFSET) + (DASHED_LINE_Y_OFFSET / 3);
884  0 int adjustedY = ypos + (above ? -adjustment : adjustment - DASHED_LINE_Y_OFFSET);
885   
886    // draw vertical line
887  0 g.drawLine(xstart, ypos, xstart, adjustedY);
888   
889    // draw label
890  0 if(above && !nodeLabel.equals(""))
891  0 g.drawString(nodeLabel, xstart + 2, adjustedY - 2);
892    }
893   
894   
895    /**
896    * DOCUMENT ME!
897    *
898    * @param x
899    * DOCUMENT ME!
900    * @param y
901    * DOCUMENT ME!
902    *
903    * @return DOCUMENT ME!
904    */
 
905  1 toggle public Object findElement(int x, int y, boolean node)
906    {
907  1 for (Entry<BinaryNode, Rectangle> entry : nameHash.entrySet())
908    {
909  0 Rectangle rect = entry.getValue();
910   
911  0 if ((x >= rect.x) && (x <= (rect.x + rect.width)) && (y >= rect.y)
912    && (y <= (rect.y + rect.height)))
913    {
914  0 if(!node) {
915  0 return entry.getKey().element();
916    }
917    else {
918  0 return entry.getKey();
919    }
920    }
921    }
922   
923  1 for (Entry<BinaryNode, Rectangle> entry : nodeHash.entrySet())
924    {
925  0 Rectangle rect = entry.getValue();
926   
927  0 if ((x >= rect.x) && (x <= (rect.x + rect.width)) && (y >= rect.y)
928    && (y <= (rect.y + rect.height)))
929    {
930  0 return entry.getKey();
931    }
932    }
933   
934  1 return null;
935    }
936   
937    /**
938    * DOCUMENT ME!
939    *
940    * @param pickBox
941    * DOCUMENT ME!
942    */
 
943  0 toggle public void pickNodes(Rectangle pickBox)
944    {
945  0 int width = getWidth();
946  0 int height = getHeight();
947   
948  0 BinaryNode top = tree.getTopNode();
949   
950  0 double wscale = ((width * .8) - (offx * 2)) / tree.getMaxHeight();
951   
952  0 if (top.count == 0)
953    {
954  0 top.count = ((BinaryNode) top.left()).count
955    + ((BinaryNode) top.right()).count;
956    }
957   
958  0 float chunk = (float) (height - (offy)) / top.count;
959   
960  0 pickNode(pickBox, top, chunk, wscale, width, offx, offy);
961    }
962   
963    /**
964    * DOCUMENT ME!
965    *
966    * @param pickBox
967    * DOCUMENT ME!
968    * @param node
969    * DOCUMENT ME!
970    * @param chunk
971    * DOCUMENT ME!
972    * @param wscale
973    * DOCUMENT ME!
974    * @param width
975    * DOCUMENT ME!
976    * @param offx
977    * DOCUMENT ME!
978    * @param offy
979    * DOCUMENT ME!
980    */
 
981  0 toggle public void pickNode(Rectangle pickBox, BinaryNode node, float chunk,
982    double wscale, int width, int offx, int offy)
983    {
984  0 if (node == null)
985    {
986  0 return;
987    }
988   
989  0 if ((node.left() == null) && (node.right() == null))
990    {
991  0 double height = node.height;
992    // double dist = node.dist;
993    // int xstart = (int) ((height - dist) * wscale) + offx;
994  0 int xend = (int) (height * wscale) + offx;
995   
996  0 int ypos = (int) (node.ycount * chunk) + offy;
997   
998  0 if (pickBox.contains(new Point(xend, ypos)))
999    {
1000  0 if (node.element() instanceof SequenceI)
1001    {
1002  0 SequenceI seq = (SequenceI) node.element();
1003  0 SequenceGroup sg = av.getSelectionGroup();
1004   
1005  0 if (sg != null)
1006    {
1007  0 sg.addOrRemove(seq, true);
1008    }
1009    }
1010    }
1011    }
1012    else
1013    {
1014  0 pickNode(pickBox, (BinaryNode) node.left(), chunk, wscale, width,
1015    offx, offy);
1016  0 pickNode(pickBox, (BinaryNode) node.right(), chunk, wscale, width,
1017    offx, offy);
1018    }
1019    }
1020   
1021    /**
1022    * DOCUMENT ME!
1023    *
1024    * @param node
1025    * DOCUMENT ME!
1026    * @param c
1027    * DOCUMENT ME!
1028    */
 
1029  141 toggle public void setColor(BinaryNode node, Color c)
1030    {
1031  141 if (node == null)
1032    {
1033  72 return;
1034    }
1035   
1036  69 node.color = c;
1037  69 if (node.element() instanceof SequenceI)
1038    {
1039  36 final SequenceI seq = (SequenceI) node.element();
1040  36 AlignmentAnnotation annotation = node.getAlignmentAnnotation();
1041  36 AlignmentPanel[] aps = getAssociatedPanels();
1042  36 if (aps != null)
1043    {
1044  72 for (int a = 0; a < aps.length; a++)
1045    {
1046  36 aps[a].av.setSequenceColour(seq, c);
1047   
1048    //Assign color to annotation
1049  36 if(annotation!=null) {
1050  8 aps[a].av.setAnnotationColour(annotation, c);
1051    }
1052    }
1053    }
1054    }
1055  69 setColor((BinaryNode) node.left(), c);
1056  69 setColor((BinaryNode) node.right(), c);
1057    }
1058   
1059    /**
1060    * DOCUMENT ME!
1061    */
 
1062  0 toggle void startPrinting()
1063    {
1064  0 Thread thread = new Thread(this);
1065  0 thread.start();
1066    }
1067   
1068    // put printing in a thread to avoid painting problems
 
1069  0 toggle @Override
1070    public void run()
1071    {
1072  0 PrinterJob printJob = PrinterJob.getPrinterJob();
1073  0 PageFormat defaultPage = printJob.defaultPage();
1074  0 PageFormat pf = printJob.pageDialog(defaultPage);
1075   
1076  0 if (defaultPage == pf)
1077    {
1078    /*
1079    * user cancelled
1080    */
1081  0 return;
1082    }
1083   
1084  0 printJob.setPrintable(this, pf);
1085   
1086  0 if (printJob.printDialog())
1087    {
1088  0 try
1089    {
1090  0 printJob.print();
1091    } catch (Exception PrintException)
1092    {
1093  0 PrintException.printStackTrace();
1094    }
1095    }
1096    }
1097   
1098    /**
1099    * DOCUMENT ME!
1100    *
1101    * @param pg
1102    * DOCUMENT ME!
1103    * @param pf
1104    * DOCUMENT ME!
1105    * @param pi
1106    * DOCUMENT ME!
1107    *
1108    * @return DOCUMENT ME!
1109    *
1110    * @throws PrinterException
1111    * DOCUMENT ME!
1112    */
 
1113  0 toggle @Override
1114    public int print(Graphics pg, PageFormat pf, int pi)
1115    throws PrinterException
1116    {
1117  0 pg.setFont(font);
1118  0 pg.translate((int) pf.getImageableX(), (int) pf.getImageableY());
1119   
1120  0 int pwidth = (int) pf.getImageableWidth();
1121  0 int pheight = (int) pf.getImageableHeight();
1122   
1123  0 int noPages = getHeight() / pheight;
1124   
1125  0 if (pi > noPages)
1126    {
1127  0 return Printable.NO_SUCH_PAGE;
1128    }
1129   
1130  0 if (pwidth > getWidth())
1131    {
1132  0 pwidth = getWidth();
1133    }
1134   
1135  0 if (fitToWindow)
1136    {
1137  0 if (pheight > getHeight())
1138    {
1139  0 pheight = getHeight();
1140    }
1141   
1142  0 noPages = 0;
1143    }
1144    else
1145    {
1146  0 FontMetrics fm = pg.getFontMetrics(font);
1147  0 int height = fm.getHeight() * nameHash.size();
1148  0 pg.translate(0, -pi * pheight);
1149  0 pg.setClip(0, pi * pheight, pwidth, (pi * pheight) + pheight);
1150   
1151    // translate number of pages,
1152    // height is screen size as this is the
1153    // non overlapping text size
1154  0 pheight = height;
1155    }
1156   
1157  0 draw(pg, pwidth, pheight);
1158   
1159  0 return Printable.PAGE_EXISTS;
1160    }
1161   
1162    /**
1163    * DOCUMENT ME!
1164    *
1165    * @param g
1166    * DOCUMENT ME!
1167    */
 
1168  0 toggle @Override
1169    public void paintComponent(Graphics g)
1170    {
1171  0 super.paintComponent(g);
1172  0 g.setFont(font);
1173   
1174  0 if (tree == null)
1175    {
1176  0 g.drawString(
1177    MessageManager.getString("label.calculating_tree") + "....",
1178    20, getHeight() / 2);
1179    }
1180    else
1181    {
1182  0 fm = g.getFontMetrics(font);
1183   
1184  0 int nameCount = nameHash.size();
1185  0 if (nameCount == 0)
1186    {
1187  0 repaint();
1188    }
1189   
1190  0 if (fitToWindow || (!fitToWindow && (scrollPane
1191    .getHeight() > ((fm.getHeight() * nameCount) + offy))))
1192    {
1193  0 draw(g, scrollPane.getWidth(), scrollPane.getHeight());
1194  0 setPreferredSize(null);
1195    }
1196    else
1197    {
1198  0 setPreferredSize(new Dimension(scrollPane.getWidth(),
1199    fm.getHeight() * nameCount));
1200  0 draw(g, scrollPane.getWidth(), fm.getHeight() * nameCount);
1201    }
1202   
1203  0 scrollPane.revalidate();
1204    }
1205    }
1206   
1207    /**
1208    * DOCUMENT ME!
1209    *
1210    * @param fontSize
1211    * DOCUMENT ME!
1212    */
 
1213  21 toggle @Override
1214    public void setFont(Font font)
1215    {
1216  21 this.font = font;
1217  21 repaint();
1218    }
1219   
1220    /**
1221    * DOCUMENT ME!
1222    *
1223    * @param g1
1224    * DOCUMENT ME!
1225    * @param width
1226    * DOCUMENT ME!
1227    * @param height
1228    * DOCUMENT ME!
1229    */
 
1230  0 toggle public void draw(Graphics g1, int width, int height)
1231    {
1232  0 Graphics2D g2 = (Graphics2D) g1;
1233  0 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1234    RenderingHints.VALUE_ANTIALIAS_ON);
1235  0 g2.setColor(Color.white);
1236  0 g2.fillRect(0, 0, width, height);
1237  0 g2.setFont(font);
1238   
1239  0 if (longestName == null || tree == null)
1240    {
1241  0 g2.drawString("Calculating tree.", 20, 20);
1242  0 return;
1243    }
1244  0 offy = font.getSize() + 10;
1245   
1246  0 fm = g2.getFontMetrics(font);
1247   
1248  0 labelLength = fm.stringWidth(longestName) + 20; // 20 allows for scrollbar
1249   
1250  0 double wscale = (width - labelLength - (offx * 2))
1251    / tree.getMaxHeight();
1252   
1253  0 BinaryNode top = tree.getTopNode();
1254   
1255  0 if (top.count == 0)
1256    {
1257  0 top.count = top.left().count + top.right().count;
1258    }
1259   
1260  0 float chunk = (float) (height - (offy)) / top.count;
1261   
1262    // Group colouring as coloured rectangular background
1263    // only for coloured line visualisation
1264  0 if (showStructureProviderColouredLines)
1265    {
1266  0 drawGroupColourBoxes(g2, tree.getTopNode(), chunk, wscale, offx, offy);
1267    }
1268   
1269  0 drawNode(g2, tree.getTopNode(), chunk, wscale, width, offx, offy);
1270   
1271   
1272  0 if (threshold != 0)
1273    {
1274  0 if (av.getCurrentTree() == tree)
1275    {
1276  0 g2.setColor(Color.red);
1277    }
1278    else
1279    {
1280  0 g2.setColor(Color.gray);
1281    }
1282   
1283  0 int x = (int) ((threshold * (getWidth() - labelLength - (2 * offx)))
1284    + offx);
1285   
1286  0 g2.drawLine(x, 0, x, getHeight());
1287    }
1288    }
1289   
1290    /**
1291    * Draws coloured background boxes for sequence groups in the tree.
1292    *
1293    * @param g2 Graphics object
1294    * @param node Binary node
1295    * @param chunk Chunk
1296    * @param wscale Width scale
1297    * @param offx X offset
1298    * @param offy Y offset
1299    */
 
1300  0 toggle private void drawGroupColourBoxes(Graphics g2, BinaryNode node, double chunk, double wscale, int offx, int offy)
1301    {
1302    // Reset the object storing current groups and colouring dimensions
1303  0 Set<SequenceGroup> currentTreeGroups = new HashSet<SequenceGroup>();
1304   
1305    // Map that stores group hash code as key and the box dimensions for
1306    // background group colouring
1307  0 HashMap<Integer, ArrayList<Integer>> boxGroupColouringDim = new HashMap<Integer, ArrayList<Integer>>();
1308   
1309    // Gives the dimensions of the rectangular background group colouring
1310  0 updateGroupBoxDimensions(g2, tree.getTopNode(), chunk, wscale, offx,
1311    offy, currentTreeGroups, boxGroupColouringDim);
1312   
1313    // Store current colour temporarily to restore after group colouring
1314  0 Color prevColour = g2.getColor();
1315   
1316    // Iterate through each existing groups
1317  0 for (SequenceGroup treeGroup : currentTreeGroups)
1318    {
1319   
1320    // Get box dimensions for colouring
1321  0 ArrayList<Integer> boxDims = boxGroupColouringDim
1322    .get(treeGroup.hashCode());
1323   
1324    // Apply group colour if exists
1325  0 if (treeGroup.idColour != null)
1326    {
1327   
1328    // Set the brightness and transparency of the colour
1329  0 Color treeGroupColor = treeGroup.idColour.brighter();
1330  0 g2.setColor(new Color(treeGroupColor.getRed(),
1331    treeGroupColor.getGreen(), treeGroupColor.getBlue(),
1332    COLOR_TRANSPARENCY_FOR_GROUP));
1333   
1334  0 int x = boxDims.get(TREE_GROUP_DIM_MIN_X_INDEX);
1335  0 int y = boxDims.get(TREE_GROUP_DIM_MIN_Y_INDEX) - 2;
1336  0 int width = boxDims.get(TREE_GROUP_DIM_MAX_X_INDEX)
1337    - boxDims.get(TREE_GROUP_DIM_MIN_X_INDEX);
1338  0 int height = boxDims.get(TREE_GROUP_DIM_MAX_Y_INDEX)
1339    - boxDims.get(TREE_GROUP_DIM_MIN_Y_INDEX) + 4;
1340   
1341    // Apply group colour as a coloured rectangular background for the group
1342  0 g2.fillRect(x, y, width, height);
1343   
1344    }
1345    }
1346    // Restore the previous colour
1347  0 g2.setColor(prevColour);
1348    }
1349   
1350   
1351    /**
1352    * Empty method to satisfy the MouseListener interface
1353    *
1354    * @param e
1355    */
 
1356  0 toggle @Override
1357    public void mouseReleased(MouseEvent e)
1358    {
1359    /*
1360    * isPopupTrigger is set on mouseReleased on Windows
1361    */
1362  0 if (e.isPopupTrigger())
1363    {
1364  0 chooseSubtreeColour();
1365  0 e.consume(); // prevent mouseClicked happening
1366    }
1367    }
1368   
1369    /**
1370    * Empty method to satisfy the MouseListener interface
1371    *
1372    * @param e
1373    */
 
1374  0 toggle @Override
1375    public void mouseEntered(MouseEvent e)
1376    {
1377    }
1378   
1379    /**
1380    * Empty method to satisfy the MouseListener interface
1381    *
1382    * @param e
1383    */
 
1384  0 toggle @Override
1385    public void mouseExited(MouseEvent e)
1386    {
1387    }
1388   
1389    /**
1390    * Handles a mouse click on a tree node (clicks elsewhere are handled in
1391    * mousePressed). Click selects the sub-tree, double-click swaps leaf nodes
1392    * order, right-click opens a dialogue to choose colour for the sub-tree.
1393    *
1394    * @param e
1395    */
 
1396  0 toggle @Override
1397    public void mouseClicked(MouseEvent evt)
1398    {
1399  0 if (highlightNode == null)
1400    {
1401  0 return;
1402    }
1403   
1404  0 if (evt.getClickCount() > 1)
1405    {
1406  0 tree.swapNodes(highlightNode);
1407  0 tree.reCount(tree.getTopNode());
1408  0 tree.findHeight(tree.getTopNode());
1409    }
1410    else
1411    {
1412  0 Vector<BinaryNode> leaves = tree.findLeaves(highlightNode);
1413  0 if (tp.isColumnWise())
1414    {
1415  0 markColumnsFor(getAssociatedPanels(), leaves, Color.red, false);
1416    }
1417   
1418    else
1419    {
1420  0 for (int i = 0; i < leaves.size(); i++)
1421    {
1422  0 SequenceI seq = (SequenceI) leaves.elementAt(i).element();
1423  0 if(leaves.get(i).hasAlignmentAnnotation())
1424    {
1425  0 treeSelectionChanged(seq, leaves.get(i).getAlignmentAnnotation());
1426   
1427    }
1428    else {
1429  0 treeSelectionChanged(seq, null);
1430    }
1431   
1432    }
1433    }
1434  0 av.sendSelection();
1435    }
1436   
1437  0 PaintRefresher.Refresh(tp, av.getSequenceSetId());
1438  0 repaint();
1439    }
1440   
1441    /**
1442    * Offer the user the option to choose a colour for the highlighted node and
1443    * its children; this colour is also applied to the corresponding sequence ids
1444    * in the alignment
1445    */
 
1446  0 toggle void chooseSubtreeColour()
1447    {
1448  0 String ttl = MessageManager.getString("label.select_subtree_colour");
1449  0 ColourChooserListener listener = new ColourChooserListener()
1450    {
 
1451  0 toggle @Override
1452    public void colourSelected(Color c)
1453    {
1454  0 setColor(highlightNode, c);
1455  0 PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
1456  0 repaint();
1457    }
1458    };
1459  0 JalviewColourChooser.showColourChooser(this, ttl, highlightNode.color,
1460    listener);
1461    }
1462   
 
1463  0 toggle @Override
1464    public void mouseMoved(MouseEvent evt)
1465    {
1466  0 av.setCurrentTree(tree);
1467   
1468  0 Object ob = findElement(evt.getX(), evt.getY(), false);
1469   
1470    // Get mouse coordinates
1471  0 int mouseX = evt.getX();
1472  0 int mouseY = evt.getY();
1473   
1474  0 if (ob instanceof BinaryNode)
1475    {
1476  0 highlightNode = (BinaryNode) ob;
1477  0 this.setToolTipText(
1478    "<html>" + MessageManager.getString("label.highlightnode"));
1479  0 repaint();
1480   
1481    }
1482    else
1483    {
1484   
1485  0 if (highlightNode != null)
1486    {
1487  0 highlightNode = null;
1488  0 setToolTipText(null);
1489  0 repaint();
1490    }
1491   
1492    // Iterate through the map of label bounding boxes
1493    // for (Map.Entry<BinaryNode, List<Rectangle>> entry : labelBoundsMap.entrySet()) {
1494    // BinaryNode node = entry.getKey();
1495    // List<Rectangle> boundsList = entry.getValue();
1496    //
1497    // // Check each bounding box for this node
1498    // for (Rectangle labelBounds : boundsList) {
1499    // if (labelBounds.contains(mouseX, mouseY)) {
1500    // // Show tooltip for this node's label
1501    // String nodeLabel = node.getDisplayName();
1502    // this.setToolTipText(nodeLabel);
1503    // repaint();
1504    // return; // Exit once we find a matching label
1505    // }
1506    // }
1507    // }
1508    // Clear tooltip if no label is hovered
1509  0 setToolTipText(null);
1510  0 repaint();
1511   
1512    }
1513    }
1514   
 
1515  0 toggle @Override
1516    public void mouseDragged(MouseEvent ect)
1517    {
1518    }
1519   
1520    /**
1521    * Handles a mouse press on a sequence name or the tree background canvas
1522    * (click on a node is handled in mouseClicked). The action is to create
1523    * groups by partitioning the tree at the mouse position. Colours for the
1524    * groups (and sequence names) are generated randomly.
1525    *
1526    * @param e
1527    */
 
1528  1 toggle @Override
1529    public void mousePressed(MouseEvent e)
1530    {
1531  1 av.setCurrentTree(tree);
1532   
1533    /*
1534    * isPopupTrigger is set for mousePressed (Mac)
1535    * or mouseReleased (Windows)
1536    */
1537  1 if (e.isPopupTrigger())
1538    {
1539  0 if (highlightNode != null)
1540    {
1541  0 chooseSubtreeColour();
1542    }
1543  0 return;
1544    }
1545   
1546    /*
1547    * defer right-click handling on Windows to
1548    * mouseClicked; note isRightMouseButton
1549    * also matches Cmd-click on Mac which should do
1550    * nothing here
1551    */
1552  1 if (SwingUtilities.isRightMouseButton(e))
1553    {
1554  0 return;
1555    }
1556   
1557  1 int x = e.getX();
1558  1 int y = e.getY();
1559   
1560  1 Object ob = findElement(x, y, true);
1561   
1562  1 if (ob instanceof BinaryNode && (((BinaryNode) ob).element() != null))
1563    {
1564  0 if(((BinaryNode) ob).hasAlignmentAnnotation())
1565    {
1566  0 treeSelectionChanged((SequenceI) ((BinaryNode) ob).element(),
1567    ((BinaryNode) ob).getAlignmentAnnotation());
1568    }
1569    else
1570    {
1571  0 treeSelectionChanged((SequenceI) ((BinaryNode) ob).element(), null);
1572   
1573    }
1574  0 PaintRefresher.Refresh(tp,
1575    getAssociatedPanel().av.getSequenceSetId());
1576  0 repaint();
1577  0 av.sendSelection();
1578  0 return;
1579    }
1580  1 else if (!(ob instanceof BinaryNode))
1581    {
1582    // Find threshold
1583  1 if (tree.getMaxHeight() != 0)
1584    {
1585  1 threshold = (float) (x - offx)
1586    / (float) (getWidth() - labelLength - (2 * offx));
1587   
1588  1 List<BinaryNode> groups = tree.groupNodes(threshold);
1589  1 setColor(tree.getTopNode(), Color.black);
1590   
1591  1 AlignmentPanel[] aps = getAssociatedPanels();
1592   
1593    // TODO push calls below into a single AlignViewportI method?
1594    // see also AlignViewController.deleteGroups
1595  2 for (int a = 0; a < aps.length; a++)
1596    {
1597  1 aps[a].av.setSelectionGroup(null);
1598  1 aps[a].av.getAlignment().deleteAllGroups();
1599  1 aps[a].av.clearSequenceColours();
1600  1 aps[a].av.clearAnnotationColours();
1601  1 if (aps[a].av.getCodingComplement() != null)
1602    {
1603  0 aps[a].av.getCodingComplement().setSelectionGroup(null);
1604  0 aps[a].av.getCodingComplement().getAlignment()
1605    .deleteAllGroups();
1606  0 aps[a].av.getCodingComplement().clearSequenceColours();
1607    }
1608  1 aps[a].av.setUpdateStructures(true);
1609    }
1610  1 colourGroups(groups);
1611   
1612    /*
1613    * clear partition (don't show vertical line) if
1614    * it is to the right of all nodes
1615    */
1616  1 if (groups.isEmpty())
1617    {
1618  0 threshold = 0f;
1619    }
1620    }
1621  1 Console.log.debug("Tree cut threshold set at:" + threshold);
1622  1 PaintRefresher.Refresh(tp,
1623    getAssociatedPanel().av.getSequenceSetId());
1624  1 repaint();
1625    }
1626   
1627    }
1628   
 
1629  1 toggle void colourGroups(List<BinaryNode> groups)
1630    {
1631  1 AlignmentPanel[] aps = getAssociatedPanels();
1632  1 List<BitSet> colGroups = new ArrayList<>();
1633  1 Map<BitSet, Color> colors = new HashMap();
1634  3 for (int i = 0; i < groups.size(); i++)
1635    {
1636  2 Color col = ColorUtils.getColorForIndex(i);
1637  2 setColor(groups.get(i), col.brighter());
1638   
1639  2 Vector<BinaryNode> l = tree.findLeaves(groups.get(i));
1640    //gatherLabelsTo(groups.get(i), l);
1641  2 if (!tp.isColumnWise())
1642    {
1643  2 createSeqGroupFor(aps, l, col, groups.get(i).getLabel());
1644    }
1645    else
1646    {
1647  0 BitSet gp = createColumnGroupFor(l, col);
1648   
1649  0 colGroups.add(gp);
1650  0 colors.put(gp, col);
1651    }
1652    }
1653  1 if (tp.isColumnWise())
1654    {
1655  0 AlignmentAnnotation aa = tp.getAssocAnnotation();
1656  0 if (aa != null)
1657    {
1658  0 ContactMatrixI cm = av.getContactMatrix(aa);
1659  0 if (cm != null)
1660    {
1661  0 cm.updateGroups(colGroups);
1662  0 for (BitSet gp : colors.keySet())
1663    {
1664  0 cm.setColorForGroup(gp, colors.get(gp));
1665    }
1666    }
1667  0 cm.transferGroupColorsTo(aa);
1668    }
1669    }
1670   
1671    // notify the panel(s) to redo any group specific stuff
1672    // also updates structure views if necessary
1673  2 for (int a = 0; a < aps.length; a++)
1674    {
1675  1 aps[a].updateAnnotation();
1676  1 final AlignViewportI codingComplement = aps[a].av
1677    .getCodingComplement();
1678  1 if (codingComplement != null)
1679    {
1680  0 ((AlignViewport) codingComplement).getAlignPanel()
1681    .updateAnnotation();
1682    }
1683    }
1684    }
1685   
 
1686  0 toggle private void gatherLabelsTo(BinaryNode binaryNode, Vector<BinaryNode> l)
1687    {
1688  0 LinkedHashSet<String> labelsForNode = new LinkedHashSet<String>();
1689  0 for (BinaryNode leaf : l)
1690    {
1691  0 if (leaf.hasLabel())
1692    {
1693  0 labelsForNode.add(leaf.getLabel());
1694    }
1695    }
1696  0 StringBuilder sb = new StringBuilder();
1697  0 boolean first = true;
1698  0 for (String label : labelsForNode)
1699    {
1700  0 if (!first)
1701    {
1702  0 sb.append(" | ");
1703    }
1704  0 first = false;
1705    // if(labelsForNode.size()>1) {
1706    // String providerAbbreviation = AlignmentUtils.getProviderKey(label);
1707    // sb.append(providerAbbreviation);
1708    // }
1709  0 sb.append(label);
1710   
1711    }
1712  0 binaryNode.setLabel(sb.toString());
1713    }
1714   
 
1715  0 toggle private int parseColumnNode(BinaryNode bn) throws NumberFormatException
1716    {
1717  0 return Integer.parseInt(
1718    bn.getName().substring(bn.getName().indexOf("c") + 1));
1719    }
1720   
 
1721  0 toggle private boolean isColumnForNodeSelected(BinaryNode bn)
1722    {
1723  0 SequenceI rseq = tp.assocAnnotation.sequenceRef;
1724  0 int colm = -1;
1725  0 try
1726    {
1727  0 colm = parseColumnNode(bn);
1728    } catch (Exception e)
1729    {
1730  0 return false;
1731    }
1732  0 if (av == null || av.getAlignment() == null)
1733    {
1734    // alignment is closed
1735  0 return false;
1736    }
1737  0 ColumnSelection cs = av.getColumnSelection();
1738  0 HiddenColumns hc = av.getAlignment().getHiddenColumns();
1739  0 AlignmentAnnotation aa = tp.getAssocAnnotation();
1740  0 int offp = -1;
1741  0 if (aa != null)
1742    {
1743  0 ContactMatrixI cm = av.getContactMatrix(aa);
1744    // generally, we assume cm has 1:1 mapping to annotation row - probably
1745    // wrong
1746    // but.. if
1747  0 if (cm instanceof MappableContactMatrixI)
1748    {
1749  0 int[] pos;
1750    // use the mappable's mapping - always the case for PAE Matrices so good
1751    // for 2.11.3
1752  0 MappableContactMatrixI mcm = (MappableContactMatrixI) cm;
1753  0 pos = mcm.getMappedPositionsFor(rseq, colm + 1);
1754    // finally, look up the position of the column
1755  0 if (pos != null)
1756    {
1757  0 offp = rseq.findIndex(pos[0]);
1758    }
1759    }
1760    else
1761    {
1762  0 offp = colm;
1763    }
1764    }
1765  0 if (offp <= 0)
1766    {
1767  0 return false;
1768    }
1769   
1770  0 offp -= 2;
1771  0 if (!av.hasHiddenColumns())
1772    {
1773  0 return cs.contains(offp);
1774    }
1775  0 if (hc.isVisible(offp))
1776    {
1777  0 return cs.contains(offp);
1778    // return cs.contains(hc.absoluteToVisibleColumn(offp));
1779    }
1780  0 return false;
1781    }
1782   
 
1783  0 toggle private BitSet createColumnGroupFor(Vector<BinaryNode> l, Color col)
1784    {
1785  0 BitSet gp = new BitSet();
1786  0 for (BinaryNode bn : l)
1787    {
1788  0 int colm = -1;
1789  0 if (bn.element() != null && bn.element() instanceof Integer)
1790    {
1791  0 colm = (Integer) bn.element();
1792    }
1793    else
1794    {
1795    // parse out from nodename
1796  0 try
1797    {
1798  0 colm = parseColumnNode(bn);
1799    } catch (Exception e)
1800    {
1801  0 continue;
1802    }
1803    }
1804  0 gp.set(colm);
1805    }
1806  0 return gp;
1807    }
1808   
 
1809  0 toggle private void markColumnsFor(AlignmentPanel[] aps, Vector<BinaryNode> l,
1810    Color col, boolean clearSelected)
1811    {
1812  0 SequenceI rseq = tp.assocAnnotation.sequenceRef;
1813  0 if (av == null || av.getAlignment() == null)
1814    {
1815    // alignment is closed
1816  0 return;
1817    }
1818   
1819    // TODO - sort indices for faster lookup
1820  0 ColumnSelection cs = av.getColumnSelection();
1821  0 HiddenColumns hc = av.getAlignment().getHiddenColumns();
1822  0 ContactMatrixI cm = av.getContactMatrix(tp.assocAnnotation);
1823  0 MappableContactMatrixI mcm = null;
1824  0 int offp;
1825  0 if (cm instanceof MappableContactMatrixI)
1826    {
1827  0 mcm = (MappableContactMatrixI) cm;
1828    }
1829  0 for (BinaryNode bn : l)
1830    {
1831  0 int colm = -1;
1832  0 try
1833    {
1834  0 colm = Integer.parseInt(
1835    bn.getName().substring(bn.getName().indexOf("c") + 1));
1836    } catch (Exception e)
1837    {
1838  0 continue;
1839    }
1840  0 if (mcm != null)
1841    {
1842  0 int[] seqpos = mcm.getMappedPositionsFor(rseq, colm);
1843  0 if (seqpos == null)
1844    {
1845    // no mapping for this column.
1846  0 continue;
1847    }
1848    // TODO: handle ranges...
1849  0 offp = rseq.findIndex(seqpos[0]) - 1;
1850    }
1851    else
1852    {
1853  0 offp = (rseq != null) ? rseq.findIndex(rseq.getStart() + colm)
1854    : colm;
1855    }
1856  0 if (!av.hasHiddenColumns() || hc.isVisible(offp))
1857    {
1858  0 if (clearSelected || cs.contains(offp))
1859    {
1860  0 cs.removeElement(offp);
1861    }
1862    else
1863    {
1864  0 cs.addElement(offp);
1865    }
1866    }
1867    }
1868  0 PaintRefresher.Refresh(tp, av.getSequenceSetId());
1869    }
1870   
 
1871  2 toggle public void createSeqGroupFor(AlignmentPanel[] aps, Vector<BinaryNode> l,
1872    Color col, String label)
1873    {
1874   
1875  2 Vector<SequenceI> sequences = new Vector<>();
1876  2 List<AlignmentAnnotation> annotationInGroup = new ArrayList<AlignmentAnnotation>();
1877   
1878  20 for (int j = 0; j < l.size(); j++)
1879    {
1880  18 SequenceI s1 = (SequenceI) l.elementAt(j).element();
1881   
1882  18 if (!sequences.contains(s1))
1883    {
1884  15 sequences.addElement(s1);
1885    }
1886   
1887  18 if(l.elementAt(j).getAlignmentAnnotation()!=null
1888    && !annotationInGroup.contains(l.elementAt(j).getAlignmentAnnotation())) {
1889  4 annotationInGroup.add(l.elementAt(j).getAlignmentAnnotation());
1890   
1891    }
1892   
1893    }
1894   
1895  2 ColourSchemeI cs = null;
1896  2 SequenceGroup _sg = new SequenceGroup(sequences, null, cs, true, true,
1897    false, 0, av.getAlignment().getWidth() - 1);
1898   
1899   
1900  2 for(AlignmentAnnotation annot:annotationInGroup) {
1901   
1902  4 _sg.addAnnotationFromTree(annot);
1903    }
1904   
1905   
1906    // Check if the label is not null and not empty
1907  2 if(label != null && !label.isEmpty()) {
1908    // Retrieve the existing groups from the alignment
1909  0 List<SequenceGroup> existingGroups = av.getAlignment().getGroups();
1910   
1911    // Reduce the label length
1912  0 label = AlignmentUtils.reduceLabelLength(label);
1913   
1914    // Create group name based on the label
1915  0 String newGroupName = "JTreeGroup:" + label;
1916   
1917    // Counter for groups with the same name
1918  0 int noOfGroupsWithSameName = 0;
1919   
1920    // Iterate through the existing groups to check for naming conflicts
1921  0 for (SequenceGroup sg : existingGroups) {
1922  0 if (sg.getName().equals(newGroupName) || sg.getName().matches(newGroupName + " \\d+")) {
1923   
1924  0 noOfGroupsWithSameName++;
1925   
1926    // If a group name matches exactly, update the group's name by appending the count
1927  0 if(sg.getName().equals(newGroupName) ) {
1928  0 String updatedGroupName = sg.getName() + " " + noOfGroupsWithSameName;
1929  0 sg.setName(updatedGroupName);
1930    }
1931    }
1932    }
1933   
1934   
1935    // If count > 0, increment the count and append it to the new group name
1936  0 if(noOfGroupsWithSameName>0) {
1937  0 noOfGroupsWithSameName++;
1938  0 newGroupName = newGroupName + " " + noOfGroupsWithSameName;
1939    }
1940   
1941  0 _sg.setName(newGroupName);
1942    }
1943    else {
1944  2 _sg.setName("JTreeGroup:" + _sg.hashCode());
1945    }
1946  2 _sg.setIdColour(col);
1947   
1948  4 for (int a = 0; a < aps.length; a++)
1949    {
1950  2 SequenceGroup sg = new SequenceGroup(_sg);
1951  2 AlignViewport viewport = aps[a].av;
1952   
1953    // Propagate group colours in each view
1954  2 if (viewport.getGlobalColourScheme() != null)
1955    {
1956  0 cs = viewport.getGlobalColourScheme().getInstance(viewport, sg);
1957  0 sg.setColourScheme(cs);
1958  0 sg.getGroupColourScheme().setThreshold(
1959    viewport.getResidueShading().getThreshold(),
1960    viewport.isIgnoreGapsConsensus());
1961   
1962  0 if (viewport.getResidueShading().conservationApplied())
1963    {
1964  0 Conservation c = new Conservation("Group", sg.getSequences(null),
1965    sg.getStartRes(), sg.getEndRes());
1966  0 c.calculate();
1967  0 c.verdict(false, viewport.getConsPercGaps());
1968  0 sg.cs.setConservation(c);
1969    }
1970  0 if (viewport.getResidueShading()
1971    .isConsensusSecondaryStructureColouring())
1972    {
1973  0 sg.getGroupColourScheme().setConsensusSecondaryStructureThreshold(
1974    viewport.getResidueShading().getThreshold());
1975  0 sg.getGroupColourScheme()
1976    .setConsensusSecondaryStructureColouring(true);
1977    }
1978    }
1979    // indicate that associated structure views will need an update
1980  2 viewport.setUpdateStructures(true);
1981    // propagate structure view update and sequence group to complement view
1982  2 viewport.addSequenceGroup(sg);
1983    }
1984    }
1985   
1986    /**
1987    * DOCUMENT ME!
1988    *
1989    * @param state
1990    * DOCUMENT ME!
1991    */
 
1992  38 toggle public void setShowDistances(boolean state)
1993    {
1994  38 this.showDistances = state;
1995  38 repaint();
1996    }
1997   
 
1998  0 toggle public void hideStructureProviders(boolean state)
1999    {
2000  0 if(state) {
2001  0 this.showStructureProviderColouredLines = false;
2002  0 this.showStructureProviderLabels = false;
2003  0 repaint();
2004    }
2005    }
2006   
 
2007  4 toggle public void setShowStructureProviderColouredLines(boolean state)
2008    {
2009  4 this.showStructureProviderColouredLines = state;
2010  4 if(state) {
2011  4 this.showStructureProviderLabels = false;
2012    }
2013  4 repaint();
2014    }
2015   
 
2016  0 toggle public void setShowStructureProviderLabels(boolean state)
2017    {
2018  0 this.showStructureProviderLabels = state;
2019  0 if(state) {
2020  0 this.showStructureProviderColouredLines = false;
2021    }
2022  0 repaint();
2023    }
2024   
2025   
 
2026  0 toggle public void toggleStructureProviderColouredLine(String provider, boolean state)
2027    {
2028  0 this.structureProviderColouredLineToggleState.put(provider.toUpperCase().trim(), state);
2029  0 repaint();
2030    }
2031   
2032   
2033   
2034    /**
2035    * DOCUMENT ME!
2036    *
2037    * @param state
2038    * DOCUMENT ME!
2039    */
 
2040  20 toggle public void setShowBootstrap(boolean state)
2041    {
2042  20 this.showBootstrap = state;
2043  20 repaint();
2044    }
2045   
2046    /**
2047    * DOCUMENT ME!
2048    *
2049    * @param state
2050    * DOCUMENT ME!
2051    */
 
2052  21 toggle public void setMarkPlaceholders(boolean state)
2053    {
2054  21 this.markPlaceholders = state;
2055  21 repaint();
2056    }
2057   
 
2058  38 toggle AlignmentPanel[] getAssociatedPanels()
2059    {
2060  38 if (applyToAllViews)
2061    {
2062  0 return PaintRefresher.getAssociatedPanels(av.getSequenceSetId());
2063    }
2064    else
2065    {
2066  38 return new AlignmentPanel[] { getAssociatedPanel() };
2067    }
2068    }
2069   
 
2070  76 toggle public AlignmentPanel getAssociatedPanel()
2071    {
2072  76 return ap;
2073    }
2074   
 
2075  18 toggle public void setAssociatedPanel(AlignmentPanel ap)
2076    {
2077  18 this.ap = ap;
2078    }
2079   
 
2080  6 toggle public AlignViewport getViewport()
2081    {
2082  6 return av;
2083    }
2084   
 
2085  0 toggle public void setViewport(AlignViewport av)
2086    {
2087  0 this.av = av;
2088    }
2089   
 
2090  6 toggle public float getThreshold()
2091    {
2092  6 return threshold;
2093    }
2094   
 
2095  3 toggle public void setThreshold(float threshold)
2096    {
2097  3 this.threshold = threshold;
2098    }
2099   
 
2100  6 toggle public boolean isApplyToAllViews()
2101    {
2102  6 return this.applyToAllViews;
2103    }
2104   
 
2105  15 toggle public void setApplyToAllViews(boolean applyToAllViews)
2106    {
2107  15 this.applyToAllViews = applyToAllViews;
2108    }
2109    }