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