JAL-2759 Replaced StringBuffer with StringBuilder; 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 = 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 gaps as a Bitset
448       BitSet gaps = origseq.gapBitset();
449
450       // now calculate hidden ^ not(gap)
451       BitSet hidden = new BitSet();
452       markHiddenRegions(hidden);
453       hidden.andNot(gaps);
454       hiddenColumns.clear();
455       this.hideColumns(hidden);
456
457       // for each sequence in the alignment, except the profile sequence,
458       // insert gaps corresponding to each hidden region but where each hidden
459       // column region is shifted backwards by the number of preceding visible
460       // gaps update hidden columns at the same time
461       List<int[]> newhidden = new ArrayList<>();
462
463       int numGapsBefore = 0;
464       int gapPosition = 0;
465       for (int[] region : hiddenColumns)
466       {
467         // get region coordinates accounting for gaps
468         // we can rely on gaps not being *in* hidden regions because we already
469         // removed those
470         while (gapPosition < region[0])
471         {
472           gapPosition++;
473           if (gaps.get(gapPosition))
474           {
475             numGapsBefore++;
476           }
477         }
478
479         int left = region[0] - numGapsBefore;
480         int right = region[1] - numGapsBefore;
481         newhidden.add(new int[] { left, right });
482
483         // make a string with number of gaps = length of hidden region
484         StringBuilder sb = new StringBuilder();
485         for (int s = 0; s < right - left + 1; s++)
486         {
487           sb.append(gc);
488         }
489         padGaps(sb, left, profileseq, al);
490
491       }
492       hiddenColumns = newhidden;
493       cursor.resetCursor(hiddenColumns);
494       numColumns = 0;
495     } finally
496     {
497       LOCK.writeLock().unlock();
498     }
499   }
500
501   /**
502    * Pad gaps in all sequences in alignment except profileseq
503    * 
504    * @param sb
505    *          gap string to insert
506    * @param left
507    *          position to insert at
508    * @param profileseq
509    *          sequence not to pad
510    * @param al
511    *          alignment to pad sequences in
512    */
513   private void padGaps(StringBuilder sb, int pos, SequenceI profileseq,
514           AlignmentI al)
515   {
516     // loop over the sequences and pad with gaps where required
517     for (int s = 0, ns = al.getHeight(); s < ns; s++)
518     {
519       SequenceI sqobj = al.getSequenceAt(s);
520       if (sqobj != profileseq)
521       {
522         String sq = al.getSequenceAt(s).getSequenceAsString();
523         if (sq.length() <= pos)
524         {
525           // pad sequence
526           int diff = pos - sq.length() - 1;
527           if (diff > 0)
528           {
529             // pad gaps
530             sq = sq + sb;
531             while ((diff = pos - sq.length() - 1) > 0)
532             {
533               if (diff >= sb.length())
534               {
535                 sq += sb.toString();
536               }
537               else
538               {
539                 char[] buf = new char[diff];
540                 sb.getChars(0, diff, buf, 0);
541                 sq += buf.toString();
542               }
543             }
544           }
545           sq += sb.toString();
546         }
547         else
548         {
549           al.getSequenceAt(s).setSequence(
550                   sq.substring(0, pos) + sb.toString() + sq.substring(pos));
551         }
552       }
553     }
554   }
555
556   /*
557    * Methods which only need read access to the hidden columns collection. 
558    * These methods should use a readLock to prevent other threads changing
559    * the hidden columns collection while it is in use.
560    */
561
562   /**
563    * Output regions data as a string. String is in the format:
564    * reg0[0]<between>reg0[1]<delimiter>reg1[0]<between>reg1[1] ... regn[1]
565    * 
566    * @param delimiter
567    *          string to delimit regions
568    * @param betweenstring
569    *          to put between start and end region values
570    * @return regions formatted according to delimiter and between strings
571    */
572   public String regionsToString(String delimiter, String between)
573   {
574     try
575     {
576       LOCK.readLock().lock();
577       StringBuilder regionBuilder = new StringBuilder();
578
579       boolean first = true;
580       for (int[] range : hiddenColumns)
581       {
582         if (!first)
583         {
584           regionBuilder.append(delimiter);
585         }
586         else
587         {
588           first = false;
589         }
590         regionBuilder.append(range[0]).append(between).append(range[1]);
591
592       }
593
594       return regionBuilder.toString();
595     } finally
596     {
597       LOCK.readLock().unlock();
598     }
599   }
600
601   /**
602    * Find the number of hidden columns
603    * 
604    * @return number of hidden columns
605    */
606   public int getSize()
607   {
608     try
609     {
610       LOCK.readLock().lock();
611
612       if (numColumns == 0)
613       {
614         // numColumns is out of date, so recalculate
615         int size = 0;
616
617         for (int[] range : hiddenColumns)
618         {
619           size += range[1] - range[0] + 1;
620         }
621
622         numColumns = size;
623       }
624
625       return numColumns;
626     } finally
627     {
628       LOCK.readLock().unlock();
629     }
630   }
631
632   /**
633    * Get the number of distinct hidden regions
634    * 
635    * @return number of regions
636    */
637   public int getNumberOfRegions()
638   {
639     try
640     {
641       LOCK.readLock().lock();
642       int num = 0;
643       if (hasHiddenColumns())
644       {
645         num = hiddenColumns.size();
646       }
647       return num;
648     } finally
649     {
650       LOCK.readLock().unlock();
651     }
652   }
653
654   @Override
655   public boolean equals(Object obj)
656   {
657     try
658     {
659       LOCK.readLock().lock();
660
661       if (!(obj instanceof HiddenColumns))
662       {
663         return false;
664       }
665       HiddenColumns that = (HiddenColumns) obj;
666
667       /*
668        * check hidden columns are either both null, or match
669        */
670
671       if (that.hiddenColumns.size() != this.hiddenColumns.size())
672       {
673         return false;
674       }
675
676       Iterator<int[]> thatit = that.iterator();
677       for (int[] thisRange : hiddenColumns)
678       {
679         int[] thatRange = thatit.next();
680         if (thisRange[0] != thatRange[0] || thisRange[1] != thatRange[1])
681         {
682           return false;
683         }
684       }
685       return true;
686     } finally
687     {
688       LOCK.readLock().unlock();
689     }
690   }
691
692   /**
693    * Return absolute column index for a visible column index
694    * 
695    * @param column
696    *          int column index in alignment view (count from zero)
697    * @return alignment column index for column
698    */
699   public int visibleToAbsoluteColumn(int column)
700   {
701     try
702     {
703       LOCK.readLock().lock();
704       int result = column;
705
706       if (!hiddenColumns.isEmpty())
707       {
708         result += cursor.findRegionForVisColumn(column).getHiddenSoFar();
709       }
710
711       return result;
712     } finally
713     {
714       LOCK.readLock().unlock();
715     }
716   }
717
718   /**
719    * Use this method to find out where a column will appear in the visible
720    * alignment when hidden columns exist. If the column is not visible, then the
721    * index of the next visible column on the left will be returned (or 0 if
722    * there is no visible column on the left)
723    * 
724    * @param hiddenColumn
725    *          the column index in the full alignment including hidden columns
726    * @return the position of the column in the visible alignment
727    */
728   public int absoluteToVisibleColumn(int hiddenColumn)
729   {
730     try
731     {
732       LOCK.readLock().lock();
733       int result = hiddenColumn;
734
735       if (!hiddenColumns.isEmpty())
736       {
737         HiddenCursorPosition cursorPos = cursor
738                 .findRegionForColumn(hiddenColumn);
739         int index = cursorPos.getRegionIndex();
740         int hiddenBeforeCol = cursorPos.getHiddenSoFar();
741     
742         // just subtract hidden cols count - this works fine if column is
743         // visible
744         result = hiddenColumn - hiddenBeforeCol;
745     
746         // now check in case column is hidden - it will be in the returned
747         // hidden region
748         if (index < hiddenColumns.size())
749         {
750           int[] region = hiddenColumns.get(index);
751           if (hiddenColumn >= region[0] && hiddenColumn <= region[1])
752           {
753             // actually col is hidden, return region[0]-1
754             // unless region[0]==0 in which case return 0
755             if (region[0] == 0)
756             {
757               result = 0;
758             }
759             else
760             {
761               result = region[0] - 1 - hiddenBeforeCol;
762             }
763           }
764         }
765       }
766
767       return result; // return the shifted position after removing hidden
768                      // columns.
769     } finally
770     {
771       LOCK.readLock().unlock();
772     }
773   }
774
775   /**
776    * Find the visible column which is a given visible number of columns to the
777    * left of another visible column. i.e. for a startColumn x, the column which
778    * is distance 1 away will be column x-1.
779    * 
780    * @param visibleDistance
781    *          the number of visible columns to offset by
782    * @param startColumn
783    *          the column to start from
784    * @return the position of the column in the visible alignment
785    */
786   public int subtractVisibleColumns(int visibleDistance, int startColumn)
787   {
788     try
789     {
790       LOCK.readLock().lock();
791       int distance = visibleDistance;
792
793       // in case startColumn is in a hidden region, move it to the left
794       int start = visibleToAbsoluteColumn(absoluteToVisibleColumn(startColumn));
795
796       Iterator<int[]> it = new ReverseRegionsIterator(0, start,
797               hiddenColumns);
798
799       while (it.hasNext() && (distance > 0))
800       {
801         int[] region = it.next();
802
803         if (start > region[1])
804         {
805           // subtract the gap to right of region from distance
806           if (start - region[1] <= distance)
807           {
808             distance -= start - region[1];
809             start = region[0] - 1;
810           }
811           else
812           {
813             start = start - distance;
814             distance = 0;
815           }
816         }
817       }
818       return start - distance;
819
820     } finally
821     {
822       LOCK.readLock().unlock();
823     }
824   }
825
826   /**
827    * This method returns the rightmost limit of a region of an alignment with
828    * hidden columns. In otherwords, the next hidden column.
829    * 
830    * @param alPos
831    *          the absolute (visible) alignmentPosition to find the next hidden
832    *          column for
833    * @return the index of the next hidden column, or alPos if there is no next
834    *         hidden column
835    */
836   public int getHiddenBoundaryRight(int alPos)
837   {
838     try
839     {
840       LOCK.readLock().lock();
841       if (!hiddenColumns.isEmpty())
842       {
843         int index = cursor.findRegionForColumn(alPos).getRegionIndex();
844         if (index < hiddenColumns.size())
845         {
846           int[] region = hiddenColumns.get(index);
847           if (alPos < region[0])
848           {
849             return region[0];
850           }
851           else if ((alPos <= region[1])
852                   && (index + 1 < hiddenColumns.size()))
853           {
854             // alPos is within a hidden region, return the next one
855             // if there is one
856             region = hiddenColumns.get(index + 1);
857             return region[0];
858           }
859         }
860       }
861       return alPos;
862     } finally
863     {
864       LOCK.readLock().unlock();
865     }
866   }
867
868   /**
869    * This method returns the leftmost limit of a region of an alignment with
870    * hidden columns. In otherwords, the previous hidden column.
871    * 
872    * @param alPos
873    *          the absolute (visible) alignmentPosition to find the previous
874    *          hidden column for
875    */
876   public int getHiddenBoundaryLeft(int alPos)
877   {
878     try
879     {
880       LOCK.readLock().lock();
881
882       if (!hiddenColumns.isEmpty())
883       {
884         int index = cursor.findRegionForColumn(alPos).getRegionIndex();
885
886         if (index > 0)
887         {
888           int[] region = hiddenColumns.get(index - 1);
889           return region[1];
890         }
891       }
892       return alPos;
893     } finally
894     {
895       LOCK.readLock().unlock();
896     }
897   }
898
899
900   /**
901    * Answers if a column in the alignment is visible
902    * 
903    * @param column
904    *          absolute position of column in the alignment
905    * @return true if column is visible
906    */
907   public boolean isVisible(int column)
908   {
909     try
910     {
911       LOCK.readLock().lock();
912
913       int regionindex = cursor.findRegionForColumn(column).getRegionIndex();
914       if (regionindex > -1 && regionindex < hiddenColumns.size())
915       {
916         int[] region = hiddenColumns.get(regionindex);
917         // already know that column <= region[1] as cursor returns containing
918         // region or region to right
919         if (column >= region[0])
920         {
921           return false;
922         }
923       }
924       return true;
925
926     } finally
927     {
928       LOCK.readLock().unlock();
929     }
930   }
931
932   /**
933    * Get the visible sections of a set of sequences
934    * 
935    * @param start
936    *          sequence position to start from
937    * @param end
938    *          sequence position to end at
939    * @param seqs
940    *          an array of sequences
941    * @return an array of strings encoding the visible parts of each sequence
942    */
943   public String[] getVisibleSequenceStrings(int start, int end,
944           SequenceI[] seqs)
945   {
946     try
947     {
948       LOCK.readLock().lock();
949       int iSize = seqs.length;
950       String[] selections = new String[iSize];
951       if (!hiddenColumns.isEmpty())
952       {
953         for (int i = 0; i < iSize; i++)
954         {
955           StringBuilder visibleSeq = new StringBuilder();
956
957           Iterator<int[]> blocks = new VisibleContigsIterator(start,
958                   end + 1, hiddenColumns);
959
960           while (blocks.hasNext())
961           {
962             int[] block = blocks.next();
963             if (blocks.hasNext())
964             {
965               visibleSeq
966                       .append(seqs[i].getSequence(block[0], block[1] + 1));
967             }
968             else
969             {
970               visibleSeq
971                       .append(seqs[i].getSequence(block[0], block[1]));
972             }
973           }
974
975           selections[i] = visibleSeq.toString();
976         }
977       }
978       else
979       {
980         for (int i = 0; i < iSize; i++)
981         {
982           selections[i] = seqs[i].getSequenceAsString(start, end);
983         }
984       }
985
986       return selections;
987     } finally
988     {
989       LOCK.readLock().unlock();
990     }
991   }
992
993   /**
994    * Locate the first position visible for this sequence. If seq isn't visible
995    * then return the position of the left side of the hidden boundary region.
996    * 
997    * @param seq
998    *          sequence to find position for
999    * @return visible start position
1000    */
1001   public int locateVisibleStartOfSequence(SequenceI seq)
1002   {
1003     try
1004     {
1005       LOCK.readLock().lock();
1006       int start = 0;
1007
1008       if (hiddenColumns.isEmpty())
1009       {
1010         return seq.findIndex(seq.getStart()) - 1;
1011       }
1012
1013       // Simply walk along the sequence whilst watching for hidden column
1014       // boundaries
1015       Iterator<int[]> regions = hiddenColumns.iterator();
1016       int hideStart = seq.getLength();
1017       int hideEnd = -1;
1018       int visPrev = 0;
1019       int visNext = 0;
1020       boolean foundStart = false;
1021
1022       // step through the non-gapped positions of the sequence
1023       for (int i = seq.getStart(); i <= seq.getEnd() && (!foundStart); i++)
1024       {
1025         // get alignment position of this residue in the sequence
1026         int p = seq.findIndex(i) - 1;
1027
1028         // update hidden region start/end
1029         while (hideEnd < p && regions.hasNext())
1030         {
1031           int[] region = regions.next();
1032           visPrev = visNext;
1033           visNext += region[0] - visPrev;
1034           hideStart = region[0];
1035           hideEnd = region[1];
1036         }
1037         if (hideEnd < p)
1038         {
1039           hideStart = seq.getLength();
1040         }
1041         // update visible boundary for sequence
1042         if (p < hideStart)
1043         {
1044           start = p;
1045           foundStart = true;
1046         }
1047       }
1048
1049       if (foundStart)
1050       {
1051         return absoluteToVisibleColumn(start);
1052       }
1053       // otherwise, sequence was completely hidden
1054       return visPrev;
1055     } finally
1056     {
1057       LOCK.readLock().unlock();
1058     }
1059   }
1060
1061   /**
1062    * delete any columns in alignmentAnnotation that are hidden (including
1063    * sequence associated annotation).
1064    * 
1065    * @param alignmentAnnotation
1066    */
1067   public void makeVisibleAnnotation(AlignmentAnnotation alignmentAnnotation)
1068   {
1069     if (alignmentAnnotation != null
1070             && alignmentAnnotation.annotations != null)
1071     {
1072       makeVisibleAnnotation(0, alignmentAnnotation.annotations.length,
1073             alignmentAnnotation);
1074     }
1075   }
1076
1077   /**
1078    * delete any columns in alignmentAnnotation that are hidden (including
1079    * sequence associated annotation).
1080    * 
1081    * @param start
1082    *          remove any annotation to the right of this column
1083    * @param end
1084    *          remove any annotation to the left of this column
1085    * @param alignmentAnnotation
1086    *          the annotation to operate on
1087    */
1088   public void makeVisibleAnnotation(int start, int end,
1089           AlignmentAnnotation alignmentAnnotation)
1090   {
1091     try
1092     {
1093       LOCK.readLock().lock();
1094
1095       int startFrom = start;
1096       int endAt = end;
1097
1098       if (alignmentAnnotation != null
1099               && alignmentAnnotation.annotations != null)
1100       {
1101         if (!hiddenColumns.isEmpty())
1102         {
1103           removeHiddenAnnotation(startFrom, endAt, alignmentAnnotation);
1104         }
1105         else
1106         {
1107           alignmentAnnotation.restrict(startFrom, endAt);
1108         }
1109       }
1110     } finally
1111     {
1112       LOCK.readLock().unlock();
1113     }
1114   }
1115
1116   private void removeHiddenAnnotation(int start, int end,
1117           AlignmentAnnotation alignmentAnnotation)
1118   {
1119     // mangle the alignmentAnnotation annotation array
1120     ArrayList<Annotation[]> annels = new ArrayList<>();
1121     Annotation[] els = null;
1122
1123     int w = 0;
1124     
1125     Iterator<int[]> blocks = new VisibleContigsIterator(start, end + 1,
1126             hiddenColumns);
1127
1128     int copylength;
1129     int annotationLength;
1130     while (blocks.hasNext())
1131     {
1132       int[] block = blocks.next();
1133       annotationLength = block[1] - block[0] + 1;
1134     
1135       if (blocks.hasNext())
1136       {
1137         // copy just the visible segment of the annotation row
1138         copylength = annotationLength;
1139       }
1140       else
1141       {
1142         if (annotationLength + block[0] <= alignmentAnnotation.annotations.length)
1143         {
1144           // copy just the visible segment of the annotation row
1145           copylength = annotationLength;
1146         }
1147         else
1148         {
1149           // copy to the end of the annotation row
1150           copylength = alignmentAnnotation.annotations.length - block[0];
1151         }
1152       }
1153       
1154       els = new Annotation[annotationLength];
1155       annels.add(els);
1156       System.arraycopy(alignmentAnnotation.annotations, block[0], els, 0,
1157               copylength);
1158       w += annotationLength;
1159     }
1160     
1161     if (w != 0)
1162     {
1163       alignmentAnnotation.annotations = new Annotation[w];
1164
1165       w = 0;
1166       for (Annotation[] chnk : annels)
1167       {
1168         System.arraycopy(chnk, 0, alignmentAnnotation.annotations, w,
1169                 chnk.length);
1170         w += chnk.length;
1171       }
1172     }
1173   }
1174
1175   /**
1176    * 
1177    * @return true if there are columns hidden
1178    */
1179   public boolean hasHiddenColumns()
1180   {
1181     try
1182     {
1183       LOCK.readLock().lock();
1184
1185       // we don't use getSize()>0 here because it has to iterate over
1186       // the full hiddenColumns collection and so will be much slower
1187       return (!hiddenColumns.isEmpty());
1188     } finally
1189     {
1190       LOCK.readLock().unlock();
1191     }
1192   }
1193
1194   /**
1195    * 
1196    * @return true if there is more than one hidden column region
1197    */
1198   public boolean hasMultiHiddenColumnRegions()
1199   {
1200     try
1201     {
1202       LOCK.readLock().lock();
1203       return !hiddenColumns.isEmpty() && hiddenColumns.size() > 1;
1204     } finally
1205     {
1206       LOCK.readLock().unlock();
1207     }
1208   }
1209
1210
1211   /**
1212    * Returns a hashCode built from hidden column ranges
1213    */
1214   @Override
1215   public int hashCode()
1216   {
1217     try
1218     {
1219       LOCK.readLock().lock();
1220       int hashCode = 1;
1221       for (int[] hidden : hiddenColumns)
1222       {
1223         hashCode = HASH_MULTIPLIER * hashCode + hidden[0];
1224         hashCode = HASH_MULTIPLIER * hashCode + hidden[1];
1225       }
1226       return hashCode;
1227     } finally
1228     {
1229       LOCK.readLock().unlock();
1230     }
1231   }
1232
1233   /**
1234    * Hide columns corresponding to the marked bits
1235    * 
1236    * @param inserts
1237    *          - columns map to bits starting from zero
1238    */
1239   public void hideColumns(BitSet inserts)
1240   {
1241     try
1242     {
1243       LOCK.writeLock().lock();
1244       for (int firstSet = inserts
1245               .nextSetBit(0), lastSet = 0; firstSet >= 0; firstSet = inserts
1246                       .nextSetBit(lastSet))
1247       {
1248         lastSet = inserts.nextClearBit(firstSet);
1249         hideColumns(firstSet, lastSet - 1);
1250       }
1251       cursor.resetCursor(hiddenColumns);
1252       numColumns = 0;
1253     } finally
1254     {
1255       LOCK.writeLock().unlock();
1256     }
1257   }
1258
1259   /**
1260    * 
1261    * @param inserts
1262    *          BitSet where hidden columns will be marked
1263    */
1264   public void markHiddenRegions(BitSet inserts)
1265   {
1266     try
1267     {
1268       LOCK.readLock().lock();
1269
1270       for (int[] range : hiddenColumns)
1271       {
1272         inserts.set(range[0], range[1] + 1);
1273       }
1274     } finally
1275     {
1276       LOCK.readLock().unlock();
1277     }
1278   }
1279
1280   /**
1281    * Calculate the visible start and end index of an alignment.
1282    * 
1283    * @param width
1284    *          full alignment width
1285    * @return integer array where: int[0] = startIndex, and int[1] = endIndex
1286    */
1287   public int[] getVisibleStartAndEndIndex(int width)
1288   {
1289     try
1290     {
1291       LOCK.readLock().lock();
1292       int[] alignmentStartEnd = new int[] { 0, width - 1 };
1293       int startPos = alignmentStartEnd[0];
1294       int endPos = alignmentStartEnd[1];
1295
1296       int[] lowestRange = new int[] { -1, -1 };
1297       int[] higestRange = new int[] { -1, -1 };
1298
1299       if (hiddenColumns.isEmpty())
1300       {
1301         return new int[] { startPos, endPos };
1302       }
1303
1304       for (int[] range : hiddenColumns)
1305       {
1306         lowestRange = (range[0] <= startPos) ? range : lowestRange;
1307         higestRange = (range[1] >= endPos) ? range : higestRange;
1308       }
1309
1310       if (lowestRange[0] == -1) // includes (lowestRange[1] == -1)
1311       {
1312         startPos = alignmentStartEnd[0];
1313       }
1314       else
1315       {
1316         startPos = lowestRange[1] + 1;
1317       }
1318
1319       if (higestRange[0] == -1) // includes (higestRange[1] == -1)
1320       {
1321         endPos = alignmentStartEnd[1];
1322       }
1323       else
1324       {
1325         endPos = higestRange[0] - 1;
1326       }
1327       return new int[] { startPos, endPos };
1328     } finally
1329     {
1330       LOCK.readLock().unlock();
1331     }
1332   }
1333
1334   /**
1335    * Finds the hidden region (if any) which starts or ends at res
1336    * 
1337    * @param res
1338    *          visible residue position, unadjusted for hidden columns
1339    * @return region as [start,end] or null if no matching region is found. If
1340    *         res is adjacent to two regions, returns the left region.
1341    */
1342   public int[] getRegionWithEdgeAtRes(int res)
1343   {
1344     try
1345     {
1346       LOCK.readLock().lock();
1347       int adjres = visibleToAbsoluteColumn(res);
1348
1349       int[] reveal = null;
1350
1351       if (!hiddenColumns.isEmpty())
1352       {
1353         // look for a region ending just before adjres
1354         int regionindex = cursor.findRegionForColumn(adjres - 1)
1355                 .getRegionIndex();
1356         if (regionindex < hiddenColumns.size()
1357                 && hiddenColumns.get(regionindex)[1] == adjres - 1)
1358         {
1359           reveal = hiddenColumns.get(regionindex);
1360         }
1361         // check if the region ends just after adjres
1362         else if (regionindex < hiddenColumns.size()
1363                 && hiddenColumns.get(regionindex)[0] == adjres + 1)
1364         {
1365           reveal = hiddenColumns.get(regionindex);
1366         }
1367       }
1368       return reveal;
1369
1370     } finally
1371     {
1372       LOCK.readLock().unlock();
1373     }
1374   }
1375
1376   /**
1377    * Return an iterator over the hidden regions
1378    */
1379   public Iterator<int[]> iterator()
1380   {
1381     try
1382     {
1383       LOCK.readLock().lock();
1384       return new HiddenColsIterator(hiddenColumns);
1385     } finally
1386     {
1387       LOCK.readLock().unlock();
1388     }
1389   }
1390
1391   /**
1392    * Return a bounded iterator over the hidden regions
1393    * 
1394    * @param start
1395    *          position to start from (inclusive, absolute column position)
1396    * @param end
1397    *          position to end at (inclusive, absolute column position)
1398    * @return
1399    */
1400   public Iterator<int[]> getBoundedIterator(int start, int end)
1401   {
1402     try
1403     {
1404       LOCK.readLock().lock();
1405       return new HiddenColsIterator(start, end, hiddenColumns);
1406     } finally
1407     {
1408       LOCK.readLock().unlock();
1409     }
1410   }
1411
1412   /**
1413    * Return a bounded iterator over the *visible* start positions of hidden
1414    * regions
1415    * 
1416    * @param start
1417    *          position to start from (inclusive, visible column position)
1418    * @param end
1419    *          position to end at (inclusive, visible column position)
1420    */
1421   public Iterator<Integer> getBoundedStartIterator(int start, int end)
1422   {
1423     try
1424     {
1425       LOCK.readLock().lock();
1426
1427       // get absolute position of column in alignment
1428       int absoluteStart = visibleToAbsoluteColumn(start);
1429
1430       // Get cursor position and supply it to the iterator:
1431       // Since we want visible region start, we look for a cursor for the
1432       // (absoluteStart-1), then if absoluteStart is the start of a visible
1433       // region we'll get the cursor pointing to the region before, which is
1434       // what we want
1435       HiddenCursorPosition pos = cursor
1436               .findRegionForColumn(absoluteStart - 1);
1437
1438       return new BoundedStartRegionIterator(pos, start, end,
1439               hiddenColumns);
1440     } finally
1441     {
1442       LOCK.readLock().unlock();
1443     }
1444   }
1445
1446   /**
1447    * Return an iterator over visible *columns* (not regions) between the given
1448    * start and end boundaries
1449    * 
1450    * @param start
1451    *          first column (inclusive)
1452    * @param end
1453    *          last column (inclusive)
1454    */
1455   public Iterator<Integer> getVisibleColsIterator(int start, int end)
1456   {
1457     try
1458     {
1459       LOCK.readLock().lock();
1460       return new VisibleColsIterator(start, end, hiddenColumns);
1461     } finally
1462     {
1463       LOCK.readLock().unlock();
1464     }
1465   }
1466
1467   /**
1468    * return an iterator over visible segments between the given start and end
1469    * boundaries
1470    * 
1471    * @param start
1472    *          first column, inclusive from 0
1473    * @param end
1474    *          last column - not inclusive
1475    * @param useVisibleCoords
1476    *          if true, start and end are visible column positions, not absolute
1477    *          positions*
1478    */
1479   public VisibleContigsIterator getVisContigsIterator(int start,
1480           int end,
1481           boolean useVisibleCoords)
1482   {
1483     int adjstart = start;
1484     int adjend = end;
1485     if (useVisibleCoords)
1486     {
1487       adjstart = visibleToAbsoluteColumn(start);
1488       adjend = visibleToAbsoluteColumn(end);
1489     }
1490
1491     try
1492     {
1493       LOCK.readLock().lock();
1494       return new VisibleContigsIterator(adjstart, adjend, hiddenColumns);
1495     } finally
1496     {
1497       LOCK.readLock().unlock();
1498     }
1499   }
1500 }