diff options
author | Minh Nguyễn <mxn@1ec5.org> | 2015-12-05 22:22:46 -0800 |
---|---|---|
committer | Minh Nguyễn <mxn@1ec5.org> | 2015-12-13 17:26:53 -0800 |
commit | 0fb2b855a4539803f42a4048257da606f66d9f4e (patch) | |
tree | 7be98f423e47a40aa6d011c5e52751d0896125e0 | |
parent | 3f2fae521cde0cc07796f65b33a227875466dc8d (diff) | |
download | qtlocation-mapboxgl-0fb2b855a4539803f42a4048257da606f66d9f4e.tar.gz |
[osx] Map camera
Shared MGLMapCamera between iOS and OS X. Unfortunately -camera and -setCamera: implementations need to be copy-pasted for now.
-rw-r--r-- | gyp/platform-ios.gypi | 4 | ||||
-rw-r--r-- | gyp/platform-osx.gypi | 1 | ||||
-rw-r--r-- | include/mbgl/darwin/MGLMapCamera.h (renamed from include/mbgl/ios/MGLMapCamera.h) | 0 | ||||
-rw-r--r-- | include/mbgl/osx/MGLMapView.h | 26 | ||||
-rw-r--r-- | platform/darwin/MGLMapCamera.mm (renamed from platform/ios/MGLMapCamera.mm) | 0 | ||||
-rw-r--r-- | platform/osx/sdk/Mapbox.h | 1 | ||||
-rw-r--r-- | platform/osx/sdk/Mapbox.m | 1 | ||||
-rw-r--r-- | platform/osx/src/MGLMapView+IBAdditions.m | 4 | ||||
-rw-r--r-- | platform/osx/src/MGLMapView.mm | 160 | ||||
-rw-r--r-- | platform/osx/src/MGLMapView_Private.h | 6 |
10 files changed, 197 insertions, 6 deletions
diff --git a/gyp/platform-ios.gypi b/gyp/platform-ios.gypi index d7e85a9516..9ae44b2d45 100644 --- a/gyp/platform-ios.gypi +++ b/gyp/platform-ios.gypi @@ -43,11 +43,11 @@ '../platform/darwin/MGLPolyline.mm', '../include/mbgl/darwin/MGLPolygon.h', '../platform/darwin/MGLPolygon.mm', + '../include/mbgl/darwin/MGLMapCamera.h', + '../platform/darwin/MGLMapCamera.mm', '../include/mbgl/ios/Mapbox.h', '../platform/ios/MGLMapboxEvents.h', '../platform/ios/MGLMapboxEvents.m', - '../include/mbgl/ios/MGLMapCamera.h', - '../platform/ios/MGLMapCamera.mm', '../include/mbgl/ios/MGLMapView.h', '../include/mbgl/ios/MGLMapView+IBAdditions.h', '../platform/ios/MGLMapView.mm', diff --git a/gyp/platform-osx.gypi b/gyp/platform-osx.gypi index 281a8a5738..6ad948c27f 100644 --- a/gyp/platform-osx.gypi +++ b/gyp/platform-osx.gypi @@ -33,6 +33,7 @@ '../platform/darwin/MGLPointAnnotation.m', '../platform/darwin/MGLPolyline.mm', '../platform/darwin/MGLPolygon.mm', + '../platform/darwin/MGLMapCamera.mm', '../platform/osx/src/MGLAccountManager_Private.h', '../platform/osx/src/MGLAccountManager.m', '../platform/osx/src/MGLMapView_Private.h', diff --git a/include/mbgl/ios/MGLMapCamera.h b/include/mbgl/darwin/MGLMapCamera.h index 7c00ad8665..7c00ad8665 100644 --- a/include/mbgl/ios/MGLMapCamera.h +++ b/include/mbgl/darwin/MGLMapCamera.h diff --git a/include/mbgl/osx/MGLMapView.h b/include/mbgl/osx/MGLMapView.h index 0a6a43ed12..41f7e02c84 100644 --- a/include/mbgl/osx/MGLMapView.h +++ b/include/mbgl/osx/MGLMapView.h @@ -23,6 +23,7 @@ typedef NS_OPTIONS(NSUInteger, MGLMapDebugMaskOptions) { }; @class MGLAnnotationImage; +@class MGLMapCamera; @protocol MGLAnnotation; @protocol MGLMapViewDelegate; @@ -223,6 +224,31 @@ IB_DESIGNABLE coordinate or zoom level. */ - (void)setDirection:(CLLocationDirection)direction animated:(BOOL)animated; +/** A camera representing the current viewpoint of the map. */ +@property (nonatomic, copy) MGLMapCamera *camera; + +/** Moves the viewpoint to a different location with respect to the map with an + optional transition animation. + + @param camera The new viewpoint. + @param animated Specify `YES` if you want the map view to animate the change + to the new viewpoint or `NO` if you want the map to display the new + viewpoint immediately. */ +- (void)setCamera:(MGLMapCamera *)camera animated:(BOOL)animated; + +/** Moves the viewpoint to a different location with respect to the map with an + optional transition duration and timing function. + + @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. + @param function A timing function used for the animation. Set this parameter + to `nil` for a transition that matches most system animations. If the + duration is `0`, this parameter is ignored. + @param completion The block to execute after the animation finishes. */ +- (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function 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 diff --git a/platform/ios/MGLMapCamera.mm b/platform/darwin/MGLMapCamera.mm index d04e46fa90..d04e46fa90 100644 --- a/platform/ios/MGLMapCamera.mm +++ b/platform/darwin/MGLMapCamera.mm diff --git a/platform/osx/sdk/Mapbox.h b/platform/osx/sdk/Mapbox.h index fdc667b56a..ca09fbd641 100644 --- a/platform/osx/sdk/Mapbox.h +++ b/platform/osx/sdk/Mapbox.h @@ -10,6 +10,7 @@ FOUNDATION_EXPORT const unsigned char MapboxVersionString[]; #import <Mapbox/MGLAnnotation.h> #import <Mapbox/MGLAnnotationImage.h> #import <Mapbox/MGLGeometry.h> +#import <Mapbox/MGLMapCamera.h> #import <Mapbox/MGLMapView.h> #import <Mapbox/MGLMapView+IBAdditions.h> #import <Mapbox/MGLMapViewDelegate.h> diff --git a/platform/osx/sdk/Mapbox.m b/platform/osx/sdk/Mapbox.m index 1ac5a2d81e..a96ec6df2d 100644 --- a/platform/osx/sdk/Mapbox.m +++ b/platform/osx/sdk/Mapbox.m @@ -19,6 +19,7 @@ static void InitializeMapbox() { [MGLAccountManager class]; [MGLAnnotationImage class]; + [MGLMapCamera class]; [MGLMapView class]; [MGLMultiPoint class]; [MGLPointAnnotation class]; diff --git a/platform/osx/src/MGLMapView+IBAdditions.m b/platform/osx/src/MGLMapView+IBAdditions.m index aaa4ddf1bb..2d37d29037 100644 --- a/platform/osx/src/MGLMapView+IBAdditions.m +++ b/platform/osx/src/MGLMapView+IBAdditions.m @@ -26,7 +26,7 @@ void mgl_linkMapViewIBCategory() {} } + (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingLatitude { - return [NSSet setWithObjects:@"centerCoordinate", nil]; + return [NSSet setWithObjects:@"centerCoordinate", @"camera", nil]; } - (double)latitude { @@ -48,7 +48,7 @@ void mgl_linkMapViewIBCategory() {} } + (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingLongitude { - return [NSSet setWithObjects:@"centerCoordinate", nil]; + return [NSSet setWithObjects:@"centerCoordinate", @"camera", nil]; } - (double)longitude { diff --git a/platform/osx/src/MGLMapView.mm b/platform/osx/src/MGLMapView.mm index 83b9bf009b..0399e7fec8 100644 --- a/platform/osx/src/MGLMapView.mm +++ b/platform/osx/src/MGLMapView.mm @@ -8,6 +8,7 @@ #import "../../darwin/MGLGeometry_Private.h" #import "../../darwin/MGLMultiPoint_Private.h" +#import <mbgl/darwin/MGLMapCamera.h> #import <mbgl/darwin/MGLPolygon.h> #import <mbgl/darwin/MGLPolyline.h> #import <mbgl/osx/MGLAnnotationImage.h> @@ -48,6 +49,15 @@ const CGFloat MGLOrnamentOpacity = 0.9; /// Default duration for programmatic animations. const NSTimeInterval MGLAnimationDuration = 0.3; +/// Angular field of view for determining altitude of viewpoint. +const CLLocationDegrees MGLAngularFieldOfView = M_PI / 6.; + +/// Minimum allowed pitch in degrees. +const CGFloat MGLMinimumPitch = 0; + +/// Maximum allowed pitch in degrees. +const CGFloat MGLMaximumPitch = 60; + /// Distance in points that a single press of the panning keyboard shortcut pans the map by. const CGFloat MGLKeyPanningIncrement = 150; @@ -101,6 +111,17 @@ std::chrono::steady_clock::duration MGLDurationInSeconds(NSTimeInterval duration std::chrono::duration<NSTimeInterval, std::chrono::seconds::period>(duration)); } +/// Converts a media timing function into a unit bezier object usable in mbgl. +mbgl::util::UnitBezier MGLUnitBezierForMediaTimingFunction(CAMediaTimingFunction *function) { + if (!function) { + function = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]; + } + float p1[2], p2[2]; + [function getControlPointAtIndex:0 values:p1]; + [function getControlPointAtIndex:1 values:p2]; + return { p1[0], p1[1], p2[0], p2[1] }; +} + /// Converts the given color into an mbgl::Color in calibrated RGB space. mbgl::Color MGLColorObjectFromNSColor(NSColor *color) { if (!color) { @@ -209,7 +230,7 @@ public: } + (NSArray *)restorableStateKeyPaths { - return @[@"latitude", @"longitude", @"zoomLevel", @"direction"]; + return @[@"camera"]; } - (void)commonInit { @@ -773,7 +794,7 @@ public: #pragma mark Viewport + (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCenterCoordinate { - return [NSSet setWithObjects:@"latitude", @"longitude", nil]; + return [NSSet setWithObjects:@"latitude", @"longitude", @"camera", nil]; } - (CLLocationCoordinate2D)centerCoordinate { @@ -815,6 +836,10 @@ public: _pendingLongitude = pendingLongitude; } ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingZoomLevel { + return [NSSet setWithObject:@"camera"]; +} + - (double)zoomLevel { return _mbglMap->getZoom(); } @@ -858,6 +883,10 @@ public: } } ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingDirection { + return [NSSet setWithObject:@"camera"]; +} + - (CLLocationDirection)direction { return mbgl::util::wrap(_mbglMap->getBearing(), 0., 360.); } @@ -879,6 +908,133 @@ public: [self didChangeValueForKey:@"direction"]; } ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCamera { + return [NSSet setWithObjects:@"latitude", @"longitude", @"centerCoordinate", @"zoomLevel", @"direction", nil]; +} + +- (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); + } + mbgl::LatLng edgeLatLng = [self convertPoint:edgePoint toLatLngFromView:self]; + mbgl::ProjectedMeters edgeMeters = _mbglMap->projectedMetersForLatLng(edgeLatLng); + + // 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); + mbgl::LatLng nearEdgeLatLng = [self convertPoint:nearEdgePoint toLatLngFromView:self]; + nearEdgeMeters = _mbglMap->projectedMetersForLatLng(nearEdgeLatLng); + } + + // The opposite side is the distance between the center and one edge. + mbgl::LatLng centerLatLng = MGLLatLngFromLocationCoordinate2D(self.centerCoordinate); + mbgl::ProjectedMeters centerMeters = _mbglMap->projectedMetersForLatLng(centerLatLng); + 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:self.centerCoordinate + fromDistance:altitude + pitch:pitch + heading:self.direction]; +} + +- (void)setCamera:(MGLMapCamera *)camera { + [self setCamera:camera animated:NO]; +} + +- (void)setCamera:(MGLMapCamera *)camera animated:(BOOL)animated { + [self setCamera:camera withDuration:animated ? MGLAnimationDuration : 0 animationTimingFunction:nil completionHandler:NULL]; +} + +- (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))completion { + _mbglMap->cancelTransitions(); + + // 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; + if (camera.heading >= 0) { + 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; + } + if (duration > 0) { + options.duration = MGLDurationInSeconds(duration); + options.easing = MGLUnitBezierForMediaTimingFunction(function); + } + if (completion) { + options.transitionFinishFn = [completion]() { + // Must run asynchronously after the transition is completely over. + // Otherwise, a call to -setCamera: within the completion handler + // would reenter the completion handler’s caller. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + completion(); + }); + }; + } + + [self willChangeValueForKey:@"camera"]; + _mbglMap->easeTo(options); + [self didChangeValueForKey:@"camera"]; +} + + (NSSet *)keyPathsForValuesAffectingVisibleCoordinateBounds { return [NSSet setWithObjects:@"centerCoordinate", @"zoomLevel", @"direction", @"bounds", nil]; } diff --git a/platform/osx/src/MGLMapView_Private.h b/platform/osx/src/MGLMapView_Private.h index d9d42c1b35..77f2aab323 100644 --- a/platform/osx/src/MGLMapView_Private.h +++ b/platform/osx/src/MGLMapView_Private.h @@ -8,6 +8,12 @@ void mgl_linkMapViewIBCategory(); /// actively drawing. @property (nonatomic, readonly, getter=isDormant) BOOL dormant; +// These properties exist because initially, both the latitude and longitude are +// NaN. You have to set both the latitude and longitude simultaneously. If you +// set the latitude but reuse the current longitude, and the current longitude +// happens to be NaN, there will be no change because the resulting coordinate +// pair is invalid. + /// Center latitude set independently of the center longitude in an inspectable. @property (nonatomic) CLLocationDegrees pendingLatitude; /// Center longitude set independently of the center latitude in an inspectable. |