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