From 20d2ceef4b6c74c83dd1d77648ee46f3e593d268 Mon Sep 17 00:00:00 2001 From: Bruno de Oliveira Abinader Date: Fri, 18 Dec 2015 15:26:36 -0800 Subject: [PATCH 01/15] [osx] Use framebuffer width/height in GLFW port Framebuffer width/height takes pixel ration in consideration. Part of #3294. --- platform/default/glfw_view.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/default/glfw_view.cpp b/platform/default/glfw_view.cpp index 9cb0ce69d..66a41b0e1 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() { From ec5afdd8b78224593bc9f29ef2c6e7c6cde60579 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Fri, 18 Dec 2015 15:27:00 -0800 Subject: [PATCH 02/15] [core] Merge default_file_source_impl.hpp into default_file_source.cpp After previous refactors, no other files include default_file_source_imp.hpp. --- src/mbgl/storage/default_file_source.cpp | 103 +++++++++++++++- src/mbgl/storage/default_file_source_impl.hpp | 115 ------------------ 2 files changed, 102 insertions(+), 116 deletions(-) delete mode 100644 src/mbgl/storage/default_file_source_impl.hpp diff --git a/src/mbgl/storage/default_file_source.cpp b/src/mbgl/storage/default_file_source.cpp index 367c08336..3a8a09a99 100644 --- a/src/mbgl/storage/default_file_source.cpp +++ b/src/mbgl/storage/default_file_source.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include @@ -11,6 +11,9 @@ #include #include #include +#include +#include +#include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wshadow" @@ -21,11 +24,109 @@ #include #include +#include +#include namespace algo = boost::algorithm; 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; +}; + +class DefaultFileRequestImpl : public util::noncopyable { +public: + using Callback = std::function; + + const Resource resource; + std::unique_ptr cacheRequest; + RequestBase* realRequest = nullptr; + std::unique_ptr 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 std::shared_ptr& 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 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 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; + + 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::Hash> pending; + FileCache* const cache; + const std::string assetRoot; + const std::unique_ptr assetContext; + const std::unique_ptr httpContext; + util::AsyncTask reachability; +}; + DefaultFileSource::DefaultFileSource(FileCache* cache, const std::string& root) : thread(std::make_unique>( util::ThreadContext{ "FileSource", util::ThreadType::Unknown, util::ThreadPriority::Low }, 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 9b5f97332..000000000 --- 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 -#include -#include -#include -#include -#include -#include - -#include -#include - -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; -}; - -class DefaultFileRequestImpl : public util::noncopyable { -public: - using Callback = std::function; - - const Resource resource; - std::unique_ptr cacheRequest; - RequestBase* realRequest = nullptr; - std::unique_ptr 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 std::shared_ptr& 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 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 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; - - 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::Hash> pending; - FileCache* const cache; - const std::string assetRoot; - const std::unique_ptr assetContext; - const std::unique_ptr httpContext; - util::AsyncTask reachability; -}; - -} // namespace mbgl - -#endif From b8378420d4cbc33d610c2d2e44a7825ba9a434f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Sat, 19 Dec 2015 14:36:22 -0800 Subject: [PATCH 03/15] [osx] Typo in osxapp help --- platform/osx/app/AppDelegate.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/osx/app/AppDelegate.m b/platform/osx/app/AppDelegate.m index bb3961388..3df79cfe0 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]; From 182380c4b1c3842bd31249eb0e29a13c7e39636d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Sat, 19 Dec 2015 20:08:07 -0800 Subject: [PATCH 04/15] [ios, osx] Corrected MGLMapCamera debug description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s MGLMapCamera (or a subclass), not MKMapCamera. --- platform/darwin/MGLMapCamera.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platform/darwin/MGLMapCamera.mm b/platform/darwin/MGLMapCamera.mm index d04e46fa9..6ebd16ce7 100644 --- a/platform/darwin/MGLMapCamera.mm +++ b/platform/darwin/MGLMapCamera.mm @@ -94,8 +94,8 @@ - (NSString *)description { - return [NSString stringWithFormat:@"", - 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]; } @end From af9de6c0ac335cc4d9d9e3a526dc6e3327a7af85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Fri, 18 Dec 2015 13:17:48 -0800 Subject: [PATCH 05/15] [core] Refined and commented flyTo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrote the flyTo implementation to more closely match GL JS’s implementation and the paper on which it is based. Rewrote CameraOptions documentation. Only document units for generic types like double. The semantics of LatLng and Duration are already baked into the types; one just needs to look up the types’ definitions. Also, the […) is set notation, so the braces are supposed to be mismatched. Fixes #3296. --- include/mbgl/map/camera.hpp | 47 +++++++++-- src/mbgl/map/transform.cpp | 141 +++++++++++++++++++------------ src/mbgl/map/transform.hpp | 2 + src/mbgl/map/transform_state.cpp | 29 +++++++ src/mbgl/map/transform_state.hpp | 3 + 5 files changed, 161 insertions(+), 61 deletions(-) diff --git a/include/mbgl/map/camera.hpp b/include/mbgl/map/camera.hpp index 184ee8746..626ccac35 100644 --- a/include/mbgl/map/camera.hpp +++ b/include/mbgl/map/camera.hpp @@ -11,16 +11,53 @@ namespace mbgl { +/** Various options for describing the viewpoint of a map, along with parameters + for transitioning to the viewpoint with animation. All fields are optional; + the default values of transition options depend on how this struct is used. + */ struct CameraOptions { - mapbox::util::optional center; // Map center (Degrees) - mapbox::util::optional zoom; // Map zoom level Positive Numbers > 0 and < 18 - mapbox::util::optional angle; // Map rotation bearing in Radians counter-clockwise from north. The value is wrapped to [−π rad, π rad] - mapbox::util::optional pitch; // Map angle in degrees at which the camera is looking to ground (Radians) - mapbox::util::optional duration; // Animation time length (Nanoseconds) + // Viewpoint options + + /** Coordinate at the center of the map. */ + mapbox::util::optional center; + + /** Zero-based zoom level. Constrained to the minimum and maximum zoom + levels. */ + mapbox::util::optional zoom; + + /** Bearing, measured in radians counterclockwise from true north. Wrapped + to [−π rad, π rad). */ + mapbox::util::optional angle; + + /** Pitch toward the horizon measured in radians, with 0 rad resulting in a + two-dimensional map. */ + mapbox::util::optional pitch; + + // Transition options + + /** Time to animate to the viewpoint defined herein. */ + mapbox::util::optional duration; + + /** Average velocity of a flyTo() transition, measured in distance units per + second. */ mapbox::util::optional speed; + + /** The relative amount of zooming that takes place along the flight path of + a flyTo() transition. A high value maximizes zooming for an exaggerated + animation, while a low value minimizes zooming for something closer to + easeTo(). */ mapbox::util::optional curve; + + /** The easing timing curve of the transition. */ mapbox::util::optional easing; + + /** A function that is called on each frame of the transition, just before a + screen update, except on the last frame. The first parameter indicates + the elapsed time as a percentage of the duration. */ std::function transitionFrameFn; + + /** A function that is called once on the last frame of the transition, just + before the corresponding screen update. */ std::function transitionFinishFn; }; diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index 84851febb..759e6b4e0 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -87,7 +87,7 @@ void Transform::easeTo(const CameraOptions& options) { return; } - double new_scale = std::pow(2.0, zoom); + double new_scale = state.zoomScale(zoom); const double s = new_scale * util::tileSize; state.Bc = s / 360; @@ -148,9 +148,9 @@ void Transform::setLatLng(const LatLng& latLng, const PrecisionPoint& point, con float rowDiff = coordAtPoint.row - coord.row; auto newLatLng = state.coordinateToLatLng({ - coordCenter.column - columnDiff, - coordCenter.row - rowDiff, - coordCenter.zoom + coordCenter.column - columnDiff, + coordCenter.row - rowDiff, + coordCenter.zoom }); setLatLng(newLatLng, duration); @@ -324,11 +324,15 @@ void Transform::_easeTo(const CameraOptions& options, double new_scale, double n } } -/** -* Flying animation to a specified location/zoom/bearing with automatic curve. -*/ +/** This method implements an “optimal path” animation, as detailed in: + + Van Wijk, Jarke J.; Nuij, Wim A. A. “Smooth and efficient zooming and + panning.” INFOVIS ’03. pp. 15–22. + . + + Where applicable, local variable documentation begins with the associated + variable or function in van Wijk (2003). */ void Transform::flyTo(const CameraOptions &options) { - CameraOptions flyOptions(options); LatLng latLng = options.center ? *options.center : getLatLng(); LatLng startLatLng = getLatLng(); @@ -340,101 +344,126 @@ void Transform::flyTo(const CameraOptions &options) { return; } - double new_scale = std::pow(2.0, zoom); - - const double scaled_tile_size = new_scale * util::tileSize; - state.Bc = scaled_tile_size / 360; - state.Cc = scaled_tile_size / util::M2PI; + const PrecisionPoint startPoint = { + state.lngX(startLatLng.longitude), + state.latY(startLatLng.latitude), + }; + const PrecisionPoint endPoint = { + state.lngX(latLng.longitude), + state.latY(latLng.latitude), + }; - const double m = 1 - 1e-15; - const double f = ::fmin(::fmax(std::sin(util::DEG2RAD * latLng.latitude), -m), m); + // Minimize rotation by taking the shorter path around the circle. + double normalizedAngle = _normalizeAngle(angle, state.angle); + state.angle = _normalizeAngle(state.angle, normalizedAngle); - double xn = -latLng.longitude * state.Bc; - double yn = 0.5 * state.Cc * std::log((1 + f) / (1 - f)); + const double startZoom = state.scaleZoom(state.scale); + const double startAngle = state.angle; + const double startPitch = state.pitch; - view.notifyMapChange(MapChangeRegionWillChangeAnimated); - - const double startZ = state.scaleZoom(state.scale); - const double startA = state.angle; - const double startP = state.pitch; - state.panning = true; - state.scaling = true; - state.rotating = true; - + /** ρ: The relative amount of zooming that takes place along the flight + path. A high value maximizes zooming for an exaggerated animation, while + a low value minimizes zooming for something closer to easeTo(). + + 1.42 is the average value selected by participants in the user study in + van Wijk (2003). A value of 6¼ would be equivalent to the + root mean squared average velocity, VRMS. */ double rho = flyOptions.curve ? *flyOptions.curve : 1.42; + /// w₀: Initial visible range. double w0 = std::max(state.width, state.height); - double w1 = w0 / new_scale; - double u1 = ::hypot(xn, yn); + /// w₁: Target visible range. + double w1 = w0 / state.zoomScale(zoom - startZoom); + /// Length of the flight path as projected onto the ground plane. + double u1 = ::hypot((endPoint - startPoint).x, (endPoint - startPoint).y); + /// ρ² double rho2 = rho * rho; + /// rᵢ auto r = [=](double i) { + /// bᵢ double b = (w1 * w1 - w0 * w0 + (i ? -1 : 1) * rho2 * rho2 * u1 * u1) / (2 * (i ? w1 : w0) * rho2 * u1); return std::log(std::sqrt(b * b + 1) - b); }; - bool is_close = std::abs(u1) < 0.000001; - if (is_close && std::abs(w0 - w1) < 0.000001) { + // When u₀ = u₁, the optimal path doesn’t require both ascent and descent. + bool isClose = std::abs(u1) < 0.000001; + // Bail if the path is too short. + if (isClose && std::abs(w0 - w1) < 0.000001) { return; } + /// r₀: rᵢ where i = 0. double r0 = r(0); + /** w(s): Visible range on the ground, proportional to the scale, measured + in world coordinates. + + Assumes an angular field of view of 2 arctan ½ ≈ 53°. */ auto w = [=](double s) { - return (is_close ? std::exp((w1 < w0 ? -1 : 1) * rho * s) + return (isClose ? std::exp((w1 < w0 ? -1 : 1) * rho * s) : (std::cosh(r0) / std::cosh(r0 + rho * s))); }; + /// u(s): Distance along the flight path as projected onto the ground plane. auto u = [=](double s) { - return (is_close ? 0. - : (w0 * ((std::cosh(r0) * std::tanh(r0 + rho * s) - std::sinh(r0)) / rho2) / u1)); + return (isClose ? 0. + : (w0 * (std::cosh(r0) * std::tanh(r0 + rho * s) - std::sinh(r0)) / rho2 / u1)); }; - double S = (is_close ? (std::abs(std::log(w1 / w0)) / rho) + /// S: Total length of the flight path. + double S = (isClose ? (std::abs(std::log(w1 / w0)) / rho) : ((r(1) - r0) / rho)); Duration duration; if (flyOptions.duration) { duration = *flyOptions.duration; } else { - double speed = flyOptions.speed ? *flyOptions.speed : 1.2; + /// V: Average velocity. + double velocity = flyOptions.speed ? *flyOptions.speed : 1.2; duration = std::chrono::duration_cast( - std::chrono::duration(S / speed)); + std::chrono::duration(S / velocity)); } if (duration == Duration::zero()) { // Atomic transition. jumpTo(options); return; } + + view.notifyMapChange(MapChangeRegionWillChangeAnimated); + + const double startWorldSize = state.worldSize(); + state.Bc = startWorldSize / 360; + state.Cc = startWorldSize / util::M2PI; + + state.panning = true; + state.scaling = true; + state.rotating = angle != startAngle; + startTransition( [=](double t) { util::UnitBezier ease = flyOptions.easing ? *flyOptions.easing : util::UnitBezier(0, 0, 0.25, 1); return ease.solve(t, 0.001); }, [=](double k) { + /// s: The distance traveled along the flight path. double s = k * S; double us = u(s); - //First calculate the desired latlng - double desiredLat = startLatLng.latitude + (latLng.latitude - startLatLng.latitude) * us; - double desiredLng = startLatLng.longitude + (latLng.longitude - startLatLng.longitude) * us; + // Calculate the current point and zoom level along the flight path. + PrecisionPoint framePoint = startPoint + (endPoint - startPoint) * us; + double frameZoom = startZoom + state.scaleZoom(1 / w(s)); - //Now calculate desired zoom - double desiredZoom = startZ + state.scaleZoom(1 / w(s)); - double desiredScale = state.zoomScale(desiredZoom); - state.scale = ::fmax(::fmin(desiredScale, state.max_scale), state.min_scale); + // Convert to geographic coordinates and set the new viewpoint. + LatLng frameLatLng = { + state.yLat(framePoint.y, startWorldSize), + state.xLng(framePoint.x, startWorldSize), + }; + state.setLatLngZoom(frameLatLng, frameZoom); - //Now set values - const double new_scaled_tile_size = state.scale * util::tileSize; - state.Bc = new_scaled_tile_size / 360; - state.Cc = new_scaled_tile_size / util::M2PI; - - const double f2 = ::fmin(::fmax(std::sin(util::DEG2RAD * desiredLat), -m), m); - state.x = -desiredLng * state.Bc; - state.y = 0.5 * state.Cc * std::log((1 + f2) / (1 - f2)); - - if (angle != startA) { - state.angle = util::wrap(util::interpolate(startA, angle, k), -M_PI, M_PI); + if (angle != startAngle) { + state.angle = util::wrap(util::interpolate(startAngle, normalizedAngle, k), -M_PI, M_PI); } - if (pitch != startP) { - state.pitch = util::clamp(util::interpolate(startP, pitch, k), 0., 60.); + if (pitch != startPitch) { + state.pitch = util::clamp(util::interpolate(startPitch, pitch, k), 0., 60.); } + // At k = 1.0, a DidChangeAnimated notification should be sent from finish(). if (k < 1.0) { if (options.transitionFrameFn) { diff --git a/src/mbgl/map/transform.hpp b/src/mbgl/map/transform.hpp index 0bb6bb851..b5bbe9d30 100644 --- a/src/mbgl/map/transform.hpp +++ b/src/mbgl/map/transform.hpp @@ -26,6 +26,8 @@ public: void jumpTo(const CameraOptions&); void easeTo(const CameraOptions&); + /** Smoothly zoom out, pan, and zoom back into the given camera along a + great circle, as though the viewer is aboard a supersonic jetcopter. */ void flyTo(const CameraOptions&); // Position diff --git a/src/mbgl/map/transform_state.cpp b/src/mbgl/map/transform_state.cpp index f608edb10..ccc5ab365 100644 --- a/src/mbgl/map/transform_state.cpp +++ b/src/mbgl/map/transform_state.cpp @@ -4,6 +4,7 @@ #include #include #include +#include using namespace mbgl; @@ -370,4 +371,32 @@ void TransformState::constrain(double& scale_, double& x_, double& y_) const { y_ = std::max(-max_y, std::min(y_, max_y)); } +void TransformState::setLatLngZoom(const LatLng &latLng, double zoom) { + double newScale = zoomScale(zoom); + const double newWorldSize = newScale * util::tileSize; + Bc = newWorldSize / 360; + Cc = newWorldSize / util::M2PI; + + const double m = 1 - 1e-15; + const double f = util::clamp(std::sin(util::DEG2RAD * latLng.latitude), -m, m); + + PrecisionPoint point = { + -latLng.longitude * Bc, + 0.5 * Cc * std::log((1 + f) / (1 - f)), + }; + setScalePoint(newScale, point); +} + +void TransformState::setScalePoint(const double newScale, const PrecisionPoint &point) { + double constrainedScale = newScale; + PrecisionPoint constrainedPoint = point; + constrain(constrainedScale, constrainedPoint.x, constrainedPoint.y); + + scale = constrainedScale; + x = constrainedPoint.x; + y = constrainedPoint.y; + Bc = worldSize() / 360; + Cc = worldSize() / util::M2PI; +} + diff --git a/src/mbgl/map/transform_state.hpp b/src/mbgl/map/transform_state.hpp index fa6ed8b58..9ae2f62a4 100644 --- a/src/mbgl/map/transform_state.hpp +++ b/src/mbgl/map/transform_state.hpp @@ -100,6 +100,9 @@ private: mat4 coordinatePointMatrix(double z) const; mat4 getPixelMatrix() const; + + void setLatLngZoom(const LatLng &latLng, double zoom); + void setScalePoint(const double scale, const PrecisionPoint &point); private: ConstrainMode constrainMode; From 97c143a3388647e74a9f74a4994ab9d6aafe26a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Fri, 18 Dec 2015 22:02:50 -0800 Subject: [PATCH 06/15] [core] Fly across the meridian or antemeridian, whichever is shorter ref mapbox/mapbox-gl-js#1853 --- src/mbgl/map/transform.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index 759e6b4e0..badd5d89a 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -344,6 +344,16 @@ void Transform::flyTo(const CameraOptions &options) { return; } + // If a path crossing the antemeridian would be shorter, extend the final + // coordinate so that interpolating between the two endpoints will cross it. + if (std::abs(startLatLng.longitude) + std::abs(latLng.longitude) > 180) { + if (startLatLng.longitude > 0 && latLng.longitude < 0) { + latLng.longitude += 360; + } else if (startLatLng.longitude < 0 && latLng.longitude > 0) { + latLng.longitude -= 360; + } + } + const PrecisionPoint startPoint = { state.lngX(startLatLng.longitude), state.latY(startLatLng.latitude), From 0a9b6a82e4189d23c565adb2bf598c1bd7a3c309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sat, 19 Dec 2015 10:22:27 -0800 Subject: [PATCH 07/15] [core] Reformulated flyTo options The speed and curve fields in CameraOptions have been replaced with velocity and minZoom fields, which expressed in screen units. Scaling and easing make it difficult to derive options that are expressed in physical units. --- include/mbgl/map/camera.hpp | 17 ++++++----- src/mbgl/map/transform.cpp | 59 +++++++++++++++++++++++++------------ 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/include/mbgl/map/camera.hpp b/include/mbgl/map/camera.hpp index 626ccac35..73d7d5e7d 100644 --- a/include/mbgl/map/camera.hpp +++ b/include/mbgl/map/camera.hpp @@ -38,15 +38,16 @@ struct CameraOptions { /** Time to animate to the viewpoint defined herein. */ mapbox::util::optional duration; - /** Average velocity of a flyTo() transition, measured in distance units per - second. */ - mapbox::util::optional speed; + /** Average velocity of a flyTo() transition, measured in screenfuls per + second, assuming a linear timing curve. + + A screenful is the visible span in pixels. It does not correspond + to a fixed physical distance but rather varies by zoom level. */ + mapbox::util::optional velocity; - /** The relative amount of zooming that takes place along the flight path of - a flyTo() transition. A high value maximizes zooming for an exaggerated - animation, while a low value minimizes zooming for something closer to - easeTo(). */ - mapbox::util::optional curve; + /** Zero-based zoom level at the peak of the flyTo() transition’s flight + path. */ + mapbox::util::optional minZoom; /** The easing timing curve of the transition. */ mapbox::util::optional easing; diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index badd5d89a..3da31d64b 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -371,24 +371,38 @@ void Transform::flyTo(const CameraOptions &options) { const double startAngle = state.angle; const double startPitch = state.pitch; + /// w₀: Initial visible span, measured in pixels at the initial scale. + /// Known henceforth as a screenful. + double w0 = std::max(state.width, state.height); + /// w₁: Final visible span, measured in pixels with respect to the initial + /// scale. + double w1 = w0 / state.zoomScale(zoom - startZoom); + /// Length of the flight path as projected onto the ground plane, measured + /// in pixels from the world image origin at the initial scale. + double u1 = ::hypot((endPoint - startPoint).x, (endPoint - startPoint).y); + /** ρ: The relative amount of zooming that takes place along the flight path. A high value maximizes zooming for an exaggerated animation, while a low value minimizes zooming for something closer to easeTo(). 1.42 is the average value selected by participants in the user study in van Wijk (2003). A value of 6¼ would be equivalent to the - root mean squared average velocity, VRMS. */ - double rho = flyOptions.curve ? *flyOptions.curve : 1.42; - /// w₀: Initial visible range. - double w0 = std::max(state.width, state.height); - /// w₁: Target visible range. - double w1 = w0 / state.zoomScale(zoom - startZoom); - /// Length of the flight path as projected onto the ground plane. - double u1 = ::hypot((endPoint - startPoint).x, (endPoint - startPoint).y); + root mean squared average velocity, VRMS. A value of 1 would + produce a circular motion. */ + double rho = 1.42; + if (flyOptions.minZoom) { + double minZoom = util::min(*flyOptions.minZoom, startZoom, zoom); + /// wm: Maximum visible span, measured in pixels with respect + /// to the initial scale. + double wMax = w0 / state.zoomScale(minZoom - startZoom); + rho = std::sqrt(wMax / u1 * 2); + } /// ρ² double rho2 = rho * rho; - /// rᵢ + /** rᵢ: Returns the zoom-out factor at one end of the animation. + + @param i 0 for the ascent or 1 for the descent. */ auto r = [=](double i) { /// bᵢ double b = (w1 * w1 - w0 * w0 + (i ? -1 : 1) * rho2 * rho2 * u1 * u1) / (2 * (i ? w1 : w0) * rho2 * u1); @@ -397,27 +411,30 @@ void Transform::flyTo(const CameraOptions &options) { // When u₀ = u₁, the optimal path doesn’t require both ascent and descent. bool isClose = std::abs(u1) < 0.000001; - // Bail if the path is too short. + // Perform a more or less instantaneous transition if the path is too short. if (isClose && std::abs(w0 - w1) < 0.000001) { + easeTo(options); return; } - /// r₀: rᵢ where i = 0. + /// r₀: Zoom-out factor during ascent. double r0 = r(0); - /** w(s): Visible range on the ground, proportional to the scale, measured - in world coordinates. + /** w(s): Returns the visible span on the ground, measured in pixels with + respect to the initial scale. Assumes an angular field of view of 2 arctan ½ ≈ 53°. */ auto w = [=](double s) { return (isClose ? std::exp((w1 < w0 ? -1 : 1) * rho * s) : (std::cosh(r0) / std::cosh(r0 + rho * s))); }; - /// u(s): Distance along the flight path as projected onto the ground plane. + /// u(s): Returns the distance along the flight path as projected onto the + /// ground plane, measured in pixels from the world image origin at the + /// initial scale. auto u = [=](double s) { return (isClose ? 0. : (w0 * (std::cosh(r0) * std::tanh(r0 + rho * s) - std::sinh(r0)) / rho2 / u1)); }; - /// S: Total length of the flight path. + /// S: Total length of the flight path, measured in ρ-screenfuls. double S = (isClose ? (std::abs(std::log(w1 / w0)) / rho) : ((r(1) - r0) / rho)); @@ -425,13 +442,16 @@ void Transform::flyTo(const CameraOptions &options) { if (flyOptions.duration) { duration = *flyOptions.duration; } else { - /// V: Average velocity. - double velocity = flyOptions.speed ? *flyOptions.speed : 1.2; + /// V: Average velocity, measured in ρ-screenfuls per second. + double velocity = 1.2; + if (flyOptions.velocity) { + velocity = *flyOptions.velocity / rho; + } duration = std::chrono::duration_cast( std::chrono::duration(S / velocity)); } if (duration == Duration::zero()) { - // Atomic transition. + // Perform an instantaneous transition. jumpTo(options); return; } @@ -452,7 +472,8 @@ void Transform::flyTo(const CameraOptions &options) { return ease.solve(t, 0.001); }, [=](double k) { - /// s: The distance traveled along the flight path. + /// s: The distance traveled along the flight path, measured in + /// ρ-screenfuls. double s = k * S; double us = u(s); From 2cb70eac3ce7350ba2c0b5e93a7184a30e07c5cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sat, 19 Dec 2015 10:28:11 -0800 Subject: [PATCH 08/15] [ios, osx] Allow zero-duration flight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For consistency with -setCamera:…, a duration of 0 jumps instantaneously to the new location, while a negative value uses the distance-dependent default. Due to the produced curve and applied easing, flyTo() doesn’t model a parabola or ballistic trajectory, but it does simulate powered flight to some extent. --- include/mbgl/ios/MGLMapView.h | 12 +++++++++--- include/mbgl/osx/MGLMapView.h | 21 ++++++++++++++------- platform/ios/src/MGLMapView.mm | 4 ++-- platform/osx/src/MGLMapView.mm | 4 ++-- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/include/mbgl/ios/MGLMapView.h b/include/mbgl/ios/MGLMapView.h index f9c829622..32d68aa2c 100644 --- a/include/mbgl/ios/MGLMapView.h +++ b/include/mbgl/ios/MGLMapView.h @@ -218,14 +218,20 @@ IB_DESIGNABLE * @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; -/** Uses a ballistic parabolic motion to "fly" the viewpoint to a different location with respect to the map with a default duration based on the length of the flight path. +/** Moves the viewpoint to a different location using a transition animation that evokes powered flight and a default duration based on the length of the flight path. +* +* 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 completion The block to execute after the animation finishes. */ - (void)flyToCamera:(MGLMapCamera *)camera completionHandler:(nullable void (^)(void))completion; -/** Uses a ballistic parabolic motion to "fly" the viewpoint to a different location with respect to the map with an optional transition duration. +/** Moves the viewpoint to a different location using a transition animation that evokes powered flight and an optional transition duration. +* +* 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 use the default duration, which is based on the length of the flight path. +* @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 completion The block to execute after the animation finishes. */ - (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration completionHandler:(nullable void (^)(void))completion; diff --git a/include/mbgl/osx/MGLMapView.h b/include/mbgl/osx/MGLMapView.h index 40fd2a6bc..6f4682544 100644 --- a/include/mbgl/osx/MGLMapView.h +++ b/include/mbgl/osx/MGLMapView.h @@ -249,21 +249,28 @@ IB_DESIGNABLE @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; -/** Uses a ballistic parabolic motion to “fly” the viewpoint to a different - location with respect to the map with a default duration based on the length - of the flight path. +/** Moves the viewpoint to a different location using a transition animation + that evokes powered flight and a default duration based on the length of the + flight path. + + 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 completion The block to execute after the animation finishes. */ - (void)flyToCamera:(MGLMapCamera *)camera completionHandler:(nullable void (^)(void))completion; -/** Uses a ballistic parabolic motion to “fly” the viewpoint to a different - location with respect to the map with an optional transition duration. +/** Moves the viewpoint to a different location using a transition animation + that evokes powered flight and an optional transition duration. + + 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 use the default duration, which is - based on the length of the flight path. + 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 completion The block to execute after the animation finishes. */ - (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration completionHandler:(nullable void (^)(void))completion; diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 98df29fdd..23c027b2a 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -1813,14 +1813,14 @@ std::chrono::steady_clock::duration MGLDurationInSeconds(float duration) - (void)flyToCamera:(MGLMapCamera *)camera completionHandler:(nullable void (^)(void))completion { - [self flyToCamera:camera withDuration:0 completionHandler:completion]; + [self flyToCamera:camera withDuration:-1 completionHandler:completion]; } - (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration completionHandler:(nullable void (^)(void))completion { _mbglMap->cancelTransitions(); mbgl::CameraOptions options = [self cameraOptionsObjectForAnimatingToCamera:camera]; - if (duration > 0) + if (duration >= 0) { options.duration = MGLDurationInSeconds(duration); } diff --git a/platform/osx/src/MGLMapView.mm b/platform/osx/src/MGLMapView.mm index 8767c3041..e4b86f36d 100644 --- a/platform/osx/src/MGLMapView.mm +++ b/platform/osx/src/MGLMapView.mm @@ -1009,14 +1009,14 @@ public: } - (void)flyToCamera:(MGLMapCamera *)camera completionHandler:(nullable void (^)(void))completion { - [self flyToCamera:camera withDuration:0 completionHandler:completion]; + [self flyToCamera:camera withDuration:-1 completionHandler:completion]; } - (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration completionHandler:(nullable void (^)(void))completion { _mbglMap->cancelTransitions(); mbgl::CameraOptions options = [self cameraOptionsObjectForAnimatingToCamera:camera]; - if (duration > 0) { + if (duration >= 0) { options.duration = MGLDurationInSeconds(duration); } if (completion) { From a5230f3e110c4c286721a45add063db2380b5c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sat, 19 Dec 2015 23:14:01 -0800 Subject: [PATCH 09/15] [core] Clamp pitch in easeTo(), flyTo() Also factored out the maximum pitch into a constant. --- include/mbgl/util/constants.hpp | 1 + src/mbgl/map/map.cpp | 2 +- src/mbgl/map/transform.cpp | 5 ++++- src/mbgl/util/constants.cpp | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/include/mbgl/util/constants.hpp b/include/mbgl/util/constants.hpp index 113a36812..22a460c9b 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/src/mbgl/map/map.cpp b/src/mbgl/map/map.cpp index ff4b2f5c0..8fe73458f 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 3da31d64b..86d204f58 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(); @@ -367,6 +368,8 @@ void Transform::flyTo(const CameraOptions &options) { 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; @@ -492,7 +495,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/util/constants.cpp b/src/mbgl/util/constants.cpp index ccdbeba23..9a5282d5e 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; From cafe591d2a84d70a2a6c076b9629135e42e68f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sat, 19 Dec 2015 23:30:21 -0800 Subject: [PATCH 10/15] [osx] Flipped drag-rotate center point --- platform/osx/src/MGLMapView.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/osx/src/MGLMapView.mm b/platform/osx/src/MGLMapView.mm index e4b86f36d..c8145a67d 100644 --- a/platform/osx/src/MGLMapView.mm +++ b/platform/osx/src/MGLMapView.mm @@ -1168,7 +1168,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"]; From d668c0fa8482c86a2a73e5fc9661e32d89f26c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 20 Dec 2015 01:22:52 -0800 Subject: [PATCH 11/15] =?UTF-8?q?[ios,=20osx]=20Better=20zoom=20level?= =?UTF-8?q?=E2=80=93altitude=20conversion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greatly simplified conversions between zoom level and altitude. Adjusted field of view to more closely match MapKit. Conversions are now well within the same order of magnitude as MapKit at all latitudes. Fixes #2266. --- CHANGELOG.md | 1 + platform/darwin/MGLGeometry.mm | 25 ++++++ platform/darwin/MGLGeometry_Private.h | 18 +++++ platform/ios/src/MGLMapView.mm | 110 ++++---------------------- platform/osx/src/MGLMapView.mm | 99 +++-------------------- src/mbgl/map/transform.cpp | 3 + 6 files changed, 71 insertions(+), 185 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23d594fe7..3c9e5e687 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/platform/darwin/MGLGeometry.mm b/platform/darwin/MGLGeometry.mm index b80203d14..70f00afd2 100644 --- a/platform/darwin/MGLGeometry.mm +++ b/platform/darwin/MGLGeometry.mm @@ -1,5 +1,15 @@ #import "MGLGeometry_Private.h" +#import + +/** 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 49a306701..bf5bc4e0f 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/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 23c027b2a..7dbce7d2e 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]; @@ -1840,62 +1800,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/src/MGLMapView.mm b/platform/osx/src/MGLMapView.mm index c8145a67d..e0d44a271 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 @@ -1038,55 +998,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; } diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index 86d204f58..04c0f8bda 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -364,6 +364,8 @@ 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); @@ -395,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()); /// wm: Maximum visible span, measured in pixels with respect /// to the initial scale. double wMax = w0 / state.zoomScale(minZoom - startZoom); From b7223f249ddd714f344a00ce4da00dafb44cc7fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 20 Dec 2015 02:01:18 -0800 Subject: [PATCH 12/15] [ios, osx] Avoid transition to current camera Fixes #2456. --- platform/darwin/MGLMapCamera.mm | 24 ++++++++++++++++++++++++ platform/ios/src/MGLMapView.mm | 10 ++++++++++ platform/osx/src/MGLMapView.mm | 6 ++++++ 3 files changed, 40 insertions(+) diff --git a/platform/darwin/MGLMapCamera.mm b/platform/darwin/MGLMapCamera.mm index 6ebd16ce7..e32ef5a10 100644 --- a/platform/darwin/MGLMapCamera.mm +++ b/platform/darwin/MGLMapCamera.mm @@ -98,4 +98,28 @@ 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/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 7dbce7d2e..84b82a640 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -1750,6 +1750,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 { + if ([self.camera isEqual:camera]) + { + return; + } + _mbglMap->cancelTransitions(); mbgl::CameraOptions options = [self cameraOptionsObjectForAnimatingToCamera:camera]; if (duration > 0) @@ -1778,6 +1783,11 @@ std::chrono::steady_clock::duration MGLDurationInSeconds(float duration) - (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration completionHandler:(nullable void (^)(void))completion { + if ([self.camera isEqual:camera]) + { + return; + } + _mbglMap->cancelTransitions(); mbgl::CameraOptions options = [self cameraOptionsObjectForAnimatingToCamera:camera]; if (duration >= 0) diff --git a/platform/osx/src/MGLMapView.mm b/platform/osx/src/MGLMapView.mm index e0d44a271..b0f402ff7 100644 --- a/platform/osx/src/MGLMapView.mm +++ b/platform/osx/src/MGLMapView.mm @@ -946,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) { @@ -974,6 +977,9 @@ public: - (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration completionHandler:(nullable void (^)(void))completion { _mbglMap->cancelTransitions(); + if ([self.camera isEqual:camera]) { + return; + } mbgl::CameraOptions options = [self cameraOptionsObjectForAnimatingToCamera:camera]; if (duration >= 0) { From 49bfdd662ff37ac77cd702c2cef92d2de8874be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 20 Dec 2015 04:11:14 -0800 Subject: [PATCH 13/15] [ios, osx] Optional peak altitude for flying --- include/mbgl/ios/MGLMapView.h | 10 ++++++++++ include/mbgl/osx/MGLMapView.h | 19 +++++++++++++++++++ platform/ios/src/MGLMapView.mm | 16 ++++++++++++++-- platform/osx/src/MGLMapView.mm | 10 ++++++++++ 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/include/mbgl/ios/MGLMapView.h b/include/mbgl/ios/MGLMapView.h index 32d68aa2c..e10932bd2 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 6f4682544..ae701de27 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/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 84b82a640..d6980e165 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -1750,12 +1750,12 @@ 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; } - _mbglMap->cancelTransitions(); mbgl::CameraOptions options = [self cameraOptionsObjectForAnimatingToCamera:camera]; if (duration > 0) { @@ -1783,17 +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; } - _mbglMap->cancelTransitions(); 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]() { diff --git a/platform/osx/src/MGLMapView.mm b/platform/osx/src/MGLMapView.mm index b0f402ff7..9e95ef666 100644 --- a/platform/osx/src/MGLMapView.mm +++ b/platform/osx/src/MGLMapView.mm @@ -976,6 +976,10 @@ 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; @@ -985,6 +989,12 @@ public: 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. From 913f49c701781a4621c7f273d0ec937f9bf92697 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Mon, 21 Dec 2015 10:33:42 -0800 Subject: [PATCH 14/15] [core] Prepare for OfflineFileSource * Rename existing DefaultFileSource to OnlineFileSource * Restore a DefaultFileSource that's a passthrough to OnlineFileSource --- include/mbgl/storage/default_file_source.hpp | 21 +- src/mbgl/storage/default_file_source.cpp | 452 +----------------- src/mbgl/storage/online_file_source.cpp | 455 +++++++++++++++++++ src/mbgl/storage/online_file_source.hpp | 34 ++ 4 files changed, 511 insertions(+), 451 deletions(-) create mode 100644 src/mbgl/storage/online_file_source.cpp create mode 100644 src/mbgl/storage/online_file_source.hpp diff --git a/include/mbgl/storage/default_file_source.hpp b/include/mbgl/storage/default_file_source.hpp index e669cebf7..3689b9e93 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 -#include namespace mbgl { -namespace util { -template 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 request(const Resource&, Callback) override; private: - friend class DefaultFileRequest; - void cancel(const Resource&, FileRequest*); - class Impl; - const std::unique_ptr> thread; - std::string accessToken; + const std::unique_ptr impl; }; } // namespace mbgl diff --git a/src/mbgl/storage/default_file_source.cpp b/src/mbgl/storage/default_file_source.cpp index 3a8a09a99..b2ab5abd6 100644 --- a/src/mbgl/storage/default_file_source.cpp +++ b/src/mbgl/storage/default_file_source.cpp @@ -1,455 +1,33 @@ #include -#include -#include -#include - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wshadow" -#pragma GCC diagnostic ignored "-Wunknown-pragmas" -#pragma GCC diagnostic ignored "-Wunused-local-typedefs" -#include -#pragma GCC diagnostic pop - -#include -#include -#include -#include - -namespace algo = boost::algorithm; +#include 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; -}; - -class DefaultFileRequestImpl : public util::noncopyable { -public: - using Callback = std::function; - - const Resource resource; - std::unique_ptr cacheRequest; - RequestBase* realRequest = nullptr; - std::unique_ptr 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 std::shared_ptr& 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 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 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; + Impl(FileCache* cache, const std::string& root) + : onlineFileSource(cache, root) { + } - 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::Hash> pending; - FileCache* const cache; - const std::string assetRoot; - const std::unique_ptr assetContext; - const std::unique_ptr httpContext; - util::AsyncTask reachability; + OnlineFileSource onlineFileSource; }; DefaultFileSource::DefaultFileSource(FileCache* cache, const std::string& root) - : thread(std::make_unique>( - util::ThreadContext{ "FileSource", util::ThreadType::Unknown, util::ThreadPriority::Low }, - cache, - root)) { + : impl(std::make_unique(cache, root)) { } DefaultFileSource::~DefaultFileSource() = default; +void DefaultFileSource::setAccessToken(const std::string& accessToken) { + impl->onlineFileSource.setAccessToken(accessToken); +} + +std::string DefaultFileSource::getAccessToken() const { + return impl->onlineFileSource.getAccessToken(); +} + std::unique_ptr 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(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(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) { - 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 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. - } -} - -void DefaultFileSource::Impl::reschedule(DefaultFileRequestImpl& 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(); - } - - 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); - } - } -} - -void DefaultFileRequestImpl::setResponse(const std::shared_ptr& 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& DefaultFileRequestImpl::getResponse() const { - return response; -} - -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; -} - -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); - staleResponse->stale = true; - response = staleResponse; - } + return impl->onlineFileSource.request(resource, callback); } } // namespace mbgl diff --git a/src/mbgl/storage/online_file_source.cpp b/src/mbgl/storage/online_file_source.cpp new file mode 100644 index 000000000..1f3ccbcbd --- /dev/null +++ b/src/mbgl/storage/online_file_source.cpp @@ -0,0 +1,455 @@ +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wshadow" +#pragma GCC diagnostic ignored "-Wunknown-pragmas" +#pragma GCC diagnostic ignored "-Wunused-local-typedefs" +#include +#pragma GCC diagnostic pop + +#include +#include +#include +#include + +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; +}; + +class OnlineFileRequestImpl : public util::noncopyable { +public: + using Callback = std::function; + + const Resource resource; + std::unique_ptr cacheRequest; + RequestBase* realRequest = nullptr; + std::unique_ptr 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 std::shared_ptr& 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 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 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; + + 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::Hash> pending; + FileCache* const cache; + const std::string assetRoot; + const std::unique_ptr assetContext; + const std::unique_ptr httpContext; + util::AsyncTask reachability; +}; + +OnlineFileSource::OnlineFileSource(FileCache* cache, const std::string& root) + : thread(std::make_unique>( + util::ThreadContext{ "OnlineFileSource", util::ThreadType::Unknown, util::ThreadPriority::Low }, + cache, + root)) { +} + +OnlineFileSource::~OnlineFileSource() = default; + +std::unique_ptr 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(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(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) { + 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 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(); + } + + 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& 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& 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); + 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 000000000..357b1773d --- /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 +#include + +namespace mbgl { + +namespace util { +template 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 request(const Resource&, Callback) override; + +private: + friend class OnlineFileRequest; + void cancel(const Resource&, FileRequest*); + + class Impl; + const std::unique_ptr> thread; + std::string accessToken; +}; + +} // namespace mbgl + +#endif From 373916420ae5504041c710fa3216ef3b2d4de122 Mon Sep 17 00:00:00 2001 From: "Thiago Marcos P. Santos" Date: Mon, 21 Dec 2015 22:04:00 +0200 Subject: [PATCH 15/15] [tests] Create less threads on the AsyncTask tests So we open less fds as new versions of OSX have a smaller limit (256). --- test/miscellaneous/async_task.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/miscellaneous/async_task.cpp b/test/miscellaneous/async_task.cpp index c1a2bd82e..c32c2396f 100644 --- a/test/miscellaneous/async_task.cpp +++ b/test/miscellaneous/async_task.cpp @@ -75,7 +75,7 @@ TEST(AsyncTask, RequestCoalescingMultithreaded) { std::vector>> 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 = std::make_unique>(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) {