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