From 08eb760acdf88692139cc9d777ed95074662387e Mon Sep 17 00:00:00 2001 From: Adrian Cojocaru Date: Tue, 27 May 2025 16:15:56 +0300 Subject: [PATCH] Add action journal (#3409) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bart Louwers --- .github/workflows/android-ci.yml | 2 +- .gitmodules | 3 + BUILD.bazel | 3 + CMakeLists.txt | 10 +- bazel/core.bzl | 10 + include/mbgl/map/map.hpp | 14 +- include/mbgl/util/action_journal.hpp | 68 +++ include/mbgl/util/action_journal_options.hpp | 101 +++ include/mbgl/util/chrono.hpp | 3 + .../MapLibreAndroid/src/cpp/CMakeLists.txt | 2 + .../MapLibreAndroid/src/cpp/jni_native.cpp | 2 + .../src/cpp/native_map_options.cpp | 43 ++ .../src/cpp/native_map_options.hpp | 25 + .../src/cpp/native_map_view.cpp | 67 +- .../src/cpp/native_map_view.hpp | 10 +- .../maplibre/android/maps/MapLibreMap.java | 43 ++ .../android/maps/MapLibreMapOptions.java | 144 +++++ .../org/maplibre/android/maps/MapView.java | 8 +- .../org/maplibre/android/maps/NativeMap.java | 6 + .../android/maps/NativeMapOptions.java | 56 ++ .../maplibre/android/maps/NativeMapView.java | 54 +- .../src/main/res-public/values/public.xml | 5 + .../src/main/res/values/attrs.xml | 5 + .../java/org/maplibre/android/MapLibreTest.kt | 3 + .../maplibre/android/maps/BaseLayerTest.kt | 2 +- .../android/maps/NativeMapViewTest.kt | 4 +- .../activity/events/ObserverActivity.kt | 51 +- .../main/res/layout/activity_map_events.xml | 29 + .../docs/observability/action-journal.md | 80 +++ .../docs/observability/observe-map-events.md | 21 + platform/darwin/bazel/files.bzl | 5 + platform/darwin/src/MLNActionJournalOptions.h | 35 ++ .../darwin/src/MLNActionJournalOptions.mm | 54 ++ .../src/MLNActionJournalOptions_Private.h | 12 + platform/darwin/src/MLNMapOptions.h | 37 ++ platform/darwin/src/MLNMapOptions.mm | 23 + platform/ios/BUILD.bazel | 1 + .../ios/MapLibre.docc/ActionJournalExample.md | 84 +++ platform/ios/MapLibre.docc/MapLibre.md | 6 +- .../app-swift/Sources/ObserverExample.swift | 26 +- platform/ios/ios.cmake | 6 + platform/ios/src/MLNMapView.h | 45 ++ platform/ios/src/MLNMapView.mm | 93 ++- platform/macos/src/MLNMapView.h | 22 + platform/macos/src/MLNMapView.mm | 67 +- platform/macos/src/MLNMapViewDelegate.h | 1 + shaders/generate_shader_code.mjs | 18 + src/mbgl/map/map.cpp | 30 +- src/mbgl/map/map_impl.cpp | 131 +++- src/mbgl/map/map_impl.hpp | 12 +- src/mbgl/map/transform.cpp | 2 +- src/mbgl/map/transform.hpp | 20 +- src/mbgl/shaders/shader_source.cpp | 44 ++ src/mbgl/tile/tile_operation.cpp | 18 + src/mbgl/util/action_journal.cpp | 33 + src/mbgl/util/action_journal_impl.cpp | 575 ++++++++++++++++++ src/mbgl/util/action_journal_impl.hpp | 105 ++++ src/mbgl/util/chrono.cpp | 16 + test/CMakeLists.txt | 1 + test/map/transform.test.cpp | 6 +- test/tile/tile_coordinate.test.cpp | 2 +- test/util/action_journal.test.cpp | 431 +++++++++++++ vendor/BUILD.bazel | 2 + vendor/maplibre-native-base.cmake | 9 + .../extras/CMakeLists.txt | 1 + vendor/maplibre-native-base/extras/filesystem | 1 + 66 files changed, 2773 insertions(+), 75 deletions(-) create mode 100644 include/mbgl/util/action_journal.hpp create mode 100644 include/mbgl/util/action_journal_options.hpp create mode 100644 platform/android/MapLibreAndroid/src/cpp/native_map_options.cpp create mode 100644 platform/android/MapLibreAndroid/src/cpp/native_map_options.hpp create mode 100644 platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapOptions.java create mode 100644 platform/android/MapLibreAndroidTestApp/src/main/res/layout/activity_map_events.xml create mode 100644 platform/android/docs/observability/action-journal.md create mode 100644 platform/android/docs/observability/observe-map-events.md create mode 100644 platform/darwin/src/MLNActionJournalOptions.h create mode 100644 platform/darwin/src/MLNActionJournalOptions.mm create mode 100644 platform/darwin/src/MLNActionJournalOptions_Private.h create mode 100644 platform/darwin/src/MLNMapOptions.h create mode 100644 platform/darwin/src/MLNMapOptions.mm create mode 100644 platform/ios/MapLibre.docc/ActionJournalExample.md create mode 100644 src/mbgl/shaders/shader_source.cpp create mode 100644 src/mbgl/tile/tile_operation.cpp create mode 100644 src/mbgl/util/action_journal.cpp create mode 100644 src/mbgl/util/action_journal_impl.cpp create mode 100644 src/mbgl/util/action_journal_impl.hpp create mode 100644 test/util/action_journal.test.cpp create mode 160000 vendor/maplibre-native-base/extras/filesystem diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml index c56d6c372..6dedab7a5 100644 --- a/.github/workflows/android-ci.yml +++ b/.github/workflows/android-ci.yml @@ -53,7 +53,7 @@ jobs: if: startsWith(runner.name, 'GitHub Actions') uses: jlumbroso/free-disk-space@main with: - tool-cache: false + tool-cache: true android: false dotnet: true haskell: true diff --git a/.gitmodules b/.gitmodules index fc2786757..7877024b0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -97,3 +97,6 @@ [submodule "vendor/maplibre-native-base/deps/geojson.hpp"] path = vendor/maplibre-native-base/deps/geojson.hpp url = https://github.com/mapbox/geojson-cpp.git +[submodule "vendor/maplibre-native-base/extras/filesystem"] + path = vendor/maplibre-native-base/extras/filesystem + url = https://github.com/gulrak/filesystem.git diff --git a/BUILD.bazel b/BUILD.bazel index cf9f9e3ae..8ec90d818 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -14,6 +14,7 @@ load( "MLN_DRAWABLES_SOURCE", "MLN_GENERATED_OPENGL_SHADER_HEADERS", "MLN_GENERATED_SHADER_HEADERS", + "MLN_GENERATED_SHADER_SOURCE", "MLN_GENERATED_STYLE_SOURCE", "MLN_OPENGL_HEADERS", "MLN_OPENGL_SOURCE", @@ -43,6 +44,7 @@ js_run_binary( allow_empty = False, ), outs = MLN_GENERATED_SHADER_HEADERS + + MLN_GENERATED_SHADER_SOURCE + MLN_GENERATED_OPENGL_SHADER_HEADERS, tool = ":generate-shader-code-script", ) @@ -75,6 +77,7 @@ cc_library( cc_library( name = "mbgl-core", srcs = MLN_CORE_SOURCE + + MLN_GENERATED_SHADER_SOURCE + MLN_GENERATED_STYLE_SOURCE + select({ ":drawable_renderer": MLN_OPENGL_SOURCE + MLN_DRAWABLES_SOURCE + MLN_DRAWABLES_GL_SOURCE, ":metal_renderer": MLN_DRAWABLES_SOURCE + MLN_DRAWABLES_MTL_SOURCE, diff --git a/CMakeLists.txt b/CMakeLists.txt index cb25edf48..a776d95ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -279,6 +279,7 @@ list(APPEND INCLUDE_FILES ${PROJECT_SOURCE_DIR}/include/mbgl/renderer/renderer_frontend.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/renderer/renderer_observer.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/renderer/renderer_state.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/shader_source.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/program_parameters.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/storage/database_file_source.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/storage/file_source.hpp @@ -386,6 +387,8 @@ list(APPEND INCLUDE_FILES ${PROJECT_SOURCE_DIR}/include/mbgl/tile/tile_id.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/tile/tile_operation.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/tile/tile_necessity.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/util/action_journal.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/util/action_journal_options.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/util/async_request.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/util/async_task.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/util/char_array_buffer.hpp @@ -645,6 +648,7 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/transition_parameters.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/update_parameters.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/renderer/upload_parameters.hpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/shader_source.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/attributes.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/program_parameters.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/segment.hpp @@ -881,10 +885,14 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/tile/tile_loader_impl.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/tile/tile_loader_observer.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/tile/tile_observer.hpp + ${PROJECT_SOURCE_DIR}/src/mbgl/tile/tile_operation.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/tile/vector_tile.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/tile/vector_tile.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/tile/vector_tile_data.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/tile/vector_tile_data.hpp + ${PROJECT_SOURCE_DIR}/src/mbgl/util/action_journal.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/util/action_journal_impl.hpp + ${PROJECT_SOURCE_DIR}/src/mbgl/util/action_journal_impl.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/util/camera.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/util/bounding_volumes.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/util/bounding_volumes.cpp @@ -972,7 +980,6 @@ if(MLN_WITH_OPENGL) ${PROJECT_SOURCE_DIR}/include/mbgl/gl/renderable_resource.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/gl/renderer_backend.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/platform/gl_functions.hpp - ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/shader_source.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/shader_manifest.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/gl/shader_group_gl.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/gl/prelude.hpp @@ -1425,6 +1432,7 @@ set(EXPORT_TARGETS mbgl-core maplibre-native-base maplibre-native-base-cheap-ruler-cpp + maplibre-native-base-extras-filesystem maplibre-native-base-extras-expected-lite maplibre-native-base-extras-kdbush.hpp maplibre-native-base-extras-rapidjson diff --git a/bazel/core.bzl b/bazel/core.bzl index 485dc8dbd..479b94841 100644 --- a/bazel/core.bzl +++ b/bazel/core.bzl @@ -54,6 +54,10 @@ MLN_GENERATED_SHADER_HEADERS = [ "include/mbgl/shaders/shader_manifest.hpp", ] +MLN_GENERATED_SHADER_SOURCE = [ + "src/mbgl/shaders/shader_source.cpp", +] + MLN_GENERATED_OPENGL_SHADER_HEADERS = [ "include/mbgl/shaders/gl/background.hpp", "include/mbgl/shaders/gl/background_pattern.hpp", @@ -498,10 +502,14 @@ MLN_CORE_SOURCE = [ "src/mbgl/tile/tile_loader_impl.hpp", "src/mbgl/tile/tile_loader_observer.hpp", "src/mbgl/tile/tile_observer.hpp", + "src/mbgl/tile/tile_operation.cpp", "src/mbgl/tile/vector_tile.cpp", "src/mbgl/tile/vector_tile.hpp", "src/mbgl/tile/vector_tile_data.cpp", "src/mbgl/tile/vector_tile_data.hpp", + "src/mbgl/util/action_journal.cpp", + "src/mbgl/util/action_journal_impl.hpp", + "src/mbgl/util/action_journal_impl.cpp", "src/mbgl/util/camera.cpp", "src/mbgl/util/camera.hpp", "src/mbgl/util/bounding_volumes.hpp", @@ -743,6 +751,8 @@ MLN_CORE_HEADERS = [ "include/mbgl/tile/tile_id.hpp", "include/mbgl/tile/tile_operation.hpp", "include/mbgl/tile/tile_necessity.hpp", + "include/mbgl/util/action_journal.hpp", + "include/mbgl/util/action_journal_options.hpp", "include/mbgl/util/async_request.hpp", "include/mbgl/util/async_task.hpp", "include/mbgl/util/bitmask_operations.hpp", diff --git a/include/mbgl/map/map.hpp b/include/mbgl/map/map.hpp index 914560c92..e68853e04 100644 --- a/include/mbgl/map/map.hpp +++ b/include/mbgl/map/map.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -31,13 +32,18 @@ class Image; class Style; } // namespace style +namespace util { +class ActionJournal; +} // namespace util + class Map : private util::noncopyable { public: explicit Map(RendererFrontend&, MapObserver&, const MapOptions&, const ResourceOptions&, - const ClientOptions& = ClientOptions()); + const ClientOptions& = ClientOptions(), + const util::ActionJournalOptions& = util::ActionJournalOptions()); ~Map(); /// Register a callback that will get called (on the render thread) when all @@ -152,12 +158,16 @@ public: void setFreeCameraOptions(const FreeCameraOptions& camera); FreeCameraOptions getFreeCameraOptions() const; + ClientOptions getClientOptions() const; + + const std::unique_ptr& getActionJournal(); + protected: class Impl; const std::unique_ptr impl; // For testing only. - Map(std::unique_ptr); + Map(std::unique_ptr, const util::ActionJournalOptions& = {}); }; } // namespace mbgl diff --git a/include/mbgl/util/action_journal.hpp b/include/mbgl/util/action_journal.hpp new file mode 100644 index 000000000..49dd8ccd2 --- /dev/null +++ b/include/mbgl/util/action_journal.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace mbgl { + +namespace util { + +class ActionJournalOptions; + +/** + * @brief Logs map events in a persistent rolling file format. + * Each event is stored as a serialized json object with the event data. + * Example + * { + * "name" : "onTileAction", + * "time" : "2025-04-17T13:13:13.974Z", + * "styleName" : "Streets", + * "styleURL" : "maptiler://maps/streets", + * "event" : { + * "action" : "RequestedFromNetwork", + * "tileX" : 0, + * "tileY" : 0, + * "tileZ" : 0, + * "overscaledZ" : 0, + * "sourceID" : "openmaptiles" + * } + * } + */ +class ActionJournal { +public: + ActionJournal(const Map& map, const ActionJournalOptions& options); + ~ActionJournal(); + + /** + * @brief Get the action journal umbrella directory containing all logs. + * @return log directory + */ + std::string getLogDirectory() const; + + /** + * @brief Get the action journal log files. + * @return list of log files + */ + std::vector getLogFiles() const; + + /** + * @brief Get the action journal events from oldest to newest. + * Each element contains a serialized json object with the event data. + * @return List of serialized events + */ + std::vector getLog(); + + /** + * @brief Clear stored action journal events by removing logs files. + */ + void clearLog(); + + class Impl; + const std::unique_ptr impl; +}; + +} // namespace util +} // namespace mbgl diff --git a/include/mbgl/util/action_journal_options.hpp b/include/mbgl/util/action_journal_options.hpp new file mode 100644 index 000000000..c666e9de8 --- /dev/null +++ b/include/mbgl/util/action_journal_options.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include + +namespace mbgl { + +namespace util { + +/** + * @brief Holds values for Action Journal options. + */ +class ActionJournalOptions { +public: + ActionJournalOptions() = default; + ~ActionJournalOptions() = default; + + /** + * @brief Enable action journal event logging, defaults to false. + * + * @param value true to enable, false to disable. + * @return ActionJournalOptions for chaining options together. + */ + ActionJournalOptions& enable(bool value = true) { + enable_ = value; + return *this; + } + + /** + * @brief Gets the previously set (or default) value. + * @return Returns ActionJournal state + */ + bool enabled() const { return enable_; } + + /** + * @brief Set the log files location. An umbrela `action_journal` directory + * will be created at the given location containing all files. + * + * @param path Action Journal log path. + * @return ActionJournalOptions for chaining options together. + */ + ActionJournalOptions& withPath(const std::string& path) { + path_ = path; + return *this; + } + + /** + * @brief Gets the previously set (or default) log path. + * @return Returns ActionJournal log path + */ + std::string path() const { return path_; } + + /** + * @brief Set the file size for the log files. The action journal uses a + * rolling log with multiple files. Total log size is equal to + * `logFileSize * logFileCount`. + * + * @param size Action Journal log file size. + * @return ActionJournalOptions for chaining options together. + */ + ActionJournalOptions& withLogFileSize(const uint32_t size) { + logFileSize_ = size; + return *this; + } + + /** + * @brief Gets the previously set (or default) log file size. + * @return Returns ActionJournal log file size + */ + uint32_t logFileSize() const { return logFileSize_; } + + /** + * @brief Set the number of log files. The action journal uses a + * rolling log with multiple files. Total log size is equal to + * `logFileSize * logFileCount`. + * + * @param count Action Journal log file count. + * @return ActionJournalOptions for chaining options together. + */ + ActionJournalOptions& withLogFileCount(const uint32_t count) { + logFileCount_ = count; + return *this; + } + + /** + * @brief Gets the previously set (or default) number of log files. + * @return Returns ActionJournal log file count + */ + uint32_t logFileCount() const { return logFileCount_; } + +protected: + bool enable_ = false; + // path of the log + std::string path_{"/tmp/"}; + // log file size + uint32_t logFileSize_ = 1024 * 1024; + // number of log files (each of `logFileSize` size) + uint32_t logFileCount_ = 5; +}; + +} // namespace util +} // namespace mbgl diff --git a/include/mbgl/util/chrono.hpp b/include/mbgl/util/chrono.hpp index de4f7d7d5..69a13383b 100644 --- a/include/mbgl/util/chrono.hpp +++ b/include/mbgl/util/chrono.hpp @@ -32,6 +32,9 @@ std::string rfc1123(Timestamp); // YYYY-mm-dd HH:MM:SS e.g. "2015-11-26 16:11:23" std::string iso8601(Timestamp); +// YYYY-mm-ddTHH:MM:SS.MS e.g. "2015-11-26T16:11:23.324Z" +std::string iso8601(std::chrono::time_point); + Timestamp parseTimestamp(const char *); Timestamp parseTimestamp(int32_t timestamp); diff --git a/platform/android/MapLibreAndroid/src/cpp/CMakeLists.txt b/platform/android/MapLibreAndroid/src/cpp/CMakeLists.txt index 462b252b4..a1db3493a 100644 --- a/platform/android/MapLibreAndroid/src/cpp/CMakeLists.txt +++ b/platform/android/MapLibreAndroid/src/cpp/CMakeLists.txt @@ -110,6 +110,8 @@ add_library(maplibre SHARED ${PROJECT_SOURCE_DIR}/map_renderer_runnable.hpp ${PROJECT_SOURCE_DIR}/native_map_view.cpp ${PROJECT_SOURCE_DIR}/native_map_view.hpp + ${PROJECT_SOURCE_DIR}/native_map_options.cpp + ${PROJECT_SOURCE_DIR}/native_map_options.hpp ${PROJECT_SOURCE_DIR}/offline/offline_manager.cpp ${PROJECT_SOURCE_DIR}/offline/offline_manager.hpp ${PROJECT_SOURCE_DIR}/offline/offline_region.cpp diff --git a/platform/android/MapLibreAndroid/src/cpp/jni_native.cpp b/platform/android/MapLibreAndroid/src/cpp/jni_native.cpp index ce343c48d..b84fd671c 100644 --- a/platform/android/MapLibreAndroid/src/cpp/jni_native.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/jni_native.cpp @@ -34,6 +34,7 @@ #include "map_renderer_runnable.hpp" #include "maplibre.hpp" #include "native_map_view.hpp" +#include "native_map_options.hpp" #include "util/tile_server_options.hpp" #ifndef MBGL_MODULE_OFFLINE_DISABLE #include "offline/offline_manager.hpp" @@ -109,6 +110,7 @@ void registerNatives(JavaVM* vm) { MapRenderer::registerNative(env); MapRendererRunnable::registerNative(env); NativeMapView::registerNative(env); + NativeMapOptions::registerNative(env); // Http RegisterNativeHTTPRequest(env); diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_options.cpp b/platform/android/MapLibreAndroid/src/cpp/native_map_options.cpp new file mode 100644 index 000000000..42ce9d2b1 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_options.cpp @@ -0,0 +1,43 @@ +#include "native_map_options.hpp" + +namespace mbgl { +namespace android { + +void NativeMapOptions::registerNative(jni::JNIEnv &env) { + jni::Class::Singleton(env); +} + +NativeMapOptions::NativeMapOptions(jni::JNIEnv &_env, const jni::Object &_obj) {} + +NativeMapOptions::~NativeMapOptions() {} + +util::ActionJournalOptions NativeMapOptions::getActionJournalOptions(jni::JNIEnv &env, + const jni::Object &obj) { + auto &javaClass = jni::Class::Singleton(env); + + auto enabledField = javaClass.GetField(env, "actionJournalEnabled"); + auto pathField = javaClass.GetField(env, "actionJournalPath"); + auto logFileSizeField = javaClass.GetField(env, "actionJournalLogFileSize"); + auto logFileCountField = javaClass.GetField(env, "actionJournalLogFileCount"); + + return util::ActionJournalOptions() + .enable(obj.Get(env, enabledField)) + .withPath(jni::Make(env, obj.Get(env, pathField))) + .withLogFileSize(obj.Get(env, logFileSizeField)) + .withLogFileCount(obj.Get(env, logFileCountField)); +} + +float NativeMapOptions::pixelRatio(jni::JNIEnv &env, const jni::Object &obj) { + auto &javaClass = jni::Class::Singleton(env); + auto pixelRatioField = javaClass.GetField(env, "pixelRatio"); + return obj.Get(env, pixelRatioField); +} + +bool NativeMapOptions::crossSourceCollisions(jni::JNIEnv &env, const jni::Object &obj) { + auto &javaClass = jni::Class::Singleton(env); + auto crossSourceCollisionsField = javaClass.GetField(env, "crossSourceCollisions"); + return obj.Get(env, crossSourceCollisionsField); +} + +} // namespace android +} // namespace mbgl diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_options.hpp b/platform/android/MapLibreAndroid/src/cpp/native_map_options.hpp new file mode 100644 index 000000000..b951af617 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_options.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace android { + +class NativeMapOptions { +public: + static constexpr auto Name() { return "org/maplibre/android/maps/NativeMapOptions"; }; + + static void registerNative(jni::JNIEnv&); + + NativeMapOptions(jni::JNIEnv&, const jni::Object&); + virtual ~NativeMapOptions(); + + static util::ActionJournalOptions getActionJournalOptions(jni::JNIEnv&, const jni::Object&); + static float pixelRatio(jni::JNIEnv&, const jni::Object&); + static bool crossSourceCollisions(jni::JNIEnv&, const jni::Object&); +}; + +} // namespace android +} // namespace mbgl diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp index 082443b98..232c2652f 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -62,11 +63,10 @@ NativeMapView::NativeMapView(jni::JNIEnv& _env, const jni::Object& _obj, const jni::Object& jFileSource, const jni::Object& jMapRenderer, - jni::jfloat pixelRatio_, - jni::jboolean crossSourceCollisions_) + const jni::Object& jNativeMapOptions) : javaPeer(_env, _obj), mapRenderer(MapRenderer::getNativePeer(_env, jMapRenderer)), - pixelRatio(pixelRatio_) { + pixelRatio(NativeMapOptions::pixelRatio(_env, jNativeMapOptions)) { // Get a reference to the JavaVM for callbacks if (_env.GetJavaVM(&vm) < 0) { _env.ExceptionDescribe(); @@ -83,14 +83,16 @@ NativeMapView::NativeMapView(jni::JNIEnv& _env, .withPixelRatio(pixelRatio) .withConstrainMode(ConstrainMode::HeightOnly) .withViewportMode(ViewportMode::Default) - .withCrossSourceCollisions(crossSourceCollisions_); + .withCrossSourceCollisions(NativeMapOptions::crossSourceCollisions(_env, jNativeMapOptions)); // Create the core map - map = std::make_unique(*rendererFrontend, - *this, - options, - mbgl::android::FileSource::getSharedResourceOptions(_env, jFileSource), - mbgl::android::FileSource::getSharedClientOptions(_env, jFileSource)); + map = std::make_unique( + *rendererFrontend, + *this, + options, + mbgl::android::FileSource::getSharedResourceOptions(_env, jFileSource), + mbgl::android::FileSource::getSharedClientOptions(_env, jFileSource), + mbgl::android::NativeMapOptions::getActionJournalOptions(_env, jNativeMapOptions)); } /** @@ -693,6 +695,47 @@ jni::jboolean NativeMapView::getDebug(JNIEnv&) { return map->getDebug() != DebugOptions::NoDebug; } +jni::Local> NativeMapView::getActionJournalLogFiles(JNIEnv& env) { + const auto& actionJournal = map->getActionJournal(); + if (!actionJournal) { + return jni::Local>(); + } + + const auto& files = actionJournal->getLogFiles(); + auto jFiles = jni::Array::New(env, files.size()); + + uint32_t index = 0; + for (const auto& file : files) { + jFiles.Set(env, index++, jni::Make(env, file)); + } + + return jFiles; +} + +jni::Local> NativeMapView::getActionJournalLog(JNIEnv& env) { + const auto& actionJournal = map->getActionJournal(); + if (!actionJournal) { + return jni::Local>(); + } + + const auto& events = actionJournal->getLog(); + auto jEvents = jni::Array::New(env, events.size()); + + uint32_t index = 0; + for (const auto& event : events) { + jEvents.Set(env, index++, jni::Make(env, event)); + } + + return jEvents; +} + +void NativeMapView::clearActionJournalLog(JNIEnv&) { + const auto& actionJournal = map->getActionJournal(); + if (actionJournal) { + actionJournal->clearLog(); + } +} + jni::jboolean NativeMapView::isFullyLoaded(JNIEnv&) { return map->isFullyLoaded(); } @@ -1246,8 +1289,7 @@ void NativeMapView::registerNative(jni::JNIEnv& env) { const jni::Object&, const jni::Object&, const jni::Object&, - jni::jfloat, - jni::jboolean>, + const jni::Object&>, "nativeInitialize", "nativeDestroy", METHOD(&NativeMapView::resizeView, "nativeResizeView"), @@ -1292,6 +1334,9 @@ void NativeMapView::registerNative(jni::JNIEnv& env) { METHOD(&NativeMapView::addMarkers, "nativeAddMarkers"), METHOD(&NativeMapView::setDebug, "nativeSetDebug"), METHOD(&NativeMapView::getDebug, "nativeGetDebug"), + METHOD(&NativeMapView::getActionJournalLogFiles, "nativeGetActionJournalLogFiles"), + METHOD(&NativeMapView::getActionJournalLog, "nativeGetActionJournalLog"), + METHOD(&NativeMapView::clearActionJournalLog, "nativeClearActionJournalLog"), METHOD(&NativeMapView::isFullyLoaded, "nativeIsFullyLoaded"), METHOD(&NativeMapView::onLowMemory, "nativeOnLowMemory"), METHOD(&NativeMapView::getMetersPerPixelAtLatitude, "nativeGetMetersPerPixelAtLatitude"), diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp index 7ec2a4dd3..6fff2e49c 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp @@ -23,6 +23,7 @@ #include "map/camera_position.hpp" #include "map/image.hpp" #include "style/light.hpp" +#include "native_map_options.hpp" #include "bitmap.hpp" #include @@ -49,8 +50,7 @@ public: const jni::Object&, const jni::Object&, const jni::Object&, - jni::jfloat, - jni::jboolean); + const jni::Object&); virtual ~NativeMapView(); @@ -199,6 +199,12 @@ public: jni::jboolean getDebug(JNIEnv&); + jni::Local> getActionJournalLogFiles(JNIEnv&); + + jni::Local> getActionJournalLog(JNIEnv&); + + void clearActionJournalLog(JNIEnv&); + jni::jboolean isFullyLoaded(JNIEnv&); jni::jdouble getMetersPerPixelAtLatitude(JNIEnv&, jni::jdouble, jni::jdouble); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java index 04d758d68..cc282134a 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java @@ -897,6 +897,49 @@ public final class MapLibreMap { nativeMapView.setDebug(debugActive); } + /** + *

+ * Get the list of action journal log files from oldest to newest. + *

+ */ + public String[] getActionJournalLogFiles() { + return nativeMapView.getActionJournalLogFiles(); + } + + /** + *

+ * Get the action journal events from oldest to newest. + *

+ * Each element contains a serialized json object with the event data. + * Example + * `{ + * "name" : "onTileAction", + * "time" : "2025-04-17T13:13:13.974Z", + * "styleName" : "Streets", + * "styleURL" : "maptiler://maps/streets", + * "event" : { + * "action" : "RequestedFromNetwork", + * "tileX" : 0, + * "tileY" : 0, + * "tileZ" : 0, + * "overscaledZ" : 0, + * "sourceID" : "openmaptiles" + * } + * }` + */ + public String[] getActionJournalLog() { + return nativeMapView.getActionJournalLog(); + } + + /** + *

+ * Clear stored action journal events. + *

+ */ + public void clearActionJournalLog() { + nativeMapView.clearActionJournalLog(); + } + // // API endpoint config // diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMapOptions.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMapOptions.java index 4f35ceecd..231f2e915 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMapOptions.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMapOptions.java @@ -94,6 +94,11 @@ public class MapLibreMapOptions implements Parcelable { private boolean crossSourceCollisions = true; + private boolean actionJournalEnabled = false; + private String actionJournalPath = ""; + private long actionJournalLogFileSize = 1024 * 1024; + private long actionJournalLogFileCount = 5; + /** * Creates a new MapLibreMapOptions object. * @@ -151,6 +156,11 @@ public class MapLibreMapOptions implements Parcelable { pixelRatio = in.readFloat(); foregroundLoadColor = in.readInt(); crossSourceCollisions = in.readByte() != 0; + + actionJournalEnabled = in.readByte() != 0; + actionJournalPath = in.readString(); + actionJournalLogFileSize = in.readLong(); + actionJournalLogFileCount = in.readLong(); } /** @@ -182,6 +192,8 @@ public class MapLibreMapOptions implements Parcelable { @NonNull Context context, @Nullable TypedArray typedArray) { float pxlRatio = context.getResources().getDisplayMetrics().density; + maplibreMapOptions.actionJournalPath(context.getFilesDir().getAbsolutePath()); + try { maplibreMapOptions.camera(new CameraPosition.Builder(typedArray).build()); @@ -305,6 +317,16 @@ public class MapLibreMapOptions implements Parcelable { maplibreMapOptions.crossSourceCollisions( typedArray.getBoolean(R.styleable.maplibre_MapView_maplibre_cross_source_collisions, true) ); + + maplibreMapOptions.actionJournalEnabled( + typedArray.getBoolean(R.styleable.maplibre_MapView_maplibre_actionJournalEnabled, false) + ); + maplibreMapOptions.actionJournalLogFileSize( + typedArray.getInteger(R.styleable.maplibre_MapView_maplibre_actionJournalLogFileSize, 1024 * 1024) + ); + maplibreMapOptions.actionJournalLogFileCount( + typedArray.getInteger(R.styleable.maplibre_MapView_maplibre_actionJournalLogFileCount, 5) + ); } finally { typedArray.recycle(); } @@ -730,6 +752,65 @@ public class MapLibreMapOptions implements Parcelable { return this; } + /** + * Enable action journal event logging, defaults to false. + *

+ * If set to true, enables map event file logging (obtainable via MapView#getActionJournalLog) + *

+ * + * @param actionJournalEnabled true to enable, false to disable + * @return This + */ + @NonNull + public MapLibreMapOptions actionJournalEnabled(boolean actionJournalEnabled) { + this.actionJournalEnabled = actionJournalEnabled; + return this; + } + + /** + * Set the action journal log path. + * + * @param actionJournalPath Path to be used + * @return This + */ + @NonNull + public MapLibreMapOptions actionJournalPath(@NonNull String actionJournalPath) { + this.actionJournalPath = actionJournalPath; + return this; + } + + /** + * Set the action journal log file size. + *

+ * The action journal uses a rolling log with multiple files. + * Total log size is equal to `actionJournalLogFileSize * actionJournalLogFileCount`. + *

+ * + * @param actionJournalLogFileSize maximum log file size + * @return This + */ + @NonNull + public MapLibreMapOptions actionJournalLogFileSize(long actionJournalLogFileSize) { + this.actionJournalLogFileSize = actionJournalLogFileSize; + return this; + } + + /** + * Set the action journal log file count. + *

+ * The action journal uses a rolling log with multiple files. + * Total log size is equal to `actionJournalLogFileSize * actionJournalLogFileCount`. + *

+ * + * @param actionJournalLogFileCount maximum number of log files + * @return This + */ + @NonNull + public MapLibreMapOptions actionJournalLogFileCount(long actionJournalLogFileCount) { + this.actionJournalLogFileCount = actionJournalLogFileCount; + return this; + } + /** * Enable local ideograph font family, defaults to true. * @@ -821,6 +902,44 @@ public class MapLibreMapOptions implements Parcelable { return crossSourceCollisions; } + /** + * Check whether action journal logging is enabled. + * + * @return true if enabled + */ + public boolean getActionJournalEnabled() { + return actionJournalEnabled; + } + + /** + * Get the current configured action journal log path. + * + * @return log file path + */ + public String getActionJournalPath() { + return actionJournalPath; + } + + /** + * Get the current configured action journal log file size. + * Total log size is equal to `actionJournalLogFileSize * actionJournalLogFileCount`. + * + * @return maximum file size + */ + public long getActionJournalLogFileSize() { + return actionJournalLogFileSize; + } + + /** + * Get the current configured action journal log file count. + * Total log size is equal to `actionJournalLogFileSize * actionJournalLogFileCount`. + * + * @return maximum log files used + */ + public long getActionJournalLogFileCount() { + return actionJournalLogFileCount; + } + /** * Set the flag to render the map surface on top of another surface. * @@ -1205,6 +1324,11 @@ public class MapLibreMapOptions implements Parcelable { dest.writeFloat(pixelRatio); dest.writeInt(foregroundLoadColor); dest.writeByte((byte) (crossSourceCollisions ? 1 : 0)); + + dest.writeByte((byte) (actionJournalEnabled ? 1 : 0)); + dest.writeString(actionJournalPath); + dest.writeLong(actionJournalLogFileSize); + dest.writeLong(actionJournalLogFileCount); } @Override @@ -1325,6 +1449,22 @@ public class MapLibreMapOptions implements Parcelable { return false; } + if (actionJournalEnabled != options.actionJournalEnabled) { + return false; + } + + if (actionJournalPath.equals(options.actionJournalPath)) { + return false; + } + + if (actionJournalLogFileSize != options.actionJournalLogFileSize) { + return false; + } + + if (actionJournalLogFileCount != options.actionJournalLogFileCount) { + return false; + } + return false; } @@ -1372,6 +1512,10 @@ public class MapLibreMapOptions implements Parcelable { result = 31 * result + Arrays.hashCode(localIdeographFontFamilies); result = 31 * result + (int) pixelRatio; result = 31 * result + (crossSourceCollisions ? 1 : 0); + result = 31 * result + (actionJournalEnabled ? 1 : 0); + result = 31 * result + (actionJournalPath != null ? actionJournalPath.hashCode() : 0); + result = 31 * result + (int) actionJournalLogFileSize; + result = 31 * result + (int) actionJournalLogFileCount; return result; } } diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapView.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapView.java index 4c62ff47e..39721f55e 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapView.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapView.java @@ -306,15 +306,13 @@ public class MapView extends FrameLayout implements NativeMapView.ViewCallback { } private void initializeDrawingSurface(MapLibreMapOptions options) { - mapRenderer = MapRenderer.create(options, getContext(), () -> MapView.this.onSurfaceCreated()); + mapRenderer = MapRenderer.create(options, getContext(), MapView.this::onSurfaceCreated); renderView = mapRenderer.getView(); addView(renderView, 0); - boolean crossSourceCollisions = options.getCrossSourceCollisions(); - nativeMapView = new NativeMapView( - getContext(), getPixelRatio(), crossSourceCollisions, this, mapChangeReceiver, mapRenderer - ); + options.pixelRatio(getPixelRatio()); + nativeMapView = new NativeMapView(getContext(), options, this, mapChangeReceiver, mapRenderer); } private void onSurfaceCreated() { diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java index 31fd4f6ba..4192b6d77 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java @@ -221,6 +221,12 @@ interface NativeMap { boolean getDebug(); + String[] getActionJournalLogFiles(); + + String[] getActionJournalLog(); + + void clearActionJournalLog(); + void setReachability(boolean status); void setApiBaseUrl(String baseUrl); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapOptions.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapOptions.java new file mode 100644 index 000000000..788a9d2d7 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapOptions.java @@ -0,0 +1,56 @@ +package org.maplibre.android.maps; + +public class NativeMapOptions { + + private final float pixelRatio; + private final boolean crossSourceCollisions; + + private final boolean actionJournalEnabled; + private final String actionJournalPath; + private final long actionJournalLogFileSize; + private final long actionJournalLogFileCount; + + public NativeMapOptions(MapLibreMapOptions options) { + pixelRatio = options.getPixelRatio(); + crossSourceCollisions = options.getCrossSourceCollisions(); + + actionJournalEnabled = options.getActionJournalEnabled(); + actionJournalPath = options.getActionJournalPath(); + actionJournalLogFileSize = options.getActionJournalLogFileSize(); + actionJournalLogFileCount = options.getActionJournalLogFileCount(); + } + + public NativeMapOptions(float pixelRatio, boolean crossSourceCollisions) { + this.pixelRatio = pixelRatio; + this.crossSourceCollisions = crossSourceCollisions; + + actionJournalEnabled = false; + actionJournalPath = ""; + actionJournalLogFileSize = 0; + actionJournalLogFileCount = 0; + } + + public float pixelRatio() { + return pixelRatio; + } + + public boolean crossSourceCollisions() { + return crossSourceCollisions; + } + + public boolean actionJournalEnabled() { + return actionJournalEnabled; + } + + public String actionJournalPath() { + return actionJournalPath; + } + + public long actionJournalLogFileSize() { + return actionJournalLogFileSize; + } + + public long actionJournalLogFileCount() { + return actionJournalLogFileCount; + } +} diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java index 4ed013c50..b714896e3 100755 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java @@ -91,23 +91,29 @@ final class NativeMapView implements NativeMap { // Constructors // - public NativeMapView(@NonNull final Context context, final boolean crossSourceCollisions, + public NativeMapView(@NonNull final Context context, final ViewCallback viewCallback, final StateCallback stateCallback, final MapRenderer mapRenderer) { - this(context, context.getResources().getDisplayMetrics().density, crossSourceCollisions, viewCallback, - stateCallback, mapRenderer); + this(context, new NativeMapOptions(context.getResources().getDisplayMetrics().density, false), + viewCallback, stateCallback, mapRenderer); } - public NativeMapView(final Context context, final float pixelRatio, final boolean crossSourceCollisions, + public NativeMapView(@NonNull final Context context, final MapLibreMapOptions options, + final ViewCallback viewCallback, final StateCallback stateCallback, + final MapRenderer mapRenderer) { + this(context, new NativeMapOptions(options), viewCallback, stateCallback, mapRenderer); + } + + public NativeMapView(@NonNull final Context context, final NativeMapOptions nativeOptions, final ViewCallback viewCallback, final StateCallback stateCallback, final MapRenderer mapRenderer) { this.mapRenderer = mapRenderer; this.viewCallback = viewCallback; this.fileSource = FileSource.getInstance(context); - this.pixelRatio = pixelRatio; + this.pixelRatio = nativeOptions.pixelRatio(); this.thread = Thread.currentThread(); this.stateCallback = stateCallback; - nativeInitialize(this, fileSource, mapRenderer, pixelRatio, crossSourceCollisions); + nativeInitialize(this, fileSource, mapRenderer, nativeOptions); } // @@ -646,6 +652,30 @@ final class NativeMapView implements NativeMap { return nativeGetDebug(); } + @Override + public String[] getActionJournalLogFiles() { + if (checkState("getActionJournalLogFiles")) { + return null; + } + return nativeGetActionJournalLogFiles(); + } + + @Override + public String[] getActionJournalLog() { + if (checkState("getActionJournalLog")) { + return null; + } + return nativeGetActionJournalLog(); + } + + @Override + public void clearActionJournalLog() { + if (checkState("clearActionJournalLog")) { + return; + } + nativeClearActionJournalLog(); + } + @Override public boolean isFullyLoaded() { if (checkState("isFullyLoaded")) { @@ -1275,8 +1305,7 @@ final class NativeMapView implements NativeMap { private native void nativeInitialize(NativeMapView nativeMap, FileSource fileSource, MapRenderer mapRenderer, - float pixelRatio, - boolean crossSourceCollisions); + NativeMapOptions nativeOptions); @Keep private native void nativeDestroy(); @@ -1429,6 +1458,15 @@ final class NativeMapView implements NativeMap { @Keep private native boolean nativeGetDebug(); + @Keep + private native String[] nativeGetActionJournalLogFiles(); + + @Keep + private native String[] nativeGetActionJournalLog(); + + @Keep + private native void nativeClearActionJournalLog(); + @Keep private native boolean nativeIsFullyLoaded(); diff --git a/platform/android/MapLibreAndroid/src/main/res-public/values/public.xml b/platform/android/MapLibreAndroid/src/main/res-public/values/public.xml index 84939ddea..bd4ea5ee2 100644 --- a/platform/android/MapLibreAndroid/src/main/res-public/values/public.xml +++ b/platform/android/MapLibreAndroid/src/main/res-public/values/public.xml @@ -17,6 +17,11 @@ + + + + + diff --git a/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml b/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml index ff471520c..658184e4d 100644 --- a/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml +++ b/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml @@ -12,6 +12,11 @@ + + + + + diff --git a/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/MapLibreTest.kt b/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/MapLibreTest.kt index 54d6b103a..ef8783a21 100644 --- a/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/MapLibreTest.kt +++ b/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/MapLibreTest.kt @@ -18,6 +18,7 @@ import org.maplibre.android.maps.MapView import org.maplibre.android.utils.ConfigUtils.Companion.getMockedOptions import org.mockito.ArgumentMatchers import org.mockito.Mockito +import java.io.File class MapLibreTest { private var context: Context? = null @@ -68,8 +69,10 @@ class MapLibreTest { fun testNoInstance() { val displayMetrics = Mockito.mock(DisplayMetrics::class.java) val resources = Mockito.mock(Resources::class.java) + val files = Mockito.mock(File::class.java) Mockito.`when`(resources.displayMetrics).thenReturn(displayMetrics) Mockito.`when`(context!!.resources).thenReturn(resources) + Mockito.`when`(context!!.filesDir).thenReturn(files) val typedArray = Mockito.mock(TypedArray::class.java) Mockito.`when`(context!!.obtainStyledAttributes(ArgumentMatchers.nullable(AttributeSet::class.java), ArgumentMatchers.any(IntArray::class.java), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())) .thenReturn(typedArray) diff --git a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/maps/BaseLayerTest.kt b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/maps/BaseLayerTest.kt index e67647441..5e6b4aa58 100644 --- a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/maps/BaseLayerTest.kt +++ b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/maps/BaseLayerTest.kt @@ -17,7 +17,7 @@ abstract class BaseLayerTest : AppCenter() { fun before() { val context = InstrumentationRegistry.getInstrumentation().context - nativeMapView = NativeMapView(context, false, null, null, NativeMapViewTest.DummyRenderer(context)) + nativeMapView = NativeMapView(context, null, null, NativeMapViewTest.DummyRenderer(context)) nativeMapView.resizeView(WIDTH, HEIGHT) } diff --git a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/maps/NativeMapViewTest.kt b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/maps/NativeMapViewTest.kt index 5664fa447..2d32ca74c 100644 --- a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/maps/NativeMapViewTest.kt +++ b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/maps/NativeMapViewTest.kt @@ -46,8 +46,10 @@ class NativeMapViewTest : AppCenter() { fun before() { val context = InstrumentationRegistry.getInstrumentation().context val apiKey = MapLibre.getApiKey() + val options = NativeMapOptions(2.0f, false) + MapLibre.getInstance(context, apiKey, WellKnownTileServer.MapTiler) - nativeMapView = NativeMapView(context, 2.0f, false, null, null, DummyRenderer(context)) + nativeMapView = NativeMapView(context, options, null, null, DummyRenderer(context)) nativeMapView.resizeView(WIDTH, HEIGHT) } diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/events/ObserverActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/events/ObserverActivity.kt index e24990b5c..9ac827fea 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/events/ObserverActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/events/ObserverActivity.kt @@ -3,6 +3,7 @@ package org.maplibre.android.testapp.activity.events import android.app.ActivityManager import android.os.Build import android.os.Bundle +import android.view.View import android.view.WindowManager import androidx.appcompat.app.AppCompatActivity import org.maplibre.android.MapLibre @@ -18,7 +19,11 @@ import kotlin.time.TimeSource import kotlin.time.TimeSource.Monotonic import org.maplibre.android.log.Logger import org.maplibre.android.log.Logger.INFO +import org.maplibre.android.maps.MapLibreMap + + +// # --8<-- [start:ObserverActivity] /** * Test activity showcasing logging observer actions from the core */ @@ -30,6 +35,8 @@ class ObserverActivity : AppCompatActivity(), MapView.OnGlyphsRequestedListener, MapView.OnSpriteLoadedListener, MapView.OnSpriteRequestedListener { + // # --8<-- [end:ObserverActivity] + private lateinit var mapView: MapView companion object { @@ -38,9 +45,11 @@ class ObserverActivity : AppCompatActivity(), override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_map_simple) + setContentView(R.layout.activity_map_events) mapView = findViewById(R.id.mapView) mapView.onCreate(savedInstanceState) + + // # --8<-- [start:addListeners] mapView.addOnPreCompileShaderListener(this) mapView.addOnPostCompileShaderListener(this) mapView.addOnTileActionListener(this) @@ -48,44 +57,67 @@ class ObserverActivity : AppCompatActivity(), mapView.addOnGlyphsRequestedListener(this) mapView.addOnSpriteLoadedListener(this) mapView.addOnSpriteRequestedListener(this) + // # --8<-- [end:addListeners] + mapView.getMapAsync { it.setStyle( - Style.Builder().fromUri(TestStyles.getPredefinedStyleWithFallback("Streets")) + Style.Builder().fromUri(TestStyles.DEMOTILES) ) } + + // print and clear action journal + val fab = findViewById(R.id.fab) + fab?.setOnClickListener { _: View? -> + mapView.getMapAsync { + printActionJournal(it) + } + } } + // # --8<-- [start:printActionJournal] + fun printActionJournal(map: MapLibreMap) { + // configure using `MapLibreMapOptions.actionJournal*` methods + + Logger.i(TAG,"ActionJournal files: \n${map.actionJournalLogFiles.joinToString("\n")}") + Logger.i(TAG,"ActionJournal : \n${map.actionJournalLog.joinToString("\n")}") + + // print only the newest events on each call + map.clearActionJournalLog() + } + // # --8<-- [end:printActionJournal] + private val shaderTimes: HashMap = HashMap() - public override fun onPreCompileShader(id: Int, type: Int, defines: String) { + // # --8<-- [start:mapEvents] + override fun onPreCompileShader(id: Int, type: Int, defines: String) { shaderTimes["${id}-${type}-${defines}"] = TimeSource.Monotonic.markNow() Logger.i(TAG, "A new shader is being compiled, shaderID:${id}, backend type:${type}, program configuration:${defines}") } - public override fun onPostCompileShader(id: Int, type: Int, defines: String) { + override fun onPostCompileShader(id: Int, type: Int, defines: String) { val startTime = shaderTimes.get("${id}-${type}-${defines}") if (startTime != null) { Logger.i(TAG, "A shader has been compiled in ${startTime.elapsedNow()}, shaderID:${id}, backend type:${type}, program configuration:${defines}") } } - public override fun onGlyphsRequested(fontStack: Array, rangeStart: Int, rangeEnd: Int) { + override fun onGlyphsRequested(fontStack: Array, rangeStart: Int, rangeEnd: Int) { Logger.i(TAG, "Glyphs are being requested for the font stack $fontStack, ranging from $rangeStart to $rangeEnd") } - public override fun onGlyphsLoaded(fontStack: Array, rangeStart: Int, rangeEnd: Int) { + override fun onGlyphsLoaded(fontStack: Array, rangeStart: Int, rangeEnd: Int) { Logger.i(TAG, "Glyphs have been loaded for the font stack $fontStack, ranging from $rangeStart to $rangeEnd") } - public override fun onSpriteRequested(id: String, url: String) { + override fun onSpriteRequested(id: String, url: String) { Logger.i(TAG, "The sprite $id has been requested from $url") } - public override fun onSpriteLoaded(id: String, url: String) { + override fun onSpriteLoaded(id: String, url: String) { Logger.i(TAG, "The sprite $id has been loaded from $url") } - public override fun onTileAction(op: TileOperation, x: Int, y: Int, z: Int, wrap: Int, overscaledZ: Int, sourceID: String) { + override fun onTileAction(op: TileOperation, x: Int, y: Int, z: Int, wrap: Int, overscaledZ: Int, sourceID: String) { val tile = "X:${x}, Y:${y}, Z:${z}, Wrap:${wrap}, OverscaledZ:${overscaledZ}, SourceID:${sourceID}" when (op) { TileOperation.RequestedFromCache -> Logger.i(TAG, "Requesting tile ${tile} from the cache") @@ -99,6 +131,7 @@ class ObserverActivity : AppCompatActivity(), TileOperation.NullOp -> Logger.e(TAG, "An unknown tile operation was emitted for tile ${tile}") } } + // # --8<-- [end:mapEvents] override fun onStart() { super.onStart() diff --git a/platform/android/MapLibreAndroidTestApp/src/main/res/layout/activity_map_events.xml b/platform/android/MapLibreAndroidTestApp/src/main/res/layout/activity_map_events.xml new file mode 100644 index 000000000..5a0cdfd4e --- /dev/null +++ b/platform/android/MapLibreAndroidTestApp/src/main/res/layout/activity_map_events.xml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/platform/android/docs/observability/action-journal.md b/platform/android/docs/observability/action-journal.md new file mode 100644 index 000000000..4f4caee74 --- /dev/null +++ b/platform/android/docs/observability/action-journal.md @@ -0,0 +1,80 @@ +# Action Journal + +{{ activity_source_note("ObserverActivity.kt") }} + +The Action Journal provides functionality for persistent logging of top level map events. + + + +Its primary use case is to assist in debugging problematic sessions and crashes by offering additional insight into the actions performed by the map at the time of failure. Data is stored in human readable format, which is useful for analyzing individual cases, but can also be easily translated and aggregated into a database, allowing for efficient analysis of multiple cases and helping to identify recurring patterns (Google BigQuery, AWS Glue + S3 + Athena, etc). + +We are always interested in improving observability, so if you have a special use case, feel free to [open an issue or pull request](https://github.com/maplibre/maplibre-native) to extend the types of observability methods. + +## Enabling the Action Journal + +You can enable the action journal either through XML: + +```xml +--8<-- "MapLibreAndroidTestApp/src/main/res/layout/activity_map_events.xml:MapView" +``` + +Or by passing the corresponding options with `MapLibreMapOptions` to `MapView`. For more information see [Configuration](../configuration.md). + +## Logging implementation details + +The logging is implemented using rolling files with a size based policy: + +- A new file is created when the current log size exceeds `MapLibreMapOptions.actionJournalLogFileSize`. +- When the maximum number of files exceeds `MapLibreMapOptions.actionJournalLogFileCount`: + - The oldest one is deleted. + - The remaining files are renamed sequentially to maintain the naming convention `action_journal.0.log` through `action_journal.{logFileCount - 1}.log`. +- Each file contains one event per line. +- All files are stored in an umbrella `action_journal` directory at `MapLibreMapOptions.actionJournalPath`. + +See also: `MapLibreMap`, `MapLibreMapOptions`. + +## Event format + +Events are stored as JSON objects with the following format: + +| Field | Type | Required | Description | +| :---- | :--: | :------: | :---------- | +| name | string | true | event name | +| time | string | true | event time ([ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) with milliseconds) | +| styleName | string | false | currently loaded style name | +| styleURL | string | false | currently loaded style URL | +| clientName | string | false | | +| clientVersion | string | false | | +| event | object | false | event specific data - consists of encoded values of the parameters passed to their `MLNMapViewDelegate` counterparts + + +``` +{ + "name" : "onTileAction", + "time" : "2025-04-17T13:13:13.974Z", + "styleName" : "Streets", + "styleURL" : "maptiler://maps/streets", + "clientName" : "App", + "clientVersion" : "1.0", + "event" : { + "action" : "RequestedFromNetwork", + "tileX" : 0, + "tileY" : 0, + "tileZ" : 0, + "overscaledZ" : 0, + "sourceID" : "openmaptiles" + } +} +``` + +## Usage + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/events/ObserverActivity.kt:printActionJournal" +``` + +## Alternative + +The implementation is kept close to the core events to minimize additional locking and avoid platform-specific conversions and calls. As a result customization options and extensibility is limited. + +For greater flexibility, consider using the `MapView` event interface (see also `MapChangeReceiver`). It provides hooks for most Action Journal events and allows for more customizable querying and storage of map data. However, this comes at the cost of added complexity. See [Observe Map Events](./observe-map-events.md) to learn about the map events that you can listen for, which mirror the events available in the action journal. diff --git a/platform/android/docs/observability/observe-map-events.md b/platform/android/docs/observability/observe-map-events.md new file mode 100644 index 000000000..4c27f6bf3 --- /dev/null +++ b/platform/android/docs/observability/observe-map-events.md @@ -0,0 +1,21 @@ +# Observe Map Events + +{{ activity_source_note("ObserverActivity.kt") }} + +You can observe-low level map events that are happening by registering listeners to a `MapView`. Below you can see the the map events that are currently available. + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/events/ObserverActivity.kt:mapEvents" +``` + +You need to register them with these APIs: + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/events/ObserverActivity.kt:addListeners" +``` + +In this case we implement them by implementing the interfaces below in the activity class, but you could also use lambdas. + +```kotlin +--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/events/ObserverActivity.kt:ObserverActivity" +``` diff --git a/platform/darwin/bazel/files.bzl b/platform/darwin/bazel/files.bzl index facd90f72..4f732095f 100644 --- a/platform/darwin/bazel/files.bzl +++ b/platform/darwin/bazel/files.bzl @@ -50,6 +50,7 @@ MLN_GENERATED_DARWIN_TEST_CODE = [ ] MLN_DARWIN_OBJC_HEADERS = [ + "src/MLNActionJournalOptions.h", "src/MLNAnnotation.h", "src/MLNAttributedExpression.h", "src/MLNAttributionInfo.h", @@ -71,6 +72,7 @@ MLN_DARWIN_OBJC_HEADERS = [ "src/MLNLocationManager.h", "src/MLNLoggingConfiguration.h", "src/MLNMapCamera.h", + "src/MLNMapOptions.h", "src/MLNMapSnapshotter.h", "src/MLNMultiPoint.h", "src/MLNNetworkConfiguration.h", @@ -126,6 +128,7 @@ MLN_DARWIN_OBJCPP_HEADERS = [ ] MLN_DARWIN_PRIVATE_HEADERS = [ + "src/MLNActionJournalOptions_Private.h", "src/MLNAttributionInfo_Private.h", "src/MLNComputedShapeSource_Private.h", "src/MLNCustomStyleLayer_Private.h", @@ -161,6 +164,7 @@ MLN_DARWIN_PRIVATE_HEADERS = [ ] MLN_DARWIN_PUBLIC_OBJCPP_SOURCE = [ + "src/MLNActionJournalOptions.mm", "src/MLNAttributionInfo.mm", "src/MLNBackendResource.mm", "src/MLNComputedShapeSource.mm", @@ -173,6 +177,7 @@ MLN_DARWIN_PUBLIC_OBJCPP_SOURCE = [ "src/MLNImageSource.mm", "src/MLNLoggingConfiguration.mm", "src/MLNMapCamera.mm", + "src/MLNMapOptions.mm", "src/MLNMapSnapshotter.mm", "src/MLNMultiPoint.mm", "src/MLNNetworkConfiguration.mm", diff --git a/platform/darwin/src/MLNActionJournalOptions.h b/platform/darwin/src/MLNActionJournalOptions.h new file mode 100644 index 000000000..635db6869 --- /dev/null +++ b/platform/darwin/src/MLNActionJournalOptions.h @@ -0,0 +1,35 @@ +#import + +#import "MLNFoundation.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + The ``MLNActionJournalOptions`` defines action journal properties such as path, log size, etc... + */ +MLN_EXPORT +@interface MLNActionJournalOptions : NSObject + +/** + * Enable/disable journal logging + */ +@property (nonatomic) BOOL enabled; + +/** + * Local file path. + */ +@property (nonatomic, nonnull) NSString* path; + +/** + * Log file size (total journal size is equal to `logFileSize * logFileCount`) + */ +@property (nonatomic) NSInteger logFileSize; + +/** + * Maximum number of log files + */ +@property (nonatomic) NSInteger logFileCount; + +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/darwin/src/MLNActionJournalOptions.mm b/platform/darwin/src/MLNActionJournalOptions.mm new file mode 100644 index 000000000..a56e684f0 --- /dev/null +++ b/platform/darwin/src/MLNActionJournalOptions.mm @@ -0,0 +1,54 @@ +#import "MLNActionJournalOptions.h" +#import + +@implementation MLNActionJournalOptions { + std::unique_ptr _actionJournalOptionsInternal; +} + +- (instancetype)init { + if (self = [super init]) { + _actionJournalOptionsInternal = std::make_unique(); + + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + _actionJournalOptionsInternal->withPath([paths.firstObject stringByAppendingPathComponent:@""].UTF8String); + } + return self; +} + +- (mbgl::util::ActionJournalOptions)getCoreOptions { + return *_actionJournalOptionsInternal; +} + +- (void)setEnabled:(BOOL)value { + _actionJournalOptionsInternal->enable(value); +} + +- (BOOL)enabled { + return _actionJournalOptionsInternal->enabled(); +} + +- (void)setPath:(NSString*)value { + _actionJournalOptionsInternal->withPath(value.UTF8String); +} + +- (NSString*)path { + return [NSString stringWithUTF8String:_actionJournalOptionsInternal->path().c_str()]; +} + +- (void)setLogFileSize:(NSInteger)value { + _actionJournalOptionsInternal->withLogFileSize(static_cast(value)); +} + +- (NSInteger)logFileSize { + return _actionJournalOptionsInternal->logFileSize(); +} + +- (void)setLogFileCount:(NSInteger)value { + _actionJournalOptionsInternal->withLogFileCount(static_cast(value)); +} + +- (NSInteger)logFileCount { + return _actionJournalOptionsInternal->logFileCount(); +} + +@end diff --git a/platform/darwin/src/MLNActionJournalOptions_Private.h b/platform/darwin/src/MLNActionJournalOptions_Private.h new file mode 100644 index 000000000..a48680b06 --- /dev/null +++ b/platform/darwin/src/MLNActionJournalOptions_Private.h @@ -0,0 +1,12 @@ +#import +#import "MLNActionJournalOptions.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MLNActionJournalOptions (Private) + +- (mbgl::util::ActionJournalOptions)getCoreOptions; + +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/darwin/src/MLNMapOptions.h b/platform/darwin/src/MLNMapOptions.h new file mode 100644 index 000000000..48267467c --- /dev/null +++ b/platform/darwin/src/MLNMapOptions.h @@ -0,0 +1,37 @@ +#import + +#import "MLNActionJournalOptions.h" +#import "MLNFoundation.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + The ``MLNMapOptions`` object provides a way to set map properties for each instance + */ +MLN_EXPORT +@interface MLNMapOptions : NSObject + +/** + URL of the map style to display. The URL may be a full HTTP + or HTTPS URL, a canonical URL or a path to a local file relative + to the application’s resource path. Specify `nil` for the default style. + */ +@property (nonatomic, nullable) NSURL *styleURL; + +/** + JSON string of the map style to display. The JSON must conform to the + MapLibre Style Specification. + Specify `nil` for the default style. + Ignored if `styleURL` is set to a non-nil value. + */ + +@property (nonatomic, nullable) NSString *styleJSON; + +/** + Action journal options + */ +@property (nonatomic, nonnull) MLNActionJournalOptions *actionJournalOptions; + +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/darwin/src/MLNMapOptions.mm b/platform/darwin/src/MLNMapOptions.mm new file mode 100644 index 000000000..d9ddabbc3 --- /dev/null +++ b/platform/darwin/src/MLNMapOptions.mm @@ -0,0 +1,23 @@ +#import "MLNMapOptions.h" + +@interface MLNMapOptions () + +@end + +@implementation MLNMapOptions + +-(instancetype _Nonnull)init +{ + self = [super init]; + if (self) + { + _styleURL = nil; + _styleJSON = nil; + + _actionJournalOptions = [[MLNActionJournalOptions alloc] init]; + } + + return self; +} + +@end diff --git a/platform/ios/BUILD.bazel b/platform/ios/BUILD.bazel index 18f05e9b2..b818510d7 100644 --- a/platform/ios/BUILD.bazel +++ b/platform/ios/BUILD.bazel @@ -4,6 +4,7 @@ load("@rules_apple//apple:apple.bzl", "apple_static_xcframework", "apple_xcframe load("@rules_apple//apple:ios.bzl", "ios_application", "ios_framework") load("@rules_apple//apple:resources.bzl", "apple_resource_bundle") load("@rules_apple//apple:versioning.bzl", "apple_bundle_version") + load( "@rules_xcodeproj//xcodeproj:defs.bzl", "top_level_target", diff --git a/platform/ios/MapLibre.docc/ActionJournalExample.md b/platform/ios/MapLibre.docc/ActionJournalExample.md new file mode 100644 index 000000000..4842f0abe --- /dev/null +++ b/platform/ios/MapLibre.docc/ActionJournalExample.md @@ -0,0 +1,84 @@ +# Action Journal + +Learn about the ``MLNMapView`` methods for logging and viewing map actions. + + + +The Action Journal provides functionality for persistent logging of top level map events. + +Its primary use case is to assist in debugging problematic sessions and crashes by offering additional insight into the actions performed by the map at the time of failure. Data is stored in human readable format, which is useful for analyzing individual cases, but can also be easily translated and aggregated into a database, allowing for efficient analysis of multiple cases and helping to identify recurring patterns (Google BigQuery, AWS Glue + S3 + Athena, etc). + +We are always interested in improving observability, so if you have a special use case, feel free to [open an issue or pull request](https://github.com/maplibre/maplibre-native) to extend the types of observability methods. + +## Logging implementation details + +The logging is implemented using rolling files with a size based policy: + +- A new file is created when the current log size exceeds ``MLNActionJournalOptions/logFileSize``. +- When the maximum number of files exceeds ``MLNActionJournalOptions/logFileCount``: + - The oldest one is deleted. + - The remaining files are renamed sequentially to maintain the naming convention `action_journal.0.log` through `action_journal.{logFileCount - 1}.log`. +- Each file contains one event per line. +- All files are stored in an umbrella "action_journal" directory at ``MLNActionJournalOptions/path``. + +See also: ``MLNSettings``, ``MLNActionJournalOptions``. + +## Event format + +Events are stored as JSON objects with the following format: + +| Field | Type | Required | Description | +| :---- | :--: | :------: | :---------- | +| name | string | true | event name | +| time | string | true | event time ([ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) with milliseconds) | +| styleName | string | false | currently loaded style name | +| styleURL | string | false | currently loaded style URL | +| clientName | string | false | | +| clientVersion | string | false | | +| event | object | false | event specific data - consists of encoded values of the parameters passed to their ``MLNMapViewDelegate`` counterparts + +``` +{ + "name" : "onTileAction", + "time" : "2025-04-17T13:13:13.974Z", + "styleName" : "Streets", + "styleURL" : "maptiler://maps/streets", + "clientName" : "App", + "clientVersion" : "1.0", + "event" : { + "action" : "RequestedFromNetwork", + "tileX" : 0, + "tileY" : 0, + "tileZ" : 0, + "overscaledZ" : 0, + "sourceID" : "openmaptiles" + } +} +``` + +## Usage + +Enabling the action journal. + + + +```swift +MLNSettings.actionJournalOptions.enabled = true +``` + + + +```swift +@objc func printActionJournal() { + print("ActionJournalLog files: \(mapView.getActionJournalLogFiles())") + print("ActionJournalLog : \(mapView.getActionJournalLog())") + // print only the newest events on each call + mapView.clearActionJournalLog() + } +``` + +## Alternative + +The implementation is kept close to the core events to minimize additional locking and avoid platform-specific conversions and calls. As a result customization options and extensibility is limited. + +For greater flexibility, consider using the ``MLNMapViewDelegate`` interface. It provides hooks for most Action Journal events and allows for more customizable querying and storage of map data. However, this comes at the cost of added complexity. See [Observe Low-Level Events](./ObserverExample.md) to learn about the map events that you can listen for, which mirror the events available in the action journal. diff --git a/platform/ios/MapLibre.docc/MapLibre.md b/platform/ios/MapLibre.docc/MapLibre.md index 051fc8dff..e459c0eb6 100644 --- a/platform/ios/MapLibre.docc/MapLibre.md +++ b/platform/ios/MapLibre.docc/MapLibre.md @@ -49,9 +49,13 @@ Powerful, free and open-source mapping toolkit with full control over data sourc - - -### Advanced +### Observability - +- + +### Advanced + - ### Other Articles diff --git a/platform/ios/app-swift/Sources/ObserverExample.swift b/platform/ios/app-swift/Sources/ObserverExample.swift index 74938ac0e..167c1bfe1 100644 --- a/platform/ios/app-swift/Sources/ObserverExample.swift +++ b/platform/ios/app-swift/Sources/ObserverExample.swift @@ -4,11 +4,18 @@ import UIKit class ObserverExampleView: UIViewController, MLNMapViewDelegate { var mapView: MLNMapView! + var button: UIButton! override func viewDidLoad() { super.viewDidLoad() - mapView = MLNMapView(frame: view.bounds, styleURL: AMERICANA_STYLE) + // #-example-code(actionJournalOptions) + let options = MLNMapOptions() + options.actionJournalOptions.enabled = true + options.styleURL = AMERICANA_STYLE + mapView = MLNMapView(frame: view.bounds, options: options) + // #-end-example-code + mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] mapView.setCenter( @@ -19,8 +26,25 @@ class ObserverExampleView: UIViewController, MLNMapViewDelegate { view.addSubview(mapView) mapView.delegate = self + + button = UIButton(frame: CGRect(x: view.bounds.width / 2 - 100, y: view.bounds.height - 200, width: 200, height: 30)) + button.setTitle("Print Action Journal", for: .normal) + button.layer.cornerRadius = 15 + button.backgroundColor = UIColor(red: 0.96, green: 0.65, blue: 0.14, alpha: 1.0) + button.addTarget(self, action: #selector(printActionJournal), for: .touchUpInside) + view.addSubview(button) } + // #-example-code(ObserverExampleActionJournal) + @objc func printActionJournal() { + print("ActionJournalLog files: \(mapView.getActionJournalLogFiles())") + print("ActionJournalLog : \(mapView.getActionJournalLog())") + // print only the newest events on each call + mapView.clearActionJournalLog() + } + + // #-end-example-code + // #-example-code(ObserverExampleShaders) func mapView(_: MLNMapView, shaderWillCompile id: Int, backend: Int, defines: String) { print("A new shader is being compiled - shaderID:\(id), backend type:\(backend), program configuration:\(defines)") diff --git a/platform/ios/ios.cmake b/platform/ios/ios.cmake index 0be447426..dddced247 100644 --- a/platform/ios/ios.cmake +++ b/platform/ios/ios.cmake @@ -4,6 +4,12 @@ target_include_directories( ${PROJECT_SOURCE_DIR}/platform/ios/src ) +target_link_libraries( + mbgl-core + PRIVATE + MapLibreNative::Base::Extras::filesystem +) + file(GLOB_RECURSE IOS_SDK_SOURCE_FILES "${PROJECT_SOURCE_DIR}/platform/darwin/src/*.m" "${PROJECT_SOURCE_DIR}/platform/darwin/src/*.mm" diff --git a/platform/ios/src/MLNMapView.h b/platform/ios/src/MLNMapView.h index 89f787841..d56c07f36 100644 --- a/platform/ios/src/MLNMapView.h +++ b/platform/ios/src/MLNMapView.h @@ -5,6 +5,7 @@ #import "MLNFoundation.h" #import "MLNGeometry.h" #import "MLNMapCamera.h" +#import "MLNMapOptions.h" #import "MLNStyle.h" #import "MLNTypes.h" @@ -219,6 +220,16 @@ MLN_EXPORT */ - (instancetype)initWithFrame:(CGRect)frame styleJSON:(NSString *)styleJSON; +/** + Initializes and returns a newly allocated map view with the specified frame + and the default style. + + @param frame The frame for the view, measured in points. + @param options The map instance options + @return An initialized map view. + */ +- (instancetype)initWithFrame:(CGRect)frame options:(MLNMapOptions *)options; + // MARK: Accessing the Delegate /** @@ -2146,6 +2157,40 @@ vertically on the map. */ @property (nonatomic) MLNMapDebugMaskOptions debugMask; +/** + Get the list of action journal log files from oldest to newest. + + @return An array of log file paths. +*/ +- (NSArray *)getActionJournalLogFiles; + +/** + Get the action journal events from oldest to newest. + + Each element contains a serialized json object with the event data. + Example + `{ + "name" : "onTileAction", + "time" : "2025-04-17T13:13:13.974Z", + "styleName" : "Streets", + "styleURL" : "maptiler://maps/streets", + "event" : { + "action" : "RequestedFromNetwork", + "tileX" : 0, + "tileY" : 0, + "tileZ" : 0, + "overscaledZ" : 0, + "sourceID" : "openmaptiles" + } + }` + */ +- (NSArray *)getActionJournalLog; + +/** + Clear stored action journal events. + */ +- (void)clearActionJournalLog; + - (MLNBackendResource *)backendResource; /** diff --git a/platform/ios/src/MLNMapView.mm b/platform/ios/src/MLNMapView.mm index 4e3083854..7eeacf3d1 100644 --- a/platform/ios/src/MLNMapView.mm +++ b/platform/ios/src/MLNMapView.mm @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -68,6 +69,7 @@ #import "MLNNetworkConfiguration_Private.h" #import "MLNReachability.h" #import "MLNSettings_Private.h" +#import "MLNActionJournalOptions_Private.h" #import "MLNMapProjection.h" #include @@ -503,7 +505,7 @@ public: { MLNLogInfo(@"Starting %@ initialization.", NSStringFromClass([self class])); MLNLogDebug(@"Initializing frame: %@", NSStringFromCGRect(frame)); - [self commonInit]; + [self commonInitWithOptions:nil]; self.styleURL = nil; MLNLogInfo(@"Finalizing %@ initialization.", NSStringFromClass([self class])); } @@ -516,7 +518,7 @@ public: { MLNLogInfo(@"Starting %@ initialization.", NSStringFromClass([self class])); MLNLogDebug(@"Initializing frame: %@ styleURL: %@", NSStringFromCGRect(frame), styleURL); - [self commonInit]; + [self commonInitWithOptions:nil]; self.styleURL = styleURL; MLNLogInfo(@"Finalizing %@ initialization.", NSStringFromClass([self class])); } @@ -529,7 +531,7 @@ public: { MLNLogInfo(@"Starting %@ initialization.", NSStringFromClass([self class])); MLNLogDebug(@"Initializing frame: %@ styleJSON: %@", NSStringFromCGRect(frame), styleJSON); - [self commonInit]; + [self commonInitWithOptions:nil]; self.styleJSON = styleJSON; _initialStyleJSON = [styleJSON copy]; MLNLogInfo(@"Finalizing %@ initialization.", NSStringFromClass([self class])); @@ -537,12 +539,39 @@ public: return self; } +- (instancetype)initWithFrame:(CGRect)frame options:(MLNMapOptions *)options +{ + if (self = [super initWithFrame:frame]) + { + MLNLogInfo(@"Starting %@ initialization.", NSStringFromClass([self class])); + MLNLogDebug(@"Initializing frame: %@ with options", NSStringFromCGRect(frame)); + [self commonInitWithOptions:options]; + + if (options) + { + if (options.styleURL) { + self.styleURL = options.styleURL; + } else if (options.styleJSON) { + self.styleJSON = options.styleJSON; + _initialStyleJSON = [options.styleJSON copy]; + } else { + self.styleURL = nil; + } + } else { + self.styleURL = nil; + } + + MLNLogInfo(@"Finalizing %@ initialization.", NSStringFromClass([self class])); + } + return self; +} + - (instancetype)initWithCoder:(nonnull NSCoder *)decoder { if (self = [super initWithCoder:decoder]) { MLNLogInfo(@"Starting %@ initialization.", NSStringFromClass([self class])); - [self commonInit]; + [self commonInitWithOptions:nil]; self.styleURL = nil; MLNLogInfo(@"Finalizing %@ initialization.", NSStringFromClass([self class])); } @@ -621,8 +650,13 @@ public: return _rendererFrontend->getRenderer(); } -- (void)commonInit +- (void)commonInitWithOptions:(MLNMapOptions*)mlnMapoptions { + if (mlnMapoptions == nil) + { + mlnMapoptions = [[MLNMapOptions alloc] init]; + } + _opaque = NO; // setup accessibility @@ -683,8 +717,11 @@ public: resourceOptions.withApiKey([apiKey UTF8String]); } + const mbgl::util::ActionJournalOptions& actionJournalOptions = [mlnMapoptions.actionJournalOptions getCoreOptions]; + NSAssert(!_mbglMap, @"_mbglMap should be NULL"); - _mbglMap = std::make_unique(*_rendererFrontend, *_mbglView, mapOptions, resourceOptions, clientOptions); + _mbglMap = std::make_unique(*_rendererFrontend, *_mbglView, mapOptions, + resourceOptions, clientOptions, actionJournalOptions); // start paused if launch into the background if (background) { @@ -7525,6 +7562,50 @@ static void *windowScreenContext = &windowScreenContext; _mbglMap->triggerRepaint(); } +- (NSArray*)getActionJournalLogFiles +{ + const auto& actionJournal = _mbglMap->getActionJournal(); + if (!actionJournal) { + return nil; + } + + const auto& files = actionJournal->getLogFiles(); + NSMutableArray* objcFiles = [NSMutableArray new]; + + for (const auto& file : files) { + [objcFiles addObject:[NSString stringWithUTF8String:file.c_str()]]; + } + + return objcFiles; +} + +- (NSArray*)getActionJournalLog +{ + const auto& actionJournal = _mbglMap->getActionJournal(); + if (!actionJournal) { + return nil; + } + + const auto& log = actionJournal->getLog(); + NSMutableArray* objcLog = [NSMutableArray new]; + + for (const auto& event : log) { + [objcLog addObject:[NSString stringWithUTF8String:event.c_str()]]; + } + + return objcLog; +} + +- (void)clearActionJournalLog +{ + const auto& actionJournal = _mbglMap->getActionJournal(); + if (!actionJournal) { + return; + } + + actionJournal->clearLog(); +} + - (MLNBackendResource *)backendResource { return _mbglView->getObject(); } diff --git a/platform/macos/src/MLNMapView.h b/platform/macos/src/MLNMapView.h index d3db2ea6f..ef4ac5e12 100644 --- a/platform/macos/src/MLNMapView.h +++ b/platform/macos/src/MLNMapView.h @@ -4,6 +4,7 @@ #import "MLNFoundation.h" #import "MLNGeometry.h" +#import "MLNMapOptions.h" #import "MLNStyle.h" #import "MLNTypes.h" @@ -90,6 +91,16 @@ MLN_EXPORT IB_DESIGNABLE @interface MLNMapView : NSView */ - (instancetype)initWithFrame:(NSRect)frame styleURL:(nullable NSURL *)styleURL; +/** + Initializes and returns a newly allocated map view with the specified frame + and the default style. + + @param frame The frame for the view, measured in points. + @param options The map instance options + @return An initialized map view. + */ +- (instancetype)initWithFrame:(CGRect)frame options:(MLNMapOptions *)options; + // MARK: Accessing the Delegate /** @@ -1270,6 +1281,17 @@ around the returned camera object if it were set as the receiver’s camera. */ @property (nonatomic) MLNMapDebugMaskOptions debugMask; +/** + Get the action journal events from oldest to newest. + Each element contains a serialized json object with the event data. + */ +- (NSArray *)getActionJournalLog; + +/** + Clear stored action journal events. + */ +- (void)clearActionJournalLog; + @end NS_ASSUME_NONNULL_END diff --git a/platform/macos/src/MLNMapView.mm b/platform/macos/src/MLNMapView.mm index 6ef3e8863..cdaf39dd2 100644 --- a/platform/macos/src/MLNMapView.mm +++ b/platform/macos/src/MLNMapView.mm @@ -34,6 +34,7 @@ #import #import #import +#import #import #import #import @@ -59,6 +60,7 @@ #import "MLNNetworkConfiguration_Private.h" #import "MLNLoggingConfiguration_Private.h" #import "MLNReachability.h" +#import "MLNActionJournalOptions_Private.h" #import "MLNSettings_Private.h" #import @@ -221,7 +223,7 @@ public: if (self = [super initWithFrame:frameRect]) { MLNLogInfo(@"Starting %@ initialization.", NSStringFromClass([self class])); MLNLogDebug(@"Initializing frame: %@", NSStringFromRect(frameRect)); - [self commonInit]; + [self commonInitWithOptions:nil]; self.styleURL = nil; MLNLogInfo(@"Finalizing %@ initialization.", NSStringFromClass([self class])); } @@ -232,17 +234,38 @@ public: if (self = [super initWithFrame:frame]) { MLNLogInfo(@"Starting %@ initialization.", NSStringFromClass([self class])); MLNLogDebug(@"Initializing frame: %@ styleURL: %@", NSStringFromRect(frame), styleURL); - [self commonInit]; + [self commonInitWithOptions:nil]; self.styleURL = styleURL; MLNLogInfo(@"Finalizing %@ initialization.", NSStringFromClass([self class])); } return self; } +- (instancetype)initWithFrame:(CGRect)frame options:(MLNMapOptions *)options +{ + if (self = [super initWithFrame:frame]) + { + MLNLogInfo(@"Starting %@ initialization.", NSStringFromClass([self class])); + MLNLogDebug(@"Initializing frame: %@ with options", NSStringFromRect(frame)); + [self commonInitWithOptions:options]; + + if (options) { + if (options.styleURL) { + self.styleURL = options.styleURL; + } + } else { + self.styleURL = nil; + } + + MLNLogInfo(@"Finalizing %@ initialization.", NSStringFromClass([self class])); + } + return self; +} + - (instancetype)initWithCoder:(nonnull NSCoder *)decoder { if (self = [super initWithCoder:decoder]) { MLNLogInfo(@"Starting %@ initialization.", NSStringFromClass([self class])); - [self commonInit]; + [self commonInitWithOptions:nil]; MLNLogInfo(@"Finalizing %@ initialization.", NSStringFromClass([self class])); } return self; @@ -262,7 +285,13 @@ public: return @[@"camera", @"debugMask"]; } -- (void)commonInit { +- (void)commonInitWithOptions:(MLNMapOptions*)mlnMapoptions +{ + if (mlnMapoptions == nil) + { + mlnMapoptions = [[MLNMapOptions alloc] init]; + } + [MLNNetworkConfiguration sharedManager]; _isTargetingInterfaceBuilder = NSProcessInfo.processInfo.mgl_isInterfaceBuilderDesignablesAgent; @@ -308,7 +337,8 @@ public: resourceOptions.withApiKey([apiKey UTF8String]); } - _mbglMap = std::make_unique(*_rendererFrontend, *_mbglView, mapOptions, resourceOptions, clientOptions); + const mbgl::util::ActionJournalOptions& actionJournalOptions = [mlnMapoptions.actionJournalOptions getCoreOptions]; + _mbglMap = std::make_unique(*_rendererFrontend, *_mbglView, mapOptions, resourceOptions, clientOptions, actionJournalOptions); // Notify map object when network reachability status changes. _reachability = [MLNReachability reachabilityForInternetConnection]; @@ -3195,4 +3225,31 @@ public: _mbglMap->setDebug(options); } +- (NSArray*)getActionJournalLog +{ + const auto& actionJournal = _mbglMap->getActionJournal(); + if (!actionJournal) { + return nil; + } + + const auto& log = actionJournal->getLog(); + NSMutableArray* objcLog = [NSMutableArray new]; + + for (const auto& event : log) { + [objcLog addObject:[NSString stringWithUTF8String:event.c_str()]]; + } + + return objcLog; +} + +- (void)clearActionJournalLog +{ + const auto& actionJournal = _mbglMap->getActionJournal(); + if (!actionJournal) { + return; + } + + actionJournal->clearLog(); +} + @end diff --git a/platform/macos/src/MLNMapViewDelegate.h b/platform/macos/src/MLNMapViewDelegate.h index 07be47b8a..9dcf94141 100644 --- a/platform/macos/src/MLNMapViewDelegate.h +++ b/platform/macos/src/MLNMapViewDelegate.h @@ -11,6 +11,7 @@ NS_ASSUME_NONNULL_BEGIN @class MLNPolyline; @class MLNShape; @class MLNStyle; +@class MLNSource; @protocol MLNAnnotation; /** diff --git a/shaders/generate_shader_code.mjs b/shaders/generate_shader_code.mjs index 1ed98ebaf..78e97f301 100644 --- a/shaders/generate_shader_code.mjs +++ b/shaders/generate_shader_code.mjs @@ -220,6 +220,7 @@ const root = path.dirname(import.meta.dirname); const outLocation = args.out ? args.out : root; const shaderRoot = path.join(root, "shaders"); const outputRoot = path.join(outLocation, "include/mbgl/shaders"); +const cppOutputRoot = path.join(outLocation, "src/mbgl/shaders"); let generatedHeaders = []; let shaderNames = []; @@ -317,4 +318,21 @@ struct ShaderSource { } // namespace mbgl `); +// Generate shader_source.cpp +fs.writeFileSync(path.join(cppOutputRoot, "shader_source.cpp"), +`${generatedHeader} +#include +#include + +namespace mbgl { + +using namespace shaders; + +MBGL_DEFINE_ENUM(BuiltIn, { +{BuiltIn::None, "None"},${shaderNames.map(name => `\n{BuiltIn::` + name + `, "` + name + `"}`)} +}); + +} // namespace mbgl +`); + console.log("Shaders generated!"); diff --git a/src/mbgl/map/map.cpp b/src/mbgl/map/map.cpp index 9bad545b1..cff580adb 100644 --- a/src/mbgl/map/map.cpp +++ b/src/mbgl/map/map.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include @@ -31,19 +32,30 @@ Map::Map(RendererFrontend& frontend, MapObserver& observer, const MapOptions& mapOptions, const ResourceOptions& resourceOptions, - const ClientOptions& clientOptions) + const ClientOptions& clientOptions, + const util::ActionJournalOptions& actionJournalOptions) : impl(std::make_unique(frontend, observer, FileSourceManager::get() ? std::shared_ptr(FileSourceManager::get()->getFileSource( ResourceLoader, resourceOptions, clientOptions)) : nullptr, - mapOptions)) {} + mapOptions)) { + if (actionJournalOptions.enabled()) { + impl->actionJournal = std::make_unique(*this, actionJournalOptions); + } +} -Map::Map(std::unique_ptr impl_) - : impl(std::move(impl_)) {} +Map::Map(std::unique_ptr impl_, const util::ActionJournalOptions& actionJournalOptions) + : impl(std::move(impl_)) { + if (actionJournalOptions.enabled()) { + impl->actionJournal = std::make_unique(*this, actionJournalOptions); + } +} -Map::~Map() = default; +Map::~Map() { + impl->actionJournal.reset(); +} void Map::renderStill(StillImageCallback callback) { if (!callback) { @@ -527,4 +539,12 @@ FreeCameraOptions Map::getFreeCameraOptions() const { return impl->transform.getFreeCameraOptions(); } +ClientOptions Map::getClientOptions() const { + return impl->fileSource ? impl->fileSource->getClientOptions() : ClientOptions(); +} + +const std::unique_ptr& Map::getActionJournal() { + return impl->actionJournal; +} + } // namespace mbgl diff --git a/src/mbgl/map/map_impl.cpp b/src/mbgl/map/map_impl.cpp index 6b5a6454f..bca50cd62 100644 --- a/src/mbgl/map/map_impl.cpp +++ b/src/mbgl/map/map_impl.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include namespace mbgl { @@ -47,7 +49,7 @@ Map::Impl::Impl(RendererFrontend& frontend_, const MapOptions& mapOptions) : observer(observer_), rendererFrontend(frontend_), - transform(observer, mapOptions.constrainMode(), mapOptions.viewportMode()), + transform(*this, mapOptions.constrainMode(), mapOptions.viewportMode()), mode(mapOptions.mapMode()), pixelRatio(mapOptions.pixelRatio()), crossSourceCollisions(mapOptions.crossSourceCollisions()), @@ -66,10 +68,38 @@ Map::Impl::~Impl() { rendererFrontend.reset(); }; +void Map::Impl::onCameraWillChange(MapObserver::CameraChangeMode cameraMode) { + observer.onCameraWillChange(cameraMode); + + if (actionJournal) { + actionJournal->impl->onCameraWillChange(cameraMode); + } +} + +void Map::Impl::onCameraIsChanging() { + observer.onCameraIsChanging(); + + if (actionJournal) { + actionJournal->impl->onCameraIsChanging(); + } +} + +void Map::Impl::onCameraDidChange(MapObserver::CameraChangeMode cameraMode) { + observer.onCameraDidChange(cameraMode); + + if (actionJournal) { + actionJournal->impl->onCameraDidChange(cameraMode); + } +} + // MARK: - Map::Impl StyleObserver void Map::Impl::onSourceChanged(style::Source& source) { observer.onSourceChanged(source); + + if (actionJournal) { + actionJournal->impl->onSourceChanged(source); + } } void Map::Impl::onUpdate() { @@ -108,6 +138,10 @@ void Map::Impl::onStyleLoading() { loading = true; rendererFullyLoaded = false; observer.onWillStartLoadingMap(); + + if (actionJournal) { + actionJournal->impl->onWillStartLoadingMap(); + } } void Map::Impl::onStyleLoaded() { @@ -117,8 +151,13 @@ void Map::Impl::onStyleLoaded() { if (LayerManager::annotationsEnabled) { annotationManager.onStyleLoaded(); } + observer.onDidFinishLoadingStyle(); + if (actionJournal) { + actionJournal->impl->onDidFinishLoadingStyle(); + } + #if !defined(NDEBUG) logStyleDependencies(EventSeverity::Info, Event::Style, *style); #endif @@ -145,18 +184,34 @@ void Map::Impl::onStyleError(std::exception_ptr error) { } observer.onDidFailLoadingMap(type, description); + + if (actionJournal) { + actionJournal->impl->onDidFailLoadingMap(type, description); + } } void Map::Impl::onSpriteLoaded(const std::optional& sprite) { observer.onSpriteLoaded(sprite); + + if (actionJournal) { + actionJournal->impl->onSpriteLoaded(sprite); + } } void Map::Impl::onSpriteError(const std::optional& sprite, std::exception_ptr ex) { observer.onSpriteError(sprite, ex); + + if (actionJournal) { + actionJournal->impl->onSpriteError(sprite, ex); + } } void Map::Impl::onSpriteRequested(const std::optional& sprite) { observer.onSpriteRequested(sprite); + + if (actionJournal) { + actionJournal->impl->onSpriteRequested(sprite); + } } // MARK: - Map::Impl RendererObserver @@ -175,6 +230,10 @@ void Map::Impl::onResourceError(std::exception_ptr error) { void Map::Impl::onWillStartRenderingFrame() { if (mode == MapMode::Continuous) { observer.onWillStartRenderingFrame(); + + if (actionJournal) { + actionJournal->impl->onWillStartRenderingFrame(); + } } } @@ -186,16 +245,23 @@ void Map::Impl::onDidFinishRenderingFrame(RenderMode renderMode, rendererFullyLoaded = renderMode == RenderMode::Full; if (mode == MapMode::Continuous) { - observer.onDidFinishRenderingFrame({MapObserver::RenderMode(renderMode), - needsRepaint, - placemenChanged, - frameEncodingTime, - frameRenderingTime}); + const MapObserver::RenderFrameStatus frameStatus{ + MapObserver::RenderMode(renderMode), needsRepaint, placemenChanged, frameEncodingTime, frameRenderingTime}; + + observer.onDidFinishRenderingFrame(frameStatus); + + if (actionJournal) { + actionJournal->impl->onDidFinishRenderingFrame(frameStatus); + } if (needsRepaint || transform.inTransition()) { onUpdate(); } else if (rendererFullyLoaded) { observer.onDidBecomeIdle(); + + if (actionJournal) { + actionJournal->impl->onDidBecomeIdle(); + } } } else if (stillImageRequest && rendererFullyLoaded) { auto request = std::move(stillImageRequest); @@ -206,15 +272,28 @@ void Map::Impl::onDidFinishRenderingFrame(RenderMode renderMode, void Map::Impl::onWillStartRenderingMap() { if (mode == MapMode::Continuous) { observer.onWillStartRenderingMap(); + + if (actionJournal) { + actionJournal->impl->onWillStartRenderingMap(); + } } } void Map::Impl::onDidFinishRenderingMap() { if (mode == MapMode::Continuous && loading) { observer.onDidFinishRenderingMap(MapObserver::RenderMode::Full); + + if (actionJournal) { + actionJournal->impl->onDidFinishRenderingMap(MapObserver::RenderMode::Full); + } + if (loading) { loading = false; observer.onDidFinishLoadingMap(); + + if (actionJournal) { + actionJournal->impl->onDidFinishLoadingMap(); + } } } }; @@ -226,7 +305,13 @@ void Map::Impl::jumpTo(const CameraOptions& camera) { } void Map::Impl::onStyleImageMissing(const std::string& id, const std::function& done) { - if (!style->getImage(id)) observer.onStyleImageMissing(id); + if (!style->getImage(id)) { + observer.onStyleImageMissing(id); + + if (actionJournal) { + actionJournal->impl->onStyleImageMissing(id); + } + } done(); onUpdate(); @@ -242,40 +327,72 @@ void Map::Impl::onRemoveUnusedStyleImages(const std::vector& unused void Map::Impl::onRegisterShaders(gfx::ShaderRegistry& registry) { observer.onRegisterShaders(registry); + + if (actionJournal) { + actionJournal->impl->onRegisterShaders(registry); + } } void Map::Impl::onPreCompileShader(shaders::BuiltIn shaderID, gfx::Backend::Type type, const std::string& additionalDefines) { observer.onPreCompileShader(shaderID, type, additionalDefines); + + if (actionJournal) { + actionJournal->impl->onPreCompileShader(shaderID, type, additionalDefines); + } } void Map::Impl::onPostCompileShader(shaders::BuiltIn shaderID, gfx::Backend::Type type, const std::string& additionalDefines) { observer.onPostCompileShader(shaderID, type, additionalDefines); + + if (actionJournal) { + actionJournal->impl->onPostCompileShader(shaderID, type, additionalDefines); + } } void Map::Impl::onShaderCompileFailed(shaders::BuiltIn shaderID, gfx::Backend::Type type, const std::string& additionalDefines) { observer.onShaderCompileFailed(shaderID, type, additionalDefines); + + if (actionJournal) { + actionJournal->impl->onShaderCompileFailed(shaderID, type, additionalDefines); + } } void Map::Impl::onGlyphsLoaded(const FontStack& fontStack, const GlyphRange& ranges) { observer.onGlyphsLoaded(fontStack, ranges); + + if (actionJournal) { + actionJournal->impl->onGlyphsLoaded(fontStack, ranges); + } } void Map::Impl::onGlyphsError(const FontStack& fontStack, const GlyphRange& ranges, std::exception_ptr ex) { observer.onGlyphsError(fontStack, ranges, ex); + + if (actionJournal) { + actionJournal->impl->onGlyphsError(fontStack, ranges, ex); + } } void Map::Impl::onGlyphsRequested(const FontStack& fontStack, const GlyphRange& ranges) { observer.onGlyphsRequested(fontStack, ranges); + + if (actionJournal) { + actionJournal->impl->onGlyphsRequested(fontStack, ranges); + } } void Map::Impl::onTileAction(TileOperation op, const OverscaledTileID& id, const std::string& sourceID) { observer.onTileAction(op, id, sourceID); + + if (actionJournal) { + actionJournal->impl->onTileAction(op, id, sourceID); + } } } // namespace mbgl diff --git a/src/mbgl/map/map_impl.hpp b/src/mbgl/map/map_impl.hpp index d093b30f2..1a8f6c4e1 100644 --- a/src/mbgl/map/map_impl.hpp +++ b/src/mbgl/map/map_impl.hpp @@ -23,6 +23,10 @@ namespace gfx { class ShaderRegistry; } // namespace gfx +namespace util { +class ActionJournal; +} // namespace util + struct StillImageRequest { StillImageRequest(Map::StillImageCallback&& callback_) : callback(std::move(callback_)) {} @@ -30,11 +34,16 @@ struct StillImageRequest { Map::StillImageCallback callback; }; -class Map::Impl final : public style::Observer, public RendererObserver { +class Map::Impl final : public TransformObserver, public style::Observer, public RendererObserver { public: Impl(RendererFrontend&, MapObserver&, std::shared_ptr, const MapOptions&); ~Impl() final; + // TransformObserver + void onCameraWillChange(MapObserver::CameraChangeMode) final; + void onCameraIsChanging() final; + void onCameraDidChange(MapObserver::CameraChangeMode) final; + // StyleObserver void onSourceChanged(style::Source&) final; void onUpdate() final; @@ -69,6 +78,7 @@ public: MapObserver& observer; RendererFrontend& rendererFrontend; + std::unique_ptr actionJournal; Transform transform; diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index dbf23ae98..72408bb6e 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -40,7 +40,7 @@ static double _normalizeAngle(double angle, double anchorAngle) { return angle; } -Transform::Transform(MapObserver& observer_, ConstrainMode constrainMode, ViewportMode viewportMode) +Transform::Transform(TransformObserver& observer_, ConstrainMode constrainMode, ViewportMode viewportMode) : observer(observer_), state(constrainMode, viewportMode) {} diff --git a/src/mbgl/map/transform.hpp b/src/mbgl/map/transform.hpp index eac0e8fb9..4828f372f 100644 --- a/src/mbgl/map/transform.hpp +++ b/src/mbgl/map/transform.hpp @@ -16,14 +16,28 @@ namespace mbgl { +class TransformObserver { +public: + virtual ~TransformObserver() = default; + + static TransformObserver& nullObserver() { + static TransformObserver observer; + return observer; + } + + virtual void onCameraWillChange(MapObserver::CameraChangeMode) {} + virtual void onCameraIsChanging() {} + virtual void onCameraDidChange(MapObserver::CameraChangeMode) {} +}; + class Transform : private util::noncopyable { public: - Transform(MapObserver& = MapObserver::nullObserver(), + Transform(TransformObserver& = TransformObserver::nullObserver(), ConstrainMode = ConstrainMode::HeightOnly, ViewportMode = ViewportMode::Default); Transform(const TransformState& state_) - : observer(MapObserver::nullObserver()), + : observer(TransformObserver::nullObserver()), state(state_) {} // Map view @@ -119,7 +133,7 @@ public: void setFreeCameraOptions(const FreeCameraOptions& options); private: - MapObserver& observer; + TransformObserver& observer; TransformState state; void startTransition(const CameraOptions&, diff --git a/src/mbgl/shaders/shader_source.cpp b/src/mbgl/shaders/shader_source.cpp new file mode 100644 index 000000000..e14fee616 --- /dev/null +++ b/src/mbgl/shaders/shader_source.cpp @@ -0,0 +1,44 @@ +// Generated code, do not modify this file! +#include +#include + +namespace mbgl { + +using namespace shaders; + +MBGL_DEFINE_ENUM(BuiltIn, + {{BuiltIn::None, "None"}, + {BuiltIn::Prelude, "Prelude"}, + {BuiltIn::ClippingMaskProgram, "ClippingMaskProgram"}, + {BuiltIn::BackgroundShader, "BackgroundShader"}, + {BuiltIn::BackgroundPatternShader, "BackgroundPatternShader"}, + {BuiltIn::CircleShader, "CircleShader"}, + {BuiltIn::CollisionBoxShader, "CollisionBoxShader"}, + {BuiltIn::CollisionCircleShader, "CollisionCircleShader"}, + {BuiltIn::CustomGeometryShader, "CustomGeometryShader"}, + {BuiltIn::CustomSymbolIconShader, "CustomSymbolIconShader"}, + {BuiltIn::DebugShader, "DebugShader"}, + {BuiltIn::FillShader, "FillShader"}, + {BuiltIn::FillOutlineShader, "FillOutlineShader"}, + {BuiltIn::FillPatternShader, "FillPatternShader"}, + {BuiltIn::FillOutlinePatternShader, "FillOutlinePatternShader"}, + {BuiltIn::FillOutlineTriangulatedShader, "FillOutlineTriangulatedShader"}, + {BuiltIn::FillExtrusionShader, "FillExtrusionShader"}, + {BuiltIn::FillExtrusionPatternShader, "FillExtrusionPatternShader"}, + {BuiltIn::HeatmapShader, "HeatmapShader"}, + {BuiltIn::HeatmapTextureShader, "HeatmapTextureShader"}, + {BuiltIn::HillshadePrepareShader, "HillshadePrepareShader"}, + {BuiltIn::HillshadeShader, "HillshadeShader"}, + {BuiltIn::LineShader, "LineShader"}, + {BuiltIn::LineGradientShader, "LineGradientShader"}, + {BuiltIn::LinePatternShader, "LinePatternShader"}, + {BuiltIn::LocationIndicatorShader, "LocationIndicatorShader"}, + {BuiltIn::LocationIndicatorTexturedShader, "LocationIndicatorTexturedShader"}, + {BuiltIn::LineSDFShader, "LineSDFShader"}, + {BuiltIn::RasterShader, "RasterShader"}, + {BuiltIn::SymbolIconShader, "SymbolIconShader"}, + {BuiltIn::SymbolSDFShader, "SymbolSDFShader"}, + {BuiltIn::SymbolTextAndIconShader, "SymbolTextAndIconShader"}, + {BuiltIn::WideVectorShader, "WideVectorShader"}}); + +} // namespace mbgl diff --git a/src/mbgl/tile/tile_operation.cpp b/src/mbgl/tile/tile_operation.cpp new file mode 100644 index 000000000..e20866d28 --- /dev/null +++ b/src/mbgl/tile/tile_operation.cpp @@ -0,0 +1,18 @@ +#include +#include + +namespace mbgl { + +MBGL_DEFINE_ENUM(TileOperation, + { + {TileOperation::RequestedFromCache, "RequestedFromCache"}, + {TileOperation::RequestedFromNetwork, "RequestedFromNetwork"}, + {TileOperation::LoadFromNetwork, "LoadFromNetwork"}, + {TileOperation::LoadFromCache, "LoadFromCache"}, + {TileOperation::StartParse, "StartParse"}, + {TileOperation::EndParse, "EndParse"}, + {TileOperation::Error, "Error"}, + {TileOperation::Cancelled, "Cancelled"}, + {TileOperation::NullOp, "NullOp"}, + }); +} // namespace mbgl diff --git a/src/mbgl/util/action_journal.cpp b/src/mbgl/util/action_journal.cpp new file mode 100644 index 000000000..532771e81 --- /dev/null +++ b/src/mbgl/util/action_journal.cpp @@ -0,0 +1,33 @@ +#include +#include + +namespace mbgl { +namespace util { + +ActionJournal::ActionJournal(const Map& map, const ActionJournalOptions& options) + : impl(std::make_unique(map, options)) { + impl->onMapCreate(); +} + +ActionJournal::~ActionJournal() { + impl->onMapDestroy(); +} + +std::string ActionJournal::getLogDirectory() const { + return impl->getLogDirectory(); +} + +std::vector ActionJournal::getLogFiles() const { + return impl->getLogFiles(); +} + +std::vector ActionJournal::getLog() { + return impl->getLog(); +} + +void ActionJournal::clearLog() { + impl->clearLog(); +} + +} // namespace util +} // namespace mbgl diff --git a/src/mbgl/util/action_journal_impl.cpp b/src/mbgl/util/action_journal_impl.cpp new file mode 100644 index 000000000..a0adebc17 --- /dev/null +++ b/src/mbgl/util/action_journal_impl.cpp @@ -0,0 +1,575 @@ +#include +#include +#include +#include + +#include +#include +#include + +#include + +#ifdef __APPLE__ +#include +#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + +#define USE_GHC_FILESYSTEM + +#endif +#endif + +#ifdef USE_GHC_FILESYSTEM + +#include +namespace mbgl { +namespace filesystem = ghc::filesystem; +} + +#else + +#include +namespace mbgl { +namespace filesystem = std::filesystem; +} + +#endif + +namespace mbgl { +namespace util { + +constexpr auto ACTION_JOURNAL_DIRECTORY_NAME = "action_journal"; +constexpr auto ACTION_JOURNAL_FILE_NAME = "action_journal"; +constexpr auto ACTION_JOURNAL_FILE_EXTENSION = "log"; + +class MapEnvironmentSnapshot { +public: + MapEnvironmentSnapshot(const ActionJournal::Impl& journal) + : time(std::chrono::time_point_cast(std::chrono::system_clock::now())), + clientName(journal.getMap().getClientOptions().name()), + clientVersion(journal.getMap().getClientOptions().version()), + styleName(journal.getMap().getStyle().getName()), + styleURL(journal.getMap().getStyle().getURL()) {} + + const auto& getTime() const { return time; } + const auto& getClientName() const { return clientName; } + const auto& getClientVersion() const { return clientVersion; } + const auto& getStyleName() const { return styleName; } + const auto& getStyleURL() const { return styleURL; } + +protected: + const std::chrono::time_point time; + const std::string clientName; + const std::string clientVersion; + const std::string styleName; + const std::string styleURL; +}; + +class ActionJournalEvent { +public: + ActionJournalEvent(const rapidjson::GenericStringRef& name, const MapEnvironmentSnapshot& env) + : json(rapidjson::kObjectType), + eventJson(rapidjson::kObjectType) { + json.AddMember("name", name, json.GetAllocator()); + json.AddMember("time", iso8601(env.getTime()), json.GetAllocator()); + + if (!env.getClientName().empty()) { + json.AddMember("clientName", env.getClientName(), json.GetAllocator()); + } + + if (!env.getClientVersion().empty()) { + json.AddMember("clientVersion", env.getClientVersion(), json.GetAllocator()); + } + + if (!env.getStyleName().empty()) { + json.AddMember("styleName", env.getStyleName(), json.GetAllocator()); + } + + if (!env.getStyleURL().empty()) { + json.AddMember("styleURL", env.getStyleURL(), json.GetAllocator()); + } + } + + ~ActionJournalEvent() = default; + + template + ActionJournalEvent& addEvent(const rapidjson::GenericStringRef& key, const T& value) { + eventJson.AddMember(key, value, json.GetAllocator()); + return *this; + } + + ActionJournalEvent& addEventStringArray(const rapidjson::GenericStringRef& key, + const std::vector& value) { + rapidjson::Value arrayJson(rapidjson::kArrayType); + + for (const auto& elem : value) { + rapidjson::Value elemJson(rapidjson::kStringType); + elemJson.SetString(elem.c_str(), static_cast(elem.size())); + arrayJson.PushBack(elemJson, json.GetAllocator()); + } + + eventJson.AddMember(key, arrayJson, json.GetAllocator()); + return *this; + } + + template + ActionJournalEvent& addEventEnum(const rapidjson::GenericStringRef& key, const T& value) { + eventJson.AddMember(key, rapidjson::StringRef(Enum::toString(value)), json.GetAllocator()); + return *this; + } + + std::string toString() { + if (!eventJson.ObjectEmpty()) { + json.AddMember("event", eventJson, json.GetAllocator()); + } + + rapidjson::StringBuffer jsonBuffer; + rapidjson::Writer jsonWriter(jsonBuffer); + + json.Accept(jsonWriter); + + return jsonBuffer.GetString(); + } + + rapidjson::Document json; + rapidjson::Value eventJson; +}; + +ActionJournal::Impl::Impl(const Map& map_, const ActionJournalOptions& options_) + : map(map_), + options(options_), + scheduler(Scheduler::GetSequenced()) { + assert(!options.path().empty()); + assert(options.logFileSize() > 0); + assert(options.logFileCount() > 1); + + options.withPath((mbgl::filesystem::canonical(options.path()) / ACTION_JOURNAL_DIRECTORY_NAME).generic_string()); + + if (!openFile(detectFiles(), false)) { + Log::Error(Event::General, "Failed to open Action Journal file"); + } +} + +ActionJournal::Impl::~Impl() { + flush(); +} + +std::string ActionJournal::Impl::getLogDirectory() const { + return options.path(); +} + +std::vector ActionJournal::Impl::getLogFiles() const { + std::set files; + for (const auto& entry : mbgl::filesystem::directory_iterator(options.path())) { + if (!entry.is_regular_file()) { + continue; + } + + files.emplace(mbgl::filesystem::canonical(entry.path()).generic_string()); + } + + return std::vector(files.begin(), files.end()); +} + +std::vector ActionJournal::Impl::getLog() { + std::lock_guard lock(fileMutex); + std::vector logEvents; + + const auto readFile = [&](std::fstream& file) { + if (!file) { + return; + } + + for (std::string line; std::getline(file, line);) { + logEvents.emplace_back(line); + } + + file.clear(); + }; + + // read current file + if (currentFile && currentFileIndex == 0) { + currentFile.seekg(std::ios::beg); + readFile(currentFile); + return logEvents; + } + + const uint32_t maxFileIndex = currentFile ? currentFileIndex - 1 : detectFiles(); + + // read previous files + for (uint32_t fileIndex = 0; fileIndex <= maxFileIndex; ++fileIndex) { + const auto& filepath = getFilepath(fileIndex); + std::fstream file(filepath, std::fstream::in); + readFile(file); + } + + if (currentFile) { + currentFile.seekg(std::ios::beg); + readFile(currentFile); + } + + return logEvents; +} + +void ActionJournal::Impl::clearLog() { + std::lock_guard lock(fileMutex); + + // close current file + currentFile.close(); + + currentFileIndex = 0; + currentFileSize = 0; + + if (!mbgl::filesystem::remove_all(options.path())) { + Log::Error(Event::General, "Failed to clear ActionJournal"); + } + + if (!openFile(0, false)) { + Log::Error(Event::General, "Failed to open Action Journal file"); + } +} + +void ActionJournal::Impl::flush() { + scheduler->waitForEmpty(); +} + +std::string ActionJournal::Impl::getDirectoryName() { + return ACTION_JOURNAL_DIRECTORY_NAME; +} + +void ActionJournal::Impl::onCameraWillChange(CameraChangeMode mode) { + scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this)]() { + log(ActionJournalEvent("onCameraWillChange", env).addEvent("cameraMode", static_cast(mode))); + }); +} + +void ActionJournal::Impl::onCameraDidChange(CameraChangeMode mode) { + scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this)]() { + log(ActionJournalEvent("onCameraDidChange", env).addEvent("cameraMode", static_cast(mode))); + }); +} + +void ActionJournal::Impl::onWillStartLoadingMap() { + scheduler->schedule( + [=, this, env = MapEnvironmentSnapshot(*this)]() { log(ActionJournalEvent("onWillStartLoadingMap", env)); }); +} + +void ActionJournal::Impl::onDidFinishLoadingMap() { + scheduler->schedule( + [=, this, env = MapEnvironmentSnapshot(*this)]() { log(ActionJournalEvent("onDidFinishLoadingMap", env)); }); +} + +void ActionJournal::Impl::onDidFailLoadingMap(MapLoadError error, const std::string& errorStr) { + scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this)]() { + log(ActionJournalEvent("onDidFailLoadingMap", env) + .addEvent("error", errorStr) + .addEvent("code", static_cast(error))); + }); +} + +void ActionJournal::Impl::onWillStartRenderingMap() { + scheduler->schedule( + [=, this, env = MapEnvironmentSnapshot(*this)]() { log(ActionJournalEvent("onWillStartRenderingMap", env)); }); +} + +void ActionJournal::Impl::onDidFinishRenderingMap(RenderMode) { + scheduler->schedule( + [=, this, env = MapEnvironmentSnapshot(*this)]() { log(ActionJournalEvent("onDidFinishRenderingMap", env)); }); +} + +void ActionJournal::Impl::onDidFinishLoadingStyle() { + scheduler->schedule( + [=, this, env = MapEnvironmentSnapshot(*this)]() { log(ActionJournalEvent("onDidFinishLoadingStyle", env)); }); +} + +void ActionJournal::Impl::onSourceChanged(style::Source& source) { + scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this), type = source.getType(), id = source.getID()]() { + log(ActionJournalEvent("onSourceChanged", env).addEventEnum("type", type).addEvent("id", id)); + }); +} + +void ActionJournal::Impl::onDidBecomeIdle() { + scheduler->schedule( + [=, this, env = MapEnvironmentSnapshot(*this)]() { log(ActionJournalEvent("onDidBecomeIdle", env)); }); +} + +void ActionJournal::Impl::onStyleImageMissing(const std::string& id) { + scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this)]() { + log(ActionJournalEvent("onStyleImageMissing", env).addEvent("id", id)); + }); +} + +void ActionJournal::Impl::onRegisterShaders(gfx::ShaderRegistry&) { + scheduler->schedule( + [=, this, env = MapEnvironmentSnapshot(*this)]() { log(ActionJournalEvent("onRegisterShaders", env)); }); +} + +void ActionJournal::Impl::onPreCompileShader(shaders::BuiltIn id, gfx::Backend::Type backend, const std::string&) { + scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this)]() { + log(ActionJournalEvent("onPreCompileShader", env) + .addEventEnum("shader", id) + .addEvent("backend", static_cast(backend))); + }); +} + +void ActionJournal::Impl::onPostCompileShader(shaders::BuiltIn id, gfx::Backend::Type backend, const std::string&) { + scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this)]() { + log(ActionJournalEvent("onPostCompileShader", env) + .addEventEnum("shader", id) + .addEvent("backend", static_cast(backend))); + }); +} + +void ActionJournal::Impl::onShaderCompileFailed(shaders::BuiltIn id, + gfx::Backend::Type backend, + const std::string& defines) { + scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this)]() { + log(ActionJournalEvent("onShaderCompileFailed", env) + .addEventEnum("shader", id) + .addEvent("backend", static_cast(backend)) + .addEvent("defines", defines)); + }); +} + +void ActionJournal::Impl::onGlyphsLoaded(const FontStack& fonts, const GlyphRange& range) { + scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this)]() { + log(ActionJournalEvent("onGlyphsLoaded", env) + .addEventStringArray("fonts", fonts) + .addEvent("rangeStart", range.first) + .addEvent("rangeEnd", range.second)); + }); +} + +void ActionJournal::Impl::onGlyphsError(const FontStack& fonts, const GlyphRange& range, std::exception_ptr error) { + scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this)]() { + ActionJournalEvent event("onGlyphsError", env); + + event.addEventStringArray("fonts", fonts); + event.addEvent("rangeStart", range.first); + event.addEvent("rangeEnd", range.second); + + if (error) { + try { + std::rethrow_exception(error); + } catch (const std::exception& e) { + event.addEvent("error", std::string(e.what())); + } + } + + log(event); + }); +} + +void ActionJournal::Impl::onGlyphsRequested(const FontStack& fonts, const GlyphRange& range) { + scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this)]() { + log(ActionJournalEvent("onGlyphsRequested", env) + .addEventStringArray("fonts", fonts) + .addEvent("rangeStart", range.first) + .addEvent("rangeEnd", range.second)); + }); +} + +void ActionJournal::Impl::onTileAction(TileOperation op, const OverscaledTileID& id, const std::string& sourceID) { + scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this)]() { + log(ActionJournalEvent("onTileAction", env) + .addEventEnum("action", op) + .addEvent("tileX", id.canonical.x) + .addEvent("tileY", id.canonical.y) + .addEvent("tileZ", id.canonical.z) + .addEvent("overscaledZ", id.overscaledZ) + .addEvent("sourceID", sourceID)); + }); +} + +void ActionJournal::Impl::onSpriteLoaded(const std::optional& sprite) { + scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this)]() { + ActionJournalEvent event("onSpriteLoaded", *this); + + if (sprite) { + event.addEvent("id", sprite.value().id); + event.addEvent("url", sprite.value().spriteURL); + } + + log(event); + }); +} + +void ActionJournal::Impl::onSpriteError(const std::optional& sprite, std::exception_ptr error) { + scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this)]() { + ActionJournalEvent event("onSpriteError", env); + + if (sprite) { + event.addEvent("id", sprite.value().id); + event.addEvent("url", sprite.value().spriteURL); + } + + if (error) { + try { + std::rethrow_exception(error); + } catch (const std::exception& e) { + event.addEvent("error", std::string(e.what())); + } + } + + log(event); + }); +} + +void ActionJournal::Impl::onSpriteRequested(const std::optional& sprite) { + scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this)]() { + ActionJournalEvent event("onSpriteRequested", env); + + if (sprite) { + event.addEvent("id", sprite.value().id); + event.addEvent("url", sprite.value().spriteURL); + } + + log(event); + }); +} + +void ActionJournal::Impl::onMapCreate() { + scheduler->schedule( + [=, this, env = MapEnvironmentSnapshot(*this)]() { log(ActionJournalEvent("onMapCreate", env)); }); +} + +void ActionJournal::Impl::onMapDestroy() { + scheduler->schedule( + [=, this, env = MapEnvironmentSnapshot(*this)]() { log(ActionJournalEvent("onMapDestroy", env)); }); +} + +void ActionJournal::Impl::log(ActionJournalEvent& value) { + logToFile(value.toString()); +} + +void ActionJournal::Impl::log(ActionJournalEvent&& value) { + logToFile(value.toString()); +} + +std::string ActionJournal::Impl::getFilepath(uint32_t fileIndex) const { + return options.path() + "/" + ACTION_JOURNAL_FILE_NAME + "." + std::to_string(fileIndex) + "." + + ACTION_JOURNAL_FILE_EXTENSION; +} + +uint32_t ActionJournal::Impl::detectFiles() const { + if (!mbgl::filesystem::exists(options.path())) { + return 0; + } + + std::map existingFiles; + + const std::regex fileRegex(std::string(R"(.*\.([0-9]+)\.)") + ACTION_JOURNAL_FILE_EXTENSION); + std::smatch fileMatch; + + for (const auto& entry : mbgl::filesystem::directory_iterator(options.path())) { + if (!entry.is_regular_file()) { + continue; + } + + const std::string& path = entry.path().string(); + if (std::regex_match(path, fileMatch, fileRegex)) { + if (fileMatch.size() == 2) { + existingFiles.emplace(std::stoi(fileMatch[1].str()), path); + } + } + } + + if (existingFiles.empty()) { + return 0; + } + + // removing extra files (old files or due to the file count changing) + for (auto it = existingFiles.begin(); existingFiles.size() > options.logFileCount();) { + mbgl::filesystem::remove(it->second); + it = existingFiles.erase(it); + } + + uint32_t expectedIndex = 0; + + // validate file index + for (const auto& file : existingFiles) { + if (file.first != expectedIndex) { + mbgl::filesystem::rename(file.second, getFilepath(expectedIndex)); + } + + ++expectedIndex; + } + + return expectedIndex - 1; +} + +uint32_t ActionJournal::Impl::rollFiles() { + // delete the oldest file + const auto& oldestFilepath = getFilepath(0); + if (mbgl::filesystem::exists(oldestFilepath)) { + mbgl::filesystem::remove(oldestFilepath); + } + + // rename the rest + uint32_t expectedIndex = 0; + for (uint32_t index = 1; index < options.logFileCount(); ++index) { + const auto& filepath = getFilepath(index); + if (mbgl::filesystem::exists(filepath)) { + mbgl::filesystem::rename(filepath, getFilepath(expectedIndex++)); + } + } + + return expectedIndex; +} + +bool ActionJournal::Impl::openFile(uint32_t fileIndex, bool truncate) { + assert(fileIndex < options.logFileCount()); + + if (!mbgl::filesystem::exists(options.path())) { + mbgl::filesystem::create_directories(options.path()); + } + + const auto& filepath = getFilepath(fileIndex); + const std::ios::openmode openMode = (truncate ? std::ios::trunc : std::ios::ate | std::ios::app) | + std::fstream::out | std::fstream::in; + + currentFile.open(filepath, openMode); + + if (currentFile.is_open()) { + currentFileIndex = fileIndex; + currentFileSize = mbgl::filesystem::file_size(filepath); + return true; + } + + return false; +} + +bool ActionJournal::Impl::prepareFile(size_t size) { + if (currentFileSize + size <= options.logFileSize() && currentFile) { + currentFileSize += size; + return true; + } + + // else roll file + currentFile.close(); + + if (currentFileIndex + 1 >= options.logFileCount()) { + currentFileIndex = rollFiles(); + } else { + ++currentFileIndex; + } + + return openFile(currentFileIndex, true); +} + +void ActionJournal::Impl::logToFile(const std::string& value) { + if (value.empty()) { + return; + } + + std::lock_guard lock(fileMutex); + + if (!prepareFile(value.size() + 1)) { + return; + } + + currentFile << value << "\n"; + currentFile.flush(); +} + +} // namespace util +} // namespace mbgl diff --git a/src/mbgl/util/action_journal_impl.hpp b/src/mbgl/util/action_journal_impl.hpp new file mode 100644 index 000000000..ce07bc3a1 --- /dev/null +++ b/src/mbgl/util/action_journal_impl.hpp @@ -0,0 +1,105 @@ +#pragma once + +#include +#include +#include + +#include + +namespace mbgl { + +class Map; +class Scheduler; + +namespace util { + +class ActionJournalEvent; + +class ActionJournal::Impl : public MapObserver { +public: + Impl(const Map& map, const ActionJournalOptions& options); + ~Impl(); + + const Map& getMap() const { return map; } + std::string getLogDirectory() const; + std::vector getLogFiles() const; + std::vector getLog(); + void clearLog(); + void flush(); + + static std::string getDirectoryName(); + + // Logged events + + // MapObserver + void onCameraWillChange(CameraChangeMode) override; + void onCameraDidChange(CameraChangeMode) override; + void onWillStartLoadingMap() override; + void onDidFinishLoadingMap() override; + void onDidFailLoadingMap(MapLoadError, const std::string&) override; + void onWillStartRenderingMap() override; + void onDidFinishRenderingMap(RenderMode) override; + void onDidFinishLoadingStyle() override; + void onSourceChanged(style::Source&) override; + void onDidBecomeIdle() override; + void onStyleImageMissing(const std::string&) override; + void onRegisterShaders(gfx::ShaderRegistry&) override; + void onPreCompileShader(shaders::BuiltIn, gfx::Backend::Type, const std::string&) override; + void onPostCompileShader(shaders::BuiltIn, gfx::Backend::Type, const std::string&) override; + void onShaderCompileFailed(shaders::BuiltIn, gfx::Backend::Type, const std::string&) override; + void onGlyphsLoaded(const FontStack&, const GlyphRange&) override; + void onGlyphsError(const FontStack&, const GlyphRange&, std::exception_ptr) override; + void onGlyphsRequested(const FontStack&, const GlyphRange&) override; + void onTileAction(TileOperation, const OverscaledTileID&, const std::string&) override; + void onSpriteLoaded(const std::optional&) override; + void onSpriteError(const std::optional&, std::exception_ptr) override; + void onSpriteRequested(const std::optional&) override; + + void onMapCreate(); + void onMapDestroy(); + +protected: + void log(ActionJournalEvent& value); + void log(ActionJournalEvent&& value); + + // file operations + + // generate the file path for the given index -> action_journal.{fileIndex}.log + std::string getFilepath(uint32_t fileIndex) const; + + // detect and validate files at `ActionJournalOptions.path()/ACTION_JOURNAL_DIRECTORY_NAME` + uint32_t detectFiles() const; + + // create a new log file with the highest unused index + // delete the oldest one if `logFileCount` number of files was reached + // rename the remaining files to match the index chain if a file was deleted + // + // action_journal.0.log -> deleted + // action_journal.1.log -> action_journal.0.log + // ... + // action_journal.{logFileCount - 1}.log -> action_journal.{logFileCount - 2}.log + // action_journal.{logFileCount - 1}.log -> new file + uint32_t rollFiles(); + + // open the given index file + bool openFile(uint32_t fileIndex, bool truncate = false); + // check if the current file can log `size` bytes of info and roll files if needed + bool prepareFile(size_t size); + // log string to file and flush + void logToFile(const std::string& value); + +protected: + const Map& map; + + ActionJournalOptions options; + + const std::shared_ptr scheduler; + + std::mutex fileMutex; + std::fstream currentFile; + uint32_t currentFileIndex{0}; + size_t currentFileSize{0}; +}; + +} // namespace util +} // namespace mbgl diff --git a/src/mbgl/util/chrono.cpp b/src/mbgl/util/chrono.cpp index d815771e7..ad42852af 100644 --- a/src/mbgl/util/chrono.cpp +++ b/src/mbgl/util/chrono.cpp @@ -49,6 +49,22 @@ std::string iso8601(Timestamp timestamp) { return buffer; } +std::string iso8601(std::chrono::time_point timestamp) { + std::time_t time = std::chrono::system_clock::to_time_t(timestamp); + std::tm info; + _gmtime(&time, &info); + + long long ms = + std::chrono::duration_cast(timestamp - std::chrono::system_clock::from_time_t(time)).count() % + 1000; + + char buffer[sizeof("yyyy-mm-ddThh:mm:ss.000Z")]; + const std::size_t offset = std::strftime(buffer, sizeof(buffer), "%FT%T", &info); + snprintf(buffer + offset, sizeof(buffer) - offset, ".%03lldZ", ms); + + return buffer; +} + Timestamp parseTimestamp(const char *timestamp) { return std::chrono::time_point_cast(std::chrono::system_clock::from_time_t(parse_date(timestamp))); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 754dc9e2b..60fc46969 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -85,6 +85,7 @@ add_library( ${PROJECT_SOURCE_DIR}/test/tile/tile_coordinate.test.cpp ${PROJECT_SOURCE_DIR}/test/tile/tile_id.test.cpp ${PROJECT_SOURCE_DIR}/test/tile/vector_tile.test.cpp + ${PROJECT_SOURCE_DIR}/test/util/action_journal.test.cpp ${PROJECT_SOURCE_DIR}/test/util/async_task.test.cpp ${PROJECT_SOURCE_DIR}/test/util/bounding_volumes.test.cpp ${PROJECT_SOURCE_DIR}/test/util/camera.test.cpp diff --git a/test/map/transform.test.cpp b/test/map/transform.test.cpp index c2efcf9d7..e98b82c7d 100644 --- a/test/map/transform.test.cpp +++ b/test/map/transform.test.cpp @@ -178,7 +178,7 @@ TEST(Transform, UnwrappedLatLng) { } TEST(Transform, ConstrainHeightOnly) { - Transform transform(MapObserver::nullObserver(), ConstrainMode::HeightOnly); + Transform transform(TransformObserver::nullObserver(), ConstrainMode::HeightOnly); transform.resize({2, 2}); transform.jumpTo(CameraOptions().withCenter(LatLngBounds::world().southwest()).withZoom(util::MAX_ZOOM)); @@ -191,7 +191,7 @@ TEST(Transform, ConstrainHeightOnly) { } TEST(Transform, ConstrainWidthAndHeight) { - Transform transform(MapObserver::nullObserver(), ConstrainMode::WidthAndHeight); + Transform transform(TransformObserver::nullObserver(), ConstrainMode::WidthAndHeight); transform.resize({2, 2}); transform.jumpTo(CameraOptions().withCenter(LatLngBounds::world().southwest()).withZoom(util::MAX_ZOOM)); @@ -541,7 +541,7 @@ TEST(Transform, IsPanning) { } TEST(Transform, DefaultTransform) { - struct TransformObserver : public mbgl::MapObserver { + struct TransformObserver : public mbgl::TransformObserver { void onCameraWillChange(MapObserver::CameraChangeMode) final { cameraWillChangeCallback(); }; void onCameraDidChange(MapObserver::CameraChangeMode) final { cameraDidChangeCallback(); }; diff --git a/test/tile/tile_coordinate.test.cpp b/test/tile/tile_coordinate.test.cpp index 695087ff6..cc3908241 100644 --- a/test/tile/tile_coordinate.test.cpp +++ b/test/tile/tile_coordinate.test.cpp @@ -12,7 +12,7 @@ using namespace mbgl; TEST(TileCoordinate, FromLatLng) { size_t changeCount = 0; - struct TransformObserver : public mbgl::MapObserver { + struct TransformObserver : public mbgl::TransformObserver { void onCameraWillChange(MapObserver::CameraChangeMode mode) final { if (mode == MapObserver::CameraChangeMode::Immediate && cameraWillChangeImmediateCallback) { cameraWillChangeImmediateCallback(); diff --git a/test/util/action_journal.test.cpp b/test/util/action_journal.test.cpp new file mode 100644 index 000000000..5a131099d --- /dev/null +++ b/test/util/action_journal.test.cpp @@ -0,0 +1,431 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include + +using namespace mbgl; +using namespace mbgl::util; +using namespace ::testing; + +class ActionJournalTest { +public: + class MapAdapter : public Map { + public: + MapAdapter(RendererFrontend& frontend, + MapObserver& observer, + std::shared_ptr fileSource, + const MapOptions& options, + const util::ActionJournalOptions& actionJournalOptions) + : Map(std::make_unique(frontend, observer, std::move(fileSource), options), + actionJournalOptions) {} + + Map::Impl& getImpl() { return *impl; } + }; + + ActionJournalOptions options; + StubMapObserver observer; + std::shared_ptr fileSource; + std::unique_ptr frontend; + std::unique_ptr map; + + util::RunLoop runLoop; + + ActionJournalTest(const ActionJournalOptions& options_, MapMode mode = MapMode::Static) + : options(options_) { + clear(); + createMap(mode); + } + + ~ActionJournalTest() { + map.reset(); + frontend.reset(); + fileSource.reset(); + clear(); + } + + void createMap(MapMode mode = MapMode::Static) { + fileSource = std::make_shared(ResourceOptions::Default(), + ClientOptions().withName("ActionJournalTest").withVersion("1.0")); + frontend = std::make_unique(1.0f); + map = std::make_unique( + *frontend, observer, fileSource, MapOptions().withMapMode(mode).withSize(frontend->getSize()), options); + } + + std::filesystem::path getDirectoryPath() { + return std::filesystem::canonical(options.path()) / ActionJournal::Impl::getDirectoryName(); + } + + void clear() { std::filesystem::remove_all(getDirectoryPath()); } +}; + +TEST(ActionJournal, Create) { + ActionJournalTest test(ActionJournalOptions().enable().withPath(".")); + + test.map->getActionJournal()->impl->flush(); + + // map creation logs `onMapCreate` + EXPECT_EQ(test.map->getActionJournal()->getLogDirectory(), test.getDirectoryPath().generic_string()); + EXPECT_TRUE(std::filesystem::exists(test.map->getActionJournal()->getLogDirectory())); + EXPECT_EQ(test.map->getActionJournal()->getLogFiles().size(), 1u); + + const auto log = test.map->getActionJournal()->getLog(); + EXPECT_EQ(log.size(), 1u); + EXPECT_THAT(log.front(), HasSubstr("onMapCreate")); +} + +TEST(ActionJournal, Clear) { + ActionJournalTest test(ActionJournalOptions().enable().withPath(".")); + + test.map->getActionJournal()->impl->flush(); + test.map->getActionJournal()->clearLog(); + + EXPECT_TRUE(std::filesystem::exists(test.map->getActionJournal()->getLogDirectory())); + EXPECT_EQ(test.map->getActionJournal()->getLogFiles().size(), 1u); + EXPECT_THAT(test.map->getActionJournal()->getLog(), IsEmpty()); +} + +TEST(ActionJournal, ReadPreviousSession) { + // generate `onMapCreate` event + ActionJournalTest test(ActionJournalOptions().enable().withPath(".")); + + // generate `onMapDestroy` event + test.map.reset(); + + // generate `onMapCreate` event + test.createMap(); + + test.map->getActionJournal()->impl->flush(); + + const auto log = test.map->getActionJournal()->getLog(); + EXPECT_EQ(log.size(), 3u); + EXPECT_THAT(log, ElementsAre(HasSubstr("onMapCreate"), HasSubstr("onMapDestroy"), HasSubstr("onMapCreate"))); +} + +TEST(ActionJournal, RollFiles) { + // limit the number of files and the size of the file + ActionJournalTest test(ActionJournalOptions().enable().withPath(".").withLogFileSize(1).withLogFileCount(2)); + + // generate multiple log events by loading a style and rendering + test.map->getStyle().loadJSON(util::read_file("test/fixtures/api/empty.json")); + + test.runLoop.runOnce(); + test.frontend->render(*test.map); + + test.map->getActionJournal()->impl->flush(); + + // check if the files roll and respect the limits + const auto files = test.map->getActionJournal()->getLogFiles(); + EXPECT_TRUE(std::filesystem::exists(test.map->getActionJournal()->getLogDirectory())); + EXPECT_EQ(files.size(), 2u); + EXPECT_EQ(test.map->getActionJournal()->getLog().size(), 2u); + + // the newest event should be `onMapDestroy` + test.map.reset(); + EXPECT_THAT(util::read_file(files.back()), HasSubstr("onMapDestroy")); +} + +namespace { + +template +void validateEventList(ActionJournalTest& test, + std::vector>>&& generatedEvents, + EventMethod&& eventGenerator, + Args&&... args) { + test.map->getActionJournal()->clearLog(); + (test.map->getImpl().*eventGenerator)(std::forward(args)...); + test.map->getActionJournal()->impl->flush(); + + const auto& log = test.map->getActionJournal()->getLog(); + EXPECT_EQ(log.size(), generatedEvents.size()); + + auto logIt = log.begin(); + for (const auto& generatedEvent : generatedEvents) { + mbgl::JSDocument document; + document.Parse(*logIt++); + + EXPECT_FALSE(document.HasParseError()); + EXPECT_TRUE(document.IsObject()); + + const auto& json = document.GetObject(); + + EXPECT_TRUE(json.HasMember("name")); + EXPECT_TRUE(json["name"].IsString()); + EXPECT_STREQ(json["name"].GetString(), generatedEvent.first); + + EXPECT_TRUE(json.HasMember("time")); + EXPECT_TRUE(json["time"].IsString()); + + EXPECT_TRUE(json.HasMember("clientName")); + EXPECT_TRUE(json["clientName"].IsString()); + EXPECT_STREQ(json["clientName"].GetString(), test.fileSource->getClientOptions().name().c_str()); + + EXPECT_TRUE(json.HasMember("clientVersion")); + EXPECT_TRUE(json["clientVersion"].IsString()); + EXPECT_STREQ(json["clientVersion"].GetString(), test.fileSource->getClientOptions().version().c_str()); + + // `MatchesRegex(R"(\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d\d\dZ)")` fails on linux gcc + // https://github.com/google/googletest/issues/3084 + // `MatchesRegex(R"([0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]\.[0-9][0-9][0-9]Z)"))` + // fails on windows CI + // using std::regex for now + std::cmatch match; + const std::regex timeRegex(R"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)"); + EXPECT_TRUE(std::regex_match(json["time"].GetString(), match, timeRegex)); + + if (generatedEvent.second) { + EXPECT_TRUE(json.HasMember("event")); + EXPECT_TRUE(json["event"].IsObject()); + + generatedEvent.second(json["event"].GetObject()); + } + } +} + +template +void validateEvent(ActionJournalTest& test, + const char* generatedEvent, + std::function&& checkOutput, + EventMethod&& eventGenerator, + Args&&... args) { + validateEventList(test, {{generatedEvent, checkOutput}}, eventGenerator, std::forward(args)...); +} + +} // namespace + +TEST(ActionJournal, ValidateEvents) { + ActionJournalTest test( + ActionJournalOptions().enable().withPath(".").withLogFileCount(2).withLogFileSize(1024 * 1024 * 5), + MapMode::Continuous); + + test.map->getStyle().loadJSON(util::read_file("test/fixtures/api/empty.json")); + test.map->getActionJournal()->impl->flush(); + + const auto testShaderRegistry = std::make_unique(); + const auto testSource = std::make_unique("testSource"); + const auto testSprite = style::Sprite("testID", "testURL"); + const auto testExceptionMessage = "test error"; + const auto testException = std::make_exception_ptr(std::runtime_error(testExceptionMessage)); + const auto testFontStack = FontStack{"Roboto Condensed Italic", "Noto Sans Italic"}; + const auto testGlyphRange = GlyphRange(0, 255); + + validateEvent( + test, + "onCameraWillChange", + [&](const mbgl::JSValue& json) { + EXPECT_TRUE(json.HasMember("cameraMode")); + EXPECT_TRUE(json["cameraMode"].IsInt()); + EXPECT_EQ(json["cameraMode"].GetInt(), static_cast(MapObserver::CameraChangeMode::Immediate)); + }, + &TransformObserver::onCameraWillChange, + MapObserver::CameraChangeMode::Immediate); + + validateEvent( + test, + "onCameraDidChange", + [&](const mbgl::JSValue& json) { + EXPECT_TRUE(json.HasMember("cameraMode")); + EXPECT_TRUE(json["cameraMode"].IsInt()); + EXPECT_EQ(json["cameraMode"].GetInt(), static_cast(MapObserver::CameraChangeMode::Immediate)); + }, + &TransformObserver::onCameraDidChange, + MapObserver::CameraChangeMode::Immediate); + + validateEvent(test, "onWillStartLoadingMap", nullptr, &style::Observer::onStyleLoading); + validateEvent(test, "onDidFinishLoadingStyle", nullptr, &style::Observer::onStyleLoaded); + + validateEvent( + test, + "onSourceChanged", + [&](const mbgl::JSValue& json) { + EXPECT_TRUE(json.HasMember("type")); + EXPECT_TRUE(json["type"].IsString()); + EXPECT_EQ(Enum::toEnum(json["type"].GetString()), style::SourceType::GeoJSON); + + EXPECT_TRUE(json.HasMember("id")); + EXPECT_TRUE(json["id"].IsString()); + EXPECT_STREQ(json["id"].GetString(), testSource->getID().c_str()); + }, + &style::Observer::onSourceChanged, + *testSource); + + validateEvent(test, "onRegisterShaders", nullptr, &RendererObserver::onRegisterShaders, *testShaderRegistry); + + validateEvent( + test, + "onDidFailLoadingMap", + [&](const mbgl::JSValue& json) { + EXPECT_TRUE(json.HasMember("error")); + EXPECT_TRUE(json["error"].IsString()); + EXPECT_STREQ(json["error"].GetString(), testExceptionMessage); + + EXPECT_TRUE(json.HasMember("code")); + EXPECT_TRUE(json["code"].IsInt()); + EXPECT_EQ(json["code"].GetInt(), static_cast(MapLoadError::UnknownError)); + }, + &style::Observer::onStyleError, + testException); + + const auto checkSprite = [&](const mbgl::JSValue& json) { + EXPECT_TRUE(json.HasMember("id")); + EXPECT_TRUE(json["id"].IsString()); + EXPECT_EQ(json["id"].GetString(), testSprite.id); + + EXPECT_TRUE(json.HasMember("url")); + EXPECT_TRUE(json["url"].IsString()); + EXPECT_EQ(json["url"].GetString(), testSprite.spriteURL); + }; + + validateEvent(test, "onSpriteLoaded", checkSprite, &style::Observer::onSpriteLoaded, testSprite); + validateEvent(test, "onSpriteRequested", checkSprite, &style::Observer::onSpriteRequested, testSprite); + validateEvent( + test, + "onSpriteError", + [&](const mbgl::JSValue& json) { + checkSprite(json); + + EXPECT_TRUE(json.HasMember("error")); + EXPECT_TRUE(json["error"].IsString()); + EXPECT_STREQ(json["error"].GetString(), testExceptionMessage); + }, + &style::Observer::onSpriteError, + testSprite, + testException); + + validateEvent(test, "onWillStartRenderingMap", nullptr, &RendererObserver::onWillStartRenderingMap); + + // `onDidFinishRenderingMap` and `onDidFinishLoadingMap` are generated by the same callback + validateEventList(test, + {{"onDidFinishRenderingMap", nullptr}, {"onDidFinishLoadingMap", nullptr}}, + &RendererObserver::onDidFinishRenderingMap); + + validateEvent( + test, + "onStyleImageMissing", + [&](const mbgl::JSValue& json) { + EXPECT_TRUE(json.HasMember("id")); + EXPECT_TRUE(json["id"].IsString()); + EXPECT_STREQ(json["id"].GetString(), "image"); + }, + &RendererObserver::onStyleImageMissing, + "image", + []() {}); + + const auto checkShader = [&](const mbgl::JSValue& json) { + EXPECT_TRUE(json.HasMember("shader")); + EXPECT_TRUE(json["shader"].IsString()); + EXPECT_EQ(Enum::toEnum(json["shader"].GetString()), shaders::BuiltIn::None); + + EXPECT_TRUE(json.HasMember("backend")); + EXPECT_TRUE(json["backend"].IsInt()); + EXPECT_EQ(json["backend"].GetInt(), static_cast(gfx::Backend::Type::OpenGL)); + }; + + validateEvent(test, + "onPreCompileShader", + checkShader, + &RendererObserver::onPreCompileShader, + shaders::BuiltIn::None, + gfx::Backend::Type::OpenGL, + "defines"); + + validateEvent(test, + "onPostCompileShader", + checkShader, + &RendererObserver::onPostCompileShader, + shaders::BuiltIn::None, + gfx::Backend::Type::OpenGL, + "defines"); + + validateEvent( + test, + "onShaderCompileFailed", + [&](const mbgl::JSValue& json) { + checkShader(json); + + EXPECT_TRUE(json.HasMember("defines")); + EXPECT_TRUE(json["defines"].IsString()); + EXPECT_STREQ(json["defines"].GetString(), "defines"); + }, + &RendererObserver::onShaderCompileFailed, + shaders::BuiltIn::None, + gfx::Backend::Type::OpenGL, + "defines"); + + const auto checkGlyphs = [&](const mbgl::JSValue& json) { + EXPECT_TRUE(json.HasMember("fonts")); + EXPECT_TRUE(json["fonts"].IsArray()); + EXPECT_EQ(json["fonts"].GetArray().Size(), testFontStack.size()); + + auto fontIt = testFontStack.begin(); + for (const auto& font : json["fonts"].GetArray()) { + EXPECT_TRUE(font.IsString()); + EXPECT_EQ(font.GetString(), *fontIt++); + } + + EXPECT_TRUE(json.HasMember("rangeStart")); + EXPECT_TRUE(json["rangeStart"].IsInt()); + EXPECT_EQ(json["rangeStart"].GetInt(), testGlyphRange.first); + + EXPECT_TRUE(json.HasMember("rangeEnd")); + EXPECT_TRUE(json["rangeEnd"].IsInt()); + EXPECT_EQ(json["rangeEnd"].GetInt(), testGlyphRange.second); + }; + + validateEvent( + test, "onGlyphsLoaded", checkGlyphs, &RendererObserver::onGlyphsLoaded, testFontStack, testGlyphRange); + validateEvent( + test, "onGlyphsRequested", checkGlyphs, &RendererObserver::onGlyphsRequested, testFontStack, testGlyphRange); + + validateEvent( + test, + "onGlyphsError", + [&](const mbgl::JSValue& json) { + checkGlyphs(json); + + EXPECT_TRUE(json.HasMember("error")); + EXPECT_TRUE(json["error"].IsString()); + EXPECT_STREQ(json["error"].GetString(), testExceptionMessage); + }, + &RendererObserver::onGlyphsError, + testFontStack, + testGlyphRange, + testException); + + validateEvent(test, + "onTileAction", + nullptr, + &RendererObserver::onTileAction, + TileOperation::StartParse, + OverscaledTileID(0, 0, 0), + ""); + + const auto onDidFinishRenderingFrame = + static_cast( + &RendererObserver::onDidFinishRenderingFrame); + + validateEvent(test, + "onDidBecomeIdle", + nullptr, + onDidFinishRenderingFrame, + RendererObserver::RenderMode::Full, + false, + false, + 0.0, + 0.0); +} diff --git a/vendor/BUILD.bazel b/vendor/BUILD.bazel index 240e2a2e5..73e83019f 100644 --- a/vendor/BUILD.bazel +++ b/vendor/BUILD.bazel @@ -17,6 +17,7 @@ cc_library( "maplibre-native-base/deps/supercluster.hpp/include/*.hpp", "maplibre-native-base/deps/variant/include/mapbox/*.hpp", "maplibre-native-base/extras/expected-lite/include/**/*.hpp", + "maplibre-native-base/extras/filesystem/include/ghc/*.hpp", "maplibre-native-base/extras/kdbush.hpp/include/*.hpp", "maplibre-native-base/extras/rapidjson/include/**/*.h", "maplibre-native-base/include/mapbox/**/*.hpp", @@ -37,6 +38,7 @@ cc_library( "maplibre-native-base/deps/variant/include", "maplibre-native-base/extras/args", "maplibre-native-base/extras/expected-lite/include", + "maplibre-native-base/extras/filesystem/include", "maplibre-native-base/extras/kdbush.hpp/include", "maplibre-native-base/extras/rapidjson/include", "maplibre-native-base/include", diff --git a/vendor/maplibre-native-base.cmake b/vendor/maplibre-native-base.cmake index 86e978a5c..633cc9269 100644 --- a/vendor/maplibre-native-base.cmake +++ b/vendor/maplibre-native-base.cmake @@ -20,6 +20,15 @@ set_target_properties( INTERFACE_MAPLIBRE_LICENSE ${CMAKE_CURRENT_LIST_DIR}/maplibre-native-base/extras/expected-lite/LICENSE.txt ) +set_target_properties( + maplibre-native-base-extras-filesystem + PROPERTIES + INTERFACE_MAPLIBRE_NAME "filesystem" + INTERFACE_MAPLIBRE_URL "https://github.com/gulrak/filesystem" + INTERFACE_MAPLIBRE_AUTHOR "Steffen Schümann" + INTERFACE_MAPLIBRE_LICENSE ${CMAKE_CURRENT_LIST_DIR}/maplibre-native-base/extras/filesystem/LICENSE.txt +) + set_target_properties( maplibre-native-base-supercluster.hpp PROPERTIES diff --git a/vendor/maplibre-native-base/extras/CMakeLists.txt b/vendor/maplibre-native-base/extras/CMakeLists.txt index f86143fab..502a55c81 100644 --- a/vendor/maplibre-native-base/extras/CMakeLists.txt +++ b/vendor/maplibre-native-base/extras/CMakeLists.txt @@ -21,6 +21,7 @@ add_library(maplibre-native-base-extras INTERFACE) add_library(MapLibreNative::Base::Extras ALIAS maplibre-native-base-extras) maplibre_native_base_extras_add_library(args ${CMAKE_CURRENT_LIST_DIR}/args) +maplibre_native_base_extras_add_library(filesystem ${CMAKE_CURRENT_LIST_DIR}/filesystem/include) maplibre_native_base_extras_add_library(expected-lite ${CMAKE_CURRENT_LIST_DIR}/expected-lite/include) maplibre_native_base_extras_add_library(kdbush.hpp ${CMAKE_CURRENT_LIST_DIR}/kdbush.hpp/include) diff --git a/vendor/maplibre-native-base/extras/filesystem b/vendor/maplibre-native-base/extras/filesystem new file mode 160000 index 000000000..9fda7b0af --- /dev/null +++ b/vendor/maplibre-native-base/extras/filesystem @@ -0,0 +1 @@ +Subproject commit 9fda7b0afbd0640f482f4aea8720a8c0afd18740