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.SequenceRenderer;
25 import jalview.api.structures.JalviewStructureDisplayI;
26 import jalview.bin.Cache;
27 import jalview.datamodel.AlignmentI;
28 import jalview.datamodel.HiddenColumns;
29 import jalview.datamodel.PDBEntry;
30 import jalview.datamodel.SearchResultMatchI;
31 import jalview.datamodel.SearchResultsI;
32 import jalview.datamodel.SequenceFeature;
33 import jalview.datamodel.SequenceI;
34 import jalview.gui.Preferences;
35 import jalview.gui.StructureViewer.ViewerType;
36 import jalview.httpserver.AbstractRequestHandler;
37 import jalview.io.DataSourceType;
38 import jalview.schemes.ColourSchemeI;
39 import jalview.schemes.ResidueProperties;
40 import jalview.structure.AtomSpec;
41 import jalview.structure.StructureMappingcommandSet;
42 import jalview.structure.StructureSelectionManager;
43 import jalview.structures.models.AAStructureBindingModel;
44 import jalview.util.MessageManager;
46 import java.awt.Color;
48 import java.io.FileOutputStream;
49 import java.io.IOException;
50 import java.io.PrintWriter;
51 import java.net.BindException;
52 import java.util.ArrayList;
53 import java.util.BitSet;
54 import java.util.Collections;
55 import java.util.Hashtable;
56 import java.util.Iterator;
57 import java.util.LinkedHashMap;
58 import java.util.List;
61 import ext.edu.ucsf.rbvi.strucviz2.ChimeraManager;
62 import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel;
63 import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
64 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
66 public abstract class JalviewChimeraBinding extends AAStructureBindingModel
68 public static final String CHIMERA_FEATURE_GROUP = "Chimera";
70 // Chimera clause to exclude alternate locations in atom selection
71 private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9";
73 private static final String COLOURING_CHIMERA = MessageManager
74 .getString("status.colouring_chimera");
76 private static final boolean debug = false;
78 private static final String PHOSPHORUS = "P";
80 private static final String ALPHACARBON = "CA";
82 private List<String> chainNames = new ArrayList<>();
84 private Hashtable<String, String> chainFile = new Hashtable<>();
87 * Object through which we talk to Chimera
89 private ChimeraManager viewer;
92 * Object which listens to Chimera notifications
94 private AbstractRequestHandler chimeraListener;
97 * set if chimera state is being restored from some source - instructs binding
98 * not to apply default display style when structure set is updated for first
101 private boolean loadingFromArchive = false;
104 * flag to indicate if the Chimera viewer should ignore sequence colouring
105 * events from the structure manager because the GUI is still setting up
107 private boolean loadingFinished = true;
110 * Map of ChimeraModel objects keyed by PDB full local file name
112 private Map<String, List<ChimeraModel>> chimeraMaps = new LinkedHashMap<>();
114 String lastHighlightCommand;
117 * incremented every time a load notification is successfully handled -
118 * lightweight mechanism for other threads to detect when they can start
119 * referring to new structures.
121 private long loadNotifiesHandled = 0;
123 private Thread chimeraMonitor;
126 * Open a PDB structure file in Chimera and set up mappings from Jalview.
128 * We check if the PDB model id is already loaded in Chimera, if so don't reopen
129 * it. This is the case if Chimera has opened a saved session file.
134 public boolean openFile(PDBEntry pe)
136 String file = pe.getFile();
139 List<ChimeraModel> modelsToMap = new ArrayList<>();
140 List<ChimeraModel> oldList = viewer.isChimeraX() ? new ArrayList<>()
141 : viewer.getModelList();
142 boolean alreadyOpen = false;
145 * If Chimera already has this model, don't reopen it, but do remap it.
147 for (ChimeraModel open : oldList)
149 if (open.getModelName().equals(pe.getId()))
152 modelsToMap.add(open);
157 * If Chimera doesn't yet have this model, ask it to open it, and retrieve
158 * the model name(s) added by Chimera.
162 viewer.openModel(file, pe.getId(), ModelType.PDB_MODEL);
163 if (viewer.isChimeraX())
166 * ChimeraX hack: force chimera model name to pdbId
168 int modelNumber = chimeraMaps.size() + 1;
169 String command = "setattr #" + modelNumber + " models name "
171 sendChimeraCommand(command, false);
172 modelsToMap.add(new ChimeraModel(pe.getId(), ModelType.PDB_MODEL,
178 * Chimera: query for actual models and find the one with
179 * matching model name - set in viewer.openModel()
181 List<ChimeraModel> newList = viewer.getModelList();
182 // JAL-1728 newList.removeAll(oldList) does not work
183 for (ChimeraModel cm : newList)
185 if (cm.getModelName().equals(pe.getId()))
193 chimeraMaps.put(file, modelsToMap);
195 if (getSsm() != null)
197 getSsm().addStructureViewerListener(this);
200 } catch (Exception q)
202 log("Exception when trying to open model " + file + "\n"
217 public JalviewChimeraBinding(StructureSelectionManager ssm,
218 PDBEntry[] pdbentry, SequenceI[][] sequenceIs,
219 DataSourceType protocol)
221 super(ssm, pdbentry, sequenceIs, protocol);
222 viewer = new ChimeraManager(new StructureManager(true));
223 String viewerType = Cache.getProperty(Preferences.STRUCTURE_DISPLAY);
224 viewer.setChimeraX(ViewerType.CHIMERAX.name().equals(viewerType));
229 * Starts a thread that waits for the Chimera process to finish, so that we can
230 * then close the associated resources. This avoids leaving orphaned Chimera
231 * viewer panels in Jalview if the user closes Chimera.
233 protected void startChimeraProcessMonitor()
235 final Process p = viewer.getChimeraProcess();
236 chimeraMonitor = new Thread(new Runnable()
245 JalviewStructureDisplayI display = getViewer();
248 display.closeViewer(false);
250 } catch (InterruptedException e)
252 // exit thread if Chimera Viewer is closed in Jalview
256 chimeraMonitor.start();
260 * Start a dedicated HttpServer to listen for Chimera notifications, and tell it
263 public void startChimeraListener()
267 chimeraListener = new ChimeraListener(this);
268 viewer.startListening(chimeraListener.getUri());
269 } catch (BindException e)
272 "Failed to start Chimera listener: " + e.getMessage());
277 * Tells Chimera to display only the specified chains
281 public void showChains(List<String> toshow)
284 * Construct a chimera command like
286 * ~display #*;~ribbon #*;ribbon :.A,:.B
288 StringBuilder cmd = new StringBuilder(64);
289 boolean first = true;
290 for (String chain : toshow)
292 int modelNumber = getModelNoForChain(chain);
293 String showChainCmd = modelNumber == -1 ? ""
294 : modelNumber + ":." + chain.split(":")[1];
299 cmd.append(showChainCmd);
304 * could append ";focus" to this command to resize the display to fill the
305 * window, but it looks more helpful not to (easier to relate chains to the
308 final String command = "~display #*; ~ribbon #*; ribbon :"
310 sendChimeraCommand(command, false);
314 * Close down the Jalview viewer and listener, and (optionally) the associated
317 public void closeViewer(boolean closeChimera)
319 getSsm().removeStructureViewerListener(this, this.getStructureFiles());
322 viewer.exitChimera();
324 if (this.chimeraListener != null)
326 chimeraListener.shutdown();
327 chimeraListener = null;
331 if (chimeraMonitor != null)
333 chimeraMonitor.interrupt();
335 releaseUIResources();
339 public void colourByChain()
341 colourBySequence = false;
342 sendAsynchronousCommand("rainbow chain", COLOURING_CHIMERA);
346 * Constructs and sends a Chimera command to colour by charge
348 * <li>Aspartic acid and Glutamic acid (negative charge) red</li>
349 * <li>Lysine and Arginine (positive charge) blue</li>
350 * <li>Cysteine - yellow</li>
351 * <li>all others - white</li>
355 public void colourByCharge()
357 colourBySequence = false;
358 String command = viewer.isChimeraX()
359 ? "color white;color :ASP,GLU red;color :LYS,ARG blue;color :CYS yellow"
360 : "color white;color red ::ASP;color red ::GLU;color blue ::LYS;color blue ::ARG;color yellow ::CYS";
361 sendAsynchronousCommand(command, COLOURING_CHIMERA);
368 public String superposeStructures(AlignmentI[] _alignment,
369 int[] _refStructure, HiddenColumns[] _hiddenCols)
371 StringBuilder allComs = new StringBuilder(128);
372 String[] files = getStructureFiles();
374 if (!waitForFileLoad(files))
380 StringBuilder selectioncom = new StringBuilder(256);
381 boolean chimeraX = viewer.isChimeraX();
382 for (int a = 0; a < _alignment.length; a++)
384 int refStructure = _refStructure[a];
385 AlignmentI alignment = _alignment[a];
386 HiddenColumns hiddenCols = _hiddenCols[a];
388 if (refStructure >= files.length)
390 System.err.println("Ignoring invalid reference structure value "
396 * 'matched' bit i will be set for visible alignment columns i where
397 * all sequences have a residue with a mapping to the PDB structure
399 BitSet matched = new BitSet();
400 for (int m = 0; m < alignment.getWidth(); m++)
402 if (hiddenCols == null || hiddenCols.isVisible(m))
408 SuperposeData[] structures = new SuperposeData[files.length];
409 for (int f = 0; f < files.length; f++)
411 structures[f] = new SuperposeData(alignment.getWidth());
415 * Calculate the superposable alignment columns ('matched'), and the
416 * corresponding structure residue positions (structures.pdbResNo)
418 int candidateRefStructure = findSuperposableResidues(alignment,
419 matched, structures);
420 if (refStructure < 0)
423 * If no reference structure was specified, pick the first one that has
424 * a mapping in the alignment
426 refStructure = candidateRefStructure;
429 int nmatched = matched.cardinality();
432 return MessageManager.formatMessage("label.insufficient_residues",
437 * Generate select statements to select regions to superimpose structures
439 String[] selcom = new String[files.length];
440 for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
442 final int modelNo = pdbfnum + (chimeraX ? 1 : 0);
443 // todo correct resolution to model number
444 String chainCd = "." + structures[pdbfnum].chain;
447 StringBuilder molsel = new StringBuilder();
450 molsel.append("/" + structures[pdbfnum].chain + ":");
453 int nextColumnMatch = matched.nextSetBit(0);
454 while (nextColumnMatch != -1)
456 int pdbResNum = structures[pdbfnum].pdbResNo[nextColumnMatch];
457 if (lpos != pdbResNum - 1)
460 * discontiguous - append last residue now
464 molsel.append(String.valueOf(lpos));
467 molsel.append(chainCd);
476 * extending a contiguous run
481 * start the range selection
483 molsel.append(String.valueOf(lpos));
489 nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
493 * and terminate final selection
497 molsel.append(String.valueOf(lpos));
500 molsel.append(chainCd);
503 if (molsel.length() > 1)
505 selcom[pdbfnum] = molsel.toString();
506 selectioncom.append("#").append(String.valueOf(modelNo));
509 selectioncom.append(":");
511 selectioncom.append(selcom[pdbfnum]);
512 // selectioncom.append(" ");
513 if (pdbfnum < files.length - 1)
515 selectioncom.append("|");
520 selcom[pdbfnum] = null;
524 StringBuilder command = new StringBuilder(256);
525 for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
527 final int modelNo = pdbfnum + (chimeraX ? 1 : 0);
528 if (pdbfnum == refStructure || selcom[pdbfnum] == null
529 || selcom[refStructure] == null)
533 if (command.length() > 0)
539 * Form Chimera match command, from the 'new' structure to the
540 * 'reference' structure e.g. (50 residues, chain B/A, alphacarbons):
542 * match #1:1-30.B,81-100.B@CA #0:21-40.A,61-90.A@CA
545 * https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/match.html
547 command.append(chimeraX ? "align " : "match ");
548 command.append(getModelSpec(modelNo));
553 command.append(selcom[pdbfnum]);
554 command.append("@").append(
555 structures[pdbfnum].isRna ? PHOSPHORUS : ALPHACARBON);
556 // JAL-1757 exclude alternate CA locations - ChimeraX syntax tbd
559 command.append(NO_ALTLOCS);
561 command.append(chimeraX ? " toAtoms " : " ")
562 .append(getModelSpec(refStructure + (chimeraX ? 1 : 0)));
567 command.append(selcom[refStructure]);
568 command.append("@").append(
569 structures[refStructure].isRna ? PHOSPHORUS : ALPHACARBON);
572 command.append(NO_ALTLOCS);
575 if (selectioncom.length() > 0)
579 System.out.println("Select regions:\n" + selectioncom.toString());
581 "Superimpose command(s):\n" + command.toString());
583 // allComs.append("~display all; ");
586 // allComs.append("show ").append(selectioncom.toString())
587 // .append(" pbonds");
591 // allComs.append("chain @CA|P; ribbon ");
592 // allComs.append(selectioncom.toString());
594 if (allComs.length() > 0) {
597 allComs.append(command.toString());
602 if (selectioncom.length() > 0)
604 // TODO: visually distinguish regions that were superposed
605 if (selectioncom.substring(selectioncom.length() - 1).equals("|"))
607 selectioncom.setLength(selectioncom.length() - 1);
611 System.out.println("Select regions:\n" + selectioncom.toString());
613 allComs.append(";~display all; ");
616 allComs.append("show @CA|P pbonds; show ")
617 .append(selectioncom.toString()).append(" ribbons; view");
621 allComs.append("chain @CA|P; ribbon ; focus");
622 allComs.append(selectioncom.toString());
624 // allComs.append("; ~display all; chain @CA|P; ribbon ")
625 // .append(selectioncom.toString()).append("; focus");
626 List<String> chimeraReplies = sendChimeraCommand(allComs.toString(),
628 for (String reply : chimeraReplies)
630 if (reply.toLowerCase().contains("unequal numbers of atoms"))
640 * Helper method to construct model spec in Chimera format:
642 * <li>#0 (#1 etc) for a PDB file with no sub-models</li>
643 * <li>#0.1 (#1.1 etc) for a PDB file with sub-models</li>
645 * Note for now we only ever choose the first of multiple models. This
646 * corresponds to the hard-coded Jmol equivalent (compare {1.1}). Refactor in
647 * future if there is a need to select specific sub-models.
652 protected String getModelSpec(int pdbfnum)
654 if (pdbfnum < 0 || pdbfnum >= getPdbCount())
656 return "#" + pdbfnum; // temp hack for ChimeraX
660 * For now, the test for having sub-models is whether multiple Chimera
661 * models are mapped for the PDB file; the models are returned as a response
662 * to the Chimera command 'list models type molecule', see
663 * ChimeraManager.getModelList().
665 List<ChimeraModel> maps = chimeraMaps.get(getStructureFiles()[pdbfnum]);
666 boolean hasSubModels = maps != null && maps.size() > 1;
667 return "#" + String.valueOf(pdbfnum) + (hasSubModels ? ".1" : "");
671 * Launch Chimera, unless an instance linked to this object is already
672 * running. Returns true if Chimera is successfully launched, or already
673 * running, else false.
677 public boolean launchChimera()
679 if (viewer.isChimeraLaunched())
684 boolean launched = viewer.launchChimera(
685 StructureManager.getChimeraPaths(viewer.isChimeraX()));
688 startChimeraProcessMonitor();
692 log("Failed to launch Chimera!");
698 * Answers true if the Chimera process is still running, false if ended or not
703 public boolean isChimeraRunning()
705 return viewer.isChimeraLaunched();
709 * Send a command to Chimera, and optionally log and return any responses.
711 * Does nothing, and returns null, if the command is the same as the last one
717 public List<String> sendChimeraCommand(final String command,
722 // ? thread running after viewer shut down
725 List<String> reply = null;
726 viewerCommandHistory(false);
727 if (true /*lastCommand == null || !lastCommand.equals(command)*/)
729 // trim command or it may never find a match in the replyLog!!
730 List<String> lastReply = viewer.sendChimeraCommand(command.trim(),
737 log("Response from command ('" + command + "') was:\n"
742 viewerCommandHistory(true);
748 * Send a Chimera command asynchronously in a new thread. If the progress
749 * message is not null, display this message while the command is executing.
754 protected abstract void sendAsynchronousCommand(String command,
758 * Sends a set of colour commands to the structure viewer
760 * @param colourBySequenceCommands
763 protected void colourBySequence(
764 StructureMappingcommandSet[] colourBySequenceCommands)
766 for (StructureMappingcommandSet cpdbbyseq : colourBySequenceCommands)
768 for (String command : cpdbbyseq.commands)
770 sendAsynchronousCommand(command, COLOURING_CHIMERA);
782 protected StructureMappingcommandSet[] getColourBySequenceCommands(
783 String[] files, SequenceRenderer sr, AlignmentViewPanel viewPanel)
785 return ChimeraCommands.getColourBySequenceCommand(getSsm(), files,
786 getSequence(), sr, viewPanel, viewer.isChimeraX());
792 protected void executeWhenReady(String command)
795 sendChimeraCommand(command, false);
799 private void waitForChimera()
801 while (viewer != null && viewer.isBusy())
806 } catch (InterruptedException q)
812 // End StructureListener
813 // //////////////////////////
816 * instruct the Jalview binding to update the pdbentries vector if necessary
817 * prior to matching the viewer's contents to the list of structure files
818 * Jalview knows about.
820 public abstract void refreshPdbEntries();
823 * map between index of model filename returned from getPdbFile and the first
824 * index of models from this file in the viewer. Note - this is not trimmed -
825 * use getPdbFile to get number of unique models.
827 private int _modelFileNameMap[];
829 // ////////////////////////////////
830 // /StructureListener
832 public synchronized String[] getStructureFiles()
836 return new String[0];
839 return chimeraMaps.keySet()
840 .toArray(modelFileNames = new String[chimeraMaps.size()]);
844 * Construct and send a command to highlight zero, one or more atoms. We do this
845 * by sending an "rlabel" command to show the residue label at that position.
848 public void highlightAtoms(List<AtomSpec> atoms)
850 if (atoms == null || atoms.size() == 0)
855 boolean forChimeraX = viewer.isChimeraX();
856 StringBuilder cmd = new StringBuilder(128);
857 boolean first = true;
858 boolean found = false;
860 for (AtomSpec atom : atoms)
862 int pdbResNum = atom.getPdbResNum();
863 String chain = atom.getChain();
864 String pdbfile = atom.getPdbFile();
865 List<ChimeraModel> cms = chimeraMaps.get(pdbfile);
866 if (cms != null && !cms.isEmpty())
870 cmd.append(forChimeraX ? "label #" : "rlabel #");
879 cmd.append(cms.get(0).getModelNumber())
880 .append("/").append(chain).append(":").append(pdbResNum);
884 cmd.append(cms.get(0).getModelNumber())
885 .append(":").append(pdbResNum);
886 if (!chain.equals(" ") && !forChimeraX)
888 cmd.append(".").append(chain);
894 String command = cmd.toString();
897 * avoid repeated commands for the same residue
899 if (command.equals(lastHighlightCommand))
905 * unshow the label for the previous residue
907 if (lastHighlightCommand != null)
909 viewer.sendChimeraCommand("~" + lastHighlightCommand, false);
913 viewer.sendChimeraCommand(command, false);
915 this.lastHighlightCommand = command;
919 * Query Chimera for its current selection, and highlight it on the alignment
921 public void highlightChimeraSelection()
924 * Ask Chimera for its current selection
926 List<String> selection = viewer.getSelectedResidueSpecs();
929 * Parse model number, residue and chain for each selected position,
930 * formatted as #0:123.A or #1.2:87.B (#model.submodel:residue.chain)
932 List<AtomSpec> atomSpecs = convertStructureResiduesToAlignment(
936 * Broadcast the selection (which may be empty, if the user just cleared all
939 getSsm().mouseOverStructure(atomSpecs);
943 * Converts a list of Chimera atomspecs to a list of AtomSpec representing the
944 * corresponding residues (if any) in Jalview
946 * @param structureSelection
949 protected List<AtomSpec> convertStructureResiduesToAlignment(
950 List<String> structureSelection)
952 boolean chimeraX = viewer.isChimeraX();
953 List<AtomSpec> atomSpecs = new ArrayList<>();
954 for (String atomSpec : structureSelection)
958 AtomSpec spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX);
959 String pdbfilename = getPdbFileForModel(spec.getModelNumber());
960 spec.setPdbFile(pdbfilename);
962 } catch (IllegalArgumentException e)
964 System.err.println("Failed to parse atomspec: " + atomSpec);
974 protected String getPdbFileForModel(int modelId)
977 * Work out the pdbfilename from the model number
979 String pdbfilename = modelFileNames[0];
980 findfileloop: for (String pdbfile : this.chimeraMaps.keySet())
982 for (ChimeraModel cm : chimeraMaps.get(pdbfile))
984 if (cm.getModelNumber() == modelId)
986 pdbfilename = pdbfile;
994 private void log(String message)
996 System.err.println("## Chimera log: " + message);
999 private void viewerCommandHistory(boolean enable)
1001 // log("(Not yet implemented) History "
1002 // + ((debug || enable) ? "on" : "off"));
1005 public long getLoadNotifiesHandled()
1007 return loadNotifiesHandled;
1011 public void setJalviewColourScheme(ColourSchemeI cs)
1013 colourBySequence = false;
1020 viewerCommandHistory(false);
1021 StringBuilder command = new StringBuilder(128);
1023 List<String> residueSet = ResidueProperties.getResidues(isNucleotide(),
1027 * concatenate colour commands, one per residue symbol
1028 * Chimera format: color 0.000000,0.372549,0.627451 ::VAL
1029 * ChimeraX format: color :VAL rgb(73,73,182)
1031 boolean chimeraX = viewer.isChimeraX();
1032 for (String resName : residueSet)
1034 char res = resName.length() == 3
1035 ? ResidueProperties.getSingleCharacterCode(resName)
1036 : resName.charAt(0);
1037 Color col = cs.findColour(res, 0, null, null, 0f);
1038 command.append("color ");
1039 String colorSpec = getRgbDescriptor(col, chimeraX);
1042 command.append(":").append(resName).append(" ").append(colorSpec);
1046 command.append(colorSpec).append(" ::").append(resName);
1048 command.append(";");
1051 sendAsynchronousCommand(command.toString(), COLOURING_CHIMERA);
1052 viewerCommandHistory(true);
1056 * called when the binding thinks the UI needs to be refreshed after a Chimera
1057 * state change. this could be because structures were loaded, or because an
1058 * error has occurred.
1060 public abstract void refreshGUI();
1063 public void setLoadingFromArchive(boolean loadingFromArchive)
1065 this.loadingFromArchive = loadingFromArchive;
1070 * @return true if Chimeral is still restoring state or loading is still going
1071 * on (see setFinsihedLoadingFromArchive)
1074 public boolean isLoadingFromArchive()
1076 return loadingFromArchive && !loadingFinished;
1080 * modify flag which controls if sequence colouring events are honoured by the
1081 * binding. Should be true for normal operation
1083 * @param finishedLoading
1086 public void setFinishedLoadingFromArchive(boolean finishedLoading)
1088 loadingFinished = finishedLoading;
1092 * Send the Chimera 'background solid <color>" command.
1095 * ://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/background
1100 public void setBackgroundColour(Color col)
1102 viewerCommandHistory(false);
1103 String command = "set bgColor "
1104 + getRgbDescriptor(col, viewer.isChimeraX());
1105 viewer.sendChimeraCommand(command, false);
1106 viewerCommandHistory(true);
1110 * Answers the Chimera/X format for RGB values of the given colour.
1113 * Chimera: r,g,b with values scaled [0=1]
1114 * ChimeraX: rgb(r,g,b) with values scaled 0-255
1121 private static String getRgbDescriptor(Color col, boolean chimeraX)
1125 return String.format("rgb(%d,%d,%d)", col.getRed(), col.getGreen(),
1130 double scale = 255D;
1131 return String.format("%f,%f,%f", col.getRed() / scale,
1132 col.getGreen() / scale, col.getBlue() / scale);
1137 * Ask Chimera to save its session to the given file. Returns true if
1138 * successful, else false.
1143 public boolean saveSession(String filepath)
1145 if (isChimeraRunning())
1148 * Chimera: https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/save.html
1149 * ChimeraX: https://www.cgl.ucsf.edu/chimerax/docs/user/commands/save.html
1151 String command = isChimeraX() ? "save session " : "save ";
1152 List<String> reply = viewer.sendChimeraCommand(command + filepath,
1154 if (reply.contains("Session written"))
1161 .error("Error saving Chimera session: " + reply.toString());
1168 * Ask Chimera to open a session file. Returns true if successful, else false.
1169 * The filename must have a .py (Chimera) or .cxs (ChimeraX) extension for
1170 * this command to work.
1175 public boolean openSession(String filepath)
1178 * Chimera: https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/open.html
1179 * ChimeraX: https://www.cgl.ucsf.edu/chimerax/docs/user/commands/open.html
1181 sendChimeraCommand("open " + filepath, true);
1182 // todo: test for failure - how?
1187 * Returns a list of chains mapped in this viewer. Note this list is not
1188 * currently scoped per structure.
1193 public List<String> getChainNames()
1199 * Send a 'focus' command to Chimera to recentre the visible display
1201 public void focusView()
1203 sendChimeraCommand(viewer.isChimeraX() ? "view" : "focus", false);
1207 * Send a 'show' command for all atoms in the currently selected columns
1209 * TODO: pull up to abstract structure viewer interface
1213 public void highlightSelection(AlignmentViewPanel vp)
1215 List<Integer> cols = vp.getAlignViewport().getColumnSelection()
1217 AlignmentI alignment = vp.getAlignment();
1218 StructureSelectionManager sm = getSsm();
1219 for (SequenceI seq : alignment.getSequences())
1222 * convert selected columns into sequence positions
1224 int[] positions = new int[cols.size()];
1226 for (Integer col : cols)
1228 positions[i++] = seq.findPosition(col);
1230 sm.highlightStructure(this, seq, positions);
1235 * Constructs and send commands to Chimera to set attributes on residues for
1236 * features visible in Jalview
1241 public int sendFeaturesToViewer(AlignmentViewPanel avp)
1243 // TODO refactor as required to pull up to an interface
1244 AlignmentI alignment = avp.getAlignment();
1246 String[] files = getStructureFiles();
1252 StructureMappingcommandSet commandSet = ChimeraCommands
1253 .getSetAttributeCommandsForFeatures(getSsm(), files,
1254 getSequence(), avp, viewer.isChimeraX());
1255 String[] commands = commandSet.commands;
1256 if (commands.length > 10)
1258 sendCommandsByFile(commands);
1262 for (String command : commands)
1264 sendAsynchronousCommand(command, null);
1267 return commands.length;
1271 * Write commands to a temporary file, and send a command to Chimera to open the
1272 * file as a commands script. For use when sending a large number of separate
1273 * commands would overload the REST interface mechanism.
1277 protected void sendCommandsByFile(String[] commands)
1279 boolean toChimeraX = viewer.isChimeraX();
1282 File tmp = File.createTempFile("chim", toChimeraX ? ".cxc" : ".com");
1284 PrintWriter out = new PrintWriter(new FileOutputStream(tmp));
1285 for (String command : commands)
1287 out.println(command);
1291 String path = tmp.getAbsolutePath();
1292 String command = "open " + (toChimeraX ? "" : "cmd:") + path;
1293 sendAsynchronousCommand(command, null);
1294 } catch (IOException e)
1296 System.err.println("Sending commands to Chimera via file failed with "
1302 * Get Chimera residues which have the named attribute, find the mapped
1303 * positions in the Jalview sequence(s), and set as sequence features
1306 * @param alignmentPanel
1308 public void copyStructureAttributesToFeatures(String attName,
1309 AlignmentViewPanel alignmentPanel)
1311 // todo pull up to AAStructureBindingModel (and interface?)
1314 * ask Chimera to list residues with the attribute, reporting its value
1316 // this alternative command
1317 // list residues spec ':*/attName' attr attName
1318 // doesn't report 'None' values (which is good), but
1319 // fails for 'average.bfactor' (which is bad):
1321 String cmd = "list residues attr '" + attName + "'";
1322 List<String> residues = sendChimeraCommand(cmd, true);
1324 boolean featureAdded = createFeaturesForAttributes(attName, residues);
1327 alignmentPanel.getFeatureRenderer().featuresAdded();
1332 * Create features in Jalview for the given attribute name and structure
1336 * The residue list should be 0, 1 or more reply lines of the format:
1337 * residue id #0:5.A isHelix -155.000836316 index 5
1339 * residue id #0:6.A isHelix None
1346 protected boolean createFeaturesForAttributes(String attName,
1347 List<String> residues)
1349 boolean featureAdded = false;
1350 String featureGroup = getViewerFeatureGroup();
1351 boolean chimeraX = viewer.isChimeraX();
1353 for (String residue : residues)
1355 AtomSpec spec = null;
1356 String[] tokens = residue.split(" ");
1357 if (tokens.length < 5)
1361 String atomSpec = tokens[2];
1362 String attValue = tokens[4];
1365 * ignore 'None' (e.g. for phi) or 'False' (e.g. for isHelix)
1367 if ("None".equalsIgnoreCase(attValue)
1368 || "False".equalsIgnoreCase(attValue))
1375 spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX);
1376 } catch (IllegalArgumentException e)
1378 System.err.println("Problem parsing atomspec " + atomSpec);
1382 String chainId = spec.getChain();
1383 String description = attValue;
1384 float score = Float.NaN;
1387 score = Float.valueOf(attValue);
1388 description = chainId;
1389 } catch (NumberFormatException e)
1391 // was not a float value
1394 String pdbFile = getPdbFileForModel(spec.getModelNumber());
1395 spec.setPdbFile(pdbFile);
1397 List<AtomSpec> atoms = Collections.singletonList(spec);
1400 * locate the mapped position in the alignment (if any)
1402 SearchResultsI sr = getSsm()
1403 .findAlignmentPositionsForStructurePositions(atoms);
1406 * expect one matched alignment position, or none
1407 * (if the structure position is not mapped)
1409 for (SearchResultMatchI m : sr.getResults())
1411 SequenceI seq = m.getSequence();
1412 int start = m.getStart();
1413 int end = m.getEnd();
1414 SequenceFeature sf = new SequenceFeature(attName, description,
1415 start, end, score, featureGroup);
1416 // todo: should SequenceFeature have an explicit property for chain?
1417 // note: repeating the action shouldn't duplicate features
1418 featureAdded |= seq.addSequenceFeature(sf);
1421 return featureAdded;
1425 * Answers the feature group name to apply to features created in Jalview from
1426 * Chimera attributes
1430 protected String getViewerFeatureGroup()
1432 // todo pull up to interface
1433 return CHIMERA_FEATURE_GROUP;
1436 public Hashtable<String, String> getChainFile()
1441 public List<ChimeraModel> getChimeraModelByChain(String chain)
1443 return chimeraMaps.get(chainFile.get(chain));
1446 public int getModelNoForChain(String chain)
1448 List<ChimeraModel> foundModels = getChimeraModelByChain(chain);
1449 if (foundModels != null && !foundModels.isEmpty())
1451 return foundModels.get(0).getModelNumber();
1457 * Answers a (possibly empty) list of attribute names in Chimera[X], excluding
1458 * any which were added from Jalview
1462 public List<String> getChimeraAttributes()
1464 List<String> atts = viewer.getAttrList();
1465 Iterator<String> it = atts.iterator();
1466 while (it.hasNext())
1468 if (it.next().startsWith(ChimeraCommands.NAMESPACE_PREFIX))
1471 * attribute added from Jalview - exclude it
1479 public boolean isChimeraX()
1481 return viewer.isChimeraX();