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