Lumiera  0.pre.03
»edit your freedom«
zoom-window.hpp
Go to the documentation of this file.
1 /*
2  ZOOM-WINDOW.hpp - generic translation from domain to screen coordinates
3 
4  Copyright (C) Lumiera.org
5  2018, Hermann Vosseler <Ichthyostega@web.de>
6 
7  This program is free software; you can redistribute it and/or
8  modify it under the terms of the GNU General Public License as
9  published by the Free Software Foundation; either version 2 of
10  the License, or (at your option) any later version.
11 
12  This program is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  GNU General Public License for more details.
16 
17  You should have received a copy of the GNU General Public License
18  along with this program; if not, write to the Free Software
19  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 
21 */
22 
23 
90 #ifndef STAGE_MODEL_ZOOM_WINDOW_H
91 #define STAGE_MODEL_ZOOM_WINDOW_H
92 
93 
94 #include "lib/error.hpp"
95 #include "lib/rational.hpp"
96 #include "lib/time/timevalue.hpp"
97 #include "lib/nocopy.hpp"
98 #include "lib/util.hpp"
99 
100 #include <limits>
101 #include <functional>
102 #include <array>
103 
104 
105 namespace stage {
106 namespace model {
107 
108  using lib::time::TimeValue;
109  using lib::time::TimeSpan;
110  using lib::time::Duration;
111  using lib::time::TimeVar;
112  using lib::time::Offset;
113  using lib::time::FSecs;
114  using lib::time::Time;
115 
116  using util::Rat;
117  using util::rational_cast;
118  using util::can_represent_Product;
119  using util::reQuant;
120 
121  using util::min;
122  using util::max;
123  using util::sgn;
124 
125  namespace {
126 
134  inline FSecs
135  _FSecs (TimeValue const& timeVal)
136  {
137  return FSecs{_raw(timeVal), TimeValue::SCALE};
138  }
139 
146  inline bool
147  isMicroGridAligned (FSecs duration)
148  {
149  return 0 == Time::SCALE % duration.denominator();
150  }
151 
152  inline double
153  approx (Rat r)
154  {
155  return util::rational_cast<double> (r);
156  }
157 
159 
163  inline TimeVar
164  operator+ (Time const& tval, TimeVar const& tvar)
165  {
166  return TimeVar(tval) += tvar;
167  }
168  inline TimeVar
169  operator- (Time const& tval, TimeVar const& tvar)
170  {
171  return TimeVar(tval) -= tvar;
172  }
173  }
174 
175 
178 
179  namespace {// initial values (rather arbitrary)
180  const FSecs DEFAULT_CANVAS{23};
181  const Rat DEFAULT_METRIC{25};
182  const uint MAX_PX_WIDTH{100000};
183  const FSecs MAX_TIMESPAN{_FSecs(Duration::MAX)};
184  const FSecs MICRO_TICK{1_r/Time::SCALE};
185 
190  const int64_t LIM_HAZARD {int64_t{1} << 40 };
191  const int64_t HAZARD_DEGREE{util::ilog2(LIM_HAZARD)};
192  const int64_t MAXDIM {util::ilog2 (std::numeric_limits<int64_t>::max())};
193 
194  inline int
195  toxicDegree (Rat poison, const int64_t THRESHOLD =HAZARD_DEGREE)
196  {
197  int64_t magNum = util::ilog2(abs(poison.numerator()));
198  int64_t magDen = util::ilog2(abs(poison.denominator()));
199  int64_t degree = max (magNum, magDen);
200  return max (0, degree - THRESHOLD);
201  }
202  }
203 
204 
205 
206 
207 
208  /******************************************************/
224  {
225  TimeVar startAll_, afterAll_,
226  startWin_, afterWin_;
227  Rat px_per_sec_;
228 
229  std::function<void()> changeSignal_{};
230 
231  public:
232  ZoomWindow (uint pxWidth, TimeSpan timeline =TimeSpan{Time::ZERO, DEFAULT_CANVAS})
233  : startAll_{ensureNonEmpty(timeline).start()}
234  , afterAll_{ensureNonEmpty(timeline).end()}
235  , startWin_{startAll_}
236  , afterWin_{afterAll_}
237  , px_per_sec_{establishMetric (pxWidth, startWin_, afterWin_)}
238  {
239  pxWidth = this->pxWidth();
240  ASSERT (0 < pxWidth);
241  conformWindowToMetricLimits (pxWidth);
242  ensureInvariants(pxWidth);
243  }
244 
245  ZoomWindow (TimeSpan timeline =TimeSpan{Time::ZERO, DEFAULT_CANVAS})
246  : ZoomWindow{0, timeline} //see establishMetric()
247  { }
248 
249  TimeSpan
250  overallSpan() const
251  {
252  return TimeSpan{startAll_, afterAll_};
253  }
254 
255  TimeSpan
256  visible() const
257  {
258  return TimeSpan{startWin_, afterWin_};
259  }
260 
261  Rat
262  px_per_sec() const
263  {
264  return px_per_sec_;
265  }
266 
267  uint
268  pxWidth() const
269  {
270  REQUIRE (startWin_ < afterWin_);
271  return calcPixelsForDurationAtScale (px_per_sec(), afterWin_-startWin_);
272  }
273 
274 
275 
276  /* === Mutators === */
277 
286  void
287  calibrateExtension (uint pxWidth)
288  {
289  adaptWindowToPixels (pxWidth);
290  fireChangeNotification();
291  }
292 
299  void
300  setMetric (Rat px_per_sec)
301  {
302  mutateScale (px_per_sec);
303  fireChangeNotification();
304  }
305 
314  void
315  nudgeMetric (int steps)
316  {
317  setMetric(
318  steps > 0 ? Rat{px_per_sec_.numerator() << steps
319  ,px_per_sec_.denominator()}
320  : Rat{px_per_sec_.numerator()
321  ,px_per_sec_.denominator() << -steps});
322  }
323 
334  void
335  setRanges (TimeSpan overall, TimeSpan visible)
336  {
337  mutateRanges (overall, visible);
338  fireChangeNotification();
339  }
340 
348  void
350  {
351  mutateCanvas (range);
352  fireChangeNotification();
353  }
354 
355  void
356  setOverallStart (TimeValue start)
357  {
358  mutateCanvas (TimeSpan{start, Duration(afterAll_-startAll_)});
359  fireChangeNotification();
360  }
361 
362  void
363  setOverallDuration (Duration duration)
364  {
365  mutateCanvas (TimeSpan{startAll_, duration});
366  fireChangeNotification();
367  }
368 
369  void
370  setVisibleStart (TimeValue start)
371  {
372  mutateWindow (TimeSpan{start, Duration(afterWin_-startWin_)});
373  fireChangeNotification();
374  }
375 
381  void
383  {
384  mutateWindow (newWindow);
385  fireChangeNotification();
386  }
387 
395  void
397  {
398  // Formulation: Assuming the current window was generated from TimeSpan
399  // by applying an affine-linear transformation f = a·x + b
400  FSecs tarDur = _FSecs(target.end()-target.start());
401  Rat a = FSecs{afterWin_-startWin_};
402  Rat b = FSecs{startWin_}*_FSecs(target.end()) - FSecs{afterWin_}*_FSecs((target.start()));
403  a /= tarDur;
404  b /= tarDur;
405  Time startNew {a * FSecs{startWin_} + b};
406  Time afterNew {a * FSecs{afterWin_} + b};
407 
408  mutateWindow(TimeSpan{startNew, afterNew});
409  fireChangeNotification();
410  }
411 
419  void
421  {
422  mutateDuration (_FSecs(duration));
423  fireChangeNotification();
424  }
425 
427  void
429  {
430  mutateWindow (TimeSpan{startWin_+offset, Duration{afterWin_-startWin_}});
431  fireChangeNotification();
432  }
433 
435  void
436  nudgeVisiblePos (int64_t steps)
437  {
438  FSecs dur{afterWin_-startWin_};
439  int64_t limPages = 2 * rational_cast<int64_t> (MAX_TIMESPAN/dur);
440  steps = util::limited(-limPages, steps, +limPages);
441  FSecs scroll = steps * dur/2; // move by half window sized steps
442  if (abs(scroll) < MICRO_TICK) scroll = sgn(steps) * MICRO_TICK;
443  setVisibleRange (TimeSpan{Time{startWin_+Offset(scroll)}, dur});
444  }
445 
450  void
451  setVisiblePos (Time posToShow)
452  {
453  FSecs canvasOffset{posToShow - startAll_};
454  anchorWindowAtPosition (canvasOffset);
455  fireChangeNotification();
456  }
457 
459  void
460  setVisiblePos (Rat percentage)
461  {
462  FSecs canvasDuration{afterAll_-startAll_};
463  anchorWindowAtPosition (scaleSafe (canvasDuration, percentage));
464  fireChangeNotification();
465  }
466 
467  void
468  setVisiblePos (double percentage)
469  { // use some arbitrary yet significantly large work scale
470  int64_t scale = max (_raw(afterAll_-startAll_), MAX_PX_WIDTH);
471  Rat factor{int64_t(scale*percentage), scale};
472  setVisiblePos (factor);
473  }
474 
475  void
476  navHistory()
477  {
478  UNIMPLEMENTED ("navigate Zoom History");
479  }
480 
481 
483  template<class FUN>
484  void
485  attachChangeNotification (FUN&& trigger)
486  {
487  changeSignal_ = std::forward<FUN> (trigger);
488  }
489 
490  void
491  detachChangeNotification()
492  {
493  changeSignal_ = std::function<void()>();
494  }
495 
496 
497  private:
498  void
499  fireChangeNotification()
500  {
501  if (changeSignal_) changeSignal_();
502  }
503 
504 
505  /* === utility functions to handle dangerous fractional values === */
506 
528  static Rat
529  detox (Rat poison)
530  {
531  int toxicity = toxicDegree (poison);
532  return toxicity ? reQuant (poison, max (poison.denominator() >> toxicity, 64))
533  : poison;
534  }
535 
546  static FSecs
547  scaleSafe (FSecs duration, Rat factor)
548  {
549  if (util::can_represent_Product(duration, factor))
550  // just calculate ordinary numbers...
551  return duration * factor;
552  else
553  {
554  auto guess{approx(duration) * approx (factor)};
555  if (approx(MAX_TIMESPAN) < abs(guess))
556  return MAX_TIMESPAN * sgn(guess); // exceeds limits of time representation => cap the result
557  if (0 == guess)
558  return 0;
565  struct ReductionStrategy
566  {
567  int64_t f1;
568  int64_t u;
569  int64_t q;
570  int64_t f2;
571  bool invert;
572 
573  int64_t
574  determineLimit()
575  {
576  REQUIRE (u != 0);
577  return isFeasible()? u : 0;
578  }
579 
580  Rat
581  calculateResult()
582  {
583  REQUIRE (isFeasible());
584  f2 = reQuant (f2, q, u);
585  return invert? Rat{f2, f1}
586  : Rat{f1, f2};
587  }
588 
589  bool
590  isFeasible()
591  { // Note: factors are nonzero,
592  REQUIRE (u and q and f2);// otherwise exit after pre-check above
593  int dim_u = util::ilog2 (abs (u));
594  int dim_q = util::ilog2 (abs (q));
595  if (dim_q > dim_u) return true; // requantisation will reduce size and thus no danger
596  int dim_f = util::ilog2 (abs (f2));
597  int deltaQ = dim_u - dim_q; // how much q must be increased to match u
598  int headroom = MAXDIM - dim_f; // how much the counter factor f2 can be increased
599  return headroom > deltaQ;
600  }
601  };
602  using Cases = std::array<ReductionStrategy, 4>;
603  // There are four possible strategy configurations.
604  // One case stands out, insofar this factor is guaranteed to be present:
605  // because one of the numbers is a quantised Time, it has Time::SCALE as denominator,
606  // maybe after cancelling out some further common integral factors
607  auto [reduction,rem] = util::iDiv (Time::SCALE, duration.denominator());
608  if (rem != 0) reduction = 1; // when duration is not µ-Tick quantised
609  int64_t durationQuant = duration.denominator()*reduction;
610  int64_t durationTicks = duration.numerator()*reduction;
611 
612  //-f1--------------------+-u-------------------+-q---------------------+-f2--------------------+-invert--
613  Cases cases{{{durationTicks , durationQuant , factor.numerator() , factor.denominator() , false}
614  ,{factor.numerator() , factor.denominator(), duration.numerator() , duration.denominator(), false}
615  ,{duration.denominator(), duration.numerator(), factor.denominator() , factor.numerator() , true}
616  ,{factor.denominator() , factor.numerator() , duration.denominator(), duration.numerator() , true}
617  }};
618  // However, some of the other cases may yield a larger denominator to be cancelled out,
619  // and thus lead to a smaller error margin. Attempt thus to find the best strategy...
620  ReductionStrategy* solution{nullptr};
621  int64_t maxLimit = 0;
622  for (auto& candidate: cases)
623  {
624  int64_t limit = candidate.determineLimit();
625  if (limit > maxLimit)
626  {
627  maxLimit = limit;
628  solution = &candidate;
629  }
630  }
631 
632  ASSERT (solution and maxLimit > 0);
633  return detox (solution->calculateResult());
634  }
635  }
636 
644  static FSecs
645  addSafe (FSecs t1, FSecs t2)
646  {
647  if (util::can_represent_Sum (t1,t2))
648  // directly calculate ordinary numbers...
649  return t1 + t2;
650  else
651  {
652  auto guess{approx(t1) + approx(t2)};
653  if (approx(MAX_TIMESPAN) < abs(guess))
654  return MAX_TIMESPAN * sgn(guess); // exceeds limits => cap the result
655 
656  // re-Quantise numbers to achieve a common denominator,
657  // thus avoiding to multiply numerators for normalisation
658  int64_t n1 = t1.numerator();
659  int64_t d1 = t1.denominator();
660  int s1 = sgn(n1)*sgn(d1);
661  n1 = abs(n1); d1 = abs(d1);
662  int64_t n2 = t2.numerator();
663  int64_t d2 = t2.denominator();
664  int s2 = sgn(n2)*sgn(d2);
665  n2 = abs(n2); d2 = abs(d2);
666  // quantise to smaller denominator to avoid increasing any numerator
667  int64_t u = d1<d2? d1:d2;
668  if (u < Time::SCALE)
669  // regarding precision, quantising to µ-grid is the better solution
670  u = Time::SCALE;
671  else //re-quantise to common denominator more fine-grained than µ-grid
672  if (s1*s2 > 0 // check numerators to detect danger of wrap-around
673  and (MAXDIM<=util::ilog2(n1) or MAXDIM<=util::ilog2(n2)))
674  u >>= 1; // danger zone! wrap-around imminent
675 
676  n1 = d1==u? n1 : reQuant (n1,d1, u);
677  n2 = d2==u? n2 : reQuant (n2,d2, u);
678  FSecs res{s1*n1 + s2*n2, u};
679 
680  auto f128 = [](Rat n){ return rational_cast<long double>(n); }; // can't use the guess from above,
681  ENSURE (abs (f128(res) - (f128(t1)+f128(t2))) < 1.0/u); // double precision is not sufficient
682  return detox (res);
683  }
684  }
685 
686 
687 
688 
689  /* === establish and maintain invariants === */
690  /*
691  * - oriented and non-empty windows
692  * - never alter given pxWidth
693  * - zoom metric factor < max zoom
694  * - visibleWindow ⊂ Canvas
695  */
696 
697  static TimeSpan
698  ensureNonEmpty (TimeSpan const& span)
699  {
700  return TimeSpan{span.start()
701  ,util::isnil(span.duration())? Duration{DEFAULT_CANVAS}
702  : span.duration()
703  }.conform();
704  }
705 
707  static void
708  ENSURE_matchesExpectedPixWidth (Rat zoomFactor, FSecs duration, uint pxWidth)
709  {
710  auto sizeAtRequestedScale = approx(zoomFactor) * approx(duration);
711  ENSURE (abs(pxWidth - sizeAtRequestedScale) <= 1
712  ,"ZoomWindow: established size or metric misses expectation "
713  "by more than 1px. %upx != %1.6f expected pixel."
714  , pxWidth, sizeAtRequestedScale);
715  }
716 
720  static int64_t
721  calcPixelsForDurationAtScale (Rat zoomFactor, FSecs duration)
722  {// break down the integer division into several steps...
723  auto zn = zoomFactor.numerator();
724  auto zd = zoomFactor.denominator();
725  auto dn = duration.numerator();
726  auto dd = duration.denominator();
727  auto [secs,r] = util::iDiv (dn, dd); // split duration in full seconds and rest
728  auto [px1,r1] = util::iDiv (secs*zn, zd); // calc pixels required for full seconds
729  auto [px2,r2] = util::iDiv (r*zn, dd*zd); // calc pixels required for rest duration
730  auto pxr = (r1*dd +r2) /(dd*zd); // and calculate integer div for combined remainders
731  ENSURE (0 <= px1 and 0 <= px2 and 0<= pxr);
732  return px1 + px2 + pxr;
733  }
734 
737  static FSecs
738  maxSaneWinExtension (uint pxWidth)
739  {
740  return min (FSecs{LIM_HAZARD * pxWidth, 1000}, MAX_TIMESPAN);
741  } // Note: denominator 1000 is additional safety margin
742  // wouldn't be necessary, but makes detox(largeTime) more precise
743 
751  Rat
752  optimiseMetric (uint pxWidth, FSecs dur, Rat rawMetric)
753  {
754  using util::ilog2;
755  REQUIRE (0 < pxWidth and 0 < dur and 0 < rawMetric);
756  REQUIRE (isMicroGridAligned (dur));
757  // circumvent numeric problems due to excessive large factors
758  int64_t magDen = ilog2(rawMetric.denominator());
759  int reduction = toxicDegree (rawMetric);
760  int quant = max (magDen-reduction, 16);
761  // re-quantise metric into power of two <= 2^40 (headroom 22 bit)
762  // Known to work always, since 9e-10 < metric < 2e+6
763  Rat adjMetric = util::reQuant (rawMetric, int64_t(1) << quant);
764 
765  // Correct that metric to reproduce expected pxWidth...
766  // Retain reduced denominator, but optimise the numerator
767  // pixel = trunc{ metric*duration }
768  double epsilon = std::numeric_limits<double>::epsilon()
769  , dn = dur.numerator()
770  , dd = dur.denominator()
771  , md = adjMetric.denominator()
772  , mn = (pxWidth+epsilon)*md*dd/dn;
773  // construct optimised zoom metric result
774  int64_t num = mn, den = adjMetric.denominator();
775  if (epsilon < mn - num)
776  {// optimisation found inter-grid result -- increase precision
777  int headroom = max (1, HAZARD_DEGREE - max (ilog2(num), ilog2(den)));
778  int64_t scale = int64_t(1) << headroom;
779  num = scale*mn; // quantise again with increased resolution
780  den = scale*den; // at least factor 2 to get some improvement
781  if (pxWidth > dn/dd*num/den) // If still some remaining error....
782  ++num; // round up to be sure to hit the next higher pixel count
783  }
784  adjMetric = Rat{num, den};
785  ENSURE (pxWidth == calcPixelsForDurationAtScale (adjMetric, dur));
786  double impliedDur = double(pxWidth)*den/num;
787  double relError = abs(dn/dd /impliedDur -1);
788  double quantErr = 1.0/(num-1);
789  ENSURE (quantErr > relError, "metric misses duration by "
790  "%3.2f%% > %3.2f%% (=relative quantisation error)"
791  ,100*relError, 100.0*quantErr);
792  return adjMetric;
793  }
794 
795 
796  static Rat
797  establishMetric (uint pxWidth, Time startWin, Time afterWin)
798  {
799  REQUIRE (startWin < afterWin);
800  FSecs dur = _FSecs(afterWin-startWin);
801  if (pxWidth == 0 or pxWidth > MAX_PX_WIDTH) // default to sane pixel width
802  pxWidth = max<uint> (1, rational_cast<uint> (DEFAULT_METRIC * dur));
803  Rat metric = Rat(pxWidth) / dur;
804  // rational arithmetic ensures we can always reproduce the pxWidth
805  ENSURE (pxWidth == calcPixelsForDurationAtScale (metric, dur));
806  ENSURE (0 < metric);
807  return metric;
808  }
809 
812  void
813  conformWindowToMetric (Rat changedMetric)
814  {
815  REQUIRE (changedMetric > 0);
816  REQUIRE (afterWin_> startWin_);
817  FSecs dur{afterWin_-startWin_};
818  uint pxWidth = calcPixelsForDurationAtScale (px_per_sec_, dur);
819  dur = Rat(pxWidth) / detox (changedMetric);
820  dur = min (dur, MAX_TIMESPAN);// limit maximum window size
821  dur = max (dur, MICRO_TICK); // prevent window going void
822  TimeVar timeDur{Duration{dur}};
823  // prefer bias towards increased window instead of increased metric
824  if (not isMicroGridAligned (dur))
825  timeDur = timeDur + TimeValue(1);
826  // resize window relative to anchor point
828  establishWindowDuration (Duration{timeDur});
829  // re-check metric to maintain precise pxWidth
830  px_per_sec_ = conformMetricToWindow (pxWidth);
831  ENSURE (_FSecs(afterWin_-startWin_) <= MAX_TIMESPAN);
832  ENSURE_matchesExpectedPixWidth (changedMetric, afterWin_-startWin_, pxWidth);
833  }
834 
835  Rat
836  conformMetricToWindow (uint pxWidth)
837  {
838  REQUIRE (pxWidth > 0);
839  REQUIRE (afterWin_> startWin_);
840  FSecs dur{afterWin_-startWin_};
841  Rat adjMetric = Rat(pxWidth) / dur;
842  if (not toxicDegree(adjMetric)
843  and pxWidth == calcPixelsForDurationAtScale (adjMetric, dur))
844  return adjMetric;
845  else
846  return optimiseMetric(pxWidth, dur, adjMetric);
847  }
848 
855  void
857  {
858  REQUIRE (pxWidth > 0);
859  FSecs dur{afterWin_-startWin_};
860  if (dur > maxSaneWinExtension (pxWidth))
861  {
862  dur = maxSaneWinExtension (pxWidth);
864  establishWindowDuration (dur);
865  }
866  }
867 
868  void
869  conformWindowToCanvas()
870  {
871  FSecs dur{afterWin_-startWin_};
872  REQUIRE (dur <= MAX_TIMESPAN);
873  startAll_ = max (startAll_, Time::MIN);
874  afterAll_ = min (afterAll_, Time::MAX);
875  if (dur <= _FSecs(afterAll_-startAll_))
876  {//possibly shift into current canvas
877  if (afterWin_ > afterAll_)
878  {
879  Offset shift{afterWin_ - afterAll_};
880  startWin_ -= shift;
881  afterWin_ -= shift;
882  }
883  else
884  if (startWin_ < startAll_)
885  {
886  Offset shift{startAll_ - startWin_};
887  startWin_ += shift;
888  afterWin_ += shift;
889  }
890  }
891  else
892  {//need to cap window to fit into canvas
893  startWin_ = startAll_;
894  afterWin_ = afterAll_;
895  }
896  ENSURE (startAll_ <= startWin_);
897  ENSURE (afterWin_ <= afterAll_);
898  ENSURE (Time::MIN <= startWin_);
899  ENSURE (afterWin_ <= Time::MAX);
900  }
901 
902  void
903  conformToBounds (Rat changedMetric)
904  {
905  if (changedMetric > ZOOM_MAX_RESOLUTION)
906  {
907  changedMetric = ZOOM_MAX_RESOLUTION;
908  conformWindowToMetric (changedMetric);
909  }
910  startAll_ = min (startAll_, startWin_);
911  afterAll_ = max (afterAll_, afterWin_);
912  ENSURE (Time::MIN <= startWin_);
913  ENSURE (afterWin_ <= Time::MAX);
914  ENSURE (startAll_ <= startWin_);
915  ENSURE (afterWin_ <= afterAll_);
916  ENSURE (px_per_sec_ <= ZOOM_MAX_RESOLUTION);
917  ENSURE (px_per_sec_ <= changedMetric); // bias
918  }
919 
930  void
931  ensureInvariants(uint px =0)
932  {
933  if (px==0) px = pxWidth();
934  conformWindowToCanvas();
935  px_per_sec_ = conformMetricToWindow (px);
936  conformToBounds (px_per_sec_);
937  }
938 
939 
940 
941  /* === adjust and coordinate window parameters === */
942 
945  void
947  {
948  startAll_ = ensureNonEmpty(canvas).start();
949  afterAll_ = ensureNonEmpty(canvas).end();
951  }
952 
956  void
958  {
959  uint px{pxWidth()};
960  startWin_ = ensureNonEmpty(window).start();
961  afterWin_ = ensureNonEmpty(window).end();
963  startAll_ = min (startAll_, startWin_);
964  afterAll_ = max (afterAll_, afterWin_);
965  ensureInvariants (px);
966  }
967 
970  void
971  mutateRanges (TimeSpan canvas, TimeSpan window)
972  {
973  uint px{pxWidth()};
974  startAll_ = ensureNonEmpty(canvas).start();
975  afterAll_ = ensureNonEmpty(canvas).end();
976  startWin_ = ensureNonEmpty(window).start();
977  afterWin_ = ensureNonEmpty(window).end();
979  ensureInvariants (px);
980  }
981 
985  void
986  mutateScale (Rat changedMetric)
987  {
988  uint px{pxWidth()};
989  changedMetric = max (changedMetric, px / maxSaneWinExtension(px));
990  changedMetric = min (detox(changedMetric), ZOOM_MAX_RESOLUTION);
991  if (changedMetric == px_per_sec_) return;
992  conformWindowToMetric (changedMetric);
993  ensureInvariants (px);
994  }
995 
998  void
999  mutateDuration (FSecs duration, uint px =0)
1000  {
1001  if (px==0)
1002  px = pxWidth();
1003  if (duration <= 0)
1004  duration = DEFAULT_CANVAS;
1005  else if (duration > maxSaneWinExtension (px))
1006  duration = maxSaneWinExtension (px);
1007  placeWindowRelativeToAnchor (duration);
1008  establishWindowDuration (duration);
1009  px_per_sec_ = conformMetricToWindow (px);
1010  ensureInvariants (px);
1011  }
1012 
1015  void
1016  adaptWindowToPixels (uint pxWidth)
1017  {
1018  pxWidth = util::limited (1u, pxWidth, MAX_PX_WIDTH);
1019  FSecs adaptedWindow{Rat{pxWidth} / px_per_sec_};
1020  adaptedWindow = max (adaptedWindow, MICRO_TICK); // prevent void window
1021  adaptedWindow = min (adaptedWindow, maxSaneWinExtension (pxWidth));
1022  establishWindowDuration (adaptedWindow);
1023  ensureInvariants (pxWidth);
1024  }
1025 
1031  void
1032  anchorWindowAtPosition (FSecs canvasOffset)
1033  {
1034  REQUIRE (afterWin_ > startWin_);
1035  REQUIRE (afterAll_ > startAll_);
1036  uint px{pxWidth()};
1037  FSecs duration{afterWin_-startWin_};
1038  Rat posFactor = canvasOffset / FSecs{afterAll_-startAll_};
1039  posFactor = parabolicAnchorRule (posFactor); // also limited 0...1
1040  FSecs partBeforeAnchor = scaleSafe (duration, posFactor);
1041  startWin_ = startAll_ + Offset{addSafe (canvasOffset, -partBeforeAnchor)};
1042  establishWindowDuration (duration);
1043  startAll_ = min (startAll_, startWin_);
1044  afterAll_ = max (afterAll_, afterWin_);
1045  ensureInvariants (px);
1046  }
1047 
1048 
1052  void
1054  {
1055  FSecs partBeforeAnchor = scaleSafe(duration, relativeAnchor());
1056  startWin_ = Time{anchorPoint()} - Time{partBeforeAnchor};
1057  }
1058 
1059  void
1060  establishWindowDuration (Duration duration)
1061  {
1062  if (startWin_<= Time::MAX - duration)
1063  afterWin_ = startWin_ + duration;
1064  else
1065  {
1066  startWin_ = Time::MAX - duration;
1067  afterWin_ = Time::MAX;
1068  }
1069  }
1070 
1071 
1072 
1083  FSecs
1084  anchorPoint() const
1085  {
1086  return startWin_ + Offset{scaleSafe (afterWin_-startWin_, relativeAnchor())};
1087  }
1088 
1096  Rat
1098  {
1099  // the visible window itself has to fit in, which reduces the action range
1100  FSecs possibleRange = (afterAll_-startAll_) - (afterWin_-startWin_);
1101  if (possibleRange <= 0) // if there is no room for scrolling...
1102  return 1_r/2; // then anchor zooming in the middle
1103 
1104  // use a 3rd degree parabola to favour positions in the middle
1105  Rat posFactor = FSecs{startWin_-startAll_} / possibleRange;
1106  return parabolicAnchorRule (posFactor);
1107  }
1108 
1118  static Rat
1119  parabolicAnchorRule (Rat posFactor)
1120  {
1121  posFactor = util::limited (0, posFactor, 1);
1122  if (toxicDegree(posFactor, 20)) // prevent integer wrap
1123  posFactor = util::reQuant(posFactor, 1 << 20);
1124  posFactor = (2*posFactor - 1); // -1 ... +1
1125  posFactor = posFactor*posFactor*posFactor; // -1 ... +1 but accelerating towards boundaries
1126  posFactor = (posFactor + 1) / 2; // 0 ... 1
1127  posFactor = util::limited (0, posFactor, 1);
1128  return detox (posFactor);
1129  }
1130  };
1131 
1132 
1133 
1134 }} // namespace stage::model
1135 #endif /*STAGE_MODEL_ZOOM_WINDOW_H*/
void setVisibleRange(TimeSpan newWindow)
explicitly set the visible window, possibly expanding the canvas to fit.
a mutable time value, behaving like a plain number, allowing copy and re-accessing ...
Definition: timevalue.hpp:241
void mutateScale(Rat changedMetric)
static const Duration MAX
maximum possible temporal extension
Definition: timevalue.hpp:516
void setVisibleDuration(Duration duration)
explicitly set the duration of the visible window range, working around the relative anchor point; po...
void ensureInvariants(uint px=0)
Procedure to (re)establish the invariants.
static int64_t calcPixelsForDurationAtScale(Rat zoomFactor, FSecs duration)
calculate rational_cast<uint> (zoomFactor * duration)
Rat reQuant(Rat src, int64_t u)
re-Quantise a rational number to a (typically smaller) denominator.
Definition: rational.hpp:159
void setVisiblePos(Time posToShow)
scroll the window to bring the denoted position in sight, retaining the current zoom factor...
void attachChangeNotification(FUN &&trigger)
Attach a λ or functor to be triggered on each actual change.
static Rat parabolicAnchorRule(Rat posFactor)
A counter movement rule to place an anchor point, based on a percentage factor.
void conformWindowToMetricLimits(uint pxWidth)
The zoom metric factor must not become "poisonous".
void setRanges(TimeSpan overall, TimeSpan visible)
Set both the overall canvas, as well as the visible part within that canvas.
Rational number support, based on boost::rational.
const int64_t LIM_HAZARD
Maximum quantiser to be handled in fractional arithmetics without hazard.
Rat relativeAnchor() const
define at which proportion to the visible window&#39;s duration the anchor should be placed ...
Any copy and copy construction prohibited.
Definition: nocopy.hpp:46
void placeWindowRelativeToAnchor(FSecs duration)
static const gavl_time_t SCALE
Number of micro ticks (µs) per second as basic time scale.
Definition: timevalue.hpp:176
void mutateCanvas(TimeSpan canvas)
void expandVisibleRange(TimeSpan target)
the »reverse zoom operation«: zoom out such as to bring the current window at the designated time spa...
void mutateRanges(TimeSpan canvas, TimeSpan window)
static Rat detox(Rat poison)
Check and possibly sanitise a rational number to avoid internal numeric overflow. ...
Lumiera&#39;s internal time value datatype.
Definition: timevalue.hpp:308
static FSecs addSafe(FSecs t1, FSecs t2)
Calculate sum (or difference) of possibly large time durations, avoiding integer wrap-around.
static void ENSURE_matchesExpectedPixWidth(Rat zoomFactor, FSecs duration, uint pxWidth)
Assertion helper: resulting pxWidth matches expectations.
TimeVar operator+(Time const &tval, TimeVar const &tvar)
Mix-Ins to allow or prohibit various degrees of copying and cloning.
void setVisiblePos(Rat percentage)
scroll to reveal position designated relative to overall canvas
A component to ensure uniform handling of zoom scale and visible interval on the timeline.
void nudgeVisiblePos(int64_t steps)
scroll by increments of half window size, possibly expanding.
Rat optimiseMetric(uint pxWidth, FSecs dur, Rat rawMetric)
Reform the effective metric in all dangerous corner cases.
Lumiera GTK UI implementation root.
Definition: guifacade.cpp:46
Tiny helper functions and shortcuts to be used everywhere Consider this header to be effectively incl...
void nudgeMetric(int steps)
scale up or down on a 2-logarithmic scale.
static FSecs scaleSafe(FSecs duration, Rat factor)
Scale a possibly large time duration by a rational factor, while attempting to avoid integer wrap-aro...
void conformWindowToMetric(Rat changedMetric)
this is the centrepiece of the whole zoom metric logic...
boost::rational< int64_t > FSecs
rational representation of fractional seconds
Definition: timevalue.hpp:229
void mutateWindow(TimeSpan window)
Lumiera error handling (C++ interface).
void adaptWindowToPixels(uint pxWidth)
const Rat ZOOM_MAX_RESOLUTION
the deepest zoom is to use 2px per micro-tick
void setOverallRange(TimeSpan range)
redefine the overall canvas range.
static FSecs maxSaneWinExtension(uint pxWidth)
window size beyond that limit would lead to numerically dangerous zoom factors (pixel/duration) ...
Offset measures a distance in time.
Definition: timevalue.hpp:367
void offsetVisiblePos(Offset offset)
scroll by arbitrary offset, possibly expanding canvas.
Duration is the internal Lumiera time metric.
Definition: timevalue.hpp:477
FSecs anchorPoint() const
The anchor point or centre for zooming operations applied to the visible window.
void mutateDuration(FSecs duration, uint px=0)
void setMetric(Rat px_per_sec)
explicitly set the zoom factor, defined as pixel per second
A time interval anchored at a specific point in time.
Definition: timevalue.hpp:582
void calibrateExtension(uint pxWidth)
Define the extension of the window in pixels.
a family of time value like entities and their relationships.
basic constant internal time value.
Definition: timevalue.hpp:142
static const Time MAX
Definition: timevalue.hpp:318
void anchorWindowAtPosition(FSecs canvasOffset)