JAL-2295 feature and colour commands refactored to use AtomSpecModel
[jalview.git] / src / jalview / ext / rbvi / chimera / ChimeraCommands.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.ext.rbvi.chimera;
22
23 import jalview.api.FeatureRenderer;
24 import jalview.api.SequenceRenderer;
25 import jalview.datamodel.AlignmentI;
26 import jalview.datamodel.SequenceFeature;
27 import jalview.datamodel.SequenceI;
28 import jalview.structure.StructureMapping;
29 import jalview.structure.StructureMappingcommandSet;
30 import jalview.structure.StructureSelectionManager;
31 import jalview.util.ColorUtils;
32 import jalview.util.Comparison;
33
34 import java.awt.Color;
35 import java.util.ArrayList;
36 import java.util.LinkedHashMap;
37 import java.util.List;
38 import java.util.Map;
39
40 /**
41  * Routines for generating Chimera commands for Jalview/Chimera binding
42  * 
43  * @author JimP
44  * 
45  */
46 public class ChimeraCommands
47 {
48
49   /**
50    * utility to construct the commands to colour chains by the given alignment
51    * for passing to Chimera
52    * 
53    * @returns Object[] { Object[] { <model being coloured>,
54    * 
55    */
56   public static StructureMappingcommandSet getColourBySequenceCommand(
57           StructureSelectionManager ssm, String[] files,
58           SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr,
59           AlignmentI alignment)
60   {
61     Map<Object, AtomSpecModel> colourMap = buildColoursMap(
62             ssm, files, sequence, sr, fr, alignment);
63
64     List<String> colourCommands = buildColourCommands(colourMap);
65
66     StructureMappingcommandSet cs = new StructureMappingcommandSet(
67             ChimeraCommands.class, null,
68             colourCommands.toArray(new String[colourCommands.size()]));
69
70     return cs;
71   }
72
73   /**
74    * Traverse the map of colours/models/chains/positions to construct a list of
75    * 'color' commands (one per distinct colour used). The format of each command
76    * is
77    * 
78    * <pre>
79    * <blockquote> 
80    * color colorname #modelnumber:range.chain 
81    * e.g. color #00ff00 #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
82    * </blockquote>
83    * </pre>
84    * 
85    * @param colourMap
86    * @return
87    */
88   protected static List<String> buildColourCommands(
89           Map<Object, AtomSpecModel> colourMap)
90   {
91     /*
92      * This version concatenates all commands into a single String (semi-colon
93      * delimited). If length limit issues arise, refactor to return one color
94      * command per colour.
95      */
96     List<String> commands = new ArrayList<String>();
97     StringBuilder sb = new StringBuilder(256);
98     boolean firstColour = true;
99     for (Object key : colourMap.keySet())
100     {
101       Color colour = (Color) key;
102       String colourCode = ColorUtils.toTkCode(colour);
103       if (!firstColour)
104       {
105         sb.append("; ");
106       }
107       sb.append("color ").append(colourCode).append(" ");
108       firstColour = false;
109       final AtomSpecModel colourData = colourMap
110               .get(colour);
111       sb.append(colourData.getAtomSpec());
112     }
113     commands.add(sb.toString());
114     return commands;
115   }
116
117   /**
118    * Traverses a map of { modelNumber, {chain, {list of from-to ranges} } } and
119    * builds a Chimera format atom spec
120    * 
121    * @param modelAndChainRanges
122    */
123   protected static String getAtomSpec(
124           Map<Integer, Map<String, List<int[]>>> modelAndChainRanges)
125   {
126     StringBuilder sb = new StringBuilder(128);
127     boolean firstModelForColour = true;
128     for (Integer model : modelAndChainRanges.keySet())
129     {
130       boolean firstPositionForModel = true;
131       if (!firstModelForColour)
132       {
133         sb.append("|");
134       }
135       firstModelForColour = false;
136       sb.append("#").append(model).append(":");
137
138       final Map<String, List<int[]>> modelData = modelAndChainRanges
139               .get(model);
140       for (String chain : modelData.keySet())
141       {
142         boolean hasChain = !"".equals(chain.trim());
143         for (int[] range : modelData.get(chain))
144         {
145           if (!firstPositionForModel)
146           {
147             sb.append(",");
148           }
149           if (range[0] == range[1])
150           {
151             sb.append(range[0]);
152           }
153           else
154           {
155             sb.append(range[0]).append("-").append(range[1]);
156           }
157           if (hasChain)
158           {
159             sb.append(".").append(chain);
160           }
161           firstPositionForModel = false;
162         }
163       }
164     }
165     return sb.toString();
166   }
167
168   /**
169    * <pre>
170    * Build a data structure which maps contiguous subsequences for each colour. 
171    * This generates a data structure from which we can easily generate the 
172    * Chimera command for colour by sequence.
173    * Color
174    *     Model number
175    *         Chain
176    *             list of start/end ranges
177    * Ordering is by order of addition (for colours and positions), natural ordering (for models and chains)
178    * </pre>
179    */
180   protected static Map<Object, AtomSpecModel> buildColoursMap(
181           StructureSelectionManager ssm, String[] files,
182           SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr,
183           AlignmentI alignment)
184   {
185     Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<Object, AtomSpecModel>();
186     Color lastColour = null;
187     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
188     {
189       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
190
191       if (mapping == null || mapping.length < 1)
192       {
193         continue;
194       }
195
196       int startPos = -1, lastPos = -1;
197       String lastChain = "";
198       for (int s = 0; s < sequence[pdbfnum].length; s++)
199       {
200         for (int sp, m = 0; m < mapping.length; m++)
201         {
202           final SequenceI seq = sequence[pdbfnum][s];
203           if (mapping[m].getSequence() == seq
204                   && (sp = alignment.findIndex(seq)) > -1)
205           {
206             SequenceI asp = alignment.getSequenceAt(sp);
207             for (int r = 0; r < asp.getLength(); r++)
208             {
209               // no mapping to gaps in sequence
210               if (Comparison.isGap(asp.getCharAt(r)))
211               {
212                 continue;
213               }
214               int pos = mapping[m].getPDBResNum(asp.findPosition(r));
215
216               if (pos < 1 || pos == lastPos)
217               {
218                 continue;
219               }
220
221               Color colour = sr.getResidueColour(seq, r, fr);
222               final String chain = mapping[m].getChain();
223
224               /*
225                * Just keep incrementing the end position for this colour range
226                * _unless_ colour, PDB model or chain has changed, or there is a
227                * gap in the mapped residue sequence
228                */
229               final boolean newColour = !colour.equals(lastColour);
230               final boolean nonContig = lastPos + 1 != pos;
231               final boolean newChain = !chain.equals(lastChain);
232               if (newColour || nonContig || newChain)
233               {
234                 if (startPos != -1)
235                 {
236                   addRange(colourMap, lastColour, pdbfnum, startPos,
237                           lastPos, lastChain);
238                 }
239                 startPos = pos;
240               }
241               lastColour = colour;
242               lastPos = pos;
243               lastChain = chain;
244             }
245             // final colour range
246             if (lastColour != null)
247             {
248               addRange(colourMap, lastColour, pdbfnum, startPos,
249                       lastPos, lastChain);
250             }
251             // break;
252           }
253         }
254       }
255     }
256     return colourMap;
257   }
258
259   /**
260    * Helper method to add one contiguous colour range to the colour map.
261    * 
262    * @param map
263    * @param key
264    * @param model
265    * @param startPos
266    * @param endPos
267    * @param chain
268    */
269   protected static void addRange(Map<Object, AtomSpecModel> map,
270           Object key, int model, int startPos, int endPos, String chain)
271   {
272     /*
273      * Get/initialize map of data for the colour
274      */
275     AtomSpecModel atomSpec = map.get(key);
276     if (atomSpec == null)
277     {
278       atomSpec = new AtomSpecModel();
279       map.put(key, atomSpec);
280     }
281
282     atomSpec.addRange(model, startPos, endPos, chain);
283   }
284
285   /**
286    * Constructs and returns a set of Chimera commands to set attributes on
287    * residues corresponding to features in Jalview
288    * 
289    * @param ssm
290    * @param files
291    * @param seqs
292    * @param fr
293    * @param alignment
294    * @return
295    */
296   public static StructureMappingcommandSet getSetAttributeCommandsForFeatures(
297           StructureSelectionManager ssm, String[] files,
298           SequenceI[][] seqs, FeatureRenderer fr, AlignmentI alignment)
299   {
300     Map<String, AtomSpecModel> featureMap = buildFeaturesMap(
301             ssm, files, seqs, fr, alignment);
302
303     List<String> commands = buildSetAttributeCommands(featureMap);
304
305     StructureMappingcommandSet cs = new StructureMappingcommandSet(
306             ChimeraCommands.class, null,
307             commands.toArray(new String[commands.size()]));
308
309     return cs;
310   }
311
312   /**
313    * Helper method to build a map of { featureType, AtomSpecModel }
314    * 
315    * @param ssm
316    * @param files
317    * @param seqs
318    * @param fr
319    * @param alignment
320    * @return
321    */
322   protected static Map<String, AtomSpecModel> buildFeaturesMap(
323           StructureSelectionManager ssm, String[] files,
324           SequenceI[][] seqs, FeatureRenderer fr, AlignmentI alignment)
325   {
326     Map<String, AtomSpecModel> theMap = new LinkedHashMap<String, AtomSpecModel>();
327
328     List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
329     if (visibleFeatures.isEmpty())
330     {
331       return theMap;
332     }
333     
334     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
335     {
336       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
337
338       if (mapping == null || mapping.length < 1)
339       {
340         continue;
341       }
342
343       for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
344       {
345         for (int m = 0; m < mapping.length; m++)
346         {
347           final SequenceI seq = seqs[pdbfnum][seqNo];
348           int sp = alignment.findIndex(seq);
349           if (mapping[m].getSequence() == seq && sp > -1)
350           {
351             /*
352              * found a sequence with a mapping to a structure;
353              * now scan its features
354              */
355             SequenceI asp = alignment.getSequenceAt(sp);
356
357             scanSequenceFeatures(visibleFeatures, mapping[m], asp, theMap,
358                     pdbfnum);
359           }
360         }
361       }
362     }
363     return theMap;
364   }
365
366   /**
367    * Inspect features on the sequence; for each feature that is visible,
368    * determine its mapped ranges in the structure (if any) according to the
369    * given mapping, and add them to the map
370    * 
371    * @param visibleFeatures
372    * @param mapping
373    * @param seq
374    * @param theMap
375    * @param modelNumber
376    */
377   protected static void scanSequenceFeatures(List<String> visibleFeatures,
378           StructureMapping mapping, SequenceI seq,
379           Map<String, AtomSpecModel> theMap, int modelNumber)
380   {
381     SequenceFeature[] sfs = seq.getSequenceFeatures();
382     if (sfs == null)
383     {
384       return;
385     }
386
387     for (SequenceFeature sf : sfs)
388     {
389       String type = sf.getType();
390       if (!visibleFeatures.contains(type))
391       {
392         continue;
393       }
394       List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
395               sf.getEnd());
396
397       if (!mappedRanges.isEmpty())
398       {
399         AtomSpecModel atomSpec = theMap.get(type);
400         if (atomSpec == null)
401         {
402           atomSpec = new AtomSpecModel();
403           theMap.put(type, atomSpec);
404         }
405         for (int[] range : mappedRanges)
406         {
407           atomSpec.addRange(modelNumber, range[0], range[1],
408                   mapping.getChain());
409         }
410       }
411     }
412   }
413
414   /**
415    * Traverse the map of features/models/chains/positions to construct a list of
416    * 'setattr' commands (one per feature type). The format of each command is
417    * 
418    * <pre>
419    * <blockquote> setattr r <featureName> " " #modelnumber:range.chain 
420    * e.g. setattr r jv:chain " " #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
421    * </blockquote>
422    * </pre>
423    * <p>
424    * Note we are not (currently) setting attribute values, only the type
425    * (presence) of each attribute. This is to avoid overloading the Chimera REST
426    * interface by sending too many distinct commands. Analysis by feature values
427    * may still be performed in Jalview, on selections created in Chimera.
428    * 
429    * @param featureMap
430    * @return
431    */
432   protected static List<String> buildSetAttributeCommands(
433           Map<String, AtomSpecModel> featureMap)
434   {
435     List<String> commands = new ArrayList<String>();
436     for (String featureType : featureMap.keySet())
437     {
438       StringBuilder sb = new StringBuilder(128);
439       String sanitised = featureType.replace(" ", "_").replace("-", "_");
440       sb.append("setattr r jv_").append(sanitised).append(" \" \" ");
441       sb.append(featureMap.get(featureType).getAtomSpec());
442       commands.add(sb.toString());
443     }
444
445     return commands;
446   }
447
448 }