summaryrefslogtreecommitdiff
path: root/platform
diff options
context:
space:
mode:
Diffstat (limited to 'platform')
-rw-r--r--platform/android/http_request_android.cpp399
-rw-r--r--platform/default/glfw_view.cpp37
-rw-r--r--platform/default/http_request_curl.cpp109
-rw-r--r--platform/default/thread.cpp18
-rw-r--r--platform/ios/MGLMapView.mm5
-rw-r--r--platform/ios/MGLUserLocationAnnotationView.m328
6 files changed, 669 insertions, 227 deletions
diff --git a/platform/android/http_request_android.cpp b/platform/android/http_request_android.cpp
new file mode 100644
index 0000000000..044f772628
--- /dev/null
+++ b/platform/android/http_request_android.cpp
@@ -0,0 +1,399 @@
+#include <mbgl/storage/http_context_base.hpp>
+#include <mbgl/storage/http_request_base.hpp>
+#include <mbgl/storage/resource.hpp>
+#include <mbgl/storage/response.hpp>
+#include <mbgl/util/chrono.hpp>
+#include <mbgl/platform/log.hpp>
+#include <mbgl/android/jni.hpp>
+
+#include <mbgl/util/time.hpp>
+#include <mbgl/util/util.hpp>
+#include <mbgl/util/string.hpp>
+#include <mbgl/util/parsedate.h>
+
+#include <jni.h>
+
+namespace mbgl {
+
+void JNICALL nativeOnFailure(JNIEnv *env, jobject obj, jlong nativePtr, jint type, jstring message);
+void JNICALL nativeOnResponse(JNIEnv *env, jobject obj, jlong nativePtr, jint code, jstring message, jstring etag, jstring modified, jstring cacheControl, jstring expires, jbyteArray body);
+
+class HTTPAndroidRequest;
+
+class HTTPAndroidContext : public HTTPContextBase {
+public:
+ explicit HTTPAndroidContext(uv_loop_t *loop);
+ ~HTTPAndroidContext();
+
+ HTTPRequestBase* createRequest(const Resource&,
+ RequestBase::Callback,
+ uv_loop_t*,
+ std::shared_ptr<const Response>) final;
+
+ uv_loop_t *loop = nullptr;
+
+ JavaVM *vm = nullptr;
+ jobject obj = nullptr;
+};
+
+class HTTPAndroidRequest : public HTTPRequestBase {
+public:
+ HTTPAndroidRequest(HTTPAndroidContext*,
+ const Resource&,
+ Callback,
+ uv_loop_t*,
+ std::shared_ptr<const Response>);
+ ~HTTPAndroidRequest();
+
+ void cancel() final;
+ void retry() final;
+
+ void onFailure(int type, std::string message);
+ void onResponse(int code, std::string message, std::string etag, std::string modified, std::string cacheControl, std::string expires, std::string body);
+
+private:
+ void retry(uint64_t timeout) final;
+#if UV_VERSION_MAJOR == 0 && UV_VERSION_MINOR <= 10
+ static void restart(uv_timer_t *timer, int);
+#else
+ static void restart(uv_timer_t *timer);
+#endif
+ void finish(ResponseStatus status);
+ void start();
+
+ HTTPAndroidContext *context = nullptr;
+
+ bool cancelled = false;
+
+ std::unique_ptr<Response> response;
+ const std::shared_ptr<const Response> existingResponse;
+
+ jobject obj = nullptr;
+
+ uv_timer_t *timer = nullptr;
+ enum : bool { PreemptImmediately, ExponentialBackoff } strategy = PreemptImmediately;
+ int attempts = 0;
+
+ static const int maxAttempts = 4;
+
+ static const int connectionError = 0;
+ static const int temporaryError = 1;
+ static const int permanentError = 1;
+};
+
+// -------------------------------------------------------------------------------------------------
+
+HTTPAndroidContext::HTTPAndroidContext(uv_loop_t *loop_)
+ : HTTPContextBase(loop_),
+ loop(loop_),
+ vm(mbgl::android::theJVM) {
+
+ JNIEnv *env = nullptr;
+ bool detach = mbgl::android::attach_jni_thread(vm, &env, "HTTPAndroidContext::HTTPAndroidContext()");
+
+ const std::vector<JNINativeMethod> methods = {
+ {"nativeOnFailure", "(JILjava/lang/String;)V", reinterpret_cast<void *>(&nativeOnFailure)},
+ {"nativeOnResponse",
+ "(JILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[B)V",
+ reinterpret_cast<void *>(&nativeOnResponse)}
+ };
+
+ if (env->RegisterNatives(mbgl::android::httpRequestClass, methods.data(), methods.size()) < 0) {
+ env->ExceptionDescribe();
+ }
+
+ obj = env->CallStaticObjectMethod(mbgl::android::httpContextClass, mbgl::android::httpContextGetInstanceId);
+ if (env->ExceptionCheck() || (obj == nullptr)) {
+ env->ExceptionDescribe();
+ }
+
+ obj = env->NewGlobalRef(obj);
+ if (obj == nullptr) {
+ env->ExceptionDescribe();
+ }
+
+ mbgl::android::detach_jni_thread(vm, &env, detach);
+}
+
+HTTPAndroidContext::~HTTPAndroidContext() {
+ JNIEnv *env = nullptr;
+ bool detach = mbgl::android::attach_jni_thread(vm, &env, "HTTPAndroidContext::~HTTPAndroidContext()");
+
+ env->DeleteGlobalRef(obj);
+ obj = nullptr;
+
+ mbgl::android::detach_jni_thread(vm, &env, detach);
+
+ vm = nullptr;
+}
+
+HTTPRequestBase* HTTPAndroidContext::createRequest(const Resource& resource,
+ RequestBase::Callback callback,
+ uv_loop_t* loop_,
+ std::shared_ptr<const Response> response) {
+ return new HTTPAndroidRequest(this, resource, callback, loop_, response);
+}
+
+HTTPAndroidRequest::HTTPAndroidRequest(HTTPAndroidContext* context_, const Resource& resource_, Callback callback_, uv_loop_t*, std::shared_ptr<const Response> response_)
+ : HTTPRequestBase(resource_, callback_),
+ context(context_),
+ existingResponse(response_) {
+
+ std::string etagStr;
+ std::string modifiedStr;
+ if (existingResponse) {
+ if (!existingResponse->etag.empty()) {
+ etagStr = existingResponse->etag;
+ } else if (existingResponse->modified) {
+ modifiedStr = util::rfc1123(existingResponse->modified);
+ }
+ }
+
+ JNIEnv *env = nullptr;
+ bool detach = mbgl::android::attach_jni_thread(context->vm, &env, "HTTPAndroidContext::HTTPAndroidRequest()");
+
+ jstring resourceUrl = mbgl::android::std_string_to_jstring(env, resource.url);
+ jstring userAgent = mbgl::android::std_string_to_jstring(env, "MapboxGL/1.0");
+ jstring etag = mbgl::android::std_string_to_jstring(env, etagStr);
+ jstring modified = mbgl::android::std_string_to_jstring(env, modifiedStr);
+ obj = env->CallObjectMethod(context->obj, mbgl::android::httpContextCreateRequestId, reinterpret_cast<jlong>(this), resourceUrl, userAgent, etag, modified);
+ if (env->ExceptionCheck() || (obj == nullptr)) {
+ env->ExceptionDescribe();
+ }
+
+ obj = env->NewGlobalRef(obj);
+ if (obj == nullptr) {
+ env->ExceptionDescribe();
+ }
+
+ mbgl::android::detach_jni_thread(context->vm, &env, detach);
+
+ context->addRequest(this);
+ start();
+}
+
+HTTPAndroidRequest::~HTTPAndroidRequest() {
+ context->removeRequest(this);
+
+ JNIEnv *env = nullptr;
+ bool detach = mbgl::android::attach_jni_thread(context->vm, &env, "HTTPAndroidContext::~HTTPAndroidRequest()");
+
+ env->DeleteGlobalRef(obj);
+ obj = nullptr;
+
+ mbgl::android::detach_jni_thread(context->vm, &env, detach);
+
+ if (timer) {
+ uv_timer_stop(timer);
+ uv::close(timer);
+ timer = nullptr;
+ }
+}
+
+void HTTPAndroidRequest::cancel() {
+ cancelled = true;
+
+ JNIEnv *env = nullptr;
+ bool detach = mbgl::android::attach_jni_thread(context->vm, &env, "HTTPAndroidContext::cancel()");
+
+ env->CallVoidMethod(obj, mbgl::android::httpRequestCancelId);
+ if (env->ExceptionCheck()) {
+ env->ExceptionDescribe();
+ }
+
+ mbgl::android::detach_jni_thread(context->vm, &env, detach);
+}
+
+void HTTPAndroidRequest::start() {
+ attempts++;
+
+ JNIEnv *env = nullptr;
+ bool detach = mbgl::android::attach_jni_thread(context->vm, &env, "HTTPAndroidContext::start()");
+
+ env->CallVoidMethod(obj, mbgl::android::httpRequestStartId);
+ if (env->ExceptionCheck()) {
+ env->ExceptionDescribe();
+ }
+
+ mbgl::android::detach_jni_thread(context->vm, &env, detach);
+}
+
+void HTTPAndroidRequest::retry(uint64_t timeout) {
+ response.reset();
+
+ assert(!timer);
+ timer = new uv_timer_t;
+ timer->data = this;
+ uv_timer_init(context->loop, timer);
+ uv_timer_start(timer, restart, timeout, 0);
+}
+
+void HTTPAndroidRequest::retry() {
+ if (timer && strategy == PreemptImmediately) {
+ uv_timer_stop(timer);
+ uv_timer_start(timer, restart, 0, 0);
+ }
+}
+
+#if UV_VERSION_MAJOR == 0 && UV_VERSION_MINOR <= 10
+void HTTPAndroidRequest::restart(uv_timer_t *timer, int) {
+#else
+void HTTPAndroidRequest::restart(uv_timer_t *timer) {
+#endif
+ auto baton = reinterpret_cast<HTTPAndroidRequest *>(timer->data);
+
+ baton->timer = nullptr;
+ uv::close(timer);
+
+ baton->start();
+}
+
+void HTTPAndroidRequest::finish(ResponseStatus status) {
+ if (status == ResponseStatus::TemporaryError && attempts < maxAttempts) {
+ strategy = ExponentialBackoff;
+ return retry((1 << (attempts - 1)) * 1000);
+ } else if (status == ResponseStatus::ConnectionError && attempts < maxAttempts) {
+ strategy = PreemptImmediately;
+ return retry(30000);
+ }
+
+ if (status == ResponseStatus::NotModified) {
+ notify(std::move(response), FileCache::Hint::Refresh);
+ } else {
+ notify(std::move(response), FileCache::Hint::Full);
+ }
+
+ delete this;
+}
+
+int64_t parseCacheControl(const char *value) {
+ if (value) {
+ unsigned long long seconds = 0;
+ // TODO: cache-control may contain other information as well:
+ // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
+ if (std::sscanf(value, "max-age=%llu", &seconds) == 1) {
+ return std::chrono::duration_cast<std::chrono::seconds>(
+ std::chrono::system_clock::now().time_since_epoch()).count() +
+ seconds;
+ }
+ }
+
+ return 0;
+}
+
+void HTTPAndroidRequest::onResponse(int code, std::string message, std::string etag, std::string modified, std::string cacheControl, std::string expires, std::string body) {
+ if (cancelled) {
+ delete this;
+ return;
+ }
+
+ if (!response) {
+ response = std::make_unique<Response>();
+ }
+
+ response->message = message;
+ response->modified = parse_date(modified.c_str());
+ response->etag = etag;
+ response->expires = parseCacheControl(cacheControl.c_str());
+ if (!expires.empty()) {
+ response->expires = parse_date(expires.c_str());
+ }
+ response->data = body;
+
+ if (code == 304) {
+ if (existingResponse) {
+ response->status = existingResponse->status;
+ response->message = existingResponse->message;
+ response->modified = existingResponse->modified;
+ response->etag = existingResponse->etag;
+ response->data = existingResponse->data;
+ return finish(ResponseStatus::NotModified);
+ } else {
+ response->status = Response::Successful;
+ return finish(ResponseStatus::Successful);
+ }
+ } else if (code == 200) {
+ response->status = Response::Successful;
+ return finish(ResponseStatus::Successful);
+ } else if (code >= 500 && code < 600) {
+ response->status = Response::Error;
+ response->message = "HTTP status code " + util::toString(code);
+ return finish(ResponseStatus::TemporaryError);
+ } else {
+ response->status = Response::Error;
+ response->message = "HTTP status code " + util::toString(code);
+ return finish(ResponseStatus::PermanentError);
+ }
+
+ throw std::runtime_error("Response hasn't been handled");
+}
+
+void HTTPAndroidRequest::onFailure(int type, std::string message) {
+ if (cancelled) {
+ delete this;
+ return;
+ }
+
+ if (!response) {
+ response = std::make_unique<Response>();
+ }
+
+ response->status = Response::Error;
+ response->message = message;
+
+ switch (type) {
+ case connectionError:
+ return finish(ResponseStatus::ConnectionError);
+
+ case temporaryError:
+ return finish(ResponseStatus::TemporaryError);
+
+ default:
+ return finish(ResponseStatus::PermanentError);
+ }
+
+ throw std::runtime_error("Response hasn't been handled");
+}
+
+std::unique_ptr<HTTPContextBase> HTTPContextBase::createContext(uv_loop_t* loop) {
+ return std::make_unique<HTTPAndroidContext>(loop);
+}
+
+#pragma clang diagnostic ignored "-Wunused-parameter"
+
+void JNICALL nativeOnFailure(JNIEnv *env, jobject obj, jlong nativePtr, jint type, jstring message) {
+ mbgl::Log::Debug(mbgl::Event::JNI, "nativeOnFailure");
+ assert(nativePtr != 0);
+ HTTPAndroidRequest *request = reinterpret_cast<HTTPAndroidRequest *>(nativePtr);
+ std::string messageStr = mbgl::android::std_string_from_jstring(env, message);
+ return request->onFailure(type, messageStr);
+}
+
+void JNICALL nativeOnResponse(JNIEnv *env, jobject obj, jlong nativePtr, jint code, jstring message, jstring etag, jstring modified, jstring cacheControl, jstring expires, jbyteArray body) {
+ mbgl::Log::Debug(mbgl::Event::JNI, "nativeOnResponse");
+ assert(nativePtr != 0);
+ HTTPAndroidRequest *request = reinterpret_cast<HTTPAndroidRequest *>(nativePtr);
+ std::string messageStr = mbgl::android::std_string_from_jstring(env, message);
+ std::string etagStr;
+ if (etag != nullptr) {
+ etagStr = mbgl::android::std_string_from_jstring(env, etag);
+ }
+ std::string modifiedStr;
+ if (modified != nullptr) {
+ modifiedStr = mbgl::android::std_string_from_jstring(env, modified);
+ }
+ std::string cacheControlStr;
+ if (cacheControl != nullptr) {
+ cacheControlStr = mbgl::android::std_string_from_jstring(env, cacheControl);
+ }
+ std::string expiresStr;
+ if (expires != nullptr) {
+ expiresStr = mbgl::android::std_string_from_jstring(env, expires);
+ }
+ jbyte* bodyData = env->GetByteArrayElements(body, nullptr);
+ std::string bodyStr(reinterpret_cast<char*>(bodyData), env->GetArrayLength(body));
+ env->ReleaseByteArrayElements(body, bodyData, JNI_ABORT);
+ return request->onResponse(code, messageStr, etagStr, modifiedStr, cacheControlStr, expiresStr, bodyStr);
+}
+
+}
diff --git a/platform/default/glfw_view.cpp b/platform/default/glfw_view.cpp
index 078c26feef..8453845d1e 100644
--- a/platform/default/glfw_view.cpp
+++ b/platform/default/glfw_view.cpp
@@ -15,7 +15,8 @@ void glfwError(int error, const char *description) {
assert(false);
}
-GLFWView::GLFWView(bool fullscreen_) : fullscreen(fullscreen_) {
+GLFWView::GLFWView(bool fullscreen_, bool benchmark_)
+ : fullscreen(fullscreen_), benchmark(benchmark_) {
glfwSetErrorCallback(glfwError);
std::srand(std::time(0));
@@ -28,6 +29,9 @@ GLFWView::GLFWView(bool fullscreen_) : fullscreen(fullscreen_) {
GLFWmonitor *monitor = nullptr;
if (fullscreen) {
monitor = glfwGetPrimaryMonitor();
+ auto videoMode = glfwGetVideoMode(monitor);
+ width = videoMode->width;
+ height = videoMode->height;
}
#ifdef DEBUG
@@ -56,7 +60,13 @@ GLFWView::GLFWView(bool fullscreen_) : fullscreen(fullscreen_) {
glfwSetWindowUserPointer(window, this);
glfwMakeContextCurrent(window);
- glfwSwapInterval(1);
+ if (benchmark) {
+ // Disables vsync on platforms that support it.
+ glfwSwapInterval(0);
+ } else {
+ glfwSwapInterval(1);
+ }
+
glfwSetCursorPosCallback(window, onMouseMove);
glfwSetMouseButtonCallback(window, onMouseClick);
@@ -336,7 +346,12 @@ void GLFWView::run() {
glfwWaitEvents();
const bool dirty = !clean.test_and_set();
if (dirty) {
+ const double started = glfwGetTime();
map->renderSync();
+ report(1000 * (glfwGetTime() - started));
+ if (benchmark) {
+ map->setNeedsRepaint();
+ }
map->nudgeTransitions();
}
}
@@ -373,20 +388,20 @@ void GLFWView::invalidate() {
void GLFWView::swap() {
glfwSwapBuffers(window);
- fps();
}
-void GLFWView::fps() {
- static int frames = 0;
- static double timeElapsed = 0;
-
+void GLFWView::report(float duration) {
frames++;
- double currentTime = glfwGetTime();
+ frameTime += duration;
- if (currentTime - timeElapsed >= 1) {
- mbgl::Log::Info(mbgl::Event::OpenGL, "FPS: %4.2f", frames / (currentTime - timeElapsed));
- timeElapsed = currentTime;
+ const double currentTime = glfwGetTime();
+ if (currentTime - lastReported >= 1) {
+ frameTime /= frames;
+ mbgl::Log::Info(mbgl::Event::OpenGL, "Frame time: %6.2fms (%6.2f fps)", frameTime,
+ 1000 / frameTime);
frames = 0;
+ frameTime = 0;
+ lastReported = currentTime;
}
}
diff --git a/platform/default/http_request_curl.cpp b/platform/default/http_request_curl.cpp
index 0f7f8c0ac5..e416034b40 100644
--- a/platform/default/http_request_curl.cpp
+++ b/platform/default/http_request_curl.cpp
@@ -11,12 +11,6 @@
#include <curl/curl.h>
-#ifdef __ANDROID__
-#include <mbgl/android/jni.hpp>
-#include <zip.h>
-#include <openssl/ssl.h>
-#endif
-
#include <queue>
#include <map>
#include <cassert>
@@ -331,104 +325,6 @@ int HTTPCURLContext::startTimeout(CURLM * /* multi */, long timeout_ms, void *us
// -------------------------------------------------------------------------------------------------
-#ifdef __ANDROID__
-
-// This function is called to load the CA bundle
-// from http://curl.haxx.se/libcurl/c/cacertinmem.html¯
-static CURLcode sslctx_function(CURL * /* curl */, void *sslctx, void * /* parm */) {
-
- int error = 0;
- struct zip *apk = zip_open(mbgl::android::apkPath.c_str(), 0, &error);
- if (apk == nullptr) {
- return CURLE_SSL_CACERT_BADFILE;
- }
-
- struct zip_file *apkFile = zip_fopen(apk, "assets/ca-bundle.crt", ZIP_FL_NOCASE);
- if (apkFile == nullptr) {
- zip_close(apk);
- apk = nullptr;
- return CURLE_SSL_CACERT_BADFILE;
- }
-
- struct zip_stat stat;
- if (zip_stat(apk, "assets/ca-bundle.crt", ZIP_FL_NOCASE, &stat) != 0) {
- zip_fclose(apkFile);
- apkFile = nullptr;
- zip_close(apk);
- apk = nullptr;
- return CURLE_SSL_CACERT_BADFILE;
- }
-
- if (stat.size > std::numeric_limits<int>::max()) {
- zip_fclose(apkFile);
- apkFile = nullptr;
- zip_close(apk);
- apk = nullptr;
- return CURLE_SSL_CACERT_BADFILE;
- }
-
- const auto pem = std::make_unique<char[]>(stat.size);
-
- if (static_cast<zip_uint64_t>(zip_fread(apkFile, reinterpret_cast<void *>(pem.get()), stat.size)) != stat.size) {
- zip_fclose(apkFile);
- apkFile = nullptr;
- zip_close(apk);
- apk = nullptr;
- return CURLE_SSL_CACERT_BADFILE;
- }
-
- // get a pointer to the X509 certificate store (which may be empty!)
- X509_STORE *store = SSL_CTX_get_cert_store((SSL_CTX *)sslctx);
- if (store == nullptr) {
- return CURLE_SSL_CACERT_BADFILE;
- }
-
- // get a BIO
- BIO *bio = BIO_new_mem_buf(pem.get(), static_cast<int>(stat.size));
- if (bio == nullptr) {
- store = nullptr;
- return CURLE_SSL_CACERT_BADFILE;
- }
-
- // use it to read the PEM formatted certificate from memory into an X509
- // structure that SSL can use
- X509 *cert = nullptr;
- while (PEM_read_bio_X509(bio, &cert, 0, nullptr) != nullptr) {
- if (cert == nullptr) {
- BIO_free(bio);
- bio = nullptr;
- store = nullptr;
- return CURLE_SSL_CACERT_BADFILE;
- }
-
- // add our certificate to this store
- if (X509_STORE_add_cert(store, cert) == 0) {
- X509_free(cert);
- cert = nullptr;
- BIO_free(bio);
- bio = nullptr;
- store = nullptr;
- return CURLE_SSL_CACERT_BADFILE;
- }
-
- X509_free(cert);
- cert = nullptr;
- }
-
- // decrease reference counts
- BIO_free(bio);
- bio = nullptr;
-
- zip_fclose(apkFile);
- apkFile = nullptr;
- zip_close(apk);
- apk = nullptr;
-
- // all set to go
- return CURLE_OK;
-}
-#endif
-
HTTPCURLRequest::HTTPCURLRequest(HTTPCURLContext* context_, const Resource& resource_, Callback callback_, uv_loop_t*, std::shared_ptr<const Response> response_)
: HTTPRequestBase(resource_, callback_),
context(context_),
@@ -458,12 +354,7 @@ HTTPCURLRequest::HTTPCURLRequest(HTTPCURLContext* context_, const Resource& reso
handleError(curl_easy_setopt(handle, CURLOPT_PRIVATE, this));
handleError(curl_easy_setopt(handle, CURLOPT_ERRORBUFFER, error));
-#ifdef __ANDROID__
- handleError(curl_easy_setopt(handle, CURLOPT_SSLCERTTYPE, "PEM"));
- handleError(curl_easy_setopt(handle, CURLOPT_SSL_CTX_FUNCTION, sslctx_function));
-#else
handleError(curl_easy_setopt(handle, CURLOPT_CAINFO, "ca-bundle.crt"));
-#endif
handleError(curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1));
handleError(curl_easy_setopt(handle, CURLOPT_URL, resource.url.c_str()));
handleError(curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, writeCallback));
diff --git a/platform/default/thread.cpp b/platform/default/thread.cpp
index c0a1069b9c..12d1802c2f 100644
--- a/platform/default/thread.cpp
+++ b/platform/default/thread.cpp
@@ -1,11 +1,23 @@
#include <mbgl/platform/platform.hpp>
+#include <mbgl/platform/log.hpp>
+
+#include <pthread.h>
+#include <sched.h>
+
namespace mbgl {
namespace platform {
void makeThreadLowPriority() {
- // no-op
+#ifdef SCHED_IDLE
+ struct sched_param param;
+ param.sched_priority = 0;
+ int status = sched_setscheduler(0, SCHED_IDLE, &param);
+ if (status != 0) {
+ Log::Warning(Event::General, "Couldn't set thread scheduling policy");
+ }
+#endif
}
-}
-}
+} // namespace platform
+} // namespace mbgl
diff --git a/platform/ios/MGLMapView.mm b/platform/ios/MGLMapView.mm
index 81338f2889..8950663551 100644
--- a/platform/ios/MGLMapView.mm
+++ b/platform/ios/MGLMapView.mm
@@ -737,6 +737,7 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration)
{
[self updateHeadingForDeviceOrientation];
[self updateCompass];
+ [self updateUserLocationAnnotationView];
}
}
@@ -2046,7 +2047,9 @@ CLLocationCoordinate2D MGLLocationCoordinate2DFromLatLng(mbgl::LatLng latLng)
self.selectedAnnotation = annotation;
- if (annotation.title && [self.delegate respondsToSelector:@selector(mapView:annotationCanShowCallout:)] &&
+ if ([annotation respondsToSelector:@selector(title)] &&
+ annotation.title &&
+ [self.delegate respondsToSelector:@selector(mapView:annotationCanShowCallout:)] &&
[self.delegate mapView:self annotationCanShowCallout:annotation])
{
// build the callout
diff --git a/platform/ios/MGLUserLocationAnnotationView.m b/platform/ios/MGLUserLocationAnnotationView.m
index 67f360f68a..536326d1f6 100644
--- a/platform/ios/MGLUserLocationAnnotationView.m
+++ b/platform/ios/MGLUserLocationAnnotationView.m
@@ -5,7 +5,8 @@
#import "MGLAnnotation.h"
#import "MGLMapView.h"
-const CGFloat MGLTrackingDotRingWidth = 24.0;
+const CGFloat MGLUserLocationAnnotationDotSize = 22.0;
+const CGFloat MGLUserLocationAnnotationHaloSize = 115.0;
@interface MGLUserLocationAnnotationView ()
@@ -17,9 +18,15 @@ const CGFloat MGLTrackingDotRingWidth = 24.0;
@implementation MGLUserLocationAnnotationView
{
+ CALayer *_headingIndicatorLayer;
+ CAShapeLayer *_headingIndicatorMaskLayer;
CALayer *_accuracyRingLayer;
CALayer *_dotBorderLayer;
CALayer *_dotLayer;
+
+ double _oldHeadingAccuracy;
+ CLLocationAccuracy _oldHorizontalAccuracy;
+ double _oldZoom;
}
- (instancetype)initWithFrame:(CGRect)frame
@@ -30,7 +37,7 @@ const CGFloat MGLTrackingDotRingWidth = 24.0;
- (instancetype)initInMapView:(MGLMapView *)mapView
{
- if (self = [super initWithFrame:CGRectMake(0, 0, MGLTrackingDotRingWidth, MGLTrackingDotRingWidth)])
+ if (self = [super initWithFrame:CGRectMake(0, 0, MGLUserLocationAnnotationDotSize, MGLUserLocationAnnotationDotSize)])
{
self.annotation = [[MGLUserLocation alloc] initWithMapView:mapView];
_mapView = mapView;
@@ -48,164 +55,279 @@ const CGFloat MGLTrackingDotRingWidth = 24.0;
- (void)setTintColor:(UIColor *)tintColor
{
- UIImage *trackingDotHaloImage = [self trackingDotHaloImage];
- _haloLayer.bounds = CGRectMake(0, 0, trackingDotHaloImage.size.width, trackingDotHaloImage.size.height);
- _haloLayer.contents = (__bridge id)[trackingDotHaloImage CGImage];
+ if (_accuracyRingLayer)
+ {
+ _accuracyRingLayer.backgroundColor = [tintColor CGColor];
+ }
- UIImage *dotImage = [self dotImage];
- _dotLayer.bounds = CGRectMake(0, 0, dotImage.size.width, dotImage.size.height);
- _dotLayer.contents = (__bridge id)[dotImage CGImage];
+ _haloLayer.backgroundColor = [tintColor CGColor];
+ _dotLayer.backgroundColor = [tintColor CGColor];
+
+ _headingIndicatorLayer.contents = (__bridge id)[[self headingIndicatorTintedGradientImage] CGImage];
}
- (void)setupLayers
{
if (CLLocationCoordinate2DIsValid(self.annotation.coordinate))
{
- if ( ! _accuracyRingLayer && self.annotation.location.horizontalAccuracy)
+ // update heading indicator
+ //
+ if (_headingIndicatorLayer)
{
- UIImage *accuracyRingImage = [self accuracyRingImage];
- _accuracyRingLayer = [CALayer layer];
- _haloLayer.bounds = CGRectMake(0, 0, accuracyRingImage.size.width, accuracyRingImage.size.height);
- _haloLayer.contents = (__bridge id)[accuracyRingImage CGImage];
- _haloLayer.position = CGPointMake(super.layer.bounds.size.width / 2.0, super.layer.bounds.size.height / 2.0);
+ _headingIndicatorLayer.hidden = (_mapView.userTrackingMode == MGLUserTrackingModeFollowWithHeading) ? NO : YES;
- [self.layer addSublayer:_accuracyRingLayer];
+ if (_oldHeadingAccuracy != self.annotation.heading.headingAccuracy)
+ {
+ // recalculate the clipping mask based on updated accuracy
+ _headingIndicatorMaskLayer.path = [[self headingIndicatorClippingMask] CGPath];
+
+ _oldHeadingAccuracy = self.annotation.heading.headingAccuracy;
+ }
}
- if ( ! _haloLayer)
+ // heading indicator (tinted, semi-circle)
+ //
+ if ( ! _headingIndicatorLayer && self.annotation.heading.headingAccuracy)
{
- UIImage *haloImage = [self trackingDotHaloImage];
- _haloLayer = [CALayer layer];
- _haloLayer.bounds = CGRectMake(0, 0, haloImage.size.width, haloImage.size.height);
- _haloLayer.contents = (__bridge id)[haloImage CGImage];
- _haloLayer.position = CGPointMake(super.layer.bounds.size.width / 2.0, super.layer.bounds.size.height / 2.0);
-
- [CATransaction begin];
+ CGFloat headingIndicatorSize = MGLUserLocationAnnotationHaloSize;
- [CATransaction setAnimationDuration:3.5];
- [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
+ _headingIndicatorLayer = [CALayer layer];
+ _headingIndicatorLayer.bounds = CGRectMake(0, 0, headingIndicatorSize, headingIndicatorSize);
+ _headingIndicatorLayer.position = CGPointMake(super.bounds.size.width / 2.0, super.bounds.size.height / 2.0);
+ _headingIndicatorLayer.contents = (__bridge id)[[self headingIndicatorTintedGradientImage] CGImage];
+ _headingIndicatorLayer.contentsGravity = kCAGravityBottom;
+ _headingIndicatorLayer.contentsScale = [UIScreen mainScreen].scale;
+ _headingIndicatorLayer.opacity = 0.4;
+ _headingIndicatorLayer.shouldRasterize = YES;
+ _headingIndicatorLayer.rasterizationScale = [UIScreen mainScreen].scale;
+ _headingIndicatorLayer.drawsAsynchronously = YES;
- // scale out radially
- //
- CABasicAnimation *boundsAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
- boundsAnimation.repeatCount = MAXFLOAT;
- boundsAnimation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)];
- boundsAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(2.0, 2.0, 1.0)];
- boundsAnimation.removedOnCompletion = NO;
-
- [_haloLayer addAnimation:boundsAnimation forKey:@"animateScale"];
+ [self.layer insertSublayer:_headingIndicatorLayer below:_dotBorderLayer];
+ }
+
+ // heading indicator accuracy mask (fan-shaped)
+ //
+ if ( ! _headingIndicatorMaskLayer && self.annotation.heading.headingAccuracy)
+ {
+ _headingIndicatorMaskLayer = [CAShapeLayer layer];
+ _headingIndicatorMaskLayer.frame = _headingIndicatorLayer.bounds;
+ _headingIndicatorMaskLayer.path = [[self headingIndicatorClippingMask] CGPath];
+
+ // apply the mask to the halo-radius-sized gradient layer
+ _headingIndicatorLayer.mask = _headingIndicatorMaskLayer;
- // go transparent as scaled out
- //
- CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
- opacityAnimation.repeatCount = MAXFLOAT;
- opacityAnimation.fromValue = [NSNumber numberWithFloat:1.0];
- opacityAnimation.toValue = [NSNumber numberWithFloat:-1.0];
- opacityAnimation.removedOnCompletion = NO;
+ _oldHeadingAccuracy = self.annotation.heading.headingAccuracy;
+ }
+
+ // update accuracy ring (if zoom or horizontal accuracy have changed)
+ //
+ if (_accuracyRingLayer && (_oldZoom != self.mapView.zoomLevel || _oldHorizontalAccuracy != self.annotation.location.horizontalAccuracy))
+ {
+ CGFloat accuracyRingSize = [self calculateAccuracyRingSize];
- [_haloLayer addAnimation:opacityAnimation forKey:@"animateOpacity"];
+ // only show the accuracy ring if it won't be obscured by the location dot
+ if (accuracyRingSize > MGLUserLocationAnnotationDotSize + 15)
+ {
+ _accuracyRingLayer.hidden = NO;
+ _accuracyRingLayer.bounds = CGRectMake(0, 0, accuracyRingSize, accuracyRingSize);
+ _accuracyRingLayer.cornerRadius = accuracyRingSize / 2;
+
+ // match the halo to the accuracy ring
+ _haloLayer.bounds = _accuracyRingLayer.bounds;
+ _haloLayer.cornerRadius = _accuracyRingLayer.cornerRadius;
+ _haloLayer.shouldRasterize = NO;
+ }
+ else
+ {
+ _accuracyRingLayer.hidden = YES;
+
+ _haloLayer.bounds = CGRectMake(0, 0, MGLUserLocationAnnotationHaloSize, MGLUserLocationAnnotationHaloSize);
+ _haloLayer.cornerRadius = MGLUserLocationAnnotationHaloSize / 2.0;
+ _haloLayer.shouldRasterize = YES;
+ _haloLayer.rasterizationScale = [UIScreen mainScreen].scale;
+ }
- [CATransaction commit];
+ // store accuracy and zoom so we're not redrawing unchanged location updates
+ _oldHorizontalAccuracy = self.annotation.location.horizontalAccuracy;
+ _oldZoom = self.mapView.zoomLevel;
+ }
+
+ // accuracy ring (circular, tinted, mostly-transparent)
+ //
+ if ( ! _accuracyRingLayer && self.annotation.location.horizontalAccuracy)
+ {
+ CGFloat accuracyRingSize = [self calculateAccuracyRingSize];
+ _accuracyRingLayer = [self circleLayerWithSize:accuracyRingSize];
+ _accuracyRingLayer.backgroundColor = [_mapView.tintColor CGColor];
+ _accuracyRingLayer.opacity = 0.1;
+ _accuracyRingLayer.shouldRasterize = NO;
+ _accuracyRingLayer.allowsGroupOpacity = NO;
- [self.layer addSublayer:_haloLayer];
+ [self.layer addSublayer:_accuracyRingLayer];
}
- // white dot background with shadow
+ // expanding sonar-like pulse (circular, tinted, fades out)
//
- if ( ! _dotBorderLayer)
+ if ( ! _haloLayer)
{
- CGRect rect = CGRectMake(0, 0, MGLTrackingDotRingWidth * 1.5, MGLTrackingDotRingWidth * 1.5);
+ _haloLayer = [self circleLayerWithSize:MGLUserLocationAnnotationHaloSize];
+ _haloLayer.backgroundColor = [_mapView.tintColor CGColor];
+ _haloLayer.allowsGroupOpacity = NO;
- UIGraphicsBeginImageContextWithOptions(rect.size, NO, [[UIScreen mainScreen] scale]);
- CGContextRef context = UIGraphicsGetCurrentContext();
+ // set defaults for the animations
+ CAAnimationGroup *animationGroup = [self loopingAnimationGroupWithDuration:3.0];
- CGContextSetShadow(context, CGSizeMake(0, 0), MGLTrackingDotRingWidth / 4.0);
+ // scale out radially with initial acceleration
+ CAKeyframeAnimation *boundsAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale.xy"];
+ boundsAnimation.values = @[@0, @0.35, @1];
+ boundsAnimation.keyTimes = @[@0, @0.2, @1];
- CGContextSetFillColorWithColor(context, [[UIColor whiteColor] CGColor]);
- CGContextFillEllipseInRect(context, CGRectMake((rect.size.width - MGLTrackingDotRingWidth) / 2.0, (rect.size.height - MGLTrackingDotRingWidth) / 2.0, MGLTrackingDotRingWidth, MGLTrackingDotRingWidth));
+ // go transparent as scaled out, start semi-opaque
+ CAKeyframeAnimation *opacityAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
+ opacityAnimation.values = @[@0.4, @0.4, @0];
+ opacityAnimation.keyTimes = @[@0, @0.2, @1];
- UIImage *whiteBackground = UIGraphicsGetImageFromCurrentImageContext();
+ animationGroup.animations = @[boundsAnimation, opacityAnimation];
- UIGraphicsEndImageContext();
+ [_haloLayer addAnimation:animationGroup forKey:@"animateTransformAndOpacity"];
+
+ [self.layer addSublayer:_haloLayer];
+ }
+
+ // background dot (white with black shadow)
+ //
+ if ( ! _dotBorderLayer)
+ {
+ _dotBorderLayer = [self circleLayerWithSize:MGLUserLocationAnnotationDotSize];
+ _dotBorderLayer.backgroundColor = [[UIColor whiteColor] CGColor];
+ _dotBorderLayer.shadowColor = [[UIColor blackColor] CGColor];
+ _dotBorderLayer.shadowOffset = CGSizeMake(0, 0);
+ _dotBorderLayer.shadowRadius = 3;
+ _dotBorderLayer.shadowOpacity = 0.25;
- _dotBorderLayer = [CALayer layer];
- _dotBorderLayer.bounds = CGRectMake(0, 0, whiteBackground.size.width, whiteBackground.size.height);
- _dotBorderLayer.contents = (__bridge id)[whiteBackground CGImage];
- _dotBorderLayer.position = CGPointMake(super.layer.bounds.size.width / 2.0, super.layer.bounds.size.height / 2.0);
[self.layer addSublayer:_dotBorderLayer];
}
-
- // pulsing, tinted dot sublayer
+
+ // inner dot (pulsing, tinted)
//
if ( ! _dotLayer)
{
- UIImage *dotImage = [self dotImage];
- _dotLayer = [CALayer layer];
- _dotLayer.bounds = CGRectMake(0, 0, dotImage.size.width, dotImage.size.height);
- _dotLayer.contents = (__bridge id)[dotImage CGImage];
- _dotLayer.position = CGPointMake(super.layer.bounds.size.width / 2.0, super.layer.bounds.size.height / 2.0);
-
- CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform"];
- animation.repeatCount = MAXFLOAT;
- animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.0, 1.0, 1.0)];
- animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.8, 0.8, 1.0)];
- animation.removedOnCompletion = NO;
- animation.autoreverses = YES;
- animation.duration = 1.5;
- animation.beginTime = CACurrentMediaTime() + 1.0;
- animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
-
- [_dotLayer addAnimation:animation forKey:@"animateTransform"];
+ _dotLayer = [self circleLayerWithSize:MGLUserLocationAnnotationDotSize * 0.75];
+ _dotLayer.backgroundColor = [_mapView.tintColor CGColor];
+ _dotLayer.shouldRasterize = NO;
+
+ // set defaults for the animations
+ CAAnimationGroup *animationGroup = [self loopingAnimationGroupWithDuration:1.5];
+ animationGroup.autoreverses = YES;
+ animationGroup.fillMode = kCAFillModeBoth;
+
+ // scale the dot up and down
+ CABasicAnimation *pulseAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale.xy"];
+ pulseAnimation.fromValue = @0.8;
+ pulseAnimation.toValue = @1;
+
+ // fade opacity in and out, subtly
+ CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
+ opacityAnimation.fromValue = @0.8;
+ opacityAnimation.toValue = @1;
+
+ animationGroup.animations = @[pulseAnimation, opacityAnimation];
+
+ [_dotLayer addAnimation:animationGroup forKey:@"animateTransformAndOpacity"];
[self.layer addSublayer:_dotLayer];
}
}
}
-- (UIImage *)accuracyRingImage
+- (CALayer *)circleLayerWithSize:(CGFloat)layerSize
{
- CGFloat latRadians = self.annotation.coordinate.latitude * M_PI / 180.0f;
- CGFloat pixelRadius = self.annotation.location.horizontalAccuracy / cos(latRadians) / [self.mapView metersPerPixelAtLatitude:self.annotation.coordinate.latitude];
- UIGraphicsBeginImageContextWithOptions(CGSizeMake(pixelRadius * 2, pixelRadius * 2), NO, [[UIScreen mainScreen] scale]);
+ CALayer *circleLayer = [CALayer layer];
+ circleLayer.bounds = CGRectMake(0, 0, layerSize, layerSize);
+ circleLayer.position = CGPointMake(super.bounds.size.width / 2.0, super.bounds.size.height / 2.0);
+ circleLayer.cornerRadius = layerSize / 2.0;
+ circleLayer.shouldRasterize = YES;
+ circleLayer.rasterizationScale = [UIScreen mainScreen].scale;
+ circleLayer.drawsAsynchronously = YES;
- CGContextSetStrokeColorWithColor(UIGraphicsGetCurrentContext(), [[UIColor colorWithRed:0.378 green:0.552 blue:0.827 alpha:0.7] CGColor]);
- CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), [[UIColor colorWithRed:0.378 green:0.552 blue:0.827 alpha:0.15] CGColor]);
- CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 2.0);
- CGContextStrokeEllipseInRect(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, pixelRadius * 2, pixelRadius * 2));
+ return circleLayer;
+}
+
+- (CAAnimationGroup *)loopingAnimationGroupWithDuration:(CGFloat)animationDuration
+{
+ CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
+ animationGroup.duration = animationDuration;
+ animationGroup.repeatCount = INFINITY;
+ animationGroup.removedOnCompletion = NO;
+ animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
- UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext();
- UIGraphicsEndImageContext();
- return finalImage;
+ return animationGroup;
}
-- (UIImage *)trackingDotHaloImage
+- (CGFloat)calculateAccuracyRingSize
{
- UIGraphicsBeginImageContextWithOptions(CGSizeMake(100, 100), NO, [[UIScreen mainScreen] scale]);
- CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), [[_mapView.tintColor colorWithAlphaComponent:0.75] CGColor]);
- CGContextFillEllipseInRect(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, 100, 100));
- UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext();
- UIGraphicsEndImageContext();
+ CGFloat latRadians = self.annotation.coordinate.latitude * M_PI / 180.0f;
+ CGFloat pixelRadius = self.annotation.location.horizontalAccuracy / cos(latRadians) / [self.mapView metersPerPixelAtLatitude:self.annotation.coordinate.latitude];
- return finalImage;
+ return pixelRadius * 2;
}
-- (UIImage *)dotImage
+- (UIImage *)headingIndicatorTintedGradientImage
{
- CGFloat tintedWidth = MGLTrackingDotRingWidth * 0.7;
+ UIImage *image;
- CGRect rect = CGRectMake(0, 0, tintedWidth, tintedWidth);
+ CGFloat haloRadius = MGLUserLocationAnnotationHaloSize / 2.0;
- UIGraphicsBeginImageContextWithOptions(rect.size, NO, [[UIScreen mainScreen] scale]);
+ UIGraphicsBeginImageContextWithOptions(CGSizeMake(MGLUserLocationAnnotationHaloSize, haloRadius), NO, 0);
+
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = UIGraphicsGetCurrentContext();
- CGContextSetFillColorWithColor(context, [_mapView.tintColor CGColor]);
- CGContextFillEllipseInRect(context, CGRectMake((rect.size.width - tintedWidth) / 2.0, (rect.size.height - tintedWidth) / 2.0, tintedWidth, tintedWidth));
+ // gradient from the tint color to no-alpha tint color
+ CGFloat gradientLocations[] = {0.0, 1.0};
+ CGGradientRef gradient = CGGradientCreateWithColors(
+ colorSpace, (__bridge CFArrayRef)@[(id)[_mapView.tintColor CGColor],
+ (id)[[_mapView.tintColor colorWithAlphaComponent:0] CGColor]], gradientLocations);
- UIImage *tintedForeground = UIGraphicsGetImageFromCurrentImageContext();
+ // draw the gradient from the center point to the edge (full halo radius)
+ CGPoint centerPoint = CGPointMake(haloRadius, haloRadius);
+ CGContextDrawRadialGradient(context, gradient,
+ centerPoint, 0.0,
+ centerPoint, haloRadius,
+ nil);
+ image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
- return tintedForeground;
+ CGGradientRelease(gradient);
+ CGColorSpaceRelease(colorSpace);
+
+ return image;
+}
+
+- (UIBezierPath *)headingIndicatorClippingMask
+{
+ CGFloat accuracy = self.annotation.heading.headingAccuracy;
+
+ // size the mask using exagerated accuracy, but keep within a good display range
+ CGFloat clippingDegrees = 90 - (accuracy * 1.5);
+ clippingDegrees = fmin(clippingDegrees, 55);
+ clippingDegrees = fmax(clippingDegrees, 10);
+
+ CGRect ovalRect = CGRectMake(0, 0, MGLUserLocationAnnotationHaloSize, MGLUserLocationAnnotationHaloSize);
+ UIBezierPath *ovalPath = UIBezierPath.bezierPath;
+
+ // clip the oval to ± incoming accuracy degrees (converted to radians), from the top
+ [ovalPath addArcWithCenter:CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect))
+ radius:CGRectGetWidth(ovalRect) / 2.0
+ startAngle:(-180 + clippingDegrees) * M_PI / 180
+ endAngle:-clippingDegrees * M_PI / 180
+ clockwise:YES];
+
+ [ovalPath addLineToPoint:CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect))];
+ [ovalPath closePath];
+
+ return ovalPath;
}
@end