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