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