JAL-2295 set attributes commands include feature value, sent via file
[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 import MCview.PDBChain;
42
43 /**
44  * Routines for generating Chimera commands for Jalview/Chimera binding
45  * 
46  * @author JimP
47  * 
48  */
49 public class ChimeraCommands
50 {
51
52   /**
53    * utility to construct the commands to colour chains by the given alignment
54    * for passing to Chimera
55    * 
56    * @returns Object[] { Object[] { <model being coloured>,
57    * 
58    */
59   public static StructureMappingcommandSet getColourBySequenceCommand(
60           StructureSelectionManager ssm, String[] files,
61           SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr,
62           AlignmentI alignment)
63   {
64     Map<Object, AtomSpecModel> colourMap = buildColoursMap(
65             ssm, files, sequence, sr, fr, alignment);
66
67     List<String> colourCommands = buildColourCommands(colourMap);
68
69     StructureMappingcommandSet cs = new StructureMappingcommandSet(
70             ChimeraCommands.class, null,
71             colourCommands.toArray(new String[colourCommands.size()]));
72
73     return cs;
74   }
75
76   /**
77    * Traverse the map of colours/models/chains/positions to construct a list of
78    * 'color' commands (one per distinct colour used). The format of each command
79    * is
80    * 
81    * <pre>
82    * <blockquote> 
83    * color colorname #modelnumber:range.chain 
84    * e.g. color #00ff00 #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
85    * </blockquote>
86    * </pre>
87    * 
88    * @param colourMap
89    * @return
90    */
91   protected static List<String> buildColourCommands(
92           Map<Object, AtomSpecModel> colourMap)
93   {
94     /*
95      * This version concatenates all commands into a single String (semi-colon
96      * delimited). If length limit issues arise, refactor to return one color
97      * command per colour.
98      */
99     List<String> commands = new ArrayList<String>();
100     StringBuilder sb = new StringBuilder(256);
101     boolean firstColour = true;
102     for (Object key : colourMap.keySet())
103     {
104       Color colour = (Color) key;
105       String colourCode = ColorUtils.toTkCode(colour);
106       if (!firstColour)
107       {
108         sb.append("; ");
109       }
110       sb.append("color ").append(colourCode).append(" ");
111       firstColour = false;
112       final AtomSpecModel colourData = colourMap
113               .get(colour);
114       sb.append(colourData.getAtomSpec());
115     }
116     commands.add(sb.toString());
117     return commands;
118   }
119
120   /**
121    * Traverses a map of { modelNumber, {chain, {list of from-to ranges} } } and
122    * builds a Chimera format atom spec
123    * 
124    * @param modelAndChainRanges
125    */
126   protected static String getAtomSpec(
127           Map<Integer, Map<String, List<int[]>>> modelAndChainRanges)
128   {
129     StringBuilder sb = new StringBuilder(128);
130     boolean firstModelForColour = true;
131     for (Integer model : modelAndChainRanges.keySet())
132     {
133       boolean firstPositionForModel = true;
134       if (!firstModelForColour)
135       {
136         sb.append("|");
137       }
138       firstModelForColour = false;
139       sb.append("#").append(model).append(":");
140
141       final Map<String, List<int[]>> modelData = modelAndChainRanges
142               .get(model);
143       for (String chain : modelData.keySet())
144       {
145         boolean hasChain = !"".equals(chain.trim());
146         for (int[] range : modelData.get(chain))
147         {
148           if (!firstPositionForModel)
149           {
150             sb.append(",");
151           }
152           if (range[0] == range[1])
153           {
154             sb.append(range[0]);
155           }
156           else
157           {
158             sb.append(range[0]).append("-").append(range[1]);
159           }
160           if (hasChain)
161           {
162             sb.append(".").append(chain);
163           }
164           firstPositionForModel = false;
165         }
166       }
167     }
168     return sb.toString();
169   }
170
171   /**
172    * <pre>
173    * Build a data structure which maps contiguous subsequences for each colour. 
174    * This generates a data structure from which we can easily generate the 
175    * Chimera command for colour by sequence.
176    * Color
177    *     Model number
178    *         Chain
179    *             list of start/end ranges
180    * Ordering is by order of addition (for colours and positions), natural ordering (for models and chains)
181    * </pre>
182    */
183   protected static Map<Object, AtomSpecModel> buildColoursMap(
184           StructureSelectionManager ssm, String[] files,
185           SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr,
186           AlignmentI alignment)
187   {
188     Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<Object, AtomSpecModel>();
189     Color lastColour = null;
190     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
191     {
192       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
193
194       if (mapping == null || mapping.length < 1)
195       {
196         continue;
197       }
198
199       int startPos = -1, lastPos = -1;
200       String lastChain = "";
201       for (int s = 0; s < sequence[pdbfnum].length; s++)
202       {
203         for (int sp, m = 0; m < mapping.length; m++)
204         {
205           final SequenceI seq = sequence[pdbfnum][s];
206           if (mapping[m].getSequence() == seq
207                   && (sp = alignment.findIndex(seq)) > -1)
208           {
209             SequenceI asp = alignment.getSequenceAt(sp);
210             for (int r = 0; r < asp.getLength(); r++)
211             {
212               // no mapping to gaps in sequence
213               if (Comparison.isGap(asp.getCharAt(r)))
214               {
215                 continue;
216               }
217               int pos = mapping[m].getPDBResNum(asp.findPosition(r));
218
219               if (pos < 1 || pos == lastPos)
220               {
221                 continue;
222               }
223
224               Color colour = sr.getResidueColour(seq, r, fr);
225               final String chain = mapping[m].getChain();
226
227               /*
228                * Just keep incrementing the end position for this colour range
229                * _unless_ colour, PDB model or chain has changed, or there is a
230                * gap in the mapped residue sequence
231                */
232               final boolean newColour = !colour.equals(lastColour);
233               final boolean nonContig = lastPos + 1 != pos;
234               final boolean newChain = !chain.equals(lastChain);
235               if (newColour || nonContig || newChain)
236               {
237                 if (startPos != -1)
238                 {
239                   addRange(colourMap, lastColour, pdbfnum, startPos,
240                           lastPos, lastChain);
241                 }
242                 startPos = pos;
243               }
244               lastColour = colour;
245               lastPos = pos;
246               lastChain = chain;
247             }
248             // final colour range
249             if (lastColour != null)
250             {
251               addRange(colourMap, lastColour, pdbfnum, startPos,
252                       lastPos, lastChain);
253             }
254             // break;
255           }
256         }
257       }
258     }
259     return colourMap;
260   }
261
262   /**
263    * Helper method to add one contiguous colour range to the colour map.
264    * 
265    * @param map
266    * @param key
267    * @param model
268    * @param startPos
269    * @param endPos
270    * @param chain
271    */
272   protected static void addRange(Map<Object, AtomSpecModel> map,
273           Object key, int model, int startPos, int endPos, String chain)
274   {
275     /*
276      * Get/initialize map of data for the colour
277      */
278     AtomSpecModel atomSpec = map.get(key);
279     if (atomSpec == null)
280     {
281       atomSpec = new AtomSpecModel();
282       map.put(key, atomSpec);
283     }
284
285     atomSpec.addRange(model, startPos, endPos, chain);
286   }
287
288   /**
289    * Constructs and returns a set of Chimera commands to set attributes on
290    * residues corresponding to features in Jalview
291    * 
292    * @param ssm
293    * @param files
294    * @param seqs
295    * @param fr
296    * @param alignment
297    * @return
298    */
299   public static StructureMappingcommandSet getSetAttributeCommandsForFeatures(
300           StructureSelectionManager ssm, String[] files,
301           SequenceI[][] seqs, FeatureRenderer fr, AlignmentI alignment)
302   {
303     Map<String, Map<Object, AtomSpecModel>> featureMap = buildFeaturesMap(
304             ssm, files, seqs, fr, alignment);
305
306     List<String> commands = buildSetAttributeCommands(featureMap);
307
308     StructureMappingcommandSet cs = new StructureMappingcommandSet(
309             ChimeraCommands.class, null,
310             commands.toArray(new String[commands.size()]));
311
312     return cs;
313   }
314
315   /**
316    * <pre>
317    * Helper method to build a map of 
318    *   { featureType, { feature value, AtomSpecModel } }
319    * </pre>
320    * 
321    * @param ssm
322    * @param files
323    * @param seqs
324    * @param fr
325    * @param alignment
326    * @return
327    */
328   protected static Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
329           StructureSelectionManager ssm, String[] files,
330           SequenceI[][] seqs, FeatureRenderer fr, AlignmentI alignment)
331   {
332     Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<String, Map<Object, AtomSpecModel>>();
333
334     List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
335     if (visibleFeatures.isEmpty())
336     {
337       return theMap;
338     }
339     
340     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
341     {
342       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
343
344       if (mapping == null || mapping.length < 1)
345       {
346         continue;
347       }
348
349       for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
350       {
351         for (int m = 0; m < mapping.length; m++)
352         {
353           final SequenceI seq = seqs[pdbfnum][seqNo];
354           int sp = alignment.findIndex(seq);
355           if (mapping[m].getSequence() == seq && sp > -1)
356           {
357             /*
358              * found a sequence with a mapping to a structure;
359              * now scan its features
360              */
361             SequenceI asp = alignment.getSequenceAt(sp);
362
363             scanSequenceFeatures(visibleFeatures, mapping[m], asp, theMap,
364                     pdbfnum);
365           }
366         }
367       }
368     }
369     return theMap;
370   }
371
372   /**
373    * Inspect features on the sequence; for each feature that is visible,
374    * determine its mapped ranges in the structure (if any) according to the
375    * given mapping, and add them to the map
376    * 
377    * @param visibleFeatures
378    * @param mapping
379    * @param seq
380    * @param theMap
381    * @param modelNumber
382    */
383   protected static void scanSequenceFeatures(List<String> visibleFeatures,
384           StructureMapping mapping, SequenceI seq,
385           Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
386   {
387     SequenceFeature[] sfs = seq.getSequenceFeatures();
388     if (sfs == null)
389     {
390       return;
391     }
392
393     for (SequenceFeature sf : sfs)
394     {
395       String type = sf.getType();
396       if (!visibleFeatures.contains(type) || suppressFeature(type))
397       {
398         continue;
399       }
400       List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
401               sf.getEnd());
402
403       if (!mappedRanges.isEmpty())
404       {
405         String value = sf.getDescription();
406         if (value == null)
407         {
408           value = type;
409         }
410         float score = sf.getScore();
411         if (score != 0f && score != Float.NaN)
412         {
413           value = Float.toString(score);
414         }
415         Map<Object, AtomSpecModel> featureValues = theMap.get(type);
416         if (featureValues == null)
417         {
418           featureValues = new HashMap<Object, AtomSpecModel>();
419           theMap.put(type, featureValues);
420         }
421         for (int[] range : mappedRanges)
422         {
423           addRange(featureValues, value, modelNumber, range[0], range[1],
424                   mapping.getChain());
425         }
426       }
427     }
428   }
429
430   /**
431    * Answers true if the feature type is one we don't wish to propagate to
432    * Chimera - for now, RESNUM
433    * 
434    * @param type
435    * @return
436    */
437   static boolean suppressFeature(String type)
438   {
439     return PDBChain.RESNUM_FEATURE.equals(type);
440   }
441
442   /**
443    * Traverse the map of features/values/models/chains/positions to construct a
444    * list of 'setattr' commands (one per distinct feature type and value).
445    * <p>
446    * The format of each command is
447    * 
448    * <pre>
449    * <blockquote> setattr r <featureName> " " #modelnumber:range.chain 
450    * e.g. setattr r jv:chain <value> #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
451    * </blockquote>
452    * </pre>
453    * 
454    * @param featureMap
455    * @return
456    */
457   protected static List<String> buildSetAttributeCommands(
458           Map<String, Map<Object, AtomSpecModel>> featureMap)
459   {
460     List<String> commands = new ArrayList<String>();
461     for (String featureType : featureMap.keySet())
462     {
463       String attributeName = "jv_"
464               + featureType.replace(" ", "_").replace("-", "_");
465
466       /*
467        * clear down existing attributes for this feature
468        */
469       // 'problem' - sets attribute to None on all residues - overkill?
470       // commands.add("~setattr r " + attributeName + " :*");
471
472       Map<Object, AtomSpecModel> values = featureMap.get(featureType);
473       for (Object value : values.keySet())
474       {
475         /*
476          * for each distinct value recorded for this feature type,
477          * add a command to set the attribute on the mapped residues
478          */
479         StringBuilder sb = new StringBuilder(128);
480         sb.append("setattr r ").append(attributeName).append(" \"")
481                 .append(value.toString()).append("\" ");
482         sb.append(values.get(value).getAtomSpec());
483         commands.add(sb.toString());
484       }
485     }
486
487     return commands;
488   }
489
490 }