X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjalview%2Fext%2Frbvi%2Fchimera%2FJalviewChimeraBinding.java;h=731ffeaa1c7a0a47d30563f5e0ed4bf325671dce;hb=9c1a9d682a2664d525bfd0f38bae861292dc3921;hp=18a614dd60a7d33725d2a5404255fc99ae27531d;hpb=acdc158f5c20ab04bb40e3b0580f3da02e4f744e;p=jalview.git diff --git a/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java b/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java index 18a614d..731ffea 100644 --- a/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java +++ b/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java @@ -21,33 +21,33 @@ package jalview.ext.rbvi.chimera; import jalview.api.AlignmentViewPanel; -import jalview.api.FeatureRenderer; -import jalview.api.SequenceRenderer; +import jalview.api.structures.JalviewStructureDisplayI; import jalview.bin.Cache; import jalview.datamodel.AlignmentI; -import jalview.datamodel.ColumnSelection; +import jalview.datamodel.HiddenColumns; import jalview.datamodel.PDBEntry; -import jalview.datamodel.SearchResults; -import jalview.datamodel.SearchResults.Match; +import jalview.datamodel.SearchResultMatchI; +import jalview.datamodel.SearchResultsI; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.gui.StructureViewer.ViewerType; import jalview.httpserver.AbstractRequestHandler; -import jalview.schemes.ColourSchemeI; -import jalview.schemes.ResidueProperties; +import jalview.io.DataSourceType; import jalview.structure.AtomSpec; -import jalview.structure.StructureMappingcommandSet; +import jalview.structure.StructureCommandsI.SuperposeData; import jalview.structure.StructureSelectionManager; import jalview.structures.models.AAStructureBindingModel; import jalview.util.MessageManager; -import java.awt.Color; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.net.BindException; import java.util.ArrayList; +import java.util.BitSet; import java.util.Collections; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -61,14 +61,9 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel { public static final String CHIMERA_FEATURE_GROUP = "Chimera"; - private static final String CHIMERA_FEATURE_PREFIX = "chim_"; - // Chimera clause to exclude alternate locations in atom selection private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9"; - private static final String COLOURING_CHIMERA = MessageManager - .getString("status.colouring_chimera"); - private static final boolean debug = false; private static final String PHOSPHORUS = "P"; @@ -78,7 +73,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel /* * Object through which we talk to Chimera */ - private ChimeraManager viewer; + private ChimeraManager chimeraManager; /* * Object which listens to Chimera notifications @@ -86,39 +81,19 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel private AbstractRequestHandler chimeraListener; /* - * set if chimera state is being restored from some source - instructs binding - * not to apply default display style when structure set is updated for first - * time. - */ - private boolean loadingFromArchive = false; - - /* - * flag to indicate if the Chimera viewer should ignore sequence colouring - * events from the structure manager because the GUI is still setting up - */ - private boolean loadingFinished = true; - - public String fileLoadingError; - - /* * Map of ChimeraModel objects keyed by PDB full local file name */ - private Map> chimeraMaps = new LinkedHashMap>(); + protected Map> chimeraMaps = new LinkedHashMap<>(); String lastHighlightCommand; - /* - * incremented every time a load notification is successfully handled - - * lightweight mechanism for other threads to detect when they can start - * referring to new structures. - */ - private long loadNotifiesHandled = 0; + private Thread chimeraMonitor; /** * Open a PDB structure file in Chimera and set up mappings from Jalview. * - * We check if the PDB model id is already loaded in Chimera, if so don't - * reopen it. This is the case if Chimera has opened a saved session file. + * We check if the PDB model id is already loaded in Chimera, if so don't reopen + * it. This is the case if Chimera has opened a saved session file. * * @param pe * @return @@ -128,8 +103,8 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel String file = pe.getFile(); try { - List modelsToMap = new ArrayList(); - List oldList = viewer.getModelList(); + List modelsToMap = new ArrayList<>(); + List oldList = chimeraManager.getModelList(); boolean alreadyOpen = false; /* @@ -150,16 +125,8 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel */ if (!alreadyOpen) { - viewer.openModel(file, pe.getId(), ModelType.PDB_MODEL); - List newList = viewer.getModelList(); - // JAL-1728 newList.removeAll(oldList) does not work - for (ChimeraModel cm : newList) - { - if (cm.getModelName().equals(pe.getId())) - { - modelsToMap.add(cm); - } - } + chimeraManager.openModel(file, pe.getId(), ModelType.PDB_MODEL); + addChimeraModel(pe, modelsToMap); } chimeraMaps.put(file, modelsToMap); @@ -167,13 +134,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel if (getSsm() != null) { getSsm().addStructureViewerListener(this); - // ssm.addSelectionListener(this); - FeatureRenderer fr = getFeatureRenderer(null); - if (fr != null) - { - fr.featuresAdded(); - } - refreshGUI(); } return true; } catch (Exception q) @@ -186,84 +146,100 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel } /** + * Adds the ChimeraModel corresponding to the given PDBEntry, based on model + * name matching PDB id + * + * @param pe + * @param modelsToMap + */ + protected void addChimeraModel(PDBEntry pe, + List modelsToMap) + { + /* + * Chimera: query for actual models and find the one with + * matching model name - already set in viewer.openModel() + */ + List newList = chimeraManager.getModelList(); + // JAL-1728 newList.removeAll(oldList) does not work + for (ChimeraModel cm : newList) + { + if (cm.getModelName().equals(pe.getId())) + { + modelsToMap.add(cm); + } + } + } + + /** * Constructor * * @param ssm * @param pdbentry * @param sequenceIs - * @param chains * @param protocol */ public JalviewChimeraBinding(StructureSelectionManager ssm, - PDBEntry[] pdbentry, SequenceI[][] sequenceIs, String[][] chains, - String protocol) + PDBEntry[] pdbentry, SequenceI[][] sequenceIs, + DataSourceType protocol) { - super(ssm, pdbentry, sequenceIs, chains, protocol); - viewer = new ChimeraManager( - new ext.edu.ucsf.rbvi.strucviz2.StructureManager(true)); + super(ssm, pdbentry, sequenceIs, protocol); + chimeraManager = new ChimeraManager(new StructureManager(true)); + chimeraManager.setChimeraX(ViewerType.CHIMERAX.equals(getViewerType())); + setStructureCommands(new ChimeraCommands()); } - /** - * Start a dedicated HttpServer to listen for Chimera notifications, and tell - * it to start listening - */ - public void startChimeraListener() + @Override + protected ViewerType getViewerType() { - try - { - chimeraListener = new ChimeraListener(this); - viewer.startListening(chimeraListener.getUri()); - } catch (BindException e) - { - System.err.println("Failed to start Chimera listener: " - + e.getMessage()); - } + return ViewerType.CHIMERA; } /** - * Construct a title string for the viewer window based on the data Jalview - * knows about - * - * @param verbose - * @return + * Starts a thread that waits for the Chimera process to finish, so that we can + * then close the associated resources. This avoids leaving orphaned Chimera + * viewer panels in Jalview if the user closes Chimera. */ - public String getViewerTitle(boolean verbose) + protected void startChimeraProcessMonitor() { - return getViewerTitle(CHIMERA_FEATURE_GROUP, verbose); + final Process p = chimeraManager.getChimeraProcess(); + chimeraMonitor = new Thread(new Runnable() + { + + @Override + public void run() + { + try + { + p.waitFor(); + JalviewStructureDisplayI display = getViewer(); + if (display != null) + { + display.closeViewer(false); + } + } catch (InterruptedException e) + { + // exit thread if Chimera Viewer is closed in Jalview + } + } + }); + chimeraMonitor.start(); } /** - * Tells Chimera to display only the specified chains - * - * @param toshow + * Start a dedicated HttpServer to listen for Chimera notifications, and tell it + * to start listening */ - public void showChains(List toshow) + public void startChimeraListener() { - /* - * Construct a chimera command like - * - * ~display #*;~ribbon #*;ribbon :.A,:.B - */ - StringBuilder cmd = new StringBuilder(64); - boolean first = true; - for (String chain : toshow) + try { - if (!first) - { - cmd.append(","); - } - cmd.append(":.").append(chain); - first = false; + chimeraListener = new ChimeraListener(this); + chimeraManager.startListening(chimeraListener.getUri()); + } catch (BindException e) + { + System.err.println( + "Failed to start Chimera listener: " + e.getMessage()); } - - /* - * could append ";focus" to this command to resize the display to fill the - * window, but it looks more helpful not to (easier to relate chains to the - * whole) - */ - final String command = "~display #*; ~ribbon #*; ribbon " - + cmd.toString(); - sendChimeraCommand(command, false); } /** @@ -272,75 +248,47 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel */ public void closeViewer(boolean closeChimera) { - getSsm().removeStructureViewerListener(this, this.getPdbFile()); + getSsm().removeStructureViewerListener(this, this.getStructureFiles()); if (closeChimera) { - viewer.exitChimera(); + chimeraManager.exitChimera(); } if (this.chimeraListener != null) { chimeraListener.shutdown(); chimeraListener = null; } - viewer = null; + chimeraManager = null; + if (chimeraMonitor != null) + { + chimeraMonitor.interrupt(); + } releaseUIResources(); } - public void colourByChain() - { - colourBySequence = false; - sendAsynchronousCommand("rainbow chain", COLOURING_CHIMERA); - } - /** - * Constructs and sends a Chimera command to colour by charge - *
    - *
  • Aspartic acid and Glutamic acid (negative charge) red
  • - *
  • Lysine and Arginine (positive charge) blue
  • - *
  • Cysteine - yellow
  • - *
  • all others - white
  • - *
+ * {@inheritDoc} */ - public void colourByCharge() - { - colourBySequence = false; - String command = "color white;color red ::ASP;color red ::GLU;color blue ::LYS;color blue ::ARG;color yellow ::CYS"; - sendAsynchronousCommand(command, COLOURING_CHIMERA); - } - - /** - * Construct and send a command to align structures against a reference - * structure, based on one or more sequence alignments - * - * @param _alignment - * an array of alignments to process - * @param _refStructure - * an array of corresponding reference structures (index into pdb - * file array); if a negative value is passed, the first PDB file - * mapped to an alignment sequence is used as the reference for - * superposition - * @param _hiddenCols - * an array of corresponding hidden columns for each alignment - */ - public void superposeStructures(AlignmentI[] _alignment, - int[] _refStructure, ColumnSelection[] _hiddenCols) + public String superposeStructures(AlignmentI[] _alignment, + int[] _refStructure, HiddenColumns[] _hiddenCols) { StringBuilder allComs = new StringBuilder(128); - String[] files = getPdbFile(); + String[] files = getStructureFiles(); if (!waitForFileLoad(files)) { - return; + return null; } refreshPdbEntries(); StringBuilder selectioncom = new StringBuilder(256); + boolean chimeraX = chimeraManager.isChimeraX(); for (int a = 0; a < _alignment.length; a++) { int refStructure = _refStructure[a]; AlignmentI alignment = _alignment[a]; - ColumnSelection hiddenCols = _hiddenCols[a]; + HiddenColumns hiddenCols = _hiddenCols[a]; if (refStructure >= files.length) { @@ -350,19 +298,22 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel } /* - * 'matched' array will hold 'true' for visible alignment columns where + * 'matched' bit i will be set for visible alignment columns i where * all sequences have a residue with a mapping to the PDB structure */ - boolean matched[] = new boolean[alignment.getWidth()]; - for (int m = 0; m < matched.length; m++) + BitSet matched = new BitSet(); + for (int m = 0; m < alignment.getWidth(); m++) { - matched[m] = (hiddenCols != null) ? hiddenCols.isVisible(m) : true; + if (hiddenCols == null || hiddenCols.isVisible(m)) + { + matched.set(m); + } } SuperposeData[] structures = new SuperposeData[files.length]; for (int f = 0; f < files.length; f++) { - structures[f] = new SuperposeData(alignment.getWidth()); + structures[f] = new SuperposeData(alignment.getWidth(), f); } /* @@ -380,17 +331,11 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel refStructure = candidateRefStructure; } - int nmatched = 0; - for (boolean b : matched) - { - if (b) - { - nmatched++; - } - } + int nmatched = matched.cardinality(); if (nmatched < 4) { - // TODO: bail out here because superposition illdefined? + return MessageManager.formatMessage("label.insufficient_residues", + nmatched); } /* @@ -399,45 +344,54 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel String[] selcom = new String[files.length]; for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) { + final int modelNo = pdbfnum + (chimeraX ? 1 : 0); + // todo correct resolution to model number String chainCd = "." + structures[pdbfnum].chain; int lpos = -1; boolean run = false; StringBuilder molsel = new StringBuilder(); - for (int r = 0; r < matched.length; r++) + if (chimeraX) + { + molsel.append("/" + structures[pdbfnum].chain + ":"); + } + + int nextColumnMatch = matched.nextSetBit(0); + while (nextColumnMatch != -1) { - if (matched[r]) + int pdbResNum = structures[pdbfnum].pdbResNo[nextColumnMatch]; + if (lpos != pdbResNum - 1) { - int pdbResNum = structures[pdbfnum].pdbResNo[r]; - if (lpos != pdbResNum - 1) + /* + * discontiguous - append last residue now + */ + if (lpos != -1) { - /* - * discontiguous - append last residue now - */ - if (lpos != -1) + molsel.append(String.valueOf(lpos)); + if (!chimeraX) { - molsel.append(String.valueOf(lpos)); molsel.append(chainCd); - molsel.append(","); } - run = false; + molsel.append(","); } - else + run = false; + } + else + { + /* + * extending a contiguous run + */ + if (!run) { /* - * extending a contiguous run + * start the range selection */ - if (!run) - { - /* - * start the range selection - */ - molsel.append(String.valueOf(lpos)); - molsel.append("-"); - } - run = true; + molsel.append(String.valueOf(lpos)); + molsel.append("-"); } - lpos = pdbResNum; + run = true; } + lpos = pdbResNum; + nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1); } /* @@ -446,18 +400,24 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel if (lpos != -1) { molsel.append(String.valueOf(lpos)); - molsel.append(chainCd); + if (!chimeraX) + { + molsel.append(chainCd); + } } if (molsel.length() > 1) { selcom[pdbfnum] = molsel.toString(); - selectioncom.append("#").append(String.valueOf(pdbfnum)) - .append(":"); + selectioncom.append("#").append(String.valueOf(modelNo)); + if (!chimeraX) + { + selectioncom.append(":"); + } selectioncom.append(selcom[pdbfnum]); - selectioncom.append(" "); + // selectioncom.append(" "); if (pdbfnum < files.length - 1) { - selectioncom.append("| "); + selectioncom.append("|"); } } else @@ -469,6 +429,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel StringBuilder command = new StringBuilder(256); for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) { + final int modelNo = pdbfnum + (chimeraX ? 1 : 0); if (pdbfnum == refStructure || selcom[pdbfnum] == null || selcom[refStructure] == null) { @@ -488,31 +449,61 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel * @see * https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/match.html */ - command.append("match ").append(getModelSpec(pdbfnum)).append(":"); + command.append(chimeraX ? "align " : "match "); + command.append(getModelSpec(modelNo)); + if (!chimeraX) + { + command.append(":"); + } command.append(selcom[pdbfnum]); command.append("@").append( structures[pdbfnum].isRna ? PHOSPHORUS : ALPHACARBON); - // JAL-1757 exclude alternate CA locations - command.append(NO_ALTLOCS); - command.append(" ").append(getModelSpec(refStructure)).append(":"); + // JAL-1757 exclude alternate CA locations - ChimeraX syntax tbd + if (!chimeraX) + { + command.append(NO_ALTLOCS); + } + command.append(chimeraX ? " toAtoms " : " ") + .append(getModelSpec(refStructure + (chimeraX ? 1 : 0))); + if (!chimeraX) + { + command.append(":"); + } command.append(selcom[refStructure]); command.append("@").append( structures[refStructure].isRna ? PHOSPHORUS : ALPHACARBON); - command.append(NO_ALTLOCS); + if (!chimeraX) + { + command.append(NO_ALTLOCS); + } } if (selectioncom.length() > 0) { if (debug) { System.out.println("Select regions:\n" + selectioncom.toString()); - System.out.println("Superimpose command(s):\n" - + command.toString()); + System.out.println( + "Superimpose command(s):\n" + command.toString()); + } + // allComs.append("~display all; "); + // if (chimeraX) + // { + // allComs.append("show ").append(selectioncom.toString()) + // .append(" pbonds"); + // } + // else + // { + // allComs.append("chain @CA|P; ribbon "); + // allComs.append(selectioncom.toString()); + // } + if (allComs.length() > 0) { + allComs.append(";"); } - allComs.append("~display all; chain @CA|P; ribbon ") - .append(selectioncom.toString()) - .append(";" + command.toString()); + allComs.append(command.toString()); } } + + String error = null; if (selectioncom.length() > 0) { // TODO: visually distinguish regions that were superposed @@ -524,11 +515,30 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel { System.out.println("Select regions:\n" + selectioncom.toString()); } - allComs.append("; ~display all; chain @CA|P; ribbon ") - .append(selectioncom.toString()).append("; focus"); - sendChimeraCommand(allComs.toString(), false); + allComs.append(";~display all; "); + if (chimeraX) + { + allComs.append("show @CA|P pbonds; show ") + .append(selectioncom.toString()).append(" ribbons; view"); + } + else + { + allComs.append("chain @CA|P; ribbon "); + allComs.append(selectioncom.toString()).append("; focus"); + } + // allComs.append("; ~display all; chain @CA|P; ribbon ") + // .append(selectioncom.toString()).append("; focus"); + List chimeraReplies = executeCommand(allComs.toString(), + true); + for (String reply : chimeraReplies) + { + if (reply.toLowerCase().contains("unequal numbers of atoms")) + { + error = reply; + } + } } - + return error; } /** @@ -548,7 +558,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel { if (pdbfnum < 0 || pdbfnum >= getPdbCount()) { - return ""; + return "#" + pdbfnum; // temp hack for ChimeraX } /* @@ -557,30 +567,45 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel * to the Chimera command 'list models type molecule', see * ChimeraManager.getModelList(). */ - List maps = chimeraMaps.get(getPdbFile()[pdbfnum]); + List maps = chimeraMaps.get(getStructureFiles()[pdbfnum]); boolean hasSubModels = maps != null && maps.size() > 1; return "#" + String.valueOf(pdbfnum) + (hasSubModels ? ".1" : ""); } /** * Launch Chimera, unless an instance linked to this object is already - * running. Returns true if chimera is successfully launched, or already + * running. Returns true if Chimera is successfully launched, or already * running, else false. * * @return */ public boolean launchChimera() { - if (!viewer.isChimeraLaunched()) + if (chimeraManager.isChimeraLaunched()) { - return viewer.launchChimera(StructureManager.getChimeraPaths()); + return true; } - if (viewer.isChimeraLaunched()) + + boolean launched = chimeraManager.launchChimera(getChimeraPaths()); + if (launched) { - return true; + startChimeraProcessMonitor(); } - log("Failed to launch Chimera!"); - return false; + else + { + log("Failed to launch Chimera!"); + } + return launched; + } + + /** + * Returns a list of candidate paths to the Chimera program executable + * + * @return + */ + protected List getChimeraPaths() + { + return StructureManager.getChimeraPaths(false); } /** @@ -591,107 +616,53 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel */ public boolean isChimeraRunning() { - return viewer.isChimeraLaunched(); + return chimeraManager.isChimeraLaunched(); } /** * Send a command to Chimera, and optionally log and return any responses. - *

- * Does nothing, and returns null, if the command is the same as the last one - * sent [why?]. * * @param command * @param getResponse */ - public List sendChimeraCommand(final String command, + @Override + public List executeCommand(final String command, boolean getResponse) { - if (viewer == null) + if (chimeraManager == null || command == null) { // ? thread running after viewer shut down return null; } List reply = null; - viewerCommandHistory(false); - if (true /*lastCommand == null || !lastCommand.equals(command)*/) + // trim command or it may never find a match in the replyLog!! + List lastReply = chimeraManager + .sendChimeraCommand(command.trim(), getResponse); + if (getResponse) { - // trim command or it may never find a match in the replyLog!! - List lastReply = viewer.sendChimeraCommand(command.trim(), - getResponse); - if (getResponse) + reply = lastReply; + if (debug) { - reply = lastReply; - if (debug) - { - log("Response from command ('" + command + "') was:\n" - + lastReply); - } + log("Response from command ('" + command + "') was:\n" + lastReply); } } - viewerCommandHistory(true); return reply; } /** - * Send a Chimera command asynchronously in a new thread. If the progress - * message is not null, display this message while the command is executing. - * - * @param command - * @param progressMsg - */ - protected abstract void sendAsynchronousCommand(String command, - String progressMsg); - - /** - * colour any structures associated with sequences in the given alignment - * using the getFeatureRenderer() and getSequenceRenderer() renderers but only - * if colourBySequence is enabled. - */ - public void colourBySequence(boolean showFeatures, - jalview.api.AlignmentViewPanel alignmentv) - { - if (!colourBySequence || !loadingFinished) - { - return; - } - if (getSsm() == null) - { - return; - } - String[] files = getPdbFile(); - - SequenceRenderer sr = getSequenceRenderer(alignmentv); - - FeatureRenderer fr = null; - if (showFeatures) - { - fr = getFeatureRenderer(alignmentv); - } - AlignmentI alignment = alignmentv.getAlignment(); - - StructureMappingcommandSet colourBySequenceCommands = ChimeraCommands - .getColourBySequenceCommand(getSsm(), files, getSequence(), sr, - fr, alignment); - for (String command : colourBySequenceCommands.commands) - { - sendAsynchronousCommand(command, COLOURING_CHIMERA); - } - } - - /** * @param command */ protected void executeWhenReady(String command) { waitForChimera(); - sendChimeraCommand(command, false); + executeCommand(command, false); waitForChimera(); } private void waitForChimera() { - while (viewer != null && viewer.isBusy()) + while (chimeraManager != null && chimeraManager.isBusy()) { try { @@ -702,92 +673,21 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel } } - // End StructureListener - // ////////////////////////// - - /** - * returns the current featureRenderer that should be used to colour the - * structures - * - * @param alignment - * - * @return - */ - public abstract FeatureRenderer getFeatureRenderer( - AlignmentViewPanel alignment); - - /** - * instruct the Jalview binding to update the pdbentries vector if necessary - * prior to matching the viewer's contents to the list of structure files - * Jalview knows about. - */ - public abstract void refreshPdbEntries(); - - private int getModelNum(String modelFileName) - { - String[] mfn = getPdbFile(); - if (mfn == null) - { - return -1; - } - for (int i = 0; i < mfn.length; i++) - { - if (mfn[i].equalsIgnoreCase(modelFileName)) - { - return i; - } - } - return -1; - } - - /** - * map between index of model filename returned from getPdbFile and the first - * index of models from this file in the viewer. Note - this is not trimmed - - * use getPdbFile to get number of unique models. - */ - private int _modelFileNameMap[]; - - // //////////////////////////////// - // /StructureListener @Override - public synchronized String[] getPdbFile() + public synchronized String[] getStructureFiles() { - if (viewer == null) + if (chimeraManager == null) { return new String[0]; } - // if (modelFileNames == null) - // { - // Collection chimodels = viewer.getChimeraModels(); - // _modelFileNameMap = new int[chimodels.size()]; - // int j = 0; - // for (ChimeraModel chimodel : chimodels) - // { - // String mdlName = chimodel.getModelName(); - // } - // modelFileNames = new String[j]; - // // System.arraycopy(mset, 0, modelFileNames, 0, j); - // } - - return chimeraMaps.keySet().toArray( - modelFileNames = new String[chimeraMaps.size()]); - } - /** - * returns the current sequenceRenderer that should be used to colour the - * structures - * - * @param alignment - * - * @return - */ - public abstract SequenceRenderer getSequenceRenderer( - AlignmentViewPanel alignment); + return chimeraMaps.keySet() + .toArray(modelFileNames = new String[chimeraMaps.size()]); + } /** - * Construct and send a command to highlight zero, one or more atoms. We do - * this by sending an "rlabel" command to show the residue label at that - * position. + * Construct and send a command to highlight zero, one or more atoms. We do this + * by sending an "rlabel" command to show the residue label at that position. */ @Override public void highlightAtoms(List atoms) @@ -797,6 +697,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel return; } + boolean forChimeraX = chimeraManager.isChimeraX(); StringBuilder cmd = new StringBuilder(128); boolean first = true; boolean found = false; @@ -811,18 +712,26 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel { if (first) { - cmd.append("rlabel #").append(cms.get(0).getModelNumber()) - .append(":"); + cmd.append(forChimeraX ? "label #" : "rlabel #"); } else { cmd.append(","); } first = false; - cmd.append(pdbResNum); - if (!chain.equals(" ")) + if (forChimeraX) { - cmd.append(".").append(chain); + cmd.append(cms.get(0).getModelNumber()) + .append("/").append(chain).append(":").append(pdbResNum); + } + else + { + cmd.append(cms.get(0).getModelNumber()) + .append(":").append(pdbResNum); + if (!chain.equals(" ") && !forChimeraX) + { + cmd.append(".").append(chain); + } } found = true; } @@ -842,11 +751,11 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel */ if (lastHighlightCommand != null) { - viewer.sendChimeraCommand("~" + lastHighlightCommand, false); + chimeraManager.sendChimeraCommand("~" + lastHighlightCommand, false); } if (found) { - viewer.sendChimeraCommand(command, false); + chimeraManager.sendChimeraCommand(command, false); } this.lastHighlightCommand = command; } @@ -859,13 +768,14 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel /* * Ask Chimera for its current selection */ - List selection = viewer.getSelectedResidueSpecs(); + List selection = chimeraManager.getSelectedResidueSpecs(); /* * Parse model number, residue and chain for each selected position, * formatted as #0:123.A or #1.2:87.B (#model.submodel:residue.chain) */ - List atomSpecs = convertStructureResiduesToAlignment(selection); + List atomSpecs = convertStructureResiduesToAlignment( + selection); /* * Broadcast the selection (which may be empty, if the user just cleared all @@ -884,12 +794,13 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel protected List convertStructureResiduesToAlignment( List structureSelection) { - List atomSpecs = new ArrayList(); + boolean chimeraX = chimeraManager.isChimeraX(); + List atomSpecs = new ArrayList<>(); for (String atomSpec : structureSelection) { try { - AtomSpec spec = AtomSpec.fromChimeraAtomspec(atomSpec); + AtomSpec spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX); String pdbfilename = getPdbFileForModel(spec.getModelNumber()); spec.setPdbFile(pdbfilename); atomSpecs.add(spec); @@ -930,100 +841,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel System.err.println("## Chimera log: " + message); } - private void viewerCommandHistory(boolean enable) - { - // log("(Not yet implemented) History " - // + ((debug || enable) ? "on" : "off")); - } - - public long getLoadNotifiesHandled() - { - return loadNotifiesHandled; - } - - public void setJalviewColourScheme(ColourSchemeI cs) - { - colourBySequence = false; - - if (cs == null) - { - return; - } - - // Chimera expects RBG values in the range 0-1 - final double normalise = 255D; - viewerCommandHistory(false); - StringBuilder command = new StringBuilder(128); - - List residueSet = ResidueProperties.getResidues(isNucleotide(), - false); - for (String res : residueSet) - { - Color col = cs.findColour(res.charAt(0)); - command.append("color " + col.getRed() / normalise + "," - + col.getGreen() / normalise + "," + col.getBlue() - / normalise + " ::" + res + ";"); - } - - sendAsynchronousCommand(command.toString(), COLOURING_CHIMERA); - viewerCommandHistory(true); - } - - /** - * called when the binding thinks the UI needs to be refreshed after a Chimera - * state change. this could be because structures were loaded, or because an - * error has occurred. - */ - public abstract void refreshGUI(); - - @Override - public void setLoadingFromArchive(boolean loadingFromArchive) - { - this.loadingFromArchive = loadingFromArchive; - } - - /** - * - * @return true if Chimeral is still restoring state or loading is still going - * on (see setFinsihedLoadingFromArchive) - */ - @Override - public boolean isLoadingFromArchive() - { - return loadingFromArchive && !loadingFinished; - } - - /** - * modify flag which controls if sequence colouring events are honoured by the - * binding. Should be true for normal operation - * - * @param finishedLoading - */ - @Override - public void setFinishedLoadingFromArchive(boolean finishedLoading) - { - loadingFinished = finishedLoading; - } - - /** - * Send the Chimera 'background solid " command. - * - * @see https - * ://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/background - * .html - * @param col - */ - public void setBackgroundColour(Color col) - { - viewerCommandHistory(false); - double normalise = 255D; - final String command = "background solid " + col.getRed() / normalise - + "," + col.getGreen() / normalise + "," + col.getBlue() - / normalise + ";"; - viewer.sendChimeraCommand(command, false); - viewerCommandHistory(true); - } - /** * Ask Chimera to save its session to the given file. Returns true if * successful, else false. @@ -1035,8 +852,12 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel { if (isChimeraRunning()) { - List reply = viewer.sendChimeraCommand("save " + filepath, - true); + /* + * Chimera: https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/save.html + * ChimeraX: https://www.cgl.ucsf.edu/chimerax/docs/user/commands/save.html + */ + String command = getCommandGenerator().saveSession(filepath); + List reply = chimeraManager.sendChimeraCommand(command, true); if (reply.contains("Session written")) { return true; @@ -1052,56 +873,24 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel /** * Ask Chimera to open a session file. Returns true if successful, else false. - * The filename must have a .py extension for this command to work. + * The filename must have a .py (Chimera) or .cxs (ChimeraX) extension for + * this command to work. * * @param filepath * @return */ public boolean openSession(String filepath) { - sendChimeraCommand("open " + filepath, true); + /* + * Chimera: https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/open.html + * ChimeraX: https://www.cgl.ucsf.edu/chimerax/docs/user/commands/open.html + */ + executeCommand("open " + filepath, true); // todo: test for failure - how? return true; } /** - * Returns a list of chains mapped in this viewer. Note this list is not - * currently scoped per structure. - * - * @return - */ - public List getChainNames() - { - List names = new ArrayList(); - String[][] allNames = getChains(); - if (allNames != null) - { - for (String[] chainsForPdb : allNames) - { - if (chainsForPdb != null) - { - for (String chain : chainsForPdb) - { - if (chain != null && !names.contains(chain)) - { - names.add(chain); - } - } - } - } - } - return names; - } - - /** - * Send a 'focus' command to Chimera to recentre the visible display - */ - public void focusView() - { - sendChimeraCommand("focus", false); - } - - /** * Send a 'show' command for all atoms in the currently selected columns * * TODO: pull up to abstract structure viewer interface @@ -1134,31 +923,19 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel * features visible in Jalview * * @param avp + * @return */ - public void sendFeaturesToViewer(AlignmentViewPanel avp) + public int sendFeaturesToViewer(AlignmentViewPanel avp) { // TODO refactor as required to pull up to an interface - AlignmentI alignment = avp.getAlignment(); - FeatureRenderer fr = getFeatureRenderer(avp); - - /* - * fr is null if feature display is turned off - */ - if (fr == null) - { - return; - } - - String[] files = getPdbFile(); + String[] files = getStructureFiles(); if (files == null) { - return; + return 0; } - StructureMappingcommandSet commandSet = ChimeraCommands - .getSetAttributeCommandsForFeatures(getSsm(), files, - getSequence(), fr, alignment); - String[] commands = commandSet.commands; + String[] commands = getCommandGenerator() + .setAttributesForFeatures(getSsm(), files, getSequence(), avp); if (commands.length > 10) { sendCommandsByFile(commands); @@ -1170,12 +947,13 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel sendAsynchronousCommand(command, null); } } + return commands.length; } /** - * Write commands to a temporary file, and send a command to Chimera to open - * the file as a commands script. For use when sending a large number of - * separate commands would overload the REST interface mechanism. + * Write commands to a temporary file, and send a command to Chimera to open the + * file as a commands script. For use when sending a large number of separate + * commands would overload the REST interface mechanism. * * @param commands */ @@ -1183,7 +961,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel { try { - File tmp = File.createTempFile("chim", ".com"); + File tmp = File.createTempFile("chim", getCommandFileExtension()); tmp.deleteOnExit(); PrintWriter out = new PrintWriter(new FileOutputStream(tmp)); for (String command : commands) @@ -1193,16 +971,26 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel out.flush(); out.close(); String path = tmp.getAbsolutePath(); - sendAsynchronousCommand("open cmd:" + path, null); + String command = getCommandGenerator().openCommandFile(path); + sendAsynchronousCommand(command, null); } catch (IOException e) { - System.err - .println("Sending commands to Chimera via file failed with " - + e.getMessage()); + System.err.println("Sending commands to Chimera via file failed with " + + e.getMessage()); } } /** + * Returns the file extension required for a file of commands to be read by + * the structure viewer + * @return + */ + protected String getCommandFileExtension() + { + return ".com"; + } + + /** * Get Chimera residues which have the named attribute, find the mapped * positions in the Jalview sequence(s), and set as sequence features * @@ -1223,40 +1011,36 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel // fails for 'average.bfactor' (which is bad): String cmd = "list residues attr '" + attName + "'"; - List residues = sendChimeraCommand(cmd, true); + List residues = executeCommand(cmd, true); - /* - * TODO check if Jalview already has this feature name, if so give it a - * distinguishing prefix e.g. chim_ - */ - FeatureRenderer fr = alignmentPanel.getFeatureRenderer(); - // todo should getRenderOrder be in api.FeatureRenderer? - // FIXME this is empty if feature display is turned off - List existingFeatures = ((jalview.gui.FeatureRenderer) fr) - .getRenderOrder(); - if (existingFeatures.contains(attName)) + boolean featureAdded = createFeaturesForAttributes(attName, residues); + if (featureAdded) { - // TODO check if feature of this name is in group Chimera - // if so don't create a new feature name! - // problem: needs a lookup of features for feature group - attName = getStructureFeaturePrefix() + attName; + alignmentPanel.getFeatureRenderer().featuresAdded(); } + } - /* - * Expect 0, 1 or more reply lines of the format (chi2 is attName): - * residue id #0:5.A chi2 -155.000836316 index 5 - * or - * residue id #0:6.A chi3 None - * - * We assume here that attributes on structure do not naturally convert - * to ranges on sequence, i.e. we just set one feature per mapped position. - * - * To conflate positions, would need to first build a map - * Map>> - * and then traverse it to find feature ranges. - */ + /** + * Create features in Jalview for the given attribute name and structure + * residues. + * + *

+   * The residue list should be 0, 1 or more reply lines of the format: 
+   *     residue id #0:5.A isHelix -155.000836316 index 5 
+   * or 
+   *     residue id #0:6.A isHelix None
+   * 
+ * + * @param attName + * @param residues + * @return + */ + protected boolean createFeaturesForAttributes(String attName, + List residues) + { boolean featureAdded = false; String featureGroup = getViewerFeatureGroup(); + boolean chimeraX = chimeraManager.isChimeraX(); for (String residue : residues) { @@ -1280,7 +1064,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel try { - spec = AtomSpec.fromChimeraAtomspec(atomSpec); + spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX); } catch (IllegalArgumentException e) { System.err.println("Problem parsing atomspec " + atomSpec); @@ -1303,14 +1087,18 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel spec.setPdbFile(pdbFile); List atoms = Collections.singletonList(spec); - SearchResults sr = getSsm() + + /* + * locate the mapped position in the alignment (if any) + */ + SearchResultsI sr = getSsm() .findAlignmentPositionsForStructurePositions(atoms); /* * expect one matched alignment position, or none * (if the structure position is not mapped) */ - for (Match m : sr.getResults()) + for (SearchResultMatchI m : sr.getResults()) { SequenceI seq = m.getSequence(); int start = m.getStart(); @@ -1322,33 +1110,67 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel featureAdded |= seq.addSequenceFeature(sf); } } - if (featureAdded) + return featureAdded; + } + + /** + * Answers the feature group name to apply to features created in Jalview from + * Chimera attributes + * + * @return + */ + protected String getViewerFeatureGroup() + { + // todo pull up to interface + return CHIMERA_FEATURE_GROUP; + } + + @Override + public int getModelNoForFile(String pdbFile) + { + List foundModels = chimeraMaps.get(pdbFile); + if (foundModels != null && !foundModels.isEmpty()) { - fr.featuresAdded(); + return foundModels.get(0).getModelNumber(); } + return -1; } /** - * Answers a 'namespace' prefix to use for features created in Jalview from - * attributes in the structure viewer + * Answers a (possibly empty) list of attribute names in Chimera[X], excluding + * any which were added from Jalview * * @return */ - protected String getStructureFeaturePrefix() + public List getChimeraAttributes() { - // TODO pull up as abstract - return CHIMERA_FEATURE_PREFIX; + List atts = chimeraManager.getAttrList(); + Iterator it = atts.iterator(); + while (it.hasNext()) + { + if (it.next().startsWith(ChimeraCommands.NAMESPACE_PREFIX)) + { + /* + * attribute added from Jalview - exclude it + */ + it.remove(); + } + } + return atts; } /** - * Answers the feature group name to apply to features created in Jalview from - * Chimera attributes + * Returns the file extension to use for a saved viewer session file * * @return */ - protected String getViewerFeatureGroup() + public String getSessionFileExtension() { - // todo pull up to interface - return CHIMERA_FEATURE_GROUP; + return ".py"; + } + + public String getHelpURL() + { + return "https://www.cgl.ucsf.edu/chimera/docs/UsersGuide"; } }