Merge branch 'develop' into features/JAL-4134_use_annotation_row_for_colours_and_groups
[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 java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.BitSet;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.Enumeration;
29 import java.util.Iterator;
30 import java.util.List;
31 import java.util.ListIterator;
32 import java.util.Vector;
33
34 import fr.orsay.lri.varna.models.rna.RNA;
35 import jalview.analysis.AlignSeq;
36 import jalview.datamodel.features.SequenceFeatures;
37 import jalview.datamodel.features.SequenceFeaturesI;
38 import jalview.util.Comparison;
39 import jalview.util.DBRefUtils;
40 import jalview.util.MapList;
41 import jalview.util.StringUtils;
42 import jalview.ws.datamodel.alphafold.MappableContactMatrix;
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    * Answers the sequence name, with '/start-end' appended if jvsuffix is true
477    * 
478    * @return
479    */
480   @Override
481   public String getDisplayId(boolean jvsuffix)
482   {
483     if (!jvsuffix)
484     {
485       return name;
486     }
487     StringBuilder result = new StringBuilder(name);
488     result.append("/").append(start).append("-").append(end);
489
490     return result.toString();
491   }
492
493   /**
494    * Sets the sequence name. If the name ends in /start-end, then the start-end
495    * values are parsed out and set, and the suffix is removed from the name.
496    * 
497    * @param theName
498    */
499   @Override
500   public void setName(String theName)
501   {
502     this.name = theName;
503     this.parseId();
504   }
505
506   /**
507    * DOCUMENT ME!
508    * 
509    * @return DOCUMENT ME!
510    */
511   @Override
512   public String getName()
513   {
514     return this.name;
515   }
516
517   /**
518    * DOCUMENT ME!
519    * 
520    * @param start
521    *          DOCUMENT ME!
522    */
523   @Override
524   public void setStart(int start)
525   {
526     this.start = start;
527     sequenceChanged();
528   }
529
530   /**
531    * DOCUMENT ME!
532    * 
533    * @return DOCUMENT ME!
534    */
535   @Override
536   public int getStart()
537   {
538     return this.start;
539   }
540
541   /**
542    * DOCUMENT ME!
543    * 
544    * @param end
545    *          DOCUMENT ME!
546    */
547   @Override
548   public void setEnd(int end)
549   {
550     this.end = end;
551   }
552
553   /**
554    * DOCUMENT ME!
555    * 
556    * @return DOCUMENT ME!
557    */
558   @Override
559   public int getEnd()
560   {
561     return this.end;
562   }
563
564   /**
565    * DOCUMENT ME!
566    * 
567    * @return DOCUMENT ME!
568    */
569   @Override
570   public int getLength()
571   {
572     return this.sequence.length;
573   }
574
575   /**
576    * DOCUMENT ME!
577    * 
578    * @param seq
579    *          DOCUMENT ME!
580    */
581   @Override
582   public void setSequence(String seq)
583   {
584     this.sequence = seq.toCharArray();
585     checkValidRange();
586     sequenceChanged();
587   }
588
589   @Override
590   public String getSequenceAsString()
591   {
592     return new String(sequence);
593   }
594
595   @Override
596   public String getSequenceAsString(int start, int end)
597   {
598     return new String(getSequence(start, end));
599   }
600
601   @Override
602   public char[] getSequence()
603   {
604     // return sequence;
605     return sequence == null ? null
606             : Arrays.copyOf(sequence, sequence.length);
607   }
608
609   /*
610    * (non-Javadoc)
611    * 
612    * @see jalview.datamodel.SequenceI#getSequence(int, int)
613    */
614   @Override
615   public char[] getSequence(int start, int end)
616   {
617     if (start < 0)
618     {
619       start = 0;
620     }
621     // JBPNote - left to user to pad the result here (TODO:Decide on this
622     // policy)
623     if (start >= sequence.length)
624     {
625       return new char[0];
626     }
627
628     if (end >= sequence.length)
629     {
630       end = sequence.length;
631     }
632
633     char[] reply = new char[end - start];
634     System.arraycopy(sequence, start, reply, 0, end - start);
635
636     return reply;
637   }
638
639   @Override
640   public SequenceI getSubSequence(int start, int end)
641   {
642     if (start < 0)
643     {
644       start = 0;
645     }
646     char[] seq = getSequence(start, end);
647     if (seq.length == 0)
648     {
649       return null;
650     }
651     int nstart = findPosition(start);
652     int nend = findPosition(end) - 1;
653     // JBPNote - this is an incomplete copy.
654     SequenceI nseq = new Sequence(this.getName(), seq, nstart, nend);
655     nseq.setDescription(description);
656     if (datasetSequence != null)
657     {
658       nseq.setDatasetSequence(datasetSequence);
659     }
660     else
661     {
662       nseq.setDatasetSequence(this);
663     }
664     return nseq;
665   }
666
667   /**
668    * Returns the character of the aligned sequence at the given position (base
669    * zero), or space if the position is not within the sequence's bounds
670    * 
671    * @return
672    */
673   @Override
674   public char getCharAt(int i)
675   {
676     if (i >= 0 && i < sequence.length)
677     {
678       return sequence[i];
679     }
680     else
681     {
682       return ' ';
683     }
684   }
685
686   /**
687    * Sets the sequence description, and also parses out any special formats of
688    * interest
689    * 
690    * @param desc
691    */
692   @Override
693   public void setDescription(String desc)
694   {
695     this.description = desc;
696   }
697
698   @Override
699   public void setGeneLoci(String speciesId, String assemblyId,
700           String chromosomeId, MapList map)
701   {
702     addDBRef(new GeneLocus(speciesId, assemblyId, chromosomeId,
703             new Mapping(map)));
704   }
705
706   /**
707    * Returns the gene loci mapping for the sequence (may be null)
708    * 
709    * @return
710    */
711   @Override
712   public GeneLociI getGeneLoci()
713   {
714     List<DBRefEntry> refs = getDBRefs();
715     if (refs != null)
716     {
717       for (final DBRefEntry ref : refs)
718       {
719         if (ref instanceof GeneLociI)
720         {
721           return (GeneLociI) ref;
722         }
723       }
724     }
725     return null;
726   }
727
728   /**
729    * Answers the description
730    * 
731    * @return
732    */
733   @Override
734   public String getDescription()
735   {
736     return this.description;
737   }
738
739   /**
740    * {@inheritDoc}
741    */
742   @Override
743   public int findIndex(int pos)
744   {
745     /*
746      * use a valid, hopefully nearby, cursor if available
747      */
748     if (isValidCursor(cursor))
749     {
750       return findIndex(pos, cursor);
751     }
752
753     int j = start;
754     int i = 0;
755     int startColumn = 0;
756
757     /*
758      * traverse sequence from the start counting gaps; make a note of
759      * the column of the first residue to save in the cursor
760      */
761     while ((i < sequence.length) && (j <= end) && (j <= pos))
762     {
763       if (!Comparison.isGap(sequence[i]))
764       {
765         if (j == start)
766         {
767           startColumn = i;
768         }
769         j++;
770       }
771       i++;
772     }
773
774     if (j == end && j < pos)
775     {
776       return end + 1;
777     }
778
779     updateCursor(pos, i, startColumn);
780     return i;
781   }
782
783   /**
784    * Updates the cursor to the latest found residue and column position
785    * 
786    * @param residuePos
787    *          (start..)
788    * @param column
789    *          (1..)
790    * @param startColumn
791    *          column position of the first sequence residue
792    */
793   protected void updateCursor(int residuePos, int column, int startColumn)
794   {
795     /*
796      * preserve end residue column provided cursor was valid
797      */
798     int endColumn = isValidCursor(cursor) ? cursor.lastColumnPosition : 0;
799
800     if (residuePos == this.end)
801     {
802       endColumn = column;
803     }
804
805     cursor = new SequenceCursor(this, residuePos, column, startColumn,
806             endColumn, this.changeCount);
807   }
808
809   /**
810    * Answers the aligned column position (1..) for the given residue position
811    * (start..) given a 'hint' of a residue/column location in the neighbourhood.
812    * The hint may be left of, at, or to the right of the required position.
813    * 
814    * @param pos
815    * @param curs
816    * @return
817    */
818   protected int findIndex(final int pos, SequenceCursor curs)
819   {
820     if (!isValidCursor(curs))
821     {
822       /*
823        * wrong or invalidated cursor, compute de novo
824        */
825       return findIndex(pos);
826     }
827
828     if (curs.residuePosition == pos)
829     {
830       return curs.columnPosition;
831     }
832
833     /*
834      * move left or right to find pos from hint.position
835      */
836     int col = curs.columnPosition - 1; // convert from base 1 to base 0
837     int newPos = curs.residuePosition;
838     int delta = newPos > pos ? -1 : 1;
839
840     while (newPos != pos)
841     {
842       col += delta; // shift one column left or right
843       if (col < 0)
844       {
845         break;
846       }
847       if (col == sequence.length)
848       {
849         col--; // return last column if we failed to reach pos
850         break;
851       }
852       if (!Comparison.isGap(sequence[col]))
853       {
854         newPos += delta;
855       }
856     }
857
858     col++; // convert back to base 1
859
860     /*
861      * only update cursor if we found the target position
862      */
863     if (newPos == pos)
864     {
865       updateCursor(pos, col, curs.firstColumnPosition);
866     }
867
868     return col;
869   }
870
871   /**
872    * {@inheritDoc}
873    */
874   @Override
875   public int findPosition(final int column)
876   {
877     /*
878      * use a valid, hopefully nearby, cursor if available
879      */
880     if (isValidCursor(cursor))
881     {
882       return findPosition(column + 1, cursor);
883     }
884
885     // TODO recode this more naturally i.e. count residues only
886     // as they are found, not 'in anticipation'
887
888     /*
889      * traverse the sequence counting gaps; note the column position
890      * of the first residue, to save in the cursor
891      */
892     int firstResidueColumn = 0;
893     int lastPosFound = 0;
894     int lastPosFoundColumn = 0;
895     int seqlen = sequence.length;
896
897     if (seqlen > 0 && !Comparison.isGap(sequence[0]))
898     {
899       lastPosFound = start;
900       lastPosFoundColumn = 0;
901     }
902
903     int j = 0;
904     int pos = start;
905
906     while (j < column && j < seqlen)
907     {
908       if (!Comparison.isGap(sequence[j]))
909       {
910         lastPosFound = pos;
911         lastPosFoundColumn = j;
912         if (pos == this.start)
913         {
914           firstResidueColumn = j;
915         }
916         pos++;
917       }
918       j++;
919     }
920     if (j < seqlen && !Comparison.isGap(sequence[j]))
921     {
922       lastPosFound = pos;
923       lastPosFoundColumn = j;
924       if (pos == this.start)
925       {
926         firstResidueColumn = j;
927       }
928     }
929
930     /*
931      * update the cursor to the last residue position found (if any)
932      * (converting column position to base 1)
933      */
934     if (lastPosFound != 0)
935     {
936       updateCursor(lastPosFound, lastPosFoundColumn + 1,
937               firstResidueColumn + 1);
938     }
939
940     return pos;
941   }
942
943   /**
944    * Answers true if the given cursor is not null, is for this sequence object,
945    * and has a token value that matches this object's changeCount, else false.
946    * This allows us to ignore a cursor as 'stale' if the sequence has been
947    * modified since the cursor was created.
948    * 
949    * @param curs
950    * @return
951    */
952   protected boolean isValidCursor(SequenceCursor curs)
953   {
954     if (curs == null || curs.sequence != this || curs.token != changeCount)
955     {
956       return false;
957     }
958     /*
959      * sanity check against range
960      */
961     if (curs.columnPosition < 0 || curs.columnPosition > sequence.length)
962     {
963       return false;
964     }
965     if (curs.residuePosition < start || curs.residuePosition > end)
966     {
967       return false;
968     }
969     return true;
970   }
971
972   /**
973    * Answers the sequence position (start..) for the given aligned column
974    * position (1..), given a hint of a cursor in the neighbourhood. The cursor
975    * may lie left of, at, or to the right of the column position.
976    * 
977    * @param col
978    * @param curs
979    * @return
980    */
981   protected int findPosition(final int col, SequenceCursor curs)
982   {
983     if (!isValidCursor(curs))
984     {
985       /*
986        * wrong or invalidated cursor, compute de novo
987        */
988       return findPosition(col - 1);// ugh back to base 0
989     }
990
991     if (curs.columnPosition == col)
992     {
993       cursor = curs; // in case this method becomes public
994       return curs.residuePosition; // easy case :-)
995     }
996
997     if (curs.lastColumnPosition > 0 && curs.lastColumnPosition < col)
998     {
999       /*
1000        * sequence lies entirely to the left of col
1001        * - return last residue + 1
1002        */
1003       return end + 1;
1004     }
1005
1006     if (curs.firstColumnPosition > 0 && curs.firstColumnPosition > col)
1007     {
1008       /*
1009        * sequence lies entirely to the right of col
1010        * - return first residue
1011        */
1012       return start;
1013     }
1014
1015     // todo could choose closest to col out of column,
1016     // firstColumnPosition, lastColumnPosition as a start point
1017
1018     /*
1019      * move left or right to find pos from cursor position
1020      */
1021     int firstResidueColumn = curs.firstColumnPosition;
1022     int column = curs.columnPosition - 1; // to base 0
1023     int newPos = curs.residuePosition;
1024     int delta = curs.columnPosition > col ? -1 : 1;
1025     boolean gapped = false;
1026     int lastFoundPosition = curs.residuePosition;
1027     int lastFoundPositionColumn = curs.columnPosition;
1028
1029     while (column != col - 1)
1030     {
1031       column += delta; // shift one column left or right
1032       if (column < 0 || column == sequence.length)
1033       {
1034         break;
1035       }
1036       gapped = Comparison.isGap(sequence[column]);
1037       if (!gapped)
1038       {
1039         newPos += delta;
1040         lastFoundPosition = newPos;
1041         lastFoundPositionColumn = column + 1;
1042         if (lastFoundPosition == this.start)
1043         {
1044           firstResidueColumn = column + 1;
1045         }
1046       }
1047     }
1048
1049     if (cursor == null || lastFoundPosition != cursor.residuePosition)
1050     {
1051       updateCursor(lastFoundPosition, lastFoundPositionColumn,
1052               firstResidueColumn);
1053     }
1054
1055     /*
1056      * hack to give position to the right if on a gap
1057      * or beyond the length of the sequence (see JAL-2562)
1058      */
1059     if (delta > 0 && (gapped || column >= sequence.length))
1060     {
1061       newPos++;
1062     }
1063
1064     return newPos;
1065   }
1066
1067   /**
1068    * {@inheritDoc}
1069    */
1070   @Override
1071   public ContiguousI findPositions(int fromColumn, int toColumn)
1072   {
1073     if (toColumn < fromColumn || fromColumn < 1)
1074     {
1075       return null;
1076     }
1077
1078     /*
1079      * find the first non-gapped position, if any
1080      */
1081     int firstPosition = 0;
1082     int col = fromColumn - 1;
1083     int length = sequence.length;
1084     while (col < length && col < toColumn)
1085     {
1086       if (!Comparison.isGap(sequence[col]))
1087       {
1088         firstPosition = findPosition(col++);
1089         break;
1090       }
1091       col++;
1092     }
1093
1094     if (firstPosition == 0)
1095     {
1096       return null;
1097     }
1098
1099     /*
1100      * find the last non-gapped position
1101      */
1102     int lastPosition = firstPosition;
1103     while (col < length && col < toColumn)
1104     {
1105       if (!Comparison.isGap(sequence[col++]))
1106       {
1107         lastPosition++;
1108       }
1109     }
1110
1111     return new Range(firstPosition, lastPosition);
1112   }
1113
1114   /**
1115    * Returns an int array where indices correspond to each residue in the
1116    * sequence and the element value gives its position in the alignment
1117    * 
1118    * @return int[SequenceI.getEnd()-SequenceI.getStart()+1] or null if no
1119    *         residues in SequenceI object
1120    */
1121   @Override
1122   public int[] gapMap()
1123   {
1124     String seq = jalview.analysis.AlignSeq.extractGaps(
1125             jalview.util.Comparison.GapChars, new String(sequence));
1126     int[] map = new int[seq.length()];
1127     int j = 0;
1128     int p = 0;
1129
1130     while (j < sequence.length)
1131     {
1132       if (!jalview.util.Comparison.isGap(sequence[j]))
1133       {
1134         map[p++] = j;
1135       }
1136
1137       j++;
1138     }
1139
1140     return map;
1141   }
1142
1143   /**
1144    * Build a bitset corresponding to sequence gaps
1145    * 
1146    * @return a BitSet where set values correspond to gaps in the sequence
1147    */
1148   @Override
1149   public BitSet gapBitset()
1150   {
1151     BitSet gaps = new BitSet(sequence.length);
1152     int j = 0;
1153     while (j < sequence.length)
1154     {
1155       if (jalview.util.Comparison.isGap(sequence[j]))
1156       {
1157         gaps.set(j);
1158       }
1159       j++;
1160     }
1161     return gaps;
1162   }
1163
1164   @Override
1165   public int[] findPositionMap()
1166   {
1167     int map[] = new int[sequence.length];
1168     int j = 0;
1169     int pos = start;
1170     int seqlen = sequence.length;
1171     while ((j < seqlen))
1172     {
1173       map[j] = pos;
1174       if (!jalview.util.Comparison.isGap(sequence[j]))
1175       {
1176         pos++;
1177       }
1178
1179       j++;
1180     }
1181     return map;
1182   }
1183
1184   @Override
1185   public List<int[]> getInsertions()
1186   {
1187     ArrayList<int[]> map = new ArrayList<>();
1188     int lastj = -1, j = 0;
1189     // int pos = start;
1190     int seqlen = sequence.length;
1191     while ((j < seqlen))
1192     {
1193       if (jalview.util.Comparison.isGap(sequence[j]))
1194       {
1195         if (lastj == -1)
1196         {
1197           lastj = j;
1198         }
1199       }
1200       else
1201       {
1202         if (lastj != -1)
1203         {
1204           map.add(new int[] { lastj, j - 1 });
1205           lastj = -1;
1206         }
1207       }
1208       j++;
1209     }
1210     if (lastj != -1)
1211     {
1212       map.add(new int[] { lastj, j - 1 });
1213       lastj = -1;
1214     }
1215     return map;
1216   }
1217
1218   @Override
1219   public BitSet getInsertionsAsBits()
1220   {
1221     BitSet map = new BitSet();
1222     int lastj = -1, j = 0;
1223     // int pos = start;
1224     int seqlen = sequence.length;
1225     while ((j < seqlen))
1226     {
1227       if (jalview.util.Comparison.isGap(sequence[j]))
1228       {
1229         if (lastj == -1)
1230         {
1231           lastj = j;
1232         }
1233       }
1234       else
1235       {
1236         if (lastj != -1)
1237         {
1238           map.set(lastj, j);
1239           lastj = -1;
1240         }
1241       }
1242       j++;
1243     }
1244     if (lastj != -1)
1245     {
1246       map.set(lastj, j);
1247       lastj = -1;
1248     }
1249     return map;
1250   }
1251
1252   @Override
1253   public void deleteChars(final int i, final int j)
1254   {
1255     int newstart = start, newend = end;
1256     if (i >= sequence.length || i < 0)
1257     {
1258       return;
1259     }
1260
1261     char[] tmp = StringUtils.deleteChars(sequence, i, j);
1262     boolean createNewDs = false;
1263     // TODO: take a (second look) at the dataset creation validation method for
1264     // the very large sequence case
1265
1266     int startIndex = findIndex(start) - 1;
1267     int endIndex = findIndex(end) - 1;
1268     int startDeleteColumn = -1; // for dataset sequence deletions
1269     int deleteCount = 0;
1270
1271     for (int s = i; s < j && s < sequence.length; s++)
1272     {
1273       if (Comparison.isGap(sequence[s]))
1274       {
1275         continue;
1276       }
1277       deleteCount++;
1278       if (startDeleteColumn == -1)
1279       {
1280         startDeleteColumn = findPosition(s) - start;
1281       }
1282       if (createNewDs)
1283       {
1284         newend--;
1285       }
1286       else
1287       {
1288         if (startIndex == s)
1289         {
1290           /*
1291            * deleting characters from start of sequence; new start is the
1292            * sequence position of the next column (position to the right
1293            * if the column position is gapped)
1294            */
1295           newstart = findPosition(j);
1296           break;
1297         }
1298         else
1299         {
1300           if (endIndex < j)
1301           {
1302             /*
1303              * deleting characters at end of sequence; new end is the sequence
1304              * position of the column before the deletion; subtract 1 if this is
1305              * gapped since findPosition returns the next sequence position
1306              */
1307             newend = findPosition(i - 1);
1308             if (Comparison.isGap(sequence[i - 1]))
1309             {
1310               newend--;
1311             }
1312             break;
1313           }
1314           else
1315           {
1316             createNewDs = true;
1317             newend--;
1318           }
1319         }
1320       }
1321     }
1322
1323     if (createNewDs && this.datasetSequence != null)
1324     {
1325       /*
1326        * if deletion occured in the middle of the sequence,
1327        * construct a new dataset sequence and delete the residues
1328        * that were deleted from the aligned sequence
1329        */
1330       Sequence ds = new Sequence(datasetSequence);
1331       ds.deleteChars(startDeleteColumn, startDeleteColumn + deleteCount);
1332       datasetSequence = ds;
1333       // TODO: remove any non-inheritable properties ?
1334       // TODO: create a sequence mapping (since there is a relation here ?)
1335     }
1336     start = newstart;
1337     end = newend;
1338     sequence = tmp;
1339     sequenceChanged();
1340   }
1341
1342   @Override
1343   public void insertCharAt(int i, int length, char c)
1344   {
1345     char[] tmp = new char[sequence.length + length];
1346
1347     if (i >= sequence.length)
1348     {
1349       System.arraycopy(sequence, 0, tmp, 0, sequence.length);
1350       i = sequence.length;
1351     }
1352     else
1353     {
1354       System.arraycopy(sequence, 0, tmp, 0, i);
1355     }
1356
1357     int index = i;
1358     while (length > 0)
1359     {
1360       tmp[index++] = c;
1361       length--;
1362     }
1363
1364     if (i < sequence.length)
1365     {
1366       System.arraycopy(sequence, i, tmp, index, sequence.length - i);
1367     }
1368
1369     sequence = tmp;
1370     sequenceChanged();
1371   }
1372
1373   @Override
1374   public void insertCharAt(int i, char c)
1375   {
1376     insertCharAt(i, 1, c);
1377   }
1378
1379   @Override
1380   public String getVamsasId()
1381   {
1382     return vamsasId;
1383   }
1384
1385   @Override
1386   public void setVamsasId(String id)
1387   {
1388     vamsasId = id;
1389   }
1390
1391   @Deprecated
1392   @Override
1393   public void setDBRefs(DBModList<DBRefEntry> newDBrefs)
1394   {
1395     if (dbrefs == null && datasetSequence != null
1396             && this != datasetSequence)
1397     {
1398       datasetSequence.setDBRefs(newDBrefs);
1399       return;
1400     }
1401     dbrefs = newDBrefs;
1402     refModCount = 0;
1403   }
1404
1405   @Override
1406   public DBModList<DBRefEntry> getDBRefs()
1407   {
1408     if (dbrefs == null && datasetSequence != null
1409             && this != datasetSequence)
1410     {
1411       return datasetSequence.getDBRefs();
1412     }
1413     return dbrefs;
1414   }
1415
1416   @Override
1417   public void addDBRef(DBRefEntry entry)
1418   {
1419     // TODO JAL-3980 maintain as sorted list
1420     if (datasetSequence != null)
1421     {
1422       datasetSequence.addDBRef(entry);
1423       return;
1424     }
1425
1426     if (dbrefs == null)
1427     {
1428       dbrefs = new DBModList<>();
1429     }
1430     // TODO JAL-3979 LOOK UP RATHER THAN SWEEP FOR EFFICIENCY
1431
1432     for (int ib = 0, nb = dbrefs.size(); ib < nb; ib++)
1433     {
1434       if (dbrefs.get(ib).updateFrom(entry))
1435       {
1436         /*
1437          * found a dbref that either matched, or could be
1438          * updated from, the new entry - no need to add it
1439          */
1440         return;
1441       }
1442     }
1443
1444     // /// BH OUCH!
1445     // /*
1446     // * extend the array to make room for one more
1447     // */
1448     // // TODO use an ArrayList instead
1449     // int j = dbrefs.length;
1450     // List<DBRefEntry> temp = new DBRefEntry[j + 1];
1451     // System.arraycopy(dbrefs, 0, temp, 0, j);
1452     // temp[temp.length - 1] = entry;
1453     //
1454     // dbrefs = temp;
1455
1456     dbrefs.add(entry);
1457   }
1458
1459   @Override
1460   public void setDatasetSequence(SequenceI seq)
1461   {
1462     if (seq == this)
1463     {
1464       throw new IllegalArgumentException(
1465               "Implementation Error: self reference passed to SequenceI.setDatasetSequence");
1466     }
1467     if (seq != null && seq.getDatasetSequence() != null)
1468     {
1469       throw new IllegalArgumentException(
1470               "Implementation error: cascading dataset sequences are not allowed.");
1471     }
1472     datasetSequence = seq;
1473   }
1474
1475   @Override
1476   public SequenceI getDatasetSequence()
1477   {
1478     return datasetSequence;
1479   }
1480
1481   @Override
1482   public AlignmentAnnotation[] getAnnotation()
1483   {
1484     return annotation == null ? null
1485             : annotation
1486                     .toArray(new AlignmentAnnotation[annotation.size()]);
1487   }
1488
1489   @Override
1490   public boolean hasAnnotation(AlignmentAnnotation ann)
1491   {
1492     return annotation == null ? false : annotation.contains(ann);
1493   }
1494
1495   @Override
1496   public void addAlignmentAnnotation(AlignmentAnnotation annotation)
1497   {
1498     if (this.annotation == null)
1499     {
1500       this.annotation = new Vector<>();
1501     }
1502     if (!this.annotation.contains(annotation))
1503     {
1504       this.annotation.addElement(annotation);
1505     }
1506     annotation.setSequenceRef(this);
1507   }
1508
1509   @Override
1510   public void removeAlignmentAnnotation(AlignmentAnnotation annotation)
1511   {
1512     if (this.annotation != null)
1513     {
1514       this.annotation.removeElement(annotation);
1515       if (this.annotation.size() == 0)
1516       {
1517         this.annotation = null;
1518       }
1519     }
1520   }
1521
1522   /**
1523    * test if this is a valid candidate for another sequence's dataset sequence.
1524    * 
1525    */
1526   private boolean isValidDatasetSequence()
1527   {
1528     if (datasetSequence != null)
1529     {
1530       return false;
1531     }
1532     for (int i = 0; i < sequence.length; i++)
1533     {
1534       if (jalview.util.Comparison.isGap(sequence[i]))
1535       {
1536         return false;
1537       }
1538     }
1539     return true;
1540   }
1541
1542   @Override
1543   public SequenceI deriveSequence()
1544   {
1545     Sequence seq = null;
1546     if (datasetSequence == null)
1547     {
1548       if (isValidDatasetSequence())
1549       {
1550         // Use this as dataset sequence
1551         seq = new Sequence(getName(), "", 1, -1);
1552         seq.setDatasetSequence(this);
1553         seq.initSeqFrom(this, getAnnotation());
1554         return seq;
1555       }
1556       else
1557       {
1558         // Create a new, valid dataset sequence
1559         createDatasetSequence();
1560       }
1561     }
1562     return new Sequence(this);
1563   }
1564
1565   private boolean _isNa;
1566
1567   private int _seqhash = 0;
1568
1569   private List<DBRefEntry> primaryRefs;
1570
1571   /**
1572    * Answers false if the sequence is more than 85% nucleotide (ACGTU), else
1573    * true
1574    */
1575   @Override
1576   public boolean isProtein()
1577   {
1578     if (datasetSequence != null)
1579     {
1580       return datasetSequence.isProtein();
1581     }
1582     if (_seqhash != sequence.hashCode())
1583     {
1584       _seqhash = sequence.hashCode();
1585       _isNa = Comparison.isNucleotide(this);
1586     }
1587     return !_isNa;
1588   }
1589
1590   /*
1591    * (non-Javadoc)
1592    * 
1593    * @see jalview.datamodel.SequenceI#createDatasetSequence()
1594    */
1595   @Override
1596   public SequenceI createDatasetSequence()
1597   {
1598     if (datasetSequence == null)
1599     {
1600       Sequence dsseq = new Sequence(getName(),
1601               AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
1602                       getSequenceAsString()),
1603               getStart(), getEnd());
1604
1605       datasetSequence = dsseq;
1606
1607       dsseq.setDescription(description);
1608       // move features and database references onto dataset sequence
1609       dsseq.sequenceFeatureStore = sequenceFeatureStore;
1610       sequenceFeatureStore = null;
1611       dsseq.dbrefs = dbrefs;
1612       dbrefs = null;
1613       // TODO: search and replace any references to this sequence with
1614       // references to the dataset sequence in Mappings on dbref
1615       dsseq.pdbIds = pdbIds;
1616       pdbIds = null;
1617       datasetSequence.updatePDBIds();
1618       if (annotation != null)
1619       {
1620         // annotation is cloned rather than moved, to preserve what's currently
1621         // on the alignment
1622         for (AlignmentAnnotation aa : annotation)
1623         {
1624           AlignmentAnnotation _aa = new AlignmentAnnotation(aa);
1625           _aa.sequenceRef = datasetSequence;
1626           _aa.adjustForAlignment(); // uses annotation's own record of
1627                                     // sequence-column mapping
1628           datasetSequence.addAlignmentAnnotation(_aa);
1629
1630           // transfer contact matrices
1631           ContactMatrixI cm = getContactMatrixFor(aa);
1632           if (cm != null)
1633           {
1634             datasetSequence.addContactListFor(_aa, cm);
1635           }
1636         }
1637       }
1638     }
1639     return datasetSequence;
1640   }
1641
1642   /*
1643    * (non-Javadoc)
1644    * 
1645    * @see
1646    * jalview.datamodel.SequenceI#setAlignmentAnnotation(AlignmmentAnnotation[]
1647    * annotations)
1648    */
1649   @Override
1650   public void setAlignmentAnnotation(AlignmentAnnotation[] annotations)
1651   {
1652     if (annotation != null)
1653     {
1654       annotation.removeAllElements();
1655     }
1656     if (annotations != null)
1657     {
1658       for (int i = 0; i < annotations.length; i++)
1659       {
1660         if (annotations[i] != null)
1661         {
1662           addAlignmentAnnotation(annotations[i]);
1663         }
1664       }
1665     }
1666   }
1667
1668   @Override
1669   public AlignmentAnnotation[] getAnnotation(String label)
1670   {
1671     if (annotation == null || annotation.size() == 0)
1672     {
1673       return null;
1674     }
1675
1676     Vector<AlignmentAnnotation> subset = new Vector<>();
1677     Enumeration<AlignmentAnnotation> e = annotation.elements();
1678     while (e.hasMoreElements())
1679     {
1680       AlignmentAnnotation ann = e.nextElement();
1681       if (ann.label != null && ann.label.equals(label))
1682       {
1683         subset.addElement(ann);
1684       }
1685     }
1686     if (subset.size() == 0)
1687     {
1688       return null;
1689     }
1690     AlignmentAnnotation[] anns = new AlignmentAnnotation[subset.size()];
1691     int i = 0;
1692     e = subset.elements();
1693     while (e.hasMoreElements())
1694     {
1695       anns[i++] = e.nextElement();
1696     }
1697     subset.removeAllElements();
1698     return anns;
1699   }
1700
1701   @Override
1702   public boolean updatePDBIds()
1703   {
1704     if (datasetSequence != null)
1705     {
1706       // TODO: could merge DBRefs
1707       return datasetSequence.updatePDBIds();
1708     }
1709     if (dbrefs == null || dbrefs.size() == 0)
1710     {
1711       return false;
1712     }
1713     boolean added = false;
1714     for (int ib = 0, nb = dbrefs.size(); ib < nb; ib++)
1715     {
1716       DBRefEntry dbr = dbrefs.get(ib);
1717       if (DBRefSource.PDB.equals(dbr.getSource()))
1718       {
1719         /*
1720          * 'Add' any PDB dbrefs as a PDBEntry - add is only performed if the
1721          * PDB id is not already present in a 'matching' PDBEntry
1722          * Constructor parses out a chain code if appended to the accession id
1723          * (a fudge used to 'store' the chain code in the DBRef)
1724          */
1725         PDBEntry pdbe = new PDBEntry(dbr);
1726         added |= addPDBId(pdbe);
1727       }
1728     }
1729     return added;
1730   }
1731
1732   @Override
1733   public void transferAnnotation(SequenceI entry, Mapping mp)
1734   {
1735     if (datasetSequence != null)
1736     {
1737       datasetSequence.transferAnnotation(entry, mp);
1738       return;
1739     }
1740     if (entry.getDatasetSequence() != null)
1741     {
1742       transferAnnotation(entry.getDatasetSequence(), mp);
1743       return;
1744     }
1745     // transfer any new features from entry onto sequence
1746     if (entry.getSequenceFeatures() != null)
1747     {
1748
1749       List<SequenceFeature> sfs = entry.getSequenceFeatures();
1750       for (SequenceFeature feature : sfs)
1751       {
1752         SequenceFeature sf[] = (mp != null) ? mp.locateFeature(feature)
1753                 : new SequenceFeature[]
1754                 { new SequenceFeature(feature) };
1755         if (sf != null)
1756         {
1757           for (int sfi = 0; sfi < sf.length; sfi++)
1758           {
1759             addSequenceFeature(sf[sfi]);
1760           }
1761         }
1762       }
1763     }
1764
1765     // transfer PDB entries
1766     if (entry.getAllPDBEntries() != null)
1767     {
1768       Enumeration<PDBEntry> e = entry.getAllPDBEntries().elements();
1769       while (e.hasMoreElements())
1770       {
1771         PDBEntry pdb = e.nextElement();
1772         addPDBId(pdb);
1773       }
1774     }
1775     // transfer database references
1776     List<DBRefEntry> entryRefs = entry.getDBRefs();
1777     if (entryRefs != null)
1778     {
1779       for (int r = 0, n = entryRefs.size(); r < n; r++)
1780       {
1781         DBRefEntry newref = new DBRefEntry(entryRefs.get(r));
1782         if (newref.getMap() != null && mp != null)
1783         {
1784           // remap ref using our local mapping
1785         }
1786         // we also assume all version string setting is done by dbSourceProxy
1787         /*
1788          * if (!newref.getSource().equalsIgnoreCase(dbSource)) {
1789          * newref.setSource(dbSource); }
1790          */
1791         addDBRef(newref);
1792       }
1793     }
1794   }
1795
1796   @Override
1797   public void setRNA(RNA r)
1798   {
1799     rna = r;
1800   }
1801
1802   @Override
1803   public RNA getRNA()
1804   {
1805     return rna;
1806   }
1807
1808   @Override
1809   public List<AlignmentAnnotation> getAlignmentAnnotations(String calcId,
1810           String label)
1811   {
1812     return getAlignmentAnnotations(calcId, label, null, true);
1813   }
1814
1815   @Override
1816   public List<AlignmentAnnotation> getAlignmentAnnotations(String calcId,
1817           String label, String description)
1818   {
1819     return getAlignmentAnnotations(calcId, label, description, false);
1820   }
1821
1822   private List<AlignmentAnnotation> getAlignmentAnnotations(String calcId,
1823           String label, String description, boolean ignoreDescription)
1824   {
1825     List<AlignmentAnnotation> result = new ArrayList<>();
1826     if (this.annotation != null)
1827     {
1828       for (AlignmentAnnotation ann : annotation)
1829       {
1830         if ((ann.calcId != null && ann.calcId.equals(calcId))
1831                 && (ann.label != null && ann.label.equals(label))
1832                 && ((ignoreDescription && description == null)
1833                         || (ann.description != null
1834                                 && ann.description.equals(description))))
1835
1836         {
1837           result.add(ann);
1838         }
1839       }
1840     }
1841     return result;
1842   }
1843
1844   @Override
1845   public String toString()
1846   {
1847     return getDisplayId(false);
1848   }
1849
1850   @Override
1851   public PDBEntry getPDBEntry(String pdbIdStr)
1852   {
1853     if (getDatasetSequence() != null)
1854     {
1855       return getDatasetSequence().getPDBEntry(pdbIdStr);
1856     }
1857     if (pdbIds == null)
1858     {
1859       return null;
1860     }
1861     List<PDBEntry> entries = getAllPDBEntries();
1862     for (PDBEntry entry : entries)
1863     {
1864       if (entry.getId().equalsIgnoreCase(pdbIdStr))
1865       {
1866         return entry;
1867       }
1868     }
1869     return null;
1870   }
1871
1872   private List<DBRefEntry> tmpList;
1873
1874   @Override
1875   public List<DBRefEntry> getPrimaryDBRefs()
1876   {
1877     if (datasetSequence != null)
1878     {
1879       return datasetSequence.getPrimaryDBRefs();
1880     }
1881     if (dbrefs == null || dbrefs.size() == 0)
1882     {
1883       return Collections.emptyList();
1884     }
1885     synchronized (dbrefs)
1886     {
1887       if (refModCount == dbrefs.getModCount() && primaryRefs != null)
1888       {
1889         return primaryRefs; // no changes
1890       }
1891       refModCount = dbrefs.getModCount();
1892       List<DBRefEntry> primaries = (primaryRefs == null
1893               ? (primaryRefs = new ArrayList<>())
1894               : primaryRefs);
1895       primaries.clear();
1896       if (tmpList == null)
1897       {
1898         tmpList = new ArrayList<>();
1899         tmpList.add(null); // for replacement
1900       }
1901       for (int i = 0, n = dbrefs.size(); i < n; i++)
1902       {
1903         DBRefEntry ref = dbrefs.get(i);
1904         if (!ref.isPrimaryCandidate())
1905         {
1906           continue;
1907         }
1908         if (ref.hasMap())
1909         {
1910           MapList mp = ref.getMap().getMap();
1911           if (mp.getFromLowest() > start || mp.getFromHighest() < end)
1912           {
1913             // map only involves a subsequence, so cannot be primary
1914             continue;
1915           }
1916         }
1917         // whilst it looks like it is a primary ref, we also sanity check type
1918         if (DBRefSource.PDB_CANONICAL_NAME
1919                 .equals(ref.getCanonicalSourceName()))
1920         {
1921           // PDB dbrefs imply there should be a PDBEntry associated
1922           // TODO: tighten PDB dbrefs
1923           // formally imply Jalview has actually downloaded and
1924           // parsed the pdb file. That means there should be a cached file
1925           // handle on the PDBEntry, and a real mapping between sequence and
1926           // extracted sequence from PDB file
1927           PDBEntry pdbentry = getPDBEntry(ref.getAccessionId());
1928           if (pdbentry == null || pdbentry.getFile() == null)
1929           {
1930             continue;
1931           }
1932         }
1933         else
1934         {
1935           // check standard protein or dna sources
1936           tmpList.set(0, ref);
1937           List<DBRefEntry> res = DBRefUtils.selectDbRefs(!isProtein(),
1938                   tmpList);
1939           if (res == null || res.get(0) != tmpList.get(0))
1940           {
1941             continue;
1942           }
1943         }
1944         primaries.add(ref);
1945       }
1946
1947       // version must be not null, as otherwise it will not be a candidate,
1948       // above
1949       DBRefUtils.ensurePrimaries(this, primaries);
1950       return primaries;
1951     }
1952   }
1953
1954   /**
1955    * {@inheritDoc}
1956    */
1957   @Override
1958   public List<SequenceFeature> findFeatures(int fromColumn, int toColumn,
1959           String... types)
1960   {
1961     int startPos = findPosition(fromColumn - 1); // convert base 1 to base 0
1962     int endPos = fromColumn == toColumn ? startPos
1963             : findPosition(toColumn - 1);
1964
1965     List<SequenceFeature> result = getFeatures().findFeatures(startPos,
1966             endPos, types);
1967
1968     /*
1969      * if end column is gapped, endPos may be to the right, 
1970      * and we may have included adjacent or enclosing features;
1971      * remove any that are not enclosing, non-contact features
1972      */
1973     boolean endColumnIsGapped = toColumn > 0 && toColumn <= sequence.length
1974             && Comparison.isGap(sequence[toColumn - 1]);
1975     if (endPos > this.end || endColumnIsGapped)
1976     {
1977       ListIterator<SequenceFeature> it = result.listIterator();
1978       while (it.hasNext())
1979       {
1980         SequenceFeature sf = it.next();
1981         int sfBegin = sf.getBegin();
1982         int sfEnd = sf.getEnd();
1983         int featureStartColumn = findIndex(sfBegin);
1984         if (featureStartColumn > toColumn)
1985         {
1986           it.remove();
1987         }
1988         else if (featureStartColumn < fromColumn)
1989         {
1990           int featureEndColumn = sfEnd == sfBegin ? featureStartColumn
1991                   : findIndex(sfEnd);
1992           if (featureEndColumn < fromColumn)
1993           {
1994             it.remove();
1995           }
1996           else if (featureEndColumn > toColumn && sf.isContactFeature())
1997           {
1998             /*
1999              * remove an enclosing feature if it is a contact feature
2000              */
2001             it.remove();
2002           }
2003         }
2004       }
2005     }
2006
2007     return result;
2008   }
2009
2010   /**
2011    * Invalidates any stale cursors (forcing recalculation) by incrementing the
2012    * token that has to match the one presented by the cursor
2013    */
2014   @Override
2015   public void sequenceChanged()
2016   {
2017     changeCount++;
2018   }
2019
2020   /**
2021    * {@inheritDoc}
2022    */
2023   @Override
2024   public int replace(char c1, char c2)
2025   {
2026     if (c1 == c2)
2027     {
2028       return 0;
2029     }
2030     int count = 0;
2031     synchronized (sequence)
2032     {
2033       for (int c = 0; c < sequence.length; c++)
2034       {
2035         if (sequence[c] == c1)
2036         {
2037           sequence[c] = c2;
2038           count++;
2039         }
2040       }
2041     }
2042     if (count > 0)
2043     {
2044       sequenceChanged();
2045     }
2046
2047     return count;
2048   }
2049
2050   @Override
2051   public String getSequenceStringFromIterator(Iterator<int[]> it)
2052   {
2053     StringBuilder newSequence = new StringBuilder();
2054     while (it.hasNext())
2055     {
2056       int[] block = it.next();
2057       if (it.hasNext())
2058       {
2059         newSequence.append(getSequence(block[0], block[1] + 1));
2060       }
2061       else
2062       {
2063         newSequence.append(getSequence(block[0], block[1]));
2064       }
2065     }
2066
2067     return newSequence.toString();
2068   }
2069
2070   @Override
2071   public int firstResidueOutsideIterator(Iterator<int[]> regions)
2072   {
2073     int start = 0;
2074
2075     if (!regions.hasNext())
2076     {
2077       return findIndex(getStart()) - 1;
2078     }
2079
2080     // Simply walk along the sequence whilst watching for region
2081     // boundaries
2082     int hideStart = getLength();
2083     int hideEnd = -1;
2084     boolean foundStart = false;
2085
2086     // step through the non-gapped positions of the sequence
2087     for (int i = getStart(); i <= getEnd() && (!foundStart); i++)
2088     {
2089       // get alignment position of this residue in the sequence
2090       int p = findIndex(i) - 1;
2091
2092       // update region start/end
2093       while (hideEnd < p && regions.hasNext())
2094       {
2095         int[] region = regions.next();
2096         hideStart = region[0];
2097         hideEnd = region[1];
2098       }
2099       if (hideEnd < p)
2100       {
2101         hideStart = getLength();
2102       }
2103       // update boundary for sequence
2104       if (p < hideStart)
2105       {
2106         start = p;
2107         foundStart = true;
2108       }
2109     }
2110
2111     if (foundStart)
2112     {
2113       return start;
2114     }
2115     // otherwise, sequence was completely hidden
2116     return 0;
2117   }
2118
2119   ////
2120   //// Contact Matrix Holder Boilerplate
2121   ////
2122   ContactMapHolderI cmholder = new ContactMapHolder();
2123
2124   @Override
2125   public Collection<ContactMatrixI> getContactMaps()
2126   {
2127     return cmholder.getContactMaps();
2128   }
2129
2130   @Override
2131   public ContactMatrixI getContactMatrixFor(AlignmentAnnotation ann)
2132   {
2133     return cmholder.getContactMatrixFor(ann);
2134   }
2135
2136   @Override
2137   public ContactListI getContactListFor(AlignmentAnnotation _aa, int column)
2138   {
2139     return cmholder.getContactListFor(_aa, column);
2140   }
2141
2142   @Override
2143   public AlignmentAnnotation addContactList(ContactMatrixI cm)
2144   {
2145     AlignmentAnnotation aa = cmholder.addContactList(cm);
2146
2147     Annotation _aa[] = new Annotation[getLength()];
2148
2149     for (int i = 0; i < _aa.length; _aa[i++] = new Annotation(0.0f))
2150     {
2151       ;
2152     }
2153     aa.annotations = _aa;
2154     aa.setSequenceRef(this);
2155     if (cm instanceof MappableContactMatrix
2156             && !((MappableContactMatrix) cm).hasReferenceSeq())
2157     {
2158       ((MappableContactMatrix) cm).setRefSeq(this);
2159     }
2160     aa.createSequenceMapping(this, getStart(), false);
2161     addAlignmentAnnotation(aa);
2162     return aa;
2163   }
2164
2165   @Override
2166   public void addContactListFor(AlignmentAnnotation annotation,
2167           ContactMatrixI cm)
2168   {
2169     cmholder.addContactListFor(annotation, cm);
2170   }
2171 }