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