JAL-2759 Fix unintended type change
[jalview.git] / src / jalview / datamodel / Sequence.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.datamodel;
22
23 import jalview.analysis.AlignSeq;
24 import jalview.api.DBRefEntryI;
25 import jalview.datamodel.features.SequenceFeatures;
26 import jalview.datamodel.features.SequenceFeaturesI;
27 import jalview.util.Comparison;
28 import jalview.util.DBRefUtils;
29 import jalview.util.MapList;
30 import jalview.util.StringUtils;
31
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.BitSet;
35 import java.util.Collections;
36 import java.util.Enumeration;
37 import java.util.Iterator;
38 import java.util.List;
39 import java.util.ListIterator;
40 import java.util.Vector;
41
42 import fr.orsay.lri.varna.models.rna.RNA;
43
44 /**
45  * 
46  * Implements the SequenceI interface for a char[] based sequence object.
47  * 
48  * @author $author$
49  * @version $Revision$
50  */
51 public class Sequence extends ASequence implements SequenceI
52 {
53   SequenceI datasetSequence;
54
55   String name;
56
57   private char[] sequence;
58
59   String description;
60
61   int start;
62
63   int end;
64
65   Vector<PDBEntry> pdbIds;
66
67   String vamsasId;
68
69   DBRefEntry[] dbrefs;
70
71   RNA rna;
72
73   /**
74    * This annotation is displayed below the alignment but the positions are tied
75    * to the residues of this sequence
76    *
77    * TODO: change to List<>
78    */
79   Vector<AlignmentAnnotation> annotation;
80
81   private SequenceFeaturesI sequenceFeatureStore;
82
83   /*
84    * A cursor holding the approximate current view position to the sequence,
85    * as determined by findIndex or findPosition or findPositions.
86    * Using a cursor as a hint allows these methods to be more performant for
87    * large sequences.
88    */
89   private SequenceCursor cursor;
90
91   /*
92    * A number that should be incremented whenever the sequence is edited.
93    * If the value matches the cursor token, then we can trust the cursor,
94    * if not then it should be recomputed. 
95    */
96   private int changeCount;
97
98   /**
99    * Creates a new Sequence object.
100    * 
101    * @param name
102    *          display name string
103    * @param sequence
104    *          string to form a possibly gapped sequence out of
105    * @param start
106    *          first position of non-gap residue in the sequence
107    * @param end
108    *          last position of ungapped residues (nearly always only used for
109    *          display purposes)
110    */
111   public Sequence(String name, String sequence, int start, int end)
112   {
113     this();
114     initSeqAndName(name, sequence.toCharArray(), start, end);
115   }
116
117   public Sequence(String name, char[] sequence, int start, int end)
118   {
119     this();
120     initSeqAndName(name, sequence, start, end);
121   }
122
123   /**
124    * Stage 1 constructor - assign name, sequence, and set start and end fields.
125    * start and end are updated values from name2 if it ends with /start-end
126    * 
127    * @param name2
128    * @param sequence2
129    * @param start2
130    * @param end2
131    */
132   protected void initSeqAndName(String name2, char[] sequence2, int start2,
133           int end2)
134   {
135     this.name = name2;
136     this.sequence = sequence2;
137     this.start = start2;
138     this.end = end2;
139     parseId();
140     checkValidRange();
141   }
142
143   /**
144    * If 'name' ends in /i-j, where i >= j > 0 are integers, extracts i and j as
145    * start and end respectively and removes the suffix from the name
146    */
147   void parseId()
148   {
149     if (name == null)
150     {
151       System.err.println(
152               "POSSIBLE IMPLEMENTATION ERROR: null sequence name passed to constructor.");
153       name = "";
154     }
155     int slashPos = name.lastIndexOf('/');
156     if (slashPos > -1 && slashPos < name.length() - 1)
157     {
158       String suffix = name.substring(slashPos + 1);
159       String[] range = suffix.split("-");
160       if (range.length == 2)
161       {
162         try
163         {
164           int from = Integer.valueOf(range[0]);
165           int to = Integer.valueOf(range[1]);
166           if (from > 0 && to >= from)
167           {
168             name = name.substring(0, slashPos);
169             setStart(from);
170             setEnd(to);
171             checkValidRange();
172           }
173         } catch (NumberFormatException e)
174         {
175           // leave name unchanged if suffix is invalid
176         }
177       }
178     }
179   }
180
181   /**
182    * Ensures that 'end' is not before the end of the sequence, that is,
183    * (end-start+1) is at least as long as the count of ungapped positions. Note
184    * that end is permitted to be beyond the end of the sequence data.
185    */
186   void checkValidRange()
187   {
188     // Note: JAL-774 :
189     // http://issues.jalview.org/browse/JAL-774?focusedCommentId=11239&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-11239
190     {
191       int endRes = 0;
192       for (int j = 0; j < sequence.length; j++)
193       {
194         if (!Comparison.isGap(sequence[j]))
195         {
196           endRes++;
197         }
198       }
199       if (endRes > 0)
200       {
201         endRes += start - 1;
202       }
203
204       if (end < endRes)
205       {
206         end = endRes;
207       }
208     }
209
210   }
211
212   /**
213    * default constructor
214    */
215   private Sequence()
216   {
217     sequenceFeatureStore = new SequenceFeatures();
218   }
219
220   /**
221    * Creates a new Sequence object.
222    * 
223    * @param name
224    *          DOCUMENT ME!
225    * @param sequence
226    *          DOCUMENT ME!
227    */
228   public Sequence(String name, String sequence)
229   {
230     this(name, sequence, 1, -1);
231   }
232
233   /**
234    * Creates a new Sequence object with new AlignmentAnnotations but inherits
235    * any existing dataset sequence reference. If non exists, everything is
236    * copied.
237    * 
238    * @param seq
239    *          if seq is a dataset sequence, behaves like a plain old copy
240    *          constructor
241    */
242   public Sequence(SequenceI seq)
243   {
244     this(seq, seq.getAnnotation());
245   }
246
247   /**
248    * Create a new sequence object with new features, DBRefEntries, and PDBIds
249    * but inherits any existing dataset sequence reference, and duplicate of any
250    * annotation that is present in the given annotation array.
251    * 
252    * @param seq
253    *          the sequence to be copied
254    * @param alAnnotation
255    *          an array of annotation including some associated with seq
256    */
257   public Sequence(SequenceI seq, AlignmentAnnotation[] alAnnotation)
258   {
259     this();
260     initSeqFrom(seq, alAnnotation);
261   }
262
263   /**
264    * does the heavy lifting when cloning a dataset sequence, or coping data from
265    * dataset to a new derived sequence.
266    * 
267    * @param seq
268    *          - source of attributes.
269    * @param alAnnotation
270    *          - alignment annotation present on seq that should be copied onto
271    *          this sequence
272    */
273   protected void initSeqFrom(SequenceI seq,
274           AlignmentAnnotation[] alAnnotation)
275   {
276     char[] oseq = seq.getSequence(); // returns a copy of the array
277     initSeqAndName(seq.getName(), oseq, seq.getStart(), seq.getEnd());
278
279     description = seq.getDescription();
280     if (seq != datasetSequence)
281     {
282       setDatasetSequence(seq.getDatasetSequence());
283     }
284     
285     /*
286      * only copy DBRefs and seqfeatures if we really are a dataset sequence
287      */
288     if (datasetSequence == null)
289     {
290       if (seq.getDBRefs() != null)
291       {
292         DBRefEntry[] dbr = seq.getDBRefs();
293         for (int i = 0; i < dbr.length; i++)
294         {
295           addDBRef(new DBRefEntry(dbr[i]));
296         }
297       }
298
299       /*
300        * make copies of any sequence features
301        */
302       for (SequenceFeature sf : seq.getSequenceFeatures())
303       {
304         addSequenceFeature(new SequenceFeature(sf));
305       }
306     }
307
308     if (seq.getAnnotation() != null)
309     {
310       AlignmentAnnotation[] sqann = seq.getAnnotation();
311       for (int i = 0; i < sqann.length; i++)
312       {
313         if (sqann[i] == null)
314         {
315           continue;
316         }
317         boolean found = (alAnnotation == null);
318         if (!found)
319         {
320           for (int apos = 0; !found && apos < alAnnotation.length; apos++)
321           {
322             found = (alAnnotation[apos] == sqann[i]);
323           }
324         }
325         if (found)
326         {
327           // only copy the given annotation
328           AlignmentAnnotation newann = new AlignmentAnnotation(sqann[i]);
329           addAlignmentAnnotation(newann);
330         }
331       }
332     }
333     if (seq.getAllPDBEntries() != null)
334     {
335       Vector<PDBEntry> ids = seq.getAllPDBEntries();
336       for (PDBEntry pdb : ids)
337       {
338         this.addPDBId(new PDBEntry(pdb));
339       }
340     }
341   }
342
343   @Override
344   public void setSequenceFeatures(List<SequenceFeature> features)
345   {
346     if (datasetSequence != null)
347     {
348       datasetSequence.setSequenceFeatures(features);
349       return;
350     }
351     sequenceFeatureStore = new SequenceFeatures(features);
352   }
353
354   @Override
355   public synchronized boolean addSequenceFeature(SequenceFeature sf)
356   {
357     if (sf.getType() == null)
358     {
359       System.err.println("SequenceFeature type may not be null: "
360               + sf.toString());
361       return false;
362     }
363
364     if (datasetSequence != null)
365     {
366       return datasetSequence.addSequenceFeature(sf);
367     }
368
369     return sequenceFeatureStore.add(sf);
370   }
371
372   @Override
373   public void deleteFeature(SequenceFeature sf)
374   {
375     if (datasetSequence != null)
376     {
377       datasetSequence.deleteFeature(sf);
378     }
379     else
380     {
381       sequenceFeatureStore.delete(sf);
382     }
383   }
384
385   /**
386    * {@inheritDoc}
387    * 
388    * @return
389    */
390   @Override
391   public List<SequenceFeature> getSequenceFeatures()
392   {
393     if (datasetSequence != null)
394     {
395       return datasetSequence.getSequenceFeatures();
396     }
397     return sequenceFeatureStore.getAllFeatures();
398   }
399
400   @Override
401   public SequenceFeaturesI getFeatures()
402   {
403     return datasetSequence != null ? datasetSequence.getFeatures()
404             : sequenceFeatureStore;
405   }
406
407   @Override
408   public boolean addPDBId(PDBEntry entry)
409   {
410     if (pdbIds == null)
411     {
412       pdbIds = new Vector<>();
413       pdbIds.add(entry);
414       return true;
415     }
416
417     for (PDBEntry pdbe : pdbIds)
418     {
419       if (pdbe.updateFrom(entry))
420       {
421         return false;
422       }
423     }
424     pdbIds.addElement(entry);
425     return true;
426   }
427
428   /**
429    * DOCUMENT ME!
430    * 
431    * @param id
432    *          DOCUMENT ME!
433    */
434   @Override
435   public void setPDBId(Vector<PDBEntry> id)
436   {
437     pdbIds = id;
438   }
439
440   /**
441    * DOCUMENT ME!
442    * 
443    * @return DOCUMENT ME!
444    */
445   @Override
446   public Vector<PDBEntry> getAllPDBEntries()
447   {
448     return pdbIds == null ? new Vector<PDBEntry>() : pdbIds;
449   }
450
451   /**
452    * DOCUMENT ME!
453    * 
454    * @return DOCUMENT ME!
455    */
456   @Override
457   public String getDisplayId(boolean jvsuffix)
458   {
459     StringBuffer result = new StringBuffer(name);
460     if (jvsuffix)
461     {
462       result.append("/" + start + "-" + end);
463     }
464
465     return result.toString();
466   }
467
468   /**
469    * Sets the sequence name. If the name ends in /start-end, then the start-end
470    * values are parsed out and set, and the suffix is removed from the name.
471    * 
472    * @param theName
473    */
474   @Override
475   public void setName(String theName)
476   {
477     this.name = theName;
478     this.parseId();
479   }
480
481   /**
482    * DOCUMENT ME!
483    * 
484    * @return DOCUMENT ME!
485    */
486   @Override
487   public String getName()
488   {
489     return this.name;
490   }
491
492   /**
493    * DOCUMENT ME!
494    * 
495    * @param start
496    *          DOCUMENT ME!
497    */
498   @Override
499   public void setStart(int start)
500   {
501     this.start = start;
502   }
503
504   /**
505    * DOCUMENT ME!
506    * 
507    * @return DOCUMENT ME!
508    */
509   @Override
510   public int getStart()
511   {
512     return this.start;
513   }
514
515   /**
516    * DOCUMENT ME!
517    * 
518    * @param end
519    *          DOCUMENT ME!
520    */
521   @Override
522   public void setEnd(int end)
523   {
524     this.end = end;
525   }
526
527   /**
528    * DOCUMENT ME!
529    * 
530    * @return DOCUMENT ME!
531    */
532   @Override
533   public int getEnd()
534   {
535     return this.end;
536   }
537
538   /**
539    * DOCUMENT ME!
540    * 
541    * @return DOCUMENT ME!
542    */
543   @Override
544   public int getLength()
545   {
546     return this.sequence.length;
547   }
548
549   /**
550    * DOCUMENT ME!
551    * 
552    * @param seq
553    *          DOCUMENT ME!
554    */
555   @Override
556   public void setSequence(String seq)
557   {
558     this.sequence = seq.toCharArray();
559     checkValidRange();
560     sequenceChanged();
561   }
562
563   @Override
564   public String getSequenceAsString()
565   {
566     return new String(sequence);
567   }
568
569   @Override
570   public String getSequenceAsString(int start, int end)
571   {
572     return new String(getSequence(start, end));
573   }
574
575   @Override
576   public char[] getSequence()
577   {
578     // return sequence;
579     return sequence == null ? null : Arrays.copyOf(sequence,
580             sequence.length);
581   }
582
583   /*
584    * (non-Javadoc)
585    * 
586    * @see jalview.datamodel.SequenceI#getSequence(int, int)
587    */
588   @Override
589   public char[] getSequence(int start, int end)
590   {
591     if (start < 0)
592     {
593       start = 0;
594     }
595     // JBPNote - left to user to pad the result here (TODO:Decide on this
596     // policy)
597     if (start >= sequence.length)
598     {
599       return new char[0];
600     }
601
602     if (end >= sequence.length)
603     {
604       end = sequence.length;
605     }
606
607     char[] reply = new char[end - start];
608     System.arraycopy(sequence, start, reply, 0, end - start);
609
610     return reply;
611   }
612
613   @Override
614   public SequenceI getSubSequence(int start, int end)
615   {
616     if (start < 0)
617     {
618       start = 0;
619     }
620     char[] seq = getSequence(start, end);
621     if (seq.length == 0)
622     {
623       return null;
624     }
625     int nstart = findPosition(start);
626     int nend = findPosition(end) - 1;
627     // JBPNote - this is an incomplete copy.
628     SequenceI nseq = new Sequence(this.getName(), seq, nstart, nend);
629     nseq.setDescription(description);
630     if (datasetSequence != null)
631     {
632       nseq.setDatasetSequence(datasetSequence);
633     }
634     else
635     {
636       nseq.setDatasetSequence(this);
637     }
638     return nseq;
639   }
640
641   /**
642    * Returns the character of the aligned sequence at the given position (base
643    * zero), or space if the position is not within the sequence's bounds
644    * 
645    * @return
646    */
647   @Override
648   public char getCharAt(int i)
649   {
650     if (i >= 0 && i < sequence.length)
651     {
652       return sequence[i];
653     }
654     else
655     {
656       return ' ';
657     }
658   }
659
660   /**
661    * DOCUMENT ME!
662    * 
663    * @param desc
664    *          DOCUMENT ME!
665    */
666   @Override
667   public void setDescription(String desc)
668   {
669     this.description = desc;
670   }
671
672   /**
673    * DOCUMENT ME!
674    * 
675    * @return DOCUMENT ME!
676    */
677   @Override
678   public String getDescription()
679   {
680     return this.description;
681   }
682
683   /**
684    * {@inheritDoc}
685    */
686   @Override
687   public int findIndex(int pos)
688   {
689     /*
690      * use a valid, hopefully nearby, cursor if available
691      */
692     if (isValidCursor(cursor))
693     {
694       return findIndex(pos, cursor);
695     }
696
697     int j = start;
698     int i = 0;
699     int startColumn = 0;
700
701     /*
702      * traverse sequence from the start counting gaps; make a note of
703      * the column of the first residue to save in the cursor
704      */
705     while ((i < sequence.length) && (j <= end) && (j <= pos))
706     {
707       if (!Comparison.isGap(sequence[i]))
708       {
709         if (j == start)
710         {
711           startColumn = i;
712         }
713         j++;
714       }
715       i++;
716     }
717
718     if (j == end && j < pos)
719     {
720       return end + 1;
721     }
722
723     updateCursor(pos, i, startColumn);
724     return i;
725   }
726
727   /**
728    * Updates the cursor to the latest found residue and column position
729    * 
730    * @param residuePos
731    *          (start..)
732    * @param column
733    *          (1..)
734    * @param startColumn
735    *          column position of the first sequence residue
736    */
737   protected void updateCursor(int residuePos, int column, int startColumn)
738   {
739     /*
740      * preserve end residue column provided cursor was valid
741      */
742     int endColumn = isValidCursor(cursor) ? cursor.lastColumnPosition : 0;
743     if (residuePos == this.end)
744     {
745       endColumn = column;
746     }
747
748     cursor = new SequenceCursor(this, residuePos, column, startColumn,
749             endColumn, this.changeCount);
750   }
751
752   /**
753    * Answers the aligned column position (1..) for the given residue position
754    * (start..) given a 'hint' of a residue/column location in the neighbourhood.
755    * The hint may be left of, at, or to the right of the required position.
756    * 
757    * @param pos
758    * @param curs
759    * @return
760    */
761   protected int findIndex(int pos, SequenceCursor curs)
762   {
763     if (!isValidCursor(curs))
764     {
765       /*
766        * wrong or invalidated cursor, compute de novo
767        */
768       return findIndex(pos);
769     }
770
771     if (curs.residuePosition == pos)
772     {
773       return curs.columnPosition;
774     }
775
776     /*
777      * move left or right to find pos from hint.position
778      */
779     int col = curs.columnPosition - 1; // convert from base 1 to 0-based array
780                                        // index
781     int newPos = curs.residuePosition;
782     int delta = newPos > pos ? -1 : 1;
783
784     while (newPos != pos)
785     {
786       col += delta; // shift one column left or right
787       if (col < 0 || col == sequence.length)
788       {
789         break;
790       }
791       if (!Comparison.isGap(sequence[col]))
792       {
793         newPos += delta;
794       }
795     }
796
797     col++; // convert back to base 1
798     updateCursor(pos, col, curs.firstColumnPosition);
799
800     return col;
801   }
802
803   /**
804    * {@inheritDoc}
805    */
806   @Override
807   public int findPosition(final int column)
808   {
809     /*
810      * use a valid, hopefully nearby, cursor if available
811      */
812     if (isValidCursor(cursor))
813     {
814       return findPosition(column + 1, cursor);
815     }
816     
817     // TODO recode this more naturally i.e. count residues only
818     // as they are found, not 'in anticipation'
819
820     /*
821      * traverse the sequence counting gaps; note the column position
822      * of the first residue, to save in the cursor
823      */
824     int firstResidueColumn = 0;
825     int lastPosFound = 0;
826     int lastPosFoundColumn = 0;
827     int seqlen = sequence.length;
828
829     if (seqlen > 0 && !Comparison.isGap(sequence[0]))
830     {
831       lastPosFound = start;
832       lastPosFoundColumn = 0;
833     }
834
835     int j = 0;
836     int pos = start;
837
838     while (j < column && j < seqlen)
839     {
840       if (!Comparison.isGap(sequence[j]))
841       {
842         lastPosFound = pos;
843         lastPosFoundColumn = j;
844         if (pos == this.start)
845         {
846           firstResidueColumn = j;
847         }
848         pos++;
849       }
850       j++;
851     }
852     if (j < seqlen && !Comparison.isGap(sequence[j]))
853     {
854       lastPosFound = pos;
855       lastPosFoundColumn = j;
856       if (pos == this.start)
857       {
858         firstResidueColumn = j;
859       }
860     }
861
862     /*
863      * update the cursor to the last residue position found (if any)
864      * (converting column position to base 1)
865      */
866     if (lastPosFound != 0)
867     {
868       updateCursor(lastPosFound, lastPosFoundColumn + 1,
869               firstResidueColumn + 1);
870     }
871
872     return pos;
873   }
874
875   /**
876    * Answers true if the given cursor is not null, is for this sequence object,
877    * and has a token value that matches this object's changeCount, else false.
878    * This allows us to ignore a cursor as 'stale' if the sequence has been
879    * modified since the cursor was created.
880    * 
881    * @param curs
882    * @return
883    */
884   protected boolean isValidCursor(SequenceCursor curs)
885   {
886     if (curs == null || curs.sequence != this || curs.token != changeCount)
887     {
888       return false;
889     }
890     /*
891      * sanity check against range
892      */
893     if (curs.columnPosition < 0 || curs.columnPosition > sequence.length)
894     {
895       return false;
896     }
897     if (curs.residuePosition < start || curs.residuePosition > end)
898     {
899       return false;
900     }
901     return true;
902   }
903
904   /**
905    * Answers the sequence position (start..) for the given aligned column
906    * position (1..), given a hint of a cursor in the neighbourhood. The cursor
907    * may lie left of, at, or to the right of the column position.
908    * 
909    * @param col
910    * @param curs
911    * @return
912    */
913   protected int findPosition(final int col, SequenceCursor curs)
914   {
915     if (!isValidCursor(curs))
916     {
917       /*
918        * wrong or invalidated cursor, compute de novo
919        */
920       return findPosition(col - 1);// ugh back to base 0
921     }
922
923     if (curs.columnPosition == col)
924     {
925       cursor = curs; // in case this method becomes public
926       return curs.residuePosition; // easy case :-)
927     }
928
929     if (curs.lastColumnPosition > 0 && curs.lastColumnPosition < col)
930     {
931       /*
932        * sequence lies entirely to the left of col
933        * - return last residue + 1
934        */
935       return end + 1;
936     }
937
938     if (curs.firstColumnPosition > 0 && curs.firstColumnPosition > col)
939     {
940       /*
941        * sequence lies entirely to the right of col
942        * - return first residue
943        */
944       return start;
945     }
946
947     // todo could choose closest to col out of column,
948     // firstColumnPosition, lastColumnPosition as a start point
949
950     /*
951      * move left or right to find pos from cursor position
952      */
953     int firstResidueColumn = curs.firstColumnPosition;
954     int column = curs.columnPosition - 1; // to base 0
955     int newPos = curs.residuePosition;
956     int delta = curs.columnPosition > col ? -1 : 1;
957     boolean gapped = false;
958     int lastFoundPosition = curs.residuePosition;
959     int lastFoundPositionColumn = curs.columnPosition;
960
961     while (column != col - 1)
962     {
963       column += delta; // shift one column left or right
964       if (column < 0 || column == sequence.length)
965       {
966         break;
967       }
968       gapped = Comparison.isGap(sequence[column]);
969       if (!gapped)
970       {
971         newPos += delta;
972         lastFoundPosition = newPos;
973         lastFoundPositionColumn = column + 1;
974         if (lastFoundPosition == this.start)
975         {
976           firstResidueColumn = column + 1;
977         }
978       }
979     }
980
981     if (cursor == null || lastFoundPosition != cursor.residuePosition)
982     {
983       updateCursor(lastFoundPosition, lastFoundPositionColumn,
984               firstResidueColumn);
985     }
986
987     /*
988      * hack to give position to the right if on a gap
989      * or beyond the length of the sequence (see JAL-2562)
990      */
991     if (delta > 0 && (gapped || column >= sequence.length))
992     {
993       newPos++;
994     }
995
996     return newPos;
997   }
998
999   /**
1000    * {@inheritDoc}
1001    */
1002   @Override
1003   public Range findPositions(int fromColumn, int toColumn)
1004   {
1005     if (toColumn < fromColumn || fromColumn < 1)
1006     {
1007       return null;
1008     }
1009
1010     /*
1011      * find the first non-gapped position, if any
1012      */
1013     int firstPosition = 0;
1014     int col = fromColumn - 1;
1015     int length = sequence.length;
1016     while (col < length && col < toColumn)
1017     {
1018       if (!Comparison.isGap(sequence[col]))
1019       {
1020         firstPosition = findPosition(col++);
1021         break;
1022       }
1023       col++;
1024     }
1025
1026     if (firstPosition == 0)
1027     {
1028       return null;
1029     }
1030
1031     /*
1032      * find the last non-gapped position
1033      */
1034     int lastPosition = firstPosition;
1035     while (col < length && col < toColumn)
1036     {
1037       if (!Comparison.isGap(sequence[col++]))
1038       {
1039         lastPosition++;
1040       }
1041     }
1042
1043     return new Range(firstPosition, lastPosition);
1044   }
1045
1046   /**
1047    * Returns an int array where indices correspond to each residue in the
1048    * sequence and the element value gives its position in the alignment
1049    * 
1050    * @return int[SequenceI.getEnd()-SequenceI.getStart()+1] or null if no
1051    *         residues in SequenceI object
1052    */
1053   @Override
1054   public int[] gapMap()
1055   {
1056     String seq = jalview.analysis.AlignSeq.extractGaps(
1057             jalview.util.Comparison.GapChars, new String(sequence));
1058     int[] map = new int[seq.length()];
1059     int j = 0;
1060     int p = 0;
1061
1062     while (j < sequence.length)
1063     {
1064       if (!jalview.util.Comparison.isGap(sequence[j]))
1065       {
1066         map[p++] = j;
1067       }
1068
1069       j++;
1070     }
1071
1072     return map;
1073   }
1074
1075   /**
1076    * Build a bitset corresponding to sequence gaps
1077    * 
1078    * @return a BitSet where set values correspond to gaps in the sequence
1079    */
1080   @Override
1081   public BitSet gapBitset()
1082   {
1083     BitSet gaps = new BitSet(sequence.length);
1084     int j = 0;
1085     while (j < sequence.length)
1086     {
1087       if (jalview.util.Comparison.isGap(sequence[j]))
1088       {
1089         gaps.set(j);
1090       }
1091       j++;
1092     }
1093     return gaps;
1094   }
1095
1096   @Override
1097   public int[] findPositionMap()
1098   {
1099     int map[] = new int[sequence.length];
1100     int j = 0;
1101     int pos = start;
1102     int seqlen = sequence.length;
1103     while ((j < seqlen))
1104     {
1105       map[j] = pos;
1106       if (!jalview.util.Comparison.isGap(sequence[j]))
1107       {
1108         pos++;
1109       }
1110
1111       j++;
1112     }
1113     return map;
1114   }
1115
1116   @Override
1117   public List<int[]> getInsertions()
1118   {
1119     ArrayList<int[]> map = new ArrayList<>();
1120     int lastj = -1, j = 0;
1121     int pos = start;
1122     int seqlen = sequence.length;
1123     while ((j < seqlen))
1124     {
1125       if (jalview.util.Comparison.isGap(sequence[j]))
1126       {
1127         if (lastj == -1)
1128         {
1129           lastj = j;
1130         }
1131       }
1132       else
1133       {
1134         if (lastj != -1)
1135         {
1136           map.add(new int[] { lastj, j - 1 });
1137           lastj = -1;
1138         }
1139       }
1140       j++;
1141     }
1142     if (lastj != -1)
1143     {
1144       map.add(new int[] { lastj, j - 1 });
1145       lastj = -1;
1146     }
1147     return map;
1148   }
1149
1150   @Override
1151   public BitSet getInsertionsAsBits()
1152   {
1153     BitSet map = new BitSet();
1154     int lastj = -1, j = 0;
1155     int pos = start;
1156     int seqlen = sequence.length;
1157     while ((j < seqlen))
1158     {
1159       if (jalview.util.Comparison.isGap(sequence[j]))
1160       {
1161         if (lastj == -1)
1162         {
1163           lastj = j;
1164         }
1165       }
1166       else
1167       {
1168         if (lastj != -1)
1169         {
1170           map.set(lastj, j);
1171           lastj = -1;
1172         }
1173       }
1174       j++;
1175     }
1176     if (lastj != -1)
1177     {
1178       map.set(lastj, j);
1179       lastj = -1;
1180     }
1181     return map;
1182   }
1183
1184   @Override
1185   public void deleteChars(final int i, final int j)
1186   {
1187     int newstart = start, newend = end;
1188     if (i >= sequence.length || i < 0)
1189     {
1190       return;
1191     }
1192
1193     char[] tmp = StringUtils.deleteChars(sequence, i, j);
1194     boolean createNewDs = false;
1195     // TODO: take a (second look) at the dataset creation validation method for
1196     // the very large sequence case
1197     int startIndex = findIndex(start) - 1;
1198     int endIndex = findIndex(end) - 1;
1199     int startDeleteColumn = -1; // for dataset sequence deletions
1200     int deleteCount = 0;
1201
1202     for (int s = i; s < j; s++)
1203     {
1204       if (Comparison.isGap(sequence[s]))
1205       {
1206         continue;
1207       }
1208       deleteCount++;
1209       if (startDeleteColumn == -1)
1210       {
1211         startDeleteColumn = findPosition(s) - start;
1212       }
1213       if (createNewDs)
1214       {
1215         newend--;
1216       }
1217       else
1218       {
1219         if (startIndex == s)
1220         {
1221           /*
1222            * deleting characters from start of sequence; new start is the
1223            * sequence position of the next column (position to the right
1224            * if the column position is gapped)
1225            */
1226           newstart = findPosition(j);
1227           break;
1228         }
1229         else
1230         {
1231           if (endIndex < j)
1232           {
1233             /*
1234              * deleting characters at end of sequence; new end is the sequence
1235              * position of the column before the deletion; subtract 1 if this is
1236              * gapped since findPosition returns the next sequence position
1237              */
1238             newend = findPosition(i - 1);
1239             if (Comparison.isGap(sequence[i - 1]))
1240             {
1241               newend--;
1242             }
1243             break;
1244           }
1245           else
1246           {
1247             createNewDs = true;
1248             newend--;
1249           }
1250         }
1251       }
1252     }
1253
1254     if (createNewDs && this.datasetSequence != null)
1255     {
1256       /*
1257        * if deletion occured in the middle of the sequence,
1258        * construct a new dataset sequence and delete the residues
1259        * that were deleted from the aligned sequence
1260        */
1261       Sequence ds = new Sequence(datasetSequence);
1262       ds.deleteChars(startDeleteColumn, startDeleteColumn + deleteCount);
1263       datasetSequence = ds;
1264       // TODO: remove any non-inheritable properties ?
1265       // TODO: create a sequence mapping (since there is a relation here ?)
1266     }
1267     start = newstart;
1268     end = newend;
1269     sequence = tmp;
1270     sequenceChanged();
1271   }
1272
1273   @Override
1274   public void insertCharAt(int i, int length, char c)
1275   {
1276     char[] tmp = new char[sequence.length + length];
1277
1278     if (i >= sequence.length)
1279     {
1280       System.arraycopy(sequence, 0, tmp, 0, sequence.length);
1281       i = sequence.length;
1282     }
1283     else
1284     {
1285       System.arraycopy(sequence, 0, tmp, 0, i);
1286     }
1287
1288     int index = i;
1289     while (length > 0)
1290     {
1291       tmp[index++] = c;
1292       length--;
1293     }
1294
1295     if (i < sequence.length)
1296     {
1297       System.arraycopy(sequence, i, tmp, index, sequence.length - i);
1298     }
1299
1300     sequence = tmp;
1301     sequenceChanged();
1302   }
1303
1304   @Override
1305   public void insertCharAt(int i, char c)
1306   {
1307     insertCharAt(i, 1, c);
1308   }
1309
1310   @Override
1311   public String getVamsasId()
1312   {
1313     return vamsasId;
1314   }
1315
1316   @Override
1317   public void setVamsasId(String id)
1318   {
1319     vamsasId = id;
1320   }
1321
1322   @Override
1323   public void setDBRefs(DBRefEntry[] dbref)
1324   {
1325     if (dbrefs == null && datasetSequence != null
1326             && this != datasetSequence)
1327     {
1328       datasetSequence.setDBRefs(dbref);
1329       return;
1330     }
1331     dbrefs = dbref;
1332     if (dbrefs != null)
1333     {
1334       DBRefUtils.ensurePrimaries(this);
1335     }
1336   }
1337
1338   @Override
1339   public DBRefEntry[] getDBRefs()
1340   {
1341     if (dbrefs == null && datasetSequence != null
1342             && this != datasetSequence)
1343     {
1344       return datasetSequence.getDBRefs();
1345     }
1346     return dbrefs;
1347   }
1348
1349   @Override
1350   public void addDBRef(DBRefEntry entry)
1351   {
1352     if (datasetSequence != null)
1353     {
1354       datasetSequence.addDBRef(entry);
1355       return;
1356     }
1357
1358     if (dbrefs == null)
1359     {
1360       dbrefs = new DBRefEntry[0];
1361     }
1362
1363     for (DBRefEntryI dbr : dbrefs)
1364     {
1365       if (dbr.updateFrom(entry))
1366       {
1367         /*
1368          * found a dbref that either matched, or could be
1369          * updated from, the new entry - no need to add it
1370          */
1371         return;
1372       }
1373     }
1374
1375     /*
1376      * extend the array to make room for one more
1377      */
1378     // TODO use an ArrayList instead
1379     int j = dbrefs.length;
1380     DBRefEntry[] temp = new DBRefEntry[j + 1];
1381     System.arraycopy(dbrefs, 0, temp, 0, j);
1382     temp[temp.length - 1] = entry;
1383
1384     dbrefs = temp;
1385
1386     DBRefUtils.ensurePrimaries(this);
1387   }
1388
1389   @Override
1390   public void setDatasetSequence(SequenceI seq)
1391   {
1392     if (seq == this)
1393     {
1394       throw new IllegalArgumentException(
1395               "Implementation Error: self reference passed to SequenceI.setDatasetSequence");
1396     }
1397     if (seq != null && seq.getDatasetSequence() != null)
1398     {
1399       throw new IllegalArgumentException(
1400               "Implementation error: cascading dataset sequences are not allowed.");
1401     }
1402     datasetSequence = seq;
1403   }
1404
1405   @Override
1406   public SequenceI getDatasetSequence()
1407   {
1408     return datasetSequence;
1409   }
1410
1411   @Override
1412   public AlignmentAnnotation[] getAnnotation()
1413   {
1414     return annotation == null ? null
1415             : annotation
1416                     .toArray(new AlignmentAnnotation[annotation.size()]);
1417   }
1418
1419   @Override
1420   public boolean hasAnnotation(AlignmentAnnotation ann)
1421   {
1422     return annotation == null ? false : annotation.contains(ann);
1423   }
1424
1425   @Override
1426   public void addAlignmentAnnotation(AlignmentAnnotation annotation)
1427   {
1428     if (this.annotation == null)
1429     {
1430       this.annotation = new Vector<>();
1431     }
1432     if (!this.annotation.contains(annotation))
1433     {
1434       this.annotation.addElement(annotation);
1435     }
1436     annotation.setSequenceRef(this);
1437   }
1438
1439   @Override
1440   public void removeAlignmentAnnotation(AlignmentAnnotation annotation)
1441   {
1442     if (this.annotation != null)
1443     {
1444       this.annotation.removeElement(annotation);
1445       if (this.annotation.size() == 0)
1446       {
1447         this.annotation = null;
1448       }
1449     }
1450   }
1451
1452   /**
1453    * test if this is a valid candidate for another sequence's dataset sequence.
1454    * 
1455    */
1456   private boolean isValidDatasetSequence()
1457   {
1458     if (datasetSequence != null)
1459     {
1460       return false;
1461     }
1462     for (int i = 0; i < sequence.length; i++)
1463     {
1464       if (jalview.util.Comparison.isGap(sequence[i]))
1465       {
1466         return false;
1467       }
1468     }
1469     return true;
1470   }
1471
1472   @Override
1473   public SequenceI deriveSequence()
1474   {
1475     Sequence seq = null;
1476     if (datasetSequence == null)
1477     {
1478       if (isValidDatasetSequence())
1479       {
1480         // Use this as dataset sequence
1481         seq = new Sequence(getName(), "", 1, -1);
1482         seq.setDatasetSequence(this);
1483         seq.initSeqFrom(this, getAnnotation());
1484         return seq;
1485       }
1486       else
1487       {
1488         // Create a new, valid dataset sequence
1489         createDatasetSequence();
1490       }
1491     }
1492     return new Sequence(this);
1493   }
1494
1495   private boolean _isNa;
1496
1497   private int _seqhash = 0;
1498
1499   /**
1500    * Answers false if the sequence is more than 85% nucleotide (ACGTU), else
1501    * true
1502    */
1503   @Override
1504   public boolean isProtein()
1505   {
1506     if (datasetSequence != null)
1507     {
1508       return datasetSequence.isProtein();
1509     }
1510     if (_seqhash != sequence.hashCode())
1511     {
1512       _seqhash = sequence.hashCode();
1513       _isNa = Comparison.isNucleotide(this);
1514     }
1515     return !_isNa;
1516   };
1517
1518   /*
1519    * (non-Javadoc)
1520    * 
1521    * @see jalview.datamodel.SequenceI#createDatasetSequence()
1522    */
1523   @Override
1524   public SequenceI createDatasetSequence()
1525   {
1526     if (datasetSequence == null)
1527     {
1528       Sequence dsseq = new Sequence(getName(),
1529               AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
1530                       getSequenceAsString()),
1531               getStart(), getEnd());
1532
1533       datasetSequence = dsseq;
1534
1535       dsseq.setDescription(description);
1536       // move features and database references onto dataset sequence
1537       dsseq.sequenceFeatureStore = sequenceFeatureStore;
1538       sequenceFeatureStore = null;
1539       dsseq.dbrefs = dbrefs;
1540       dbrefs = null;
1541       // TODO: search and replace any references to this sequence with
1542       // references to the dataset sequence in Mappings on dbref
1543       dsseq.pdbIds = pdbIds;
1544       pdbIds = null;
1545       datasetSequence.updatePDBIds();
1546       if (annotation != null)
1547       {
1548         // annotation is cloned rather than moved, to preserve what's currently
1549         // on the alignment
1550         for (AlignmentAnnotation aa : annotation)
1551         {
1552           AlignmentAnnotation _aa = new AlignmentAnnotation(aa);
1553           _aa.sequenceRef = datasetSequence;
1554           _aa.adjustForAlignment(); // uses annotation's own record of
1555                                     // sequence-column mapping
1556           datasetSequence.addAlignmentAnnotation(_aa);
1557         }
1558       }
1559     }
1560     return datasetSequence;
1561   }
1562
1563   /*
1564    * (non-Javadoc)
1565    * 
1566    * @see
1567    * jalview.datamodel.SequenceI#setAlignmentAnnotation(AlignmmentAnnotation[]
1568    * annotations)
1569    */
1570   @Override
1571   public void setAlignmentAnnotation(AlignmentAnnotation[] annotations)
1572   {
1573     if (annotation != null)
1574     {
1575       annotation.removeAllElements();
1576     }
1577     if (annotations != null)
1578     {
1579       for (int i = 0; i < annotations.length; i++)
1580       {
1581         if (annotations[i] != null)
1582         {
1583           addAlignmentAnnotation(annotations[i]);
1584         }
1585       }
1586     }
1587   }
1588
1589   @Override
1590   public AlignmentAnnotation[] getAnnotation(String label)
1591   {
1592     if (annotation == null || annotation.size() == 0)
1593     {
1594       return null;
1595     }
1596
1597     Vector<AlignmentAnnotation> subset = new Vector<>();
1598     Enumeration<AlignmentAnnotation> e = annotation.elements();
1599     while (e.hasMoreElements())
1600     {
1601       AlignmentAnnotation ann = e.nextElement();
1602       if (ann.label != null && ann.label.equals(label))
1603       {
1604         subset.addElement(ann);
1605       }
1606     }
1607     if (subset.size() == 0)
1608     {
1609       return null;
1610     }
1611     AlignmentAnnotation[] anns = new AlignmentAnnotation[subset.size()];
1612     int i = 0;
1613     e = subset.elements();
1614     while (e.hasMoreElements())
1615     {
1616       anns[i++] = e.nextElement();
1617     }
1618     subset.removeAllElements();
1619     return anns;
1620   }
1621
1622   @Override
1623   public boolean updatePDBIds()
1624   {
1625     if (datasetSequence != null)
1626     {
1627       // TODO: could merge DBRefs
1628       return datasetSequence.updatePDBIds();
1629     }
1630     if (dbrefs == null || dbrefs.length == 0)
1631     {
1632       return false;
1633     }
1634     boolean added = false;
1635     for (DBRefEntry dbr : dbrefs)
1636     {
1637       if (DBRefSource.PDB.equals(dbr.getSource()))
1638       {
1639         /*
1640          * 'Add' any PDB dbrefs as a PDBEntry - add is only performed if the
1641          * PDB id is not already present in a 'matching' PDBEntry
1642          * Constructor parses out a chain code if appended to the accession id
1643          * (a fudge used to 'store' the chain code in the DBRef)
1644          */
1645         PDBEntry pdbe = new PDBEntry(dbr);
1646         added |= addPDBId(pdbe);
1647       }
1648     }
1649     return added;
1650   }
1651
1652   @Override
1653   public void transferAnnotation(SequenceI entry, Mapping mp)
1654   {
1655     if (datasetSequence != null)
1656     {
1657       datasetSequence.transferAnnotation(entry, mp);
1658       return;
1659     }
1660     if (entry.getDatasetSequence() != null)
1661     {
1662       transferAnnotation(entry.getDatasetSequence(), mp);
1663       return;
1664     }
1665     // transfer any new features from entry onto sequence
1666     if (entry.getSequenceFeatures() != null)
1667     {
1668
1669       List<SequenceFeature> sfs = entry.getSequenceFeatures();
1670       for (SequenceFeature feature : sfs)
1671       {
1672        SequenceFeature sf[] = (mp != null) ? mp.locateFeature(feature)
1673                 : new SequenceFeature[] { new SequenceFeature(feature) };
1674         if (sf != null)
1675         {
1676           for (int sfi = 0; sfi < sf.length; sfi++)
1677           {
1678             addSequenceFeature(sf[sfi]);
1679           }
1680         }
1681       }
1682     }
1683
1684     // transfer PDB entries
1685     if (entry.getAllPDBEntries() != null)
1686     {
1687       Enumeration<PDBEntry> e = entry.getAllPDBEntries().elements();
1688       while (e.hasMoreElements())
1689       {
1690         PDBEntry pdb = e.nextElement();
1691         addPDBId(pdb);
1692       }
1693     }
1694     // transfer database references
1695     DBRefEntry[] entryRefs = entry.getDBRefs();
1696     if (entryRefs != null)
1697     {
1698       for (int r = 0; r < entryRefs.length; r++)
1699       {
1700         DBRefEntry newref = new DBRefEntry(entryRefs[r]);
1701         if (newref.getMap() != null && mp != null)
1702         {
1703           // remap ref using our local mapping
1704         }
1705         // we also assume all version string setting is done by dbSourceProxy
1706         /*
1707          * if (!newref.getSource().equalsIgnoreCase(dbSource)) {
1708          * newref.setSource(dbSource); }
1709          */
1710         addDBRef(newref);
1711       }
1712     }
1713   }
1714
1715   @Override
1716   public void setRNA(RNA r)
1717   {
1718     rna = r;
1719   }
1720
1721   @Override
1722   public RNA getRNA()
1723   {
1724     return rna;
1725   }
1726
1727   @Override
1728   public List<AlignmentAnnotation> getAlignmentAnnotations(String calcId,
1729           String label)
1730   {
1731     List<AlignmentAnnotation> result = new ArrayList<>();
1732     if (this.annotation != null)
1733     {
1734       for (AlignmentAnnotation ann : annotation)
1735       {
1736         if (ann.calcId != null && ann.calcId.equals(calcId)
1737                 && ann.label != null && ann.label.equals(label))
1738         {
1739           result.add(ann);
1740         }
1741       }
1742     }
1743     return result;
1744   }
1745
1746   @Override
1747   public String toString()
1748   {
1749     return getDisplayId(false);
1750   }
1751
1752   @Override
1753   public PDBEntry getPDBEntry(String pdbIdStr)
1754   {
1755     if (getDatasetSequence() != null)
1756     {
1757       return getDatasetSequence().getPDBEntry(pdbIdStr);
1758     }
1759     if (pdbIds == null)
1760     {
1761       return null;
1762     }
1763     List<PDBEntry> entries = getAllPDBEntries();
1764     for (PDBEntry entry : entries)
1765     {
1766       if (entry.getId().equalsIgnoreCase(pdbIdStr))
1767       {
1768         return entry;
1769       }
1770     }
1771     return null;
1772   }
1773
1774   @Override
1775   public List<DBRefEntry> getPrimaryDBRefs()
1776   {
1777     if (datasetSequence != null)
1778     {
1779       return datasetSequence.getPrimaryDBRefs();
1780     }
1781     if (dbrefs == null || dbrefs.length == 0)
1782     {
1783       return Collections.emptyList();
1784     }
1785     synchronized (dbrefs)
1786     {
1787       List<DBRefEntry> primaries = new ArrayList<>();
1788       DBRefEntry[] tmp = new DBRefEntry[1];
1789       for (DBRefEntry ref : dbrefs)
1790       {
1791         if (!ref.isPrimaryCandidate())
1792         {
1793           continue;
1794         }
1795         if (ref.hasMap())
1796         {
1797           MapList mp = ref.getMap().getMap();
1798           if (mp.getFromLowest() > start || mp.getFromHighest() < end)
1799           {
1800             // map only involves a subsequence, so cannot be primary
1801             continue;
1802           }
1803         }
1804         // whilst it looks like it is a primary ref, we also sanity check type
1805         if (DBRefUtils.getCanonicalName(DBRefSource.PDB)
1806                 .equals(DBRefUtils.getCanonicalName(ref.getSource())))
1807         {
1808           // PDB dbrefs imply there should be a PDBEntry associated
1809           // TODO: tighten PDB dbrefs
1810           // formally imply Jalview has actually downloaded and
1811           // parsed the pdb file. That means there should be a cached file
1812           // handle on the PDBEntry, and a real mapping between sequence and
1813           // extracted sequence from PDB file
1814           PDBEntry pdbentry = getPDBEntry(ref.getAccessionId());
1815           if (pdbentry != null && pdbentry.getFile() != null)
1816           {
1817             primaries.add(ref);
1818           }
1819           continue;
1820         }
1821         // check standard protein or dna sources
1822         tmp[0] = ref;
1823         DBRefEntry[] res = DBRefUtils.selectDbRefs(!isProtein(), tmp);
1824         if (res != null && res[0] == tmp[0])
1825         {
1826           primaries.add(ref);
1827           continue;
1828         }
1829       }
1830       return primaries;
1831     }
1832   }
1833
1834   /**
1835    * {@inheritDoc}
1836    */
1837   @Override
1838   public List<SequenceFeature> findFeatures(int fromColumn, int toColumn,
1839           String... types)
1840   {
1841     int startPos = findPosition(fromColumn - 1); // convert base 1 to base 0
1842     int endPos = fromColumn == toColumn ? startPos
1843             : findPosition(toColumn - 1);
1844
1845     List<SequenceFeature> result = getFeatures().findFeatures(startPos,
1846             endPos, types);
1847
1848     /*
1849      * if end column is gapped, endPos may be to the right, 
1850      * and we may have included adjacent or enclosing features;
1851      * remove any that are not enclosing, non-contact features
1852      */
1853     boolean endColumnIsGapped = toColumn > 0 && toColumn <= sequence.length
1854             && Comparison.isGap(sequence[toColumn - 1]);
1855     if (endPos > this.end || endColumnIsGapped)
1856     {
1857       ListIterator<SequenceFeature> it = result.listIterator();
1858       while (it.hasNext())
1859       {
1860         SequenceFeature sf = it.next();
1861         int sfBegin = sf.getBegin();
1862         int sfEnd = sf.getEnd();
1863         int featureStartColumn = findIndex(sfBegin);
1864         if (featureStartColumn > toColumn)
1865         {
1866           it.remove();
1867         }
1868         else if (featureStartColumn < fromColumn)
1869         {
1870           int featureEndColumn = sfEnd == sfBegin ? featureStartColumn
1871                   : findIndex(sfEnd);
1872           if (featureEndColumn < fromColumn)
1873           {
1874             it.remove();
1875           }
1876           else if (featureEndColumn > toColumn && sf.isContactFeature())
1877           {
1878             /*
1879              * remove an enclosing feature if it is a contact feature
1880              */
1881             it.remove();
1882           }
1883         }
1884       }
1885     }
1886
1887     return result;
1888   }
1889
1890   /**
1891    * Invalidates any stale cursors (forcing recalculation) by incrementing the
1892    * token that has to match the one presented by the cursor
1893    */
1894   @Override
1895   public void sequenceChanged()
1896   {
1897     changeCount++;
1898   }
1899
1900   /**
1901    * {@inheritDoc}
1902    */
1903   @Override
1904   public int replace(char c1, char c2)
1905   {
1906     if (c1 == c2)
1907     {
1908       return 0;
1909     }
1910     int count = 0;
1911     synchronized (sequence)
1912     {
1913       for (int c = 0; c < sequence.length; c++)
1914       {
1915         if (sequence[c] == c1)
1916         {
1917           sequence[c] = c2;
1918           count++;
1919         }
1920       }
1921     }
1922     if (count > 0)
1923     {
1924       sequenceChanged();
1925     }
1926
1927     return count;
1928   }
1929
1930   @Override
1931   public String getSequenceStringFromIterator(Iterator<int[]> it)
1932   {
1933     StringBuilder newSequence = new StringBuilder();
1934     while (it.hasNext())
1935     {
1936       int[] block = it.next();
1937       if (it.hasNext())
1938       {
1939         newSequence.append(getSequence(block[0], block[1] + 1));
1940       }
1941       else
1942       {
1943         newSequence.append(getSequence(block[0], block[1]));
1944       }
1945     }
1946
1947     return newSequence.toString();
1948   }
1949
1950   @Override
1951   public int firstResidueOutsideIterator(Iterator<int[]> regions)
1952   {
1953     int start = 0;
1954
1955     if (!regions.hasNext())
1956     {
1957       return findIndex(getStart()) - 1;
1958     }
1959
1960     // Simply walk along the sequence whilst watching for region
1961     // boundaries
1962     int hideStart = getLength();
1963     int hideEnd = -1;
1964     boolean foundStart = false;
1965
1966     // step through the non-gapped positions of the sequence
1967     for (int i = getStart(); i <= getEnd() && (!foundStart); i++)
1968     {
1969       // get alignment position of this residue in the sequence
1970       int p = findIndex(i) - 1;
1971
1972       // update region start/end
1973       while (hideEnd < p && regions.hasNext())
1974       {
1975         int[] region = regions.next();
1976         hideStart = region[0];
1977         hideEnd = region[1];
1978       }
1979       if (hideEnd < p)
1980       {
1981         hideStart = getLength();
1982       }
1983       // update boundary for sequence
1984       if (p < hideStart)
1985       {
1986         start = p;
1987         foundStart = true;
1988       }
1989     }
1990
1991     if (foundStart)
1992     {
1993       return start;
1994     }
1995     // otherwise, sequence was completely hidden
1996     return 0;
1997   }
1998 }