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)
5  2018, Hermann Vosseler <Ichthyostega@web.de>
6 
7   **Lumiera** is free software; you can redistribute it and/or modify it
8   under the terms of the GNU General Public License as published by the
9   Free Software Foundation; either version 2 of the License, or (at your
10   option) any later version. See the file COPYING for further details.
11 
12 */
13 
14 
81 #ifndef STAGE_MODEL_ZOOM_WINDOW_H
82 #define STAGE_MODEL_ZOOM_WINDOW_H
83 
84 
85 #include "lib/error.hpp"
86 #include "lib/rational.hpp"
87 #include "lib/time/timevalue.hpp"
88 #include "lib/nocopy.hpp"
89 #include "lib/util.hpp"
90 
91 #include <limits>
92 #include <functional>
93 #include <array>
94 
95 
96 namespace stage {
97 namespace model {
98 
100  using lib::time::TimeSpan;
101  using lib::time::Duration;
102  using lib::time::TimeVar;
103  using lib::time::Offset;
104  using lib::time::FSecs;
105  using lib::time::Time;
106 
107  using util::Rat;
108  using util::rational_cast;
109  using util::can_represent_Product;
110  using util::reQuant;
111 
112  using util::min;
113  using util::max;
114  using util::sgn;
115 
116  namespace {
117 
125  inline FSecs
126  _FSecs (TimeValue const& timeVal)
127  {
128  return FSecs{_raw(timeVal), TimeValue::SCALE};
129  }
130 
137  inline bool
138  isMicroGridAligned (FSecs duration)
139  {
140  return 0 == Time::SCALE % duration.denominator();
141  }
142 
143  inline double
144  approx (Rat r)
145  {
146  return util::rational_cast<double> (r);
147  }
148 
150 
154  inline TimeVar
155  operator+ (Time const& tval, TimeVar const& tvar)
156  {
157  return TimeVar(tval) += tvar;
158  }
159  inline TimeVar
160  operator- (Time const& tval, TimeVar const& tvar)
161  {
162  return TimeVar(tval) -= tvar;
163  }
164  }
165 
166 
169 
170  namespace {// initial values (rather arbitrary)
171  const FSecs DEFAULT_CANVAS{23};
172  const Rat DEFAULT_METRIC{25};
173  const uint MAX_PX_WIDTH{100000};
174  const FSecs MAX_TIMESPAN{_FSecs(Duration::MAX)};
175  const FSecs MICRO_TICK{1_r/Time::SCALE};
176 
181  const int64_t LIM_HAZARD {int64_t{1} << 40 };
182  const int64_t HAZARD_DEGREE{util::ilog2(LIM_HAZARD)};
183  const int64_t MAXDIM {util::ilog2 (std::numeric_limits<int64_t>::max())};
184 
185  inline int
186  toxicDegree (Rat poison, const int64_t THRESHOLD =HAZARD_DEGREE)
187  {
188  int64_t magNum = util::ilog2(abs(poison.numerator()));
189  int64_t magDen = util::ilog2(abs(poison.denominator()));
190  int64_t degree = max (magNum, magDen);
191  return max (0, degree - THRESHOLD);
192  }
193  }
194 
195 
196 
197 
198 
199  /******************************************************/
215  {
216  TimeVar startAll_, afterAll_,
217  startWin_, afterWin_;
218  Rat px_per_sec_;
219 
220  std::function<void()> changeSignal_{};
221 
222  public:
223  ZoomWindow (uint pxWidth, TimeSpan timeline =TimeSpan{Time::ZERO, DEFAULT_CANVAS})
224  : startAll_{ensureNonEmpty(timeline).start()}
225  , afterAll_{ensureNonEmpty(timeline).end()}
226  , startWin_{startAll_}
227  , afterWin_{afterAll_}
228  , px_per_sec_{establishMetric (pxWidth, startWin_, afterWin_)}
229  {
230  pxWidth = this->pxWidth();
231  ASSERT (0 < pxWidth);
232  conformWindowToMetricLimits (pxWidth);
233  ensureInvariants(pxWidth);
234  }
235 
236  ZoomWindow (TimeSpan timeline =TimeSpan{Time::ZERO, DEFAULT_CANVAS})
237  : ZoomWindow{0, timeline} //see establishMetric()
238  { }
239 
240  TimeSpan
241  overallSpan() const
242  {
243  return TimeSpan{startAll_, afterAll_};
244  }
245 
246  TimeSpan
247  visible() const
248  {
249  return TimeSpan{startWin_, afterWin_};
250  }
251 
252  Rat
253  px_per_sec() const
254  {
255  return px_per_sec_;
256  }
257 
258  uint
259  pxWidth() const
260  {
261  REQUIRE (startWin_ < afterWin_);
262  return calcPixelsForDurationAtScale (px_per_sec(), afterWin_-startWin_);
263  }
264 
265 
266 
267  /* === Mutators === */
268 
277  void
278  calibrateExtension (uint pxWidth)
279  {
280  adaptWindowToPixels (pxWidth);
281  fireChangeNotification();
282  }
283 
290  void
291  setMetric (Rat px_per_sec)
292  {
293  mutateScale (px_per_sec);
294  fireChangeNotification();
295  }
296 
305  void
306  nudgeMetric (int steps)
307  {
308  setMetric(
309  steps > 0 ? Rat{px_per_sec_.numerator() << steps
310  ,px_per_sec_.denominator()}
311  : Rat{px_per_sec_.numerator()
312  ,px_per_sec_.denominator() << -steps});
313  }
314 
325  void
326  setRanges (TimeSpan overall, TimeSpan visible)
327  {
328  mutateRanges (overall, visible);
329  fireChangeNotification();
330  }
331 
339  void
341  {
342  mutateCanvas (range);
343  fireChangeNotification();
344  }
345 
346  void
347  setOverallStart (TimeValue start)
348  {
349  mutateCanvas (TimeSpan{start, Duration(afterAll_-startAll_)});
350  fireChangeNotification();
351  }
352 
353  void
354  setOverallDuration (Duration duration)
355  {
356  mutateCanvas (TimeSpan{startAll_, duration});
357  fireChangeNotification();
358  }
359 
360  void
361  setVisibleStart (TimeValue start)
362  {
363  mutateWindow (TimeSpan{start, Duration(afterWin_-startWin_)});
364  fireChangeNotification();
365  }
366 
372  void
374  {
375  mutateWindow (newWindow);
376  fireChangeNotification();
377  }
378 
386  void
388  {
389  // Formulation: Assuming the current window was generated from TimeSpan
390  // by applying an affine-linear transformation f = a·x + b
391  FSecs tarDur = _FSecs(target.end()-target.start());
392  Rat a = FSecs{afterWin_-startWin_};
393  Rat b = FSecs{startWin_}*_FSecs(target.end()) - FSecs{afterWin_}*_FSecs((target.start()));
394  a /= tarDur;
395  b /= tarDur;
396  Time startNew {a * FSecs{startWin_} + b};
397  Time afterNew {a * FSecs{afterWin_} + b};
398 
399  mutateWindow(TimeSpan{startNew, afterNew});
400  fireChangeNotification();
401  }
402 
410  void
412  {
413  mutateDuration (_FSecs(duration));
414  fireChangeNotification();
415  }
416 
418  void
420  {
421  mutateWindow (TimeSpan{startWin_+offset, Duration{afterWin_-startWin_}});
422  fireChangeNotification();
423  }
424 
426  void
427  nudgeVisiblePos (int64_t steps)
428  {
429  FSecs dur{afterWin_-startWin_};
430  int64_t limPages = 2 * rational_cast<int64_t> (MAX_TIMESPAN/dur);
431  steps = util::limited(-limPages, steps, +limPages);
432  FSecs scroll = steps * dur/2; // move by half window sized steps
433  if (abs(scroll) < MICRO_TICK) scroll = sgn(steps) * MICRO_TICK;
434  setVisibleRange (TimeSpan{Time{startWin_+Offset(scroll)}, dur});
435  }
436 
441  void
442  setVisiblePos (Time posToShow)
443  {
444  FSecs canvasOffset{posToShow - startAll_};
445  anchorWindowAtPosition (canvasOffset);
446  fireChangeNotification();
447  }
448 
450  void
451  setVisiblePos (Rat percentage)
452  {
453  FSecs canvasDuration{afterAll_-startAll_};
454  anchorWindowAtPosition (scaleSafe (canvasDuration, percentage));
455  fireChangeNotification();
456  }
457 
458  void
459  setVisiblePos (double percentage)
460  { // use some arbitrary yet significantly large work scale
461  int64_t scale = max (_raw(afterAll_-startAll_), MAX_PX_WIDTH);
462  Rat factor{int64_t(scale*percentage), scale};
463  setVisiblePos (factor);
464  }
465 
466  void
467  navHistory()
468  {
469  UNIMPLEMENTED ("navigate Zoom History");
470  }
471 
472 
474  template<class FUN>
475  void
476  attachChangeNotification (FUN&& trigger)
477  {
478  changeSignal_ = std::forward<FUN> (trigger);
479  }
480 
481  void
482  detachChangeNotification()
483  {
484  changeSignal_ = std::function<void()>();
485  }
486 
487 
488  private:
489  void
490  fireChangeNotification()
491  {
492  if (changeSignal_) changeSignal_();
493  }
494 
495 
496  /* === utility functions to handle dangerous fractional values === */
497 
519  static Rat
520  detox (Rat poison)
521  {
522  int toxicity = toxicDegree (poison);
523  return toxicity ? reQuant (poison, max (poison.denominator() >> toxicity, 64))
524  : poison;
525  }
526 
537  static FSecs
538  scaleSafe (FSecs duration, Rat factor)
539  {
540  if (util::can_represent_Product(duration, factor))
541  // just calculate ordinary numbers...
542  return duration * factor;
543  else
544  {
545  auto guess{approx(duration) * approx (factor)};
546  if (approx(MAX_TIMESPAN) < abs(guess))
547  return MAX_TIMESPAN * sgn(guess); // exceeds limits of time representation => cap the result
548  if (0 == guess)
549  return 0;
556  struct ReductionStrategy
557  {
558  int64_t f1;
559  int64_t u;
560  int64_t q;
561  int64_t f2;
562  bool invert;
563 
564  int64_t
565  determineLimit()
566  {
567  REQUIRE (u != 0);
568  return isFeasible()? u : 0;
569  }
570 
571  Rat
572  calculateResult()
573  {
574  REQUIRE (isFeasible());
575  f2 = reQuant (f2, q, u);
576  return invert? Rat{f2, f1}
577  : Rat{f1, f2};
578  }
579 
580  bool
581  isFeasible()
582  { // Note: factors are nonzero,
583  REQUIRE (u and q and f2);// otherwise exit after pre-check above
584  int dim_u = util::ilog2 (abs (u));
585  int dim_q = util::ilog2 (abs (q));
586  if (dim_q > dim_u) return true; // requantisation will reduce size and thus no danger
587  int dim_f = util::ilog2 (abs (f2));
588  int deltaQ = dim_u - dim_q; // how much q must be increased to match u
589  int headroom = MAXDIM - dim_f; // how much the counter factor f2 can be increased
590  return headroom > deltaQ;
591  }
592  };
593  using Cases = std::array<ReductionStrategy, 4>;
594  // There are four possible strategy configurations.
595  // One case stands out, insofar this factor is guaranteed to be present:
596  // because one of the numbers is a quantised Time, it has Time::SCALE as denominator,
597  // maybe after cancelling out some further common integral factors
598  auto [reduction,rem] = util::iDiv (Time::SCALE, duration.denominator());
599  if (rem != 0) reduction = 1; // when duration is not µ-Tick quantised
600  int64_t durationQuant = duration.denominator()*reduction;
601  int64_t durationTicks = duration.numerator()*reduction;
602 
603  //-f1--------------------+-u-------------------+-q---------------------+-f2--------------------+-invert--
604  Cases cases{{{durationTicks , durationQuant , factor.numerator() , factor.denominator() , false}
605  ,{factor.numerator() , factor.denominator(), duration.numerator() , duration.denominator(), false}
606  ,{duration.denominator(), duration.numerator(), factor.denominator() , factor.numerator() , true}
607  ,{factor.denominator() , factor.numerator() , duration.denominator(), duration.numerator() , true}
608  }};
609  // However, some of the other cases may yield a larger denominator to be cancelled out,
610  // and thus lead to a smaller error margin. Attempt thus to find the best strategy...
611  ReductionStrategy* solution{nullptr};
612  int64_t maxLimit = 0;
613  for (auto& candidate: cases)
614  {
615  int64_t limit = candidate.determineLimit();
616  if (limit > maxLimit)
617  {
618  maxLimit = limit;
619  solution = &candidate;
620  }
621  }
622 
623  ASSERT (solution and maxLimit > 0);
624  return detox (solution->calculateResult());
625  }
626  }
627 
635  static FSecs
636  addSafe (FSecs t1, FSecs t2)
637  {
638  if (util::can_represent_Sum (t1,t2))
639  // directly calculate ordinary numbers...
640  return t1 + t2;
641  else
642  {
643  auto guess{approx(t1) + approx(t2)};
644  if (approx(MAX_TIMESPAN) < abs(guess))
645  return MAX_TIMESPAN * sgn(guess); // exceeds limits => cap the result
646 
647  // re-Quantise numbers to achieve a common denominator,
648  // thus avoiding to multiply numerators for normalisation
649  int64_t n1 = t1.numerator();
650  int64_t d1 = t1.denominator();
651  int s1 = sgn(n1)*sgn(d1);
652  n1 = abs(n1); d1 = abs(d1);
653  int64_t n2 = t2.numerator();
654  int64_t d2 = t2.denominator();
655  int s2 = sgn(n2)*sgn(d2);
656  n2 = abs(n2); d2 = abs(d2);
657  // quantise to smaller denominator to avoid increasing any numerator
658  int64_t u = d1<d2? d1:d2;
659  if (u < Time::SCALE)
660  // regarding precision, quantising to µ-grid is the better solution
661  u = Time::SCALE;
662  else //re-quantise to common denominator more fine-grained than µ-grid
663  if (s1*s2 > 0 // check numerators to detect danger of wrap-around
664  and (MAXDIM<=util::ilog2(n1) or MAXDIM<=util::ilog2(n2)))
665  u >>= 1; // danger zone! wrap-around imminent
666 
667  n1 = d1==u? n1 : reQuant (n1,d1, u);
668  n2 = d2==u? n2 : reQuant (n2,d2, u);
669  FSecs res{s1*n1 + s2*n2, u};
670 
671  auto f128 = [](Rat n){ return rational_cast<long double>(n); }; // can't use the guess from above,
672  ENSURE (abs (f128(res) - (f128(t1)+f128(t2))) < 1.0/u); // double precision is not sufficient
673  return detox (res);
674  }
675  }
676 
677 
678 
679 
680  /* === establish and maintain invariants === */
681  /*
682  * - oriented and non-empty windows
683  * - never alter given pxWidth
684  * - zoom metric factor < max zoom
685  * - visibleWindow ⊂ Canvas
686  */
687 
688  static TimeSpan
689  ensureNonEmpty (TimeSpan const& span)
690  {
691  return TimeSpan{span.start()
692  ,util::isnil(span.duration())? Duration{DEFAULT_CANVAS}
693  : span.duration()
694  }.conform();
695  }
696 
698  static void
699  ENSURE_matchesExpectedPixWidth (Rat zoomFactor, FSecs duration, uint pxWidth)
700  {
701  auto sizeAtRequestedScale = approx(zoomFactor) * approx(duration);
702  ENSURE (abs(pxWidth - sizeAtRequestedScale) <= 1
703  ,"ZoomWindow: established size or metric misses expectation "
704  "by more than 1px. %upx != %1.6f expected pixel."
705  , pxWidth, sizeAtRequestedScale);
706  }
707 
711  static int64_t
712  calcPixelsForDurationAtScale (Rat zoomFactor, FSecs duration)
713  {// break down the integer division into several steps...
714  auto zn = zoomFactor.numerator();
715  auto zd = zoomFactor.denominator();
716  auto dn = duration.numerator();
717  auto dd = duration.denominator();
718  auto [secs,r] = util::iDiv (dn, dd); // split duration in full seconds and rest
719  auto [px1,r1] = util::iDiv (secs*zn, zd); // calc pixels required for full seconds
720  auto [px2,r2] = util::iDiv (r*zn, dd*zd); // calc pixels required for rest duration
721  auto pxr = (r1*dd +r2) /(dd*zd); // and calculate integer div for combined remainders
722  ENSURE (0 <= px1 and 0 <= px2 and 0<= pxr);
723  return px1 + px2 + pxr;
724  }
725 
728  static FSecs
729  maxSaneWinExtension (uint pxWidth)
730  {
731  return min (FSecs{LIM_HAZARD * pxWidth, 1000}, MAX_TIMESPAN);
732  } // Note: denominator 1000 is additional safety margin
733  // wouldn't be necessary, but makes detox(largeTime) more precise
734 
742  Rat
743  optimiseMetric (uint pxWidth, FSecs dur, Rat rawMetric)
744  {
745  using util::ilog2;
746  REQUIRE (0 < pxWidth and 0 < dur and 0 < rawMetric);
747  REQUIRE (isMicroGridAligned (dur));
748  // circumvent numeric problems due to excessive large factors
749  int64_t magDen = ilog2(rawMetric.denominator());
750  int reduction = toxicDegree (rawMetric);
751  int quant = max (magDen-reduction, 16);
752  // re-quantise metric into power of two <= 2^40 (headroom 22 bit)
753  // Known to work always, since 9e-10 < metric < 2e+6
754  Rat adjMetric = util::reQuant (rawMetric, int64_t(1) << quant);
755 
756  // Correct that metric to reproduce expected pxWidth...
757  // Retain reduced denominator, but optimise the numerator
758  // pixel = trunc{ metric*duration }
759  double epsilon = std::numeric_limits<double>::epsilon()
760  , dn = dur.numerator()
761  , dd = dur.denominator()
762  , md = adjMetric.denominator()
763  , mn = (pxWidth+epsilon)*md*dd/dn;
764  // construct optimised zoom metric result
765  int64_t num = mn, den = adjMetric.denominator();
766  if (epsilon < mn - num)
767  {// optimisation found inter-grid result -- increase precision
768  int headroom = max (1, HAZARD_DEGREE - max (ilog2(num), ilog2(den)));
769  int64_t scale = int64_t(1) << headroom;
770  num = scale*mn; // quantise again with increased resolution
771  den = scale*den; // at least factor 2 to get some improvement
772  if (pxWidth > dn/dd*num/den) // If still some remaining error....
773  ++num; // round up to be sure to hit the next higher pixel count
774  }
775  adjMetric = Rat{num, den};
776  ENSURE (pxWidth == calcPixelsForDurationAtScale (adjMetric, dur));
777  double impliedDur = double(pxWidth)*den/num;
778  double relError = abs(dn/dd /impliedDur -1);
779  double quantErr = 1.0/(num-1);
780  ENSURE (quantErr > relError, "metric misses duration by "
781  "%3.2f%% > %3.2f%% (=relative quantisation error)"
782  ,100*relError, 100.0*quantErr);
783  return adjMetric;
784  }
785 
786 
787  static Rat
788  establishMetric (uint pxWidth, Time startWin, Time afterWin)
789  {
790  REQUIRE (startWin < afterWin);
791  FSecs dur = _FSecs(afterWin-startWin);
792  if (pxWidth == 0 or pxWidth > MAX_PX_WIDTH) // default to sane pixel width
793  pxWidth = max<uint> (1, rational_cast<uint> (DEFAULT_METRIC * dur));
794  Rat metric = Rat(pxWidth) / dur;
795  // rational arithmetic ensures we can always reproduce the pxWidth
796  ENSURE (pxWidth == calcPixelsForDurationAtScale (metric, dur));
797  ENSURE (0 < metric);
798  return metric;
799  }
800 
803  void
804  conformWindowToMetric (Rat changedMetric)
805  {
806  REQUIRE (changedMetric > 0);
807  REQUIRE (afterWin_> startWin_);
808  FSecs dur{afterWin_-startWin_};
809  uint pxWidth = calcPixelsForDurationAtScale (px_per_sec_, dur);
810  dur = Rat(pxWidth) / detox (changedMetric);
811  dur = min (dur, MAX_TIMESPAN);// limit maximum window size
812  dur = max (dur, MICRO_TICK); // prevent window going void
813  TimeVar timeDur{Duration{dur}};
814  // prefer bias towards increased window instead of increased metric
815  if (not isMicroGridAligned (dur))
816  timeDur = timeDur + TimeValue(1);
817  // resize window relative to anchor point
819  establishWindowDuration (Duration{timeDur});
820  // re-check metric to maintain precise pxWidth
821  px_per_sec_ = conformMetricToWindow (pxWidth);
822  ENSURE (_FSecs(afterWin_-startWin_) <= MAX_TIMESPAN);
823  ENSURE_matchesExpectedPixWidth (changedMetric, afterWin_-startWin_, pxWidth);
824  }
825 
826  Rat
827  conformMetricToWindow (uint pxWidth)
828  {
829  REQUIRE (pxWidth > 0);
830  REQUIRE (afterWin_> startWin_);
831  FSecs dur{afterWin_-startWin_};
832  Rat adjMetric = Rat(pxWidth) / dur;
833  if (not toxicDegree(adjMetric)
834  and pxWidth == calcPixelsForDurationAtScale (adjMetric, dur))
835  return adjMetric;
836  else
837  return optimiseMetric(pxWidth, dur, adjMetric);
838  }
839 
846  void
848  {
849  REQUIRE (pxWidth > 0);
850  FSecs dur{afterWin_-startWin_};
851  if (dur > maxSaneWinExtension (pxWidth))
852  {
853  dur = maxSaneWinExtension (pxWidth);
855  establishWindowDuration (dur);
856  }
857  }
858 
859  void
860  conformWindowToCanvas()
861  {
862  FSecs dur{afterWin_-startWin_};
863  REQUIRE (dur <= MAX_TIMESPAN);
864  startAll_ = max (startAll_, Time::MIN);
865  afterAll_ = min (afterAll_, Time::MAX);
866  if (dur <= _FSecs(afterAll_-startAll_))
867  {//possibly shift into current canvas
868  if (afterWin_ > afterAll_)
869  {
870  Offset shift{afterWin_ - afterAll_};
871  startWin_ -= shift;
872  afterWin_ -= shift;
873  }
874  else
875  if (startWin_ < startAll_)
876  {
877  Offset shift{startAll_ - startWin_};
878  startWin_ += shift;
879  afterWin_ += shift;
880  }
881  }
882  else
883  {//need to cap window to fit into canvas
884  startWin_ = startAll_;
885  afterWin_ = afterAll_;
886  }
887  ENSURE (startAll_ <= startWin_);
888  ENSURE (afterWin_ <= afterAll_);
889  ENSURE (Time::MIN <= startWin_);
890  ENSURE (afterWin_ <= Time::MAX);
891  }
892 
893  void
894  conformToBounds (Rat changedMetric)
895  {
896  if (changedMetric > ZOOM_MAX_RESOLUTION)
897  {
898  changedMetric = ZOOM_MAX_RESOLUTION;
899  conformWindowToMetric (changedMetric);
900  }
901  startAll_ = min (startAll_, startWin_);
902  afterAll_ = max (afterAll_, afterWin_);
903  ENSURE (Time::MIN <= startWin_);
904  ENSURE (afterWin_ <= Time::MAX);
905  ENSURE (startAll_ <= startWin_);
906  ENSURE (afterWin_ <= afterAll_);
907  ENSURE (px_per_sec_ <= ZOOM_MAX_RESOLUTION);
908  ENSURE (px_per_sec_ <= changedMetric); // bias
909  }
910 
921  void
922  ensureInvariants(uint px =0)
923  {
924  if (px==0) px = pxWidth();
925  conformWindowToCanvas();
926  px_per_sec_ = conformMetricToWindow (px);
927  conformToBounds (px_per_sec_);
928  }
929 
930 
931 
932  /* === adjust and coordinate window parameters === */
933 
936  void
938  {
939  startAll_ = ensureNonEmpty(canvas).start();
940  afterAll_ = ensureNonEmpty(canvas).end();
942  }
943 
947  void
949  {
950  uint px{pxWidth()};
951  startWin_ = ensureNonEmpty(window).start();
952  afterWin_ = ensureNonEmpty(window).end();
954  startAll_ = min (startAll_, startWin_);
955  afterAll_ = max (afterAll_, afterWin_);
956  ensureInvariants (px);
957  }
958 
961  void
962  mutateRanges (TimeSpan canvas, TimeSpan window)
963  {
964  uint px{pxWidth()};
965  startAll_ = ensureNonEmpty(canvas).start();
966  afterAll_ = ensureNonEmpty(canvas).end();
967  startWin_ = ensureNonEmpty(window).start();
968  afterWin_ = ensureNonEmpty(window).end();
970  ensureInvariants (px);
971  }
972 
976  void
977  mutateScale (Rat changedMetric)
978  {
979  uint px{pxWidth()};
980  changedMetric = max (changedMetric, px / maxSaneWinExtension(px));
981  changedMetric = min (detox(changedMetric), ZOOM_MAX_RESOLUTION);
982  if (changedMetric == px_per_sec_) return;
983  conformWindowToMetric (changedMetric);
984  ensureInvariants (px);
985  }
986 
989  void
990  mutateDuration (FSecs duration, uint px =0)
991  {
992  if (px==0)
993  px = pxWidth();
994  if (duration <= 0)
995  duration = DEFAULT_CANVAS;
996  else if (duration > maxSaneWinExtension (px))
997  duration = maxSaneWinExtension (px);
998  placeWindowRelativeToAnchor (duration);
999  establishWindowDuration (duration);
1000  px_per_sec_ = conformMetricToWindow (px);
1001  ensureInvariants (px);
1002  }
1003 
1006  void
1007  adaptWindowToPixels (uint pxWidth)
1008  {
1009  pxWidth = util::limited (1u, pxWidth, MAX_PX_WIDTH);
1010  FSecs adaptedWindow{Rat{pxWidth} / px_per_sec_};
1011  adaptedWindow = max (adaptedWindow, MICRO_TICK); // prevent void window
1012  adaptedWindow = min (adaptedWindow, maxSaneWinExtension (pxWidth));
1013  establishWindowDuration (adaptedWindow);
1014  ensureInvariants (pxWidth);
1015  }
1016 
1022  void
1023  anchorWindowAtPosition (FSecs canvasOffset)
1024  {
1025  REQUIRE (afterWin_ > startWin_);
1026  REQUIRE (afterAll_ > startAll_);
1027  uint px{pxWidth()};
1028  FSecs duration{afterWin_-startWin_};
1029  Rat posFactor = canvasOffset / FSecs{afterAll_-startAll_};
1030  posFactor = parabolicAnchorRule (posFactor); // also limited 0...1
1031  FSecs partBeforeAnchor = scaleSafe (duration, posFactor);
1032  startWin_ = startAll_ + Offset{addSafe (canvasOffset, -partBeforeAnchor)};
1033  establishWindowDuration (duration);
1034  startAll_ = min (startAll_, startWin_);
1035  afterAll_ = max (afterAll_, afterWin_);
1036  ensureInvariants (px);
1037  }
1038 
1039 
1043  void
1045  {
1046  FSecs partBeforeAnchor = scaleSafe(duration, relativeAnchor());
1047  startWin_ = Time{anchorPoint()} - Time{partBeforeAnchor};
1048  }
1049 
1050  void
1051  establishWindowDuration (Duration duration)
1052  {
1053  if (startWin_<= Time::MAX - duration)
1054  afterWin_ = startWin_ + duration;
1055  else
1056  {
1057  startWin_ = Time::MAX - duration;
1058  afterWin_ = Time::MAX;
1059  }
1060  }
1061 
1062 
1063 
1074  FSecs
1075  anchorPoint() const
1076  {
1077  return startWin_ + Offset{scaleSafe (afterWin_-startWin_, relativeAnchor())};
1078  }
1079 
1087  Rat
1089  {
1090  // the visible window itself has to fit in, which reduces the action range
1091  FSecs possibleRange = (afterAll_-startAll_) - (afterWin_-startWin_);
1092  if (possibleRange <= 0) // if there is no room for scrolling...
1093  return 1_r/2; // then anchor zooming in the middle
1094 
1095  // use a 3rd degree parabola to favour positions in the middle
1096  Rat posFactor = FSecs{startWin_-startAll_} / possibleRange;
1097  return parabolicAnchorRule (posFactor);
1098  }
1099 
1109  static Rat
1110  parabolicAnchorRule (Rat posFactor)
1111  {
1112  posFactor = util::limited (0, posFactor, 1);
1113  if (toxicDegree(posFactor, 20)) // prevent integer wrap
1114  posFactor = util::reQuant(posFactor, 1 << 20);
1115  posFactor = (2*posFactor - 1); // -1 ... +1
1116  posFactor = posFactor*posFactor*posFactor; // -1 ... +1 but accelerating towards boundaries
1117  posFactor = (posFactor + 1) / 2; // 0 ... 1
1118  posFactor = util::limited (0, posFactor, 1);
1119  return detox (posFactor);
1120  }
1121  };
1122 
1123 
1124 
1125 }} // namespace stage::model
1126 #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:232
void mutateScale(Rat changedMetric)
static const Duration MAX
maximum possible temporal extension
Definition: timevalue.hpp:507
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:150
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:37
void placeWindowRelativeToAnchor(FSecs duration)
static const gavl_time_t SCALE
Number of micro ticks (µs) per second as basic time scale.
Definition: timevalue.hpp:167
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:299
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:37
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:220
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:358
void offsetVisiblePos(Offset offset)
scroll by arbitrary offset, possibly expanding canvas.
Duration is the internal Lumiera time metric.
Definition: timevalue.hpp:468
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:573
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:133
static const Time MAX
Definition: timevalue.hpp:309
void anchorWindowAtPosition(FSecs canvasOffset)