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