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 <bart@emeel.net>
This commit is contained in:
Adrian Cojocaru
2025-05-27 16:15:56 +03:00
committed by GitHub
parent 5ec8430fa3
commit 08eb760acd
66 changed files with 2773 additions and 75 deletions

View File

@@ -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

3
.gitmodules vendored
View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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",

View File

@@ -13,6 +13,7 @@
#include <mbgl/map/projection_mode.hpp>
#include <mbgl/storage/resource_options.hpp>
#include <mbgl/util/client_options.hpp>
#include <mbgl/util/action_journal_options.hpp>
#include <cstdint>
#include <string>
@@ -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<util::ActionJournal>& getActionJournal();
protected:
class Impl;
const std::unique_ptr<Impl> impl;
// For testing only.
Map(std::unique_ptr<Impl>);
Map(std::unique_ptr<Impl>, const util::ActionJournalOptions& = {});
};
} // namespace mbgl

View File

@@ -0,0 +1,68 @@
#pragma once
#include <mbgl/map/map.hpp>
#include <string>
#include <vector>
#include <memory>
#include <filesystem>
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<std::string> 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<std::string> getLog();
/**
* @brief Clear stored action journal events by removing logs files.
*/
void clearLog();
class Impl;
const std::unique_ptr<Impl> impl;
};
} // namespace util
} // namespace mbgl

View File

@@ -0,0 +1,101 @@
#pragma once
#include <string>
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

View File

@@ -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<std::chrono::system_clock, Milliseconds>);
Timestamp parseTimestamp(const char *);
Timestamp parseTimestamp(int32_t timestamp);

View File

@@ -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

View File

@@ -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);

View File

@@ -0,0 +1,43 @@
#include "native_map_options.hpp"
namespace mbgl {
namespace android {
void NativeMapOptions::registerNative(jni::JNIEnv &env) {
jni::Class<NativeMapOptions>::Singleton(env);
}
NativeMapOptions::NativeMapOptions(jni::JNIEnv &_env, const jni::Object<NativeMapOptions> &_obj) {}
NativeMapOptions::~NativeMapOptions() {}
util::ActionJournalOptions NativeMapOptions::getActionJournalOptions(jni::JNIEnv &env,
const jni::Object<NativeMapOptions> &obj) {
auto &javaClass = jni::Class<NativeMapOptions>::Singleton(env);
auto enabledField = javaClass.GetField<jni::jboolean>(env, "actionJournalEnabled");
auto pathField = javaClass.GetField<jni::String>(env, "actionJournalPath");
auto logFileSizeField = javaClass.GetField<jni::jlong>(env, "actionJournalLogFileSize");
auto logFileCountField = javaClass.GetField<jni::jlong>(env, "actionJournalLogFileCount");
return util::ActionJournalOptions()
.enable(obj.Get(env, enabledField))
.withPath(jni::Make<std::string>(env, obj.Get(env, pathField)))
.withLogFileSize(obj.Get(env, logFileSizeField))
.withLogFileCount(obj.Get(env, logFileCountField));
}
float NativeMapOptions::pixelRatio(jni::JNIEnv &env, const jni::Object<NativeMapOptions> &obj) {
auto &javaClass = jni::Class<NativeMapOptions>::Singleton(env);
auto pixelRatioField = javaClass.GetField<float>(env, "pixelRatio");
return obj.Get(env, pixelRatioField);
}
bool NativeMapOptions::crossSourceCollisions(jni::JNIEnv &env, const jni::Object<NativeMapOptions> &obj) {
auto &javaClass = jni::Class<NativeMapOptions>::Singleton(env);
auto crossSourceCollisionsField = javaClass.GetField<jni::jboolean>(env, "crossSourceCollisions");
return obj.Get(env, crossSourceCollisionsField);
}
} // namespace android
} // namespace mbgl

View File

@@ -0,0 +1,25 @@
#pragma once
#include <jni.h>
#include <jni/jni.hpp>
#include <mbgl/util/action_journal_options.hpp>
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<NativeMapOptions>&);
virtual ~NativeMapOptions();
static util::ActionJournalOptions getActionJournalOptions(jni::JNIEnv&, const jni::Object<NativeMapOptions>&);
static float pixelRatio(jni::JNIEnv&, const jni::Object<NativeMapOptions>&);
static bool crossSourceCollisions(jni::JNIEnv&, const jni::Object<NativeMapOptions>&);
};
} // namespace android
} // namespace mbgl

View File

@@ -16,6 +16,7 @@
#include <mbgl/map/map.hpp>
#include <mbgl/map/map_options.hpp>
#include <mbgl/math/minmax.hpp>
#include <mbgl/util/action_journal.hpp>
#include <mbgl/util/constants.hpp>
#include <mbgl/util/event.hpp>
#include <mbgl/util/exception.hpp>
@@ -62,11 +63,10 @@ NativeMapView::NativeMapView(jni::JNIEnv& _env,
const jni::Object<NativeMapView>& _obj,
const jni::Object<FileSource>& jFileSource,
const jni::Object<MapRenderer>& jMapRenderer,
jni::jfloat pixelRatio_,
jni::jboolean crossSourceCollisions_)
const jni::Object<NativeMapOptions>& 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<mbgl::Map>(*rendererFrontend,
*this,
options,
mbgl::android::FileSource::getSharedResourceOptions(_env, jFileSource),
mbgl::android::FileSource::getSharedClientOptions(_env, jFileSource));
map = std::make_unique<mbgl::Map>(
*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<jni::Array<jni::String>> NativeMapView::getActionJournalLogFiles(JNIEnv& env) {
const auto& actionJournal = map->getActionJournal();
if (!actionJournal) {
return jni::Local<jni::Array<jni::String>>();
}
const auto& files = actionJournal->getLogFiles();
auto jFiles = jni::Array<jni::String>::New(env, files.size());
uint32_t index = 0;
for (const auto& file : files) {
jFiles.Set(env, index++, jni::Make<jni::String>(env, file));
}
return jFiles;
}
jni::Local<jni::Array<jni::String>> NativeMapView::getActionJournalLog(JNIEnv& env) {
const auto& actionJournal = map->getActionJournal();
if (!actionJournal) {
return jni::Local<jni::Array<jni::String>>();
}
const auto& events = actionJournal->getLog();
auto jEvents = jni::Array<jni::String>::New(env, events.size());
uint32_t index = 0;
for (const auto& event : events) {
jEvents.Set(env, index++, jni::Make<jni::String>(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<NativeMapView>&,
const jni::Object<FileSource>&,
const jni::Object<MapRenderer>&,
jni::jfloat,
jni::jboolean>,
const jni::Object<NativeMapOptions>&>,
"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"),

View File

@@ -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 <exception>
@@ -49,8 +50,7 @@ public:
const jni::Object<NativeMapView>&,
const jni::Object<FileSource>&,
const jni::Object<MapRenderer>&,
jni::jfloat,
jni::jboolean);
const jni::Object<NativeMapOptions>&);
virtual ~NativeMapView();
@@ -199,6 +199,12 @@ public:
jni::jboolean getDebug(JNIEnv&);
jni::Local<jni::Array<jni::String>> getActionJournalLogFiles(JNIEnv&);
jni::Local<jni::Array<jni::String>> getActionJournalLog(JNIEnv&);
void clearActionJournalLog(JNIEnv&);
jni::jboolean isFullyLoaded(JNIEnv&);
jni::jdouble getMetersPerPixelAtLatitude(JNIEnv&, jni::jdouble, jni::jdouble);

View File

@@ -897,6 +897,49 @@ public final class MapLibreMap {
nativeMapView.setDebug(debugActive);
}
/**
* <p>
* Get the list of action journal log files from oldest to newest.
* </p>
*/
public String[] getActionJournalLogFiles() {
return nativeMapView.getActionJournalLogFiles();
}
/**
* <p>
* Get the action journal events from oldest to newest.
* </p>
* 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();
}
/**
* <p>
* Clear stored action journal events.
* </p>
*/
public void clearActionJournalLog() {
nativeMapView.clearActionJournalLog();
}
//
// API endpoint config
//

View File

@@ -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.
* <p>
* If set to true, enables map event file logging (obtainable via MapView#getActionJournalLog)
* </p>
*
* @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.
* <p>
* The action journal uses a rolling log with multiple files.
* Total log size is equal to `actionJournalLogFileSize * actionJournalLogFileCount`.
* </p>
*
* @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.
* <p>
* The action journal uses a rolling log with multiple files.
* Total log size is equal to `actionJournalLogFileSize * actionJournalLogFileCount`.
* </p>
*
* @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;
}
}

View File

@@ -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() {

View File

@@ -221,6 +221,12 @@ interface NativeMap {
boolean getDebug();
String[] getActionJournalLogFiles();
String[] getActionJournalLog();
void clearActionJournalLog();
void setReachability(boolean status);
void setApiBaseUrl(String baseUrl);

View File

@@ -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;
}
}

View File

@@ -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();

View File

@@ -17,6 +17,11 @@
<public name="maplibre_cross_source_collisions" type="attr" />
<public name="maplibre_pixelRatio" type="float" />
<!--Action Journal-->
<public name="maplibre_actionJournalEnabled" type="attr"/>
<public name="maplibre_actionJournalLogFileSize" type="attr"/>
<public name="maplibre_actionJournalLogFileCount" type="attr"/>
<!--Camera-->
<public name="maplibre_cameraTargetLng" type="attr" />
<public name="maplibre_cameraTargetLat" type="attr" />

View File

@@ -12,6 +12,11 @@
<attr name="maplibre_localIdeographFontFamilies" format="reference"/>
<attr name="maplibre_cross_source_collisions" format="boolean"/>
<!--Action Journal-->
<attr name="maplibre_actionJournalEnabled" format="boolean"/>
<attr name="maplibre_actionJournalLogFileSize" format="integer"/>
<attr name="maplibre_actionJournalLogFileCount" format="integer"/>
<!--Camera-->
<attr name="maplibre_cameraTargetLat" format="float"/>
<attr name="maplibre_cameraTargetLng" format="float"/>

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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<View>(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<String, TimeMark> = 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<String>, rangeStart: Int, rangeEnd: Int) {
override fun onGlyphsRequested(fontStack: Array<String>, 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<String>, rangeStart: Int, rangeEnd: Int) {
override fun onGlyphsLoaded(fontStack: Array<String>, 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()

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/coordinator_layout">
<!-- -8<- [start:MapView] -->
<org.maplibre.android.maps.MapView
android:id="@id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:maplibre_actionJournalEnabled="true"
app:maplibre_actionJournalLogFileSize="1024"
app:maplibre_actionJournalLogFileCount="2"/>
<!-- -8<- [end:MapView] -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@drawable/ic_print"
app:backgroundTint="@color/accent"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,80 @@
# Action Journal
{{ activity_source_note("ObserverActivity.kt") }}
The Action Journal provides functionality for persistent logging of top level map events.
<!-- NOTE: keep this text in sync with platform/ios/MapLibre.docc/ActionJournalExample.md -->
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.

View File

@@ -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"
```

View File

@@ -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",

View File

@@ -0,0 +1,35 @@
#import <Foundation/Foundation.h>
#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

View File

@@ -0,0 +1,54 @@
#import "MLNActionJournalOptions.h"
#import <mbgl/util/action_journal_options.hpp>
@implementation MLNActionJournalOptions {
std::unique_ptr<mbgl::util::ActionJournalOptions> _actionJournalOptionsInternal;
}
- (instancetype)init {
if (self = [super init]) {
_actionJournalOptionsInternal = std::make_unique<mbgl::util::ActionJournalOptions>();
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<uint32_t>(value));
}
- (NSInteger)logFileSize {
return _actionJournalOptionsInternal->logFileSize();
}
- (void)setLogFileCount:(NSInteger)value {
_actionJournalOptionsInternal->withLogFileCount(static_cast<uint32_t>(value));
}
- (NSInteger)logFileCount {
return _actionJournalOptionsInternal->logFileCount();
}
@end

View File

@@ -0,0 +1,12 @@
#import <mbgl/util/action_journal_options.hpp>
#import "MLNActionJournalOptions.h"
NS_ASSUME_NONNULL_BEGIN
@interface MLNActionJournalOptions (Private)
- (mbgl::util::ActionJournalOptions)getCoreOptions;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,37 @@
#import <Foundation/Foundation.h>
#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
<a href="https://maplibre.org/maplibre-style-spec/">MapLibre Style Specification</a>.
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

View File

@@ -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

View File

@@ -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",

View File

@@ -0,0 +1,84 @@
# Action Journal
Learn about the ``MLNMapView`` methods for logging and viewing map actions.
<!-- NOTE: keep this text in sync with platform/android/docs/observability/action-journal.md -->
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.
<!-- include-example(actionJournalOptions) -->
```swift
MLNSettings.actionJournalOptions.enabled = true
```
<!-- include-example(ObserverExampleActionJournal) -->
```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.

View File

@@ -49,9 +49,13 @@ Powerful, free and open-source mapping toolkit with full control over data sourc
- <doc:OfflinePackExample>
- <doc:ManageOfflineRegionsExample>
### Advanced
### Observability
- <doc:ObserverExample>
- <doc:ActionJournalExample>
### Advanced
- <doc:CustomStyleLayerExample>
### Other Articles

View File

@@ -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)")

View File

@@ -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"

View File

@@ -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<NSString *> *)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<NSString *> *)getActionJournalLog;
/**
Clear stored action journal events.
*/
- (void)clearActionJournalLog;
- (MLNBackendResource *)backendResource;
/**

View File

@@ -15,6 +15,7 @@
#include <mbgl/style/layers/custom_layer.hpp>
#include <mbgl/renderer/renderer.hpp>
#include <mbgl/math/wrap.hpp>
#include <mbgl/util/action_journal.hpp>
#include <mbgl/util/client_options.hpp>
#include <mbgl/util/exception.hpp>
#include <mbgl/util/geo.hpp>
@@ -68,6 +69,7 @@
#import "MLNNetworkConfiguration_Private.h"
#import "MLNReachability.h"
#import "MLNSettings_Private.h"
#import "MLNActionJournalOptions_Private.h"
#import "MLNMapProjection.h"
#include <algorithm>
@@ -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<mbgl::Map>(*_rendererFrontend, *_mbglView, mapOptions, resourceOptions, clientOptions);
_mbglMap = std::make_unique<mbgl::Map>(*_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<NSString*>*)getActionJournalLogFiles
{
const auto& actionJournal = _mbglMap->getActionJournal();
if (!actionJournal) {
return nil;
}
const auto& files = actionJournal->getLogFiles();
NSMutableArray<NSString*>* objcFiles = [NSMutableArray new];
for (const auto& file : files) {
[objcFiles addObject:[NSString stringWithUTF8String:file.c_str()]];
}
return objcFiles;
}
- (NSArray<NSString*>*)getActionJournalLog
{
const auto& actionJournal = _mbglMap->getActionJournal();
if (!actionJournal) {
return nil;
}
const auto& log = actionJournal->getLog();
NSMutableArray<NSString*>* 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();
}

View File

@@ -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<MLNStylable>
*/
- (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<NSString *> *)getActionJournalLog;
/**
Clear stored action journal events.
*/
- (void)clearActionJournalLog;
@end
NS_ASSUME_NONNULL_END

View File

@@ -34,6 +34,7 @@
#import <mbgl/storage/network_status.hpp>
#import <mbgl/storage/resource_options.hpp>
#import <mbgl/math/wrap.hpp>
#import <mbgl/util/action_journal.hpp>
#import <mbgl/util/client_options.hpp>
#import <mbgl/util/constants.hpp>
#import <mbgl/util/chrono.hpp>
@@ -59,6 +60,7 @@
#import "MLNNetworkConfiguration_Private.h"
#import "MLNLoggingConfiguration_Private.h"
#import "MLNReachability.h"
#import "MLNActionJournalOptions_Private.h"
#import "MLNSettings_Private.h"
#import <CoreImage/CIFilter.h>
@@ -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<mbgl::Map>(*_rendererFrontend, *_mbglView, mapOptions, resourceOptions, clientOptions);
const mbgl::util::ActionJournalOptions& actionJournalOptions = [mlnMapoptions.actionJournalOptions getCoreOptions];
_mbglMap = std::make_unique<mbgl::Map>(*_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<NSString*>*)getActionJournalLog
{
const auto& actionJournal = _mbglMap->getActionJournal();
if (!actionJournal) {
return nil;
}
const auto& log = actionJournal->getLog();
NSMutableArray<NSString*>* 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

View File

@@ -11,6 +11,7 @@ NS_ASSUME_NONNULL_BEGIN
@class MLNPolyline;
@class MLNShape;
@class MLNStyle;
@class MLNSource;
@protocol MLNAnnotation;
/**

View File

@@ -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<BuiltIn::None, gfx::Backend::Type::OpenGL> {
} // namespace mbgl
`);
// Generate shader_source.cpp
fs.writeFileSync(path.join(cppOutputRoot, "shader_source.cpp"),
`${generatedHeader}
#include <mbgl/shaders/shader_source.hpp>
#include <mbgl/util/enum.hpp>
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!");

View File

@@ -20,6 +20,7 @@
#include <mbgl/util/mapbox.hpp>
#include <mbgl/util/math.hpp>
#include <mbgl/util/tile_coordinate.hpp>
#include <mbgl/util/action_journal.hpp>
#include <utility>
@@ -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<Impl>(frontend,
observer,
FileSourceManager::get()
? std::shared_ptr<FileSource>(FileSourceManager::get()->getFileSource(
ResourceLoader, resourceOptions, clientOptions))
: nullptr,
mapOptions)) {}
mapOptions)) {
if (actionJournalOptions.enabled()) {
impl->actionJournal = std::make_unique<util::ActionJournal>(*this, actionJournalOptions);
}
}
Map::Map(std::unique_ptr<Impl> impl_)
: impl(std::move(impl_)) {}
Map::Map(std::unique_ptr<Impl> impl_, const util::ActionJournalOptions& actionJournalOptions)
: impl(std::move(impl_)) {
if (actionJournalOptions.enabled()) {
impl->actionJournal = std::make_unique<util::ActionJournal>(*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<util::ActionJournal>& Map::getActionJournal() {
return impl->actionJournal;
}
} // namespace mbgl

View File

@@ -6,6 +6,8 @@
#include <mbgl/util/exception.hpp>
#include <mbgl/util/logging.hpp>
#include <mbgl/util/traits.hpp>
#include <mbgl/util/action_journal.hpp>
#include <mbgl/util/action_journal_impl.hpp>
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<style::Sprite>& sprite) {
observer.onSpriteLoaded(sprite);
if (actionJournal) {
actionJournal->impl->onSpriteLoaded(sprite);
}
}
void Map::Impl::onSpriteError(const std::optional<style::Sprite>& sprite, std::exception_ptr ex) {
observer.onSpriteError(sprite, ex);
if (actionJournal) {
actionJournal->impl->onSpriteError(sprite, ex);
}
}
void Map::Impl::onSpriteRequested(const std::optional<style::Sprite>& 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<void()>& 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<std::string>& 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

View File

@@ -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<FileSource>, 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<util::ActionJournal> actionJournal;
Transform transform;

View File

@@ -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) {}

View File

@@ -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&,

View File

@@ -0,0 +1,44 @@
// Generated code, do not modify this file!
#include <mbgl/shaders/shader_source.hpp>
#include <mbgl/util/enum.hpp>
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

View File

@@ -0,0 +1,18 @@
#include <mbgl/tile/tile_operation.hpp>
#include <mbgl/util/enum.hpp>
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

View File

@@ -0,0 +1,33 @@
#include <mbgl/util/action_journal.hpp>
#include <mbgl/util/action_journal_impl.hpp>
namespace mbgl {
namespace util {
ActionJournal::ActionJournal(const Map& map, const ActionJournalOptions& options)
: impl(std::make_unique<Impl>(map, options)) {
impl->onMapCreate();
}
ActionJournal::~ActionJournal() {
impl->onMapDestroy();
}
std::string ActionJournal::getLogDirectory() const {
return impl->getLogDirectory();
}
std::vector<std::string> ActionJournal::getLogFiles() const {
return impl->getLogFiles();
}
std::vector<std::string> ActionJournal::getLog() {
return impl->getLog();
}
void ActionJournal::clearLog() {
impl->clearLog();
}
} // namespace util
} // namespace mbgl

View File

@@ -0,0 +1,575 @@
#include <mbgl/util/action_journal_impl.hpp>
#include <mbgl/util/logging.hpp>
#include <mbgl/style/style.hpp>
#include <mbgl/map/map.hpp>
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <regex>
#ifdef __APPLE__
#include <TargetConditionals.h>
#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
#define USE_GHC_FILESYSTEM
#endif
#endif
#ifdef USE_GHC_FILESYSTEM
#include <ghc/filesystem.hpp>
namespace mbgl {
namespace filesystem = ghc::filesystem;
}
#else
#include <filesystem>
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::milliseconds>(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<std::chrono::system_clock, std::chrono::milliseconds> time;
const std::string clientName;
const std::string clientVersion;
const std::string styleName;
const std::string styleURL;
};
class ActionJournalEvent {
public:
ActionJournalEvent(const rapidjson::GenericStringRef<char>& 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 <typename T>
ActionJournalEvent& addEvent(const rapidjson::GenericStringRef<char>& key, const T& value) {
eventJson.AddMember(key, value, json.GetAllocator());
return *this;
}
ActionJournalEvent& addEventStringArray(const rapidjson::GenericStringRef<char>& key,
const std::vector<std::string>& value) {
rapidjson::Value arrayJson(rapidjson::kArrayType);
for (const auto& elem : value) {
rapidjson::Value elemJson(rapidjson::kStringType);
elemJson.SetString(elem.c_str(), static_cast<rapidjson::SizeType>(elem.size()));
arrayJson.PushBack(elemJson, json.GetAllocator());
}
eventJson.AddMember(key, arrayJson, json.GetAllocator());
return *this;
}
template <typename T>
ActionJournalEvent& addEventEnum(const rapidjson::GenericStringRef<char>& key, const T& value) {
eventJson.AddMember(key, rapidjson::StringRef(Enum<T>::toString(value)), json.GetAllocator());
return *this;
}
std::string toString() {
if (!eventJson.ObjectEmpty()) {
json.AddMember("event", eventJson, json.GetAllocator());
}
rapidjson::StringBuffer jsonBuffer;
rapidjson::Writer<rapidjson::StringBuffer> 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<std::string> ActionJournal::Impl::getLogFiles() const {
std::set<std::string> 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<std::string>(files.begin(), files.end());
}
std::vector<std::string> ActionJournal::Impl::getLog() {
std::lock_guard lock(fileMutex);
std::vector<std::string> 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<int>(mode)));
});
}
void ActionJournal::Impl::onCameraDidChange(CameraChangeMode mode) {
scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this)]() {
log(ActionJournalEvent("onCameraDidChange", env).addEvent("cameraMode", static_cast<int>(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<int>(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<int>(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<int>(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<int>(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<style::Sprite>& 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<style::Sprite>& 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<style::Sprite>& 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<uint32_t, mbgl::filesystem::path> 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

View File

@@ -0,0 +1,105 @@
#pragma once
#include <mbgl/util/action_journal.hpp>
#include <mbgl/util/action_journal_options.hpp>
#include <mbgl/map/map_observer.hpp>
#include <fstream>
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<std::string> getLogFiles() const;
std::vector<std::string> 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<style::Sprite>&) override;
void onSpriteError(const std::optional<style::Sprite>&, std::exception_ptr) override;
void onSpriteRequested(const std::optional<style::Sprite>&) 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> scheduler;
std::mutex fileMutex;
std::fstream currentFile;
uint32_t currentFileIndex{0};
size_t currentFileSize{0};
};
} // namespace util
} // namespace mbgl

View File

@@ -49,6 +49,22 @@ std::string iso8601(Timestamp timestamp) {
return buffer;
}
std::string iso8601(std::chrono::time_point<std::chrono::system_clock, Milliseconds> 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<Milliseconds>(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<Seconds>(std::chrono::system_clock::from_time_t(parse_date(timestamp)));
}

View File

@@ -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

View File

@@ -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(); };

View File

@@ -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();

View File

@@ -0,0 +1,431 @@
#include <mbgl/util/action_journal.hpp>
#include <mbgl/util/action_journal_impl.hpp>
#include <mbgl/util/run_loop.hpp>
#include <mbgl/util/io.hpp>
#include <mbgl/util/rapidjson.hpp>
#include <mbgl/util/chrono.hpp>
#include <mbgl/style/style.hpp>
#include <mbgl/style/sources/geojson_source.hpp>
#include <mbgl/gfx/headless_frontend.hpp>
#include <mbgl/gfx/shader_registry.hpp>
#include <mbgl/map/map_impl.hpp>
#include <mbgl/test/util.hpp>
#include <mbgl/test/stub_file_source.hpp>
#include <mbgl/test/stub_map_observer.hpp>
#include <gmock/gmock-matchers.h>
#include <gmock/gmock-more-matchers.h>
#include <regex>
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> fileSource,
const MapOptions& options,
const util::ActionJournalOptions& actionJournalOptions)
: Map(std::make_unique<Map::Impl>(frontend, observer, std::move(fileSource), options),
actionJournalOptions) {}
Map::Impl& getImpl() { return *impl; }
};
ActionJournalOptions options;
StubMapObserver observer;
std::shared_ptr<StubFileSource> fileSource;
std::unique_ptr<HeadlessFrontend> frontend;
std::unique_ptr<MapAdapter> 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<StubFileSource>(ResourceOptions::Default(),
ClientOptions().withName("ActionJournalTest").withVersion("1.0"));
frontend = std::make_unique<HeadlessFrontend>(1.0f);
map = std::make_unique<MapAdapter>(
*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 <typename EventMethod, typename... Args>
void validateEventList(ActionJournalTest& test,
std::vector<std::pair<const char*, std::function<void(const mbgl::JSValue&)>>>&& generatedEvents,
EventMethod&& eventGenerator,
Args&&... args) {
test.map->getActionJournal()->clearLog();
(test.map->getImpl().*eventGenerator)(std::forward<Args>(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<rapidjson::kParseDefaultFlags>(*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 <typename EventMethod, typename... Args>
void validateEvent(ActionJournalTest& test,
const char* generatedEvent,
std::function<void(const mbgl::JSValue&)>&& checkOutput,
EventMethod&& eventGenerator,
Args&&... args) {
validateEventList(test, {{generatedEvent, checkOutput}}, eventGenerator, std::forward<Args>(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<gfx::ShaderRegistry>();
const auto testSource = std::make_unique<style::GeoJSONSource>("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<int>(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<int>(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<style::SourceType>::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<int>(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<shaders::BuiltIn>::toEnum(json["shader"].GetString()), shaders::BuiltIn::None);
EXPECT_TRUE(json.HasMember("backend"));
EXPECT_TRUE(json["backend"].IsInt());
EXPECT_EQ(json["backend"].GetInt(), static_cast<int>(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<void (RendererObserver::*)(RendererObserver::RenderMode, bool, bool, double, double)>(
&RendererObserver::onDidFinishRenderingFrame);
validateEvent(test,
"onDidBecomeIdle",
nullptr,
onDidFinishRenderingFrame,
RendererObserver::RenderMode::Full,
false,
false,
0.0,
0.0);
}

2
vendor/BUILD.bazel vendored
View File

@@ -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",

View File

@@ -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

View File

@@ -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)