JAL-2759 Refactor of hideInsertionsFor after review
[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.BitSet;
25 import java.util.Iterator;
26 import java.util.List;
27 import java.util.concurrent.locks.ReentrantReadWriteLock;
28
29 /**
30  * This class manages the collection of hidden columns associated with an
31  * alignment. To iterate over the collection, or over visible columns/regions,
32  * use an iterator obtained from one of:
33  * 
34  * - getBoundedIterator: iterates over the hidden regions, within some bounds,
35  * returning absolute positions
36  * 
37  * - getBoundedStartIterator: iterates over the start positions of hidden
38  * regions, within some bounds, returning visible positions
39  * 
40  * - getVisContigsIterator: iterates over visible regions in a range, returning
41  * absolute positions
42  * 
43  * - getVisibleColsIterator: iterates over the visible *columns*
44  * 
45  * For performance reasons, provide bounds where possible. Note that column
46  * numbering begins at 0 throughout this class.
47  * 
48  * @author kmourao
49  *
50  */
51 public class HiddenColumns
52 {
53   private static final int HASH_MULTIPLIER = 31;
54
55   private static final ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();
56
57   /*
58    * Cursor which tracks the last used hidden columns region, and the number 
59    * of hidden columns up to (but not including) that region.
60    */
61   private HiddenColumnsCursor cursor = new HiddenColumnsCursor();
62
63   /*
64    * cache of the number of hidden columns
65    */
66   private int numColumns = 0;
67
68   /*
69    * list of hidden column [start, end] ranges; the list is maintained in
70    * ascending start column order
71    */
72   private List<int[]> hiddenColumns;
73
74   /**
75    * Constructor
76    */
77   public HiddenColumns()
78   {
79   }
80
81   /* 
82    * Methods which change the hiddenColumns collection. These methods should 
83    * use a writeLock to prevent other threads accessing the hiddenColumns
84    * collection while changes are being made. They should also reset the hidden
85    * columns cursor, and either update the hidden columns count, or set it to 0 
86    * (so that it will later be updated when needed).
87    */
88
89   /**
90    * Copy constructor
91    * 
92    * @param copy
93    *          the HiddenColumns object to copy from
94    */
95   public HiddenColumns(HiddenColumns copy)
96   {
97     this(copy, Integer.MIN_VALUE, Integer.MAX_VALUE, 0);
98   }
99
100   /**
101    * Copy constructor within bounds and with offset. Copies hidden column
102    * regions fully contained between start and end, and offsets positions by
103    * subtracting offset.
104    * 
105    * @param copy
106    *          HiddenColumns instance to copy from
107    * @param start
108    *          lower bound to copy from
109    * @param end
110    *          upper bound to copy to
111    * @param offset
112    *          offset to subtract from each region boundary position
113    * 
114    */
115   public HiddenColumns(HiddenColumns copy, int start, int end, int offset)
116   {
117     try
118     {
119       LOCK.writeLock().lock();
120       if (copy != null)
121       {
122         hiddenColumns = new ArrayList<>();
123         numColumns = 0;
124         Iterator<int[]> it = copy.getBoundedIterator(start, end);
125         while (it.hasNext())
126         {
127           int[] region = it.next();
128           // still need to check boundaries because iterator returns
129           // all overlapping regions and we need contained regions
130           if (region[0] >= start && region[1] <= end)
131           {
132             hiddenColumns.add(
133                     new int[]
134             { region[0] - offset, region[1] - offset });
135             numColumns += region[1] - region[0] + 1;
136           }
137         }
138         cursor.resetCursor(hiddenColumns);
139       }
140     } finally
141     {
142       LOCK.writeLock().unlock();
143     }
144   }
145
146   /**
147    * Adds the specified column range to the hidden columns collection
148    * 
149    * @param start
150    *          start of range to add (absolute position in alignment)
151    * @param end
152    *          end of range to add (absolute position in alignment)
153    */
154   public void hideColumns(int start, int end)
155   {
156     boolean wasAlreadyLocked = false;
157     try
158     {
159       // check if the write lock was already locked by this thread,
160       // as this method can be called internally in loops within HiddenColumns
161       if (!LOCK.isWriteLockedByCurrentThread())
162       {
163         LOCK.writeLock().lock();
164       }
165       else
166       {
167         wasAlreadyLocked = true;
168       }
169
170       int previndex = 0;
171       int prevHiddenCount = 0;
172       int regionindex = 0;
173       if (hiddenColumns == null)
174       {
175         hiddenColumns = new ArrayList<>();
176       }
177       else
178       {
179         // set up cursor reset values
180         HiddenCursorPosition cursorPos = cursor.findRegionForColumn(start);
181         regionindex = cursorPos.getRegionIndex();
182
183         if (regionindex > 0)
184         {
185           // get previous index and hidden count for updating the cursor later
186           previndex = regionindex - 1;
187           int[] prevRegion = hiddenColumns.get(previndex);
188           prevHiddenCount = cursorPos.getHiddenSoFar()
189                   - (prevRegion[1] - prevRegion[0] + 1);
190         }
191       }
192
193       /*
194        * new range follows everything else; check first to avoid looping over whole hiddenColumns collection
195        */
196       if (hiddenColumns.isEmpty()
197               || start > hiddenColumns.get(hiddenColumns.size() - 1)[1])
198       {
199         hiddenColumns.add(new int[] { start, end });
200       }
201       else
202       {
203         /*
204          * traverse existing hidden ranges and insert / amend / append as
205          * appropriate
206          */
207         boolean added = false;
208         if (regionindex > 0)
209         {
210           added = insertRangeAtRegion(regionindex - 1, start, end);
211         }
212         if (!added && regionindex < hiddenColumns.size())
213         {
214           insertRangeAtRegion(regionindex, start, end);
215         }
216       }
217
218       // reset the cursor to just before our insertion point: this saves
219       // a lot of reprocessing in large alignments
220       cursor.resetCursor(hiddenColumns, previndex, prevHiddenCount);
221
222       // reset the number of columns so they will be recounted
223       numColumns = 0;
224
225     } finally
226     {
227       if (!wasAlreadyLocked)
228       {
229         LOCK.writeLock().unlock();
230       }
231     }
232   }
233
234   /**
235    * Insert [start, range] at the region at index i in hiddenColumns, if
236    * feasible
237    * 
238    * @param i
239    *          index to insert at
240    * @param start
241    *          start of range to insert
242    * @param end
243    *          end of range to insert
244    * @return true if range was successfully inserted
245    */
246   private boolean insertRangeAtRegion(int i, int start, int end)
247   {
248     boolean added = false;
249
250     int[] region = hiddenColumns.get(i);
251     if (end < region[0] - 1)
252     {
253       /*
254        * insert discontiguous preceding range
255        */
256       hiddenColumns.add(i, new int[] { start, end });
257       added = true;
258     }
259     else if (end <= region[1])
260     {
261       /*
262        * new range overlaps existing, or is contiguous preceding it - adjust
263        * start column
264        */
265       region[0] = Math.min(region[0], start);
266       added = true;
267     }
268     else if (start <= region[1] + 1)
269     {
270       /*
271        * new range overlaps existing, or is contiguous following it - adjust
272        * start and end columns
273        */
274       region[0] = Math.min(region[0], start);
275       region[1] = Math.max(region[1], end);
276
277       /*
278        * also update or remove any subsequent ranges 
279        * that are overlapped
280        */
281       while (i < hiddenColumns.size() - 1)
282       {
283         int[] nextRegion = hiddenColumns.get(i + 1);
284         if (nextRegion[0] > end + 1)
285         {
286           /*
287            * gap to next hidden range - no more to update
288            */
289           break;
290         }
291         region[1] = Math.max(nextRegion[1], end);
292
293         // in theory this is faster than hiddenColumns.remove(i+1)
294         // benchmarking results a bit ambivalent
295         hiddenColumns.subList(i + 1, i + 2).clear();
296       }
297       added = true;
298     }
299     return added;
300   }
301
302   /**
303    * mark the columns corresponding to gap characters as hidden in the column
304    * selection
305    * 
306    * @param insertions
307    */
308   public void hideInsertionsFor(List<int[]> inserts)
309   {
310     try
311     {
312       LOCK.writeLock().lock();
313       for (int[] r : inserts)
314       {
315         hideColumns(r[0], r[1]);
316       }
317       cursor.resetCursor(hiddenColumns);
318       numColumns = 0;
319     } finally
320     {
321       LOCK.writeLock().unlock();
322     }
323   }
324
325   /**
326    * Unhides, and adds to the selection list, all hidden columns
327    */
328   public void revealAllHiddenColumns(ColumnSelection sel)
329   {
330     try
331     {
332       LOCK.writeLock().lock();
333       if (hiddenColumns != null)
334       {
335         Iterator<int[]> it = hiddenColumns.iterator();
336         while (it.hasNext())
337         {
338           int[] region = it.next();
339           for (int j = region[0]; j < region[1] + 1; j++)
340           {
341             sel.addElement(j);
342           }
343         }
344         hiddenColumns = null;
345         cursor.resetCursor(hiddenColumns);
346         numColumns = 0;
347       }
348     } finally
349     {
350       LOCK.writeLock().unlock();
351     }
352   }
353
354   /**
355    * Reveals, and marks as selected, the hidden column range with the given
356    * start column
357    * 
358    * @param start
359    *          the start column to look for
360    * @param sel
361    *          the column selection to add the hidden column range to
362    */
363   public void revealHiddenColumns(int start, ColumnSelection sel)
364   {
365     try
366     {
367       LOCK.writeLock().lock();
368
369       if (hiddenColumns != null)
370       {
371         int regionIndex = cursor.findRegionForColumn(start)
372                 .getRegionIndex();
373
374         if (regionIndex != -1 && regionIndex != hiddenColumns.size())
375         {
376           // regionIndex is the region which either contains start
377           // or lies to the right of start
378           int[] region = hiddenColumns.get(regionIndex);
379           if (start == region[0])
380           {
381             for (int j = region[0]; j < region[1] + 1; j++)
382             {
383               sel.addElement(j);
384             }
385             int colsToRemove = region[1] - region[0] + 1;
386             hiddenColumns.remove(regionIndex);
387
388             if (hiddenColumns.isEmpty())
389             {
390               hiddenColumns = null;
391               numColumns = 0;
392             }
393             else
394             {
395               numColumns -= colsToRemove;
396             }
397             cursor.updateForDeletedRegion(hiddenColumns);
398           }
399         }
400       }
401     } finally
402     {
403       LOCK.writeLock().unlock();
404     }
405   }
406
407   /**
408    * Add gaps into the sequences aligned to profileseq under the given
409    * AlignmentView
410    * 
411    * @param profileseq
412    *          sequence in al which sequences are aligned to
413    * @param al
414    *          alignment to have gaps inserted into it
415    * @param input
416    *          alignment view where sequence corresponding to profileseq is first
417    *          entry
418    * @return new HiddenColumns for new alignment view, with insertions into
419    *         profileseq marked as hidden.
420    */
421   public static HiddenColumns propagateInsertions(SequenceI profileseq,
422           AlignmentI al, AlignmentView input)
423   {
424     int profsqpos = 0;
425
426     char gc = al.getGapCharacter();
427     Object[] alandhidden = input.getAlignmentAndHiddenColumns(gc);
428     HiddenColumns nview = (HiddenColumns) alandhidden[1];
429     SequenceI origseq = ((SequenceI[]) alandhidden[0])[profsqpos];
430     nview.propagateInsertions(profileseq, al, origseq);
431     return nview;
432   }
433
434   /**
435    * 
436    * @param profileseq
437    *          sequence in al which corresponds to origseq
438    * @param al
439    *          alignment which is to have gaps inserted into it
440    * @param origseq
441    *          sequence corresponding to profileseq which defines gap map for
442    *          modifying al
443    */
444   private void propagateInsertions(SequenceI profileseq, AlignmentI al,
445           SequenceI origseq)
446   {
447     try
448     {
449       LOCK.writeLock().lock();
450
451       char gc = al.getGapCharacter();
452
453       // take the set of hidden columns, and the set of gaps in origseq,
454       // and remove all the hidden gaps from hiddenColumns
455
456       // first get the gaps as a Bitset
457       BitSet gaps = origseq.gapBitset();
458
459       // now calculate hidden ^ not(gap)
460       BitSet hidden = new BitSet();
461       markHiddenRegions(hidden);
462       hidden.andNot(gaps);
463       hiddenColumns = null;
464       this.hideColumns(hidden);
465
466       // for each sequence in the alignment, except the profile sequence,
467       // insert gaps corresponding to each hidden region but where each hidden
468       // column region is shifted backwards by the number of preceding visible
469       // gaps update hidden columns at the same time
470       Iterator<int[]> regions = hiddenColumns.iterator();
471       List<int[]> newhidden = new ArrayList<>();
472
473       int numGapsBefore = 0;
474       int gapPosition = 0;
475       while (regions.hasNext())
476       {
477         // get region coordinates accounting for gaps
478         // we can rely on gaps not being *in* hidden regions because we already
479         // removed those
480         int[] region = regions.next();
481         while (gapPosition < region[0])
482         {
483           gapPosition++;
484           if (gaps.get(gapPosition))
485           {
486             numGapsBefore++;
487           }
488         }
489
490         int left = region[0] - numGapsBefore;
491         int right = region[1] - numGapsBefore;
492         newhidden.add(new int[] { left, right });
493
494         // make a string with number of gaps = length of hidden region
495         StringBuffer sb = new StringBuffer();
496         for (int s = 0; s < right - left + 1; s++)
497         {
498           sb.append(gc);
499         }
500         padGaps(sb, left, profileseq, al);
501
502       }
503       hiddenColumns = newhidden;
504       cursor.resetCursor(hiddenColumns);
505       numColumns = 0;
506     } finally
507     {
508       LOCK.writeLock().unlock();
509     }
510   }
511
512   /**
513    * Pad gaps in all sequences in alignment except profileseq
514    * 
515    * @param sb
516    *          gap string to insert
517    * @param left
518    *          position to insert at
519    * @param profileseq
520    *          sequence not to pad
521    * @param al
522    *          alignment to pad sequences in
523    */
524   private void padGaps(StringBuffer sb, int pos, SequenceI profileseq,
525           AlignmentI al)
526   {
527     // loop over the sequences and pad with gaps where required
528     for (int s = 0, ns = al.getHeight(); s < ns; s++)
529     {
530       SequenceI sqobj = al.getSequenceAt(s);
531       if (sqobj != profileseq)
532       {
533         String sq = al.getSequenceAt(s).getSequenceAsString();
534         if (sq.length() <= pos)
535         {
536           // pad sequence
537           int diff = pos - sq.length() - 1;
538           if (diff > 0)
539           {
540             // pad gaps
541             sq = sq + sb;
542             while ((diff = pos - sq.length() - 1) > 0)
543             {
544               if (diff >= sb.length())
545               {
546                 sq += sb.toString();
547               }
548               else
549               {
550                 char[] buf = new char[diff];
551                 sb.getChars(0, diff, buf, 0);
552                 sq += buf.toString();
553               }
554             }
555           }
556           sq += sb.toString();
557         }
558         else
559         {
560           al.getSequenceAt(s).setSequence(
561                   sq.substring(0, pos) + sb.toString() + sq.substring(pos));
562         }
563       }
564     }
565   }
566
567   /*
568    * Methods which only need read access to the hidden columns collection. 
569    * These methods should use a readLock to prevent other threads changing
570    * the hidden columns collection while it is in use.
571    */
572
573   /**
574    * Output regions data as a string. String is in the format:
575    * reg0[0]<between>reg0[1]<delimiter>reg1[0]<between>reg1[1] ... regn[1]
576    * 
577    * @param delimiter
578    *          string to delimit regions
579    * @param betweenstring
580    *          to put between start and end region values
581    * @return regions formatted according to delimiter and between strings
582    */
583   public String regionsToString(String delimiter, String between)
584   {
585     try
586     {
587       LOCK.readLock().lock();
588       StringBuilder regionBuilder = new StringBuilder();
589       if (hiddenColumns != null)
590       {
591         Iterator<int[]> it = hiddenColumns.iterator();
592         while (it.hasNext())
593         {
594           int[] range = it.next();
595           regionBuilder.append(delimiter).append(range[0]).append(between)
596                   .append(range[1]);
597           if (!it.hasNext())
598           {
599             regionBuilder.deleteCharAt(0);
600           }
601         }
602       }
603       return regionBuilder.toString();
604     } finally
605     {
606       LOCK.readLock().unlock();
607     }
608   }
609
610   /**
611    * Find the number of hidden columns
612    * 
613    * @return number of hidden columns
614    */
615   public int getSize()
616   {
617     try
618     {
619       LOCK.readLock().lock();
620
621       if (numColumns == 0 && hiddenColumns != null)
622       {
623         // numColumns is out of date, so recalculate
624         int size = 0;
625
626         Iterator<int[]> it = hiddenColumns.iterator();
627         while (it.hasNext())
628         {
629           int[] range = it.next();
630           size += range[1] - range[0] + 1;
631         }
632
633         numColumns = size;
634       }
635
636       return numColumns;
637     } finally
638     {
639       LOCK.readLock().unlock();
640     }
641   }
642
643   /**
644    * Get the number of distinct hidden regions
645    * 
646    * @return number of regions
647    */
648   public int getNumberOfRegions()
649   {
650     try
651     {
652       LOCK.readLock().lock();
653       int num = 0;
654       if (hasHiddenColumns())
655       {
656         num = hiddenColumns.size();
657       }
658       return num;
659     } finally
660     {
661       LOCK.readLock().unlock();
662     }
663   }
664
665   @Override
666   public boolean equals(Object obj)
667   {
668     try
669     {
670       LOCK.readLock().lock();
671
672       if (!(obj instanceof HiddenColumns))
673       {
674         return false;
675       }
676       HiddenColumns that = (HiddenColumns) obj;
677
678       /*
679        * check hidden columns are either both null, or match
680        */
681       if (this.hiddenColumns == null)
682       {
683         return (that.hiddenColumns == null);
684       }
685       if (that.hiddenColumns == null
686               || that.hiddenColumns.size() != this.hiddenColumns.size())
687       {
688         return false;
689       }
690
691       Iterator<int[]> it = hiddenColumns.iterator();
692       Iterator<int[]> thatit = that.iterator();
693       while (it.hasNext())
694       {
695         int[] thisRange = it.next();
696         int[] thatRange = thatit.next();
697         if (thisRange[0] != thatRange[0] || thisRange[1] != thatRange[1])
698         {
699           return false;
700         }
701       }
702       return true;
703     } finally
704     {
705       LOCK.readLock().unlock();
706     }
707   }
708
709   /**
710    * Return absolute column index for a visible column index
711    * 
712    * @param column
713    *          int column index in alignment view (count from zero)
714    * @return alignment column index for column
715    */
716   public int visibleToAbsoluteColumn(int column)
717   {
718     try
719     {
720       LOCK.readLock().lock();
721       int result = column;
722
723       if (hiddenColumns != null)
724       {
725         result += cursor.findRegionForVisColumn(column).getHiddenSoFar();
726       }
727
728       return result;
729     } finally
730     {
731       LOCK.readLock().unlock();
732     }
733   }
734
735   /**
736    * Use this method to find out where a column will appear in the visible
737    * alignment when hidden columns exist. If the column is not visible, then the
738    * index of the next visible column on the left will be returned (or 0 if
739    * there is no visible column on the left)
740    * 
741    * @param hiddenColumn
742    *          the column index in the full alignment including hidden columns
743    * @return the position of the column in the visible alignment
744    */
745   public int absoluteToVisibleColumn(int hiddenColumn)
746   {
747     try
748     {
749       LOCK.readLock().lock();
750       int result = hiddenColumn;
751
752       if (hiddenColumns != null)
753       {
754         HiddenCursorPosition cursorPos = cursor
755                 .findRegionForColumn(hiddenColumn);
756         int index = cursorPos.getRegionIndex();
757         int hiddenBeforeCol = cursorPos.getHiddenSoFar();
758     
759         // just subtract hidden cols count - this works fine if column is
760         // visible
761         result = hiddenColumn - hiddenBeforeCol;
762     
763         // now check in case column is hidden - it will be in the returned
764         // hidden region
765         if (index < hiddenColumns.size())
766         {
767           int[] region = hiddenColumns.get(index);
768           if (hiddenColumn >= region[0] && hiddenColumn <= region[1])
769           {
770             // actually col is hidden, return region[0]-1
771             // unless region[0]==0 in which case return 0
772             if (region[0] == 0)
773             {
774               result = 0;
775             }
776             else
777             {
778               result = region[0] - 1 - hiddenBeforeCol;
779             }
780           }
781         }
782       }
783
784       return result; // return the shifted position after removing hidden
785                      // columns.
786     } finally
787     {
788       LOCK.readLock().unlock();
789     }
790   }
791
792   /**
793    * Find the visible column which is a given visible number of columns to the
794    * left of another visible column. i.e. for a startColumn x, the column which
795    * is distance 1 away will be column x-1.
796    * 
797    * @param visibleDistance
798    *          the number of visible columns to offset by
799    * @param startColumn
800    *          the column to start from
801    * @return the position of the column in the visible alignment
802    */
803   public int subtractVisibleColumns(int visibleDistance, int startColumn)
804   {
805     try
806     {
807       LOCK.readLock().lock();
808       int distance = visibleDistance;
809
810       // in case startColumn is in a hidden region, move it to the left
811       int start = visibleToAbsoluteColumn(absoluteToVisibleColumn(startColumn));
812
813       Iterator<int[]> it = new ReverseRegionsIterator(0, start,
814               hiddenColumns);
815
816       while (it.hasNext() && (distance > 0))
817       {
818         int[] region = it.next();
819
820         if (start > region[1])
821         {
822           // subtract the gap to right of region from distance
823           if (start - region[1] <= distance)
824           {
825             distance -= start - region[1];
826             start = region[0] - 1;
827           }
828           else
829           {
830             start = start - distance;
831             distance = 0;
832           }
833         }
834       }
835       return start - distance;
836
837     } finally
838     {
839       LOCK.readLock().unlock();
840     }
841   }
842
843   /**
844    * This method returns the rightmost limit of a region of an alignment with
845    * hidden columns. In otherwords, the next hidden column.
846    * 
847    * @param alPos
848    *          the absolute (visible) alignmentPosition to find the next hidden
849    *          column for
850    * @return the index of the next hidden column, or alPos if there is no next
851    *         hidden column
852    */
853   public int getHiddenBoundaryRight(int alPos)
854   {
855     try
856     {
857       LOCK.readLock().lock();
858       if (hiddenColumns != null)
859       {
860         int index = cursor.findRegionForColumn(alPos).getRegionIndex();
861         if (index < hiddenColumns.size())
862         {
863           int[] region = hiddenColumns.get(index);
864           if (alPos < region[0])
865           {
866             return region[0];
867           }
868           else if ((alPos <= region[1])
869                   && (index + 1 < hiddenColumns.size()))
870           {
871             // alPos is within a hidden region, return the next one
872             // if there is one
873             region = hiddenColumns.get(index + 1);
874             return region[0];
875           }
876         }
877       }
878       return alPos;
879     } finally
880     {
881       LOCK.readLock().unlock();
882     }
883   }
884
885   /**
886    * This method returns the leftmost limit of a region of an alignment with
887    * hidden columns. In otherwords, the previous hidden column.
888    * 
889    * @param alPos
890    *          the absolute (visible) alignmentPosition to find the previous
891    *          hidden column for
892    */
893   public int getHiddenBoundaryLeft(int alPos)
894   {
895     try
896     {
897       LOCK.readLock().lock();
898
899       if (hiddenColumns != null)
900       {
901         int index = cursor.findRegionForColumn(alPos).getRegionIndex();
902
903         if (index > 0)
904         {
905           int[] region = hiddenColumns.get(index - 1);
906           return region[1];
907         }
908       }
909       return alPos;
910     } finally
911     {
912       LOCK.readLock().unlock();
913     }
914   }
915
916
917   /**
918    * Answers if a column in the alignment is visible
919    * 
920    * @param column
921    *          absolute position of column in the alignment
922    * @return true if column is visible
923    */
924   public boolean isVisible(int column)
925   {
926     try
927     {
928       LOCK.readLock().lock();
929
930       int regionindex = cursor.findRegionForColumn(column).getRegionIndex();
931       if (regionindex > -1 && regionindex < hiddenColumns.size())
932       {
933         int[] region = hiddenColumns.get(regionindex);
934         // already know that column <= region[1] as cursor returns containing
935         // region or region to right
936         if (column >= region[0])
937         {
938           return false;
939         }
940       }
941       return true;
942
943     } finally
944     {
945       LOCK.readLock().unlock();
946     }
947   }
948
949   /**
950    * Get the visible sections of a set of sequences
951    * 
952    * @param start
953    *          sequence position to start from
954    * @param end
955    *          sequence position to end at
956    * @param seqs
957    *          an array of sequences
958    * @return an array of strings encoding the visible parts of each sequence
959    */
960   public String[] getVisibleSequenceStrings(int start, int end,
961           SequenceI[] seqs)
962   {
963     try
964     {
965       LOCK.readLock().lock();
966       int iSize = seqs.length;
967       String[] selections = new String[iSize];
968       if (hiddenColumns != null && hiddenColumns.size() > 0)
969       {
970         for (int i = 0; i < iSize; i++)
971         {
972           StringBuffer visibleSeq = new StringBuffer();
973
974           Iterator<int[]> blocks = new VisibleContigsIterator(start,
975                   end + 1, hiddenColumns);
976
977           while (blocks.hasNext())
978           {
979             int[] block = blocks.next();
980             if (blocks.hasNext())
981             {
982               visibleSeq
983                       .append(seqs[i].getSequence(block[0], block[1] + 1));
984             }
985             else
986             {
987               visibleSeq
988                       .append(seqs[i].getSequence(block[0], block[1]));
989             }
990           }
991
992           selections[i] = visibleSeq.toString();
993         }
994       }
995       else
996       {
997         for (int i = 0; i < iSize; i++)
998         {
999           selections[i] = seqs[i].getSequenceAsString(start, end);
1000         }
1001       }
1002
1003       return selections;
1004     } finally
1005     {
1006       LOCK.readLock().unlock();
1007     }
1008   }
1009
1010   /**
1011    * Locate the first position visible for this sequence. If seq isn't visible
1012    * then return the position of the left side of the hidden boundary region.
1013    * 
1014    * @param seq
1015    *          sequence to find position for
1016    * @return visible start position
1017    */
1018   public int locateVisibleStartOfSequence(SequenceI seq)
1019   {
1020     try
1021     {
1022       LOCK.readLock().lock();
1023       int start = 0;
1024
1025       if (hiddenColumns == null || hiddenColumns.size() == 0)
1026       {
1027         return seq.findIndex(seq.getStart()) - 1;
1028       }
1029
1030       // Simply walk along the sequence whilst watching for hidden column
1031       // boundaries
1032       Iterator<int[]> regions = hiddenColumns.iterator();
1033       int hideStart = seq.getLength();
1034       int hideEnd = -1;
1035       int visPrev = 0;
1036       int visNext = 0;
1037       boolean foundStart = false;
1038
1039       // step through the non-gapped positions of the sequence
1040       for (int i = seq.getStart(); i <= seq.getEnd() && (!foundStart); i++)
1041       {
1042         // get alignment position of this residue in the sequence
1043         int p = seq.findIndex(i) - 1;
1044
1045         // update hidden region start/end
1046         while (hideEnd < p && regions.hasNext())
1047         {
1048           int[] region = regions.next();
1049           visPrev = visNext;
1050           visNext += region[0] - visPrev;
1051           hideStart = region[0];
1052           hideEnd = region[1];
1053         }
1054         if (hideEnd < p)
1055         {
1056           hideStart = seq.getLength();
1057         }
1058         // update visible boundary for sequence
1059         if (p < hideStart)
1060         {
1061           start = p;
1062           foundStart = true;
1063         }
1064       }
1065
1066       if (foundStart)
1067       {
1068         return absoluteToVisibleColumn(start);
1069       }
1070       // otherwise, sequence was completely hidden
1071       return visPrev;
1072     } finally
1073     {
1074       LOCK.readLock().unlock();
1075     }
1076   }
1077
1078   /**
1079    * delete any columns in alignmentAnnotation that are hidden (including
1080    * sequence associated annotation).
1081    * 
1082    * @param alignmentAnnotation
1083    */
1084   public void makeVisibleAnnotation(AlignmentAnnotation alignmentAnnotation)
1085   {
1086     if (alignmentAnnotation != null
1087             && alignmentAnnotation.annotations != null)
1088     {
1089       makeVisibleAnnotation(0, alignmentAnnotation.annotations.length,
1090             alignmentAnnotation);
1091     }
1092   }
1093
1094   /**
1095    * delete any columns in alignmentAnnotation that are hidden (including
1096    * sequence associated annotation).
1097    * 
1098    * @param start
1099    *          remove any annotation to the right of this column
1100    * @param end
1101    *          remove any annotation to the left of this column
1102    * @param alignmentAnnotation
1103    *          the annotation to operate on
1104    */
1105   public void makeVisibleAnnotation(int start, int end,
1106           AlignmentAnnotation alignmentAnnotation)
1107   {
1108     try
1109     {
1110       LOCK.readLock().lock();
1111
1112       int startFrom = start;
1113       int endAt = end;
1114
1115       if (alignmentAnnotation != null
1116               && alignmentAnnotation.annotations != null)
1117       {
1118         if (hiddenColumns != null && hiddenColumns.size() > 0)
1119         {
1120           removeHiddenAnnotation(startFrom, endAt, alignmentAnnotation);
1121         }
1122         else
1123         {
1124           alignmentAnnotation.restrict(startFrom, endAt);
1125         }
1126       }
1127     } finally
1128     {
1129       LOCK.readLock().unlock();
1130     }
1131   }
1132
1133   private void removeHiddenAnnotation(int start, int end,
1134           AlignmentAnnotation alignmentAnnotation)
1135   {
1136     // mangle the alignmentAnnotation annotation array
1137     ArrayList<Annotation[]> annels = new ArrayList<>();
1138     Annotation[] els = null;
1139
1140     int w = 0;
1141     
1142     Iterator<int[]> blocks = new VisibleContigsIterator(start, end + 1,
1143             hiddenColumns);
1144
1145     int copylength;
1146     int annotationLength;
1147     while (blocks.hasNext())
1148     {
1149       int[] block = blocks.next();
1150       annotationLength = block[1] - block[0] + 1;
1151     
1152       if (blocks.hasNext())
1153       {
1154         // copy just the visible segment of the annotation row
1155         copylength = annotationLength;
1156       }
1157       else
1158       {
1159         if (annotationLength + block[0] <= alignmentAnnotation.annotations.length)
1160         {
1161           // copy just the visible segment of the annotation row
1162           copylength = annotationLength;
1163         }
1164         else
1165         {
1166           // copy to the end of the annotation row
1167           copylength = alignmentAnnotation.annotations.length - block[0];
1168         }
1169       }
1170       
1171       els = new Annotation[annotationLength];
1172       annels.add(els);
1173       System.arraycopy(alignmentAnnotation.annotations, block[0], els, 0,
1174               copylength);
1175       w += annotationLength;
1176     }
1177     
1178     if (w != 0)
1179     {
1180       alignmentAnnotation.annotations = new Annotation[w];
1181
1182       w = 0;
1183       for (Annotation[] chnk : annels)
1184       {
1185         System.arraycopy(chnk, 0, alignmentAnnotation.annotations, w,
1186                 chnk.length);
1187         w += chnk.length;
1188       }
1189     }
1190   }
1191
1192   /**
1193    * 
1194    * @return true if there are columns hidden
1195    */
1196   public boolean hasHiddenColumns()
1197   {
1198     try
1199     {
1200       LOCK.readLock().lock();
1201
1202       // we don't use getSize()>0 here because it has to iterate over
1203       // the full hiddenColumns collection and so will be much slower
1204       return hiddenColumns != null && hiddenColumns.size() > 0;
1205     } finally
1206     {
1207       LOCK.readLock().unlock();
1208     }
1209   }
1210
1211   /**
1212    * 
1213    * @return true if there is more than one hidden column region
1214    */
1215   public boolean hasMultiHiddenColumnRegions()
1216   {
1217     try
1218     {
1219       LOCK.readLock().lock();
1220       return hiddenColumns != null && hiddenColumns.size() > 1;
1221     } finally
1222     {
1223       LOCK.readLock().unlock();
1224     }
1225   }
1226
1227
1228   /**
1229    * Returns a hashCode built from hidden column ranges
1230    */
1231   @Override
1232   public int hashCode()
1233   {
1234     try
1235     {
1236       LOCK.readLock().lock();
1237       int hashCode = 1;
1238       Iterator<int[]> it = hiddenColumns.iterator();
1239       while (it.hasNext())
1240       {
1241         int[] hidden = it.next();
1242         hashCode = HASH_MULTIPLIER * hashCode + hidden[0];
1243         hashCode = HASH_MULTIPLIER * hashCode + hidden[1];
1244       }
1245       return hashCode;
1246     } finally
1247     {
1248       LOCK.readLock().unlock();
1249     }
1250   }
1251
1252   /**
1253    * Hide columns corresponding to the marked bits
1254    * 
1255    * @param inserts
1256    *          - columns map to bits starting from zero
1257    */
1258   public void hideColumns(BitSet inserts)
1259   {
1260     try
1261     {
1262       LOCK.writeLock().lock();
1263       for (int firstSet = inserts
1264               .nextSetBit(0), lastSet = 0; firstSet >= 0; firstSet = inserts
1265                       .nextSetBit(lastSet))
1266       {
1267         lastSet = inserts.nextClearBit(firstSet);
1268         hideColumns(firstSet, lastSet - 1);
1269       }
1270       cursor.resetCursor(hiddenColumns);
1271       numColumns = 0;
1272     } finally
1273     {
1274       LOCK.writeLock().unlock();
1275     }
1276   }
1277
1278   /**
1279    * 
1280    * @param inserts
1281    *          BitSet where hidden columns will be marked
1282    */
1283   public void markHiddenRegions(BitSet inserts)
1284   {
1285     try
1286     {
1287       LOCK.readLock().lock();
1288       if (hiddenColumns == null)
1289       {
1290         return;
1291       }
1292       Iterator<int[]> it = hiddenColumns.iterator();
1293       while (it.hasNext())
1294       {
1295         int[] range = it.next();
1296         inserts.set(range[0], range[1] + 1);
1297       }
1298     } finally
1299     {
1300       LOCK.readLock().unlock();
1301     }
1302   }
1303
1304   /**
1305    * Calculate the visible start and end index of an alignment.
1306    * 
1307    * @param width
1308    *          full alignment width
1309    * @return integer array where: int[0] = startIndex, and int[1] = endIndex
1310    */
1311   public int[] getVisibleStartAndEndIndex(int width)
1312   {
1313     try
1314     {
1315       LOCK.readLock().lock();
1316       int[] alignmentStartEnd = new int[] { 0, width - 1 };
1317       int startPos = alignmentStartEnd[0];
1318       int endPos = alignmentStartEnd[1];
1319
1320       int[] lowestRange = new int[] { -1, -1 };
1321       int[] higestRange = new int[] { -1, -1 };
1322
1323       if (hiddenColumns == null)
1324       {
1325         return new int[] { startPos, endPos };
1326       }
1327
1328       Iterator<int[]> it = hiddenColumns.iterator();
1329       while (it.hasNext())
1330       {
1331         int[] range = it.next();
1332         lowestRange = (range[0] <= startPos) ? range : lowestRange;
1333         higestRange = (range[1] >= endPos) ? range : higestRange;
1334       }
1335
1336       if (lowestRange[0] == -1) // includes (lowestRange[1] == -1)
1337       {
1338         startPos = alignmentStartEnd[0];
1339       }
1340       else
1341       {
1342         startPos = lowestRange[1] + 1;
1343       }
1344
1345       if (higestRange[0] == -1) // includes (higestRange[1] == -1)
1346       {
1347         endPos = alignmentStartEnd[1];
1348       }
1349       else
1350       {
1351         endPos = higestRange[0] - 1;
1352       }
1353       return new int[] { startPos, endPos };
1354     } finally
1355     {
1356       LOCK.readLock().unlock();
1357     }
1358   }
1359
1360   /**
1361    * Finds the hidden region (if any) which starts or ends at res
1362    * 
1363    * @param res
1364    *          visible residue position, unadjusted for hidden columns
1365    * @return region as [start,end] or null if no matching region is found. If
1366    *         res is adjacent to two regions, returns the left region.
1367    */
1368   public int[] getRegionWithEdgeAtRes(int res)
1369   {
1370     try
1371     {
1372       LOCK.readLock().lock();
1373       int adjres = visibleToAbsoluteColumn(res);
1374
1375       int[] reveal = null;
1376
1377       if (hiddenColumns != null)
1378       {
1379         // look for a region ending just before adjres
1380         int regionindex = cursor.findRegionForColumn(adjres - 1)
1381                 .getRegionIndex();
1382         if (regionindex < hiddenColumns.size()
1383                 && hiddenColumns.get(regionindex)[1] == adjres - 1)
1384         {
1385           reveal = hiddenColumns.get(regionindex);
1386         }
1387         // check if the region ends just after adjres
1388         else if (regionindex < hiddenColumns.size()
1389                 && hiddenColumns.get(regionindex)[0] == adjres + 1)
1390         {
1391           reveal = hiddenColumns.get(regionindex);
1392         }
1393       }
1394       return reveal;
1395
1396     } finally
1397     {
1398       LOCK.readLock().unlock();
1399     }
1400   }
1401
1402   /**
1403    * Return an iterator over the hidden regions
1404    */
1405   public Iterator<int[]> iterator()
1406   {
1407     try
1408     {
1409       LOCK.readLock().lock();
1410       return new HiddenColsIterator(hiddenColumns);
1411     } finally
1412     {
1413       LOCK.readLock().unlock();
1414     }
1415   }
1416
1417   /**
1418    * Return a bounded iterator over the hidden regions
1419    * 
1420    * @param start
1421    *          position to start from (inclusive, absolute column position)
1422    * @param end
1423    *          position to end at (inclusive, absolute column position)
1424    * @return
1425    */
1426   public Iterator<int[]> getBoundedIterator(int start, int end)
1427   {
1428     try
1429     {
1430       LOCK.readLock().lock();
1431       return new HiddenColsIterator(start, end, hiddenColumns);
1432     } finally
1433     {
1434       LOCK.readLock().unlock();
1435     }
1436   }
1437
1438   /**
1439    * Return a bounded iterator over the *visible* start positions of hidden
1440    * regions
1441    * 
1442    * @param start
1443    *          position to start from (inclusive, visible column position)
1444    * @param end
1445    *          position to end at (inclusive, visible column position)
1446    */
1447   public Iterator<Integer> getBoundedStartIterator(int start, int end)
1448   {
1449     try
1450     {
1451       LOCK.readLock().lock();
1452
1453       // get absolute position of column in alignment
1454       int absoluteStart = visibleToAbsoluteColumn(start);
1455
1456       // Get cursor position and supply it to the iterator:
1457       // Since we want visible region start, we look for a cursor for the
1458       // (absoluteStart-1), then if absoluteStart is the start of a visible
1459       // region we'll get the cursor pointing to the region before, which is
1460       // what we want
1461       HiddenCursorPosition pos = cursor
1462               .findRegionForColumn(absoluteStart - 1);
1463
1464       return new BoundedStartRegionIterator(pos, start, end,
1465               hiddenColumns);
1466     } finally
1467     {
1468       LOCK.readLock().unlock();
1469     }
1470   }
1471
1472   /**
1473    * Return an iterator over visible *columns* (not regions) between the given
1474    * start and end boundaries
1475    * 
1476    * @param start
1477    *          first column (inclusive)
1478    * @param end
1479    *          last column (inclusive)
1480    */
1481   public Iterator<Integer> getVisibleColsIterator(int start, int end)
1482   {
1483     try
1484     {
1485       LOCK.readLock().lock();
1486       return new VisibleColsIterator(start, end, hiddenColumns);
1487     } finally
1488     {
1489       LOCK.readLock().unlock();
1490     }
1491   }
1492
1493   /**
1494    * return an iterator over visible segments between the given start and end
1495    * boundaries
1496    * 
1497    * @param start
1498    *          first column, inclusive from 0
1499    * @param end
1500    *          last column - not inclusive
1501    * @param useVisibleCoords
1502    *          if true, start and end are visible column positions, not absolute
1503    *          positions*
1504    */
1505   public Iterator<int[]> getVisContigsIterator(int start, int end,
1506           boolean useVisibleCoords)
1507   {
1508     int adjstart = start;
1509     int adjend = end;
1510     if (useVisibleCoords)
1511     {
1512       adjstart = visibleToAbsoluteColumn(start);
1513       adjend = visibleToAbsoluteColumn(end);
1514     }
1515
1516     try
1517     {
1518       LOCK.readLock().lock();
1519       return new VisibleContigsIterator(adjstart, adjend, hiddenColumns);
1520     } finally
1521     {
1522       LOCK.readLock().unlock();
1523     }
1524   }
1525 }