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