From 8e506c2a8c9275491838ba11eb3b82016cb4a394 Mon Sep 17 00:00:00 2001 From: Yousif Aldolaijan Date: Mon, 19 May 2025 21:07:50 +0300 Subject: [PATCH 01/15] modify the transform implementation to allow for concurrent animations The previous implementation of the transform class overrides the current animation with the new one. This new implmentation changes that to allow for concurrent transformations to occur for different types of transfomrations at the same time. For example, you can have two concurrent animations where one modifies the zoom while the other modifies the position of the camera without overriding the other. This change is necessary to allow for concurrent camera animations during navigation. --- include/mbgl/map/camera.hpp | 30 +++ src/mbgl/map/transform.cpp | 410 +++++++++++++++++++++--------------- src/mbgl/map/transform.hpp | 43 +++- 3 files changed, 311 insertions(+), 172 deletions(-) diff --git a/include/mbgl/map/camera.hpp b/include/mbgl/map/camera.hpp index 3bdacf5e8bed..1361ac067ca7 100644 --- a/include/mbgl/map/camera.hpp +++ b/include/mbgl/map/camera.hpp @@ -156,4 +156,34 @@ struct FreeCameraOptions { void setPitchBearing(double pitch, double bearing) noexcept; }; +struct PropertyAnimation { + TimePoint start; + Duration duration; + AnimationOptions animation; + bool ran = false, finished = false, done = false; + bool panning = false, scaling = false, rotating = false; + + PropertyAnimation( + TimePoint start_, Duration duration_, AnimationOptions animation_, bool panning_, bool scaling_, bool rotating_) + : start(start_), + duration(duration_), + animation(animation_), + panning(panning_), + scaling(scaling_), + rotating(rotating_) {} + + double t(TimePoint now) { + bool isAnimated = duration != Duration::zero(); + double t = isAnimated ? (std::chrono::duration(now - start) / duration) : 1.0f; + if (t >= 1.0) { + return 1.0; + } + + util::UnitBezier ease = animation.easing ? *animation.easing : util::DEFAULT_TRANSITION_EASE; + return ease.solve(t, 0.001); + } + + bool isAnimated() const { return duration != Duration::zero(); } +}; + } // namespace mbgl diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index 72408bb6e141..26a04a23d6e4 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -158,37 +158,72 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& const double startZoom = state.getZoom(); const double startBearing = state.getBearing(); const double startPitch = state.getPitch(); - state.setProperties(TransformStateProperties() - .withPanningInProgress(unwrappedLatLng != startLatLng) - .withScalingInProgress(zoom != startZoom) - .withRotatingInProgress(bearing != startBearing)); const EdgeInsets startEdgeInsets = state.getEdgeInsets(); - startTransition( - camera, - animation, - [=, this](double t) { - Point framePoint = util::interpolate(startPoint, endPoint, t); - LatLng frameLatLng = Projection::unproject(framePoint, state.zoomScale(startZoom)); - double frameZoom = util::interpolate(startZoom, zoom, t); - state.setLatLngZoom(frameLatLng, frameZoom); - if (bearing != startBearing) { - state.setBearing(util::wrap(util::interpolate(startBearing, bearing, t), -pi, pi)); - } - if (padding != startEdgeInsets) { - // Interpolate edge insets - EdgeInsets edgeInsets; - state.setEdgeInsets({util::interpolate(startEdgeInsets.top(), padding.top(), t), - util::interpolate(startEdgeInsets.left(), padding.left(), t), - util::interpolate(startEdgeInsets.bottom(), padding.bottom(), t), - util::interpolate(startEdgeInsets.right(), padding.right(), t)}); - } - double maxPitch = getMaxPitchForEdgeInsets(state.getEdgeInsets()); - if (pitch != startPitch || maxPitch < startPitch) { - state.setPitch(std::min(maxPitch, util::interpolate(startPitch, pitch, t))); - } - }, - duration); + auto pa = std::make_shared( + Clock::now(), duration, animation, unwrappedLatLng != startLatLng, zoom != startZoom, bearing != startBearing); + + // NOTE: For tests only + transitionStart = pa->start; + transitionDuration = pa->duration; + + if (!pas.zoom.set || startZoom != zoom) { + animationFinishFrame(pas.zoom.pa); + pas.zoom = { + .pa = pa, + .current = startZoom, + .target = zoom, + .set = true, + .frameZoomFunc = + [=, this](TimePoint now) { + return util::interpolate(pas.zoom.current, pas.zoom.target, pas.zoom.pa->t(now)); + }, + }; + } + if (!pas.latlng.set || startPoint != endPoint) { + animationFinishFrame(pas.latlng.pa); + pas.latlng = { + .pa = pa, + .current = startPoint, + .target = endPoint, + .set = true, + .frameLatLngFunc = + [=, this](TimePoint now) { + Point framePoint = util::interpolate( + pas.latlng.current, pas.latlng.target, pas.latlng.pa->t(now)); + return Projection::unproject(framePoint, state.zoomScale(startZoom)); + }, + }; + } + if (!pas.bearing.set || bearing != startBearing) { + animationFinishFrame(pas.bearing.pa); + pas.bearing = { + .pa = pa, + .current = startBearing, + .target = bearing, + .set = true, + }; + } + if (!pas.padding.set || padding != startEdgeInsets) { + animationFinishFrame(pas.padding.pa); + pas.padding = { + .pa = pa, + .current = startEdgeInsets, + .target = padding, + .set = true, + }; + } + if (!pas.pitch.set || pitch != startPitch) { + animationFinishFrame(pas.pitch.pa); + pas.pitch = { + .pa = pa, + .current = startPitch, + .target = pitch, + .set = true, + }; + } + + startTransition(camera, duration); } /** This method implements an “optimal path” animation, as detailed in: @@ -323,51 +358,99 @@ void Transform::flyTo(const CameraOptions& inputCamera, } const double startScale = state.getScale(); - state.setProperties( - TransformStateProperties().withPanningInProgress(true).withScalingInProgress(true).withRotatingInProgress( - bearing != startBearing)); const EdgeInsets startEdgeInsets = state.getEdgeInsets(); - startTransition( - camera, - animation, - [=, this](double k) { - /// s: The distance traveled along the flight path, measured in - /// ρ-screenfuls. - double s = k * S; - double us = k == 1.0 ? 1.0 : u(s); - - // Calculate the current point and zoom level along the flight path. - Point framePoint = util::interpolate(startPoint, endPoint, us); - double frameZoom = linearZoomInterpolation ? util::interpolate(startZoom, zoom, k) - : startZoom + state.scaleZoom(1 / w(s)); - - // Zoom can be NaN if size is empty. - if (std::isnan(frameZoom)) { - frameZoom = zoom; - } + auto pa = std::make_shared( + Clock::now(), duration, animation, true, true, bearing != startBearing); + + // NOTE: For tests only + transitionStart = pa->start; + transitionDuration = pa->duration; + + if (!pas.zoom.set || startZoom != zoom) { + animationFinishFrame(pas.zoom.pa); + pas.zoom = { + .pa = pa, + .current = startZoom, + .target = zoom, + .set = true, + .frameZoomFunc = + [=, this](TimePoint now) { + double t = pas.zoom.pa->t(now); + double s = t * S; + double frameZoom = linearZoomInterpolation ? util::interpolate(pas.zoom.current, pas.zoom.target, t) + : pas.zoom.current + state.scaleZoom(1 / w(s)); + + if (std::isnan(frameZoom)) { + frameZoom = pas.zoom.target; + } + + return frameZoom; + }, + }; + } + if (!pas.latlng.set || startPoint != endPoint) { + animationFinishFrame(pas.latlng.pa); + pas.latlng = { + .pa = pa, + .current = startPoint, + .target = endPoint, + .set = true, + .frameLatLngFunc = + [=, this](TimePoint now) { + double t = pas.latlng.pa->t(now); + double s = t * S; + double us = t == 1.0 ? 1.0 : u(s); + + Point framePoint = util::interpolate(pas.latlng.current, pas.latlng.target, us); + return Projection::unproject(framePoint, startScale); + }, + }; + } + if (!pas.bearing.set || bearing != startBearing) { + animationFinishFrame(pas.bearing.pa); + pas.bearing = { + .pa = pa, + .current = startBearing, + .target = bearing, + .set = true, + }; + } + if (!pas.padding.set || padding != startEdgeInsets) { + animationFinishFrame(pas.padding.pa); + pas.padding = {.pa = pa, .current = startEdgeInsets, .target = padding, .set = true}; + } + if (!pas.pitch.set || pitch != startPitch) { + animationFinishFrame(pas.pitch.pa); + pas.pitch = { + .pa = pa, + .current = startPitch, + .target = pitch, + .set = true, + }; + } - // Convert to geographic coordinates and set the new viewpoint. - LatLng frameLatLng = Projection::unproject(framePoint, startScale); - state.setLatLngZoom(frameLatLng, frameZoom); - if (bearing != startBearing) { - state.setBearing(util::wrap(util::interpolate(startBearing, bearing, k), -pi, pi)); - } + startTransition(camera, duration); +} - if (padding != startEdgeInsets) { - // Interpolate edge insets - state.setEdgeInsets({util::interpolate(startEdgeInsets.top(), padding.top(), k), - util::interpolate(startEdgeInsets.left(), padding.left(), k), - util::interpolate(startEdgeInsets.bottom(), padding.bottom(), k), - util::interpolate(startEdgeInsets.right(), padding.right(), k)}); - } - double maxPitch = getMaxPitchForEdgeInsets(state.getEdgeInsets()); +bool Transform::animationTransitionFrame(std::shared_ptr& pa, double t) { + if (pa->ran) { + return pa->done; + } - if (pitch != startPitch || maxPitch < startPitch) { - state.setPitch(std::min(maxPitch, util::interpolate(startPitch, pitch, k))); - } - }, - duration); + pa->ran = true; + if (t < 1.0) { + if (pa->animation.transitionFrameFn) { + pa->animation.transitionFrameFn(t); + } + + observer.onCameraIsChanging(); + pa->done = false; + } else { + pa->done = true; + } + + return pa->done; } // MARK: - Position @@ -519,14 +602,22 @@ ProjectionMode Transform::getProjectionMode() const { // MARK: - Transition -void Transform::startTransition(const CameraOptions& camera, - const AnimationOptions& animation, - const std::function& frame, - const Duration& duration) { - if (transitionFinishFn) { - transitionFinishFn(); +void Transform::animationFinishFrame(std::shared_ptr& pa) { + if (!pa || pa->finished) { + return; } + if (pa->animation.transitionFinishFn) { + pa->animation.transitionFinishFn(); + } + + pa->finished = true; + + observer.onCameraDidChange(pa->isAnimated() ? MapObserver::CameraChangeMode::Animated + : MapObserver::CameraChangeMode::Immediate); +} + +void Transform::startTransition(const CameraOptions& camera, const Duration& duration) { bool isAnimated = duration != Duration::zero(); observer.onCameraWillChange(isAnimated ? MapObserver::CameraChangeMode::Animated : MapObserver::CameraChangeMode::Immediate); @@ -534,115 +625,102 @@ void Transform::startTransition(const CameraOptions& camera, // Associate the anchor, if given, with a coordinate. // Anchor and center points are mutually exclusive, with preference for the // center point when both are set. - std::optional anchor = camera.center ? std::nullopt : camera.anchor; - LatLng anchorLatLng; - if (anchor) { - anchor->y = state.getSize().height - anchor->y; - anchorLatLng = state.screenCoordinateToLatLng(*anchor); + pas.anchor = camera.center ? std::nullopt : camera.anchor; + if (pas.anchor) { + pas.anchor->y = state.getSize().height - pas.anchor->y; + pas.anchorLatLng = state.screenCoordinateToLatLng(*pas.anchor); } - transitionStart = Clock::now(); - transitionDuration = duration; - - transitionFrameFn = [isAnimated, animation, frame, anchor, anchorLatLng, this](const TimePoint now) { - float t = isAnimated ? (std::chrono::duration(now - transitionStart) / transitionDuration) : 1.0f; - if (t >= 1.0) { - frame(1.0); - } else { - util::UnitBezier ease = animation.easing ? *animation.easing : util::DEFAULT_TRANSITION_EASE; - frame(ease.solve(t, 0.001)); - } + if (!isAnimated) { + activeAnimation = false; + updateTransitions(Clock::now()); + } +} - if (anchor) state.moveLatLng(anchorLatLng, *anchor); +bool Transform::inTransition() const { + return pas.latlng.set || pas.zoom.set || pas.bearing.set || pas.padding.set || pas.pitch.set; +} - // At t = 1.0, a DidChangeAnimated notification should be sent from finish(). - if (t < 1.0) { - if (animation.transitionFrameFn) { - animation.transitionFrameFn(t); +void Transform::updateTransitions(const TimePoint& now) { + if (!activeAnimation) { + activeAnimation = true; + + bool panning = false, scaling = false, rotating = false; + visit_pas([&](std::shared_ptr& pa) { + if (pa) { + panning |= pa->panning; + scaling |= pa->scaling; + rotating |= pa->rotating; + } + }); + + state.setProperties(TransformStateProperties() + .withPanningInProgress(panning) + .withScalingInProgress(scaling) + .withRotatingInProgress(rotating)); + + if (pas.latlng.frameLatLngFunc && pas.zoom.frameZoomFunc) { + if (pas.latlng.set || pas.zoom.set) { + state.setLatLngZoom(pas.latlng.frameLatLngFunc(now), pas.zoom.frameZoomFunc(now)); + if (animationTransitionFrame(pas.latlng.pa, pas.latlng.pa->t(now))) { + pas.latlng.set = false; + } + if (animationTransitionFrame(pas.zoom.pa, pas.zoom.pa->t(now))) { + pas.zoom.set = false; + } } - observer.onCameraIsChanging(); - return false; - } else { - // Indicate that we need to terminate this transition - return true; } - }; - transitionFinishFn = [isAnimated, animation, this] { - state.setProperties( - TransformStateProperties().withPanningInProgress(false).withScalingInProgress(false).withRotatingInProgress( - false)); - if (animation.transitionFinishFn) { - animation.transitionFinishFn(); + if (pas.bearing.set) { + double bearing_t = pas.bearing.pa->t(now); + state.setBearing( + util::wrap(util::interpolate(pas.bearing.current, pas.bearing.target, bearing_t), -pi, pi)); + if (animationTransitionFrame(pas.bearing.pa, bearing_t)) { + pas.bearing.set = false; + } } - observer.onCameraDidChange(isAnimated ? MapObserver::CameraChangeMode::Animated - : MapObserver::CameraChangeMode::Immediate); - }; - if (!isAnimated) { - auto update = std::move(transitionFrameFn); - auto finish = std::move(transitionFinishFn); + if (pas.padding.set) { + double padding_t = pas.padding.pa->t(now); + state.setEdgeInsets( + {util::interpolate(pas.padding.current.top(), pas.padding.target.top(), padding_t), + util::interpolate(pas.padding.current.left(), pas.padding.target.left(), padding_t), + util::interpolate(pas.padding.current.bottom(), pas.padding.target.bottom(), padding_t), + util::interpolate(pas.padding.current.right(), pas.padding.target.right(), padding_t)}); + if (animationTransitionFrame(pas.padding.pa, padding_t)) { + pas.padding.set = false; + } + } - transitionFrameFn = nullptr; - transitionFinishFn = nullptr; + double maxPitch = getMaxPitchForEdgeInsets(state.getEdgeInsets()); + if (pas.pitch.set || maxPitch < pas.pitch.current) { + double pitch_t = pas.pitch.pa->t(now); + state.setPitch(std::min(maxPitch, util::interpolate(pas.pitch.current, pas.pitch.target, pitch_t))); + if (animationTransitionFrame(pas.pitch.pa, pitch_t)) { + pas.pitch.set = false; + } + } - update(Clock::now()); - finish(); - } -} + if (pas.anchor) { + state.moveLatLng(pas.anchorLatLng, *pas.anchor); + } -bool Transform::inTransition() const { - return transitionFrameFn != nullptr; -} + visit_pas([&](std::shared_ptr& pa) { + if (pa) { + if (pa->done) animationFinishFrame(pa); + pa->ran = false; + } + }); -void Transform::updateTransitions(const TimePoint& now) { - // Use a temporary function to ensure that the transitionFrameFn lambda is - // called only once per update. - - // This addresses the symptoms of - // https://github.com/mapbox/mapbox-gl-native/issues/11180 where setting a - // shape source to nil (or similar) in the `onCameraIsChanging` observer - // function causes `Map::Impl::onUpdate()` to be called which in turn calls - // this function (before the current iteration has completed), leading to an - // infinite loop. See https://github.com/mapbox/mapbox-gl-native/issues/5833 - // for a similar, related, issue. - // - // By temporarily nulling the `transitionFrameFn` (and then restoring it - // after the temporary has been called) we stop this recursion. - // - // It's important to note that the scope of this change is stop the above - // crashes. It doesn't address any potential deeper issue (for example - // user error, how often and when transition callbacks are called). - - auto transition = std::move(transitionFrameFn); - transitionFrameFn = nullptr; - - if (transition && transition(now)) { - // If the transition indicates that it is complete, then we should call - // the finish lambda (going via a temporary as above) - auto finish = std::move(transitionFinishFn); - - transitionFinishFn = nullptr; - transitionFrameFn = nullptr; - - if (finish) { - finish(); - } - } else if (!transitionFrameFn) { - // We have to check `transitionFrameFn` is nil here, since a new - // transition may have been triggered in a user callback (from the - // transition call above) - transitionFrameFn = std::move(transition); + activeAnimation = false; } } void Transform::cancelTransitions() { - if (transitionFinishFn) { - transitionFinishFn(); - } + visit_pas([this](std::shared_ptr& pa) { animationFinishFrame(pa); }); - transitionFrameFn = nullptr; - transitionFinishFn = nullptr; + pas = {}; + activeAnimation = false; } void Transform::setGestureInProgress(bool inProgress) { diff --git a/src/mbgl/map/transform.hpp b/src/mbgl/map/transform.hpp index 4828f372f686..be416458d867 100644 --- a/src/mbgl/map/transform.hpp +++ b/src/mbgl/map/transform.hpp @@ -16,6 +16,29 @@ namespace mbgl { +template +struct TransformProperty { + std::shared_ptr pa; + T current, target; + bool set = false; + + std::function frameLatLngFunc = nullptr; + std::function frameZoomFunc = nullptr; + + // Anchor + std::optional anchor = std::nullopt; + LatLng anchorLatLng = {}; +}; + +struct PropertyAnimations { + TransformProperty> latlng; + TransformProperty zoom, bearing, pitch; + TransformProperty padding; + + // Anchor + std::optional anchor; + LatLng anchorLatLng; + class TransformObserver { public: virtual ~TransformObserver() = default; @@ -136,18 +159,26 @@ class Transform : private util::noncopyable { TransformObserver& observer; TransformState state; - void startTransition(const CameraOptions&, - const AnimationOptions&, - const std::function&, - const Duration&); + void startTransition(const CameraOptions&, const Duration&); + bool animationTransitionFrame(std::shared_ptr&, double); + void animationFinishFrame(std::shared_ptr&); + + void visit_pas(const std::function&)>& f) { + f(pas.latlng.pa); + f(pas.zoom.pa); + f(pas.bearing.pa); + f(pas.pitch.pa); + f(pas.padding.pa); + } // We don't want to show horizon: limit max pitch based on edge insets. double getMaxPitchForEdgeInsets(const EdgeInsets& insets) const; + PropertyAnimations pas; + bool activeAnimation = false; + TimePoint transitionStart; Duration transitionDuration; - std::function transitionFrameFn; - std::function transitionFinishFn; }; } // namespace mbgl From 15f5534eece8dc1090f44a50e7e56e998c09bbf5 Mon Sep 17 00:00:00 2001 From: Yousif Aldolaijan Date: Mon, 23 Jun 2025 15:04:41 +0300 Subject: [PATCH 02/15] cleanup code and make things more private --- include/mbgl/map/camera.hpp | 30 ----- src/mbgl/map/transform.cpp | 244 ++++++++++++++++++++---------------- src/mbgl/map/transform.hpp | 86 ++++++++----- 3 files changed, 192 insertions(+), 168 deletions(-) diff --git a/include/mbgl/map/camera.hpp b/include/mbgl/map/camera.hpp index 1361ac067ca7..3bdacf5e8bed 100644 --- a/include/mbgl/map/camera.hpp +++ b/include/mbgl/map/camera.hpp @@ -156,34 +156,4 @@ struct FreeCameraOptions { void setPitchBearing(double pitch, double bearing) noexcept; }; -struct PropertyAnimation { - TimePoint start; - Duration duration; - AnimationOptions animation; - bool ran = false, finished = false, done = false; - bool panning = false, scaling = false, rotating = false; - - PropertyAnimation( - TimePoint start_, Duration duration_, AnimationOptions animation_, bool panning_, bool scaling_, bool rotating_) - : start(start_), - duration(duration_), - animation(animation_), - panning(panning_), - scaling(scaling_), - rotating(rotating_) {} - - double t(TimePoint now) { - bool isAnimated = duration != Duration::zero(); - double t = isAnimated ? (std::chrono::duration(now - start) / duration) : 1.0f; - if (t >= 1.0) { - return 1.0; - } - - util::UnitBezier ease = animation.easing ? *animation.easing : util::DEFAULT_TRANSITION_EASE; - return ease.solve(t, 0.001); - } - - bool isAnimated() const { return duration != Duration::zero(); } -}; - } // namespace mbgl diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index 26a04a23d6e4..b1ca8a9e01e9 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -160,63 +160,61 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& const double startPitch = state.getPitch(); const EdgeInsets startEdgeInsets = state.getEdgeInsets(); - auto pa = std::make_shared( + auto propertyAnimation = std::make_shared( Clock::now(), duration, animation, unwrappedLatLng != startLatLng, zoom != startZoom, bearing != startBearing); // NOTE: For tests only - transitionStart = pa->start; - transitionDuration = pa->duration; + transitionStart = propertyAnimation->start; + transitionDuration = propertyAnimation->duration; - if (!pas.zoom.set || startZoom != zoom) { - animationFinishFrame(pas.zoom.pa); - pas.zoom = { - .pa = pa, + if (!propertyAnimations.zoom.set || startZoom != zoom) { + animationFinishFrame(propertyAnimations.zoom.propertyAnimation); + propertyAnimations.zoom = { + .propertyAnimation = propertyAnimation, .current = startZoom, .target = zoom, .set = true, .frameZoomFunc = - [=, this](TimePoint now) { - return util::interpolate(pas.zoom.current, pas.zoom.target, pas.zoom.pa->t(now)); - }, + [=](TimePoint now) { return util::interpolate(startZoom, zoom, propertyAnimation->interpolant(now)); }, }; } - if (!pas.latlng.set || startPoint != endPoint) { - animationFinishFrame(pas.latlng.pa); - pas.latlng = { - .pa = pa, + if (!propertyAnimations.latlng.set || startPoint != endPoint) { + animationFinishFrame(propertyAnimations.latlng.propertyAnimation); + propertyAnimations.latlng = { + .propertyAnimation = propertyAnimation, .current = startPoint, .target = endPoint, .set = true, .frameLatLngFunc = [=, this](TimePoint now) { Point framePoint = util::interpolate( - pas.latlng.current, pas.latlng.target, pas.latlng.pa->t(now)); + startPoint, endPoint, propertyAnimation->interpolant(now)); return Projection::unproject(framePoint, state.zoomScale(startZoom)); }, }; } - if (!pas.bearing.set || bearing != startBearing) { - animationFinishFrame(pas.bearing.pa); - pas.bearing = { - .pa = pa, + if (!propertyAnimations.bearing.set || bearing != startBearing) { + animationFinishFrame(propertyAnimations.bearing.propertyAnimation); + propertyAnimations.bearing = { + .propertyAnimation = propertyAnimation, .current = startBearing, .target = bearing, .set = true, }; } - if (!pas.padding.set || padding != startEdgeInsets) { - animationFinishFrame(pas.padding.pa); - pas.padding = { - .pa = pa, + if (!propertyAnimations.padding.set || padding != startEdgeInsets) { + animationFinishFrame(propertyAnimations.padding.propertyAnimation); + propertyAnimations.padding = { + .propertyAnimation = propertyAnimation, .current = startEdgeInsets, .target = padding, .set = true, }; } - if (!pas.pitch.set || pitch != startPitch) { - animationFinishFrame(pas.pitch.pa); - pas.pitch = { - .pa = pa, + if (!propertyAnimations.pitch.set || pitch != startPitch) { + animationFinishFrame(propertyAnimations.pitch.propertyAnimation); + propertyAnimations.pitch = { + .propertyAnimation = propertyAnimation, .current = startPitch, .target = pitch, .set = true, @@ -360,70 +358,74 @@ void Transform::flyTo(const CameraOptions& inputCamera, const double startScale = state.getScale(); const EdgeInsets startEdgeInsets = state.getEdgeInsets(); - auto pa = std::make_shared( + auto propertyAnimation = std::make_shared( Clock::now(), duration, animation, true, true, bearing != startBearing); // NOTE: For tests only - transitionStart = pa->start; - transitionDuration = pa->duration; + transitionStart = propertyAnimation->start; + transitionDuration = propertyAnimation->duration; - if (!pas.zoom.set || startZoom != zoom) { - animationFinishFrame(pas.zoom.pa); - pas.zoom = { - .pa = pa, + if (!propertyAnimations.zoom.set || startZoom != zoom) { + animationFinishFrame(propertyAnimations.zoom.propertyAnimation); + propertyAnimations.zoom = { + .propertyAnimation = propertyAnimation, .current = startZoom, .target = zoom, .set = true, .frameZoomFunc = [=, this](TimePoint now) { - double t = pas.zoom.pa->t(now); + double t = propertyAnimations.zoom.propertyAnimation->interpolant(now); double s = t * S; - double frameZoom = linearZoomInterpolation ? util::interpolate(pas.zoom.current, pas.zoom.target, t) - : pas.zoom.current + state.scaleZoom(1 / w(s)); + double frameZoom = linearZoomInterpolation + ? util::interpolate( + propertyAnimations.zoom.current, propertyAnimations.zoom.target, t) + : propertyAnimations.zoom.current + state.scaleZoom(1 / w(s)); if (std::isnan(frameZoom)) { - frameZoom = pas.zoom.target; + frameZoom = propertyAnimations.zoom.target; } return frameZoom; }, }; } - if (!pas.latlng.set || startPoint != endPoint) { - animationFinishFrame(pas.latlng.pa); - pas.latlng = { - .pa = pa, + if (!propertyAnimations.latlng.set || startPoint != endPoint) { + animationFinishFrame(propertyAnimations.latlng.propertyAnimation); + propertyAnimations.latlng = { + .propertyAnimation = propertyAnimation, .current = startPoint, .target = endPoint, .set = true, .frameLatLngFunc = [=, this](TimePoint now) { - double t = pas.latlng.pa->t(now); + double t = propertyAnimations.latlng.propertyAnimation->interpolant(now); double s = t * S; double us = t == 1.0 ? 1.0 : u(s); - Point framePoint = util::interpolate(pas.latlng.current, pas.latlng.target, us); + Point framePoint = util::interpolate( + propertyAnimations.latlng.current, propertyAnimations.latlng.target, us); return Projection::unproject(framePoint, startScale); }, }; } - if (!pas.bearing.set || bearing != startBearing) { - animationFinishFrame(pas.bearing.pa); - pas.bearing = { - .pa = pa, + if (!propertyAnimations.bearing.set || bearing != startBearing) { + animationFinishFrame(propertyAnimations.bearing.propertyAnimation); + propertyAnimations.bearing = { + .propertyAnimation = propertyAnimation, .current = startBearing, .target = bearing, .set = true, }; } - if (!pas.padding.set || padding != startEdgeInsets) { - animationFinishFrame(pas.padding.pa); - pas.padding = {.pa = pa, .current = startEdgeInsets, .target = padding, .set = true}; + if (!propertyAnimations.padding.set || padding != startEdgeInsets) { + animationFinishFrame(propertyAnimations.padding.propertyAnimation); + propertyAnimations.padding = { + .propertyAnimation = propertyAnimation, .current = startEdgeInsets, .target = padding, .set = true}; } - if (!pas.pitch.set || pitch != startPitch) { - animationFinishFrame(pas.pitch.pa); - pas.pitch = { - .pa = pa, + if (!propertyAnimations.pitch.set || pitch != startPitch) { + animationFinishFrame(propertyAnimations.pitch.propertyAnimation); + propertyAnimations.pitch = { + .propertyAnimation = propertyAnimation, .current = startPitch, .target = pitch, .set = true, @@ -433,24 +435,24 @@ void Transform::flyTo(const CameraOptions& inputCamera, startTransition(camera, duration); } -bool Transform::animationTransitionFrame(std::shared_ptr& pa, double t) { - if (pa->ran) { - return pa->done; +bool Transform::animationTransitionFrame(std::shared_ptr& propertyAnimation, double t) { + if (propertyAnimation->ran) { + return propertyAnimation->done; } - pa->ran = true; + propertyAnimation->ran = true; if (t < 1.0) { - if (pa->animation.transitionFrameFn) { - pa->animation.transitionFrameFn(t); + if (propertyAnimation->animation.transitionFrameFn) { + propertyAnimation->animation.transitionFrameFn(t); } observer.onCameraIsChanging(); - pa->done = false; + propertyAnimation->done = false; } else { - pa->done = true; + propertyAnimation->done = true; } - return pa->done; + return propertyAnimation->done; } // MARK: - Position @@ -625,10 +627,10 @@ void Transform::startTransition(const CameraOptions& camera, const Duration& dur // Associate the anchor, if given, with a coordinate. // Anchor and center points are mutually exclusive, with preference for the // center point when both are set. - pas.anchor = camera.center ? std::nullopt : camera.anchor; - if (pas.anchor) { - pas.anchor->y = state.getSize().height - pas.anchor->y; - pas.anchorLatLng = state.screenCoordinateToLatLng(*pas.anchor); + propertyAnimations.anchor = camera.center ? std::nullopt : camera.anchor; + if (propertyAnimations.anchor) { + propertyAnimations.anchor->y = state.getSize().height - propertyAnimations.anchor->y; + propertyAnimations.anchorLatLng = state.screenCoordinateToLatLng(*propertyAnimations.anchor); } if (!isAnimated) { @@ -638,7 +640,8 @@ void Transform::startTransition(const CameraOptions& camera, const Duration& dur } bool Transform::inTransition() const { - return pas.latlng.set || pas.zoom.set || pas.bearing.set || pas.padding.set || pas.pitch.set; + return propertyAnimations.latlng.set || propertyAnimations.zoom.set || propertyAnimations.bearing.set || + propertyAnimations.padding.set || propertyAnimations.pitch.set; } void Transform::updateTransitions(const TimePoint& now) { @@ -646,11 +649,11 @@ void Transform::updateTransitions(const TimePoint& now) { activeAnimation = true; bool panning = false, scaling = false, rotating = false; - visit_pas([&](std::shared_ptr& pa) { - if (pa) { - panning |= pa->panning; - scaling |= pa->scaling; - rotating |= pa->rotating; + visitPropertyAnimations([&](std::shared_ptr& propertyAnimation) { + if (propertyAnimation) { + panning |= propertyAnimation->panning; + scaling |= propertyAnimation->scaling; + rotating |= propertyAnimation->rotating; } }); @@ -659,56 +662,69 @@ void Transform::updateTransitions(const TimePoint& now) { .withScalingInProgress(scaling) .withRotatingInProgress(rotating)); - if (pas.latlng.frameLatLngFunc && pas.zoom.frameZoomFunc) { - if (pas.latlng.set || pas.zoom.set) { - state.setLatLngZoom(pas.latlng.frameLatLngFunc(now), pas.zoom.frameZoomFunc(now)); - if (animationTransitionFrame(pas.latlng.pa, pas.latlng.pa->t(now))) { - pas.latlng.set = false; + if (propertyAnimations.latlng.frameLatLngFunc && propertyAnimations.zoom.frameZoomFunc) { + if (propertyAnimations.latlng.set || propertyAnimations.zoom.set) { + state.setLatLngZoom(propertyAnimations.latlng.frameLatLngFunc(now), + propertyAnimations.zoom.frameZoomFunc(now)); + if (animationTransitionFrame(propertyAnimations.latlng.propertyAnimation, + propertyAnimations.latlng.propertyAnimation->interpolant(now))) { + propertyAnimations.latlng.set = false; } - if (animationTransitionFrame(pas.zoom.pa, pas.zoom.pa->t(now))) { - pas.zoom.set = false; + if (animationTransitionFrame(propertyAnimations.zoom.propertyAnimation, + propertyAnimations.zoom.propertyAnimation->interpolant(now))) { + propertyAnimations.zoom.set = false; } } } - if (pas.bearing.set) { - double bearing_t = pas.bearing.pa->t(now); - state.setBearing( - util::wrap(util::interpolate(pas.bearing.current, pas.bearing.target, bearing_t), -pi, pi)); - if (animationTransitionFrame(pas.bearing.pa, bearing_t)) { - pas.bearing.set = false; + if (propertyAnimations.bearing.set) { + double bearing_t = propertyAnimations.bearing.propertyAnimation->interpolant(now); + state.setBearing(util::wrap( + util::interpolate(propertyAnimations.bearing.current, propertyAnimations.bearing.target, bearing_t), + -pi, + pi)); + if (animationTransitionFrame(propertyAnimations.bearing.propertyAnimation, bearing_t)) { + propertyAnimations.bearing.set = false; } } - if (pas.padding.set) { - double padding_t = pas.padding.pa->t(now); + if (propertyAnimations.padding.set) { + double padding_t = propertyAnimations.padding.propertyAnimation->interpolant(now); state.setEdgeInsets( - {util::interpolate(pas.padding.current.top(), pas.padding.target.top(), padding_t), - util::interpolate(pas.padding.current.left(), pas.padding.target.left(), padding_t), - util::interpolate(pas.padding.current.bottom(), pas.padding.target.bottom(), padding_t), - util::interpolate(pas.padding.current.right(), pas.padding.target.right(), padding_t)}); - if (animationTransitionFrame(pas.padding.pa, padding_t)) { - pas.padding.set = false; + {util::interpolate( + propertyAnimations.padding.current.top(), propertyAnimations.padding.target.top(), padding_t), + util::interpolate( + propertyAnimations.padding.current.left(), propertyAnimations.padding.target.left(), padding_t), + util::interpolate(propertyAnimations.padding.current.bottom(), + propertyAnimations.padding.target.bottom(), + padding_t), + util::interpolate(propertyAnimations.padding.current.right(), + propertyAnimations.padding.target.right(), + padding_t)}); + if (animationTransitionFrame(propertyAnimations.padding.propertyAnimation, padding_t)) { + propertyAnimations.padding.set = false; } } double maxPitch = getMaxPitchForEdgeInsets(state.getEdgeInsets()); - if (pas.pitch.set || maxPitch < pas.pitch.current) { - double pitch_t = pas.pitch.pa->t(now); - state.setPitch(std::min(maxPitch, util::interpolate(pas.pitch.current, pas.pitch.target, pitch_t))); - if (animationTransitionFrame(pas.pitch.pa, pitch_t)) { - pas.pitch.set = false; + if (propertyAnimations.pitch.set || maxPitch < propertyAnimations.pitch.current) { + double pitch_t = propertyAnimations.pitch.propertyAnimation->interpolant(now); + state.setPitch(std::min( + maxPitch, + util::interpolate(propertyAnimations.pitch.current, propertyAnimations.pitch.target, pitch_t))); + if (animationTransitionFrame(propertyAnimations.pitch.propertyAnimation, pitch_t)) { + propertyAnimations.pitch.set = false; } } - if (pas.anchor) { - state.moveLatLng(pas.anchorLatLng, *pas.anchor); + if (propertyAnimations.anchor) { + state.moveLatLng(propertyAnimations.anchorLatLng, *propertyAnimations.anchor); } - visit_pas([&](std::shared_ptr& pa) { - if (pa) { - if (pa->done) animationFinishFrame(pa); - pa->ran = false; + visitPropertyAnimations([&](std::shared_ptr& propertyAnimation) { + if (propertyAnimation) { + if (propertyAnimation->done) animationFinishFrame(propertyAnimation); + propertyAnimation->ran = false; } }); @@ -717,9 +733,10 @@ void Transform::updateTransitions(const TimePoint& now) { } void Transform::cancelTransitions() { - visit_pas([this](std::shared_ptr& pa) { animationFinishFrame(pa); }); + visitPropertyAnimations( + [this](std::shared_ptr& propertyAnimation) { animationFinishFrame(propertyAnimation); }); - pas = {}; + propertyAnimations = {}; activeAnimation = false; } @@ -770,4 +787,15 @@ void Transform::setFreeCameraOptions(const FreeCameraOptions& options) { state.setFreeCameraOptions(options); } +double Transform::PropertyAnimation::interpolant(TimePoint now) { + bool isAnimated = duration != Duration::zero(); + double t = isAnimated ? (std::chrono::duration(now - start) / duration) : 1.0f; + if (t >= 1.0) { + return 1.0; + } + + util::UnitBezier ease = animation.easing ? *animation.easing : util::DEFAULT_TRANSITION_EASE; + return ease.solve(t, 0.001); +} + } // namespace mbgl diff --git a/src/mbgl/map/transform.hpp b/src/mbgl/map/transform.hpp index be416458d867..6965f47dea48 100644 --- a/src/mbgl/map/transform.hpp +++ b/src/mbgl/map/transform.hpp @@ -16,29 +16,6 @@ namespace mbgl { -template -struct TransformProperty { - std::shared_ptr pa; - T current, target; - bool set = false; - - std::function frameLatLngFunc = nullptr; - std::function frameZoomFunc = nullptr; - - // Anchor - std::optional anchor = std::nullopt; - LatLng anchorLatLng = {}; -}; - -struct PropertyAnimations { - TransformProperty> latlng; - TransformProperty zoom, bearing, pitch; - TransformProperty padding; - - // Anchor - std::optional anchor; - LatLng anchorLatLng; - class TransformObserver { public: virtual ~TransformObserver() = default; @@ -155,7 +132,56 @@ class Transform : private util::noncopyable { FreeCameraOptions getFreeCameraOptions() const; void setFreeCameraOptions(const FreeCameraOptions& options); + struct PropertyAnimation { + TimePoint start; + Duration duration; + AnimationOptions animation; + bool ran = false, finished = false, done = false; + bool panning = false, scaling = false, rotating = false; + + PropertyAnimation(TimePoint start_, + Duration duration_, + AnimationOptions animation_, + bool panning_, + bool scaling_, + bool rotating_) + : start(start_), + duration(duration_), + animation(animation_), + panning(panning_), + scaling(scaling_), + rotating(rotating_) {} + + double interpolant(TimePoint); + + bool isAnimated() const { return duration != Duration::zero(); } + }; + private: + template + struct TransformProperty { + std::shared_ptr propertyAnimation; + T current, target; + bool set = false; + + std::function frameLatLngFunc = nullptr; + std::function frameZoomFunc = nullptr; + + // Anchor + std::optional anchor = std::nullopt; + LatLng anchorLatLng = {}; + }; + + struct PropertyAnimations { + TransformProperty> latlng; + TransformProperty zoom, bearing, pitch; + TransformProperty padding; + + // Anchor + std::optional anchor; + LatLng anchorLatLng; + }; + TransformObserver& observer; TransformState state; @@ -163,18 +189,18 @@ class Transform : private util::noncopyable { bool animationTransitionFrame(std::shared_ptr&, double); void animationFinishFrame(std::shared_ptr&); - void visit_pas(const std::function&)>& f) { - f(pas.latlng.pa); - f(pas.zoom.pa); - f(pas.bearing.pa); - f(pas.pitch.pa); - f(pas.padding.pa); + void visitPropertyAnimations(const std::function&)>& f) { + f(propertyAnimations.latlng.propertyAnimation); + f(propertyAnimations.zoom.propertyAnimation); + f(propertyAnimations.bearing.propertyAnimation); + f(propertyAnimations.pitch.propertyAnimation); + f(propertyAnimations.padding.propertyAnimation); } // We don't want to show horizon: limit max pitch based on edge insets. double getMaxPitchForEdgeInsets(const EdgeInsets& insets) const; - PropertyAnimations pas; + PropertyAnimations propertyAnimations; bool activeAnimation = false; TimePoint transitionStart; From b6ad42b53acf42a8c85f5730f6902ddffedaed2b Mon Sep 17 00:00:00 2001 From: Yousif Aldolaijan Date: Thu, 26 Jun 2025 16:54:24 +0300 Subject: [PATCH 03/15] handle panning, rotating, and scale state correctly during concurrent transforms --- src/mbgl/map/transform.cpp | 18 ++++++++++++++++-- src/mbgl/map/transform.hpp | 6 ++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index b1ca8a9e01e9..dc3e548ad6ee 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -650,7 +650,7 @@ void Transform::updateTransitions(const TimePoint& now) { bool panning = false, scaling = false, rotating = false; visitPropertyAnimations([&](std::shared_ptr& propertyAnimation) { - if (propertyAnimation) { + if (propertyAnimation && !propertyAnimation->done) { panning |= propertyAnimation->panning; scaling |= propertyAnimation->scaling; rotating |= propertyAnimation->rotating; @@ -721,13 +721,27 @@ void Transform::updateTransitions(const TimePoint& now) { state.moveLatLng(propertyAnimations.anchorLatLng, *propertyAnimations.anchor); } + panning = false; + scaling = false; + rotating = false; visitPropertyAnimations([&](std::shared_ptr& propertyAnimation) { if (propertyAnimation) { - if (propertyAnimation->done) animationFinishFrame(propertyAnimation); + if (propertyAnimation->done) { + animationFinishFrame(propertyAnimation); + } else { + panning |= propertyAnimation->panning; + scaling |= propertyAnimation->scaling; + rotating |= propertyAnimation->rotating; + } propertyAnimation->ran = false; } }); + state.setProperties(TransformStateProperties() + .withPanningInProgress(panning) + .withScalingInProgress(scaling) + .withRotatingInProgress(rotating)); + activeAnimation = false; } } diff --git a/src/mbgl/map/transform.hpp b/src/mbgl/map/transform.hpp index 6965f47dea48..8ecb1c89da2b 100644 --- a/src/mbgl/map/transform.hpp +++ b/src/mbgl/map/transform.hpp @@ -136,7 +136,9 @@ class Transform : private util::noncopyable { TimePoint start; Duration duration; AnimationOptions animation; - bool ran = false, finished = false, done = false; + bool ran = false; // Did this property animation run this frame + bool finished = false; // Did we execute the finish frame for this property animation this frame + bool done = false; // Did this property animation reach the end of the frame bool panning = false, scaling = false, rotating = false; PropertyAnimation(TimePoint start_, @@ -181,7 +183,7 @@ class Transform : private util::noncopyable { std::optional anchor; LatLng anchorLatLng; }; - + TransformObserver& observer; TransformState state; From c220b3dc239dd004440bbdd5508dc074b53139bd Mon Sep 17 00:00:00 2001 From: Yousif Aldolaijan Date: Thu, 26 Jun 2025 16:54:49 +0300 Subject: [PATCH 04/15] add toggle for ignored failed tests to results viewer --- render-test/parser.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/render-test/parser.cpp b/render-test/parser.cpp index 1bd2ad13886f..9f0d10ad66c4 100644 --- a/render-test/parser.cpp +++ b/render-test/parser.cpp @@ -90,6 +90,11 @@ document.getElementById('toggle-ignored').addEventListener('click', function (e) row.classList.toggle('hide'); } }); +document.getElementById('toggle-ignored-failed').addEventListener('click', function (e) { + for (const row of document.querySelectorAll('.test.ignored, .test.failed')) { + row.classList.toggle('hide'); + } +}); document.getElementById('toggle-sequence').addEventListener('click', function (e) { document.getElementById('test-sequence').classList.toggle('hide'); }); @@ -100,6 +105,7 @@ const char* resultsHeaderButtons = R"HTML( + )HTML"; From 003eeaeae36258c454caf629bc4d63d7232ce111 Mon Sep 17 00:00:00 2001 From: Yousif Aldolaijan Date: Tue, 1 Jul 2025 12:34:36 +0300 Subject: [PATCH 05/15] resolve all test and naming issues with the new transform implementation --- src/mbgl/map/transform.cpp | 323 ++++++++++++++++++------------------- src/mbgl/map/transform.hpp | 62 ++++--- 2 files changed, 185 insertions(+), 200 deletions(-) diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index dc3e548ad6ee..dc3d47f59437 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -103,13 +103,13 @@ void Transform::jumpTo(const CameraOptions& camera) { * smooth animation between old and new values. The map will retain the current * values for any options not included in `options`. */ -void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& animation) { +void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& options) { CameraOptions camera = inputCamera; - Duration duration = animation.duration.value_or(Duration::zero()); + Duration duration = options.duration.value_or(Duration::zero()); if (state.getLatLngBounds() == LatLngBounds() && !isGestureInProgress() && duration != Duration::zero()) { // reuse flyTo, without exaggerated animation, to achieve constant ground speed. - return flyTo(camera, animation, true); + return flyTo(camera, options, true); } double zoom = camera.zoom.value_or(getZoom()); @@ -124,8 +124,8 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& double pitch = camera.pitch ? util::deg2rad(*camera.pitch) : getPitch(); if (std::isnan(zoom) || std::isnan(bearing) || std::isnan(pitch)) { - if (animation.transitionFinishFn) { - animation.transitionFinishFn(); + if (options.transitionFinishFn) { + options.transitionFinishFn(); } return; } @@ -160,68 +160,67 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& const double startPitch = state.getPitch(); const EdgeInsets startEdgeInsets = state.getEdgeInsets(); - auto propertyAnimation = std::make_shared( - Clock::now(), duration, animation, unwrappedLatLng != startLatLng, zoom != startZoom, bearing != startBearing); + auto animation = std::make_shared( + Clock::now(), duration, options, unwrappedLatLng != startLatLng, zoom != startZoom, bearing != startBearing); // NOTE: For tests only - transitionStart = propertyAnimation->start; - transitionDuration = propertyAnimation->duration; + transitionStart = animation->start; + transitionDuration = animation->duration; - if (!propertyAnimations.zoom.set || startZoom != zoom) { - animationFinishFrame(propertyAnimations.zoom.propertyAnimation); - propertyAnimations.zoom = { - .propertyAnimation = propertyAnimation, + if (!properties.zoom.set || startZoom != zoom) { + animationFinishFrame(properties.zoom.animation); + properties.zoom = { + .animation = animation, .current = startZoom, .target = zoom, .set = true, .frameZoomFunc = - [=](TimePoint now) { return util::interpolate(startZoom, zoom, propertyAnimation->interpolant(now)); }, + [=](TimePoint now) { return util::interpolate(startZoom, zoom, animation->interpolant(now)); }, }; } - if (!propertyAnimations.latlng.set || startPoint != endPoint) { - animationFinishFrame(propertyAnimations.latlng.propertyAnimation); - propertyAnimations.latlng = { - .propertyAnimation = propertyAnimation, + if (!properties.latlng.set || startPoint != endPoint) { + animationFinishFrame(properties.latlng.animation); + properties.latlng = { + .animation = animation, .current = startPoint, .target = endPoint, .set = true, .frameLatLngFunc = [=, this](TimePoint now) { - Point framePoint = util::interpolate( - startPoint, endPoint, propertyAnimation->interpolant(now)); + Point framePoint = util::interpolate(startPoint, endPoint, animation->interpolant(now)); return Projection::unproject(framePoint, state.zoomScale(startZoom)); }, }; } - if (!propertyAnimations.bearing.set || bearing != startBearing) { - animationFinishFrame(propertyAnimations.bearing.propertyAnimation); - propertyAnimations.bearing = { - .propertyAnimation = propertyAnimation, + if (!properties.bearing.set || bearing != startBearing) { + animationFinishFrame(properties.bearing.animation); + properties.bearing = { + .animation = animation, .current = startBearing, .target = bearing, .set = true, }; } - if (!propertyAnimations.padding.set || padding != startEdgeInsets) { - animationFinishFrame(propertyAnimations.padding.propertyAnimation); - propertyAnimations.padding = { - .propertyAnimation = propertyAnimation, + if (!properties.padding.set || padding != startEdgeInsets) { + animationFinishFrame(properties.padding.animation); + properties.padding = { + .animation = animation, .current = startEdgeInsets, .target = padding, .set = true, }; } - if (!propertyAnimations.pitch.set || pitch != startPitch) { - animationFinishFrame(propertyAnimations.pitch.propertyAnimation); - propertyAnimations.pitch = { - .propertyAnimation = propertyAnimation, + if (!properties.pitch.set || pitch != startPitch) { + animationFinishFrame(properties.pitch.animation); + properties.pitch = { + .animation = animation, .current = startPitch, .target = pitch, .set = true, }; } - startTransition(camera, duration); + startTransition(camera, duration, animation); } /** This method implements an “optimal path” animation, as detailed in: @@ -232,9 +231,7 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& Where applicable, local variable documentation begins with the associated variable or function in van Wijk (2003). */ -void Transform::flyTo(const CameraOptions& inputCamera, - const AnimationOptions& animation, - bool linearZoomInterpolation) { +void Transform::flyTo(const CameraOptions& inputCamera, const AnimationOptions& options, bool linearZoomInterpolation) { CameraOptions camera = inputCamera; double zoom = camera.zoom.value_or(getZoom()); @@ -247,8 +244,8 @@ void Transform::flyTo(const CameraOptions& inputCamera, double pitch = camera.pitch ? util::deg2rad(*camera.pitch) : getPitch(); if (std::isnan(zoom) || std::isnan(bearing) || std::isnan(pitch) || state.getSize().isEmpty()) { - if (animation.transitionFinishFn) { - animation.transitionFinishFn(); + if (options.transitionFinishFn) { + options.transitionFinishFn(); } return; } @@ -292,8 +289,8 @@ void Transform::flyTo(const CameraOptions& inputCamera, root mean squared average velocity, VRMS. A value of 1 produces a circular motion. */ double rho = 1.42; - if (animation.minZoom || linearZoomInterpolation) { - double minZoom = util::min(animation.minZoom.value_or(startZoom), startZoom, zoom); + if (options.minZoom || linearZoomInterpolation) { + double minZoom = util::min(options.minZoom.value_or(startZoom), startZoom, zoom); minZoom = util::clamp(minZoom, state.getMinZoom(), state.getMaxZoom()); /// wm: Maximum visible span, measured in pixels with respect /// to the initial scale. @@ -336,21 +333,21 @@ void Transform::flyTo(const CameraOptions& inputCamera, double S = (isClose ? (std::abs(std::log(w1 / w0)) / rho) : ((r1 - r0) / rho)); Duration duration; - if (animation.duration) { - duration = *animation.duration; + if (options.duration) { + duration = *options.duration; } else { /// V: Average velocity, measured in ρ-screenfuls per second. double velocity = 1.2; - if (animation.velocity) { - velocity = *animation.velocity / rho; + if (options.velocity) { + velocity = *options.velocity / rho; } duration = std::chrono::duration_cast(std::chrono::duration(S / velocity)); } if (duration == Duration::zero()) { // Perform an instantaneous transition. jumpTo(camera); - if (animation.transitionFinishFn) { - animation.transitionFinishFn(); + if (options.transitionFinishFn) { + options.transitionFinishFn(); } return; } @@ -358,101 +355,97 @@ void Transform::flyTo(const CameraOptions& inputCamera, const double startScale = state.getScale(); const EdgeInsets startEdgeInsets = state.getEdgeInsets(); - auto propertyAnimation = std::make_shared( - Clock::now(), duration, animation, true, true, bearing != startBearing); + auto animation = std::make_shared(Clock::now(), duration, options, true, true, bearing != startBearing); // NOTE: For tests only - transitionStart = propertyAnimation->start; - transitionDuration = propertyAnimation->duration; + transitionStart = animation->start; + transitionDuration = animation->duration; - if (!propertyAnimations.zoom.set || startZoom != zoom) { - animationFinishFrame(propertyAnimations.zoom.propertyAnimation); - propertyAnimations.zoom = { - .propertyAnimation = propertyAnimation, + if (!properties.zoom.set || startZoom != zoom) { + animationFinishFrame(properties.zoom.animation); + properties.zoom = { + .animation = animation, .current = startZoom, .target = zoom, .set = true, .frameZoomFunc = [=, this](TimePoint now) { - double t = propertyAnimations.zoom.propertyAnimation->interpolant(now); + double t = properties.zoom.animation->interpolant(now); double s = t * S; - double frameZoom = linearZoomInterpolation - ? util::interpolate( - propertyAnimations.zoom.current, propertyAnimations.zoom.target, t) - : propertyAnimations.zoom.current + state.scaleZoom(1 / w(s)); + double frameZoom = linearZoomInterpolation ? util::interpolate(startZoom, zoom, t) + : startZoom + state.scaleZoom(1 / w(s)); if (std::isnan(frameZoom)) { - frameZoom = propertyAnimations.zoom.target; + frameZoom = zoom; } return frameZoom; }, }; } - if (!propertyAnimations.latlng.set || startPoint != endPoint) { - animationFinishFrame(propertyAnimations.latlng.propertyAnimation); - propertyAnimations.latlng = { - .propertyAnimation = propertyAnimation, + if (!properties.latlng.set || startPoint != endPoint) { + animationFinishFrame(properties.latlng.animation); + properties.latlng = { + .animation = animation, .current = startPoint, .target = endPoint, .set = true, .frameLatLngFunc = [=, this](TimePoint now) { - double t = propertyAnimations.latlng.propertyAnimation->interpolant(now); + double t = properties.latlng.animation->interpolant(now); double s = t * S; double us = t == 1.0 ? 1.0 : u(s); Point framePoint = util::interpolate( - propertyAnimations.latlng.current, propertyAnimations.latlng.target, us); + properties.latlng.current, properties.latlng.target, us); return Projection::unproject(framePoint, startScale); }, }; } - if (!propertyAnimations.bearing.set || bearing != startBearing) { - animationFinishFrame(propertyAnimations.bearing.propertyAnimation); - propertyAnimations.bearing = { - .propertyAnimation = propertyAnimation, + if (!properties.bearing.set || bearing != startBearing) { + animationFinishFrame(properties.bearing.animation); + properties.bearing = { + .animation = animation, .current = startBearing, .target = bearing, .set = true, }; } - if (!propertyAnimations.padding.set || padding != startEdgeInsets) { - animationFinishFrame(propertyAnimations.padding.propertyAnimation); - propertyAnimations.padding = { - .propertyAnimation = propertyAnimation, .current = startEdgeInsets, .target = padding, .set = true}; + if (!properties.padding.set || padding != startEdgeInsets) { + animationFinishFrame(properties.padding.animation); + properties.padding = {.animation = animation, .current = startEdgeInsets, .target = padding, .set = true}; } - if (!propertyAnimations.pitch.set || pitch != startPitch) { - animationFinishFrame(propertyAnimations.pitch.propertyAnimation); - propertyAnimations.pitch = { - .propertyAnimation = propertyAnimation, + if (!properties.pitch.set || pitch != startPitch) { + animationFinishFrame(properties.pitch.animation); + properties.pitch = { + .animation = animation, .current = startPitch, .target = pitch, .set = true, }; } - startTransition(camera, duration); + startTransition(camera, duration, animation); } -bool Transform::animationTransitionFrame(std::shared_ptr& propertyAnimation, double t) { - if (propertyAnimation->ran) { - return propertyAnimation->done; +bool Transform::animationTransitionFrame(std::shared_ptr& animation, double t) { + if (animation->ran) { + return animation->done; } - propertyAnimation->ran = true; + animation->ran = true; if (t < 1.0) { - if (propertyAnimation->animation.transitionFrameFn) { - propertyAnimation->animation.transitionFrameFn(t); + if (animation->options.transitionFrameFn) { + animation->options.transitionFrameFn(t); } observer.onCameraIsChanging(); - propertyAnimation->done = false; + animation->done = false; } else { - propertyAnimation->done = true; + animation->done = true; } - return propertyAnimation->done; + return animation->done; } // MARK: - Position @@ -604,22 +597,26 @@ ProjectionMode Transform::getProjectionMode() const { // MARK: - Transition -void Transform::animationFinishFrame(std::shared_ptr& pa) { - if (!pa || pa->finished) { +void Transform::animationFinishFrame(std::shared_ptr& animation) { + if (!animation || animation->finished) { return; } - if (pa->animation.transitionFinishFn) { - pa->animation.transitionFinishFn(); + if (animation->options.transitionFinishFn) { + animation->options.transitionFinishFn(); } - pa->finished = true; + animation->finished = true; + + if (animation->anchor) animation->anchor = std::nullopt; - observer.onCameraDidChange(pa->isAnimated() ? MapObserver::CameraChangeMode::Animated - : MapObserver::CameraChangeMode::Immediate); + observer.onCameraDidChange(animation->isAnimated() ? MapObserver::CameraChangeMode::Animated + : MapObserver::CameraChangeMode::Immediate); } -void Transform::startTransition(const CameraOptions& camera, const Duration& duration) { +void Transform::startTransition(const CameraOptions& camera, + const Duration& duration, + std::shared_ptr& animation) { bool isAnimated = duration != Duration::zero(); observer.onCameraWillChange(isAnimated ? MapObserver::CameraChangeMode::Animated : MapObserver::CameraChangeMode::Immediate); @@ -627,10 +624,10 @@ void Transform::startTransition(const CameraOptions& camera, const Duration& dur // Associate the anchor, if given, with a coordinate. // Anchor and center points are mutually exclusive, with preference for the // center point when both are set. - propertyAnimations.anchor = camera.center ? std::nullopt : camera.anchor; - if (propertyAnimations.anchor) { - propertyAnimations.anchor->y = state.getSize().height - propertyAnimations.anchor->y; - propertyAnimations.anchorLatLng = state.screenCoordinateToLatLng(*propertyAnimations.anchor); + if (!camera.center && camera.anchor) { + animation->anchor = camera.anchor; + animation->anchor->y = state.getSize().height - animation->anchor->y; + animation->anchorLatLng = state.screenCoordinateToLatLng(*animation->anchor); } if (!isAnimated) { @@ -640,8 +637,8 @@ void Transform::startTransition(const CameraOptions& camera, const Duration& dur } bool Transform::inTransition() const { - return propertyAnimations.latlng.set || propertyAnimations.zoom.set || propertyAnimations.bearing.set || - propertyAnimations.padding.set || propertyAnimations.pitch.set; + return properties.latlng.set || properties.zoom.set || properties.bearing.set || properties.padding.set || + properties.pitch.set; } void Transform::updateTransitions(const TimePoint& now) { @@ -649,11 +646,11 @@ void Transform::updateTransitions(const TimePoint& now) { activeAnimation = true; bool panning = false, scaling = false, rotating = false; - visitPropertyAnimations([&](std::shared_ptr& propertyAnimation) { - if (propertyAnimation && !propertyAnimation->done) { - panning |= propertyAnimation->panning; - scaling |= propertyAnimation->scaling; - rotating |= propertyAnimation->rotating; + visitProperties([&](std::shared_ptr& animation) { + if (animation && !animation->done) { + panning |= animation->panning; + scaling |= animation->scaling; + rotating |= animation->rotating; } }); @@ -662,78 +659,71 @@ void Transform::updateTransitions(const TimePoint& now) { .withScalingInProgress(scaling) .withRotatingInProgress(rotating)); - if (propertyAnimations.latlng.frameLatLngFunc && propertyAnimations.zoom.frameZoomFunc) { - if (propertyAnimations.latlng.set || propertyAnimations.zoom.set) { - state.setLatLngZoom(propertyAnimations.latlng.frameLatLngFunc(now), - propertyAnimations.zoom.frameZoomFunc(now)); - if (animationTransitionFrame(propertyAnimations.latlng.propertyAnimation, - propertyAnimations.latlng.propertyAnimation->interpolant(now))) { - propertyAnimations.latlng.set = false; - } - if (animationTransitionFrame(propertyAnimations.zoom.propertyAnimation, - propertyAnimations.zoom.propertyAnimation->interpolant(now))) { - propertyAnimations.zoom.set = false; - } + bool zoomSet = properties.zoom.set && properties.zoom.animation; + if ((properties.latlng.set && properties.latlng.animation) || zoomSet) { + state.setLatLngZoom( + properties.latlng.frameLatLngFunc ? properties.latlng.frameLatLngFunc(now) : state.getLatLng(), + properties.zoom.frameZoomFunc ? properties.zoom.frameZoomFunc(now) : state.getZoom()); + if (properties.latlng.animation && + animationTransitionFrame(properties.latlng.animation, properties.latlng.animation->interpolant(now))) { + properties.latlng.set = false; + } + if (properties.zoom.animation && + animationTransitionFrame(properties.zoom.animation, properties.zoom.animation->interpolant(now))) { + properties.zoom.set = false; } - } - if (propertyAnimations.bearing.set) { - double bearing_t = propertyAnimations.bearing.propertyAnimation->interpolant(now); + if (zoomSet && properties.zoom.animation->anchor) { + state.moveLatLng(properties.zoom.animation->anchorLatLng, *properties.zoom.animation->anchor); + } + } + if (properties.bearing.set && properties.bearing.animation) { + double bearing_t = properties.bearing.animation->interpolant(now); state.setBearing(util::wrap( - util::interpolate(propertyAnimations.bearing.current, propertyAnimations.bearing.target, bearing_t), - -pi, - pi)); - if (animationTransitionFrame(propertyAnimations.bearing.propertyAnimation, bearing_t)) { - propertyAnimations.bearing.set = false; + util::interpolate(properties.bearing.current, properties.bearing.target, bearing_t), -pi, pi)); + if (animationTransitionFrame(properties.bearing.animation, bearing_t)) { + properties.bearing.set = false; } } - if (propertyAnimations.padding.set) { - double padding_t = propertyAnimations.padding.propertyAnimation->interpolant(now); + if (properties.padding.set && properties.padding.animation) { + double padding_t = properties.padding.animation->interpolant(now); state.setEdgeInsets( - {util::interpolate( - propertyAnimations.padding.current.top(), propertyAnimations.padding.target.top(), padding_t), - util::interpolate( - propertyAnimations.padding.current.left(), propertyAnimations.padding.target.left(), padding_t), - util::interpolate(propertyAnimations.padding.current.bottom(), - propertyAnimations.padding.target.bottom(), - padding_t), - util::interpolate(propertyAnimations.padding.current.right(), - propertyAnimations.padding.target.right(), - padding_t)}); - if (animationTransitionFrame(propertyAnimations.padding.propertyAnimation, padding_t)) { - propertyAnimations.padding.set = false; + {util::interpolate(properties.padding.current.top(), properties.padding.target.top(), padding_t), + util::interpolate(properties.padding.current.left(), properties.padding.target.left(), padding_t), + util::interpolate(properties.padding.current.bottom(), properties.padding.target.bottom(), padding_t), + util::interpolate(properties.padding.current.right(), properties.padding.target.right(), padding_t)}); + if (animationTransitionFrame(properties.padding.animation, padding_t)) { + properties.padding.set = false; } } double maxPitch = getMaxPitchForEdgeInsets(state.getEdgeInsets()); - if (propertyAnimations.pitch.set || maxPitch < propertyAnimations.pitch.current) { - double pitch_t = propertyAnimations.pitch.propertyAnimation->interpolant(now); - state.setPitch(std::min( - maxPitch, - util::interpolate(propertyAnimations.pitch.current, propertyAnimations.pitch.target, pitch_t))); - if (animationTransitionFrame(propertyAnimations.pitch.propertyAnimation, pitch_t)) { - propertyAnimations.pitch.set = false; + bool pitchSet = properties.pitch.set && properties.pitch.animation; + if (pitchSet || maxPitch < properties.pitch.current) { + double pitch_t = properties.pitch.animation->interpolant(now); + state.setPitch( + std::min(maxPitch, util::interpolate(properties.pitch.current, properties.pitch.target, pitch_t))); + if (animationTransitionFrame(properties.pitch.animation, pitch_t)) { + properties.pitch.set = false; } - } - if (propertyAnimations.anchor) { - state.moveLatLng(propertyAnimations.anchorLatLng, *propertyAnimations.anchor); + if (pitchSet && properties.pitch.animation->anchor) { + state.moveLatLng(properties.pitch.animation->anchorLatLng, *properties.pitch.animation->anchor); + } } - panning = false; - scaling = false; - rotating = false; - visitPropertyAnimations([&](std::shared_ptr& propertyAnimation) { - if (propertyAnimation) { - if (propertyAnimation->done) { - animationFinishFrame(propertyAnimation); + panning = scaling = rotating = false; + visitProperties([&](std::shared_ptr& animation) { + if (animation) { + if (animation->done) { + animationFinishFrame(animation); } else { - panning |= propertyAnimation->panning; - scaling |= propertyAnimation->scaling; - rotating |= propertyAnimation->rotating; + panning |= animation->panning; + scaling |= animation->scaling; + rotating |= animation->rotating; } - propertyAnimation->ran = false; + animation->ran = false; } }); @@ -747,10 +737,9 @@ void Transform::updateTransitions(const TimePoint& now) { } void Transform::cancelTransitions() { - visitPropertyAnimations( - [this](std::shared_ptr& propertyAnimation) { animationFinishFrame(propertyAnimation); }); + visitProperties([this](std::shared_ptr& animation) { animationFinishFrame(animation); }); - propertyAnimations = {}; + properties = {}; activeAnimation = false; } @@ -801,14 +790,14 @@ void Transform::setFreeCameraOptions(const FreeCameraOptions& options) { state.setFreeCameraOptions(options); } -double Transform::PropertyAnimation::interpolant(TimePoint now) { +double Transform::Animation::interpolant(TimePoint now) { bool isAnimated = duration != Duration::zero(); double t = isAnimated ? (std::chrono::duration(now - start) / duration) : 1.0f; if (t >= 1.0) { return 1.0; } - util::UnitBezier ease = animation.easing ? *animation.easing : util::DEFAULT_TRANSITION_EASE; + util::UnitBezier ease = options.easing ? *options.easing : util::DEFAULT_TRANSITION_EASE; return ease.solve(t, 0.001); } diff --git a/src/mbgl/map/transform.hpp b/src/mbgl/map/transform.hpp index 8ecb1c89da2b..58c926a3fbf2 100644 --- a/src/mbgl/map/transform.hpp +++ b/src/mbgl/map/transform.hpp @@ -132,24 +132,28 @@ class Transform : private util::noncopyable { FreeCameraOptions getFreeCameraOptions() const; void setFreeCameraOptions(const FreeCameraOptions& options); - struct PropertyAnimation { + struct Animation { TimePoint start; Duration duration; - AnimationOptions animation; + AnimationOptions options; bool ran = false; // Did this property animation run this frame bool finished = false; // Did we execute the finish frame for this property animation this frame bool done = false; // Did this property animation reach the end of the frame bool panning = false, scaling = false, rotating = false; - PropertyAnimation(TimePoint start_, - Duration duration_, - AnimationOptions animation_, - bool panning_, - bool scaling_, - bool rotating_) + // Anchor + std::optional anchor; + LatLng anchorLatLng; + + Animation(TimePoint start_, + Duration duration_, + AnimationOptions options_, + bool panning_, + bool scaling_, + bool rotating_) : start(start_), duration(duration_), - animation(animation_), + options(options_), panning(panning_), scaling(scaling_), rotating(rotating_) {} @@ -161,48 +165,40 @@ class Transform : private util::noncopyable { private: template - struct TransformProperty { - std::shared_ptr propertyAnimation; + struct Property { + std::shared_ptr animation; T current, target; bool set = false; std::function frameLatLngFunc = nullptr; std::function frameZoomFunc = nullptr; - - // Anchor - std::optional anchor = std::nullopt; - LatLng anchorLatLng = {}; }; - struct PropertyAnimations { - TransformProperty> latlng; - TransformProperty zoom, bearing, pitch; - TransformProperty padding; - - // Anchor - std::optional anchor; - LatLng anchorLatLng; + struct Properties { + Property> latlng; + Property zoom, bearing, pitch; + Property padding; }; TransformObserver& observer; TransformState state; - void startTransition(const CameraOptions&, const Duration&); - bool animationTransitionFrame(std::shared_ptr&, double); - void animationFinishFrame(std::shared_ptr&); + void startTransition(const CameraOptions&, const Duration&, std::shared_ptr&); + bool animationTransitionFrame(std::shared_ptr&, double); + void animationFinishFrame(std::shared_ptr&); - void visitPropertyAnimations(const std::function&)>& f) { - f(propertyAnimations.latlng.propertyAnimation); - f(propertyAnimations.zoom.propertyAnimation); - f(propertyAnimations.bearing.propertyAnimation); - f(propertyAnimations.pitch.propertyAnimation); - f(propertyAnimations.padding.propertyAnimation); + void visitProperties(const std::function&)>& f) { + f(properties.latlng.animation); + f(properties.zoom.animation); + f(properties.bearing.animation); + f(properties.pitch.animation); + f(properties.padding.animation); } // We don't want to show horizon: limit max pitch based on edge insets. double getMaxPitchForEdgeInsets(const EdgeInsets& insets) const; - PropertyAnimations propertyAnimations; + Properties properties; bool activeAnimation = false; TimePoint transitionStart; From 6e87e7d7209f6f12f7ed0b61bd732b8d49ed476b Mon Sep 17 00:00:00 2001 From: Yousif Aldolaijan Date: Tue, 1 Jul 2025 12:34:57 +0300 Subject: [PATCH 06/15] expose the ability to have concurrent animations on ios This exposes a new flag to the maplibre ios interface to allow users to choose to have concurrent camera animations during tracking. This makes it so that all calls to `cancelTransforms` from ios are ignored and independent concurrent camera animations are ran concurrently rather than overriding each other. --- platform/ios/src/MLNMapView.h | 6 ++++++ platform/ios/src/MLNMapView.mm | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/platform/ios/src/MLNMapView.h b/platform/ios/src/MLNMapView.h index 05107dac0cf3..603a9cebb942 100644 --- a/platform/ios/src/MLNMapView.h +++ b/platform/ios/src/MLNMapView.h @@ -574,6 +574,12 @@ MLN_EXPORT */ @property (nonatomic, readonly, nullable) MLNUserLocation *userLocation; +/** + * A Boolean value indicating whether independent camera animations + * can be applied at the same time or not. + */ +@property (nonatomic, assign) BOOL enableConcurrentCameraAnimation; + /** The mode used to track the user location. The default value is ``MLNUserTrackingMode/MLNUserTrackingModeNone``. diff --git a/platform/ios/src/MLNMapView.mm b/platform/ios/src/MLNMapView.mm index 16acd1925826..f9baa2786c37 100644 --- a/platform/ios/src/MLNMapView.mm +++ b/platform/ios/src/MLNMapView.mm @@ -3966,6 +3966,7 @@ - (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated { MLNLogDebug(@"Setting zoomLevel: %f animated: %@", zoomLevel, MLNStringFromBOOL(animated)); if (zoomLevel == self.zoomLevel) return; + [self cancelTransitions]; self.cameraChangeReasonBitmask |= MLNCameraChangeReasonProgrammatic; @@ -4446,7 +4447,7 @@ - (void)_flyToCamera:(MLNMapCamera *)camera edgePadding:(UIEdgeInsets)insets wit } - (void)cancelTransitions { - if (!_mbglMap) + if (!_mbglMap || (self.enableConcurrentCameraAnimation && self.userTrackingMode != MLNUserTrackingModeNone && self.userTrackingMode != MLNUserTrackingModeFollow)) { return; } From 5caff410d16245c9dcb4aece26fc807f94b8fa1b Mon Sep 17 00:00:00 2001 From: Yousif Aldolaijan Date: Tue, 1 Jul 2025 12:36:39 +0300 Subject: [PATCH 07/15] expose the concurrent camera animations during navigation to android this makes it so that users of the android integration can choose to have indepedent camera animations run concurrently instead of them waiting on each other or canceling each other. --- .../location/LocationCameraController.java | 2 +- .../android/location/LocationComponent.java | 6 ++- .../location/LocationComponentOptions.java | 41 ++++++++++++++++++- .../maplibre/android/maps/MapKeyListener.java | 8 ++++ .../org/maplibre/android/maps/Transform.java | 5 +++ .../src/main/res-public/values/public.xml | 3 ++ .../src/main/res/values/attrs.xml | 3 ++ .../src/main/res/values/styles.xml | 2 + 8 files changed, 66 insertions(+), 4 deletions(-) diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationCameraController.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationCameraController.java index c923308079cf..e3393264f7a3 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationCameraController.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationCameraController.java @@ -330,7 +330,7 @@ void setEnabled(boolean enabled) { isEnabled = enabled; } - private boolean isLocationTracking() { + boolean isLocationTracking() { return cameraMode == CameraMode.TRACKING || cameraMode == CameraMode.TRACKING_COMPASS || cameraMode == CameraMode.TRACKING_GPS diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponent.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponent.java index 5809d13f12af..a55f388c121d 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponent.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponent.java @@ -1387,6 +1387,10 @@ private void updateAnimatorListenerHolders() { locationAnimatorCoordinator.resetAllLayerAnimations(); } + public boolean isLocationTracking() { + return locationCameraController.isLocationTracking(); + } + @NonNull private OnCameraMoveListener onCameraMoveListener = new OnCameraMoveListener() { @Override @@ -1546,7 +1550,7 @@ public void onRenderModeChanged(int currentMode) { new MapLibreMap.OnDeveloperAnimationListener() { @Override public void onDeveloperAnimationStarted() { - if (isComponentInitialized && isEnabled) { + if (isComponentInitialized && isEnabled && !options.concurrentCameraAnimationsEnabled()) { setCameraMode(CameraMode.NONE); } } diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponentOptions.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponentOptions.java index 83b6db8a282e..58cd54f56263 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponentOptions.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponentOptions.java @@ -144,6 +144,7 @@ public class LocationComponentOptions implements Parcelable { private float pulseAlpha; @Nullable private Interpolator pulseInterpolator; + private Boolean concurrentCameraAnimationsEnabled; public LocationComponentOptions( float accuracyAlpha, @@ -186,7 +187,8 @@ public LocationComponentOptions( float pulseSingleDuration, float pulseMaxRadius, float pulseAlpha, - @Nullable Interpolator pulseInterpolator) { + @Nullable Interpolator pulseInterpolator, + Boolean concurrentCameraAnimationsEnabled) { this.accuracyAlpha = accuracyAlpha; this.accuracyColor = accuracyColor; this.backgroundDrawableStale = backgroundDrawableStale; @@ -231,6 +233,7 @@ public LocationComponentOptions( this.pulseMaxRadius = pulseMaxRadius; this.pulseAlpha = pulseAlpha; this.pulseInterpolator = pulseInterpolator; + this.concurrentCameraAnimationsEnabled = concurrentCameraAnimationsEnabled; } /** @@ -373,6 +376,10 @@ public static LocationComponentOptions createFromAttributes(@NonNull Context con builder.pulseAlpha = typedArray.getFloat( R.styleable.maplibre_LocationComponent_maplibre_pulsingLocationCircleAlpha, CIRCLE_PULSING_ALPHA_DEFAULT); + builder.concurrentCameraAnimationsEnabled = typedArray.getBoolean( + R.styleable.maplibre_LocationComponent_maplibre_concurrentCameraAnimationsEnabled, false + ); + typedArray.recycle(); return builder.build(); @@ -892,6 +899,15 @@ public Interpolator pulseInterpolator() { return pulseInterpolator; } + /** + * Enable or disable concurrent camera animations during navigation + * + * @return whether concurrent camera animations are enabled or disabled during navigation + */ + public Boolean concurrentCameraAnimationsEnabled() { + return concurrentCameraAnimationsEnabled; + } + @NonNull @Override public String toString() { @@ -934,6 +950,7 @@ public String toString() { + "pulseSingleDuration=" + pulseSingleDuration + "pulseMaxRadius=" + pulseMaxRadius + "pulseAlpha=" + pulseAlpha + + "concurrentCameraAnimationsEnabled=" + concurrentCameraAnimationsEnabled + "}"; } @@ -1081,6 +1098,10 @@ public boolean equals(Object o) { return false; } + if (concurrentCameraAnimationsEnabled != options.concurrentCameraAnimationsEnabled) { + return false; + } + return layerBelow != null ? layerBelow.equals(options.layerBelow) : options.layerBelow == null; } @@ -1130,6 +1151,7 @@ public int hashCode() { result = 31 * result + (pulseSingleDuration != +0.0f ? Float.floatToIntBits(pulseSingleDuration) : 0); result = 31 * result + (pulseMaxRadius != +0.0f ? Float.floatToIntBits(pulseMaxRadius) : 0); result = 31 * result + (pulseAlpha != +0.0f ? Float.floatToIntBits(pulseAlpha) : 0); + result = 31 * result + (concurrentCameraAnimationsEnabled ? 1 : 0); return result; } @@ -1180,6 +1202,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeFloat(this.pulseSingleDuration); dest.writeFloat(this.pulseMaxRadius); dest.writeFloat(this.pulseAlpha); + dest.writeByte(this.concurrentCameraAnimationsEnabled ? (byte) 1 : (byte) 0); } protected LocationComponentOptions(Parcel in) { @@ -1223,6 +1246,7 @@ protected LocationComponentOptions(Parcel in) { this.pulseSingleDuration = in.readFloat(); this.pulseMaxRadius = in.readFloat(); this.pulseAlpha = in.readFloat(); + this.concurrentCameraAnimationsEnabled = in.readByte() != 0; } public static final Parcelable.Creator CREATOR = @@ -1349,6 +1373,7 @@ public LocationComponentOptions build() { private float pulseAlpha; @Nullable private Interpolator pulseInterpolator; + private Boolean concurrentCameraAnimationsEnabled; Builder() { } @@ -1395,6 +1420,7 @@ private Builder(LocationComponentOptions source) { this.pulseMaxRadius = source.pulseMaxRadius; this.pulseAlpha = source.pulseAlpha; this.pulseInterpolator = source.pulseInterpolator; + this.concurrentCameraAnimationsEnabled = source.concurrentCameraAnimationsEnabled; } /** @@ -1973,6 +1999,16 @@ public LocationComponentOptions.Builder pulseInterpolator(Interpolator pulseInte return this; } + /** + * Enable or disable the concurrent camera animations during navigation feature + * + * @return whether concurrent camera animations are enabled or disabled + */ + public LocationComponentOptions.Builder concurrentCameraAnimationsEnabled(Boolean concurrentCameraAnimationsEnabled) { + this.concurrentCameraAnimationsEnabled = concurrentCameraAnimationsEnabled; + return this; + } + @Nullable LocationComponentOptions autoBuild() { String missing = ""; @@ -2074,7 +2110,8 @@ LocationComponentOptions autoBuild() { this.pulseSingleDuration, this.pulseMaxRadius, this.pulseAlpha, - this.pulseInterpolator); + this.pulseInterpolator, + this.concurrentCameraAnimationsEnabled); } } } diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapKeyListener.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapKeyListener.java index 238cce138548..70cefe0668c2 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapKeyListener.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapKeyListener.java @@ -167,6 +167,14 @@ boolean onKeyUp(int keyCode, KeyEvent event) { PointF focalPoint = new PointF(uiSettings.getWidth() / 2, uiSettings.getHeight() / 2); mapGestureDetector.zoomInAnimated(focalPoint, true); return true; + case KeyEvent.KEYCODE_DEL: + if (!uiSettings.isZoomGesturesEnabled()) { + return false; + } + + // Zoom out + focalPoint = new PointF(uiSettings.getWidth() / 2, uiSettings.getHeight() / 2); + mapGestureDetector.zoomOutAnimated(focalPoint, true); } // We are not interested in this key diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/Transform.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/Transform.java index 3efc3c254df7..d838dc88fa80 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/Transform.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/Transform.java @@ -190,6 +190,11 @@ CameraPosition invalidateCameraPosition() { } void cancelTransitions() { + MapLibreMap map = mapView.getMapLibreMap(); + if (map != null && map.getLocationComponent().isLocationComponentActivated() && map.getLocationComponent().getLocationComponentOptions().concurrentCameraAnimationsEnabled() && map.getLocationComponent().isLocationTracking()) { + return; + } + // notify user about cancel cameraChangeDispatcher.onCameraMoveCanceled(); diff --git a/platform/android/MapLibreAndroid/src/main/res-public/values/public.xml b/platform/android/MapLibreAndroid/src/main/res-public/values/public.xml index bd4ea5ee290a..9fd55c41c8b1 100644 --- a/platform/android/MapLibreAndroid/src/main/res-public/values/public.xml +++ b/platform/android/MapLibreAndroid/src/main/res-public/values/public.xml @@ -173,4 +173,7 @@ + + + diff --git a/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml b/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml index 658184e4d227..d3cf85d00cff 100644 --- a/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml +++ b/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml @@ -202,5 +202,8 @@ + + + diff --git a/platform/android/MapLibreAndroid/src/main/res/values/styles.xml b/platform/android/MapLibreAndroid/src/main/res/values/styles.xml index 38459eb3a70c..18ad932628f0 100644 --- a/platform/android/MapLibreAndroid/src/main/res/values/styles.xml +++ b/platform/android/MapLibreAndroid/src/main/res/values/styles.xml @@ -42,5 +42,7 @@ 0.4 decelerate + + false From 0f387f2ad5d1bdf79acef2ef3d0751d2e3ebc940 Mon Sep 17 00:00:00 2001 From: Yousif Aldolaijan Date: Wed, 2 Jul 2025 10:43:41 +0300 Subject: [PATCH 08/15] pass animation by reference rather than pointer reference --- src/mbgl/map/transform.cpp | 128 +++++++++++++++++++++---------------- src/mbgl/map/transform.hpp | 30 ++++++--- 2 files changed, 92 insertions(+), 66 deletions(-) diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index dc3d47f59437..0e1a7536b262 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -168,7 +168,9 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& transitionDuration = animation->duration; if (!properties.zoom.set || startZoom != zoom) { - animationFinishFrame(properties.zoom.animation); + if (properties.zoom.animation) { + animationFinishFrame(*properties.zoom.animation); + } properties.zoom = { .animation = animation, .current = startZoom, @@ -179,7 +181,9 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& }; } if (!properties.latlng.set || startPoint != endPoint) { - animationFinishFrame(properties.latlng.animation); + if (properties.latlng.animation) { + animationFinishFrame(*properties.latlng.animation); + } properties.latlng = { .animation = animation, .current = startPoint, @@ -193,7 +197,9 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& }; } if (!properties.bearing.set || bearing != startBearing) { - animationFinishFrame(properties.bearing.animation); + if (properties.bearing.animation) { + animationFinishFrame(*properties.bearing.animation); + } properties.bearing = { .animation = animation, .current = startBearing, @@ -202,7 +208,9 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& }; } if (!properties.padding.set || padding != startEdgeInsets) { - animationFinishFrame(properties.padding.animation); + if (properties.padding.animation) { + animationFinishFrame(*properties.padding.animation); + } properties.padding = { .animation = animation, .current = startEdgeInsets, @@ -211,7 +219,9 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& }; } if (!properties.pitch.set || pitch != startPitch) { - animationFinishFrame(properties.pitch.animation); + if (properties.pitch.animation) { + animationFinishFrame(*properties.pitch.animation); + } properties.pitch = { .animation = animation, .current = startPitch, @@ -220,7 +230,7 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& }; } - startTransition(camera, duration, animation); + startTransition(camera, duration, *animation); } /** This method implements an “optimal path” animation, as detailed in: @@ -362,7 +372,9 @@ void Transform::flyTo(const CameraOptions& inputCamera, const AnimationOptions& transitionDuration = animation->duration; if (!properties.zoom.set || startZoom != zoom) { - animationFinishFrame(properties.zoom.animation); + if (properties.zoom.animation) { + animationFinishFrame(*properties.zoom.animation); + } properties.zoom = { .animation = animation, .current = startZoom, @@ -384,7 +396,9 @@ void Transform::flyTo(const CameraOptions& inputCamera, const AnimationOptions& }; } if (!properties.latlng.set || startPoint != endPoint) { - animationFinishFrame(properties.latlng.animation); + if (properties.latlng.animation) { + animationFinishFrame(*properties.latlng.animation); + } properties.latlng = { .animation = animation, .current = startPoint, @@ -403,7 +417,9 @@ void Transform::flyTo(const CameraOptions& inputCamera, const AnimationOptions& }; } if (!properties.bearing.set || bearing != startBearing) { - animationFinishFrame(properties.bearing.animation); + if (properties.bearing.animation) { + animationFinishFrame(*properties.bearing.animation); + } properties.bearing = { .animation = animation, .current = startBearing, @@ -412,11 +428,15 @@ void Transform::flyTo(const CameraOptions& inputCamera, const AnimationOptions& }; } if (!properties.padding.set || padding != startEdgeInsets) { - animationFinishFrame(properties.padding.animation); + if (properties.padding.animation) { + animationFinishFrame(*properties.padding.animation); + } properties.padding = {.animation = animation, .current = startEdgeInsets, .target = padding, .set = true}; } if (!properties.pitch.set || pitch != startPitch) { - animationFinishFrame(properties.pitch.animation); + if (properties.pitch.animation) { + animationFinishFrame(*properties.pitch.animation); + } properties.pitch = { .animation = animation, .current = startPitch, @@ -425,27 +445,27 @@ void Transform::flyTo(const CameraOptions& inputCamera, const AnimationOptions& }; } - startTransition(camera, duration, animation); + startTransition(camera, duration, *animation); } -bool Transform::animationTransitionFrame(std::shared_ptr& animation, double t) { - if (animation->ran) { - return animation->done; +bool Transform::animationTransitionFrame(Animation& animation, double t) { + if (animation.ran) { + return animation.done; } - animation->ran = true; + animation.ran = true; if (t < 1.0) { - if (animation->options.transitionFrameFn) { - animation->options.transitionFrameFn(t); + if (animation.options.transitionFrameFn) { + animation.options.transitionFrameFn(t); } observer.onCameraIsChanging(); - animation->done = false; + animation.done = false; } else { - animation->done = true; + animation.done = true; } - return animation->done; + return animation.done; } // MARK: - Position @@ -597,26 +617,24 @@ ProjectionMode Transform::getProjectionMode() const { // MARK: - Transition -void Transform::animationFinishFrame(std::shared_ptr& animation) { - if (!animation || animation->finished) { +void Transform::animationFinishFrame(Animation& animation) { + if (animation.finished) { return; } - if (animation->options.transitionFinishFn) { - animation->options.transitionFinishFn(); + if (animation.options.transitionFinishFn) { + animation.options.transitionFinishFn(); } - animation->finished = true; + animation.finished = true; - if (animation->anchor) animation->anchor = std::nullopt; + if (animation.anchor) animation.anchor = std::nullopt; - observer.onCameraDidChange(animation->isAnimated() ? MapObserver::CameraChangeMode::Animated - : MapObserver::CameraChangeMode::Immediate); + observer.onCameraDidChange(animation.isAnimated() ? MapObserver::CameraChangeMode::Animated + : MapObserver::CameraChangeMode::Immediate); } -void Transform::startTransition(const CameraOptions& camera, - const Duration& duration, - std::shared_ptr& animation) { +void Transform::startTransition(const CameraOptions& camera, const Duration& duration, Animation& animation) { bool isAnimated = duration != Duration::zero(); observer.onCameraWillChange(isAnimated ? MapObserver::CameraChangeMode::Animated : MapObserver::CameraChangeMode::Immediate); @@ -625,9 +643,9 @@ void Transform::startTransition(const CameraOptions& camera, // Anchor and center points are mutually exclusive, with preference for the // center point when both are set. if (!camera.center && camera.anchor) { - animation->anchor = camera.anchor; - animation->anchor->y = state.getSize().height - animation->anchor->y; - animation->anchorLatLng = state.screenCoordinateToLatLng(*animation->anchor); + animation.anchor = camera.anchor; + animation.anchor->y = state.getSize().height - animation.anchor->y; + animation.anchorLatLng = state.screenCoordinateToLatLng(*animation.anchor); } if (!isAnimated) { @@ -646,11 +664,11 @@ void Transform::updateTransitions(const TimePoint& now) { activeAnimation = true; bool panning = false, scaling = false, rotating = false; - visitProperties([&](std::shared_ptr& animation) { - if (animation && !animation->done) { - panning |= animation->panning; - scaling |= animation->scaling; - rotating |= animation->rotating; + visitProperties([&](Animation& animation) { + if (!animation.done) { + panning |= animation.panning; + scaling |= animation.scaling; + rotating |= animation.rotating; } }); @@ -665,11 +683,11 @@ void Transform::updateTransitions(const TimePoint& now) { properties.latlng.frameLatLngFunc ? properties.latlng.frameLatLngFunc(now) : state.getLatLng(), properties.zoom.frameZoomFunc ? properties.zoom.frameZoomFunc(now) : state.getZoom()); if (properties.latlng.animation && - animationTransitionFrame(properties.latlng.animation, properties.latlng.animation->interpolant(now))) { + animationTransitionFrame(*properties.latlng.animation, properties.latlng.animation->interpolant(now))) { properties.latlng.set = false; } if (properties.zoom.animation && - animationTransitionFrame(properties.zoom.animation, properties.zoom.animation->interpolant(now))) { + animationTransitionFrame(*properties.zoom.animation, properties.zoom.animation->interpolant(now))) { properties.zoom.set = false; } @@ -681,7 +699,7 @@ void Transform::updateTransitions(const TimePoint& now) { double bearing_t = properties.bearing.animation->interpolant(now); state.setBearing(util::wrap( util::interpolate(properties.bearing.current, properties.bearing.target, bearing_t), -pi, pi)); - if (animationTransitionFrame(properties.bearing.animation, bearing_t)) { + if (animationTransitionFrame(*properties.bearing.animation, bearing_t)) { properties.bearing.set = false; } } @@ -693,7 +711,7 @@ void Transform::updateTransitions(const TimePoint& now) { util::interpolate(properties.padding.current.left(), properties.padding.target.left(), padding_t), util::interpolate(properties.padding.current.bottom(), properties.padding.target.bottom(), padding_t), util::interpolate(properties.padding.current.right(), properties.padding.target.right(), padding_t)}); - if (animationTransitionFrame(properties.padding.animation, padding_t)) { + if (animationTransitionFrame(*properties.padding.animation, padding_t)) { properties.padding.set = false; } } @@ -704,7 +722,7 @@ void Transform::updateTransitions(const TimePoint& now) { double pitch_t = properties.pitch.animation->interpolant(now); state.setPitch( std::min(maxPitch, util::interpolate(properties.pitch.current, properties.pitch.target, pitch_t))); - if (animationTransitionFrame(properties.pitch.animation, pitch_t)) { + if (animationTransitionFrame(*properties.pitch.animation, pitch_t)) { properties.pitch.set = false; } @@ -714,17 +732,15 @@ void Transform::updateTransitions(const TimePoint& now) { } panning = scaling = rotating = false; - visitProperties([&](std::shared_ptr& animation) { - if (animation) { - if (animation->done) { - animationFinishFrame(animation); - } else { - panning |= animation->panning; - scaling |= animation->scaling; - rotating |= animation->rotating; - } - animation->ran = false; + visitProperties([&](Animation& animation) { + if (animation.done) { + animationFinishFrame(animation); + } else { + panning |= animation.panning; + scaling |= animation.scaling; + rotating |= animation.rotating; } + animation.ran = false; }); state.setProperties(TransformStateProperties() @@ -737,7 +753,7 @@ void Transform::updateTransitions(const TimePoint& now) { } void Transform::cancelTransitions() { - visitProperties([this](std::shared_ptr& animation) { animationFinishFrame(animation); }); + visitProperties([this](Animation& animation) { animationFinishFrame(animation); }); properties = {}; activeAnimation = false; diff --git a/src/mbgl/map/transform.hpp b/src/mbgl/map/transform.hpp index 58c926a3fbf2..04d6b3e4cc99 100644 --- a/src/mbgl/map/transform.hpp +++ b/src/mbgl/map/transform.hpp @@ -183,16 +183,26 @@ class Transform : private util::noncopyable { TransformObserver& observer; TransformState state; - void startTransition(const CameraOptions&, const Duration&, std::shared_ptr&); - bool animationTransitionFrame(std::shared_ptr&, double); - void animationFinishFrame(std::shared_ptr&); - - void visitProperties(const std::function&)>& f) { - f(properties.latlng.animation); - f(properties.zoom.animation); - f(properties.bearing.animation); - f(properties.pitch.animation); - f(properties.padding.animation); + void startTransition(const CameraOptions&, const Duration&, Animation&); + bool animationTransitionFrame(Animation&, double); + void animationFinishFrame(Animation&); + + void visitProperties(const std::function& f) { + if (properties.zoom.animation) { + f(*properties.zoom.animation); + } + if (properties.latlng.animation) { + f(*properties.latlng.animation); + } + if (properties.bearing.animation) { + f(*properties.bearing.animation); + } + if (properties.padding.animation) { + f(*properties.padding.animation); + } + if (properties.pitch.animation) { + f(*properties.pitch.animation); + } } // We don't want to show horizon: limit max pitch based on edge insets. From 9ce818d6a28a6cebcfe7ccb591bd44f89d9c787b Mon Sep 17 00:00:00 2001 From: Yousif Aldolaijan Date: Thu, 17 Jul 2025 16:55:06 +0300 Subject: [PATCH 09/15] cleanup and refactor code based on comments and style expectations --- .../location/LocationComponentOptions.java | 11 +-- .../maplibre/android/maps/MapKeyListener.java | 4 +- .../org/maplibre/android/maps/Transform.java | 6 +- src/mbgl/map/transform.cpp | 83 +++++++++++-------- src/mbgl/map/transform.hpp | 26 +++--- 5 files changed, 75 insertions(+), 55 deletions(-) diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponentOptions.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponentOptions.java index 58cd54f56263..b1bebcc10083 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponentOptions.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LocationComponentOptions.java @@ -905,7 +905,7 @@ public Interpolator pulseInterpolator() { * @return whether concurrent camera animations are enabled or disabled during navigation */ public Boolean concurrentCameraAnimationsEnabled() { - return concurrentCameraAnimationsEnabled; + return concurrentCameraAnimationsEnabled; } @NonNull @@ -1099,7 +1099,7 @@ public boolean equals(Object o) { } if (concurrentCameraAnimationsEnabled != options.concurrentCameraAnimationsEnabled) { - return false; + return false; } return layerBelow != null ? layerBelow.equals(options.layerBelow) : options.layerBelow == null; @@ -2004,9 +2004,10 @@ public LocationComponentOptions.Builder pulseInterpolator(Interpolator pulseInte * * @return whether concurrent camera animations are enabled or disabled */ - public LocationComponentOptions.Builder concurrentCameraAnimationsEnabled(Boolean concurrentCameraAnimationsEnabled) { - this.concurrentCameraAnimationsEnabled = concurrentCameraAnimationsEnabled; - return this; + public LocationComponentOptions.Builder concurrentCameraAnimationsEnabled( + Boolean concurrentCameraAnimationsEnabled) { + this.concurrentCameraAnimationsEnabled = concurrentCameraAnimationsEnabled; + return this; } @Nullable diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapKeyListener.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapKeyListener.java index 70cefe0668c2..175b0fbe9df4 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapKeyListener.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapKeyListener.java @@ -167,9 +167,9 @@ boolean onKeyUp(int keyCode, KeyEvent event) { PointF focalPoint = new PointF(uiSettings.getWidth() / 2, uiSettings.getHeight() / 2); mapGestureDetector.zoomInAnimated(focalPoint, true); return true; - case KeyEvent.KEYCODE_DEL: + case KeyEvent.KEYCODE_DEL: if (!uiSettings.isZoomGesturesEnabled()) { - return false; + return false; } // Zoom out diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/Transform.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/Transform.java index d838dc88fa80..3b7f5a9cc1c2 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/Transform.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/Transform.java @@ -191,8 +191,10 @@ CameraPosition invalidateCameraPosition() { void cancelTransitions() { MapLibreMap map = mapView.getMapLibreMap(); - if (map != null && map.getLocationComponent().isLocationComponentActivated() && map.getLocationComponent().getLocationComponentOptions().concurrentCameraAnimationsEnabled() && map.getLocationComponent().isLocationTracking()) { - return; + if (map != null && map.getLocationComponent().isLocationComponentActivated() + && map.getLocationComponent().getLocationComponentOptions().concurrentCameraAnimationsEnabled() + && map.getLocationComponent().isLocationTracking()) { + return; } // notify user about cancel diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index 0e1a7536b262..a114da5a1a4f 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -103,13 +103,13 @@ void Transform::jumpTo(const CameraOptions& camera) { * smooth animation between old and new values. The map will retain the current * values for any options not included in `options`. */ -void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& options) { +void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& animationOptions) { CameraOptions camera = inputCamera; - Duration duration = options.duration.value_or(Duration::zero()); + Duration duration = animationOptions.duration.value_or(Duration::zero()); if (state.getLatLngBounds() == LatLngBounds() && !isGestureInProgress() && duration != Duration::zero()) { // reuse flyTo, without exaggerated animation, to achieve constant ground speed. - return flyTo(camera, options, true); + return flyTo(camera, animationOptions, true); } double zoom = camera.zoom.value_or(getZoom()); @@ -124,8 +124,8 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& double pitch = camera.pitch ? util::deg2rad(*camera.pitch) : getPitch(); if (std::isnan(zoom) || std::isnan(bearing) || std::isnan(pitch)) { - if (options.transitionFinishFn) { - options.transitionFinishFn(); + if (animationOptions.transitionFinishFn) { + animationOptions.transitionFinishFn(); } return; } @@ -160,8 +160,12 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& const double startPitch = state.getPitch(); const EdgeInsets startEdgeInsets = state.getEdgeInsets(); - auto animation = std::make_shared( - Clock::now(), duration, options, unwrappedLatLng != startLatLng, zoom != startZoom, bearing != startBearing); + auto animation = std::make_shared(Clock::now(), + duration, + animationOptions, + unwrappedLatLng != startLatLng, + zoom != startZoom, + bearing != startBearing); // NOTE: For tests only transitionStart = animation->start; @@ -177,7 +181,9 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& .target = zoom, .set = true, .frameZoomFunc = - [=](TimePoint now) { return util::interpolate(startZoom, zoom, animation->interpolant(now)); }, + [startZoom, zoom, animation](TimePoint now) { + return util::interpolate(startZoom, zoom, animation->interpolant(now)); + }, }; } if (!properties.latlng.set || startPoint != endPoint) { @@ -190,7 +196,7 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& .target = endPoint, .set = true, .frameLatLngFunc = - [=, this](TimePoint now) { + [startPoint, endPoint, startZoom, animation, this](TimePoint now) { Point framePoint = util::interpolate(startPoint, endPoint, animation->interpolant(now)); return Projection::unproject(framePoint, state.zoomScale(startZoom)); }, @@ -241,7 +247,9 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& Where applicable, local variable documentation begins with the associated variable or function in van Wijk (2003). */ -void Transform::flyTo(const CameraOptions& inputCamera, const AnimationOptions& options, bool linearZoomInterpolation) { +void Transform::flyTo(const CameraOptions& inputCamera, + const AnimationOptions& animationOptions, + bool linearZoomInterpolation) { CameraOptions camera = inputCamera; double zoom = camera.zoom.value_or(getZoom()); @@ -254,8 +262,8 @@ void Transform::flyTo(const CameraOptions& inputCamera, const AnimationOptions& double pitch = camera.pitch ? util::deg2rad(*camera.pitch) : getPitch(); if (std::isnan(zoom) || std::isnan(bearing) || std::isnan(pitch) || state.getSize().isEmpty()) { - if (options.transitionFinishFn) { - options.transitionFinishFn(); + if (animationOptions.transitionFinishFn) { + animationOptions.transitionFinishFn(); } return; } @@ -299,8 +307,8 @@ void Transform::flyTo(const CameraOptions& inputCamera, const AnimationOptions& root mean squared average velocity, VRMS. A value of 1 produces a circular motion. */ double rho = 1.42; - if (options.minZoom || linearZoomInterpolation) { - double minZoom = util::min(options.minZoom.value_or(startZoom), startZoom, zoom); + if (animationOptions.minZoom || linearZoomInterpolation) { + double minZoom = util::min(animationOptions.minZoom.value_or(startZoom), startZoom, zoom); minZoom = util::clamp(minZoom, state.getMinZoom(), state.getMaxZoom()); /// wm: Maximum visible span, measured in pixels with respect /// to the initial scale. @@ -343,21 +351,21 @@ void Transform::flyTo(const CameraOptions& inputCamera, const AnimationOptions& double S = (isClose ? (std::abs(std::log(w1 / w0)) / rho) : ((r1 - r0) / rho)); Duration duration; - if (options.duration) { - duration = *options.duration; + if (animationOptions.duration) { + duration = *animationOptions.duration; } else { /// V: Average velocity, measured in ρ-screenfuls per second. double velocity = 1.2; - if (options.velocity) { - velocity = *options.velocity / rho; + if (animationOptions.velocity) { + velocity = *animationOptions.velocity / rho; } duration = std::chrono::duration_cast(std::chrono::duration(S / velocity)); } if (duration == Duration::zero()) { // Perform an instantaneous transition. jumpTo(camera); - if (options.transitionFinishFn) { - options.transitionFinishFn(); + if (animationOptions.transitionFinishFn) { + animationOptions.transitionFinishFn(); } return; } @@ -365,7 +373,8 @@ void Transform::flyTo(const CameraOptions& inputCamera, const AnimationOptions& const double startScale = state.getScale(); const EdgeInsets startEdgeInsets = state.getEdgeInsets(); - auto animation = std::make_shared(Clock::now(), duration, options, true, true, bearing != startBearing); + auto animation = std::make_shared( + Clock::now(), duration, animationOptions, true, true, bearing != startBearing); // NOTE: For tests only transitionStart = animation->start; @@ -381,7 +390,7 @@ void Transform::flyTo(const CameraOptions& inputCamera, const AnimationOptions& .target = zoom, .set = true, .frameZoomFunc = - [=, this](TimePoint now) { + [linearZoomInterpolation, startZoom, zoom, S, w, this](TimePoint now) { double t = properties.zoom.animation->interpolant(now); double s = t * S; double frameZoom = linearZoomInterpolation ? util::interpolate(startZoom, zoom, t) @@ -405,7 +414,7 @@ void Transform::flyTo(const CameraOptions& inputCamera, const AnimationOptions& .target = endPoint, .set = true, .frameLatLngFunc = - [=, this](TimePoint now) { + [startScale, S, u, this](TimePoint now) { double t = properties.latlng.animation->interpolant(now); double s = t * S; double us = t == 1.0 ? 1.0 : u(s); @@ -635,7 +644,7 @@ void Transform::animationFinishFrame(Animation& animation) { } void Transform::startTransition(const CameraOptions& camera, const Duration& duration, Animation& animation) { - bool isAnimated = duration != Duration::zero(); + const bool isAnimated = duration != Duration::zero(); observer.onCameraWillChange(isAnimated ? MapObserver::CameraChangeMode::Animated : MapObserver::CameraChangeMode::Immediate); @@ -663,7 +672,9 @@ void Transform::updateTransitions(const TimePoint& now) { if (!activeAnimation) { activeAnimation = true; - bool panning = false, scaling = false, rotating = false; + bool panning = false; + bool scaling = false; + bool rotating = false; visitProperties([&](Animation& animation) { if (!animation.done) { panning |= animation.panning; @@ -677,7 +688,7 @@ void Transform::updateTransitions(const TimePoint& now) { .withScalingInProgress(scaling) .withRotatingInProgress(rotating)); - bool zoomSet = properties.zoom.set && properties.zoom.animation; + const bool zoomSet = properties.zoom.set && properties.zoom.animation; if ((properties.latlng.set && properties.latlng.animation) || zoomSet) { state.setLatLngZoom( properties.latlng.frameLatLngFunc ? properties.latlng.frameLatLngFunc(now) : state.getLatLng(), @@ -696,7 +707,7 @@ void Transform::updateTransitions(const TimePoint& now) { } } if (properties.bearing.set && properties.bearing.animation) { - double bearing_t = properties.bearing.animation->interpolant(now); + const double bearing_t = properties.bearing.animation->interpolant(now); state.setBearing(util::wrap( util::interpolate(properties.bearing.current, properties.bearing.target, bearing_t), -pi, pi)); if (animationTransitionFrame(*properties.bearing.animation, bearing_t)) { @@ -705,7 +716,7 @@ void Transform::updateTransitions(const TimePoint& now) { } if (properties.padding.set && properties.padding.animation) { - double padding_t = properties.padding.animation->interpolant(now); + const double padding_t = properties.padding.animation->interpolant(now); state.setEdgeInsets( {util::interpolate(properties.padding.current.top(), properties.padding.target.top(), padding_t), util::interpolate(properties.padding.current.left(), properties.padding.target.left(), padding_t), @@ -716,9 +727,8 @@ void Transform::updateTransitions(const TimePoint& now) { } } - double maxPitch = getMaxPitchForEdgeInsets(state.getEdgeInsets()); - bool pitchSet = properties.pitch.set && properties.pitch.animation; - if (pitchSet || maxPitch < properties.pitch.current) { + const double maxPitch = getMaxPitchForEdgeInsets(state.getEdgeInsets()); + if ((properties.pitch.set || maxPitch < properties.pitch.current) && properties.pitch.animation) { double pitch_t = properties.pitch.animation->interpolant(now); state.setPitch( std::min(maxPitch, util::interpolate(properties.pitch.current, properties.pitch.target, pitch_t))); @@ -726,12 +736,14 @@ void Transform::updateTransitions(const TimePoint& now) { properties.pitch.set = false; } - if (pitchSet && properties.pitch.animation->anchor) { + if (properties.pitch.set && properties.pitch.animation && properties.pitch.animation->anchor) { state.moveLatLng(properties.pitch.animation->anchorLatLng, *properties.pitch.animation->anchor); } } - panning = scaling = rotating = false; + panning = false; + scaling = false; + rotating = false; visitProperties([&](Animation& animation) { if (animation.done) { animationFinishFrame(animation); @@ -806,9 +818,8 @@ void Transform::setFreeCameraOptions(const FreeCameraOptions& options) { state.setFreeCameraOptions(options); } -double Transform::Animation::interpolant(TimePoint now) { - bool isAnimated = duration != Duration::zero(); - double t = isAnimated ? (std::chrono::duration(now - start) / duration) : 1.0f; +double Transform::Animation::interpolant(const TimePoint& now) const { + double t = isAnimated() ? (std::chrono::duration(now - start) / duration) : 1.0f; if (t >= 1.0) { return 1.0; } diff --git a/src/mbgl/map/transform.hpp b/src/mbgl/map/transform.hpp index 04d6b3e4cc99..b01ccaf429c1 100644 --- a/src/mbgl/map/transform.hpp +++ b/src/mbgl/map/transform.hpp @@ -132,14 +132,20 @@ class Transform : private util::noncopyable { FreeCameraOptions getFreeCameraOptions() const; void setFreeCameraOptions(const FreeCameraOptions& options); +private: struct Animation { - TimePoint start; - Duration duration; - AnimationOptions options; + const TimePoint start; + const Duration duration; + const AnimationOptions options; bool ran = false; // Did this property animation run this frame bool finished = false; // Did we execute the finish frame for this property animation this frame bool done = false; // Did this property animation reach the end of the frame - bool panning = false, scaling = false, rotating = false; + // The below variables keep track of the panning, scaling, and rotating transform state + // so we can correctly set it at the end of the `updateTransitions` if more + // than one `Animation` is running at the same time. + const bool panning; + const bool scaling; + const bool rotating; // Anchor std::optional anchor; @@ -158,20 +164,20 @@ class Transform : private util::noncopyable { scaling(scaling_), rotating(rotating_) {} - double interpolant(TimePoint); + double interpolant(const TimePoint&) const; bool isAnimated() const { return duration != Duration::zero(); } }; -private: template struct Property { std::shared_ptr animation; - T current, target; + T current; + T target; bool set = false; - std::function frameLatLngFunc = nullptr; - std::function frameZoomFunc = nullptr; + std::function frameLatLngFunc = nullptr; + std::function frameZoomFunc = nullptr; }; struct Properties { @@ -184,7 +190,7 @@ class Transform : private util::noncopyable { TransformState state; void startTransition(const CameraOptions&, const Duration&, Animation&); - bool animationTransitionFrame(Animation&, double); + bool animationTransitionFrame(Animation&, const double); void animationFinishFrame(Animation&); void visitProperties(const std::function& f) { From 70ecf8d62fddfbc8abf1bc44582590b9d7d2540b Mon Sep 17 00:00:00 2001 From: Yousif Aldolaijan Date: Thu, 17 Jul 2025 16:56:25 +0300 Subject: [PATCH 10/15] add tests and fix code based on these test fixes --- src/mbgl/map/transform.cpp | 40 ++++++------- test/map/transform.test.cpp | 116 ++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 20 deletions(-) diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index a114da5a1a4f..232e0b8d62da 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -171,8 +171,8 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& transitionStart = animation->start; transitionDuration = animation->duration; - if (!properties.zoom.set || startZoom != zoom) { - if (properties.zoom.animation) { + if (startZoom != zoom) { + if (properties.zoom.set && properties.zoom.animation) { animationFinishFrame(*properties.zoom.animation); } properties.zoom = { @@ -186,8 +186,8 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& }, }; } - if (!properties.latlng.set || startPoint != endPoint) { - if (properties.latlng.animation) { + if (startPoint != endPoint) { + if (properties.latlng.set && properties.latlng.animation) { animationFinishFrame(*properties.latlng.animation); } properties.latlng = { @@ -202,8 +202,8 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& }, }; } - if (!properties.bearing.set || bearing != startBearing) { - if (properties.bearing.animation) { + if (bearing != startBearing) { + if (properties.bearing.set && properties.bearing.animation) { animationFinishFrame(*properties.bearing.animation); } properties.bearing = { @@ -213,8 +213,8 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& .set = true, }; } - if (!properties.padding.set || padding != startEdgeInsets) { - if (properties.padding.animation) { + if (padding != startEdgeInsets) { + if (properties.padding.set && properties.padding.animation) { animationFinishFrame(*properties.padding.animation); } properties.padding = { @@ -224,8 +224,8 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& .set = true, }; } - if (!properties.pitch.set || pitch != startPitch) { - if (properties.pitch.animation) { + if (pitch != startPitch) { + if (properties.pitch.set && properties.pitch.animation) { animationFinishFrame(*properties.pitch.animation); } properties.pitch = { @@ -380,8 +380,8 @@ void Transform::flyTo(const CameraOptions& inputCamera, transitionStart = animation->start; transitionDuration = animation->duration; - if (!properties.zoom.set || startZoom != zoom) { - if (properties.zoom.animation) { + if (startZoom != zoom) { + if (properties.zoom.set && properties.zoom.animation) { animationFinishFrame(*properties.zoom.animation); } properties.zoom = { @@ -404,8 +404,8 @@ void Transform::flyTo(const CameraOptions& inputCamera, }, }; } - if (!properties.latlng.set || startPoint != endPoint) { - if (properties.latlng.animation) { + if (startPoint != endPoint) { + if (properties.latlng.set && properties.latlng.animation) { animationFinishFrame(*properties.latlng.animation); } properties.latlng = { @@ -425,8 +425,8 @@ void Transform::flyTo(const CameraOptions& inputCamera, }, }; } - if (!properties.bearing.set || bearing != startBearing) { - if (properties.bearing.animation) { + if (bearing != startBearing) { + if (properties.bearing.set && properties.bearing.animation) { animationFinishFrame(*properties.bearing.animation); } properties.bearing = { @@ -436,14 +436,14 @@ void Transform::flyTo(const CameraOptions& inputCamera, .set = true, }; } - if (!properties.padding.set || padding != startEdgeInsets) { - if (properties.padding.animation) { + if (padding != startEdgeInsets) { + if (properties.padding.set && properties.padding.animation) { animationFinishFrame(*properties.padding.animation); } properties.padding = {.animation = animation, .current = startEdgeInsets, .target = padding, .set = true}; } - if (!properties.pitch.set || pitch != startPitch) { - if (properties.pitch.animation) { + if (pitch != startPitch) { + if (properties.pitch.set && properties.pitch.animation) { animationFinishFrame(*properties.pitch.animation); } properties.pitch = { diff --git a/test/map/transform.test.cpp b/test/map/transform.test.cpp index e98b82c7d159..00101c7bb9f3 100644 --- a/test/map/transform.test.cpp +++ b/test/map/transform.test.cpp @@ -1189,3 +1189,119 @@ TEST(Transform, FreeCameraOptionsStateSynchronization) { EXPECT_THAT(up, Vec3NearEquals1E5(vec3{{0, -0.5, 0.866025}})); EXPECT_THAT(forward, Vec3NearEquals1E5(vec3{{0, -0.866025, -0.5}})); } + +TEST(Transform, ConcurrentAnimation) { + Transform transform; + transform.resize({1, 1}); + + const LatLng defaultLatLng{0, 0}; + CameraOptions defaultCameraOptions = + CameraOptions().withCenter(defaultLatLng).withZoom(0).withPitch(0).withBearing(0); + transform.jumpTo(defaultCameraOptions); + ASSERT_DOUBLE_EQ(transform.getLatLng().latitude(), 0); + ASSERT_DOUBLE_EQ(transform.getLatLng().longitude(), 0); + ASSERT_DOUBLE_EQ(0, transform.getZoom()); + ASSERT_DOUBLE_EQ(0, transform.getPitch()); + ASSERT_DOUBLE_EQ(0, transform.getBearing()); + + const LatLng latLng{45, 135}; + const double zoom = 10; + CameraOptions zoomLatLngCameraOptions = CameraOptions().withCenter(latLng).withZoom(zoom); + AnimationOptions zoomLatLngOptions(Seconds(1)); + int zoomLatLngFrameCallbackCount = 0; + zoomLatLngOptions.transitionFrameFn = [&](double t) { + zoomLatLngFrameCallbackCount++; + + ASSERT_TRUE(t >= 0 && t <= 1); + ASSERT_GE(latLng.latitude(), transform.getLatLng().latitude()); + ASSERT_GE(latLng.longitude(), transform.getLatLng().longitude()); + ASSERT_GE(zoom, transform.getZoom()); + }; + int zoomLatLngFinishCallbackCount = 0; + zoomLatLngOptions.transitionFinishFn = [&]() { + zoomLatLngFinishCallbackCount++; + + ASSERT_DOUBLE_EQ(latLng.latitude(), transform.getLatLng().latitude()); + ASSERT_DOUBLE_EQ(latLng.longitude(), transform.getLatLng().longitude()); + ASSERT_DOUBLE_EQ(zoom, transform.getZoom()); + }; + transform.easeTo(zoomLatLngCameraOptions, zoomLatLngOptions); + + ASSERT_TRUE(transform.inTransition()); + + TimePoint transitionStart = transform.getTransitionStart(); + + const double pitch = 60; + const double bearing = 45; + CameraOptions pitchBearingCameraOptions = CameraOptions().withPitch(pitch).withBearing(bearing); + AnimationOptions pitchBearingOptions(Seconds(2)); + int pitchBearingFrameCallbackCount = 0; + pitchBearingOptions.transitionFrameFn = [&](double t) { + pitchBearingFrameCallbackCount++; + + ASSERT_TRUE(t >= 0 && t <= 1); + ASSERT_GE(util::deg2rad(pitch), transform.getPitch()); + ASSERT_LE(-util::deg2rad(bearing), transform.getBearing()); + }; + int pitchBearingFinishCallbackCount = 0; + pitchBearingOptions.transitionFinishFn = [&]() { + pitchBearingFinishCallbackCount++; + + ASSERT_DOUBLE_EQ(util::deg2rad(pitch), transform.getPitch()); + ASSERT_DOUBLE_EQ(-util::deg2rad(bearing), transform.getBearing()); + }; + transform.easeTo(pitchBearingCameraOptions, pitchBearingOptions); + + ASSERT_TRUE(transform.inTransition()); + transform.updateTransitions(transitionStart + Milliseconds(500)); + transform.updateTransitions(transitionStart + Milliseconds(900)); + ASSERT_TRUE(transform.inTransition()); // Second Transition is still running + transform.updateTransitions(transform.getTransitionStart() + Seconds(2)); + ASSERT_FALSE(transform.inTransition()); + + ASSERT_EQ(zoomLatLngFrameCallbackCount, 2); + ASSERT_EQ(zoomLatLngFinishCallbackCount, 1); + ASSERT_EQ(pitchBearingFrameCallbackCount, 2); + ASSERT_EQ(pitchBearingFinishCallbackCount, 1); + + // Test cancelTransitions with concurrent animations + const LatLng latLng2{0, 0}; + const double zoom2 = 0; + CameraOptions zoomLatLngCameraOptions2 = CameraOptions().withCenter(latLng2).withZoom(zoom2); + AnimationOptions zoomLatLngOptions2(Seconds(1)); + transform.easeTo(zoomLatLngCameraOptions2, zoomLatLngOptions2); + + const double pitch2 = 0; + const double bearing2 = 0; + CameraOptions pitchBearingCameraOptions2 = CameraOptions().withPitch(pitch2).withBearing(bearing2); + AnimationOptions pitchBearingOptions2(Seconds(2)); + transform.easeTo(pitchBearingCameraOptions2, pitchBearingOptions2); + + ASSERT_TRUE(transform.inTransition()); + transform.cancelTransitions(); + ASSERT_FALSE(transform.inTransition()); + + // Reset State + transform.jumpTo(defaultCameraOptions); + + zoomLatLngFrameCallbackCount = 0; + zoomLatLngFinishCallbackCount = 0; + transform.easeTo(zoomLatLngCameraOptions, zoomLatLngOptions); + + transitionStart = transform.getTransitionStart(); + + pitchBearingFrameCallbackCount = 0; + pitchBearingFinishCallbackCount = 0; + pitchBearingOptions.duration = Seconds(0); + transform.easeTo(pitchBearingCameraOptions, pitchBearingOptions); + + ASSERT_TRUE(transform.inTransition()); + transform.updateTransitions(transitionStart + Milliseconds(500)); + transform.updateTransitions(transitionStart + Seconds(1)); + ASSERT_FALSE(transform.inTransition()); + + ASSERT_EQ(zoomLatLngFrameCallbackCount, 2); + ASSERT_EQ(zoomLatLngFinishCallbackCount, 1); + ASSERT_EQ(pitchBearingFrameCallbackCount, 0); + ASSERT_EQ(pitchBearingFinishCallbackCount, 1); +} From 5730ce7237a42223a95e5dc10a0589cc6c838080 Mon Sep 17 00:00:00 2001 From: Yousif Aldolaijan Date: Tue, 22 Jul 2025 13:00:08 +0300 Subject: [PATCH 11/15] correctly call the finish callback when a value is set but not used --- src/mbgl/map/transform.cpp | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index 232e0b8d62da..8e868ed04c90 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -172,7 +172,7 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& transitionDuration = animation->duration; if (startZoom != zoom) { - if (properties.zoom.set && properties.zoom.animation) { + if (properties.zoom.set && properties.zoom.current != properties.zoom.target && properties.zoom.animation) { animationFinishFrame(*properties.zoom.animation); } properties.zoom = { @@ -187,7 +187,8 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& }; } if (startPoint != endPoint) { - if (properties.latlng.set && properties.latlng.animation) { + if (properties.latlng.set && properties.latlng.current != properties.latlng.target && + properties.latlng.animation) { animationFinishFrame(*properties.latlng.animation); } properties.latlng = { @@ -203,7 +204,8 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& }; } if (bearing != startBearing) { - if (properties.bearing.set && properties.bearing.animation) { + if (properties.bearing.set && properties.bearing.current != properties.bearing.target && + properties.bearing.animation) { animationFinishFrame(*properties.bearing.animation); } properties.bearing = { @@ -214,7 +216,8 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& }; } if (padding != startEdgeInsets) { - if (properties.padding.set && properties.padding.animation) { + if (properties.padding.set && properties.padding.current != properties.padding.target && + properties.padding.animation) { animationFinishFrame(*properties.padding.animation); } properties.padding = { @@ -225,7 +228,7 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& }; } if (pitch != startPitch) { - if (properties.pitch.set && properties.pitch.animation) { + if (properties.pitch.set && properties.pitch.current != properties.pitch.target && properties.pitch.animation) { animationFinishFrame(*properties.pitch.animation); } properties.pitch = { @@ -381,7 +384,7 @@ void Transform::flyTo(const CameraOptions& inputCamera, transitionDuration = animation->duration; if (startZoom != zoom) { - if (properties.zoom.set && properties.zoom.animation) { + if (properties.zoom.set && properties.zoom.current != properties.zoom.target && properties.zoom.animation) { animationFinishFrame(*properties.zoom.animation); } properties.zoom = { @@ -405,7 +408,8 @@ void Transform::flyTo(const CameraOptions& inputCamera, }; } if (startPoint != endPoint) { - if (properties.latlng.set && properties.latlng.animation) { + if (properties.latlng.set && properties.latlng.current != properties.latlng.target && + properties.latlng.animation) { animationFinishFrame(*properties.latlng.animation); } properties.latlng = { @@ -426,7 +430,8 @@ void Transform::flyTo(const CameraOptions& inputCamera, }; } if (bearing != startBearing) { - if (properties.bearing.set && properties.bearing.animation) { + if (properties.bearing.set && properties.bearing.current != properties.bearing.target && + properties.bearing.animation) { animationFinishFrame(*properties.bearing.animation); } properties.bearing = { @@ -437,13 +442,14 @@ void Transform::flyTo(const CameraOptions& inputCamera, }; } if (padding != startEdgeInsets) { - if (properties.padding.set && properties.padding.animation) { + if (properties.padding.set && properties.padding.current != properties.padding.target && + properties.padding.animation) { animationFinishFrame(*properties.padding.animation); } properties.padding = {.animation = animation, .current = startEdgeInsets, .target = padding, .set = true}; } if (pitch != startPitch) { - if (properties.pitch.set && properties.pitch.animation) { + if (properties.pitch.set && properties.pitch.current != properties.pitch.target && properties.pitch.animation) { animationFinishFrame(*properties.pitch.animation); } properties.pitch = { From f6a66c18ff105fb7c6168b6445a0eaef66336c20 Mon Sep 17 00:00:00 2001 From: Yousif Aldolaijan Date: Tue, 22 Jul 2025 13:17:46 +0300 Subject: [PATCH 12/15] resolve changing parameters through flyto --- src/mbgl/map/transform.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index 8e868ed04c90..814d6693e9e3 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -171,7 +171,7 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& transitionStart = animation->start; transitionDuration = animation->duration; - if (startZoom != zoom) { + if (!properties.zoom.set || startZoom != zoom) { if (properties.zoom.set && properties.zoom.current != properties.zoom.target && properties.zoom.animation) { animationFinishFrame(*properties.zoom.animation); } @@ -186,7 +186,7 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& }, }; } - if (startPoint != endPoint) { + if (!properties.latlng.set || startPoint != endPoint) { if (properties.latlng.set && properties.latlng.current != properties.latlng.target && properties.latlng.animation) { animationFinishFrame(*properties.latlng.animation); @@ -203,7 +203,7 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& }, }; } - if (bearing != startBearing) { + if (!properties.bearing.set || bearing != startBearing) { if (properties.bearing.set && properties.bearing.current != properties.bearing.target && properties.bearing.animation) { animationFinishFrame(*properties.bearing.animation); @@ -215,7 +215,7 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& .set = true, }; } - if (padding != startEdgeInsets) { + if (!properties.padding.set || padding != startEdgeInsets) { if (properties.padding.set && properties.padding.current != properties.padding.target && properties.padding.animation) { animationFinishFrame(*properties.padding.animation); @@ -227,7 +227,7 @@ void Transform::easeTo(const CameraOptions& inputCamera, const AnimationOptions& .set = true, }; } - if (pitch != startPitch) { + if (!properties.pitch.set || pitch != startPitch) { if (properties.pitch.set && properties.pitch.current != properties.pitch.target && properties.pitch.animation) { animationFinishFrame(*properties.pitch.animation); } @@ -383,7 +383,7 @@ void Transform::flyTo(const CameraOptions& inputCamera, transitionStart = animation->start; transitionDuration = animation->duration; - if (startZoom != zoom) { + if (!properties.zoom.set || startZoom != zoom) { if (properties.zoom.set && properties.zoom.current != properties.zoom.target && properties.zoom.animation) { animationFinishFrame(*properties.zoom.animation); } @@ -407,7 +407,7 @@ void Transform::flyTo(const CameraOptions& inputCamera, }, }; } - if (startPoint != endPoint) { + if (!properties.latlng.set || startPoint != endPoint) { if (properties.latlng.set && properties.latlng.current != properties.latlng.target && properties.latlng.animation) { animationFinishFrame(*properties.latlng.animation); @@ -429,7 +429,7 @@ void Transform::flyTo(const CameraOptions& inputCamera, }, }; } - if (bearing != startBearing) { + if (!properties.bearing.set || bearing != startBearing) { if (properties.bearing.set && properties.bearing.current != properties.bearing.target && properties.bearing.animation) { animationFinishFrame(*properties.bearing.animation); @@ -441,14 +441,14 @@ void Transform::flyTo(const CameraOptions& inputCamera, .set = true, }; } - if (padding != startEdgeInsets) { + if (!properties.padding.set || padding != startEdgeInsets) { if (properties.padding.set && properties.padding.current != properties.padding.target && properties.padding.animation) { animationFinishFrame(*properties.padding.animation); } properties.padding = {.animation = animation, .current = startEdgeInsets, .target = padding, .set = true}; } - if (pitch != startPitch) { + if (!properties.pitch.set || pitch != startPitch) { if (properties.pitch.set && properties.pitch.current != properties.pitch.target && properties.pitch.animation) { animationFinishFrame(*properties.pitch.animation); } From adf734cd70d113467cd5aca45dbeb7f973245bf3 Mon Sep 17 00:00:00 2001 From: Yousif Aldolaijan Date: Wed, 5 Nov 2025 12:48:04 +0300 Subject: [PATCH 13/15] disable cancel transitions when concurrent animations are set --- .../src/main/java/org/maplibre/android/maps/Transform.java | 3 +-- platform/ios/src/MLNMapView.mm | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/Transform.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/Transform.java index 3b7f5a9cc1c2..a26fb0c942d3 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/Transform.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/Transform.java @@ -192,8 +192,7 @@ CameraPosition invalidateCameraPosition() { void cancelTransitions() { MapLibreMap map = mapView.getMapLibreMap(); if (map != null && map.getLocationComponent().isLocationComponentActivated() - && map.getLocationComponent().getLocationComponentOptions().concurrentCameraAnimationsEnabled() - && map.getLocationComponent().isLocationTracking()) { + && map.getLocationComponent().getLocationComponentOptions().concurrentCameraAnimationsEnabled()) { return; } diff --git a/platform/ios/src/MLNMapView.mm b/platform/ios/src/MLNMapView.mm index 8c7b0e19b62b..da45100d4e78 100644 --- a/platform/ios/src/MLNMapView.mm +++ b/platform/ios/src/MLNMapView.mm @@ -4484,7 +4484,7 @@ - (void)_flyToCamera:(MLNMapCamera *)camera edgePadding:(UIEdgeInsets)insets wit } - (void)cancelTransitions { - if (!_mbglMap || (self.enableConcurrentCameraAnimation && self.userTrackingMode != MLNUserTrackingModeNone && self.userTrackingMode != MLNUserTrackingModeFollow)) + if (!_mbglMap || self.enableConcurrentCameraAnimation) { return; } From a703442c9d4995935b5fec40d1e31ea0064d9183 Mon Sep 17 00:00:00 2001 From: Yousif Aldolaijan Date: Tue, 11 Nov 2025 16:28:05 +0300 Subject: [PATCH 14/15] fix concurrent anchor with zoom and latlng changes --- src/mbgl/map/transform.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index 5eb28fa0eb0a..517e954ec357 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -698,7 +698,8 @@ void Transform::updateTransitions(const TimePoint& now) { .withRotatingInProgress(rotating)); const bool zoomSet = properties.zoom.set && properties.zoom.animation; - if ((properties.latlng.set && properties.latlng.animation) || zoomSet) { + const bool latlngSet = properties.latlng.set && properties.latlng.animation; + if (latlngSet || zoomSet) { state.setLatLngZoom( properties.latlng.frameLatLngFunc ? properties.latlng.frameLatLngFunc(now) : state.getLatLng(), properties.zoom.frameZoomFunc ? properties.zoom.frameZoomFunc(now) : state.getZoom()); @@ -711,8 +712,12 @@ void Transform::updateTransitions(const TimePoint& now) { properties.zoom.set = false; } + // Prioritize zoom anchor over latlng anchor if two concurrent animations are active, + // if it's the same animation then we apply the first one only. if (zoomSet && properties.zoom.animation->anchor) { state.moveLatLng(properties.zoom.animation->anchorLatLng, *properties.zoom.animation->anchor); + } else if (latlngSet && properties.latlng.animation->anchor) { + state.moveLatLng(properties.latlng.animation->anchorLatLng, *properties.latlng.animation->anchor); } } if (properties.bearing.set && properties.bearing.animation) { @@ -744,10 +749,6 @@ void Transform::updateTransitions(const TimePoint& now) { if (animationTransitionFrame(*properties.pitch.animation, pitch_t)) { properties.pitch.set = false; } - - if (properties.pitch.set && properties.pitch.animation && properties.pitch.animation->anchor) { - state.moveLatLng(properties.pitch.animation->anchorLatLng, *properties.pitch.animation->anchor); - } } panning = false; From cb059d32e8e41f957096ca0e0a1d2dc02e4d0177 Mon Sep 17 00:00:00 2001 From: Yousif Aldolaijan Date: Wed, 12 Nov 2025 14:32:33 +0300 Subject: [PATCH 15/15] add missing mocked methods to android transform test --- .../src/test/java/org/maplibre/android/maps/TransformTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/TransformTest.kt b/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/TransformTest.kt index 75acdd8c60a0..e64a95e7a7e5 100644 --- a/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/TransformTest.kt +++ b/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/TransformTest.kt @@ -29,6 +29,7 @@ class TransformTest : BaseTest() { nativeMapView = mockk() mainLooper = shadowOf(getMainLooper()) transform = Transform(mapView, nativeMapView, cameraChangeDispatcher) + every { mapView.getMapLibreMap() } returns null every { nativeMapView.isDestroyed } returns false every { nativeMapView.cameraPosition } returns CameraPosition.DEFAULT every { nativeMapView.cancelTransitions() } answers {}