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