Merge branch 'develop' into feature/JAL-3187linkedFeatures
[jalview.git] / src / jalview / gui / CrossRefAction.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.gui;
22
23 import jalview.analysis.AlignmentUtils;
24 import jalview.analysis.CrossRef;
25 import jalview.api.AlignmentViewPanel;
26 import jalview.api.FeatureSettingsModelI;
27 import jalview.bin.Cache;
28 import jalview.datamodel.Alignment;
29 import jalview.datamodel.AlignmentI;
30 import jalview.datamodel.DBRefEntry;
31 import jalview.datamodel.DBRefSource;
32 import jalview.datamodel.GeneLociI;
33 import jalview.datamodel.SequenceI;
34 import jalview.ext.ensembl.EnsemblInfo;
35 import jalview.ext.ensembl.EnsemblMap;
36 import jalview.io.gff.SequenceOntologyI;
37 import jalview.structure.StructureSelectionManager;
38 import jalview.util.DBRefUtils;
39 import jalview.util.MapList;
40 import jalview.util.MappingUtils;
41 import jalview.util.MessageManager;
42 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
43 import jalview.ws.SequenceFetcher;
44
45 import java.util.ArrayList;
46 import java.util.HashMap;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Set;
50
51 /**
52  * Factory constructor and runnable for discovering and displaying
53  * cross-references for a set of aligned sequences
54  * 
55  * @author jprocter
56  *
57  */
58 public class CrossRefAction implements Runnable
59 {
60   private AlignFrame alignFrame;
61
62   private SequenceI[] sel;
63
64   private final boolean _odna;
65
66   private String source;
67
68   List<AlignmentViewPanel> xrefViews = new ArrayList<>();
69
70   List<AlignmentViewPanel> getXrefViews()
71   {
72     return xrefViews;
73   }
74
75   @Override
76   public void run()
77   {
78     final long sttime = System.currentTimeMillis();
79     alignFrame.setProgressBar(MessageManager.formatMessage(
80             "status.searching_for_sequences_from", new Object[]
81             { source }), sttime);
82     try
83     {
84       AlignmentI alignment = alignFrame.getViewport().getAlignment();
85       AlignmentI dataset = alignment.getDataset() == null ? alignment
86               : alignment.getDataset();
87       boolean dna = alignment.isNucleotide();
88       if (_odna != dna)
89       {
90         System.err
91                 .println("Conflict: showProducts for alignment originally "
92                         + "thought to be " + (_odna ? "DNA" : "Protein")
93                         + " now searching for " + (dna ? "DNA" : "Protein")
94                         + " Context.");
95       }
96       AlignmentI xrefs = new CrossRef(sel, dataset)
97               .findXrefSequences(source, dna);
98       if (xrefs == null)
99       {
100         return;
101       }
102
103       /*
104        * try to look up chromosomal coordinates for nucleotide
105        * sequences (if not already retrieved)
106        */
107       findGeneLoci(xrefs.getSequences());
108
109       /*
110        * get display scheme (if any) to apply to features
111        */
112       FeatureSettingsModelI featureColourScheme = new SequenceFetcher()
113               .getFeatureColourScheme(source);
114
115       AlignmentI xrefsAlignment = makeCrossReferencesAlignment(dataset,
116               xrefs);
117       if (!dna)
118       {
119         xrefsAlignment = AlignmentUtils.makeCdsAlignment(
120                 xrefsAlignment.getSequencesArray(), dataset, sel);
121         xrefsAlignment.alignAs(alignment);
122       }
123
124       /*
125        * If we are opening a splitframe, make a copy of this alignment (sharing the same dataset
126        * sequences). If we are DNA, drop introns and update mappings
127        */
128       AlignmentI copyAlignment = null;
129
130       if (Cache.getDefault(Preferences.ENABLE_SPLIT_FRAME, true))
131       {
132         copyAlignment = copyAlignmentForSplitFrame(alignment, dataset, dna,
133                 xrefs, xrefsAlignment);
134         if (copyAlignment == null)
135         {
136           return; // failed
137         }
138       }
139
140       /*
141        * build AlignFrame(s) according to available alignment data
142        */
143       AlignFrame newFrame = new AlignFrame(xrefsAlignment,
144               AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
145       if (Cache.getDefault("HIDE_INTRONS", true))
146       {
147         newFrame.hideFeatureColumns(SequenceOntologyI.EXON, false);
148       }
149       String newtitle = String.format("%s %s %s",
150               dna ? MessageManager.getString("label.proteins")
151                       : MessageManager.getString("label.nucleotides"),
152               MessageManager.getString("label.for"), alignFrame.getTitle());
153       newFrame.setTitle(newtitle);
154
155       if (copyAlignment == null)
156       {
157         /*
158          * split frame display is turned off in preferences file
159          */
160         Desktop.addInternalFrame(newFrame, newtitle,
161                 AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
162         xrefViews.add(newFrame.alignPanel);
163         return; // via finally clause
164       }
165
166       AlignFrame copyThis = new AlignFrame(copyAlignment,
167               AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
168       copyThis.setTitle(alignFrame.getTitle());
169
170       boolean showSequenceFeatures = alignFrame.getViewport()
171               .isShowSequenceFeatures();
172       newFrame.setShowSeqFeatures(showSequenceFeatures);
173       copyThis.setShowSeqFeatures(showSequenceFeatures);
174       FeatureRendererModel myFeatureStyling = alignFrame.alignPanel
175               .getSeqPanel().seqCanvas.getFeatureRenderer();
176
177       /*
178        * copy feature rendering settings to split frame
179        */
180       FeatureRendererModel fr1 = newFrame.alignPanel.getSeqPanel().seqCanvas
181               .getFeatureRenderer();
182       fr1.transferSettings(myFeatureStyling);
183       fr1.findAllFeatures(true);
184       FeatureRendererModel fr2 = copyThis.alignPanel.getSeqPanel().seqCanvas
185               .getFeatureRenderer();
186       fr2.transferSettings(myFeatureStyling);
187       fr2.findAllFeatures(true);
188
189       /*
190        * apply 'database source' feature configuration
191        * if any was found
192        */
193       // TODO is this the feature colouring for the original
194       // alignment or the fetched xrefs? either could be Ensembl
195       newFrame.getViewport().applyFeaturesStyle(featureColourScheme);
196       copyThis.getViewport().applyFeaturesStyle(featureColourScheme);
197
198       SplitFrame sf = new SplitFrame(dna ? copyThis : newFrame,
199               dna ? newFrame : copyThis);
200       newFrame.setVisible(true);
201       copyThis.setVisible(true);
202       String linkedTitle = MessageManager
203               .getString("label.linked_view_title");
204       Desktop.addInternalFrame(sf, linkedTitle, -1, -1);
205       sf.adjustInitialLayout();
206
207       // finally add the top, then bottom frame to the view list
208       xrefViews.add(dna ? copyThis.alignPanel : newFrame.alignPanel);
209       xrefViews.add(!dna ? copyThis.alignPanel : newFrame.alignPanel);
210
211     } catch (OutOfMemoryError e)
212     {
213       new OOMWarning("whilst fetching crossreferences", e);
214     } catch (Throwable e)
215     {
216       Cache.log.error("Error when finding crossreferences", e);
217     } finally
218     {
219       alignFrame.setProgressBar(MessageManager.formatMessage(
220               "status.finished_searching_for_sequences_from", new Object[]
221               { source }), sttime);
222     }
223   }
224
225   /**
226    * Tries to add chromosomal coordinates to any nucleotide sequence which does
227    * not already have them. Coordinates are retrieved from Ensembl given an
228    * Ensembl identifier, either on the sequence itself or on a peptide sequence
229    * it has a reference to.
230    * 
231    * <pre>
232    * Example (human):
233    * - fetch EMBLCDS cross-references for Uniprot entry P30419
234    * - the EMBL sequences do not have xrefs to Ensembl
235    * - the Uniprot entry has xrefs to 
236    *    ENSP00000258960, ENSP00000468424, ENST00000258960, ENST00000592782
237    * - either of the transcript ids can be used to retrieve gene loci e.g.
238    *    http://rest.ensembl.org/map/cds/ENST00000592782/1..100000
239    * Example (invertebrate):
240    * - fetch EMBLCDS cross-references for Uniprot entry Q43517 (FER1_SOLLC)
241    * - the Uniprot entry has an xref to ENSEMBLPLANTS Solyc10g044520.1.1
242    * - can retrieve gene loci with
243    *    http://rest.ensemblgenomes.org/map/cds/Solyc10g044520.1.1/1..100000
244    * </pre>
245    * 
246    * @param sequences
247    */
248   public static void findGeneLoci(List<SequenceI> sequences)
249   {
250     Map<DBRefEntry, GeneLociI> retrievedLoci = new HashMap<>();
251     for (SequenceI seq : sequences)
252     {
253       findGeneLoci(seq, retrievedLoci);
254     }
255   }
256
257   /**
258    * Tres to find chromosomal coordinates for the sequence, by searching its
259    * direct and indirect cross-references for Ensembl. If the loci have already
260    * been retrieved, just reads them out of the map of retrievedLoci; this is
261    * the case of an alternative transcript for the same protein. Otherwise calls
262    * a REST service to retrieve the loci, and if successful, adds them to the
263    * sequence and to the retrievedLoci.
264    * 
265    * @param seq
266    * @param retrievedLoci
267    */
268   static void findGeneLoci(SequenceI seq,
269           Map<DBRefEntry, GeneLociI> retrievedLoci)
270   {
271     /*
272      * don't replace any existing chromosomal coordinates
273      */
274     if (seq == null || seq.isProtein() || seq.getGeneLoci() != null
275             || seq.getDBRefs() == null)
276     {
277       return;
278     }
279     
280     Set<String> ensemblDivisions = new EnsemblInfo().getDivisions();
281     
282     /*
283      * first look for direct dbrefs from sequence to Ensembl
284      */
285     String[] divisionsArray = ensemblDivisions
286             .toArray(new String[ensemblDivisions.size()]);
287     DBRefEntry[] seqRefs = seq.getDBRefs();
288     DBRefEntry[] directEnsemblRefs = DBRefUtils.selectRefs(seqRefs,
289             divisionsArray);
290     if (directEnsemblRefs != null)
291     {
292       for (DBRefEntry ensemblRef : directEnsemblRefs)
293       {
294         if (fetchGeneLoci(seq, ensemblRef, retrievedLoci))
295         {
296           return;
297         }
298       }
299     }
300
301     /*
302      * else look for indirect dbrefs from sequence to Ensembl
303      */
304     for (DBRefEntry dbref : seq.getDBRefs())
305     {
306       if (dbref.getMap() != null && dbref.getMap().getTo() != null)
307       {
308         DBRefEntry[] dbrefs = dbref.getMap().getTo().getDBRefs();
309         DBRefEntry[] indirectEnsemblRefs = DBRefUtils.selectRefs(dbrefs,
310                 divisionsArray);
311         if (indirectEnsemblRefs != null)
312         {
313           for (DBRefEntry ensemblRef : indirectEnsemblRefs)
314           {
315             if (fetchGeneLoci(seq, ensemblRef, retrievedLoci))
316             {
317               return;
318             }
319           }
320         }
321       }
322     }
323   }
324
325   /**
326    * Retrieves chromosomal coordinates for the Ensembl (or EnsemblGenomes)
327    * identifier in dbref. If successful, and the sequence length matches gene
328    * loci length, then add it to the sequence, and to the retrievedLoci map.
329    * Answers true if successful, else false.
330    * 
331    * @param seq
332    * @param dbref
333    * @param retrievedLoci
334    * @return
335    */
336   static boolean fetchGeneLoci(SequenceI seq, DBRefEntry dbref,
337           Map<DBRefEntry, GeneLociI> retrievedLoci)
338   {
339     String accession = dbref.getAccessionId();
340     String division = dbref.getSource();
341
342     /*
343      * hack: ignore cross-references to Ensembl protein ids
344      * (or use map/translation perhaps?)
345      * todo: is there an equivalent in EnsemblGenomes?
346      */
347     if (accession.startsWith("ENSP"))
348     {
349       return false;
350     }
351     EnsemblMap mapper = new EnsemblMap();
352
353     /*
354      * try CDS mapping first
355      */
356     GeneLociI geneLoci = mapper.getCdsMapping(division, accession, 1,
357             seq.getLength());
358     if (geneLoci != null)
359     {
360       MapList map = geneLoci.getMapping();
361       int mappedFromLength = MappingUtils.getLength(map.getFromRanges());
362       if (mappedFromLength == seq.getLength())
363       {
364         seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(),
365                 geneLoci.getChromosomeId(), map);
366         retrievedLoci.put(dbref, geneLoci);
367         return true;
368       }
369     }
370
371     /*
372      * else try CDNA mapping
373      */
374     geneLoci = mapper.getCdnaMapping(division, accession, 1,
375             seq.getLength());
376     if (geneLoci != null)
377     {
378       MapList map = geneLoci.getMapping();
379       int mappedFromLength = MappingUtils.getLength(map.getFromRanges());
380       if (mappedFromLength == seq.getLength())
381       {
382         seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(),
383                 geneLoci.getChromosomeId(), map);
384         retrievedLoci.put(dbref, geneLoci);
385         return true;
386       }
387     }
388
389     return false;
390   }
391
392   /**
393    * @param alignment
394    * @param dataset
395    * @param dna
396    * @param xrefs
397    * @param xrefsAlignment
398    * @return
399    */
400   protected AlignmentI copyAlignmentForSplitFrame(AlignmentI alignment,
401           AlignmentI dataset, boolean dna, AlignmentI xrefs,
402           AlignmentI xrefsAlignment)
403   {
404     AlignmentI copyAlignment;
405     boolean copyAlignmentIsAligned = false;
406     if (dna)
407     {
408       copyAlignment = AlignmentUtils.makeCdsAlignment(sel, dataset,
409               xrefsAlignment.getSequencesArray());
410       if (copyAlignment.getHeight() == 0)
411       {
412         JvOptionPane.showMessageDialog(alignFrame,
413                 MessageManager.getString("label.cant_map_cds"),
414                 MessageManager.getString("label.operation_failed"),
415                 JvOptionPane.OK_OPTION);
416         System.err.println("Failed to make CDS alignment");
417         return null;
418       }
419
420       /*
421        * pending getting Embl transcripts to 'align', 
422        * we are only doing this for Ensembl
423        */
424       // TODO proper criteria for 'can align as cdna'
425       if (DBRefSource.ENSEMBL.equalsIgnoreCase(source)
426               || AlignmentUtils.looksLikeEnsembl(alignment))
427       {
428         copyAlignment.alignAs(alignment);
429         copyAlignmentIsAligned = true;
430       }
431     }
432     else
433     {
434       copyAlignment = AlignmentUtils.makeCopyAlignment(sel,
435               xrefs.getSequencesArray(), dataset);
436     }
437     copyAlignment
438             .setGapCharacter(alignFrame.viewport.getGapCharacter());
439
440     StructureSelectionManager ssm = StructureSelectionManager
441             .getStructureSelectionManager(Desktop.instance);
442
443     /*
444      * register any new mappings for sequence mouseover etc
445      * (will not duplicate any previously registered mappings)
446      */
447     ssm.registerMappings(dataset.getCodonFrames());
448
449     if (copyAlignment.getHeight() <= 0)
450     {
451       System.err.println(
452               "No Sequences generated for xRef type " + source);
453       return null;
454     }
455
456     /*
457      * align protein to dna
458      */
459     if (dna && copyAlignmentIsAligned)
460     {
461       xrefsAlignment.alignAs(copyAlignment);
462     }
463     else
464     {
465       /*
466        * align cdna to protein - currently only if 
467        * fetching and aligning Ensembl transcripts!
468        */
469       // TODO: generalise for other sources of locus/transcript/cds data
470       if (dna && DBRefSource.ENSEMBL.equalsIgnoreCase(source))
471       {
472         copyAlignment.alignAs(xrefsAlignment);
473       }
474     }
475
476     return copyAlignment;
477   }
478
479   /**
480    * Makes an alignment containing the given sequences, and adds them to the
481    * given dataset, which is also set as the dataset for the new alignment
482    * 
483    * TODO: refactor to DatasetI method
484    * 
485    * @param dataset
486    * @param seqs
487    * @return
488    */
489   protected AlignmentI makeCrossReferencesAlignment(AlignmentI dataset,
490           AlignmentI seqs)
491   {
492     SequenceI[] sprods = new SequenceI[seqs.getHeight()];
493     for (int s = 0; s < sprods.length; s++)
494     {
495       sprods[s] = (seqs.getSequenceAt(s)).deriveSequence();
496       if (dataset.getSequences() == null || !dataset.getSequences()
497               .contains(sprods[s].getDatasetSequence()))
498       {
499         dataset.addSequence(sprods[s].getDatasetSequence());
500       }
501       sprods[s].updatePDBIds();
502     }
503     Alignment al = new Alignment(sprods);
504     al.setDataset(dataset);
505     return al;
506   }
507
508   /**
509    * Constructor
510    * 
511    * @param af
512    * @param seqs
513    * @param fromDna
514    * @param dbSource
515    */
516   CrossRefAction(AlignFrame af, SequenceI[] seqs, boolean fromDna,
517           String dbSource)
518   {
519     this.alignFrame = af;
520     this.sel = seqs;
521     this._odna = fromDna;
522     this.source = dbSource;
523   }
524
525   public static CrossRefAction getHandlerFor(final SequenceI[] sel,
526           final boolean fromDna, final String source,
527           final AlignFrame alignFrame)
528   {
529     return new CrossRefAction(alignFrame, sel, fromDna, source);
530   }
531
532 }