From: gmungoc Date: Sat, 25 Apr 2020 09:09:29 +0000 (+0100) Subject: Merge branch 'develop' into feature/JAL-3551Pymol X-Git-Tag: Release_2_11_2_0~37^2~22 X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=26bb6eb4cbe7da79d8e1c43b23f68071e40a3380;hp=eda1adb8ef48c7c0da70635db7253a7489335603;p=jalview.git Merge branch 'develop' into feature/JAL-3551Pymol --- diff --git a/resources/lang/Messages.properties b/resources/lang/Messages.properties index a4b24ed..51500c6 100644 --- a/resources/lang/Messages.properties +++ b/resources/lang/Messages.properties @@ -268,7 +268,7 @@ label.autoadd_secstr = Add secondary structure annotation to alignment label.autoadd_temp = Add Temperature Factor annotation to alignment label.structure_viewer = Default structure viewer label.double_click_to_browse = Double-click to browse for file -label.chimera_path = Path to Chimera program +label.chimera_path = Path to {0} program label.chimera_path_tip = Jalview will first try any path entered here, else standard installation locations.
Double-click to browse for file. label.invalid_chimera_path = Chimera path not found or not executable label.chimera_missing = Chimera structure viewer not found.
Please enter the path to Chimera (if installed),
or download and install UCSF Chimera. @@ -1129,7 +1129,7 @@ status.fetching_db_refs = Fetching db refs status.loading_cached_pdb_entries = Loading Cached PDB Entries status.searching_for_pdb_structures = Searching for PDB Structures status.opening_file_for = opening file for -status.colouring_chimera = Colouring Chimera +status.colouring_structures = Colouring structures label.font_doesnt_have_letters_defined = Font doesn't have letters defined\nso cannot be used\nwith alignment data label.font_too_small = Font size is too small label.error_loading_file_params = Error loading file {0} diff --git a/resources/lang/Messages_es.properties b/resources/lang/Messages_es.properties index 4e0fb1f..199a65d 100644 --- a/resources/lang/Messages_es.properties +++ b/resources/lang/Messages_es.properties @@ -1198,12 +1198,12 @@ label.confirm_close_chimera=Cerrar tooltip.rnalifold_calculations=Se calcularán predicciones de estructura secondaria de RNA para el alineaminento, y se actualizarán si se efectuan cambios tooltip.rnalifold_settings=Modificar la configuración de la predicción RNAAlifold. Úselo para ocultar o mostrar resultados del cálculo de RNA, o cambiar parámetros de el plegado de RNA. label.show_selected_annotations=Mostrar anotaciones seleccionadas -status.colouring_chimera=Coloreando Chimera +status.colouring_structures=Coloreando estructuras label.configure_displayed_columns=Configurar Columnas Mostradas label.aacon_calculations=cálculos AACon label.pdb_web-service_error=Error de servicio web PDB exception.unable_to_detect_internet_connection=Jalview no puede detectar una conexión a Internet -label.chimera_path=Ruta de acceso a Chimera +label.chimera_path=Ruta de acceso a {0} warn.delete_all=Borrar todas las secuencias cerrará la ventana del alineamiento.
Confirmar o Cancelar. label.select_all=Seleccionar Todos label.alpha_helix=Hélice Alfa diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java b/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java index a910a5a..6bb3b71 100644 --- a/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java +++ b/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java @@ -254,7 +254,7 @@ public class ChimeraManager for (ChimeraModel chimeraModel : modelList) { // get model color - Color modelColor = getModelColor(chimeraModel); + Color modelColor = isChimeraX() ? null : getModelColor(chimeraModel); if (modelColor != null) { chimeraModel.setModelColor(modelColor); @@ -265,7 +265,7 @@ public class ChimeraManager // chimeraSend("repr stick "+newModel.toSpec()); // Create the information we need for the navigator - if (type != ModelType.SMILES) + if (type != ModelType.SMILES && !isChimeraX()) { addResidues(chimeraModel); } @@ -334,7 +334,11 @@ public class ChimeraManager public void stopListening() { - sendChimeraCommand("listen stop models ; listen stop selection ", false); + // TODO send this command when viewer connection is closed in Jalview + String command = isChimeraX + ? "info notify stop models jalview; info notify stop selection jalview" + : "listen stop models ; listen stop selection "; + sendChimeraCommand(command, false); } /** @@ -344,9 +348,23 @@ public class ChimeraManager */ public void startListening(String uri) { - sendChimeraCommand("listen start models url " + uri - + ";listen start select prefix SelectionChanged url " + uri, - false); + /* + * listen for model changes + */ + String command = isChimeraX + ? ("info notify start models prefix ModelChanged jalview url " + + uri) + : ("listen start models url " + uri); + sendChimeraCommand(command, false); + + /* + * listen for selection changes + */ + command = isChimeraX + ? ("info notify start selection jalview prefix SelectionChanged url " + + uri) + : ("listen start select prefix SelectionChanged url " + uri); + sendChimeraCommand(command, false); } /** @@ -420,19 +438,34 @@ public class ChimeraManager public List getSelectedResidueSpecs() { List selectedResidues = new ArrayList<>(); - List chimeraReply = sendChimeraCommand( - "list selection level residue", true); + + /* + * skip for now if ChimeraX - request times out + */ + if (isChimeraX) + { + return selectedResidues; + } + + // in fact 'listinfo' (undocumented) works in ChimeraX + String command = (isChimeraX + ? "info" + : "list") + " selection level residue"; + List chimeraReply = sendChimeraCommand(command, true); if (chimeraReply != null) { /* - * expect 0, 1 or more lines of the format + * expect 0, 1 or more lines of the format either + * Chimera: * residue id #0:43.A type GLY - * where we are only interested in the atomspec #0.43.A + * ChimeraX: + * residue id /A:89 name THR index 88 + * We are only interested in the atomspec (third token of the reply) */ for (String inputLine : chimeraReply) { String[] inputLineParts = inputLine.split("\\s+"); - if (inputLineParts.length == 5) + if (inputLineParts.length >= 5) { selectedResidues.add(inputLineParts[2]); } @@ -473,14 +506,21 @@ public class ChimeraManager public List getModelList() { List modelList = new ArrayList<>(); - List list = sendChimeraCommand("list models type molecule", - true); + String command = "list models type " + + (isChimeraX ? "AtomicStructure" : "molecule"); + List list = sendChimeraCommand(command, true); if (list != null) { for (String modelLine : list) { - ChimeraModel chimeraModel = new ChimeraModel(modelLine); - modelList.add(chimeraModel); + try + { + ChimeraModel chimeraModel = new ChimeraModel(modelLine); + modelList.add(chimeraModel); + } catch (NullPointerException e) + { + // hack for now + } } } return modelList; @@ -555,6 +595,7 @@ public class ChimeraManager { // ensure symbolic links are resolved chimeraPath = Paths.get(chimeraPath).toRealPath().toString(); + isChimeraX = chimeraPath.toLowerCase().contains("chimerax"); File path = new File(chimeraPath); // uncomment the next line to simulate Chimera not installed // path = new File(chimeraPath + "x"); @@ -567,8 +608,16 @@ public class ChimeraManager args.add(chimeraPath); // shows Chimera output window but suppresses REST responses: // args.add("--debug"); - args.add("--start"); - args.add("RESTServer"); + if (isChimeraX()) + { + args.add("--cmd"); + args.add("remote rest start"); + } + else + { + args.add("--start"); + args.add("RESTServer"); + } ProcessBuilder pb = new ProcessBuilder(args); chimera = pb.start(); error = ""; @@ -616,15 +665,23 @@ public class ChimeraManager { responses.append("\n" + response); // expect: REST server on host 127.0.0.1 port port_number + // ChimeraX is the same except "REST server started on host..." if (response.startsWith("REST server")) { String[] tokens = response.split(" "); - if (tokens.length == 7 && "port".equals(tokens[5])) + for (int i = 0; i < tokens.length - 1; i++) { - port = Integer.parseInt(tokens[6]); - break; + if ("port".equals(tokens[i])) + { + port = Integer.parseInt(tokens[i + 1]); + break; + } } } + if (port > 0) + { + break; // hack for hanging readLine() + } response = lineReader.readLine(); } } catch (Exception e) @@ -703,7 +760,8 @@ public class ChimeraManager public List getAttrList() { List attributes = new ArrayList<>(); - final List reply = sendChimeraCommand("list resattr", true); + String command = (isChimeraX ? "info " : "list ") + "resattr"; + final List reply = sendChimeraCommand(command, true); if (reply != null) { for (String inputLine : reply) @@ -762,6 +820,8 @@ public class ChimeraManager private volatile boolean busy = false; + private boolean isChimeraX; + /** * Send a command to Chimera. * @@ -775,7 +835,7 @@ public class ChimeraManager */ public List sendChimeraCommand(String command, boolean reply) { - // System.out.println("chimeradebug>> " + command); + System.out.println("chimeradebug>> " + command); if (!isChimeraLaunched() || command == null || "".equals(command.trim())) { @@ -822,14 +882,23 @@ public class ChimeraManager { String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run"; List commands = new ArrayList<>(1); + String method = isChimeraX() ? "GET" : "POST"; + if ("GET".equals(method)) + { + command = command.replace(" ", "+").replace("#", "%23") + .replace("|", "%7C").replace(";", "%3B"); + } commands.add(new BasicNameValuePair("command", command)); List reply = new ArrayList<>(); BufferedReader response = null; try { - response = HttpClientUtils.doHttpUrlPost(restUrl, commands, CONNECTION_TIMEOUT_MS, - REST_REPLY_TIMEOUT_MS); + response = "GET".equals(method) + ? HttpClientUtils.doHttpGet(restUrl, commands, + CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS) + : HttpClientUtils.doHttpUrlPost(restUrl, commands, + CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS); String line = ""; while ((line = response.readLine()) != null) { @@ -901,4 +970,14 @@ public class ChimeraManager { return chimera; } + + public boolean isChimeraX() + { + return isChimeraX; + } + + public void setChimeraX(boolean b) + { + isChimeraX = b; + } } diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/StructureManager.java b/src/ext/edu/ucsf/rbvi/strucviz2/StructureManager.java index 22c9098..b7ea4ad 100644 --- a/src/ext/edu/ucsf/rbvi/strucviz2/StructureManager.java +++ b/src/ext/edu/ucsf/rbvi/strucviz2/StructureManager.java @@ -896,7 +896,7 @@ public class StructureManager StructureSettings defaultSettings = null; // TODO: [Optional] Change priority of Chimera paths - public static List getChimeraPaths() + public static List getChimeraPaths(boolean isChimeraX) { List pathList = new ArrayList<>(); @@ -918,20 +918,25 @@ public class StructureManager /* * Jalview addition: check if path set in user preferences. */ - String userPath = Cache.getDefault(Preferences.CHIMERA_PATH, null); + String userPath = Cache + .getDefault(isChimeraX ? Preferences.CHIMERAX_PATH + : Preferences.CHIMERA_PATH, null); if (userPath != null) { pathList.add(0, userPath); } + // FIXME get an updated StructureManager for code for ChimeraX paths + String chimera = isChimeraX ? "ChimeraX" : "chimera"; + // Add default installation paths String os = System.getProperty("os.name"); if (os.startsWith("Linux")) { - pathList.add("/usr/local/chimera/bin/chimera"); - pathList.add("/usr/local/bin/chimera"); - pathList.add("/usr/bin/chimera"); - pathList.add(System.getProperty("user.home") + "/opt/bin/chimera"); + pathList.add("/usr/local/chimera/bin/" + chimera); + pathList.add("/usr/local/bin/" + chimera); + pathList.add("/usr/bin/" + chimera); + pathList.add(System.getProperty("user.home") + "/opt/bin/" + chimera); } else if (os.startsWith("Windows")) { @@ -942,15 +947,16 @@ public class StructureManager for (String version : new String[] { "1.11", "1.11.1", "1.11.2", "1.12", "1.12.1", "1.12.2", "1.13" }) { - pathList.add(root + "\\Chimera " + version + "\\bin\\chimera"); + pathList.add(root + "\\Chimera " + version + "\\bin\\" + chimera); pathList.add( - root + "\\Chimera " + version + "\\bin\\chimera.exe"); + root + "\\Chimera " + version + "\\bin\\" + chimera + + ".exe"); } } } else if (os.startsWith("Mac")) { - pathList.add("/Applications/Chimera.app/Contents/MacOS/chimera"); + pathList.add("/Applications/Chimera.app/Contents/MacOS/" + chimera); } return pathList; } diff --git a/src/jalview/api/structures/JalviewStructureDisplayI.java b/src/jalview/api/structures/JalviewStructureDisplayI.java index 8f778f7..d8c8371 100644 --- a/src/jalview/api/structures/JalviewStructureDisplayI.java +++ b/src/jalview/api/structures/JalviewStructureDisplayI.java @@ -23,7 +23,6 @@ package jalview.api.structures; import jalview.api.AlignmentViewPanel; import jalview.datamodel.PDBEntry; import jalview.datamodel.SequenceI; -import jalview.schemes.ColourSchemeI; import jalview.structures.models.AAStructureBindingModel; public interface JalviewStructureDisplayI @@ -59,13 +58,6 @@ public interface JalviewStructureDisplayI void closeViewer(boolean closeExternalViewer); /** - * apply a colourscheme to the structures in the viewer - * - * @param colourScheme - */ - void setJalviewColourScheme(ColourSchemeI colourScheme); - - /** * * @return true if all background sequence/structure binding threads have * completed for this viewer instance @@ -125,4 +117,48 @@ public interface JalviewStructureDisplayI */ void raiseViewer(); + AlignmentViewPanel getAlignmentPanel(); + + /** + * Answers true if the given alignment view is used to colour structures by + * sequence, false if not + * + * @param ap + * @return + */ + boolean isUsedForColourBy(AlignmentViewPanel ap); + + /** + * If implemented, shows a command line console in the structure viewer + * + * @param show + * true to show, false to hide + */ + void showConsole(boolean show); + + /** + * Remove references to the given alignment view for this structure viewer + * + * @param avp + */ + void removeAlignmentPanel(AlignmentViewPanel avp); + + /** + * Updates the progress bar if there is one. Call stopProgressBar with the + * returned handle to remove the message. + * + * @param msg + * @return handle + */ + long startProgressBar(String msg); + + /** + * Ends the progress bar with the specified handle, leaving a message (if not + * null) on the status bar + * + * @param msg + * @param handle + */ + void stopProgressBar(String msg, long handle); + } diff --git a/src/jalview/appletgui/AlignFrame.java b/src/jalview/appletgui/AlignFrame.java index 0bc45e2..f4abd3d 100644 --- a/src/jalview/appletgui/AlignFrame.java +++ b/src/jalview/appletgui/AlignFrame.java @@ -3963,7 +3963,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener, * without an additional javascript library to exchange messages between the * distinct applets. See http://issues.jalview.org/browse/JAL-621 * - * @param viewer + * @param jmolViewer * JmolViewer instance * @param sequenceIds * - sequence Ids to search for associations diff --git a/src/jalview/appletgui/AppletJmol.java b/src/jalview/appletgui/AppletJmol.java index 3d1442d..89a912f 100644 --- a/src/jalview/appletgui/AppletJmol.java +++ b/src/jalview/appletgui/AppletJmol.java @@ -307,7 +307,7 @@ public class AppletJmol extends EmbmenuFrame implements else if (protocol == DataSourceType.FILE || protocol == DataSourceType.URL) { - jmb.viewer.openFile(pdbentry.getFile()); + jmb.jmolViewer.openFile(pdbentry.getFile()); } else { @@ -350,7 +350,7 @@ public class AppletJmol extends EmbmenuFrame implements throw new Exception(MessageManager.getString( "exception.invalid_datasource_couldnt_obtain_reader")); } - jmb.viewer.openReader(pdbentry.getFile(), pdbentry.getId(), + jmb.jmolViewer.openReader(pdbentry.getFile(), pdbentry.getId(), freader); } catch (Exception e) { @@ -406,7 +406,7 @@ public class AppletJmol extends EmbmenuFrame implements } } } - jmb.centerViewer(toshow); + jmb.showChains(toshow); } void closeViewer() @@ -455,41 +455,41 @@ public class AppletJmol extends EmbmenuFrame implements else if (evt.getSource() == zappo) { setEnabled(zappo); - jmb.setJalviewColourScheme(new ZappoColourScheme()); + jmb.colourByJalviewColourScheme(new ZappoColourScheme()); } else if (evt.getSource() == taylor) { setEnabled(taylor); - jmb.setJalviewColourScheme(new TaylorColourScheme()); + jmb.colourByJalviewColourScheme(new TaylorColourScheme()); } else if (evt.getSource() == hydro) { setEnabled(hydro); - jmb.setJalviewColourScheme(new HydrophobicColourScheme()); + jmb.colourByJalviewColourScheme(new HydrophobicColourScheme()); } else if (evt.getSource() == helix) { setEnabled(helix); - jmb.setJalviewColourScheme(new HelixColourScheme()); + jmb.colourByJalviewColourScheme(new HelixColourScheme()); } else if (evt.getSource() == strand) { setEnabled(strand); - jmb.setJalviewColourScheme(new StrandColourScheme()); + jmb.colourByJalviewColourScheme(new StrandColourScheme()); } else if (evt.getSource() == turn) { setEnabled(turn); - jmb.setJalviewColourScheme(new TurnColourScheme()); + jmb.colourByJalviewColourScheme(new TurnColourScheme()); } else if (evt.getSource() == buried) { setEnabled(buried); - jmb.setJalviewColourScheme(new BuriedColourScheme()); + jmb.colourByJalviewColourScheme(new BuriedColourScheme()); } else if (evt.getSource() == purinepyrimidine) { - jmb.setJalviewColourScheme(new PurinePyrimidineColourScheme()); + jmb.colourByJalviewColourScheme(new PurinePyrimidineColourScheme()); } else if (evt.getSource() == user) { @@ -658,7 +658,7 @@ public class AppletJmol extends EmbmenuFrame implements { currentSize = this.getSize(); - if (jmb.viewer == null) + if (jmb.jmolViewer == null) { g.setColor(Color.black); g.fillRect(0, 0, currentSize.width, currentSize.height); @@ -669,7 +669,7 @@ public class AppletJmol extends EmbmenuFrame implements } else { - jmb.viewer.renderScreenImage(g, currentSize.width, + jmb.jmolViewer.renderScreenImage(g, currentSize.width, currentSize.height); } } @@ -693,9 +693,9 @@ public class AppletJmol extends EmbmenuFrame implements * * } */ - public void setJalviewColourScheme(UserColourScheme ucs) + public void colourByJalviewColourScheme(UserColourScheme ucs) { - jmb.setJalviewColourScheme(ucs); + jmb.colourByJalviewColourScheme(ucs); } public AlignmentPanel getAlignmentPanelFor(AlignmentI alignment) diff --git a/src/jalview/appletgui/AppletJmolBinding.java b/src/jalview/appletgui/AppletJmolBinding.java index e5767b6..dd6dea3 100644 --- a/src/jalview/appletgui/AppletJmolBinding.java +++ b/src/jalview/appletgui/AppletJmolBinding.java @@ -51,13 +51,6 @@ class AppletJmolBinding extends JalviewJmolBinding } @Override - public jalview.api.FeatureRenderer getFeatureRenderer( - AlignmentViewPanel alignment) - { - return appletJmolBinding.ap.getFeatureRenderer(); - } - - @Override public jalview.api.SequenceRenderer getSequenceRenderer( AlignmentViewPanel alignment) { @@ -154,7 +147,7 @@ class AppletJmolBinding extends JalviewJmolBinding Container consolePanel, String buttonsToShow) { JmolAppConsoleInterface appc = new AppletConsole(); - appc.start(viewer); + appc.start(jmolViewer); return appc; } diff --git a/src/jalview/appletgui/ExtJmol.java b/src/jalview/appletgui/ExtJmol.java index b0d3f7a..5c20136 100644 --- a/src/jalview/appletgui/ExtJmol.java +++ b/src/jalview/appletgui/ExtJmol.java @@ -21,7 +21,6 @@ package jalview.appletgui; import jalview.api.AlignmentViewPanel; -import jalview.api.FeatureRenderer; import jalview.api.SequenceRenderer; import jalview.datamodel.PDBEntry; import jalview.datamodel.SequenceI; @@ -76,21 +75,6 @@ public class ExtJmol extends JalviewJmolBinding } @Override - public FeatureRenderer getFeatureRenderer(AlignmentViewPanel alignment) - { - AlignmentPanel alignPanel = (AlignmentPanel) alignment; - if (alignPanel.av.isShowSequenceFeatures()) - { - return alignPanel.getFeatureRenderer(); - } - else - { - return null; - } - } - - - @Override public SequenceRenderer getSequenceRenderer(AlignmentViewPanel alignment) { return ((AlignmentPanel) alignment).getSequenceRenderer(); @@ -191,14 +175,8 @@ public class ExtJmol extends JalviewJmolBinding } @Override - public void releaseReferences(Object svl) - { - } - - @Override public Map getJSpecViewProperty(String arg0) { return null; } - } diff --git a/src/jalview/appletgui/UserDefinedColours.java b/src/jalview/appletgui/UserDefinedColours.java index 6831a73..bfce880 100644 --- a/src/jalview/appletgui/UserDefinedColours.java +++ b/src/jalview/appletgui/UserDefinedColours.java @@ -524,7 +524,7 @@ public class UserDefinedColours extends Panel } else if (jmol != null) { - jmol.setJalviewColourScheme(ucs); + jmol.colourByJalviewColourScheme(ucs); } else if (pdbcanvas != null) { diff --git a/src/jalview/ext/jmol/JalviewJmolBinding.java b/src/jalview/ext/jmol/JalviewJmolBinding.java index 1ceabd1..4c19f6e 100644 --- a/src/jalview/ext/jmol/JalviewJmolBinding.java +++ b/src/jalview/ext/jmol/JalviewJmolBinding.java @@ -20,33 +20,25 @@ */ package jalview.ext.jmol; -import jalview.api.AlignmentViewPanel; import jalview.api.FeatureRenderer; -import jalview.api.SequenceRenderer; -import jalview.datamodel.AlignmentI; -import jalview.datamodel.HiddenColumns; import jalview.datamodel.PDBEntry; import jalview.datamodel.SequenceI; import jalview.gui.IProgressIndicator; +import jalview.gui.StructureViewer.ViewerType; import jalview.io.DataSourceType; import jalview.io.StructureFile; -import jalview.schemes.ColourSchemeI; -import jalview.schemes.ResidueProperties; import jalview.structure.AtomSpec; -import jalview.structure.StructureMappingcommandSet; +import jalview.structure.StructureCommand; +import jalview.structure.StructureCommandI; import jalview.structure.StructureSelectionManager; import jalview.structures.models.AAStructureBindingModel; -import jalview.util.MessageManager; -import java.awt.Color; import java.awt.Container; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.io.File; import java.net.URL; import java.util.ArrayList; -import java.util.BitSet; -import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.StringTokenizer; @@ -58,7 +50,6 @@ import org.jmol.api.JmolSelectionListener; import org.jmol.api.JmolStatusListener; import org.jmol.api.JmolViewer; import org.jmol.c.CBK; -import org.jmol.script.T; import org.jmol.viewer.Viewer; public abstract class JalviewJmolBinding extends AAStructureBindingModel @@ -67,41 +58,28 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel { private String lastMessage; - boolean allChainsSelected = false; - /* * when true, try to search the associated datamodel for sequences that are * associated with any unknown structures in the Jmol view. */ private boolean associateNewStructs = false; - Vector atomsPicked = new Vector<>(); - - private List chainNames; - - Hashtable chainFile; - - /* - * the default or current model displayed if the model cannot be identified - * from the selection message - */ - int frameNo = 0; - - // protected JmolGenericPopup jmolpopup; // not used - remove? + private Vector atomsPicked = new Vector<>(); - String lastCommand; + private String lastCommand; - boolean loadedInline; + private boolean loadedInline; - StringBuffer resetLastRes = new StringBuffer(); + private StringBuffer resetLastRes = new StringBuffer(); - public Viewer viewer; + public Viewer jmolViewer; public JalviewJmolBinding(StructureSelectionManager ssm, PDBEntry[] pdbentry, SequenceI[][] sequenceIs, DataSourceType protocol) { super(ssm, pdbentry, sequenceIs, protocol); + setStructureCommands(new JmolCommands()); /* * viewer = JmolViewer.allocateViewer(renderPanel, new SmarterJmolAdapter(), * "jalviewJmol", ap.av.applet .getDocumentBase(), @@ -116,9 +94,10 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel { super(ssm, seqs); - viewer = theViewer; - viewer.setJmolStatusListener(this); - viewer.addSelectionListener(this); + jmolViewer = theViewer; + jmolViewer.setJmolStatusListener(this); + jmolViewer.addSelectionListener(this); + setStructureCommands(new JmolCommands()); } /** @@ -132,404 +111,33 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel return getViewerTitle("Jmol", true); } - /** - * prepare the view for a given set of models/chains. chainList contains - * strings of the form 'pdbfilename:Chaincode' - * - * @param chainList - * list of chains to make visible - */ - public void centerViewer(Vector chainList) - { - StringBuilder cmd = new StringBuilder(128); - int mlength, p; - for (String lbl : chainList) - { - mlength = 0; - do - { - p = mlength; - mlength = lbl.indexOf(":", p); - } while (p < mlength && mlength < (lbl.length() - 2)); - // TODO: lookup each pdb id and recover proper model number for it. - cmd.append(":" + lbl.substring(mlength + 1) + " /" - + (1 + getModelNum(chainFile.get(lbl))) + " or "); - } - if (cmd.length() > 0) - { - cmd.setLength(cmd.length() - 4); - } - evalStateCommand("select *;restrict " + cmd + ";cartoon;center " + cmd); - } - public void closeViewer() { // remove listeners for all structures in viewer getSsm().removeStructureViewerListener(this, this.getStructureFiles()); - viewer.dispose(); + jmolViewer.dispose(); lastCommand = null; - viewer = null; + jmolViewer = null; releaseUIResources(); } @Override - public void colourByChain() - { - colourBySequence = false; - // TODO: colour by chain should colour each chain distinctly across all - // visible models - // TODO: http://issues.jalview.org/browse/JAL-628 - evalStateCommand("select *;color chain"); - } - - @Override - public void colourByCharge() - { - colourBySequence = false; - evalStateCommand("select *;color white;select ASP,GLU;color red;" - + "select LYS,ARG;color blue;select CYS;color yellow"); - } - - /** - * superpose the structures associated with sequences in the alignment - * according to their corresponding positions. - */ - public void superposeStructures(AlignmentI alignment) - { - superposeStructures(alignment, -1, null); - } - - /** - * superpose the structures associated with sequences in the alignment - * according to their corresponding positions. ded) - * - * @param refStructure - * - select which pdb file to use as reference (default is -1 - the - * first structure in the alignment) - */ - public void superposeStructures(AlignmentI alignment, int refStructure) - { - superposeStructures(alignment, refStructure, null); - } - - /** - * superpose the structures associated with sequences in the alignment - * according to their corresponding positions. ded) - * - * @param refStructure - * - select which pdb file to use as reference (default is -1 - the - * first structure in the alignment) - * @param hiddenCols - * TODO - */ - public void superposeStructures(AlignmentI alignment, int refStructure, - HiddenColumns hiddenCols) - { - superposeStructures(new AlignmentI[] { alignment }, - new int[] - { refStructure }, new HiddenColumns[] { hiddenCols }); - } - - /** - * {@inheritDoc} - */ - @Override - public String superposeStructures(AlignmentI[] _alignment, - int[] _refStructure, HiddenColumns[] _hiddenCols) + public List executeCommand(StructureCommandI command, + boolean getReply) { - while (viewer.isScriptExecuting()) - { - try - { - Thread.sleep(10); - } catch (InterruptedException i) - { - } - } - - /* - * get the distinct structure files modelled - * (a file with multiple chains may map to multiple sequences) - */ - String[] files = getStructureFiles(); - if (!waitForFileLoad(files)) + if (command == null) { return null; } - - StringBuilder selectioncom = new StringBuilder(256); - // In principle - nSeconds specifies the speed of animation for each - // superposition - but is seems to behave weirdly, so we don't specify it. - String nSeconds = " "; - if (files.length > 10) - { - nSeconds = " 0.005 "; - } - else - { - nSeconds = " " + (2.0 / files.length) + " "; - // if (nSeconds).substring(0,5)+" "; - } - - // see JAL-1345 - should really automatically turn off the animation for - // large numbers of structures, but Jmol doesn't seem to allow that. - // nSeconds = " "; - // union of all aligned positions are collected together. - for (int a = 0; a < _alignment.length; a++) - { - int refStructure = _refStructure[a]; - AlignmentI alignment = _alignment[a]; - HiddenColumns hiddenCols = _hiddenCols[a]; - if (a > 0 && selectioncom.length() > 0 && !selectioncom - .substring(selectioncom.length() - 1).equals("|")) - { - selectioncom.append("|"); - } - // process this alignment - if (refStructure >= files.length) - { - System.err.println( - "Invalid reference structure value " + refStructure); - refStructure = -1; - } - - /* - * 'matched' bit j will be set for visible alignment columns j 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; - } - - String[] selcom = new String[files.length]; - int nmatched = matched.cardinality(); - if (nmatched < 4) - { - return (MessageManager.formatMessage("label.insufficient_residues", - nmatched)); - } - - /* - * generate select statements to select regions to superimpose structures - */ - { - // TODO extract method to construct selection statements - for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) - { - String chainCd = ":" + structures[pdbfnum].chain; - int lpos = -1; - boolean run = false; - StringBuilder molsel = new StringBuilder(); - molsel.append("{"); - - int nextColumnMatch = matched.nextSetBit(0); - while (nextColumnMatch != -1) - { - int pdbResNo = structures[pdbfnum].pdbResNo[nextColumnMatch]; - if (lpos != pdbResNo - 1) - { - // discontinuity - if (lpos != -1) - { - molsel.append(lpos); - molsel.append(chainCd); - molsel.append("|"); - } - run = false; - } - else - { - // continuous run - and lpos >-1 - if (!run) - { - // at the beginning, so add dash - molsel.append(lpos); - molsel.append("-"); - } - run = true; - } - lpos = pdbResNo; - nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1); - } - /* - * add final selection phrase - */ - if (lpos != -1) - { - molsel.append(lpos); - molsel.append(chainCd); - molsel.append("}"); - } - if (molsel.length() > 1) - { - selcom[pdbfnum] = molsel.toString(); - selectioncom.append("(("); - selectioncom.append(selcom[pdbfnum].substring(1, - selcom[pdbfnum].length() - 1)); - selectioncom.append(" )& "); - selectioncom.append(pdbfnum + 1); - selectioncom.append(".1)"); - if (pdbfnum < files.length - 1) - { - selectioncom.append("|"); - } - } - else - { - selcom[pdbfnum] = null; - } - } - } - StringBuilder command = new StringBuilder(256); - // command.append("set spinFps 10;\n"); - - for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) - { - if (pdbfnum == refStructure || selcom[pdbfnum] == null - || selcom[refStructure] == null) - { - continue; - } - command.append("echo "); - command.append("\"Superposing ("); - command.append(structures[pdbfnum].pdbId); - command.append(") against reference ("); - command.append(structures[refStructure].pdbId); - command.append(")\";\ncompare " + nSeconds); - command.append("{"); - command.append(Integer.toString(1 + pdbfnum)); - command.append(".1} {"); - command.append(Integer.toString(1 + refStructure)); - // conformation=1 excludes alternate locations for CA (JAL-1757) - command.append( - ".1} SUBSET {(*.CA | *.P) and conformation=1} ATOMS "); - - // for (int s = 0; s < 2; s++) - // { - // command.append(selcom[(s == 0 ? pdbfnum : refStructure)]); - // } - command.append(selcom[pdbfnum]); - command.append(selcom[refStructure]); - command.append(" ROTATE TRANSLATE;\n"); - } - if (selectioncom.length() > 0) - { - // TODO is performing selectioncom redundant here? is done later on - // System.out.println("Select regions:\n" + selectioncom.toString()); - evalStateCommand("select *; cartoons off; backbone; select (" - + selectioncom.toString() + "); cartoons; "); - // selcom.append("; ribbons; "); - String cmdString = command.toString(); - // System.out.println("Superimpose command(s):\n" + cmdString); - - evalStateCommand(cmdString); - } - } - if (selectioncom.length() > 0) - {// finally, mark all regions that were superposed. - if (selectioncom.substring(selectioncom.length() - 1).equals("|")) - { - selectioncom.setLength(selectioncom.length() - 1); - } - // System.out.println("Select regions:\n" + selectioncom.toString()); - evalStateCommand("select *; cartoons off; backbone; select (" - + selectioncom.toString() + "); cartoons; "); - // evalStateCommand("select *; backbone; select "+selcom.toString()+"; - // cartoons; center "+selcom.toString()); - } - - return null; - } - - public void evalStateCommand(String command) - { + String cmd = command.getCommand(); jmolHistory(false); - if (lastCommand == null || !lastCommand.equals(command)) + if (lastCommand == null || !lastCommand.equals(cmd)) { - viewer.evalStringQuiet(command + "\n"); + jmolViewer.evalStringQuiet(cmd + "\n"); } jmolHistory(true); - lastCommand = command; - } - - Thread colourby = null; - /** - * Sends a set of colour commands to the structure viewer - * - * @param colourBySequenceCommands - */ - @Override - protected void colourBySequence( - final StructureMappingcommandSet[] colourBySequenceCommands) - { - if (colourby != null) - { - colourby.interrupt(); - colourby = null; - } - colourby = new Thread(new Runnable() - { - @Override - public void run() - { - for (StructureMappingcommandSet cpdbbyseq : colourBySequenceCommands) - { - for (String cbyseq : cpdbbyseq.commands) - { - executeWhenReady(cbyseq); - } - } - } - }); - colourby.start(); - } - - /** - * @param files - * @param sr - * @param viewPanel - * @return - */ - @Override - protected StructureMappingcommandSet[] getColourBySequenceCommands( - String[] files, SequenceRenderer sr, AlignmentViewPanel viewPanel) - { - return JmolCommands.getColourBySequenceCommand(getSsm(), files, - getSequence(), sr, viewPanel); - } - - /** - * @param command - */ - protected void executeWhenReady(String command) - { - evalStateCommand(command); + lastCommand = cmd; + return null; } public void createImage(String file, String type, int quality) @@ -570,43 +178,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel return null; } - public Color getColour(int atomIndex, int pdbResNum, String chain, - String pdbfile) - { - if (getModelNum(pdbfile) < 0) - { - return null; - } - // TODO: verify atomIndex is selecting correct model. - // return new Color(viewer.getAtomArgb(atomIndex)); Jmol 12.2.4 - int colour = viewer.ms.at[atomIndex].atomPropertyInt(T.color); - return new Color(colour); - } - - /** - * instruct the Jalview binding to update the pdbentries vector if necessary - * prior to matching the jmol view's contents to the list of structure files - * Jalview knows about. - */ - public abstract void refreshPdbEntries(); - - private int getModelNum(String modelFileName) - { - String[] mfn = getStructureFiles(); - 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 - @@ -618,18 +189,18 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel public synchronized String[] getStructureFiles() { List mset = new ArrayList<>(); - if (viewer == null) + if (jmolViewer == null) { return new String[0]; } if (modelFileNames == null) { - int modelCount = viewer.ms.mc; + int modelCount = jmolViewer.ms.mc; String filePath = null; for (int i = 0; i < modelCount; ++i) { - filePath = viewer.ms.getModelFileName(i); + filePath = jmolViewer.ms.getModelFileName(i); if (!mset.contains(filePath)) { mset.add(filePath); @@ -670,7 +241,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel { if (resetLastRes.length() > 0) { - viewer.evalStringQuiet(resetLastRes.toString()); + jmolViewer.evalStringQuiet(resetLastRes.toString()); resetLastRes.setLength(0); } for (AtomSpec atom : atoms) @@ -706,9 +277,9 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel jmolHistory(false); StringBuilder cmd = new StringBuilder(64); - cmd.append("select " + pdbResNum); // +modelNum + cmd.append("select ").append(String.valueOf(pdbResNum)); // +modelNum - resetLastRes.append("select " + pdbResNum); // +modelNum + resetLastRes.append("select ").append(String.valueOf(pdbResNum)); // +modelNum cmd.append(":"); resetLastRes.append(":"); @@ -718,8 +289,8 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel resetLastRes.append(chain); } { - cmd.append(" /" + (mdlNum + 1)); - resetLastRes.append("/" + (mdlNum + 1)); + cmd.append(" /").append(String.valueOf(mdlNum + 1)); + resetLastRes.append("/").append(String.valueOf(mdlNum + 1)); } cmd.append(";wireframe 100;" + cmd.toString() + " and not hetero;"); @@ -728,16 +299,16 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel cmd.append("spacefill 200;select none"); - viewer.evalStringQuiet(cmd.toString()); + jmolViewer.evalStringQuiet(cmd.toString()); jmolHistory(true); } - boolean debug = true; + private boolean debug = true; private void jmolHistory(boolean enable) { - viewer.evalStringQuiet("History " + ((debug || enable) ? "on" : "off")); + jmolViewer.evalStringQuiet("History " + ((debug || enable) ? "on" : "off")); } public void loadInline(String string) @@ -751,7 +322,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel // Then, construct pass a reader for the string to Jmol. // ((org.jmol.Viewer.Viewer) viewer).loadModelFromFile(fullPathName, // fileName, null, reader, false, null, null, 0); - viewer.openStringInline(string); + jmolViewer.openStringInline(string); } protected void mouseOverStructure(int atomIndex, final String strInfo) @@ -794,8 +365,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel chainId = " "; } - String pdbfilename = modelFileNames[frameNo]; // default is first or current - // model + String pdbfilename = modelFileNames[0]; // default is first model if (mdlSep > -1) { if (chainSeparator1 == -1) @@ -828,7 +398,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel if (pdbfilename == null) { - pdbfilename = new File(viewer.ms.getModelFileName(mnumber)) + pdbfilename = new File(jmolViewer.ms.getModelFileName(mnumber)) .getAbsolutePath(); } } @@ -854,7 +424,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel sb.append(";set hoverLabel \"").append(toks.nextToken()).append(" ") .append(toks.nextToken()); sb.append("|").append(label).append("\""); - evalStateCommand(sb.toString()); + executeCommand(new StructureCommand(sb.toString()), false); } } @@ -915,12 +485,12 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel if (!atomsPicked.contains(picked)) { - viewer.evalStringQuiet("select " + picked + ";label %n %r:%c"); + jmolViewer.evalStringQuiet("select " + picked + ";label %n %r:%c"); atomsPicked.addElement(picked); } else { - viewer.evalString("select " + picked + ";label off"); + jmolViewer.evalString("select " + picked + ";label off"); atomsPicked.removeElement(picked); } jmolHistory(true); @@ -1036,8 +606,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel fileLoadingError = null; String[] oldmodels = modelFileNames; modelFileNames = null; - chainNames = new ArrayList<>(); - chainFile = new Hashtable<>(); boolean notifyLoaded = false; String[] modelfilenames = getStructureFiles(); // first check if we've lost any structures @@ -1088,7 +656,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel // calculate essential attributes for the pdb data imported inline. // prolly need to resolve modelnumber properly - for now just use our // 'best guess' - pdbfile = viewer.getData( + pdbfile = jmolViewer.getData( "" + (1 + _modelFileNameMap[modelnum]) + ".0", "PDB"); } // search pdbentries and sequences to find correct pdbentry for this @@ -1143,14 +711,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel } if (matches) { - // add an entry for every chain in the model - for (int i = 0; i < pdb.getChains().size(); i++) - { - String chid = new String( - pdb.getId() + ":" + pdb.getChains().elementAt(i).id); - chainFile.put(chid, fileName); - chainNames.add(chid); - } + stashFoundChains(pdb, fileName); notifyLoaded = true; } } @@ -1160,7 +721,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel // this is a foreign pdb file that jalview doesn't know about - add // it to the dataset and try to find a home - either on a matching // sequence or as a new sequence. - String pdbcontent = viewer.getData("/" + (modelnum + 1) + ".1", + String pdbcontent = jmolViewer.getData("/" + (modelnum + 1) + ".1", "PDB"); // parse pdb file into a chain, etc. // locate best match for pdb in associated views and add mapping to @@ -1179,7 +740,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel // } if (!isLoadingFromArchive()) { - viewer.evalStringQuiet( + jmolViewer.evalStringQuiet( "model *; select backbone;restrict;cartoon;wireframe off;spacefill off"); } // register ourselves as a listener and notify the gui that it needs to @@ -1198,12 +759,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel setLoadingFromArchive(false); } - @Override - public List getChainNames() - { - return chainNames; - } - protected IProgressIndicator getIProgressIndicator() { return null; @@ -1248,35 +803,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel } - @Override - public void setJalviewColourScheme(ColourSchemeI cs) - { - colourBySequence = false; - - if (cs == null) - { - return; - } - - jmolHistory(false); - StringBuilder command = new StringBuilder(128); - command.append("select *;color white;"); - List residueSet = ResidueProperties.getResidues(isNucleotide(), - false); - for (String resName : residueSet) - { - char res = resName.length() == 3 - ? ResidueProperties.getSingleCharacterCode(resName) - : resName.charAt(0); - Color col = cs.findColour(res, 0, null, null, 0f); - command.append("select " + resName + ";color[" + col.getRed() + "," - + col.getGreen() + "," + col.getBlue() + "];"); - } - - evalStateCommand(command.toString()); - jmolHistory(true); - } - public void showHelp() { showUrl("http://jmol.sourceforge.net/docs/JmolUserGuide/", "jmolHelp"); @@ -1290,13 +816,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel public abstract void showUrl(String url, String target); /** - * called when the binding thinks the UI needs to be refreshed after a Jmol - * state change. this could be because structures were loaded, or because an - * error has occured. - */ - public abstract void refreshGUI(); - - /** * called to show or hide the associated console window container. * * @param show @@ -1345,12 +864,12 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel { commandOptions = ""; } - viewer = (Viewer) JmolViewer.allocateViewer(renderPanel, + jmolViewer = (Viewer) JmolViewer.allocateViewer(renderPanel, (jmolfileio ? new SmarterJmolAdapter() : null), htmlName + ((Object) this).toString(), documentBase, codeBase, commandOptions, this); - viewer.setJmolStatusListener(this); // extends JmolCallbackListener + jmolViewer.setJmolStatusListener(this); // extends JmolCallbackListener console = createJmolConsole(consolePanel, buttonsToShow); if (consolePanel != null) @@ -1367,15 +886,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel protected org.jmol.api.JmolAppConsoleInterface console = null; @Override - public void setBackgroundColour(java.awt.Color col) - { - jmolHistory(false); - viewer.evalStringQuiet("background [" + col.getRed() + "," - + col.getGreen() + "," + col.getBlue() + "];"); - jmolHistory(true); - } - - @Override public int[] resizeInnerPanel(String data) { // Jalview doesn't honour resize panel requests @@ -1435,4 +945,33 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel { showConsole(false); } + + @Override + protected String getModelIdForFile(String pdbFile) + { + if (modelFileNames == null) + { + return ""; + } + for (int i = 0; i < modelFileNames.length; i++) + { + if (modelFileNames[i].equalsIgnoreCase(pdbFile)) + { + return String.valueOf(i + 1); + } + } + return ""; + } + + @Override + protected ViewerType getViewerType() + { + return ViewerType.JMOL; + } + + @Override + protected String getModelId(int pdbfnum, String file) + { + return String.valueOf(pdbfnum + 1); + } } diff --git a/src/jalview/ext/jmol/JmolCommands.java b/src/jalview/ext/jmol/JmolCommands.java index 8fb0de6..bd44921 100644 --- a/src/jalview/ext/jmol/JmolCommands.java +++ b/src/jalview/ext/jmol/JmolCommands.java @@ -28,49 +28,96 @@ import jalview.datamodel.AlignmentI; import jalview.datamodel.HiddenColumns; import jalview.datamodel.SequenceI; import jalview.renderer.seqfeatures.FeatureColourFinder; +import jalview.structure.AtomSpecModel; +import jalview.structure.StructureCommand; +import jalview.structure.StructureCommandI; +import jalview.structure.StructureCommandsBase; import jalview.structure.StructureMapping; -import jalview.structure.StructureMappingcommandSet; import jalview.structure.StructureSelectionManager; +import jalview.util.Comparison; import java.awt.Color; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Map; /** - * Routines for generating Jmol commands for Jalview/Jmol binding another - * cruisecontrol test. + * Routines for generating Jmol commands for Jalview/Jmol binding * * @author JimP * */ -public class JmolCommands +public class JmolCommands extends StructureCommandsBase { + private static final StructureCommand SHOW_BACKBONE = new StructureCommand( + "select *; cartoons off; backbone"); + + private static final StructureCommand FOCUS_VIEW = new StructureCommand("zoom 0"); + + private static final StructureCommand COLOUR_ALL_WHITE = new StructureCommand( + "select *;color white;"); + + private static final StructureCommandI COLOUR_BY_CHARGE = new StructureCommand( + "select *;color white;select ASP,GLU;color red;" + + "select LYS,ARG;color blue;select CYS;color yellow"); + + private static final StructureCommandI COLOUR_BY_CHAIN = new StructureCommand( + "select *;color chain"); + + private static final String PIPE = "|"; + + private static final String HYPHEN = "-"; + + private static final String COLON = ":"; + + private static final String SLASH = "/"; /** - * Jmol utility which constructs the commands to colour chains by the given - * alignment + * {@inheritDoc} * - * @returns Object[] { Object[] { , + * @return + */ + @Override + public int getModelStartNo() + { + return 1; + } + + /** + * Returns a string representation of the given colour suitable for inclusion + * in Jmol commands * + * @param c + * @return */ - public static StructureMappingcommandSet[] getColourBySequenceCommand( - StructureSelectionManager ssm, String[] files, + protected String getColourString(Color c) + { + return c == null ? null + : String.format("[%d,%d,%d]", c.getRed(), c.getGreen(), + c.getBlue()); + } + + + public String[] colourBySequence(StructureSelectionManager ssm, + String[] files, SequenceI[][] sequence, SequenceRenderer sr, AlignmentViewPanel viewPanel) { + // TODO delete method + FeatureRenderer fr = viewPanel.getFeatureRenderer(); FeatureColourFinder finder = new FeatureColourFinder(fr); AlignViewportI viewport = viewPanel.getAlignViewport(); HiddenColumns cs = viewport.getAlignment().getHiddenColumns(); AlignmentI al = viewport.getAlignment(); - List cset = new ArrayList(); + List cset = new ArrayList<>(); for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) { StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]); - StringBuffer command = new StringBuffer(); - StructureMappingcommandSet smc; - ArrayList str = new ArrayList(); + StringBuilder command = new StringBuilder(128); + List str = new ArrayList<>(); if (mapping == null || mapping.length < 1) { @@ -89,7 +136,7 @@ public class JmolCommands for (int r = 0; r < asp.getLength(); r++) { // no mapping to gaps in sequence - if (jalview.util.Comparison.isGap(asp.getCharAt(r))) + if (Comparison.isGap(asp.getCharAt(r))) { continue; } @@ -127,9 +174,8 @@ public class JmolCommands String newSelcom = (mapping[m].getChain() != " " ? ":" + mapping[m].getChain() - : "") + "/" + (pdbfnum + 1) + ".1" + ";color[" - + col.getRed() + "," + col.getGreen() + "," - + col.getBlue() + "]"; + : "") + "/" + (pdbfnum + 1) + ".1" + ";color" + + getColourString(col); if (command.length() > newSelcom.length() && command .substring(command.length() - newSelcom.length()) .equals(newSelcom)) @@ -164,15 +210,14 @@ public class JmolCommands str.add(command.toString()); command.setLength(0); } - // Finally, add the command set ready to be returned. - cset.add(new StructureMappingcommandSet(JmolCommands.class, - files[pdbfnum], str.toArray(new String[str.size()]))); + cset.addAll(str); } - return cset.toArray(new StructureMappingcommandSet[cset.size()]); + return cset.toArray(new String[cset.size()]); } - public static StringBuffer condenseCommand(StringBuffer command, int pos) + public static StringBuilder condenseCommand(StringBuilder command, + int pos) { // work back to last 'select' @@ -187,7 +232,7 @@ public class JmolCommands ; } while ((q = command.indexOf("select", p)) == -1 && p > 0); - StringBuffer sb = new StringBuffer(command.substring(0, q + 7)); + StringBuilder sb = new StringBuilder(command.substring(0, q + 7)); command = command.delete(0, q + 7); @@ -207,4 +252,200 @@ public class JmolCommands return sb; } + @Override + public StructureCommandI colourByChain() + { + return COLOUR_BY_CHAIN; + } + + @Override + public List colourByCharge() + { + return Arrays.asList(COLOUR_BY_CHARGE); + } + + @Override + public List colourByResidues(Map colours) + { + List cmds = super.colourByResidues(colours); + cmds.add(0, COLOUR_ALL_WHITE); + return cmds; + } + + @Override + public StructureCommandI setBackgroundColour(Color col) + { + return new StructureCommand("background " + getColourString(col)); + } + + @Override + public StructureCommandI focusView() + { + return FOCUS_VIEW; + } + + @Override + public List showChains(List toShow) + { + StringBuilder atomSpec = new StringBuilder(128); + boolean first = true; + for (String chain : toShow) + { + String[] tokens = chain.split(":"); + if (tokens.length == 2) + { + if (!first) + { + atomSpec.append(" or "); + } + first = false; + atomSpec.append(":").append(tokens[1]).append(" /").append(tokens[0]); + } + } + + String spec = atomSpec.toString(); + String command = "select *;restrict " + spec + ";cartoon;center " + + spec; + return Arrays.asList(new StructureCommand(command)); + } + + /** + * Returns a command to superpose atoms in {@code atomSpec} to those in + * {@code refAtoms}, restricted to alpha carbons only (Phosphorous for rna). + * For example + * + *
+   * compare {2.1} {1.1} SUBSET {(*.CA | *.P) and conformation=1} 
+   *         ATOMS {1-87:A}{2-54:A|61-94:A} ROTATE TRANSLATE 1.0;
+   * 
+ * + * where {@code conformation=1} excludes ALTLOC atom locations, and 1.0 is the + * time in seconds to animate the action. For this example, atoms in model 2 + * are moved towards atoms in model 1. + *

+ * The two atomspecs should each be for one model only, but may have more than + * one chain. The number of atoms specified should be the same for both + * models, though if not, Jmol may make a 'best effort' at superposition. + * + * @see https://chemapps.stolaf.edu/jmol/docs/#compare + */ + @Override + public List superposeStructures(AtomSpecModel refAtoms, + AtomSpecModel atomSpec) + { + StringBuilder sb = new StringBuilder(64); + String refModel = refAtoms.getModels().iterator().next(); + String model2 = atomSpec.getModels().iterator().next(); + sb.append(String.format("compare {%s.1} {%s.1}", model2, refModel)); + sb.append(" SUBSET {(*.CA | *.P) and conformation=1} ATOMS {"); + + /* + * command examples don't include modelspec with atoms, getAtomSpec does; + * it works, so leave it as it is for simplicity + */ + sb.append(getAtomSpec(atomSpec, true)).append("}{"); + sb.append(getAtomSpec(refAtoms, true)).append("}"); + sb.append(" ROTATE TRANSLATE "); + sb.append(getCommandSeparator()); + + /* + * show residues used for superposition as ribbon + */ + sb.append("select ").append(getAtomSpec(atomSpec, false)).append("|"); + sb.append(getAtomSpec(refAtoms, false)).append(getCommandSeparator()) + .append("cartoons"); + + return Arrays.asList(new StructureCommand(sb.toString())); + } + + @Override + public StructureCommandI openCommandFile(String path) + { + /* + * https://chemapps.stolaf.edu/jmol/docs/#script + * not currently used in Jalview + */ + return new StructureCommand("script " + path); + } + + @Override + public StructureCommandI saveSession(String filepath) + { + /* + * https://chemapps.stolaf.edu/jmol/docs/#write + * not currently used in Jalview + */ + return new StructureCommand("write \"" + filepath + "\""); + } + + @Override + protected StructureCommandI getColourCommand(String atomSpec, Color colour) + { + StringBuilder sb = new StringBuilder(atomSpec.length()+20); + sb.append("select ").append(atomSpec).append(getCommandSeparator()) + .append("color").append(getColourString(colour)); + return new StructureCommand(sb.toString()); + } + + @Override + protected String getResidueSpec(String residue) + { + return residue; + } + + /** + * Generates a Jmol atomspec string like + * + *

+   * 2-5:A/1.1,8:A/1.1,5-10:B/2.1
+   * 
+ * + * Parameter {@code alphaOnly} is not used here - this restriction is made by + * a separate clause in the {@code compare} (superposition) command. + */ + @Override + public String getAtomSpec(AtomSpecModel model, boolean alphaOnly) + { + StringBuilder sb = new StringBuilder(128); + + boolean first = true; + for (String modelNo : model.getModels()) + { + for (String chain : model.getChains(modelNo)) + { + for (int[] range : model.getRanges(modelNo, chain)) + { + if (!first) + { + sb.append(PIPE); + } + first = false; + if (range[0] == range[1]) + { + sb.append(range[0]); + } + else + { + sb.append(range[0]).append(HYPHEN).append(range[1]); + } + sb.append(COLON).append(chain.trim()).append(SLASH); + sb.append(String.valueOf(modelNo)).append(".1"); + } + } + } + + return sb.toString(); + } + + @Override + public List showBackbone() + { + return Arrays.asList(SHOW_BACKBONE); + } + + @Override + public StructureCommandI loadFile(String file) + { + return null; + } } diff --git a/src/jalview/ext/pymol/PymolCommands.java b/src/jalview/ext/pymol/PymolCommands.java new file mode 100644 index 0000000..910aae1 --- /dev/null +++ b/src/jalview/ext/pymol/PymolCommands.java @@ -0,0 +1,213 @@ +package jalview.ext.pymol; + +import jalview.structure.AtomSpecModel; +import jalview.structure.StructureCommand; +import jalview.structure.StructureCommandI; +import jalview.structure.StructureCommandsBase; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; + +/** + * A class that generates commands to send to PyMol over its XML-RPC interface. + *

+ * Note that because the xml-rpc interface can only accept one command at a + * time, we can't concatenate commands, and must instead form and send them + * individually. + * + * @see https://pymolwiki.org/index.php/Category:Commands + * @see https://pymolwiki.org/index.php/RPC + */ +public class PymolCommands extends StructureCommandsBase +{ + private static final StructureCommand COLOUR_BY_CHAIN = new StructureCommand("spectrum", "chain"); + + private static final List COLOR_BY_CHARGE = new ArrayList<>(); + + private static final List SHOW_BACKBONE = new ArrayList<>(); + + static { + COLOR_BY_CHARGE.add(new StructureCommand("color", "white", "*")); + COLOR_BY_CHARGE + .add(new StructureCommand("color", "red", "resn ASP resn GLU")); + COLOR_BY_CHARGE.add( + new StructureCommand("color", "blue", "resn LYS resn ARG")); + COLOR_BY_CHARGE + .add(new StructureCommand("color", "yellow", "resn CYS")); + SHOW_BACKBONE.add(new StructureCommand("hide", "everything")); + SHOW_BACKBONE.add(new StructureCommand("show", "ribbon")); + } + + @Override + public StructureCommandI colourByChain() + { + return COLOUR_BY_CHAIN; + } + + @Override + public List colourByCharge() + { + return COLOR_BY_CHARGE; + } + + @Override + public StructureCommandI setBackgroundColour(Color col) + { + // https://pymolwiki.org/index.php/Bg_Color + return new StructureCommand("bg_color", getColourString(col)); + } + + /** + * Returns a colour formatted suitable for use in viewer command syntax. For + * example, red is {@code "0xff0000"}. + * + * @param c + * @return + */ + protected String getColourString(Color c) + { + return String.format("0x%02x%02x%02x", c.getRed(), c.getGreen(), + c.getBlue()); + } + + @Override + public StructureCommandI focusView() + { + // TODO what? + return null; + } + + @Override + public List showChains(List toShow) + { + // https://pymolwiki.org/index.php/Show + List commands = new ArrayList<>(); + commands.add(new StructureCommand("hide", "everything")); + commands.add(new StructureCommand("show", "lines")); + StringBuilder chains = new StringBuilder(); + for (String chain : toShow) + { + chains.append(" chain ").append(chain); + } + commands.add(new StructureCommand("show", "cartoon", chains.toString())); + return commands; + } + + @Override + public List superposeStructures(AtomSpecModel refAtoms, + AtomSpecModel atomSpec) + { + // https://pymolwiki.org/index.php/Super + List commands = new ArrayList<>(); + String refAtomsAlphaOnly = getAtomSpec(refAtoms, true); + String atomSpec2AlphaOnly = getAtomSpec(atomSpec, true); + commands.add(new StructureCommand("super", refAtomsAlphaOnly, + atomSpec2AlphaOnly)); + + /* + * and show superposed residues as cartoon + */ + String refAtomsAll = getAtomSpec(refAtoms, false); + String atomSpec2All = getAtomSpec(atomSpec, false); + commands.add(new StructureCommand("show", "cartoon", + refAtomsAll + " " + atomSpec2All)); + + return commands; + } + + @Override + public StructureCommandI openCommandFile(String path) + { + // where is this documented by PyMol? + // todo : xml-rpc answers 'method "@" is not supported' + return new StructureCommand("@" + path); // should be .pml + } + + @Override + public StructureCommandI saveSession(String filepath) + { + // https://pymolwiki.org/index.php/Save#EXAMPLES + return new StructureCommand("save", filepath); // should be .pse + } + + /** + * Returns a selection string in PyMOL 'selection macro' format: + * + *

+   * modelId// chain/residues/
+   * 
+ * + * If more than one chain, makes a selection expression for each, and they are + * separated by spaces. + * + * @see https://pymolwiki.org/index.php/Selection_Macros + */ + @Override + public String getAtomSpec(AtomSpecModel model, boolean alphaOnly) + { + StringBuilder sb = new StringBuilder(64); + boolean first = true; + for (String modelId : model.getModels()) + { + for (String chain : model.getChains(modelId)) + { + if (!first) + { + sb.append(" "); + } + first = false; + List rangeList = model.getRanges(modelId, chain); + chain = chain.trim(); + sb.append(modelId).append("//").append(chain).append("/"); + boolean firstRange = true; + for (int[] range : rangeList) + { + if (!firstRange) + { + sb.append("+"); + } + firstRange = false; + sb.append(String.valueOf(range[0])); + if (range[0] != range[1]) + { + sb.append("-").append(String.valueOf(range[1])); + } + } + sb.append("/"); + if (alphaOnly) + { + sb.append("CA"); + } + } + } + return sb.toString(); + } + + @Override + public List showBackbone() + { + return SHOW_BACKBONE; + } + + @Override + protected StructureCommandI getColourCommand(String atomSpec, Color colour) + { + // https://pymolwiki.org/index.php/Color + return new StructureCommand("color", getColourString(colour), atomSpec); + } + + @Override + protected String getResidueSpec(String residue) + { + // https://pymolwiki.org/index.php/Selection_Algebra + return "resn " + residue; + } + + @Override + public StructureCommandI loadFile(String file) + { + return new StructureCommand("load", file); + } + +} diff --git a/src/jalview/ext/pymol/PymolManager.java b/src/jalview/ext/pymol/PymolManager.java new file mode 100644 index 0000000..ad44677 --- /dev/null +++ b/src/jalview/ext/pymol/PymolManager.java @@ -0,0 +1,298 @@ +package jalview.ext.pymol; + +import jalview.bin.Cache; +import jalview.gui.Preferences; +import jalview.structure.StructureCommand; +import jalview.structure.StructureCommandI; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +public class PymolManager +{ + private static final int RPC_REPLY_TIMEOUT_MS = 15000; + + private static final int CONNECTION_TIMEOUT_MS = 100; + + private static final String POST1 = ""; + + private static final String POST2 = ""; + + private static final String POST3 = ""; + + private Process pymolProcess; + + private int pymolXmlRpcPort; + + /** + * Returns a list of paths to try for the PyMOL executable. Any user + * preference is placed first, otherwise 'standard' paths depending on the + * operating system. + * + * @return + */ + public static List getPymolPaths() + { + List pathList = new ArrayList<>(); + + String userPath = Cache + .getDefault(Preferences.PYMOL_PATH, null); + if (userPath != null) + { + pathList.add(0, userPath); + } + + /* + * add default installation paths + */ + String pymol = "PyMOL"; + String os = System.getProperty("os.name"); + if (os.startsWith("Linux")) + { + pathList.add("/usr/local/pymol/bin/" + pymol); + pathList.add("/usr/local/bin/" + pymol); + pathList.add("/usr/bin/" + pymol); + pathList.add(System.getProperty("user.home") + "/opt/bin/" + pymol); + } + else if (os.startsWith("Windows")) + { + // todo Windows installation path(s) + } + else if (os.startsWith("Mac")) + { + pathList.add("/Applications/PyMOL.app/Contents/MacOS/" + pymol); + } + return pathList; + } + + public boolean isPymolLaunched() + { + // TODO pull up generic methods for external viewer processes + boolean launched = false; + if (pymolProcess != null) + { + try + { + pymolProcess.exitValue(); + // if we get here, process has ended + } catch (IllegalThreadStateException e) + { + // ok - not yet terminated + launched = true; + } + } + return launched; + } + + public void exitPymol() + { + if (isPymolLaunched() && pymolProcess != null) + { + sendCommand(new StructureCommand("quit"), false); + } + pymolProcess = null; + // currentModelsMap.clear(); + this.pymolXmlRpcPort = 0; + } + + /** + * Sends the command to Pymol; if requested, tries to get and return any + * replies, else returns null + * + * @param command + * @param getReply + * @return + */ + public List sendCommand(StructureCommandI command, + boolean getReply) + { + String postBody = getPostRequest(command); + // System.out.println(postBody);// debug + String rpcUrl = "http://127.0.0.1:" + this.pymolXmlRpcPort; + PrintWriter out = null; + BufferedReader in = null; + List result = new ArrayList<>(); + try + { + URL realUrl = new URL(rpcUrl); + URLConnection conn = realUrl.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("content-type", "text/xml"); + conn.setDoOutput(true); + conn.setDoInput(true); + out = new PrintWriter(conn.getOutputStream()); + out.print(postBody); + out.flush(); + in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + String line; + while ((line = in.readLine()) != null) + { + result.add(line); + } + } catch (Exception e) + { + e.printStackTrace(); + } finally + { + try + { + if (out != null) + { + out.close(); + } + if (in != null) + { + in.close(); + } + } catch (IOException ex) + { + ex.printStackTrace(); + } + } + return result; + } + + /** + * Builds the body of the XML-RPC format POST request to execute the command + * + * @param command + * @return + */ + static String getPostRequest(StructureCommandI command) + { + StringBuilder sb = new StringBuilder(64); + sb.append(POST1).append(command.getCommand()).append(POST2); + if (command.hasParameters()) + { + for (String p : command.getParameters()) + { + /* + * for now assuming all are string - element is optional + * refactor in future if other data types needed + * https://www.tutorialspoint.com/xml-rpc/xml_rpc_data_model.htm + */ + sb.append("").append(p) + .append(""); + } + } + sb.append(POST3); + return sb.toString(); + } + + public boolean launchPymol() + { + // todo pull up much of this + // Do nothing if already launched + if (isPymolLaunched()) + { + return true; + } + + String error = "Error message: "; + for (String pymolPath : getPymolPaths()) + { + try + { + // ensure symbolic links are resolved + pymolPath = Paths.get(pymolPath).toRealPath().toString(); + File path = new File(pymolPath); + // uncomment the next line to simulate Pymol not installed + // path = new File(pymolPath + "x"); + if (!path.canExecute()) + { + error += "File '" + path + "' does not exist.\n"; + continue; + } + List args = new ArrayList<>(); + args.add(pymolPath); + args.add("-R"); // https://pymolwiki.org/index.php/RPC + ProcessBuilder pb = new ProcessBuilder(args); + pymolProcess = pb.start(); + error = ""; + break; + } catch (Exception e) + { + // pPymol could not be started using this path + error += e.getMessage(); + } + } + if (error.length() == 0) + { + this.pymolXmlRpcPort = getPortNumber(); + System.out.println( + "PyMOL XMLRPC started on port " + pymolXmlRpcPort); + return (pymolXmlRpcPort > 0); + } + + // logger.warn(error); + return false; + } + + private int getPortNumber() + { + // TODO pull up most of this! + int port = 0; + InputStream readChan = pymolProcess.getInputStream(); + BufferedReader lineReader = new BufferedReader( + new InputStreamReader(readChan)); + StringBuilder responses = new StringBuilder(); + try + { + String response = lineReader.readLine(); + while (response != null) + { + responses.append("\n" + response); + // expect: xml-rpc server running on host localhost, port 9123 + if (response.contains("xml-rpc")) + { + String[] tokens = response.split(" "); + for (int i = 0; i < tokens.length - 1; i++) + { + if ("port".equals(tokens[i])) + { + port = Integer.parseInt(tokens[i + 1]); + break; + } + } + } + if (port > 0) + { + break; // hack for hanging readLine() + } + response = lineReader.readLine(); + } + } catch (Exception e) + { + System.err.println( + "Failed to get REST port number from " + responses + ": " + + e.getMessage()); + // logger.error("Failed to get REST port number from " + responses + ": " + // + e.getMessage()); + } finally + { + try + { + lineReader.close(); + } catch (IOException e2) + { + } + } + if (port == 0) + { + System.err.println("Failed to start PyMOL with XMLRPC, response was: " + + responses); + } + System.err.println("PyMOL started with XMLRPC on port " + port); + return port; + } + +} diff --git a/src/jalview/ext/rbvi/chimera/AtomSpecModel.java b/src/jalview/ext/rbvi/chimera/AtomSpecModel.java deleted file mode 100644 index 39d6704..0000000 --- a/src/jalview/ext/rbvi/chimera/AtomSpecModel.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * 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.util.IntRangeComparator; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -/** - * A class to model a Chimera atomspec pattern, for example - * - *
- * #0:15.A,28.A,54.A,63.A,70-72.A,83-84.A,97-98.A|#1:2.A,6.A,11.A,13-14.A,70.A,82.A,96-97.A
- * 
- * - * where - *
    - *
  • #0 is a model number
  • - *
  • 15 or 70-72 is a residue number, or range of residue numbers
  • - *
  • .A is a chain identifier
  • - *
  • residue ranges are separated by comma
  • - *
  • atomspecs for distinct models are separated by | (or)
  • - *
- * - *
- * @see http://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec.html
- * 
- */ -public class AtomSpecModel -{ - private Map>> atomSpec; - - /** - * Constructor - */ - public AtomSpecModel() - { - atomSpec = new TreeMap>>(); - } - - /** - * Adds one contiguous range to this atom spec - * - * @param model - * @param startPos - * @param endPos - * @param chain - */ - public void addRange(int model, int startPos, int endPos, String chain) - { - /* - * Get/initialize map of data for the colour and model - */ - Map> modelData = atomSpec.get(model); - if (modelData == null) - { - atomSpec.put(model, modelData = new TreeMap>()); - } - - /* - * Get/initialize map of data for colour, model and chain - */ - List chainData = modelData.get(chain); - if (chainData == null) - { - chainData = new ArrayList(); - modelData.put(chain, chainData); - } - - /* - * Add the start/end positions - */ - chainData.add(new int[] { startPos, endPos }); - // TODO add intelligently, using a RangeList class - } - - /** - * Returns the range(s) formatted as a Chimera atomspec - * - * @return - */ - public String getAtomSpec() - { - StringBuilder sb = new StringBuilder(128); - boolean firstModel = true; - for (Integer model : atomSpec.keySet()) - { - if (!firstModel) - { - sb.append("|"); - } - firstModel = false; - sb.append("#").append(model).append(":"); - - boolean firstPositionForModel = true; - final Map> modelData = atomSpec.get(model); - - for (String chain : modelData.keySet()) - { - chain = " ".equals(chain) ? chain : chain.trim(); - - List rangeList = modelData.get(chain); - - /* - * sort ranges into ascending start position order - */ - Collections.sort(rangeList, IntRangeComparator.ASCENDING); - - int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0]; - int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1]; - - Iterator iterator = rangeList.iterator(); - while (iterator.hasNext()) - { - int[] range = iterator.next(); - if (range[0] <= end + 1) - { - /* - * range overlaps or is contiguous with the last one - * - so just extend the end position, and carry on - * (unless this is the last in the list) - */ - end = Math.max(end, range[1]); - } - else - { - /* - * we have a break so append the last range - */ - appendRange(sb, start, end, chain, firstPositionForModel); - firstPositionForModel = false; - start = range[0]; - end = range[1]; - } - } - - /* - * and append the last range - */ - if (!rangeList.isEmpty()) - { - appendRange(sb, start, end, chain, firstPositionForModel); - firstPositionForModel = false; - } - } - } - return sb.toString(); - } - - /** - * @param sb - * @param start - * @param end - * @param chain - * @param firstPositionForModel - */ - protected void appendRange(StringBuilder sb, int start, int end, - String chain, boolean firstPositionForModel) - { - if (!firstPositionForModel) - { - sb.append(","); - } - if (end == start) - { - sb.append(start); - } - else - { - sb.append(start).append("-").append(end); - } - - sb.append("."); - if (!" ".equals(chain)) { - sb.append(chain); - } - } -} diff --git a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java index 3caaac3..c355abe 100644 --- a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java +++ b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java @@ -23,22 +23,22 @@ package jalview.ext.rbvi.chimera; import jalview.api.AlignViewportI; import jalview.api.AlignmentViewPanel; import jalview.api.FeatureRenderer; -import jalview.api.SequenceRenderer; import jalview.datamodel.AlignmentI; -import jalview.datamodel.HiddenColumns; import jalview.datamodel.MappedFeatures; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.gui.Desktop; -import jalview.renderer.seqfeatures.FeatureColourFinder; +import jalview.structure.AtomSpecModel; +import jalview.structure.StructureCommand; +import jalview.structure.StructureCommandI; +import jalview.structure.StructureCommandsBase; import jalview.structure.StructureMapping; -import jalview.structure.StructureMappingcommandSet; import jalview.structure.StructureSelectionManager; import jalview.util.ColorUtils; -import jalview.util.Comparison; import java.awt.Color; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -50,274 +50,45 @@ import java.util.Map; * @author JimP * */ -public class ChimeraCommands +public class ChimeraCommands extends StructureCommandsBase { + private static final StructureCommand SHOW_BACKBONE = new StructureCommand( + "~display all;~ribbon;chain @CA|P"); public static final String NAMESPACE_PREFIX = "jv_"; - /** - * Constructs Chimera commands to colour residues as per the Jalview alignment - * - * @param ssm - * @param files - * @param sequence - * @param sr - * @param fr - * @param viewPanel - * @return - */ - public static StructureMappingcommandSet[] getColourBySequenceCommand( - StructureSelectionManager ssm, String[] files, - SequenceI[][] sequence, SequenceRenderer sr, - AlignmentViewPanel viewPanel) - { - Map colourMap = buildColoursMap(ssm, files, - sequence, sr, viewPanel); - - List colourCommands = buildColourCommands(colourMap); + private static final StructureCommandI COLOUR_BY_CHARGE = new StructureCommand( + "color white;color red ::ASP,GLU;color blue ::LYS,ARG;color yellow ::CYS"); - StructureMappingcommandSet cs = new StructureMappingcommandSet( - ChimeraCommands.class, null, - colourCommands.toArray(new String[colourCommands.size()])); + private static final StructureCommandI COLOUR_BY_CHAIN = new StructureCommand( + "rainbow chain"); - return new StructureMappingcommandSet[] { cs }; - } + // Chimera clause to exclude alternate locations in atom selection + private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9"; - /** - * Traverse the map of colours/models/chains/positions to construct a list of - * 'color' commands (one per distinct colour used). The format of each command - * is - * - *
-   * 
- * color colorname #modelnumber:range.chain - * e.g. color #00ff00 #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,... - *
- *
- * - * @param colourMap - * @return - */ - protected static List buildColourCommands( - Map colourMap) + @Override + public StructureCommandI getColourCommand(String atomSpec, Color colour) { - /* - * This version concatenates all commands into a single String (semi-colon - * delimited). If length limit issues arise, refactor to return one color - * command per colour. - */ - List commands = new ArrayList<>(); - StringBuilder sb = new StringBuilder(256); - boolean firstColour = true; - for (Object key : colourMap.keySet()) - { - Color colour = (Color) key; - String colourCode = ColorUtils.toTkCode(colour); - if (!firstColour) - { - sb.append("; "); - } - sb.append("color ").append(colourCode).append(" "); - firstColour = false; - final AtomSpecModel colourData = colourMap.get(colour); - sb.append(colourData.getAtomSpec()); - } - commands.add(sb.toString()); - return commands; + // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/color.html + String colourCode = getColourString(colour); + return new StructureCommand("color " + colourCode + " " + atomSpec); } /** - * Traverses a map of { modelNumber, {chain, {list of from-to ranges} } } and - * builds a Chimera format atom spec + * Returns a colour formatted suitable for use in viewer command syntax * - * @param modelAndChainRanges - */ - protected static String getAtomSpec( - Map>> modelAndChainRanges) - { - StringBuilder sb = new StringBuilder(128); - boolean firstModelForColour = true; - for (Integer model : modelAndChainRanges.keySet()) - { - boolean firstPositionForModel = true; - if (!firstModelForColour) - { - sb.append("|"); - } - firstModelForColour = false; - sb.append("#").append(model).append(":"); - - final Map> modelData = modelAndChainRanges - .get(model); - for (String chain : modelData.keySet()) - { - boolean hasChain = !"".equals(chain.trim()); - for (int[] range : modelData.get(chain)) - { - if (!firstPositionForModel) - { - sb.append(","); - } - if (range[0] == range[1]) - { - sb.append(range[0]); - } - else - { - sb.append(range[0]).append("-").append(range[1]); - } - if (hasChain) - { - sb.append(".").append(chain); - } - firstPositionForModel = false; - } - } - } - return sb.toString(); - } - - /** - *
-   * Build a data structure which records contiguous subsequences for each colour. 
-   * From this we can easily generate the Chimera command for colour by sequence.
-   * Color
-   *     Model number
-   *         Chain
-   *             list of start/end ranges
-   * Ordering is by order of addition (for colours and positions), natural ordering (for models and chains)
-   * 
- */ - protected static Map buildColoursMap( - StructureSelectionManager ssm, String[] files, - SequenceI[][] sequence, SequenceRenderer sr, - AlignmentViewPanel viewPanel) - { - FeatureRenderer fr = viewPanel.getFeatureRenderer(); - FeatureColourFinder finder = new FeatureColourFinder(fr); - AlignViewportI viewport = viewPanel.getAlignViewport(); - HiddenColumns cs = viewport.getAlignment().getHiddenColumns(); - AlignmentI al = viewport.getAlignment(); - Map colourMap = new LinkedHashMap<>(); - Color lastColour = null; - - for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) - { - StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]); - - if (mapping == null || mapping.length < 1) - { - continue; - } - - int startPos = -1, lastPos = -1; - String lastChain = ""; - for (int s = 0; s < sequence[pdbfnum].length; s++) - { - for (int sp, m = 0; m < mapping.length; m++) - { - final SequenceI seq = sequence[pdbfnum][s]; - if (mapping[m].getSequence() == seq - && (sp = al.findIndex(seq)) > -1) - { - SequenceI asp = al.getSequenceAt(sp); - for (int r = 0; r < asp.getLength(); r++) - { - // no mapping to gaps in sequence - if (Comparison.isGap(asp.getCharAt(r))) - { - continue; - } - int pos = mapping[m].getPDBResNum(asp.findPosition(r)); - - if (pos < 1 || pos == lastPos) - { - continue; - } - - Color colour = sr.getResidueColour(seq, r, finder); - - /* - * darker colour for hidden regions - */ - if (!cs.isVisible(r)) - { - colour = Color.GRAY; - } - - final String chain = mapping[m].getChain(); - - /* - * Just keep incrementing the end position for this colour range - * _unless_ colour, PDB model or chain has changed, or there is a - * gap in the mapped residue sequence - */ - final boolean newColour = !colour.equals(lastColour); - final boolean nonContig = lastPos + 1 != pos; - final boolean newChain = !chain.equals(lastChain); - if (newColour || nonContig || newChain) - { - if (startPos != -1) - { - addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos, - lastPos, lastChain); - } - startPos = pos; - } - lastColour = colour; - lastPos = pos; - lastChain = chain; - } - // final colour range - if (lastColour != null) - { - addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos, - lastPos, lastChain); - } - // break; - } - } - } - } - return colourMap; - } - - /** - * Helper method to add one contiguous range to the AtomSpec model for the given - * value (creating the model if necessary). As used by Jalview, {@code value} is - *
    - *
  • a colour, when building a 'colour structure by sequence' command
  • - *
  • a feature value, when building a 'set Chimera attributes from features' - * command
  • - *
- * - * @param map - * @param value - * @param model - * @param startPos - * @param endPos - * @param chain + * @param colour + * @return */ - protected static void addAtomSpecRange(Map map, - Object value, int model, int startPos, int endPos, String chain) + protected String getColourString(Color colour) { - /* - * Get/initialize map of data for the colour - */ - AtomSpecModel atomSpec = map.get(value); - if (atomSpec == null) - { - atomSpec = new AtomSpecModel(); - map.put(value, atomSpec); - } - - atomSpec.addRange(model, startPos, endPos, chain); + return ColorUtils.toTkCode(colour); } /** * Constructs and returns Chimera commands to set attributes on residues - * corresponding to features in Jalview. Attribute names are the Jalview - * feature type, with a "jv_" prefix. + * corresponding to features in Jalview. Attribute names are the Jalview feature + * type, with a "jv_" prefix. * * @param ssm * @param files @@ -325,20 +96,15 @@ public class ChimeraCommands * @param viewPanel * @return */ - public static StructureMappingcommandSet getSetAttributeCommandsForFeatures( + @Override + public List setAttributesForFeatures( StructureSelectionManager ssm, String[] files, SequenceI[][] seqs, AlignmentViewPanel viewPanel) { Map> featureMap = buildFeaturesMap( ssm, files, seqs, viewPanel); - List commands = buildSetAttributeCommands(featureMap); - - StructureMappingcommandSet cs = new StructureMappingcommandSet( - ChimeraCommands.class, null, - commands.toArray(new String[commands.size()])); - - return cs; + return setAttributes(featureMap); } /** @@ -353,7 +119,7 @@ public class ChimeraCommands * @param viewPanel * @return */ - protected static Map> buildFeaturesMap( + protected Map> buildFeaturesMap( StructureSelectionManager ssm, String[] files, SequenceI[][] seqs, AlignmentViewPanel viewPanel) { @@ -393,6 +159,8 @@ public class ChimeraCommands AlignmentI alignment = viewPanel.getAlignment(); for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) { + final int modelNumber = pdbfnum + getModelStartNo(); + String modelId = String.valueOf(modelNumber); StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]); if (mapping == null || mapping.length < 1) @@ -416,12 +184,12 @@ public class ChimeraCommands if (!visibleFeatures.isEmpty()) { scanSequenceFeatures(visibleFeatures, structureMapping, seq, - theMap, pdbfnum); + theMap, modelId); } if (showLinkedFeatures) { scanComplementFeatures(complementRenderer, structureMapping, - seq, theMap, pdbfnum); + seq, theMap, modelId); } } } @@ -443,7 +211,8 @@ public class ChimeraCommands protected static void scanComplementFeatures( FeatureRenderer complementRenderer, StructureMapping structureMapping, SequenceI seq, - Map> theMap, int modelNumber) + Map> theMap, + String modelNumber) { /* * for each sequence residue mapped to a structure position... @@ -507,19 +276,19 @@ public class ChimeraCommands } /** - * Inspect features on the sequence; for each feature that is visible, determine - * its mapped ranges in the structure (if any) according to the given mapping, - * and add them to the map. + * Inspect features on the sequence; for each feature that is visible, + * determine its mapped ranges in the structure (if any) according to the + * given mapping, and add them to the map. * * @param visibleFeatures * @param mapping * @param seq * @param theMap - * @param modelNumber + * @param modelId */ protected static void scanSequenceFeatures(List visibleFeatures, StructureMapping mapping, SequenceI seq, - Map> theMap, int modelNumber) + Map> theMap, String modelId) { List sfs = seq.getFeatures().getPositionalFeatures( visibleFeatures.toArray(new String[visibleFeatures.size()])); @@ -559,7 +328,7 @@ public class ChimeraCommands } for (int[] range : mappedRanges) { - addAtomSpecRange(featureValues, value, modelNumber, range[0], + addAtomSpecRange(featureValues, value, modelId, range[0], range[1], mapping.getChain()); } } @@ -574,17 +343,17 @@ public class ChimeraCommands * *
    * 
setattr r " " #modelnumber:range.chain - * e.g. setattr r jv:chain #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,... + * e.g. setattr r jv_chain <value> #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,... *
*
* * @param featureMap * @return */ - protected static List buildSetAttributeCommands( + protected List setAttributes( Map> featureMap) { - List commands = new ArrayList<>(); + List commands = new ArrayList<>(); for (String featureType : featureMap.keySet()) { String attributeName = makeAttributeName(featureType); @@ -603,13 +372,12 @@ public class ChimeraCommands * add a command to set the attribute on the mapped residues * Put values in single quotes, encoding any embedded single quotes */ - StringBuilder sb = new StringBuilder(128); + AtomSpecModel atomSpecModel = values.get(value); String featureValue = value.toString(); featureValue = featureValue.replaceAll("\\'", "'"); - sb.append("setattr r ").append(attributeName).append(" '") - .append(featureValue).append("' "); - sb.append(values.get(value).getAtomSpec()); - commands.add(sb.toString()); + StructureCommandI cmd = setAttribute(attributeName, featureValue, + atomSpecModel); + commands.add(cmd); } } @@ -617,16 +385,37 @@ public class ChimeraCommands } /** + * Returns a viewer command to set the given residue attribute value on + * residues specified by the AtomSpecModel, for example + * + *
+   * setatr res jv_chain 'primary' #1:12-34,48-55.B
+   * 
+ * + * @param attributeName + * @param attributeValue + * @param atomSpecModel + * @return + */ + protected StructureCommandI setAttribute(String attributeName, + String attributeValue, + AtomSpecModel atomSpecModel) + { + StringBuilder sb = new StringBuilder(128); + sb.append("setattr res ").append(attributeName).append(" '") + .append(attributeValue).append("' "); + sb.append(getAtomSpec(atomSpecModel, false)); + return new StructureCommand(sb.toString()); + } + + /** * Makes a prefixed and valid Chimera attribute name. A jv_ prefix is applied * for a 'Jalview' namespace, and any non-alphanumeric character is converted * to an underscore. * * @param featureType * @return - * - *
-   * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html
-   *         
+ * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html */ protected static String makeAttributeName(String featureType) { @@ -652,4 +441,204 @@ public class ChimeraCommands return attName; } + @Override + public StructureCommandI colourByChain() + { + return COLOUR_BY_CHAIN; + } + + @Override + public List colourByCharge() + { + return Arrays.asList(COLOUR_BY_CHARGE); + } + + @Override + public String getResidueSpec(String residue) + { + return "::" + residue; + } + + @Override + public StructureCommandI setBackgroundColour(Color col) + { + // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/set.html#bgcolor + return new StructureCommand("set bgColor " + ColorUtils.toTkCode(col)); + } + + @Override + public StructureCommandI focusView() + { + // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/focus.html + return new StructureCommand("focus"); + } + + @Override + public List showChains(List toShow) + { + /* + * Construct a chimera command like + * + * ~display #*;~ribbon #*;ribbon :.A,:.B + */ + StringBuilder cmd = new StringBuilder(64); + boolean first = true; + for (String chain : toShow) + { + String[] tokens = chain.split(":"); + if (tokens.length == 2) + { + String showChainCmd = tokens[0] + ":." + tokens[1]; + if (!first) + { + cmd.append(","); + } + cmd.append(showChainCmd); + first = false; + } + } + + /* + * 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(); + return Arrays.asList(new StructureCommand(command)); + } + + @Override + public List superposeStructures(AtomSpecModel ref, + AtomSpecModel spec) + { + /* + * Form Chimera match command to match spec to ref + * (the first set of atoms are moved on to the second) + * + * 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 + */ + StringBuilder cmd = new StringBuilder(); + String atomSpecAlphaOnly = getAtomSpec(spec, true); + String refSpecAlphaOnly = getAtomSpec(ref, true); + cmd.append("match ").append(atomSpecAlphaOnly).append(" ").append(refSpecAlphaOnly); + + /* + * show superposed residues as ribbon + */ + String atomSpec = getAtomSpec(spec, false); + String refSpec = getAtomSpec(ref, false); + cmd.append("; ribbon "); + cmd.append(atomSpec).append("|").append(refSpec).append("; focus"); + + return Arrays.asList(new StructureCommand(cmd.toString())); + } + + @Override + public StructureCommandI openCommandFile(String path) + { + // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/filetypes.html + return new StructureCommand("open cmd:" + path); + } + + @Override + public StructureCommandI saveSession(String filepath) + { + // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/save.html + return new StructureCommand("save " + filepath); + } + + /** + * Returns the range(s) modelled by {@code atomSpec} formatted as a Chimera + * atomspec string, e.g. + * + *
+   * #0:15.A,28.A,54.A,70-72.A|#1:2.A,6.A,11.A,13-14.A
+   * 
+ * + * where + *
    + *
  • #0 is a model number
  • + *
  • 15 or 70-72 is a residue number, or range of residue numbers
  • + *
  • .A is a chain identifier
  • + *
  • residue ranges are separated by comma
  • + *
  • atomspecs for distinct models are separated by | (or)
  • + *
+ * + *
+   * 
+   * @param model
+   * @param alphaOnly
+   * @return
+   * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec.html
+   */
+  @Override
+  public String getAtomSpec(AtomSpecModel atomSpec, boolean alphaOnly)
+  {
+    StringBuilder sb = new StringBuilder(128);
+    boolean firstModel = true;
+    for (String model : atomSpec.getModels())
+    {
+      if (!firstModel)
+      {
+        sb.append("|");
+      }
+      firstModel = false;
+      appendModel(sb, model, atomSpec, alphaOnly);
+    }
+    return sb.toString();
+  }
+
+  /**
+   * A helper method to append an atomSpec string for atoms in the given model
+   * 
+   * @param sb
+   * @param model
+   * @param atomSpec
+   * @param alphaOnly
+   */
+  protected void appendModel(StringBuilder sb, String model,
+          AtomSpecModel atomSpec, boolean alphaOnly)
+  {
+    sb.append("#").append(model).append(":");
+
+    boolean firstPositionForModel = true;
+
+    for (String chain : atomSpec.getChains(model))
+    {
+      chain = " ".equals(chain) ? chain : chain.trim();
+
+      List rangeList = atomSpec.getRanges(model, chain);
+      for (int[] range : rangeList)
+      {
+        appendRange(sb, range[0], range[1], chain, firstPositionForModel,
+                false);
+        firstPositionForModel = false;
+      }
+    }
+    if (alphaOnly)
+    {
+      /*
+       * restrict to alpha carbon, no alternative locations
+       * (needed to ensuring matching atom counts for superposition)
+       */
+      // TODO @P instead if RNA - add nucleotide flag to AtomSpecModel?
+      sb.append("@CA").append(NO_ALTLOCS);
+    }
+  }
+
+  @Override
+  public List showBackbone()
+  {
+    return Arrays.asList(SHOW_BACKBONE);
+  }
+
+  @Override
+  public StructureCommandI loadFile(String file)
+  {
+    return new StructureCommand("open " + file);
+  }
+
 }
diff --git a/src/jalview/ext/rbvi/chimera/ChimeraListener.java b/src/jalview/ext/rbvi/chimera/ChimeraListener.java
index a0d74bc..40b0ff0 100644
--- a/src/jalview/ext/rbvi/chimera/ChimeraListener.java
+++ b/src/jalview/ext/rbvi/chimera/ChimeraListener.java
@@ -114,17 +114,25 @@ public class ChimeraListener extends AbstractRequestHandler
   {
     // dumpRequest(request);
     String message = request.getParameter(CHIMERA_NOTIFICATION);
-    if (SELECTION_CHANGED.equals(message))
+    if (message == null)
     {
-      this.chimeraBinding.highlightChimeraSelection();
+      message = request.getParameter("chimerax_notification");
     }
-    else if (message != null && message.startsWith(MODEL_CHANGED))
+    if (message != null)
     {
-      processModelChanged(message.substring(MODEL_CHANGED.length()));
-    }
-    else
-    {
-      System.err.println("Unexpected chimeraNotification: " + message);
+      if (message.startsWith("SelectionChanged"))
+      {
+        this.chimeraBinding.highlightChimeraSelection();
+      }
+      else if (message.startsWith(MODEL_CHANGED))
+      {
+        System.err.println(message);
+        processModelChanged(message.substring(MODEL_CHANGED.length()));
+      }
+      else
+      {
+        System.err.println("Unexpected chimeraNotification: " + message);
+      }
     }
   }
 
diff --git a/src/jalview/ext/rbvi/chimera/ChimeraXCommands.java b/src/jalview/ext/rbvi/chimera/ChimeraXCommands.java
new file mode 100644
index 0000000..3341199
--- /dev/null
+++ b/src/jalview/ext/rbvi/chimera/ChimeraXCommands.java
@@ -0,0 +1,225 @@
+/*
+ * 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.structure.AtomSpecModel;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureCommandI;
+import jalview.util.ColorUtils;
+
+import java.awt.Color;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Routines for generating ChimeraX commands for Jalview/ChimeraX binding
+ */
+public class ChimeraXCommands extends ChimeraCommands
+{
+  private static final StructureCommand SHOW_BACKBONE = new StructureCommand(
+          "~display all;~ribbon;show @CA|P atoms");
+
+  private static final StructureCommand FOCUS_VIEW = new StructureCommand(
+          "view");
+
+  private static final StructureCommandI COLOUR_BY_CHARGE = new StructureCommand(
+          "color white;color :ASP,GLU red;color :LYS,ARG blue;color :CYS yellow");
+
+  @Override
+  public List colourByCharge()
+  {
+    return Arrays.asList(COLOUR_BY_CHARGE);
+  }
+
+  @Override
+  public String getResidueSpec(String residue)
+  {
+    return ":" + residue;
+  }
+
+  @Override
+  public StructureCommandI setBackgroundColour(Color col)
+  {
+    // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/set.html
+    return new StructureCommand("set bgColor " + ColorUtils.toTkCode(col));
+  }
+
+  @Override
+  public StructureCommandI getColourCommand(String atomSpec, Color colour)
+  {
+    // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/color.html
+    String colourCode = getColourString(colour);
+
+    return new StructureCommand("color " + atomSpec + " " + colourCode);
+  }
+
+  @Override
+  public StructureCommandI focusView()
+  {
+    // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/view.html
+    return FOCUS_VIEW;
+  }
+
+  /**
+   * {@inheritDoc}
+   * 
+   * @return
+   */
+  @Override
+  public int getModelStartNo()
+  {
+    return 1;
+  }
+
+  /**
+   * Returns a viewer command to set the given residue attribute value on
+   * residues specified by the AtomSpecModel, for example
+   * 
+   * 
+   * setattr #0/A:3-9,14-20,39-43 res jv_strand 'strand' create true
+   * 
+ * + * @param attributeName + * @param attributeValue + * @param atomSpecModel + * @return + */ + @Override + protected StructureCommandI setAttribute(String attributeName, + String attributeValue, AtomSpecModel atomSpecModel) + { + StringBuilder sb = new StringBuilder(128); + sb.append("setattr ").append(getAtomSpec(atomSpecModel, false)); + sb.append(" res ").append(attributeName).append(" '") + .append(attributeValue).append("'"); + sb.append(" create true"); + return new StructureCommand(sb.toString()); + } + + @Override + public StructureCommandI openCommandFile(String path) + { + // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/open.html + return new StructureCommand("open " + path); + } + + @Override + public StructureCommandI saveSession(String filepath) + { + // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/save.html + return new StructureCommand("save session " + filepath); + } + + /** + * Returns the range(s) formatted as a ChimeraX atomspec, for example + *

+ * #1/A:2-20,30-40/B:10-20|#2/A:12-30 + * + * @return + */ + @Override + public String getAtomSpec(AtomSpecModel atomSpec, boolean alphaOnly) + { + StringBuilder sb = new StringBuilder(128); + boolean firstModel = true; + for (String model : atomSpec.getModels()) + { + if (!firstModel) + { + sb.append("|"); + } + firstModel = false; + appendModel(sb, model, atomSpec); + if (alphaOnly) + { + // TODO @P if RNA - add nucleotide flag to AtomSpecModel? + sb.append("@CA"); + } + // todo: is there ChimeraX syntax to exclude altlocs? + } + return sb.toString(); + } + + /** + * A helper method to append an atomSpec string for atoms in the given model + * + * @param sb + * @param model + * @param atomSpec + */ + protected void appendModel(StringBuilder sb, String model, + AtomSpecModel atomSpec) + { + sb.append("#").append(model); + + for (String chain : atomSpec.getChains(model)) + { + boolean firstPositionForChain = true; + sb.append("/").append(chain.trim()).append(":"); + List rangeList = atomSpec.getRanges(model, chain); + boolean first = true; + for (int[] range : rangeList) + { + if (!first) + { + sb.append(","); + } + first = false; + appendRange(sb, range[0], range[1], chain, firstPositionForChain, + true); + } + } + } + + @Override + public List showBackbone() + { + return Arrays.asList(SHOW_BACKBONE); + } + + @Override + public List superposeStructures(AtomSpecModel ref, + AtomSpecModel spec) + { + /* + * Form ChimeraX match command to match spec to ref + * + * match #1/A:2-94 toAtoms #2/A:1-93 + * + * @see https://www.cgl.ucsf.edu/chimerax/docs/user/commands/align.html + */ + StringBuilder cmd = new StringBuilder(); + String atomSpec = getAtomSpec(spec, true); + String refSpec = getAtomSpec(ref, true); + cmd.append("align ").append(atomSpec).append(" toAtoms ") + .append(refSpec); + + /* + * show superposed residues as ribbon, others as chain + */ + cmd.append("; ribbon "); + cmd.append(getAtomSpec(spec, false)).append("|"); + cmd.append(getAtomSpec(ref, false)).append("; view"); + + return Arrays.asList(new StructureCommand(cmd.toString())); + } + +} diff --git a/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java b/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java index 00446f2..3553d68 100644 --- a/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java +++ b/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java @@ -21,36 +21,31 @@ package jalview.ext.rbvi.chimera; import jalview.api.AlignmentViewPanel; -import jalview.api.SequenceRenderer; 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.StructureViewer.ViewerType; import jalview.httpserver.AbstractRequestHandler; import jalview.io.DataSourceType; -import jalview.schemes.ColourSchemeI; -import jalview.schemes.ResidueProperties; import jalview.structure.AtomSpec; -import jalview.structure.StructureMappingcommandSet; +import jalview.structure.StructureCommand; +import jalview.structure.StructureCommandI; 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.Hashtable; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -67,23 +62,16 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel // 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"; private static final String ALPHACARBON = "CA"; - private List chainNames = new ArrayList(); - - private Hashtable chainFile = new Hashtable(); - /* * Object through which we talk to Chimera */ - private ChimeraManager viewer; + private ChimeraManager chimeraManager; /* * Object which listens to Chimera notifications @@ -91,39 +79,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; - - /* * 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 @@ -133,8 +101,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; /* @@ -155,16 +123,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); @@ -184,6 +144,31 @@ 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 @@ -196,17 +181,25 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel DataSourceType protocol) { super(ssm, pdbentry, sequenceIs, protocol); - viewer = new ChimeraManager(new StructureManager(true)); + chimeraManager = new ChimeraManager(new StructureManager(true)); + chimeraManager.setChimeraX(ViewerType.CHIMERAX.equals(getViewerType())); + setStructureCommands(new ChimeraCommands()); + } + + @Override + protected ViewerType getViewerType() + { + return ViewerType.CHIMERA; } /** - * 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. + * 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 = viewer.getChimeraProcess(); + final Process p = chimeraManager.getChimeraProcess(); chimeraMonitor = new Thread(new Runnable() { @@ -231,15 +224,15 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel } /** - * Start a dedicated HttpServer to listen for Chimera notifications, and tell - * it to start listening + * Start a dedicated HttpServer to listen for Chimera notifications, and tell it + * to start listening */ public void startChimeraListener() { try { chimeraListener = new ChimeraListener(this); - viewer.startListening(chimeraListener.getUri()); + chimeraManager.startListening(chimeraListener.getUri()); } catch (BindException e) { System.err.println( @@ -248,43 +241,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel } /** - * Tells Chimera to display only the specified chains - * - * @param toshow - */ - public void showChains(List toshow) - { - /* - * Construct a chimera command like - * - * ~display #*;~ribbon #*;ribbon :.A,:.B - */ - StringBuilder cmd = new StringBuilder(64); - boolean first = true; - for (String chain : toshow) - { - int modelNumber = getModelNoForChain(chain); - String showChainCmd = modelNumber == -1 ? "" - : modelNumber + ":." + chain.split(":")[1]; - if (!first) - { - cmd.append(","); - } - cmd.append(showChainCmd); - first = false; - } - - /* - * 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); - } - - /** * Close down the Jalview viewer and listener, and (optionally) the associated * Chimera window. */ @@ -293,14 +249,14 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel 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) { @@ -309,249 +265,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel releaseUIResources(); } - @Override - 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
  • - *
- */ - @Override - 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); - } - - /** - * {@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); - 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++) - { - String chainCd = "." + structures[pdbfnum].chain; - int lpos = -1; - boolean run = false; - StringBuilder molsel = new StringBuilder(); - - 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)); - 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)); - molsel.append(chainCd); - } - if (molsel.length() > 1) - { - selcom[pdbfnum] = molsel.toString(); - selectioncom.append("#").append(String.valueOf(pdbfnum)) - .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++) - { - 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("match ").append(getModelSpec(pdbfnum)).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(":"); - command.append(selcom[refStructure]); - command.append("@").append( - structures[refStructure].isRna ? PHOSPHORUS : ALPHACARBON); - 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; chain @CA|P; ribbon ") - .append(selectioncom.toString()) - .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; chain @CA|P; ribbon ") - .append(selectioncom.toString()).append("; focus"); - List chimeraReplies = sendChimeraCommand(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: *