Merge branch 'releases/Release_2_10_4_Branch' into develop
[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    * Sets the sequence description, and also parses out any special formats of
662    * interest
663    * 
664    * @param desc
665    */
666   @Override
667   public void setDescription(String desc)
668   {
669     this.description = desc;
670   }
671
672   @Override
673   public void setGeneLoci(String speciesId, String assemblyId,
674           String chromosomeId, MapList map)
675   {
676     addDBRef(new DBRefEntry(speciesId, assemblyId, DBRefEntry.CHROMOSOME
677             + ":" + chromosomeId, new Mapping(map)));
678   }
679
680   /**
681    * Returns the gene loci mapping for the sequence (may be null)
682    * 
683    * @return
684    */
685   @Override
686   public GeneLociI getGeneLoci()
687   {
688     DBRefEntry[] refs = getDBRefs();
689     if (refs != null)
690     {
691       for (final DBRefEntry ref : refs)
692       {
693         if (ref.isChromosome())
694         {
695           return new GeneLociI()
696           {
697             @Override
698             public String getSpeciesId()
699             {
700               return ref.getSource();
701             }
702
703             @Override
704             public String getAssemblyId()
705             {
706               return ref.getVersion();
707             }
708
709             @Override
710             public String getChromosomeId()
711             {
712               // strip off "chromosome:" prefix to chrId
713               return ref.getAccessionId().substring(
714                       DBRefEntry.CHROMOSOME.length() + 1);
715             }
716
717             @Override
718             public MapList getMap()
719             {
720               return ref.getMap().getMap();
721             }
722           };
723         }
724       }
725     }
726     return null;
727   }
728
729   /**
730    * Answers the description
731    * 
732    * @return
733    */
734   @Override
735   public String getDescription()
736   {
737     return this.description;
738   }
739
740   /**
741    * {@inheritDoc}
742    */
743   @Override
744   public int findIndex(int pos)
745   {
746     /*
747      * use a valid, hopefully nearby, cursor if available
748      */
749     if (isValidCursor(cursor))
750     {
751       return findIndex(pos, cursor);
752     }
753
754     int j = start;
755     int i = 0;
756     int startColumn = 0;
757
758     /*
759      * traverse sequence from the start counting gaps; make a note of
760      * the column of the first residue to save in the cursor
761      */
762     while ((i < sequence.length) && (j <= end) && (j <= pos))
763     {
764       if (!Comparison.isGap(sequence[i]))
765       {
766         if (j == start)
767         {
768           startColumn = i;
769         }
770         j++;
771       }
772       i++;
773     }
774
775     if (j == end && j < pos)
776     {
777       return end + 1;
778     }
779
780     updateCursor(pos, i, startColumn);
781     return i;
782   }
783
784   /**
785    * Updates the cursor to the latest found residue and column position
786    * 
787    * @param residuePos
788    *          (start..)
789    * @param column
790    *          (1..)
791    * @param startColumn
792    *          column position of the first sequence residue
793    */
794   protected void updateCursor(int residuePos, int column, int startColumn)
795   {
796     /*
797      * preserve end residue column provided cursor was valid
798      */
799     int endColumn = isValidCursor(cursor) ? cursor.lastColumnPosition : 0;
800     if (residuePos == this.end)
801     {
802       endColumn = column;
803     }
804
805     cursor = new SequenceCursor(this, residuePos, column, startColumn,
806             endColumn, this.changeCount);
807   }
808
809   /**
810    * Answers the aligned column position (1..) for the given residue position
811    * (start..) given a 'hint' of a residue/column location in the neighbourhood.
812    * The hint may be left of, at, or to the right of the required position.
813    * 
814    * @param pos
815    * @param curs
816    * @return
817    */
818   protected int findIndex(int pos, SequenceCursor curs)
819   {
820     if (!isValidCursor(curs))
821     {
822       /*
823        * wrong or invalidated cursor, compute de novo
824        */
825       return findIndex(pos);
826     }
827
828     if (curs.residuePosition == pos)
829     {
830       return curs.columnPosition;
831     }
832
833     /*
834      * move left or right to find pos from hint.position
835      */
836     int col = curs.columnPosition - 1; // convert from base 1 to 0-based array
837                                        // index
838     int newPos = curs.residuePosition;
839     int delta = newPos > pos ? -1 : 1;
840
841     while (newPos != pos)
842     {
843       col += delta; // shift one column left or right
844       if (col < 0 || col == sequence.length)
845       {
846         break;
847       }
848       if (!Comparison.isGap(sequence[col]))
849       {
850         newPos += delta;
851       }
852     }
853
854     col++; // convert back to base 1
855     updateCursor(pos, col, curs.firstColumnPosition);
856
857     return col;
858   }
859
860   /**
861    * {@inheritDoc}
862    */
863   @Override
864   public int findPosition(final int column)
865   {
866     /*
867      * use a valid, hopefully nearby, cursor if available
868      */
869     if (isValidCursor(cursor))
870     {
871       return findPosition(column + 1, cursor);
872     }
873     
874     // TODO recode this more naturally i.e. count residues only
875     // as they are found, not 'in anticipation'
876
877     /*
878      * traverse the sequence counting gaps; note the column position
879      * of the first residue, to save in the cursor
880      */
881     int firstResidueColumn = 0;
882     int lastPosFound = 0;
883     int lastPosFoundColumn = 0;
884     int seqlen = sequence.length;
885
886     if (seqlen > 0 && !Comparison.isGap(sequence[0]))
887     {
888       lastPosFound = start;
889       lastPosFoundColumn = 0;
890     }
891
892     int j = 0;
893     int pos = start;
894
895     while (j < column && j < seqlen)
896     {
897       if (!Comparison.isGap(sequence[j]))
898       {
899         lastPosFound = pos;
900         lastPosFoundColumn = j;
901         if (pos == this.start)
902         {
903           firstResidueColumn = j;
904         }
905         pos++;
906       }
907       j++;
908     }
909     if (j < seqlen && !Comparison.isGap(sequence[j]))
910     {
911       lastPosFound = pos;
912       lastPosFoundColumn = j;
913       if (pos == this.start)
914       {
915         firstResidueColumn = j;
916       }
917     }
918
919     /*
920      * update the cursor to the last residue position found (if any)
921      * (converting column position to base 1)
922      */
923     if (lastPosFound != 0)
924     {
925       updateCursor(lastPosFound, lastPosFoundColumn + 1,
926               firstResidueColumn + 1);
927     }
928
929     return pos;
930   }
931
932   /**
933    * Answers true if the given cursor is not null, is for this sequence object,
934    * and has a token value that matches this object's changeCount, else false.
935    * This allows us to ignore a cursor as 'stale' if the sequence has been
936    * modified since the cursor was created.
937    * 
938    * @param curs
939    * @return
940    */
941   protected boolean isValidCursor(SequenceCursor curs)
942   {
943     if (curs == null || curs.sequence != this || curs.token != changeCount)
944     {
945       return false;
946     }
947     /*
948      * sanity check against range
949      */
950     if (curs.columnPosition < 0 || curs.columnPosition > sequence.length)
951     {
952       return false;
953     }
954     if (curs.residuePosition < start || curs.residuePosition > end)
955     {
956       return false;
957     }
958     return true;
959   }
960
961   /**
962    * Answers the sequence position (start..) for the given aligned column
963    * position (1..), given a hint of a cursor in the neighbourhood. The cursor
964    * may lie left of, at, or to the right of the column position.
965    * 
966    * @param col
967    * @param curs
968    * @return
969    */
970   protected int findPosition(final int col, SequenceCursor curs)
971   {
972     if (!isValidCursor(curs))
973     {
974       /*
975        * wrong or invalidated cursor, compute de novo
976        */
977       return findPosition(col - 1);// ugh back to base 0
978     }
979
980     if (curs.columnPosition == col)
981     {
982       cursor = curs; // in case this method becomes public
983       return curs.residuePosition; // easy case :-)
984     }
985
986     if (curs.lastColumnPosition > 0 && curs.lastColumnPosition < col)
987     {
988       /*
989        * sequence lies entirely to the left of col
990        * - return last residue + 1
991        */
992       return end + 1;
993     }
994
995     if (curs.firstColumnPosition > 0 && curs.firstColumnPosition > col)
996     {
997       /*
998        * sequence lies entirely to the right of col
999        * - return first residue
1000        */
1001       return start;
1002     }
1003
1004     // todo could choose closest to col out of column,
1005     // firstColumnPosition, lastColumnPosition as a start point
1006
1007     /*
1008      * move left or right to find pos from cursor position
1009      */
1010     int firstResidueColumn = curs.firstColumnPosition;
1011     int column = curs.columnPosition - 1; // to base 0
1012     int newPos = curs.residuePosition;
1013     int delta = curs.columnPosition > col ? -1 : 1;
1014     boolean gapped = false;
1015     int lastFoundPosition = curs.residuePosition;
1016     int lastFoundPositionColumn = curs.columnPosition;
1017
1018     while (column != col - 1)
1019     {
1020       column += delta; // shift one column left or right
1021       if (column < 0 || column == sequence.length)
1022       {
1023         break;
1024       }
1025       gapped = Comparison.isGap(sequence[column]);
1026       if (!gapped)
1027       {
1028         newPos += delta;
1029         lastFoundPosition = newPos;
1030         lastFoundPositionColumn = column + 1;
1031         if (lastFoundPosition == this.start)
1032         {
1033           firstResidueColumn = column + 1;
1034         }
1035       }
1036     }
1037
1038     if (cursor == null || lastFoundPosition != cursor.residuePosition)
1039     {
1040       updateCursor(lastFoundPosition, lastFoundPositionColumn,
1041               firstResidueColumn);
1042     }
1043
1044     /*
1045      * hack to give position to the right if on a gap
1046      * or beyond the length of the sequence (see JAL-2562)
1047      */
1048     if (delta > 0 && (gapped || column >= sequence.length))
1049     {
1050       newPos++;
1051     }
1052
1053     return newPos;
1054   }
1055
1056   /**
1057    * {@inheritDoc}
1058    */
1059   @Override
1060   public Range findPositions(int fromColumn, int toColumn)
1061   {
1062     if (toColumn < fromColumn || fromColumn < 1)
1063     {
1064       return null;
1065     }
1066
1067     /*
1068      * find the first non-gapped position, if any
1069      */
1070     int firstPosition = 0;
1071     int col = fromColumn - 1;
1072     int length = sequence.length;
1073     while (col < length && col < toColumn)
1074     {
1075       if (!Comparison.isGap(sequence[col]))
1076       {
1077         firstPosition = findPosition(col++);
1078         break;
1079       }
1080       col++;
1081     }
1082
1083     if (firstPosition == 0)
1084     {
1085       return null;
1086     }
1087
1088     /*
1089      * find the last non-gapped position
1090      */
1091     int lastPosition = firstPosition;
1092     while (col < length && col < toColumn)
1093     {
1094       if (!Comparison.isGap(sequence[col++]))
1095       {
1096         lastPosition++;
1097       }
1098     }
1099
1100     return new Range(firstPosition, lastPosition);
1101   }
1102
1103   /**
1104    * Returns an int array where indices correspond to each residue in the
1105    * sequence and the element value gives its position in the alignment
1106    * 
1107    * @return int[SequenceI.getEnd()-SequenceI.getStart()+1] or null if no
1108    *         residues in SequenceI object
1109    */
1110   @Override
1111   public int[] gapMap()
1112   {
1113     String seq = jalview.analysis.AlignSeq.extractGaps(
1114             jalview.util.Comparison.GapChars, new String(sequence));
1115     int[] map = new int[seq.length()];
1116     int j = 0;
1117     int p = 0;
1118
1119     while (j < sequence.length)
1120     {
1121       if (!jalview.util.Comparison.isGap(sequence[j]))
1122       {
1123         map[p++] = j;
1124       }
1125
1126       j++;
1127     }
1128
1129     return map;
1130   }
1131
1132   /**
1133    * Build a bitset corresponding to sequence gaps
1134    * 
1135    * @return a BitSet where set values correspond to gaps in the sequence
1136    */
1137   @Override
1138   public BitSet gapBitset()
1139   {
1140     BitSet gaps = new BitSet(sequence.length);
1141     int j = 0;
1142     while (j < sequence.length)
1143     {
1144       if (jalview.util.Comparison.isGap(sequence[j]))
1145       {
1146         gaps.set(j);
1147       }
1148       j++;
1149     }
1150     return gaps;
1151   }
1152
1153   @Override
1154   public int[] findPositionMap()
1155   {
1156     int map[] = new int[sequence.length];
1157     int j = 0;
1158     int pos = start;
1159     int seqlen = sequence.length;
1160     while ((j < seqlen))
1161     {
1162       map[j] = pos;
1163       if (!jalview.util.Comparison.isGap(sequence[j]))
1164       {
1165         pos++;
1166       }
1167
1168       j++;
1169     }
1170     return map;
1171   }
1172
1173   @Override
1174   public List<int[]> getInsertions()
1175   {
1176     ArrayList<int[]> map = new ArrayList<>();
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.add(new int[] { lastj, j - 1 });
1194           lastj = -1;
1195         }
1196       }
1197       j++;
1198     }
1199     if (lastj != -1)
1200     {
1201       map.add(new int[] { lastj, j - 1 });
1202       lastj = -1;
1203     }
1204     return map;
1205   }
1206
1207   @Override
1208   public BitSet getInsertionsAsBits()
1209   {
1210     BitSet map = new BitSet();
1211     int lastj = -1, j = 0;
1212     int pos = start;
1213     int seqlen = sequence.length;
1214     while ((j < seqlen))
1215     {
1216       if (jalview.util.Comparison.isGap(sequence[j]))
1217       {
1218         if (lastj == -1)
1219         {
1220           lastj = j;
1221         }
1222       }
1223       else
1224       {
1225         if (lastj != -1)
1226         {
1227           map.set(lastj, j);
1228           lastj = -1;
1229         }
1230       }
1231       j++;
1232     }
1233     if (lastj != -1)
1234     {
1235       map.set(lastj, j);
1236       lastj = -1;
1237     }
1238     return map;
1239   }
1240
1241   @Override
1242   public void deleteChars(final int i, final int j)
1243   {
1244     int newstart = start, newend = end;
1245     if (i >= sequence.length || i < 0)
1246     {
1247       return;
1248     }
1249
1250     char[] tmp = StringUtils.deleteChars(sequence, i, j);
1251     boolean createNewDs = false;
1252     // TODO: take a (second look) at the dataset creation validation method for
1253     // the very large sequence case
1254     int startIndex = findIndex(start) - 1;
1255     int endIndex = findIndex(end) - 1;
1256     int startDeleteColumn = -1; // for dataset sequence deletions
1257     int deleteCount = 0;
1258
1259     for (int s = i; s < j; s++)
1260     {
1261       if (Comparison.isGap(sequence[s]))
1262       {
1263         continue;
1264       }
1265       deleteCount++;
1266       if (startDeleteColumn == -1)
1267       {
1268         startDeleteColumn = findPosition(s) - start;
1269       }
1270       if (createNewDs)
1271       {
1272         newend--;
1273       }
1274       else
1275       {
1276         if (startIndex == s)
1277         {
1278           /*
1279            * deleting characters from start of sequence; new start is the
1280            * sequence position of the next column (position to the right
1281            * if the column position is gapped)
1282            */
1283           newstart = findPosition(j);
1284           break;
1285         }
1286         else
1287         {
1288           if (endIndex < j)
1289           {
1290             /*
1291              * deleting characters at end of sequence; new end is the sequence
1292              * position of the column before the deletion; subtract 1 if this is
1293              * gapped since findPosition returns the next sequence position
1294              */
1295             newend = findPosition(i - 1);
1296             if (Comparison.isGap(sequence[i - 1]))
1297             {
1298               newend--;
1299             }
1300             break;
1301           }
1302           else
1303           {
1304             createNewDs = true;
1305             newend--;
1306           }
1307         }
1308       }
1309     }
1310
1311     if (createNewDs && this.datasetSequence != null)
1312     {
1313       /*
1314        * if deletion occured in the middle of the sequence,
1315        * construct a new dataset sequence and delete the residues
1316        * that were deleted from the aligned sequence
1317        */
1318       Sequence ds = new Sequence(datasetSequence);
1319       ds.deleteChars(startDeleteColumn, startDeleteColumn + deleteCount);
1320       datasetSequence = ds;
1321       // TODO: remove any non-inheritable properties ?
1322       // TODO: create a sequence mapping (since there is a relation here ?)
1323     }
1324     start = newstart;
1325     end = newend;
1326     sequence = tmp;
1327     sequenceChanged();
1328   }
1329
1330   @Override
1331   public void insertCharAt(int i, int length, char c)
1332   {
1333     char[] tmp = new char[sequence.length + length];
1334
1335     if (i >= sequence.length)
1336     {
1337       System.arraycopy(sequence, 0, tmp, 0, sequence.length);
1338       i = sequence.length;
1339     }
1340     else
1341     {
1342       System.arraycopy(sequence, 0, tmp, 0, i);
1343     }
1344
1345     int index = i;
1346     while (length > 0)
1347     {
1348       tmp[index++] = c;
1349       length--;
1350     }
1351
1352     if (i < sequence.length)
1353     {
1354       System.arraycopy(sequence, i, tmp, index, sequence.length - i);
1355     }
1356
1357     sequence = tmp;
1358     sequenceChanged();
1359   }
1360
1361   @Override
1362   public void insertCharAt(int i, char c)
1363   {
1364     insertCharAt(i, 1, c);
1365   }
1366
1367   @Override
1368   public String getVamsasId()
1369   {
1370     return vamsasId;
1371   }
1372
1373   @Override
1374   public void setVamsasId(String id)
1375   {
1376     vamsasId = id;
1377   }
1378
1379   @Override
1380   public void setDBRefs(DBRefEntry[] dbref)
1381   {
1382     if (dbrefs == null && datasetSequence != null
1383             && this != datasetSequence)
1384     {
1385       datasetSequence.setDBRefs(dbref);
1386       return;
1387     }
1388     dbrefs = dbref;
1389     if (dbrefs != null)
1390     {
1391       DBRefUtils.ensurePrimaries(this);
1392     }
1393   }
1394
1395   @Override
1396   public DBRefEntry[] getDBRefs()
1397   {
1398     if (dbrefs == null && datasetSequence != null
1399             && this != datasetSequence)
1400     {
1401       return datasetSequence.getDBRefs();
1402     }
1403     return dbrefs;
1404   }
1405
1406   @Override
1407   public void addDBRef(DBRefEntry entry)
1408   {
1409     if (datasetSequence != null)
1410     {
1411       datasetSequence.addDBRef(entry);
1412       return;
1413     }
1414
1415     if (dbrefs == null)
1416     {
1417       dbrefs = new DBRefEntry[0];
1418     }
1419
1420     for (DBRefEntryI dbr : dbrefs)
1421     {
1422       if (dbr.updateFrom(entry))
1423       {
1424         /*
1425          * found a dbref that either matched, or could be
1426          * updated from, the new entry - no need to add it
1427          */
1428         return;
1429       }
1430     }
1431
1432     /*
1433      * extend the array to make room for one more
1434      */
1435     // TODO use an ArrayList instead
1436     int j = dbrefs.length;
1437     DBRefEntry[] temp = new DBRefEntry[j + 1];
1438     System.arraycopy(dbrefs, 0, temp, 0, j);
1439     temp[temp.length - 1] = entry;
1440
1441     dbrefs = temp;
1442
1443     DBRefUtils.ensurePrimaries(this);
1444   }
1445
1446   @Override
1447   public void setDatasetSequence(SequenceI seq)
1448   {
1449     if (seq == this)
1450     {
1451       throw new IllegalArgumentException(
1452               "Implementation Error: self reference passed to SequenceI.setDatasetSequence");
1453     }
1454     if (seq != null && seq.getDatasetSequence() != null)
1455     {
1456       throw new IllegalArgumentException(
1457               "Implementation error: cascading dataset sequences are not allowed.");
1458     }
1459     datasetSequence = seq;
1460   }
1461
1462   @Override
1463   public SequenceI getDatasetSequence()
1464   {
1465     return datasetSequence;
1466   }
1467
1468   @Override
1469   public AlignmentAnnotation[] getAnnotation()
1470   {
1471     return annotation == null ? null
1472             : annotation
1473                     .toArray(new AlignmentAnnotation[annotation.size()]);
1474   }
1475
1476   @Override
1477   public boolean hasAnnotation(AlignmentAnnotation ann)
1478   {
1479     return annotation == null ? false : annotation.contains(ann);
1480   }
1481
1482   @Override
1483   public void addAlignmentAnnotation(AlignmentAnnotation annotation)
1484   {
1485     if (this.annotation == null)
1486     {
1487       this.annotation = new Vector<>();
1488     }
1489     if (!this.annotation.contains(annotation))
1490     {
1491       this.annotation.addElement(annotation);
1492     }
1493     annotation.setSequenceRef(this);
1494   }
1495
1496   @Override
1497   public void removeAlignmentAnnotation(AlignmentAnnotation annotation)
1498   {
1499     if (this.annotation != null)
1500     {
1501       this.annotation.removeElement(annotation);
1502       if (this.annotation.size() == 0)
1503       {
1504         this.annotation = null;
1505       }
1506     }
1507   }
1508
1509   /**
1510    * test if this is a valid candidate for another sequence's dataset sequence.
1511    * 
1512    */
1513   private boolean isValidDatasetSequence()
1514   {
1515     if (datasetSequence != null)
1516     {
1517       return false;
1518     }
1519     for (int i = 0; i < sequence.length; i++)
1520     {
1521       if (jalview.util.Comparison.isGap(sequence[i]))
1522       {
1523         return false;
1524       }
1525     }
1526     return true;
1527   }
1528
1529   @Override
1530   public SequenceI deriveSequence()
1531   {
1532     Sequence seq = null;
1533     if (datasetSequence == null)
1534     {
1535       if (isValidDatasetSequence())
1536       {
1537         // Use this as dataset sequence
1538         seq = new Sequence(getName(), "", 1, -1);
1539         seq.setDatasetSequence(this);
1540         seq.initSeqFrom(this, getAnnotation());
1541         return seq;
1542       }
1543       else
1544       {
1545         // Create a new, valid dataset sequence
1546         createDatasetSequence();
1547       }
1548     }
1549     return new Sequence(this);
1550   }
1551
1552   private boolean _isNa;
1553
1554   private int _seqhash = 0;
1555
1556   /**
1557    * Answers false if the sequence is more than 85% nucleotide (ACGTU), else
1558    * true
1559    */
1560   @Override
1561   public boolean isProtein()
1562   {
1563     if (datasetSequence != null)
1564     {
1565       return datasetSequence.isProtein();
1566     }
1567     if (_seqhash != sequence.hashCode())
1568     {
1569       _seqhash = sequence.hashCode();
1570       _isNa = Comparison.isNucleotide(this);
1571     }
1572     return !_isNa;
1573   };
1574
1575   /*
1576    * (non-Javadoc)
1577    * 
1578    * @see jalview.datamodel.SequenceI#createDatasetSequence()
1579    */
1580   @Override
1581   public SequenceI createDatasetSequence()
1582   {
1583     if (datasetSequence == null)
1584     {
1585       Sequence dsseq = new Sequence(getName(),
1586               AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
1587                       getSequenceAsString()),
1588               getStart(), getEnd());
1589
1590       datasetSequence = dsseq;
1591
1592       dsseq.setDescription(description);
1593       // move features and database references onto dataset sequence
1594       dsseq.sequenceFeatureStore = sequenceFeatureStore;
1595       sequenceFeatureStore = null;
1596       dsseq.dbrefs = dbrefs;
1597       dbrefs = null;
1598       // TODO: search and replace any references to this sequence with
1599       // references to the dataset sequence in Mappings on dbref
1600       dsseq.pdbIds = pdbIds;
1601       pdbIds = null;
1602       datasetSequence.updatePDBIds();
1603       if (annotation != null)
1604       {
1605         // annotation is cloned rather than moved, to preserve what's currently
1606         // on the alignment
1607         for (AlignmentAnnotation aa : annotation)
1608         {
1609           AlignmentAnnotation _aa = new AlignmentAnnotation(aa);
1610           _aa.sequenceRef = datasetSequence;
1611           _aa.adjustForAlignment(); // uses annotation's own record of
1612                                     // sequence-column mapping
1613           datasetSequence.addAlignmentAnnotation(_aa);
1614         }
1615       }
1616     }
1617     return datasetSequence;
1618   }
1619
1620   /*
1621    * (non-Javadoc)
1622    * 
1623    * @see
1624    * jalview.datamodel.SequenceI#setAlignmentAnnotation(AlignmmentAnnotation[]
1625    * annotations)
1626    */
1627   @Override
1628   public void setAlignmentAnnotation(AlignmentAnnotation[] annotations)
1629   {
1630     if (annotation != null)
1631     {
1632       annotation.removeAllElements();
1633     }
1634     if (annotations != null)
1635     {
1636       for (int i = 0; i < annotations.length; i++)
1637       {
1638         if (annotations[i] != null)
1639         {
1640           addAlignmentAnnotation(annotations[i]);
1641         }
1642       }
1643     }
1644   }
1645
1646   @Override
1647   public AlignmentAnnotation[] getAnnotation(String label)
1648   {
1649     if (annotation == null || annotation.size() == 0)
1650     {
1651       return null;
1652     }
1653
1654     Vector<AlignmentAnnotation> subset = new Vector<>();
1655     Enumeration<AlignmentAnnotation> e = annotation.elements();
1656     while (e.hasMoreElements())
1657     {
1658       AlignmentAnnotation ann = e.nextElement();
1659       if (ann.label != null && ann.label.equals(label))
1660       {
1661         subset.addElement(ann);
1662       }
1663     }
1664     if (subset.size() == 0)
1665     {
1666       return null;
1667     }
1668     AlignmentAnnotation[] anns = new AlignmentAnnotation[subset.size()];
1669     int i = 0;
1670     e = subset.elements();
1671     while (e.hasMoreElements())
1672     {
1673       anns[i++] = e.nextElement();
1674     }
1675     subset.removeAllElements();
1676     return anns;
1677   }
1678
1679   @Override
1680   public boolean updatePDBIds()
1681   {
1682     if (datasetSequence != null)
1683     {
1684       // TODO: could merge DBRefs
1685       return datasetSequence.updatePDBIds();
1686     }
1687     if (dbrefs == null || dbrefs.length == 0)
1688     {
1689       return false;
1690     }
1691     boolean added = false;
1692     for (DBRefEntry dbr : dbrefs)
1693     {
1694       if (DBRefSource.PDB.equals(dbr.getSource()))
1695       {
1696         /*
1697          * 'Add' any PDB dbrefs as a PDBEntry - add is only performed if the
1698          * PDB id is not already present in a 'matching' PDBEntry
1699          * Constructor parses out a chain code if appended to the accession id
1700          * (a fudge used to 'store' the chain code in the DBRef)
1701          */
1702         PDBEntry pdbe = new PDBEntry(dbr);
1703         added |= addPDBId(pdbe);
1704       }
1705     }
1706     return added;
1707   }
1708
1709   @Override
1710   public void transferAnnotation(SequenceI entry, Mapping mp)
1711   {
1712     if (datasetSequence != null)
1713     {
1714       datasetSequence.transferAnnotation(entry, mp);
1715       return;
1716     }
1717     if (entry.getDatasetSequence() != null)
1718     {
1719       transferAnnotation(entry.getDatasetSequence(), mp);
1720       return;
1721     }
1722     // transfer any new features from entry onto sequence
1723     if (entry.getSequenceFeatures() != null)
1724     {
1725
1726       List<SequenceFeature> sfs = entry.getSequenceFeatures();
1727       for (SequenceFeature feature : sfs)
1728       {
1729        SequenceFeature sf[] = (mp != null) ? mp.locateFeature(feature)
1730                 : new SequenceFeature[] { new SequenceFeature(feature) };
1731         if (sf != null)
1732         {
1733           for (int sfi = 0; sfi < sf.length; sfi++)
1734           {
1735             addSequenceFeature(sf[sfi]);
1736           }
1737         }
1738       }
1739     }
1740
1741     // transfer PDB entries
1742     if (entry.getAllPDBEntries() != null)
1743     {
1744       Enumeration<PDBEntry> e = entry.getAllPDBEntries().elements();
1745       while (e.hasMoreElements())
1746       {
1747         PDBEntry pdb = e.nextElement();
1748         addPDBId(pdb);
1749       }
1750     }
1751     // transfer database references
1752     DBRefEntry[] entryRefs = entry.getDBRefs();
1753     if (entryRefs != null)
1754     {
1755       for (int r = 0; r < entryRefs.length; r++)
1756       {
1757         DBRefEntry newref = new DBRefEntry(entryRefs[r]);
1758         if (newref.getMap() != null && mp != null)
1759         {
1760           // remap ref using our local mapping
1761         }
1762         // we also assume all version string setting is done by dbSourceProxy
1763         /*
1764          * if (!newref.getSource().equalsIgnoreCase(dbSource)) {
1765          * newref.setSource(dbSource); }
1766          */
1767         addDBRef(newref);
1768       }
1769     }
1770   }
1771
1772   @Override
1773   public void setRNA(RNA r)
1774   {
1775     rna = r;
1776   }
1777
1778   @Override
1779   public RNA getRNA()
1780   {
1781     return rna;
1782   }
1783
1784   @Override
1785   public List<AlignmentAnnotation> getAlignmentAnnotations(String calcId,
1786           String label)
1787   {
1788     List<AlignmentAnnotation> result = new ArrayList<>();
1789     if (this.annotation != null)
1790     {
1791       for (AlignmentAnnotation ann : annotation)
1792       {
1793         if (ann.calcId != null && ann.calcId.equals(calcId)
1794                 && ann.label != null && ann.label.equals(label))
1795         {
1796           result.add(ann);
1797         }
1798       }
1799     }
1800     return result;
1801   }
1802
1803   @Override
1804   public String toString()
1805   {
1806     return getDisplayId(false);
1807   }
1808
1809   @Override
1810   public PDBEntry getPDBEntry(String pdbIdStr)
1811   {
1812     if (getDatasetSequence() != null)
1813     {
1814       return getDatasetSequence().getPDBEntry(pdbIdStr);
1815     }
1816     if (pdbIds == null)
1817     {
1818       return null;
1819     }
1820     List<PDBEntry> entries = getAllPDBEntries();
1821     for (PDBEntry entry : entries)
1822     {
1823       if (entry.getId().equalsIgnoreCase(pdbIdStr))
1824       {
1825         return entry;
1826       }
1827     }
1828     return null;
1829   }
1830
1831   @Override
1832   public List<DBRefEntry> getPrimaryDBRefs()
1833   {
1834     if (datasetSequence != null)
1835     {
1836       return datasetSequence.getPrimaryDBRefs();
1837     }
1838     if (dbrefs == null || dbrefs.length == 0)
1839     {
1840       return Collections.emptyList();
1841     }
1842     synchronized (dbrefs)
1843     {
1844       List<DBRefEntry> primaries = new ArrayList<>();
1845       DBRefEntry[] tmp = new DBRefEntry[1];
1846       for (DBRefEntry ref : dbrefs)
1847       {
1848         if (!ref.isPrimaryCandidate())
1849         {
1850           continue;
1851         }
1852         if (ref.hasMap())
1853         {
1854           MapList mp = ref.getMap().getMap();
1855           if (mp.getFromLowest() > start || mp.getFromHighest() < end)
1856           {
1857             // map only involves a subsequence, so cannot be primary
1858             continue;
1859           }
1860         }
1861         // whilst it looks like it is a primary ref, we also sanity check type
1862         if (DBRefUtils.getCanonicalName(DBRefSource.PDB)
1863                 .equals(DBRefUtils.getCanonicalName(ref.getSource())))
1864         {
1865           // PDB dbrefs imply there should be a PDBEntry associated
1866           // TODO: tighten PDB dbrefs
1867           // formally imply Jalview has actually downloaded and
1868           // parsed the pdb file. That means there should be a cached file
1869           // handle on the PDBEntry, and a real mapping between sequence and
1870           // extracted sequence from PDB file
1871           PDBEntry pdbentry = getPDBEntry(ref.getAccessionId());
1872           if (pdbentry != null && pdbentry.getFile() != null)
1873           {
1874             primaries.add(ref);
1875           }
1876           continue;
1877         }
1878         // check standard protein or dna sources
1879         tmp[0] = ref;
1880         DBRefEntry[] res = DBRefUtils.selectDbRefs(!isProtein(), tmp);
1881         if (res != null && res[0] == tmp[0])
1882         {
1883           primaries.add(ref);
1884           continue;
1885         }
1886       }
1887       return primaries;
1888     }
1889   }
1890
1891   /**
1892    * {@inheritDoc}
1893    */
1894   @Override
1895   public List<SequenceFeature> findFeatures(int fromColumn, int toColumn,
1896           String... types)
1897   {
1898     int startPos = findPosition(fromColumn - 1); // convert base 1 to base 0
1899     int endPos = fromColumn == toColumn ? startPos
1900             : findPosition(toColumn - 1);
1901
1902     List<SequenceFeature> result = getFeatures().findFeatures(startPos,
1903             endPos, types);
1904
1905     /*
1906      * if end column is gapped, endPos may be to the right, 
1907      * and we may have included adjacent or enclosing features;
1908      * remove any that are not enclosing, non-contact features
1909      */
1910     boolean endColumnIsGapped = toColumn > 0 && toColumn <= sequence.length
1911             && Comparison.isGap(sequence[toColumn - 1]);
1912     if (endPos > this.end || endColumnIsGapped)
1913     {
1914       ListIterator<SequenceFeature> it = result.listIterator();
1915       while (it.hasNext())
1916       {
1917         SequenceFeature sf = it.next();
1918         int sfBegin = sf.getBegin();
1919         int sfEnd = sf.getEnd();
1920         int featureStartColumn = findIndex(sfBegin);
1921         if (featureStartColumn > toColumn)
1922         {
1923           it.remove();
1924         }
1925         else if (featureStartColumn < fromColumn)
1926         {
1927           int featureEndColumn = sfEnd == sfBegin ? featureStartColumn
1928                   : findIndex(sfEnd);
1929           if (featureEndColumn < fromColumn)
1930           {
1931             it.remove();
1932           }
1933           else if (featureEndColumn > toColumn && sf.isContactFeature())
1934           {
1935             /*
1936              * remove an enclosing feature if it is a contact feature
1937              */
1938             it.remove();
1939           }
1940         }
1941       }
1942     }
1943
1944     return result;
1945   }
1946
1947   /**
1948    * Invalidates any stale cursors (forcing recalculation) by incrementing the
1949    * token that has to match the one presented by the cursor
1950    */
1951   @Override
1952   public void sequenceChanged()
1953   {
1954     changeCount++;
1955   }
1956
1957   /**
1958    * {@inheritDoc}
1959    */
1960   @Override
1961   public int replace(char c1, char c2)
1962   {
1963     if (c1 == c2)
1964     {
1965       return 0;
1966     }
1967     int count = 0;
1968     synchronized (sequence)
1969     {
1970       for (int c = 0; c < sequence.length; c++)
1971       {
1972         if (sequence[c] == c1)
1973         {
1974           sequence[c] = c2;
1975           count++;
1976         }
1977       }
1978     }
1979     if (count > 0)
1980     {
1981       sequenceChanged();
1982     }
1983
1984     return count;
1985   }
1986
1987   @Override
1988   public String getSequenceStringFromIterator(Iterator<int[]> it)
1989   {
1990     StringBuilder newSequence = new StringBuilder();
1991     while (it.hasNext())
1992     {
1993       int[] block = it.next();
1994       if (it.hasNext())
1995       {
1996         newSequence.append(getSequence(block[0], block[1] + 1));
1997       }
1998       else
1999       {
2000         newSequence.append(getSequence(block[0], block[1]));
2001       }
2002     }
2003
2004     return newSequence.toString();
2005   }
2006
2007   @Override
2008   public int firstResidueOutsideIterator(Iterator<int[]> regions)
2009   {
2010     int start = 0;
2011
2012     if (!regions.hasNext())
2013     {
2014       return findIndex(getStart()) - 1;
2015     }
2016
2017     // Simply walk along the sequence whilst watching for region
2018     // boundaries
2019     int hideStart = getLength();
2020     int hideEnd = -1;
2021     boolean foundStart = false;
2022
2023     // step through the non-gapped positions of the sequence
2024     for (int i = getStart(); i <= getEnd() && (!foundStart); i++)
2025     {
2026       // get alignment position of this residue in the sequence
2027       int p = findIndex(i) - 1;
2028
2029       // update region start/end
2030       while (hideEnd < p && regions.hasNext())
2031       {
2032         int[] region = regions.next();
2033         hideStart = region[0];
2034         hideEnd = region[1];
2035       }
2036       if (hideEnd < p)
2037       {
2038         hideStart = getLength();
2039       }
2040       // update boundary for sequence
2041       if (p < hideStart)
2042       {
2043         start = p;
2044         foundStart = true;
2045       }
2046     }
2047
2048     if (foundStart)
2049     {
2050       return start;
2051     }
2052     // otherwise, sequence was completely hidden
2053     return 0;
2054   }
2055 }