050603ef54d2236a2e439cc2bac72d824e4c15f7
[jalview.git] / src / jalview / datamodel / HiddenColumns.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.BitSet;
25 import java.util.Iterator;
26 import java.util.List;
27 import java.util.concurrent.locks.ReentrantReadWriteLock;
28
29 public class HiddenColumns
30 {
31   private static final int HASH_MULTIPLIER = 31;
32
33   private static final ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();
34
35   private HiddenColumnsCursor cursor = new HiddenColumnsCursor();
36
37   /*
38    * list of hidden column [start, end] ranges; the list is maintained in
39    * ascending start column order
40    */
41   private ArrayList<int[]> hiddenColumns;
42
43   /**
44    * Constructor
45    */
46   public HiddenColumns()
47   {
48   }
49
50   /**
51    * Copy constructor
52    * 
53    * @param copy
54    */
55   public HiddenColumns(HiddenColumns copy)
56   {
57     try
58     {
59       LOCK.writeLock().lock();
60       if (copy != null)
61       {
62         if (copy.hiddenColumns != null)
63         {
64           hiddenColumns = new ArrayList<>();
65           Iterator<int[]> it = copy.iterator();
66           while (it.hasNext())
67           {
68             hiddenColumns.add(it.next());
69           }
70           cursor.resetCursor(hiddenColumns);
71         }
72       }
73     } finally
74     {
75       LOCK.writeLock().unlock();
76     }
77   }
78
79   /**
80    * Copy constructor within bounds and with offset. Copies hidden column
81    * regions fully contained between start and end, and offsets positions by
82    * subtracting offset.
83    * 
84    * @param copy
85    *          HiddenColumns instance to copy from
86    * @param start
87    *          lower bound to copy from
88    * @param end
89    *          upper bound to copy to
90    * @param offset
91    *          offset to subtract from each region boundary position
92    * 
93    */
94   public HiddenColumns(HiddenColumns copy, int start, int end, int offset)
95   {
96     try
97     {
98       LOCK.writeLock().lock();
99       if (copy != null)
100       {
101         hiddenColumns = new ArrayList<>();
102         Iterator<int[]> it = copy.getBoundedIterator(start, end);
103         while (it.hasNext())
104         {
105           int[] region = it.next();
106           // still need to check boundaries because iterator returns
107           // all overlapping regions and we need contained regions
108           if (region[0] >= start && region[1] <= end)
109           {
110             hiddenColumns.add(
111                     new int[]
112             { region[0] - offset, region[1] - offset });
113           }
114         }
115         cursor.resetCursor(hiddenColumns);
116       }
117     } finally
118     {
119       LOCK.writeLock().unlock();
120     }
121   }
122
123   /**
124    * Output regions data as a string. String is in the format:
125    * reg0[0]<between>reg0[1]<delimiter>reg1[0]<between>reg1[1] ... regn[1]
126    * 
127    * @param delimiter
128    *          string to delimit regions
129    * @param betweenstring
130    *          to put between start and end region values
131    * @return regions formatted according to delimiter and between strings
132    */
133   public String regionsToString(String delimiter, String between)
134   {
135     try
136     {
137       LOCK.readLock().lock();
138       StringBuilder regionBuilder = new StringBuilder();
139       if (hiddenColumns != null)
140       {
141         Iterator<int[]> it = hiddenColumns.iterator();
142         while (it.hasNext())
143         {
144           int[] range = it.next();
145           regionBuilder.append(delimiter).append(range[0]).append(between)
146                   .append(range[1]);
147           if (!it.hasNext())
148           {
149             regionBuilder.deleteCharAt(0);
150           }
151         }
152       }
153       return regionBuilder.toString();
154     } finally
155     {
156       LOCK.readLock().unlock();
157     }
158   }
159
160   /**
161    * Find the number of hidden columns
162    * 
163    * @return number of hidden columns
164    */
165   public int getSize()
166   {
167     try
168     {
169       LOCK.readLock().lock();
170       int size = 0;
171       if (hiddenColumns != null)
172       {
173         Iterator<int[]> it = hiddenColumns.iterator();
174         while (it.hasNext())
175         {
176           int[] range = it.next();
177           size += range[1] - range[0] + 1;
178         }
179       }
180       return size;
181     } finally
182     {
183       LOCK.readLock().unlock();
184     }
185   }
186
187   /**
188    * Get the number of distinct hidden regions
189    * 
190    * @return number of regions
191    */
192   public int getNumberOfRegions()
193   {
194     try
195     {
196       LOCK.readLock().lock();
197       int num = 0;
198       if (hasHiddenColumns())
199       {
200         num = hiddenColumns.size();
201       }
202       return num;
203     } finally
204     {
205       LOCK.readLock().unlock();
206     }
207   }
208
209   @Override
210   public boolean equals(Object obj)
211   {
212     try
213     {
214       LOCK.readLock().lock();
215
216       if (!(obj instanceof HiddenColumns))
217       {
218         return false;
219       }
220       HiddenColumns that = (HiddenColumns) obj;
221
222       /*
223        * check hidden columns are either both null, or match
224        */
225       if (this.hiddenColumns == null)
226       {
227         return (that.hiddenColumns == null);
228       }
229       if (that.hiddenColumns == null
230               || that.hiddenColumns.size() != this.hiddenColumns.size())
231       {
232         return false;
233       }
234
235       Iterator<int[]> it = hiddenColumns.iterator();
236       Iterator<int[]> thatit = that.iterator();
237       while (it.hasNext())
238       {
239         int[] thisRange = it.next();
240         int[] thatRange = thatit.next();
241         if (thisRange[0] != thatRange[0] || thisRange[1] != thatRange[1])
242         {
243           return false;
244         }
245       }
246       return true;
247     } finally
248     {
249       LOCK.readLock().unlock();
250     }
251   }
252
253   /**
254    * Return absolute column index for a visible column index
255    * 
256    * @param column
257    *          int column index in alignment view (count from zero)
258    * @return alignment column index for column
259    */
260   public int adjustForHiddenColumns(int column)
261   {
262     try
263     {
264       LOCK.readLock().lock();
265       int result = column;
266
267       if (hiddenColumns != null)
268       {
269         result += cursor.getHiddenOffset(column);
270       }
271
272       return result;
273     } finally
274     {
275       LOCK.readLock().unlock();
276     }
277   }
278
279   /**
280    * Use this method to find out where a column will appear in the visible
281    * alignment when hidden columns exist. If the column is not visible, then the
282    * left-most visible column will always be returned.
283    * 
284    * @param hiddenColumn
285    *          the column index in the full alignment including hidden columns
286    * @return the position of the column in the visible alignment
287    */
288   public int findColumnPosition(int hiddenColumn)
289   {
290     try
291     {
292       LOCK.readLock().lock();
293       int result = hiddenColumn;
294
295       if (hiddenColumns != null)
296       {
297         int index = cursor.findRegionForColumn(hiddenColumn);
298         int hiddenBeforeCol = cursor.getHiddenSoFar();
299     
300         // just subtract hidden cols count - this works fine if column is
301         // visible
302         result = hiddenColumn - hiddenBeforeCol;
303     
304         // now check in case column is hidden - it will be in the returned
305         // hidden region
306         if (index < hiddenColumns.size())
307         {
308           int[] region = hiddenColumns.get(index);
309           if (hiddenColumn >= region[0] && hiddenColumn <= region[1])
310           {
311             // actually col is hidden, return region[0]-1
312             // unless region[0]==0 in which case return 0
313             if (region[0] == 0)
314             {
315               result = 0;
316             }
317             else
318             {
319               result = region[0] - 1 - hiddenBeforeCol;
320             }
321           }
322         }
323       }
324       return result; // return the shifted position after removing hidden
325                      // columns.
326     } finally
327     {
328       LOCK.readLock().unlock();
329     }
330   }
331
332   /**
333    * Find the visible column which is a given visible number of columns to the
334    * left of another visible column. i.e. for a startColumn x, the column which
335    * is distance 1 away will be column x-1.
336    * 
337    * @param visibleDistance
338    *          the number of visible columns to offset by
339    * @param startColumn
340    *          the column to start from
341    * @return the position of the column in the visible alignment
342    */
343   public int subtractVisibleColumns(int visibleDistance, int startColumn)
344   {
345     try
346     {
347       LOCK.readLock().lock();
348       int distance = visibleDistance;
349
350       // in case startColumn is in a hidden region, move it to the left
351       int start = adjustForHiddenColumns(findColumnPosition(startColumn));
352
353       Iterator<int[]> it = new ReverseRegionsIterator(0, start,
354               hiddenColumns);
355
356       while (it.hasNext() && (distance > 0))
357       {
358         int[] region = it.next();
359
360         if (start > region[1])
361         {
362           // subtract the gap to right of region from distance
363           if (start - region[1] <= distance)
364           {
365             distance -= start - region[1];
366             start = region[0] - 1;
367           }
368           else
369           {
370             start = start - distance;
371             distance = 0;
372           }
373         }
374       }
375
376       return start - distance;
377
378     } finally
379     {
380       LOCK.readLock().unlock();
381     }
382   }
383
384   /**
385    * This method returns the rightmost limit of a region of an alignment with
386    * hidden columns. In otherwords, the next hidden column.
387    * 
388    * @param alPos
389    *          the absolute (visible) alignmentPosition to find the next hidden
390    *          column for
391    */
392   public int getHiddenBoundaryRight(int alPos)
393   {
394     try
395     {
396       LOCK.readLock().lock();
397       if (hiddenColumns != null)
398       {
399         int index = cursor.findRegionForColumn(alPos);
400         if (index < hiddenColumns.size())
401         {
402           int[] region = hiddenColumns.get(index);
403           if (alPos < region[0])
404           {
405             return region[0];
406           }
407           else if ((alPos <= region[1])
408                   && (index + 1 < hiddenColumns.size()))
409           {
410             // alPos is within a hidden region, return the next one
411             // if there is one
412             region = hiddenColumns.get(index + 1);
413             return region[0];
414           }
415         }
416       }
417       return alPos;
418     } finally
419     {
420       LOCK.readLock().unlock();
421     }
422   }
423
424   /**
425    * This method returns the leftmost limit of a region of an alignment with
426    * hidden columns. In otherwords, the previous hidden column.
427    * 
428    * @param alPos
429    *          the absolute (visible) alignmentPosition to find the previous
430    *          hidden column for
431    */
432   public int getHiddenBoundaryLeft(int alPos)
433   {
434     try
435     {
436       LOCK.readLock().lock();
437
438       if (hiddenColumns != null)
439       {
440         int index = cursor.findRegionForColumn(alPos);
441
442         if (index > 0)
443         {
444           int[] region = hiddenColumns.get(index - 1);
445           return region[1];
446         }
447       }
448       return alPos;
449     } finally
450     {
451       LOCK.readLock().unlock();
452     }
453   }
454
455   /**
456    * Adds the specified column range to the hidden columns collection
457    * 
458    * @param start
459    *          start of range to add (absolute position in alignment)
460    * @param end
461    *          end of range to add (absolute position in alignment)
462    */
463   public void hideColumns(int start, int end)
464   {
465     boolean wasAlreadyLocked = false;
466     try
467     {
468       // check if the write lock was already locked by this thread,
469       // as this method can be called internally in loops within HiddenColumns
470       if (!LOCK.isWriteLockedByCurrentThread())
471       {
472         LOCK.writeLock().lock();
473       }
474       else
475       {
476         wasAlreadyLocked = true;
477       }
478
479       if (hiddenColumns == null)
480       {
481         hiddenColumns = new ArrayList<>();
482       }
483
484       /*
485        * new range follows everything else; check first to avoid looping over whole hiddenColumns collection
486        */
487       if (hiddenColumns.isEmpty()
488               || start > hiddenColumns.get(hiddenColumns.size() - 1)[1])
489       {
490         hiddenColumns.add(new int[] { start, end });
491       }
492       else
493       {
494         /*
495          * traverse existing hidden ranges and insert / amend / append as
496          * appropriate
497          */
498         boolean added = false;
499         for (int i = 0; !added && i < hiddenColumns.size(); i++)
500         {
501           added = insertRangeAtRegion(i, start, end);
502         } // for
503       }
504       if (!wasAlreadyLocked)
505       {
506         cursor.resetCursor(hiddenColumns);
507       }
508     } finally
509     {
510       if (!wasAlreadyLocked)
511       {
512         LOCK.writeLock().unlock();
513       }
514     }
515   }
516
517   /**
518    * Insert [start, range] at the region at index i in hiddenColumns, if
519    * feasible
520    * 
521    * @param i
522    *          index to insert at
523    * @param start
524    *          start of range to insert
525    * @param end
526    *          end of range to insert
527    * @return true if range was successfully inserted
528    */
529   private boolean insertRangeAtRegion(int i, int start, int end)
530   {
531     boolean added = false;
532
533     int[] region = hiddenColumns.get(i);
534     if (end < region[0] - 1)
535     {
536       /*
537        * insert discontiguous preceding range
538        */
539       hiddenColumns.add(i, new int[] { start, end });
540       added = true;
541     }
542     else if (end <= region[1])
543     {
544       /*
545        * new range overlaps existing, or is contiguous preceding it - adjust
546        * start column
547        */
548       region[0] = Math.min(region[0], start);
549       added = true;
550     }
551     else if (start <= region[1] + 1)
552     {
553       /*
554        * new range overlaps existing, or is contiguous following it - adjust
555        * start and end columns
556        */
557       region[0] = Math.min(region[0], start);
558       region[1] = Math.max(region[1], end);
559
560       /*
561        * also update or remove any subsequent ranges 
562        * that are overlapped
563        */
564       while (i < hiddenColumns.size() - 1)
565       {
566         int[] nextRegion = hiddenColumns.get(i + 1);
567         if (nextRegion[0] > end + 1)
568         {
569           /*
570            * gap to next hidden range - no more to update
571            */
572           break;
573         }
574         region[1] = Math.max(nextRegion[1], end);
575         hiddenColumns.subList(i + 1, i + 2).clear();
576       }
577       added = true;
578     }
579     return added;
580   }
581
582   /**
583    * Answers if a column in the alignment is visible
584    * 
585    * @param column
586    *          absolute position of column in the alignment
587    * @return true if column is visible
588    */
589   public boolean isVisible(int column)
590   {
591     try
592     {
593       LOCK.readLock().lock();
594
595       Iterator<int[]> it = new RegionsIterator(column, column,
596               hiddenColumns, cursor);
597       while (it.hasNext())
598       {
599         int[] region = it.next();
600         if (column >= region[0] && column <= region[1])
601         {
602           return false;
603         }
604       }
605
606       return true;
607     } finally
608     {
609       LOCK.readLock().unlock();
610     }
611   }
612
613   /**
614    * Get the visible sections of a set of sequences
615    * 
616    * @param start
617    *          sequence position to start from
618    * @param end
619    *          sequence position to end at
620    * @param seqs
621    *          an array of sequences
622    * @return an array of strings encoding the visible parts of each sequence
623    */
624   public String[] getVisibleSequenceStrings(int start, int end,
625           SequenceI[] seqs)
626   {
627     try
628     {
629       LOCK.readLock().lock();
630       int iSize = seqs.length;
631       String[] selections = new String[iSize];
632       if (hiddenColumns != null && hiddenColumns.size() > 0)
633       {
634         for (int i = 0; i < iSize; i++)
635         {
636           StringBuffer visibleSeq = new StringBuffer();
637
638           Iterator<int[]> blocks = new VisibleContigsIterator(start,
639                   end + 1, hiddenColumns);
640
641           while (blocks.hasNext())
642           {
643             int[] block = blocks.next();
644             if (blocks.hasNext())
645             {
646               visibleSeq
647                       .append(seqs[i].getSequence(block[0], block[1] + 1));
648             }
649             else
650             {
651               visibleSeq
652                       .append(seqs[i].getSequence(block[0], block[1]));
653             }
654           }
655
656           selections[i] = visibleSeq.toString();
657         }
658       }
659       else
660       {
661         for (int i = 0; i < iSize; i++)
662         {
663           selections[i] = seqs[i].getSequenceAsString(start, end);
664         }
665       }
666
667       return selections;
668     } finally
669     {
670       LOCK.readLock().unlock();
671     }
672   }
673
674   /**
675    * Locate the first position visible for this sequence. If seq isn't visible
676    * then return the position of the left side of the hidden boundary region.
677    * 
678    * @param seq
679    *          sequence to find position for
680    * @return visible start position
681    */
682   public int locateVisibleStartOfSequence(SequenceI seq)
683   {
684     try
685     {
686       LOCK.readLock().lock();
687       int start = 0;
688
689       if (hiddenColumns == null || hiddenColumns.size() == 0)
690       {
691         return seq.findIndex(seq.getStart()) - 1;
692       }
693
694       // Simply walk along the sequence whilst watching for hidden column
695       // boundaries
696       Iterator<int[]> regions = hiddenColumns.iterator();
697       int hideStart = seq.getLength();
698       int hideEnd = -1;
699       int visPrev = 0;
700       int visNext = 0;
701       boolean foundStart = false;
702
703       // step through the non-gapped positions of the sequence
704       for (int i = seq.getStart(); i <= seq.getEnd() && (!foundStart); i++)
705       {
706         // get alignment position of this residue in the sequence
707         int p = seq.findIndex(i) - 1;
708
709         // update hidden region start/end
710         while (hideEnd < p && regions.hasNext())
711         {
712           int[] region = regions.next();
713           visPrev = visNext;
714           visNext += region[0] - visPrev;
715           hideStart = region[0];
716           hideEnd = region[1];
717         }
718         if (hideEnd < p)
719         {
720           hideStart = seq.getLength();
721         }
722         // update visible boundary for sequence
723         if (p < hideStart)
724         {
725           start = p;
726           foundStart = true;
727         }
728       }
729
730       if (foundStart)
731       {
732         return findColumnPosition(start);
733       }
734       // otherwise, sequence was completely hidden
735       return visPrev;
736     } finally
737     {
738       LOCK.readLock().unlock();
739     }
740   }
741
742   /**
743    * delete any columns in alignmentAnnotation that are hidden (including
744    * sequence associated annotation).
745    * 
746    * @param alignmentAnnotation
747    */
748   public void makeVisibleAnnotation(AlignmentAnnotation alignmentAnnotation)
749   {
750     makeVisibleAnnotation(0, alignmentAnnotation.annotations.length,
751             alignmentAnnotation);
752   }
753
754   /**
755    * delete any columns in alignmentAnnotation that are hidden (including
756    * sequence associated annotation).
757    * 
758    * @param start
759    *          remove any annotation to the right of this column
760    * @param end
761    *          remove any annotation to the left of this column
762    * @param alignmentAnnotation
763    *          the annotation to operate on
764    */
765   public void makeVisibleAnnotation(int start, int end,
766           AlignmentAnnotation alignmentAnnotation)
767   {
768     try
769     {
770       LOCK.readLock().lock();
771
772       int startFrom = start;
773       int endAt = end;
774
775       if (alignmentAnnotation.annotations != null)
776       {
777         if (hiddenColumns != null && hiddenColumns.size() > 0)
778         {
779           removeHiddenAnnotation(startFrom, endAt, alignmentAnnotation);
780         }
781         else
782         {
783           alignmentAnnotation.restrict(startFrom, endAt);
784         }
785       }
786     } finally
787     {
788       LOCK.readLock().unlock();
789     }
790   }
791
792   private void removeHiddenAnnotation(int start, int end,
793           AlignmentAnnotation alignmentAnnotation)
794   {
795     // mangle the alignmentAnnotation annotation array
796     ArrayList<Annotation[]> annels = new ArrayList<>();
797     Annotation[] els = null;
798
799     int w = 0;
800     
801     Iterator<int[]> blocks = new VisibleContigsIterator(start, end + 1,
802             hiddenColumns);
803
804     int copylength;
805     int annotationLength;
806     while (blocks.hasNext())
807     {
808       int[] block = blocks.next();
809       annotationLength = block[1] - block[0] + 1;
810     
811       if (blocks.hasNext())
812       {
813         // copy just the visible segment of the annotation row
814         copylength = annotationLength;
815       }
816       else
817       {
818         if (annotationLength + block[0] <= alignmentAnnotation.annotations.length)
819         {
820           // copy just the visible segment of the annotation row
821           copylength = annotationLength;
822         }
823         else
824         {
825           // copy to the end of the annotation row
826           copylength = alignmentAnnotation.annotations.length - block[0];
827         }
828       }
829       
830       els = new Annotation[annotationLength];
831       annels.add(els);
832       System.arraycopy(alignmentAnnotation.annotations, block[0], els, 0,
833               copylength);
834       w += annotationLength;
835     }
836     
837     if (w != 0)
838     {
839       alignmentAnnotation.annotations = new Annotation[w];
840
841       w = 0;
842       for (Annotation[] chnk : annels)
843       {
844         System.arraycopy(chnk, 0, alignmentAnnotation.annotations, w,
845                 chnk.length);
846         w += chnk.length;
847       }
848     }
849   }
850
851   /**
852    * 
853    * @return true if there are columns hidden
854    */
855   public boolean hasHiddenColumns()
856   {
857     try
858     {
859       LOCK.readLock().lock();
860       return hiddenColumns != null && hiddenColumns.size() > 0;
861     } finally
862     {
863       LOCK.readLock().unlock();
864     }
865   }
866
867   /**
868    * 
869    * @return true if there are more than one set of columns hidden
870    */
871   public boolean hasManyHiddenColumns()
872   {
873     try
874     {
875       LOCK.readLock().lock();
876       return hiddenColumns != null && hiddenColumns.size() > 1;
877     } finally
878     {
879       LOCK.readLock().unlock();
880     }
881   }
882
883   /**
884    * mark the columns corresponding to gap characters as hidden in the column
885    * selection
886    * 
887    * @param sr
888    */
889   public void hideInsertionsFor(SequenceI sr)
890   {
891     try
892     {
893       LOCK.writeLock().lock();
894       List<int[]> inserts = sr.getInsertions();
895       for (int[] r : inserts)
896       {
897         hideColumns(r[0], r[1]);
898       }
899       cursor.resetCursor(hiddenColumns);
900     } finally
901     {
902       LOCK.writeLock().unlock();
903     }
904   }
905
906   /**
907    * Unhides, and adds to the selection list, all hidden columns
908    */
909   public void revealAllHiddenColumns(ColumnSelection sel)
910   {
911     try
912     {
913       LOCK.writeLock().lock();
914       if (hiddenColumns != null)
915       {
916         Iterator<int[]> it = hiddenColumns.iterator();
917         while (it.hasNext())
918         {
919           int[] region = it.next();
920           for (int j = region[0]; j < region[1] + 1; j++)
921           {
922             sel.addElement(j);
923           }
924         }
925         hiddenColumns = null;
926         cursor.resetCursor(hiddenColumns);
927       }
928     } finally
929     {
930       LOCK.writeLock().unlock();
931     }
932   }
933
934   /**
935    * Reveals, and marks as selected, the hidden column range with the given
936    * start column
937    * 
938    * @param start
939    */
940   public void revealHiddenColumns(int start, ColumnSelection sel)
941   {
942     try
943     {
944       LOCK.writeLock().lock();
945
946       if (hiddenColumns != null)
947       {
948         int regionIndex = cursor.findRegionForColumn(start);
949
950         if (regionIndex != -1 && regionIndex != hiddenColumns.size())
951         {
952           // regionIndex is the region which either contains start
953           // or lies to the right of start
954           int[] region = hiddenColumns.get(regionIndex);
955           if (start == region[0])
956           {
957             for (int j = region[0]; j < region[1] + 1; j++)
958             {
959               sel.addElement(j);
960             }
961             hiddenColumns.remove(regionIndex);
962
963             if (hiddenColumns.isEmpty())
964             {
965               hiddenColumns = null;
966             }
967             cursor.updateForDeletedRegion(hiddenColumns);
968           }
969         }
970       }
971     } finally
972     {
973       LOCK.writeLock().unlock();
974     }
975   }
976
977   /**
978    * Add gaps into the sequences aligned to profileseq under the given
979    * AlignmentView
980    * 
981    * @param profileseq
982    * @param al
983    *          - alignment to have gaps inserted into it
984    * @param input
985    *          - alignment view where sequence corresponding to profileseq is
986    *          first entry
987    * @return new HiddenColumns for new alignment view, with insertions into
988    *         profileseq marked as hidden.
989    */
990   public static HiddenColumns propagateInsertions(SequenceI profileseq,
991           AlignmentI al, AlignmentView input)
992   {
993     int profsqpos = 0;
994
995     char gc = al.getGapCharacter();
996     Object[] alandhidden = input.getAlignmentAndHiddenColumns(gc);
997     HiddenColumns nview = (HiddenColumns) alandhidden[1];
998     SequenceI origseq = ((SequenceI[]) alandhidden[0])[profsqpos];
999     nview.propagateInsertions(profileseq, al, origseq);
1000     return nview;
1001   }
1002
1003   /**
1004    * 
1005    * @param profileseq
1006    *          - sequence in al which corresponds to origseq
1007    * @param al
1008    *          - alignment which is to have gaps inserted into it
1009    * @param origseq
1010    *          - sequence corresponding to profileseq which defines gap map for
1011    *          modifying al
1012    */
1013   private void propagateInsertions(SequenceI profileseq, AlignmentI al,
1014           SequenceI origseq)
1015   {
1016     try
1017     {
1018       LOCK.writeLock().lock();
1019
1020       char gc = al.getGapCharacter();
1021
1022       // take the set of hidden columns, and the set of gaps in origseq,
1023       // and remove all the hidden gaps from hiddenColumns
1024
1025       // first get the gaps as a Bitset
1026       BitSet gaps = origseq.gapBitset();
1027
1028       // now calculate hidden ^ not(gap)
1029       BitSet hidden = new BitSet();
1030       markHiddenRegions(hidden);
1031       hidden.andNot(gaps);
1032       hiddenColumns = null;
1033       this.hideMarkedBits(hidden);
1034
1035       // for each sequence in the alignment, except the profile sequence,
1036       // insert gaps corresponding to each hidden region
1037       // but where each hidden column region is shifted backwards by the number
1038       // of
1039       // preceding visible gaps
1040       // update hidden columns at the same time
1041       Iterator<int[]> regions = hiddenColumns.iterator();
1042       ArrayList<int[]> newhidden = new ArrayList<>();
1043
1044       int numGapsBefore = 0;
1045       int gapPosition = 0;
1046       while (regions.hasNext())
1047       {
1048         // get region coordinates accounting for gaps
1049         // we can rely on gaps not being *in* hidden regions because we already
1050         // removed those
1051         int[] region = regions.next();
1052         while (gapPosition < region[0])
1053         {
1054           gapPosition++;
1055           if (gaps.get(gapPosition))
1056           {
1057             numGapsBefore++;
1058           }
1059         }
1060
1061         int left = region[0] - numGapsBefore;
1062         int right = region[1] - numGapsBefore;
1063         newhidden.add(new int[] { left, right });
1064
1065         // make a string with number of gaps = length of hidden region
1066         StringBuffer sb = new StringBuffer();
1067         for (int s = 0; s < right - left + 1; s++)
1068         {
1069           sb.append(gc);
1070         }
1071         padGaps(sb, left, profileseq, al);
1072
1073       }
1074       hiddenColumns = newhidden;
1075       cursor.resetCursor(hiddenColumns);
1076     } finally
1077     {
1078       LOCK.writeLock().unlock();
1079     }
1080   }
1081
1082   /**
1083    * Pad gaps in all sequences in alignment except profileseq
1084    * 
1085    * @param sb
1086    *          gap string to insert
1087    * @param left
1088    *          position to insert at
1089    * @param profileseq
1090    *          sequence not to pad
1091    * @param al
1092    *          alignment to pad sequences in
1093    */
1094   private void padGaps(StringBuffer sb, int pos, SequenceI profileseq,
1095           AlignmentI al)
1096   {
1097     // loop over the sequences and pad with gaps where required
1098     for (int s = 0, ns = al.getHeight(); s < ns; s++)
1099     {
1100       SequenceI sqobj = al.getSequenceAt(s);
1101       if (sqobj != profileseq)
1102       {
1103         String sq = al.getSequenceAt(s).getSequenceAsString();
1104         if (sq.length() <= pos)
1105         {
1106           // pad sequence
1107           int diff = pos - sq.length() - 1;
1108           if (diff > 0)
1109           {
1110             // pad gaps
1111             sq = sq + sb;
1112             while ((diff = pos - sq.length() - 1) > 0)
1113             {
1114               if (diff >= sb.length())
1115               {
1116                 sq += sb.toString();
1117               }
1118               else
1119               {
1120                 char[] buf = new char[diff];
1121                 sb.getChars(0, diff, buf, 0);
1122                 sq += buf.toString();
1123               }
1124             }
1125           }
1126           sq += sb.toString();
1127         }
1128         else
1129         {
1130           al.getSequenceAt(s).setSequence(
1131                   sq.substring(0, pos) + sb.toString() + sq.substring(pos));
1132         }
1133       }
1134     }
1135   }
1136
1137   /**
1138    * Returns a hashCode built from hidden column ranges
1139    */
1140   @Override
1141   public int hashCode()
1142   {
1143     try
1144     {
1145       LOCK.readLock().lock();
1146       int hashCode = 1;
1147       Iterator<int[]> it = hiddenColumns.iterator();
1148       while (it.hasNext())
1149       {
1150         int[] hidden = it.next();
1151         hashCode = HASH_MULTIPLIER * hashCode + hidden[0];
1152         hashCode = HASH_MULTIPLIER * hashCode + hidden[1];
1153       }
1154       return hashCode;
1155     } finally
1156     {
1157       LOCK.readLock().unlock();
1158     }
1159   }
1160
1161   /**
1162    * Hide columns corresponding to the marked bits
1163    * 
1164    * @param inserts
1165    *          - columns map to bits starting from zero
1166    */
1167   public void hideMarkedBits(BitSet inserts)
1168   {
1169     try
1170     {
1171       LOCK.writeLock().lock();
1172       for (int firstSet = inserts
1173               .nextSetBit(0), lastSet = 0; firstSet >= 0; firstSet = inserts
1174                       .nextSetBit(lastSet))
1175       {
1176         lastSet = inserts.nextClearBit(firstSet);
1177         hideColumns(firstSet, lastSet - 1);
1178       }
1179       cursor.resetCursor(hiddenColumns);
1180     } finally
1181     {
1182       LOCK.writeLock().unlock();
1183     }
1184   }
1185
1186   /**
1187    * 
1188    * @param inserts
1189    *          BitSet where hidden columns will be marked
1190    */
1191   public void markHiddenRegions(BitSet inserts)
1192   {
1193     try
1194     {
1195       LOCK.readLock().lock();
1196       if (hiddenColumns == null)
1197       {
1198         return;
1199       }
1200       Iterator<int[]> it = hiddenColumns.iterator();
1201       while (it.hasNext())
1202       {
1203         int[] range = it.next();
1204         inserts.set(range[0], range[1] + 1);
1205       }
1206     } finally
1207     {
1208       LOCK.readLock().unlock();
1209     }
1210   }
1211
1212   /**
1213    * Calculate the visible start and end index of an alignment.
1214    * 
1215    * @param width
1216    *          full alignment width
1217    * @return integer array where: int[0] = startIndex, and int[1] = endIndex
1218    */
1219   public int[] getVisibleStartAndEndIndex(int width)
1220   {
1221     try
1222     {
1223       LOCK.readLock().lock();
1224       int[] alignmentStartEnd = new int[] { 0, width - 1 };
1225       int startPos = alignmentStartEnd[0];
1226       int endPos = alignmentStartEnd[1];
1227
1228       int[] lowestRange = new int[] { -1, -1 };
1229       int[] higestRange = new int[] { -1, -1 };
1230
1231       if (hiddenColumns == null)
1232       {
1233         return new int[] { startPos, endPos };
1234       }
1235
1236       Iterator<int[]> it = hiddenColumns.iterator();
1237       while (it.hasNext())
1238       {
1239         int[] range = it.next();
1240         lowestRange = (range[0] <= startPos) ? range : lowestRange;
1241         higestRange = (range[1] >= endPos) ? range : higestRange;
1242       }
1243
1244       if (lowestRange[0] == -1 && lowestRange[1] == -1)
1245       {
1246         startPos = alignmentStartEnd[0];
1247       }
1248       else
1249       {
1250         startPos = lowestRange[1] + 1;
1251       }
1252
1253       if (higestRange[0] == -1 && higestRange[1] == -1)
1254       {
1255         endPos = alignmentStartEnd[1];
1256       }
1257       else
1258       {
1259         endPos = higestRange[0] - 1;
1260       }
1261       return new int[] { startPos, endPos };
1262     } finally
1263     {
1264       LOCK.readLock().unlock();
1265     }
1266   }
1267
1268   /**
1269    * Finds the hidden region (if any) which starts or ends at res
1270    * 
1271    * @param res
1272    *          visible residue position, unadjusted for hidden columns
1273    * @return region as [start,end] or null if no matching region is found
1274    */
1275   public int[] getRegionWithEdgeAtRes(int res)
1276   {
1277     try
1278     {
1279       LOCK.readLock().lock();
1280       int adjres = adjustForHiddenColumns(res);
1281
1282       int[] reveal = null;
1283
1284       if (hiddenColumns != null)
1285       {
1286         int regionindex = cursor.findRegionForColumn(adjres - 1);
1287         if (regionindex < hiddenColumns.size()
1288                 && hiddenColumns.get(regionindex)[1] == adjres - 1)
1289         {
1290           reveal = hiddenColumns.get(regionindex);
1291         }
1292         else
1293         {
1294           regionindex = cursor.findRegionForColumn(adjres + 1);
1295           if (regionindex < hiddenColumns.size()
1296                   && hiddenColumns.get(regionindex)[0] == adjres + 1)
1297           {
1298             reveal = hiddenColumns.get(regionindex);
1299           }
1300         }
1301       }
1302       return reveal;
1303
1304     } finally
1305     {
1306       LOCK.readLock().unlock();
1307     }
1308   }
1309
1310   /**
1311    * Return an iterator over the hidden regions
1312    */
1313   public Iterator<int[]> iterator()
1314   {
1315     try
1316     {
1317       LOCK.readLock().lock();
1318       return new BoundedHiddenColsIterator(hiddenColumns);
1319     } finally
1320     {
1321       LOCK.readLock().unlock();
1322     }
1323   }
1324
1325   /**
1326    * Return a bounded iterator over the hidden regions
1327    * 
1328    * @param start
1329    *          position to start from (inclusive, absolute column position)
1330    * @param end
1331    *          position to end at (inclusive, absolute column position)
1332    * @return
1333    */
1334   public Iterator<int[]> getBoundedIterator(int start, int end)
1335   {
1336     try
1337     {
1338       LOCK.readLock().lock();
1339       return new BoundedHiddenColsIterator(start, end, hiddenColumns);
1340     } finally
1341     {
1342       LOCK.readLock().unlock();
1343     }
1344   }
1345
1346   /**
1347    * Return a bounded iterator over the *visible* start positions of hidden
1348    * regions
1349    * 
1350    * @param start
1351    *          position to start from (inclusive, visible column position)
1352    * @param end
1353    *          position to end at (inclusive, visible column position)
1354    */
1355   public Iterator<Integer> getBoundedStartIterator(int start, int end)
1356   {
1357     try
1358     {
1359       LOCK.readLock().lock();
1360       return new BoundedStartRegionIterator(start, end, hiddenColumns);
1361     } finally
1362     {
1363       LOCK.readLock().unlock();
1364     }
1365   }
1366
1367   /**
1368    * Return an iterator over visible *columns* (not regions) between the given
1369    * start and end boundaries
1370    * 
1371    * @param start
1372    *          first column (inclusive)
1373    * @param end
1374    *          last column (inclusive)
1375    */
1376   public Iterator<Integer> getVisibleColsIterator(int start, int end)
1377   {
1378     try
1379     {
1380       LOCK.readLock().lock();
1381       return new VisibleColsIterator(start, end, hiddenColumns);
1382     } finally
1383     {
1384       LOCK.readLock().unlock();
1385     }
1386   }
1387
1388   /**
1389    * return an iterator over visible segments between the given start and end
1390    * boundaries
1391    * 
1392    * @param start
1393    *          (first column inclusive from 0)
1394    * @param end
1395    *          (last column - not inclusive)
1396    */
1397   public Iterator<int[]> getVisContigsIterator(int start, int end)
1398   {
1399     try
1400     {
1401       LOCK.readLock().lock();
1402       return new VisibleContigsIterator(start, end, hiddenColumns);
1403     } finally
1404     {
1405       LOCK.readLock().unlock();
1406     }
1407   }
1408
1409   /**
1410    * return an iterator over visible segments between the given start and end
1411    * boundaries
1412    * 
1413    * @param start
1414    *          (first column - inclusive from 0)
1415    * @param end
1416    *          (last column - inclusive)
1417    * @param useVisibleCoords
1418    *          if true, start and end are visible column positions, not absolute
1419    *          positions
1420    */
1421   public Iterator<int[]> getVisibleBlocksIterator(int start, int end,
1422           boolean useVisibleCoords)
1423   {
1424     if (useVisibleCoords)
1425     {
1426       // TODO
1427       // we should really just convert start and end here with
1428       // adjustForHiddenColumns
1429       // and then create a VisibleContigsIterator
1430       // but without a cursor this will be horribly slow in some situations
1431       // ... so until then...
1432       return new VisibleBlocksVisBoundsIterator(start, end, true);
1433     }
1434     else
1435     {
1436       try
1437       {
1438         LOCK.readLock().lock();
1439         return new VisibleContigsIterator(start, end + 1, hiddenColumns);
1440       } finally
1441     {
1442         LOCK.readLock().unlock();
1443       }
1444     }
1445   }
1446
1447   /**
1448    * An iterator which iterates over visible regions in a range. The range is
1449    * specified in terms of visible column positions. Provides a special
1450    * "endsAtHidden" indicator to allow callers to determine if the final visible
1451    * column is adjacent to a hidden region.
1452    */
1453   public class VisibleBlocksVisBoundsIterator implements Iterator<int[]>
1454   {
1455     private List<int[]> vcontigs = new ArrayList<>();
1456
1457     private int currentPosition = 0;
1458
1459     private boolean endsAtHidden = false;
1460
1461     /**
1462      * Constructor for iterator over visible regions in a range.
1463      * 
1464      * @param start
1465      *          start position in terms of visible column position
1466      * @param end
1467      *          end position in terms of visible column position
1468      * @param usecopy
1469      *          whether to use a local copy of hidden columns
1470      */
1471     VisibleBlocksVisBoundsIterator(int start, int end, boolean usecopy)
1472     {
1473       /* actually this implementation always uses a local copy but this may change in future */
1474       try
1475       {
1476         if (usecopy)
1477         {
1478           LOCK.readLock().lock();
1479         }
1480
1481         if (hiddenColumns != null && hiddenColumns.size() > 0)
1482         {
1483           int blockStart = start;
1484           int blockEnd = end;
1485           int hiddenSoFar = 0;
1486           int visSoFar = 0;
1487
1488           // iterate until a region begins within (start,end]
1489           int i = 0;
1490           while ((i < hiddenColumns.size())
1491                   && (hiddenColumns.get(i)[0] <= blockStart + hiddenSoFar))
1492           {
1493             hiddenSoFar += hiddenColumns.get(i)[1] - hiddenColumns.get(i)[0]
1494                     + 1;
1495             i++;
1496           }
1497
1498           blockStart += hiddenSoFar; // convert start to absolute position
1499           blockEnd += hiddenSoFar; // convert end to absolute position
1500
1501           // iterate from start to end, adding each visible region. Positions
1502           // are
1503           // absolute, and all hidden regions which overlap [start,end] are
1504           // used.
1505           while (i < hiddenColumns.size()
1506                   && (hiddenColumns.get(i)[0] <= blockEnd))
1507           {
1508             int[] region = hiddenColumns.get(i);
1509
1510             // end position of this visible region is either just before the
1511             // start of the next hidden region, or the absolute position of
1512             // 'end', whichever is lowest
1513             blockEnd = Math.min(blockEnd, region[0] - 1);
1514
1515             vcontigs.add(new int[] { blockStart, blockEnd });
1516
1517             visSoFar += blockEnd - blockStart + 1;
1518
1519             // next visible region starts after this hidden region
1520             blockStart = region[1] + 1;
1521
1522             hiddenSoFar += region[1] - region[0] + 1;
1523
1524             // reset blockEnd to absolute position of 'end', assuming we've now
1525             // passed all hidden regions before end
1526             blockEnd = end + hiddenSoFar;
1527
1528             i++;
1529           }
1530           if (visSoFar < end - start + 1)
1531           {
1532             // the number of visible columns we've accounted for is less than
1533             // the number specified by end-start; work out the end position of
1534             // the last visible region
1535             blockEnd = blockStart + end - start - visSoFar;
1536             vcontigs.add(new int[] { blockStart, blockEnd });
1537
1538             // if the last visible region ends at the next hidden region, set
1539             // endsAtHidden=true
1540             if (i < hiddenColumns.size()
1541                     && hiddenColumns.get(i)[0] - 1 == blockEnd)
1542             {
1543               endsAtHidden = true;
1544             }
1545           }
1546         }
1547         else
1548         {
1549           // there are no hidden columns, return a single visible contig
1550           vcontigs.add(new int[] { start, end });
1551           endsAtHidden = false;
1552         }
1553       } finally
1554       {
1555         if (usecopy)
1556         {
1557           LOCK.readLock().unlock();
1558         }
1559       }
1560     }
1561
1562     @Override
1563     public boolean hasNext()
1564     {
1565       return (currentPosition < vcontigs.size());
1566     }
1567
1568     @Override
1569     public int[] next()
1570     {
1571       int[] result = vcontigs.get(currentPosition);
1572       currentPosition++;
1573       return result;
1574     }
1575
1576     public boolean endsAtHidden()
1577     {
1578       return endsAtHidden;
1579     }
1580   }
1581 }