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