X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjalview%2Fext%2Fensembl%2FEnsemblSeqProxy.java;h=7b448fd8a6389becba113ec2e2b40d6bb997b959;hb=ef84f77ebe6c73e67e8ec789b02f41891715ebdd;hp=e986ba832d7994e38bab3469f25a48262389ef26;hpb=b03ec66ae6238b44bd20d2403d1157cadc5f0e01;p=jalview.git diff --git a/src/jalview/ext/ensembl/EnsemblSeqProxy.java b/src/jalview/ext/ensembl/EnsemblSeqProxy.java index e986ba8..7b448fd 100644 --- a/src/jalview/ext/ensembl/EnsemblSeqProxy.java +++ b/src/jalview/ext/ensembl/EnsemblSeqProxy.java @@ -1,54 +1,93 @@ +/* + * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) + * Copyright (C) $$Year-Rel$$ The Jalview Authors + * + * This file is part of Jalview. + * + * Jalview is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Jalview is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Jalview. If not, see . + * The Jalview Authors are detailed in the 'AUTHORS' file. + */ package jalview.ext.ensembl; +import jalview.analysis.AlignmentUtils; +import jalview.analysis.Dna; +import jalview.bin.Cache; import jalview.datamodel.Alignment; import jalview.datamodel.AlignmentI; import jalview.datamodel.DBRefEntry; import jalview.datamodel.DBRefSource; import jalview.datamodel.Mapping; +import jalview.datamodel.Sequence; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.SequenceFeatures; import jalview.exceptions.JalviewException; -import jalview.io.FastaFile; -import jalview.io.FileParse; -import jalview.io.gff.SequenceOntology; +import jalview.io.gff.Gff3Helper; +import jalview.io.gff.SequenceOntologyFactory; +import jalview.io.gff.SequenceOntologyI; +import jalview.util.Comparison; import jalview.util.DBRefUtils; +import jalview.util.IntRangeComparator; import jalview.util.MapList; +import java.io.BufferedReader; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.List; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + /** * Base class for Ensembl sequence fetchers * + * @see http://rest.ensembl.org/documentation/info/sequence_id * @author gmcarstairs */ public abstract class EnsemblSeqProxy extends EnsemblRestClient { + protected static final String NAME = "Name"; + + protected static final String DESCRIPTION = "description"; + + /* + * enum for 'type' parameter to the /sequence REST service + */ public enum EnsemblSeqType { /** - * type=genomic for the full dna including introns + * type=genomic to fetch full dna including introns */ GENOMIC("genomic"), /** - * type=cdna for transcribed dna including UTRs + * type=cdna to fetch coding dna including UTRs */ CDNA("cdna"), /** - * type=cds for coding dna excluding UTRs + * type=cds to fetch coding dna excluding UTRs */ CDS("cds"), /** - * type=protein for the peptide product sequence + * type=protein to fetch peptide product sequence */ PROTEIN("protein"); @@ -71,35 +110,24 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient } /** - * A comparator to sort ranges into ascending start position order + * Default constructor (to use rest.ensembl.org) */ - private class RangeSorter implements Comparator + public EnsemblSeqProxy() { - boolean forwards; - - RangeSorter(boolean forward) - { - forwards = forward; - } - - @Override - public int compare(int[] o1, int[] o2) - { - return (forwards ? 1 : -1) * Integer.compare(o1[0], o2[0]); - } - - }; + super(); + } /** - * Constructor + * Constructor given the target domain to fetch data from */ - public EnsemblSeqProxy() + public EnsemblSeqProxy(String d) { + super(d); } /** * Makes the sequence queries to Ensembl's REST service and returns an - * alignment consisting of the returned sequences + * alignment consisting of the returned sequences. */ @Override public AlignmentI getSequenceRecords(String query) throws Exception @@ -108,7 +136,8 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient // danger: accession separator used as a regex here, a string elsewhere // in this case it is ok (it is just a space), but (e.g.) '\' would not be - List allIds = Arrays.asList(query.split(getAccessionSeparator())); + List allIds = Arrays + .asList(query.split(getAccessionSeparator())); AlignmentI alignment = null; inProgress = true; @@ -131,26 +160,30 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient + " chunks. Unexpected problem (" + r.getLocalizedMessage() + ")"; System.err.println(msg); - if (alignment != null) - { - break; // return what we got - } - else - { - throw new JalviewException(msg, r); - } + r.printStackTrace(); + break; } } + if (alignment == null) + { + return null; + } + /* - * fetch and transfer genomic sequence features + * fetch and transfer genomic sequence features, + * fetch protein product and add as cross-reference */ for (String accId : allIds) { addFeaturesAndProduct(accId, alignment); } - inProgress = false; + for (SequenceI seq : alignment.getSequences()) + { + getCrossReferences(seq); + } + return alignment; } @@ -165,34 +198,46 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient */ protected void addFeaturesAndProduct(String accId, AlignmentI alignment) { + if (alignment == null) + { + return; + } + try { /* - * get 'dummy' genomic sequence with exon, cds and variation features + * get 'dummy' genomic sequence with gene, transcript, + * exon, cds and variation features */ - EnsemblOverlap gffFetcher = new EnsemblOverlap(); + SequenceI genomicSequence = null; + EnsemblFeatures gffFetcher = new EnsemblFeatures(getDomain()); EnsemblFeatureType[] features = getFeaturesToFetch(); AlignmentI geneFeatures = gffFetcher.getSequenceRecords(accId, features); - if (geneFeatures.getHeight() > 0) + if (geneFeatures != null && geneFeatures.getHeight() > 0) + { + genomicSequence = geneFeatures.getSequenceAt(0); + } + if (genomicSequence != null) { /* * transfer features to the query sequence */ - SequenceI genomicSequence = geneFeatures.getSequenceAt(0); - SequenceI querySeq = alignment.findName(accId); - transferFeatures(accId, genomicSequence, querySeq); + SequenceI querySeq = alignment.findName(accId, true); + if (transferFeatures(accId, genomicSequence, querySeq)) + { - /* - * fetch and map protein product, and add it as a cross-reference - * of the retrieved sequence - */ - addProteinProduct(querySeq); + /* + * fetch and map protein product, and add it as a cross-reference + * of the retrieved sequence + */ + addProteinProduct(querySeq); + } } } catch (IOException e) { - System.err.println("Error transferring Ensembl features: " - + e.getMessage()); + System.err.println( + "Error transferring Ensembl features: " + e.getMessage()); } } @@ -215,10 +260,11 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient String accId = querySeq.getName(); try { - AlignmentI protein = new EnsemblProtein().getSequenceRecords(accId); + AlignmentI protein = new EnsemblProtein(getDomain()) + .getSequenceRecords(accId); if (protein == null || protein.getHeight() == 0) { - System.out.println("Failed to retrieve protein for " + accId); + System.out.println("No protein product found for " + accId); return; } SequenceI proteinSeq = protein.getSequenceAt(0); @@ -229,13 +275,65 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient proteinSeq.createDatasetSequence(); querySeq.createDatasetSequence(); - MapList mapList = mapCdsToProtein(querySeq, proteinSeq); + MapList mapList = AlignmentUtils.mapCdsToProtein(querySeq, + proteinSeq); if (mapList != null) { - Mapping map = new Mapping(proteinSeq.getDatasetSequence(), mapList); - DBRefEntry dbr = new DBRefEntry(getDbSource(), getDbVersion(), - accId, map); + // clunky: ensure Uniprot xref if we have one is on mapped sequence + SequenceI ds = proteinSeq.getDatasetSequence(); + // TODO: Verify ensp primary ref is on proteinSeq.getDatasetSequence() + Mapping map = new Mapping(ds, mapList); + DBRefEntry dbr = new DBRefEntry(getDbSource(), + getEnsemblDataVersion(), proteinSeq.getName(), map); querySeq.getDatasetSequence().addDBRef(dbr); + DBRefEntry[] uprots = DBRefUtils.selectRefs(ds.getDBRefs(), + new String[] + { DBRefSource.UNIPROT }); + DBRefEntry[] upxrefs = DBRefUtils.selectRefs(querySeq.getDBRefs(), + new String[] + { DBRefSource.UNIPROT }); + if (uprots != null) + { + for (DBRefEntry up : uprots) + { + // locate local uniprot ref and map + List upx = DBRefUtils.searchRefs(upxrefs, + up.getAccessionId()); + DBRefEntry upxref; + if (upx.size() != 0) + { + upxref = upx.get(0); + + if (upx.size() > 1) + { + Cache.log.warn( + "Implementation issue - multiple uniprot acc on product sequence."); + } + } + else + { + upxref = new DBRefEntry(DBRefSource.UNIPROT, + getEnsemblDataVersion(), up.getAccessionId()); + } + + Mapping newMap = new Mapping(ds, mapList); + upxref.setVersion(getEnsemblDataVersion()); + upxref.setMap(newMap); + if (upx.size() == 0) + { + // add the new uniprot ref + querySeq.getDatasetSequence().addDBRef(upxref); + } + + } + } + + /* + * copy exon features to protein, compute peptide variants from dna + * variants and add as features on the protein sequence ta-da + */ + AlignmentUtils.computeProteinFeatures(querySeq, proteinSeq, + mapList); } } catch (Exception e) { @@ -246,55 +344,31 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient } /** - * Returns a mapping from dna to protein by inspecting sequence features of - * type "CDS" on the dna. + * Get database xrefs from Ensembl, and attach them to the sequence * - * @param dnaSeq - * @param proteinSeq - * @return + * @param seq */ - protected MapList mapCdsToProtein(SequenceI dnaSeq, SequenceI proteinSeq) + protected void getCrossReferences(SequenceI seq) { - SequenceFeature[] sfs = dnaSeq.getSequenceFeatures(); - if (sfs == null) + while (seq.getDatasetSequence() != null) { - return null; + seq = seq.getDatasetSequence(); } - List ranges = new ArrayList(50); - SequenceOntology so = SequenceOntology.getInstance(); - - int mappedDnaLength = 0; - - /* - * Map CDS columns of dna to peptide. No need to worry about reverse strand - * dna here since the retrieved sequence is as transcribed (reverse - * complement for reverse strand), i.e in the same sense as the peptide. - */ - for (SequenceFeature sf : sfs) + EnsemblXref xrefFetcher = new EnsemblXref(getDomain(), getDbSource(), + getEnsemblDataVersion()); + List xrefs = xrefFetcher.getCrossReferences(seq.getName()); + for (DBRefEntry xref : xrefs) { - /* - * process a CDS feature (or a sub-type of CDS) - */ - if (so.isA(sf.getType(), SequenceOntology.CDS)) - { - ranges.add(new int[] { sf.getBegin(), sf.getEnd() }); - mappedDnaLength += Math.abs(sf.getEnd() - sf.getBegin()) + 1; - } + seq.addDBRef(xref); } - int proteinLength = proteinSeq.getLength(); - List proteinRange = new ArrayList(); - proteinRange.add(new int[] { 1, proteinLength }); /* - * dna length should map to protein (or protein minus stop codon) + * and add a reference to itself */ - if (mappedDnaLength == 3 * proteinLength - || mappedDnaLength == 3 * (proteinLength + 1)) - { - return new MapList(ranges, proteinRange, 3, 1); - } - return null; + DBRefEntry self = new DBRefEntry(getDbSource(), getEnsemblDataVersion(), + seq.getName()); + seq.addDBRef(self); } /** @@ -307,43 +381,52 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient * @throws JalviewException * @throws IOException */ - protected AlignmentI fetchSequences(List ids, AlignmentI alignment) - throws JalviewException, IOException + protected AlignmentI fetchSequences(List ids, + AlignmentI alignment) throws JalviewException, IOException { if (!isEnsemblAvailable()) { inProgress = false; throw new JalviewException("ENSEMBL Rest API not available."); } - FileParse fp = getSequenceReader(ids); - FastaFile fr = new FastaFile(fp); - if (fr.hasWarningMessage()) + BufferedReader br = getSequenceReader(ids); + if (br == null) { - System.out.println(String.format( - "Warning when retrieving %d ids %s\n%s", ids.size(), - ids.toString(), fr.getWarningMessage())); + return alignment; } - else if (fr.getSeqs().size() != ids.size()) + + List seqs = parseSequenceJson(br); + + if (seqs.isEmpty()) + { + throw new IOException("No data returned for " + ids); + } + + if (seqs.size() != ids.size()) { System.out.println(String.format( - "Only retrieved %d sequences for %d query strings", fr - .getSeqs().size(), ids.size())); + "Only retrieved %d sequences for %d query strings", + seqs.size(), ids.size())); } - if (fr.getSeqs().size() > 0) + + if (!seqs.isEmpty()) { AlignmentI seqal = new Alignment( - fr.getSeqsAsArray()); - for (SequenceI sq:seqal.getSequences()) + seqs.toArray(new SequenceI[seqs.size()])); + for (SequenceI seq : seqs) { - if (sq.getDescription() == null) + if (seq.getDescription() == null) { - sq.setDescription(getDbName()); + seq.setDescription(getDbName()); } - String name = sq.getName(); + String name = seq.getName(); if (ids.contains(name) || ids.contains(name.replace("ENSP", "ENST"))) { - DBRefUtils.parseToDbRef(sq, DBRefSource.ENSEMBL, "0", name); + // TODO JAL-3077 use true accession version in dbref + DBRefEntry dbref = DBRefUtils.parseToDbRef(seq, getDbSource(), + getEnsemblDataVersion(), name); + seq.addDBRef(dbref); } } if (alignment == null) @@ -359,6 +442,49 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient } /** + * Parses a JSON response for a single sequence ID query + * + * @param br + * @return a single jalview.datamodel.Sequence + * @see http://rest.ensembl.org/documentation/info/sequence_id + */ + protected List parseSequenceJson(BufferedReader br) + { + JSONParser jp = new JSONParser(); + List result = new ArrayList<>(); + try + { + /* + * for now, assumes only one sequence returned; refactor if needed + * in future to handle a JSONArray with more than one + */ + final JSONObject val = (JSONObject) jp.parse(br); + Object s = val.get("desc"); + String desc = s == null ? null : s.toString(); + s = val.get("id"); + String id = s == null ? null : s.toString(); + s = val.get("seq"); + String seq = s == null ? null : s.toString(); + Sequence sequence = new Sequence(id, seq); + if (desc != null) + { + sequence.setDescription(desc); + } + // todo JAL-3077 make a DBRefEntry with true accession version + // s = val.get("version"); + // String version = s == null ? "0" : s.toString(); + // DBRefEntry dbref = new DBRefEntry(getDbSource(), version, id); + // sequence.addDBRef(dbref); + result.add(sequence); + } catch (ParseException | IOException e) + { + System.err.println("Error processing JSON response: " + e.toString()); + // ignore + } + return result; + } + + /** * Returns the URL for the REST call * * @return @@ -367,19 +493,43 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient @Override protected URL getUrl(List ids) throws MalformedURLException { - // ids are not used - they go in the POST body instead + /* + * a single id is included in the URL path + * multiple ids go in the POST body instead + */ StringBuffer urlstring = new StringBuffer(128); - urlstring.append(SEQUENCE_ID_URL); - + urlstring.append(getDomain() + "/sequence/id"); + if (ids.size() == 1) + { + urlstring.append("/").append(ids.get(0)); + } // @see https://github.com/Ensembl/ensembl-rest/wiki/Output-formats urlstring.append("?type=").append(getSourceEnsemblType().getType()); - urlstring.append(("&Accept=text/x-fasta")); + urlstring.append(("&Accept=application/json")); + urlstring.append(("&Content-Type=application/json")); + + String objectType = getObjectType(); + if (objectType != null) + { + urlstring.append("&").append(OBJECT_TYPE).append("=") + .append(objectType); + } URL url = new URL(urlstring.toString()); return url; } /** + * Override this method to specify object_type request parameter + * + * @return + */ + protected String getObjectType() + { + return null; + } + + /** * A sequence/id POST request currently allows up to 50 queries * * @see http://rest.ensembl.org/documentation/info/sequence_id_post @@ -391,23 +541,11 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient } @Override - public boolean useGetRequest() + protected boolean useGetRequest() { return false; } - @Override - public String getRequestMimeType() - { - return "application/json"; - } - - @Override - public String getResponseMimeType() - { - return "text/x-fasta"; - } - /** * * @return the configured sequence return type for this source @@ -434,112 +572,205 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient * backwards (for negative strand). Aborts and returns null if both positive * and negative strand are found (this should not normally happen). * - * @param sfs + * @param sourceSequence * @param accId + * @param start + * the start position of the sequence we are mapping to * @return */ - protected MapList getGenomicRanges(SequenceFeature[] sfs, String accId) + protected MapList getGenomicRangesFromFeatures(SequenceI sourceSequence, + String accId, int start) { + List sfs = getIdentifyingFeatures(sourceSequence, + accId); + if (sfs.isEmpty()) + { + return null; + } + /* - * generously size for initial number of cds regions + * generously initial size for number of cds regions * (worst case titin Q8WZ42 has c. 313 exons) */ - List regions = new ArrayList(100); + List regions = new ArrayList<>(100); int mappedLength = 0; int direction = 1; // forward boolean directionSet = false; - + for (SequenceFeature sf : sfs) { + int strand = sf.getStrand(); + strand = strand == 0 ? 1 : strand; // treat unknown as forward + + if (directionSet && strand != direction) + { + // abort - mix of forward and backward + System.err + .println("Error: forward and backward strand for " + accId); + return null; + } + direction = strand; + directionSet = true; + /* - * accept the target feature type or a specialisation of it - * (e.g. coding_exon for exon) + * add to CDS ranges, semi-sorted forwards/backwards */ - if (identifiesSequence(sf, accId)) + if (strand < 0) { - int strand = sf.getStrand(); - - if (directionSet && strand != direction) - { - // abort - mix of forward and backward - System.err.println("Error: forward and backward strand for " - + accId); - return null; - } - direction = strand; - directionSet = true; - - /* - * add to CDS ranges, semi-sorted forwards/backwards - */ - if (strand < 0) - { - regions.add(0, new int[] { sf.getEnd(), sf.getBegin() }); - } - else - { - regions.add(new int[] { sf.getBegin(), sf.getEnd() }); - } - mappedLength += Math.abs(sf.getEnd() - sf.getBegin() + 1); - } + regions.add(0, new int[] { sf.getEnd(), sf.getBegin() }); + } + else + { + regions.add(new int[] { sf.getBegin(), sf.getEnd() }); + } + mappedLength += Math.abs(sf.getEnd() - sf.getBegin() + 1); } - + + if (regions.isEmpty()) + { + System.out.println("Failed to identify target sequence for " + accId + + " from genomic features"); + return null; + } + /* * a final sort is needed since Ensembl returns CDS sorted within source * (havana / ensembl_havana) */ - Collections.sort(regions, new RangeSorter(direction == 1)); - - List to = new ArrayList(); - to.add(new int[] { 1, mappedLength }); - + Collections.sort(regions, direction == 1 ? IntRangeComparator.ASCENDING + : IntRangeComparator.DESCENDING); + + List to = Arrays + .asList(new int[] + { start, start + mappedLength - 1 }); + return new MapList(regions, to, 1, 1); } /** - * Returns true if the sequence feature identifies positions of the genomic - * sequence feature which are within the sequence being retrieved. + * Answers a list of sequence features that mark positions of the genomic + * sequence feature which are within the sequence being retrieved. For + * example, an 'exon' feature whose parent is the target transcript marks the + * cdna positions of the transcript. For a gene sequence, this is trivially + * just the 'gene' feature with matching gene id. * - * @param sf + * @param seq * @param accId * @return */ - protected abstract boolean identifiesSequence(SequenceFeature sf, - String accId); + protected abstract List getIdentifyingFeatures( + SequenceI seq, String accId); /** - * Transfers the sequence feature to the target sequence, adjusting its start - * and end range based on the 'overlap' ranges. Features which do not overlap - * the target sequence are ignored, as are features with a parent other than - * the target sequence id. + * Transfers the sequence feature to the target sequence, locating its start + * and end range based on the mapping. Features which do not overlap the + * target sequence are ignored. * * @param sf * @param targetSequence - * @param overlap + * @param mapping + * mapping from the sequence feature's coordinates to the target + * sequence + * @param forwardStrand */ protected void transferFeature(SequenceFeature sf, - SequenceI targetSequence, MapList overlap) + SequenceI targetSequence, MapList mapping, boolean forwardStrand) { - String parent = (String) sf.getValue("Parent"); - if (parent != null && !parent.contains(targetSequence.getName())) - { - // this genomic feature belongs to a different transcript - return; - } - int start = sf.getBegin(); int end = sf.getEnd(); - int[] mappedRange = overlap.locateInTo(start, end); - + int[] mappedRange = mapping.locateInTo(start, end); + if (mappedRange != null) { - SequenceFeature copy = new SequenceFeature(sf); - int offset = targetSequence.getStart() - 1; - copy.setBegin(offset + Math.min(mappedRange[0], mappedRange[1])); - copy.setEnd(offset + Math.max(mappedRange[0], mappedRange[1])); + String group = sf.getFeatureGroup(); + if (".".equals(group)) + { + group = getDbSource(); + } + int newBegin = Math.min(mappedRange[0], mappedRange[1]); + int newEnd = Math.max(mappedRange[0], mappedRange[1]); + SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd, + group, sf.getScore()); targetSequence.addSequenceFeature(copy); + + /* + * for sequence_variant on reverse strand, have to convert the allele + * values to their complements + */ + if (!forwardStrand && SequenceOntologyFactory.getInstance() + .isA(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT)) + { + reverseComplementAlleles(copy); + } + } + } + + /** + * Change the 'alleles' value of a feature by converting to complementary + * bases, and also update the feature description to match + * + * @param sf + */ + static void reverseComplementAlleles(SequenceFeature sf) + { + final String alleles = (String) sf.getValue(Gff3Helper.ALLELES); + if (alleles == null) + { + return; + } + StringBuilder complement = new StringBuilder(alleles.length()); + for (String allele : alleles.split(",")) + { + reverseComplementAllele(complement, allele); + } + String comp = complement.toString(); + sf.setValue(Gff3Helper.ALLELES, comp); + sf.setDescription(comp); + + /* + * replace value of "alleles=" in sf.ATTRIBUTES as well + * so 'output as GFF' shows reverse complement alleles + */ + String atts = sf.getAttributes(); + if (atts != null) + { + atts = atts.replace(Gff3Helper.ALLELES + "=" + alleles, + Gff3Helper.ALLELES + "=" + comp); + sf.setAttributes(atts); + } + } + + /** + * Makes the 'reverse complement' of the given allele and appends it to the + * buffer, after a comma separator if not the first + * + * @param complement + * @param allele + */ + static void reverseComplementAllele(StringBuilder complement, + String allele) + { + if (complement.length() > 0) + { + complement.append(","); + } + + /* + * some 'alleles' are actually descriptive terms + * e.g. HGMD_MUTATION, PhenCode_variation + * - we don't want to 'reverse complement' these + */ + if (!Comparison.isNucleotideSequence(allele, true)) + { + complement.append(allele); + } + else + { + for (int i = allele.length() - 1; i >= 0; i--) + { + complement.append(Dna.getComplement(allele.charAt(i))); + } } - } /** @@ -548,51 +779,154 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient * @param accessionId * @param sourceSequence * @param targetSequence + * @return true if any features were transferred, else false */ - protected void transferFeatures(String accessionId, + protected boolean transferFeatures(String accessionId, SequenceI sourceSequence, SequenceI targetSequence) { if (sourceSequence == null || targetSequence == null) { - return; + return false; } - SequenceFeature[] sfs = sourceSequence.getSequenceFeatures(); - MapList overlap = getGenomicRanges(sfs, accessionId); +// long start = System.currentTimeMillis(); + List sfs = sourceSequence.getFeatures() + .getPositionalFeatures(); + MapList mapping = getGenomicRangesFromFeatures(sourceSequence, + accessionId, targetSequence.getStart()); + if (mapping == null) + { + return false; + } + + boolean result = transferFeatures(sfs, targetSequence, mapping, + accessionId); +// System.out.println("transferFeatures (" + (sfs.size()) + " --> " +// + targetSequence.getFeatures().getFeatureCount(true) + ") to " +// + targetSequence.getName() + " took " +// + (System.currentTimeMillis() - start) + "ms"); + return result; + } - final boolean forwardStrand = overlap.isFromForwardStrand(); + /** + * Transfer features to the target sequence. The start/end positions are + * converted using the mapping. Features which do not overlap are ignored. + * Features whose parent is not the specified identifier are also ignored. + * + * @param sfs + * @param targetSequence + * @param mapping + * @param parentId + * @return + */ + protected boolean transferFeatures(List sfs, + SequenceI targetSequence, MapList mapping, String parentId) + { + final boolean forwardStrand = mapping.isFromForwardStrand(); /* - * sort features by start position (descending if reverse strand) - * before transferring (in forwards order) to the target sequence + * sort features by start position (which corresponds to end + * position descending if reverse strand) so as to add them in + * 'forwards' order to the target sequence */ - Arrays.sort(sfs, new Comparator() + SequenceFeatures.sortFeatures(sfs, forwardStrand); + + boolean transferred = false; + for (SequenceFeature sf : sfs) { - @Override - public int compare(SequenceFeature o1, SequenceFeature o2) + if (retainFeature(sf, parentId)) { - int c = Integer.compare(o1.getBegin(), o2.getBegin()); - return forwardStrand ? c : -c; + transferFeature(sf, targetSequence, mapping, forwardStrand); + transferred = true; } - }); + } + return transferred; + } + + /** + * Answers true if the feature type is one we want to keep for the sequence. + * Some features are only retrieved in order to identify the sequence range, + * and may then be discarded as redundant information (e.g. "CDS" feature for + * a CDS sequence). + */ + @SuppressWarnings("unused") + protected boolean retainFeature(SequenceFeature sf, String accessionId) + { + return true; // override as required + } + + /** + * Answers true if the feature has a Parent which refers to the given + * accession id, or if the feature has no parent. Answers false if the + * feature's Parent is for a different accession id. + * + * @param sf + * @param identifier + * @return + */ + protected boolean featureMayBelong(SequenceFeature sf, String identifier) + { + String parent = (String) sf.getValue(PARENT); + // using contains to allow for prefix "gene:", "transcript:" etc + if (parent != null + && !parent.toUpperCase().contains(identifier.toUpperCase())) + { + // this genomic feature belongs to a different transcript + return false; + } + return true; + } + + @Override + public String getDescription() + { + return "Ensembl " + getSourceEnsemblType().getType() + + " sequence with variant features"; + } + /** + * Returns a (possibly empty) list of features on the sequence which have the + * specified sequence ontology term (or a sub-type of it), and the given + * identifier as parent + * + * @param sequence + * @param term + * @param parentId + * @return + */ + protected List findFeatures(SequenceI sequence, + String term, String parentId) + { + List result = new ArrayList<>(); + + List sfs = sequence.getFeatures() + .getFeaturesByOntology(term); for (SequenceFeature sf : sfs) { - if (retainFeature(sf.getType())) + String parent = (String) sf.getValue(PARENT); + if (parent != null && parent.equalsIgnoreCase(parentId)) { - transferFeature(sf, targetSequence, overlap); + result.add(sf); } } + + return result; } /** - * Answers true if the feature type is one to attach to the retrieved sequence + * Answers true if the feature type is either 'NMD_transcript_variant' or + * 'transcript' or one of its sub-types in the Sequence Ontology. This is + * needed because NMD_transcript_variant behaves like 'transcript' in Ensembl + * although strictly speaking it is not (it is a sub-type of + * sequence_variant). * - * @param type + * @param featureType * @return */ - protected boolean retainFeature(@SuppressWarnings("unused") String type) + public static boolean isTranscript(String featureType) { - return true; // default is to keep all + return SequenceOntologyI.NMD_TRANSCRIPT_VARIANT.equals(featureType) + || SequenceOntologyFactory.getInstance().isA(featureType, + SequenceOntologyI.TRANSCRIPT); } }