package jalview.api;
import java.util.Collection;
-import java.util.Iterator;
+import java.util.Set;
public interface FeaturesDisplayedI
{
- Iterator<String> getVisibleFeatures();
+ /**
+ * answers an unmodifiable view of the set of visible feature types
+ */
+ Set<String> getVisibleFeatures();
boolean isVisible(String featureType);
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<String> featureTypes);
boolean isRegistered(String type);
*/
package jalview.appletgui;
-import jalview.datamodel.Sequence;
import jalview.datamodel.SequenceFeature;
import jalview.datamodel.SequenceGroup;
import jalview.datamodel.SequenceI;
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
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<String> nlinks = urlProvider.getLinksForMenu();
- 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);
}
}
}
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;
/** array of sequence features - may not be null for a valid sequence object */
public SequenceFeature[] sequenceFeatures;
+ private SequenceFeatures sequenceFeatureStore;
+
/**
* Creates a new Sequence object.
*
this.sequence = sequence2;
this.start = start2;
this.end = end2;
+ sequenceFeatureStore = new SequenceFeatures();
parseId();
checkValidRange();
}
@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);
temp[sequenceFeatures.length] = sf;
sequenceFeatures = temp;
+
+ sequenceFeatureStore.add(sf);
return true;
}
return;
}
+ /*
+ * new way
+ */
+ sequenceFeatureStore.delete(sf);
+
+ /*
+ * old way - to be removed
+ */
int index = 0;
for (index = 0; index < sequenceFeatures.length; index++)
{
}
@Override
+ public SequenceFeaturesI getFeatures()
+ {
+ return datasetSequence != null ? datasetSequence.getFeatures()
+ : sequenceFeatureStore;
+ }
+
+ @Override
public boolean addPDBId(PDBEntry entry)
{
if (pdbIds == null)
}
}
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<SequenceFeature> findFeatures(int from, int to,
+ String... types)
+ {
+ if (datasetSequence != null)
+ {
+ return datasetSequence.findFeatures(from, to, types);
+ }
+ return sequenceFeatureStore.findFeatures(from, to, types);
+ }
}
*/
package jalview.datamodel;
+import jalview.datamodel.features.FeatureLocationI;
+
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
* @author $author$
* @version $Revision$
*/
-public class SequenceFeature
+public class SequenceFeature implements FeatureLocationI
{
private static final String STATUS = "status";
*
* @return DOCUMENT ME!
*/
+ @Override
public int getBegin()
{
return begin;
*
* @return DOCUMENT ME!
*/
+ @Override
public int getEnd()
{
return end;
* 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
}
return false;
}
+
+ /**
+ * Answers true if the sequence has zero start and end position
+ *
+ * @return
+ */
+ public boolean isNonPositional()
+ {
+ return begin == 0 && end == 0;
+ }
}
*/
package jalview.datamodel;
+import jalview.datamodel.features.SequenceFeaturesI;
+
import java.util.List;
import java.util.Vector;
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
/**
* 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
* list
*/
public List<DBRefEntry> 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<SequenceFeature> findFeatures(int from, int to, String... types);
}
--- /dev/null
+package jalview.datamodel.features;
+
+public interface ContiguousI
+{
+ int getBegin(); // todo want long for genomic positions?
+
+ int getEnd();
+}
--- /dev/null
+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();
+}
--- /dev/null
+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<ContiguousI> rc)
+ {
+ return new SearchCriterion()
+ {
+
+ @Override
+ boolean compare(SequenceFeature entry)
+ {
+ return rc.compare(entry, to) >= 0;
+ }
+ };
+ }
+ }
+
+ Comparator<ContiguousI> startOrdering = new RangeComparator(true);
+
+ Comparator<ContiguousI> endOrdering = new RangeComparator(false);
+
+ /*
+ * Non-positional features have no (zero) start/end position.
+ * Kept as a separate list in case this criterion changes in future.
+ */
+ List<SequenceFeature> 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<SequenceFeature> nonNestedFeatures;
+
+ /*
+ * contact features ordered by first contact position
+ */
+ List<SequenceFeature> contactFeatureStarts;
+
+ /*
+ * contact features ordered by second contact position
+ */
+ List<SequenceFeature> 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<SequenceFeature> nestedFeatures;
+
+ /*
+ * Feature groups represented in stored positional features
+ * (possibly including null)
+ */
+ Set<String> positionalFeatureGroups;
+
+ /*
+ * Feature groups represented in stored non-positional features
+ * (possibly including null)
+ */
+ Set<String> 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<SequenceFeature>();
+ positionalFeatureGroups = new HashSet<String>();
+ nonPositionalFeatureGroups = new HashSet<String>();
+ 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 (!nonNestedFeatures.contains(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<SequenceFeature>();
+ }
+ 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<SequenceFeature>(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, startOrdering));
+
+ /*
+ * 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<SequenceFeature>();
+ }
+ if (contactFeatureEnds == null)
+ {
+ contactFeatureEnds = new ArrayList<SequenceFeature>();
+ }
+
+ // TODO binary search for insertion points!
+ if (contactFeatureStarts.contains(feature))
+ {
+ return false;
+ }
+
+ contactFeatureStarts.add(feature);
+ Collections.sort(contactFeatureStarts, startOrdering);
+ contactFeatureEnds.add(feature);
+ Collections.sort(contactFeatureEnds, endOrdering);
+
+ return true;
+ }
+
+ /**
+ * 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<SequenceFeature> findOverlappingFeatures(long start, long end)
+ {
+ List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+
+ 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<SequenceFeature> 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<SequenceFeature> 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<SequenceFeature> 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<SequenceFeature> 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<SequenceFeature> 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<SequenceFeature> getPositionalFeatures()
+ {
+ /*
+ * add non-nested features (may be all features for many cases)
+ */
+ List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+ 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<SequenceFeature> getContactFeatures()
+ {
+ if (contactFeatureStarts == null)
+ {
+ return Collections.emptyList();
+ }
+ return new ArrayList<SequenceFeature>(contactFeatureStarts);
+ }
+
+ /**
+ * Answers a list of all non-positional features. If there are none, returns
+ * an immutable empty list.
+ *
+ * @return
+ */
+ public List<SequenceFeature> getNonPositionalFeatures()
+ {
+ if (nonPositionalFeatures == null)
+ {
+ return Collections.emptyList();
+ }
+ return new ArrayList<SequenceFeature>(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
+ */
+ 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<String> getFeatureGroups(boolean positionalFeatures)
+ {
+ if (positionalFeatures)
+ {
+ return Collections.unmodifiableSet(positionalFeatureGroups);
+ }
+ else
+ {
+ return nonPositionalFeatureGroups == null ? Collections
+ .<String> 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 int binarySearch(List<SequenceFeature> 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;
+ }
+}
--- /dev/null
+package jalview.datamodel.features;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * An adapted implementation of NCList as described in the paper
+ *
+ * <pre>
+ * 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
+ * </pre>
+ */
+public class NCList<T extends ContiguousI>
+{
+ /*
+ * 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<NCNode<T>> subranges;
+
+ /*
+ * a comparator to sort intervals by start position ascending, with
+ * longer (enclosing) intervals preceding those they enclose
+ */
+ Comparator<ContiguousI> intervalSorter = new RangeComparator(true);
+
+ /**
+ * Constructor given a list of things that are each located on a contiguous
+ * interval. Note that the constructor may reorder the list.
+ * <p>
+ * We assume here that for each range, start <= end. Behaviour for reverse
+ * ordered ranges is undefined.
+ *
+ * @param ranges
+ */
+ public NCList(List<T> 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<T> ranges)
+ {
+ /*
+ * sort by start ascending so that contained intervals
+ * follow their containing interval
+ */
+ Collections.sort(ranges, intervalSorter);
+
+ List<Range> 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<T>(ranges.subList(sublist.start,
+ sublist.end + 1)));
+ }
+
+ size = ranges.size();
+ }
+
+ public NCList(T entry)
+ {
+ this();
+ subranges.add(new NCNode<T>(entry));
+ size = 1;
+ }
+
+ public NCList()
+ {
+ subranges = new ArrayList<NCNode<T>>();
+ }
+
+ /**
+ * Traverses the sorted ranges to identify sublists, within which each
+ * interval contains the one that follows it
+ *
+ * @param ranges
+ * @return
+ */
+ protected List<Range> buildSubranges(List<T> ranges)
+ {
+ List<Range> sublists = new ArrayList<Range>();
+
+ 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<T>(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<T> subrange = subranges.get(j);
+
+ if (end < subrange.getBegin() && !overlapping && !enclosing)
+ {
+ /*
+ * new entry lies between subranges j-1 j
+ */
+ subranges.add(j, new NCNode<T>(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<T>(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<T>(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<T> 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<T> newNCList = new NCList<T>();
+ newNCList.addNodes(subranges.subList(i, j + 1));
+ NCNode<T> newNode = new NCNode<T>(entry, newNCList);
+ for (int k = j; k >= i; k--)
+ {
+ subranges.remove(k);
+ }
+ subranges.add(i, newNode);
+ }
+
+ protected void addNodes(List<NCNode<T>> nodes)
+ {
+ for (NCNode<T> 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<T> findOverlaps(long from, long to)
+ {
+ List<T> result = new ArrayList<T>();
+
+ 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<T> 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<T> 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<T> subrange : subranges)
+ {
+ if (subrange.getEnd() >= from)
+ {
+ return i;
+ }
+ i++;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Formats the tree as a bracketed list e.g.
+ *
+ * <pre>
+ * [1-100 [10-30 [10-20]], 15-30 [20-20]]
+ * </pre>
+ */
+ @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<T> 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.
+ * <p>
+ * Each subrange must lie within start-end (inclusive). Subranges must be
+ * ordered by start position ascending.
+ * <p>
+ *
+ * @param start
+ * @param end
+ * @return
+ */
+ boolean isValid(final int start, final int end)
+ {
+ int lastStart = start;
+ for (NCNode<T> 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<T> getEntries()
+ {
+ List<T> result = new ArrayList<T>();
+ getEntries(result);
+ return result;
+ }
+
+ /**
+ * Adds all contained entries to the given list
+ *
+ * @param result
+ */
+ void getEntries(List<T> result)
+ {
+ for (NCNode<T> 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<T> subrange = subranges.get(i);
+ NCList<T> 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, intervalSorter);
+ }
+ size--;
+ return true;
+ }
+ else
+ {
+ if (subRegions != null && subRegions.delete(entry))
+ {
+ size--;
+ subrange.deleteSubRegionsIfEmpty();
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
--- /dev/null
+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 <V>
+ */
+class NCNode<V extends ContiguousI> 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<V> subregions;
+
+ /**
+ * Constructor given a list of ranges
+ *
+ * @param ranges
+ */
+ NCNode(List<V> ranges)
+ {
+ build(ranges);
+ }
+
+ /**
+ * Constructor given a single range
+ *
+ * @param range
+ */
+ NCNode(V range)
+ {
+ List<V> ranges = new ArrayList<V>();
+ ranges.add(range);
+ build(ranges);
+ }
+
+ NCNode(V entry, NCList<V> newNCList)
+ {
+ region = entry;
+ subregions = newNCList;
+ size = 1 + newNCList.size();
+ }
+
+ /**
+ * @param ranges
+ */
+ protected void build(List<V> ranges)
+ {
+ size = ranges.size();
+
+ if (!ranges.isEmpty())
+ {
+ region = ranges.get(0);
+ }
+ if (ranges.size() > 1)
+ {
+ subregions = new NCList<V>(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.
+ *
+ * <pre>
+ * [1-100 [10-30 [10-20]], 15-30 [20-20]]
+ * </pre>
+ */
+ @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<V> 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<V>(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<V> 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<V> 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;
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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,
+ *
+ * @author gmcarstairs
+ *
+ */
+public class RangeComparator implements Comparator<ContiguousI>
+{
+ boolean byStart;
+
+ @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;
+ }
+
+ /**
+ * Constructor
+ *
+ * @param byStartPosition
+ * if true, order based on start position, if false by end position
+ */
+ public RangeComparator(boolean byStartPosition)
+ {
+ byStart = byStartPosition;
+ }
+}
--- /dev/null
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+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
+{
+
+ /*
+ * map from feature type to structured store of features for that type
+ * null types are permitted (but not a good idea!)
+ */
+ private Map<String, FeatureStore> 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<String, FeatureStore>());
+ }
+
+ /**
+ * {@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<SequenceFeature> findFeatures(int from, int to,
+ String... type)
+ {
+ List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+
+ 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<SequenceFeature> getAllFeatures(String... type)
+ {
+ List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+
+ result.addAll(getPositionalFeatures(type));
+
+ result.addAll(getNonPositionalFeatures(type));
+
+ return result;
+ }
+
+ /**
+ * {@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<SequenceFeature> getPositionalFeatures(String... type)
+ {
+ List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+
+ 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<String> 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
+ */
+ List<String> types = new ArrayList<String>(Arrays.asList(type));
+ types.remove(null);
+ return types;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<SequenceFeature> getContactFeatures(String... type)
+ {
+ List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+
+ for (String featureType : varargToTypes(type))
+ {
+ FeatureStore featureSet = featureStore.get(featureType);
+ if (featureSet != null)
+ {
+ result.addAll(featureSet.getContactFeatures());
+ }
+ }
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<SequenceFeature> getNonPositionalFeatures(String... type)
+ {
+ List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+
+ 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<String> getFeatureGroups(boolean positionalFeatures,
+ String... type)
+ {
+ Set<String> groups = new HashSet<String>();
+
+ Iterable<String> 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<String> getFeatureTypesForGroups(boolean positionalFeatures,
+ String... groups)
+ {
+ Set<String> result = new HashSet<String>();
+
+ for (Entry<String, FeatureStore> featureType : featureStore.entrySet())
+ {
+ Set<String> 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<String> getFeatureTypes()
+ {
+ Set<String> types = new HashSet<String>();
+ for (Entry<String, FeatureStore> entry : featureStore.entrySet())
+ {
+ if (!entry.getValue().isEmpty())
+ {
+ types.add(entry.getKey());
+ }
+ }
+ return types;
+ }
+
+ /**
+ * {@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;
+ }
+}
--- /dev/null
+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<SequenceFeature> findFeatures(int from, int to,
+ String... type);
+
+ /**
+ * Answers a list of all features stored, optionally restricted to specified
+ * types, in no particular guaranteed order
+ *
+ * @param type
+ * @return
+ */
+ List<SequenceFeature> getAllFeatures(String... type);
+
+ /**
+ * 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<SequenceFeature> getPositionalFeatures(
+ String... type);
+
+ /**
+ * Answers a list of all contact features, optionally restricted to specified
+ * types, in no particular guaranteed order
+ *
+ * @return
+ */
+ List<SequenceFeature> 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<SequenceFeature> 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<String> 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<String> getFeatureTypesForGroups(
+ boolean positionalFeatures, String... groups);
+
+ /**
+ * Answers a set of the distinct feature types for which a feature is stored
+ *
+ * @return
+ */
+ Set<String> getFeatureTypes();
+
+ /**
+ * 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);
+}
\ No newline at end of file
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;
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;
private boolean handlingUpdate = false;
/**
- * contains a float[3] for each feature type string. created by setTableData
+ * holds {featureCount, totalExtent} for each feature type
*/
Map<String, float[]> typeWidth = null;
@Override
synchronized public void discoverAllFeatureData()
{
- Vector<String> allFeatures = new Vector<String>();
- Vector<String> allGroups = new Vector<String>();
- 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<String> allGroups = new HashSet<String>();
+ 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++;
}
}
synchronized void resetTable(String[] groupChanged)
{
- if (resettingTable == true)
+ if (resettingTable)
{
return;
}
typeWidth = new Hashtable<String, float[]>();
// TODO: change avWidth calculation to 'per-sequence' average and use long
// rather than float
- float[] avWidth = null;
- SequenceFeature[] tmpfeatures;
- String group = null, type;
- Vector<String> visibleChecks = new Vector<String>();
-
- // Find out which features should be visible depending on which groups
- // are selected / deselected
- // and recompute average width ordering
+
+ Set<String> displayableTypes = new HashSet<String>();
+
+ /*
+ * 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<String> groups = seq.getFeatures().getFeatureGroups(true);
+ Set<String> visibleGroups = new HashSet<String>();
+ 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
+ }
+
+ /*
+ * get distinct feature types for visible groups
+ * record distinct visible types, and their count and total length
+ */
+ Set<String> 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())
List<String> 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;
}
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);
data[dataIndex][2] = new Boolean(true);
dataIndex++;
+ displayableTypes.remove(type);
}
if (originalData == null)
{
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<String> 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);
}
}
}
float sampleCol = alwidth / (float) od.getWidth();
float sampleRow = alheight / (float) od.getSequencesHeight();
+ long start = System.currentTimeMillis();
buildImage(sampleRow, sampleCol);
// check for conservation annotation to make sure overview works for DNA too
}
}
+ System.out.println("Overview took "
+ + (System.currentTimeMillis() - start) + "ms");
System.gc();
resizing = false;
+++ /dev/null
-/*
- * 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 <http://www.gnu.org/licenses/>.
- * 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
-{
-
-}
+++ /dev/null
-/*
- * 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 <http://www.gnu.org/licenses/>.
- * 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<id or empty for whole matrix>\tname\ttype\tvalue
- * ROW\tRow i label (ID)/tPrinciple text/tprinciple description/t...
- * COLUMN\t(similar, optional).. .. <float>\t<float>...(column-wise data for row
- * i)
- */
-
-public class MatrixFile extends FileParse
-{
-
-}
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<DBRefEntry> comparator = new Comparator<DBRefEntry>()
{
{
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("</i>");
+ 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("<br>");
- }
- 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("<br>").append(ELLIPSIS).append(COMMA).append(source)
- .append(COMMA).append(ELLIPSIS);
+ lineLength = 0;
+ countForSource = 0;
+ sourceCount++;
}
- if (ellipsis)
+ if (sourceCount > MAX_SOURCES && summary)
{
- sb.append("<br>(");
- 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("<br>");
+ }
+ 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("</i>");
- return maxWidth;
+ if (moreSources)
+ {
+ sb.append("<br>").append(source)
+ .append(COMMA).append(ELLIPSIS);
+ }
+ if (ellipsis)
+ {
+ sb.append("<br>(");
+ sb.append(MessageManager.getString("label.output_seq_details"));
+ sb.append(")");
+ }
+
+ return maxLineLength;
}
public void createTooltipAnnotationReport(final StringBuilder tip,
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
+import java.util.List;
public class FeatureRenderer extends FeatureRendererModel
{
int startPos = seq.findPosition(start);
int endPos = seq.findPosition(end);
- int sfSize = sequenceFeatures.length;
Color drawnColour = null;
/*
continue;
}
- // loop through all features in sequence to find
- // current feature to render
- for (int sfindex = 0; sfindex < sfSize; sfindex++)
+ List<SequenceFeature> 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
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();
}
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))
{
}
else
{
+ */
boolean drawn = renderFeature(g, seq,
seq.findIndex(sequenceFeature.begin) - 1,
seq.findIndex(sequenceFeature.end) - 1, featureColour,
{
drawnColour = featureColour;
}
- }
+ /*}*/
}
}
}
}
/**
- * 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.
*
*/
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
*/
continue;
}
- for (int sfindex = 0; sfindex < sequenceFeatures.length; sfindex++)
+ List<SequenceFeature> 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);
}
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;
synchronized (fd)
{
fd.clear();
- java.util.Iterator<String> fdisp = _fr.getFeaturesDisplayed()
- .getVisibleFeatures();
- while (fdisp.hasNext())
+ for (String type : _fr.getFeaturesDisplayed()
+ .getVisibleFeatures())
{
- fd.setVisible(fdisp.next());
+ fd.setVisible(type);
}
}
}
@Override
public List<SequenceFeature> findFeaturesAtRes(SequenceI sequence, int res)
{
- ArrayList<SequenceFeature> tmp = new ArrayList<SequenceFeature>();
- SequenceFeature[] features = sequence.getSequenceFeatures();
-
- if (features != null)
+ List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+ 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<String> 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<SequenceFeature> 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
*/
}
FeaturesDisplayedI featuresDisplayed = av.getFeaturesDisplayed();
- ArrayList<String> allfeatures = new ArrayList<String>();
- ArrayList<String> oldfeatures = new ArrayList<String>();
+ Set<String> oldfeatures = new HashSet<String>();
if (renderOrder != null)
{
for (int i = 0; i < renderOrder.length; i++)
}
}
}
- if (minmax == null)
- {
- minmax = new Hashtable<String, float[][]>();
- }
+
AlignmentI alignment = av.getAlignment();
+ List<String> allfeatures = new ArrayList<String>(); // or HashSet?
+
for (int i = 0; i < alignment.getHeight(); i++)
{
SequenceI asq = alignment.getSequenceAt(i);
- SequenceFeature[] features = asq.getSequenceFeatures();
-
- if (features == null)
+ for (String group : asq.getFeatures().getFeatureGroups(true))
{
- continue;
- }
-
- int index = 0;
- while (index < features.length)
- {
- if (!featuresDisplayed.isRegistered(features[index].getType()))
+ if (group == null)
{
- String fgrp = features[index].getFeatureGroup();
- if (fgrp != null)
- {
- Boolean groupDisplayed = featureGroups.get(fgrp);
- if (groupDisplayed == null)
- {
- groupDisplayed = Boolean.valueOf(newMadeVisible);
- featureGroups.put(fgrp, groupDisplayed);
- }
- if (!groupDisplayed.booleanValue())
- {
- index++;
- continue;
- }
- }
- if (!(features[index].begin == 0 && features[index].end == 0))
- {
- // 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);
- }
- }
+ continue;
}
- if (!allfeatures.contains(features[index].getType()))
+ Boolean groupDisplayed = featureGroups.get(group);
+ if (groupDisplayed == null)
{
- allfeatures.add(features[index].getType());
+ groupDisplayed = Boolean.valueOf(newMadeVisible);
+ featureGroups.put(group, groupDisplayed);
}
- 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)
+ Set<String> types = asq.getFeatures().getFeatureTypesForGroups(
+ true, group);
+ for (String type : types)
{
- mm = new float[][] { null, null };
- minmax.put(features[index].getType(), mm);
- }
- if (mm[nonpos] == null)
- {
- mm[nonpos] = new float[] { features[index].score,
- features[index].score };
-
- }
- else
- {
- if (mm[nonpos][0] > features[index].score)
+ if (!allfeatures.contains(type)) // or use HashSet and no test?
{
- mm[nonpos][0] = features[index].score;
- }
- if (mm[nonpos][1] < features[index].score)
- {
- mm[nonpos][1] = features[index].score;
+ allfeatures.add(type);
}
+ updateMinMax(asq, type, true); // todo: for all features?
}
}
- index++;
}
}
+
+ /*
+ * 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);
+ }
+ }
+ }
+
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<String, float[][]>();
+ }
+ 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;
/**
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);
}
/**
- * 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) }
{
return fcols;
}
- Iterator<String> features = getViewport().getFeaturesDisplayed()
+ Set<String> features = getViewport().getFeaturesDisplayed()
.getVisibleFeatures();
- while (features.hasNext())
+ for (String feature : features)
{
- String feature = features.next();
fcols.put(feature, getFeatureStyle(feature));
}
return fcols;
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();
+ }
+
}
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<String> featuresDisplayed = new HashSet<String>();
+ private Set<String> featuresDisplayed = new HashSet<String>();
- private HashSet<String> featuresRegistered = new HashSet<String>();
+ private Set<String> featuresRegistered = new HashSet<String>();
public FeaturesDisplayed(FeaturesDisplayedI featuresDisplayed2)
{
- Iterator<String> fdisp = featuresDisplayed2.getVisibleFeatures();
- String ftype;
- while (fdisp.hasNext())
+ Set<String> fdisp = featuresDisplayed2.getVisibleFeatures();
+ for (String ftype : fdisp)
{
- ftype = fdisp.next();
featuresDisplayed.add(ftype);
featuresRegistered.add(ftype);
}
public FeaturesDisplayed()
{
- // TODO Auto-generated constructor stub
}
@Override
- public Iterator<String> getVisibleFeatures()
+ public Set<String> getVisibleFeatures()
{
- return featuresDisplayed.iterator();
+ return Collections.unmodifiableSet(featuresDisplayed);
}
@Override
/*
* 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);
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
*
--- /dev/null
+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.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<SequenceFeature> 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<SequenceFeature> 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<SequenceFeature> 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<SequenceFeature> 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<SequenceFeature> 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<String> 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);
+ }
+}
--- /dev/null
+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<ContiguousI> sorter = new RangeComparator(true);
+
+ /**
+ * A basic sanity test of the constructor
+ */
+ @Test(groups = "Functional")
+ public void testConstructor()
+ {
+ List<Range> ranges = new ArrayList<Range>();
+ 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<Range> ncl = new NCList<Range>(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<Range>(ranges);
+ assertEquals(ncl.toString(), expected);
+ assertTrue(ncl.isValid());
+ }
+
+ @Test(groups = "Functional")
+ public void testFindOverlaps()
+ {
+ List<Range> ranges = new ArrayList<Range>();
+ 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<Range> ncl = new NCList<Range>(ranges);
+
+ List<Range> 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<Range> ranges = new ArrayList<Range>();
+ ranges.add(new Range(20, 50));
+ NCList<Range> ncl = new NCList<Range>(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<Range> ranges = new ArrayList<Range>();
+ ranges.add(new Range(20, 50));
+ NCList<Range> ncl = new NCList<Range>(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<Range> ranges = new ArrayList<Range>();
+ ranges.add(new Range(20, 50));
+ NCList<Range> ncl = new NCList<Range>(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<Range> ranges = new ArrayList<Range>();
+ ranges.add(new Range(20, 50));
+ ranges.add(new Range(30, 60));
+ NCList<Range> ncl = new NCList<Range>(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<Range> ranges = new ArrayList<Range>();
+ ranges.add(new Range(20, 40));
+ ranges.add(new Range(60, 70));
+ NCList<Range> ncl = new NCList<Range>(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
+ * <ul>
+ * <li>verify that add adds an entry and increments size</li>
+ * <li>...except where the entry is already contained (by equals test)</li>
+ * <li>verify that the structure is valid at all stages of construction</li>
+ * <li>generate, run and verify a range of overlap queries</li>
+ * <li>tear down the structure by deleting entries, verifying correctness at
+ * each stage</li>
+ * </ul>
+ */
+ @Test(groups = "Functional", dataProvider = "scalesOfLife")
+ public void test_pseudoRandom(Integer scale)
+ {
+ NCList<SequenceFeature> ncl = new NCList<SequenceFeature>();
+ List<SequenceFeature> features = new ArrayList<SequenceFeature>(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<SequenceFeature> ncl,
+ List<SequenceFeature> 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<SequenceFeature> ncl,
+ List<SequenceFeature> 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<SequenceFeature> ncl,
+ int scale,
+ List<SequenceFeature> 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<SequenceFeature> ncl, int from,
+ int to, List<SequenceFeature> features)
+ {
+ List<SequenceFeature> 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<Range> ranges = new ArrayList<Range>();
+ 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<Range> ncl = new NCList<Range>(ranges);
+ Range r7 = new Range(1, 100);
+ ncl.add(r7);
+
+ List<Range> 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<Range>();
+ assertTrue(ncl.getEntries().isEmpty());
+ }
+
+ @Test(groups = "Functional")
+ public void testDelete()
+ {
+ List<Range> ranges = new ArrayList<Range>();
+ Range r1 = new Range(20, 30);
+ ranges.add(r1);
+ NCList<Range> ncl = new NCList<Range>(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<SequenceFeature> features = new NCList<SequenceFeature>();
+ 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<Range> ranges = new ArrayList<Range>();
+ ranges.add(new Range(40, 50));
+ ranges.add(new Range(20, 30));
+ NCList<Range> ncl = new NCList<Range>(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<SequenceFeature> ncl = new NCList<SequenceFeature>();
+ 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<Range> ranges = new ArrayList<Range>();
+ Range r1 = new Range(40, 50);
+ ranges.add(r1);
+ NCList<Range> ncl = new NCList<Range>(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<Range> ranges = new ArrayList<Range>();
+ 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<Range> ncl = new NCList<Range>(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<Range>(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<Range> ranges = new ArrayList<Range>();
+ 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<Range> ncl = new NCList<Range>(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<Range>(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());
+ }
+}
--- /dev/null
+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<Range> node = new NCNode<Range>(r1);
+ assertEquals(node.getBegin(), 10);
+ Range r2 = new Range(10, 15);
+ node.add(r2);
+
+ List<Range> contents = new ArrayList<Range>();
+ 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<Range> node = new NCNode<Range>(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<Range> node = new NCNode<Range>(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<Range> node = new NCNode<Range>(r1);
+ List<Range> entries = new ArrayList<Range>();
+
+ 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<SequenceFeature> node = new NCNode<SequenceFeature>(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<Range> node = new NCNode<Range>(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
+ }
+}
--- /dev/null
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+
+import org.testng.annotations.Test;
+
+public class RangeComparatorTest
+{
+ class Range implements ContiguousI
+ {
+ int begin;
+
+ int end;
+
+ @Override
+ public int getBegin()
+ {
+ return begin;
+ }
+
+ @Override
+ public int getEnd()
+ {
+ return end;
+ }
+
+ Range(int i, int j)
+ {
+ begin = i;
+ end = j;
+ }
+ }
+
+ @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()
+ {
+ RangeComparator comp = new RangeComparator(true);
+
+ // 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()
+ {
+ RangeComparator comp = new RangeComparator(false);
+
+ // 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);
+ }
+}
--- /dev/null
+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.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<SequenceFeature> 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<SequenceFeature> 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<SequenceFeature> 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<SequenceFeature> 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<String> 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<String> 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<String> 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<SequenceFeature> 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));
+ features = store.getAllFeatures("Cath");
+ assertTrue(features.isEmpty());
+ 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<String> types = sf.varargToTypes();
+ Iterator<String> 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
+ */
+ types = sf.varargToTypes("Metal", "Helix");
+ iterator = types.iterator();
+ assertTrue(iterator.hasNext());
+ assertEquals(iterator.next(), "Metal");
+ assertTrue(iterator.hasNext());
+ assertEquals(iterator.next(), "Helix");
+ assertFalse(iterator.hasNext());
+
+ /*
+ * null type included - should get removed
+ */
+ types = sf.varargToTypes("Metal", null, "Helix");
+ iterator = types.iterator();
+ assertTrue(iterator.hasNext());
+ assertEquals(iterator.next(), "Metal");
+ assertTrue(iterator.hasNext());
+ assertEquals(iterator.next(), "Helix");
+ assertFalse(iterator.hasNext());
+ }
+}
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;
// if no <html> 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("<i><br>SeqDesc</i>", 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 = "<i><br>SeqDesc<br>Type1 ; Nonpos</i>";
+ assertEquals(expected, sb.toString());
+
+ /*
+ * non-positional features not wanted
+ */
+ sb.setLength(0);
+ sar.createSequenceAnnotationReport(sb, seq, true, false, null);
+ assertEquals("<i><br>SeqDesc</i>", 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<String, float[][]> minmax = new HashMap<String, float[][]>();
+ minmax.put("Metal", new float[][] { null, new float[] { 2f, 5f } });
+ sb.setLength(0);
+ sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
+ expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos</i>";
+ 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 = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos<br>Variant ; Havana; benign</i>";
+ 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 = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1<br>Metal ; Desc<br>Type1 ; Nonpos<br>Variant ; Havana; benign</i>";
+ assertEquals(expected, sb.toString());
+ // with showNonPositionalFeatures = false
+ sb.setLength(0);
+ sar.createSequenceAnnotationReport(sb, seq, true, false, minmax);
+ expected = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1</i>";
+ 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("<i><br>UNIPROT P30410, P30411, P30412, P30413,...<br>PDB0 3iu1"));
+ assertTrue(report
+ .endsWith("<br>PDB7 3iu1<br>PDB8,...<br>(Output Sequence Details to list all database references)</i>"));
+ }
}
--- /dev/null
+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<SequenceI> 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<String> types = fr.getRenderOrder();
+ List<String> 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"));
+ seqs.get(3).addSequenceFeature(
+ new SequenceFeature("Rfam", "Desc", 5, 9, Float.NaN, null));
+ // null value for type produces NullPointerException
+ fr.findAllFeatures(true);
+ types = fr.getRenderOrder();
+ groups = fr.getFeatureGroups();
+ assertEquals(types.size(), 2);
+ assertFalse(types.contains("Type"));
+ assertTrue(types.contains("Pfam"));
+ assertTrue(types.contains("Rfam"));
+ 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<String, float[][]> 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<String> renderOrder = fr.getRenderOrder();
+ assertEquals(renderOrder, Arrays.asList("Rfam", "Pfam"));
+
+ /*
+ * change render order (todo: an easier way)
+ * nb here last comes first in the data array
+ */
+ Object[][] data = new Object[2][];
+ FeatureColourI colour = new FeatureColour(Color.RED);
+ data[0] = new Object[] { "Rfam", colour, true };
+ data[1] = new Object[] { "Pfam", colour, false };
+ fr.setFeaturePriority(data);
+ assertEquals(fr.getRenderOrder(), Arrays.asList("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("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<SequenceFeature> 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));
+ }
+}