JAL-2759 Keep numColumns up to date
[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     numColumns += region[1] - oldend;
291
292     /*
293      * also update or remove any subsequent ranges 
294      * that are overlapped
295      */
296     int endi = i;
297     while (endi < hiddenColumns.size() - 1)
298     {
299       int[] nextRegion = hiddenColumns.get(endi + 1);
300       if (nextRegion[0] > end + 1)
301       {
302         /*
303          * gap to next hidden range - no more to update
304          */
305         break;
306       }
307       numColumns -= nextRegion[1] - nextRegion[0] + 1;
308       oldend = nextRegion[1];
309       region[1] = Math.max(nextRegion[1], end);
310       numColumns += region[1] - oldend + 1;
311       endi++;
312     }
313     hiddenColumns.subList(i + 1, endi + 1).clear();
314   }
315
316   /**
317    * hide a list of ranges
318    * 
319    * @param ranges
320    */
321   public void hideList(List<int[]> ranges)
322   {
323     try
324     {
325       LOCK.writeLock().lock();
326       for (int[] r : ranges)
327       {
328         hideColumns(r[0], r[1]);
329       }
330       cursor = new HiddenColumnsCursor(hiddenColumns);
331
332     } finally
333     {
334       LOCK.writeLock().unlock();
335     }
336   }
337
338   /**
339    * Unhides, and adds to the selection list, all hidden columns
340    */
341   public void revealAllHiddenColumns(ColumnSelection sel)
342   {
343     try
344     {
345       LOCK.writeLock().lock();
346
347       for (int[] region : hiddenColumns)
348       {
349         for (int j = region[0]; j < region[1] + 1; j++)
350         {
351           sel.addElement(j);
352         }
353       }
354       hiddenColumns.clear();
355       cursor = new HiddenColumnsCursor(hiddenColumns);
356       numColumns = 0;
357
358     } finally
359     {
360       LOCK.writeLock().unlock();
361     }
362   }
363
364   /**
365    * Reveals, and marks as selected, the hidden column range with the given
366    * start column
367    * 
368    * @param start
369    *          the start column to look for
370    * @param sel
371    *          the column selection to add the hidden column range to
372    */
373   public void revealHiddenColumns(int start, ColumnSelection sel)
374   {
375     try
376     {
377       LOCK.writeLock().lock();
378
379       if (!hiddenColumns.isEmpty())
380       {
381         int regionIndex = cursor.findRegionForColumn(start, false)
382                 .getRegionIndex();
383
384         if (regionIndex != -1 && regionIndex != hiddenColumns.size())
385         {
386           // regionIndex is the region which either contains start
387           // or lies to the right of start
388           int[] region = hiddenColumns.get(regionIndex);
389           if (start == region[0])
390           {
391             for (int j = region[0]; j < region[1] + 1; j++)
392             {
393               sel.addElement(j);
394             }
395             int colsToRemove = region[1] - region[0] + 1;
396             hiddenColumns.remove(regionIndex);
397             numColumns -= colsToRemove;
398
399             cursor.updateForDeletedRegion(hiddenColumns);
400           }
401         }
402       }
403     } finally
404     {
405       LOCK.writeLock().unlock();
406     }
407   }
408
409   /**
410    * Output regions data as a string. String is in the format:
411    * reg0[0]<between>reg0[1]<delimiter>reg1[0]<between>reg1[1] ... regn[1]
412    * 
413    * @param delimiter
414    *          string to delimit regions
415    * @param betweenstring
416    *          to put between start and end region values
417    * @return regions formatted according to delimiter and between strings
418    */
419   public String regionsToString(String delimiter, String between)
420   {
421     try
422     {
423       LOCK.readLock().lock();
424       StringBuilder regionBuilder = new StringBuilder();
425
426       boolean first = true;
427       for (int[] range : hiddenColumns)
428       {
429         if (!first)
430         {
431           regionBuilder.append(delimiter);
432         }
433         else
434         {
435           first = false;
436         }
437         regionBuilder.append(range[0]).append(between).append(range[1]);
438
439       }
440
441       return regionBuilder.toString();
442     } finally
443     {
444       LOCK.readLock().unlock();
445     }
446   }
447
448   /**
449    * Find the number of hidden columns
450    * 
451    * @return number of hidden columns
452    */
453   public int getSize()
454   {
455     return numColumns;
456   }
457
458   /**
459    * Get the number of distinct hidden regions
460    * 
461    * @return number of regions
462    */
463   public int getNumberOfRegions()
464   {
465     try
466     {
467       LOCK.readLock().lock();
468       return hiddenColumns.size();
469     } finally
470     {
471       LOCK.readLock().unlock();
472     }
473   }
474
475   @Override
476   public boolean equals(Object obj)
477   {
478     try
479     {
480       LOCK.readLock().lock();
481
482       if (!(obj instanceof HiddenColumns))
483       {
484         return false;
485       }
486       HiddenColumns that = (HiddenColumns) obj;
487
488       /*
489        * check hidden columns are either both null, or match
490        */
491
492       if (that.hiddenColumns.size() != this.hiddenColumns.size())
493       {
494         return false;
495       }
496
497       Iterator<int[]> it = this.iterator();
498       Iterator<int[]> thatit = that.iterator();
499       while (it.hasNext())
500       {
501         if (!(Arrays.equals(it.next(), thatit.next())))
502         {
503           return false;
504         }
505       }
506       return true;
507
508     } finally
509     {
510       LOCK.readLock().unlock();
511     }
512   }
513
514   /**
515    * Return absolute column index for a visible column index
516    * 
517    * @param column
518    *          int column index in alignment view (count from zero)
519    * @return alignment column index for column
520    */
521   public int visibleToAbsoluteColumn(int column)
522   {
523     try
524     {
525       LOCK.readLock().lock();
526       int result = column;
527
528       if (!hiddenColumns.isEmpty())
529       {
530         result += cursor.findRegionForColumn(column, true)
531                 .getHiddenSoFar();
532       }
533
534       return result;
535     } finally
536     {
537       LOCK.readLock().unlock();
538     }
539   }
540
541   /**
542    * Use this method to find out where a column will appear in the visible
543    * alignment when hidden columns exist. If the column is not visible, then the
544    * index of the next visible column on the left will be returned (or 0 if
545    * there is no visible column on the left)
546    * 
547    * @param hiddenColumn
548    *          the column index in the full alignment including hidden columns
549    * @return the position of the column in the visible alignment
550    */
551   public int absoluteToVisibleColumn(int hiddenColumn)
552   {
553     try
554     {
555       LOCK.readLock().lock();
556       int result = hiddenColumn;
557
558       if (!hiddenColumns.isEmpty())
559       {
560         HiddenCursorPosition cursorPos = cursor
561                 .findRegionForColumn(hiddenColumn, false);
562         int index = cursorPos.getRegionIndex();
563         int hiddenBeforeCol = cursorPos.getHiddenSoFar();
564     
565         // just subtract hidden cols count - this works fine if column is
566         // visible
567         result = hiddenColumn - hiddenBeforeCol;
568     
569         // now check in case column is hidden - it will be in the returned
570         // hidden region
571         if (index < hiddenColumns.size())
572         {
573           int[] region = hiddenColumns.get(index);
574           if (hiddenColumn >= region[0] && hiddenColumn <= region[1])
575           {
576             // actually col is hidden, return region[0]-1
577             // unless region[0]==0 in which case return 0
578             if (region[0] == 0)
579             {
580               result = 0;
581             }
582             else
583             {
584               result = region[0] - 1 - hiddenBeforeCol;
585             }
586           }
587         }
588       }
589
590       return result; // return the shifted position after removing hidden
591                      // columns.
592     } finally
593     {
594       LOCK.readLock().unlock();
595     }
596   }
597
598   /**
599    * Find the visible column which is a given visible number of columns to the
600    * left (negative visibleDistance) or right (positive visibleDistance) of
601    * startColumn. If startColumn is not visible, we use the visible column at
602    * the left boundary of the hidden region containing startColumn.
603    * 
604    * @param visibleDistance
605    *          the number of visible columns to offset by (left offset = negative
606    *          value; right offset = positive value)
607    * @param startColumn
608    *          the position of the column to start from (absolute position)
609    * @return the position of the column which is <visibleDistance> away
610    *         (absolute position)
611    */
612   public int offsetByVisibleColumns(int visibleDistance, int startColumn)
613   {
614     try
615     {
616       LOCK.readLock().lock();
617       int start = absoluteToVisibleColumn(startColumn);
618       return visibleToAbsoluteColumn(start + visibleDistance);
619
620     } finally
621     {
622       LOCK.readLock().unlock();
623     }
624   }
625
626   /**
627    * This method returns the rightmost limit of a region of an alignment with
628    * hidden columns. In otherwords, the next hidden column.
629    * 
630    * @param alPos
631    *          the absolute (visible) alignmentPosition to find the next hidden
632    *          column for
633    * @return the index of the next hidden column, or alPos if there is no next
634    *         hidden column
635    */
636   public int getNextHiddenBoundary(boolean left, int alPos)
637   {
638     try
639     {
640       LOCK.readLock().lock();
641       if (!hiddenColumns.isEmpty())
642       {
643         int index = cursor.findRegionForColumn(alPos, false)
644                 .getRegionIndex();
645
646         if (left && index > 0)
647         {
648           int[] region = hiddenColumns.get(index - 1);
649           return region[1];
650         }
651         else if (!left && index < hiddenColumns.size())
652         {
653           int[] region = hiddenColumns.get(index);
654           if (alPos < region[0])
655           {
656             return region[0];
657           }
658           else if ((alPos <= region[1])
659                   && (index + 1 < hiddenColumns.size()))
660           {
661             // alPos is within a hidden region, return the next one
662             // if there is one
663             region = hiddenColumns.get(index + 1);
664             return region[0];
665           }
666         }
667       }
668       return alPos;
669     } finally
670     {
671       LOCK.readLock().unlock();
672     }
673   }
674
675   /**
676    * Answers if a column in the alignment is visible
677    * 
678    * @param column
679    *          absolute position of column in the alignment
680    * @return true if column is visible
681    */
682   public boolean isVisible(int column)
683   {
684     try
685     {
686       LOCK.readLock().lock();
687
688       int regionindex = cursor.findRegionForColumn(column, false)
689               .getRegionIndex();
690       if (regionindex > -1 && regionindex < hiddenColumns.size())
691       {
692         int[] region = hiddenColumns.get(regionindex);
693         // already know that column <= region[1] as cursor returns containing
694         // region or region to right
695         if (column >= region[0])
696         {
697           return false;
698         }
699       }
700       return true;
701
702     } finally
703     {
704       LOCK.readLock().unlock();
705     }
706   }
707
708   /**
709    * 
710    * @return true if there are columns hidden
711    */
712   public boolean hasHiddenColumns()
713   {
714     try
715     {
716       LOCK.readLock().lock();
717
718       // we don't use getSize()>0 here because it has to iterate over
719       // the full hiddenColumns collection and so will be much slower
720       return (!hiddenColumns.isEmpty());
721     } finally
722     {
723       LOCK.readLock().unlock();
724     }
725   }
726
727   /**
728    * 
729    * @return true if there is more than one hidden column region
730    */
731   public boolean hasMultiHiddenColumnRegions()
732   {
733     try
734     {
735       LOCK.readLock().lock();
736       return !hiddenColumns.isEmpty() && hiddenColumns.size() > 1;
737     } finally
738     {
739       LOCK.readLock().unlock();
740     }
741   }
742
743
744   /**
745    * Returns a hashCode built from hidden column ranges
746    */
747   @Override
748   public int hashCode()
749   {
750     try
751     {
752       LOCK.readLock().lock();
753       int hashCode = 1;
754
755       for (int[] hidden : hiddenColumns)
756       {
757         hashCode = HASH_MULTIPLIER * hashCode + hidden[0];
758         hashCode = HASH_MULTIPLIER * hashCode + hidden[1];
759       }
760       return hashCode;
761     } finally
762     {
763       LOCK.readLock().unlock();
764     }
765   }
766
767   /**
768    * Hide columns corresponding to the marked bits
769    * 
770    * @param inserts
771    *          - columns mapped to bits starting from zero
772    */
773   public void hideColumns(BitSet inserts)
774   {
775     try
776     {
777       LOCK.writeLock().lock();
778       for (int firstSet = inserts
779               .nextSetBit(0), lastSet = 0; firstSet >= 0; firstSet = inserts
780                       .nextSetBit(lastSet))
781       {
782         lastSet = inserts.nextClearBit(firstSet);
783         hideColumns(firstSet, lastSet - 1);
784       }
785       cursor = new HiddenColumnsCursor(hiddenColumns);
786     } finally
787     {
788       LOCK.writeLock().unlock();
789     }
790   }
791
792   /**
793    * Hide columns corresponding to the marked bits, within the range
794    * [start,end]. Entries in tohide which are outside [start,end] are ignored.
795    * 
796    * @param tohide
797    *          columns mapped to bits starting from zero
798    * @param start
799    *          start of range to hide columns within
800    * @param end
801    *          end of range to hide columns within
802    */
803   public void hideColumns(BitSet tohide, int start, int end)
804   {
805     clearHiddenColumnsInRange(start, end);
806
807     // make sure only bits between start and end are set
808     if (!tohide.isEmpty())
809     {
810       tohide.clear(0, start);
811       tohide.clear(Math.min(end + 1, tohide.length() + 1),
812               tohide.length() + 1);
813     }
814
815     hideColumns(tohide);
816   }
817
818   /**
819    * Make all columns in the range [start,end] visible
820    * 
821    * @param start
822    *          start of range to show columns
823    * @param end
824    *          end of range to show columns
825    */
826   private void clearHiddenColumnsInRange(int start, int end)
827   {
828     try
829     {
830       LOCK.writeLock().lock();
831       
832       if (!hiddenColumns.isEmpty())
833       {
834         HiddenCursorPosition pos = cursor.findRegionForColumn(start, false);
835         int index = pos.getRegionIndex();
836
837         if (index != -1 && index != hiddenColumns.size())
838         {
839           // regionIndex is the region which either contains start
840           // or lies to the right of start
841           int[] region = hiddenColumns.get(index);
842           if (region[0] < start && region[1] >= start)
843           {
844             // region contains start, truncate so that it ends just before start
845             numColumns -= region[1] - start + 1;
846             region[1] = start - 1;
847             index++;
848           }
849
850           int endi = index;
851           while (endi < hiddenColumns.size())
852           {
853             region = hiddenColumns.get(endi);
854
855             if (region[1] > end)
856             {
857               if (region[0] <= end)
858               {
859                 // region contains end, truncate so it starts just after end
860                 numColumns -= end - region[0] + 1;
861                 region[0] = end + 1;
862               }
863               break;
864             }
865
866             numColumns -= region[1] - region[0] + 1;
867             endi++;
868           }
869           hiddenColumns.subList(index, endi).clear();
870
871         }
872
873         cursor = new HiddenColumnsCursor(hiddenColumns);
874       }
875     } finally
876     {
877       LOCK.writeLock().unlock();
878     }
879   }
880
881   /**
882    * 
883    * @param updates
884    *          BitSet where hidden columns will be marked
885    */
886   protected void andNot(BitSet updates)
887   {
888     try
889     {
890       LOCK.writeLock().lock();
891
892       BitSet hiddenBitSet = new BitSet();
893       for (int[] range : hiddenColumns)
894       {
895         hiddenBitSet.set(range[0], range[1] + 1);
896       }
897       hiddenBitSet.andNot(updates);
898       hiddenColumns.clear();
899       hideColumns(hiddenBitSet);
900     } finally
901     {
902       LOCK.writeLock().unlock();
903     }
904   }
905
906   /**
907    * Calculate the visible start and end index of an alignment.
908    * 
909    * @param width
910    *          full alignment width
911    * @return integer array where: int[0] = startIndex, and int[1] = endIndex
912    */
913   public int[] getVisibleStartAndEndIndex(int width)
914   {
915     try
916     {
917       LOCK.readLock().lock();
918
919       int firstVisible = 0;
920       int lastVisible = width - 1;
921
922       if (!hiddenColumns.isEmpty())
923       {
924         // first visible col with index 0, convert to absolute index
925         firstVisible = visibleToAbsoluteColumn(0);
926
927         // last visible column is either immediately to left of
928         // last hidden region, or is just the last column in the alignment
929         int[] lastregion = hiddenColumns.get(hiddenColumns.size() - 1);
930         if (lastregion[1] == width - 1)
931         {
932           // last region is at very end of alignment
933           // last visible column immediately precedes it
934           lastVisible = lastregion[0] - 1;
935         }
936       }
937       return new int[] { firstVisible, lastVisible };
938
939     } finally
940     {
941       LOCK.readLock().unlock();
942     }
943   }
944
945   /**
946    * Finds the hidden region (if any) which starts or ends at res
947    * 
948    * @param res
949    *          visible residue position, unadjusted for hidden columns
950    * @return region as [start,end] or null if no matching region is found. If
951    *         res is adjacent to two regions, returns the left region.
952    */
953   public int[] getRegionWithEdgeAtRes(int res)
954   {
955     try
956     {
957       LOCK.readLock().lock();
958       int adjres = visibleToAbsoluteColumn(res);
959
960       int[] reveal = null;
961
962       if (!hiddenColumns.isEmpty())
963       {
964         // look for a region ending just before adjres
965         int regionindex = cursor.findRegionForColumn(adjres - 1, false)
966                 .getRegionIndex();
967         if (regionindex < hiddenColumns.size()
968                 && hiddenColumns.get(regionindex)[1] == adjres - 1)
969         {
970           reveal = hiddenColumns.get(regionindex);
971         }
972         // check if the region ends just after adjres
973         else if (regionindex < hiddenColumns.size()
974                 && hiddenColumns.get(regionindex)[0] == adjres + 1)
975         {
976           reveal = hiddenColumns.get(regionindex);
977         }
978       }
979       return reveal;
980
981     } finally
982     {
983       LOCK.readLock().unlock();
984     }
985   }
986
987   /**
988    * Return an iterator over the hidden regions
989    */
990   public Iterator<int[]> iterator()
991   {
992     try
993     {
994       LOCK.readLock().lock();
995       return new HiddenColsIterator(hiddenColumns);
996     } finally
997     {
998       LOCK.readLock().unlock();
999     }
1000   }
1001
1002   /**
1003    * Return a bounded iterator over the hidden regions
1004    * 
1005    * @param start
1006    *          position to start from (inclusive, absolute column position)
1007    * @param end
1008    *          position to end at (inclusive, absolute column position)
1009    * @return
1010    */
1011   public Iterator<int[]> getBoundedIterator(int start, int end)
1012   {
1013     try
1014     {
1015       LOCK.readLock().lock();
1016       return new HiddenColsIterator(start, end, hiddenColumns);
1017     } finally
1018     {
1019       LOCK.readLock().unlock();
1020     }
1021   }
1022
1023   /**
1024    * Return a bounded iterator over the *visible* start positions of hidden
1025    * regions
1026    * 
1027    * @param start
1028    *          position to start from (inclusive, visible column position)
1029    * @param end
1030    *          position to end at (inclusive, visible column position)
1031    */
1032   public Iterator<Integer> getBoundedStartIterator(int start, int end)
1033   {
1034     try
1035     {
1036       LOCK.readLock().lock();
1037
1038       // get absolute position of column in alignment
1039       int absoluteStart = visibleToAbsoluteColumn(start);
1040
1041       // Get cursor position and supply it to the iterator:
1042       // Since we want visible region start, we look for a cursor for the
1043       // (absoluteStart-1), then if absoluteStart is the start of a visible
1044       // region we'll get the cursor pointing to the region before, which is
1045       // what we want
1046       HiddenCursorPosition pos = cursor
1047               .findRegionForColumn(absoluteStart - 1, false);
1048
1049       return new BoundedStartRegionIterator(pos, start, end,
1050               hiddenColumns);
1051     } finally
1052     {
1053       LOCK.readLock().unlock();
1054     }
1055   }
1056
1057   /**
1058    * Return an iterator over visible *columns* (not regions) between the given
1059    * start and end boundaries
1060    * 
1061    * @param start
1062    *          first column (inclusive)
1063    * @param end
1064    *          last column (inclusive)
1065    */
1066   public Iterator<Integer> getVisibleColsIterator(int start, int end)
1067   {
1068     try
1069     {
1070       LOCK.readLock().lock();
1071       return new VisibleColsIterator(start, end, hiddenColumns);
1072     } finally
1073     {
1074       LOCK.readLock().unlock();
1075     }
1076   }
1077
1078   /**
1079    * return an iterator over visible segments between the given start and end
1080    * boundaries
1081    * 
1082    * @param start
1083    *          first column, inclusive from 0
1084    * @param end
1085    *          last column - not inclusive
1086    * @param useVisibleCoords
1087    *          if true, start and end are visible column positions, not absolute
1088    *          positions*
1089    */
1090   public VisibleContigsIterator getVisContigsIterator(int start,
1091           int end,
1092           boolean useVisibleCoords)
1093   {
1094     int adjstart = start;
1095     int adjend = end;
1096     if (useVisibleCoords)
1097     {
1098       adjstart = visibleToAbsoluteColumn(start);
1099       adjend = visibleToAbsoluteColumn(end);
1100     }
1101
1102     try
1103     {
1104       LOCK.readLock().lock();
1105       return new VisibleContigsIterator(adjstart, adjend, hiddenColumns);
1106     } finally
1107     {
1108       LOCK.readLock().unlock();
1109     }
1110   }
1111 }