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