diff options
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | include/mbgl/ios/MGLMapView.h | 10 | ||||
-rw-r--r-- | include/mbgl/osx/MGLMapView.h | 19 | ||||
-rw-r--r-- | include/mbgl/storage/default_file_source.hpp | 21 | ||||
-rw-r--r-- | include/mbgl/util/constants.hpp | 1 | ||||
-rw-r--r-- | platform/darwin/MGLGeometry.mm | 25 | ||||
-rw-r--r-- | platform/darwin/MGLGeometry_Private.h | 18 | ||||
-rw-r--r-- | platform/darwin/MGLMapCamera.mm | 28 | ||||
-rw-r--r-- | platform/default/glfw_view.cpp | 2 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.mm | 132 | ||||
-rw-r--r-- | platform/osx/app/AppDelegate.m | 2 | ||||
-rw-r--r-- | platform/osx/src/MGLMapView.mm | 117 | ||||
-rw-r--r-- | src/mbgl/map/map.cpp | 2 | ||||
-rw-r--r-- | src/mbgl/map/transform.cpp | 8 | ||||
-rw-r--r-- | src/mbgl/storage/default_file_source.cpp | 355 | ||||
-rw-r--r-- | src/mbgl/storage/default_file_source_impl.hpp | 115 | ||||
-rw-r--r-- | src/mbgl/storage/online_file_source.cpp | 455 | ||||
-rw-r--r-- | src/mbgl/storage/online_file_source.hpp | 34 | ||||
-rw-r--r-- | src/mbgl/util/constants.cpp | 1 | ||||
-rw-r--r-- | test/miscellaneous/async_task.cpp | 4 |
20 files changed, 689 insertions, 661 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 66aa092898..3ab11b36ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,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 diff --git a/include/mbgl/ios/MGLMapView.h b/include/mbgl/ios/MGLMapView.h index 32d68aa2cd..e10932bd27 100644 --- a/include/mbgl/ios/MGLMapView.h +++ b/include/mbgl/ios/MGLMapView.h @@ -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 */ diff --git a/include/mbgl/osx/MGLMapView.h b/include/mbgl/osx/MGLMapView.h index 6f46825448..ae701de27a 100644 --- a/include/mbgl/osx/MGLMapView.h +++ b/include/mbgl/osx/MGLMapView.h @@ -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 diff --git a/include/mbgl/storage/default_file_source.hpp b/include/mbgl/storage/default_file_source.hpp index e669cebf72..3689b9e932 100644 --- a/include/mbgl/storage/default_file_source.hpp +++ b/include/mbgl/storage/default_file_source.hpp @@ -1,32 +1,25 @@ -#ifndef MBGL_STORAGE_DEFAULT_DEFAULT_FILE_SOURCE -#define MBGL_STORAGE_DEFAULT_DEFAULT_FILE_SOURCE +#ifndef MBGL_STORAGE_DEFAULT_FILE_SOURCE +#define MBGL_STORAGE_DEFAULT_FILE_SOURCE #include <mbgl/storage/file_source.hpp> -#include <mbgl/storage/file_cache.hpp> namespace mbgl { -namespace util { -template <typename T> class Thread; -} // namespace util +class FileCache; class DefaultFileSource : public FileSource { public: - DefaultFileSource(FileCache *cache, const std::string &root = ""); + DefaultFileSource(FileCache*, const std::string& root = ""); ~DefaultFileSource() override; - void setAccessToken(const std::string& t) { accessToken = t; } - std::string getAccessToken() const { return accessToken; } + void setAccessToken(const std::string&); + std::string getAccessToken() const; std::unique_ptr<FileRequest> request(const Resource&, Callback) override; private: - friend class DefaultFileRequest; - void cancel(const Resource&, FileRequest*); - class Impl; - const std::unique_ptr<util::Thread<Impl>> thread; - std::string accessToken; + const std::unique_ptr<Impl> impl; }; } // namespace mbgl diff --git a/include/mbgl/util/constants.hpp b/include/mbgl/util/constants.hpp index 113a368128..22a460c9b0 100644 --- a/include/mbgl/util/constants.hpp +++ b/include/mbgl/util/constants.hpp @@ -16,6 +16,7 @@ extern const double RAD2DEG; extern const double M2PI; extern const double EARTH_RADIUS_M; extern const double LATITUDE_MAX; +extern const double PITCH_MAX; } // namespace util diff --git a/platform/darwin/MGLGeometry.mm b/platform/darwin/MGLGeometry.mm index b80203d142..70f00afd2f 100644 --- a/platform/darwin/MGLGeometry.mm +++ b/platform/darwin/MGLGeometry.mm @@ -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) { @@ -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); +} diff --git a/platform/darwin/MGLGeometry_Private.h b/platform/darwin/MGLGeometry_Private.h index 49a306701d..bf5bc4e0ff 100644 --- a/platform/darwin/MGLGeometry_Private.h +++ b/platform/darwin/MGLGeometry_Private.h @@ -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); diff --git a/platform/darwin/MGLMapCamera.mm b/platform/darwin/MGLMapCamera.mm index d04e46fa90..e32ef5a10c 100644 --- a/platform/darwin/MGLMapCamera.mm +++ b/platform/darwin/MGLMapCamera.mm @@ -94,8 +94,32 @@ - (NSString *)description { - return [NSString stringWithFormat:@"<MKMapCamera %p centerCoordinate:%f, %f altitude:%.0fm heading:%.0f° pitch:%.0f°>", - self, _centerCoordinate.latitude, _centerCoordinate.longitude, _altitude, _heading, _pitch]; + return [NSString stringWithFormat:@"<%@ %p centerCoordinate:%f, %f altitude:%.0fm heading:%.0f° pitch:%.0f°>", + 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 +{ + return (@(_centerCoordinate.latitude).hash + @(_centerCoordinate.longitude).hash + + @(_altitude).hash + @(_pitch).hash + @(_heading).hash); } @end diff --git a/platform/default/glfw_view.cpp b/platform/default/glfw_view.cpp index 9cb0ce69d9..66a41b0e10 100644 --- a/platform/default/glfw_view.cpp +++ b/platform/default/glfw_view.cpp @@ -434,7 +434,7 @@ void GLFWView::beforeRender() { // can be accessed with no race because the main thread is blocked // when we render. This will be more straightforward when we move // rendering to the main thread. - glViewport(0, 0, width, height); + glViewport(0, 0, fbWidth, fbHeight); } void GLFWView::afterRender() { diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 23c027b2a1..d6980e1656 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -55,9 +55,6 @@ NSString *const MGLMapboxSetupDocumentationURLDisplayString = @"mapbox.com/help/ 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. @@ -1285,7 +1282,7 @@ std::chrono::steady_clock::duration MGLDurationInSeconds(float duration) 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); @@ -1726,48 +1723,11 @@ std::chrono::steady_clock::duration MGLDurationInSeconds(float duration) - (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]; @@ -1791,6 +1751,11 @@ std::chrono::steady_clock::duration MGLDurationInSeconds(float duration) - (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) { @@ -1818,12 +1783,29 @@ std::chrono::steady_clock::duration MGLDurationInSeconds(float duration) - (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]() { @@ -1840,62 +1822,20 @@ std::chrono::steady_clock::duration MGLDurationInSeconds(float 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; } diff --git a/platform/osx/app/AppDelegate.m b/platform/osx/app/AppDelegate.m index bb39613881..3df79cfe0f 100644 --- a/platform/osx/app/AppDelegate.m +++ b/platform/osx/app/AppDelegate.m @@ -342,7 +342,7 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = { • To scroll, swipe with two fingers, drag the cursor, or press the arrow keys.\n\ • To zoom, pinch with two fingers, or hold down Shift while dragging the cursor up and down, or hold down Option while pressing the up and down arrow keys.\n\ • To rotate, move two fingers opposite each other in a circle, or hold down Option while dragging the cursor left and right, or hold down Option while pressing the left and right arrow keys.\n\ -• To tilt, hold down Option while dragging the cursor up and down.\ +• To tilt, hold down Option while dragging the cursor up and down.\n\ • To drop a pin, click and hold.\ "; [alert runModal]; diff --git a/platform/osx/src/MGLMapView.mm b/platform/osx/src/MGLMapView.mm index e4b86f36db..9e95ef6660 100644 --- a/platform/osx/src/MGLMapView.mm +++ b/platform/osx/src/MGLMapView.mm @@ -49,15 +49,6 @@ 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; @@ -935,41 +926,10 @@ public: } - (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(); - + CLLocationDistance altitude = MGLAltitudeForZoomLevel(self.zoomLevel, pitch, + self.centerCoordinate.latitude, + self.frame.size); return [MGLMapCamera cameraLookingAtCenterCoordinate:self.centerCoordinate fromDistance:altitude pitch:pitch @@ -986,6 +946,9 @@ public: - (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) { @@ -1013,12 +976,25 @@ public: } - (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]() { // Must run asynchronously after the transition is completely over. @@ -1038,55 +1014,16 @@ public: /// 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 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; } @@ -1168,7 +1105,7 @@ public: _directionAtBeginningOfGesture = self.direction; _pitchAtBeginningOfGesture = _mbglMap->getPitch(); } else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) { - mbgl::PrecisionPoint center(startPoint.x, startPoint.y); + mbgl::PrecisionPoint center(startPoint.x, self.bounds.size.height - startPoint.y); if (self.rotateEnabled) { CLLocationDirection newDirection = _directionAtBeginningOfGesture - delta.x / 10; [self willChangeValueForKey:@"direction"]; diff --git a/src/mbgl/map/map.cpp b/src/mbgl/map/map.cpp index ff4b2f5c06..8fe73458fc 100644 --- a/src/mbgl/map/map.cpp +++ b/src/mbgl/map/map.cpp @@ -334,7 +334,7 @@ void Map::resetNorth(const Duration& duration) { #pragma mark - Pitch void Map::setPitch(double pitch, const Duration& duration) { - transform->setPitch(util::clamp(pitch, 0., 60.) * M_PI / 180, duration); + transform->setPitch(pitch * M_PI / 180, duration); update(Update::Repaint); } diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index 3da31d64b5..04c0f8bda6 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -256,6 +256,7 @@ void Transform::_easeTo(const CameraOptions& options, double new_scale, double n double angle = _normalizeAngle(new_angle, state.angle); state.angle = _normalizeAngle(state.angle, angle); double pitch = easeOptions.pitch ? *easeOptions.pitch : state.pitch; + pitch = util::clamp(pitch, 0., util::PITCH_MAX); if (!easeOptions.duration) { easeOptions.duration = Duration::zero(); @@ -363,10 +364,14 @@ void Transform::flyTo(const CameraOptions &options) { state.latY(latLng.latitude), }; + zoom = util::clamp(zoom, state.getMinZoom(), state.getMaxZoom()); + // Minimize rotation by taking the shorter path around the circle. double normalizedAngle = _normalizeAngle(angle, state.angle); state.angle = _normalizeAngle(state.angle, normalizedAngle); + pitch = util::clamp(pitch, 0., util::PITCH_MAX); + const double startZoom = state.scaleZoom(state.scale); const double startAngle = state.angle; const double startPitch = state.pitch; @@ -392,6 +397,7 @@ void Transform::flyTo(const CameraOptions &options) { double rho = 1.42; if (flyOptions.minZoom) { double minZoom = util::min(*flyOptions.minZoom, startZoom, zoom); + minZoom = util::clamp(minZoom, state.getMinZoom(), state.getMaxZoom()); /// w<sub>m</sub>: Maximum visible span, measured in pixels with respect /// to the initial scale. double wMax = w0 / state.zoomScale(minZoom - startZoom); @@ -492,7 +498,7 @@ void Transform::flyTo(const CameraOptions &options) { state.angle = util::wrap(util::interpolate(startAngle, normalizedAngle, k), -M_PI, M_PI); } if (pitch != startPitch) { - state.pitch = util::clamp(util::interpolate(startPitch, pitch, k), 0., 60.); + state.pitch = util::interpolate(startPitch, pitch, k); } // At k = 1.0, a DidChangeAnimated notification should be sent from finish(). diff --git a/src/mbgl/storage/default_file_source.cpp b/src/mbgl/storage/default_file_source.cpp index 367c083360..b2ab5abd6c 100644 --- a/src/mbgl/storage/default_file_source.cpp +++ b/src/mbgl/storage/default_file_source.cpp @@ -1,354 +1,33 @@ -#include <mbgl/storage/default_file_source_impl.hpp> -#include <mbgl/storage/asset_context_base.hpp> -#include <mbgl/storage/http_context_base.hpp> -#include <mbgl/storage/network_status.hpp> - -#include <mbgl/storage/response.hpp> -#include <mbgl/platform/platform.hpp> -#include <mbgl/platform/log.hpp> - -#include <mbgl/util/thread.hpp> -#include <mbgl/util/mapbox.hpp> -#include <mbgl/util/exception.hpp> -#include <mbgl/util/chrono.hpp> - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wshadow" -#pragma GCC diagnostic ignored "-Wunknown-pragmas" -#pragma GCC diagnostic ignored "-Wunused-local-typedefs" -#include <boost/algorithm/string.hpp> -#pragma GCC diagnostic pop - -#include <algorithm> -#include <cassert> - -namespace algo = boost::algorithm; +#include <mbgl/storage/default_file_source.hpp> +#include <mbgl/storage/online_file_source.hpp> namespace mbgl { -DefaultFileSource::DefaultFileSource(FileCache* cache, const std::string& root) - : thread(std::make_unique<util::Thread<Impl>>( - util::ThreadContext{ "FileSource", util::ThreadType::Unknown, util::ThreadPriority::Low }, - cache, - root)) { -} - -DefaultFileSource::~DefaultFileSource() = default; - -std::unique_ptr<FileRequest> DefaultFileSource::request(const Resource& resource, Callback callback) { - if (!callback) { - throw util::MisuseException("FileSource callback can't be empty"); - } - - std::string url; - - switch (resource.kind) { - case Resource::Kind::Style: - url = mbgl::util::mapbox::normalizeStyleURL(resource.url, accessToken); - break; - - case Resource::Kind::Source: - url = util::mapbox::normalizeSourceURL(resource.url, accessToken); - break; - - case Resource::Kind::Glyphs: - url = util::mapbox::normalizeGlyphsURL(resource.url, accessToken); - break; - - case Resource::Kind::SpriteImage: - case Resource::Kind::SpriteJSON: - url = util::mapbox::normalizeSpriteURL(resource.url, accessToken); - break; - - default: - url = resource.url; - } - - Resource res { resource.kind, url }; - auto req = std::make_unique<DefaultFileRequest>(res, *this); - req->workRequest = thread->invokeWithCallback(&Impl::add, callback, res, req.get()); - return std::move(req); -} - -void DefaultFileSource::cancel(const Resource& res, FileRequest* req) { - thread->invoke(&Impl::cancel, res, req); -} - -// ----- Impl ----- - -DefaultFileSource::Impl::Impl(FileCache* cache_, const std::string& root) - : cache(cache_), - assetRoot(root.empty() ? platform::assetRoot() : root), - assetContext(AssetContextBase::createContext()), - httpContext(HTTPContextBase::createContext()), - reachability(std::bind(&Impl::networkIsReachableAgain, this)) { - // Subscribe to network status changes, but make sure that this async handle doesn't keep the - // loop alive; otherwise our app wouldn't terminate. After all, we only need status change - // notifications when our app is still running. - NetworkStatus::Subscribe(&reachability); - reachability.unref(); -} - -DefaultFileSource::Impl::~Impl() { - NetworkStatus::Unsubscribe(&reachability); -} - -void DefaultFileSource::Impl::networkIsReachableAgain() { - for (auto& req : pending) { - auto& request = *req.second; - auto& response = request.getResponse(); - if (!request.realRequest && response && response->error && response->error->reason == Response::Error::Reason::Connection) { - // We need all requests to fail at least once before we are going to start retrying - // them, and we only immediately restart request that failed due to connection issues. - startRealRequest(request); - } - } -} - -void DefaultFileSource::Impl::add(Resource resource, FileRequest* req, Callback callback) { - auto& request = *pending.emplace(resource, - std::make_unique<DefaultFileRequestImpl>(resource)).first->second; - - // Trigger a potentially required refresh of this Request - update(request); - - // Add this request as an observer so that it'll get notified when something about this - // request changes. - request.addObserver(req, callback); -} - -void DefaultFileSource::Impl::update(DefaultFileRequestImpl& request) { - if (request.getResponse()) { - // We've at least obtained a cache value, potentially we also got a final response. - // The observers have been notified already; send what we have to the new one as well. - - // Before returning the existing response, make sure that it is still fresh, or update the - // `stale` flag. - request.checkResponseFreshness(); - - if (request.getResponse()->stale && !request.realRequest) { - // We've returned a stale response; now make sure the requester also gets a fresh - // response eventually. It's possible that there's already a request in progress. - // Note that this will also trigger updates to all other existing listeners. - // Since we already have data, we're going to verify - startRealRequest(request); - } else { - // The response is still fresh (or there's already a request for refreshing the resource - // in progress), so there's nothing we need to do. - } - } else if (!request.cacheRequest && !request.realRequest) { - // There is no request in progress, and we don't have a response yet. This means we'll have - // to start the request ourselves. - if (cache) { - startCacheRequest(request); - } else { - startRealRequest(request); - } - } else { - // There is a request in progress. We just have to wait. - } -} - -void DefaultFileSource::Impl::startCacheRequest(DefaultFileRequestImpl& request) { - // Check the cache for existing data so that we can potentially - // revalidate the information without having to redownload everything. - request.cacheRequest = - cache->get(request.resource, [this, &request](std::shared_ptr<Response> response) { - request.cacheRequest = nullptr; - if (response) { - response->stale = response->isExpired(); - request.setResponse(response); - } - - if (!response || response->stale) { - // No response or stale cache. Run the real request. - startRealRequest(request); - } - - // Notify in all cases; requestors can decide whether they want to use stale responses. - request.notify(); - - reschedule(request); - }); -} - -void DefaultFileSource::Impl::startRealRequest(DefaultFileRequestImpl& request) { - assert(!request.realRequest); - - // Cancel the timer if we have one. - if (request.timerRequest) { - request.timerRequest->stop(); - } - - auto callback = [this, &request](std::shared_ptr<const Response> response) { - request.realRequest = nullptr; - - if (cache) { - // Store response in database. Make sure we only refresh the expires column if the data - // didn't change. - FileCache::Hint hint = FileCache::Hint::Full; - if (request.getResponse() && response->data == request.getResponse()->data) { - hint = FileCache::Hint::Refresh; - } - cache->put(request.resource, response, hint); - } - - request.setResponse(response); - request.notify(); - reschedule(request); - }; - - if (algo::starts_with(request.resource.url, "asset://")) { - request.realRequest = - assetContext->createRequest(request.resource, callback, assetRoot); - } else { - request.realRequest = - httpContext->createRequest(request.resource, callback, request.getResponse()); - } -} - -void DefaultFileSource::Impl::cancel(Resource resource, FileRequest* req) { - auto it = pending.find(resource); - if (it != pending.end()) { - // If the number of dependent requests of the DefaultFileRequest drops to zero, - // cancel the request and remove it from the pending list. - auto& request = *it->second; - request.removeObserver(req); - if (!request.hasObservers()) { - pending.erase(it); - } - } else { - // There is no request for this URL anymore. Likely, the request already completed - // before we got around to process the cancelation request. +class DefaultFileSource::Impl { +public: + Impl(FileCache* cache, const std::string& root) + : onlineFileSource(cache, root) { } -} -void DefaultFileSource::Impl::reschedule(DefaultFileRequestImpl& request) { - if (request.realRequest) { - // There's already a request in progress; don't start another one. - return; - } + OnlineFileSource onlineFileSource; +}; - const Seconds timeout = request.getRetryTimeout(); - - if (timeout == Seconds::zero()) { - update(request); - } else if (timeout > Seconds::zero()) { - if (!request.timerRequest) { - request.timerRequest = std::make_unique<util::Timer>(); - } - - request.timerRequest->start(timeout, Duration::zero(), [this, &request] { - assert(!request.realRequest); - startRealRequest(request); - }); - } -} - -// ----- DefaultFileRequest ----- - -DefaultFileRequestImpl::~DefaultFileRequestImpl() { - if (realRequest) { - realRequest->cancel(); - realRequest = nullptr; - } - // timerRequest and cacheRequest are automatically canceld upon destruction. -} - -void DefaultFileRequestImpl::addObserver(FileRequest* req, Callback callback) { - observers.emplace(req, callback); - - if (response) { - // We've got a response, so send the (potentially stale) response to the requester. - callback(*response); - } -} - -void DefaultFileRequestImpl::removeObserver(FileRequest* req) { - observers.erase(req); -} - -bool DefaultFileRequestImpl::hasObservers() const { - return !observers.empty(); -} - -void DefaultFileRequestImpl::notify() { - if (response) { - for (auto& req : observers) { - req.second(*response); - } - } +DefaultFileSource::DefaultFileSource(FileCache* cache, const std::string& root) + : impl(std::make_unique<DefaultFileSource::Impl>(cache, root)) { } -void DefaultFileRequestImpl::setResponse(const std::shared_ptr<const Response>& response_) { - response = response_; - - if (response->error) { - failedRequests++; - } else { - // Reset the number of subsequent failed requests after we got a successful one. - failedRequests = 0; - } -} +DefaultFileSource::~DefaultFileSource() = default; -const std::shared_ptr<const Response>& DefaultFileRequestImpl::getResponse() const { - return response; +void DefaultFileSource::setAccessToken(const std::string& accessToken) { + impl->onlineFileSource.setAccessToken(accessToken); } -Seconds DefaultFileRequestImpl::getRetryTimeout() const { - Seconds timeout = Seconds::zero(); - - if (!response) { - // If we don't have a response, we should retry immediately. - return timeout; - } - - // A value < 0 means that we should not retry. - timeout = Seconds(-1); - - if (response->error) { - assert(failedRequests > 0); - switch (response->error->reason) { - case Response::Error::Reason::Server: { - // Retry immediately, unless we have a certain number of attempts already - const int graceRetries = 3; - if (failedRequests <= graceRetries) { - timeout = Seconds(1); - } else { - timeout = Seconds(1 << std::min(failedRequests - graceRetries, 31)); - } - } break; - case Response::Error::Reason::Connection: { - // Exponential backoff - timeout = Seconds(1 << std::min(failedRequests - 1, 31)); - } break; - default: - // Do not retry due to error. - break; - } - } - - // Check to see if this response expires earlier than a potential error retry. - if (response->expires > Seconds::zero()) { - const Seconds secsToExpire = response->expires - toSeconds(SystemClock::now()); - // Only update the timeout if we don't have one yet, and only if the new timeout is shorter - // than the previous one. - timeout = timeout < Seconds::zero() ? secsToExpire: std::min(timeout, std::max(Seconds::zero(), secsToExpire)); - } - - return timeout; +std::string DefaultFileSource::getAccessToken() const { + return impl->onlineFileSource.getAccessToken(); } -void DefaultFileRequestImpl::checkResponseFreshness() { - if (response && !response->stale && response->isExpired()) { - // Create a new Response object with `stale = true`, but the same data, and - // replace the current request object we have. - // We're not immediately swapping the member variable because it's declared as `const`, and - // we first have to update the `stale` flag. - auto staleResponse = std::make_shared<Response>(*response); - staleResponse->stale = true; - response = staleResponse; - } +std::unique_ptr<FileRequest> DefaultFileSource::request(const Resource& resource, Callback callback) { + return impl->onlineFileSource.request(resource, callback); } } // namespace mbgl diff --git a/src/mbgl/storage/default_file_source_impl.hpp b/src/mbgl/storage/default_file_source_impl.hpp deleted file mode 100644 index 9b5f97332a..0000000000 --- a/src/mbgl/storage/default_file_source_impl.hpp +++ /dev/null @@ -1,115 +0,0 @@ -#ifndef MBGL_STORAGE_DEFAULT_DEFAULT_FILE_SOURCE_IMPL -#define MBGL_STORAGE_DEFAULT_DEFAULT_FILE_SOURCE_IMPL - -#include <mbgl/storage/default_file_source.hpp> -#include <mbgl/storage/asset_context_base.hpp> -#include <mbgl/storage/http_context_base.hpp> -#include <mbgl/util/async_task.hpp> -#include <mbgl/util/noncopyable.hpp> -#include <mbgl/util/chrono.hpp> -#include <mbgl/util/timer.hpp> - -#include <set> -#include <unordered_map> - -namespace mbgl { - -class RequestBase; - -class DefaultFileRequest : public FileRequest { -public: - DefaultFileRequest(const Resource& resource_, - DefaultFileSource& fileSource_) - : resource(resource_), - fileSource(fileSource_) { - } - - ~DefaultFileRequest() { - fileSource.cancel(resource, this); - } - - Resource resource; - DefaultFileSource& fileSource; - - std::unique_ptr<WorkRequest> workRequest; -}; - -class DefaultFileRequestImpl : public util::noncopyable { -public: - using Callback = std::function<void (Response)>; - - const Resource resource; - std::unique_ptr<WorkRequest> cacheRequest; - RequestBase* realRequest = nullptr; - std::unique_ptr<util::Timer> timerRequest; - - inline DefaultFileRequestImpl(const Resource& resource_) - : resource(resource_) {} - - ~DefaultFileRequestImpl(); - - // Observer accessors. - void addObserver(FileRequest*, Callback); - void removeObserver(FileRequest*); - bool hasObservers() const; - - // Updates/gets the response of this request object. - void setResponse(const std::shared_ptr<const Response>&); - const std::shared_ptr<const Response>& getResponse() const; - - // Returns the seconds we have to wait until we need to redo this request. A value of 0 - // means that we need to redo it immediately, and a negative value means that we're not setting - // a timeout at all. - Seconds getRetryTimeout() const; - - // Checks the currently stored response and replaces it with an idential one, except with the - // stale flag set, if the response is expired. - void checkResponseFreshness(); - - // Notifies all observers. - void notify(); - - -private: - // Stores a set of all observing Request objects. - std::unordered_map<FileRequest*, Callback> observers; - - // The current response data. We're storing it because we can satisfy requests for the same - // resource directly by returning this response object. We also need it to create conditional - // HTTP requests, and to check whether new responses we got changed any data. - std::shared_ptr<const Response> response; - - // Counts the number of subsequent failed requests. We're using this value for exponential - // backoff when retrying requests. - int failedRequests = 0; -}; - -class DefaultFileSource::Impl { -public: - using Callback = std::function<void (Response)>; - - Impl(FileCache*, const std::string& = ""); - ~Impl(); - - void networkIsReachableAgain(); - - void add(Resource, FileRequest*, Callback); - void cancel(Resource, FileRequest*); - -private: - void update(DefaultFileRequestImpl&); - void startCacheRequest(DefaultFileRequestImpl&); - void startRealRequest(DefaultFileRequestImpl&); - void reschedule(DefaultFileRequestImpl&); - - std::unordered_map<Resource, std::unique_ptr<DefaultFileRequestImpl>, Resource::Hash> pending; - FileCache* const cache; - const std::string assetRoot; - const std::unique_ptr<AssetContextBase> assetContext; - const std::unique_ptr<HTTPContextBase> httpContext; - util::AsyncTask reachability; -}; - -} // namespace mbgl - -#endif diff --git a/src/mbgl/storage/online_file_source.cpp b/src/mbgl/storage/online_file_source.cpp new file mode 100644 index 0000000000..1f3ccbcbda --- /dev/null +++ b/src/mbgl/storage/online_file_source.cpp @@ -0,0 +1,455 @@ +#include <mbgl/storage/online_file_source.hpp> +#include <mbgl/storage/asset_context_base.hpp> +#include <mbgl/storage/http_context_base.hpp> +#include <mbgl/storage/network_status.hpp> + +#include <mbgl/storage/response.hpp> +#include <mbgl/platform/platform.hpp> +#include <mbgl/platform/log.hpp> + +#include <mbgl/util/thread.hpp> +#include <mbgl/util/mapbox.hpp> +#include <mbgl/util/exception.hpp> +#include <mbgl/util/chrono.hpp> +#include <mbgl/util/async_task.hpp> +#include <mbgl/util/noncopyable.hpp> +#include <mbgl/util/timer.hpp> + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wshadow" +#pragma GCC diagnostic ignored "-Wunknown-pragmas" +#pragma GCC diagnostic ignored "-Wunused-local-typedefs" +#include <boost/algorithm/string.hpp> +#pragma GCC diagnostic pop + +#include <algorithm> +#include <cassert> +#include <set> +#include <unordered_map> + +namespace algo = boost::algorithm; + +namespace mbgl { + +class RequestBase; + +class OnlineFileRequest : public FileRequest { +public: + OnlineFileRequest(const Resource& resource_, + OnlineFileSource& fileSource_) + : resource(resource_), + fileSource(fileSource_) { + } + + ~OnlineFileRequest() { + fileSource.cancel(resource, this); + } + + Resource resource; + OnlineFileSource& fileSource; + + std::unique_ptr<WorkRequest> workRequest; +}; + +class OnlineFileRequestImpl : public util::noncopyable { +public: + using Callback = std::function<void (Response)>; + + const Resource resource; + std::unique_ptr<WorkRequest> cacheRequest; + RequestBase* realRequest = nullptr; + std::unique_ptr<util::Timer> timerRequest; + + inline OnlineFileRequestImpl(const Resource& resource_) + : resource(resource_) {} + + ~OnlineFileRequestImpl(); + + // Observer accessors. + void addObserver(FileRequest*, Callback); + void removeObserver(FileRequest*); + bool hasObservers() const; + + // Updates/gets the response of this request object. + void setResponse(const std::shared_ptr<const Response>&); + const std::shared_ptr<const Response>& getResponse() const; + + // Returns the seconds we have to wait until we need to redo this request. A value of 0 + // means that we need to redo it immediately, and a negative value means that we're not setting + // a timeout at all. + Seconds getRetryTimeout() const; + + // Checks the currently stored response and replaces it with an idential one, except with the + // stale flag set, if the response is expired. + void checkResponseFreshness(); + + // Notifies all observers. + void notify(); + + +private: + // Stores a set of all observing Request objects. + std::unordered_map<FileRequest*, Callback> observers; + + // The current response data. We're storing it because we can satisfy requests for the same + // resource directly by returning this response object. We also need it to create conditional + // HTTP requests, and to check whether new responses we got changed any data. + std::shared_ptr<const Response> response; + + // Counts the number of subsequent failed requests. We're using this value for exponential + // backoff when retrying requests. + int failedRequests = 0; +}; + +class OnlineFileSource::Impl { +public: + using Callback = std::function<void (Response)>; + + Impl(FileCache*, const std::string& = ""); + ~Impl(); + + void networkIsReachableAgain(); + + void add(Resource, FileRequest*, Callback); + void cancel(Resource, FileRequest*); + +private: + void update(OnlineFileRequestImpl&); + void startCacheRequest(OnlineFileRequestImpl&); + void startRealRequest(OnlineFileRequestImpl&); + void reschedule(OnlineFileRequestImpl&); + + std::unordered_map<Resource, std::unique_ptr<OnlineFileRequestImpl>, Resource::Hash> pending; + FileCache* const cache; + const std::string assetRoot; + const std::unique_ptr<AssetContextBase> assetContext; + const std::unique_ptr<HTTPContextBase> httpContext; + util::AsyncTask reachability; +}; + +OnlineFileSource::OnlineFileSource(FileCache* cache, const std::string& root) + : thread(std::make_unique<util::Thread<Impl>>( + util::ThreadContext{ "OnlineFileSource", util::ThreadType::Unknown, util::ThreadPriority::Low }, + cache, + root)) { +} + +OnlineFileSource::~OnlineFileSource() = default; + +std::unique_ptr<FileRequest> OnlineFileSource::request(const Resource& resource, Callback callback) { + if (!callback) { + throw util::MisuseException("FileSource callback can't be empty"); + } + + std::string url; + + switch (resource.kind) { + case Resource::Kind::Style: + url = mbgl::util::mapbox::normalizeStyleURL(resource.url, accessToken); + break; + + case Resource::Kind::Source: + url = util::mapbox::normalizeSourceURL(resource.url, accessToken); + break; + + case Resource::Kind::Glyphs: + url = util::mapbox::normalizeGlyphsURL(resource.url, accessToken); + break; + + case Resource::Kind::SpriteImage: + case Resource::Kind::SpriteJSON: + url = util::mapbox::normalizeSpriteURL(resource.url, accessToken); + break; + + default: + url = resource.url; + } + + Resource res { resource.kind, url }; + auto req = std::make_unique<OnlineFileRequest>(res, *this); + req->workRequest = thread->invokeWithCallback(&Impl::add, callback, res, req.get()); + return std::move(req); +} + +void OnlineFileSource::cancel(const Resource& res, FileRequest* req) { + thread->invoke(&Impl::cancel, res, req); +} + +// ----- Impl ----- + +OnlineFileSource::Impl::Impl(FileCache* cache_, const std::string& root) + : cache(cache_), + assetRoot(root.empty() ? platform::assetRoot() : root), + assetContext(AssetContextBase::createContext()), + httpContext(HTTPContextBase::createContext()), + reachability(std::bind(&Impl::networkIsReachableAgain, this)) { + // Subscribe to network status changes, but make sure that this async handle doesn't keep the + // loop alive; otherwise our app wouldn't terminate. After all, we only need status change + // notifications when our app is still running. + NetworkStatus::Subscribe(&reachability); + reachability.unref(); +} + +OnlineFileSource::Impl::~Impl() { + NetworkStatus::Unsubscribe(&reachability); +} + +void OnlineFileSource::Impl::networkIsReachableAgain() { + for (auto& req : pending) { + auto& request = *req.second; + auto& response = request.getResponse(); + if (!request.realRequest && response && response->error && response->error->reason == Response::Error::Reason::Connection) { + // We need all requests to fail at least once before we are going to start retrying + // them, and we only immediately restart request that failed due to connection issues. + startRealRequest(request); + } + } +} + +void OnlineFileSource::Impl::add(Resource resource, FileRequest* req, Callback callback) { + auto& request = *pending.emplace(resource, + std::make_unique<OnlineFileRequestImpl>(resource)).first->second; + + // Trigger a potentially required refresh of this Request + update(request); + + // Add this request as an observer so that it'll get notified when something about this + // request changes. + request.addObserver(req, callback); +} + +void OnlineFileSource::Impl::update(OnlineFileRequestImpl& request) { + if (request.getResponse()) { + // We've at least obtained a cache value, potentially we also got a final response. + // The observers have been notified already; send what we have to the new one as well. + + // Before returning the existing response, make sure that it is still fresh, or update the + // `stale` flag. + request.checkResponseFreshness(); + + if (request.getResponse()->stale && !request.realRequest) { + // We've returned a stale response; now make sure the requester also gets a fresh + // response eventually. It's possible that there's already a request in progress. + // Note that this will also trigger updates to all other existing listeners. + // Since we already have data, we're going to verify + startRealRequest(request); + } else { + // The response is still fresh (or there's already a request for refreshing the resource + // in progress), so there's nothing we need to do. + } + } else if (!request.cacheRequest && !request.realRequest) { + // There is no request in progress, and we don't have a response yet. This means we'll have + // to start the request ourselves. + if (cache) { + startCacheRequest(request); + } else { + startRealRequest(request); + } + } else { + // There is a request in progress. We just have to wait. + } +} + +void OnlineFileSource::Impl::startCacheRequest(OnlineFileRequestImpl& request) { + // Check the cache for existing data so that we can potentially + // revalidate the information without having to redownload everything. + request.cacheRequest = + cache->get(request.resource, [this, &request](std::shared_ptr<Response> response) { + request.cacheRequest = nullptr; + if (response) { + response->stale = response->isExpired(); + request.setResponse(response); + } + + if (!response || response->stale) { + // No response or stale cache. Run the real request. + startRealRequest(request); + } + + // Notify in all cases; requestors can decide whether they want to use stale responses. + request.notify(); + + reschedule(request); + }); +} + +void OnlineFileSource::Impl::startRealRequest(OnlineFileRequestImpl& request) { + assert(!request.realRequest); + + // Cancel the timer if we have one. + if (request.timerRequest) { + request.timerRequest->stop(); + } + + auto callback = [this, &request](std::shared_ptr<const Response> response) { + request.realRequest = nullptr; + + if (cache) { + // Store response in database. Make sure we only refresh the expires column if the data + // didn't change. + FileCache::Hint hint = FileCache::Hint::Full; + if (request.getResponse() && response->data == request.getResponse()->data) { + hint = FileCache::Hint::Refresh; + } + cache->put(request.resource, response, hint); + } + + request.setResponse(response); + request.notify(); + reschedule(request); + }; + + if (algo::starts_with(request.resource.url, "asset://")) { + request.realRequest = + assetContext->createRequest(request.resource, callback, assetRoot); + } else { + request.realRequest = + httpContext->createRequest(request.resource, callback, request.getResponse()); + } +} + +void OnlineFileSource::Impl::cancel(Resource resource, FileRequest* req) { + auto it = pending.find(resource); + if (it != pending.end()) { + // If the number of dependent requests of the OnlineFileRequest drops to zero, + // cancel the request and remove it from the pending list. + auto& request = *it->second; + request.removeObserver(req); + if (!request.hasObservers()) { + pending.erase(it); + } + } else { + // There is no request for this URL anymore. Likely, the request already completed + // before we got around to process the cancelation request. + } +} + +void OnlineFileSource::Impl::reschedule(OnlineFileRequestImpl& request) { + if (request.realRequest) { + // There's already a request in progress; don't start another one. + return; + } + + const Seconds timeout = request.getRetryTimeout(); + + if (timeout == Seconds::zero()) { + update(request); + } else if (timeout > Seconds::zero()) { + if (!request.timerRequest) { + request.timerRequest = std::make_unique<util::Timer>(); + } + + request.timerRequest->start(timeout, Duration::zero(), [this, &request] { + assert(!request.realRequest); + startRealRequest(request); + }); + } +} + +// ----- OnlineFileRequest ----- + +OnlineFileRequestImpl::~OnlineFileRequestImpl() { + if (realRequest) { + realRequest->cancel(); + realRequest = nullptr; + } + // timerRequest and cacheRequest are automatically canceld upon destruction. +} + +void OnlineFileRequestImpl::addObserver(FileRequest* req, Callback callback) { + observers.emplace(req, callback); + + if (response) { + // We've got a response, so send the (potentially stale) response to the requester. + callback(*response); + } +} + +void OnlineFileRequestImpl::removeObserver(FileRequest* req) { + observers.erase(req); +} + +bool OnlineFileRequestImpl::hasObservers() const { + return !observers.empty(); +} + +void OnlineFileRequestImpl::notify() { + if (response) { + for (auto& req : observers) { + req.second(*response); + } + } +} + +void OnlineFileRequestImpl::setResponse(const std::shared_ptr<const Response>& response_) { + response = response_; + + if (response->error) { + failedRequests++; + } else { + // Reset the number of subsequent failed requests after we got a successful one. + failedRequests = 0; + } +} + +const std::shared_ptr<const Response>& OnlineFileRequestImpl::getResponse() const { + return response; +} + +Seconds OnlineFileRequestImpl::getRetryTimeout() const { + Seconds timeout = Seconds::zero(); + + if (!response) { + // If we don't have a response, we should retry immediately. + return timeout; + } + + // A value < 0 means that we should not retry. + timeout = Seconds(-1); + + if (response->error) { + assert(failedRequests > 0); + switch (response->error->reason) { + case Response::Error::Reason::Server: { + // Retry immediately, unless we have a certain number of attempts already + const int graceRetries = 3; + if (failedRequests <= graceRetries) { + timeout = Seconds(1); + } else { + timeout = Seconds(1 << std::min(failedRequests - graceRetries, 31)); + } + } break; + case Response::Error::Reason::Connection: { + // Exponential backoff + timeout = Seconds(1 << std::min(failedRequests - 1, 31)); + } break; + default: + // Do not retry due to error. + break; + } + } + + // Check to see if this response expires earlier than a potential error retry. + if (response->expires > Seconds::zero()) { + const Seconds secsToExpire = response->expires - toSeconds(SystemClock::now()); + // Only update the timeout if we don't have one yet, and only if the new timeout is shorter + // than the previous one. + timeout = timeout < Seconds::zero() ? secsToExpire: std::min(timeout, std::max(Seconds::zero(), secsToExpire)); + } + + return timeout; +} + +void OnlineFileRequestImpl::checkResponseFreshness() { + if (response && !response->stale && response->isExpired()) { + // Create a new Response object with `stale = true`, but the same data, and + // replace the current request object we have. + // We're not immediately swapping the member variable because it's declared as `const`, and + // we first have to update the `stale` flag. + auto staleResponse = std::make_shared<Response>(*response); + staleResponse->stale = true; + response = staleResponse; + } +} + +} // namespace mbgl diff --git a/src/mbgl/storage/online_file_source.hpp b/src/mbgl/storage/online_file_source.hpp new file mode 100644 index 0000000000..357b1773d7 --- /dev/null +++ b/src/mbgl/storage/online_file_source.hpp @@ -0,0 +1,34 @@ +#ifndef MBGL_STORAGE_ONLINE_FILE_SOURCE +#define MBGL_STORAGE_ONLINE_FILE_SOURCE + +#include <mbgl/storage/file_source.hpp> +#include <mbgl/storage/file_cache.hpp> + +namespace mbgl { + +namespace util { +template <typename T> class Thread; +} // namespace util + +class OnlineFileSource : public FileSource { +public: + OnlineFileSource(FileCache *cache, const std::string &root = ""); + ~OnlineFileSource() override; + + void setAccessToken(const std::string& t) { accessToken = t; } + std::string getAccessToken() const { return accessToken; } + + std::unique_ptr<FileRequest> request(const Resource&, Callback) override; + +private: + friend class OnlineFileRequest; + void cancel(const Resource&, FileRequest*); + + class Impl; + const std::unique_ptr<util::Thread<Impl>> thread; + std::string accessToken; +}; + +} // namespace mbgl + +#endif diff --git a/src/mbgl/util/constants.cpp b/src/mbgl/util/constants.cpp index ccdbeba23a..9a5282d5e0 100644 --- a/src/mbgl/util/constants.cpp +++ b/src/mbgl/util/constants.cpp @@ -7,6 +7,7 @@ const double mbgl::util::RAD2DEG = 180.0 / M_PI; const double mbgl::util::M2PI = 2 * M_PI; const double mbgl::util::EARTH_RADIUS_M = 6378137; const double mbgl::util::LATITUDE_MAX = 85.05112878; +const double mbgl::util::PITCH_MAX = M_PI / 3; #if defined(DEBUG) const bool mbgl::debug::tileParseWarnings = false; diff --git a/test/miscellaneous/async_task.cpp b/test/miscellaneous/async_task.cpp index c1a2bd82e0..c32c2396fb 100644 --- a/test/miscellaneous/async_task.cpp +++ b/test/miscellaneous/async_task.cpp @@ -75,7 +75,7 @@ TEST(AsyncTask, RequestCoalescingMultithreaded) { std::vector<std::unique_ptr<Thread<TestWorker>>> threads; ThreadContext context = {"Test", ThreadType::Map, ThreadPriority::Regular}; - unsigned numThreads = 50; + unsigned numThreads = 25; for (unsigned i = 0; i < numThreads; ++i) { std::unique_ptr<Thread<TestWorker>> thread = std::make_unique<Thread<TestWorker>>(context, &async); @@ -99,7 +99,7 @@ TEST(AsyncTask, ThreadSafety) { AsyncTask async([&count] { ++count; }); async.unref(); - unsigned numThreads = 50; + unsigned numThreads = 25; auto callback = [&] { if (!--numThreads) { |