Clover icon

Coverage Report

  1. Project Clover database Wed Dec 3 2025 15:58:31 GMT
  2. Package jalview.gui

File TreeCanvas.java

 

Coverage histogram

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

Code metrics

322
645
54
1
2,113
1,430
271
0.42
11.94
54
5.02

Classes

Class Line # Actions
TreeCanvas 86 645 271
0.1821743418.2%
 

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