X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjalview%2Fext%2Frbvi%2Fchimera%2FChimeraCommands.java;h=dad85117fb15622f90656836a1bc7fffcc0f31e4;hb=72404f561ccd190eab1990874f017bbe94bf9e10;hp=3c47ed1a3e4befa31041491ddb95ea757e8ed454;hpb=81316c6cc11e29c893d55e87ebd16d9ecd09c1f3;p=jalview.git diff --git a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java index 3c47ed1..dad8511 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,20 +20,24 @@ */ 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.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.ColorUtils; import jalview.util.Comparison; import java.awt.Color; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -47,43 +51,156 @@ import java.util.Map; public class ChimeraCommands { + public static final String NAMESPACE_PREFIX = "jv_"; + /** - * 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 ssm + * @param files + * @param sequence + * @param sr + * @param fr + * @param viewPanel + * @return */ public static StructureMappingcommandSet[] getColourBySequenceCommand( StructureSelectionManager ssm, String[] files, - SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr, - AlignmentI alignment) + SequenceI[][] sequence, SequenceRenderer sr, + AlignmentViewPanel viewPanel) + { + Map colourMap = buildColoursMap(ssm, files, + sequence, sr, viewPanel); + + List colourCommands = buildColourCommands(colourMap); + + StructureMappingcommandSet cs = new StructureMappingcommandSet( + ChimeraCommands.class, null, + colourCommands.toArray(new String[colourCommands.size()])); + + return new StructureMappingcommandSet[] { cs }; + } + + /** + * Traverse the map of colours/models/chains/positions to construct a list of + * 'color' commands (one per distinct colour used). The format of each command + * is + * + *
+   * 
+ * color colorname #modelnumber:range.chain + * e.g. color #00ff00 #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,... + *
+ *
+ * + * @param colourMap + * @return + */ + protected static List buildColourCommands( + Map colourMap) { - String defAttrPath = null; - FileOutputStream fos = null; - try + /* + * 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()) { - File outFile = File.createTempFile("jalviewdefattr", ".xml"); - outFile.deleteOnExit(); - defAttrPath = outFile.getPath(); - fos = new FileOutputStream(outFile); - fos.write("attribute: jalviewclr\n".getBytes()); - } catch (IOException e1) + Color colour = (Color) key; + String colourCode = ColorUtils.toTkCode(colour); + if (!firstColour) + { + sb.append("; "); + } + sb.append("color ").append(colourCode).append(" "); + firstColour = false; + final AtomSpecModel colourData = colourMap.get(colour); + sb.append(colourData.getAtomSpec()); + } + commands.add(sb.toString()); + return commands; + } + + /** + * 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()) { - e1.printStackTrace(); + 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; + } + } } - List cset = new ArrayList(); + return sb.toString(); + } + + /** + *
+   * Build a data structure which records contiguous subsequences for each colour. 
+   * From this we can easily generate the Chimera command for colour by sequence.
+   * Color
+   *     Model number
+   *         Chain
+   *             list of start/end ranges
+   * Ordering is by order of addition (for colours and positions), natural ordering (for models and chains)
+   * 
+ */ + protected static Map buildColoursMap( + StructureSelectionManager ssm, String[] files, + SequenceI[][] sequence, SequenceRenderer sr, + AlignmentViewPanel viewPanel) + { + FeatureRenderer fr = viewPanel.getFeatureRenderer(); + FeatureColourFinder finder = new FeatureColourFinder(fr); + AlignViewportI viewport = viewPanel.getAlignViewport(); + HiddenColumns cs = viewport.getAlignment().getHiddenColumns(); + AlignmentI al = viewport.getAlignment(); + Map colourMap = new LinkedHashMap(); + Color lastColour = null; - /* - * Map of { colour, positionSpecs} - */ - Map colranges = new LinkedHashMap(); - StringBuilder setAttributes = new StringBuilder(256); - String lastColour = "none"; - Color lastCol = null; for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) { - boolean startModel = true; StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]); if (mapping == null || mapping.length < 1) @@ -99,9 +216,9 @@ public class ChimeraCommands { final SequenceI seq = sequence[pdbfnum][s]; if (mapping[m].getSequence() == seq - && (sp = alignment.findIndex(seq)) > -1) + && (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 @@ -116,200 +233,314 @@ public class ChimeraCommands continue; } - Color col = getResidueColour(seq, r, sr, fr); + 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 = !col.equals(lastCol); + final boolean newColour = !colour.equals(lastColour); final boolean nonContig = lastPos + 1 != pos; - final boolean newChain = !mapping[m].getChain().equals(lastChain); - if (newColour || nonContig || startModel || newChain) + final boolean newChain = !chain.equals(lastChain); + if (newColour || nonContig || newChain) { - if (/* lastCol != null */startPos != -1) + if (startPos != -1) { - addColourRange(colranges, lastCol, pdbfnum, startPos, - lastPos, lastChain, startModel); - startModel = false; + addColourRange(colourMap, lastColour, pdbfnum, startPos, + lastPos, lastChain); } - // lastCol = null; startPos = pos; } - lastCol = col; + lastColour = colour; lastPos = pos; - // lastModel = pdbfnum; - lastChain = mapping[m].getChain(); + lastChain = chain; } // final colour range - if (lastCol != null) + if (lastColour != null) { - addColourRange(colranges, lastCol, pdbfnum, startPos, - lastPos, lastChain, false); + addColourRange(colourMap, lastColour, pdbfnum, startPos, + lastPos, lastChain); } - break; + // break; } } } } - try - { - lastColour = buildColourCommands(cset, colranges, - fos, setAttributes); - } catch (IOException e) - { - e.printStackTrace(); - } + return colourMap; + } - try + /** + * Helper method to add one contiguous colour range to the colour map. + * + * @param map + * @param key + * @param model + * @param startPos + * @param endPos + * @param chain + */ + protected static void addColourRange(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) { - fos.close(); - } catch (IOException e) + 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 + * @return + */ + public static StructureMappingcommandSet getSetAttributeCommandsForFeatures( + StructureSelectionManager ssm, String[] files, SequenceI[][] seqs, + AlignmentViewPanel viewPanel) + { + Map> featureMap = buildFeaturesMap( + ssm, files, seqs, viewPanel); + + List commands = buildSetAttributeCommands(featureMap); + + StructureMappingcommandSet cs = new StructureMappingcommandSet( + ChimeraCommands.class, null, + commands.toArray(new String[commands.size()])); + + return cs; + } + + /** + *
+   * 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) { - e.printStackTrace(); + return theMap; } - /* - * Send a rangeColor command, preceded by either defattr or setattr, - * whichever we end up preferring! - * - * rangecolor requires a minimum of two attribute values to operate on - */ - StringBuilder rangeColor = new StringBuilder(256); - rangeColor.append("rangecolor jalviewclr"); - int colourId = 0; - for (String colour : colranges.keySet()) + List visibleFeatures = fr.getDisplayedFeatureTypes(); + if (visibleFeatures.isEmpty()) { - colourId++; - rangeColor.append(" " + colourId + " " + colour); + return theMap; } - String rangeColorCommand = rangeColor.toString(); - if (rangeColorCommand.split(" ").length < 5) + + AlignmentI alignment = viewPanel.getAlignment(); + for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) { - rangeColorCommand += " max " + lastColour; + 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); + } + } + } } - final String defAttrCommand = "defattr " + defAttrPath - + " raiseTool false"; - final String setAttrCommand = setAttributes.toString(); - final String attrCommand = false ? defAttrCommand : setAttrCommand; - cset.add(new StructureMappingcommandSet(ChimeraCommands.class, null, - new String[] - { attrCommand /* , rangeColorCommand */})); - - return cset.toArray(new StructureMappingcommandSet[cset.size()]); + return theMap; } /** - * Get the residue colour at the given sequence position - as determined by - * the sequence group colour (if any), else the colour scheme, possibly - * overridden by a feature colour. + * 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 position - * @param sr - * @param fr - * @return + * @param theMap + * @param modelNumber */ - protected static Color getResidueColour(final SequenceI seq, - int position, SequenceRenderer sr, FeatureRenderer fr) + protected static void scanSequenceFeatures(List visibleFeatures, + StructureMapping mapping, SequenceI seq, + Map> theMap, int modelNumber) { - Color col = sr.getResidueBoxColour(seq, position); - - if (fr != null) + List sfs = seq.getFeatures().getPositionalFeatures( + visibleFeatures.toArray(new String[visibleFeatures.size()])); + for (SequenceFeature sf : sfs) { - col = fr.findFeatureColour(col, seq, position); + 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) + { + addColourRange(featureValues, value, modelNumber, range[0], + range[1], mapping.getChain()); + } + } } - return col; } /** - * Helper method to build the colour commands for one PDBfile. + * 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 cset - * the list of commands to be added to - * @param colranges - * the map of colours to residue positions already determined - * @param fos - * file to write 'defattr' commands to - * @param setAttributes - * @throws IOException + *

+   * 
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 + * @return */ - protected static String buildColourCommands( - List cset, - Map colranges, - FileOutputStream fos, StringBuilder setAttributes) - throws IOException + protected static List buildSetAttributeCommands( + Map> featureMap) { - int colourId = 0; - String lastColour = null; - for (String colour : colranges.keySet()) + List commands = new ArrayList(); + for (String featureType : featureMap.keySet()) { - lastColour = colour; - colourId++; + String attributeName = makeAttributeName(featureType); + /* - * Using color command directly is slow for larger structures. - * setAttributes.append("color #" + colour + " " + colranges.get(colour)+ - * ";"); + * clear down existing attributes for this feature */ - setAttributes.append("color " + colour + " " + colranges.get(colour) - + ";"); - final String atomSpec = new String(colranges.get(colour)); - // setAttributes.append("setattr r jalviewclr " + colourId + " " - // + atomSpec + ";"); - fos.write(("\t" + atomSpec + "\t" + colourId + "\n").getBytes()); + // '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(values.get(value).getAtomSpec()); + commands.add(sb.toString()); + } } - return lastColour; + + return commands; } /** - * Helper method to record a range of positions of the same colour. + * 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 colranges - * @param colour - * @param model - * @param startPos - * @param endPos - * @param chain - * @param changeModel + * @param featureType + * @return + * + *
+   * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html
+   *         
*/ - private static void addColourRange(Map colranges, - Color colour, int model, int startPos, int endPos, String chain, - boolean startModel) + protected static String makeAttributeName(String featureType) { - String colstring = "#" + ((colour.getRed() < 16) ? "0" : "") - + Integer.toHexString(colour.getRed()) - + ((colour.getGreen()< 16) ? "0":"")+Integer.toHexString(colour.getGreen()) - + ((colour.getBlue()< 16) ? "0":"")+Integer.toHexString(colour.getBlue()); - StringBuilder currange = colranges.get(colstring); - if (currange == null) + StringBuilder sb = new StringBuilder(); + if (featureType != null) { - colranges.put(colstring, currange = new StringBuilder(256)); + for (char c : featureType.toCharArray()) + { + sb.append(Character.isLetterOrDigit(c) ? c : '_'); + } } + String attName = NAMESPACE_PREFIX + sb.toString(); + /* - * Format as (e.g.) #0:1-3.A,5.A,7-10.A,...#1:1-4.B,..etc + * Chimera treats an attribute name ending in 'color' as colour-valued; + * Jalview doesn't, so prevent this by appending an underscore */ - // if (currange.length() > 0) - // { - // currange.append("|"); - // } - // currange.append("#" + model + ":" + ((startPos==endPos) ? startPos : - // startPos + "-" - // + endPos) + "." + chain); - if (currange.length() == 0) + if (attName.toUpperCase().endsWith("COLOR")) { - currange.append("#" + model + ":"); + attName += "_"; } - else if (startModel) - { - currange.append(",#" + model + ":"); - } - else - { - currange.append(","); - } - final String rangeSpec = (startPos == endPos) ? Integer - .toString(startPos) : (startPos + "-" + endPos); - currange.append(rangeSpec + "." + chain); + + return attName; } }