From 9c1a9d682a2664d525bfd0f38bae861292dc3921 Mon Sep 17 00:00:00 2001 From: gmungoc Date: Fri, 28 Feb 2020 16:59:25 +0000 Subject: [PATCH] JAL-3518 basic refactoring / pull up of superposeStructures; to tidy! --- src/jalview/appletgui/AppletJmolBinding.java | 7 + src/jalview/appletgui/ExtJmol.java | 7 + src/jalview/ext/jmol/JalviewJmolBinding.java | 10 +- src/jalview/ext/jmol/JmolCommands.java | 198 ++++++- src/jalview/ext/rbvi/chimera/AtomSpecModel.java | 281 --------- src/jalview/ext/rbvi/chimera/ChimeraCommands.java | 461 ++++++--------- src/jalview/ext/rbvi/chimera/ChimeraXCommands.java | 598 +++++--------------- .../ext/rbvi/chimera/JalviewChimeraBinding.java | 55 +- src/jalview/gui/ChimeraViewFrame.java | 26 +- src/jalview/gui/ChimeraXViewFrame.java | 24 +- src/jalview/gui/JalviewChimeraBindingModel.java | 30 - src/jalview/gui/JalviewChimeraXBindingModel.java | 29 +- src/jalview/gui/StructureViewer.java | 15 +- src/jalview/gui/StructureViewerBase.java | 17 +- src/jalview/structure/AtomSpecModel.java | 123 ++++ src/jalview/structure/StructureCommandsBase.java | 305 ++++++++++ src/jalview/structure/StructureCommandsI.java | 108 +++- .../structures/models/AAStructureBindingModel.java | 357 ++++++++++-- test/jalview/ext/jmol/JmolCommandsTest.java | 60 ++ .../ext/rbvi/chimera/AtomSpecModelTest.java | 39 -- .../ext/rbvi/chimera/ChimeraCommandsTest.java | 96 ++++ .../ext/rbvi/chimera/ChimeraXCommandsTest.java | 23 + test/jalview/structure/AtomSpecModelTest.java | 51 ++ .../models/AAStructureBindingModelTest.java | 36 +- 24 files changed, 1681 insertions(+), 1275 deletions(-) delete mode 100644 src/jalview/ext/rbvi/chimera/AtomSpecModel.java create mode 100644 src/jalview/structure/AtomSpecModel.java delete mode 100644 test/jalview/ext/rbvi/chimera/AtomSpecModelTest.java create mode 100644 test/jalview/structure/AtomSpecModelTest.java diff --git a/src/jalview/appletgui/AppletJmolBinding.java b/src/jalview/appletgui/AppletJmolBinding.java index dd6dea3..9a72b2e 100644 --- a/src/jalview/appletgui/AppletJmolBinding.java +++ b/src/jalview/appletgui/AppletJmolBinding.java @@ -174,4 +174,11 @@ class AppletJmolBinding extends JalviewJmolBinding { return null; } + + @Override + protected void sendAsynchronousCommand(String command, String progressMsg) + { + // TODO Auto-generated method stub + + } } diff --git a/src/jalview/appletgui/ExtJmol.java b/src/jalview/appletgui/ExtJmol.java index 28381bc..5be53a3 100644 --- a/src/jalview/appletgui/ExtJmol.java +++ b/src/jalview/appletgui/ExtJmol.java @@ -180,4 +180,11 @@ public class ExtJmol extends JalviewJmolBinding return null; } + @Override + protected void sendAsynchronousCommand(String command, String progressMsg) + { + // TODO Auto-generated method stub + + } + } diff --git a/src/jalview/ext/jmol/JalviewJmolBinding.java b/src/jalview/ext/jmol/JalviewJmolBinding.java index 3b4a958..6b0a696 100644 --- a/src/jalview/ext/jmol/JalviewJmolBinding.java +++ b/src/jalview/ext/jmol/JalviewJmolBinding.java @@ -26,9 +26,11 @@ 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.structure.AtomSpec; +import jalview.structure.StructureCommandsI.SuperposeData; import jalview.structure.StructureSelectionManager; import jalview.structures.models.AAStructureBindingModel; import jalview.util.MessageManager; @@ -172,7 +174,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel /** * {@inheritDoc} */ - @Override public String superposeStructures(AlignmentI[] _alignment, int[] _refStructure, HiddenColumns[] _hiddenCols) { @@ -248,7 +249,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel SuperposeData[] structures = new SuperposeData[files.length]; for (int f = 0; f < files.length; f++) { - structures[f] = new SuperposeData(alignment.getWidth()); + structures[f] = new SuperposeData(alignment.getWidth(), f); } /* @@ -1244,6 +1245,11 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel } } return -1; + } + @Override + protected ViewerType getViewerType() + { + return ViewerType.JMOL; } } diff --git a/src/jalview/ext/jmol/JmolCommands.java b/src/jalview/ext/jmol/JmolCommands.java index c8a54cd..7dd5c0b 100644 --- a/src/jalview/ext/jmol/JmolCommands.java +++ b/src/jalview/ext/jmol/JmolCommands.java @@ -28,6 +28,7 @@ import jalview.datamodel.AlignmentI; import jalview.datamodel.HiddenColumns; import jalview.datamodel.SequenceI; import jalview.renderer.seqfeatures.FeatureColourFinder; +import jalview.structure.AtomSpecModel; import jalview.structure.StructureCommandsBase; import jalview.structure.StructureMapping; import jalview.structure.StructureSelectionManager; @@ -37,7 +38,6 @@ import java.awt.Color; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Map.Entry; /** * Routines for generating Jmol commands for Jalview/Jmol binding @@ -52,15 +52,50 @@ public class JmolCommands extends StructureCommandsBase private static final String CMD_COLOUR_BY_CHAIN = "select *;color chain"; - private static String formatRGB(Color c) { + private static final String PIPE = "|"; + + private static final String HYPHEN = "-"; + + private static final String COLON = ":"; + + private static final String SLASH = "/"; + + /** + * {@inheritDoc} + * + * @return + */ + @Override + public int getModelStartNo() + { + return 1; + } + + @Override + protected String getColourString(Color c) + { return c == null ? null : String.format("[%d,%d,%d]", c.getRed(), c.getGreen(), c.getBlue()); } + /** + * Returns commands (one per colour key in the map) like + * + *
+   *   select 2:A/1.1|3-27:B/1.1|9-12:A/2.1;color[173,0,82]
+   * 
+ */ @Override - public String[] colourBySequence( - StructureSelectionManager ssm, String[] files, + public String[] colourBySequence(Map colourMap) + { + List colourCommands = buildColourCommands(colourMap); + + return colourCommands.toArray(new String[colourCommands.size()]); + } + + public String[] colourBySequence(StructureSelectionManager ssm, + String[] files, SequenceI[][] sequence, SequenceRenderer sr, AlignmentViewPanel viewPanel) { @@ -134,9 +169,8 @@ public class JmolCommands extends StructureCommandsBase 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)) @@ -230,20 +264,7 @@ public class JmolCommands extends StructureCommandsBase { StringBuilder cmd = new StringBuilder(128); cmd.append("select *;color white;"); - - /* - * concatenate commands like - * select VAL;color[100,215,55] - */ - for (Entry entry : colours.entrySet()) - { - Color col = entry.getValue(); - String resCode = entry.getKey(); - cmd.append("select ").append(resCode).append(";"); - cmd.append("color["); - cmd.append(formatRGB(col)); - cmd.append("];"); - } + cmd.append(super.colourByResidues(colours)); return cmd.toString(); } @@ -251,7 +272,7 @@ public class JmolCommands extends StructureCommandsBase @Override public String setBackgroundColour(Color col) { - return "background " + formatRGB(col); + return "background " + getColourString(col); } @Override @@ -285,4 +306,137 @@ public class JmolCommands extends StructureCommandsBase return 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 String superposeStructures(AtomSpecModel refAtoms, + AtomSpecModel atomSpec) + { + StringBuilder sb = new StringBuilder(64); + int refModel = refAtoms.getModels().iterator().next(); + int model2 = atomSpec.getModels().iterator().next(); + sb.append(String.format("compare {%d.1} {%d.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 sb.toString(); + } + + @Override + public String openCommandFile(String path) + { + /* + * https://chemapps.stolaf.edu/jmol/docs/#script + * not currently used in Jalview + */ + return "script " + path; + } + + @Override + public String saveSession(String filepath) + { + /* + * https://chemapps.stolaf.edu/jmol/docs/#write + * not currently used in Jalview + */ + return "write \"" + filepath + "\""; + } + + @Override + protected String 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 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 (int 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 String showBackbone() + { + return "select *; cartoons off; backbone"; + } } diff --git a/src/jalview/ext/rbvi/chimera/AtomSpecModel.java b/src/jalview/ext/rbvi/chimera/AtomSpecModel.java deleted file mode 100644 index a72844e..0000000 --- a/src/jalview/ext/rbvi/chimera/AtomSpecModel.java +++ /dev/null @@ -1,281 +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, - false); - firstPositionForModel = false; - start = range[0]; - end = range[1]; - } - } - - /* - * and append the last range - */ - if (!rangeList.isEmpty()) - { - appendRange(sb, start, end, chain, firstPositionForModel, false); - 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, boolean isChimeraX) - { - if (!firstPositionForModel) - { - sb.append(","); - } - if (end == start) - { - sb.append(start); - } - else - { - sb.append(start).append("-").append(end); - } - - if (!isChimeraX) - { - sb.append("."); - if (!" ".equals(chain)) - { - sb.append(chain); - } - } - } - - /** - * 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 - */ - public String getAtomSpecX() - { - StringBuilder sb = new StringBuilder(128); - boolean firstModel = true; - for (Integer model : atomSpec.keySet()) - { - if (!firstModel) - { - sb.append("|"); - } - firstModel = false; - sb.append("#").append(model); - - final Map> modelData = atomSpec.get(model); - - for (String chain : modelData.keySet()) - { - boolean firstPositionForChain = true; - chain = " ".equals(chain) ? chain : chain.trim(); - sb.append("/").append(chain).append(":"); - 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, firstPositionForChain, true); - start = range[0]; - end = range[1]; - firstPositionForChain = false; - } - } - - /* - * and append the last range - */ - if (!rangeList.isEmpty()) - { - appendRange(sb, start, end, chain, firstPositionForChain, true); - } - firstPositionForChain = false; - } - } - return sb.toString(); - } -} diff --git a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java index 61adefe..14699ef 100644 --- a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java +++ b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java @@ -23,27 +23,26 @@ 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.StructureCommandsBase; import jalview.structure.StructureMapping; import jalview.structure.StructureSelectionManager; import jalview.util.ColorUtils; -import jalview.util.Comparison; +import jalview.util.IntRangeComparator; import java.awt.Color; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; /** * Routines for generating Chimera commands for Jalview/Chimera binding @@ -55,279 +54,39 @@ public class ChimeraCommands extends StructureCommandsBase { public static final String NAMESPACE_PREFIX = "jv_"; - protected static final String CMD_SEPARATOR = ";"; - private static final String CMD_COLOUR_BY_CHARGE = "color white;color red ::ASP;color red ::GLU;color blue ::LYS;color blue ::ARG;color yellow ::CYS"; private static final String CMD_COLOUR_BY_CHAIN = "rainbow chain"; + // Chimera clause to exclude alternate locations in atom selection + private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9"; + @Override - public String[] colourBySequence( - StructureSelectionManager ssm, String[] files, - SequenceI[][] sequence, SequenceRenderer sr, - AlignmentViewPanel viewPanel) + public String[] colourBySequence(Map colourMap) { - Map colourMap = buildColoursMap(ssm, files, - sequence, sr, viewPanel); - List colourCommands = buildColourCommands(colourMap); return colourCommands.toArray(new String[colourCommands.size()]); } - /** - * 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 List buildColourCommands( - Map colourMap) - { - /* - * 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("; "); - } - firstColour = false; - final AtomSpecModel colourData = colourMap.get(colour); - sb.append(getColourCommand(colourData, colourCode)); - } - commands.add(sb.toString()); - return commands; - } - - protected String getColourCommand(AtomSpecModel colourData, - String colourCode) - { - return "color " + colourCode + " " + colourData.getAtomSpec(); - } - - /** - * Traverses a map of { modelNumber, {chain, {list of from-to ranges} } } and - * builds a Chimera format atom spec - * - * @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)
-   * 
- * - * @param ssm - * @param files - * @param sequence - * @param sr - * @param viewPanel - * @return - */ - protected static Map buildColoursMap( - StructureSelectionManager ssm, String[] files, - SequenceI[][] sequence, SequenceRenderer sr, - AlignmentViewPanel viewPanel) + @Override + public String getColourCommand(String atomSpec, Color colour) { - 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++) - { - final int modelNumber = pdbfnum + getModelStartNo(); - 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, modelNumber, - startPos, lastPos, lastChain); - } - startPos = pos; - } - lastColour = colour; - lastPos = pos; - lastChain = chain; - } - // final colour range - if (lastColour != null) - { - addAtomSpecRange(colourMap, lastColour, modelNumber, startPos, - lastPos, lastChain); - } - // break; - } - } - } - } - return colourMap; + // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/color.html + String colourCode = getColourString(colour); + return "color " + colourCode + " " + atomSpec; } /** - * Returns the lowest model number used by the structure viewer + * Returns a colour formatted suitable for use in viewer command syntax * + * @param colour * @return */ - protected static int getModelStartNo() + @Override + protected String getColourString(Color colour) { - return 0; - } - - /** - * 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 - */ - protected static final void addAtomSpecRange( - Map map, - Object value, int model, int startPos, int endPos, String chain) - { - /* - * 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); } /** @@ -366,7 +125,7 @@ public class ChimeraCommands extends StructureCommandsBase * @param viewPanel * @return */ - protected static Map> buildFeaturesMap( + protected Map> buildFeaturesMap( StructureSelectionManager ssm, String[] files, SequenceI[][] seqs, AlignmentViewPanel viewPanel) { @@ -649,7 +408,7 @@ public class ChimeraCommands extends StructureCommandsBase StringBuilder sb = new StringBuilder(128); sb.append("setattr res ").append(attributeName).append(" '") .append(attributeValue).append("' "); - sb.append(atomSpecModel.getAtomSpec()); + sb.append(getAtomSpec(atomSpecModel, false)); return sb.toString(); } @@ -699,33 +458,22 @@ public class ChimeraCommands extends StructureCommandsBase } @Override - public String colourByResidues(Map colours) + public String getResidueSpec(String residue) { - StringBuilder cmd = new StringBuilder(12 * colours.size()); - - /* - * concatenate commands like - * color #4949b6 ::VAL - */ - for (Entry entry : colours.entrySet()) - { - String colorSpec = ColorUtils.toTkCode(entry.getValue()); - String resCode = entry.getKey(); - cmd.append("color ").append(colorSpec).append(" ::").append(resCode) - .append(CMD_SEPARATOR); - } - return cmd.toString(); + return "::" + residue; } @Override public String setBackgroundColour(Color col) { + // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/set.html#bgcolor return "set bgColor " + ColorUtils.toTkCode(col); } @Override public String focusView() { + // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/focus.html return "focus"; } @@ -764,4 +512,165 @@ public class ChimeraCommands extends StructureCommandsBase return command; } + @Override + public String superposeStructures(AtomSpecModel spec, AtomSpecModel ref) + { + /* + * Form Chimera match command to match spec to ref + * + * 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 atomSpec = getAtomSpec(spec, true); + String refSpec = getAtomSpec(ref, true); + cmd.append("match ").append(atomSpec).append(" ").append(refSpec); + + /* + * show superposed residues as ribbon, others as chain + */ + // fixme this should precede the loop over all alignments/structures + cmd.append(";~display all; chain @CA|P"); + cmd.append("; ribbon "); + cmd.append(atomSpec).append("|").append(refSpec).append("; focus"); + + return cmd.toString(); + } + + @Override + public String openCommandFile(String path) + { + // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/filetypes.html + return "open cmd:" + path; + } + + @Override + public String saveSession(String filepath) + { + // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/save.html + return "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 (Integer 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, Integer 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);
+
+      /*
+       * 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, false);
+          firstPositionForModel = false;
+          start = range[0];
+          end = range[1];
+        }
+      }
+
+      /*
+       * and append the last range
+       */
+      if (!rangeList.isEmpty())
+      {
+        appendRange(sb, start, end, chain, firstPositionForModel, false);
+        firstPositionForModel = false;
+      }
+    }
+    if (alphaOnly)
+    {
+      /*
+       * restrict to alpha carbon, no alternative locations
+       * (needed to ensuring matching atom counts for superposition)
+       */
+      sb.append("@CA|P").append(NO_ALTLOCS);
+    }
+  }
+
+  @Override
+  public String showBackbone()
+  {
+    return "~display all;chain @CA|P";
+  }
+
 }
diff --git a/src/jalview/ext/rbvi/chimera/ChimeraXCommands.java b/src/jalview/ext/rbvi/chimera/ChimeraXCommands.java
index 7693802..580fa4b 100644
--- a/src/jalview/ext/rbvi/chimera/ChimeraXCommands.java
+++ b/src/jalview/ext/rbvi/chimera/ChimeraXCommands.java
@@ -20,29 +20,14 @@
  */
 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.StructureMapping;
-import jalview.structure.StructureSelectionManager;
+import jalview.structure.AtomSpecModel;
 import jalview.util.ColorUtils;
-import jalview.util.Comparison;
+import jalview.util.IntRangeComparator;
 
 import java.awt.Color;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
+import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
 
 /**
  * Routines for generating ChimeraX commands for Jalview/ChimeraX binding
@@ -51,500 +36,215 @@ public class ChimeraXCommands extends ChimeraCommands
 {
   private static final String CMD_COLOUR_BY_CHARGE = "color white;color :ASP,GLU red;color :LYS,ARG blue;color :CYS yellow";
 
-  /**
-   * Traverses a map of { modelNumber, {chain, {list of from-to ranges} } } and
-   * builds a ChimeraX format atom spec
-   * 
-   * @param modelAndChainRanges
-   */
-  protected static String getAtomSpec(
-          Map>> modelAndChainRanges)
+  @Override
+  public String colourByCharge()
   {
-    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();
+    return CMD_COLOUR_BY_CHARGE;
   }
 
-  /**
-   * 
-   * 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) + @Override + public String getResidueSpec(String residue) { - 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)); + return ":" + residue; + } - if (pos < 1 || pos == lastPos) - { - continue; - } + @Override + public String setBackgroundColour(Color col) + { + // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/set.html + return "set bgColor " + ColorUtils.toTkCode(col); + } - Color colour = sr.getResidueColour(seq, r, finder); + @Override + protected String getColourCommand(AtomSpecModel colourData, + Color colour) + { + // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/color.html + String colourCode = getColourString(colour); - /* - * darker colour for hidden regions - */ - if (!cs.isVisible(r)) - { - colour = Color.GRAY; - } + return "color " + getAtomSpec(colourData, false) + " " + colourCode; + } - final String chain = mapping[m].getChain(); + @Override + public String focusView() + { + // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/view.html + return "view"; + } - /* - * 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; + /** + * {@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 + * *
-   * Helper method to build a map of 
-   *   { featureType, { feature value, AtomSpecModel } }
+   * setattr #0/A:3-9,14-20,39-43 res jv_strand 'strand' create true
    * 
* - * @param ssm - * @param files - * @param seqs - * @param viewPanel + * @param attributeName + * @param attributeValue + * @param atomSpecModel * @return */ - protected static Map> buildFeaturesMap( - StructureSelectionManager ssm, String[] files, SequenceI[][] seqs, - AlignmentViewPanel viewPanel) + @Override + protected String getSetAttributeCommand(String attributeName, + String attributeValue, AtomSpecModel atomSpecModel) { - Map> theMap = new LinkedHashMap<>(); - - FeatureRenderer fr = viewPanel.getFeatureRenderer(); - if (fr == null) - { - return theMap; - } - - AlignViewportI viewport = viewPanel.getAlignViewport(); - List visibleFeatures = fr.getDisplayedFeatureTypes(); - - /* - * if alignment is showing features from complement, we also transfer - * these features to the corresponding mapped structure residues - */ - boolean showLinkedFeatures = viewport.isShowComplementFeatures(); - List complementFeatures = new ArrayList<>(); - FeatureRenderer complementRenderer = null; - if (showLinkedFeatures) - { - AlignViewportI comp = fr.getViewport().getCodingComplement(); - if (comp != null) - { - complementRenderer = Desktop.getAlignFrameFor(comp) - .getFeatureRenderer(); - complementFeatures = complementRenderer.getDisplayedFeatureTypes(); - } - } - if (visibleFeatures.isEmpty() && complementFeatures.isEmpty()) - { - return theMap; - } - - AlignmentI alignment = viewPanel.getAlignment(); - for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) - { - StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]); + 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 sb.toString(); + } - if (mapping == null || mapping.length < 1) - { - continue; - } + @Override + public String openCommandFile(String path) + { + // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/open.html + return "open " + path; + } - for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++) - { - for (int m = 0; m < mapping.length; m++) - { - final SequenceI seq = seqs[pdbfnum][seqNo]; - int sp = alignment.findIndex(seq); - StructureMapping structureMapping = mapping[m]; - if (structureMapping.getSequence() == seq && sp > -1) - { - /* - * found a sequence with a mapping to a structure; - * now scan its features - */ - if (!visibleFeatures.isEmpty()) - { - scanSequenceFeatures(visibleFeatures, structureMapping, seq, - theMap, pdbfnum); - } - if (showLinkedFeatures) - { - scanComplementFeatures(complementRenderer, structureMapping, - seq, theMap, pdbfnum); - } - } - } - } - } - return theMap; + @Override + public String saveSession(String filepath) + { + // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/save.html + return "save session " + filepath; } /** - * Scans visible features in mapped positions of the CDS/peptide complement, and - * adds any found to the map of attribute values/structure positions + * Returns the range(s) formatted as a ChimeraX atomspec, for example + *

+ * #1/A:2-20,30-40/B:10-20|#2/A:12-30 * - * @param complementRenderer - * @param structureMapping - * @param seq - * @param theMap - * @param modelNumber + * @return */ - protected static void scanComplementFeatures( - FeatureRenderer complementRenderer, - StructureMapping structureMapping, SequenceI seq, - Map> theMap, int modelNumber) + @Override + public String getAtomSpec(AtomSpecModel atomSpec, boolean alphaOnly) { - /* - * for each sequence residue mapped to a structure position... - */ - for (int seqPos : structureMapping.getMapping().keySet()) + StringBuilder sb = new StringBuilder(128); + boolean firstModel = true; + for (Integer model : atomSpec.getModels()) { - /* - * find visible complementary features at mapped position(s) - */ - MappedFeatures mf = complementRenderer - .findComplementFeaturesAtResidue(seq, seqPos); - if (mf != null) + if (!firstModel) { - for (SequenceFeature sf : mf.features) - { - String type = sf.getType(); - - /* - * Don't copy features which originated from Chimera - */ - if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP - .equals(sf.getFeatureGroup())) - { - continue; - } - - /* - * record feature 'value' (score/description/type) as at the - * corresponding structure position - */ - List mappedRanges = structureMapping - .getPDBResNumRanges(seqPos, seqPos); - - if (!mappedRanges.isEmpty()) - { - String value = sf.getDescription(); - if (value == null || value.length() == 0) - { - value = type; - } - float score = sf.getScore(); - if (score != 0f && !Float.isNaN(score)) - { - value = Float.toString(score); - } - Map featureValues = theMap.get(type); - if (featureValues == null) - { - featureValues = new HashMap<>(); - theMap.put(type, featureValues); - } - for (int[] range : mappedRanges) - { - addAtomSpecRange(featureValues, value, modelNumber, range[0], - range[1], structureMapping.getChain()); - } - } - } + sb.append("|"); + } + firstModel = false; + appendModel(sb, model, atomSpec); + if (alphaOnly) + { + sb.append("@CA|P"); } + // todo: is there ChimeraX syntax to exclude altlocs? } + return sb.toString(); } /** - * 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. + * A helper method to append an atomSpec string for atoms in the given model * - * @param visibleFeatures - * @param mapping - * @param seq - * @param theMap - * @param modelNumber + * @param sb + * @param model + * @param atomSpec */ - protected static void scanSequenceFeatures(List visibleFeatures, - StructureMapping mapping, SequenceI seq, - Map> theMap, int modelNumber) + protected void appendModel(StringBuilder sb, Integer model, + AtomSpecModel atomSpec) { - List sfs = seq.getFeatures().getPositionalFeatures( - visibleFeatures.toArray(new String[visibleFeatures.size()])); - for (SequenceFeature sf : sfs) + sb.append("#").append(model); + + for (String chain : atomSpec.getChains(model)) { - String type = sf.getType(); + boolean firstPositionForChain = true; + chain = " ".equals(chain) ? chain : chain.trim(); + sb.append("/").append(chain).append(":"); + List rangeList = atomSpec.getRanges(model, chain); /* - * Don't copy features which originated from Chimera + * sort ranges into ascending start position order */ - if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP - .equals(sf.getFeatureGroup())) - { - continue; - } + Collections.sort(rangeList, IntRangeComparator.ASCENDING); - List mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(), - sf.getEnd()); + int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0]; + int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1]; - if (!mappedRanges.isEmpty()) + Iterator iterator = rangeList.iterator(); + while (iterator.hasNext()) { - String value = sf.getDescription(); - if (value == null || value.length() == 0) + int[] range = iterator.next(); + if (range[0] <= end + 1) { - value = type; - } - float score = sf.getScore(); - if (score != 0f && !Float.isNaN(score)) - { - value = Float.toString(score); - } - Map featureValues = theMap.get(type); - if (featureValues == null) - { - featureValues = new HashMap<>(); - theMap.put(type, featureValues); + /* + * 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]); } - for (int[] range : mappedRanges) + else { - addAtomSpecRange(featureValues, value, modelNumber, range[0], - range[1], mapping.getChain()); + /* + * we have a break so append the last range + */ + appendRange(sb, start, end, chain, firstPositionForChain, true); + start = range[0]; + end = range[1]; + firstPositionForChain = false; } } - } - } - /** - * 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
-   *         
- */ - protected static String makeAttributeName(String featureType) - { - StringBuilder sb = new StringBuilder(); - if (featureType != null) - { - for (char c : featureType.toCharArray()) + /* + * and append the last range + */ + if (!rangeList.isEmpty()) { - sb.append(Character.isLetterOrDigit(c) ? c : '_'); + appendRange(sb, start, end, chain, firstPositionForChain, true); } + firstPositionForChain = false; } - String attName = NAMESPACE_PREFIX + sb.toString(); - - /* - * Chimera treats an attribute name ending in 'color' as colour-valued; - * Jalview doesn't, so prevent this by appending an underscore - */ - if (attName.toUpperCase().endsWith("COLOR")) - { - attName += "_"; - } - - return attName; } @Override - public String colourByCharge() + public String showBackbone() { - return CMD_COLOUR_BY_CHARGE; + return "~display all;show @CA|P pbonds"; } @Override - public String colourByResidues(Map colours) + public String superposeStructures(AtomSpecModel spec, AtomSpecModel ref) { - StringBuilder cmd = new StringBuilder(12 * colours.size()); - /* - * concatenate commands like - * color :VAL #4949b6 + * 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/chimera/docs/UsersGuide/midas/match.html */ - for (Entry entry : colours.entrySet()) - { - String colorSpec = ColorUtils.toTkCode(entry.getValue()); - String resCode = entry.getKey(); - cmd.append("color :").append(resCode).append(" ").append(colorSpec) - .append(CMD_SEPARATOR); - } - return cmd.toString(); - } - - @Override - public String setBackgroundColour(Color col) - { - return "set bgColor " + ColorUtils.toTkCode(col); - } - - @Override - protected String getColourCommand(AtomSpecModel colourData, - String colourCode) - { - return "color " + colourData.getAtomSpecX() + " " + colourCode; - } - - @Override - public String focusView() - { - return "view"; - } + StringBuilder cmd = new StringBuilder(); + String atomSpec = getAtomSpec(spec, true); + String refSpec = getAtomSpec(ref, true); + cmd.append("align ").append(atomSpec).append(" toAtoms ") + .append(refSpec); - /** - * {@inheritDoc} - * - * @return - */ - protected static int getModelStartNo() - { - return 1; - } + /* + * show superposed residues as ribbon, others as chain + */ + cmd.append("; ribbon "); + cmd.append(getAtomSpec(spec, false)).append("|"); + cmd.append(getAtomSpec(ref, false)).append("; view"); - /** - * 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 String getSetAttributeCommand(String attributeName, - String attributeValue, AtomSpecModel atomSpecModel) - { - StringBuilder sb = new StringBuilder(128); - sb.append("setattr ").append(atomSpecModel.getAtomSpecX()); - sb.append(" res ").append(attributeName).append(" '") - .append(attributeValue).append("'"); - sb.append(" create true"); - return sb.toString(); + return cmd.toString(); } } diff --git a/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java b/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java index be1de5a..731ffea 100644 --- a/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java +++ b/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java @@ -30,11 +30,11 @@ import jalview.datamodel.SearchResultMatchI; import jalview.datamodel.SearchResultsI; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; -import jalview.gui.Preferences; import jalview.gui.StructureViewer.ViewerType; import jalview.httpserver.AbstractRequestHandler; import jalview.io.DataSourceType; import jalview.structure.AtomSpec; +import jalview.structure.StructureCommandsI.SuperposeData; import jalview.structure.StructureSelectionManager; import jalview.structures.models.AAStructureBindingModel; import jalview.util.MessageManager; @@ -184,11 +184,16 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel { super(ssm, pdbentry, sequenceIs, protocol); chimeraManager = new ChimeraManager(new StructureManager(true)); - String viewerType = Cache.getProperty(Preferences.STRUCTURE_DISPLAY); - chimeraManager.setChimeraX(ViewerType.CHIMERAX.name().equals(viewerType)); + 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 @@ -265,7 +270,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel /** * {@inheritDoc} */ - @Override public String superposeStructures(AlignmentI[] _alignment, int[] _refStructure, HiddenColumns[] _hiddenCols) { @@ -309,7 +313,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel SuperposeData[] structures = new SuperposeData[files.length]; for (int f = 0; f < files.length; f++) { - structures[f] = new SuperposeData(alignment.getWidth()); + structures[f] = new SuperposeData(alignment.getWidth(), f); } /* @@ -519,8 +523,8 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel } else { - allComs.append("chain @CA|P; ribbon ; focus"); - allComs.append(selectioncom.toString()); + allComs.append("chain @CA|P; ribbon "); + allComs.append(selectioncom.toString()).append("; focus"); } // allComs.append("; ~display all; chain @CA|P; ribbon ") // .append(selectioncom.toString()).append("; focus"); @@ -647,16 +651,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel } /** - * Send a Chimera command asynchronously in a new thread. If the progress - * message is not null, display this message while the command is executing. - * - * @param command - * @param progressMsg - */ - protected abstract void sendAsynchronousCommand(String command, - String progressMsg); - - /** * @param command */ protected void executeWhenReady(String command) @@ -862,7 +856,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel * Chimera: https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/save.html * ChimeraX: https://www.cgl.ucsf.edu/chimerax/docs/user/commands/save.html */ - String command = getSaveSessionCommand(filepath); + String command = getCommandGenerator().saveSession(filepath); List reply = chimeraManager.sendChimeraCommand(command, true); if (reply.contains("Session written")) { @@ -878,17 +872,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel } /** - * Returns the command to save the viewer session to the given file path - * - * @param filepath - * @return - */ - protected String getSaveSessionCommand(String filepath) - { - return "save " + filepath; - } - - /** * Ask Chimera to open a session file. Returns true if successful, else false. * The filename must have a .py (Chimera) or .cxs (ChimeraX) extension for * this command to work. @@ -988,7 +971,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel out.flush(); out.close(); String path = tmp.getAbsolutePath(); - String command = getOpenCommandFileCommand(path); + String command = getCommandGenerator().openCommandFile(path); sendAsynchronousCommand(command, null); } catch (IOException e) { @@ -998,18 +981,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel } /** - * Returns the command for the structure viewer to open a file of commands at - * the given file path - * - * @param path - * @return - */ - protected String getOpenCommandFileCommand(String path) - { - return "open cmd:" + path; - } - - /** * Returns the file extension required for a file of commands to be read by * the structure viewer * @return diff --git a/src/jalview/gui/ChimeraViewFrame.java b/src/jalview/gui/ChimeraViewFrame.java index ab5ee7a..1a5e901 100644 --- a/src/jalview/gui/ChimeraViewFrame.java +++ b/src/jalview/gui/ChimeraViewFrame.java @@ -208,8 +208,7 @@ public class ChimeraViewFrame extends StructureViewerBase SequenceI[][] seqs) { createProgressBar(); - jmb = new JalviewChimeraBindingModel(this, - ap.getStructureSelectionManager(), pdbentrys, seqs, null); + jmb = newBindingModel(ap, pdbentrys, seqs); addAlignmentPanel(ap); useAlignmentPanelForColourbyseq(ap); @@ -237,6 +236,13 @@ public class ChimeraViewFrame extends StructureViewerBase } + protected JalviewChimeraBindingModel newBindingModel(AlignmentPanel ap, + PDBEntry[] pdbentrys, SequenceI[][] seqs) + { + return new JalviewChimeraBindingModel(this, + ap.getStructureSelectionManager(), pdbentrys, seqs, null); + } + /** * Create a new viewer from saved session state data including Chimera session * file @@ -753,20 +759,4 @@ public class ChimeraViewFrame extends StructureViewerBase { return "Chimera"; } - - /** - * Sends commands to align structures according to associated alignment(s). - * - * @return - */ - @Override - protected String alignStructsWithAllAlignPanels() - { - String reply = super.alignStructsWithAllAlignPanels(); - if (reply != null) - { - statusBar.setText("Superposition failed: " + reply); - } - return reply; - } } diff --git a/src/jalview/gui/ChimeraXViewFrame.java b/src/jalview/gui/ChimeraXViewFrame.java index de8820d..b33ccd6 100644 --- a/src/jalview/gui/ChimeraXViewFrame.java +++ b/src/jalview/gui/ChimeraXViewFrame.java @@ -1,5 +1,7 @@ package jalview.gui; +import jalview.datamodel.PDBEntry; +import jalview.datamodel.SequenceI; import jalview.gui.StructureViewer.ViewerType; /** @@ -13,10 +15,22 @@ import jalview.gui.StructureViewer.ViewerType; public class ChimeraXViewFrame extends ChimeraViewFrame { + public ChimeraXViewFrame(PDBEntry pdb, SequenceI[] seqsForPdb, + String[] chains, AlignmentPanel ap) + { + super(pdb, seqsForPdb, chains, ap); + } + + public ChimeraXViewFrame(PDBEntry[] pdbsForFile, boolean superposeAdded, + SequenceI[][] theSeqs, AlignmentPanel ap) + { + super(pdbsForFile, superposeAdded, theSeqs, ap); + } + @Override public ViewerType getViewerType() { - return null;// ViewerType.CHIMERAX; + return ViewerType.CHIMERAX; } @Override @@ -25,4 +39,12 @@ public class ChimeraXViewFrame extends ChimeraViewFrame return "ChimeraX"; } + @Override + protected JalviewChimeraBindingModel newBindingModel(AlignmentPanel ap, + PDBEntry[] pdbentrys, SequenceI[][] seqs) + { + return new JalviewChimeraXBindingModel(this, + ap.getStructureSelectionManager(), pdbentrys, seqs, null); + } + } diff --git a/src/jalview/gui/JalviewChimeraBindingModel.java b/src/jalview/gui/JalviewChimeraBindingModel.java index f6cfb59..49655a4 100644 --- a/src/jalview/gui/JalviewChimeraBindingModel.java +++ b/src/jalview/gui/JalviewChimeraBindingModel.java @@ -62,34 +62,4 @@ public class JalviewChimeraBindingModel extends JalviewChimeraBinding } }); } - - /** - * Send an asynchronous command to Chimera, in a new thread, optionally with - * an 'in progress' message in a progress bar somewhere - */ - @Override - protected void sendAsynchronousCommand(final String command, - final String progressMsg) - { - final JalviewStructureDisplayI theViewer = getViewer(); - final long handle = progressMsg == null ? 0 - : theViewer.startProgressBar(progressMsg); - SwingUtilities.invokeLater(new Runnable() - { - @Override - public void run() - { - try - { - executeCommand(command, false); - } finally - { - if (progressMsg != null) - { - theViewer.stopProgressBar(null, handle); - } - } - } - }); - } } diff --git a/src/jalview/gui/JalviewChimeraXBindingModel.java b/src/jalview/gui/JalviewChimeraXBindingModel.java index 0779bef..3a6c89c 100644 --- a/src/jalview/gui/JalviewChimeraXBindingModel.java +++ b/src/jalview/gui/JalviewChimeraXBindingModel.java @@ -2,6 +2,8 @@ package jalview.gui; import jalview.datamodel.PDBEntry; import jalview.datamodel.SequenceI; +import jalview.ext.rbvi.chimera.ChimeraXCommands; +import jalview.gui.StructureViewer.ViewerType; import jalview.io.DataSourceType; import jalview.structure.StructureSelectionManager; @@ -19,6 +21,7 @@ public class JalviewChimeraXBindingModel extends JalviewChimeraBindingModel SequenceI[][] sequenceIs, DataSourceType protocol) { super(chimeraViewFrame, ssm, pdbentry, sequenceIs, protocol); + setStructureCommands(new ChimeraXCommands()); } @Override @@ -54,26 +57,6 @@ public class JalviewChimeraXBindingModel extends JalviewChimeraBindingModel } /** - * {@inheritDoc} - * - * @return - */ - @Override - protected String getOpenCommandFileCommand(String path) - { - return "open " + path; - } - - /** - * {@inheritDoc} - */ - @Override - protected String getSaveSessionCommand(String filepath) - { - return "save session " + filepath; - } - - /** * Returns the file extension to use for a saved viewer session file * * @return @@ -90,4 +73,10 @@ public class JalviewChimeraXBindingModel extends JalviewChimeraBindingModel return "http://www.rbvi.ucsf.edu/chimerax/docs/user/index.html"; } + @Override + protected ViewerType getViewerType() + { + return ViewerType.CHIMERAX; + } + } diff --git a/src/jalview/gui/StructureViewer.java b/src/jalview/gui/StructureViewer.java index c8012a6..79d3836 100644 --- a/src/jalview/gui/StructureViewer.java +++ b/src/jalview/gui/StructureViewer.java @@ -160,12 +160,16 @@ public class StructureViewer { sview = new AppJmol(ap, superposeAdded, pdbsForFile, theSeqs); } - else if (viewerType.equals(ViewerType.CHIMERA) - || viewerType.equals(ViewerType.CHIMERAX)) + else if (viewerType.equals(ViewerType.CHIMERA)) { sview = new ChimeraViewFrame(pdbsForFile, superposeAdded, theSeqs, ap); } + else if (viewerType.equals(ViewerType.CHIMERAX)) + { + sview = new ChimeraXViewFrame(pdbsForFile, superposeAdded, theSeqs, + ap); + } else { Cache.log.error(UNKNOWN_VIEWER_TYPE + getViewerType().toString()); @@ -302,11 +306,14 @@ public class StructureViewer { sview = new AppJmol(pdb, seqsForPdb, null, ap); } - else if (viewerType.equals(ViewerType.CHIMERA) - || viewerType.equals(ViewerType.CHIMERAX)) + else if (viewerType.equals(ViewerType.CHIMERA)) { sview = new ChimeraViewFrame(pdb, seqsForPdb, null, ap); } + else if (viewerType.equals(ViewerType.CHIMERAX)) + { + sview = new ChimeraXViewFrame(pdb, seqsForPdb, null, ap); + } else { Cache.log.error(UNKNOWN_VIEWER_TYPE + getViewerType().toString()); diff --git a/src/jalview/gui/StructureViewerBase.java b/src/jalview/gui/StructureViewerBase.java index aa39ee7..6dd7d50 100644 --- a/src/jalview/gui/StructureViewerBase.java +++ b/src/jalview/gui/StructureViewerBase.java @@ -22,9 +22,7 @@ package jalview.gui; import jalview.api.AlignmentViewPanel; import jalview.bin.Cache; -import jalview.datamodel.Alignment; import jalview.datamodel.AlignmentI; -import jalview.datamodel.HiddenColumns; import jalview.datamodel.PDBEntry; import jalview.datamodel.SequenceI; import jalview.gui.StructureViewer.ViewerType; @@ -772,19 +770,8 @@ public abstract class StructureViewerBase extends GStructureViewer String reply = null; try { - AlignmentI[] als = new Alignment[_alignwith.size()]; - HiddenColumns[] alc = new HiddenColumns[_alignwith.size()]; - int[] alm = new int[_alignwith.size()]; - int a = 0; - - for (AlignmentViewPanel alignPanel : _alignwith) - { - als[a] = alignPanel.getAlignment(); - alm[a] = -1; - alc[a++] = alignPanel.getAlignment().getHiddenColumns(); - } - reply = getBinding().superposeStructures(als, alm, alc); - if (reply != null) + reply = getBinding().superposeStructures(_alignwith); + if (reply != null && !reply.isEmpty()) { String text = MessageManager .formatMessage("error.superposition_failed", reply); diff --git a/src/jalview/structure/AtomSpecModel.java b/src/jalview/structure/AtomSpecModel.java new file mode 100644 index 0000000..1b7d284 --- /dev/null +++ b/src/jalview/structure/AtomSpecModel.java @@ -0,0 +1,123 @@ +/* + * 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.structure; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * A class to model a set of models, chains and atom range positions + * + */ +public class AtomSpecModel +{ + /* + * { modelNo, {chainCode, List ranges} } + */ + 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 + */ + BitSet chainData = modelData.get(chain); + if (chainData == null) + { + chainData = new BitSet(); + modelData.put(chain, chainData); + } + + /* + * Add the start/end positions + */ + chainData.set(startPos, endPos + 1); + } + + public Iterable getModels() + { + return atomSpec.keySet(); + } + + public Iterable getChains(Integer model) + { + return atomSpec.containsKey(model) ? atomSpec.get(model).keySet() + : null; + } + + /** + * Returns a (possibly empty) ordered list of contiguous atom ranges for the + * given model and chain. + * + * @param model + * @param chain + * @return + */ + public List getRanges(Integer model, String chain) + { + List ranges = new ArrayList<>(); + if (atomSpec.containsKey(model)) + { + BitSet bs = atomSpec.get(model).get(chain); + int start = 0; + if (bs != null) + { + start = bs.nextSetBit(start); + int end = 0; + while (start != -1) + { + end = bs.nextClearBit(start); + ranges.add(new int[] { start, end - 1 }); + start = bs.nextSetBit(end); + } + } + } + return ranges; + } +} diff --git a/src/jalview/structure/StructureCommandsBase.java b/src/jalview/structure/StructureCommandsBase.java index 119a27a..921c9cd 100644 --- a/src/jalview/structure/StructureCommandsBase.java +++ b/src/jalview/structure/StructureCommandsBase.java @@ -1,7 +1,21 @@ package jalview.structure; +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.SequenceI; +import jalview.renderer.seqfeatures.FeatureColourFinder; +import jalview.util.Comparison; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; /** * A base class holding methods useful to all classes that implement commands @@ -12,6 +26,17 @@ import jalview.datamodel.SequenceI; */ public abstract class StructureCommandsBase implements StructureCommandsI { + private static final String CMD_SEPARATOR = ";"; + + /** + * Returns something that separates concatenated commands + * + * @return + */ + protected static String getCommandSeparator() + { + return CMD_SEPARATOR; + } @Override public String[] setAttributesForFeatures(StructureSelectionManager ssm, @@ -20,4 +45,284 @@ public abstract class StructureCommandsBase implements StructureCommandsI // default does nothing, override where this is implemented return null; } + + /** + * Returns the lowest model number used by the structure viewer + * + * @return + */ + @Override + public int getModelStartNo() + { + return 0; + } + + /** + *
+   * Build a data structure which records contiguous subsequences for each colour. 
+   * From this we can easily generate the viewer 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)
+   * 
+ * + * @param ssm + * @param files + * @param sequence + * @param sr + * @param viewPanel + * @return + */ + protected 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++) + { + final int modelNumber = pdbfnum + getModelStartNo(); + 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, modelNumber, + startPos, lastPos, lastChain); + } + startPos = pos; + } + lastColour = colour; + lastPos = pos; + lastChain = chain; + } + // final colour range + if (lastColour != null) + { + addAtomSpecRange(colourMap, lastColour, modelNumber, 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 + */ + public static final void addAtomSpecRange(Map map, + Object value, + int model, int startPos, int endPos, String chain) + { + /* + * 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); + } + + /** + * Returns a colour formatted suitable for use in viewer command syntax + * + * @param colour + * @return + */ + protected abstract String getColourString(Color colour); + + /** + * 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 specific to the structure viewer. + * + * @param colourMap + * @return + */ + public List buildColourCommands( + Map colourMap) + { + /* + * 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; + if (!firstColour) + { + sb.append(getCommandSeparator()).append(" "); + } + firstColour = false; + final AtomSpecModel colourData = colourMap.get(colour); + sb.append(getColourCommand(colourData, colour)); + } + commands.add(sb.toString()); + return commands; + } + + /** + * Returns a command to colour the atoms represented by {@code atomSpecModel} + * with the colour specified by {@code colourCode}. + * + * @param atomSpecModel + * @param colour + * @return + */ + protected String getColourCommand(AtomSpecModel atomSpecModel, Color colour) + { + String atomSpec = getAtomSpec(atomSpecModel, false); + return getColourCommand(atomSpec, colour); + } + + /** + * Returns a command to colour the atoms described (in viewer command syntax) + * by {@code atomSpec} with the colour specified by {@code colourCode} + * + * @param atomSpec + * @param colour + * @return + */ + protected abstract String getColourCommand(String atomSpec, Color colour); + + @Override + public String colourByResidues(Map colours) + { + StringBuilder cmd = new StringBuilder(12 * colours.size()); + + for (Entry entry : colours.entrySet()) + { + String residue = entry.getKey(); + String atomSpec = getResidueSpec(residue); + cmd.append(getColourCommand(atomSpec, entry.getValue())); + cmd.append(getCommandSeparator()); + } + return cmd.toString(); + } + + /** + * Helper method to append one start-end range to an atomspec string + * + * @param sb + * @param start + * @param end + * @param chain + * @param firstPositionForModel + */ + protected void appendRange(StringBuilder sb, int start, int end, + String chain, boolean firstPositionForModel, boolean isChimeraX) + { + if (!firstPositionForModel) + { + sb.append(","); + } + if (end == start) + { + sb.append(start); + } + else + { + sb.append(start).append("-").append(end); + } + + if (!isChimeraX) + { + sb.append("."); + if (!" ".equals(chain)) + { + sb.append(chain); + } + } + } + + /** + * Returns the atom specifier meaning all occurrences of the given residue + * + * @param residue + * @return + */ + protected abstract String getResidueSpec(String residue); } diff --git a/src/jalview/structure/StructureCommandsI.java b/src/jalview/structure/StructureCommandsI.java index eda5aa9..359eac6 100644 --- a/src/jalview/structure/StructureCommandsI.java +++ b/src/jalview/structure/StructureCommandsI.java @@ -1,7 +1,6 @@ package jalview.structure; import jalview.api.AlignmentViewPanel; -import jalview.api.SequenceRenderer; import jalview.datamodel.SequenceI; import java.awt.Color; @@ -17,6 +16,41 @@ import java.util.Map; */ public interface StructureCommandsI { + /** + * Data bean class to simplify parameterisation in superposeStructures + */ + public class SuperposeData + { + public String filename; + + public String pdbId; + + public String chain = ""; + + public boolean isRna; + + /* + * The pdb residue number (if any) mapped to columns of the alignment + */ + public int[] pdbResNo; // or use SparseIntArray? + + public int modelNo; + + /** + * Constructor + * + * @param width + * width of alignment (number of columns that may potentially + * participate in superposition) + * @param model + * structure viewer model number + */ + public SuperposeData(int width, int model) + { + pdbResNo = new int[width]; + modelNo = model; + } + } /** * Returns the command to colour by chain @@ -57,19 +91,14 @@ public interface StructureCommandsI /** * Returns commands to colour mapped residues of structures according to - * Jalview's colouring (including feature colouring if applied) + * Jalview's colouring (including feature colouring if applied). Parameter is + * a map from Color to a model of all residues assigned that colour. * - * @param structureSelectionManager - * @param files - * @param seqs - * @param sr - * @param alignmentv + * @param colourMap * @return */ - String[] colourBySequence( - StructureSelectionManager structureSelectionManager, - String[] files, SequenceI[][] seqs, SequenceRenderer sr, - AlignmentViewPanel alignmentv); + + String[] colourBySequence(Map colourMap); /** * Returns a command to centre the display in the structure viewer @@ -100,4 +129,61 @@ public interface StructureCommandsI String[] setAttributesForFeatures(StructureSelectionManager ssm, String[] files, SequenceI[][] sequence, AlignmentViewPanel avp); + /** + * Returns a command to superpose structures by closest positioning of + * residues in {@code atomSpec} to the corresponding residues in {@ refAtoms}. + * If wanted, this may include commands to visually highlight the residues + * that were used for the superposition. + * + * @param refAtoms + * @param atomSpec + * @return + */ + String superposeStructures(AtomSpecModel refAtoms, + AtomSpecModel atomSpec); + + /** + * Returns a command to open a file of commands at the given path + * + * @param path + * @return + */ + String openCommandFile(String path); + + /** + * Returns a command to save the current viewer session state to the given + * file + * + * @param filepath + * @return + */ + String saveSession(String filepath); + + /** + * Returns a representation of the atom set represented by the model, in + * viewer syntax format. If {@code alphaOnly} is true, this is restricted to + * Alpha Carbon (peptide) or Phosphorous (rna) only + * + * @param model + * @param alphaOnly + * @return + */ + String getAtomSpec(AtomSpecModel model, boolean alphaOnly); + + /** + * Returns the lowest model number used by the structure viewer (likely 0 or + * 1) + * + * @return + */ + // TODO remove by refactoring so command generation is purely driven by + // AtomSpecModel objects derived in the binding classes? + int getModelStartNo(); + + /** + * Show only the backbone of the peptide (cartoons in Jmol, chain in Chimera) + * + * @return + */ + String showBackbone(); } diff --git a/src/jalview/structures/models/AAStructureBindingModel.java b/src/jalview/structures/models/AAStructureBindingModel.java index 92b00c7..4dfdc2a 100644 --- a/src/jalview/structures/models/AAStructureBindingModel.java +++ b/src/jalview/structures/models/AAStructureBindingModel.java @@ -20,6 +20,7 @@ */ package jalview.structures.models; +import jalview.api.AlignViewportI; import jalview.api.AlignmentViewPanel; import jalview.api.FeatureRenderer; import jalview.api.SequenceRenderer; @@ -29,11 +30,15 @@ import jalview.datamodel.AlignmentI; import jalview.datamodel.HiddenColumns; import jalview.datamodel.PDBEntry; import jalview.datamodel.SequenceI; +import jalview.gui.StructureViewer.ViewerType; import jalview.io.DataSourceType; +import jalview.renderer.seqfeatures.FeatureColourFinder; import jalview.schemes.ColourSchemeI; import jalview.schemes.ResidueProperties; import jalview.structure.AtomSpec; +import jalview.structure.AtomSpecModel; import jalview.structure.StructureCommandsI; +import jalview.structure.StructureCommandsI.SuperposeData; import jalview.structure.StructureListener; import jalview.structure.StructureMapping; import jalview.structure.StructureSelectionManager; @@ -45,6 +50,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -63,6 +69,8 @@ public abstract class AAStructureBindingModel extends SequenceStructureBindingModel implements StructureListener, StructureSelectionManagerProvider { + private static final int MIN_POS_TO_SUPERPOSE = 4; + private static final String COLOURING_STRUCTURES = MessageManager .getString("status.colouring_structures"); @@ -124,35 +132,6 @@ public abstract class AAStructureBindingModel public String fileLoadingError; /** - * Data bean class to simplify parameterisation in superposeStructures - */ - protected class SuperposeData - { - /** - * Constructor with alignment width argument - * - * @param width - */ - public SuperposeData(int width) - { - pdbResNo = new int[width]; - } - - public String filename; - - public String pdbId; - - public String chain = ""; - - public boolean isRna; - - /* - * The pdb residue number (if any) mapped to each column of the alignment - */ - public int[] pdbResNo; - } - - /** * Constructor * * @param ssm @@ -692,7 +671,7 @@ public abstract class AAStructureBindingModel * for the same structure) */ s = seqCountForPdbFile; - break; + break; // fixme break out of two loops here! } } } @@ -813,21 +792,121 @@ public abstract class AAStructureBindingModel /** * Constructs and sends a command to align structures against a reference * structure, based on one or more sequence alignments. May optionally return - * an error or warning message for the alignment command. + * an error or warning message for the alignment command(s). * - * @param alignments - * an array of alignments to process - * @param structureIndices - * an array of corresponding reference structures (index into pdb - * file array); if a negative value is passed, the first PDB file - * mapped to an alignment sequence is used as the reference for - * superposition - * @param hiddenCols - * an array of corresponding hidden columns for each alignment + * @param alignWith + * an array of one or more alignment views to process * @return */ - public abstract String superposeStructures(AlignmentI[] alignments, - int[] structureIndices, HiddenColumns[] hiddenCols); + public String superposeStructures(List alignWith) + { + String error = ""; + String[] files = getStructureFiles(); + + if (!waitForFileLoad(files)) + { + return null; + } + refreshPdbEntries(); + + for (AlignmentViewPanel view : alignWith) + { + AlignmentI alignment = view.getAlignment(); + HiddenColumns hiddenCols = alignment.getHiddenColumns(); + + /* + * 'matched' bit i will be set for visible alignment columns i where + * all sequences have a residue with a mapping to their PDB structure + */ + BitSet matched = new BitSet(); + final int width = alignment.getWidth(); + for (int m = 0; m < width; 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(width, + f + commandGenerator.getModelStartNo()); + } + + /* + * Calculate the superposable alignment columns ('matched'), and the + * corresponding structure residue positions (structures.pdbResNo) + */ + int refStructure = findSuperposableResidues(alignment, + matched, structures); + + /* + * require at least 4 positions to be able to execute superposition + */ + int nmatched = matched.cardinality(); + if (nmatched < MIN_POS_TO_SUPERPOSE) + { + String msg = MessageManager.formatMessage("label.insufficient_residues", + nmatched); + error += view.getViewName() + ": " + msg + "; "; + continue; + } + + /* + * get a model of the superposable residues in the reference structure + */ + AtomSpecModel refAtoms = getAtomSpec(structures[refStructure], + matched); + + /* + * Show all as backbone before doing superposition(s) + * (residues used for matching will be shown as ribbon) + */ + executeCommand(commandGenerator.showBackbone(), false); + + /* + * superpose each (other) sequence to it in turn + */ + for (int i = 0; i < structures.length; i++) + { + if (i != refStructure) + { + AtomSpecModel atomSpec = getAtomSpec(structures[i], matched); + String commands = commandGenerator.superposeStructures(refAtoms, + atomSpec); + List replies = executeCommands(true, commands); + for (String reply : replies) + { + // return this error (Chimera only) to the user + if (reply.toLowerCase().contains("unequal numbers of atoms")) + { + error += "; " + reply; + } + } + } + } + } + + return error; + } + + private AtomSpecModel getAtomSpec(SuperposeData superposeData, + BitSet matched) + { + AtomSpecModel model = new AtomSpecModel(); + int nextColumnMatch = matched.nextSetBit(0); + while (nextColumnMatch != -1) + { + int pdbResNum = superposeData.pdbResNo[nextColumnMatch]; + model.addRange(superposeData.modelNo, pdbResNum, pdbResNum, + superposeData.chain); + nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1); + } + + return model; + } /** * returns the current sequenceRenderer that should be used to colour the @@ -1001,10 +1080,15 @@ public abstract class AAStructureBindingModel protected List executeCommands(boolean getReply, String... commands) { - List response = null; + // todo: tidy this up + List response = getReply ? new ArrayList<>() : null; for (String cmd : commands) { - response = executeCommand(cmd, getReply); + List replies = executeCommand(cmd, getReply); + if (getReply && replies != null) + { + response.addAll(replies); + } } return response; } @@ -1027,10 +1111,11 @@ public abstract class AAStructureBindingModel String[] files = getStructureFiles(); SequenceRenderer sr = getSequenceRenderer(alignmentv); + Map colourMap = buildColoursMap(ssm, files, + sequence, sr, alignmentv); String[] colourBySequenceCommands = commandGenerator - .colourBySequence(getSsm(), files, getSequence(), sr, - alignmentv); + .colourBySequence(colourMap); executeCommands(false, colourBySequenceCommands); } @@ -1151,4 +1236,184 @@ public abstract class AAStructureBindingModel { return commandGenerator; } + + protected abstract ViewerType getViewerType(); + + /** + * Send a structure viewer command asynchronously in a new thread. If the + * progress message is not null, display this message while the command is + * executing. + * + * @param command + * @param progressMsg + */ + protected void sendAsynchronousCommand(String command, String progressMsg) + { + final JalviewStructureDisplayI theViewer = getViewer(); + final long handle = progressMsg == null ? 0 + : theViewer.startProgressBar(progressMsg); + SwingUtilities.invokeLater(new Runnable() + { + @Override + public void run() + { + try + { + executeCommand(command, false); + } finally + { + if (progressMsg != null) + { + theViewer.stopProgressBar(null, handle); + } + } + } + }); + + } + + /** + *
+   * Build a data structure which records residues for each colour. 
+   * From this we can easily generate the viewer command for colour by sequence.
+   * Color
+   *     Model number
+   *         Chain
+   *             Residue positions
+   * Ordering is by order of addition (for colours and positions), natural ordering (for models and chains)
+   * 
+ * + * @param ssm + * @param files + * @param sequence + * @param sr + * @param viewPanel + * @return + */ + protected 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++) + { + final int modelNumber = pdbfnum + commandGenerator.getModelStartNo(); + 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, modelNumber, + startPos, lastPos, lastChain); + } + startPos = pos; + } + lastColour = colour; + lastPos = pos; + lastChain = chain; + } + // final colour range + if (lastColour != null) + { + addAtomSpecRange(colourMap, lastColour, modelNumber, 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 + */ + public static final void addAtomSpecRange(Map map, + Object value, + int model, int startPos, int endPos, String chain) + { + /* + * 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); + } } diff --git a/test/jalview/ext/jmol/JmolCommandsTest.java b/test/jalview/ext/jmol/JmolCommandsTest.java index 5846b33..c3ece9d 100644 --- a/test/jalview/ext/jmol/JmolCommandsTest.java +++ b/test/jalview/ext/jmol/JmolCommandsTest.java @@ -32,6 +32,8 @@ import jalview.gui.AlignFrame; import jalview.gui.JvOptionPane; import jalview.gui.SequenceRenderer; import jalview.schemes.JalviewColourScheme; +import jalview.structure.AtomSpecModel; +import jalview.structure.StructureCommandsI; import jalview.structure.StructureMapping; import jalview.structure.StructureSelectionManager; @@ -138,4 +140,62 @@ public class JmolCommandsTest assertTrue( chainBCommand.contains(";select 26-30:B/2.1;color[73,73,182]")); } + + @Test(groups = "Functional") + public void testGetAtomSpec() + { + StructureCommandsI testee = new JmolCommands(); + AtomSpecModel model = new AtomSpecModel(); + assertEquals(testee.getAtomSpec(model, false), ""); + model.addRange(1, 2, 4, "A"); + assertEquals(testee.getAtomSpec(model, false), "2-4:A/1.1"); + model.addRange(1, 8, 8, "A"); + assertEquals(testee.getAtomSpec(model, false), "2-4:A/1.1|8:A/1.1"); + model.addRange(1, 5, 7, "B"); + assertEquals(testee.getAtomSpec(model, false), + "2-4:A/1.1|8:A/1.1|5-7:B/1.1"); + model.addRange(1, 3, 5, "A"); + assertEquals(testee.getAtomSpec(model, false), + "2-5:A/1.1|8:A/1.1|5-7:B/1.1"); + model.addRange(2, 1, 4, "B"); + assertEquals(testee.getAtomSpec(model, false), + "2-5:A/1.1|8:A/1.1|5-7:B/1.1|1-4:B/2.1"); + model.addRange(2, 5, 9, "C"); + assertEquals(testee.getAtomSpec(model, false), + "2-5:A/1.1|8:A/1.1|5-7:B/1.1|1-4:B/2.1|5-9:C/2.1"); + model.addRange(1, 8, 10, "B"); + assertEquals(testee.getAtomSpec(model, false), + "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|5-9:C/2.1"); + model.addRange(1, 8, 9, "B"); + assertEquals(testee.getAtomSpec(model, false), + "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|5-9:C/2.1"); + model.addRange(2, 3, 10, "C"); // subsumes 5-9 + assertEquals(testee.getAtomSpec(model, false), + "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|3-10:C/2.1"); + model.addRange(5, 25, 35, " "); + assertEquals(testee.getAtomSpec(model, false), + "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|3-10:C/2.1|25-35:/5.1"); + + } + + @Test(groups = { "Functional" }) + public void testSuperposeStructures() + { + StructureCommandsI testee = new JmolCommands(); + AtomSpecModel ref = new AtomSpecModel(); + ref.addRange(1, 12, 14, "A"); + ref.addRange(1, 18, 18, "B"); + ref.addRange(1, 22, 23, "B"); + AtomSpecModel toAlign = new AtomSpecModel(); + toAlign.addRange(2, 15, 17, "B"); + toAlign.addRange(2, 20, 21, "B"); + toAlign.addRange(2, 22, 22, "C"); + String command = testee.superposeStructures(ref, toAlign); + String refSpec = "12-14:A/1.1|18:B/1.1|22-23:B/1.1"; + String toAlignSpec = "15-17:B/2.1|20-21:B/2.1|22:C/2.1"; + String expected = String.format( + "compare {2.1} {1.1} SUBSET {(*.CA | *.P) and conformation=1} ATOMS {%s}{%s} ROTATE TRANSLATE ;select %s|%s;cartoons", + toAlignSpec, refSpec, toAlignSpec, refSpec); + assertEquals(command, expected); + } } diff --git a/test/jalview/ext/rbvi/chimera/AtomSpecModelTest.java b/test/jalview/ext/rbvi/chimera/AtomSpecModelTest.java deleted file mode 100644 index 63d5e4e..0000000 --- a/test/jalview/ext/rbvi/chimera/AtomSpecModelTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package jalview.ext.rbvi.chimera; - -import static org.testng.Assert.assertEquals; - -import org.testng.annotations.Test; - -public class AtomSpecModelTest -{ - @Test(groups = "Functional") - public void testGetAtomSpec() - { - AtomSpecModel model = new AtomSpecModel(); - assertEquals(model.getAtomSpec(), ""); - model.addRange(1, 2, 4, "A"); - assertEquals(model.getAtomSpec(), "#1:2-4.A"); - model.addRange(1, 8, 8, "A"); - assertEquals(model.getAtomSpec(), "#1:2-4.A,8.A"); - model.addRange(1, 5, 7, "B"); - assertEquals(model.getAtomSpec(), "#1:2-4.A,8.A,5-7.B"); - model.addRange(1, 3, 5, "A"); - assertEquals(model.getAtomSpec(), "#1:2-5.A,8.A,5-7.B"); - model.addRange(0, 1, 4, "B"); - assertEquals(model.getAtomSpec(), "#0:1-4.B|#1:2-5.A,8.A,5-7.B"); - model.addRange(0, 5, 9, "C"); - assertEquals(model.getAtomSpec(), "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-7.B"); - model.addRange(1, 8, 10, "B"); - assertEquals(model.getAtomSpec(), "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B"); - model.addRange(1, 8, 9, "B"); - assertEquals(model.getAtomSpec(), "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B"); - model.addRange(0, 3, 10, "C"); // subsumes 5-9 - assertEquals(model.getAtomSpec(), "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B"); - model.addRange(5, 25, 35, " "); // empty chain code - e.g. from homology - // modelling - assertEquals(model.getAtomSpec(), - "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B|#5:25-35."); - - } - -} diff --git a/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java b/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java index 0679098..d0e6155 100644 --- a/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java +++ b/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java @@ -31,6 +31,8 @@ import jalview.datamodel.SequenceI; import jalview.gui.AlignFrame; import jalview.gui.SequenceRenderer; import jalview.schemes.JalviewColourScheme; +import jalview.structure.AtomSpecModel; +import jalview.structure.StructureCommandsI; import jalview.structure.StructureMapping; import jalview.structure.StructureSelectionManager; @@ -209,4 +211,98 @@ public class ChimeraCommandsTest // S and G are both coloured #4949b6 assertTrue(theCommand.contains("color #4949b6 #0:26-30.A|#1:26-30.B")); } + + @Test(groups = "Functional") + public void testGetAtomSpec() + { + StructureCommandsI testee = new ChimeraCommands(); + AtomSpecModel model = new AtomSpecModel(); + assertEquals(testee.getAtomSpec(model, false), ""); + model.addRange(1, 2, 4, "A"); + assertEquals(testee.getAtomSpec(model, false), "#1:2-4.A"); + model.addRange(1, 8, 8, "A"); + assertEquals(testee.getAtomSpec(model, false), "#1:2-4.A,8.A"); + model.addRange(1, 5, 7, "B"); + assertEquals(testee.getAtomSpec(model, false), "#1:2-4.A,8.A,5-7.B"); + model.addRange(1, 3, 5, "A"); + assertEquals(testee.getAtomSpec(model, false), "#1:2-5.A,8.A,5-7.B"); + model.addRange(0, 1, 4, "B"); + assertEquals(testee.getAtomSpec(model, false), + "#0:1-4.B|#1:2-5.A,8.A,5-7.B"); + model.addRange(0, 5, 9, "C"); + assertEquals(testee.getAtomSpec(model, false), + "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-7.B"); + model.addRange(1, 8, 10, "B"); + assertEquals(testee.getAtomSpec(model, false), + "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B"); + model.addRange(1, 8, 9, "B"); + assertEquals(testee.getAtomSpec(model, false), + "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B"); + model.addRange(0, 3, 10, "C"); // subsumes 5-9 + assertEquals(testee.getAtomSpec(model, false), + "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B"); + model.addRange(5, 25, 35, " "); // empty chain code - e.g. from homology + // modelling + assertEquals(testee.getAtomSpec(model, false), + "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B|#5:25-35."); + + } + + @Test(groups = "Functional") + public void testGetAtomSpec_alphaOnly() + { + StructureCommandsI testee = new ChimeraCommands(); + AtomSpecModel model = new AtomSpecModel(); + assertEquals(testee.getAtomSpec(model, true), ""); + model.addRange(1, 2, 4, "A"); + assertEquals(testee.getAtomSpec(model, true), "#1:2-4.A@CA|P"); + model.addRange(1, 8, 8, "A"); + assertEquals(testee.getAtomSpec(model, true), "#1:2-4.A,8.A@CA|P"); + model.addRange(1, 5, 7, "B"); + assertEquals(testee.getAtomSpec(model, true), + "#1:2-4.A,8.A,5-7.B@CA|P"); + model.addRange(1, 3, 5, "A"); + assertEquals(testee.getAtomSpec(model, true), + "#1:2-5.A,8.A,5-7.B@CA|P"); + model.addRange(0, 1, 4, "B"); + assertEquals(testee.getAtomSpec(model, true), + "#0:1-4.B@CA|P|#1:2-5.A,8.A,5-7.B@CA|P"); + model.addRange(0, 5, 9, "C"); + assertEquals(testee.getAtomSpec(model, true), + "#0:1-4.B,5-9.C@CA|P|#1:2-5.A,8.A,5-7.B@CA|P"); + model.addRange(1, 8, 10, "B"); + assertEquals(testee.getAtomSpec(model, true), + "#0:1-4.B,5-9.C@CA|P|#1:2-5.A,8.A,5-10.B@CA|P"); + model.addRange(1, 8, 9, "B"); + assertEquals(testee.getAtomSpec(model, true), + "#0:1-4.B,5-9.C@CA|P|#1:2-5.A,8.A,5-10.B@CA|P"); + model.addRange(0, 3, 10, "C"); // subsumes 5-9 + assertEquals(testee.getAtomSpec(model, true), + "#0:1-4.B,3-10.C@CA|P|#1:2-5.A,8.A,5-10.B@CA|P"); + model.addRange(5, 25, 35, " "); // empty chain code + assertEquals(testee.getAtomSpec(model, true), + "#0:1-4.B,3-10.C@CA|P|#1:2-5.A,8.A,5-10.B@CA|P|#5:25-35.@CA|P"); + + } + + @Test(groups = { "Functional" }) + public void testSuperposeStructures() + { + StructureCommandsI testee = new ChimeraCommands(); + AtomSpecModel ref = new AtomSpecModel(); + ref.addRange(1, 12, 14, "A"); + ref.addRange(1, 18, 18, "B"); + ref.addRange(1, 22, 23, "B"); + AtomSpecModel toAlign = new AtomSpecModel(); + toAlign.addRange(2, 15, 17, "B"); + toAlign.addRange(2, 20, 21, "B"); + toAlign.addRange(2, 22, 22, "C"); + String command = testee.superposeStructures(ref, toAlign); + String refSpec = "#1:12-14.A,18.B,22-23.B@CA|P&~@.B-Z&~@.2-9"; + String toAlignSpec = "#2:15-17.B,20-21.B,22.C@CA|P&~@.B-Z&~@.2-9"; + String expected = String.format( + "match %s %s;~display all; chain @CA|P; ribbon %s|%s; focus", + refSpec, toAlignSpec, refSpec, toAlignSpec); + assertEquals(command, expected); + } } diff --git a/test/jalview/ext/rbvi/chimera/ChimeraXCommandsTest.java b/test/jalview/ext/rbvi/chimera/ChimeraXCommandsTest.java index d8b60c2..a9eaca3 100644 --- a/test/jalview/ext/rbvi/chimera/ChimeraXCommandsTest.java +++ b/test/jalview/ext/rbvi/chimera/ChimeraXCommandsTest.java @@ -31,6 +31,8 @@ import jalview.datamodel.SequenceI; import jalview.gui.AlignFrame; import jalview.gui.SequenceRenderer; import jalview.schemes.JalviewColourScheme; +import jalview.structure.AtomSpecModel; +import jalview.structure.StructureCommandsI; import jalview.structure.StructureMapping; import jalview.structure.StructureSelectionManager; @@ -191,4 +193,25 @@ public class ChimeraXCommandsTest // S and G are both coloured #4949b6 assertTrue(theCommand.contains("color #0/A:26-30|#1/B:26-30")); } + + @Test(groups = { "Functional" }) + public void testSuperposeStructures() + { + StructureCommandsI testee = new ChimeraXCommands(); + AtomSpecModel ref = new AtomSpecModel(); + ref.addRange(1, 12, 14, "A"); + ref.addRange(1, 18, 18, "B"); + ref.addRange(1, 22, 23, "B"); + AtomSpecModel toAlign = new AtomSpecModel(); + toAlign.addRange(2, 15, 17, "B"); + toAlign.addRange(2, 20, 21, "B"); + toAlign.addRange(2, 22, 22, "C"); + String command = testee.superposeStructures(ref, toAlign); + String refSpec = "#1/A:12-14/B:18,22-23"; + String toAlignSpec = "#2/B:15-17,20-21/C:22"; + String expected = String.format( + "align %s %s;~display all; chain @CA|P; ribbon %s|%s; focus", + refSpec, toAlignSpec, refSpec, toAlignSpec); + assertEquals(command, expected); + } } diff --git a/test/jalview/structure/AtomSpecModelTest.java b/test/jalview/structure/AtomSpecModelTest.java new file mode 100644 index 0000000..8c6ead7 --- /dev/null +++ b/test/jalview/structure/AtomSpecModelTest.java @@ -0,0 +1,51 @@ +package jalview.structure; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import java.util.List; + +import org.testng.annotations.Test; + +public class AtomSpecModelTest +{ + @Test(groups="Functional") + public void testGetRanges() + { + AtomSpecModel model = new AtomSpecModel(); + assertFalse(model.getModels().iterator().hasNext()); + List ranges = model.getRanges(1, "A"); + assertTrue(ranges.isEmpty()); + + model.addRange(1, 12, 14, "A"); + assertTrue(model.getRanges(1, "B").isEmpty()); + assertTrue(model.getRanges(2, "A").isEmpty()); + ranges = model.getRanges(1, "A"); + assertEquals(ranges.size(), 1); + int[] range = ranges.get(0); + assertEquals(range[0], 12); + assertEquals(range[1], 14); + + /* + * add some ranges; they should be coalesced and + * ordered when retrieved + */ + model.addRange(1, 25, 25, "A"); + model.addRange(1, 20, 24, "A"); + model.addRange(1, 6, 8, "A"); + model.addRange(1, 13, 18, "A"); + model.addRange(1, 5, 6, "A"); + ranges = model.getRanges(1, "A"); + assertEquals(ranges.size(), 3); + range = ranges.get(0); + assertEquals(range[0], 5); + assertEquals(range[1], 8); + range = ranges.get(1); + assertEquals(range[0], 12); + assertEquals(range[1], 18); + range = ranges.get(2); + assertEquals(range[0], 20); + assertEquals(range[1], 25); + } +} diff --git a/test/jalview/structures/models/AAStructureBindingModelTest.java b/test/jalview/structures/models/AAStructureBindingModelTest.java index c890536..c201926 100644 --- a/test/jalview/structures/models/AAStructureBindingModelTest.java +++ b/test/jalview/structures/models/AAStructureBindingModelTest.java @@ -28,17 +28,17 @@ import jalview.api.AlignmentViewPanel; import jalview.api.SequenceRenderer; import jalview.datamodel.Alignment; import jalview.datamodel.AlignmentI; -import jalview.datamodel.HiddenColumns; import jalview.datamodel.PDBEntry; import jalview.datamodel.PDBEntry.Type; import jalview.datamodel.Sequence; import jalview.datamodel.SequenceI; import jalview.gui.JvOptionPane; +import jalview.gui.StructureViewer.ViewerType; import jalview.io.DataSourceType; import jalview.io.FileFormats; import jalview.structure.AtomSpec; +import jalview.structure.StructureCommandsI.SuperposeData; import jalview.structure.StructureSelectionManager; -import jalview.structures.models.AAStructureBindingModel.SuperposeData; import java.io.IOException; import java.util.Arrays; @@ -149,13 +149,6 @@ public class AAStructureBindingModelTest } @Override - public String superposeStructures(AlignmentI[] alignments, - int[] structureIndices, HiddenColumns[] hiddenCols) - { - return null; - } - - @Override public void highlightAtoms(List atoms) { } @@ -178,6 +171,12 @@ public class AAStructureBindingModelTest { return 0; } + + @Override + protected ViewerType getViewerType() + { + return null; + } }; String[][] chains = binder.getChains(); assertFalse(chains == null || chains[0] == null, @@ -247,13 +246,6 @@ public class AAStructureBindingModelTest } @Override - public String superposeStructures(AlignmentI[] als, int[] alm, - HiddenColumns[] alc) - { - return null; - } - - @Override public SequenceRenderer getSequenceRenderer( AlignmentViewPanel alignment) { @@ -272,6 +264,12 @@ public class AAStructureBindingModelTest { return 0; } + + @Override + protected ViewerType getViewerType() + { + return null; + } }; } @@ -288,7 +286,7 @@ public class AAStructureBindingModelTest SuperposeData[] structs = new SuperposeData[testee.getStructureFiles().length]; for (int i = 0; i < structs.length; i++) { - structs[i] = testee.new SuperposeData(al.getWidth()); + structs[i] = new SuperposeData(al.getWidth(), 0); } /* * initialise BitSet of 'superposable columns' to true (would be false for @@ -335,7 +333,7 @@ public class AAStructureBindingModelTest SuperposeData[] structs = new SuperposeData[al.getHeight()]; for (int i = 0; i < structs.length; i++) { - structs[i] = testee.new SuperposeData(al.getWidth()); + structs[i] = new SuperposeData(al.getWidth(), 0); } /* * initialise BitSet of 'superposable columns' to true (would be false for @@ -363,4 +361,4 @@ public class AAStructureBindingModelTest assertFalse(matched.get(4)); // superposable, but hidden, column assertTrue(matched.get(5)); } -} +} \ No newline at end of file -- 1.7.10.2