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