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