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/?p=jalview.git;a=commitdiff_plain;h=88694463a2aea303694231603b61970f72a5a259;hp=f41d78dbf890fbb7597f75019d05841312fa4f52 Merge branch 'features/JAL-2446NCList' into features/JAL-2526sequenceCursor Conflicts: src/jalview/datamodel/SequenceI.java --- diff --git a/src/jalview/commands/EditCommand.java b/src/jalview/commands/EditCommand.java index bba0dfb..9eaeb7a 100644 --- a/src/jalview/commands/EditCommand.java +++ b/src/jalview/commands/EditCommand.java @@ -798,6 +798,8 @@ public class EditCommand implements CommandI AlignmentAnnotation[] tmp; for (int s = 0; s < command.seqs.length; s++) { + command.seqs[s].sequenceChanged(); + if (modifyVisibility) { // Rows are only removed or added to sequence object. diff --git a/src/jalview/datamodel/Sequence.java b/src/jalview/datamodel/Sequence.java index 324d21b..ab6639a 100755 --- a/src/jalview/datamodel/Sequence.java +++ b/src/jalview/datamodel/Sequence.java @@ -90,6 +90,21 @@ public class Sequence extends ASequence implements SequenceI 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. * @@ -529,6 +544,7 @@ public class Sequence extends ASequence implements SequenceI { this.sequence = seq.toCharArray(); checkValidRange(); + sequenceChanged(); } @Override @@ -649,15 +665,20 @@ public class Sequence extends ASequence implements SequenceI 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. @@ -667,45 +688,248 @@ public class Sequence extends ASequence implements SequenceI { 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 */ @@ -726,7 +950,6 @@ public class Sequence extends ASequence implements SequenceI */ int firstPos = 0; int lastPos = 0; - int firstPosCol = 0; boolean foundFirst = false; while (j <= toCol && j < seqlen) @@ -737,7 +960,6 @@ public class Sequence extends ASequence implements SequenceI if (!foundFirst) { firstPos = count; - firstPosCol = j; foundFirst = true; } lastPos = count; @@ -763,6 +985,104 @@ public class Sequence extends ASequence implements SequenceI } /** + * 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 * @@ -952,6 +1272,7 @@ public class Sequence extends ASequence implements SequenceI start = newstart; end = newend; sequence = tmp; + sequenceChanged(); } @Override @@ -982,6 +1303,7 @@ public class Sequence extends ASequence implements SequenceI } sequence = tmp; + sequenceChanged(); } @Override @@ -1549,4 +1871,14 @@ public class Sequence extends ASequence implements SequenceI } 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 --git a/src/jalview/datamodel/SequenceCursor.java b/src/jalview/datamodel/SequenceCursor.java new file mode 100644 index 0000000..f439ee1 --- /dev/null +++ b/src/jalview/datamodel/SequenceCursor.java @@ -0,0 +1,86 @@ +package jalview.datamodel; + +/** + * An immutable object representing one or more residue and corresponding + * alignment column positions for a sequence + */ +public class SequenceCursor +{ + /** + * the aligned sequence this cursor applies to + */ + public final SequenceI sequence; + + /** + * residue position in sequence (start...), 0 if undefined + */ + public final int residuePosition; + + /** + * column position (1...) corresponding to residuePosition, or 0 if undefined + */ + public final int columnPosition; + + /** + * a token which may be used to check whether this cursor is still valid for + * its sequence (allowing it to be ignored if the sequence has changed) + */ + public final int token; + + /** + * Constructor + * + * @param seq + * sequence this cursor applies to + * @param resPos + * residue position in sequence (start..) + * @param column + * column position in alignment (1..) + * @param tok + * a token that may be validated by the sequence to check the cursor + * is not stale + */ + public SequenceCursor(SequenceI seq, int resPos, int column, int tok) + { + sequence = seq; + residuePosition = resPos; + columnPosition = column; + token = tok; + } + + @Override + public int hashCode() + { + int hash = 31 * residuePosition; + hash = 31 * hash + columnPosition; + hash = 31 * hash + token; + if (sequence != null) + { + hash += sequence.hashCode(); + } + return hash; + } + + /** + * Two cursors are equal if they refer to the same sequence object and have + * the same residue position, column position and token value + */ + @Override + public boolean equals(Object obj) + { + if (!(obj instanceof SequenceCursor)) + { + return false; + } + SequenceCursor sc = (SequenceCursor) obj; + return sequence == sc.sequence && residuePosition == sc.residuePosition + && columnPosition == sc.columnPosition && token == sc.token; + } + + @Override + public String toString() + { + return (sequence == null ? "" : sequence.getName()) + ":Pos" + + residuePosition + ":Col" + columnPosition + ":tok" + token; + } +} diff --git a/src/jalview/datamodel/SequenceI.java b/src/jalview/datamodel/SequenceI.java index cf9d6ad..38be37f 100755 --- a/src/jalview/datamodel/SequenceI.java +++ b/src/jalview/datamodel/SequenceI.java @@ -521,6 +521,13 @@ public interface SequenceI extends ASequenceI * @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(); /** * diff --git a/test/jalview/datamodel/SequenceTest.java b/test/jalview/datamodel/SequenceTest.java index 4e8efcd..c5850dc 100644 --- a/test/jalview/datamodel/SequenceTest.java +++ b/test/jalview/datamodel/SequenceTest.java @@ -28,6 +28,8 @@ import static org.testng.AssertJUnit.assertNull; 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; @@ -237,64 +239,152 @@ public class SequenceTest @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" }) @@ -1194,4 +1284,227 @@ public class SequenceTest 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); + } } diff --git a/test/jalview/schemes/AnnotationColourGradientTest.java b/test/jalview/schemes/AnnotationColourGradientTest.java index 1c93856..b7a5164 100644 --- a/test/jalview/schemes/AnnotationColourGradientTest.java +++ b/test/jalview/schemes/AnnotationColourGradientTest.java @@ -49,7 +49,7 @@ public class AnnotationColourGradientTest anns[col] = new Annotation("a", "a", 'a', col, colour); } - seq = new Sequence("", ""); + seq = new Sequence("Seq", ""); al = new Alignment(new SequenceI[]{ seq}); /*