2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
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.
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.
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.
21 package jalview.ext.rbvi.chimera;
23 import jalview.api.AlignmentViewPanel;
24 import jalview.api.structures.JalviewStructureDisplayI;
25 import jalview.bin.Cache;
26 import jalview.datamodel.AlignmentI;
27 import jalview.datamodel.HiddenColumns;
28 import jalview.datamodel.PDBEntry;
29 import jalview.datamodel.SearchResultMatchI;
30 import jalview.datamodel.SearchResultsI;
31 import jalview.datamodel.SequenceFeature;
32 import jalview.datamodel.SequenceI;
33 import jalview.gui.Preferences;
34 import jalview.gui.StructureViewer.ViewerType;
35 import jalview.httpserver.AbstractRequestHandler;
36 import jalview.io.DataSourceType;
37 import jalview.structure.AtomSpec;
38 import jalview.structure.StructureMappingcommandSet;
39 import jalview.structure.StructureSelectionManager;
40 import jalview.structures.models.AAStructureBindingModel;
41 import jalview.util.MessageManager;
44 import java.io.FileOutputStream;
45 import java.io.IOException;
46 import java.io.PrintWriter;
47 import java.net.BindException;
48 import java.util.ArrayList;
49 import java.util.BitSet;
50 import java.util.Collections;
51 import java.util.Iterator;
52 import java.util.LinkedHashMap;
53 import java.util.List;
56 import ext.edu.ucsf.rbvi.strucviz2.ChimeraManager;
57 import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel;
58 import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
59 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
61 public abstract class JalviewChimeraBinding extends AAStructureBindingModel
63 public static final String CHIMERA_FEATURE_GROUP = "Chimera";
65 // Chimera clause to exclude alternate locations in atom selection
66 private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9";
68 private static final boolean debug = false;
70 private static final String PHOSPHORUS = "P";
72 private static final String ALPHACARBON = "CA";
75 * Object through which we talk to Chimera
77 private ChimeraManager chimeraManager;
80 * Object which listens to Chimera notifications
82 private AbstractRequestHandler chimeraListener;
85 * set if chimera state is being restored from some source - instructs binding
86 * not to apply default display style when structure set is updated for first
89 private boolean loadingFromArchive = false;
92 * flag to indicate if the Chimera viewer should ignore sequence colouring
93 * events from the structure manager because the GUI is still setting up
95 private boolean loadingFinished = true;
98 * Map of ChimeraModel objects keyed by PDB full local file name
100 private Map<String, List<ChimeraModel>> chimeraMaps = new LinkedHashMap<>();
102 String lastHighlightCommand;
105 * incremented every time a load notification is successfully handled -
106 * lightweight mechanism for other threads to detect when they can start
107 * referring to new structures.
109 private long loadNotifiesHandled = 0;
111 private Thread chimeraMonitor;
114 * Open a PDB structure file in Chimera and set up mappings from Jalview.
116 * We check if the PDB model id is already loaded in Chimera, if so don't reopen
117 * it. This is the case if Chimera has opened a saved session file.
122 public boolean openFile(PDBEntry pe)
124 String file = pe.getFile();
127 List<ChimeraModel> modelsToMap = new ArrayList<>();
128 List<ChimeraModel> oldList = chimeraManager.getModelList();
129 boolean alreadyOpen = false;
132 * If Chimera already has this model, don't reopen it, but do remap it.
134 for (ChimeraModel open : oldList)
136 if (open.getModelName().equals(pe.getId()))
139 modelsToMap.add(open);
144 * If Chimera doesn't yet have this model, ask it to open it, and retrieve
145 * the model name(s) added by Chimera.
149 chimeraManager.openModel(file, pe.getId(), ModelType.PDB_MODEL);
150 if (chimeraManager.isChimeraX())
153 * ChimeraX hack: force chimera model name to pdbId
155 int modelNumber = chimeraMaps.size() + 1;
156 String command = "setattr #" + modelNumber + " models name "
158 executeCommand(command, false);
159 modelsToMap.add(new ChimeraModel(pe.getId(), ModelType.PDB_MODEL,
165 * Chimera: query for actual models and find the one with
166 * matching model name - set in viewer.openModel()
168 List<ChimeraModel> newList = chimeraManager.getModelList();
169 // JAL-1728 newList.removeAll(oldList) does not work
170 for (ChimeraModel cm : newList)
172 if (cm.getModelName().equals(pe.getId()))
180 chimeraMaps.put(file, modelsToMap);
182 if (getSsm() != null)
184 getSsm().addStructureViewerListener(this);
187 } catch (Exception q)
189 log("Exception when trying to open model " + file + "\n"
204 public JalviewChimeraBinding(StructureSelectionManager ssm,
205 PDBEntry[] pdbentry, SequenceI[][] sequenceIs,
206 DataSourceType protocol)
208 super(ssm, pdbentry, sequenceIs, protocol);
209 chimeraManager = new ChimeraManager(new StructureManager(true));
210 String viewerType = Cache.getProperty(Preferences.STRUCTURE_DISPLAY);
211 chimeraManager.setChimeraX(ViewerType.CHIMERAX.name().equals(viewerType));
212 setStructureCommands(new ChimeraCommands());
216 * Starts a thread that waits for the Chimera process to finish, so that we can
217 * then close the associated resources. This avoids leaving orphaned Chimera
218 * viewer panels in Jalview if the user closes Chimera.
220 protected void startChimeraProcessMonitor()
222 final Process p = chimeraManager.getChimeraProcess();
223 chimeraMonitor = new Thread(new Runnable()
232 JalviewStructureDisplayI display = getViewer();
235 display.closeViewer(false);
237 } catch (InterruptedException e)
239 // exit thread if Chimera Viewer is closed in Jalview
243 chimeraMonitor.start();
247 * Start a dedicated HttpServer to listen for Chimera notifications, and tell it
250 public void startChimeraListener()
254 chimeraListener = new ChimeraListener(this);
255 chimeraManager.startListening(chimeraListener.getUri());
256 } catch (BindException e)
259 "Failed to start Chimera listener: " + e.getMessage());
264 * Close down the Jalview viewer and listener, and (optionally) the associated
267 public void closeViewer(boolean closeChimera)
269 getSsm().removeStructureViewerListener(this, this.getStructureFiles());
272 chimeraManager.exitChimera();
274 if (this.chimeraListener != null)
276 chimeraListener.shutdown();
277 chimeraListener = null;
279 chimeraManager = null;
281 if (chimeraMonitor != null)
283 chimeraMonitor.interrupt();
285 releaseUIResources();
292 public String superposeStructures(AlignmentI[] _alignment,
293 int[] _refStructure, HiddenColumns[] _hiddenCols)
295 StringBuilder allComs = new StringBuilder(128);
296 String[] files = getStructureFiles();
298 if (!waitForFileLoad(files))
304 StringBuilder selectioncom = new StringBuilder(256);
305 boolean chimeraX = chimeraManager.isChimeraX();
306 for (int a = 0; a < _alignment.length; a++)
308 int refStructure = _refStructure[a];
309 AlignmentI alignment = _alignment[a];
310 HiddenColumns hiddenCols = _hiddenCols[a];
312 if (refStructure >= files.length)
314 System.err.println("Ignoring invalid reference structure value "
320 * 'matched' bit i will be set for visible alignment columns i where
321 * all sequences have a residue with a mapping to the PDB structure
323 BitSet matched = new BitSet();
324 for (int m = 0; m < alignment.getWidth(); m++)
326 if (hiddenCols == null || hiddenCols.isVisible(m))
332 SuperposeData[] structures = new SuperposeData[files.length];
333 for (int f = 0; f < files.length; f++)
335 structures[f] = new SuperposeData(alignment.getWidth());
339 * Calculate the superposable alignment columns ('matched'), and the
340 * corresponding structure residue positions (structures.pdbResNo)
342 int candidateRefStructure = findSuperposableResidues(alignment,
343 matched, structures);
344 if (refStructure < 0)
347 * If no reference structure was specified, pick the first one that has
348 * a mapping in the alignment
350 refStructure = candidateRefStructure;
353 int nmatched = matched.cardinality();
356 return MessageManager.formatMessage("label.insufficient_residues",
361 * Generate select statements to select regions to superimpose structures
363 String[] selcom = new String[files.length];
364 for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
366 final int modelNo = pdbfnum + (chimeraX ? 1 : 0);
367 // todo correct resolution to model number
368 String chainCd = "." + structures[pdbfnum].chain;
371 StringBuilder molsel = new StringBuilder();
374 molsel.append("/" + structures[pdbfnum].chain + ":");
377 int nextColumnMatch = matched.nextSetBit(0);
378 while (nextColumnMatch != -1)
380 int pdbResNum = structures[pdbfnum].pdbResNo[nextColumnMatch];
381 if (lpos != pdbResNum - 1)
384 * discontiguous - append last residue now
388 molsel.append(String.valueOf(lpos));
391 molsel.append(chainCd);
400 * extending a contiguous run
405 * start the range selection
407 molsel.append(String.valueOf(lpos));
413 nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
417 * and terminate final selection
421 molsel.append(String.valueOf(lpos));
424 molsel.append(chainCd);
427 if (molsel.length() > 1)
429 selcom[pdbfnum] = molsel.toString();
430 selectioncom.append("#").append(String.valueOf(modelNo));
433 selectioncom.append(":");
435 selectioncom.append(selcom[pdbfnum]);
436 // selectioncom.append(" ");
437 if (pdbfnum < files.length - 1)
439 selectioncom.append("|");
444 selcom[pdbfnum] = null;
448 StringBuilder command = new StringBuilder(256);
449 for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
451 final int modelNo = pdbfnum + (chimeraX ? 1 : 0);
452 if (pdbfnum == refStructure || selcom[pdbfnum] == null
453 || selcom[refStructure] == null)
457 if (command.length() > 0)
463 * Form Chimera match command, from the 'new' structure to the
464 * 'reference' structure e.g. (50 residues, chain B/A, alphacarbons):
466 * match #1:1-30.B,81-100.B@CA #0:21-40.A,61-90.A@CA
469 * https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/match.html
471 command.append(chimeraX ? "align " : "match ");
472 command.append(getModelSpec(modelNo));
477 command.append(selcom[pdbfnum]);
478 command.append("@").append(
479 structures[pdbfnum].isRna ? PHOSPHORUS : ALPHACARBON);
480 // JAL-1757 exclude alternate CA locations - ChimeraX syntax tbd
483 command.append(NO_ALTLOCS);
485 command.append(chimeraX ? " toAtoms " : " ")
486 .append(getModelSpec(refStructure + (chimeraX ? 1 : 0)));
491 command.append(selcom[refStructure]);
492 command.append("@").append(
493 structures[refStructure].isRna ? PHOSPHORUS : ALPHACARBON);
496 command.append(NO_ALTLOCS);
499 if (selectioncom.length() > 0)
503 System.out.println("Select regions:\n" + selectioncom.toString());
505 "Superimpose command(s):\n" + command.toString());
507 // allComs.append("~display all; ");
510 // allComs.append("show ").append(selectioncom.toString())
511 // .append(" pbonds");
515 // allComs.append("chain @CA|P; ribbon ");
516 // allComs.append(selectioncom.toString());
518 if (allComs.length() > 0) {
521 allComs.append(command.toString());
526 if (selectioncom.length() > 0)
528 // TODO: visually distinguish regions that were superposed
529 if (selectioncom.substring(selectioncom.length() - 1).equals("|"))
531 selectioncom.setLength(selectioncom.length() - 1);
535 System.out.println("Select regions:\n" + selectioncom.toString());
537 allComs.append(";~display all; ");
540 allComs.append("show @CA|P pbonds; show ")
541 .append(selectioncom.toString()).append(" ribbons; view");
545 allComs.append("chain @CA|P; ribbon ; focus");
546 allComs.append(selectioncom.toString());
548 // allComs.append("; ~display all; chain @CA|P; ribbon ")
549 // .append(selectioncom.toString()).append("; focus");
550 List<String> chimeraReplies = executeCommand(allComs.toString(),
552 for (String reply : chimeraReplies)
554 if (reply.toLowerCase().contains("unequal numbers of atoms"))
564 * Helper method to construct model spec in Chimera format:
566 * <li>#0 (#1 etc) for a PDB file with no sub-models</li>
567 * <li>#0.1 (#1.1 etc) for a PDB file with sub-models</li>
569 * Note for now we only ever choose the first of multiple models. This
570 * corresponds to the hard-coded Jmol equivalent (compare {1.1}). Refactor in
571 * future if there is a need to select specific sub-models.
576 protected String getModelSpec(int pdbfnum)
578 if (pdbfnum < 0 || pdbfnum >= getPdbCount())
580 return "#" + pdbfnum; // temp hack for ChimeraX
584 * For now, the test for having sub-models is whether multiple Chimera
585 * models are mapped for the PDB file; the models are returned as a response
586 * to the Chimera command 'list models type molecule', see
587 * ChimeraManager.getModelList().
589 List<ChimeraModel> maps = chimeraMaps.get(getStructureFiles()[pdbfnum]);
590 boolean hasSubModels = maps != null && maps.size() > 1;
591 return "#" + String.valueOf(pdbfnum) + (hasSubModels ? ".1" : "");
595 * Launch Chimera, unless an instance linked to this object is already
596 * running. Returns true if Chimera is successfully launched, or already
597 * running, else false.
601 public boolean launchChimera()
603 if (chimeraManager.isChimeraLaunched())
608 boolean launched = chimeraManager.launchChimera(
609 StructureManager.getChimeraPaths(chimeraManager.isChimeraX()));
612 startChimeraProcessMonitor();
616 log("Failed to launch Chimera!");
622 * Answers true if the Chimera process is still running, false if ended or not
627 public boolean isChimeraRunning()
629 return chimeraManager.isChimeraLaunched();
633 * Send a command to Chimera, and optionally log and return any responses.
635 * Does nothing, and returns null, if the command is the same as the last one
642 public List<String> executeCommand(final String command,
645 if (chimeraManager == null || command == null)
647 // ? thread running after viewer shut down
650 List<String> reply = null;
651 viewerCommandHistory(false);
652 if (true /*lastCommand == null || !lastCommand.equals(command)*/)
654 // trim command or it may never find a match in the replyLog!!
655 List<String> lastReply = chimeraManager.sendChimeraCommand(command.trim(),
662 log("Response from command ('" + command + "') was:\n"
667 viewerCommandHistory(true);
673 * Send a Chimera command asynchronously in a new thread. If the progress
674 * message is not null, display this message while the command is executing.
679 protected abstract void sendAsynchronousCommand(String command,
685 protected void executeWhenReady(String command)
688 executeCommand(command, false);
692 private void waitForChimera()
694 while (chimeraManager != null && chimeraManager.isBusy())
699 } catch (InterruptedException q)
705 // End StructureListener
706 // //////////////////////////
709 * instruct the Jalview binding to update the pdbentries vector if necessary
710 * prior to matching the viewer's contents to the list of structure files
711 * Jalview knows about.
713 public abstract void refreshPdbEntries();
716 * map between index of model filename returned from getPdbFile and the first
717 * index of models from this file in the viewer. Note - this is not trimmed -
718 * use getPdbFile to get number of unique models.
720 private int _modelFileNameMap[];
722 // ////////////////////////////////
723 // /StructureListener
725 public synchronized String[] getStructureFiles()
727 if (chimeraManager == null)
729 return new String[0];
732 return chimeraMaps.keySet()
733 .toArray(modelFileNames = new String[chimeraMaps.size()]);
737 * Construct and send a command to highlight zero, one or more atoms. We do this
738 * by sending an "rlabel" command to show the residue label at that position.
741 public void highlightAtoms(List<AtomSpec> atoms)
743 if (atoms == null || atoms.size() == 0)
748 boolean forChimeraX = chimeraManager.isChimeraX();
749 StringBuilder cmd = new StringBuilder(128);
750 boolean first = true;
751 boolean found = false;
753 for (AtomSpec atom : atoms)
755 int pdbResNum = atom.getPdbResNum();
756 String chain = atom.getChain();
757 String pdbfile = atom.getPdbFile();
758 List<ChimeraModel> cms = chimeraMaps.get(pdbfile);
759 if (cms != null && !cms.isEmpty())
763 cmd.append(forChimeraX ? "label #" : "rlabel #");
772 cmd.append(cms.get(0).getModelNumber())
773 .append("/").append(chain).append(":").append(pdbResNum);
777 cmd.append(cms.get(0).getModelNumber())
778 .append(":").append(pdbResNum);
779 if (!chain.equals(" ") && !forChimeraX)
781 cmd.append(".").append(chain);
787 String command = cmd.toString();
790 * avoid repeated commands for the same residue
792 if (command.equals(lastHighlightCommand))
798 * unshow the label for the previous residue
800 if (lastHighlightCommand != null)
802 chimeraManager.sendChimeraCommand("~" + lastHighlightCommand, false);
806 chimeraManager.sendChimeraCommand(command, false);
808 this.lastHighlightCommand = command;
812 * Query Chimera for its current selection, and highlight it on the alignment
814 public void highlightChimeraSelection()
817 * Ask Chimera for its current selection
819 List<String> selection = chimeraManager.getSelectedResidueSpecs();
822 * Parse model number, residue and chain for each selected position,
823 * formatted as #0:123.A or #1.2:87.B (#model.submodel:residue.chain)
825 List<AtomSpec> atomSpecs = convertStructureResiduesToAlignment(
829 * Broadcast the selection (which may be empty, if the user just cleared all
832 getSsm().mouseOverStructure(atomSpecs);
836 * Converts a list of Chimera atomspecs to a list of AtomSpec representing the
837 * corresponding residues (if any) in Jalview
839 * @param structureSelection
842 protected List<AtomSpec> convertStructureResiduesToAlignment(
843 List<String> structureSelection)
845 boolean chimeraX = chimeraManager.isChimeraX();
846 List<AtomSpec> atomSpecs = new ArrayList<>();
847 for (String atomSpec : structureSelection)
851 AtomSpec spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX);
852 String pdbfilename = getPdbFileForModel(spec.getModelNumber());
853 spec.setPdbFile(pdbfilename);
855 } catch (IllegalArgumentException e)
857 System.err.println("Failed to parse atomspec: " + atomSpec);
867 protected String getPdbFileForModel(int modelId)
870 * Work out the pdbfilename from the model number
872 String pdbfilename = modelFileNames[0];
873 findfileloop: for (String pdbfile : this.chimeraMaps.keySet())
875 for (ChimeraModel cm : chimeraMaps.get(pdbfile))
877 if (cm.getModelNumber() == modelId)
879 pdbfilename = pdbfile;
887 private void log(String message)
889 System.err.println("## Chimera log: " + message);
892 private void viewerCommandHistory(boolean enable)
894 // log("(Not yet implemented) History "
895 // + ((debug || enable) ? "on" : "off"));
898 public long getLoadNotifiesHandled()
900 return loadNotifiesHandled;
904 * called when the binding thinks the UI needs to be refreshed after a Chimera
905 * state change. this could be because structures were loaded, or because an
906 * error has occurred.
908 public abstract void refreshGUI();
911 public void setLoadingFromArchive(boolean loadingFromArchive)
913 this.loadingFromArchive = loadingFromArchive;
918 * @return true if Chimeral is still restoring state or loading is still going
919 * on (see setFinsihedLoadingFromArchive)
922 public boolean isLoadingFromArchive()
924 return loadingFromArchive && !loadingFinished;
928 * modify flag which controls if sequence colouring events are honoured by the
929 * binding. Should be true for normal operation
931 * @param finishedLoading
934 public void setFinishedLoadingFromArchive(boolean finishedLoading)
936 loadingFinished = finishedLoading;
940 * Ask Chimera to save its session to the given file. Returns true if
941 * successful, else false.
946 public boolean saveSession(String filepath)
948 if (isChimeraRunning())
951 * Chimera: https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/save.html
952 * ChimeraX: https://www.cgl.ucsf.edu/chimerax/docs/user/commands/save.html
954 String command = isChimeraX() ? "save session " : "save ";
955 List<String> reply = chimeraManager.sendChimeraCommand(command + filepath,
957 if (reply.contains("Session written"))
964 .error("Error saving Chimera session: " + reply.toString());
971 * Ask Chimera to open a session file. Returns true if successful, else false.
972 * The filename must have a .py (Chimera) or .cxs (ChimeraX) extension for
973 * this command to work.
978 public boolean openSession(String filepath)
981 * Chimera: https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/open.html
982 * ChimeraX: https://www.cgl.ucsf.edu/chimerax/docs/user/commands/open.html
984 executeCommand("open " + filepath, true);
985 // todo: test for failure - how?
990 * Send a 'show' command for all atoms in the currently selected columns
992 * TODO: pull up to abstract structure viewer interface
996 public void highlightSelection(AlignmentViewPanel vp)
998 List<Integer> cols = vp.getAlignViewport().getColumnSelection()
1000 AlignmentI alignment = vp.getAlignment();
1001 StructureSelectionManager sm = getSsm();
1002 for (SequenceI seq : alignment.getSequences())
1005 * convert selected columns into sequence positions
1007 int[] positions = new int[cols.size()];
1009 for (Integer col : cols)
1011 positions[i++] = seq.findPosition(col);
1013 sm.highlightStructure(this, seq, positions);
1018 * Constructs and send commands to Chimera to set attributes on residues for
1019 * features visible in Jalview
1024 public int sendFeaturesToViewer(AlignmentViewPanel avp)
1026 // TODO refactor as required to pull up to an interface
1027 AlignmentI alignment = avp.getAlignment();
1029 String[] files = getStructureFiles();
1035 StructureMappingcommandSet commandSet = ChimeraCommands
1036 .getSetAttributeCommandsForFeatures(getSsm(), files,
1037 getSequence(), avp, chimeraManager.isChimeraX());
1038 String[] commands = commandSet.commands;
1039 if (commands.length > 10)
1041 sendCommandsByFile(commands);
1045 for (String command : commands)
1047 sendAsynchronousCommand(command, null);
1050 return commands.length;
1054 * Write commands to a temporary file, and send a command to Chimera to open the
1055 * file as a commands script. For use when sending a large number of separate
1056 * commands would overload the REST interface mechanism.
1060 protected void sendCommandsByFile(String[] commands)
1062 boolean toChimeraX = chimeraManager.isChimeraX();
1065 File tmp = File.createTempFile("chim", toChimeraX ? ".cxc" : ".com");
1067 PrintWriter out = new PrintWriter(new FileOutputStream(tmp));
1068 for (String command : commands)
1070 out.println(command);
1074 String path = tmp.getAbsolutePath();
1075 String command = "open " + (toChimeraX ? "" : "cmd:") + path;
1076 sendAsynchronousCommand(command, null);
1077 } catch (IOException e)
1079 System.err.println("Sending commands to Chimera via file failed with "
1085 * Get Chimera residues which have the named attribute, find the mapped
1086 * positions in the Jalview sequence(s), and set as sequence features
1089 * @param alignmentPanel
1091 public void copyStructureAttributesToFeatures(String attName,
1092 AlignmentViewPanel alignmentPanel)
1094 // todo pull up to AAStructureBindingModel (and interface?)
1097 * ask Chimera to list residues with the attribute, reporting its value
1099 // this alternative command
1100 // list residues spec ':*/attName' attr attName
1101 // doesn't report 'None' values (which is good), but
1102 // fails for 'average.bfactor' (which is bad):
1104 String cmd = "list residues attr '" + attName + "'";
1105 List<String> residues = executeCommand(cmd, true);
1107 boolean featureAdded = createFeaturesForAttributes(attName, residues);
1110 alignmentPanel.getFeatureRenderer().featuresAdded();
1115 * Create features in Jalview for the given attribute name and structure
1119 * The residue list should be 0, 1 or more reply lines of the format:
1120 * residue id #0:5.A isHelix -155.000836316 index 5
1122 * residue id #0:6.A isHelix None
1129 protected boolean createFeaturesForAttributes(String attName,
1130 List<String> residues)
1132 boolean featureAdded = false;
1133 String featureGroup = getViewerFeatureGroup();
1134 boolean chimeraX = chimeraManager.isChimeraX();
1136 for (String residue : residues)
1138 AtomSpec spec = null;
1139 String[] tokens = residue.split(" ");
1140 if (tokens.length < 5)
1144 String atomSpec = tokens[2];
1145 String attValue = tokens[4];
1148 * ignore 'None' (e.g. for phi) or 'False' (e.g. for isHelix)
1150 if ("None".equalsIgnoreCase(attValue)
1151 || "False".equalsIgnoreCase(attValue))
1158 spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX);
1159 } catch (IllegalArgumentException e)
1161 System.err.println("Problem parsing atomspec " + atomSpec);
1165 String chainId = spec.getChain();
1166 String description = attValue;
1167 float score = Float.NaN;
1170 score = Float.valueOf(attValue);
1171 description = chainId;
1172 } catch (NumberFormatException e)
1174 // was not a float value
1177 String pdbFile = getPdbFileForModel(spec.getModelNumber());
1178 spec.setPdbFile(pdbFile);
1180 List<AtomSpec> atoms = Collections.singletonList(spec);
1183 * locate the mapped position in the alignment (if any)
1185 SearchResultsI sr = getSsm()
1186 .findAlignmentPositionsForStructurePositions(atoms);
1189 * expect one matched alignment position, or none
1190 * (if the structure position is not mapped)
1192 for (SearchResultMatchI m : sr.getResults())
1194 SequenceI seq = m.getSequence();
1195 int start = m.getStart();
1196 int end = m.getEnd();
1197 SequenceFeature sf = new SequenceFeature(attName, description,
1198 start, end, score, featureGroup);
1199 // todo: should SequenceFeature have an explicit property for chain?
1200 // note: repeating the action shouldn't duplicate features
1201 featureAdded |= seq.addSequenceFeature(sf);
1204 return featureAdded;
1208 * Answers the feature group name to apply to features created in Jalview from
1209 * Chimera attributes
1213 protected String getViewerFeatureGroup()
1215 // todo pull up to interface
1216 return CHIMERA_FEATURE_GROUP;
1220 public int getModelNoForFile(String pdbFile)
1222 List<ChimeraModel> foundModels = chimeraMaps.get(pdbFile);
1223 if (foundModels != null && !foundModels.isEmpty())
1225 return foundModels.get(0).getModelNumber();
1231 * Answers a (possibly empty) list of attribute names in Chimera[X], excluding
1232 * any which were added from Jalview
1236 public List<String> getChimeraAttributes()
1238 List<String> atts = chimeraManager.getAttrList();
1239 Iterator<String> it = atts.iterator();
1240 while (it.hasNext())
1242 if (it.next().startsWith(ChimeraCommands.NAMESPACE_PREFIX))
1245 * attribute added from Jalview - exclude it
1253 public boolean isChimeraX()
1255 return chimeraManager.isChimeraX();