Merge branch 'JAL-3878_ws-overhaul-3' into with_ws_overhaul-3
[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: must be kept up to date by methods 
79    * which add or remove hidden columns
80    */
81   private int numColumns = 0;
82
83   /*
84    * list of hidden column [start, end] ranges; the list is maintained in
85    * ascending start column order
86    */
87   private List<int[]> hiddenColumns = new ArrayList<>();
88
89   /**
90    * Constructor
91    */
92   public HiddenColumns()
93   {
94   }
95
96   /**
97    * Copy constructor
98    * 
99    * @param copy
100    *          the HiddenColumns object to copy from
101    */
102   public HiddenColumns(HiddenColumns copy)
103   {
104     this(copy, Integer.MIN_VALUE, Integer.MAX_VALUE, 0);
105   }
106
107   /**
108    * Copy constructor within bounds and with offset. Copies hidden column
109    * regions fully contained between start and end, and offsets positions by
110    * subtracting offset.
111    * 
112    * @param copy
113    *          HiddenColumns instance to copy from
114    * @param start
115    *          lower bound to copy from
116    * @param end
117    *          upper bound to copy to
118    * @param offset
119    *          offset to subtract from each region boundary position
120    * 
121    */
122   public HiddenColumns(HiddenColumns copy, int start, int end, int offset)
123   {
124     try
125     {
126       LOCK.writeLock().lock();
127       if (copy != null)
128       {
129         numColumns = 0;
130         Iterator<int[]> it = copy.getBoundedIterator(start, end);
131         while (it.hasNext())
132         {
133           int[] region = it.next();
134           // still need to check boundaries because iterator returns
135           // all overlapping regions and we need contained regions
136           if (region[0] >= start && region[1] <= end)
137           {
138             hiddenColumns
139                     .add(new int[]
140                     { region[0] - offset, region[1] - offset });
141             numColumns += region[1] - region[0] + 1;
142           }
143         }
144         cursor = new HiddenColumnsCursor(hiddenColumns);
145       }
146     } finally
147     {
148       LOCK.writeLock().unlock();
149     }
150   }
151
152   /**
153    * Adds the specified column range to the hidden columns collection
154    * 
155    * @param start
156    *          start of range to add (absolute position in alignment)
157    * @param end
158    *          end of range to add (absolute position in alignment)
159    */
160   public void hideColumns(int start, int end)
161   {
162     try
163     {
164       LOCK.writeLock().lock();
165
166       int previndex = 0;
167       int prevHiddenCount = 0;
168       int regionindex = 0;
169       if (!hiddenColumns.isEmpty())
170       {
171         // set up cursor reset values
172         HiddenCursorPosition cursorPos = cursor.findRegionForColumn(start,
173                 false);
174         regionindex = cursorPos.getRegionIndex();
175
176         if (regionindex > 0)
177         {
178           // get previous index and hidden count for updating the cursor later
179           previndex = regionindex - 1;
180           int[] prevRegion = hiddenColumns.get(previndex);
181           prevHiddenCount = cursorPos.getHiddenSoFar()
182                   - (prevRegion[1] - prevRegion[0] + 1);
183         }
184       }
185
186       // new range follows everything else; check first to avoid looping over
187       // whole hiddenColumns collection
188       if (hiddenColumns.isEmpty()
189               || start > hiddenColumns.get(hiddenColumns.size() - 1)[1])
190       {
191         hiddenColumns.add(new int[] { start, end });
192         numColumns += end - start + 1;
193       }
194       else
195       {
196         /*
197          * traverse existing hidden ranges and insert / amend / append as
198          * appropriate
199          */
200         boolean added = false;
201         if (regionindex > 0)
202         {
203           added = insertRangeAtRegion(regionindex - 1, start, end);
204         }
205         if (!added && regionindex < hiddenColumns.size())
206         {
207           insertRangeAtRegion(regionindex, start, end);
208         }
209       }
210
211       // reset the cursor to just before our insertion point: this saves
212       // a lot of reprocessing in large alignments
213       cursor = new HiddenColumnsCursor(hiddenColumns, previndex,
214               prevHiddenCount);
215     } finally
216     {
217       LOCK.writeLock().unlock();
218     }
219   }
220
221   /**
222    * Insert [start, range] at the region at index i in hiddenColumns, if
223    * feasible
224    * 
225    * @param i
226    *          index to insert at
227    * @param start
228    *          start of range to insert
229    * @param end
230    *          end of range to insert
231    * @return true if range was successfully inserted
232    */
233   private boolean insertRangeAtRegion(int i, int start, int end)
234   {
235     boolean added = false;
236
237     int[] region = hiddenColumns.get(i);
238     if (end < region[0] - 1)
239     {
240       /*
241        * insert discontiguous preceding range
242        */
243       hiddenColumns.add(i, new int[] { start, end });
244       numColumns += end - start + 1;
245       added = true;
246     }
247     else if (end <= region[1])
248     {
249       /*
250        * new range overlaps existing, or is contiguous preceding it - adjust
251        * start column
252        */
253       int oldstart = region[0];
254       region[0] = Math.min(region[0], start);
255       numColumns += oldstart - region[0]; // new columns are between old and
256                                           // adjusted starts
257       added = true;
258     }
259     else if (start <= region[1] + 1)
260     {
261       /*
262        * new range overlaps existing, or is contiguous following it - adjust
263        * start and end columns
264        */
265       insertRangeAtOverlap(i, start, end, region);
266       added = true;
267     }
268     return added;
269   }
270
271   /**
272    * Insert a range whose start position overlaps an existing region and/or is
273    * contiguous to the right of the region
274    * 
275    * @param i
276    *          index to insert at
277    * @param start
278    *          start of range to insert
279    * @param end
280    *          end of range to insert
281    * @param region
282    *          the overlapped/continued region
283    */
284   private void insertRangeAtOverlap(int i, int start, int end, int[] region)
285   {
286     int oldstart = region[0];
287     int oldend = region[1];
288     region[0] = Math.min(region[0], start);
289     region[1] = Math.max(region[1], end);
290
291     numColumns += oldstart - region[0];
292
293     /*
294      * also update or remove any subsequent ranges 
295      * that are overlapped
296      */
297     int endi = i;
298     while (endi < hiddenColumns.size() - 1)
299     {
300       int[] nextRegion = hiddenColumns.get(endi + 1);
301       if (nextRegion[0] > end + 1)
302       {
303         /*
304          * gap to next hidden range - no more to update
305          */
306         break;
307       }
308       numColumns -= nextRegion[1] - nextRegion[0] + 1;
309       region[1] = Math.max(nextRegion[1], end);
310       endi++;
311     }
312     numColumns += region[1] - oldend;
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         }
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   /**
474    * Answers true if obj is an instance of HiddenColumns, and holds the same
475    * array of start-end column ranges as this, else answers false
476    */
477   @Override
478   public boolean equals(Object obj)
479   {
480     try
481     {
482       LOCK.readLock().lock();
483
484       if (!(obj instanceof HiddenColumns))
485       {
486         return false;
487       }
488       HiddenColumns that = (HiddenColumns) obj;
489
490       /*
491        * check hidden columns are either both null, or match
492        */
493
494       if (that.hiddenColumns.size() != this.hiddenColumns.size())
495       {
496         return false;
497       }
498
499       Iterator<int[]> it = this.iterator();
500       Iterator<int[]> thatit = that.iterator();
501       while (it.hasNext())
502       {
503         if (!(Arrays.equals(it.next(), thatit.next())))
504         {
505           return false;
506         }
507       }
508       return true;
509
510     } finally
511     {
512       LOCK.readLock().unlock();
513     }
514   }
515
516   /**
517    * Return absolute column index for a visible column index
518    * 
519    * @param column
520    *          int column index in alignment view (count from zero)
521    * @return alignment column index for column
522    */
523   public int visibleToAbsoluteColumn(int column)
524   {
525     try
526     {
527       LOCK.readLock().lock();
528       int result = column;
529
530       if (!hiddenColumns.isEmpty())
531       {
532         result += cursor.findRegionForColumn(column, true).getHiddenSoFar();
533       }
534
535       return result;
536     } finally
537     {
538       LOCK.readLock().unlock();
539     }
540   }
541
542   /**
543    * Use this method to find out where a column will appear in the visible
544    * alignment when hidden columns exist. If the column is not visible, then the
545    * index of the next visible column on the left will be returned (or 0 if
546    * there is no visible column on the left)
547    * 
548    * @param hiddenColumn
549    *          the column index in the full alignment including hidden columns
550    * @return the position of the column in the visible alignment
551    */
552   public int absoluteToVisibleColumn(int hiddenColumn)
553   {
554     try
555     {
556       LOCK.readLock().lock();
557       int result = hiddenColumn;
558
559       if (!hiddenColumns.isEmpty())
560       {
561         HiddenCursorPosition cursorPos = cursor
562                 .findRegionForColumn(hiddenColumn, false);
563         int index = cursorPos.getRegionIndex();
564         int hiddenBeforeCol = cursorPos.getHiddenSoFar();
565
566         // just subtract hidden cols count - this works fine if column is
567         // visible
568         result = hiddenColumn - hiddenBeforeCol;
569
570         // now check in case column is hidden - it will be in the returned
571         // hidden region
572         if (index < hiddenColumns.size())
573         {
574           int[] region = hiddenColumns.get(index);
575           if (hiddenColumn >= region[0] && hiddenColumn <= region[1])
576           {
577             // actually col is hidden, return region[0]-1
578             // unless region[0]==0 in which case return 0
579             if (region[0] == 0)
580             {
581               result = 0;
582             }
583             else
584             {
585               result = region[0] - 1 - hiddenBeforeCol;
586             }
587           }
588         }
589       }
590
591       return result; // return the shifted position after removing hidden
592                      // columns.
593     } finally
594     {
595       LOCK.readLock().unlock();
596     }
597   }
598
599   /**
600    * Find the visible column which is a given visible number of columns to the
601    * left (negative visibleDistance) or right (positive visibleDistance) of
602    * startColumn. If startColumn is not visible, we use the visible column at
603    * the left boundary of the hidden region containing startColumn.
604    * 
605    * @param visibleDistance
606    *          the number of visible columns to offset by (left offset = negative
607    *          value; right offset = positive value)
608    * @param startColumn
609    *          the position of the column to start from (absolute position)
610    * @return the position of the column which is <visibleDistance> away
611    *         (absolute position)
612    */
613   public int offsetByVisibleColumns(int visibleDistance, int startColumn)
614   {
615     try
616     {
617       LOCK.readLock().lock();
618       int start = absoluteToVisibleColumn(startColumn);
619       return visibleToAbsoluteColumn(start + visibleDistance);
620
621     } finally
622     {
623       LOCK.readLock().unlock();
624     }
625   }
626
627   /**
628    * This method returns the rightmost limit of a region of an alignment with
629    * hidden columns. In otherwords, the next hidden column.
630    * 
631    * @param alPos
632    *          the absolute (visible) alignmentPosition to find the next hidden
633    *          column for
634    * @return the index of the next hidden column, or alPos if there is no next
635    *         hidden column
636    */
637   public int getNextHiddenBoundary(boolean left, int alPos)
638   {
639     try
640     {
641       LOCK.readLock().lock();
642       if (!hiddenColumns.isEmpty())
643       {
644         int index = cursor.findRegionForColumn(alPos, false)
645                 .getRegionIndex();
646
647         if (left && index > 0)
648         {
649           int[] region = hiddenColumns.get(index - 1);
650           return region[1];
651         }
652         else if (!left && index < hiddenColumns.size())
653         {
654           int[] region = hiddenColumns.get(index);
655           if (alPos < region[0])
656           {
657             return region[0];
658           }
659           else if ((alPos <= region[1])
660                   && (index + 1 < hiddenColumns.size()))
661           {
662             // alPos is within a hidden region, return the next one
663             // if there is one
664             region = hiddenColumns.get(index + 1);
665             return region[0];
666           }
667         }
668       }
669       return alPos;
670     } finally
671     {
672       LOCK.readLock().unlock();
673     }
674   }
675
676   /**
677    * Answers if a column in the alignment is visible
678    * 
679    * @param column
680    *          absolute position of column in the alignment
681    * @return true if column is visible
682    */
683   public boolean isVisible(int column)
684   {
685     try
686     {
687       LOCK.readLock().lock();
688
689       if (!hiddenColumns.isEmpty())
690       {
691         int regionindex = cursor.findRegionForColumn(column, false)
692                 .getRegionIndex();
693         if (regionindex > -1 && regionindex < hiddenColumns.size())
694         {
695           int[] region = hiddenColumns.get(regionindex);
696           // already know that column <= region[1] as cursor returns containing
697           // region or region to right
698           if (column >= region[0])
699           {
700             return false;
701           }
702         }
703       }
704       return true;
705
706     } finally
707     {
708       LOCK.readLock().unlock();
709     }
710   }
711
712   /**
713    * 
714    * @return true if there are columns hidden
715    */
716   public boolean hasHiddenColumns()
717   {
718     try
719     {
720       LOCK.readLock().lock();
721
722       // we don't use getSize()>0 here because it has to iterate over
723       // the full hiddenColumns collection and so will be much slower
724       return (!hiddenColumns.isEmpty());
725     } finally
726     {
727       LOCK.readLock().unlock();
728     }
729   }
730
731   /**
732    * 
733    * @return true if there is more than one hidden column region
734    */
735   public boolean hasMultiHiddenColumnRegions()
736   {
737     try
738     {
739       LOCK.readLock().lock();
740       return !hiddenColumns.isEmpty() && hiddenColumns.size() > 1;
741     } finally
742     {
743       LOCK.readLock().unlock();
744     }
745   }
746
747   /**
748    * Returns a hashCode built from hidden column ranges
749    */
750   @Override
751   public int hashCode()
752   {
753     try
754     {
755       LOCK.readLock().lock();
756       int hashCode = 1;
757
758       for (int[] hidden : hiddenColumns)
759       {
760         hashCode = HASH_MULTIPLIER * hashCode + hidden[0];
761         hashCode = HASH_MULTIPLIER * hashCode + hidden[1];
762       }
763       return hashCode;
764     } finally
765     {
766       LOCK.readLock().unlock();
767     }
768   }
769
770   /**
771    * Hide columns corresponding to the marked bits
772    * 
773    * @param inserts
774    *          - columns mapped to bits starting from zero
775    */
776   public void hideColumns(BitSet inserts)
777   {
778     hideColumns(inserts, 0, inserts.length() - 1);
779   }
780
781   /**
782    * Hide columns corresponding to the marked bits, within the range
783    * [start,end]. Entries in tohide which are outside [start,end] are ignored.
784    * 
785    * @param tohide
786    *          columns mapped to bits starting from zero
787    * @param start
788    *          start of range to hide columns within
789    * @param end
790    *          end of range to hide columns within
791    */
792   private void hideColumns(BitSet tohide, int start, int end)
793   {
794     try
795     {
796       LOCK.writeLock().lock();
797       for (int firstSet = tohide
798               .nextSetBit(start), lastSet = start; firstSet >= start
799                       && lastSet <= end; firstSet = tohide
800                               .nextSetBit(lastSet))
801       {
802         lastSet = tohide.nextClearBit(firstSet);
803         if (lastSet <= end)
804         {
805           hideColumns(firstSet, lastSet - 1);
806         }
807         else if (firstSet <= end)
808         {
809           hideColumns(firstSet, end);
810         }
811       }
812       cursor = new HiddenColumnsCursor(hiddenColumns);
813     } finally
814     {
815       LOCK.writeLock().unlock();
816     }
817   }
818
819   /**
820    * Hide columns corresponding to the marked bits, within the range
821    * [start,end]. Entries in tohide which are outside [start,end] are ignored.
822    * NB Existing entries in [start,end] are cleared.
823    * 
824    * @param tohide
825    *          columns mapped to bits starting from zero
826    * @param start
827    *          start of range to hide columns within
828    * @param end
829    *          end of range to hide columns within
830    */
831   public void clearAndHideColumns(BitSet tohide, int start, int end)
832   {
833     clearHiddenColumnsInRange(start, end);
834     hideColumns(tohide, start, end);
835   }
836
837   /**
838    * Make all columns in the range [start,end] visible
839    * 
840    * @param start
841    *          start of range to show columns
842    * @param end
843    *          end of range to show columns
844    */
845   private void clearHiddenColumnsInRange(int start, int end)
846   {
847     try
848     {
849       LOCK.writeLock().lock();
850
851       if (!hiddenColumns.isEmpty())
852       {
853         HiddenCursorPosition pos = cursor.findRegionForColumn(start, false);
854         int index = pos.getRegionIndex();
855
856         if (index != -1 && index != hiddenColumns.size())
857         {
858           // regionIndex is the region which either contains start
859           // or lies to the right of start
860           int[] region = hiddenColumns.get(index);
861           if (region[0] < start && region[1] >= start)
862           {
863             // region contains start, truncate so that it ends just before start
864             numColumns -= region[1] - start + 1;
865             region[1] = start - 1;
866             index++;
867           }
868
869           int endi = index;
870           while (endi < hiddenColumns.size())
871           {
872             region = hiddenColumns.get(endi);
873
874             if (region[1] > end)
875             {
876               if (region[0] <= end)
877               {
878                 // region contains end, truncate so it starts just after end
879                 numColumns -= end - region[0] + 1;
880                 region[0] = end + 1;
881               }
882               break;
883             }
884
885             numColumns -= region[1] - region[0] + 1;
886             endi++;
887           }
888           hiddenColumns.subList(index, endi).clear();
889
890         }
891
892         cursor = new HiddenColumnsCursor(hiddenColumns);
893       }
894     } finally
895     {
896       LOCK.writeLock().unlock();
897     }
898   }
899
900   /**
901    * 
902    * @param updates
903    *          BitSet where hidden columns will be marked
904    */
905   protected void andNot(BitSet updates)
906   {
907     try
908     {
909       LOCK.writeLock().lock();
910
911       BitSet hiddenBitSet = new BitSet();
912       for (int[] range : hiddenColumns)
913       {
914         hiddenBitSet.set(range[0], range[1] + 1);
915       }
916       hiddenBitSet.andNot(updates);
917       hiddenColumns.clear();
918       hideColumns(hiddenBitSet);
919     } finally
920     {
921       LOCK.writeLock().unlock();
922     }
923   }
924
925   /**
926    * Calculate the visible start and end index of an alignment.
927    * 
928    * @param width
929    *          full alignment width
930    * @return integer array where: int[0] = startIndex, and int[1] = endIndex
931    */
932   public int[] getVisibleStartAndEndIndex(int width)
933   {
934     try
935     {
936       LOCK.readLock().lock();
937
938       int firstVisible = 0;
939       int lastVisible = width - 1;
940
941       if (!hiddenColumns.isEmpty())
942       {
943         // first visible col with index 0, convert to absolute index
944         firstVisible = visibleToAbsoluteColumn(0);
945
946         // last visible column is either immediately to left of
947         // last hidden region, or is just the last column in the alignment
948         int[] lastregion = hiddenColumns.get(hiddenColumns.size() - 1);
949         if (lastregion[1] == width - 1)
950         {
951           // last region is at very end of alignment
952           // last visible column immediately precedes it
953           lastVisible = lastregion[0] - 1;
954         }
955       }
956       return new int[] { firstVisible, lastVisible };
957
958     } finally
959     {
960       LOCK.readLock().unlock();
961     }
962   }
963
964   /**
965    * Finds the hidden region (if any) which starts or ends at res
966    * 
967    * @param res
968    *          visible residue position, unadjusted for hidden columns
969    * @return region as [start,end] or null if no matching region is found. If
970    *         res is adjacent to two regions, returns the left region.
971    */
972   public int[] getRegionWithEdgeAtRes(int res)
973   {
974     try
975     {
976       LOCK.readLock().lock();
977       int adjres = visibleToAbsoluteColumn(res);
978
979       int[] reveal = null;
980
981       if (!hiddenColumns.isEmpty())
982       {
983         // look for a region ending just before adjres
984         int regionindex = cursor.findRegionForColumn(adjres - 1, false)
985                 .getRegionIndex();
986         if (regionindex < hiddenColumns.size()
987                 && hiddenColumns.get(regionindex)[1] == adjres - 1)
988         {
989           reveal = hiddenColumns.get(regionindex);
990         }
991         // check if the region ends just after adjres
992         else if (regionindex < hiddenColumns.size()
993                 && hiddenColumns.get(regionindex)[0] == adjres + 1)
994         {
995           reveal = hiddenColumns.get(regionindex);
996         }
997       }
998       return reveal;
999
1000     } finally
1001     {
1002       LOCK.readLock().unlock();
1003     }
1004   }
1005
1006   /**
1007    * Return an iterator over the hidden regions
1008    */
1009   public Iterator<int[]> iterator()
1010   {
1011     try
1012     {
1013       LOCK.readLock().lock();
1014       return new RangeIterator(hiddenColumns);
1015     } finally
1016     {
1017       LOCK.readLock().unlock();
1018     }
1019   }
1020
1021   /**
1022    * Return a bounded iterator over the hidden regions
1023    * 
1024    * @param start
1025    *          position to start from (inclusive, absolute column position)
1026    * @param end
1027    *          position to end at (inclusive, absolute column position)
1028    * @return
1029    */
1030   public Iterator<int[]> getBoundedIterator(int start, int end)
1031   {
1032     try
1033     {
1034       LOCK.readLock().lock();
1035       return new RangeIterator(start, end, hiddenColumns);
1036     } finally
1037     {
1038       LOCK.readLock().unlock();
1039     }
1040   }
1041
1042   /**
1043    * Return a bounded iterator over the *visible* start positions of hidden
1044    * regions
1045    * 
1046    * @param start
1047    *          position to start from (inclusive, visible column position)
1048    * @param end
1049    *          position to end at (inclusive, visible column position)
1050    */
1051   public Iterator<Integer> getStartRegionIterator(int start, int end)
1052   {
1053     try
1054     {
1055       LOCK.readLock().lock();
1056
1057       // get absolute position of column in alignment
1058       int absoluteStart = visibleToAbsoluteColumn(start);
1059
1060       // Get cursor position and supply it to the iterator:
1061       // Since we want visible region start, we look for a cursor for the
1062       // (absoluteStart-1), then if absoluteStart is the start of a visible
1063       // region we'll get the cursor pointing to the region before, which is
1064       // what we want
1065       HiddenCursorPosition pos = cursor
1066               .findRegionForColumn(absoluteStart - 1, false);
1067
1068       return new StartRegionIterator(pos, start, end, hiddenColumns);
1069     } finally
1070     {
1071       LOCK.readLock().unlock();
1072     }
1073   }
1074
1075   /**
1076    * Return an iterator over visible *columns* (not regions) between the given
1077    * start and end boundaries
1078    * 
1079    * @param start
1080    *          first column (inclusive)
1081    * @param end
1082    *          last column (inclusive)
1083    */
1084   public Iterator<Integer> getVisibleColsIterator(int start, int end)
1085   {
1086     try
1087     {
1088       LOCK.readLock().lock();
1089       return new RangeElementsIterator(
1090               new VisibleContigsIterator(start, end + 1, hiddenColumns));
1091     } finally
1092     {
1093       LOCK.readLock().unlock();
1094     }
1095   }
1096
1097   /**
1098    * return an iterator over visible segments between the given start and end
1099    * boundaries
1100    * 
1101    * @param start
1102    *          first column, inclusive from 0
1103    * @param end
1104    *          last column - not inclusive
1105    * @param useVisibleCoords
1106    *          if true, start and end are visible column positions, not absolute
1107    *          positions*
1108    */
1109   public VisibleContigsIterator getVisContigsIterator(int start, int end,
1110           boolean useVisibleCoords)
1111   {
1112     int adjstart = start;
1113     int adjend = end;
1114     if (useVisibleCoords)
1115     {
1116       adjstart = visibleToAbsoluteColumn(start);
1117       adjend = visibleToAbsoluteColumn(end);
1118     }
1119
1120     try
1121     {
1122       LOCK.readLock().lock();
1123       return new VisibleContigsIterator(adjstart, adjend, hiddenColumns);
1124     } finally
1125     {
1126       LOCK.readLock().unlock();
1127     }
1128   }
1129 }