update author list in license for (JAL-826)
[jalview.git] / src / jalview / datamodel / SeqCigar.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.7)
3  * Copyright (C) 2011 J Procter, AM Waterhouse, J Engelhardt, LM Lui, G Barton, M Clamp, S Searle
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 of the License, or (at your option) any later version.
10  * 
11  * Jalview is distributed in the hope that it will be useful, but 
12  * WITHOUT ANY WARRANTY; without even the implied warranty 
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
14  * PURPOSE.  See the GNU General Public License for more details.
15  * 
16  * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 package jalview.datamodel;
19
20 import java.util.Enumeration;
21 import java.util.Hashtable;
22 import java.util.Vector;
23
24 import jalview.analysis.*;
25 import jalview.util.*;
26
27 public class SeqCigar extends CigarSimple
28 {
29   /**
30    * start(inclusive) and end(exclusive) of subsequence on refseq
31    */
32   private int start, end;
33
34   private SequenceI refseq = null;
35
36   private Hashtable seqProps;
37
38   /**
39    * Reference dataset sequence for the cigar string
40    * 
41    * @return SequenceI
42    */
43   public SequenceI getRefSeq()
44   {
45     return refseq;
46   }
47
48   /**
49    * 
50    * @return int start index of cigar ops on refSeq
51    */
52   public int getStart()
53   {
54     return start;
55   }
56
57   /**
58    * 
59    * @return int end index (exclusive) of cigar ops on refSeq
60    */
61   public int getEnd()
62   {
63     return end;
64   }
65
66   /**
67    * Returns sequence as a string with cigar operations applied to it
68    * 
69    * @return String
70    */
71   public String getSequenceString(char GapChar)
72   {
73     return (length == 0) ? "" : (String) getSequenceAndDeletions(
74             refseq.getSequenceAsString(start, end), GapChar)[0];
75   }
76
77   /**
78    * recreates a gapped and edited version of RefSeq or null for an empty cigar
79    * string
80    * 
81    * @return SequenceI
82    */
83   public SequenceI getSeq(char GapChar)
84   {
85     Sequence seq;
86     if (refseq == null || length == 0)
87     {
88       return null;
89     }
90     Object[] edit_result = getSequenceAndDeletions(
91             refseq.getSequenceAsString(start, end), GapChar);
92     if (edit_result == null)
93     {
94       throw new Error(
95               "Implementation Error - unexpected null from getSequenceAndDeletions");
96     }
97     int bounds[] = (int[]) edit_result[1];
98     seq = new Sequence(refseq.getName(), (String) edit_result[0],
99             refseq.getStart() + start + bounds[0], refseq.getStart()
100                     + start + ((bounds[2] == 0) ? -1 : bounds[2]));
101     seq.setDescription(refseq.getDescription());
102     int sstart = seq.getStart(), send = seq.getEnd();
103     // seq.checkValidRange(); probably not needed
104     // recover local properties if present
105     if (seqProps != null)
106     {
107       // this recovers dataset sequence reference as well as local features,
108       // names, start/end settings.
109       SeqsetUtils.SeqCharacterUnhash(seq, seqProps);
110     }
111     // ensure dataset sequence is up to date from local reference
112     seq.setDatasetSequence(refseq);
113     seq.setStart(sstart);
114     seq.setEnd(send);
115     return seq;
116   }
117
118   /*
119    * We don't allow this - refseq is given at construction time only public void
120    * setSeq(SequenceI seq) { this.seq = seq; }
121    */
122   /**
123    * internal constructor - sets seq to a gapless sequence derived from seq and
124    * prepends any 'D' operations needed to get to the first residue of seq.
125    * 
126    * @param seq
127    *          SequenceI
128    * @param initialDeletion
129    *          true to mark initial dataset sequence residues as deleted in
130    *          subsequence
131    * @param _s
132    *          index of first position in seq
133    * @param _e
134    *          index after last position in (possibly gapped) seq
135    * @return true if gaps are present in seq
136    */
137   private boolean _setSeq(SequenceI seq, boolean initialDeletion, int _s,
138           int _e)
139   {
140     boolean hasgaps = false;
141     if (seq == null)
142     {
143       throw new Error("Implementation Error - _setSeq(null,...)");
144     }
145     if (_s < 0)
146     {
147       throw new Error("Implementation Error: _s=" + _s);
148     }
149     String seq_string = seq.getSequenceAsString();
150     if (_e == 0 || _e < _s || _e > seq_string.length())
151     {
152       _e = seq_string.length();
153     }
154     // resolve start and end positions relative to ungapped reference sequence
155     start = seq.findPosition(_s) - seq.getStart();
156     end = seq.findPosition(_e) - seq.getStart();
157     int l_ungapped = end - start;
158     // Find correct sequence to reference and correct start and end - if
159     // necessary
160     SequenceI ds = seq.getDatasetSequence();
161     if (ds == null)
162     {
163       // make a new dataset sequence
164       String ungapped = AlignSeq.extractGaps(
165               jalview.util.Comparison.GapChars, new String(seq_string));
166       l_ungapped = ungapped.length();
167       // check that we haven't just duplicated an ungapped sequence.
168       if (l_ungapped == seq.getLength())
169       {
170         ds = seq;
171       }
172       else
173       {
174         ds = new Sequence(seq.getName(), ungapped, seq.getStart(),
175                 seq.getStart() + ungapped.length() - 1);
176         // JBPNote: this would be consistent but may not be useful
177         // seq.setDatasetSequence(ds);
178       }
179     }
180     // add in offset between seq and the dataset sequence
181     if (ds.getStart() < seq.getStart())
182     {
183       int offset = seq.getStart() - ds.getStart();
184       if (initialDeletion)
185       {
186         // absolute cigar string
187         addDeleted(_s + offset);
188         start = 0;
189         end += offset;
190       }
191       else
192       {
193         // normal behaviour - just mark start and end subsequence
194         start += offset;
195         end += offset;
196
197       }
198
199     }
200
201     // any gaps to process ?
202     if (l_ungapped != (_e - _s))
203     {
204       hasgaps = true;
205     }
206
207     refseq = ds;
208     // copy over local properties for the sequence instance of the refseq
209     seqProps = SeqsetUtils.SeqCharacterHash(seq);
210     // Check offsets
211     if (end > ds.getLength())
212     {
213       throw new Error(
214               "SeqCigar: Possible implementation error: sequence is longer than dataset sequence");
215       // end = ds.getLength();
216     }
217
218     return hasgaps;
219   }
220
221   /**
222    * directly initialise a cigar object with a sequence of range, operation
223    * pairs and a sequence to apply it to. operation and range should be relative
224    * to the seq.getStart()'th residue of the dataset seq resolved from seq.
225    * 
226    * @param seq
227    *          SequenceI
228    * @param operation
229    *          char[]
230    * @param range
231    *          int[]
232    */
233   public SeqCigar(SequenceI seq, char operation[], int range[])
234   {
235     super();
236     if (seq == null)
237     {
238       throw new Error("Implementation Bug. Null seq !");
239     }
240     if (operation.length != range.length)
241     {
242       throw new Error(
243               "Implementation Bug. Cigar Operation list!= range list");
244     }
245
246     if (operation != null)
247     {
248       this.operation = new char[operation.length + _inc_length];
249       this.range = new int[operation.length + _inc_length];
250
251       if (_setSeq(seq, false, 0, 0))
252       {
253         throw new Error(
254                 "NOT YET Implemented: Constructing a Cigar object from a cigar string and a gapped sequence.");
255       }
256       for (int i = this.length, j = 0; j < operation.length; i++, j++)
257       {
258         char op = operation[j];
259         if (op != M && op != I && op != D)
260         {
261           throw new Error("Implementation Bug. Cigar Operation '" + j
262                   + "' '" + op + "' not one of '" + M + "', '" + I
263                   + "', or '" + D + "'.");
264         }
265         this.operation[i] = op;
266         this.range[i] = range[j];
267       }
268       this.length += operation.length;
269     }
270     else
271     {
272       this.operation = null;
273       this.range = null;
274       this.length = 0;
275       if (_setSeq(seq, false, 0, 0))
276       {
277         throw new Error(
278                 "NOT YET Implemented: Constructing a Cigar object from a cigar string and a gapped sequence.");
279       }
280     }
281   }
282
283   /**
284    * add range matched residues to cigar string
285    * 
286    * @param range
287    *          int
288    */
289   public void addMatch(int range)
290   {
291     this.addOperation(M, range);
292   }
293
294   /**
295    * Adds insertion and match operations based on seq to the cigar up to the
296    * endpos column of seq.
297    * 
298    * @param cigar
299    *          CigarBase
300    * @param seq
301    *          SequenceI
302    * @param startpos
303    *          int
304    * @param endpos
305    *          int
306    * @param initialDeletions
307    *          if true then initial deletions will be added from start of seq to
308    *          startpos
309    */
310   protected static void addSequenceOps(CigarBase cigar, SequenceI seq,
311           int startpos, int endpos, boolean initialDeletions)
312   {
313     char op = '\0';
314     int range = 0;
315     int p = 0, res = seq.getLength();
316
317     if (!initialDeletions)
318     {
319       p = startpos;
320     }
321
322     while (p <= endpos)
323     {
324       boolean isGap = (p < res) ? jalview.util.Comparison.isGap(seq
325               .getCharAt(p)) : true;
326       if ((startpos <= p) && (p <= endpos))
327       {
328         if (isGap)
329         {
330           if (range > 0 && op != I)
331           {
332             cigar.addOperation(op, range);
333             range = 0;
334           }
335           op = I;
336           range++;
337         }
338         else
339         {
340           if (range > 0 && op != M)
341           {
342             cigar.addOperation(op, range);
343             range = 0;
344           }
345           op = M;
346           range++;
347         }
348       }
349       else
350       {
351         if (!isGap)
352         {
353           if (range > 0 && op != D)
354           {
355             cigar.addOperation(op, range);
356             range = 0;
357           }
358           op = D;
359           range++;
360         }
361         else
362         {
363           // do nothing - insertions are not made in flanking regions
364         }
365       }
366       p++;
367     }
368     if (range > 0)
369     {
370       cigar.addOperation(op, range);
371     }
372   }
373
374   /**
375    * create a cigar string for given sequence
376    * 
377    * @param seq
378    *          SequenceI
379    */
380   public SeqCigar(SequenceI seq)
381   {
382     super();
383     if (seq == null)
384     {
385       throw new Error("Implementation error for new Cigar(SequenceI)");
386     }
387     _setSeq(seq, false, 0, 0);
388     // there is still work to do
389     addSequenceOps(this, seq, 0, seq.getLength() - 1, false);
390   }
391
392   /**
393    * Create Cigar from a range of gaps and residues on a sequence object
394    * 
395    * @param seq
396    *          SequenceI
397    * @param start
398    *          int - first column in range
399    * @param end
400    *          int - last column in range
401    */
402   public SeqCigar(SequenceI seq, int start, int end)
403   {
404     super();
405     if (seq == null)
406     {
407       throw new Error("Implementation error for new Cigar(SequenceI)");
408     }
409     _setSeq(seq, false, start, end + 1);
410     // there is still work to do
411     addSequenceOps(this, seq, start, end, false);
412   }
413
414   /**
415    * Create a cigar object from a cigar string like '[<I|D|M><range>]+' Will
416    * fail if the given seq already contains gaps (JBPNote: future implementation
417    * will fix)
418    * 
419    * @param seq
420    *          SequenceI object resolvable to a dataset sequence
421    * @param cigarString
422    *          String
423    * @return Cigar
424    */
425   public static SeqCigar parseCigar(SequenceI seq, String cigarString)
426           throws Exception
427   {
428     Object[] opsandrange = parseCigarString(cigarString);
429     return new SeqCigar(seq, (char[]) opsandrange[0],
430             (int[]) opsandrange[1]);
431   }
432
433   /**
434    * create an alignment from the given array of cigar sequences and gap
435    * character, and marking the given segments as visible in the given
436    * columselection.
437    * 
438    * @param alseqs
439    * @param gapCharacter
440    * @param colsel
441    *          - columnSelection where hidden regions are marked
442    * @param segments
443    *          - visible regions of alignment
444    * @return SequenceI[]
445    */
446   public static SequenceI[] createAlignmentSequences(SeqCigar[] alseqs,
447           char gapCharacter, ColumnSelection colsel, int[] segments)
448   {
449     SequenceI[] seqs = new SequenceI[alseqs.length];
450     StringBuffer[] g_seqs = new StringBuffer[alseqs.length];
451     String[] alseqs_string = new String[alseqs.length];
452     Object[] gs_regions = new Object[alseqs.length];
453     for (int i = 0; i < alseqs.length; i++)
454     {
455       alseqs_string[i] = alseqs[i].getRefSeq().getSequenceAsString(
456               alseqs[i].start, alseqs[i].end);
457       gs_regions[i] = alseqs[i].getSequenceAndDeletions(alseqs_string[i],
458               gapCharacter); // gapped sequence, {start, start col, end.
459       // endcol}, hidden regions {{start, end, col}})
460       if (gs_regions[i] == null)
461       {
462         throw new Error("Implementation error: " + i
463                 + "'th sequence Cigar has no operations.");
464       }
465       g_seqs[i] = new StringBuffer((String) ((Object[]) gs_regions[i])[0]); // the
466       // visible
467       // gapped
468       // sequence
469     }
470     // Now account for insertions. (well - deletions)
471     // this is complicated because we must keep track of shifted positions in
472     // each sequence
473     ShiftList shifts = new ShiftList();
474     for (int i = 0; i < alseqs.length; i++)
475     {
476       Object[] gs_region = ((Object[]) ((Object[]) gs_regions[i])[2]);
477       if (gs_region != null)
478
479       {
480         for (int hr = 0; hr < gs_region.length; hr++)
481         {
482           int[] region = (int[]) gs_region[hr];
483           char[] insert = new char[region[1] - region[0] + 1];
484           for (int s = 0; s < insert.length; s++)
485           {
486             insert[s] = gapCharacter;
487           }
488           int inspos = shifts.shift(region[2]); // resolve insertion position in
489           // current alignment frame of
490           // reference
491           for (int s = 0; s < alseqs.length; s++)
492           {
493             if (s != i)
494             {
495               if (g_seqs[s].length() <= inspos)
496               {
497                 // prefix insertion with more gaps.
498                 for (int l = inspos - g_seqs[s].length(); l > 0; l--)
499                 {
500                   g_seqs[s].append(gapCharacter); // to debug - use a diffferent
501                   // gap character here
502                 }
503               }
504               g_seqs[s].insert(inspos, insert);
505             }
506             else
507             {
508               g_seqs[s].insert(inspos,
509                       alseqs_string[i].substring(region[0], region[1] + 1));
510             }
511           }
512           shifts.addShift(region[2], insert.length); // update shift in
513           // alignment frame of
514           // reference
515           if (segments == null)
516           {
517             // add a hidden column for this deletion
518             colsel.hideColumns(inspos, inspos + insert.length - 1);
519           }
520         }
521       }
522     }
523     for (int i = 0; i < alseqs.length; i++)
524     {
525       int[] bounds = ((int[]) ((Object[]) gs_regions[i])[1]);
526       SequenceI ref = alseqs[i].getRefSeq();
527       seqs[i] = new Sequence(ref.getName(), g_seqs[i].toString(),
528               ref.getStart() + alseqs[i].start + bounds[0], ref.getStart()
529                       + alseqs[i].start + (bounds[2] == 0 ? -1 : bounds[2]));
530       seqs[i].setDatasetSequence(ref);
531       seqs[i].setDescription(ref.getDescription());
532     }
533     if (segments != null)
534     {
535       for (int i = 0; i < segments.length; i += 3)
536       {
537         // int start=shifts.shift(segments[i]-1)+1;
538         // int end=shifts.shift(segments[i]+segments[i+1]-1)-1;
539         colsel.hideColumns(segments[i + 1], segments[i + 1]
540                 + segments[i + 2] - 1);
541       }
542     }
543     return seqs;
544   }
545
546   /**
547    * non rigorous testing
548    */
549   /**
550    * 
551    * @param seq
552    *          Sequence
553    * @param ex_cs_gapped
554    *          String
555    * @return String
556    */
557   public static String testCigar_string(Sequence seq, String ex_cs_gapped)
558   {
559     SeqCigar c_sgapped = new SeqCigar(seq);
560     String cs_gapped = c_sgapped.getCigarstring();
561     if (!cs_gapped.equals(ex_cs_gapped))
562     {
563       System.err.println("Failed getCigarstring: incorect string '"
564               + cs_gapped + "' != " + ex_cs_gapped);
565     }
566     return cs_gapped;
567   }
568
569   public static boolean testSeqRecovery(SeqCigar gen_sgapped,
570           SequenceI s_gapped)
571   {
572     // this is non-rigorous - start and end recovery is not tested.
573     SequenceI gen_sgapped_s = gen_sgapped.getSeq('-');
574     if (!gen_sgapped_s.getSequence().equals(s_gapped.getSequence()))
575     {
576       System.err.println("Couldn't reconstruct sequence.\n"
577               + gen_sgapped_s.getSequenceAsString() + "\n"
578               + s_gapped.getSequenceAsString());
579       return false;
580     }
581     return true;
582   }
583
584   public static void main(String argv[]) throws Exception
585   {
586     String o_seq;
587     Sequence s = new Sequence("MySeq",
588             o_seq = "asdfktryasdtqwrtsaslldddptyipqqwaslchvhttt", 39, 80);
589     String orig_gapped;
590     Sequence s_gapped = new Sequence(
591             "MySeq",
592             orig_gapped = "----asdf------ktryas---dtqwrtsasll----dddptyipqqwa----slchvhttt",
593             39, 80);
594     String ex_cs_gapped = "4I4M6I6M3I11M4I12M4I9M";
595     s_gapped.setDatasetSequence(s);
596     String sub_gapped_s;
597     Sequence s_subsequence_gapped = new Sequence(
598             "MySeq",
599             sub_gapped_s = "------ktryas---dtqwrtsasll----dddptyipqqwa----slchvh",
600             43, 77);
601
602     s_subsequence_gapped.setDatasetSequence(s);
603     SeqCigar c_null = new SeqCigar(s);
604     String cs_null = c_null.getCigarstring();
605     if (!cs_null.equals("42M"))
606     {
607       System.err
608               .println("Failed to recover ungapped sequence cigar operations:"
609                       + ((cs_null == "") ? "empty string" : cs_null));
610     }
611     testCigar_string(s_gapped, ex_cs_gapped);
612     SeqCigar gen_sgapped = SeqCigar.parseCigar(s, ex_cs_gapped);
613     if (!gen_sgapped.getCigarstring().equals(ex_cs_gapped))
614     {
615       System.err.println("Failed parseCigar(" + ex_cs_gapped
616               + ")->getCigarString()->'" + gen_sgapped.getCigarstring()
617               + "'");
618     }
619     testSeqRecovery(gen_sgapped, s_gapped);
620     // Test dataset resolution
621     SeqCigar sub_gapped = new SeqCigar(s_subsequence_gapped);
622     if (!testSeqRecovery(sub_gapped, s_subsequence_gapped))
623     {
624       System.err
625               .println("Failed recovery for subsequence of dataset sequence");
626     }
627     // width functions
628     if (sub_gapped.getWidth() != sub_gapped_s.length())
629     {
630       System.err.println("Failed getWidth()");
631     }
632
633     sub_gapped.getFullWidth();
634     if (sub_gapped.hasDeletedRegions())
635     {
636       System.err.println("hasDeletedRegions is incorrect.");
637     }
638     // Test start-end region SeqCigar
639     SeqCigar sub_se_gp = new SeqCigar(s_subsequence_gapped, 8, 48);
640     if (sub_se_gp.getWidth() != 41)
641     {
642       System.err
643               .println("SeqCigar(seq, start, end) not properly clipped alignsequence.");
644     }
645     System.out.println("Original sequence align:\n" + sub_gapped_s
646             + "\nReconstructed window from 8 to 48\n" + "XXXXXXXX"
647             + sub_se_gp.getSequenceString('-') + "..." + "\nCigar String:"
648             + sub_se_gp.getCigarstring() + "\n");
649     SequenceI ssgp = sub_se_gp.getSeq('-');
650     System.out.println("\t " + ssgp.getSequenceAsString());
651     for (int r = 0; r < 10; r++)
652     {
653       sub_se_gp = new SeqCigar(s_subsequence_gapped, 8, 48);
654       int sl = sub_se_gp.getWidth();
655       int st = sl - 1 - r;
656       for (int rs = 0; rs < 10; rs++)
657       {
658         int e = st + rs;
659         sub_se_gp.deleteRange(st, e);
660         String ssgapedseq = sub_se_gp.getSeq('-').getSequenceAsString();
661         System.out.println(st + "," + e + "\t:" + ssgapedseq);
662         st -= 3;
663       }
664     }
665     {
666       SeqCigar[] set = new SeqCigar[]
667       { new SeqCigar(s), new SeqCigar(s_subsequence_gapped, 8, 48),
668           new SeqCigar(s_gapped) };
669       Alignment al = new Alignment(set);
670       for (int i = 0; i < al.getHeight(); i++)
671       {
672         System.out.println("" + al.getSequenceAt(i).getName() + "\t"
673                 + al.getSequenceAt(i).getStart() + "\t"
674                 + al.getSequenceAt(i).getEnd() + "\t"
675                 + al.getSequenceAt(i).getSequenceAsString());
676       }
677     }
678     {
679       System.out.println("Gapped.");
680       SeqCigar[] set = new SeqCigar[]
681       { new SeqCigar(s), new SeqCigar(s_subsequence_gapped, 8, 48),
682           new SeqCigar(s_gapped) };
683       set[0].deleteRange(20, 25);
684       Alignment al = new Alignment(set);
685       for (int i = 0; i < al.getHeight(); i++)
686       {
687         System.out.println("" + al.getSequenceAt(i).getName() + "\t"
688                 + al.getSequenceAt(i).getStart() + "\t"
689                 + al.getSequenceAt(i).getEnd() + "\t"
690                 + al.getSequenceAt(i).getSequenceAsString());
691       }
692     }
693     // if (!ssgapedseq.equals("ryas---dtqqwa----slchvh"))
694     // System.err.println("Subseqgaped\n------ktryas---dtqwrtsasll----dddptyipqqwa----slchvhryas---dtqwrtsasll--qwa----slchvh\n"+ssgapedseq+"\n"+sub_se_gp.getCigarstring());
695   }
696
697   /**
698    * references to entities that this sequence cigar is associated with.
699    */
700   private Hashtable selGroups = null;
701
702   public void setGroupMembership(Object group)
703   {
704     if (selGroups == null)
705     {
706       selGroups = new Hashtable();
707     }
708     selGroups.put(group, new int[0]);
709   }
710
711   /**
712    * Test for and if present remove association to group.
713    * 
714    * @param group
715    * @return true if group was associated and it was removed
716    */
717   public boolean removeGroupMembership(Object group)
718   {
719     if (selGroups != null && selGroups.containsKey(group))
720     {
721       selGroups.remove(group);
722       return true;
723     }
724     return false;
725   }
726
727   /**
728    * forget all associations for this sequence.
729    */
730   public void clearMemberships()
731   {
732     if (selGroups != null)
733     {
734       selGroups.clear();
735     }
736     selGroups = null;
737   }
738
739   /**
740    * 
741    * @return null or array of all associated entities
742    */
743   public Object[] getAllMemberships()
744   {
745     if (selGroups == null)
746     {
747       return null;
748     }
749     Object[] mmbs = new Object[selGroups.size()];
750     Enumeration en = selGroups.keys();
751     for (int i = 0; en.hasMoreElements(); i++)
752     {
753       mmbs[i] = en.nextElement();
754     }
755     return mmbs;
756   }
757
758   /**
759    * Test for group membership
760    * 
761    * @param sgr
762    *          - a selection group or some other object that may be associated
763    *          with seqCigar
764    * @return true if sgr is associated with this seqCigar
765    */
766   public boolean isMemberOf(Object sgr)
767   {
768     return (selGroups != null) && selGroups.get(sgr) != null;
769   }
770 }