X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjalview%2Fdatamodel%2FSequence.java;h=96747e4fba3a1c36dd4b227c7fe67eeb25a7b83b;hb=ff450fad8709ae81919af7a15ea382af7292794c;hp=0055d0e605b71e889530ed3fee929e1e8b867cf5;hpb=4c6cd08af580ff73cfce39bec34ae4658a4b4b34;p=jalview.git diff --git a/src/jalview/datamodel/Sequence.java b/src/jalview/datamodel/Sequence.java index 0055d0e..96747e4 100755 --- a/src/jalview/datamodel/Sequence.java +++ b/src/jalview/datamodel/Sequence.java @@ -31,6 +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; @@ -90,6 +91,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. * @@ -244,9 +260,8 @@ public class Sequence extends ASequence implements SequenceI protected void initSeqFrom(SequenceI seq, AlignmentAnnotation[] alAnnotation) { - char[] oseq = seq.getSequence(); - initSeqAndName(seq.getName(), Arrays.copyOf(oseq, oseq.length), - seq.getStart(), seq.getEnd()); + char[] oseq = seq.getSequence(); // returns a copy of the array + initSeqAndName(seq.getName(), oseq, seq.getStart(), seq.getEnd()); description = seq.getDescription(); if (seq != datasetSequence) @@ -529,6 +544,7 @@ public class Sequence extends ASequence implements SequenceI { this.sequence = seq.toCharArray(); checkValidRange(); + sequenceChanged(); } @Override @@ -546,7 +562,9 @@ public class Sequence extends ASequence implements SequenceI @Override public char[] getSequence() { - return sequence; + // return sequence; + return sequence == null ? null : Arrays.copyOf(sequence, + sequence.length); } /* @@ -649,58 +667,320 @@ 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. + int startColumn = 0; + + /* + * traverse sequence from the start counting gaps; make a note of + * the column of the first residue to save in the cursor + */ while ((i < sequence.length) && (j <= end) && (j <= pos)) { if (!Comparison.isGap(sequence[i])) { + if (j == start) + { + startColumn = i; + } j++; } - i++; } - if ((j == end) && (j < pos)) + if (j == end && j < pos) { return end + 1; } - else + + updateCursor(pos, i, startColumn); + return i; + } + + /** + * Updates the cursor to the latest found residue and column position + * + * @param residuePos + * (start..) + * @param column + * (1..) + * @param startColumn + * column position of the first sequence residue + */ + protected void updateCursor(int residuePos, int column, int startColumn) + { + int endColumn = cursor == null ? 0 : cursor.lastColumnPosition; + if (residuePos == this.end) { - return i; + endColumn = column; } + + cursor = new SequenceCursor(this, residuePos, column, startColumn, + endColumn, 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) + { + 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, curs.firstColumnPosition); + + 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' + + /* + * traverse the sequence counting gaps; note the column position + * of the first residue, to save in the cursor + */ + int firstResidueColumn = 0; + 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; + if (pos == this.start) + { + firstResidueColumn = j; + } pos++; } - j++; } + if (j < seqlen && !Comparison.isGap(sequence[j])) + { + lastPosFound = pos; + lastPosFoundColumn = j; + if (pos == this.start) + { + firstResidueColumn = 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, + firstResidueColumn + 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 :-) + } + + if (curs.lastColumnPosition > 0 && curs.lastColumnPosition < col) + { + /* + * sequence lies entirely to the left of col + * - return last residue + 1 + */ + return end + 1; + } + + if (curs.firstColumnPosition > 0 && curs.firstColumnPosition > col) + { + /* + * sequence lies entirely to the right of col + * - return first residue + */ + return start; + } + + // todo could choose closest to col out of column, + // firstColumnPosition, lastColumnPosition as a start point + + /* + * move left or right to find pos from cursor position + */ + int firstResidueColumn = curs.firstColumnPosition; + 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 (lastFoundPosition == this.start) + { + firstResidueColumn = column + 1; + } + } + } + + if (cursor == null || lastFoundPosition != cursor.residuePosition) + { + updateCursor(lastFoundPosition, lastFoundPositionColumn, + firstResidueColumn); + } + + /* + * 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; + } + + /** * Returns an int array where indices correspond to each residue in the * sequence and the element value gives its position in the alignment * @@ -784,6 +1064,40 @@ public class Sequence extends ASequence implements SequenceI } @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; @@ -856,6 +1170,7 @@ public class Sequence extends ASequence implements SequenceI start = newstart; end = newend; sequence = tmp; + sequenceChanged(); } @Override @@ -886,6 +1201,7 @@ public class Sequence extends ASequence implements SequenceI } sequence = tmp; + sequenceChanged(); } @Override @@ -1080,7 +1396,7 @@ public class Sequence extends ASequence implements SequenceI private boolean _isNa; - private long _seqhash = 0; + private int _seqhash = 0; /** * Answers false if the sequence is more than 85% nucleotide (ACGTU), else @@ -1449,7 +1765,12 @@ public class Sequence extends ASequence implements SequenceI { int startPos = findPosition(fromColumn - 1); // convert base 1 to base 0 int endPos = findPosition(toColumn - 1); - + // to trace / debug behaviour: + // System.out + // .println(String + // .format("%s.findFeatures columns [%d-%d] positions [%d-%d] leaves cursor %s", + // getName(), fromColumn, toColumn, startPos, + // endPos, cursor)); List result = new ArrayList<>(); if (datasetSequence != null) { @@ -1495,4 +1816,44 @@ public class Sequence extends ASequence implements SequenceI return result; } + + /** + * 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++; + } + + /** + * {@inheritDoc} + */ + @Override + public int replace(char c1, char c2) + { + if (c1 == c2) + { + return 0; + } + int count = 0; + synchronized (sequence) + { + for (int c = 0; c < sequence.length; c++) + { + if (sequence[c] == c1) + { + sequence[c] = c2; + count++; + } + } + } + if (count > 0) + { + sequenceChanged(); + } + + return count; + } }