Merge branch 'develop' into bug/JAL-2541cutRelocateFeatures
authorJim Procter <jprocter@issues.jalview.org>
Mon, 17 Dec 2018 11:54:21 +0000 (11:54 +0000)
committerJim Procter <jprocter@issues.jalview.org>
Mon, 17 Dec 2018 11:54:21 +0000 (11:54 +0000)
1  2 
src/jalview/datamodel/Sequence.java
src/jalview/datamodel/SequenceI.java
src/jalview/datamodel/features/SequenceFeatures.java
src/jalview/datamodel/features/SequenceFeaturesI.java
src/jalview/ws/DBRefFetcher.java
test/jalview/datamodel/SequenceTest.java
test/jalview/datamodel/features/SequenceFeaturesTest.java

@@@ -34,6 -34,7 +34,7 @@@ import java.util.Arrays
  import java.util.BitSet;
  import java.util.Collections;
  import java.util.Enumeration;
+ import java.util.Iterator;
  import java.util.List;
  import java.util.ListIterator;
  import java.util.Vector;
@@@ -42,7 -43,10 +43,7 @@@ import fr.orsay.lri.varna.models.rna.RN
  
  /**
   * 
 - * Implements the SequenceI interface for a char[] based sequence object.
 - * 
 - * @author $author$
 - * @version $Revision$
 + * Implements the SequenceI interface for a char[] based sequence object
   */
  public class Sequence extends ASequence implements SequenceI
  {
    {
      if (pdbIds == null)
      {
-       pdbIds = new Vector<PDBEntry>();
+       pdbIds = new Vector<>();
        pdbIds.add(entry);
        return true;
      }
    @Override
    public Vector<PDBEntry> getAllPDBEntries()
    {
-     return pdbIds == null ? new Vector<PDBEntry>() : pdbIds;
+     return pdbIds == null ? new Vector<>() : pdbIds;
    }
  
    /**
    }
  
    /**
-    * DOCUMENT ME!
+    * Sets the sequence description, and also parses out any special formats of
+    * interest
     * 
     * @param desc
-    *          DOCUMENT ME!
     */
    @Override
    public void setDescription(String desc)
      this.description = desc;
    }
  
+   @Override
+   public void setGeneLoci(String speciesId, String assemblyId,
+           String chromosomeId, MapList map)
+   {
+     addDBRef(new DBRefEntry(speciesId, assemblyId, DBRefEntry.CHROMOSOME
+             + ":" + chromosomeId, new Mapping(map)));
+   }
    /**
-    * DOCUMENT ME!
+    * Returns the gene loci mapping for the sequence (may be null)
     * 
-    * @return DOCUMENT ME!
+    * @return
+    */
+   @Override
+   public GeneLociI getGeneLoci()
+   {
+     DBRefEntry[] refs = getDBRefs();
+     if (refs != null)
+     {
+       for (final DBRefEntry ref : refs)
+       {
+         if (ref.isChromosome())
+         {
+           return new GeneLociI()
+           {
+             @Override
+             public String getSpeciesId()
+             {
+               return ref.getSource();
+             }
+             @Override
+             public String getAssemblyId()
+             {
+               return ref.getVersion();
+             }
+             @Override
+             public String getChromosomeId()
+             {
+               // strip off "chromosome:" prefix to chrId
+               return ref.getAccessionId().substring(
+                       DBRefEntry.CHROMOSOME.length() + 1);
+             }
+             @Override
+             public MapList getMap()
+             {
+               return ref.getMap().getMap();
+             }
+           };
+         }
+       }
+     }
+     return null;
+   }
+   /**
+    * Answers the description
+    * 
+    * @return
     */
    @Override
    public String getDescription()
       * preserve end residue column provided cursor was valid
       */
      int endColumn = isValidCursor(cursor) ? cursor.lastColumnPosition : 0;
 +
      if (residuePos == this.end)
      {
        endColumn = column;
     * @param curs
     * @return
     */
-   protected int findIndex(int pos, SequenceCursor curs)
+   protected int findIndex(final int pos, SequenceCursor curs)
    {
      if (!isValidCursor(curs))
      {
      /*
       * move left or right to find pos from hint.position
       */
 -    int col = curs.columnPosition - 1; // convert from base 1 to 0-based array
 -                                       // index
 +    int col = curs.columnPosition - 1; // convert from base 1 to base 0
      int newPos = curs.residuePosition;
      int delta = newPos > pos ? -1 : 1;
  
      while (newPos != pos)
      {
        col += delta; // shift one column left or right
-       if (col < 0 || col == sequence.length)
+       if (col < 0)
        {
          break;
        }
+       if (col == sequence.length)
+       {
+         col--; // return last column if we failed to reach pos
+         break;
+       }
        if (!Comparison.isGap(sequence[col]))
        {
          newPos += delta;
      }
  
      col++; // convert back to base 1
-     updateCursor(pos, col, curs.firstColumnPosition);
+     /*
+      * only update cursor if we found the target position
+      */
+     if (newPos == pos)
+     {
+       updateCursor(pos, col, curs.firstColumnPosition);
+     }
  
      return col;
    }
      return map;
    }
  
+   /**
+    * Build a bitset corresponding to sequence gaps
+    * 
+    * @return a BitSet where set values correspond to gaps in the sequence
+    */
+   @Override
+   public BitSet gapBitset()
+   {
+     BitSet gaps = new BitSet(sequence.length);
+     int j = 0;
+     while (j < sequence.length)
+     {
+       if (jalview.util.Comparison.isGap(sequence[j]))
+       {
+         gaps.set(j);
+       }
+       j++;
+     }
+     return gaps;
+   }
    @Override
    public int[] findPositionMap()
    {
    @Override
    public List<int[]> getInsertions()
    {
-     ArrayList<int[]> map = new ArrayList<int[]>();
+     ArrayList<int[]> map = new ArrayList<>();
      int lastj = -1, j = 0;
      int pos = start;
      int seqlen = sequence.length;
      boolean createNewDs = false;
      // TODO: take a (second look) at the dataset creation validation method for
      // the very large sequence case
 +
      int startIndex = findIndex(start) - 1;
      int endIndex = findIndex(end) - 1;
      int startDeleteColumn = -1; // for dataset sequence deletions
      int deleteCount = 0;
  
 -    for (int s = i; s < j; s++)
 +    for (int s = i; s < j && s < sequence.length; s++)
      {
        if (Comparison.isGap(sequence[s]))
        {
    {
      if (this.annotation == null)
      {
-       this.annotation = new Vector<AlignmentAnnotation>();
+       this.annotation = new Vector<>();
      }
      if (!this.annotation.contains(annotation))
      {
        return null;
      }
  
-     Vector<AlignmentAnnotation> subset = new Vector<AlignmentAnnotation>();
+     Vector<AlignmentAnnotation> subset = new Vector<>();
      Enumeration<AlignmentAnnotation> e = annotation.elements();
      while (e.hasMoreElements())
      {
    public List<AlignmentAnnotation> getAlignmentAnnotations(String calcId,
            String label)
    {
-     List<AlignmentAnnotation> result = new ArrayList<AlignmentAnnotation>();
+     List<AlignmentAnnotation> result = new ArrayList<>();
      if (this.annotation != null)
      {
        for (AlignmentAnnotation ann : annotation)
      }
      synchronized (dbrefs)
      {
-       List<DBRefEntry> primaries = new ArrayList<DBRefEntry>();
+       List<DBRefEntry> primaries = new ArrayList<>();
        DBRefEntry[] tmp = new DBRefEntry[1];
        for (DBRefEntry ref : dbrefs)
        {
  
      List<SequenceFeature> result = getFeatures().findFeatures(startPos,
              endPos, types);
 +    if (datasetSequence != null)
 +    {
 +      result = datasetSequence.getFeatures().findFeatures(startPos, endPos,
 +              types);
 +    }
 +    else
 +    {
 +      result = sequenceFeatureStore.findFeatures(startPos, endPos, types);
 +    }
  
      /*
       * if end column is gapped, endPos may be to the right, 
  
      return count;
    }
+   @Override
+   public String getSequenceStringFromIterator(Iterator<int[]> it)
+   {
+     StringBuilder newSequence = new StringBuilder();
+     while (it.hasNext())
+     {
+       int[] block = it.next();
+       if (it.hasNext())
+       {
+         newSequence.append(getSequence(block[0], block[1] + 1));
+       }
+       else
+       {
+         newSequence.append(getSequence(block[0], block[1]));
+       }
+     }
+     return newSequence.toString();
+   }
+   @Override
+   public int firstResidueOutsideIterator(Iterator<int[]> regions)
+   {
+     int start = 0;
+     if (!regions.hasNext())
+     {
+       return findIndex(getStart()) - 1;
+     }
+     // Simply walk along the sequence whilst watching for region
+     // boundaries
+     int hideStart = getLength();
+     int hideEnd = -1;
+     boolean foundStart = false;
+     // step through the non-gapped positions of the sequence
+     for (int i = getStart(); i <= getEnd() && (!foundStart); i++)
+     {
+       // get alignment position of this residue in the sequence
+       int p = findIndex(i) - 1;
+       // update region start/end
+       while (hideEnd < p && regions.hasNext())
+       {
+         int[] region = regions.next();
+         hideStart = region[0];
+         hideEnd = region[1];
+       }
+       if (hideEnd < p)
+       {
+         hideStart = getLength();
+       }
+       // update boundary for sequence
+       if (p < hideStart)
+       {
+         start = p;
+         foundStart = true;
+       }
+     }
+     if (foundStart)
+     {
+       return start;
+     }
+     // otherwise, sequence was completely hidden
+     return 0;
+   }
  }
  package jalview.datamodel;
  
  import jalview.datamodel.features.SequenceFeaturesI;
+ import jalview.util.MapList;
  
  import java.util.BitSet;
+ import java.util.Iterator;
  import java.util.List;
  import java.util.Vector;
  
@@@ -204,14 -206,11 +206,14 @@@ public interface SequenceI extends ASeq
    public int findPosition(int i);
  
    /**
 -   * Returns the from-to sequence positions (start..) for the given column
 -   * positions (1..), or null if no residues are included in the range
 +   * Returns the sequence positions for first and last residues lying within the
 +   * given column positions [fromColum,toColumn] (where columns are numbered
 +   * from 1), or null if no residues are included in the range
     * 
     * @param fromColum
 +   *          - first column base 1
     * @param toColumn
 +   *          - last column, base 1
     * @return
     */
    public Range findPositions(int fromColum, int toColumn);
    public int[] gapMap();
  
    /**
+    * Build a bitset corresponding to sequence gaps
+    * 
+    * @return a BitSet where set values correspond to gaps in the sequence
+    */
+   public BitSet gapBitset();
+   /**
     * Returns an int array where indices correspond to each position in sequence
     * char array and the element value gives the result of findPosition for that
     * index in the sequence.
     * @param c1
     * @param c2
     */
-   int replace(char c1, char c2);
+   public int replace(char c1, char c2);
+   /**
+    * Answers the GeneLociI, or null if not known
+    * 
+    * @return
+    */
+   GeneLociI getGeneLoci();
+   /**
+    * Sets the mapping to gene loci for the sequence
+    * 
+    * @param speciesId
+    * @param assemblyId
+    * @param chromosomeId
+    * @param map
+    */
+   void setGeneLoci(String speciesId, String assemblyId,
+           String chromosomeId, MapList map);
+   /**
+    * Returns the sequence string constructed from the substrings of a sequence
+    * defined by the int[] ranges provided by an iterator. E.g. the iterator
+    * could iterate over all visible regions of the alignment
+    * 
+    * @param it
+    *          the iterator to use
+    * @return a String corresponding to the sequence
+    */
+   public String getSequenceStringFromIterator(Iterator<int[]> it);
+   /**
+    * Locate the first position in this sequence which is not contained in an
+    * iterator region. If no such position exists, return 0
+    * 
+    * @param it
+    *          iterator over regions
+    * @return first residue not contained in regions
+    */
+   public int firstResidueOutsideIterator(Iterator<int[]> it);
  }
@@@ -87,7 -87,7 +87,7 @@@ public class SequenceFeatures implement
       */
      // featureStore = Collections
      // .synchronizedSortedMap(new TreeMap<String, FeatureStore>());
-     featureStore = new TreeMap<String, FeatureStore>();
+     featureStore = new TreeMap<>();
    }
  
    /**
    }
  
    /**
-    * Answers true if the given type is one of the specified sequence ontology
-    * terms (or a sub-type of one), or if no terms are supplied. Answers false if
-    * filter terms are specified and the given term does not match any of them.
+    * Answers true if the given type matches one of the specified terms (or is a
+    * sub-type of one in the Sequence Ontology), or if no terms are supplied.
+    * Answers false if filter terms are specified and the given term does not
+    * match any of them.
     * 
     * @param type
     * @param soTerm
      SequenceOntologyI so = SequenceOntologyFactory.getInstance();
      for (String term : soTerm)
      {
-       if (so.isA(type, term))
+       if (type.equals(term) || so.isA(type, term))
        {
          return true;
        }
     * {@inheritDoc}
     */
    @Override
 -  public boolean shiftFeatures(int shift)
 +  public boolean shiftFeatures(int fromPosition, int shiftBy)
    {
      boolean modified = false;
      for (FeatureStore fs : featureStore.values())
      {
 -      modified |= fs.shiftFeatures(shift);
 +      modified |= fs.shiftFeatures(fromPosition, shiftBy);
      }
      return modified;
    }
 -}
 +
 +  /**
 +   * {@inheritDoc}
 +   */
 +  @Override
 +  public void deleteAll()
 +  {
 +    featureStore.clear();
 +  }
 +}
@@@ -82,9 -82,9 +82,9 @@@ public interface SequenceFeatures
            String group, String... type);
  
    /**
-    * Answers a list of all features stored, whose type either matches one of the
-    * given ontology terms, or is a specialisation of a term in the Sequence
-    * Ontology. Results are returned in no particular guaranteed order.
+    * Answers a list of all features stored, whose type either matches, or is a
+    * specialisation (in the Sequence Ontology) of, one of the given terms.
+    * Results are returned in no particular order.
     * 
     * @param ontologyTerm
     * @return
    float getMaximumScore(String type, boolean positional);
  
    /**
 -   * Adds the shift amount to the start and end of all positional features,
 -   * returning true if at least one feature was shifted, else false
 +   * Adds the shift amount to the start and end of all positional features whose
 +   * start position is at or after fromPosition. Returns true if at least one
 +   * feature was shifted, else false.
     * 
 -   * @param shift
 +   * @param fromPosition
 +   * @param shiftBy
     */
 -  abstract boolean shiftFeatures(int shift);
 -}
 +  boolean shiftFeatures(int fromPosition, int shiftBy);
 +
 +  /**
 +   * Deletes all positional and non-positional features
 +   */
 +  void deleteAll();
 +}
@@@ -28,15 -28,12 +28,12 @@@ import jalview.datamodel.DBRefSource
  import jalview.datamodel.Mapping;
  import jalview.datamodel.SequenceI;
  import jalview.gui.CutAndPasteTransfer;
- import jalview.gui.DasSourceBrowser;
  import jalview.gui.Desktop;
  import jalview.gui.FeatureSettings;
  import jalview.gui.IProgressIndicator;
  import jalview.gui.OOMWarning;
  import jalview.util.DBRefUtils;
  import jalview.util.MessageManager;
- import jalview.ws.dbsources.das.api.jalviewSourceI;
- import jalview.ws.dbsources.das.datamodel.DasSequenceSource;
  import jalview.ws.seqfetcher.DbSourceProxy;
  
  import java.util.ArrayList;
@@@ -61,6 -58,8 +58,8 @@@ public class DBRefFetcher implements Ru
  {
    private static final String NEWLINE = System.lineSeparator();
  
+   public static final String TRIM_RETRIEVED_SEQUENCES = "TRIM_FETCHED_DATASET_SEQS";
    public interface FetchFinishedListenerI
    {
      void finished();
            DbSourceProxy[] sources, FeatureSettings featureSettings,
            boolean isNucleotide)
    {
-     listeners = new ArrayList<FetchFinishedListenerI>();
+     listeners = new ArrayList<>();
      this.progressWindow = progressIndicatorFrame;
      alseqs = new SequenceI[seqs.length];
      SequenceI[] ds = new SequenceI[seqs.length];
              .getSequenceFetcherSingleton(progressIndicatorFrame);
      // set default behaviour for transferring excess sequence data to the
      // dataset
-     trimDsSeqs = Cache.getDefault("TRIM_FETCHED_DATASET_SEQS", true);
+     trimDsSeqs = Cache.getDefault(TRIM_RETRIEVED_SEQUENCES, true);
      if (sources == null)
      {
        setDatabaseSources(featureSettings, isNucleotide);
    {
      // af.featureSettings_actionPerformed(null);
      String[] defdb = null;
-     List<DbSourceProxy> selsources = new ArrayList<DbSourceProxy>();
-     Vector<jalviewSourceI> dasselsrc = (featureSettings != null)
-             ? featureSettings.getSelectedSources()
-             : new DasSourceBrowser().getSelectedSources();
-     for (jalviewSourceI src : dasselsrc)
-     {
-       List<DbSourceProxy> sp = src.getSequenceSourceProxies();
-       if (sp != null)
-       {
-         selsources.addAll(sp);
-         if (sp.size() > 1)
-         {
-           Cache.log.debug("Added many Db Sources for :" + src.getTitle());
-         }
-       }
-     }
+     List<DbSourceProxy> selsources = new ArrayList<>();
      // select appropriate databases based on alignFrame context.
      if (forNucleotide)
      {
      {
        defdb = DBRefSource.PROTEINDBS;
      }
-     List<DbSourceProxy> srces = new ArrayList<DbSourceProxy>();
+     List<DbSourceProxy> srces = new ArrayList<>();
      for (String ddb : defdb)
      {
        List<DbSourceProxy> srcesfordb = sfetcher.getSourceProxy(ddb);
    }
  
    /**
-    * retrieve all the das sequence sources and add them to the list of db
-    * sources to retrieve from
-    */
-   public void appendAllDasSources()
-   {
-     if (dbSources == null)
-     {
-       dbSources = new DbSourceProxy[0];
-     }
-     // append additional sources
-     DbSourceProxy[] otherdb = sfetcher
-             .getDbSourceProxyInstances(DasSequenceSource.class);
-     if (otherdb != null && otherdb.length > 0)
-     {
-       DbSourceProxy[] newsrc = new DbSourceProxy[dbSources.length
-               + otherdb.length];
-       System.arraycopy(dbSources, 0, newsrc, 0, dbSources.length);
-       System.arraycopy(otherdb, 0, newsrc, dbSources.length,
-               otherdb.length);
-       dbSources = newsrc;
-     }
-   }
-   /**
     * start the fetcher thread
     * 
     * @param waitTillFinished
        }
        else if (seqs == null)
        {
-         seqs = new Vector<SequenceI>();
+         seqs = new Vector<>();
          seqs.addElement(seq);
        }
  
      }
      else
      {
-       seqs = new Vector<SequenceI>();
+       seqs = new Vector<>();
        seqs.addElement(seq);
      }
  
        e.printStackTrace();
      }
  
-     Vector<SequenceI> sdataset = new Vector<SequenceI>(
+     Vector<SequenceI> sdataset = new Vector<>(
              Arrays.asList(dataset));
-     List<String> warningMessages = new ArrayList<String>();
+     List<String> warningMessages = new ArrayList<>();
  
      int db = 0;
      while (sdataset.size() > 0 && db < dbSources.length)
        SequenceI[] currSeqs = new SequenceI[sdataset.size()];
        sdataset.copyInto(currSeqs);// seqs that are to be validated against
        // dbSources[db]
-       Vector<String> queries = new Vector<String>(); // generated queries curSeq
-       seqRefs = new Hashtable<String, Vector<SequenceI>>();
+       Vector<String> queries = new Vector<>(); // generated queries curSeq
+       seqRefs = new Hashtable<>();
  
        int seqIndex = 0;
  
      {
        // Work out which sequences this sequence matches,
        // taking into account all accessionIds and names in the file
-       Vector<SequenceI> sequenceMatches = new Vector<SequenceI>();
+       Vector<SequenceI> sequenceMatches = new Vector<>();
        // look for corresponding accession ids
        DBRefEntry[] entryRefs = DBRefUtils
                .selectRefs(retrievedSeq.getDBRefs(), new String[]
              int startShift = absStart - sequenceStart + 1;
              if (startShift != 0)
              {
 -              modified |= sequence.getFeatures().shiftFeatures(startShift);
 +              modified |= sequence.getFeatures().shiftFeatures(1,
 +                      startShift);
              }
            }
          }
     */
    private SequenceI[] recoverDbSequences(SequenceI[] sequencesArray)
    {
-     Vector<SequenceI> nseq = new Vector<SequenceI>();
+     Vector<SequenceI> nseq = new Vector<>();
      for (int i = 0; sequencesArray != null
              && i < sequencesArray.length; i++)
      {
@@@ -28,6 -28,7 +28,7 @@@ import static org.testng.AssertJUnit.as
  import static org.testng.AssertJUnit.assertSame;
  import static org.testng.AssertJUnit.assertTrue;
  
+ import jalview.analysis.AlignmentGenerator;
  import jalview.commands.EditCommand;
  import jalview.commands.EditCommand.Action;
  import jalview.datamodel.PDBEntry.Type;
@@@ -38,16 -39,17 +39,17 @@@ import java.io.File
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.BitSet;
+ import java.util.Iterator;
  import java.util.List;
  import java.util.Vector;
  
- import junit.extensions.PA;
  import org.testng.Assert;
  import org.testng.annotations.BeforeClass;
  import org.testng.annotations.BeforeMethod;
  import org.testng.annotations.Test;
  
+ import junit.extensions.PA;
  public class SequenceTest
  {
  
      sq.sequenceChanged();
      assertEquals(6, sq.findIndex(9));
  
-     sq = new Sequence("test/8-13", "-A--B-C-D-E-F--");
+     final String aligned = "-A--B-C-D-E-F--";
+     assertEquals(15, aligned.length());
+     sq = new Sequence("test/8-13", aligned);
      assertEquals(2, sq.findIndex(8));
      sq.sequenceChanged();
      assertEquals(5, sq.findIndex(9));
      // beyond end returns last residue column
      sq.sequenceChanged();
      assertEquals(13, sq.findIndex(99));
+     /*
+      * residue before sequence 'end' but beyond end of sequence returns 
+      * length of sequence (last column) (rightly or wrongly!)
+      */
+     sq = new Sequence("test/8-15", "A-B-C-"); // trailing gap case
+     assertEquals(6, sq.getLength());
+     sq.sequenceChanged();
+     assertEquals(sq.getLength(), sq.findIndex(14));
+     sq = new Sequence("test/8-99", "-A--B-C-D"); // trailing residue case
+     sq.sequenceChanged();
+     assertEquals(sq.getLength(), sq.findIndex(65));
+     /*
+      * residue after sequence 'start' but before first residue returns 
+      * zero (before first column) (rightly or wrongly!)
+      */
+     sq = new Sequence("test/8-15", "-A-B-C-"); // leading gap case
+     sq.sequenceChanged();
+     assertEquals(0, sq.findIndex(3));
+     sq = new Sequence("test/8-15", "A-B-C-"); // leading residue case
+     sq.sequenceChanged();
+     assertEquals(0, sq.findIndex(2));
    }
  
 +  @Test(groups = { "Functional" })
 +  public void testFindPositions()
 +  {
 +    SequenceI sq = new Sequence("test/8-13", "-ABC---DE-F--");
 +
 +    /*
 +     * invalid inputs
 +     */
 +    assertNull(sq.findPositions(6, 5));
 +    assertNull(sq.findPositions(0, 5));
 +    assertNull(sq.findPositions(-1, 5));
 +
 +    /*
 +     * all gapped ranges
 +     */
 +    assertNull(sq.findPositions(1, 1)); // 1-based columns
 +    assertNull(sq.findPositions(5, 5));
 +    assertNull(sq.findPositions(5, 6));
 +    assertNull(sq.findPositions(5, 7));
 +
 +    /*
 +     * all ungapped ranges
 +     */
 +    assertEquals(new Range(8, 8), sq.findPositions(2, 2)); // A
 +    assertEquals(new Range(8, 9), sq.findPositions(2, 3)); // AB
 +    assertEquals(new Range(8, 10), sq.findPositions(2, 4)); // ABC
 +    assertEquals(new Range(9, 10), sq.findPositions(3, 4)); // BC
 +
 +    /*
 +     * gap to ungapped range
 +     */
 +    assertEquals(new Range(8, 10), sq.findPositions(1, 4)); // ABC
 +    assertEquals(new Range(11, 12), sq.findPositions(6, 9)); // DE
 +
 +    /*
 +     * ungapped to gapped range
 +     */
 +    assertEquals(new Range(10, 10), sq.findPositions(4, 5)); // C
 +    assertEquals(new Range(9, 13), sq.findPositions(3, 11)); // BCDEF
 +
 +    /*
 +     * ungapped to ungapped enclosing gaps
 +     */
 +    assertEquals(new Range(10, 11), sq.findPositions(4, 8)); // CD
 +    assertEquals(new Range(8, 13), sq.findPositions(2, 11)); // ABCDEF
 +
 +    /*
 +     * gapped to gapped enclosing ungapped
 +     */
 +    assertEquals(new Range(8, 10), sq.findPositions(1, 5)); // ABC
 +    assertEquals(new Range(11, 12), sq.findPositions(5, 10)); // DE
 +    assertEquals(new Range(8, 13), sq.findPositions(1, 13)); // the lot
 +    assertEquals(new Range(8, 13), sq.findPositions(1, 99));
 +  }
 +
    /**
     * Tests for the method that returns a dataset sequence position (start..) for
     * an aligned column position (base 0).
      assertEquals("test:Pos13:Col10:startCol3:endCol10:tok0",
              PA.getValue(sq, "cursor").toString());
      sq.sequenceChanged();
 -    assertEquals(12, sq.findPosition(8));
 -    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
 +    assertEquals(12, sq.findPosition(8)); // E12
      // sequenceChanged() invalidates cursor.lastResidueColumn
      cursor = (SequenceCursor) PA.getValue(sq, "cursor");
      assertEquals("test:Pos12:Col9:startCol3:endCol0:tok1",
      assertEquals(6, sq.getEnd());
      assertNull(PA.getValue(sq, "datasetSequence"));
  
 +    sq = new Sequence("test", "ABCDE");
 +    sq.deleteChars(0, 3);
 +    assertEquals("DE", sq.getSequenceAsString());
 +    assertEquals(4, sq.getStart());
 +    assertEquals(5, sq.getEnd());
 +    assertNull(PA.getValue(sq, "datasetSequence"));
 +
      /*
       * delete at end
       */
      assertEquals(1, sq.getStart());
      assertEquals(4, sq.getEnd());
      assertNull(PA.getValue(sq, "datasetSequence"));
 +
 +    /*
 +     * delete more positions than there are
 +     */
 +    sq = new Sequence("test/8-11", "ABCD");
 +    sq.deleteChars(0, 99);
 +    assertEquals("", sq.getSequenceAsString());
 +    assertEquals(12, sq.getStart()); // = findPosition(99) ?!?
 +    assertEquals(11, sq.getEnd());
 +
 +    sq = new Sequence("test/8-11", "----");
 +    sq.deleteChars(0, 99); // ArrayIndexOutOfBoundsException <= 2.10.2
 +    assertEquals("", sq.getSequenceAsString());
 +    assertEquals(8, sq.getStart());
 +    assertEquals(11, sq.getEnd());
    }
  
    @Test(groups = { "Functional" })
      Assert.assertEquals(pdbe1a,
              sq.getDatasetSequence().getPDBEntry("1PDB"),
              "PDB Entry '1PDB' not found on dataset sequence via getPDBEntry.");
-     ArrayList<Annotation> annotsList = new ArrayList<Annotation>();
+     ArrayList<Annotation> annotsList = new ArrayList<>();
      System.out.println(">>>>>> " + sq.getSequenceAsString().length());
      annotsList.add(new Annotation("A", "A", 'X', 0.1f));
      annotsList.add(new Annotation("A", "A", 'X', 0.1f));
    {
      Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--");
  
-     // find F given A
+     // find F given A, check cursor is now at the found position
      assertEquals(10, sq.findIndex(13, new SequenceCursor(sq, 8, 2, 0)));
+     SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+     assertEquals(13, cursor.residuePosition);
+     assertEquals(10, cursor.columnPosition);
  
      // find A given F
      assertEquals(2, sq.findIndex(8, new SequenceCursor(sq, 13, 10, 0)));
+     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+     assertEquals(8, cursor.residuePosition);
+     assertEquals(2, cursor.columnPosition);
  
-     // find C given C
+     // find C given C (no cursor update is done for this case)
      assertEquals(6, sq.findIndex(10, new SequenceCursor(sq, 10, 6, 0)));
+     SequenceCursor cursor2 = (SequenceCursor) PA.getValue(sq, "cursor");
+     assertSame(cursor2, cursor);
+     /*
+      * sequence 'end' beyond end of sequence returns length of sequence 
+      *  (for compatibility with pre-cursor code)
+      *  - also verify the cursor is left in a valid state
+      */
+     sq = new Sequence("test/8-99", "-A--B-C-D-E-F--"); // trailing gap case
+     assertEquals(7, sq.findIndex(10)); // establishes a cursor
+     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+     assertEquals(10, cursor.residuePosition);
+     assertEquals(7, cursor.columnPosition);
+     assertEquals(sq.getLength(), sq.findIndex(65));
+     cursor2 = (SequenceCursor) PA.getValue(sq, "cursor");
+     assertSame(cursor, cursor2); // not updated for this case!
+     sq = new Sequence("test/8-99", "-A--B-C-D-E-F"); // trailing residue case
+     sq.findIndex(10); // establishes a cursor
+     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+     assertEquals(sq.getLength(), sq.findIndex(65));
+     cursor2 = (SequenceCursor) PA.getValue(sq, "cursor");
+     assertSame(cursor, cursor2); // not updated for this case!
+     /*
+      * residue after sequence 'start' but before first residue should return 
+      * zero (for compatibility with pre-cursor code)
+      */
+     sq = new Sequence("test/8-15", "-A-B-C-"); // leading gap case
+     sq.findIndex(10); // establishes a cursor
+     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+     assertEquals(0, sq.findIndex(3));
+     cursor2 = (SequenceCursor) PA.getValue(sq, "cursor");
+     assertSame(cursor, cursor2); // not updated for this case!
+     sq = new Sequence("test/8-15", "A-B-C-"); // leading residue case
+     sq.findIndex(10); // establishes a cursor
+     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+     assertEquals(0, sq.findIndex(2));
+     cursor2 = (SequenceCursor) PA.getValue(sq, "cursor");
+     assertSame(cursor, cursor2); // not updated for this case!
    }
  
    @Test(groups = { "Functional" })
      // cursor should now be at [D 6]
      cursor = (SequenceCursor) PA.getValue(sq, "cursor");
      assertEquals(new SequenceCursor(sq, 11, 6, ++token), cursor);
 +    assertEquals(0, cursor.lastColumnPosition); // not yet found
 +    assertEquals(13, sq.findPosition(8)); // E13
 +    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
 +    assertEquals(9, cursor.lastColumnPosition); // found
  
      /*
       * deleteChars should invalidate the cached cursor
    }
  
    @Test(groups = { "Functional" })
 -  public void testFindPositions()
 -  {
 -    SequenceI sq = new Sequence("test/8-13", "-ABC---DE-F--");
 -
 -    /*
 -     * invalid inputs
 -     */
 -    assertNull(sq.findPositions(6, 5));
 -    assertNull(sq.findPositions(0, 5));
 -    assertNull(sq.findPositions(-1, 5));
 -
 -    /*
 -     * all gapped ranges
 -     */
 -    assertNull(sq.findPositions(1, 1)); // 1-based columns
 -    assertNull(sq.findPositions(5, 5));
 -    assertNull(sq.findPositions(5, 6));
 -    assertNull(sq.findPositions(5, 7));
 -
 -    /*
 -     * all ungapped ranges
 -     */
 -    assertEquals(new Range(8, 8), sq.findPositions(2, 2)); // A
 -    assertEquals(new Range(8, 9), sq.findPositions(2, 3)); // AB
 -    assertEquals(new Range(8, 10), sq.findPositions(2, 4)); // ABC
 -    assertEquals(new Range(9, 10), sq.findPositions(3, 4)); // BC
 -
 -    /*
 -     * gap to ungapped range
 -     */
 -    assertEquals(new Range(8, 10), sq.findPositions(1, 4)); // ABC
 -    assertEquals(new Range(11, 12), sq.findPositions(6, 9)); // DE
 -
 -    /*
 -     * ungapped to gapped range
 -     */
 -    assertEquals(new Range(10, 10), sq.findPositions(4, 5)); // C
 -    assertEquals(new Range(9, 13), sq.findPositions(3, 11)); // BCDEF
 -
 -    /*
 -     * ungapped to ungapped enclosing gaps
 -     */
 -    assertEquals(new Range(10, 11), sq.findPositions(4, 8)); // CD
 -    assertEquals(new Range(8, 13), sq.findPositions(2, 11)); // ABCDEF
 -
 -    /*
 -     * gapped to gapped enclosing ungapped
 -     */
 -    assertEquals(new Range(8, 10), sq.findPositions(1, 5)); // ABC
 -    assertEquals(new Range(11, 12), sq.findPositions(5, 10)); // DE
 -    assertEquals(new Range(8, 13), sq.findPositions(1, 13)); // the lot
 -    assertEquals(new Range(8, 13), sq.findPositions(1, 99));
 -  }
 -
 -  @Test(groups = { "Functional" })
+   public void testGapBitset()
+   {
+     SequenceI sq = new Sequence("test/8-13", "-ABC---DE-F--");
+     BitSet bs = sq.gapBitset();
+     BitSet expected = new BitSet();
+     expected.set(0);
+     expected.set(4, 7);
+     expected.set(9);
+     expected.set(11, 13);
+     assertTrue(bs.equals(expected));
+   }
    public void testFindFeatures_largeEndPos()
    {
      /*
      assertEquals(8, sq.getDatasetSequence().getStart());
      assertEquals(9, sq.getDatasetSequence().getEnd());
    }
+   /**
+    * Test the code used to locate the reference sequence ruler origin
+    */
+   @Test(groups = { "Functional" })
+   public void testLocateVisibleStartofSequence()
+   {
+     // create random alignment
+     AlignmentGenerator gen = new AlignmentGenerator(false);
+     AlignmentI al = gen.generate(50, 20, 123, 5, 5);
+     HiddenColumns cs = al.getHiddenColumns();
+     ColumnSelection colsel = new ColumnSelection();
+     SequenceI seq = new Sequence("RefSeq", "-A-SD-ASD--E---");
+     assertEquals(2, seq.findIndex(seq.getStart()));
+     // no hidden columns
+     assertEquals(seq.findIndex(seq.getStart()) - 1,
+             seq.firstResidueOutsideIterator(cs.iterator()));
+     // hidden column on gap after end of sequence - should not affect bounds
+     colsel.hideSelectedColumns(13, al.getHiddenColumns());
+     assertEquals(seq.findIndex(seq.getStart()) - 1,
+             seq.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     // hidden column on gap before beginning of sequence - should vis bounds by
+     // one
+     colsel.hideSelectedColumns(0, al.getHiddenColumns());
+     assertEquals(seq.findIndex(seq.getStart()) - 2,
+             cs.absoluteToVisibleColumn(
+                     seq.firstResidueOutsideIterator(cs.iterator())));
+     cs.revealAllHiddenColumns(colsel);
+     // hide columns around most of sequence - leave one residue remaining
+     cs.hideColumns(1, 3);
+     cs.hideColumns(6, 11);
+     Iterator<int[]> it = cs.getVisContigsIterator(0, 6, false);
+     assertEquals("-D", seq.getSequenceStringFromIterator(it));
+     // cs.getVisibleSequenceStrings(0, 5, new SequenceI[]
+     // { seq })[0]);
+     assertEquals(4, seq.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     // hide whole sequence - should just get location of hidden region
+     // containing sequence
+     cs.hideColumns(1, 11);
+     assertEquals(0, seq.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(0, 15);
+     assertEquals(0, seq.firstResidueOutsideIterator(cs.iterator()));
+     SequenceI seq2 = new Sequence("RefSeq2", "-------A-SD-ASD--E---");
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(7, 17);
+     assertEquals(0, seq2.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(3, 17);
+     assertEquals(0, seq2.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(3, 19);
+     assertEquals(0, seq2.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(0, 0);
+     assertEquals(1, seq.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(0, 1);
+     assertEquals(3, seq.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(0, 2);
+     assertEquals(3, seq.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(1, 1);
+     assertEquals(3, seq.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(1, 2);
+     assertEquals(3, seq.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(1, 3);
+     assertEquals(4, seq.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(0, 2);
+     cs.hideColumns(5, 6);
+     assertEquals(3, seq.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(0, 2);
+     cs.hideColumns(5, 6);
+     cs.hideColumns(9, 10);
+     assertEquals(3, seq.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(0, 2);
+     cs.hideColumns(7, 11);
+     assertEquals(3, seq.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(2, 4);
+     cs.hideColumns(7, 11);
+     assertEquals(1, seq.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(2, 4);
+     cs.hideColumns(7, 12);
+     assertEquals(1, seq.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(1, 11);
+     assertEquals(0, seq.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(0, 12);
+     assertEquals(0, seq.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(0, 4);
+     cs.hideColumns(6, 12);
+     assertEquals(0, seq.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(0, 1);
+     cs.hideColumns(3, 12);
+     assertEquals(0, seq.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(3, 14);
+     cs.hideColumns(17, 19);
+     assertEquals(0, seq2.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(3, 7);
+     cs.hideColumns(9, 14);
+     cs.hideColumns(17, 19);
+     assertEquals(0, seq2.firstResidueOutsideIterator(cs.iterator()));
+     cs.revealAllHiddenColumns(colsel);
+     cs.hideColumns(0, 1);
+     cs.hideColumns(3, 4);
+     cs.hideColumns(6, 8);
+     cs.hideColumns(10, 12);
+     assertEquals(0, seq.firstResidueOutsideIterator(cs.iterator()));
+   }
  }
@@@ -13,10 -13,10 +13,10 @@@ import java.util.List
  import java.util.Map;
  import java.util.Set;
  
- import junit.extensions.PA;
  import org.testng.annotations.Test;
  
+ import junit.extensions.PA;
  public class SequenceFeaturesTest
  {
    @Test(groups = "Functional")
      assertTrue(store.getFeaturesByOntology(new String[] {}).isEmpty());
      assertTrue(store.getFeaturesByOntology((String[]) null).isEmpty());
    
-     SequenceFeature sf1 = new SequenceFeature("transcript", "desc", 10, 20,
+     SequenceFeature transcriptFeature = new SequenceFeature("transcript", "desc", 10, 20,
              Float.NaN, null);
-     store.add(sf1);
+     store.add(transcriptFeature);
  
-     // mRNA isA transcript; added here 'as if' non-positional
-     // just to show that non-positional features are included in results
-     SequenceFeature sf2 = new SequenceFeature("mRNA", "desc", 0, 0,
+     /*
+      * mRNA is a sub-type of transcript; added here 'as if' non-positional
+      * just to show that non-positional features are included in results
+      */
+     SequenceFeature mrnaFeature = new SequenceFeature("mRNA", "desc", 0, 0,
              Float.NaN, null);
-     store.add(sf2);
+     store.add(mrnaFeature);
  
-     SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 30, 40,
+     SequenceFeature pfamFeature = new SequenceFeature("Pfam", "desc", 30, 40,
              Float.NaN, null);
-     store.add(sf3);
+     store.add(pfamFeature);
  
+     /*
+      * "transcript" matches both itself and the sub-term "mRNA"
+      */
      features = store.getFeaturesByOntology("transcript");
      assertEquals(features.size(), 2);
-     assertTrue(features.contains(sf1));
-     assertTrue(features.contains(sf2));
+     assertTrue(features.contains(transcriptFeature));
+     assertTrue(features.contains(mrnaFeature));
  
+     /*
+      * "mRNA" matches itself but not parent term "transcript"
+      */
      features = store.getFeaturesByOntology("mRNA");
      assertEquals(features.size(), 1);
-     assertTrue(features.contains(sf2));
+     assertTrue(features.contains(mrnaFeature));
  
+     /*
+      * "pfam" is not an SO term but is included as an exact match
+      */
      features = store.getFeaturesByOntology("mRNA", "Pfam");
      assertEquals(features.size(), 2);
-     assertTrue(features.contains(sf2));
-     assertTrue(features.contains(sf3));
+     assertTrue(features.contains(mrnaFeature));
+     assertTrue(features.contains(pfamFeature));
  
      features = store.getFeaturesByOntology("sequence_variant");
      assertTrue(features.isEmpty());
    @Test(groups = "Functional")
    public void testSortFeatures()
    {
-     List<SequenceFeature> sfs = new ArrayList<SequenceFeature>();
+     List<SequenceFeature> sfs = new ArrayList<>();
      SequenceFeature sf1 = new SequenceFeature("Pfam", "desc", 30, 80,
              Float.NaN, null);
      sfs.add(sf1);
    public void testShiftFeatures()
    {
      SequenceFeatures store = new SequenceFeatures();
 -    assertFalse(store.shiftFeatures(1));
 +    assertFalse(store.shiftFeatures(0, 1));
  
      SequenceFeature sf1 = new SequenceFeature("Cath", "", 2, 5, 0f, null);
      store.add(sf1);
      /*
       * shift features right by 5
       */
 -    assertTrue(store.shiftFeatures(5));
 +    assertTrue(store.shiftFeatures(0, 5));
    
      // non-positional features untouched:
      List<SequenceFeature> nonPos = store.getNonPositionalFeatures();
       * feature at [7-10] should be removed
       * feature at [13-19] should become [1-4] 
       */
 -    assertTrue(store.shiftFeatures(-15));
 +    assertTrue(store.shiftFeatures(0, -15));
      pos = store.getPositionalFeatures();
      assertEquals(pos.size(), 2);
      SequenceFeatures.sortFeatures(pos, true);
      assertEquals(pos.get(1).getBegin(), 13);
      assertEquals(pos.get(1).getEnd(), 22);
      assertEquals(pos.get(1).getType(), "Disulfide bond");
 +
 +    /*
 +     * shift right by 4 from column 2
 +     * feature at [1-4] should be unchanged
 +     * feature at [13-22] should become [17-26] 
 +     */
 +    assertTrue(store.shiftFeatures(2, 4));
 +    pos = store.getPositionalFeatures();
 +    assertEquals(pos.size(), 2);
 +    SequenceFeatures.sortFeatures(pos, true);
 +    assertEquals(pos.get(0).getBegin(), 1);
 +    assertEquals(pos.get(0).getEnd(), 4);
 +    assertEquals(pos.get(0).getType(), "Metal");
 +    assertEquals(pos.get(1).getBegin(), 17);
 +    assertEquals(pos.get(1).getEnd(), 26);
 +    assertEquals(pos.get(1).getType(), "Disulfide bond");
 +
 +    /*
 +     * shift right from column 18
 +     * should be no updates
 +     */
 +    SequenceFeature f1 = pos.get(0);
 +    SequenceFeature f2 = pos.get(1);
 +    assertFalse(store.shiftFeatures(18, 6));
 +    pos = store.getPositionalFeatures();
 +    assertEquals(pos.size(), 2);
 +    SequenceFeatures.sortFeatures(pos, true);
 +    assertSame(pos.get(0), f1);
 +    assertSame(pos.get(1), f2);
    }
  
    @Test(groups = "Functional")
      assertTrue(store.isOntologyTerm("junk", new String[] {}));
      assertTrue(store.isOntologyTerm("junk", (String[]) null));
    }
 +
 +  @Test(groups = "Functional")
 +  public void testDeleteAll()
 +  {
 +    SequenceFeaturesI store = new SequenceFeatures();
 +    assertFalse(store.hasFeatures());
 +    store.deleteAll();
 +    assertFalse(store.hasFeatures());
 +    store.add(new SequenceFeature("Cath", "Desc", 12, 20, 0f, "Group"));
 +    store.add(new SequenceFeature("Pfam", "Desc", 6, 12, 2f, "Group2"));
 +    assertTrue(store.hasFeatures());
 +    store.deleteAll();
 +    assertFalse(store.hasFeatures());
 +  }
  }