JAL-2295 extracted AtomSpecModel for reuse in Chimera commands
[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.HashMap;
37 import java.util.LinkedHashMap;
38 import java.util.List;
39 import java.util.Map;
40
41 /**
42  * Routines for generating Chimera commands for Jalview/Chimera binding
43  * 
44  * @author JimP
45  * 
46  */
47 public class ChimeraCommands
48 {
49
50   /**
51    * utility to construct the commands to colour chains by the given alignment
52    * for passing to Chimera
53    * 
54    * @returns Object[] { Object[] { <model being coloured>,
55    * 
56    */
57   public static StructureMappingcommandSet getColourBySequenceCommand(
58           StructureSelectionManager ssm, String[] files,
59           SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr,
60           AlignmentI alignment)
61   {
62     Map<Color, AtomSpecModel> colourMap = buildColoursMap(
63             ssm, files, sequence, sr, fr, alignment);
64
65     List<String> colourCommands = buildColourCommands(colourMap);
66
67     StructureMappingcommandSet cs = new StructureMappingcommandSet(
68             ChimeraCommands.class, null,
69             colourCommands.toArray(new String[colourCommands.size()]));
70
71     return cs;
72   }
73
74   /**
75    * Traverse the map of colours/models/chains/positions to construct a list of
76    * 'color' commands (one per distinct colour used). The format of each command
77    * is
78    * 
79    * <pre>
80    * <blockquote> 
81    * color colorname #modelnumber:range.chain 
82    * e.g. color #00ff00 #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
83    * </blockquote>
84    * </pre>
85    * 
86    * @param colourMap
87    * @return
88    */
89   protected static List<String> buildColourCommands(
90           Map<Color, AtomSpecModel> colourMap)
91   {
92     /*
93      * This version concatenates all commands into a single String (semi-colon
94      * delimited). If length limit issues arise, refactor to return one color
95      * command per colour.
96      */
97     List<String> commands = new ArrayList<String>();
98     StringBuilder sb = new StringBuilder(256);
99     boolean firstColour = true;
100     for (Color colour : colourMap.keySet())
101     {
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<Color, AtomSpecModel> buildColoursMap(
181           StructureSelectionManager ssm, String[] files,
182           SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr,
183           AlignmentI alignment)
184   {
185     Map<Color, AtomSpecModel> colourMap = new LinkedHashMap<Color, 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                   addColourRange(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               addColourRange(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 colourMap
263    * @param colour
264    * @param model
265    * @param startPos
266    * @param endPos
267    * @param chain
268    */
269   protected static void addColourRange(
270 Map<Color, AtomSpecModel> colourMap,
271           Color colour, int model, int startPos, int endPos, String chain)
272   {
273     // refactor for reuse as addRange
274     /*
275      * Get/initialize map of data for the colour
276      */
277     AtomSpecModel colourData = colourMap.get(colour);
278     if (colourData == null)
279     {
280       colourData = new AtomSpecModel();
281       colourMap.put(colour, colourData);
282     }
283
284     colourData.addRange(model, startPos, endPos, chain);
285   }
286
287   /**
288    * Constructs and returns a set of Chimera commands to set attributes on
289    * residues corresponding to features in Jalview.
290    * 
291    * @param ssm
292    * @param files
293    * @param seqs
294    * @param fr
295    * @param alignment
296    * @return
297    */
298   public static StructureMappingcommandSet getSetAttributeCommandsForFeatures(
299           StructureSelectionManager ssm, String[] files,
300           SequenceI[][] seqs, FeatureRenderer fr, AlignmentI alignment)
301   {
302     Map<String, Map<Integer, Map<String, List<int[]>>>> featureMap = buildFeaturesMap(
303             ssm, files, seqs, fr, alignment);
304
305     List<String> colourCommands = buildSetAttributeCommands(featureMap);
306
307     StructureMappingcommandSet cs = new StructureMappingcommandSet(
308             ChimeraCommands.class, null,
309             colourCommands.toArray(new String[colourCommands.size()]));
310
311     return cs;
312   }
313
314   /**
315    * <pre>
316    * Helper method to build a map of 
317    * { featureType, {modelNumber, {chain, {list of from-to ranges} } } }
318    * </pre>
319    * 
320    * @param ssm
321    * @param files
322    * @param seqs
323    * @param fr
324    * @param alignment
325    * @return
326    */
327   protected static Map<String, Map<Integer, Map<String, List<int[]>>>> buildFeaturesMap(
328           StructureSelectionManager ssm, String[] files,
329           SequenceI[][] seqs, FeatureRenderer fr, AlignmentI alignment)
330   {
331     Map<String, Map<Integer, Map<String, List<int[]>>>> theMap = new HashMap<String, Map<Integer, Map<String, List<int[]>>>>();
332
333     List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
334     if (visibleFeatures.isEmpty())
335     {
336       return theMap;
337     }
338     
339     /*
340      * traverse mappings to structures 
341      */
342     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
343     {
344       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
345
346       if (mapping == null || mapping.length < 1)
347       {
348         continue;
349       }
350
351       int lastPos = -1;
352       for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
353       {
354         for (int m = 0; m < mapping.length; m++)
355         {
356           final SequenceI seq = seqs[pdbfnum][seqNo];
357           int sp = alignment.findIndex(seq);
358           if (mapping[m].getSequence() == seq && sp > -1)
359           {
360             SequenceI asp = alignment.getSequenceAt(sp);
361
362             /*
363              * traverse each sequence for its mapped positions
364              */
365             for (int r = 0; r < asp.getLength(); r++)
366             {
367               // no mapping to gaps in sequence
368               if (Comparison.isGap(asp.getCharAt(r)))
369               {
370                 continue;
371               }
372               int residuePos = asp.findPosition(r);
373               int pos = mapping[m].getPDBResNum(residuePos);
374
375               if (pos < 1 || pos == lastPos)
376               {
377                 continue;
378               }
379               final String chain = mapping[m].getChain();
380
381               /*
382                * record any features at this position, with the model, chain
383                * and residue number they map to
384                */
385               List<SequenceFeature> features = fr.findFeaturesAtRes(asp,
386                       residuePos);
387               for (SequenceFeature feature : features)
388               {
389                 if (!visibleFeatures.contains(feature))
390                 {
391                   continue;
392                 }
393               }
394             }
395           }
396         }
397       }
398       }
399     return theMap;
400   }
401
402   /**
403    * Traverse the map of features/models/chains/positions to construct a list of
404    * 'setattr' commands (one per feature type). The format of each command is
405    * 
406    * <pre>
407    * <blockquote> setattr r <featureName> " " #modelnumber:range.chain 
408    * e.g. setattr r jv:chain " " #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
409    * </blockquote>
410    * </pre>
411    * <p>
412    * Note we are not (currently) setting attribute values, only the type
413    * (presence) of each attribute. This is to avoid overloading the Chimera REST
414    * interface by sending too many distinct commands. Analysis by feature values
415    * may still be performed in Jalview, on selections created in Chimera.
416    * 
417    * @param featureMap
418    * @return
419    * @see http 
420    *      ://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec
421    *      .html
422    */
423   protected static List<String> buildSetAttributeCommands(
424           Map<String, Map<Integer, Map<String, List<int[]>>>> featureMap)
425   {
426     List<String> commands = new ArrayList<String>();
427     for (String featureType : featureMap.keySet())
428     {
429       StringBuilder sb = new StringBuilder(128);
430       featureType = featureType.replace(" ", "_");
431       sb.append("setattr r jv:").append(featureType).append(" \" \" ");
432       final Map<Integer, Map<String, List<int[]>>> featureData = featureMap
433               .get(featureType);
434       sb.append(getAtomSpec(featureData));
435       commands.add(sb.toString());
436     }
437
438     return commands;
439   }
440
441 }