JAL-3253 temporary branch SwingJS upgrade with testNG fixes Java 8
[jalview.git] / src / jalview / viewmodel / ViewportRanges.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.viewmodel;
22
23 import jalview.datamodel.AlignmentI;
24 import jalview.datamodel.HiddenColumns;
25
26 /**
27  * Supplies and updates viewport properties relating to position such as: start
28  * and end residues and sequences; ideally will serve hidden columns/rows too.
29  * Intention also to support calculations for positioning, scrolling etc. such
30  * as finding the middle of the viewport, checking for scrolls off screen
31  */
32 public class ViewportRanges extends ViewportProperties
33 {
34   public static final String STARTRES = "startres";
35
36   public static final String ENDRES = "endres";
37
38   public static final String STARTSEQ = "startseq";
39
40   public static final String ENDSEQ = "endseq";
41
42   public static final String STARTRESANDSEQ = "startresandseq";
43
44   public static final String MOVE_VIEWPORT = "move_viewport";
45
46   private boolean wrappedMode = false;
47
48   // start residue of viewport
49   private int startRes;
50
51   // end residue of viewport
52   private int endRes;
53
54   // start sequence of viewport
55   private int startSeq;
56
57   // end sequence of viewport
58   private int endSeq;
59
60   // alignment
61   private AlignmentI al;
62
63   /**
64    * Constructor
65    * 
66    * @param alignment
67    *          the viewport's alignment
68    */
69   public ViewportRanges(AlignmentI alignment)
70   {
71     // initial values of viewport settings
72     this.startRes = 0;
73     this.endRes = alignment.getWidth() - 1;
74     this.startSeq = 0;
75     this.endSeq = alignment.getHeight() - 1;
76     this.al = alignment;
77   }
78
79   /**
80    * Get alignment width in cols, including hidden cols
81    */
82   public int getAbsoluteAlignmentWidth()
83   {
84     return al.getWidth();
85   }
86
87   /**
88    * Get alignment height in rows, including hidden rows
89    */
90   public int getAbsoluteAlignmentHeight()
91   {
92     return al.getHeight() + al.getHiddenSequences().getSize();
93   }
94
95   /**
96    * Get alignment width in cols, excluding hidden cols
97    */
98   public int getVisibleAlignmentWidth()
99   {
100     return al.getVisibleWidth();
101   }
102
103   /**
104    * Get alignment height in rows, excluding hidden rows
105    */
106   public int getVisibleAlignmentHeight()
107   {
108     return al.getHeight();
109   }
110
111   /**
112    * Set first residue visible in the viewport, and retain the current width.
113    * Fires a property change event.
114    * 
115    * @param res
116    *          residue position
117    */
118   public void setStartRes(int res)
119   {
120     int width = getViewportWidth();
121     setStartEndRes(res, res + width - 1);
122   }
123
124   /**
125    * Set start and end residues at the same time. This method only fires one
126    * event for the two changes, and should be used in preference to separate
127    * calls to setStartRes and setEndRes.
128    * 
129    * @param start
130    *          the start residue
131    * @param end
132    *          the end residue
133    */
134   public void setStartEndRes(int start, int end)
135   {
136     int[] oldvalues = updateStartEndRes(start, end);
137     int oldstartres = oldvalues[0];
138     int oldendres = oldvalues[1];
139
140     if (oldstartres == startRes && oldendres == endRes)
141     {
142       return; // BH 2019.07.27 standard check for no changes
143     }
144
145     // "STARTRES" is a misnomer here -- really "STARTORENDRES"
146     // note that this could be "no change" if the range is just being expanded
147     changeSupport.firePropertyChange(STARTRES, oldstartres, startRes);
148     if (oldstartres == startRes)
149     {
150       // No listener cares about this
151       // "ENDRES" is a misnomer here -- really "ENDONLYRES"
152       // BH 2019.07.27 adds end change check
153       // fire only if only the end is changed
154       changeSupport.firePropertyChange(ENDRES, oldendres, endRes);
155     }
156   }
157
158   /**
159    * Update start and end residue values, adjusting for width constraints if
160    * necessary
161    * 
162    * @param start
163    *          start residue
164    * @param end
165    *          end residue
166    * @return array containing old start and end residue values
167    */
168   private int[] updateStartEndRes(int start, int end)
169   {
170     int oldstartres = this.startRes;
171
172     /*
173      * if not wrapped, don't leave white space at the right margin
174      */
175     int lastColumn = getVisibleAlignmentWidth() - 1;
176     if (!wrappedMode && (start > lastColumn))
177     {
178       startRes = Math.max(lastColumn, 0);
179     }
180     else if (start < 0)
181     {
182       startRes = 0;
183     }
184     else
185     {
186       startRes = start;
187     }
188
189     int oldendres = this.endRes;
190     if (end < 0)
191     {
192       endRes = 0;
193     }
194     else if (!wrappedMode && (end > lastColumn))
195     {
196       endRes = Math.max(lastColumn, 0);
197     }
198     else
199     {
200       endRes = end;
201     }
202     return new int[] { oldstartres, oldendres };
203   }
204
205   /**
206    * Set the first sequence visible in the viewport, maintaining the height. If
207    * the viewport would extend past the last sequence, sets the viewport so it
208    * sits at the bottom of the alignment. Fires a property change event.
209    * 
210    * @param seq
211    *          sequence position
212    */
213   public void setStartSeq(int seq)
214   {
215     int height = getViewportHeight();
216     int startseq = Math.min(seq, getVisibleAlignmentHeight() - height);
217     // BH 2019.07.27 cosmetic only -- was:
218     // if (startseq + height - 1 > getVisibleAlignmentHeight() - 1)
219     // {
220     // startseq = getVisibleAlignmentHeight() - height;
221     // }
222     setStartEndSeq(startseq, startseq + height - 1);
223   }
224
225   /**
226    * Set start and end sequences at the same time. The viewport height may
227    * change. This method only fires one event for the two changes, and should be
228    * used in preference to separate calls to setStartSeq and setEndSeq.
229    * 
230    * @param start
231    *          the start sequence
232    * @param end
233    *          the end sequence
234    */
235   public void setStartEndSeq(int start, int end)
236   {
237     // System.out.println("ViewportRange setStartEndSeq " + start + " " + end);
238     int[] oldvalues = updateStartEndSeq(start, end);
239     int oldstartseq = oldvalues[0];
240     int oldendseq = oldvalues[1];
241
242     if (oldstartseq == startSeq && oldendseq == endSeq)
243     {
244       return; // BH 2019.07.27 standard check for no changes
245     }
246
247     // "STARTSEQ" is a misnomer here -- really "STARTORENDSEQ"
248     changeSupport.firePropertyChange(STARTSEQ, oldstartseq, startSeq);
249     if (oldstartseq == startSeq)
250     {
251       // Note that all listeners ignore this - could be removed, or there is a
252       // bug.
253       // "ENDSEQ" is a misnomer here -- really "ENDONLYSEQ"
254       // additional fire, only if only the end is changed
255       changeSupport.firePropertyChange(ENDSEQ, oldendseq, endSeq);
256     }
257   }
258
259   /**
260    * Update start and end sequence values, adjusting for height constraints if
261    * necessary
262    * 
263    * @param start
264    *          start sequence
265    * @param end
266    *          end sequence
267    * @return array containing old start and end sequence values
268    */
269   private int[] updateStartEndSeq(int start, int end)
270   {
271     int oldstartseq = this.startSeq;
272     int visibleHeight = getVisibleAlignmentHeight();
273     if (start > visibleHeight - 1)
274     {
275       startSeq = Math.max(visibleHeight - 1, 0);
276     }
277     else if (start < 0)
278     {
279       startSeq = 0;
280     }
281     else
282     {
283       startSeq = start;
284     }
285
286     int oldendseq = this.endSeq;
287     if (end >= visibleHeight)
288     {
289       endSeq = Math.max(visibleHeight - 1, 0);
290     }
291     else if (end < 0)
292     {
293       endSeq = 0;
294     }
295     else
296     {
297       endSeq = end;
298     }
299     return new int[] { oldstartseq, oldendseq };
300   }
301
302   /**
303    * Set the last sequence visible in the viewport. Fires a property change
304    * event.
305    * 
306    * @param seq
307    *          sequence position in the range [0, height)
308    */
309   public void setEndSeq(int seq)
310   {
311     // BH 2018.04.18 added safety for seq < 0; comment about not being >= height
312     setStartEndSeq(Math.max(0, seq + 1 - getViewportHeight()), seq);
313   }
314
315   /**
316    * Set start residue and start sequence together (fires single event). The
317    * event supplies a pair of old values and a pair of new values: [old start
318    * residue, old start sequence] and [new start residue, new start sequence]
319    * 
320    * @param res
321    *          the start residue
322    * @param seq
323    *          the start sequence
324    */
325   public void setStartResAndSeq(int res, int seq)
326   {
327     int width = getViewportWidth();
328     int[] oldresvalues = updateStartEndRes(res, res + width - 1);
329
330     int startseq = seq;
331     int height = getViewportHeight();
332     if (startseq + height - 1 > getVisibleAlignmentHeight() - 1)
333     {
334       startseq = getVisibleAlignmentHeight() - height;
335     }
336     int[] oldseqvalues = updateStartEndSeq(startseq, startseq + height - 1);
337
338     int[] oldvalues = new int[] { oldresvalues[0], oldseqvalues[0] };
339     int[] newvalues = new int[] { startRes, startSeq };
340     changeSupport.firePropertyChange(STARTRESANDSEQ, oldvalues, newvalues);
341   }
342
343   /**
344    * Get start residue of viewport
345    */
346   public int getStartRes()
347   {
348     return startRes;
349   }
350
351   /**
352    * Get end residue of viewport
353    */
354   public int getEndRes()
355   {
356     return endRes;
357   }
358
359   /**
360    * Get start sequence of viewport
361    */
362   public int getStartSeq()
363   {
364     return startSeq;
365   }
366
367   /**
368    * Get end sequence of viewport
369    */
370   public int getEndSeq()
371   {
372     return endSeq;
373   }
374
375   /**
376    * Set viewport width in residues, without changing startRes. Use in
377    * preference to calculating endRes from the width, to avoid out by one
378    * errors! Fires a property change event.
379    * 
380    * @param w
381    *          width in residues
382    */
383   public void setViewportWidth(int w)
384   {
385     setStartEndRes(startRes, startRes + w - 1);
386   }
387
388   /**
389    * Set viewport height in residues, without changing startSeq. Use in
390    * preference to calculating endSeq from the height, to avoid out by one
391    * errors! Fires a property change event.
392    * 
393    * @param h
394    *          height in sequences
395    */
396   public void setViewportHeight(int h)
397   {
398     setStartEndSeq(startSeq, startSeq + h - 1);
399   }
400
401   /**
402    * Set viewport horizontal start position and width. Use in preference to
403    * calculating endRes from the width, to avoid out by one errors! Fires a
404    * property change event.
405    * 
406    * @param start
407    *          start residue
408    * @param w
409    *          width in residues
410    */
411   public void setViewportStartAndWidth(int start, int w)
412   {
413     int vpstart = start;
414     if (vpstart < 0)
415     {
416       vpstart = 0;
417     }
418
419     /*
420      * if not wrapped, don't leave white space at the right margin
421      */
422     if (!wrappedMode)
423     {
424       if ((w <= getVisibleAlignmentWidth())
425               && (vpstart + w - 1 > getVisibleAlignmentWidth() - 1))
426       {
427         vpstart = getVisibleAlignmentWidth() - w;
428       }
429
430     }
431     setStartEndRes(vpstart, vpstart + w - 1);
432   }
433
434   /**
435    * Set viewport vertical start position and height. Use in preference to
436    * calculating endSeq from the height, to avoid out by one errors! Fires a
437    * property change event.
438    * 
439    * @param start
440    *          start sequence
441    * @param h
442    *          height in sequences
443    */
444   public void setViewportStartAndHeight(int start, int h)
445   {
446     int vpstart = start;
447
448     int visHeight = getVisibleAlignmentHeight();
449     if (vpstart < 0)
450     {
451       vpstart = 0;
452     }
453     else if (h <= visHeight && vpstart + h > visHeight)
454     // viewport height is less than the full alignment and we are running off
455     // the bottom
456     {
457       vpstart = visHeight - h;
458     }
459
460     setStartEndSeq(vpstart, vpstart + h - 1);
461   }
462
463   /**
464    * Get width of viewport in residues
465    * 
466    * @return width of viewport
467    */
468   public int getViewportWidth()
469   {
470     return (endRes - startRes + 1);
471   }
472
473   /**
474    * Get height of viewport in residues
475    * 
476    * @return height of viewport
477    */
478   public int getViewportHeight()
479   {
480     return (endSeq - startSeq + 1);
481   }
482
483   /**
484    * Scroll the viewport range vertically. Fires a property change event.
485    * 
486    * @param up
487    *          true if scrolling up, false if down
488    * 
489    * @return true if the scroll is valid
490    */
491   public boolean scrollUp(boolean up)
492   {
493     /*
494      * if in unwrapped mode, scroll up or down one sequence row;
495      * if in wrapped mode, scroll by one visible width of columns
496      */
497     if (up)
498     {
499       if (wrappedMode)
500       {
501         pageUp();
502       }
503       else
504       {
505         if (startSeq < 1)
506         {
507           return false;
508         }
509         setStartSeq(startSeq - 1);
510       }
511     }
512     else
513     {
514       if (wrappedMode)
515       {
516         pageDown();
517       }
518       else
519       {
520         if (endSeq >= getVisibleAlignmentHeight() - 1)
521         {
522           return false;
523         }
524         setStartSeq(startSeq + 1);
525       }
526     }
527     return true;
528   }
529
530   /**
531    * Scroll the viewport range horizontally. Fires a property change event.
532    * 
533    * @param right
534    *          true if scrolling right, false if left
535    * 
536    * @return true if the scroll is valid
537    */
538   public boolean scrollRight(boolean right)
539   {
540     if (!right)
541     {
542       if (startRes < 1)
543       {
544         return false;
545       }
546
547       setStartRes(startRes - 1);
548     }
549     else
550     {
551       if (endRes >= getVisibleAlignmentWidth() - 1)
552       {
553         return false;
554       }
555
556       setStartRes(startRes + 1);
557     }
558
559     return true;
560   }
561
562   /**
563    * Scroll a wrapped alignment so that the specified residue is in the first
564    * repeat of the wrapped view. Fires a property change event. Answers true if
565    * the startRes changed, else false.
566    * 
567    * @param res
568    *          residue position to scroll to NB visible position not absolute
569    *          alignment position
570    * @return
571    */
572   public boolean scrollToWrappedVisible(int res)
573   {
574     int newStartRes = calcWrappedStartResidue(res);
575     if (newStartRes == startRes)
576     {
577       return false;
578     }
579     setStartRes(newStartRes);
580
581     return true;
582   }
583
584   /**
585    * Calculate wrapped start residue from visible start residue
586    * 
587    * @param res
588    *          visible start residue
589    * @return left column of panel res will be located in
590    */
591   private int calcWrappedStartResidue(int res)
592   {
593     int oldStartRes = startRes;
594     int width = getViewportWidth();
595
596     boolean up = res < oldStartRes;
597     int widthsToScroll = Math.abs((res - oldStartRes) / width);
598     if (up)
599     {
600       widthsToScroll++;
601     }
602
603     int residuesToScroll = width * widthsToScroll;
604     int newStartRes = up ? oldStartRes - residuesToScroll : oldStartRes
605             + residuesToScroll;
606     if (newStartRes < 0)
607     {
608       newStartRes = 0;
609     }
610     return newStartRes;
611   }
612
613   /**
614    * Scroll so that (x,y) is visible. Fires a property change event.
615    * 
616    * @param x
617    *          x position in alignment (absolute position)
618    * @param y
619    *          y position in alignment (absolute position)
620    */
621   public void scrollToVisible(int x, int y)
622   {
623     while (y < startSeq)
624     {
625       scrollUp(true);
626     }
627     while (y > endSeq)
628     {
629       scrollUp(false);
630     }
631     
632     HiddenColumns hidden = al.getHiddenColumns();
633     while (x < hidden.visibleToAbsoluteColumn(startRes))
634     {
635       if (!scrollRight(false))
636       {
637         break;
638       }
639     }
640     while (x > hidden.visibleToAbsoluteColumn(endRes))
641     {
642       if (!scrollRight(true))
643       {
644         break;
645       }
646     }
647   }
648
649   /**
650    * Set the viewport location so that a position is visible
651    * 
652    * @param x
653    *          column to be visible: absolute position in alignment
654    * @param y
655    *          row to be visible: absolute position in alignment
656    */
657   public boolean setViewportLocation(int x, int y)
658   {
659     boolean changedLocation = false;
660
661     // convert the x,y location to visible coordinates
662     int visX = al.getHiddenColumns().absoluteToVisibleColumn(x);
663     int visY = al.getHiddenSequences().findIndexWithoutHiddenSeqs(y);
664
665     // if (vis_x,vis_y) is already visible don't do anything
666     if (startRes > visX || visX > endRes
667             || startSeq > visY && visY > endSeq)
668     {
669       int[] old = new int[] { startRes, startSeq };
670       int[] newresseq;
671       if (wrappedMode)
672       {
673         int newstartres = calcWrappedStartResidue(visX);
674         setStartRes(newstartres);
675         newresseq = new int[] { startRes, startSeq };
676       }
677       else
678       {
679         // set the viewport x location to contain vis_x
680         int newstartres = visX;
681         int width = getViewportWidth();
682         if (newstartres + width - 1 > getVisibleAlignmentWidth() - 1)
683         {
684           newstartres = getVisibleAlignmentWidth() - width;
685         }
686         updateStartEndRes(newstartres, newstartres + width - 1);
687
688         // set the viewport y location to contain vis_y
689         int newstartseq = visY;
690         int height = getViewportHeight();
691         if (newstartseq + height - 1 > getVisibleAlignmentHeight() - 1)
692         {
693           newstartseq = getVisibleAlignmentHeight() - height;
694         }
695         updateStartEndSeq(newstartseq, newstartseq + height - 1);
696
697         newresseq = new int[] { startRes, startSeq };
698       }
699       changedLocation = true;
700       changeSupport.firePropertyChange(MOVE_VIEWPORT, old, newresseq);
701     }
702     return changedLocation;
703   }
704
705   /**
706    * Adjust sequence position for page up. Fires a property change event.
707    */
708   public void pageUp()
709   {
710     if (wrappedMode)
711     {
712       setStartRes(Math.max(0, getStartRes() - getViewportWidth()));
713     }
714     else
715     {
716       setViewportStartAndHeight(startSeq - (endSeq - startSeq),
717               getViewportHeight());
718     }
719   }
720
721   /**
722    * Adjust sequence position for page down. Fires a property change event.
723    */
724   public void pageDown()
725   {
726     if (wrappedMode)
727     {
728       /*
729        * if height is more than width (i.e. not all sequences fit on screen),
730        * increase page down to height
731        */
732       int newStart = getStartRes()
733               + Math.max(getViewportHeight(), getViewportWidth());
734
735       /*
736        * don't page down beyond end of alignment, or if not all
737        * sequences fit in the visible height
738        */
739       if (newStart < getVisibleAlignmentWidth())
740       {
741         setStartRes(newStart);
742       }
743     }
744     else
745     {
746       setViewportStartAndHeight(endSeq, getViewportHeight());
747     }
748   }
749
750   public void setWrappedMode(boolean wrapped)
751   {
752     wrappedMode = wrapped;
753   }
754
755   public boolean isWrappedMode()
756   {
757     return wrappedMode;
758   }
759
760   /**
761    * Answers the vertical scroll position (0..) to set, given the visible column
762    * that is at top left.
763    * 
764    * <pre>
765    * Example:
766    *    viewport width 40 columns (0-39, 40-79, 80-119...)
767    *    column 0 returns scroll position 0
768    *    columns 1-40 return scroll position 1
769    *    columns 41-80 return scroll position 2
770    *    etc
771    * </pre>
772    * 
773    * @param topLeftColumn
774    *          (0..)
775    * @return
776    */
777   public int getWrappedScrollPosition(final int topLeftColumn)
778   {
779     int w = getViewportWidth();
780
781     /*
782      * visible whole widths
783      */
784     int scroll = topLeftColumn / w;
785
786     /*
787      * add 1 for a part width if there is one
788      */
789     scroll += topLeftColumn % w > 0 ? 1 : 0;
790
791     return scroll;
792   }
793
794   /**
795    * Answers the maximum wrapped vertical scroll value, given the column
796    * position (0..) to show at top left of the visible region.
797    * 
798    * @param topLeftColumn
799    * @return
800    */
801   public int getWrappedMaxScroll(int topLeftColumn)
802   {
803     int scrollPosition = getWrappedScrollPosition(topLeftColumn);
804
805     /*
806      * how many more widths could be drawn after this one?
807      */
808     int columnsRemaining = getVisibleAlignmentWidth() - topLeftColumn;
809     int width = getViewportWidth();
810     int widthsRemaining = columnsRemaining / width
811             + (columnsRemaining % width > 0 ? 1 : 0) - 1;
812     int maxScroll = scrollPosition + widthsRemaining;
813
814     return maxScroll;
815   }
816
817   @Override
818   public String toString()
819   {
820     return "[ViewportRange startRes=" + startRes + " endRes=" + endRes
821             + " startSeq=" + startSeq + " endSeq=" + endSeq + "]";
822   }
823 }