diff --git a/.bazelignore b/.bazelignore index 4ea3360fd472..451097014c83 100644 --- a/.bazelignore +++ b/.bazelignore @@ -4,6 +4,7 @@ bazel-out bazel-bin bazel-testlogs .cache +.pixi .idea .vscode .cxx diff --git a/.github/workflows/ohos-ci.yml b/.github/workflows/ohos-ci.yml new file mode 100644 index 000000000000..e65bea9154da --- /dev/null +++ b/.github/workflows/ohos-ci.yml @@ -0,0 +1,120 @@ +name: ohos-ci + +on: + workflow_dispatch: + push: + branches: + - main + - ohos-*.*.x + paths: + - CMakeLists.txt + - CMakePresets.json + - "platform/default/**" + - "platform/linux/**" + - "platform/ohos/**" + - ".github/workflows/ohos-ci.yml" + - "bin/**" + - "expression-test/**" + - "include/**" + - "metrics/**" + - "render-test/**" + - "scripts/**" + - "src/**" + - "test/**" + - "vendor/**" + - ".gitmodules" + - "!**/*.md" + + pull_request: + branches: + - "*" + paths: + - CMakeLists.txt + - CMakePresets.json + - "platform/default/**" + - "platform/linux/**" + - "platform/ohos/**" + - ".github/workflows/ohos-ci.yml" + - "bin/**" + - "expression-test/**" + - "include/**" + - "metrics/**" + - "render-test/**" + - "scripts/**" + - "src/**" + - "test/**" + - "vendor/**" + - ".gitmodules" + - "!**/*.md" + +concurrency: + # cancel jobs on PRs only + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + build: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - name: OpenGL + preset: harmonyos-opengl + - name: Vulkan + preset: harmonyos-vulkan + name: ${{ matrix.name }} + env: + OHOS_SDK_VERSION: "6.1" + + defaults: + run: + working-directory: ./ + shell: bash + + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 + with: + submodules: recursive + persist-credentials: false + + - name: Install dependencies + env: + DEBIAN_FRONTEND: noninteractive + run: | + sudo apt-get update + sudo apt-get install -y \ + ccache \ + ninja-build + /usr/sbin/update-ccache-symlinks + + - name: Set up OpenHarmony SDK + uses: openharmony-rs/setup-ohos-sdk@eb82b94ef522b07269679195c2512f22e922ef3b # v1.0.0 + with: + version: ${{ env.OHOS_SDK_VERSION }} + components: native + + - name: Set up ccache + uses: hendrikmuhs/ccache-action@33522472633dbd32578e909b315f5ee43ba878ce # v1.2.22 + with: + key: OHOS_${{ matrix.preset }}_${{ env.OHOS_SDK_VERSION }}_${{ github.sha }} + restore-keys: | + OHOS_${{ matrix.preset }}_${{ env.OHOS_SDK_VERSION }}_${{ github.ref }} + OHOS_${{ matrix.preset }}_${{ env.OHOS_SDK_VERSION }} + max-size: 200M + + - name: Print tool versions + run: | + cmake --version + ninja --version + "${OHOS_SDK_NATIVE}/llvm/bin/clang" --version + + - name: Configure maplibre-native + run: | + cmake --preset "${{ matrix.preset }}" \ + -DCMAKE_C_COMPILER_LAUNCHER="ccache" \ + -DCMAKE_CXX_COMPILER_LAUNCHER="ccache" + + - name: Build mbgl-core + run: cmake --build --preset "${{ matrix.preset }}" diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a680480d448..a8507e2e0442 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,16 @@ endif() project("MapLibre Native" LANGUAGES CXX C) +if(CMAKE_SYSTEM_NAME STREQUAL OHOS) + # The OHOS SDK toolchain injects --gcc-toolchain, which clang reports as unused. + # OHOS SDK 6.0.1 ships libc++ ranges algorithms behind -fexperimental-library. + add_compile_options( + $<$:-Wno-unused-command-line-argument> + $<$:-Wno-unused-command-line-argument> + $<$:-fexperimental-library> + ) +endif() + set_property(GLOBAL PROPERTY USE_FOLDERS ON) set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER MapLibre) @@ -954,6 +964,7 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/util/quaternion.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/util/rapidjson.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/util/rapidjson.hpp + ${PROJECT_SOURCE_DIR}/src/mbgl/util/source_location.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/util/std.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/util/stopwatch.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/util/stopwatch.hpp @@ -1275,6 +1286,8 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL Android) include(${PROJECT_SOURCE_DIR}/platform/android/android.cmake) elseif(CMAKE_SYSTEM_NAME STREQUAL Linux) include(${PROJECT_SOURCE_DIR}/platform/linux/linux.cmake) +elseif(CMAKE_SYSTEM_NAME STREQUAL OHOS) + include(${PROJECT_SOURCE_DIR}/platform/ohos/ohos.cmake) elseif(CMAKE_SYSTEM_NAME STREQUAL Darwin) include(${PROJECT_SOURCE_DIR}/platform/darwin/darwin.cmake) include(${PROJECT_SOURCE_DIR}/platform/macos/macos.cmake) diff --git a/CMakePresets.json b/CMakePresets.json index d55efb6e2ed1..f71a078a3104 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -249,6 +249,46 @@ "CMAKE_CXX_COMPILER_LAUNCHER": "ccache" } }, + { + "name": "harmonyos", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-harmonyos", + "condition": { + "type": "notEquals", + "lhs": "$env{OHOS_SDK_NATIVE}", + "rhs": "" + }, + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "$env{OHOS_SDK_NATIVE}/build/cmake/ohos.toolchain.cmake", + "OHOS_SDK_NATIVE": "$env{OHOS_SDK_NATIVE}", + "OHOS_ARCH": "arm64-v8a", + "OHOS_STL": "c++_shared", + "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "MLN_WITH_GLFW": "OFF", + "MLN_WITH_OPENGL": "ON", + "MLN_WITH_EGL": "ON" + } + }, + { + "name": "harmonyos-opengl", + "inherits": "harmonyos", + "binaryDir": "${sourceDir}/build-harmonyos-opengl", + "displayName": "HarmonyOS OpenGL", + "description": "OpenHarmony GLES/EGL and NetworkKit HTTP build" + }, + { + "name": "harmonyos-vulkan", + "inherits": "harmonyos", + "binaryDir": "${sourceDir}/build-harmonyos-vulkan", + "displayName": "HarmonyOS Vulkan", + "description": "OpenHarmony Vulkan and NetworkKit HTTP build", + "cacheVariables": { + "MLN_WITH_OPENGL": "OFF", + "MLN_WITH_EGL": "OFF", + "MLN_WITH_VULKAN": "ON" + } + }, { "name": "windows", "hidden": true, @@ -377,6 +417,27 @@ "configurePreset": "linux-vulkan", "targets": ["mbgl-core"] }, + { + "name": "harmonyos-build", + "hidden": true, + "condition": { + "type": "notEquals", + "lhs": "$env{OHOS_SDK_NATIVE}", + "rhs": "" + } + }, + { + "name": "harmonyos-opengl", + "inherits": "harmonyos-build", + "configurePreset": "harmonyos-opengl", + "targets": ["mbgl-core"] + }, + { + "name": "harmonyos-vulkan", + "inherits": "harmonyos-build", + "configurePreset": "harmonyos-vulkan", + "targets": ["mbgl-core"] + }, { "name": "windows-opengl-core", "configurePreset": "windows-opengl", diff --git a/platform/linux/src/gl_functions.cpp b/platform/linux/src/gl_functions.cpp index 3fcc0ae47b5a..f06e6941aeab 100644 --- a/platform/linux/src/gl_functions.cpp +++ b/platform/linux/src/gl_functions.cpp @@ -3,7 +3,6 @@ #define GL_GLEXT_PROTOTYPES #include -#include namespace mbgl { namespace platform { diff --git a/platform/ohos/README.md b/platform/ohos/README.md new file mode 100644 index 000000000000..e13a1d621fcb --- /dev/null +++ b/platform/ohos/README.md @@ -0,0 +1,32 @@ +# OpenHarmony + +This platform support is experimental. It targets the +[OpenHarmony](https://en.wikipedia.org/wiki/OpenHarmony) family of operating +systems, including [HarmonyOS](https://en.wikipedia.org/wiki/HarmonyOS). + +Public OpenHarmony platform documentation is available in the +[OpenHarmony docs repository](https://github.com/openharmony/docs). + +See [the sample readme](./sample/README.md) to build and run the NAPI/XComponent +integration example in DevEco Studio. + +## CMake Build + +When invoking CMake directly from this repository root, set the native SDK paths +before using the HarmonyOS presets: + +```sh +export OHOS_SDK_NATIVE=/path/to/openharmony/native + +cmake --preset harmonyos-opengl +cmake --build --preset harmonyos-opengl + +cmake --preset harmonyos-vulkan +cmake --build --preset harmonyos-vulkan +``` + +The presets use the OpenHarmony SDK from `OHOS_SDK_NATIVE`. They are disabled +until that variable is set. + +DevEco Studio and Hvigor provide the native SDK path from the configured SDK +when building `platform/ohos/sample`. diff --git a/platform/ohos/ohos.cmake b/platform/ohos/ohos.cmake new file mode 100644 index 000000000000..e560ff11d977 --- /dev/null +++ b/platform/ohos/ohos.cmake @@ -0,0 +1,142 @@ +if(NOT CMAKE_SYSTEM_NAME STREQUAL OHOS) + message(FATAL_ERROR "OpenHarmony platform support requires the OpenHarmony CMake toolchain") +endif() + +include(${PROJECT_SOURCE_DIR}/vendor/icu.cmake) +include(${PROJECT_SOURCE_DIR}/vendor/nunicode.cmake) +include(${PROJECT_SOURCE_DIR}/vendor/sqlite.cmake) + +find_library(MLN_OHOS_NET_HTTP_LIBRARY NAMES net_http REQUIRED) + +set_source_files_properties( + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/i18n/number_format.cpp + PROPERTIES + COMPILE_DEFINITIONS MBGL_USE_BUILTIN_ICU +) + +target_compile_definitions( + mbgl-core + PRIVATE + OHOS_PLATFORM +) + +target_sources( + mbgl-core + PRIVATE + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/gfx/headless_backend.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/gfx/headless_frontend.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/i18n/collator.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/i18n/number_format.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/layermanager/layer_manager.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/map/map_snapshotter.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/platform/time.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/asset_file_source.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/database_file_source.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/file_source_manager.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/file_source_request.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/local_file_request.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/local_file_source.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/main_resource_loader.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/mbtiles_file_source.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/offline.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/offline_database.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/offline_download.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/online_file_source.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/$,pmtiles_file_source.cpp,pmtiles_file_source_stub.cpp> + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/sqlite3.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/text/bidi.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/text/local_glyph_rasterizer.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/util/async_task.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/util/compression.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/util/filesystem.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/util/monotonic_timer.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/util/png_writer.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/util/run_loop.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/util/string_stdlib.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/util/thread.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/util/thread_local.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/util/timer.cpp + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/util/utf.cpp + ${PROJECT_SOURCE_DIR}/platform/ohos/src/http_file_source.cpp + ${PROJECT_SOURCE_DIR}/platform/ohos/src/image.cpp + ${PROJECT_SOURCE_DIR}/platform/ohos/src/logging_hilog.cpp +) + +if(MLN_WITH_OPENGL) + target_sources( + mbgl-core + PRIVATE + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/gl/headless_backend.cpp + ${PROJECT_SOURCE_DIR}/platform/linux/src/gl_functions.cpp + ${PROJECT_SOURCE_DIR}/platform/linux/src/headless_backend_egl.cpp + ) +endif() + +if(MLN_WITH_VULKAN) + target_sources( + mbgl-core + PRIVATE + ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/vulkan/headless_backend.cpp + ) + + target_compile_definitions( + mbgl-core + PUBLIC + VK_USE_PLATFORM_OHOS=1 + ) + + target_link_libraries( + mbgl-core + PRIVATE + vulkan + ) +endif() + +target_include_directories( + mbgl-core + PRIVATE + ${PROJECT_SOURCE_DIR}/platform/default/include +) + +target_link_libraries( + mbgl-core + PRIVATE + image_source + mbgl-vendor-icu + mbgl-vendor-nunicode + mbgl-vendor-sqlite + pixelmap + hilog_ndk.z + ${MLN_OHOS_NET_HTTP_LIBRARY} + uv + z +) + +if(MLN_WITH_OPENGL) + target_link_libraries( + mbgl-core + PRIVATE + EGL + GLESv3 + ) +endif() + +# The OHOS toolchain is not listed as a built-in CMake platform for this +# feature, so teach CMake how to whole-archive static targets for the native +# module link. +set(CMAKE_LINK_LIBRARY_USING_WHOLE_ARCHIVE "-Wl,--whole-archive -Wl,--no-whole-archive") +set(CMAKE_LINK_LIBRARY_USING_WHOLE_ARCHIVE_SUPPORTED True) + +function(mln_ohos_link_core_whole_archive target) + target_link_libraries( + ${target} + PRIVATE + $ + ) + + target_link_options( + ${target} + PRIVATE + LINKER:--no-undefined + ) +endfunction() diff --git a/platform/ohos/sample/.clang-tidy b/platform/ohos/sample/.clang-tidy new file mode 100644 index 000000000000..69a9148598f5 --- /dev/null +++ b/platform/ohos/sample/.clang-tidy @@ -0,0 +1 @@ +Checks: 'misc-missing-switch-cases,misc-napi-module-name,misc-replace-if-else-with-ternary-operator,misc-unused-local-variable,misc-unused-parameters,modernize-use-auto,readability-system-capabilities,misc-napi-macro-module-name,capi-version-validation' diff --git a/platform/ohos/sample/.clangd b/platform/ohos/sample/.clangd new file mode 100644 index 000000000000..c0ae0a8b3810 --- /dev/null +++ b/platform/ohos/sample/.clangd @@ -0,0 +1,15 @@ +CompileFlags: + Add: [-Wunreachable-code-aggressive] +Diagnostics: + ClangTidy: + Add: [misc-missing-switch-cases,misc-napi-module-name,misc-replace-if-else-with-ternary-operator,misc-unused-local-variable,misc-unused-parameters,modernize-use-auto,readability-system-capabilities,misc-napi-macro-module-name,capi-version-validation] + Remove: [] + CheckOptions: + misc-unused-parameters.StrictMode: true + misc-unused-parameters.IgnoreVirtual: true + modernize-use-auto.MinTypeNameLength: 0 + + UnusedIncludes: Strict + UnusedFunctions: Check +Hover: + ShowNewHover: Yes diff --git a/platform/ohos/sample/.gitignore b/platform/ohos/sample/.gitignore new file mode 100644 index 000000000000..bfe7a1a48e6f --- /dev/null +++ b/platform/ohos/sample/.gitignore @@ -0,0 +1,12 @@ +.hvigor/ +.appanalyzer/ +oh_modules/ +build/ +entry/.cxx/ +entry/build/ +sign/*.p12 +sign/*.cer +sign/*.p7b +sign/*.hap +sign/material/ +sign/signing.local.json diff --git a/platform/ohos/sample/AppScope/app.json5 b/platform/ohos/sample/AppScope/app.json5 new file mode 100644 index 000000000000..194c2876daea --- /dev/null +++ b/platform/ohos/sample/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "org.maplibre.native.demo", + "vendor": "maplibre", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:icon", + "label": "$string:app_name" + } +} diff --git a/platform/ohos/sample/README.md b/platform/ohos/sample/README.md new file mode 100644 index 000000000000..73cdd784e746 --- /dev/null +++ b/platform/ohos/sample/README.md @@ -0,0 +1,29 @@ +# MapLibre Native HarmonyOS Sample + +Open this directory in DevEco Studio and let it sync the project. + +The default product builds the Vulkan renderer. Select the `opengl` product in +DevEco Studio to build the EGL/GLES renderer instead. + +## Signing + +Before installing on a device, place your AppGallery Connect debug signing files +in `sign/` with these names: + +- `maplibre-debug.p12` — private keystore +- `maplibre-debug.cer` — debug certificate +- `maplibre-debug.p7b` — debug profile for bundle `org.maplibre.native.demo` + +Generate the local Hvigor signing config: + +```sh +node sign/generate-signing-config.mjs +``` + +The script writes `sign/signing.local.json` and `sign/material/`. Keep both +uncommitted. + +If DevEco Studio writes a signing config into `build-profile.json5`, revert +`build-profile.json5` before sending a PR. + +Keep signing passwords outside the repository. diff --git a/platform/ohos/sample/build-profile.json5 b/platform/ohos/sample/build-profile.json5 new file mode 100644 index 000000000000..577526b907ff --- /dev/null +++ b/platform/ohos/sample/build-profile.json5 @@ -0,0 +1,45 @@ +{ + "app": { + "signingConfigs": [], + "products": [ + { + "name": "default", + "compatibleSdkVersion": "6.0.1(21)", + "targetSdkVersion": "6.0.1(21)", + "runtimeOS": "HarmonyOS", + "buildOption": { + "nativeCompiler": "BiSheng" + } + }, + { + "name": "opengl", + "compatibleSdkVersion": "6.0.1(21)", + "targetSdkVersion": "6.0.1(21)", + "runtimeOS": "HarmonyOS", + "buildOption": { + "nativeCompiler": "BiSheng" + } + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + }, + { + "name": "opengl", + "applyToProducts": [ + "opengl" + ] + } + ] + } + ] +} diff --git a/platform/ohos/sample/entry/build-profile.json5 b/platform/ohos/sample/entry/build-profile.json5 new file mode 100644 index 000000000000..61e84a9e736e --- /dev/null +++ b/platform/ohos/sample/entry/build-profile.json5 @@ -0,0 +1,37 @@ +{ + "apiType": "stageMode", + "buildOption": { + "externalNativeOptions": { + "path": "./src/main/cpp/CMakeLists.txt", + "arguments": "-DOHOS_STL=c++_shared -DMLN_OHOS_SAMPLE_RENDERER=Vulkan", + "abiFilters": [ + "arm64-v8a", + "x86_64" + ], + "cppFlags": "" + } + }, + "targets": [ + { + "name": "default", + "runtimeOS": "HarmonyOS" + }, + { + "name": "opengl", + "runtimeOS": "HarmonyOS", + "config": { + "buildOption": { + "externalNativeOptions": { + "path": "./src/main/cpp/CMakeLists.txt", + "arguments": "-DOHOS_STL=c++_shared -DMLN_OHOS_SAMPLE_RENDERER=OpenGL", + "abiFilters": [ + "arm64-v8a", + "x86_64" + ], + "cppFlags": "" + } + } + } + } + ] +} diff --git a/platform/ohos/sample/entry/hvigorfile.ts b/platform/ohos/sample/entry/hvigorfile.ts new file mode 100644 index 000000000000..dcb747583970 --- /dev/null +++ b/platform/ohos/sample/entry/hvigorfile.ts @@ -0,0 +1 @@ +export { hapTasks } from '@ohos/hvigor-ohos-plugin'; diff --git a/platform/ohos/sample/entry/oh-package-lock.json5 b/platform/ohos/sample/entry/oh-package-lock.json5 new file mode 100644 index 000000000000..62c2b573e689 --- /dev/null +++ b/platform/ohos/sample/entry/oh-package-lock.json5 @@ -0,0 +1,19 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "libmaplibre_native_ohos.so@types/libmaplibre_native_ohos": "libmaplibre_native_ohos.so@types/libmaplibre_native_ohos" + }, + "packages": { + "libmaplibre_native_ohos.so@types/libmaplibre_native_ohos": { + "name": "libmaplibre_native_ohos.so", + "version": "0.1.0", + "resolved": "types/libmaplibre_native_ohos", + "registryType": "local" + } + } +} diff --git a/platform/ohos/sample/entry/oh-package.json5 b/platform/ohos/sample/entry/oh-package.json5 new file mode 100644 index 000000000000..4b4df2e59990 --- /dev/null +++ b/platform/ohos/sample/entry/oh-package.json5 @@ -0,0 +1,12 @@ +{ + "name": "entry", + "version": "1.0.0", + "description": "MapLibre Native HarmonyOS sample entry module.", + "license": "BSD-2-Clause", + "author": "MapLibre contributors", + "main": "", + "dependencies": {}, + "devDependencies": { + "libmaplibre_native_ohos.so": "file:types/libmaplibre_native_ohos" + } +} diff --git a/platform/ohos/sample/entry/src/main/cpp/CMakeLists.txt b/platform/ohos/sample/entry/src/main/cpp/CMakeLists.txt new file mode 100644 index 000000000000..475993bdbbbc --- /dev/null +++ b/platform/ohos/sample/entry/src/main/cpp/CMakeLists.txt @@ -0,0 +1,89 @@ +cmake_minimum_required(VERSION 3.16) + +project(MapLibreNativeOhosSample) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(MAPLIBRE_NATIVE_ROOT + "${CMAKE_CURRENT_LIST_DIR}/../../../../../../.." + CACHE PATH + "Path to the MapLibre Native checkout" +) +get_filename_component(MAPLIBRE_NATIVE_ROOT "${MAPLIBRE_NATIVE_ROOT}" ABSOLUTE) + +set(MLN_WITH_GLFW OFF CACHE BOOL "" FORCE) +set(MLN_OHOS_SAMPLE_RENDERER "Vulkan" CACHE STRING "Sample renderer backend: Vulkan or OpenGL") +set_property(CACHE MLN_OHOS_SAMPLE_RENDERER PROPERTY STRINGS Vulkan OpenGL) + +if(MLN_OHOS_SAMPLE_RENDERER STREQUAL "Vulkan") + set(MLN_WITH_VULKAN ON CACHE BOOL "" FORCE) + set(MLN_WITH_OPENGL OFF CACHE BOOL "" FORCE) + set(MLN_WITH_EGL OFF CACHE BOOL "" FORCE) +elseif(MLN_OHOS_SAMPLE_RENDERER STREQUAL "OpenGL") + set(MLN_WITH_VULKAN OFF CACHE BOOL "" FORCE) + set(MLN_WITH_OPENGL ON CACHE BOOL "" FORCE) + set(MLN_WITH_EGL ON CACHE BOOL "" FORCE) +else() + message(FATAL_ERROR "Unsupported MLN_OHOS_SAMPLE_RENDERER: ${MLN_OHOS_SAMPLE_RENDERER}") +endif() + +add_subdirectory("${MAPLIBRE_NATIVE_ROOT}" "${CMAKE_BINARY_DIR}/maplibre-native") + +if(MLN_WITH_VULKAN) + set(MLN_OHOS_WINDOW_BACKEND_SOURCE vulkan_window_backend.cpp) +else() + set(MLN_OHOS_WINDOW_BACKEND_SOURCE egl_window_backend.cpp) +endif() + +add_library( + maplibre_native_ohos + SHARED + ${MLN_OHOS_WINDOW_BACKEND_SOURCE} + gesture_handler.cpp + map_view.cpp + native_module.cpp + native_values.cpp + renderer_frontend.cpp +) + +set_target_properties( + maplibre_native_ohos + PROPERTIES + OUTPUT_NAME maplibre_native_ohos +) + +target_compile_definitions( + maplibre_native_ohos + PRIVATE + OHOS_PLATFORM + MLN_RENDER_BACKEND_VULKAN=$ +) + +target_compile_options( + maplibre_native_ohos + PRIVATE + $<$:-Wno-unused-command-line-argument> + $<$:-fexperimental-library> +) + +target_include_directories( + maplibre_native_ohos + PRIVATE + ${MAPLIBRE_NATIVE_ROOT}/src +) + +target_link_libraries( + maplibre_native_ohos + PRIVATE + mbgl-compiler-options + ace_napi.z + ace_ndk.z + hilog_ndk.z + native_window + $<$:EGL> + $<$:GLESv3> + $<$:vulkan> +) + +mln_ohos_link_core_whole_archive(maplibre_native_ohos) diff --git a/platform/ohos/sample/entry/src/main/cpp/camera_bounds_options.hpp b/platform/ohos/sample/entry/src/main/cpp/camera_bounds_options.hpp new file mode 100644 index 000000000000..5b4d89cc84e5 --- /dev/null +++ b/platform/ohos/sample/entry/src/main/cpp/camera_bounds_options.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include + +namespace mbgl { +namespace ohos { + +struct CameraBoundsOptions { + LatLngBounds bounds; + EdgeInsets padding; + std::optional bearing; + std::optional pitch; +}; + +} // namespace ohos +} // namespace mbgl diff --git a/platform/ohos/sample/entry/src/main/cpp/egl_window_backend.cpp b/platform/ohos/sample/entry/src/main/cpp/egl_window_backend.cpp new file mode 100644 index 000000000000..64c186be5f25 --- /dev/null +++ b/platform/ohos/sample/entry/src/main/cpp/egl_window_backend.cpp @@ -0,0 +1,394 @@ +#include "egl_window_backend.hpp" + +#include "native_window_utils.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace mbgl { +namespace ohos { +namespace { + +void logDiagnostic(const std::string& message) { + Log::Info(Event::OpenGL, message); +} + +std::string eglErrorMessage(const char* operation) { + std::ostringstream stream; + stream << operation << " failed with EGL error 0x" << std::hex << eglGetError(); + return stream.str(); +} + +void setNativeWindowPixelFormat(OHNativeWindow* window) { + if (window == nullptr) { + return; + } + + const auto format = static_cast(NATIVEBUFFER_PIXEL_FMT_RGBA_8888); + if (OH_NativeWindow_NativeWindowHandleOpt(window, SET_FORMAT, format) != 0) { + Log::Warning(Event::OpenGL, "OH_NativeWindow_NativeWindowHandleOpt SET_FORMAT failed"); + } +} + +void logNativeWindowFormat(OHNativeWindow* window) { + if (window == nullptr) { + return; + } + + std::int32_t format = 0; + if (OH_NativeWindow_NativeWindowHandleOpt(window, GET_FORMAT, &format) == 0) { + logDiagnostic("Native window pixel format: " + std::to_string(format)); + } +} + +struct PixelFormatPreference { + EGLint red; + EGLint green; + EGLint blue; + EGLint alpha; +}; + +EGLint getConfigAttribute(EGLDisplay display, EGLConfig config, EGLint attribute) { + EGLint value = 0; + if (!eglGetConfigAttrib(display, config, attribute, &value)) { + return -1; + } + return value; +} + +std::string glString(GLenum name) { + const auto* value = platform::glGetString(name); + return value ? reinterpret_cast(value) : ""; +} + +std::string formatEGLConfig(EGLDisplay display, EGLConfig config, std::int32_t contextClientVersion) { + std::ostringstream stream; + stream << "EGL config" + << " id=" << getConfigAttribute(display, config, EGL_CONFIG_ID) + << " visual=" << getConfigAttribute(display, config, EGL_NATIVE_VISUAL_ID) + << " rgba=" << getConfigAttribute(display, config, EGL_RED_SIZE) << '/' + << getConfigAttribute(display, config, EGL_GREEN_SIZE) << '/' + << getConfigAttribute(display, config, EGL_BLUE_SIZE) << '/' + << getConfigAttribute(display, config, EGL_ALPHA_SIZE) + << " depth=" << getConfigAttribute(display, config, EGL_DEPTH_SIZE) + << " stencil=" << getConfigAttribute(display, config, EGL_STENCIL_SIZE) << " surface=0x" << std::hex + << getConfigAttribute(display, config, EGL_SURFACE_TYPE) << " renderable=0x" + << getConfigAttribute(display, config, EGL_RENDERABLE_TYPE) << std::dec << " es=" << contextClientVersion; + return stream.str(); +} + +std::string formatDefaultFramebuffer() { + platform::GLint red = 0; + platform::GLint green = 0; + platform::GLint blue = 0; + platform::GLint alpha = 0; + platform::GLint depth = 0; + platform::GLint stencil = 0; + platform::glGetIntegerv(GL_RED_BITS, &red); + platform::glGetIntegerv(GL_GREEN_BITS, &green); + platform::glGetIntegerv(GL_BLUE_BITS, &blue); + platform::glGetIntegerv(GL_ALPHA_BITS, &alpha); + platform::glGetIntegerv(GL_DEPTH_BITS, &depth); + platform::glGetIntegerv(GL_STENCIL_BITS, &stencil); + const auto framebufferStatus = platform::glCheckFramebufferStatus(GL_FRAMEBUFFER); + const auto error = platform::glGetError(); + + std::ostringstream stream; + stream << "GL default framebuffer" + << " rgba=" << red << '/' << green << '/' << blue << '/' << alpha << " depth=" << depth + << " stencil=" << stencil << " status=0x" << std::hex << framebufferStatus << " err=0x" << error << std::dec + << " vendor=" << glString(GL_VENDOR) << " renderer=" << glString(GL_RENDERER) + << " version=" << glString(GL_VERSION); + return stream.str(); +} + +bool tryCreateWindowSurface(EGLDisplay display, + OHNativeWindow* window, + EGLint renderableType, + EGLint contextVersion, + PixelFormatPreference format, + EGLConfig& chosenConfig, + EGLContext& chosenContext, + EGLSurface& chosenSurface) { + const EGLint attributes[] = { + EGL_SURFACE_TYPE, + EGL_WINDOW_BIT, + EGL_RENDERABLE_TYPE, + renderableType, + EGL_RED_SIZE, + format.red, + EGL_GREEN_SIZE, + format.green, + EGL_BLUE_SIZE, + format.blue, + EGL_ALPHA_SIZE, + format.alpha, + EGL_DEPTH_SIZE, + 16, + EGL_STENCIL_SIZE, + 8, + EGL_NONE, + }; + + std::array configs{}; + EGLint configCount = 0; + if (!eglChooseConfig(display, attributes, configs.data(), configs.size(), &configCount) || configCount <= 0) { + return false; + } + + const EGLint contextAttributes[] = {EGL_CONTEXT_CLIENT_VERSION, contextVersion, EGL_NONE}; + for (EGLint index = 0; index < configCount; ++index) { + EGLContext context = eglCreateContext(display, configs[index], EGL_NO_CONTEXT, contextAttributes); + if (context == EGL_NO_CONTEXT) { + continue; + } + + EGLSurface surface = eglCreateWindowSurface(display, configs[index], window, nullptr); + if (surface != EGL_NO_SURFACE) { + chosenConfig = configs[index]; + chosenContext = context; + chosenSurface = surface; + return true; + } + + eglDestroyContext(display, context); + eglGetError(); + } + + return false; +} + +bool createWindowEglState(EGLDisplay display, + OHNativeWindow* window, + EGLConfig& config, + EGLContext& context, + EGLSurface& surface, + std::int32_t& contextClientVersion) { + static constexpr std::array formats{ + PixelFormatPreference{8, 8, 8, 8}, + PixelFormatPreference{8, 8, 8, 0}, + PixelFormatPreference{5, 6, 5, 0}, + }; + + static constexpr EGLint renderableType = EGL_OPENGL_ES3_BIT; + static constexpr EGLint contextVersion = 3; + + for (const auto& format : formats) { + if (tryCreateWindowSurface(display, window, renderableType, contextVersion, format, config, context, surface)) { + contextClientVersion = contextVersion; + return true; + } + } + + return false; +} + +class EGLWindowRenderableResource final : public gl::RenderableResource { +public: + explicit EGLWindowRenderableResource(EGLWindowBackend& backend_) + : backend(backend_) {} + + void bind() override { + MLN_TRACE_FUNC(); + + backend.setFramebufferBinding(0); + backend.setViewport(0, 0, backend.getSize()); + } + + void swap() override { + MLN_TRACE_FUNC(); + + backend.swap(); + } + +private: + EGLWindowBackend& backend; +}; + +} // namespace + +class EGLWindowBackend::DisplayConfig { +private: + struct Key { + explicit Key() = default; + }; + +public: + explicit DisplayConfig(Key) { + display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (display == EGL_NO_DISPLAY) { + throw std::runtime_error("Failed to obtain a valid EGL display"); + } + + EGLint major = 0; + EGLint minor = 0; + if (!eglInitialize(display, &major, &minor)) { + throw std::runtime_error(eglErrorMessage("eglInitialize")); + } + + if (!eglBindAPI(EGL_OPENGL_ES_API)) { + throw std::runtime_error(eglErrorMessage("eglBindAPI")); + } + } + + ~DisplayConfig() { + if (display != EGL_NO_DISPLAY) { + eglTerminate(display); + } + } + + static std::shared_ptr create() { + static std::weak_ptr instance; + auto shared = instance.lock(); + if (!shared) { + instance = shared = std::make_shared(Key{}); + } + return shared; + } + + EGLDisplay display = EGL_NO_DISPLAY; +}; + +EGLWindowBackend::EGLWindowBackend(OHNativeWindow* window_, Size size_) + : gl::RendererBackend(gfx::ContextMode::Unique), + gfx::Renderable(size_, std::make_unique(*this)), + displayConfig(DisplayConfig::create()), + window(window_) { + MLN_TRACE_FUNC(); + + if (window == nullptr) { + throw std::invalid_argument("EGLWindowBackend requires a native window"); + } + + const auto referenceResult = OH_NativeWindow_NativeObjectReference(window); + if (referenceResult != 0) { + throw std::runtime_error("OH_NativeWindow_NativeObjectReference failed"); + } + + try { + if (!setNativeWindowBufferGeometry(window, size_)) { + Log::Warning(Event::OpenGL, "OH_NativeWindow_NativeWindowHandleOpt SET_BUFFER_GEOMETRY failed"); + } + setNativeWindowPixelFormat(window); + logNativeWindowFormat(window); + + EGLConfig windowConfig = nullptr; + if (!createWindowEglState( + displayConfig->display, window, windowConfig, eglContext, eglSurface, contextClientVersion)) { + throw std::runtime_error(eglErrorMessage("eglCreateWindowSurface")); + } + logDiagnostic(formatEGLConfig(displayConfig->display, windowConfig, contextClientVersion)); + } catch (...) { + if (eglSurface != EGL_NO_SURFACE) { + eglDestroySurface(displayConfig->display, eglSurface); + eglSurface = EGL_NO_SURFACE; + } + if (eglContext != EGL_NO_CONTEXT) { + eglDestroyContext(displayConfig->display, eglContext); + eglContext = EGL_NO_CONTEXT; + } + OH_NativeWindow_NativeObjectUnreference(window); + window = nullptr; + throw; + } +} + +EGLWindowBackend::~EGLWindowBackend() { + MLN_TRACE_FUNC(); + + if (eglContext != EGL_NO_CONTEXT && eglSurface != EGL_NO_SURFACE) { + if (!eglMakeCurrent(displayConfig->display, eglSurface, eglSurface, eglContext)) { + Log::Warning(Event::OpenGL, eglErrorMessage("eglMakeCurrent")); + } + } + + context.reset(); + + if (eglContext != EGL_NO_CONTEXT) { + if (!eglMakeCurrent(displayConfig->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) { + Log::Warning(Event::OpenGL, eglErrorMessage("eglMakeCurrent")); + } + } + + if (eglSurface != EGL_NO_SURFACE) { + if (!eglDestroySurface(displayConfig->display, eglSurface)) { + Log::Warning(Event::OpenGL, eglErrorMessage("eglDestroySurface")); + } + eglSurface = EGL_NO_SURFACE; + } + + if (eglContext != EGL_NO_CONTEXT) { + if (!eglDestroyContext(displayConfig->display, eglContext)) { + Log::Warning(Event::OpenGL, eglErrorMessage("eglDestroyContext")); + } + eglContext = EGL_NO_CONTEXT; + } + + if (window != nullptr) { + OH_NativeWindow_NativeObjectUnreference(window); + window = nullptr; + } +} + +void EGLWindowBackend::setSize(Size size_) { + size = size_; + if (!setNativeWindowBufferGeometry(window, size)) { + Log::Warning(Event::OpenGL, "OH_NativeWindow_NativeWindowHandleOpt SET_BUFFER_GEOMETRY failed"); + } +} + +void EGLWindowBackend::swap() { + MLN_TRACE_FUNC(); + + if (!eglSwapBuffers(displayConfig->display, eglSurface)) { + Log::Warning(Event::OpenGL, eglErrorMessage("eglSwapBuffers")); + } +} + +void EGLWindowBackend::activate() { + MLN_TRACE_FUNC(); + + if (!eglMakeCurrent(displayConfig->display, eglSurface, eglSurface, eglContext)) { + throw std::runtime_error(eglErrorMessage("eglMakeCurrent")); + } + if (framebufferDiagnostic.empty()) { + framebufferDiagnostic = formatDefaultFramebuffer(); + logDiagnostic(framebufferDiagnostic); + } +} + +void EGLWindowBackend::deactivate() { + MLN_TRACE_FUNC(); + + if (!eglMakeCurrent(displayConfig->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) { + throw std::runtime_error(eglErrorMessage("eglMakeCurrent")); + } +} + +gl::ProcAddress EGLWindowBackend::getExtensionFunctionPointer(const char* name) { + MLN_TRACE_FUNC(); + + return eglGetProcAddress(name); +} + +void EGLWindowBackend::updateAssumedState() { + MLN_TRACE_FUNC(); + + assumeFramebufferBinding(0); + assumeViewport(0, 0, size); +} + +} // namespace ohos +} // namespace mbgl diff --git a/platform/ohos/sample/entry/src/main/cpp/egl_window_backend.hpp b/platform/ohos/sample/entry/src/main/cpp/egl_window_backend.hpp new file mode 100644 index 000000000000..8f3ade77f392 --- /dev/null +++ b/platform/ohos/sample/entry/src/main/cpp/egl_window_backend.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include "window_backend.hpp" + +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace mbgl { +namespace ohos { + +class EGLWindowBackend final : public WindowBackend, public gl::RendererBackend, public gfx::Renderable { +public: + EGLWindowBackend(OHNativeWindow*, Size); + ~EGLWindowBackend() override; + + EGLWindowBackend(const EGLWindowBackend&) = delete; + EGLWindowBackend& operator=(const EGLWindowBackend&) = delete; + + gfx::RendererBackend& getRendererBackend() override { return *this; } + gfx::Renderable& getDefaultRenderable() override { return *this; } + + std::int32_t getGlesContextClientVersion() const override { return contextClientVersion; } + void setSize(Size) override; + void swap(); + +protected: + void activate() override; + void deactivate() override; + + gl::ProcAddress getExtensionFunctionPointer(const char*) override; + void updateAssumedState() override; + +private: + class DisplayConfig; + + std::shared_ptr displayConfig; + EGLContext eglContext = EGL_NO_CONTEXT; + EGLSurface eglSurface = EGL_NO_SURFACE; + OHNativeWindow* window = nullptr; + std::int32_t contextClientVersion = 0; + std::string framebufferDiagnostic; +}; + +} // namespace ohos +} // namespace mbgl diff --git a/platform/ohos/sample/entry/src/main/cpp/gesture_handler.cpp b/platform/ohos/sample/entry/src/main/cpp/gesture_handler.cpp new file mode 100644 index 000000000000..990ed5708a49 --- /dev/null +++ b/platform/ohos/sample/entry/src/main/cpp/gesture_handler.cpp @@ -0,0 +1,361 @@ +#include "gesture_handler.hpp" + +#include "map_view.hpp" + +#include + +#include +#include +#include +#include + +namespace mbgl { +namespace ohos { +namespace { + +struct PinchState { + float centerX = 0.0f; + float centerY = 0.0f; + double distance = 0.0; + double angle = 0.0; +}; + +constexpr auto MaxTapDuration = std::chrono::milliseconds(300); +constexpr auto MaxDoubleTapInterval = std::chrono::milliseconds(300); +constexpr double MaxTapMovement = 24.0; +constexpr double MaxDoubleTapDistance = 48.0; +constexpr double DoubleTapZoomScale = 2.0; +constexpr auto DoubleTapAnimationDuration = std::chrono::milliseconds(300); +constexpr double FlingAnimationBaseMilliseconds = 180.0; +constexpr double MinFlingVelocity = 1000.0; +constexpr double FlingVelocityDivisor = 7.0; +constexpr double FlingTiltBaseFactor = 1.5; +constexpr double FlingTiltScale = 10.0; +constexpr double FlingOffsetFactor = 0.32; +constexpr double MaxFlingDistance = 1200.0; +constexpr double FlingViewportMargin = 1.0; +constexpr double FlingVerticalPitchLimitScale = 90.0; +constexpr double Pi = 3.14159265358979323846; +constexpr double TwoPi = 2.0 * Pi; +constexpr double ShoveStartMovement = 16.0; +constexpr double ShoveVerticalRatio = 1.5; +constexpr double ShoveMaxDistanceScaleDelta = 0.06; +constexpr double ShoveMaxAngleDelta = Pi / 18.0; +constexpr double ShovePixelChangeFactor = 0.1; + +double distanceBetween(float firstX, float firstY, float secondX, float secondY) { + return std::hypot(static_cast(secondX - firstX), static_cast(secondY - firstY)); +} + +double normalizedAngleDelta(double firstAngle, double secondAngle) { + const double delta = std::remainder(secondAngle - firstAngle, TwoPi); + return std::abs(delta); +} + +double normalizedFlingVelocity(double velocityX, double velocityY, float pixelRatio) { + if (!std::isfinite(pixelRatio) || pixelRatio <= 0.0f) { + pixelRatio = 1.0f; + } + return std::hypot(velocityX / pixelRatio, velocityY / pixelRatio); +} + +std::chrono::milliseconds flingAnimationDuration(double normalizedVelocity, double pitch) { + if (!std::isfinite(pitch)) { + pitch = 0.0; + } + const double tiltFactor = std::max(0.1, FlingTiltBaseFactor + (pitch != 0.0 ? pitch / FlingTiltScale : 0.0)); + const double duration = normalizedVelocity / FlingVelocityDivisor / tiltFactor + FlingAnimationBaseMilliseconds; + return std::chrono::milliseconds(static_cast(duration)); +} + +double flingLimitForExtent(std::uint32_t extent) { + if (extent <= 2) { + return MaxFlingDistance; + } + return std::max(0.0, std::min(MaxFlingDistance, static_cast(extent) * 0.5 - FlingViewportMargin)); +} + +double clampHorizontalFlingOffset(double offset, Size surfaceSize) { + if (!std::isfinite(offset)) { + return 0.0; + } + const double limit = flingLimitForExtent(surfaceSize.width); + return std::clamp(offset, -limit, limit); +} + +double clampVerticalFlingOffset(double offset, Size surfaceSize, double pitch) { + if (!std::isfinite(offset)) { + return 0.0; + } + double positiveLimit = flingLimitForExtent(surfaceSize.height); + if (std::isfinite(pitch) && pitch > 0.0) { + positiveLimit /= 1.0 + pitch / FlingVerticalPitchLimitScale; + } + const double negativeLimit = flingLimitForExtent(surfaceSize.height); + return std::clamp(offset, -negativeLimit, positiveLimit); +} + +std::optional touchPointForEvent(const TouchEvent& event, std::optional activeId) { + if (!activeId) { + return TouchPoint{event.id, event.x, event.y}; + } + + for (const auto& point : event.points) { + if (!activeId || point.id == *activeId) { + return point; + } + } + + return std::nullopt; +} + +std::optional> touchPairForEvent(const TouchEvent& event) { + if (event.points.size() < 2) { + return std::nullopt; + } + + return std::array{ + event.points[0], + event.points[1], + }; +} + +std::optional pinchStateForPoints(const std::optional>& points) { + if (!points) { + return std::nullopt; + } + + const auto& first = (*points)[0]; + const auto& second = (*points)[1]; + const auto distance = distanceBetween(first.x, first.y, second.x, second.y); + if (!std::isfinite(distance) || distance <= 0.0) { + return std::nullopt; + } + + return PinchState{ + (first.x + second.x) * 0.5f, + (first.y + second.y) * 0.5f, + distance, + std::atan2(static_cast(second.y - first.y), static_cast(second.x - first.x)), + }; +} + +bool handleTouchAction(GestureState& state, + MapView* mapView, + TouchAction action, + std::optional point, + std::optional pinch, + bool hasMultiplePoints) { + bool needsRender = false; + + auto handleTap = [&](const TouchPoint& tap) { + if (!state.tapCandidate) { + return false; + } + + const auto now = std::chrono::steady_clock::now(); + const auto tapDuration = now - state.tapStartTime; + state.tapCandidate = false; + if (tapDuration > MaxTapDuration || + distanceBetween(tap.x, tap.y, state.tapStartX, state.tapStartY) > MaxTapMovement) { + state.lastTapActive = false; + return false; + } + + const bool doubleTap = state.lastTapActive && now - state.lastTapTime <= MaxDoubleTapInterval && + distanceBetween(tap.x, tap.y, state.lastTapX, state.lastTapY) <= MaxDoubleTapDistance; + if (doubleTap) { + state.lastTapActive = false; + if (mapView) { + mapView->flyBy(DoubleTapZoomScale, tap.x, tap.y, mbgl::AnimationOptions{DoubleTapAnimationDuration}); + needsRender = true; + } + return true; + } + + state.lastTapActive = true; + state.lastTapX = tap.x; + state.lastTapY = tap.y; + state.lastTapTime = now; + return false; + }; + + auto beginPinch = [&] { + if (!pinch) { + return; + } + state.touchActive = false; + state.tapCandidate = false; + state.lastTapActive = false; + state.touchVelocityX = 0.0; + state.touchVelocityY = 0.0; + state.pinchActive = true; + state.shoveActive = false; + state.pinchStartCenterX = pinch->centerX; + state.pinchStartCenterY = pinch->centerY; + state.pinchStartDistance = pinch->distance; + state.pinchStartAngle = pinch->angle; + state.pinchCenterX = pinch->centerX; + state.pinchCenterY = pinch->centerY; + state.pinchDistance = pinch->distance; + state.pinchAngle = pinch->angle; + if (mapView) { + mapView->setGestureInProgress(true); + } + }; + + switch (action) { + case TouchAction::Down: { + if (pinch) { + beginPinch(); + return false; + } + if (!point) { + return false; + } + state.touchActive = true; + state.pinchActive = false; + state.shoveActive = false; + state.touchId = point->id; + state.touchX = point->x; + state.touchY = point->y; + state.tapCandidate = true; + state.tapStartX = point->x; + state.tapStartY = point->y; + state.tapStartTime = std::chrono::steady_clock::now(); + state.touchSampleTime = state.tapStartTime; + state.touchVelocityX = 0.0; + state.touchVelocityY = 0.0; + if (mapView) { + mapView->setGestureInProgress(true); + } + break; + } + case TouchAction::Move: { + if (pinch) { + if (!state.pinchActive) { + beginPinch(); + return false; + } + if (!mapView) { + return false; + } + const double totalDx = pinch->centerX - state.pinchStartCenterX; + const double totalDy = pinch->centerY - state.pinchStartCenterY; + const double totalVerticalMovement = std::abs(totalDy); + const double totalHorizontalMovement = std::abs(totalDx); + const double distanceScaleDelta = state.pinchStartDistance > 0.0 + ? std::abs((pinch->distance / state.pinchStartDistance) - 1.0) + : 0.0; + const double angleDelta = normalizedAngleDelta(state.pinchStartAngle, pinch->angle); + if (!state.shoveActive && totalVerticalMovement >= ShoveStartMovement && + totalVerticalMovement >= totalHorizontalMovement * ShoveVerticalRatio && + distanceScaleDelta <= ShoveMaxDistanceScaleDelta && angleDelta <= ShoveMaxAngleDelta) { + state.shoveActive = true; + } + + const double scale = pinch->distance / state.pinchDistance; + const double dx = pinch->centerX - state.pinchCenterX; + const double dy = pinch->centerY - state.pinchCenterY; + const double previousAngle = state.pinchAngle; + state.pinchCenterX = pinch->centerX; + state.pinchCenterY = pinch->centerY; + state.pinchDistance = pinch->distance; + state.pinchAngle = pinch->angle; + if (state.shoveActive) { + mapView->pitchBy(-dy * ShovePixelChangeFactor); + return true; + } + + mapView->scaleBy(scale, pinch->centerX, pinch->centerY); + mapView->rotateBy(previousAngle, pinch->angle, pinch->centerX, pinch->centerY); + mapView->moveBy(dx, dy); + return true; + } + if (state.pinchActive) { + return false; + } + if (!state.touchActive || hasMultiplePoints) { + return false; + } + if (!point || !mapView) { + return false; + } + const auto now = std::chrono::steady_clock::now(); + const double dx = point->x - state.touchX; + const double dy = point->y - state.touchY; + state.touchX = point->x; + state.touchY = point->y; + const auto elapsed = std::chrono::duration(now - state.touchSampleTime).count(); + if (elapsed > 0.0) { + state.touchVelocityX = dx / elapsed; + state.touchVelocityY = dy / elapsed; + state.touchSampleTime = now; + } + if (distanceBetween(point->x, point->y, state.tapStartX, state.tapStartY) > MaxTapMovement) { + state.tapCandidate = false; + } + mapView->moveBy(dx, dy); + needsRender = true; + break; + } + case TouchAction::Up: + case TouchAction::Cancel: { + const bool canHandleTap = action == TouchAction::Up && !state.pinchActive && point; + const double velocity = mapView ? normalizedFlingVelocity( + state.touchVelocityX, state.touchVelocityY, mapView->getPixelRatio()) + : 0.0; + const bool shouldFling = action == TouchAction::Up && !state.tapCandidate && !state.pinchActive && + std::isfinite(velocity) && velocity >= MinFlingVelocity; + state.touchActive = false; + state.pinchActive = false; + state.shoveActive = false; + if (mapView) { + mapView->setGestureInProgress(false); + if (!canHandleTap || !handleTap(*point)) { + if (shouldFling) { + const double pitch = mapView->getCameraOptions().pitch.value_or(0.0); + const auto duration = flingAnimationDuration(velocity, pitch); + const double durationSeconds = std::chrono::duration(duration).count(); + const auto surfaceSize = mapView->getSurfaceSize(); + const double offsetX = state.touchVelocityX * durationSeconds * FlingOffsetFactor; + const double offsetY = state.touchVelocityY * durationSeconds * FlingOffsetFactor; + mapView->moveBy(clampHorizontalFlingOffset(offsetX, surfaceSize), + clampVerticalFlingOffset(offsetY, surfaceSize, pitch), + mbgl::AnimationOptions{duration}); + } + needsRender = true; + } + } + state.tapCandidate = false; + state.touchVelocityX = 0.0; + state.touchVelocityY = 0.0; + break; + } + case TouchAction::Unknown: + break; + } + + return needsRender; +} + +} // namespace + +bool hasActiveGesture(const GestureState& state) { + return state.touchActive || state.pinchActive; +} + +void resetGestureState(GestureState& state) { + state = GestureState(); +} + +bool handleTouchEvent(GestureState& state, MapView* mapView, const TouchEvent& event) { + const auto action = event.action; + const auto point = action == TouchAction::Move ? touchPointForEvent(event, state.touchId) + : touchPointForEvent(event, std::nullopt); + const auto pinch = pinchStateForPoints(touchPairForEvent(event)); + return handleTouchAction(state, mapView, action, point, pinch, event.points.size() > 1); +} + +} // namespace ohos +} // namespace mbgl diff --git a/platform/ohos/sample/entry/src/main/cpp/gesture_handler.hpp b/platform/ohos/sample/entry/src/main/cpp/gesture_handler.hpp new file mode 100644 index 000000000000..c2f324b9ef85 --- /dev/null +++ b/platform/ohos/sample/entry/src/main/cpp/gesture_handler.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace ohos { + +class MapView; + +struct GestureState { + bool touchActive = false; + int32_t touchId = 0; + float touchX = 0.0f; + float touchY = 0.0f; + bool tapCandidate = false; + float tapStartX = 0.0f; + float tapStartY = 0.0f; + std::chrono::steady_clock::time_point tapStartTime; + std::chrono::steady_clock::time_point touchSampleTime; + double touchVelocityX = 0.0; + double touchVelocityY = 0.0; + bool lastTapActive = false; + float lastTapX = 0.0f; + float lastTapY = 0.0f; + std::chrono::steady_clock::time_point lastTapTime; + bool pinchActive = false; + bool shoveActive = false; + float pinchStartCenterX = 0.0f; + float pinchStartCenterY = 0.0f; + double pinchStartDistance = 0.0; + double pinchStartAngle = 0.0; + float pinchCenterX = 0.0f; + float pinchCenterY = 0.0f; + double pinchDistance = 0.0; + double pinchAngle = 0.0; +}; + +enum class TouchAction { + Down, + Move, + Up, + Cancel, + Unknown, +}; + +struct TouchPoint { + int32_t id = 0; + float x = 0.0f; + float y = 0.0f; +}; + +struct TouchEvent { + TouchAction action = TouchAction::Unknown; + int32_t id = 0; + float x = 0.0f; + float y = 0.0f; + std::vector points; +}; + +bool hasActiveGesture(const GestureState&); +void resetGestureState(GestureState&); +bool handleTouchEvent(GestureState&, MapView*, const TouchEvent&); + +} // namespace ohos +} // namespace mbgl diff --git a/platform/ohos/sample/entry/src/main/cpp/map_view.cpp b/platform/ohos/sample/entry/src/main/cpp/map_view.cpp new file mode 100644 index 000000000000..c9dfe00b790d --- /dev/null +++ b/platform/ohos/sample/entry/src/main/cpp/map_view.cpp @@ -0,0 +1,571 @@ +#include "map_view.hpp" + +#if MLN_RENDER_BACKEND_VULKAN +#include "vulkan_window_backend.hpp" +#else +#include "egl_window_backend.hpp" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace mbgl { +namespace ohos { +namespace { + +std::uint32_t logicalDimensionForFramebufferDimension(const std::uint32_t dimension, const float pixelRatio) { + return std::max(1, static_cast(std::ceil(dimension / pixelRatio))); +} + +Size logicalSizeForFramebufferSize(const Size size, const float pixelRatio) { + return {logicalDimensionForFramebufferDimension(size.width, pixelRatio), + logicalDimensionForFramebufferDimension(size.height, pixelRatio)}; +} + +double logicalCoordinateForFramebufferCoordinate(const double coordinate, const float pixelRatio) { + return coordinate / pixelRatio; +} + +ScreenCoordinate logicalCoordinateForFramebufferCoordinate(const double x, const double y, const float pixelRatio) { + return {logicalCoordinateForFramebufferCoordinate(x, pixelRatio), + logicalCoordinateForFramebufferCoordinate(y, pixelRatio)}; +} + +} // namespace + +MapView::MapView(const float pixelRatio_) + : pixelRatio(pixelRatio_) { + if (!std::isfinite(pixelRatio) || pixelRatio <= 0.0f) { + throw std::invalid_argument("Pixel ratio must be a finite positive value"); + } +} + +MapView::~MapView() { + clearSurface(); +} + +void MapView::setSurface(OHNativeWindow* newWindow, Size size) { + if (newWindow == nullptr || size.isEmpty()) { + clearSurface(); + return; + } + + if (backend && window == newWindow) { + setSize(size); + return; + } + + createMap(newWindow, size); +} + +void MapView::clearSurface() { + map.reset(); + frontend.reset(); + backend.reset(); + window = nullptr; + surfaceSize = {}; + resetRuntimeState(); +} + +bool MapView::renderFrame() { + bool attemptedRender = false; + bool rendered = false; + auto renderIfNeeded = [&] { + if (frontend && frontend->hasPendingRender()) { + attemptedRender = true; + if (frontend->renderFrame()) { + ++renderedFrameCount; + rendered = true; + } + } + }; + + // Keep interaction frames from waiting behind a burst of resource callbacks. + renderIfNeeded(); + runLoopOnce(); + if (!attemptedRender) { + renderIfNeeded(); + } + + return rendered; +} + +void MapView::runLoopOnce() { + util::RunLoop::Get()->runOnce(); +} + +void MapView::reduceMemoryUse() { + if (frontend) { + frontend->reduceMemoryUse(); + } +} + +void MapView::setStyleURL(const std::string& url) { + if (!map) { + return; + } + + resetRuntimeState(); + map->getStyle().loadURL(url); +} + +void MapView::setStyleJSON(const std::string& json) { + if (map) { + resetRuntimeState(); + map->getStyle().loadJSON(json); + } +} + +void MapView::jumpTo(CameraOptions cameraOptions) { + desiredCameraBounds.reset(); + desiredFreeCamera.reset(); + desiredCamera = mergeCameraOptions(cameraOptions); + if (map) { + map->jumpTo(std::move(cameraOptions)); + } +} + +void MapView::easeTo(CameraOptions cameraOptions, AnimationOptions animationOptions) { + desiredCameraBounds.reset(); + desiredFreeCamera.reset(); + desiredCamera = mergeCameraOptions(cameraOptions); + if (map) { + map->easeTo(std::move(cameraOptions), std::move(animationOptions)); + } +} + +void MapView::flyTo(CameraOptions cameraOptions, AnimationOptions animationOptions) { + desiredCameraBounds.reset(); + desiredFreeCamera.reset(); + desiredCamera = mergeCameraOptions(cameraOptions); + if (map) { + map->flyTo(std::move(cameraOptions), std::move(animationOptions)); + } +} + +void MapView::fitBounds(CameraBoundsOptions options) { + desiredFreeCamera.reset(); + desiredCameraBounds = options; + if (map) { + desiredCamera = cameraForBounds(options); + map->jumpTo(*desiredCamera); + } +} + +void MapView::setFreeCameraOptions(FreeCameraOptions cameraOptions) { + if (!cameraOptions.position && !cameraOptions.orientation) { + return; + } + + desiredCamera.reset(); + desiredCameraBounds.reset(); + if (map) { + map->setFreeCameraOptions(cameraOptions); + desiredFreeCamera = map->getFreeCameraOptions(); + } else { + desiredFreeCamera = mergeFreeCameraOptions(cameraOptions); + } +} + +void MapView::setGestureInProgress(bool inProgress) { + if (map) { + map->setGestureInProgress(inProgress); + } +} + +void MapView::moveBy(double x, double y, AnimationOptions animationOptions) { + if (!map) { + return; + } + + map->moveBy(logicalCoordinateForFramebufferCoordinate(x, y, pixelRatio), std::move(animationOptions)); + desiredCameraBounds.reset(); + desiredFreeCamera.reset(); + desiredCamera = map->getCameraOptions(); +} + +void MapView::pitchBy(double deltaPitch) { + if (!map || !std::isfinite(deltaPitch)) { + return; + } + + const auto currentCamera = map->getCameraOptions(); + const auto bounds = map->getBounds(); + const double minPitch = bounds.minPitch.value_or(0.0); + const double maxPitch = bounds.maxPitch.value_or(60.0); + const double lowerPitch = std::min(minPitch, maxPitch); + const double upperPitch = std::max(minPitch, maxPitch); + const double currentPitch = currentCamera.pitch.value_or(0.0); + const double nextPitch = std::clamp(currentPitch + deltaPitch, lowerPitch, upperPitch); + map->easeTo(CameraOptions().withPitch(nextPitch), AnimationOptions{}); + desiredCameraBounds.reset(); + desiredFreeCamera.reset(); + desiredCamera = map->getCameraOptions(); +} + +void MapView::scaleBy(double scale, double anchorX, double anchorY) { + if (!map || !std::isfinite(scale) || scale <= 0.0) { + return; + } + + map->scaleBy(scale, logicalCoordinateForFramebufferCoordinate(anchorX, anchorY, pixelRatio)); + desiredCameraBounds.reset(); + desiredFreeCamera.reset(); + desiredCamera = map->getCameraOptions(); +} + +void MapView::flyBy(double scale, double anchorX, double anchorY, AnimationOptions animationOptions) { + if (!map || !std::isfinite(scale) || scale <= 0.0 || !std::isfinite(anchorX) || !std::isfinite(anchorY)) { + return; + } + + const auto currentCamera = map->getCameraOptions(); + const double currentZoom = currentCamera.zoom.value_or(0.0); + const double nextZoom = currentZoom + std::log2(scale); + map->flyTo(CameraOptions().withZoom(nextZoom).withAnchor( + logicalCoordinateForFramebufferCoordinate(anchorX, anchorY, pixelRatio)), + std::move(animationOptions)); + desiredCameraBounds.reset(); + desiredFreeCamera.reset(); + desiredCamera = map->getCameraOptions(); +} + +void MapView::rotateBy(double previousAngle, double currentAngle, double anchorX, double anchorY) { + if (!map || !std::isfinite(previousAngle) || !std::isfinite(currentAngle) || !std::isfinite(anchorX) || + !std::isfinite(anchorY)) { + return; + } + + const auto currentCamera = map->getCameraOptions(); + const double currentBearing = currentCamera.bearing.value_or(0.0); + const double bearingDelta = util::rad2deg(currentAngle - previousAngle); + map->easeTo(CameraOptions() + .withBearing(currentBearing - bearingDelta) + .withAnchor(logicalCoordinateForFramebufferCoordinate(anchorX, anchorY, pixelRatio)), + AnimationOptions{}); + desiredCameraBounds.reset(); + desiredFreeCamera.reset(); + desiredCamera = map->getCameraOptions(); +} + +CameraOptions MapView::getCameraOptions() const { + return map ? map->getCameraOptions() : desiredCamera.value_or(CameraOptions()); +} + +std::vector MapView::getStyleAttributions() const { + if (!map) { + return {}; + } + + std::set seen; + std::vector attributions; + for (const auto* source : map->getStyle().getSources()) { + if (source == nullptr) { + continue; + } + + auto attribution = source->getAttribution(); + if (!attribution) { + continue; + } + + const auto first = attribution->find_first_not_of(" \t\r\n"); + if (first == std::string::npos) { + continue; + } + + const auto last = attribution->find_last_not_of(" \t\r\n"); + auto trimmed = attribution->substr(first, last - first + 1); + if (seen.insert(trimmed).second) { + attributions.push_back(std::move(trimmed)); + } + } + return attributions; +} + +FreeCameraOptions MapView::getFreeCameraOptions() const { + return map ? map->getFreeCameraOptions() : desiredFreeCamera.value_or(FreeCameraOptions()); +} + +CameraOptions MapView::cameraForBounds(const CameraBoundsOptions& options) const { + if (!map) { + throw std::runtime_error("Camera for bounds requires an active map surface"); + } + return map->cameraForLatLngBounds(options.bounds, options.padding, options.bearing, options.pitch); +} + +void MapView::setDebugOptions(MapDebugOptions debugOptions_) { + debugOptions = debugOptions_; + if (map) { + map->setDebug(debugOptions); + } +} + +void MapView::setBounds(BoundOptions boundOptions) { + desiredBounds = mergeBoundOptions(boundOptions); + if (map) { + map->setBounds(std::move(boundOptions)); + } +} + +BoundOptions MapView::getBounds() const { + return map ? map->getBounds() : desiredBounds.value_or(BoundOptions()); +} + +bool MapView::hasPendingRender() const { + return frontend && frontend->hasPendingRender(); +} + +bool MapView::isFullyLoaded() const { + return map && map->isFullyLoaded(); +} + +std::int32_t MapView::getGlesContextClientVersion() const { + return backend ? backend->getGlesContextClientVersion() : 0; +} + +const std::string& MapView::getRendererDiagnostic() const { + static const std::string empty; + return backend ? backend->getRendererDiagnostic() : empty; +} + +void MapView::setClientOptions(std::string name, std::string version) { + if (clientName == name && clientVersion == version) { + return; + } + + clientName = std::move(name); + clientVersion = std::move(version); + if (window != nullptr && !surfaceSize.isEmpty()) { + createMap(window, surfaceSize); + } +} + +void MapView::setResourceOptions(ResourceOptions options) { + resourceOptions = std::move(options); + if (window != nullptr && !surfaceSize.isEmpty()) { + createMap(window, surfaceSize); + } +} + +void MapView::setTileCacheEnabled(bool enabled) { + tileCacheEnabled = enabled; + if (frontend) { + frontend->setTileCacheEnabled(tileCacheEnabled); + } +} + +void MapView::setPixelRatio(float pixelRatio_) { + if (!std::isfinite(pixelRatio_) || pixelRatio_ <= 0.0f) { + throw std::invalid_argument("Pixel ratio must be a finite positive value"); + } + + if (pixelRatio == pixelRatio_) { + return; + } + + pixelRatio = pixelRatio_; + if (window != nullptr && !surfaceSize.isEmpty()) { + createMap(window, surfaceSize); + } +} + +void MapView::setSize(Size size) { + surfaceSize = size; + if (backend) { + backend->setSize(size); + } + if (map) { + map->setSize(logicalSizeForFramebufferSize(size, pixelRatio)); + } +} + +void MapView::createMap(OHNativeWindow* newWindow, Size size) { + clearSurface(); + +#if MLN_RENDER_BACKEND_VULKAN + backend = std::make_unique(newWindow, size); +#else + backend = std::make_unique(newWindow, size); +#endif + frontend = std::make_unique(backend->getRendererBackend(), pixelRatio); + frontend->setTileCacheEnabled(tileCacheEnabled); + + MapOptions mapOptions; + mapOptions.withSize(logicalSizeForFramebufferSize(size, pixelRatio)) + .withPixelRatio(pixelRatio) + .withMapMode(MapMode::Continuous); + + ClientOptions clientOptions; + clientOptions.withName(clientName).withVersion(clientVersion); + + map = std::make_unique(*frontend, *this, mapOptions, resourceOptions, clientOptions); + window = newWindow; + surfaceSize = size; + map->setDebug(debugOptions); + + applyDesiredBounds(); + applyDesiredCamera(); +} + +CameraOptions MapView::mergeCameraOptions(const CameraOptions& cameraOptions) const { + CameraOptions merged = map ? map->getCameraOptions() : desiredCamera.value_or(CameraOptions()); + if (cameraOptions.center) { + merged.center = cameraOptions.center; + } + if (cameraOptions.centerAltitude) { + merged.centerAltitude = cameraOptions.centerAltitude; + } + if (cameraOptions.padding) { + merged.padding = cameraOptions.padding; + } + if (cameraOptions.anchor) { + merged.anchor = cameraOptions.anchor; + } + if (cameraOptions.zoom) { + merged.zoom = cameraOptions.zoom; + } + if (cameraOptions.bearing) { + merged.bearing = cameraOptions.bearing; + } + if (cameraOptions.pitch) { + merged.pitch = cameraOptions.pitch; + } + if (cameraOptions.roll) { + merged.roll = cameraOptions.roll; + } + if (cameraOptions.fov) { + merged.fov = cameraOptions.fov; + } + return merged; +} + +FreeCameraOptions MapView::mergeFreeCameraOptions(const FreeCameraOptions& cameraOptions) const { + FreeCameraOptions merged = map ? map->getFreeCameraOptions() : desiredFreeCamera.value_or(FreeCameraOptions()); + if (cameraOptions.position) { + merged.position = cameraOptions.position; + } + if (cameraOptions.orientation) { + merged.orientation = cameraOptions.orientation; + } + return merged; +} + +BoundOptions MapView::mergeBoundOptions(const BoundOptions& boundOptions) const { + BoundOptions merged = getBounds(); + if (boundOptions.bounds) { + merged.bounds = boundOptions.bounds; + } + if (boundOptions.minZoom) { + merged.minZoom = boundOptions.minZoom; + } + if (boundOptions.maxZoom) { + merged.maxZoom = boundOptions.maxZoom; + } + if (boundOptions.minPitch) { + merged.minPitch = boundOptions.minPitch; + } + if (boundOptions.maxPitch) { + merged.maxPitch = boundOptions.maxPitch; + } + return merged; +} + +void MapView::applyDesiredBounds() { + if (map && desiredBounds) { + map->setBounds(*desiredBounds); + } +} + +void MapView::applyDesiredCamera() { + if (!map) { + return; + } + if (desiredFreeCamera) { + map->setFreeCameraOptions(*desiredFreeCamera); + return; + } + if (desiredCameraBounds) { + desiredCamera = cameraForBounds(*desiredCameraBounds); + } + if (desiredCamera) { + map->jumpTo(*desiredCamera); + } +} + +void MapView::onDidFailLoadingMap(MapLoadError, const std::string& message) { + mapLoaded = false; + styleLoaded = false; + lastMapLoadError = message; + Log::Error(Event::Style, lastMapLoadError); +} + +void MapView::onDidFinishLoadingMap() { + mapLoaded = true; +} + +void MapView::onDidFinishLoadingStyle() { + styleLoaded = true; + if (!map) { + return; + } + try { + const auto defaultCamera = map->getStyle().getDefaultCamera(); + if (!desiredCamera && !desiredCameraBounds && !desiredFreeCamera && defaultCamera.center && + defaultCamera.zoom) { + desiredCameraBounds.reset(); + desiredCamera = defaultCamera; + map->jumpTo(defaultCamera); + return; + } + } catch (const std::exception& exception) { + Log::Error(Event::Style, std::string("Style camera jump failed: ") + exception.what()); + } catch (...) { + Log::Error(Event::Style, "Style camera jump failed"); + } + applyDesiredCamera(); +} + +void MapView::onStyleImageMissing(const std::string& id) { + lastStyleImageMissing = id; +} + +void MapView::onGlyphsError(const FontStack&, const GlyphRange&, std::exception_ptr error) { + lastGlyphsError = util::toString(error); +} + +void MapView::onSpriteError(const std::optional&, std::exception_ptr error) { + lastSpritesError = util::toString(error); +} + +void MapView::onRenderError(std::exception_ptr error) { + lastRenderError = util::toString(error); + Log::Error(Event::Render, lastRenderError); +} + +void MapView::resetRuntimeState() { + styleLoaded = false; + mapLoaded = false; + renderedFrameCount = 0; + lastMapLoadError.clear(); + lastRenderError.clear(); + lastStyleImageMissing.clear(); + lastGlyphsError.clear(); + lastSpritesError.clear(); +} + +} // namespace ohos +} // namespace mbgl diff --git a/platform/ohos/sample/entry/src/main/cpp/map_view.hpp b/platform/ohos/sample/entry/src/main/cpp/map_view.hpp new file mode 100644 index 000000000000..6d21d580964b --- /dev/null +++ b/platform/ohos/sample/entry/src/main/cpp/map_view.hpp @@ -0,0 +1,129 @@ +#pragma once + +#include "camera_bounds_options.hpp" +#include "renderer_frontend.hpp" +#include "window_backend.hpp" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace mbgl { +namespace ohos { + +class MapView final : public MapObserver { +public: + explicit MapView(float pixelRatio = 1.0f); + ~MapView() override; + + MapView(const MapView&) = delete; + MapView& operator=(const MapView&) = delete; + + void setSurface(OHNativeWindow*, Size); + void clearSurface(); + bool renderFrame(); + void reduceMemoryUse(); + + void setStyleURL(const std::string&); + void setStyleJSON(const std::string&); + void jumpTo(CameraOptions); + void easeTo(CameraOptions, AnimationOptions); + void flyTo(CameraOptions, AnimationOptions); + void fitBounds(CameraBoundsOptions); + void setFreeCameraOptions(FreeCameraOptions); + void setGestureInProgress(bool); + void moveBy(double x, double y, AnimationOptions = {}); + void pitchBy(double deltaPitch); + void scaleBy(double scale, double anchorX, double anchorY); + void flyBy(double scale, double anchorX, double anchorY, AnimationOptions = {}); + void rotateBy(double previousAngle, double currentAngle, double anchorX, double anchorY); + void setDebugOptions(MapDebugOptions); + void setBounds(BoundOptions); + void setClientOptions(std::string name, std::string version); + void setResourceOptions(ResourceOptions); + void setTileCacheEnabled(bool); + void setPixelRatio(float); + void setSize(Size); + + bool hasMap() const { return map != nullptr; } + OHNativeWindow* getNativeWindow() const { return window; } + Size getSurfaceSize() const { return surfaceSize; } + float getPixelRatio() const { return pixelRatio; } + MapDebugOptions getDebugOptions() const { return debugOptions; } + BoundOptions getBounds() const; + CameraOptions getCameraOptions() const; + FreeCameraOptions getFreeCameraOptions() const; + CameraOptions cameraForBounds(const CameraBoundsOptions&) const; + const std::string& getClientName() const { return clientName; } + const std::string& getClientVersion() const { return clientVersion; } + const ResourceOptions& getResourceOptions() const { return resourceOptions; } + std::vector getStyleAttributions() const; + bool hasPendingRender() const; + std::uint64_t getRenderedFrameCount() const { return renderedFrameCount; } + bool hasLoadedStyle() const { return styleLoaded; } + bool hasLoadedMap() const { return mapLoaded; } + bool isFullyLoaded() const; + std::int32_t getGlesContextClientVersion() const; + const std::string& getRendererDiagnostic() const; + const std::string& getLastMapLoadError() const { return lastMapLoadError; } + const std::string& getLastRenderError() const { return lastRenderError; } + const std::string& getLastStyleImageMissing() const { return lastStyleImageMissing; } + const std::string& getLastGlyphsError() const { return lastGlyphsError; } + const std::string& getLastSpritesError() const { return lastSpritesError; } + +private: + void createMap(OHNativeWindow*, Size); + CameraOptions mergeCameraOptions(const CameraOptions&) const; + FreeCameraOptions mergeFreeCameraOptions(const FreeCameraOptions&) const; + BoundOptions mergeBoundOptions(const BoundOptions&) const; + void applyDesiredBounds(); + void applyDesiredCamera(); + void runLoopOnce(); + + void onDidFailLoadingMap(MapLoadError, const std::string&) override; + void onDidFinishLoadingMap() override; + void onDidFinishLoadingStyle() override; + void onStyleImageMissing(const std::string&) override; + void onGlyphsError(const FontStack&, const GlyphRange&, std::exception_ptr) override; + void onSpriteError(const std::optional&, std::exception_ptr) override; + void onRenderError(std::exception_ptr) override; + + void resetRuntimeState(); + + float pixelRatio = 1.0f; + OHNativeWindow* window = nullptr; + Size surfaceSize; + std::unique_ptr backend; + std::unique_ptr frontend; + std::unique_ptr map; + std::optional desiredCamera; + std::optional desiredCameraBounds; + std::optional desiredFreeCamera; + std::optional desiredBounds; + MapDebugOptions debugOptions = MapDebugOptions::NoDebug; + bool tileCacheEnabled = true; + bool styleLoaded = false; + bool mapLoaded = false; + std::uint64_t renderedFrameCount = 0; + std::string lastMapLoadError; + std::string lastRenderError; + std::string lastStyleImageMissing; + std::string lastGlyphsError; + std::string lastSpritesError; + std::string clientName; + std::string clientVersion; + ResourceOptions resourceOptions; +}; + +} // namespace ohos +} // namespace mbgl diff --git a/platform/ohos/sample/entry/src/main/cpp/native_module.cpp b/platform/ohos/sample/entry/src/main/cpp/native_module.cpp new file mode 100644 index 000000000000..305184a44bd8 --- /dev/null +++ b/platform/ohos/sample/entry/src/main/cpp/native_module.cpp @@ -0,0 +1,1349 @@ +#include "map_view.hpp" + +#include "gesture_handler.hpp" +#include "native_values.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using mbgl::ohos::createCameraOptionsObject; +using mbgl::ohos::createStringValue; +using mbgl::ohos::getBool; +using mbgl::ohos::getBoundOptions; +using mbgl::ohos::getCameraOptionsObject; +using mbgl::ohos::getDouble; +using mbgl::ohos::getOptionalStringProperty; +using mbgl::ohos::getRequiredInt32Property; +using mbgl::ohos::getString; +using mbgl::ohos::isNullOrUndefined; +using mbgl::ohos::isObject; + +class SurfaceController; + +std::unordered_map>& controllersByNode() { + static std::unordered_map> controllers; + return controllers; +} + +std::unordered_map>& controllersByHolder() { + static std::unordered_map> controllers; + return controllers; +} + +std::mutex& controllerRegistryMutex() { + static std::mutex mutex; + return mutex; +} + +void registerController(ArkUI_NodeHandle node, + OH_ArkUI_SurfaceHolder* holder, + const std::shared_ptr& controller) { + std::lock_guard lock(controllerRegistryMutex()); + controllersByNode()[node] = controller; + controllersByHolder()[holder] = controller; +} + +void unregisterController(ArkUI_NodeHandle node, OH_ArkUI_SurfaceHolder* holder, const SurfaceController* controller) { + std::lock_guard lock(controllerRegistryMutex()); + + if (auto it = controllersByNode().find(node); it != controllersByNode().end()) { + const auto existing = it->second.lock(); + if (!existing || existing.get() == controller) { + controllersByNode().erase(it); + } + } + + if (auto it = controllersByHolder().find(holder); it != controllersByHolder().end()) { + const auto existing = it->second.lock(); + if (!existing || existing.get() == controller) { + controllersByHolder().erase(it); + } + } +} + +std::shared_ptr findController(ArkUI_NodeHandle node) { + std::lock_guard lock(controllerRegistryMutex()); + const auto it = controllersByNode().find(node); + if (it == controllersByNode().end()) { + return nullptr; + } + auto controller = it->second.lock(); + if (!controller) { + controllersByNode().erase(it); + } + return controller; +} + +std::shared_ptr findController(OH_ArkUI_SurfaceHolder* holder) { + std::lock_guard lock(controllerRegistryMutex()); + const auto it = controllersByHolder().find(holder); + if (it == controllersByHolder().end()) { + return nullptr; + } + auto controller = it->second.lock(); + if (!controller) { + controllersByHolder().erase(it); + } + return controller; +} + +napi_value getUndefined(napi_env env) { + napi_value result = nullptr; + napi_get_undefined(env, &result); + return result; +} + +napi_value throwError(napi_env env, const char* message) { + napi_throw_error(env, nullptr, message); + return getUndefined(env); +} + +ArkUI_NativeNodeAPI_1* nativeNodeApi() { + static auto* api = reinterpret_cast( + OH_ArkUI_QueryModuleInterfaceByName(ARKUI_NATIVE_NODE, "ArkUI_NativeNodeAPI_1")); + return api; +} + +ArkUI_NativeNodeAPI_1& requireNativeNodeApi() { + auto* api = nativeNodeApi(); + if (api == nullptr) { + throw std::runtime_error("Could not load ArkUI_NativeNodeAPI_1"); + } + return *api; +} + +std::uint32_t toSizeDimension(std::uint64_t value) { + constexpr auto maxDimension = static_cast(std::numeric_limits::max()); + if (value > maxDimension) { + return std::numeric_limits::max(); + } + return static_cast(value); +} + +mbgl::Size toSize(std::uint64_t width, std::uint64_t height) { + return {toSizeDimension(width), toSizeDimension(height)}; +} + +bool exposedResourceOptionsEqual(const mbgl::ResourceOptions& lhs, const mbgl::ResourceOptions& rhs) { + return lhs.apiKey() == rhs.apiKey() && lhs.cachePath() == rhs.cachePath() && lhs.assetPath() == rhs.assetPath(); +} + +bool isValidFrameRateRange(const OH_NativeXComponent_ExpectedRateRange& range) { + return range.min > 0 && range.max >= range.min && range.expected >= range.min && range.expected <= range.max; +} + +bool parseFrameRateRange(napi_env env, napi_value value, OH_NativeXComponent_ExpectedRateRange& range) { + return isObject(env, value) && getRequiredInt32Property(env, value, "min", range.min) && + getRequiredInt32Property(env, value, "max", range.max) && + getRequiredInt32Property(env, value, "expected", range.expected) && isValidFrameRateRange(range); +} + +void setFloatAttribute(ArkUI_NodeHandle node, + ArkUI_NodeAttributeType attribute, + float value, + const char* errorMessage) { + ArkUI_NumberValue numberValue{}; + numberValue.f32 = value; + + ArkUI_AttributeItem item{}; + item.value = &numberValue; + item.size = 1; + + if (requireNativeNodeApi().setAttribute(node, attribute, &item) != ARKUI_ERROR_CODE_NO_ERROR) { + throw std::runtime_error(errorMessage); + } +} + +ArkUI_NodeHandle createNativeXComponentNode() { + auto& api = requireNativeNodeApi(); + ArkUI_NodeHandle node = api.createNode(ARKUI_NODE_XCOMPONENT); + if (node == nullptr) { + throw std::runtime_error("Could not create native XComponent node"); + } + + try { + setFloatAttribute(node, NODE_WIDTH_PERCENT, 1.0f, "Could not set native XComponent width"); + setFloatAttribute(node, NODE_HEIGHT_PERCENT, 1.0f, "Could not set native XComponent height"); + } catch (...) { + api.disposeNode(node); + throw; + } + + return node; +} + +napi_value createStringArray(napi_env env, const std::vector& values) { + napi_value array = nullptr; + napi_create_array_with_length(env, values.size(), &array); + for (std::size_t i = 0; i < values.size(); ++i) { + napi_set_element(env, array, static_cast(i), createStringValue(env, values[i])); + } + return array; +} + +void setBoolProperty(napi_env env, napi_value object, const char* name, bool value) { + napi_value property = nullptr; + napi_get_boolean(env, value, &property); + napi_set_named_property(env, object, name, property); +} + +void setSizeProperty(napi_env env, napi_value object, const char* name, std::uint64_t value) { + napi_value property = nullptr; + napi_create_double(env, static_cast(value), &property); + napi_set_named_property(env, object, name, property); +} + +void setDoubleProperty(napi_env env, napi_value object, const char* name, double value) { + napi_value property = nullptr; + napi_create_double(env, value, &property); + napi_set_named_property(env, object, name, property); +} + +void setStringProperty(napi_env env, napi_value object, const char* name, const std::string& value) { + napi_set_named_property(env, object, name, createStringValue(env, value)); +} + +mbgl::ohos::TouchAction toTouchAction(int32_t action) { + switch (action) { + case UI_TOUCH_EVENT_ACTION_DOWN: + return mbgl::ohos::TouchAction::Down; + case UI_TOUCH_EVENT_ACTION_MOVE: + return mbgl::ohos::TouchAction::Move; + case UI_TOUCH_EVENT_ACTION_UP: + return mbgl::ohos::TouchAction::Up; + case UI_TOUCH_EVENT_ACTION_CANCEL: + return mbgl::ohos::TouchAction::Cancel; + default: + return mbgl::ohos::TouchAction::Unknown; + } +} + +class SurfaceController final : public std::enable_shared_from_this { +public: + static std::shared_ptr create(ArkUI_NodeHandle node_) { + auto controller = std::shared_ptr(new SurfaceController(node_, nullptr, false)); + try { + controller->initialize(); + } catch (...) { + controller->close(); + throw; + } + return controller; + } + + static std::shared_ptr create(ArkUI_NodeContentHandle content_) { + if (content_ == nullptr) { + throw std::invalid_argument("Expected a NodeContent host"); + } + + auto controller = std::shared_ptr( + new SurfaceController(createNativeXComponentNode(), content_, true)); + try { + controller->initialize(); + controller->addNodeToContent(); + } catch (...) { + controller->close(); + throw; + } + return controller; + } + + ~SurfaceController() { close(); } + + SurfaceController(const SurfaceController&) = delete; + SurfaceController& operator=(const SurfaceController&) = delete; + + bool isClosed() const { return closed; } + + void close() { + if (closed) { + return; + } + + closed = true; + + if (frameCallbackRegistered && node != nullptr && + OH_ArkUI_XComponent_UnregisterOnFrameCallback(node) != ARKUI_ERROR_CODE_NO_ERROR) { + mbgl::Log::Warning(mbgl::Event::Setup, "Could not unregister XComponent frame callback"); + } + frameCallbackRegistered = false; + + if (auto* api = nativeNodeApi()) { + if (touchEventRegistered && node != nullptr) { + api->unregisterNodeEvent(node, NODE_TOUCH_EVENT); + } + touchEventRegistered = false; + + if (nodeEventReceiverRegistered && node != nullptr) { + if (api->removeNodeEventReceiver(node, onNodeEvent) != ARKUI_ERROR_CODE_NO_ERROR) { + mbgl::Log::Warning(mbgl::Event::Setup, "Could not remove XComponent touch event receiver"); + } + } + nodeEventReceiverRegistered = false; + } + + if (surfaceCallbackRegistered && holder != nullptr && surfaceCallback != nullptr && + OH_ArkUI_SurfaceHolder_RemoveSurfaceCallback(holder, surfaceCallback) != ARKUI_ERROR_CODE_NO_ERROR) { + mbgl::Log::Warning(mbgl::Event::Setup, "Could not remove XComponent surface callback"); + } + surfaceCallbackRegistered = false; + + unregisterController(node, holder, this); + clearMapState(); + + if (nodeAddedToContent && content != nullptr && node != nullptr && + OH_ArkUI_NodeContent_RemoveNode(content, node) != ARKUI_ERROR_CODE_NO_ERROR) { + mbgl::Log::Warning(mbgl::Event::Setup, "Could not remove native XComponent node from NodeContent"); + } + nodeAddedToContent = false; + content = nullptr; + + if (surfaceCallback != nullptr) { + OH_ArkUI_SurfaceCallback_Dispose(surfaceCallback); + surfaceCallback = nullptr; + } + + if (holder != nullptr) { + OH_ArkUI_SurfaceHolder_Dispose(holder); + holder = nullptr; + } + + if (ownsNode && node != nullptr) { + if (auto* api = nativeNodeApi()) { + api->disposeNode(node); + } + } + + node = nullptr; + } + + void onSurfaceCreated(OH_ArkUI_SurfaceHolder* eventHolder) { + if (closed || eventHolder != holder) { + return; + } + + window = OH_ArkUI_XComponent_GetNativeWindow(holder); + surfaceVisible = window != nullptr; + if (window == nullptr) { + lastSurfaceError = "XComponent surface did not provide a native window"; + return; + } + + updateSurface(); + } + + void onSurfaceChanged(OH_ArkUI_SurfaceHolder* eventHolder, std::uint64_t width_, std::uint64_t height_) { + if (closed || eventHolder != holder) { + return; + } + + window = OH_ArkUI_XComponent_GetNativeWindow(holder); + width = width_; + height = height_; + surfaceVisible = window != nullptr; + updateSurface(); + } + + void onSurfaceDestroyed(OH_ArkUI_SurfaceHolder* eventHolder) { + if (closed || eventHolder != holder) { + return; + } + + surfaceVisible = false; + clearSurface(); + window = nullptr; + width = 0; + height = 0; + } + + void onFrame() { + if (closed) { + return; + } + + ++frameCallbackCount; + renderFrame(); + } + + void handleNodeEvent(ArkUI_NodeEvent* event) { + if (closed || event == nullptr || OH_ArkUI_NodeEvent_GetEventType(event) != NODE_TOUCH_EVENT) { + return; + } + + auto* inputEvent = OH_ArkUI_NodeEvent_GetInputEvent(event); + if (inputEvent == nullptr || OH_ArkUI_UIInputEvent_GetType(inputEvent) != ARKUI_UIINPUTEVENT_TYPE_TOUCH) { + return; + } + + auto touchEvent = createTouchEvent(inputEvent); + if (mbgl::ohos::handleTouchEvent(gesture, mapView.get(), touchEvent)) { + renderFrame(); + } + } + + void renderFrame() { + if (!closed && renderingEnabled && surfaceVisible && mapView) { + mapView->renderFrame(); + } + } + + void reduceMemoryUse() { + if (!closed && mapView) { + mapView->reduceMemoryUse(); + } + } + + void setStyleUrl(std::string style_) { + if (closed) { + return; + } + + style = std::move(style_); + ++styleGeneration; + applyDesiredStyle(); + } + + void jumpTo(mbgl::CameraOptions cameraOptions) { + if (closed) { + return; + } + + ensureMapView().jumpTo(std::move(cameraOptions)); + renderFrame(); + } + + void setPixelRatio(float newPixelRatio) { + if (closed) { + return; + } + + if (!mapView) { + if (pixelRatio == newPixelRatio) { + return; + } + mapView = std::make_unique(newPixelRatio); + } else if (mapView->getPixelRatio() == newPixelRatio) { + return; + } else { + mapView->setPixelRatio(newPixelRatio); + } + + pixelRatio = newPixelRatio; + appliedStyleGeneration = 0; + updateSurface(); + } + + void setBounds(mbgl::BoundOptions boundOptions) { + if (closed) { + return; + } + + ensureMapView().setBounds(std::move(boundOptions)); + renderFrame(); + } + + void setRenderingEnabled(bool enabled) { + if (closed) { + return; + } + + renderingEnabled = enabled; + if (enabled) { + renderFrame(); + } + } + + bool setFrameRateRange(const OH_NativeXComponent_ExpectedRateRange& range) { + if (closed || node == nullptr) { + return false; + } + + if (OH_ArkUI_XComponent_SetExpectedFrameRateRange(node, range) != ARKUI_ERROR_CODE_NO_ERROR) { + return false; + } + + frameRateRange = range; + return true; + } + + void setTileCacheEnabled(bool enabled) { + if (closed) { + return; + } + + ensureMapView().setTileCacheEnabled(enabled); + } + + void setClientOptions(std::string clientName, std::string clientVersion) { + if (closed) { + return; + } + + if (!mapView && clientName.empty() && clientVersion.empty()) { + return; + } + if (mapView && mapView->getClientName() == clientName && mapView->getClientVersion() == clientVersion) { + return; + } + + ensureMapView().setClientOptions(std::move(clientName), std::move(clientVersion)); + appliedStyleGeneration = 0; + applyDesiredStyle(); + renderFrame(); + } + + void setResourceOptions(const std::optional& apiKey, + const std::optional& cachePath, + const std::optional& assetPath) { + if (closed) { + return; + } + if (!mapView && !apiKey && !cachePath && !assetPath) { + return; + } + + auto& currentMapView = ensureMapView(); + auto resourceOptions = currentMapView.getResourceOptions().clone(); + if (apiKey) { + resourceOptions.withApiKey(*apiKey); + } + if (cachePath) { + resourceOptions.withCachePath(*cachePath); + } + if (assetPath) { + resourceOptions.withAssetPath(*assetPath); + } + + if (exposedResourceOptionsEqual(resourceOptions, currentMapView.getResourceOptions())) { + return; + } + + currentMapView.setResourceOptions(std::move(resourceOptions)); + appliedStyleGeneration = 0; + applyDesiredStyle(); + renderFrame(); + } + + float getPixelRatio() const { return mapView ? mapView->getPixelRatio() : pixelRatio; } + + std::vector getStyleAttributions() { + if (closed) { + return {}; + } + + return ensureMapView().getStyleAttributions(); + } + + mbgl::CameraOptions getCameraOptions() { + if (closed) { + return {}; + } + + return ensureMapView().getCameraOptions(); + } + + napi_value createSurfaceStateObject(napi_env env) { + updateFrameRates(); + + const bool hasWindow = window != nullptr; + const bool hasSurface = hasWindow && width > 0 && height > 0; + const bool hasMap = mapView && mapView->hasMap(); + const bool needsRender = mapView && mapView->hasPendingRender(); + const bool styleLoaded = mapView && mapView->hasLoadedStyle(); + const bool mapLoaded = mapView && mapView->hasLoadedMap(); + const bool fullyLoaded = mapView && mapView->isFullyLoaded(); + std::string backendLabel; + if (mapView && mapView->getGlesContextClientVersion() > 0) { + backendLabel = std::string{"OpenGL ES "} + std::to_string(mapView->getGlesContextClientVersion()); + } else if (mapView && !mapView->getRendererDiagnostic().empty()) { + backendLabel = "Vulkan"; + } + + napi_value object = nullptr; + napi_create_object(env, &object); + setSizeProperty(env, object, "width", width); + setSizeProperty(env, object, "height", height); + setBoolProperty(env, object, "hasWindow", hasWindow); + setBoolProperty(env, object, "hasSurface", hasSurface); + setBoolProperty(env, object, "hasMap", hasMap); + setBoolProperty(env, object, "needsRender", needsRender); + setBoolProperty(env, object, "styleLoaded", styleLoaded); + setBoolProperty(env, object, "mapLoaded", mapLoaded); + setBoolProperty(env, object, "fullyLoaded", fullyLoaded); + setDoubleProperty(env, object, "renderedFrameRate", renderedFrameRate); + setDoubleProperty(env, object, "frameCallbackRate", frameCallbackRate); + if (!backendLabel.empty()) { + setStringProperty(env, object, "backend", backendLabel); + } + setBoolProperty(env, object, "surfaceVisible", surfaceVisible); + if (!lastSurfaceError.empty()) { + setStringProperty(env, object, "lastSurfaceError", lastSurfaceError); + } + if (mapView && !mapView->getLastMapLoadError().empty()) { + setStringProperty(env, object, "lastMapLoadError", mapView->getLastMapLoadError()); + } + if (mapView && !mapView->getLastRenderError().empty()) { + setStringProperty(env, object, "lastRenderError", mapView->getLastRenderError()); + } + if (mapView && !mapView->getLastStyleImageMissing().empty()) { + setStringProperty(env, object, "lastStyleImageMissing", mapView->getLastStyleImageMissing()); + } + if (mapView && !mapView->getLastGlyphsError().empty()) { + setStringProperty(env, object, "lastGlyphsError", mapView->getLastGlyphsError()); + } + if (mapView && !mapView->getLastSpritesError().empty()) { + setStringProperty(env, object, "lastSpritesError", mapView->getLastSpritesError()); + } + return object; + } + +private: + SurfaceController(ArkUI_NodeHandle node_, ArkUI_NodeContentHandle content_, bool ownsNode_) + : node(node_), + content(content_), + ownsNode(ownsNode_) {} + + void initialize() { + if (node == nullptr) { + throw std::invalid_argument("Expected an XComponent node"); + } + + holder = OH_ArkUI_SurfaceHolder_Create(node); + if (holder == nullptr) { + throw std::runtime_error("Could not create XComponent surface holder"); + } + + surfaceCallback = OH_ArkUI_SurfaceCallback_Create(); + if (surfaceCallback == nullptr) { + throw std::runtime_error("Could not create XComponent surface callback"); + } + + OH_ArkUI_SurfaceCallback_SetSurfaceCreatedEvent(surfaceCallback, handleSurfaceCreated); + OH_ArkUI_SurfaceCallback_SetSurfaceChangedEvent(surfaceCallback, handleSurfaceChanged); + OH_ArkUI_SurfaceCallback_SetSurfaceDestroyedEvent(surfaceCallback, handleSurfaceDestroyed); + + registerController(node, holder, shared_from_this()); + + if (OH_ArkUI_SurfaceHolder_AddSurfaceCallback(holder, surfaceCallback) != ARKUI_ERROR_CODE_NO_ERROR) { + throw std::runtime_error("Could not add XComponent surface callback"); + } + surfaceCallbackRegistered = true; + + if (OH_ArkUI_XComponent_RegisterOnFrameCallback(node, onFrame) != ARKUI_ERROR_CODE_NO_ERROR) { + throw std::runtime_error("Could not register XComponent frame callback"); + } + frameCallbackRegistered = true; + + auto& api = requireNativeNodeApi(); + if (api.addNodeEventReceiver(node, onNodeEvent) != ARKUI_ERROR_CODE_NO_ERROR) { + throw std::runtime_error("Could not add XComponent touch event receiver"); + } + nodeEventReceiverRegistered = true; + + if (api.registerNodeEvent(node, NODE_TOUCH_EVENT, 0, nullptr) != ARKUI_ERROR_CODE_NO_ERROR) { + throw std::runtime_error("Could not register XComponent touch event"); + } + touchEventRegistered = true; + } + + void addNodeToContent() { + if (content == nullptr || node == nullptr) { + throw std::invalid_argument("Expected a NodeContent host"); + } + if (OH_ArkUI_NodeContent_AddNode(content, node) != ARKUI_ERROR_CODE_NO_ERROR) { + throw std::runtime_error("Could not add native XComponent node to NodeContent"); + } + nodeAddedToContent = true; + } + + static void handleSurfaceCreated(OH_ArkUI_SurfaceHolder* holder) { + if (auto controller = findController(holder)) { + controller->onSurfaceCreated(holder); + } + } + + static void handleSurfaceChanged(OH_ArkUI_SurfaceHolder* holder, std::uint64_t width, std::uint64_t height) { + if (auto controller = findController(holder)) { + controller->onSurfaceChanged(holder, width, height); + } + } + + static void handleSurfaceDestroyed(OH_ArkUI_SurfaceHolder* holder) { + if (auto controller = findController(holder)) { + controller->onSurfaceDestroyed(holder); + } + } + + static void onFrame(ArkUI_NodeHandle node, std::uint64_t, std::uint64_t) { + if (auto controller = findController(node)) { + controller->onFrame(); + } + } + + static void onNodeEvent(ArkUI_NodeEvent* event) { + if (event == nullptr) { + return; + } + if (auto controller = findController(OH_ArkUI_NodeEvent_GetNodeHandle(event))) { + controller->handleNodeEvent(event); + } + } + + float coordinateScale() const { return std::isfinite(pixelRatio) && pixelRatio > 0.0f ? pixelRatio : 1.0f; } + + mbgl::ohos::TouchPoint touchPointAt(const ArkUI_UIInputEvent* event, std::uint32_t pointerIndex) const { + const auto scale = coordinateScale(); + return { + OH_ArkUI_PointerEvent_GetPointerId(event, pointerIndex), + OH_ArkUI_PointerEvent_GetXByIndex(event, pointerIndex) * scale, + OH_ArkUI_PointerEvent_GetYByIndex(event, pointerIndex) * scale, + }; + } + + mbgl::ohos::TouchEvent createTouchEvent(const ArkUI_UIInputEvent* event) const { + mbgl::ohos::TouchEvent touchEvent; + touchEvent.action = toTouchAction(OH_ArkUI_UIInputEvent_GetAction(event)); + + const auto pointerCount = OH_ArkUI_PointerEvent_GetPointerCount(event); + touchEvent.points.reserve(pointerCount); + for (std::uint32_t i = 0; i < pointerCount; ++i) { + touchEvent.points.push_back(touchPointAt(event, i)); + } + + std::uint32_t changedPointerId = 0; + const bool hasChangedPointerId = OH_ArkUI_PointerEvent_GetChangedPointerId(event, &changedPointerId) == + ARKUI_ERROR_CODE_NO_ERROR; + + std::optional changedIndex; + if (hasChangedPointerId) { + touchEvent.id = static_cast(changedPointerId); + for (std::uint32_t i = 0; i < pointerCount; ++i) { + if (touchEvent.points[i].id == touchEvent.id) { + changedIndex = i; + break; + } + } + } else if (!touchEvent.points.empty()) { + changedIndex = 0; + } + + if (changedIndex) { + const auto& changedPoint = touchEvent.points[*changedIndex]; + touchEvent.id = changedPoint.id; + touchEvent.x = changedPoint.x; + touchEvent.y = changedPoint.y; + } else { + const auto scale = coordinateScale(); + touchEvent.x = OH_ArkUI_PointerEvent_GetX(event) * scale; + touchEvent.y = OH_ArkUI_PointerEvent_GetY(event) * scale; + } + + return touchEvent; + } + + void clearSurface() { + if (mapView) { + if (mbgl::ohos::hasActiveGesture(gesture)) { + mapView->setGestureInProgress(false); + } + mapView->clearSurface(); + } + appliedStyleGeneration = 0; + mbgl::ohos::resetGestureState(gesture); + } + + void clearMapState() { + clearSurface(); + window = nullptr; + width = 0; + height = 0; + surfaceVisible = false; + mapView.reset(); + } + + void applyDesiredStyle() { + if (closed || !mapView || !mapView->hasMap() || style.empty() || appliedStyleGeneration == styleGeneration) { + return; + } + + mapView->setStyleURL(style); + appliedStyleGeneration = styleGeneration; + } + + void updateSurface() { + if (closed) { + return; + } + + auto& currentMapView = ensureMapView(); + if (window == nullptr || width == 0 || height == 0) { + clearSurface(); + return; + } + + try { + const bool needsStyleReapply = !currentMapView.hasMap() || currentMapView.getNativeWindow() != window; + currentMapView.setSurface(window, toSize(width, height)); + if (needsStyleReapply) { + appliedStyleGeneration = 0; + } + applyDesiredStyle(); + renderFrame(); + lastSurfaceError.clear(); + } catch (const std::exception& exception) { + lastSurfaceError = exception.what(); + clearSurface(); + mbgl::Log::Error(mbgl::Event::Render, exception.what()); + } + } + + mbgl::ohos::MapView& ensureMapView() { + if (!mapView) { + mapView = std::make_unique(pixelRatio); + } + return *mapView; + } + + void updateFrameRates() { + const auto now = std::chrono::steady_clock::now(); + const auto renderedFrames = mapView ? mapView->getRenderedFrameCount() : 0; + const auto frameCallbacks = frameCallbackCount; + + if (frameRateSampleTime.time_since_epoch().count() == 0) { + frameRateSampleTime = now; + frameRateRenderedFrames = renderedFrames; + frameRateCallbacks = frameCallbacks; + return; + } + + const auto elapsed = std::chrono::duration(now - frameRateSampleTime).count(); + if (elapsed <= 0.0) { + return; + } + + renderedFrameRate = static_cast(renderedFrames - frameRateRenderedFrames) / elapsed; + frameCallbackRate = static_cast(frameCallbacks - frameRateCallbacks) / elapsed; + frameRateSampleTime = now; + frameRateRenderedFrames = renderedFrames; + frameRateCallbacks = frameCallbacks; + } + + ArkUI_NodeHandle node = nullptr; + ArkUI_NodeContentHandle content = nullptr; + OH_ArkUI_SurfaceHolder* holder = nullptr; + OH_ArkUI_SurfaceCallback* surfaceCallback = nullptr; + OHNativeWindow* window = nullptr; + std::unique_ptr mapView; + std::string style; + std::uint64_t styleGeneration = 0; + std::uint64_t appliedStyleGeneration = 0; + float pixelRatio = 1.0f; + std::uint64_t width = 0; + std::uint64_t height = 0; + bool renderingEnabled = true; + bool frameCallbackRegistered = false; + bool nodeEventReceiverRegistered = false; + bool touchEventRegistered = false; + bool surfaceCallbackRegistered = false; + bool ownsNode = false; + bool nodeAddedToContent = false; + bool closed = false; + std::uint64_t frameCallbackCount = 0; + bool surfaceVisible = false; + std::string lastSurfaceError; + mbgl::ohos::GestureState gesture; + std::optional frameRateRange; + std::chrono::steady_clock::time_point frameRateSampleTime; + std::uint64_t frameRateRenderedFrames = 0; + std::uint64_t frameRateCallbacks = 0; + double renderedFrameRate = 0.0; + double frameCallbackRate = 0.0; +}; + +using ControllerHandle = std::shared_ptr; + +napi_value destroy(napi_env env, napi_callback_info info); +napi_value renderFrame(napi_env env, napi_callback_info info); +napi_value reduceMemoryUse(napi_env env, napi_callback_info info); +napi_value setStyleUrl(napi_env env, napi_callback_info info); +napi_value jumpTo(napi_env env, napi_callback_info info); +napi_value setPixelRatio(napi_env env, napi_callback_info info); +napi_value setBounds(napi_env env, napi_callback_info info); +napi_value setRenderingEnabled(napi_env env, napi_callback_info info); +napi_value setFrameRateRange(napi_env env, napi_callback_info info); +napi_value setTileCacheEnabled(napi_env env, napi_callback_info info); +napi_value setClientOptions(napi_env env, napi_callback_info info); +napi_value setResourceOptions(napi_env env, napi_callback_info info); +napi_value getPixelRatio(napi_env env, napi_callback_info info); +napi_value getStyleAttributions(napi_env env, napi_callback_info info); +napi_value getSurfaceState(napi_env env, napi_callback_info info); +napi_value getCameraOptions(napi_env env, napi_callback_info info); + +ControllerHandle resolveController(napi_env env, napi_value thisArg) { + void* nativeController = nullptr; + if (napi_unwrap(env, thisArg, &nativeController) != napi_ok || nativeController == nullptr) { + throwError(env, "Expected a MapLibre XComponent context"); + return nullptr; + } + + auto* controller = static_cast(nativeController); + if (controller == nullptr || !*controller) { + throwError(env, "Expected a MapLibre XComponent context"); + return nullptr; + } + + return *controller; +} + +void finalizeController(napi_env, void* data, void*) { + auto* controller = static_cast(data); + if (controller != nullptr) { + if (*controller) { + (*controller)->close(); + } + delete controller; + } +} + +const napi_property_descriptor* contextProperties(std::size_t& count) { + static napi_property_descriptor properties[] = { + {"destroy", nullptr, destroy, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"getCameraOptions", nullptr, getCameraOptions, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"getPixelRatio", nullptr, getPixelRatio, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"getStyleAttributions", nullptr, getStyleAttributions, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"getSurfaceState", nullptr, getSurfaceState, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"jumpTo", nullptr, jumpTo, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"reduceMemoryUse", nullptr, reduceMemoryUse, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"renderFrame", nullptr, renderFrame, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"setBounds", nullptr, setBounds, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"setClientOptions", nullptr, setClientOptions, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"setFrameRateRange", nullptr, setFrameRateRange, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"setPixelRatio", nullptr, setPixelRatio, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"setRenderingEnabled", nullptr, setRenderingEnabled, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"setResourceOptions", nullptr, setResourceOptions, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"setTileCacheEnabled", nullptr, setTileCacheEnabled, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"setStyleUrl", nullptr, setStyleUrl, nullptr, nullptr, nullptr, napi_default, nullptr}, + }; + count = sizeof(properties) / sizeof(properties[0]); + return properties; +} + +napi_value createContextObject(napi_env env, ControllerHandle controller) { + napi_value object = nullptr; + if (napi_create_object(env, &object) != napi_ok || object == nullptr) { + return throwError(env, "Could not create MapLibre XComponent context"); + } + + std::size_t propertyCount = 0; + const auto* properties = contextProperties(propertyCount); + if (napi_define_properties(env, object, propertyCount, properties) != napi_ok) { + return throwError(env, "Could not define MapLibre XComponent context"); + } + + auto* boxedController = new ControllerHandle(std::move(controller)); + if (napi_wrap(env, object, boxedController, finalizeController, nullptr, nullptr) != napi_ok) { + delete boxedController; + return throwError(env, "Could not wrap MapLibre XComponent context"); + } + + return object; +} + +ControllerHandle createController(ArkUI_NodeContentHandle content) { + return SurfaceController::create(content); +} + +napi_value createMap(napi_env env, napi_callback_info info) { + std::size_t argc = 1; + napi_value argv[1] = {nullptr}; + if (napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr) != napi_ok || argc < 1 || argv[0] == nullptr) { + return throwError(env, "Expected a NodeContent host"); + } + + ArkUI_NodeContentHandle content = nullptr; + if (OH_ArkUI_GetNodeContentFromNapiValue(env, argv[0], &content) != ARKUI_ERROR_CODE_NO_ERROR || + content == nullptr) { + return throwError(env, "Expected a NodeContent host"); + } + + try { + return createContextObject(env, createController(content)); + } catch (const std::exception& exception) { + return throwError(env, exception.what()); + } +} + +napi_value destroy(napi_env env, napi_callback_info info) { + std::size_t argc = 0; + napi_value thisArg = nullptr; + if (napi_get_cb_info(env, info, &argc, nullptr, &thisArg, nullptr) != napi_ok) { + return throwError(env, "Expected a MapLibre XComponent context"); + } + + if (auto controller = resolveController(env, thisArg)) { + controller->close(); + } + return getUndefined(env); +} + +napi_value renderFrame(napi_env env, napi_callback_info info) { + std::size_t argc = 0; + napi_value thisArg = nullptr; + if (napi_get_cb_info(env, info, &argc, nullptr, &thisArg, nullptr) != napi_ok) { + return throwError(env, "Expected a MapLibre XComponent context"); + } + + if (auto controller = resolveController(env, thisArg)) { + controller->renderFrame(); + } + return getUndefined(env); +} + +napi_value reduceMemoryUse(napi_env env, napi_callback_info info) { + std::size_t argc = 0; + napi_value thisArg = nullptr; + if (napi_get_cb_info(env, info, &argc, nullptr, &thisArg, nullptr) != napi_ok) { + return throwError(env, "Expected a MapLibre XComponent context"); + } + + if (auto controller = resolveController(env, thisArg)) { + controller->reduceMemoryUse(); + } + return getUndefined(env); +} + +napi_value setStyleUrl(napi_env env, napi_callback_info info) { + std::size_t argc = 1; + napi_value argv[1] = {nullptr}; + napi_value thisArg = nullptr; + if (napi_get_cb_info(env, info, &argc, argv, &thisArg, nullptr) != napi_ok || argc < 1 || argv[0] == nullptr) { + return throwError(env, "Expected style URL"); + } + + auto controller = resolveController(env, thisArg); + if (!controller) { + return getUndefined(env); + } + + std::string style; + if (!getString(env, argv[0], style)) { + return throwError(env, "Expected style URL string"); + } + + controller->setStyleUrl(std::move(style)); + return getUndefined(env); +} + +napi_value jumpTo(napi_env env, napi_callback_info info) { + std::size_t argc = 1; + napi_value argv[1] = {nullptr}; + napi_value thisArg = nullptr; + if (napi_get_cb_info(env, info, &argc, argv, &thisArg, nullptr) != napi_ok || argc < 1 || argv[0] == nullptr) { + return throwError(env, "Expected camera options"); + } + + auto controller = resolveController(env, thisArg); + if (!controller) { + return getUndefined(env); + } + + mbgl::CameraOptions cameraOptions; + if (!getCameraOptionsObject(env, argv[0], cameraOptions)) { + return throwError(env, "Expected valid camera options object"); + } + + try { + controller->jumpTo(std::move(cameraOptions)); + } catch (const std::exception& exception) { + return throwError(env, exception.what()); + } + return getUndefined(env); +} + +napi_value setPixelRatio(napi_env env, napi_callback_info info) { + std::size_t argc = 1; + napi_value argv[1] = {nullptr}; + napi_value thisArg = nullptr; + if (napi_get_cb_info(env, info, &argc, argv, &thisArg, nullptr) != napi_ok || argc < 1 || argv[0] == nullptr) { + return throwError(env, "Expected a pixel ratio"); + } + + auto controller = resolveController(env, thisArg); + if (!controller) { + return getUndefined(env); + } + + double pixelRatio = 0.0; + if (!getDouble(env, argv[0], pixelRatio) || !std::isfinite(pixelRatio) || pixelRatio <= 0.0) { + return throwError(env, "Expected a finite positive pixel ratio"); + } + + try { + controller->setPixelRatio(static_cast(pixelRatio)); + } catch (const std::exception& exception) { + return throwError(env, exception.what()); + } + return getUndefined(env); +} + +napi_value setBounds(napi_env env, napi_callback_info info) { + std::size_t argc = 1; + napi_value argv[1] = {nullptr}; + napi_value thisArg = nullptr; + if (napi_get_cb_info(env, info, &argc, argv, &thisArg, nullptr) != napi_ok || argc < 1 || argv[0] == nullptr) { + return throwError(env, "Expected bounds options"); + } + + auto controller = resolveController(env, thisArg); + if (!controller) { + return getUndefined(env); + } + + mbgl::BoundOptions boundOptions; + if (!getBoundOptions(env, argv[0], boundOptions)) { + return throwError(env, "Expected valid bounds options object"); + } + + try { + controller->setBounds(std::move(boundOptions)); + } catch (const std::exception& exception) { + return throwError(env, exception.what()); + } + return getUndefined(env); +} + +napi_value setRenderingEnabled(napi_env env, napi_callback_info info) { + std::size_t argc = 1; + napi_value argv[1] = {nullptr}; + napi_value thisArg = nullptr; + if (napi_get_cb_info(env, info, &argc, argv, &thisArg, nullptr) != napi_ok || argc < 1 || argv[0] == nullptr) { + return throwError(env, "Expected rendering enabled boolean"); + } + + auto controller = resolveController(env, thisArg); + if (!controller) { + return getUndefined(env); + } + + bool enabled = false; + if (!getBool(env, argv[0], enabled)) { + return throwError(env, "Expected rendering enabled boolean"); + } + + controller->setRenderingEnabled(enabled); + return getUndefined(env); +} + +napi_value setFrameRateRange(napi_env env, napi_callback_info info) { + std::size_t argc = 1; + napi_value argv[1] = {nullptr}; + napi_value thisArg = nullptr; + if (napi_get_cb_info(env, info, &argc, argv, &thisArg, nullptr) != napi_ok || argc < 1 || argv[0] == nullptr) { + return throwError(env, "Expected frame-rate range"); + } + + auto controller = resolveController(env, thisArg); + if (!controller) { + return getUndefined(env); + } + + OH_NativeXComponent_ExpectedRateRange range{}; + if (!parseFrameRateRange(env, argv[0], range)) { + return throwError(env, "Expected frame-rate range object with positive min <= expected <= max"); + } + + if (!controller->setFrameRateRange(range)) { + return throwError(env, "Could not apply XComponent frame-rate range"); + } + return getUndefined(env); +} + +napi_value setTileCacheEnabled(napi_env env, napi_callback_info info) { + std::size_t argc = 1; + napi_value argv[1] = {nullptr}; + napi_value thisArg = nullptr; + if (napi_get_cb_info(env, info, &argc, argv, &thisArg, nullptr) != napi_ok || argc < 1 || argv[0] == nullptr) { + return throwError(env, "Expected tile cache enabled boolean"); + } + + auto controller = resolveController(env, thisArg); + if (!controller) { + return getUndefined(env); + } + + bool enabled = false; + if (!getBool(env, argv[0], enabled)) { + return throwError(env, "Expected tile cache enabled boolean"); + } + + controller->setTileCacheEnabled(enabled); + return getUndefined(env); +} + +napi_value setClientOptions(napi_env env, napi_callback_info info) { + std::size_t argc = 2; + napi_value argv[2] = {nullptr, nullptr}; + napi_value thisArg = nullptr; + if (napi_get_cb_info(env, info, &argc, argv, &thisArg, nullptr) != napi_ok || argc < 1 || argv[0] == nullptr) { + return throwError(env, "Expected client name and optional client version"); + } + + auto controller = resolveController(env, thisArg); + if (!controller) { + return getUndefined(env); + } + + std::string clientName; + if (!getString(env, argv[0], clientName)) { + return throwError(env, "Expected client name string"); + } + + std::string clientVersion; + if (argc > 1 && !isNullOrUndefined(env, argv[1])) { + if (!getString(env, argv[1], clientVersion)) { + return throwError(env, "Expected client version string"); + } + } + + try { + controller->setClientOptions(std::move(clientName), std::move(clientVersion)); + } catch (const std::exception& exception) { + return throwError(env, exception.what()); + } + return getUndefined(env); +} + +napi_value setResourceOptions(napi_env env, napi_callback_info info) { + std::size_t argc = 1; + napi_value argv[1] = {nullptr}; + napi_value thisArg = nullptr; + if (napi_get_cb_info(env, info, &argc, argv, &thisArg, nullptr) != napi_ok || argc < 1 || argv[0] == nullptr) { + return throwError(env, "Expected resource options"); + } + + auto controller = resolveController(env, thisArg); + if (!controller) { + return getUndefined(env); + } + if (!isObject(env, argv[0])) { + return throwError(env, "Expected resource options object"); + } + + std::optional apiKey; + std::optional cachePath; + std::optional assetPath; + if (!getOptionalStringProperty(env, argv[0], "apiKey", apiKey) || + !getOptionalStringProperty(env, argv[0], "cachePath", cachePath) || + !getOptionalStringProperty(env, argv[0], "assetPath", assetPath)) { + return throwError(env, "Expected string resource option values"); + } + + try { + controller->setResourceOptions(apiKey, cachePath, assetPath); + } catch (const std::exception& exception) { + return throwError(env, exception.what()); + } + return getUndefined(env); +} + +napi_value getPixelRatio(napi_env env, napi_callback_info info) { + std::size_t argc = 0; + napi_value thisArg = nullptr; + if (napi_get_cb_info(env, info, &argc, nullptr, &thisArg, nullptr) != napi_ok) { + return throwError(env, "Expected a MapLibre XComponent context"); + } + + auto controller = resolveController(env, thisArg); + if (!controller) { + return getUndefined(env); + } + + napi_value result = nullptr; + napi_create_double(env, controller->getPixelRatio(), &result); + return result; +} + +napi_value getStyleAttributions(napi_env env, napi_callback_info info) { + std::size_t argc = 0; + napi_value thisArg = nullptr; + if (napi_get_cb_info(env, info, &argc, nullptr, &thisArg, nullptr) != napi_ok) { + return throwError(env, "Expected a MapLibre XComponent context"); + } + + auto controller = resolveController(env, thisArg); + if (!controller) { + return getUndefined(env); + } + + return createStringArray(env, controller->getStyleAttributions()); +} + +napi_value getSurfaceState(napi_env env, napi_callback_info info) { + std::size_t argc = 0; + napi_value thisArg = nullptr; + if (napi_get_cb_info(env, info, &argc, nullptr, &thisArg, nullptr) != napi_ok) { + return throwError(env, "Expected a MapLibre XComponent context"); + } + + auto controller = resolveController(env, thisArg); + if (!controller) { + return getUndefined(env); + } + + return controller->createSurfaceStateObject(env); +} + +napi_value getCameraOptions(napi_env env, napi_callback_info info) { + std::size_t argc = 0; + napi_value thisArg = nullptr; + if (napi_get_cb_info(env, info, &argc, nullptr, &thisArg, nullptr) != napi_ok) { + return throwError(env, "Expected a MapLibre XComponent context"); + } + + auto controller = resolveController(env, thisArg); + if (!controller) { + return getUndefined(env); + } + + return createCameraOptionsObject(env, controller->getCameraOptions()); +} + +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor properties[] = { + {"createMap", nullptr, createMap, nullptr, nullptr, nullptr, napi_default, nullptr}, + }; + + napi_define_properties(env, exports, sizeof(properties) / sizeof(properties[0]), properties); + return exports; +} + +} // namespace + +NAPI_MODULE(maplibre_native_ohos, Init) diff --git a/platform/ohos/sample/entry/src/main/cpp/native_values.cpp b/platform/ohos/sample/entry/src/main/cpp/native_values.cpp new file mode 100644 index 000000000000..fe6df4ee49a9 --- /dev/null +++ b/platform/ohos/sample/entry/src/main/cpp/native_values.cpp @@ -0,0 +1,391 @@ +#include "native_values.hpp" + +#include +#include +#include +#include + +namespace mbgl { +namespace ohos { +namespace { + +bool isString(napi_env env, napi_value value) { + napi_valuetype type = napi_undefined; + return value != nullptr && napi_typeof(env, value, &type) == napi_ok && type == napi_string; +} + +bool getInt32(napi_env env, napi_value value, std::int32_t& result) { + return value != nullptr && napi_get_value_int32(env, value, &result) == napi_ok; +} + +bool getOptionalObjectProperty(napi_env env, napi_value object, const char* name, napi_value& result) { + result = nullptr; + + bool hasProperty = false; + if (napi_has_named_property(env, object, name, &hasProperty) != napi_ok || !hasProperty) { + return true; + } + + napi_value value = nullptr; + if (napi_get_named_property(env, object, name, &value) != napi_ok || isNullOrUndefined(env, value)) { + return true; + } + + if (!isObject(env, value)) { + return false; + } + + result = value; + return true; +} + +bool getOptionalFiniteDoubleProperty(napi_env env, napi_value object, const char* name, std::optional& result) { + bool hasProperty = false; + if (napi_has_named_property(env, object, name, &hasProperty) != napi_ok || !hasProperty) { + return true; + } + + napi_value value = nullptr; + if (napi_get_named_property(env, object, name, &value) != napi_ok || isNullOrUndefined(env, value)) { + return true; + } + + double number = 0.0; + if (!getDouble(env, value, number) || !std::isfinite(number)) { + return false; + } + + result = number; + return true; +} + +bool getCameraCenter(napi_env env, napi_value object, std::optional& center) { + std::optional longitude; + std::optional latitude; + if (!getOptionalFiniteDoubleProperty(env, object, "longitude", longitude) || + !getOptionalFiniteDoubleProperty(env, object, "latitude", latitude)) { + return false; + } + + napi_value centerObject = nullptr; + if (!longitude && !latitude && !getOptionalObjectProperty(env, object, "center", centerObject)) { + return false; + } + if (!longitude && !latitude && centerObject != nullptr) { + if (!getOptionalFiniteDoubleProperty(env, centerObject, "longitude", longitude) || + !getOptionalFiniteDoubleProperty(env, centerObject, "latitude", latitude)) { + return false; + } + } + + if (static_cast(longitude) != static_cast(latitude)) { + return false; + } + if (!longitude) { + return true; + } + + try { + center = LatLng(*latitude, *longitude); + } catch (...) { + return false; + } + return true; +} + +bool getCameraPadding(napi_env env, napi_value object, std::optional& padding) { + napi_value paddingObject = nullptr; + if (!getOptionalObjectProperty(env, object, "padding", paddingObject)) { + return false; + } + if (paddingObject == nullptr) { + return true; + } + + std::optional top; + std::optional left; + std::optional bottom; + std::optional right; + if (!getOptionalFiniteDoubleProperty(env, paddingObject, "top", top) || + !getOptionalFiniteDoubleProperty(env, paddingObject, "left", left) || + !getOptionalFiniteDoubleProperty(env, paddingObject, "bottom", bottom) || + !getOptionalFiniteDoubleProperty(env, paddingObject, "right", right)) { + return false; + } + + try { + padding = EdgeInsets(top.value_or(0.0), left.value_or(0.0), bottom.value_or(0.0), right.value_or(0.0)); + } catch (...) { + return false; + } + return true; +} + +bool getCameraAnchor(napi_env env, napi_value object, std::optional& anchor) { + napi_value anchorObject = nullptr; + if (!getOptionalObjectProperty(env, object, "anchor", anchorObject)) { + return false; + } + if (anchorObject == nullptr) { + return true; + } + + std::optional x; + std::optional y; + if (!getOptionalFiniteDoubleProperty(env, anchorObject, "x", x) || + !getOptionalFiniteDoubleProperty(env, anchorObject, "y", y) || static_cast(x) != static_cast(y)) { + return false; + } + if (x) { + anchor = ScreenCoordinate{*x, *y}; + } + return true; +} + +bool getOptionalLatLngBoundsProperty(napi_env env, + napi_value object, + const char* name, + std::optional& result) { + napi_value boundsObject = nullptr; + if (!getOptionalObjectProperty(env, object, name, boundsObject)) { + return false; + } + if (boundsObject == nullptr) { + return true; + } + + std::optional west; + std::optional south; + std::optional east; + std::optional north; + if (!getOptionalFiniteDoubleProperty(env, boundsObject, "west", west) || + !getOptionalFiniteDoubleProperty(env, boundsObject, "south", south) || + !getOptionalFiniteDoubleProperty(env, boundsObject, "east", east) || + !getOptionalFiniteDoubleProperty(env, boundsObject, "north", north) || !west || !south || !east || !north || + *west > *east || *south > *north) { + return false; + } + + try { + result = LatLngBounds::hull(LatLng(*south, *west), LatLng(*north, *east)); + } catch (...) { + return false; + } + return true; +} + +void setDoubleProperty(napi_env env, napi_value object, const char* name, double value) { + napi_value property = nullptr; + napi_create_double(env, value, &property); + napi_set_named_property(env, object, name, property); +} + +void setOptionalDoubleProperty(napi_env env, napi_value object, const char* name, const std::optional& value) { + if (value) { + setDoubleProperty(env, object, name, *value); + } +} + +void setCameraPaddingProperty(napi_env env, napi_value object, const EdgeInsets& padding) { + napi_value paddingObject = nullptr; + napi_create_object(env, &paddingObject); + setDoubleProperty(env, paddingObject, "top", padding.top()); + setDoubleProperty(env, paddingObject, "left", padding.left()); + setDoubleProperty(env, paddingObject, "bottom", padding.bottom()); + setDoubleProperty(env, paddingObject, "right", padding.right()); + napi_set_named_property(env, object, "padding", paddingObject); +} + +void setCameraAnchorProperty(napi_env env, napi_value object, const ScreenCoordinate& anchor) { + napi_value anchorObject = nullptr; + napi_create_object(env, &anchorObject); + setDoubleProperty(env, anchorObject, "x", anchor.x); + setDoubleProperty(env, anchorObject, "y", anchor.y); + napi_set_named_property(env, object, "anchor", anchorObject); +} + +} // namespace + +bool getString(napi_env env, napi_value value, std::string& result) { + if (!isString(env, value)) { + return false; + } + + std::size_t length = 0; + if (napi_get_value_string_utf8(env, value, nullptr, 0, &length) != napi_ok) { + return false; + } + + std::vector buffer(length + 1); + if (napi_get_value_string_utf8(env, value, buffer.data(), buffer.size(), &length) != napi_ok) { + return false; + } + + result.assign(buffer.data(), length); + return true; +} + +bool isNullOrUndefined(napi_env env, napi_value value) { + if (value == nullptr) { + return true; + } + + napi_valuetype type = napi_undefined; + return napi_typeof(env, value, &type) == napi_ok && (type == napi_null || type == napi_undefined); +} + +bool getDouble(napi_env env, napi_value value, double& result) { + return value != nullptr && napi_get_value_double(env, value, &result) == napi_ok; +} + +bool getBool(napi_env env, napi_value value, bool& result) { + return value != nullptr && napi_get_value_bool(env, value, &result) == napi_ok; +} + +bool isObject(napi_env env, napi_value value) { + napi_valuetype type = napi_undefined; + return value != nullptr && napi_typeof(env, value, &type) == napi_ok && type == napi_object; +} + +bool getOptionalStringProperty(napi_env env, napi_value object, const char* name, std::optional& result) { + bool hasProperty = false; + if (napi_has_named_property(env, object, name, &hasProperty) != napi_ok || !hasProperty) { + return true; + } + + napi_value value = nullptr; + if (napi_get_named_property(env, object, name, &value) != napi_ok || isNullOrUndefined(env, value)) { + return true; + } + + std::string valueString; + if (!getString(env, value, valueString)) { + return false; + } + + result = std::move(valueString); + return true; +} + +bool getRequiredInt32Property(napi_env env, napi_value object, const char* name, std::int32_t& result) { + bool hasProperty = false; + if (napi_has_named_property(env, object, name, &hasProperty) != napi_ok || !hasProperty) { + return false; + } + + napi_value value = nullptr; + return napi_get_named_property(env, object, name, &value) == napi_ok && getInt32(env, value, result); +} + +bool getCameraOptionsObject(napi_env env, napi_value value, CameraOptions& cameraOptions) { + if (!isObject(env, value)) { + return false; + } + + std::optional center; + std::optional padding; + std::optional anchor; + if (!getCameraCenter(env, value, center) || !getCameraPadding(env, value, padding) || + !getCameraAnchor(env, value, anchor)) { + return false; + } + + std::optional centerAltitude; + std::optional zoom; + std::optional bearing; + std::optional pitch; + std::optional roll; + std::optional fov; + if (!getOptionalFiniteDoubleProperty(env, value, "centerAltitude", centerAltitude) || + !getOptionalFiniteDoubleProperty(env, value, "zoom", zoom) || + !getOptionalFiniteDoubleProperty(env, value, "bearing", bearing) || + !getOptionalFiniteDoubleProperty(env, value, "pitch", pitch) || + !getOptionalFiniteDoubleProperty(env, value, "roll", roll) || + !getOptionalFiniteDoubleProperty(env, value, "fov", fov)) { + return false; + } + + cameraOptions.withCenter(center) + .withCenterAltitude(centerAltitude) + .withPadding(padding) + .withAnchor(anchor) + .withZoom(zoom) + .withBearing(bearing) + .withPitch(pitch) + .withRoll(roll) + .withFov(fov); + return true; +} + +bool getBoundOptions(napi_env env, napi_value value, BoundOptions& boundOptions) { + if (!isObject(env, value)) { + return false; + } + + std::optional bounds; + std::optional minZoom; + std::optional maxZoom; + std::optional minPitch; + std::optional maxPitch; + if (!getOptionalLatLngBoundsProperty(env, value, "bounds", bounds) || + !getOptionalFiniteDoubleProperty(env, value, "minZoom", minZoom) || + !getOptionalFiniteDoubleProperty(env, value, "maxZoom", maxZoom) || + !getOptionalFiniteDoubleProperty(env, value, "minPitch", minPitch) || + !getOptionalFiniteDoubleProperty(env, value, "maxPitch", maxPitch)) { + return false; + } + if (minZoom && maxZoom && *minZoom > *maxZoom) { + return false; + } + if (minPitch && maxPitch && *minPitch > *maxPitch) { + return false; + } + + if (bounds) { + boundOptions.withLatLngBounds(*bounds); + } + if (minZoom) { + boundOptions.withMinZoom(*minZoom); + } + if (maxZoom) { + boundOptions.withMaxZoom(*maxZoom); + } + if (minPitch) { + boundOptions.withMinPitch(*minPitch); + } + if (maxPitch) { + boundOptions.withMaxPitch(*maxPitch); + } + return true; +} + +napi_value createCameraOptionsObject(napi_env env, const CameraOptions& cameraOptions) { + napi_value object = nullptr; + napi_create_object(env, &object); + if (cameraOptions.center) { + setDoubleProperty(env, object, "longitude", cameraOptions.center->longitude()); + setDoubleProperty(env, object, "latitude", cameraOptions.center->latitude()); + } + setOptionalDoubleProperty(env, object, "centerAltitude", cameraOptions.centerAltitude); + if (cameraOptions.padding) { + setCameraPaddingProperty(env, object, *cameraOptions.padding); + } + if (cameraOptions.anchor) { + setCameraAnchorProperty(env, object, *cameraOptions.anchor); + } + setOptionalDoubleProperty(env, object, "zoom", cameraOptions.zoom); + setOptionalDoubleProperty(env, object, "bearing", cameraOptions.bearing); + setOptionalDoubleProperty(env, object, "pitch", cameraOptions.pitch); + setOptionalDoubleProperty(env, object, "roll", cameraOptions.roll); + setOptionalDoubleProperty(env, object, "fov", cameraOptions.fov); + return object; +} + +napi_value createStringValue(napi_env env, const std::string& value) { + napi_value result = nullptr; + napi_create_string_utf8(env, value.c_str(), value.size(), &result); + return result; +} + +} // namespace ohos +} // namespace mbgl diff --git a/platform/ohos/sample/entry/src/main/cpp/native_values.hpp b/platform/ohos/sample/entry/src/main/cpp/native_values.hpp new file mode 100644 index 000000000000..52e518490d0a --- /dev/null +++ b/platform/ohos/sample/entry/src/main/cpp/native_values.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include + +namespace mbgl { +namespace ohos { + +bool getString(napi_env env, napi_value value, std::string& result); +bool isNullOrUndefined(napi_env env, napi_value value); +bool getDouble(napi_env env, napi_value value, double& result); +bool getBool(napi_env env, napi_value value, bool& result); +bool isObject(napi_env env, napi_value value); +bool getOptionalStringProperty(napi_env env, napi_value object, const char* name, std::optional& result); +bool getRequiredInt32Property(napi_env env, napi_value object, const char* name, std::int32_t& result); + +bool getCameraOptionsObject(napi_env env, napi_value value, CameraOptions& cameraOptions); +bool getBoundOptions(napi_env env, napi_value value, BoundOptions& boundOptions); + +napi_value createCameraOptionsObject(napi_env env, const CameraOptions& cameraOptions); +napi_value createStringValue(napi_env env, const std::string& value); + +} // namespace ohos +} // namespace mbgl diff --git a/platform/ohos/sample/entry/src/main/cpp/native_window_utils.hpp b/platform/ohos/sample/entry/src/main/cpp/native_window_utils.hpp new file mode 100644 index 000000000000..c261d1010ece --- /dev/null +++ b/platform/ohos/sample/entry/src/main/cpp/native_window_utils.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include +#include + +#include +#include + +namespace mbgl { +namespace ohos { + +inline bool setNativeWindowBufferGeometry(OHNativeWindow* window, Size size) { + if (window == nullptr || size.isEmpty() || + size.width > static_cast(std::numeric_limits::max()) || + size.height > static_cast(std::numeric_limits::max())) { + return false; + } + + return OH_NativeWindow_NativeWindowHandleOpt(window, + SET_BUFFER_GEOMETRY, + static_cast(size.width), + static_cast(size.height)) == 0; +} + +} // namespace ohos +} // namespace mbgl diff --git a/platform/ohos/sample/entry/src/main/cpp/renderer_frontend.cpp b/platform/ohos/sample/entry/src/main/cpp/renderer_frontend.cpp new file mode 100644 index 000000000000..8d197d608af3 --- /dev/null +++ b/platform/ohos/sample/entry/src/main/cpp/renderer_frontend.cpp @@ -0,0 +1,82 @@ +#include "renderer_frontend.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace mbgl { +namespace ohos { + +RendererFrontend::RendererFrontend(gfx::RendererBackend& backend_, + const float pixelRatio, + std::optional localFontFamily) + : backend(backend_), + renderer(std::make_unique(backend, pixelRatio, std::move(localFontFamily))) {} + +RendererFrontend::~RendererFrontend() = default; + +void RendererFrontend::reset() { + renderer.reset(); + updateParameters.reset(); + needsRender = false; +} + +void RendererFrontend::setObserver(RendererObserver& observer) { + assert(renderer); + renderer->setObserver(&observer); +} + +void RendererFrontend::update(std::shared_ptr params) { + updateParameters = std::move(params); + needsRender = true; +} + +const TaggedScheduler& RendererFrontend::getThreadPool() const { + return backend.getThreadPool(); +} + +bool RendererFrontend::renderFrame() { + MLN_TRACE_FUNC(); + + if (!renderer || !updateParameters || !needsRender) { + return false; + } + + needsRender = false; + try { + gfx::BackendScope guard{backend}; + auto params = updateParameters; + renderer->render(params); + return true; + } catch (...) { + needsRender = true; + Log::Error(Event::Render, util::toString(std::current_exception())); + return false; + } +} + +bool RendererFrontend::hasPendingRender() const { + return renderer && updateParameters && needsRender; +} + +void RendererFrontend::setTileCacheEnabled(bool enable) { + if (renderer) { + renderer->setTileCacheEnabled(enable); + } +} + +void RendererFrontend::reduceMemoryUse() { + if (renderer) { + renderer->reduceMemoryUse(); + } +} + +} // namespace ohos +} // namespace mbgl diff --git a/platform/ohos/sample/entry/src/main/cpp/renderer_frontend.hpp b/platform/ohos/sample/entry/src/main/cpp/renderer_frontend.hpp new file mode 100644 index 000000000000..bf628b76b2ac --- /dev/null +++ b/platform/ohos/sample/entry/src/main/cpp/renderer_frontend.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace mbgl { + +class Renderer; +class UpdateParameters; + +namespace ohos { + +class RendererFrontend final : public mbgl::RendererFrontend { +public: + RendererFrontend(gfx::RendererBackend&, + float pixelRatio, + std::optional localFontFamily = std::nullopt); + ~RendererFrontend() override; + + void reset() override; + void setObserver(RendererObserver&) override; + void update(std::shared_ptr) override; + const TaggedScheduler& getThreadPool() const override; + + bool renderFrame(); + bool hasPendingRender() const; + void setTileCacheEnabled(bool); + void reduceMemoryUse(); + +private: + gfx::RendererBackend& backend; + std::unique_ptr renderer; + std::shared_ptr updateParameters; + bool needsRender = false; +}; + +} // namespace ohos +} // namespace mbgl diff --git a/platform/ohos/sample/entry/src/main/cpp/vulkan_window_backend.cpp b/platform/ohos/sample/entry/src/main/cpp/vulkan_window_backend.cpp new file mode 100644 index 000000000000..4730dca51e18 --- /dev/null +++ b/platform/ohos/sample/entry/src/main/cpp/vulkan_window_backend.cpp @@ -0,0 +1,178 @@ +#include "vulkan_window_backend.hpp" + +#include "native_window_utils.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace mbgl { +namespace ohos { +namespace { + +std::string formatVulkanDiagnostic(const vulkan::RendererBackend& backend) { + const auto& properties = backend.getDeviceProperties(); + const auto apiVersion = properties.apiVersion; + std::ostringstream stream; + stream << "Vulkan device name=\"" << properties.deviceName.data() << "\"" + << " api=" << VK_VERSION_MAJOR(apiVersion) << '.' << VK_VERSION_MINOR(apiVersion) << '.' + << VK_VERSION_PATCH(apiVersion) << " vendor=0x" << std::hex << properties.vendorID << " device=0x" + << properties.deviceID << std::dec << " graphicsQueue=" << backend.getGraphicsQueueIndex() + << " presentQueue=" << backend.getPresentQueueIndex(); + return stream.str(); +} + +std::string formatVulkanLoaderDiagnostic(const vulkan::DispatchLoaderDynamic& dispatcher) { + std::uint32_t apiVersion = VK_API_VERSION_1_0; + const auto enumerateInstanceVersion = reinterpret_cast( + dispatcher.vkGetInstanceProcAddr(nullptr, "vkEnumerateInstanceVersion")); + if (enumerateInstanceVersion) { + enumerateInstanceVersion(&apiVersion); + } + + bool hasKhrSurface = false; + bool hasOhosSurface = false; + auto extensionCount = std::size_t{0}; + try { + const auto extensions = vk::enumerateInstanceExtensionProperties(nullptr, dispatcher); + extensionCount = extensions.size(); + hasKhrSurface = std::any_of(extensions.begin(), extensions.end(), [](const vk::ExtensionProperties& value) { + return std::string_view(value.extensionName.data()) == VK_KHR_SURFACE_EXTENSION_NAME; + }); + hasOhosSurface = std::any_of(extensions.begin(), extensions.end(), [](const vk::ExtensionProperties& value) { + return std::string_view(value.extensionName.data()) == VK_OHOS_SURFACE_EXTENSION_NAME; + }); + } catch (const std::exception& exception) { + std::ostringstream stream; + stream << "Vulkan loader api=" << VK_VERSION_MAJOR(apiVersion) << '.' << VK_VERSION_MINOR(apiVersion) << '.' + << VK_VERSION_PATCH(apiVersion) << " extension enumeration failed: " << exception.what(); + return stream.str(); + } + + std::ostringstream stream; + stream << "Vulkan loader api=" << VK_VERSION_MAJOR(apiVersion) << '.' << VK_VERSION_MINOR(apiVersion) << '.' + << VK_VERSION_PATCH(apiVersion) << " instanceExtensions=" << extensionCount << " " + << VK_KHR_SURFACE_EXTENSION_NAME << '=' << (hasKhrSurface ? "yes" : "no") << " " + << VK_OHOS_SURFACE_EXTENSION_NAME << '=' << (hasOhosSurface ? "yes" : "no"); + return stream.str(); +} + +class VulkanWindowRenderableResource final : public vulkan::SurfaceRenderableResource { +public: + explicit VulkanWindowRenderableResource(VulkanWindowBackend& backend_) + : SurfaceRenderableResource(backend_) {} + + std::vector getDeviceExtensions() override { return {VK_KHR_SWAPCHAIN_EXTENSION_NAME}; } + + void createPlatformSurface() override { + auto& backendImpl = static_cast(backend); + + const auto instance = static_cast(backendImpl.getInstance().get()); + const auto createSurface = reinterpret_cast( + backendImpl.getDispatcher().vkGetInstanceProcAddr(instance, "vkCreateSurfaceOHOS")); + if (!createSurface) { + throw std::runtime_error("vkCreateSurfaceOHOS is not available"); + } + + const VkSurfaceCreateInfoOHOS createInfo{ + .sType = VK_STRUCTURE_TYPE_SURFACE_CREATE_INFO_OHOS, + .pNext = nullptr, + .flags = 0, + .window = backendImpl.getNativeWindow(), + }; + + VkSurfaceKHR rawSurface = VK_NULL_HANDLE; + const auto result = createSurface(instance, &createInfo, nullptr, &rawSurface); + if (result != VK_SUCCESS) { + throw std::runtime_error("vkCreateSurfaceOHOS failed with VkResult " + std::to_string(result)); + } + + surface = vk::UniqueSurfaceKHR( + rawSurface, + vulkan::ObjectDestroy(backendImpl.getInstance().get(), nullptr, backendImpl.getDispatcher())); + setSurfaceTransformPollingInterval(30); + } + + void bind() override {} +}; + +} // namespace + +VulkanWindowBackend::VulkanWindowBackend(OHNativeWindow* window_, Size size_) + : vulkan::RendererBackend(gfx::ContextMode::Unique), + vulkan::Renderable(size_, std::make_unique(*this)), + window(window_) { + MLN_TRACE_FUNC(); + + if (window == nullptr) { + throw std::invalid_argument("VulkanWindowBackend requires a native window"); + } + + const auto referenceResult = OH_NativeWindow_NativeObjectReference(window); + if (referenceResult != 0) { + throw std::runtime_error("OH_NativeWindow_NativeObjectReference failed"); + } + + try { + if (!setNativeWindowBufferGeometry(window, size_)) { + Log::Warning(Event::Render, "OH_NativeWindow_NativeWindowHandleOpt SET_BUFFER_GEOMETRY failed"); + } + init(); + rendererDiagnostic = formatVulkanDiagnostic(*this); + Log::Info(Event::Render, rendererDiagnostic); + } catch (...) { + setResource(nullptr); + OH_NativeWindow_NativeObjectUnreference(window); + window = nullptr; + throw; + } +} + +VulkanWindowBackend::~VulkanWindowBackend() { + MLN_TRACE_FUNC(); + + context.reset(); + setResource(nullptr); + if (window != nullptr) { + OH_NativeWindow_NativeObjectUnreference(window); + window = nullptr; + } +} + +void VulkanWindowBackend::setSize(Size size_) { + size = size_; + if (!setNativeWindowBufferGeometry(window, size)) { + Log::Warning(Event::Render, "OH_NativeWindow_NativeWindowHandleOpt SET_BUFFER_GEOMETRY failed"); + } + if (context) { + static_cast(*context).requestSurfaceUpdate(); + } +} + +void VulkanWindowBackend::initInstance() { + const auto loaderDiagnostic = formatVulkanLoaderDiagnostic(dispatcher); + Log::Info(Event::Render, loaderDiagnostic); + + try { + vulkan::RendererBackend::initInstance(); + } catch (const std::exception& exception) { + throw std::runtime_error(std::string(exception.what()) + " (" + loaderDiagnostic + ")"); + } +} + +std::vector VulkanWindowBackend::getInstanceExtensions() { + auto extensions = vulkan::RendererBackend::getInstanceExtensions(); + extensions.push_back(VK_KHR_SURFACE_EXTENSION_NAME); + extensions.push_back(VK_OHOS_SURFACE_EXTENSION_NAME); + return extensions; +} + +} // namespace ohos +} // namespace mbgl diff --git a/platform/ohos/sample/entry/src/main/cpp/vulkan_window_backend.hpp b/platform/ohos/sample/entry/src/main/cpp/vulkan_window_backend.hpp new file mode 100644 index 000000000000..294a73431a1f --- /dev/null +++ b/platform/ohos/sample/entry/src/main/cpp/vulkan_window_backend.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "window_backend.hpp" + +#include +#include +#include + +#include +#include +#include + +namespace mbgl { +namespace ohos { + +class VulkanWindowBackend final : public WindowBackend, public vulkan::RendererBackend, public vulkan::Renderable { +public: + VulkanWindowBackend(OHNativeWindow*, Size); + ~VulkanWindowBackend() override; + + VulkanWindowBackend(const VulkanWindowBackend&) = delete; + VulkanWindowBackend& operator=(const VulkanWindowBackend&) = delete; + + gfx::RendererBackend& getRendererBackend() override { return *this; } + gfx::Renderable& getDefaultRenderable() override { return *this; } + + OHNativeWindow* getNativeWindow() const { return window; } + void setSize(Size) override; + const std::string& getRendererDiagnostic() const override { return rendererDiagnostic; } + +protected: + std::vector getInstanceExtensions() override; + void initInstance() override; + void activate() override {} + void deactivate() override {} + +private: + OHNativeWindow* window = nullptr; + std::string rendererDiagnostic; +}; + +} // namespace ohos +} // namespace mbgl diff --git a/platform/ohos/sample/entry/src/main/cpp/window_backend.hpp b/platform/ohos/sample/entry/src/main/cpp/window_backend.hpp new file mode 100644 index 000000000000..80960cb82d87 --- /dev/null +++ b/platform/ohos/sample/entry/src/main/cpp/window_backend.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include +#include + +namespace mbgl { +namespace ohos { + +class WindowBackend { +public: + virtual ~WindowBackend() = default; + + virtual gfx::RendererBackend& getRendererBackend() = 0; + virtual void setSize(Size) = 0; + + virtual std::int32_t getGlesContextClientVersion() const { return 0; } + virtual const std::string& getRendererDiagnostic() const { return emptyDiagnostic(); } + +private: + static const std::string& emptyDiagnostic() { + static const std::string empty; + return empty; + } +}; + +} // namespace ohos +} // namespace mbgl diff --git a/platform/ohos/sample/entry/src/main/ets/entryability/EntryAbility.ets b/platform/ohos/sample/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 000000000000..046ea0807bbf --- /dev/null +++ b/platform/ohos/sample/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,16 @@ +import { UIAbility } from '@kit.AbilityKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { window } from '@kit.ArkUI'; + +const LOG_DOMAIN: number = 0x4d4c4e; +const LOG_TAG: string = 'MapLibreSample'; + +export default class EntryAbility extends UIAbility { + onWindowStageCreate(windowStage: window.WindowStage): void { + windowStage.loadContent('pages/Index', (error) => { + if (error.code) { + hilog.error(LOG_DOMAIN, LOG_TAG, 'Could not load page: %{public}s', JSON.stringify(error)); + } + }); + } +} diff --git a/platform/ohos/sample/entry/src/main/ets/map/Attribution.ets b/platform/ohos/sample/entry/src/main/ets/map/Attribution.ets new file mode 100644 index 000000000000..ec5994385727 --- /dev/null +++ b/platform/ohos/sample/entry/src/main/ets/map/Attribution.ets @@ -0,0 +1,71 @@ +export interface AttributionItem { + key: string; + label: string; + url?: string; +} + +function decodeHtmlEntities(value: string): string { + return value + .replace(/©/g, '(c)') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/\s+/g, ' ') + .trim(); +} + +function stripHtml(value: string): string { + return decodeHtmlEntities(value.replace(/<[^>]*>/g, '')); +} + +function appendItem(items: AttributionItem[], label: string, url?: string): void { + const trimmed = label.replace(/\s+/g, ' ').trim(); + if (trimmed.length === 0) { + return; + } + + items.push({ + key: items.length.toString() + ':' + trimmed + ':' + (url ?? ''), + label: trimmed, + url: url + }); +} + +function appendHtml(items: AttributionItem[], value: string): void { + const anchorPattern: RegExp = /]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/g; + let cursor = 0; + let match: RegExpExecArray | null = anchorPattern.exec(value); + while (match !== null) { + if (match.index > cursor) { + appendItem(items, stripHtml(value.slice(cursor, match.index))); + } + + appendItem(items, stripHtml(match[2]), decodeHtmlEntities(match[1])); + cursor = match.index + match[0].length; + match = anchorPattern.exec(value); + } + + if (cursor < value.length) { + appendItem(items, stripHtml(value.slice(cursor))); + } +} + +export function parseAttributions(values: string[]): AttributionItem[] { + const items: AttributionItem[] = []; + const seen: string[] = []; + values.forEach((value: string) => { + const trimmed = value.trim(); + if (trimmed.length === 0 || seen.indexOf(trimmed) >= 0) { + return; + } + + if (items.length > 0) { + appendItem(items, '|'); + } + seen.push(trimmed); + appendHtml(items, trimmed); + }); + return items; +} diff --git a/platform/ohos/sample/entry/src/main/ets/map/MapStatus.ets b/platform/ohos/sample/entry/src/main/ets/map/MapStatus.ets new file mode 100644 index 000000000000..38713d2d197f --- /dev/null +++ b/platform/ohos/sample/entry/src/main/ets/map/MapStatus.ets @@ -0,0 +1,78 @@ +import type { CameraOptions, SurfaceState } from 'libmaplibre_native_ohos.so'; +import { parseAttributions, type AttributionItem } from './Attribution'; + +export interface MapStatus { + text: string; + detail: string; + attributions: AttributionItem[]; +} + +function formatNumber(value: number, digits: number): string { + if (!Number.isFinite(value)) { + return 'n/a'; + } + return value.toFixed(digits); +} + +function cameraSummary(camera: CameraOptions): string { + const latitude = camera.latitude ?? camera.center?.latitude; + const longitude = camera.longitude ?? camera.center?.longitude; + let summary = ''; + if (latitude !== undefined && longitude !== undefined) { + summary = summary + ' cam=' + formatNumber(latitude, 4) + ',' + formatNumber(longitude, 4); + } + if (camera.zoom !== undefined) { + summary = summary + ' z=' + formatNumber(camera.zoom, 2); + } + return summary; +} + +function statusText(label: string, state: SurfaceState, pixelRatio: number): string { + return label + ': ' + state.width.toString() + 'x' + state.height.toString() + + ' pr=' + formatNumber(pixelRatio, 2) + + ' ' + (state.backend ?? 'unknown') + + ' fps=' + formatNumber(state.renderedFrameRate, 1) + + ' tick=' + formatNumber(state.frameCallbackRate, 1) + + ' render=' + (state.needsRender ? 'pending' : 'idle'); +} + +function statusDetail(state: SurfaceState, camera: CameraOptions): string { + if (state.lastSurfaceError !== undefined) { + return 'surface error: ' + state.lastSurfaceError; + } + if (state.lastMapLoadError !== undefined) { + return 'map error: ' + state.lastMapLoadError; + } + if (state.lastRenderError !== undefined) { + return 'render error: ' + state.lastRenderError; + } + if (state.lastGlyphsError !== undefined) { + return 'glyph error: ' + state.lastGlyphsError; + } + if (state.lastSpritesError !== undefined) { + return 'sprite error: ' + state.lastSpritesError; + } + if (state.lastStyleImageMissing !== undefined) { + return 'missing image: ' + state.lastStyleImageMissing; + } + + return 'window=' + (state.hasWindow ? 'yes' : 'no') + + ' map=' + (state.hasMap ? 'yes' : 'no') + + ' visible=' + (state.surfaceVisible ? 'yes' : 'no') + + ' style=' + (state.styleLoaded ? 'loaded' : 'pending') + + ' loaded=' + (state.mapLoaded ? 'yes' : 'no') + + ' full=' + (state.fullyLoaded ? 'yes' : 'no') + + cameraSummary(camera); +} + +export function createMapStatus(label: string, + state: SurfaceState, + camera: CameraOptions, + pixelRatio: number, + attributions: string[]): MapStatus { + return { + text: statusText(label, state, pixelRatio), + detail: statusDetail(state, camera), + attributions: parseAttributions(attributions) + }; +} diff --git a/platform/ohos/sample/entry/src/main/ets/map/SampleConfig.ets b/platform/ohos/sample/entry/src/main/ets/map/SampleConfig.ets new file mode 100644 index 000000000000..ba0cf141fe22 --- /dev/null +++ b/platform/ohos/sample/entry/src/main/ets/map/SampleConfig.ets @@ -0,0 +1,46 @@ +import type { BoundOptions, CameraOptions } from 'libmaplibre_native_ohos.so'; + +export const LOG_DOMAIN: number = 0x4d4c4e; +export const LOG_TAG: string = 'MapLibreSample'; + +export const CLIENT_NAME: string = 'MapLibreOhosSample'; +export const CLIENT_VERSION: string = '0.1.0'; + +export interface StyleOption { + name: string; + url: string; +} + +export const STYLE_OPTIONS: StyleOption[] = [ + { + name: 'Demo', + url: 'https://demotiles.maplibre.org/style.json' + }, + { + name: 'Bright', + url: 'https://tiles.openfreemap.org/styles/bright' + }, + { + name: 'Liberty', + url: 'https://tiles.openfreemap.org/styles/liberty' + } +]; + +export const DEFAULT_STYLE_INDEX: number = 1; +export const DEFAULT_STYLE: StyleOption = STYLE_OPTIONS[DEFAULT_STYLE_INDEX]; + +export const DEFAULT_CAMERA: CameraOptions = { + longitude: 106.5750, + latitude: 29.5570, + zoom: 12.0, + bearing: 0, + pitch: 0 +}; + +export const DEFAULT_BOUNDS: BoundOptions = { + bounds: { west: -180, south: -85, east: 180, north: 85 }, + minZoom: 0, + maxZoom: 22, + minPitch: 0, + maxPitch: 60 +}; diff --git a/platform/ohos/sample/entry/src/main/ets/pages/Index.ets b/platform/ohos/sample/entry/src/main/ets/pages/Index.ets new file mode 100644 index 000000000000..5ca6d69f69bb --- /dev/null +++ b/platform/ohos/sample/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,294 @@ +import { common } from '@kit.AbilityKit'; +import { display, NodeContent } from '@kit.ArkUI'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import maplibreNative from 'libmaplibre_native_ohos.so'; +import type { CameraOptions, ResourceOptions, SurfaceState, XComponentContext } from 'libmaplibre_native_ohos.so'; +import type { AttributionItem } from '../map/Attribution'; +import { createMapStatus } from '../map/MapStatus'; +import { + CLIENT_NAME, + CLIENT_VERSION, + DEFAULT_BOUNDS, + DEFAULT_CAMERA, + DEFAULT_STYLE, + LOG_DOMAIN, + LOG_TAG, + STYLE_OPTIONS, + type StyleOption +} from '../map/SampleConfig'; + +@Entry +@Component +struct Index { + @State private statusText: string = 'Waiting for XComponent'; + @State private statusDetail: string = 'No native binding'; + @State private attributionItems: AttributionItem[] = []; + + private map?: XComponentContext; + private statusTimer: number = -1; + private readonly mapNodeContent: NodeContent = new NodeContent(); + + aboutToDisappear(): void { + this.stopStatusUpdates(); + this.destroyMap(); + } + + onPageShow(): void { + if (this.map) { + this.map.setRenderingEnabled(true); + this.map.renderFrame(); + this.updateSurfaceState('Visible'); + } + this.startStatusUpdates(); + } + + onPageHide(): void { + this.stopStatusUpdates(); + if (!this.map) { + return; + } + + this.map.setRenderingEnabled(false); + this.map.reduceMemoryUse(); + this.updateSurfaceState('Hidden'); + } + + private startStatusUpdates(): void { + this.stopStatusUpdates(); + this.statusTimer = setInterval(() => { + if (this.map) { + this.map.renderFrame(); + this.updateSurfaceState('Live'); + } + }, 500); + } + + private stopStatusUpdates(): void { + if (this.statusTimer >= 0) { + clearInterval(this.statusTimer); + this.statusTimer = -1; + } + } + + private updateSurfaceState(label: string): void { + if (!this.map) { + this.clearStatus(); + return; + } + + const state: SurfaceState = this.map.getSurfaceState(); + const camera: CameraOptions = this.map.getCameraOptions(); + const pixelRatio = this.map.getPixelRatio(); + const status = createMapStatus(label, state, camera, pixelRatio, this.map.getStyleAttributions()); + this.statusText = status.text; + this.statusDetail = status.detail; + this.attributionItems = status.attributions; + } + + private clearStatus(): void { + this.statusText = 'No native binding'; + this.statusDetail = 'Waiting for XComponent'; + this.attributionItems = []; + } + + private destroyMap(): void { + if (!this.map) { + return; + } + + this.map.setRenderingEnabled(false); + this.map.reduceMemoryUse(); + this.map.destroy(); + this.map = undefined; + } + + private describeError(error: Error): string { + if (error.message) { + return error.message; + } + + const serialized = JSON.stringify(error); + if (serialized && serialized !== '{}') { + return serialized; + } + + return String(error); + } + + private detachMap(): void { + this.destroyMap(); + this.statusText = 'XComponent detached'; + this.statusDetail = 'No native binding'; + this.attributionItems = []; + } + + private attachMap(): void { + this.destroyMap(); + + try { + this.configureMap(maplibreNative.createMap(this.mapNodeContent)); + } catch (error) { + const detail = this.describeError(error as Error); + this.statusText = 'Could not create native map'; + this.statusDetail = detail; + this.attributionItems = []; + hilog.error(LOG_DOMAIN, LOG_TAG, 'Could not create native map: %{public}s', detail); + } + } + + private openAttributionUrl(url: string): void { + const hostContext = this.getUIContext().getHostContext() as common.UIAbilityContext | undefined; + if (!hostContext) { + return; + } + + hostContext.startAbility({ + action: 'ohos.want.action.viewData', + uri: url + }).catch((error: Error) => { + hilog.warn(LOG_DOMAIN, LOG_TAG, 'Could not open attribution URL: %{public}s', JSON.stringify(error)); + }); + } + + private useStyle(style: StyleOption): void { + if (!this.map) { + return; + } + + this.statusText = style.name + ': loading...'; + this.map.setStyleUrl(style.url); + this.map.renderFrame(); + this.updateSurfaceState(style.name); + } + + private resourceOptions(): ResourceOptions { + const hostContext = this.getUIContext().getHostContext() as common.Context | undefined; + if (!hostContext) { + return {}; + } + + return { cachePath: hostContext.cacheDir + '/maplibre-cache.db' }; + } + + private displayPixelRatio(): number { + try { + const defaultDisplay = display.getDefaultDisplaySync(); + const pixelRatio = defaultDisplay.densityPixels; + if (Number.isFinite(pixelRatio) && pixelRatio > 0) { + return pixelRatio; + } + } catch (error) { + hilog.warn(LOG_DOMAIN, LOG_TAG, 'Could not read display density: %{public}s', JSON.stringify(error)); + } + + return 1; + } + + private displayRefreshRate(): number { + try { + const defaultDisplay = display.getDefaultDisplaySync(); + const refreshRate = Math.round(defaultDisplay.refreshRate); + if (Number.isFinite(refreshRate) && refreshRate > 0) { + return refreshRate; + } + } catch (error) { + hilog.warn(LOG_DOMAIN, LOG_TAG, 'Could not read display refresh rate: %{public}s', JSON.stringify(error)); + } + + return 60; + } + + private configureMap(map: XComponentContext): void { + this.map = map; + map.setClientOptions(CLIENT_NAME, CLIENT_VERSION); + map.setResourceOptions(this.resourceOptions()); + map.setTileCacheEnabled(true); + const refreshRate = this.displayRefreshRate(); + map.setFrameRateRange({ min: Math.min(30, refreshRate), max: refreshRate, expected: refreshRate }); + map.setPixelRatio(this.displayPixelRatio()); + map.setRenderingEnabled(true); + map.setBounds(DEFAULT_BOUNDS); + map.jumpTo(DEFAULT_CAMERA); + map.renderFrame(); + this.useStyle(DEFAULT_STYLE); + } + + build() { + Column() { + Stack({ alignContent: Alignment.TopStart }) { + Stack() { + ContentSlot(this.mapNodeContent) + } + .onAttach(() => { + this.attachMap(); + }) + .onDetach(() => { + this.detachMap(); + }) + .width('100%') + .height('100%') + + Text(this.statusText) + .fontSize(10) + .fontColor('#111827') + .maxLines(1) + .textShadow({ radius: 2, color: '#ffffff', offsetX: 0, offsetY: 0 }) + .margin({ left: 8, top: 8 }) + .hitTestBehavior(HitTestMode.Transparent) + + Text(this.statusDetail) + .fontSize(10) + .fontColor('#111827') + .maxLines(1) + .textShadow({ radius: 2, color: '#ffffff', offsetX: 0, offsetY: 0 }) + .margin({ left: 8, top: 24, right: 8 }) + .hitTestBehavior(HitTestMode.Transparent) + + Stack({ alignContent: Alignment.BottomEnd }) { + Row() { + ForEach(this.attributionItems, (item: AttributionItem) => { + Text(item.label) + .fontSize(10) + .fontColor(item.url === undefined ? '#111827' : '#0645ad') + .decoration({ + type: item.url === undefined ? TextDecorationType.None : TextDecorationType.Underline + }) + .textShadow({ radius: 2, color: '#ffffff', offsetX: 0, offsetY: 0 }) + .margin({ right: 2 }) + .onClick(() => { + if (item.url !== undefined) { + this.openAttributionUrl(item.url); + } + }) + }, (item: AttributionItem) => item.key) + } + .justifyContent(FlexAlign.End) + .width('100%') + .padding({ left: 8, right: 8, bottom: 8 }) + .hitTestBehavior(HitTestMode.Transparent) + } + .width('100%') + .height('100%') + .hitTestBehavior(HitTestMode.Transparent) + } + .width('100%') + .layoutWeight(1) + + Row() { + ForEach(STYLE_OPTIONS, (style: StyleOption) => { + Button(style.name) + .onClick(() => { + this.useStyle(style); + }) + .height(44) + }, (style: StyleOption) => style.name) + } + .width('100%') + .padding({ left: 12, right: 12, top: 8, bottom: 8 }) + .justifyContent(FlexAlign.SpaceEvenly) + .backgroundColor('#f8fafc') + } + .width('100%') + .height('100%') + } +} diff --git a/platform/ohos/sample/entry/src/main/module.json5 b/platform/ohos/sample/entry/src/main/module.json5 new file mode 100644 index 000000000000..b1ae8cf6baa0 --- /dev/null +++ b/platform/ohos/sample/entry/src/main/module.json5 @@ -0,0 +1,42 @@ +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "phone", + "tablet" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "requestPermissions": [ + { + "name": "ohos.permission.INTERNET" + } + ], + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:layered_image", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:icon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "action.system.home" + ] + } + ] + } + ] + } +} diff --git a/platform/ohos/sample/entry/src/main/resources/base/element/color.json b/platform/ohos/sample/entry/src/main/resources/base/element/color.json new file mode 100644 index 000000000000..c2d657a4c8ad --- /dev/null +++ b/platform/ohos/sample/entry/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#0f172a" + } + ] +} diff --git a/platform/ohos/sample/entry/src/main/resources/base/element/string.json b/platform/ohos/sample/entry/src/main/resources/base/element/string.json new file mode 100644 index 000000000000..c343f6482253 --- /dev/null +++ b/platform/ohos/sample/entry/src/main/resources/base/element/string.json @@ -0,0 +1,20 @@ +{ + "string": [ + { + "name": "app_name", + "value": "MapLibre Native" + }, + { + "name": "module_desc", + "value": "MapLibre Native HarmonyOS sample" + }, + { + "name": "EntryAbility_desc", + "value": "MapLibre Native sample entry ability" + }, + { + "name": "EntryAbility_label", + "value": "MapLibre Native" + } + ] +} diff --git a/platform/ohos/sample/entry/src/main/resources/base/media/icon.svg b/platform/ohos/sample/entry/src/main/resources/base/media/icon.svg new file mode 100644 index 000000000000..2cc03fecabff --- /dev/null +++ b/platform/ohos/sample/entry/src/main/resources/base/media/icon.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + diff --git a/platform/ohos/sample/entry/src/main/resources/base/media/icon_background.svg b/platform/ohos/sample/entry/src/main/resources/base/media/icon_background.svg new file mode 100644 index 000000000000..f483abd36ef8 --- /dev/null +++ b/platform/ohos/sample/entry/src/main/resources/base/media/icon_background.svg @@ -0,0 +1,4 @@ + + + + diff --git a/platform/ohos/sample/entry/src/main/resources/base/media/icon_foreground.svg b/platform/ohos/sample/entry/src/main/resources/base/media/icon_foreground.svg new file mode 100644 index 000000000000..9a01aa3f3391 --- /dev/null +++ b/platform/ohos/sample/entry/src/main/resources/base/media/icon_foreground.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + diff --git a/platform/ohos/sample/entry/src/main/resources/base/media/layered_image.json b/platform/ohos/sample/entry/src/main/resources/base/media/layered_image.json new file mode 100644 index 000000000000..5b24f75f07fd --- /dev/null +++ b/platform/ohos/sample/entry/src/main/resources/base/media/layered_image.json @@ -0,0 +1,6 @@ +{ + "layered-image": { + "background": "$media:icon_background", + "foreground": "$media:icon_foreground" + } +} diff --git a/platform/ohos/sample/entry/src/main/resources/base/profile/main_pages.json b/platform/ohos/sample/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 000000000000..1898d94f58d6 --- /dev/null +++ b/platform/ohos/sample/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/platform/ohos/sample/entry/types/libmaplibre_native_ohos/index.d.ts b/platform/ohos/sample/entry/types/libmaplibre_native_ohos/index.d.ts new file mode 100644 index 000000000000..e74f5898968d --- /dev/null +++ b/platform/ohos/sample/entry/types/libmaplibre_native_ohos/index.d.ts @@ -0,0 +1,108 @@ +import type { NodeContent } from '@kit.ArkUI'; + +export interface ResourceOptions { + apiKey?: string; + cachePath?: string; + assetPath?: string; +} + +export interface LatLngBounds { + west: number; + south: number; + east: number; + north: number; +} + +export interface BoundOptions { + bounds?: LatLngBounds; + minZoom?: number; + maxZoom?: number; + minPitch?: number; + maxPitch?: number; +} + +export interface Coordinate { + longitude: number; + latitude: number; +} + +export interface CameraPadding { + top?: number; + left?: number; + bottom?: number; + right?: number; +} + +export interface ScreenPoint { + x: number; + y: number; +} + +export interface CameraOptions { + longitude?: number; + latitude?: number; + center?: Coordinate; + centerAltitude?: number; + padding?: CameraPadding; + anchor?: ScreenPoint; + zoom?: number; + bearing?: number; + pitch?: number; + roll?: number; + fov?: number; +} + +export interface FrameRateRange { + min: number; + max: number; + expected: number; +} + +export interface SurfaceState { + width: number; + height: number; + hasWindow: boolean; + hasSurface: boolean; + hasMap: boolean; + needsRender: boolean; + styleLoaded: boolean; + mapLoaded: boolean; + fullyLoaded: boolean; + renderedFrameRate: number; + frameCallbackRate: number; + backend?: string; + surfaceVisible: boolean; + lastSurfaceError?: string; + lastMapLoadError?: string; + lastRenderError?: string; + lastStyleImageMissing?: string; + lastGlyphsError?: string; + lastSpritesError?: string; +} + +export interface XComponentContext { + destroy: () => void; + getCameraOptions: () => CameraOptions; + getPixelRatio: () => number; + getStyleAttributions: () => string[]; + getSurfaceState: () => SurfaceState; + jumpTo: (options: CameraOptions) => void; + reduceMemoryUse: () => void; + renderFrame: () => void; + setBounds: (options: BoundOptions) => void; + setClientOptions: (name: string, version?: string) => void; + setFrameRateRange: (range: FrameRateRange) => void; + setPixelRatio: (pixelRatio: number) => void; + setRenderingEnabled: (enabled: boolean) => void; + setResourceOptions: (options: ResourceOptions) => void; + setStyleUrl: (url: string) => void; + setTileCacheEnabled: (enabled: boolean) => void; +} + +export function createMap(nodeContent: NodeContent): XComponentContext; + +declare const maplibreNative: { + createMap: typeof createMap; +}; + +export default maplibreNative; diff --git a/platform/ohos/sample/entry/types/libmaplibre_native_ohos/oh-package.json5 b/platform/ohos/sample/entry/types/libmaplibre_native_ohos/oh-package.json5 new file mode 100644 index 000000000000..480eec031aad --- /dev/null +++ b/platform/ohos/sample/entry/types/libmaplibre_native_ohos/oh-package.json5 @@ -0,0 +1,8 @@ +{ + "name": "libmaplibre_native_ohos.so", + "types": "./index.d.ts", + "version": "0.1.0", + "description": "MapLibre Native HarmonyOS NAPI module.", + "license": "BSD-2-Clause", + "author": "MapLibre contributors" +} diff --git a/platform/ohos/sample/hvigor/hvigor-config.json5 b/platform/ohos/sample/hvigor/hvigor-config.json5 new file mode 100644 index 000000000000..eab9ccb6d201 --- /dev/null +++ b/platform/ohos/sample/hvigor/hvigor-config.json5 @@ -0,0 +1,4 @@ +{ + "modelVersion": "6.0.0", + "dependencies": {} +} diff --git a/platform/ohos/sample/hvigorfile.ts b/platform/ohos/sample/hvigorfile.ts new file mode 100644 index 000000000000..53f77ae8ad54 --- /dev/null +++ b/platform/ohos/sample/hvigorfile.ts @@ -0,0 +1,22 @@ +import { existsSync, readFileSync } from 'fs'; +import { resolve } from 'path'; +import { appTasks } from '@ohos/hvigor-ohos-plugin'; + +const localSigningConfigPath = resolve(__dirname, 'sign/signing.local.json'); +const localSigningConfig = existsSync(localSigningConfigPath) + ? JSON.parse(readFileSync(localSigningConfigPath, 'utf8')) + : undefined; + +export default { + system: appTasks, + plugins: [], + config: localSigningConfig + ? { + ohos: { + overrides: { + signingConfig: localSigningConfig, + }, + }, + } + : {}, +}; diff --git a/platform/ohos/sample/oh-package.json5 b/platform/ohos/sample/oh-package.json5 new file mode 100644 index 000000000000..13a4d67c2e33 --- /dev/null +++ b/platform/ohos/sample/oh-package.json5 @@ -0,0 +1,11 @@ +{ + "modelVersion": "6.0.0", + "name": "maplibre-native-ohos-sample", + "version": "1.0.0", + "description": "Minimal MapLibre Native HarmonyOS sample.", + "license": "BSD-2-Clause", + "author": "MapLibre contributors", + "main": "", + "dependencies": {}, + "devDependencies": {} +} diff --git a/platform/ohos/sample/sign/generate-signing-config.mjs b/platform/ohos/sample/sign/generate-signing-config.mjs new file mode 100644 index 000000000000..bfacb27b752e --- /dev/null +++ b/platform/ohos/sample/sign/generate-signing-config.mjs @@ -0,0 +1,198 @@ +#!/usr/bin/env node + +// Generates the local Hvigor signing config consumed by ../hvigorfile.ts. +// Hvigor has some weird scheme where it encrypts the signing password and +// then requires keys in a sibling directory material/* to decrypt it. + +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import readline from "node:readline"; +import { fileURLToPath } from "node:url"; + +const signDir = path.dirname(fileURLToPath(import.meta.url)); +const sampleDir = path.resolve(signDir, ".."); +const materialDir = path.join(signDir, "material"); +const signingConfigPath = path.join(signDir, "signing.local.json"); + +// Mirrors Hvigor's DecipherUtil.component constant, used when deriving the +// local signing-material root key. +const component = Buffer.from([ + 49, 243, 9, 115, 214, 175, 91, 184, 211, 190, 177, 88, 101, 131, 192, 119, +]); + +const defaults = { + certpath: "./sign/maplibre-debug.cer", + keyAlias: "maplibre_debug", + profile: "./sign/maplibre-debug.p7b", + signAlg: "SHA256withECDSA", + storeFile: "./sign/maplibre-debug.p12", + type: "HarmonyOS", +}; + +function parseArgs(argv) { + const args = { ...defaults }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + const value = argv[index + 1]; + switch (arg) { + case "--certpath": + case "--keyAlias": + case "--profile": + case "--signAlg": + case "--storeFile": + case "--type": + if (!value) { + throw new Error(`${arg} requires a value`); + } + args[arg.slice(2)] = value; + index += 1; + break; + default: + throw new Error(`Unknown argument: ${arg}`); + } + } + return args; +} + +function resolveFromSample(filePath) { + return path.isAbsolute(filePath) + ? filePath + : path.resolve(sampleDir, filePath); +} + +async function promptHidden(prompt) { + const input = process.stdin; + const output = process.stdout; + const rl = readline.createInterface({ input, output }); + const originalWrite = output.write; + let muted = false; + + output.write = function writeMuted(chunk, encoding, callback) { + if (muted && chunk !== "\n") { + return true; + } + return originalWrite.call(output, chunk, encoding, callback); + }; + + try { + return await new Promise((resolve) => { + rl.question(prompt, (answer) => { + output.write("\n"); + resolve(answer); + }); + muted = true; + }); + } finally { + output.write = originalWrite; + rl.close(); + } +} + +function xor(left, right) { + const result = Buffer.alloc(left.length); + for (let index = 0; index < left.length; index += 1) { + result[index] = left[index] ^ right[index]; + } + return result; +} + +function encryptPacket(key, plaintext) { + const iv = crypto.randomBytes(12); + const cipher = crypto.createCipheriv("aes-128-gcm", key, iv); + const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]); + const authTag = cipher.getAuthTag(); + const header = Buffer.alloc(4); + header.writeUInt32BE(encrypted.length + authTag.length, 0); + return Buffer.concat([header, iv, encrypted, authTag]); +} + +function writeMaterialFile(directory, data) { + const filename = crypto.randomBytes(16).toString("hex"); + fs.writeFileSync(path.join(directory, filename), data, { mode: 0o600 }); +} + +function generateMaterial() { + fs.rmSync(materialDir, { force: true, recursive: true }); + for (const directory of ["fd/0", "fd/1", "fd/2", "ac", "ce"]) { + fs.mkdirSync(path.join(materialDir, directory), { recursive: true }); + } + + const fd0 = crypto.randomBytes(16); + const fd1 = crypto.randomBytes(16); + const fd2 = crypto.randomBytes(16); + const salt = crypto.randomBytes(16); + const workKey = crypto.randomBytes(16); + + let rootComponent = xor(fd0, fd1); + rootComponent = xor(rootComponent, fd2); + rootComponent = xor(rootComponent, component); + const rootKey = crypto.pbkdf2Sync( + rootComponent.toString(), + salt, + 10000, + 16, + "sha256", + ); + + writeMaterialFile(path.join(materialDir, "fd/0"), fd0); + writeMaterialFile(path.join(materialDir, "fd/1"), fd1); + writeMaterialFile(path.join(materialDir, "fd/2"), fd2); + writeMaterialFile(path.join(materialDir, "ac"), salt); + writeMaterialFile( + path.join(materialDir, "ce"), + encryptPacket(rootKey, workKey), + ); + + return workKey; +} + +function encryptPassword(workKey, password) { + return encryptPacket(workKey, Buffer.from(password, "utf8")) + .toString("hex") + .toUpperCase(); +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + for (const filePath of [args.storeFile, args.certpath, args.profile]) { + if (!fs.existsSync(resolveFromSample(filePath))) { + throw new Error(`Missing signing file: ${filePath}`); + } + } + + const storePassword = + process.env.HARMONY_STORE_PASSWORD || + (await promptHidden("Keystore password: ")); + const keyPassword = + process.env.HARMONY_KEY_PASSWORD || (await promptHidden("Key password: ")); + if (!storePassword || !keyPassword) { + throw new Error("Both passwords are required"); + } + + const workKey = generateMaterial(); + const signingConfig = { + type: args.type, + material: { + storeFile: args.storeFile, + storePassword: encryptPassword(workKey, storePassword), + keyAlias: args.keyAlias, + keyPassword: encryptPassword(workKey, keyPassword), + signAlg: args.signAlg, + profile: args.profile, + certpath: args.certpath, + }, + }; + + fs.writeFileSync( + signingConfigPath, + `${JSON.stringify(signingConfig, null, 2)}\n`, + { mode: 0o600 }, + ); + console.log(`Wrote ${path.relative(sampleDir, signingConfigPath)}`); +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +}); diff --git a/platform/ohos/src/http_file_source.cpp b/platform/ohos/src/http_file_source.cpp new file mode 100644 index 000000000000..afdcc5201fe3 --- /dev/null +++ b/platform/ohos/src/http_file_source.cpp @@ -0,0 +1,839 @@ +#include "http_user_agent.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mbgl { +namespace { + +constexpr std::size_t kMaxActiveRequests = 128; +constexpr std::size_t kCallbackGenerations = 64; +constexpr std::size_t kCallbackTokens = kMaxActiveRequests * kCallbackGenerations; + +class RequestState; + +void destroyResponse(Http_Response* response) { + if (response && response->destroyResponse) { + // The current OpenHarmony wrapper exposes cookies as a pointer into an + // internal string, but its destroy callback frees the pointer. + response->cookies = nullptr; + response->destroyResponse(&response); + } +} + +std::string toLowerASCII(const char* value) { + std::string result = value ? value : ""; + std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return result; +} + +std::optional headerValue(Http_Headers* headers, const char* name) { + if (!headers) { + return std::nullopt; + } + + const auto expected = toLowerASCII(name); + Http_HeaderEntry* entries = OH_Http_GetHeaderEntries(headers); + if (!entries) { + return std::nullopt; + } + + Http_HeaderEntry* entriesToDestroy = entries; + for (auto* entry = entries; entry; entry = entry->next) { + if (toLowerASCII(entry->key) != expected || !entry->value) { + continue; + } + + std::string value; + for (auto* item = entry->value; item; item = item->next) { + if (!item->value) { + continue; + } + if (!value.empty()) { + value += ", "; + } + value += item->value; + } + + OH_Http_DestroyHeaderEntries(&entriesToDestroy); + return value; + } + + OH_Http_DestroyHeaderEntries(&entriesToDestroy); + return std::nullopt; +} + +Response::Error::Reason errorReason(uint32_t errCode) { + switch (errCode) { + case OH_HTTP_RESOLVE_PROXY_FAILED: + case OH_HTTP_RESOLVE_HOST_FAILED: + case OH_HTTP_CONNECT_SERVER_FAILED: + case OH_HTTP_OPERATION_TIMEOUT: + case OH_HTTP_RECEIVE_DATA_FAILED: + case OH_HTTP_INVALID_SSL_PEER_CERT: + case OH_HTTP_SSL_CA_NOT_EXIST: + return Response::Error::Reason::Connection; + default: + return Response::Error::Reason::Other; + } +} + +const char* errorCodeName(uint32_t errCode) { + switch (errCode) { + case OH_HTTP_PARAMETER_ERROR: + return "OH_HTTP_PARAMETER_ERROR"; + case OH_HTTP_PERMISSION_DENIED: + return "OH_HTTP_PERMISSION_DENIED"; + case OH_HTTP_UNSUPPORTED_PROTOCOL: + return "OH_HTTP_UNSUPPORTED_PROTOCOL"; + case OH_HTTP_INVALID_URL: + return "OH_HTTP_INVALID_URL"; + case OH_HTTP_RESOLVE_PROXY_FAILED: + return "OH_HTTP_RESOLVE_PROXY_FAILED"; + case OH_HTTP_RESOLVE_HOST_FAILED: + return "OH_HTTP_RESOLVE_HOST_FAILED"; + case OH_HTTP_CONNECT_SERVER_FAILED: + return "OH_HTTP_CONNECT_SERVER_FAILED"; + case OH_HTTP_INVALID_SERVER_RESPONSE: + return "OH_HTTP_INVALID_SERVER_RESPONSE"; + case OH_HTTP_ACCESS_REMOTE_DENIED: + return "OH_HTTP_ACCESS_REMOTE_DENIED"; + case OH_HTTP_HTTP2_FRAMING_ERROR: + return "OH_HTTP_HTTP2_FRAMING_ERROR"; + case OH_HTTP_TRANSFER_PARTIAL_FILE: + return "OH_HTTP_TRANSFER_PARTIAL_FILE"; + case OH_HTTP_WRITE_DATA_FAILED: + return "OH_HTTP_WRITE_DATA_FAILED"; + case OH_HTTP_UPLOAD_FAILED: + return "OH_HTTP_UPLOAD_FAILED"; + case OH_HTTP_OPEN_LOCAL_DATA_FAILED: + return "OH_HTTP_OPEN_LOCAL_DATA_FAILED"; + case OH_HTTP_OUT_OF_MEMORY: + return "OH_HTTP_OUT_OF_MEMORY"; + case OH_HTTP_OPERATION_TIMEOUT: + return "OH_HTTP_OPERATION_TIMEOUT"; + case OH_HTTP_TOO_MANY_REDIRECTIONS: + return "OH_HTTP_TOO_MANY_REDIRECTIONS"; + case OH_HTTP_SERVER_RETURNED_NOTHING: + return "OH_HTTP_SERVER_RETURNED_NOTHING"; + case OH_HTTP_SEND_DATA_FAILED: + return "OH_HTTP_SEND_DATA_FAILED"; + case OH_HTTP_RECEIVE_DATA_FAILED: + return "OH_HTTP_RECEIVE_DATA_FAILED"; + case OH_HTTP_SSL_CERTIFICATE_ERROR: + return "OH_HTTP_SSL_CERTIFICATE_ERROR"; + case OH_HTTP_SSL_CIPHER_USED_ERROR: + return "OH_HTTP_SSL_CIPHER_USED_ERROR"; + case OH_HTTP_INVALID_SSL_PEER_CERT: + return "OH_HTTP_INVALID_SSL_PEER_CERT"; + case OH_HTTP_INVALID_ENCODING_FORMAT: + return "OH_HTTP_INVALID_ENCODING_FORMAT"; + case OH_HTTP_FILE_TOO_LARGE: + return "OH_HTTP_FILE_TOO_LARGE"; + case OH_HTTP_REMOTE_DISK_FULL: + return "OH_HTTP_REMOTE_DISK_FULL"; + case OH_HTTP_REMOTE_FILE_EXISTS: + return "OH_HTTP_REMOTE_FILE_EXISTS"; + case OH_HTTP_SSL_CA_NOT_EXIST: + return "OH_HTTP_SSL_CA_NOT_EXIST"; + case OH_HTTP_REMOTE_FILE_NOT_FOUND: + return "OH_HTTP_REMOTE_FILE_NOT_FOUND"; + case OH_HTTP_AUTHENTICATION_ERROR: + return "OH_HTTP_AUTHENTICATION_ERROR"; + case OH_HTTP_ACCESS_DOMAIN_NOT_ALLOWED: + return "OH_HTTP_ACCESS_DOMAIN_NOT_ALLOWED"; + case OH_HTTP_UNKNOWN_ERROR: + return "OH_HTTP_UNKNOWN_ERROR"; + default: + return nullptr; + } +} + +std::string errorMessage(const Resource& resource, uint32_t errCode) { + std::string message{"OpenHarmony HTTP request failed with "}; + if (const char* name = errorCodeName(errCode)) { + message += name; + message += " ("; + message += util::toString(errCode); + message += ")"; + } else { + message += "error "; + message += util::toString(errCode); + } + if (!resource.url.empty()) { + message += " for "; + message += resource.url; + } + return message; +} + +Response makeNetworkError(const Resource& resource, uint32_t errCode) { + Response response; + response.error = std::make_unique(errorReason(errCode), errorMessage(resource, errCode)); + return response; +} + +void applyCacheHeaders(Response& response, Http_Headers* headers) { + response.etag = headerValue(headers, "etag"); + if (const auto modified = headerValue(headers, "last-modified")) { + response.modified = util::parseTimestamp(modified->c_str()); + } + if (const auto expires = headerValue(headers, "expires")) { + response.expires = util::parseTimestamp(expires->c_str()); + } + if (const auto cacheControl = headerValue(headers, "cache-control")) { + const auto cc = http::CacheControl::parse(*cacheControl); + if (cc.maxAge) { + response.expires = cc.toTimePoint(); + } + response.mustRevalidate = cc.mustRevalidate; + } +} + +Response makeResponse(const Resource& resource, Http_Response* httpResponse, uint32_t errCode) { + if (errCode != OH_HTTP_RESULT_OK) { + return makeNetworkError(resource, errCode); + } + + Response response; + if (!httpResponse) { + response.error = std::make_unique(Response::Error::Reason::Other, + "OpenHarmony HTTP returned no response"); + return response; + } + + applyCacheHeaders(response, httpResponse->headers); + + const int code = static_cast(httpResponse->responseCode); + if (code == 200 || code == 206) { + if (httpResponse->body.buffer && httpResponse->body.length > 0) { + response.data = std::make_shared(httpResponse->body.buffer, httpResponse->body.length); + } else { + response.data = std::make_shared(); + } + } else if (code == 204 || (code == 404 && resource.kind == Resource::Kind::Tile)) { + response.noContent = true; + } else if (code == 304) { + response.notModified = true; + } else if (code == 404) { + response.error = std::make_unique(Response::Error::Reason::NotFound, "HTTP status code 404"); + } else if (code == 429) { + response.error = std::make_unique( + Response::Error::Reason::RateLimit, + "HTTP status code 429", + http::parseRetryHeaders(headerValue(httpResponse->headers, "retry-after"), + headerValue(httpResponse->headers, "x-rate-limit-reset"))); + } else if (code >= 500 && code < 600) { + response.error = std::make_unique(Response::Error::Reason::Server, + std::string{"HTTP status code "} + util::toString(code)); + } else { + response.error = std::make_unique(Response::Error::Reason::Other, + std::string{"HTTP status code "} + util::toString(code)); + } + + return response; +} + +class SlotPool { +public: + static void enqueueOrStart(const std::shared_ptr& state); + static void release(std::size_t token, const RequestState* state); + static void handleResponse(std::size_t token, Http_Response* response, uint32_t errCode); + static void handleCanceled(std::size_t token); + static Http_ResponseCallback responseCallback(std::size_t token); + static Http_OnVoidCallback canceledCallback(std::size_t token); + +private: + struct Slot { + std::weak_ptr state; + uint64_t generation = 0; + }; + + static std::shared_ptr stateForToken(std::size_t token); + static std::optional assignSlotLocked(const std::shared_ptr& state); + static void startNextPending(); + + static std::mutex mutex; + static std::array slots; + static std::deque> pending; + static std::size_t nextSlot; +}; + +template +void responseThunk(Http_Response* response, uint32_t errCode) { + SlotPool::handleResponse(Token, response, errCode); +} + +template +void canceledThunk() { + SlotPool::handleCanceled(Token); +} + +template +constexpr auto makeResponseCallbacks(std::index_sequence) { + return std::array{&responseThunk...}; +} + +template +constexpr auto makeCanceledCallbacks(std::index_sequence) { + return std::array{&canceledThunk...}; +} + +constexpr auto responseCallbacks = makeResponseCallbacks(std::make_index_sequence{}); +constexpr auto canceledCallbacks = makeCanceledCallbacks(std::make_index_sequence{}); + +class RequestState : public std::enable_shared_from_this { +public: + RequestState(util::RunLoop& runLoop_, Resource resource_, std::string userAgent_, FileSource::Callback callback_) + : runLoop(runLoop_), + resource(std::move(resource_)), + userAgent(std::move(userAgent_)), + callback(std::move(callback_)) {} + + void begin() { + { + std::scoped_lock lock(mutex); + self = shared_from_this(); + } + SlotPool::enqueueOrStart(shared_from_this()); + } + + void setSlot(std::size_t token_) { + std::scoped_lock lock(mutex); + token = token_; + } + + bool isCanceled() const { + std::scoped_lock lock(mutex); + return canceled; + } + + void startNativeRequest() { + const auto activeToken = getToken(); + if (!activeToken) { + releaseSelf(); + return; + } + + { + std::scoped_lock lock(mutex); + if (canceled) { + finished = true; + } else { + starting = true; + } + } + if (isFinished()) { + SlotPool::release(*activeToken, this); + releaseSelf(); + return; + } + + Http_Request* newRequest = OH_Http_CreateRequest(resource.url.c_str()); + if (!newRequest) { + complete(nullptr, OH_HTTP_OUT_OF_MEMORY); + return; + } + + Http_Headers* newHeaders = OH_Http_CreateHeaders(); + if (!newHeaders) { + OH_Http_Destroy(&newRequest); + complete(nullptr, OH_HTTP_OUT_OF_MEMORY); + return; + } + + if (!setHeader(newHeaders, "Accept-Encoding", "gzip, deflate") || + !setHeader(newHeaders, "User-Agent", userAgent.c_str())) { + OH_Http_DestroyHeaders(&newHeaders); + OH_Http_Destroy(&newRequest); + complete(nullptr, OH_HTTP_OUT_OF_MEMORY); + return; + } + + if (resource.dataRange) { + const auto range = std::string{"bytes="} + util::toString(resource.dataRange->first) + "-" + + util::toString(resource.dataRange->second); + if (!setHeader(newHeaders, "Range", range.c_str())) { + OH_Http_DestroyHeaders(&newHeaders); + OH_Http_Destroy(&newRequest); + complete(nullptr, OH_HTTP_OUT_OF_MEMORY); + return; + } + } + + if (resource.priorEtag) { + if (!setHeader(newHeaders, "If-None-Match", resource.priorEtag->c_str())) { + OH_Http_DestroyHeaders(&newHeaders); + OH_Http_Destroy(&newRequest); + complete(nullptr, OH_HTTP_OUT_OF_MEMORY); + return; + } + } else if (resource.priorModified) { + const auto modified = util::rfc1123(*resource.priorModified); + if (!setHeader(newHeaders, "If-Modified-Since", modified.c_str())) { + OH_Http_DestroyHeaders(&newHeaders); + OH_Http_Destroy(&newRequest); + complete(nullptr, OH_HTTP_OUT_OF_MEMORY); + return; + } + } + + Http_EventsHandler handler{}; + handler.onCanceled = SlotPool::canceledCallback(*activeToken); + + { + std::scoped_lock lock(mutex); + if (canceled) { + OH_Http_DestroyHeaders(&newHeaders); + OH_Http_Destroy(&newRequest); + finished = true; + } else { + proxy.proxyType = HTTP_PROXY_SYSTEM; + options.method = NET_HTTP_METHOD_GET; + options.headers = newHeaders; + options.readTimeout = 60000; + options.connectTimeout = 60000; + options.httpProtocol = OH_HTTP_NONE; + options.httpProxy = &proxy; + request = newRequest; + headers = newHeaders; + request->options = &options; + starting = true; + } + } + + if (isFinished()) { + SlotPool::release(*activeToken, this); + releaseSelf(); + return; + } + + const int result = OH_Http_Request(newRequest, SlotPool::responseCallback(*activeToken), handler); + { + std::scoped_lock lock(mutex); + starting = false; + } + if (isCanceled()) { + if (result == OH_HTTP_RESULT_OK) { + cancelStartedRequest(); + } else { + finish(std::nullopt); + } + return; + } + if (result != OH_HTTP_RESULT_OK) { + complete(nullptr, static_cast(result)); + } + } + + void cancel() { + Http_Request* requestToDestroy = nullptr; + Http_Headers* headersToDestroy = nullptr; + std::optional activeToken; + bool shouldReleasePending = false; + bool shouldWaitForNativeCallback = false; + { + std::scoped_lock lock(mutex); + canceled = true; + callback = nullptr; + if (finished) { + return; + } + if (awaitingNativeCallback) { + return; + } + if (starting) { + return; + } + activeToken = token; + requestToDestroy = request; + request = nullptr; + headersToDestroy = headers; + headers = nullptr; + options.headers = nullptr; + if (!activeToken) { + finished = true; + shouldReleasePending = true; + } else if (requestToDestroy) { + // The callback token cannot be reused until net_http reports + // the cancellation; callbacks do not carry request context. + awaitingNativeCallback = true; + shouldWaitForNativeCallback = true; + } else { + finished = true; + } + } + + if (requestToDestroy) { + OH_Http_Destroy(&requestToDestroy); + } + if (headersToDestroy) { + OH_Http_DestroyHeaders(&headersToDestroy); + } + if (activeToken && !shouldWaitForNativeCallback) { + SlotPool::release(*activeToken, this); + } + if (shouldReleasePending) { + releaseSelf(); + } else if (activeToken && !shouldWaitForNativeCallback) { + releaseSelf(); + } + } + + void complete(Http_Response* httpResponse, uint32_t errCode) { + auto response = makeResponse(resource, httpResponse, errCode); + destroyResponse(httpResponse); + finish(std::move(response)); + } + + void completeCanceled() { finish(std::nullopt); } + +private: + static bool setHeader(Http_Headers* headers, const char* name, const char* value) { + return OH_Http_SetHeaderValue(headers, name, value) == OH_HTTP_RESULT_OK; + } + + std::optional getToken() const { + std::scoped_lock lock(mutex); + return token; + } + + bool isFinished() const { + std::scoped_lock lock(mutex); + return finished; + } + + void cancelStartedRequest() { + Http_Request* requestToDestroy = nullptr; + Http_Headers* headersToDestroy = nullptr; + { + std::scoped_lock lock(mutex); + if (finished || awaitingNativeCallback) { + return; + } + requestToDestroy = request; + request = nullptr; + headersToDestroy = headers; + headers = nullptr; + options.headers = nullptr; + if (requestToDestroy) { + awaitingNativeCallback = true; + } else { + finished = true; + } + } + + if (requestToDestroy) { + OH_Http_Destroy(&requestToDestroy); + } + if (headersToDestroy) { + OH_Http_DestroyHeaders(&headersToDestroy); + } + if (!requestToDestroy) { + const auto activeToken = getToken(); + if (activeToken) { + SlotPool::release(*activeToken, this); + } + releaseSelf(); + } + } + + void finish(std::optional response) { + auto keepAlive = shared_from_this(); + + const auto activeToken = getToken(); + Http_Request* requestToDestroy = nullptr; + Http_Headers* headersToDestroy = nullptr; + bool shouldCallback = false; + { + std::scoped_lock lock(mutex); + if (finished) { + return; + } + finished = true; + starting = false; + requestToDestroy = request; + request = nullptr; + headersToDestroy = headers; + headers = nullptr; + options.headers = nullptr; + awaitingNativeCallback = false; + shouldCallback = response && !canceled && static_cast(callback); + } + + if (requestToDestroy) { + OH_Http_Destroy(&requestToDestroy); + } + if (headersToDestroy) { + OH_Http_DestroyHeaders(&headersToDestroy); + } + if (activeToken) { + SlotPool::release(*activeToken, this); + } + + if (shouldCallback) { + runLoop.invoke([state = shared_from_this(), response = std::move(*response)]() mutable { + FileSource::Callback localCallback; + { + std::scoped_lock lock(state->mutex); + if (!state->canceled) { + localCallback = std::move(state->callback); + } + state->callback = nullptr; + } + + state->releaseSelf(); + if (localCallback) { + localCallback(response); + } + }); + } else { + releaseSelf(); + } + } + + void releaseSelf() { + std::scoped_lock lock(mutex); + self.reset(); + } + + util::RunLoop& runLoop; + Resource resource; + std::string userAgent; + FileSource::Callback callback; + + mutable std::mutex mutex; + std::shared_ptr self; + std::optional token; + Http_Request* request = nullptr; + Http_Headers* headers = nullptr; + Http_RequestOptions options{}; + Http_Proxy proxy{}; + bool canceled = false; + bool finished = false; + bool starting = false; + bool awaitingNativeCallback = false; +}; + +std::mutex SlotPool::mutex; +std::array SlotPool::slots; +std::deque> SlotPool::pending; +std::size_t SlotPool::nextSlot = 0; + +std::optional SlotPool::assignSlotLocked(const std::shared_ptr& state) { + for (std::size_t offset = 0; offset < slots.size(); ++offset) { + const auto index = (nextSlot + offset) % slots.size(); + if (!slots[index].state.expired()) { + continue; + } + + const auto token = index * kCallbackGenerations + (slots[index].generation % kCallbackGenerations); + slots[index].state = state; + nextSlot = (index + 1) % slots.size(); + state->setSlot(token); + return token; + } + return std::nullopt; +} + +void SlotPool::enqueueOrStart(const std::shared_ptr& state) { + std::optional token; + { + std::scoped_lock lock(mutex); + token = assignSlotLocked(state); + if (!token) { + pending.emplace_back(state); + return; + } + } + + state->startNativeRequest(); +} + +void SlotPool::release(std::size_t token, const RequestState* state) { + const auto index = token / kCallbackGenerations; + if (index >= slots.size()) { + return; + } + + { + std::scoped_lock lock(mutex); + auto current = slots[index].state.lock(); + if (current.get() != state) { + return; + } + + slots[index].state.reset(); + ++slots[index].generation; + } + + startNextPending(); +} + +void SlotPool::startNextPending() { + std::shared_ptr next; + { + std::scoped_lock lock(mutex); + while (!pending.empty()) { + next = pending.front().lock(); + pending.pop_front(); + if (!next || next->isCanceled()) { + next.reset(); + continue; + } + if (assignSlotLocked(next)) { + break; + } + pending.emplace_front(next); + next.reset(); + break; + } + } + + if (next) { + next->startNativeRequest(); + } +} + +std::shared_ptr SlotPool::stateForToken(std::size_t token) { + const auto index = token / kCallbackGenerations; + const auto generation = token % kCallbackGenerations; + if (index >= slots.size()) { + return nullptr; + } + + std::scoped_lock lock(mutex); + if ((slots[index].generation % kCallbackGenerations) != generation) { + return nullptr; + } + return slots[index].state.lock(); +} + +void SlotPool::handleResponse(std::size_t token, Http_Response* response, uint32_t errCode) { + auto state = stateForToken(token); + if (!state) { + destroyResponse(response); + return; + } + state->complete(response, errCode); +} + +void SlotPool::handleCanceled(std::size_t token) { + auto state = stateForToken(token); + if (!state) { + return; + } + state->completeCanceled(); +} + +Http_ResponseCallback SlotPool::responseCallback(std::size_t token) { + return token < responseCallbacks.size() ? responseCallbacks[token] : nullptr; +} + +Http_OnVoidCallback SlotPool::canceledCallback(std::size_t token) { + return token < canceledCallbacks.size() ? canceledCallbacks[token] : nullptr; +} + +class HTTPRequest final : public AsyncRequest { +public: + HTTPRequest(Resource resource, std::string userAgent, FileSource::Callback callback) + : state(std::make_shared( + *util::RunLoop::Get(), std::move(resource), std::move(userAgent), std::move(callback))) { + state->begin(); + } + + ~HTTPRequest() override { state->cancel(); } + +private: + std::shared_ptr state; +}; + +} // namespace + +class HTTPFileSource::Impl { +public: + Impl(const ResourceOptions& resourceOptions_, const ClientOptions& clientOptions_) + : resourceOptions(resourceOptions_.clone()), + clientOptions(clientOptions_.clone()) {} + + void setResourceOptions(ResourceOptions options) { + std::scoped_lock lock(mutex); + resourceOptions = options.clone(); + } + + ResourceOptions getResourceOptions() { + std::scoped_lock lock(mutex); + return resourceOptions.clone(); + } + + void setClientOptions(ClientOptions options) { + std::scoped_lock lock(mutex); + clientOptions = options.clone(); + } + + ClientOptions getClientOptions() { + std::scoped_lock lock(mutex); + return clientOptions.clone(); + } + + std::string getUserAgent() { + std::scoped_lock lock(mutex); + return ohos::buildUserAgent(clientOptions); + } + +private: + std::mutex mutex; + ResourceOptions resourceOptions; + ClientOptions clientOptions; +}; + +HTTPFileSource::HTTPFileSource(const ResourceOptions& resourceOptions, const ClientOptions& clientOptions) + : impl(std::make_unique(resourceOptions, clientOptions)) {} + +HTTPFileSource::~HTTPFileSource() = default; + +std::unique_ptr HTTPFileSource::request(const Resource& resource, Callback callback) { + return std::make_unique(resource, impl->getUserAgent(), std::move(callback)); +} + +void HTTPFileSource::setResourceOptions(ResourceOptions options) { + impl->setResourceOptions(std::move(options)); +} + +ResourceOptions HTTPFileSource::getResourceOptions() { + return impl->getResourceOptions(); +} + +void HTTPFileSource::setClientOptions(ClientOptions options) { + impl->setClientOptions(std::move(options)); +} + +ClientOptions HTTPFileSource::getClientOptions() { + return impl->getClientOptions(); +} + +} // namespace mbgl diff --git a/platform/ohos/src/http_user_agent.hpp b/platform/ohos/src/http_user_agent.hpp new file mode 100644 index 000000000000..55dfef8e3413 --- /dev/null +++ b/platform/ohos/src/http_user_agent.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +#include + +namespace mbgl { +namespace ohos { + +inline std::string buildUserAgent(const ClientOptions& clientOptions) { + std::string userAgent; + if (!clientOptions.name().empty()) { + userAgent += clientOptions.name(); + if (!clientOptions.version().empty()) { + userAgent += "/"; + userAgent += clientOptions.version(); + } + userAgent += " "; + } + + userAgent += "MapLibreNative/0.0.0 ("; + userAgent += version::revision; + userAgent += "; HarmonyOS)"; + return userAgent; +} + +} // namespace ohos +} // namespace mbgl diff --git a/platform/ohos/src/image.cpp b/platform/ohos/src/image.cpp new file mode 100644 index 000000000000..77985924156f --- /dev/null +++ b/platform/ohos/src/image.cpp @@ -0,0 +1,290 @@ +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace mbgl { +namespace { + +const char* imageErrorCodeName(Image_ErrorCode code) { + switch (code) { + case IMAGE_SUCCESS: + return "IMAGE_SUCCESS"; + case IMAGE_BAD_PARAMETER: + return "IMAGE_BAD_PARAMETER"; + case IMAGE_UNSUPPORTED_MIME_TYPE: + return "IMAGE_UNSUPPORTED_MIME_TYPE"; + case IMAGE_UNKNOWN_MIME_TYPE: + return "IMAGE_UNKNOWN_MIME_TYPE"; + case IMAGE_TOO_LARGE: + return "IMAGE_TOO_LARGE"; + case IMAGE_DMA_NOT_EXIST: + return "IMAGE_DMA_NOT_EXIST"; + case IMAGE_DMA_OPERATION_FAILED: + return "IMAGE_DMA_OPERATION_FAILED"; + case IMAGE_UNSUPPORTED_OPERATION: + return "IMAGE_UNSUPPORTED_OPERATION"; + case IMAGE_UNSUPPORTED_METADATA: + return "IMAGE_UNSUPPORTED_METADATA"; + case IMAGE_UNSUPPORTED_CONVERSION: + return "IMAGE_UNSUPPORTED_CONVERSION"; + case IMAGE_INVALID_REGION: + return "IMAGE_INVALID_REGION"; + case IMAGE_UNSUPPORTED_MEMORY_FORMAT: + return "IMAGE_UNSUPPORTED_MEMORY_FORMAT"; + case IMAGE_INVALID_PARAMETER: + return "IMAGE_INVALID_PARAMETER"; + case IMAGE_ALLOC_FAILED: + return "IMAGE_ALLOC_FAILED"; + case IMAGE_COPY_FAILED: + return "IMAGE_COPY_FAILED"; + case IMAGE_LOCK_UNLOCK_FAILED: + return "IMAGE_LOCK_UNLOCK_FAILED"; + case IMAGE_ALLOCATOR_MODE_UNSUPPORTED: + return "IMAGE_ALLOCATOR_MODE_UNSUPPORTED"; + case IMAGE_UNKNOWN_ERROR: + return "IMAGE_UNKNOWN_ERROR"; + case IMAGE_BAD_SOURCE: + return "IMAGE_BAD_SOURCE"; + case IMAGE_SOURCE_UNSUPPORTED_MIME_TYPE: + return "IMAGE_SOURCE_UNSUPPORTED_MIME_TYPE"; + case IMAGE_SOURCE_TOO_LARGE: + return "IMAGE_SOURCE_TOO_LARGE"; + case IMAGE_SOURCE_UNSUPPORTED_ALLOCATOR_TYPE: + return "IMAGE_SOURCE_UNSUPPORTED_ALLOCATOR_TYPE"; + case IMAGE_SOURCE_UNSUPPORTED_OPTIONS: + return "IMAGE_SOURCE_UNSUPPORTED_OPTIONS"; + case IMAGE_SOURCE_INVALID_PARAMETER: + return "IMAGE_SOURCE_INVALID_PARAMETER"; + case IMAGE_DECODE_FAILED: + return "IMAGE_DECODE_FAILED"; + case IMAGE_SOURCE_ALLOC_FAILED: + return "IMAGE_SOURCE_ALLOC_FAILED"; + case IMAGE_PACKER_INVALID_PARAMETER: + return "IMAGE_PACKER_INVALID_PARAMETER"; + case IMAGE_ENCODE_FAILED: + return "IMAGE_ENCODE_FAILED"; + case IMAGE_RECEIVER_INVALID_PARAMETER: + return "IMAGE_RECEIVER_INVALID_PARAMETER"; + default: + return nullptr; + } +} + +std::string imageErrorCodeMessage(Image_ErrorCode code) { + if (const char* name = imageErrorCodeName(code)) { + return std::string{name} + " (" + std::to_string(code) + ")"; + } + return "Image_ErrorCode " + std::to_string(code); +} + +void checkImageResult(Image_ErrorCode code, const char* operation) { + if (code != IMAGE_SUCCESS) { + throw std::runtime_error(std::string(operation) + " failed with " + imageErrorCodeMessage(code)); + } +} + +const char* pixelFormatName(int32_t format) { + switch (format) { + case PIXEL_FORMAT_UNKNOWN: + return "PIXEL_FORMAT_UNKNOWN"; + case PIXEL_FORMAT_RGB_565: + return "PIXEL_FORMAT_RGB_565"; + case PIXEL_FORMAT_RGBA_8888: + return "PIXEL_FORMAT_RGBA_8888"; + case PIXEL_FORMAT_BGRA_8888: + return "PIXEL_FORMAT_BGRA_8888"; + case PIXEL_FORMAT_RGB_888: + return "PIXEL_FORMAT_RGB_888"; + case PIXEL_FORMAT_ALPHA_8: + return "PIXEL_FORMAT_ALPHA_8"; + case PIXEL_FORMAT_RGBA_F16: + return "PIXEL_FORMAT_RGBA_F16"; + case PIXEL_FORMAT_NV21: + return "PIXEL_FORMAT_NV21"; + case PIXEL_FORMAT_NV12: + return "PIXEL_FORMAT_NV12"; + case PIXEL_FORMAT_RGBA_1010102: + return "PIXEL_FORMAT_RGBA_1010102"; + case PIXEL_FORMAT_YCBCR_P010: + return "PIXEL_FORMAT_YCBCR_P010"; + case PIXEL_FORMAT_YCRCB_P010: + return "PIXEL_FORMAT_YCRCB_P010"; + default: + return nullptr; + } +} + +const char* alphaTypeName(int32_t alphaType) { + switch (alphaType) { + case PIXELMAP_ALPHA_TYPE_UNKNOWN: + return "PIXELMAP_ALPHA_TYPE_UNKNOWN"; + case PIXELMAP_ALPHA_TYPE_OPAQUE: + return "PIXELMAP_ALPHA_TYPE_OPAQUE"; + case PIXELMAP_ALPHA_TYPE_PREMULTIPLIED: + return "PIXELMAP_ALPHA_TYPE_PREMULTIPLIED"; + case PIXELMAP_ALPHA_TYPE_UNPREMULTIPLIED: + return "PIXELMAP_ALPHA_TYPE_UNPREMULTIPLIED"; + default: + return nullptr; + } +} + +std::string namedValue(const char* name, int32_t value) { + if (name) { + return std::string{name} + " (" + std::to_string(value) + ")"; + } + return std::to_string(value); +} + +std::string pixelmapDescription( + uint32_t width, uint32_t height, uint32_t rowStride, int32_t format, int32_t alphaType) { + return "width=" + std::to_string(width) + ", height=" + std::to_string(height) + + ", rowStride=" + std::to_string(rowStride) + ", pixelFormat=" + namedValue(pixelFormatName(format), format) + + ", alphaType=" + namedValue(alphaTypeName(alphaType), alphaType); +} + +struct ImageSourceDeleter { + void operator()(OH_ImageSourceNative* source) const { + if (source) { + OH_ImageSourceNative_Release(source); + } + } +}; + +struct DecodingOptionsDeleter { + void operator()(OH_DecodingOptions* options) const { + if (options) { + OH_DecodingOptions_Release(options); + } + } +}; + +struct PixelmapDeleter { + void operator()(OH_PixelmapNative* pixelmap) const { + if (pixelmap) { + OH_PixelmapNative_Release(pixelmap); + } + } +}; + +struct PixelmapInfoDeleter { + void operator()(OH_Pixelmap_ImageInfo* info) const { + if (info) { + OH_PixelmapImageInfo_Release(info); + } + } +}; + +using ImageSourcePtr = std::unique_ptr; +using DecodingOptionsPtr = std::unique_ptr; +using PixelmapPtr = std::unique_ptr; +using PixelmapInfoPtr = std::unique_ptr; + +std::size_t checkedByteCount(std::size_t width, std::size_t height, const char* description) { + if (height != 0 && width > std::numeric_limits::max() / height) { + throw std::runtime_error(std::string("OHOS image decoder returned overflowing ") + description); + } + return width * height; +} + +void copyRows( + uint8_t* dst, const uint8_t* src, uint32_t width, uint32_t height, std::size_t rowStride, int32_t format) { + const auto tightStride = checkedByteCount(static_cast(width), 4, "row stride"); + + for (uint32_t y = 0; y < height; ++y) { + const auto* srcRow = src + y * rowStride; + auto* dstRow = dst + y * tightStride; + + if (format == PIXEL_FORMAT_BGRA_8888) { + for (uint32_t x = 0; x < width; ++x) { + dstRow[4 * x + 0] = srcRow[4 * x + 2]; + dstRow[4 * x + 1] = srcRow[4 * x + 1]; + dstRow[4 * x + 2] = srcRow[4 * x + 0]; + dstRow[4 * x + 3] = srcRow[4 * x + 3]; + } + } else { + std::memcpy(dstRow, srcRow, tightStride); + } + } +} + +} // namespace + +PremultipliedImage decodeImage(const std::string& string) { + auto data = std::vector(string.begin(), string.end()); + + OH_ImageSourceNative* sourceRaw = nullptr; + checkImageResult(OH_ImageSourceNative_CreateFromData(data.data(), data.size(), &sourceRaw), + "OH_ImageSourceNative_CreateFromData"); + ImageSourcePtr source(sourceRaw); + + OH_DecodingOptions* optionsRaw = nullptr; + checkImageResult(OH_DecodingOptions_Create(&optionsRaw), "OH_DecodingOptions_Create"); + DecodingOptionsPtr options(optionsRaw); + checkImageResult(OH_DecodingOptions_SetPixelFormat(options.get(), PIXEL_FORMAT_RGBA_8888), + "OH_DecodingOptions_SetPixelFormat"); + + OH_PixelmapNative* pixelmapRaw = nullptr; + checkImageResult(OH_ImageSourceNative_CreatePixelmap(source.get(), options.get(), &pixelmapRaw), + "OH_ImageSourceNative_CreatePixelmap"); + PixelmapPtr pixelmap(pixelmapRaw); + + OH_Pixelmap_ImageInfo* infoRaw = nullptr; + checkImageResult(OH_PixelmapImageInfo_Create(&infoRaw), "OH_PixelmapImageInfo_Create"); + PixelmapInfoPtr info(infoRaw); + checkImageResult(OH_PixelmapNative_GetImageInfo(pixelmap.get(), info.get()), "OH_PixelmapNative_GetImageInfo"); + + uint32_t width = 0; + uint32_t height = 0; + uint32_t rowStride = 0; + int32_t pixelFormat = PIXEL_FORMAT_UNKNOWN; + int32_t alphaType = PIXELMAP_ALPHA_TYPE_UNKNOWN; + checkImageResult(OH_PixelmapImageInfo_GetWidth(info.get(), &width), "OH_PixelmapImageInfo_GetWidth"); + checkImageResult(OH_PixelmapImageInfo_GetHeight(info.get(), &height), "OH_PixelmapImageInfo_GetHeight"); + checkImageResult(OH_PixelmapImageInfo_GetRowStride(info.get(), &rowStride), "OH_PixelmapImageInfo_GetRowStride"); + checkImageResult(OH_PixelmapImageInfo_GetPixelFormat(info.get(), &pixelFormat), + "OH_PixelmapImageInfo_GetPixelFormat"); + checkImageResult(OH_PixelmapImageInfo_GetAlphaType(info.get(), &alphaType), "OH_PixelmapImageInfo_GetAlphaType"); + + const auto description = pixelmapDescription(width, height, rowStride, pixelFormat, alphaType); + if (width == 0 || height == 0) { + throw std::runtime_error("OHOS image decoder returned empty image dimensions: " + description); + } + if (pixelFormat != PIXEL_FORMAT_RGBA_8888 && pixelFormat != PIXEL_FORMAT_BGRA_8888) { + throw std::runtime_error("OHOS image decoder returned unsupported pixel format: " + description); + } + + const auto sourceRowStride = checkedByteCount(static_cast(width), 4, "row stride"); + const auto bytesRequired = checkedByteCount(sourceRowStride, height, "pixel buffer size"); + std::vector pixels(bytesRequired); + size_t bytesRead = pixels.size(); + checkImageResult(OH_PixelmapNative_ReadPixels(pixelmap.get(), pixels.data(), &bytesRead), + "OH_PixelmapNative_ReadPixels"); + if (bytesRead < bytesRequired) { + throw std::runtime_error("OHOS image decoder returned fewer pixel bytes than expected: read " + + std::to_string(bytesRead) + " of " + std::to_string(bytesRequired) + " bytes, " + + description); + } + + const Size imageSize{width, height}; + if (alphaType == PIXELMAP_ALPHA_TYPE_UNPREMULTIPLIED) { + UnassociatedImage image(imageSize); + copyRows(image.data.get(), pixels.data(), width, height, sourceRowStride, pixelFormat); + return util::premultiply(std::move(image)); + } + + PremultipliedImage image(imageSize); + copyRows(image.data.get(), pixels.data(), width, height, sourceRowStride, pixelFormat); + return image; +} + +} // namespace mbgl diff --git a/platform/ohos/src/logging_hilog.cpp b/platform/ohos/src/logging_hilog.cpp new file mode 100644 index 000000000000..39a59897eecf --- /dev/null +++ b/platform/ohos/src/logging_hilog.cpp @@ -0,0 +1,37 @@ +#include +#include + +#include + +#include + +namespace mbgl { +namespace { + +constexpr unsigned int kMapLibreHilogDomain = 0x4d4c; +constexpr char kMapLibreHilogTag[] = "MapLibreNative"; + +LogLevel logLevelForSeverity(EventSeverity severity) { + switch (severity) { + case EventSeverity::Debug: + return LOG_DEBUG; + case EventSeverity::Info: + return LOG_INFO; + case EventSeverity::Warning: + return LOG_WARN; + case EventSeverity::Error: + return LOG_ERROR; + case EventSeverity::SeverityCount: + break; + } + return LOG_INFO; +} + +} // namespace + +void Log::platformRecord(EventSeverity severity, const std::string& msg) { + const auto message = std::string("[") + Enum::toString(severity) + "] " + msg; + OH_LOG_PrintMsg(LOG_APP, logLevelForSeverity(severity), kMapLibreHilogDomain, kMapLibreHilogTag, message.c_str()); +} + +} // namespace mbgl diff --git a/src/mbgl/gl/attribute.hpp b/src/mbgl/gl/attribute.hpp index 9dbd96343d22..c69102680b7a 100644 --- a/src/mbgl/gl/attribute.hpp +++ b/src/mbgl/gl/attribute.hpp @@ -5,10 +5,11 @@ #include #include -#include -#include +#include #include +#include #include +#include namespace mbgl { namespace gl { @@ -34,9 +35,12 @@ class AttributeLocations> final { void queryLocations(const ProgramID& id) { locations = Locations{ queryLocation(id, concat_literals<&string_literal<'a', '_'>::value, &As::name>::value())...}; + } + + bool hasExpectedFirstLocation() const { using TypeOfFirst = typename std::tuple_element_t<0, std::tuple>; - [[maybe_unused]] auto first = locations.template get(); - assert(first && first.value() == 0); + const auto location = locations.template get(); + return location && *location == 0; } static constexpr const char* getFirstAttribName() { @@ -67,12 +71,24 @@ class AttributeLocations> final { AttributeBindingArray toBindingArray(const gfx::AttributeBindings>& bindings) const { AttributeBindingArray result; - result.resize(sizeof...(As)); + + std::optional maxLocation; + auto updateMaxLocation = [&](const std::optional& location) { + if (location) { + maxLocation = maxLocation ? std::max(*maxLocation, *location) : *location; + } + }; + + util::ignore({(updateMaxLocation(locations.template get()), 0)...}); + + if (maxLocation) { + result.resize(*maxLocation + 1); + } auto maybeAddBinding = [&](const std::optional& location, const std::optional& binding) { if (location) { - result.at(*location) = binding; + result[*location] = binding; } }; diff --git a/src/mbgl/gl/context.cpp b/src/mbgl/gl/context.cpp index ff6a159d4a02..b0b4f5a33c2b 100644 --- a/src/mbgl/gl/context.cpp +++ b/src/mbgl/gl/context.cpp @@ -693,6 +693,12 @@ void Context::setStencilMode(const gfx::StencilMode& stencil) { } } +bool Context::hasStencilBuffer() const { + GLint bits = 0; + MBGL_CHECK_ERROR(glGetIntegerv(GL_STENCIL_BITS, &bits)); + return bits > 0; +} + void Context::setColorMode(const gfx::ColorMode& color) { if (color.blendFunction.is()) { blend = false; diff --git a/src/mbgl/gl/context.hpp b/src/mbgl/gl/context.hpp index b83625fe38a7..0d654c4ba2a9 100644 --- a/src/mbgl/gl/context.hpp +++ b/src/mbgl/gl/context.hpp @@ -79,6 +79,7 @@ class Context final : public gfx::Context { void setColorMode(const gfx::ColorMode&); void setCullFaceMode(const gfx::CullFaceMode&); void setScissorTest(const gfx::ScissorRect&); + bool hasStencilBuffer() const; void draw(const gfx::DrawMode&, std::size_t indexOffset, std::size_t indexLength); diff --git a/src/mbgl/gl/drawable_gl.cpp b/src/mbgl/gl/drawable_gl.cpp index 7fca47b45763..7d1710bf9668 100644 --- a/src/mbgl/gl/drawable_gl.cpp +++ b/src/mbgl/gl/drawable_gl.cpp @@ -243,7 +243,7 @@ gfx::ColorMode DrawableGL::makeColorMode(PaintParameters& parameters) const { } gfx::StencilMode DrawableGL::makeStencilMode(PaintParameters& parameters) const { - if (enableStencil) { + if (enableStencil && parameters.stencilClippingAvailable) { if (!is3D && tileID) { return parameters.stencilModeForClipping(tileID->toUnwrapped()); } diff --git a/src/mbgl/gl/layer_group_gl.cpp b/src/mbgl/gl/layer_group_gl.cpp index 6dab46f05a2b..08ec7b234448 100644 --- a/src/mbgl/gl/layer_group_gl.cpp +++ b/src/mbgl/gl/layer_group_gl.cpp @@ -64,7 +64,9 @@ void TileLayerGroupGL::render(RenderOrchestrator&, PaintParameters& parameters) bool stencil3d = false; gfx::StencilMode stencilMode3d; - if (getDrawableCount()) { + parameters.stencilClippingAvailable = parameters.renderTargetHasStencilBuffer; + + if (getDrawableCount() && parameters.stencilClippingAvailable) { MLN_TRACE_ZONE(clip masks); #if !defined(NDEBUG) const auto label_clip = getName() + (getName().empty() ? "" : "-") + "tile-clip-masks"; @@ -90,10 +92,16 @@ void TileLayerGroupGL::render(RenderOrchestrator&, PaintParameters& parameters) if (features3d) { stencilMode3d = stencil3d ? parameters.stencilModeFor3D() : gfx::StencilMode::disabled(); } else if (stencilTiles && !stencilTiles->empty()) { - parameters.renderTileClippingMasks(stencilTiles); + if (!parameters.renderTileClippingMasks(stencilTiles)) { + parameters.stencilClippingAvailable = false; + } } } + if (!parameters.stencilClippingAvailable) { + context.setStencilMode(gfx::StencilMode::disabled()); + } + #if !defined(NDEBUG) const auto label_render = getName() + (getName().empty() ? "" : "-") + "render"; const auto debugGroupRender = parameters.encoder->createDebugGroup(label_render.c_str()); @@ -167,6 +175,12 @@ void LayerGroupGL::render(RenderOrchestrator&, PaintParameters& parameters) { return; } + auto& context = static_cast(parameters.context); + parameters.stencilClippingAvailable = parameters.renderTargetHasStencilBuffer; + if (!parameters.stencilClippingAvailable) { + context.setStencilMode(gfx::StencilMode::disabled()); + } + bool bindUBOs = false; visitDrawables([&](gfx::Drawable& drawable) { if (!drawable.getEnabled() || !drawable.hasRenderPass(parameters.pass)) { diff --git a/src/mbgl/renderer/paint_parameters.cpp b/src/mbgl/renderer/paint_parameters.cpp index c5a6eb5bbf04..6ca276bf98cd 100644 --- a/src/mbgl/renderer/paint_parameters.cpp +++ b/src/mbgl/renderer/paint_parameters.cpp @@ -12,6 +12,7 @@ #include #if MLN_RENDER_BACKEND_OPENGL +#include #include #endif @@ -95,6 +96,18 @@ PaintParameters::PaintParameters(gfx::Context& context_, PaintParameters::~PaintParameters() = default; +#if MLN_RENDER_BACKEND_OPENGL +void PaintParameters::updateStencilBufferAvailability() { + auto& glContext = static_cast(context); + renderTargetHasStencilBuffer = glContext.hasStencilBuffer(); + stencilClippingAvailable = renderTargetHasStencilBuffer; + + if (!stencilClippingAvailable) { + glContext.setStencilMode(gfx::StencilMode::disabled()); + } +} +#endif + mat4 PaintParameters::matrixForTile(const UnwrappedTileID& tileID, bool aligned) const { mat4 matrix; state.matrixFor(matrix, tileID); @@ -169,10 +182,14 @@ void PaintParameters::clearStencil() { #endif } -void PaintParameters::renderTileClippingMasks(const RenderTiles& renderTiles) { +bool PaintParameters::renderTileClippingMasks(const RenderTiles& renderTiles) { // We can avoid updating the mask if it already contains the same set of tiles. - if (!renderTiles || !renderPass || tileIDsCovered(renderTiles, tileClippingMaskIDs)) { - return; + if (!renderTiles || tileIDsCovered(renderTiles, tileClippingMaskIDs)) { + return true; + } + + if (!renderPass) { + return false; } tileClippingMaskIDs.clear(); @@ -292,11 +309,9 @@ void PaintParameters::renderTileClippingMasks(const RenderTiles& renderTiles) { auto program = staticData.shaders->getLegacyGroup().get(); if (!program) { - return; + return false; } - static_cast(context).renderingStats().stencilUpdates++; - const style::Properties<>::PossiblyEvaluated properties{}; const ClippingMaskProgram::Binders paintAttributeData(properties, 0); @@ -313,32 +328,38 @@ void PaintParameters::renderTileClippingMasks(const RenderTiles& renderTiles) { continue; } - program->draw(context, - *renderPass, - gfx::Triangles(), - gfx::DepthMode::disabled(), - gfx::StencilMode{gfx::StencilMode::Always{}, - stencilID, - 0b11111111, - gfx::StencilOpType::Keep, - gfx::StencilOpType::Keep, - gfx::StencilOpType::Replace}, - gfx::ColorMode::disabled(), - gfx::CullFaceMode::disabled(), - *staticData.quadTriangleIndexBuffer, - staticData.clippingMaskSegments, - ClippingMaskProgram::computeAllUniformValues( - ClippingMaskProgram::LayoutUniformValues{ - uniforms::matrix::Value(matrixForTile(tileID)), - }, - paintAttributeData, - properties, - static_cast(state.getZoom())), - ClippingMaskProgram::computeAllAttributeBindings( - *staticData.tileVertexBuffer, paintAttributeData, properties), - "clipping/" + util::toString(stencilID)); + if (!program->draw(context, + *renderPass, + gfx::Triangles(), + gfx::DepthMode::disabled(), + gfx::StencilMode{gfx::StencilMode::Always{}, + stencilID, + 0b11111111, + gfx::StencilOpType::Keep, + gfx::StencilOpType::Keep, + gfx::StencilOpType::Replace}, + gfx::ColorMode::disabled(), + gfx::CullFaceMode::disabled(), + *staticData.quadTriangleIndexBuffer, + staticData.clippingMaskSegments, + ClippingMaskProgram::computeAllUniformValues( + ClippingMaskProgram::LayoutUniformValues{ + uniforms::matrix::Value(matrixForTile(tileID)), + }, + paintAttributeData, + properties, + static_cast(state.getZoom())), + ClippingMaskProgram::computeAllAttributeBindings( + *staticData.tileVertexBuffer, paintAttributeData, properties), + "clipping/" + util::toString(stencilID))) { + tileClippingMaskIDs.clear(); + return false; + } } + static_cast(context).renderingStats().stencilUpdates++; #endif // MLN_RENDER_BACKEND_OPENGL + + return true; } gfx::StencilMode PaintParameters::stencilModeForClipping(const UnwrappedTileID& tileID) const { diff --git a/src/mbgl/renderer/paint_parameters.hpp b/src/mbgl/renderer/paint_parameters.hpp index abcac59d82cc..a585ba07acf5 100644 --- a/src/mbgl/renderer/paint_parameters.hpp +++ b/src/mbgl/renderer/paint_parameters.hpp @@ -97,7 +97,20 @@ class PaintParameters { // Stencil handling public: - void renderTileClippingMasks(const RenderTiles&); +#if MLN_RENDER_BACKEND_OPENGL + /// Update cached stencil availability for the currently bound GL framebuffer. + void updateStencilBufferAvailability(); + + /// Whether the current GL render target has a stencil buffer. + bool renderTargetHasStencilBuffer = true; + + /// Transient GL draw state for the current layer group. GL layer groups reset this from + /// `renderTargetHasStencilBuffer`, then disable it if clipping-mask setup fails. + bool stencilClippingAvailable = true; +#endif + + /// @return True when clipping mask state is ready or no update was needed. + bool renderTileClippingMasks(const RenderTiles&); /// Clear the stencil buffer, even if there are no tile masks (for 3D) void clearStencil(); diff --git a/src/mbgl/renderer/render_target.cpp b/src/mbgl/renderer/render_target.cpp index 447d57879ea0..81254ba07c72 100644 --- a/src/mbgl/renderer/render_target.cpp +++ b/src/mbgl/renderer/render_target.cpp @@ -70,6 +70,9 @@ void RenderTarget::render(RenderOrchestrator& orchestrator, const RenderTree& re .clearColor = Color{0.0f, 0.0f, 0.0f, 1.0f}, .clearDepth = {}, .clearStencil = {}}); +#if MLN_RENDER_BACKEND_OPENGL + parameters.updateStencilBufferAvailability(); +#endif const gfx::ScissorRect prevScissorRect = parameters.scissorRect; const auto& size = getTexture()->getSize(); diff --git a/src/mbgl/renderer/renderer_impl.cpp b/src/mbgl/renderer/renderer_impl.cpp index 5c1b3ad1fc82..eb67e8a12692 100644 --- a/src/mbgl/renderer/renderer_impl.cpp +++ b/src/mbgl/renderer/renderer_impl.cpp @@ -318,6 +318,9 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr< const auto debugGroup(parameters.encoder->createDebugGroup("common-3d")); parameters.pass = RenderPass::Pass3D; +#if MLN_RENDER_BACKEND_OPENGL + parameters.updateStencilBufferAvailability(); +#endif // TODO is this needed? // if (!parameters.staticData.depthRenderbuffer || @@ -368,6 +371,9 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr< .clearColor = color, .clearDepth = 1.0f, .clearStencil = 0}); +#if MLN_RENDER_BACKEND_OPENGL + parameters.updateStencilBufferAvailability(); +#endif } }; diff --git a/src/mbgl/shaders/gl/legacy/program.hpp b/src/mbgl/shaders/gl/legacy/program.hpp index 4a01420b8699..dedbd81ab976 100644 --- a/src/mbgl/shaders/gl/legacy/program.hpp +++ b/src/mbgl/shaders/gl/legacy/program.hpp @@ -91,7 +91,7 @@ class Program : public gfx::Shader { } template - void draw(gfx::Context& context, + bool draw(gfx::Context& context, gfx::RenderPass& renderPass, const DrawMode& drawMode, const gfx::DepthMode& depthMode, @@ -106,7 +106,7 @@ class Program : public gfx::Shader { static_assert(Primitive == gfx::PrimitiveTypeOf::value, "incompatible draw mode"); if (!programBase) { - return; + return false; } auto drawScopeIt = segment.drawScopes.find(layerID); @@ -114,23 +114,23 @@ class Program : public gfx::Shader { drawScopeIt = segment.drawScopes.emplace(layerID, context.createDrawScope()).first; } - programBase->draw(context, - renderPass, - drawMode, - depthMode, - stencilMode, - colorMode, - cullFaceMode, - uniformValues, - drawScopeIt->second, - allAttributeBindings.offset(segment.vertexOffset), - indexBuffer, - segment.indexOffset, - segment.indexLength); + return programBase->draw(context, + renderPass, + drawMode, + depthMode, + stencilMode, + colorMode, + cullFaceMode, + uniformValues, + drawScopeIt->second, + allAttributeBindings.offset(segment.vertexOffset), + indexBuffer, + segment.indexOffset, + segment.indexLength); } template - void draw(gfx::Context& context, + bool draw(gfx::Context& context, gfx::RenderPass& renderPass, const DrawMode& drawMode, const gfx::DepthMode& depthMode, @@ -145,7 +145,7 @@ class Program : public gfx::Shader { static_assert(Primitive == gfx::PrimitiveTypeOf::value, "incompatible draw mode"); if (!programBase) { - return; + return false; } for (auto& segment : segments) { @@ -155,20 +155,23 @@ class Program : public gfx::Shader { drawScopeIt = segment.drawScopes.emplace(layerID, context.createDrawScope()).first; } - programBase->draw(context, - renderPass, - drawMode, - depthMode, - stencilMode, - colorMode, - cullFaceMode, - uniformValues, - drawScopeIt->second, - allAttributeBindings.offset(segment.vertexOffset), - indexBuffer, - segment.indexOffset, - segment.indexLength); + if (!programBase->draw(context, + renderPass, + drawMode, + depthMode, + stencilMode, + colorMode, + cullFaceMode, + uniformValues, + drawScopeIt->second, + allAttributeBindings.offset(segment.vertexOffset), + indexBuffer, + segment.indexOffset, + segment.indexLength)) { + return false; + } } + return true; } }; diff --git a/src/mbgl/shaders/gl/legacy/program_base.hpp b/src/mbgl/shaders/gl/legacy/program_base.hpp index bda40764f773..a8654b3f36cc 100644 --- a/src/mbgl/shaders/gl/legacy/program_base.hpp +++ b/src/mbgl/shaders/gl/legacy/program_base.hpp @@ -15,6 +15,8 @@ #include #include +#include +#include #include namespace mbgl { @@ -45,6 +47,10 @@ class ProgramBase { context.createShader(ShaderType::Fragment, fragmentSource), attributeLocations.getFirstAttribName())) { attributeLocations.queryLocations(program); + if (!attributeLocations.hasExpectedFirstLocation()) { + throw std::runtime_error("Shader is missing active attribute at location 0: " + + std::string(attributeLocations.getFirstAttribName())); + } uniformStates.queryLocations(program); } @@ -80,7 +86,7 @@ class ProgramBase { gl::UniformStates uniformStates; }; - void draw(gfx::Context& genericContext, + bool draw(gfx::Context& genericContext, gfx::RenderPass&, const gfx::DrawMode& drawMode, const gfx::DepthMode& depthMode, @@ -101,6 +107,10 @@ class ProgramBase { context.setCullFaceMode(cullFaceMode); const uint32_t key = gl::AttributeKey::compute(attributeBindings); + if (failedInstances.contains(key)) { + return false; + } + auto it = instances.find(key); if (it == instances.end()) { try { @@ -112,7 +122,8 @@ class ProgramBase { .first; } catch (const std::runtime_error& e) { Log::Error(Event::OpenGL, e.what()); - return; + failedInstances.insert(key); + return false; } } @@ -125,10 +136,12 @@ class ProgramBase { vertexArray.bind(context, indexBuffer, instance.attributeLocations.toBindingArray(attributeBindings)); context.draw(drawMode, indexOffset, indexLength); + return true; } private: std::map> instances; + std::set failedInstances; }; } // namespace gl