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