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