JAL-629 Tidy up tests and replaced methods before merge to develop
[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 java.util.ArrayList;
24 import java.util.BitSet;
25 import java.util.Collections;
26 import java.util.List;
27 import java.util.regex.PatternSyntaxException;
28
29 import jalview.viewmodel.annotationfilter.AnnotationFilterParameter;
30 import jalview.viewmodel.annotationfilter.AnnotationFilterParameter.SearchableAnnotationField;
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<>();
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 = Integer.valueOf(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, Integer.valueOf(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<>();
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   private IntList selection = new IntList();
268
269   /**
270    * Add a column to the selection
271    * 
272    * @param col
273    *          index of column
274    */
275   public void addElement(int col)
276   {
277     selection.add(col);
278   }
279
280   /**
281    * clears column selection
282    */
283   public void clear()
284   {
285     selection.clear();
286   }
287
288   /**
289    * Removes value 'col' from the selection (not the col'th item)
290    * 
291    * @param col
292    *          index of column to be removed
293    */
294   public void removeElement(int col)
295   {
296     selection.remove(col);
297   }
298
299   /**
300    * removes a range of columns from the selection
301    * 
302    * @param start
303    *          int - first column in range to be removed
304    * @param end
305    *          int - last col
306    */
307   public void removeElements(int start, int end)
308   {
309     Integer colInt;
310     for (int i = start; i < end; i++)
311     {
312       colInt = Integer.valueOf(i);
313       if (selection.contains(colInt))
314       {
315         selection.remove(colInt);
316       }
317     }
318   }
319
320   /**
321    * Returns a read-only view of the (possibly empty) list of selected columns
322    * <p>
323    * The list contains no duplicates but is not necessarily ordered. It also may
324    * include columns hidden from the current view. To modify (for example sort)
325    * the list, you should first make a copy.
326    * <p>
327    * The list is not thread-safe: iterating over it could result in
328    * ConcurrentModificationException if it is modified by another thread.
329    */
330   public List<Integer> getSelected()
331   {
332     return selection.getList();
333   }
334
335   /**
336    * @return list of int arrays containing start and end column position for
337    *         runs of selected columns ordered from right to left.
338    */
339   public List<int[]> getSelectedRanges()
340   {
341     return selection.getRanges();
342   }
343
344   /**
345    * 
346    * @param col
347    *          index to search for in column selection
348    * 
349    * @return true if col is selected
350    */
351   public boolean contains(int col)
352   {
353     return (col > -1) ? selection.isSelected(col) : false;
354   }
355
356   /**
357    * 
358    */
359   public boolean intersects(int from, int to)
360   {
361     // TODO: do this in a more efficient bitwise way
362     for (int f = from; f <= to; f++)
363     {
364       if (selection.isSelected(f))
365       {
366         return true;
367       }
368     }
369     return false;
370   }
371
372   /**
373    * Answers true if no columns are selected, else false
374    */
375   public boolean isEmpty()
376   {
377     return selection == null || selection.isEmpty();
378   }
379
380   /**
381    * rightmost selected column
382    * 
383    * @return rightmost column in alignment that is selected
384    */
385   public int getMax()
386   {
387     if (selection.isEmpty())
388     {
389       return -1;
390     }
391     return selection.getMaxColumn();
392   }
393
394   /**
395    * Leftmost column in selection
396    * 
397    * @return column index of leftmost column in selection
398    */
399   public int getMin()
400   {
401     if (selection.isEmpty())
402     {
403       return 1000000000;
404     }
405     return selection.getMinColumn();
406   }
407
408   public void hideSelectedColumns(AlignmentI al)
409   {
410     synchronized (selection)
411     {
412       for (int[] selregions : selection.getRanges())
413       {
414         al.getHiddenColumns().hideColumns(selregions[0], selregions[1]);
415       }
416       selection.clear();
417     }
418
419   }
420
421   /**
422    * Hides the specified column and any adjacent selected columns
423    * 
424    * @param res
425    *          int
426    */
427   public void hideSelectedColumns(int col, HiddenColumns hidden)
428   {
429     /*
430      * deselect column (whether selected or not!)
431      */
432     removeElement(col);
433
434     /*
435      * find adjacent selected columns
436      */
437     int min = col - 1, max = col + 1;
438     while (contains(min))
439     {
440       removeElement(min);
441       min--;
442     }
443
444     while (contains(max))
445     {
446       removeElement(max);
447       max++;
448     }
449
450     /*
451      * min, max are now the closest unselected columns
452      */
453     min++;
454     max--;
455     if (min > max)
456     {
457       min = max;
458     }
459
460     hidden.hideColumns(min, max);
461   }
462
463   /**
464    * Copy constructor
465    * 
466    * @param copy
467    */
468   public ColumnSelection(ColumnSelection copy)
469   {
470     if (copy != null)
471     {
472       selection = new IntList(copy.selection);
473     }
474   }
475
476   /**
477    * ColumnSelection
478    */
479   public ColumnSelection()
480   {
481   }
482
483   /**
484    * Invert the column selection from first to end-1. leaves hiddenColumns
485    * untouched (and unselected)
486    * 
487    * @param first
488    * @param end
489    */
490   public void invertColumnSelection(int first, int width, AlignmentI al)
491   {
492     boolean hasHidden = al.getHiddenColumns().hasHiddenColumns();
493     for (int i = first; i < width; i++)
494     {
495       if (contains(i))
496       {
497         removeElement(i);
498       }
499       else
500       {
501         if (!hasHidden || al.getHiddenColumns().isVisible(i))
502         {
503           addElement(i);
504         }
505       }
506     }
507   }
508
509   /**
510    * set the selected columns to the given column selection, excluding any
511    * columns that are hidden.
512    * 
513    * @param colsel
514    */
515   public void setElementsFrom(ColumnSelection colsel,
516           HiddenColumns hiddenColumns)
517   {
518     selection = new IntList();
519     if (colsel.selection != null && colsel.selection.size() > 0)
520     {
521       if (hiddenColumns.hasHiddenColumns())
522       {
523         // only select visible columns in this columns selection
524         for (Integer col : colsel.getSelected())
525         {
526           if (hiddenColumns != null
527                   && hiddenColumns.isVisible(col.intValue()))
528           {
529             selection.add(col);
530           }
531         }
532       }
533       else
534       {
535         // add everything regardless
536         for (Integer col : colsel.getSelected())
537         {
538           addElement(col);
539         }
540       }
541     }
542   }
543
544   /**
545    * 
546    * @return true if there are columns marked
547    */
548   public boolean hasSelectedColumns()
549   {
550     return (selection != null && selection.size() > 0);
551   }
552
553   /**
554    * Selects columns where the given annotation matches the provided filter
555    * condition(s). Any existing column selections are first cleared. Answers the
556    * number of columns added.
557    * 
558    * @param annotations
559    * @param filterParams
560    * @return
561    */
562   public int filterAnnotations(AlignmentAnnotation ann_row,
563           AnnotationFilterParameter filterParams)
564   {
565     Annotation[] annotations = ann_row.annotations;
566     // JBPNote - this method needs to be refactored to become independent of
567     // viewmodel package
568     this.clear();
569
570     if (ann_row.graph == AlignmentAnnotation.CONTACT_MAP && (filterParams
571             .getThresholdType() == AnnotationFilterParameter.ThresholdType.ABOVE_THRESHOLD
572             || filterParams
573                     .getThresholdType() == AnnotationFilterParameter.ThresholdType.BELOW_THRESHOLD))
574     {
575       float tVal = filterParams.getThresholdValue();
576       if (ann_row.sequenceRef != null)
577       {
578         // TODO - get ContactList from AlignmentView for non-seq-ref associatd
579         for (int column = 0; column < annotations.length; column++)
580         {
581           if (ann_row.annotations[column] == null)
582           {
583             continue;
584           }
585
586           int cpos = ann_row.sequenceRef.findPosition(column) - 1;
587           ContactListI clist = ann_row.sequenceRef
588                   .getContactListFor(ann_row, cpos);
589           for (int row = column + 8,
590                   rowEnd = clist.getContactHeight(); row < rowEnd; row++)
591           {
592             if (filterParams
593                     .getThresholdType() == AnnotationFilterParameter.ThresholdType.ABOVE_THRESHOLD
594                             ? (clist.getContactAt(row) > tVal)
595                             : (clist.getContactAt(row) < tVal))
596             {
597               addElement(column);
598               break;
599               // int column_forrowpos = ann_row.sequenceRef.findIndex(row + 1);
600               // addElement(column_forrowpos);
601             }
602           }
603         }
604       }
605       return selection.size();
606     }
607
608     int addedCount = 0;
609     int column = 0;
610     do
611     {
612       Annotation ann = annotations[column];
613       if (ann != null)
614       {
615         float value = ann.value;
616         boolean matched = false;
617
618         /*
619          * filter may have multiple conditions - 
620          * these are or'd until a match is found
621          */
622         if (filterParams
623                 .getThresholdType() == AnnotationFilterParameter.ThresholdType.ABOVE_THRESHOLD
624                 && value > filterParams.getThresholdValue())
625         {
626           matched = true;
627         }
628
629         if (!matched && filterParams
630                 .getThresholdType() == AnnotationFilterParameter.ThresholdType.BELOW_THRESHOLD
631                 && value < filterParams.getThresholdValue())
632         {
633           matched = true;
634         }
635
636         if (!matched && filterParams.isFilterAlphaHelix()
637                 && ann.secondaryStructure == 'H')
638         {
639           matched = true;
640         }
641
642         if (!matched && filterParams.isFilterBetaSheet()
643                 && ann.secondaryStructure == 'E')
644         {
645           matched = true;
646         }
647
648         if (!matched && filterParams.isFilterTurn()
649                 && ann.secondaryStructure == 'S')
650         {
651           matched = true;
652         }
653
654         String regexSearchString = filterParams.getRegexString();
655         if (!matched && regexSearchString != null)
656         {
657           List<SearchableAnnotationField> fields = filterParams
658                   .getRegexSearchFields();
659           for (SearchableAnnotationField field : fields)
660           {
661             String compareTo = field == SearchableAnnotationField.DISPLAY_STRING
662                     ? ann.displayCharacter // match 'Label'
663                     : ann.description; // and/or 'Description'
664             if (compareTo != null)
665             {
666               try
667               {
668                 if (compareTo.matches(regexSearchString))
669                 {
670                   matched = true;
671                 }
672               } catch (PatternSyntaxException pse)
673               {
674                 if (compareTo.equals(regexSearchString))
675                 {
676                   matched = true;
677                 }
678               }
679               if (matched)
680               {
681                 break;
682               }
683             }
684           }
685         }
686
687         if (matched)
688         {
689           this.addElement(column);
690           addedCount++;
691         }
692       }
693       column++;
694     } while (column < annotations.length);
695
696     return addedCount;
697   }
698
699   /**
700    * Returns a hashCode built from selected columns ranges
701    */
702   @Override
703   public int hashCode()
704   {
705     return selection.hashCode();
706   }
707
708   /**
709    * Answers true if comparing to a ColumnSelection with the same selected
710    * columns and hidden columns, else false
711    */
712   @Override
713   public boolean equals(Object obj)
714   {
715     if (!(obj instanceof ColumnSelection))
716     {
717       return false;
718     }
719     ColumnSelection that = (ColumnSelection) obj;
720
721     /*
722      * check columns selected are either both null, or match
723      */
724     if (this.selection == null)
725     {
726       if (that.selection != null)
727       {
728         return false;
729       }
730     }
731     if (!this.selection.equals(that.selection))
732     {
733       return false;
734     }
735
736     return true;
737   }
738
739   /**
740    * Updates the column selection depending on the parameters, and returns true
741    * if any change was made to the selection
742    * 
743    * @param markedColumns
744    *          a set identifying marked columns (base 0)
745    * @param startCol
746    *          the first column of the range to operate over (base 0)
747    * @param endCol
748    *          the last column of the range to operate over (base 0)
749    * @param invert
750    *          if true, deselect marked columns and select unmarked
751    * @param extendCurrent
752    *          if true, extend rather than replacing the current column selection
753    * @param toggle
754    *          if true, toggle the selection state of marked columns
755    * 
756    * @return
757    */
758   public boolean markColumns(BitSet markedColumns, int startCol, int endCol,
759           boolean invert, boolean extendCurrent, boolean toggle)
760   {
761     boolean changed = false;
762     if (!extendCurrent && !toggle)
763     {
764       changed = !this.isEmpty();
765       clear();
766     }
767     if (invert)
768     {
769       // invert only in the currently selected sequence region
770       int i = markedColumns.nextClearBit(startCol);
771       int ibs = markedColumns.nextSetBit(startCol);
772       while (i >= startCol && i <= endCol)
773       {
774         if (ibs < 0 || i < ibs)
775         {
776           changed = true;
777           if (toggle && contains(i))
778           {
779             removeElement(i++);
780           }
781           else
782           {
783             addElement(i++);
784           }
785         }
786         else
787         {
788           i = markedColumns.nextClearBit(ibs);
789           ibs = markedColumns.nextSetBit(i);
790         }
791       }
792     }
793     else
794     {
795       int i = markedColumns.nextSetBit(startCol);
796       while (i >= startCol && i <= endCol)
797       {
798         changed = true;
799         if (toggle && contains(i))
800         {
801           removeElement(i);
802         }
803         else
804         {
805           addElement(i);
806         }
807         i = markedColumns.nextSetBit(i + 1);
808       }
809     }
810     return changed;
811   }
812
813   /**
814    * Adjusts column selections, and the given selection group, to match the
815    * range of a stretch (e.g. mouse drag) operation
816    * <p>
817    * Method refactored from ScalePanel.mouseDragged
818    * 
819    * @param res
820    *          current column position, adjusted for hidden columns
821    * @param sg
822    *          current selection group
823    * @param min
824    *          start position of the stretch group
825    * @param max
826    *          end position of the stretch group
827    */
828   public void stretchGroup(int res, SequenceGroup sg, int min, int max)
829   {
830     if (!contains(res))
831     {
832       addElement(res);
833     }
834
835     if (res > sg.getStartRes())
836     {
837       // expand selection group to the right
838       sg.setEndRes(res);
839     }
840     if (res < sg.getStartRes())
841     {
842       // expand selection group to the left
843       sg.setStartRes(res);
844     }
845
846     /*
847      * expand or shrink column selection to match the
848      * range of the drag operation
849      */
850     for (int col = min; col <= max; col++)
851     {
852       if (col < sg.getStartRes() || col > sg.getEndRes())
853       {
854         // shrinking drag - remove from selection
855         removeElement(col);
856       }
857       else
858       {
859         // expanding drag - add to selection
860         addElement(col);
861       }
862     }
863   }
864 }