From: gmungoc Date: Tue, 8 Aug 2017 10:30:04 +0000 (+0100) Subject: Merge branch 'develop' into features/JAL-2446NCList X-Git-Tag: Release_2_10_3b1~159 X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=624302131158c667f0a94971cf81817bf5fa94f7;hp=894a45573741b1b3a4a5d478499824ba653c3ae0;p=jalview.git Merge branch 'develop' into features/JAL-2446NCList --- diff --git a/examples/example.json b/examples/example.json index 5f6e784..93c19db 100644 --- a/examples/example.json +++ b/examples/example.json @@ -1 +1 @@ -{"seqs":[{"name":"FER_CAPAN/3-34","start":3,"svid":"1.0","end":34,"id":"1665704504","seq":"SVSATMISTSFMPRKPAVTSL-KPIPNVGE--ALF","order":1},{"name":"FER1_SOLLC/3-34","start":3,"svid":"1.0","end":34,"id":"1003594867","seq":"SISGTMISTSFLPRKPAVTSL-KAISNVGE--ALF","order":2},{"name":"Q93XJ9_SOLTU/3-34","start":3,"svid":"1.0","end":34,"id":"1332961135","seq":"SISGTMISTSFLPRKPVVTSL-KAISNVGE--ALF","order":3},{"name":"FER1_PEA/6-37","start":6,"svid":"1.0","end":37,"id":"1335040546","seq":"ALYGTAVSTSFLRTQPMPMSV-TTTKAFSN--GFL","order":4},{"name":"Q7XA98_TRIPR/6-39","start":6,"svid":"1.0","end":39,"id":"1777084554","seq":"ALYGTAVSTSFMRRQPVPMSV-ATTTTTKAFPSGF","order":5},{"name":"FER_TOCH/3-34","start":3,"svid":"1.0","end":34,"id":"823528539","seq":"FILGTMISKSFLFRKPAVTSL-KAISNVGE--ALF","order":6}],"appSettings":{"globalColorScheme":"zappo","webStartUrl":"www.jalview.org/services/launchApp","application":"Jalview","hiddenSeqs":"823528539","showSeqFeatures":"true","version":"2.9","hiddenCols":"32-33;34-34"},"seqGroups":[{"displayText":true,"startRes":21,"groupName":"JGroup:1883305585","endRes":29,"colourText":false,"sequenceRefs":["1003594867","1332961135","1335040546","1777084554"],"svid":"1.0","showNonconserved":false,"colourScheme":"Zappo","displayBoxes":true}],"alignAnnotation":[{"svid":"1.0","annotations":[{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"}],"description":"New description","label":"Secondary Structure"}],"svid":"1.0","seqFeatures":[{"fillColor":"#7d1633","score":0,"sequenceRef":"1332961135","featureGroup":"Jalview","svid":"1.0","description":"desciption","xStart":3,"xEnd":13,"type":"feature_x"},{"fillColor":"#7d1633","score":0,"sequenceRef":"1335040546","featureGroup":"Jalview","svid":"1.0","description":"desciption","xStart":3,"xEnd":13,"type":"feature_x"},{"fillColor":"#7d1633","score":0,"sequenceRef":"1777084554","featureGroup":"Jalview","svid":"1.0","description":"desciption","xStart":3,"xEnd":13,"type":"feature_x"}]} \ No newline at end of file +{"seqs":[{"name":"FER_CAPAN/3-34","start":3,"svid":"1.0","end":34,"id":"1665704504","seq":"SVSATMISTSFMPRKPAVTSL-KPIPNVGE--ALF","order":1},{"name":"FER1_SOLLC/3-34","start":3,"svid":"1.0","end":34,"id":"1003594867","seq":"SISGTMISTSFLPRKPAVTSL-KAISNVGE--ALF","order":2},{"name":"Q93XJ9_SOLTU/3-34","start":3,"svid":"1.0","end":34,"id":"1332961135","seq":"SISGTMISTSFLPRKPVVTSL-KAISNVGE--ALF","order":3},{"name":"FER1_PEA/6-37","start":6,"svid":"1.0","end":37,"id":"1335040546","seq":"ALYGTAVSTSFLRTQPMPMSV-TTTKAFSN--GFL","order":4},{"name":"Q7XA98_TRIPR/6-39","start":6,"svid":"1.0","end":39,"id":"1777084554","seq":"ALYGTAVSTSFMRRQPVPMSV-ATTTTTKAFPSGF","order":5},{"name":"FER_TOCH/3-34","start":3,"svid":"1.0","end":34,"id":"823528539","seq":"FILGTMISKSFLFRKPAVTSL-KAISNVGE--ALF","order":6}],"appSettings":{"globalColorScheme":"zappo","webStartUrl":"www.jalview.org/services/launchApp","application":"Jalview","hiddenSeqs":"823528539","showSeqFeatures":"true","version":"2.9","hiddenCols":"32-33;34-34"},"seqGroups":[{"displayText":true,"startRes":21,"groupName":"JGroup:1883305585","endRes":29,"colourText":false,"sequenceRefs":["1003594867","1332961135","1335040546","1777084554"],"svid":"1.0","showNonconserved":false,"colourScheme":"Zappo","displayBoxes":true}],"alignAnnotation":[{"svid":"1.0","annotations":[{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"}],"description":"New description","label":"Secondary Structure"}],"svid":"1.0","seqFeatures":[{"fillColor":"#7d1633","score":0,"otherDetails":{"status":"+"},"sequenceRef":"1332961135","featureGroup":"Pfam","svid":"1.0","description":"My description","xStart":0,"xEnd":0,"type":"Domain"},{"fillColor":"#7d1633","score":0,"sequenceRef":"1332961135","featureGroup":"Jalview","svid":"1.0","description":"theDesc","xStart":3,"xEnd":13,"type":"feature_x"},{"fillColor":"#7d1633","score":0,"sequenceRef":"1335040546","featureGroup":"Jalview","svid":"1.0","description":"theDesc","xStart":3,"xEnd":13,"type":"feature_x"},{"fillColor":"#7d1633","score":0,"sequenceRef":"1777084554","featureGroup":"Jalview","svid":"1.0","description":"theDesc","xStart":3,"xEnd":13,"type":"feature_x"}]} \ No newline at end of file diff --git a/resources/lang/Messages.properties b/resources/lang/Messages.properties index 162f10f..c9d2e63 100644 --- a/resources/lang/Messages.properties +++ b/resources/lang/Messages.properties @@ -913,7 +913,6 @@ label.as_percentage = As Percentage error.not_implemented = Not implemented error.no_such_method_as_clone1_for = No such method as clone1 for {0} error.null_from_clone1 = Null from clone1! -error.implementation_error_sortbyfeature = Implementation Error - sortByFeature method must be one of FEATURE_SCORE, FEATURE_LABEL or FEATURE_DENSITY. error.not_yet_implemented = Not yet implemented error.unknown_type_dna_or_pep = Unknown Type {0} - dna or pep are the only allowed values. error.implementation_error_dont_know_threshold_annotationcolourgradient = Implementation error: don't know about threshold setting for current AnnotationColourGradient. diff --git a/resources/lang/Messages_es.properties b/resources/lang/Messages_es.properties index 8385142..64f06c8 100644 --- a/resources/lang/Messages_es.properties +++ b/resources/lang/Messages_es.properties @@ -838,7 +838,6 @@ label.as_percentage = Como Porcentaje error.not_implemented = No implementado error.no_such_method_as_clone1_for = No existe ese método como un clone1 de {0} error.null_from_clone1 = Nulo de clone1! -error.implementation_error_sortbyfeature = Error de implementación - sortByFeature debe ser uno de FEATURE_SCORE, FEATURE_LABEL o FEATURE_DENSITY. error.not_yet_implemented = No se ha implementado todavía error.unknown_type_dna_or_pep = Tipo desconocido {0} - dna o pep son los únicos valores permitidos error.implementation_error_dont_know_threshold_annotationcolourgradient = Error de implementación: no se conoce el valor umbral para el AnnotationColourGradient actual. diff --git a/resources/uniprot_mapping.xml b/resources/uniprot_mapping.xml index 4a981ad..6344d1e 100755 --- a/resources/uniprot_mapping.xml +++ b/resources/uniprot_mapping.xml @@ -18,39 +18,39 @@ * The Jalview Authors are detailed in the 'AUTHORS' file. --> - + - + - + - - + + - + - + - + @@ -71,7 +71,7 @@ - + diff --git a/src/MCview/PDBChain.java b/src/MCview/PDBChain.java index ba93046..8285d88 100755 --- a/src/MCview/PDBChain.java +++ b/src/MCview/PDBChain.java @@ -167,15 +167,14 @@ public class PDBChain } /** - * copy over the RESNUM seqfeatures from the internal chain sequence to the + * Copies over the RESNUM seqfeatures from the internal chain sequence to the * mapped sequence * * @param seq * @param status * The Status of the transferred annotation - * @return the features added to sq (or its dataset) */ - public SequenceFeature[] transferRESNUMFeatures(SequenceI seq, + public void transferRESNUMFeatures(SequenceI seq, String status) { SequenceI sq = seq; @@ -184,10 +183,11 @@ public class PDBChain sq = sq.getDatasetSequence(); if (sq == sequence) { - return null; + return; } } - /** + + /* * Remove any existing features for this chain if they exist ? * SequenceFeature[] seqsfeatures=seq.getSequenceFeatures(); int * totfeat=seqsfeatures.length; // Remove any features for this exact chain @@ -197,21 +197,19 @@ public class PDBChain { status = PDBChain.IEASTATUS; } - SequenceFeature[] features = sequence.getSequenceFeatures(); - if (features == null) - { - return null; - } - for (int i = 0; i < features.length; i++) + + List features = sequence.getSequenceFeatures(); + for (SequenceFeature feature : features) { - if (features[i].getFeatureGroup() != null - && features[i].getFeatureGroup().equals(pdbid)) + if (feature.getFeatureGroup() != null + && feature.getFeatureGroup().equals(pdbid)) { - SequenceFeature tx = new SequenceFeature(features[i]); - tx.setBegin(1 + residues.elementAt(tx.getBegin() - offset).atoms - .elementAt(0).alignmentMapping); - tx.setEnd(1 + residues.elementAt(tx.getEnd() - offset).atoms - .elementAt(0).alignmentMapping); + int newBegin = 1 + residues.elementAt(feature.getBegin() - offset).atoms + .elementAt(0).alignmentMapping; + int newEnd = 1 + residues.elementAt(feature.getEnd() - offset).atoms + .elementAt(0).alignmentMapping; + SequenceFeature tx = new SequenceFeature(feature, newBegin, newEnd, + feature.getFeatureGroup(), feature.getScore()); tx.setStatus(status + ((tx.getStatus() == null || tx.getStatus().length() == 0) ? "" : ":" + tx.getStatus())); @@ -221,7 +219,6 @@ public class PDBChain } } } - return features; } /** @@ -353,25 +350,25 @@ public class PDBChain && !residues.isEmpty() && residues.lastElement().atoms.get(0).resNumber == currAtom.resNumber) { - SequenceFeature sf = new SequenceFeature("INSERTION", - currAtom.resName + ":" + currAtom.resNumIns + " " + pdbid - + id, "", offset + count - 1, offset + count - 1, - "PDB_INS"); + String desc = currAtom.resName + ":" + currAtom.resNumIns + " " + + pdbid + id; + SequenceFeature sf = new SequenceFeature("INSERTION", desc, offset + + count - 1, offset + count - 1, "PDB_INS"); resFeatures.addElement(sf); residues.lastElement().atoms.addAll(resAtoms); } else { - // Make a new Residue object with the new atoms vector residues.addElement(new Residue(resAtoms, resNumber - 1, count)); Residue tmpres = residues.lastElement(); Atom tmpat = tmpres.atoms.get(0); // Make A new SequenceFeature for the current residue numbering - SequenceFeature sf = new SequenceFeature(RESNUM_FEATURE, tmpat.resName - + ":" + tmpat.resNumIns + " " + pdbid + id, "", offset - + count, offset + count, pdbid); + String desc = tmpat.resName + + ":" + tmpat.resNumIns + " " + pdbid + id; + SequenceFeature sf = new SequenceFeature(RESNUM_FEATURE, desc, + offset + count, offset + count, pdbid); resFeatures.addElement(sf); resAnnotation.addElement(new Annotation(tmpat.tfactor)); // Keep totting up the sequence diff --git a/src/jalview/analysis/AAFrequency.java b/src/jalview/analysis/AAFrequency.java index b806355..a792d24 100755 --- a/src/jalview/analysis/AAFrequency.java +++ b/src/jalview/analysis/AAFrequency.java @@ -151,10 +151,9 @@ public class AAFrequency .println("WARNING: Consensus skipping null sequence - possible race condition."); continue; } - char[] seq = sequences[row].getSequence(); - if (seq.length > column) + if (sequences[row].getLength() > column) { - char c = seq[column]; + char c = sequences[row].getCharAt(column); residueCounts.add(c); if (Comparison.isNucleotide(c)) { diff --git a/src/jalview/analysis/AlignmentSorter.java b/src/jalview/analysis/AlignmentSorter.java index 693e794..9943a22 100755 --- a/src/jalview/analysis/AlignmentSorter.java +++ b/src/jalview/analysis/AlignmentSorter.java @@ -29,11 +29,11 @@ import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceGroup; import jalview.datamodel.SequenceI; import jalview.datamodel.SequenceNode; -import jalview.util.MessageManager; import jalview.util.QuickSort; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; import java.util.List; /** @@ -53,7 +53,7 @@ import java.util.List; */ public class AlignmentSorter { - /** + /* * todo: refactor searches to follow a basic pattern: (search property, last * search state, current sort direction) */ @@ -71,19 +71,18 @@ public class AlignmentSorter static boolean sortTreeAscending = true; - /** - * last Annotation Label used by sortByScore + /* + * last Annotation Label used for sort by Annotation score */ - private static String lastSortByScore; - - private static boolean sortByScoreAscending = true; + private static String lastSortByAnnotation; - /** - * compact representation of last arguments to SortByFeatureScore + /* + * string hash of last arguments to sortByFeature + * (sort order toggles if this is unchanged between sorts) */ - private static String lastSortByFeatureScore; + private static String sortByFeatureCriteria; - private static boolean sortByFeatureScoreAscending = true; + private static boolean sortByFeatureAscending = true; private static boolean sortLengthAscending; @@ -659,9 +658,9 @@ public class AlignmentSorter } jalview.util.QuickSort.sort(scores, seqs); - if (lastSortByScore != scoreLabel) + if (lastSortByAnnotation != scoreLabel) { - lastSortByScore = scoreLabel; + lastSortByAnnotation = scoreLabel; setOrder(alignment, seqs); } else @@ -682,35 +681,6 @@ public class AlignmentSorter public static String FEATURE_DENSITY = "density"; - /** - * sort the alignment using the features on each sequence found between start - * and stop with the given featureLabel (and optional group qualifier) - * - * @param featureLabel - * (may not be null) - * @param groupLabel - * (may be null) - * @param start - * (-1 to include non-positional features) - * @param stop - * (-1 to only sort on non-positional features) - * @param alignment - * - aligned sequences containing features - * @param method - * - one of the string constants FEATURE_SCORE, FEATURE_LABEL, - * FEATURE_DENSITY - */ - public static void sortByFeature(String featureLabel, String groupLabel, - int start, int stop, AlignmentI alignment, String method) - { - sortByFeature( - featureLabel == null ? null - : Arrays.asList(new String[] { featureLabel }), - groupLabel == null ? null : Arrays - .asList(new String[] { groupLabel }), start, stop, - alignment, method); - } - private static boolean containsIgnoreCase(final String lab, final List labs) { @@ -732,51 +702,41 @@ public class AlignmentSorter return false; } - public static void sortByFeature(List featureLabels, - List groupLabels, int start, int stop, + /** + * Sort sequences by feature score or density, optionally restricted by + * feature types, feature groups, or alignment start/end positions. + *

+ * If the sort is repeated for the same combination of types and groups, sort + * order is reversed. + * + * @param featureTypes + * a list of feature types to include (or null for all) + * @param groups + * a list of feature groups to include (or null for all) + * @param startCol + * start column position to include (base zero) + * @param endCol + * end column position to include (base zero) + * @param alignment + * the alignment to be sorted + * @param method + * either "average_score" or "density" ("text" not yet implemented) + */ + public static void sortByFeature(List featureTypes, + List groups, final int startCol, final int endCol, AlignmentI alignment, String method) { if (method != FEATURE_SCORE && method != FEATURE_LABEL && method != FEATURE_DENSITY) { - throw new Error( - MessageManager - .getString("error.implementation_error_sortbyfeature")); - } - - boolean ignoreScore = method != FEATURE_SCORE; - StringBuffer scoreLabel = new StringBuffer(); - scoreLabel.append(start + stop + method); - // This doesn't quite work yet - we'd like to have a canonical ordering that - // can be preserved from call to call - if (featureLabels != null) - { - for (String label : featureLabels) - { - scoreLabel.append(label); - } - } - if (groupLabels != null) - { - for (String label : groupLabels) - { - scoreLabel.append(label); - } + String msg = String + .format("Implementation Error - sortByFeature method must be either '%s' or '%s'", + FEATURE_SCORE, FEATURE_DENSITY); + System.err.println(msg); + return; } - /* - * if resorting the same feature, toggle sort order - */ - if (lastSortByFeatureScore == null - || !scoreLabel.toString().equals(lastSortByFeatureScore)) - { - sortByFeatureScoreAscending = true; - } - else - { - sortByFeatureScoreAscending = !sortByFeatureScoreAscending; - } - lastSortByFeatureScore = scoreLabel.toString(); + flipFeatureSortIfUnchanged(method, featureTypes, groups, startCol, endCol); SequenceI[] seqs = alignment.getSequencesArray(); @@ -785,58 +745,44 @@ public class AlignmentSorter int hasScores = 0; // number of scores present on set double[] scores = new double[seqs.length]; int[] seqScores = new int[seqs.length]; - Object[] feats = new Object[seqs.length]; - double min = 0, max = 0; + Object[][] feats = new Object[seqs.length][]; + double min = 0d; + double max = 0d; + for (int i = 0; i < seqs.length; i++) { - SequenceFeature[] sf = seqs[i].getSequenceFeatures(); - if (sf == null) - { - sf = new SequenceFeature[0]; - } - else - { - SequenceFeature[] tmp = new SequenceFeature[sf.length]; - for (int s = 0; s < tmp.length; s++) - { - tmp[s] = sf[s]; - } - sf = tmp; - } - int sstart = (start == -1) ? start : seqs[i].findPosition(start); - int sstop = (stop == -1) ? stop : seqs[i].findPosition(stop); + /* + * get sequence residues overlapping column region + * and features for residue positions and specified types + */ + String[] types = featureTypes == null ? null : featureTypes + .toArray(new String[featureTypes.size()]); + List sfs = seqs[i].findFeatures(startCol + 1, + endCol + 1, types); + seqScores[i] = 0; scores[i] = 0.0; - int n = sf.length; - for (int f = 0; f < sf.length; f++) + + Iterator it = sfs.listIterator(); + while (it.hasNext()) { - // filter for selection criteria - SequenceFeature feature = sf[f]; + SequenceFeature sf = it.next(); /* - * double-check feature overlaps columns (JAL-2544) - * (could avoid this with a findPositions(fromCol, toCol) method) - * findIndex returns base 1 column values, startCol/endCol are base 0 + * accept all features with null or empty group, otherwise + * check group is one of the currently visible groups */ - boolean noOverlap = seqs[i].findIndex(feature.getBegin()) > stop + 1 - || seqs[i].findIndex(feature.getEnd()) < start + 1; - boolean skipFeatureType = featureLabels != null - && !AlignmentSorter.containsIgnoreCase(feature.type, - featureLabels); - boolean skipFeatureGroup = groupLabels != null - && (feature.getFeatureGroup() != null && !AlignmentSorter - .containsIgnoreCase(feature.getFeatureGroup(), - groupLabels)); - if (noOverlap || skipFeatureType || skipFeatureGroup) + String featureGroup = sf.getFeatureGroup(); + if (groups != null && featureGroup != null + && !"".equals(featureGroup) + && !groups.contains(featureGroup)) { - // forget about this feature - sf[f] = null; - n--; + it.remove(); } else { - // or, also take a look at the scores if necessary. - if (!ignoreScore && !Float.isNaN(feature.getScore())) + float score = sf.getScore(); + if (FEATURE_SCORE.equals(method) && !Float.isNaN(score)) { if (seqScores[i] == 0) { @@ -844,33 +790,26 @@ public class AlignmentSorter } seqScores[i]++; hasScore[i] = true; - scores[i] += feature.getScore(); // take the first instance of this - // score. + scores[i] += score; + // take the first instance of this score // ?? } } } - SequenceFeature[] fs; - feats[i] = fs = new SequenceFeature[n]; - if (n > 0) + + feats[i] = sfs.toArray(new SequenceFeature[sfs.size()]); + if (!sfs.isEmpty()) { - n = 0; - for (int f = 0; f < sf.length; f++) - { - if (sf[f] != null) - { - ((SequenceFeature[]) feats[i])[n++] = sf[f]; - } - } if (method == FEATURE_LABEL) { - // order the labels by alphabet - String[] labs = new String[fs.length]; - for (int l = 0; l < labs.length; l++) + // order the labels by alphabet (not yet implemented) + String[] labs = new String[sfs.size()]; + for (int l = 0; l < sfs.size(); l++) { - labs[l] = (fs[l].getDescription() != null ? fs[l] - .getDescription() : fs[l].getType()); + SequenceFeature sf = sfs.get(l); + String description = sf.getDescription(); + labs[l] = (description != null ? description : sf.getType()); } - QuickSort.sort(labs, ((Object[]) feats[i])); + QuickSort.sort(labs, feats[i]); } } if (hasScore[i]) @@ -880,23 +819,18 @@ public class AlignmentSorter // update the score bounds. if (hasScores == 1) { - max = min = scores[i]; + min = scores[i]; + max = min; } else { - if (max < scores[i]) - { - max = scores[i]; - } - if (min > scores[i]) - { - min = scores[i]; - } + max = Math.max(max, scores[i]); + min = Math.min(min, scores[i]); } } } - if (method == FEATURE_SCORE) + if (FEATURE_SCORE.equals(method)) { if (hasScores == 0) { @@ -921,9 +855,9 @@ public class AlignmentSorter } } } - QuickSort.sortByDouble(scores, seqs, sortByFeatureScoreAscending); + QuickSort.sortByDouble(scores, seqs, sortByFeatureAscending); } - else if (method == FEATURE_DENSITY) + else if (FEATURE_DENSITY.equals(method)) { for (int i = 0; i < seqs.length; i++) { @@ -933,18 +867,53 @@ public class AlignmentSorter // System.err.println("Sorting on Density: seq "+seqs[i].getName()+ // " Feats: "+featureCount+" Score : "+scores[i]); } - QuickSort.sortByDouble(scores, seqs, sortByFeatureScoreAscending); + QuickSort.sortByDouble(scores, seqs, sortByFeatureAscending); } - else + + setOrder(alignment, seqs); + } + + /** + * Builds a string hash of criteria for sorting, and if unchanged from last + * time, reverse the sort order + * + * @param method + * @param featureTypes + * @param groups + * @param startCol + * @param endCol + */ + protected static void flipFeatureSortIfUnchanged(String method, + List featureTypes, List groups, + final int startCol, final int endCol) + { + StringBuilder sb = new StringBuilder(64); + sb.append(startCol).append(method).append(endCol); + if (featureTypes != null) { - if (method == FEATURE_LABEL) - { - throw new Error( - MessageManager.getString("error.not_yet_implemented")); - } + Collections.sort(featureTypes); + sb.append(featureTypes.toString()); } + if (groups != null) + { + Collections.sort(groups); + sb.append(groups.toString()); + } + String scoreCriteria = sb.toString(); - setOrder(alignment, seqs); + /* + * if resorting on the same criteria, toggle sort order + */ + if (sortByFeatureCriteria == null + || !scoreCriteria.equals(sortByFeatureCriteria)) + { + sortByFeatureAscending = true; + } + else + { + sortByFeatureAscending = !sortByFeatureAscending; + } + sortByFeatureCriteria = scoreCriteria; } } diff --git a/src/jalview/analysis/AlignmentUtils.java b/src/jalview/analysis/AlignmentUtils.java index 232cb5d..b65096c 100644 --- a/src/jalview/analysis/AlignmentUtils.java +++ b/src/jalview/analysis/AlignmentUtils.java @@ -35,14 +35,14 @@ import jalview.datamodel.Sequence; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceGroup; import jalview.datamodel.SequenceI; -import jalview.io.gff.SequenceOntologyFactory; +import jalview.datamodel.features.SequenceFeatures; import jalview.io.gff.SequenceOntologyI; import jalview.schemes.ResidueProperties; import jalview.util.Comparison; import jalview.util.DBRefUtils; +import jalview.util.IntRangeComparator; import jalview.util.MapList; import jalview.util.MappingUtils; -import jalview.util.RangeComparator; import jalview.util.StringUtils; import java.io.UnsupportedEncodingException; @@ -51,7 +51,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -651,15 +650,16 @@ public class AlignmentUtils int toOffset = alignTo.getStart() - 1; int sourceGapMappedLength = 0; boolean inExon = false; - final char[] thisSeq = alignTo.getSequence(); - final char[] thatAligned = alignFrom.getSequence(); - StringBuilder thisAligned = new StringBuilder(2 * thisSeq.length); + final int toLength = alignTo.getLength(); + final int fromLength = alignFrom.getLength(); + StringBuilder thisAligned = new StringBuilder(2 * toLength); /* * Traverse the 'model' aligned sequence */ - for (char sourceChar : thatAligned) + for (int i = 0; i < fromLength; i++) { + char sourceChar = alignFrom.getCharAt(i); if (sourceChar == sourceGap) { sourceGapMappedLength += ratio; @@ -699,9 +699,9 @@ public class AlignmentUtils */ int intronLength = 0; while (basesWritten + toOffset < mappedCodonEnd - && thisSeqPos < thisSeq.length) + && thisSeqPos < toLength) { - final char c = thisSeq[thisSeqPos++]; + final char c = alignTo.getCharAt(thisSeqPos++); if (c != myGapChar) { basesWritten++; @@ -727,7 +727,7 @@ public class AlignmentUtils int gapsToAdd = calculateGapsToInsert(preserveMappedGaps, preserveUnmappedGaps, sourceGapMappedLength, inExon, trailingCopiedGap.length(), intronLength, startOfCodon); - for (int i = 0; i < gapsToAdd; i++) + for (int k = 0; k < gapsToAdd; k++) { thisAligned.append(myGapChar); } @@ -755,9 +755,9 @@ public class AlignmentUtils * At end of model aligned sequence. Copy any remaining target sequence, optionally * including (intron) gaps. */ - while (thisSeqPos < thisSeq.length) + while (thisSeqPos < toLength) { - final char c = thisSeq[thisSeqPos++]; + final char c = alignTo.getCharAt(thisSeqPos++); if (c != myGapChar || preserveUnmappedGaps) { thisAligned.append(c); @@ -947,7 +947,7 @@ public class AlignmentUtils SequenceI peptide = mapping.findAlignedSequence(cdsSeq, protein); if (peptide != null) { - int peptideLength = peptide.getLength(); + final int peptideLength = peptide.getLength(); Mapping map = mapping.getMappingBetween(cdsSeq, peptide); if (map != null) { @@ -956,7 +956,7 @@ public class AlignmentUtils { mapList = mapList.getInverse(); } - int cdsLength = cdsDss.getLength(); + final int cdsLength = cdsDss.getLength(); int mappedFromLength = MappingUtils.getLength(mapList .getFromRanges()); int mappedToLength = MappingUtils @@ -984,14 +984,15 @@ public class AlignmentUtils * walk over the aligned peptide sequence and insert mapped * codons for residues in the aligned cds sequence */ - char[] alignedPeptide = peptide.getSequence(); - char[] nucleotides = cdsDss.getSequence(); int copiedBases = 0; int cdsStart = cdsDss.getStart(); int proteinPos = peptide.getStart() - 1; int cdsCol = 0; - for (char residue : alignedPeptide) + + for (int col = 0; col < peptideLength; col++) { + char residue = peptide.getCharAt(col); + if (Comparison.isGap(residue)) { cdsCol += CODON_LENGTH; @@ -1009,7 +1010,7 @@ public class AlignmentUtils { for (int j = codon[0]; j <= codon[1]; j++) { - char mappedBase = nucleotides[j - cdsStart]; + char mappedBase = cdsDss.getCharAt(j - cdsStart); alignedCds[cdsCol++] = mappedBase; copiedBases++; } @@ -1021,7 +1022,7 @@ public class AlignmentUtils * append stop codon if not mapped from protein, * closing it up to the end of the mapped sequence */ - if (copiedBases == nucleotides.length - CODON_LENGTH) + if (copiedBases == cdsLength - CODON_LENGTH) { for (int i = alignedCds.length - 1; i >= 0; i--) { @@ -1031,9 +1032,9 @@ public class AlignmentUtils break; } } - for (int i = nucleotides.length - CODON_LENGTH; i < nucleotides.length; i++) + for (int i = cdsLength - CODON_LENGTH; i < cdsLength; i++) { - alignedCds[cdsCol++] = nucleotides[i]; + alignedCds[cdsCol++] = cdsDss.getCharAt(i); } } cdsSeq.setSequence(new String(alignedCds)); @@ -1203,21 +1204,26 @@ public class AlignmentUtils List unmappedProtein) { /* - * Prefill aligned sequences with gaps before inserting aligned protein - * residues. + * prefill peptide sequences with gaps */ int alignedWidth = alignedCodons.size(); char[] gaps = new char[alignedWidth]; Arrays.fill(gaps, protein.getGapCharacter()); - String allGaps = String.valueOf(gaps); + Map peptides = new HashMap<>(); for (SequenceI seq : protein.getSequences()) { if (!unmappedProtein.contains(seq)) { - seq.setSequence(allGaps); + peptides.put(seq, Arrays.copyOf(gaps, gaps.length)); } } + /* + * Traverse the codons left to right (as defined by CodonComparator) + * and insert peptides in each column where the sequence is mapped. + * This gives a peptide 'alignment' where residues are aligned if their + * corresponding codons occupy the same columns in the cdna alignment. + */ int column = 0; for (AlignedCodon codon : alignedCodons.keySet()) { @@ -1225,12 +1231,20 @@ public class AlignmentUtils .get(codon); for (Entry entry : columnResidues.entrySet()) { - // place translated codon at its column position in sequence - entry.getKey().getSequence()[column] = entry.getValue().product - .charAt(0); + char residue = entry.getValue().product.charAt(0); + peptides.get(entry.getKey())[column] = residue; } column++; } + + /* + * and finally set the constructed sequences + */ + for (Entry entry : peptides.entrySet()) + { + entry.getKey().setSequence(new String(entry.getValue())); + } + return 0; } @@ -2055,11 +2069,11 @@ public class AlignmentUtils * * @param fromSeq * @param toSeq + * @param mapping + * the mapping from 'fromSeq' to 'toSeq' * @param select * if not null, only features of this type are copied (including * subtypes in the Sequence Ontology) - * @param mapping - * the mapping from 'fromSeq' to 'toSeq' * @param omitting */ public static int transferFeatures(SequenceI fromSeq, SequenceI toSeq, @@ -2071,75 +2085,74 @@ public class AlignmentUtils copyTo = copyTo.getDatasetSequence(); } - SequenceOntologyI so = SequenceOntologyFactory.getInstance(); + /* + * get features, optionally restricted by an ontology term + */ + List sfs = select == null ? fromSeq.getFeatures() + .getPositionalFeatures() : fromSeq.getFeatures() + .getFeaturesByOntology(select); + int count = 0; - SequenceFeature[] sfs = fromSeq.getSequenceFeatures(); - if (sfs != null) + for (SequenceFeature sf : sfs) { - for (SequenceFeature sf : sfs) + String type = sf.getType(); + boolean omit = false; + for (String toOmit : omitting) { - String type = sf.getType(); - if (select != null && !so.isA(type, select)) - { - continue; - } - boolean omit = false; - for (String toOmit : omitting) - { - if (type.equals(toOmit)) - { - omit = true; - } - } - if (omit) + if (type.equals(toOmit)) { - continue; + omit = true; } + } + if (omit) + { + continue; + } - /* - * locate the mapped range - null if either start or end is - * not mapped (no partial overlaps are calculated) - */ - int start = sf.getBegin(); - int end = sf.getEnd(); - int[] mappedTo = mapping.locateInTo(start, end); - /* - * if whole exon range doesn't map, try interpreting it - * as 5' or 3' exon overlapping the CDS range - */ - if (mappedTo == null) - { - mappedTo = mapping.locateInTo(end, end); - if (mappedTo != null) - { - /* - * end of exon is in CDS range - 5' overlap - * to a range from the start of the peptide - */ - mappedTo[0] = 1; - } - } - if (mappedTo == null) + /* + * locate the mapped range - null if either start or end is + * not mapped (no partial overlaps are calculated) + */ + int start = sf.getBegin(); + int end = sf.getEnd(); + int[] mappedTo = mapping.locateInTo(start, end); + /* + * if whole exon range doesn't map, try interpreting it + * as 5' or 3' exon overlapping the CDS range + */ + if (mappedTo == null) + { + mappedTo = mapping.locateInTo(end, end); + if (mappedTo != null) { - mappedTo = mapping.locateInTo(start, start); - if (mappedTo != null) - { - /* - * start of exon is in CDS range - 3' overlap - * to a range up to the end of the peptide - */ - mappedTo[1] = toSeq.getLength(); - } + /* + * end of exon is in CDS range - 5' overlap + * to a range from the start of the peptide + */ + mappedTo[0] = 1; } + } + if (mappedTo == null) + { + mappedTo = mapping.locateInTo(start, start); if (mappedTo != null) { - SequenceFeature copy = new SequenceFeature(sf); - copy.setBegin(Math.min(mappedTo[0], mappedTo[1])); - copy.setEnd(Math.max(mappedTo[0], mappedTo[1])); - copyTo.addSequenceFeature(copy); - count++; + /* + * start of exon is in CDS range - 3' overlap + * to a range up to the end of the peptide + */ + mappedTo[1] = toSeq.getLength(); } } + if (mappedTo != null) + { + int newBegin = Math.min(mappedTo[0], mappedTo[1]); + int newEnd = Math.max(mappedTo[0], mappedTo[1]); + SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd, + sf.getFeatureGroup(), sf.getScore()); + copyTo.addSequenceFeature(copy); + count++; + } } return count; } @@ -2204,49 +2217,44 @@ public class AlignmentUtils public static List findCdsPositions(SequenceI dnaSeq) { List result = new ArrayList(); - SequenceFeature[] sfs = dnaSeq.getSequenceFeatures(); - if (sfs == null) + + List sfs = dnaSeq.getFeatures().getFeaturesByOntology( + SequenceOntologyI.CDS); + if (sfs.isEmpty()) { return result; } - - SequenceOntologyI so = SequenceOntologyFactory.getInstance(); + SequenceFeatures.sortFeatures(sfs, true); int startPhase = 0; for (SequenceFeature sf : sfs) { + int phase = 0; + try + { + phase = Integer.parseInt(sf.getPhase()); + } catch (NumberFormatException e) + { + // ignore + } /* - * process a CDS feature (or a sub-type of CDS) + * phase > 0 on first codon means 5' incomplete - skip to the start + * of the next codon; example ENST00000496384 */ - if (so.isA(sf.getType(), SequenceOntologyI.CDS)) + int begin = sf.getBegin(); + int end = sf.getEnd(); + if (result.isEmpty()) { - int phase = 0; - try - { - phase = Integer.parseInt(sf.getPhase()); - } catch (NumberFormatException e) - { - // ignore - } - /* - * phase > 0 on first codon means 5' incomplete - skip to the start - * of the next codon; example ENST00000496384 - */ - int begin = sf.getBegin(); - int end = sf.getEnd(); - if (result.isEmpty()) + begin += phase; + if (begin > end) { - begin += phase; - if (begin > end) - { - // shouldn't happen! - System.err - .println("Error: start phase extends beyond start CDS in " - + dnaSeq.getName()); - } + // shouldn't happen! + System.err + .println("Error: start phase extends beyond start CDS in " + + dnaSeq.getName()); } - result.add(new int[] { begin, end }); } + result.add(new int[] { begin, end }); } /* @@ -2266,7 +2274,7 @@ public class AlignmentUtils * ranges are assembled in order. Other cases should not use this method, * but instead construct an explicit mapping for CDS (e.g. EMBL parsing). */ - Collections.sort(result, new RangeComparator(true)); + Collections.sort(result, IntRangeComparator.ASCENDING); return result; } @@ -2319,24 +2327,6 @@ public class AlignmentUtils count += computePeptideVariants(peptide, peptidePos, codonVariants); } - /* - * sort to get sequence features in start position order - * - would be better to store in Sequence as a TreeSet or NCList? - */ - if (peptide.getSequenceFeatures() != null) - { - Arrays.sort(peptide.getSequenceFeatures(), - new Comparator() - { - @Override - public int compare(SequenceFeature o1, SequenceFeature o2) - { - int c = Integer.compare(o1.getBegin(), o2.getBegin()); - return c == 0 ? Integer.compare(o1.getEnd(), o2.getEnd()) - : c; - } - }); - } return count; } @@ -2464,10 +2454,9 @@ public class AlignmentUtils String trans3Char = StringUtils .toSentenceCase(ResidueProperties.aa2Triplet.get(trans)); String desc = "p." + residue3Char + peptidePos + trans3Char; - // set score to 0f so 'graduated colour' option is offered! JAL-2060 SequenceFeature sf = new SequenceFeature( SequenceOntologyI.SEQUENCE_VARIANT, desc, peptidePos, - peptidePos, 0f, var.getSource()); + peptidePos, var.getSource()); StringBuilder attributes = new StringBuilder(32); String id = (String) var.variant.getValue(ID); if (id != null) @@ -2527,10 +2516,10 @@ public class AlignmentUtils * LinkedHashMap ensures we keep the peptide features in sequence order */ LinkedHashMap[]> variants = new LinkedHashMap[]>(); - SequenceOntologyI so = SequenceOntologyFactory.getInstance(); - SequenceFeature[] dnaFeatures = dnaSeq.getSequenceFeatures(); - if (dnaFeatures == null) + List dnaFeatures = dnaSeq.getFeatures() + .getFeaturesByOntology(SequenceOntologyI.SEQUENCE_VARIANT); + if (dnaFeatures.isEmpty()) { return variants; } @@ -2550,84 +2539,80 @@ public class AlignmentUtils // not handling multi-locus variant features continue; } - if (so.isA(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT)) + int[] mapsTo = dnaToProtein.locateInTo(dnaCol, dnaCol); + if (mapsTo == null) { - int[] mapsTo = dnaToProtein.locateInTo(dnaCol, dnaCol); - if (mapsTo == null) - { - // feature doesn't lie within coding region - continue; - } - int peptidePosition = mapsTo[0]; - List[] codonVariants = variants.get(peptidePosition); - if (codonVariants == null) - { - codonVariants = new ArrayList[CODON_LENGTH]; - codonVariants[0] = new ArrayList(); - codonVariants[1] = new ArrayList(); - codonVariants[2] = new ArrayList(); - variants.put(peptidePosition, codonVariants); - } + // feature doesn't lie within coding region + continue; + } + int peptidePosition = mapsTo[0]; + List[] codonVariants = variants.get(peptidePosition); + if (codonVariants == null) + { + codonVariants = new ArrayList[CODON_LENGTH]; + codonVariants[0] = new ArrayList(); + codonVariants[1] = new ArrayList(); + codonVariants[2] = new ArrayList(); + variants.put(peptidePosition, codonVariants); + } - /* - * extract dna variants to a string array - */ - String alls = (String) sf.getValue("alleles"); - if (alls == null) - { - continue; - } - String[] alleles = alls.toUpperCase().split(","); - int i = 0; - for (String allele : alleles) - { - alleles[i++] = allele.trim(); // lose any space characters "A, G" - } + /* + * extract dna variants to a string array + */ + String alls = (String) sf.getValue("alleles"); + if (alls == null) + { + continue; + } + String[] alleles = alls.toUpperCase().split(","); + int i = 0; + for (String allele : alleles) + { + alleles[i++] = allele.trim(); // lose any space characters "A, G" + } - /* - * get this peptide's codon positions e.g. [3, 4, 5] or [4, 7, 10] - */ - int[] codon = peptidePosition == lastPeptidePostion ? lastCodon - : MappingUtils.flattenRanges(dnaToProtein.locateInFrom( - peptidePosition, peptidePosition)); - lastPeptidePostion = peptidePosition; - lastCodon = codon; + /* + * get this peptide's codon positions e.g. [3, 4, 5] or [4, 7, 10] + */ + int[] codon = peptidePosition == lastPeptidePostion ? lastCodon + : MappingUtils.flattenRanges(dnaToProtein.locateInFrom( + peptidePosition, peptidePosition)); + lastPeptidePostion = peptidePosition; + lastCodon = codon; - /* - * save nucleotide (and any variant) for each codon position - */ - for (int codonPos = 0; codonPos < CODON_LENGTH; codonPos++) + /* + * save nucleotide (and any variant) for each codon position + */ + for (int codonPos = 0; codonPos < CODON_LENGTH; codonPos++) + { + String nucleotide = String.valueOf( + dnaSeq.getCharAt(codon[codonPos] - dnaStart)).toUpperCase(); + List codonVariant = codonVariants[codonPos]; + if (codon[codonPos] == dnaCol) { - String nucleotide = String.valueOf( - dnaSeq.getCharAt(codon[codonPos] - dnaStart)) - .toUpperCase(); - List codonVariant = codonVariants[codonPos]; - if (codon[codonPos] == dnaCol) + if (!codonVariant.isEmpty() + && codonVariant.get(0).variant == null) { - if (!codonVariant.isEmpty() - && codonVariant.get(0).variant == null) - { - /* - * already recorded base value, add this variant - */ - codonVariant.get(0).variant = sf; - } - else - { - /* - * add variant with base value - */ - codonVariant.add(new DnaVariant(nucleotide, sf)); - } + /* + * already recorded base value, add this variant + */ + codonVariant.get(0).variant = sf; } - else if (codonVariant.isEmpty()) + else { /* - * record (possibly non-varying) base value + * add variant with base value */ - codonVariant.add(new DnaVariant(nucleotide)); + codonVariant.add(new DnaVariant(nucleotide, sf)); } } + else if (codonVariant.isEmpty()) + { + /* + * record (possibly non-varying) base value + */ + codonVariant.add(new DnaVariant(nucleotide)); + } } } return variants; @@ -2904,9 +2889,7 @@ public class AlignmentUtils .getInverse()); } - char[] fromChars = fromSeq.getSequence(); int toStart = seq.getStart(); - char[] toChars = seq.getSequence(); /* * traverse [start, end, start, end...] ranges in fromSeq @@ -2937,10 +2920,10 @@ public class AlignmentUtils * of the next character of the mapped-to sequence; stop when all * the characters of the range have been counted */ - while (mappedCharPos <= range[1] && fromCol <= fromChars.length + while (mappedCharPos <= range[1] && fromCol <= fromSeq.getLength() && fromCol >= 0) { - if (!Comparison.isGap(fromChars[fromCol - 1])) + if (!Comparison.isGap(fromSeq.getCharAt(fromCol - 1))) { /* * mapped from sequence has a character in this column @@ -2952,7 +2935,7 @@ public class AlignmentUtils seqsMap = new HashMap(); map.put(fromCol, seqsMap); } - seqsMap.put(seq, toChars[mappedCharPos - toStart]); + seqsMap.put(seq, seq.getCharAt(mappedCharPos - toStart)); mappedCharPos++; } fromCol += (forward ? 1 : -1); diff --git a/src/jalview/analysis/Conservation.java b/src/jalview/analysis/Conservation.java index 2b5a8f6..f94a658 100755 --- a/src/jalview/analysis/Conservation.java +++ b/src/jalview/analysis/Conservation.java @@ -734,28 +734,23 @@ public class Conservation public void completeAnnotations(AlignmentAnnotation conservation, AlignmentAnnotation quality2, int istart, int alWidth) { - char[] sequence = getConsSequence().getSequence(); - float minR; - float minG; - float minB; - float maxR; - float maxG; - float maxB; - minR = 0.3f; - minG = 0.0f; - minB = 0f; - maxR = 1.0f - minR; - maxG = 0.9f - minG; - maxB = 0f - minB; // scalable range for colouring both Conservation and - // Quality + SequenceI cons = getConsSequence(); + + /* + * colour scale for Conservation and Quality; + */ + float minR = 0.3f; + float minG = 0.0f; + float minB = 0f; + float maxR = 1.0f - minR; + float maxG = 0.9f - minG; + float maxB = 0f - minB; float min = 0f; float max = 11f; float qmin = 0f; float qmax = 0f; - char c; - if (conservation != null && conservation.annotations != null && conservation.annotations.length < alWidth) { @@ -778,7 +773,7 @@ public class Conservation { float value = 0; - c = sequence[i]; + char c = cons.getCharAt(i); if (Character.isDigit(c)) { @@ -865,8 +860,8 @@ public class Conservation */ String getTooltip(int column) { - char[] sequence = getConsSequence().getSequence(); - char val = column < sequence.length ? sequence[column] : '-'; + SequenceI cons = getConsSequence(); + char val = column < cons.getLength() ? cons.getCharAt(column) : '-'; boolean hasConservation = val != '-' && val != '0'; int consp = column - start; String tip = (hasConservation && consp > -1 && consp < consSymbs.length) ? consSymbs[consp] diff --git a/src/jalview/analysis/CrossRef.java b/src/jalview/analysis/CrossRef.java index 4ba7e41..b77e403 100644 --- a/src/jalview/analysis/CrossRef.java +++ b/src/jalview/analysis/CrossRef.java @@ -619,28 +619,25 @@ public class CrossRef * duplication (e.g. same variation from two * transcripts) */ - SequenceFeature[] sfs = ms.getSequenceFeatures(); - if (sfs != null) + List sfs = ms.getFeatures() + .getAllFeatures(); + for (SequenceFeature feat : sfs) { - for (SequenceFeature feat : sfs) + /* + * make a flyweight feature object which ignores Parent + * attribute in equality test; this avoids creating many + * otherwise duplicate exon features on genomic sequence + */ + SequenceFeature newFeature = new SequenceFeature(feat) { - /* - * make a flyweight feature object which ignores Parent - * attribute in equality test; this avoids creating many - * otherwise duplicate exon features on genomic sequence - */ - SequenceFeature newFeature = new SequenceFeature(feat) + @Override + public boolean equals(Object o) { - @Override - public boolean equals(Object o) - { - return super.equals(o, true); - } - }; - matched.addSequenceFeature(newFeature); - } + return super.equals(o, true); + } + }; + matched.addSequenceFeature(newFeature); } - } cf.addMap(retrievedSequence, map.getTo(), map.getMap()); } catch (Exception e) @@ -786,15 +783,15 @@ public class CrossRef { return false; } - char[] c1 = seq1.getSequence(); - char[] c2 = seq2.getSequence(); - if (c1.length != c2.length) + + if (seq1.getLength() != seq2.getLength()) { return false; } - for (int i = 0; i < c1.length; i++) + int length = seq1.getLength(); + for (int i = 0; i < length; i++) { - int diff = c1[i] - c2[i]; + int diff = seq1.getCharAt(i) - seq2.getCharAt(i); /* * same char or differ in case only ('a'-'A' == 32) */ diff --git a/src/jalview/analysis/Dna.java b/src/jalview/analysis/Dna.java index 799a8ed..2106dc2 100644 --- a/src/jalview/analysis/Dna.java +++ b/src/jalview/analysis/Dna.java @@ -45,7 +45,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; -import java.util.Map; public class Dna { @@ -685,7 +684,7 @@ public class Dna */ MapList map = new MapList(scontigs, new int[] { 1, resSize }, 3, 1); - transferCodedFeatures(selection, newseq, map, null, null); + transferCodedFeatures(selection, newseq, map); /* * Construct a dataset sequence for our new peptide. @@ -754,25 +753,15 @@ public class Dna /** * Given a peptide newly translated from a dna sequence, copy over and set any - * features on the peptide from the DNA. If featureTypes is null, all features - * on the dna sequence are searched (rather than just the displayed ones), and - * similarly for featureGroups. + * features on the peptide from the DNA. * * @param dna * @param pep * @param map - * @param featureTypes - * hash whose keys are the displayed feature type strings - * @param featureGroups - * hash where keys are feature groups and values are Boolean objects - * indicating if they are displayed. */ private static void transferCodedFeatures(SequenceI dna, SequenceI pep, - MapList map, Map featureTypes, - Map featureGroups) + MapList map) { - SequenceFeature[] sfs = dna.getSequenceFeatures(); - Boolean fgstate; DBRefEntry[] dnarefs = DBRefUtils.selectRefs(dna.getDBRefs(), DBRefSource.DNACODINGDBS); if (dnarefs != null) @@ -786,24 +775,15 @@ public class Dna } } } - if (sfs != null) + for (SequenceFeature sf : dna.getFeatures().getAllFeatures()) { - for (SequenceFeature sf : sfs) - { - fgstate = (featureGroups == null) ? null : featureGroups - .get(sf.featureGroup); - if ((featureTypes == null || featureTypes.containsKey(sf.getType())) - && (fgstate == null || fgstate.booleanValue())) + if (FeatureProperties.isCodingFeature(null, sf.getType())) { - if (FeatureProperties.isCodingFeature(null, sf.getType())) + // if (map.intersectsFrom(sf[f].begin, sf[f].end)) { - // if (map.intersectsFrom(sf[f].begin, sf[f].end)) - { - } } } - } } } diff --git a/src/jalview/analysis/Rna.java b/src/jalview/analysis/Rna.java index 89c5c30..3108ef3 100644 --- a/src/jalview/analysis/Rna.java +++ b/src/jalview/analysis/Rna.java @@ -31,10 +31,11 @@ import jalview.datamodel.SequenceFeature; import jalview.util.MessageManager; import java.util.ArrayList; +import java.util.HashMap; import java.util.Hashtable; import java.util.List; +import java.util.Map; import java.util.Stack; -import java.util.Vector; public class Rna { @@ -132,11 +133,11 @@ public class Rna * @return * @throw {@link WUSSParseException} */ - public static Vector getSimpleBPs(CharSequence line) + protected static List getSimpleBPs(CharSequence line) throws WUSSParseException { Hashtable> stacks = new Hashtable>(); - Vector pairs = new Vector(); + List pairs = new ArrayList(); int i = 0; while (i < line.length()) { @@ -195,25 +196,9 @@ public class Rna return pairs; } - public static SequenceFeature[] getBasePairs(List bps) - throws WUSSParseException - { - SequenceFeature[] outPairs = new SequenceFeature[bps.size()]; - for (int p = 0; p < bps.size(); p++) - { - SimpleBP bp = bps.get(p); - outPairs[p] = new SequenceFeature("RNA helix", "", "", bp.getBP5(), - bp.getBP3(), ""); - } - return outPairs; - } + - public static List getModeleBP(CharSequence line) - throws WUSSParseException - { - Vector bps = getSimpleBPs(line); - return new ArrayList(bps); - } + /** * Function to get the end position corresponding to a given start position @@ -230,88 +215,6 @@ public class Rna */ /** - * Figures out which helix each position belongs to and stores the helix - * number in the 'featureGroup' member of a SequenceFeature Based off of RALEE - * code ralee-helix-map. - * - * @param pairs - * Array of SequenceFeature (output from Rna.GetBasePairs) - */ - public static void HelixMap(SequenceFeature[] pairs) - { - - int helix = 0; // Number of helices/current helix - int lastopen = 0; // Position of last open bracket reviewed - int lastclose = 9999999; // Position of last close bracket reviewed - int i = pairs.length; // Number of pairs - - int open; // Position of an open bracket under review - int close; // Position of a close bracket under review - int j; // Counter - - Hashtable helices = new Hashtable(); - // Keep track of helix number for each position - - // Go through each base pair and assign positions a helix - for (i = 0; i < pairs.length; i++) - { - - open = pairs[i].getBegin(); - close = pairs[i].getEnd(); - - // System.out.println("open " + open + " close " + close); - // System.out.println("lastclose " + lastclose + " lastopen " + lastopen); - - // we're moving from right to left based on closing pair - /* - * catch things like <<..>>..<<..>> | - */ - if (open > lastclose) - { - helix++; - } - - /* - * catch things like <<..<<..>>..<<..>>>> | - */ - j = pairs.length - 1; - while (j >= 0) - { - int popen = pairs[j].getBegin(); - - // System.out.println("j " + j + " popen " + popen + " lastopen " - // +lastopen + " open " + open); - if ((popen < lastopen) && (popen > open)) - { - if (helices.containsValue(popen) - && ((helices.get(popen)) == helix)) - { - continue; - } - else - { - helix++; - break; - } - } - - j -= 1; - } - - // Put positions and helix information into the hashtable - helices.put(open, helix); - helices.put(close, helix); - - // Record helix as featuregroup - pairs[i].setFeatureGroup(Integer.toString(helix)); - - lastopen = open; - lastclose = close; - - } - } - - /** * Answers true if the character is a recognised symbol for RNA secondary * structure. Currently accepts a-z, A-Z, ()[]{}<>. * @@ -500,4 +403,76 @@ public class Rna return c; } } + + public static SequenceFeature[] getHelixMap(CharSequence rnaAnnotation) + throws WUSSParseException + { + List result = new ArrayList(); + + int helix = 0; // Number of helices/current helix + int lastopen = 0; // Position of last open bracket reviewed + int lastclose = 9999999; // Position of last close bracket reviewed + + Map helices = new HashMap(); + // Keep track of helix number for each position + + // Go through each base pair and assign positions a helix + List bps = getSimpleBPs(rnaAnnotation); + for (SimpleBP basePair : bps) + { + final int open = basePair.getBP5(); + final int close = basePair.getBP3(); + + // System.out.println("open " + open + " close " + close); + // System.out.println("lastclose " + lastclose + " lastopen " + lastopen); + + // we're moving from right to left based on closing pair + /* + * catch things like <<..>>..<<..>> | + */ + if (open > lastclose) + { + helix++; + } + + /* + * catch things like <<..<<..>>..<<..>>>> | + */ + int j = bps.size() - 1; + while (j >= 0) + { + int popen = bps.get(j).getBP5(); + + // System.out.println("j " + j + " popen " + popen + " lastopen " + // +lastopen + " open " + open); + if ((popen < lastopen) && (popen > open)) + { + if (helices.containsValue(popen) + && ((helices.get(popen)) == helix)) + { + continue; + } + else + { + helix++; + break; + } + } + j -= 1; + } + + // Put positions and helix information into the hashtable + helices.put(open, helix); + helices.put(close, helix); + + // Record helix as featuregroup + result.add(new SequenceFeature("RNA helix", "", open, close, + String.valueOf(helix))); + + lastopen = open; + lastclose = close; + } + + return result.toArray(new SequenceFeature[result.size()]); + } } diff --git a/src/jalview/analysis/SeqsetUtils.java b/src/jalview/analysis/SeqsetUtils.java index 21ad1cc..27b3041 100755 --- a/src/jalview/analysis/SeqsetUtils.java +++ b/src/jalview/analysis/SeqsetUtils.java @@ -27,6 +27,7 @@ import jalview.datamodel.SequenceI; import java.util.Enumeration; import java.util.Hashtable; +import java.util.List; import java.util.Vector; public class SeqsetUtils @@ -50,15 +51,11 @@ public class SeqsetUtils { sqinfo.put("Description", seq.getDescription()); } - Vector sfeat = new Vector(); - jalview.datamodel.SequenceFeature[] sfarray = seq.getSequenceFeatures(); - if (sfarray != null && sfarray.length > 0) - { - for (int i = 0; i < sfarray.length; i++) - { - sfeat.addElement(sfarray[i]); - } - } + + Vector sfeat = new Vector(); + List sfs = seq.getFeatures().getAllFeatures(); + sfeat.addAll(sfs); + if (seq.getDatasetSequence() == null) { sqinfo.put("SeqFeatures", sfeat); @@ -95,7 +92,8 @@ public class SeqsetUtils String oldname = (String) sqinfo.get("Name"); Integer start = (Integer) sqinfo.get("Start"); Integer end = (Integer) sqinfo.get("End"); - Vector sfeatures = (Vector) sqinfo.get("SeqFeatures"); + Vector sfeatures = (Vector) sqinfo + .get("SeqFeatures"); Vector pdbid = (Vector) sqinfo.get("PdbId"); String description = (String) sqinfo.get("Description"); Sequence seqds = (Sequence) sqinfo.get("datasetSequence"); @@ -118,14 +116,9 @@ public class SeqsetUtils sq.setEnd(end.intValue()); } - if ((sfeatures != null) && (sfeatures.size() > 0)) + if (sfeatures != null && !sfeatures.isEmpty()) { - SequenceFeature[] sfarray = new SequenceFeature[sfeatures.size()]; - for (int is = 0, isize = sfeatures.size(); is < isize; is++) - { - sfarray[is] = (SequenceFeature) sfeatures.elementAt(is); - } - sq.setSequenceFeatures(sfarray); + sq.setSequenceFeatures(sfeatures); } if (description != null) { diff --git a/src/jalview/analysis/scoremodels/FeatureDistanceModel.java b/src/jalview/analysis/scoremodels/FeatureDistanceModel.java index 056ecdb..ddbaf73 100644 --- a/src/jalview/analysis/scoremodels/FeatureDistanceModel.java +++ b/src/jalview/analysis/scoremodels/FeatureDistanceModel.java @@ -177,10 +177,12 @@ public class FeatureDistanceModel extends DistanceScoreModel /** * Builds and returns a map containing a (possibly empty) list (one per * SeqCigar) of visible feature types at the given column position. The map - * has no entry for sequences which are gapped at the column position. + * does not include entries for features which straddle a gapped column + * positions. * * @param seqs * @param columnPosition + * (0..) * @return */ protected Map> findFeatureTypesAtColumn( @@ -192,9 +194,12 @@ public class FeatureDistanceModel extends DistanceScoreModel int spos = seq.findPosition(columnPosition); if (spos != -1) { + /* + * position is not a gap + */ Set types = new HashSet(); - List sfs = fr.findFeaturesAtRes(seq.getRefSeq(), - spos); + List sfs = fr.findFeaturesAtResidue( + seq.getRefSeq(), spos); for (SequenceFeature sf : sfs) { types.add(sf.getType()); diff --git a/src/jalview/api/FeatureColourI.java b/src/jalview/api/FeatureColourI.java index 01eb7fa..0ded079 100644 --- a/src/jalview/api/FeatureColourI.java +++ b/src/jalview/api/FeatureColourI.java @@ -146,7 +146,9 @@ public interface FeatureColourI boolean hasThreshold(); /** - * Returns the computed colour for the given sequence feature + * Returns the computed colour for the given sequence feature. Answers null if + * the score of this feature instance is outside the range to render (if any), + * i.e. lies below or above a configured threshold. * * @param feature * @return @@ -154,17 +156,6 @@ public interface FeatureColourI Color getColor(SequenceFeature feature); /** - * Answers true if the feature has a simple colour, or is coloured by label, - * or has a graduated colour and the score of this feature instance is within - * the range to render (if any), i.e. does not lie below or above any - * threshold set. - * - * @param feature - * @return - */ - boolean isColored(SequenceFeature feature); - - /** * Update the min-max range for a graduated colour scheme * * @param min diff --git a/src/jalview/api/FeatureRenderer.java b/src/jalview/api/FeatureRenderer.java index 7123b8c..9d2d7f4 100644 --- a/src/jalview/api/FeatureRenderer.java +++ b/src/jalview/api/FeatureRenderer.java @@ -60,6 +60,7 @@ public interface FeatureRenderer * * @param sequence * @param column + * aligned column position (1..) * @param g * @return */ @@ -147,14 +148,27 @@ public interface FeatureRenderer void setGroupVisibility(String group, boolean visible); /** - * Returns features at the specified position on the given sequence. + * Returns features at the specified aligned column on the given sequence. + * Non-positional features are not included. If the column has a gap, then + * enclosing features are included (but not contact features). + * + * @param sequence + * @param column + * aligned column position (1..) + * @return + */ + List findFeaturesAtColumn(SequenceI sequence, int column); + + /** + * Returns features at the specified residue position on the given sequence. * Non-positional features are not included. * * @param sequence - * @param res + * @param resNo + * residue position (start..) * @return */ - List findFeaturesAtRes(SequenceI sequence, int res); + List findFeaturesAtResidue(SequenceI sequence, int resNo); /** * get current displayed types, in ordering of rendering (on top last) @@ -165,9 +179,9 @@ public interface FeatureRenderer List getDisplayedFeatureTypes(); /** - * get current displayed groups + * Returns a (possibly empty) list of currently visible feature groups * - * @return a (possibly empty) list of feature groups + * @return */ List getDisplayedFeatureGroups(); @@ -200,4 +214,5 @@ public interface FeatureRenderer * @return */ float getTransparency(); + } diff --git a/src/jalview/api/FeaturesDisplayedI.java b/src/jalview/api/FeaturesDisplayedI.java index 32b0565..e69785f 100644 --- a/src/jalview/api/FeaturesDisplayedI.java +++ b/src/jalview/api/FeaturesDisplayedI.java @@ -21,12 +21,15 @@ package jalview.api; import java.util.Collection; -import java.util.Iterator; +import java.util.Set; public interface FeaturesDisplayedI { - Iterator getVisibleFeatures(); + /** + * answers an unmodifiable view of the set of visible feature types + */ + Set getVisibleFeatures(); boolean isVisible(String featureType); @@ -36,6 +39,12 @@ public interface FeaturesDisplayedI void setVisible(String featureType); + /** + * Sets all the specified feature types to visible. Visibility of other + * feature types is not changed. + * + * @param featureTypes + */ void setAllVisible(Collection featureTypes); boolean isRegistered(String type); diff --git a/src/jalview/appletgui/APopupMenu.java b/src/jalview/appletgui/APopupMenu.java index 86610a2..80b2d73 100644 --- a/src/jalview/appletgui/APopupMenu.java +++ b/src/jalview/appletgui/APopupMenu.java @@ -831,7 +831,7 @@ public class APopupMenu extends java.awt.PopupMenu implements if (start <= end) { seqs.add(sg.getSequenceAt(i)); - features.add(new SequenceFeature(null, null, null, start, end, + features.add(new SequenceFeature(null, null, start, end, "Jalview")); } } @@ -843,7 +843,8 @@ public class APopupMenu extends java.awt.PopupMenu implements { ap.alignFrame.sequenceFeatures.setState(true); ap.av.setShowSequenceFeatures(true); - ap.highlightSearchResults(null); + ap.av.setSearchResults(null); // clear highlighting + ap.repaint(); // draw new/amended features } } } diff --git a/src/jalview/appletgui/AlignFrame.java b/src/jalview/appletgui/AlignFrame.java index 65d652d..b30c5ae 100644 --- a/src/jalview/appletgui/AlignFrame.java +++ b/src/jalview/appletgui/AlignFrame.java @@ -1424,6 +1424,17 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener, return null; } + private List getDisplayedFeatureGroups() + { + if (alignPanel.getFeatureRenderer() != null + && viewport.getFeaturesDisplayed() != null) + { + return alignPanel.getFeatureRenderer().getDisplayedFeatureGroups(); + + } + return null; + } + public String outputFeatures(boolean displayTextbox, String format) { String features; @@ -1431,12 +1442,14 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener, if (format.equalsIgnoreCase("Jalview")) { features = formatter.printJalviewFormat(viewport.getAlignment() - .getSequencesArray(), getDisplayedFeatureCols()); + .getSequencesArray(), getDisplayedFeatureCols(), + getDisplayedFeatureGroups(), true); } else { features = formatter.printGffFormat(viewport.getAlignment() - .getSequencesArray(), getDisplayedFeatureCols()); + .getSequencesArray(), getDisplayedFeatureCols(), + getDisplayedFeatureGroups(), true); } if (displayTextbox) diff --git a/src/jalview/appletgui/FeatureColourChooser.java b/src/jalview/appletgui/FeatureColourChooser.java index 72fa982..98ade60 100644 --- a/src/jalview/appletgui/FeatureColourChooser.java +++ b/src/jalview/appletgui/FeatureColourChooser.java @@ -42,6 +42,8 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.MouseEvent; @@ -232,6 +234,14 @@ public class FeatureColourChooser extends Panel implements ActionListener, threshold.addItem(MessageManager .getString("label.threshold_feature_below_threshold")); thresholdValue.addActionListener(this); + thresholdValue.addFocusListener(new FocusAdapter() + { + @Override + public void focusLost(FocusEvent e) + { + thresholdValue_actionPerformed(); + } + }); slider.setBackground(Color.white); slider.setEnabled(false); slider.setSize(new Dimension(93, 21)); @@ -272,19 +282,7 @@ public class FeatureColourChooser extends Panel implements ActionListener, { if (evt.getSource() == thresholdValue) { - try - { - float f = new Float(thresholdValue.getText()).floatValue(); - slider.setValue((int) (f * SCALE_FACTOR_1K)); - adjustmentValueChanged(null); - - /* - * force repaint of any Overview window or structure - */ - changeColour(true); - } catch (NumberFormatException ex) - { - } + thresholdValue_actionPerformed(); } else if (evt.getSource() == minColour) { @@ -300,6 +298,26 @@ public class FeatureColourChooser extends Panel implements ActionListener, } } + /** + * Action on input of a value for colour score threshold + */ + protected void thresholdValue_actionPerformed() + { + try + { + float f = new Float(thresholdValue.getText()).floatValue(); + slider.setValue((int) (f * SCALE_FACTOR_1K)); + adjustmentValueChanged(null); + + /* + * force repaint of any Overview window or structure + */ + changeColour(true); + } catch (NumberFormatException ex) + { + } + } + @Override public void itemStateChanged(ItemEvent evt) { diff --git a/src/jalview/appletgui/FeatureRenderer.java b/src/jalview/appletgui/FeatureRenderer.java index 3c2715f..8721ff4 100644 --- a/src/jalview/appletgui/FeatureRenderer.java +++ b/src/jalview/appletgui/FeatureRenderer.java @@ -395,11 +395,14 @@ public class FeatureRenderer extends /* * only update default type and group if we used defaults */ - String enteredType = name.getText().trim(); + final String enteredType = name.getText().trim(); + final String enteredGroup = group.getText().trim(); + final String enteredDesc = description.getText().replace('\n', ' '); + if (dialog.accept && useLastDefaults) { lastFeatureAdded = enteredType; - lastFeatureGroupAdded = group.getText().trim(); + lastFeatureGroupAdded = enteredGroup; } if (!create) @@ -407,28 +410,36 @@ public class FeatureRenderer extends SequenceFeature sf = features.get(featureIndex); if (dialog.accept) { - sf.type = enteredType; - sf.featureGroup = group.getText().trim(); - if (sf.featureGroup != null && sf.featureGroup.length() < 1) - { - sf.featureGroup = null; - } - sf.description = description.getText().replace('\n', ' '); if (!colourPanel.isGcol) { // update colour - otherwise its already done. - setColour(sf.type, new FeatureColour(colourPanel.getBackground())); + setColour(enteredType, + new FeatureColour(colourPanel.getBackground())); } + int newBegin = sf.begin; + int newEnd = sf.end; try { - sf.begin = Integer.parseInt(start.getText()); - sf.end = Integer.parseInt(end.getText()); + newBegin = Integer.parseInt(start.getText()); + newEnd = Integer.parseInt(end.getText()); } catch (NumberFormatException ex) { - // + // } - boolean typeOrGroupChanged = (!featureType.equals(sf.type) || !featureGroup - .equals(sf.featureGroup)); + + /* + * replace the feature by deleting it and adding a new one + * (to ensure integrity of SequenceFeatures data store) + */ + sequences.get(0).deleteFeature(sf); + SequenceFeature newSf = new SequenceFeature(sf, enteredType, + newBegin, newEnd, enteredGroup, sf.getScore()); + newSf.setDescription(enteredDesc); + ffile.parseDescriptionHTML(newSf, false); + // amend features dialog only updates one sequence at a time + sequences.get(0).addSequenceFeature(newSf); + boolean typeOrGroupChanged = (!featureType.equals(newSf.getType()) || !featureGroup + .equals(newSf.getFeatureGroup())); ffile.parseDescriptionHTML(sf, false); if (typeOrGroupChanged) @@ -452,12 +463,11 @@ public class FeatureRenderer extends { for (int i = 0; i < sequences.size(); i++) { - features.get(i).type = enteredType; - features.get(i).featureGroup = group.getText().trim(); - features.get(i).description = description.getText() - .replace('\n', ' '); - sequences.get(i).addSequenceFeature(features.get(i)); - ffile.parseDescriptionHTML(features.get(i), false); + SequenceFeature sf = features.get(i); + SequenceFeature sf2 = new SequenceFeature(enteredType, + enteredDesc, sf.getBegin(), sf.getEnd(), enteredGroup); + ffile.parseDescriptionHTML(sf2, false); + sequences.get(i).addSequenceFeature(sf2); } Color newColour = colourPanel.getBackground(); diff --git a/src/jalview/appletgui/FeatureSettings.java b/src/jalview/appletgui/FeatureSettings.java index 7d00afd..f6f9727 100755 --- a/src/jalview/appletgui/FeatureSettings.java +++ b/src/jalview/appletgui/FeatureSettings.java @@ -23,7 +23,7 @@ package jalview.appletgui; import jalview.api.FeatureColourI; import jalview.api.FeatureSettingsControllerI; import jalview.datamodel.AlignmentI; -import jalview.datamodel.SequenceFeature; +import jalview.datamodel.SequenceI; import jalview.util.MessageManager; import java.awt.BorderLayout; @@ -56,13 +56,12 @@ import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; +import java.util.ArrayList; import java.util.Arrays; -import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.Vector; public class FeatureSettings extends Panel implements ItemListener, MouseListener, MouseMotionListener, ActionListener, @@ -370,36 +369,38 @@ public class FeatureSettings extends Panel implements ItemListener, // Group selection states void resetTable(boolean groupsChanged) { - SequenceFeature[] tmpfeatures; - String group = null, type; - Vector visibleChecks = new Vector(); + List displayableTypes = new ArrayList(); Set foundGroups = new HashSet(); + AlignmentI alignment = av.getAlignment(); for (int i = 0; i < alignment.getHeight(); i++) { - if (alignment.getSequenceAt(i).getSequenceFeatures() == null) - { - continue; - } + SequenceI seq = alignment.getSequenceAt(i); - tmpfeatures = alignment.getSequenceAt(i).getSequenceFeatures(); - int index = 0; - while (index < tmpfeatures.length) + /* + * get the sequence's groups for positional features + * and keep track of which groups are visible + */ + Set groups = seq.getFeatures().getFeatureGroups(true); + Set visibleGroups = new HashSet(); + for (String group : groups) { - group = tmpfeatures[index].featureGroup; - foundGroups.add(group); - + // if (group == null || fr.checkGroupVisibility(group, true)) if (group == null || checkGroupState(group)) { - type = tmpfeatures[index].getType(); - if (!visibleChecks.contains(type)) - { - visibleChecks.addElement(type); - } + visibleGroups.add(group); } - index++; } + foundGroups.addAll(groups); + + /* + * get distinct feature types for visible groups + * record distinct visible types + */ + Set types = seq.getFeatures().getFeatureTypesForGroups(true, + visibleGroups.toArray(new String[visibleGroups.size()])); + displayableTypes.addAll(types); } /* @@ -416,7 +417,7 @@ public class FeatureSettings extends Panel implements ItemListener, { comps = featurePanel.getComponents(); check = (MyCheckbox) comps[i]; - if (!visibleChecks.contains(check.type)) + if (!displayableTypes.contains(check.type)) { featurePanel.remove(i); cSize--; @@ -433,24 +434,24 @@ public class FeatureSettings extends Panel implements ItemListener, { String item = rol.get(ro); - if (!visibleChecks.contains(item)) + if (!displayableTypes.contains(item)) { continue; } - visibleChecks.removeElement(item); + displayableTypes.remove(item); addCheck(false, item); } } - // now add checkboxes which should be visible, - // if they have not already been added - Enumeration en = visibleChecks.elements(); - - while (en.hasMoreElements()) + /* + * now add checkboxes which should be visible, + * if they have not already been added + */ + for (String type : displayableTypes) { - addCheck(groupsChanged, en.nextElement().toString()); + addCheck(groupsChanged, type); } featurePanel.setLayout(new GridLayout(featurePanel.getComponentCount(), diff --git a/src/jalview/appletgui/Finder.java b/src/jalview/appletgui/Finder.java index f7ebab6..41e2a64 100644 --- a/src/jalview/appletgui/Finder.java +++ b/src/jalview/appletgui/Finder.java @@ -122,9 +122,8 @@ public class Finder extends Panel implements ActionListener for (SearchResultMatchI match : searchResults.getResults()) { seqs.add(match.getSequence().getDatasetSequence()); - features.add(new SequenceFeature(searchString, - "Search Results", null, match.getStart(), match.getEnd(), - "Search Results")); + features.add(new SequenceFeature(searchString, "Search Results", + match.getStart(), match.getEnd(), "Search Results")); } if (ap.seqPanel.seqCanvas.getFeatureRenderer().amendFeatures(seqs, diff --git a/src/jalview/appletgui/IdPanel.java b/src/jalview/appletgui/IdPanel.java index 66eb053..a5c2e5a 100755 --- a/src/jalview/appletgui/IdPanel.java +++ b/src/jalview/appletgui/IdPanel.java @@ -20,7 +20,6 @@ */ package jalview.appletgui; -import jalview.datamodel.Sequence; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceGroup; import jalview.datamodel.SequenceI; @@ -57,11 +56,11 @@ public class IdPanel extends Panel implements MouseListener, UrlProviderI urlProvider = null; - public IdPanel(AlignViewport av, AlignmentPanel parent) + public IdPanel(AlignViewport viewport, AlignmentPanel parent) { - this.av = av; + this.av = viewport; alignPanel = parent; - idCanvas = new IdCanvas(av); + idCanvas = new IdCanvas(viewport); setLayout(new BorderLayout()); add(idCanvas, BorderLayout.CENTER); idCanvas.addMouseListener(this); @@ -72,12 +71,12 @@ public class IdPanel extends Panel implements MouseListener, // make a list of label,url pairs HashMap urlList = new HashMap(); - if (av.applet != null) + if (viewport.applet != null) { for (int i = 1; i < 10; i++) { - label = av.applet.getParameter("linkLabel_" + i); - url = av.applet.getParameter("linkURL_" + i); + label = viewport.applet.getParameter("linkLabel_" + i); + url = viewport.applet.getParameter("linkURL_" + i); // only add non-null parameters if (label != null) @@ -89,7 +88,7 @@ public class IdPanel extends Panel implements MouseListener, if (!urlList.isEmpty()) { // set default as first entry in list - String defaultUrl = av.applet.getParameter("linkLabel_1"); + String defaultUrl = viewport.applet.getParameter("linkLabel_1"); UrlProviderFactoryI factory = new AppletUrlProviderFactory( defaultUrl, urlList); urlProvider = factory.createUrlProvider(); @@ -106,64 +105,57 @@ public class IdPanel extends Panel implements MouseListener, SequenceI sequence = av.getAlignment().getSequenceAt(seq); - // look for non-pos features StringBuffer tooltiptext = new StringBuffer(); - if (sequence != null) + if (sequence == null) { - if (sequence.getDescription() != null) + return; + } + if (sequence.getDescription() != null) + { + tooltiptext.append(sequence.getDescription()); + tooltiptext.append("\n"); + } + + for (SequenceFeature sf : sequence.getFeatures() + .getNonPositionalFeatures()) + { + boolean nl = false; + if (sf.getFeatureGroup() != null) { - tooltiptext.append(sequence.getDescription()); - tooltiptext.append("\n"); + tooltiptext.append(sf.getFeatureGroup()); + nl = true; } - - SequenceFeature sf[] = sequence.getSequenceFeatures(); - for (int sl = 0; sf != null && sl < sf.length; sl++) + if (sf.getType() != null) { - if (sf[sl].begin == sf[sl].end && sf[sl].begin == 0) - { - boolean nl = false; - if (sf[sl].getFeatureGroup() != null) - { - tooltiptext.append(sf[sl].getFeatureGroup()); - nl = true; - } - ; - if (sf[sl].getType() != null) - { - tooltiptext.append(" "); - tooltiptext.append(sf[sl].getType()); - nl = true; - } - ; - if (sf[sl].getDescription() != null) - { - tooltiptext.append(" "); - tooltiptext.append(sf[sl].getDescription()); - nl = true; - } - ; - if (!Float.isNaN(sf[sl].getScore()) && sf[sl].getScore() != 0f) - { - tooltiptext.append(" Score = "); - tooltiptext.append(sf[sl].getScore()); - nl = true; - } - ; - if (sf[sl].getStatus() != null && sf[sl].getStatus().length() > 0) - { - tooltiptext.append(" ("); - tooltiptext.append(sf[sl].getStatus()); - tooltiptext.append(")"); - nl = true; - } - ; - if (nl) - { - tooltiptext.append("\n"); - } - } + tooltiptext.append(" "); + tooltiptext.append(sf.getType()); + nl = true; + } + if (sf.getDescription() != null) + { + tooltiptext.append(" "); + tooltiptext.append(sf.getDescription()); + nl = true; + } + if (!Float.isNaN(sf.getScore()) && sf.getScore() != 0f) + { + tooltiptext.append(" Score = "); + tooltiptext.append(sf.getScore()); + nl = true; + } + if (sf.getStatus() != null && sf.getStatus().length() > 0) + { + tooltiptext.append(" ("); + tooltiptext.append(sf.getStatus()); + tooltiptext.append(")"); + nl = true; + } + if (nl) + { + tooltiptext.append("\n"); } } + if (tooltiptext.length() == 0) { // nothing to display - so clear tooltip if one is visible @@ -288,10 +280,12 @@ public class IdPanel extends Panel implements MouseListener, if ((e.getModifiers() & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK) { - Sequence sq = (Sequence) av.getAlignment().getSequenceAt(seq); + SequenceI sq = av.getAlignment().getSequenceAt(seq); - // build a new links menu based on the current links + any non-positional - // features + /* + * build a new links menu based on the current links + * and any non-positional features + */ List nlinks; if (urlProvider != null) { @@ -301,17 +295,14 @@ public class IdPanel extends Panel implements MouseListener, { nlinks = new ArrayList(); } - SequenceFeature sf[] = sq == null ? null : sq.getSequenceFeatures(); - for (int sl = 0; sf != null && sl < sf.length; sl++) + + for (SequenceFeature sf : sq.getFeatures().getNonPositionalFeatures()) { - if (sf[sl].begin == sf[sl].end && sf[sl].begin == 0) + if (sf.links != null) { - if (sf[sl].links != null && sf[sl].links.size() > 0) + for (String link : sf.links) { - for (int l = 0, lSize = sf[sl].links.size(); l < lSize; l++) - { - nlinks.add(sf[sl].links.elementAt(l)); - } + nlinks.add(link); } } } @@ -424,9 +415,9 @@ public class IdPanel extends Panel implements MouseListener, boolean up = true; - public ScrollThread(boolean up) + public ScrollThread(boolean isUp) { - this.up = up; + this.up = isUp; start(); } diff --git a/src/jalview/appletgui/SeqCanvas.java b/src/jalview/appletgui/SeqCanvas.java index 9de5452..fe7abbb 100755 --- a/src/jalview/appletgui/SeqCanvas.java +++ b/src/jalview/appletgui/SeqCanvas.java @@ -555,8 +555,8 @@ public class SeqCanvas extends Panel implements ViewportListenerI return annotations.adjustPanelHeight(); } - private void drawPanel(Graphics g1, int startRes, int endRes, - int startSeq, int endSeq, int offset) + private void drawPanel(Graphics g1, final int startRes, final int endRes, + final int startSeq, final int endSeq, final int offset) { if (!av.hasHiddenColumns()) @@ -565,8 +565,8 @@ public class SeqCanvas extends Panel implements ViewportListenerI } else { - int screenY = 0; + final int screenYMax = endRes - startRes; int blockStart = startRes; int blockEnd = endRes; @@ -584,13 +584,22 @@ public class SeqCanvas extends Panel implements ViewportListenerI continue; } - blockEnd = hideStart - 1; + /* + * draw up to just before the next hidden region, or the end of + * the visible region, whichever comes first + */ + blockEnd = Math.min(hideStart - 1, blockStart + screenYMax + - screenY); g1.translate(screenY * avcharWidth, 0); draw(g1, blockStart, blockEnd, startSeq, endSeq, offset); - if (av.getShowHiddenMarkers()) + /* + * draw the downline of the hidden column marker (ScalePanel draws the + * triangle on top) if we reached it + */ + if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1) { g1.setColor(Color.blue); g1.drawLine((blockEnd - blockStart + 1) * avcharWidth - 1, @@ -603,14 +612,14 @@ public class SeqCanvas extends Panel implements ViewportListenerI screenY += blockEnd - blockStart + 1; blockStart = hideEnd + 1; - if (screenY > (endRes - startRes)) + if (screenY > screenYMax) { // already rendered last block return; } } } - if (screenY <= (endRes - startRes)) + if (screenY <= screenYMax) { // remaining visible region to render blockEnd = blockStart + (endRes - startRes) - screenY; diff --git a/src/jalview/appletgui/SeqPanel.java b/src/jalview/appletgui/SeqPanel.java index d000c73..21eb6a4 100644 --- a/src/jalview/appletgui/SeqPanel.java +++ b/src/jalview/appletgui/SeqPanel.java @@ -54,10 +54,8 @@ import java.awt.event.InputEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; -import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.ListIterator; import java.util.Vector; public class SeqPanel extends Panel implements MouseMotionListener, @@ -531,7 +529,7 @@ public class SeqPanel extends Panel implements MouseMotionListener, } int seq = findSeq(evt); - int res = findRes(evt); + int res = findColumn(evt); if (seq < 0 || res < 0) { @@ -567,14 +565,9 @@ public class SeqPanel extends Panel implements MouseMotionListener, av.setSelectionGroup(null); } - int column = findRes(evt); - boolean isGapped = Comparison.isGap(sequence.getCharAt(column)); - List features = findFeaturesAtRes(sequence, - sequence.findPosition(column)); - if (isGapped) - { - removeAdjacentFeatures(features, column + 1, sequence); - } + int column = findColumn(evt); + List features = findFeaturesAtColumn(sequence, + column + 1); if (!features.isEmpty()) { @@ -584,8 +577,8 @@ public class SeqPanel extends Panel implements MouseMotionListener, seqCanvas.highlightSearchResults(highlight); seqCanvas.getFeatureRenderer().amendFeatures( Collections.singletonList(sequence), features, false, ap); - - seqCanvas.highlightSearchResults(null); + av.setSearchResults(null); // clear highlighting + seqCanvas.repaint(); // draw new/amended features } } } @@ -611,7 +604,14 @@ public class SeqPanel extends Panel implements MouseMotionListener, int wrappedBlock = -1; - int findRes(MouseEvent evt) + /** + * Returns the aligned sequence position (base 0) at the mouse position, or + * the closest visible one + * + * @param evt + * @return + */ + int findColumn(MouseEvent evt) { int res = 0; int x = evt.getX(); @@ -716,7 +716,7 @@ public class SeqPanel extends Panel implements MouseMotionListener, { int seq = findSeq(evt); - int res = findRes(evt); + int res = findColumn(evt); if (seq < av.getAlignment().getHeight() && res < av.getAlignment().getSequenceAt(seq).getLength()) @@ -788,7 +788,7 @@ public class SeqPanel extends Panel implements MouseMotionListener, @Override public void mouseMoved(MouseEvent evt) { - final int column = findRes(evt); + final int column = findColumn(evt); int seq = findSeq(evt); if (seq >= av.getAlignment().getHeight() || seq < 0 || column < 0) @@ -871,12 +871,8 @@ public class SeqPanel extends Panel implements MouseMotionListener, */ if (av.isShowSequenceFeatures()) { - List allFeatures = findFeaturesAtRes(sequence, - sequence.findPosition(column)); - if (isGapped) - { - removeAdjacentFeatures(allFeatures, column + 1, sequence); - } + List allFeatures = findFeaturesAtColumn(sequence, + column + 1); for (SequenceFeature sf : allFeatures) { tooltipText.append(sf.getType() + " " + sf.begin + ":" + sf.end); @@ -909,65 +905,18 @@ public class SeqPanel extends Panel implements MouseMotionListener, } /** - * Removes from the list of features any that start after, or end before, the - * given column position. This allows us to retain only those features - * adjacent to a gapped position that straddle the position. Contact features - * that 'straddle' the position are also removed, since they are not 'at' the - * position. + * Returns features at the specified aligned column on the given sequence. + * Non-positional features are not included. If the column has a gap, then + * enclosing features are included (but not contact features). * - * @param features - * @param column - * alignment column (1..) * @param sequence + * @param column + * (1..) + * @return */ - protected void removeAdjacentFeatures(List features, - int column, SequenceI sequence) + List findFeaturesAtColumn(SequenceI sequence, int column) { - // TODO should this be an AlignViewController method (shared by gui)? - ListIterator it = features.listIterator(); - while (it.hasNext()) - { - SequenceFeature sf = it.next(); - if (sf.isContactFeature() - || sequence.findIndex(sf.getBegin()) > column - || sequence.findIndex(sf.getEnd()) < column) - { - it.remove(); - } - } - } - - List findFeaturesAtRes(SequenceI sequence, int res) - { - List result = new ArrayList<>(); - SequenceFeature[] features = sequence.getSequenceFeatures(); - if (features != null) - { - for (int i = 0; i < features.length; i++) - { - if (av.getFeaturesDisplayed() == null - || !av.getFeaturesDisplayed().isVisible( - features[i].getType())) - { - continue; - } - - if (features[i].featureGroup != null - && !seqCanvas.fr.checkGroupVisibility( - features[i].featureGroup, false)) - { - continue; - } - - if ((features[i].getBegin() <= res) - && (features[i].getEnd() >= res)) - { - result.add(features[i]); - } - } - } - - return result; + return seqCanvas.getFeatureRenderer().findFeaturesAtColumn(sequence, column); } Tooltip tooltip; @@ -1047,7 +996,7 @@ public class SeqPanel extends Panel implements MouseMotionListener, return; } - int res = findRes(evt); + int res = findColumn(evt); if (res < 0) { @@ -1229,7 +1178,7 @@ public class SeqPanel extends Panel implements MouseMotionListener, // Find the next gap before the end // of the visible region boundary boolean blank = false; - for (fixedRight = fixedRight; fixedRight > lastres; fixedRight--) + for (; fixedRight > lastres; fixedRight--) { blank = true; @@ -1460,7 +1409,7 @@ public class SeqPanel extends Panel implements MouseMotionListener, scrollThread = null; } - int res = findRes(evt); + int column = findColumn(evt); int seq = findSeq(evt); oldSeq = seq; startWrapBlock = wrappedBlock; @@ -1472,16 +1421,16 @@ public class SeqPanel extends Panel implements MouseMotionListener, SequenceI sequence = av.getAlignment().getSequenceAt(seq); - if (sequence == null || res > sequence.getLength()) + if (sequence == null || column > sequence.getLength()) { return; } stretchGroup = av.getSelectionGroup(); - if (stretchGroup == null || !stretchGroup.contains(sequence, res)) + if (stretchGroup == null || !stretchGroup.contains(sequence, column)) { - stretchGroup = av.getAlignment().findGroup(sequence, res); + stretchGroup = av.getAlignment().findGroup(sequence, column); if (stretchGroup != null) { // only update the current selection if the popup menu has a group to @@ -1493,8 +1442,8 @@ public class SeqPanel extends Panel implements MouseMotionListener, // DETECT RIGHT MOUSE BUTTON IN AWT if ((evt.getModifiers() & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK) { - List allFeatures = findFeaturesAtRes(sequence, - sequence.findPosition(res)); + List allFeatures = findFeaturesAtColumn(sequence, + sequence.findPosition(column + 1)); Vector links = null; for (SequenceFeature sf : allFeatures) @@ -1503,12 +1452,9 @@ public class SeqPanel extends Panel implements MouseMotionListener, { if (links == null) { - links = new Vector<>(); - } - for (int j = 0; j < sf.links.size(); j++) - { - links.addElement(sf.links.elementAt(j)); + links = new Vector(); } + links.addAll(sf.links); } } APopupMenu popup = new APopupMenu(ap, null, links); @@ -1519,7 +1465,7 @@ public class SeqPanel extends Panel implements MouseMotionListener, if (av.cursorMode) { - seqCanvas.cursorX = findRes(evt); + seqCanvas.cursorX = findColumn(evt); seqCanvas.cursorY = findSeq(evt); seqCanvas.repaint(); return; @@ -1531,8 +1477,8 @@ public class SeqPanel extends Panel implements MouseMotionListener, { // define a new group here SequenceGroup sg = new SequenceGroup(); - sg.setStartRes(res); - sg.setEndRes(res); + sg.setStartRes(column); + sg.setEndRes(column); sg.addSequence(sequence, false); av.setSelectionGroup(sg); stretchGroup = sg; @@ -1590,7 +1536,7 @@ public class SeqPanel extends Panel implements MouseMotionListener, public void doMouseDraggedDefineMode(MouseEvent evt) { - int res = findRes(evt); + int res = findColumn(evt); int y = findSeq(evt); if (wrappedBlock != startWrapBlock) diff --git a/src/jalview/commands/EditCommand.java b/src/jalview/commands/EditCommand.java index 21ff841..9eaeb7a 100644 --- a/src/jalview/commands/EditCommand.java +++ b/src/jalview/commands/EditCommand.java @@ -122,15 +122,15 @@ public class EditCommand implements CommandI { } - public EditCommand(String description) + public EditCommand(String desc) { - this.description = description; + this.description = desc; } - public EditCommand(String description, Action command, SequenceI[] seqs, + public EditCommand(String desc, Action command, SequenceI[] seqs, int position, int number, AlignmentI al) { - this.description = description; + this.description = desc; if (command == Action.CUT || command == Action.PASTE) { setEdit(new Edit(command, seqs, position, number, al)); @@ -139,10 +139,10 @@ public class EditCommand implements CommandI performEdit(0, null); } - public EditCommand(String description, Action command, String replace, + public EditCommand(String desc, Action command, String replace, SequenceI[] seqs, int position, int number, AlignmentI al) { - this.description = description; + this.description = desc; if (command == Action.REPLACE) { setEdit(new Edit(command, seqs, position, number, al, replace)); @@ -548,13 +548,14 @@ public class EditCommand implements CommandI { // modify the oldds if necessary if (oldds != sequence.getDatasetSequence() - || sequence.getSequenceFeatures() != null) + || sequence.getFeatures().hasFeatures()) { if (command.oldds == null) { command.oldds = new SequenceI[command.seqs.length]; } command.oldds[i] = oldds; + // FIXME JAL-2541 JAL-2526 get correct positions if on a gap adjustFeatures( command, i, @@ -797,6 +798,8 @@ public class EditCommand implements CommandI AlignmentAnnotation[] tmp; for (int s = 0; s < command.seqs.length; s++) { + command.seqs[s].sequenceChanged(); + if (modifyVisibility) { // Rows are only removed or added to sequence object. @@ -1101,8 +1104,8 @@ public class EditCommand implements CommandI } } - final static void adjustFeatures(Edit command, int index, int i, int j, - boolean insert) + final static void adjustFeatures(Edit command, int index, final int i, + final int j, boolean insert) { SequenceI seq = command.seqs[index]; SequenceI sequence = seq.getDatasetSequence(); @@ -1122,56 +1125,73 @@ public class EditCommand implements CommandI return; } - SequenceFeature[] sf = sequence.getSequenceFeatures(); + List sf = sequence.getFeatures() + .getPositionalFeatures(); - if (sf == null) + if (sf.isEmpty()) { return; } - SequenceFeature[] oldsf = new SequenceFeature[sf.length]; + List oldsf = new ArrayList(); int cSize = j - i; - for (int s = 0; s < sf.length; s++) + for (SequenceFeature feature : sf) { - SequenceFeature copy = new SequenceFeature(sf[s]); + SequenceFeature copy = new SequenceFeature(feature); - oldsf[s] = copy; + oldsf.add(copy); - if (sf[s].getEnd() < i) + if (feature.getEnd() < i) { continue; } - if (sf[s].getBegin() > j) + if (feature.getBegin() > j) { - sf[s].setBegin(copy.getBegin() - cSize); - sf[s].setEnd(copy.getEnd() - cSize); + int newBegin = copy.getBegin() - cSize; + int newEnd = copy.getEnd() - cSize; + SequenceFeature newSf = new SequenceFeature(feature, newBegin, + newEnd, feature.getFeatureGroup(), feature.getScore()); + sequence.deleteFeature(feature); + sequence.addSequenceFeature(newSf); + // feature.setBegin(newBegin); + // feature.setEnd(newEnd); continue; } - if (sf[s].getBegin() >= i) + int newBegin = feature.getBegin(); + int newEnd = feature.getEnd(); + if (newBegin >= i) { - sf[s].setBegin(i); + newBegin = i; + // feature.setBegin(i); } - if (sf[s].getEnd() < j) + if (newEnd < j) { - sf[s].setEnd(j - 1); + newEnd = j - 1; + // feature.setEnd(j - 1); } + newEnd = newEnd - cSize; + // feature.setEnd(feature.getEnd() - (cSize)); - sf[s].setEnd(sf[s].getEnd() - (cSize)); - - if (sf[s].getBegin() > sf[s].getEnd()) + sequence.deleteFeature(feature); + if (newEnd >= newBegin) { - sequence.deleteFeature(sf[s]); + sequence.addSequenceFeature(new SequenceFeature(feature, newBegin, + newEnd, feature.getFeatureGroup(), feature.getScore())); } + // if (feature.getBegin() > feature.getEnd()) + // { + // sequence.deleteFeature(feature); + // } } if (command.editedFeatures == null) { - command.editedFeatures = new Hashtable(); + command.editedFeatures = new Hashtable>(); } command.editedFeatures.put(seq, oldsf); @@ -1298,7 +1318,7 @@ public class EditCommand implements CommandI Hashtable deletedAnnotations; - Hashtable editedFeatures; + Hashtable> editedFeatures; AlignmentI al; @@ -1314,51 +1334,51 @@ public class EditCommand implements CommandI char gapChar; - public Edit(Action command, SequenceI[] seqs, int position, int number, - char gapChar) + public Edit(Action cmd, SequenceI[] sqs, int pos, int count, + char gap) { - this.command = command; - this.seqs = seqs; - this.position = position; - this.number = number; - this.gapChar = gapChar; + this.command = cmd; + this.seqs = sqs; + this.position = pos; + this.number = count; + this.gapChar = gap; } - Edit(Action command, SequenceI[] seqs, int position, int number, - AlignmentI al) + Edit(Action cmd, SequenceI[] sqs, int pos, int count, + AlignmentI align) { - this.gapChar = al.getGapCharacter(); - this.command = command; - this.seqs = seqs; - this.position = position; - this.number = number; - this.al = al; - - alIndex = new int[seqs.length]; - for (int i = 0; i < seqs.length; i++) + this.gapChar = align.getGapCharacter(); + this.command = cmd; + this.seqs = sqs; + this.position = pos; + this.number = count; + this.al = align; + + alIndex = new int[sqs.length]; + for (int i = 0; i < sqs.length; i++) { - alIndex[i] = al.findIndex(seqs[i]); + alIndex[i] = align.findIndex(sqs[i]); } - fullAlignmentHeight = (al.getHeight() == seqs.length); + fullAlignmentHeight = (align.getHeight() == sqs.length); } - Edit(Action command, SequenceI[] seqs, int position, int number, - AlignmentI al, String replace) + Edit(Action cmd, SequenceI[] sqs, int pos, int count, + AlignmentI align, String replace) { - this.command = command; - this.seqs = seqs; - this.position = position; - this.number = number; - this.al = al; - this.gapChar = al.getGapCharacter(); - string = new char[seqs.length][]; - for (int i = 0; i < seqs.length; i++) + this.command = cmd; + this.seqs = sqs; + this.position = pos; + this.number = count; + this.al = align; + this.gapChar = align.getGapCharacter(); + string = new char[sqs.length][]; + for (int i = 0; i < sqs.length; i++) { string[i] = replace.toCharArray(); } - fullAlignmentHeight = (al.getHeight() == seqs.length); + fullAlignmentHeight = (align.getHeight() == sqs.length); } public SequenceI[] getSequences() diff --git a/src/jalview/controller/AlignViewController.java b/src/jalview/controller/AlignViewController.java index bc7f212..5c1f403 100644 --- a/src/jalview/controller/AlignViewController.java +++ b/src/jalview/controller/AlignViewController.java @@ -232,91 +232,66 @@ public class AlignViewController implements AlignViewControllerI static int findColumnsWithFeature(String featureType, SequenceCollectionI sqcol, BitSet bs) { - final int startPosition = sqcol.getStartRes() + 1; // converted to base 1 - final int endPosition = sqcol.getEndRes() + 1; + final int startColumn = sqcol.getStartRes() + 1; // converted to base 1 + final int endColumn = sqcol.getEndRes() + 1; List seqs = sqcol.getSequences(); int nseq = 0; for (SequenceI sq : seqs) { - boolean sequenceHasFeature = false; if (sq != null) { - SequenceFeature[] sfs = sq.getSequenceFeatures(); - if (sfs != null) + // int ist = sq.findPosition(sqcol.getStartRes()); + List sfs = sq.findFeatures(startColumn, + endColumn, featureType); + + if (!sfs.isEmpty()) { - int ist = sq.findIndex(sq.getStart()); - int iend = sq.findIndex(sq.getEnd()); - if (iend < startPosition || ist > endPosition) - { - // sequence not in region - continue; - } - for (SequenceFeature sf : sfs) + nseq++; + } + + for (SequenceFeature sf : sfs) + { + int sfStartCol = sq.findIndex(sf.getBegin()); + int sfEndCol = sq.findIndex(sf.getEnd()); + + if (sf.isContactFeature()) { - // future functionality - featureType == null means mark columns - // containing all displayed features - if (sf != null && (featureType.equals(sf.getType()))) + /* + * 'contact' feature - check for 'start' or 'end' + * position within the selected region + */ + if (sfStartCol >= startColumn && sfStartCol <= endColumn) + { + bs.set(sfStartCol - 1); + } + if (sfEndCol >= startColumn && sfEndCol <= endColumn) { - // optimisation - could consider 'spos,apos' like cursor argument - // - findIndex wastes time by starting from first character and - // counting - - int sfStartCol = sq.findIndex(sf.getBegin()); - int sfEndCol = sq.findIndex(sf.getEnd()); - - if (sf.isContactFeature()) - { - /* - * 'contact' feature - check for 'start' or 'end' - * position within the selected region - */ - if (sfStartCol >= startPosition - && sfStartCol <= endPosition) - { - bs.set(sfStartCol - 1); - sequenceHasFeature = true; - } - if (sfEndCol >= startPosition && sfEndCol <= endPosition) - { - bs.set(sfEndCol - 1); - sequenceHasFeature = true; - } - continue; - } - - /* - * contiguous feature - select feature positions (if any) - * within the selected region - */ - if (sfStartCol > endPosition || sfEndCol < startPosition) - { - // feature is outside selected region - continue; - } - sequenceHasFeature = true; - if (sfStartCol < startPosition) - { - sfStartCol = startPosition; - } - if (sfStartCol < ist) - { - sfStartCol = ist; - } - if (sfEndCol > endPosition) - { - sfEndCol = endPosition; - } - for (; sfStartCol <= sfEndCol; sfStartCol++) - { - bs.set(sfStartCol - 1); // convert to base 0 - } + bs.set(sfEndCol - 1); } + continue; } - } - if (sequenceHasFeature) - { - nseq++; + /* + * contiguous feature - select feature positions (if any) + * within the selected region + */ + if (sfStartCol < startColumn) + { + sfStartCol = startColumn; + } + // not sure what the point of this is + // if (sfStartCol < ist) + // { + // sfStartCol = ist; + // } + if (sfEndCol > endColumn) + { + sfEndCol = endColumn; + } + for (; sfStartCol <= sfEndCol; sfStartCol++) + { + bs.set(sfStartCol - 1); // convert to base 0 + } } } } diff --git a/src/jalview/datamodel/AlignedCodonFrame.java b/src/jalview/datamodel/AlignedCodonFrame.java index 4fbfd62..54f41f8 100644 --- a/src/jalview/datamodel/AlignedCodonFrame.java +++ b/src/jalview/datamodel/AlignedCodonFrame.java @@ -504,10 +504,11 @@ public class AlignedCodonFrame * Read off the mapped nucleotides (converting to position base 0) */ codonPos = MappingUtils.flattenRanges(codonPos); - char[] dna = dnaSeq.getSequence(); int start = dnaSeq.getStart(); - result.add(new char[] { dna[codonPos[0] - start], - dna[codonPos[1] - start], dna[codonPos[2] - start] }); + char c1 = dnaSeq.getCharAt(codonPos[0] - start); + char c2 = dnaSeq.getCharAt(codonPos[1] - start); + char c3 = dnaSeq.getCharAt(codonPos[2] - start); + result.add(new char[] { c1, c2, c3 }); } } return result.isEmpty() ? null : result; diff --git a/src/jalview/datamodel/Alignment.java b/src/jalview/datamodel/Alignment.java index 098222f..26e8db1 100755 --- a/src/jalview/datamodel/Alignment.java +++ b/src/jalview/datamodel/Alignment.java @@ -1473,8 +1473,8 @@ public class Alignment implements AlignmentI { // TODO JAL-1270 needs test coverage // currently tested for use in jalview.gui.SequenceFetcher - boolean samegap = toappend.getGapCharacter() == getGapCharacter(); char oldc = toappend.getGapCharacter(); + boolean samegap = oldc == getGapCharacter(); boolean hashidden = toappend.getHiddenSequences() != null && toappend.getHiddenSequences().hiddenSequences != null; // get all sequences including any hidden ones @@ -1490,14 +1490,7 @@ public class Alignment implements AlignmentI { if (!samegap) { - char[] oldseq = addedsq.getSequence(); - for (int c = 0; c < oldseq.length; c++) - { - if (oldseq[c] == oldc) - { - oldseq[c] = gapCharacter; - } - } + addedsq.replace(oldc, gapCharacter); } toappendsq.add(addedsq); } diff --git a/src/jalview/datamodel/AlignmentAnnotation.java b/src/jalview/datamodel/AlignmentAnnotation.java index 1594f2b..56bfd74 100755 --- a/src/jalview/datamodel/AlignmentAnnotation.java +++ b/src/jalview/datamodel/AlignmentAnnotation.java @@ -96,14 +96,13 @@ public class AlignmentAnnotation * Updates the _rnasecstr field Determines the positions that base pair and * the positions of helices based on secondary structure from a Stockholm file * - * @param RNAannot + * @param rnaAnnotation */ - private void _updateRnaSecStr(CharSequence RNAannot) + private void _updateRnaSecStr(CharSequence rnaAnnotation) { try { - bps = Rna.getModeleBP(RNAannot); - _rnasecstr = Rna.getBasePairs(bps); + _rnasecstr = Rna.getHelixMap(rnaAnnotation); invalidrnastruc = -1; } catch (WUSSParseException px) { @@ -114,8 +113,6 @@ public class AlignmentAnnotation { return; } - Rna.HelixMap(_rnasecstr); - // setRNAStruc(RNAannot); if (_rnasecstr != null && _rnasecstr.length > 0) { @@ -273,12 +270,6 @@ public class AlignmentAnnotation } } - // JBPNote: what does this do ? - public void ConcenStru(CharSequence RNAannot) throws WUSSParseException - { - bps = Rna.getModeleBP(RNAannot); - } - /** * Creates a new AlignmentAnnotation object. * diff --git a/src/jalview/datamodel/BinarySequence.java b/src/jalview/datamodel/BinarySequence.java index b7e15a6..477f4a7 100755 --- a/src/jalview/datamodel/BinarySequence.java +++ b/src/jalview/datamodel/BinarySequence.java @@ -70,7 +70,7 @@ public class BinarySequence extends Sequence int nores = (isNa) ? ResidueProperties.maxNucleotideIndex : ResidueProperties.maxProteinIndex; - dbinary = new double[getSequence().length * nores]; + dbinary = new double[getLength() * nores]; return nores; } @@ -88,7 +88,7 @@ public class BinarySequence extends Sequence { int nores = initMatrixGetNoRes(); final int[] sindex = getSymbolmatrix(); - for (int i = 0; i < getSequence().length; i++) + for (int i = 0; i < getLength(); i++) { int aanum = nores - 1; @@ -132,7 +132,7 @@ public class BinarySequence extends Sequence { int nores = initMatrixGetNoRes(); - for (int i = 0, iSize = getSequence().length; i < iSize; i++) + for (int i = 0, iSize = getLength(); i < iSize; i++) { int aanum = nores - 1; diff --git a/src/jalview/datamodel/ContiguousI.java b/src/jalview/datamodel/ContiguousI.java new file mode 100644 index 0000000..f2ae4b7 --- /dev/null +++ b/src/jalview/datamodel/ContiguousI.java @@ -0,0 +1,8 @@ +package jalview.datamodel; + +public interface ContiguousI +{ + int getBegin(); // todo want long for genomic positions? + + int getEnd(); +} diff --git a/src/jalview/datamodel/Mapping.java b/src/jalview/datamodel/Mapping.java index 1c196be..66425d2 100644 --- a/src/jalview/datamodel/Mapping.java +++ b/src/jalview/datamodel/Mapping.java @@ -46,7 +46,7 @@ public class Mapping /* * The characters of the aligned sequence e.g. "-cGT-ACgTG-" */ - private final char[] alignedSeq; + private final SequenceI alignedSeq; /* * the sequence start residue @@ -102,7 +102,7 @@ public class Mapping */ public AlignedCodonIterator(SequenceI seq, char gapChar) { - this.alignedSeq = seq.getSequence(); + this.alignedSeq = seq; this.start = seq.getStart(); this.gap = gapChar; fromRanges = map.getFromRanges().iterator(); @@ -176,7 +176,7 @@ public class Mapping if (toPosition <= currentToRange[1]) { SequenceI seq = Mapping.this.to; - char pep = seq.getSequence()[toPosition - seq.getStart()]; + char pep = seq.getCharAt(toPosition - seq.getStart()); toPosition++; return String.valueOf(pep); } @@ -257,9 +257,10 @@ public class Mapping * allow for offset e.g. treat pos 8 as 2 if sequence starts at 7 */ int truePos = sequencePos - (start - 1); - while (alignedBases < truePos && alignedColumn < alignedSeq.length) + int length = alignedSeq.getLength(); + while (alignedBases < truePos && alignedColumn < length) { - char c = alignedSeq[alignedColumn++]; + char c = alignedSeq.getCharAt(alignedColumn++); if (c != gap && !Comparison.isGap(c)) { alignedBases++; @@ -530,9 +531,8 @@ public class Mapping SequenceFeature[] vf = new SequenceFeature[frange.length / 2]; for (int i = 0, v = 0; i < frange.length; i += 2, v++) { - vf[v] = new SequenceFeature(f); - vf[v].setBegin(frange[i]); - vf[v].setEnd(frange[i + 1]); + vf[v] = new SequenceFeature(f, frange[i], frange[i + 1], + f.getFeatureGroup(), f.getScore()); if (frange.length > 2) { vf[v].setDescription(f.getDescription() + "\nPart " + (v + 1)); @@ -541,27 +541,7 @@ public class Mapping return vf; } } - if (false) // else - { - int[] word = getWord(f.getBegin()); - if (word[0] < word[1]) - { - f.setBegin(word[0]); - } - else - { - f.setBegin(word[1]); - } - word = getWord(f.getEnd()); - if (word[0] > word[1]) - { - f.setEnd(word[0]); - } - else - { - f.setEnd(word[1]); - } - } + // give up and just return the feature. return new SequenceFeature[] { f }; } diff --git a/src/jalview/datamodel/Range.java b/src/jalview/datamodel/Range.java new file mode 100644 index 0000000..7886713 --- /dev/null +++ b/src/jalview/datamodel/Range.java @@ -0,0 +1,52 @@ +package jalview.datamodel; + +/** + * An immutable data bean that models a start-end range + */ +public class Range implements ContiguousI +{ + public final int start; + + public final int end; + + @Override + public int getBegin() + { + return start; + } + + @Override + public int getEnd() + { + return end; + } + + public Range(int i, int j) + { + start = i; + end = j; + } + + @Override + public String toString() + { + return String.valueOf(start) + "-" + String.valueOf(end); + } + + @Override + public int hashCode() + { + return start * 31 + end; + } + + @Override + public boolean equals(Object obj) + { + if (obj instanceof Range) + { + Range r = (Range) obj; + return (start == r.start && end == r.end); + } + return false; + } +} diff --git a/src/jalview/datamodel/SearchResults.java b/src/jalview/datamodel/SearchResults.java index 1bf5475..8ed47dc 100755 --- a/src/jalview/datamodel/SearchResults.java +++ b/src/jalview/datamodel/SearchResults.java @@ -34,7 +34,7 @@ import java.util.List; public class SearchResults implements SearchResultsI { - private List matches = new ArrayList(); + private List matches = new ArrayList<>(); /** * One match consists of a sequence reference, start and end positions. @@ -42,17 +42,17 @@ public class SearchResults implements SearchResultsI */ public class Match implements SearchResultMatchI { - SequenceI sequence; + final SequenceI sequence; /** * Start position of match in sequence (base 1) */ - int start; + final int start; /** * End position (inclusive) (base 1) */ - int end; + final int end; /** * create a Match on a range of sequence. Match always holds region in @@ -133,11 +133,6 @@ public class SearchResults implements SearchResultsI return sb.toString(); } - public void setSequence(SequenceI seq) - { - this.sequence = seq; - } - /** * Hashcode is the hashcode of the matched sequence plus a hash of start and * end positions. Match objects that pass the test for equals are guaranteed @@ -219,20 +214,15 @@ public class SearchResults implements SearchResultsI m = (Match) _m; mfound = false; - if (m.sequence == sequence) - { - mfound = true; - // locate aligned position - matchStart = sequence.findIndex(m.start) - 1; - matchEnd = sequence.findIndex(m.end) - 1; - } - else if (m.sequence == sequence.getDatasetSequence()) + if (m.sequence == sequence + || m.sequence == sequence.getDatasetSequence()) { mfound = true; - // locate region in local context matchStart = sequence.findIndex(m.start) - 1; - matchEnd = sequence.findIndex(m.end) - 1; + matchEnd = m.start == m.end ? matchStart : sequence + .findIndex(m.end) - 1; } + if (mfound) { if (matchStart <= end && matchEnd >= start) @@ -363,4 +353,10 @@ public class SearchResults implements SearchResultsI SearchResultsI sr = (SearchResultsI) obj; return matches.equals(sr.getResults()); } + + @Override + public void addSearchResults(SearchResultsI toAdd) + { + matches.addAll(toAdd.getResults()); + } } diff --git a/src/jalview/datamodel/SearchResultsI.java b/src/jalview/datamodel/SearchResultsI.java index 52a0467..c3dc0e8 100644 --- a/src/jalview/datamodel/SearchResultsI.java +++ b/src/jalview/datamodel/SearchResultsI.java @@ -44,6 +44,13 @@ public interface SearchResultsI SearchResultMatchI addResult(SequenceI seq, int start, int end); /** + * adds all match results in the argument to this set + * + * @param toAdd + */ + void addSearchResults(SearchResultsI toAdd); + + /** * Answers true if the search results include the given sequence (or its * dataset sequence), else false * diff --git a/src/jalview/datamodel/Sequence.java b/src/jalview/datamodel/Sequence.java index 8176221..a01d185 100755 --- a/src/jalview/datamodel/Sequence.java +++ b/src/jalview/datamodel/Sequence.java @@ -22,6 +22,8 @@ package jalview.datamodel; import jalview.analysis.AlignSeq; import jalview.api.DBRefEntryI; +import jalview.datamodel.features.SequenceFeatures; +import jalview.datamodel.features.SequenceFeaturesI; import jalview.util.Comparison; import jalview.util.DBRefUtils; import jalview.util.MapList; @@ -33,8 +35,11 @@ import java.util.BitSet; import java.util.Collections; import java.util.Enumeration; import java.util.List; +import java.util.ListIterator; import java.util.Vector; +import com.stevesoft.pat.Regex; + import fr.orsay.lri.varna.models.rna.RNA; /** @@ -46,6 +51,11 @@ import fr.orsay.lri.varna.models.rna.RNA; */ public class Sequence extends ASequence implements SequenceI { + private static final Regex limitrx = new Regex( + "[/][0-9]{1,}[-][0-9]{1,}$"); + + private static final Regex endrx = new Regex("[0-9]{1,}$"); + SequenceI datasetSequence; String name; @@ -79,8 +89,22 @@ public class Sequence extends ASequence implements SequenceI */ int index = -1; - /** array of sequence features - may not be null for a valid sequence object */ - public SequenceFeature[] sequenceFeatures; + private SequenceFeatures sequenceFeatureStore; + + /* + * A cursor holding the approximate current view position to the sequence, + * as determined by findIndex or findPosition or findPositions. + * Using a cursor as a hint allows these methods to be more performant for + * large sequences. + */ + private SequenceCursor cursor; + + /* + * A number that should be incremented whenever the sequence is edited. + * If the value matches the cursor token, then we can trust the cursor, + * if not then it should be recomputed. + */ + private int changeCount; /** * Creates a new Sequence object. @@ -97,11 +121,13 @@ public class Sequence extends ASequence implements SequenceI */ public Sequence(String name, String sequence, int start, int end) { + this(); initSeqAndName(name, sequence.toCharArray(), start, end); } public Sequence(String name, char[] sequence, int start, int end) { + this(); initSeqAndName(name, sequence, start, end); } @@ -125,11 +151,6 @@ public class Sequence extends ASequence implements SequenceI checkValidRange(); } - com.stevesoft.pat.Regex limitrx = new com.stevesoft.pat.Regex( - "[/][0-9]{1,}[-][0-9]{1,}$"); - - com.stevesoft.pat.Regex endrx = new com.stevesoft.pat.Regex("[0-9]{1,}$"); - void parseId() { if (name == null) @@ -176,6 +197,14 @@ public class Sequence extends ASequence implements SequenceI } /** + * default constructor + */ + private Sequence() + { + sequenceFeatureStore = new SequenceFeatures(); + } + + /** * Creates a new Sequence object. * * @param name @@ -214,8 +243,8 @@ public class Sequence extends ASequence implements SequenceI */ public Sequence(SequenceI seq, AlignmentAnnotation[] alAnnotation) { + this(); initSeqFrom(seq, alAnnotation); - } /** @@ -231,33 +260,38 @@ public class Sequence extends ASequence implements SequenceI protected void initSeqFrom(SequenceI seq, AlignmentAnnotation[] alAnnotation) { - { - char[] oseq = seq.getSequence(); - initSeqAndName(seq.getName(), Arrays.copyOf(oseq, oseq.length), - seq.getStart(), seq.getEnd()); - } + char[] oseq = seq.getSequence(); // returns a copy of the array + initSeqAndName(seq.getName(), oseq, seq.getStart(), seq.getEnd()); + description = seq.getDescription(); if (seq != datasetSequence) { setDatasetSequence(seq.getDatasetSequence()); } - if (datasetSequence == null && seq.getDBRefs() != null) + + /* + * only copy DBRefs and seqfeatures if we really are a dataset sequence + */ + if (datasetSequence == null) { - // only copy DBRefs and seqfeatures if we really are a dataset sequence - DBRefEntry[] dbr = seq.getDBRefs(); - for (int i = 0; i < dbr.length; i++) - { - addDBRef(new DBRefEntry(dbr[i])); - } - if (seq.getSequenceFeatures() != null) + if (seq.getDBRefs() != null) { - SequenceFeature[] sf = seq.getSequenceFeatures(); - for (int i = 0; i < sf.length; i++) + DBRefEntry[] dbr = seq.getDBRefs(); + for (int i = 0; i < dbr.length; i++) { - addSequenceFeature(new SequenceFeature(sf[i])); + addDBRef(new DBRefEntry(dbr[i])); } } + + /* + * make copies of any sequence features + */ + for (SequenceFeature sf : seq.getSequenceFeatures()) + { + addSequenceFeature(new SequenceFeature(sf)); + } } + if (seq.getAnnotation() != null) { AlignmentAnnotation[] sqann = seq.getAnnotation(); @@ -294,122 +328,67 @@ public class Sequence extends ASequence implements SequenceI } @Override - public void setSequenceFeatures(SequenceFeature[] features) + public void setSequenceFeatures(List features) { - if (datasetSequence == null) - { - sequenceFeatures = features; - } - else + if (datasetSequence != null) { - if (datasetSequence.getSequenceFeatures() != features - && datasetSequence.getSequenceFeatures() != null - && datasetSequence.getSequenceFeatures().length > 0) - { - new Exception( - "Warning: JAL-2046 side effect ? Possible implementation error: overwriting dataset sequence features by setting sequence features on alignment") - .printStackTrace(); - } datasetSequence.setSequenceFeatures(features); + return; } + sequenceFeatureStore = new SequenceFeatures(features); } @Override public synchronized boolean addSequenceFeature(SequenceFeature sf) { - if (sequenceFeatures == null && datasetSequence != null) - { - return datasetSequence.addSequenceFeature(sf); - } - if (sequenceFeatures == null) + if (sf.getType() == null) { - sequenceFeatures = new SequenceFeature[0]; + System.err.println("SequenceFeature type may not be null: " + + sf.toString()); + return false; } - for (int i = 0; i < sequenceFeatures.length; i++) + if (datasetSequence != null) { - if (sequenceFeatures[i].equals(sf)) - { - return false; - } + return datasetSequence.addSequenceFeature(sf); } - SequenceFeature[] temp = new SequenceFeature[sequenceFeatures.length + 1]; - System.arraycopy(sequenceFeatures, 0, temp, 0, sequenceFeatures.length); - temp[sequenceFeatures.length] = sf; - - sequenceFeatures = temp; - return true; + return sequenceFeatureStore.add(sf); } @Override public void deleteFeature(SequenceFeature sf) { - if (sequenceFeatures == null) - { - if (datasetSequence != null) - { - datasetSequence.deleteFeature(sf); - } - return; - } - - int index = 0; - for (index = 0; index < sequenceFeatures.length; index++) - { - if (sequenceFeatures[index].equals(sf)) - { - break; - } - } - - if (index == sequenceFeatures.length) - { - return; - } - - int sfLength = sequenceFeatures.length; - if (sfLength < 2) + if (datasetSequence != null) { - sequenceFeatures = null; + datasetSequence.deleteFeature(sf); } else { - SequenceFeature[] temp = new SequenceFeature[sfLength - 1]; - System.arraycopy(sequenceFeatures, 0, temp, 0, index); - - if (index < sfLength) - { - System.arraycopy(sequenceFeatures, index + 1, temp, index, - sequenceFeatures.length - index - 1); - } - - sequenceFeatures = temp; + sequenceFeatureStore.delete(sf); } } /** - * Returns the sequence features (if any), looking first on the sequence, then - * on its dataset sequence, and so on until a non-null value is found (or - * none). This supports retrieval of sequence features stored on the sequence - * (as in the applet) or on the dataset sequence (as in the Desktop version). + * {@inheritDoc} * * @return */ @Override - public SequenceFeature[] getSequenceFeatures() + public List getSequenceFeatures() { - SequenceFeature[] features = sequenceFeatures; - - SequenceI seq = this; - int count = 0; // failsafe against loop in sequence.datasetsequence... - while (features == null && seq.getDatasetSequence() != null - && count++ < 10) + if (datasetSequence != null) { - seq = seq.getDatasetSequence(); - features = ((Sequence) seq).sequenceFeatures; + return datasetSequence.getSequenceFeatures(); } - return features; + return sequenceFeatureStore.getAllFeatures(); + } + + @Override + public SequenceFeaturesI getFeatures() + { + return datasetSequence != null ? datasetSequence.getFeatures() + : sequenceFeatureStore; } @Override @@ -565,6 +544,7 @@ public class Sequence extends ASequence implements SequenceI { this.sequence = seq.toCharArray(); checkValidRange(); + sequenceChanged(); } @Override @@ -582,7 +562,9 @@ public class Sequence extends ASequence implements SequenceI @Override public char[] getSequence() { - return sequence; + // return sequence; + return sequence == null ? null : Arrays.copyOf(sequence, + sequence.length); } /* @@ -685,58 +667,370 @@ public class Sequence extends ASequence implements SequenceI return this.description; } - /* - * (non-Javadoc) - * - * @see jalview.datamodel.SequenceI#findIndex(int) + /** + * {@inheritDoc} */ @Override public int findIndex(int pos) { - // returns the alignment position for a residue + /* + * use a valid, hopefully nearby, cursor if available + */ + if (isValidCursor(cursor)) + { + return findIndex(pos, cursor); + } + int j = start; int i = 0; - // Rely on end being at least as long as the length of the sequence. + int startColumn = 0; + + /* + * traverse sequence from the start counting gaps; make a note of + * the column of the first residue to save in the cursor + */ while ((i < sequence.length) && (j <= end) && (j <= pos)) { - if (!jalview.util.Comparison.isGap(sequence[i])) + if (!Comparison.isGap(sequence[i])) { + if (j == start) + { + startColumn = i; + } j++; } - i++; } - if ((j == end) && (j < pos)) + if (j == end && j < pos) { return end + 1; } - else + + updateCursor(pos, i, startColumn); + return i; + } + + /** + * Updates the cursor to the latest found residue and column position + * + * @param residuePos + * (start..) + * @param column + * (1..) + * @param startColumn + * column position of the first sequence residue + */ + protected void updateCursor(int residuePos, int column, int startColumn) + { + /* + * preserve end residue column provided cursor was valid + */ + int endColumn = isValidCursor(cursor) ? cursor.lastColumnPosition : 0; + if (residuePos == this.end) { - return i; + endColumn = column; } + + cursor = new SequenceCursor(this, residuePos, column, startColumn, + endColumn, this.changeCount); } + /** + * Answers the aligned column position (1..) for the given residue position + * (start..) given a 'hint' of a residue/column location in the neighbourhood. + * The hint may be left of, at, or to the right of the required position. + * + * @param pos + * @param curs + * @return + */ + protected int findIndex(int pos, SequenceCursor curs) + { + if (!isValidCursor(curs)) + { + /* + * wrong or invalidated cursor, compute de novo + */ + return findIndex(pos); + } + + if (curs.residuePosition == pos) + { + return curs.columnPosition; + } + + /* + * move left or right to find pos from hint.position + */ + int col = curs.columnPosition - 1; // convert from base 1 to 0-based array + // index + int newPos = curs.residuePosition; + int delta = newPos > pos ? -1 : 1; + + while (newPos != pos) + { + col += delta; // shift one column left or right + if (col < 0 || col == sequence.length) + { + break; + } + if (!Comparison.isGap(sequence[col])) + { + newPos += delta; + } + } + + col++; // convert back to base 1 + updateCursor(pos, col, curs.firstColumnPosition); + + return col; + } + + /** + * {@inheritDoc} + */ @Override - public int findPosition(int i) + public int findPosition(final int column) { + /* + * use a valid, hopefully nearby, cursor if available + */ + if (isValidCursor(cursor)) + { + return findPosition(column + 1, cursor); + } + + // TODO recode this more naturally i.e. count residues only + // as they are found, not 'in anticipation' + + /* + * traverse the sequence counting gaps; note the column position + * of the first residue, to save in the cursor + */ + int firstResidueColumn = 0; + int lastPosFound = 0; + int lastPosFoundColumn = 0; + int seqlen = sequence.length; + + if (seqlen > 0 && !Comparison.isGap(sequence[0])) + { + lastPosFound = start; + lastPosFoundColumn = 0; + } + int j = 0; int pos = start; - int seqlen = sequence.length; - while ((j < i) && (j < seqlen)) + + while (j < column && j < seqlen) { - if (!jalview.util.Comparison.isGap(sequence[j])) + if (!Comparison.isGap(sequence[j])) { + lastPosFound = pos; + lastPosFoundColumn = j; + if (pos == this.start) + { + firstResidueColumn = j; + } pos++; } - j++; } + if (j < seqlen && !Comparison.isGap(sequence[j])) + { + lastPosFound = pos; + lastPosFoundColumn = j; + if (pos == this.start) + { + firstResidueColumn = j; + } + } + + /* + * update the cursor to the last residue position found (if any) + * (converting column position to base 1) + */ + if (lastPosFound != 0) + { + updateCursor(lastPosFound, lastPosFoundColumn + 1, + firstResidueColumn + 1); + } return pos; } /** + * Answers true if the given cursor is not null, is for this sequence object, + * and has a token value that matches this object's changeCount, else false. + * This allows us to ignore a cursor as 'stale' if the sequence has been + * modified since the cursor was created. + * + * @param curs + * @return + */ + protected boolean isValidCursor(SequenceCursor curs) + { + if (curs == null || curs.sequence != this || curs.token != changeCount) + { + return false; + } + /* + * sanity check against range + */ + if (curs.columnPosition < 0 || curs.columnPosition > sequence.length) + { + return false; + } + if (curs.residuePosition < start || curs.residuePosition > end) + { + return false; + } + return true; + } + + /** + * Answers the sequence position (start..) for the given aligned column + * position (1..), given a hint of a cursor in the neighbourhood. The cursor + * may lie left of, at, or to the right of the column position. + * + * @param col + * @param curs + * @return + */ + protected int findPosition(final int col, SequenceCursor curs) + { + if (!isValidCursor(curs)) + { + /* + * wrong or invalidated cursor, compute de novo + */ + return findPosition(col - 1);// ugh back to base 0 + } + + if (curs.columnPosition == col) + { + cursor = curs; // in case this method becomes public + return curs.residuePosition; // easy case :-) + } + + if (curs.lastColumnPosition > 0 && curs.lastColumnPosition < col) + { + /* + * sequence lies entirely to the left of col + * - return last residue + 1 + */ + return end + 1; + } + + if (curs.firstColumnPosition > 0 && curs.firstColumnPosition > col) + { + /* + * sequence lies entirely to the right of col + * - return first residue + */ + return start; + } + + // todo could choose closest to col out of column, + // firstColumnPosition, lastColumnPosition as a start point + + /* + * move left or right to find pos from cursor position + */ + int firstResidueColumn = curs.firstColumnPosition; + int column = curs.columnPosition - 1; // to base 0 + int newPos = curs.residuePosition; + int delta = curs.columnPosition > col ? -1 : 1; + boolean gapped = false; + int lastFoundPosition = curs.residuePosition; + int lastFoundPositionColumn = curs.columnPosition; + + while (column != col - 1) + { + column += delta; // shift one column left or right + if (column < 0 || column == sequence.length) + { + break; + } + gapped = Comparison.isGap(sequence[column]); + if (!gapped) + { + newPos += delta; + lastFoundPosition = newPos; + lastFoundPositionColumn = column + 1; + if (lastFoundPosition == this.start) + { + firstResidueColumn = column + 1; + } + } + } + + if (cursor == null || lastFoundPosition != cursor.residuePosition) + { + updateCursor(lastFoundPosition, lastFoundPositionColumn, + firstResidueColumn); + } + + /* + * hack to give position to the right if on a gap + * or beyond the length of the sequence (see JAL-2562) + */ + if (delta > 0 && (gapped || column >= sequence.length)) + { + newPos++; + } + + return newPos; + } + + /** + * {@inheritDoc} + */ + @Override + public Range findPositions(int fromColumn, int toColumn) + { + if (toColumn < fromColumn || fromColumn < 1) + { + return null; + } + + /* + * find the first non-gapped position, if any + */ + int firstPosition = 0; + int col = fromColumn - 1; + int length = sequence.length; + while (col < length && col < toColumn) + { + if (!Comparison.isGap(sequence[col])) + { + firstPosition = findPosition(col++); + break; + } + col++; + } + + if (firstPosition == 0) + { + return null; + } + + /* + * find the last non-gapped position + */ + int lastPosition = firstPosition; + while (col < length && col < toColumn) + { + if (!Comparison.isGap(sequence[col++])) + { + lastPosition++; + } + } + + return new Range(firstPosition, lastPosition); + } + + /** * Returns an int array where indices correspond to each residue in the * sequence and the element value gives its position in the alignment * @@ -926,6 +1220,7 @@ public class Sequence extends ASequence implements SequenceI start = newstart; end = newend; sequence = tmp; + sequenceChanged(); } @Override @@ -956,6 +1251,7 @@ public class Sequence extends ASequence implements SequenceI } sequence = tmp; + sequenceChanged(); } @Override @@ -1150,7 +1446,7 @@ public class Sequence extends ASequence implements SequenceI private boolean _isNa; - private long _seqhash = 0; + private int _seqhash = 0; /** * Answers false if the sequence is more than 85% nucleotide (ACGTU), else @@ -1189,8 +1485,8 @@ public class Sequence extends ASequence implements SequenceI dsseq.setDescription(description); // move features and database references onto dataset sequence - dsseq.sequenceFeatures = sequenceFeatures; - sequenceFeatures = null; + dsseq.sequenceFeatureStore = sequenceFeatureStore; + sequenceFeatureStore = null; dsseq.dbrefs = dbrefs; dbrefs = null; // TODO: search and replace any references to this sequence with @@ -1249,11 +1545,11 @@ public class Sequence extends ASequence implements SequenceI return null; } - Vector subset = new Vector(); - Enumeration e = annotation.elements(); + Vector subset = new Vector(); + Enumeration e = annotation.elements(); while (e.hasMoreElements()) { - AlignmentAnnotation ann = (AlignmentAnnotation) e.nextElement(); + AlignmentAnnotation ann = e.nextElement(); if (ann.label != null && ann.label.equals(label)) { subset.addElement(ann); @@ -1268,7 +1564,7 @@ public class Sequence extends ASequence implements SequenceI e = subset.elements(); while (e.hasMoreElements()) { - anns[i++] = (AlignmentAnnotation) e.nextElement(); + anns[i++] = e.nextElement(); } subset.removeAllElements(); return anns; @@ -1321,12 +1617,12 @@ public class Sequence extends ASequence implements SequenceI if (entry.getSequenceFeatures() != null) { - SequenceFeature[] sfs = entry.getSequenceFeatures(); - for (int si = 0; si < sfs.length; si++) + List sfs = entry.getSequenceFeatures(); + for (SequenceFeature feature : sfs) { - SequenceFeature sf[] = (mp != null) ? mp.locateFeature(sfs[si]) - : new SequenceFeature[] { new SequenceFeature(sfs[si]) }; - if (sf != null && sf.length > 0) + SequenceFeature sf[] = (mp != null) ? mp.locateFeature(feature) + : new SequenceFeature[] { new SequenceFeature(feature) }; + if (sf != null) { for (int sfi = 0; sfi < sf.length; sfi++) { @@ -1339,10 +1635,10 @@ public class Sequence extends ASequence implements SequenceI // transfer PDB entries if (entry.getAllPDBEntries() != null) { - Enumeration e = entry.getAllPDBEntries().elements(); + Enumeration e = entry.getAllPDBEntries().elements(); while (e.hasMoreElements()) { - PDBEntry pdb = (PDBEntry) e.nextElement(); + PDBEntry pdb = e.nextElement(); addPDBId(pdb); } } @@ -1510,4 +1806,97 @@ public class Sequence extends ASequence implements SequenceI } } + /** + * {@inheritDoc} + */ + @Override + public List findFeatures(int fromColumn, int toColumn, + String... types) + { + int startPos = findPosition(fromColumn - 1); // convert base 1 to base 0 + int endPos = fromColumn == toColumn ? startPos + : findPosition(toColumn - 1); + + List result = getFeatures().findFeatures(startPos, + endPos, types); + + /* + * if end column is gapped, endPos may be to the right, + * and we may have included adjacent or enclosing features; + * remove any that are not enclosing, non-contact features + */ + if (endPos > this.end || Comparison.isGap(sequence[toColumn - 1])) + { + ListIterator it = result.listIterator(); + while (it.hasNext()) + { + SequenceFeature sf = it.next(); + int sfBegin = sf.getBegin(); + int sfEnd = sf.getEnd(); + int featureStartColumn = findIndex(sfBegin); + if (featureStartColumn > toColumn) + { + it.remove(); + } + else if (featureStartColumn < fromColumn) + { + int featureEndColumn = sfEnd == sfBegin ? featureStartColumn + : findIndex(sfEnd); + if (featureEndColumn < fromColumn) + { + it.remove(); + } + else if (featureEndColumn > toColumn && sf.isContactFeature()) + { + /* + * remove an enclosing feature if it is a contact feature + */ + it.remove(); + } + } + } + } + + return result; + } + + /** + * Invalidates any stale cursors (forcing recalculation) by incrementing the + * token that has to match the one presented by the cursor + */ + @Override + public void sequenceChanged() + { + changeCount++; + } + + /** + * {@inheritDoc} + */ + @Override + public int replace(char c1, char c2) + { + if (c1 == c2) + { + return 0; + } + int count = 0; + synchronized (sequence) + { + for (int c = 0; c < sequence.length; c++) + { + if (sequence[c] == c1) + { + sequence[c] = c2; + count++; + } + } + } + if (count > 0) + { + sequenceChanged(); + } + + return count; + } } diff --git a/src/jalview/datamodel/SequenceCursor.java b/src/jalview/datamodel/SequenceCursor.java new file mode 100644 index 0000000..b5929bf --- /dev/null +++ b/src/jalview/datamodel/SequenceCursor.java @@ -0,0 +1,125 @@ +package jalview.datamodel; + +/** + * An immutable object representing one or more residue and corresponding + * alignment column positions for a sequence + */ +public class SequenceCursor +{ + /** + * the aligned sequence this cursor applies to + */ + public final SequenceI sequence; + + /** + * residue position in sequence (start...), 0 if undefined + */ + public final int residuePosition; + + /** + * column position (1...) corresponding to residuePosition, or 0 if undefined + */ + public final int columnPosition; + + /** + * column position (1...) of first residue in the sequence, or 0 if undefined + */ + public final int firstColumnPosition; + + /** + * column position (1...) of last residue in the sequence, or 0 if undefined + */ + public final int lastColumnPosition; + + /** + * a token which may be used to check whether this cursor is still valid for + * its sequence (allowing it to be ignored if the sequence has changed) + */ + public final int token; + + /** + * Constructor + * + * @param seq + * sequence this cursor applies to + * @param resPos + * residue position in sequence (start..) + * @param column + * column position in alignment (1..) + * @param tok + * a token that may be validated by the sequence to check the cursor + * is not stale + */ + public SequenceCursor(SequenceI seq, int resPos, int column, int tok) + { + this(seq, resPos, column, 0, 0, tok); + } + + /** + * Constructor + * + * @param seq + * sequence this cursor applies to + * @param resPos + * residue position in sequence (start..) + * @param column + * column position in alignment (1..) + * @param firstResCol + * column position of the first residue in the sequence (1..), or 0 + * if not known + * @param lastResCol + * column position of the last residue in the sequence (1..), or 0 if + * not known + * @param tok + * a token that may be validated by the sequence to check the cursor + * is not stale + */ + public SequenceCursor(SequenceI seq, int resPos, int column, int firstResCol, + int lastResCol, int tok) + { + sequence = seq; + residuePosition = resPos; + columnPosition = column; + firstColumnPosition = firstResCol; + lastColumnPosition = lastResCol; + token = tok; + } + + @Override + public int hashCode() + { + int hash = 31 * residuePosition; + hash = 31 * hash + columnPosition; + hash = 31 * hash + token; + if (sequence != null) + { + hash += sequence.hashCode(); + } + return hash; + } + + /** + * Two cursors are equal if they refer to the same sequence object and have + * the same residue position, column position and token value + */ + @Override + public boolean equals(Object obj) + { + if (!(obj instanceof SequenceCursor)) + { + return false; + } + SequenceCursor sc = (SequenceCursor) obj; + return sequence == sc.sequence && residuePosition == sc.residuePosition + && columnPosition == sc.columnPosition && token == sc.token; + } + + @Override + public String toString() + { + String name = sequence == null ? "" : sequence.getName(); + return String.format("%s:Pos%d:Col%d:startCol%d:endCol%d:tok%d", name, + residuePosition, columnPosition, firstColumnPosition, + lastColumnPosition, token); + } +} diff --git a/src/jalview/datamodel/SequenceFeature.java b/src/jalview/datamodel/SequenceFeature.java index 15f54b9..ba7412c 100755 --- a/src/jalview/datamodel/SequenceFeature.java +++ b/src/jalview/datamodel/SequenceFeature.java @@ -20,8 +20,11 @@ */ package jalview.datamodel; +import jalview.datamodel.features.FeatureLocationI; + import java.util.HashMap; import java.util.Map; +import java.util.Map.Entry; import java.util.Vector; /** @@ -30,8 +33,14 @@ import java.util.Vector; * @author $author$ * @version $Revision$ */ -public class SequenceFeature +public class SequenceFeature implements FeatureLocationI { + /* + * score value if none is set; preferably Float.Nan, but see + * JAL-2060 and JAL-2554 for a couple of blockers to that + */ + private static final float NO_SCORE = 0f; + private static final String STATUS = "status"; private static final String STRAND = "STRAND"; @@ -48,13 +57,22 @@ public class SequenceFeature */ private static final String ATTRIBUTES = "ATTRIBUTES"; - public int begin; + /* + * type, begin, end, featureGroup, score and contactFeature are final + * to ensure that the integrity of SequenceFeatures data store + * can't be broken by direct update of these fields + */ + public final String type; - public int end; + public final int begin; - public float score; + public final int end; - public String type; + public final String featureGroup; + + public final float score; + + private final boolean contactFeature; public String description; @@ -66,14 +84,6 @@ public class SequenceFeature public Vector links; - // Feature group can be set from a features file - // as a group of features between STARTGROUP and ENDGROUP markers - public String featureGroup; - - public SequenceFeature() - { - } - /** * Constructs a duplicate feature. Note: Uses makes a shallow copy of the * otherDetails map, so the new and original SequenceFeature may reference the @@ -83,96 +93,99 @@ public class SequenceFeature */ public SequenceFeature(SequenceFeature cpy) { - if (cpy != null) - { - begin = cpy.begin; - end = cpy.end; - score = cpy.score; - if (cpy.type != null) - { - type = new String(cpy.type); - } - if (cpy.description != null) - { - description = new String(cpy.description); - } - if (cpy.featureGroup != null) - { - featureGroup = new String(cpy.featureGroup); - } - if (cpy.otherDetails != null) - { - try - { - otherDetails = (Map) ((HashMap) cpy.otherDetails) - .clone(); - } catch (Exception e) - { - // ignore - } - } - if (cpy.links != null && cpy.links.size() > 0) - { - links = new Vector(); - for (int i = 0, iSize = cpy.links.size(); i < iSize; i++) - { - links.addElement(cpy.links.elementAt(i)); - } - } - } + this(cpy, cpy.getBegin(), cpy.getEnd(), cpy.getFeatureGroup(), cpy + .getScore()); } /** - * Constructor including a Status value + * Constructor * - * @param type - * @param desc - * @param status - * @param begin - * @param end - * @param featureGroup + * @param theType + * @param theDesc + * @param theBegin + * @param theEnd + * @param group */ - public SequenceFeature(String type, String desc, String status, - int begin, int end, String featureGroup) + public SequenceFeature(String theType, String theDesc, int theBegin, + int theEnd, String group) { - this(type, desc, begin, end, featureGroup); - setStatus(status); + this(theType, theDesc, theBegin, theEnd, NO_SCORE, group); } /** - * Constructor + * Constructor including a score value * - * @param type - * @param desc - * @param begin - * @param end - * @param featureGroup + * @param theType + * @param theDesc + * @param theBegin + * @param theEnd + * @param theScore + * @param group */ - SequenceFeature(String type, String desc, int begin, int end, - String featureGroup) + public SequenceFeature(String theType, String theDesc, int theBegin, + int theEnd, float theScore, String group) { - this.type = type; - this.description = desc; - this.begin = begin; - this.end = end; - this.featureGroup = featureGroup; + this.type = theType; + this.description = theDesc; + this.begin = theBegin; + this.end = theEnd; + this.featureGroup = group; + this.score = theScore; + + /* + * for now, only "Disulfide/disulphide bond" is treated as a contact feature + */ + this.contactFeature = "disulfide bond".equalsIgnoreCase(type) + || "disulphide bond".equalsIgnoreCase(type); } /** - * Constructor including a score value + * A copy constructor that allows the value of final fields to be 'modified' + * + * @param sf + * @param newType + * @param newBegin + * @param newEnd + * @param newGroup + * @param newScore + */ + public SequenceFeature(SequenceFeature sf, String newType, int newBegin, + int newEnd, String newGroup, float newScore) + { + this(newType, sf.getDescription(), newBegin, newEnd, newScore, + newGroup); + + if (sf.otherDetails != null) + { + otherDetails = new HashMap(); + for (Entry entry : sf.otherDetails.entrySet()) + { + otherDetails.put(entry.getKey(), entry.getValue()); + } + } + if (sf.links != null && sf.links.size() > 0) + { + links = new Vector(); + for (int i = 0, iSize = sf.links.size(); i < iSize; i++) + { + links.addElement(sf.links.elementAt(i)); + } + } + } + + /** + * A copy constructor that allows the value of final fields to be 'modified' * - * @param type - * @param desc - * @param begin - * @param end - * @param score - * @param featureGroup + * @param sf + * @param newBegin + * @param newEnd + * @param newGroup + * @param newScore */ - public SequenceFeature(String type, String desc, int begin, int end, - float score, String featureGroup) + public SequenceFeature(SequenceFeature sf, int newBegin, int newEnd, + String newGroup, float newScore) { - this(type, desc, begin, end, featureGroup); - this.score = score; + this(sf, sf.getType(), newBegin, newEnd, newGroup, newScore); } /** @@ -268,31 +281,23 @@ public class SequenceFeature * * @return DOCUMENT ME! */ + @Override public int getBegin() { return begin; } - public void setBegin(int start) - { - this.begin = start; - } - /** * DOCUMENT ME! * * @return DOCUMENT ME! */ + @Override public int getEnd() { return end; } - public void setEnd(int end) - { - this.end = end; - } - /** * DOCUMENT ME! * @@ -303,11 +308,6 @@ public class SequenceFeature return type; } - public void setType(String type) - { - this.type = type; - } - /** * DOCUMENT ME! * @@ -328,11 +328,6 @@ public class SequenceFeature return featureGroup; } - public void setFeatureGroup(String featureGroup) - { - this.featureGroup = featureGroup; - } - public void addLink(String labelLink) { if (links == null) @@ -340,7 +335,10 @@ public class SequenceFeature links = new Vector(); } - links.insertElementAt(labelLink, 0); + if (!links.contains(labelLink)) + { + links.insertElementAt(labelLink, 0); + } } public float getScore() @@ -348,11 +346,6 @@ public class SequenceFeature return score; } - public void setScore(float value) - { - score = value; - } - /** * Used for getting values which are not in the basic set. eg STRAND, PHASE * for GFF file @@ -432,17 +425,6 @@ public class SequenceFeature return (String) getValue(ATTRIBUTES); } - public void setPosition(int pos) - { - begin = pos; - end = pos; - } - - public int getPosition() - { - return begin; - } - /** * Return 1 for forward strand ('+' in GFF), -1 for reverse strand ('-' in * GFF), and 0 for unknown or not (validly) specified @@ -538,14 +520,19 @@ public class SequenceFeature * positions, rather than ends of a range. Such features may be visualised or * reported differently to features on a range. */ + @Override public boolean isContactFeature() { - // TODO abstract one day to a FeatureType class - if ("disulfide bond".equalsIgnoreCase(type) - || "disulphide bond".equalsIgnoreCase(type)) - { - return true; - } - return false; + return contactFeature; + } + + /** + * Answers true if the sequence has zero start and end position + * + * @return + */ + public boolean isNonPositional() + { + return begin == 0 && end == 0; } } diff --git a/src/jalview/datamodel/SequenceI.java b/src/jalview/datamodel/SequenceI.java index 12ddf60..6e6d1aa 100755 --- a/src/jalview/datamodel/SequenceI.java +++ b/src/jalview/datamodel/SequenceI.java @@ -20,6 +20,8 @@ */ package jalview.datamodel; +import jalview.datamodel.features.SequenceFeaturesI; + import java.util.BitSet; import java.util.List; import java.util.Vector; @@ -117,9 +119,9 @@ public interface SequenceI extends ASequenceI public String getSequenceAsString(int start, int end); /** - * Get the sequence as a character array + * Answers a copy of the sequence as a character array * - * @return seqeunce and any gaps + * @return */ public char[] getSequence(); @@ -175,7 +177,7 @@ public interface SequenceI extends ASequenceI public String getDescription(); /** - * Return the alignment column for a sequence position + * Return the alignment column (from 1..) for a sequence position * * @param pos * lying from start to end @@ -201,6 +203,16 @@ public interface SequenceI extends ASequenceI public int findPosition(int i); /** + * Returns the from-to sequence positions (start..) for the given column + * positions (1..), or null if no residues are included in the range + * + * @param fromColum + * @param toColumn + * @return + */ + public Range findPositions(int fromColum, int toColumn); + + /** * Returns an int array where indices correspond to each residue in the * sequence and the element value gives its position in the alignment * @@ -261,22 +273,28 @@ public interface SequenceI extends ASequenceI public void insertCharAt(int position, int count, char ch); /** - * Gets array holding sequence features associated with this sequence. The - * array may be held by the sequence's dataset sequence if that is defined. + * Answers a list of all sequence features associated with this sequence. The + * list may be held by the sequence's dataset sequence if that is defined. + * + * @return + */ + public List getSequenceFeatures(); + + /** + * Answers the object holding features for the sequence * - * @return hard reference to array + * @return */ - public SequenceFeature[] getSequenceFeatures(); + SequenceFeaturesI getFeatures(); /** - * Replaces the array of sequence features associated with this sequence with - * a new array reference. If this sequence has a dataset sequence, then this - * method will update the dataset sequence's feature array + * Replaces the sequence features associated with this sequence with the given + * features. If this sequence has a dataset sequence, then this method will + * update the dataset sequence's features instead. * * @param features - * New array of sequence features */ - public void setSequenceFeatures(SequenceFeature[] features); + public void setSequenceFeatures(List features); /** * DOCUMENT ME! @@ -341,7 +359,7 @@ public interface SequenceI extends ASequenceI /** * Adds the given sequence feature and returns true, or returns false if it is - * already present on the sequence + * already present on the sequence, or if the feature type is null. * * @param sf * @return @@ -479,9 +497,41 @@ public interface SequenceI extends ASequenceI public List getPrimaryDBRefs(); /** + * Returns a (possibly empty) list of sequence features that overlap the given + * alignment column range, optionally restricted to one or more specified + * feature types. If the range is all gaps, then features which enclose it are + * included (but not contact features). + * + * @param fromCol + * start column of range inclusive (1..) + * @param toCol + * end column of range inclusive (1..) + * @param types + * optional feature types to restrict results to + * @return + */ + List findFeatures(int fromCol, int toCol, String... types); + + /** + * Method to call to indicate that the sequence (characters or alignment/gaps) + * has been modified. Provided to allow any cursors on residue/column + * positions to be invalidated. + */ + void sequenceChanged(); + + /** * * @return BitSet corresponding to index [0,length) where Comparison.isGap() * returns true. */ BitSet getInsertionsAsBits(); + + /** + * Replaces every occurrence of c1 in the sequence with c2 and returns the + * number of characters changed + * + * @param c1 + * @param c2 + */ + public int replace(char c1, char c2); } diff --git a/src/jalview/datamodel/features/FeatureLocationI.java b/src/jalview/datamodel/features/FeatureLocationI.java new file mode 100644 index 0000000..e651c13 --- /dev/null +++ b/src/jalview/datamodel/features/FeatureLocationI.java @@ -0,0 +1,12 @@ +package jalview.datamodel.features; + +import jalview.datamodel.ContiguousI; + +/** + * An extension of ContiguousI that allows start/end values to be interpreted + * instead as two contact positions + */ +public interface FeatureLocationI extends ContiguousI +{ + boolean isContactFeature(); +} diff --git a/src/jalview/datamodel/features/FeatureStore.java b/src/jalview/datamodel/features/FeatureStore.java new file mode 100644 index 0000000..b082b56 --- /dev/null +++ b/src/jalview/datamodel/features/FeatureStore.java @@ -0,0 +1,1078 @@ +package jalview.datamodel.features; + +import jalview.datamodel.ContiguousI; +import jalview.datamodel.SequenceFeature; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * A data store for a set of sequence features that supports efficient lookup of + * features overlapping a given range. Intended for (but not limited to) storage + * of features for one sequence and feature type. + * + * @author gmcarstairs + * + */ +public class FeatureStore +{ + /** + * a class providing criteria for performing a binary search of a list + */ + abstract static class SearchCriterion + { + /** + * Answers true if the entry passes the search criterion test + * + * @param entry + * @return + */ + abstract boolean compare(SequenceFeature entry); + + /** + * serves a search condition for finding the first feature whose start + * position follows a given target location + * + * @param target + * @return + */ + static SearchCriterion byStart(final long target) + { + return new SearchCriterion() { + + @Override + boolean compare(SequenceFeature entry) + { + return entry.getBegin() >= target; + } + }; + } + + /** + * serves a search condition for finding the first feature whose end + * position follows a given target location + * + * @param target + * @return + */ + static SearchCriterion byEnd(final long target) + { + return new SearchCriterion() + { + + @Override + boolean compare(SequenceFeature entry) + { + return entry.getEnd() >= target; + } + }; + } + + /** + * serves a search condition for finding the first feature which follows the + * given range as determined by a supplied comparator + * + * @param target + * @return + */ + static SearchCriterion byFeature(final ContiguousI to, + final Comparator rc) + { + return new SearchCriterion() + { + + @Override + boolean compare(SequenceFeature entry) + { + return rc.compare(entry, to) >= 0; + } + }; + } + } + + /* + * Non-positional features have no (zero) start/end position. + * Kept as a separate list in case this criterion changes in future. + */ + List nonPositionalFeatures; + + /* + * An ordered list of features, with the promise that no feature in the list + * properly contains any other. This constraint allows bounded linear search + * of the list for features overlapping a region. + * Contact features are not included in this list. + */ + List nonNestedFeatures; + + /* + * contact features ordered by first contact position + */ + List contactFeatureStarts; + + /* + * contact features ordered by second contact position + */ + List contactFeatureEnds; + + /* + * Nested Containment List is used to hold any features that are nested + * within (properly contained by) any other feature. This is a recursive tree + * which supports depth-first scan for features overlapping a range. + * It is used here as a 'catch-all' fallback for features that cannot be put + * into a simple ordered list without invalidating the search methods. + */ + NCList nestedFeatures; + + /* + * Feature groups represented in stored positional features + * (possibly including null) + */ + Set positionalFeatureGroups; + + /* + * Feature groups represented in stored non-positional features + * (possibly including null) + */ + Set nonPositionalFeatureGroups; + + /* + * the total length of all positional features; contact features count 1 to + * the total and 1 to size(), consistent with an average 'feature length' of 1 + */ + int totalExtent; + + float positionalMinScore; + + float positionalMaxScore; + + float nonPositionalMinScore; + + float nonPositionalMaxScore; + + /** + * Constructor + */ + public FeatureStore() + { + nonNestedFeatures = new ArrayList(); + positionalFeatureGroups = new HashSet(); + nonPositionalFeatureGroups = new HashSet(); + positionalMinScore = Float.NaN; + positionalMaxScore = Float.NaN; + nonPositionalMinScore = Float.NaN; + nonPositionalMaxScore = Float.NaN; + + // we only construct nonPositionalFeatures, contactFeatures + // or the NCList if we need to + } + + /** + * Adds one sequence feature to the store, and returns true, unless the + * feature is already contained in the store, in which case this method + * returns false. Containment is determined by SequenceFeature.equals() + * comparison. + * + * @param feature + */ + public boolean addFeature(SequenceFeature feature) + { + if (contains(feature)) + { + return false; + } + + /* + * keep a record of feature groups + */ + if (!feature.isNonPositional()) + { + positionalFeatureGroups.add(feature.getFeatureGroup()); + } + + boolean added = false; + + if (feature.isContactFeature()) + { + added = addContactFeature(feature); + } + else if (feature.isNonPositional()) + { + added = addNonPositionalFeature(feature); + } + else + { + added = addNonNestedFeature(feature); + if (!added) + { + /* + * detected a nested feature - put it in the NCList structure + */ + added = addNestedFeature(feature); + } + } + + if (added) + { + /* + * record the total extent of positional features, to make + * getTotalFeatureLength possible; we count the length of a + * contact feature as 1 + */ + totalExtent += getFeatureLength(feature); + + /* + * record the minimum and maximum score for positional + * and non-positional features + */ + float score = feature.getScore(); + if (!Float.isNaN(score)) + { + if (feature.isNonPositional()) + { + nonPositionalMinScore = min(nonPositionalMinScore, score); + nonPositionalMaxScore = max(nonPositionalMaxScore, score); + } + else + { + positionalMinScore = min(positionalMinScore, score); + positionalMaxScore = max(positionalMaxScore, score); + } + } + } + + return added; + } + + /** + * Answers true if this store contains the given feature (testing by + * SequenceFeature.equals), else false + * + * @param feature + * @return + */ + public boolean contains(SequenceFeature feature) + { + if (feature.isNonPositional()) + { + return nonPositionalFeatures == null ? false : nonPositionalFeatures + .contains(feature); + } + + if (feature.isContactFeature()) + { + return contactFeatureStarts == null ? false : listContains( + contactFeatureStarts, feature); + } + + if (listContains(nonNestedFeatures, feature)) + { + return true; + } + + return nestedFeatures == null ? false : nestedFeatures + .contains(feature); + } + + /** + * Answers the 'length' of the feature, counting 0 for non-positional features + * and 1 for contact features + * + * @param feature + * @return + */ + protected static int getFeatureLength(SequenceFeature feature) + { + if (feature.isNonPositional()) + { + return 0; + } + if (feature.isContactFeature()) + { + return 1; + } + return 1 + feature.getEnd() - feature.getBegin(); + } + + /** + * Adds the feature to the list of non-positional features (with lazy + * instantiation of the list if it is null), and returns true. The feature + * group is added to the set of distinct feature groups for non-positional + * features. This method allows duplicate features, so test before calling to + * prevent this. + * + * @param feature + */ + protected boolean addNonPositionalFeature(SequenceFeature feature) + { + if (nonPositionalFeatures == null) + { + nonPositionalFeatures = new ArrayList(); + } + + nonPositionalFeatures.add(feature); + + nonPositionalFeatureGroups.add(feature.getFeatureGroup()); + + return true; + } + + /** + * Adds one feature to the NCList that can manage nested features (creating + * the NCList if necessary), and returns true. If the feature is already + * stored in the NCList (by equality test), then it is not added, and this + * method returns false. + */ + protected synchronized boolean addNestedFeature(SequenceFeature feature) + { + if (nestedFeatures == null) + { + nestedFeatures = new NCList<>(feature); + return true; + } + return nestedFeatures.add(feature, false); + } + + /** + * Add a feature to the list of non-nested features, maintaining the ordering + * of the list. A check is made for whether the feature is nested in (properly + * contained by) an existing feature. If there is no nesting, the feature is + * added to the list and the method returns true. If nesting is found, the + * feature is not added and the method returns false. + * + * @param feature + * @return + */ + protected boolean addNonNestedFeature(SequenceFeature feature) + { + synchronized (nonNestedFeatures) + { + /* + * find the first stored feature which doesn't precede the new one + */ + int insertPosition = binarySearch(nonNestedFeatures, + SearchCriterion.byFeature(feature, RangeComparator.BY_START_POSITION)); + + /* + * fail if we detect feature enclosure - of the new feature by + * the one preceding it, or of the next feature by the new one + */ + if (insertPosition > 0) + { + if (encloses(nonNestedFeatures.get(insertPosition - 1), feature)) + { + return false; + } + } + if (insertPosition < nonNestedFeatures.size()) + { + if (encloses(feature, nonNestedFeatures.get(insertPosition))) + { + return false; + } + } + + /* + * checks passed - add the feature + */ + nonNestedFeatures.add(insertPosition, feature); + + return true; + } + } + + /** + * Answers true if range1 properly encloses range2, else false + * + * @param range1 + * @param range2 + * @return + */ + protected boolean encloses(ContiguousI range1, ContiguousI range2) + { + int begin1 = range1.getBegin(); + int begin2 = range2.getBegin(); + int end1 = range1.getEnd(); + int end2 = range2.getEnd(); + if (begin1 == begin2 && end1 > end2) + { + return true; + } + if (begin1 < begin2 && end1 >= end2) + { + return true; + } + return false; + } + + /** + * Add a contact feature to the lists that hold them ordered by start (first + * contact) and by end (second contact) position, ensuring the lists remain + * ordered, and returns true. This method allows duplicate features to be + * added, so test before calling to avoid this. + * + * @param feature + * @return + */ + protected synchronized boolean addContactFeature(SequenceFeature feature) + { + if (contactFeatureStarts == null) + { + contactFeatureStarts = new ArrayList(); + } + if (contactFeatureEnds == null) + { + contactFeatureEnds = new ArrayList(); + } + + /* + * binary search the sorted list to find the insertion point + */ + int insertPosition = binarySearch(contactFeatureStarts, + SearchCriterion.byFeature(feature, + RangeComparator.BY_START_POSITION)); + contactFeatureStarts.add(insertPosition, feature); + // and resort to mak siccar...just in case insertion point not quite right + Collections.sort(contactFeatureStarts, RangeComparator.BY_START_POSITION); + + insertPosition = binarySearch(contactFeatureStarts, + SearchCriterion.byFeature(feature, + RangeComparator.BY_END_POSITION)); + contactFeatureEnds.add(feature); + Collections.sort(contactFeatureEnds, RangeComparator.BY_END_POSITION); + + return true; + } + + /** + * Answers true if the list contains the feature, else false. This method is + * optimised for the condition that the list is sorted on feature start + * position ascending, and will give unreliable results if this does not hold. + * + * @param features + * @param feature + * @return + */ + protected static boolean listContains(List features, + SequenceFeature feature) + { + if (features == null || feature == null) + { + return false; + } + + /* + * locate the first entry in the list which does not precede the feature + */ + int pos = binarySearch(features, + SearchCriterion.byFeature(feature, RangeComparator.BY_START_POSITION)); + int len = features.size(); + while (pos < len) + { + SequenceFeature sf = features.get(pos); + if (sf.getBegin() > feature.getBegin()) + { + return false; // no match found + } + if (sf.equals(feature)) + { + return true; + } + pos++; + } + return false; + } + + /** + * Returns a (possibly empty) list of features whose extent overlaps the given + * range. The returned list is not ordered. Contact features are included if + * either of the contact points lies within the range. + * + * @param start + * start position of overlap range (inclusive) + * @param end + * end position of overlap range (inclusive) + * @return + */ + public List findOverlappingFeatures(long start, long end) + { + List result = new ArrayList<>(); + + findNonNestedFeatures(start, end, result); + + findContactFeatures(start, end, result); + + if (nestedFeatures != null) + { + result.addAll(nestedFeatures.findOverlaps(start, end)); + } + + return result; + } + + /** + * Adds contact features to the result list where either the second or the + * first contact position lies within the target range + * + * @param from + * @param to + * @param result + */ + protected void findContactFeatures(long from, long to, + List result) + { + if (contactFeatureStarts != null) + { + findContactStartFeatures(from, to, result); + } + if (contactFeatureEnds != null) + { + findContactEndFeatures(from, to, result); + } + } + + /** + * Adds to the result list any contact features whose end (second contact + * point), but not start (first contact point), lies in the query from-to + * range + * + * @param from + * @param to + * @param result + */ + protected void findContactEndFeatures(long from, long to, + List result) + { + /* + * find the first contact feature (if any) that does not lie + * entirely before the target range + */ + int startPosition = binarySearch(contactFeatureEnds, + SearchCriterion.byEnd(from)); + for (; startPosition < contactFeatureEnds.size(); startPosition++) + { + SequenceFeature sf = contactFeatureEnds.get(startPosition); + if (!sf.isContactFeature()) + { + System.err.println("Error! non-contact feature type " + + sf.getType() + " in contact features list"); + continue; + } + + int begin = sf.getBegin(); + if (begin >= from && begin <= to) + { + /* + * this feature's first contact position lies in the search range + * so we don't include it in results a second time + */ + continue; + } + + int end = sf.getEnd(); + if (end >= from && end <= to) + { + result.add(sf); + } + if (end > to) + { + break; + } + } + } + + /** + * Adds non-nested features to the result list that lie within the target + * range. Non-positional features (start=end=0), contact features and nested + * features are excluded. + * + * @param from + * @param to + * @param result + */ + protected void findNonNestedFeatures(long from, long to, + List result) + { + int startIndex = binarySearch(nonNestedFeatures, + SearchCriterion.byEnd(from)); + + findNonNestedFeatures(startIndex, from, to, result); + } + + /** + * Scans the list of non-nested features, starting from startIndex, to find + * those that overlap the from-to range, and adds them to the result list. + * Returns the index of the first feature whose start position is after the + * target range (or the length of the whole list if no such feature exists). + * + * @param startIndex + * @param from + * @param to + * @param result + * @return + */ + protected int findNonNestedFeatures(final int startIndex, long from, + long to, List result) + { + int i = startIndex; + while (i < nonNestedFeatures.size()) + { + SequenceFeature sf = nonNestedFeatures.get(i); + if (sf.getBegin() > to) + { + break; + } + if (sf.getBegin() <= to && sf.getEnd() >= from) + { + result.add(sf); + } + i++; + } + return i; + } + + /** + * Adds contact features whose start position lies in the from-to range to the + * result list + * + * @param from + * @param to + * @param result + */ + protected void findContactStartFeatures(long from, long to, + List result) + { + int startPosition = binarySearch(contactFeatureStarts, + SearchCriterion.byStart(from)); + + for (; startPosition < contactFeatureStarts.size(); startPosition++) + { + SequenceFeature sf = contactFeatureStarts.get(startPosition); + if (!sf.isContactFeature()) + { + System.err.println("Error! non-contact feature type " + + sf.getType() + " in contact features list"); + continue; + } + int begin = sf.getBegin(); + if (begin >= from && begin <= to) + { + result.add(sf); + } + } + } + + /** + * Answers a list of all positional features stored, in no guaranteed order + * + * @return + */ + public List getPositionalFeatures() + { + /* + * add non-nested features (may be all features for many cases) + */ + List result = new ArrayList<>(); + result.addAll(nonNestedFeatures); + + /* + * add any contact features - from the list by start position + */ + if (contactFeatureStarts != null) + { + result.addAll(contactFeatureStarts); + } + + /* + * add any nested features + */ + if (nestedFeatures != null) + { + result.addAll(nestedFeatures.getEntries()); + } + + return result; + } + + /** + * Answers a list of all contact features. If there are none, returns an + * immutable empty list. + * + * @return + */ + public List getContactFeatures() + { + if (contactFeatureStarts == null) + { + return Collections.emptyList(); + } + return new ArrayList<>(contactFeatureStarts); + } + + /** + * Answers a list of all non-positional features. If there are none, returns + * an immutable empty list. + * + * @return + */ + public List getNonPositionalFeatures() + { + if (nonPositionalFeatures == null) + { + return Collections.emptyList(); + } + return new ArrayList<>(nonPositionalFeatures); + } + + /** + * Deletes the given feature from the store, returning true if it was found + * (and deleted), else false. This method makes no assumption that the feature + * is in the 'expected' place in the store, in case it has been modified since + * it was added. + * + * @param sf + */ + public synchronized boolean delete(SequenceFeature sf) + { + /* + * try the non-nested positional features first + */ + boolean removed = nonNestedFeatures.remove(sf); + + /* + * if not found, try contact positions (and if found, delete + * from both lists of contact positions) + */ + if (!removed && contactFeatureStarts != null) + { + removed = contactFeatureStarts.remove(sf); + if (removed) + { + contactFeatureEnds.remove(sf); + } + } + + boolean removedNonPositional = false; + + /* + * if not found, try non-positional features + */ + if (!removed && nonPositionalFeatures != null) + { + removedNonPositional = nonPositionalFeatures.remove(sf); + removed = removedNonPositional; + } + + /* + * if not found, try nested features + */ + if (!removed && nestedFeatures != null) + { + removed = nestedFeatures.delete(sf); + } + + if (removed) + { + rescanAfterDelete(); + } + + return removed; + } + + /** + * Rescan all features to recompute any cached values after an entry has been + * deleted. This is expected to be an infrequent event, so performance here is + * not critical. + */ + protected synchronized void rescanAfterDelete() + { + positionalFeatureGroups.clear(); + nonPositionalFeatureGroups.clear(); + totalExtent = 0; + positionalMinScore = Float.NaN; + positionalMaxScore = Float.NaN; + nonPositionalMinScore = Float.NaN; + nonPositionalMaxScore = Float.NaN; + + /* + * scan non-positional features for groups and scores + */ + for (SequenceFeature sf : getNonPositionalFeatures()) + { + nonPositionalFeatureGroups.add(sf.getFeatureGroup()); + float score = sf.getScore(); + nonPositionalMinScore = min(nonPositionalMinScore, score); + nonPositionalMaxScore = max(nonPositionalMaxScore, score); + } + + /* + * scan positional features for groups, scores and extents + */ + for (SequenceFeature sf : getPositionalFeatures()) + { + positionalFeatureGroups.add(sf.getFeatureGroup()); + float score = sf.getScore(); + positionalMinScore = min(positionalMinScore, score); + positionalMaxScore = max(positionalMaxScore, score); + totalExtent += getFeatureLength(sf); + } + } + + /** + * A helper method to return the minimum of two floats, where a non-NaN value + * is treated as 'less than' a NaN value (unlike Math.min which does the + * opposite) + * + * @param f1 + * @param f2 + */ + protected static float min(float f1, float f2) + { + if (Float.isNaN(f1)) + { + return Float.isNaN(f2) ? f1 : f2; + } + else + { + return Float.isNaN(f2) ? f1 : Math.min(f1, f2); + } + } + + /** + * A helper method to return the maximum of two floats, where a non-NaN value + * is treated as 'greater than' a NaN value (unlike Math.max which does the + * opposite) + * + * @param f1 + * @param f2 + */ + protected static float max(float f1, float f2) + { + if (Float.isNaN(f1)) + { + return Float.isNaN(f2) ? f1 : f2; + } + else + { + return Float.isNaN(f2) ? f1 : Math.max(f1, f2); + } + } + + /** + * Answers true if this store has no features, else false + * + * @return + */ + public boolean isEmpty() + { + boolean hasFeatures = !nonNestedFeatures.isEmpty() + || (contactFeatureStarts != null && !contactFeatureStarts + .isEmpty()) + || (nonPositionalFeatures != null && !nonPositionalFeatures + .isEmpty()) + || (nestedFeatures != null && nestedFeatures.size() > 0); + + return !hasFeatures; + } + + /** + * Answers the set of distinct feature groups stored, possibly including null, + * as an unmodifiable view of the set. The parameter determines whether the + * groups for positional or for non-positional features are returned. + * + * @param positionalFeatures + * @return + */ + public Set getFeatureGroups(boolean positionalFeatures) + { + if (positionalFeatures) + { + return Collections.unmodifiableSet(positionalFeatureGroups); + } + else + { + return nonPositionalFeatureGroups == null ? Collections + . emptySet() : Collections + .unmodifiableSet(nonPositionalFeatureGroups); + } + } + + /** + * Performs a binary search of the (sorted) list to find the index of the + * first entry which returns true for the given comparator function. Returns + * the length of the list if there is no such entry. + * + * @param features + * @param sc + * @return + */ + protected static int binarySearch(List features, + SearchCriterion sc) + { + int start = 0; + int end = features.size() - 1; + int matched = features.size(); + + while (start <= end) + { + int mid = (start + end) / 2; + SequenceFeature entry = features.get(mid); + boolean compare = sc.compare(entry); + if (compare) + { + matched = mid; + end = mid - 1; + } + else + { + start = mid + 1; + } + } + + return matched; + } + + /** + * Answers the number of positional (or non-positional) features stored. + * Contact features count as 1. + * + * @param positional + * @return + */ + public int getFeatureCount(boolean positional) + { + if (!positional) + { + return nonPositionalFeatures == null ? 0 : nonPositionalFeatures + .size(); + } + + int size = nonNestedFeatures.size(); + + if (contactFeatureStarts != null) + { + // note a contact feature (start/end) counts as one + size += contactFeatureStarts.size(); + } + + if (nestedFeatures != null) + { + size += nestedFeatures.size(); + } + + return size; + } + + /** + * Answers the total length of positional features (or zero if there are + * none). Contact features contribute a value of 1 to the total. + * + * @return + */ + public int getTotalFeatureLength() + { + return totalExtent; + } + + /** + * Answers the minimum score held for positional or non-positional features. + * This may be Float.NaN if there are no features, are none has a non-NaN + * score. + * + * @param positional + * @return + */ + public float getMinimumScore(boolean positional) + { + return positional ? positionalMinScore : nonPositionalMinScore; + } + + /** + * Answers the maximum score held for positional or non-positional features. + * This may be Float.NaN if there are no features, are none has a non-NaN + * score. + * + * @param positional + * @return + */ + public float getMaximumScore(boolean positional) + { + return positional ? positionalMaxScore : nonPositionalMaxScore; + } + + /** + * Answers a list of all either positional or non-positional features whose + * feature group matches the given group (which may be null) + * + * @param positional + * @param group + * @return + */ + public List getFeaturesForGroup(boolean positional, + String group) + { + List result = new ArrayList<>(); + + /* + * if we know features don't include the target group, no need + * to inspect them for matches + */ + if (positional && !positionalFeatureGroups.contains(group) + || !positional && !nonPositionalFeatureGroups.contains(group)) + { + return result; + } + + List sfs = positional ? getPositionalFeatures() + : getNonPositionalFeatures(); + for (SequenceFeature sf : sfs) + { + String featureGroup = sf.getFeatureGroup(); + if (group == null && featureGroup == null || group != null + && group.equals(featureGroup)) + { + result.add(sf); + } + } + return result; + } + + /** + * Adds the shift value to the start and end of all positional features. + * Returns true if at least one feature was updated, else false. + * + * @param shift + * @return + */ + public synchronized boolean shiftFeatures(int shift) + { + /* + * Because begin and end are final fields (to ensure the data store's + * integrity), we have to delete each feature and re-add it as amended. + * (Although a simple shift of all values would preserve data integrity!) + */ + boolean modified = false; + for (SequenceFeature sf : getPositionalFeatures()) + { + modified = true; + int newBegin = sf.getBegin() + shift; + int newEnd = sf.getEnd() + shift; + + /* + * sanity check: don't shift left of the first residue + */ + if (newEnd > 0) + { + newBegin = Math.max(1, newBegin); + SequenceFeature sf2 = new SequenceFeature(sf, newBegin, newEnd, + sf.getFeatureGroup(), sf.getScore()); + addFeature(sf2); + } + delete(sf); + } + return modified; + } +} diff --git a/src/jalview/datamodel/features/NCList.java b/src/jalview/datamodel/features/NCList.java new file mode 100644 index 0000000..b8160d3 --- /dev/null +++ b/src/jalview/datamodel/features/NCList.java @@ -0,0 +1,626 @@ +package jalview.datamodel.features; + +import jalview.datamodel.ContiguousI; +import jalview.datamodel.Range; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * An adapted implementation of NCList as described in the paper + * + *

+ * Nested Containment List (NCList): a new algorithm for accelerating
+ * interval query of genome alignment and interval databases
+ * - Alexander V. Alekseyenko, Christopher J. Lee
+ * https://doi.org/10.1093/bioinformatics/btl647
+ * 
+ */ +public class NCList +{ + /* + * the number of ranges represented + */ + private int size; + + /* + * a list, in start position order, of sublists of ranges ordered so + * that each contains (or is the same as) the one that follows it + */ + private List> subranges; + + /** + * Constructor given a list of things that are each located on a contiguous + * interval. Note that the constructor may reorder the list. + *

+ * We assume here that for each range, start <= end. Behaviour for reverse + * ordered ranges is undefined. + * + * @param ranges + */ + public NCList(List ranges) + { + this(); + build(ranges); + } + + /** + * Sort and group ranges into sublists where each sublist represents a region + * and its contained subregions + * + * @param ranges + */ + protected void build(List ranges) + { + /* + * sort by start ascending so that contained intervals + * follow their containing interval + */ + Collections.sort(ranges, RangeComparator.BY_START_POSITION); + + List sublists = buildSubranges(ranges); + + /* + * convert each subrange to an NCNode consisting of a range and + * (possibly) its contained NCList + */ + for (Range sublist : sublists) + { + subranges.add(new NCNode(ranges.subList(sublist.start, + sublist.end + 1))); + } + + size = ranges.size(); + } + + public NCList(T entry) + { + this(); + subranges.add(new NCNode<>(entry)); + size = 1; + } + + public NCList() + { + subranges = new ArrayList>(); + } + + /** + * Traverses the sorted ranges to identify sublists, within which each + * interval contains the one that follows it + * + * @param ranges + * @return + */ + protected List buildSubranges(List ranges) + { + List sublists = new ArrayList<>(); + + if (ranges.isEmpty()) + { + return sublists; + } + + int listStartIndex = 0; + long lastEndPos = Long.MAX_VALUE; + + for (int i = 0; i < ranges.size(); i++) + { + ContiguousI nextInterval = ranges.get(i); + long nextStart = nextInterval.getBegin(); + long nextEnd = nextInterval.getEnd(); + if (nextStart > lastEndPos || nextEnd > lastEndPos) + { + /* + * this interval is not contained in the preceding one + * close off the last sublist + */ + sublists.add(new Range(listStartIndex, i - 1)); + listStartIndex = i; + } + lastEndPos = nextEnd; + } + + sublists.add(new Range(listStartIndex, ranges.size() - 1)); + return sublists; + } + + /** + * Adds one entry to the stored set (with duplicates allowed) + * + * @param entry + */ + public void add(T entry) + { + add(entry, true); + } + + /** + * Adds one entry to the stored set, and returns true, unless allowDuplicates + * is set to false and it is already contained (by object equality test), in + * which case it is not added and this method returns false. + * + * @param entry + * @param allowDuplicates + * @return + */ + public synchronized boolean add(T entry, boolean allowDuplicates) + { + if (!allowDuplicates && contains(entry)) + { + return false; + } + + size++; + long start = entry.getBegin(); + long end = entry.getEnd(); + + /* + * cases: + * - precedes all subranges: add as NCNode on front of list + * - follows all subranges: add as NCNode on end of list + * - enclosed by a subrange - add recursively to subrange + * - encloses one or more subranges - push them inside it + * - none of the above - add as a new node and resort nodes list (?) + */ + + /* + * find the first subrange whose end does not precede entry's start + */ + int candidateIndex = findFirstOverlap(start); + if (candidateIndex == -1) + { + /* + * all subranges precede this one - add it on the end + */ + subranges.add(new NCNode<>(entry)); + return true; + } + + /* + * search for maximal span of subranges i-k that the new entry + * encloses; or a subrange that encloses the new entry + */ + boolean enclosing = false; + int firstEnclosed = 0; + int lastEnclosed = 0; + boolean overlapping = false; + + for (int j = candidateIndex; j < subranges.size(); j++) + { + NCNode subrange = subranges.get(j); + + if (end < subrange.getBegin() && !overlapping && !enclosing) + { + /* + * new entry lies between subranges j-1 j + */ + subranges.add(j, new NCNode<>(entry)); + return true; + } + + if (subrange.getBegin() <= start && subrange.getEnd() >= end) + { + /* + * push new entry inside this subrange as it encloses it + */ + subrange.add(entry); + return true; + } + + if (start <= subrange.getBegin()) + { + if (end >= subrange.getEnd()) + { + /* + * new entry encloses this subrange (and possibly preceding ones); + * continue to find the maximal list it encloses + */ + if (!enclosing) + { + firstEnclosed = j; + } + lastEnclosed = j; + enclosing = true; + continue; + } + else + { + /* + * entry spans from before this subrange to inside it + */ + if (enclosing) + { + /* + * entry encloses one or more preceding subranges + */ + addEnclosingRange(entry, firstEnclosed, lastEnclosed); + return true; + } + else + { + /* + * entry spans two subranges but doesn't enclose any + * so just add it + */ + subranges.add(j, new NCNode<>(entry)); + return true; + } + } + } + else + { + overlapping = true; + } + } + + /* + * drops through to here if new range encloses all others + * or overlaps the last one + */ + if (enclosing) + { + addEnclosingRange(entry, firstEnclosed, lastEnclosed); + } + else + { + subranges.add(new NCNode<>(entry)); + } + + return true; + } + + /** + * Answers true if this NCList contains the given entry (by object equality + * test), else false + * + * @param entry + * @return + */ + public boolean contains(T entry) + { + /* + * find the first sublist that might overlap, i.e. + * the first whose end position is >= from + */ + int candidateIndex = findFirstOverlap(entry.getBegin()); + + if (candidateIndex == -1) + { + return false; + } + + int to = entry.getEnd(); + + for (int i = candidateIndex; i < subranges.size(); i++) + { + NCNode candidate = subranges.get(i); + if (candidate.getBegin() > to) + { + /* + * we are past the end of our target range + */ + break; + } + if (candidate.contains(entry)) + { + return true; + } + } + return false; + } + + /** + * Update the tree so that the range of the new entry encloses subranges i to + * j (inclusive). That is, replace subranges i-j (inclusive) with a new + * subrange that contains them. + * + * @param entry + * @param i + * @param j + */ + protected synchronized void addEnclosingRange(T entry, final int i, + final int j) + { + NCList newNCList = new NCList<>(); + newNCList.addNodes(subranges.subList(i, j + 1)); + NCNode newNode = new NCNode<>(entry, newNCList); + for (int k = j; k >= i; k--) + { + subranges.remove(k); + } + subranges.add(i, newNode); + } + + protected void addNodes(List> nodes) + { + for (NCNode node : nodes) + { + subranges.add(node); + size += node.size(); + } + } + + /** + * Returns a (possibly empty) list of items whose extent overlaps the given + * range + * + * @param from + * start of overlap range (inclusive) + * @param to + * end of overlap range (inclusive) + * @return + */ + public List findOverlaps(long from, long to) + { + List result = new ArrayList<>(); + + findOverlaps(from, to, result); + + return result; + } + + /** + * Recursively searches the NCList adding any items that overlap the from-to + * range to the result list + * + * @param from + * @param to + * @param result + */ + protected void findOverlaps(long from, long to, List result) + { + /* + * find the first sublist that might overlap, i.e. + * the first whose end position is >= from + */ + int candidateIndex = findFirstOverlap(from); + + if (candidateIndex == -1) + { + return; + } + + for (int i = candidateIndex; i < subranges.size(); i++) + { + NCNode candidate = subranges.get(i); + if (candidate.getBegin() > to) + { + /* + * we are past the end of our target range + */ + break; + } + candidate.findOverlaps(from, to, result); + } + + } + + /** + * Search subranges for the first one whose end position is not before the + * target range's start position, i.e. the first one that may overlap the + * target range. Returns the index in the list of the first such range found, + * or -1 if none found. + * + * @param from + * @return + */ + protected int findFirstOverlap(long from) + { + /* + * The NCList paper describes binary search for this step, + * but this not implemented here as (a) I haven't understood it yet + * and (b) it seems to imply complications for adding to an NCList + */ + + int i = 0; + if (subranges != null) + { + for (NCNode subrange : subranges) + { + if (subrange.getEnd() >= from) + { + return i; + } + i++; + } + } + return -1; + } + + /** + * Formats the tree as a bracketed list e.g. + * + *

+   * [1-100 [10-30 [10-20]], 15-30 [20-20]]
+   * 
+ */ + @Override + public String toString() + { + return subranges.toString(); + } + + /** + * Returns a string representation of the data where containment is shown by + * indentation on new lines + * + * @return + */ + public String prettyPrint() + { + StringBuilder sb = new StringBuilder(512); + int offset = 0; + int indent = 2; + prettyPrint(sb, offset, indent); + sb.append(System.lineSeparator()); + return sb.toString(); + } + + /** + * @param sb + * @param offset + * @param indent + */ + void prettyPrint(StringBuilder sb, int offset, int indent) + { + boolean first = true; + for (NCNode subrange : subranges) + { + if (!first) + { + sb.append(System.lineSeparator()); + } + first = false; + subrange.prettyPrint(sb, offset, indent); + } + } + + /** + * Answers true if the data held satisfy the rules of construction of an + * NCList, else false. + * + * @return + */ + public boolean isValid() + { + return isValid(Integer.MIN_VALUE, Integer.MAX_VALUE); + } + + /** + * Answers true if the data held satisfy the rules of construction of an + * NCList bounded within the given start-end range, else false. + *

+ * Each subrange must lie within start-end (inclusive). Subranges must be + * ordered by start position ascending. + *

+ * + * @param start + * @param end + * @return + */ + boolean isValid(final int start, final int end) + { + int lastStart = start; + for (NCNode subrange : subranges) + { + if (subrange.getBegin() < lastStart) + { + System.err.println("error in NCList: range " + subrange.toString() + + " starts before " + lastStart); + return false; + } + if (subrange.getEnd() > end) + { + System.err.println("error in NCList: range " + subrange.toString() + + " ends after " + end); + return false; + } + lastStart = subrange.getBegin(); + + if (!subrange.isValid()) + { + return false; + } + } + return true; + } + + /** + * Answers the lowest start position enclosed by the ranges + * + * @return + */ + public int getStart() + { + return subranges.isEmpty() ? 0 : subranges.get(0).getBegin(); + } + + /** + * Returns the number of ranges held (deep count) + * + * @return + */ + public int size() + { + return size; + } + + /** + * Returns a list of all entries stored + * + * @return + */ + public List getEntries() + { + List result = new ArrayList<>(); + getEntries(result); + return result; + } + + /** + * Adds all contained entries to the given list + * + * @param result + */ + void getEntries(List result) + { + for (NCNode subrange : subranges) + { + subrange.getEntries(result); + } + } + + /** + * Deletes the given entry from the store, returning true if it was found (and + * deleted), else false. This method makes no assumption that the entry is in + * the 'expected' place in the store, in case it has been modified since it + * was added. Only the first 'same object' match is deleted, not 'equal' or + * multiple objects. + * + * @param entry + */ + public synchronized boolean delete(T entry) + { + if (entry == null) + { + return false; + } + for (int i = 0; i < subranges.size(); i++) + { + NCNode subrange = subranges.get(i); + NCList subRegions = subrange.getSubRegions(); + + if (subrange.getRegion() == entry) + { + /* + * if the subrange is rooted on this entry, promote its + * subregions (if any) to replace the subrange here; + * NB have to resort subranges after doing this since e.g. + * [10-30 [12-20 [16-18], 13-19]] + * after deleting 12-20, 16-18 is promoted to sibling of 13-19 + * but should follow it in the list of subranges of 10-30 + */ + subranges.remove(i); + if (subRegions != null) + { + subranges.addAll(subRegions.subranges); + Collections.sort(subranges, RangeComparator.BY_START_POSITION); + } + size--; + return true; + } + else + { + if (subRegions != null && subRegions.delete(entry)) + { + size--; + subrange.deleteSubRegionsIfEmpty(); + return true; + } + } + } + return false; + } +} diff --git a/src/jalview/datamodel/features/NCNode.java b/src/jalview/datamodel/features/NCNode.java new file mode 100644 index 0000000..007f3b1 --- /dev/null +++ b/src/jalview/datamodel/features/NCNode.java @@ -0,0 +1,255 @@ +package jalview.datamodel.features; + +import jalview.datamodel.ContiguousI; + +import java.util.ArrayList; +import java.util.List; + +/** + * Each node of the NCList tree consists of a range, and (optionally) the NCList + * of ranges it encloses + * + * @param + */ +class NCNode implements ContiguousI +{ + /* + * deep size (number of ranges included) + */ + private int size; + + private V region; + + /* + * null, or an object holding contained subregions of this nodes region + */ + private NCList subregions; + + /** + * Constructor given a list of ranges + * + * @param ranges + */ + NCNode(List ranges) + { + build(ranges); + } + + /** + * Constructor given a single range + * + * @param range + */ + NCNode(V range) + { + List ranges = new ArrayList<>(); + ranges.add(range); + build(ranges); + } + + NCNode(V entry, NCList newNCList) + { + region = entry; + subregions = newNCList; + size = 1 + newNCList.size(); + } + + /** + * @param ranges + */ + protected void build(List ranges) + { + size = ranges.size(); + + if (!ranges.isEmpty()) + { + region = ranges.get(0); + } + if (ranges.size() > 1) + { + subregions = new NCList(ranges.subList(1, ranges.size())); + } + } + + @Override + public int getBegin() + { + return region.getBegin(); + } + + @Override + public int getEnd() + { + return region.getEnd(); + } + + /** + * Formats the node as a bracketed list e.g. + * + *

+   * [1-100 [10-30 [10-20]], 15-30 [20-20]]
+   * 
+ */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(10 * size); + sb.append(region.getBegin()).append("-").append(region.getEnd()); + if (subregions != null) + { + sb.append(" ").append(subregions.toString()); + } + return sb.toString(); + } + + void prettyPrint(StringBuilder sb, int offset, int indent) { + for (int i = 0 ; i < offset ; i++) { + sb.append(" "); + } + sb.append(region.getBegin()).append("-").append(region.getEnd()); + if (subregions != null) + { + sb.append(System.lineSeparator()); + subregions.prettyPrint(sb, offset + 2, indent); + } + } + /** + * Add any ranges that overlap the from-to range to the result list + * + * @param from + * @param to + * @param result + */ + void findOverlaps(long from, long to, List result) + { + if (region.getBegin() <= to && region.getEnd() >= from) + { + result.add(region); + } + if (subregions != null) + { + subregions.findOverlaps(from, to, result); + } + } + + /** + * Add one range to this subrange + * + * @param entry + */ + synchronized void add(V entry) + { + if (entry.getBegin() < region.getBegin() || entry.getEnd() > region.getEnd()) { + throw new IllegalArgumentException(String.format( + "adding improper subrange %d-%d to range %d-%d", + entry.getBegin(), entry.getEnd(), region.getBegin(), + region.getEnd())); + } + if (subregions == null) + { + subregions = new NCList(entry); + } + else + { + subregions.add(entry); + } + size++; + } + + /** + * Answers true if the data held satisfy the rules of construction of an + * NCList, else false. + * + * @return + */ + boolean isValid() + { + /* + * we don't handle reverse ranges + */ + if (region != null && region.getBegin() > region.getEnd()) + { + return false; + } + if (subregions == null) + { + return true; + } + return subregions.isValid(getBegin(), getEnd()); + } + + /** + * Adds all contained entries to the given list + * + * @param entries + */ + void getEntries(List entries) + { + entries.add(region); + if (subregions != null) + { + subregions.getEntries(entries); + } + } + + /** + * Answers true if this object contains the given entry (by object equals + * test), else false + * + * @param entry + * @return + */ + boolean contains(V entry) + { + if (entry == null) + { + return false; + } + if (entry.equals(region)) + { + return true; + } + return subregions == null ? false : subregions.contains(entry); + } + + /** + * Answers the 'root' region modelled by this object + * + * @return + */ + V getRegion() + { + return region; + } + + /** + * Answers the (possibly null) contained regions within this object + * + * @return + */ + NCList getSubRegions() + { + return subregions; + } + + /** + * Nulls the subregion reference if it is empty (after a delete entry + * operation) + */ + void deleteSubRegionsIfEmpty() + { + if (subregions != null && subregions.size() == 0) + { + subregions = null; + } + } + + /** + * Answers the (deep) size of this node i.e. the number of ranges it models + * + * @return + */ + int size() + { + return size; + } +} diff --git a/src/jalview/datamodel/features/RangeComparator.java b/src/jalview/datamodel/features/RangeComparator.java new file mode 100644 index 0000000..26ffee1 --- /dev/null +++ b/src/jalview/datamodel/features/RangeComparator.java @@ -0,0 +1,78 @@ +package jalview.datamodel.features; + +import jalview.datamodel.ContiguousI; + +import java.util.Comparator; + +/** + * A comparator that orders ranges by either start position or end position + * ascending. If the position matches, ordering is resolved by end position (or + * start position). + * + * @author gmcarstairs + * + */ +public class RangeComparator implements Comparator +{ + public static final Comparator BY_START_POSITION = new RangeComparator( + true); + + public static final Comparator BY_END_POSITION = new RangeComparator( + false); + + boolean byStart; + + /** + * Constructor + * + * @param byStartPosition + * if true, order based on start position, if false by end position + */ + RangeComparator(boolean byStartPosition) + { + byStart = byStartPosition; + } + + @Override + public int compare(ContiguousI o1, ContiguousI o2) + { + int len1 = o1.getEnd() - o1.getBegin(); + int len2 = o2.getEnd() - o2.getBegin(); + + if (byStart) + { + return compare(o1.getBegin(), o2.getBegin(), len1, len2); + } + else + { + return compare(o1.getEnd(), o2.getEnd(), len1, len2); + } + } + + /** + * Compares two ranges for ordering + * + * @param pos1 + * first range positional ordering criterion + * @param pos2 + * second range positional ordering criterion + * @param len1 + * first range length ordering criterion + * @param len2 + * second range length ordering criterion + * @return + */ + public int compare(long pos1, long pos2, int len1, int len2) + { + int order = Long.compare(pos1, pos2); + if (order == 0) + { + /* + * if tied on position order, longer length sorts to left + * i.e. the negation of normal ordering by length + */ + order = -Integer.compare(len1, len2); + } + return order; + } +} diff --git a/src/jalview/datamodel/features/SequenceFeatures.java b/src/jalview/datamodel/features/SequenceFeatures.java new file mode 100644 index 0000000..52da8c7 --- /dev/null +++ b/src/jalview/datamodel/features/SequenceFeatures.java @@ -0,0 +1,455 @@ +package jalview.datamodel.features; + +import jalview.datamodel.ContiguousI; +import jalview.datamodel.SequenceFeature; +import jalview.io.gff.SequenceOntologyFactory; +import jalview.io.gff.SequenceOntologyI; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; + +/** + * A class that stores sequence features in a way that supports efficient + * querying by type and location (overlap). Intended for (but not limited to) + * storage of features for one sequence. + * + * @author gmcarstairs + * + */ +public class SequenceFeatures implements SequenceFeaturesI +{ + /** + * a comparator for sorting features by start position ascending + */ + private static Comparator FORWARD_STRAND = new Comparator() + { + @Override + public int compare(ContiguousI o1, ContiguousI o2) + { + return Integer.compare(o1.getBegin(), o2.getBegin()); + } + }; + + /** + * a comparator for sorting features by end position descending + */ + private static Comparator REVERSE_STRAND = new Comparator() + { + @Override + public int compare(ContiguousI o1, ContiguousI o2) + { + return Integer.compare(o2.getEnd(), o1.getEnd()); + } + }; + + /* + * map from feature type to structured store of features for that type + * null types are permitted (but not a good idea!) + */ + private Map featureStore; + + /** + * Constructor + */ + public SequenceFeatures() + { + /* + * use a TreeMap so that features are returned in alphabetical order of type + * ? wrap as a synchronized map for add and delete operations + */ + // featureStore = Collections + // .synchronizedSortedMap(new TreeMap()); + featureStore = new TreeMap(); + } + + /** + * Constructor given a list of features + */ + public SequenceFeatures(List features) + { + this(); + if (features != null) + { + for (SequenceFeature feature : features) + { + add(feature); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean add(SequenceFeature sf) + { + String type = sf.getType(); + if (type == null) + { + System.err.println("Feature type may not be null: " + sf.toString()); + return false; + } + + if (featureStore.get(type) == null) + { + featureStore.put(type, new FeatureStore()); + } + return featureStore.get(type).addFeature(sf); + } + + /** + * {@inheritDoc} + */ + @Override + public List findFeatures(int from, int to, + String... type) + { + List result = new ArrayList<>(); + + for (FeatureStore featureSet : varargToTypes(type)) + { + result.addAll(featureSet.findOverlappingFeatures(from, to)); + } + + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public List getAllFeatures(String... type) + { + List result = new ArrayList<>(); + + result.addAll(getPositionalFeatures(type)); + + result.addAll(getNonPositionalFeatures()); + + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public List getFeaturesByOntology(String... ontologyTerm) + { + if (ontologyTerm == null || ontologyTerm.length == 0) + { + return new ArrayList<>(); + } + + Set featureTypes = getFeatureTypes(ontologyTerm); + return getAllFeatures(featureTypes.toArray(new String[featureTypes + .size()])); + } + + /** + * {@inheritDoc} + */ + @Override + public int getFeatureCount(boolean positional, String... type) + { + int result = 0; + + for (FeatureStore featureSet : varargToTypes(type)) + { + result += featureSet.getFeatureCount(positional); + } + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public int getTotalFeatureLength(String... type) + { + int result = 0; + + for (FeatureStore featureSet : varargToTypes(type)) + { + result += featureSet.getTotalFeatureLength(); + } + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public List getPositionalFeatures(String... type) + { + List result = new ArrayList<>(); + + for (FeatureStore featureSet : varargToTypes(type)) + { + result.addAll(featureSet.getPositionalFeatures()); + } + return result; + } + + /** + * A convenience method that converts a vararg for feature types to an + * Iterable over matched feature sets in key order + * + * @param type + * @return + */ + protected Iterable varargToTypes(String... type) + { + if (type == null || type.length == 0) + { + /* + * no vararg parameter supplied - return all + */ + return featureStore.values(); + } + + List types = new ArrayList<>(); + List args = Arrays.asList(type); + for (Entry featureType : featureStore.entrySet()) + { + if (args.contains(featureType.getKey())) + { + types.add(featureType.getValue()); + } + } + return types; + } + + /** + * {@inheritDoc} + */ + @Override + public List getContactFeatures(String... type) + { + List result = new ArrayList<>(); + + for (FeatureStore featureSet : varargToTypes(type)) + { + result.addAll(featureSet.getContactFeatures()); + } + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public List getNonPositionalFeatures(String... type) + { + List result = new ArrayList<>(); + + for (FeatureStore featureSet : varargToTypes(type)) + { + result.addAll(featureSet.getNonPositionalFeatures()); + } + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean delete(SequenceFeature sf) + { + for (FeatureStore featureSet : featureStore.values()) + { + if (featureSet.delete(sf)) + { + return true; + } + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean hasFeatures() + { + for (FeatureStore featureSet : featureStore.values()) + { + if (!featureSet.isEmpty()) + { + return true; + } + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public Set getFeatureGroups(boolean positionalFeatures, + String... type) + { + Set groups = new HashSet<>(); + + for (FeatureStore featureSet : varargToTypes(type)) + { + groups.addAll(featureSet.getFeatureGroups(positionalFeatures)); + } + + return groups; + } + + /** + * {@inheritDoc} + */ + @Override + public Set getFeatureTypesForGroups(boolean positionalFeatures, + String... groups) + { + Set result = new HashSet<>(); + + for (Entry featureType : featureStore.entrySet()) + { + Set featureGroups = featureType.getValue().getFeatureGroups( + positionalFeatures); + for (String group : groups) + { + if (featureGroups.contains(group)) + { + /* + * yes this feature type includes one of the query groups + */ + result.add(featureType.getKey()); + break; + } + } + } + + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public Set getFeatureTypes(String... soTerm) + { + Set types = new HashSet<>(); + for (Entry entry : featureStore.entrySet()) + { + String type = entry.getKey(); + if (!entry.getValue().isEmpty() && isOntologyTerm(type, soTerm)) + { + types.add(type); + } + } + return types; + } + + /** + * Answers true if the given type is one of the specified sequence ontology + * terms (or a sub-type of one), or if no terms are supplied. Answers false if + * filter terms are specified and the given term does not match any of them. + * + * @param type + * @param soTerm + * @return + */ + protected boolean isOntologyTerm(String type, String... soTerm) + { + if (soTerm == null || soTerm.length == 0) + { + return true; + } + SequenceOntologyI so = SequenceOntologyFactory.getInstance(); + for (String term : soTerm) + { + if (so.isA(type, term)) + { + return true; + } + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public float getMinimumScore(String type, boolean positional) + { + return featureStore.containsKey(type) ? featureStore.get(type) + .getMinimumScore(positional) : Float.NaN; + } + + /** + * {@inheritDoc} + */ + @Override + public float getMaximumScore(String type, boolean positional) + { + return featureStore.containsKey(type) ? featureStore.get(type) + .getMaximumScore(positional) : Float.NaN; + } + + /** + * A convenience method to sort features by start position ascending (if on + * forward strand), or end position descending (if on reverse strand) + * + * @param features + * @param forwardStrand + */ + public static void sortFeatures(List features, + final boolean forwardStrand) + { + Collections.sort(features, forwardStrand ? FORWARD_STRAND + : REVERSE_STRAND); + } + + /** + * {@inheritDoc} This method is 'semi-optimised': it only inspects features + * for types that include the specified group, but has to inspect every + * feature of those types for matching feature group. This is efficient unless + * a sequence has features that share the same type but are in different + * groups - an unlikely case. + *

+ * For example, if RESNUM feature is created with group = PDBID, then features + * would only be retrieved for those sequences associated with the target + * PDBID (group). + */ + @Override + public List getFeaturesForGroup(boolean positional, + String group, String... type) + { + List result = new ArrayList<>(); + for (FeatureStore featureSet : varargToTypes(type)) + { + if (featureSet.getFeatureGroups(positional).contains(group)) + { + result.addAll(featureSet.getFeaturesForGroup(positional, group)); + } + } + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean shiftFeatures(int shift) + { + boolean modified = false; + for (FeatureStore fs : featureStore.values()) + { + modified |= fs.shiftFeatures(shift); + } + return modified; + } +} \ No newline at end of file diff --git a/src/jalview/datamodel/features/SequenceFeaturesI.java b/src/jalview/datamodel/features/SequenceFeaturesI.java new file mode 100644 index 0000000..58beca2 --- /dev/null +++ b/src/jalview/datamodel/features/SequenceFeaturesI.java @@ -0,0 +1,204 @@ +package jalview.datamodel.features; + +import jalview.datamodel.SequenceFeature; + +import java.util.List; +import java.util.Set; + +public interface SequenceFeaturesI +{ + + /** + * Adds one sequence feature to the store, and returns true, unless the + * feature is already contained in the store, in which case this method + * returns false. Containment is determined by SequenceFeature.equals() + * comparison. Answers false, and does not add the feature, if feature type is + * null. + * + * @param sf + */ + boolean add(SequenceFeature sf); + + /** + * Returns a (possibly empty) list of features, optionally restricted to + * specified types, which overlap the given (inclusive) sequence position + * range + * + * @param from + * @param to + * @param type + * @return + */ + List findFeatures(int from, int to, + String... type); + + /** + * Answers a list of all features stored, in no particular guaranteed order. + * Positional features may optionally be restricted to specified types, but + * all non-positional features (if any) are always returned. + *

+ * To filter non-positional features by type, use + * getNonPositionalFeatures(type). + * + * @param type + * @return + */ + List getAllFeatures(String... type); + + /** + * Answers a list of all positional (or non-positional) features which are in + * the specified feature group, optionally restricted to features of specified + * types. + * + * @param positional + * if true returns positional features, else non-positional features + * @param group + * the feature group to be matched (which may be null) + * @param type + * optional feature types to filter by + * @return + */ + List getFeaturesForGroup(boolean positional, + String group, String... type); + + /** + * Answers a list of all features stored, whose type either matches one of the + * given ontology terms, or is a specialisation of a term in the Sequence + * Ontology. Results are returned in no particular guaranteed order. + * + * @param ontologyTerm + * @return + */ + List getFeaturesByOntology(String... ontologyTerm); + + /** + * Answers the number of (positional or non-positional) features, optionally + * restricted to specified feature types. Contact features are counted as 1. + * + * @param positional + * @param type + * @return + */ + int getFeatureCount(boolean positional, String... type); + + /** + * Answers the total length of positional features, optionally restricted to + * specified feature types. Contact features are counted as length 1. + * + * @param type + * @return + */ + int getTotalFeatureLength(String... type); + + /** + * Answers a list of all positional features, optionally restricted to + * specified types, in no particular guaranteed order + * + * @param type + * @return + */ + List getPositionalFeatures( + String... type); + + /** + * Answers a list of all contact features, optionally restricted to specified + * types, in no particular guaranteed order + * + * @return + */ + List getContactFeatures(String... type); + + /** + * Answers a list of all non-positional features, optionally restricted to + * specified types, in no particular guaranteed order + * + * @param type + * if no type is specified, all are returned + * @return + */ + List getNonPositionalFeatures( + String... type); + + /** + * Deletes the given feature from the store, returning true if it was found + * (and deleted), else false. This method makes no assumption that the feature + * is in the 'expected' place in the store, in case it has been modified since + * it was added. + * + * @param sf + */ + boolean delete(SequenceFeature sf); + + /** + * Answers true if this store contains at least one feature, else false + * + * @return + */ + boolean hasFeatures(); + + /** + * Returns a set of the distinct feature groups present in the collection. The + * set may include null. The boolean parameter determines whether the groups + * for positional or for non-positional features are returned. The optional + * type parameter may be used to restrict to groups for specified feature + * types. + * + * @param positionalFeatures + * @param type + * @return + */ + Set getFeatureGroups(boolean positionalFeatures, + String... type); + + /** + * Answers the set of distinct feature types for which there is at least one + * feature with one of the given feature group(s). The boolean parameter + * determines whether the groups for positional or for non-positional features + * are returned. + * + * @param positionalFeatures + * @param groups + * @return + */ + Set getFeatureTypesForGroups( + boolean positionalFeatures, String... groups); + + /** + * Answers a set of the distinct feature types for which a feature is stored. + * The types may optionally be restricted to those which match, or are a + * subtype of, given sequence ontology terms + * + * @return + */ + Set getFeatureTypes(String... soTerm); + + /** + * Answers the minimum score held for positional or non-positional features + * for the specified type. This may be Float.NaN if there are no features, or + * none has a non-NaN score. + * + * @param type + * @param positional + * @return + */ + float getMinimumScore(String type, boolean positional); + + /** + * Answers the maximum score held for positional or non-positional features + * for the specified type. This may be Float.NaN if there are no features, or + * none has a non-NaN score. + * + * @param type + * @param positional + * @return + */ + float getMaximumScore(String type, boolean positional); + + /** + * Adds the shift amount to the start and end of all positional features, + * returning true if at least one feature was shifted, else false + * + * @param shift + */ + abstract boolean shiftFeatures(int shift); +} \ No newline at end of file diff --git a/src/jalview/datamodel/xdb/embl/EmblEntry.java b/src/jalview/datamodel/xdb/embl/EmblEntry.java index 4d09bdc..5784f04 100644 --- a/src/jalview/datamodel/xdb/embl/EmblEntry.java +++ b/src/jalview/datamodel/xdb/embl/EmblEntry.java @@ -367,7 +367,8 @@ public class EmblEntry System.err .println("Implementation Notice: EMBLCDS records not properly supported yet - Making up the CDNA region of this sequence... may be incorrect (" + sourceDb + ":" + getAccession() + ")"); - if (translationLength * 3 == (1 - codonStart + dna.getSequence().length)) + int dnaLength = dna.getLength(); + if (translationLength * 3 == (1 - codonStart + dnaLength)) { System.err .println("Not allowing for additional stop codon at end of cDNA fragment... !"); @@ -377,8 +378,7 @@ public class EmblEntry dnaToProteinMapping = new Mapping(product, exons, new int[] { 1, translationLength }, 3, 1); } - if ((translationLength + 1) * 3 == (1 - codonStart + dna - .getSequence().length)) + if ((translationLength + 1) * 3 == (1 - codonStart + dnaLength)) { System.err .println("Allowing for additional stop codon at end of cDNA fragment... will probably cause an error in VAMSAs!"); @@ -443,13 +443,27 @@ public class EmblEntry /* * add cds features to dna sequence */ - for (int xint = 0; exons != null && xint < exons.length; xint += 2) + String cds = feature.getName(); // "CDS" + for (int xint = 0; exons != null && xint < exons.length - 1; xint += 2) { - SequenceFeature sf = makeCdsFeature(exons, xint, proteinName, - proteinId, vals, codonStart); - sf.setType(feature.getName()); // "CDS" + int exonStart = exons[xint]; + int exonEnd = exons[xint + 1]; + int begin = Math.min(exonStart, exonEnd); + int end = Math.max(exonStart, exonEnd); + int exonNumber = xint / 2 + 1; + String desc = String.format("Exon %d for protein '%s' EMBLCDS:%s", + exonNumber, proteinName, proteinId); + + SequenceFeature sf = makeCdsFeature(cds, desc, begin, end, + sourceDb, vals); + sf.setEnaLocation(feature.getLocation()); - sf.setFeatureGroup(sourceDb); + boolean forwardStrand = exonStart <= exonEnd; + sf.setStrand(forwardStrand ? "+" : "-"); + sf.setPhase(String.valueOf(codonStart - 1)); + sf.setValue(FeatureProperties.EXONPOS, exonNumber); + sf.setValue(FeatureProperties.EXONPRODUCT, proteinName); + dna.addSequenceFeature(sf); } } @@ -563,33 +577,25 @@ public class EmblEntry /** * Helper method to construct a SequenceFeature for one cds range * - * @param exons - * array of cds [start, end, ...] positions - * @param exonStartIndex - * offset into the exons array - * @param proteinName - * @param proteinAccessionId + * @param type + * feature type ("CDS") + * @param desc + * description + * @param begin + * start position + * @param end + * end position + * @param group + * feature group * @param vals * map of 'miscellaneous values' for feature - * @param codonStart - * codon start position for CDS (1/2/3, normally 1) * @return */ - protected SequenceFeature makeCdsFeature(int[] exons, int exonStartIndex, - String proteinName, String proteinAccessionId, - Map vals, int codonStart) - { - int exonNumber = exonStartIndex / 2 + 1; - SequenceFeature sf = new SequenceFeature(); - sf.setBegin(Math.min(exons[exonStartIndex], exons[exonStartIndex + 1])); - sf.setEnd(Math.max(exons[exonStartIndex], exons[exonStartIndex + 1])); - sf.setDescription(String.format("Exon %d for protein '%s' EMBLCDS:%s", - exonNumber, proteinName, proteinAccessionId)); - sf.setPhase(String.valueOf(codonStart - 1)); - sf.setStrand(exons[exonStartIndex] <= exons[exonStartIndex + 1] ? "+" - : "-"); - sf.setValue(FeatureProperties.EXONPOS, exonNumber); - sf.setValue(FeatureProperties.EXONPRODUCT, proteinName); + protected SequenceFeature makeCdsFeature(String type, String desc, + int begin, int end, String group, Map vals) + { + SequenceFeature sf = new SequenceFeature(type, desc, begin, end, group); + if (!vals.isEmpty()) { StringBuilder sb = new StringBuilder(); diff --git a/src/jalview/datamodel/UniprotEntry.java b/src/jalview/datamodel/xdb/uniprot/UniprotEntry.java similarity index 90% rename from src/jalview/datamodel/UniprotEntry.java rename to src/jalview/datamodel/xdb/uniprot/UniprotEntry.java index 4cf0f13..a3537c9 100755 --- a/src/jalview/datamodel/UniprotEntry.java +++ b/src/jalview/datamodel/xdb/uniprot/UniprotEntry.java @@ -18,7 +18,9 @@ * along with Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ -package jalview.datamodel; +package jalview.datamodel.xdb.uniprot; + +import jalview.datamodel.PDBEntry; import java.util.Vector; @@ -36,7 +38,7 @@ public class UniprotEntry Vector accession; - Vector feature; + Vector feature; Vector dbrefs; @@ -47,12 +49,12 @@ public class UniprotEntry accession = items; } - public void setFeature(Vector items) + public void setFeature(Vector items) { feature = items; } - public Vector getFeature() + public Vector getFeature() { return feature; } diff --git a/src/jalview/datamodel/xdb/uniprot/UniprotFeature.java b/src/jalview/datamodel/xdb/uniprot/UniprotFeature.java new file mode 100644 index 0000000..4a359ff --- /dev/null +++ b/src/jalview/datamodel/xdb/uniprot/UniprotFeature.java @@ -0,0 +1,78 @@ +package jalview.datamodel.xdb.uniprot; + +/** + * A data model class for binding from Uniprot XML via uniprot_mapping.xml + */ +public class UniprotFeature +{ + private String type; + + private String description; + + private String status; + + private int begin; + + private int end; + + public String getType() + { + return type; + } + + public void setType(String t) + { + this.type = t; + } + + public String getDescription() + { + return description; + } + + public void setDescription(String d) + { + this.description = d; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String s) + { + this.status = s; + } + + public int getBegin() + { + return begin; + } + + public void setBegin(int b) + { + this.begin = b; + } + + public int getEnd() + { + return end; + } + + public void setEnd(int e) + { + this.end = e; + } + + public int getPosition() + { + return begin; + } + + public void setPosition(int p) + { + this.begin = p; + this.end = p; + } +} diff --git a/src/jalview/datamodel/UniprotFile.java b/src/jalview/datamodel/xdb/uniprot/UniprotFile.java similarity index 96% rename from src/jalview/datamodel/UniprotFile.java rename to src/jalview/datamodel/xdb/uniprot/UniprotFile.java index f0e38d8..9cc0391 100755 --- a/src/jalview/datamodel/UniprotFile.java +++ b/src/jalview/datamodel/xdb/uniprot/UniprotFile.java @@ -18,7 +18,7 @@ * along with Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ -package jalview.datamodel; +package jalview.datamodel.xdb.uniprot; import java.util.Vector; diff --git a/src/jalview/datamodel/UniprotProteinName.java b/src/jalview/datamodel/xdb/uniprot/UniprotProteinName.java similarity index 97% rename from src/jalview/datamodel/UniprotProteinName.java rename to src/jalview/datamodel/xdb/uniprot/UniprotProteinName.java index 0a317e6..2335e71 100755 --- a/src/jalview/datamodel/UniprotProteinName.java +++ b/src/jalview/datamodel/xdb/uniprot/UniprotProteinName.java @@ -18,7 +18,7 @@ * along with Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ -package jalview.datamodel; +package jalview.datamodel.xdb.uniprot; import java.util.Vector; diff --git a/src/jalview/datamodel/UniprotSequence.java b/src/jalview/datamodel/xdb/uniprot/UniprotSequence.java similarity index 97% rename from src/jalview/datamodel/UniprotSequence.java rename to src/jalview/datamodel/xdb/uniprot/UniprotSequence.java index 1150f1e..bdba73f 100755 --- a/src/jalview/datamodel/UniprotSequence.java +++ b/src/jalview/datamodel/xdb/uniprot/UniprotSequence.java @@ -18,7 +18,7 @@ * along with Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ -package jalview.datamodel; +package jalview.datamodel.xdb.uniprot; /** * Data model for the sequence returned by a Uniprot query diff --git a/src/jalview/ext/ensembl/EnsemblCdna.java b/src/jalview/ext/ensembl/EnsemblCdna.java index dc000c6..6d031b7 100644 --- a/src/jalview/ext/ensembl/EnsemblCdna.java +++ b/src/jalview/ext/ensembl/EnsemblCdna.java @@ -24,6 +24,9 @@ import jalview.datamodel.SequenceFeature; import jalview.io.gff.SequenceOntologyFactory; import jalview.io.gff.SequenceOntologyI; +import java.util.HashMap; +import java.util.Map; + import com.stevesoft.pat.Regex; /** @@ -44,6 +47,13 @@ public class EnsemblCdna extends EnsemblSeqProxy private static final Regex ACCESSION_REGEX = new Regex( "(ENS([A-Z]{3}|)[TG][0-9]{11}$)" + "|" + "(CCDS[0-9.]{3,}$)"); + private static Map params = new HashMap(); + + static + { + params.put("object_type", "transcript"); + } + /* * fetch exon features on genomic sequence (to identify the cdna regions) * and cds and variation features (to retain) @@ -128,4 +138,14 @@ public class EnsemblCdna extends EnsemblSeqProxy return false; } + /** + * Parameter object_type=cdna added to ensure cdna and not peptide is returned + * (JAL-2529) + */ + @Override + protected Map getAdditionalParameters() + { + return params; + } + } diff --git a/src/jalview/ext/ensembl/EnsemblGene.java b/src/jalview/ext/ensembl/EnsemblGene.java index 37c787b..915fa0a 100644 --- a/src/jalview/ext/ensembl/EnsemblGene.java +++ b/src/jalview/ext/ensembl/EnsemblGene.java @@ -26,6 +26,7 @@ import jalview.datamodel.AlignmentI; import jalview.datamodel.Sequence; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.SequenceFeatures; import jalview.io.gff.SequenceOntologyFactory; import jalview.io.gff.SequenceOntologyI; import jalview.schemes.FeatureColour; @@ -282,22 +283,20 @@ public class EnsemblGene extends EnsemblSeqProxy */ protected void clearGeneFeatures(SequenceI gene) { - SequenceFeature[] sfs = gene.getSequenceFeatures(); - if (sfs != null) + /* + * Note we include NMD_transcript_variant here because it behaves like + * 'transcript' in Ensembl, although strictly speaking it is not + * (it is a sub-type of sequence_variant) + */ + String[] soTerms = new String[] { + SequenceOntologyI.NMD_TRANSCRIPT_VARIANT, + SequenceOntologyI.TRANSCRIPT, SequenceOntologyI.EXON, + SequenceOntologyI.CDS }; + List sfs = gene.getFeatures().getFeaturesByOntology( + soTerms); + for (SequenceFeature sf : sfs) { - SequenceOntologyI so = SequenceOntologyFactory.getInstance(); - List filtered = new ArrayList(); - for (SequenceFeature sf : sfs) - { - String type = sf.getType(); - if (!isTranscript(type) && !so.isA(type, SequenceOntologyI.EXON) - && !so.isA(type, SequenceOntologyI.CDS)) - { - filtered.add(sf); - } - } - gene.setSequenceFeatures(filtered - .toArray(new SequenceFeature[filtered.size()])); + gene.deleteFeature(sf); } } @@ -347,6 +346,7 @@ public class EnsemblGene extends EnsemblSeqProxy { splices = findFeatures(gene, SequenceOntologyI.CDS, parentId); } + SequenceFeatures.sortFeatures(splices, true); int transcriptLength = 0; final char[] geneChars = gene.getSequence(); @@ -396,7 +396,7 @@ public class EnsemblGene extends EnsemblSeqProxy mapTo.add(new int[] { 1, transcriptLength }); MapList mapping = new MapList(mappedFrom, mapTo, 1, 1); EnsemblCdna cdna = new EnsemblCdna(getDomain()); - cdna.transferFeatures(gene.getSequenceFeatures(), + cdna.transferFeatures(gene.getFeatures().getPositionalFeatures(), transcript.getDatasetSequence(), mapping, parentId); /* @@ -437,19 +437,18 @@ public class EnsemblGene extends EnsemblSeqProxy List transcriptFeatures = new ArrayList(); String parentIdentifier = GENE_PREFIX + accId; - SequenceFeature[] sfs = geneSequence.getSequenceFeatures(); + // todo optimise here by transcript type! + List sfs = geneSequence.getFeatures() + .getPositionalFeatures(); - if (sfs != null) + for (SequenceFeature sf : sfs) { - for (SequenceFeature sf : sfs) + if (isTranscript(sf.getType())) { - if (isTranscript(sf.getType())) + String parent = (String) sf.getValue(PARENT); + if (parentIdentifier.equals(parent)) { - String parent = (String) sf.getValue(PARENT); - if (parentIdentifier.equals(parent)) - { - transcriptFeatures.add(sf); - } + transcriptFeatures.add(sf); } } } diff --git a/src/jalview/ext/ensembl/EnsemblSeqProxy.java b/src/jalview/ext/ensembl/EnsemblSeqProxy.java index 233707b..79d6c0a 100644 --- a/src/jalview/ext/ensembl/EnsemblSeqProxy.java +++ b/src/jalview/ext/ensembl/EnsemblSeqProxy.java @@ -30,6 +30,7 @@ import jalview.datamodel.DBRefSource; import jalview.datamodel.Mapping; 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; @@ -37,8 +38,8 @@ 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 jalview.util.RangeComparator; import java.io.IOException; import java.net.MalformedURLException; @@ -46,8 +47,9 @@ 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 java.util.Map; +import java.util.Map.Entry; /** * Base class for Ensembl sequence fetchers @@ -468,11 +470,31 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient urlstring.append("?type=").append(getSourceEnsemblType().getType()); urlstring.append(("&Accept=text/x-fasta")); + Map params = getAdditionalParameters(); + if (params != null) + { + for (Entry entry : params.entrySet()) + { + urlstring.append("&").append(entry.getKey()).append("=") + .append(entry.getValue()); + } + } + URL url = new URL(urlstring.toString()); return url; } /** + * Override this method to add any additional x=y URL parameters needed + * + * @return + */ + protected Map getAdditionalParameters() + { + return null; + } + + /** * A sequence/id POST request currently allows up to 50 queries * * @see http://rest.ensembl.org/documentation/info/sequence_id_post @@ -536,8 +558,10 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient protected MapList getGenomicRangesFromFeatures(SequenceI sourceSequence, String accId, int start) { - SequenceFeature[] sfs = sourceSequence.getSequenceFeatures(); - if (sfs == null) + // SequenceFeature[] sfs = sourceSequence.getSequenceFeatures(); + List sfs = sourceSequence.getFeatures() + .getPositionalFeatures(); + if (sfs.isEmpty()) { return null; } @@ -607,7 +631,8 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient * a final sort is needed since Ensembl returns CDS sorted within source * (havana / ensembl_havana) */ - Collections.sort(regions, new RangeComparator(direction == 1)); + Collections.sort(regions, direction == 1 ? IntRangeComparator.ASCENDING + : IntRangeComparator.DESCENDING); List to = Arrays.asList(new int[] { start, start + mappedLength - 1 }); @@ -658,13 +683,15 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient if (mappedRange != null) { - SequenceFeature copy = new SequenceFeature(sf); - copy.setBegin(Math.min(mappedRange[0], mappedRange[1])); - copy.setEnd(Math.max(mappedRange[0], mappedRange[1])); - if (".".equals(copy.getFeatureGroup())) + String group = sf.getFeatureGroup(); + if (".".equals(group)) { - copy.setFeatureGroup(getDbSource()); + 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); /* @@ -763,8 +790,10 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient return false; } - // long start = System.currentTimeMillis(); - SequenceFeature[] sfs = sourceSequence.getSequenceFeatures(); + long start = System.currentTimeMillis(); + // SequenceFeature[] sfs = sourceSequence.getSequenceFeatures(); + List sfs = sourceSequence.getFeatures() + .getPositionalFeatures(); MapList mapping = getGenomicRangesFromFeatures(sourceSequence, accessionId, targetSequence.getStart()); if (mapping == null) @@ -774,10 +803,10 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient boolean result = transferFeatures(sfs, targetSequence, mapping, accessionId); - // System.out.println("transferFeatures (" + (sfs.length) + " --> " - // + targetSequence.getSequenceFeatures().length + ") to " - // + targetSequence.getName() - // + " took " + (System.currentTimeMillis() - start) + "ms"); + System.out.println("transferFeatures (" + (sfs.size()) + " --> " + + targetSequence.getFeatures().getFeatureCount(true) + ") to " + + targetSequence.getName() + " took " + + (System.currentTimeMillis() - start) + "ms"); return result; } @@ -786,13 +815,13 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient * converted using the mapping. Features which do not overlap are ignored. * Features whose parent is not the specified identifier are also ignored. * - * @param features + * @param sfs * @param targetSequence * @param mapping * @param parentId * @return */ - protected boolean transferFeatures(SequenceFeature[] features, + protected boolean transferFeatures(List sfs, SequenceI targetSequence, MapList mapping, String parentId) { final boolean forwardStrand = mapping.isFromForwardStrand(); @@ -802,10 +831,10 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient * position descending if reverse strand) so as to add them in * 'forwards' order to the target sequence */ - sortFeatures(features, forwardStrand); + SequenceFeatures.sortFeatures(sfs, forwardStrand); boolean transferred = false; - for (SequenceFeature sf : features) + for (SequenceFeature sf : sfs) { if (retainFeature(sf, parentId)) { @@ -817,33 +846,6 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient } /** - * Sort features by start position ascending (if on forward strand), or end - * position descending (if on reverse strand) - * - * @param features - * @param forwardStrand - */ - protected static void sortFeatures(SequenceFeature[] features, - final boolean forwardStrand) - { - Arrays.sort(features, new Comparator() - { - @Override - public int compare(SequenceFeature o1, SequenceFeature o2) - { - if (forwardStrand) - { - return Integer.compare(o1.getBegin(), o2.getBegin()); - } - else - { - return Integer.compare(o2.getEnd(), o1.getEnd()); - } - } - }); - } - - /** * 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 @@ -885,35 +887,30 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient /** * Returns a (possibly empty) list of features on the sequence which have the - * specified sequence ontology type (or a sub-type of it), and the given + * specified sequence ontology term (or a sub-type of it), and the given * identifier as parent * * @param sequence - * @param type + * @param term * @param parentId * @return */ protected List findFeatures(SequenceI sequence, - String type, String parentId) + String term, String parentId) { List result = new ArrayList(); - SequenceFeature[] sfs = sequence.getSequenceFeatures(); - if (sfs != null) + List sfs = sequence.getFeatures() + .getFeaturesByOntology(term); + for (SequenceFeature sf : sfs) { - SequenceOntologyI so = SequenceOntologyFactory.getInstance(); - for (SequenceFeature sf : sfs) + String parent = (String) sf.getValue(PARENT); + if (parent != null && parent.equals(parentId)) { - if (so.isA(sf.getType(), type)) - { - String parent = (String) sf.getValue(PARENT); - if (parent.equals(parentId)) - { - result.add(sf); - } - } + result.add(sf); } } + return result; } diff --git a/src/jalview/ext/jmol/JmolParser.java b/src/jalview/ext/jmol/JmolParser.java index f08e40e..beaaf79 100644 --- a/src/jalview/ext/jmol/JmolParser.java +++ b/src/jalview/ext/jmol/JmolParser.java @@ -351,10 +351,10 @@ public class JmolParser extends StructureFile implements JmolStatusListener SequenceI sq, char[] secstr, char[] secstrcode, String chainId, int firstResNum) { - char[] seq = sq.getSequence(); + int length = sq.getLength(); boolean ssFound = false; - Annotation asecstr[] = new Annotation[seq.length + firstResNum - 1]; - for (int p = 0; p < seq.length; p++) + Annotation asecstr[] = new Annotation[length + firstResNum - 1]; + for (int p = 0; p < length; p++) { if (secstr[p] >= 'A' && secstr[p] <= 'z') { diff --git a/src/jalview/ext/rbvi/chimera/AtomSpecModel.java b/src/jalview/ext/rbvi/chimera/AtomSpecModel.java index d62cc3c..f3c9c1e 100644 --- a/src/jalview/ext/rbvi/chimera/AtomSpecModel.java +++ b/src/jalview/ext/rbvi/chimera/AtomSpecModel.java @@ -1,6 +1,6 @@ package jalview.ext.rbvi.chimera; -import jalview.util.RangeComparator; +import jalview.util.IntRangeComparator; import java.util.ArrayList; import java.util.Collections; @@ -107,7 +107,7 @@ public class AtomSpecModel /* * sort ranges into ascending start position order */ - Collections.sort(rangeList, new RangeComparator(true)); + Collections.sort(rangeList, IntRangeComparator.ASCENDING); int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0]; int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1]; diff --git a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java index 62aaa1c..e9ce49b 100644 --- a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java +++ b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java @@ -411,12 +411,8 @@ public class ChimeraCommands StructureMapping mapping, SequenceI seq, Map> theMap, int modelNumber) { - SequenceFeature[] sfs = seq.getSequenceFeatures(); - if (sfs == null) - { - return; - } - + List sfs = seq.getFeatures().getPositionalFeatures( + visibleFeatures.toArray(new String[visibleFeatures.size()])); for (SequenceFeature sf : sfs) { String type = sf.getType(); @@ -427,7 +423,7 @@ public class ChimeraCommands */ boolean isFromViewer = JalviewChimeraBinding.CHIMERA_FEATURE_GROUP .equals(sf.getFeatureGroup()); - if (isFromViewer || !visibleFeatures.contains(type)) + if (isFromViewer) { continue; } diff --git a/src/jalview/gui/AlignFrame.java b/src/jalview/gui/AlignFrame.java index 2a4b6dc..162d100 100644 --- a/src/jalview/gui/AlignFrame.java +++ b/src/jalview/gui/AlignFrame.java @@ -2442,7 +2442,6 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener, viewport.setSelectionGroup(null); viewport.getColumnSelection().clear(); viewport.setSelectionGroup(null); - alignPanel.getSeqPanel().seqCanvas.highlightSearchResults(null); alignPanel.getIdPanel().getIdCanvas().searchResults = null; // JAL-2034 - should delegate to // alignPanel to decide if overview needs diff --git a/src/jalview/gui/AlignmentPanel.java b/src/jalview/gui/AlignmentPanel.java index e62707f..fc687b4 100644 --- a/src/jalview/gui/AlignmentPanel.java +++ b/src/jalview/gui/AlignmentPanel.java @@ -30,10 +30,12 @@ import jalview.datamodel.SearchResultsI; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceGroup; import jalview.datamodel.SequenceI; +import jalview.io.HTMLOutput; import jalview.jbgui.GAlignmentPanel; import jalview.math.AlignmentDimension; import jalview.schemes.ResidueProperties; import jalview.structure.StructureSelectionManager; +import jalview.util.Comparison; import jalview.util.MessageManager; import jalview.util.Platform; import jalview.viewmodel.ViewportListenerI; @@ -341,19 +343,11 @@ public class AlignmentPanel extends GAlignmentPanel implements */ public void highlightSearchResults(SearchResultsI results) { - scrollToPosition(results); - getSeqPanel().seqCanvas.highlightSearchResults(results); - } + boolean scrolled = scrollToPosition(results, 0, true, false); - /** - * Scroll the view to show the position of the highlighted region in results - * (if any) and redraw the overview - * - * @param results - */ - public boolean scrollToPosition(SearchResultsI results) - { - return scrollToPosition(results, 0, true, false); + boolean noFastPaint = scrolled && av.getWrapAlignment(); + + getSeqPanel().seqCanvas.highlightSearchResults(results, noFastPaint); } /** @@ -371,8 +365,10 @@ public class AlignmentPanel extends GAlignmentPanel implements } /** - * Scroll the view to show the position of the highlighted region in results - * (if any) + * Scrolls the view (if necessary) to show the position of the first + * highlighted region in results (if any). Answers true if the view was + * scrolled, or false if no matched region was found, or it is already + * visible. * * @param results * @param verticalOffset @@ -382,116 +378,117 @@ public class AlignmentPanel extends GAlignmentPanel implements * - when set, the overview will be recalculated (takes longer) * @param centre * if true, try to centre the search results horizontally in the view - * @return false if results were not found + * @return */ - public boolean scrollToPosition(SearchResultsI results, + protected boolean scrollToPosition(SearchResultsI results, int verticalOffset, boolean redrawOverview, boolean centre) { int startv, endv, starts, ends; - // TODO: properly locate search results in view when large numbers of hidden - // columns exist before highlighted region - // do we need to scroll the panel? - // TODO: tons of nullpointerexceptions raised here. - if (results != null && results.getSize() > 0 && av != null - && av.getAlignment() != null) - { - int seqIndex = av.getAlignment().findIndex(results); - if (seqIndex == -1) - { - return false; - } - SequenceI seq = av.getAlignment().getSequenceAt(seqIndex); - int[] r = results.getResults(seq, 0, av.getAlignment().getWidth()); - if (r == null) - { - return false; - } - int start = r[0]; - int end = r[1]; + if (results == null || results.isEmpty() || av == null + || av.getAlignment() == null) + { + return false; + } + int seqIndex = av.getAlignment().findIndex(results); + if (seqIndex == -1) + { + return false; + } + SequenceI seq = av.getAlignment().getSequenceAt(seqIndex); - /* - * To centre results, scroll to positions half the visible width - * left/right of the start/end positions - */ - if (centre) - { - int offset = (vpRanges.getEndRes() - vpRanges.getStartRes() + 1) / 2 - - 1; - start = Math.max(start - offset, 0); - end = end + offset - 1; - } - if (start < 0) - { - return false; - } - if (end == seq.getEnd()) - { - return false; - } - if (av.hasHiddenColumns()) + int[] r = results.getResults(seq, 0, av.getAlignment().getWidth()); + if (r == null) + { + return false; + } + int start = r[0]; + int end = r[1]; + + /* + * To centre results, scroll to positions half the visible width + * left/right of the start/end positions + */ + if (centre) + { + int offset = (vpRanges.getEndRes() - vpRanges.getStartRes() + 1) / 2 - 1; + start = Math.max(start - offset, 0); + end = end + offset - 1; + } + if (start < 0) + { + return false; + } + if (end == seq.getEnd()) + { + return false; + } + + if (av.hasHiddenColumns()) + { + HiddenColumns hidden = av.getAlignment().getHiddenColumns(); + start = hidden.findColumnPosition(start); + end = hidden.findColumnPosition(end); + if (start == end) { - HiddenColumns hidden = av.getAlignment().getHiddenColumns(); - start = hidden.findColumnPosition(start); - end = hidden.findColumnPosition(end); - if (start == end) + if (!hidden.isVisible(r[0])) { - if (!hidden.isVisible(r[0])) - { - // don't scroll - position isn't visible - return false; - } + // don't scroll - position isn't visible + return false; } } + } - /* - * allow for offset of target sequence (actually scroll to one above it) - */ - seqIndex = Math.max(0, seqIndex - verticalOffset); + /* + * allow for offset of target sequence (actually scroll to one above it) + */ + seqIndex = Math.max(0, seqIndex - verticalOffset); + boolean scrollNeeded = true; - if (!av.getWrapAlignment()) + if (!av.getWrapAlignment()) + { + if ((startv = vpRanges.getStartRes()) >= start) { - if ((startv = vpRanges.getStartRes()) >= start) - { - /* - * Scroll left to make start of search results visible - */ - setScrollValues(start, seqIndex); - } - else if ((endv = vpRanges.getEndRes()) <= end) - { - /* - * Scroll right to make end of search results visible - */ - setScrollValues(startv + end - endv, seqIndex); - } - else if ((starts = vpRanges.getStartSeq()) > seqIndex) - { - /* - * Scroll up to make start of search results visible - */ - setScrollValues(vpRanges.getStartRes(), seqIndex); - } - else if ((ends = vpRanges.getEndSeq()) <= seqIndex) - { - /* - * Scroll down to make end of search results visible - */ - setScrollValues(vpRanges.getStartRes(), starts + seqIndex - ends - + 1); - } /* - * Else results are already visible - no need to scroll + * Scroll left to make start of search results visible */ + setScrollValues(start, seqIndex); } - else + else if ((endv = vpRanges.getEndRes()) <= end) + { + /* + * Scroll right to make end of search results visible + */ + setScrollValues(startv + end - endv, seqIndex); + } + else if ((starts = vpRanges.getStartSeq()) > seqIndex) + { + /* + * Scroll up to make start of search results visible + */ + setScrollValues(vpRanges.getStartRes(), seqIndex); + } + else if ((ends = vpRanges.getEndSeq()) <= seqIndex) { - vpRanges.scrollToWrappedVisible(start); + /* + * Scroll down to make end of search results visible + */ + setScrollValues(vpRanges.getStartRes(), starts + seqIndex - ends + + 1); } + /* + * Else results are already visible - no need to scroll + */ + scrollNeeded = false; + } + else + { + scrollNeeded = vpRanges.scrollToWrappedVisible(start); } paintAlignment(redrawOverview); - return true; + + return scrollNeeded; } /** @@ -1444,33 +1441,32 @@ public class AlignmentPanel extends GAlignmentPanel implements { try { - int s, sSize = av.getAlignment().getHeight(), res, alwidth = av - .getAlignment().getWidth(), g, gSize, f, fSize, sy; + int sSize = av.getAlignment().getHeight(); + int alwidth = av.getAlignment().getWidth(); PrintWriter out = new PrintWriter(new FileWriter(imgMapFile)); - out.println(jalview.io.HTMLOutput.getImageMapHTML()); + out.println(HTMLOutput.getImageMapHTML()); out.println("" + ""); - for (s = 0; s < sSize; s++) + for (int s = 0; s < sSize; s++) { - sy = s * av.getCharHeight() + scaleHeight; + int sy = s * av.getCharHeight() + scaleHeight; SequenceI seq = av.getAlignment().getSequenceAt(s); - SequenceFeature[] features = seq.getSequenceFeatures(); SequenceGroup[] groups = av.getAlignment().findAllGroups(seq); - for (res = 0; res < alwidth; res++) + for (int column = 0; column < alwidth; column++) { - StringBuilder text = new StringBuilder(); + StringBuilder text = new StringBuilder(512); String triplet = null; if (av.getAlignment().isNucleotide()) { triplet = ResidueProperties.nucleotideName.get(seq - .getCharAt(res) + ""); + .getCharAt(column) + ""); } else { - triplet = ResidueProperties.aa2Triplet.get(seq.getCharAt(res) + triplet = ResidueProperties.aa2Triplet.get(seq.getCharAt(column) + ""); } @@ -1479,84 +1475,73 @@ public class AlignmentPanel extends GAlignmentPanel implements continue; } - int alIndex = seq.findPosition(res); - gSize = groups.length; - for (g = 0; g < gSize; g++) + int seqPos = seq.findPosition(column); + int gSize = groups.length; + for (int g = 0; g < gSize; g++) { if (text.length() < 1) { text.append(" res) + if (groups[g].getStartRes() < column + && groups[g].getEndRes() > column) { text.append("
").append(groups[g].getName()) .append(""); } } - if (features != null) + if (text.length() < 1) { - if (text.length() < 1) - { - text.append(" features = seq.findFeatures(column, column); + for (SequenceFeature sf : features) { - - if ((features[f].getBegin() <= seq.findPosition(res)) - && (features[f].getEnd() >= seq.findPosition(res))) + if (sf.isContactFeature()) { - if (features[f].isContactFeature()) - { - if (features[f].getBegin() == seq.findPosition(res) - || features[f].getEnd() == seq - .findPosition(res)) - { - text.append("
").append(features[f].getType()) - .append(" ").append(features[f].getBegin()) - .append(":").append(features[f].getEnd()); - } - } - else + text.append("
").append(sf.getType()).append(" ") + .append(sf.getBegin()).append(":") + .append(sf.getEnd()); + } + else + { + text.append("
"); + text.append(sf.getType()); + String description = sf.getDescription(); + if (description != null + && !sf.getType().equals(description)) { - text.append("
"); - text.append(features[f].getType()); - if (features[f].getDescription() != null - && !features[f].getType().equals( - features[f].getDescription())) - { - text.append(" ").append(features[f].getDescription()); - } - - if (features[f].getValue("status") != null) - { - text.append(" (").append(features[f].getValue("status")) - .append(")"); - } + description = description.replace("\"", """); + text.append(" ").append(description); } } - + String status = sf.getStatus(); + if (status != null && !"".equals(status)) + { + text.append(" (").append(status).append(")"); + } + } + if (text.length() > 1) + { + text.append("')\"; onMouseOut=\"toolTip()\"; href=\"#\">"); + out.println(text.toString()); } - } - if (text.length() > 1) - { - text.append("')\"; onMouseOut=\"toolTip()\"; href=\"#\">"); - out.println(text.toString()); } } } @@ -1825,7 +1810,7 @@ public class AlignmentPanel extends GAlignmentPanel implements * @param verticalOffset * the number of visible sequences to show above the mapped region */ - public void scrollToCentre(SearchResultsI sr, int verticalOffset) + protected void scrollToCentre(SearchResultsI sr, int verticalOffset) { /* * To avoid jumpy vertical scrolling (if some sequences are gapped or not diff --git a/src/jalview/gui/AnnotationExporter.java b/src/jalview/gui/AnnotationExporter.java index 0d47e36..42913de 100644 --- a/src/jalview/gui/AnnotationExporter.java +++ b/src/jalview/gui/AnnotationExporter.java @@ -34,6 +34,7 @@ import java.awt.Color; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.util.List; import java.util.Map; import javax.swing.BorderFactory; @@ -156,28 +157,22 @@ public class AnnotationExporter extends JPanel .getString("label.no_features_on_alignment"); if (features) { - Map displayedFeatureColours = ap - .getFeatureRenderer().getDisplayedFeatureCols(); FeaturesFile formatter = new FeaturesFile(); SequenceI[] sequences = ap.av.getAlignment().getSequencesArray(); Map featureColours = ap.getFeatureRenderer() .getDisplayedFeatureCols(); + List featureGroups = ap.getFeatureRenderer() + .getDisplayedFeatureGroups(); boolean includeNonPositional = ap.av.isShowNPFeats(); if (GFFFormat.isSelected()) { - text = new FeaturesFile().printGffFormat(ap.av.getAlignment() - .getDataset().getSequencesArray(), displayedFeatureColours, - true, ap.av.isShowNPFeats()); - text = formatter.printGffFormat(sequences, featureColours, true, - includeNonPositional); + text = formatter.printGffFormat(sequences, featureColours, + featureGroups, includeNonPositional); } else { - text = new FeaturesFile().printJalviewFormat(ap.av.getAlignment() - .getDataset().getSequencesArray(), displayedFeatureColours, - true, ap.av.isShowNPFeats()); // ap.av.featuresDisplayed); text = formatter.printJalviewFormat(sequences, featureColours, - true, includeNonPositional); + featureGroups, includeNonPositional); } } else diff --git a/src/jalview/gui/AppVarna.java b/src/jalview/gui/AppVarna.java index a50de77..079645f 100644 --- a/src/jalview/gui/AppVarna.java +++ b/src/jalview/gui/AppVarna.java @@ -622,11 +622,10 @@ public class AppVarna extends JInternalFrame implements SelectionListener, ShiftList offset = new ShiftList(); int ofstart = -1; int sleng = seq.getLength(); - char[] seqChars = seq.getSequence(); for (int i = 0; i < sleng; i++) { - if (Comparison.isGap(seqChars[i])) + if (Comparison.isGap(seq.getCharAt(i))) { if (ofstart == -1) { diff --git a/src/jalview/gui/CutAndPasteTransfer.java b/src/jalview/gui/CutAndPasteTransfer.java index a5aa9eb..3eced2f 100644 --- a/src/jalview/gui/CutAndPasteTransfer.java +++ b/src/jalview/gui/CutAndPasteTransfer.java @@ -298,6 +298,7 @@ public class CutAndPasteTransfer extends GCutAndPasteTransfer AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT); af.getViewport().setShowSequenceFeatures(showSeqFeatures); af.getViewport().setFeaturesDisplayed(fd); + af.setMenusForViewport(); ColourSchemeI cs = ColourSchemeMapper.getJalviewColourScheme( colourSchemeName, al); if (cs != null) diff --git a/src/jalview/gui/FeatureColourChooser.java b/src/jalview/gui/FeatureColourChooser.java index 4172819..192fd23 100644 --- a/src/jalview/gui/FeatureColourChooser.java +++ b/src/jalview/gui/FeatureColourChooser.java @@ -31,6 +31,8 @@ import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; @@ -286,6 +288,14 @@ public class FeatureColourChooser extends JalviewDialog thresholdValue_actionPerformed(); } }); + thresholdValue.addFocusListener(new FocusAdapter() + { + @Override + public void focusLost(FocusEvent e) + { + thresholdValue_actionPerformed(); + } + }); slider.setPaintLabels(false); slider.setPaintTicks(true); slider.setBackground(Color.white); diff --git a/src/jalview/gui/FeatureRenderer.java b/src/jalview/gui/FeatureRenderer.java index 55c4323..6f6bc02 100644 --- a/src/jalview/gui/FeatureRenderer.java +++ b/src/jalview/gui/FeatureRenderer.java @@ -271,7 +271,8 @@ public class FeatureRenderer extends highlight.addResult(sequences.get(0), sf.getBegin(), sf.getEnd()); - alignPanel.getSeqPanel().seqCanvas.highlightSearchResults(highlight); + alignPanel.getSeqPanel().seqCanvas.highlightSearchResults( + highlight, false); } FeatureColourI col = getFeatureStyle(name.getText()); @@ -386,7 +387,10 @@ public class FeatureRenderer extends FeaturesFile ffile = new FeaturesFile(); - String enteredType = name.getText().trim(); + final String enteredType = name.getText().trim(); + final String enteredGroup = group.getText().trim(); + final String enteredDescription = description.getText().replaceAll("\n", " "); + if (reply == JvOptionPane.OK_OPTION && enteredType.length() > 0) { /* @@ -395,7 +399,7 @@ public class FeatureRenderer extends if (useLastDefaults) { lastFeatureAdded = enteredType; - lastFeatureGroupAdded = group.getText().trim(); + lastFeatureGroupAdded = enteredGroup; // TODO: determine if the null feature group is valid if (lastFeatureGroupAdded.length() < 1) { @@ -421,26 +425,37 @@ public class FeatureRenderer extends { /* * YES_OPTION corresponds to the Amend button - * need to refresh Feature Settings if type, group or colour changed + * need to refresh Feature Settings if type, group or colour changed; + * note we don't force the feature to be visible - the user has been + * warned if a hidden feature type or group was entered */ - sf.type = enteredType; - sf.featureGroup = group.getText().trim(); - sf.description = description.getText().replaceAll("\n", " "); - boolean refreshSettings = (!featureType.equals(sf.type) || !featureGroup - .equals(sf.featureGroup)); + boolean refreshSettings = (!featureType.equals(enteredType) || !featureGroup + .equals(enteredGroup)); refreshSettings |= (fcol != oldcol); - - setColour(sf.type, fcol); - + setColour(enteredType, fcol); + int newBegin = sf.begin; + int newEnd = sf.end; try { - sf.begin = ((Integer) start.getValue()).intValue(); - sf.end = ((Integer) end.getValue()).intValue(); + newBegin = ((Integer) start.getValue()).intValue(); + newEnd = ((Integer) end.getValue()).intValue(); } catch (NumberFormatException ex) { + // JSpinner doesn't accept invalid format data :-) } - ffile.parseDescriptionHTML(sf, false); + /* + * replace the feature by deleting it and adding a new one + * (to ensure integrity of SequenceFeatures data store) + */ + sequences.get(0).deleteFeature(sf); + SequenceFeature newSf = new SequenceFeature(sf, enteredType, + newBegin, newEnd, enteredGroup, sf.getScore()); + sf.setDescription(enteredDescription); + ffile.parseDescriptionHTML(newSf, false); + // amend features dialog only updates one sequence at a time + sequences.get(0).addSequenceFeature(newSf); + if (refreshSettings) { featuresAdded(); @@ -455,12 +470,11 @@ public class FeatureRenderer extends for (int i = 0; i < sequences.size(); i++) { SequenceFeature sf = features.get(i); - sf.type = enteredType; - // fix for JAL-1538 - always set feature group here - sf.featureGroup = group.getText().trim(); - sf.description = description.getText().replaceAll("\n", " "); - sequences.get(i).addSequenceFeature(sf); - ffile.parseDescriptionHTML(sf, false); + SequenceFeature sf2 = new SequenceFeature(enteredType, + enteredDescription, sf.getBegin(), sf.getEnd(), + enteredGroup); + ffile.parseDescriptionHTML(sf2, false); + sequences.get(i).addSequenceFeature(sf2); } setColour(enteredType, fcol); diff --git a/src/jalview/gui/FeatureSettings.java b/src/jalview/gui/FeatureSettings.java index 34f0b4a..45b6b0d 100644 --- a/src/jalview/gui/FeatureSettings.java +++ b/src/jalview/gui/FeatureSettings.java @@ -23,7 +23,7 @@ package jalview.gui; import jalview.api.FeatureColourI; import jalview.api.FeatureSettingsControllerI; import jalview.bin.Cache; -import jalview.datamodel.SequenceFeature; +import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceI; import jalview.gui.Help.HelpId; import jalview.io.JalviewFileChooser; @@ -475,50 +475,26 @@ public class FeatureSettings extends JPanel implements private boolean handlingUpdate = false; /** - * contains a float[3] for each feature type string. created by setTableData + * holds {featureCount, totalExtent} for each feature type */ Map typeWidth = null; @Override synchronized public void discoverAllFeatureData() { - Vector allFeatures = new Vector(); - Vector allGroups = new Vector(); - SequenceFeature[] tmpfeatures; - String group; - for (int i = 0; i < af.getViewport().getAlignment().getHeight(); i++) - { - tmpfeatures = af.getViewport().getAlignment().getSequenceAt(i) - .getSequenceFeatures(); - if (tmpfeatures == null) - { - continue; - } + Set allGroups = new HashSet(); + AlignmentI alignment = af.getViewport().getAlignment(); - int index = 0; - while (index < tmpfeatures.length) + for (int i = 0; i < alignment.getHeight(); i++) + { + SequenceI seq = alignment.getSequenceAt(i); + for (String group : seq.getFeatures().getFeatureGroups(true)) { - if (tmpfeatures[index].begin == 0 && tmpfeatures[index].end == 0) + if (group != null && !allGroups.contains(group)) { - index++; - continue; + allGroups.add(group); + checkGroupState(group); } - - if (tmpfeatures[index].getFeatureGroup() != null) - { - group = tmpfeatures[index].featureGroup; - if (!allGroups.contains(group)) - { - allGroups.addElement(group); - checkGroupState(group); - } - } - - if (!allFeatures.contains(tmpfeatures[index].getType())) - { - allFeatures.addElement(tmpfeatures[index].getType()); - } - index++; } } @@ -572,7 +548,7 @@ public class FeatureSettings extends JPanel implements synchronized void resetTable(String[] groupChanged) { - if (resettingTable == true) + if (resettingTable) { return; } @@ -580,71 +556,59 @@ public class FeatureSettings extends JPanel implements typeWidth = new Hashtable(); // TODO: change avWidth calculation to 'per-sequence' average and use long // rather than float - float[] avWidth = null; - SequenceFeature[] tmpfeatures; - String group = null, type; - Vector visibleChecks = new Vector(); + + Set displayableTypes = new HashSet(); Set foundGroups = new HashSet(); - // Find out which features should be visible depending on which groups - // are selected / deselected - // and recompute average width ordering + /* + * determine which feature types may be visible depending on + * which groups are selected, and recompute average width data + */ for (int i = 0; i < af.getViewport().getAlignment().getHeight(); i++) { - tmpfeatures = af.getViewport().getAlignment().getSequenceAt(i) - .getSequenceFeatures(); - if (tmpfeatures == null) - { - continue; - } + SequenceI seq = af.getViewport().getAlignment().getSequenceAt(i); - int index = 0; - while (index < tmpfeatures.length) + /* + * get the sequence's groups for positional features + * and keep track of which groups are visible + */ + Set groups = seq.getFeatures().getFeatureGroups(true); + Set visibleGroups = new HashSet(); + for (String group : groups) { - group = tmpfeatures[index].featureGroup; - foundGroups.add(group); - - if (tmpfeatures[index].begin == 0 && tmpfeatures[index].end == 0) - { - index++; - continue; - } - if (group == null || checkGroupState(group)) { - type = tmpfeatures[index].getType(); - if (!visibleChecks.contains(type)) - { - visibleChecks.addElement(type); - } - } - if (!typeWidth.containsKey(tmpfeatures[index].getType())) - { - typeWidth.put(tmpfeatures[index].getType(), - avWidth = new float[3]); + visibleGroups.add(group); } - else - { - avWidth = typeWidth.get(tmpfeatures[index].getType()); - } - avWidth[0]++; - if (tmpfeatures[index].getBegin() > tmpfeatures[index].getEnd()) - { - avWidth[1] += 1 + tmpfeatures[index].getBegin() - - tmpfeatures[index].getEnd(); - } - else + } + foundGroups.addAll(groups); + + /* + * get distinct feature types for visible groups + * record distinct visible types, and their count and total length + */ + Set types = seq.getFeatures().getFeatureTypesForGroups(true, + visibleGroups.toArray(new String[visibleGroups.size()])); + for (String type : types) + { + displayableTypes.add(type); + float[] avWidth = typeWidth.get(type); + if (avWidth == null) { - avWidth[1] += 1 + tmpfeatures[index].getEnd() - - tmpfeatures[index].getBegin(); + avWidth = new float[2]; + typeWidth.put(type, avWidth); } - index++; + // todo this could include features with a non-visible group + // - do we greatly care? + // todo should we include non-displayable features here, and only + // update when features are added? + avWidth[0] += seq.getFeatures().getFeatureCount(true, type); + avWidth[1] += seq.getFeatures().getTotalFeatureLength(type); } } - int fSize = visibleChecks.size(); - Object[][] data = new Object[fSize][3]; + Object[][] data = new Object[displayableTypes.size()][3]; int dataIndex = 0; if (fr.hasRenderOrder()) @@ -660,9 +624,9 @@ public class FeatureSettings extends JPanel implements List frl = fr.getRenderOrder(); for (int ro = frl.size() - 1; ro > -1; ro--) { - type = frl.get(ro); + String type = frl.get(ro); - if (!visibleChecks.contains(type)) + if (!displayableTypes.contains(type)) { continue; } @@ -672,16 +636,17 @@ public class FeatureSettings extends JPanel implements data[dataIndex][2] = new Boolean(af.getViewport() .getFeaturesDisplayed().isVisible(type)); dataIndex++; - visibleChecks.removeElement(type); + displayableTypes.remove(type); } } - fSize = visibleChecks.size(); - for (int i = 0; i < fSize; i++) + /* + * process any extra features belonging only to + * a group which was just selected + */ + while (!displayableTypes.isEmpty()) { - // These must be extra features belonging to the group - // which was just selected - type = visibleChecks.elementAt(i).toString(); + String type = displayableTypes.iterator().next(); data[dataIndex][0] = type; data[dataIndex][1] = fr.getFeatureStyle(type); @@ -694,6 +659,7 @@ public class FeatureSettings extends JPanel implements data[dataIndex][2] = new Boolean(true); dataIndex++; + displayableTypes.remove(type); } if (originalData == null) diff --git a/src/jalview/gui/Finder.java b/src/jalview/gui/Finder.java index 457d871..5c917ae 100755 --- a/src/jalview/gui/Finder.java +++ b/src/jalview/gui/Finder.java @@ -223,7 +223,8 @@ public class Finder extends GFinder for (SearchResultMatchI match : searchResults.getResults()) { seqs.add(match.getSequence().getDatasetSequence()); - features.add(new SequenceFeature(searchString, desc, null, match + features.add(new SequenceFeature(searchString, desc, + match .getStart(), match.getEnd(), desc)); } diff --git a/src/jalview/gui/IdPanel.java b/src/jalview/gui/IdPanel.java index 2d24512..1271fa3 100755 --- a/src/jalview/gui/IdPanel.java +++ b/src/jalview/gui/IdPanel.java @@ -325,23 +325,19 @@ public class IdPanel extends JPanel implements MouseListener, { int seq2 = alignPanel.getSeqPanel().findSeq(e); Sequence sq = (Sequence) av.getAlignment().getSequenceAt(seq2); - // build a new links menu based on the current links + any non-positional - // features + + /* + * build a new links menu based on the current links + * and any non-positional features + */ List nlinks = Preferences.sequenceUrlLinks.getLinksForMenu(); - SequenceFeature sfs[] = sq == null ? null : sq.getSequenceFeatures(); - if (sfs != null) + for (SequenceFeature sf : sq.getFeatures().getNonPositionalFeatures()) { - for (SequenceFeature sf : sfs) + if (sf.links != null) { - if (sf.begin == sf.end && sf.begin == 0) + for (String link : sf.links) { - if (sf.links != null && sf.links.size() > 0) - { - for (int l = 0, lSize = sf.links.size(); l < lSize; l++) - { - nlinks.add(sf.links.elementAt(l)); - } - } + nlinks.add(link); } } } diff --git a/src/jalview/gui/Jalview2XML.java b/src/jalview/gui/Jalview2XML.java index d472ef8..5070884 100644 --- a/src/jalview/gui/Jalview2XML.java +++ b/src/jalview/gui/Jalview2XML.java @@ -32,6 +32,7 @@ import jalview.datamodel.AlignmentI; import jalview.datamodel.GraphLine; import jalview.datamodel.PDBEntry; import jalview.datamodel.RnaViewerModel; +import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceGroup; import jalview.datamodel.SequenceI; import jalview.datamodel.StructureViewerModel; @@ -882,48 +883,43 @@ public class Jalview2XML // TODO: omit sequence features from each alignment view's XML dump if we // are storing dataset - if (jds.getSequenceFeatures() != null) + List sfs = jds + .getSequenceFeatures(); + for (SequenceFeature sf : sfs) { - jalview.datamodel.SequenceFeature[] sf = jds.getSequenceFeatures(); - int index = 0; - while (index < sf.length) - { - Features features = new Features(); + Features features = new Features(); - features.setBegin(sf[index].getBegin()); - features.setEnd(sf[index].getEnd()); - features.setDescription(sf[index].getDescription()); - features.setType(sf[index].getType()); - features.setFeatureGroup(sf[index].getFeatureGroup()); - features.setScore(sf[index].getScore()); - if (sf[index].links != null) + features.setBegin(sf.getBegin()); + features.setEnd(sf.getEnd()); + features.setDescription(sf.getDescription()); + features.setType(sf.getType()); + features.setFeatureGroup(sf.getFeatureGroup()); + features.setScore(sf.getScore()); + if (sf.links != null) + { + for (int l = 0; l < sf.links.size(); l++) { - for (int l = 0; l < sf[index].links.size(); l++) - { - OtherData keyValue = new OtherData(); - keyValue.setKey("LINK_" + l); - keyValue.setValue(sf[index].links.elementAt(l).toString()); - features.addOtherData(keyValue); - } + OtherData keyValue = new OtherData(); + keyValue.setKey("LINK_" + l); + keyValue.setValue(sf.links.elementAt(l).toString()); + features.addOtherData(keyValue); } - if (sf[index].otherDetails != null) + } + if (sf.otherDetails != null) + { + String key; + Iterator keys = sf.otherDetails.keySet().iterator(); + while (keys.hasNext()) { - String key; - Iterator keys = sf[index].otherDetails.keySet() - .iterator(); - while (keys.hasNext()) - { - key = keys.next(); - OtherData keyValue = new OtherData(); - keyValue.setKey(key); - keyValue.setValue(sf[index].otherDetails.get(key).toString()); - features.addOtherData(keyValue); - } + key = keys.next(); + OtherData keyValue = new OtherData(); + keyValue.setKey(key); + keyValue.setValue(sf.otherDetails.get(key).toString()); + features.addOtherData(keyValue); } - - jseq.addFeatures(features); - index++; } + + jseq.addFeatures(features); } if (jdatasq.getAllPDBEntries() != null) @@ -2985,12 +2981,11 @@ public class Jalview2XML Features[] features = jseqs[i].getFeatures(); for (int f = 0; f < features.length; f++) { - jalview.datamodel.SequenceFeature sf = new jalview.datamodel.SequenceFeature( - features[f].getType(), features[f].getDescription(), - features[f].getStatus(), features[f].getBegin(), - features[f].getEnd(), features[f].getFeatureGroup()); - - sf.setScore(features[f].getScore()); + SequenceFeature sf = new SequenceFeature(features[f].getType(), + features[f].getDescription(), features[f].getBegin(), + features[f].getEnd(), features[f].getScore(), + features[f].getFeatureGroup()); + sf.setStatus(features[f].getStatus()); for (int od = 0; od < features[f].getOtherDataCount(); od++) { OtherData keyValue = features[f].getOtherData(od); diff --git a/src/jalview/gui/Jalview2XML_V1.java b/src/jalview/gui/Jalview2XML_V1.java index 8d71ccf..614efd2 100755 --- a/src/jalview/gui/Jalview2XML_V1.java +++ b/src/jalview/gui/Jalview2XML_V1.java @@ -36,6 +36,7 @@ import jalview.binding.Tree; import jalview.binding.UserColours; import jalview.binding.Viewport; import jalview.datamodel.PDBEntry; +import jalview.datamodel.SequenceFeature; import jalview.io.FileFormat; import jalview.schemes.ColourSchemeI; import jalview.schemes.ColourSchemeProperty; @@ -224,11 +225,10 @@ public class Jalview2XML_V1 Features[] features = JSEQ[i].getFeatures(); for (int f = 0; f < features.length; f++) { - jalview.datamodel.SequenceFeature sf = new jalview.datamodel.SequenceFeature( - features[f].getType(), features[f].getDescription(), - features[f].getStatus(), features[f].getBegin(), + SequenceFeature sf = new SequenceFeature(features[f].getType(), + features[f].getDescription(), features[f].getBegin(), features[f].getEnd(), null); - + sf.setStatus(features[f].getStatus()); al.getSequenceAt(i).getDatasetSequence().addSequenceFeature(sf); } } diff --git a/src/jalview/gui/PopupMenu.java b/src/jalview/gui/PopupMenu.java index 756b77b..c78021c 100644 --- a/src/jalview/gui/PopupMenu.java +++ b/src/jalview/gui/PopupMenu.java @@ -1940,7 +1940,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener if (start <= end) { seqs.add(sg.getSequenceAt(i).getDatasetSequence()); - features.add(new SequenceFeature(null, null, null, start, end, null)); + features.add(new SequenceFeature(null, null, start, end, null)); } } @@ -1953,7 +1953,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener seqs, features, true, ap)) { ap.alignFrame.setShowSeqFeatures(true); - ap.highlightSearchResults(null); + ap.av.setSearchResults(null); // clear highlighting + ap.repaint(); // draw new/amended features } } } diff --git a/src/jalview/gui/SeqCanvas.java b/src/jalview/gui/SeqCanvas.java index a134afa..0e31246 100755 --- a/src/jalview/gui/SeqCanvas.java +++ b/src/jalview/gui/SeqCanvas.java @@ -52,6 +52,8 @@ import javax.swing.JComponent; */ public class SeqCanvas extends JComponent implements ViewportListenerI { + private static String ZEROS = "0000000000"; + final FeatureRenderer fr; final SequenceRenderer sr; @@ -68,9 +70,9 @@ public class SeqCanvas extends JComponent implements ViewportListenerI boolean fastPaint = false; - int LABEL_WEST; + int labelWidthWest; - int LABEL_EAST; + int labelWidthEast; int cursorX = 0; @@ -206,7 +208,7 @@ public class SeqCanvas extends JComponent implements ViewportListenerI if (value != -1) { - int x = LABEL_WEST - fm.stringWidth(String.valueOf(value)) + int x = labelWidthWest - fm.stringWidth(String.valueOf(value)) - charWidth / 2; g.drawString(value + "", x, (ypos + (i * charHeight)) - (charHeight / 5)); @@ -288,10 +290,10 @@ public class SeqCanvas extends JComponent implements ViewportListenerI updateViewport(); ViewportRanges ranges = av.getRanges(); - int sr = ranges.getStartRes(); - int er = ranges.getEndRes(); - int ss = ranges.getStartSeq(); - int es = ranges.getEndSeq(); + int startRes = ranges.getStartRes(); + int endRes = ranges.getEndRes(); + int startSeq = ranges.getStartSeq(); + int endSeq = ranges.getEndSeq(); int transX = 0; int transY = 0; @@ -300,20 +302,20 @@ public class SeqCanvas extends JComponent implements ViewportListenerI if (horizontal > 0) // scrollbar pulled right, image to the left { - transX = (er - sr - horizontal) * charWidth; - sr = er - horizontal; + transX = (endRes - startRes - horizontal) * charWidth; + startRes = endRes - horizontal; } else if (horizontal < 0) { - er = sr - horizontal; + endRes = startRes - horizontal; } else if (vertical > 0) // scroll down { - ss = es - vertical; + startSeq = endSeq - vertical; - if (ss < ranges.getStartSeq()) + if (startSeq < ranges.getStartSeq()) { // ie scrolling too fast, more than a page at a time - ss = ranges.getStartSeq(); + startSeq = ranges.getStartSeq(); } else { @@ -322,32 +324,22 @@ public class SeqCanvas extends JComponent implements ViewportListenerI } else if (vertical < 0) { - es = ss - vertical; + endSeq = startSeq - vertical; - if (es > ranges.getEndSeq()) + if (endSeq > ranges.getEndSeq()) { - es = ranges.getEndSeq(); + endSeq = ranges.getEndSeq(); } } gg.translate(transX, transY); - drawPanel(gg, sr, er, ss, es, 0); + drawPanel(gg, startRes, endRes, startSeq, endSeq, 0); gg.translate(-transX, -transY); repaint(); fastpainting = false; } - /** - * Definitions of startx and endx (hopefully): SMJS This is what I'm working - * towards! startx is the first residue (starting at 0) to display. endx is - * the last residue to display (starting at 0). starty is the first sequence - * to display (starting at 0). endy is the last sequence to display (starting - * at 0). NOTE 1: The av limits are set in setFont in this class and in the - * adjustment listener in SeqPanel when the scrollbars move. - */ - - // Set this to false to force a full panel paint @Override public void paintComponent(Graphics g) { @@ -422,57 +414,63 @@ public class SeqCanvas extends JComponent implements ViewportListenerI } /** - * DOCUMENT ME! + * Returns the visible width of the canvas in residues, after allowing for + * East or West scales (if shown) * - * @param cwidth - * DOCUMENT ME! + * @param canvasWidth + * the width in pixels (possibly including scales) * - * @return DOCUMENT ME! + * @return */ - public int getWrappedCanvasWidth(int cwidth) + public int getWrappedCanvasWidth(int canvasWidth) { FontMetrics fm = getFontMetrics(av.getFont()); - LABEL_EAST = 0; - LABEL_WEST = 0; + labelWidthEast = 0; + labelWidthWest = 0; if (av.getScaleRightWrapped()) { - LABEL_EAST = fm.stringWidth(getMask()); + labelWidthEast = getLabelWidth(fm); } if (av.getScaleLeftWrapped()) { - LABEL_WEST = fm.stringWidth(getMask()); + labelWidthWest = labelWidthEast > 0 ? labelWidthEast + : getLabelWidth(fm); } - return (cwidth - LABEL_EAST - LABEL_WEST) / charWidth; + return (canvasWidth - labelWidthEast - labelWidthWest) / charWidth; } /** - * Generates a string of zeroes. + * Returns a pixel width suitable for showing the largest sequence coordinate + * (end position) in the alignment. Returns 2 plus the number of decimal + * digits to be shown (3 for 1-10, 4 for 11-99 etc). * - * @return String + * @param fm + * @return */ - String getMask() + protected int getLabelWidth(FontMetrics fm) { - String mask = "00"; + /* + * find the biggest sequence end position we need to show + * (note this is not necessarily the sequence length) + */ int maxWidth = 0; - int tmp; - for (int i = 0; i < av.getAlignment().getHeight(); i++) + AlignmentI alignment = av.getAlignment(); + for (int i = 0; i < alignment.getHeight(); i++) { - tmp = av.getAlignment().getSequenceAt(i).getEnd(); - if (tmp > maxWidth) - { - maxWidth = tmp; - } + maxWidth = Math.max(maxWidth, alignment.getSequenceAt(i).getEnd()); } + int length = 2; for (int i = maxWidth; i > 0; i /= 10) { - mask += "0"; + length++; } - return mask; + + return fm.stringWidth(ZEROS.substring(0, length)); } /** @@ -493,20 +491,15 @@ public class SeqCanvas extends JComponent implements ViewportListenerI updateViewport(); AlignmentI al = av.getAlignment(); - FontMetrics fm = getFontMetrics(av.getFont()); - - LABEL_EAST = 0; - LABEL_WEST = 0; - - if (av.getScaleRightWrapped()) + int labelWidth = 0; + if (av.getScaleRightWrapped() || av.getScaleLeftWrapped()) { - LABEL_EAST = fm.stringWidth(getMask()); + FontMetrics fm = getFontMetrics(av.getFont()); + labelWidth = getLabelWidth(fm); } - if (av.getScaleLeftWrapped()) - { - LABEL_WEST = fm.stringWidth(getMask()); - } + labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0; + labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0; int hgap = charHeight; if (av.getScaleAboveWrapped()) @@ -514,7 +507,7 @@ public class SeqCanvas extends JComponent implements ViewportListenerI hgap += charHeight; } - int cWidth = (canvasWidth - LABEL_EAST - LABEL_WEST) / charWidth; + int cWidth = (canvasWidth - labelWidthEast - labelWidthWest) / charWidth; int cHeight = av.getAlignment().getHeight() * charHeight; av.setWrappedWidth(cWidth); @@ -531,6 +524,8 @@ public class SeqCanvas extends JComponent implements ViewportListenerI .findColumnPosition(maxwidth); } + int annotationHeight = getAnnotationHeight(); + while ((ypos <= canvasHeight) && (startRes < maxwidth)) { endx = startRes + cWidth - 1; @@ -550,12 +545,12 @@ public class SeqCanvas extends JComponent implements ViewportListenerI if (av.getScaleRightWrapped()) { - g.translate(canvasWidth - LABEL_EAST, 0); + g.translate(canvasWidth - labelWidthEast, 0); drawEastScale(g, startRes, endx, ypos); - g.translate(-(canvasWidth - LABEL_EAST), 0); + g.translate(-(canvasWidth - labelWidthEast), 0); } - g.translate(LABEL_WEST, 0); + g.translate(labelWidthWest, 0); if (av.getScaleAboveWrapped()) { @@ -616,9 +611,9 @@ public class SeqCanvas extends JComponent implements ViewportListenerI g.translate(0, -cHeight - ypos - 3); } g.setClip(clip); - g.translate(-LABEL_WEST, 0); + g.translate(-labelWidthWest, 0); - ypos += cHeight + getAnnotationHeight() + hgap; + ypos += cHeight + annotationHeight + hgap; startRes += cWidth; } @@ -642,32 +637,35 @@ public class SeqCanvas extends JComponent implements ViewportListenerI } /** - * DOCUMENT ME! + * Draws the visible region of the alignment on the graphics context. If there + * are hidden column markers in the visible region, then each sub-region + * between the markers is drawn separately, followed by the hidden column + * marker. * * @param g1 - * DOCUMENT ME! * @param startRes - * DOCUMENT ME! + * offset of the first column in the visible region (0..) * @param endRes - * DOCUMENT ME! + * offset of the last column in the visible region (0..) * @param startSeq - * DOCUMENT ME! + * offset of the first sequence in the visible region (0..) * @param endSeq - * DOCUMENT ME! - * @param offset - * DOCUMENT ME! + * offset of the last sequence in the visible region (0..) + * @param yOffset + * vertical offset at which to draw (for wrapped alignments) */ - public void drawPanel(Graphics g1, int startRes, int endRes, - int startSeq, int endSeq, int offset) + public void drawPanel(Graphics g1, final int startRes, final int endRes, + final int startSeq, final int endSeq, final int yOffset) { updateViewport(); if (!av.hasHiddenColumns()) { - draw(g1, startRes, endRes, startSeq, endSeq, offset); + draw(g1, startRes, endRes, startSeq, endSeq, yOffset); } else { int screenY = 0; + final int screenYMax = endRes - startRes; int blockStart = startRes; int blockEnd = endRes; @@ -683,38 +681,47 @@ public class SeqCanvas extends JComponent implements ViewportListenerI continue; } - blockEnd = hideStart - 1; + /* + * draw up to just before the next hidden region, or the end of + * the visible region, whichever comes first + */ + blockEnd = Math.min(hideStart - 1, blockStart + screenYMax + - screenY); g1.translate(screenY * charWidth, 0); - draw(g1, blockStart, blockEnd, startSeq, endSeq, offset); + draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset); - if (av.getShowHiddenMarkers()) + /* + * draw the downline of the hidden column marker (ScalePanel draws the + * triangle on top) if we reached it + */ + if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1) { g1.setColor(Color.blue); g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1, - 0 + offset, (blockEnd - blockStart + 1) * charWidth - 1, - (endSeq - startSeq + 1) * charHeight + offset); + 0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1, + (endSeq - startSeq + 1) * charHeight + yOffset); } g1.translate(-screenY * charWidth, 0); screenY += blockEnd - blockStart + 1; blockStart = hideEnd + 1; - if (screenY > (endRes - startRes)) + if (screenY > screenYMax) { // already rendered last block return; } } - if (screenY <= (endRes - startRes)) + if (screenY <= screenYMax) { // remaining visible region to render - blockEnd = blockStart + (endRes - startRes) - screenY; + blockEnd = blockStart + screenYMax - screenY; g1.translate(screenY * charWidth, 0); - draw(g1, blockStart, blockEnd, startSeq, endSeq, offset); + draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset); g1.translate(-screenY * charWidth, 0); } @@ -722,8 +729,21 @@ public class SeqCanvas extends JComponent implements ViewportListenerI } - // int startRes, int endRes, int startSeq, int endSeq, int x, int y, - // int x1, int x2, int y1, int y2, int startx, int starty, + /** + * Draws a region of the visible alignment + * + * @param g1 + * @param startRes + * offset of the first column in the visible region (0..) + * @param endRes + * offset of the last column in the visible region (0..) + * @param startSeq + * offset of the first sequence in the visible region (0..) + * @param endSeq + * offset of the last sequence in the visible region (0..) + * @param yOffset + * vertical offset at which to draw (for wrapped alignments) + */ private void draw(Graphics g, int startRes, int endRes, int startSeq, int endSeq, int offset) { @@ -752,11 +772,13 @@ public class SeqCanvas extends JComponent implements ViewportListenerI + ((i - startSeq) * charHeight), false); } - // / Highlight search Results once all sequences have been drawn - // //////////////////////////////////////////////////////// + /* + * highlight search Results once sequence has been drawn + */ if (av.hasSearchResults()) { - int[] visibleResults = av.getSearchResults().getResults(nextSeq, + SearchResultsI searchResults = av.getSearchResults(); + int[] visibleResults = searchResults.getResults(nextSeq, startRes, endRes); if (visibleResults != null) { @@ -974,18 +996,169 @@ public class SeqCanvas extends JComponent implements ViewportListenerI } /** - * DOCUMENT ME! + * Highlights search results in the visible region by rendering as white text + * on a black background. Any previous highlighting is removed. Answers true + * if any highlight was left on the visible alignment (so status bar should be + * set to match), else false. + *

+ * Currently fastPaint is not implemented for wrapped alignments. If a wrapped + * alignment had to be scrolled to show the highlighted region, then it should + * be fully redrawn, otherwise a fast paint can be performed. This argument + * could be removed if fast paint of scrolled wrapped alignment is coded in + * future (JAL-2609). * * @param results - * DOCUMENT ME! + * @param noFastPaint + * @return */ - public void highlightSearchResults(SearchResultsI results) + public boolean highlightSearchResults(SearchResultsI results, + boolean noFastPaint) { - img = null; + if (fastpainting) + { + return false; + } + boolean wrapped = av.getWrapAlignment(); - av.setSearchResults(results); + try + { + fastPaint = !noFastPaint; + fastpainting = fastPaint; + + updateViewport(); + + /* + * to avoid redrawing the whole visible region, we instead + * redraw just the minimal regions to remove previous highlights + * and add new ones + */ + SearchResultsI previous = av.getSearchResults(); + av.setSearchResults(results); + boolean redrawn = false; + boolean drawn = false; + if (wrapped) + { + redrawn = drawMappedPositionsWrapped(previous); + drawn = drawMappedPositionsWrapped(results); + redrawn |= drawn; + } + else + { + redrawn = drawMappedPositions(previous); + drawn = drawMappedPositions(results); + redrawn |= drawn; + } - repaint(); + /* + * if highlights were either removed or added, repaint + */ + if (redrawn) + { + repaint(); + } + + /* + * return true only if highlights were added + */ + return drawn; + + } finally + { + fastpainting = false; + } + } + + /** + * Redraws the minimal rectangle in the visible region (if any) that includes + * mapped positions of the given search results. Whether or not positions are + * highlighted depends on the SearchResults set on the Viewport. This allows + * this method to be called to either clear or set highlighting. Answers true + * if any positions were drawn (in which case a repaint is still required), + * else false. + * + * @param results + * @return + */ + protected boolean drawMappedPositions(SearchResultsI results) + { + if (results == null) + { + return false; + } + + /* + * calculate the minimal rectangle to redraw that + * includes both new and existing search results + */ + int firstSeq = Integer.MAX_VALUE; + int lastSeq = -1; + int firstCol = Integer.MAX_VALUE; + int lastCol = -1; + boolean matchFound = false; + + ViewportRanges ranges = av.getRanges(); + int firstVisibleColumn = ranges.getStartRes(); + int lastVisibleColumn = ranges.getEndRes(); + AlignmentI alignment = av.getAlignment(); + if (av.hasHiddenColumns()) + { + firstVisibleColumn = alignment.getHiddenColumns() + .adjustForHiddenColumns(firstVisibleColumn); + lastVisibleColumn = alignment.getHiddenColumns() + .adjustForHiddenColumns(lastVisibleColumn); + } + + for (int seqNo = ranges.getStartSeq(); seqNo <= ranges + .getEndSeq(); seqNo++) + { + SequenceI seq = alignment.getSequenceAt(seqNo); + + int[] visibleResults = results.getResults(seq, firstVisibleColumn, + lastVisibleColumn); + if (visibleResults != null) + { + for (int i = 0; i < visibleResults.length - 1; i += 2) + { + int firstMatchedColumn = visibleResults[i]; + int lastMatchedColumn = visibleResults[i + 1]; + if (firstMatchedColumn <= lastVisibleColumn + && lastMatchedColumn >= firstVisibleColumn) + { + /* + * found a search results match in the visible region - + * remember the first and last sequence matched, and the first + * and last visible columns in the matched positions + */ + matchFound = true; + firstSeq = Math.min(firstSeq, seqNo); + lastSeq = Math.max(lastSeq, seqNo); + firstMatchedColumn = Math.max(firstMatchedColumn, + firstVisibleColumn); + lastMatchedColumn = Math.min(lastMatchedColumn, + lastVisibleColumn); + firstCol = Math.min(firstCol, firstMatchedColumn); + lastCol = Math.max(lastCol, lastMatchedColumn); + } + } + } + } + + if (matchFound) + { + if (av.hasHiddenColumns()) + { + firstCol = alignment.getHiddenColumns() + .findColumnPosition(firstCol); + lastCol = alignment.getHiddenColumns().findColumnPosition(lastCol); + } + int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth(); + int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight(); + gg.translate(transX, transY); + drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0); + gg.translate(-transX, -transY); + } + + return matchFound; } @Override @@ -1037,4 +1210,143 @@ public class SeqCanvas extends JComponent implements ViewportListenerI } } } + + /** + * Redraws any positions in the search results in the visible region of a + * wrapped alignment. Any highlights are drawn depending on the search results + * set on the Viewport, not the results argument. This allows + * this method to be called either to clear highlights (passing the previous + * search results), or to draw new highlights. + * + * @param results + * @return + */ + protected boolean drawMappedPositionsWrapped(SearchResultsI results) + { + if (results == null) + { + return false; + } + + boolean matchFound = false; + + int wrappedWidth = av.getWrappedWidth(); + int wrappedHeight = getRepeatHeightWrapped(); + + ViewportRanges ranges = av.getRanges(); + int canvasHeight = getHeight(); + int repeats = canvasHeight / wrappedHeight; + if (canvasHeight / wrappedHeight > 0) + { + repeats++; + } + + int firstVisibleColumn = ranges.getStartRes(); + int lastVisibleColumn = ranges.getStartRes() + repeats + * ranges.getViewportWidth() - 1; + + AlignmentI alignment = av.getAlignment(); + if (av.hasHiddenColumns()) + { + firstVisibleColumn = alignment.getHiddenColumns() + .adjustForHiddenColumns(firstVisibleColumn); + lastVisibleColumn = alignment.getHiddenColumns() + .adjustForHiddenColumns(lastVisibleColumn); + } + + int gapHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1); + + for (int seqNo = ranges.getStartSeq(); seqNo <= ranges + .getEndSeq(); seqNo++) + { + SequenceI seq = alignment.getSequenceAt(seqNo); + + int[] visibleResults = results.getResults(seq, firstVisibleColumn, + lastVisibleColumn); + if (visibleResults != null) + { + for (int i = 0; i < visibleResults.length - 1; i += 2) + { + int firstMatchedColumn = visibleResults[i]; + int lastMatchedColumn = visibleResults[i + 1]; + if (firstMatchedColumn <= lastVisibleColumn + && lastMatchedColumn >= firstVisibleColumn) + { + /* + * found a search results match in the visible region + */ + firstMatchedColumn = Math.max(firstMatchedColumn, + firstVisibleColumn); + lastMatchedColumn = Math.min(lastMatchedColumn, + lastVisibleColumn); + + /* + * draw each mapped position separately (as contiguous positions may + * wrap across lines) + */ + for (int mappedPos = firstMatchedColumn; mappedPos <= lastMatchedColumn; mappedPos++) + { + int displayColumn = mappedPos; + if (av.hasHiddenColumns()) + { + displayColumn = alignment.getHiddenColumns() + .findColumnPosition(displayColumn); + } + + /* + * transX: offset from left edge of canvas to residue position + */ + int transX = labelWidthWest + + ((displayColumn - ranges.getStartRes()) % wrappedWidth) + * av.getCharWidth(); + + /* + * transY: offset from top edge of canvas to residue position + */ + int transY = gapHeight; + transY += (displayColumn - ranges.getStartRes()) + / wrappedWidth * wrappedHeight; + transY += (seqNo - ranges.getStartSeq()) * av.getCharHeight(); + + /* + * yOffset is from graphics origin to start of visible region + */ + int yOffset = 0;// (displayColumn / wrappedWidth) * wrappedHeight; + if (transY < getHeight()) + { + matchFound = true; + gg.translate(transX, transY); + drawPanel(gg, displayColumn, displayColumn, seqNo, seqNo, + yOffset); + gg.translate(-transX, -transY); + } + } + } + } + } + } + + return matchFound; + } + + /** + * Answers the height in pixels of a repeating section of the wrapped + * alignment, including space above, scale above if shown, sequences, and + * annotation panel if shown + * + * @return + */ + protected int getRepeatHeightWrapped() + { + // gap (and maybe scale) above + int repeatHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1); + + // add sequences + repeatHeight += av.getRanges().getViewportHeight() * charHeight; + + // add annotations panel height if shown + repeatHeight += getAnnotationHeight(); + + return repeatHeight; + } } diff --git a/src/jalview/gui/SeqPanel.java b/src/jalview/gui/SeqPanel.java index 26096e6..d14e908 100644 --- a/src/jalview/gui/SeqPanel.java +++ b/src/jalview/gui/SeqPanel.java @@ -62,7 +62,6 @@ import java.awt.event.MouseWheelListener; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.ListIterator; import javax.swing.JPanel; import javax.swing.SwingUtilities; @@ -85,6 +84,16 @@ public class SeqPanel extends JPanel implements MouseListener, /** DOCUMENT ME!! */ public AlignmentPanel ap; + /* + * last column position for mouseMoved event + */ + private int lastMouseColumn; + + /* + * last sequence offset for mouseMoved event + */ + private int lastMouseSeq; + protected int lastres; protected int startseq; @@ -171,6 +180,9 @@ public class SeqPanel extends JPanel implements MouseListener, ssm.addStructureViewerListener(this); ssm.addSelectionListener(this); } + + lastMouseColumn = -1; + lastMouseSeq = -1; } int startWrapBlock = -1; @@ -204,7 +216,7 @@ public class SeqPanel extends JPanel implements MouseListener, int y = evt.getY(); y -= hgap; - x = Math.max(0, x - seqCanvas.LABEL_WEST); + x = Math.max(0, x - seqCanvas.labelWidthWest); int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth()); if (cwidth < 1) @@ -671,6 +683,8 @@ public class SeqPanel extends JPanel implements MouseListener, } lastSearchResults = results; + boolean wasScrolled = false; + if (av.isFollowHighlight()) { // don't allow highlight of protein/cDNA to also scroll a complementary @@ -678,14 +692,19 @@ public class SeqPanel extends JPanel implements MouseListener, // over residue to change abruptly, causing highlighted residue in panel 2 // to change, causing a scroll in panel 1 etc) ap.setToScrollComplementPanel(false); - if (ap.scrollToPosition(results, false)) + wasScrolled = ap.scrollToPosition(results, false); + if (wasScrolled) { seqCanvas.revalidate(); } ap.setToScrollComplementPanel(true); } - setStatusMessage(results); - seqCanvas.highlightSearchResults(results); + + boolean noFastPaint = wasScrolled && av.getWrapAlignment(); + if (seqCanvas.highlightSearchResults(results, noFastPaint)) + { + setStatusMessage(results); + } } @Override @@ -721,8 +740,18 @@ public class SeqPanel extends JPanel implements MouseListener, int seq = findSeq(evt); if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight()) { + lastMouseSeq = -1; return; } + if (column == lastMouseColumn && seq == lastMouseSeq) + { + /* + * just a pixel move without change of residue + */ + return; + } + lastMouseColumn = column; + lastMouseSeq = seq; SequenceI sequence = av.getAlignment().getSequenceAt(seq); @@ -773,11 +802,7 @@ public class SeqPanel extends JPanel implements MouseListener, if (av.isShowSequenceFeatures()) { List features = ap.getFeatureRenderer() - .findFeaturesAtRes(sequence.getDatasetSequence(), pos); - if (isGapped) - { - removeAdjacentFeatures(features, column + 1, sequence); - } + .findFeaturesAtColumn(sequence, column + 1); seqARep.appendFeatures(tooltipText, pos, features, this.ap.getSeqPanel().seqCanvas.fr.getMinMax()); } @@ -788,45 +813,13 @@ public class SeqPanel extends JPanel implements MouseListener, } else { - if (lastTooltip == null - || !lastTooltip.equals(tooltipText.toString())) - { - String formatedTooltipText = JvSwingUtils.wrapTooltip(true, - tooltipText.toString()); - // String formatedTooltipText = tooltipText.toString(); - setToolTipText(formatedTooltipText); - lastTooltip = tooltipText.toString(); - } - - } - - } - - /** - * Removes from the list of features any that start after, or end before, the - * given column position. This allows us to retain only those features - * adjacent to a gapped position that straddle the position. Contact features - * that 'straddle' the position are also removed, since they are not 'at' the - * position. - * - * @param features - * @param column - * alignment column (1..) - * @param sequence - */ - protected void removeAdjacentFeatures(List features, - final int column, SequenceI sequence) - { - // TODO should this be an AlignViewController method (and reused by applet)? - ListIterator it = features.listIterator(); - while (it.hasNext()) - { - SequenceFeature sf = it.next(); - if (sf.isContactFeature() - || sequence.findIndex(sf.getBegin()) > column - || sequence.findIndex(sf.getEnd()) < column) + String textString = tooltipText.toString(); + if (lastTooltip == null || !lastTooltip.equals(textString)) { - it.remove(); + String formattedTooltipText = JvSwingUtils.wrapTooltip(true, + textString); + setToolTipText(formattedTooltipText); + lastTooltip = textString; } } } @@ -884,19 +877,48 @@ public class SeqPanel extends JPanel implements MouseListener, * aligned sequence object * @param column * alignment column - * @param seq + * @param seqIndex * index of sequence in alignment * @return sequence position of residue at column, or adjacent residue if at a * gap */ - int setStatusMessage(SequenceI sequence, final int column, int seq) + int setStatusMessage(SequenceI sequence, final int column, int seqIndex) + { + char sequenceChar = sequence.getCharAt(column); + int pos = sequence.findPosition(column); + setStatusMessage(sequence, seqIndex, sequenceChar, pos); + + return pos; + } + + /** + * Builds the status message for the current cursor location and writes it to + * the status bar, for example + * + *

+   * Sequence 3 ID: FER1_SOLLC
+   * Sequence 5 ID: FER1_PEA Residue: THR (4)
+   * Sequence 5 ID: FER1_PEA Residue: B (3)
+   * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
+   * 
+ * + * @param sequence + * @param seqIndex + * sequence position in the alignment (1..) + * @param sequenceChar + * the character under the cursor + * @param residuePos + * the sequence residue position (if not over a gap) + */ + protected void setStatusMessage(SequenceI sequence, int seqIndex, + char sequenceChar, int residuePos) { StringBuilder text = new StringBuilder(32); /* * Sequence number (if known), and sequence name. */ - String seqno = seq == -1 ? "" : " " + (seq + 1); + String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1); text.append("Sequence").append(seqno).append(" ID: ") .append(sequence.getName()); @@ -905,13 +927,12 @@ public class SeqPanel extends JPanel implements MouseListener, /* * Try to translate the display character to residue name (null for gap). */ - final String displayChar = String.valueOf(sequence.getCharAt(column)); - boolean isGapped = Comparison.isGap(sequence.getCharAt(column)); - int pos = sequence.findPosition(column); + boolean isGapped = Comparison.isGap(sequenceChar); if (!isGapped) { boolean nucleotide = av.getAlignment().isNucleotide(); + String displayChar = String.valueOf(sequenceChar); if (nucleotide) { residue = ResidueProperties.nucleotideName.get(displayChar); @@ -925,11 +946,9 @@ public class SeqPanel extends JPanel implements MouseListener, text.append(" ").append(nucleotide ? "Nucleotide" : "Residue") .append(": ").append(residue == null ? displayChar : residue); - text.append(" (").append(Integer.toString(pos)).append(")"); + text.append(" (").append(Integer.toString(residuePos)).append(")"); } ap.alignFrame.statusBar.setText(text.toString()); - - return pos; } /** @@ -957,12 +976,9 @@ public class SeqPanel extends JPanel implements MouseListener, if (seq == ds) { - /* - * Convert position in sequence (base 1) to sequence character array - * index (base 0) - */ - int start = m.getStart() - m.getSequence().getStart(); - setStatusMessage(seq, start, sequenceIndex); + int start = m.getStart(); + setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1), + start); return; } } @@ -1569,19 +1585,13 @@ public class SeqPanel extends JPanel implements MouseListener, } int column = findColumn(evt); - boolean isGapped = Comparison.isGap(sequence.getCharAt(column)); /* * find features at the position (if not gapped), or straddling * the position (if at a gap) */ List features = seqCanvas.getFeatureRenderer() - .findFeaturesAtRes(sequence.getDatasetSequence(), - sequence.findPosition(column)); - if (isGapped) - { - removeAdjacentFeatures(features, column, sequence); - } + .findFeaturesAtColumn(sequence, column + 1); if (!features.isEmpty()) { @@ -1591,7 +1601,7 @@ public class SeqPanel extends JPanel implements MouseListener, SearchResultsI highlight = new SearchResults(); highlight.addResult(sequence, features.get(0).getBegin(), features .get(0).getEnd()); - seqCanvas.highlightSearchResults(highlight); + seqCanvas.highlightSearchResults(highlight, false); /* * open the Amend Features dialog; clear highlighting afterwards, @@ -1600,7 +1610,8 @@ public class SeqPanel extends JPanel implements MouseListener, List seqs = Collections.singletonList(sequence); seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false, ap); - seqCanvas.highlightSearchResults(null); + av.setSearchResults(null); // clear highlighting + seqCanvas.repaint(); // draw new/amended features } } } @@ -1758,12 +1769,11 @@ public class SeqPanel extends JPanel implements MouseListener, */ void showPopupMenu(MouseEvent evt) { - final int res = findColumn(evt); + final int column = findColumn(evt); final int seq = findSeq(evt); SequenceI sequence = av.getAlignment().getSequenceAt(seq); List allFeatures = ap.getFeatureRenderer() - .findFeaturesAtRes(sequence.getDatasetSequence(), - sequence.findPosition(res)); + .findFeaturesAtColumn(sequence, column + 1); List links = new ArrayList<>(); for (SequenceFeature sf : allFeatures) { diff --git a/src/jalview/gui/SequenceFetcher.java b/src/jalview/gui/SequenceFetcher.java index bf0ab70..da026b5 100755 --- a/src/jalview/gui/SequenceFetcher.java +++ b/src/jalview/gui/SequenceFetcher.java @@ -1006,15 +1006,11 @@ public class SequenceFetcher extends JPanel implements Runnable { for (SequenceI sq : alsqs) { - if ((sfs = sq.getSequenceFeatures()) != null) + if (sq.getFeatures().hasFeatures()) { - if (sfs.length > 0) - { - af.setShowSeqFeatures(true); - break; - } + af.setShowSeqFeatures(true); + break; } - } } diff --git a/src/jalview/gui/TreePanel.java b/src/jalview/gui/TreePanel.java index 35998eb..32c5702 100755 --- a/src/jalview/gui/TreePanel.java +++ b/src/jalview/gui/TreePanel.java @@ -789,19 +789,17 @@ public class TreePanel extends GTreePanel } if (newname == null) { - SequenceFeature sf[] = sq.getSequenceFeatures(); - for (int i = 0; sf != null && i < sf.length; i++) + List features = sq.getFeatures() + .getPositionalFeatures(labelClass); + for (SequenceFeature feature : features) { - if (sf[i].getType().equals(labelClass)) + if (newname == null) + { + newname = feature.getDescription(); + } + else { - if (newname == null) - { - newname = new String(sf[i].getDescription()); - } - else - { - newname = newname + "; " + sf[i].getDescription(); - } + newname = newname + "; " + feature.getDescription(); } } } diff --git a/src/jalview/io/BLCFile.java b/src/jalview/io/BLCFile.java index 6317e83..1b93892 100755 --- a/src/jalview/io/BLCFile.java +++ b/src/jalview/io/BLCFile.java @@ -246,10 +246,7 @@ public class BLCFile extends AlignFile out.append(newline); - if (s[i].getSequence().length > max) - { - max = s[i].getSequence().length; - } + max = Math.max(max, s[i].getLength()); i++; } diff --git a/src/jalview/io/ClansFile.java b/src/jalview/io/ClansFile.java deleted file mode 100644 index d0b1c72..0000000 --- a/src/jalview/io/ClansFile.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.io; - -/** - * Read or write a CLANS style score matrix file. - */ - -public class ClansFile extends FileParse -{ - -} diff --git a/src/jalview/io/ClustalFile.java b/src/jalview/io/ClustalFile.java index 5d58d42..d618809 100755 --- a/src/jalview/io/ClustalFile.java +++ b/src/jalview/io/ClustalFile.java @@ -210,10 +210,7 @@ public class ClustalFile extends AlignFile { String tmp = printId(s[i], jvsuffix); - if (s[i].getSequence().length > max) - { - max = s[i].getSequence().length; - } + max = Math.max(max, s[i].getLength()); if (tmp.length() > maxid) { @@ -245,14 +242,14 @@ public class ClustalFile extends AlignFile int start = i * len; int end = start + len; - if ((end < s[j].getSequence().length) - && (start < s[j].getSequence().length)) + int length = s[j].getLength(); + if ((end < length) && (start < length)) { out.append(s[j].getSequenceAsString(start, end)); } else { - if (start < s[j].getSequence().length) + if (start < length) { out.append(s[j].getSequenceAsString().substring(start)); } diff --git a/src/jalview/io/FeaturesFile.java b/src/jalview/io/FeaturesFile.java index 48eeee3..e1fccaf 100755 --- a/src/jalview/io/FeaturesFile.java +++ b/src/jalview/io/FeaturesFile.java @@ -44,8 +44,9 @@ import java.awt.Color; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -76,6 +77,19 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI protected static final String GFF_VERSION = "##gff-version"; + private static final Comparator SORT_NULL_LAST = new Comparator() + { + @Override + public int compare(String o1, String o2) + { + if (o1 == null) + { + return o2 == null ? 0 : 1; + } + return (o2 == null ? -1 : o1.compareTo(o2)); + } + }; + private AlignmentI lastmatchedAl = null; private SequenceIdMatcher matcher = null; @@ -282,7 +296,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI */ for (SequenceI newseq : newseqs) { - if (newseq.getSequenceFeatures() != null) + if (newseq.getFeatures().hasFeatures()) { align.addSequence(newseq); } @@ -359,20 +373,23 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI Color colour = ColorUtils.createColourFromName(ft); featureColours.put(ft, new FeatureColour(colour)); } - SequenceFeature sf = new SequenceFeature(ft, desc, "", startPos, - endPos, featureGroup); + SequenceFeature sf = null; if (gffColumns.length > 6) { float score = Float.NaN; try { score = new Float(gffColumns[6]).floatValue(); - // update colourgradient bounds if allowed to } catch (NumberFormatException ex) { - // leave as NaN + sf = new SequenceFeature(ft, desc, startPos, endPos, featureGroup); } - sf.setScore(score); + sf = new SequenceFeature(ft, desc, startPos, endPos, score, + featureGroup); + } + else + { + sf = new SequenceFeature(ft, desc, startPos, endPos, featureGroup); } parseDescriptionHTML(sf, removeHTML); @@ -472,219 +489,191 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI ParseHtmlBodyAndLinks parsed = new ParseHtmlBodyAndLinks( sf.getDescription(), removeHTML, newline); - sf.description = (removeHTML) ? parsed.getNonHtmlContent() - : sf.description; + if (removeHTML) + { + sf.setDescription(parsed.getNonHtmlContent()); + } + for (String link : parsed.getLinks()) { sf.addLink(link); } - } /** - * generate a features file for seqs includes non-pos features by default. - * - * @param sequences - * source of sequence features - * @param visible - * hash of feature types and colours - * @return features file contents - */ - public String printJalviewFormat(SequenceI[] sequences, - Map visible) - { - return printJalviewFormat(sequences, visible, true, true); - } - - /** - * generate a features file for seqs with colours from visible (if any) + * Returns contents of a Jalview format features file, for visible features, + * as filtered by type and group. Features with a null group are displayed if + * their feature type is visible. Non-positional features may optionally be + * included (with no check on type or group). * * @param sequences * source of features * @param visible - * hash of Colours for each feature type - * @param visOnly - * when true only feature types in 'visible' will be output - * @param nonpos - * indicates if non-positional features should be output (regardless - * of group or type) - * @return features file contents + * map of colour for each visible feature type + * @param visibleFeatureGroups + * @param includeNonPositional + * if true, include non-positional features (regardless of group or + * type) + * @return */ public String printJalviewFormat(SequenceI[] sequences, - Map visible, boolean visOnly, - boolean nonpos) + Map visible, + List visibleFeatureGroups, boolean includeNonPositional) { - StringBuilder out = new StringBuilder(256); - boolean featuresGen = false; - if (visOnly && !nonpos && (visible == null || visible.size() < 1)) + if (!includeNonPositional && (visible == null || visible.isEmpty())) { // no point continuing. return "No Features Visible"; } - if (visible != null && visOnly) + /* + * write out feature colours (if we know them) + */ + // TODO: decide if feature links should also be written here ? + StringBuilder out = new StringBuilder(256); + if (visible != null) { - // write feature colours only if we're given them and we are generating - // viewed features - // TODO: decide if feature links should also be written here ? - Iterator en = visible.keySet().iterator(); - while (en.hasNext()) + for (Entry featureColour : visible.entrySet()) { - String featureType = en.next().toString(); - FeatureColourI colour = visible.get(featureType); - out.append(colour.toJalviewFormat(featureType)).append(newline); + FeatureColourI colour = featureColour.getValue(); + out.append(colour.toJalviewFormat(featureColour.getKey())).append( + newline); } } - // Work out which groups are both present and visible - List groups = new ArrayList(); - int groupIndex = 0; - boolean isnonpos = false; + String[] types = visible == null ? new String[0] : visible.keySet() + .toArray(new String[visible.keySet().size()]); + + /* + * sort groups alphabetically, and ensure that features with a + * null or empty group are output after those in named groups + */ + List sortedGroups = new ArrayList(visibleFeatureGroups); + sortedGroups.remove(null); + sortedGroups.remove(""); + Collections.sort(sortedGroups); + sortedGroups.add(null); + sortedGroups.add(""); - SequenceFeature[] features; - for (int i = 0; i < sequences.length; i++) + boolean foundSome = false; + + /* + * first output any non-positional features + */ + if (includeNonPositional) { - features = sequences[i].getSequenceFeatures(); - if (features != null) + for (int i = 0; i < sequences.length; i++) { - for (int j = 0; j < features.length; j++) + String sequenceName = sequences[i].getName(); + for (SequenceFeature feature : sequences[i].getFeatures() + .getNonPositionalFeatures()) { - isnonpos = features[j].begin == 0 && features[j].end == 0; - if ((!nonpos && isnonpos) - || (!isnonpos && visOnly && !visible - .containsKey(features[j].type))) - { - continue; - } - - if (features[j].featureGroup != null - && !groups.contains(features[j].featureGroup)) - { - groups.add(features[j].featureGroup); - } + foundSome = true; + out.append(formatJalviewFeature(sequenceName, feature)); } } } - String group = null; - do + for (String group : sortedGroups) { - if (groups.size() > 0 && groupIndex < groups.size()) + boolean isNamedGroup = (group != null && !"".equals(group)); + if (isNamedGroup) { - group = groups.get(groupIndex); out.append(newline); out.append("STARTGROUP").append(TAB); out.append(group); out.append(newline); } - else - { - group = null; - } + /* + * output positional features within groups + */ for (int i = 0; i < sequences.length; i++) { - features = sequences[i].getSequenceFeatures(); - if (features != null) + String sequenceName = sequences[i].getName(); + List features = new ArrayList(); + if (types.length > 0) { - for (SequenceFeature sequenceFeature : features) - { - isnonpos = sequenceFeature.begin == 0 - && sequenceFeature.end == 0; - if ((!nonpos && isnonpos) - || (!isnonpos && visOnly && !visible - .containsKey(sequenceFeature.type))) - { - // skip if feature is nonpos and we ignore them or if we only - // output visible and it isn't non-pos and it's not visible - continue; - } - - if (group != null - && (sequenceFeature.featureGroup == null || !sequenceFeature.featureGroup - .equals(group))) - { - continue; - } + features.addAll(sequences[i].getFeatures().getFeaturesForGroup( + true, group, types)); + } - if (group == null && sequenceFeature.featureGroup != null) - { - continue; - } - // we have features to output - featuresGen = true; - if (sequenceFeature.description == null - || sequenceFeature.description.equals("")) - { - out.append(sequenceFeature.type).append(TAB); - } - else - { - if (sequenceFeature.links != null - && sequenceFeature.getDescription().indexOf("") == -1) - { - out.append(""); - } - - out.append(sequenceFeature.description); - if (sequenceFeature.links != null) - { - for (int l = 0; l < sequenceFeature.links.size(); l++) - { - String label = sequenceFeature.links.elementAt(l); - String href = label.substring(label.indexOf("|") + 1); - label = label.substring(0, label.indexOf("|")); - - if (sequenceFeature.description.indexOf(href) == -1) - { - out.append(" " + label - + ""); - } - } - - if (sequenceFeature.getDescription().indexOf("") == -1) - { - out.append(""); - } - } - - out.append(TAB); - } - out.append(sequences[i].getName()); - out.append("\t-1\t"); - out.append(sequenceFeature.begin); - out.append(TAB); - out.append(sequenceFeature.end); - out.append(TAB); - out.append(sequenceFeature.type); - if (!Float.isNaN(sequenceFeature.score)) - { - out.append(TAB); - out.append(sequenceFeature.score); - } - out.append(newline); - } + for (SequenceFeature sequenceFeature : features) + { + foundSome = true; + out.append(formatJalviewFeature(sequenceName, sequenceFeature)); } } - if (group != null) + if (isNamedGroup) { out.append("ENDGROUP").append(TAB); out.append(group); out.append(newline); - groupIndex++; } - else + } + + return foundSome ? out.toString() : "No Features Visible"; + } + + /** + * @param out + * @param sequenceName + * @param sequenceFeature + */ + protected String formatJalviewFeature( + String sequenceName, SequenceFeature sequenceFeature) + { + StringBuilder out = new StringBuilder(64); + if (sequenceFeature.description == null + || sequenceFeature.description.equals("")) + { + out.append(sequenceFeature.type).append(TAB); + } + else + { + if (sequenceFeature.links != null + && sequenceFeature.getDescription().indexOf("") == -1) { - break; + out.append(""); } - } while (groupIndex < groups.size() + 1); + out.append(sequenceFeature.description); + if (sequenceFeature.links != null) + { + for (int l = 0; l < sequenceFeature.links.size(); l++) + { + String label = sequenceFeature.links.elementAt(l); + String href = label.substring(label.indexOf("|") + 1); + label = label.substring(0, label.indexOf("|")); + + if (sequenceFeature.description.indexOf(href) == -1) + { + out.append(" " + label + ""); + } + } - if (!featuresGen) + if (sequenceFeature.getDescription().indexOf("") == -1) + { + out.append(""); + } + } + + out.append(TAB); + } + out.append(sequenceName); + out.append("\t-1\t"); + out.append(sequenceFeature.begin); + out.append(TAB); + out.append(sequenceFeature.end); + out.append(TAB); + out.append(sequenceFeature.type); + if (!Float.isNaN(sequenceFeature.score)) { - return "No Features Visible"; + out.append(TAB); + out.append(sequenceFeature.score); } + out.append(newline); return out.toString(); } @@ -742,102 +731,90 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } /** - * Returns features output in GFF2 format, including hidden and non-positional - * features - * - * @param sequences - * the sequences whose features are to be output - * @param visible - * a map whose keys are the type names of visible features - * @return - */ - public String printGffFormat(SequenceI[] sequences, - Map visible) - { - return printGffFormat(sequences, visible, true, true); - } - - /** * Returns features output in GFF2 format * * @param sequences * the sequences whose features are to be output * @param visible * a map whose keys are the type names of visible features - * @param outputVisibleOnly + * @param visibleFeatureGroups * @param includeNonPositionalFeatures * @return */ public String printGffFormat(SequenceI[] sequences, - Map visible, boolean outputVisibleOnly, + Map visible, + List visibleFeatureGroups, boolean includeNonPositionalFeatures) { StringBuilder out = new StringBuilder(256); - int version = gffVersion == 0 ? 2 : gffVersion; - out.append(String.format("%s %d\n", GFF_VERSION, version)); - String source; - boolean isnonpos; + + out.append(String.format("%s %d\n", GFF_VERSION, gffVersion == 0 ? 2 : gffVersion)); + + if (!includeNonPositionalFeatures + && (visible == null || visible.isEmpty())) + { + return out.toString(); + } + + String[] types = visible == null ? new String[0] : visible.keySet() + .toArray( + new String[visible.keySet().size()]); + for (SequenceI seq : sequences) { - SequenceFeature[] features = seq.getSequenceFeatures(); - if (features != null) + List features = new ArrayList(); + if (includeNonPositionalFeatures) { - for (SequenceFeature sf : features) - { - isnonpos = sf.begin == 0 && sf.end == 0; - if (!includeNonPositionalFeatures && isnonpos) - { - /* - * ignore non-positional features if not wanted - */ - continue; - } - // TODO why the test !isnonpos here? - // what about not visible non-positional features? - if (!isnonpos && outputVisibleOnly - && !visible.containsKey(sf.type)) - { - /* - * ignore not visible features if not wanted - */ - continue; - } + features.addAll(seq.getFeatures().getNonPositionalFeatures()); + } + if (visible != null && !visible.isEmpty()) + { + features.addAll(seq.getFeatures().getPositionalFeatures(types)); + } - source = sf.featureGroup; - if (source == null) - { - source = sf.getDescription(); - } + for (SequenceFeature sf : features) + { + String source = sf.featureGroup; + if (!sf.isNonPositional() && source != null + && !visibleFeatureGroups.contains(source)) + { + // group is not visible + continue; + } - out.append(seq.getName()); - out.append(TAB); - out.append(source); - out.append(TAB); - out.append(sf.type); - out.append(TAB); - out.append(sf.begin); - out.append(TAB); - out.append(sf.end); - out.append(TAB); - out.append(sf.score); - out.append(TAB); - - int strand = sf.getStrand(); - out.append(strand == 1 ? "+" : (strand == -1 ? "-" : ".")); - out.append(TAB); - - String phase = sf.getPhase(); - out.append(phase == null ? "." : phase); - - // miscellaneous key-values (GFF column 9) - String attributes = sf.getAttributes(); - if (attributes != null) - { - out.append(TAB).append(attributes); - } + if (source == null) + { + source = sf.getDescription(); + } - out.append(newline); + out.append(seq.getName()); + out.append(TAB); + out.append(source); + out.append(TAB); + out.append(sf.type); + out.append(TAB); + out.append(sf.begin); + out.append(TAB); + out.append(sf.end); + out.append(TAB); + out.append(sf.score); + out.append(TAB); + + int strand = sf.getStrand(); + out.append(strand == 1 ? "+" : (strand == -1 ? "-" : ".")); + out.append(TAB); + + String phase = sf.getPhase(); + out.append(phase == null ? "." : phase); + + // miscellaneous key-values (GFF column 9) + String attributes = sf.getAttributes(); + if (attributes != null) + { + out.append(TAB).append(attributes); } + + out.append(newline); } } @@ -1098,10 +1075,11 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI // rename sequences if GFF handler requested this // TODO a more elegant way e.g. gffHelper.postProcess(newseqs) ? - SequenceFeature[] sfs = seq.getSequenceFeatures(); - if (sfs != null) + List sfs = seq.getFeatures().getPositionalFeatures(); + if (!sfs.isEmpty()) { - String newName = (String) sfs[0].getValue(GffHelperI.RENAME_TOKEN); + String newName = (String) sfs.get(0).getValue( + GffHelperI.RENAME_TOKEN); if (newName != null) { seq.setName(newName); diff --git a/src/jalview/io/IdentifyFile.java b/src/jalview/io/IdentifyFile.java index 035c1fa..be0df21 100755 --- a/src/jalview/io/IdentifyFile.java +++ b/src/jalview/io/IdentifyFile.java @@ -274,6 +274,11 @@ public class IdentifyFile // read as a FASTA (probably) break; } + if (data.indexOf("{\"") > -1) + { + reply = FileFormat.Json; + break; + } int lessThan = data.indexOf("<"); if ((lessThan > -1)) // possible Markup Language data i.e HTML, // RNAML, XML @@ -291,11 +296,6 @@ public class IdentifyFile } } - if (data.indexOf("{\"") > -1) - { - reply = FileFormat.Json; - break; - } if ((data.length() < 1) || (data.indexOf("#") == 0)) { lineswereskipped = true; diff --git a/src/jalview/io/JSONFile.java b/src/jalview/io/JSONFile.java index c293cd4..36fe35a 100644 --- a/src/jalview/io/JSONFile.java +++ b/src/jalview/io/JSONFile.java @@ -51,6 +51,7 @@ import jalview.schemes.ColourSchemeProperty; import jalview.schemes.JalviewColourScheme; import jalview.schemes.ResidueColourScheme; import jalview.util.ColorUtils; +import jalview.util.Format; import jalview.viewmodel.seqfeatures.FeaturesDisplayed; import java.awt.Color; @@ -228,8 +229,7 @@ public class JSONFile extends AlignFile implements ComplexAlignFile if (exportSettings.isExportFeatures()) { - jsonAlignmentPojo - .setSeqFeatures(sequenceFeatureToJsonPojo(sqs, fr)); + jsonAlignmentPojo.setSeqFeatures(sequenceFeatureToJsonPojo(sqs)); } if (exportSettings.isExportGroups() && seqGroups != null @@ -309,8 +309,8 @@ public class JSONFile extends AlignFile implements ComplexAlignFile return hiddenSections; } - public List sequenceFeatureToJsonPojo( - SequenceI[] sqs, FeatureRenderer fr) + protected List sequenceFeatureToJsonPojo( + SequenceI[] sqs) { displayedFeatures = (fr == null) ? null : fr.getFeaturesDisplayed(); List sequenceFeaturesPojo = new ArrayList<>(); @@ -321,41 +321,38 @@ public class JSONFile extends AlignFile implements ComplexAlignFile FeatureColourFinder finder = new FeatureColourFinder(fr); + String[] visibleFeatureTypes = displayedFeatures == null ? null + : displayedFeatures.getVisibleFeatures().toArray( + new String[displayedFeatures.getVisibleFeatureCount()]); + for (SequenceI seq : sqs) { - SequenceI dataSetSequence = seq.getDatasetSequence(); - SequenceFeature[] seqFeatures = (dataSetSequence == null) ? null - : seq.getDatasetSequence().getSequenceFeatures(); - - seqFeatures = (seqFeatures == null) ? seq.getSequenceFeatures() - : seqFeatures; - if (seqFeatures == null) - { - continue; - } - + /* + * get all features currently visible (and any non-positional features) + */ + List seqFeatures = seq.getFeatures().getAllFeatures( + visibleFeatureTypes); for (SequenceFeature sf : seqFeatures) { - if (displayedFeatures != null - && displayedFeatures.isVisible(sf.getType())) - { - SequenceFeaturesPojo jsonFeature = new SequenceFeaturesPojo( - String.valueOf(seq.hashCode())); - - String featureColour = (fr == null) ? null : jalview.util.Format - .getHexString(finder.findFeatureColour(Color.white, seq, - seq.findIndex(sf.getBegin()))); - jsonFeature.setXstart(seq.findIndex(sf.getBegin()) - 1); - jsonFeature.setXend(seq.findIndex(sf.getEnd())); - jsonFeature.setType(sf.getType()); - jsonFeature.setDescription(sf.getDescription()); - jsonFeature.setLinks(sf.links); - jsonFeature.setOtherDetails(sf.otherDetails); - jsonFeature.setScore(sf.getScore()); - jsonFeature.setFillColor(featureColour); - jsonFeature.setFeatureGroup(sf.getFeatureGroup()); - sequenceFeaturesPojo.add(jsonFeature); - } + SequenceFeaturesPojo jsonFeature = new SequenceFeaturesPojo( + String.valueOf(seq.hashCode())); + + String featureColour = (fr == null) ? null : Format + .getHexString(finder.findFeatureColour(Color.white, seq, + seq.findIndex(sf.getBegin()))); + int xStart = sf.getBegin() == 0 ? 0 + : seq.findIndex(sf.getBegin()) - 1; + int xEnd = sf.getEnd() == 0 ? 0 : seq.findIndex(sf.getEnd()); + jsonFeature.setXstart(xStart); + jsonFeature.setXend(xEnd); + jsonFeature.setType(sf.getType()); + jsonFeature.setDescription(sf.getDescription()); + jsonFeature.setLinks(sf.links); + jsonFeature.setOtherDetails(sf.otherDetails); + jsonFeature.setScore(sf.getScore()); + jsonFeature.setFillColor(featureColour); + jsonFeature.setFeatureGroup(sf.getFeatureGroup()); + sequenceFeaturesPojo.add(jsonFeature); } } return sequenceFeaturesPojo; @@ -681,12 +678,23 @@ public class JSONFile extends AlignFile implements ComplexAlignFile Long end = (Long) jsonFeature.get("xEnd"); String type = (String) jsonFeature.get("type"); String featureGrp = (String) jsonFeature.get("featureGroup"); - String descripiton = (String) jsonFeature.get("description"); + String description = (String) jsonFeature.get("description"); String seqRef = (String) jsonFeature.get("sequenceRef"); Float score = Float.valueOf(jsonFeature.get("score").toString()); Sequence seq = seqMap.get(seqRef); - SequenceFeature sequenceFeature = new SequenceFeature(); + + /* + * begin/end of 0 is for a non-positional feature + */ + int featureBegin = begin.intValue() == 0 ? 0 : seq + .findPosition(begin.intValue()); + int featureEnd = end.intValue() == 0 ? 0 : seq.findPosition(end + .intValue()) - 1; + + SequenceFeature sequenceFeature = new SequenceFeature(type, + description, featureBegin, featureEnd, score, featureGrp); + JSONArray linksJsonArray = (JSONArray) jsonFeature.get("links"); if (linksJsonArray != null && linksJsonArray.size() > 0) { @@ -697,12 +705,7 @@ public class JSONFile extends AlignFile implements ComplexAlignFile sequenceFeature.addLink(link); } } - sequenceFeature.setFeatureGroup(featureGrp); - sequenceFeature.setScore(score); - sequenceFeature.setDescription(descripiton); - sequenceFeature.setType(type); - sequenceFeature.setBegin(seq.findPosition(begin.intValue())); - sequenceFeature.setEnd(seq.findPosition(end.intValue()) - 1); + seq.addSequenceFeature(sequenceFeature); displayedFeatures.setVisible(type); } diff --git a/src/jalview/io/JnetAnnotationMaker.java b/src/jalview/io/JnetAnnotationMaker.java index 3feae5d..2a8a00f 100755 --- a/src/jalview/io/JnetAnnotationMaker.java +++ b/src/jalview/io/JnetAnnotationMaker.java @@ -59,7 +59,7 @@ public class JnetAnnotationMaker // in the future we could search for the query // sequence in the alignment before calling this function. SequenceI seqRef = al.getSequenceAt(firstSeq); - int width = preds[0].getSequence().length; + int width = preds[0].getLength(); int[] gapmap = al.getSequenceAt(firstSeq).gapMap(); if ((delMap != null && delMap.length > width) || (delMap == null && gapmap.length != width)) diff --git a/src/jalview/io/MSFfile.java b/src/jalview/io/MSFfile.java index f379724..b05acff 100755 --- a/src/jalview/io/MSFfile.java +++ b/src/jalview/io/MSFfile.java @@ -294,7 +294,7 @@ public class MSFfile extends AlignFile } long maxNB = 0; - out.append(" MSF: " + s[0].getSequence().length + " Type: " + out.append(" MSF: " + s[0].getLength() + " Type: " + (is_NA ? "N" : "P") + " Check: " + (bigChecksum % 10000) + " .."); out.append(newline); @@ -310,9 +310,9 @@ public class MSFfile extends AlignFile nameBlock[i] = new String(" Name: " + printId(s[i], jvSuffix) + " "); - idBlock[i] = new String("Len: " - + maxLenpad.form(s[i].getSequence().length) + " Check: " - + maxChkpad.form(checksums[i]) + " Weight: 1.00" + newline); + idBlock[i] = new String("Len: " + maxLenpad.form(s[i].getLength()) + + " Check: " + maxChkpad.form(checksums[i]) + + " Weight: 1.00" + newline); if (s[i].getName().length() > maxid) { @@ -369,8 +369,9 @@ public class MSFfile extends AlignFile int start = (i * 50) + (k * 10); int end = start + 10; - if ((end < s[j].getSequence().length) - && (start < s[j].getSequence().length)) + int length = s[j].getLength(); + if ((end < length) + && (start < length)) { out.append(s[j].getSequence(start, end)); @@ -385,7 +386,7 @@ public class MSFfile extends AlignFile } else { - if (start < s[j].getSequence().length) + if (start < length) { out.append(s[j].getSequenceAsString().substring(start)); out.append(newline); diff --git a/src/jalview/io/MatrixFile.java b/src/jalview/io/MatrixFile.java deleted file mode 100644 index 418eea2..0000000 --- a/src/jalview/io/MatrixFile.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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.io; - -/** - * IO for asymmetric matrix with arbitrary dimension with labels, as displayed - * by PCA viewer. Form is: tab separated entity defs header line TITLE\ttitle - * DESC\tdesc PROPERTY\t\tname\ttype\tvalue - * ROW\tRow i label (ID)/tPrinciple text/tprinciple description/t... - * COLUMN\t(similar, optional).. .. \t...(column-wise data for row - * i) - */ - -public class MatrixFile extends FileParse -{ - -} diff --git a/src/jalview/io/PfamFile.java b/src/jalview/io/PfamFile.java index bc22fae..9f152cc 100755 --- a/src/jalview/io/PfamFile.java +++ b/src/jalview/io/PfamFile.java @@ -157,10 +157,7 @@ public class PfamFile extends AlignFile { String tmp = printId(s[i], jvsuffix); - if (s[i].getSequence().length > max) - { - max = s[i].getSequence().length; - } + max = Math.max(max, s[i].getLength()); if (tmp.length() > maxid) { diff --git a/src/jalview/io/PhylipFile.java b/src/jalview/io/PhylipFile.java index e8fe7e9..e1d82ee 100644 --- a/src/jalview/io/PhylipFile.java +++ b/src/jalview/io/PhylipFile.java @@ -247,7 +247,7 @@ public class PhylipFile extends AlignFile sb.append(" "); // if there are no sequences, then define the number of characters as 0 sb.append( - (sqs.length > 0) ? Integer.toString(sqs[0].getSequence().length) +(sqs.length > 0) ? Integer.toString(sqs[0].getLength()) : "0") .append(newline); @@ -279,13 +279,13 @@ public class PhylipFile extends AlignFile // sequential has the entire sequence following the name if (sequential) { - sb.append(s.getSequence()); + sb.append(s.getSequenceAsString()); } else { // Jalview ensures all sequences are of same length so no need // to keep track of min/max length - sequenceLength = s.getSequence().length; + sequenceLength = s.getLength(); // interleaved breaks the sequence into chunks for // interleavedColumns characters sb.append(s.getSequence(0, diff --git a/src/jalview/io/PileUpfile.java b/src/jalview/io/PileUpfile.java index 84be72c..4a0885c 100755 --- a/src/jalview/io/PileUpfile.java +++ b/src/jalview/io/PileUpfile.java @@ -92,7 +92,7 @@ public class PileUpfile extends MSFfile i++; } - out.append(" MSF: " + s[0].getSequence().length + out.append(" MSF: " + s[0].getLength() + " Type: P Check: " + bigChecksum % 10000 + " .."); out.append(newline); out.append(newline); @@ -151,8 +151,8 @@ public class PileUpfile extends MSFfile int start = (i * 50) + (k * 10); int end = start + 10; - if ((end < s[j].getSequence().length) - && (start < s[j].getSequence().length)) + int length = s[j].getLength(); + if ((end < length) && (start < length)) { out.append(s[j].getSequence(start, end)); @@ -167,7 +167,7 @@ public class PileUpfile extends MSFfile } else { - if (start < s[j].getSequence().length) + if (start < length) { out.append(s[j].getSequenceAsString().substring(start)); out.append(newline); diff --git a/src/jalview/io/SequenceAnnotationReport.java b/src/jalview/io/SequenceAnnotationReport.java index 6c8f40f..c3b076c 100644 --- a/src/jalview/io/SequenceAnnotationReport.java +++ b/src/jalview/io/SequenceAnnotationReport.java @@ -57,7 +57,8 @@ public class SequenceAnnotationReport final String linkImageURL; /* - * Comparator to order DBRefEntry by Source + accession id (case-insensitive) + * Comparator to order DBRefEntry by Source + accession id (case-insensitive), + * with 'Primary' sources placed before others */ private static Comparator comparator = new Comparator() { @@ -356,100 +357,121 @@ public class SequenceAnnotationReport { ds = ds.getDatasetSequence(); } + + if (showDbRefs) + { + maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary)); + } + + /* + * add non-positional features if wanted + */ + if (showNpFeats) + { + for (SequenceFeature sf : sequence.getFeatures() + .getNonPositionalFeatures()) + { + int sz = -sb.length(); + appendFeature(sb, 0, minmax, sf); + sz += sb.length(); + maxWidth = Math.max(maxWidth, sz); + } + } + sb.append(""); + return maxWidth; + } + + /** + * A helper method that appends any DBRefs, returning the maximum line length + * added + * + * @param sb + * @param ds + * @param summary + * @return + */ + protected int appendDbRefs(final StringBuilder sb, SequenceI ds, + boolean summary) + { DBRefEntry[] dbrefs = ds.getDBRefs(); - if (showDbRefs && dbrefs != null) + if (dbrefs == null) + { + return 0; + } + + // note this sorts the refs held on the sequence! + Arrays.sort(dbrefs, comparator); + boolean ellipsis = false; + String source = null; + String lastSource = null; + int countForSource = 0; + int sourceCount = 0; + boolean moreSources = false; + int maxLineLength = 0; + int lineLength = 0; + + for (DBRefEntry ref : dbrefs) { - // note this sorts the refs held on the sequence! - Arrays.sort(dbrefs, comparator); - boolean ellipsis = false; - String source = null; - String lastSource = null; - int countForSource = 0; - int sourceCount = 0; - boolean moreSources = false; - int lineLength = 0; - - for (DBRefEntry ref : dbrefs) + source = ref.getSource(); + if (source == null) { - source = ref.getSource(); - if (source == null) - { - // shouldn't happen - continue; - } - boolean sourceChanged = !source.equals(lastSource); - if (sourceChanged) - { - lineLength = 0; - countForSource = 0; - sourceCount++; - } - if (sourceCount > MAX_SOURCES && summary) - { - ellipsis = true; - moreSources = true; - break; - } - lastSource = source; - countForSource++; - if (countForSource == 1 || !summary) - { - sb.append("
"); - } - if (countForSource <= MAX_REFS_PER_SOURCE || !summary) - { - String accessionId = ref.getAccessionId(); - lineLength += accessionId.length() + 1; - if (countForSource > 1 && summary) - { - sb.append(", ").append(accessionId); - lineLength++; - } - else - { - sb.append(source).append(" ").append(accessionId); - lineLength += source.length(); - } - maxWidth = Math.max(maxWidth, lineLength); - } - if (countForSource == MAX_REFS_PER_SOURCE && summary) - { - sb.append(COMMA).append(ELLIPSIS); - ellipsis = true; - } + // shouldn't happen + continue; } - if (moreSources) + boolean sourceChanged = !source.equals(lastSource); + if (sourceChanged) { - sb.append("
").append(ELLIPSIS).append(COMMA).append(source) - .append(COMMA).append(ELLIPSIS); + lineLength = 0; + countForSource = 0; + sourceCount++; } - if (ellipsis) + if (sourceCount > MAX_SOURCES && summary) { - sb.append("
("); - sb.append(MessageManager.getString("label.output_seq_details")); - sb.append(")"); + ellipsis = true; + moreSources = true; + break; } - } - - /* - * add non-positional features if wanted - */ - SequenceFeature[] features = sequence.getSequenceFeatures(); - if (showNpFeats && features != null) - { - for (int i = 0; i < features.length; i++) + lastSource = source; + countForSource++; + if (countForSource == 1 || !summary) + { + sb.append("
"); + } + if (countForSource <= MAX_REFS_PER_SOURCE || !summary) { - if (features[i].begin == 0 && features[i].end == 0) + String accessionId = ref.getAccessionId(); + lineLength += accessionId.length() + 1; + if (countForSource > 1 && summary) { - int sz = -sb.length(); - appendFeature(sb, 0, minmax, features[i]); - sz += sb.length(); - maxWidth = Math.max(maxWidth, sz); + sb.append(", ").append(accessionId); + lineLength++; } + else + { + sb.append(source).append(" ").append(accessionId); + lineLength += source.length(); + } + maxLineLength = Math.max(maxLineLength, lineLength); + } + if (countForSource == MAX_REFS_PER_SOURCE && summary) + { + sb.append(COMMA).append(ELLIPSIS); + ellipsis = true; } } - sb.append(""); - return maxWidth; + if (moreSources) + { + sb.append("
").append(source) + .append(COMMA).append(ELLIPSIS); + } + if (ellipsis) + { + sb.append("
("); + sb.append(MessageManager.getString("label.output_seq_details")); + sb.append(")"); + } + + return maxLineLength; } public void createTooltipAnnotationReport(final StringBuilder tip, diff --git a/src/jalview/io/StockholmFile.java b/src/jalview/io/StockholmFile.java index c2f3683..798a77e 100644 --- a/src/jalview/io/StockholmFile.java +++ b/src/jalview/io/StockholmFile.java @@ -74,6 +74,8 @@ import fr.orsay.lri.varna.models.rna.RNA; */ public class StockholmFile extends AlignFile { + private static final String ANNOTATION = "annotation"; + private static final Regex OPEN_PAREN = new Regex("(<|\\[)", "("); private static final Regex CLOSE_PAREN = new Regex("(>|\\])", ")"); @@ -392,7 +394,7 @@ public class StockholmFile extends AlignFile while (j.hasMoreElements()) { String desc = j.nextElement().toString(); - if ("annotations".equals(desc) && annotsAdded) + if (ANNOTATION.equals(desc) && annotsAdded) { // don't add features if we already added an annotation row continue; @@ -412,7 +414,7 @@ public class StockholmFile extends AlignFile int new_pos = posmap[k]; // look up nearest seqeunce // position to this column SequenceFeature feat = new SequenceFeature(type, desc, - new_pos, new_pos, 0f, null); + new_pos, new_pos, null); seqO.addSequenceFeature(feat); } @@ -635,7 +637,7 @@ public class StockholmFile extends AlignFile content = new Hashtable(); features.put(this.id2type(type), content); } - String ns = (String) content.get("annotation"); + String ns = (String) content.get(ANNOTATION); if (ns == null) { @@ -643,7 +645,7 @@ public class StockholmFile extends AlignFile } // finally, append the annotation line ns += seq; - content.put("annotation", ns); + content.put(ANNOTATION, ns); // // end of wrapped annotation block. // // Now a new row is created with the current set of data @@ -928,10 +930,7 @@ public class StockholmFile extends AlignFile while ((in < s.length) && (s[in] != null)) { String tmp = printId(s[in], jvSuffix); - if (s[in].getSequence().length > max) - { - max = s[in].getSequence().length; - } + max = Math.max(max, s[in].getLength()); if (tmp.length() > maxid) { diff --git a/src/jalview/io/StructureFile.java b/src/jalview/io/StructureFile.java index ab220f0..7628115 100644 --- a/src/jalview/io/StructureFile.java +++ b/src/jalview/io/StructureFile.java @@ -392,8 +392,10 @@ public abstract class StructureFile extends AlignFile public static boolean isRNA(SequenceI seq) { - for (char c : seq.getSequence()) + int length = seq.getLength(); + for (int i = 0; i < length; i++) { + char c = seq.getCharAt(i); if ((c != 'A') && (c != 'C') && (c != 'G') && (c != 'U')) { return false; diff --git a/src/jalview/io/gff/ExonerateHelper.java b/src/jalview/io/gff/ExonerateHelper.java index eb74eea..873fd27 100644 --- a/src/jalview/io/gff/ExonerateHelper.java +++ b/src/jalview/io/gff/ExonerateHelper.java @@ -352,12 +352,16 @@ public class ExonerateHelper extends Gff2Helper return false; } + /** + * An override to set feature group to "exonerate" instead of the default GFF + * source value (column 2) + */ @Override protected SequenceFeature buildSequenceFeature(String[] gff, Map> set) { - SequenceFeature sf = super.buildSequenceFeature(gff, set); - sf.setFeatureGroup("exonerate"); + SequenceFeature sf = super.buildSequenceFeature(gff, TYPE_COL, + "exonerate", set); return sf; } diff --git a/src/jalview/io/gff/Gff3Helper.java b/src/jalview/io/gff/Gff3Helper.java index 82e5313..28941f5 100644 --- a/src/jalview/io/gff/Gff3Helper.java +++ b/src/jalview/io/gff/Gff3Helper.java @@ -310,10 +310,9 @@ public class Gff3Helper extends GffHelperBase * give the mapped sequence a copy of the sequence feature, with * start/end range adjusted */ - SequenceFeature sf2 = new SequenceFeature(sf); - sf2.setBegin(1); int sequenceFeatureLength = 1 + sf.getEnd() - sf.getBegin(); - sf2.setEnd(sequenceFeatureLength); + SequenceFeature sf2 = new SequenceFeature(sf, 1, + sequenceFeatureLength, sf.getFeatureGroup(), sf.getScore()); mappedSequence.addSequenceFeature(sf2); /* @@ -362,9 +361,11 @@ public class Gff3Helper extends GffHelperBase */ @Override protected SequenceFeature buildSequenceFeature(String[] gff, + int typeColumn, String group, Map> attributes) { - SequenceFeature sf = super.buildSequenceFeature(gff, attributes); + SequenceFeature sf = super.buildSequenceFeature(gff, typeColumn, group, + attributes); String desc = getDescription(sf, attributes); if (desc != null) { diff --git a/src/jalview/io/gff/GffHelperBase.java b/src/jalview/io/gff/GffHelperBase.java index 48c33e5..41f141b 100644 --- a/src/jalview/io/gff/GffHelperBase.java +++ b/src/jalview/io/gff/GffHelperBase.java @@ -337,6 +337,19 @@ public abstract class GffHelperBase implements GffHelperI protected SequenceFeature buildSequenceFeature(String[] gff, Map> attributes) { + return buildSequenceFeature(gff, TYPE_COL, gff[SOURCE_COL], attributes); + } + + /** + * @param gff + * @param typeColumn + * @param group + * @param attributes + * @return + */ + protected SequenceFeature buildSequenceFeature(String[] gff, + int typeColumn, String group, Map> attributes) + { try { int start = Integer.parseInt(gff[START_COL]); @@ -355,8 +368,8 @@ public abstract class GffHelperBase implements GffHelperI // e.g. '.' - leave as zero } - SequenceFeature sf = new SequenceFeature(gff[TYPE_COL], - gff[SOURCE_COL], start, end, score, gff[SOURCE_COL]); + SequenceFeature sf = new SequenceFeature(gff[typeColumn], + gff[SOURCE_COL], start, end, score, group); sf.setStrand(gff[STRAND_COL]); diff --git a/src/jalview/io/gff/InterProScanHelper.java b/src/jalview/io/gff/InterProScanHelper.java index e1334e1..0aa3b74 100644 --- a/src/jalview/io/gff/InterProScanHelper.java +++ b/src/jalview/io/gff/InterProScanHelper.java @@ -73,13 +73,19 @@ public class InterProScanHelper extends Gff3Helper } /** - * - */ + * An override that + *
    + *
  • uses Source (column 2) as feature type instead of the default column 3
  • + *
  • sets "InterProScan" as the feature group
  • + *
  • extracts "signature_desc" attribute as the feature description
  • + *
+ */ @Override protected SequenceFeature buildSequenceFeature(String[] gff, Map> attributes) { - SequenceFeature sf = super.buildSequenceFeature(gff, attributes); + SequenceFeature sf = super.buildSequenceFeature(gff, SOURCE_COL, + INTER_PRO_SCAN, attributes); /* * signature_desc is a more informative source of description @@ -91,13 +97,6 @@ public class InterProScanHelper extends Gff3Helper sf.setDescription(description); } - /* - * Set sequence feature group as 'InterProScan', and type as the source - * database for this match (e.g. 'Pfam') - */ - sf.setType(gff[SOURCE_COL]); - sf.setFeatureGroup(INTER_PRO_SCAN); - return sf; } diff --git a/src/jalview/io/vamsas/Datasetsequence.java b/src/jalview/io/vamsas/Datasetsequence.java index 9db7a8e..e1340e2 100644 --- a/src/jalview/io/vamsas/Datasetsequence.java +++ b/src/jalview/io/vamsas/Datasetsequence.java @@ -21,9 +21,12 @@ package jalview.io.vamsas; import jalview.datamodel.DBRefEntry; +import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.io.VamsasAppDatastore; +import java.util.List; + import uk.ac.vamsas.objects.core.DataSet; import uk.ac.vamsas.objects.core.DbRef; import uk.ac.vamsas.objects.core.Sequence; @@ -61,6 +64,7 @@ public class Datasetsequence extends DatastoreItem doJvUpdate(); } + @Override public void addFromDocument() { Sequence vseq = (Sequence) vobj; @@ -73,6 +77,7 @@ public class Datasetsequence extends DatastoreItem modified = true; } + @Override public void updateFromDoc() { Sequence sq = (Sequence) vobj; @@ -128,25 +133,21 @@ public class Datasetsequence extends DatastoreItem */ private boolean updateSqFeatures() { - boolean modified = false; + boolean changed = false; SequenceI sq = (SequenceI) jvobj; // add or update any new features/references on dataset sequence - if (sq.getSequenceFeatures() != null) + List sfs = sq.getSequenceFeatures(); + for (SequenceFeature sf : sfs) { - int sfSize = sq.getSequenceFeatures().length; - - for (int sf = 0; sf < sfSize; sf++) - { - modified |= new jalview.io.vamsas.Sequencefeature(datastore, - (jalview.datamodel.SequenceFeature) sq - .getSequenceFeatures()[sf], dataset, - (Sequence) vobj).docWasUpdated(); - } + changed |= new jalview.io.vamsas.Sequencefeature(datastore, sf, + dataset, (Sequence) vobj).docWasUpdated(); } - return modified; + + return changed; } + @Override public void addToDocument() { SequenceI sq = (SequenceI) jvobj; @@ -217,6 +218,7 @@ public class Datasetsequence extends DatastoreItem return modifiedtheseq; } + @Override public void conflict() { log.warn("Conflict in dataset sequence update to document. Overwriting document"); @@ -226,6 +228,7 @@ public class Datasetsequence extends DatastoreItem boolean modified = false; + @Override public void updateToDoc() { SequenceI sq = (SequenceI) jvobj; diff --git a/src/jalview/io/vamsas/Sequencefeature.java b/src/jalview/io/vamsas/Sequencefeature.java index 61491b2..363f6f1 100644 --- a/src/jalview/io/vamsas/Sequencefeature.java +++ b/src/jalview/io/vamsas/Sequencefeature.java @@ -284,9 +284,39 @@ public class Sequencefeature extends Rangetype private SequenceFeature getJalviewSeqFeature(RangeAnnotation dseta) { int[] se = getBounds(dseta); - SequenceFeature sf = new jalview.datamodel.SequenceFeature( - dseta.getType(), dseta.getDescription(), dseta.getStatus(), - se[0], se[1], dseta.getGroup()); + + /* + * try to identify feature score + */ + boolean scoreFound = false; + float theScore = 0f; + String featureType = dseta.getType(); + if (dseta.getScoreCount() > 0) + { + Enumeration scr = dseta.enumerateScore(); + while (scr.hasMoreElements()) + { + Score score = (Score) scr.nextElement(); + if (score.getName().equals(featureType)) + { + theScore = score.getContent(); + scoreFound = true; + } + } + } + + SequenceFeature sf = null; + if (scoreFound) + { + sf = new SequenceFeature(featureType, dseta.getDescription(), se[0], + se[1], theScore, dseta.getGroup()); + } + else + { + sf = new SequenceFeature(featureType, dseta.getDescription(), se[0], + se[1], dseta.getGroup()); + } + sf.setStatus(dseta.getStatus()); if (dseta.getLinkCount() > 0) { Link[] links = dseta.getLink(); @@ -302,11 +332,7 @@ public class Sequencefeature extends Rangetype while (scr.hasMoreElements()) { Score score = (Score) scr.nextElement(); - if (score.getName().equals(sf.getType())) - { - sf.setScore(score.getContent()); - } - else + if (!score.getName().equals(sf.getType())) { sf.setValue(score.getName(), "" + score.getContent()); } diff --git a/src/jalview/renderer/seqfeatures/FeatureColourFinder.java b/src/jalview/renderer/seqfeatures/FeatureColourFinder.java index 1db2004..ba63834 100644 --- a/src/jalview/renderer/seqfeatures/FeatureColourFinder.java +++ b/src/jalview/renderer/seqfeatures/FeatureColourFinder.java @@ -55,7 +55,7 @@ public class FeatureColourFinder * @param defaultColour * @param seq * @param column - * alignment column position (base zero) + * alignment column position (0..) * @return */ public Color findFeatureColour(Color defaultColour, SequenceI seq, @@ -81,7 +81,7 @@ public class FeatureColourFinder } } - Color c = featureRenderer.findFeatureColour(seq, column, g); + Color c = featureRenderer.findFeatureColour(seq, column + 1, g); if (c == null) { return defaultColour; diff --git a/src/jalview/renderer/seqfeatures/FeatureRenderer.java b/src/jalview/renderer/seqfeatures/FeatureRenderer.java index 759101d..8feee1f 100644 --- a/src/jalview/renderer/seqfeatures/FeatureRenderer.java +++ b/src/jalview/renderer/seqfeatures/FeatureRenderer.java @@ -21,6 +21,8 @@ package jalview.renderer.seqfeatures; import jalview.api.AlignViewportI; +import jalview.api.FeatureColourI; +import jalview.datamodel.Range; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.util.Comparison; @@ -31,6 +33,7 @@ import java.awt.Color; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; +import java.util.List; public class FeatureRenderer extends FeatureRendererModel { @@ -214,18 +217,11 @@ public class FeatureRenderer extends FeatureRendererModel return null; } - SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures(); - - if (sequenceFeatures == null || sequenceFeatures.length == 0) - { - return null; - } - if (Comparison.isGap(seq.getCharAt(column))) { /* * returning null allows the colour scheme to provide gap colour - * - normally white, but can be customised otherwise + * - normally white, but can be customised */ return null; } @@ -236,7 +232,7 @@ public class FeatureRenderer extends FeatureRendererModel /* * simple case - just find the topmost rendered visible feature colour */ - renderedColour = findFeatureColour(seq, seq.findPosition(column)); + renderedColour = findFeatureColour(seq, column); } else { @@ -273,8 +269,11 @@ public class FeatureRenderer extends FeatureRendererModel final SequenceI seq, int start, int end, int y1, boolean colourOnly) { - SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures(); - if (sequenceFeatures == null || sequenceFeatures.length == 0) + /* + * if columns are all gapped, or sequence has no features, nothing to do + */ + Range visiblePositions = seq.findPositions(start+1, end+1); + if (visiblePositions == null || !seq.getFeatures().hasFeatures()) { return null; } @@ -288,10 +287,6 @@ public class FeatureRenderer extends FeatureRendererModel transparency)); } - int startPos = seq.findPosition(start); - int endPos = seq.findPosition(end); - - int sfSize = sequenceFeatures.length; Color drawnColour = null; /* @@ -305,54 +300,55 @@ public class FeatureRenderer extends FeatureRendererModel continue; } - // loop through all features in sequence to find - // current feature to render - for (int sfindex = 0; sfindex < sfSize; sfindex++) + FeatureColourI fc = getFeatureStyle(type); + List overlaps = seq.getFeatures().findFeatures( + visiblePositions.getBegin(), visiblePositions.getEnd(), type); + + filterFeaturesForDisplay(overlaps, fc); + + for (SequenceFeature sf : overlaps) { - final SequenceFeature sequenceFeature = sequenceFeatures[sfindex]; - if (!sequenceFeature.type.equals(type)) + Color featureColour = fc.getColor(sf); + if (featureColour == null) { + // score feature outwith threshold for colouring continue; } /* - * a feature type may be flagged as shown but the group - * an instance of it belongs to may be hidden + * if feature starts/ends outside the visible range, + * restrict to visible positions (or if a contact feature, + * to a single position) */ - if (featureGroupNotShown(sequenceFeature)) + int visibleStart = sf.getBegin(); + if (visibleStart < visiblePositions.getBegin()) { - continue; + visibleStart = sf.isContactFeature() ? sf.getEnd() + : visiblePositions.getBegin(); } - - /* - * check feature overlaps the target range - * TODO: efficient retrieval of features overlapping a range - */ - if (sequenceFeature.getBegin() > endPos - || sequenceFeature.getEnd() < startPos) + int visibleEnd = sf.getEnd(); + if (visibleEnd > visiblePositions.getEnd()) { - continue; + visibleEnd = sf.isContactFeature() ? sf.getBegin() + : visiblePositions.getEnd(); } - Color featureColour = getColour(sequenceFeature); - if (featureColour == null) - { - // score feature outwith threshold for colouring - continue; - } + int featureStartCol = seq.findIndex(visibleStart); + int featureEndCol = sf.begin == sf.end ? featureStartCol : seq + .findIndex(visibleEnd); + + // Color featureColour = getColour(sequenceFeature); - boolean isContactFeature = sequenceFeature.isContactFeature(); + boolean isContactFeature = sf.isContactFeature(); if (isContactFeature) { - boolean drawn = renderFeature(g, seq, - seq.findIndex(sequenceFeature.begin) - 1, - seq.findIndex(sequenceFeature.begin) - 1, featureColour, - start, end, y1, colourOnly); - drawn |= renderFeature(g, seq, - seq.findIndex(sequenceFeature.end) - 1, - seq.findIndex(sequenceFeature.end) - 1, featureColour, - start, end, y1, colourOnly); + boolean drawn = renderFeature(g, seq, featureStartCol - 1, + featureStartCol - 1, featureColour, start, end, y1, + colourOnly); + drawn |= renderFeature(g, seq, featureEndCol - 1, + featureEndCol - 1, featureColour, start, end, y1, + colourOnly); if (drawn) { drawnColour = featureColour; @@ -360,6 +356,10 @@ public class FeatureRenderer extends FeatureRendererModel } else { + /* + * showing feature score by height of colour + * is not implemented as a selectable option + * if (av.isShowSequenceFeaturesHeight() && !Float.isNaN(sequenceFeature.score)) { @@ -375,15 +375,16 @@ public class FeatureRenderer extends FeatureRendererModel } else { + */ boolean drawn = renderFeature(g, seq, - seq.findIndex(sequenceFeature.begin) - 1, - seq.findIndex(sequenceFeature.end) - 1, featureColour, + featureStartCol - 1, + featureEndCol - 1, featureColour, start, end, y1, colourOnly); if (drawn) { drawnColour = featureColour; } - } + /*}*/ } } } @@ -401,24 +402,6 @@ public class FeatureRenderer extends FeatureRendererModel } /** - * Answers true if the feature belongs to a feature group which is not - * currently displayed, else false - * - * @param sequenceFeature - * @return - */ - protected boolean featureGroupNotShown( - final SequenceFeature sequenceFeature) - { - return featureGroups != null - && sequenceFeature.featureGroup != null - && sequenceFeature.featureGroup.length() != 0 - && featureGroups.containsKey(sequenceFeature.featureGroup) - && !featureGroups.get(sequenceFeature.featureGroup) - .booleanValue(); - } - - /** * Called when alignment in associated view has new/modified features to * discover and display. * @@ -430,23 +413,23 @@ public class FeatureRenderer extends FeatureRendererModel } /** - * Returns the sequence feature colour rendered at the given sequence - * position, or null if none found. The feature of highest render order (i.e. - * on top) is found, subject to both feature type and feature group being - * visible, and its colour returned. + * Returns the sequence feature colour rendered at the given column position, + * or null if none found. The feature of highest render order (i.e. on top) is + * found, subject to both feature type and feature group being visible, and + * its colour returned. This method is suitable when no feature transparency + * applied (only the topmost visible feature colour is rendered). + *

+ * Note this method does not check for a gap in the column so would return the + * colour for features enclosing a gapped column. Check for gap before calling + * if different behaviour is wanted. * * @param seq - * @param pos + * @param column + * (1..) * @return */ - Color findFeatureColour(SequenceI seq, int pos) + Color findFeatureColour(SequenceI seq, int column) { - SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures(); - if (sequenceFeatures == null || sequenceFeatures.length == 0) - { - return null; - } - /* * check for new feature added while processing */ @@ -464,33 +447,17 @@ public class FeatureRenderer extends FeatureRendererModel continue; } - for (int sfindex = 0; sfindex < sequenceFeatures.length; sfindex++) + List overlaps = seq.findFeatures(column, column, + type); + for (SequenceFeature sequenceFeature : overlaps) { - SequenceFeature sequenceFeature = sequenceFeatures[sfindex]; - if (!sequenceFeature.type.equals(type)) + if (!featureGroupNotShown(sequenceFeature)) { - continue; - } - - if (featureGroupNotShown(sequenceFeature)) - { - continue; - } - - /* - * check the column position is within the feature range - * (or is one of the two contact positions for a contact feature) - */ - boolean featureIsAtPosition = sequenceFeature.begin <= pos - && sequenceFeature.end >= pos; - if (sequenceFeature.isContactFeature()) - { - featureIsAtPosition = sequenceFeature.begin == pos - || sequenceFeature.end == pos; - } - if (featureIsAtPosition) - { - return getColour(sequenceFeature); + Color col = getColour(sequenceFeature); + if (col != null) + { + return col; + } } } } diff --git a/src/jalview/schemes/ClustalxColourScheme.java b/src/jalview/schemes/ClustalxColourScheme.java index f13a90c..9df7ab8 100755 --- a/src/jalview/schemes/ClustalxColourScheme.java +++ b/src/jalview/schemes/ClustalxColourScheme.java @@ -106,19 +106,18 @@ public class ClustalxColourScheme extends ResidueColourScheme for (SequenceI sq : seqs) { - char[] seq = sq.getSequence(); - - int end_j = seq.length - 1; + int end_j = sq.getLength() - 1; + int length = sq.getLength(); for (int i = 0; i <= end_j; i++) { - if ((seq.length - 1) < i) + if (length - 1 < i) { res = 23; } else { - res = ResidueProperties.aaIndex[seq[i]]; + res = ResidueProperties.aaIndex[sq.getCharAt(i)]; } cons2[i][res]++; } diff --git a/src/jalview/schemes/FeatureColour.java b/src/jalview/schemes/FeatureColour.java index dbe4901..b748d9e 100644 --- a/src/jalview/schemes/FeatureColour.java +++ b/src/jalview/schemes/FeatureColour.java @@ -551,16 +551,27 @@ public class FeatureColour implements FeatureColourI return getColour(); } - // todo should we check for above/below threshold here? - if (range == 0.0) - { - return getMaxColour(); - } + /* + * graduated colour case, optionally with threshold + * Float.NaN is assigned minimum visible score colour + */ float scr = feature.getScore(); if (Float.isNaN(scr)) { return getMinColour(); } + if (isAboveThreshold() && scr <= threshold) + { + return null; + } + if (isBelowThreshold() && scr >= threshold) + { + return null; + } + if (range == 0.0) + { + return getMaxColour(); + } float scl = (scr - base) / range; if (isHighToLow) { @@ -602,44 +613,6 @@ public class FeatureColour implements FeatureColourI return (isHighToLow) ? (base + range) : base; } - /** - * Answers true if the feature has a simple colour, or is coloured by label, - * or has a graduated colour and the score of this feature instance is within - * the range to render (if any), i.e. does not lie below or above any - * threshold set. - * - * @param feature - * @return - */ - @Override - public boolean isColored(SequenceFeature feature) - { - if (isColourByLabel() || !isGraduatedColour()) - { - return true; - } - - float val = feature.getScore(); - if (Float.isNaN(val)) - { - return true; - } - if (Float.isNaN(this.threshold)) - { - return true; - } - - if (isAboveThreshold() && val <= threshold) - { - return false; - } - if (isBelowThreshold() && val >= threshold) - { - return false; - } - return true; - } - @Override public boolean isSimpleColour() { diff --git a/src/jalview/util/Comparison.java b/src/jalview/util/Comparison.java index 22e1ab7..94d6300 100644 --- a/src/jalview/util/Comparison.java +++ b/src/jalview/util/Comparison.java @@ -282,35 +282,10 @@ public class Comparison { return false; } - char[][] letters = new char[seqs.length][]; - for (int i = 0; i < seqs.length; i++) - { - if (seqs[i] != null) - { - char[] sequence = seqs[i].getSequence(); - if (sequence != null) - { - letters[i] = sequence; - } - } - } - - return areNucleotide(letters); - } - /** - * Answers true if more than 85% of the sequence residues (ignoring gaps) are - * A, G, C, T or U, else false. This is just a heuristic guess and may give a - * wrong answer (as AGCT are also amino acid codes). - * - * @param letters - * @return - */ - static final boolean areNucleotide(char[][] letters) - { int ntCount = 0; int aaCount = 0; - for (char[] seq : letters) + for (SequenceI seq : seqs) { if (seq == null) { @@ -318,8 +293,10 @@ public class Comparison } // TODO could possibly make an informed guess just from the first sequence // to save a lengthy calculation - for (char c : seq) + int len = seq.getLength(); + for (int i = 0; i < len; i++) { + char c = seq.getCharAt(i); if (isNucleotide(c)) { ntCount++; diff --git a/src/jalview/util/RangeComparator.java b/src/jalview/util/IntRangeComparator.java similarity index 56% rename from src/jalview/util/RangeComparator.java rename to src/jalview/util/IntRangeComparator.java index f911a9b..cb32a0e 100644 --- a/src/jalview/util/RangeComparator.java +++ b/src/jalview/util/IntRangeComparator.java @@ -6,11 +6,17 @@ import java.util.Comparator; * A comparator to order [from, to] ranges into ascending or descending order of * their start position */ -public class RangeComparator implements Comparator +public class IntRangeComparator implements Comparator { + public static final Comparator ASCENDING = new IntRangeComparator( + true); + + public static final Comparator DESCENDING = new IntRangeComparator( + false); + boolean forwards; - public RangeComparator(boolean forward) + IntRangeComparator(boolean forward) { forwards = forward; } diff --git a/src/jalview/viewmodel/ViewportRanges.java b/src/jalview/viewmodel/ViewportRanges.java index 10cf583..49d0f65 100644 --- a/src/jalview/viewmodel/ViewportRanges.java +++ b/src/jalview/viewmodel/ViewportRanges.java @@ -456,18 +456,42 @@ public class ViewportRanges extends ViewportProperties } /** - * Scroll a wrapped alignment so that the specified residue is visible. Fires - * a property change event. + * Scroll a wrapped alignment so that the specified residue is in the first + * repeat of the wrapped view. Fires a property change event. Answers true if + * the startRes changed, else false. * * @param res * residue position to scroll to + * @return */ - public void scrollToWrappedVisible(int res) + public boolean scrollToWrappedVisible(int res) { - // get the start residue of the wrapped row which res is in - // and set that as our start residue + int oldStartRes = startRes; int width = getViewportWidth(); - setStartRes((res / width) * width); + + if (res >= oldStartRes && res < oldStartRes + width) + { + return false; + } + + boolean up = res < oldStartRes; + int widthsToScroll = Math.abs((res - oldStartRes) / width); + if (up) + { + widthsToScroll++; + } + + int residuesToScroll = width * widthsToScroll; + int newStartRes = up ? oldStartRes - residuesToScroll : oldStartRes + + residuesToScroll; + if (newStartRes < 0) + { + newStartRes = 0; + } + + setStartRes(newStartRes); + + return true; } /** diff --git a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java index 40f38b6..1d09dca 100644 --- a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java +++ b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java @@ -26,6 +26,7 @@ import jalview.api.FeaturesDisplayedI; import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.SequenceFeatures; import jalview.renderer.seqfeatures.FeatureRenderer; import jalview.schemes.FeatureColour; import jalview.util.ColorUtils; @@ -116,11 +117,10 @@ public abstract class FeatureRendererModel implements synchronized (fd) { fd.clear(); - java.util.Iterator fdisp = _fr.getFeaturesDisplayed() - .getVisibleFeatures(); - while (fdisp.hasNext()) + for (String type : _fr.getFeaturesDisplayed() + .getVisibleFeatures()) { - fd.setVisible(fdisp.next()); + fd.setVisible(type); } } } @@ -264,50 +264,40 @@ public abstract class FeatureRendererModel implements } @Override - public List findFeaturesAtRes(SequenceI sequence, int res) + public List findFeaturesAtColumn(SequenceI sequence, int column) { - ArrayList tmp = new ArrayList(); - SequenceFeature[] features = sequence.getSequenceFeatures(); - - if (features != null) + /* + * include features at the position provided their feature type is + * displayed, and feature group is null or marked for display + */ + List result = new ArrayList(); + if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null) { - for (int i = 0; i < features.length; i++) - { - if (!av.areFeaturesDisplayed() - || !av.getFeaturesDisplayed().isVisible( - features[i].getType())) - { - continue; - } + return result; + } - if (features[i].featureGroup != null - && featureGroups != null - && featureGroups.containsKey(features[i].featureGroup) - && !featureGroups.get(features[i].featureGroup) - .booleanValue()) - { - continue; - } + Set visibleFeatures = getFeaturesDisplayed() + .getVisibleFeatures(); + String[] visibleTypes = visibleFeatures + .toArray(new String[visibleFeatures.size()]); + List features = sequence.findFeatures(column, column, + visibleTypes); - // check if start/end are at res, and if not a contact feature, that res - // lies between start and end - if ((features[i].getBegin() == res || features[i].getEnd() == res) - || (!features[i].isContactFeature() - && (features[i].getBegin() < res) && (features[i] - .getEnd() >= res))) - { - tmp.add(features[i]); - } + for (SequenceFeature sf : features) + { + if (!featureGroupNotShown(sf)) + { + result.add(sf); } } - return tmp; + return result; } /** * Searches alignment for all features and updates colours * * @param newMadeVisible - * if true newly added feature types will be rendered immediatly + * if true newly added feature types will be rendered immediately * TODO: check to see if this method should actually be proxied so * repaint events can be propagated by the renderer code */ @@ -329,8 +319,7 @@ public abstract class FeatureRendererModel implements } FeaturesDisplayedI featuresDisplayed = av.getFeaturesDisplayed(); - ArrayList allfeatures = new ArrayList(); - ArrayList oldfeatures = new ArrayList(); + Set oldfeatures = new HashSet(); if (renderOrder != null) { for (int i = 0; i < renderOrder.length; i++) @@ -341,107 +330,110 @@ public abstract class FeatureRendererModel implements } } } - if (minmax == null) - { - minmax = new Hashtable(); - } - Set oldGroups = new HashSet(featureGroups.keySet()); AlignmentI alignment = av.getAlignment(); + List allfeatures = new ArrayList(); + for (int i = 0; i < alignment.getHeight(); i++) { SequenceI asq = alignment.getSequenceAt(i); - SequenceFeature[] features = asq.getSequenceFeatures(); - - if (features == null) - { - continue; - } - - int index = 0; - while (index < features.length) + for (String group : asq.getFeatures().getFeatureGroups(true)) { - String fgrp = features[index].getFeatureGroup(); - oldGroups.remove(fgrp); - if (!featuresDisplayed.isRegistered(features[index].getType())) + boolean groupDisplayed = true; + if (group != null) { - if (fgrp != null) + if (featureGroups.containsKey(group)) { - Boolean groupDisplayed = featureGroups.get(fgrp); - if (groupDisplayed == null) - { - groupDisplayed = Boolean.valueOf(newMadeVisible); - featureGroups.put(fgrp, groupDisplayed); - } - if (!groupDisplayed.booleanValue()) - { - index++; - continue; - } + groupDisplayed = featureGroups.get(group); } - if (!(features[index].begin == 0 && features[index].end == 0)) + else { - // If beginning and end are 0, the feature is for the whole sequence - // and we don't want to render the feature in the normal way - - if (newMadeVisible - && !oldfeatures.contains(features[index].getType())) - { - // this is a new feature type on the alignment. Mark it for - // display. - featuresDisplayed.setVisible(features[index].getType()); - setOrder(features[index].getType(), 0); - } + groupDisplayed = newMadeVisible; + featureGroups.put(group, groupDisplayed); } } - if (!allfeatures.contains(features[index].getType())) + if (groupDisplayed) { - allfeatures.add(features[index].getType()); - } - if (!Float.isNaN(features[index].score)) - { - int nonpos = features[index].getBegin() >= 1 ? 0 : 1; - float[][] mm = minmax.get(features[index].getType()); - if (mm == null) - { - mm = new float[][] { null, null }; - minmax.put(features[index].getType(), mm); - } - if (mm[nonpos] == null) - { - mm[nonpos] = new float[] { features[index].score, - features[index].score }; - - } - else + Set types = asq.getFeatures().getFeatureTypesForGroups( + true, group); + for (String type : types) { - if (mm[nonpos][0] > features[index].score) + if (!allfeatures.contains(type)) // or use HashSet and no test? { - mm[nonpos][0] = features[index].score; - } - if (mm[nonpos][1] < features[index].score) - { - mm[nonpos][1] = features[index].score; + allfeatures.add(type); } + updateMinMax(asq, type, true); // todo: for all features? } } - index++; } } - /* - * oldGroups now consists of groups that no longer - * have any feature in them - remove these - */ - for (String grp : oldGroups) + // uncomment to add new features in alphebetical order (but JAL-2575) + // Collections.sort(allfeatures, String.CASE_INSENSITIVE_ORDER); + if (newMadeVisible) { - featureGroups.remove(grp); + for (String type : allfeatures) + { + if (!oldfeatures.contains(type)) + { + featuresDisplayed.setVisible(type); + setOrder(type, 0); + } + } } updateRenderOrder(allfeatures); findingFeatures = false; } + /** + * Updates the global (alignment) min and max values for a feature type from + * the score for a sequence, if the score is not NaN. Values are stored + * separately for positional and non-positional features. + * + * @param seq + * @param featureType + * @param positional + */ + protected void updateMinMax(SequenceI seq, String featureType, + boolean positional) + { + float min = seq.getFeatures().getMinimumScore(featureType, positional); + if (Float.isNaN(min)) + { + return; + } + + float max = seq.getFeatures().getMaximumScore(featureType, positional); + + /* + * stored values are + * { {positionalMin, positionalMax}, {nonPositionalMin, nonPositionalMax} } + */ + if (minmax == null) + { + minmax = new Hashtable(); + } + synchronized (minmax) + { + float[][] mm = minmax.get(featureType); + int index = positional ? 0 : 1; + if (mm == null) + { + mm = new float[][] { null, null }; + minmax.put(featureType, mm); + } + if (mm[index] == null) + { + mm[index] = new float[] { min, max }; + } + else + { + mm[index][0] = Math.min(mm[index][0], min); + mm[index][1] = Math.max(mm[index][1], max); + } + } + } protected Boolean firing = Boolean.FALSE; /** @@ -567,7 +559,8 @@ public abstract class FeatureRendererModel implements * Returns the configured colour for a particular feature instance. This * includes calculation of 'colour by label', or of a graduated score colour, * if applicable. It does not take into account feature visibility or colour - * transparency. + * transparency. Returns null for a score feature whose score value lies + * outside any colour threshold. * * @param feature * @return @@ -575,21 +568,7 @@ public abstract class FeatureRendererModel implements public Color getColour(SequenceFeature feature) { FeatureColourI fc = getFeatureStyle(feature.getType()); - return fc.isColored(feature) ? fc.getColor(feature) : null; - } - - /** - * Answers true unless the feature has a score value which lies outside a - * minimum or maximum threshold configured for colouring. This method does not - * check feature type or group visibility. - * - * @param sequenceFeature - * @return - */ - protected boolean showFeature(SequenceFeature sequenceFeature) - { - FeatureColourI fc = getFeatureStyle(sequenceFeature.type); - return fc.isColored(sequenceFeature); + return fc.getColor(feature); } /** @@ -679,7 +658,8 @@ public abstract class FeatureRendererModel implements } /** - * Sets the priority order for features + * Sets the priority order for features, with the highest priority (displayed + * on top) at the start of the data array * * @param data * { String(Type), Colour(Type), Boolean(Displayed) } @@ -903,11 +883,10 @@ public abstract class FeatureRendererModel implements { return fcols; } - Iterator features = getViewport().getFeaturesDisplayed() + Set features = getViewport().getFeaturesDisplayed() .getVisibleFeatures(); - while (features.hasNext()) + for (String feature : features) { - String feature = features.next(); fcols.put(feature, getFeatureStyle(feature)); } return fcols; @@ -949,25 +928,115 @@ public abstract class FeatureRendererModel implements public List getDisplayedFeatureGroups() { List _gps = new ArrayList(); - boolean valid = false; for (String gp : getFeatureGroups()) { if (checkGroupVisibility(gp, false)) { - valid = true; _gps.add(gp); } - if (!valid) + } + return _gps; + } + + /** + * Answers true if the feature belongs to a feature group which is not + * currently displayed, else false + * + * @param sequenceFeature + * @return + */ + protected boolean featureGroupNotShown(final SequenceFeature sequenceFeature) + { + return featureGroups != null + && sequenceFeature.featureGroup != null + && sequenceFeature.featureGroup.length() != 0 + && featureGroups.containsKey(sequenceFeature.featureGroup) + && !featureGroups.get(sequenceFeature.featureGroup) + .booleanValue(); + } + + /** + * {@inheritDoc} + */ + @Override + public List findFeaturesAtResidue(SequenceI sequence, + int resNo) + { + List result = new ArrayList(); + if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null) + { + return result; + } + + /* + * include features at the position provided their feature type is + * displayed, and feature group is null or the empty string + * or marked for display + */ + Set visibleFeatures = getFeaturesDisplayed() + .getVisibleFeatures(); + String[] visibleTypes = visibleFeatures + .toArray(new String[visibleFeatures.size()]); + List features = sequence.getFeatures().findFeatures( + resNo, resNo, visibleTypes); + + for (SequenceFeature sf : features) + { + if (!featureGroupNotShown(sf)) { - return null; + result.add(sf); } - else + } + return result; + } + + /** + * Removes from the list of features any that have a feature group that is not + * displayed, or duplicate the location of a feature of the same type (unless + * a graduated colour scheme is applied) + * + * @param features + * @param fc + */ + public void filterFeaturesForDisplay(List features, + FeatureColourI fc) + { + if (features.isEmpty()) + { + return; + } + SequenceFeatures.sortFeatures(features, true); + boolean graduated = fc != null && fc.isGraduatedColour(); + SequenceFeature lastFeature = null; + + Iterator it = features.iterator(); + while (it.hasNext()) + { + SequenceFeature sf = it.next(); + if (featureGroupNotShown(sf)) { - // gps = new String[_gps.size()]; - // _gps.toArray(gps); + it.remove(); + continue; } + + /* + * a feature is redundant for rendering purposes if it has the + * same extent as another (so would just redraw the same colour); + * (checking type and isContactFeature as a fail-safe here, although + * currently they are guaranteed to match in this context) + */ + if (!graduated) + { + if (lastFeature != null && sf.getBegin() == lastFeature.getBegin() + && sf.getEnd() == lastFeature.getEnd() + && sf.isContactFeature() == lastFeature.isContactFeature() + && sf.getType().equals(lastFeature.getType())) + { + it.remove(); + } + } + lastFeature = sf; } - return _gps; } } diff --git a/src/jalview/viewmodel/seqfeatures/FeaturesDisplayed.java b/src/jalview/viewmodel/seqfeatures/FeaturesDisplayed.java index 4c7e3c4..f44a2d1 100644 --- a/src/jalview/viewmodel/seqfeatures/FeaturesDisplayed.java +++ b/src/jalview/viewmodel/seqfeatures/FeaturesDisplayed.java @@ -23,22 +23,21 @@ package jalview.viewmodel.seqfeatures; import jalview.api.FeaturesDisplayedI; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; -import java.util.Iterator; +import java.util.Set; public class FeaturesDisplayed implements FeaturesDisplayedI { - private HashSet featuresDisplayed = new HashSet(); + private Set featuresDisplayed = new HashSet(); - private HashSet featuresRegistered = new HashSet(); + private Set featuresRegistered = new HashSet(); public FeaturesDisplayed(FeaturesDisplayedI featuresDisplayed2) { - Iterator fdisp = featuresDisplayed2.getVisibleFeatures(); - String ftype; - while (fdisp.hasNext()) + Set fdisp = featuresDisplayed2.getVisibleFeatures(); + for (String ftype : fdisp) { - ftype = fdisp.next(); featuresDisplayed.add(ftype); featuresRegistered.add(ftype); } @@ -46,13 +45,12 @@ public class FeaturesDisplayed implements FeaturesDisplayedI public FeaturesDisplayed() { - // TODO Auto-generated constructor stub } @Override - public Iterator getVisibleFeatures() + public Set getVisibleFeatures() { - return featuresDisplayed.iterator(); + return Collections.unmodifiableSet(featuresDisplayed); } @Override diff --git a/src/jalview/workers/ColumnCounterSetWorker.java b/src/jalview/workers/ColumnCounterSetWorker.java index 2422748..24cb717 100644 --- a/src/jalview/workers/ColumnCounterSetWorker.java +++ b/src/jalview/workers/ColumnCounterSetWorker.java @@ -229,6 +229,7 @@ class ColumnCounterSetWorker extends AlignCalcWorker * * @param alignment * @param col + * (0..) * @param row * @param fr */ @@ -249,14 +250,12 @@ class ColumnCounterSetWorker extends AlignCalcWorker { return null; } - int pos = seq.findPosition(col); /* * compute a count for any displayed features at residue */ - // NB have to adjust pos if using AlignmentView.getVisibleAlignment // see JAL-2075 - List features = fr.findFeaturesAtRes(seq, pos); + List features = fr.findFeaturesAtColumn(seq, col + 1); int[] count = this.counter.count(String.valueOf(res), features); return count; } diff --git a/src/jalview/ws/DBRefFetcher.java b/src/jalview/ws/DBRefFetcher.java index fd511dc..8c8a717 100644 --- a/src/jalview/ws/DBRefFetcher.java +++ b/src/jalview/ws/DBRefFetcher.java @@ -26,7 +26,6 @@ import jalview.datamodel.AlignmentI; import jalview.datamodel.DBRefEntry; import jalview.datamodel.DBRefSource; import jalview.datamodel.Mapping; -import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.gui.CutAndPasteTransfer; import jalview.gui.DasSourceBrowser; @@ -697,28 +696,13 @@ public class DBRefFetcher implements Runnable if (updateRefFrame) { - SequenceFeature[] sfs = sequence.getSequenceFeatures(); - if (sfs != null) + /* + * relocate existing sequence features by offset + */ + int startShift = absStart - sequenceStart + 1; + if (startShift != 0) { - /* - * relocate existing sequence features by offset - */ - int start = sequenceStart; - int end = sequence.getEnd(); - int startShift = 1 - absStart - start; - - if (startShift != 0) - { - for (SequenceFeature sf : sfs) - { - if (sf.getBegin() >= start && sf.getEnd() <= end) - { - sf.setBegin(sf.getBegin() + startShift); - sf.setEnd(sf.getEnd() + startShift); - modified = true; - } - } - } + modified |= sequence.getFeatures().shiftFeatures(startShift); } } } diff --git a/src/jalview/ws/dbsources/Uniprot.java b/src/jalview/ws/dbsources/Uniprot.java index 3afe8ec..7261cba 100644 --- a/src/jalview/ws/dbsources/Uniprot.java +++ b/src/jalview/ws/dbsources/Uniprot.java @@ -28,8 +28,9 @@ import jalview.datamodel.PDBEntry; import jalview.datamodel.Sequence; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; -import jalview.datamodel.UniprotEntry; -import jalview.datamodel.UniprotFile; +import jalview.datamodel.xdb.uniprot.UniprotEntry; +import jalview.datamodel.xdb.uniprot.UniprotFeature; +import jalview.datamodel.xdb.uniprot.UniprotFile; import jalview.ws.ebi.EBIFetchClient; import jalview.ws.seqfetcher.DbSourceProxyImpl; @@ -254,16 +255,17 @@ public class Uniprot extends DbSourceProxyImpl } } - } sequence.setPDBId(onlyPdbEntries); if (entry.getFeature() != null) { - for (SequenceFeature sf : entry.getFeature()) + for (UniprotFeature uf : entry.getFeature()) { - sf.setFeatureGroup("Uniprot"); - sequence.addSequenceFeature(sf); + SequenceFeature copy = new SequenceFeature(uf.getType(), + uf.getDescription(), uf.getBegin(), uf.getEnd(), "Uniprot"); + copy.setStatus(uf.getStatus()); + sequence.addSequenceFeature(copy); } } for (DBRefEntry dbr : dbRefs) diff --git a/src/jalview/ws/jws2/AADisorderClient.java b/src/jalview/ws/jws2/AADisorderClient.java index 001f6a8..b877d20 100644 --- a/src/jalview/ws/jws2/AADisorderClient.java +++ b/src/jalview/ws/jws2/AADisorderClient.java @@ -20,7 +20,6 @@ */ package jalview.ws.jws2; -import jalview.api.AlignCalcWorkerI; import jalview.api.FeatureColourI; import jalview.bin.Cache; import jalview.datamodel.AlignmentAnnotation; @@ -238,14 +237,14 @@ public class AADisorderClient extends JabawsCalcWorker } if (vals.hasNext()) { + val = vals.next().floatValue(); sf = new SequenceFeature(type[0], type[1], - base + rn.from, base + rn.to, val = vals.next() - .floatValue(), methodName); + base + rn.from, base + rn.to, val, methodName); } else { - sf = new SequenceFeature(type[0], type[1], null, base - + rn.from, base + rn.to, methodName); + sf = new SequenceFeature(type[0], type[1], + base + rn.from, base + rn.to, methodName); } dseq.addSequenceFeature(sf); if (last != val && !Float.isNaN(last)) diff --git a/src/jalview/ws/rest/params/SeqVector.java b/src/jalview/ws/rest/params/SeqVector.java index cbd73dd..578e7cc 100644 --- a/src/jalview/ws/rest/params/SeqVector.java +++ b/src/jalview/ws/rest/params/SeqVector.java @@ -65,7 +65,7 @@ public class SeqVector extends InputType { idvector.append(sep); } - idvector.append(seq.getSequence()); + idvector.append(seq.getSequenceAsString()); } return new StringBody(idvector.toString()); } diff --git a/test/MCview/PDBChainTest.java b/test/MCview/PDBChainTest.java index 7132939..defcdbc 100644 --- a/test/MCview/PDBChainTest.java +++ b/test/MCview/PDBChainTest.java @@ -36,6 +36,7 @@ import jalview.schemes.TaylorColourScheme; import jalview.structure.StructureImportSettings; import java.awt.Color; +import java.util.List; import java.util.Vector; import org.testng.annotations.BeforeClass; @@ -258,19 +259,19 @@ public class PDBChainTest /* * check sequence features */ - SequenceFeature[] sfs = c.sequence.getSequenceFeatures(); - assertEquals(3, sfs.length); - assertEquals("RESNUM", sfs[0].type); - assertEquals("MET:4 1gaqA", sfs[0].description); - assertEquals(4, sfs[0].begin); - assertEquals(4, sfs[0].end); - assertEquals("RESNUM", sfs[0].type); - assertEquals("LYS:5 1gaqA", sfs[1].description); - assertEquals(5, sfs[1].begin); - assertEquals(5, sfs[1].end); - assertEquals("LEU:6 1gaqA", sfs[2].description); - assertEquals(6, sfs[2].begin); - assertEquals(6, sfs[2].end); + List sfs = c.sequence.getSequenceFeatures(); + assertEquals(3, sfs.size()); + assertEquals("RESNUM", sfs.get(0).type); + assertEquals("MET:4 1gaqA", sfs.get(0).description); + assertEquals(4, sfs.get(0).begin); + assertEquals(4, sfs.get(0).end); + assertEquals("RESNUM", sfs.get(0).type); + assertEquals("LYS:5 1gaqA", sfs.get(1).description); + assertEquals(5, sfs.get(1).begin); + assertEquals(5, sfs.get(1).end); + assertEquals("LEU:6 1gaqA", sfs.get(2).description); + assertEquals(6, sfs.get(2).begin); + assertEquals(6, sfs.get(2).end); } private Atom makeAtom(int resnum, String name, String resname) diff --git a/test/jalview/analysis/AlignmentSorterTest.java b/test/jalview/analysis/AlignmentSorterTest.java index 088611e..3b9be23 100644 --- a/test/jalview/analysis/AlignmentSorterTest.java +++ b/test/jalview/analysis/AlignmentSorterTest.java @@ -31,10 +31,9 @@ public class AlignmentSorterTest /* * sort with no score features does nothing */ - PA.setValue(AlignmentSorter.class, "lastSortByFeatureScore", null); + PA.setValue(AlignmentSorter.class, "sortByFeatureCriteria", null); - AlignmentSorter.sortByFeature((String) null, null, 0, al.getWidth(), - al, + AlignmentSorter.sortByFeature(null, null, 0, al.getWidth(), al, AlignmentSorter.FEATURE_SCORE); assertSame(al.getSequenceAt(0), seq1); assertSame(al.getSequenceAt(1), seq2); @@ -65,9 +64,9 @@ public class AlignmentSorterTest * sort by ascending score, no filter on feature type or group * NB sort order for the same feature set (none) gets toggled, so descending */ - PA.setValue(AlignmentSorter.class, "sortByFeatureScoreAscending", true); - AlignmentSorter.sortByFeature((String) null, null, 0, al.getWidth(), - al, AlignmentSorter.FEATURE_SCORE); + PA.setValue(AlignmentSorter.class, "sortByFeatureAscending", true); + AlignmentSorter.sortByFeature(null, null, 0, al.getWidth(), al, + AlignmentSorter.FEATURE_SCORE); assertSame(al.getSequenceAt(3), seq3); // -0.5 assertSame(al.getSequenceAt(2), seq2); // 2.5 assertSame(al.getSequenceAt(1), seq1); // 3.0 @@ -76,8 +75,8 @@ public class AlignmentSorterTest /* * repeat sort toggles order - now ascending */ - AlignmentSorter.sortByFeature((String) null, null, 0, al.getWidth(), - al, AlignmentSorter.FEATURE_SCORE); + AlignmentSorter.sortByFeature(null, null, 0, al.getWidth(), al, + AlignmentSorter.FEATURE_SCORE); assertSame(al.getSequenceAt(0), seq3); // -0.5 assertSame(al.getSequenceAt(1), seq2); // 2.5 assertSame(al.getSequenceAt(2), seq1); // 3.0 @@ -116,7 +115,7 @@ public class AlignmentSorterTest */ // fails because seq1.findPosition(4) returns 4 // although residue 4 is in column 5! - JAL-2544 - AlignmentSorter.sortByFeature((String) null, null, 0, 4, al, + AlignmentSorter.sortByFeature(null, null, 0, 4, al, AlignmentSorter.FEATURE_SCORE); assertSame(al.getSequenceAt(0), seq3); // -4 assertSame(al.getSequenceAt(1), seq1); // 2.0 diff --git a/test/jalview/analysis/AlignmentUtilsTests.java b/test/jalview/analysis/AlignmentUtilsTests.java index bada3ca..4439bb9 100644 --- a/test/jalview/analysis/AlignmentUtilsTests.java +++ b/test/jalview/analysis/AlignmentUtilsTests.java @@ -40,6 +40,7 @@ import jalview.datamodel.SearchResultsI; import jalview.datamodel.Sequence; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.SequenceFeatures; import jalview.gui.JvOptionPane; import jalview.io.AppletFormatAdapter; import jalview.io.DataSourceType; @@ -1179,12 +1180,12 @@ public class AlignmentUtilsTests /* * check cds2 acquired a variant feature in position 5 */ - SequenceFeature[] sfs = cds2Dss.getSequenceFeatures(); + List sfs = cds2Dss.getSequenceFeatures(); assertNotNull(sfs); - assertEquals(1, sfs.length); - assertEquals("variant", sfs[0].type); - assertEquals(5, sfs[0].begin); - assertEquals(5, sfs[0].end); + assertEquals(1, sfs.size()); + assertEquals("variant", sfs.get(0).type); + assertEquals(5, sfs.get(0).begin); + assertEquals(5, sfs.get(0).end); } /** @@ -1489,39 +1490,39 @@ public class AlignmentUtilsTests * that partially overlap 5' or 3' (start or end) of target sequence */ AlignmentUtils.transferFeatures(dna, cds, map, null); - SequenceFeature[] sfs = cds.getSequenceFeatures(); - assertEquals(6, sfs.length); + List sfs = cds.getSequenceFeatures(); + assertEquals(6, sfs.size()); - SequenceFeature sf = sfs[0]; + SequenceFeature sf = sfs.get(0); assertEquals("type2", sf.getType()); assertEquals("desc2", sf.getDescription()); assertEquals(2f, sf.getScore()); assertEquals(1, sf.getBegin()); assertEquals(1, sf.getEnd()); - sf = sfs[1]; + sf = sfs.get(1); assertEquals("type3", sf.getType()); assertEquals("desc3", sf.getDescription()); assertEquals(3f, sf.getScore()); assertEquals(1, sf.getBegin()); assertEquals(3, sf.getEnd()); - sf = sfs[2]; + sf = sfs.get(2); assertEquals("type4", sf.getType()); assertEquals(2, sf.getBegin()); assertEquals(5, sf.getEnd()); - sf = sfs[3]; + sf = sfs.get(3); assertEquals("type5", sf.getType()); assertEquals(1, sf.getBegin()); assertEquals(6, sf.getEnd()); - sf = sfs[4]; + sf = sfs.get(4); assertEquals("type8", sf.getType()); assertEquals(6, sf.getBegin()); assertEquals(6, sf.getEnd()); - sf = sfs[5]; + sf = sfs.get(5); assertEquals("type9", sf.getType()); assertEquals(6, sf.getBegin()); assertEquals(6, sf.getEnd()); @@ -1551,10 +1552,10 @@ public class AlignmentUtilsTests // desc4 and desc8 are the 'omit these' varargs AlignmentUtils.transferFeatures(dna, cds, map, null, "type4", "type8"); - SequenceFeature[] sfs = cds.getSequenceFeatures(); - assertEquals(1, sfs.length); + List sfs = cds.getSequenceFeatures(); + assertEquals(1, sfs.size()); - SequenceFeature sf = sfs[0]; + SequenceFeature sf = sfs.get(0); assertEquals("type5", sf.getType()); assertEquals(1, sf.getBegin()); assertEquals(6, sf.getEnd()); @@ -1584,10 +1585,10 @@ public class AlignmentUtilsTests // "type5" is the 'select this type' argument AlignmentUtils.transferFeatures(dna, cds, map, "type5"); - SequenceFeature[] sfs = cds.getSequenceFeatures(); - assertEquals(1, sfs.length); + List sfs = cds.getSequenceFeatures(); + assertEquals(1, sfs.size()); - SequenceFeature sf = sfs[0]; + SequenceFeature sf = sfs.get(0); assertEquals("type5", sf.getType()); assertEquals(1, sf.getBegin()); assertEquals(6, sf.getEnd()); @@ -2078,24 +2079,29 @@ public class AlignmentUtilsTests * var6 P -> H COSMIC * var6 P -> R COSMIC */ - SequenceFeature[] sfs = peptide.getSequenceFeatures(); - assertEquals(5, sfs.length); + List sfs = peptide.getSequenceFeatures(); + SequenceFeatures.sortFeatures(sfs, true); + assertEquals(5, sfs.size()); - SequenceFeature sf = sfs[0]; + /* + * features are sorted by start position ascending, but in no + * particular order where start positions match; asserts here + * simply match the data returned (the order is not important) + */ + SequenceFeature sf = sfs.get(0); assertEquals(1, sf.getBegin()); assertEquals(1, sf.getEnd()); - assertEquals("p.Lys1Glu", sf.getDescription()); - assertEquals("var1.125A>G", sf.getValue("ID")); - assertNull(sf.getValue("clinical_significance")); - assertEquals("ID=var1.125A>G", sf.getAttributes()); + assertEquals("p.Lys1Asn", sf.getDescription()); + assertEquals("var4", sf.getValue("ID")); + assertEquals("Benign", sf.getValue("clinical_significance")); + assertEquals("ID=var4;clinical_significance=Benign", sf.getAttributes()); assertEquals(1, sf.links.size()); - // link to variation is urlencoded assertEquals( - "p.Lys1Glu var1.125A>G|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var1.125A%3EG", + "p.Lys1Asn var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4", sf.links.get(0)); assertEquals(ensembl, sf.getFeatureGroup()); - sf = sfs[1]; + sf = sfs.get(1); assertEquals(1, sf.getBegin()); assertEquals(1, sf.getEnd()); assertEquals("p.Lys1Gln", sf.getDescription()); @@ -2108,43 +2114,44 @@ public class AlignmentUtilsTests sf.links.get(0)); assertEquals(dbSnp, sf.getFeatureGroup()); - sf = sfs[2]; + sf = sfs.get(2); assertEquals(1, sf.getBegin()); assertEquals(1, sf.getEnd()); - assertEquals("p.Lys1Asn", sf.getDescription()); - assertEquals("var4", sf.getValue("ID")); - assertEquals("Benign", sf.getValue("clinical_significance")); - assertEquals("ID=var4;clinical_significance=Benign", sf.getAttributes()); + assertEquals("p.Lys1Glu", sf.getDescription()); + assertEquals("var1.125A>G", sf.getValue("ID")); + assertNull(sf.getValue("clinical_significance")); + assertEquals("ID=var1.125A>G", sf.getAttributes()); assertEquals(1, sf.links.size()); + // link to variation is urlencoded assertEquals( - "p.Lys1Asn var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4", + "p.Lys1Glu var1.125A>G|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var1.125A%3EG", sf.links.get(0)); assertEquals(ensembl, sf.getFeatureGroup()); - // var5 generates two distinct protein variant features - sf = sfs[3]; + sf = sfs.get(3); assertEquals(3, sf.getBegin()); assertEquals(3, sf.getEnd()); - assertEquals("p.Pro3His", sf.getDescription()); + assertEquals("p.Pro3Arg", sf.getDescription()); assertEquals("var6", sf.getValue("ID")); assertEquals("Good", sf.getValue("clinical_significance")); assertEquals("ID=var6;clinical_significance=Good", sf.getAttributes()); assertEquals(1, sf.links.size()); assertEquals( - "p.Pro3His var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6", + "p.Pro3Arg var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6", sf.links.get(0)); assertEquals(cosmic, sf.getFeatureGroup()); - sf = sfs[4]; + // var5 generates two distinct protein variant features + sf = sfs.get(4); assertEquals(3, sf.getBegin()); assertEquals(3, sf.getEnd()); - assertEquals("p.Pro3Arg", sf.getDescription()); + assertEquals("p.Pro3His", sf.getDescription()); assertEquals("var6", sf.getValue("ID")); assertEquals("Good", sf.getValue("clinical_significance")); assertEquals("ID=var6;clinical_significance=Good", sf.getAttributes()); assertEquals(1, sf.links.size()); assertEquals( - "p.Pro3Arg var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6", + "p.Pro3His var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6", sf.links.get(0)); assertEquals(cosmic, sf.getFeatureGroup()); } diff --git a/test/jalview/analysis/RnaTest.java b/test/jalview/analysis/RnaTest.java index 814d2d4..1faf3f2 100644 --- a/test/jalview/analysis/RnaTest.java +++ b/test/jalview/analysis/RnaTest.java @@ -27,9 +27,10 @@ import static org.testng.AssertJUnit.assertTrue; import static org.testng.AssertJUnit.fail; import jalview.analysis.SecStrConsensus.SimpleBP; +import jalview.datamodel.SequenceFeature; import jalview.gui.JvOptionPane; -import java.util.Vector; +import java.util.List; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -48,7 +49,7 @@ public class RnaTest public void testGetSimpleBPs() throws WUSSParseException { String rna = "([{})]"; // JAL-1081 example - Vector bps = Rna.getSimpleBPs(rna); + List bps = Rna.getSimpleBPs(rna); assertEquals(3, bps.size()); /* @@ -313,4 +314,54 @@ public class RnaTest .valueOf((char) i) + " ")); } } + + @Test(groups = "Functional") + public void testGetHelixMap_oneHelix() throws WUSSParseException + { + String rna = ".(..[{.<..>}..].)"; + SequenceFeature[] sfs = Rna.getHelixMap(rna); + assertEquals(4, sfs.length); + + /* + * pairs are added in the order in which the closing bracket is found + * (see testGetSimpleBPs) + */ + assertEquals(7, sfs[0].getBegin()); + assertEquals(10, sfs[0].getEnd()); + assertEquals("0", sfs[0].getFeatureGroup()); + assertEquals(5, sfs[1].getBegin()); + assertEquals(11, sfs[1].getEnd()); + assertEquals("0", sfs[1].getFeatureGroup()); + assertEquals(4, sfs[2].getBegin()); + assertEquals(14, sfs[2].getEnd()); + assertEquals("0", sfs[2].getFeatureGroup()); + assertEquals(1, sfs[3].getBegin()); + assertEquals(16, sfs[3].getEnd()); + assertEquals("0", sfs[3].getFeatureGroup()); + } + + @Test(groups = "Functional") + public void testGetHelixMap_twoHelices() throws WUSSParseException + { + String rna = ".([.)]..{.<}.>"; + SequenceFeature[] sfs = Rna.getHelixMap(rna); + assertEquals(4, sfs.length); + + /* + * pairs are added in the order in which the closing bracket is found + * (see testGetSimpleBPs) + */ + assertEquals(1, sfs[0].getBegin()); + assertEquals(4, sfs[0].getEnd()); + assertEquals("0", sfs[0].getFeatureGroup()); + assertEquals(2, sfs[1].getBegin()); + assertEquals(5, sfs[1].getEnd()); + assertEquals("0", sfs[1].getFeatureGroup()); + assertEquals(8, sfs[2].getBegin()); + assertEquals(11, sfs[2].getEnd()); + assertEquals("1", sfs[2].getFeatureGroup()); + assertEquals(10, sfs[3].getBegin()); + assertEquals(13, sfs[3].getEnd()); + assertEquals("1", sfs[3].getFeatureGroup()); + } } diff --git a/test/jalview/analysis/SeqsetUtilsTest.java b/test/jalview/analysis/SeqsetUtilsTest.java index 11cb10c..9839ba0 100644 --- a/test/jalview/analysis/SeqsetUtilsTest.java +++ b/test/jalview/analysis/SeqsetUtilsTest.java @@ -62,26 +62,25 @@ public class SeqsetUtilsTest AlignmentI al = new Alignment(sqset); al.setDataset(null); AlignmentI ds = al.getDataset(); - SequenceFeature sf1 = new SequenceFeature("f1", "foo", "bleh", 2, 3, - "far"), sf2 = new SequenceFeature("f2", "foo", "bleh", 2, 3, - "far"); + SequenceFeature sf1 = new SequenceFeature("f1", "foo", 2, 3, "far"); + SequenceFeature sf2 = new SequenceFeature("f2", "foo", 2, 3, "far"); ds.getSequenceAt(0).addSequenceFeature(sf1); Hashtable unq = SeqsetUtils.uniquify(sqset, true); SequenceI[] sqset2 = new SequenceI[] { new Sequence(sqset[0].getName(), sqset[0].getSequenceAsString()), new Sequence(sqset[1].getName(), sqset[1].getSequenceAsString()) }; - Assert.assertTrue(sqset[0].getSequenceFeatures()[0] == sf1); - Assert.assertEquals(sqset2[0].getSequenceFeatures(), null); + Assert.assertSame(sqset[0].getSequenceFeatures().get(0), sf1); + Assert.assertTrue(sqset2[0].getSequenceFeatures().isEmpty()); ds.getSequenceAt(0).addSequenceFeature(sf2); - Assert.assertEquals(sqset[0].getSequenceFeatures().length, 2); + Assert.assertEquals(sqset[0].getSequenceFeatures().size(), 2); SeqsetUtils.deuniquify(unq, sqset2); // explicitly test that original sequence features still exist because they // are on the shared dataset sequence - Assert.assertEquals(sqset[0].getSequenceFeatures().length, 2); - Assert.assertEquals(sqset2[0].getSequenceFeatures().length, 2); - Assert.assertTrue(sqset[0].getSequenceFeatures()[0] == sqset2[0] - .getSequenceFeatures()[0]); - Assert.assertTrue(sqset[0].getSequenceFeatures()[1] == sqset2[0] - .getSequenceFeatures()[1]); + Assert.assertEquals(sqset[0].getSequenceFeatures().size(), 2); + Assert.assertEquals(sqset2[0].getSequenceFeatures().size(), 2); + Assert.assertSame(sqset[0].getSequenceFeatures().get(0), sqset2[0] + .getSequenceFeatures().get(0)); + Assert.assertSame(sqset[0].getSequenceFeatures().get(1), sqset2[0] + .getSequenceFeatures().get(1)); } } diff --git a/test/jalview/analysis/scoremodels/FeatureDistanceModelTest.java b/test/jalview/analysis/scoremodels/FeatureDistanceModelTest.java index 0577fae..16ca70d 100644 --- a/test/jalview/analysis/scoremodels/FeatureDistanceModelTest.java +++ b/test/jalview/analysis/scoremodels/FeatureDistanceModelTest.java @@ -23,6 +23,7 @@ package jalview.analysis.scoremodels; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; +import jalview.api.analysis.ScoreModelI; import jalview.api.analysis.SimilarityParamsI; import jalview.datamodel.Alignment; import jalview.datamodel.AlignmentI; @@ -84,18 +85,18 @@ public class FeatureDistanceModelTest SequenceI ds = al.getSequenceAt(i).getDatasetSequence(); if (sf1[i * 2] > 0) { - ds.addSequenceFeature(new SequenceFeature("sf1", "sf1", "sf1", - sf1[i * 2], sf1[i * 2 + 1], "sf1")); + ds.addSequenceFeature(new SequenceFeature("sf1", "sf1", sf1[i * 2], + sf1[i * 2 + 1], "sf1")); } if (sf2[i * 2] > 0) { - ds.addSequenceFeature(new SequenceFeature("sf2", "sf2", "sf2", - sf2[i * 2], sf2[i * 2 + 1], "sf2")); + ds.addSequenceFeature(new SequenceFeature("sf2", "sf2", sf2[i * 2], + sf2[i * 2 + 1], "sf2")); } if (sf3[i * 2] > 0) { - ds.addSequenceFeature(new SequenceFeature("sf3", "sf3", "sf3", - sf3[i * 2], sf3[i * 2 + 1], "sf3")); + ds.addSequenceFeature(new SequenceFeature("sf3", "sf3", sf3[i * 2], + sf3[i * 2 + 1], "sf3")); } } alf.setShowSeqFeatures(true); @@ -113,12 +114,12 @@ public class FeatureDistanceModelTest public void testFeatureScoreModel() throws Exception { AlignFrame alf = getTestAlignmentFrame(); - FeatureDistanceModel fsm = new FeatureDistanceModel(); - assertTrue(fsm.configureFromAlignmentView(alf.getCurrentView() - .getAlignPanel())); + ScoreModelI sm = new FeatureDistanceModel(); + sm = ScoreModels.getInstance().getScoreModel(sm.getName(), + alf.getCurrentView().getAlignPanel()); alf.selectAllSequenceMenuItem_actionPerformed(null); - MatrixI dm = fsm.findDistances( + MatrixI dm = sm.findDistances( alf.getViewport().getAlignmentView(true), SimilarityParams.Jalview); assertEquals(dm.getValue(0, 2), 0d, @@ -133,11 +134,11 @@ public class FeatureDistanceModelTest AlignFrame alf = getTestAlignmentFrame(); // hiding first two columns shouldn't affect the tree alf.getViewport().hideColumns(0, 1); - FeatureDistanceModel fsm = new FeatureDistanceModel(); - assertTrue(fsm.configureFromAlignmentView(alf.getCurrentView() - .getAlignPanel())); + ScoreModelI sm = new FeatureDistanceModel(); + sm = ScoreModels.getInstance().getScoreModel(sm.getName(), + alf.getCurrentView().getAlignPanel()); alf.selectAllSequenceMenuItem_actionPerformed(null); - MatrixI dm = fsm.findDistances( + MatrixI dm = sm.findDistances( alf.getViewport().getAlignmentView(true), SimilarityParams.Jalview); assertEquals(dm.getValue(0, 2), 0d, @@ -153,11 +154,12 @@ public class FeatureDistanceModelTest // hide columns and check tree changes alf.getViewport().hideColumns(3, 4); alf.getViewport().hideColumns(0, 1); - FeatureDistanceModel fsm = new FeatureDistanceModel(); - assertTrue(fsm.configureFromAlignmentView(alf.getCurrentView() - .getAlignPanel())); + // getName() can become static in Java 8 + ScoreModelI sm = new FeatureDistanceModel(); + sm = ScoreModels.getInstance().getScoreModel(sm.getName(), + alf.getCurrentView().getAlignPanel()); alf.selectAllSequenceMenuItem_actionPerformed(null); - MatrixI dm = fsm.findDistances( + MatrixI dm = sm.findDistances( alf.getViewport().getAlignmentView(true), SimilarityParams.Jalview); assertEquals( @@ -197,22 +199,22 @@ public class FeatureDistanceModelTest Assert.assertEquals(af.getFeatureRenderer().getDisplayedFeatureTypes() .size(), 1, "Should be just one feature type displayed"); // step through and check for pointwise feature presence/absence - Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 1) + Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 1) .size(), 0); // step through and check for pointwise feature presence/absence - Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 2) + Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 2) .size(), 1); // step through and check for pointwise feature presence/absence - Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 3) + Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 3) .size(), 0); // step through and check for pointwise feature presence/absence - Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 4) + Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 4) .size(), 0); // step through and check for pointwise feature presence/absence - Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 5) + Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 5) .size(), 1); // step through and check for pointwise feature presence/absence - Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 6) + Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 6) .size(), 0); } @@ -252,13 +254,15 @@ public class FeatureDistanceModelTest alf.setShowSeqFeatures(true); alf.getFeatureRenderer().findAllFeatures(true); - FeatureDistanceModel fsm = new FeatureDistanceModel(); - assertTrue(fsm.configureFromAlignmentView(alf.getCurrentView() - .getAlignPanel())); + ScoreModelI sm = new FeatureDistanceModel(); + sm = ScoreModels.getInstance().getScoreModel(sm.getName(), + alf.getCurrentView().getAlignPanel()); alf.selectAllSequenceMenuItem_actionPerformed(null); - MatrixI distances = fsm.findDistances(alf.getViewport() - .getAlignmentView(true), SimilarityParams.Jalview); + AlignmentView alignmentView = alf.getViewport() + .getAlignmentView(true); + MatrixI distances = sm.findDistances(alignmentView, + SimilarityParams.Jalview); assertEquals(distances.width(), 2); assertEquals(distances.height(), 2); assertEquals(distances.getValue(0, 0), 0d); @@ -279,9 +283,10 @@ public class FeatureDistanceModelTest AlignViewport viewport = af.getViewport(); AlignmentView view = viewport.getAlignmentView(false); - FeatureDistanceModel sm = new FeatureDistanceModel(); - sm.configureFromAlignmentView(af.alignPanel); - + ScoreModelI sm = new FeatureDistanceModel(); + sm = ScoreModels.getInstance().getScoreModel(sm.getName(), + af.alignPanel); + /* * feature distance model always normalises by region width * gap-gap is always included (but scores zero) diff --git a/test/jalview/commands/EditCommandTest.java b/test/jalview/commands/EditCommandTest.java index 3223042..155f00e 100644 --- a/test/jalview/commands/EditCommandTest.java +++ b/test/jalview/commands/EditCommandTest.java @@ -21,6 +21,7 @@ package jalview.commands; import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertNull; import static org.testng.AssertJUnit.assertSame; import jalview.commands.EditCommand.Action; @@ -28,11 +29,15 @@ import jalview.commands.EditCommand.Edit; import jalview.datamodel.Alignment; import jalview.datamodel.AlignmentI; import jalview.datamodel.Sequence; +import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.SequenceFeatures; import jalview.gui.JvOptionPane; +import java.util.List; import java.util.Map; +import org.testng.Assert; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -45,6 +50,14 @@ import org.testng.annotations.Test; */ public class EditCommandTest { + /* + * compute n(n+1)/2 e.g. + * func(5) = 5 + 4 + 3 + 2 + 1 = 15 + */ + private static int func(int i) + { + return i * (i + 1) / 2; + } @BeforeClass(alwaysRun = true) public void setUpJvOptionPane() @@ -639,4 +652,222 @@ public class EditCommandTest assertEquals(ds2, unwound.get(ds2).getDatasetSequence()); assertEquals(ds3, unwound.get(ds3).getDatasetSequence()); } + + /** + * Test a cut action's relocation of sequence features + */ + @Test(groups = { "Functional" }) + public void testCut_withFeatures() + { + /* + * create sequence features before, after and overlapping + * a cut of columns/residues 4-7 + */ + SequenceI seq0 = seqs[0]; + seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f, + null)); + seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6, + 0f, null)); + seq0.addSequenceFeature(new SequenceFeature("internal", "", 5, 6, 0f, + null)); + seq0.addSequenceFeature(new SequenceFeature("overlap right", "", 7, 8, + 0f, null)); + seq0.addSequenceFeature(new SequenceFeature("after", "", 8, 10, 0f, + null)); + + Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0 + EditCommand.cut(ec, new AlignmentI[] { al }); + + List sfs = seq0.getSequenceFeatures(); + SequenceFeatures.sortFeatures(sfs, true); + + assertEquals(4, sfs.size()); // feature internal to cut has been deleted + SequenceFeature sf = sfs.get(0); + assertEquals("before", sf.getType()); + assertEquals(1, sf.getBegin()); + assertEquals(3, sf.getEnd()); + sf = sfs.get(1); + assertEquals("overlap left", sf.getType()); + assertEquals(2, sf.getBegin()); + assertEquals(3, sf.getEnd()); // truncated by cut + sf = sfs.get(2); + assertEquals("overlap right", sf.getType()); + assertEquals(4, sf.getBegin()); // shifted left by cut + assertEquals(5, sf.getEnd()); // truncated by cut + sf = sfs.get(3); + assertEquals("after", sf.getType()); + assertEquals(4, sf.getBegin()); // shifted left by cut + assertEquals(6, sf.getEnd()); // shifted left by cut + } + + /** + * Test a cut action's relocation of sequence features, with full coverage of + * all possible feature and cut locations for a 5-position ungapped sequence + */ + @Test(groups = { "Functional" }) + public void testCut_withFeatures_exhaustive() + { + /* + * create a sequence features on each subrange of 1-5 + */ + SequenceI seq0 = new Sequence("seq", "ABCDE"); + AlignmentI alignment = new Alignment(new SequenceI[] { seq0 }); + alignment.setDataset(null); + for (int from = 1; from <= seq0.getLength(); from++) + { + for (int to = from; to <= seq0.getLength(); to++) + { + String desc = String.format("%d-%d", from, to); + SequenceFeature sf = new SequenceFeature("test", desc, from, to, + 0f, + null); + sf.setValue("from", Integer.valueOf(from)); + sf.setValue("to", Integer.valueOf(to)); + seq0.addSequenceFeature(sf); + } + } + // sanity check + List sfs = seq0.getSequenceFeatures(); + assertEquals(func(5), sfs.size()); + + /* + * now perform all possible cuts of subranges of 1-5 (followed by Undo) + * and validate the resulting remaining sequence features! + */ + SequenceI[] sqs = new SequenceI[] { seq0 }; + + // goal is to have this passing for all from/to values!! + // for (int from = 0; from < seq0.getLength(); from++) + // { + // for (int to = from; to < seq0.getLength(); to++) + for (int from = 1; from < 3; from++) + { + for (int to = 2; to < 3; to++) + { + testee.appendEdit(Action.CUT, sqs, from, (to - from + 1), + alignment, true); + + sfs = seq0.getSequenceFeatures(); + + /* + * confirm the number of features has reduced by the + * number of features within the cut region i.e. by + * func(length of cut) + */ + String msg = String.format("Cut %d-%d ", from, to); + if (to - from == 4) + { + // all columns cut + assertNull(sfs); + } + else + { + assertEquals(msg + "wrong number of features left", func(5) + - func(to - from + 1), sfs.size()); + } + + /* + * inspect individual features + */ + if (sfs != null) + { + for (SequenceFeature sf : sfs) + { + checkFeatureRelocation(sf, from + 1, to + 1); + } + } + /* + * undo ready for next cut + */ + testee.undoCommand(new AlignmentI[] { alignment }); + assertEquals(func(5), seq0.getSequenceFeatures().size()); + } + } + } + + /** + * Helper method to check a feature has been correctly relocated after a cut + * + * @param sf + * @param from + * start of cut (first residue cut) + * @param to + * end of cut (last residue cut) + */ + private void checkFeatureRelocation(SequenceFeature sf, int from, int to) + { + // TODO handle the gapped sequence case as well + int cutSize = to - from + 1; + int oldFrom = ((Integer) sf.getValue("from")).intValue(); + int oldTo = ((Integer) sf.getValue("to")).intValue(); + + String msg = String.format( + "Feature %s relocated to %d-%d after cut of %d-%d", + sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to); + if (oldTo < from) + { + // before cut region so unchanged + assertEquals("1: " + msg, oldFrom, sf.getBegin()); + assertEquals("2: " + msg, oldTo, sf.getEnd()); + } + else if (oldFrom > to) + { + // follows cut region - shift by size of cut + assertEquals("3: " + msg, oldFrom - cutSize, sf.getBegin()); + assertEquals("4: " + msg, oldTo - cutSize, sf.getEnd()); + } + else if (oldFrom < from && oldTo > to) + { + // feature encloses cut region - shrink it right + assertEquals("5: " + msg, oldFrom, sf.getBegin()); + assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd()); + } + else if (oldFrom < from) + { + // feature overlaps left side of cut region - truncated right + assertEquals("7: " + msg, from - 1, sf.getEnd()); + } + else if (oldTo > to) + { + // feature overlaps right side of cut region - truncated left + assertEquals("8: " + msg, from, sf.getBegin()); + assertEquals("9: " + msg, from + oldTo - to - 1, sf.getEnd()); + } + else + { + // feature internal to cut - should have been deleted! + Assert.fail(msg + " - should have been deleted"); + } + } + + /** + * Test a cut action's relocation of sequence features + */ + @Test(groups = { "Functional" }) + public void testCut_gappedWithFeatures() + { + /* + * create sequence features before, after and overlapping + * a cut of columns/residues 4-7 + */ + SequenceI seq0 = new Sequence("seq", "A-BCC"); + seq0.addSequenceFeature(new SequenceFeature("", "", 3, 4, 0f, + null)); + AlignmentI alignment = new Alignment(new SequenceI[] { seq0 }); + // cut columns of A-B + Edit ec = testee.new Edit(Action.CUT, seqs, 0, 3, alignment); // cols 0-3 + // base 0 + EditCommand.cut(ec, new AlignmentI[] { alignment }); + + /* + * feature on CC(3-4) should now be on CC(1-2) + */ + List sfs = seq0.getSequenceFeatures(); + assertEquals(1, sfs.size()); + SequenceFeature sf = sfs.get(0); + assertEquals(1, sf.getBegin()); + assertEquals(2, sf.getEnd()); + + // TODO add further cases including Undo - see JAL-2541 + } } diff --git a/test/jalview/datamodel/AlignmentTest.java b/test/jalview/datamodel/AlignmentTest.java index 1cfa771..4b5d096 100644 --- a/test/jalview/datamodel/AlignmentTest.java +++ b/test/jalview/datamodel/AlignmentTest.java @@ -1300,4 +1300,25 @@ public class AlignmentTest AlignmentI alignment = new Alignment(new SequenceI[] { seq }); alignment.setDataset(alignment); } + + @Test(groups = "Functional") + public void testAppend() + { + SequenceI seq = new Sequence("seq1", "FRMLPSRT-A--L-"); + AlignmentI alignment = new Alignment(new SequenceI[] { seq }); + alignment.setGapCharacter('-'); + SequenceI seq2 = new Sequence("seq1", "KP..L.FQII."); + AlignmentI alignment2 = new Alignment(new SequenceI[] { seq2 }); + alignment2.setGapCharacter('.'); + + alignment.append(alignment2); + + assertEquals('-', alignment.getGapCharacter()); + assertSame(seq, alignment.getSequenceAt(0)); + assertEquals("KP--L-FQII-", alignment.getSequenceAt(1) + .getSequenceAsString()); + + // todo test coverage for annotations, mappings, groups, + // hidden sequences, properties + } } diff --git a/test/jalview/datamodel/SeqCigarTest.java b/test/jalview/datamodel/SeqCigarTest.java index ab25aa6..89169d6 100644 --- a/test/jalview/datamodel/SeqCigarTest.java +++ b/test/jalview/datamodel/SeqCigarTest.java @@ -121,7 +121,7 @@ public class SeqCigarTest /* * TODO: can we add assertions to the sysouts that follow? */ - System.out.println("Original sequence align:\n" + sub_gapped_s + System.out.println("\nOriginal sequence align:\n" + sub_gapped_s + "\nReconstructed window from 8 to 48\n" + "XXXXXXXX" + sub_se_gp.getSequenceString('-') + "..." + "\nCigar String:" + sub_se_gp.getCigarstring() + "\n"); @@ -193,7 +193,8 @@ public class SeqCigarTest SequenceI gen_sgapped_s = gen_sgapped.getSeq('-'); // assertEquals("Couldn't reconstruct sequence", s_gapped.getSequence(), // gen_sgapped_s); - if (!gen_sgapped_s.getSequence().equals(s_gapped.getSequence())) + if (!gen_sgapped_s.getSequenceAsString().equals( + s_gapped.getSequenceAsString())) { // TODO: investigate errors reported here, to allow full conversion to // passing JUnit assertion form diff --git a/test/jalview/datamodel/SequenceFeatureTest.java b/test/jalview/datamodel/SequenceFeatureTest.java index 2da8918..fbeb365 100644 --- a/test/jalview/datamodel/SequenceFeatureTest.java +++ b/test/jalview/datamodel/SequenceFeatureTest.java @@ -42,7 +42,7 @@ public class SequenceFeatureTest } @Test(groups = { "Functional" }) - public void testCopyConstructor() + public void testCopyConstructors() { SequenceFeature sf1 = new SequenceFeature("type", "desc", 22, 33, 12.5f, "group"); @@ -56,10 +56,41 @@ public class SequenceFeatureTest assertEquals("desc", sf2.getDescription()); assertEquals(22, sf2.getBegin()); assertEquals(33, sf2.getEnd()); + assertEquals(12.5f, sf2.getScore()); assertEquals("+", sf2.getValue("STRAND")); assertEquals("Testing", sf2.getValue("Note")); // shallow clone of otherDetails map - contains the same object values! assertSame(count, sf2.getValue("Count")); + + /* + * copy constructor modifying begin/end/group/score + */ + SequenceFeature sf3 = new SequenceFeature(sf1, 11, 14, "group2", 17.4f); + assertEquals("type", sf3.getType()); + assertEquals("desc", sf3.getDescription()); + assertEquals(11, sf3.getBegin()); + assertEquals(14, sf3.getEnd()); + assertEquals(17.4f, sf3.getScore()); + assertEquals("+", sf3.getValue("STRAND")); + assertEquals("Testing", sf3.getValue("Note")); + // shallow clone of otherDetails map - contains the same object values! + assertSame(count, sf3.getValue("Count")); + + /* + * copy constructor modifying type/begin/end/group/score + */ + SequenceFeature sf4 = new SequenceFeature(sf1, "Disulfide bond", 12, + 15, "group3", -9.1f); + assertEquals("Disulfide bond", sf4.getType()); + assertTrue(sf4.isContactFeature()); + assertEquals("desc", sf4.getDescription()); + assertEquals(12, sf4.getBegin()); + assertEquals(15, sf4.getEnd()); + assertEquals(-9.1f, sf4.getScore()); + assertEquals("+", sf4.getValue("STRAND")); + assertEquals("Testing", sf4.getValue("Note")); + // shallow clone of otherDetails map - contains the same object values! + assertSame(count, sf4.getValue("Count")); } /** @@ -123,51 +154,61 @@ public class SequenceFeatureTest assertEquals(sf1.hashCode(), sf2.hashCode()); // changing type breaks equals: - String restores = sf2.getType(); - sf2.setType("Type"); - assertFalse(sf1.equals(sf2)); - sf2.setType(restores); + SequenceFeature sf3 = new SequenceFeature("type", "desc", 22, 33, + 12.5f, "group"); + SequenceFeature sf4 = new SequenceFeature("Type", "desc", 22, 33, + 12.5f, "group"); + assertFalse(sf3.equals(sf4)); // changing description breaks equals: - restores = sf2.getDescription(); + String restores = sf2.getDescription(); sf2.setDescription("Desc"); assertFalse(sf1.equals(sf2)); sf2.setDescription(restores); // changing score breaks equals: float restoref = sf2.getScore(); - sf2.setScore(12.4f); + sf2 = new SequenceFeature(sf2, sf2.getBegin(), sf2.getEnd(), + sf2.getFeatureGroup(), 10f); assertFalse(sf1.equals(sf2)); - sf2.setScore(restoref); + sf2 = new SequenceFeature(sf2, sf2.getBegin(), sf2.getEnd(), + sf2.getFeatureGroup(), restoref); // NaN doesn't match a number restoref = sf2.getScore(); - sf2.setScore(Float.NaN); + sf2 = new SequenceFeature(sf2, sf2.getBegin(), sf2.getEnd(), + sf2.getFeatureGroup(), Float.NaN); assertFalse(sf1.equals(sf2)); // NaN matches NaN - sf1.setScore(Float.NaN); + sf1 = new SequenceFeature(sf1, sf1.getBegin(), sf1.getEnd(), + sf1.getFeatureGroup(), Float.NaN); assertTrue(sf1.equals(sf2)); - sf1.setScore(restoref); - sf2.setScore(restoref); + sf1 = new SequenceFeature(sf1, sf1.getBegin(), sf1.getEnd(), + sf1.getFeatureGroup(), restoref); + sf2 = new SequenceFeature(sf2, sf2.getBegin(), sf2.getEnd(), + sf2.getFeatureGroup(), restoref); // changing start position breaks equals: int restorei = sf2.getBegin(); - sf2.setBegin(21); + sf2 = new SequenceFeature(sf2, 21, sf2.getEnd(), sf2.getFeatureGroup(), sf2.getScore()); assertFalse(sf1.equals(sf2)); - sf2.setBegin(restorei); + sf2 = new SequenceFeature(sf2, restorei, sf2.getEnd(), + sf2.getFeatureGroup(), sf2.getScore()); // changing end position breaks equals: restorei = sf2.getEnd(); - sf2.setEnd(32); + sf2 = new SequenceFeature(sf2, sf2.getBegin(), 32, + sf2.getFeatureGroup(), sf2.getScore()); assertFalse(sf1.equals(sf2)); - sf2.setEnd(restorei); + sf2 = new SequenceFeature(sf2, sf2.getBegin(), restorei, + sf2.getFeatureGroup(), sf2.getScore()); // changing feature group breaks equals: restores = sf2.getFeatureGroup(); - sf2.setFeatureGroup("Group"); + sf2 = new SequenceFeature(sf2, sf2.getBegin(), sf2.getEnd(), "Group", sf2.getScore()); assertFalse(sf1.equals(sf2)); - sf2.setFeatureGroup(restores); + sf2 = new SequenceFeature(sf2, sf2.getBegin(), sf2.getEnd(), restores, sf2.getScore()); // changing ID breaks equals: restores = (String) sf2.getValue("ID"); @@ -215,17 +256,21 @@ public class SequenceFeatureTest SequenceFeature sf = new SequenceFeature("type", "desc", 22, 33, 12.5f, "group"); assertFalse(sf.isContactFeature()); - sf.setType(""); + sf = new SequenceFeature("", "desc", 22, 33, 12.5f, "group"); assertFalse(sf.isContactFeature()); - sf.setType(null); + sf = new SequenceFeature(null, "desc", 22, 33, 12.5f, "group"); assertFalse(sf.isContactFeature()); - sf.setType("Disulfide Bond"); + sf = new SequenceFeature("Disulfide Bond", "desc", 22, 33, 12.5f, + "group"); assertTrue(sf.isContactFeature()); - sf.setType("disulfide bond"); + sf = new SequenceFeature("disulfide bond", "desc", 22, 33, 12.5f, + "group"); assertTrue(sf.isContactFeature()); - sf.setType("Disulphide Bond"); + sf = new SequenceFeature("Disulphide Bond", "desc", 22, 33, 12.5f, + "group"); assertTrue(sf.isContactFeature()); - sf.setType("disulphide bond"); + sf = new SequenceFeature("disulphide bond", "desc", 22, 33, 12.5f, + "group"); assertTrue(sf.isContactFeature()); } } diff --git a/test/jalview/datamodel/SequenceTest.java b/test/jalview/datamodel/SequenceTest.java index a52f0a2..2f824ca 100644 --- a/test/jalview/datamodel/SequenceTest.java +++ b/test/jalview/datamodel/SequenceTest.java @@ -23,11 +23,13 @@ package jalview.datamodel; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertFalse; import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.AssertJUnit.assertNotSame; import static org.testng.AssertJUnit.assertNull; import static org.testng.AssertJUnit.assertSame; import static org.testng.AssertJUnit.assertTrue; -import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals; +import jalview.commands.EditCommand; +import jalview.commands.EditCommand.Action; import jalview.datamodel.PDBEntry.Type; import jalview.gui.JvOptionPane; import jalview.util.MapList; @@ -39,6 +41,8 @@ import java.util.BitSet; import java.util.List; import java.util.Vector; +import junit.extensions.PA; + import org.testng.Assert; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; @@ -102,15 +106,6 @@ public class SequenceTest // change sequence, should trigger an update of cached result sq.setSequence("ASDFASDFADSF"); assertTrue(sq.isProtein()); - /* - * in situ change of sequence doesn't change hashcode :-O - * (sequence should not expose internal implementation) - */ - for (int i = 0; i < sq.getSequence().length; i++) - { - sq.getSequence()[i] = "acgtu".charAt(i % 5); - } - assertTrue(sq.isProtein()); // but it isn't } @Test(groups = { "Functional" }) @@ -235,82 +230,353 @@ public class SequenceTest @Test(groups = { "Functional" }) public void testFindIndex() { + /* + * call sequenceChanged() after each test to invalidate any cursor, + * forcing the 1-arg findIndex to be executed + */ SequenceI sq = new Sequence("test", "ABCDEF"); assertEquals(0, sq.findIndex(0)); + sq.sequenceChanged(); assertEquals(1, sq.findIndex(1)); + sq.sequenceChanged(); assertEquals(5, sq.findIndex(5)); + sq.sequenceChanged(); assertEquals(6, sq.findIndex(6)); + sq.sequenceChanged(); assertEquals(6, sq.findIndex(9)); - sq = new Sequence("test", "-A--B-C-D-E-F--"); - assertEquals(2, sq.findIndex(1)); - assertEquals(5, sq.findIndex(2)); - assertEquals(7, sq.findIndex(3)); + sq = new Sequence("test/8-13", "-A--B-C-D-E-F--"); + assertEquals(2, sq.findIndex(8)); + sq.sequenceChanged(); + assertEquals(5, sq.findIndex(9)); + sq.sequenceChanged(); + assertEquals(7, sq.findIndex(10)); // before start returns 0 + sq.sequenceChanged(); assertEquals(0, sq.findIndex(0)); + sq.sequenceChanged(); assertEquals(0, sq.findIndex(-1)); // beyond end returns last residue column + sq.sequenceChanged(); assertEquals(13, sq.findIndex(99)); - } /** - * Tests for the method that returns a dataset sequence position (base 1) for + * Tests for the method that returns a dataset sequence position (start..) for * an aligned column position (base 0). */ @Test(groups = { "Functional" }) public void testFindPosition() { - SequenceI sq = new Sequence("test", "ABCDEF"); - assertEquals(1, sq.findPosition(0)); - assertEquals(6, sq.findPosition(5)); + /* + * call sequenceChanged() after each test to invalidate any cursor, + * forcing the 1-arg findPosition to be executed + */ + SequenceI sq = new Sequence("test/8-13", "ABCDEF"); + assertEquals(8, sq.findPosition(0)); + // Sequence should now hold a cursor at [8, 0] + assertEquals("test:Pos8:Col1:startCol1:endCol0:tok0", + PA.getValue(sq, "cursor").toString()); + SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + int token = (int) PA.getValue(sq, "changeCount"); + assertEquals(new SequenceCursor(sq, 8, 1, token), cursor); + + sq.sequenceChanged(); + + /* + * find F13 at column offset 5, cursor should update to [13, 6] + * endColumn is found and saved in cursor + */ + assertEquals(13, sq.findPosition(5)); + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(++token, (int) PA.getValue(sq, "changeCount")); + assertEquals(new SequenceCursor(sq, 13, 6, token), cursor); + assertEquals("test:Pos13:Col6:startCol1:endCol6:tok1", + PA.getValue(sq, "cursor").toString()); + // assertEquals(-1, seq.findPosition(6)); // fails - sq = new Sequence("test", "AB-C-D--"); - assertEquals(1, sq.findPosition(0)); - assertEquals(2, sq.findPosition(1)); + sq = new Sequence("test/8-11", "AB-C-D--"); + token = (int) PA.getValue(sq, "changeCount"); // 0 + assertEquals(8, sq.findPosition(0)); + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 8, 1, token), cursor); + assertEquals("test:Pos8:Col1:startCol1:endCol0:tok0", + PA.getValue(sq, "cursor").toString()); + + sq.sequenceChanged(); + assertEquals(9, sq.findPosition(1)); + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 9, 2, ++token), cursor); + assertEquals("test:Pos9:Col2:startCol1:endCol0:tok1", + PA.getValue(sq, "cursor").toString()); + + sq.sequenceChanged(); // gap position 'finds' residue to the right (not the left as per javadoc) - assertEquals(3, sq.findPosition(2)); - assertEquals(3, sq.findPosition(3)); - assertEquals(4, sq.findPosition(4)); - assertEquals(4, sq.findPosition(5)); + // cursor is set to the last residue position found [B 2] + assertEquals(10, sq.findPosition(2)); + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 9, 2, ++token), cursor); + assertEquals("test:Pos9:Col2:startCol1:endCol0:tok2", + PA.getValue(sq, "cursor").toString()); + + sq.sequenceChanged(); + assertEquals(10, sq.findPosition(3)); + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 10, 4, ++token), cursor); + assertEquals("test:Pos10:Col4:startCol1:endCol0:tok3", + PA.getValue(sq, "cursor").toString()); + + sq.sequenceChanged(); + // column[4] is the gap after C - returns D11 + // cursor is set to [C 4] + assertEquals(11, sq.findPosition(4)); + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 10, 4, ++token), cursor); + assertEquals("test:Pos10:Col4:startCol1:endCol0:tok4", + PA.getValue(sq, "cursor").toString()); + + sq.sequenceChanged(); + assertEquals(11, sq.findPosition(5)); // D + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 11, 6, ++token), cursor); + // lastCol has been found and saved in the cursor + assertEquals("test:Pos11:Col6:startCol1:endCol6:tok5", + PA.getValue(sq, "cursor").toString()); + + sq.sequenceChanged(); // returns 1 more than sequence length if off the end ?!? - assertEquals(5, sq.findPosition(6)); - assertEquals(5, sq.findPosition(7)); + assertEquals(12, sq.findPosition(6)); - sq = new Sequence("test", "--AB-C-DEF--"); - assertEquals(1, sq.findPosition(0)); - assertEquals(1, sq.findPosition(1)); - assertEquals(1, sq.findPosition(2)); - assertEquals(2, sq.findPosition(3)); - assertEquals(3, sq.findPosition(4)); - assertEquals(3, sq.findPosition(5)); - assertEquals(4, sq.findPosition(6)); - assertEquals(4, sq.findPosition(7)); - assertEquals(5, sq.findPosition(8)); - assertEquals(6, sq.findPosition(9)); - assertEquals(7, sq.findPosition(10)); - assertEquals(7, sq.findPosition(11)); + sq.sequenceChanged(); + assertEquals(12, sq.findPosition(7)); + + /* + * first findPosition should also set firstResCol in cursor + */ + sq = new Sequence("test/8-13", "--AB-C-DEF--"); + assertEquals(8, sq.findPosition(0)); + assertNull(PA.getValue(sq, "cursor")); + + sq.sequenceChanged(); + assertEquals(8, sq.findPosition(1)); + assertNull(PA.getValue(sq, "cursor")); + + sq.sequenceChanged(); + assertEquals(8, sq.findPosition(2)); + assertEquals("test:Pos8:Col3:startCol3:endCol0:tok2", + PA.getValue(sq, "cursor").toString()); + + sq.sequenceChanged(); + assertEquals(9, sq.findPosition(3)); + assertEquals("test:Pos9:Col4:startCol3:endCol0:tok3", + PA.getValue(sq, "cursor").toString()); + + sq.sequenceChanged(); + // column[4] is a gap, returns next residue pos (C10) + // cursor is set to last residue found [B] + assertEquals(10, sq.findPosition(4)); + assertEquals("test:Pos9:Col4:startCol3:endCol0:tok4", + PA.getValue(sq, "cursor").toString()); + + sq.sequenceChanged(); + assertEquals(10, sq.findPosition(5)); + assertEquals("test:Pos10:Col6:startCol3:endCol0:tok5", + PA.getValue(sq, "cursor").toString()); + + sq.sequenceChanged(); + // column[6] is a gap, returns next residue pos (D11) + // cursor is set to last residue found [C] + assertEquals(11, sq.findPosition(6)); + assertEquals("test:Pos10:Col6:startCol3:endCol0:tok6", + PA.getValue(sq, "cursor").toString()); + + sq.sequenceChanged(); + assertEquals(11, sq.findPosition(7)); + assertEquals("test:Pos11:Col8:startCol3:endCol0:tok7", + PA.getValue(sq, "cursor").toString()); + + sq.sequenceChanged(); + assertEquals(12, sq.findPosition(8)); + assertEquals("test:Pos12:Col9:startCol3:endCol0:tok8", + PA.getValue(sq, "cursor").toString()); + + /* + * when the last residue column is found, it is set in the cursor + */ + sq.sequenceChanged(); + assertEquals(13, sq.findPosition(9)); + assertEquals("test:Pos13:Col10:startCol3:endCol10:tok9", + PA.getValue(sq, "cursor").toString()); + + sq.sequenceChanged(); + assertEquals(14, sq.findPosition(10)); + assertEquals("test:Pos13:Col10:startCol3:endCol10:tok10", + PA.getValue(sq, "cursor").toString()); + + /* + * findPosition for column beyond sequence length + * returns 1 more than last residue position + */ + sq.sequenceChanged(); + assertEquals(14, sq.findPosition(11)); + assertEquals("test:Pos13:Col10:startCol3:endCol10:tok11", + PA.getValue(sq, "cursor").toString()); + + sq.sequenceChanged(); + assertEquals(14, sq.findPosition(99)); + assertEquals("test:Pos13:Col10:startCol3:endCol10:tok12", + PA.getValue(sq, "cursor").toString()); + + /* + * gapped sequence ending in non-gap + */ + sq = new Sequence("test/8-13", "--AB-C-DEF"); + assertEquals(13, sq.findPosition(9)); + assertEquals("test:Pos13:Col10:startCol3:endCol10:tok0", + PA.getValue(sq, "cursor").toString()); + sq.sequenceChanged(); + assertEquals(12, sq.findPosition(8)); + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + // sequenceChanged() invalidates cursor.lastResidueColumn + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals("test:Pos12:Col9:startCol3:endCol0:tok1", + cursor.toString()); + // findPosition with cursor accepts base 1 column values + assertEquals(13, ((Sequence) sq).findPosition(10, cursor)); + assertEquals(13, sq.findPosition(9)); // F13 + // lastResidueColumn has now been found and saved in cursor + assertEquals("test:Pos13:Col10:startCol3:endCol10:tok1", + PA.getValue(sq, "cursor").toString()); } @Test(groups = { "Functional" }) public void testDeleteChars() { + /* + * internal delete + */ + SequenceI sq = new Sequence("test", "ABCDEF"); + assertNull(PA.getValue(sq, "datasetSequence")); + assertEquals(1, sq.getStart()); + assertEquals(6, sq.getEnd()); + sq.deleteChars(2, 3); + assertEquals("ABDEF", sq.getSequenceAsString()); + assertEquals(1, sq.getStart()); + assertEquals(5, sq.getEnd()); + assertNull(PA.getValue(sq, "datasetSequence")); + + /* + * delete at start + */ + sq = new Sequence("test", "ABCDEF"); + sq.deleteChars(0, 2); + assertEquals("CDEF", sq.getSequenceAsString()); + assertEquals(3, sq.getStart()); + assertEquals(6, sq.getEnd()); + assertNull(PA.getValue(sq, "datasetSequence")); + + /* + * delete at end + */ + sq = new Sequence("test", "ABCDEF"); + sq.deleteChars(4, 6); + assertEquals("ABCD", sq.getSequenceAsString()); + assertEquals(1, sq.getStart()); + assertEquals(4, sq.getEnd()); + assertNull(PA.getValue(sq, "datasetSequence")); + } + + @Test(groups = { "Functional" }) + public void testDeleteChars_withDbRefsAndFeatures() + { + /* + * internal delete - new dataset sequence created + * gets a copy of any dbrefs + */ SequenceI sq = new Sequence("test", "ABCDEF"); + sq.createDatasetSequence(); + DBRefEntry dbr1 = new DBRefEntry("Uniprot", "0", "a123"); + sq.addDBRef(dbr1); + Object ds = PA.getValue(sq, "datasetSequence"); + assertNotNull(ds); assertEquals(1, sq.getStart()); assertEquals(6, sq.getEnd()); sq.deleteChars(2, 3); assertEquals("ABDEF", sq.getSequenceAsString()); assertEquals(1, sq.getStart()); assertEquals(5, sq.getEnd()); + Object newDs = PA.getValue(sq, "datasetSequence"); + assertNotNull(newDs); + assertNotSame(ds, newDs); + assertNotNull(sq.getDBRefs()); + assertEquals(1, sq.getDBRefs().length); + assertNotSame(dbr1, sq.getDBRefs()[0]); + assertEquals(dbr1, sq.getDBRefs()[0]); + /* + * internal delete with sequence features + * (failure case for JAL-2541) + */ sq = new Sequence("test", "ABCDEF"); + sq.createDatasetSequence(); + SequenceFeature sf1 = new SequenceFeature("Cath", "desc", 2, 4, 2f, + "CathGroup"); + sq.addSequenceFeature(sf1); + ds = PA.getValue(sq, "datasetSequence"); + assertNotNull(ds); + assertEquals(1, sq.getStart()); + assertEquals(6, sq.getEnd()); + sq.deleteChars(2, 4); + assertEquals("ABEF", sq.getSequenceAsString()); + assertEquals(1, sq.getStart()); + assertEquals(4, sq.getEnd()); + newDs = PA.getValue(sq, "datasetSequence"); + assertNotNull(newDs); + assertNotSame(ds, newDs); + List sfs = sq.getSequenceFeatures(); + assertEquals(1, sfs.size()); + assertNotSame(sf1, sfs.get(0)); + assertEquals(sf1, sfs.get(0)); + + /* + * delete at start - no new dataset sequence created + * any sequence features remain as before + */ + sq = new Sequence("test", "ABCDEF"); + sq.createDatasetSequence(); + ds = PA.getValue(sq, "datasetSequence"); + sf1 = new SequenceFeature("Cath", "desc", 2, 4, 2f, "CathGroup"); + sq.addSequenceFeature(sf1); sq.deleteChars(0, 2); assertEquals("CDEF", sq.getSequenceAsString()); assertEquals(3, sq.getStart()); assertEquals(6, sq.getEnd()); + assertSame(ds, PA.getValue(sq, "datasetSequence")); + sfs = sq.getSequenceFeatures(); + assertNotNull(sfs); + assertEquals(1, sfs.size()); + assertSame(sf1, sfs.get(0)); + + /* + * delete at end - no new dataset sequence created + * any dbrefs remain as before + */ + sq = new Sequence("test", "ABCDEF"); + sq.createDatasetSequence(); + ds = PA.getValue(sq, "datasetSequence"); + dbr1 = new DBRefEntry("Uniprot", "0", "a123"); + sq.addDBRef(dbr1); + sq.deleteChars(4, 6); + assertEquals("ABCD", sq.getSequenceAsString()); + assertEquals(1, sq.getStart()); + assertEquals(4, sq.getEnd()); + assertSame(ds, PA.getValue(sq, "datasetSequence")); + assertNotNull(sq.getDBRefs()); + assertEquals(1, sq.getDBRefs().length); + assertSame(dbr1, sq.getDBRefs()[0]); } @Test(groups = { "Functional" }) @@ -348,16 +614,16 @@ public class SequenceTest SequenceI sq = new Sequence("test", "GATCAT"); sq.createDatasetSequence(); - assertNull(sq.getSequenceFeatures()); + assertTrue(sq.getSequenceFeatures().isEmpty()); /* * SequenceFeature on sequence */ - SequenceFeature sf = new SequenceFeature(); + SequenceFeature sf = new SequenceFeature("Cath", "desc", 2, 4, 2f, null); sq.addSequenceFeature(sf); - SequenceFeature[] sfs = sq.getSequenceFeatures(); - assertEquals(1, sfs.length); - assertSame(sf, sfs[0]); + List sfs = sq.getSequenceFeatures(); + assertEquals(1, sfs.size()); + assertSame(sf, sfs.get(0)); /* * SequenceFeature on sequence and dataset sequence; returns that on @@ -366,18 +632,19 @@ public class SequenceTest * Note JAL-2046: spurious: we have no use case for this at the moment. * This test also buggy - as sf2.equals(sf), no new feature is added */ - SequenceFeature sf2 = new SequenceFeature(); + SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 2, 4, 2f, + null); sq.getDatasetSequence().addSequenceFeature(sf2); sfs = sq.getSequenceFeatures(); - assertEquals(1, sfs.length); - assertSame(sf, sfs[0]); + assertEquals(1, sfs.size()); + assertSame(sf, sfs.get(0)); /* * SequenceFeature on dataset sequence only * Note JAL-2046: spurious: we have no use case for setting a non-dataset sequence's feature array to null at the moment. */ sq.setSequenceFeatures(null); - assertNull(sq.getDatasetSequence().getSequenceFeatures()); + assertTrue(sq.getDatasetSequence().getSequenceFeatures().isEmpty()); /* * Corrupt case - no SequenceFeature, dataset's dataset is the original @@ -398,7 +665,7 @@ public class SequenceTest assertTrue(e.getMessage().toLowerCase() .contains("implementation error")); } - assertNull(sq.getSequenceFeatures()); + assertTrue(sq.getSequenceFeatures().isEmpty()); } /** @@ -448,11 +715,23 @@ public class SequenceTest public void testCreateDatasetSequence() { SequenceI sq = new Sequence("my", "ASDASD"); + sq.addSequenceFeature(new SequenceFeature("type", "desc", 1, 10, 1f, + "group")); + sq.addDBRef(new DBRefEntry("source", "version", "accession")); assertNull(sq.getDatasetSequence()); + assertNotNull(PA.getValue(sq, "sequenceFeatureStore")); + assertNotNull(PA.getValue(sq, "dbrefs")); + SequenceI rds = sq.createDatasetSequence(); assertNotNull(rds); assertNull(rds.getDatasetSequence()); - assertEquals(sq.getDatasetSequence(), rds); + assertSame(sq.getDatasetSequence(), rds); + + // sequence features and dbrefs transferred to dataset sequence + assertNull(PA.getValue(sq, "sequenceFeatureStore")); + assertNull(PA.getValue(sq, "dbrefs")); + assertNotNull(PA.getValue(rds, "sequenceFeatureStore")); + assertNotNull(PA.getValue(rds, "dbrefs")); } /** @@ -559,12 +838,9 @@ public class SequenceTest assertEquals("CD", derived.getSequenceAsString()); assertSame(sq.getDatasetSequence(), derived.getDatasetSequence()); - assertNull(sq.sequenceFeatures); - assertNull(derived.sequenceFeatures); // derived sequence should access dataset sequence features assertNotNull(sq.getSequenceFeatures()); - assertArrayEquals(sq.getSequenceFeatures(), - derived.getSequenceFeatures()); + assertEquals(sq.getSequenceFeatures(), derived.getSequenceFeatures()); /* * verify we have primary db refs *just* for PDB IDs with associated @@ -694,18 +970,18 @@ public class SequenceTest assertEquals(anns[0].score, seq1.getAnnotation()[0].score); // copy has a copy of the sequence feature: - SequenceFeature[] sfs = copy.getSequenceFeatures(); - assertEquals(1, sfs.length); + List sfs = copy.getSequenceFeatures(); + assertEquals(1, sfs.size()); if (seq1.getDatasetSequence() != null && copy.getDatasetSequence() == seq1.getDatasetSequence()) { - assertTrue(sfs[0] == seq1.getSequenceFeatures()[0]); + assertSame(sfs.get(0), seq1.getSequenceFeatures().get(0)); } else { - assertFalse(sfs[0] == seq1.getSequenceFeatures()[0]); + assertNotSame(sfs.get(0), seq1.getSequenceFeatures().get(0)); } - assertTrue(sfs[0].equals(seq1.getSequenceFeatures()[0])); + assertEquals(sfs.get(0), seq1.getSequenceFeatures().get(0)); // copy has a copy of the PDB entry Vector pdbs = copy.getAllPDBEntries(); @@ -724,6 +1000,36 @@ public class SequenceTest assertEquals(' ', sq.getCharAt(-1)); } + @Test(groups = { "Functional" }) + public void testAddSequenceFeatures() + { + SequenceI sq = new Sequence("", "abcde"); + // type may not be null + assertFalse(sq.addSequenceFeature(new SequenceFeature(null, "desc", 4, + 8, 0f, null))); + assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", 4, + 8, 0f, null))); + // can't add a duplicate feature + assertFalse(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", + 4, 8, 0f, null))); + // can add a different feature + assertTrue(sq.addSequenceFeature(new SequenceFeature("Scop", "desc", 4, + 8, 0f, null))); // different type + assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", + "description", 4, 8, 0f, null)));// different description + assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", 3, + 8, 0f, null))); // different start position + assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", 4, + 9, 0f, null))); // different end position + assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", 4, + 8, 1f, null))); // different score + assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", 4, + 8, Float.NaN, null))); // score NaN + assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", 4, + 8, 0f, "Metal"))); // different group + assertEquals(8, sq.getFeatures().getAllFeatures().size()); + } + /** * Tests for adding (or updating) dbrefs * @@ -1023,4 +1329,338 @@ public class SequenceTest seq2.createDatasetSequence(); seq.setDatasetSequence(seq2); } + + @Test(groups = { "Functional" }) + public void testFindFeatures() + { + SequenceI sq = new Sequence("test/8-16", "-ABC--DEF--GHI--"); + sq.createDatasetSequence(); + + assertTrue(sq.findFeatures(1, 99).isEmpty()); + + // add non-positional feature + SequenceFeature sf0 = new SequenceFeature("Cath", "desc", 0, 0, 2f, + null); + sq.addSequenceFeature(sf0); + // add feature on BCD + SequenceFeature sfBCD = new SequenceFeature("Cath", "desc", 9, 11, 2f, + null); + sq.addSequenceFeature(sfBCD); + // add feature on DE + SequenceFeature sfDE = new SequenceFeature("Cath", "desc", 11, 12, 2f, + null); + sq.addSequenceFeature(sfDE); + // add contact feature at [B, H] + SequenceFeature sfContactBH = new SequenceFeature("Disulphide bond", + "desc", 9, 15, 2f, null); + sq.addSequenceFeature(sfContactBH); + // add contact feature at [F, G] + SequenceFeature sfContactFG = new SequenceFeature("Disulfide Bond", + "desc", 13, 14, 2f, null); + sq.addSequenceFeature(sfContactFG); + + // no features in columns 1-2 (-A) + List found = sq.findFeatures(1, 2); + assertTrue(found.isEmpty()); + + // columns 1-6 (-ABC--) includes BCD and B/H feature but not DE + found = sq.findFeatures(1, 6); + assertEquals(2, found.size()); + assertTrue(found.contains(sfBCD)); + assertTrue(found.contains(sfContactBH)); + + // columns 5-6 (--) includes (enclosing) BCD but not (contact) B/H feature + found = sq.findFeatures(5, 6); + assertEquals(1, found.size()); + assertTrue(found.contains(sfBCD)); + + // columns 7-10 (DEF-) includes BCD, DE, F/G but not B/H feature + found = sq.findFeatures(7, 10); + assertEquals(3, found.size()); + assertTrue(found.contains(sfBCD)); + assertTrue(found.contains(sfDE)); + assertTrue(found.contains(sfContactFG)); + + // columns 10-11 (--) should find nothing + found = sq.findFeatures(10, 11); + assertEquals(0, found.size()); + } + + @Test(groups = { "Functional" }) + public void testFindIndex_withCursor() + { + Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--"); + + // find F given A + assertEquals(10, sq.findIndex(13, new SequenceCursor(sq, 8, 2, 0))); + + // find A given F + assertEquals(2, sq.findIndex(8, new SequenceCursor(sq, 13, 10, 0))); + + // find C given C + assertEquals(6, sq.findIndex(10, new SequenceCursor(sq, 10, 6, 0))); + } + + @Test(groups = { "Functional" }) + public void testFindPosition_withCursor() + { + Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--"); + + // find F pos given A - lastCol gets set in cursor + assertEquals(13, sq.findPosition(10, new SequenceCursor(sq, 8, 2, 0))); + assertEquals("test:Pos13:Col10:startCol0:endCol10:tok0", + PA.getValue(sq, "cursor").toString()); + + // find A pos given F - first residue column is saved in cursor + assertEquals(8, sq.findPosition(2, new SequenceCursor(sq, 13, 10, 0))); + assertEquals("test:Pos8:Col2:startCol2:endCol10:tok0", + PA.getValue(sq, "cursor").toString()); + + // find C pos given C (neither startCol nor endCol is set) + assertEquals(10, sq.findPosition(6, new SequenceCursor(sq, 10, 6, 0))); + assertEquals("test:Pos10:Col6:startCol0:endCol0:tok0", + PA.getValue(sq, "cursor").toString()); + + // now the grey area - what residue position for a gapped column? JAL-2562 + + // find 'residue' for column 3 given cursor for D (so working left) + // returns B9; cursor is updated to [B 5] + assertEquals(9, sq.findPosition(3, new SequenceCursor(sq, 11, 7, 0))); + assertEquals("test:Pos9:Col5:startCol0:endCol0:tok0", + PA.getValue(sq, "cursor").toString()); + + // find 'residue' for column 8 given cursor for D (so working right) + // returns E12; cursor is updated to [D 7] + assertEquals(12, sq.findPosition(8, new SequenceCursor(sq, 11, 7, 0))); + assertEquals("test:Pos11:Col7:startCol0:endCol0:tok0", + PA.getValue(sq, "cursor").toString()); + + // find 'residue' for column 12 given cursor for B + // returns 1 more than last residue position; cursor is updated to [F 10] + // lastCol position is saved in cursor + assertEquals(14, sq.findPosition(12, new SequenceCursor(sq, 9, 5, 0))); + assertEquals("test:Pos13:Col10:startCol0:endCol10:tok0", + PA.getValue(sq, "cursor").toString()); + + /* + * findPosition for column beyond length of sequence + * returns 1 more than the last residue position + * cursor is set to last real residue position [F 10] + */ + assertEquals(14, sq.findPosition(99, new SequenceCursor(sq, 8, 2, 0))); + assertEquals("test:Pos13:Col10:startCol0:endCol10:tok0", + PA.getValue(sq, "cursor").toString()); + + /* + * and the case without a trailing gap + */ + sq = new Sequence("test/8-13", "-A--BCD-EF"); + // first find C from A + assertEquals(10, sq.findPosition(6, new SequenceCursor(sq, 8, 2, 0))); + SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals("test:Pos10:Col6:startCol0:endCol0:tok0", + cursor.toString()); + // now 'find' 99 from C + // cursor is set to [F 10] and saved lastCol + assertEquals(14, sq.findPosition(99, cursor)); + assertEquals("test:Pos13:Col10:startCol0:endCol10:tok0", + PA.getValue(sq, "cursor").toString()); + } + + @Test + public void testIsValidCursor() + { + Sequence sq = new Sequence("Seq", "ABC--DE-F", 8, 13); + assertFalse(sq.isValidCursor(null)); + + /* + * cursor is valid if it has valid sequence ref and changeCount token + * and positions within the range of the sequence + */ + int changeCount = (int) PA.getValue(sq, "changeCount"); + SequenceCursor cursor = new SequenceCursor(sq, 13, 1, changeCount); + assertTrue(sq.isValidCursor(cursor)); + + /* + * column position outside [0 - length] is rejected + */ + cursor = new SequenceCursor(sq, 13, -1, changeCount); + assertFalse(sq.isValidCursor(cursor)); + cursor = new SequenceCursor(sq, 13, 10, changeCount); + assertFalse(sq.isValidCursor(cursor)); + cursor = new SequenceCursor(sq, 7, 8, changeCount); + assertFalse(sq.isValidCursor(cursor)); + cursor = new SequenceCursor(sq, 14, 2, changeCount); + assertFalse(sq.isValidCursor(cursor)); + + /* + * wrong sequence is rejected + */ + cursor = new SequenceCursor(null, 13, 1, changeCount); + assertFalse(sq.isValidCursor(cursor)); + cursor = new SequenceCursor(new Sequence("Seq", "abc"), 13, 1, + changeCount); + assertFalse(sq.isValidCursor(cursor)); + + /* + * wrong token value is rejected + */ + cursor = new SequenceCursor(sq, 13, 1, changeCount + 1); + assertFalse(sq.isValidCursor(cursor)); + cursor = new SequenceCursor(sq, 13, 1, changeCount - 1); + assertFalse(sq.isValidCursor(cursor)); + } + + @Test(groups = { "Functional" }) + public void testFindPosition_withCursorAndEdits() + { + Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--"); + + // find F pos given A + assertEquals(13, sq.findPosition(10, new SequenceCursor(sq, 8, 2, 0))); + int token = (int) PA.getValue(sq, "changeCount"); // 0 + SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 13, 10, token), cursor); + + /* + * setSequence should invalidate the cursor cached by the sequence + */ + sq.setSequence("-A-BCD-EF---"); // one gap removed + assertEquals(8, sq.getStart()); // sanity check + assertEquals(11, sq.findPosition(5)); // D11 + // cursor should now be at [D 6] + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 11, 6, ++token), cursor); + + /* + * deleteChars should invalidate the cached cursor + */ + sq.deleteChars(2, 5); // delete -BC + assertEquals("-AD-EF---", sq.getSequenceAsString()); + assertEquals(8, sq.getStart()); // sanity check + assertEquals(10, sq.findPosition(4)); // E10 + // cursor should now be at [E 5] + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 10, 5, ++token), cursor); + + /* + * Edit to insert gaps should invalidate the cached cursor + * insert 2 gaps at column[3] to make -AD---EF--- + */ + SequenceI[] seqs = new SequenceI[] { sq }; + AlignmentI al = new Alignment(seqs); + new EditCommand().appendEdit(Action.INSERT_GAP, seqs, 3, 2, al, true); + assertEquals("-AD---EF---", sq.getSequenceAsString()); + assertEquals(10, sq.findPosition(4)); // E10 + // cursor should now be at [D 3] + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 9, 3, ++token), cursor); + + /* + * insertCharAt should invalidate the cached cursor + * insert CC at column[4] to make -AD-CC--EF--- + */ + sq.insertCharAt(4, 2, 'C'); + assertEquals("-AD-CC--EF---", sq.getSequenceAsString()); + assertEquals(13, sq.findPosition(9)); // F13 + // cursor should now be at [F 10] + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 13, 10, ++token), cursor); + } + + @Test(groups = { "Functional" }) + public void testGetSequence() + { + String seqstring = "-A--BCD-EF--"; + Sequence sq = new Sequence("test/8-13", seqstring); + sq.createDatasetSequence(); + assertTrue(Arrays.equals(sq.getSequence(), seqstring.toCharArray())); + assertTrue(Arrays.equals(sq.getDatasetSequence().getSequence(), + "ABCDEF".toCharArray())); + + // verify a copy of the sequence array is returned + char[] theSeq = (char[]) PA.getValue(sq, "sequence"); + assertNotSame(theSeq, sq.getSequence()); + theSeq = (char[]) PA.getValue(sq.getDatasetSequence(), "sequence"); + assertNotSame(theSeq, sq.getDatasetSequence().getSequence()); + } + + @Test(groups = { "Functional" }) + public void testReplace() + { + String seqstring = "-A--BCD-EF--"; + SequenceI sq = new Sequence("test/8-13", seqstring); + assertEquals(0, PA.getValue(sq, "changeCount")); + + assertEquals(0, sq.replace('A', 'A')); // same char + assertEquals(seqstring, sq.getSequenceAsString()); + assertEquals(0, PA.getValue(sq, "changeCount")); + + assertEquals(0, sq.replace('X', 'Y')); // not there + assertEquals(seqstring, sq.getSequenceAsString()); + assertEquals(0, PA.getValue(sq, "changeCount")); + + assertEquals(1, sq.replace('A', 'K')); + assertEquals("-K--BCD-EF--", sq.getSequenceAsString()); + assertEquals(1, PA.getValue(sq, "changeCount")); + + assertEquals(6, sq.replace('-', '.')); + assertEquals(".K..BCD.EF..", sq.getSequenceAsString()); + assertEquals(2, PA.getValue(sq, "changeCount")); + } + + @Test(groups = { "Functional" }) + public void testFindPositions() + { + SequenceI sq = new Sequence("test/8-13", "-ABC---DE-F--"); + + /* + * invalid inputs + */ + assertNull(sq.findPositions(6, 5)); + assertNull(sq.findPositions(0, 5)); + assertNull(sq.findPositions(-1, 5)); + + /* + * all gapped ranges + */ + assertNull(sq.findPositions(1, 1)); // 1-based columns + assertNull(sq.findPositions(5, 5)); + assertNull(sq.findPositions(5, 6)); + assertNull(sq.findPositions(5, 7)); + + /* + * all ungapped ranges + */ + assertEquals(new Range(8, 8), sq.findPositions(2, 2)); // A + assertEquals(new Range(8, 9), sq.findPositions(2, 3)); // AB + assertEquals(new Range(8, 10), sq.findPositions(2, 4)); // ABC + assertEquals(new Range(9, 10), sq.findPositions(3, 4)); // BC + + /* + * gap to ungapped range + */ + assertEquals(new Range(8, 10), sq.findPositions(1, 4)); // ABC + assertEquals(new Range(11, 12), sq.findPositions(6, 9)); // DE + + /* + * ungapped to gapped range + */ + assertEquals(new Range(10, 10), sq.findPositions(4, 5)); // C + assertEquals(new Range(9, 13), sq.findPositions(3, 11)); // BCDEF + + /* + * ungapped to ungapped enclosing gaps + */ + assertEquals(new Range(10, 11), sq.findPositions(4, 8)); // CD + assertEquals(new Range(8, 13), sq.findPositions(2, 11)); // ABCDEF + + /* + * gapped to gapped enclosing ungapped + */ + assertEquals(new Range(8, 10), sq.findPositions(1, 5)); // ABC + assertEquals(new Range(11, 12), sq.findPositions(5, 10)); // DE + assertEquals(new Range(8, 13), sq.findPositions(1, 13)); // the lot + assertEquals(new Range(8, 13), sq.findPositions(1, 99)); + } } diff --git a/test/jalview/datamodel/features/FeatureStoreTest.java b/test/jalview/datamodel/features/FeatureStoreTest.java new file mode 100644 index 0000000..db21c2f --- /dev/null +++ b/test/jalview/datamodel/features/FeatureStoreTest.java @@ -0,0 +1,911 @@ +package jalview.datamodel.features; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import jalview.datamodel.SequenceFeature; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.testng.annotations.Test; + +public class FeatureStoreTest +{ + + @Test(groups = "Functional") + public void testFindFeatures_nonNested() + { + FeatureStore fs = new FeatureStore(); + fs.addFeature(new SequenceFeature("", "", 10, 20, Float.NaN, + null)); + // same range different description + fs.addFeature(new SequenceFeature("", "desc", 10, 20, Float.NaN, null)); + fs.addFeature(new SequenceFeature("", "", 15, 25, Float.NaN, null)); + fs.addFeature(new SequenceFeature("", "", 20, 35, Float.NaN, null)); + + List overlaps = fs.findOverlappingFeatures(1, 9); + assertTrue(overlaps.isEmpty()); + + overlaps = fs.findOverlappingFeatures(8, 10); + assertEquals(overlaps.size(), 2); + assertEquals(overlaps.get(0).getEnd(), 20); + assertEquals(overlaps.get(1).getEnd(), 20); + + overlaps = fs.findOverlappingFeatures(12, 16); + assertEquals(overlaps.size(), 3); + assertEquals(overlaps.get(0).getEnd(), 20); + assertEquals(overlaps.get(1).getEnd(), 20); + assertEquals(overlaps.get(2).getEnd(), 25); + + overlaps = fs.findOverlappingFeatures(33, 33); + assertEquals(overlaps.size(), 1); + assertEquals(overlaps.get(0).getEnd(), 35); + } + + @Test(groups = "Functional") + public void testFindFeatures_nested() + { + FeatureStore fs = new FeatureStore(); + SequenceFeature sf1 = addFeature(fs, 10, 50); + SequenceFeature sf2 = addFeature(fs, 10, 40); + SequenceFeature sf3 = addFeature(fs, 20, 30); + // fudge feature at same location but different group (so is added) + SequenceFeature sf4 = new SequenceFeature("", "", 20, 30, Float.NaN, + "different group"); + fs.addFeature(sf4); + SequenceFeature sf5 = addFeature(fs, 35, 36); + + List overlaps = fs.findOverlappingFeatures(1, 9); + assertTrue(overlaps.isEmpty()); + + overlaps = fs.findOverlappingFeatures(10, 15); + assertEquals(overlaps.size(), 2); + assertTrue(overlaps.contains(sf1)); + assertTrue(overlaps.contains(sf2)); + + overlaps = fs.findOverlappingFeatures(45, 60); + assertEquals(overlaps.size(), 1); + assertTrue(overlaps.contains(sf1)); + + overlaps = fs.findOverlappingFeatures(32, 38); + assertEquals(overlaps.size(), 3); + assertTrue(overlaps.contains(sf1)); + assertTrue(overlaps.contains(sf2)); + assertTrue(overlaps.contains(sf5)); + + overlaps = fs.findOverlappingFeatures(15, 25); + assertEquals(overlaps.size(), 4); + assertTrue(overlaps.contains(sf1)); + assertTrue(overlaps.contains(sf2)); + assertTrue(overlaps.contains(sf3)); + assertTrue(overlaps.contains(sf4)); + } + + @Test(groups = "Functional") + public void testFindFeatures_mixed() + { + FeatureStore fs = new FeatureStore(); + SequenceFeature sf1 = addFeature(fs, 10, 50); + SequenceFeature sf2 = addFeature(fs, 1, 15); + SequenceFeature sf3 = addFeature(fs, 20, 30); + SequenceFeature sf4 = addFeature(fs, 40, 100); + SequenceFeature sf5 = addFeature(fs, 60, 100); + SequenceFeature sf6 = addFeature(fs, 70, 70); + + List overlaps = fs.findOverlappingFeatures(200, 200); + assertTrue(overlaps.isEmpty()); + + overlaps = fs.findOverlappingFeatures(1, 9); + assertEquals(overlaps.size(), 1); + assertTrue(overlaps.contains(sf2)); + + overlaps = fs.findOverlappingFeatures(5, 18); + assertEquals(overlaps.size(), 2); + assertTrue(overlaps.contains(sf1)); + assertTrue(overlaps.contains(sf2)); + + overlaps = fs.findOverlappingFeatures(30, 40); + assertEquals(overlaps.size(), 3); + assertTrue(overlaps.contains(sf1)); + assertTrue(overlaps.contains(sf3)); + assertTrue(overlaps.contains(sf4)); + + overlaps = fs.findOverlappingFeatures(80, 90); + assertEquals(overlaps.size(), 2); + assertTrue(overlaps.contains(sf4)); + assertTrue(overlaps.contains(sf5)); + + overlaps = fs.findOverlappingFeatures(68, 70); + assertEquals(overlaps.size(), 3); + assertTrue(overlaps.contains(sf4)); + assertTrue(overlaps.contains(sf5)); + assertTrue(overlaps.contains(sf6)); + } + + /** + * Helper method to add a feature of no particular type + * + * @param fs + * @param from + * @param to + * @return + */ + SequenceFeature addFeature(FeatureStore fs, int from, int to) + { + SequenceFeature sf1 = new SequenceFeature("", "", from, to, Float.NaN, + null); + fs.addFeature(sf1); + return sf1; + } + + @Test(groups = "Functional") + public void testFindFeatures_contactFeatures() + { + FeatureStore fs = new FeatureStore(); + + SequenceFeature sf = new SequenceFeature("disulphide bond", "bond", 10, + 20, Float.NaN, null); + fs.addFeature(sf); + + /* + * neither contact point in range + */ + List overlaps = fs.findOverlappingFeatures(1, 9); + assertTrue(overlaps.isEmpty()); + + /* + * neither contact point in range + */ + overlaps = fs.findOverlappingFeatures(11, 19); + assertTrue(overlaps.isEmpty()); + + /* + * first contact point in range + */ + overlaps = fs.findOverlappingFeatures(5, 15); + assertEquals(overlaps.size(), 1); + assertTrue(overlaps.contains(sf)); + + /* + * second contact point in range + */ + overlaps = fs.findOverlappingFeatures(15, 25); + assertEquals(overlaps.size(), 1); + assertTrue(overlaps.contains(sf)); + + /* + * both contact points in range + */ + overlaps = fs.findOverlappingFeatures(5, 25); + assertEquals(overlaps.size(), 1); + assertTrue(overlaps.contains(sf)); + } + + /** + * Tests for the method that returns false for an attempt to add a feature + * that would enclose, or be enclosed by, another feature + */ + @Test(groups = "Functional") + public void testAddNonNestedFeature() + { + FeatureStore fs = new FeatureStore(); + + String type = "Domain"; + SequenceFeature sf1 = new SequenceFeature(type, type, 10, 20, + Float.NaN, null); + assertTrue(fs.addNonNestedFeature(sf1)); + + // co-located feature is ok + SequenceFeature sf2 = new SequenceFeature(type, type, 10, 20, + Float.NaN, null); + assertTrue(fs.addNonNestedFeature(sf2)); + + // overlap left is ok + SequenceFeature sf3 = new SequenceFeature(type, type, 5, 15, Float.NaN, + null); + assertTrue(fs.addNonNestedFeature(sf3)); + + // overlap right is ok + SequenceFeature sf4 = new SequenceFeature(type, type, 15, 25, + Float.NaN, null); + assertTrue(fs.addNonNestedFeature(sf4)); + + // add enclosing feature is not ok + SequenceFeature sf5 = new SequenceFeature(type, type, 10, 21, + Float.NaN, null); + assertFalse(fs.addNonNestedFeature(sf5)); + SequenceFeature sf6 = new SequenceFeature(type, type, 4, 15, Float.NaN, + null); + assertFalse(fs.addNonNestedFeature(sf6)); + SequenceFeature sf7 = new SequenceFeature(type, type, 1, 50, Float.NaN, + null); + assertFalse(fs.addNonNestedFeature(sf7)); + + // add enclosed feature is not ok + SequenceFeature sf8 = new SequenceFeature(type, type, 10, 19, + Float.NaN, null); + assertFalse(fs.addNonNestedFeature(sf8)); + SequenceFeature sf9 = new SequenceFeature(type, type, 16, 25, + Float.NaN, null); + assertFalse(fs.addNonNestedFeature(sf9)); + SequenceFeature sf10 = new SequenceFeature(type, type, 7, 7, Float.NaN, + null); + assertFalse(fs.addNonNestedFeature(sf10)); + } + + @Test(groups = "Functional") + public void testGetPositionalFeatures() + { + FeatureStore store = new FeatureStore(); + SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20, + Float.NaN, null); + store.addFeature(sf1); + // same range, different description + SequenceFeature sf2 = new SequenceFeature("Metal", "desc2", 10, 20, + Float.NaN, null); + store.addFeature(sf2); + // discontiguous range + SequenceFeature sf3 = new SequenceFeature("Metal", "desc", 30, 40, + Float.NaN, null); + store.addFeature(sf3); + // overlapping range + SequenceFeature sf4 = new SequenceFeature("Metal", "desc", 15, 35, + Float.NaN, null); + store.addFeature(sf4); + // enclosing range + SequenceFeature sf5 = new SequenceFeature("Metal", "desc", 5, 50, + Float.NaN, null); + store.addFeature(sf5); + // non-positional feature + SequenceFeature sf6 = new SequenceFeature("Metal", "desc", 0, 0, + Float.NaN, null); + store.addFeature(sf6); + // contact feature + SequenceFeature sf7 = new SequenceFeature("Disulphide bond", "desc", + 18, 45, Float.NaN, null); + store.addFeature(sf7); + + List features = store.getPositionalFeatures(); + assertEquals(features.size(), 6); + assertTrue(features.contains(sf1)); + assertTrue(features.contains(sf2)); + assertTrue(features.contains(sf3)); + assertTrue(features.contains(sf4)); + assertTrue(features.contains(sf5)); + assertFalse(features.contains(sf6)); + assertTrue(features.contains(sf7)); + + features = store.getNonPositionalFeatures(); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf6)); + } + + @Test(groups = "Functional") + public void testDelete() + { + FeatureStore store = new FeatureStore(); + SequenceFeature sf1 = addFeature(store, 10, 20); + assertTrue(store.getPositionalFeatures().contains(sf1)); + + /* + * simple deletion + */ + assertTrue(store.delete(sf1)); + assertTrue(store.getPositionalFeatures().isEmpty()); + + /* + * non-positional feature deletion + */ + SequenceFeature sf2 = addFeature(store, 0, 0); + assertFalse(store.getPositionalFeatures().contains(sf2)); + assertTrue(store.getNonPositionalFeatures().contains(sf2)); + assertTrue(store.delete(sf2)); + assertTrue(store.getNonPositionalFeatures().isEmpty()); + + /* + * contact feature deletion + */ + SequenceFeature sf3 = new SequenceFeature("", "Disulphide Bond", 11, + 23, Float.NaN, null); + store.addFeature(sf3); + assertEquals(store.getPositionalFeatures().size(), 1); + assertTrue(store.getPositionalFeatures().contains(sf3)); + assertTrue(store.delete(sf3)); + assertTrue(store.getPositionalFeatures().isEmpty()); + + /* + * nested feature deletion + */ + SequenceFeature sf4 = addFeature(store, 20, 30); + SequenceFeature sf5 = addFeature(store, 22, 26); // to NCList + SequenceFeature sf6 = addFeature(store, 23, 24); // child of sf5 + SequenceFeature sf7 = addFeature(store, 25, 25); // sibling of sf6 + SequenceFeature sf8 = addFeature(store, 24, 24); // child of sf6 + SequenceFeature sf9 = addFeature(store, 23, 23); // child of sf6 + assertEquals(store.getPositionalFeatures().size(), 6); + + // delete a node with children - they take its place + assertTrue(store.delete(sf6)); // sf8, sf9 should become children of sf5 + assertEquals(store.getPositionalFeatures().size(), 5); + assertFalse(store.getPositionalFeatures().contains(sf6)); + + // delete a node with no children + assertTrue(store.delete(sf7)); + assertEquals(store.getPositionalFeatures().size(), 4); + assertFalse(store.getPositionalFeatures().contains(sf7)); + + // delete root of NCList + assertTrue(store.delete(sf5)); + assertEquals(store.getPositionalFeatures().size(), 3); + assertFalse(store.getPositionalFeatures().contains(sf5)); + + // continue the killing fields + assertTrue(store.delete(sf4)); + assertEquals(store.getPositionalFeatures().size(), 2); + assertFalse(store.getPositionalFeatures().contains(sf4)); + + assertTrue(store.delete(sf9)); + assertEquals(store.getPositionalFeatures().size(), 1); + assertFalse(store.getPositionalFeatures().contains(sf9)); + + assertTrue(store.delete(sf8)); + assertTrue(store.getPositionalFeatures().isEmpty()); + } + + @Test(groups = "Functional") + public void testAddFeature() + { + FeatureStore fs = new FeatureStore(); + + SequenceFeature sf1 = new SequenceFeature("Cath", "", 10, 20, + Float.NaN, null); + SequenceFeature sf2 = new SequenceFeature("Cath", "", 10, 20, + Float.NaN, null); + + assertTrue(fs.addFeature(sf1)); + assertEquals(fs.getFeatureCount(true), 1); // positional + assertEquals(fs.getFeatureCount(false), 0); // non-positional + + /* + * re-adding the same or an identical feature should fail + */ + assertFalse(fs.addFeature(sf1)); + assertEquals(fs.getFeatureCount(true), 1); + assertFalse(fs.addFeature(sf2)); + assertEquals(fs.getFeatureCount(true), 1); + + /* + * add non-positional + */ + SequenceFeature sf3 = new SequenceFeature("Cath", "", 0, 0, Float.NaN, + null); + assertTrue(fs.addFeature(sf3)); + assertEquals(fs.getFeatureCount(true), 1); // positional + assertEquals(fs.getFeatureCount(false), 1); // non-positional + SequenceFeature sf4 = new SequenceFeature("Cath", "", 0, 0, Float.NaN, + null); + assertFalse(fs.addFeature(sf4)); // already stored + assertEquals(fs.getFeatureCount(true), 1); // positional + assertEquals(fs.getFeatureCount(false), 1); // non-positional + + /* + * add contact + */ + SequenceFeature sf5 = new SequenceFeature("Disulfide bond", "", 10, 20, + Float.NaN, null); + assertTrue(fs.addFeature(sf5)); + assertEquals(fs.getFeatureCount(true), 2); // positional - add 1 for contact + assertEquals(fs.getFeatureCount(false), 1); // non-positional + SequenceFeature sf6 = new SequenceFeature("Disulfide bond", "", 10, 20, + Float.NaN, null); + assertFalse(fs.addFeature(sf6)); // already stored + assertEquals(fs.getFeatureCount(true), 2); // no change + assertEquals(fs.getFeatureCount(false), 1); // no change + } + + @Test(groups = "Functional") + public void testIsEmpty() + { + FeatureStore fs = new FeatureStore(); + assertTrue(fs.isEmpty()); + assertEquals(fs.getFeatureCount(true), 0); + + /* + * non-nested feature + */ + SequenceFeature sf1 = new SequenceFeature("Cath", "", 10, 20, + Float.NaN, null); + fs.addFeature(sf1); + assertFalse(fs.isEmpty()); + assertEquals(fs.getFeatureCount(true), 1); + fs.delete(sf1); + assertTrue(fs.isEmpty()); + assertEquals(fs.getFeatureCount(true), 0); + + /* + * non-positional feature + */ + sf1 = new SequenceFeature("Cath", "", 0, 0, Float.NaN, null); + fs.addFeature(sf1); + assertFalse(fs.isEmpty()); + assertEquals(fs.getFeatureCount(false), 1); // non-positional + assertEquals(fs.getFeatureCount(true), 0); // positional + fs.delete(sf1); + assertTrue(fs.isEmpty()); + assertEquals(fs.getFeatureCount(false), 0); + + /* + * contact feature + */ + sf1 = new SequenceFeature("Disulfide bond", "", 19, 49, Float.NaN, null); + fs.addFeature(sf1); + assertFalse(fs.isEmpty()); + assertEquals(fs.getFeatureCount(true), 1); + fs.delete(sf1); + assertTrue(fs.isEmpty()); + assertEquals(fs.getFeatureCount(true), 0); + + /* + * sf2, sf3 added as nested features + */ + sf1 = new SequenceFeature("Cath", "", 19, 49, Float.NaN, null); + SequenceFeature sf2 = new SequenceFeature("Cath", "", 20, 40, + Float.NaN, null); + SequenceFeature sf3 = new SequenceFeature("Cath", "", 25, 35, + Float.NaN, null); + fs.addFeature(sf1); + fs.addFeature(sf2); + fs.addFeature(sf3); + assertEquals(fs.getFeatureCount(true), 3); + assertTrue(fs.delete(sf1)); + assertEquals(fs.getFeatureCount(true), 2); + // FeatureStore should now only contain features in the NCList + assertTrue(fs.nonNestedFeatures.isEmpty()); + assertEquals(fs.nestedFeatures.size(), 2); + assertFalse(fs.isEmpty()); + assertTrue(fs.delete(sf2)); + assertEquals(fs.getFeatureCount(true), 1); + assertFalse(fs.isEmpty()); + assertTrue(fs.delete(sf3)); + assertEquals(fs.getFeatureCount(true), 0); + assertTrue(fs.isEmpty()); // all gone + } + + @Test(groups = "Functional") + public void testGetFeatureGroups() + { + FeatureStore fs = new FeatureStore(); + assertTrue(fs.getFeatureGroups(true).isEmpty()); + assertTrue(fs.getFeatureGroups(false).isEmpty()); + + SequenceFeature sf1 = new SequenceFeature("Cath", "desc", 10, 20, 1f, "group1"); + fs.addFeature(sf1); + Set groups = fs.getFeatureGroups(true); + assertEquals(groups.size(), 1); + assertTrue(groups.contains("group1")); + + /* + * add another feature of the same group, delete one, delete both + */ + SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 20, 30, 1f, "group1"); + fs.addFeature(sf2); + groups = fs.getFeatureGroups(true); + assertEquals(groups.size(), 1); + assertTrue(groups.contains("group1")); + fs.delete(sf2); + groups = fs.getFeatureGroups(true); + assertEquals(groups.size(), 1); + assertTrue(groups.contains("group1")); + fs.delete(sf1); + groups = fs.getFeatureGroups(true); + assertTrue(fs.getFeatureGroups(true).isEmpty()); + + SequenceFeature sf3 = new SequenceFeature("Cath", "desc", 20, 30, 1f, "group2"); + fs.addFeature(sf3); + SequenceFeature sf4 = new SequenceFeature("Cath", "desc", 20, 30, 1f, "Group2"); + fs.addFeature(sf4); + SequenceFeature sf5 = new SequenceFeature("Cath", "desc", 20, 30, 1f, null); + fs.addFeature(sf5); + groups = fs.getFeatureGroups(true); + assertEquals(groups.size(), 3); + assertTrue(groups.contains("group2")); + assertTrue(groups.contains("Group2")); // case sensitive + assertTrue(groups.contains(null)); // null allowed + assertTrue(fs.getFeatureGroups(false).isEmpty()); // non-positional + + fs.delete(sf3); + groups = fs.getFeatureGroups(true); + assertEquals(groups.size(), 2); + assertFalse(groups.contains("group2")); + fs.delete(sf4); + groups = fs.getFeatureGroups(true); + assertEquals(groups.size(), 1); + assertFalse(groups.contains("Group2")); + fs.delete(sf5); + groups = fs.getFeatureGroups(true); + assertTrue(groups.isEmpty()); + + /* + * add non-positional feature + */ + SequenceFeature sf6 = new SequenceFeature("Cath", "desc", 0, 0, 1f, + "CathGroup"); + fs.addFeature(sf6); + groups = fs.getFeatureGroups(false); + assertEquals(groups.size(), 1); + assertTrue(groups.contains("CathGroup")); + assertTrue(fs.delete(sf6)); + assertTrue(fs.getFeatureGroups(false).isEmpty()); + } + + @Test(groups = "Functional") + public void testGetTotalFeatureLength() + { + FeatureStore fs = new FeatureStore(); + assertEquals(fs.getTotalFeatureLength(), 0); + + addFeature(fs, 10, 20); // 11 + assertEquals(fs.getTotalFeatureLength(), 11); + addFeature(fs, 17, 37); // 21 + SequenceFeature sf1 = addFeature(fs, 14, 74); // 61 + assertEquals(fs.getTotalFeatureLength(), 93); + + // non-positional features don't count + SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 0, 0, 1f, + "group1"); + fs.addFeature(sf2); + assertEquals(fs.getTotalFeatureLength(), 93); + + // contact features count 1 + SequenceFeature sf3 = new SequenceFeature("disulphide bond", "desc", + 15, 35, 1f, "group1"); + fs.addFeature(sf3); + assertEquals(fs.getTotalFeatureLength(), 94); + + assertTrue(fs.delete(sf1)); + assertEquals(fs.getTotalFeatureLength(), 33); + assertFalse(fs.delete(sf1)); + assertEquals(fs.getTotalFeatureLength(), 33); + assertTrue(fs.delete(sf2)); + assertEquals(fs.getTotalFeatureLength(), 33); + assertTrue(fs.delete(sf3)); + assertEquals(fs.getTotalFeatureLength(), 32); + } + + @Test(groups = "Functional") + public void testGetFeatureLength() + { + /* + * positional feature + */ + SequenceFeature sf1 = new SequenceFeature("Cath", "desc", 10, 20, 1f, "group1"); + assertEquals(FeatureStore.getFeatureLength(sf1), 11); + + /* + * non-positional feature + */ + SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 0, 0, 1f, + "CathGroup"); + assertEquals(FeatureStore.getFeatureLength(sf2), 0); + + /* + * contact feature counts 1 + */ + SequenceFeature sf3 = new SequenceFeature("Disulphide Bond", "desc", + 14, 28, 1f, "AGroup"); + assertEquals(FeatureStore.getFeatureLength(sf3), 1); + } + + @Test(groups = "Functional") + public void testMin() + { + assertEquals(FeatureStore.min(Float.NaN, Float.NaN), Float.NaN); + assertEquals(FeatureStore.min(Float.NaN, 2f), 2f); + assertEquals(FeatureStore.min(-2f, Float.NaN), -2f); + assertEquals(FeatureStore.min(2f, -3f), -3f); + } + + @Test(groups = "Functional") + public void testMax() + { + assertEquals(FeatureStore.max(Float.NaN, Float.NaN), Float.NaN); + assertEquals(FeatureStore.max(Float.NaN, 2f), 2f); + assertEquals(FeatureStore.max(-2f, Float.NaN), -2f); + assertEquals(FeatureStore.max(2f, -3f), 2f); + } + + @Test(groups = "Functional") + public void testGetMinimumScore_getMaximumScore() + { + FeatureStore fs = new FeatureStore(); + assertEquals(fs.getMinimumScore(true), Float.NaN); // positional + assertEquals(fs.getMaximumScore(true), Float.NaN); + assertEquals(fs.getMinimumScore(false), Float.NaN); // non-positional + assertEquals(fs.getMaximumScore(false), Float.NaN); + + // add features with no score + SequenceFeature sf1 = new SequenceFeature("type", "desc", 0, 0, + Float.NaN, "group"); + fs.addFeature(sf1); + SequenceFeature sf2 = new SequenceFeature("type", "desc", 10, 20, + Float.NaN, "group"); + fs.addFeature(sf2); + assertEquals(fs.getMinimumScore(true), Float.NaN); + assertEquals(fs.getMaximumScore(true), Float.NaN); + assertEquals(fs.getMinimumScore(false), Float.NaN); + assertEquals(fs.getMaximumScore(false), Float.NaN); + + // add positional features with score + SequenceFeature sf3 = new SequenceFeature("type", "desc", 10, 20, 1f, + "group"); + fs.addFeature(sf3); + SequenceFeature sf4 = new SequenceFeature("type", "desc", 12, 16, 4f, + "group"); + fs.addFeature(sf4); + assertEquals(fs.getMinimumScore(true), 1f); + assertEquals(fs.getMaximumScore(true), 4f); + assertEquals(fs.getMinimumScore(false), Float.NaN); + assertEquals(fs.getMaximumScore(false), Float.NaN); + + // add non-positional features with score + SequenceFeature sf5 = new SequenceFeature("type", "desc", 0, 0, 11f, + "group"); + fs.addFeature(sf5); + SequenceFeature sf6 = new SequenceFeature("type", "desc", 0, 0, -7f, + "group"); + fs.addFeature(sf6); + assertEquals(fs.getMinimumScore(true), 1f); + assertEquals(fs.getMaximumScore(true), 4f); + assertEquals(fs.getMinimumScore(false), -7f); + assertEquals(fs.getMaximumScore(false), 11f); + + // delete one positional and one non-positional + // min-max should be recomputed + assertTrue(fs.delete(sf6)); + assertTrue(fs.delete(sf3)); + assertEquals(fs.getMinimumScore(true), 4f); + assertEquals(fs.getMaximumScore(true), 4f); + assertEquals(fs.getMinimumScore(false), 11f); + assertEquals(fs.getMaximumScore(false), 11f); + + // delete remaining features with score + assertTrue(fs.delete(sf4)); + assertTrue(fs.delete(sf5)); + assertEquals(fs.getMinimumScore(true), Float.NaN); + assertEquals(fs.getMaximumScore(true), Float.NaN); + assertEquals(fs.getMinimumScore(false), Float.NaN); + assertEquals(fs.getMaximumScore(false), Float.NaN); + + // delete all features + assertTrue(fs.delete(sf1)); + assertTrue(fs.delete(sf2)); + assertTrue(fs.isEmpty()); + assertEquals(fs.getMinimumScore(true), Float.NaN); + assertEquals(fs.getMaximumScore(true), Float.NaN); + assertEquals(fs.getMinimumScore(false), Float.NaN); + assertEquals(fs.getMaximumScore(false), Float.NaN); + } + + @Test(groups = "Functional") + public void testListContains() + { + assertFalse(FeatureStore.listContains(null, null)); + List features = new ArrayList(); + assertFalse(FeatureStore.listContains(features, null)); + + SequenceFeature sf1 = new SequenceFeature("type1", "desc1", 20, 30, 3f, + "group1"); + assertFalse(FeatureStore.listContains(null, sf1)); + assertFalse(FeatureStore.listContains(features, sf1)); + + features.add(sf1); + SequenceFeature sf2 = new SequenceFeature("type1", "desc1", 20, 30, 3f, + "group1"); + SequenceFeature sf3 = new SequenceFeature("type1", "desc1", 20, 40, 3f, + "group1"); + + // sf2.equals(sf1) so contains should return true + assertTrue(FeatureStore.listContains(features, sf2)); + assertFalse(FeatureStore.listContains(features, sf3)); + } + + @Test(groups = "Functional") + public void testGetFeaturesForGroup() + { + FeatureStore fs = new FeatureStore(); + + /* + * with no features + */ + assertTrue(fs.getFeaturesForGroup(true, null).isEmpty()); + assertTrue(fs.getFeaturesForGroup(false, null).isEmpty()); + assertTrue(fs.getFeaturesForGroup(true, "uniprot").isEmpty()); + assertTrue(fs.getFeaturesForGroup(false, "uniprot").isEmpty()); + + /* + * sf1: positional feature in the null group + */ + SequenceFeature sf1 = new SequenceFeature("Pfam", "desc", 4, 10, 0f, + null); + fs.addFeature(sf1); + assertTrue(fs.getFeaturesForGroup(true, "uniprot").isEmpty()); + assertTrue(fs.getFeaturesForGroup(false, "uniprot").isEmpty()); + assertTrue(fs.getFeaturesForGroup(false, null).isEmpty()); + List features = fs.getFeaturesForGroup(true, null); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf1)); + + /* + * sf2: non-positional feature in the null group + * sf3: positional feature in a non-null group + * sf4: non-positional feature in a non-null group + */ + SequenceFeature sf2 = new SequenceFeature("Pfam", "desc", 0, 0, 0f, + null); + SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 4, 10, 0f, + "Uniprot"); + SequenceFeature sf4 = new SequenceFeature("Pfam", "desc", 0, 0, 0f, + "Rfam"); + fs.addFeature(sf2); + fs.addFeature(sf3); + fs.addFeature(sf4); + + features = fs.getFeaturesForGroup(true, null); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf1)); + + features = fs.getFeaturesForGroup(false, null); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf2)); + + features = fs.getFeaturesForGroup(true, "Uniprot"); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf3)); + + features = fs.getFeaturesForGroup(false, "Rfam"); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf4)); + } + + @Test(groups = "Functional") + public void testShiftFeatures() + { + FeatureStore fs = new FeatureStore(); + assertFalse(fs.shiftFeatures(1)); + + SequenceFeature sf1 = new SequenceFeature("Cath", "", 2, 5, 0f, null); + fs.addFeature(sf1); + // nested feature: + SequenceFeature sf2 = new SequenceFeature("Cath", "", 8, 14, 0f, null); + fs.addFeature(sf2); + // contact feature: + SequenceFeature sf3 = new SequenceFeature("Disulfide bond", "", 23, 32, + 0f, null); + fs.addFeature(sf3); + // non-positional feature: + SequenceFeature sf4 = new SequenceFeature("Cath", "", 0, 0, 0f, null); + fs.addFeature(sf4); + + /* + * shift features right by 5 + */ + assertTrue(fs.shiftFeatures(5)); + + // non-positional features untouched: + List nonPos = fs.getNonPositionalFeatures(); + assertEquals(nonPos.size(), 1); + assertTrue(nonPos.contains(sf4)); + + // positional features are replaced + List pos = fs.getPositionalFeatures(); + assertEquals(pos.size(), 3); + assertFalse(pos.contains(sf1)); + assertFalse(pos.contains(sf2)); + assertFalse(pos.contains(sf3)); + SequenceFeatures.sortFeatures(pos, true); // ascending start pos + assertEquals(pos.get(0).getBegin(), 7); + assertEquals(pos.get(0).getEnd(), 10); + assertEquals(pos.get(1).getBegin(), 13); + assertEquals(pos.get(1).getEnd(), 19); + assertEquals(pos.get(2).getBegin(), 28); + assertEquals(pos.get(2).getEnd(), 37); + + /* + * now shift left by 15 + * feature at [7-10] should be removed + * feature at [13-19] should become [1-4] + */ + assertTrue(fs.shiftFeatures(-15)); + pos = fs.getPositionalFeatures(); + assertEquals(pos.size(), 2); + SequenceFeatures.sortFeatures(pos, true); + assertEquals(pos.get(0).getBegin(), 1); + assertEquals(pos.get(0).getEnd(), 4); + assertEquals(pos.get(1).getBegin(), 13); + assertEquals(pos.get(1).getEnd(), 22); + } + + @Test(groups = "Functional") + public void testDelete_readd() + { + /* + * add a feature and a nested feature + */ + FeatureStore store = new FeatureStore(); + SequenceFeature sf1 = addFeature(store, 10, 20); + // sf2 is nested in sf1 so will be stored in nestedFeatures + SequenceFeature sf2 = addFeature(store, 12, 14); + List features = store.getPositionalFeatures(); + assertEquals(features.size(), 2); + assertTrue(features.contains(sf1)); + assertTrue(features.contains(sf2)); + assertTrue(store.nonNestedFeatures.contains(sf1)); + assertTrue(store.nestedFeatures.contains(sf2)); + + /* + * delete the first feature + */ + assertTrue(store.delete(sf1)); + features = store.getPositionalFeatures(); + assertFalse(features.contains(sf1)); + assertTrue(features.contains(sf2)); + + /* + * re-add the 'nested' feature; is it now duplicated? + */ + store.addFeature(sf2); + features = store.getPositionalFeatures(); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf2)); + } + + @Test(groups = "Functional") + public void testContains() + { + FeatureStore fs = new FeatureStore(); + SequenceFeature sf1 = new SequenceFeature("Cath", "", 10, 20, + Float.NaN, "group1"); + SequenceFeature sf2 = new SequenceFeature("Cath", "", 10, 20, + Float.NaN, "group2"); + SequenceFeature sf3 = new SequenceFeature("Cath", "", 0, 0, Float.NaN, + "group1"); + SequenceFeature sf4 = new SequenceFeature("Cath", "", 0, 0, 0f, + "group1"); + SequenceFeature sf5 = new SequenceFeature("Disulphide Bond", "", 5, 15, + Float.NaN, "group1"); + SequenceFeature sf6 = new SequenceFeature("Disulphide Bond", "", 5, 15, + Float.NaN, "group2"); + + fs.addFeature(sf1); + fs.addFeature(sf3); + fs.addFeature(sf5); + assertTrue(fs.contains(sf1)); // positional feature + assertTrue(fs.contains(new SequenceFeature(sf1))); // identical feature + assertFalse(fs.contains(sf2)); // different group + assertTrue(fs.contains(sf3)); // non-positional + assertTrue(fs.contains(new SequenceFeature(sf3))); + assertFalse(fs.contains(sf4)); // different score + assertTrue(fs.contains(sf5)); // contact feature + assertTrue(fs.contains(new SequenceFeature(sf5))); + assertFalse(fs.contains(sf6)); // different group + + /* + * add a nested feature + */ + SequenceFeature sf7 = new SequenceFeature("Cath", "", 12, 16, + Float.NaN, "group1"); + fs.addFeature(sf7); + assertTrue(fs.contains(sf7)); + assertTrue(fs.contains(new SequenceFeature(sf7))); + + /* + * delete the outer (enclosing, non-nested) feature + */ + fs.delete(sf1); + assertFalse(fs.contains(sf1)); + assertTrue(fs.contains(sf7)); + } +} diff --git a/test/jalview/datamodel/features/NCListTest.java b/test/jalview/datamodel/features/NCListTest.java new file mode 100644 index 0000000..2c7f752 --- /dev/null +++ b/test/jalview/datamodel/features/NCListTest.java @@ -0,0 +1,682 @@ +package jalview.datamodel.features; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; + +import jalview.datamodel.ContiguousI; +import jalview.datamodel.Range; +import jalview.datamodel.SequenceFeature; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Random; + +import junit.extensions.PA; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class NCListTest +{ + + private Random random = new Random(107); + + private Comparator sorter = new RangeComparator(true); + + /** + * A basic sanity test of the constructor + */ + @Test(groups = "Functional") + public void testConstructor() + { + List ranges = new ArrayList(); + ranges.add(new Range(20, 20)); + ranges.add(new Range(10, 20)); + ranges.add(new Range(15, 30)); + ranges.add(new Range(10, 30)); + ranges.add(new Range(11, 19)); + ranges.add(new Range(10, 20)); + ranges.add(new Range(1, 100)); + + NCList ncl = new NCList(ranges); + String expected = "[1-100 [10-30 [10-20 [10-20 [11-19]]]], 15-30 [20-20]]"; + assertEquals(ncl.toString(), expected); + assertTrue(ncl.isValid()); + + Collections.reverse(ranges); + ncl = new NCList(ranges); + assertEquals(ncl.toString(), expected); + assertTrue(ncl.isValid()); + } + + @Test(groups = "Functional") + public void testFindOverlaps() + { + List ranges = new ArrayList(); + ranges.add(new Range(20, 50)); + ranges.add(new Range(30, 70)); + ranges.add(new Range(1, 100)); + ranges.add(new Range(70, 120)); + + NCList ncl = new NCList(ranges); + + List overlaps = ncl.findOverlaps(121, 122); + assertEquals(overlaps.size(), 0); + + overlaps = ncl.findOverlaps(21, 22); + assertEquals(overlaps.size(), 2); + assertEquals(((ContiguousI) overlaps.get(0)).getBegin(), 1); + assertEquals(((ContiguousI) overlaps.get(0)).getEnd(), 100); + assertEquals(((ContiguousI) overlaps.get(1)).getBegin(), 20); + assertEquals(((ContiguousI) overlaps.get(1)).getEnd(), 50); + + overlaps = ncl.findOverlaps(110, 110); + assertEquals(overlaps.size(), 1); + assertEquals(((ContiguousI) overlaps.get(0)).getBegin(), 70); + assertEquals(((ContiguousI) overlaps.get(0)).getEnd(), 120); + } + + @Test(groups = "Functional") + public void testAdd_onTheEnd() + { + List ranges = new ArrayList(); + ranges.add(new Range(20, 50)); + NCList ncl = new NCList(ranges); + assertEquals(ncl.toString(), "[20-50]"); + assertTrue(ncl.isValid()); + + ncl.add(new Range(60, 70)); + assertEquals(ncl.toString(), "[20-50, 60-70]"); + assertTrue(ncl.isValid()); + } + + @Test(groups = "Functional") + public void testAdd_inside() + { + List ranges = new ArrayList(); + ranges.add(new Range(20, 50)); + NCList ncl = new NCList(ranges); + assertEquals(ncl.toString(), "[20-50]"); + assertTrue(ncl.isValid()); + + ncl.add(new Range(30, 40)); + assertEquals(ncl.toString(), "[20-50 [30-40]]"); + } + + @Test(groups = "Functional") + public void testAdd_onTheFront() + { + List ranges = new ArrayList(); + ranges.add(new Range(20, 50)); + NCList ncl = new NCList(ranges); + assertEquals(ncl.toString(), "[20-50]"); + assertTrue(ncl.isValid()); + + ncl.add(new Range(5, 15)); + assertEquals(ncl.toString(), "[5-15, 20-50]"); + assertTrue(ncl.isValid()); + } + + @Test(groups = "Functional") + public void testAdd_enclosing() + { + List ranges = new ArrayList(); + ranges.add(new Range(20, 50)); + ranges.add(new Range(30, 60)); + NCList ncl = new NCList(ranges); + assertEquals(ncl.toString(), "[20-50, 30-60]"); + assertTrue(ncl.isValid()); + assertEquals(ncl.getStart(), 20); + + ncl.add(new Range(10, 70)); + assertEquals(ncl.toString(), "[10-70 [20-50, 30-60]]"); + assertTrue(ncl.isValid()); + } + + @Test(groups = "Functional") + public void testAdd_spanning() + { + List ranges = new ArrayList(); + ranges.add(new Range(20, 40)); + ranges.add(new Range(60, 70)); + NCList ncl = new NCList(ranges); + assertEquals(ncl.toString(), "[20-40, 60-70]"); + assertTrue(ncl.isValid()); + + ncl.add(new Range(30, 50)); + assertEquals(ncl.toString(), "[20-40, 30-50, 60-70]"); + assertTrue(ncl.isValid()); + + ncl.add(new Range(40, 65)); + assertEquals(ncl.toString(), "[20-40, 30-50, 40-65, 60-70]"); + assertTrue(ncl.isValid()); + } + + /** + * Provides the scales for pseudo-random NCLists i.e. the range of the maximal + * [0-scale] interval to be stored + * + * @return + */ + @DataProvider(name = "scalesOfLife") + public Object[][] getScales() + { + return new Object[][] { new Integer[] { 10 }, new Integer[] { 100 } }; + } + + /** + * Do a number of pseudo-random (reproducible) builds of an NCList, to + * exercise as many methods of the class as possible while generating the + * range of possible structure topologies + *

    + *
  • verify that add adds an entry and increments size
  • + *
  • ...except where the entry is already contained (by equals test)
  • + *
  • verify that the structure is valid at all stages of construction
  • + *
  • generate, run and verify a range of overlap queries
  • + *
  • tear down the structure by deleting entries, verifying correctness at + * each stage
  • + *
+ */ + @Test(groups = "Functional", dataProvider = "scalesOfLife") + public void test_pseudoRandom(Integer scale) + { + NCList ncl = new NCList(); + List features = new ArrayList(scale); + + testAdd_pseudoRandom(scale, ncl, features); + + /* + * sort the list of added ranges - this doesn't affect the test, + * just makes it easier to inspect the data in the debugger + */ + Collections.sort(features, sorter); + + testFindOverlaps_pseudoRandom(ncl, scale, features); + + testDelete_pseudoRandom(ncl, features); + } + + /** + * Pick randomly selected entries to delete in turn, checking the NCList size + * and validity at each stage, until it is empty + * + * @param ncl + * @param features + */ + protected void testDelete_pseudoRandom(NCList ncl, + List features) + { + int deleted = 0; + + while (!features.isEmpty()) + { + assertEquals(ncl.size(), features.size()); + int toDelete = random.nextInt(features.size()); + SequenceFeature entry = features.get(toDelete); + assertTrue(ncl.contains(entry), String.format( + "NCList doesn't contain entry [%d] '%s'!", deleted, + entry.toString())); + + ncl.delete(entry); + assertFalse(ncl.contains(entry), String.format( + "NCList still contains deleted entry [%d] '%s'!", deleted, + entry.toString())); + features.remove(toDelete); + deleted++; + + assertTrue(ncl.isValid(), String.format( + "NCList invalid after %d deletions, last deleted was '%s'", + deleted, entry.toString())); + + /* + * brute force check that deleting one entry didn't delete any others + */ + for (int i = 0; i < features.size(); i++) + { + SequenceFeature sf = features.get(i); + assertTrue(ncl.contains(sf), String.format( + "NCList doesn't contain entry [%d] %s after deleting '%s'!", + i, sf.toString(), entry.toString())); + } + } + assertEquals(ncl.size(), 0); // all gone + } + + /** + * Randomly generate entries and add them to the NCList, checking its validity + * and size at each stage. A few entries should be duplicates (by equals test) + * so not get added. + * + * @param scale + * @param ncl + * @param features + */ + protected void testAdd_pseudoRandom(Integer scale, + NCList ncl, + List features) + { + int count = 0; + final int size = 50; + + for (int i = 0; i < size; i++) + { + int r1 = random.nextInt(scale + 1); + int r2 = random.nextInt(scale + 1); + int from = Math.min(r1, r2); + int to = Math.max(r1, r2); + + /* + * choice of two feature values means that occasionally an identical + * feature may be generated, in which case it should not be added + */ + float value = (float) i % 2; + SequenceFeature feature = new SequenceFeature("Pfam", "", from, to, + value, "group"); + + /* + * add to NCList - with duplicate entries (by equals) disallowed + */ + ncl.add(feature, false); + if (features.contains(feature)) + { + System.out.println("Duplicate feature generated " + + feature.toString()); + } + else + { + features.add(feature); + count++; + } + + /* + * check list format is valid at each stage of its construction + */ + assertTrue(ncl.isValid(), + String.format("Failed for scale = %d, i=%d", scale, i)); + assertEquals(ncl.size(), count); + } + // System.out.println(ncl.prettyPrint()); + } + + /** + * A helper method that generates pseudo-random range queries and veries that + * findOverlaps returns the correct matches + * + * @param ncl + * the NCList to query + * @param scale + * ncl maximal range is [0, scale] + * @param features + * a list of the ranges stored in ncl + */ + protected void testFindOverlaps_pseudoRandom(NCList ncl, + int scale, + List features) + { + int halfScale = scale / 2; + int minIterations = 20; + + /* + * generates ranges in [-halfScale, scale+halfScale] + * - some should be internal to [0, scale] P = 1/4 + * - some should lie before 0 P = 1/16 + * - some should lie after scale P = 1/16 + * - some should overlap left P = 1/4 + * - some should overlap right P = 1/4 + * - some should enclose P = 1/8 + * + * 50 iterations give a 96% probability of including the + * unlikeliest case; keep going until we have done all! + */ + boolean inside = false; + boolean enclosing = false; + boolean before = false; + boolean after = false; + boolean overlapLeft = false; + boolean overlapRight = false; + boolean allCasesCovered = false; + + int i = 0; + while (i < minIterations || !allCasesCovered) + { + i++; + int r1 = random.nextInt((scale + 1) * 2); + int r2 = random.nextInt((scale + 1) * 2); + int from = Math.min(r1, r2) - halfScale; + int to = Math.max(r1, r2) - halfScale; + + /* + * ensure all cases of interest get covered + */ + inside |= from >= 0 && to <= scale; + enclosing |= from <= 0 && to >= scale; + before |= to < 0; + after |= from > scale; + overlapLeft |= from < 0 && to >= 0 && to <= scale; + overlapRight |= from >= 0 && from <= scale && to > scale; + if (!allCasesCovered) + { + allCasesCovered |= inside && enclosing && before && after + && overlapLeft && overlapRight; + if (allCasesCovered) + { + System.out + .println(String + .format("Covered all findOverlaps cases after %d iterations for scale %d", + i, scale)); + } + } + + verifyFindOverlaps(ncl, from, to, features); + } + } + + /** + * A helper method that verifies that overlaps found by interrogating an + * NCList correctly match those found by brute force search + * + * @param ncl + * @param from + * @param to + * @param features + */ + protected void verifyFindOverlaps(NCList ncl, int from, + int to, List features) + { + List overlaps = ncl.findOverlaps(from, to); + + /* + * check returned entries do indeed overlap from-to range + */ + for (ContiguousI sf : overlaps) + { + int begin = sf.getBegin(); + int end = sf.getEnd(); + assertTrue(begin <= to && end >= from, String.format( + "[%d, %d] does not overlap query range [%d, %d]", begin, end, + from, to)); + } + + /* + * check overlapping ranges are included in the results + * (the test above already shows non-overlapping ranges are not) + */ + for (ContiguousI sf : features) + { + int begin = sf.getBegin(); + int end = sf.getEnd(); + if (begin <= to && end >= from) + { + boolean found = overlaps.contains(sf); + assertTrue(found, String.format( + "[%d, %d] missing in query range [%d, %d]", begin, end, + from, to)); + } + } + } + + @Test(groups = "Functional") + public void testGetEntries() + { + List ranges = new ArrayList(); + Range r1 = new Range(20, 20); + Range r2 = new Range(10, 20); + Range r3 = new Range(15, 30); + Range r4 = new Range(10, 30); + Range r5 = new Range(11, 19); + Range r6 = new Range(10, 20); + ranges.add(r1); + ranges.add(r2); + ranges.add(r3); + ranges.add(r4); + ranges.add(r5); + ranges.add(r6); + + NCList ncl = new NCList(ranges); + Range r7 = new Range(1, 100); + ncl.add(r7); + + List contents = ncl.getEntries(); + assertEquals(contents.size(), 7); + assertTrue(contents.contains(r1)); + assertTrue(contents.contains(r2)); + assertTrue(contents.contains(r3)); + assertTrue(contents.contains(r4)); + assertTrue(contents.contains(r5)); + assertTrue(contents.contains(r6)); + assertTrue(contents.contains(r7)); + + ncl = new NCList(); + assertTrue(ncl.getEntries().isEmpty()); + } + + @Test(groups = "Functional") + public void testDelete() + { + List ranges = new ArrayList(); + Range r1 = new Range(20, 30); + ranges.add(r1); + NCList ncl = new NCList(ranges); + assertTrue(ncl.getEntries().contains(r1)); + + Range r2 = new Range(20, 30); + assertFalse(ncl.delete(null)); // null argument + assertFalse(ncl.delete(r2)); // never added + assertTrue(ncl.delete(r1)); // success + assertTrue(ncl.getEntries().isEmpty()); + + /* + * tests where object.equals() == true + */ + NCList features = new NCList(); + SequenceFeature sf1 = new SequenceFeature("type", "desc", 1, 10, 2f, + "group"); + SequenceFeature sf2 = new SequenceFeature("type", "desc", 1, 10, 2f, + "group"); + features.add(sf1); + assertEquals(sf1, sf2); // sf1.equals(sf2) + assertFalse(features.delete(sf2)); // equality is not enough for deletion + assertTrue(features.getEntries().contains(sf1)); // still there! + assertTrue(features.delete(sf1)); + assertTrue(features.getEntries().isEmpty()); // gone now + + /* + * test with duplicate objects in NCList + */ + features.add(sf1); + features.add(sf1); + assertEquals(features.getEntries().size(), 2); + assertSame(features.getEntries().get(0), sf1); + assertSame(features.getEntries().get(1), sf1); + assertTrue(features.delete(sf1)); // first match only is deleted + assertTrue(features.contains(sf1)); + assertEquals(features.size(), 1); + assertTrue(features.delete(sf1)); + assertTrue(features.getEntries().isEmpty()); + } + + @Test(groups = "Functional") + public void testAdd_overlapping() + { + List ranges = new ArrayList(); + ranges.add(new Range(40, 50)); + ranges.add(new Range(20, 30)); + NCList ncl = new NCList(ranges); + assertEquals(ncl.toString(), "[20-30, 40-50]"); + assertTrue(ncl.isValid()); + + /* + * add range overlapping internally + */ + ncl.add(new Range(25, 35)); + assertEquals(ncl.toString(), "[20-30, 25-35, 40-50]"); + assertTrue(ncl.isValid()); + + /* + * add range overlapping last range + */ + ncl.add(new Range(45, 55)); + assertEquals(ncl.toString(), "[20-30, 25-35, 40-50, 45-55]"); + assertTrue(ncl.isValid()); + + /* + * add range overlapping first range + */ + ncl.add(new Range(15, 25)); + assertEquals(ncl.toString(), "[15-25, 20-30, 25-35, 40-50, 45-55]"); + assertTrue(ncl.isValid()); + } + + /** + * Test the contains method (which uses object equals test) + */ + @Test(groups = "Functional") + public void testContains() + { + NCList ncl = new NCList(); + SequenceFeature sf1 = new SequenceFeature("type", "desc", 1, 10, 2f, + "group"); + SequenceFeature sf2 = new SequenceFeature("type", "desc", 1, 10, 2f, + "group"); + SequenceFeature sf3 = new SequenceFeature("type", "desc", 1, 10, 2f, + "anothergroup"); + ncl.add(sf1); + + assertTrue(ncl.contains(sf1)); + assertTrue(ncl.contains(sf2)); // sf1.equals(sf2) + assertFalse(ncl.contains(sf3)); // !sf1.equals(sf3) + + /* + * make some deeper structure in the NCList + */ + SequenceFeature sf4 = new SequenceFeature("type", "desc", 2, 9, 2f, + "group"); + ncl.add(sf4); + assertTrue(ncl.contains(sf4)); + SequenceFeature sf5 = new SequenceFeature("type", "desc", 4, 5, 2f, + "group"); + SequenceFeature sf6 = new SequenceFeature("type", "desc", 6, 8, 2f, + "group"); + ncl.add(sf5); + ncl.add(sf6); + assertTrue(ncl.contains(sf5)); + assertTrue(ncl.contains(sf6)); + } + + @Test(groups = "Functional") + public void testIsValid() + { + List ranges = new ArrayList(); + Range r1 = new Range(40, 50); + ranges.add(r1); + NCList ncl = new NCList(ranges); + assertTrue(ncl.isValid()); + + Range r2 = new Range(42, 44); + ncl.add(r2); + assertTrue(ncl.isValid()); + Range r3 = new Range(46, 48); + ncl.add(r3); + assertTrue(ncl.isValid()); + Range r4 = new Range(43, 43); + ncl.add(r4); + assertTrue(ncl.isValid()); + + assertEquals(ncl.toString(), "[40-50 [42-44 [43-43], 46-48]]"); + assertTrue(ncl.isValid()); + + PA.setValue(r1, "start", 43); + assertFalse(ncl.isValid()); // r2 not inside r1 + PA.setValue(r1, "start", 40); + assertTrue(ncl.isValid()); + + PA.setValue(r3, "start", 41); + assertFalse(ncl.isValid()); // r3 should precede r2 + PA.setValue(r3, "start", 46); + assertTrue(ncl.isValid()); + + PA.setValue(r4, "start", 41); + assertFalse(ncl.isValid()); // r4 not inside r2 + PA.setValue(r4, "start", 43); + assertTrue(ncl.isValid()); + + PA.setValue(r4, "start", 44); + assertFalse(ncl.isValid()); // r4 has reverse range + } + + @Test(groups = "Functional") + public void testPrettyPrint() + { + /* + * construct NCList from a list of ranges + * they are sorted then assembled into NCList subregions + * notice that 42-42 end up inside 41-46 + */ + List ranges = new ArrayList(); + ranges.add(new Range(40, 50)); + ranges.add(new Range(45, 55)); + ranges.add(new Range(40, 45)); + ranges.add(new Range(41, 46)); + ranges.add(new Range(42, 42)); + ranges.add(new Range(42, 42)); + NCList ncl = new NCList(ranges); + assertTrue(ncl.isValid()); + assertEquals(ncl.toString(), + "[40-50 [40-45], 41-46 [42-42 [42-42]], 45-55]"); + String expected = "40-50\n 40-45\n41-46\n 42-42\n 42-42\n45-55\n"; + assertEquals(ncl.prettyPrint(), expected); + + /* + * repeat but now add ranges one at a time + * notice that 42-42 end up inside 40-50 so we get + * a different but equal valid NCList structure + */ + ranges.clear(); + ncl = new NCList(ranges); + ncl.add(new Range(40, 50)); + ncl.add(new Range(45, 55)); + ncl.add(new Range(40, 45)); + ncl.add(new Range(41, 46)); + ncl.add(new Range(42, 42)); + ncl.add(new Range(42, 42)); + assertTrue(ncl.isValid()); + assertEquals(ncl.toString(), + "[40-50 [40-45 [42-42 [42-42]], 41-46], 45-55]"); + expected = "40-50\n 40-45\n 42-42\n 42-42\n 41-46\n45-55\n"; + assertEquals(ncl.prettyPrint(), expected); + } + + /** + * A test that shows different valid trees can be constructed from the same + * set of ranges, depending on the order of construction + */ + @Test(groups = "Functional") + public void testConstructor_alternativeTrees() + { + List ranges = new ArrayList(); + ranges.add(new Range(10, 60)); + ranges.add(new Range(20, 30)); + ranges.add(new Range(40, 50)); + + /* + * constructor with greedy traversal of sorted ranges to build nested + * containment lists results in 20-30 inside 10-60, 40-50 a sibling + */ + NCList ncl = new NCList(ranges); + assertEquals(ncl.toString(), "[10-60 [20-30], 40-50]"); + assertTrue(ncl.isValid()); + + /* + * adding ranges one at a time results in 40-50 + * a sibling of 20-30 inside 10-60 + */ + ncl = new NCList(new Range(10, 60)); + ncl.add(new Range(20, 30)); + ncl.add(new Range(40, 50)); + assertEquals(ncl.toString(), "[10-60 [20-30, 40-50]]"); + assertTrue(ncl.isValid()); + } +} diff --git a/test/jalview/datamodel/features/NCNodeTest.java b/test/jalview/datamodel/features/NCNodeTest.java new file mode 100644 index 0000000..4713084 --- /dev/null +++ b/test/jalview/datamodel/features/NCNodeTest.java @@ -0,0 +1,136 @@ +package jalview.datamodel.features; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import jalview.datamodel.Range; +import jalview.datamodel.SequenceFeature; + +import java.util.ArrayList; +import java.util.List; + +import junit.extensions.PA; + +import org.testng.annotations.Test; + +public class NCNodeTest +{ + @Test(groups = "Functional") + public void testAdd() + { + Range r1 = new Range(10, 20); + NCNode node = new NCNode(r1); + assertEquals(node.getBegin(), 10); + Range r2 = new Range(10, 15); + node.add(r2); + + List contents = new ArrayList(); + node.getEntries(contents); + assertEquals(contents.size(), 2); + assertTrue(contents.contains(r1)); + assertTrue(contents.contains(r2)); + } + + @Test( + groups = "Functional", + expectedExceptions = { IllegalArgumentException.class }) + public void testAdd_invalidRangeStart() + { + Range r1 = new Range(10, 20); + NCNode node = new NCNode(r1); + assertEquals(node.getBegin(), 10); + Range r2 = new Range(9, 15); + node.add(r2); + } + + @Test( + groups = "Functional", + expectedExceptions = { IllegalArgumentException.class }) + public void testAdd_invalidRangeEnd() + { + Range r1 = new Range(10, 20); + NCNode node = new NCNode(r1); + assertEquals(node.getBegin(), 10); + Range r2 = new Range(12, 21); + node.add(r2); + } + + @Test(groups = "Functional") + public void testGetEntries() + { + Range r1 = new Range(10, 20); + NCNode node = new NCNode(r1); + List entries = new ArrayList(); + + node.getEntries(entries); + assertEquals(entries.size(), 1); + assertTrue(entries.contains(r1)); + + // clearing the returned list does not affect the NCNode + entries.clear(); + node.getEntries(entries); + assertEquals(entries.size(), 1); + assertTrue(entries.contains(r1)); + + Range r2 = new Range(15, 18); + node.add(r2); + entries.clear(); + node.getEntries(entries); + assertEquals(entries.size(), 2); + assertTrue(entries.contains(r1)); + assertTrue(entries.contains(r2)); + } + + /** + * Tests for the contains method (uses entry.equals() test) + */ + @Test(groups = "Functional") + public void testContains() + { + SequenceFeature sf1 = new SequenceFeature("type", "desc", 1, 10, 2f, + "group"); + SequenceFeature sf2 = new SequenceFeature("type", "desc", 1, 10, 2f, + "group"); + SequenceFeature sf3 = new SequenceFeature("type", "desc", 1, 10, 2f, + "anothergroup"); + NCNode node = new NCNode(sf1); + + assertFalse(node.contains(null)); + assertTrue(node.contains(sf1)); + assertTrue(node.contains(sf2)); // sf1.equals(sf2) + assertFalse(node.contains(sf3)); // !sf1.equals(sf3) + } + + /** + * Test method that checks for valid structure. Valid means that all + * subregions (if any) lie within the root range, and that all subregions have + * valid structure. + */ + @Test(groups = "Functional") + public void testIsValid() + { + Range r1 = new Range(10, 20); + Range r2 = new Range(14, 15); + Range r3 = new Range(16, 17); + NCNode node = new NCNode(r1); + node.add(r2); + node.add(r3); + + /* + * node has root range [10-20] and contains an + * NCList of [14-15, 16-17] + */ + assertTrue(node.isValid()); + PA.setValue(r1, "start", 15); + assertFalse(node.isValid()); // r2 not within r1 + PA.setValue(r1, "start", 10); + assertTrue(node.isValid()); + PA.setValue(r1, "end", 16); + assertFalse(node.isValid()); // r3 not within r1 + PA.setValue(r1, "end", 20); + assertTrue(node.isValid()); + PA.setValue(r3, "start", 12); + assertFalse(node.isValid()); // r3 should precede r2 + } +} diff --git a/test/jalview/datamodel/features/RangeComparatorTest.java b/test/jalview/datamodel/features/RangeComparatorTest.java new file mode 100644 index 0000000..4849b38 --- /dev/null +++ b/test/jalview/datamodel/features/RangeComparatorTest.java @@ -0,0 +1,65 @@ +package jalview.datamodel.features; + +import static org.testng.Assert.assertEquals; + +import jalview.datamodel.ContiguousI; +import jalview.datamodel.Range; + +import java.util.Comparator; + +import org.testng.annotations.Test; + +public class RangeComparatorTest +{ + + @Test(groups = "Functional") + public void testCompare() + { + RangeComparator comp = new RangeComparator(true); + + // same position, same length + assertEquals(comp.compare(10, 10, 20, 20), 0); + // same position, len1 > len2 + assertEquals(comp.compare(10, 10, 20, 19), -1); + // same position, len1 < len2 + assertEquals(comp.compare(10, 10, 20, 21), 1); + // pos1 > pos2 + assertEquals(comp.compare(11, 10, 20, 20), 1); + // pos1 < pos2 + assertEquals(comp.compare(10, 11, 20, 10), -1); + } + + @Test(groups = "Functional") + public void testCompare_byStart() + { + Comparator comp = RangeComparator.BY_START_POSITION; + + // same start position, same length + assertEquals(comp.compare(new Range(10, 20), new Range(10, 20)), 0); + // same start position, len1 > len2 + assertEquals(comp.compare(new Range(10, 20), new Range(10, 19)), -1); + // same start position, len1 < len2 + assertEquals(comp.compare(new Range(10, 18), new Range(10, 20)), 1); + // pos1 > pos2 + assertEquals(comp.compare(new Range(11, 20), new Range(10, 20)), 1); + // pos1 < pos2 + assertEquals(comp.compare(new Range(10, 20), new Range(11, 20)), -1); + } + + @Test(groups = "Functional") + public void testCompare_byEnd() + { + Comparator comp = RangeComparator.BY_END_POSITION; + + // same end position, same length + assertEquals(comp.compare(new Range(10, 20), new Range(10, 20)), 0); + // same end position, len1 > len2 + assertEquals(comp.compare(new Range(10, 20), new Range(11, 20)), -1); + // same end position, len1 < len2 + assertEquals(comp.compare(new Range(11, 20), new Range(10, 20)), 1); + // end1 > end2 + assertEquals(comp.compare(new Range(10, 21), new Range(10, 20)), 1); + // end1 < end2 + assertEquals(comp.compare(new Range(10, 20), new Range(10, 21)), -1); + } +} diff --git a/test/jalview/datamodel/features/SequenceFeaturesTest.java b/test/jalview/datamodel/features/SequenceFeaturesTest.java new file mode 100644 index 0000000..a144f03 --- /dev/null +++ b/test/jalview/datamodel/features/SequenceFeaturesTest.java @@ -0,0 +1,1221 @@ +package jalview.datamodel.features; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; + +import jalview.datamodel.SequenceFeature; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import junit.extensions.PA; + +import org.testng.annotations.Test; + +public class SequenceFeaturesTest +{ + @Test(groups = "Functional") + public void testConstructor() + { + SequenceFeaturesI store = new SequenceFeatures(); + assertFalse(store.hasFeatures()); + + store = new SequenceFeatures((List) null); + assertFalse(store.hasFeatures()); + + List features = new ArrayList<>(); + store = new SequenceFeatures(features); + assertFalse(store.hasFeatures()); + + SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20, + Float.NaN, null); + features.add(sf1); + SequenceFeature sf2 = new SequenceFeature("Metal", "desc", 15, 18, + Float.NaN, null); + features.add(sf2); // nested + SequenceFeature sf3 = new SequenceFeature("Pfam", "desc2", 0, 0, + Float.NaN, null); // non-positional + features.add(sf3); + store = new SequenceFeatures(features); + assertTrue(store.hasFeatures()); + assertEquals(2, store.getFeatureCount(true)); // positional + assertEquals(1, store.getFeatureCount(false)); // non-positional + assertFalse(store.add(sf1)); // already contained + assertFalse(store.add(sf2)); // already contained + assertFalse(store.add(sf3)); // already contained + } + + @Test(groups = "Functional") + public void testGetPositionalFeatures() + { + SequenceFeaturesI store = new SequenceFeatures(); + SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20, + Float.NaN, null); + store.add(sf1); + // same range, different description + SequenceFeature sf2 = new SequenceFeature("Metal", "desc2", 10, 20, + Float.NaN, null); + store.add(sf2); + // discontiguous range + SequenceFeature sf3 = new SequenceFeature("Metal", "desc", 30, 40, + Float.NaN, null); + store.add(sf3); + // overlapping range + SequenceFeature sf4 = new SequenceFeature("Metal", "desc", 15, 35, + Float.NaN, null); + store.add(sf4); + // enclosing range + SequenceFeature sf5 = new SequenceFeature("Metal", "desc", 5, 50, + Float.NaN, null); + store.add(sf5); + // non-positional feature + SequenceFeature sf6 = new SequenceFeature("Metal", "desc", 0, 0, + Float.NaN, null); + store.add(sf6); + // contact feature + SequenceFeature sf7 = new SequenceFeature("Disulphide bond", "desc", + 18, 45, Float.NaN, null); + store.add(sf7); + // different feature type + SequenceFeature sf8 = new SequenceFeature("Pfam", "desc", 30, 40, + Float.NaN, null); + store.add(sf8); + SequenceFeature sf9 = new SequenceFeature("Pfam", "desc", 15, 35, + Float.NaN, null); + store.add(sf9); + + /* + * get all positional features + */ + List features = store.getPositionalFeatures(); + assertEquals(features.size(), 8); + assertTrue(features.contains(sf1)); + assertTrue(features.contains(sf2)); + assertTrue(features.contains(sf3)); + assertTrue(features.contains(sf4)); + assertTrue(features.contains(sf5)); + assertFalse(features.contains(sf6)); // non-positional + assertTrue(features.contains(sf7)); + assertTrue(features.contains(sf8)); + assertTrue(features.contains(sf9)); + + /* + * get features by type + */ + assertTrue(store.getPositionalFeatures((String) null).isEmpty()); + assertTrue(store.getPositionalFeatures("Cath").isEmpty()); + assertTrue(store.getPositionalFeatures("METAL").isEmpty()); + + features = store.getPositionalFeatures("Metal"); + assertEquals(features.size(), 5); + assertTrue(features.contains(sf1)); + assertTrue(features.contains(sf2)); + assertTrue(features.contains(sf3)); + assertTrue(features.contains(sf4)); + assertTrue(features.contains(sf5)); + assertFalse(features.contains(sf6)); + + features = store.getPositionalFeatures("Disulphide bond"); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf7)); + + features = store.getPositionalFeatures("Pfam"); + assertEquals(features.size(), 2); + assertTrue(features.contains(sf8)); + assertTrue(features.contains(sf9)); + } + + @Test(groups = "Functional") + public void testGetContactFeatures() + { + SequenceFeaturesI store = new SequenceFeatures(); + // non-contact + SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20, + Float.NaN, null); + store.add(sf1); + // non-positional + SequenceFeature sf2 = new SequenceFeature("Metal", "desc", 0, 0, + Float.NaN, null); + store.add(sf2); + // contact feature + SequenceFeature sf3 = new SequenceFeature("Disulphide bond", "desc", + 18, 45, Float.NaN, null); + store.add(sf3); + // repeat for different feature type + SequenceFeature sf4 = new SequenceFeature("Pfam", "desc", 10, 20, + Float.NaN, null); + store.add(sf4); + SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 0, 0, + Float.NaN, null); + store.add(sf5); + SequenceFeature sf6 = new SequenceFeature("Disulfide bond", "desc", 18, + 45, Float.NaN, null); + store.add(sf6); + + /* + * get all contact features + */ + List features = store.getContactFeatures(); + assertEquals(features.size(), 2); + assertTrue(features.contains(sf3)); + assertTrue(features.contains(sf6)); + + /* + * get contact features by type + */ + assertTrue(store.getContactFeatures((String) null).isEmpty()); + assertTrue(store.getContactFeatures("Cath").isEmpty()); + assertTrue(store.getContactFeatures("Pfam").isEmpty()); + assertTrue(store.getContactFeatures("DISULPHIDE BOND").isEmpty()); + + features = store.getContactFeatures("Disulphide bond"); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf3)); + + features = store.getContactFeatures("Disulfide bond"); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf6)); + } + + @Test(groups = "Functional") + public void testGetNonPositionalFeatures() + { + SequenceFeaturesI store = new SequenceFeatures(); + // positional + SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20, + Float.NaN, null); + store.add(sf1); + // non-positional + SequenceFeature sf2 = new SequenceFeature("Metal", "desc", 0, 0, + Float.NaN, null); + store.add(sf2); + // contact feature + SequenceFeature sf3 = new SequenceFeature("Disulphide bond", "desc", + 18, 45, Float.NaN, null); + store.add(sf3); + // repeat for different feature type + SequenceFeature sf4 = new SequenceFeature("Pfam", "desc", 10, 20, + Float.NaN, null); + store.add(sf4); + SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 0, 0, + Float.NaN, null); + store.add(sf5); + SequenceFeature sf6 = new SequenceFeature("Disulfide bond", "desc", 18, + 45, Float.NaN, null); + store.add(sf6); + // one more non-positional, different description + SequenceFeature sf7 = new SequenceFeature("Pfam", "desc2", 0, 0, + Float.NaN, null); + store.add(sf7); + + /* + * get all non-positional features + */ + List features = store.getNonPositionalFeatures(); + assertEquals(features.size(), 3); + assertTrue(features.contains(sf2)); + assertTrue(features.contains(sf5)); + assertTrue(features.contains(sf7)); + + /* + * get non-positional features by type + */ + assertTrue(store.getNonPositionalFeatures((String) null).isEmpty()); + assertTrue(store.getNonPositionalFeatures("Cath").isEmpty()); + assertTrue(store.getNonPositionalFeatures("PFAM").isEmpty()); + + features = store.getNonPositionalFeatures("Metal"); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf2)); + + features = store.getNonPositionalFeatures("Pfam"); + assertEquals(features.size(), 2); + assertTrue(features.contains(sf5)); + assertTrue(features.contains(sf7)); + } + + /** + * Helper method to add a feature of no particular type + * + * @param sf + * @param type + * @param from + * @param to + * @return + */ + SequenceFeature addFeature(SequenceFeaturesI sf, String type, int from, + int to) + { + SequenceFeature sf1 = new SequenceFeature(type, "", from, to, + Float.NaN, + null); + sf.add(sf1); + return sf1; + } + + @Test(groups = "Functional") + public void testFindFeatures() + { + SequenceFeaturesI sf = new SequenceFeatures(); + SequenceFeature sf1 = addFeature(sf, "Pfam", 10, 50); + SequenceFeature sf2 = addFeature(sf, "Pfam", 1, 15); + SequenceFeature sf3 = addFeature(sf, "Pfam", 20, 30); + SequenceFeature sf4 = addFeature(sf, "Pfam", 40, 100); + SequenceFeature sf5 = addFeature(sf, "Pfam", 60, 100); + SequenceFeature sf6 = addFeature(sf, "Pfam", 70, 70); + SequenceFeature sf7 = addFeature(sf, "Cath", 10, 50); + SequenceFeature sf8 = addFeature(sf, "Cath", 1, 15); + SequenceFeature sf9 = addFeature(sf, "Cath", 20, 30); + SequenceFeature sf10 = addFeature(sf, "Cath", 40, 100); + SequenceFeature sf11 = addFeature(sf, "Cath", 60, 100); + SequenceFeature sf12 = addFeature(sf, "Cath", 70, 70); + + List overlaps = sf.findFeatures(200, 200, "Pfam"); + assertTrue(overlaps.isEmpty()); + + overlaps = sf.findFeatures( 1, 9, "Pfam"); + assertEquals(overlaps.size(), 1); + assertTrue(overlaps.contains(sf2)); + + overlaps = sf.findFeatures( 5, 18, "Pfam"); + assertEquals(overlaps.size(), 2); + assertTrue(overlaps.contains(sf1)); + assertTrue(overlaps.contains(sf2)); + + overlaps = sf.findFeatures(30, 40, "Pfam"); + assertEquals(overlaps.size(), 3); + assertTrue(overlaps.contains(sf1)); + assertTrue(overlaps.contains(sf3)); + assertTrue(overlaps.contains(sf4)); + + overlaps = sf.findFeatures( 80, 90, "Pfam"); + assertEquals(overlaps.size(), 2); + assertTrue(overlaps.contains(sf4)); + assertTrue(overlaps.contains(sf5)); + + overlaps = sf.findFeatures( 68, 70, "Pfam"); + assertEquals(overlaps.size(), 3); + assertTrue(overlaps.contains(sf4)); + assertTrue(overlaps.contains(sf5)); + assertTrue(overlaps.contains(sf6)); + + overlaps = sf.findFeatures(16, 69, "Cath"); + assertEquals(overlaps.size(), 4); + assertTrue(overlaps.contains(sf7)); + assertFalse(overlaps.contains(sf8)); + assertTrue(overlaps.contains(sf9)); + assertTrue(overlaps.contains(sf10)); + assertTrue(overlaps.contains(sf11)); + assertFalse(overlaps.contains(sf12)); + + assertTrue(sf.findFeatures(0, 1000, "Metal").isEmpty()); + + overlaps = sf.findFeatures(7, 7, (String) null); + assertTrue(overlaps.isEmpty()); + } + + @Test(groups = "Functional") + public void testDelete() + { + SequenceFeaturesI sf = new SequenceFeatures(); + SequenceFeature sf1 = addFeature(sf, "Pfam", 10, 50); + assertTrue(sf.getPositionalFeatures().contains(sf1)); + + assertFalse(sf.delete(null)); + SequenceFeature sf2 = new SequenceFeature("Cath", "", 10, 15, 0f, null); + assertFalse(sf.delete(sf2)); // not added, can't delete it + assertTrue(sf.delete(sf1)); + assertTrue(sf.getPositionalFeatures().isEmpty()); + } + + @Test(groups = "Functional") + public void testHasFeatures() + { + SequenceFeaturesI sf = new SequenceFeatures(); + assertFalse(sf.hasFeatures()); + + SequenceFeature sf1 = addFeature(sf, "Pfam", 10, 50); + assertTrue(sf.hasFeatures()); + + sf.delete(sf1); + assertFalse(sf.hasFeatures()); + } + + /** + * Tests for the method that gets feature groups for positional or + * non-positional features + */ + @Test(groups = "Functional") + public void testGetFeatureGroups() + { + SequenceFeaturesI sf = new SequenceFeatures(); + assertTrue(sf.getFeatureGroups(true).isEmpty()); + assertTrue(sf.getFeatureGroups(false).isEmpty()); + + /* + * add a non-positional feature (begin/end = 0/0) + */ + SequenceFeature sfx = new SequenceFeature("AType", "Desc", 0, 0, 0f, + "AGroup"); + sf.add(sfx); + Set groups = sf.getFeatureGroups(true); // for positional + assertTrue(groups.isEmpty()); + groups = sf.getFeatureGroups(false); // for non-positional + assertEquals(groups.size(), 1); + assertTrue(groups.contains("AGroup")); + groups = sf.getFeatureGroups(false, "AType"); + assertEquals(groups.size(), 1); + assertTrue(groups.contains("AGroup")); + groups = sf.getFeatureGroups(true, "AnotherType"); + assertTrue(groups.isEmpty()); + + /* + * add, then delete, more non-positional features of different types + */ + SequenceFeature sfy = new SequenceFeature("AnotherType", "Desc", 0, 0, + 0f, + "AnotherGroup"); + sf.add(sfy); + SequenceFeature sfz = new SequenceFeature("AThirdType", "Desc", 0, 0, + 0f, + null); + sf.add(sfz); + groups = sf.getFeatureGroups(false); + assertEquals(groups.size(), 3); + assertTrue(groups.contains("AGroup")); + assertTrue(groups.contains("AnotherGroup")); + assertTrue(groups.contains(null)); // null is a possible group + sf.delete(sfz); + sf.delete(sfy); + groups = sf.getFeatureGroups(false); + assertEquals(groups.size(), 1); + assertTrue(groups.contains("AGroup")); + + /* + * add positional features + */ + SequenceFeature sf1 = new SequenceFeature("Pfam", "Desc", 10, 50, 0f, + "PfamGroup"); + sf.add(sf1); + groups = sf.getFeatureGroups(true); + assertEquals(groups.size(), 1); + assertTrue(groups.contains("PfamGroup")); + groups = sf.getFeatureGroups(false); // non-positional unchanged + assertEquals(groups.size(), 1); + assertTrue(groups.contains("AGroup")); + + SequenceFeature sf2 = new SequenceFeature("Cath", "Desc", 10, 50, 0f, + null); + sf.add(sf2); + groups = sf.getFeatureGroups(true); + assertEquals(groups.size(), 2); + assertTrue(groups.contains("PfamGroup")); + assertTrue(groups.contains(null)); + + sf.delete(sf1); + sf.delete(sf2); + assertTrue(sf.getFeatureGroups(true).isEmpty()); + + SequenceFeature sf3 = new SequenceFeature("CDS", "", 10, 50, 0f, + "Ensembl"); + sf.add(sf3); + SequenceFeature sf4 = new SequenceFeature("exon", "", 10, 50, 0f, + "Ensembl"); + sf.add(sf4); + groups = sf.getFeatureGroups(true); + assertEquals(groups.size(), 1); + assertTrue(groups.contains("Ensembl")); + + /* + * delete last Ensembl group feature from CDS features + * but still have one in exon features + */ + sf.delete(sf3); + groups = sf.getFeatureGroups(true); + assertEquals(groups.size(), 1); + assertTrue(groups.contains("Ensembl")); + + /* + * delete the last non-positional feature + */ + sf.delete(sfx); + groups = sf.getFeatureGroups(false); + assertTrue(groups.isEmpty()); + } + + @Test(groups = "Functional") + public void testGetFeatureTypesForGroups() + { + SequenceFeaturesI sf = new SequenceFeatures(); + assertTrue(sf.getFeatureTypesForGroups(true, (String) null).isEmpty()); + + /* + * add feature with group = "Uniprot", type = "helix" + */ + String groupUniprot = "Uniprot"; + SequenceFeature sf1 = new SequenceFeature("helix", "Desc", 10, 50, 0f, + groupUniprot); + sf.add(sf1); + Set groups = sf.getFeatureTypesForGroups(true, groupUniprot); + assertEquals(groups.size(), 1); + assertTrue(groups.contains("helix")); + assertTrue(sf.getFeatureTypesForGroups(true, (String) null).isEmpty()); + + /* + * add feature with group = "Uniprot", type = "strand" + */ + SequenceFeature sf2 = new SequenceFeature("strand", "Desc", 10, 50, 0f, + groupUniprot); + sf.add(sf2); + groups = sf.getFeatureTypesForGroups(true, groupUniprot); + assertEquals(groups.size(), 2); + assertTrue(groups.contains("helix")); + assertTrue(groups.contains("strand")); + + /* + * delete the "strand" Uniprot feature - still have "helix" + */ + sf.delete(sf2); + groups = sf.getFeatureTypesForGroups(true, groupUniprot); + assertEquals(groups.size(), 1); + assertTrue(groups.contains("helix")); + + /* + * delete the "helix" Uniprot feature - none left + */ + sf.delete(sf1); + assertTrue(sf.getFeatureTypesForGroups(true, groupUniprot).isEmpty()); + + /* + * add some null group features + */ + SequenceFeature sf3 = new SequenceFeature("strand", "Desc", 10, 50, 0f, + null); + sf.add(sf3); + SequenceFeature sf4 = new SequenceFeature("turn", "Desc", 10, 50, 0f, + null); + sf.add(sf4); + groups = sf.getFeatureTypesForGroups(true, (String) null); + assertEquals(groups.size(), 2); + assertTrue(groups.contains("strand")); + assertTrue(groups.contains("turn")); + + /* + * add strand/Cath and turn/Scop and query for one or both groups + * (find feature types for groups selected in Feature Settings) + */ + SequenceFeature sf5 = new SequenceFeature("strand", "Desc", 10, 50, 0f, + "Cath"); + sf.add(sf5); + SequenceFeature sf6 = new SequenceFeature("turn", "Desc", 10, 50, 0f, + "Scop"); + sf.add(sf6); + groups = sf.getFeatureTypesForGroups(true, "Cath"); + assertEquals(groups.size(), 1); + assertTrue(groups.contains("strand")); + groups = sf.getFeatureTypesForGroups(true, "Scop"); + assertEquals(groups.size(), 1); + assertTrue(groups.contains("turn")); + groups = sf.getFeatureTypesForGroups(true, "Cath", "Scop"); + assertEquals(groups.size(), 2); + assertTrue(groups.contains("turn")); + assertTrue(groups.contains("strand")); + // alternative vararg syntax + groups = sf.getFeatureTypesForGroups(true, new String[] { "Cath", + "Scop" }); + assertEquals(groups.size(), 2); + assertTrue(groups.contains("turn")); + assertTrue(groups.contains("strand")); + } + + @Test(groups = "Functional") + public void testGetFeatureTypes() + { + SequenceFeaturesI store = new SequenceFeatures(); + Set types = store.getFeatureTypes(); + assertTrue(types.isEmpty()); + + SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20, + Float.NaN, null); + store.add(sf1); + types = store.getFeatureTypes(); + assertEquals(types.size(), 1); + assertTrue(types.contains("Metal")); + + // null type is rejected... + SequenceFeature sf2 = new SequenceFeature(null, "desc", 10, 20, + Float.NaN, null); + assertFalse(store.add(sf2)); + types = store.getFeatureTypes(); + assertEquals(types.size(), 1); + assertFalse(types.contains(null)); + assertTrue(types.contains("Metal")); + + /* + * add non-positional feature + */ + SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 0, 0, + Float.NaN, null); + store.add(sf3); + types = store.getFeatureTypes(); + assertEquals(types.size(), 2); + assertTrue(types.contains("Pfam")); + + /* + * add contact feature + */ + SequenceFeature sf4 = new SequenceFeature("Disulphide Bond", "desc", + 10, 20, Float.NaN, null); + store.add(sf4); + types = store.getFeatureTypes(); + assertEquals(types.size(), 3); + assertTrue(types.contains("Disulphide Bond")); + + /* + * add another Pfam + */ + SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 10, 20, + Float.NaN, null); + store.add(sf5); + types = store.getFeatureTypes(); + assertEquals(types.size(), 3); // unchanged + + /* + * delete first Pfam - still have one + */ + assertTrue(store.delete(sf3)); + types = store.getFeatureTypes(); + assertEquals(types.size(), 3); + assertTrue(types.contains("Pfam")); + + /* + * delete second Pfam - no longer have one + */ + assertTrue(store.delete(sf5)); + types = store.getFeatureTypes(); + assertEquals(types.size(), 2); + assertFalse(types.contains("Pfam")); + } + + @Test(groups = "Functional") + public void testGetFeatureCount() + { + SequenceFeaturesI store = new SequenceFeatures(); + assertEquals(store.getFeatureCount(true), 0); + assertEquals(store.getFeatureCount(false), 0); + + /* + * add positional + */ + SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20, + Float.NaN, null); + store.add(sf1); + assertEquals(store.getFeatureCount(true), 1); + assertEquals(store.getFeatureCount(false), 0); + + /* + * null feature type is rejected + */ + SequenceFeature sf2 = new SequenceFeature(null, "desc", 10, 20, + Float.NaN, null); + assertFalse(store.add(sf2)); + assertEquals(store.getFeatureCount(true), 1); + assertEquals(store.getFeatureCount(false), 0); + + /* + * add non-positional feature + */ + SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 0, 0, + Float.NaN, null); + store.add(sf3); + assertEquals(store.getFeatureCount(true), 1); + assertEquals(store.getFeatureCount(false), 1); + + /* + * add contact feature (counts as 1) + */ + SequenceFeature sf4 = new SequenceFeature("Disulphide Bond", "desc", + 10, 20, Float.NaN, null); + store.add(sf4); + assertEquals(store.getFeatureCount(true), 2); + assertEquals(store.getFeatureCount(false), 1); + + /* + * add another Pfam but this time as a positional feature + */ + SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 10, 20, + Float.NaN, null); + store.add(sf5); + assertEquals(store.getFeatureCount(true), 3); // sf1, sf4, sf5 + assertEquals(store.getFeatureCount(false), 1); // sf3 + assertEquals(store.getFeatureCount(true, "Pfam"), 1); // positional + assertEquals(store.getFeatureCount(false, "Pfam"), 1); // non-positional + // search for type==null + assertEquals(store.getFeatureCount(true, (String) null), 0); + // search with no type specified + assertEquals(store.getFeatureCount(true, (String[]) null), 3); + assertEquals(store.getFeatureCount(true, "Metal", "Cath"), 1); + assertEquals(store.getFeatureCount(true, "Disulphide Bond"), 1); + assertEquals(store.getFeatureCount(true, "Metal", "Pfam", null), 2); + + /* + * delete first Pfam (non-positional) + */ + assertTrue(store.delete(sf3)); + assertEquals(store.getFeatureCount(true), 3); + assertEquals(store.getFeatureCount(false), 0); + + /* + * delete second Pfam (positional) + */ + assertTrue(store.delete(sf5)); + assertEquals(store.getFeatureCount(true), 2); + assertEquals(store.getFeatureCount(false), 0); + } + + @Test(groups = "Functional") + public void testGetAllFeatures() + { + SequenceFeaturesI store = new SequenceFeatures(); + List features = store.getAllFeatures(); + assertTrue(features.isEmpty()); + + SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20, + Float.NaN, null); + store.add(sf1); + features = store.getAllFeatures(); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf1)); + + SequenceFeature sf2 = new SequenceFeature("Metallic", "desc", 10, 20, + Float.NaN, null); + store.add(sf2); + features = store.getAllFeatures(); + assertEquals(features.size(), 2); + assertTrue(features.contains(sf2)); + + /* + * add non-positional feature + */ + SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 0, 0, + Float.NaN, null); + store.add(sf3); + features = store.getAllFeatures(); + assertEquals(features.size(), 3); + assertTrue(features.contains(sf3)); + + /* + * add contact feature + */ + SequenceFeature sf4 = new SequenceFeature("Disulphide Bond", "desc", + 10, 20, Float.NaN, null); + store.add(sf4); + features = store.getAllFeatures(); + assertEquals(features.size(), 4); + assertTrue(features.contains(sf4)); + + /* + * add another Pfam + */ + SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 10, 20, + Float.NaN, null); + store.add(sf5); + features = store.getAllFeatures(); + assertEquals(features.size(), 5); + assertTrue(features.contains(sf5)); + + /* + * select by type does not apply to non-positional features + */ + features = store.getAllFeatures("Cath"); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf3)); + + features = store.getAllFeatures("Pfam", "Cath", "Metal"); + assertEquals(features.size(), 3); + assertTrue(features.contains(sf1)); + assertTrue(features.contains(sf3)); + assertTrue(features.contains(sf5)); + + /* + * delete first Pfam + */ + assertTrue(store.delete(sf3)); + features = store.getAllFeatures(); + assertEquals(features.size(), 4); + assertFalse(features.contains(sf3)); + + /* + * delete second Pfam + */ + assertTrue(store.delete(sf5)); + features = store.getAllFeatures(); + assertEquals(features.size(), 3); + assertFalse(features.contains(sf3)); + } + + @Test(groups = "Functional") + public void testGetTotalFeatureLength() + { + SequenceFeaturesI store = new SequenceFeatures(); + assertEquals(store.getTotalFeatureLength(), 0); + + SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20, + Float.NaN, null); + assertTrue(store.add(sf1)); + assertEquals(store.getTotalFeatureLength(), 11); + assertEquals(store.getTotalFeatureLength("Metal"), 11); + assertEquals(store.getTotalFeatureLength("Plastic"), 0); + + // re-add does nothing! + assertFalse(store.add(sf1)); + assertEquals(store.getTotalFeatureLength(), 11); + + /* + * add non-positional feature + */ + SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 0, 0, + Float.NaN, null); + store.add(sf3); + assertEquals(store.getTotalFeatureLength(), 11); + + /* + * add contact feature - counts 1 to feature length + */ + SequenceFeature sf4 = new SequenceFeature("Disulphide Bond", "desc", + 10, 20, Float.NaN, null); + store.add(sf4); + assertEquals(store.getTotalFeatureLength(), 12); + + /* + * add another Pfam + */ + SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 10, 20, + Float.NaN, null); + store.add(sf5); + assertEquals(store.getTotalFeatureLength(), 23); + + /* + * delete features + */ + assertTrue(store.delete(sf3)); // non-positional + assertEquals(store.getTotalFeatureLength(), 23); // no change + + assertTrue(store.delete(sf5)); + assertEquals(store.getTotalFeatureLength(), 12); + + assertTrue(store.delete(sf4)); // contact + assertEquals(store.getTotalFeatureLength(), 11); + + assertTrue(store.delete(sf1)); + assertEquals(store.getTotalFeatureLength(), 0); + } + + @Test(groups = "Functional") + public void testGetMinimumScore_getMaximumScore() + { + SequenceFeatures sf = new SequenceFeatures(); + SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 0, 0, + Float.NaN, "group"); // non-positional, no score + sf.add(sf1); + SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 10, 20, + Float.NaN, "group"); // positional, no score + sf.add(sf2); + SequenceFeature sf3 = new SequenceFeature("Metal", "desc", 10, 20, 1f, + "group"); + sf.add(sf3); + SequenceFeature sf4 = new SequenceFeature("Metal", "desc", 12, 16, 4f, + "group"); + sf.add(sf4); + SequenceFeature sf5 = new SequenceFeature("Cath", "desc", 0, 0, 11f, + "group"); + sf.add(sf5); + SequenceFeature sf6 = new SequenceFeature("Cath", "desc", 0, 0, -7f, + "group"); + sf.add(sf6); + + assertEquals(sf.getMinimumScore("nosuchtype", true), Float.NaN); + assertEquals(sf.getMinimumScore("nosuchtype", false), Float.NaN); + assertEquals(sf.getMaximumScore("nosuchtype", true), Float.NaN); + assertEquals(sf.getMaximumScore("nosuchtype", false), Float.NaN); + + // positional features min-max: + assertEquals(sf.getMinimumScore("Metal", true), 1f); + assertEquals(sf.getMaximumScore("Metal", true), 4f); + assertEquals(sf.getMinimumScore("Cath", true), Float.NaN); + assertEquals(sf.getMaximumScore("Cath", true), Float.NaN); + + // non-positional features min-max: + assertEquals(sf.getMinimumScore("Cath", false), -7f); + assertEquals(sf.getMaximumScore("Cath", false), 11f); + assertEquals(sf.getMinimumScore("Metal", false), Float.NaN); + assertEquals(sf.getMaximumScore("Metal", false), Float.NaN); + + // delete features; min-max should get recomputed + sf.delete(sf6); + assertEquals(sf.getMinimumScore("Cath", false), 11f); + assertEquals(sf.getMaximumScore("Cath", false), 11f); + sf.delete(sf4); + assertEquals(sf.getMinimumScore("Metal", true), 1f); + assertEquals(sf.getMaximumScore("Metal", true), 1f); + sf.delete(sf5); + assertEquals(sf.getMinimumScore("Cath", false), Float.NaN); + assertEquals(sf.getMaximumScore("Cath", false), Float.NaN); + sf.delete(sf3); + assertEquals(sf.getMinimumScore("Metal", true), Float.NaN); + assertEquals(sf.getMaximumScore("Metal", true), Float.NaN); + sf.delete(sf1); + sf.delete(sf2); + assertFalse(sf.hasFeatures()); + assertEquals(sf.getMinimumScore("Cath", false), Float.NaN); + assertEquals(sf.getMaximumScore("Cath", false), Float.NaN); + assertEquals(sf.getMinimumScore("Metal", true), Float.NaN); + assertEquals(sf.getMaximumScore("Metal", true), Float.NaN); + } + + @Test(groups = "Functional") + public void testVarargsToTypes() + { + SequenceFeatures sf = new SequenceFeatures(); + sf.add(new SequenceFeature("Metal", "desc", 0, 0, Float.NaN, "group")); + sf.add(new SequenceFeature("Cath", "desc", 10, 20, Float.NaN, "group")); + + /* + * no type specified - get all types stored + * they are returned in keyset (alphabetical) order + */ + Map featureStores = (Map) PA + .getValue(sf, "featureStore"); + + Iterable types = sf.varargToTypes(); + Iterator iterator = types.iterator(); + assertTrue(iterator.hasNext()); + assertSame(iterator.next(), featureStores.get("Cath")); + assertTrue(iterator.hasNext()); + assertSame(iterator.next(), featureStores.get("Metal")); + assertFalse(iterator.hasNext()); + + /* + * empty array is the same as no vararg parameter supplied + * so treated as all stored types + */ + types = sf.varargToTypes(new String[] {}); + iterator = types.iterator(); + assertTrue(iterator.hasNext()); + assertSame(iterator.next(), featureStores.get("Cath")); + assertTrue(iterator.hasNext()); + assertSame(iterator.next(), featureStores.get("Metal")); + assertFalse(iterator.hasNext()); + + /* + * null type specified; this is passed as vararg + * String[1] {null} + */ + types = sf.varargToTypes((String) null); + assertFalse(types.iterator().hasNext()); + + /* + * null types array specified; this is passed as vararg null + */ + types = sf.varargToTypes((String[]) null); + iterator = types.iterator(); + assertTrue(iterator.hasNext()); + assertSame(iterator.next(), featureStores.get("Cath")); + assertTrue(iterator.hasNext()); + assertSame(iterator.next(), featureStores.get("Metal")); + assertFalse(iterator.hasNext()); + + /* + * one type specified + */ + types = sf.varargToTypes("Metal"); + iterator = types.iterator(); + assertTrue(iterator.hasNext()); + assertSame(iterator.next(), featureStores.get("Metal")); + assertFalse(iterator.hasNext()); + + /* + * two types specified - get sorted alphabetically + */ + types = sf.varargToTypes("Metal", "Cath"); + iterator = types.iterator(); + assertTrue(iterator.hasNext()); + assertSame(iterator.next(), featureStores.get("Cath")); + assertTrue(iterator.hasNext()); + assertSame(iterator.next(), featureStores.get("Metal")); + assertFalse(iterator.hasNext()); + + /* + * null type included - should be ignored + */ + types = sf.varargToTypes("Metal", null, "Helix"); + iterator = types.iterator(); + assertTrue(iterator.hasNext()); + assertSame(iterator.next(), featureStores.get("Metal")); + assertFalse(iterator.hasNext()); + } + + @Test(groups = "Functional") + public void testGetFeatureTypes_byOntology() + { + SequenceFeaturesI store = new SequenceFeatures(); + + SequenceFeature sf1 = new SequenceFeature("transcript", "desc", 10, 20, + Float.NaN, null); + store.add(sf1); + // mRNA isA mature_transcript isA transcript + SequenceFeature sf2 = new SequenceFeature("mRNA", "desc", 10, 20, + Float.NaN, null); + store.add(sf2); + // just to prove non-positional feature types are included + SequenceFeature sf3 = new SequenceFeature("mRNA", "desc", 0, 0, + Float.NaN, null); + store.add(sf3); + SequenceFeature sf4 = new SequenceFeature("CDS", "desc", 0, 0, + Float.NaN, null); + store.add(sf4); + + Set types = store.getFeatureTypes("transcript"); + assertEquals(types.size(), 2); + assertTrue(types.contains("transcript")); + assertTrue(types.contains("mRNA")); + + // matches include arguments whether SO terms or not + types = store.getFeatureTypes("transcript", "CDS"); + assertEquals(types.size(), 3); + assertTrue(types.contains("transcript")); + assertTrue(types.contains("mRNA")); + assertTrue(types.contains("CDS")); + + types = store.getFeatureTypes("exon"); + assertTrue(types.isEmpty()); + } + + @Test(groups = "Functional") + public void testGetFeaturesByOntology() + { + SequenceFeaturesI store = new SequenceFeatures(); + List features = store.getFeaturesByOntology(); + assertTrue(features.isEmpty()); + assertTrue(store.getFeaturesByOntology(new String[] {}).isEmpty()); + assertTrue(store.getFeaturesByOntology((String[]) null).isEmpty()); + + SequenceFeature sf1 = new SequenceFeature("transcript", "desc", 10, 20, + Float.NaN, null); + store.add(sf1); + + // mRNA isA transcript; added here 'as if' non-positional + // just to show that non-positional features are included in results + SequenceFeature sf2 = new SequenceFeature("mRNA", "desc", 0, 0, + Float.NaN, null); + store.add(sf2); + + SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 30, 40, + Float.NaN, null); + store.add(sf3); + + features = store.getFeaturesByOntology("transcript"); + assertEquals(features.size(), 2); + assertTrue(features.contains(sf1)); + assertTrue(features.contains(sf2)); + + features = store.getFeaturesByOntology("mRNA"); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf2)); + + features = store.getFeaturesByOntology("mRNA", "Pfam"); + assertEquals(features.size(), 2); + assertTrue(features.contains(sf2)); + assertTrue(features.contains(sf3)); + } + + @Test(groups = "Functional") + public void testSortFeatures() + { + List sfs = new ArrayList(); + SequenceFeature sf1 = new SequenceFeature("Pfam", "desc", 30, 80, + Float.NaN, null); + sfs.add(sf1); + SequenceFeature sf2 = new SequenceFeature("Rfam", "desc", 40, 50, + Float.NaN, null); + sfs.add(sf2); + SequenceFeature sf3 = new SequenceFeature("Rfam", "desc", 50, 60, + Float.NaN, null); + sfs.add(sf3); + + // sort by end position descending + SequenceFeatures.sortFeatures(sfs, false); + assertSame(sfs.get(0), sf1); + assertSame(sfs.get(1), sf3); + assertSame(sfs.get(2), sf2); + + // sort by start position ascending + SequenceFeatures.sortFeatures(sfs, true); + assertSame(sfs.get(0), sf1); + assertSame(sfs.get(1), sf2); + assertSame(sfs.get(2), sf3); + } + + @Test(groups = "Functional") + public void testGetFeaturesForGroup() + { + SequenceFeaturesI store = new SequenceFeatures(); + + List features = store.getFeaturesForGroup(true, null); + assertTrue(features.isEmpty()); + assertTrue(store.getFeaturesForGroup(false, null).isEmpty()); + assertTrue(store.getFeaturesForGroup(true, "Uniprot").isEmpty()); + assertTrue(store.getFeaturesForGroup(false, "Uniprot").isEmpty()); + + SequenceFeature sf1 = new SequenceFeature("Pfam", "desc", 4, 10, 0f, + null); + SequenceFeature sf2 = new SequenceFeature("Pfam", "desc", 0, 0, 0f, + null); + SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 4, 10, 0f, + "Uniprot"); + SequenceFeature sf4 = new SequenceFeature("Metal", "desc", 0, 0, 0f, + "Rfam"); + SequenceFeature sf5 = new SequenceFeature("Cath", "desc", 5, 15, 0f, + null); + store.add(sf1); + store.add(sf2); + store.add(sf3); + store.add(sf4); + store.add(sf5); + + // positional features for null group, any type + features = store.getFeaturesForGroup(true, null); + assertEquals(features.size(), 2); + assertTrue(features.contains(sf1)); + assertTrue(features.contains(sf5)); + + // positional features for null group, specified type + features = store.getFeaturesForGroup(true, null, new String[] { "Pfam", + "Xfam" }); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf1)); + features = store.getFeaturesForGroup(true, null, new String[] { "Pfam", + "Xfam", "Cath" }); + assertEquals(features.size(), 2); + assertTrue(features.contains(sf1)); + assertTrue(features.contains(sf5)); + + // positional features for non-null group, any type + features = store.getFeaturesForGroup(true, "Uniprot"); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf3)); + assertTrue(store.getFeaturesForGroup(true, "Rfam").isEmpty()); + + // positional features for non-null group, specified type + features = store.getFeaturesForGroup(true, "Uniprot", "Pfam", "Xfam", + "Rfam"); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf3)); + assertTrue(store.getFeaturesForGroup(true, "Uniprot", "Cath").isEmpty()); + + // non-positional features for null group, any type + features = store.getFeaturesForGroup(false, null); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf2)); + + // non-positional features for null group, specified type + features = store.getFeaturesForGroup(false, null, "Pfam", "Xfam"); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf2)); + assertTrue(store.getFeaturesForGroup(false, null, "Cath").isEmpty()); + + // non-positional features for non-null group, any type + features = store.getFeaturesForGroup(false, "Rfam"); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf4)); + assertTrue(store.getFeaturesForGroup(false, "Uniprot").isEmpty()); + + // non-positional features for non-null group, specified type + features = store.getFeaturesForGroup(false, "Rfam", "Pfam", "Metal"); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf4)); + assertTrue(store.getFeaturesForGroup(false, "Rfam", "Cath", "Pfam") + .isEmpty()); + } + + @Test(groups = "Functional") + public void testShiftFeatures() + { + SequenceFeatures store = new SequenceFeatures(); + assertFalse(store.shiftFeatures(1)); + + SequenceFeature sf1 = new SequenceFeature("Cath", "", 2, 5, 0f, null); + store.add(sf1); + // nested feature: + SequenceFeature sf2 = new SequenceFeature("Metal", "", 8, 14, 0f, null); + store.add(sf2); + // contact feature: + SequenceFeature sf3 = new SequenceFeature("Disulfide bond", "", 23, 32, + 0f, null); + store.add(sf3); + // non-positional feature: + SequenceFeature sf4 = new SequenceFeature("Pfam", "", 0, 0, 0f, null); + store.add(sf4); + + /* + * shift features right by 5 + */ + assertTrue(store.shiftFeatures(5)); + + // non-positional features untouched: + List nonPos = store.getNonPositionalFeatures(); + assertEquals(nonPos.size(), 1); + assertTrue(nonPos.contains(sf4)); + + // positional features are replaced + List pos = store.getPositionalFeatures(); + assertEquals(pos.size(), 3); + assertFalse(pos.contains(sf1)); + assertFalse(pos.contains(sf2)); + assertFalse(pos.contains(sf3)); + SequenceFeatures.sortFeatures(pos, true); // ascending start pos + assertEquals(pos.get(0).getBegin(), 7); + assertEquals(pos.get(0).getEnd(), 10); + assertEquals(pos.get(0).getType(), "Cath"); + assertEquals(pos.get(1).getBegin(), 13); + assertEquals(pos.get(1).getEnd(), 19); + assertEquals(pos.get(1).getType(), "Metal"); + assertEquals(pos.get(2).getBegin(), 28); + assertEquals(pos.get(2).getEnd(), 37); + assertEquals(pos.get(2).getType(), "Disulfide bond"); + + /* + * now shift left by 15 + * feature at [7-10] should be removed + * feature at [13-19] should become [1-4] + */ + assertTrue(store.shiftFeatures(-15)); + pos = store.getPositionalFeatures(); + assertEquals(pos.size(), 2); + SequenceFeatures.sortFeatures(pos, true); + assertEquals(pos.get(0).getBegin(), 1); + assertEquals(pos.get(0).getEnd(), 4); + assertEquals(pos.get(0).getType(), "Metal"); + assertEquals(pos.get(1).getBegin(), 13); + assertEquals(pos.get(1).getEnd(), 22); + assertEquals(pos.get(1).getType(), "Disulfide bond"); + } + + @Test(groups = "Functional") + public void testIsOntologyTerm() + { + SequenceFeatures store = new SequenceFeatures(); + assertTrue(store.isOntologyTerm("gobbledygook")); + assertTrue(store.isOntologyTerm("transcript", "transcript")); + assertTrue(store.isOntologyTerm("mRNA", "transcript")); + assertFalse(store.isOntologyTerm("transcript", "mRNA")); + assertTrue(store.isOntologyTerm("junk", "transcript", "junk")); + assertTrue(store.isOntologyTerm("junk", new String[] {})); + assertTrue(store.isOntologyTerm("junk", (String[]) null)); + } +} diff --git a/test/jalview/ext/ensembl/EnsemblCdnaTest.java b/test/jalview/ext/ensembl/EnsemblCdnaTest.java index fb0204b..6611e05 100644 --- a/test/jalview/ext/ensembl/EnsemblCdnaTest.java +++ b/test/jalview/ext/ensembl/EnsemblCdnaTest.java @@ -212,14 +212,16 @@ public class EnsemblCdnaTest 20500, 0f, null); assertFalse(testee.retainFeature(sf, accId)); - sf.setType("aberrant_processed_transcript"); + sf = new SequenceFeature("aberrant_processed_transcript", "", 20000, + 20500, 0f, null); assertFalse(testee.retainFeature(sf, accId)); - sf.setType("NMD_transcript_variant"); + sf = new SequenceFeature("NMD_transcript_variant", "", 20000, 20500, + 0f, null); assertFalse(testee.retainFeature(sf, accId)); // other feature with no parent is retained - sf.setType("sequence_variant"); + sf = new SequenceFeature("sequence_variant", "", 20000, 20500, 0f, null); assertTrue(testee.retainFeature(sf, accId)); // other feature with desired parent is retained @@ -254,15 +256,18 @@ public class EnsemblCdnaTest assertTrue(testee.identifiesSequence(sf, accId)); // exon sub-type with right parent is valid - sf.setType("coding_exon"); + sf = new SequenceFeature("coding_exon", "", 1, 2, 0f, null); + sf.setValue("Parent", "transcript:" + accId); assertTrue(testee.identifiesSequence(sf, accId)); // transcript not valid: - sf.setType("transcript"); + sf = new SequenceFeature("transcript", "", 1, 2, 0f, null); + sf.setValue("Parent", "transcript:" + accId); assertFalse(testee.identifiesSequence(sf, accId)); // CDS not valid: - sf.setType("CDS"); + sf = new SequenceFeature("CDS", "", 1, 2, 0f, null); + sf.setValue("Parent", "transcript:" + accId); assertFalse(testee.identifiesSequence(sf, accId)); } diff --git a/test/jalview/ext/ensembl/EnsemblCdsTest.java b/test/jalview/ext/ensembl/EnsemblCdsTest.java index b7f9f8d..8482c90 100644 --- a/test/jalview/ext/ensembl/EnsemblCdsTest.java +++ b/test/jalview/ext/ensembl/EnsemblCdsTest.java @@ -130,11 +130,12 @@ public class EnsemblCdsTest null); assertFalse(testee.retainFeature(sf, accId)); - sf.setType("CDS_predicted"); + sf = new SequenceFeature("CDS_predicted", "", 20000, 20500, 0f, null); assertFalse(testee.retainFeature(sf, accId)); // other feature with no parent is retained - sf.setType("sequence_variant"); + sf = new SequenceFeature("CDS_psequence_variantredicted", "", 20000, + 20500, 0f, null); assertTrue(testee.retainFeature(sf, accId)); // other feature with desired parent is retained @@ -169,15 +170,18 @@ public class EnsemblCdsTest assertTrue(testee.identifiesSequence(sf, accId)); // cds sub-type with right parent is valid - sf.setType("CDS_predicted"); + sf = new SequenceFeature("CDS_predicted", "", 1, 2, 0f, null); + sf.setValue("Parent", "transcript:" + accId); assertTrue(testee.identifiesSequence(sf, accId)); // transcript not valid: - sf.setType("transcript"); + sf = new SequenceFeature("transcript", "", 1, 2, 0f, null); + sf.setValue("Parent", "transcript:" + accId); assertFalse(testee.identifiesSequence(sf, accId)); // exon not valid: - sf.setType("exon"); + sf = new SequenceFeature("exon", "", 1, 2, 0f, null); + sf.setValue("Parent", "transcript:" + accId); assertFalse(testee.identifiesSequence(sf, accId)); } diff --git a/test/jalview/ext/ensembl/EnsemblGeneTest.java b/test/jalview/ext/ensembl/EnsemblGeneTest.java index 6cfd85b..a8c491c 100644 --- a/test/jalview/ext/ensembl/EnsemblGeneTest.java +++ b/test/jalview/ext/ensembl/EnsemblGeneTest.java @@ -22,7 +22,6 @@ package jalview.ext.ensembl; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertFalse; -import static org.testng.AssertJUnit.assertSame; import static org.testng.AssertJUnit.assertTrue; import jalview.api.FeatureSettingsModelI; @@ -76,7 +75,9 @@ public class EnsemblGeneTest genomic.setEnd(50000); String geneId = "ABC123"; - // gene at (start+10000) length 501 + // gene at (start+20000) length 501 + // should be ignored - the first 'gene' found defines the whole range + // (note features are found in position order, not addition order) SequenceFeature sf = new SequenceFeature("gene", "", 20000, 20500, 0f, null); sf.setValue("ID", "gene:" + geneId); @@ -84,7 +85,6 @@ public class EnsemblGeneTest genomic.addSequenceFeature(sf); // gene at (start + 10500) length 101 - // should be ignored - the first 'gene' found defines the whole range sf = new SequenceFeature("gene", "", 10500, 10600, 0f, null); sf.setValue("ID", "gene:" + geneId); sf.setStrand("+"); @@ -94,13 +94,13 @@ public class EnsemblGeneTest 23); List fromRanges = ranges.getFromRanges(); assertEquals(1, fromRanges.size()); - assertEquals(20000, fromRanges.get(0)[0]); - assertEquals(20500, fromRanges.get(0)[1]); + assertEquals(10500, fromRanges.get(0)[0]); + assertEquals(10600, fromRanges.get(0)[1]); // to range should start from given start numbering List toRanges = ranges.getToRanges(); assertEquals(1, toRanges.size()); assertEquals(23, toRanges.get(0)[0]); - assertEquals(523, toRanges.get(0)[1]); + assertEquals(123, toRanges.get(0)[1]); } /** @@ -115,7 +115,9 @@ public class EnsemblGeneTest genomic.setEnd(50000); String geneId = "ABC123"; - // gene at (start+10000) length 501 + // gene at (start+20000) length 501 + // should be ignored - the first 'gene' found defines the whole range + // (real data would only have one such feature) SequenceFeature sf = new SequenceFeature("ncRNA_gene", "", 20000, 20500, 0f, null); sf.setValue("ID", "gene:" + geneId); @@ -123,8 +125,6 @@ public class EnsemblGeneTest genomic.addSequenceFeature(sf); // gene at (start + 10500) length 101 - // should be ignored - the first 'gene' found defines the whole range - // (real data would only have one such feature) sf = new SequenceFeature("gene", "", 10500, 10600, 0f, null); sf.setValue("ID", "gene:" + geneId); sf.setStrand("+"); @@ -135,13 +135,13 @@ public class EnsemblGeneTest List fromRanges = ranges.getFromRanges(); assertEquals(1, fromRanges.size()); // from range on reverse strand: - assertEquals(20500, fromRanges.get(0)[0]); - assertEquals(20000, fromRanges.get(0)[1]); + assertEquals(10500, fromRanges.get(0)[0]); + assertEquals(10600, fromRanges.get(0)[1]); // to range should start from given start numbering List toRanges = ranges.getToRanges(); assertEquals(1, toRanges.size()); assertEquals(23, toRanges.get(0)[0]); - assertEquals(523, toRanges.get(0)[1]); + assertEquals(123, toRanges.get(0)[1]); } /** @@ -164,7 +164,7 @@ public class EnsemblGeneTest genomic.addSequenceFeature(sf1); // transcript sub-type feature - SequenceFeature sf2 = new SequenceFeature("snRNA", "", 20000, 20500, + SequenceFeature sf2 = new SequenceFeature("snRNA", "", 21000, 21500, 0f, null); sf2.setValue("Parent", "gene:" + geneId); sf2.setValue("transcript_id", "transcript2"); @@ -172,13 +172,13 @@ public class EnsemblGeneTest // NMD_transcript_variant treated like transcript in Ensembl SequenceFeature sf3 = new SequenceFeature("NMD_transcript_variant", "", - 20000, 20500, 0f, null); + 22000, 22500, 0f, null); sf3.setValue("Parent", "gene:" + geneId); sf3.setValue("transcript_id", "transcript3"); genomic.addSequenceFeature(sf3); // transcript for a different gene - ignored - SequenceFeature sf4 = new SequenceFeature("snRNA", "", 20000, 20500, + SequenceFeature sf4 = new SequenceFeature("snRNA", "", 23000, 23500, 0f, null); sf4.setValue("Parent", "gene:XYZ"); sf4.setValue("transcript_id", "transcript4"); @@ -192,9 +192,9 @@ public class EnsemblGeneTest List features = testee.getTranscriptFeatures(geneId, genomic); assertEquals(3, features.size()); - assertSame(sf1, features.get(0)); - assertSame(sf2, features.get(1)); - assertSame(sf3, features.get(2)); + assertTrue(features.contains(sf1)); + assertTrue(features.contains(sf2)); + assertTrue(features.contains(sf3)); } /** @@ -211,22 +211,24 @@ public class EnsemblGeneTest sf.setValue("ID", "gene:" + geneId); assertFalse(testee.retainFeature(sf, geneId)); - sf.setType("transcript"); + sf = new SequenceFeature("transcript", "", 20000, 20500, 0f, null); sf.setValue("Parent", "gene:" + geneId); assertTrue(testee.retainFeature(sf, geneId)); - sf.setType("mature_transcript"); + sf = new SequenceFeature("mature_transcript", "", 20000, 20500, 0f, + null); sf.setValue("Parent", "gene:" + geneId); assertTrue(testee.retainFeature(sf, geneId)); - sf.setType("NMD_transcript_variant"); + sf = new SequenceFeature("NMD_transcript_variant", "", 20000, 20500, + 0f, null); sf.setValue("Parent", "gene:" + geneId); assertTrue(testee.retainFeature(sf, geneId)); sf.setValue("Parent", "gene:XYZ"); assertFalse(testee.retainFeature(sf, geneId)); - sf.setType("anything"); + sf = new SequenceFeature("anything", "", 20000, 20500, 0f, null); assertTrue(testee.retainFeature(sf, geneId)); } @@ -253,15 +255,18 @@ public class EnsemblGeneTest assertTrue(testee.identifiesSequence(sf, accId)); // gene sub-type with right ID is valid - sf.setType("snRNA_gene"); + sf = new SequenceFeature("snRNA_gene", "", 1, 2, 0f, null); + sf.setValue("ID", "gene:" + accId); assertTrue(testee.identifiesSequence(sf, accId)); // transcript not valid: - sf.setType("transcript"); + sf = new SequenceFeature("transcript", "", 1, 2, 0f, null); + sf.setValue("ID", "gene:" + accId); assertFalse(testee.identifiesSequence(sf, accId)); // exon not valid: - sf.setType("exon"); + sf = new SequenceFeature("exon", "", 1, 2, 0f, null); + sf.setValue("ID", "gene:" + accId); assertFalse(testee.identifiesSequence(sf, accId)); } diff --git a/test/jalview/ext/ensembl/EnsemblGenomeTest.java b/test/jalview/ext/ensembl/EnsemblGenomeTest.java index 654797c..8687da9 100644 --- a/test/jalview/ext/ensembl/EnsemblGenomeTest.java +++ b/test/jalview/ext/ensembl/EnsemblGenomeTest.java @@ -136,14 +136,16 @@ public class EnsemblGenomeTest 20500, 0f, null); assertFalse(testee.retainFeature(sf, accId)); - sf.setType("mature_transcript"); + sf = new SequenceFeature("mature_transcript", "", 20000, 20500, 0f, + null); assertFalse(testee.retainFeature(sf, accId)); - sf.setType("NMD_transcript_variant"); + sf = new SequenceFeature("NMD_transcript_variant", "", 20000, 20500, + 0f, null); assertFalse(testee.retainFeature(sf, accId)); // other feature with no parent is kept - sf.setType("anything"); + sf = new SequenceFeature("anything", "", 20000, 20500, 0f, null); assertTrue(testee.retainFeature(sf, accId)); // other feature with correct parent is kept @@ -179,19 +181,23 @@ public class EnsemblGenomeTest assertTrue(testee.identifiesSequence(sf, accId)); // transcript sub-type with right ID is valid - sf.setType("ncRNA"); + sf = new SequenceFeature("ncRNA", "", 1, 2, 0f, null); + sf.setValue("ID", "transcript:" + accId); assertTrue(testee.identifiesSequence(sf, accId)); // Ensembl treats NMD_transcript_variant as if a transcript - sf.setType("NMD_transcript_variant"); + sf = new SequenceFeature("NMD_transcript_variant", "", 1, 2, 0f, null); + sf.setValue("ID", "transcript:" + accId); assertTrue(testee.identifiesSequence(sf, accId)); // gene not valid: - sf.setType("gene"); + sf = new SequenceFeature("gene", "", 1, 2, 0f, null); + sf.setValue("ID", "transcript:" + accId); assertFalse(testee.identifiesSequence(sf, accId)); // exon not valid: - sf.setType("exon"); + sf = new SequenceFeature("exon", "", 1, 2, 0f, null); + sf.setValue("ID", "transcript:" + accId); assertFalse(testee.identifiesSequence(sf, accId)); } diff --git a/test/jalview/ext/ensembl/EnsemblSeqProxyTest.java b/test/jalview/ext/ensembl/EnsemblSeqProxyTest.java index e977233..aa2c315 100644 --- a/test/jalview/ext/ensembl/EnsemblSeqProxyTest.java +++ b/test/jalview/ext/ensembl/EnsemblSeqProxyTest.java @@ -22,12 +22,13 @@ package jalview.ext.ensembl; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertSame; import static org.testng.AssertJUnit.assertTrue; -import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals; import jalview.datamodel.Alignment; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.SequenceFeatures; import jalview.gui.JvOptionPane; import jalview.io.DataSourceType; import jalview.io.FastaFile; @@ -37,6 +38,7 @@ import jalview.io.gff.SequenceOntologyLite; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.List; import org.testng.Assert; import org.testng.annotations.AfterClass; @@ -269,15 +271,22 @@ public class EnsemblSeqProxyTest SequenceFeature sf2 = new SequenceFeature("", "", 8, 12, 0f, null); SequenceFeature sf3 = new SequenceFeature("", "", 8, 13, 0f, null); SequenceFeature sf4 = new SequenceFeature("", "", 11, 11, 0f, null); - SequenceFeature[] sfs = new SequenceFeature[] { sf1, sf2, sf3, sf4 }; + List sfs = Arrays.asList(new SequenceFeature[] { sf1, + sf2, sf3, sf4 }); // sort by start position ascending (forward strand) // sf2 and sf3 tie and should not be reordered by sorting - EnsemblSeqProxy.sortFeatures(sfs, true); - assertArrayEquals(new SequenceFeature[] { sf2, sf3, sf1, sf4 }, sfs); + SequenceFeatures.sortFeatures(sfs, true); + assertSame(sfs.get(0), sf2); + assertSame(sfs.get(1), sf3); + assertSame(sfs.get(2), sf1); + assertSame(sfs.get(3), sf4); // sort by end position descending (reverse strand) - EnsemblSeqProxy.sortFeatures(sfs, false); - assertArrayEquals(new SequenceFeature[] { sf1, sf3, sf2, sf4 }, sfs); + SequenceFeatures.sortFeatures(sfs, false); + assertSame(sfs.get(0), sf1); + assertSame(sfs.get(1), sf3); + assertSame(sfs.get(2), sf2); + assertSame(sfs.get(3), sf4); } } diff --git a/test/jalview/ext/jmol/JmolParserTest.java b/test/jalview/ext/jmol/JmolParserTest.java index 36e9b20..f5e637c 100644 --- a/test/jalview/ext/jmol/JmolParserTest.java +++ b/test/jalview/ext/jmol/JmolParserTest.java @@ -277,10 +277,9 @@ public class JmolParserTest /* * the ID is also the group for features derived from structure data */ - assertNotNull(structureData.getSeqs().get(0).getSequenceFeatures()[0].featureGroup); - assertEquals( - structureData.getSeqs().get(0).getSequenceFeatures()[0].featureGroup, - "localstruct"); - + String featureGroup = structureData.getSeqs().get(0) + .getSequenceFeatures().get(0).featureGroup; + assertNotNull(featureGroup); + assertEquals(featureGroup, "localstruct"); } } diff --git a/test/jalview/ext/paradise/TestAnnotate3D.java b/test/jalview/ext/paradise/TestAnnotate3D.java index 85fc039..c6c1a29 100644 --- a/test/jalview/ext/paradise/TestAnnotate3D.java +++ b/test/jalview/ext/paradise/TestAnnotate3D.java @@ -152,10 +152,10 @@ public class TestAnnotate3D { { SequenceI struseq = null; - String sq_ = new String(sq.getSequence()).toLowerCase(); + String sq_ = sq.getSequenceAsString().toLowerCase(); for (SequenceI _struseq : pdbf.getSeqsAsArray()) { - final String lowerCase = new String(_struseq.getSequence()) + final String lowerCase = _struseq.getSequenceAsString() .toLowerCase(); if (lowerCase.equals(sq_)) { diff --git a/test/jalview/ext/rbvi/chimera/JalviewChimeraView.java b/test/jalview/ext/rbvi/chimera/JalviewChimeraView.java index 29fd092..734f7eb 100644 --- a/test/jalview/ext/rbvi/chimera/JalviewChimeraView.java +++ b/test/jalview/ext/rbvi/chimera/JalviewChimeraView.java @@ -39,6 +39,7 @@ import jalview.gui.JvOptionPane; import jalview.gui.Preferences; import jalview.gui.StructureViewer; import jalview.gui.StructureViewer.ViewerType; +import jalview.io.DataSourceType; import jalview.io.FileLoader; import jalview.structure.StructureMapping; import jalview.structure.StructureSelectionManager; @@ -50,7 +51,6 @@ import java.io.File; import java.io.IOException; import java.util.List; import java.util.Vector; -import jalview.io.DataSourceType; import org.testng.annotations.AfterClass; import org.testng.annotations.AfterMethod; @@ -440,15 +440,18 @@ public class JalviewChimeraView binding.copyStructureAttributesToFeatures("phi", af.getViewport() .getAlignPanel()); fr.setVisible("phi"); - List fs = fr.findFeaturesAtRes(fer2Arath, 54); + List fs = fer2Arath.getFeatures().findFeatures(54, 54); assertEquals(fs.size(), 3); - assertEquals(fs.get(0).getType(), "RESNUM"); - assertEquals(fs.get(1).getType(), "phi"); - assertEquals(fs.get(2).getType(), "phi"); - assertEquals(fs.get(1).getDescription(), "A"); // chain - assertEquals(fs.get(2).getDescription(), "B"); - assertEquals(fs.get(1).getScore(), -131.0713f, 0.001f); - assertEquals(fs.get(2).getScore(), -127.39512, 0.001f); + /* + * order of returned features is not guaranteed + */ + assertTrue("RESNUM".equals(fs.get(0).getType()) + || "RESNUM".equals(fs.get(1).getType()) + || "RESNUM".equals(fs.get(2).getType())); + assertTrue(fs.contains(new SequenceFeature("phi", "A", 54, 54, + -131.0713f, "Chimera"))); + assertTrue(fs.contains(new SequenceFeature("phi", "B", 54, 54, + -127.39512f, "Chimera"))); /* * tear down - also in AfterMethod @@ -470,7 +473,8 @@ public class JalviewChimeraView int res, String featureType) { String where = "at position " + res; - List fs = fr.findFeaturesAtRes(seq, res); + List fs = seq.getFeatures().findFeatures(res, res); + assertEquals(fs.size(), 2, where); assertEquals(fs.get(0).getType(), "RESNUM", where); SequenceFeature sf = fs.get(1); diff --git a/test/jalview/io/AnnotatedPDBFileInputTest.java b/test/jalview/io/AnnotatedPDBFileInputTest.java index d8ae999..e14a478 100644 --- a/test/jalview/io/AnnotatedPDBFileInputTest.java +++ b/test/jalview/io/AnnotatedPDBFileInputTest.java @@ -30,12 +30,14 @@ import jalview.datamodel.AlignmentI; import jalview.datamodel.PDBEntry; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.SequenceFeatures; import jalview.gui.AlignFrame; import jalview.gui.JvOptionPane; import jalview.structure.StructureImportSettings; import jalview.structure.StructureImportSettings.StructureParser; import java.io.File; +import java.util.List; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; @@ -127,32 +129,35 @@ public class AnnotatedPDBFileInputTest /* * 1GAQ/A */ - SequenceFeature[] sf = al.getSequenceAt(0).getSequenceFeatures(); - assertEquals(296, sf.length); - assertEquals("RESNUM", sf[0].getType()); - assertEquals("GLU: 19 1gaqA", sf[0].getDescription()); - assertEquals("RESNUM", sf[295].getType()); - assertEquals("TYR: 314 1gaqA", sf[295].getDescription()); + List sf = al.getSequenceAt(0).getSequenceFeatures(); + SequenceFeatures.sortFeatures(sf, true); + assertEquals(296, sf.size()); + assertEquals("RESNUM", sf.get(0).getType()); + assertEquals("GLU: 19 1gaqA", sf.get(0).getDescription()); + assertEquals("RESNUM", sf.get(295).getType()); + assertEquals("TYR: 314 1gaqA", sf.get(295).getDescription()); /* * 1GAQ/B */ sf = al.getSequenceAt(1).getSequenceFeatures(); - assertEquals(98, sf.length); - assertEquals("RESNUM", sf[0].getType()); - assertEquals("ALA: 1 1gaqB", sf[0].getDescription()); - assertEquals("RESNUM", sf[97].getType()); - assertEquals("ALA: 98 1gaqB", sf[97].getDescription()); + SequenceFeatures.sortFeatures(sf, true); + assertEquals(98, sf.size()); + assertEquals("RESNUM", sf.get(0).getType()); + assertEquals("ALA: 1 1gaqB", sf.get(0).getDescription()); + assertEquals("RESNUM", sf.get(97).getType()); + assertEquals("ALA: 98 1gaqB", sf.get(97).getDescription()); /* * 1GAQ/C */ sf = al.getSequenceAt(2).getSequenceFeatures(); - assertEquals(296, sf.length); - assertEquals("RESNUM", sf[0].getType()); - assertEquals("GLU: 19 1gaqC", sf[0].getDescription()); - assertEquals("RESNUM", sf[295].getType()); - assertEquals("TYR: 314 1gaqC", sf[295].getDescription()); + SequenceFeatures.sortFeatures(sf, true); + assertEquals(296, sf.size()); + assertEquals("RESNUM", sf.get(0).getType()); + assertEquals("GLU: 19 1gaqC", sf.get(0).getDescription()); + assertEquals("RESNUM", sf.get(295).getType()); + assertEquals("TYR: 314 1gaqC", sf.get(295).getDescription()); } @Test(groups = { "Functional" }) diff --git a/test/jalview/io/FeaturesFileTest.java b/test/jalview/io/FeaturesFileTest.java index cc7dca0..3b688db 100644 --- a/test/jalview/io/FeaturesFileTest.java +++ b/test/jalview/io/FeaturesFileTest.java @@ -23,7 +23,6 @@ package jalview.io; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertFalse; import static org.testng.AssertJUnit.assertNotNull; -import static org.testng.AssertJUnit.assertNull; import static org.testng.AssertJUnit.assertTrue; import jalview.api.FeatureColourI; @@ -33,12 +32,17 @@ import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceDummy; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.SequenceFeatures; import jalview.gui.AlignFrame; import jalview.gui.JvOptionPane; import java.awt.Color; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; import java.util.Map; import org.testng.annotations.BeforeClass; @@ -86,10 +90,15 @@ public class FeaturesFileTest /* * verify (some) features on sequences */ - SequenceFeature[] sfs = al.getSequenceAt(0).getDatasetSequence() + List sfs = al.getSequenceAt(0).getDatasetSequence() .getSequenceFeatures(); // FER_CAPAA - assertEquals(8, sfs.length); - SequenceFeature sf = sfs[0]; + SequenceFeatures.sortFeatures(sfs, true); + assertEquals(8, sfs.size()); + + /* + * verify (in ascending start position order) + */ + SequenceFeature sf = sfs.get(0); assertEquals("Pfam family%LINK%", sf.description); assertEquals(0, sf.begin); assertEquals(0, sf.end); @@ -99,46 +108,52 @@ public class FeaturesFileTest assertEquals("Pfam family|http://pfam.xfam.org/family/PF00111", sf.links.get(0)); - sf = sfs[1]; + sf = sfs.get(1); + assertEquals("Ferredoxin_fold Status: True Positive ", sf.description); + assertEquals(3, sf.begin); + assertEquals(93, sf.end); + assertEquals("uniprot", sf.featureGroup); + assertEquals("Cath", sf.type); + + sf = sfs.get(2); + assertEquals("Fer2 Status: True Positive Pfam 8_8%LINK%", + sf.description); + assertEquals("Pfam 8_8|http://pfam.xfam.org/family/PF00111", + sf.links.get(0)); + assertEquals(8, sf.begin); + assertEquals(83, sf.end); + assertEquals("uniprot", sf.featureGroup); + assertEquals("Pfam", sf.type); + + sf = sfs.get(3); assertEquals("Iron-sulfur (2Fe-2S)", sf.description); assertEquals(39, sf.begin); assertEquals(39, sf.end); assertEquals("uniprot", sf.featureGroup); assertEquals("METAL", sf.type); - sf = sfs[2]; + + sf = sfs.get(4); assertEquals("Iron-sulfur (2Fe-2S)", sf.description); assertEquals(44, sf.begin); assertEquals(44, sf.end); assertEquals("uniprot", sf.featureGroup); assertEquals("METAL", sf.type); - sf = sfs[3]; + + sf = sfs.get(5); assertEquals("Iron-sulfur (2Fe-2S)", sf.description); assertEquals(47, sf.begin); assertEquals(47, sf.end); assertEquals("uniprot", sf.featureGroup); assertEquals("METAL", sf.type); - sf = sfs[4]; + + sf = sfs.get(6); assertEquals("Iron-sulfur (2Fe-2S)", sf.description); assertEquals(77, sf.begin); assertEquals(77, sf.end); assertEquals("uniprot", sf.featureGroup); assertEquals("METAL", sf.type); - sf = sfs[5]; - assertEquals("Fer2 Status: True Positive Pfam 8_8%LINK%", - sf.description); - assertEquals("Pfam 8_8|http://pfam.xfam.org/family/PF00111", - sf.links.get(0)); - assertEquals(8, sf.begin); - assertEquals(83, sf.end); - assertEquals("uniprot", sf.featureGroup); - assertEquals("Pfam", sf.type); - sf = sfs[6]; - assertEquals("Ferredoxin_fold Status: True Positive ", sf.description); - assertEquals(3, sf.begin); - assertEquals(93, sf.end); - assertEquals("uniprot", sf.featureGroup); - assertEquals("Cath", sf.type); - sf = sfs[7]; + + sf = sfs.get(7); assertEquals( "High confidence server. Only hits with scores over 0.8 are reported. PHOSPHORYLATION (T) 89_8%LINK%", sf.description); @@ -181,10 +196,10 @@ public class FeaturesFileTest assertEquals(colours.get("METAL").getColour(), new Color(0xcc9900)); // verify feature on FER_CAPAA - SequenceFeature[] sfs = al.getSequenceAt(0).getDatasetSequence() + List sfs = al.getSequenceAt(0).getDatasetSequence() .getSequenceFeatures(); - assertEquals(1, sfs.length); - SequenceFeature sf = sfs[0]; + assertEquals(1, sfs.size()); + SequenceFeature sf = sfs.get(0); assertEquals("Iron-sulfur,2Fe-2S", sf.description); assertEquals(44, sf.begin); assertEquals(45, sf.end); @@ -194,8 +209,8 @@ public class FeaturesFileTest // verify feature on FER1_SOLLC sfs = al.getSequenceAt(2).getDatasetSequence().getSequenceFeatures(); - assertEquals(1, sfs.length); - sf = sfs[0]; + assertEquals(1, sfs.size()); + sf = sfs.get(0); assertEquals("uniprot", sf.description); assertEquals(55, sf.begin); assertEquals(130, sf.end); @@ -242,10 +257,10 @@ public class FeaturesFileTest featuresFile.parse(al.getDataset(), colours, true)); // verify feature on FER_CAPAA - SequenceFeature[] sfs = al.getSequenceAt(0).getDatasetSequence() + List sfs = al.getSequenceAt(0).getDatasetSequence() .getSequenceFeatures(); - assertEquals(1, sfs.length); - SequenceFeature sf = sfs[0]; + assertEquals(1, sfs.size()); + SequenceFeature sf = sfs.get(0); // description parsed from Note attribute assertEquals("Iron-sulfur (2Fe-2S),another note", sf.description); assertEquals(39, sf.begin); @@ -258,8 +273,8 @@ public class FeaturesFileTest // verify feature on FER1_SOLLC1 sfs = al.getSequenceAt(2).getDatasetSequence().getSequenceFeatures(); - assertEquals(1, sfs.length); - sf = sfs[0]; + assertEquals(1, sfs.size()); + sf = sfs.get(0); // ID used for description if available assertEquals("$23", sf.description); assertEquals(55, sf.begin); @@ -295,10 +310,10 @@ public class FeaturesFileTest featuresFile.parse(al.getDataset(), colours, true)); // verify FER_CAPAA feature - SequenceFeature[] sfs = al.getSequenceAt(0).getDatasetSequence() + List sfs = al.getSequenceAt(0).getDatasetSequence() .getSequenceFeatures(); - assertEquals(1, sfs.length); - SequenceFeature sf = sfs[0]; + assertEquals(1, sfs.size()); + SequenceFeature sf = sfs.get(0); assertEquals("Iron-sulfur (2Fe-2S)", sf.description); assertEquals(39, sf.begin); assertEquals(39, sf.end); @@ -306,8 +321,8 @@ public class FeaturesFileTest // verify FER1_SOLLC feature sfs = al.getSequenceAt(2).getDatasetSequence().getSequenceFeatures(); - assertEquals(1, sfs.length); - sf = sfs[0]; + assertEquals(1, sfs.size()); + sf = sfs.get(0); assertEquals("Iron-phosphorus (2Fe-P)", sf.description); assertEquals(86, sf.begin); assertEquals(87, sf.end); @@ -337,14 +352,14 @@ public class FeaturesFileTest assertFalse("dummy replacement buggy for seq2", placeholderseq.equals(seq2.getSequenceAsString())); assertNotNull("No features added to seq1", seq1.getSequenceFeatures()); - assertEquals("Wrong number of features", 3, - seq1.getSequenceFeatures().length); - assertNull(seq2.getSequenceFeatures()); + assertEquals("Wrong number of features", 3, seq1.getSequenceFeatures() + .size()); + assertTrue(seq2.getSequenceFeatures().isEmpty()); assertEquals( "Wrong number of features", 0, seq2.getSequenceFeatures() == null ? 0 : seq2 - .getSequenceFeatures().length); + .getSequenceFeatures().size()); assertTrue( "Expected at least one CDNA/Protein mapping for seq1", dataset.getCodonFrame(seq1) != null @@ -410,6 +425,7 @@ public class FeaturesFileTest + "GAMMA-TURN\tred|0,255,255|20.0|95.0|below|66.0\n" + "Pfam\tred\n" + "STARTGROUP\tuniprot\n" + + "Cath\tFER_CAPAA\t-1\t0\t0\tDomain\n" // non-positional feature + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\n" + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\n" + "Pfam domainPfam_3_4\tFER_CAPAA\t-1\t20\t20\tPfam\n" @@ -419,28 +435,57 @@ public class FeaturesFileTest featuresFile.parse(al.getDataset(), colours, false); /* - * first with no features displayed + * add positional and non-positional features with null and + * empty feature group to check handled correctly + */ + SequenceI seq = al.getSequenceAt(1); // FER_CAPAN + seq.addSequenceFeature(new SequenceFeature("Pfam", "desc1", 0, 0, 1.3f, + null)); + seq.addSequenceFeature(new SequenceFeature("Pfam", "desc2", 4, 9, + Float.NaN, null)); + seq = al.getSequenceAt(2); // FER1_SOLLC + seq.addSequenceFeature(new SequenceFeature("Pfam", "desc3", 0, 0, + Float.NaN, "")); + seq.addSequenceFeature(new SequenceFeature("Pfam", "desc4", 5, 8, + -2.6f, "")); + + /* + * first with no features displayed, exclude non-positional features */ FeatureRenderer fr = af.alignPanel.getFeatureRenderer(); Map visible = fr.getDisplayedFeatureCols(); + List visibleGroups = new ArrayList( + Arrays.asList(new String[] {})); String exported = featuresFile.printJalviewFormat( - al.getSequencesArray(), visible); + al.getSequencesArray(), visible, visibleGroups, false); String expected = "No Features Visible"; assertEquals(expected, exported); /* + * include non-positional features + */ + visibleGroups.add("uniprot"); + exported = featuresFile.printJalviewFormat(al.getSequencesArray(), + visible, visibleGroups, true); + expected = "Cath\tFER_CAPAA\t-1\t0\t0\tDomain\t0.0\n" + + "desc1\tFER_CAPAN\t-1\t0\t0\tPfam\t1.3\n" + + "desc3\tFER1_SOLLC\t-1\t0\t0\tPfam\n" // NaN is not output + + "\nSTARTGROUP\tuniprot\nENDGROUP\tuniprot\n"; + assertEquals(expected, exported); + + /* * set METAL (in uniprot group) and GAMMA-TURN visible, but not Pfam */ fr.setVisible("METAL"); fr.setVisible("GAMMA-TURN"); visible = fr.getDisplayedFeatureCols(); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), - visible); + visible, visibleGroups, false); expected = "METAL\tcc9900\n" + "GAMMA-TURN\tff0000|00ffff|20.0|95.0|below|66.0\n" + "\nSTARTGROUP\tuniprot\n" - + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n" + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n" + + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n" + "ENDGROUP\tuniprot\n"; assertEquals(expected, exported); @@ -450,19 +495,119 @@ public class FeaturesFileTest fr.setVisible("Pfam"); visible = fr.getDisplayedFeatureCols(); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), - visible); + visible, visibleGroups, false); /* - * note the order of feature types is uncontrolled - derives from - * FeaturesDisplayed.featuresDisplayed which is a HashSet + * features are output within group, ordered by sequence and by type */ expected = "METAL\tcc9900\n" + "Pfam\tff0000\n" + "GAMMA-TURN\tff0000|00ffff|20.0|95.0|below|66.0\n" + "\nSTARTGROUP\tuniprot\n" - + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n" + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n" + + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n" + "Pfam domainPfam_3_4\tFER_CAPAA\t-1\t20\t20\tPfam\t0.0\n" - + "ENDGROUP\tuniprot\n"; + + "ENDGROUP\tuniprot\n" + // null / empty group features output after features in named + // groups: + + "desc2\tFER_CAPAN\t-1\t4\t9\tPfam\n" + + "desc4\tFER1_SOLLC\t-1\t5\t8\tPfam\t-2.6\n"; + assertEquals(expected, exported); + } + + @Test(groups = { "Functional" }) + public void testPrintGffFormat() throws Exception + { + File f = new File("examples/uniref50.fa"); + AlignmentI al = readAlignmentFile(f); + AlignFrame af = new AlignFrame(al, 500, 500); + + /* + * no features + */ + FeaturesFile featuresFile = new FeaturesFile(); + FeatureRenderer fr = af.alignPanel.getFeatureRenderer(); + Map visible = new HashMap(); + List visibleGroups = new ArrayList( + Arrays.asList(new String[] {})); + String exported = featuresFile.printGffFormat(al.getSequencesArray(), + visible, visibleGroups, false); + String gffHeader = "##gff-version 2\n"; + assertEquals(gffHeader, exported); + exported = featuresFile.printGffFormat(al.getSequencesArray(), visible, + visibleGroups, true); + assertEquals(gffHeader, exported); + + /* + * add some features + */ + al.getSequenceAt(0).addSequenceFeature( + new SequenceFeature("Domain", "Cath", 0, 0, 0f, "Uniprot")); + al.getSequenceAt(0).addSequenceFeature( + new SequenceFeature("METAL", "Cath", 39, 39, 1.2f, null)); + al.getSequenceAt(1) + .addSequenceFeature( + new SequenceFeature("GAMMA-TURN", "Turn", 36, 38, 2.1f, + "s3dm")); + SequenceFeature sf = new SequenceFeature("Pfam", "", 20, 20, 0f, + "Uniprot"); + sf.setAttributes("x=y;black=white"); + sf.setStrand("+"); + sf.setPhase("2"); + al.getSequenceAt(1).addSequenceFeature(sf); + + /* + * with no features displayed, exclude non-positional features + */ + exported = featuresFile.printGffFormat(al.getSequencesArray(), visible, + visibleGroups, false); + assertEquals(gffHeader, exported); + + /* + * include non-positional features + */ + visibleGroups.add("Uniprot"); + exported = featuresFile.printGffFormat(al.getSequencesArray(), visible, + visibleGroups, true); + String expected = gffHeader + + "FER_CAPAA\tUniprot\tDomain\t0\t0\t0.0\t.\t.\n"; + assertEquals(expected, exported); + + /* + * set METAL (in uniprot group) and GAMMA-TURN visible, but not Pfam + * only Uniprot group visible here... + */ + fr.setVisible("METAL"); + fr.setVisible("GAMMA-TURN"); + visible = fr.getDisplayedFeatureCols(); + exported = featuresFile.printGffFormat(al.getSequencesArray(), visible, + visibleGroups, false); + // METAL feature has null group: description used for column 2 + expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"; + assertEquals(expected, exported); + + /* + * set s3dm group visible + */ + visibleGroups.add("s3dm"); + exported = featuresFile.printGffFormat(al.getSequencesArray(), visible, + visibleGroups, false); + // METAL feature has null group: description used for column 2 + expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n" + + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n"; + assertEquals(expected, exported); + + /* + * now set Pfam visible + */ + fr.setVisible("Pfam"); + visible = fr.getDisplayedFeatureCols(); + exported = featuresFile.printGffFormat(al.getSequencesArray(), visible, + visibleGroups, false); + // Pfam feature columns include strand(+), phase(2), attributes + expected = gffHeader + + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n" + + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n" + + "FER_CAPAN\tUniprot\tPfam\t20\t20\t0.0\t+\t2\tx=y;black=white\n"; assertEquals(expected, exported); } } diff --git a/test/jalview/io/JSONFileTest.java b/test/jalview/io/JSONFileTest.java index e046d94..158c901 100644 --- a/test/jalview/io/JSONFileTest.java +++ b/test/jalview/io/JSONFileTest.java @@ -32,6 +32,7 @@ import jalview.datamodel.Sequence; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceGroup; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.SequenceFeatures; import jalview.gui.AlignFrame; import jalview.gui.JvOptionPane; import jalview.json.binding.biojson.v1.ColourSchemeMapper; @@ -42,6 +43,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import org.testng.Assert; import org.testng.AssertJUnit; @@ -96,6 +98,10 @@ public class JSONFileTest @BeforeTest(alwaysRun = true) public void setup() throws Exception { + /* + * construct expected values + * nb this have to match the data in examples/example.json + */ // create and add sequences Sequence[] seqs = new Sequence[5]; seqs[0] = new Sequence("FER_CAPAN", @@ -115,14 +121,18 @@ public class JSONFileTest // create and add sequence features SequenceFeature seqFeature2 = new SequenceFeature("feature_x", - "desciption", "status", 6, 15, "Jalview"); + "theDesc", 6, 15, "Jalview"); SequenceFeature seqFeature3 = new SequenceFeature("feature_x", - "desciption", "status", 9, 18, "Jalview"); + "theDesc", 9, 18, "Jalview"); SequenceFeature seqFeature4 = new SequenceFeature("feature_x", - "desciption", "status", 9, 18, "Jalview"); + "theDesc", 9, 18, "Jalview"); + // non-positional feature: + SequenceFeature seqFeature5 = new SequenceFeature("Domain", + "My description", 0, 0, "Pfam"); seqs[2].addSequenceFeature(seqFeature2); seqs[3].addSequenceFeature(seqFeature3); seqs[4].addSequenceFeature(seqFeature4); + seqs[2].addSequenceFeature(seqFeature5); for (Sequence seq : seqs) { @@ -456,7 +466,7 @@ public class JSONFileTest return true; } - public boolean isSeqMatched(SequenceI expectedSeq, SequenceI actualSeq) + boolean isSeqMatched(SequenceI expectedSeq, SequenceI actualSeq) { System.out.println("Testing >>> " + actualSeq.getName()); @@ -490,14 +500,19 @@ public class JSONFileTest + actualGrp.getStartRes()); System.out.println(expectedGrp.getEndRes() + " | " + actualGrp.getEndRes()); - System.out.println(expectedGrp.cs + " | " + actualGrp.cs); + System.out.println(expectedGrp.cs.getColourScheme() + " | " + + actualGrp.cs.getColourScheme()); + boolean colourSchemeMatches = (expectedGrp.cs.getColourScheme() == null && actualGrp.cs + .getColourScheme() == null) + || expectedGrp.cs.getColourScheme().getClass() + .equals(actualGrp.cs.getColourScheme().getClass()); if (expectedGrp.getName().equals(actualGrp.getName()) && expectedGrp.getColourText() == actualGrp.getColourText() && expectedGrp.getDisplayBoxes() == actualGrp.getDisplayBoxes() && expectedGrp.getIgnoreGapsConsensus() == actualGrp .getIgnoreGapsConsensus() - && (expectedGrp.cs.getClass().equals(actualGrp.cs.getClass())) + && colourSchemeMatches && expectedGrp.getSequences().size() == actualGrp .getSequences().size() && expectedGrp.getStartRes() == actualGrp.getStartRes() @@ -510,7 +525,6 @@ public class JSONFileTest private boolean featuresMatched(SequenceI seq1, SequenceI seq2) { - boolean matched = false; try { if (seq1 == null && seq2 == null) @@ -518,52 +532,95 @@ public class JSONFileTest return true; } - SequenceFeature[] inFeature = seq1.getSequenceFeatures(); - SequenceFeature[] outFeature = seq2.getSequenceFeatures(); + List inFeature = seq1.getFeatures().getAllFeatures(); + List outFeature = seq2.getFeatures() + .getAllFeatures(); - if (inFeature == null && outFeature == null) - { - return true; - } - else if ((inFeature == null && outFeature != null) - || (inFeature != null && outFeature == null)) + if (inFeature.size() != outFeature.size()) { + System.err.println("Feature count in: " + inFeature.size() + + ", out: " + outFeature.size()); return false; } - int testSize = inFeature.length; - int matchedCount = 0; + SequenceFeatures.sortFeatures(inFeature, true); + SequenceFeatures.sortFeatures(outFeature, true); + int i = 0; for (SequenceFeature in : inFeature) { - for (SequenceFeature out : outFeature) + SequenceFeature out = outFeature.get(i); + /* + System.out.println(out.getType() + " | " + in.getType()); + System.out.println(out.getBegin() + " | " + in.getBegin()); + System.out.println(out.getEnd() + " | " + in.getEnd()); + */ + if (!in.equals(out)) { - System.out.println(out.getType() + " | " + in.getType()); - System.out.println(out.getBegin() + " | " + in.getBegin()); - System.out.println(out.getEnd() + " | " + in.getEnd()); - - if (inFeature.length == outFeature.length - && in.getBegin() == out.getBegin() - && in.getEnd() == out.getEnd() - && in.getScore() == out.getScore() - && in.getFeatureGroup().equals(out.getFeatureGroup()) - && in.getType().equals(out.getType())) - { - - ++matchedCount; - } + System.err.println("Mismatch of " + in.toString() + " " + + out.toString()); + return false; } - } - System.out.println("matched count >>>>>> " + matchedCount); - if (testSize == matchedCount) - { - matched = true; + /* + if (in.getBegin() == out.getBegin() && in.getEnd() == out.getEnd() + && in.getScore() == out.getScore() + && in.getFeatureGroup().equals(out.getFeatureGroup()) + && in.getType().equals(out.getType()) + && mapsMatch(in.otherDetails, out.otherDetails)) + { + } + else + { + System.err.println("Feature[" + i + "] mismatch, in: " + + in.toString() + ", out: " + + outFeature.get(i).toString()); + return false; + } + */ + i++; } } catch (Exception e) { e.printStackTrace(); } // System.out.println(">>>>>>>>>>>>>> features matched : " + matched); - return matched; + return true; + } + + boolean mapsMatch(Map m1, Map m2) + { + if (m1 == null || m2 == null) + { + if (m1 != null || m2 != null) + { + System.err + .println("only one SequenceFeature.otherDetails is not null"); + return false; + } + else + { + return true; + } + } + if (m1.size() != m2.size()) + { + System.err.println("otherDetails map different sizes"); + return false; + } + for (String key : m1.keySet()) + { + if (!m2.containsKey(key)) + { + System.err.println(key + " in only one otherDetails"); + return false; + } + if (m1.get(key) == null && m2.get(key) != null || m1.get(key) != null + && m2.get(key) == null || !m1.get(key).equals(m2.get(key))) + { + System.err.println(key + " values in otherDetails don't match"); + return false; + } + } + return true; } /** @@ -599,7 +656,7 @@ public class JSONFileTest Assert.assertNotNull(newAlignment.getGroups()); for (SequenceGroup seqGrp : newAlignment.getGroups()) { - SequenceGroup expectedGrp = expectedGrps.get(seqGrp.getName()); + SequenceGroup expectedGrp = copySg; AssertJUnit.assertTrue( "Failed SequenceGroup Test for >>> " + seqGrp.getName(), isGroupMatched(expectedGrp, seqGrp)); diff --git a/test/jalview/io/SequenceAnnotationReportTest.java b/test/jalview/io/SequenceAnnotationReportTest.java index 2895874..9e61bec 100644 --- a/test/jalview/io/SequenceAnnotationReportTest.java +++ b/test/jalview/io/SequenceAnnotationReportTest.java @@ -21,13 +21,21 @@ package jalview.io; import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertTrue; +import jalview.datamodel.DBRefEntry; +import jalview.datamodel.Sequence; import jalview.datamodel.SequenceFeature; +import jalview.datamodel.SequenceI; import jalview.gui.JvOptionPane; +import jalview.io.gff.GffConstants; +import java.util.HashMap; import java.util.Hashtable; import java.util.Map; +import junit.extensions.PA; + import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -192,4 +200,134 @@ public class SequenceAnnotationReportTest // if no tag, html-encodes > and < (only): assertEquals("METAL 1 3; <br>&kHD>6", sb.toString()); } + + @Test(groups = "Functional") + public void testCreateSequenceAnnotationReport() + { + SequenceAnnotationReport sar = new SequenceAnnotationReport(null); + StringBuilder sb = new StringBuilder(); + + SequenceI seq = new Sequence("s1", "MAKLKRFQSSTLL"); + seq.setDescription("SeqDesc"); + + sar.createSequenceAnnotationReport(sb, seq, true, true, null); + + /* + * positional features are ignored + */ + seq.addSequenceFeature(new SequenceFeature("Domain", "Ferredoxin", 5, + 10, 1f, null)); + assertEquals("
SeqDesc
", sb.toString()); + + /* + * non-positional feature + */ + seq.addSequenceFeature(new SequenceFeature("Type1", "Nonpos", 0, 0, 1f, + null)); + sb.setLength(0); + sar.createSequenceAnnotationReport(sb, seq, true, true, null); + String expected = "
SeqDesc
Type1 ; Nonpos
"; + assertEquals(expected, sb.toString()); + + /* + * non-positional features not wanted + */ + sb.setLength(0); + sar.createSequenceAnnotationReport(sb, seq, true, false, null); + assertEquals("
SeqDesc
", sb.toString()); + + /* + * add non-pos feature with score inside min-max range for feature type + * minmax holds { [positionalMin, positionalMax], [nonPosMin, nonPosMax] } + * score is only appended for positional features so ignored here! + * minMax are not recorded for non-positional features + */ + seq.addSequenceFeature(new SequenceFeature("Metal", "Desc", 0, 0, 5f, + null)); + Map minmax = new HashMap(); + minmax.put("Metal", new float[][] { null, new float[] { 2f, 5f } }); + sb.setLength(0); + sar.createSequenceAnnotationReport(sb, seq, true, true, minmax); + expected = "
SeqDesc
Metal ; Desc
Type1 ; Nonpos
"; + assertEquals(expected, sb.toString()); + + /* + * 'linkonly' features are ignored; this is obsolete, as linkonly + * is only set by DasSequenceFetcher, and DAS is history + */ + SequenceFeature sf = new SequenceFeature("Metal", "Desc", 0, 0, 5f, + null); + sf.setValue("linkonly", Boolean.TRUE); + seq.addSequenceFeature(sf); + sb.setLength(0); + sar.createSequenceAnnotationReport(sb, seq, true, true, minmax); + assertEquals(expected, sb.toString()); // unchanged! + + /* + * 'clinical_significance' currently being specially included + */ + SequenceFeature sf2 = new SequenceFeature("Variant", "Havana", 0, 0, + 5f, null); + sf2.setValue(GffConstants.CLINICAL_SIGNIFICANCE, "benign"); + seq.addSequenceFeature(sf2); + sb.setLength(0); + sar.createSequenceAnnotationReport(sb, seq, true, true, minmax); + expected = "
SeqDesc
Metal ; Desc
Type1 ; Nonpos
Variant ; Havana; benign
"; + assertEquals(expected, sb.toString()); + + /* + * add dbrefs + */ + seq.addDBRef(new DBRefEntry("PDB", "0", "3iu1")); + seq.addDBRef(new DBRefEntry("Uniprot", "1", "P30419")); + // with showDbRefs = false + sb.setLength(0); + sar.createSequenceAnnotationReport(sb, seq, false, true, minmax); + assertEquals(expected, sb.toString()); // unchanged + // with showDbRefs = true + sb.setLength(0); + sar.createSequenceAnnotationReport(sb, seq, true, true, minmax); + expected = "
SeqDesc
UNIPROT P30419
PDB 3iu1
Metal ; Desc
Type1 ; Nonpos
Variant ; Havana; benign
"; + assertEquals(expected, sb.toString()); + // with showNonPositionalFeatures = false + sb.setLength(0); + sar.createSequenceAnnotationReport(sb, seq, true, false, minmax); + expected = "
SeqDesc
UNIPROT P30419
PDB 3iu1
"; + assertEquals(expected, sb.toString()); + + // see other tests for treatment of status and html + } + + /** + * Test that exercises an abbreviated sequence details report, with ellipsis + * where there are more than 40 different sources, or more than 4 dbrefs for a + * single source + */ + @Test(groups = "Functional") + public void testCreateSequenceAnnotationReport_withEllipsis() + { + SequenceAnnotationReport sar = new SequenceAnnotationReport(null); + StringBuilder sb = new StringBuilder(); + + SequenceI seq = new Sequence("s1", "ABC"); + + int maxSources = (int) PA.getValue(sar, "MAX_SOURCES"); + for (int i = 0; i <= maxSources; i++) + { + seq.addDBRef(new DBRefEntry("PDB" + i, "0", "3iu1")); + } + + int maxRefs = (int) PA.getValue(sar, "MAX_REFS_PER_SOURCE"); + for (int i = 0; i <= maxRefs; i++) + { + seq.addDBRef(new DBRefEntry("Uniprot", "0", "P3041" + i)); + } + + sar.createSequenceAnnotationReport(sb, seq, true, true, null, true); + String report = sb.toString(); + assertTrue(report + .startsWith("
UNIPROT P30410, P30411, P30412, P30413,...
PDB0 3iu1")); + assertTrue(report + .endsWith("
PDB7 3iu1
PDB8,...
(Output Sequence Details to list all database references)
")); + } } diff --git a/test/jalview/io/StockholmFileTest.java b/test/jalview/io/StockholmFileTest.java index 228c935..4273e6c 100644 --- a/test/jalview/io/StockholmFileTest.java +++ b/test/jalview/io/StockholmFileTest.java @@ -287,7 +287,8 @@ public class StockholmFileTest seq_original = al.getSequencesArray(); SequenceI[] seq_new = new SequenceI[al_input.getSequencesArray().length]; seq_new = al_input.getSequencesArray(); - SequenceFeature[] sequenceFeatures_original, sequenceFeatures_new; + List sequenceFeatures_original; + List sequenceFeatures_new; AlignmentAnnotation annot_original, annot_new; // for (int i = 0; i < al.getSequencesArray().length; i++) @@ -323,23 +324,20 @@ public class StockholmFileTest && seq_new[in].getSequenceFeatures() != null) { System.out.println("There are feature!!!"); - sequenceFeatures_original = new SequenceFeature[seq_original[i] - .getSequenceFeatures().length]; sequenceFeatures_original = seq_original[i] .getSequenceFeatures(); - sequenceFeatures_new = new SequenceFeature[seq_new[in] - .getSequenceFeatures().length]; sequenceFeatures_new = seq_new[in].getSequenceFeatures(); - assertEquals("different number of features", - seq_original[i].getSequenceFeatures().length, - seq_new[in].getSequenceFeatures().length); + assertEquals("different number of features", seq_original[i] + .getSequenceFeatures().size(), seq_new[in] + .getSequenceFeatures().size()); - for (int feat = 0; feat < seq_original[i].getSequenceFeatures().length; feat++) + for (int feat = 0; feat < seq_original[i].getSequenceFeatures() + .size(); feat++) { assertEquals("Different features", - sequenceFeatures_original[feat], - sequenceFeatures_new[feat]); + sequenceFeatures_original.get(feat), + sequenceFeatures_new.get(feat)); } } // compare alignment annotation diff --git a/test/jalview/io/gff/Gff3HelperTest.java b/test/jalview/io/gff/Gff3HelperTest.java index bf038ac..cd5a0d8 100644 --- a/test/jalview/io/gff/Gff3HelperTest.java +++ b/test/jalview/io/gff/Gff3HelperTest.java @@ -37,7 +37,9 @@ import jalview.gui.JvOptionPane; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -231,4 +233,48 @@ public class Gff3HelperTest .getToRanges().get(0)); } + @Test(groups = "Functional") + public void testGetDescription() + { + Gff3Helper testee = new Gff3Helper(); + SequenceFeature sf = new SequenceFeature("type", "desc", 10, 20, 3f, + "group"); + Map> attributes = new HashMap>(); + assertNull(testee.getDescription(sf, attributes)); + + // ID if any is a fall-back for description + sf.setValue("ID", "Patrick"); + assertEquals("Patrick", testee.getDescription(sf, attributes)); + + // Target is set by Exonerate + sf.setValue("Target", "Destination Moon"); + assertEquals("Destination", testee.getDescription(sf, attributes)); + + // Ensembl variant feature - extract "alleles" value + // may be sequence_variant or a sub-type in the sequence ontology + sf = new SequenceFeature("feature_variant", "desc", 10, 20, 3f, "group"); + List atts = new ArrayList(); + atts.add("A"); + atts.add("C"); + atts.add("T"); + attributes.put("alleles", atts); + assertEquals("A,C,T", testee.getDescription(sf, attributes)); + + // Ensembl transcript or exon feature - extract Name + List atts2 = new ArrayList(); + atts2.add("ENSE00001871077"); + attributes.put("Name", atts2); + sf = new SequenceFeature("transcript", "desc", 10, 20, 3f, "group"); + assertEquals("ENSE00001871077", testee.getDescription(sf, attributes)); + // transcript sub-type in SO + sf = new SequenceFeature("mRNA", "desc", 10, 20, 3f, "group"); + assertEquals("ENSE00001871077", testee.getDescription(sf, attributes)); + // special usage of feature by Ensembl + sf = new SequenceFeature("NMD_transcript_variant", "desc", 10, 20, 3f, + "group"); + assertEquals("ENSE00001871077", testee.getDescription(sf, attributes)); + // exon feature + sf = new SequenceFeature("exon", "desc", 10, 20, 3f, "group"); + assertEquals("ENSE00001871077", testee.getDescription(sf, attributes)); + } } diff --git a/test/jalview/io/gff/InterProScanHelperTest.java b/test/jalview/io/gff/InterProScanHelperTest.java index bcccf35..dde83a3 100644 --- a/test/jalview/io/gff/InterProScanHelperTest.java +++ b/test/jalview/io/gff/InterProScanHelperTest.java @@ -21,6 +21,7 @@ package jalview.io.gff; import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertNotNull; import static org.testng.AssertJUnit.assertSame; import static org.testng.AssertJUnit.assertTrue; import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals; @@ -30,6 +31,7 @@ import jalview.datamodel.Alignment; import jalview.datamodel.AlignmentI; import jalview.datamodel.Sequence; import jalview.datamodel.SequenceDummy; +import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.gui.JvOptionPane; @@ -76,6 +78,16 @@ public class InterProScanHelperTest assertEquals(1, newseqs.size()); assertTrue(newseqs.get(0) instanceof SequenceDummy); assertEquals("match$17_5_30", newseqs.get(0).getName()); + + assertNotNull(newseqs.get(0).getSequenceFeatures()); + assertEquals(1, newseqs.get(0).getSequenceFeatures().size()); + SequenceFeature sf = newseqs.get(0).getSequenceFeatures().get(0); + assertEquals(1, sf.getBegin()); + assertEquals(26, sf.getEnd()); + assertEquals("Pfam", sf.getType()); + assertEquals("4Fe-4S dicluster domain", sf.getDescription()); + assertEquals("InterProScan", sf.getFeatureGroup()); + assertEquals(1, align.getCodonFrames().size()); AlignedCodonFrame mapping = align.getCodonFrames().iterator().next(); diff --git a/test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java b/test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java index 4fc079e..7fd7abc 100644 --- a/test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java +++ b/test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java @@ -16,6 +16,7 @@ import jalview.io.FileLoader; import jalview.schemes.FeatureColour; import java.awt.Color; +import java.util.List; import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeTest; @@ -70,13 +71,10 @@ public class FeatureColourFinderTest @BeforeMethod(alwaysRun = true) public void setUpBeforeTest() { - SequenceFeature[] sfs = seq.getSequenceFeatures(); - if (sfs != null) + List sfs = seq.getSequenceFeatures(); + for (SequenceFeature sf : sfs) { - for (SequenceFeature sf : sfs) - { - seq.deleteFeature(sf); - } + seq.deleteFeature(sf); } fr.findAllFeatures(true); @@ -454,15 +452,19 @@ public class FeatureColourFinderTest @Test(groups = "Functional") public void testFindFeatureColour_graduatedWithThreshold() { - seq.addSequenceFeature(new SequenceFeature("kd", "hydrophobicity", 2, + String kdFeature = "kd"; + String metalFeature = "Metal"; + seq.addSequenceFeature(new SequenceFeature(kdFeature, "hydrophobicity", 2, 2, 0f, "KdGroup")); - seq.addSequenceFeature(new SequenceFeature("kd", "hydrophobicity", 4, + seq.addSequenceFeature(new SequenceFeature(kdFeature, "hydrophobicity", 4, 4, 5f, "KdGroup")); - seq.addSequenceFeature(new SequenceFeature("kd", "hydrophobicity", 7, + seq.addSequenceFeature(new SequenceFeature(metalFeature, "Fe", 4, 4, + 5f, "MetalGroup")); + seq.addSequenceFeature(new SequenceFeature(kdFeature, "hydrophobicity", 7, 7, 10f, "KdGroup")); /* - * graduated colour from 0 to 10 + * kd feature has graduated colour from 0 to 10 * above threshold value of 5 */ Color min = new Color(100, 50, 150); @@ -470,8 +472,19 @@ public class FeatureColourFinderTest FeatureColourI fc = new FeatureColour(min, max, 0, 10); fc.setAboveThreshold(true); fc.setThreshold(5f); - fr.setColour("kd", fc); + fr.setColour(kdFeature, fc); + FeatureColour green = new FeatureColour(Color.green); + fr.setColour(metalFeature, green); fr.featuresAdded(); + + /* + * render order is kd above Metal + */ + Object[][] data = new Object[2][]; + data[0] = new Object[] { kdFeature, fc, true }; + data[1] = new Object[] { metalFeature, green, true }; + fr.setFeaturePriority(data); + av.setShowSequenceFeatures(true); /* @@ -481,10 +494,11 @@ public class FeatureColourFinderTest assertEquals(c, Color.blue); /* - * position 4, column 3, score 5 - at threshold - default colour + * position 4, column 3, score 5 - at threshold + * should return Green (colour of Metal feature) */ c = finder.findFeatureColour(Color.blue, seq, 3); - assertEquals(c, Color.blue); + assertEquals(c, Color.green); /* * position 7, column 9, score 10 - maximum colour in range @@ -504,10 +518,11 @@ public class FeatureColourFinderTest assertEquals(c, min); /* - * position 4, column 3, score 5 - at threshold - default colour + * position 4, column 3, score 5 - at threshold + * should return Green (colour of Metal feature) */ c = finder.findFeatureColour(Color.blue, seq, 3); - assertEquals(c, Color.blue); + assertEquals(c, Color.green); /* * position 7, column 9, score 10 - above threshold - default colour diff --git a/test/jalview/renderer/seqfeatures/FeatureRendererTest.java b/test/jalview/renderer/seqfeatures/FeatureRendererTest.java new file mode 100644 index 0000000..dc86605 --- /dev/null +++ b/test/jalview/renderer/seqfeatures/FeatureRendererTest.java @@ -0,0 +1,363 @@ +package jalview.renderer.seqfeatures; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import jalview.api.AlignViewportI; +import jalview.api.FeatureColourI; +import jalview.datamodel.SequenceFeature; +import jalview.datamodel.SequenceI; +import jalview.gui.AlignFrame; +import jalview.io.DataSourceType; +import jalview.io.FileLoader; +import jalview.schemes.FeatureColour; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.testng.annotations.Test; + +public class FeatureRendererTest +{ + + @Test(groups = "Functional") + public void testFindAllFeatures() + { + String seqData = ">s1\nabcdef\n>s2\nabcdef\n>s3\nabcdef\n>s4\nabcdef\n"; + AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqData, + DataSourceType.PASTE); + AlignViewportI av = af.getViewport(); + FeatureRenderer fr = new FeatureRenderer(av); + + /* + * with no features + */ + fr.findAllFeatures(true); + assertTrue(fr.getRenderOrder().isEmpty()); + assertTrue(fr.getFeatureGroups().isEmpty()); + + List seqs = av.getAlignment().getSequences(); + + // add a non-positional feature - should be ignored by FeatureRenderer + SequenceFeature sf1 = new SequenceFeature("Type", "Desc", 0, 0, 1f, + "Group"); + seqs.get(0).addSequenceFeature(sf1); + fr.findAllFeatures(true); + // ? bug - types and groups added for non-positional features + List types = fr.getRenderOrder(); + List groups = fr.getFeatureGroups(); + assertEquals(types.size(), 0); + assertFalse(types.contains("Type")); + assertEquals(groups.size(), 0); + assertFalse(groups.contains("Group")); + + // add some positional features + seqs.get(1).addSequenceFeature( + new SequenceFeature("Pfam", "Desc", 5, 9, 1f, "PfamGroup")); + seqs.get(2).addSequenceFeature( + new SequenceFeature("Pfam", "Desc", 14, 22, 2f, "RfamGroup")); + // bug in findAllFeatures - group not checked for a known feature type + seqs.get(2).addSequenceFeature( + new SequenceFeature("Rfam", "Desc", 5, 9, Float.NaN, + "RfamGroup")); + // existing feature type with null group + seqs.get(3).addSequenceFeature( + new SequenceFeature("Rfam", "Desc", 5, 9, Float.NaN, null)); + // new feature type with null group + seqs.get(3).addSequenceFeature( + new SequenceFeature("Scop", "Desc", 5, 9, Float.NaN, null)); + // null value for type produces NullPointerException + fr.findAllFeatures(true); + types = fr.getRenderOrder(); + groups = fr.getFeatureGroups(); + assertEquals(types.size(), 3); + assertFalse(types.contains("Type")); + assertTrue(types.contains("Pfam")); + assertTrue(types.contains("Rfam")); + assertTrue(types.contains("Scop")); + assertEquals(groups.size(), 2); + assertFalse(groups.contains("Group")); + assertTrue(groups.contains("PfamGroup")); + assertTrue(groups.contains("RfamGroup")); + assertFalse(groups.contains(null)); // null group is ignored + + /* + * check min-max values + */ + Map minMax = fr.getMinMax(); + assertEquals(minMax.size(), 1); // non-positional and NaN not stored + assertEquals(minMax.get("Pfam")[0][0], 1f); // positional min + assertEquals(minMax.get("Pfam")[0][1], 2f); // positional max + + // increase max for Pfam, add scores for Rfam + seqs.get(0).addSequenceFeature( + new SequenceFeature("Pfam", "Desc", 14, 22, 8f, "RfamGroup")); + seqs.get(1).addSequenceFeature( + new SequenceFeature("Rfam", "Desc", 5, 9, 6f, "RfamGroup")); + fr.findAllFeatures(true); + // note minMax is not a defensive copy, shouldn't expose this + assertEquals(minMax.size(), 2); + assertEquals(minMax.get("Pfam")[0][0], 1f); + assertEquals(minMax.get("Pfam")[0][1], 8f); + assertEquals(minMax.get("Rfam")[0][0], 6f); + assertEquals(minMax.get("Rfam")[0][1], 6f); + + /* + * check render order (last is on top) + */ + List renderOrder = fr.getRenderOrder(); + assertEquals(renderOrder, Arrays.asList("Scop", "Rfam", "Pfam")); + + /* + * change render order (todo: an easier way) + * nb here last comes first in the data array + */ + Object[][] data = new Object[3][]; + FeatureColourI colour = new FeatureColour(Color.RED); + data[0] = new Object[] { "Rfam", colour, true }; + data[1] = new Object[] { "Pfam", colour, false }; + data[2] = new Object[] { "Scop", colour, false }; + fr.setFeaturePriority(data); + assertEquals(fr.getRenderOrder(), Arrays.asList("Scop", "Pfam", "Rfam")); + assertEquals(fr.getDisplayedFeatureTypes(), Arrays.asList("Rfam")); + + /* + * add a new feature type: should go on top of render order as visible, + * other feature ordering and visibility should be unchanged + */ + seqs.get(2).addSequenceFeature( + new SequenceFeature("Metal", "Desc", 14, 22, 8f, "MetalGroup")); + fr.findAllFeatures(true); + assertEquals(fr.getRenderOrder(), + Arrays.asList("Scop", "Pfam", "Rfam", "Metal")); + assertEquals(fr.getDisplayedFeatureTypes(), + Arrays.asList("Rfam", "Metal")); + } + + @Test(groups = "Functional") + public void testFindFeaturesAtColumn() + { + String seqData = ">s1/4-29\n-ab--cdefghijklmnopqrstuvwxyz\n"; + AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqData, + DataSourceType.PASTE); + AlignViewportI av = af.getViewport(); + FeatureRenderer fr = new FeatureRenderer(av); + SequenceI seq = av.getAlignment().getSequenceAt(0); + + /* + * with no features + */ + List features = fr.findFeaturesAtColumn(seq, 3); + assertTrue(features.isEmpty()); + + /* + * add features + */ + SequenceFeature sf1 = new SequenceFeature("Type1", "Desc", 0, 0, 1f, + "Group"); // non-positional + seq.addSequenceFeature(sf1); + SequenceFeature sf2 = new SequenceFeature("Type2", "Desc", 8, 18, 1f, + "Group1"); + seq.addSequenceFeature(sf2); + SequenceFeature sf3 = new SequenceFeature("Type3", "Desc", 8, 18, 1f, + "Group2"); + seq.addSequenceFeature(sf3); + SequenceFeature sf4 = new SequenceFeature("Type3", "Desc", 8, 18, 1f, + null); // null group is always treated as visible + seq.addSequenceFeature(sf4); + + /* + * add contact features + */ + SequenceFeature sf5 = new SequenceFeature("Disulphide Bond", "Desc", 7, + 15, 1f, "Group1"); + seq.addSequenceFeature(sf5); + SequenceFeature sf6 = new SequenceFeature("Disulphide Bond", "Desc", 7, + 15, 1f, "Group2"); + seq.addSequenceFeature(sf6); + SequenceFeature sf7 = new SequenceFeature("Disulphide Bond", "Desc", 7, + 15, 1f, null); + seq.addSequenceFeature(sf7); + + // feature spanning B--C + SequenceFeature sf8 = new SequenceFeature("Type1", "Desc", 5, 6, 1f, + "Group"); + seq.addSequenceFeature(sf8); + // contact feature B/C + SequenceFeature sf9 = new SequenceFeature("Disulphide Bond", "Desc", 5, + 6, 1f, "Group"); + seq.addSequenceFeature(sf9); + + /* + * let feature renderer discover features (and make visible) + */ + fr.findAllFeatures(true); + features = fr.findFeaturesAtColumn(seq, 15); // all positional + assertEquals(features.size(), 6); + assertTrue(features.contains(sf2)); + assertTrue(features.contains(sf3)); + assertTrue(features.contains(sf4)); + assertTrue(features.contains(sf5)); + assertTrue(features.contains(sf6)); + assertTrue(features.contains(sf7)); + + /* + * at a non-contact position + */ + features = fr.findFeaturesAtColumn(seq, 14); + assertEquals(features.size(), 3); + assertTrue(features.contains(sf2)); + assertTrue(features.contains(sf3)); + assertTrue(features.contains(sf4)); + + /* + * make "Type2" not displayed + */ + Object[][] data = new Object[4][]; + FeatureColourI colour = new FeatureColour(Color.RED); + data[0] = new Object[] { "Type1", colour, true }; + data[1] = new Object[] { "Type2", colour, false }; + data[2] = new Object[] { "Type3", colour, true }; + data[3] = new Object[] { "Disulphide Bond", colour, true }; + fr.setFeaturePriority(data); + + features = fr.findFeaturesAtColumn(seq, 15); + assertEquals(features.size(), 5); // no sf2 + assertTrue(features.contains(sf3)); + assertTrue(features.contains(sf4)); + assertTrue(features.contains(sf5)); + assertTrue(features.contains(sf6)); + assertTrue(features.contains(sf7)); + + /* + * make "Group2" not displayed + */ + fr.setGroupVisibility("Group2", false); + + features = fr.findFeaturesAtColumn(seq, 15); + assertEquals(features.size(), 3); // no sf2, sf3, sf6 + assertTrue(features.contains(sf4)); + assertTrue(features.contains(sf5)); + assertTrue(features.contains(sf7)); + + // features 'at' a gap between b and c + // - returns enclosing feature BC but not contact feature B/C + features = fr.findFeaturesAtColumn(seq, 4); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf8)); + features = fr.findFeaturesAtColumn(seq, 5); + assertEquals(features.size(), 1); + assertTrue(features.contains(sf8)); + } + + @Test(groups = "Functional") + public void testFilterFeaturesForDisplay() + { + String seqData = ">s1\nabcdef\n"; + AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqData, + DataSourceType.PASTE); + AlignViewportI av = af.getViewport(); + FeatureRenderer fr = new FeatureRenderer(av); + + List features = new ArrayList<>(); + fr.filterFeaturesForDisplay(features, null); // empty list, does nothing + + SequenceI seq = av.getAlignment().getSequenceAt(0); + SequenceFeature sf1 = new SequenceFeature("Cath", "", 6, 8, Float.NaN, + "group1"); + seq.addSequenceFeature(sf1); + SequenceFeature sf2 = new SequenceFeature("Cath", "", 5, 11, 2f, + "group2"); + seq.addSequenceFeature(sf2); + SequenceFeature sf3 = new SequenceFeature("Cath", "", 5, 11, 3f, + "group3"); + seq.addSequenceFeature(sf3); + SequenceFeature sf4 = new SequenceFeature("Cath", "", 6, 8, 4f, + "group4"); + seq.addSequenceFeature(sf4); + SequenceFeature sf5 = new SequenceFeature("Cath", "", 6, 9, 5f, + "group4"); + seq.addSequenceFeature(sf5); + + fr.findAllFeatures(true); + + features = seq.getSequenceFeatures(); + assertEquals(features.size(), 5); + assertTrue(features.contains(sf1)); + assertTrue(features.contains(sf2)); + assertTrue(features.contains(sf3)); + assertTrue(features.contains(sf4)); + assertTrue(features.contains(sf5)); + + /* + * filter out duplicate (co-located) features + * note: which gets removed is not guaranteed + */ + fr.filterFeaturesForDisplay(features, new FeatureColour(Color.blue)); + assertEquals(features.size(), 3); + assertTrue(features.contains(sf1) || features.contains(sf4)); + assertFalse(features.contains(sf1) && features.contains(sf4)); + assertTrue(features.contains(sf2) || features.contains(sf3)); + assertFalse(features.contains(sf2) && features.contains(sf3)); + assertTrue(features.contains(sf5)); + + /* + * hide group 3 - sf3 is removed, sf2 is retained + */ + fr.setGroupVisibility("group3", false); + features = seq.getSequenceFeatures(); + fr.filterFeaturesForDisplay(features, new FeatureColour(Color.blue)); + assertEquals(features.size(), 3); + assertTrue(features.contains(sf1) || features.contains(sf4)); + assertFalse(features.contains(sf1) && features.contains(sf4)); + assertTrue(features.contains(sf2)); + assertFalse(features.contains(sf3)); + assertTrue(features.contains(sf5)); + + /* + * hide group 2, show group 3 - sf2 is removed, sf3 is retained + */ + fr.setGroupVisibility("group2", false); + fr.setGroupVisibility("group3", true); + features = seq.getSequenceFeatures(); + fr.filterFeaturesForDisplay(features, null); + assertEquals(features.size(), 3); + assertTrue(features.contains(sf1) || features.contains(sf4)); + assertFalse(features.contains(sf1) && features.contains(sf4)); + assertFalse(features.contains(sf2)); + assertTrue(features.contains(sf3)); + assertTrue(features.contains(sf5)); + + /* + * no filtering of co-located features with graduated colour scheme + * filterFeaturesForDisplay does _not_ check colour threshold + * sf2 is removed as its group is hidden + */ + features = seq.getSequenceFeatures(); + fr.filterFeaturesForDisplay(features, new FeatureColour(Color.black, + Color.white, 0f, 1f)); + assertEquals(features.size(), 4); + assertTrue(features.contains(sf1)); + assertTrue(features.contains(sf3)); + assertTrue(features.contains(sf4)); + assertTrue(features.contains(sf5)); + + /* + * filtering of co-located features with colour by label + */ + features = seq.getSequenceFeatures(); + FeatureColour fc = new FeatureColour(Color.black); + fc.setColourByLabel(true); + fr.filterFeaturesForDisplay(features, fc); + assertEquals(features.size(), 3); + assertTrue(features.contains(sf1) || features.contains(sf4)); + assertFalse(features.contains(sf1) && features.contains(sf4)); + assertFalse(features.contains(sf2)); + assertTrue(features.contains(sf3)); + assertTrue(features.contains(sf5)); + } +} diff --git a/test/jalview/schemes/AnnotationColourGradientTest.java b/test/jalview/schemes/AnnotationColourGradientTest.java index 1c93856..b7a5164 100644 --- a/test/jalview/schemes/AnnotationColourGradientTest.java +++ b/test/jalview/schemes/AnnotationColourGradientTest.java @@ -49,7 +49,7 @@ public class AnnotationColourGradientTest anns[col] = new Annotation("a", "a", 'a', col, colour); } - seq = new Sequence("", ""); + seq = new Sequence("Seq", ""); al = new Alignment(new SequenceI[]{ seq}); /* diff --git a/test/jalview/schemes/FeatureColourTest.java b/test/jalview/schemes/FeatureColourTest.java index c16d541..7a72c15 100644 --- a/test/jalview/schemes/FeatureColourTest.java +++ b/test/jalview/schemes/FeatureColourTest.java @@ -22,6 +22,7 @@ package jalview.schemes; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertNull; import static org.testng.AssertJUnit.assertTrue; import static org.testng.AssertJUnit.fail; @@ -84,60 +85,11 @@ public class FeatureColourTest } @Test(groups = { "Functional" }) - public void testIsColored_simpleColour() - { - FeatureColour fc = new FeatureColour(Color.RED); - assertTrue(fc.isColored(new SequenceFeature())); - } - - @Test(groups = { "Functional" }) - public void testIsColored_colourByLabel() - { - FeatureColour fc = new FeatureColour(); - fc.setColourByLabel(true); - assertTrue(fc.isColored(new SequenceFeature())); - } - - @Test(groups = { "Functional" }) - public void testIsColored_aboveThreshold() - { - // graduated colour range from score 20 to 100 - FeatureColour fc = new FeatureColour(Color.WHITE, Color.BLACK, 20f, - 100f); - - // score 0 is adjusted to bottom of range - SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 0f, - null); - assertTrue(fc.isColored(sf)); - assertEquals(Color.WHITE, fc.getColor(sf)); - - // score 120 is adjusted to top of range - sf.setScore(120f); - assertEquals(Color.BLACK, fc.getColor(sf)); - - // value below threshold is still rendered - // setting threshold has no effect yet... - fc.setThreshold(60f); - sf.setScore(36f); - assertTrue(fc.isColored(sf)); - assertEquals(new Color(204, 204, 204), fc.getColor(sf)); - - // now apply threshold: - fc.setAboveThreshold(true); - assertFalse(fc.isColored(sf)); - // colour is still returned though ?!? - assertEquals(new Color(204, 204, 204), fc.getColor(sf)); - - sf.setScore(84); // above threshold now - assertTrue(fc.isColored(sf)); - assertEquals(new Color(51, 51, 51), fc.getColor(sf)); - } - - @Test(groups = { "Functional" }) public void testGetColor_simpleColour() { FeatureColour fc = new FeatureColour(Color.RED); - assertEquals(Color.RED, fc.getColor(new SequenceFeature())); + assertEquals(Color.RED, + fc.getColor(new SequenceFeature("Cath", "", 1, 2, 0f, null))); } @Test(groups = { "Functional" }) @@ -169,20 +121,35 @@ public class FeatureColourTest } @Test(groups = { "Functional" }) - public void testGetColor_belowThreshold() + public void testGetColor_aboveBelowThreshold() { // gradient from [50, 150] from WHITE(255, 255, 255) to BLACK(0, 0, 0) FeatureColour fc = new FeatureColour(Color.WHITE, Color.BLACK, 50f, 150f); SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 70f, null); + + /* + * feature with score of Float.NaN is always assigned minimum colour + */ + SequenceFeature sf2 = new SequenceFeature("type", "desc", 0, 20, + Float.NaN, null); + fc.setThreshold(100f); // ignore for now - assertTrue(fc.isColored(sf)); assertEquals(new Color(204, 204, 204), fc.getColor(sf)); + assertEquals(Color.white, fc.getColor(sf2)); fc.setAboveThreshold(true); // feature lies below threshold - assertFalse(fc.isColored(sf)); - assertEquals(new Color(204, 204, 204), fc.getColor(sf)); + assertNull(fc.getColor(sf)); + assertEquals(Color.white, fc.getColor(sf2)); + + fc.setBelowThreshold(true); + fc.setThreshold(70f); + assertNull(fc.getColor(sf)); // feature score == threshold - hidden + assertEquals(Color.white, fc.getColor(sf2)); + fc.setThreshold(69f); + assertNull(fc.getColor(sf)); // feature score > threshold - hidden + assertEquals(Color.white, fc.getColor(sf2)); } /** diff --git a/test/jalview/structure/StructureSelectionManagerTest.java b/test/jalview/structure/StructureSelectionManagerTest.java index a7e52ff..a59fbde 100644 --- a/test/jalview/structure/StructureSelectionManagerTest.java +++ b/test/jalview/structure/StructureSelectionManagerTest.java @@ -145,7 +145,7 @@ public class StructureSelectionManagerTest /* * Verify a RESNUM sequence feature in the PDBfile sequence */ - SequenceFeature sf = pmap.getSeqs().get(0).getSequenceFeatures()[0]; + SequenceFeature sf = pmap.getSeqs().get(0).getSequenceFeatures().get(0); assertEquals("RESNUM", sf.getType()); assertEquals("1gaq", sf.getFeatureGroup()); assertEquals("GLU: 19 1gaqA", sf.getDescription()); @@ -155,7 +155,7 @@ public class StructureSelectionManagerTest * sequence */ StructureMapping map = sm.getMapping("examples/1gaq.txt")[0]; - sf = map.sequence.getSequenceFeatures()[0]; + sf = map.sequence.getSequenceFeatures().get(0); assertEquals("RESNUM", sf.getType()); assertEquals("1gaq", sf.getFeatureGroup()); assertEquals("ALA: 1 1gaqB", sf.getDescription()); diff --git a/test/jalview/viewmodel/ViewportRangesTest.java b/test/jalview/viewmodel/ViewportRangesTest.java index 70a3687..851b1b7 100644 --- a/test/jalview/viewmodel/ViewportRangesTest.java +++ b/test/jalview/viewmodel/ViewportRangesTest.java @@ -1,6 +1,7 @@ package jalview.viewmodel; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; import jalview.analysis.AlignmentGenerator; @@ -12,6 +13,7 @@ import jalview.datamodel.HiddenSequences; import java.beans.PropertyChangeEvent; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.testng.annotations.BeforeClass; @@ -346,17 +348,41 @@ public class ViewportRangesTest { @Test(groups = { "Functional" }) public void testScrollToWrappedVisible() { - ViewportRanges vr = new ViewportRanges(al); + AlignmentI al2 = gen.generate(60, 30, 1, 5, 5); + + ViewportRanges vr = new ViewportRanges(al2); + + // start with viewport on 5-14 vr.setViewportStartAndWidth(5, 10); + assertEquals(vr.getStartRes(), 5); + assertEquals(vr.getEndRes(), 14); - vr.scrollToWrappedVisible(0); + // scroll to 12 - no change + assertFalse(vr.scrollToWrappedVisible(12)); + assertEquals(vr.getStartRes(), 5); + + // scroll to 2 - back to 0-9 + assertTrue(vr.scrollToWrappedVisible(2)); assertEquals(vr.getStartRes(), 0); + assertEquals(vr.getEndRes(), 9); - vr.scrollToWrappedVisible(10); - assertEquals(vr.getStartRes(), 10); + // scroll to 9 - no change + assertFalse(vr.scrollToWrappedVisible(9)); + assertEquals(vr.getStartRes(), 0); - vr.scrollToWrappedVisible(15); + // scroll to 12 - moves to 10-19 + assertTrue(vr.scrollToWrappedVisible(12)); assertEquals(vr.getStartRes(), 10); + assertEquals(vr.getEndRes(), 19); + + vr.setStartRes(13); + assertEquals(vr.getStartRes(), 13); + assertEquals(vr.getEndRes(), 22); + + // scroll to 45 - jumps to 43-52 + assertTrue(vr.scrollToWrappedVisible(45)); + assertEquals(vr.getStartRes(), 43); + assertEquals(vr.getEndRes(), 52); } // leave until JAL-2388 is merged and we can do without viewport @@ -509,9 +535,16 @@ public class ViewportRangesTest { Arrays.asList("startseq", "startseq", "startseq", "startseq"))); l.reset(); - vr.scrollToWrappedVisible(5); - assertTrue(l.verify(1, Arrays.asList("startres"))); + /* + * scrollToWrappedVisible does nothing if the target position is + * within the current startRes-endRes range + */ + assertFalse(vr.scrollToWrappedVisible(5)); + assertTrue(l.verify(0, Collections. emptyList())); l.reset(); + + vr.scrollToWrappedVisible(25); + assertTrue(l.verify(1, Arrays.asList("startres"))); } @Test(groups = { "Functional" }) diff --git a/test/jalview/ws/dbsources/UniprotTest.java b/test/jalview/ws/dbsources/UniprotTest.java index 2f548d0..2d4be71 100644 --- a/test/jalview/ws/dbsources/UniprotTest.java +++ b/test/jalview/ws/dbsources/UniprotTest.java @@ -26,9 +26,9 @@ import static org.testng.AssertJUnit.assertNotNull; import static org.testng.AssertJUnit.assertNull; import jalview.datamodel.PDBEntry; -import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; -import jalview.datamodel.UniprotEntry; +import jalview.datamodel.xdb.uniprot.UniprotEntry; +import jalview.datamodel.xdb.uniprot.UniprotFeature; import jalview.gui.JvOptionPane; import java.io.Reader; @@ -97,13 +97,12 @@ public class UniprotTest /* * Check sequence features */ - Vector features = entry.getFeature(); + Vector features = entry.getFeature(); assertEquals(3, features.size()); - SequenceFeature sf = features.get(0); + UniprotFeature sf = features.get(0); assertEquals("signal peptide", sf.getType()); assertNull(sf.getDescription()); assertNull(sf.getStatus()); - assertEquals(1, sf.getPosition()); assertEquals(1, sf.getBegin()); assertEquals(18, sf.getEnd()); sf = features.get(1); @@ -139,10 +138,8 @@ public class UniprotTest xref = xrefs.get(2); assertEquals("AE007869", xref.getId()); assertEquals("EMBL", xref.getType()); - assertEquals("AAK85932.1", - xref.getProperty("protein sequence ID")); - assertEquals("Genomic_DNA", - xref.getProperty("molecule type")); + assertEquals("AAK85932.1", xref.getProperty("protein sequence ID")); + assertEquals("Genomic_DNA", xref.getProperty("molecule type")); } @Test(groups = { "Functional" }) diff --git a/test/jalview/ws/seqfetcher/DbRefFetcherTest.java b/test/jalview/ws/seqfetcher/DbRefFetcherTest.java index e35f83e..de91af3 100644 --- a/test/jalview/ws/seqfetcher/DbRefFetcherTest.java +++ b/test/jalview/ws/seqfetcher/DbRefFetcherTest.java @@ -21,6 +21,7 @@ package jalview.ws.seqfetcher; import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; import static org.testng.AssertJUnit.assertNotNull; import static org.testng.AssertJUnit.assertTrue; @@ -173,13 +174,13 @@ public class DbRefFetcherTest SequenceI seq = alsq.getSequenceAt(0); assertEquals("Wrong sequence name", embl.getDbSource() + "|" + retrievalId, seq.getName()); - SequenceFeature[] sfs = seq.getSequenceFeatures(); - assertNotNull("Sequence features missing", sfs); + List sfs = seq.getSequenceFeatures(); + assertFalse("Sequence features missing", sfs.isEmpty()); assertTrue( "Feature not CDS", FeatureProperties.isCodingFeature(embl.getDbSource(), - sfs[0].getType())); - assertEquals(embl.getDbSource(), sfs[0].getFeatureGroup()); + sfs.get(0).getType())); + assertEquals(embl.getDbSource(), sfs.get(0).getFeatureGroup()); DBRefEntry[] dr = DBRefUtils.selectRefs(seq.getDBRefs(), new String[] { DBRefSource.UNIPROT }); assertNotNull(dr);