From: gmungoc Date: Mon, 5 Jun 2017 07:54:11 +0000 (+0100) Subject: Merge branch 'features/JAL-2446NCList' into X-Git-Tag: Release_2_10_3b1~214^2 X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=88694463a2aea303694231603b61970f72a5a259;hp=-c;p=jalview.git Merge branch 'features/JAL-2446NCList' into features/JAL-2526sequenceCursor Conflicts: src/jalview/datamodel/SequenceI.java --- 88694463a2aea303694231603b61970f72a5a259 diff --combined src/jalview/datamodel/Sequence.java index 24f904c,324d21b..ab6639a --- a/src/jalview/datamodel/Sequence.java +++ b/src/jalview/datamodel/Sequence.java @@@ -31,6 -31,7 +31,7 @@@ import jalview.util.StringUtils import java.util.ArrayList; import java.util.Arrays; + import java.util.BitSet; import java.util.Collections; import java.util.Enumeration; import java.util.List; @@@ -89,21 -90,6 +90,21 @@@ public class Sequence extends ASequenc private SequenceFeatures sequenceFeatureStore; + /* + * A cursor holding the approximate current view position to the sequence, + * as determined by findIndex or findPosition or findPositions. + * Using a cursor as a hint allows these methods to be more performant for + * large sequences. + */ + private SequenceCursor cursor; + + /* + * A number that should be incremented whenever the sequence is edited. + * If the value matches the cursor token, then we can trust the cursor, + * if not then it should be recomputed. + */ + private int changeCount; + /** * Creates a new Sequence object. * @@@ -543,7 -529,6 +544,7 @@@ { this.sequence = seq.toCharArray(); checkValidRange(); + sequenceChanged(); } @Override @@@ -664,20 -649,15 +665,20 @@@ return this.description; } - /* - * (non-Javadoc) - * - * @see jalview.datamodel.SequenceI#findIndex(int) + /** + * {@inheritDoc} */ @Override public int findIndex(int pos) { - // returns the alignment position for a residue + /* + * use a valid, hopefully nearby, cursor if available + */ + if (isValidCursor(cursor)) + { + return findIndex(pos, cursor); + } + int j = start; int i = 0; // Rely on end being at least as long as the length of the sequence. @@@ -687,248 -667,45 +688,248 @@@ { j++; } i++; } - if ((j == end) && (j < pos)) + if (j == end && j < pos) { return end + 1; } - else + + updateCursor(pos, i); + return i; + } + + /** + * Updates the cursor to the latest found residue and column position + * + * @param residuePos + * (start..) + * @param column + * (1..) + */ + protected void updateCursor(int residuePos, int column) + { + cursor = new SequenceCursor(this, residuePos, column, this.changeCount); + } + + /** + * Answers the aligned column position (1..) for the given residue position + * (start..) given a 'hint' of a residue/column location in the neighbourhood. + * The hint may be left of, at, or to the right of the required position. + * + * @param pos + * @param curs + * @return + */ + protected int findIndex(int pos, SequenceCursor curs) + { + if (!isValidCursor(curs)) + { + /* + * wrong or invalidated cursor, compute de novo + */ + return findIndex(pos); + } + + if (curs.residuePosition == pos) + { + return curs.columnPosition; + } + + /* + * move left or right to find pos from hint.position + */ + int col = curs.columnPosition - 1; // convert from base 1 to 0-based array + // index + int newPos = curs.residuePosition; + int delta = newPos > pos ? -1 : 1; + + while (newPos != pos) { - return i; + col += delta; // shift one column left or right + if (col < 0 || col == sequence.length) + { + break; + } + if (!Comparison.isGap(sequence[col])) + { + newPos += delta; + } } + + col++; // convert back to base 1 + updateCursor(pos, col); + + return col; } + /** + * {@inheritDoc} + */ @Override - public int findPosition(int i) + public int findPosition(final int column) { + /* + * use a valid, hopefully nearby, cursor if available + */ + if (isValidCursor(cursor)) + { + return findPosition(column + 1, cursor); + } + + // TODO recode this more naturally i.e. count residues only + // as they are found, not 'in anticipation' + + int lastPosFound = 0; + int lastPosFoundColumn = 0; + int seqlen = sequence.length; + if (seqlen > 0 && !Comparison.isGap(sequence[0])) + { + lastPosFound = start; + lastPosFoundColumn = 0; + } + int j = 0; int pos = start; - int seqlen = sequence.length; - while ((j < i) && (j < seqlen)) + + while (j < column && j < seqlen) { if (!Comparison.isGap(sequence[j])) { + lastPosFound = pos; + lastPosFoundColumn = j; pos++; } - j++; } + if (j < seqlen && !Comparison.isGap(sequence[j])) + { + lastPosFound = pos; + lastPosFoundColumn = j; + } + + /* + * update the cursor to the last residue position found (if any) + * (converting column position to base 1) + */ + if (lastPosFound != 0) + { + updateCursor(lastPosFound, lastPosFoundColumn + 1); + } return pos; } /** + * Answers true if the given cursor is not null, is for this sequence object, + * and has a token value that matches this object's changeCount, else false. + * This allows us to ignore a cursor as 'stale' if the sequence has been + * modified since the cursor was created. + * + * @param curs + * @return + */ + protected boolean isValidCursor(SequenceCursor curs) + { + if (curs == null || curs.sequence != this || curs.token != changeCount) + { + return false; + } + /* + * sanity check against range + */ + if (curs.columnPosition < 0 || curs.columnPosition >= sequence.length) + { + return false; + } + if (curs.residuePosition < start || curs.residuePosition > end) + { + return false; + } + return true; + } + + /** + * Answers the sequence position (start..) for the given aligned column + * position (1..), given a hint of a cursor in the neighbourhood. The cursor + * may lie left of, at, or to the right of the column position. + * + * @param col + * @param curs + * @return + */ + protected int findPosition(final int col, SequenceCursor curs) + { + if (!isValidCursor(curs)) + { + /* + * wrong or invalidated cursor, compute de novo + */ + return findPosition(col - 1);// ugh back to base 0 + } + + if (curs.columnPosition == col) + { + cursor = curs; // in case this method becomes public + return curs.residuePosition; // easy case :-) + } + + /* + * move left or right to find pos from cursor position + */ + int column = curs.columnPosition - 1; // to base 0 + int newPos = curs.residuePosition; + int delta = curs.columnPosition > col ? -1 : 1; + boolean gapped = false; + int lastFoundPosition = curs.residuePosition; + int lastFoundPositionColumn = curs.columnPosition; + + while (column != col - 1) + { + column += delta; // shift one column left or right + if (column < 0 || column == sequence.length) + { + break; + } + gapped = Comparison.isGap(sequence[column]); + if (!gapped) + { + newPos += delta; + lastFoundPosition = newPos; + lastFoundPositionColumn = column + 1; + } + } + + if (cursor == null || lastFoundPosition != cursor.residuePosition) + { + updateCursor(lastFoundPosition, lastFoundPositionColumn); + } + + /* + * hack to give position to the right if on a gap + * or beyond the length of the sequence (see JAL-2562) + */ + if (delta > 0 && (gapped || column >= sequence.length)) + { + newPos++; + } + + return newPos; + } + + /** * {@inheritDoc} */ @Override public Range findPositions(int fromCol, int toCol) { + if (cursor != null && cursor.sequence == this + && cursor.token == changeCount) + { + return findPositions(fromCol, toCol, cursor); + } + /* * count residues before fromCol */ @@@ -949,6 -726,7 +950,6 @@@ */ int firstPos = 0; int lastPos = 0; - int firstPosCol = 0; boolean foundFirst = false; while (j <= toCol && j < seqlen) @@@ -959,6 -737,7 +960,6 @@@ if (!foundFirst) { firstPos = count; - firstPosCol = j; foundFirst = true; } lastPos = count; @@@ -984,104 -763,6 +985,104 @@@ } /** + * Returns the range of sequence positions included in the given alignment + * position range. If no positions are included (the range is entirely gaps), + * then returns null. The cursor parameter may provide a starting position in + * the neighbourhood of the search (which may be left of, right of, or + * overlapping the search region). + * + * @param fromCol + * start column of region (0..) + * @param toCol + * end column of region (0..) + * @param curs + * @return + */ + protected Range findPositions(int fromCol, int toCol, SequenceCursor curs) + { + if (!isValidCursor(curs)) + { + /* + * wrong or invalidated cursor, compute de novo + */ + return findPositions(fromCol, toCol); + } + + /* + * keep this simple...first step from cursor to fromCol... + */ + final int seqlen = sequence.length; + int resNo = curs.residuePosition; + int col = curs.columnPosition - 1; // from base 1 to base 0 + if (col != fromCol) + { + int delta = col > fromCol ? -1 : 1; + while (col != fromCol && col >= 0 && col < seqlen) + { + if (!Comparison.isGap(sequence[col])) + { + resNo += delta; + } + col += delta; + } + } + + if (col < fromCol || col == seqlen) + { + /* + * sequence lies to the left of the target region + */ + return null; + } + + /* + * resNo is now the residue at fromCol (if not gapped), else the one + * before it (if delta == 1), else the one after (if delta == -1); + * we want the residue before fromCol + */ + if (!Comparison.isGap(sequence[fromCol])) + { + resNo--; + } + else if (curs.columnPosition > fromCol) + { + resNo -= 2; + } + + /* + * now first and last residues between fromCol and toCol + */ + int firstPos = 0; + int lastPos = 0; + boolean foundFirst = false; + + while (col <= toCol && col < seqlen) + { + if (!Comparison.isGap(sequence[col])) + { + resNo++; + if (!foundFirst) + { + firstPos = resNo; + foundFirst = true; + } + lastPos = resNo; + } + col++; + } + + if (firstPos == 0) + { + /* + * no residues in this range + */ + return null; + } + + return new Range(firstPos, lastPos); + } + + /** * Returns an int array where indices correspond to each residue in the * sequence and the element value gives its position in the alignment * @@@ -1165,6 -846,40 +1166,40 @@@ } @Override + public BitSet getInsertionsAsBits() + { + BitSet map = new BitSet(); + int lastj = -1, j = 0; + int pos = start; + int seqlen = sequence.length; + while ((j < seqlen)) + { + if (jalview.util.Comparison.isGap(sequence[j])) + { + if (lastj == -1) + { + lastj = j; + } + } + else + { + if (lastj != -1) + { + map.set(lastj, j); + lastj = -1; + } + } + j++; + } + if (lastj != -1) + { + map.set(lastj, j); + lastj = -1; + } + return map; + } + + @Override public void deleteChars(int i, int j) { int newstart = start, newend = end; @@@ -1237,7 -952,6 +1272,7 @@@ start = newstart; end = newend; sequence = tmp; + sequenceChanged(); } @Override @@@ -1268,7 -982,6 +1303,7 @@@ } sequence = tmp; + sequenceChanged(); } @Override @@@ -1836,14 -1549,4 +1871,14 @@@ } return sequenceFeatureStore.findFeatures(from, to, types); } + + /** + * Invalidates any stale cursors (forcing recalculation) by incrementing the + * token that has to match the one presented by the cursor + */ + @Override + public void sequenceChanged() + { + changeCount++; + } } diff --combined src/jalview/datamodel/SequenceI.java index 18f0948,cf9d6ad..38be37f --- a/src/jalview/datamodel/SequenceI.java +++ b/src/jalview/datamodel/SequenceI.java @@@ -22,6 -22,7 +22,7 @@@ package jalview.datamodel import jalview.datamodel.features.SequenceFeaturesI; + import java.util.BitSet; import java.util.List; import java.util.Vector; @@@ -520,11 -521,11 +521,18 @@@ public interface SequenceI extends ASeq * @return */ List findFeatures(int from, int to, String... types); + + /** + * Method to call to indicate that the sequence (characters or alignment/gaps) + * has been modified. Provided to allow any cursors on residue/column + * positions to be invalidated. + */ + void sequenceChanged(); + + /** + * + * @return BitSet corresponding to index [0,length) where Comparison.isGap() + * returns true. + */ + BitSet getInsertionsAsBits(); } diff --combined test/jalview/datamodel/SequenceTest.java index 4f42947,4e8efcd..c5850dc --- a/test/jalview/datamodel/SequenceTest.java +++ b/test/jalview/datamodel/SequenceTest.java @@@ -28,8 -28,6 +28,8 @@@ import static org.testng.AssertJUnit.as import static org.testng.AssertJUnit.assertSame; import static org.testng.AssertJUnit.assertTrue; +import jalview.commands.EditCommand; +import jalview.commands.EditCommand.Action; import jalview.datamodel.PDBEntry.Type; import jalview.gui.JvOptionPane; import jalview.util.MapList; @@@ -37,6 -35,7 +37,7 @@@ import java.io.File; import java.util.ArrayList; import java.util.Arrays; + import java.util.BitSet; import java.util.List; import java.util.Vector; @@@ -78,6 -77,18 +79,18 @@@ public class SequenceTes assertEquals("Gap interval 1 end wrong", 4, gapInt.get(0)[1]); assertEquals("Gap interval 2 start wrong", 6, gapInt.get(1)[0]); assertEquals("Gap interval 2 end wrong", 8, gapInt.get(1)[1]); + + BitSet gapfield = aseq.getInsertionsAsBits(); + BitSet expectedgaps = new BitSet(); + expectedgaps.set(2, 5); + expectedgaps.set(6, 9); + + assertEquals(6, expectedgaps.cardinality()); + + assertEquals("getInsertionsAsBits didn't mark expected number of gaps", + 6, gapfield.cardinality()); + + assertEquals("getInsertionsAsBits not correct.", expectedgaps, gapfield); } @Test(groups = ("Functional")) @@@ -226,152 -237,64 +239,152 @@@ @Test(groups = { "Functional" }) public void testFindIndex() { + /* + * call sequenceChanged() after each test to invalidate any cursor, + * forcing the 1-arg findIndex to be executed + */ SequenceI sq = new Sequence("test", "ABCDEF"); assertEquals(0, sq.findIndex(0)); + sq.sequenceChanged(); assertEquals(1, sq.findIndex(1)); + sq.sequenceChanged(); assertEquals(5, sq.findIndex(5)); + sq.sequenceChanged(); assertEquals(6, sq.findIndex(6)); + sq.sequenceChanged(); assertEquals(6, sq.findIndex(9)); sq = new Sequence("test/8-13", "-A--B-C-D-E-F--"); assertEquals(2, sq.findIndex(8)); + sq.sequenceChanged(); assertEquals(5, sq.findIndex(9)); + sq.sequenceChanged(); assertEquals(7, sq.findIndex(10)); // before start returns 0 + sq.sequenceChanged(); assertEquals(0, sq.findIndex(0)); + sq.sequenceChanged(); assertEquals(0, sq.findIndex(-1)); // beyond end returns last residue column + sq.sequenceChanged(); assertEquals(13, sq.findIndex(99)); } /** - * Tests for the method that returns a dataset sequence position (base 1) for + * Tests for the method that returns a dataset sequence position (start..) for * an aligned column position (base 0). */ @Test(groups = { "Functional" }) public void testFindPosition() { - SequenceI sq = new Sequence("test", "ABCDEF"); - assertEquals(1, sq.findPosition(0)); - assertEquals(6, sq.findPosition(5)); + /* + * call sequenceChanged() after each test to invalidate any cursor, + * forcing the 1-arg findPosition to be executed + */ + SequenceI sq = new Sequence("test/8-13", "ABCDEF"); + assertEquals(8, sq.findPosition(0)); + // Sequence should now hold a cursor at [8, 0] + SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + int token = (int) PA.getValue(sq, "changeCount"); + assertEquals(new SequenceCursor(sq, 8, 1, token), cursor); + + sq.sequenceChanged(); + + /* + * find F13 at column offset 5, cursor should update to [13, 6] + */ + 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(-1, seq.findPosition(6)); // fails - sq = new Sequence("test", "AB-C-D--"); - assertEquals(1, sq.findPosition(0)); - assertEquals(2, sq.findPosition(1)); + sq = new Sequence("test/8-11", "AB-C-D--"); + token = (int) PA.getValue(sq, "changeCount"); // 0 + assertEquals(8, sq.findPosition(0)); + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 8, 1, token), cursor); + + sq.sequenceChanged(); + assertEquals(9, sq.findPosition(1)); + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 9, 2, ++token), cursor); + + sq.sequenceChanged(); // gap position 'finds' residue to the right (not the left as per javadoc) - assertEquals(3, sq.findPosition(2)); - assertEquals(3, sq.findPosition(3)); - assertEquals(4, sq.findPosition(4)); - assertEquals(4, sq.findPosition(5)); + // cursor is set to the last residue position found [B 2] + assertEquals(10, sq.findPosition(2)); + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 9, 2, ++token), cursor); + + sq.sequenceChanged(); + assertEquals(10, sq.findPosition(3)); + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 10, 4, ++token), cursor); + + sq.sequenceChanged(); + // column[4] is the gap after C - returns D11 + // cursor is set to [C 4] + assertEquals(11, sq.findPosition(4)); + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 10, 4, ++token), cursor); + + sq.sequenceChanged(); + assertEquals(11, sq.findPosition(5)); // D + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 11, 6, ++token), cursor); + + sq.sequenceChanged(); // returns 1 more than sequence length if off the end ?!? - assertEquals(5, sq.findPosition(6)); - assertEquals(5, sq.findPosition(7)); + assertEquals(12, sq.findPosition(6)); - sq = new Sequence("test", "--AB-C-DEF--"); - assertEquals(1, sq.findPosition(0)); - assertEquals(1, sq.findPosition(1)); - assertEquals(1, sq.findPosition(2)); - assertEquals(2, sq.findPosition(3)); - assertEquals(3, sq.findPosition(4)); - assertEquals(3, sq.findPosition(5)); - assertEquals(4, sq.findPosition(6)); - assertEquals(4, sq.findPosition(7)); - assertEquals(5, sq.findPosition(8)); - assertEquals(6, sq.findPosition(9)); - assertEquals(7, sq.findPosition(10)); - assertEquals(7, sq.findPosition(11)); + sq.sequenceChanged(); + assertEquals(12, sq.findPosition(7)); + + sq = new Sequence("test/8-13", "--AB-C-DEF--"); + assertEquals(8, sq.findPosition(0)); + + sq.sequenceChanged(); + assertEquals(8, sq.findPosition(1)); + + sq.sequenceChanged(); + assertEquals(8, sq.findPosition(2)); + + sq.sequenceChanged(); + assertEquals(9, sq.findPosition(3)); + + sq.sequenceChanged(); + assertEquals(10, sq.findPosition(4)); + + sq.sequenceChanged(); + assertEquals(10, sq.findPosition(5)); + + sq.sequenceChanged(); + assertEquals(11, sq.findPosition(6)); + + sq.sequenceChanged(); + assertEquals(11, sq.findPosition(7)); + + sq.sequenceChanged(); + assertEquals(12, sq.findPosition(8)); + + sq.sequenceChanged(); + assertEquals(13, sq.findPosition(9)); + + sq.sequenceChanged(); + assertEquals(14, sq.findPosition(10)); + + /* + * findPosition for column beyond sequence length + * returns 1 more than last residue position + */ + sq.sequenceChanged(); + assertEquals(14, sq.findPosition(11)); + sq.sequenceChanged(); + assertEquals(14, sq.findPosition(99)); } @Test(groups = { "Functional" }) @@@ -1271,227 -1194,4 +1284,227 @@@ range = sq.findPositions(3, 7); // DE assertEquals(new Range(11, 12), range); } + + @Test(groups = { "Functional" }) + public void testFindIndex_withCursor() + { + Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--"); + + // find F given A + assertEquals(10, sq.findIndex(13, new SequenceCursor(sq, 8, 2, 0))); + + // find A given F + assertEquals(2, sq.findIndex(8, new SequenceCursor(sq, 13, 10, 0))); + + // find C given C + assertEquals(6, sq.findIndex(10, new SequenceCursor(sq, 10, 6, 0))); + } + + @Test(groups = { "Functional" }) + public void testFindPosition_withCursor() + { + Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--"); + + // find F pos given A + 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 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); + + // 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); + + // 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); + + // 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); + + /* + * 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); + + /* + * 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); + // now 'find' 99 from C + // cursor is set to [F 10] + 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); + } + + @Test + public void testIsValidCursor() + { + Sequence sq = new Sequence("Seq", "ABC--DE-F", 8, 13); + assertFalse(sq.isValidCursor(null)); + + /* + * cursor is valid if it has valid sequence ref and changeCount token + * and positions within the range of the sequence + */ + int changeCount = (int) PA.getValue(sq, "changeCount"); + SequenceCursor cursor = new SequenceCursor(sq, 13, 1, changeCount); + assertTrue(sq.isValidCursor(cursor)); + + /* + * column position outside [0 - length-1] is rejected + */ + cursor = new SequenceCursor(sq, 13, -1, changeCount); + assertFalse(sq.isValidCursor(cursor)); + cursor = new SequenceCursor(sq, 13, 9, changeCount); + assertFalse(sq.isValidCursor(cursor)); + cursor = new SequenceCursor(sq, 7, 8, changeCount); + assertFalse(sq.isValidCursor(cursor)); + cursor = new SequenceCursor(sq, 14, 2, changeCount); + assertFalse(sq.isValidCursor(cursor)); + + /* + * wrong sequence is rejected + */ + cursor = new SequenceCursor(null, 13, 1, changeCount); + assertFalse(sq.isValidCursor(cursor)); + cursor = new SequenceCursor(new Sequence("Seq", "abc"), 13, 1, + changeCount); + assertFalse(sq.isValidCursor(cursor)); + + /* + * wrong token value is rejected + */ + cursor = new SequenceCursor(sq, 13, 1, changeCount + 1); + assertFalse(sq.isValidCursor(cursor)); + cursor = new SequenceCursor(sq, 13, 1, changeCount - 1); + assertFalse(sq.isValidCursor(cursor)); + } + + @Test(groups = { "Functional" }) + public void testFindPosition_withCursorAndEdits() + { + Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--"); + + // find F pos given A + assertEquals(13, sq.findPosition(10, new SequenceCursor(sq, 8, 2, 0))); + int token = (int) PA.getValue(sq, "changeCount"); // 0 + SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 13, 10, token), cursor); + + /* + * setSequence should invalidate the cursor cached by the sequence + */ + sq.setSequence("-A-BCD-EF---"); // one gap removed + assertEquals(8, sq.getStart()); // sanity check + assertEquals(11, sq.findPosition(5)); // D11 + // cursor should now be at [D 6] + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 11, 6, ++token), cursor); + + /* + * deleteChars should invalidate the cached cursor + */ + sq.deleteChars(2, 5); // delete -BC + assertEquals("-AD-EF---", sq.getSequenceAsString()); + assertEquals(8, sq.getStart()); // sanity check + assertEquals(10, sq.findPosition(4)); // E10 + // cursor should now be at [E 5] + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 10, 5, ++token), cursor); + + /* + * Edit to insert gaps should invalidate the cached cursor + * insert 2 gaps at column[3] to make -AD---EF--- + */ + SequenceI[] seqs = new SequenceI[] { sq }; + AlignmentI al = new Alignment(seqs); + new EditCommand().appendEdit(Action.INSERT_GAP, seqs, 3, 2, al, true); + assertEquals("-AD---EF---", sq.getSequenceAsString()); + assertEquals(10, sq.findPosition(4)); // E10 + // cursor should now be at [D 3] + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 9, 3, ++token), cursor); + + /* + * insertCharAt should invalidate the cached cursor + * insert CC at column[4] to make -AD-CC--EF--- + */ + sq.insertCharAt(4, 2, 'C'); + assertEquals("-AD-CC--EF---", sq.getSequenceAsString()); + assertEquals(13, sq.findPosition(9)); // F13 + // cursor should now be at [F 10] + cursor = (SequenceCursor) PA.getValue(sq, "cursor"); + assertEquals(new SequenceCursor(sq, 13, 10, ++token), cursor); + } }