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