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