Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

[ios] Map view can now be optionally configured to make pitch adjustments w… #12518

Closed
wants to merge 289 commits into from

Conversation

d-prukop
Copy link
Contributor

@d-prukop d-prukop commented Jul 31, 2018

Map view can now be optionally configured to make pitch adjustments without affecting altitude.

This addresses part of #9808

Pitch changes using MGLAltitudeForZoomLevel and MGLZoomLevelForAltitude from the MGLGeometry library were animating in a strange fashion when using the new proposed camera animation system (located here: https://github.com/mapbox/mapbox-ios-map-camera).

I added a flag to turn off this association between pitch and altitude.

I also added a function to directly update the camera with edge insets. Edge insets in this method are used to control the point on screen where the map rotates and where the center coordinate will be displayed.

Before | After
before | after

@julianrex
Copy link
Contributor

@d-prukop is there any possible overlap with #12203?

@d-prukop
Copy link
Contributor Author

d-prukop commented Jul 31, 2018

@julianrex It does seem like it has some overlap with #12203. This approach is not specifically moving the center coord to screen point location. I'm using padding to get that effect. It shouldn't help or hinder any efforts to get a specific anchor point built. @1ec5 do you want to talk about this approach?


When this property is set to `YES`, pitch will work independently from altitude.
*/
@property(nonatomic, getter=isAltitudeUnaffectedByPitch) BOOL altitudeUnaffectedByPitch;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the opposite altitudeAffectedByPitch might be less confusing? Also, we should specify the default value.

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 chose this naming for the purpose of defaulting to false. I'm definitely open to better naming. When you get altitude from the internal GL core zoom level, pitch is taken into account in the calculation. Same thing when you set zoom level from altitude. I want just the pure altitude and the pure pitch. Any suggestions?

@param padding The minimum padding (in screen points) that would be visible

*/
- (void)jumpToCamera:(MGLMapCamera *)camera edgePadding:(UIEdgeInsets)insets;
Copy link
Contributor

Choose a reason for hiding this comment

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

My understanding is that this essentially exposes core's jumpTo method. But it feels close to -flyToCamera:withDuration:completionHandler: with a 0 duration (which calls down to the internal method -_flyToCamera:edgePadding:withDuration:peakAltitude:completionHandler:.

I think this could be confusing. Thoughts?

Secondly, I think this method might need a completion handler.

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 definitely need direct access to the edgePadding argument. I also don't want any animation since I'm controlling the animation outside of the iOS SDK. I looked into using one of the setCamera methods but I couldn't get it to work the way I wanted. Plus it appeared to drop frames vs this method. This could probably use a closer look.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right know we have two direct flavors to set the camera the setCamera: XXX and flyToCamera, jumpToCamera sounds like falls into the first flavor, would it be possible to add setCamera:edgePadding: instead?

Copy link
Contributor

@fabian-guerra fabian-guerra left a comment

Choose a reason for hiding this comment

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

As we discussed could you please add a gif for how it looks when enabled and disabled.


When this property is set to `YES`, pitch will work independently from altitude.
*/
@property(nonatomic, getter=isAltitudeUnaffectedByPitch) BOOL altitudeUnaffectedByPitch;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it OK if we specify that this property controls the camera updates and nothing more, something along the lines —feel free to add different wording—:

A Boolean value that determines wether updating the camera pitch will also affect the altitude.
When this property is set to YES, pitch will work independently from altitude.
The default value of this property is NO.

Also I think the documentation needs a way to clarify the scope of this change and how people may take advantage, I know you already have a repo with experimental changes, but communicating how devs may take advantage of this I think is important.

In the SDK we follow the convention of avoid using negatives as part of boolean properties. As @julianrex said isAltitudeAffectedByPitch follows that pattern. But also is it more clear the intent using something like isCameraAltitudeAffectedByPitch?

@param padding The minimum padding (in screen points) that would be visible

*/
- (void)jumpToCamera:(MGLMapCamera *)camera edgePadding:(UIEdgeInsets)insets;
Copy link
Contributor

Choose a reason for hiding this comment

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

Right know we have two direct flavors to set the camera the setCamera: XXX and flyToCamera, jumpToCamera sounds like falls into the first flavor, would it be possible to add setCamera:edgePadding: instead?

@friedbunny friedbunny added feature iOS Mapbox Maps SDK for iOS labels Jul 31, 2018
map camera without the built-in side effect that pitch influences altitude. This will make the
results of using outside animation more predictable and easier to control.
*/
@property(nonatomic, getter=isCameraAltitudeAffectedByPitch) BOOL cameraAltitudeAffectedByPitch;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I think the second line is redundant, what do you think about:

A Boolean value that determines whether the updating pitch will also affect the altitude.

Setting this property to `NO` will allow animation libraries outside of this SDK to control the
map camera without the built-in side effect that pitch influences altitude. This will make the
results of using outside animation more predictable and easier to control.

The default value of this property is `YES`.

@param edgePadding The minimum padding (in screen points) that would be visible

*/
- (void)setCamera:(MGLMapCamera *)camera edgePadding:(UIEdgeInsets)edgePadding;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: In this case it may not be necessary to set a specific use case since this falls into the setCamera methods.

 Moves the viewpoint to a different location without using a transition.
 
 @param camera The new viewpoint.
 @param edgePadding The minimum padding (in screen points) that would be visible.

map camera without the built-in side effect that pitch influences altitude. This will make the
results of using outside animation more predictable and easier to control.

The default value of this property is YES.
Copy link
Contributor

Choose a reason for hiding this comment

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

We require all types be marked with ` to get rendered properly in our documentation.
Nit: YES

Copy link
Contributor

@fabian-guerra fabian-guerra left a comment

Choose a reason for hiding this comment

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

LGTM

@julianrex
Copy link
Contributor

Thanks @d-prukop!

@friedbunny
Copy link
Contributor

friedbunny commented Aug 1, 2018

I’m excited about improvements to the camera system — these animations look quite nice. I have a couple questions about how we should implement these particular changes:

Should MGLMapView.cameraAltitudeAffectedByPitch be public API?

Or would it be acceptable to stash it in MGLMapView_Private.h and forward-declare in client implementations? As we prototype a new camera system, it feels like we’d be best served by not broadening/muddying the public API surface until we have a solid vision of what we want to support going forward.

Should pitch affect altitude?

Would it make sense to change the default behavior instead?

@d-prukop
Copy link
Contributor Author

d-prukop commented Aug 1, 2018

I'm OK with making this private since it will probably only be used by a new camera system. We should talk about the future of the camera!

I think the default behavior was put in there to mimic the way mapkit works. Whether we want that is a larger discussion.

Copy link
Contributor

@1ec5 1ec5 left a comment

Choose a reason for hiding this comment

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

I think it would be beneficial to consider alternatives to this approach that still address the issue you’ve brought up. In addition to the feedback below, please note that all changes to the iOS public API require corresponding blurbs in the iOS changelog, and that we try to keep the iOS and macOS implementations of MGLMapView in sync as much as possible.

@@ -3374,7 +3382,8 @@ - (MGLMapCamera *)cameraForCameraOptions:(const mbgl::CameraOptions &)cameraOpti
double zoomLevel = cameraOptions.zoom ? *cameraOptions.zoom : self.zoomLevel;
CLLocationDirection direction = cameraOptions.angle ? mbgl::util::wrap(-MGLDegreesFromRadians(*cameraOptions.angle), 0., 360.) : self.direction;
CGFloat pitch = cameraOptions.pitch ? MGLDegreesFromRadians(*cameraOptions.pitch) : _mbglMap->getPitch();
CLLocationDistance altitude = MGLAltitudeForZoomLevel(zoomLevel, pitch, centerCoordinate.latitude, self.frame.size);
CLLocationDegrees pitchForAltitude = self.isCameraAltitudeAffectedByPitch ? pitch : 0.0;
CLLocationDistance altitude = MGLAltitudeForZoomLevel(zoomLevel, pitchForAltitude, centerCoordinate.latitude, self.frame.size);
Copy link
Contributor

Choose a reason for hiding this comment

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

Pitch is by definition affected by zoom level: when the map is tilted and the zoom level changes (whether programmatically or via a gesture), the viewing distance from the focal point and the viewpoint’s distance from the ground (the altitude) both change.

These changes affect the camera getters only. So the new property is a strange way of telling the getter to effectively lie about the camera relative to what mbgl thinks the camera is. Is there no way to derive the correct value from the existing return value, perhaps by exposing MGLAltitudeForZoomLevel publicly, as proposed in #5583?

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 don't think mbgl comes in to this at all. As far as I can tell this is all happening in MGLGeometry in the iOS SDK. The pure values in mbgl are what I want.

Copy link
Contributor Author

@d-prukop d-prukop Aug 1, 2018

Choose a reason for hiding this comment

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

The pure values in mbgl are what I want.

This is wrong.
The problem with exposing MGLAltitudeForZoomLevel is that, if you use setCamera, mbgl sets it's state from the result of MGLZoomLevelForAltitude. The stored zoomLevel is affected by pitch. I want to prevent the mbgl state from getting that value. I want it to store the zoomLevel without regards to pitch.

Copy link
Contributor

@1ec5 1ec5 Aug 1, 2018

Choose a reason for hiding this comment

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

If you want to change the zoom level without changing the pitch, set a camera whose pitch is -1. This behavior is documented in MGLMapCamera but it could stand to be documented more discoverably. (If this SDK were written in Swift without regard for Obective-C compatibility, the various properties of MGLMapView would all be optional.)

Copy link
Contributor

@1ec5 1ec5 Aug 3, 2018

Choose a reason for hiding this comment

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

@d-prukop and I chatted about this today. The pitch is considered twice when calculating the camera options, once for setting the camera’s actual pitch and once in calculating the zoom level. I would’ve thought that this is ideal, because the zoom level on a tilted map is really the viewing distance, but I suppose if the intention is to iteratively change the viewing distance by getting and setting the camera iteratively, then there could be a sort of feedback effect.

The essence of my concern here is that we’re adding a separate code path for a specific use case. If we really believe that the altitude-to-zoom-level conversion shouldn’t be affected by the pitch, then why not remove the pitch argument from MGLZoomLevelForAltitude() and MGLAltitudeForZoomLevel() and always assume a pitch of 0°? The obvious consequence would be that the MGLMapCamera returned by the camera getter would have the same altitude regardless of the pitch.

In other words, isCameraAltitudeAffectedByPitch changes the meaning of altitude from “shortest distance between the viewpoint and the ground” to “viewing distance from the focal point”, essentially a linear, physical-unit version of zoomLevel (and not what the word “altitude” means in real life). Maybe that’s what the use case above calls for: a property for explicitly setting the viewing distance of a camera instead of the altitude. We already have a zoomLevel property in MGLMapSnapshotOptions, so why not add an alternative to altitude to MGLMapCamera? One reason why is that it’s awkward for an object to sport mutually exclusive properties. Any future attempt at making the isCameraAltitudeAffectedByPitch option public or defaulting it to YES will need to consider whether we instead need a viewingDistance property on MGLMapCamera.


The default value of this property is `YES`.
*/
@property(nonatomic, getter=isCameraAltitudeAffectedByPitch) BOOL cameraAltitudeAffectedByPitch;
Copy link
Contributor

Choose a reason for hiding this comment

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

MGLMapView’s public API is expressed in terms of zoom levels, not in terms of altitude. Generally speaking, MGLMapView’s direct parameters are tied to 2D concepts for simplicity, while MGLMapCamera is better suited for expressing 3D concepts because it can account for two different frames of reference (viewpoint versus focal point). Mixing the two concepts would promote one frame of reference over the other, which would call into question the very purpose of having MGLMapCamera as a separate class.

As far as MGLMapView is concerned, pitch is a detail of MGLMapCamera. You’ll note that the rest of MGLMapView avoids referring to pitch, because normally the pitch is intertwined with other camera parameters and the camera’s frame of reference.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right now if I want to change pitch I have to use MGLMapCamera. When the MGLMapCamera is passed to cameraOptionsObjectForAnimatingToCamera, mbgl updates it's state with the zoomLevel calculated from MGLZoomLevelForAltitude.

All of the setCamera functions use cameraOptionsObjectForAnimatingToCamera.

Copy link
Contributor

Choose a reason for hiding this comment

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

My point is that MGLMapView’s API shouldn’t even mention pitch. If mentions pitch, it should provide the full complement of ways to interact with the pitch, which it doesn’t by design.

Think of camera as a set of parameters that get updated together. They have to be updated together in order to maintain consistency. If you want to update the pitch without updating other parameters, you set the other parameters to special values. For example, set heading to -1 or centerCoordinate to an invalid coordinate.

@julianrex
Copy link
Contributor

Perhaps moving the property to MGLCamera is a solution here. The property name already hints that it might be in the wrong place. You'd need some additional scaffolding to pass it down appropriately though.

@d-prukop
Copy link
Contributor Author

d-prukop commented Aug 1, 2018

I'm not sure I can move it to MGLCamera. MGLMapView makes MGLCameras from mgbl::CameraOptions and I need to tell it how to handle those in the case where I want pitch and altitude separated.

Copy link
Contributor

@1ec5 1ec5 left a comment

Choose a reason for hiding this comment

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

It might be worth rewording the PR description so that the public change gets top billing and the unrelated private change (which ideally would go in a separate PR) is mentioned in passing, rather than the other way around.

@param edgePadding The minimum padding (in screen points) that would be visible

*/
- (void)setCamera:(MGLMapCamera *)camera edgePadding:(UIEdgeInsets)edgePadding;
Copy link
Contributor

Choose a reason for hiding this comment

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

This method should accept an animated parameter. Just as there’s a -setVisibleCoordinateBounds:animated: and -setVisibleCoordinateBounds:edgePadding:animated:, there should be -setCamera:animated: and -setCamera:edgePadding:animated:. It seems unfair to force the developer to choose between specifying an edge padding and using the built-in animation capabilities, even if your particular use case doesn’t require those capabilities.

@@ -240,6 +240,9 @@ @interface MGLMapView () <UIGestureRecognizerDelegate,
@property (nonatomic) MGLUserLocation *userLocation;
@property (nonatomic) NSMutableDictionary<NSString *, NSMutableArray<MGLAnnotationView *> *> *annotationViewReuseQueueByIdentifier;

/// A Boolean value that determines whether the updating pitch will also affect the altitude.
@property(nonatomic, getter=isCameraAltitudeAffectedByPitch) BOOL cameraAltitudeAffectedByPitch;
Copy link
Contributor

Choose a reason for hiding this comment

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

A more natural name for the getter would be cameraAltitudeIsAffectedByPitch or, given #12518 (comment), cameraAltitudeIsViewingDistance.

jfirebaugh and others added 25 commits September 19, 2018 14:54
This is meant to
(1) Make it easier for new developers to find the source
(2) Make it easier to look at shader diffs when the GL JS pin changes
@d-prukop
Copy link
Contributor Author

Abandoning this PR. Going to start a new one

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
feature iOS Mapbox Maps SDK for iOS
Projects
None yet
Development

Successfully merging this pull request may close these issues.