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