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 }