JAL-3551 working proof of concept of Jalview driving PyMOL
[jalview.git] / src / jalview / ext / rbvi / chimera / ChimeraCommands.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.ext.rbvi.chimera;
22
23 import jalview.api.AlignViewportI;
24 import jalview.api.AlignmentViewPanel;
25 import jalview.api.FeatureRenderer;
26 import jalview.datamodel.AlignmentI;
27 import jalview.datamodel.MappedFeatures;
28 import jalview.datamodel.SequenceFeature;
29 import jalview.datamodel.SequenceI;
30 import jalview.gui.Desktop;
31 import jalview.structure.AtomSpecModel;
32 import jalview.structure.StructureCommand;
33 import jalview.structure.StructureCommandI;
34 import jalview.structure.StructureCommandsBase;
35 import jalview.structure.StructureMapping;
36 import jalview.structure.StructureSelectionManager;
37 import jalview.util.ColorUtils;
38
39 import java.awt.Color;
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.HashMap;
43 import java.util.LinkedHashMap;
44 import java.util.List;
45 import java.util.Map;
46
47 /**
48  * Routines for generating Chimera commands for Jalview/Chimera binding
49  * 
50  * @author JimP
51  * 
52  */
53 public class ChimeraCommands extends StructureCommandsBase
54 {
55   private static final StructureCommand SHOW_BACKBONE = new StructureCommand("~display all;chain @CA|P");
56
57   public static final String NAMESPACE_PREFIX = "jv_";
58
59   private static final StructureCommandI COLOUR_BY_CHARGE = new StructureCommand(
60           "color white;color red ::ASP;color red ::GLU;color blue ::LYS;color blue ::ARG;color yellow ::CYS");
61
62   private static final StructureCommandI COLOUR_BY_CHAIN = new StructureCommand(
63           "rainbow chain");
64
65   // Chimera clause to exclude alternate locations in atom selection
66   private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9";
67
68   @Override
69   public StructureCommandI getColourCommand(String atomSpec, Color colour)
70   {
71     // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/color.html
72     String colourCode = getColourString(colour);
73     return new StructureCommand("color " + colourCode + " " + atomSpec);
74   }
75
76   /**
77    * Returns a colour formatted suitable for use in viewer command syntax
78    * 
79    * @param colour
80    * @return
81    */
82   protected String getColourString(Color colour)
83   {
84     return ColorUtils.toTkCode(colour);
85   }
86
87   /**
88    * Constructs and returns Chimera commands to set attributes on residues
89    * corresponding to features in Jalview. Attribute names are the Jalview feature
90    * type, with a "jv_" prefix.
91    * 
92    * @param ssm
93    * @param files
94    * @param seqs
95    * @param viewPanel
96    * @return
97    */
98   @Override
99   public List<StructureCommandI> setAttributesForFeatures(
100           StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
101           AlignmentViewPanel viewPanel)
102   {
103     Map<String, Map<Object, AtomSpecModel>> featureMap = buildFeaturesMap(
104             ssm, files, seqs, viewPanel);
105
106     return setAttributes(featureMap);
107   }
108
109   /**
110    * <pre>
111    * Helper method to build a map of 
112    *   { featureType, { feature value, AtomSpecModel } }
113    * </pre>
114    * 
115    * @param ssm
116    * @param files
117    * @param seqs
118    * @param viewPanel
119    * @return
120    */
121   protected Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
122           StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
123           AlignmentViewPanel viewPanel)
124   {
125     Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
126
127     FeatureRenderer fr = viewPanel.getFeatureRenderer();
128     if (fr == null)
129     {
130       return theMap;
131     }
132
133     AlignViewportI viewport = viewPanel.getAlignViewport();
134     List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
135
136     /*
137      * if alignment is showing features from complement, we also transfer
138      * these features to the corresponding mapped structure residues
139      */
140     boolean showLinkedFeatures = viewport.isShowComplementFeatures();
141     List<String> complementFeatures = new ArrayList<>();
142     FeatureRenderer complementRenderer = null;
143     if (showLinkedFeatures)
144     {
145       AlignViewportI comp = fr.getViewport().getCodingComplement();
146       if (comp != null)
147       {
148         complementRenderer = Desktop.getAlignFrameFor(comp)
149                 .getFeatureRenderer();
150         complementFeatures = complementRenderer.getDisplayedFeatureTypes();
151       }
152     }
153     if (visibleFeatures.isEmpty() && complementFeatures.isEmpty())
154     {
155       return theMap;
156     }
157
158     AlignmentI alignment = viewPanel.getAlignment();
159     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
160     {
161       final int modelNumber = pdbfnum + getModelStartNo();
162       String modelId = String.valueOf(modelNumber);
163       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
164
165       if (mapping == null || mapping.length < 1)
166       {
167         continue;
168       }
169
170       for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
171       {
172         for (int m = 0; m < mapping.length; m++)
173         {
174           final SequenceI seq = seqs[pdbfnum][seqNo];
175           int sp = alignment.findIndex(seq);
176           StructureMapping structureMapping = mapping[m];
177           if (structureMapping.getSequence() == seq && sp > -1)
178           {
179             /*
180              * found a sequence with a mapping to a structure;
181              * now scan its features
182              */
183             if (!visibleFeatures.isEmpty())
184             {
185               scanSequenceFeatures(visibleFeatures, structureMapping, seq,
186                       theMap, modelId);
187             }
188             if (showLinkedFeatures)
189             {
190               scanComplementFeatures(complementRenderer, structureMapping,
191                       seq, theMap, modelId);
192             }
193           }
194         }
195       }
196     }
197     return theMap;
198   }
199
200   /**
201    * Scans visible features in mapped positions of the CDS/peptide complement, and
202    * adds any found to the map of attribute values/structure positions
203    * 
204    * @param complementRenderer
205    * @param structureMapping
206    * @param seq
207    * @param theMap
208    * @param modelNumber
209    */
210   protected static void scanComplementFeatures(
211           FeatureRenderer complementRenderer,
212           StructureMapping structureMapping, SequenceI seq,
213           Map<String, Map<Object, AtomSpecModel>> theMap,
214           String modelNumber)
215   {
216     /*
217      * for each sequence residue mapped to a structure position...
218      */
219     for (int seqPos : structureMapping.getMapping().keySet())
220     {
221       /*
222        * find visible complementary features at mapped position(s)
223        */
224       MappedFeatures mf = complementRenderer
225               .findComplementFeaturesAtResidue(seq, seqPos);
226       if (mf != null)
227       {
228         for (SequenceFeature sf : mf.features)
229         {
230           String type = sf.getType();
231
232           /*
233            * Don't copy features which originated from Chimera
234            */
235           if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
236                   .equals(sf.getFeatureGroup()))
237           {
238             continue;
239           }
240
241           /*
242            * record feature 'value' (score/description/type) as at the
243            * corresponding structure position
244            */
245           List<int[]> mappedRanges = structureMapping
246                   .getPDBResNumRanges(seqPos, seqPos);
247
248           if (!mappedRanges.isEmpty())
249           {
250             String value = sf.getDescription();
251             if (value == null || value.length() == 0)
252             {
253               value = type;
254             }
255             float score = sf.getScore();
256             if (score != 0f && !Float.isNaN(score))
257             {
258               value = Float.toString(score);
259             }
260             Map<Object, AtomSpecModel> featureValues = theMap.get(type);
261             if (featureValues == null)
262             {
263               featureValues = new HashMap<>();
264               theMap.put(type, featureValues);
265             }
266             for (int[] range : mappedRanges)
267             {
268               addAtomSpecRange(featureValues, value, modelNumber, range[0],
269                       range[1], structureMapping.getChain());
270             }
271           }
272         }
273       }
274     }
275   }
276
277   /**
278    * Inspect features on the sequence; for each feature that is visible,
279    * determine its mapped ranges in the structure (if any) according to the
280    * given mapping, and add them to the map.
281    * 
282    * @param visibleFeatures
283    * @param mapping
284    * @param seq
285    * @param theMap
286    * @param modelId
287    */
288   protected static void scanSequenceFeatures(List<String> visibleFeatures,
289           StructureMapping mapping, SequenceI seq,
290           Map<String, Map<Object, AtomSpecModel>> theMap, String modelId)
291   {
292     List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
293             visibleFeatures.toArray(new String[visibleFeatures.size()]));
294     for (SequenceFeature sf : sfs)
295     {
296       String type = sf.getType();
297
298       /*
299        * Don't copy features which originated from Chimera
300        */
301       if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
302               .equals(sf.getFeatureGroup()))
303       {
304         continue;
305       }
306
307       List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
308               sf.getEnd());
309
310       if (!mappedRanges.isEmpty())
311       {
312         String value = sf.getDescription();
313         if (value == null || value.length() == 0)
314         {
315           value = type;
316         }
317         float score = sf.getScore();
318         if (score != 0f && !Float.isNaN(score))
319         {
320           value = Float.toString(score);
321         }
322         Map<Object, AtomSpecModel> featureValues = theMap.get(type);
323         if (featureValues == null)
324         {
325           featureValues = new HashMap<>();
326           theMap.put(type, featureValues);
327         }
328         for (int[] range : mappedRanges)
329         {
330           addAtomSpecRange(featureValues, value, modelId, range[0],
331                   range[1], mapping.getChain());
332         }
333       }
334     }
335   }
336
337   /**
338    * Traverse the map of features/values/models/chains/positions to construct a
339    * list of 'setattr' commands (one per distinct feature type and value).
340    * <p>
341    * The format of each command is
342    * 
343    * <pre>
344    * <blockquote> setattr r <featureName> " " #modelnumber:range.chain 
345    * e.g. setattr r jv_chain &lt;value&gt; #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
346    * </blockquote>
347    * </pre>
348    * 
349    * @param featureMap
350    * @return
351    */
352   protected List<StructureCommandI> setAttributes(
353           Map<String, Map<Object, AtomSpecModel>> featureMap)
354   {
355     List<StructureCommandI> commands = new ArrayList<>();
356     for (String featureType : featureMap.keySet())
357     {
358       String attributeName = makeAttributeName(featureType);
359
360       /*
361        * clear down existing attributes for this feature
362        */
363       // 'problem' - sets attribute to None on all residues - overkill?
364       // commands.add("~setattr r " + attributeName + " :*");
365
366       Map<Object, AtomSpecModel> values = featureMap.get(featureType);
367       for (Object value : values.keySet())
368       {
369         /*
370          * for each distinct value recorded for this feature type,
371          * add a command to set the attribute on the mapped residues
372          * Put values in single quotes, encoding any embedded single quotes
373          */
374         AtomSpecModel atomSpecModel = values.get(value);
375         String featureValue = value.toString();
376         featureValue = featureValue.replaceAll("\\'", "&#39;");
377         StructureCommandI cmd = setAttribute(attributeName, featureValue,
378                 atomSpecModel);
379         commands.add(cmd);
380       }
381     }
382
383     return commands;
384   }
385
386   /**
387    * Returns a viewer command to set the given residue attribute value on
388    * residues specified by the AtomSpecModel, for example
389    * 
390    * <pre>
391    * setatr res jv_chain 'primary' #1:12-34,48-55.B
392    * </pre>
393    * 
394    * @param attributeName
395    * @param attributeValue
396    * @param atomSpecModel
397    * @return
398    */
399   protected StructureCommandI setAttribute(String attributeName,
400           String attributeValue,
401           AtomSpecModel atomSpecModel)
402   {
403     StringBuilder sb = new StringBuilder(128);
404     sb.append("setattr res ").append(attributeName).append(" '")
405             .append(attributeValue).append("' ");
406     sb.append(getAtomSpec(atomSpecModel, false));
407     return new StructureCommand(sb.toString());
408   }
409
410   /**
411    * Makes a prefixed and valid Chimera attribute name. A jv_ prefix is applied
412    * for a 'Jalview' namespace, and any non-alphanumeric character is converted
413    * to an underscore.
414    * 
415    * @param featureType
416    * @return
417    * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html
418    */
419   protected static String makeAttributeName(String featureType)
420   {
421     StringBuilder sb = new StringBuilder();
422     if (featureType != null)
423     {
424       for (char c : featureType.toCharArray())
425       {
426         sb.append(Character.isLetterOrDigit(c) ? c : '_');
427       }
428     }
429     String attName = NAMESPACE_PREFIX + sb.toString();
430
431     /*
432      * Chimera treats an attribute name ending in 'color' as colour-valued;
433      * Jalview doesn't, so prevent this by appending an underscore
434      */
435     if (attName.toUpperCase().endsWith("COLOR"))
436     {
437       attName += "_";
438     }
439
440     return attName;
441   }
442
443   @Override
444   public StructureCommandI colourByChain()
445   {
446     return COLOUR_BY_CHAIN;
447   }
448
449   @Override
450   public List<StructureCommandI> colourByCharge()
451   {
452     return Arrays.asList(COLOUR_BY_CHARGE);
453   }
454
455   @Override
456   public String getResidueSpec(String residue)
457   {
458     return "::" + residue;
459   }
460
461   @Override
462   public StructureCommandI setBackgroundColour(Color col)
463   {
464     // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/set.html#bgcolor
465     return new StructureCommand("set bgColor " + ColorUtils.toTkCode(col));
466   }
467
468   @Override
469   public StructureCommandI focusView()
470   {
471     // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/focus.html
472     return new StructureCommand("focus");
473   }
474
475   @Override
476   public List<StructureCommandI> showChains(List<String> toShow)
477   {
478     /*
479      * Construct a chimera command like
480      * 
481      * ~display #*;~ribbon #*;ribbon :.A,:.B
482      */
483     StringBuilder cmd = new StringBuilder(64);
484     boolean first = true;
485     for (String chain : toShow)
486     {
487       String[] tokens = chain.split(":");
488       if (tokens.length == 2)
489       {
490         String showChainCmd = tokens[0] + ":." + tokens[1];
491         if (!first)
492         {
493           cmd.append(",");
494         }
495         cmd.append(showChainCmd);
496         first = false;
497       }
498     }
499
500     /*
501      * could append ";focus" to this command to resize the display to fill the
502      * window, but it looks more helpful not to (easier to relate chains to the
503      * whole)
504      */
505     final String command = "~display #*; ~ribbon #*; ribbon :"
506             + cmd.toString();
507     return Arrays.asList(new StructureCommand(command));
508   }
509
510   @Override
511   public List<StructureCommandI> superposeStructures(AtomSpecModel spec,
512           AtomSpecModel ref)
513   {
514     /*
515      * Form Chimera match command to match spec to ref
516      * 
517      * match #1:1-30.B,81-100.B@CA #0:21-40.A,61-90.A@CA
518      * 
519      * @see
520      * https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/match.html
521      */
522     StringBuilder cmd = new StringBuilder();
523     String atomSpec = getAtomSpec(spec, true);
524     String refSpec = getAtomSpec(ref, true);
525     cmd.append("match ").append(atomSpec).append(" ").append(refSpec);
526
527     /*
528      * show superposed residues as ribbon
529      */
530     cmd.append("; ribbon ");
531     cmd.append(atomSpec).append("|").append(refSpec).append("; focus");
532
533     return Arrays.asList(new StructureCommand(cmd.toString()));
534   }
535
536   @Override
537   public StructureCommandI openCommandFile(String path)
538   {
539     // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/filetypes.html
540     return new StructureCommand("open cmd:" + path);
541   }
542
543   @Override
544   public StructureCommandI saveSession(String filepath)
545   {
546     // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/save.html
547     return new StructureCommand("save " + filepath);
548   }
549
550   /**
551    * Returns the range(s) modelled by {@code atomSpec} formatted as a Chimera
552    * atomspec string, e.g.
553    * 
554    * <pre>
555    * #0:15.A,28.A,54.A,70-72.A|#1:2.A,6.A,11.A,13-14.A
556    * </pre>
557    * 
558    * where
559    * <ul>
560    * <li>#0 is a model number</li>
561    * <li>15 or 70-72 is a residue number, or range of residue numbers</li>
562    * <li>.A is a chain identifier</li>
563    * <li>residue ranges are separated by comma</li>
564    * <li>atomspecs for distinct models are separated by | (or)</li>
565    * </ul>
566    * 
567    * <pre>
568    * 
569    * @param model
570    * @param alphaOnly
571    * @return
572    * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec.html
573    */
574   @Override
575   public String getAtomSpec(AtomSpecModel atomSpec, boolean alphaOnly)
576   {
577     StringBuilder sb = new StringBuilder(128);
578     boolean firstModel = true;
579     for (String model : atomSpec.getModels())
580     {
581       if (!firstModel)
582       {
583         sb.append("|");
584       }
585       firstModel = false;
586       appendModel(sb, model, atomSpec, alphaOnly);
587     }
588     return sb.toString();
589   }
590
591   /**
592    * A helper method to append an atomSpec string for atoms in the given model
593    * 
594    * @param sb
595    * @param model
596    * @param atomSpec
597    * @param alphaOnly
598    */
599   protected void appendModel(StringBuilder sb, String model,
600           AtomSpecModel atomSpec, boolean alphaOnly)
601   {
602     sb.append("#").append(model).append(":");
603
604     boolean firstPositionForModel = true;
605
606     for (String chain : atomSpec.getChains(model))
607     {
608       chain = " ".equals(chain) ? chain : chain.trim();
609
610       List<int[]> rangeList = atomSpec.getRanges(model, chain);
611       for (int[] range : rangeList)
612       {
613         appendRange(sb, range[0], range[1], chain, firstPositionForModel,
614                 false);
615         firstPositionForModel = false;
616       }
617     }
618     if (alphaOnly)
619     {
620       /*
621        * restrict to alpha carbon, no alternative locations
622        * (needed to ensuring matching atom counts for superposition)
623        */
624       sb.append("@CA|P").append(NO_ALTLOCS);
625     }
626   }
627
628   @Override
629   public List<StructureCommandI> showBackbone()
630   {
631     return Arrays.asList(SHOW_BACKBONE);
632   }
633
634   @Override
635   public StructureCommandI loadFile(String file)
636   {
637     return new StructureCommand("open " + file);
638   }
639
640 }