JAL-2759 Additional tests and tweaks to numColumns update
[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, false);
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       // new range follows everything else; check first to avoid looping over
185       // whole hiddenColumns collection
186       if (hiddenColumns.isEmpty()
187               || start > hiddenColumns.get(hiddenColumns.size() - 1)[1])
188       {
189         hiddenColumns.add(new int[] { start, end });
190         numColumns += end - start + 1;
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     } finally
214     {
215       LOCK.writeLock().unlock();
216     }
217   }
218
219   /**
220    * Insert [start, range] at the region at index i in hiddenColumns, if
221    * feasible
222    * 
223    * @param i
224    *          index to insert at
225    * @param start
226    *          start of range to insert
227    * @param end
228    *          end of range to insert
229    * @return true if range was successfully inserted
230    */
231   private boolean insertRangeAtRegion(int i, int start, int end)
232   {
233     boolean added = false;
234
235     int[] region = hiddenColumns.get(i);
236     if (end < region[0] - 1)
237     {
238       /*
239        * insert discontiguous preceding range
240        */
241       hiddenColumns.add(i, new int[] { start, end });
242       numColumns += end - start + 1;
243       added = true;
244     }
245     else if (end <= region[1])
246     {
247       /*
248        * new range overlaps existing, or is contiguous preceding it - adjust
249        * start column
250        */
251       int oldstart = region[0];
252       region[0] = Math.min(region[0], start);
253       numColumns += oldstart - region[0]; // new columns are between old and
254                                               // adjusted starts
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       insertRangeAtOverlap(i, start, end, region);
264       added = true;
265     }
266     return added;
267   }
268
269   /**
270    * Insert a range whose start position overlaps an existing region and/or is
271    * contiguous to the right of the region
272    * 
273    * @param i
274    *          index to insert at
275    * @param start
276    *          start of range to insert
277    * @param end
278    *          end of range to insert
279    * @param region
280    *          the overlapped/continued region
281    */
282   private void insertRangeAtOverlap(int i, int start, int end, int[] region)
283   {
284     int oldstart = region[0];
285     int oldend = region[1];
286     region[0] = Math.min(region[0], start);
287     region[1] = Math.max(region[1], end);
288
289     numColumns += oldstart - region[0];
290
291     /*
292      * also update or remove any subsequent ranges 
293      * that are overlapped
294      */
295     int endi = i;
296     while (endi < hiddenColumns.size() - 1)
297     {
298       int[] nextRegion = hiddenColumns.get(endi + 1);
299       if (nextRegion[0] > end + 1)
300       {
301         /*
302          * gap to next hidden range - no more to update
303          */
304         break;
305       }
306       numColumns -= nextRegion[1] - nextRegion[0] + 1;
307       region[1] = Math.max(nextRegion[1], end);
308       endi++;
309     }
310     numColumns += region[1] - oldend;
311     hiddenColumns.subList(i + 1, endi + 1).clear();
312   }
313
314   /**
315    * hide a list of ranges
316    * 
317    * @param ranges
318    */
319   public void hideList(List<int[]> ranges)
320   {
321     try
322     {
323       LOCK.writeLock().lock();
324       for (int[] r : ranges)
325       {
326         hideColumns(r[0], r[1]);
327       }
328       cursor = new HiddenColumnsCursor(hiddenColumns);
329
330     } finally
331     {
332       LOCK.writeLock().unlock();
333     }
334   }
335
336   /**
337    * Unhides, and adds to the selection list, all hidden columns
338    */
339   public void revealAllHiddenColumns(ColumnSelection sel)
340   {
341     try
342     {
343       LOCK.writeLock().lock();
344
345       for (int[] region : hiddenColumns)
346       {
347         for (int j = region[0]; j < region[1] + 1; j++)
348         {
349           sel.addElement(j);
350         }
351       }
352       hiddenColumns.clear();
353       cursor = new HiddenColumnsCursor(hiddenColumns);
354       numColumns = 0;
355
356     } finally
357     {
358       LOCK.writeLock().unlock();
359     }
360   }
361
362   /**
363    * Reveals, and marks as selected, the hidden column range with the given
364    * start column
365    * 
366    * @param start
367    *          the start column to look for
368    * @param sel
369    *          the column selection to add the hidden column range to
370    */
371   public void revealHiddenColumns(int start, ColumnSelection sel)
372   {
373     try
374     {
375       LOCK.writeLock().lock();
376
377       if (!hiddenColumns.isEmpty())
378       {
379         int regionIndex = cursor.findRegionForColumn(start, false)
380                 .getRegionIndex();
381
382         if (regionIndex != -1 && regionIndex != hiddenColumns.size())
383         {
384           // regionIndex is the region which either contains start
385           // or lies to the right of start
386           int[] region = hiddenColumns.get(regionIndex);
387           if (start == region[0])
388           {
389             for (int j = region[0]; j < region[1] + 1; j++)
390             {
391               sel.addElement(j);
392             }
393             int colsToRemove = region[1] - region[0] + 1;
394             hiddenColumns.remove(regionIndex);
395             numColumns -= colsToRemove;
396
397             cursor.updateForDeletedRegion(hiddenColumns);
398           }
399         }
400       }
401     } finally
402     {
403       LOCK.writeLock().unlock();
404     }
405   }
406
407   /**
408    * Output regions data as a string. String is in the format:
409    * reg0[0]<between>reg0[1]<delimiter>reg1[0]<between>reg1[1] ... regn[1]
410    * 
411    * @param delimiter
412    *          string to delimit regions
413    * @param betweenstring
414    *          to put between start and end region values
415    * @return regions formatted according to delimiter and between strings
416    */
417   public String regionsToString(String delimiter, String between)
418   {
419     try
420     {
421       LOCK.readLock().lock();
422       StringBuilder regionBuilder = new StringBuilder();
423
424       boolean first = true;
425       for (int[] range : hiddenColumns)
426       {
427         if (!first)
428         {
429           regionBuilder.append(delimiter);
430         }
431         else
432         {
433           first = false;
434         }
435         regionBuilder.append(range[0]).append(between).append(range[1]);
436
437       }
438
439       return regionBuilder.toString();
440     } finally
441     {
442       LOCK.readLock().unlock();
443     }
444   }
445
446   /**
447    * Find the number of hidden columns
448    * 
449    * @return number of hidden columns
450    */
451   public int getSize()
452   {
453     return numColumns;
454   }
455
456   /**
457    * Get the number of distinct hidden regions
458    * 
459    * @return number of regions
460    */
461   public int getNumberOfRegions()
462   {
463     try
464     {
465       LOCK.readLock().lock();
466       return hiddenColumns.size();
467     } finally
468     {
469       LOCK.readLock().unlock();
470     }
471   }
472
473   @Override
474   public boolean equals(Object obj)
475   {
476     try
477     {
478       LOCK.readLock().lock();
479
480       if (!(obj instanceof HiddenColumns))
481       {
482         return false;
483       }
484       HiddenColumns that = (HiddenColumns) obj;
485
486       /*
487        * check hidden columns are either both null, or match
488        */
489
490       if (that.hiddenColumns.size() != this.hiddenColumns.size())
491       {
492         return false;
493       }
494
495       Iterator<int[]> it = this.iterator();
496       Iterator<int[]> thatit = that.iterator();
497       while (it.hasNext())
498       {
499         if (!(Arrays.equals(it.next(), thatit.next())))
500         {
501           return false;
502         }
503       }
504       return true;
505
506     } finally
507     {
508       LOCK.readLock().unlock();
509     }
510   }
511
512   /**
513    * Return absolute column index for a visible column index
514    * 
515    * @param column
516    *          int column index in alignment view (count from zero)
517    * @return alignment column index for column
518    */
519   public int visibleToAbsoluteColumn(int column)
520   {
521     try
522     {
523       LOCK.readLock().lock();
524       int result = column;
525
526       if (!hiddenColumns.isEmpty())
527       {
528         result += cursor.findRegionForColumn(column, true)
529                 .getHiddenSoFar();
530       }
531
532       return result;
533     } finally
534     {
535       LOCK.readLock().unlock();
536     }
537   }
538
539   /**
540    * Use this method to find out where a column will appear in the visible
541    * alignment when hidden columns exist. If the column is not visible, then the
542    * index of the next visible column on the left will be returned (or 0 if
543    * there is no visible column on the left)
544    * 
545    * @param hiddenColumn
546    *          the column index in the full alignment including hidden columns
547    * @return the position of the column in the visible alignment
548    */
549   public int absoluteToVisibleColumn(int hiddenColumn)
550   {
551     try
552     {
553       LOCK.readLock().lock();
554       int result = hiddenColumn;
555
556       if (!hiddenColumns.isEmpty())
557       {
558         HiddenCursorPosition cursorPos = cursor
559                 .findRegionForColumn(hiddenColumn, false);
560         int index = cursorPos.getRegionIndex();
561         int hiddenBeforeCol = cursorPos.getHiddenSoFar();
562     
563         // just subtract hidden cols count - this works fine if column is
564         // visible
565         result = hiddenColumn - hiddenBeforeCol;
566     
567         // now check in case column is hidden - it will be in the returned
568         // hidden region
569         if (index < hiddenColumns.size())
570         {
571           int[] region = hiddenColumns.get(index);
572           if (hiddenColumn >= region[0] && hiddenColumn <= region[1])
573           {
574             // actually col is hidden, return region[0]-1
575             // unless region[0]==0 in which case return 0
576             if (region[0] == 0)
577             {
578               result = 0;
579             }
580             else
581             {
582               result = region[0] - 1 - hiddenBeforeCol;
583             }
584           }
585         }
586       }
587
588       return result; // return the shifted position after removing hidden
589                      // columns.
590     } finally
591     {
592       LOCK.readLock().unlock();
593     }
594   }
595
596   /**
597    * Find the visible column which is a given visible number of columns to the
598    * left (negative visibleDistance) or right (positive visibleDistance) of
599    * startColumn. If startColumn is not visible, we use the visible column at
600    * the left boundary of the hidden region containing startColumn.
601    * 
602    * @param visibleDistance
603    *          the number of visible columns to offset by (left offset = negative
604    *          value; right offset = positive value)
605    * @param startColumn
606    *          the position of the column to start from (absolute position)
607    * @return the position of the column which is <visibleDistance> away
608    *         (absolute position)
609    */
610   public int offsetByVisibleColumns(int visibleDistance, int startColumn)
611   {
612     try
613     {
614       LOCK.readLock().lock();
615       int start = absoluteToVisibleColumn(startColumn);
616       return visibleToAbsoluteColumn(start + visibleDistance);
617
618     } finally
619     {
620       LOCK.readLock().unlock();
621     }
622   }
623
624   /**
625    * This method returns the rightmost limit of a region of an alignment with
626    * hidden columns. In otherwords, the next hidden column.
627    * 
628    * @param alPos
629    *          the absolute (visible) alignmentPosition to find the next hidden
630    *          column for
631    * @return the index of the next hidden column, or alPos if there is no next
632    *         hidden column
633    */
634   public int getNextHiddenBoundary(boolean left, int alPos)
635   {
636     try
637     {
638       LOCK.readLock().lock();
639       if (!hiddenColumns.isEmpty())
640       {
641         int index = cursor.findRegionForColumn(alPos, false)
642                 .getRegionIndex();
643
644         if (left && index > 0)
645         {
646           int[] region = hiddenColumns.get(index - 1);
647           return region[1];
648         }
649         else if (!left && index < hiddenColumns.size())
650         {
651           int[] region = hiddenColumns.get(index);
652           if (alPos < region[0])
653           {
654             return region[0];
655           }
656           else if ((alPos <= region[1])
657                   && (index + 1 < hiddenColumns.size()))
658           {
659             // alPos is within a hidden region, return the next one
660             // if there is one
661             region = hiddenColumns.get(index + 1);
662             return region[0];
663           }
664         }
665       }
666       return alPos;
667     } finally
668     {
669       LOCK.readLock().unlock();
670     }
671   }
672
673   /**
674    * Answers if a column in the alignment is visible
675    * 
676    * @param column
677    *          absolute position of column in the alignment
678    * @return true if column is visible
679    */
680   public boolean isVisible(int column)
681   {
682     try
683     {
684       LOCK.readLock().lock();
685
686       int regionindex = cursor.findRegionForColumn(column, false)
687               .getRegionIndex();
688       if (regionindex > -1 && regionindex < hiddenColumns.size())
689       {
690         int[] region = hiddenColumns.get(regionindex);
691         // already know that column <= region[1] as cursor returns containing
692         // region or region to right
693         if (column >= region[0])
694         {
695           return false;
696         }
697       }
698       return true;
699
700     } finally
701     {
702       LOCK.readLock().unlock();
703     }
704   }
705
706   /**
707    * 
708    * @return true if there are columns hidden
709    */
710   public boolean hasHiddenColumns()
711   {
712     try
713     {
714       LOCK.readLock().lock();
715
716       // we don't use getSize()>0 here because it has to iterate over
717       // the full hiddenColumns collection and so will be much slower
718       return (!hiddenColumns.isEmpty());
719     } finally
720     {
721       LOCK.readLock().unlock();
722     }
723   }
724
725   /**
726    * 
727    * @return true if there is more than one hidden column region
728    */
729   public boolean hasMultiHiddenColumnRegions()
730   {
731     try
732     {
733       LOCK.readLock().lock();
734       return !hiddenColumns.isEmpty() && hiddenColumns.size() > 1;
735     } finally
736     {
737       LOCK.readLock().unlock();
738     }
739   }
740
741
742   /**
743    * Returns a hashCode built from hidden column ranges
744    */
745   @Override
746   public int hashCode()
747   {
748     try
749     {
750       LOCK.readLock().lock();
751       int hashCode = 1;
752
753       for (int[] hidden : hiddenColumns)
754       {
755         hashCode = HASH_MULTIPLIER * hashCode + hidden[0];
756         hashCode = HASH_MULTIPLIER * hashCode + hidden[1];
757       }
758       return hashCode;
759     } finally
760     {
761       LOCK.readLock().unlock();
762     }
763   }
764
765   /**
766    * Hide columns corresponding to the marked bits
767    * 
768    * @param inserts
769    *          - columns mapped to bits starting from zero
770    */
771   public void hideColumns(BitSet inserts)
772   {
773     try
774     {
775       LOCK.writeLock().lock();
776       for (int firstSet = inserts
777               .nextSetBit(0), lastSet = 0; firstSet >= 0; firstSet = inserts
778                       .nextSetBit(lastSet))
779       {
780         lastSet = inserts.nextClearBit(firstSet);
781         hideColumns(firstSet, lastSet - 1);
782       }
783       cursor = new HiddenColumnsCursor(hiddenColumns);
784     } finally
785     {
786       LOCK.writeLock().unlock();
787     }
788   }
789
790   /**
791    * Hide columns corresponding to the marked bits, within the range
792    * [start,end]. Entries in tohide which are outside [start,end] are ignored.
793    * 
794    * @param tohide
795    *          columns mapped to bits starting from zero
796    * @param start
797    *          start of range to hide columns within
798    * @param end
799    *          end of range to hide columns within
800    */
801   public void hideColumns(BitSet tohide, int start, int end)
802   {
803     clearHiddenColumnsInRange(start, end);
804
805     // make sure only bits between start and end are set
806     if (!tohide.isEmpty())
807     {
808       tohide.clear(0, start);
809       tohide.clear(Math.min(end + 1, tohide.length() + 1),
810               tohide.length() + 1);
811     }
812
813     hideColumns(tohide);
814   }
815
816   /**
817    * Make all columns in the range [start,end] visible
818    * 
819    * @param start
820    *          start of range to show columns
821    * @param end
822    *          end of range to show columns
823    */
824   private void clearHiddenColumnsInRange(int start, int end)
825   {
826     try
827     {
828       LOCK.writeLock().lock();
829       
830       if (!hiddenColumns.isEmpty())
831       {
832         HiddenCursorPosition pos = cursor.findRegionForColumn(start, false);
833         int index = pos.getRegionIndex();
834
835         if (index != -1 && index != hiddenColumns.size())
836         {
837           // regionIndex is the region which either contains start
838           // or lies to the right of start
839           int[] region = hiddenColumns.get(index);
840           if (region[0] < start && region[1] >= start)
841           {
842             // region contains start, truncate so that it ends just before start
843             numColumns -= region[1] - start + 1;
844             region[1] = start - 1;
845             index++;
846           }
847
848           int endi = index;
849           while (endi < hiddenColumns.size())
850           {
851             region = hiddenColumns.get(endi);
852
853             if (region[1] > end)
854             {
855               if (region[0] <= end)
856               {
857                 // region contains end, truncate so it starts just after end
858                 numColumns -= end - region[0] + 1;
859                 region[0] = end + 1;
860               }
861               break;
862             }
863
864             numColumns -= region[1] - region[0] + 1;
865             endi++;
866           }
867           hiddenColumns.subList(index, endi).clear();
868
869         }
870
871         cursor = new HiddenColumnsCursor(hiddenColumns);
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, false)
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, false);
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 }