View Javadoc
1   // ******************************************************************************
2   //
3   // Title:       Force Field X.
4   // Description: Force Field X - Software for Molecular Biophysics.
5   // Copyright:   Copyright (c) Michael J. Schnieders 2001-2025.
6   //
7   // This file is part of Force Field X.
8   //
9   // Force Field X is free software; you can redistribute it and/or modify it
10  // under the terms of the GNU General Public License version 3 as published by
11  // the Free Software Foundation.
12  //
13  // Force Field X is distributed in the hope that it will be useful, but WITHOUT
14  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15  // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16  // details.
17  //
18  // You should have received a copy of the GNU General Public License along with
19  // Force Field X; if not, write to the Free Software Foundation, Inc., 59 Temple
20  // Place, Suite 330, Boston, MA 02111-1307 USA
21  //
22  // Linking this library statically or dynamically with other modules is making a
23  // combined work based on this library. Thus, the terms and conditions of the
24  // GNU General Public License cover the whole combination.
25  //
26  // As a special exception, the copyright holders of this library give you
27  // permission to link this library with independent modules to produce an
28  // executable, regardless of the license terms of these independent modules, and
29  // to copy and distribute the resulting executable under terms of your choice,
30  // provided that you also meet, for each linked independent module, the terms
31  // and conditions of the license of that module. An independent module is a
32  // module which is not derived from or based on this library. If you modify this
33  // library, you may extend this exception to your version of the library, but
34  // you are not obligated to do so. If you do not wish to do so, delete this
35  // exception statement from your version.
36  //
37  // ******************************************************************************
38  package ffx.numerics.clustering.visualization;
39  
40  import ffx.numerics.clustering.Cluster;
41  
42  import java.awt.FontMetrics;
43  import java.awt.Graphics2D;
44  import java.awt.geom.Rectangle2D;
45  import java.util.ArrayList;
46  import java.util.List;
47  
48  /**
49   * Visual component representing a single cluster node within a dendrogram.
50   * Responsible for rendering the node, its label, and the connector lines to
51   * its parent/children based on virtual coordinates.
52   *
53   * <p>Used by DendrogramPanel to draw hierarchical clustering results.</p>
54   *
55   * @author Lars Behnke, 2013
56   * @author Michael J. Schnieders
57   * @since 1.0
58   */
59  public class ClusterComponent implements Paintable {
60  
61    private Cluster cluster;
62    private VCoord linkPoint;
63    private VCoord initPoint;
64    private boolean printName;
65    private int dotRadius = 2;
66    private int namePadding = 6;
67  
68    private List<ClusterComponent> children;
69  
70    /**
71     * Returns the child visual components corresponding to child clusters.
72     *
73     * @return list of child ClusterComponents (lazy-initialized)
74     */
75    public List<ClusterComponent> getChildren() {
76      if (children == null) {
77        children = new ArrayList<>();
78      }
79      return children;
80    }
81  
82    /**
83     * Gets the pixel padding between a leaf node and its name text.
84     *
85     * @return name padding in pixels
86     */
87    public int getNamePadding() {
88      return namePadding;
89    }
90  
91    /**
92     * Sets the pixel padding between a leaf node and its name text.
93     *
94     * @param namePadding name padding in pixels
95     */
96    public void setNamePadding(int namePadding) {
97      this.namePadding = namePadding;
98    }
99  
100   /**
101    * Gets the radius of node dots in pixels.
102    *
103    * @return dot radius in pixels
104    */
105   public int getDotRadius() {
106     return dotRadius;
107   }
108 
109   /**
110    * Sets the radius of node dots in pixels.
111    *
112    * @param dotRadius dot radius in pixels
113    */
114   public void setDotRadius(int dotRadius) {
115     this.dotRadius = dotRadius;
116   }
117 
118   /**
119    * Sets the list of child visual components.
120    *
121    * @param children list of child components
122    */
123   public void setChildren(List<ClusterComponent> children) {
124     this.children = children;
125   }
126 
127   /**
128    * Gets the virtual coordinate where this node connects to its parent.
129    *
130    * @return link point coordinate
131    */
132   public VCoord getLinkPoint() {
133     return linkPoint;
134   }
135 
136   /**
137    * Sets the virtual coordinate where this node connects to its parent.
138    *
139    * @param linkPoint link point coordinate
140    */
141   public void setLinkPoint(VCoord linkPoint) {
142     this.linkPoint = linkPoint;
143   }
144 
145   /**
146    * Gets the virtual coordinate at which this node is drawn.
147    *
148    * @return initial coordinate for this node
149    */
150   public VCoord getInitPoint() {
151     return initPoint;
152   }
153 
154   /**
155    * Sets the virtual coordinate at which this node is drawn.
156    *
157    * @param initPoint initial coordinate for this node
158    */
159   public void setInitPoint(VCoord initPoint) {
160     this.initPoint = initPoint;
161   }
162 
163   /**
164    * Gets the Cluster model represented by this component.
165    *
166    * @return the associated Cluster
167    */
168   public Cluster getCluster() {
169     return cluster;
170   }
171 
172   /**
173    * Sets the Cluster model represented by this component.
174    *
175    * @param cluster the Cluster to associate with this component
176    */
177   public void setCluster(Cluster cluster) {
178     this.cluster = cluster;
179   }
180 
181   /**
182    * Returns whether the node name should be drawn.
183    *
184    * @return true if the name should be drawn
185    */
186   public boolean isPrintName() {
187     return printName;
188   }
189 
190   /**
191    * Sets whether the node name should be drawn.
192    *
193    * @param printName true to draw the node name
194    */
195   public void setPrintName(boolean printName) {
196     this.printName = printName;
197   }
198 
199   /**
200    * Constructs a visual node component for a Cluster.
201    *
202    * @param cluster   the cluster represented by this component
203    * @param printName whether to render the cluster name
204    * @param initPoint the initial virtual coordinate of this node
205    */
206   public ClusterComponent(Cluster cluster, boolean printName, VCoord initPoint) {
207     this.printName = printName;
208     this.cluster = cluster;
209     this.initPoint = initPoint;
210     this.linkPoint = initPoint;
211   }
212 
213   /**
214    * {@inheritDoc}
215    * Draws the node dot, horizontal and vertical connectors, and optionally labels.
216    */
217   @Override
218   public void paint(Graphics2D g, int xDisplayOffset, int yDisplayOffset, double xDisplayFactor, double yDisplayFactor, boolean decorated) {
219     int x1, y1, x2, y2;
220     FontMetrics fontMetrics = g.getFontMetrics();
221     x1 = (int) (initPoint.x() * xDisplayFactor + xDisplayOffset);
222     y1 = (int) (initPoint.y() * yDisplayFactor + yDisplayOffset);
223     x2 = (int) (linkPoint.x() * xDisplayFactor + xDisplayOffset);
224     y2 = y1;
225     g.fillOval(x1 - dotRadius, y1 - dotRadius, dotRadius * 2, dotRadius * 2);
226     g.drawLine(x1, y1, x2, y2);
227 
228     if (cluster.isLeaf()) {
229       g.drawString(cluster.getName(), x1 + namePadding, y1 + (fontMetrics.getHeight() / 2) - 2);
230     }
231     if (decorated && cluster.getDistance() != null && !cluster.getDistance().isNaN() && cluster.getDistance().getDistance() > 0) {
232       String s = String.format("%.2f", cluster.getDistance().getDistance());
233       Rectangle2D rect = fontMetrics.getStringBounds(s, g);
234       g.drawString(s, x1 - (int) rect.getWidth(), y1 - 2);
235     }
236 
237     x1 = x2;
238     y1 = y2;
239     y2 = (int) (linkPoint.y() * yDisplayFactor + yDisplayOffset);
240     g.drawLine(x1, y1, x2, y2);
241 
242 
243     for (ClusterComponent child : children) {
244       child.paint(g, xDisplayOffset, yDisplayOffset, xDisplayFactor, yDisplayFactor, decorated);
245     }
246   }
247 
248   /**
249    * Computes the minimal X value of this component and its children in model space.
250    *
251    * @return minimal X coordinate across subtree
252    */
253   public double getRectMinX() {
254 
255     // TODO Better use closure / callback here
256     assert initPoint != null && linkPoint != null;
257     double val = Math.min(initPoint.x(), linkPoint.x());
258     for (ClusterComponent child : getChildren()) {
259       val = Math.min(val, child.getRectMinX());
260     }
261     return val;
262   }
263 
264   /**
265    * Computes the minimal Y value of this component and its children in model space.
266    *
267    * @return minimal Y coordinate across subtree
268    */
269   public double getRectMinY() {
270 
271     // TODO Better use closure here
272     assert initPoint != null && linkPoint != null;
273     double val = Math.min(initPoint.y(), linkPoint.y());
274     for (ClusterComponent child : getChildren()) {
275       val = Math.min(val, child.getRectMinY());
276     }
277     return val;
278   }
279 
280   /**
281    * Computes the maximal X value of this component and its children in model space.
282    *
283    * @return maximal X coordinate across subtree
284    */
285   public double getRectMaxX() {
286 
287     // TODO Better use closure here
288     assert initPoint != null && linkPoint != null;
289     double val = Math.max(initPoint.x(), linkPoint.x());
290     for (ClusterComponent child : getChildren()) {
291       val = Math.max(val, child.getRectMaxX());
292     }
293     return val;
294   }
295 
296   /**
297    * Computes the maximal Y value of this component and its children in model space.
298    *
299    * @return maximal Y coordinate across subtree
300    */
301   public double getRectMaxY() {
302 
303     // TODO Better use closure here
304     assert initPoint != null && linkPoint != null;
305     double val = Math.max(initPoint.y(), linkPoint.y());
306     for (ClusterComponent child : getChildren()) {
307       val = Math.max(val, child.getRectMaxY());
308     }
309     return val;
310   }
311 
312   /**
313    * Computes the width in pixels of this node's name label.
314    *
315    * @param g               graphics context used for font metrics
316    * @param includeNonLeafs if true, include internal nodes; otherwise only leaf names
317    * @return width in pixels of the label (0 if not drawn)
318    */
319   public int getNameWidth(Graphics2D g, boolean includeNonLeafs) {
320     int width = 0;
321     if (includeNonLeafs || cluster.isLeaf()) {
322       Rectangle2D rect = g.getFontMetrics().getStringBounds(cluster.getName(), g);
323       width = (int) rect.getWidth();
324     }
325     return width;
326   }
327 
328   /**
329    * Recursively computes the maximal name width across this node and its children.
330    *
331    * @param g               graphics context used for font metrics
332    * @param includeNonLeafs if true, include internal nodes; otherwise only leaf names
333    * @return maximum label width in pixels across the subtree
334    */
335   public int getMaxNameWidth(Graphics2D g, boolean includeNonLeafs) {
336     int width = getNameWidth(g, includeNonLeafs);
337     for (ClusterComponent comp : getChildren()) {
338       int childWidth = comp.getMaxNameWidth(g, includeNonLeafs);
339       if (childWidth > width) {
340         width = childWidth;
341       }
342     }
343     return width;
344   }
345 }