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