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