JAL-2388 Hidden cols separated from column selection (almost complete)
[jalview.git] / src / jalview / datamodel / ColumnSelection.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.ShiftList;
24 import jalview.viewmodel.annotationfilter.AnnotationFilterParameter;
25 import jalview.viewmodel.annotationfilter.AnnotationFilterParameter.SearchableAnnotationField;
26
27 import java.util.ArrayList;
28 import java.util.BitSet;
29 import java.util.Collections;
30 import java.util.List;
31
32 /**
33  * Data class holding the selected columns and hidden column ranges for a view.
34  * Ranges are base 1.
35  */
36 public class ColumnSelection
37 {
38   /**
39    * A class to hold an efficient representation of selected columns
40    */
41   private class IntList
42   {
43     /*
44      * list of selected columns (ordered by selection order, not column order)
45      */
46     private List<Integer> order;
47
48     /*
49      * an unmodifiable view of the selected columns list
50      */
51     private List<Integer> _uorder;
52
53     /**
54      * bitfield for column selection - allows quick lookup
55      */
56     private BitSet selected;
57
58     /**
59      * Constructor
60      */
61     IntList()
62     {
63       order = new ArrayList<Integer>();
64       _uorder = Collections.unmodifiableList(order);
65       selected = new BitSet();
66     }
67
68     /**
69      * Copy constructor
70      * 
71      * @param other
72      */
73     IntList(IntList other)
74     {
75       this();
76       if (other != null)
77       {
78         int j = other.size();
79         for (int i = 0; i < j; i++)
80         {
81           add(other.elementAt(i));
82         }
83       }
84     }
85
86     /**
87      * adds a new column i to the selection - only if i is not already selected
88      * 
89      * @param i
90      */
91     void add(int i)
92     {
93       if (!selected.get(i))
94       {
95         order.add(Integer.valueOf(i));
96         selected.set(i);
97       }
98     }
99
100     void clear()
101     {
102       order.clear();
103       selected.clear();
104     }
105
106     void remove(int col)
107     {
108
109       Integer colInt = new Integer(col);
110
111       if (selected.get(col))
112       {
113         // if this ever changes to List.remove(), ensure Integer not int
114         // argument
115         // as List.remove(int i) removes the i'th item which is wrong
116         order.remove(colInt);
117         selected.clear(col);
118       }
119     }
120
121     boolean contains(Integer colInt)
122     {
123       return selected.get(colInt);
124     }
125
126     boolean isEmpty()
127     {
128       return order.isEmpty();
129     }
130
131     /**
132      * Returns a read-only view of the selected columns list
133      * 
134      * @return
135      */
136     List<Integer> getList()
137     {
138       return _uorder;
139     }
140
141     int size()
142     {
143       return order.size();
144     }
145
146     /**
147      * gets the column that was selected first, second or i'th
148      * 
149      * @param i
150      * @return
151      */
152     int elementAt(int i)
153     {
154       return order.get(i);
155     }
156
157     protected boolean pruneColumnList(final List<int[]> shifts)
158     {
159       int s = 0, t = shifts.size();
160       int[] sr = shifts.get(s++);
161       boolean pruned = false;
162       int i = 0, j = order.size();
163       while (i < j && s <= t)
164       {
165         int c = order.get(i++).intValue();
166         if (sr[0] <= c)
167         {
168           if (sr[1] + sr[0] >= c)
169           { // sr[1] -ve means inseriton.
170             order.remove(--i);
171             selected.clear(c);
172             j--;
173           }
174           else
175           {
176             if (s < t)
177             {
178               sr = shifts.get(s);
179             }
180             s++;
181           }
182         }
183       }
184       return pruned;
185     }
186
187     /**
188      * shift every selected column at or above start by change
189      * 
190      * @param start
191      *          - leftmost column to be shifted
192      * @param change
193      *          - delta for shift
194      */
195     void compensateForEdits(int start, int change)
196     {
197       BitSet mask = new BitSet();
198       for (int i = 0; i < order.size(); i++)
199       {
200         int temp = order.get(i);
201
202         if (temp >= start)
203         {
204           // clear shifted bits and update List of selected columns
205           selected.clear(temp);
206           mask.set(temp - change);
207           order.set(i, new Integer(temp - change));
208         }
209       }
210       // lastly update the bitfield all at once
211       selected.or(mask);
212     }
213
214     boolean isSelected(int column)
215     {
216       return selected.get(column);
217     }
218
219     int getMaxColumn()
220     {
221       return selected.length() - 1;
222     }
223
224     int getMinColumn()
225     {
226       return selected.get(0) ? 0 : selected.nextSetBit(0);
227     }
228
229     /**
230      * @return a series of selection intervals along the range
231      */
232     List<int[]> getRanges()
233     {
234       List<int[]> rlist = new ArrayList<int[]>();
235       if (selected.isEmpty())
236       {
237         return rlist;
238       }
239       int next = selected.nextSetBit(0), clear = -1;
240       while (next != -1)
241       {
242         clear = selected.nextClearBit(next);
243         rlist.add(new int[] { next, clear - 1 });
244         next = selected.nextSetBit(clear);
245       }
246       return rlist;
247     }
248
249     @Override
250     public int hashCode()
251     {
252       // TODO Auto-generated method stub
253       return selected.hashCode();
254     }
255
256     @Override
257     public boolean equals(Object obj)
258     {
259       if (obj instanceof IntList)
260       {
261         return ((IntList) obj).selected.equals(selected);
262       }
263       return false;
264     }
265   }
266
267   IntList selection = new IntList();
268
269   HiddenColumns hiddenColumns = new HiddenColumns();
270
271   /**
272    * Add a column to the selection
273    * 
274    * @param col
275    *          index of column
276    */
277   public void addElement(int col)
278   {
279     selection.add(col);
280   }
281
282   /**
283    * clears column selection
284    */
285   public void clear()
286   {
287     selection.clear();
288   }
289
290   /**
291    * Removes value 'col' from the selection (not the col'th item)
292    * 
293    * @param col
294    *          index of column to be removed
295    */
296   public void removeElement(int col)
297   {
298     selection.remove(col);
299   }
300
301   /**
302    * removes a range of columns from the selection
303    * 
304    * @param start
305    *          int - first column in range to be removed
306    * @param end
307    *          int - last col
308    */
309   public void removeElements(int start, int end)
310   {
311     Integer colInt;
312     for (int i = start; i < end; i++)
313     {
314       colInt = new Integer(i);
315       if (selection.contains(colInt))
316       {
317         selection.remove(colInt);
318       }
319     }
320   }
321
322   /**
323    * Returns a read-only view of the (possibly empty) list of selected columns
324    * <p>
325    * The list contains no duplicates but is not necessarily ordered. It also may
326    * include columns hidden from the current view. To modify (for example sort)
327    * the list, you should first make a copy.
328    * <p>
329    * The list is not thread-safe: iterating over it could result in
330    * ConcurrentModificationException if it is modified by another thread.
331    */
332   public List<Integer> getSelected()
333   {
334     return selection.getList();
335   }
336
337   /**
338    * @return list of int arrays containing start and end column position for
339    *         runs of selected columns ordered from right to left.
340    */
341   public List<int[]> getSelectedRanges()
342   {
343     return selection.getRanges();
344   }
345
346   /**
347    * 
348    * @param col
349    *          index to search for in column selection
350    * 
351    * @return true if col is selected
352    */
353   public boolean contains(int col)
354   {
355     return (col > -1) ? selection.isSelected(col) : false;
356   }
357
358   /**
359    * Answers true if no columns are selected, else false
360    */
361   public boolean isEmpty()
362   {
363     return selection == null || selection.isEmpty();
364   }
365
366   /**
367    * rightmost selected column
368    * 
369    * @return rightmost column in alignment that is selected
370    */
371   public int getMax()
372   {
373     if (selection.isEmpty())
374     {
375       return -1;
376     }
377     return selection.getMaxColumn();
378   }
379
380   /**
381    * Leftmost column in selection
382    * 
383    * @return column index of leftmost column in selection
384    */
385   public int getMin()
386   {
387     if (selection.isEmpty())
388     {
389       return 1000000000;
390     }
391     return selection.getMinColumn();
392   }
393
394   /**
395    * propagate shift in alignment columns to column selection
396    * 
397    * @param start
398    *          beginning of edit
399    * @param left
400    *          shift in edit (+ve for removal, or -ve for inserts)
401    */
402   /*  public List<int[]> compensateForEdit(int start, int change)
403     {
404       selection.compensateForEdits(start, change);
405       return hiddenColumns.compensateForEdit(start, change, this);
406     }
407   */
408   /**
409    * propagate shift in alignment columns to column selection special version of
410    * compensateForEdit - allowing for edits within hidden regions
411    * 
412    * @param start
413    *          beginning of edit
414    * @param left
415    *          shift in edit (+ve for removal, or -ve for inserts)
416    */
417   private void compensateForDelEdits(int start, int change)
418   {
419     selection.compensateForEdits(start, change);
420     hiddenColumns.compensateForDelEdits(start, change);
421   }
422
423   /**
424    * Adjust hidden column boundaries based on a series of column additions or
425    * deletions in visible regions.
426    * 
427    * @param shiftrecord
428    * @return
429    */
430   private ShiftList compensateForEdits(ShiftList shiftrecord)
431   {
432     if (shiftrecord != null)
433     {
434       final List<int[]> shifts = shiftrecord.getShifts();
435       if (shifts != null && shifts.size() > 0)
436       {
437         int shifted = 0;
438         for (int i = 0, j = shifts.size(); i < j; i++)
439         {
440           int[] sh = shifts.get(i);
441           compensateForDelEdits(shifted + sh[0], sh[1]);
442           shifted -= sh[1];
443         }
444       }
445       return shiftrecord.getInverse();
446     }
447     return null;
448   }
449
450
451   /**
452    * remove any hiddenColumns or selected columns and shift remaining based on a
453    * series of position, range deletions.
454    * 
455    * @param deletions
456    */
457   private void pruneDeletions(ShiftList deletions)
458   {
459     if (deletions != null)
460     {
461       final List<int[]> shifts = deletions.getShifts();
462       if (shifts != null && shifts.size() > 0)
463       {
464         hiddenColumns.pruneDeletions(shifts);
465
466         if (selection != null && selection.size() > 0)
467         {
468           selection.pruneColumnList(shifts);
469           if (selection != null && selection.size() == 0)
470           {
471             selection = null;
472           }
473         }
474         // and shift the rest.
475         this.compensateForEdits(deletions);
476       }
477     }
478   }
479
480
481   public void hideSelectedColumns(AlignmentI al)
482   {
483     synchronized (selection)
484     {
485       for (int[] selregions : selection.getRanges())
486       {
487         al.getHiddenColumns().hideColumns(selregions[0], selregions[1]);
488       }
489       selection.clear();
490     }
491
492   }
493
494
495   /**
496    * Hides the specified column and any adjacent selected columns
497    * 
498    * @param res
499    *          int
500    */
501   public void hideSelectedColumns(int col, AlignmentI al)
502   {
503     /*
504      * deselect column (whether selected or not!)
505      */
506     removeElement(col);
507
508     /*
509      * find adjacent selected columns
510      */
511     int min = col - 1, max = col + 1;
512     while (contains(min))
513     {
514       removeElement(min);
515       min--;
516     }
517
518     while (contains(max))
519     {
520       removeElement(max);
521       max++;
522     }
523
524     /*
525      * min, max are now the closest unselected columns
526      */
527     min++;
528     max--;
529     if (min > max)
530     {
531       min = max;
532     }
533
534     al.getHiddenColumns().hideColumns(min, max);
535   }
536
537
538
539
540
541   /**
542    * Copy constructor
543    * 
544    * @param copy
545    */
546   public ColumnSelection(ColumnSelection copy)
547   {
548     if (copy != null)
549     {
550       selection = new IntList(copy.selection);
551     }
552   }
553
554   /**
555    * ColumnSelection
556    */
557   public ColumnSelection()
558   {
559   }
560
561
562
563
564
565
566   /**
567    * Invert the column selection from first to end-1. leaves hiddenColumns
568    * untouched (and unselected)
569    * 
570    * @param first
571    * @param end
572    */
573   public void invertColumnSelection(int first, int width, AlignmentI al)
574   {
575     boolean hasHidden = al.getHiddenColumns().hasHidden();
576     for (int i = first; i < width; i++)
577     {
578       if (contains(i))
579       {
580         removeElement(i);
581       }
582       else
583       {
584         if (!hasHidden || al.getHiddenColumns().isVisible(i))
585         {
586           addElement(i);
587         }
588       }
589     }
590   }
591
592   /**
593    * add in any unselected columns from the given column selection, excluding
594    * any that are hidden.
595    * 
596    * @param colsel
597    */
598   public void addElementsFrom(ColumnSelection colsel)
599   {
600     if (colsel != null && !colsel.isEmpty())
601     {
602       for (Integer col : colsel.getSelected())
603       {
604         if (hiddenColumns != null
605                 && hiddenColumns.isVisible(col.intValue()))
606         {
607           selection.add(col);
608         }
609       }
610     }
611   }
612
613   /**
614    * set the selected columns the given column selection, excluding any columns
615    * that are hidden.
616    * 
617    * @param colsel
618    */
619   public void setElementsFrom(ColumnSelection colsel)
620   {
621     selection = new IntList();
622     if (colsel.selection != null && colsel.selection.size() > 0)
623     {
624       if (hiddenColumns.hasHidden())
625       {
626         // only select visible columns in this columns selection
627         addElementsFrom(colsel);
628       }
629       else
630       {
631         // add everything regardless
632         for (Integer col : colsel.getSelected())
633         {
634           addElement(col);
635         }
636       }
637     }
638   }
639
640   /**
641    * Add gaps into the sequences aligned to profileseq under the given
642    * AlignmentView
643    * 
644    * @param profileseq
645    * @param al
646    *          - alignment to have gaps inserted into it
647    * @param input
648    *          - alignment view where sequence corresponding to profileseq is
649    *          first entry
650    * @return new Column selection for new alignment view, with insertions into
651    *         profileseq marked as hidden.
652    */
653   public static ColumnSelection propagateInsertions(SequenceI profileseq,
654           AlignmentI al, AlignmentView input)
655   {
656     int profsqpos = 0;
657
658     // return propagateInsertions(profileseq, al, )
659     char gc = al.getGapCharacter();
660     Object[] alandcolsel = input.getAlignmentAndHiddenColumns(gc);
661     ColumnSelection nview = (ColumnSelection) alandcolsel[1];
662     SequenceI origseq = ((SequenceI[]) alandcolsel[0])[profsqpos];
663     nview.propagateInsertions(profileseq, al, origseq);
664     return nview;
665   }
666
667   /**
668    * 
669    * @param profileseq
670    *          - sequence in al which corresponds to origseq
671    * @param al
672    *          - alignment which is to have gaps inserted into it
673    * @param origseq
674    *          - sequence corresponding to profileseq which defines gap map for
675    *          modifying al
676    */
677   private void propagateInsertions(SequenceI profileseq, AlignmentI al,
678           SequenceI origseq)
679   {
680     char gc = al.getGapCharacter();
681     // recover mapping between sequence's non-gap positions and positions
682     // mapping to view.
683     pruneDeletions(ShiftList.parseMap(origseq.gapMap()));
684     int[] viscontigs = hiddenColumns.getVisibleContigs(0,
685             profileseq.getLength());
686     int spos = 0;
687     int offset = 0;
688
689     // add profile to visible contigs
690     for (int v = 0; v < viscontigs.length; v += 2)
691     {
692       if (viscontigs[v] > spos)
693       {
694         StringBuffer sb = new StringBuffer();
695         for (int s = 0, ns = viscontigs[v] - spos; s < ns; s++)
696         {
697           sb.append(gc);
698         }
699         for (int s = 0, ns = al.getHeight(); s < ns; s++)
700         {
701           SequenceI sqobj = al.getSequenceAt(s);
702           if (sqobj != profileseq)
703           {
704             String sq = al.getSequenceAt(s).getSequenceAsString();
705             if (sq.length() <= spos + offset)
706             {
707               // pad sequence
708               int diff = spos + offset - sq.length() - 1;
709               if (diff > 0)
710               {
711                 // pad gaps
712                 sq = sq + sb;
713                 while ((diff = spos + offset - sq.length() - 1) > 0)
714                 {
715                   // sq = sq
716                   // + ((diff >= sb.length()) ? sb.toString() : sb
717                   // .substring(0, diff));
718                   if (diff >= sb.length())
719                   {
720                     sq += sb.toString();
721                   }
722                   else
723                   {
724                     char[] buf = new char[diff];
725                     sb.getChars(0, diff, buf, 0);
726                     sq += buf.toString();
727                   }
728                 }
729               }
730               sq += sb.toString();
731             }
732             else
733             {
734               al.getSequenceAt(s).setSequence(
735                       sq.substring(0, spos + offset) + sb.toString()
736                               + sq.substring(spos + offset));
737             }
738           }
739         }
740         // offset+=sb.length();
741       }
742       spos = viscontigs[v + 1] + 1;
743     }
744     if ((offset + spos) < profileseq.getLength())
745     {
746       // pad the final region with gaps.
747       StringBuffer sb = new StringBuffer();
748       for (int s = 0, ns = profileseq.getLength() - spos - offset; s < ns; s++)
749       {
750         sb.append(gc);
751       }
752       for (int s = 0, ns = al.getHeight(); s < ns; s++)
753       {
754         SequenceI sqobj = al.getSequenceAt(s);
755         if (sqobj == profileseq)
756         {
757           continue;
758         }
759         String sq = sqobj.getSequenceAsString();
760         // pad sequence
761         int diff = origseq.getLength() - sq.length();
762         while (diff > 0)
763         {
764           // sq = sq
765           // + ((diff >= sb.length()) ? sb.toString() : sb
766           // .substring(0, diff));
767           if (diff >= sb.length())
768           {
769             sq += sb.toString();
770           }
771           else
772           {
773             char[] buf = new char[diff];
774             sb.getChars(0, diff, buf, 0);
775             sq += buf.toString();
776           }
777           diff = origseq.getLength() - sq.length();
778         }
779       }
780     }
781   }
782
783   /**
784    * 
785    * @return true if there are columns marked
786    */
787   public boolean hasSelectedColumns()
788   {
789     return (selection != null && selection.size() > 0);
790   }
791
792
793
794   public boolean filterAnnotations(Annotation[] annotations,
795           AnnotationFilterParameter filterParams)
796   {
797     // JBPNote - this method needs to be refactored to become independent of
798     // viewmodel package
799     hiddenColumns.revealAllHiddenColumns(this);
800     this.clear();
801     int count = 0;
802     do
803     {
804       if (annotations[count] != null)
805       {
806
807         boolean itemMatched = false;
808
809         if (filterParams.getThresholdType() == AnnotationFilterParameter.ThresholdType.ABOVE_THRESHOLD
810                 && annotations[count].value >= filterParams
811                         .getThresholdValue())
812         {
813           itemMatched = true;
814         }
815         if (filterParams.getThresholdType() == AnnotationFilterParameter.ThresholdType.BELOW_THRESHOLD
816                 && annotations[count].value <= filterParams
817                         .getThresholdValue())
818         {
819           itemMatched = true;
820         }
821
822         if (filterParams.isFilterAlphaHelix()
823                 && annotations[count].secondaryStructure == 'H')
824         {
825           itemMatched = true;
826         }
827
828         if (filterParams.isFilterBetaSheet()
829                 && annotations[count].secondaryStructure == 'E')
830         {
831           itemMatched = true;
832         }
833
834         if (filterParams.isFilterTurn()
835                 && annotations[count].secondaryStructure == 'S')
836         {
837           itemMatched = true;
838         }
839
840         String regexSearchString = filterParams.getRegexString();
841         if (regexSearchString != null
842                 && !filterParams.getRegexSearchFields().isEmpty())
843         {
844           List<SearchableAnnotationField> fields = filterParams
845                   .getRegexSearchFields();
846           try
847           {
848             if (fields.contains(SearchableAnnotationField.DISPLAY_STRING)
849                     && annotations[count].displayCharacter
850                             .matches(regexSearchString))
851             {
852               itemMatched = true;
853             }
854           } catch (java.util.regex.PatternSyntaxException pse)
855           {
856             if (annotations[count].displayCharacter
857                     .equals(regexSearchString))
858             {
859               itemMatched = true;
860             }
861           }
862           if (fields.contains(SearchableAnnotationField.DESCRIPTION)
863                   && annotations[count].description != null
864                   && annotations[count].description
865                           .matches(regexSearchString))
866           {
867             itemMatched = true;
868           }
869         }
870
871         if (itemMatched)
872         {
873           this.addElement(count);
874         }
875       }
876       count++;
877     } while (count < annotations.length);
878     return false;
879   }
880
881   /**
882    * Returns a hashCode built from selected columns and hidden column ranges
883    */
884   @Override
885   public int hashCode()
886   {
887     int hashCode = selection.hashCode();
888     return hiddenColumns.hashCode(hashCode);
889   }
890
891   /**
892    * Answers true if comparing to a ColumnSelection with the same selected
893    * columns and hidden columns, else false
894    */
895   @Override
896   public boolean equals(Object obj)
897   {
898     if (!(obj instanceof ColumnSelection))
899     {
900       return false;
901     }
902     ColumnSelection that = (ColumnSelection) obj;
903
904     /*
905      * check columns selected are either both null, or match
906      */
907     if (this.selection == null)
908     {
909       if (that.selection != null)
910       {
911         return false;
912       }
913     }
914     if (!this.selection.equals(that.selection))
915     {
916       return false;
917     }
918
919     return this.hiddenColumns.equals(that.hiddenColumns);
920   }
921
922   /**
923    * Updates the column selection depending on the parameters, and returns true
924    * if any change was made to the selection
925    * 
926    * @param markedColumns
927    *          a set identifying marked columns (base 0)
928    * @param startCol
929    *          the first column of the range to operate over (base 0)
930    * @param endCol
931    *          the last column of the range to operate over (base 0)
932    * @param invert
933    *          if true, deselect marked columns and select unmarked
934    * @param extendCurrent
935    *          if true, extend rather than replacing the current column selection
936    * @param toggle
937    *          if true, toggle the selection state of marked columns
938    * 
939    * @return
940    */
941   public boolean markColumns(BitSet markedColumns, int startCol,
942           int endCol, boolean invert, boolean extendCurrent, boolean toggle)
943   {
944     boolean changed = false;
945     if (!extendCurrent && !toggle)
946     {
947       changed = !this.isEmpty();
948       clear();
949     }
950     if (invert)
951     {
952       // invert only in the currently selected sequence region
953       int i = markedColumns.nextClearBit(startCol);
954       int ibs = markedColumns.nextSetBit(startCol);
955       while (i >= startCol && i <= endCol)
956       {
957         if (ibs < 0 || i < ibs)
958         {
959           changed = true;
960           if (toggle && contains(i))
961           {
962             removeElement(i++);
963           }
964           else
965           {
966             addElement(i++);
967           }
968         }
969         else
970         {
971           i = markedColumns.nextClearBit(ibs);
972           ibs = markedColumns.nextSetBit(i);
973         }
974       }
975     }
976     else
977     {
978       int i = markedColumns.nextSetBit(startCol);
979       while (i >= startCol && i <= endCol)
980       {
981         changed = true;
982         if (toggle && contains(i))
983         {
984           removeElement(i);
985         }
986         else
987         {
988           addElement(i);
989         }
990         i = markedColumns.nextSetBit(i + 1);
991       }
992     }
993     return changed;
994   }
995
996   /**
997    * Adjusts column selections, and the given selection group, to match the
998    * range of a stretch (e.g. mouse drag) operation
999    * <p>
1000    * Method refactored from ScalePanel.mouseDragged
1001    * 
1002    * @param res
1003    *          current column position, adjusted for hidden columns
1004    * @param sg
1005    *          current selection group
1006    * @param min
1007    *          start position of the stretch group
1008    * @param max
1009    *          end position of the stretch group
1010    */
1011   public void stretchGroup(int res, SequenceGroup sg, int min, int max)
1012   {
1013     if (!contains(res))
1014     {
1015       addElement(res);
1016     }
1017
1018     if (res > sg.getStartRes())
1019     {
1020       // expand selection group to the right
1021       sg.setEndRes(res);
1022     }
1023     if (res < sg.getStartRes())
1024     {
1025       // expand selection group to the left
1026       sg.setStartRes(res);
1027     }
1028
1029     /*
1030      * expand or shrink column selection to match the
1031      * range of the drag operation
1032      */
1033     for (int col = min; col <= max; col++)
1034     {
1035       if (col < sg.getStartRes() || col > sg.getEndRes())
1036       {
1037         // shrinking drag - remove from selection
1038         removeElement(col);
1039       }
1040       else
1041       {
1042         // expanding drag - add to selection
1043         addElement(col);
1044       }
1045     }
1046   }
1047 }