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