JAL-2759 Changes to getVisibleSequenceStrings after review
[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.Arrays;
25 import java.util.BitSet;
26 import java.util.Iterator;
27 import java.util.List;
28 import java.util.concurrent.locks.ReentrantReadWriteLock;
29
30 /**
31  * This class manages the collection of hidden columns associated with an
32  * alignment. To iterate over the collection, or over visible columns/regions,
33  * use an iterator obtained from one of:
34  * 
35  * - getBoundedIterator: iterates over the hidden regions, within some bounds,
36  * returning absolute positions
37  * 
38  * - getBoundedStartIterator: iterates over the start positions of hidden
39  * regions, within some bounds, returning visible positions
40  * 
41  * - getVisContigsIterator: iterates over visible regions in a range, returning
42  * absolute positions
43  * 
44  * - getVisibleColsIterator: iterates over the visible *columns*
45  * 
46  * For performance reasons, provide bounds where possible. Note that column
47  * numbering begins at 0 throughout this class.
48  * 
49  * @author kmourao
50  *
51  */
52 public class HiddenColumns
53 {
54   private static final int HASH_MULTIPLIER = 31;
55
56   private static final ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();
57
58   /*
59    * Cursor which tracks the last used hidden columns region, and the number 
60    * of hidden columns up to (but not including) that region.
61    */
62   private HiddenColumnsCursor cursor = new HiddenColumnsCursor();
63
64   /*
65    * cache of the number of hidden columns
66    */
67   private int numColumns = 0;
68
69   /*
70    * list of hidden column [start, end] ranges; the list is maintained in
71    * ascending start column order
72    */
73   private List<int[]> hiddenColumns = new ArrayList<>();
74
75   /**
76    * Constructor
77    */
78   public HiddenColumns()
79   {
80   }
81
82   /* 
83    * Methods which change the hiddenColumns collection. These methods should 
84    * use a writeLock to prevent other threads accessing the hiddenColumns
85    * collection while changes are being made. They should also reset the hidden
86    * columns cursor, and either update the hidden columns count, or set it to 0 
87    * (so that it will later be updated when needed).
88    */
89
90   /**
91    * Copy constructor
92    * 
93    * @param copy
94    *          the HiddenColumns object to copy from
95    */
96   public HiddenColumns(HiddenColumns copy)
97   {
98     this(copy, Integer.MIN_VALUE, Integer.MAX_VALUE, 0);
99   }
100
101   /**
102    * Copy constructor within bounds and with offset. Copies hidden column
103    * regions fully contained between start and end, and offsets positions by
104    * subtracting offset.
105    * 
106    * @param copy
107    *          HiddenColumns instance to copy from
108    * @param start
109    *          lower bound to copy from
110    * @param end
111    *          upper bound to copy to
112    * @param offset
113    *          offset to subtract from each region boundary position
114    * 
115    */
116   public HiddenColumns(HiddenColumns copy, int start, int end, int offset)
117   {
118     try
119     {
120       LOCK.writeLock().lock();
121       if (copy != null)
122       {
123         numColumns = 0;
124         Iterator<int[]> it = copy.getBoundedIterator(start, end);
125         while (it.hasNext())
126         {
127           int[] region = it.next();
128           // still need to check boundaries because iterator returns
129           // all overlapping regions and we need contained regions
130           if (region[0] >= start && region[1] <= end)
131           {
132             hiddenColumns.add(
133                     new int[]
134             { region[0] - offset, region[1] - offset });
135             numColumns += region[1] - region[0] + 1;
136           }
137         }
138         cursor.resetCursor(hiddenColumns);
139       }
140     } finally
141     {
142       LOCK.writeLock().unlock();
143     }
144   }
145
146   /**
147    * Adds the specified column range to the hidden columns collection
148    * 
149    * @param start
150    *          start of range to add (absolute position in alignment)
151    * @param end
152    *          end of range to add (absolute position in alignment)
153    */
154   public void hideColumns(int start, int end)
155   {
156     try
157     {
158       LOCK.writeLock().lock();
159
160       int previndex = 0;
161       int prevHiddenCount = 0;
162       int regionindex = 0;
163       if (!hiddenColumns.isEmpty())
164       {
165         // set up cursor reset values
166         HiddenCursorPosition cursorPos = cursor.findRegionForColumn(start);
167         regionindex = cursorPos.getRegionIndex();
168
169         if (regionindex > 0)
170         {
171           // get previous index and hidden count for updating the cursor later
172           previndex = regionindex - 1;
173           int[] prevRegion = hiddenColumns.get(previndex);
174           prevHiddenCount = cursorPos.getHiddenSoFar()
175                   - (prevRegion[1] - prevRegion[0] + 1);
176         }
177       }
178
179       /*
180        * new range follows everything else; check first to avoid looping over whole hiddenColumns collection
181        */
182       if (hiddenColumns.isEmpty()
183               || start > hiddenColumns.get(hiddenColumns.size() - 1)[1])
184       {
185         hiddenColumns.add(new int[] { start, end });
186       }
187       else
188       {
189         /*
190          * traverse existing hidden ranges and insert / amend / append as
191          * appropriate
192          */
193         boolean added = false;
194         if (regionindex > 0)
195         {
196           added = insertRangeAtRegion(regionindex - 1, start, end);
197         }
198         if (!added && regionindex < hiddenColumns.size())
199         {
200           insertRangeAtRegion(regionindex, start, end);
201         }
202       }
203
204       // reset the cursor to just before our insertion point: this saves
205       // a lot of reprocessing in large alignments
206       cursor.resetCursor(hiddenColumns, previndex, prevHiddenCount);
207
208       // reset the number of columns so they will be recounted
209       numColumns = 0;
210
211     } finally
212     {
213       LOCK.writeLock().unlock();
214     }
215   }
216
217   /**
218    * Insert [start, range] at the region at index i in hiddenColumns, if
219    * feasible
220    * 
221    * @param i
222    *          index to insert at
223    * @param start
224    *          start of range to insert
225    * @param end
226    *          end of range to insert
227    * @return true if range was successfully inserted
228    */
229   private boolean insertRangeAtRegion(int i, int start, int end)
230   {
231     boolean added = false;
232
233     int[] region = hiddenColumns.get(i);
234     if (end < region[0] - 1)
235     {
236       /*
237        * insert discontiguous preceding range
238        */
239       hiddenColumns.add(i, new int[] { start, end });
240       added = true;
241     }
242     else if (end <= region[1])
243     {
244       /*
245        * new range overlaps existing, or is contiguous preceding it - adjust
246        * start column
247        */
248       region[0] = Math.min(region[0], start);
249       added = true;
250     }
251     else if (start <= region[1] + 1)
252     {
253       /*
254        * new range overlaps existing, or is contiguous following it - adjust
255        * start and end columns
256        */
257       region[0] = Math.min(region[0], start);
258       region[1] = Math.max(region[1], end);
259
260       /*
261        * also update or remove any subsequent ranges 
262        * that are overlapped
263        */
264       while (i < hiddenColumns.size() - 1)
265       {
266         int[] nextRegion = hiddenColumns.get(i + 1);
267         if (nextRegion[0] > end + 1)
268         {
269           /*
270            * gap to next hidden range - no more to update
271            */
272           break;
273         }
274         region[1] = Math.max(nextRegion[1], end);
275
276         // in theory this is faster than hiddenColumns.remove(i+1)
277         // benchmarking results a bit ambivalent
278         hiddenColumns.subList(i + 1, i + 2).clear();
279       }
280       added = true;
281     }
282     return added;
283   }
284
285   /**
286    * hide a list of ranges
287    * 
288    * @param ranges
289    */
290   public void hideList(List<int[]> ranges)
291   {
292     try
293     {
294       LOCK.writeLock().lock();
295       for (int[] r : ranges)
296       {
297         hideColumns(r[0], r[1]);
298       }
299       cursor.resetCursor(hiddenColumns);
300       numColumns = 0;
301     } finally
302     {
303       LOCK.writeLock().unlock();
304     }
305   }
306
307   /**
308    * Unhides, and adds to the selection list, all hidden columns
309    */
310   public void revealAllHiddenColumns(ColumnSelection sel)
311   {
312     try
313     {
314       LOCK.writeLock().lock();
315
316       for (int[] region : hiddenColumns)
317       {
318         for (int j = region[0]; j < region[1] + 1; j++)
319         {
320           sel.addElement(j);
321         }
322       }
323       hiddenColumns.clear();
324       cursor.resetCursor(hiddenColumns);
325       numColumns = 0;
326
327     } finally
328     {
329       LOCK.writeLock().unlock();
330     }
331   }
332
333   /**
334    * Reveals, and marks as selected, the hidden column range with the given
335    * start column
336    * 
337    * @param start
338    *          the start column to look for
339    * @param sel
340    *          the column selection to add the hidden column range to
341    */
342   public void revealHiddenColumns(int start, ColumnSelection sel)
343   {
344     try
345     {
346       LOCK.writeLock().lock();
347
348       if (!hiddenColumns.isEmpty())
349       {
350         int regionIndex = cursor.findRegionForColumn(start)
351                 .getRegionIndex();
352
353         if (regionIndex != -1 && regionIndex != hiddenColumns.size())
354         {
355           // regionIndex is the region which either contains start
356           // or lies to the right of start
357           int[] region = hiddenColumns.get(regionIndex);
358           if (start == region[0])
359           {
360             for (int j = region[0]; j < region[1] + 1; j++)
361             {
362               sel.addElement(j);
363             }
364             int colsToRemove = region[1] - region[0] + 1;
365             hiddenColumns.remove(regionIndex);
366
367             if (hiddenColumns.isEmpty())
368             {
369               hiddenColumns.clear();
370               numColumns = 0;
371             }
372             else
373             {
374               numColumns -= colsToRemove;
375             }
376             cursor.updateForDeletedRegion(hiddenColumns);
377           }
378         }
379       }
380     } finally
381     {
382       LOCK.writeLock().unlock();
383     }
384   }
385
386
387
388
389   /*
390    * Methods which only need read access to the hidden columns collection. 
391    * These methods should use a readLock to prevent other threads changing
392    * the hidden columns collection while it is in use.
393    */
394
395   /**
396    * Output regions data as a string. String is in the format:
397    * reg0[0]<between>reg0[1]<delimiter>reg1[0]<between>reg1[1] ... regn[1]
398    * 
399    * @param delimiter
400    *          string to delimit regions
401    * @param betweenstring
402    *          to put between start and end region values
403    * @return regions formatted according to delimiter and between strings
404    */
405   public String regionsToString(String delimiter, String between)
406   {
407     try
408     {
409       LOCK.readLock().lock();
410       StringBuilder regionBuilder = new StringBuilder();
411
412       boolean first = true;
413       for (int[] range : hiddenColumns)
414       {
415         if (!first)
416         {
417           regionBuilder.append(delimiter);
418         }
419         else
420         {
421           first = false;
422         }
423         regionBuilder.append(range[0]).append(between).append(range[1]);
424
425       }
426
427       return regionBuilder.toString();
428     } finally
429     {
430       LOCK.readLock().unlock();
431     }
432   }
433
434   /**
435    * Find the number of hidden columns
436    * 
437    * @return number of hidden columns
438    */
439   public int getSize()
440   {
441     try
442     {
443       LOCK.readLock().lock();
444
445       if (numColumns == 0)
446       {
447         // numColumns is out of date, so recalculate
448         int size = 0;
449
450         for (int[] range : hiddenColumns)
451         {
452           size += range[1] - range[0] + 1;
453         }
454
455         numColumns = size;
456       }
457
458       return numColumns;
459     } finally
460     {
461       LOCK.readLock().unlock();
462     }
463   }
464
465   /**
466    * Get the number of distinct hidden regions
467    * 
468    * @return number of regions
469    */
470   public int getNumberOfRegions()
471   {
472     try
473     {
474       LOCK.readLock().lock();
475       return hiddenColumns.size();
476     } finally
477     {
478       LOCK.readLock().unlock();
479     }
480   }
481
482   @Override
483   public boolean equals(Object obj)
484   {
485     try
486     {
487       LOCK.readLock().lock();
488
489       if (!(obj instanceof HiddenColumns))
490       {
491         return false;
492       }
493       HiddenColumns that = (HiddenColumns) obj;
494
495       /*
496        * check hidden columns are either both null, or match
497        */
498
499       if (that.hiddenColumns.size() != this.hiddenColumns.size())
500       {
501         return false;
502       }
503
504       Iterator<int[]> it = this.iterator();
505       Iterator<int[]> thatit = that.iterator();
506       while (it.hasNext())
507       {
508         if (!(Arrays.equals(it.next(), thatit.next())))
509         {
510           return false;
511         }
512       }
513       return true;
514
515     } finally
516     {
517       LOCK.readLock().unlock();
518     }
519   }
520
521   /**
522    * Return absolute column index for a visible column index
523    * 
524    * @param column
525    *          int column index in alignment view (count from zero)
526    * @return alignment column index for column
527    */
528   public int visibleToAbsoluteColumn(int column)
529   {
530     try
531     {
532       LOCK.readLock().lock();
533       int result = column;
534
535       if (!hiddenColumns.isEmpty())
536       {
537         result += cursor.findRegionForVisColumn(column).getHiddenSoFar();
538       }
539
540       return result;
541     } finally
542     {
543       LOCK.readLock().unlock();
544     }
545   }
546
547   /**
548    * Use this method to find out where a column will appear in the visible
549    * alignment when hidden columns exist. If the column is not visible, then the
550    * index of the next visible column on the left will be returned (or 0 if
551    * there is no visible column on the left)
552    * 
553    * @param hiddenColumn
554    *          the column index in the full alignment including hidden columns
555    * @return the position of the column in the visible alignment
556    */
557   public int absoluteToVisibleColumn(int hiddenColumn)
558   {
559     try
560     {
561       LOCK.readLock().lock();
562       int result = hiddenColumn;
563
564       if (!hiddenColumns.isEmpty())
565       {
566         HiddenCursorPosition cursorPos = cursor
567                 .findRegionForColumn(hiddenColumn);
568         int index = cursorPos.getRegionIndex();
569         int hiddenBeforeCol = cursorPos.getHiddenSoFar();
570     
571         // just subtract hidden cols count - this works fine if column is
572         // visible
573         result = hiddenColumn - hiddenBeforeCol;
574     
575         // now check in case column is hidden - it will be in the returned
576         // hidden region
577         if (index < hiddenColumns.size())
578         {
579           int[] region = hiddenColumns.get(index);
580           if (hiddenColumn >= region[0] && hiddenColumn <= region[1])
581           {
582             // actually col is hidden, return region[0]-1
583             // unless region[0]==0 in which case return 0
584             if (region[0] == 0)
585             {
586               result = 0;
587             }
588             else
589             {
590               result = region[0] - 1 - hiddenBeforeCol;
591             }
592           }
593         }
594       }
595
596       return result; // return the shifted position after removing hidden
597                      // columns.
598     } finally
599     {
600       LOCK.readLock().unlock();
601     }
602   }
603
604   /**
605    * Find the visible column which is a given visible number of columns to the
606    * left (negative visibleDistance) or right (positive visibleDistance) of
607    * startColumn. If startColumn is not visible, we use the visible column at
608    * the left boundary of the hidden region containing startColumn.
609    * 
610    * @param visibleDistance
611    *          the number of visible columns to offset by (left offset = negative
612    *          value; right offset = positive value)
613    * @param startColumn
614    *          the position of the column to start from (absolute position)
615    * @return the position of the column which is <visibleDistance> away
616    *         (absolute position)
617    */
618   public int offsetByVisibleColumns(int visibleDistance, int startColumn)
619   {
620     try
621     {
622       LOCK.readLock().lock();
623       int start = absoluteToVisibleColumn(startColumn);
624       return visibleToAbsoluteColumn(start + visibleDistance);
625
626     } finally
627     {
628       LOCK.readLock().unlock();
629     }
630   }
631
632   /**
633    * This method returns the rightmost limit of a region of an alignment with
634    * hidden columns. In otherwords, the next hidden column.
635    * 
636    * @param alPos
637    *          the absolute (visible) alignmentPosition to find the next hidden
638    *          column for
639    * @return the index of the next hidden column, or alPos if there is no next
640    *         hidden column
641    */
642   public int getNextHiddenBoundary(boolean left, int alPos)
643   {
644     try
645     {
646       LOCK.readLock().lock();
647       if (!hiddenColumns.isEmpty())
648       {
649         int index = cursor.findRegionForColumn(alPos).getRegionIndex();
650
651         if (left && index > 0)
652         {
653           int[] region = hiddenColumns.get(index - 1);
654           return region[1];
655         }
656         else if (!left && index < hiddenColumns.size())
657         {
658           int[] region = hiddenColumns.get(index);
659           if (alPos < region[0])
660           {
661             return region[0];
662           }
663           else if ((alPos <= region[1])
664                   && (index + 1 < hiddenColumns.size()))
665           {
666             // alPos is within a hidden region, return the next one
667             // if there is one
668             region = hiddenColumns.get(index + 1);
669             return region[0];
670           }
671         }
672       }
673       return alPos;
674     } finally
675     {
676       LOCK.readLock().unlock();
677     }
678   }
679
680   /**
681    * Answers if a column in the alignment is visible
682    * 
683    * @param column
684    *          absolute position of column in the alignment
685    * @return true if column is visible
686    */
687   public boolean isVisible(int column)
688   {
689     try
690     {
691       LOCK.readLock().lock();
692
693       int regionindex = cursor.findRegionForColumn(column).getRegionIndex();
694       if (regionindex > -1 && regionindex < hiddenColumns.size())
695       {
696         int[] region = hiddenColumns.get(regionindex);
697         // already know that column <= region[1] as cursor returns containing
698         // region or region to right
699         if (column >= region[0])
700         {
701           return false;
702         }
703       }
704       return true;
705
706     } finally
707     {
708       LOCK.readLock().unlock();
709     }
710   }
711
712   /**
713    * Locate the first position visible for this sequence. If seq isn't visible
714    * then return the position of the left side of the hidden boundary region.
715    * 
716    * @param seq
717    *          sequence to find position for
718    * @return visible start position
719    */
720   public int locateVisibleStartOfSequence(SequenceI seq)
721   {
722     try
723     {
724       LOCK.readLock().lock();
725       int start = 0;
726
727       if (hiddenColumns.isEmpty())
728       {
729         return seq.findIndex(seq.getStart()) - 1;
730       }
731
732       // Simply walk along the sequence whilst watching for hidden column
733       // boundaries
734       Iterator<int[]> regions = hiddenColumns.iterator();
735       int hideStart = seq.getLength();
736       int hideEnd = -1;
737       int visPrev = 0;
738       int visNext = 0;
739       boolean foundStart = false;
740
741       // step through the non-gapped positions of the sequence
742       for (int i = seq.getStart(); i <= seq.getEnd() && (!foundStart); i++)
743       {
744         // get alignment position of this residue in the sequence
745         int p = seq.findIndex(i) - 1;
746
747         // update hidden region start/end
748         while (hideEnd < p && regions.hasNext())
749         {
750           int[] region = regions.next();
751           visPrev = visNext;
752           visNext += region[0] - visPrev;
753           hideStart = region[0];
754           hideEnd = region[1];
755         }
756         if (hideEnd < p)
757         {
758           hideStart = seq.getLength();
759         }
760         // update visible boundary for sequence
761         if (p < hideStart)
762         {
763           start = p;
764           foundStart = true;
765         }
766       }
767
768       if (foundStart)
769       {
770         return absoluteToVisibleColumn(start);
771       }
772       // otherwise, sequence was completely hidden
773       return visPrev;
774     } finally
775     {
776       LOCK.readLock().unlock();
777     }
778   }
779
780
781   /**
782    * 
783    * @return true if there are columns hidden
784    */
785   public boolean hasHiddenColumns()
786   {
787     try
788     {
789       LOCK.readLock().lock();
790
791       // we don't use getSize()>0 here because it has to iterate over
792       // the full hiddenColumns collection and so will be much slower
793       return (!hiddenColumns.isEmpty());
794     } finally
795     {
796       LOCK.readLock().unlock();
797     }
798   }
799
800   /**
801    * 
802    * @return true if there is more than one hidden column region
803    */
804   public boolean hasMultiHiddenColumnRegions()
805   {
806     try
807     {
808       LOCK.readLock().lock();
809       return !hiddenColumns.isEmpty() && hiddenColumns.size() > 1;
810     } finally
811     {
812       LOCK.readLock().unlock();
813     }
814   }
815
816
817   /**
818    * Returns a hashCode built from hidden column ranges
819    */
820   @Override
821   public int hashCode()
822   {
823     try
824     {
825       LOCK.readLock().lock();
826       int hashCode = 1;
827       for (int[] hidden : hiddenColumns)
828       {
829         hashCode = HASH_MULTIPLIER * hashCode + hidden[0];
830         hashCode = HASH_MULTIPLIER * hashCode + hidden[1];
831       }
832       return hashCode;
833     } finally
834     {
835       LOCK.readLock().unlock();
836     }
837   }
838
839   /**
840    * Hide columns corresponding to the marked bits
841    * 
842    * @param inserts
843    *          - columns mapped to bits starting from zero
844    */
845   public void hideColumns(BitSet inserts)
846   {
847     try
848     {
849       LOCK.writeLock().lock();
850       for (int firstSet = inserts
851               .nextSetBit(0), lastSet = 0; firstSet >= 0; firstSet = inserts
852                       .nextSetBit(lastSet))
853       {
854         lastSet = inserts.nextClearBit(firstSet);
855         hideColumns(firstSet, lastSet - 1);
856       }
857       cursor.resetCursor(hiddenColumns);
858       numColumns = 0;
859     } finally
860     {
861       LOCK.writeLock().unlock();
862     }
863   }
864
865   /**
866    * Hide columns corresponding to the marked bits, within the range
867    * [start,end]. Entries in tohide which are outside [start,end] are ignored.
868    * 
869    * @param tohide
870    *          columns mapped to bits starting from zero
871    * @param start
872    *          start of range to hide columns within
873    * @param end
874    *          end of range to hide columns within
875    */
876   public void hideColumns(BitSet tohide, int start, int end)
877   {
878     clearHiddenColumnsInRange(start, end);
879
880     // make sure only bits between start and end are set
881     if (!tohide.isEmpty())
882     {
883       tohide.clear(0, start);
884       tohide.clear(Math.min(end + 1, tohide.length() + 1),
885               tohide.length() + 1);
886     }
887
888     hideColumns(tohide);
889   }
890
891   /**
892    * Make all columns in the range [start,end] visible
893    * 
894    * @param start
895    *          start of range to show columns
896    * @param end
897    *          end of range to show columns
898    */
899   private void clearHiddenColumnsInRange(int start, int end)
900   {
901     try
902     {
903       LOCK.writeLock().lock();
904       
905       if (!hiddenColumns.isEmpty())
906       {
907         HiddenCursorPosition pos = cursor.findRegionForColumn(start);
908         int index = pos.getRegionIndex();
909         int startindex = index; // first index in hiddenColumns to remove
910
911         if (index != -1 && index != hiddenColumns.size())
912         {
913           // regionIndex is the region which either contains start
914           // or lies to the right of start
915           int[] region = hiddenColumns.get(index);
916           if (region[0] < start && region[1] >= start)
917           {
918             // region contains start, truncate so that it ends just before start
919             region[1] = start - 1;
920             startindex++;
921           }
922         }
923
924         pos = cursor.findRegionForColumn(end);
925         index = pos.getRegionIndex();
926         int endindex = index - 1; // last index in hiddenColumns to remove
927
928         if (index != -1 && index != hiddenColumns.size())
929         {
930           // regionIndex is the region which either contains end
931           // or lies to the right of end
932           int[] region = hiddenColumns.get(index);
933           if (region[0] <= end && region[1] > end)
934           {
935             // region contains end, truncate so that it starts just after end
936             region[0] = end + 1;
937           }
938         }
939
940         hiddenColumns.subList(startindex, endindex + 1).clear();
941         cursor.resetCursor(hiddenColumns);
942         numColumns = 0;
943       }
944     } finally
945     {
946       LOCK.writeLock().unlock();
947     }
948   }
949
950   /**
951    * 
952    * @param updates
953    *          BitSet where hidden columns will be marked
954    */
955   protected void andNot(BitSet updates)
956   {
957     try
958     {
959       LOCK.writeLock().lock();
960
961       BitSet hiddenBitSet = new BitSet();
962       for (int[] range : hiddenColumns)
963       {
964         hiddenBitSet.set(range[0], range[1] + 1);
965       }
966       hiddenBitSet.andNot(updates);
967       hiddenColumns.clear();
968       hideColumns(hiddenBitSet);
969     } finally
970     {
971       LOCK.writeLock().unlock();
972     }
973   }
974
975   /**
976    * Calculate the visible start and end index of an alignment.
977    * 
978    * @param width
979    *          full alignment width
980    * @return integer array where: int[0] = startIndex, and int[1] = endIndex
981    */
982   public int[] getVisibleStartAndEndIndex(int width)
983   {
984     try
985     {
986       LOCK.readLock().lock();
987
988       int firstVisible = 0;
989       int lastVisible = width - 1;
990
991       if (!hiddenColumns.isEmpty())
992       {
993         // first visible col with index 0, convert to absolute index
994         firstVisible = visibleToAbsoluteColumn(0);
995
996         // last visible column is either immediately to left of
997         // last hidden region, or is just the last column in the alignment
998         int[] lastregion = hiddenColumns.get(hiddenColumns.size() - 1);
999         if (lastregion[1] == width - 1)
1000         {
1001           // last region is at very end of alignment
1002           // last visible column immediately precedes it
1003           lastVisible = lastregion[0] - 1;
1004         }
1005       }
1006       return new int[] { firstVisible, lastVisible };
1007
1008     } finally
1009     {
1010       LOCK.readLock().unlock();
1011     }
1012   }
1013
1014   /**
1015    * Finds the hidden region (if any) which starts or ends at res
1016    * 
1017    * @param res
1018    *          visible residue position, unadjusted for hidden columns
1019    * @return region as [start,end] or null if no matching region is found. If
1020    *         res is adjacent to two regions, returns the left region.
1021    */
1022   public int[] getRegionWithEdgeAtRes(int res)
1023   {
1024     try
1025     {
1026       LOCK.readLock().lock();
1027       int adjres = visibleToAbsoluteColumn(res);
1028
1029       int[] reveal = null;
1030
1031       if (!hiddenColumns.isEmpty())
1032       {
1033         // look for a region ending just before adjres
1034         int regionindex = cursor.findRegionForColumn(adjres - 1)
1035                 .getRegionIndex();
1036         if (regionindex < hiddenColumns.size()
1037                 && hiddenColumns.get(regionindex)[1] == adjres - 1)
1038         {
1039           reveal = hiddenColumns.get(regionindex);
1040         }
1041         // check if the region ends just after adjres
1042         else if (regionindex < hiddenColumns.size()
1043                 && hiddenColumns.get(regionindex)[0] == adjres + 1)
1044         {
1045           reveal = hiddenColumns.get(regionindex);
1046         }
1047       }
1048       return reveal;
1049
1050     } finally
1051     {
1052       LOCK.readLock().unlock();
1053     }
1054   }
1055
1056   /**
1057    * Return an iterator over the hidden regions
1058    */
1059   public Iterator<int[]> iterator()
1060   {
1061     try
1062     {
1063       LOCK.readLock().lock();
1064       return new HiddenColsIterator(hiddenColumns);
1065     } finally
1066     {
1067       LOCK.readLock().unlock();
1068     }
1069   }
1070
1071   /**
1072    * Return a bounded iterator over the hidden regions
1073    * 
1074    * @param start
1075    *          position to start from (inclusive, absolute column position)
1076    * @param end
1077    *          position to end at (inclusive, absolute column position)
1078    * @return
1079    */
1080   public Iterator<int[]> getBoundedIterator(int start, int end)
1081   {
1082     try
1083     {
1084       LOCK.readLock().lock();
1085       return new HiddenColsIterator(start, end, hiddenColumns);
1086     } finally
1087     {
1088       LOCK.readLock().unlock();
1089     }
1090   }
1091
1092   /**
1093    * Return a bounded iterator over the *visible* start positions of hidden
1094    * regions
1095    * 
1096    * @param start
1097    *          position to start from (inclusive, visible column position)
1098    * @param end
1099    *          position to end at (inclusive, visible column position)
1100    */
1101   public Iterator<Integer> getBoundedStartIterator(int start, int end)
1102   {
1103     try
1104     {
1105       LOCK.readLock().lock();
1106
1107       // get absolute position of column in alignment
1108       int absoluteStart = visibleToAbsoluteColumn(start);
1109
1110       // Get cursor position and supply it to the iterator:
1111       // Since we want visible region start, we look for a cursor for the
1112       // (absoluteStart-1), then if absoluteStart is the start of a visible
1113       // region we'll get the cursor pointing to the region before, which is
1114       // what we want
1115       HiddenCursorPosition pos = cursor
1116               .findRegionForColumn(absoluteStart - 1);
1117
1118       return new BoundedStartRegionIterator(pos, start, end,
1119               hiddenColumns);
1120     } finally
1121     {
1122       LOCK.readLock().unlock();
1123     }
1124   }
1125
1126   /**
1127    * Return an iterator over visible *columns* (not regions) between the given
1128    * start and end boundaries
1129    * 
1130    * @param start
1131    *          first column (inclusive)
1132    * @param end
1133    *          last column (inclusive)
1134    */
1135   public Iterator<Integer> getVisibleColsIterator(int start, int end)
1136   {
1137     try
1138     {
1139       LOCK.readLock().lock();
1140       return new VisibleColsIterator(start, end, hiddenColumns);
1141     } finally
1142     {
1143       LOCK.readLock().unlock();
1144     }
1145   }
1146
1147   /**
1148    * return an iterator over visible segments between the given start and end
1149    * boundaries
1150    * 
1151    * @param start
1152    *          first column, inclusive from 0
1153    * @param end
1154    *          last column - not inclusive
1155    * @param useVisibleCoords
1156    *          if true, start and end are visible column positions, not absolute
1157    *          positions*
1158    */
1159   public VisibleContigsIterator getVisContigsIterator(int start,
1160           int end,
1161           boolean useVisibleCoords)
1162   {
1163     int adjstart = start;
1164     int adjend = end;
1165     if (useVisibleCoords)
1166     {
1167       adjstart = visibleToAbsoluteColumn(start);
1168       adjend = visibleToAbsoluteColumn(end);
1169     }
1170
1171     try
1172     {
1173       LOCK.readLock().lock();
1174       return new VisibleContigsIterator(adjstart, adjend, hiddenColumns);
1175     } finally
1176     {
1177       LOCK.readLock().unlock();
1178     }
1179   }
1180 }