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