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