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