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