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