JAL-3949 fixed failing commandlinetests due to more output and line count limit
[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.add(
139                     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, false);
173         regionindex = cursorPos.getRegionIndex();
174
175         if (regionindex > 0)
176         {
177           // get previous index and hidden count for updating the cursor later
178           previndex = regionindex - 1;
179           int[] prevRegion = hiddenColumns.get(previndex);
180           prevHiddenCount = cursorPos.getHiddenSoFar()
181                   - (prevRegion[1] - prevRegion[0] + 1);
182         }
183       }
184
185       // new range follows everything else; check first to avoid looping over
186       // whole hiddenColumns collection
187       if (hiddenColumns.isEmpty()
188               || start > hiddenColumns.get(hiddenColumns.size() - 1)[1])
189       {
190         hiddenColumns.add(new int[] { start, end });
191         numColumns += end - start + 1;
192       }
193       else
194       {
195         /*
196          * traverse existing hidden ranges and insert / amend / append as
197          * appropriate
198          */
199         boolean added = false;
200         if (regionindex > 0)
201         {
202           added = insertRangeAtRegion(regionindex - 1, start, end);
203         }
204         if (!added && regionindex < hiddenColumns.size())
205         {
206           insertRangeAtRegion(regionindex, start, end);
207         }
208       }
209
210       // reset the cursor to just before our insertion point: this saves
211       // a lot of reprocessing in large alignments
212       cursor = new HiddenColumnsCursor(hiddenColumns, previndex,
213               prevHiddenCount);
214     } finally
215     {
216       LOCK.writeLock().unlock();
217     }
218   }
219
220   /**
221    * Insert [start, range] at the region at index i in hiddenColumns, if
222    * feasible
223    * 
224    * @param i
225    *          index to insert at
226    * @param start
227    *          start of range to insert
228    * @param end
229    *          end of range to insert
230    * @return true if range was successfully inserted
231    */
232   private boolean insertRangeAtRegion(int i, int start, int end)
233   {
234     boolean added = false;
235
236     int[] region = hiddenColumns.get(i);
237     if (end < region[0] - 1)
238     {
239       /*
240        * insert discontiguous preceding range
241        */
242       hiddenColumns.add(i, new int[] { start, end });
243       numColumns += end - start + 1;
244       added = true;
245     }
246     else if (end <= region[1])
247     {
248       /*
249        * new range overlaps existing, or is contiguous preceding it - adjust
250        * start column
251        */
252       int oldstart = region[0];
253       region[0] = Math.min(region[0], start);
254       numColumns += oldstart - region[0]; // new columns are between old and
255                                               // adjusted starts
256       added = true;
257     }
258     else if (start <= region[1] + 1)
259     {
260       /*
261        * new range overlaps existing, or is contiguous following it - adjust
262        * start and end columns
263        */
264       insertRangeAtOverlap(i, start, end, region);
265       added = true;
266     }
267     return added;
268   }
269
270   /**
271    * Insert a range whose start position overlaps an existing region and/or is
272    * contiguous to the right of the region
273    * 
274    * @param i
275    *          index to insert at
276    * @param start
277    *          start of range to insert
278    * @param end
279    *          end of range to insert
280    * @param region
281    *          the overlapped/continued region
282    */
283   private void insertRangeAtOverlap(int i, int start, int end, int[] region)
284   {
285     int oldstart = region[0];
286     int oldend = region[1];
287     region[0] = Math.min(region[0], start);
288     region[1] = Math.max(region[1], end);
289
290     numColumns += oldstart - region[0];
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       region[1] = Math.max(nextRegion[1], end);
309       endi++;
310     }
311     numColumns += region[1] - oldend;
312     hiddenColumns.subList(i + 1, endi + 1).clear();
313   }
314
315   /**
316    * hide a list of ranges
317    * 
318    * @param ranges
319    */
320   public void hideList(List<int[]> ranges)
321   {
322     try
323     {
324       LOCK.writeLock().lock();
325       for (int[] r : ranges)
326       {
327         hideColumns(r[0], r[1]);
328       }
329       cursor = new HiddenColumnsCursor(hiddenColumns);
330
331     } finally
332     {
333       LOCK.writeLock().unlock();
334     }
335   }
336
337   /**
338    * Unhides, and adds to the selection list, all hidden columns
339    */
340   public void revealAllHiddenColumns(ColumnSelection sel)
341   {
342     try
343     {
344       LOCK.writeLock().lock();
345
346       for (int[] region : hiddenColumns)
347       {
348         for (int j = region[0]; j < region[1] + 1; j++)
349         {
350           sel.addElement(j);
351         }
352       }
353       hiddenColumns.clear();
354       cursor = new HiddenColumnsCursor(hiddenColumns);
355       numColumns = 0;
356
357     } finally
358     {
359       LOCK.writeLock().unlock();
360     }
361   }
362
363   /**
364    * Reveals, and marks as selected, the hidden column range with the given
365    * start column
366    * 
367    * @param start
368    *          the start column to look for
369    * @param sel
370    *          the column selection to add the hidden column range to
371    */
372   public void revealHiddenColumns(int start, ColumnSelection sel)
373   {
374     try
375     {
376       LOCK.writeLock().lock();
377
378       if (!hiddenColumns.isEmpty())
379       {
380         int regionIndex = cursor.findRegionForColumn(start, false)
381                 .getRegionIndex();
382
383         if (regionIndex != -1 && regionIndex != hiddenColumns.size())
384         {
385           // regionIndex is the region which either contains start
386           // or lies to the right of start
387           int[] region = hiddenColumns.get(regionIndex);
388           if (start == region[0])
389           {
390             for (int j = region[0]; j < region[1] + 1; j++)
391             {
392               sel.addElement(j);
393             }
394             int colsToRemove = region[1] - region[0] + 1;
395             hiddenColumns.remove(regionIndex);
396             numColumns -= colsToRemove;
397           }
398         }
399       }
400     } finally
401     {
402       LOCK.writeLock().unlock();
403     }
404   }
405
406   /**
407    * Output regions data as a string. String is in the format:
408    * reg0[0]<between>reg0[1]<delimiter>reg1[0]<between>reg1[1] ... regn[1]
409    * 
410    * @param delimiter
411    *          string to delimit regions
412    * @param betweenstring
413    *          to put between start and end region values
414    * @return regions formatted according to delimiter and between strings
415    */
416   public String regionsToString(String delimiter, String between)
417   {
418     try
419     {
420       LOCK.readLock().lock();
421       StringBuilder regionBuilder = new StringBuilder();
422
423       boolean first = true;
424       for (int[] range : hiddenColumns)
425       {
426         if (!first)
427         {
428           regionBuilder.append(delimiter);
429         }
430         else
431         {
432           first = false;
433         }
434         regionBuilder.append(range[0]).append(between).append(range[1]);
435
436       }
437
438       return regionBuilder.toString();
439     } finally
440     {
441       LOCK.readLock().unlock();
442     }
443   }
444
445   /**
446    * Find the number of hidden columns
447    * 
448    * @return number of hidden columns
449    */
450   public int getSize()
451   {
452     return numColumns;
453   }
454
455   /**
456    * Get the number of distinct hidden regions
457    * 
458    * @return number of regions
459    */
460   public int getNumberOfRegions()
461   {
462     try
463     {
464       LOCK.readLock().lock();
465       return hiddenColumns.size();
466     } finally
467     {
468       LOCK.readLock().unlock();
469     }
470   }
471
472   /**
473    * Answers true if obj is an instance of HiddenColumns, and holds the same
474    * array of start-end column ranges as this, else answers false
475    */
476   @Override
477   public boolean equals(Object obj)
478   {
479     try
480     {
481       LOCK.readLock().lock();
482
483       if (!(obj instanceof HiddenColumns))
484       {
485         return false;
486       }
487       HiddenColumns that = (HiddenColumns) obj;
488
489       /*
490        * check hidden columns are either both null, or match
491        */
492
493       if (that.hiddenColumns.size() != this.hiddenColumns.size())
494       {
495         return false;
496       }
497
498       Iterator<int[]> it = this.iterator();
499       Iterator<int[]> thatit = that.iterator();
500       while (it.hasNext())
501       {
502         if (!(Arrays.equals(it.next(), thatit.next())))
503         {
504           return false;
505         }
506       }
507       return true;
508
509     } finally
510     {
511       LOCK.readLock().unlock();
512     }
513   }
514
515   /**
516    * Return absolute column index for a visible column index
517    * 
518    * @param column
519    *          int column index in alignment view (count from zero)
520    * @return alignment column index for column
521    */
522   public int visibleToAbsoluteColumn(int column)
523   {
524     try
525     {
526       LOCK.readLock().lock();
527       int result = column;
528
529       if (!hiddenColumns.isEmpty())
530       {
531         result += cursor.findRegionForColumn(column, true)
532                 .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   /**
749    * Returns a hashCode built from hidden column ranges
750    */
751   @Override
752   public int hashCode()
753   {
754     try
755     {
756       LOCK.readLock().lock();
757       int hashCode = 1;
758
759       for (int[] hidden : hiddenColumns)
760       {
761         hashCode = HASH_MULTIPLIER * hashCode + hidden[0];
762         hashCode = HASH_MULTIPLIER * hashCode + hidden[1];
763       }
764       return hashCode;
765     } finally
766     {
767       LOCK.readLock().unlock();
768     }
769   }
770
771   /**
772    * Hide columns corresponding to the marked bits
773    * 
774    * @param inserts
775    *          - columns mapped to bits starting from zero
776    */
777   public void hideColumns(BitSet inserts)
778   {
779     hideColumns(inserts, 0, inserts.length() - 1);
780   }
781
782   /**
783    * Hide columns corresponding to the marked bits, within the range
784    * [start,end]. Entries in tohide which are outside [start,end] are ignored.
785    * 
786    * @param tohide
787    *          columns mapped to bits starting from zero
788    * @param start
789    *          start of range to hide columns within
790    * @param end
791    *          end of range to hide columns within
792    */
793   private void hideColumns(BitSet tohide, int start, int end)
794   {
795     try
796     {
797       LOCK.writeLock().lock();
798       for (int firstSet = tohide
799               .nextSetBit(start), lastSet = start; firstSet >= start
800                       && lastSet <= end; firstSet = tohide
801                       .nextSetBit(lastSet))
802       {
803         lastSet = tohide.nextClearBit(firstSet);
804         if (lastSet <= end)
805         {
806           hideColumns(firstSet, lastSet - 1);
807         }
808         else if (firstSet <= end)
809         {
810           hideColumns(firstSet, end);
811         }
812       }
813       cursor = new HiddenColumnsCursor(hiddenColumns);
814     } finally
815     {
816       LOCK.writeLock().unlock();
817     }
818   }
819
820   /**
821    * Hide columns corresponding to the marked bits, within the range
822    * [start,end]. Entries in tohide which are outside [start,end] are ignored.
823    * NB Existing entries in [start,end] are cleared.
824    * 
825    * @param tohide
826    *          columns mapped to bits starting from zero
827    * @param start
828    *          start of range to hide columns within
829    * @param end
830    *          end of range to hide columns within
831    */
832   public void clearAndHideColumns(BitSet tohide, int start, int end)
833   {
834     clearHiddenColumnsInRange(start, end);
835     hideColumns(tohide, start, end);
836   }
837
838   /**
839    * Make all columns in the range [start,end] visible
840    * 
841    * @param start
842    *          start of range to show columns
843    * @param end
844    *          end of range to show columns
845    */
846   private void clearHiddenColumnsInRange(int start, int end)
847   {
848     try
849     {
850       LOCK.writeLock().lock();
851       
852       if (!hiddenColumns.isEmpty())
853       {
854         HiddenCursorPosition pos = cursor.findRegionForColumn(start, false);
855         int index = pos.getRegionIndex();
856
857         if (index != -1 && index != hiddenColumns.size())
858         {
859           // regionIndex is the region which either contains start
860           // or lies to the right of start
861           int[] region = hiddenColumns.get(index);
862           if (region[0] < start && region[1] >= start)
863           {
864             // region contains start, truncate so that it ends just before start
865             numColumns -= region[1] - start + 1;
866             region[1] = start - 1;
867             index++;
868           }
869
870           int endi = index;
871           while (endi < hiddenColumns.size())
872           {
873             region = hiddenColumns.get(endi);
874
875             if (region[1] > end)
876             {
877               if (region[0] <= end)
878               {
879                 // region contains end, truncate so it starts just after end
880                 numColumns -= end - region[0] + 1;
881                 region[0] = end + 1;
882               }
883               break;
884             }
885
886             numColumns -= region[1] - region[0] + 1;
887             endi++;
888           }
889           hiddenColumns.subList(index, endi).clear();
890
891         }
892
893         cursor = new HiddenColumnsCursor(hiddenColumns);
894       }
895     } finally
896     {
897       LOCK.writeLock().unlock();
898     }
899   }
900
901   /**
902    * 
903    * @param updates
904    *          BitSet where hidden columns will be marked
905    */
906   protected void andNot(BitSet updates)
907   {
908     try
909     {
910       LOCK.writeLock().lock();
911
912       BitSet hiddenBitSet = new BitSet();
913       for (int[] range : hiddenColumns)
914       {
915         hiddenBitSet.set(range[0], range[1] + 1);
916       }
917       hiddenBitSet.andNot(updates);
918       hiddenColumns.clear();
919       hideColumns(hiddenBitSet);
920     } finally
921     {
922       LOCK.writeLock().unlock();
923     }
924   }
925
926   /**
927    * Calculate the visible start and end index of an alignment.
928    * 
929    * @param width
930    *          full alignment width
931    * @return integer array where: int[0] = startIndex, and int[1] = endIndex
932    */
933   public int[] getVisibleStartAndEndIndex(int width)
934   {
935     try
936     {
937       LOCK.readLock().lock();
938
939       int firstVisible = 0;
940       int lastVisible = width - 1;
941
942       if (!hiddenColumns.isEmpty())
943       {
944         // first visible col with index 0, convert to absolute index
945         firstVisible = visibleToAbsoluteColumn(0);
946
947         // last visible column is either immediately to left of
948         // last hidden region, or is just the last column in the alignment
949         int[] lastregion = hiddenColumns.get(hiddenColumns.size() - 1);
950         if (lastregion[1] == width - 1)
951         {
952           // last region is at very end of alignment
953           // last visible column immediately precedes it
954           lastVisible = lastregion[0] - 1;
955         }
956       }
957       return new int[] { firstVisible, lastVisible };
958
959     } finally
960     {
961       LOCK.readLock().unlock();
962     }
963   }
964
965   /**
966    * Finds the hidden region (if any) which starts or ends at res
967    * 
968    * @param res
969    *          visible residue position, unadjusted for hidden columns
970    * @return region as [start,end] or null if no matching region is found. If
971    *         res is adjacent to two regions, returns the left region.
972    */
973   public int[] getRegionWithEdgeAtRes(int res)
974   {
975     try
976     {
977       LOCK.readLock().lock();
978       int adjres = visibleToAbsoluteColumn(res);
979
980       int[] reveal = null;
981
982       if (!hiddenColumns.isEmpty())
983       {
984         // look for a region ending just before adjres
985         int regionindex = cursor.findRegionForColumn(adjres - 1, false)
986                 .getRegionIndex();
987         if (regionindex < hiddenColumns.size()
988                 && hiddenColumns.get(regionindex)[1] == adjres - 1)
989         {
990           reveal = hiddenColumns.get(regionindex);
991         }
992         // check if the region ends just after adjres
993         else if (regionindex < hiddenColumns.size()
994                 && hiddenColumns.get(regionindex)[0] == adjres + 1)
995         {
996           reveal = hiddenColumns.get(regionindex);
997         }
998       }
999       return reveal;
1000
1001     } finally
1002     {
1003       LOCK.readLock().unlock();
1004     }
1005   }
1006
1007   /**
1008    * Return an iterator over the hidden regions
1009    */
1010   public Iterator<int[]> iterator()
1011   {
1012     try
1013     {
1014       LOCK.readLock().lock();
1015       return new RangeIterator(hiddenColumns);
1016     } finally
1017     {
1018       LOCK.readLock().unlock();
1019     }
1020   }
1021
1022   /**
1023    * Return a bounded iterator over the hidden regions
1024    * 
1025    * @param start
1026    *          position to start from (inclusive, absolute column position)
1027    * @param end
1028    *          position to end at (inclusive, absolute column position)
1029    * @return
1030    */
1031   public Iterator<int[]> getBoundedIterator(int start, int end)
1032   {
1033     try
1034     {
1035       LOCK.readLock().lock();
1036       return new RangeIterator(start, end, hiddenColumns);
1037     } finally
1038     {
1039       LOCK.readLock().unlock();
1040     }
1041   }
1042
1043   /**
1044    * Return a bounded iterator over the *visible* start positions of hidden
1045    * regions
1046    * 
1047    * @param start
1048    *          position to start from (inclusive, visible column position)
1049    * @param end
1050    *          position to end at (inclusive, visible column position)
1051    */
1052   public Iterator<Integer> getStartRegionIterator(int start, int end)
1053   {
1054     try
1055     {
1056       LOCK.readLock().lock();
1057
1058       // get absolute position of column in alignment
1059       int absoluteStart = visibleToAbsoluteColumn(start);
1060
1061       // Get cursor position and supply it to the iterator:
1062       // Since we want visible region start, we look for a cursor for the
1063       // (absoluteStart-1), then if absoluteStart is the start of a visible
1064       // region we'll get the cursor pointing to the region before, which is
1065       // what we want
1066       HiddenCursorPosition pos = cursor
1067               .findRegionForColumn(absoluteStart - 1, false);
1068
1069       return new StartRegionIterator(pos, start, end,
1070               hiddenColumns);
1071     } finally
1072     {
1073       LOCK.readLock().unlock();
1074     }
1075   }
1076
1077   /**
1078    * Return an iterator over visible *columns* (not regions) between the given
1079    * start and end boundaries
1080    * 
1081    * @param start
1082    *          first column (inclusive)
1083    * @param end
1084    *          last column (inclusive)
1085    */
1086   public Iterator<Integer> getVisibleColsIterator(int start, int end)
1087   {
1088     try
1089     {
1090       LOCK.readLock().lock();
1091       return new RangeElementsIterator(
1092               new VisibleContigsIterator(start, end + 1, hiddenColumns));
1093     } finally
1094     {
1095       LOCK.readLock().unlock();
1096     }
1097   }
1098
1099   /**
1100    * return an iterator over visible segments between the given start and end
1101    * boundaries
1102    * 
1103    * @param start
1104    *          first column, inclusive from 0
1105    * @param end
1106    *          last column - not inclusive
1107    * @param useVisibleCoords
1108    *          if true, start and end are visible column positions, not absolute
1109    *          positions*
1110    */
1111   public VisibleContigsIterator getVisContigsIterator(int start,
1112           int end,
1113           boolean useVisibleCoords)
1114   {
1115     int adjstart = start;
1116     int adjend = end;
1117     if (useVisibleCoords)
1118     {
1119       adjstart = visibleToAbsoluteColumn(start);
1120       adjend = visibleToAbsoluteColumn(end);
1121     }
1122
1123     try
1124     {
1125       LOCK.readLock().lock();
1126       return new VisibleContigsIterator(adjstart, adjend, hiddenColumns);
1127     } finally
1128     {
1129       LOCK.readLock().unlock();
1130     }
1131   }
1132 }