JAL-3390 StructureCommands atomspec helper, corrected Jmol atomspec/test
[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.AlignViewportI;
24 import jalview.api.AlignmentViewPanel;
25 import jalview.api.FeatureRenderer;
26 import jalview.datamodel.AlignmentI;
27 import jalview.datamodel.MappedFeatures;
28 import jalview.datamodel.SequenceFeature;
29 import jalview.datamodel.SequenceI;
30 import jalview.gui.Desktop;
31 import jalview.structure.StructureMapping;
32 import jalview.structure.StructureMappingcommandSet;
33 import jalview.structure.StructureSelectionManager;
34 import jalview.structures.models.AAStructureBindingModel;
35 import jalview.util.ColorUtils;
36
37 import java.awt.Color;
38 import java.util.ArrayList;
39 import java.util.HashMap;
40 import java.util.LinkedHashMap;
41 import java.util.List;
42 import java.util.Map;
43
44 /**
45  * Routines for generating Chimera commands for Jalview/Chimera binding
46  * 
47  * @author JimP
48  * 
49  */
50 public class ChimeraCommands extends StructureCommands
51 {
52   public static final String NAMESPACE_PREFIX = "jv_";
53
54   /*
55    * colour for residues shown in structure but hidden in alignment
56    */
57   private static final String COLOR_GRAY_HEX = "color "
58           + ColorUtils.toTkCode(Color.GRAY);
59
60   /**
61    * Constructs Chimera commands to colour residues as per the Jalview alignment
62    * 
63    * @param colourMap
64    * @param binding
65    * @return
66    */
67   public static String[] getColourBySequenceCommand(
68           Map<Object, AtomSpecModel> colourMap,
69           AAStructureBindingModel binding)
70   {
71     List<String> colourCommands = buildColourCommands(colourMap, binding);
72
73     return colourCommands.toArray(new String[colourCommands.size()]);
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    * @param binding
90    * @return
91    */
92   protected static List<String> buildColourCommands(
93           Map<Object, AtomSpecModel> colourMap,
94           AAStructureBindingModel binding)
95   {
96     /*
97      * This version concatenates all commands into a single String (semi-colon
98      * delimited). If length limit issues arise, refactor to return one color
99      * command per colour.
100      */
101     List<String> commands = new ArrayList<>();
102     StringBuilder sb = new StringBuilder(256);
103     sb.append(COLOR_GRAY_HEX);
104
105     for (Object key : colourMap.keySet())
106     {
107       Color colour = (Color) key;
108       String colourCode = ColorUtils.toTkCode(colour);
109       sb.append("; ");
110       sb.append("color ").append(colourCode).append(" ");
111       final AtomSpecModel colourData = colourMap.get(colour);
112       sb.append(getAtomSpec(colourData, binding));
113     }
114     commands.add(sb.toString());
115     return commands;
116   }
117
118   /**
119    * Constructs and returns Chimera commands to set attributes on residues
120    * corresponding to features in Jalview. Attribute names are the Jalview feature
121    * type, with a "jv_" prefix.
122    * 
123    * @param ssm
124    * @param files
125    * @param seqs
126    * @param viewPanel
127    * @param binding
128    * @return
129    */
130   public static StructureMappingcommandSet getSetAttributeCommandsForFeatures(
131           AlignmentViewPanel viewPanel, AAStructureBindingModel binding)
132   {
133     StructureSelectionManager ssm = binding.getSsm();
134     String[] files = binding.getStructureFiles();
135     SequenceI[][] seqs = binding.getSequence();
136
137     Map<String, Map<Object, AtomSpecModel>> featureMap = buildFeaturesMap(
138             ssm, files, seqs, viewPanel);
139
140     List<String> commands = buildSetAttributeCommands(featureMap, binding);
141
142     StructureMappingcommandSet cs = new StructureMappingcommandSet(
143             ChimeraCommands.class, null,
144             commands.toArray(new String[commands.size()]));
145
146     return cs;
147   }
148
149   /**
150    * <pre>
151    * Helper method to build a map of 
152    *   { featureType, { feature value, AtomSpecModel } }
153    * </pre>
154    * 
155    * @param ssm
156    * @param files
157    * @param seqs
158    * @param viewPanel
159    * @return
160    */
161   protected static Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
162           StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
163           AlignmentViewPanel viewPanel)
164   {
165     Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
166
167     FeatureRenderer fr = viewPanel.getFeatureRenderer();
168     if (fr == null)
169     {
170       return theMap;
171     }
172
173     AlignViewportI viewport = viewPanel.getAlignViewport();
174     List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
175
176     /*
177      * if alignment is showing features from complement, we also transfer
178      * these features to the corresponding mapped structure residues
179      */
180     boolean showLinkedFeatures = viewport.isShowComplementFeatures();
181     List<String> complementFeatures = new ArrayList<>();
182     FeatureRenderer complementRenderer = null;
183     if (showLinkedFeatures)
184     {
185       AlignViewportI comp = fr.getViewport().getCodingComplement();
186       if (comp != null)
187       {
188         complementRenderer = Desktop.getAlignFrameFor(comp)
189                 .getFeatureRenderer();
190         complementFeatures = complementRenderer.getDisplayedFeatureTypes();
191       }
192     }
193     if (visibleFeatures.isEmpty() && complementFeatures.isEmpty())
194     {
195       return theMap;
196     }
197
198     AlignmentI alignment = viewPanel.getAlignment();
199     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
200     {
201       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
202
203       if (mapping == null || mapping.length < 1)
204       {
205         continue;
206       }
207
208       for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
209       {
210         for (int m = 0; m < mapping.length; m++)
211         {
212           final SequenceI seq = seqs[pdbfnum][seqNo];
213           int sp = alignment.findIndex(seq);
214           StructureMapping structureMapping = mapping[m];
215           if (structureMapping.getSequence() == seq && sp > -1)
216           {
217             /*
218              * found a sequence with a mapping to a structure;
219              * now scan its features
220              */
221             if (!visibleFeatures.isEmpty())
222             {
223               scanSequenceFeatures(visibleFeatures, structureMapping, seq,
224                       theMap, pdbfnum);
225             }
226             if (showLinkedFeatures)
227             {
228               scanComplementFeatures(complementRenderer, structureMapping,
229                       seq, theMap, pdbfnum);
230             }
231           }
232         }
233       }
234     }
235     return theMap;
236   }
237
238   /**
239    * Scans visible features in mapped positions of the CDS/peptide complement, and
240    * adds any found to the map of attribute values/structure positions
241    * 
242    * @param complementRenderer
243    * @param structureMapping
244    * @param seq
245    * @param theMap
246    * @param modelNumber
247    */
248   protected static void scanComplementFeatures(
249           FeatureRenderer complementRenderer,
250           StructureMapping structureMapping, SequenceI seq,
251           Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
252   {
253     /*
254      * for each sequence residue mapped to a structure position...
255      */
256     for (int seqPos : structureMapping.getMapping().keySet())
257     {
258       /*
259        * find visible complementary features at mapped position(s)
260        */
261       MappedFeatures mf = complementRenderer
262               .findComplementFeaturesAtResidue(seq, seqPos);
263       if (mf != null)
264       {
265         for (SequenceFeature sf : mf.features)
266         {
267           String type = sf.getType();
268
269           /*
270            * Don't copy features which originated from Chimera
271            */
272           if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
273                   .equals(sf.getFeatureGroup()))
274           {
275             continue;
276           }
277
278           /*
279            * record feature 'value' (score/description/type) as at the
280            * corresponding structure position
281            */
282           List<int[]> mappedRanges = structureMapping
283                   .getPDBResNumRanges(seqPos, seqPos);
284
285           if (!mappedRanges.isEmpty())
286           {
287             String value = sf.getDescription();
288             if (value == null || value.length() == 0)
289             {
290               value = type;
291             }
292             float score = sf.getScore();
293             if (score != 0f && !Float.isNaN(score))
294             {
295               value = Float.toString(score);
296             }
297             Map<Object, AtomSpecModel> featureValues = theMap.get(type);
298             if (featureValues == null)
299             {
300               featureValues = new HashMap<>();
301               theMap.put(type, featureValues);
302             }
303             for (int[] range : mappedRanges)
304             {
305               addAtomSpecRange(featureValues, value, modelNumber, range[0],
306                       range[1], structureMapping.getChain());
307             }
308           }
309         }
310       }
311     }
312   }
313
314   /**
315    * Inspect features on the sequence; for each feature that is visible, determine
316    * its mapped ranges in the structure (if any) according to the given mapping,
317    * and add them to the map.
318    * 
319    * @param visibleFeatures
320    * @param mapping
321    * @param seq
322    * @param theMap
323    * @param modelNumber
324    */
325   protected static void scanSequenceFeatures(List<String> visibleFeatures,
326           StructureMapping mapping, SequenceI seq,
327           Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
328   {
329     List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
330             visibleFeatures.toArray(new String[visibleFeatures.size()]));
331     for (SequenceFeature sf : sfs)
332     {
333       String type = sf.getType();
334
335       /*
336        * Don't copy features which originated from Chimera
337        */
338       if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
339               .equals(sf.getFeatureGroup()))
340       {
341         continue;
342       }
343
344       List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
345               sf.getEnd());
346
347       if (!mappedRanges.isEmpty())
348       {
349         String value = sf.getDescription();
350         if (value == null || value.length() == 0)
351         {
352           value = type;
353         }
354         float score = sf.getScore();
355         if (score != 0f && !Float.isNaN(score))
356         {
357           value = Float.toString(score);
358         }
359         Map<Object, AtomSpecModel> featureValues = theMap.get(type);
360         if (featureValues == null)
361         {
362           featureValues = new HashMap<>();
363           theMap.put(type, featureValues);
364         }
365         for (int[] range : mappedRanges)
366         {
367           addAtomSpecRange(featureValues, value, modelNumber, range[0],
368                   range[1], mapping.getChain());
369         }
370       }
371     }
372   }
373
374   /**
375    * Traverse the map of features/values/models/chains/positions to construct a
376    * list of 'setattr' commands (one per distinct feature type and value).
377    * <p>
378    * The format of each command is
379    * 
380    * <pre>
381    * <blockquote> setattr r <featureName> " " #modelnumber:range.chain 
382    * e.g. setattr r jv:chain <value> #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
383    * </blockquote>
384    * </pre>
385    * 
386    * @param featureMap
387    * @param binding
388    * @return
389    */
390   protected static List<String> buildSetAttributeCommands(
391           Map<String, Map<Object, AtomSpecModel>> featureMap,
392           AAStructureBindingModel binding)
393   {
394     List<String> commands = new ArrayList<>();
395     for (String featureType : featureMap.keySet())
396     {
397       String attributeName = makeAttributeName(featureType);
398
399       /*
400        * clear down existing attributes for this feature
401        */
402       // 'problem' - sets attribute to None on all residues - overkill?
403       // commands.add("~setattr r " + attributeName + " :*");
404
405       Map<Object, AtomSpecModel> values = featureMap.get(featureType);
406       for (Object value : values.keySet())
407       {
408         /*
409          * for each distinct value recorded for this feature type,
410          * add a command to set the attribute on the mapped residues
411          * Put values in single quotes, encoding any embedded single quotes
412          */
413         StringBuilder sb = new StringBuilder(128);
414         String featureValue = value.toString();
415         featureValue = featureValue.replaceAll("\\'", "&#39;");
416         sb.append("setattr r ").append(attributeName).append(" '")
417                 .append(featureValue).append("' ");
418         sb.append(getAtomSpec(values.get(value), binding));
419         commands.add(sb.toString());
420       }
421     }
422
423     return commands;
424   }
425
426   /**
427    * Makes a prefixed and valid Chimera attribute name. A jv_ prefix is applied
428    * for a 'Jalview' namespace, and any non-alphanumeric character is converted
429    * to an underscore.
430    * 
431    * @param featureType
432    * @return
433    * 
434    *         <pre>
435    * &#64;see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html
436    *         </pre>
437    */
438   protected static String makeAttributeName(String featureType)
439   {
440     StringBuilder sb = new StringBuilder();
441     if (featureType != null)
442     {
443       for (char c : featureType.toCharArray())
444       {
445         sb.append(Character.isLetterOrDigit(c) ? c : '_');
446       }
447     }
448     String attName = NAMESPACE_PREFIX + sb.toString();
449
450     /*
451      * Chimera treats an attribute name ending in 'color' as colour-valued;
452      * Jalview doesn't, so prevent this by appending an underscore
453      */
454     if (attName.toUpperCase().endsWith("COLOR"))
455     {
456       attName += "_";
457     }
458
459     return attName;
460   }
461
462   /**
463    * Returns the range(s) formatted as a Chimera atomspec
464    * 
465    * @return
466    */
467   public static String getAtomSpec(AtomSpecModel atomSpec,
468           AAStructureBindingModel binding)
469   {
470     StringBuilder sb = new StringBuilder(128);
471     boolean firstModel = true;
472     for (Integer model : atomSpec.getModels())
473     {
474       if (!firstModel)
475       {
476         sb.append("|");
477       }
478       firstModel = false;
479       sb.append(binding.getModelSpec(model)).append(":");
480
481       boolean firstPositionForModel = true;
482
483       for (String chain : atomSpec.getChains(model))
484       {
485         chain = " ".equals(chain) ? chain : chain.trim();
486
487         List<int[]> rangeList = atomSpec.getRanges(model, chain);
488
489         String chainToken = " ".equals(chain) ? "." : "." + chain;
490         appendResidueRange(sb, rangeList, chainToken,
491                 firstPositionForModel);
492         firstPositionForModel = false;
493       }
494     }
495     return sb.toString();
496   }
497
498   /**
499    * Chimera atomspec requires chain to be specified for each start-end residue
500    * range, otherwise it will apply to all chains
501    * 
502    * @param sb
503    * @param chain
504    */
505   protected static void appendChainToRange(StringBuilder sb, String chain)
506   {
507     sb.append(".");
508     if (!" ".equals(chain))
509     {
510       sb.append(chain);
511     }
512   }
513
514 }