/* * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) * Copyright (C) $$Year-Rel$$ The Jalview Authors * * This file is part of Jalview. * * Jalview is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * Jalview is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ package jalview.ext.rbvi.chimera; import jalview.api.AlignmentViewPanel; import jalview.api.structures.JalviewStructureDisplayI; import jalview.bin.Cache; import jalview.datamodel.AlignmentI; import jalview.datamodel.HiddenColumns; import jalview.datamodel.PDBEntry; import jalview.datamodel.SearchResultMatchI; import jalview.datamodel.SearchResultsI; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.gui.Preferences; import jalview.gui.StructureViewer.ViewerType; import jalview.httpserver.AbstractRequestHandler; import jalview.io.DataSourceType; import jalview.structure.AtomSpec; import jalview.structure.StructureMappingcommandSet; import jalview.structure.StructureSelectionManager; import jalview.structures.models.AAStructureBindingModel; import jalview.util.MessageManager; 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; import ext.edu.ucsf.rbvi.strucviz2.ChimeraManager; import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel; import ext.edu.ucsf.rbvi.strucviz2.StructureManager; import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType; public abstract class JalviewChimeraBinding extends AAStructureBindingModel { public static final String CHIMERA_FEATURE_GROUP = "Chimera"; // Chimera clause to exclude alternate locations in atom selection private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9"; private static final boolean debug = false; private static final String PHOSPHORUS = "P"; private static final String ALPHACARBON = "CA"; /* * Object through which we talk to Chimera */ private ChimeraManager chimeraManager; /* * Object which listens to Chimera notifications */ private AbstractRequestHandler chimeraListener; /* * Map of ChimeraModel objects keyed by PDB full local file name */ private Map> chimeraMaps = new LinkedHashMap<>(); String lastHighlightCommand; 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. * * @param pe * @return */ public boolean openFile(PDBEntry pe) { String file = pe.getFile(); try { List modelsToMap = new ArrayList<>(); List oldList = chimeraManager.getModelList(); boolean alreadyOpen = false; /* * If Chimera already has this model, don't reopen it, but do remap it. */ for (ChimeraModel open : oldList) { if (open.getModelName().equals(pe.getId())) { alreadyOpen = true; modelsToMap.add(open); } } /* * If Chimera doesn't yet have this model, ask it to open it, and retrieve * the model name(s) added by Chimera. */ if (!alreadyOpen) { chimeraManager.openModel(file, pe.getId(), ModelType.PDB_MODEL); if (chimeraManager.isChimeraX()) { /* * ChimeraX hack: force chimera model name to pdbId */ int modelNumber = chimeraMaps.size() + 1; String command = "setattr #" + modelNumber + " models name " + pe.getId(); executeCommand(command, false); modelsToMap.add(new ChimeraModel(pe.getId(), ModelType.PDB_MODEL, modelNumber, 0)); } else { /* * Chimera: query for actual models and find the one with * matching model name - 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); } } } } chimeraMaps.put(file, modelsToMap); if (getSsm() != null) { getSsm().addStructureViewerListener(this); } return true; } catch (Exception q) { log("Exception when trying to open model " + file + "\n" + q.toString()); q.printStackTrace(); } return false; } /** * Constructor * * @param ssm * @param pdbentry * @param sequenceIs * @param protocol */ public JalviewChimeraBinding(StructureSelectionManager ssm, PDBEntry[] pdbentry, SequenceI[][] sequenceIs, DataSourceType protocol) { super(ssm, pdbentry, sequenceIs, protocol); chimeraManager = new ChimeraManager(new StructureManager(true)); String viewerType = Cache.getProperty(Preferences.STRUCTURE_DISPLAY); chimeraManager.setChimeraX(ViewerType.CHIMERAX.name().equals(viewerType)); setStructureCommands(new ChimeraCommands()); } /** * 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. */ protected void startChimeraProcessMonitor() { 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(); } /** * Start a dedicated HttpServer to listen for Chimera notifications, and tell it * to start listening */ public void startChimeraListener() { try { chimeraListener = new ChimeraListener(this); chimeraManager.startListening(chimeraListener.getUri()); } catch (BindException e) { System.err.println( "Failed to start Chimera listener: " + e.getMessage()); } } /** * Close down the Jalview viewer and listener, and (optionally) the associated * Chimera window. */ public void closeViewer(boolean closeChimera) { getSsm().removeStructureViewerListener(this, this.getStructureFiles()); if (closeChimera) { chimeraManager.exitChimera(); } if (this.chimeraListener != null) { chimeraListener.shutdown(); chimeraListener = null; } chimeraManager = null; if (chimeraMonitor != null) { chimeraMonitor.interrupt(); } releaseUIResources(); } /** * {@inheritDoc} */ @Override public String superposeStructures(AlignmentI[] _alignment, int[] _refStructure, HiddenColumns[] _hiddenCols) { StringBuilder allComs = new StringBuilder(128); String[] files = getStructureFiles(); if (!waitForFileLoad(files)) { 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]; HiddenColumns hiddenCols = _hiddenCols[a]; if (refStructure >= files.length) { System.err.println("Ignoring invalid reference structure value " + refStructure); refStructure = -1; } /* * 'matched' bit i will be set for visible alignment columns i where * all sequences have a residue with a mapping to the PDB structure */ BitSet matched = new BitSet(); for (int m = 0; m < alignment.getWidth(); m++) { 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()); } /* * Calculate the superposable alignment columns ('matched'), and the * corresponding structure residue positions (structures.pdbResNo) */ int candidateRefStructure = findSuperposableResidues(alignment, matched, structures); if (refStructure < 0) { /* * If no reference structure was specified, pick the first one that has * a mapping in the alignment */ refStructure = candidateRefStructure; } int nmatched = matched.cardinality(); if (nmatched < 4) { return MessageManager.formatMessage("label.insufficient_residues", nmatched); } /* * Generate select statements to select regions to superimpose structures */ 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(); if (chimeraX) { molsel.append("/" + structures[pdbfnum].chain + ":"); } int nextColumnMatch = matched.nextSetBit(0); while (nextColumnMatch != -1) { int pdbResNum = structures[pdbfnum].pdbResNo[nextColumnMatch]; if (lpos != pdbResNum - 1) { /* * discontiguous - append last residue now */ if (lpos != -1) { molsel.append(String.valueOf(lpos)); if (!chimeraX) { molsel.append(chainCd); } molsel.append(","); } run = false; } else { /* * extending a contiguous run */ if (!run) { /* * start the range selection */ molsel.append(String.valueOf(lpos)); molsel.append("-"); } run = true; } lpos = pdbResNum; nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1); } /* * and terminate final selection */ if (lpos != -1) { molsel.append(String.valueOf(lpos)); if (!chimeraX) { molsel.append(chainCd); } } if (molsel.length() > 1) { selcom[pdbfnum] = molsel.toString(); selectioncom.append("#").append(String.valueOf(modelNo)); if (!chimeraX) { selectioncom.append(":"); } selectioncom.append(selcom[pdbfnum]); // selectioncom.append(" "); if (pdbfnum < files.length - 1) { selectioncom.append("|"); } } else { selcom[pdbfnum] = null; } } 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) { continue; } if (command.length() > 0) { command.append(";"); } /* * Form Chimera match command, from the 'new' structure to the * 'reference' structure e.g. (50 residues, chain B/A, alphacarbons): * * match #1:1-30.B,81-100.B@CA #0:21-40.A,61-90.A@CA * * @see * https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/match.html */ 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 - 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); 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()); } // 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(command.toString()); } } String error = null; if (selectioncom.length() > 0) { // TODO: visually distinguish regions that were superposed if (selectioncom.substring(selectioncom.length() - 1).equals("|")) { selectioncom.setLength(selectioncom.length() - 1); } if (debug) { System.out.println("Select regions:\n" + selectioncom.toString()); } 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 ; focus"); allComs.append(selectioncom.toString()); } // 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; } /** * Helper method to construct model spec in Chimera format: *
    *
  • #0 (#1 etc) for a PDB file with no sub-models
  • *
  • #0.1 (#1.1 etc) for a PDB file with sub-models
  • *
      * Note for now we only ever choose the first of multiple models. This * corresponds to the hard-coded Jmol equivalent (compare {1.1}). Refactor in * future if there is a need to select specific sub-models. * * @param pdbfnum * @return */ protected String getModelSpec(int pdbfnum) { if (pdbfnum < 0 || pdbfnum >= getPdbCount()) { return "#" + pdbfnum; // temp hack for ChimeraX } /* * For now, the test for having sub-models is whether multiple Chimera * models are mapped for the PDB file; the models are returned as a response * to the Chimera command 'list models type molecule', see * ChimeraManager.getModelList(). */ 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, else false. * * @return */ public boolean launchChimera() { if (chimeraManager.isChimeraLaunched()) { return true; } boolean launched = chimeraManager.launchChimera( StructureManager.getChimeraPaths(chimeraManager.isChimeraX())); if (launched) { startChimeraProcessMonitor(); } else { log("Failed to launch Chimera!"); } return launched; } /** * Answers true if the Chimera process is still running, false if ended or not * started. * * @return */ public boolean isChimeraRunning() { return chimeraManager.isChimeraLaunched(); } /** * Send a command to Chimera, and optionally log and return any responses. * * @param command * @param getResponse */ @Override public List executeCommand(final String command, boolean getResponse) { if (chimeraManager == null || command == null) { // ? thread running after viewer shut down return null; } List reply = null; // trim command or it may never find a match in the replyLog!! List lastReply = chimeraManager .sendChimeraCommand(command.trim(), getResponse); if (getResponse) { reply = lastReply; if (debug) { log("Response from command ('" + command + "') was:\n" + lastReply); } } 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); /** * @param command */ protected void executeWhenReady(String command) { waitForChimera(); executeCommand(command, false); waitForChimera(); } private void waitForChimera() { while (chimeraManager != null && chimeraManager.isBusy()) { try { Thread.sleep(15); } catch (InterruptedException q) { } } } @Override public synchronized String[] getStructureFiles() { if (chimeraManager == null) { return new String[0]; } 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. */ @Override public void highlightAtoms(List atoms) { if (atoms == null || atoms.size() == 0) { return; } boolean forChimeraX = chimeraManager.isChimeraX(); StringBuilder cmd = new StringBuilder(128); boolean first = true; boolean found = false; for (AtomSpec atom : atoms) { int pdbResNum = atom.getPdbResNum(); String chain = atom.getChain(); String pdbfile = atom.getPdbFile(); List cms = chimeraMaps.get(pdbfile); if (cms != null && !cms.isEmpty()) { if (first) { cmd.append(forChimeraX ? "label #" : "rlabel #"); } else { cmd.append(","); } first = false; if (forChimeraX) { 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; } } String command = cmd.toString(); /* * avoid repeated commands for the same residue */ if (command.equals(lastHighlightCommand)) { return; } /* * unshow the label for the previous residue */ if (lastHighlightCommand != null) { chimeraManager.sendChimeraCommand("~" + lastHighlightCommand, false); } if (found) { chimeraManager.sendChimeraCommand(command, false); } this.lastHighlightCommand = command; } /** * Query Chimera for its current selection, and highlight it on the alignment */ public void highlightChimeraSelection() { /* * Ask Chimera for its current selection */ 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); /* * Broadcast the selection (which may be empty, if the user just cleared all * selections) */ getSsm().mouseOverStructure(atomSpecs); } /** * Converts a list of Chimera atomspecs to a list of AtomSpec representing the * corresponding residues (if any) in Jalview * * @param structureSelection * @return */ protected List convertStructureResiduesToAlignment( List structureSelection) { boolean chimeraX = chimeraManager.isChimeraX(); List atomSpecs = new ArrayList<>(); for (String atomSpec : structureSelection) { try { AtomSpec spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX); String pdbfilename = getPdbFileForModel(spec.getModelNumber()); spec.setPdbFile(pdbfilename); atomSpecs.add(spec); } catch (IllegalArgumentException e) { System.err.println("Failed to parse atomspec: " + atomSpec); } } return atomSpecs; } /** * @param modelId * @return */ protected String getPdbFileForModel(int modelId) { /* * Work out the pdbfilename from the model number */ String pdbfilename = modelFileNames[0]; findfileloop: for (String pdbfile : this.chimeraMaps.keySet()) { for (ChimeraModel cm : chimeraMaps.get(pdbfile)) { if (cm.getModelNumber() == modelId) { pdbfilename = pdbfile; break findfileloop; } } } return pdbfilename; } private void log(String message) { System.err.println("## Chimera log: " + message); } /** * Ask Chimera to save its session to the given file. Returns true if * successful, else false. * * @param filepath * @return */ public boolean saveSession(String filepath) { if (isChimeraRunning()) { /* * 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 = isChimeraX() ? "save session " : "save "; List reply = chimeraManager.sendChimeraCommand(command + filepath, true); if (reply.contains("Session written")) { return true; } else { Cache.log .error("Error saving Chimera session: " + reply.toString()); } } return false; } /** * Ask Chimera to open a session file. Returns true if successful, else false. * The filename must have a .py (Chimera) or .cxs (ChimeraX) extension for * this command to work. * * @param filepath * @return */ public boolean openSession(String filepath) { /* * 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; } /** * Send a 'show' command for all atoms in the currently selected columns * * TODO: pull up to abstract structure viewer interface * * @param vp */ public void highlightSelection(AlignmentViewPanel vp) { List cols = vp.getAlignViewport().getColumnSelection() .getSelected(); AlignmentI alignment = vp.getAlignment(); StructureSelectionManager sm = getSsm(); for (SequenceI seq : alignment.getSequences()) { /* * convert selected columns into sequence positions */ int[] positions = new int[cols.size()]; int i = 0; for (Integer col : cols) { positions[i++] = seq.findPosition(col); } sm.highlightStructure(this, seq, positions); } } /** * Constructs and send commands to Chimera to set attributes on residues for * features visible in Jalview * * @param avp * @return */ public int sendFeaturesToViewer(AlignmentViewPanel avp) { // TODO refactor as required to pull up to an interface String[] files = getStructureFiles(); if (files == null) { return 0; } StructureMappingcommandSet commandSet = ChimeraCommands .getSetAttributeCommandsForFeatures(getSsm(), files, getSequence(), avp, chimeraManager.isChimeraX()); String[] commands = commandSet.commands; if (commands.length > 10) { sendCommandsByFile(commands); } else { for (String command : commands) { 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. * * @param commands */ protected void sendCommandsByFile(String[] commands) { boolean toChimeraX = chimeraManager.isChimeraX(); try { File tmp = File.createTempFile("chim", toChimeraX ? ".cxc" : ".com"); tmp.deleteOnExit(); PrintWriter out = new PrintWriter(new FileOutputStream(tmp)); for (String command : commands) { out.println(command); } out.flush(); out.close(); String path = tmp.getAbsolutePath(); String command = "open " + (toChimeraX ? "" : "cmd:") + path; sendAsynchronousCommand(command, null); } catch (IOException e) { System.err.println("Sending commands to Chimera via file failed with " + e.getMessage()); } } /** * Get Chimera residues which have the named attribute, find the mapped * positions in the Jalview sequence(s), and set as sequence features * * @param attName * @param alignmentPanel */ public void copyStructureAttributesToFeatures(String attName, AlignmentViewPanel alignmentPanel) { // todo pull up to AAStructureBindingModel (and interface?) /* * ask Chimera to list residues with the attribute, reporting its value */ // this alternative command // list residues spec ':*/attName' attr attName // doesn't report 'None' values (which is good), but // fails for 'average.bfactor' (which is bad): String cmd = "list residues attr '" + attName + "'"; List residues = executeCommand(cmd, true); boolean featureAdded = createFeaturesForAttributes(attName, residues); if (featureAdded) { alignmentPanel.getFeatureRenderer().featuresAdded(); } } /** * 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) { AtomSpec spec = null; String[] tokens = residue.split(" "); if (tokens.length < 5) { continue; } String atomSpec = tokens[2]; String attValue = tokens[4]; /* * ignore 'None' (e.g. for phi) or 'False' (e.g. for isHelix) */ if ("None".equalsIgnoreCase(attValue) || "False".equalsIgnoreCase(attValue)) { continue; } try { spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX); } catch (IllegalArgumentException e) { System.err.println("Problem parsing atomspec " + atomSpec); continue; } String chainId = spec.getChain(); String description = attValue; float score = Float.NaN; try { score = Float.valueOf(attValue); description = chainId; } catch (NumberFormatException e) { // was not a float value } String pdbFile = getPdbFileForModel(spec.getModelNumber()); spec.setPdbFile(pdbFile); List atoms = Collections.singletonList(spec); /* * 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 (SearchResultMatchI m : sr.getResults()) { SequenceI seq = m.getSequence(); int start = m.getStart(); int end = m.getEnd(); SequenceFeature sf = new SequenceFeature(attName, description, start, end, score, featureGroup); // todo: should SequenceFeature have an explicit property for chain? // note: repeating the action shouldn't duplicate features featureAdded |= seq.addSequenceFeature(sf); } } 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()) { return foundModels.get(0).getModelNumber(); } return -1; } /** * Answers a (possibly empty) list of attribute names in Chimera[X], excluding * any which were added from Jalview * * @return */ public List getChimeraAttributes() { 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; } public boolean isChimeraX() { return chimeraManager.isChimeraX(); } }