Merge branch 'develop' into features/JAL-2094_colourInterface
[jalview.git] / src / jalview / ext / ensembl / EnsemblGene.java
1 package jalview.ext.ensembl;
2
3 import jalview.api.ColorI;
4 import jalview.api.FeatureColourI;
5 import jalview.api.FeatureSettingsModelI;
6 import jalview.datamodel.AlignmentI;
7 import jalview.datamodel.Sequence;
8 import jalview.datamodel.SequenceFeature;
9 import jalview.datamodel.SequenceI;
10 import jalview.io.gff.SequenceOntologyFactory;
11 import jalview.io.gff.SequenceOntologyI;
12 import jalview.schemes.Colour;
13 import jalview.schemes.FeatureColourAdapter;
14 import jalview.schemes.FeatureSettingsAdapter;
15 import jalview.util.MapList;
16
17 import java.io.UnsupportedEncodingException;
18 import java.net.URLDecoder;
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import java.util.List;
22
23 import com.stevesoft.pat.Regex;
24
25 /**
26  * A class that fetches genomic sequence and all transcripts for an Ensembl gene
27  * 
28  * @author gmcarstairs
29  */
30 public class EnsemblGene extends EnsemblSeqProxy
31 {
32   private static final String GENE_PREFIX = "gene:";
33
34   /*
35    * accepts anything as we will attempt lookup of gene or 
36    * transcript id or gene name
37    */
38   private static final Regex ACCESSION_REGEX = new Regex(".*");
39
40   private static final EnsemblFeatureType[] FEATURES_TO_FETCH = {
41       EnsemblFeatureType.gene, EnsemblFeatureType.transcript,
42       EnsemblFeatureType.exon, EnsemblFeatureType.cds,
43       EnsemblFeatureType.variation };
44
45   /**
46    * Default constructor (to use rest.ensembl.org)
47    */
48   public EnsemblGene()
49   {
50     super();
51   }
52
53   /**
54    * Constructor given the target domain to fetch data from
55    * 
56    * @param d
57    */
58   public EnsemblGene(String d)
59   {
60     super(d);
61   }
62
63   @Override
64   public String getDbName()
65   {
66     return "ENSEMBL";
67   }
68
69   @Override
70   protected EnsemblFeatureType[] getFeaturesToFetch()
71   {
72     return FEATURES_TO_FETCH;
73   }
74
75   @Override
76   protected EnsemblSeqType getSourceEnsemblType()
77   {
78     return EnsemblSeqType.GENOMIC;
79   }
80
81   /**
82    * Returns an alignment containing the gene(s) for the given gene or
83    * transcript identifier, or external identifier (e.g. Uniprot id). If given a
84    * gene name or external identifier, returns any related gene sequences found
85    * for model organisms. If only a single gene is queried for, then its
86    * transcripts are also retrieved and added to the alignment. <br>
87    * Method:
88    * <ul>
89    * <li>resolves a transcript identifier by looking up its parent gene id</li>
90    * <li>resolves an external identifier by looking up xref-ed gene ids</li>
91    * <li>fetches the gene sequence</li>
92    * <li>fetches features on the sequence</li>
93    * <li>identifies "transcript" features whose Parent is the requested gene</li>
94    * <li>fetches the transcript sequence for each transcript</li>
95    * <li>makes a mapping from the gene to each transcript</li>
96    * <li>copies features from gene to transcript sequences</li>
97    * <li>fetches the protein sequence for each transcript, maps and saves it as
98    * a cross-reference</li>
99    * <li>aligns each transcript against the gene sequence based on the position
100    * mappings</li>
101    * </ul>
102    * 
103    * @param query
104    *          a single gene or transcript identifier or gene name
105    * @return an alignment containing a gene, and possibly transcripts, or null
106    */
107   @Override
108   public AlignmentI getSequenceRecords(String query) throws Exception
109   {
110     /*
111      * convert to a non-duplicated list of gene identifiers
112      */
113     List<String> geneIds = getGeneIds(query);
114
115     AlignmentI al = null;
116     for (String geneId : geneIds)
117     {
118       /*
119        * fetch the gene sequence(s) with features and xrefs
120        */
121       AlignmentI geneAlignment = super.getSequenceRecords(geneId);
122       if (geneAlignment == null)
123       {
124         continue;
125       }
126       if (geneAlignment.getHeight() == 1)
127       {
128         getTranscripts(geneAlignment, geneId);
129       }
130       if (al == null)
131       {
132         al = geneAlignment;
133       }
134       else
135       {
136         al.append(geneAlignment);
137       }
138     }
139     return al;
140   }
141
142   /**
143    * Converts a query, which may contain one or more gene or transcript
144    * identifiers, into a non-redundant list of gene identifiers.
145    * 
146    * @param accessions
147    * @return
148    */
149   List<String> getGeneIds(String accessions)
150   {
151     List<String> geneIds = new ArrayList<String>();
152
153     for (String acc : accessions.split(getAccessionSeparator()))
154     {
155       if (isGeneIdentifier(acc))
156       {
157         if (!geneIds.contains(acc))
158         {
159           geneIds.add(acc);
160         }
161       }
162
163       /*
164        * if given a transcript id, look up its gene parent
165        */
166       else if (isTranscriptIdentifier(acc))
167       {
168         String geneId = new EnsemblLookup(getDomain()).getParent(acc);
169         if (geneId != null && !geneIds.contains(geneId))
170         {
171           geneIds.add(geneId);
172         }
173       }
174
175       /*
176        * if given a gene or other external name, lookup and fetch 
177        * the corresponding gene for all model organisms 
178        */
179       else
180       {
181         List<String> ids = new EnsemblSymbol(getDomain(), getDbSource(),
182                 getDbVersion()).getIds(acc);
183         for (String geneId : ids)
184         {
185           if (!geneIds.contains(geneId))
186           {
187             geneIds.add(geneId);
188           }
189         }
190       }
191     }
192     return geneIds;
193   }
194
195   /**
196    * Attempts to get Ensembl stable identifiers for model organisms for a gene
197    * name by calling the xrefs symbol REST service to resolve the gene name.
198    * 
199    * @param query
200    * @return
201    */
202   protected String getGeneIdentifiersForName(String query)
203   {
204     List<String> ids = new EnsemblSymbol(getDomain(), getDbSource(),
205             getDbVersion()).getIds(query);
206     if (ids != null)
207     {
208       for (String id : ids)
209       {
210         if (isGeneIdentifier(id))
211         {
212           return id;
213         }
214       }
215     }
216     return null;
217   }
218
219   /**
220    * Constructs all transcripts for the gene, as identified by "transcript"
221    * features whose Parent is the requested gene. The coding transcript
222    * sequences (i.e. with introns omitted) are added to the alignment.
223    * 
224    * @param al
225    * @param accId
226    * @throws Exception
227    */
228   protected void getTranscripts(AlignmentI al, String accId)
229           throws Exception
230   {
231     SequenceI gene = al.getSequenceAt(0);
232     List<SequenceFeature> transcriptFeatures = getTranscriptFeatures(accId,
233             gene);
234
235     for (SequenceFeature transcriptFeature : transcriptFeatures)
236     {
237       makeTranscript(transcriptFeature, al, gene);
238     }
239
240     clearGeneFeatures(gene);
241   }
242
243   /**
244    * Remove unwanted features (transcript, exon, CDS) from the gene sequence
245    * after we have used them to derive transcripts and transfer features
246    * 
247    * @param gene
248    */
249   protected void clearGeneFeatures(SequenceI gene)
250   {
251     SequenceFeature[] sfs = gene.getSequenceFeatures();
252     if (sfs != null)
253     {
254       SequenceOntologyI so = SequenceOntologyFactory.getInstance();
255       List<SequenceFeature> filtered = new ArrayList<SequenceFeature>();
256       for (SequenceFeature sf : sfs)
257       {
258         String type = sf.getType();
259         if (!isTranscript(type) && !so.isA(type, SequenceOntologyI.EXON)
260                 && !so.isA(type, SequenceOntologyI.CDS))
261         {
262           filtered.add(sf);
263         }
264       }
265       gene.setSequenceFeatures(filtered
266               .toArray(new SequenceFeature[filtered
267               .size()]));
268     }
269   }
270
271   /**
272    * Constructs a spliced transcript sequence by finding 'exon' features for the
273    * given id (or failing that 'CDS'). Copies features on to the new sequence.
274    * 'Aligns' the new sequence against the gene sequence by padding with gaps,
275    * and adds it to the alignment.
276    * 
277    * @param transcriptFeature
278    * @param al
279    *          the alignment to which to add the new sequence
280    * @param gene
281    *          the parent gene sequence, with features
282    * @return
283    */
284   SequenceI makeTranscript(SequenceFeature transcriptFeature,
285           AlignmentI al, SequenceI gene)
286   {
287     String accId = getTranscriptId(transcriptFeature);
288     if (accId == null)
289     {
290       return null;
291     }
292
293     /*
294      * NB we are mapping from gene sequence (not genome), so do not
295      * need to check for reverse strand (gene and transcript sequences 
296      * are in forward sense)
297      */
298
299     /*
300      * make a gene-length sequence filled with gaps
301      * we will fill in the bases for transcript regions
302      */
303     char[] seqChars = new char[gene.getLength()];
304     Arrays.fill(seqChars, al.getGapCharacter());
305
306     /*
307      * look for exon features of the transcript, failing that for CDS
308      * (for example ENSG00000124610 has 1 CDS but no exon features)
309      */
310     String parentId = "transcript:" + accId;
311     List<SequenceFeature> splices = findFeatures(gene,
312             SequenceOntologyI.EXON, parentId);
313     if (splices.isEmpty())
314     {
315       splices = findFeatures(gene, SequenceOntologyI.CDS, parentId);
316     }
317
318     int transcriptLength = 0;
319     final char[] geneChars = gene.getSequence();
320     int offset = gene.getStart(); // to convert to 0-based positions
321     List<int[]> mappedFrom = new ArrayList<int[]>();
322
323     for (SequenceFeature sf : splices)
324     {
325       int start = sf.getBegin() - offset;
326       int end = sf.getEnd() - offset;
327       int spliceLength = end - start + 1;
328       System.arraycopy(geneChars, start, seqChars, start, spliceLength);
329       transcriptLength += spliceLength;
330       mappedFrom.add(new int[] { sf.getBegin(), sf.getEnd() });
331     }
332
333     Sequence transcript = new Sequence(accId, seqChars, 1, transcriptLength);
334
335     /*
336      * Ensembl has gene name as transcript Name
337      * EnsemblGenomes doesn't, but has a url-encoded description field
338      */
339     String description = (String) transcriptFeature.getValue(NAME);
340     if (description == null)
341     {
342       description = (String) transcriptFeature.getValue(DESCRIPTION);
343     }
344     if (description != null)
345     {
346       try
347       {
348         transcript.setDescription(URLDecoder.decode(description, "UTF-8"));
349       } catch (UnsupportedEncodingException e)
350       {
351         e.printStackTrace(); // as if
352       }
353     }
354     transcript.createDatasetSequence();
355
356     al.addSequence(transcript);
357
358     /*
359      * transfer features to the new sequence; we use EnsemblCdna to do this,
360      * to filter out unwanted features types (see method retainFeature)
361      */
362     List<int[]> mapTo = new ArrayList<int[]>();
363     mapTo.add(new int[] { 1, transcriptLength });
364     MapList mapping = new MapList(mappedFrom, mapTo, 1, 1);
365     EnsemblCdna cdna = new EnsemblCdna(getDomain());
366     cdna.transferFeatures(gene.getSequenceFeatures(),
367             transcript.getDatasetSequence(), mapping, parentId);
368
369     /*
370      * fetch and save cross-references
371      */
372     cdna.getCrossReferences(transcript);
373
374     /*
375      * and finally fetch the protein product and save as a cross-reference
376      */
377     cdna.addProteinProduct(transcript);
378
379     return transcript;
380   }
381
382   /**
383    * Returns the 'transcript_id' property of the sequence feature (or null)
384    * 
385    * @param feature
386    * @return
387    */
388   protected String getTranscriptId(SequenceFeature feature)
389   {
390     return (String) feature.getValue("transcript_id");
391   }
392
393   /**
394    * Returns a list of the transcript features on the sequence whose Parent is
395    * the gene for the accession id.
396    * 
397    * @param accId
398    * @param geneSequence
399    * @return
400    */
401   protected List<SequenceFeature> getTranscriptFeatures(String accId,
402           SequenceI geneSequence)
403   {
404     List<SequenceFeature> transcriptFeatures = new ArrayList<SequenceFeature>();
405
406     String parentIdentifier = GENE_PREFIX + accId;
407     SequenceFeature[] sfs = geneSequence.getSequenceFeatures();
408
409     if (sfs != null)
410     {
411       for (SequenceFeature sf : sfs)
412       {
413         if (isTranscript(sf.getType()))
414         {
415           String parent = (String) sf.getValue(PARENT);
416           if (parentIdentifier.equals(parent))
417           {
418             transcriptFeatures.add(sf);
419           }
420         }
421       }
422     }
423
424     return transcriptFeatures;
425   }
426
427   @Override
428   public String getDescription()
429   {
430     return "Fetches all transcripts and variant features for a gene or transcript";
431   }
432
433   /**
434    * Default test query is a gene id (can also enter a transcript id)
435    */
436   @Override
437   public String getTestQuery()
438   {
439     return "ENSG00000157764"; // BRAF, 5 transcripts, reverse strand
440     // ENSG00000090266 // NDUFB2, 15 transcripts, forward strand
441     // ENSG00000101812 // H2BFM histone, 3 transcripts, forward strand
442     // ENSG00000123569 // H2BFWT histone, 2 transcripts, reverse strand
443   }
444
445   /**
446    * Answers true for a feature of type 'gene' (or a sub-type of gene in the
447    * Sequence Ontology), whose ID is the accession we are retrieving
448    */
449   @Override
450   protected boolean identifiesSequence(SequenceFeature sf, String accId)
451   {
452     if (SequenceOntologyFactory.getInstance().isA(sf.getType(),
453             SequenceOntologyI.GENE))
454     {
455       String id = (String) sf.getValue(ID);
456       if ((GENE_PREFIX + accId).equals(id))
457       {
458         return true;
459       }
460     }
461     return false;
462   }
463
464   /**
465    * Answers true unless feature type is 'gene', or 'transcript' with a parent
466    * which is a different gene. We need the gene features to identify the range,
467    * but it is redundant information on the gene sequence. Checking the parent
468    * allows us to drop transcript features which belong to different
469    * (overlapping) genes.
470    */
471   @Override
472   protected boolean retainFeature(SequenceFeature sf, String accessionId)
473   {
474     SequenceOntologyI so = SequenceOntologyFactory.getInstance();
475     String type = sf.getType();
476     if (so.isA(type, SequenceOntologyI.GENE))
477     {
478       return false;
479     }
480     if (isTranscript(type))
481     {
482       String parent = (String) sf.getValue(PARENT);
483       if (!(GENE_PREFIX + accessionId).equals(parent))
484       {
485         return false;
486       }
487     }
488     return true;
489   }
490
491   /**
492    * Answers false. This allows an optimisation - a single 'gene' feature is all
493    * that is needed to identify the positions of the gene on the genomic
494    * sequence.
495    */
496   @Override
497   protected boolean isSpliceable()
498   {
499     return false;
500   }
501
502   /**
503    * Override to do nothing as Ensembl doesn't return a protein sequence for a
504    * gene identifier
505    */
506   @Override
507   protected void addProteinProduct(SequenceI querySeq)
508   {
509   }
510
511   @Override
512   public Regex getAccessionValidator()
513   {
514     return ACCESSION_REGEX;
515   }
516
517   /**
518    * Returns a descriptor for suitable feature display settings with
519    * <ul>
520    * <li>only exon or sequence_variant features (or their subtypes in the
521    * Sequence Ontology) visible</li>
522    * <li>variant features coloured red</li>
523    * <li>exon features coloured by label (exon name)</li>
524    * <li>variants displayed above (on top of) exons</li>
525    * </ul>
526    */
527   @Override
528   public FeatureSettingsModelI getFeatureColourScheme()
529   {
530     return new FeatureSettingsAdapter()
531     {
532       SequenceOntologyI so = SequenceOntologyFactory.getInstance();
533       @Override
534       public boolean isFeatureDisplayed(String type)
535       {
536         return (so.isA(type, SequenceOntologyI.EXON) || so.isA(type,
537                 SequenceOntologyI.SEQUENCE_VARIANT));
538       }
539
540       @Override
541       public FeatureColourI getFeatureColour(String type)
542       {
543         if (so.isA(type, SequenceOntologyI.EXON))
544         {
545           return new FeatureColourAdapter()
546           {
547             @Override
548             public boolean isColourByLabel()
549             {
550               return true;
551             }
552           };
553         }
554         if (so.isA(type, SequenceOntologyI.SEQUENCE_VARIANT))
555         {
556           return new FeatureColourAdapter()
557           {
558
559             @Override
560             public ColorI getColour()
561             {
562               return Colour.red;
563             }
564           };
565         }
566         return null;
567       }
568
569       /**
570        * order to render sequence_variant after exon after the rest
571        */
572       @Override
573       public int compare(String feature1, String feature2)
574       {
575         if (so.isA(feature1, SequenceOntologyI.SEQUENCE_VARIANT))
576         {
577           return +1;
578         }
579         if (so.isA(feature2, SequenceOntologyI.SEQUENCE_VARIANT))
580         {
581           return -1;
582         }
583         if (so.isA(feature1, SequenceOntologyI.EXON))
584         {
585           return +1;
586         }
587         if (so.isA(feature2, SequenceOntologyI.EXON))
588         {
589           return -1;
590         }
591         return 0;
592       }
593     };
594   }
595
596 }