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

Better zoom level–altitude conversion #3362

Merged
merged 3 commits into from
Dec 21, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Known issues:
- You can now modify an annotation’s image after adding the annotation to the map. ([#3146](https://github.com/mapbox/mapbox-gl-native/pull/3146))
- Tapping now selects annotations more reliably. Tapping near the top of a large annotation image now selects that annotation. An annotation image’s alignment insets influence how far away the user can tap and still select the annotation. For example, if your annotation image has a large shadow, you can keep that shadow from being tappable by excluding it from the image’s alignment rect. ([#3261](https://github.com/mapbox/mapbox-gl-native/pull/3261))
- A new method on MGLMapView, `-flyToCamera:withDuration:completionHandler:`, lets you transition between viewpoints along an arc as if by aircraft. ([#3171](https://github.com/mapbox/mapbox-gl-native/pull/3171), [#3301](https://github.com/mapbox/mapbox-gl-native/pull/3301))
- MGLMapCamera’s `altitude` values now match those of MKMapCamera. ([#3362](https://github.com/mapbox/mapbox-gl-native/pull/3362))
- The user dot’s callout view is now centered above the user dot. It was previously offset slightly to the left. ([#3261](https://github.com/mapbox/mapbox-gl-native/pull/3261))

## iOS 3.0.1
Expand Down
10 changes: 10 additions & 0 deletions include/mbgl/ios/MGLMapView.h
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,16 @@ IB_DESIGNABLE
* @param completion The block to execute after the animation finishes. */
- (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration completionHandler:(nullable void (^)(void))completion;

/** Moves the viewpoint to a different location using a transition animation that evokes powered flight and an optional transition duration and peak altitude.
*
* The transition animation seamlessly incorporates zooming and panning to help the user find his or her bearings even after traversing a great distance.
*
* @param camera The new viewpoint.
* @param duration The amount of time, measured in seconds, that the transition animation should take. Specify `0` to jump to the new viewpoint instantaneously. Specify a negative value to use the default duration, which is based on the length of the flight path.
* @param peakAltitude The altitude, measured in meters, at the midpoint of the animation. The value of this parameter is ignored if it is negative or if the animation transition resulting from a similar call to `-setCamera:animated:` would have a midpoint at a higher altitude.
* @param completion The block to execute after the animation finishes. */
- (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration peakAltitude:(CLLocationDistance)peakAltitude completionHandler:(nullable void (^)(void))completion;

#pragma mark - Converting Map Coordinates

/** @name Converting Map Coordinates */
Expand Down
19 changes: 19 additions & 0 deletions include/mbgl/osx/MGLMapView.h
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,25 @@ IB_DESIGNABLE
@param completion The block to execute after the animation finishes. */
- (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration completionHandler:(nullable void (^)(void))completion;

/** Moves the viewpoint to a different location using a transition animation
that evokes powered flight and an optional transition duration and peak
altitude.

The transition animation seamlessly incorporates zooming and panning to help
the user find his or her bearings even after traversing a great distance.

@param camera The new viewpoint.
@param duration The amount of time, measured in seconds, that the transition
animation should take. Specify `0` to jump to the new viewpoint
instantaneously. Specify a negative value to use the default duration,
which is based on the length of the flight path.
@param peakAltitude The altitude, measured in meters, at the midpoint of the
animation. The value of this parameter is ignored if it is negative or
if the animation transition resulting from a similar call to
`-setCamera:animated:` would have a midpoint at a higher altitude.
@param completion The block to execute after the animation finishes. */
- (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration peakAltitude:(CLLocationDistance)peakAltitude completionHandler:(nullable void (^)(void))completion;

/** The geographic coordinate bounds visible in the receiver’s viewport.

Changing the value of this property updates the receiver immediately. If you
Expand Down
25 changes: 25 additions & 0 deletions platform/darwin/MGLGeometry.mm
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
#import "MGLGeometry_Private.h"

#import <mbgl/util/projection.hpp>

/** Vertical field of view, measured in degrees, for determining the altitude
of the viewpoint.

TransformState::getProjMatrix() assumes a vertical field of view of
2 arctan ⅓ rad ≈ 36.9°, but MapKit uses a vertical field of view of 30°.
flyTo() assumes a field of view of 2 arctan ½ rad. */
const CLLocationDegrees MGLAngularFieldOfView = 30;

const MGLCoordinateSpan MGLCoordinateSpanZero = {0, 0};

CGRect MGLExtendRect(CGRect rect, CGPoint point) {
Expand All @@ -19,3 +29,18 @@ CGRect MGLExtendRect(CGRect rect, CGPoint point) {
}
return rect;
}

CLLocationDistance MGLAltitudeForZoomLevel(double zoomLevel, CGFloat pitch, CLLocationDegrees latitude, CGSize size) {
CLLocationDistance metersPerPixel = mbgl::Projection::getMetersPerPixelAtLatitude(latitude, zoomLevel);
CLLocationDistance metersTall = metersPerPixel * size.height;
CLLocationDistance altitude = metersTall / 2 / std::tan(MGLRadiansFromDegrees(MGLAngularFieldOfView) / 2.);
return altitude * std::sin(M_PI_2 - MGLRadiansFromDegrees(pitch)) / std::sin(M_PI_2);
}

double MGLZoomLevelForAltitude(CLLocationDistance altitude, CGFloat pitch, CLLocationDegrees latitude, CGSize size) {
CLLocationDistance eyeAltitude = altitude / std::sin(M_PI_2 - MGLRadiansFromDegrees(pitch)) * std::sin(M_PI_2);
CLLocationDistance metersTall = eyeAltitude * 2 * std::tan(MGLRadiansFromDegrees(MGLAngularFieldOfView) / 2.);
CLLocationDistance metersPerPixel = metersTall / size.height;
CGFloat mapPixelWidthAtZoom = std::cos(MGLRadiansFromDegrees(latitude)) * mbgl::util::M2PI * mbgl::util::EARTH_RADIUS_M / metersPerPixel;
return ::log2(mapPixelWidthAtZoom / mbgl::util::tileSize);
}
18 changes: 18 additions & 0 deletions platform/darwin/MGLGeometry_Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,21 @@ NS_INLINE mbgl::EdgeInsets MGLEdgeInsetsFromNSEdgeInsets(NSEdgeInsets insets) {
return { insets.top, insets.left, insets.bottom, insets.right };
}
#endif

/** Converts a map zoom level to a camera altitude.

@param zoomLevel The zoom level to convert.
@param pitch The camera pitch, measured in degrees.
@param latitude The latitude of the point at the center of the viewport.
@param size The size of the viewport.
@return An altitude measured in meters. */
CLLocationDistance MGLAltitudeForZoomLevel(double zoomLevel, CGFloat pitch, CLLocationDegrees latitude, CGSize size);

/** Converts a camera altitude to a map zoom level.

@param altitude The altitude to convert, measured in meters.
@param pitch The camera pitch, measured in degrees.
@param latitude The latitude of the point at the center of the viewport.
@param size The size of the viewport.
@return A zero-based zoom level. */
double MGLZoomLevelForAltitude(CLLocationDistance altitude, CGFloat pitch, CLLocationDegrees latitude, CGSize size);
24 changes: 24 additions & 0 deletions platform/darwin/MGLMapCamera.mm
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,28 @@ - (NSString *)description
NSStringFromClass([self class]), self, _centerCoordinate.latitude, _centerCoordinate.longitude, _altitude, _heading, _pitch];
}

- (BOOL)isEqual:(id)other
{
if ( ! [other isKindOfClass:[self class]])
{
return NO;
}
if (other == self)
{
return YES;
}

MGLMapCamera *otherCamera = other;
return (_centerCoordinate.latitude == otherCamera.centerCoordinate.latitude
&& _centerCoordinate.longitude == otherCamera.centerCoordinate.longitude
&& _altitude == otherCamera.altitude
&& _pitch == otherCamera.pitch && _heading == otherCamera.heading);
}

- (NSUInteger)hash
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for adding isEqual:, will look at adding this to the a camera example.

Copy link
Contributor

Choose a reason for hiding this comment

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

(Grr, commented on the wrong line there.)

On this line, I meant to ask: where do you see hash coming in useful?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Any class that implements -isEqual: must also implement -hash, because equal objects must have the same hash value.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oooh, NSObject. 🏫

{
return (@(_centerCoordinate.latitude).hash + @(_centerCoordinate.longitude).hash
+ @(_altitude).hash + @(_pitch).hash + @(_heading).hash);
}

@end
132 changes: 36 additions & 96 deletions platform/ios/src/MGLMapView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,6 @@
const NSTimeInterval MGLAnimationDuration = 0.3;
const CGSize MGLAnnotationUpdateViewportOutset = {150, 150};
const CGFloat MGLMinimumZoom = 3;
const CGFloat MGLMinimumPitch = 0;
const CGFloat MGLMaximumPitch = 60;
const CLLocationDegrees MGLAngularFieldOfView = M_PI / 6.;
const NSUInteger MGLTargetFrameInterval = 1; //Target FPS will be 60 divided by this value

/// Reuse identifier and file name of the default point annotation image.
Expand Down Expand Up @@ -1285,7 +1282,7 @@ - (void)handleTwoFingerDragGesture:(UIPanGestureRecognizer *)twoFingerDrag
CGFloat currentPitch = _mbglMap->getPitch();
CGFloat slowdown = 20.0;

CGFloat pitchNew = mbgl::util::clamp(currentPitch - (gestureDistance / slowdown), MGLMinimumPitch, MGLMaximumPitch);
CGFloat pitchNew = currentPitch - (gestureDistance / slowdown);

_mbglMap->setPitch(pitchNew);

Expand Down Expand Up @@ -1726,48 +1723,11 @@ - (void)setDirection:(CLLocationDirection)direction

- (MGLMapCamera *)camera
{
CGRect frame = self.frame;
CGPoint edgePoint;
// Constrain by the shorter of the two axes.
if (frame.size.width > frame.size.height) // landscape
{
edgePoint = CGPointMake(0, frame.size.height / 2.);
}
else // portrait
{
edgePoint = CGPointMake(frame.size.width / 2., 0);
}
CLLocationCoordinate2D edgeCoordinate = [self convertPoint:edgePoint toCoordinateFromView:self];
mbgl::ProjectedMeters edgeMeters = _mbglMap->projectedMetersForLatLng(MGLLatLngFromLocationCoordinate2D(edgeCoordinate));

// Because we constrain the zoom level vertically in portrait orientation,
// the visible medial span is affected by pitch: the distance from the
// center point to the near edge is less than than distance from the center
// point to the far edge. Average the two distances.
mbgl::ProjectedMeters nearEdgeMeters;
if (frame.size.width > frame.size.height)
{
nearEdgeMeters = edgeMeters;
}
else
{
CGPoint nearEdgePoint = CGPointMake(frame.size.width / 2., frame.size.height);
CLLocationCoordinate2D nearEdgeCoordinate = [self convertPoint:nearEdgePoint toCoordinateFromView:self];
nearEdgeMeters = _mbglMap->projectedMetersForLatLng(MGLLatLngFromLocationCoordinate2D(nearEdgeCoordinate));
}

// The opposite side is the distance between the center and one edge.
CLLocationCoordinate2D centerCoordinate = self.centerCoordinate;
mbgl::ProjectedMeters centerMeters = _mbglMap->projectedMetersForLatLng(MGLLatLngFromLocationCoordinate2D(centerCoordinate));
CLLocationDistance centerToEdge = std::hypot(centerMeters.easting - edgeMeters.easting,
centerMeters.northing - edgeMeters.northing);
CLLocationDistance centerToNearEdge = std::hypot(centerMeters.easting - nearEdgeMeters.easting,
centerMeters.northing - nearEdgeMeters.northing);
CLLocationDistance altitude = (centerToEdge + centerToNearEdge) / 2 / std::tan(MGLAngularFieldOfView / 2.);

CGFloat pitch = _mbglMap->getPitch();

return [MGLMapCamera cameraLookingAtCenterCoordinate:centerCoordinate
CLLocationDistance altitude = MGLAltitudeForZoomLevel(self.zoomLevel, pitch,
self.centerCoordinate.latitude,
self.frame.size);
return [MGLMapCamera cameraLookingAtCenterCoordinate:self.centerCoordinate
fromDistance:altitude
pitch:pitch
heading:self.direction];
Expand All @@ -1791,6 +1751,11 @@ - (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration a
- (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))completion
{
_mbglMap->cancelTransitions();
if ([self.camera isEqual:camera])
{
return;
}

mbgl::CameraOptions options = [self cameraOptionsObjectForAnimatingToCamera:camera];
if (duration > 0)
{
Expand All @@ -1817,13 +1782,30 @@ - (void)flyToCamera:(MGLMapCamera *)camera completionHandler:(nullable void (^)(
}

- (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration completionHandler:(nullable void (^)(void))completion
{
[self flyToCamera:camera withDuration:duration peakAltitude:-1 completionHandler:completion];
}

- (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration peakAltitude:(CLLocationDistance)peakAltitude completionHandler:(nullable void (^)(void))completion
{
_mbglMap->cancelTransitions();
if ([self.camera isEqual:camera])
{
return;
}

mbgl::CameraOptions options = [self cameraOptionsObjectForAnimatingToCamera:camera];
if (duration >= 0)
{
options.duration = MGLDurationInSeconds(duration);
}
if (peakAltitude >= 0)
{
CLLocationDegrees peakLatitude = (self.centerCoordinate.latitude + camera.centerCoordinate.latitude) / 2;
CLLocationDegrees peakPitch = (self.camera.pitch + camera.pitch) / 2;
options.minZoom = MGLZoomLevelForAltitude(peakAltitude, peakPitch,
peakLatitude, self.frame.size);
}
if (completion)
{
options.transitionFinishFn = [completion]() {
Expand All @@ -1840,62 +1822,20 @@ - (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration

/// Returns a CameraOptions object that specifies parameters for animating to
/// the given camera.
- (mbgl::CameraOptions)cameraOptionsObjectForAnimatingToCamera:(MGLMapCamera *)camera {
// The opposite side is the distance between the center and one edge.
mbgl::LatLng centerLatLng = MGLLatLngFromLocationCoordinate2D(camera.centerCoordinate);
mbgl::ProjectedMeters centerMeters = _mbglMap->projectedMetersForLatLng(centerLatLng);
CLLocationDistance centerToEdge = camera.altitude * std::tan(MGLAngularFieldOfView / 2.);

double angle = -1;
- (mbgl::CameraOptions)cameraOptionsObjectForAnimatingToCamera:(MGLMapCamera *)camera
{
mbgl::CameraOptions options;
options.center = MGLLatLngFromLocationCoordinate2D(camera.centerCoordinate);
options.zoom = MGLZoomLevelForAltitude(camera.altitude, camera.pitch,
camera.centerCoordinate.latitude,
self.frame.size);
if (camera.heading >= 0)
{
angle = MGLRadiansFromDegrees(-camera.heading);
options.angle = MGLRadiansFromDegrees(-camera.heading);
}
double pitch = -1;
if (camera.pitch >= 0)
{
pitch = MGLRadiansFromDegrees(mbgl::util::clamp(camera.pitch, MGLMinimumPitch, MGLMaximumPitch));
}

// Make a visible bounds that extends in the constrained direction (the
// shorter of the two axes).
CGRect frame = self.frame;
mbgl::LatLng sw, ne;
if (frame.size.width > frame.size.height) // landscape
{
sw = _mbglMap->latLngForProjectedMeters({
centerMeters.northing - centerToEdge * std::sin(angle),
centerMeters.easting - centerToEdge * std::cos(angle),
});
ne = _mbglMap->latLngForProjectedMeters({
centerMeters.northing + centerToEdge * std::sin(angle),
centerMeters.easting + centerToEdge * std::cos(angle),
});
}
else // portrait
{
sw = _mbglMap->latLngForProjectedMeters({
centerMeters.northing - centerToEdge * std::cos(-angle) + centerToEdge * std::cos(-angle) * std::sin(pitch) / 2,
centerMeters.easting - centerToEdge * std::sin(-angle) + centerToEdge * std::sin(-angle) * std::sin(pitch) / 2,
});
ne = _mbglMap->latLngForProjectedMeters({
centerMeters.northing + centerToEdge * std::cos(-angle) - centerToEdge * std::cos(-angle) * std::sin(pitch) / 2,
centerMeters.easting + centerToEdge * std::sin(-angle) - centerToEdge * std::sin(-angle) * std::sin(pitch) / 2,
});
}

// Fit the viewport to the bounds. Correct the center in case pitch should
// cause the visual center to lie above the screen center.
mbgl::CameraOptions options = _mbglMap->cameraForLatLngs({ sw, ne }, {});
options.center = centerLatLng;

if (camera.heading >= 0)
{
options.angle = angle;
}
if (pitch >= 0)
{
options.pitch = pitch;
options.pitch = MGLRadiansFromDegrees(camera.pitch);
}
return options;
}
Expand Down
Loading