1 #include <mbgl/map/camera.hpp>
2 #include <mbgl/map/transform.hpp>
3 #include <mbgl/util/constants.hpp>
4 #include <mbgl/util/mat4.hpp>
5 #include <mbgl/util/math.hpp>
6 #include <mbgl/util/unitbezier.hpp>
7 #include <mbgl/util/interpolate.hpp>
8 #include <mbgl/util/chrono.hpp>
9 #include <mbgl/util/projection.hpp>
10 #include <mbgl/math/clamp.hpp>
11 #include <mbgl/util/logging.hpp>
12 #include <mbgl/util/platform.hpp>
13 
14 #include <cstdio>
15 #include <cmath>
16 
17 namespace mbgl {
18 
19 /** Converts the given angle (in radians) to be numerically close to the anchor angle, allowing it to be interpolated properly without sudden jumps. */
_normalizeAngle(double angle,double anchorAngle)20 static double _normalizeAngle(double angle, double anchorAngle)
21 {
22     if (std::isnan(angle) || std::isnan(anchorAngle)) {
23         return 0;
24     }
25 
26     angle = util::wrap(angle, -M_PI, M_PI);
27     if (angle == -M_PI) angle = M_PI;
28     double diff = std::abs(angle - anchorAngle);
29     if (std::abs(angle - util::M2PI - anchorAngle) < diff) {
30         angle -= util::M2PI;
31     }
32     if (std::abs(angle + util::M2PI - anchorAngle) < diff) {
33         angle += util::M2PI;
34     }
35 
36     return angle;
37 }
38 
Transform(MapObserver & observer_,ConstrainMode constrainMode,ViewportMode viewportMode)39 Transform::Transform(MapObserver& observer_,
40                      ConstrainMode constrainMode,
41                      ViewportMode viewportMode)
42     : observer(observer_), state(constrainMode, viewportMode) {
43 }
44 
45 #pragma mark - Map View
46 
resize(const Size size)47 void Transform::resize(const Size size) {
48     if (size.isEmpty()) {
49         throw std::runtime_error("failed to resize: size is empty");
50     }
51 
52     if (state.size == size) {
53         return;
54     }
55 
56     observer.onCameraWillChange(MapObserver::CameraChangeMode::Immediate);
57 
58     state.size = size;
59     state.constrain(state.scale, state.x, state.y);
60 
61     observer.onCameraDidChange(MapObserver::CameraChangeMode::Immediate);
62 }
63 
64 #pragma mark - Camera
65 
getCameraOptions(const EdgeInsets & padding) const66 CameraOptions Transform::getCameraOptions(const EdgeInsets& padding) const {
67     CameraOptions camera;
68     camera.center = getLatLng(padding);
69     camera.padding = padding;
70     camera.zoom = getZoom();
71     camera.angle = getAngle();
72     camera.pitch = getPitch();
73     return camera;
74 }
75 
76 /**
77  * Change any combination of center, zoom, bearing, and pitch, without
78  * a transition. The map will retain the current values for any options
79  * not included in `options`.
80  */
jumpTo(const CameraOptions & camera)81 void Transform::jumpTo(const CameraOptions& camera) {
82     easeTo(camera);
83 }
84 
85 /**
86  * Change any combination of center, zoom, bearing, and pitch, with a smooth animation
87  * between old and new values. The map will retain the current values for any options
88  * not included in `options`.
89  */
easeTo(const CameraOptions & camera,const AnimationOptions & animation)90 void Transform::easeTo(const CameraOptions& camera, const AnimationOptions& animation) {
91     const LatLng unwrappedLatLng = camera.center.value_or(getLatLng());
92     const LatLng latLng = unwrappedLatLng.wrapped();
93     double zoom = camera.zoom.value_or(getZoom());
94     double angle = camera.angle.value_or(getAngle());
95     double pitch = camera.pitch.value_or(getPitch());
96 
97     if (std::isnan(zoom)) {
98         return;
99     }
100 
101     // Determine endpoints.
102     EdgeInsets padding = camera.padding;
103     LatLng startLatLng = getLatLng(padding);
104     // If gesture in progress, we transfer the world rounds from the end
105     // longitude into start, so we can guarantee the "scroll effect" of rounding
106     // the world while assuring the end longitude remains wrapped.
107     if (isGestureInProgress()) {
108         startLatLng = LatLng(startLatLng.latitude(), startLatLng.longitude() - (unwrappedLatLng.longitude() - latLng.longitude()));
109     }
110     // Find the shortest path otherwise.
111     else startLatLng.unwrapForShortestPath(latLng);
112 
113     const Point<double> startPoint = Projection::project(startLatLng, state.scale);
114     const Point<double> endPoint = Projection::project(latLng, state.scale);
115 
116     ScreenCoordinate center = getScreenCoordinate(padding);
117     center.y = state.size.height - center.y;
118 
119     // Constrain camera options.
120     zoom = util::clamp(zoom, state.getMinZoom(), state.getMaxZoom());
121     const double scale = state.zoomScale(zoom);
122     pitch = util::clamp(pitch, state.min_pitch, state.max_pitch);
123 
124     // Minimize rotation by taking the shorter path around the circle.
125     angle = _normalizeAngle(angle, state.angle);
126     state.angle = _normalizeAngle(state.angle, angle);
127 
128     Duration duration = animation.duration ? *animation.duration : Duration::zero();
129 
130     const double startScale = state.scale;
131     const double startAngle = state.angle;
132     const double startPitch = state.pitch;
133     state.panning = latLng != startLatLng;
134     state.scaling = scale != startScale;
135     state.rotating = angle != startAngle;
136 
137     startTransition(camera, animation, [=](double t) {
138         Point<double> framePoint = util::interpolate(startPoint, endPoint, t);
139         LatLng frameLatLng = Projection::unproject(framePoint, startScale);
140         double frameScale = util::interpolate(startScale, scale, t);
141         state.setLatLngZoom(frameLatLng, state.scaleZoom(frameScale));
142 
143         if (angle != startAngle) {
144             state.angle = util::wrap(util::interpolate(startAngle, angle, t), -M_PI, M_PI);
145         }
146         if (pitch != startPitch) {
147             state.pitch = util::interpolate(startPitch, pitch, t);
148         }
149 
150         if (!padding.isFlush()) {
151             state.moveLatLng(frameLatLng, center);
152         }
153     }, duration);
154 }
155 
156 /** This method implements an “optimal path” animation, as detailed in:
157 
158     Van Wijk, Jarke J.; Nuij, Wim A. A. “Smooth and efficient zooming and
159         panning.” INFOVIS ’03. pp. 15–22.
160         <https://www.win.tue.nl/~vanwijk/zoompan.pdf#page=5>.
161 
162     Where applicable, local variable documentation begins with the associated
163     variable or function in van Wijk (2003). */
flyTo(const CameraOptions & camera,const AnimationOptions & animation)164 void Transform::flyTo(const CameraOptions &camera, const AnimationOptions &animation) {
165     const LatLng latLng = camera.center.value_or(getLatLng()).wrapped();
166     double zoom = camera.zoom.value_or(getZoom());
167     double angle = camera.angle.value_or(getAngle());
168     double pitch = camera.pitch.value_or(getPitch());
169 
170     if (std::isnan(zoom) || state.size.isEmpty()) {
171         return;
172     }
173 
174     // Determine endpoints.
175     EdgeInsets padding = camera.padding;
176     LatLng startLatLng = getLatLng(padding).wrapped();
177     startLatLng.unwrapForShortestPath(latLng);
178 
179     const Point<double> startPoint = Projection::project(startLatLng, state.scale);
180     const Point<double> endPoint = Projection::project(latLng, state.scale);
181 
182     ScreenCoordinate center = getScreenCoordinate(padding);
183     center.y = state.size.height - center.y;
184 
185     // Constrain camera options.
186     zoom = util::clamp(zoom, state.getMinZoom(), state.getMaxZoom());
187     pitch = util::clamp(pitch, state.min_pitch, state.max_pitch);
188 
189     // Minimize rotation by taking the shorter path around the circle.
190     angle = _normalizeAngle(angle, state.angle);
191     state.angle = _normalizeAngle(state.angle, angle);
192 
193     const double startZoom = state.scaleZoom(state.scale);
194     const double startAngle = state.angle;
195     const double startPitch = state.pitch;
196 
197     /// w₀: Initial visible span, measured in pixels at the initial scale.
198     /// Known henceforth as a <i>screenful</i>.
199     double w0 = std::max(state.size.width - padding.left() - padding.right(),
200                          state.size.height - padding.top() - padding.bottom());
201     /// w₁: Final visible span, measured in pixels with respect to the initial
202     /// scale.
203     double w1 = w0 / state.zoomScale(zoom - startZoom);
204     /// Length of the flight path as projected onto the ground plane, measured
205     /// in pixels from the world image origin at the initial scale.
206     double u1 = ::hypot((endPoint - startPoint).x, (endPoint - startPoint).y);
207 
208     /** ρ: The relative amount of zooming that takes place along the flight
209         path. A high value maximizes zooming for an exaggerated animation, while
210         a low value minimizes zooming for something closer to easeTo().
211 
212         1.42 is the average value selected by participants in the user study in
213         van Wijk (2003). A value of 6<sup>¼</sup> would be equivalent to the
214         root mean squared average velocity, V<sub>RMS</sub>. A value of 1 would
215         produce a circular motion. */
216     double rho = 1.42;
217     if (animation.minZoom) {
218         double minZoom = util::min(*animation.minZoom, startZoom, zoom);
219         minZoom = util::clamp(minZoom, state.getMinZoom(), state.getMaxZoom());
220         /// w<sub>m</sub>: Maximum visible span, measured in pixels with respect
221         /// to the initial scale.
222         double wMax = w0 / state.zoomScale(minZoom - startZoom);
223         rho = std::sqrt(wMax / u1 * 2);
224     }
225     /// ρ²
226     double rho2 = rho * rho;
227 
228     /** rᵢ: Returns the zoom-out factor at one end of the animation.
229 
230         @param i 0 for the ascent or 1 for the descent. */
231     auto r = [=](double i) {
232         /// bᵢ
233         double b = (w1 * w1 - w0 * w0 + (i ? -1 : 1) * rho2 * rho2 * u1 * u1) / (2 * (i ? w1 : w0) * rho2 * u1);
234         return std::log(std::sqrt(b * b + 1) - b);
235     };
236 
237     /// r₀: Zoom-out factor during ascent.
238     double r0 = r(0);
239     double r1 = r(1);
240 
241     // When u₀ = u₁, the optimal path doesn’t require both ascent and descent.
242     bool isClose = std::abs(u1) < 1.0 || !std::isfinite(r0) || !std::isfinite(r1);
243 
244     /** w(s): Returns the visible span on the ground, measured in pixels with
245         respect to the initial scale.
246 
247         Assumes an angular field of view of 2 arctan ½ ≈ 53°. */
248     auto w = [=](double s) {
249         return (isClose ? std::exp((w1 < w0 ? -1 : 1) * rho * s)
250                 : (std::cosh(r0) / std::cosh(r0 + rho * s)));
251     };
252     /// u(s): Returns the distance along the flight path as projected onto the
253     /// ground plane, measured in pixels from the world image origin at the
254     /// initial scale.
255     auto u = [=](double s) {
256         return (isClose ? 0.
257                 : (w0 * (std::cosh(r0) * std::tanh(r0 + rho * s) - std::sinh(r0)) / rho2 / u1));
258     };
259     /// S: Total length of the flight path, measured in ρ-screenfuls.
260     double S = (isClose ? (std::abs(std::log(w1 / w0)) / rho)
261                 : ((r1 - r0) / rho));
262 
263     Duration duration;
264     if (animation.duration) {
265         duration = *animation.duration;
266     } else {
267         /// V: Average velocity, measured in ρ-screenfuls per second.
268         double velocity = 1.2;
269         if (animation.velocity) {
270             velocity = *animation.velocity / rho;
271         }
272         duration = std::chrono::duration_cast<Duration>(std::chrono::duration<double>(S / velocity));
273     }
274     if (duration == Duration::zero()) {
275         // Perform an instantaneous transition.
276         jumpTo(camera);
277         return;
278     }
279 
280     const double startScale = state.scale;
281     state.panning = true;
282     state.scaling = true;
283     state.rotating = angle != startAngle;
284 
285     startTransition(camera, animation, [=](double k) {
286         /// s: The distance traveled along the flight path, measured in
287         /// ρ-screenfuls.
288         double s = k * S;
289         double us = k == 1.0 ? 1.0 : u(s);
290 
291         // Calculate the current point and zoom level along the flight path.
292         Point<double> framePoint = util::interpolate(startPoint, endPoint, us);
293         double frameZoom = startZoom + state.scaleZoom(1 / w(s));
294 
295         // Zoom can be NaN if size is empty.
296         if (std::isnan(frameZoom)) {
297             frameZoom = zoom;
298         }
299 
300         // Convert to geographic coordinates and set the new viewpoint.
301         LatLng frameLatLng = Projection::unproject(framePoint, startScale);
302         state.setLatLngZoom(frameLatLng, frameZoom);
303 
304         if (angle != startAngle) {
305             state.angle = util::wrap(util::interpolate(startAngle, angle, k), -M_PI, M_PI);
306         }
307         if (pitch != startPitch) {
308             state.pitch = util::interpolate(startPitch, pitch, k);
309         }
310 
311         if (!padding.isFlush()) {
312             state.moveLatLng(frameLatLng, center);
313         }
314     }, duration);
315 }
316 
317 #pragma mark - Position
318 
moveBy(const ScreenCoordinate & offset,const AnimationOptions & animation)319 void Transform::moveBy(const ScreenCoordinate& offset, const AnimationOptions& animation) {
320     ScreenCoordinate centerOffset = {
321         offset.x,
322         -offset.y,
323     };
324     ScreenCoordinate centerPoint = getScreenCoordinate() - centerOffset;
325 
326     CameraOptions camera;
327     camera.center = state.screenCoordinateToLatLng(centerPoint);
328     easeTo(camera, animation);
329 }
330 
setLatLng(const LatLng & latLng,const AnimationOptions & animation)331 void Transform::setLatLng(const LatLng& latLng, const AnimationOptions& animation) {
332     setLatLng(latLng, optional<ScreenCoordinate> {}, animation);
333 }
334 
setLatLng(const LatLng & latLng,const EdgeInsets & padding,const AnimationOptions & animation)335 void Transform::setLatLng(const LatLng& latLng, const EdgeInsets& padding, const AnimationOptions& animation) {
336     CameraOptions camera;
337     camera.center = latLng;
338     camera.padding = padding;
339     easeTo(camera, animation);
340 }
341 
setLatLng(const LatLng & latLng,optional<ScreenCoordinate> anchor,const AnimationOptions & animation)342 void Transform::setLatLng(const LatLng& latLng, optional<ScreenCoordinate> anchor, const AnimationOptions& animation) {
343     CameraOptions camera;
344     camera.center = latLng;
345     if (anchor) {
346         camera.padding = EdgeInsets(anchor->y, anchor->x, state.size.height - anchor->y, state.size.width - anchor->x);
347     }
348     easeTo(camera, animation);
349 }
350 
setLatLngZoom(const LatLng & latLng,double zoom,const AnimationOptions & animation)351 void Transform::setLatLngZoom(const LatLng& latLng, double zoom, const AnimationOptions& animation) {
352     setLatLngZoom(latLng, zoom, EdgeInsets(), animation);
353 }
354 
setLatLngZoom(const LatLng & latLng,double zoom,const EdgeInsets & padding,const AnimationOptions & animation)355 void Transform::setLatLngZoom(const LatLng& latLng, double zoom, const EdgeInsets& padding, const AnimationOptions& animation) {
356     if (std::isnan(zoom)) return;
357 
358     CameraOptions camera;
359     camera.center = latLng;
360     camera.padding = padding;
361     camera.zoom = zoom;
362     easeTo(camera, animation);
363 }
364 
getLatLng(const EdgeInsets & padding) const365 LatLng Transform::getLatLng(const EdgeInsets& padding) const {
366     if (padding.isFlush()) {
367         return state.getLatLng();
368     } else {
369         return screenCoordinateToLatLng(padding.getCenter(state.size.width, state.size.height));
370     }
371 }
372 
getScreenCoordinate(const EdgeInsets & padding) const373 ScreenCoordinate Transform::getScreenCoordinate(const EdgeInsets& padding) const {
374     if (padding.isFlush()) {
375         return { state.size.width / 2., state.size.height / 2. };
376     } else {
377         return padding.getCenter(state.size.width, state.size.height);
378     }
379 }
380 
381 
382 #pragma mark - Zoom
383 
setZoom(double zoom,const AnimationOptions & animation)384 void Transform::setZoom(double zoom, const AnimationOptions& animation) {
385     CameraOptions camera;
386     camera.zoom = zoom;
387     easeTo(camera, animation);
388 }
389 
setZoom(double zoom,optional<ScreenCoordinate> anchor,const AnimationOptions & animation)390 void Transform::setZoom(double zoom, optional<ScreenCoordinate> anchor, const AnimationOptions& animation) {
391     CameraOptions camera;
392     camera.zoom = zoom;
393     camera.anchor = anchor;
394     easeTo(camera, animation);
395 }
396 
setZoom(double zoom,const EdgeInsets & padding,const AnimationOptions & animation)397 void Transform::setZoom(double zoom, const EdgeInsets& padding, const AnimationOptions& animation) {
398     CameraOptions camera;
399     camera.zoom = zoom;
400     if (!padding.isFlush()) camera.anchor = getScreenCoordinate(padding);
401     easeTo(camera, animation);
402 }
403 
getZoom() const404 double Transform::getZoom() const {
405     return state.getZoom();
406 }
407 
408 #pragma mark - Bounds
409 
setLatLngBounds(optional<LatLngBounds> bounds)410 void Transform::setLatLngBounds(optional<LatLngBounds> bounds) {
411     if (bounds && !bounds->valid()) {
412         throw std::runtime_error("failed to set bounds: bounds are invalid");
413     }
414     state.setLatLngBounds(bounds);
415 }
416 
setMinZoom(const double minZoom)417 void Transform::setMinZoom(const double minZoom) {
418     if (std::isnan(minZoom)) return;
419     state.setMinZoom(minZoom);
420 }
421 
setMaxZoom(const double maxZoom)422 void Transform::setMaxZoom(const double maxZoom) {
423     if (std::isnan(maxZoom)) return;
424     state.setMaxZoom(maxZoom);
425 }
426 
setMinPitch(double minPitch)427 void Transform::setMinPitch(double minPitch) {
428     if (std::isnan(minPitch)) return;
429     state.setMinPitch(minPitch);
430 }
431 
setMaxPitch(double maxPitch)432 void Transform::setMaxPitch(double maxPitch) {
433     if (std::isnan(maxPitch)) return;
434     state.setMaxPitch(maxPitch);
435 }
436 
437 #pragma mark - Angle
438 
rotateBy(const ScreenCoordinate & first,const ScreenCoordinate & second,const AnimationOptions & animation)439 void Transform::rotateBy(const ScreenCoordinate& first, const ScreenCoordinate& second,  const AnimationOptions& animation) {
440     ScreenCoordinate center = getScreenCoordinate();
441     const ScreenCoordinate offset = first - center;
442     const double distance = std::sqrt(std::pow(2, offset.x) + std::pow(2, offset.y));
443 
444     // If the first click was too close to the center, move the center of rotation by 200 pixels
445     // in the direction of the click.
446     if (distance < 200) {
447         const double heightOffset = -200;
448         const double rotateAngle = std::atan2(offset.y, offset.x);
449         center.x = first.x + std::cos(rotateAngle) * heightOffset;
450         center.y = first.y + std::sin(rotateAngle) * heightOffset;
451     }
452 
453     CameraOptions camera;
454     camera.angle = state.angle + util::angle_between(first - center, second - center);
455     easeTo(camera, animation);
456 }
457 
setAngle(double angle,const AnimationOptions & animation)458 void Transform::setAngle(double angle, const AnimationOptions& animation) {
459     setAngle(angle, optional<ScreenCoordinate> {}, animation);
460 }
461 
setAngle(double angle,optional<ScreenCoordinate> anchor,const AnimationOptions & animation)462 void Transform::setAngle(double angle, optional<ScreenCoordinate> anchor, const AnimationOptions& animation) {
463     if (std::isnan(angle)) return;
464     CameraOptions camera;
465     camera.angle = angle;
466     camera.anchor = anchor;
467     easeTo(camera, animation);
468 }
469 
setAngle(double angle,const EdgeInsets & padding,const AnimationOptions & animation)470 void Transform::setAngle(double angle, const EdgeInsets& padding, const AnimationOptions& animation) {
471     optional<ScreenCoordinate> anchor;
472     if (!padding.isFlush()) anchor = getScreenCoordinate(padding);
473     setAngle(angle, anchor, animation);
474 }
475 
getAngle() const476 double Transform::getAngle() const {
477     return state.angle;
478 }
479 
480 #pragma mark - Pitch
481 
setPitch(double pitch,const AnimationOptions & animation)482 void Transform::setPitch(double pitch, const AnimationOptions& animation) {
483     setPitch(pitch, optional<ScreenCoordinate> {}, animation);
484 }
485 
setPitch(double pitch,optional<ScreenCoordinate> anchor,const AnimationOptions & animation)486 void Transform::setPitch(double pitch, optional<ScreenCoordinate> anchor, const AnimationOptions& animation) {
487     if (std::isnan(pitch)) return;
488     CameraOptions camera;
489     camera.pitch = pitch;
490     camera.anchor = anchor;
491     easeTo(camera, animation);
492 }
493 
getPitch() const494 double Transform::getPitch() const {
495     return state.pitch;
496 }
497 
498 #pragma mark - North Orientation
499 
setNorthOrientation(NorthOrientation orientation)500 void Transform::setNorthOrientation(NorthOrientation orientation) {
501     state.orientation = orientation;
502     state.constrain(state.scale, state.x, state.y);
503 }
504 
getNorthOrientation() const505 NorthOrientation Transform::getNorthOrientation() const {
506     return state.getNorthOrientation();
507 }
508 
509 #pragma mark - Constrain mode
510 
setConstrainMode(mbgl::ConstrainMode mode)511 void Transform::setConstrainMode(mbgl::ConstrainMode mode) {
512     state.constrainMode = mode;
513     state.constrain(state.scale, state.x, state.y);
514 }
515 
getConstrainMode() const516 ConstrainMode Transform::getConstrainMode() const {
517     return state.getConstrainMode();
518 }
519 
520 #pragma mark - Viewport mode
521 
setViewportMode(mbgl::ViewportMode mode)522 void Transform::setViewportMode(mbgl::ViewportMode mode) {
523     state.viewportMode = mode;
524 }
525 
getViewportMode() const526 ViewportMode Transform::getViewportMode() const {
527     return state.getViewportMode();
528 }
529 
530 #pragma mark - Projection mode
531 
setAxonometric(bool axonometric)532 void Transform::setAxonometric(bool axonometric) {
533     state.axonometric = axonometric;
534 }
535 
getAxonometric() const536 bool Transform::getAxonometric() const {
537     return state.axonometric;
538 }
539 
setXSkew(double xSkew)540 void Transform::setXSkew(double xSkew) {
541     state.xSkew = xSkew;
542 }
543 
getXSkew() const544 double Transform::getXSkew() const {
545     return state.xSkew;
546 }
547 
setYSkew(double ySkew)548 void Transform::setYSkew(double ySkew) {
549     state.ySkew = ySkew;
550 }
551 
getYSkew() const552 double Transform::getYSkew() const {
553     return state.ySkew;
554 }
555 
556 #pragma mark - Transition
557 
startTransition(const CameraOptions & camera,const AnimationOptions & animation,std::function<void (double)> frame,const Duration & duration)558 void Transform::startTransition(const CameraOptions& camera,
559                                 const AnimationOptions& animation,
560                                 std::function<void(double)> frame,
561                                 const Duration& duration) {
562     if (transitionFinishFn) {
563         transitionFinishFn();
564     }
565 
566     bool isAnimated = duration != Duration::zero();
567     observer.onCameraWillChange(isAnimated ? MapObserver::CameraChangeMode::Animated : MapObserver::CameraChangeMode::Immediate);
568 
569     // Associate the anchor, if given, with a coordinate.
570     optional<ScreenCoordinate> anchor = camera.anchor;
571     LatLng anchorLatLng;
572     if (anchor) {
573         anchor->y = state.size.height - anchor->y;
574         anchorLatLng = state.screenCoordinateToLatLng(*anchor);
575     }
576 
577     transitionStart = Clock::now();
578     transitionDuration = duration;
579 
580     transitionFrameFn = [isAnimated, animation, frame, anchor, anchorLatLng, this](const TimePoint now) {
581         float t = isAnimated ? (std::chrono::duration<float>(now - transitionStart) / transitionDuration) : 1.0;
582         if (t >= 1.0) {
583             frame(1.0);
584         } else {
585             util::UnitBezier ease = animation.easing ? *animation.easing : util::DEFAULT_TRANSITION_EASE;
586             frame(ease.solve(t, 0.001));
587         }
588 
589         if (anchor) state.moveLatLng(anchorLatLng, *anchor);
590 
591         // At t = 1.0, a DidChangeAnimated notification should be sent from finish().
592         if (t < 1.0) {
593             if (animation.transitionFrameFn) {
594                 animation.transitionFrameFn(t);
595             }
596             observer.onCameraIsChanging();
597             return false;
598         } else {
599             // Indicate that we need to terminate this transition
600             return true;
601         }
602     };
603 
604     transitionFinishFn = [isAnimated, animation, this] {
605         state.panning = false;
606         state.scaling = false;
607         state.rotating = false;
608         if (animation.transitionFinishFn) {
609             animation.transitionFinishFn();
610         }
611         observer.onCameraDidChange(isAnimated ? MapObserver::CameraChangeMode::Animated : MapObserver::CameraChangeMode::Immediate);
612     };
613 
614     if (!isAnimated) {
615         auto update = std::move(transitionFrameFn);
616         auto finish = std::move(transitionFinishFn);
617 
618         transitionFrameFn = nullptr;
619         transitionFinishFn = nullptr;
620 
621         update(Clock::now());
622         finish();
623     }
624 }
625 
inTransition() const626 bool Transform::inTransition() const {
627     return transitionFrameFn != nullptr;
628 }
629 
updateTransitions(const TimePoint & now)630 void Transform::updateTransitions(const TimePoint& now) {
631 
632     // Use a temporary function to ensure that the transitionFrameFn lambda is
633     // called only once per update.
634 
635     // This addresses the symptoms of https://github.com/mapbox/mapbox-gl-native/issues/11180
636     // where setting a shape source to nil (or similar) in the `onCameraIsChanging`
637     // observer function causes `Map::Impl::onUpdate()` to be called which
638     // in turn calls this function (before the current iteration has completed),
639     // leading to an infinite loop. See https://github.com/mapbox/mapbox-gl-native/issues/5833
640     // for a similar, related, issue.
641     //
642     // By temporarily nulling the `transitionFrameFn` (and then restoring it
643     // after the temporary has been called) we stop this recursion.
644     //
645     // It's important to note that the scope of this change is stop the above
646     // crashes. It doesn't address any potential deeper issue (for example
647     // user error, how often and when transition callbacks are called).
648 
649     auto transition = std::move(transitionFrameFn);
650     transitionFrameFn = nullptr;
651 
652     if (transition && transition(now)) {
653         // If the transition indicates that it is complete, then we should call
654         // the finish lambda (going via a temporary as above)
655         auto finish = std::move(transitionFinishFn);
656 
657         transitionFinishFn = nullptr;
658         transitionFrameFn = nullptr;
659 
660         if (finish) {
661             finish();
662         }
663     } else if (!transitionFrameFn) {
664         // We have to check `transitionFrameFn` is nil here, since a new transition
665         // may have been triggered in a user callback (from the transition call
666         // above)
667         transitionFrameFn = std::move(transition);
668     }
669 }
670 
cancelTransitions()671 void Transform::cancelTransitions() {
672     if (transitionFinishFn) {
673         transitionFinishFn();
674     }
675 
676     transitionFrameFn = nullptr;
677     transitionFinishFn = nullptr;
678 }
679 
setGestureInProgress(bool inProgress)680 void Transform::setGestureInProgress(bool inProgress) {
681     state.gestureInProgress = inProgress;
682 }
683 
684 #pragma mark Conversion and projection
685 
latLngToScreenCoordinate(const LatLng & latLng) const686 ScreenCoordinate Transform::latLngToScreenCoordinate(const LatLng& latLng) const {
687     ScreenCoordinate point = state.latLngToScreenCoordinate(latLng);
688     point.y = state.size.height - point.y;
689     return point;
690 }
691 
screenCoordinateToLatLng(const ScreenCoordinate & point) const692 LatLng Transform::screenCoordinateToLatLng(const ScreenCoordinate& point) const {
693     ScreenCoordinate flippedPoint = point;
694     flippedPoint.y = state.size.height - flippedPoint.y;
695     return state.screenCoordinateToLatLng(flippedPoint).wrapped();
696 }
697 
698 } // namespace mbgl
699