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-2024.
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.ui;
39  
40  import static ffx.numerics.math.DoubleMath.bondAngle;
41  import static ffx.numerics.math.DoubleMath.dihedralAngle;
42  import static ffx.numerics.math.DoubleMath.dist;
43  import static java.lang.String.format;
44  import static org.apache.commons.math3.util.FastMath.toDegrees;
45  
46  import ffx.potential.MolecularAssembly;
47  import ffx.potential.bonded.Atom;
48  import ffx.potential.bonded.BondedTerm;
49  import ffx.potential.bonded.MSNode;
50  import ffx.potential.bonded.Molecule;
51  import ffx.potential.bonded.Polymer;
52  import ffx.potential.bonded.RendererCache;
53  import ffx.potential.bonded.Residue;
54  import ffx.ui.behaviors.PickMouseBehavior;
55  
56  import java.util.Hashtable;
57  import java.util.List;
58  import java.util.Vector;
59  import java.util.logging.Logger;
60  import javax.swing.tree.TreePath;
61  
62  import org.jogamp.java3d.Bounds;
63  import org.jogamp.java3d.BranchGroup;
64  import org.jogamp.java3d.Node;
65  import org.jogamp.java3d.SceneGraphPath;
66  import org.jogamp.java3d.Shape3D;
67  import org.jogamp.java3d.Transform3D;
68  import org.jogamp.java3d.utils.picking.PickCanvas;
69  import org.jogamp.java3d.utils.picking.PickIntersection;
70  import org.jogamp.java3d.utils.picking.PickResult;
71  import org.jogamp.vecmath.Vector3d;
72  
73  /**
74   * The GraphicsPicking class is used to make selections and measurements.
75   *
76   * @author Michael J. Schnieders
77   */
78  public class GraphicsPicking extends PickMouseBehavior {
79  
80    /**
81     * Constant <code>pickLevelHash</code>
82     */
83    static final Hashtable<String, PickLevel> pickLevelHash = new Hashtable<>();
84  
85    private static final Logger logger = Logger.getLogger(GraphicsPicking.class.getName());
86  
87    static {
88      PickLevel[] values = PickLevel.values();
89      for (PickLevel value : values) {
90        pickLevelHash.put(value.toString(), value);
91      }
92    }
93  
94    private final MainPanel mainPanel;
95    // Turn On/Off picking
96    private boolean picking = false;
97    // Picking Level
98    private PickLevel pickLevel = PickLevel.PICKATOM;
99    private PickLevel newPickLevel = PickLevel.PICKATOM;
100   // Previously picked Atom
101   private Atom previousAtom = null;
102   // Number of times the previousAtom has been picked consecutively
103   private int pickNumber = 0;
104   // Previously picked MSNode
105   private MSNode previousPick = null;
106   // Selected Atoms for Measuring
107   private final Vector<Atom> atomCache = new Vector<>(4);
108   private int count = 0;
109   // A few arrays for reuse
110   private final double[] a = new double[3];
111   private final double[] b = new double[3];
112   private final double[] c = new double[3];
113   private final double[] d = new double[3];
114   private final Transform3D systemTransform3D = new Transform3D();
115   private final Vector3d systemPosition = new Vector3d();
116   private final Vector3d atomPosition = new Vector3d();
117 
118   /**
119    * Constructor
120    *
121    * @param base           Base of the Scenegraph
122    * @param bounds         Behavior bounds
123    * @param graphicsCanvas Scene Canvas3D
124    * @param mainPanel      MainPanel
125    */
126   public GraphicsPicking(
127       BranchGroup base, Bounds bounds, GraphicsCanvas graphicsCanvas, MainPanel mainPanel) {
128     super(graphicsCanvas, base, bounds);
129     this.mainPanel = mainPanel;
130     pickCanvas.setMode(PickCanvas.GEOMETRY);
131     pickCanvas.setTolerance(3.0f);
132   }
133 
134   /**
135    * Clear currently selected nodes
136    */
137   public void clear() {
138     if (previousPick != null) {
139       mainPanel.getHierarchy().collapsePath(new TreePath(previousPick.getPath()));
140       previousPick.setSelected(false);
141       previousPick.setColor(RendererCache.ColorModel.SELECT, null, null);
142       previousPick = null;
143       pickNumber = 0;
144     }
145     for (Atom a : atomCache) {
146       a.setSelected(false);
147       a.setColor(RendererCache.ColorModel.SELECT, null, null);
148     }
149     atomCache.clear();
150   }
151 
152   /**
153    * Getter for the field <code>picking</code>.
154    *
155    * @return a boolean.
156    */
157   public boolean getPicking() {
158     return picking;
159   }
160 
161   /**
162    * Setter for the field <code>picking</code>.
163    *
164    * @param m a boolean.
165    */
166   public void setPicking(boolean m) {
167     picking = m;
168     if (!picking) {
169       clear();
170     }
171   }
172 
173   /**
174    * {@inheritDoc}
175    *
176    * <p>Called by Java3D when an atom is picked
177    */
178   public void updateScene(int xpos, int ypos) {
179     if (!picking) {
180       return;
181     }
182     // Determine what FNode was picked
183     pickCanvas.setShapeLocation(xpos, ypos);
184     PickResult result = pickCanvas.pickClosest();
185     if (result != null) {
186       SceneGraphPath sceneGraphPath = result.getSceneGraphPath();
187       Node node = sceneGraphPath.getObject();
188       if (!(node instanceof Shape3D pickedShape3D)) {
189         return;
190       }
191       Object userData = pickedShape3D.getUserData();
192       if (userData instanceof MolecularAssembly) {
193         FFXSystem sys = (FFXSystem) userData;
194         if (result.numIntersections() > 0) {
195           PickIntersection pickIntersection = result.getIntersection(0);
196           int[] coords = pickIntersection.getPrimitiveCoordinateIndices();
197           userData = sys.getAtomFromWireVertex(coords[0]);
198         } else {
199           return;
200         }
201       }
202       if (userData instanceof Atom atom) {
203         // Check to see if the pickLevel has changed
204         if (!(pickLevel == newPickLevel)) {
205           pickLevel = newPickLevel;
206           pickNumber = 0;
207         }
208         // Clear selections between measurements
209         String pickLevelString = pickLevel.toString();
210         boolean measure = pickLevelString.startsWith("MEASURE");
211         if (!measure || count == 0) {
212           for (Atom matom : atomCache) {
213             matom.setSelected(false);
214             matom.setColor(RendererCache.ColorModel.SELECT, null, null);
215           }
216           atomCache.clear();
217           count = 0;
218         }
219         // If measuring, select the current atom and add it to the cache
220         if (measure && !atomCache.contains(atom)) {
221           atomCache.add(0, atom);
222           atom.setSelected(true);
223           atom.setColor(RendererCache.ColorModel.PICK, null, null);
224           count++;
225           measure();
226         }
227         if (!measure) {
228           // Check to see if the same Atom has been selected twice in a row.
229           // This allows iteration through the atom's terms.
230           if (atom == previousAtom) {
231             pickNumber++;
232           } else {
233             previousAtom = atom;
234             pickNumber = 0;
235           }
236           MSNode currentPick = null;
237           switch (pickLevel) {
238             case PICKATOM:
239               currentPick = atom;
240               break;
241             case PICKBOND:
242             case PICKANGLE:
243             case PICKDIHEDRAL:
244               List<? extends BondedTerm> terms;
245               if (pickLevel == PickLevel.PICKBOND) {
246                 terms = atom.getBonds();
247               } else if (pickLevel == PickLevel.PICKANGLE) {
248                 terms = atom.getAngles();
249               } else {
250                 terms = atom.getTorsions();
251               }
252               if (terms == null) {
253                 return;
254               }
255               int num = terms.size();
256               if (pickNumber >= num) {
257                 pickNumber = 0;
258               }
259               currentPick = terms.get(pickNumber);
260               break;
261             case PICKRESIDUE:
262             case PICKPOLYMER:
263             case PICKMOLECULE:
264             case PICKSYSTEM:
265               MSNode dataNode;
266               if (pickLevel == PickLevel.PICKRESIDUE) {
267                 dataNode = atom.getMSNode(Residue.class);
268               } else if (pickLevel == PickLevel.PICKPOLYMER) {
269                 dataNode = atom.getMSNode(Polymer.class);
270               } else if (pickLevel == PickLevel.PICKSYSTEM) {
271                 dataNode = atom.getMSNode(MolecularAssembly.class);
272               } else {
273                 dataNode = atom.getMSNode(Molecule.class);
274                 if (dataNode == null) {
275                   dataNode = atom.getMSNode(Polymer.class);
276                 }
277               }
278               currentPick = dataNode;
279               break;
280             case MEASUREANGLE:
281             case MEASUREDIHEDRAL:
282             case MEASUREDISTANCE:
283               break;
284           }
285           // Add the selected node to the Tree View
286           if (currentPick != null) {
287             if (controlButton) {
288               mainPanel.getHierarchy().toggleSelection(currentPick);
289             } else if (currentPick != previousPick) {
290               mainPanel.getHierarchy().onlySelection(currentPick);
291             }
292             // Color the Current Pick by Picking Color
293             mainPanel
294                 .getGraphics3D()
295                 .updateScene(currentPick, false, false, null, true, RendererCache.ColorModel.PICK);
296           }
297           // Remove picking color from the previousPick
298           if (previousPick != null && previousPick != currentPick) {
299             previousPick.setColor(RendererCache.ColorModel.REVERT, null, null);
300           }
301           previousPick = currentPick;
302         }
303       }
304     }
305   }
306 
307   private void distance(Atom atom, double[] pos) {
308     MolecularAssembly m = atom.getMSNode(MolecularAssembly.class);
309     m.getTransformGroup().getTransform(systemTransform3D);
310     systemTransform3D.get(systemPosition);
311     systemTransform3D.setScale(1.0d);
312     systemTransform3D.setTranslation(new Vector3d(0, 0, 0));
313     atom.getV3D(atomPosition);
314     systemTransform3D.transform(atomPosition);
315     atomPosition.add(systemPosition);
316     atomPosition.get(pos);
317   }
318 
319   /**
320    * getPick
321    *
322    * @return a {@link ffx.potential.bonded.MSNode} object.
323    */
324   MSNode getPick() {
325     return previousPick;
326   }
327 
328   /**
329    * Getter for the field <code>pickLevel</code>.
330    *
331    * @return a {@link java.lang.String} object.
332    */
333   String getPickLevel() {
334     return pickLevel.toString();
335   }
336 
337   /**
338    * Setter for the field <code>pickLevel</code>.
339    *
340    * @param newPick a {@link java.lang.String} object.
341    */
342   void setPickLevel(String newPick) {
343     if (pickLevelHash.containsKey(newPick.toUpperCase())) {
344       newPickLevel = pickLevelHash.get(newPick.toUpperCase());
345     }
346   }
347 
348   private void measure() {
349     String measurement;
350     double value;
351     Atom a1, a2, a3, a4;
352     switch (pickLevel) {
353       case MEASUREDISTANCE -> {
354         if (atomCache.size() < 2) {
355           return;
356         }
357         a1 = atomCache.get(0);
358         a2 = atomCache.get(1);
359         distance(a1, a);
360         distance(a2, b);
361         value = dist(a, b);
362         measurement = "\nDistance\t" + a1.getIndex() + ", " + a2.getIndex() + ":   \t" + format("%10.5f", value);
363       }
364       case MEASUREANGLE -> {
365         if (atomCache.size() < 3) {
366           return;
367         }
368         a1 = atomCache.get(0);
369         a2 = atomCache.get(1);
370         a3 = atomCache.get(2);
371         distance(a1, a);
372         distance(a2, b);
373         distance(a3, c);
374         value = bondAngle(a, b, c);
375         value = toDegrees(value);
376         measurement = "\nAngle\t" + a1.getIndex() + ", " + a2.getIndex() + ", " + a3.getIndex() + ":   \t" + format("%10.5f", value);
377       }
378       case MEASUREDIHEDRAL -> {
379         if (atomCache.size() < 4) {
380           return;
381         }
382         a1 = atomCache.get(0);
383         a2 = atomCache.get(1);
384         a3 = atomCache.get(2);
385         a4 = atomCache.get(3);
386         distance(a1, a);
387         distance(a2, b);
388         distance(a3, c);
389         distance(a4, d);
390         value = dihedralAngle(a, b, c, d);
391         value = toDegrees(value);
392         measurement = "\nDihedral\t" + a1.getIndex() + ", " + a2.getIndex() + ", " + a3.getIndex()
393             + ", " + a4.getIndex() + ":\t" + format("%10.5f", value);
394       }
395       default -> {
396         return;
397       }
398     }
399     logger.info(measurement);
400     ModelingShell modelingShell = mainPanel.getModelingShell();
401     modelingShell.setMeasurement(measurement, value);
402     count = 0;
403   }
404 
405   /**
406    * resetCount
407    */
408   void resetCount() {
409     count = 0;
410   }
411 
412   public enum PickLevel {
413     PICKATOM,
414     PICKBOND,
415     PICKANGLE,
416     PICKDIHEDRAL,
417     PICKRESIDUE,
418     PICKMOLECULE,
419     PICKPOLYMER,
420     PICKSYSTEM,
421     MEASUREDISTANCE,
422     MEASUREANGLE,
423     MEASUREDIHEDRAL
424   }
425 }