Jalview.isJS() --> Platform.isJS(), DBRefEntry[] --> List<DBRefEntry>
[jalview.git] / src / jalview / ext / ensembl / EnsemblSeqProxy.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.ensembl;
22
23 import jalview.analysis.AlignmentUtils;
24 import jalview.analysis.Dna;
25 import jalview.bin.Cache;
26 import jalview.datamodel.Alignment;
27 import jalview.datamodel.AlignmentI;
28 import jalview.datamodel.DBRefEntry;
29 import jalview.datamodel.DBRefSource;
30 import jalview.datamodel.Mapping;
31 import jalview.datamodel.Sequence;
32 import jalview.datamodel.SequenceFeature;
33 import jalview.datamodel.SequenceI;
34 import jalview.datamodel.features.SequenceFeatures;
35 import jalview.exceptions.JalviewException;
36 import jalview.io.gff.Gff3Helper;
37 import jalview.io.gff.SequenceOntologyFactory;
38 import jalview.io.gff.SequenceOntologyI;
39 import jalview.util.Comparison;
40 import jalview.util.DBRefUtils;
41 import jalview.util.IntRangeComparator;
42 import jalview.util.MapList;
43 import jalview.util.Platform;
44
45 import java.io.IOException;
46 import java.net.MalformedURLException;
47 import java.net.URL;
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.Collections;
51 import java.util.List;
52 import java.util.Map;
53
54 import org.json.simple.parser.ParseException;
55
56 /**
57  * Base class for Ensembl sequence fetchers
58  * 
59  * @see http://rest.ensembl.org/documentation/info/sequence_id
60  * @author gmcarstairs
61  */
62 public abstract class EnsemblSeqProxy extends EnsemblRestClient
63 {
64   protected static final String DESCRIPTION = "description";
65
66   /*
67    * enum for 'type' parameter to the /sequence REST service
68    */
69   public enum EnsemblSeqType
70   {
71     /**
72      * type=genomic to fetch full dna including introns
73      */
74     GENOMIC("genomic"),
75
76     /**
77      * type=cdna to fetch coding dna including UTRs
78      */
79     CDNA("cdna"),
80
81     /**
82      * type=cds to fetch coding dna excluding UTRs
83      */
84     CDS("cds"),
85
86     /**
87      * type=protein to fetch peptide product sequence
88      */
89     PROTEIN("protein");
90
91     /*
92      * the value of the 'type' parameter to fetch this version of 
93      * an Ensembl sequence
94      */
95     private String type;
96
97     EnsemblSeqType(String t)
98     {
99       type = t;
100     }
101
102     public String getType()
103     {
104       return type;
105     }
106
107   }
108
109   /**
110    * Default constructor (to use rest.ensembl.org)
111    */
112   public EnsemblSeqProxy()
113   {
114     super();
115   }
116
117   /**
118    * Constructor given the target domain to fetch data from
119    */
120   public EnsemblSeqProxy(String d)
121   {
122     super(d);
123   }
124
125   /**
126    * Makes the sequence queries to Ensembl's REST service and returns an
127    * alignment consisting of the returned sequences.
128    */
129   @Override
130   public AlignmentI getSequenceRecords(String query) throws Exception
131   {
132     // TODO use a String... query vararg instead?
133
134     // danger: accession separator used as a regex here, a string elsewhere
135     // in this case it is ok (it is just a space), but (e.g.) '\' would not be
136     List<String> allIds = Arrays
137             .asList(query.split(getAccessionSeparator()));
138     AlignmentI alignment = null;
139     inProgress = true;
140
141     /*
142      * execute queries, if necessary in batches of the
143      * maximum allowed number of ids
144      */
145     int maxQueryCount = getMaximumQueryCount();
146     for (int v = 0, vSize = allIds.size(); v < vSize; v += maxQueryCount)
147     {
148       int p = Math.min(vSize, v + maxQueryCount);
149       List<String> ids = allIds.subList(v, p);
150       try
151       {
152         alignment = fetchSequences(ids, alignment);
153       } catch (Throwable r)
154       {
155         inProgress = false;
156         String msg = "Aborting ID retrieval after " + v
157                 + " chunks. Unexpected problem (" + r.getLocalizedMessage()
158                 + ")";
159         System.err.println(msg);
160         r.printStackTrace();
161         break;
162       }
163     }
164
165     if (alignment == null)
166     {
167       return null;
168     }
169
170     /*
171      * fetch and transfer genomic sequence features,
172      * fetch protein product and add as cross-reference
173      */
174     for (String accId : allIds)
175     {
176       addFeaturesAndProduct(accId, alignment);
177     }
178
179     for (SequenceI seq : alignment.getSequences())
180     {
181       getCrossReferences(seq);
182     }
183
184     return alignment;
185   }
186
187   /**
188    * Fetches Ensembl features using the /overlap REST endpoint, and adds them to
189    * the sequence in the alignment. Also fetches the protein product, maps it
190    * from the CDS features of the sequence, and saves it as a cross-reference of
191    * the dna sequence.
192    * 
193    * @param accId
194    * @param alignment
195    */
196   protected void addFeaturesAndProduct(String accId, AlignmentI alignment)
197   {
198     if (alignment == null)
199     {
200       return;
201     }
202
203     try
204     {
205       /*
206        * get 'dummy' genomic sequence with gene, transcript, 
207        * exon, cds and variation features
208        */
209       SequenceI genomicSequence = null;
210       EnsemblFeatures gffFetcher = new EnsemblFeatures(getDomain());
211       EnsemblFeatureType[] features = getFeaturesToFetch();
212       
213       Platform.timeCheck("ESP.getsequencerec1", Platform.TIME_MARK);      
214
215       
216       AlignmentI geneFeatures = gffFetcher.getSequenceRecords(accId,
217               features);
218       if (geneFeatures != null && geneFeatures.getHeight() > 0)
219       {
220         genomicSequence = geneFeatures.getSequenceAt(0);
221       }
222       
223       Platform.timeCheck("ESP.getsequencerec2", Platform.TIME_MARK);      
224       
225       if (genomicSequence != null)
226       {
227         /*
228          * transfer features to the query sequence
229          */
230         SequenceI querySeq = alignment.findName(accId, true);
231         
232         Platform.timeCheck("ESP.transferfeat", Platform.TIME_MARK);       
233
234         if (transferFeatures(accId, genomicSequence, querySeq))
235         {
236
237           /*
238            * fetch and map protein product, and add it as a cross-reference
239            * of the retrieved sequence
240            */
241             Platform.timeCheck("ESP.addprotein", Platform.TIME_MARK);     
242           addProteinProduct(querySeq);
243         }
244       }
245     } catch (IOException e)
246     {
247       System.err.println(
248               "Error transferring Ensembl features: " + e.getMessage());
249     }
250     Platform.timeCheck("ESP.addfeat done", Platform.TIME_MARK);   
251   }
252
253   /**
254    * Returns those sequence feature types to fetch from Ensembl. We may want
255    * features either because they are of interest to the user, or as means to
256    * identify the locations of the sequence on the genomic sequence (CDS
257    * features identify CDS, exon features identify cDNA etc).
258    * 
259    * @return
260    */
261   protected abstract EnsemblFeatureType[] getFeaturesToFetch();
262
263   /**
264    * Fetches and maps the protein product, and adds it as a cross-reference of
265    * the retrieved sequence
266    */
267   protected void addProteinProduct(SequenceI querySeq)
268   {
269     String accId = querySeq.getName();
270     try
271     {
272       System.out.println("Adding protein product for " + accId);
273       AlignmentI protein = new EnsemblProtein(getDomain())
274               .getSequenceRecords(accId);
275       if (protein == null || protein.getHeight() == 0)
276       {
277         System.out.println("No protein product found for " + accId);
278         return;
279       }
280       SequenceI proteinSeq = protein.getSequenceAt(0);
281
282       /*
283        * need dataset sequences (to be the subject of mappings)
284        */
285       proteinSeq.createDatasetSequence();
286       querySeq.createDatasetSequence();
287
288       MapList mapList = AlignmentUtils.mapCdsToProtein(querySeq,
289               proteinSeq);
290       if (mapList != null)
291       {
292         // clunky: ensure Uniprot xref if we have one is on mapped sequence
293         SequenceI ds = proteinSeq.getDatasetSequence();
294         // TODO: Verify ensp primary ref is on proteinSeq.getDatasetSequence()
295         Mapping map = new Mapping(ds, mapList);
296         DBRefEntry dbr = new DBRefEntry(getDbSource(),
297                 getEnsemblDataVersion(), proteinSeq.getName(), map);
298         querySeq.getDatasetSequence().addDBRef(dbr);
299         List<DBRefEntry> uprots = DBRefUtils.selectRefs(ds.getDBRefs(),
300                 new String[]
301                 { DBRefSource.UNIPROT });
302         List<DBRefEntry> upxrefs = DBRefUtils.selectRefs(querySeq.getDBRefs(),
303                 new String[]
304                 { DBRefSource.UNIPROT });
305         if (uprots != null)
306         {
307           for (DBRefEntry up : uprots)
308           {
309             // locate local uniprot ref and map
310             List<DBRefEntry> upx = DBRefUtils.searchRefs(upxrefs,
311                     up.getAccessionId());
312             DBRefEntry upxref;
313             if (upx.size() != 0)
314             {
315               upxref = upx.get(0);
316
317               if (upx.size() > 1)
318               {
319                 Cache.log.warn(
320                         "Implementation issue - multiple uniprot acc on product sequence.");
321               }
322             }
323             else
324             {
325               upxref = new DBRefEntry(DBRefSource.UNIPROT,
326                       getEnsemblDataVersion(), up.getAccessionId());
327             }
328
329             Mapping newMap = new Mapping(ds, mapList);
330             upxref.setVersion(getEnsemblDataVersion());
331             upxref.setMap(newMap);
332             if (upx.size() == 0)
333             {
334               // add the new uniprot ref
335               querySeq.getDatasetSequence().addDBRef(upxref);
336             }
337
338           }
339         }
340
341         /*
342          * copy exon features to protein, compute peptide variants from dna 
343          * variants and add as features on the protein sequence ta-da
344          */
345         AlignmentUtils.computeProteinFeatures(querySeq, proteinSeq,
346                 mapList);
347       }
348     } catch (Exception e)
349     {
350       System.err
351               .println(String.format("Error retrieving protein for %s: %s",
352                       accId, e.getMessage()));
353     }
354   }
355
356   /**
357    * Get database xrefs from Ensembl, and attach them to the sequence
358    * 
359    * @param seq
360    */
361   protected void getCrossReferences(SequenceI seq)
362   {
363           
364       Platform.timeCheck("ESP. getdataseq ", Platform.TIME_MARK);         
365
366           
367     while (seq.getDatasetSequence() != null)
368     {
369       seq = seq.getDatasetSequence();
370     }
371
372     Platform.timeCheck("ESP. getxref ", Platform.TIME_MARK);      
373
374     EnsemblXref xrefFetcher = new EnsemblXref(getDomain(), getDbSource(),
375             getEnsemblDataVersion());
376     List<DBRefEntry> xrefs = xrefFetcher.getCrossReferences(seq.getName());
377     
378     for (int i = 0, n = xrefs.size(); i < n; i++)
379     {
380         Platform.timeCheck("ESP. getxref + " + (i) + "/" + n, Platform.TIME_MARK);        
381         // BH 2019.01.25 this next method was taking 174 ms PER addition for a 266-reference example.
382         //    DBRefUtils.ensurePrimaries(seq) 
383         //        was at the end of seq.addDBRef, so executed after ever addition!
384         //        This method was moved to     seq.getPrimaryDBRefs()
385       seq.addDBRef(xrefs.get(i));
386     }
387
388     System.out.println("primaries are " + seq.getPrimaryDBRefs().toString());
389     /*
390      * and add a reference to itself
391      */
392     
393     Platform.timeCheck("ESP. getxref self ", Platform.TIME_MARK);         
394
395     DBRefEntry self = new DBRefEntry(getDbSource(), getEnsemblDataVersion(),
396             seq.getName());
397
398     Platform.timeCheck("ESP. getxref self add ", Platform.TIME_MARK);     
399
400     seq.addDBRef(self);
401     
402     Platform.timeCheck("ESP. seqprox done ", Platform.TIME_MARK);         
403
404   }
405
406   /**
407    * Fetches sequences for the list of accession ids and adds them to the
408    * alignment. Returns the extended (or created) alignment.
409    * 
410    * @param ids
411    * @param alignment
412    * @return
413    * @throws JalviewException
414    * @throws IOException
415    */
416   protected AlignmentI fetchSequences(List<String> ids,
417           AlignmentI alignment) throws JalviewException, IOException
418   {
419     if (!isEnsemblAvailable())
420     {
421       inProgress = false;
422       throw new JalviewException("ENSEMBL Rest API not available.");
423     }
424         Platform.timeCheck("EnsemblSeqProx.fetchSeq ", Platform.TIME_MARK);
425
426     List<SequenceI> seqs = parseSequenceJson(ids);
427     if (seqs == null)
428         return alignment;
429
430     if (seqs.isEmpty())
431     {
432       throw new IOException("No data returned for " + ids);
433     }
434
435     if (seqs.size() != ids.size())
436     {
437       System.out.println(String.format(
438               "Only retrieved %d sequences for %d query strings",
439               seqs.size(), ids.size()));
440     }
441
442     if (!seqs.isEmpty())
443     {
444       AlignmentI seqal = new Alignment(
445               seqs.toArray(new SequenceI[seqs.size()]));
446       for (SequenceI seq : seqs)
447       {
448         if (seq.getDescription() == null)
449         {
450           seq.setDescription(getDbName());
451         }
452         String name = seq.getName();
453         if (ids.contains(name)
454                 || ids.contains(name.replace("ENSP", "ENST")))
455         {
456           // TODO JAL-3077 use true accession version in dbref
457           DBRefEntry dbref = DBRefUtils.parseToDbRef(seq, getDbSource(),
458                   getEnsemblDataVersion(), name);
459           seq.addDBRef(dbref);
460         }
461       }
462       if (alignment == null)
463       {
464         alignment = seqal;
465       }
466       else
467       {
468         alignment.append(seqal);
469       }
470     }
471     return alignment;
472   }
473
474   /**
475    * Parses a JSON response for a single sequence ID query
476    * 
477    * @param br
478    * @return a single jalview.datamodel.Sequence
479    * @see http://rest.ensembl.org/documentation/info/sequence_id
480    */
481   @SuppressWarnings("unchecked")
482   protected List<SequenceI> parseSequenceJson(List<String> ids)
483   {
484     List<SequenceI> result = new ArrayList<>();
485     try
486     {
487       /*
488        * for now, assumes only one sequence returned; refactor if needed
489        * in future to handle a JSONArray with more than one
490        */
491         
492         Platform.timeCheck("ENS seqproxy", Platform.TIME_MARK);
493       Map<String, Object> val = (Map<String, Object>) getJSON(null, ids, -1, MODE_MAP, null);
494       if (val == null)
495           return null;
496       Object s = val.get("desc");
497       String desc = s == null ? null : s.toString();
498       s = val.get("id");
499       String id = s == null ? null : s.toString();
500       s = val.get("seq");
501       String seq = s == null ? null : s.toString();
502       Sequence sequence = new Sequence(id, seq);
503       if (desc != null)
504       {
505         sequence.setDescription(desc);
506       }
507       // todo JAL-3077 make a DBRefEntry with true accession version
508       // s = val.get("version");
509       // String version = s == null ? "0" : s.toString();
510       // DBRefEntry dbref = new DBRefEntry(getDbSource(), version, id);
511       // sequence.addDBRef(dbref);
512       result.add(sequence);
513     } catch (ParseException | IOException e)
514     {
515       System.err.println("Error processing JSON response: " + e.toString());
516       // ignore
517     }
518         Platform.timeCheck("ENS seqproxy2", Platform.TIME_MARK);
519     return result;
520   }
521
522   /**
523    * Returns the URL for the REST call
524    * 
525    * @return
526    * @throws MalformedURLException
527    */
528   @Override
529   protected URL getUrl(List<String> ids) throws MalformedURLException
530   {
531     /*
532      * a single id is included in the URL path
533      * multiple ids go in the POST body instead
534      */
535     StringBuffer urlstring = new StringBuffer(128);
536     urlstring.append(getDomain() + "/sequence/id");
537     if (ids.size() == 1)
538     {
539       urlstring.append("/").append(ids.get(0));
540     }
541     // @see https://github.com/Ensembl/ensembl-rest/wiki/Output-formats
542     urlstring.append("?type=").append(getSourceEnsemblType().getType());
543     urlstring.append(("&Accept=application/json"));
544     urlstring.append(("&content-type=application/json"));
545
546     String objectType = getObjectType();
547     if (objectType != null)
548     {
549       urlstring.append("&").append(OBJECT_TYPE).append("=")
550               .append(objectType);
551     }
552
553     URL url = new URL(urlstring.toString());
554     return url;
555   }
556
557   /**
558    * Override this method to specify object_type request parameter
559    * 
560    * @return
561    */
562   protected String getObjectType()
563   {
564     return null;
565   }
566
567   /**
568    * A sequence/id POST request currently allows up to 50 queries
569    * 
570    * @see http://rest.ensembl.org/documentation/info/sequence_id_post
571    */
572   @Override
573   public int getMaximumQueryCount()
574   {
575     return 50;
576   }
577
578   @Override
579   protected boolean useGetRequest()
580   {
581     return false;
582   }
583
584   /**
585    * 
586    * @return the configured sequence return type for this source
587    */
588   protected abstract EnsemblSeqType getSourceEnsemblType();
589
590   /**
591    * Returns a list of [start, end] genomic ranges corresponding to the sequence
592    * being retrieved.
593    * 
594    * The correspondence between the frames of reference is made by locating
595    * those features on the genomic sequence which identify the retrieved
596    * sequence. Specifically
597    * <ul>
598    * <li>genomic sequence is identified by "transcript" features with
599    * ID=transcript:transcriptId</li>
600    * <li>cdna sequence is identified by "exon" features with
601    * Parent=transcript:transcriptId</li>
602    * <li>cds sequence is identified by "CDS" features with
603    * Parent=transcript:transcriptId</li>
604    * </ul>
605    * 
606    * The returned ranges are sorted to run forwards (for positive strand) or
607    * backwards (for negative strand). Aborts and returns null if both positive
608    * and negative strand are found (this should not normally happen).
609    * 
610    * @param sourceSequence
611    * @param accId
612    * @param start
613    *          the start position of the sequence we are mapping to
614    * @return
615    */
616   protected MapList getGenomicRangesFromFeatures(SequenceI sourceSequence,
617           String accId, int start)
618   {
619     List<SequenceFeature> sfs = getIdentifyingFeatures(sourceSequence,
620             accId);
621     if (sfs.isEmpty())
622     {
623       return null;
624     }
625
626     /*
627      * generously initial size for number of cds regions
628      * (worst case titin Q8WZ42 has c. 313 exons)
629      */
630     List<int[]> regions = new ArrayList<>(100);
631     int mappedLength = 0;
632     int direction = 1; // forward
633     boolean directionSet = false;
634
635     for (SequenceFeature sf : sfs)
636     {
637       int strand = sf.getStrand();
638       strand = strand == 0 ? 1 : strand; // treat unknown as forward
639
640       if (directionSet && strand != direction)
641       {
642         // abort - mix of forward and backward
643         System.err
644                 .println("Error: forward and backward strand for " + accId);
645         return null;
646       }
647       direction = strand;
648       directionSet = true;
649
650       /*
651        * add to CDS ranges, semi-sorted forwards/backwards
652        */
653       if (strand < 0)
654       {
655         regions.add(0, new int[] { sf.getEnd(), sf.getBegin() });
656       }
657       else
658       {
659         regions.add(new int[] { sf.getBegin(), sf.getEnd() });
660       }
661       mappedLength += Math.abs(sf.getEnd() - sf.getBegin() + 1);
662     }
663
664     if (regions.isEmpty())
665     {
666       System.out.println("Failed to identify target sequence for " + accId
667               + " from genomic features");
668       return null;
669     }
670
671     /*
672      * a final sort is needed since Ensembl returns CDS sorted within source
673      * (havana / ensembl_havana)
674      */
675     Collections.sort(regions, direction == 1 ? IntRangeComparator.ASCENDING
676             : IntRangeComparator.DESCENDING);
677
678     List<int[]> to = Arrays
679             .asList(new int[]
680             { start, start + mappedLength - 1 });
681
682     return new MapList(regions, to, 1, 1);
683   }
684
685   /**
686    * Answers a list of sequence features that mark positions of the genomic
687    * sequence feature which are within the sequence being retrieved. For
688    * example, an 'exon' feature whose parent is the target transcript marks the
689    * cdna positions of the transcript. For a gene sequence, this is trivially
690    * just the 'gene' feature with matching gene id.
691    * 
692    * @param seq
693    * @param accId
694    * @return
695    */
696   protected abstract List<SequenceFeature> getIdentifyingFeatures(
697           SequenceI seq, String accId);
698
699   /**
700    * Transfers the sequence feature to the target sequence, locating its start
701    * and end range based on the mapping. Features which do not overlap the
702    * target sequence are ignored.
703    * 
704    * @param sf
705    * @param targetSequence
706    * @param mapping
707    *          mapping from the sequence feature's coordinates to the target
708    *          sequence
709    * @param forwardStrand
710    */
711   protected void transferFeature(SequenceFeature sf,
712           SequenceI targetSequence, MapList mapping, boolean forwardStrand)
713   {
714     int start = sf.getBegin();
715     int end = sf.getEnd();
716     int[] mappedRange = mapping.locateInTo(start, end);
717
718     if (mappedRange != null)
719     {
720       String group = sf.getFeatureGroup();
721       if (".".equals(group))
722       {
723         group = getDbSource();
724       }
725       int newBegin = Math.min(mappedRange[0], mappedRange[1]);
726       int newEnd = Math.max(mappedRange[0], mappedRange[1]);
727       SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
728               group, sf.getScore());
729       targetSequence.addSequenceFeature(copy);
730
731       /*
732        * for sequence_variant on reverse strand, have to convert the allele
733        * values to their complements
734        */
735       if (!forwardStrand && SequenceOntologyFactory.getInstance()
736               .isA(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT))
737       {
738         reverseComplementAlleles(copy);
739       }
740     }
741   }
742
743   /**
744    * Change the 'alleles' value of a feature by converting to complementary
745    * bases, and also update the feature description to match
746    * 
747    * @param sf
748    */
749   static void reverseComplementAlleles(SequenceFeature sf)
750   {
751     final String alleles = (String) sf.getValue(Gff3Helper.ALLELES);
752     if (alleles == null)
753     {
754       return;
755     }
756     StringBuilder complement = new StringBuilder(alleles.length());
757     for (String allele : alleles.split(","))
758     {
759       reverseComplementAllele(complement, allele);
760     }
761     String comp = complement.toString();
762     sf.setValue(Gff3Helper.ALLELES, comp);
763     sf.setDescription(comp);
764
765     /*
766      * replace value of "alleles=" in sf.ATTRIBUTES as well
767      * so 'output as GFF' shows reverse complement alleles
768      */
769     String atts = sf.getAttributes();
770     if (atts != null)
771     {
772       atts = atts.replace(Gff3Helper.ALLELES + "=" + alleles,
773               Gff3Helper.ALLELES + "=" + comp);
774       sf.setAttributes(atts);
775     }
776   }
777
778   /**
779    * Makes the 'reverse complement' of the given allele and appends it to the
780    * buffer, after a comma separator if not the first
781    * 
782    * @param complement
783    * @param allele
784    */
785   static void reverseComplementAllele(StringBuilder complement,
786           String allele)
787   {
788     if (complement.length() > 0)
789     {
790       complement.append(",");
791     }
792
793     /*
794      * some 'alleles' are actually descriptive terms 
795      * e.g. HGMD_MUTATION, PhenCode_variation
796      * - we don't want to 'reverse complement' these
797      */
798     if (!Comparison.isNucleotideSequence(allele, true))
799     {
800       complement.append(allele);
801     }
802     else
803     {
804       for (int i = allele.length() - 1; i >= 0; i--)
805       {
806         complement.append(Dna.getComplement(allele.charAt(i)));
807       }
808     }
809   }
810
811   /**
812    * Transfers features from sourceSequence to targetSequence
813    * 
814    * @param accessionId
815    * @param sourceSequence
816    * @param targetSequence
817    * @return true if any features were transferred, else false
818    */
819   protected boolean transferFeatures(String accessionId,
820           SequenceI sourceSequence, SequenceI targetSequence)
821   {
822     if (sourceSequence == null || targetSequence == null)
823     {
824       return false;
825     }
826
827 //    long start = System.currentTimeMillis();
828     List<SequenceFeature> sfs = sourceSequence.getFeatures()
829             .getPositionalFeatures();
830     MapList mapping = getGenomicRangesFromFeatures(sourceSequence,
831             accessionId, targetSequence.getStart());
832     if (mapping == null)
833     {
834       return false;
835     }
836
837     boolean result = transferFeatures(sfs, targetSequence, mapping,
838             accessionId);
839 //    System.out.println("transferFeatures (" + (sfs.size()) + " --> "
840 //            + targetSequence.getFeatures().getFeatureCount(true) + ") to "
841 //            + targetSequence.getName() + " took "
842 //            + (System.currentTimeMillis() - start) + "ms");
843     return result;
844   }
845
846   /**
847    * Transfer features to the target sequence. The start/end positions are
848    * converted using the mapping. Features which do not overlap are ignored.
849    * Features whose parent is not the specified identifier are also ignored.
850    * 
851    * @param sfs
852    * @param targetSequence
853    * @param mapping
854    * @param parentId
855    * @return
856    */
857   protected boolean transferFeatures(List<SequenceFeature> sfs,
858           SequenceI targetSequence, MapList mapping, String parentId)
859   {
860     final boolean forwardStrand = mapping.isFromForwardStrand();
861
862     /*
863      * sort features by start position (which corresponds to end
864      * position descending if reverse strand) so as to add them in
865      * 'forwards' order to the target sequence
866      */
867     SequenceFeatures.sortFeatures(sfs, forwardStrand);
868
869     boolean transferred = false;
870     for (SequenceFeature sf : sfs)
871     {
872       if (retainFeature(sf, parentId))
873       {
874         transferFeature(sf, targetSequence, mapping, forwardStrand);
875         transferred = true;
876       }
877     }
878     return transferred;
879   }
880
881   /**
882    * Answers true if the feature type is one we want to keep for the sequence.
883    * Some features are only retrieved in order to identify the sequence range,
884    * and may then be discarded as redundant information (e.g. "CDS" feature for
885    * a CDS sequence).
886    */
887   @SuppressWarnings("unused")
888   protected boolean retainFeature(SequenceFeature sf, String accessionId)
889   {
890     return true; // override as required
891   }
892
893   /**
894    * Answers true if the feature has a Parent which refers to the given
895    * accession id, or if the feature has no parent. Answers false if the
896    * feature's Parent is for a different accession id.
897    * 
898    * @param sf
899    * @param identifier
900    * @return
901    */
902   protected boolean featureMayBelong(SequenceFeature sf, String identifier)
903   {
904     String parent = (String) sf.getValue(PARENT);
905     if (parent != null
906             && !parent.equalsIgnoreCase(identifier))
907     {
908       // this genomic feature belongs to a different transcript
909       return false;
910     }
911     return true;
912   }
913
914   /**
915    * Answers a short description of the sequence fetcher
916    */
917   @Override
918   public String getDescription()
919   {
920     return "Ensembl " + getSourceEnsemblType().getType()
921             + " sequence with variant features";
922   }
923
924   /**
925    * Returns a (possibly empty) list of features on the sequence which have the
926    * specified sequence ontology term (or a sub-type of it), and the given
927    * identifier as parent
928    * 
929    * @param sequence
930    * @param term
931    * @param parentId
932    * @return
933    */
934   protected List<SequenceFeature> findFeatures(SequenceI sequence,
935           String term, String parentId)
936   {
937     List<SequenceFeature> result = new ArrayList<>();
938
939     List<SequenceFeature> sfs = sequence.getFeatures()
940             .getFeaturesByOntology(term);
941     for (SequenceFeature sf : sfs)
942     {
943       String parent = (String) sf.getValue(PARENT);
944       if (parent != null && parent.equalsIgnoreCase(parentId))
945       {
946         result.add(sf);
947       }
948     }
949
950     return result;
951   }
952
953   /**
954    * Answers true if the feature type is either 'NMD_transcript_variant' or
955    * 'transcript' (or one of its sub-types in the Sequence Ontology). This is
956    * because NMD_transcript_variant behaves like 'transcript' in Ensembl
957    * although strictly speaking it is not (it is a sub-type of
958    * sequence_variant).
959    * <p>
960    * (This test was needed when fetching transcript features as GFF. As we are
961    * now fetching as JSON, all features have type 'transcript' so the check for
962    * NMD_transcript_variant is redundant. Left in for any future case arising.)
963    * 
964    * @param featureType
965    * @return
966    */
967   public static boolean isTranscript(String featureType)
968   {
969     return SequenceOntologyI.NMD_TRANSCRIPT_VARIANT.equals(featureType)
970             || SequenceOntologyFactory.getInstance().isA(featureType,
971                     SequenceOntologyI.TRANSCRIPT);
972   }
973 }