From: gmungoc Date: Wed, 24 May 2017 14:21:43 +0000 (+0100) Subject: Merge branch 'bug/JAL-2541cutWithFeatures' into features/JAL-2446NCList X-Git-Tag: Release_2_10_3b1~239 X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=f1fbc7674102f63dfe1bd156a2d19f3c658e35d5;hp=82c9e6981596955f5f5cea41b25adc7e7581746b;p=jalview.git Merge branch 'bug/JAL-2541cutWithFeatures' into features/JAL-2446NCList --- diff --git a/examples/example.json b/examples/example.json index 5f6e784..5055d04 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,"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":"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 diff --git a/help/html/features/chimera.html b/help/html/features/chimera.html index 5ae00af..68ac465 100644 --- a/help/html/features/chimera.html +++ b/help/html/features/chimera.html @@ -211,10 +211,41 @@ structure in the alignment. The regions used to calculate the superposition will be highlighted using the 'Cartoon' rendering style, and the remaining data shown as a chain - trace.
+ trace.

- -
  • Help
    +
  • EXPERIMENTAL FEATURES
    + + These are only available if the Tools→Enable + Experimental Features option is enabled. (Since Jalview 2.10.2) +
      +
    • Write Jalview features
      Selecting + this option will create new residue attributes for any + features currently visible in the associated alignment + views, allowing those positions to be selected and + analysed with via Chimera's 'Render by Attribute' tool + (found in the Tools submenu called Structure Analysis).
      +
      If you use this option, please remember to select + the Refresh Menus option in Chimera's Render by + Attribute dialog box in order to see the attributes + derived from Jalview sequence features. +

      + View + this function's issue in Jalview's bug tracker
    • +
    • Fetch Chimera Attributes
      This + submenu lists available Chimera residue attributes that + can be imported as Jalview features on associated + sequences.
      This is particularly useful for + transferring quantitative positional annotation. For + example, structure similarity for an alignment can be + visualised by transferring the local RMSD attributes + generated by Chimera's Match->Align tool onto aligned + sequences and displayed with a Graduated feature colour + scheme. +
      View + this function's issue in Jalview's bug tracker
    • +
  • +
  • Help
    • Chimera Help
      diff --git a/help/html/menus/desktopMenu.html b/help/html/menus/desktopMenu.html index 20e784b..a93ce4b 100755 --- a/help/html/menus/desktopMenu.html +++ b/help/html/menus/desktopMenu.html @@ -86,6 +86,7 @@ the Groovy Console for interactive scripting.
    • +
    • Enable Experimental Features Enable or disable features still under development in Jalview's user interface. This setting is remembered in your preferences.
  • Vamsas For more details, read the diff --git a/help/html/releases.html b/help/html/releases.html index 2f983fa..1fe2602 100755 --- a/help/html/releases.html +++ b/help/html/releases.html @@ -79,19 +79,36 @@ li:before {
    • More robust colours and shader model for alignments and groups
    • Custom shading schemes created via groovy scripts
    • -
    • Test suite expanded and debugged (over 940 functional unit tests, only 3 failing due to ongoing work!)
    Application
      -
    • +
    • + + Experimental Features Checkbox in Desktop's Tools + menu to hide or show untested features in the application. +
    • +
    • Warning in alignment status bar when there are not enough columns to superimpose structures in Chimera
    • Faster Chimera/Jalview communication by file-based command exchange
    • URLs for viewing database cross-references provided by identifiers.org and the EMBL-EBI's MIRIAM DB
    + Experimental features +
      +
    • + New entries in the Chimera menu + to transfer Chimera's structure attributes as Jalview + features, and vice-versa. +
    • +
    Applet
    + Test Suite +
  • Added PrivelegedAccessor to test suite
  • +
  • Prevent or clear modal dialogs raised during tests
  • +
  • +
    General
      diff --git a/help/html/whatsNew.html b/help/html/whatsNew.html index 0734271..f5fcf18 100755 --- a/help/html/whatsNew.html +++ b/help/html/whatsNew.html @@ -27,12 +27,30 @@ What's new in Jalview 2.10.2 ?

      - Full details about Jalview 2.10.2 are in the - Release Notes, but the highlights are below.

      + Full details about Jalview 2.10.2 are in the Release Notes, but the + highlights are below. +

      + +

      + Experimental Features +

      +

      This release of Jalview includes a new option in the Jalview Desktop + that allows you to try out features that are still in development. To + access the features described below, please first enable the + Tools→Enable Experimental Features option, and then restart Jalview. +

      - diff --git a/resources/lang/Messages.properties b/resources/lang/Messages.properties index f6eeb26..8099dff 100644 --- a/resources/lang/Messages.properties +++ b/resources/lang/Messages.properties @@ -914,7 +914,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. @@ -1308,3 +1307,6 @@ label.consensus_descr = PID label.complement_consensus_descr = PID for cDNA label.strucconsensus_descr = PID for base pairs label.occupancy_descr = Number of aligned positions +label.show_experimental = Enable experimental features +label.show_experimental_tip = Enable any new and currently 'experimental' features (see Latest Release Notes for details) +label.warning_hidden = Warning: {0} {1} is currently hidden diff --git a/resources/lang/Messages_es.properties b/resources/lang/Messages_es.properties index ad4d2c4..7808480 100644 --- a/resources/lang/Messages_es.properties +++ b/resources/lang/Messages_es.properties @@ -839,7 +839,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. @@ -1308,3 +1307,4 @@ label.complement_consensus_descr = % Identidad para cDNA label.strucconsensus_descr = % Identidad para pares de bases label.occupancy_descr = Número de posiciones alineadas label.togglehidden = Show hidden regions +label.warning_hidden = Advertencia: {0} {1} está actualmente oculto diff --git a/src/MCview/PDBChain.java b/src/MCview/PDBChain.java index ba93046..1f47014 100755 --- a/src/MCview/PDBChain.java +++ b/src/MCview/PDBChain.java @@ -207,11 +207,13 @@ public class PDBChain if (features[i].getFeatureGroup() != null && features[i].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(features[i].getBegin() + - offset).atoms + .elementAt(0).alignmentMapping; + int newEnd = 1 + residues.elementAt(features[i].getEnd() - offset).atoms + .elementAt(0).alignmentMapping; + SequenceFeature tx = new SequenceFeature(features[i], newBegin, + newEnd, features[i].getFeatureGroup()); tx.setStatus(status + ((tx.getStatus() == null || tx.getStatus().length() == 0) ? "" : ":" + tx.getStatus())); diff --git a/src/jalview/analysis/AlignmentSorter.java b/src/jalview/analysis/AlignmentSorter.java index 6c46a3e..e7733e9 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,52 +745,59 @@ 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 + */ + // TODO new method findPositions(startCol, endCol)? JAL-2544 + int startResidue = seqs[i].findPosition(startCol); + int endResidue = seqs[i].findPosition(endCol); + String[] types = featureTypes == null ? null : featureTypes + .toArray(new String[featureTypes.size()]); + List sfs = seqs[i].getFeatures().findFeatures( + startResidue, endResidue, 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 - if ( - // ignore features outwith alignment start-stop positions. - (sf[f].end < sstart || sf[f].begin > sstop) || - // or ignore based on selection criteria - (featureLabels != null && !AlignmentSorter - .containsIgnoreCase(sf[f].type, featureLabels)) - || (groupLabels != null - // problem here: we cannot eliminate null feature group features - && (sf[f].getFeatureGroup() != null && !AlignmentSorter - .containsIgnoreCase(sf[f].getFeatureGroup(), - groupLabels)))) + 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 + */ + if (seqs[i].findIndex(sf.getBegin()) > endCol + 1 + || seqs[i].findIndex(sf.getEnd()) < startCol + 1) + { + it.remove(); + continue; + } + + /* + * accept all features with null or empty group, otherwise + * check group is one of the currently visible groups + */ + 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(sf[f].getScore())) + float score = sf.getScore(); + if (FEATURE_SCORE.equals(method) && !Float.isNaN(score)) { if (seqScores[i] == 0) { @@ -838,33 +805,26 @@ public class AlignmentSorter } seqScores[i]++; hasScore[i] = true; - scores[i] += sf[f].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]) @@ -874,23 +834,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) { @@ -915,9 +870,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++) { @@ -927,18 +882,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..66be623 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; @@ -2055,11 +2054,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 +2070,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)) + if (type.equals(toOmit)) { - continue; - } - boolean omit = false; - for (String toOmit : omitting) - { - if (type.equals(toOmit)) - { - omit = true; - } - } - if (omit) - { - 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()); + copyTo.addSequenceFeature(copy); + count++; + } } return count; } @@ -2204,49 +2202,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 + begin += phase; + if (begin > end) { - 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) - { - // 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 +2259,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 +2312,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; } @@ -2527,10 +2502,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 +2525,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; diff --git a/src/jalview/analysis/Rna.java b/src/jalview/analysis/Rna.java index 89c5c30..34233f0 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/api/FeatureRenderer.java b/src/jalview/api/FeatureRenderer.java index 7123b8c..edd236b 100644 --- a/src/jalview/api/FeatureRenderer.java +++ b/src/jalview/api/FeatureRenderer.java @@ -165,9 +165,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(); 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 cd49f63..8fd317a 100644 --- a/src/jalview/appletgui/APopupMenu.java +++ b/src/jalview/appletgui/APopupMenu.java @@ -843,7 +843,7 @@ public class APopupMenu extends java.awt.PopupMenu implements seqs = rseqs; if (ap.seqPanel.seqCanvas.getFeatureRenderer().amendFeatures(seqs, - features, true, ap, null)) + features, true, ap)) { ap.alignFrame.sequenceFeatures.setState(true); ap.av.setShowSequenceFeatures(true); diff --git a/src/jalview/appletgui/AlignFrame.java b/src/jalview/appletgui/AlignFrame.java index f914108..2dde2ab 100644 --- a/src/jalview/appletgui/AlignFrame.java +++ b/src/jalview/appletgui/AlignFrame.java @@ -1440,6 +1440,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; @@ -1447,12 +1458,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/FeatureRenderer.java b/src/jalview/appletgui/FeatureRenderer.java index be027ec..81b207f 100644 --- a/src/jalview/appletgui/FeatureRenderer.java +++ b/src/jalview/appletgui/FeatureRenderer.java @@ -36,7 +36,9 @@ import java.awt.Button; import java.awt.Choice; import java.awt.Color; import java.awt.Dimension; +import java.awt.FlowLayout; import java.awt.Font; +import java.awt.Frame; import java.awt.Graphics; import java.awt.GridLayout; import java.awt.Label; @@ -48,6 +50,8 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.awt.event.TextEvent; +import java.awt.event.TextListener; import java.util.Hashtable; /** @@ -59,6 +63,13 @@ import java.util.Hashtable; public class FeatureRenderer extends jalview.renderer.seqfeatures.FeatureRenderer { + /* + * creating a new feature defaults to the type and group as + * the last one created + */ + static String lastFeatureAdded = "feature_1"; + + static String lastFeatureGroupAdded = "Jalview"; // Holds web links for feature groups and feature types // in the form label|link @@ -75,12 +86,6 @@ public class FeatureRenderer extends } - static String lastFeatureAdded; - - static String lastFeatureGroupAdded; - - static String lastDescriptionAdded; - int featureIndex = 0; boolean deleteFeature = false; @@ -167,22 +172,23 @@ public class FeatureRenderer extends /** * Shows a dialog allowing the user to create, or amend or delete, sequence - * features. + * features. If null in the supplied feature(s), feature type and group + * default to those for the last feature created (with initial defaults of + * "feature_1" and "Jalview"). * * @param sequences * @param features * @param create * @param ap - * @param featureType * @return */ boolean amendFeatures(final SequenceI[] sequences, final SequenceFeature[] features, boolean create, - final AlignmentPanel ap, String featureType) + final AlignmentPanel ap) { - Panel bigPanel = new Panel(new BorderLayout()); + final Panel bigPanel = new Panel(new BorderLayout()); final TextField name = new TextField(16); - final TextField source = new TextField(16); + final TextField group = new TextField(16); final TextArea description = new TextArea(3, 35); final TextField start = new TextField(8); final TextField end = new TextField(8); @@ -190,6 +196,22 @@ public class FeatureRenderer extends Button deleteButton = new Button("Delete"); deleteFeature = false; + name.addTextListener(new TextListener() + { + @Override + public void textValueChanged(TextEvent e) + { + warnIfTypeHidden(ap.alignFrame, name.getText()); + } + }); + group.addTextListener(new TextListener() + { + @Override + public void textValueChanged(TextEvent e) + { + warnIfGroupHidden(ap.alignFrame, group.getText()); + } + }); colourPanel = new FeatureColourPanel(); colourPanel.setSize(110, 15); final FeatureRenderer fr = this; @@ -233,7 +255,7 @@ public class FeatureRenderer extends featureIndex = index; name.setText(features[index].getType()); description.setText(features[index].getDescription()); - source.setText(features[index].getFeatureGroup()); + group.setText(features[index].getFeatureGroup()); start.setText(features[index].getBegin() + ""); end.setText(features[index].getEnd() + ""); @@ -269,7 +291,7 @@ public class FeatureRenderer extends tmp = new Panel(); panel.add(tmp); tmp.add(new Label(MessageManager.getString("label.group:"), Label.RIGHT)); - tmp.add(source); + tmp.add(group); tmp = new Panel(); panel.add(tmp); @@ -301,36 +323,16 @@ public class FeatureRenderer extends bigPanel.add(panel, BorderLayout.CENTER); } - if (featureType != null) - { - lastFeatureAdded = featureType; - } - else - { - if (lastFeatureAdded == null) - { - if (features[0].type != null) - { - lastFeatureAdded = features[0].type; - } - else - { - lastFeatureAdded = "feature_1"; - } - } - } - - if (lastFeatureGroupAdded == null) - { - if (features[0].featureGroup != null) - { - lastFeatureGroupAdded = features[0].featureGroup; - } - else - { - lastFeatureAdded = "Jalview"; - } - } + /* + * use defaults for type and group (and update them on Confirm) only + * if feature type has not been supplied by the caller + * (e.g. for Amend, or create features from Find) + */ + boolean useLastDefaults = features[0].getType() == null; + String featureType = useLastDefaults ? lastFeatureAdded : features[0] + .getType(); + String featureGroup = useLastDefaults ? lastFeatureGroupAdded + : features[0].getFeatureGroup(); String title = create ? MessageManager .getString("label.create_new_sequence_features") @@ -342,12 +344,10 @@ public class FeatureRenderer extends dialog.setMainPanel(bigPanel); - if (create) - { - name.setText(lastFeatureAdded); - source.setText(lastFeatureGroupAdded); - } - else + name.setText(featureType); + group.setText(featureGroup); + + if (!create) { dialog.ok.setLabel(MessageManager.getString("label.amend")); dialog.buttonPanel.add(deleteButton, 1); @@ -360,8 +360,6 @@ public class FeatureRenderer extends dialog.setVisible(false); } }); - name.setText(features[0].getType()); - source.setText(features[0].getFeatureGroup()); } start.setText(features[0].getBegin() + ""); @@ -393,74 +391,89 @@ public class FeatureRenderer extends FeaturesFile ffile = new FeaturesFile(); - if (dialog.accept) - { - lastFeatureAdded = name.getText().trim(); - lastFeatureGroupAdded = source.getText().trim(); - lastDescriptionAdded = description.getText().replace('\n', ' '); - } + /* + * only update default type and group if we used defaults + */ + final String enteredType = name.getText().trim(); + final String enteredGroup = group.getText().trim(); + final String enteredDesc = description.getText().replace('\n', ' '); - if (lastFeatureGroupAdded != null && lastFeatureGroupAdded.length() < 1) + if (dialog.accept && useLastDefaults) { - lastFeatureGroupAdded = null; + lastFeatureAdded = enteredType; + lastFeatureGroupAdded = enteredGroup; } if (!create) { - SequenceFeature sf = features[featureIndex]; if (dialog.accept) { - sf.type = lastFeatureAdded; - sf.featureGroup = lastFeatureGroupAdded; - sf.description = lastDescriptionAdded; if (!colourPanel.isGcol) { // update colour - otherwise its already done. setColour(sf.type, 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) { + // } + /* + * replace the feature by deleting it and adding a new one + * (to ensure integrity of SequenceFeatures data store) + */ + sequences[0].deleteFeature(sf); + SequenceFeature newSf = new SequenceFeature(sf, newBegin, newEnd, + enteredGroup); + newSf.setDescription(enteredDesc); + ffile.parseDescriptionHTML(newSf, false); + // amend features dialog only updates one sequence at a time + sequences[0].addSequenceFeature(newSf); + boolean typeOrGroupChanged = (!featureType.equals(sf.type) || !featureGroup + .equals(sf.featureGroup)); + ffile.parseDescriptionHTML(sf, false); - setVisible(lastFeatureAdded); // if user edited name then make sure new - // type is visible + if (typeOrGroupChanged) + { + featuresAdded(); + } } if (deleteFeature) { sequences[0].deleteFeature(sf); + // ensure Feature Settings reflects removal of feature / group + featuresAdded(); } - } else { + /* + * adding feature(s) + */ if (dialog.accept && name.getText().length() > 0) { for (int i = 0; i < sequences.length; i++) { - features[i].type = lastFeatureAdded; - features[i].featureGroup = lastFeatureGroupAdded; - features[i].description = lastDescriptionAdded; - sequences[i].addSequenceFeature(features[i]); - ffile.parseDescriptionHTML(features[i], false); + SequenceFeature sf = features[i]; + SequenceFeature sf2 = new SequenceFeature(enteredType, + enteredDesc, sf.getBegin(), sf.getEnd(), Float.NaN, + enteredGroup); + ffile.parseDescriptionHTML(sf2, false); + sequences[i].addSequenceFeature(sf2); } Color newColour = colourPanel.getBackground(); // setColour(lastFeatureAdded, fcol); - if (lastFeatureGroupAdded != null) - { - setGroupVisibility(lastFeatureGroupAdded, true); - } - setColour(lastFeatureAdded, new FeatureColour(newColour)); // was fcol - setVisible(lastFeatureAdded); - findAllFeatures(false); // different to original applet behaviour ? - // findAllFeatures(); + setColour(enteredType, new FeatureColour(newColour)); // was fcol + featuresAdded(); } else { @@ -479,4 +492,43 @@ public class FeatureRenderer extends return true; } + + protected void warnIfGroupHidden(Frame frame, String group) + { + if (featureGroups.containsKey(group) && !featureGroups.get(group)) + { + String msg = MessageManager.formatMessage("label.warning_hidden", + MessageManager.getString("label.group"), group); + showWarning(frame, msg); + } + } + + protected void warnIfTypeHidden(Frame frame, String type) + { + if (getRenderOrder().contains(type)) + { + if (!showFeatureOfType(type)) + { + String msg = MessageManager.formatMessage("label.warning_hidden", + MessageManager.getString("label.feature_type"), type); + showWarning(frame, msg); + } + } + } + + /** + * @param frame + * @param msg + */ + protected void showWarning(Frame frame, String msg) + { + JVDialog d = new JVDialog(frame, "", true, 350, 200); + Panel mp = new Panel(); + d.ok.setLabel(MessageManager.getString("action.ok")); + d.cancel.setVisible(false); + mp.setLayout(new FlowLayout()); + mp.add(new Label(msg)); + d.setMainPanel(mp); + d.setVisible(true); + } } diff --git a/src/jalview/appletgui/FeatureSettings.java b/src/jalview/appletgui/FeatureSettings.java index 1b9fbf9..b0bb372 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,11 +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.Vector; +import java.util.Set; public class FeatureSettings extends Panel implements ItemListener, MouseListener, MouseMotionListener, ActionListener, @@ -106,6 +107,7 @@ public class FeatureSettings extends Panel implements ItemListener, { fr.findAllFeatures(true); // was default - now true to make all visible } + groupPanel = new Panel(); discoverAllFeatureData(); @@ -133,17 +135,15 @@ public class FeatureSettings extends Panel implements ItemListener, add(lowerPanel, BorderLayout.SOUTH); - if (groupPanel != null) - { - groupPanel.setLayout(new GridLayout( - (fr.getFeatureGroupsSize()) / 4 + 1, 4)); // JBPNote - this was - // scaled on number of - // visible groups. seems - // broken - groupPanel.validate(); + groupPanel.setLayout(new GridLayout( + (fr.getFeatureGroupsSize()) / 4 + 1, 4)); // JBPNote - this was + // scaled on number of + // visible groups. seems + // broken + groupPanel.validate(); + + add(groupPanel, BorderLayout.NORTH); - add(groupPanel, BorderLayout.NORTH); - } frame = new Frame(); frame.add(this); final FeatureSettings me = this; @@ -326,79 +326,86 @@ public class FeatureSettings extends Panel implements ItemListener, if (fr.getAllFeatureColours() != null && fr.getAllFeatureColours().size() > 0) { - rebuildGroups(); + // rebuildGroups(); } resetTable(false); } /** - * rebuilds the group panel + * Answers the visibility of the given group, and adds a checkbox for it if + * there is not one already */ - public void rebuildGroups() + public boolean checkGroupState(String group) { - boolean rdrw = false; - if (groupPanel == null) - { - groupPanel = new Panel(); - } - else - { - rdrw = true; - groupPanel.removeAll(); - } - // TODO: JAL-964 - smoothly incorporate new group entries if panel already - // displayed and new groups present - for (String group : fr.getFeatureGroups()) - { - boolean vis = fr.checkGroupVisibility(group, false); - Checkbox check = new MyCheckbox(group, vis, false); - check.addMouseListener(this); - check.setFont(new Font("Serif", Font.BOLD, 12)); - check.addItemListener(groupItemListener); - // note - visibility seems to correlate with displayed. ???wtf ?? - check.setVisible(vis); - groupPanel.add(check); - } - if (rdrw) + boolean visible = fr.checkGroupVisibility(group, true); + + /* + * is there already a checkbox for this group? + */ + for (int g = 0; g < groupPanel.getComponentCount(); g++) { - groupPanel.validate(); + if (((Checkbox) groupPanel.getComponent(g)).getLabel().equals(group)) + { + ((Checkbox) groupPanel.getComponent(g)).setState(visible); + return visible; + } } + + /* + * add a new checkbox + */ + Checkbox check = new MyCheckbox(group, visible, false); + check.addMouseListener(this); + check.setFont(new Font("Serif", Font.BOLD, 12)); + check.addItemListener(groupItemListener); + groupPanel.add(check); + + groupPanel.validate(); + return visible; } // This routine adds and removes checkboxes depending on // 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; - if (group == null || fr.checkGroupVisibility(group, true)) { - type = tmpfeatures[index].getType(); - if (!visibleChecks.contains(type)) - { - visibleChecks.addElement(type); - } + visibleGroups.add(group); } - index++; } + + /* + * 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); } + /* + * remove any checkboxes for groups not present + */ + pruneGroups(foundGroups); + Component[] comps; int cSize = featurePanel.getComponentCount(); MyCheckbox check; @@ -408,7 +415,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--; @@ -425,24 +432,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(), @@ -458,6 +465,25 @@ public class FeatureSettings extends Panel implements ItemListener, } /** + * Remove from the groups panel any checkboxes for groups that are not in the + * foundGroups set. This enables removing a group from the display when the + * last feature in that group is deleted. + * + * @param foundGroups + */ + protected void pruneGroups(Set foundGroups) + { + for (int g = 0; g < groupPanel.getComponentCount(); g++) + { + Checkbox checkbox = (Checkbox) groupPanel.getComponent(g); + if (!foundGroups.contains(checkbox.getLabel())) + { + groupPanel.remove(checkbox); + } + } + } + + /** * update the checklist of feature types with the given type * * @param groupsChanged diff --git a/src/jalview/appletgui/Finder.java b/src/jalview/appletgui/Finder.java index 2579d91..a342736 100644 --- a/src/jalview/appletgui/Finder.java +++ b/src/jalview/appletgui/Finder.java @@ -130,7 +130,7 @@ public class Finder extends Panel implements ActionListener } if (ap.seqPanel.seqCanvas.getFeatureRenderer().amendFeatures(seqs, - features, true, ap, searchString)) + features, true, ap)) { ap.alignFrame.sequenceFeatures.setState(true); av.setShowSequenceFeatures(true); diff --git a/src/jalview/appletgui/IdPanel.java b/src/jalview/appletgui/IdPanel.java index 4cc4a3a..39a15b8 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/SeqPanel.java b/src/jalview/appletgui/SeqPanel.java index 708bc6b..c10038f 100644 --- a/src/jalview/appletgui/SeqPanel.java +++ b/src/jalview/appletgui/SeqPanel.java @@ -53,6 +53,7 @@ import java.awt.event.InputEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; +import java.util.List; import java.util.Vector; public class SeqPanel extends Panel implements MouseMotionListener, @@ -562,20 +563,19 @@ public class SeqPanel extends Panel implements MouseMotionListener, av.setSelectionGroup(null); } - SequenceFeature[] features = findFeaturesAtRes(sequence, + List features = findFeaturesAtRes(sequence, sequence.findPosition(findRes(evt))); - if (features != null && features.length > 0) + if (!features.isEmpty()) { SearchResultsI highlight = new SearchResults(); - highlight.addResult(sequence, features[0].getBegin(), - features[0].getEnd()); + highlight.addResult(sequence, features.get(0).getBegin(), features + .get(0).getEnd()); seqCanvas.highlightSearchResults(highlight); - } - if (features != null && features.length > 0) - { + SequenceFeature[] featuresArray = features + .toArray(new SequenceFeature[features.size()]); seqCanvas.getFeatureRenderer().amendFeatures( - new SequenceI[] { sequence }, features, false, ap, null); + new SequenceI[] { sequence }, featuresArray, false, ap); seqCanvas.highlightSearchResults(null); } @@ -854,13 +854,13 @@ public class SeqPanel extends Panel implements MouseMotionListener, } // use aa to see if the mouse pointer is on a - SequenceFeature[] allFeatures = findFeaturesAtRes(sequence, + List allFeatures = findFeaturesAtRes(sequence, sequence.findPosition(res)); int index = 0; - while (index < allFeatures.length) + while (index < allFeatures.size()) { - SequenceFeature sf = allFeatures[index]; + SequenceFeature sf = allFeatures.get(index); tooltipText.append(sf.getType() + " " + sf.begin + ":" + sf.end); @@ -892,40 +892,9 @@ public class SeqPanel extends Panel implements MouseMotionListener, } } - SequenceFeature[] findFeaturesAtRes(SequenceI sequence, int res) + List findFeaturesAtRes(SequenceI sequence, int res) { - Vector tmp = new Vector(); - 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)) - { - tmp.addElement(features[i]); - } - } - } - - features = new SequenceFeature[tmp.size()]; - tmp.copyInto(features); - - return features; + return seqCanvas.getFeatureRenderer().findFeaturesAtRes(sequence, res); } Tooltip tooltip; @@ -1451,25 +1420,20 @@ public class SeqPanel extends Panel implements MouseMotionListener, // DETECT RIGHT MOUSE BUTTON IN AWT if ((evt.getModifiers() & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK) { - SequenceFeature[] allFeatures = findFeaturesAtRes(sequence, + List allFeatures = findFeaturesAtRes(sequence, sequence.findPosition(res)); Vector links = null; - if (allFeatures != null) + for (int i = 0; i < allFeatures.size(); i++) { - for (int i = 0; i < allFeatures.length; i++) + SequenceFeature sf = allFeatures.get(i); + if (sf.links != null) { - if (allFeatures[i].links != null) + if (links == null) { - if (links == null) - { - links = new Vector(); - } - for (int j = 0; j < allFeatures[i].links.size(); j++) - { - links.addElement(allFeatures[i].links.elementAt(j)); - } + links = new Vector(); } + links.addAll(sf.links); } } APopupMenu popup = new APopupMenu(ap, null, links); diff --git a/src/jalview/commands/EditCommand.java b/src/jalview/commands/EditCommand.java index 21ff841..388c533 100644 --- a/src/jalview/commands/EditCommand.java +++ b/src/jalview/commands/EditCommand.java @@ -555,6 +555,7 @@ public class EditCommand implements CommandI 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, @@ -1101,8 +1102,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,51 +1123,69 @@ 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]; + SequenceFeature[] oldsf = new SequenceFeature[sf.size()]; int cSize = j - i; - for (int s = 0; s < sf.length; s++) + int s = 0; + for (SequenceFeature feature : sf) { - SequenceFeature copy = new SequenceFeature(sf[s]); + SequenceFeature copy = new SequenceFeature(feature); - oldsf[s] = copy; + oldsf[s++] = 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()); + 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())); } + // if (feature.getBegin() > feature.getEnd()) + // { + // sequence.deleteFeature(feature); + // } } if (command.editedFeatures == null) diff --git a/src/jalview/controller/AlignViewController.java b/src/jalview/controller/AlignViewController.java index bc7f212..d1d61d2 100644 --- a/src/jalview/controller/AlignViewController.java +++ b/src/jalview/controller/AlignViewController.java @@ -238,83 +238,65 @@ public class AlignViewController implements AlignViewControllerI 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()); + int iend = sq.findPosition(sqcol.getEndRes()); // see JAL-2526 + List sfs = sq.getFeatures().findFeatures(ist, + iend, featureType); + boolean overlap = false; + for (SequenceFeature sf : sfs) { - int ist = sq.findIndex(sq.getStart()); - int iend = sq.findIndex(sq.getEnd()); - if (iend < startPosition || ist > endPosition) + // future functionality - featureType == null means mark columns + // containing all displayed features + if (sf != null && (featureType.equals(sf.getType()))) { - // sequence not in region - continue; - } - for (SequenceFeature sf : sfs) - { - // future functionality - featureType == null means mark columns - // containing all displayed features - if (sf != null && (featureType.equals(sf.getType()))) - { - // 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; - } + int sfStartCol = sq.findIndex(sf.getBegin()); + int sfEndCol = sq.findIndex(sf.getEnd()); // inefficient - JAL-2526 + if (sf.isContactFeature()) + { /* - * contiguous feature - select feature positions (if any) - * within the selected region + * 'contact' feature - check for 'start' or 'end' + * position 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) + if (sfStartCol >= startPosition && sfStartCol <= endPosition) { - sfStartCol = ist; + bs.set(sfStartCol - 1); + overlap = true; } - if (sfEndCol > endPosition) + if (sfEndCol >= startPosition && sfEndCol <= endPosition) { - sfEndCol = endPosition; - } - for (; sfStartCol <= sfEndCol; sfStartCol++) - { - bs.set(sfStartCol - 1); // convert to base 0 + bs.set(sfEndCol - 1); + overlap = true; } + continue; + } + + /* + * contiguous feature - select feature positions (if any) + * within the selected region + */ + 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 + overlap = true; } } } - - if (sequenceHasFeature) + if (overlap) { nseq++; } 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/AlignmentView.java b/src/jalview/datamodel/AlignmentView.java index 5058dcf..9ca70f2 100644 --- a/src/jalview/datamodel/AlignmentView.java +++ b/src/jalview/datamodel/AlignmentView.java @@ -918,7 +918,7 @@ public class AlignmentView } if (nvismsa[0] != null) { - return new Object[] { nvismsa[0], new ColumnSelection() }; + return new Object[] { nvismsa[0], new HiddenColumns() }; } else { diff --git a/src/jalview/datamodel/AllColsIterator.java b/src/jalview/datamodel/AllColsIterator.java index c7a0bb1..c1296d5 100644 --- a/src/jalview/datamodel/AllColsIterator.java +++ b/src/jalview/datamodel/AllColsIterator.java @@ -48,13 +48,13 @@ public class AllColsIterator implements Iterator @Override public boolean hasNext() { - return current + 1 <= last; + return next <= last; } @Override public Integer next() { - if (current + 1 > last) + if (next > last) { throw new NoSuchElementException(); } diff --git a/src/jalview/datamodel/AllRowsIterator.java b/src/jalview/datamodel/AllRowsIterator.java index aefed60..b6d45f8 100644 --- a/src/jalview/datamodel/AllRowsIterator.java +++ b/src/jalview/datamodel/AllRowsIterator.java @@ -51,13 +51,13 @@ public class AllRowsIterator implements Iterator @Override public boolean hasNext() { - return current + 1 <= last; + return next <= last; } @Override public Integer next() { - if (current + 1 > last) + if (next > last) { throw new NoSuchElementException(); } diff --git a/src/jalview/datamodel/CigarArray.java b/src/jalview/datamodel/CigarArray.java index aab82a1..837a10b 100644 --- a/src/jalview/datamodel/CigarArray.java +++ b/src/jalview/datamodel/CigarArray.java @@ -20,8 +20,6 @@ */ package jalview.datamodel; -import htsjdk.samtools.Cigar; - import java.util.List; public class CigarArray extends CigarBase @@ -220,7 +218,7 @@ public class CigarArray extends CigarBase } /** - * @see Cigar.getSequenceAndDeletions + * @see CigarBase.getSequenceAndDeletions * @param GapChar * char * @return Object[][] diff --git a/src/jalview/datamodel/Mapping.java b/src/jalview/datamodel/Mapping.java index 1c196be..5a4689d 100644 --- a/src/jalview/datamodel/Mapping.java +++ b/src/jalview/datamodel/Mapping.java @@ -530,9 +530,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()); if (frange.length > 2) { vf[v].setDescription(f.getDescription() + "\nPart " + (v + 1)); @@ -541,27 +540,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/Sequence.java b/src/jalview/datamodel/Sequence.java index 9994675..9f3e7b8 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; @@ -88,6 +90,8 @@ public class Sequence extends ASequence implements SequenceI /** array of sequence features - may not be null for a valid sequence object */ public SequenceFeature[] sequenceFeatures; + private SequenceFeatures sequenceFeatureStore; + /** * Creates a new Sequence object. * @@ -127,6 +131,7 @@ public class Sequence extends ASequence implements SequenceI this.sequence = sequence2; this.start = start2; this.end = end2; + sequenceFeatureStore = new SequenceFeatures(); parseId(); checkValidRange(); } @@ -324,6 +329,13 @@ public class Sequence extends ASequence implements SequenceI @Override public synchronized boolean addSequenceFeature(SequenceFeature sf) { + if (sf.getType() == null) + { + System.err.println("SequenceFeature type may not be null: " + + sf.toString()); + return false; + } + if (sequenceFeatures == null && datasetSequence != null) { return datasetSequence.addSequenceFeature(sf); @@ -346,6 +358,8 @@ public class Sequence extends ASequence implements SequenceI temp[sequenceFeatures.length] = sf; sequenceFeatures = temp; + + sequenceFeatureStore.add(sf); return true; } @@ -361,6 +375,14 @@ public class Sequence extends ASequence implements SequenceI return; } + /* + * new way + */ + sequenceFeatureStore.delete(sf); + + /* + * old way - to be removed + */ int index = 0; for (index = 0; index < sequenceFeatures.length; index++) { @@ -420,6 +442,13 @@ public class Sequence extends ASequence implements SequenceI } @Override + public SequenceFeaturesI getFeatures() + { + return datasetSequence != null ? datasetSequence.getFeatures() + : sequenceFeatureStore; + } + + @Override public boolean addPDBId(PDBEntry entry) { if (pdbIds == null) @@ -1164,6 +1193,8 @@ public class Sequence extends ASequence implements SequenceI // 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 @@ -1483,4 +1514,17 @@ public class Sequence extends ASequence implements SequenceI } } + /** + * {@inheritDoc} + */ + @Override + public List findFeatures(int from, int to, + String... types) + { + if (datasetSequence != null) + { + return datasetSequence.findFeatures(from, to, types); + } + return sequenceFeatureStore.findFeatures(from, to, types); + } } diff --git a/src/jalview/datamodel/SequenceFeature.java b/src/jalview/datamodel/SequenceFeature.java index 15f54b9..719cf52 100755 --- a/src/jalview/datamodel/SequenceFeature.java +++ b/src/jalview/datamodel/SequenceFeature.java @@ -20,6 +20,8 @@ */ package jalview.datamodel; +import jalview.datamodel.features.FeatureLocationI; + import java.util.HashMap; import java.util.Map; import java.util.Vector; @@ -30,7 +32,7 @@ import java.util.Vector; * @author $author$ * @version $Revision$ */ -public class SequenceFeature +public class SequenceFeature implements FeatureLocationI { private static final String STATUS = "status"; @@ -176,6 +178,24 @@ public class SequenceFeature } /** + * A copy constructor that allows the begin and end positions and group to be + * modified + * + * @param sf + * @param newBegin + * @param newEnd + * @param newGroup + */ + public SequenceFeature(SequenceFeature sf, int newBegin, int newEnd, + String newGroup) + { + this(sf); + begin = newBegin; + end = newEnd; + featureGroup = newGroup; + } + + /** * Two features are considered equal if they have the same type, group, * description, start, end, phase, strand, and (if present) 'Name', ID' and * 'Parent' attributes. @@ -268,6 +288,7 @@ public class SequenceFeature * * @return DOCUMENT ME! */ + @Override public int getBegin() { return begin; @@ -283,6 +304,7 @@ public class SequenceFeature * * @return DOCUMENT ME! */ + @Override public int getEnd() { return end; @@ -340,7 +362,10 @@ public class SequenceFeature links = new Vector(); } - links.insertElementAt(labelLink, 0); + if (!links.contains(labelLink)) + { + links.insertElementAt(labelLink, 0); + } } public float getScore() @@ -538,6 +563,7 @@ 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 @@ -548,4 +574,14 @@ public class SequenceFeature } return false; } + + /** + * 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 92f797f..6c82bf3 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.List; import java.util.Vector; @@ -267,6 +269,13 @@ public interface SequenceI extends ASequenceI public SequenceFeature[] getSequenceFeatures(); /** + * Answers the object holding features for the sequence + * + * @return + */ + 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 @@ -339,7 +348,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 @@ -475,4 +484,16 @@ public interface SequenceI extends ASequenceI * list */ public List getPrimaryDBRefs(); + + /** + * Returns a (possibly empty) list of sequence features that overlap the range + * from-to (inclusive), optionally restricted to one or more specified feature + * types + * + * @param from + * @param to + * @param types + * @return + */ + List findFeatures(int from, int to, String... types); } diff --git a/src/jalview/datamodel/features/ContiguousI.java b/src/jalview/datamodel/features/ContiguousI.java new file mode 100644 index 0000000..d0b3259 --- /dev/null +++ b/src/jalview/datamodel/features/ContiguousI.java @@ -0,0 +1,8 @@ +package jalview.datamodel.features; + +public interface ContiguousI +{ + int getBegin(); // todo want long for genomic positions? + + int getEnd(); +} diff --git a/src/jalview/datamodel/features/FeatureLocationI.java b/src/jalview/datamodel/features/FeatureLocationI.java new file mode 100644 index 0000000..d6f0389 --- /dev/null +++ b/src/jalview/datamodel/features/FeatureLocationI.java @@ -0,0 +1,10 @@ +package jalview.datamodel.features; + +/** + * 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..7218b38 --- /dev/null +++ b/src/jalview/datamodel/features/FeatureStore.java @@ -0,0 +1,1056 @@ +package jalview.datamodel.features; + +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); + + static SearchCriterion byStart(final long target) + { + return new SearchCriterion() { + + @Override + boolean compare(SequenceFeature entry) + { + return entry.getBegin() >= target; + } + }; + } + + static SearchCriterion byEnd(final long target) + { + return new SearchCriterion() + { + + @Override + boolean compare(SequenceFeature entry) + { + return entry.getEnd() >= target; + } + }; + } + + 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) + { + /* + * 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 + { + if (!contains(nonNestedFeatures, feature)) + { + 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 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. If the + * non-positional features already include the new feature (by equality test), + * then it is not added, and this method returns false. The feature group is + * added to the set of distinct feature groups for non-positional features. + * + * @param feature + */ + protected boolean addNonPositionalFeature(SequenceFeature feature) + { + if (nonPositionalFeatures == null) + { + nonPositionalFeatures = new ArrayList(); + } + if (nonPositionalFeatures.contains(feature)) + { + return false; + } + + 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. If the contact feature lists already contain the + * given feature (by test for equality), does not add it and returns false. + * + * @param feature + * @return + */ + protected synchronized boolean addContactFeature(SequenceFeature feature) + { + if (contactFeatureStarts == null) + { + contactFeatureStarts = new ArrayList(); + } + if (contactFeatureEnds == null) + { + contactFeatureEnds = new ArrayList(); + } + + if (contains(contactFeatureStarts, feature)) + { + return false; + } + + /* + * 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 contains(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 none 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; + } + int start = sf.getBegin(); + int end = sf.getEnd(); + if (start <= to && end >= 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); + } + } + + /** + * Scans all positional features to check whether the given feature group is + * found, and returns true if found, else false + * + * @param featureGroup + * @return + */ + protected boolean findFeatureGroup(String featureGroup) + { + for (SequenceFeature sf : getPositionalFeatures()) + { + String group = sf.getFeatureGroup(); + if (group == featureGroup + || (group != null && group.equals(featureGroup))) + { + return true; + } + } + return false; + } + + /** + * 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()); + 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..a911666 --- /dev/null +++ b/src/jalview/datamodel/features/NCList.java @@ -0,0 +1,623 @@ +package jalview.datamodel.features; + +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..38c091e --- /dev/null +++ b/src/jalview/datamodel/features/NCNode.java @@ -0,0 +1,253 @@ +package jalview.datamodel.features; + +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/Range.java b/src/jalview/datamodel/features/Range.java new file mode 100644 index 0000000..beb2874 --- /dev/null +++ b/src/jalview/datamodel/features/Range.java @@ -0,0 +1,33 @@ +package jalview.datamodel.features; + + +public class Range implements ContiguousI +{ + final int start; + + 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); + } +} diff --git a/src/jalview/datamodel/features/RangeComparator.java b/src/jalview/datamodel/features/RangeComparator.java new file mode 100644 index 0000000..05d3f0a --- /dev/null +++ b/src/jalview/datamodel/features/RangeComparator.java @@ -0,0 +1,76 @@ +package jalview.datamodel.features; + +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..f263938 --- /dev/null +++ b/src/jalview/datamodel/features/SequenceFeatures.java @@ -0,0 +1,478 @@ +package jalview.datamodel.features; + +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(); + } + + /** + * {@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 (String featureType : varargToTypes(type)) + { + FeatureStore features = featureStore.get(featureType); + if (features != null) + { + result.addAll(features.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 (String featureType : varargToTypes(type)) + { + FeatureStore featureSet = featureStore.get(featureType); + if (featureSet != null) + { + result += featureSet.getFeatureCount(positional); + } + } + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public int getTotalFeatureLength(String... type) + { + int result = 0; + + for (String featureType : varargToTypes(type)) + { + FeatureStore featureSet = featureStore.get(featureType); + if (featureSet != null) + { + result += featureSet.getTotalFeatureLength(); + } + } + return result; + + } + + /** + * {@inheritDoc} + */ + @Override + public List getPositionalFeatures(String... type) + { + List result = new ArrayList(); + + for (String featureType : varargToTypes(type)) + { + FeatureStore featureSet = featureStore.get(featureType); + if (featureSet != null) + { + result.addAll(featureSet.getPositionalFeatures()); + } + } + return result; + } + + /** + * A convenience method that converts a vararg for feature types to an + * Iterable, replacing the value with the stored feature types if it is null + * or empty + * + * @param type + * @return + */ + protected Iterable varargToTypes(String... type) + { + if (type == null || type.length == 0) + { + /* + * no vararg parameter supplied + */ + return featureStore.keySet(); + } + + /* + * else make a copy of the list, and remove any null value just in case, + * as it would cause errors looking up the features Map + * sort in alphabetical order for consistent output behaviour + */ + List types = new ArrayList(Arrays.asList(type)); + types.remove(null); + Collections.sort(types); + return types; + } + + /** + * {@inheritDoc} + */ + @Override + public List getContactFeatures(String... type) + { + List result = new ArrayList(); + + for (String featureType : varargToTypes(type)) + { + FeatureStore featureSet = featureStore.get(featureType); + if (featureSet != null) + { + result.addAll(featureSet.getContactFeatures()); + } + } + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public List getNonPositionalFeatures(String... type) + { + List result = new ArrayList(); + + for (String featureType : varargToTypes(type)) + { + FeatureStore featureSet = featureStore.get(featureType); + if (featureSet != null) + { + 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(); + + Iterable types = varargToTypes(type); + + for (String featureType : types) + { + FeatureStore featureSet = featureStore.get(featureType); + if (featureSet != null) + { + 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(); + Iterable types = varargToTypes(type); + + for (String featureType : types) + { + /* + * check whether the feature type is present, and also + * whether it has features for the specified group + */ + FeatureStore features = featureStore.get(featureType); + if (features != null + && features.getFeatureGroups(positional).contains(group)) + { + result.addAll(features.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..6f9c884 100644 --- a/src/jalview/datamodel/xdb/embl/EmblEntry.java +++ b/src/jalview/datamodel/xdb/embl/EmblEntry.java @@ -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,26 @@ 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, + Float.NaN, group); + if (!vals.isEmpty()) { StringBuilder sb = new StringBuilder(); diff --git a/src/jalview/ext/ensembl/EnsemblGene.java b/src/jalview/ext/ensembl/EnsemblGene.java index 24e3e95..2d4d61a 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; @@ -267,22 +268,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); } } @@ -332,6 +331,7 @@ public class EnsemblGene extends EnsemblSeqProxy { splices = findFeatures(gene, SequenceOntologyI.CDS, parentId); } + SequenceFeatures.sortFeatures(splices, true); int transcriptLength = 0; final char[] geneChars = gene.getSequence(); @@ -381,7 +381,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); /* @@ -422,19 +422,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..dda77d7 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,7 +47,6 @@ import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.List; /** @@ -536,8 +536,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 +609,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 +661,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); targetSequence.addSequenceFeature(copy); /* @@ -763,8 +768,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 +781,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 +793,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 +809,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 +824,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 +865,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/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/AlignmentPanel.java b/src/jalview/gui/AlignmentPanel.java index 885d79d..2f6e3e4 100644 --- a/src/jalview/gui/AlignmentPanel.java +++ b/src/jalview/gui/AlignmentPanel.java @@ -34,6 +34,7 @@ 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.ViewportRanges; @@ -1495,11 +1496,10 @@ public class AlignmentPanel extends GAlignmentPanel implements 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++) { - StringBuilder text = new StringBuilder(); + StringBuilder text = new StringBuilder(512); String triplet = null; if (av.getAlignment().isNucleotide()) { @@ -1517,7 +1517,7 @@ public class AlignmentPanel extends GAlignmentPanel implements continue; } - int alIndex = seq.findPosition(res); + int seqPos = seq.findPosition(res); gSize = groups.length; for (g = 0; g < gSize; g++) { @@ -1529,7 +1529,7 @@ public class AlignmentPanel extends GAlignmentPanel implements .append((idWidth + (res + 1) * av.getCharWidth())) .append(",").append((av.getCharHeight() + sy)) .append("\"").append(" onMouseOver=\"toolTip('") - .append(alIndex).append(" ").append(triplet); + .append(seqPos).append(" ").append(triplet); } if (groups[g].getStartRes() < res @@ -1540,61 +1540,51 @@ public class AlignmentPanel extends GAlignmentPanel implements } } - if (features != null) + if (text.length() < 1) { - if (text.length() < 1) - { - text.append(" features = seq.getFeatures() + .findFeatures(seqPos, seqPos); + 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()); } } } 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/ChimeraViewFrame.java b/src/jalview/gui/ChimeraViewFrame.java index ec9feb7..ab6f6c8 100644 --- a/src/jalview/gui/ChimeraViewFrame.java +++ b/src/jalview/gui/ChimeraViewFrame.java @@ -100,35 +100,41 @@ public class ChimeraViewFrame extends StructureViewerBase savemenu.setVisible(false); // not yet implemented viewMenu.add(fitToWindow); - JMenuItem writeFeatures = new JMenuItem( - MessageManager.getString("label.create_chimera_attributes")); - writeFeatures.setToolTipText(MessageManager - .getString("label.create_chimera_attributes_tip")); - writeFeatures.addActionListener(new ActionListener() + /* + * exchange of Jalview features and Chimera attributes is for now + * an optionally enabled experimental feature + */ + if (Desktop.instance.showExperimental()) { - @Override - public void actionPerformed(ActionEvent e) + JMenuItem writeFeatures = new JMenuItem( + MessageManager.getString("label.create_chimera_attributes")); + writeFeatures.setToolTipText(MessageManager + .getString("label.create_chimera_attributes_tip")); + writeFeatures.addActionListener(new ActionListener() { - sendFeaturesToChimera(); - } - }); - viewerActionMenu.add(writeFeatures); - - final JMenu fetchAttributes = new JMenu( - MessageManager.getString("label.fetch_chimera_attributes")); - fetchAttributes.setToolTipText(MessageManager - .getString("label.fetch_chimera_attributes_tip")); - fetchAttributes.addMouseListener(new MouseAdapter() - { - - @Override - public void mouseEntered(MouseEvent e) + @Override + public void actionPerformed(ActionEvent e) + { + sendFeaturesToChimera(); + } + }); + viewerActionMenu.add(writeFeatures); + + final JMenu fetchAttributes = new JMenu( + MessageManager.getString("label.fetch_chimera_attributes")); + fetchAttributes.setToolTipText(MessageManager + .getString("label.fetch_chimera_attributes_tip")); + fetchAttributes.addMouseListener(new MouseAdapter() { - buildAttributesMenu(fetchAttributes); - } - }); - viewerActionMenu.add(fetchAttributes); + @Override + public void mouseEntered(MouseEvent e) + { + buildAttributesMenu(fetchAttributes); + } + }); + viewerActionMenu.add(fetchAttributes); + } } /** 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/Desktop.java b/src/jalview/gui/Desktop.java index d6c25a8..4877d7f 100644 --- a/src/jalview/gui/Desktop.java +++ b/src/jalview/gui/Desktop.java @@ -133,6 +133,8 @@ public class Desktop extends jalview.jbgui.GDesktop implements private static int DEFAULT_MIN_HEIGHT = 250; + private static final String EXPERIMENTAL_FEATURES = "EXPERIMENTAL_FEATURES"; + private JalviewChangeSupport changeSupport = new JalviewChangeSupport(); /** @@ -330,19 +332,6 @@ public class Desktop extends jalview.jbgui.GDesktop implements instance = this; doVamsasClientCheck(); - groovyShell = new JMenuItem(); - groovyShell.setText(MessageManager.getString("label.groovy_console")); - groovyShell.addActionListener(new ActionListener() - { - @Override - public void actionPerformed(ActionEvent e) - { - groovyShell_actionPerformed(); - } - }); - toolsMenu.add(groovyShell); - groovyShell.setVisible(true); - doConfigureStructurePrefs(); setTitle("Jalview " + jalview.bin.Cache.getProperty("VERSION")); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); @@ -397,6 +386,8 @@ public class Desktop extends jalview.jbgui.GDesktop implements showConsole(showjconsole); showNews.setVisible(false); + + experimentalFeatures.setSelected(showExperimental()); getIdentifiersOrgData(); @@ -493,6 +484,19 @@ public class Desktop extends jalview.jbgui.GDesktop implements }); } + /** + * Answers true if user preferences to enable experimental features is True + * (on), else false + * + * @return + */ + public boolean showExperimental() + { + String experimental = Cache.getDefault(EXPERIMENTAL_FEATURES, + Boolean.FALSE.toString()); + return Boolean.valueOf(experimental).booleanValue(); + } + public void doConfigureStructurePrefs() { // configure services @@ -2497,8 +2501,6 @@ public class Desktop extends jalview.jbgui.GDesktop implements } - protected JMenuItem groovyShell; - /** * Accessor method to quickly get all the AlignmentFrames loaded. * @@ -2584,6 +2586,7 @@ public class Desktop extends jalview.jbgui.GDesktop implements /** * Add Groovy Support to Jalview */ + @Override public void groovyShell_actionPerformed() { try @@ -3398,4 +3401,14 @@ public class Desktop extends jalview.jbgui.GDesktop implements } } } + + /** + * Sets the Preferences property for experimental features to True or False + * depending on the state of the controlling menu item + */ + @Override + protected void showExperimental_actionPerformed(boolean selected) + { + Cache.setProperty(EXPERIMENTAL_FEATURES, Boolean.toString(selected)); + } } diff --git a/src/jalview/gui/FeatureRenderer.java b/src/jalview/gui/FeatureRenderer.java index 50a8689..c6610e1 100644 --- a/src/jalview/gui/FeatureRenderer.java +++ b/src/jalview/gui/FeatureRenderer.java @@ -54,6 +54,8 @@ import javax.swing.JSpinner; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.SwingConstants; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; /** * DOCUMENT ME! @@ -64,6 +66,14 @@ import javax.swing.SwingConstants; public class FeatureRenderer extends jalview.renderer.seqfeatures.FeatureRenderer { + /* + * defaults for creating a new feature are the last created + * feature type and group + */ + static String lastFeatureAdded = "feature_1"; + + static String lastFeatureGroupAdded = "Jalview"; + Color resBoxColour; AlignmentPanel ap; @@ -85,16 +95,6 @@ public class FeatureRenderer extends } } - // // ///////////// - // // Feature Editing Dialog - // // Will be refactored in next release. - - static String lastFeatureAdded; - - static String lastFeatureGroupAdded; - - static String lastDescriptionAdded; - FeatureColourI oldcol, fcol; int featureIndex = 0; @@ -107,32 +107,73 @@ public class FeatureRenderer extends *

    • Create sequence feature from pop-up menu on selected region
    • *
    • Create features for pattern matches from Find
    • *
    + * If the supplied feature type is null, show (and update on confirm) the type + * and group of the last new feature created (with initial defaults of + * "feature_1" and "Jalview"). * * @param sequences * the sequences features are to be created on (if creating * features), or a single sequence (if amending features) * @param features * the current features at the position (if amending), or template - * new features with start/end position set (if creating) + * new feature(s) with start/end position set (if creating) * @param create * true to create features, false to amend or delete - * @param featureType - * the feature type to set on new features; if null, defaults to the - * type of the last new feature created if any, failing that to - * "feature_1" * @param alignPanel * @return */ protected boolean amendFeatures(final List sequences, final List features, boolean create, - final AlignmentPanel alignPanel, String featureType) + final AlignmentPanel alignPanel) { - featureIndex = 0; final JPanel mainPanel = new JPanel(new BorderLayout()); + final JTextField name = new JTextField(25); - final JTextField source = new JTextField(25); + name.getDocument().addDocumentListener(new DocumentListener() + { + @Override + public void insertUpdate(DocumentEvent e) + { + warnIfTypeHidden(mainPanel, name.getText()); + } + + @Override + public void removeUpdate(DocumentEvent e) + { + warnIfTypeHidden(mainPanel, name.getText()); + } + + @Override + public void changedUpdate(DocumentEvent e) + { + warnIfTypeHidden(mainPanel, name.getText()); + } + }); + + final JTextField group = new JTextField(25); + group.getDocument().addDocumentListener(new DocumentListener() + { + @Override + public void insertUpdate(DocumentEvent e) + { + warnIfGroupHidden(mainPanel, group.getText()); + } + + @Override + public void removeUpdate(DocumentEvent e) + { + warnIfGroupHidden(mainPanel, group.getText()); + } + + @Override + public void changedUpdate(DocumentEvent e) + { + warnIfGroupHidden(mainPanel, group.getText()); + } + }); + final JTextArea description = new JTextArea(3, 25); final JSpinner start = new JSpinner(); final JSpinner end = new JSpinner(); @@ -158,7 +199,7 @@ public class FeatureRenderer extends if (col != null) { fcol = new FeatureColour(col); - updateColourButton(mainPanel, colour, new FeatureColour(col)); + updateColourButton(mainPanel, colour, fcol); } } else @@ -222,7 +263,7 @@ public class FeatureRenderer extends SequenceFeature sf = features.get(index); name.setText(sf.getType()); description.setText(sf.getDescription()); - source.setText(sf.getFeatureGroup()); + group.setText(sf.getFeatureGroup()); start.setValue(new Integer(sf.getBegin())); end.setValue(new Integer(sf.getEnd())); @@ -246,8 +287,6 @@ public class FeatureRenderer extends gridPanel.add(choosePanel); } - // //////// - // //////////////////////////////////// JPanel namePanel = new JPanel(); gridPanel.add(namePanel); @@ -259,7 +298,7 @@ public class FeatureRenderer extends gridPanel.add(groupPanel); groupPanel.add(new JLabel(MessageManager.getString("label.group:"), JLabel.RIGHT)); - groupPanel.add(source); + groupPanel.add(group); JPanel colourPanel = new JPanel(); gridPanel.add(colourPanel); @@ -301,54 +340,24 @@ public class FeatureRenderer extends mainPanel.add(descriptionPanel, BorderLayout.CENTER); } + /* + * default feature type and group to that of the first feature supplied, + * or to the last feature created if not supplied (null value) + */ SequenceFeature firstFeature = features.get(0); - if (featureType != null) - { - lastFeatureAdded = featureType; - } - else - { - if (lastFeatureAdded == null) - { - if (firstFeature.type != null) - { - lastFeatureAdded = firstFeature.type; - } - else - { - lastFeatureAdded = "feature_1"; - } - } - } - - if (lastFeatureGroupAdded == null) - { - if (firstFeature.featureGroup != null) - { - lastFeatureGroupAdded = firstFeature.featureGroup; - } - else - { - lastFeatureGroupAdded = "Jalview"; - } - } - - if (create) - { - name.setText(lastFeatureAdded); - source.setText(lastFeatureGroupAdded); - } - else - { - name.setText(firstFeature.getType()); - source.setText(firstFeature.getFeatureGroup()); - } + boolean useLastDefaults = firstFeature.getType() == null; + final String featureType = useLastDefaults ? lastFeatureAdded + : firstFeature.getType(); + final String featureGroup = useLastDefaults ? lastFeatureGroupAdded + : firstFeature.getFeatureGroup(); + name.setText(featureType); + group.setText(featureGroup); start.setValue(new Integer(firstFeature.getBegin())); end.setValue(new Integer(firstFeature.getEnd())); description.setText(firstFeature.getDescription()); updateColourButton(mainPanel, colour, - (oldcol = fcol = getFeatureStyle(name.getText()))); + (oldcol = fcol = getFeatureStyle(featureType))); Object[] options; if (!create) { @@ -377,15 +386,24 @@ public class FeatureRenderer extends FeaturesFile ffile = new FeaturesFile(); - if (reply == JvOptionPane.OK_OPTION && name.getText().length() > 0) + 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) { - lastFeatureAdded = name.getText().trim(); - lastFeatureGroupAdded = source.getText().trim(); - lastDescriptionAdded = description.getText().replaceAll("\n", " "); - // TODO: determine if the null feature group is valid - if (lastFeatureGroupAdded.length() < 1) + /* + * update default values only if creating using default values + */ + if (useLastDefaults) { - lastFeatureGroupAdded = null; + lastFeatureAdded = enteredType; + lastFeatureGroupAdded = enteredGroup; + // TODO: determine if the null feature group is valid + if (lastFeatureGroupAdded.length() < 1) + { + lastFeatureGroupAdded = null; + } } } @@ -399,59 +417,68 @@ public class FeatureRenderer extends * NO_OPTION corresponds to the Delete button */ sequences.get(0).getDatasetSequence().deleteFeature(sf); + // update Feature Settings for removal of feature / group + featuresAdded(); } else if (reply == JvOptionPane.YES_OPTION) { /* * YES_OPTION corresponds to the Amend button + * 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 */ - boolean typeChanged = !lastFeatureAdded.equals(sf.type); - sf.type = lastFeatureAdded; - sf.featureGroup = lastFeatureGroupAdded; - sf.description = lastDescriptionAdded; - - setColour(sf.type, fcol); - getFeaturesDisplayed().setVisible(sf.type); - + boolean refreshSettings = (!featureType.equals(enteredType) || !featureGroup + .equals(enteredGroup)); + refreshSettings |= (fcol != oldcol); + 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); - if (typeChanged) + /* + * 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, newBegin, newEnd, + enteredGroup); + sf.setDescription(enteredDescription); + ffile.parseDescriptionHTML(newSf, false); + // amend features dialog only updates one sequence at a time + sequences.get(0).addSequenceFeature(newSf); + + if (refreshSettings) { - findAllFeatures(); + featuresAdded(); } } } else // NEW FEATURES ADDED { - if (reply == JvOptionPane.OK_OPTION && lastFeatureAdded.length() > 0) + if (reply == JvOptionPane.OK_OPTION && enteredType.length() > 0) { for (int i = 0; i < sequences.size(); i++) { SequenceFeature sf = features.get(i); - sf.type = lastFeatureAdded; - // fix for JAL-1538 - always set feature group here - sf.featureGroup = lastFeatureGroupAdded; - sf.description = lastDescriptionAdded; - sequences.get(i).addSequenceFeature(sf); - ffile.parseDescriptionHTML(sf, false); + SequenceFeature sf2 = new SequenceFeature(enteredType, + enteredDescription, sf.getBegin(), sf.getEnd(), + Float.NaN, enteredGroup); + ffile.parseDescriptionHTML(sf2, false); + sequences.get(i).addSequenceFeature(sf2); } - if (lastFeatureGroupAdded != null) - { - setGroupVisibility(lastFeatureGroupAdded, true); - } - setColour(lastFeatureAdded, fcol); - setVisible(lastFeatureAdded); + setColour(enteredType, fcol); - findAllFeatures(false); + featuresAdded(); alignPanel.paintAlignment(true); @@ -469,6 +496,42 @@ public class FeatureRenderer extends } /** + * Show a warning message if the entered type is one that is currently hidden + * + * @param panel + * @param type + */ + protected void warnIfTypeHidden(JPanel panel, String type) + { + if (getRenderOrder().contains(type)) + { + if (!showFeatureOfType(type)) + { + String msg = MessageManager.formatMessage("label.warning_hidden", + MessageManager.getString("label.feature_type"), type); + JvOptionPane.showMessageDialog(panel, msg, "", + JvOptionPane.OK_OPTION); + } + } + } + + /** + * Show a warning message if the entered group is one that is currently hidden + * + * @param panel + * @param group + */ + protected void warnIfGroupHidden(JPanel panel, String group) + { + if (featureGroups.containsKey(group) && !featureGroups.get(group)) + { + String msg = MessageManager.formatMessage("label.warning_hidden", + MessageManager.getString("label.group"), group); + JvOptionPane.showMessageDialog(panel, msg, "", JvOptionPane.OK_OPTION); + } + } + + /** * update the amend feature button dependent on the given style * * @param bigPanel diff --git a/src/jalview/gui/FeatureSettings.java b/src/jalview/gui/FeatureSettings.java index feb09fc..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; @@ -61,6 +61,7 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.util.Arrays; +import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.List; @@ -131,6 +132,11 @@ public class FeatureSettings extends JPanel implements private static final int MIN_HEIGHT = 400; + /** + * Constructor + * + * @param af + */ public FeatureSettings(AlignFrame af) { this.af = af; @@ -469,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++; } } @@ -531,27 +513,15 @@ public class FeatureSettings extends JPanel implements { boolean visible = fr.checkGroupVisibility(group, true); - if (groupPanel == null) - { - groupPanel = new JPanel(); - } - - boolean alreadyAdded = false; for (int g = 0; g < groupPanel.getComponentCount(); g++) { if (((JCheckBox) groupPanel.getComponent(g)).getText().equals(group)) { - alreadyAdded = true; ((JCheckBox) groupPanel.getComponent(g)).setSelected(visible); - break; + return visible; } } - if (alreadyAdded) - { - - return visible; - } final String grp = group; final JCheckBox check = new JCheckBox(group, visible); check.setFont(new Font("Serif", Font.BOLD, 12)); @@ -578,7 +548,7 @@ public class FeatureSettings extends JPanel implements synchronized void resetTable(String[] groupChanged) { - if (resettingTable == true) + if (resettingTable) { return; } @@ -586,69 +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(); - - // Find out which features should be visible depending on which groups - // are selected / deselected - // and recompute average width ordering + + Set displayableTypes = new HashSet(); + Set foundGroups = new HashSet(); + + /* + * 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; - - 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()) @@ -664,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; } @@ -676,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); @@ -698,6 +659,7 @@ public class FeatureSettings extends JPanel implements data[dataIndex][2] = new Boolean(true); dataIndex++; + displayableTypes.remove(type); } if (originalData == null) @@ -708,24 +670,105 @@ public class FeatureSettings extends JPanel implements System.arraycopy(data[i], 0, originalData[i], 0, 3); } } + else + { + updateOriginalData(data); + } table.setModel(new FeatureTableModel(data)); table.getColumnModel().getColumn(0).setPreferredWidth(200); - if (groupPanel != null) - { - groupPanel.setLayout(new GridLayout( - fr.getFeatureGroupsSize() / 4 + 1, 4)); - - groupPanel.validate(); - bigPanel.add(groupPanel, BorderLayout.NORTH); - } + groupPanel.setLayout(new GridLayout(fr.getFeatureGroupsSize() / 4 + 1, + 4)); + pruneGroups(foundGroups); + groupPanel.validate(); updateFeatureRenderer(data, groupChanged != null); resettingTable = false; } /** + * Updates 'originalData' (used for restore on Cancel) if we detect that + * changes have been made outwith this dialog + *
      + *
    • a new feature type added (and made visible)
    • + *
    • a feature colour changed (in the Amend Features dialog)
    • + *
    + * + * @param foundData + */ + protected void updateOriginalData(Object[][] foundData) + { + // todo LinkedHashMap instead of Object[][] would be nice + + Object[][] currentData = ((FeatureTableModel) table.getModel()) + .getData(); + for (Object[] row : foundData) + { + String type = (String) row[0]; + boolean found = false; + for (Object[] current : currentData) + { + if (type.equals(current[0])) + { + found = true; + /* + * currently dependent on object equality here; + * really need an equals method on FeatureColour + */ + if (!row[1].equals(current[1])) + { + /* + * feature colour has changed externally - update originalData + */ + for (Object[] original : originalData) + { + if (type.equals(original[0])) + { + original[1] = row[1]; + break; + } + } + } + break; + } + } + if (!found) + { + /* + * new feature detected - add to original data (on top) + */ + Object[][] newData = new Object[originalData.length + 1][3]; + for (int i = 0; i < originalData.length; i++) + { + System.arraycopy(originalData[i], 0, newData[i + 1], 0, 3); + } + newData[0] = row; + originalData = newData; + } + } + } + + /** + * Remove from the groups panel any checkboxes for groups that are not in the + * foundGroups set. This enables removing a group from the display when the + * last feature in that group is deleted. + * + * @param foundGroups + */ + protected void pruneGroups(Set foundGroups) + { + for (int g = 0; g < groupPanel.getComponentCount(); g++) + { + JCheckBox checkbox = (JCheckBox) groupPanel.getComponent(g); + if (!foundGroups.contains(checkbox.getText())) + { + groupPanel.remove(checkbox); + } + } + } + + /** * reorder data based on the featureRenderers global priority list. * * @param data @@ -1065,6 +1108,10 @@ public class FeatureSettings extends JPanel implements settingsPane.setLayout(borderLayout2); dasSettingsPane.setLayout(borderLayout3); bigPanel.setLayout(borderLayout4); + + groupPanel = new JPanel(); + bigPanel.add(groupPanel, BorderLayout.NORTH); + invert.setFont(JvSwingUtils.getLabelFont()); invert.setText(MessageManager.getString("label.invert_selection")); invert.addActionListener(new ActionListener() diff --git a/src/jalview/gui/Finder.java b/src/jalview/gui/Finder.java index 21c6c8a..625fc27 100755 --- a/src/jalview/gui/Finder.java +++ b/src/jalview/gui/Finder.java @@ -227,7 +227,7 @@ public class Finder extends GFinder } if (ap.getSeqPanel().seqCanvas.getFeatureRenderer().amendFeatures(seqs, - features, true, ap, searchString)) + features, true, ap)) { /* * ensure feature display is turned on to show the new features, diff --git a/src/jalview/gui/IdPanel.java b/src/jalview/gui/IdPanel.java index 2074900..32768b7 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/PopupMenu.java b/src/jalview/gui/PopupMenu.java index 09e3263..3de7c3c 100644 --- a/src/jalview/gui/PopupMenu.java +++ b/src/jalview/gui/PopupMenu.java @@ -1904,13 +1904,12 @@ 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, - "Jalview")); + features.add(new SequenceFeature(null, null, null, start, end, null)); } } if (ap.getSeqPanel().seqCanvas.getFeatureRenderer().amendFeatures(seqs, - features, true, ap, null)) + features, true, ap)) { ap.alignFrame.setShowSeqFeatures(true); ap.highlightSearchResults(null); diff --git a/src/jalview/gui/SeqPanel.java b/src/jalview/gui/SeqPanel.java index a2c2bd9..7dfac5e 100644 --- a/src/jalview/gui/SeqPanel.java +++ b/src/jalview/gui/SeqPanel.java @@ -183,7 +183,7 @@ public class SeqPanel extends JPanel implements MouseListener, * @param evt * @return */ - int findRes(MouseEvent evt) + int findColumn(MouseEvent evt) { int res = 0; int x = evt.getX(); @@ -642,7 +642,7 @@ public class SeqPanel extends JPanel implements MouseListener, } int seq = findSeq(evt); - int res = findRes(evt); + int res = findColumn(evt); if (seq < 0 || res < 0) { @@ -741,25 +741,27 @@ public class SeqPanel extends JPanel implements MouseListener, mouseDragged(evt); } - int res = findRes(evt); + final int column = findColumn(evt); int seq = findSeq(evt); - int pos; - if (res < 0 || seq < 0 || seq >= av.getAlignment().getHeight()) + if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight()) { return; } SequenceI sequence = av.getAlignment().getSequenceAt(seq); - if (res >= sequence.getLength()) + if (column >= sequence.getLength()) { return; } - pos = setStatusMessage(sequence, res, seq); + /* + * set status bar message, returning residue position in sequence + */ + final int pos = setStatusMessage(sequence, column, seq); if (ssm != null && pos > -1) { - mouseOverSequence(sequence, res, pos); + mouseOverSequence(sequence, column, pos); } tooltipText.setLength(6); // Cuts the buffer back to @@ -769,7 +771,8 @@ public class SeqPanel extends JPanel implements MouseListener, { for (int g = 0; g < groups.length; g++) { - if (groups[g].getStartRes() <= res && groups[g].getEndRes() >= res) + if (groups[g].getStartRes() <= column + && groups[g].getEndRes() >= column) { if (!groups[g].getName().startsWith("JTreeGroup") && !groups[g].getName().startsWith("JGroup")) @@ -785,14 +788,11 @@ public class SeqPanel extends JPanel implements MouseListener, } } - // use aa to see if the mouse pointer is on a if (av.isShowSequenceFeatures()) { - int rpos; List features = ap.getFeatureRenderer() - .findFeaturesAtRes(sequence.getDatasetSequence(), - rpos = sequence.findPosition(res)); - seqARep.appendFeatures(tooltipText, rpos, features, + .findFeaturesAtRes(sequence.getDatasetSequence(), pos); + seqARep.appendFeatures(tooltipText, pos, features, this.ap.getSeqPanel().seqCanvas.fr.getMinMax()); } if (tooltipText.length() == 6) // @@ -859,17 +859,19 @@ public class SeqPanel extends JPanel implements MouseListener, // avcontroller or viewModel /** - * Set status message in alignment panel + * Sets the status message in alignment panel, showing the sequence number + * (index) and id, residue and residue position for the given sequence and + * column position. Returns the calculated residue position in the sequence. * * @param sequence * aligned sequence object - * @param res + * @param column * alignment column * @param seq * index of sequence in alignment * @return position of res in sequence */ - int setStatusMessage(SequenceI sequence, int res, int seq) + int setStatusMessage(SequenceI sequence, final int column, int seq) { StringBuilder text = new StringBuilder(32); @@ -884,7 +886,7 @@ 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(res)); + final String displayChar = String.valueOf(sequence.getCharAt(column)); if (av.getAlignment().isNucleotide()) { residue = ResidueProperties.nucleotideName.get(displayChar); @@ -905,9 +907,9 @@ public class SeqPanel extends JPanel implements MouseListener, } int pos = -1; + pos = sequence.findPosition(column); if (residue != null) { - pos = sequence.findPosition(res); text.append(" (").append(Integer.toString(pos)).append(")"); } ap.alignFrame.statusBar.setText(text.toString()); @@ -1055,7 +1057,7 @@ public class SeqPanel extends JPanel implements MouseListener, return; } - int res = findRes(evt); + int res = findColumn(evt); if (res < 0) { @@ -1552,7 +1554,7 @@ public class SeqPanel extends JPanel implements MouseListener, List features = seqCanvas.getFeatureRenderer() .findFeaturesAtRes(sequence.getDatasetSequence(), - sequence.findPosition(findRes(evt))); + sequence.findPosition(findColumn(evt))); if (!features.isEmpty()) { @@ -1570,7 +1572,7 @@ public class SeqPanel extends JPanel implements MouseListener, */ List seqs = Collections.singletonList(sequence); seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false, - ap, null); + ap); seqCanvas.highlightSearchResults(null); } } @@ -1614,7 +1616,7 @@ public class SeqPanel extends JPanel implements MouseListener, */ public void doMousePressedDefineMode(MouseEvent evt) { - final int res = findRes(evt); + final int res = findColumn(evt); final int seq = findSeq(evt); oldSeq = seq; needOverviewUpdate = false; @@ -1673,7 +1675,7 @@ public class SeqPanel extends JPanel implements MouseListener, if (av.cursorMode) { - seqCanvas.cursorX = findRes(evt); + seqCanvas.cursorX = findColumn(evt); seqCanvas.cursorY = findSeq(evt); seqCanvas.repaint(); return; @@ -1729,7 +1731,7 @@ public class SeqPanel extends JPanel implements MouseListener, */ void showPopupMenu(MouseEvent evt) { - final int res = findRes(evt); + final int res = findColumn(evt); final int seq = findSeq(evt); SequenceI sequence = av.getAlignment().getSequenceAt(seq); List allFeatures = ap.getFeatureRenderer() @@ -1800,7 +1802,7 @@ public class SeqPanel extends JPanel implements MouseListener, */ 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/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/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/FeaturesFile.java b/src/jalview/io/FeaturesFile.java index 48eeee3..afc00ee 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); } @@ -472,198 +486,115 @@ 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()]); - SequenceFeature[] features; - for (int i = 0; i < sequences.length; i++) + /* + * sort groups alphabetically, and ensure that null group is output last + */ + List sortedGroups = new ArrayList(visibleFeatureGroups); + sortedGroups.remove(null); + Collections.sort(sortedGroups); + sortedGroups.add(null); + + 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()) + if (group != null) { - 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)); } } @@ -672,19 +603,70 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI 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 (sequenceFeature.getDescription().indexOf("") == -1) + { + out.append(""); + } + } - if (!featuresGen) + 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 +724,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 +1068,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 816346a..14574d0 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 @@ -319,8 +319,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(); @@ -331,41 +331,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; @@ -691,12 +688,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) { @@ -707,12 +715,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/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/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/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..8af3933 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()); 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/jbgui/GDesktop.java b/src/jalview/jbgui/GDesktop.java index 63ecdaf..3e3691c 100755 --- a/src/jalview/jbgui/GDesktop.java +++ b/src/jalview/jbgui/GDesktop.java @@ -23,6 +23,7 @@ package jalview.jbgui; import jalview.api.AlignmentViewPanel; import jalview.io.FileFormatException; import jalview.util.MessageManager; +import jalview.util.Platform; import java.awt.FlowLayout; import java.awt.Toolkit; @@ -99,6 +100,10 @@ public class GDesktop extends JFrame JMenuItem garbageCollect = new JMenuItem(); + protected JMenuItem groovyShell; + + protected JCheckBoxMenuItem experimentalFeatures; + protected JCheckBoxMenuItem showConsole = new JCheckBoxMenuItem(); protected JCheckBoxMenuItem showNews = new JCheckBoxMenuItem(); @@ -119,7 +124,7 @@ public class GDesktop extends JFrame e.printStackTrace(); } - if (!new jalview.util.Platform().isAMac()) + if (!Platform.isAMac()) { FileMenu.setMnemonic('F'); inputLocalFileMenuItem.setMnemonic('L'); @@ -374,6 +379,30 @@ public class GDesktop extends JFrame showNews_actionPerformed(e); } }); + groovyShell = new JMenuItem(); + groovyShell.setText(MessageManager.getString("label.groovy_console")); + groovyShell.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + groovyShell_actionPerformed(); + } + }); + experimentalFeatures = new JCheckBoxMenuItem(); + experimentalFeatures.setText(MessageManager + .getString("label.show_experimental")); + experimentalFeatures.setToolTipText(MessageManager + .getString("label.show_experimental_tip")); + experimentalFeatures.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + showExperimental_actionPerformed(experimentalFeatures.isSelected()); + } + }); + snapShotWindow.setText(MessageManager.getString("label.take_snapshot")); snapShotWindow.addActionListener(new ActionListener() { @@ -410,6 +439,8 @@ public class GDesktop extends JFrame toolsMenu.add(showConsole); toolsMenu.add(showNews); toolsMenu.add(garbageCollect); + toolsMenu.add(groovyShell); + toolsMenu.add(experimentalFeatures); // toolsMenu.add(snapShotWindow); inputMenu.add(inputLocalFileMenuItem); inputMenu.add(inputURLMenuItem); @@ -421,6 +452,14 @@ public class GDesktop extends JFrame // inputMenu.add(vamsasLoad); } + protected void showExperimental_actionPerformed(boolean selected) + { + } + + protected void groovyShell_actionPerformed() + { + } + protected void snapShotWindow_actionPerformed(ActionEvent e) { // TODO Auto-generated method stub diff --git a/src/jalview/renderer/seqfeatures/FeatureRenderer.java b/src/jalview/renderer/seqfeatures/FeatureRenderer.java index 72ac2c8..d6be4c2 100644 --- a/src/jalview/renderer/seqfeatures/FeatureRenderer.java +++ b/src/jalview/renderer/seqfeatures/FeatureRenderer.java @@ -31,6 +31,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,13 +215,6 @@ 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))) { return Color.white; @@ -269,8 +263,7 @@ 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 (!seq.getFeatures().hasFeatures()) { return null; } @@ -285,9 +278,8 @@ public class FeatureRenderer extends FeatureRendererModel } int startPos = seq.findPosition(start); - int endPos = seq.findPosition(end); + int endPos = seq.findPosition(end);// todo a performant overload of this! - int sfSize = sequenceFeatures.length; Color drawnColour = null; /* @@ -301,16 +293,10 @@ 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++) + List overlaps = seq.findFeatures(startPos, endPos, + type); + for (SequenceFeature sequenceFeature : overlaps) { - final SequenceFeature sequenceFeature = sequenceFeatures[sfindex]; - if (!sequenceFeature.type.equals(type)) - { - continue; - } - /* * a feature type may be flagged as shown but the group * an instance of it belongs to may be hidden @@ -320,16 +306,6 @@ public class FeatureRenderer extends FeatureRendererModel continue; } - /* - * check feature overlaps the target range - * TODO: efficient retrieval of features overlapping a range - */ - if (sequenceFeature.getBegin() > endPos - || sequenceFeature.getEnd() < startPos) - { - continue; - } - Color featureColour = getColour(sequenceFeature); boolean isContactFeature = sequenceFeature.isContactFeature(); @@ -350,6 +326,10 @@ public class FeatureRenderer extends FeatureRendererModel } else if (showFeature(sequenceFeature)) { + /* + * showing feature score by height of colour + * is not implemented as a selectable option + * if (av.isShowSequenceFeaturesHeight() && !Float.isNaN(sequenceFeature.score)) { @@ -365,6 +345,7 @@ public class FeatureRenderer extends FeatureRendererModel } else { + */ boolean drawn = renderFeature(g, seq, seq.findIndex(sequenceFeature.begin) - 1, seq.findIndex(sequenceFeature.end) - 1, featureColour, @@ -373,7 +354,7 @@ public class FeatureRenderer extends FeatureRendererModel { drawnColour = featureColour; } - } + /*}*/ } } } @@ -391,24 +372,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. * @@ -431,12 +394,6 @@ public class FeatureRenderer extends FeatureRendererModel */ Color findFeatureColour(SequenceI seq, int pos) { - SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures(); - if (sequenceFeatures == null || sequenceFeatures.length == 0) - { - return null; - } - /* * check for new feature added while processing */ @@ -454,31 +411,10 @@ public class FeatureRenderer extends FeatureRendererModel continue; } - for (int sfindex = 0; sfindex < sequenceFeatures.length; sfindex++) + List overlaps = seq.findFeatures(pos, pos, type); + for (SequenceFeature sequenceFeature : overlaps) { - SequenceFeature sequenceFeature = sequenceFeatures[sfindex]; - if (!sequenceFeature.type.equals(type)) - { - 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) + if (!featureGroupNotShown(sequenceFeature)) { return getColour(sequenceFeature); } 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/seqfeatures/FeatureRendererModel.java b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java index 84c9477..a8e8989 100644 --- a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java +++ b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java @@ -35,9 +35,10 @@ import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Hashtable; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -115,11 +116,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); } } } @@ -265,48 +265,39 @@ public abstract class FeatureRendererModel implements @Override public List findFeaturesAtRes(SequenceI sequence, int res) { - ArrayList tmp = new ArrayList(); - SequenceFeature[] features = sequence.getSequenceFeatures(); - - if (features != null) + List result = new ArrayList(); + if (!av.areFeaturesDisplayed()) { - 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()]); - // 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]); - } + /* + * include features at the position provided their feature type is + * displayed, and feature group is null or marked for display + */ + List features = sequence.getFeatures().findFeatures( + res, res, visibleTypes); + + 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 */ @@ -328,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++) @@ -340,94 +330,150 @@ public abstract class FeatureRendererModel implements } } } - if (minmax == null) - { - minmax = new Hashtable(); - } + // <<<<<<< HEAD + // + // ======= + // if (minmax == null) + // { + // minmax = new Hashtable(); + // } + // + // Set oldGroups = new HashSet(featureGroups.keySet()); + // >>>>>>> refs/heads/develop AlignmentI alignment = av.getAlignment(); + List allfeatures = new ArrayList(); // or HashSet? + 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)) { - if (!featuresDisplayed.isRegistered(features[index].getType())) + // <<<<<<< HEAD + /* + * features in null group are always displayed; other groups + * keep their current visibility; new groups as 'newMadeVisible' + */ + boolean groupDisplayed = true; + if (group != null) + // ======= + // continue; + // } + // + // int index = 0; + // while (index < features.length) + // { + // String fgrp = features[index].getFeatureGroup(); + // oldGroups.remove(fgrp); + // if (!featuresDisplayed.isRegistered(features[index].getType())) + // >>>>>>> refs/heads/develop { - String fgrp = features[index].getFeatureGroup(); - if (fgrp != null) + // <<<<<<< HEAD + if (featureGroups.containsKey(group)) + // ======= + // if (fgrp != null) + // >>>>>>> refs/heads/develop { - 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())) - { - allfeatures.add(features[index].getType()); - } - if (!Float.isNaN(features[index].score)) + if (groupDisplayed) { - 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) + Set types = asq.getFeatures().getFeatureTypesForGroups( + true, group); + for (String type : types) { - mm[nonpos] = new float[] { features[index].score, - features[index].score }; - - } - else - { - if (mm[nonpos][0] > features[index].score) - { - mm[nonpos][0] = features[index].score; - } - if (mm[nonpos][1] < features[index].score) + if (!allfeatures.contains(type)) // or use HashSet and no test? { - mm[nonpos][1] = features[index].score; + allfeatures.add(type); } + updateMinMax(asq, type, true); // todo: for all features? } } - index++; } } + + /* + //<<<<<<< HEAD + * mark any new feature types as visible + */ + Collections.sort(allfeatures, String.CASE_INSENSITIVE_ORDER); + if (newMadeVisible) + { + for (String type : allfeatures) + { + if (!oldfeatures.contains(type)) + { + featuresDisplayed.setVisible(type); + setOrder(type, 0); + } + } + // ======= + // * oldGroups now consists of groups that no longer + // * have any feature in them - remove these + // */ + // for (String grp : oldGroups) + // { + // featureGroups.remove(grp); + // >>>>>>> refs/heads/develop + } + 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; /** @@ -564,6 +610,13 @@ public abstract class FeatureRendererModel implements return fc.getColor(feature); } + /** + * Answers true unless the feature has a graduated colour scheme and the + * feature value lies outside the current threshold for display + * + * @param sequenceFeature + * @return + */ protected boolean showFeature(SequenceFeature sequenceFeature) { FeatureColourI fc = getFeatureStyle(sequenceFeature.type); @@ -657,7 +710,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) } @@ -881,11 +935,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; @@ -927,25 +980,31 @@ 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 null; - } - else - { - // gps = new String[_gps.size()]; - // _gps.toArray(gps); - } } 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(); + } + } 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/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..4898b42 100644 --- a/src/jalview/ws/dbsources/Uniprot.java +++ b/src/jalview/ws/dbsources/Uniprot.java @@ -262,8 +262,9 @@ public class Uniprot extends DbSourceProxyImpl { for (SequenceFeature sf : entry.getFeature()) { - sf.setFeatureGroup("Uniprot"); - sequence.addSequenceFeature(sf); + SequenceFeature copy = new SequenceFeature(sf, sf.getBegin(), + sf.getEnd(), "Uniprot"); + sequence.addSequenceFeature(copy); } } for (DBRefEntry dbr : dbRefs) diff --git a/test/jalview/analysis/AlignmentSorterTest.java b/test/jalview/analysis/AlignmentSorterTest.java new file mode 100644 index 0000000..3b9be23 --- /dev/null +++ b/test/jalview/analysis/AlignmentSorterTest.java @@ -0,0 +1,131 @@ +package jalview.analysis; + +import static org.testng.Assert.assertSame; + +import jalview.datamodel.Alignment; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.Sequence; +import jalview.datamodel.SequenceFeature; +import jalview.datamodel.SequenceI; + +import java.util.Arrays; +import java.util.List; + +import junit.extensions.PA; + +import org.testng.annotations.Test; + +public class AlignmentSorterTest +{ + @Test(groups = "Functional") + public void testSortByFeature_score() + { + SequenceI seq1 = new Sequence("Seq1", "ABC--D-EFGHIJ"); + SequenceI seq2 = new Sequence("Seq2", "ABCDEFGHIJ"); + SequenceI seq3 = new Sequence("Seq3", "ABCDE-FGHIJ"); + SequenceI seq4 = new Sequence("Seq4", "ABCDEFGHIJ"); + SequenceI[] seqs = new SequenceI[] { seq1, seq2, seq3, seq4 }; + AlignmentI al = new Alignment(seqs); + al.setDataset(null); + + /* + * sort with no score features does nothing + */ + PA.setValue(AlignmentSorter.class, "sortByFeatureCriteria", null); + + AlignmentSorter.sortByFeature(null, null, 0, al.getWidth(), al, + AlignmentSorter.FEATURE_SCORE); + assertSame(al.getSequenceAt(0), seq1); + assertSame(al.getSequenceAt(1), seq2); + assertSame(al.getSequenceAt(2), seq3); + assertSame(al.getSequenceAt(3), seq4); + + /* + * add score and non-score features + * seq1 Cath(2.0) Pfam(4.0) average 3.0 + * seq2 Cath(2.5) Metal(NaN) average 2.5 + * seq3 KD(-4), KD(3.0) average -0.5 + * seq4 Helix(NaN) - should sort as if largest score + */ + seq1.addSequenceFeature(new SequenceFeature("Cath", "", 2, 3, 2.0f, + "g1")); + seq1.addSequenceFeature(new SequenceFeature("Pfam", "", 4, 5, 4.0f, + "g2")); + seq2.addSequenceFeature(new SequenceFeature("Cath", "", 2, 3, 2.5f, + "g3")); + seq2.addSequenceFeature(new SequenceFeature("Metal", "", 2, 3, + Float.NaN, "g4")); + seq3.addSequenceFeature(new SequenceFeature("kD", "", 2, 3, -4f, "g5")); + seq3.addSequenceFeature(new SequenceFeature("kD", "", 5, 6, 3.0f, "g6")); + seq4.addSequenceFeature(new SequenceFeature("Helix", "", 2, 3, + Float.NaN, "g7")); + + /* + * 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, "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 + assertSame(al.getSequenceAt(0), seq4); // maximum 'score' + + /* + * repeat sort toggles order - now ascending + */ + 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 + assertSame(al.getSequenceAt(3), seq4); + + /* + * specify features, excluding Pfam + * seq1 average is now 2.0 + * next sort is ascending (not toggled) as for a different feature set + */ + List types = Arrays.asList(new String[] { "Cath", "kD" }); + AlignmentSorter.sortByFeature(types, null, 0, al.getWidth(), al, + AlignmentSorter.FEATURE_SCORE); + assertSame(al.getSequenceAt(0), seq3); // -0.5 + assertSame(al.getSequenceAt(1), seq1); // 2.0 + assertSame(al.getSequenceAt(2), seq2); // 2.5 + assertSame(al.getSequenceAt(3), seq4); + + /* + * specify groups, excluding g5 (kD -4 score) + * seq3 average is now 3.0 + * next sort is ascending (not toggled) as for a different group spec + */ + List groups = Arrays.asList(new String[] { "g1", "g2", "g3", + "g6" }); + AlignmentSorter.sortByFeature(types, groups, 0, al.getWidth(), al, + AlignmentSorter.FEATURE_SCORE); + assertSame(al.getSequenceAt(0), seq1); // 2.0 + assertSame(al.getSequenceAt(1), seq2); // 2.5 + assertSame(al.getSequenceAt(2), seq3); // 3.0 + assertSame(al.getSequenceAt(3), seq4); + + /* + * limit to columns 0-4, excluding 2nd feature of seq1 and seq3 + * seq1 is now 2.0, seq3 is now -4 + */ + // fails because seq1.findPosition(4) returns 4 + // although residue 4 is in column 5! - JAL-2544 + AlignmentSorter.sortByFeature(null, null, 0, 4, al, + AlignmentSorter.FEATURE_SCORE); + assertSame(al.getSequenceAt(0), seq3); // -4 + assertSame(al.getSequenceAt(1), seq1); // 2.0 + assertSame(al.getSequenceAt(2), seq2); // 2.5 + assertSame(al.getSequenceAt(3), seq4); + } + + @Test(groups = "Functional") + public void testSortByFeature_density() + { + // TODO + } +} 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/scoremodels/FeatureDistanceModelTest.java b/test/jalview/analysis/scoremodels/FeatureDistanceModelTest.java index 0577fae..4bcf5ab 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; @@ -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( @@ -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/datamodel/AllColsIteratorTest.java b/test/jalview/datamodel/AllColsIteratorTest.java index fbb20be..3942f0b 100644 --- a/test/jalview/datamodel/AllColsIteratorTest.java +++ b/test/jalview/datamodel/AllColsIteratorTest.java @@ -82,4 +82,21 @@ public class AllColsIteratorTest AllColsIterator it = new AllColsIterator(0, 3, hiddenCols); it.remove(); } + + /* + * Test iterator behaves correctly when there is only one element in the collection + */ + @Test(groups = { "Functional" }) + public void testOneElement() + { + HiddenColumns hidden = new HiddenColumns(); + AllColsIterator it = new AllColsIterator(0, 0, hidden); + int count = 0; + while (it.hasNext()) + { + it.next(); + count++; + } + assertTrue(count == 1, "hasNext() is false after 1 iteration"); + } } diff --git a/test/jalview/datamodel/AllRowsIteratorTest.java b/test/jalview/datamodel/AllRowsIteratorTest.java index fd1d29d..aeff71d 100644 --- a/test/jalview/datamodel/AllRowsIteratorTest.java +++ b/test/jalview/datamodel/AllRowsIteratorTest.java @@ -34,7 +34,7 @@ public class AllRowsIteratorTest { AlignmentI al; - Hashtable hiddenRepSequences = new Hashtable(); + Hashtable hiddenRepSequences = new Hashtable<>(); @BeforeClass public void setup() @@ -110,4 +110,21 @@ public class AllRowsIteratorTest hiddenRepSequences.put(allseqs[start], theseSeqs); } + + /* + * Test iterator behaves correctly when there is only one element in the collection + */ + @Test(groups = { "Functional" }) + public void testOneElement() + { + AllRowsIterator it = new AllRowsIterator(0, 0, al); + int count = 0; + while (it.hasNext()) + { + it.next(); + count++; + } + assertTrue(count == 1, "hasNext() is false after 1 iteration"); + } + } diff --git a/test/jalview/datamodel/SequenceTest.java b/test/jalview/datamodel/SequenceTest.java index 739ef5d..ebf4857 100644 --- a/test/jalview/datamodel/SequenceTest.java +++ b/test/jalview/datamodel/SequenceTest.java @@ -41,6 +41,8 @@ import java.util.Vector; import junit.extensions.PA; +import junit.extensions.PA; + import org.testng.Assert; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; @@ -453,7 +455,7 @@ public class SequenceTest /* * 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); @@ -548,11 +550,26 @@ 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, "sequenceFeatures")); // to be removed! + 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, "sequenceFeatures")); + assertNull(PA.getValue(sq, "sequenceFeatureStore")); + assertNull(PA.getValue(sq, "dbrefs")); + assertNotNull(PA.getValue(rds, "sequenceFeatures")); + assertNotNull(PA.getValue(rds, "sequenceFeatureStore")); + assertNotNull(PA.getValue(rds, "dbrefs")); } /** @@ -824,6 +841,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 * diff --git a/test/jalview/datamodel/features/FeatureStoreTest.java b/test/jalview/datamodel/features/FeatureStoreTest.java new file mode 100644 index 0000000..f5be818 --- /dev/null +++ b/test/jalview/datamodel/features/FeatureStoreTest.java @@ -0,0 +1,830 @@ +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", "", 0, 0, + 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", "", 0, 0, + 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 testContains() + { + assertFalse(FeatureStore.contains(null, null)); + List features = new ArrayList(); + assertFalse(FeatureStore.contains(features, null)); + + SequenceFeature sf1 = new SequenceFeature("type1", "desc1", 20, 30, 3f, + "group1"); + assertFalse(FeatureStore.contains(null, sf1)); + assertFalse(FeatureStore.contains(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.contains(features, sf2)); + assertFalse(FeatureStore.contains(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); + } +} diff --git a/test/jalview/datamodel/features/NCListTest.java b/test/jalview/datamodel/features/NCListTest.java new file mode 100644 index 0000000..3561a78 --- /dev/null +++ b/test/jalview/datamodel/features/NCListTest.java @@ -0,0 +1,680 @@ +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.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..ca227c5 --- /dev/null +++ b/test/jalview/datamodel/features/NCNodeTest.java @@ -0,0 +1,135 @@ +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 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..e58ce6a --- /dev/null +++ b/test/jalview/datamodel/features/RangeComparatorTest.java @@ -0,0 +1,62 @@ +package jalview.datamodel.features; + +import static org.testng.Assert.assertEquals; + +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..f4ec05b --- /dev/null +++ b/test/jalview/datamodel/features/SequenceFeaturesTest.java @@ -0,0 +1,1166 @@ +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.Set; + +import org.testng.annotations.Test; + +public class SequenceFeaturesTest +{ + @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")); + + /* + * 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); + + // 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 + */ + Iterable types = sf.varargToTypes(); + Iterator iterator = types.iterator(); + assertTrue(iterator.hasNext()); + assertEquals(iterator.next(), "Cath"); + assertTrue(iterator.hasNext()); + assertEquals(iterator.next(), "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()); + assertEquals(iterator.next(), "Cath"); + assertTrue(iterator.hasNext()); + assertEquals(iterator.next(), "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()); + assertEquals(iterator.next(), "Cath"); + assertTrue(iterator.hasNext()); + assertEquals(iterator.next(), "Metal"); + assertFalse(iterator.hasNext()); + + /* + * one type specified + */ + types = sf.varargToTypes("Metal"); + iterator = types.iterator(); + assertTrue(iterator.hasNext()); + assertEquals(iterator.next(), "Metal"); + assertFalse(iterator.hasNext()); + + /* + * two types specified - get sorted alphabetically + */ + types = sf.varargToTypes("Metal", "Helix"); + iterator = types.iterator(); + assertTrue(iterator.hasNext()); + assertEquals(iterator.next(), "Helix"); + assertTrue(iterator.hasNext()); + assertEquals(iterator.next(), "Metal"); + assertFalse(iterator.hasNext()); + + /* + * null type included - should get removed + */ + types = sf.varargToTypes("Metal", null, "Helix"); + iterator = types.iterator(); + assertTrue(iterator.hasNext()); + assertEquals(iterator.next(), "Helix"); + assertTrue(iterator.hasNext()); + assertEquals(iterator.next(), "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"); + } +} diff --git a/test/jalview/ext/ensembl/EnsemblGeneTest.java b/test/jalview/ext/ensembl/EnsemblGeneTest.java index 6cfd85b..edecc23 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)); } /** diff --git a/test/jalview/ext/ensembl/EnsemblSeqProxyTest.java b/test/jalview/ext/ensembl/EnsemblSeqProxyTest.java index e977233..c8fa3c2 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; @@ -166,6 +168,8 @@ public class EnsemblSeqProxyTest Alignment ral = new Alignment(sqs); for (SequenceI tr : trueSqs) { + // 12/05/2017 failing for EnsemblCdna which is returning protein + // Ensembl helpdesk ticket 187998 SequenceI[] rseq; Assert.assertNotNull( rseq = ral.findSequenceMatch(tr.getName()), @@ -269,15 +273,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/io/FeaturesFileTest.java b/test/jalview/io/FeaturesFileTest.java index d6f1e8b..d59c6bb 100644 --- a/test/jalview/io/FeaturesFileTest.java +++ b/test/jalview/io/FeaturesFileTest.java @@ -39,6 +39,10 @@ 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; @@ -406,6 +410,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" @@ -415,28 +420,39 @@ public class FeaturesFileTest featuresFile.parse(al.getDataset(), colours, false); /* - * first with no features displayed + * 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\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); @@ -446,19 +462,115 @@ 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"; 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 2aff5cc..410263c 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; @@ -96,6 +97,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 +120,18 @@ public class JSONFileTest // create and add sequence features SequenceFeature seqFeature2 = new SequenceFeature("feature_x", - "desciption", "status", 6, 15, "Jalview"); + "description", "status", 6, 15, "Jalview"); SequenceFeature seqFeature3 = new SequenceFeature("feature_x", - "desciption", "status", 9, 18, "Jalview"); + "description", "status", 9, 18, "Jalview"); SequenceFeature seqFeature4 = new SequenceFeature("feature_x", - "desciption", "status", 9, 18, "Jalview"); + "description", "status", 9, 18, "Jalview"); + // non-positional feature: + SequenceFeature seqFeature5 = new SequenceFeature("Domain", + "My description", "status", 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 +465,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 +499,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 +524,6 @@ public class JSONFileTest private boolean featuresMatched(SequenceI seq1, SequenceI seq2) { - boolean matched = false; try { if (seq1 == null && seq2 == null) @@ -518,52 +531,48 @@ 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.getBegin() == out.getBegin() && in.getEnd() == out.getEnd() + && in.getScore() == out.getScore() + && in.getFeatureGroup().equals(out.getFeatureGroup()) + && in.getType().equals(out.getType())) { - 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.out.println("matched count >>>>>> " + matchedCount); - if (testSize == matchedCount) - { - matched = true; + 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; } /** @@ -599,7 +608,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/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..59935dd 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().length); + SequenceFeature sf = newseqs.get(0).getSequenceFeatures()[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/FeatureRendererTest.java b/test/jalview/renderer/seqfeatures/FeatureRendererTest.java new file mode 100644 index 0000000..ab5c137 --- /dev/null +++ b/test/jalview/renderer/seqfeatures/FeatureRendererTest.java @@ -0,0 +1,235 @@ +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.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 testFindFeaturesAtRes() + { + String seqData = ">s1\nabcdefghijklmnopqrstuvwxyz\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.findFeaturesAtRes(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", 5, 15, 1f, + "Group1"); + seq.addSequenceFeature(sf2); + SequenceFeature sf3 = new SequenceFeature("Type3", "Desc", 5, 15, 1f, + "Group2"); + seq.addSequenceFeature(sf3); + SequenceFeature sf4 = new SequenceFeature("Type3", "Desc", 5, 15, 1f, + null); // null group is always treated as visible + seq.addSequenceFeature(sf4); + + /* + * add contact features + */ + SequenceFeature sf5 = new SequenceFeature("Disulphide Bond", "Desc", 4, + 12, 1f, "Group1"); + seq.addSequenceFeature(sf5); + SequenceFeature sf6 = new SequenceFeature("Disulphide Bond", "Desc", 4, + 12, 1f, "Group2"); + seq.addSequenceFeature(sf6); + SequenceFeature sf7 = new SequenceFeature("Disulphide Bond", "Desc", 4, + 12, 1f, null); + seq.addSequenceFeature(sf7); + + /* + * let feature renderer discover features (and make visible) + */ + fr.findAllFeatures(true); + features = fr.findFeaturesAtRes(seq, 12); // 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.findFeaturesAtRes(seq, 11); + 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.findFeaturesAtRes(seq, 12); + 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.findFeaturesAtRes(seq, 12); + assertEquals(features.size(), 3); // no sf2, sf3, sf6 + assertTrue(features.contains(sf4)); + assertTrue(features.contains(sf5)); + assertTrue(features.contains(sf7)); + } +}