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