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