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