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