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