JAL-3794 new test for recognition of protein or DNA with ambiguity characters, includ...
[jalview.git] / test / jalview / datamodel / SequenceTest.java
index c5850dc..6d07113 100644 (file)
@@ -28,29 +28,31 @@ import static org.testng.AssertJUnit.assertNull;
 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;
 import jalview.gui.JvOptionPane;
 import jalview.util.MapList;
+import jalview.ws.params.InvalidArgumentException;
 
 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
 {
-
   @BeforeClass(alwaysRun = true)
   public void setUpJvOptionPane()
   {
@@ -106,15 +108,23 @@ public class SequenceTest
     // change sequence, should trigger an update of cached result
     sq.setSequence("ASDFASDFADSF");
     assertTrue(sq.isProtein());
-    /*
-     * in situ change of sequence doesn't change hashcode :-O
-     * (sequence should not expose internal implementation)
-     */
-    for (int i = 0; i < sq.getSequence().length; i++)
-    {
-      sq.getSequence()[i] = "acgtu".charAt(i % 5);
-    }
-    assertTrue(sq.isProtein()); // but it isn't
+  }
+
+  @Test(groups = ("Functional"))
+  public void testIsProteinWithXorNAmbiguityCodes()
+  {
+    // test Protein with N - poly asparagine 
+    assertTrue(new Sequence("prot", "ASDFASDFASDFNNNNNNNNN").isProtein());
+    assertTrue(new Sequence("prot", "NNNNNNNNNNNNNNNNNNNNN").isProtein());
+    // test Protein with X
+    assertTrue(new Sequence("prot", "ASDFASDFASDFXXXXXXXXX").isProtein());
+    // test DNA with X
+    assertFalse(new Sequence("prot", "ACGTACGTACGTXXXXXXXX").isProtein());
+    // test DNA with N
+    assertFalse(new Sequence("prot", "ACGTACGTACGTNNNNNNNN").isProtein());
+    // test RNA with X
+    assertFalse(new Sequence("prot", "ACGUACGUACGUXXXXXXXXX").isProtein());
+    assertFalse(new Sequence("prot", "ACGUACGUACGUNNNNNNNNN").isProtein());
   }
 
   @Test(groups = { "Functional" })
@@ -254,7 +264,9 @@ 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));
@@ -270,6 +282,84 @@ public class SequenceTest
     // 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));
   }
 
   /**
@@ -286,6 +376,8 @@ public class SequenceTest
     SequenceI sq = new Sequence("test/8-13", "ABCDEF");
     assertEquals(8, sq.findPosition(0));
     // Sequence should now hold a cursor at [8, 0]
+    assertEquals("test:Pos8:Col1:startCol1:endCol0:tok1",
+            PA.getValue(sq, "cursor").toString());
     SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     int token = (int) PA.getValue(sq, "changeCount");
     assertEquals(new SequenceCursor(sq, 8, 1, token), cursor);
@@ -294,24 +386,31 @@ public class SequenceTest
 
     /*
      * find F13 at column offset 5, cursor should update to [13, 6]
+     * endColumn is found and saved in cursor
      */
     assertEquals(13, sq.findPosition(5));
     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     assertEquals(++token, (int) PA.getValue(sq, "changeCount"));
     assertEquals(new SequenceCursor(sq, 13, 6, token), cursor);
+    assertEquals("test:Pos13:Col6:startCol1:endCol6:tok2",
+            PA.getValue(sq, "cursor").toString());
 
     // assertEquals(-1, seq.findPosition(6)); // fails
 
     sq = new Sequence("test/8-11", "AB-C-D--");
-    token = (int) PA.getValue(sq, "changeCount"); // 0
+    token = (int) PA.getValue(sq, "changeCount"); // 1 for setStart
     assertEquals(8, sq.findPosition(0));
     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     assertEquals(new SequenceCursor(sq, 8, 1, token), cursor);
+    assertEquals("test:Pos8:Col1:startCol1:endCol0:tok1",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     assertEquals(9, sq.findPosition(1));
     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     assertEquals(new SequenceCursor(sq, 9, 2, ++token), cursor);
+    assertEquals("test:Pos9:Col2:startCol1:endCol0:tok2",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     // gap position 'finds' residue to the right (not the left as per javadoc)
@@ -319,11 +418,15 @@ public class SequenceTest
     assertEquals(10, sq.findPosition(2));
     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     assertEquals(new SequenceCursor(sq, 9, 2, ++token), cursor);
+    assertEquals("test:Pos9:Col2:startCol1:endCol0:tok3",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     assertEquals(10, sq.findPosition(3));
     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     assertEquals(new SequenceCursor(sq, 10, 4, ++token), cursor);
+    assertEquals("test:Pos10:Col4:startCol1:endCol0:tok4",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     // column[4] is the gap after C - returns D11
@@ -331,11 +434,16 @@ public class SequenceTest
     assertEquals(11, sq.findPosition(4));
     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     assertEquals(new SequenceCursor(sq, 10, 4, ++token), cursor);
+    assertEquals("test:Pos10:Col4:startCol1:endCol0:tok5",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     assertEquals(11, sq.findPosition(5)); // D
     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     assertEquals(new SequenceCursor(sq, 11, 6, ++token), cursor);
+    // lastCol has been found and saved in the cursor
+    assertEquals("test:Pos11:Col6:startCol1:endCol6:tok6",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     // returns 1 more than sequence length if off the end ?!?
@@ -344,38 +452,69 @@ public class SequenceTest
     sq.sequenceChanged();
     assertEquals(12, sq.findPosition(7));
 
+    /*
+     * first findPosition should also set firstResCol in cursor
+     */
     sq = new Sequence("test/8-13", "--AB-C-DEF--");
     assertEquals(8, sq.findPosition(0));
+    assertNull(PA.getValue(sq, "cursor"));
+    assertEquals(1, PA.getValue(sq, "changeCount"));
 
     sq.sequenceChanged();
     assertEquals(8, sq.findPosition(1));
+    assertNull(PA.getValue(sq, "cursor"));
 
     sq.sequenceChanged();
     assertEquals(8, sq.findPosition(2));
+    assertEquals("test:Pos8:Col3:startCol3:endCol0:tok3",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     assertEquals(9, sq.findPosition(3));
+    assertEquals("test:Pos9:Col4:startCol3:endCol0:tok4",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
+    // column[4] is a gap, returns next residue pos (C10)
+    // cursor is set to last residue found [B]
     assertEquals(10, sq.findPosition(4));
+    assertEquals("test:Pos9:Col4:startCol3:endCol0:tok5",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     assertEquals(10, sq.findPosition(5));
+    assertEquals("test:Pos10:Col6:startCol3:endCol0:tok6",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
+    // column[6] is a gap, returns next residue pos (D11)
+    // cursor is set to last residue found [C]
     assertEquals(11, sq.findPosition(6));
+    assertEquals("test:Pos10:Col6:startCol3:endCol0:tok7",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     assertEquals(11, sq.findPosition(7));
+    assertEquals("test:Pos11:Col8:startCol3:endCol0:tok8",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     assertEquals(12, sq.findPosition(8));
+    assertEquals("test:Pos12:Col9:startCol3:endCol0:tok9",
+            PA.getValue(sq, "cursor").toString());
 
+    /*
+     * when the last residue column is found, it is set in the cursor
+     */
     sq.sequenceChanged();
     assertEquals(13, sq.findPosition(9));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok10",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     assertEquals(14, sq.findPosition(10));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok11",
+            PA.getValue(sq, "cursor").toString());
 
     /*
      * findPosition for column beyond sequence length
@@ -383,8 +522,33 @@ public class SequenceTest
      */
     sq.sequenceChanged();
     assertEquals(14, sq.findPosition(11));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok12",
+            PA.getValue(sq, "cursor").toString());
+
     sq.sequenceChanged();
     assertEquals(14, sq.findPosition(99));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok13",
+            PA.getValue(sq, "cursor").toString());
+
+    /*
+     * gapped sequence ending in non-gap
+     */
+    sq = new Sequence("test/8-13", "--AB-C-DEF");
+    assertEquals(13, sq.findPosition(9));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok1",
+            PA.getValue(sq, "cursor").toString());
+    sq.sequenceChanged();
+    assertEquals(12, sq.findPosition(8)); // E12
+    // sequenceChanged() invalidates cursor.lastResidueColumn
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals("test:Pos12:Col9:startCol3:endCol0:tok2",
+            cursor.toString());
+    // findPosition with cursor accepts base 1 column values
+    assertEquals(13, ((Sequence) sq).findPosition(10, cursor));
+    assertEquals(13, sq.findPosition(9)); // F13
+    // lastResidueColumn has now been found and saved in cursor
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok2",
+            PA.getValue(sq, "cursor").toString());
   }
 
   @Test(groups = { "Functional" })
@@ -413,6 +577,13 @@ public class SequenceTest
     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
      */
@@ -422,6 +593,21 @@ public class SequenceTest
     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" })
@@ -447,9 +633,9 @@ public class SequenceTest
     assertNotNull(newDs);
     assertNotSame(ds, newDs);
     assertNotNull(sq.getDBRefs());
-    assertEquals(1, sq.getDBRefs().length);
-    assertNotSame(dbr1, sq.getDBRefs()[0]);
-    assertEquals(dbr1, sq.getDBRefs()[0]);
+    assertEquals(1, sq.getDBRefs().size());
+    assertNotSame(dbr1, sq.getDBRefs().get(0));
+    assertEquals(dbr1, sq.getDBRefs().get(0));
 
     /*
      * internal delete with sequence features
@@ -510,8 +696,8 @@ public class SequenceTest
     assertEquals(4, sq.getEnd());
     assertSame(ds, PA.getValue(sq, "datasetSequence"));
     assertNotNull(sq.getDBRefs());
-    assertEquals(1, sq.getDBRefs().length);
-    assertSame(dbr1, sq.getDBRefs()[0]);
+    assertEquals(1, sq.getDBRefs().size());
+    assertSame(dbr1, sq.getDBRefs().get(0));
   }
 
   @Test(groups = { "Functional" })
@@ -734,7 +920,7 @@ public class SequenceTest
     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));
@@ -745,12 +931,12 @@ public class SequenceTest
             new AlignmentAnnotation("Test annot", "Test annot description",
                     annots));
     Assert.assertEquals(sq.getDescription(), "Test sequence description..");
-    Assert.assertEquals(sq.getDBRefs().length, 5); // DBRefs are on dataset
+    Assert.assertEquals(sq.getDBRefs().size(), 5); // DBRefs are on dataset
                                                    // sequence
     Assert.assertEquals(sq.getAllPDBEntries().size(), 4);
     Assert.assertNotNull(sq.getAnnotation());
     Assert.assertEquals(sq.getAnnotation()[0].annotations.length, 2);
-    Assert.assertEquals(sq.getDatasetSequence().getDBRefs().length, 5); // same
+    Assert.assertEquals(sq.getDatasetSequence().getDBRefs().size(), 5); // same
                                                                         // as
                                                                         // sq.getDBRefs()
     Assert.assertEquals(sq.getDatasetSequence().getAllPDBEntries().size(),
@@ -761,11 +947,11 @@ public class SequenceTest
 
     Assert.assertEquals(derived.getDescription(),
             "Test sequence description..");
-    Assert.assertEquals(derived.getDBRefs().length, 5); // come from dataset
+    Assert.assertEquals(derived.getDBRefs().size(), 5); // come from dataset
     Assert.assertEquals(derived.getAllPDBEntries().size(), 4);
     Assert.assertNotNull(derived.getAnnotation());
     Assert.assertEquals(derived.getAnnotation()[0].annotations.length, 2);
-    Assert.assertEquals(derived.getDatasetSequence().getDBRefs().length, 5);
+    Assert.assertEquals(derived.getDatasetSequence().getDBRefs().size(), 5);
     Assert.assertEquals(derived.getDatasetSequence().getAllPDBEntries()
             .size(), 4);
     Assert.assertNotNull(derived.getDatasetSequence().getAnnotation());
@@ -844,10 +1030,10 @@ public class SequenceTest
     // but that doesn't distinguish it from an aligned sequence
     // which has not yet generated a dataset sequence
     // NB getDBRef looks inside dataset sequence if not null
-    DBRefEntry[] dbrefs = copy.getDBRefs();
-    assertEquals(1, dbrefs.length);
-    assertFalse(dbrefs[0] == seq1.getDBRefs()[0]);
-    assertTrue(dbrefs[0].equals(seq1.getDBRefs()[0]));
+    List<DBRefEntry> dbrefs = copy.getDBRefs();
+    assertEquals(1, dbrefs.size());
+    assertFalse(dbrefs.get(0) == seq1.getDBRefs().get(0));
+    assertTrue(dbrefs.get(0).equals(seq1.getDBRefs().get(0)));
   }
 
   @Test(groups = { "Functional" })
@@ -876,9 +1062,9 @@ public class SequenceTest
 
     // getDBRef looks inside dataset sequence and this is shared,
     // so holds the same dbref objects
-    DBRefEntry[] dbrefs = copy.getDBRefs();
-    assertEquals(1, dbrefs.length);
-    assertSame(dbrefs[0], seq1.getDBRefs()[0]);
+    List<DBRefEntry> dbrefs = copy.getDBRefs();
+    assertEquals(1, dbrefs.size());
+    assertSame(dbrefs.get(0), seq1.getDBRefs().get(0));
   }
 
   /**
@@ -977,39 +1163,39 @@ public class SequenceTest
     assertNull(sq.getDBRefs());
     DBRefEntry dbref = new DBRefEntry("Uniprot", "1", "P00340");
     sq.addDBRef(dbref);
-    assertEquals(1, sq.getDBRefs().length);
-    assertSame(dbref, sq.getDBRefs()[0]);
+    assertEquals(1, sq.getDBRefs().size());
+    assertSame(dbref, sq.getDBRefs().get(0));
 
     /*
      * change of version - new entry
      */
     DBRefEntry dbref2 = new DBRefEntry("Uniprot", "2", "P00340");
     sq.addDBRef(dbref2);
-    assertEquals(2, sq.getDBRefs().length);
-    assertSame(dbref, sq.getDBRefs()[0]);
-    assertSame(dbref2, sq.getDBRefs()[1]);
+    assertEquals(2, sq.getDBRefs().size());
+    assertSame(dbref, sq.getDBRefs().get(0));
+    assertSame(dbref2, sq.getDBRefs().get(1));
 
     /*
      * matches existing entry - not added
      */
     sq.addDBRef(new DBRefEntry("UNIPROT", "1", "p00340"));
-    assertEquals(2, sq.getDBRefs().length);
+    assertEquals(2, sq.getDBRefs().size());
 
     /*
      * different source = new entry
      */
     DBRefEntry dbref3 = new DBRefEntry("UniRef", "1", "p00340");
     sq.addDBRef(dbref3);
-    assertEquals(3, sq.getDBRefs().length);
-    assertSame(dbref3, sq.getDBRefs()[2]);
+    assertEquals(3, sq.getDBRefs().size());
+    assertSame(dbref3, sq.getDBRefs().get(2));
 
     /*
      * different ref = new entry
      */
     DBRefEntry dbref4 = new DBRefEntry("UniRef", "1", "p00341");
     sq.addDBRef(dbref4);
-    assertEquals(4, sq.getDBRefs().length);
-    assertSame(dbref4, sq.getDBRefs()[3]);
+    assertEquals(4, sq.getDBRefs().size());
+    assertSame(dbref4, sq.getDBRefs().get(3));
 
     /*
      * matching ref with a mapping - map updated
@@ -1019,8 +1205,8 @@ public class SequenceTest
         1, 1 }, 3, 1));
     dbref5.setMap(map);
     sq.addDBRef(dbref5);
-    assertEquals(4, sq.getDBRefs().length);
-    assertSame(dbref4, sq.getDBRefs()[3]);
+    assertEquals(4, sq.getDBRefs().size());
+    assertSame(dbref4, sq.getDBRefs().get(3));
     assertSame(map, dbref4.getMap());
 
     /*
@@ -1030,8 +1216,8 @@ public class SequenceTest
     DBRefEntry dbref6 = new DBRefEntry(dbref2.getSource(), "3",
             dbref2.getAccessionId());
     sq.addDBRef(dbref6);
-    assertEquals(4, sq.getDBRefs().length);
-    assertSame(dbref2, sq.getDBRefs()[1]);
+    assertEquals(4, sq.getDBRefs().size());
+    assertSame(dbref2, sq.getDBRefs().get(1));
     assertEquals("3", dbref2.getVersion());
 
     /*
@@ -1041,8 +1227,8 @@ public class SequenceTest
     DBRefEntry dbref7 = new DBRefEntry(dbref3.getSource(), "3",
             dbref3.getAccessionId());
     sq.addDBRef(dbref7);
-    assertEquals(4, sq.getDBRefs().length);
-    assertSame(dbref3, sq.getDBRefs()[2]);
+    assertEquals(4, sq.getDBRefs().size());
+    assertSame(dbref3, sq.getDBRefs().get(2));
     assertEquals("3", dbref2.getVersion());
   }
 
@@ -1056,7 +1242,7 @@ public class SequenceTest
     assertTrue(primaryDBRefs.isEmpty());
 
     // empty dbrefs
-    sq.setDBRefs(new DBRefEntry[] {});
+       sq.setDBRefs(null);
     primaryDBRefs = sq.getPrimaryDBRefs();
     assertTrue(primaryDBRefs.isEmpty());
 
@@ -1265,146 +1451,207 @@ public class SequenceTest
     seq.setDatasetSequence(seq2);
   }
 
-  @Test
-  public void testFindPositions()
+  @Test(groups = { "Functional" })
+  public void testFindFeatures()
   {
-    SequenceI sq = new Sequence("Seq", "ABC--DE-F", 8, 13);
-
-    Range range = sq.findPositions(1, 4); // BC
-    assertEquals(new Range(9, 10), range);
-
-    range = sq.findPositions(2, 4); // C
-    assertEquals(new Range(10, 10), range);
-
-    assertNull(sq.findPositions(3, 4)); // all gaps
+    SequenceI sq = new Sequence("test/8-16", "-ABC--DEF--GHI--");
+    sq.createDatasetSequence();
 
-    range = sq.findPositions(2, 6); // CDE
-    assertEquals(new Range(10, 12), range);
+    assertTrue(sq.findFeatures(1, 99).isEmpty());
 
-    range = sq.findPositions(3, 7); // DE
-    assertEquals(new Range(11, 12), range);
+    // add non-positional feature
+    SequenceFeature sf0 = new SequenceFeature("Cath", "desc", 0, 0, 2f,
+            null);
+    sq.addSequenceFeature(sf0);
+    // add feature on BCD
+    SequenceFeature sfBCD = new SequenceFeature("Cath", "desc", 9, 11, 2f,
+            null);
+    sq.addSequenceFeature(sfBCD);
+    // add feature on DE
+    SequenceFeature sfDE = new SequenceFeature("Cath", "desc", 11, 12, 2f,
+            null);
+    sq.addSequenceFeature(sfDE);
+    // add contact feature at [B, H]
+    SequenceFeature sfContactBH = new SequenceFeature("Disulphide bond",
+            "desc", 9, 15, 2f, null);
+    sq.addSequenceFeature(sfContactBH);
+    // add contact feature at [F, G]
+    SequenceFeature sfContactFG = new SequenceFeature("Disulfide Bond",
+            "desc", 13, 14, 2f, null);
+    sq.addSequenceFeature(sfContactFG);
+    // add single position feature at [I]
+    SequenceFeature sfI = new SequenceFeature("Disulfide Bond",
+            "desc", 16, 16, null);
+    sq.addSequenceFeature(sfI);
+
+    // no features in columns 1-2 (-A)
+    List<SequenceFeature> found = sq.findFeatures(1, 2);
+    assertTrue(found.isEmpty());
+
+    // columns 1-6 (-ABC--) includes BCD and B/H feature but not DE
+    found = sq.findFeatures(1, 6);
+    assertEquals(2, found.size());
+    assertTrue(found.contains(sfBCD));
+    assertTrue(found.contains(sfContactBH));
+
+    // columns 5-6 (--) includes (enclosing) BCD but not (contact) B/H feature
+    found = sq.findFeatures(5, 6);
+    assertEquals(1, found.size());
+    assertTrue(found.contains(sfBCD));
+
+    // columns 7-10 (DEF-) includes BCD, DE, F/G but not B/H feature
+    found = sq.findFeatures(7, 10);
+    assertEquals(3, found.size());
+    assertTrue(found.contains(sfBCD));
+    assertTrue(found.contains(sfDE));
+    assertTrue(found.contains(sfContactFG));
+
+    // columns 10-11 (--) should find nothing
+    found = sq.findFeatures(10, 11);
+    assertEquals(0, found.size());
+
+    // columns 14-14 (I) should find variant feature
+    found = sq.findFeatures(14, 14);
+    assertEquals(1, found.size());
+    assertTrue(found.contains(sfI));
   }
 
   @Test(groups = { "Functional" })
   public void testFindIndex_withCursor()
   {
     Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--");
+    final int tok = (int) PA.getValue(sq, "changeCount");
+    assertEquals(1, tok);
 
-    // find F given A
-    assertEquals(10, sq.findIndex(13, new SequenceCursor(sq, 8, 2, 0)));
+    // find F given A, check cursor is now at the found position
+    assertEquals(10, sq.findIndex(13, new SequenceCursor(sq, 8, 2, tok)));
+    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)));
+    assertEquals(2, sq.findIndex(8, new SequenceCursor(sq, 13, 10, tok)));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(8, cursor.residuePosition);
+    assertEquals(2, cursor.columnPosition);
 
-    // find C given C
-    assertEquals(6, sq.findIndex(10, new SequenceCursor(sq, 10, 6, 0)));
+    // find C given C (no cursor update is done for this case)
+    assertEquals(6, sq.findIndex(10, new SequenceCursor(sq, 10, 6, tok)));
+    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" })
   public void testFindPosition_withCursor()
   {
     Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--");
+    final int tok = (int) PA.getValue(sq, "changeCount");
+    assertEquals(1, tok);
   
-    // find F pos given A
-    assertEquals(13, sq.findPosition(10, new SequenceCursor(sq, 8, 2, 0)));
-    int token = (int) PA.getValue(sq, "changeCount"); // 0
-    SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor");
-    assertEquals(new SequenceCursor(sq, 13, 10, token), cursor);
-
-    // find A pos given F
-    assertEquals(8, sq.findPosition(2, new SequenceCursor(sq, 13, 10, 0)));
-    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
-    assertEquals(new SequenceCursor(sq, 8, 2, token), cursor);
+    // find F pos given A - lastCol gets set in cursor
+    assertEquals(13,
+            sq.findPosition(10, new SequenceCursor(sq, 8, 2, tok)));
+    assertEquals("test:Pos13:Col10:startCol0:endCol10:tok1",
+            PA.getValue(sq, "cursor").toString());
+
+    // find A pos given F - first residue column is saved in cursor
+    assertEquals(8,
+            sq.findPosition(2, new SequenceCursor(sq, 13, 10, tok)));
+    assertEquals("test:Pos8:Col2:startCol2:endCol10:tok1",
+            PA.getValue(sq, "cursor").toString());
   
-    // find C pos given C
-    assertEquals(10, sq.findPosition(6, new SequenceCursor(sq, 10, 6, 0)));
-    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
-    assertEquals(new SequenceCursor(sq, 10, 6, token), cursor);
+    // find C pos given C (neither startCol nor endCol is set)
+    assertEquals(10,
+            sq.findPosition(6, new SequenceCursor(sq, 10, 6, tok)));
+    assertEquals("test:Pos10:Col6:startCol0:endCol0:tok1",
+            PA.getValue(sq, "cursor").toString());
 
     // now the grey area - what residue position for a gapped column? JAL-2562
 
     // find 'residue' for column 3 given cursor for D (so working left)
     // returns B9; cursor is updated to [B 5]
-    assertEquals(9, sq.findPosition(3, new SequenceCursor(sq, 11, 7, 0)));
-    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
-    assertEquals(new SequenceCursor(sq, 9, 5, token), cursor);
+    assertEquals(9, sq.findPosition(3, new SequenceCursor(sq, 11, 7, tok)));
+    assertEquals("test:Pos9:Col5:startCol0:endCol0:tok1",
+            PA.getValue(sq, "cursor").toString());
 
     // find 'residue' for column 8 given cursor for D (so working right)
     // returns E12; cursor is updated to [D 7]
-    assertEquals(12, sq.findPosition(8, new SequenceCursor(sq, 11, 7, 0)));
-    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
-    assertEquals(new SequenceCursor(sq, 11, 7, token), cursor);
+    assertEquals(12,
+            sq.findPosition(8, new SequenceCursor(sq, 11, 7, tok)));
+    assertEquals("test:Pos11:Col7:startCol0:endCol0:tok1",
+            PA.getValue(sq, "cursor").toString());
 
     // find 'residue' for column 12 given cursor for B
     // returns 1 more than last residue position; cursor is updated to [F 10]
-    assertEquals(14, sq.findPosition(12, new SequenceCursor(sq, 9, 5, 0)));
-    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
-    assertEquals(new SequenceCursor(sq, 13, 10, token), cursor);
+    // lastCol position is saved in cursor
+    assertEquals(14,
+            sq.findPosition(12, new SequenceCursor(sq, 9, 5, tok)));
+    assertEquals("test:Pos13:Col10:startCol0:endCol10:tok1",
+            PA.getValue(sq, "cursor").toString());
 
     /*
      * findPosition for column beyond length of sequence
      * returns 1 more than the last residue position
      * cursor is set to last real residue position [F 10]
      */
-    assertEquals(14, sq.findPosition(99, new SequenceCursor(sq, 8, 2, 0)));
-    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
-    assertEquals(new SequenceCursor(sq, 13, 10, token), cursor);
+    assertEquals(14,
+            sq.findPosition(99, new SequenceCursor(sq, 8, 2, tok)));
+    assertEquals("test:Pos13:Col10:startCol0:endCol10:tok1",
+            PA.getValue(sq, "cursor").toString());
 
     /*
      * and the case without a trailing gap
      */
     sq = new Sequence("test/8-13", "-A--BCD-EF");
     // first find C from A
-    assertEquals(10, sq.findPosition(6, new SequenceCursor(sq, 8, 2, 0)));
-    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
-    assertEquals(new SequenceCursor(sq, 10, 6, token), cursor);
+    assertEquals(10, sq.findPosition(6, new SequenceCursor(sq, 8, 2, tok)));
+    SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals("test:Pos10:Col6:startCol0:endCol0:tok1",
+            cursor.toString());
     // now 'find' 99 from C
-    // cursor is set to [F 10]
+    // cursor is set to [F 10] and saved lastCol
     assertEquals(14, sq.findPosition(99, cursor));
-    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
-    assertEquals(new SequenceCursor(sq, 13, 10, token), cursor);
-  }
-
-  @Test
-  public void testFindPositions_withCursor()
-  {
-    Sequence sq = new Sequence("Seq", "ABC--DE-F", 8, 13);
-  
-    // find positions for columns 1-4 (BC--) given E cursor
-    Range range = sq.findPositions(1, 4, new SequenceCursor(sq, 12, 7, 0)); // BC
-    assertEquals(new Range(9, 10), range);
-
-    // repeat using B cursor
-    range = sq.findPositions(1, 4, new SequenceCursor(sq, 9, 2, 0)); // BC
-    assertEquals(new Range(9, 10), range);
-  
-    // find positions for columns 2-4 (C--) given A cursor
-    range = sq.findPositions(2, 4, new SequenceCursor(sq, 8, 1, 0)); // C
-    assertEquals(new Range(10, 10), range);
-  
-    // gapped region
-    assertNull(sq.findPositions(3, 4, new SequenceCursor(sq, 10, 3, 0)));
-    assertNull(sq.findPositions(3, 4, new SequenceCursor(sq, 12, 7, 0)));
-  
-    // find positions for columns 2-6 (C--DE) given B cursor
-    range = sq.findPositions(2, 6, new SequenceCursor(sq, 9, 2, 0)); // CDE
-    assertEquals(new Range(10, 12), range);
-
-    // repeat using C as cursor
-    range = sq.findPositions(2, 6, new SequenceCursor(sq, 10, 3, 0));
-    assertEquals(new Range(10, 12), range);
-
-    // repeat using D as cursor
-    range = sq.findPositions(2, 6, new SequenceCursor(sq, 11, 6, 0));
-    assertEquals(new Range(10, 12), range);
-
-    // repeat using E as cursor
-    range = sq.findPositions(2, 6, new SequenceCursor(sq, 12, 7, 0));
-    assertEquals(new Range(10, 12), range);
-
-    // repeat using F as cursor
-    range = sq.findPositions(2, 6, new SequenceCursor(sq, 13, 9, 0));
-    assertEquals(new Range(10, 12), range);
+    assertEquals("test:Pos13:Col10:startCol0:endCol10:tok1",
+            PA.getValue(sq, "cursor").toString());
   }
 
   @Test
@@ -1422,11 +1669,11 @@ public class SequenceTest
     assertTrue(sq.isValidCursor(cursor));
 
     /*
-     * column position outside [0 - length-1] is rejected
+     * column position outside [0 - length] is rejected
      */
     cursor = new SequenceCursor(sq, 13, -1, changeCount);
     assertFalse(sq.isValidCursor(cursor));
-    cursor = new SequenceCursor(sq, 13, 9, changeCount);
+    cursor = new SequenceCursor(sq, 13, 10, changeCount);
     assertFalse(sq.isValidCursor(cursor));
     cursor = new SequenceCursor(sq, 7, 8, changeCount);
     assertFalse(sq.isValidCursor(cursor));
@@ -1471,6 +1718,10 @@ public class SequenceTest
     // 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
@@ -1506,5 +1757,460 @@ public class SequenceTest
     // cursor should now be at [F 10]
     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     assertEquals(new SequenceCursor(sq, 13, 10, ++token), cursor);
+
+    /*
+     * changing sequence start should invalidate cursor
+     */
+    sq = new Sequence("test/8-13", "-A--BCD-EF--");
+    assertEquals(8, sq.getStart());
+    assertEquals(9, sq.findPosition(4)); // B(9)
+    sq.setStart(7);
+    assertEquals(8, sq.findPosition(4)); // is now B(8)
+    sq.setStart(10);
+    assertEquals(11, sq.findPosition(4)); // is now B(11)
+  }
+
+  @Test(groups = { "Functional" })
+  public void testGetSequence()
+  {
+    String seqstring = "-A--BCD-EF--";
+    Sequence sq = new Sequence("test/8-13", seqstring);
+    sq.createDatasetSequence();
+    assertTrue(Arrays.equals(sq.getSequence(), seqstring.toCharArray()));
+    assertTrue(Arrays.equals(sq.getDatasetSequence().getSequence(),
+            "ABCDEF".toCharArray()));
+
+    // verify a copy of the sequence array is returned
+    char[] theSeq = (char[]) PA.getValue(sq, "sequence");
+    assertNotSame(theSeq, sq.getSequence());
+    theSeq = (char[]) PA.getValue(sq.getDatasetSequence(), "sequence");
+    assertNotSame(theSeq, sq.getDatasetSequence().getSequence());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testReplace()
+  {
+    String seqstring = "-A--BCD-EF--";
+    SequenceI sq = new Sequence("test/8-13", seqstring);
+    // changeCount is incremented for setStart
+    assertEquals(1, PA.getValue(sq, "changeCount"));
+
+    assertEquals(0, sq.replace('A', 'A')); // same char
+    assertEquals(seqstring, sq.getSequenceAsString());
+    assertEquals(1, PA.getValue(sq, "changeCount"));
+
+    assertEquals(0, sq.replace('X', 'Y')); // not there
+    assertEquals(seqstring, sq.getSequenceAsString());
+    assertEquals(1, PA.getValue(sq, "changeCount"));
+
+    assertEquals(1, sq.replace('A', 'K'));
+    assertEquals("-K--BCD-EF--", sq.getSequenceAsString());
+    assertEquals(2, PA.getValue(sq, "changeCount"));
+
+    assertEquals(6, sq.replace('-', '.'));
+    assertEquals(".K..BCD.EF..", sq.getSequenceAsString());
+    assertEquals(3, PA.getValue(sq, "changeCount"));
+  }
+
+  @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()
+  {
+    /*
+     * imitate a PDB sequence where end is larger than end position
+     */
+    SequenceI sq = new Sequence("test", "-ABC--DEF--", 1, 20);
+    sq.createDatasetSequence();
+  
+    assertTrue(sq.findFeatures(1, 9).isEmpty());
+    // should be no array bounds exception - JAL-2772
+    assertTrue(sq.findFeatures(1, 15).isEmpty());
+  
+    // add feature on BCD
+    SequenceFeature sfBCD = new SequenceFeature("Cath", "desc", 2, 4, 2f,
+            null);
+    sq.addSequenceFeature(sfBCD);
+  
+    // no features in columns 1-2 (-A)
+    List<SequenceFeature> found = sq.findFeatures(1, 2);
+    assertTrue(found.isEmpty());
+  
+    // columns 1-6 (-ABC--) includes BCD
+    found = sq.findFeatures(1, 6);
+    assertEquals(1, found.size());
+    assertTrue(found.contains(sfBCD));
+
+    // columns 10-11 (--) should find nothing
+    found = sq.findFeatures(10, 11);
+    assertEquals(0, found.size());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetName()
+  {
+    SequenceI sq = new Sequence("test", "-ABC---DE-F--");
+    assertEquals("test", sq.getName());
+    assertEquals(1, sq.getStart());
+    assertEquals(6, sq.getEnd());
+
+    sq.setName("testing");
+    assertEquals("testing", sq.getName());
+
+    sq.setName("test/8-10");
+    assertEquals("test", sq.getName());
+    assertEquals(8, sq.getStart());
+    assertEquals(13, sq.getEnd()); // note end is recomputed
+
+    sq.setName("testing/7-99");
+    assertEquals("testing", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd()); // end may be beyond physical end
+
+    sq.setName("/2-3");
+    assertEquals("", sq.getName());
+    assertEquals(2, sq.getStart());
+    assertEquals(7, sq.getEnd());
+
+    sq.setName("test/"); // invalid
+    assertEquals("test/", sq.getName());
+    assertEquals(2, sq.getStart());
+    assertEquals(7, sq.getEnd());
+
+    sq.setName("test/6-13/7-99");
+    assertEquals("test/6-13", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd());
+
+    sq.setName("test/0-5"); // 0 is invalid - ignored
+    assertEquals("test/0-5", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd());
+
+    sq.setName("test/a-5"); // a is invalid - ignored
+    assertEquals("test/a-5", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd());
+
+    sq.setName("test/6-5"); // start > end is invalid - ignored
+    assertEquals("test/6-5", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd());
+
+    sq.setName("test/5"); // invalid - ignored
+    assertEquals("test/5", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd());
+
+    sq.setName("test/-5"); // invalid - ignored
+    assertEquals("test/-5", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd());
+
+    sq.setName("test/5-"); // invalid - ignored
+    assertEquals("test/5-", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd());
+
+    sq.setName("test/5-6-7"); // invalid - ignored
+    assertEquals("test/5-6-7", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd());
+
+    sq.setName(null); // invalid, gets converted to space
+    assertEquals("", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testCheckValidRange()
+  {
+    Sequence sq = new Sequence("test/7-12", "-ABC---DE-F--");
+    assertEquals(7, sq.getStart());
+    assertEquals(12, sq.getEnd());
+
+    /*
+     * checkValidRange ensures end is at least the last residue position
+     */
+    PA.setValue(sq, "end", 2);
+    sq.checkValidRange();
+    assertEquals(12, sq.getEnd());
+
+    /*
+     * end may be beyond the last residue position
+     */
+    PA.setValue(sq, "end", 22);
+    sq.checkValidRange();
+    assertEquals(22, sq.getEnd());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testDeleteChars_withGaps()
+  {
+    /*
+     * delete gaps only
+     */
+    SequenceI sq = new Sequence("test/8-10", "A-B-C");
+    sq.createDatasetSequence();
+    assertEquals("ABC", sq.getDatasetSequence().getSequenceAsString());
+    sq.deleteChars(1, 2); // delete first gap
+    assertEquals("AB-C", sq.getSequenceAsString());
+    assertEquals(8, sq.getStart());
+    assertEquals(10, sq.getEnd());
+    assertEquals("ABC", sq.getDatasetSequence().getSequenceAsString());
+
+    /*
+     * delete gaps and residues at start (no new dataset sequence)
+     */
+    sq = new Sequence("test/8-10", "A-B-C");
+    sq.createDatasetSequence();
+    sq.deleteChars(0, 3); // delete A-B
+    assertEquals("-C", sq.getSequenceAsString());
+    assertEquals(10, sq.getStart());
+    assertEquals(10, sq.getEnd());
+    assertEquals("ABC", sq.getDatasetSequence().getSequenceAsString());
+
+    /*
+     * delete gaps and residues at end (no new dataset sequence)
+     */
+    sq = new Sequence("test/8-10", "A-B-C");
+    sq.createDatasetSequence();
+    sq.deleteChars(2, 5); // delete B-C
+    assertEquals("A-", sq.getSequenceAsString());
+    assertEquals(8, sq.getStart());
+    assertEquals(8, sq.getEnd());
+    assertEquals("ABC", sq.getDatasetSequence().getSequenceAsString());
+
+    /*
+     * delete gaps and residues internally (new dataset sequence)
+     * first delete from gap to residue
+     */
+    sq = new Sequence("test/8-10", "A-B-C");
+    sq.createDatasetSequence();
+    sq.deleteChars(1, 3); // delete -B
+    assertEquals("A-C", sq.getSequenceAsString());
+    assertEquals(8, sq.getStart());
+    assertEquals(9, sq.getEnd());
+    assertEquals("AC", sq.getDatasetSequence().getSequenceAsString());
+    assertEquals(8, sq.getDatasetSequence().getStart());
+    assertEquals(9, sq.getDatasetSequence().getEnd());
+
+    /*
+     * internal delete from gap to gap
+     */
+    sq = new Sequence("test/8-10", "A-B-C");
+    sq.createDatasetSequence();
+    sq.deleteChars(1, 4); // delete -B-
+    assertEquals("AC", sq.getSequenceAsString());
+    assertEquals(8, sq.getStart());
+    assertEquals(9, sq.getEnd());
+    assertEquals("AC", sq.getDatasetSequence().getSequenceAsString());
+    assertEquals(8, sq.getDatasetSequence().getStart());
+    assertEquals(9, sq.getDatasetSequence().getEnd());
+
+    /*
+     * internal delete from residue to residue
+     */
+    sq = new Sequence("test/8-10", "A-B-C");
+    sq.createDatasetSequence();
+    sq.deleteChars(2, 3); // delete B
+    assertEquals("A--C", sq.getSequenceAsString());
+    assertEquals(8, sq.getStart());
+    assertEquals(9, sq.getEnd());
+    assertEquals("AC", sq.getDatasetSequence().getSequenceAsString());
+    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()));
+
+  }
+  @Test(groups= {"Functional"})
+  public void testTransferAnnotation() {
+    Sequence origSeq = new Sequence("MYSEQ","THISISASEQ");
+    Sequence toSeq = new Sequence("MYSEQ","THISISASEQ");
+    origSeq.addDBRef(new DBRefEntry("UNIPROT", "0", "Q12345", null, true));
+    toSeq.transferAnnotation(origSeq, null);
+    assertTrue(toSeq.getDBRefs().size()==1);
+    
+    assertTrue(toSeq.getDBRefs().get(0).isCanonical());
+    
+    // check for promotion of non-canonical 
+    // to canonical (e.g. fetch-db-refs on a jalview project pre 2.11.2)
+    toSeq.setDBRefs(null);
+    toSeq.addDBRef(new DBRefEntry("UNIPROT", "0", "Q12345", null, false));
+    toSeq.transferAnnotation(origSeq, null);
+    assertTrue(toSeq.getDBRefs().size()==1);
+    
+    assertTrue("Promotion of non-canonical DBRefEntry failed",toSeq.getDBRefs().get(0).isCanonical());
+    
+    
   }
 }