X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjalview%2Fext%2Frbvi%2Fchimera%2FChimeraCommands.java;h=bfec5b274ae824ceb7cedeaaf60417453737e8af;hb=970992fea3af6d3495bf4bc795e81295e885df4d;hp=fa1b7e217afb58fa8a0fc35e642f3e8db6fe3c1f;hpb=ad15cff29620f960119f80176f1fd443da9f6763;p=jalview.git diff --git a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java index fa1b7e2..bfec5b2 100644 --- a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java +++ b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java @@ -20,22 +20,27 @@ */ 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.MappedFeatures; +import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.gui.Desktop; import jalview.structure.StructureMapping; import jalview.structure.StructureMappingcommandSet; import jalview.structure.StructureSelectionManager; +import jalview.structures.models.AAStructureBindingModel; import jalview.util.ColorUtils; -import jalview.util.Comparison; +import jalview.util.StructureCommands; import java.awt.Color; import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.TreeMap; /** * Routines for generating Chimera commands for Jalview/Chimera binding @@ -43,32 +48,30 @@ import java.util.TreeMap; * @author JimP * */ -public class ChimeraCommands +public class ChimeraCommands extends StructureCommands { + public static final String NAMESPACE_PREFIX = "jv_"; + + /* + * colour for residues shown in structure but hidden in alignment + */ + private static final String COLOR_GRAY_HEX = "color " + + ColorUtils.toTkCode(Color.GRAY); /** - * utility to construct the commands to colour chains by the given alignment - * for passing to Chimera - * - * @returns Object[] { Object[] { , + * Constructs Chimera commands to colour residues as per the Jalview alignment * + * @param colourMap + * @param binding + * @return */ - public static StructureMappingcommandSet[] getColourBySequenceCommand( - StructureSelectionManager ssm, String[] files, - SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr, - AlignmentI alignment) + public static String[] getColourBySequenceCommand( + Map colourMap, + AAStructureBindingModel binding) { - Map>>> colourMap = buildColoursMap( - ssm, files, sequence, sr, fr, alignment); + List colourCommands = buildColourCommands(colourMap, binding); - List colourCommands = buildColourCommands(colourMap); - - StructureMappingcommandSet cs = new StructureMappingcommandSet( - ChimeraCommands.class, null, - colourCommands.toArray(new String[0])); - - return new StructureMappingcommandSet[] - { cs }; + return colourCommands.toArray(new String[colourCommands.size()]); } /** @@ -76,215 +79,437 @@ public class ChimeraCommands * '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,... - * - * @see http - * ://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec - * .html + *
+   * 
+ * 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 + * @param binding * @return */ protected static List buildColourCommands( - Map>>> colourMap) + Map colourMap, + AAStructureBindingModel binding) { /* * 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(); + List commands = new ArrayList<>(); StringBuilder sb = new StringBuilder(256); - boolean firstColour = true; - for (Color colour : colourMap.keySet()) + sb.append(COLOR_GRAY_HEX); + + for (Object key : colourMap.keySet()) { + Color colour = (Color) key; String colourCode = ColorUtils.toTkCode(colour); - if (!firstColour) + sb.append("; "); + sb.append("color ").append(colourCode).append(" "); + final AtomSpecModel colourData = colourMap.get(colour); + sb.append(getAtomSpec(colourData, binding)); + } + commands.add(sb.toString()); + return commands; + } + + /** + * Constructs and returns Chimera commands to set attributes on residues + * corresponding to features in Jalview. Attribute names are the Jalview feature + * type, with a "jv_" prefix. + * + * @param ssm + * @param files + * @param seqs + * @param viewPanel + * @param binding + * @return + */ + public static StructureMappingcommandSet getSetAttributeCommandsForFeatures( + AlignmentViewPanel viewPanel, AAStructureBindingModel binding) + { + StructureSelectionManager ssm = binding.getSsm(); + String[] files = binding.getStructureFiles(); + SequenceI[][] seqs = binding.getSequence(); + + Map> featureMap = buildFeaturesMap( + ssm, files, seqs, viewPanel); + + List commands = buildSetAttributeCommands(featureMap, binding); + + StructureMappingcommandSet cs = new StructureMappingcommandSet( + ChimeraCommands.class, null, + commands.toArray(new String[commands.size()])); + + return cs; + } + + /** + *
+   * Helper method to build a map of 
+   *   { featureType, { feature value, AtomSpecModel } }
+   * 
+ * + * @param ssm + * @param files + * @param seqs + * @param viewPanel + * @return + */ + protected static Map> buildFeaturesMap( + StructureSelectionManager ssm, String[] files, SequenceI[][] seqs, + AlignmentViewPanel viewPanel) + { + 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) { - sb.append("; "); + complementRenderer = Desktop.getAlignFrameFor(comp) + .getFeatureRenderer(); + complementFeatures = complementRenderer.getDisplayedFeatureTypes(); } - sb.append("color ").append(colourCode).append(" "); - firstColour = false; - boolean firstModelForColour = true; - final Map>> colourData = colourMap.get(colour); - for (Integer model : colourData.keySet()) + } + 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]); + + if (mapping == null || mapping.length < 1) + { + continue; + } + + for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++) { - boolean firstPositionForModel = true; - if (!firstModelForColour) + for (int m = 0; m < mapping.length; m++) { - sb.append("|"); + 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); + } + } } - firstModelForColour = false; - sb.append("#").append(model).append(":"); + } + } + return theMap; + } - final Map> modelData = colourData.get(model); - for (String chain : modelData.keySet()) + /** + * Scans visible features in mapped positions of the CDS/peptide complement, and + * adds any found to the map of attribute values/structure positions + * + * @param complementRenderer + * @param structureMapping + * @param seq + * @param theMap + * @param modelNumber + */ + protected static void scanComplementFeatures( + FeatureRenderer complementRenderer, + StructureMapping structureMapping, SequenceI seq, + Map> theMap, int modelNumber) + { + /* + * for each sequence residue mapped to a structure position... + */ + for (int seqPos : structureMapping.getMapping().keySet()) + { + /* + * find visible complementary features at mapped position(s) + */ + MappedFeatures mf = complementRenderer + .findComplementFeaturesAtResidue(seq, seqPos); + if (mf != null) + { + for (SequenceFeature sf : mf.features) { - for (int[] range : modelData.get(chain)) + String type = sf.getType(); + + /* + * Don't copy features which originated from Chimera + */ + if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP + .equals(sf.getFeatureGroup())) { - if (!firstPositionForModel) + 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)) { - sb.append(","); + value = Float.toString(score); } - if (range[0] == range[1]) + Map featureValues = theMap.get(type); + if (featureValues == null) { - sb.append(range[0]); + featureValues = new HashMap<>(); + theMap.put(type, featureValues); } - else + for (int[] range : mappedRanges) { - sb.append(range[0]).append("-").append(range[1]); + addAtomSpecRange(featureValues, value, modelNumber, range[0], + range[1], structureMapping.getChain()); } - sb.append(".").append(chain); - firstPositionForModel = false; } } } } - commands.add(sb.toString()); - return commands; } /** - *
-   * Build a data structure which maps contiguous subsequences for each colour. 
-   * This generates a data structure from which 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)
-   * 
+ * Inspect features on the sequence; for each feature that is visible, determine + * its mapped ranges in the structure (if any) according to the given mapping, + * and add them to the map. + * + * @param visibleFeatures + * @param mapping + * @param seq + * @param theMap + * @param modelNumber */ - protected static Map>>> buildColoursMap( - StructureSelectionManager ssm, String[] files, - SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr, - AlignmentI alignment) + protected static void scanSequenceFeatures(List visibleFeatures, + StructureMapping mapping, SequenceI seq, + Map> theMap, int modelNumber) { - Map>>> colourMap = new LinkedHashMap>>>(); - Color lastColour = null; - for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) + List sfs = seq.getFeatures().getPositionalFeatures( + visibleFeatures.toArray(new String[visibleFeatures.size()])); + for (SequenceFeature sf : sfs) { - StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]); + String type = sf.getType(); - if (mapping == null || mapping.length < 1) + /* + * Don't copy features which originated from Chimera + */ + if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP + .equals(sf.getFeatureGroup())) { continue; } - int startPos = -1, lastPos = -1; - String lastChain = ""; - for (int s = 0; s < sequence[pdbfnum].length; s++) + List mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(), + sf.getEnd()); + + if (!mappedRanges.isEmpty()) { - for (int sp, m = 0; m < mapping.length; m++) + String value = sf.getDescription(); + if (value == null || value.length() == 0) { - final SequenceI seq = sequence[pdbfnum][s]; - if (mapping[m].getSequence() == seq - && (sp = alignment.findIndex(seq)) > -1) - { - SequenceI asp = alignment.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, fr); - 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) - { - addColourRange(colourMap, lastColour, pdbfnum, startPos, - lastPos, lastChain); - } - startPos = pos; - } - lastColour = colour; - lastPos = pos; - lastChain = chain; - } - // final colour range - if (lastColour != null) - { - addColourRange(colourMap, lastColour, pdbfnum, startPos, - lastPos, lastChain); - } - break; - } + 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], mapping.getChain()); } } } - return colourMap; } /** - * Helper method to add one contiguous colour range to the colour map. + * Traverse the map of features/values/models/chains/positions to construct a + * list of 'setattr' commands (one per distinct feature type and value). + *

+ * The format of each command is * - * @param colourMap - * @param colour - * @param model - * @param startPos - * @param endPos - * @param chain + *

+   * 
setattr r " " #modelnumber:range.chain + * e.g. setattr r jv:chain #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,... + *
+ *
+ * + * @param featureMap + * @param binding + * @return */ - protected static void addColourRange( - Map>>> colourMap, - Color colour, int model, int startPos, int endPos, String chain) + protected static List buildSetAttributeCommands( + Map> featureMap, + AAStructureBindingModel binding) { - /* - * Get/initialize map of data for the colour - */ - Map>> colourData = colourMap - .get(colour); - if (colourData == null) + List commands = new ArrayList<>(); + for (String featureType : featureMap.keySet()) { - colourMap - .put(colour, - colourData = new TreeMap>>()); + String attributeName = makeAttributeName(featureType); + + /* + * clear down existing attributes for this feature + */ + // 'problem' - sets attribute to None on all residues - overkill? + // commands.add("~setattr r " + attributeName + " :*"); + + Map values = featureMap.get(featureType); + for (Object value : values.keySet()) + { + /* + * for each distinct value recorded for this feature type, + * add a command to set the attribute on the mapped residues + * Put values in single quotes, encoding any embedded single quotes + */ + StringBuilder sb = new StringBuilder(128); + String featureValue = value.toString(); + featureValue = featureValue.replaceAll("\\'", "'"); + sb.append("setattr r ").append(attributeName).append(" '") + .append(featureValue).append("' "); + sb.append(getAtomSpec(values.get(value), binding)); + commands.add(sb.toString()); + } } - /* - * Get/initialize map of data for the colour and model - */ - Map> modelData = colourData.get(model); - if (modelData == null) + return commands; + } + + /** + * 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) { - colourData.put(model, modelData = new TreeMap>()); + for (char c : featureType.toCharArray()) + { + sb.append(Character.isLetterOrDigit(c) ? c : '_'); + } } + String attName = NAMESPACE_PREFIX + sb.toString(); /* - * Get/initialize map of data for colour, model and chain + * Chimera treats an attribute name ending in 'color' as colour-valued; + * Jalview doesn't, so prevent this by appending an underscore */ - List chainData = modelData.get(chain); - if (chainData == null) + if (attName.toUpperCase().endsWith("COLOR")) { - modelData.put(chain, chainData = new ArrayList()); + attName += "_"; } - /* - * Add the start/end positions - */ - chainData.add(new int[] - { startPos, endPos }); + return attName; + } + + /** + * Returns the range(s) formatted as a Chimera atomspec + * + * @return + */ + public static String getAtomSpec(AtomSpecModel atomSpec, + AAStructureBindingModel binding) + { + StringBuilder sb = new StringBuilder(128); + boolean firstModel = true; + for (Integer model : atomSpec.getModels()) + { + if (!firstModel) + { + sb.append("|"); + } + firstModel = false; + sb.append(binding.getModelSpec(model)).append(":"); + + boolean firstPositionForModel = true; + + for (String chain : atomSpec.getChains(model)) + { + chain = " ".equals(chain) ? chain : chain.trim(); + + List rangeList = atomSpec.getRanges(model, chain); + + String chainToken = " ".equals(chain) ? "." : "." + chain; + appendResidueRange(sb, rangeList, chainToken, + firstPositionForModel); + firstPositionForModel = false; + } + } + return sb.toString(); + } + + /** + * Chimera atomspec requires chain to be specified for each start-end residue + * range, otherwise it will apply to all chains + * + * @param sb + * @param chain + */ + protected static void appendChainToRange(StringBuilder sb, String chain) + { + sb.append("."); + if (!" ".equals(chain)) + { + sb.append(chain); + } } }