X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjalview%2Fext%2Frbvi%2Fchimera%2FChimeraCommands.java;h=a6a72e0f75040e1daa1cbfb40280609c8cb8db8e;hb=6bf5604d306cbcd55289639967411ddbafcac682;hp=41c7abbf251beda9f67558e735ece5d708fc795e;hpb=ae216fceddd8539d20fa5425cdaef3da73aba8f0;p=jalview.git diff --git a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java index 41c7abb..a6a72e0 100644 --- a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java +++ b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java @@ -1,6 +1,6 @@ /* - * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8.2) - * Copyright (C) 2014 The Jalview Authors + * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) + * Copyright (C) $$Year-Rel$$ The Jalview Authors * * This file is part of Jalview. * @@ -20,18 +20,31 @@ */ package jalview.ext.rbvi.chimera; +import jalview.api.AlignViewportI; +import jalview.api.AlignmentViewPanel; import jalview.api.FeatureRenderer; import jalview.api.SequenceRenderer; -import jalview.api.structures.JalviewStructureDisplayI; import jalview.datamodel.AlignmentI; +import jalview.datamodel.HiddenColumns; +import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.renderer.seqfeatures.FeatureColourFinder; import jalview.structure.StructureMapping; import jalview.structure.StructureMappingcommandSet; import jalview.structure.StructureSelectionManager; -import jalview.util.Format; +import jalview.structures.models.AAStructureBindingModel; +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; /** * Routines for generating Chimera commands for Jalview/Chimera binding @@ -41,122 +54,570 @@ import java.util.ArrayList; */ public class ChimeraCommands { + 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); + + /** + * Constructs Chimera commands to colour residues as per the Jalview alignment + * + * @param colourMap + * @param binding + * @return + */ + public static String[] getColourBySequenceCommand( + Map colourMap, + AAStructureBindingModel binding) + { + List colourCommands = buildColourCommands(colourMap, binding); + + return colourCommands.toArray(new String[colourCommands.size()]); + } /** - * utility to construct the commands to colour chains by the given alignment - * for passing to Chimera + * 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 * - * @returns Object[] { Object[] { , + *
+   * 
+ * 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 */ - public static StructureMappingcommandSet[] getColourBySequenceCommand( - StructureSelectionManager ssm, String[] files, - SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr, - AlignmentI alignment) + protected static List buildColourCommands( + 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<>(); + StringBuilder sb = new StringBuilder(256); + sb.append(COLOR_GRAY_HEX); - ArrayList cset = new ArrayList(); + for (Object key : colourMap.keySet()) + { + Color colour = (Color) key; + String colourCode = ColorUtils.toTkCode(colour); + 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; + } + + /** + * 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 hideHiddenRegions + * @param viewPanel + * @return + */ + protected static Map buildColoursMap( + StructureSelectionManager ssm, String[] files, + SequenceI[][] sequence, SequenceRenderer sr, + boolean hideHiddenRegions, 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++) { - float cols[] = new float[4]; StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]); - StringBuffer command = new StringBuffer(); - StructureMappingcommandSet smc; - ArrayList str = new ArrayList(); if (mapping == null || mapping.length < 1) + { continue; + } - int startPos = -1, lastPos = -1, startModel = -1, lastModel = -1; - String startChain = "", lastChain = ""; - Color lastCol = null; + 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++) { - if (mapping[m].getSequence() == sequence[pdbfnum][s] - && (sp = alignment.findIndex(sequence[pdbfnum][s])) > -1) + final SequenceI seq = sequence[pdbfnum][s]; + if (mapping[m].getSequence() == seq + && (sp = al.findIndex(seq)) > -1) { - SequenceI asp = alignment.getSequenceAt(sp); + SequenceI asp = al.getSequenceAt(sp); for (int r = 0; r < asp.getLength(); r++) { // no mapping to gaps in sequence - if (jalview.util.Comparison.isGap(asp.getCharAt(r))) + if (Comparison.isGap(asp.getCharAt(r))) { continue; } int pos = mapping[m].getPDBResNum(asp.findPosition(r)); if (pos < 1 || pos == lastPos) + { continue; + } - Color col = sr.getResidueBoxColour(sequence[pdbfnum][s], r); + Color colour = sr.getResidueColour(seq, r, finder); - if (fr != null) - col = fr.findFeatureColour(col, sequence[pdbfnum][s], r); - if (lastCol != col || lastPos + 1 != pos - || pdbfnum != lastModel - || !mapping[m].getChain().equals(lastChain)) + /* + * hidden regions are shown gray or, optionally, ignored + */ + if (!cs.isVisible(r)) { - if (lastCol != null) + if (hideHiddenRegions) { + continue; + } + else + { + colour = Color.GRAY; + } + } + + final String chain = mapping[m].getChain(); - lastCol.getRGBComponents(cols); - String newSelcom = "color " + cols[0] + "," + cols[1] - + "," + cols[2] + " #" + startModel + ":" - + startPos + "-" + lastPos + "." + lastChain; - if (str.size() > 0 - && (str.get(str.size() - 1).length() + newSelcom - .length()) < 4096) - { - str.set(str.size() - 1, str.get(str.size() - 1) + ";" - + newSelcom); - } - else - { - str.add(newSelcom); - } + /* + * 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) + { + addMapRange(colourMap, lastColour, pdbfnum, startPos, + lastPos, lastChain); } - lastCol = null; startPos = pos; - startModel = pdbfnum; - startChain = mapping[m].getChain(); } - lastCol = col; + lastColour = colour; lastPos = pos; - lastModel = pdbfnum; - lastChain = mapping[m].getChain(); + lastChain = chain; } // final colour range - if (lastCol != null) + if (lastColour != null) { - - lastCol.getRGBComponents(cols); - String newSelcom = "color " + cols[0] + "," + cols[1] + "," - + cols[2] + " #" + startModel + ":" + startPos + "-" - + lastPos + "." + lastChain; - if (str.size() > 0 - && (str.get(str.size() - 1).length() + newSelcom - .length()) < 4096) - { - str.set(str.size() - 1, str.get(str.size() - 1) + ";" - + newSelcom); - } - else - { - str.add(newSelcom); - } + addMapRange(colourMap, lastColour, pdbfnum, startPos, + lastPos, lastChain); } - break; + // break; + } + } + } + } + return colourMap; + } + + /** + * Helper method to add one contiguous range to the map, for a given value key + * (e.g. colour or feature type), structure model number, and chain + * + * @param map + * @param key + * @param model + * @param startPos + * @param endPos + * @param chain + */ + public static void addMapRange(Map map, + Object key, int model, int startPos, int endPos, String chain) + { + /* + * Get/initialize map of data for the colour + */ + AtomSpecModel atomSpec = map.get(key); + if (atomSpec == null) + { + atomSpec = new AtomSpecModel(); + map.put(key, atomSpec); + } + + atomSpec.addRange(model, startPos, endPos, chain); + } + + /** + * 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; + } + + List visibleFeatures = fr.getDisplayedFeatureTypes(); + if (visibleFeatures.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++) + { + for (int m = 0; m < mapping.length; m++) + { + final SequenceI seq = seqs[pdbfnum][seqNo]; + int sp = alignment.findIndex(seq); + if (mapping[m].getSequence() == seq && sp > -1) + { + /* + * found a sequence with a mapping to a structure; + * now scan its features + */ + SequenceI asp = alignment.getSequenceAt(sp); + + scanSequenceFeatures(visibleFeatures, mapping[m], asp, theMap, + pdbfnum); + } + } + } + } + return theMap; + } + + /** + * 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 void scanSequenceFeatures(List visibleFeatures, + StructureMapping mapping, SequenceI seq, + Map> theMap, int modelNumber) + { + List sfs = seq.getFeatures().getPositionalFeatures( + visibleFeatures.toArray(new String[visibleFeatures.size()])); + for (SequenceFeature sf : sfs) + { + String type = sf.getType(); + + /* + * Only copy visible features, don't copy any which originated + * from Chimera, and suppress uninteresting ones (e.g. RESNUM) + */ + boolean isFromViewer = JalviewChimeraBinding.CHIMERA_FEATURE_GROUP + .equals(sf.getFeatureGroup()); + if (isFromViewer) + { + continue; + } + List mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(), + sf.getEnd()); + + 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) + { + addMapRange(featureValues, value, modelNumber, range[0], + range[1], mapping.getChain()); + } + } + } + } + + /** + * 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 + * + *

+   * 
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 List buildSetAttributeCommands( + Map> featureMap, + AAStructureBindingModel binding) + { + List commands = new ArrayList<>(); + for (String featureType : featureMap.keySet()) + { + 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()); + } + } + + 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) + { + for (char c : featureType.toCharArray()) + { + sb.append(Character.isLetterOrDigit(c) ? c : '_'); + } + } + 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; + } + + /** + * 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; + // todo use JalviewChimeraBinding.getModelSpec(model) + // which means this cannot be static + 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); + + /* + * sort ranges into ascending start position order + */ + Collections.sort(rangeList, IntRangeComparator.ASCENDING); + + int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0]; + int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1]; + + Iterator iterator = rangeList.iterator(); + while (iterator.hasNext()) + { + int[] range = iterator.next(); + if (range[0] <= end + 1) + { + /* + * range overlaps or is contiguous with the last one + * - so just extend the end position, and carry on + * (unless this is the last in the list) + */ + end = Math.max(end, range[1]); + } + else + { + /* + * we have a break so append the last range + */ + appendRange(sb, start, end, chain, firstPositionForModel); + firstPositionForModel = false; + start = range[0]; + end = range[1]; } } + + /* + * and append the last range + */ + if (!rangeList.isEmpty()) + { + appendRange(sb, start, end, chain, firstPositionForModel); + firstPositionForModel = false; + } } - // Finally, add the command set ready to be returned. - cset.add(new StructureMappingcommandSet(ChimeraCommands.class, - files[pdbfnum], str.toArray(new String[str.size()]))); } - return cset.toArray(new StructureMappingcommandSet[cset.size()]); + return sb.toString(); + } + + /** + * A helper method that appends one start-end range to a Chimera atomspec + * + * @param sb + * @param start + * @param end + * @param chain + * @param firstPositionForModel + */ + static void appendRange(StringBuilder sb, int start, int end, + String chain, boolean firstPositionForModel) + { + if (!firstPositionForModel) + { + sb.append(","); + } + if (end == start) + { + sb.append(start); + } + else + { + sb.append(start).append("-").append(end); + } + + sb.append("."); + if (!" ".equals(chain)) + { + sb.append(chain); + } } }