Clover icon

Coverage Report

  1. Project Clover database Sun Jan 11 2026 02:28:45 GMT
  2. Package jalview.gui

File TreeCanvas.java

 

Coverage histogram

../../img/srcFileCovDistChart5.png
43% of files have more coverage

Code metrics

330
666
54
1
2,157
1,469
279
0.42
12.33
54
5.17

Classes

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