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