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