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