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