Skip to content

Conversation

yousifd
Copy link
Contributor

@yousifd yousifd commented May 19, 2025

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.

Copy link
Member

@louwers louwers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allowing concurrent animations is a nice feature, but without any changes to the platform APIs I don't think this implementation is backward compatible? It should be.

Some general notes:

  • Implementations should be added to .cpp files (unless it's a class/function template implementation), not in headers.
  • Do not use single-letter variables.

@@ -16,6 +16,30 @@

namespace mbgl {

template <class T>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these structs with everything public?

Why are you using a shared pointer? Does that data need to be heap allocated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these structs with everything public?

They don't have to be public, will move them to be private.

Why are you using a shared pointer? Does that data need to be heap allocated?

It needs to be shared across all PropertyAnimations member variables. We need it so that a latlng and zoom that share the same PropertyAnimation (such as when you update the camera to translate and zoom at the same time) you don't have to keep track of two copies of the same PropertyAnimation and only have to update one when necessary and the other would be updated as well. For example, if an animation has a finish frame callback and that animation updates both the latlng and the zoom values of the camera we only call the finish frame callback once for either of the latlng or zoom PropertyAnimation objects and we wouldn't call it again for the other PropertyAnimation. This is necessary to keep backwards compatibility for animations that span multiple properties.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@louwers I've exposed a flag to enable or disable this feature during navigation for both android and ios. I've also cleaned up large parts of the code and fixed some of the failing tests. Please let me know if you need help understanding the changes or if you think some of the changes should be done differently.

bool animationTransitionFrame(std::shared_ptr<PropertyAnimation>&, double);
void animationFinishFrame(std::shared_ptr<PropertyAnimation>&);

void visit_pas(const std::function<void(std::shared_ptr<PropertyAnimation>&)>& f) {
Copy link
Member

@louwers louwers May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we tend to use camelCase.

Copy link

Bloaty Results (iOS) 🐋

Compared to main

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.1% +13.6Ki  +0.1% +16.0Ki    TOTAL

Full report: https://maplibre-native.s3.eu-central-1.amazonaws.com/bloaty-results-ios/pr-3487-compared-to-main.txt

@yousifd
Copy link
Contributor Author

yousifd commented Jun 22, 2025

@louwers

but without any changes to the platform APIs I don't think this implementation is backward compatible? It should be.

This implementation is backwards compatible. What changes to the platform APIs are you expecting?

yousifd added 7 commits June 26, 2025 16:05
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.
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.
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.
@yousifd yousifd force-pushed the concurrent_transforms branch from 06e7a0e to 5caff41 Compare July 1, 2025 09:38
@yousifd yousifd requested a review from louwers July 1, 2025 11:25
const AnimationOptions&,
const std::function<void(double)>&,
const Duration&);
void startTransition(const CameraOptions&, const Duration&, std::shared_ptr<Animation>&);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing a shared pointer by reference does not make sense I think.

Just take a normal reference and dereference the pointer when calling it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done 0f387f2

scaling(scaling_),
rotating(rotating_) {}

double interpolant(TimePoint);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure about the name myself but that's the name of a function that returns the value used for interpolation https://en.wiktionary.org/wiki/interpolant

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it can be const right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

*
* @return whether concurrent camera animations are enabled or disabled
*/
public LocationComponentOptions.Builder concurrentCameraAnimationsEnabled(Boolean concurrentCameraAnimationsEnabled) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enabling concurrent camera animations is not only relevant for the LocationComponent, is it? So it should not be a setting that exists only here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently that's the only use case I can think of. If you want to have multiple animations at the same time from the application side then you could just bundle the animations into a single setCamera call.

If you still think that concurrent camera animations should be universal then I'd also have to change the logic around cancelTransitions to not check for the current camera mode.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to have multiple animations at the same time from the application side then you could just bundle the animations into a single setCamera call.

Why is this not possible in the location component?

What if someone wants to implement their own location component?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I understand. Would it make more sense to add a boolean to the java Transform class and expose the concurrent animations flag from the MapView class instead?

@yousifd yousifd requested a review from louwers July 2, 2025 08:38
private:
template <class T>
struct Property {
std::shared_ptr<Animation> animation;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a shared pointer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Animation object is used to keep track of the properties current animation. It keeps track whether we've ran the animation's frame callback, animation's finish callback, and the panning, scaling, and rotating state.

So, to avoid updating the above for every property we use a shared pointer to ensure that if any one of the properties finished running the animation's frame or finish callbacks then the other one doesn't do it again.

bool animationTransitionFrame(Animation&, double);
void animationFinishFrame(Animation&);

void visitProperties(const std::function<void(Animation&)>& f) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't use single-letter variables.

Copy link
Contributor Author

@yousifd yousifd Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if I can come up with a more descrptive name for the function, maybe I can call it func. I followed the same style you already have for visit functions similar to RenderOrchestrator::visitLayerGroups.

@@ -692,4 +806,15 @@ void Transform::setFreeCameraOptions(const FreeCameraOptions& options) {
state.setFreeCameraOptions(options);
}

double Transform::Animation::interpolant(TimePoint now) {
bool isAnimated = duration != Duration::zero();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use the method you added.

.target = endPoint,
.set = true,
.frameLatLngFunc =
[=, this](TimePoint now) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!activeAnimation) {
activeAnimation = true;

bool panning = false, scaling = false, rotating = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we tend not to use multiple variable declarations.

https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-name-one

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -132,22 +132,87 @@ class Transform : private util::noncopyable {
FreeCameraOptions getFreeCameraOptions() const;
void setFreeCameraOptions(const FreeCameraOptions& options);

struct Animation {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be a struct? Can it be a class?

Copy link
Contributor Author

@yousifd yousifd Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just a container class of related information. I don't think there is any need to make the properties private by default and add accessors. I can make the whole definition of Animation be private to avoid others using it if that makes the usage clearer?

@@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: instead of renaming this to options, perhaps use animationOptions? Goes for several places.

struct Animation {
TimePoint start;
Duration duration;
AnimationOptions options;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An Animation has a duration, but an Animation also has an AnimationsOptions object with an optional duration. I think this needs some more refactoring...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in the flyTo and easeTo functions where we initialize the Animation object there is logic around setting the duration value that doesn't necessarily equal to the duration value in AnimationOptions. Maybe I could change those to override the value in AnimationOptions but that might have unintended side effects.

Copy link
Member

@louwers louwers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some more comments. As discussed on Slack, needs test coverage before we can consider merging this.

General comment: please use const for definitions wherever possible.

Copy link

Bloaty Results 🐋

Compared to main

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.1%  +114Ki  +0.1% +28.9Ki    TOTAL

Full report: https://maplibre-native.s3.eu-central-1.amazonaws.com/bloaty-results/pr-3487-compared-to-main.txt

Compared to d387090 (legacy)

    FILE SIZE        VM SIZE    
 --------------  -------------- 
   +33% +38.3Mi  +449% +26.8Mi    TOTAL

Full report: https://maplibre-native.s3.eu-central-1.amazonaws.com/bloaty-results/pr-3487-compared-to-legacy.txt

Copy link

Benchmark Results ⚡

Benchmark                                                     Time             CPU      Time Old      Time New       CPU Old       CPU New
------------------------------------------------------------------------------------------------------------------------------------------
OVERALL_GEOMEAN                                            +0.0194         +0.0192             0             0             0             0

Full report: https://maplibre-native.s3.eu-central-1.amazonaws.com/benchmark-results/pr-3487-compared-to-main.txt

@yousifd yousifd requested a review from louwers July 17, 2025 14:31
Copy link
Member

@louwers louwers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried the GLFW app, you can build and run it with:

cmake --preset macos
cmake --build build-macos --target mbgl-glfw
build-macos/platform/glfw/mbgl-glfw --style https://tiles.openfreemap.org/styles/liberty

You can jump between various locations with the 'A' key.

It seems to be broken with this PR.

Before:

before.mov

After:

Screen.Recording.2025-07-21.at.18.54.56.mov

I am skeptical of the approach taken by this PR. Whether concurrent camera animations are enabled or not should be set on the C++ level, not defined at the platform level, definitely not tucked away in an internal component only used by the platform itself. Relying on cancelTransitions being called is brittle and not backward-compatible on all platforms. Also the Transform class having knowledge of the location component is not a good design. In its current form I do not feel comfortable approving this PR.

What I suggest now and for next time: create an issue describing the problem first. Perhaps with a video or two. I still do not 100% understand the problem being addressed here. Especially when modifying fundamental code in the library that has the potential to disrupt our users, it is better to discuss the issue first or even write a design proposal. That way we can together explore the solution space and choose a good approach before code is written.

It took a while for my opinion to crystalize. I apologize that you went though several rounds of reviews and addressing comments. I aim to do better next time and I hope this does not discourage you from contributing.

@yousifd
Copy link
Contributor Author

yousifd commented Jul 22, 2025

@louwers I fully understand and I should have started with an issue or some sort of longer form discussion about the problem. I just got a bit too excited and shared this PR hoping we can have this discussion here. I'll post the issue with all the necessary details and the approach I took here and we can go from there.

I fully understand your reluctance to merge this and I expected it. I'll make sure to follow the same process going forward for other significant changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants